Skip to content

feat(k8s): the ConfigMap effect-receipt row, observed, guarded and never pruned (#2074) - #2210

Merged
lex00 merged 3 commits into
mainfrom
feat/2074-k8s-configmap-receipt
Sep 7, 2026
Merged

feat(k8s): the ConfigMap effect-receipt row, observed, guarded and never pruned (#2074)#2210
lex00 merged 3 commits into
mainfrom
feat/2074-k8s-configmap-receipt

Conversation

@lex00

@lex00 lex00 commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Item 1 of #2074. Item 2 stays open on the issue with a comment recording what a decision needs.

The problem

The only receipt materialization row was aws SsmParameter, so a cluster-only project could not use effect() receipts or WatchOp stale-receipt reporting at all, even though the effect-receipts concept page documents the feature without qualification.

What this lands

A k8s ConfigMap receipt row: EffectReceipt under the k8s lexicon with entityType K8s::Core::ConfigMap, carrying the core marker so lint, plan and apply recognize it lexicon-independently. The resolution split is unchanged from the aws row: static hashes at synthesis, references render the placeholder note.

The receipt ConfigMap is named chant-receipt.<stack>.<env>.<effect>, from the same ownership fields that stamp markers, with . as the separator because no segment may contain one, so two different stacks can never render the same name. It lives in k8s.receipts.namespace from chant.config.ts, defaulting to default, never derived from the stack or environment, because a guessed namespace is one chant would then have to create.

Three legs are covered against the fake cluster. The store server-side-applies the ConfigMap. describeResources reads it back where core's readReceiptValue looks, and planReceipts turns that into an effect row when stale and a noop when applied. The WatchOp staleness phase reports absent, differs or clean and issues no write.

Two properties the orchestrator asked for. The written ConfigMap carries the ownership marker labels plus an effect-receipt label, and pruneOrphans now reports it retained rather than deleting it, the same treatment a generated-once Secret gets, because a receipt is chant-owned and in no apply set by construction. The deep observation leg returns presence and uid with an empty property tree, so a receipt is never drift and never an unclaimed field; a test drives the real deep diff with the real k8s hooks over a stale receipt and asserts every bucket is empty.

The rendering channel

The aws row parks receipt rows in the CloudFormation template's Metadata, outside Resources. Kubernetes YAML has no channel outside the documents, and a document is precisely what an applier applies, so the k8s rows ride a trailing # chant:effect-receipts <json> comment on the manifest output. loadAll yields no object for it, so a receipt is structurally unreachable from applyManifest, while it still rides the one build string the observation legs receive. A test asserts a manifest plus the comment parses to exactly one document. The alternative, a sidecar file, was rejected because it would cost the observation leg its address source.

Docs

The concept page's aws sentence became a two-row table plus a kubectl get configmap walk-away example and a paragraph on the prune exclusion. The k8s serialization page gained the row; its twin was regenerated. COR022 and COR023 fixtures cover the k8s row.

The docs prose baseline entry for the effect-receipts page is ratcheted for the findings this branch removed. The gate reports three further below baseline that belong to the peer's #2198, #2203 and #2206.

Gates

Run by the orchestrator on the rebased head after the agent's own run.

npx vitest run packages/core lexicons/k8s   458 files passed; the two security-boundary suites timed out under the run and pass alone (6/6), the flake the brief documents
npx tsc --noEmit                       clean
npx tsx scripts/typecheck.ts           37 known failure(s), no regressions (37 baselined)
npx tsx scripts/check-lexicons.ts      every lexicon 23/23 tier-1, no untracked failures
chant dev check-lexicon lexicons/k8s   23/23 tier 1
node scripts/lint-docs-sentences.mjs   4106 gated finding(s), no regressions
node scripts/check-docs-diataxis.mjs   clean
npx vitest run examples/fold-differential.test.ts   112 passed

Read first

lexicons/k8s/src/serializer.ts for the trailing-comment channel, then receipt-store.ts for the address derivation, then the deep-observe test asserting the receipt is never drift.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FoXyD9UvKQ5ZdhiR9JT1yB

lex00 and others added 3 commits September 7, 2026 09:52
…ver pruned (#2074)

The only receipt materialization row was aws SSM (#1835), so a project whose
effects run against a cluster and never touch AWS had no receipt target:
`effect()` could not record that it fired and WatchOp's staleness phase had
nothing to read. This is the k8s row, built to the same shape.

- `EffectReceipt` under `lexicon: "k8s"`, entityType `K8s::Core::ConfigMap`,
  named `chant-receipt.<stack>.<env>.<effect>` from the ownership fields that
  stamp markers, env explicit (epic decision 4). The `.` separator is excluded
  from every segment, so two identities cannot render one name. The namespace
  is `k8s.receipts.namespace`, `default` when unset, never invented.
- The serializer renders the rows into a trailing `# chant:effect-receipts`
  comment rather than a document. Kubernetes YAML has no metadata channel
  outside the documents, and a document is exactly what an applier applies, so
  the comment is the k8s equivalent of the aws row's template `Metadata`:
  visible to the observation leg, structurally unreachable from `applyManifest`.
- `k8sReceiptStore` over the typed client implements core's receipt seam, and
  the op activities barrel exports `receiptRead`/`receiptWrite`/
  `receiptStaleness` bound to it, which is WatchOp's staleness leg.
- The observation reads the receipt back on both the thin and the deep paths.
  The thin read maps the stored value onto `attributes.value`, where
  `readReceiptValue` looks; the deep read reports presence with an EMPTY
  property tree, so a receipt is never property drift and never an unclaimed
  field (#2160) on any reading.
- `delete: "owned-only"` retains the receipt instead of pruning it. A receipt
  is chant-owned and is in no apply set by construction, which is the exact
  shape the sweep deletes, so it takes the generated-once Secret treatment.
- COR022 and COR023 fixtures over the ConfigMap row, and the concept page and
  the k8s serialization page name it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FoXyD9UvKQ5ZdhiR9JT1yB
The #2074 rewrite of the materialization-row paragraph into a table dropped
one em dash from the page, so its gated count is 29 rather than 30. Ratchet
the entry rather than leave the baseline one above what the page costs.

Only this page's entry moves. The suite is a further one below the baseline
after #2198, which belongs to that change to ratchet.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FoXyD9UvKQ5ZdhiR9JT1yB
…ts on its own branch

The brief forbids em dashes in prose written for this change; twenty-eight
came in across code comments, doc comments, test names and one line of the
effect-receipts concept page. Each becomes a comma, a period or a colon as the
sentence needs. No behaviour changes, and no pre-existing line is touched.

The receipt branch of pruneOrphans also moves below the generated-once branch.
It was inserted between the generated-once comment and the `if` that comment
describes, which read as though the comment belonged to the receipt check.

Removing the concept page's em dash drops its gated count again, so its
baseline entry follows it down from 29 to 28. Only that entry moves.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FoXyD9UvKQ5ZdhiR9JT1yB
@lex00
lex00 merged commit 7456390 into main Sep 7, 2026
6 checks passed
lex00 added a commit that referenced this pull request Sep 7, 2026
…2214)

Peer session dropped findings in #2198, #2203, #2206; this session dropped
findings in #2208, #2210, #2213. Each PR deferred the ratchet so the drops
would not conflict on the same baseline file, per PR #2174's precedent that
the baseline is ratcheted down rather than left slack.


Claude-Session: https://claude.ai/code/session_01FoXyD9UvKQ5ZdhiR9JT1yB

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant