Post-Synth Check Guide
Post-synth checks validate the serialized output after the build pipeline completes. They catch issues in the generated templates that pre-synth rules (which operate on TypeScript AST) cannot detect.
Anatomy of a PostSynthCheck
Section titled “Anatomy of a PostSynthCheck”import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic,} from "@intentius/chant/lint/post-synth";
export const myCheck: PostSynthCheck = { id: "MYD010", description: "Human-readable description of what this check validates",
check(ctx: PostSynthContext): PostSynthDiagnostic[] { const diagnostics: PostSynthDiagnostic[] = [];
for (const [_lexicon, output] of ctx.outputs) { // Parse the output (JSON, YAML, etc.) // Iterate resources // Push diagnostics for violations }
return diagnostics; },};PostSynthContext
Section titled “PostSynthContext”The ctx parameter provides:
ctx.outputs—Map<string, string | SerializerResult>of serialized outputs per lexiconctx.entities—Map<string, Declarable>of all declared entitiesctx.buildResult— Full build result including warnings and errors
PostSynthDiagnostic
Section titled “PostSynthDiagnostic”Each diagnostic requires:
{ checkId: "MYD010", // Matches the check ID severity: "warning", // "warning" | "error" message: "Human-readable message with resource name", entity: "resourceName", // Optional: the resource that triggered it lexicon: "my-lexicon", // Optional: lexicon identifier}Category Taxonomy
Section titled “Category Taxonomy”Organize checks into categories for clarity:
Security
Section titled “Security”Encryption, TLS, HTTPS, access control, identity configuration.
// Example: Missing encryptionif (!props.encryption) { diagnostics.push({ checkId: "MYD015", severity: "warning", message: `Resource "${name}" has no encryption — enable encryption to protect data at rest`, });}Correctness
Section titled “Correctness”Required fields, valid values, correct dependencies.
// Example: Missing required fieldif (!resource.apiVersion) { diagnostics.push({ checkId: "MYD011", severity: "error", message: `Resource "${name}" is missing apiVersion`, });}Best Practices
Section titled “Best Practices”Naming conventions, tagging/labeling, resource configuration.
// Example: Redundant dependencyif (propertyRefs.has(depName)) { diagnostics.push({ checkId: "MYD010", severity: "warning", message: `Resource "${name}" has redundant dependency on "${depName}"`, });}Deprecation
Section titled “Deprecation”Outdated API versions, legacy features.
// Example: Old API versionif (apiDate < deprecationThreshold) { diagnostics.push({ checkId: "MYD012", severity: "warning", message: `Resource "${name}" uses outdated apiVersion`, });}Testing Pattern
Section titled “Testing Pattern”Every check should have both a positive (flagged) and negative (clean) test:
import { describe, test, expect } from "bun:test";import { createPostSynthContext } from "@intentius/chant-test-utils";import { myCheck } from "./my-check";
describe("MYD010: My Check", () => { test("flags when condition is violated", () => { const ctx = createPostSynthContext({ "my-lexicon": { /* template with violation */ }, }); const diags = myCheck.check(ctx); expect(diags).toHaveLength(1); expect(diags[0].checkId).toBe("MYD010"); });
test("passes when condition is satisfied", () => { const ctx = createPostSynthContext({ "my-lexicon": { /* template without violation */ }, }); const diags = myCheck.check(ctx); expect(diags).toHaveLength(0); });});Universal Check Patterns
Section titled “Universal Check Patterns”These patterns apply to most IaC formats:
| Pattern | Description |
|---|---|
| Missing encryption | Data at rest should be encrypted |
| Overly permissive access | Wildcard rules, public access, admin credentials |
| Deprecated versions | Old API versions, deprecated features |
| Missing identity/auth | No managed identity, no RBAC, shared credentials |
| Public access enabled | Resources exposed to the internet unintentionally |
| Missing diagnostics | No logging, monitoring, or audit trail |
| Missing TLS/HTTPS | Unencrypted transport |
| Missing network isolation | No NSG, no network policy, no firewall rules |
Wiring Checks into Plugin
Section titled “Wiring Checks into Plugin”Return checks from your plugin’s postSynthChecks() method:
postSynthChecks(): PostSynthCheck[] { const { myCheck010 } = require("./lint/post-synth/check010"); const { myCheck011 } = require("./lint/post-synth/check011"); return [myCheck010, myCheck011];}Target Count
Section titled “Target Count”A mature lexicon should have at least 15 post-synth checks covering all four categories (security, correctness, best practices, deprecation). The K8s and Azure lexicons each have 20 checks.