@intentius/behold 0.17.0 → 0.19.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 +757 -26
- package/LICENSE +190 -0
- package/README.md +251 -187
- package/THIRD_PARTY.md +75 -0
- package/demos.json +80 -9
- package/dist/cli.js +5479 -1673
- package/example-argo-estate/package-lock.json +6 -6
- package/example-carve/app/package-lock.json +3 -3
- package/example-choudoufu-estate/.behold.json +8 -0
- package/example-choudoufu-estate/README.md +48 -0
- package/example-choudoufu-estate/monolith/behaviour.live.json +495 -0
- package/example-choudoufu-estate/monolith/carve.json +12 -0
- package/example-choudoufu-estate/monolith/main.tf +127 -0
- package/example-choudoufu-estate/scripts/choudoufu-down.sh +8 -0
- package/example-choudoufu-estate/scripts/choudoufu-up.sh +62 -0
- package/example-choudoufu-estate/team-a/behaviour.live.json +179 -0
- package/example-choudoufu-estate/team-a/main.tf +58 -0
- package/example-choudoufu-estate/team-b/behaviour.live.json +179 -0
- package/example-choudoufu-estate/team-b/main.tf +73 -0
- package/example-choudoufu-estate/team-c/behaviour.live.json +179 -0
- package/example-choudoufu-estate/team-c/main.tf +51 -0
- package/example-flux-estate/package-lock.json +6 -6
- package/example-k8s/package-lock.json +6 -6
- package/example-terraform-estate/README.md +109 -0
- package/example-terraform-estate/backends/backend.s3.tf +10 -0
- package/example-terraform-estate/envs/dev/README.md +5 -0
- package/example-terraform-estate/envs/prod/data.tf +18 -0
- package/example-terraform-estate/envs/prod/network.tf +80 -0
- package/example-terraform-estate/envs/prod/outputs.tf +13 -0
- package/example-terraform-estate/envs/prod/provider.tf +10 -0
- package/example-terraform-estate/envs/prod/services.tf +21 -0
- package/example-terraform-estate/envs/prod/storage.tf +31 -0
- package/example-terraform-estate/envs/prod/variables.tf +17 -0
- package/example-terraform-estate/envs/prod/versions.tf +10 -0
- package/example-terraform-estate/modules/service/main.tf +58 -0
- package/example-terraform-estate/modules/service/outputs.tf +7 -0
- package/example-terraform-estate/modules/service/variables.tf +16 -0
- package/example-terraform-estate/modules/service/versions.tf +10 -0
- package/example-terraform-estate/platform/iam.tf +40 -0
- package/example-terraform-estate/platform/kms.tf +16 -0
- package/example-terraform-estate/platform/outputs.tf +7 -0
- package/example-terraform-estate/platform/variables.tf +10 -0
- package/example-terraform-estate/platform/versions.tf +10 -0
- package/example-writes/package-lock.json +3 -3
- package/licenses/cncf-artwork-LICENSE.md +13 -0
- package/licenses/iTerm2-Color-Schemes-LICENSE +31 -0
- package/licenses/kubernetes-icons-LICENSE +201 -0
- package/package.json +25 -7
- package/web/app.js +1309 -41
- package/web/behaviour-scale.js +380 -0
- package/web/behaviour-scale.test.js +307 -0
- package/web/cross-env.js +58 -0
- package/web/cross-env.test.js +61 -0
- package/web/index.html +62 -2
- package/web/pending.js +67 -0
- package/web/pending.test.js +62 -0
- package/web/read-cost.js +49 -0
- package/web/read-cost.test.js +76 -0
- package/web/refresh-queue.js +26 -0
- package/web/refresh-queue.test.js +54 -0
package/AGENTS.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Driving behold (for agents)
|
|
2
2
|
|
|
3
3
|
behold is a **read-only control plane** over a chant estate, with **delegated,
|
|
4
|
-
gated** writes. As an agent you drive it the same way a human does
|
|
4
|
+
gated** writes. As an agent you drive it the same way a human does, and the
|
|
5
5
|
mutating capabilities are chant's MCP Op tools, not behold's, so nothing here holds
|
|
6
6
|
apply creds.
|
|
7
7
|
|
|
@@ -25,6 +25,12 @@ 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
|
+
In a checkout, `behold demo --list` prints a second block under the bundled
|
|
29
|
+
one: the eleven workbench entries from `workbench.json` (this repo's own
|
|
30
|
+
catalog of the internal estates behold is developed against), the same
|
|
31
|
+
`behold demo <entry>` load, `just example name="<entry>"` to serve one, and
|
|
32
|
+
"The workbench catalog" below for what each is and what it boots.
|
|
33
|
+
|
|
28
34
|
A running server offers the same catalog over HTTP (#268): `GET /api/demos`
|
|
29
35
|
lists every bundled demo with `{name, description, requires, satisfiable,
|
|
30
36
|
reason?, fetches, repo?, target, loaded}` — `satisfiable` is doctor's PATH probe
|
|
@@ -37,7 +43,7 @@ preview-locked, and one load runs at a time (409 otherwise).
|
|
|
37
43
|
|
|
38
44
|
## The read loop
|
|
39
45
|
|
|
40
|
-
0. **discover
|
|
46
|
+
0. **discover.** GET `/api` lists every route with a one-line description,
|
|
41
47
|
plus the server's version and a link back to this guide. Before the server
|
|
42
48
|
exists (or when a route answers with an error you'd have to guess at), run
|
|
43
49
|
`npx @intentius/behold doctor <dir> --json`: a read-only diagnosis of the
|
|
@@ -46,7 +52,7 @@ preview-locked, and one load runs at a time (409 otherwise).
|
|
|
46
52
|
substrate readiness and committed Ops. Each check is
|
|
47
53
|
`{name, status: pass|warn|fail, detail, fix}`; the process exits non-zero
|
|
48
54
|
iff something failed. It starts no server and changes nothing.
|
|
49
|
-
1. **observe
|
|
55
|
+
1. **observe.** GET `/api/graph` (JSON: `{ ir, svg, meta }`). The mixed graph
|
|
50
56
|
of the project, every node with `id`/`kind`/`lexicon`/`attrs`/`sourceLoc`. Drift
|
|
51
57
|
status, when present, is `attrs._status` (`good`=managed, `warn`=foreign,
|
|
52
58
|
`accent`=pending, `neutral`=unobserved, `runtime`=runtime child). With
|
|
@@ -61,7 +67,26 @@ preview-locked, and one load runs at a time (409 otherwise).
|
|
|
61
67
|
itself carries an address (chant#2045); `originSource: "inferred"` means
|
|
62
68
|
behold read the id's spelling and nothing more, and a `forge: "unknown"` id
|
|
63
69
|
is never resolved to a link on behold's guess.
|
|
64
|
-
`?
|
|
70
|
+
`?stacks=1` is the apply-order lens (#426): the cross-stack ordering chant
|
|
71
|
+
computes at synthesis and prints from `chant graph --stacks --json`, which
|
|
72
|
+
behold read nowhere and re-derived a subset of. One card per stack (a
|
|
73
|
+
lexicon partition), one box per wave, edges consumer to producer;
|
|
74
|
+
`meta.stacks` carries `order`, `waves` and `cycles` verbatim. It is
|
|
75
|
+
source-only (no env, no credentials, no substrate) and asks chant for the
|
|
76
|
+
project ROOT, not `graphPath`, because `runStackGraph` only sees
|
|
77
|
+
`ops/*.op.ts` from there (measured: `example-writes` answers `[aws,
|
|
78
|
+
temporal]` from `.` and `[aws]` from `src`). Single project only: an estate
|
|
79
|
+
is refused with `422 stacks-estate`, since `composeStacks` pools members
|
|
80
|
+
into one bucket per bare lexicon name and two members' `aws` stacks are not
|
|
81
|
+
one stack. A cycle is NOT a refusal — chant exits 1 with valid JSON and the
|
|
82
|
+
entangled stacks absent from `order`/`waves`, so they are drawn unwaved in
|
|
83
|
+
their own box and the note says so; nothing invents a wave number for a
|
|
84
|
+
stack chant could not order. `/api/project`'s `stacksCapable` gates the
|
|
85
|
+
stop. Note that `chant graph --stacks --json` is malformed today on two of
|
|
86
|
+
chant's own examples (an edge with no `from`, a `null` in `order` and in a
|
|
87
|
+
wave); `sanitizeStackGraph` treats `nodes` as the roster and drops anything
|
|
88
|
+
named nowhere else.
|
|
89
|
+
`?ops=1` is the ops lens: the project's declared Ops as a phase track, read
|
|
65
90
|
from each emitted `dist/ops/<name>/op.json`, with the current run painted
|
|
66
91
|
over it (`meta.run`, `meta.gate`); `/api/ops/<name>/status` reads that Op's
|
|
67
92
|
durable run status and pending gate (`chant run status --temporal`). A
|
|
@@ -71,14 +96,222 @@ preview-locked, and one load runs at a time (409 otherwise).
|
|
|
71
96
|
every rule, verbatim. `then: run(<op>)` is an edge to that Op's first step
|
|
72
97
|
when the Op is in the rendered set, and `attrs.dangling` when it isn't.
|
|
73
98
|
`meta.operator` on that same lens names the ConvergeOps the project declares
|
|
74
|
-
(chant's own `searchAttributes.Converge === "true"`, read from op.json
|
|
99
|
+
(chant's own `searchAttributes.Converge === "true"`, read from op.json, no
|
|
75
100
|
subprocess), and `/api/operator/status` fills the strip in from
|
|
76
101
|
`chant operator status --json`.
|
|
77
|
-
2. **focus
|
|
102
|
+
2. **focus.** Narrow with chant graph options as query params: `?detail=0..3`,
|
|
78
103
|
`?components=1`, `?logical=1`, `?lens=blast:<id>&down=1`, `?lens=lexicon:aws`,
|
|
79
|
-
`?env=`, `?stack=`, `?tier=`, `?target
|
|
80
|
-
|
|
104
|
+
`?env=`, `?stack=`, `?tier=`, `?target=`, `?collapse=1`.
|
|
105
|
+
`?collapse=1` (#393) shuts every member box holding more than 40 cards: the
|
|
106
|
+
box is drawn as ONE card carrying the count it was badged with (`301
|
|
107
|
+
resources · 84 bound · 85 unowned · 132 neutral`, in the estate's own
|
|
108
|
+
words), ids namespaced `box:<member>`, and the returned IR is the collapsed
|
|
109
|
+
one so every count and note speaks about the picture on screen — except its
|
|
110
|
+
NOTE, which is written about the expanded estate plus one collapse clause
|
|
111
|
+
(#396): a shut box hides what the estate references rather than changing it.
|
|
112
|
+
⌘K → "Collapse large boxes" / "Expand all". Below the limit it changes
|
|
113
|
+
nothing. **The toggle costs no live read** (#396): a member's overlay
|
|
114
|
+
document is cached under its source stamp and the read's own options, the
|
|
115
|
+
same key half `memberIr` and #404's plan cache use, so collapse and expand
|
|
116
|
+
are re-renders of a document behold already holds. `src/overlay-ir.ts` is
|
|
117
|
+
that cache and its header is the stated exception to src/member-ir.ts's
|
|
118
|
+
"a live read is never cached"; it names everything that drops an entry —
|
|
119
|
+
`POST /api/refresh` ("Re-check live"), `GET /api/overlay?plan=1`
|
|
120
|
+
("Re-check live with plan", which re-checks both halves), the capture that
|
|
121
|
+
ends an Op run, and the member's source moving. A change made to the account
|
|
122
|
+
by something that is not behold is what the re-check rows are for.
|
|
123
|
+
3. **inspect.** A node's `sourceLoc.file` is the typed source that declared it;
|
|
81
124
|
edit there to change the estate (chant is the source of truth, not behold).
|
|
125
|
+
4. **find.** ⌘K also takes an ADDRESS (#393). Two characters in, the palette
|
|
126
|
+
matches the ids and addresses of the graph the page is currently showing and
|
|
127
|
+
offers up to twelve `node: <address>` rows, prefix matches first, each with
|
|
128
|
+
its member and kind on a second line (two members of one estate can declare
|
|
129
|
+
the same address). Enter takes the same path a click on the card does (the
|
|
130
|
+
inspect pane and the highlight) and pans the graph onto it: `revealNode` in
|
|
131
|
+
web/app.js drives the same viewBox the wheel, the drag and "⤢ fit" drive.
|
|
132
|
+
Nothing here fetches, so it works in a static export too.
|
|
133
|
+
|
|
134
|
+
### The picture arrives in pieces (#422)
|
|
135
|
+
|
|
136
|
+
`GET /api/overlay?progressive=1` answers at once with the estate composed from
|
|
137
|
+
every member's SOURCE, each node carrying `attrs._pendingRead`, and then
|
|
138
|
+
broadcasts one `member` SSE frame per member as its own live read settles —
|
|
139
|
+
`{member, statuses: {<composed id>: <status>}, unobserved?}`, or `{error}` if
|
|
140
|
+
the pass itself failed. `meta.mode` is `progressive` and `meta.pending` names
|
|
141
|
+
the members not yet read.
|
|
142
|
+
|
|
143
|
+
**Opt-in, and never the default.** `/api/overlay` without the flag is byte-for-
|
|
144
|
+
byte what it was: the blocking, complete live answer this guide's read loop
|
|
145
|
+
names, which is what an agent GETs and what `src/export.ts` captures into a
|
|
146
|
+
static bundle. A route that answered with members it had not read would hand an
|
|
147
|
+
agent a half-picture it could not know was half, and freeze a bundle with
|
|
148
|
+
members permanently still-reading.
|
|
149
|
+
|
|
150
|
+
**Pending is a third claim.** `_unobserved` is "behold looked and could not
|
|
151
|
+
see"; `neutral` is "the plan did not mention it"; `_pendingRead` is "the read
|
|
152
|
+
has not finished". It rides its own attr rather than a fifth `_status` because
|
|
153
|
+
a fifth `_status` is not paintable — pinhole's painter takes a closed set and
|
|
154
|
+
falls back to `neutral` for anything else, which is `unobserved`, the exact
|
|
155
|
+
wrong answer. The SPA draws it dashed and dimmed, never filled: the fill is the
|
|
156
|
+
status channel and a pending card has no status yet.
|
|
157
|
+
|
|
158
|
+
No live result is cached to build the first picture and none is reused. A
|
|
159
|
+
pending member becomes a real one only when its own read completes, and the
|
|
160
|
+
live pass a progressive request starts is the same one per member under the
|
|
161
|
+
same budget: a progressive load costs the estate no extra spawn.
|
|
162
|
+
|
|
163
|
+
### What a read cost (#420, #421)
|
|
164
|
+
|
|
165
|
+
`GET /api/doctor` serves `diagnose()`, the same report `behold doctor` prints,
|
|
166
|
+
which until #421 only the CLI could ask for — with two extra blocks on it:
|
|
167
|
+
|
|
168
|
+
- `reads`, the ledger from `src/read-stats.ts`. Every scheduled chant read
|
|
169
|
+
passes through `ReadScheduler`, so each one is filed there with its running
|
|
170
|
+
time, its outcome, and which side of the source/live split it sits on. A bare
|
|
171
|
+
`chant graph` is the member's own TypeScript evaluation; anything with
|
|
172
|
+
`--live`/`--overlay`, and every other verb `isScheduledRead` allows, reaches
|
|
173
|
+
past it. `shared` counts subscribers handed an in-flight spawn, work not
|
|
174
|
+
done, never a run.
|
|
175
|
+
- `cache`, `memberIrCacheStats()`.
|
|
176
|
+
|
|
177
|
+
`GET /api/doctor?reads=1` is the same two blocks WITHOUT the diagnosis, and it
|
|
178
|
+
is what the SPA asks for. The split is the one #421 states: the full report is
|
|
179
|
+
for a person diagnosing and the panel line is for a person waiting. It is also a
|
|
180
|
+
cost split too: `diagnose()` probes docker, gh, helm and k3d through
|
|
181
|
+
src/substrates.ts, whose `probe` has no timeout and no kill, and the SPA asks
|
|
182
|
+
after every load settles. A static export asks for neither; `refreshReadCost`
|
|
183
|
+
returns on `staticMode` rather than relying on the bundle's own 404.
|
|
184
|
+
|
|
185
|
+
`reads.inFlight` is `{running, queued}` off the scheduler itself. The ledger
|
|
186
|
+
cannot answer it: a read still in flight has finished nothing, so it has filed
|
|
187
|
+
nothing. "What is queued now" is what someone watching a slow read wants.
|
|
188
|
+
|
|
189
|
+
Three things to know before reading a number off it. `queuedMs` is contention
|
|
190
|
+
**in the scheduler** (the HTTP-versus-poll-versus-capture competition #367
|
|
191
|
+
bounded) rather than the whole of what a member waits: the estate fan-out bounds
|
|
192
|
+
upstream through `mapPool` (src/estate.ts), so a three-member estate measured
|
|
193
|
+
3033/3035/2218 ms of running time with `queuedMs` 0 against 5.2 s of wall clock.
|
|
194
|
+
The gap is the upstream wait and #422 is where it gets measured.
|
|
195
|
+
|
|
196
|
+
`reads.unmeasured` counts the served members the ledger is BLIND to, and the
|
|
197
|
+
panel line prints it rather than quietly excluding them. Only reads that pass
|
|
198
|
+
`ReadScheduler` are filed, which is every chant read and therefore a terraform
|
|
199
|
+
member too (its `via.read` shells `chant graph`, src/terraform-member.ts). A
|
|
200
|
+
choudoufu member spawns its own binary through its own helper and is filed
|
|
201
|
+
nowhere, so on a chant+choudoufu estate the slowest card may be one that never
|
|
202
|
+
reached `recent` — "slowest 3s · 1 not measured" is the honest form of that, and
|
|
203
|
+
an unqualified "slowest 3s" is not.
|
|
204
|
+
|
|
205
|
+
And preview mode keeps the totals but drops `recent`, whose samples are keyed by
|
|
206
|
+
member directory.
|
|
207
|
+
|
|
208
|
+
The SPA asks once after each load settles and shows the answer on the loading
|
|
209
|
+
scrim the *next* time one goes up (`web/read-cost.js`, `#loading-cost`): the
|
|
210
|
+
slowest recent member, because an estate read finishes when its last member
|
|
211
|
+
does, so a sum would report a number nobody waits for.
|
|
212
|
+
|
|
213
|
+
### The behaviour overlay (#398, M1 of #397)
|
|
214
|
+
|
|
215
|
+
What the estate costs and where it runs out of headroom, on `/api/overlay`
|
|
216
|
+
only. **behold never calls a behavioural engine**, never holds its key, and
|
|
217
|
+
produces no figure of its own — it renders what a chant lexicon handed it, the
|
|
218
|
+
way it already renders drift and the carve score. The one arithmetic it does is
|
|
219
|
+
addition, and the result is named `sum` so nobody can read it as a bill.
|
|
220
|
+
|
|
221
|
+
The contract's source of truth is the comment titled "The behaviour block,
|
|
222
|
+
proposed" on INTENTIUS/behold#398 (chant#2356 is the writing half). Field names
|
|
223
|
+
here match it exactly; `src/behaviour.ts` is the implementation and the
|
|
224
|
+
validator.
|
|
225
|
+
|
|
226
|
+
Per entity, `attrs._behaviour` on the overlay IR node — the channel `_status`,
|
|
227
|
+
`_release` and `_carve` already ride: `{at: {traffic}, cost: {perHour,
|
|
228
|
+
currency}, headroom: {cpu?, latency?}, errorRate, resilience: {failure, verdict:
|
|
229
|
+
survives|degrades|fails, note?}, rightSize?: {suggestion, reason?}, provenance:
|
|
230
|
+
{engine, version, tolerance, basis: modeled|validated}}`. An entity nothing
|
|
231
|
+
priced carries **no `_behaviour` key at all**, never a zeroed block, and a
|
|
232
|
+
missing headroom axis is absent rather than 0. The block stays on the node and
|
|
233
|
+
there is deliberately no top-level `behaviour: {}` map: the SPA already walks
|
|
234
|
+
`ir.nodes` reading `attrs._status`, and a colour-by-cost mode is that same walk,
|
|
235
|
+
with no join.
|
|
236
|
+
|
|
237
|
+
Two sources, in this order:
|
|
238
|
+
|
|
239
|
+
1. `attrs._behaviour` on the nodes, as `chant graph --live --overlay` painted
|
|
240
|
+
them, with `meta._behaviour` beside them for the graph-level half.
|
|
241
|
+
2. `behaviour.<env>.json` at a member's root — `{meta, entities: {<address>:
|
|
242
|
+
block}}` — read **only** when no node of that member carries the attr, and
|
|
243
|
+
**only on the overlay** (`/api/graph` is the source graph; a prediction
|
|
244
|
+
about a live account has no business on it). The file's keys are the
|
|
245
|
+
member's own addresses; behold prefixes them to reach the composed id
|
|
246
|
+
`<member>/<address>`.
|
|
247
|
+
|
|
248
|
+
`meta.behaviour` carries the graph-level half: `engine`, `version`, `at`, the
|
|
249
|
+
engine's own `total` when it stated one, else behold's `sum` and the same per
|
|
250
|
+
box under `boxes[<boxKey>]` (`{perHour, currency, priced, unpriced}`). Mixed
|
|
251
|
+
currencies produce no sum and a diagnostic — behold converts no currency. A
|
|
252
|
+
malformed block is dropped whole with its reason in `diagnostics`, never
|
|
253
|
+
half-rendered. A member with neither source gets `{absent: "…"}` naming both
|
|
254
|
+
places behold looked; that is an absence, not a refusal, because nothing was
|
|
255
|
+
configured.
|
|
256
|
+
|
|
257
|
+
`meta.behaviour.refusal` is `{reason, remedy}` in the lexicon's own words,
|
|
258
|
+
printed as it came, and it is present **instead of** everything above: a
|
|
259
|
+
refusal emits no entity block at all and strips any that had arrived. The drift
|
|
260
|
+
overlay is untouched either way: nothing in this pass reads or writes
|
|
261
|
+
`_status`. `?logical=1` projects a different picture and carries no behaviour
|
|
262
|
+
block in M1.
|
|
263
|
+
|
|
264
|
+
### Colour by drift, cost or headroom (#399, M2)
|
|
265
|
+
|
|
266
|
+
The SPA colours the graph by one of three modes. The mode is **client-side
|
|
267
|
+
state** — `web/app.js`'s `colourMode`, persisted under `behold.colourBy` the way
|
|
268
|
+
the theme is. It is deliberately NOT in `LENS_PARAMS`/`canonicalKey`: a lens
|
|
269
|
+
param means "a different snapshot comes back", and a mode change changes no
|
|
270
|
+
fetch at all. The block is already on every priced card, so a switch is a
|
|
271
|
+
repaint of the SVG on screen.
|
|
272
|
+
|
|
273
|
+
`drift` keeps the categorical fill and #393's vocabulary legend, unchanged. The
|
|
274
|
+
two behaviour modes replace the card's FILL only, so the drift bar underneath
|
|
275
|
+
keeps saying what chant observed. `cost` is a sequential scale over
|
|
276
|
+
`cost.perHour` between the estate's own min and max; `headroom` is absolute over
|
|
277
|
+
0..1 and reads **the lower of the axes present**. An entity with no block draws
|
|
278
|
+
the drift overlay's neutral (`pinTokensFor().neutralFill`) and is marked
|
|
279
|
+
`data-unpriced`, never the zero end of the scale — "nothing priced this" and
|
|
280
|
+
"this is free" are opposite claims.
|
|
281
|
+
|
|
282
|
+
Both ramps are derived from the active palette (`rampFor` in
|
|
283
|
+
`web/behaviour-scale.js`), the way #229 derives the chrome: cost is a single hue
|
|
284
|
+
off the accent walked in lightness away from the background — expensive is not a
|
|
285
|
+
verdict behold gets to paint, and headroom rides the three status hues the
|
|
286
|
+
drift overlay already anchors, degraded → foreign → managed.
|
|
287
|
+
|
|
288
|
+
One legend per mode, in the Model tab. The Scope tab carries a totals row per
|
|
289
|
+
box and per estate for the active mode: `cost` quotes `meta.behaviour.boxes[key]`
|
|
290
|
+
and `.sum`, or `meta.behaviour.total` when the ENGINE stated one (labelled
|
|
291
|
+
"engine total"); `headroom` has no server-side aggregate, since no engine states one —
|
|
292
|
+
so the SPA takes the scope's min and median itself and the row says "computed".
|
|
293
|
+
Every row carries `n priced · m unpriced`. In drift mode there is no row.
|
|
294
|
+
|
|
295
|
+
Everything either mode decides is pure in `web/behaviour-scale.js`, with
|
|
296
|
+
`web/behaviour-scale.test.js` beside it; the SPA owns the fetches and the SVG.
|
|
297
|
+
|
|
298
|
+
### Provenance on every figure, and the refusal (#401, M4)
|
|
299
|
+
|
|
300
|
+
Every figure the SPA shows, the totals rows, the legend, and the inspect pane's
|
|
301
|
+
`behaviour` section — carries `{engine} {version} · {tolerance} · {basis}`
|
|
302
|
+
beside it, never once per page. `modeled` is spelled out on hover as "modeled,
|
|
303
|
+
not billed": #397's one prohibition is a prediction presented as a bill, and the
|
|
304
|
+
badge is where that is prevented. Provenance is per ENTITY in #398's contract, so
|
|
305
|
+
a badge over a set (a box, the estate) says `mixed` on any field the set
|
|
306
|
+
disagrees on rather than picking one.
|
|
307
|
+
|
|
308
|
+
`meta.behaviour.refusal` disables both behaviour modes in the View tab and in
|
|
309
|
+
⌘K (visibly, with the lexicon's reason and remedy as their tooltip) and prints
|
|
310
|
+
those words verbatim where the legend would be. The drift overlay keeps
|
|
311
|
+
rendering; it never depended on an engine. `meta.behaviour.absent` disables the
|
|
312
|
+
same two modes with the absent line as their tooltip and prints **nothing**:
|
|
313
|
+
nothing was configured, so nothing refused. `meta.behaviour.diagnostics` render
|
|
314
|
+
in the Model tab under the legend.
|
|
82
315
|
|
|
83
316
|
## The carve loop (Terraform → chant, #230)
|
|
84
317
|
|
|
@@ -106,7 +339,7 @@ session.
|
|
|
106
339
|
{human, note}}`. Each entry carries `target`, `stage`
|
|
107
340
|
(`emitted`/`bridged`/`applied`), `graduated`, `note` and a retypeable
|
|
108
341
|
`applyCommand`. The same field appears on an ordinary `behold serve` whose
|
|
109
|
-
project directory carries a carveout
|
|
342
|
+
project directory carries a carveout (no report and no demo needed) and is
|
|
110
343
|
absent entirely when nothing has been carved.
|
|
111
344
|
|
|
112
345
|
In the graph, an `applied` address draws inside the chant member box (keeping
|
|
@@ -158,7 +391,7 @@ behold does not apply. To change the estate:
|
|
|
158
391
|
- `op-run <name>` — start the project's `ApplyOp` (code→cloud) or `ReconcileOp`
|
|
159
392
|
(cloud→code PR).
|
|
160
393
|
- `op-signal <name> <gate>` — approve a gate (e.g. a destructive apply).
|
|
161
|
-
- `op-status <name>` — watch phases; `op-report <name
|
|
394
|
+
- `op-status <name>` — watch phases; `op-report <name>`, the run report.
|
|
162
395
|
|
|
163
396
|
Every mutation is a gated, durable Temporal workflow with a human-confirmable gate
|
|
164
397
|
and saga rollback. There is no behold endpoint that mutates the cloud.
|
|
@@ -166,7 +399,7 @@ and saga rollback. There is no behold endpoint that mutates the cloud.
|
|
|
166
399
|
A run behold triggered is asked for structured per-step records (chant#1676:
|
|
167
400
|
`--progress-json` on the durable path, `--json` on the local one), so the ops
|
|
168
401
|
lens paints the run over the declared track and a pending gate renders as a card
|
|
169
|
-
with an Approve button. That button is `op-signal
|
|
402
|
+
with an Approve button. That button is `op-signal`, the same delegated write —
|
|
170
403
|
and nothing else about the run is behold's to decide: a stream that dies leaves
|
|
171
404
|
the playhead at the last settled step and says so, and a gate paints as pending
|
|
172
405
|
only when chant's `gateState` query named it.
|
|
@@ -201,13 +434,16 @@ for another env never stands in. `just e2e-ci-github` proves the contract
|
|
|
201
434
|
against the real forge (example-ci + `.github/workflows/behold-e2e-dispatch.yml`),
|
|
202
435
|
including the `lost` verdict, which `BEHOLD_CI_FOLLOW_TIMEOUT_MS` and
|
|
203
436
|
`BEHOLD_CI_POLL_FAIL_BUDGET` let a run force on purpose; a lost run is the one
|
|
204
|
-
`POST /api/ci/readopt` re-follows.
|
|
437
|
+
`POST /api/ci/readopt` re-follows. The same e2e dispatches into an environment
|
|
438
|
+
with a required reviewer (`behold-e2e-gated`): the run holds at `waiting`, the
|
|
439
|
+
progress state carries `waiting: true` and the run's `url`, the now-line says
|
|
440
|
+
the approval is granted on the forge, and a cancellation lands as `failed`.
|
|
205
441
|
|
|
206
442
|
A dispatched run is followed honestly (#165 §6, PR #350): only GitHub's own
|
|
207
443
|
`completed` + conclusion paints `ok`/`failed`; a poll-failure budget or the
|
|
208
444
|
follow deadline promotes the run to `lost` (chips frozen at last-observed,
|
|
209
445
|
the run possibly still live at its own page), never to a verdict. The adopted
|
|
210
|
-
run id is persisted as one JSON per project under `~/.behold/ci-runs
|
|
446
|
+
run id is persisted as one JSON per project under `~/.behold/ci-runs/`, the
|
|
211
447
|
operator's machine-state, outside the project tree, so the Invariant below
|
|
212
448
|
still names the whole in-project write surface. `GET /api/ci/run` reads the
|
|
213
449
|
record; `POST /api/ci/readopt` (also attempted once at boot) re-follows an
|
|
@@ -219,14 +455,14 @@ reader instead of nothing.
|
|
|
219
455
|
A project that declares a `ConvergeOp` gets an operator strip on the ops lens:
|
|
220
456
|
per loop, the last tick's own log line (verbatim, with its instant), the lease,
|
|
221
457
|
and how many gates are pending. **One** tick — `chant operator status` keeps only
|
|
222
|
-
`records.at(-1)
|
|
458
|
+
`records.at(-1)`, so the strip is never a timeline, and it says so.
|
|
223
459
|
|
|
224
460
|
The timeline is its own read: `GET /api/operator/log` → `chant operator log
|
|
225
461
|
--json` (chant#2029), the tick records and the gate resolutions against them
|
|
226
462
|
merged oldest-first. It lives in a panel that grows from the strip, and two rules
|
|
227
463
|
hold it there. It is **pulled**, on the click that opens it and never on a timer.
|
|
228
464
|
And it is **bounded** — `--limit` on every invocation, 50 by default and 200 at
|
|
229
|
-
most, `--since` when asked
|
|
465
|
+
most, `--since` when asked, because the ledger grows one line per tick forever;
|
|
230
466
|
the answer reports the window it used, so a full one can't read as the end of
|
|
231
467
|
history. The route checks the project's chant version *before* it spawns: below
|
|
232
468
|
0.53.1, `chant operator log` resolves to `chant operator`, the tick daemon, and
|
|
@@ -235,12 +471,12 @@ behold reading a history must never become behold running an operator.
|
|
|
235
471
|
Since chant 0.53.1 (chant#2027) a tick record carries an `id` and the
|
|
236
472
|
per-component verdicts it derived, and both ride through to
|
|
237
473
|
`[].lastTick.{id,components}`. The strip line names the tick (truncated), and
|
|
238
|
-
the verdicts join onto the component DAG by component name
|
|
474
|
+
the verdicts join onto the component DAG by component name, the same key the
|
|
239
475
|
live `chant components status` read is joined by. They join **under** that read:
|
|
240
476
|
a tick only ever feeds the last tier of `componentStatusColor` (the
|
|
241
477
|
reconciliation verdict), only on a node the live read left unpainted, and only
|
|
242
478
|
while the tick is younger than fifteen of chant's own operator rounds. Past
|
|
243
|
-
that it is named on the node, dated, and painted nothing
|
|
479
|
+
that it is named on the node, dated, and painted nothing: a graph fill has
|
|
244
480
|
nowhere to put "as of an hour ago". A chant older than 0.53.1 sends neither
|
|
245
481
|
field and every one of these paths is a no-op.
|
|
246
482
|
|
|
@@ -260,13 +496,13 @@ design (a local tick with no PR behind it has none) and never synthesized, so a
|
|
|
260
496
|
fact without one renders no link and no placeholder for one, and a url behold
|
|
261
497
|
would not put behind an `href` reads as no address at all.
|
|
262
498
|
|
|
263
|
-
A gate someone already resolved simply leaves `pendingGates
|
|
264
|
-
carries no `resolvedBy`/`when
|
|
499
|
+
A gate someone already resolved simply leaves `pendingGates`, the status read
|
|
500
|
+
carries no `resolvedBy`/`when`, so the strip shows no resolved-by line rather
|
|
265
501
|
than attributing an approval to nobody. The timeline is where resolutions are
|
|
266
502
|
named, because there they are records rather than an inference.
|
|
267
503
|
|
|
268
504
|
An `OperatorStack` (chant#1940) declares the loop as a k8s estate, so it already
|
|
269
|
-
appears in the entity graph. behold names it there
|
|
505
|
+
appears in the entity graph. behold names it there, the Namespace as the loop's
|
|
270
506
|
home, each CronJob as a converge tick — off chant's own
|
|
271
507
|
`app.kubernetes.io/component: converge-tick` labels, never a naming convention.
|
|
272
508
|
|
|
@@ -279,7 +515,7 @@ executor.
|
|
|
279
515
|
### The exceptions, and their exact size
|
|
280
516
|
|
|
281
517
|
`POST /api/layout` (#228) writes **one** file in the served project:
|
|
282
|
-
`.behold/layout.json
|
|
518
|
+
`.behold/layout.json`, the hand-layout sidecar, `{version, lenses: {<lens>:
|
|
283
519
|
{<node id>: {dx,dy,dw,dh}}}}`. That is the whole of behold's write surface
|
|
284
520
|
inside a project, and it does not weaken the invariant above:
|
|
285
521
|
|
|
@@ -288,7 +524,7 @@ inside a project, and it does not weaken the invariant above:
|
|
|
288
524
|
chant's, and a delta for a node that left the estate is dropped on read.
|
|
289
525
|
- It **never touches the cloud and never touches your source**. No `.ts`, no
|
|
290
526
|
`chant.config.ts`, no `.behold.json`. The path is `cfg.projectDir` + two
|
|
291
|
-
constants
|
|
527
|
+
constants; nothing from the request reaches the filesystem.
|
|
292
528
|
- It refuses politely when it shouldn't write: preview mode, a static-export
|
|
293
529
|
capture, a read-only project directory, an oversized or malformed body.
|
|
294
530
|
- It is **per-user state**, unlike `.behold.json` (config, meant to be tracked).
|
|
@@ -304,14 +540,24 @@ copy, they write only into `<copy>/app/carveout/`, and the directory they write
|
|
|
304
540
|
into is a scratch dir behold created inside a directory it copied for you a
|
|
305
541
|
minute earlier. `carve bridge` runs without `--apply-rewrites`, so the demo's
|
|
306
542
|
own Terraform is not edited either. The only request-derived value is `select`,
|
|
307
|
-
and it must be an address the served report already ranks
|
|
543
|
+
and it must be an address the served report already ranks, the value that
|
|
308
544
|
reaches the spawn's argv comes from a closed set read off disk. No cloud write,
|
|
309
545
|
no Terraform mutation, no edit to anyone's chant source.
|
|
310
546
|
|
|
547
|
+
A choudoufu estate member (#366) adds no exception. A move there is one tag
|
|
548
|
+
write through choudoufu's own `live-mv`, and behold never makes it: `GET
|
|
549
|
+
/api/choudoufu/moves` reads a plan (`carve.json`) from inside a served member,
|
|
550
|
+
previews each move with choudoufu's `live-mv -json -dry-run` (the only spelling
|
|
551
|
+
of `live-mv` in the tree, src/choudoufu-moves.ts `dryRunArgs`, asserted by
|
|
552
|
+
test), hands the lines back with copy buttons, and `?receipt=1` reads the
|
|
553
|
+
listing after a person ran them. **There is no `/api/choudoufu/mv`**, and
|
|
554
|
+
src/choudoufu-route.test.ts asserts it stays absent, for the same reason
|
|
555
|
+
`/api/carve/apply` does not exist.
|
|
556
|
+
|
|
311
557
|
## Changing behold (for agents working on this repo)
|
|
312
558
|
|
|
313
559
|
The sections above are about driving a running behold. This one is about
|
|
314
|
-
changing it
|
|
560
|
+
changing it, the working rules that used to live in a handoff note and now
|
|
315
561
|
live here, with the enforceable ones enforced.
|
|
316
562
|
|
|
317
563
|
- **Branches and merges.** Work on a branch off `main` (an isolated worktree
|
|
@@ -322,7 +568,7 @@ live here, with the enforceable ones enforced.
|
|
|
322
568
|
next onto what landed.
|
|
323
569
|
- **The local gate** is `just check` (tsc, tests, build). Every vitest run
|
|
324
570
|
also writes `.vitest-last.json` (the json reporter, PR #351), and `just
|
|
325
|
-
test` keeps the reporter's full output in `vitest.log
|
|
571
|
+
test` keeps the reporter's full output in `vitest.log`, both gitignored —
|
|
326
572
|
because #334's one-shot first-run failures kept losing the failing file's
|
|
327
573
|
name. If the suite fails once and passes on rerun, those two files from the
|
|
328
574
|
first run are the evidence; attach them to #334 (`jq '.testResults[] |
|
|
@@ -330,7 +576,7 @@ live here, with the enforceable ones enforced.
|
|
|
330
576
|
- **Releasing** is `just release`, after the version-bump PR merged: it tags
|
|
331
577
|
`behold-v<version>`, pushes the tag (which is what runs `release.yml`), and
|
|
332
578
|
waits for npm to show the version. It refuses in every state where a tag
|
|
333
|
-
push would be wrong. Never re-push a `behold-v*` tag by hand
|
|
579
|
+
push would be wrong. Never re-push a `behold-v*` tag by hand: a tag push
|
|
334
580
|
re-fires publish. Tags absent from origin (behold-v0.10.0, 0.10.1) stay
|
|
335
581
|
absent for that reason.
|
|
336
582
|
- **Scratch infrastructure** is governed by `src/scratch.ts`, asserted by
|
|
@@ -345,3 +591,488 @@ live here, with the enforceable ones enforced.
|
|
|
345
591
|
stays that way.
|
|
346
592
|
- **chant's working rules** (its single-lane checkout, its release path) are
|
|
347
593
|
chant's and live in chant's repo, not here.
|
|
594
|
+
|
|
595
|
+
### Adding a member kind
|
|
596
|
+
|
|
597
|
+
An estate member has a kind (#368): what a directory has to look like, what
|
|
598
|
+
tool answers reads for it, and how it becomes a `GraphIR`. `src/member-kind.ts`
|
|
599
|
+
is the one table; `chant` is the first row and every member was that row
|
|
600
|
+
until the table existed. A new kind is:
|
|
601
|
+
|
|
602
|
+
1. A `registerMemberKind({ kind, probe, expects, via, passes })` call in
|
|
603
|
+
`src/member-kind.ts`, the `probe` is sync, read-only and runs no code (a
|
|
604
|
+
file's presence, a regex over a root file, or either: the choudoufu probe
|
|
605
|
+
takes the `estate.chdf.hcl` sidecar, choudoufu's leading form, OR a `live {`
|
|
606
|
+
block in a root *.tf, #387); `via.tool` stamps the binary and
|
|
607
|
+
version that would answer, which is the version half of `memberIr`'s
|
|
608
|
+
cache key; `via.read` is the one uncached read, source or live per `opts`.
|
|
609
|
+
The module imports nothing from the read path at runtime (src/chant.ts
|
|
610
|
+
imports src/project.ts, which imports it); a kind's `via` closes over its
|
|
611
|
+
own tool module.
|
|
612
|
+
Add the word to `MEMBER_KINDS`. A word in the vocabulary that no row
|
|
613
|
+
registers is a doctor fail with the reason, never silently chant.
|
|
614
|
+
2. Nothing in `src/estate.ts`: `composeEstate`, `composeEstateOverlay` and
|
|
615
|
+
`estateNamespaceScopes` already dispatch through `memberKindOf(dir)`.
|
|
616
|
+
The two routes that render an estate — `/api/graph`'s estate branch and
|
|
617
|
+
`/api/overlay`'s — run the same passes in the same order and must not
|
|
618
|
+
fork per kind; a kind's differences live inside its `read` or its `passes`.
|
|
619
|
+
|
|
620
|
+
`passes` (#427) is the render-time half, and it exists because the read-time
|
|
621
|
+
one could not hold it: the Terraform kind's third pass takes the request's
|
|
622
|
+
zoom, so it cannot run where the reader runs. `applyMemberPasses(ir,
|
|
623
|
+
{detail})` is the one call the six render sites make, and `memberPassNote(run,
|
|
624
|
+
dirs)` the line they return, each kind handed only the served dirs that are
|
|
625
|
+
its own, so a route never names a kind. Three rules a `passes` holds. It
|
|
626
|
+
mutates IN PLACE, because the existing passes do and their callers depend on
|
|
627
|
+
it. It self-guards on the IR's own CONTENT and never on the served members'
|
|
628
|
+
kinds. A chant project may declare a kind's lexicon; a chant project
|
|
629
|
+
declaring `terraform` is what `src/terraform-route.test.ts`'s first block
|
|
630
|
+
serves, and `memberKindOf` says `chant` there, so membership dispatch would
|
|
631
|
+
silently stop rendering it. And a run is carried forward, never re-run: a
|
|
632
|
+
pass that already elided its cards returns an empty elision the second time,
|
|
633
|
+
which drops the note on every composed estate. A test that re-registers a
|
|
634
|
+
kind to stub its reader spreads the real spec (`{ ...terraformSpec, via: … }`)
|
|
635
|
+
or it drops the passes with it.
|
|
636
|
+
3. A `registerPack({ lexicon, iconFor, fields })` in `src/render.ts`, or the
|
|
637
|
+
kind's cards lead with the alphabetically first two short attrs. A `fields`
|
|
638
|
+
function takes the box the card is drawn in as its second argument, so a
|
|
639
|
+
row the box already carries can be left off (#393 item 9,
|
|
640
|
+
`src/card-face.ts`).
|
|
641
|
+
4. A row in `statusVocabulary` (`src/status-vocabulary.ts`) when the kind has
|
|
642
|
+
its own words for the four overlay colours — choudoufu's `bound` is not
|
|
643
|
+
chant's `managed`, and a legend that says `managed` over a card carrying no
|
|
644
|
+
marker is a claim behold has no right to make. The colours never move; only
|
|
645
|
+
the naming does, and an estate of several kinds keeps chant's words and says
|
|
646
|
+
so in the legend's tooltip.
|
|
647
|
+
5. A `DoctorCheck` line when the kind needs a tool on PATH or a per-member
|
|
648
|
+
precondition (a version floor checked before the spawn, the way
|
|
649
|
+
`carveStatusReader` does; a PATH probe the way `src/demos.ts` does).
|
|
650
|
+
6. Tests: the probe and the object form in `src/project.test.ts`; dispatch
|
|
651
|
+
in `src/estate.test.ts`'s "#368" block, which registers a fake kind and
|
|
652
|
+
asserts chant members still go through exactly the calls they did, and its
|
|
653
|
+
"#427" block for the render passes; the
|
|
654
|
+
kind's own reader off recorded documents in `src/__fixtures__/`, with
|
|
655
|
+
provenance in prose above the load.
|
|
656
|
+
|
|
657
|
+
`.behold.json` names a kind as `{ "dir": "x", "kind": "<kind>" }`; a bare
|
|
658
|
+
string is `chant`. Anything behold boots for a kind goes through
|
|
659
|
+
`assertScratch` first.
|
|
660
|
+
|
|
661
|
+
**What the first screen owes a non-chant member (#393).** Three chant-shaped
|
|
662
|
+
things used to be offered to every member whatever it was, and a new kind gets
|
|
663
|
+
all three answered for free:
|
|
664
|
+
|
|
665
|
+
- `/api/project` publishes `memberKinds`, the served members' kinds, in
|
|
666
|
+
composition order. The SPA opens on the `resources` zoom when none of them is
|
|
667
|
+
`chant`, because `components` is a projection of a chant project's own
|
|
668
|
+
component DAG and a member without one opens on "the components lens doesn't
|
|
669
|
+
apply to a composed estate yet". #182's zero-node fallback still covers a
|
|
670
|
+
chant project that declares no components.
|
|
671
|
+
- `/api/project` publishes `runtimeCapable`, and the palette and the View tab
|
|
672
|
+
offer `zoom: runtime` only then. The tier below the declaration boundary is
|
|
673
|
+
the owner-referenced children chant stamps `runtimeOwner` on, and only a
|
|
674
|
+
Kubernetes read has an owner chain to resolve — `RUNTIME_LEXICONS` in
|
|
675
|
+
`src/server.ts` is the one word (`k8s`), read off the members' DECLARED
|
|
676
|
+
lexicons so the stop exists or not before anyone picks it.
|
|
677
|
+
- A chant-only facet answers emptily rather than 500ing for a non-chant
|
|
678
|
+
primary: `/api/resources` returns `{byComponent: {}}`, the shape carve mode
|
|
679
|
+
already returns and the SPA already reads as "no resource facet here".
|
|
680
|
+
|
|
681
|
+
A kind that grows components, or a substrate with an owner chain, changes those
|
|
682
|
+
answers where they are decided rather than per kind in a route.
|
|
683
|
+
|
|
684
|
+
### The workbench catalog
|
|
685
|
+
|
|
686
|
+
`demos.json` is the catalog that ships. `workbench.json`, read beside it and
|
|
687
|
+
deliberately absent from package.json's `files`, is the catalog of internal
|
|
688
|
+
estates this checkout is developed against (#386, #388). Four rules, and
|
|
689
|
+
src/demos.ts holds them:
|
|
690
|
+
|
|
691
|
+
1. A third source, `local`, and a second file. A local entry's `path` is
|
|
692
|
+
relative to the directory of the catalog file that named it, the intentius
|
|
693
|
+
checkouts are siblings, so the workbench writes `../choudoufu`,
|
|
694
|
+
`../waterpark`, `../chant`, and the file stays committed and reproducible.
|
|
695
|
+
An entry whose path is not checked out is unsatisfiable exactly as a
|
|
696
|
+
missing binary is: `--list` and `/api/demos` say so, CI stays clean.
|
|
697
|
+
`BEHOLD_WORKBENCH=<file>` names a catalog elsewhere; a workbench name that
|
|
698
|
+
collides with a bundled one is dropped with a stderr line.
|
|
699
|
+
2. In place or copied, said explicitly. `inPlace: true` serves `path` where it
|
|
700
|
+
sits; the default copies to `behold-demos/<name>` with the bundled filter.
|
|
701
|
+
Anything whose setup writes into the tree — `init`, `apply`, a rendered
|
|
702
|
+
generator — is copied or generated into the target, never run in a
|
|
703
|
+
checkout, which is how #366's "behold never runs `choudoufu init` in a
|
|
704
|
+
served project" survives. An `inPlace` entry with a `setup` says so in its
|
|
705
|
+
description.
|
|
706
|
+
3. A generator is a source. An entry with no `path` at all renders its estate
|
|
707
|
+
into an empty target through its own `setup`, which runs with cwd = the
|
|
708
|
+
target and two extra variables: `BEHOLD_WORKBENCH_DIR` (the catalog file's
|
|
709
|
+
directory, so a script can reach `../choudoufu`) and `BEHOLD_DEMO_NAME`.
|
|
710
|
+
The up script writes the matching down script into the target, the way the
|
|
711
|
+
bundled choudoufu demo ships `scripts/choudoufu-down.sh`.
|
|
712
|
+
4. `CHOUDOUFU_BIN`. `choudoufuBinary()` (src/choudoufu-member.ts) names the
|
|
713
|
+
binary and every spawn, the doctor probe and the demo requirement check go
|
|
714
|
+
through it; the doctor line prints which binary answered. Workbench scripts
|
|
715
|
+
spell the same fallback, `${CHOUDOUFU_BIN:-choudoufu}`.
|
|
716
|
+
|
|
717
|
+
`just example name="<entry>"` serves one. Scratch discipline is unchanged: an
|
|
718
|
+
emulator a workbench entry boots is `behold-wb-<entry>` on its own port, and
|
|
719
|
+
the up scripts (`workbench/<entry>/up.sh`, helpers in `workbench/lib/`) assert
|
|
720
|
+
the `behold-wb-` prefix and the not-4566 rule themselves, in bash, because they
|
|
721
|
+
are the boot site. Each writes the matching `scripts/down.sh` into its target.
|
|
722
|
+
|
|
723
|
+
**The eleven entries**, in catalog order. The counts are measured, not
|
|
724
|
+
estimated — every entry the e2e below can reach on a developer machine prints
|
|
725
|
+
its own graph and overlay counts as it runs.
|
|
726
|
+
|
|
727
|
+
| entry | what it serves | needs |
|
|
728
|
+
|---|---|---|
|
|
729
|
+
| `chant-getting-started` | chant's own getting-started example from `../chant`, in place: the source graph, 8 nodes, no substrate. The one-second answer to "did I break plain chant reading?" | that example's own `node_modules`, nothing is installed in your chant tree |
|
|
730
|
+
| `chant-local-cloud-trio` | chant's local-cloud-trio, in place: one project declaring across aws, azure and gcp, 8 nodes and 2 edges of source | the same |
|
|
731
|
+
| `fountain-ops` | `../fountain-ops` in place with `--env local`, the mature estate on your working checkout | docker, k3d, kubectl, jq, just. **The one entry whose setup runs in your working copy**: the checkout's own `just up`, a five-minute k3d cluster, and it switches your kubectl context. `just down` there removes it; behold never does |
|
|
732
|
+
| `choudoufu-workbench` | the live-mv workbench's four estates copied out of `../choudoufu`, composed with `--env live`: 42 cards — 24 bound, the 12 team cards reading `owned by tlmig-sample-monolith`, 6 neutral | docker, choudoufu |
|
|
733
|
+
| `choudoufu-cohort-s3` | `estate-gen`'s `s3` cohort rendered into the target, a sidecar-declared estate (#387): 6 cards, all neutral, because the apply stops where floci answers S3 Control tag reads on a hostname that does not resolve | docker, choudoufu, go, terraform |
|
|
734
|
+
| `choudoufu-cohort-iam-ecr` | the same for `iam-ecr`, the one cohort floci implements end to end: 6 cards, all 6 bound | the same |
|
|
735
|
+
| `choudoufu-cohort-ec2-networking` | the same for `ec2-networking`: 49 cards, the widest roster, all neutral (floci refuses a transit gateway call and the apply stops) | the same |
|
|
736
|
+
| `terralith-1` | `terralith-gen` at scale 1 plus the `estate.chdf.hcl` sidecar it omits, applied by choudoufu from nothing: 79 cards, all 79 bound | docker, choudoufu, go |
|
|
737
|
+
| `terralith-4` | the same at scale 4: 301 cards, all 301 bound. The estate behold is sized against | the same |
|
|
738
|
+
| `terralith-4-adopt` | scale 4 again, but STOCK terraform applies it first, so nothing wears a marker: 301 cards — 85 UNOWNED, 84 bound by derived identity alone, 132 neutral. One `choudoufu live-import` line, which the up script prints and you run, and all 301 read bound | docker, choudoufu, go, terraform |
|
|
739
|
+
| `waterpark` | `../waterpark/access` in place as a bare Terraform directory (#384): five roots as boxes, 58 cards, 49 edges, nothing written under the estate | `@intentius/chant-lexicon-terraform` + `@cdktf/hcl2json` beside behold — optional peers behold declares and does not install |
|
|
740
|
+
|
|
741
|
+
**The scratch, by name and port.** Each emulator is the entry's own, booted by
|
|
742
|
+
its up script and removed by the `scripts/down.sh` that script wrote into the
|
|
743
|
+
target rather than by pattern.
|
|
744
|
+
|
|
745
|
+
| entry | container | host port |
|
|
746
|
+
|---|---|---|
|
|
747
|
+
| `choudoufu-workbench` | `behold-wb-choudoufu-workbench` | 4651 |
|
|
748
|
+
| `choudoufu-cohort-s3` | `behold-wb-choudoufu-cohort-s3` | 4652 |
|
|
749
|
+
| `choudoufu-cohort-iam-ecr` | `behold-wb-choudoufu-cohort-iam-ecr` | 4653 |
|
|
750
|
+
| `choudoufu-cohort-ec2-networking` | `behold-wb-choudoufu-cohort-ec2-networking` | 4654 |
|
|
751
|
+
| `terralith-1` | `behold-wb-terralith-1` | 4655 |
|
|
752
|
+
| `terralith-4` | `behold-wb-terralith-4` | 4656 |
|
|
753
|
+
| `terralith-4-adopt` | `behold-wb-terralith-4-adopt` | 4657 |
|
|
754
|
+
| `chant-*`, `waterpark` | none (source reads, no substrate |) |
|
|
755
|
+
| `fountain-ops` | the k3d cluster `fountain-local`, which is the checkout's, not behold's | — |
|
|
756
|
+
|
|
757
|
+
The port is the floci's host binding and the entry's `serve.spawnEnv`
|
|
758
|
+
`AWS_ENDPOINT_URL` at once, so the estate's own applies and the choudoufu
|
|
759
|
+
behold spawns talk to the same emulator.
|
|
760
|
+
|
|
761
|
+
**The e2e** is `just e2e-workbench` (`e2e/workbench-e2e.sh`, #391): every entry
|
|
762
|
+
loaded through `behold demo <entry> <tmp target> --port <p>` on its own port
|
|
763
|
+
from 4720 up, `/api/graph` asserted to carry nodes and a live entry's
|
|
764
|
+
`/api/overlay?env=live` to answer with its bound/unowned/neutral split, timings
|
|
765
|
+
per entry; eight entries and 553s on the machine this was written on.
|
|
766
|
+
|
|
767
|
+
Time is asserted, not only printed (#423). Each entry carries a ceiling at
|
|
768
|
+
roughly twice its own measured value, and the run carries one too, because
|
|
769
|
+
#419's whole subject was a number nobody was watching: the ratio could not
|
|
770
|
+
regress silently and the duration could. The measured figures the ceilings come
|
|
771
|
+
from are in the script beside them; `BEHOLD_E2E_SECS_<entry>` and
|
|
772
|
+
`BEHOLD_E2E_RUN_SECS` override. A live entry also asserts #422's shape: the
|
|
773
|
+
progressive overlay answers with every member marked pending and does so before
|
|
774
|
+
the blocking read returns, so the estate cannot quietly regress to
|
|
775
|
+
all-or-nothing. Where the blocking read is already sub-second there is nothing
|
|
776
|
+
to beat, and the run reports the two numbers rather than claiming a pass.
|
|
777
|
+
|
|
778
|
+
`terralith-4-adopt` is asserted twice — 85 UNOWNED, then the
|
|
779
|
+
`live-import` line the up script printed, run by the script itself in the
|
|
780
|
+
target the way a person would, then 301 bound, because that write is the
|
|
781
|
+
person's, never behold's (#372). `waterpark` is checked for an untouched
|
|
782
|
+
checkout afterwards. `missingRequirements` decides what runs: a missing binary,
|
|
783
|
+
an unchecked-out sibling, an uninstalled chant example or the absent Terraform
|
|
784
|
+
lexicon each print `skip: <entry>: needs …`, so in CI every entry skips and the
|
|
785
|
+
run exits 0. `fountain-ops` is skipped by name everywhere, for the reason in
|
|
786
|
+
the table. A `behold-wb-*` container left standing at the end fails the run.
|
|
787
|
+
|
|
788
|
+
The seeded catalog (#389) found one thing missing in src/: a LONE choudoufu
|
|
789
|
+
estate (every generated entry is one) served the no-project card, because the
|
|
790
|
+
single-project read is `chant graph <dir>` and such a directory has no lexicon
|
|
791
|
+
for it to read. `servesAsEstate` (src/member-kind.ts) is the predicate that
|
|
792
|
+
routes one directory of a non-chant kind through the estate compose path, where
|
|
793
|
+
the member's own kind reads it. One member composes exactly as four do, ids
|
|
794
|
+
namespaced under the member's short name, so the graph, the pane and the morph
|
|
795
|
+
agree on what a node is called.
|
|
796
|
+
|
|
797
|
+
### Rendering a Terraform estate
|
|
798
|
+
|
|
799
|
+
A Terraform estate reaches behold through chant.
|
|
800
|
+
`@intentius/chant-lexicon-terraform` reads the HCL an estate already has and
|
|
801
|
+
emits one entity per block, so **a Terraform estate is a chant project whose
|
|
802
|
+
only lexicon is a reader** and `chant graph --format ir` serves it like any
|
|
803
|
+
other. behold parses no HCL and ships no HCL parser, the same posture
|
|
804
|
+
`src/carve-lens.ts` states for the carve report (#378).
|
|
805
|
+
|
|
806
|
+
Three passes turn what arrives into a picture (`src/terraform-lens.ts`), in this
|
|
807
|
+
order, guarded on the IR carrying terraform entities so every other estate gets
|
|
808
|
+
the identical object back:
|
|
809
|
+
|
|
810
|
+
1. **`normalizeTerraformNodes`.** A node's `kind` arrives as the entity class
|
|
811
|
+
(`Terraform::Resource`), not the resource type, so every card would be titled
|
|
812
|
+
and iconed the same. The type moves out of `attrs.address` into `kind` and
|
|
813
|
+
the block class lands in `attrs.block`. That is the shape a carve node
|
|
814
|
+
already has, which is why one presentation pack serves both.
|
|
815
|
+
2. **`groupTerraformByRoot`.** Roots are a Terraform project's only grouping.
|
|
816
|
+
It retires itself when chant#2266 groups upstream. A node sits in exactly one
|
|
817
|
+
box, so a box every node has left is dropped rather than drawn empty (#393):
|
|
818
|
+
a served directory arrives composed, `composeStacks` boxes the whole member,
|
|
819
|
+
and the root boxes then take those same nodes, which is what put an empty
|
|
820
|
+
box named `access` on water park's canvas. What the member box was there to
|
|
821
|
+
say moves into the root box's title, which is `<member>/<root>` whenever the
|
|
822
|
+
estate holds more than one member and the bare root name when it does not
|
|
823
|
+
(two members with a root apiece named `prod` would otherwise merge silently).
|
|
824
|
+
3. **`filterTerraformCards`.** What is a card, below.
|
|
825
|
+
|
|
826
|
+
**What is a card (#382).** Measured on a real estate: 247 nodes for 43
|
|
827
|
+
resources, four fifths of it not infrastructure.
|
|
828
|
+
|
|
829
|
+
| tier | blocks | why |
|
|
830
|
+
|---|---|---|
|
|
831
|
+
| default (detail 0-2) | `resource`, `data`, `module` | the estate: what is declared, what it reads, what it composes |
|
|
832
|
+
| attributes (detail 3) | + `output`, `variable` | its interface — real, but a second question |
|
|
833
|
+
| never | `terraform`, `provider`, `locals` | settings, not estate |
|
|
834
|
+
|
|
835
|
+
Nothing is dropped silently: `terraformElisionNote` says what is not drawn and
|
|
836
|
+
where to see it, the way `edgelessNote` says why a view has no edges.
|
|
837
|
+
|
|
838
|
+
**The note, at every zoom and in a 260px strip (#393).** The roots note and the
|
|
839
|
+
elision note are built by one function in `/api/graph` and returned by all three
|
|
840
|
+
of its branches, the logical lens included — it was the lens that most needed
|
|
841
|
+
the line and the only one that dropped it. Both routes send `note` and, when
|
|
842
|
+
there is a shorter true form, `noteShort` (`5 roots · 2 skipped · 189 blocks not
|
|
843
|
+
drawn`). The strip shows the short one with the long one on its tooltip, and the
|
|
844
|
+
panel's Model tab prints it whole. The server writes both: the SPA does not
|
|
845
|
+
author notes, and truncating this one on a sentence boundary would keep the list
|
|
846
|
+
of root names and drop the counts.
|
|
847
|
+
|
|
848
|
+
**Do not invent edges.** They arrive from chant or not at all: the fixtures here
|
|
849
|
+
were recorded when a Terraform IR carried none, and lexicon 0.61.0 (chant#2265,
|
|
850
|
+
which resolves a block's `"${…}"` references) draws 390 over water park's five
|
|
851
|
+
roots with no change on this side. The one relationship that looked derivable
|
|
852
|
+
without it (a cross-root read by name) was measured and refused (#381): both
|
|
853
|
+
ends carry the same unresolved interpolation, so a match would be a coincidence
|
|
854
|
+
of variable naming. A data source says what it reads as a row instead.
|
|
855
|
+
|
|
856
|
+
**Serving a directory that declares nothing (#384).** Every estate this lane
|
|
857
|
+
exists to draw is a directory of `.tf` files and nothing else, and #378 chose
|
|
858
|
+
not to ask one for a `chant.config.ts` of its own — INTENTIUS/waterpark#88 was
|
|
859
|
+
withdrawn because the estate is more useful untouched. So `behold serve
|
|
860
|
+
<terraform-dir>` generates the reader config itself, outside the estate, and
|
|
861
|
+
points chant at it. The Invariant's one in-project write stays
|
|
862
|
+
`.behold/layout.json`; nothing is written under the served directory, and
|
|
863
|
+
`src/terraform-member.test.ts` asserts the estate's source stamp is unchanged
|
|
864
|
+
across a read. Three decisions, each a trade #384 left open and each measured on
|
|
865
|
+
water park's `access/` before it was taken:
|
|
866
|
+
|
|
867
|
+
1. **The lexicon is opt-in.** chant resolves the lexicon from the config file's
|
|
868
|
+
own location, so the generated project has to see it, and making it a
|
|
869
|
+
dependency would put `@cdktf/hcl2json`, a ~1.8 MB wasm blob, in the install
|
|
870
|
+
of every user who serves a chant project. `@intentius/chant-lexicon-terraform`
|
|
871
|
+
and `@cdktf/hcl2json` are therefore **optional peers**: declared in
|
|
872
|
+
package.json (the only place their versions are named, the refusal reads
|
|
873
|
+
them from there), never installed by behold, probed at serve and doctor time,
|
|
874
|
+
and refused with the one install line and where behold looked. The same gate
|
|
875
|
+
`behold demo` puts on a binary it does not ship. The lexicon's own chant peer
|
|
876
|
+
is what moved behold's `@intentius/chant` floor to `^0.61.0`: chant 0.54
|
|
877
|
+
loads no published version of it (`applyLineage is not a function`).
|
|
878
|
+
2. **Roots are discovered, and the skips are reported.** A root is a directory
|
|
879
|
+
with a `.tf` declaring a line-start `terraform {` or `provider "` block
|
|
880
|
+
beside a `resource`, `data` or `module` block, a regex probe at the depth
|
|
881
|
+
the choudoufu probe uses, no HCL parsed. #384 proposed the first half alone
|
|
882
|
+
("what a root has and a called module does not") and the estate refuted it:
|
|
883
|
+
water park's `modules/persona` is a shared module called by three roots and
|
|
884
|
+
its `versions.tf` is `baseline`'s byte for byte. So two exclusions stand
|
|
885
|
+
beside the probe, a directory under a `modules/` segment (Terraform's own
|
|
886
|
+
standard module structure; the roots that call it draw its blocks already)
|
|
887
|
+
and one with nothing to draw (`access/backends` is two backend fragments) —
|
|
888
|
+
and both are named in the graph's note with their reason, the way
|
|
889
|
+
`terraformElisionNote` names what a zoom left out. Measured on `access/`:
|
|
890
|
+
five roots (`envs/prod`, `identity`, `github`, `baseline`,
|
|
891
|
+
`satellites/waterpark-runner`), two skipped, `envs/dev` neither drawn nor
|
|
892
|
+
reported because it holds only a README.
|
|
893
|
+
3. **A `terraform` member kind, after chant and choudoufu.** #378 said there is
|
|
894
|
+
no such kind and meant it about *reading*: behold parses no HCL and the
|
|
895
|
+
render goes through chant. A kind whose `read` shells `chant graph` against a
|
|
896
|
+
generated config is a scaffold, not a second reader, and it inherits the
|
|
897
|
+
probe, the cache stamp, the doctor line and estate composition (#368) for
|
|
898
|
+
free, so a Terraform root composes in an estate beside a chant project and a
|
|
899
|
+
choudoufu estate at no extra cost. `src/terraform-member.ts` is the whole of
|
|
900
|
+
it; `detectProjectShape` answers a directory that is its own member with
|
|
901
|
+
`membersFrom: "probe"`, which is what retired #384's `no chant.config.ts
|
|
902
|
+
here` dead end.
|
|
903
|
+
|
|
904
|
+
The scratch project is `<tmpdir>/behold-tf-<hash of the estate path>`:
|
|
905
|
+
`behold-*` and cleared through `assertScratch` (src/scratch.ts), one directory
|
|
906
|
+
per estate reused across runs, asserted to be outside the estate before a byte
|
|
907
|
+
is written. It holds the generated `chant.config.ts` and two symlinks —
|
|
908
|
+
`node_modules` to behold's own, which is how the config resolves the lexicon,
|
|
909
|
+
and `estate` to the served directory, which is how each root's `dir` is spelled.
|
|
910
|
+
The second is not decoration: the lexicon sets a root's module boundary to the
|
|
911
|
+
root's own directory when its `dir` resolves outside the project root, so
|
|
912
|
+
absolute paths cost every `../modules/x` call the estate makes — 72 nodes and 6
|
|
913
|
+
resources on water park, against 247 and 43 through the symlink. Nothing in the
|
|
914
|
+
answer mentions the scratch path (a terraform entity carries `attrs.file`
|
|
915
|
+
relative to its root, and no `sourceLoc`).
|
|
916
|
+
|
|
917
|
+
### A choudoufu member's overlay is ownership; attribute drift is the plan (#404)
|
|
918
|
+
|
|
919
|
+
**Read a green choudoufu card as "this is ours", never as "nothing changed".**
|
|
920
|
+
The overlay is composed from `live-ls -json` and `live-plan -json`, and those
|
|
921
|
+
two documents answer one question — bound, unowned, omitted, or invisible to
|
|
922
|
+
the listing. Neither of them ever compares an attribute VALUE. A resource whose
|
|
923
|
+
tag, path or mutability was changed out of band still carries its markers, so
|
|
924
|
+
it is still bound, and it is still green.
|
|
925
|
+
|
|
926
|
+
The other half of the terminal's answer ("would a plan change anything?") is
|
|
927
|
+
a **separate, opt-in read**: `choudoufu plan -input=false -out=<tmp>` then
|
|
928
|
+
`choudoufu show -json <tmp>`, whose `resource_changes[].change.{actions,before,
|
|
929
|
+
after}` name the attributes and both their values. (`choudoufu plan -json` is
|
|
930
|
+
NOT this: on 0.16.0 it prints choudoufu's own ownership document, the same
|
|
931
|
+
shape as `live-plan -json`, with no attribute values in it at all. The
|
|
932
|
+
measurement is in src/choudoufu-plan.ts's header.) The plan file is written to
|
|
933
|
+
a scratch directory of behold's own and removed after rather than inside the served
|
|
934
|
+
member, which would be a write into someone's source.
|
|
935
|
+
|
|
936
|
+
It is opt-in because it costs differently: the ownership read is answered out
|
|
937
|
+
of the tagging index and is flat in the estate's size, while `plan` refreshes
|
|
938
|
+
every resource, one provider read per card, 301 of them on `terralith-4`. So:
|
|
939
|
+
|
|
940
|
+
- **Never on an ordinary overlay read.** `GET /api/overlay?plan=1` and
|
|
941
|
+
`GET /api/diff?plan=1` are the only things that spawn one, and the palette's
|
|
942
|
+
**"Re-check live with plan (attribute drift)"** row is the only thing in the
|
|
943
|
+
SPA that sends the flag. The cheap **"Re-check live (refresh drift)"** row is
|
|
944
|
+
deliberately left alone — it is `POST /api/refresh`, a primary-only re-observe
|
|
945
|
+
that captures a lanes frame and never composes the estate.
|
|
946
|
+
- **Cached under the member's source stamp**, the same key half `memberIr`
|
|
947
|
+
uses. A read that did NOT ask is served that stored answer, so a reload after
|
|
948
|
+
a refresh keeps showing the drift without paying for a second pass; a read
|
|
949
|
+
that DID ask always re-plans, because "re-check" means re-check.
|
|
950
|
+
- **The ownership verdict stays the card's colour.** A bound card that drifted
|
|
951
|
+
is still painted bound and still counted `bound`; it gains `attrs._planDrift`
|
|
952
|
+
and wears a dashed `--degraded` edge plus a `~ n attributes` corner glyph
|
|
953
|
+
(web/app.js `markDriftedCards`, the same post-render stamp as the carve and
|
|
954
|
+
operator marks). A card that is not bound is never marked: an unowned
|
|
955
|
+
object's planned `create` is an ownership fact the overlay already paints,
|
|
956
|
+
and `planDrift` drops creates for that reason.
|
|
957
|
+
- **`/api/overlay`'s meta carries `drift`**: `{read: false}` or `{read: true,
|
|
958
|
+
drifted: n}`. The two are different answers — "nobody looked" versus "looked,
|
|
959
|
+
nothing drifted", and the legend prints a count only for the second.
|
|
960
|
+
|
|
961
|
+
### A lexicon behold draws but cannot aim (#430)
|
|
962
|
+
|
|
963
|
+
`behold doctor`'s **targets** line. `SUBSTRATE_TARGET_VARS` (src/targets.ts)
|
|
964
|
+
names four lexicons against the nine behold draws, deliberately — it withholds
|
|
965
|
+
one until chant can actually be pointed at an emulator for it. That module also
|
|
966
|
+
names the cost: a landed chant change leaves a stale omission "until someone
|
|
967
|
+
notices", which happened once (#125) and left behold showing a floci-gcp pill it
|
|
968
|
+
could not aim a read at. This is the line someone reads instead.
|
|
969
|
+
|
|
970
|
+
`NO_AMBIENT_TARGET` (same module) is what stops it being noise: k8s and temporal
|
|
971
|
+
are not withheld, they are finished — they bind from `chant.config` rather than
|
|
972
|
+
an ambient variable, so there is nothing to override and nothing for a picker.
|
|
973
|
+
Those report as settled, not as a gap.
|
|
974
|
+
|
|
975
|
+
What the line CANNOT say, stated because the gap is the point: the honest
|
|
976
|
+
question is "chant can route this and behold does not list it", and behold
|
|
977
|
+
cannot ask it. Since chant 0.61 the endpoint variable is declared by the LEXICON
|
|
978
|
+
PLUGIN (`endpointEnvVarsFor`), not by a table in core, and behold installs no
|
|
979
|
+
lexicon plugins; the served project does. No chant CLI reports them either
|
|
980
|
+
(`lifecycle whoami --json` answers identity and says nothing about endpoints).
|
|
981
|
+
So the check warns on the weaker question it can answer and says it may be
|
|
982
|
+
over-reporting. The real fix is upstream, and #430 records the ask.
|
|
983
|
+
|
|
984
|
+
### A card with no live half is not an unobserved one (#429)
|
|
985
|
+
|
|
986
|
+
pinhole's painter takes a closed `_status` set and falls back to `neutral` for
|
|
987
|
+
anything else, `undefined` included. So on a mixed overlay a member with no live
|
|
988
|
+
half, a Terraform root, whose `live`/`overlay`/`env` are stripped in
|
|
989
|
+
src/terraform-member.ts — painted exactly like a card behold looked at and could
|
|
990
|
+
not see, while the legend counted only the latter. Measured on
|
|
991
|
+
`serve example-writes example-terraform-estate --env prod`: 37 Terraform cards
|
|
992
|
+
carrying no `_status`, 2 chant cards carrying `neutral`, one fill between them.
|
|
993
|
+
|
|
994
|
+
Those cards now carry `data-no-live` and the legend gives them their own row.
|
|
995
|
+
Named rather than folded into `unobserved`, because "I looked and could not see"
|
|
996
|
+
and "there is nothing here to look at" are different claims, the same argument
|
|
997
|
+
#399 makes for an unpriced card, and the same answer: mark it, do not merely
|
|
998
|
+
colour it. The mark is overlay-only; on the source graph no card has a status
|
|
999
|
+
and marking every one of them would say nothing.
|
|
1000
|
+
|
|
1001
|
+
This is presentation, not vocabulary. `statusVocabulary` gets no terraform row:
|
|
1002
|
+
there is no status to name. See #429 for the measurement that established it.
|
|
1003
|
+
|
|
1004
|
+
### The adoption sweep is off unless a serve asks (#412)
|
|
1005
|
+
|
|
1006
|
+
`live-plan -json` carries `adoptable[]` (live resources the estate-wide sweep
|
|
1007
|
+
matched to a declared instance by content) and `swept` (the types it listed in
|
|
1008
|
+
full). Both are empty on a run that did not ask the account-inventory question,
|
|
1009
|
+
and behold does not ask by default.
|
|
1010
|
+
|
|
1011
|
+
The opt-in is `TOFU_LIVE_COLLECT_UNCLAIMED=1` (`ADOPTION_SWEEP_VAR`,
|
|
1012
|
+
src/choudoufu-live.ts), carried through `serve.spawnEnv`, the seam
|
|
1013
|
+
`setChoudoufuSpawnEnv` already feeds every choudoufu spawn. The env var rather
|
|
1014
|
+
than `-adoption-only`, because that flag suppresses the resource diff the
|
|
1015
|
+
overlay needs. Off by default because the sweep is bounded by the ACCOUNT rather
|
|
1016
|
+
than the estate, and behold polls. `0` means no, which is choudoufu's own
|
|
1017
|
+
spelling, not a truthiness test.
|
|
1018
|
+
|
|
1019
|
+
**Content matching is a closed per-type table**, and that is what decides
|
|
1020
|
+
whether a declaration can ever produce an `adoptable[]` row. `matchTable`
|
|
1021
|
+
(choudoufu's internal/live/foreign/classify.go) lists eleven types against the
|
|
1022
|
+
identity-bearing arguments a match may use — `aws_vpc` on `cidr_block`,
|
|
1023
|
+
`aws_security_group`/`aws_lb`/`aws_sns_topic` and friends on `name`,
|
|
1024
|
+
`aws_subnet` on `cidr_block` + `availability_zone`, `aws_acm_certificate` on
|
|
1025
|
+
`domain_name`. A type absent from it, in the table's own words, "can never
|
|
1026
|
+
produce a bind candidate". `aws_iam_role` is absent, which is why #412's first
|
|
1027
|
+
attempt measured a shape that cannot match rather than a gap. A resource with a
|
|
1028
|
+
`count`/`for_each` key is skipped too, so a fixture wants a scalar.
|
|
1029
|
+
|
|
1030
|
+
`src/__fixtures__/choudoufu-live-plan-adoptable.json` is the real thing,
|
|
1031
|
+
recorded off floci: one scalar `aws_vpc` declaring a static `cidr_block`, its
|
|
1032
|
+
unmarked live twin created out of band, and the run asked with the opt-in. It
|
|
1033
|
+
carries the pairing M2 is about, a `NEEDS_DISCOVERY` omission and an
|
|
1034
|
+
`adoptable[]` row at the same address — plus `matched[]` naming what was
|
|
1035
|
+
compared and choudoufu's own `adopt_command`.
|
|
1036
|
+
|
|
1037
|
+
### A choudoufu estate's own references (#393)
|
|
1038
|
+
|
|
1039
|
+
`choudoufu live-check -json` states the roster and `references[]`, and that
|
|
1040
|
+
second list is *cross-estate by construction* — data sources filtered on
|
|
1041
|
+
another estate's marker tags. So a 301-resource terralith of roles, policies
|
|
1042
|
+
and attachments drew no edge at all and the graph asserted "nothing in this
|
|
1043
|
+
estate references anything else", which is false about every one of them.
|
|
1044
|
+
|
|
1045
|
+
The references were never missing; nothing was reading them. **A choudoufu
|
|
1046
|
+
member's read now also runs the terraform kind's reader over its own
|
|
1047
|
+
directory**, one root, the estate directory itself, through the same scratch
|
|
1048
|
+
project machinery above (nothing written under the estate), and joins the two
|
|
1049
|
+
documents by address. behold still parses no HCL: `src/choudoufu-refs.ts` is
|
|
1050
|
+
two lists of strings and the rules that match them, and its header is the
|
|
1051
|
+
argument for each. In short: the lexicon names *blocks* in a path of module
|
|
1052
|
+
*calls* (`estate/module.team_pod/aws_iam_role.pod_role`) and choudoufu names
|
|
1053
|
+
*instances* in a path of module *instances*
|
|
1054
|
+
(`module.team_pod["pod-a"].aws_iam_role.pod_role[0]`), so one lexicon edge is a
|
|
1055
|
+
product — kept inside one module instance (Terraform's scoping, not a guess),
|
|
1056
|
+
joined key to key when both ends expand over the same keys (266 true edges on
|
|
1057
|
+
`terralith-4` against 666 of which ~400 would be false), landing on a module
|
|
1058
|
+
call's whole interior, and dropped whole when either end is a `var`, a
|
|
1059
|
+
`locals` or anything else the roster declares no card for.
|
|
1060
|
+
|
|
1061
|
+
Every edge is `inferred` and carries the lexicon's own attribute name (`role`,
|
|
1062
|
+
`policy_arn`), so the card says what made the reference. **Without the
|
|
1063
|
+
lexicon there are no edges and the note says so** — carrying the terraform
|
|
1064
|
+
kind's own install line rather than the sentence behold has no reader to
|
|
1065
|
+
assert, and the lexicon's version is in the member's cache stamp, so
|
|
1066
|
+
installing it invalidates the edgeless IR rather than serving it forever.
|
|
1067
|
+
|
|
1068
|
+
The layout half is `packBoxComponents` (src/render.ts). src/edgeless.ts wraps a
|
|
1069
|
+
box whose cards reference *nothing*; a box that has edges is never wrapped, so
|
|
1070
|
+
`terralith-4` came back as a 110996 x 628 strip the day the join landed —
|
|
1071
|
+
dagre lays 42 connected components side by side on three ranks. The pass packs
|
|
1072
|
+
a box's components into shelves and resizes the box around them, re-laying each
|
|
1073
|
+
component on its own first (inside a cluster dagre interleaves them: a six-card
|
|
1074
|
+
cluster's bounding box spanned 46598 units) and wrapping a component that is a
|
|
1075
|
+
strip either way: the DNS fan is 19564 x 400 upright and 900 x 10046 on its
|
|
1076
|
+
side. A box under 4:1 is left exactly as it laid out. Measured: `terralith-4`
|
|
1077
|
+
7660 x 5162 with 266 edges, waterpark 3536 x 3138 at detail 2 and 7142 x 6974
|
|
1078
|
+
at detail 3 (from 4.2:1 and 15.1:1, the case #393's wrap did not answer).
|