Skip to content

feat(terraform): render an estate chant read — types as kinds, roots as boxes, and a decision about what is a card (#379, #380, #382) - #383

Merged
lex00 merged 1 commit into
mainfrom
feat/terraform-render-379-382
Sep 8, 2026
Merged

feat(terraform): render an estate chant read — types as kinds, roots as boxes, and a decision about what is a card (#379, #380, #382)#383
lex00 merged 1 commit into
mainfrom
feat/terraform-render-379-382

Conversation

@lex00

@lex00 lex00 commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Closes #379, #380, #382. Child work of #378; #381 was answered and closed first, and its finding shaped this.

The starting point

behold serve already rendered a Terraform estate, because chant's terraform lexicon reads the estate's own HCL and a project using it is a chant project. It was just unreadable: 247 nodes, every card titled Terraform::Resource with a generic glyph, one box, and 108 variables plus 53 outputs plus 14 terraform blocks competing with 43 resources for the canvas.

Three passes

pass issue what it does
normalizeTerraformNodes #379 resource type out of attrs.address into kind, block class into attrs.block; a data source gains reads
groupTerraformByRoot #380 roots become the boxes; detects upstream grouping and retires itself when chant#2266 lands
filterTerraformCards #382 what is a card, with a note saying what was left out

All guarded on hasTerraformEntities, so a chant, k8s or choudoufu estate gets the identical object back — asserted.

#382's decision, written into AGENTS.md so the next lens can find it:

tier blocks why
default resource, data, module the estate
detail 3 + output, variable its interface
never terraform, provider, locals settings

One pack, two producers. render.ts registered the terraform lexicon for carveCardFields, and a read block has no score. It now dispatches: carve node by score, read block by block. Neither producer had to change lexicon, which matters — a carve node and a read block are both Terraform and should not disagree about what they are.

No invented edges. A stock estate has none until chant#2265. #381 measured the one relationship that looked derivable and refused it; a data source states what it reads as a row (reads name ${var.boundary_name}), interpolation unresolved.

Verified against real output

Two recorded chant graph --format ir documents from the published lexicon 0.59.0, provenance in the test files: behold's own example-carve/legacy-tf as one root (29 nodes, all seven entity kinds), and water park's baseline plus waterpark-runner as two roots at detail 3 (30 nodes, the cross-root read). Served for real:

detail=2   4 cards   boxes {baseline: 1, runner: 3}   kinds aws_iam_policy ×2, aws_ecr_repository, module
           note: showing the estate — 11 variables, 9 outputs, 3 terraform blocks,
                 2 locals blocks, 1 provider not drawn (outputs and variables appear at detail 3 …)
detail=3  24 cards   note drops the interface line; the data card reads "name ${var.boundary_name}"
logical=1  4 cards   byContainer {root baseline: 1, root runner: 3}

Tests

just check green: 88 files, 1718 tests. terraform-lens.test.ts (18) covers each pass, idempotence, an unknown future entity kind left untouched, a non-Terraform estate byte-identical, the elision arithmetic and its wording, the pack's dispatch, and the lens. terraform-route.test.ts (4) drives /api/graph at both details and through the logical lens with the chant shell-out mocked, and asserts a k8s estate is unaffected.

🤖 Generated with Claude Code

https://claude.ai/code/session_0124XP1LJTZxgJRy7Dh1UfXt

…as boxes, and a decision about what is a card (#379, #380, #382)

A Terraform estate reaches behold through chant's terraform lexicon, which
reads the HCL an estate already has and emits one entity per block. So it is
already a chant project as far as behold is concerned, and `behold serve`
already drew it — 247 nodes on the estate this was built against, every card
titled `Terraform::Resource`, all in one box, four fifths of them variables
and outputs and settings.

Three passes, guarded on the IR carrying terraform entities so every other
estate gets the identical object back:

normalizeTerraformNodes moves the resource type out of attrs.address into
kind, keeping the block class in attrs.block. That is the shape a carve node
already has, so one presentation pack now serves both producers — dispatching
on score for a carve node and on block for a read one — and pinhole's keyword
icon heuristic resolves aws_s3_bucket and friends for both. A data source also
gains a `reads` row naming what it filters on, with the interpolation left
unresolved, which is #381's recommendation after that spike measured the
cross-root read and refused to infer an edge from it.

groupTerraformByRoot makes the roots the boxes, from attrs.root or the id
prefix. It detects an IR already grouped upstream and leaves it alone, so
chant#2266 retires it rather than fighting it.

filterTerraformCards is #382's decision, written down here and in AGENTS.md:
resources, data sources and modules are the estate and are cards by default;
outputs and variables are its interface and appear at detail 3, where chant
also starts carrying parsed bodies; terraform, provider and locals blocks are
settings and are never cards. Nothing is dropped silently — the note says what
is not drawn and where to see it, the way edgelessNote says why a view has no
edges.

A logical lens beside the choudoufu one: one box per root, and only the edges
the graph states. No edges are invented anywhere; a stock estate has none
until chant#2265.

Wired into both /api/graph branches and both /api/overlay branches, plus the
two logical paths, each running the passes exactly once.

Verified against two real IR documents from chant's published lexicon 0.59.0 —
behold's own example-carve/legacy-tf as one root, and water park's baseline
and satellite as two, the pair carrying the cross-root read. Served for real:
4 cards at detail 2, 24 at detail 3, root boxes on both, the note naming the
26 blocks it left out.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0124XP1LJTZxgJRy7Dh1UfXt
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.

terraform cards: lead with the resource type and the address, not Terraform::Resource and the filename (#378)

1 participant