Skip to content

Look at a Terraform carve-out

chant carve advise reads a Terraform estate and ranks each resource by peelability: how cleanly it could be carved into native chant source. behold carve draws that ranking.

Terminal window
chant carve advise --from ./terraform --report carve.json
npx behold carve carve.json

Read-only twice over. chant’s advisor emits no chant source, patches no Terraform and makes no cloud calls; behold only draws what the report says. behold parses no HCL and needs no Terraform tooling installed: the report is the whole contract between the two.

One card per ranked resource or module, in three panels:

  • carve now (green), score 80-100. A clean leaf with a 1:1 native mapping and little or no boundary work.
  • boundary work (amber), score 50-79. Carvable, but survivors depend on it, so carving means patching the surviving Terraform.
  • leave in Terraform (grey), score 0-49. Unmappable, or too load-bearing to be worth it.

The bands ride the same attrs._status channel the live drift overlay paints, so the colours mean what they always mean here: green is the good outcome, amber is work, grey is “not a claim”.

Click a card. The inspect pane shows the score spelled out (100 - 12x3 inbound = 64) alongside the counts it came from: inbound edges, which cost a Terraform data-source patch each, outbound edges, which defer to a deploy-time input, and the native tier the resource maps to.

Terraform sub-resources that inline into a parent, such as aws_s3_bucket_versioning into its bucket, are not ranked separately. They carve with the parent.

npx behold demo carve copies a bundled half-migrated estate (a small chant project beside a Terraform one, both describing the same AWS account), installs the chant it will shell, runs the advisor over the copy, and opens the graph with a six-step stepper on the panel’s Carve tab. No Docker, no cloud account, no terraform binary.

The demo’s graph is an estate frame: the banded ranking sits inside a legacy-tf box, and the copy’s chant project draws as an app box beside it, holding the pieces carved last month. The migration reads as half-done from the first frame. A plain behold carve report.json has no chant project to show, so it draws the banded ranking alone.

  1. Advise. The bands, with what each one means and how many landed in it.
  2. Pick. Click a green card. The inspect pane shows the score arithmetic; the step names the boundary the cut crosses (and says plainly when the report carries counts rather than edge lists).
  3. Emit. Runs chant carve emit --state --select <address> into app/carveout/ in the copy, then shows the emitted chant source, chant’s own boundary report for the carve, and the chant lint result.
  4. Bridge. Runs chant carve bridge, never with --apply-rewrites. The proposed data source, the rewired survivors and a git-applyable patch land beside the runbook; the demo’s own .tf files are not edited.
  5. Handoff. The runbook’s commands, each with a copy button, and deliberately not a run button. terraform state rm releases Terraform’s claim on a live resource and terraform apply writes to your cloud, so behold echoes them and leaves them yours to run.
  6. Done. The card is marked chant-owned at the observe position, and terraform import reverses all of it.

The Emit step reports chant lint, not chant build. chant#1637: carve emit folds the bucket’s versioning and public-access-block sub-resources into the carve set but does not yet carry them as native props, so build fails two AWS policy rules on source the advisor scored 88. The step links the reason rather than letting a lint pass read as a build pass.

behold writes only into the copy it just made, and only into app/carveout/ inside it. Both steps refuse outside a demo copy; a plain behold carve report.json shows the same six steps with the runs greyed out and a reason.

The strangler fig: what has actually graduated

Section titled “The strangler fig: what has actually graduated”

chant carve emit (chant ≥ 0.52.2) writes a state manifest beside the source it emits: <address>.carve.json in the --output directory. carve bridge adds its record to the same file, and carve apply adds the ownership marker. behold reads those manifests and never writes one, which is what makes the picture survive a restart: the progression lives on disk in chant’s file.

Three stages, and only the last of them is a graduation:

  • emitted. Typed chant source exists; the surviving Terraform still reads the resource directly. The card stays in its band, repainted in the in-flight blue with the words emitted — not bridged.
  • bridged. The survivor patch is proposed. Still banded, still Terraform’s: bridged — apply is yours.
  • applied. carve apply recorded the ownership marker. The card leaves the Terraform bands and draws inside the chant project box, keeping its Terraform address, with carved → chant on it.

The chant box’s panel carries the count (carved so far — 1 of 12 carved) and the panel’s project section lists each carved address with its stage. 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.

Point behold carve at a report in a directory that also holds manifests and you get the same reading with no demo at all: 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 the state in the same panel section, off /api/project.

carve apply graduates ownership. behold renders what the manifest records about it and echoes the command, with no --select, because the manifest in the output directory already names the target:

Terminal window
chant carve apply --from legacy-tf --output app/carveout --env prod --stack assets --write-source

There is no /api/carve/apply endpoint and no apply button anywhere in the UI. That is the same line the Handoff step draws around terraform state rm: behold triggers delegated work, it does not decide when an estate changes hands.

npx behold demo carve --live (docker + terraform on PATH) boots a scratch Floci — its own container name and port, refused if taken, deleted on exit — arms the estate’s provider override into the demo copy, and really applies the starred resources into it. The advisor then reads a tfstate terraform wrote, and the Handoff step gains a read-only terraform plan button: run it after pasting terraform state rm and the carved resource is simply absent from the plan — 0 to destroy, nothing blinked. The Emit step gains the observe beat: watch chant read it live runs chant lifecycle diff --live in the carveout against the scratch Floci, and the carved resource reads observed — EXTERNAL, ownership foreign — while Terraform still owns it (chant#1647’s identity read path, chant ≥ 0.44.12).

Terminal window
curl localhost:4600/api/carve # the raw report, verbatim
curl localhost:4600/api/graph # the same thing as {ir, svg, meta}
# demo only (behold demo carve): the walkthrough's two safe steps
curl -X POST localhost:4600/api/carve/emit -H 'content-type: application/json' -d '{"select":"aws_s3_bucket.assets"}'
curl -X POST localhost:4600/api/carve/bridge -H 'content-type: application/json' -d '{"select":"aws_s3_bucket.assets"}'

select must name a resource the served report ranks; anything else is a 400. Outside a demo copy both POSTs answer 403 {code: "read-only"}, and on an ordinary project serve they aren’t registered at all. GET /api/project’s carve.demo.runnable is how a client knows which it is.

A file that isn’t a peelability report gets a structured {error, code: "carve-report", remedy} — in the terminal before the server starts, and from the routes if it goes bad later. It never renders as a blank graph.

Since chant 0.54 a carve manifest records its emitted files relative to its own directory, and chant carve status --from <root> --json is chant’s own read over a project’s manifests — behold uses it (the local walk stands in below that chant) and joins each manifest’s emitted file to the chant entity that file declares. On the entity graph and the overlay that entity carries carved: <address> and the carved → chant mark; in the carve lens a graduated card takes on the kind, lexicon and declared attributes of the entity it became, while keeping its Terraform address as its id so the morph still glides.

Boundary edges draw from the lists a chant ≥ 0.52 report carries (chant#1636); a report from an older chant has counts only, and the graph says so on its status line. The cross-environment question — is prod running the render staging tested — now has digests on both sides but no two-environment surface to ask it on.