Composition Rules
Composition rules check the Component graph. They are the long-term entropy guards described in Orchestration: “publishes-but-never-applies, an imageRef to a phase that does not exist, a mutating capability with no rollback and no opt-out, a gate in a local-executor component, a capability named after a component.”
COR/EVL rules check one file’s AST. COMP* rules see the whole discovered component graph: every *.component.ts file under the project, cross-referenced. They run in chant lint and block the build at error severity, like a COR* error.
How discovery works for these rules
Section titled “How discovery works for these rules”COMP* rules run against real, imported Component values (see Component Discovery). chant imports each *.component.ts file as well as parsing it, so a rule can answer questions a single-file AST check cannot: “does any component consume this one’s published artifact?”, “does this wiring reference point at a phase that actually exists?”, “do two components compose an identical shape?”
These checks report per component, so a diagnostic sits at the top of the file the component was declared in (1:1), the same convention COR009 uses for its own whole-file diagnostic. Only the file-level disable directive form is honored for COMP* rules:
// chant-disable COMP004 -- the release manager approves this rolloutchant-disable-line / chant-disable-next-line are not supported for COMP* rules, since a whole-component diagnostic has no single line to anchor to.
COMP001: Publishes But Never Applies
Section titled “COMP001: Publishes But Never Applies”Flags a component that publishes an image or artifact (publish-image, publish-artifact, load-image-on-host) that nothing consumes. A consumer is a later step in the same component (@Publish.digest) or another one (@<name>.publish.uri|digest|key).
| Severity | Error |
| Category | Correctness |
// ❌ Triggers COMP001 — nothing references "@orphan-lib.publish.uri"export const orphanLib: Component = { name: "orphan-lib", dependsOn: [], build: { kind: "jvm-build" }, deploy: [phase("Publish", [{ kind: "publish-artifact", from: "archive", to: "$env.s3" }])],};// ✅ Fixed — a consumer component references the publish outputexport const jarLib: Component = { name: "jar-lib", dependsOn: [], build: { kind: "jvm-build" }, deploy: [phase("Publish", [{ kind: "publish-artifact", from: "archive", to: "$env.s3" }])],};
export const emrJob: Component = { name: "emr-job", dependsOn: ["jar-lib"], deploy: [phase("Submit", [{ kind: "emr-start-job-run", jar: "@jar-lib.publish.uri" }])],};Fix: wire the publish output into a consuming apply/submit step, or add a downstream component that depends on this one and references @<name>.publish.*.
COMP002: Dangling Wiring Reference
Section titled “COMP002: Dangling Wiring Reference”Flags an @Phase.field or @<component>.publish.* reference that points at a phase or component that does not exist: @Phase.field naming a phase absent from the component’s own deploy, or @<component>.publish.* naming an undiscovered component, or one missing from dependsOn.
| Severity | Error |
| Category | Correctness |
// ❌ Triggers COMP002 — no "Build" phase exists (it's named "Publish"),// and "missing-lib" was never discovered.deploy: [ phase("Publish", [{ kind: "publish-image", from: "archive", to: "$env.registry" }]), phase("Apply", [ { kind: "cfn-deploy", template: "t.json", imageRef: "@Build.digest", inputs: { jar: "@missing-lib.publish.uri" } }, ]),],// ✅ Fixed — the reference matches the real phase name, and the referenced// component is discovered and listed in dependsOn.dependsOn: ["jar-lib"],deploy: [ phase("Publish", [{ kind: "publish-image", from: "archive", to: "$env.registry" }]), phase("Apply", [ { kind: "cfn-deploy", template: "t.json", imageRef: "@Publish.digest", inputs: { jar: "@jar-lib.publish.uri" } }, ]),],Fix: correct the phase name, add the missing component to dependsOn, or check for a typo in the component name.
COMP003: Mutating Capability With No Rollback
Section titled “COMP003: Mutating Capability With No Rollback”Flags a mutating capability step (s3-sync, cdn-invalidate, run-migration, emr-start-job-run, emr-submit-step, copy-to-host, remote-exec, or any non-starter kind) with no known native rollback and no explicit opt-out. cfn-deploy/ecs-update-service/lambda-deploy/code-deploy (native rollback) and the whole publish/build family (an already-built or published artifact is immutable and content-addressed, so there is nothing to roll back) are never flagged.
| Severity | Error |
| Category | Correctness |
// ❌ Triggers COMP003 — no rollback, no opt-outdeploy: [phase("Apply", [{ kind: "run-migration", tool: "flyway", target: "ecs-task:migrate" }])],// ✅ Fixed — explicit noRollback reasondeploy: [ phase("Apply", [ { kind: "run-migration", tool: "flyway", target: "ecs-task:migrate", noRollback: "forward-only migration; rolling back would drop backfilled columns", }, ]),],Fix: add a noRollback: "<reason>" property directly on the step, add a component-level rollback phase, or add a sibling rollback-previous/snapshot-before step in the same phase.
COMP004: Gate Ends The Run Pending Approval
Section titled “COMP004: Gate Ends The Run Pending Approval”The rule id stays COMP004, and so does its file name
(comp004-gate-needs-durable-runtime.ts). Renaming either would touch every
chant-disable comment and every import, so the heading above is the rule’s
current description and the file name is historical.
Flags a gate step anywhere in a component’s composition. A gate ends the run pending chant approve <component> <gate>: the driver decides it against the gate ledger, and one nobody has approved stops the run there, with no later phase running. Which runtime hosts the run is chosen per run via chant run --on <lexicon>, never declared in the component itself.
| Severity | Error |
| Category | Correctness |
// ❌ Triggers COMP004 — no acknowledgment that this component stops for a persondeploy: [ phase("Node 1", [ gate("approve-node-1", { description: "Confirm the seed node is healthy" }), { kind: "cfn-deploy", template: "t.json" }, ]),],// ✅ Fixed — the gate is acknowledged with a file-level disable directive// chant-disable COMP004 -- the release manager approves this rolloutdeploy: [ phase("Node 1", [ gate("approve-node-1", { description: "Confirm the seed node is healthy" }), { kind: "cfn-deploy", template: "t.json" }, ]),],Fix: if the component is meant to stop for a person, suppress with // chant-disable COMP004 -- <reason> to document who approves it and why. The declaration names no runtime and runs wherever it is invoked, so this rule flags every gate that has no suppression comment.
COMP005: Capability Kind Is A Noun
Section titled “COMP005: Capability Kind Is A Noun”Flags a step kind named after a component rather than an operation, the “capability-per-component” failure mode (epic #551, “renamed sprawl”). A capability kind must be a verb (cfn-deploy, publish-image, wait-steady-state); a kind equal to or built from a discovered component’s own name is the sprawl the capability model is meant to remove.
| Severity | Error |
| Category | Style |
// ❌ Triggers COMP005 — "deploy-search-service" is named after this componentexport const searchService: Component = { name: "search-service", dependsOn: [], deploy: [phase("Apply", [{ kind: "deploy-search-service", imageRef: "@Publish.digest" }])],};// ✅ Fixed — a verb-named capabilityexport const searchService: Component = { name: "search-service", dependsOn: [], deploy: [ phase("Apply", [ { kind: "cfn-deploy", template: "search.template.json", imageRef: "@Publish.digest" }, { kind: "ecs-update-service", cluster: "$env.cluster", service: "search" }, ]), ],};Fix: name the capability after the operation it performs. See Capabilities.
COMP006: Shell Needs A Reason
Section titled “COMP006: Shell Needs A Reason”Flags a raw shell step (the deliberate escape hatch) with a missing, empty or blank reason.
| Severity | Error |
| Category | Correctness |
// ❌ Triggers COMP006 — no reason declareddeploy: [phase("Apply", [{ kind: "shell", cmd: "./legacy-deploy.sh" }])],// ✅ Fixeddeploy: [ phase("Apply", [ { kind: "shell", cmd: "./legacy-deploy.sh", reason: "no capability wraps this vendor's proprietary CLI yet" }, ]),],Fix: add a non-empty reason explaining why no capability covers this case. ShellInput.reason is already required on the typed createShellCapability input; this rule applies the same requirement to hand-authored and JSON-only components.
COMP007: Composition Sprawl (hint)
Section titled “COMP007: Composition Sprawl (hint)”Flags two or more components whose deploy composition is structurally identical — same phases, same parallel/nesting shape, same step kinds in the same order (ignoring name, dependsOn, and literal wiring values/params). This is a hint, reported as a warning, to extract a preset.
| Severity | Warning |
| Category | Style |
// ⚠ Triggers COMP007 — identical shape to inventory-tableexport const ordersTable: Component = { name: "orders-table", dependsOn: [], deploy: [ phase("Apply", [{ kind: "cfn-deploy", template: "orders-table.template.json" }]), phase("Verify", [{ kind: "wait-for-stack", stack: "orders-table" }]), ],};
export const inventoryTable: Component = { name: "inventory-table", dependsOn: [], deploy: [ phase("Apply", [{ kind: "cfn-deploy", template: "inventory-table.template.json" }]), phase("Verify", [{ kind: "wait-for-stack", stack: "inventory-table" }]), ],};Fix: extract a named preset (e.g. DynamoTableComponent({ name, template })) that both components call, the same way ApplyOp composites the common shape today. Structurally distinct components (a different archetype, a different apply family, fan-out vs. no fan-out) are never flagged. The four pilots were chosen to be structurally distinct and pass this rule: ALB/ECS, DynamoDB, Neo4j fan-out and an image-processor Lambda.
Auto-fix
Section titled “Auto-fix”No COMP* rule has an automatic fix yet (see #562). COMP007 is the likeliest first one. A fix could scaffold the shared preset call, but the author still picks the preset’s name and parameter shape.
See Also
Section titled “See Also”- Lint Rules Overview
- Component Contract — the
Component/Phase/Step/Gateshapes these rules check - Composition & Wiring — the wiring reference forms COMP002 validates
- Capabilities — the verb/noun and escape-hatch discipline COMP005/COMP006 enforce
- Orchestration — “what chant adds” over a plain JSON contract