Skip to content

Drift Record

llms.txt lists every page for an agent

A drift record says what one read found for one resource instance (#4451). Each engine’s reads become the same records. On a chant member that means the live diff, deep observe and component status. A stock terraform root gives its state and plan. A choudoufu root gives live-plan, live-ls and its plan. A renderer then colours a card from one pair, (status, read), with no per-engine branches.

The module is @intentius/chant/drift-record, the terraform family’s adapters are @intentius/chant-lexicon-terraform/drift-record, and the Pulumi one is @intentius/chant-lexicon-pulumi/drift-record. Both bundle with the change-set code, under the same guard, described below. The schema ships at src/workspace/drift.schema.json, $id https://intentius.io/chant/schemas/workspace/drift/v1/drift.schema.json. The document is versioned by its drift: "v1" field rather than contract. Within the version, fields are only added.

Change sets keep holding planned changes only. A drift record authorizes nothing and is bound to no gate.

FieldHolds
memberthe workspace member, or a choudoufu estate
engineterraform, tofu, choudoufu, chant, warden, sql, pulumi or plunge
addressthe engine’s own address, at instance level: aws_eip.pool[0], a Pulumi URN, a chant entity name
resourceTypethe resource type, when the read reports one
nodethe graph node, by the change set’s rule (graphNodeFor): an instance of a count or for_each block is on <blockNode>[0] or <blockNode>["a"], an instance under an expanded module call is on <root>/module.app[0]/<block>[1], the module key on the call’s segment (#4486). null when the graph holds none. A Pulumi URN is on <member>/<qualified type>::<name>, the URN rule of @intentius/chant/pulumi-urn, and a provider or the root stack on none (#4453), and a plunge URN, also a Pulumi URN, by the same rule
instanceOfon a record whose node is an instance node, the block node
observedAtRFC 3339, per record
readstate, plan, refresh, live-read or ownership
fellBackFromthe read that was asked for and failed, what RootRead.depth holds when fellBackTo is set
statussee Statuses
unobservedon not-observed only: { reason, detail, queried }
attributeson drifted only: { path, before, after, sensitive }, sorted by path
attributesUnobservedon in-sync or drifted: presence is known and the attributes are not, as after a failed deep read
ownership{ verdict, by, markerOnObject, ownedBy, adopt }, with verdict one of owned, foreign, adoptable, unknown and by one of marker, derived, record, cache, state, label
evidence{ tool, version, command, engineCode }. engineCode keeps the engine’s own token, such as UNOWNED, NEEDS_DISCOVERY or ABSENT
StatusMeans
in-syncpresent, and nothing the read compares differs
driftedpresent, and something differs
pendingdeclared and confirmed absent, never deployed
missingrecorded or deployed, and now gone
unclaimedsomething live sits at a declared identity without this member’s marker
undeclaredlive or recorded, and not declared
runtimelive, undeclared and expected, such as a controller’s child
recordedin the engine’s state
not-observedthe read could not say; unobserved.reason says why

driftRecord builds every record and refuses one that breaks a rule, with a DriftRecordError naming the member, the address, the read, the status and the rule:

  • A state read is recorded, pending or not-observed, never in-sync or drifted. State says what the last apply recorded, not what the account holds.
  • not-observed always carries a reason from the observation contract, and is never absence. not-reached is the reason a stream gives an address it expected and never heard about.
  • An attribute the planner marked sensitive keeps its path and drops both values. A second pass does the same for any path with a key segment isSensitiveKey names, such as db_password or tags["api_token"].

In an attribute, before is what was expected and after is what the read found. The expected value comes from the record, a snapshot or the declaration. Values are optional; a card needs only the paths.

FieldHolds
$schemathe schema’s $id
drift"v1"
chantthe chant that wrote it, when known
recordssorted by member, address and read, one per (member, address, read); a later record replaces an earlier one
countsrecords per status, every status present

driftDocumentDigest hashes the records without observedAt and evidence. It is for a WatchOp that keeps its document as evidence in its own run ledger; no gate binds it.

mapDriftToGraph fills node and instanceOf against a member’s graph. rollupDriftByBlock groups records by block node and rolls each block up with core’s rollupInstances over DRIFT_INSTANCE_ROLLUP: not-observed, drifted, missing, unclaimed, undeclared, pending, in-sync, recorded, runtime, first match wins.

SourceFunctionReadStatus
chant lifecycle diff --liveliveDiffDriftRecordslive-readunchanged and newly observed: in-sync. Drifted since snapshot: drifted. Missing: pending. Disappeared: missing. Orphan: undeclared. Runtime children: runtime. Unobserved: not-observed
deep observedeepDiffDriftRecords, mergeDeepDriftlive-reada changed or absent path: drifted with paths. Laid over the thin read, a deep hole becomes attributesUnobserved and leaves the status alone
chant components status --livecomponentStatusDriftRecordslive-readreconciled: in-sync. Drifted: drifted. Stale: missing. Unrecorded: drifted when live, pending when not. Unknown: not-observed
terraform show -json over stateterraformStateDriftRecordsstatein state: recorded. Declared and not in state: pending
terraform show -json of a planterraformPlanDriftRecordsplana resource_drift update: drifted. A drift delete: missing. A create with no prior row: pending. A delete with no drift: undeclared. Otherwise in-sync
choudoufu live-plan -jsonchoudoufuLivePlanDriftRecordsownershipbound: in-sync, owned by its source. Unowned and adoptable: unclaimed. ABSENT: pending. Any other omission: not-observed, VERIFIED_NOT_IMPORTABLE as read-failed and OUT_OF_SCOPE as filtered. Foreign: undeclared. Controller held: runtime
choudoufu live-ls -jsonchoudoufuLiveLsDriftRecords, choudoufuLiveDriftRecordsownershipdeclared: false: undeclared. A gap live-plan did not answer: not-observed
live-plan with a planchoudoufuLivePlusPlanDriftRecordsplanbound with a planned update or replace: drifted with paths, live-plan’s ownership kept
pulumi refresh --preview-only --json --target <urn>pulumiRefreshDriftRecords, one read per URN; pulumiRefreshDocumentRecords for an untargeted refreshrefreshsame: in-sync. update: drifted, a path per detailedDiff key. delete: missing. A refresh step: missing with no new state, drifted when its outputs differ, else in-sync. No step for the target: not-observed, not-reached. A secret-wrapped value is sensitive
plunge drift --resource <urn> --stream, for a Pulumi member on plungeplungeDriftStreamRecordsrefreshplunge’s own drift-record v1 stream, read with decodeDriftLines and foldDriftLines; the record for the URN keeps plunge’s status, and node comes from the URN rule. A stream with nothing for the URN: not-observed, not-reached. One that does not decode: not-observed, read-failed

A member runs on plunge when its config says engine: "plunge" or its Pulumi.yaml names a backend URL starting with plunge (pulumiMemberEngine); chant then runs plunge drift and never pulumi for its drift. refreshPulumiMember in @intentius/chant-lexicon-pulumi/run runs the refresh once per resource of the member’s checked-in stack export, so one failing resource holds back only its own record. The Pulumi shapes were written from Pulumi’s documented --json output and still want checking against a real run (#4453).

An UNOWNED omission with no unowned[] row is not-observed, read-failed, with engineCode: "UNOWNED": nothing in the document says what holds the identity.

A read can stream its records as NDJSON, one object per line, in the layout choudoufu’s live-plan -resource writes. The type field on each line says what it holds.

typeHolds
drift-startdrift, member, engine, read, and expected, the addresses it will report, each a bare address or { address, node, instanceOf, resourceType }. Optionally resource, fellBackFrom, at and evidence
drift-recorda record’s own fields. A later line for the same (member, address, read) replaces the earlier one
drift-settledrift, member, engine, read, and counts per status (a status with none may be left out). Optionally reads, the engine’s own account of what the read cost, carried and never interpreted, at, and notReached

At settle, every expected address no record answered, under any read, becomes not-observed with reason not-reached. Between start and settle a renderer shows an unanswered card as reading, which is not a status. encodeDriftLine, decodeDriftLine, decodeDriftLines, driftStart, driftSettle and foldDriftLines read and write the stream.

chant workspace drift --member <name> [--read <state|plan|refresh|live-read|ownership>] [--resource <addr>] [--stack <name>] [--env <env>] [--components] [--stream] [-o <file>]

One member’s drift, read by the engine that owns it (behold#995). It is a read-contract command: it changes nothing in the workspace and starts no apply.

Member kind--readWhat runs
chantlive-read, the only onethe member’s own chant lifecycle diff <env> --live --json, under its toolchain, through liveDiffDriftRecords (with mergeDeepDrift when a lexicon has a deep reader). --components adds chant components status <env> --live --json through componentStatusDriftRecords. Needs --env
terraformstate (default) or planchant lifecycle drift in the member’s reader project (the one workspace graph writes): show -json over state, or plan -input=false -lock=false -out=<tmp> and show -json. A plan that fails falls back to state, with fellBackFrom: "plan"
choudoufuownership (default) or planthe same, through choudoufu live-plan -json and live-ls -json, or for plan live-plan with a plan beside it (choudoufuLivePlusPlanDriftRecords). A plan that fails falls back to ownership, with fellBackFrom: "live+plan". --resource <addr> runs choudoufu live-plan -resource=<addr> and passes its stream through, decoded and re-encoded with this codec
pulumirefresh, the only onechant lifecycle drift in the member’s reader project: the pulumi lexicon’s readDrift (#4455) runs pulumi refresh --preview-only --json --target <urn> once per resource in the checked-in stack-export.json (pulumiRefreshDriftRecords), or one untargeted refresh with no export. A member on plunge runs plunge drift --resource <urn> --stream per resource instead (plungeDriftStreamRecords). --resource <urn> reads that URN alone, on either engine

--stack <name> reads that stack’s export for a pulumi member, on either engine. A member of several stacks holds one export per stack in the index store (refs/chant/snapshots, written after each apply through chant), and without --stack the read takes the export the store holds, which is the right one only when the member has a single stack. With --stack the read uses that stack’s export and passes --stack <name> to every refresh or plunge drift, over the member’s declared pulumi.member.stack. When the store holds no export for that stack the document carries error: { code: "no-drift-read" } and a message naming the stack, and nothing runs: the read never falls back to another stack’s export or to the checked-in stack-export.json. On a member of any other kind --stack answers read-unsupported. Without the flag nothing changes.

Any other kind answers the error no-drift-read. Nothing is written in a member: init is never run, so a root that was never initialised reads not-observed with the init line in its detail.

Every record’s node is placed on the member’s source graph by graphNodeFor, the change set’s rule, from the graph chant workspace graph --member <name> reads (served from the per-member cache when the source has not moved). For a terraform or choudoufu member that is <member>/<root>/<block key>, the root being named after the member in its reader project, and an instance of a count or for_each block is on its instance node. A Pulumi or plunge record is placed by the URN rule (driftPlanner), the node chant workspace graph gives the resource in the member’s export. choudoufu names its own estate in member; the command puts the workspace member’s name there.

Without --stream it prints one drift document with the read contract’s head beside it: contract: 1, chant and member. When the read could not run, the document has no records and error: { code, message }, and the command exits 1. The codes are the declaration’s, and member-required, no-drift-read, read-unsupported, resource-unsupported, env-required and command-failed.

With --stream it prints the NDJSON stream: a drift-start, the records and a drift-settle for each read. For a chant member chant writes the start and settle itself around the diff’s records. A read that could not run prints nothing on stdout and says why on stderr, with exit 1.

The member half is chant lifecycle drift [--member <name>] [--read <read>] [--resource <addr>] [--stack <name>], which asks each lexicon’s readDrift (LexiconPlugin.readDrift) and prints its stream. A refusal is one stdout line, {"refused": {code, message}}, and exit 3.

@intentius/chant/drift-record and @intentius/chant-lexicon-terraform/drift-record import only these modules:

  • the plan hashing code
  • the observation contract’s reason list
  • the change-set graph’s node rule
  • the instance rollup
  • the sensitive-key list
  • the plan projection

change-set-bundle.test.ts lists every module in that bundle. It fails if TypeScript or esbuild joins it. It also fails on zod and on chant’s own fold, lint and codegen code.

chant