@intentius/behold 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/AGENTS.md +94 -2
  2. package/README.md +133 -2
  3. package/demos.json +25 -1
  4. package/dist/cli.js +3176 -330
  5. package/example-argo-estate/README.md +71 -0
  6. package/example-argo-estate/app-a/chant.config.ts +18 -0
  7. package/example-argo-estate/app-a/manifests/app.yaml +54 -0
  8. package/example-argo-estate/app-a/package.json +13 -0
  9. package/example-argo-estate/app-a/src/app.ts +35 -0
  10. package/example-argo-estate/app-a/tsconfig.json +1 -0
  11. package/example-argo-estate/app-b/chant.config.ts +18 -0
  12. package/example-argo-estate/app-b/manifests/app.yaml +54 -0
  13. package/example-argo-estate/app-b/package.json +13 -0
  14. package/example-argo-estate/app-b/src/app.ts +34 -0
  15. package/example-argo-estate/app-b/tsconfig.json +1 -0
  16. package/example-argo-estate/control-plane/chant.config.ts +29 -0
  17. package/example-argo-estate/control-plane/package.json +13 -0
  18. package/example-argo-estate/control-plane/src/argo.ts +69 -0
  19. package/example-argo-estate/control-plane/tsconfig.json +1 -0
  20. package/example-argo-estate/package-lock.json +1949 -0
  21. package/example-argo-estate/package.json +10 -0
  22. package/example-carve/README.md +194 -0
  23. package/example-carve/app/chant.config.ts +6 -0
  24. package/example-carve/app/package-lock.json +1075 -0
  25. package/example-carve/app/package.json +13 -0
  26. package/example-carve/app/src/carved.ts +30 -0
  27. package/example-carve/app/tsconfig.json +1 -0
  28. package/example-carve/carve-report.json +872 -0
  29. package/example-carve/legacy-tf/cdn.tf +23 -0
  30. package/example-carve/legacy-tf/compute.tf +63 -0
  31. package/example-carve/legacy-tf/floci-override.tf.disabled +61 -0
  32. package/example-carve/legacy-tf/modules/cdn/main.tf +72 -0
  33. package/example-carve/legacy-tf/naming.tf +10 -0
  34. package/example-carve/legacy-tf/network.tf +119 -0
  35. package/example-carve/legacy-tf/observability.tf +18 -0
  36. package/example-carve/legacy-tf/outputs.tf +16 -0
  37. package/example-carve/legacy-tf/storage.tf +33 -0
  38. package/example-carve/legacy-tf/terraform.tfstate +602 -0
  39. package/example-carve/legacy-tf/versions.tf +40 -0
  40. package/example-flux-estate/README.md +22 -12
  41. package/example-flux-estate/app-a/package.json +2 -2
  42. package/example-flux-estate/app-a/src/app.ts +2 -1
  43. package/example-flux-estate/app-b/chant.config.ts +4 -3
  44. package/example-flux-estate/app-b/package.json +2 -2
  45. package/example-flux-estate/app-b/src/app.ts +5 -3
  46. package/example-flux-estate/control-plane/package.json +2 -2
  47. package/example-flux-estate/control-plane/src/flux.ts +10 -2
  48. package/example-flux-estate/package-lock.json +17 -17
  49. package/example-k8s/README.md +16 -2
  50. package/example-k8s/base/deployment.yaml +21 -0
  51. package/example-k8s/base/kustomization.yaml +3 -0
  52. package/example-k8s/base/service.yaml +12 -0
  53. package/example-k8s/chant.config.ts +10 -0
  54. package/example-k8s/overlays/dev/kustomization.yaml +6 -0
  55. package/example-k8s/package-lock.json +18 -18
  56. package/example-k8s/package.json +3 -3
  57. package/example-writes/package-lock.json +14 -14
  58. package/example-writes/package.json +3 -3
  59. package/package.json +9 -6
  60. package/web/app.js +1027 -42
  61. package/web/carve-steps.js +610 -0
  62. package/web/carve-steps.test.js +233 -0
  63. package/web/demos.js +71 -0
  64. package/web/demos.test.js +83 -0
  65. package/web/icons/cncf/argo.svg +1 -0
  66. package/web/icons/cncf/flux.svg +1 -0
  67. package/web/icons/cncf/helm.svg +1 -0
  68. package/web/icons/k8s/c-role.svg +1 -0
  69. package/web/icons/k8s/cm.svg +1 -0
  70. package/web/icons/k8s/crb.svg +1 -0
  71. package/web/icons/k8s/crd.svg +1 -0
  72. package/web/icons/k8s/cronjob.svg +1 -0
  73. package/web/icons/k8s/deploy.svg +1 -0
  74. package/web/icons/k8s/ds.svg +1 -0
  75. package/web/icons/k8s/ep.svg +1 -0
  76. package/web/icons/k8s/group.svg +1 -0
  77. package/web/icons/k8s/hpa.svg +1 -0
  78. package/web/icons/k8s/ing.svg +1 -0
  79. package/web/icons/k8s/job.svg +1 -0
  80. package/web/icons/k8s/limits.svg +1 -0
  81. package/web/icons/k8s/netpol.svg +1 -0
  82. package/web/icons/k8s/ns.svg +1 -0
  83. package/web/icons/k8s/pod.svg +1 -0
  84. package/web/icons/k8s/psp.svg +1 -0
  85. package/web/icons/k8s/pv.svg +1 -0
  86. package/web/icons/k8s/pvc.svg +1 -0
  87. package/web/icons/k8s/quota.svg +1 -0
  88. package/web/icons/k8s/rb.svg +1 -0
  89. package/web/icons/k8s/role.svg +1 -0
  90. package/web/icons/k8s/rs.svg +1 -0
  91. package/web/icons/k8s/sa.svg +1 -0
  92. package/web/icons/k8s/sc.svg +1 -0
  93. package/web/icons/k8s/secret.svg +1 -0
  94. package/web/icons/k8s/sts.svg +1 -0
  95. package/web/icons/k8s/svc.svg +1 -0
  96. package/web/icons/k8s/user.svg +1 -0
  97. package/web/icons/k8s/vol.svg +1 -0
  98. package/web/index.html +285 -85
  99. package/web/json-view.js +334 -0
  100. package/web/json-view.test.js +218 -0
  101. package/web/layout-store.js +278 -0
  102. package/web/layout-store.test.js +388 -0
  103. package/web/panel.js +28 -0
  104. package/web/theme.js +114 -6
package/AGENTS.md CHANGED
@@ -17,7 +17,7 @@ apply creds.
17
17
  ## Getting a server
18
18
 
19
19
  ```sh
20
- npx @intentius/behold serve <chant-project-dir> --port 4600 # or: preview / demo
20
+ npx @intentius/behold serve <chant-project-dir> --port 4600 # or: preview / demo / doctor
21
21
  ```
22
22
 
23
23
  `behold demo` needs no project at all — it copies the bundled example and serves
@@ -25,10 +25,27 @@ it against a local emulator (Docker). A directory that is not a chant project
25
25
  gets a structured `{code: "no-project"}` error from `/api/graph`, not a blank
26
26
  graph.
27
27
 
28
+ A running server offers the same catalog over HTTP (#268): `GET /api/demos`
29
+ lists every bundled demo with `{name, description, requires, satisfiable,
30
+ reason?, fetches, repo?, target, loaded}` — `satisfiable` is doctor's PATH probe
31
+ for that demo's `requires`, and `fetches` marks the one kind of entry that
32
+ reaches the network (a git demo is cloned). `POST /api/demos/open` with
33
+ `{name}` loads it and switches the served project to it: the same copy/clone →
34
+ install → setup the CLI runs, then an in-place switch. It takes a catalog
35
+ **name** and never a path, so it cannot be aimed outside the install; it is
36
+ preview-locked, and one load runs at a time (409 otherwise).
37
+
28
38
  ## The read loop
29
39
 
30
40
  0. **discover** — GET `/api` lists every route with a one-line description,
31
- plus the server's version and a link back to this guide.
41
+ plus the server's version and a link back to this guide. Before the server
42
+ exists (or when a route answers with an error you'd have to guess at), run
43
+ `npx @intentius/behold doctor <dir> --json`: a read-only diagnosis of the
44
+ project's kind, its own chant install and version, declared lexicons, the
45
+ envs the picker will infer, the bound kube context versus the ambient one,
46
+ substrate readiness and committed Ops. Each check is
47
+ `{name, status: pass|warn|fail, detail, fix}`; the process exits non-zero
48
+ iff something failed. It starts no server and changes nothing.
32
49
  1. **observe** — GET `/api/graph` (JSON: `{ ir, svg, meta }`). The mixed graph
33
50
  of the project, every node with `id`/`kind`/`lexicon`/`attrs`/`sourceLoc`. Drift
34
51
  status, when present, is `attrs._status` (`good`=managed, `warn`=foreign,
@@ -43,6 +60,49 @@ graph.
43
60
  3. **inspect** — a node's `sourceLoc.file` is the typed source that declared it;
44
61
  edit there to change the estate (chant is the source of truth, not behold).
45
62
 
63
+ ## The carve loop (Terraform → chant, #230)
64
+
65
+ `behold carve <report.json>` serves a `chant carve advise --json` peelability
66
+ report instead of a chant project. Same SPA, same `/api/graph` shape; `attrs.
67
+ _status` carries the band (`good` = carve now, `warn` = has boundary work,
68
+ `neutral` = leave in Terraform) and `attrs` carries the score arithmetic
69
+ (`score`, `arithmetic`, `inbound`, `outbound`, `tier`, `mapsTo`).
70
+ `GET /api/carve` returns the raw report verbatim.
71
+
72
+ To move a resource: confirm its band on `/api/carve`, then run chant's own
73
+ `carve emit --state` and `carve bridge` in the project. `terraform state rm` and
74
+ applying the generated survivor rewrites stay a human gate — behold has no
75
+ endpoint that writes Terraform, and adding one would break the invariant below.
76
+
77
+ ### The walkthrough (`behold demo carve`, #254)
78
+
79
+ `behold demo carve` copies a bundled half-migrated estate (a chant project
80
+ beside a Terraform one), runs the advisor over the copy, and serves the same
81
+ carve view plus a six-step stepper on the panel's Carve tab: advise → pick →
82
+ emit → bridge → handoff → done.
83
+
84
+ Two of those steps are POST routes, and they exist **only** in a demo copy:
85
+
86
+ - `POST /api/carve/emit` — body `{select}`; runs `chant carve emit --state
87
+ --select <addr> --output <copy>/app/carveout` and then `chant lint` on the
88
+ result. Answers `{select, command, output, artifacts[], boundary, lint,
89
+ buildCaveat}`.
90
+ - `POST /api/carve/bridge` — body `{select}`; runs `chant carve bridge`
91
+ **without** `--apply-rewrites`. Answers `{select, command, output, runbook,
92
+ proposals[]}`.
93
+
94
+ `GET /api/project`'s `carve.demo` says whether they can act (`runnable`, plus a
95
+ `reason` when not). `select` must name a resource the served report ranks;
96
+ anything else is a 400. Outside a demo copy both routes answer 403
97
+ `{code: "read-only"}`, and on an ordinary project serve they don't exist at all.
98
+
99
+ The gate the Emit step reports is `chant lint`, not `chant build` — chant#1637
100
+ means `build` fails on the emitted bucket. Don't read a lint pass as a build
101
+ pass.
102
+
103
+ There is still **no** endpoint that runs `terraform`. The handoff step hands
104
+ back the runbook's commands as text.
105
+
46
106
  ## The act loop (delegated, never direct)
47
107
 
48
108
  behold does not apply. To change the estate:
@@ -62,3 +122,35 @@ and saga rollback. There is no behold endpoint that mutates the cloud.
62
122
  If a request would have behold write to a cloud or to source directly, it's wrong.
63
123
  behold shows truth and triggers Ops. Authority stays in the committed source and the
64
124
  executor.
125
+
126
+ ### The exceptions, and their exact size
127
+
128
+ `POST /api/layout` (#228) writes **one** file in the served project:
129
+ `.behold/layout.json` — the hand-layout sidecar, `{version, lenses: {<lens>:
130
+ {<node id>: {dx,dy,dw,dh}}}}`. That is the whole of behold's write surface
131
+ inside a project, and it does not weaken the invariant above:
132
+
133
+ - It is **workspace metadata**, not estate truth. Deltas describe how *you* want
134
+ the picture arranged on top of dagre's layout; the graph underneath stays
135
+ chant's, and a delta for a node that left the estate is dropped on read.
136
+ - It **never touches the cloud and never touches your source**. No `.ts`, no
137
+ `chant.config.ts`, no `.behold.json`. The path is `cfg.projectDir` + two
138
+ constants — nothing from the request reaches the filesystem.
139
+ - It refuses politely when it shouldn't write: preview mode, a static-export
140
+ capture, a read-only project directory, an oversized or malformed body.
141
+ - It is **per-user state**, unlike `.behold.json` (config, meant to be tracked).
142
+ Projects should gitignore `.behold/`.
143
+
144
+ `GET /api/layout` reads it back; `GET /api/graph?layout=1` (and `/api/overlay`)
145
+ render with the deltas baked into the SVG, which is how `behold export` and
146
+ static snapshots honour a hand layout.
147
+
148
+ The second exception is the carve walkthrough's two steps above (#254), and it
149
+ is narrower still: they exist only when behold booted a `behold demo carve`
150
+ copy, they write only into `<copy>/app/carveout/`, and the directory they write
151
+ into is a scratch dir behold created inside a directory it copied for you a
152
+ minute earlier. `carve bridge` runs without `--apply-rewrites`, so the demo's
153
+ own Terraform is not edited either. The only request-derived value is `select`,
154
+ and it must be an address the served report already ranks — the value that
155
+ reaches the spawn's argv comes from a closed set read off disk. No cloud write,
156
+ no Terraform mutation, no edit to anyone's chant source.
package/README.md CHANGED
@@ -30,17 +30,33 @@ npx @intentius/behold demo # copies the example to ./behold-demo, ins
30
30
  The copied project is yours: edit its source and watch the graph change live.
31
31
  There's a whole catalog — `behold demo --list` names the rest (`behold demo
32
32
  k8s` stands the same loop up on a throwaway k3d cluster: runtime Pods, field
33
- ownership). Every loaded demo lands in the panel's recents, so switching
34
- between them is the Scope tab.
33
+ ownership; `behold demo argo-estate` needs nothing at all a three-project
34
+ Argo CD estate, declared only, so it runs where Docker doesn't; `behold demo
35
+ carve` is the Terraform peel walkthrough, below). Every loaded demo lands in
36
+ the panel's recents, so switching between them is the Scope tab.
37
+
38
+ The catalog is in the panel too (#268): the Scope tab's switcher lists every
39
+ bundled demo under your recents, one click to copy, install and serve it —
40
+ demos whose prerequisites are missing stay visible, disabled, saying what to
41
+ install, and one that would clone from the network says so on the button.
35
42
 
36
43
  Already have a chant project?
37
44
 
38
45
  ```sh
39
46
  cd my-chant-project
47
+ npx @intentius/behold doctor # will this project serve well? (read-only)
40
48
  npx @intentius/behold preview # → http://localhost:4600, this project's graph
41
49
  npx @intentius/behold serve . --env prod --poll 30 # live drift overlay
42
50
  ```
43
51
 
52
+ `behold doctor` is the first thing to run on a project behold hasn't seen: one
53
+ line each for the project's kind, its own chant install and version, declared
54
+ lexicons, the envs the picker will infer, the kube context chant binds versus
55
+ your ambient one, substrate readiness and committed Ops — pass/warn/fail with
56
+ a one-line fix. It starts nothing and changes nothing; it exits non-zero only
57
+ when something would actually stop behold serving the project well, so CI can
58
+ gate on it. `--json` for scripts and agents.
59
+
44
60
  Driving it from an agent or script? `GET /api` lists every JSON route;
45
61
  [AGENTS.md](./AGENTS.md) (shipped in the package) is the read/act contract.
46
62
 
@@ -256,6 +272,59 @@ behold shells the **project's own** chant (resolved from the project's
256
272
  `@intentius/chant ^0.18.1` or later for the live overlay (`graph --live` observed
257
273
  nothing before that fix).
258
274
 
275
+ ## Terraform carve-out: `behold carve <report.json>`
276
+
277
+ `chant carve advise` ranks a Terraform estate by **peelability** — how cleanly
278
+ each resource could be carved into native chant source. `behold carve` draws
279
+ that ranking: one card per resource, three panels (carve now / boundary work /
280
+ leave in Terraform) on the same `attrs._status` drift palette every other view
281
+ uses, and the score arithmetic behind each rank in the inspect pane.
282
+
283
+ ```sh
284
+ chant carve advise --from ./terraform --report carve.json
285
+ behold carve carve.json # → http://localhost:4600
286
+ curl localhost:4600/api/carve # the raw report, for agents
287
+ ```
288
+
289
+ behold parses no HCL and needs no Terraform tooling: the report **is** the
290
+ contract. A file that isn't a peelability report is refused with a structured
291
+ `{error, code: "carve-report", remedy}` — in the terminal, and from the routes —
292
+ never a blank graph. See `docs/using/carve` and issue #230 for the roadmap
293
+ (the post-emit diff, then Terraform as an estate member).
294
+
295
+ ### The walkthrough: `behold demo carve`
296
+
297
+ ```sh
298
+ npx @intentius/behold demo carve # no Docker, no cloud, no terraform binary
299
+ ```
300
+
301
+ Copies a half-migrated estate — a small chant project beside a Terraform one,
302
+ both describing the same AWS account — installs the chant it will shell, runs
303
+ `chant carve advise` over the copy, and opens the banded graph with a six-step
304
+ stepper on the panel's **Carve** tab:
305
+
306
+ 1. **Advise** — the bands, with what each one means.
307
+ 2. **Pick** — click a green card. The inspect pane shows the score arithmetic;
308
+ the step names the boundary the cut crosses.
309
+ 3. **Emit** — runs `chant carve emit --state --select <addr>` into
310
+ `app/carveout/` in the copy, then shows the emitted chant source and the
311
+ `chant lint` result.
312
+ 4. **Bridge** — runs `chant carve bridge` (never `--apply-rewrites`) and renders
313
+ the proposed data source, the rewired survivors and the patch.
314
+ 5. **Handoff** — the runbook's commands with copy buttons, and **not** a button:
315
+ `terraform state rm` and `terraform apply` change who owns a live resource,
316
+ so they stay yours to run. The panel says so.
317
+ 6. **Done** — the card is marked chant-owned at the observe position;
318
+ `terraform import` reverses all of it.
319
+
320
+ The Emit step reports `chant lint`, not `chant build`: chant#1637 means `build`
321
+ fails on the emitted bucket even though the advisor scored it 88, and the panel
322
+ links the reason. `example-carve/README.md` has the estate's full story, the
323
+ band table, and the offline/`--live` split.
324
+
325
+ behold writes only into the demo copy it made — `app/carveout/`, and nothing
326
+ else. Your Terraform is never edited; see AGENTS.md, "Invariant".
327
+
259
328
  ## Configuration — `.behold.json`
260
329
 
261
330
  An optional `.behold.json` in the served project's root is **behold's own**
@@ -284,6 +353,34 @@ render and the graph loads with no tier selected — the default for any project
284
353
  that doesn't opt in. There's no other tier config surface (not
285
354
  `chant.config.ts`, not an env var behold guesses the name of).
286
355
 
356
+ ### The hand-layout sidecar — `.behold/layout.json`
357
+
358
+ dagre places your nodes; you can move them. Drag a card, resize a containment
359
+ box, and the offsets are remembered per project + lens — in `localStorage`
360
+ first, and (when the served project is writable) in a `.behold/layout.json`
361
+ sidecar beside it, so a layout is shareable, reviewable in a diff, and honoured
362
+ by `behold export`:
363
+
364
+ ```json
365
+ { "version": 1, "lenses": { "components": { "src/api#Component": { "dx": 40, "dy": -25 } } } }
366
+ ```
367
+
368
+ This is the **only** file behold writes inside a served project. It stores
369
+ deltas, never absolute positions — the graph stays chant's and your layout sits
370
+ on top of it — and a delta whose node has left the estate is dropped silently.
371
+ `POST /api/layout` refuses politely in preview mode, during a static-export
372
+ capture, on a read-only directory, and above its size caps. `↺ layout` in the
373
+ graph clears the current lens on both tiers.
374
+
375
+ **Gitignore it.** `.behold.json` (above) is config and belongs in the repo;
376
+ `.behold/` is per-user state — one person's arrangement of the picture — so add
377
+ it to the served project's `.gitignore` unless you actually want to share and
378
+ review a layout:
379
+
380
+ ```gitignore
381
+ .behold/
382
+ ```
383
+
287
384
  ## Layout
288
385
 
289
386
  ```
@@ -309,6 +406,40 @@ and paints it with `layoutIr` + `renderSvg` (`src/render.ts`); the SPA inlines t
309
406
  SVG and wires click-inspect by `data-node-id` against the IR. pinhole's layout is
310
407
  dagre — pure JS, no native dependency.
311
408
 
409
+ Where an official mark exists, a node paints it instead of a generic glyph — a
410
+ Deployment gets the Kubernetes wheel-and-helm heptagon, a Kustomization the
411
+ Flux mark, a `Helm::Release` the Helm wheel. The corpus is vendored under
412
+ `web/icons/` (30 kubernetes/community SVGs, 3 cncf/artwork marks for
413
+ Flux/Argo/Helm, licensing in `THIRD_PARTY.md`) and mapped kind by kind in
414
+ `src/icon-packs.ts`; a kind with no official icon falls through to pinhole's
415
+ keyword heuristic rather than a wrong picture.
416
+
417
+ dagre's layout is a good first draft, not a final one: drag a card to move
418
+ it, grab a containment box's corner to resize it, and both survive a reload.
419
+ What persists is a delta — `{dx,dy}` for a card, `{dw,dh}` for a box, never
420
+ an absolute position — keyed by `behold.layout.<project>.<lens>`
421
+ (`web/layout-store.js`), so the graph stays chant's and the arrangement on
422
+ top of it is yours. `↺ layout` sits beside `⤢ fit` and shows up only once
423
+ something on the current lens is hand-placed.
424
+
425
+ Every JSON value the UI shows goes through one renderer (`web/json-view.js`):
426
+ 2-space pretty printed, objects and arrays collapsible (the first tier open,
427
+ anything deeper or wider than a dozen entries folded), long strings truncated
428
+ with an expander, and a `copy` on every node that yields that subtree's raw
429
+ JSON. Enter or Space toggles the focused node. It paints with `--fg` and
430
+ `--muted` and nothing else, so all 552 palettes keep it readable. That covers
431
+ the inspect pane's declared attributes, observed live state, drift pairs and
432
+ field ownership, the op log's JSON lines, and the payload behind an `/api`
433
+ error card.
434
+
435
+ Type splits by purpose: mono (system stacks — ui-monospace, SF Mono,
436
+ Cascadia, JetBrains, IBM Plex) carries node ids, ARNs, statuses and counts;
437
+ sans carries labels only. Colour comes from 552 Ghostty terminal palettes run
438
+ through an OKLCH-derived token pipeline (`src/theme.ts`), so a theme switch
439
+ re-derives the whole chrome, not just the graph — and a node whose drift
440
+ status just changed pulses once in the colour it became, off under
441
+ `prefers-reduced-motion`.
442
+
312
443
  ## Local development
313
444
 
314
445
  `just` lists everything. The core loop:
package/demos.json CHANGED
@@ -19,13 +19,37 @@
19
19
  },
20
20
  {
21
21
  "name": "flux-estate",
22
- "description": "A GitOps estate: a Flux control plane (GitRepository + Kustomizations) plus two app projects, served as one composed estate — cross-stack sourceRef edges, estate-wide drift, and the namespace-mismatch note on purpose (app-b uses targetNamespace). k3d + Flux, no cloud. scripts/estate-down.sh removes the cluster.",
22
+ "description": "A GitOps estate: a Flux control plane (GitRepository + Kustomizations) plus two app projects, served as one composed estate — cross-stack sourceRef edges, estate-wide drift, and the namespace join that reads app-b where the control plane's targetNamespace puts it. k3d + Flux, no cloud. scripts/estate-down.sh removes the cluster.",
23
23
  "source": "bundled",
24
24
  "dir": "example-flux-estate",
25
25
  "requires": ["docker", "k3d", "kubectl"],
26
26
  "setup": "bash scripts/estate-up.sh",
27
27
  "serve": { "env": "local", "dirs": ["control-plane", "app-a", "app-b"] }
28
28
  },
29
+ {
30
+ "name": "argo-estate",
31
+ "description": "The Argo CD mirror of flux-estate, declared only: an AppProject plus one Application per app project, served with the two apps as one composed estate — project edges from each Application to the AppProject it names, and each Application's destination namespace drawn as a box holding that app's objects. No cluster, no Argo install, no Docker.",
32
+ "source": "bundled",
33
+ "dir": "example-argo-estate",
34
+ "requires": [],
35
+ "serve": { "dirs": ["control-plane", "app-a", "app-b"] }
36
+ },
37
+ {
38
+ "name": "carve",
39
+ "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.",
40
+ "source": "bundled",
41
+ "dir": "example-carve",
42
+ "requires": [],
43
+ "serve": {
44
+ "carve": {
45
+ "report": "carve-report.json",
46
+ "from": "legacy-tf",
47
+ "state": "legacy-tf/terraform.tfstate",
48
+ "project": "app",
49
+ "out": "app/carveout"
50
+ }
51
+ }
52
+ },
29
53
  {
30
54
  "name": "fountain",
31
55
  "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.",