Organizational Policy
Lexicon lint answers “is this a coherent resource for its domain.” It does not express organizational policy — “no public load balancers in prod,” “every workload carries a cost-center tag,” “instance types from this allowlist.” Those are cross-cutting, org-specific, and often need to branch on environment.
chant does not add a separate policy language for this. Organizational policy is
project-authored post-synth checks,
run by the existing engine, with the current environment in context — the same
PostSynthCheck shape lexicons ship as domain rules, authored by your org.
Writing a policy
Section titled “Writing a policy”A policy reads the resolved resources and returns diagnostics. A diagnostic with
severity: "error" fails chant build — that is the gate. The check receives
ctx.env, so a rule can branch on the environment.
// Organizational policy — project-authored post-synth checks.//// These are the same `PostSynthCheck` shape lexicons ship as domain rules, but// authored by *your org* and registered via `lint.policies` in chant.config.ts.// They run during `chant build` over the resolved resources, with the current// `--env` in context, so a policy can branch on environment. A check returning// `severity: "error"` fails the build — the gate.import { getPrimaryOutput, type PostSynthCheck, type PostSynthContext, type PostSynthDiagnostic,} from "@intentius/chant/lint/post-synth";import { parseYAML } from "@intentius/chant/yaml";
interface Manifest { kind?: string; metadata?: { name?: string; labels?: Record<string, string> }; spec?: { tls?: unknown[] };}
/** Parse every lexicon's serialized output into flat Kubernetes manifests. */function manifests(ctx: PostSynthContext): Manifest[] { const out: Manifest[] = []; for (const [, output] of ctx.outputs) { for (const doc of getPrimaryOutput(output).split(/\n---\n/)) { const trimmed = doc.trim(); if (!trimmed) continue; try { const m = parseYAML(trimmed); if (m && typeof m === "object") out.push(m as Manifest); } catch { // skip unparseable documents } } } return out;}
const COST_CENTER = "acme.io/cost-center";
/** Every workload must be attributable to a cost center — in every environment. */export const costCenterRequired: PostSynthCheck = { id: "ORG-COST-CENTER", description: "every Deployment must carry an acme.io/cost-center label", check(ctx): PostSynthDiagnostic[] { const diags: PostSynthDiagnostic[] = []; for (const m of manifests(ctx)) { if (m.kind !== "Deployment") continue; if (!m.metadata?.labels?.[COST_CENTER]) { diags.push({ checkId: "ORG-COST-CENTER", severity: "error", message: `Deployment "${m.metadata?.name}" is missing the ${COST_CENTER} label`, entity: m.metadata?.name, }); } } return diags; },};
/** Production ingress must terminate TLS. Lower environments may skip it. */export const tlsRequiredInProd: PostSynthCheck = { id: "ORG-PROD-TLS", description: "in prod, every Ingress must terminate TLS", check(ctx): PostSynthDiagnostic[] { if (ctx.env !== "prod") return []; // the environment-aware branch const diags: PostSynthDiagnostic[] = []; for (const m of manifests(ctx)) { if (m.kind !== "Ingress") continue; const tls = m.spec?.tls; if (!Array.isArray(tls) || tls.length === 0) { diags.push({ checkId: "ORG-PROD-TLS", severity: "error", message: `Ingress "${m.metadata?.name}" must terminate TLS in prod`, entity: m.metadata?.name, }); } } return diags; },};ORG-COST-CENTER runs in every environment; ORG-PROD-TLS only fires when the
build is for prod.
Registering it
Section titled “Registering it”Add the policy file(s) to lint.policies in chant.config.ts. This is distinct
from lint.plugins (declarative lint rules) by authorship and phase — policies
reason about the resolved resources during build — but it is the same engine.
import type { ChantConfig } from "@intentius/chant";
// `lint.policies` registers project-authored organizational policy checks. They// run during `chant build` over the resolved resources, with `--env` in// context, and fail the build on violation. Distinct from `plugins` (declarative// lint rules) by authorship and phase — same engine.export default { lexicons: ["k8s"], ownership: { stack: "storefront" }, lint: { policies: ["policies/org.ts"] },} satisfies ChantConfig;Running it
Section titled “Running it”Policy runs during chant build. Pass the environment with --env (or set
ownership.env in the config):
chant build src # cost-center enforced; prod-only rules dormantchant build src --env dev # same — ORG-PROD-TLS skippedchant build src --env prod # ORG-PROD-TLS now enforcedFor the example above, the prod build fails:
error: [policy:ORG-PROD-TLS] [storefront] Ingress "storefront" must terminate TLS in prodPolicy evaluates the synthesized artifact, offline — it never touches the
cloud. Live checks are chant lifecycle, not this.
Why not a separate engine
Section titled “Why not a separate engine”chant already has the machinery: a post-synth phase that reasons about resolved resources, project-authored rule loading, and the Op gate model for the apply side. A separate policy language (Rego/OPA-style) would duplicate the lint engine and break “TypeScript is the one language.” Org policy is post-synth checks the org authors — nothing more.
Gating an apply
Section titled “Gating an apply”chant build already fails on a policy violation, so any pipeline that builds
before applying is gated. To gate an apply inside an Op,
add a policyGate() step before the apply phase:
import { Op, phase, build, policyGate, kubectlApply } from "@intentius/chant-lexicon-temporal";
export default Op({ name: "deploy", phases: [ phase("Build", [build(".")]), // Re-runs lint.policies over the resolved resources; a violation fails the // workflow here, so nothing is applied. phase("Policy", [policyGate({ env: "prod" })]), phase("Apply", [kubectlApply("k8s.yaml")]), ],});policyGate builds the project, runs lint.policies with the given env (or
ownership.env), and blocks on any violation — non-retryable and
single-attempt (a deterministic violation is not worth retrying). It is a plain
activity, so it gates under both the local executor and Temporal. A clean
evaluation passes through to the apply.
Signed override (deferred). Pausing for a human, signed, audited override to proceed despite a violation requires conditional gating in the generated workflow — it is tracked as a follow-on, not in this version. Today
policyGateblocks; loosen the policy or fix the violation to proceed.
Try it
Section titled “Try it”The full example is
lexicons/k8s/examples/org-policy
— it is exercised in CI (clean in dev, blocked in prod).