Implementing Apply
An applier writes declared source to a provider. This page is the write-side peer of Implementing Observation, and it exists for the same reason: a result that cannot say what it did not do is a result a caller cannot trust.
The tri-state
Section titled “The tri-state”Per resource in the plan, an applier returns exactly one verdict:
| verdict | means |
|---|---|
| APPLIED | the provider was called and the resource converged — created, updated or unchanged |
| PRUNED | owned, no longer declared, deleted |
| NOT-ATTEMPTED | no provider call was made, carrying a total reason |
The three are disjoint and total. Every resource handed to the applier appears in exactly one of them. A resource that appears in none was silently dropped, which is the failure this contract exists to prevent.
Why NOT-ATTEMPTED
Section titled “Why NOT-ATTEMPTED”The read path had two ways to return nothing for a declared entity and no way to tell them apart — the provider said it is absent, versus the lexicon never looked — and the second was classifying as create. That is what observation.ts fixed.
The write path had the identical hole:
const mapper = MAPPERS[r.kind];if (!mapper) { console.log(`skip: no mapper for kind ${r.kind}`); continue; // never appears in `applied`, or anywhere}A caller receiving { applied: [...] } could not distinguish a complete apply from one that dropped half the manifest, and ApplyOp had nothing to gate on. A console.log on stdout is not a signal in a result, the same way a warn on stderr was not a signal in a change set.
The reasons are total
Section titled “The reasons are total”type NotAttemptedReason = | "unsupported-kind" // no mapper/writer for this type | "no-credentials" // no usable authorization for the target | "no-binding" // no concrete target resolved | "dependency-failed" // an upstream in the same run failed | "filtered" // withheld by a caller-requested scope | "not-prunable"; // an owned orphan of a kind that cannot be enumeratedPick one. Consumers switch exhaustively, so a free-form string is not an option — which is deliberate, because a free-form string is what a console.log already was.
not-prunable is the prune-side shape and worth calling out: it is the difference between “there was nothing to prune” and “I could not look for anything to prune here.” An applier that cannot enumerate a kind cannot know whether an owned orphan of it exists.
The envelope
Section titled “The envelope”import { applyResult } from "@intentius/chant";
return applyResult(applied, pruned, notAttempted);Versioned and self-discriminating (apply: "v1"), so an un-migrated applier may keep returning its own shape — that normalizes to “everything I was handed, I attempted”, which is exactly the claim it is implicitly making.
Reporting NOT-ATTEMPTED requires the envelope. You cannot claim the guarantee without adopting the shape that can express its absence.
Ownership
Section titled “Ownership”Prune deletes only what chant owns. The marker channel differs per provider — tags on Azure, labels on GCP, metadata on Fly, the field manager on Kubernetes — but the rule does not: read the marker before issuing a delete, and never delete a resource that does not carry it.
Assertion 4 is what holds you to it. A comment saying prune is owned-only is not enforcement, and every applier that got this wrong had the comment.
See also
Section titled “See also”- Apply Conformance Suite — the suite that proves an applier honors this contract