Skip to content

feat: behold carve <report.json> — the Terraform peelability report as a graph (#252) - #258

Merged
lex00 merged 3 commits into
mainfrom
feat/carve-lens
Aug 10, 2026
Merged

feat: behold carve <report.json> — the Terraform peelability report as a graph (#252)#258
lex00 merged 3 commits into
mainfrom
feat/carve-lens

Conversation

@lex00

@lex00 lex00 commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Closes #252. M1 of #230.

chant carve advise --json already ranks a Terraform estate by peelability — how cleanly each resource could be carved into native chant source. That report is the whole contract: behold parses no HCL, shells no Terraform, and adds no Terraform tooling to its dependency tree.

chant carve advise --from ./terraform --report carve.json
behold carve carve.json          # -> http://localhost:4600
curl localhost:4600/api/carve    # the raw report, verbatim, for agents

The sample estate renders as three stacked, titled panels: carve now (green) with the log group at 100, the three subnets at 96, the bucket at 88 and the Lambda at 81; boundary work (amber) with aws_vpc.main at 64; leave in Terraform (grey) with random_pet.suffix at 0. Each card shows its score and verdict; clicking one opens the arithmetic.

What landed

src/carve-lens.tscarveReportToIr(). One node per ranked resource or module: id = the Terraform address, kind = the Terraform type (aws_s3_bucket), lexicon terraform. The band rides attrs._status (clean leaf -> good, carvable w/ edits -> warn, leave in Terraform -> neutral), with a fallback to the documented score thresholds so a renamed band still colours honestly. Boundary edges are drawn in dependency direction and tagged viaAttr: "inbound" | "outbound" — which the SPA already renders as the edge tooltip and dashed style.

Score arithmetic in attrs, so the inspect pane's existing "declared" section shows it with no client work: score, arithmetic (100 - 12x3 inbound = 64, and it says when chant clamped), boundaryWork, mapsTo, tier, inbound, outbound, instances, dynamic. Once a report carries edge lists, patchOnCarve names the survivors needing a data-source patch and deferredInputs the values that become deploy-time inputs — the predicted diff, in words.

behold carve <report.json> [--port] + carve-mode routes. /api/carve serves the report verbatim; /api/graph and /api/project answer exactly the shape the SPA already expects. Carve mode shells no chant, watches no source, captures no frames, and probes neither Docker nor git — a static analysis of foreign Terraform has none of those axes, so every picker comes back empty rather than lying.

Polite refusal, on #193's standard. {error, code: "carve-report", remedy} — in the terminal before a server ever starts, and from the routes if the file goes bad later (it is re-read per request, so a regenerated report shows up on reload). The shape check runs before the version check: plenty of JSON carries a version, and "behold reads version 1" is a baffling thing to say about a package.json. An absent version is accepted, as #252 asks, until chant#1636 lands.

renderBanded() (src/render.ts) — a ranking is not a topology. Every carve node is its own connected component, so dagre put them all in rank 0 and strung them along one row: the 8-resource sample estate came out 2433x316, and a 150-resource estate 40680x316 — a picture no viewport can show. Stacked, titled band panels with grid-wrapped members give 1152x902 and 3300x1958 instead, and stay near a screen's aspect at 600 resources. Panels are tinted with the status their members agree on; a node no band claims still gets drawn.

Fixture: a real report. Generated by running chant's own carve advise --json over a scratch copy of packages/core/src/terraform/__fixtures__/sample-estate — nothing in the chant checkout was touched, and no Terraform tooling entered behold.

The contract, as found

carveJson (chant packages/core/src/cli/commands/carve.ts) emits:

{ "from": "sample-estate", "advisory": "read-only — …", "count": 8,
  "bands": { "clean leaf": 6, "carvable w/ edits": 1, "leave in Terraform": 1 },
  "resources": [{ "address": "aws_s3_bucket.assets", "kind": "resource", "score": 88,
                  "band": "clean leaf", "mapsTo": "AWS::S3::Bucket",
                  "breakdown": { "inbound": 1, "outbound": 0, "tier": 1, "hasDynamic": false,
                                 "instances": 1, "penalties": { "inbound": -12,  } } }] }

Two gaps against what #230's plan assumed, both worth chant#1636 knowing:

  1. No version field. Accepted (that is exactly what carve lens M1: behold carve <report.json> — render the peelability report (#230) #252 said to do).
  2. No boundary edge lists — only per-resource inbound/outbound counts. They cannot be paired back into edges: the sample estate has 4 inbound and 4 outbound, which admit several different matchings, and inventing one would be a lie about which survivors need patching. So today's report draws zero edges, the status line says so in as many words, and a test asserts the gap so we hear about it the day chant closes it. The lens already draws the lists (chant's own BoundaryEdge shape, either flat or split by direction) with no further work here — there is a full test suite against that forward shape.

SPA changes

One line: a PRECONDITION_TITLE entry for the new carve-report code. Carve mode is served a report, not a project, so the error card must not say "chant couldn't evaluate this project" about a project that was never involved. Everything else — the drift palette, the inspect pane, the edge tooltips, the pickers — works unchanged.

Meeting #228 (merged from main)

The hand-layout sidecar landed while this was open, and its write surface is cfg.projectDir + .behold/layout.json. Carve mode's projectDir is wherever the report file happens to sit — a Downloads folder, someone else's Terraform repo — so layoutWriteBlock names carve mode as its own reason and both /api/layout routes decline, rather than behold dropping a .behold/ into a stranger's directory.

Gates

just check (828 tests, 54 files) and npm run smoke:ui both green. 34 new tests across the converter, the routes and the layout.

🤖 Generated with Claude Code

lex00 and others added 3 commits August 9, 2026 22:14
… as a graph (#252) (#230)

M1 of #230. chant's `carve advise --json` already ranks a Terraform estate by
how cheaply each resource could be carved into native chant, so behold never
parses HCL and pulls in no Terraform tooling: the report is the contract.

- src/carve-lens.ts: `carveReportToIr()` — one node per ranked resource, kinded
  by Terraform type on a `terraform` lexicon, band on `attrs._status` (clean
  leaf -> good, carvable w/ edits -> warn, leave in Terraform -> neutral) so the
  existing drift palette colours it, score arithmetic in `attrs` so the inspect
  pane renders it, and boundary edges tagged `viaAttr: inbound|outbound`.
- `behold carve <report.json> [--port]` and carve-mode routes: `/api/carve`
  serves the report verbatim, `/api/graph` and `/api/project` answer the shape
  the SPA already expects. No chant is shelled, no source watched, no Docker or
  git probed — a static analysis of foreign Terraform has none of those axes.
- Refusals follow #193: `{error, code: "carve-report", remedy}` in the terminal
  before a server starts, and from the routes if the file goes bad later. Shape
  is checked before the version, so a stray package.json is told what it is
  missing rather than lectured about a schema it never claimed.
- `renderBanded()` (src/render.ts): a ranking is not a topology. Every carve
  node is its own connected component, so dagre laid the 8-resource sample
  estate out 2433x316 and a 150-resource estate 40680x316. Stacked, titled band
  panels with grid-wrapped members keep it near a screen's aspect at any size.
- Fixture: a REAL report, generated by running chant's own `carve advise --json`
  over a scratch copy of its sample-estate.

The one client change is a title for the new error code — carve mode is served a
report, not a project, so the card must not blame chant for failing to evaluate
a project that was never involved.

chant#1636 is still open: today's report carries per-resource boundary COUNTS
and no edge lists, which cannot be paired back into edges (4 inbound and 4
outbound admit several matchings). So no boundary edges are drawn, the status
line says exactly that, and the lens already draws them the day chant publishes
the lists.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…and the attrs alike (#252)

Nothing in a boundary edge distinguishes inbound from outbound on its own —
both name the same carve-set side — so the edge builder was inferring one thing
from the owning resource and the predicted-diff attrs another. Share the
conservative default (inbound: a survivor needing an immediate data-source
patch) so the picture and the inspect pane cannot disagree about which it chose.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Conflicts in src/server.ts, both from the hand-layout sidecar (#228/#257)
landing beside carve mode: two new `ServerOptions` fields, and two additions
to the `/api` route index. Both sides kept.

Carve mode now also declines the sidecar. #228's write surface is
`cfg.projectDir` + `.behold/layout.json`, and carve mode's `projectDir` is
wherever the report file happens to sit — a Downloads folder, someone else's
Terraform repo. There is no project there to keep a hand layout in, so
`layoutWriteBlock` names carve mode as its own reason rather than letting
behold drop a `.behold/` into a stranger's directory.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@lex00
lex00 merged commit d25b9b2 into main Aug 10, 2026
3 checks passed
@lex00
lex00 deleted the feat/carve-lens branch August 30, 2026 02:19
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.

carve lens M1: behold carve <report.json> — render the peelability report (#230)

1 participant