Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 40 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,35 @@ To move a resource: confirm its band on `/api/carve`, then run chant's own
applying the generated survivor rewrites stay a human gate — behold has no
endpoint that writes Terraform, and adding one would break the invariant below.

### The walkthrough (`behold demo carve`, #254)

`behold demo carve` copies a bundled half-migrated estate (a chant project
beside a Terraform one), runs the advisor over the copy, and serves the same
carve view plus a six-step stepper on the panel's Carve tab: advise → pick →
emit → bridge → handoff → done.

Two of those steps are POST routes, and they exist **only** in a demo copy:

- `POST /api/carve/emit` — body `{select}`; runs `chant carve emit --state
--select <addr> --output <copy>/app/carveout` and then `chant lint` on the
result. Answers `{select, command, output, artifacts[], boundary, lint,
buildCaveat}`.
- `POST /api/carve/bridge` — body `{select}`; runs `chant carve bridge`
**without** `--apply-rewrites`. Answers `{select, command, output, runbook,
proposals[]}`.

`GET /api/project`'s `carve.demo` says whether they can act (`runnable`, plus a
`reason` when not). `select` must name a resource the served report ranks;
anything else is a 400. Outside a demo copy both routes answer 403
`{code: "read-only"}`, and on an ordinary project serve they don't exist at all.

The gate the Emit step reports is `chant lint`, not `chant build` — chant#1637
means `build` fails on the emitted bucket. Don't read a lint pass as a build
pass.

There is still **no** endpoint that runs `terraform`. The handoff step hands
back the runbook's commands as text.

## The act loop (delegated, never direct)

behold does not apply. To change the estate:
Expand All @@ -94,7 +123,7 @@ If a request would have behold write to a cloud or to source directly, it's wron
behold shows truth and triggers Ops. Authority stays in the committed source and the
executor.

### The one exception, and its exact size
### The exceptions, and their exact size

`POST /api/layout` (#228) writes **one** file in the served project:
`.behold/layout.json` — the hand-layout sidecar, `{version, lenses: {<lens>:
Expand All @@ -115,3 +144,13 @@ inside a project, and it does not weaken the invariant above:
`GET /api/layout` reads it back; `GET /api/graph?layout=1` (and `/api/overlay`)
render with the deltas baked into the SVG, which is how `behold export` and
static snapshots honour a hand layout.

The second exception is the carve walkthrough's two steps above (#254), and it
is narrower still: they exist only when behold booted a `behold demo carve`
copy, they write only into `<copy>/app/carveout/`, and the directory they write
into is a scratch dir behold created inside a directory it copied for you a
minute earlier. `carve bridge` runs without `--apply-rewrites`, so the demo's
own Terraform is not edited either. The only request-derived value is `select`,
and it must be an address the served report already ranks — the value that
reaches the spawn's argv comes from a closed set read off disk. No cloud write,
no Terraform mutation, no edit to anyone's chant source.
38 changes: 36 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,9 @@ The copied project is yours: edit its source and watch the graph change live.
There's a whole catalog — `behold demo --list` names the rest (`behold demo
k8s` stands the same loop up on a throwaway k3d cluster: runtime Pods, field
ownership; `behold demo argo-estate` needs nothing at all — a three-project
Argo CD estate, declared only, so it runs where Docker doesn't). Every loaded
demo lands in the panel's recents, so switching between them is the Scope tab.
Argo CD estate, declared only, so it runs where Docker doesn't; `behold demo
carve` is the Terraform peel walkthrough, below). Every loaded demo lands in
the panel's recents, so switching between them is the Scope tab.

The catalog is in the panel too (#268): the Scope tab's switcher lists every
bundled demo under your recents, one click to copy, install and serve it —
Expand Down Expand Up @@ -291,6 +292,39 @@ contract. A file that isn't a peelability report is refused with a structured
never a blank graph. See `docs/using/carve` and issue #230 for the roadmap
(the post-emit diff, then Terraform as an estate member).

### The walkthrough: `behold demo carve`

```sh
npx @intentius/behold demo carve # no Docker, no cloud, no terraform binary
```

Copies a half-migrated estate — a small chant project beside a Terraform one,
both describing the same AWS account — installs the chant it will shell, runs
`chant carve advise` over the copy, and opens the banded graph with a six-step
stepper on the panel's **Carve** tab:

1. **Advise** — the bands, with what each one means.
2. **Pick** — click a green card. The inspect pane shows the score arithmetic;
the step names the boundary the cut crosses.
3. **Emit** — runs `chant carve emit --state --select <addr>` into
`app/carveout/` in the copy, then shows the emitted chant source and the
`chant lint` result.
4. **Bridge** — runs `chant carve bridge` (never `--apply-rewrites`) and renders
the proposed data source, the rewired survivors and the patch.
5. **Handoff** — the runbook's commands with copy buttons, and **not** a button:
`terraform state rm` and `terraform apply` change who owns a live resource,
so they stay yours to run. The panel says so.
6. **Done** — the card is marked chant-owned at the observe position;
`terraform import` reverses all of it.

The Emit step reports `chant lint`, not `chant build`: chant#1637 means `build`
fails on the emitted bucket even though the advisor scored it 88, and the panel
links the reason. `example-carve/README.md` has the estate's full story, the
band table, and the offline/`--live` split.

behold writes only into the demo copy it made — `app/carveout/`, and nothing
else. Your Terraform is never edited; see AGENTS.md, "Invariant".

## Configuration — `.behold.json`

An optional `.behold.json` in the served project's root is **behold's own**
Expand Down
16 changes: 16 additions & 0 deletions demos.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,22 @@
"requires": [],
"serve": { "dirs": ["control-plane", "app-a", "app-b"] }
},
{
"name": "carve",
"description": "The peel walkthrough: a half-migrated estate — a chant project beside a Terraform one — scored by `chant carve advise`, then a six-step stepper that carves one S3 bucket across the line on camera (advise, pick, emit, bridge, handoff, done). Emit and bridge really run, into the demo copy; the destructive terraform commands stay yours to paste. No cloud, no Docker, no terraform binary.",
"source": "bundled",
"dir": "example-carve",
"requires": [],
"serve": {
"carve": {
"report": "carve-report.json",
"from": "legacy-tf",
"state": "legacy-tf/terraform.tfstate",
"project": "app",
"out": "app/carveout"
}
}
},
{
"name": "fountain",
"description": "The mature estate: self-hosted fountain (a real app) on a throwaway k3d cluster — tiers (try the ha tier picker), seams, drift, runtime Pods. Clones INTENTIUS/fountain-ops; ~5 minutes, mostly image pulls. `just down` in the clone removes the cluster.",
Expand Down
23 changes: 23 additions & 0 deletions docs/src/content/docs/using/carve.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,36 @@ Click a card. The inspect pane shows the score spelled out — `100 - 12x3 inbou

Terraform sub-resources that inline into a parent — `aws_s3_bucket_versioning` into its bucket — are not ranked separately. They carve with the parent, so they never appear as their own card.

## The walkthrough: `behold demo carve`

`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 banded graph with a six-step stepper on the panel's **Carve** tab. No Docker, no cloud account, no `terraform` binary.

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; behold triggers delegated work, it does not decide when an estate changes hands. The panel says exactly that.
6. **Done** — the card is marked chant-owned at the observe position. `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.

Deferred to a follow-up: the Floci `--live` tier (`terraform apply` into a scratch emulator, live observe beats, a real `terraform plan` showing no destroy) and the morph animation that slides the carved card out of the Terraform boundary and into the chant project.

## For agents

```sh
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.

## What is not here yet
Expand Down
32 changes: 28 additions & 4 deletions example-carve/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,38 @@ A migration caught half-done. `app/` is chant; `legacy-tf/` is Terraform; both
describe one AWS account, and the walkthrough carves one more resource across
the line while the camera is running.

This directory is the fixture only. The stepper, the server wiring and the
`demos.json` entry land with the walkthrough PR (behold #254 part 2).
Run it:

```sh
behold demo carve # copies this directory, installs, advises, serves
```

The boot does three things before a port opens: `npm install` in `app/` (whose
chant every step of the walkthrough shells), `@cdktf/hcl2json` into the copy's
root `node_modules` (chant lazy-loads the HCL parser from its own install
upward, so `<copy>/node_modules` is where it resolves — not beside the `.tf`
files), and `chant carve advise --report` over the copy's own Terraform. If any
of that fails, the committed `carve-report.json` is served instead and the
reason is on screen; a blank graph is the one outcome that's never allowed.

```
example-carve/
app/ a small chant project — the pieces already carved
carveout/ where Emit and Bridge write, in a copy (gitignored)
legacy-tf/ the Terraform half, still Terraform-owned
terraform.tfstate synthetic state (fake account, fake ARNs)
floci-override.tf.disabled provider endpoints for the --live tier, inert
modules/cdn/ a local module, so module.cdn resolves offline
carve-report.json the committed `carve advise` output
```

`carveout/` sits INSIDE `app/` rather than beside it, and that is load-bearing:
the emitted source imports `@intentius/chant-lexicon-aws`, and Node resolves
that from the file's own directory upward. From `app/carveout/src/assets.ts` it
reaches `app/node_modules`; from a sibling `carveout/` it would reach nothing,
and the Emit step's `chant lint` would fail on an install problem rather than
on the source.

## The estate is mixed from the first frame

`app/src/carved.ts` holds a CloudWatch log group and an SSM parameter that came
Expand Down Expand Up @@ -92,6 +111,9 @@ from the repository root, so the `from` field stays a relative path.

## The six beats

These are the six steps on the panel's Carve tab. behold runs beats 4 and 5
itself, into `app/carveout/` in the copy; beat 6 is copy buttons, on purpose.

1. **The green star.** The estate view, banded. Two resources are green; the
100 is a log group nobody will miss, so the eye lands on the 88 next to it.
2. **The arithmetic.** Open `aws_s3_bucket.assets`: 100 minus 12 for one
Expand Down Expand Up @@ -150,8 +172,10 @@ passes with warnings, but `chant build` fails on two AWS policy rules
(`PublicAccessBlockConfiguration` missing, no TLS-deny bucket policy) even
though the Terraform declared the first of those.

Verified against chant 0.44.4. The walkthrough's Emit beat should show `lint`,
not `build`, until the fold is applied as well as reported.
Verified against chant 0.44.4. The walkthrough's Emit beat shows `lint`, not
`build`, until the fold is applied as well as reported — the step's "why lint
and not build?" note says the same thing on screen, so nobody reads a lint pass
as a build pass. That is chant#1637.

## Values

Expand Down
3 changes: 3 additions & 0 deletions example-carve/app/.gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
node_modules
# chant build artifacts — regenerated by `chant build`
template.json
# #254: where `behold demo carve`'s Emit and Bridge steps write. Generated per
# run, inside the demo copy — never committed.
carveout/
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"example-k8s",
"example-flux-estate",
"example-argo-estate",
"example-carve",
"demos.json",
"AGENTS.md"
],
Expand Down
Loading
Loading