feat: behold carve <report.json> — the Terraform peelability report as a graph (#252) - #258
Merged
Conversation
… 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #252. M1 of #230.
chant carve advise --jsonalready 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.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.mainat 64; leave in Terraform (grey) withrandom_pet.suffixat 0. Each card shows its score and verdict; clicking one opens the arithmetic.What landed
src/carve-lens.ts—carveReportToIr(). One node per ranked resource or module:id= the Terraform address,kind= the Terraform type (aws_s3_bucket), lexiconterraform. The band ridesattrs._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 taggedviaAttr: "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,patchOnCarvenames the survivors needing a data-source patch anddeferredInputsthe values that become deploy-time inputs — the predicted diff, in words.behold carve <report.json> [--port]+ carve-mode routes./api/carveserves the report verbatim;/api/graphand/api/projectanswer 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 aversion, and "behold reads version 1" is a baffling thing to say about apackage.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 --jsonover a scratch copy ofpackages/core/src/terraform/__fixtures__/sample-estate— nothing in the chant checkout was touched, and no Terraform tooling entered behold.The contract, as found
carveJson(chantpackages/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:
versionfield. Accepted (that is exactly what carve lens M1: behold carve <report.json> — render the peelability report (#230) #252 said to do).BoundaryEdgeshape, 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_TITLEentry for the newcarve-reportcode. 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'sprojectDiris wherever the report file happens to sit — a Downloads folder, someone else's Terraform repo — solayoutWriteBlocknames carve mode as its own reason and both/api/layoutroutes decline, rather than behold dropping a.behold/into a stranger's directory.Gates
just check(828 tests, 54 files) andnpm run smoke:uiboth green. 34 new tests across the converter, the routes and the layout.🤖 Generated with Claude Code