@intentius/behold 0.22.0 → 0.24.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 CHANGED
@@ -8,7 +8,8 @@ apply creds.
8
8
  ## The division of labour
9
9
 
10
10
  - **behold** serves the live, mixed-substrate graph (and, later, the deployment-lanes
11
- timeline). It reads; it never mutates.
11
+ timeline). It reads; every write it offers is a command it starts (an Op, a
12
+ `chant run`, a forge dispatch), and on a terragucci repo it starts none.
12
13
  - **chant's MCP** is where the real capabilities live. Prefer it over shelling.
13
14
  - Reads: `lifecycle-diff`, `lifecycle-snapshot`, `build`, `lint`.
14
15
  - Actions (delegated writes): `op-run` (start an `ApplyOp`/`ReconcileOp`),
@@ -19,18 +20,73 @@ apply creds.
19
20
  A directory with a `chant.workspace.json` is a chant workspace (chant ≥ 0.81.0,
20
21
  INTENTIUS/chant#2524). `behold serve <root>` serves it from chant's own member
21
22
  list (`chant workspace ls --json`, read contract 1; any other contract is
22
- refused), and reads each `chant` and `terraform` member through `chant
23
- workspace graph --member <name>`. A `choudoufu` member keeps behold's own
24
- reader, whose vocabulary (rungs, ownership, adoptable rows) chant's graph does
25
- not carry. Node ids are `<member>/<id>` with the declared member name, the same
23
+ refused), and reads each `chant`, `terraform` and `choudoufu` member through
24
+ `chant workspace graph --member <name>`. A `choudoufu` member takes its block
25
+ graph from it (the estate's own edges, each block's `file` and `line`) and
26
+ joins it onto choudoufu's `live-check` roster, which still names the cards,
27
+ the rungs and the cross-estate references: chant's graph is one node per
28
+ block, ownership is per instance, and chant carries no rung or reference
29
+ (src/choudoufu-workspace.ts, #465). The ids are the roster's, as before, so a
30
+ saved layout keeps matching. behold's own lexicon read of a choudoufu
31
+ directory stays for a loose view only. Node ids are `<member>/<id>` with the declared member name, the same
26
32
  form chant composes. A member chant cannot read is drawn as one
27
33
  `UnreadableMember` node carrying `attrs._unreadable: {code, message}` with
28
34
  chant's reason code. An `other` member is listed on startup and in doctor and
29
- draws nothing. `.behold.json`'s `members` are ignored beside a declaration;
35
+ draws nothing. A declaration with `members: []` (terragucci writes one for its
36
+ gates) declares nothing to draw, so serve, export and doctor read the
37
+ directory as they would without one, and say so (#489). `.behold.json`'s `members` are ignored beside a declaration;
30
38
  `behold doctor --fix` writes the declaration for an estate that has only them.
31
39
  Several directories on the command line (`behold serve a b c`) stay the loose
32
40
  view behold composes itself.
33
41
 
42
+ Every workspace read goes through one module, `src/workspace-reader.ts`: one
43
+ chant call per read, the document handed back as chant printed it, any
44
+ contract but 1 refused. chant's reader conformance suite
45
+ (`@intentius/chant/workspace/conformance`) runs against that module in `npm
46
+ test` (`src/workspace-reader-conformance.test.ts`, #468), so a new workspace
47
+ read is added to the reader and to the suite's `commands`, never shelled from
48
+ somewhere else.
49
+
50
+ `GET /api/workspace/why?member=<name>` or `?node=<member>/<id>` (#471) answers
51
+ why a member or a card is the way it is, from `chant workspace graph --intent
52
+ <region> --json` (chant 0.102.0 or newer writes its `why`): the decisions
53
+ covering it in chant's order, the agent runs and the commits behind it, and the
54
+ gaps chant names. The region is the member's declared directory or the node id,
55
+ never a path from the caller. It takes seconds on a small workspace and tens of
56
+ seconds on a large one, so the inspect pane reads it only on a click, and it is
57
+ a scheduled read: it holds a slot in the read budget, has the read deadline,
58
+ and stops when the request does. `serve --hud <url>` (or `BEHOLD_HUD_URL`)
59
+ links a proposed decision to its review in hud.
60
+
61
+ ## A terragucci estate, from its reports (#490)
62
+
63
+ `behold serve <repo> --terragucci <dir|s3://bucket/prefix|https://…>`
64
+ (`--terragucci-project <host/path>` when the bucket holds several projects
65
+ and none is the checkout's git remote) adds two reads, in
66
+ `src/terragucci-view.ts`:
67
+
68
+ - `GET /api/terragucci`: `{source, project, validation, read, reports, files,
69
+ estate?, roots[], cards{}, waiting[], unmatched}`. `roots[]` is every
70
+ Terraform root of the checkout with the newest `plan`, `drift` and `apply`
71
+ run that held it; a missing stage means no report holds that root, which is
72
+ not "no changes". `cards` maps a card id to its marks (`verdict: drift |
73
+ plan | wave`, terragucci's words, the instance addresses, the run). Nothing
74
+ sets `_status`: a report is never painted as live. `waiting[]` carries each
75
+ wave's roots, destroys, and the `command` a person runs. `estate` is
76
+ terragucci's own counts when the source has `estate.json`. A key such as a
77
+ run's `report` opens at `files` + the key. Refusals are `422 {code:
78
+ "terragucci-report"}`. `?fresh=1` skips the 30 s cache.
79
+ - `GET /api/terragucci/file?key=<key>`: one report file from the source,
80
+ sandboxed.
81
+
82
+ The reads are `src/terragucci-reports.ts` (index, each run's report, estate;
83
+ terragucci's JSON Schemas when `@intentius/terragucci` resolves, a structural
84
+ check otherwise; the package is looked up on every read and the compiled
85
+ schemas cached by its path and files' mtimes, #500) and the join is `src/terragucci-overlay.ts` (root path to
86
+ `attrs.root`, instance address to block path). An S3 source shells the
87
+ operator's `aws s3 cp <key> -`. There is no approve route, by design: hand the
88
+ operator `waiting[].command`.
89
+
34
90
  ## Getting a server
35
91
 
36
92
  ```sh
@@ -565,6 +621,25 @@ If a request would have behold write to a cloud or to source directly, it's wron
565
621
  behold shows truth and triggers Ops. Authority stays in the committed source and the
566
622
  executor.
567
623
 
624
+ The commands behold starts run with behold's own environment, so Deploy (`POST
625
+ /api/apply`, chant's local executor) applies with the credentials of whoever
626
+ started it. On a terragucci repo (a `terragucci.yml` in the served directory or
627
+ above it, up to the git root) behold starts nothing: every POST that deploys,
628
+ approves or starts a run answers `409` with `code: "terragucci"` and names the
629
+ pipeline (`src/terragucci-repo.ts`, #489), `/api/project` carries `terragucci`
630
+ so the page offers no write, and auto-sync declines. The boundary is an
631
+ allowlist (#500): every request whose method is not GET, HEAD or OPTIONS, on
632
+ any path, answers that `409` unless `TERRAGUCCI_ALLOWED_WRITES` names its
633
+ method and route, and that list holds only the exceptions below (the layout
634
+ sidecar, project open and reveal, demo open, refresh, and the four carve
635
+ steps). A new write route is refused on a terragucci repo until it is added
636
+ there. `src/terragucci-repo.test.ts` walks every non-GET route of the real
637
+ app (`app.routes`, built plain, with a carve report and with `--terragucci`)
638
+ and asserts each one is allowlisted or answers 409, and that every allowlist
639
+ entry is a registered route. `TERRAGUCCI_REFUSED_WRITES` only words the
640
+ refusal for known routes. The remedy names `npx terragucci approve wave-<k>
641
+ --plan <digest>`, the digest from the wave's gate card.
642
+
568
643
  ### The exceptions, and their exact size
569
644
 
570
645
  `POST /api/layout` (#228) writes **one** file in the served project:
@@ -607,6 +682,22 @@ and it must be an address the served report already ranks, the value that
607
682
  reaches the spawn's argv comes from a closed set read off disk. No cloud write,
608
683
  no Terraform mutation, no edit to anyone's chant source.
609
684
 
685
+ `behold export --publish s3://<bucket>/<prefix>/views/<name>` (#491) is the
686
+ one write to a cloud, and it is not a server route: an export, asked for it on
687
+ the command line, uploads the bundle it just wrote, signed with the AWS
688
+ credentials in the environment (src/s3-object.ts). `publishTarget` refuses
689
+ any destination that is not a `views/<name>` directory, terragucci's reserved
690
+ viewer prefix, so the bundle's `index.html` can never land on a page behold did
691
+ not make. `publishBundle` (src/export.ts, #500) uploads every other file
692
+ first, then `manifest.json`, then `index.html`. Snapshot names carry the first
693
+ 16 hex digits of their SHA-256 (`CONTENT_ADDRESSED`) and go up with
694
+ `Cache-Control: public, max-age=31536000, immutable`; everything else gets
695
+ `no-cache`. It GETs the previous `manifest.json` under the prefix before
696
+ uploading (no list call) and, after `index.html`, deletes the
697
+ `snapshots/<name>` files that manifest named and this export did not write.
698
+ Nothing outside the prefix is deleted. A refused delete stops the sweep with
699
+ a warning naming `s3:DeleteObject`, and the publish still succeeds.
700
+
610
701
  A choudoufu estate member (#366) adds no exception. A move there is one tag
611
702
  write through choudoufu's own `live-mv`, and behold never makes it: `GET
612
703
  /api/choudoufu/moves` reads a plan (`carve.json`) from inside a served member,
package/README.md CHANGED
@@ -145,6 +145,26 @@ Set the Worker name with `--name`, or edit `wrangler.jsonc`. Auth via
145
145
  Workers deploy). GitHub Pages, S3, nginx and Cloudflare Pages
146
146
  (`wrangler pages deploy .`) all work too.
147
147
 
148
+ A bundle is made to be opened by other people, so it carries the picture and
149
+ nothing about the machine that made it: no list of your recent projects, no
150
+ user name, and every absolute path reduced to its directory's name. Each card
151
+ of a Terraform root carries that root's whole source file (`attrs.source`);
152
+ `--no-source` leaves it out.
153
+
154
+ `--terragucci <src>` (with `--terragucci-project`, as for `serve`) captures the
155
+ terragucci marks too, for the view terragucci's estate job can publish beside
156
+ `estate.html`. The bundle's report links are relative to
157
+ `<prefix>/views/behold/` (`--reports-base` changes that), so uploaded there they
158
+ open the bucket's own report pages through the same presigned link or front
159
+ door. An export asked for marks it cannot read fails rather than publishing a
160
+ picture without them.
161
+
162
+ `--publish s3://<bucket>/<prefix>/views/<name>` uploads the bundle there too,
163
+ each file with its content type, signed with the AWS credentials in the
164
+ environment (no SDK, no aws CLI needed). It writes only under a `views/<name>`
165
+ directory, terragucci's reserved viewer prefix, so a bundle's `index.html`
166
+ never lands on a page behold did not make.
167
+
148
168
  ## Try it: your first apply, no cloud account
149
169
 
150
170
  The bundled `example-writes` is one S3 bucket. `serve --local` boots *that
@@ -202,21 +222,33 @@ including the exact commands and what each state looks like over the API:
202
222
 
203
223
  ## Read-only core, delegated gated writes (the invariant)
204
224
 
205
- **behold never mutates anything itself.**
225
+ **behold writes nothing to a cloud or to your source itself.** Every write it
226
+ offers is a command it starts for you, run by a tool you already trust.
206
227
 
207
228
  - The vizes only read (`chant graph`, snapshots, Temporal history).
208
- - Actions don't mutate directly. Sync starts your `ApplyOp`; Adopt starts your
209
- `ReconcileOp` (opens a PR a human merges). behold *triggers* Ops you committed,
210
- running on your executor. It holds no apply creds.
211
- - Two write gestures, both human-confirmed: **Apply** (gate signal) and **Open PR**
212
- (merge). Authority stays in your source and your worker, never in behold.
229
+ - Sync starts your committed `ApplyOp`; Adopt starts your `ReconcileOp` (opens a
230
+ PR a human merges). Those run on your executor.
231
+ - Deploy on a project with no committed ApplyOp runs `chant run <component>
232
+ --components` on chant's local executor, as a child of behold. That child has
233
+ behold's environment, so it applies with the credentials of whoever started
234
+ behold: to Deploy from behold, that environment needs write roles. Reads
235
+ alone need only read roles.
236
+ - An env `.behold.json` designates to a forge is never applied from the laptop;
237
+ Deploy dispatches the forge's workflow through your `gh` instead.
238
+ - `behold export --publish s3://…/views/<name>` uploads the bundle it just made.
239
+ It is the one write behold makes to a cloud, and only when an export is
240
+ asked for it.
241
+ - A terragucci repo (a `terragucci.yml` in the served directory or above it) is
242
+ applied by its pipeline only. behold refuses every write there (Deploy, Op
243
+ runs, gate approvals, pipeline dispatch, rollback) with `409 terragucci` and
244
+ offers none of them on the page.
213
245
 
214
246
  ## Why a Node service (not an edge function)
215
247
 
216
248
  The live path (`chant graph --live --overlay`, `chant lifecycle plan`) shells
217
249
  `kubectl`/`aws`/`az`/the Temporal client and holds cloud creds. That needs a real
218
- process, so behold is a Node service you run where your creds live. Read-only means it only needs **read**
219
- roles (describe/list), so it's least-privilege to run.
250
+ process, so behold is a Node service you run where your creds live. Viewing needs only **read**
251
+ roles (describe/list); a Deploy from behold applies with whatever credentials its environment holds.
220
252
 
221
253
  ## Agent-drivable, on chant's MCP
222
254
 
@@ -359,10 +391,42 @@ workspace block keys them, so the two show the same set. Standalone, a gate's
359
391
  button says who chant will record (the user running behold) before the click.
360
392
  Every approve route refuses in preview mode.
361
393
 
394
+ In a declared workspace, the inspect pane of a member's box or a card has a
395
+ "why" section. "Read why" asks `chant workspace graph --intent <member dir or
396
+ node id> --json` (chant 0.102.0 or newer in the workspace root) and shows the
397
+ decisions covering it, in chant's order, with the agent runs and commits behind
398
+ it. Nothing is read until the click: on chant's own repository one member takes
399
+ about 30 s, a small workspace about 1 s. While it reads the section says
400
+ "reading…" with a Stop button, and picking something else stops the read.
401
+ Start behold with `--hud <url>` (or `BEHOLD_HUD_URL`) and a proposed decision
402
+ links to its review in hud; otherwise it is named by id. Framed, the section is
403
+ drawn the same, and clicks on the graph still go to the host.
404
+
362
405
  A host that already watches the workspace tells behold it changed with
363
406
  `POST /api/refresh?notify=1`, instead of running behold with `--poll` beside
364
407
  it. behold drops its cached reads and every open page re-pulls.
365
408
 
409
+ ## A terragucci estate, from its reports: `--terragucci`
410
+
411
+ ```sh
412
+ npx @intentius/behold serve . --terragucci s3://acme-terragucci-reports # or a synced dir, or an https address
413
+ ```
414
+
415
+ In a [terragucci](https://intentius.io/terragucci/) repo, behold draws every
416
+ root from the checkout and marks each card with what terragucci's newest runs
417
+ found: the newest `tf-drift` and `tf-plan` report per root (one GET of
418
+ `index.json`, then one of each run's `report.json`), and the waves waiting for
419
+ an approval. Every mark is dated by its run and links its report and job
420
+ ("drift found 06:04 UTC, 4h ago"); none is a fill, and nothing is read from a
421
+ cloud. Counts and waiting waves come from `estate.json` when the source has
422
+ one, so behold agrees with terragucci's estate page and links it. A waiting
423
+ wave offers `npx terragucci approve wave-<k> --plan <digest>` to copy; there is
424
+ no approve button, and every behold write refuses on a terragucci repo.
425
+ Documents are checked against terragucci's JSON Schemas when
426
+ `@intentius/terragucci` is installed, structurally otherwise. Full page:
427
+ docs `using/terragucci`. `example-terragucci-reports/` is a bucket for
428
+ terragucci's own example, written by terragucci's code.
429
+
366
430
  ## Terraform carve-out: `behold carve <report.json>`
367
431
 
368
432
  `chant carve advise` ranks a Terraform estate by **peelability**: how cleanly
package/demos.json CHANGED
@@ -65,7 +65,7 @@
65
65
  },
66
66
  {
67
67
  "name": "terraform-estate",
68
- "description": "A multi-root Terraform estate, declared only: two roots applied separately (envs/prod and platform) plus the shared module both are built on — 37 cards boxed by root, module calls expanded into their own contents, and the two look-alike versions.tf files told apart. Shows what behold does NOT infer as clearly as what it draws: platform declares the KMS key and prod reads it back by alias, and no edge is drawn between them, because both ends carry the same unresolved interpolation (#381). Needs chant's terraform lexicon beside behold, which behold does not install: npm install --no-save @intentius/chant-lexicon-terraform @cdktn/hcl2json (the refusal names the exact line). No cloud, no Docker, no terraform binary.",
68
+ "description": "A multi-root Terraform estate, declared only: two roots applied separately (envs/prod and platform) plus the shared module both are built on — 37 cards boxed by root, module calls expanded into their own contents, and the two look-alike versions.tf files told apart. Shows what behold does NOT infer as clearly as what it draws: platform declares the KMS key and prod reads it back by alias, and no edge is drawn between them, because both ends carry the same unresolved interpolation (#381). Needs chant's terraform lexicon beside behold, which behold does not install: npm install --no-save @intentius/chant-lexicon-terraform@^0.121.0 @cdktn/hcl2json@^0.24.0 (without them the demo refuses with that line); the copy is linked to the lexicon behold resolved, since chant reads a workspace's pinned kinds from beside the workspace. No cloud, no Docker, no terraform binary.",
69
69
  "source": "bundled",
70
70
  "dir": "example-terraform-estate",
71
71
  "requires": [],
@@ -89,7 +89,7 @@
89
89
  },
90
90
  {
91
91
  "name": "choudoufu-estate",
92
- "description": "A tag-owned estate: four choudoufu estates (an OpenTofu fork whose ownership is two AWS tags) served composed — the terralith applied to a scratch floci, three team estates whose declared resources are the monolith's (every card `owned by` it, dashed to its card), a cross-estate data-source edge, and a move plan whose `live-mv` lines you run by hand while behold previews with -dry-run and reads the receipt. A card's colour is ownership, not \"nothing changed\": attribute drift is a separate plan read, opt-in through ⌘K → \"Re-check live with plan\". Docker + choudoufu (0.16.0+, or a build from main) on PATH; provider download on first init. scripts/choudoufu-down.sh removes the container.",
92
+ "description": "A tag-owned estate: four choudoufu estates (an OpenTofu fork whose ownership is two AWS tags) served composed — the terralith applied to a scratch floci, three team estates whose declared resources are the monolith's (every card `owned by` it, dashed to its card), a cross-estate data-source edge, and a move plan whose `live-mv` lines you run by hand while behold previews with -dry-run and reads the receipt. A card's colour is ownership, not \"nothing changed\": attribute drift is a separate plan read, opt-in through ⌘K → \"Re-check live with plan\". Docker + choudoufu (0.16.0+, or a build from main) on PATH, and chant's terraform lexicon beside behold (it supplies the choudoufu kind; the copy is linked to it); provider download on first init. scripts/choudoufu-down.sh removes the container.",
93
93
  "source": "bundled",
94
94
  "dir": "example-choudoufu-estate",
95
95
  "requires": [