@intentius/behold 0.23.0 → 0.25.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
@@ -78,14 +78,101 @@ and none is the checkout's git remote) adds two reads, in
78
78
  "terragucci-report"}`. `?fresh=1` skips the 30 s cache.
79
79
  - `GET /api/terragucci/file?key=<key>`: one report file from the source,
80
80
  sandboxed.
81
+ - `GET /api/terragucci/timeline?root=<path>&limit=<n>` (#506): terragucci's
82
+ audit record (`audit.jsonl` at the top of the prefix, one GET) as `{source,
83
+ project, validation, read, record: {key, present}, absent?, files, root?,
84
+ total, limit, entries[]}`, newest first, 200 by default and 1000 at most.
85
+ An entry is `{id, kind, at, who, what, wave?, digest, result, roots[], run:
86
+ {commit?, job_url?, run_id?, pull_request?, report?}, evidence, detail?}`;
87
+ `run.report` opens at `files` + the key. An approval takes the roots and
88
+ report of its wave's other entries (same gate and digest). No
89
+ `audit.jsonl` is `present: false` plus an `absent` sentence, never an
90
+ empty history; a bad line is the `422 terragucci-report` refusal naming it.
91
+ `src/terragucci-timeline.ts` holds the read and the check (terragucci's
92
+ `dist/audit.schema.json` when it resolves, structural otherwise). The SPA
93
+ draws it in the Terragucci tab and, filtered to the card's root, in the
94
+ inspect pane (`web/terragucci-timeline.js`); an export carries it as one
95
+ snapshot and does not fail on it.
96
+ - `GET /api/terragucci/lifecycle` (#505): the repo's `chant/lifecycle` lane,
97
+ `{branch, found, via, origin?, fetch: {ok, error?}, commit?: {sha,
98
+ committed}, read: {at}, gates[], locks[], note}`. `gates[]` is one entry per
99
+ `wave-<k>` in `_gates/tf-apply.jsonl` and `_gates/tf-apply/applied.jsonl`:
100
+ `state` (`waiting | expired | approved | applied | failed`), `digest`,
101
+ `pending`, `approval` (by, at, signed), `applied`, the kept reports under
102
+ `_gates/tf-apply/wave-<k>/`, and `command` when it waits. `locks[]` is
103
+ `_locks/tf-apply.json`, one per root (`pr, by, at, head, via?, stage?`).
104
+ Every gate and lock carries `source: {commit, committed, path}`, the newest
105
+ commit of the branch that changed its file. The branch is fetched from the
106
+ checkout's origin URL into `<tmpdir>/behold-lifecycle-<hash of the URL>`, a
107
+ bare repo behold owns (`assertScratch`), with the person's own git
108
+ credentials and no prompt; the checkout is never fetched into and no ref of
109
+ it moves. A failed fetch falls back to the checkout's own
110
+ `refs/remotes/origin/chant/lifecycle`, read-only (`via: "checkout-ref"`,
111
+ `fetch.error`); a remote without the branch is `found: false, via:
112
+ "fetch"`, not an error. 30 s cache, `?fresh=1` skips it. Reader:
113
+ `src/terragucci-lifecycle.ts`.
114
+ - `GET /api/terragucci/events` (#505): SSE. While at least one page holds it
115
+ open, a timer (`--terragucci-poll <secs>`, default 30, 0 off) asks
116
+ whether `index.json` changed (`If-None-Match` with the last ETag for S3 and
117
+ http, mtime for a directory) and fetches `chant/lifecycle` again. Events:
118
+ `hello {pollSecs}`, `reports {at, tag}` (the page reads `/api/terragucci`
119
+ again), `lifecycle <the whole answer>` when the read differs from the last,
120
+ and `polled {at, reports, lifecycle}` after every tick. The last page
121
+ leaving stops the timer (`src/terragucci-poll.ts`). Neither route writes,
122
+ approves, locks or unlocks.
123
+ - `GET /api/terragucci/progress` (#511): a choudoufu wave's progress per
124
+ resource from the run view of the newest commit `index.json` names a
125
+ `tf-apply` run of (one GET of `index.json`, one of
126
+ `<project dir>/runs/<commit>/run.json`, the directory taken from the index
127
+ row's own path). `{source, kind, project, validation, read, run: {commit,
128
+ key, present, updated?, html?} | null, absent?, files, watching, waves[],
129
+ cards{}, unmatched[]}`. A wave is `{number, state, gate, read, counts,
130
+ resources[]}`, a resource `{root, address, action, status: done | in-flight
131
+ | waiting | not-applied, done_at?, card?}`. `cards` is keyed by the same ids
132
+ as `/api/terragucci`'s marks (root path to `attrs.root`, instance to block),
133
+ one entry per wave with the wave's records `read` time and the card's
134
+ status (not applied, else in flight, else waiting, else done). It is a
135
+ dated corner tag, never a fill. No `run.json` is `run.present: false` plus
136
+ an `absent` sentence. `watching` is true while a wave is applying or a
137
+ resource is in flight or waiting; only then does the poll ask whether that
138
+ `run.json` changed (`If-None-Match`, mtime for a directory) and push a
139
+ `progress` event with the whole answer, and `polled` carries `progress:
140
+ {asked, key?, changed?, error?}`. Checked against `dist/run.schema.json`
141
+ when `@intentius/terragucci` resolves, structurally otherwise; reader
142
+ `src/terragucci-progress.ts`, page `web/terragucci-progress.js`.
81
143
 
82
144
  The reads are `src/terragucci-reports.ts` (index, each run's report, estate;
83
145
  terragucci's JSON Schemas when `@intentius/terragucci` resolves, a structural
84
- check otherwise) and the join is `src/terragucci-overlay.ts` (root path to
146
+ check otherwise; the package is looked up on every read and the compiled
147
+ schemas cached by its path and files' mtimes, #500) and the join is `src/terragucci-overlay.ts` (root path to
85
148
  `attrs.root`, instance address to block path). An S3 source shells the
86
149
  operator's `aws s3 cp <key> -`. There is no approve route, by design: hand the
87
150
  operator `waiting[].command`.
88
151
 
152
+ A control repo (#509), one whose terragucci config has `projects:` and whose
153
+ checkout declares no Terraform root (`controlRepo` in
154
+ `src/terragucci-estate.ts`), gets one more read, and `/api/project` carries
155
+ `terragucciControl: {config, projects}`:
156
+
157
+ - `GET /api/terragucci/estate`: `{source, kind, config, validation: {estate,
158
+ run}, read, estate: {key, html, generated}, gets, projects[], edges[],
159
+ files}`. It reads `estate.json` and each project's newest run view,
160
+ `<project>/runs/<commit>/run.json`, one GET each (the commit is the
161
+ project's `run_view.commit`, else its `apply.commit`). A project is
162
+ `{project, status, index?, counts: {drifted, failed, waiting, dated},
163
+ waiting[], run? | missing? | refused?, checkout?, configured}`. Every
164
+ figure carries `dated: {key, at}`: estate.json's `generated`, or the run
165
+ view's `updated`. `waiting[].command` is the run view's approve line when it
166
+ holds that wave. `edges[]` is `{from, to, cross}`, `to` reading `from`'s
167
+ state; a cross edge matches a root's `external` read to another project's
168
+ root `state`, as terragucci's `estateGraph` does. A malformed run view is
169
+ `refused: {code: "terragucci-report"}` on its project only; a missing or
170
+ malformed `estate.json` is a 422. Not a control repo: 404 `code:
171
+ "terragucci-not-control"`. Run views are checked against
172
+ `@intentius/terragucci`'s `dist/run.schema.json` when it resolves, a
173
+ structural check otherwise. The page (`web/terragucci-estate.js`) draws it
174
+ in a layer over the graph area.
175
+
89
176
  ## Getting a server
90
177
 
91
178
  ```sh
@@ -626,9 +713,18 @@ started it. On a terragucci repo (a `terragucci.yml` in the served directory or
626
713
  above it, up to the git root) behold starts nothing: every POST that deploys,
627
714
  approves or starts a run answers `409` with `code: "terragucci"` and names the
628
715
  pipeline (`src/terragucci-repo.ts`, #489), `/api/project` carries `terragucci`
629
- so the page offers no write, and auto-sync declines. A new POST route goes in
630
- `TERRAGUCCI_REFUSED_WRITES` or in the test's list of routes that write nothing
631
- outside behold; `src/terragucci-repo.test.ts` fails until it is in one.
716
+ so the page offers no write, and auto-sync declines. The boundary is an
717
+ allowlist (#500): every request whose method is not GET, HEAD or OPTIONS, on
718
+ any path, answers that `409` unless `TERRAGUCCI_ALLOWED_WRITES` names its
719
+ method and route, and that list holds only the exceptions below (the layout
720
+ sidecar, project open and reveal, demo open, refresh, and the four carve
721
+ steps). A new write route is refused on a terragucci repo until it is added
722
+ there. `src/terragucci-repo.test.ts` walks every non-GET route of the real
723
+ app (`app.routes`, built plain, with a carve report and with `--terragucci`)
724
+ and asserts each one is allowlisted or answers 409, and that every allowlist
725
+ entry is a registered route. `TERRAGUCCI_REFUSED_WRITES` only words the
726
+ refusal for known routes. The remedy names `npx terragucci approve wave-<k>
727
+ --plan <digest>`, the digest from the wave's gate card.
632
728
 
633
729
  ### The exceptions, and their exact size
634
730
 
@@ -678,7 +774,29 @@ the command line, uploads the bundle it just wrote, signed with the AWS
678
774
  credentials in the environment (src/s3-object.ts). `publishTarget` refuses
679
775
  any destination that is not a `views/<name>` directory, terragucci's reserved
680
776
  viewer prefix, so the bundle's `index.html` can never land on a page behold did
681
- not make.
777
+ not make. `publishBundle` (src/export.ts, #500) uploads every other file
778
+ first, then `manifest.json`, then `index.html`. Snapshot names carry the first
779
+ 16 hex digits of their SHA-256 (`CONTENT_ADDRESSED`) and go up with
780
+ `Cache-Control: public, max-age=31536000, immutable`; everything else gets
781
+ `no-cache`. It GETs the previous `manifest.json` under the prefix before
782
+ uploading (no list call) and, after `index.html`, deletes the
783
+ `snapshots/<name>` files that manifest named and this export did not write.
784
+ Nothing outside the prefix is deleted. A refused delete stops the sweep with
785
+ a warning naming `s3:DeleteObject`, and the publish still succeeds.
786
+
787
+ Since #510 the bundle goes to `<prefix>/<commit>/` (the checkout's HEAD, or
788
+ `--commit`), with publishBundle's order and sweep one level down, and
789
+ `src/publish-snapshots.ts` then writes, in order, `history.json` (newest
790
+ first, `{commit, at, project, generated}`, one GET of the old one),
791
+ `latest.json` (`{commit}`) and the root `index.html`, a no-cache page that
792
+ opens `<latest>/index.html`, so old links keep working. `--keep <n>` (30)
793
+ drops older commits from `history.json` and then deletes their files as each
794
+ commit's `manifest.json` lists them (`files`, which runExport now writes,
795
+ plus `keyToFile`), best effort with the same `s3:DeleteObject` warning. A
796
+ view from before the layout keeps its files; only its `index.html` is
797
+ replaced. With `--publish` the default `--reports-base` is `../../../`. The
798
+ view's history picker (`web/history-picker.js`) reads `../history.json` and
799
+ only navigates.
682
800
 
683
801
  A choudoufu estate member (#366) adds no exception. A move there is one tag
684
802
  write through choudoufu's own `live-mv`, and behold never makes it: `GET
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@^0.102.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.",
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": [],