@intentius/behold 0.8.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.
- package/AGENTS.md +85 -0
- package/README.md +123 -2
- package/demos.json +17 -1
- package/dist/cli.js +2522 -294
- package/example-argo-estate/README.md +43 -18
- package/example-argo-estate/app-a/chant.config.ts +10 -2
- package/example-argo-estate/app-a/package.json +2 -2
- package/example-argo-estate/app-b/chant.config.ts +11 -2
- package/example-argo-estate/app-b/package.json +2 -2
- package/example-argo-estate/control-plane/chant.config.ts +20 -5
- package/example-argo-estate/control-plane/package.json +2 -2
- package/example-argo-estate/package-lock.json +17 -17
- package/example-carve/README.md +194 -0
- package/example-carve/app/chant.config.ts +6 -0
- package/example-carve/app/package-lock.json +1075 -0
- package/example-carve/app/package.json +13 -0
- package/example-carve/app/src/carved.ts +30 -0
- package/example-carve/app/tsconfig.json +1 -0
- package/example-carve/carve-report.json +872 -0
- package/example-carve/legacy-tf/cdn.tf +23 -0
- package/example-carve/legacy-tf/compute.tf +63 -0
- package/example-carve/legacy-tf/floci-override.tf.disabled +61 -0
- package/example-carve/legacy-tf/modules/cdn/main.tf +72 -0
- package/example-carve/legacy-tf/naming.tf +10 -0
- package/example-carve/legacy-tf/network.tf +119 -0
- package/example-carve/legacy-tf/observability.tf +18 -0
- package/example-carve/legacy-tf/outputs.tf +16 -0
- package/example-carve/legacy-tf/storage.tf +33 -0
- package/example-carve/legacy-tf/terraform.tfstate +602 -0
- package/example-carve/legacy-tf/versions.tf +40 -0
- package/example-flux-estate/README.md +9 -4
- package/example-flux-estate/app-a/package.json +2 -2
- package/example-flux-estate/app-a/src/app.ts +2 -1
- package/example-flux-estate/app-b/chant.config.ts +4 -3
- package/example-flux-estate/app-b/package.json +2 -2
- package/example-flux-estate/app-b/src/app.ts +5 -3
- package/example-flux-estate/control-plane/package.json +2 -2
- package/example-flux-estate/control-plane/src/flux.ts +4 -2
- package/example-flux-estate/package-lock.json +17 -17
- package/example-k8s/package-lock.json +18 -18
- package/example-k8s/package.json +3 -3
- package/example-writes/package-lock.json +14 -14
- package/example-writes/package.json +3 -3
- package/package.json +8 -6
- package/web/app.js +714 -57
- package/web/carve-steps.js +610 -0
- package/web/carve-steps.test.js +233 -0
- package/web/demos.js +71 -0
- package/web/demos.test.js +83 -0
- package/web/index.html +93 -1
- package/web/json-view.js +334 -0
- package/web/json-view.test.js +218 -0
- package/web/layout-store.js +164 -4
- package/web/layout-store.test.js +226 -1
- package/web/panel.js +28 -0
- package/web/theme.js +57 -1
package/AGENTS.md
CHANGED
|
@@ -25,6 +25,16 @@ 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,
|
|
@@ -50,6 +60,49 @@ graph.
|
|
|
50
60
|
3. **inspect** — a node's `sourceLoc.file` is the typed source that declared it;
|
|
51
61
|
edit there to change the estate (chant is the source of truth, not behold).
|
|
52
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
|
+
|
|
53
106
|
## The act loop (delegated, never direct)
|
|
54
107
|
|
|
55
108
|
behold does not apply. To change the estate:
|
|
@@ -69,3 +122,35 @@ and saga rollback. There is no behold endpoint that mutates the cloud.
|
|
|
69
122
|
If a request would have behold write to a cloud or to source directly, it's wrong.
|
|
70
123
|
behold shows truth and triggers Ops. Authority stays in the committed source and the
|
|
71
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
|
@@ -31,8 +31,14 @@ 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
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
|
|
35
|
-
|
|
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.
|
|
36
42
|
|
|
37
43
|
Already have a chant project?
|
|
38
44
|
|
|
@@ -266,6 +272,59 @@ behold shells the **project's own** chant (resolved from the project's
|
|
|
266
272
|
`@intentius/chant ^0.18.1` or later for the live overlay (`graph --live` observed
|
|
267
273
|
nothing before that fix).
|
|
268
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
|
+
|
|
269
328
|
## Configuration — `.behold.json`
|
|
270
329
|
|
|
271
330
|
An optional `.behold.json` in the served project's root is **behold's own**
|
|
@@ -294,6 +353,34 @@ render and the graph loads with no tier selected — the default for any project
|
|
|
294
353
|
that doesn't opt in. There's no other tier config surface (not
|
|
295
354
|
`chant.config.ts`, not an env var behold guesses the name of).
|
|
296
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
|
+
|
|
297
384
|
## Layout
|
|
298
385
|
|
|
299
386
|
```
|
|
@@ -319,6 +406,40 @@ and paints it with `layoutIr` + `renderSvg` (`src/render.ts`); the SPA inlines t
|
|
|
319
406
|
SVG and wires click-inspect by `data-node-id` against the IR. pinhole's layout is
|
|
320
407
|
dagre — pure JS, no native dependency.
|
|
321
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
|
+
|
|
322
443
|
## Local development
|
|
323
444
|
|
|
324
445
|
`just` lists everything. The core loop:
|
package/demos.json
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
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
|
|
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"],
|
|
@@ -34,6 +34,22 @@
|
|
|
34
34
|
"requires": [],
|
|
35
35
|
"serve": { "dirs": ["control-plane", "app-a", "app-b"] }
|
|
36
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
|
+
},
|
|
37
53
|
{
|
|
38
54
|
"name": "fountain",
|
|
39
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.",
|