Skip to content

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.

Per resource in the plan, an applier returns exactly one verdict:

verdictmeans
APPLIEDthe provider was called and the resource converged — created, updated or unchanged
PRUNEDowned, no longer declared, deleted
NOT-ATTEMPTEDno 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.

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.

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 enumerated

Pick 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.

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.

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.