Skip to content

feat: the carve estate reads chant's state manifests — the strangler fig survives a restart (#230 M3, chant#998) - #320

Merged
lex00 merged 1 commit into
mainfrom
carve-m3-manifest-state
Aug 30, 2026
Merged

feat: the carve estate reads chant's state manifests — the strangler fig survives a restart (#230 M3, chant#998)#320
lex00 merged 1 commit into
mainfrom
carve-m3-manifest-state

Conversation

@lex00

@lex00 lex00 commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Closes nothing on its own; this is M3 of #230 — TF as an estate member, the strangler-fig progress — built on the #254 walkthrough and the #283 estate frame.

What the manifest gives

chant 0.52.2 (chant#998, chant PR #1575) persists a carve state manifest beside the emitted source: <address>.carve.json in carve emit --output, with carve bridge and carve apply adding their own sections to the same file. It is the no-database progression source this milestone was gated on, and it is already on disk — behold just had to read it.

The manifest's three sections are cumulative, so their presence is the stage:

sections stage what the picture says
emit emitted stays in its band, repainted accent, card reads emitted — not bridged
emit + bridge bridged still banded, still Terraform's, card reads bridged — apply is yours
+ apply applied leaves the bands for the chant member box, keeping its Terraform address, card reads carved → chant

Only apply is graduation. A card that read "carved" the moment source was emitted would claim an ownership transfer that has not happened, which is the one class of claim behold's views exist to avoid. Both partial stages share the in-flight tone and are told apart by their word, which is on the card — tone never carries a state on its own here, and a second tone would have to borrow a token that already means something else in this picture.

Where the progress lands:

  • the chant panel's own title — carved so far — 1 of 12 carved, where the estate frame already summarizes that member
  • the statusbar note, saying how many manifests the claim is drawn from
  • the scope panel's project section, one row per carve with its stage
  • /api/project's carve.state, for agents: {manifests, progress, states[], apply: {human, note}}

A resource that graduated in a previous session opens in the chant box; a partly-carved one opens back in its band saying how far it got. Nothing is inferred from what happened in the tab.

Real estates, not just the demo. A bare behold carve out/report.json with manifests beside it gets the same reading — the graduated pile becomes its own band above the ranking, since there is no chant member box to move a card into. An ordinary behold serve <project> whose directory carries a carveout surfaces carve.state on /api/project with no report, no stepper and no demo scaffolding, and grows nothing at all when the project was never carved. It is deliberately not joined onto that project's graph: the manifest keys on a Terraform address and those nodes key on chant entity names, and matching the two is the dedupe question #230's own tier grading called unsolved.

The write boundary, restated

carve apply graduates ownership — it resolves the marker that makes chant the owner of a live resource. There is no /api/carve/apply and no apply button. behold renders what the manifest records about an apply and echoes the retypeable command (no --select; the manifest in the output dir already names the target), which is exactly the posture the Handoff step already takes with terraform state rm.

runStep would take the endpoint in four lines and every guard already written would still hold — which is why the refusal is written down where the route and the runner would go rather than left to be inferred from their absence: APPLY_IS_HUMAN in src/carve-manifest.ts, and pointers to it in src/carve-actions.ts, carveRoutes in src/server.ts, and the Handoff renderer in web/carve-steps.js. The boundary is not that behold can't; it is that behold won't decide when an estate changes hands.

Everything else is unchanged: emit and bridge still run only inside a behold demo carve copy, still write only into app/carveout/, still never pass --apply-rewrites. behold reads manifests and never writes one — the manifest is chant's file.

Fixture provenance

src/__fixtures__/carve-manifests/ holds three REAL manifests. This repo's own example-carve was copied to a scratch dir (nothing in the checkout was touched), its app/ installed with @intentius/chant@0.52.2 + @intentius/chant-lexicon-aws@0.52.2 and @cdktf/hcl2json beside it, then the carve verbs run against legacy-tf/ with --output app/carveout:

  • aws_iam_role.apicarve emitemitted (and it carries chant's emit.params, the deferred name ← random_pet.suffix input)
  • aws_cloudwatch_log_group.worker — emit + carve bridgebridged
  • aws_s3_bucket.assets — emit + bridge + carve apply --env prod --stack assets --write --write-sourceapplied

Committed as chant wrote them, with one substitution: the scratch directory's absolute prefix was rewritten to /tmp/carve-fixture, because the real one carried a session id and a home path and neither is part of the shape under test. The applied one exists only because carve apply was run by hand in a terminal — which is the only way that stage is ever reached.

Verification

  • npm run tsc, npm run build, npm run smoke:ui — clean
  • npm test — 1175 passing (66 files), up from 1118 on main
  • Booted behold demo carve, ran Emit through the UI's own endpoint: the bucket repainted in-band as emitted — not bridged, statusbar read Carve state (from 1 manifest): 0 of 12 carved, 1 emitted — apply is yours. Ran chant carve apply by hand in the copy, restarted the server, and the card came back inside app — chant with carved so far — 1 of 12 carved. POST /api/carve/apply → 404.

What M3 still lacks upstream

  • The manifest records the carve, not the estate. There is no way to ask chant "which addresses in this directory have manifests anywhere" — behold discovers them by walking, bounded at two levels under a project root and flat under a named output dir. A chant carve status --json over a tree would replace the walk with a contract.
  • emit.files and bridge.written are absolute paths chant resolved at run time. They read fine in the session that wrote them and are meaningless if the tree moves; behold trims them for display, which is presentation over a field that would be better relative to the manifest.
  • Nothing joins a carved Terraform address to the chant entity it became. TfNode.identity is the only handle and it is only populated for literal names, so a real project's graph still cannot colour a carved resource — the dedupe ask from design: a Terraform carve lens — chant carve advise as a scored overlay, TF as an estate member #230's tier grading is still open, and this PR deliberately stops short of guessing at it.
  • carve apply is plan-only and BYOL by design, so applied means "the marker is resolved and recorded", not "the cloud resource is tagged". The observe-side confirmation still depends on chant#1647's identity read path and the live tier.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Rfs1mLVWmwginHnDTg75sG

…fig survives a restart (#230 M3, chant#998)

The carve view's progress used to be whatever this tab remembered. chant
0.52.2 persists `<address>.carve.json` beside the emitted source — `carve
emit` writes it, `carve bridge` and `carve apply` add their sections — so the
progression already lives on disk, in chant's own file. behold now reads it.

src/carve-manifest.ts is the reader: a v1 parse that refuses a major it does
not speak, a bounded discovery walk, and the split that decides which side of
the estate each ranked address draws on. Only `apply` is graduation — an
`applied` address leaves the Terraform bands for the chant member box keeping
its Terraform address (the morph's identity continuity), while `emitted` and
`bridged` stay banded, repainted in the in-flight `accent` tone with the stage
in words on the card. A card that read "carved" the moment source was emitted
would claim an ownership transfer that has not happened.

The chant panel's title carries the count (`carved so far — 1 of 12 carved`),
the statusbar note says how many manifests the claim came from, and the scope
panel lists each carve with its stage. A bare `behold carve report.json` with
manifests beside it gets the same reading, with the graduated pile as its own
band above the ranking; an ordinary `behold serve` whose project directory
carries a carveout surfaces the state on `/api/project` with no report, no
stepper and no demo scaffolding. Deliberately not joined onto that project's
graph: the manifest keys on a Terraform address and those nodes key on chant
entity names, which is #230's unsolved dedupe question.

`carve apply` stays human. It graduates ownership, so there is no
`/api/carve/apply` and no apply button — behold renders what the manifest
records and echoes the retypeable command (no `--select`; the manifest names
the target), exactly the posture the Handoff step takes with `terraform state
rm`. The refusal is written down where the route and the runner would go, not
left to be inferred from their absence.

Fixtures are real: example-carve copied to a scratch dir, chant 0.52.2 pinned
in its app/, and the three carve verbs run against legacy-tf — one manifest per
stage, committed as chant wrote them, with the scratch path prefix normalized.
The applied one was produced by running `chant carve apply` by hand, which is
the only way that stage is ever reached.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rfs1mLVWmwginHnDTg75sG
@lex00
lex00 force-pushed the carve-m3-manifest-state branch from 370799b to f74dbf0 Compare August 30, 2026 04:38
@lex00
lex00 merged commit ff349f7 into main Aug 30, 2026
3 checks passed
lex00 added a commit that referenced this pull request Aug 30, 2026
Two textual conflicts, both additive:

- AGENTS.md — #323 documented the ops lens's per-rule cards on the same
  sentence this branch extended with `meta.operator`. Both statements kept, the
  rule table first (it is about the lens's own cards) and the operator strip
  after (it is about what rides beside them).
- smoke/stub.mjs — #230 M3 and this branch each added state to `startStub`'s
  preamble. Both kept.

Nothing semantic collided: #323 reads `args.rules` off the same op.json this
branch reads `searchAttributes.Converge` off, and the two passes are
independent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rfs1mLVWmwginHnDTg75sG
lex00 added a commit that referenced this pull request Aug 30, 2026
… first cut, the strangler fig (#325)

Since 0.11.0: the chant 0.52 line across root and every example (#316,
chant 0.52.2's yaml fix in the locks), warm estate reads from a
per-member IR cache (#312#307), the ops lens (#315) with its run
playhead (#319) and ConvergeOp rule table (#323), the operator strip and
gate approval card with approve's honest record-a-fact semantics (#324),
disruption classes in the reconcile dial (#314), the helm drift axis
(#318), manifest-driven carve state that survives a restart (#320,
#230 M3), the observe {stacks} refusal and the pinned contrast floor
(#313), and the CDK carve report compat pin (#317). pinhole 0.3.6 rides
in the lock — the nested chant 0.44.14 is gone.


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

Co-authored-by: Claude Fable 5 <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