@intentius/behold 0.18.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 +287 -71
- package/README.md +234 -190
- package/demos.json +15 -7
- package/dist/cli.js +11881 -11193
- package/example-argo-estate/package-lock.json +6 -6
- package/example-carve/app/package-lock.json +3 -3
- 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/package.json +2 -1
- package/web/app.js +385 -26
- package/web/index.html +23 -1
- 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
|
|
|
@@ -26,8 +26,8 @@ gets a structured `{code: "no-project"}` error from `/api/graph`, not a blank
|
|
|
26
26
|
graph.
|
|
27
27
|
|
|
28
28
|
In a checkout, `behold demo --list` prints a second block under the bundled
|
|
29
|
-
one: the eleven workbench entries from `workbench.json
|
|
30
|
-
catalog of the internal estates behold is developed against
|
|
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
31
|
`behold demo <entry>` load, `just example name="<entry>"` to serve one, and
|
|
32
32
|
"The workbench catalog" below for what each is and what it boots.
|
|
33
33
|
|
|
@@ -43,7 +43,7 @@ preview-locked, and one load runs at a time (409 otherwise).
|
|
|
43
43
|
|
|
44
44
|
## The read loop
|
|
45
45
|
|
|
46
|
-
0. **discover
|
|
46
|
+
0. **discover.** GET `/api` lists every route with a one-line description,
|
|
47
47
|
plus the server's version and a link back to this guide. Before the server
|
|
48
48
|
exists (or when a route answers with an error you'd have to guess at), run
|
|
49
49
|
`npx @intentius/behold doctor <dir> --json`: a read-only diagnosis of the
|
|
@@ -52,7 +52,7 @@ preview-locked, and one load runs at a time (409 otherwise).
|
|
|
52
52
|
substrate readiness and committed Ops. Each check is
|
|
53
53
|
`{name, status: pass|warn|fail, detail, fix}`; the process exits non-zero
|
|
54
54
|
iff something failed. It starts no server and changes nothing.
|
|
55
|
-
1. **observe
|
|
55
|
+
1. **observe.** GET `/api/graph` (JSON: `{ ir, svg, meta }`). The mixed graph
|
|
56
56
|
of the project, every node with `id`/`kind`/`lexicon`/`attrs`/`sourceLoc`. Drift
|
|
57
57
|
status, when present, is `attrs._status` (`good`=managed, `warn`=foreign,
|
|
58
58
|
`accent`=pending, `neutral`=unobserved, `runtime`=runtime child). With
|
|
@@ -67,7 +67,26 @@ preview-locked, and one load runs at a time (409 otherwise).
|
|
|
67
67
|
itself carries an address (chant#2045); `originSource: "inferred"` means
|
|
68
68
|
behold read the id's spelling and nothing more, and a `forge: "unknown"` id
|
|
69
69
|
is never resolved to a link on behold's guess.
|
|
70
|
-
`?
|
|
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
|
|
71
90
|
from each emitted `dist/ops/<name>/op.json`, with the current run painted
|
|
72
91
|
over it (`meta.run`, `meta.gate`); `/api/ops/<name>/status` reads that Op's
|
|
73
92
|
durable run status and pending gate (`chant run status --temporal`). A
|
|
@@ -77,29 +96,120 @@ preview-locked, and one load runs at a time (409 otherwise).
|
|
|
77
96
|
every rule, verbatim. `then: run(<op>)` is an edge to that Op's first step
|
|
78
97
|
when the Op is in the rendered set, and `attrs.dangling` when it isn't.
|
|
79
98
|
`meta.operator` on that same lens names the ConvergeOps the project declares
|
|
80
|
-
(chant's own `searchAttributes.Converge === "true"`, read from op.json
|
|
99
|
+
(chant's own `searchAttributes.Converge === "true"`, read from op.json, no
|
|
81
100
|
subprocess), and `/api/operator/status` fills the strip in from
|
|
82
101
|
`chant operator status --json`.
|
|
83
|
-
2. **focus
|
|
102
|
+
2. **focus.** Narrow with chant graph options as query params: `?detail=0..3`,
|
|
84
103
|
`?components=1`, `?logical=1`, `?lens=blast:<id>&down=1`, `?lens=lexicon:aws`,
|
|
85
104
|
`?env=`, `?stack=`, `?tier=`, `?target=`, `?collapse=1`.
|
|
86
105
|
`?collapse=1` (#393) shuts every member box holding more than 40 cards: the
|
|
87
106
|
box is drawn as ONE card carrying the count it was badged with (`301
|
|
88
107
|
resources · 84 bound · 85 unowned · 132 neutral`, in the estate's own
|
|
89
108
|
words), ids namespaced `box:<member>`, and the returned IR is the collapsed
|
|
90
|
-
one so every count and note speaks about the picture on screen
|
|
91
|
-
|
|
92
|
-
|
|
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;
|
|
93
124
|
edit there to change the estate (chant is the source of truth, not behold).
|
|
94
|
-
4. **find
|
|
125
|
+
4. **find.** ⌘K also takes an ADDRESS (#393). Two characters in, the palette
|
|
95
126
|
matches the ids and addresses of the graph the page is currently showing and
|
|
96
127
|
offers up to twelve `node: <address>` rows, prefix matches first, each with
|
|
97
128
|
its member and kind on a second line (two members of one estate can declare
|
|
98
|
-
the same address). Enter takes the same path a click on the card does
|
|
99
|
-
inspect pane and the highlight
|
|
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
|
|
100
131
|
web/app.js drives the same viewBox the wheel, the drag and "⤢ fit" drive.
|
|
101
132
|
Nothing here fetches, so it works in a static export too.
|
|
102
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
|
+
|
|
103
213
|
### The behaviour overlay (#398, M1 of #397)
|
|
104
214
|
|
|
105
215
|
What the estate costs and where it runs out of headroom, on `/api/overlay`
|
|
@@ -147,7 +257,7 @@ configured.
|
|
|
147
257
|
`meta.behaviour.refusal` is `{reason, remedy}` in the lexicon's own words,
|
|
148
258
|
printed as it came, and it is present **instead of** everything above: a
|
|
149
259
|
refusal emits no entity block at all and strips any that had arrived. The drift
|
|
150
|
-
overlay is untouched either way
|
|
260
|
+
overlay is untouched either way: nothing in this pass reads or writes
|
|
151
261
|
`_status`. `?logical=1` projects a different picture and carries no behaviour
|
|
152
262
|
block in M1.
|
|
153
263
|
|
|
@@ -172,13 +282,13 @@ the drift overlay's neutral (`pinTokensFor().neutralFill`) and is marked
|
|
|
172
282
|
Both ramps are derived from the active palette (`rampFor` in
|
|
173
283
|
`web/behaviour-scale.js`), the way #229 derives the chrome: cost is a single hue
|
|
174
284
|
off the accent walked in lightness away from the background — expensive is not a
|
|
175
|
-
verdict behold gets to paint
|
|
285
|
+
verdict behold gets to paint, and headroom rides the three status hues the
|
|
176
286
|
drift overlay already anchors, degraded → foreign → managed.
|
|
177
287
|
|
|
178
288
|
One legend per mode, in the Model tab. The Scope tab carries a totals row per
|
|
179
289
|
box and per estate for the active mode: `cost` quotes `meta.behaviour.boxes[key]`
|
|
180
290
|
and `.sum`, or `meta.behaviour.total` when the ENGINE stated one (labelled
|
|
181
|
-
"engine total"); `headroom` has no server-side aggregate
|
|
291
|
+
"engine total"); `headroom` has no server-side aggregate, since no engine states one —
|
|
182
292
|
so the SPA takes the scope's min and median itself and the row says "computed".
|
|
183
293
|
Every row carries `n priced · m unpriced`. In drift mode there is no row.
|
|
184
294
|
|
|
@@ -187,7 +297,7 @@ Everything either mode decides is pure in `web/behaviour-scale.js`, with
|
|
|
187
297
|
|
|
188
298
|
### Provenance on every figure, and the refusal (#401, M4)
|
|
189
299
|
|
|
190
|
-
Every figure the SPA shows
|
|
300
|
+
Every figure the SPA shows, the totals rows, the legend, and the inspect pane's
|
|
191
301
|
`behaviour` section — carries `{engine} {version} · {tolerance} · {basis}`
|
|
192
302
|
beside it, never once per page. `modeled` is spelled out on hover as "modeled,
|
|
193
303
|
not billed": #397's one prohibition is a prediction presented as a bill, and the
|
|
@@ -196,7 +306,7 @@ a badge over a set (a box, the estate) says `mixed` on any field the set
|
|
|
196
306
|
disagrees on rather than picking one.
|
|
197
307
|
|
|
198
308
|
`meta.behaviour.refusal` disables both behaviour modes in the View tab and in
|
|
199
|
-
⌘K
|
|
309
|
+
⌘K (visibly, with the lexicon's reason and remedy as their tooltip) and prints
|
|
200
310
|
those words verbatim where the legend would be. The drift overlay keeps
|
|
201
311
|
rendering; it never depended on an engine. `meta.behaviour.absent` disables the
|
|
202
312
|
same two modes with the absent line as their tooltip and prints **nothing**:
|
|
@@ -229,7 +339,7 @@ session.
|
|
|
229
339
|
{human, note}}`. Each entry carries `target`, `stage`
|
|
230
340
|
(`emitted`/`bridged`/`applied`), `graduated`, `note` and a retypeable
|
|
231
341
|
`applyCommand`. The same field appears on an ordinary `behold serve` whose
|
|
232
|
-
project directory carries a carveout
|
|
342
|
+
project directory carries a carveout (no report and no demo needed) and is
|
|
233
343
|
absent entirely when nothing has been carved.
|
|
234
344
|
|
|
235
345
|
In the graph, an `applied` address draws inside the chant member box (keeping
|
|
@@ -281,7 +391,7 @@ behold does not apply. To change the estate:
|
|
|
281
391
|
- `op-run <name>` — start the project's `ApplyOp` (code→cloud) or `ReconcileOp`
|
|
282
392
|
(cloud→code PR).
|
|
283
393
|
- `op-signal <name> <gate>` — approve a gate (e.g. a destructive apply).
|
|
284
|
-
- `op-status <name>` — watch phases; `op-report <name
|
|
394
|
+
- `op-status <name>` — watch phases; `op-report <name>`, the run report.
|
|
285
395
|
|
|
286
396
|
Every mutation is a gated, durable Temporal workflow with a human-confirmable gate
|
|
287
397
|
and saga rollback. There is no behold endpoint that mutates the cloud.
|
|
@@ -289,7 +399,7 @@ and saga rollback. There is no behold endpoint that mutates the cloud.
|
|
|
289
399
|
A run behold triggered is asked for structured per-step records (chant#1676:
|
|
290
400
|
`--progress-json` on the durable path, `--json` on the local one), so the ops
|
|
291
401
|
lens paints the run over the declared track and a pending gate renders as a card
|
|
292
|
-
with an Approve button. That button is `op-signal
|
|
402
|
+
with an Approve button. That button is `op-signal`, the same delegated write —
|
|
293
403
|
and nothing else about the run is behold's to decide: a stream that dies leaves
|
|
294
404
|
the playhead at the last settled step and says so, and a gate paints as pending
|
|
295
405
|
only when chant's `gateState` query named it.
|
|
@@ -333,7 +443,7 @@ A dispatched run is followed honestly (#165 §6, PR #350): only GitHub's own
|
|
|
333
443
|
`completed` + conclusion paints `ok`/`failed`; a poll-failure budget or the
|
|
334
444
|
follow deadline promotes the run to `lost` (chips frozen at last-observed,
|
|
335
445
|
the run possibly still live at its own page), never to a verdict. The adopted
|
|
336
|
-
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
|
|
337
447
|
operator's machine-state, outside the project tree, so the Invariant below
|
|
338
448
|
still names the whole in-project write surface. `GET /api/ci/run` reads the
|
|
339
449
|
record; `POST /api/ci/readopt` (also attempted once at boot) re-follows an
|
|
@@ -345,14 +455,14 @@ reader instead of nothing.
|
|
|
345
455
|
A project that declares a `ConvergeOp` gets an operator strip on the ops lens:
|
|
346
456
|
per loop, the last tick's own log line (verbatim, with its instant), the lease,
|
|
347
457
|
and how many gates are pending. **One** tick — `chant operator status` keeps only
|
|
348
|
-
`records.at(-1)
|
|
458
|
+
`records.at(-1)`, so the strip is never a timeline, and it says so.
|
|
349
459
|
|
|
350
460
|
The timeline is its own read: `GET /api/operator/log` → `chant operator log
|
|
351
461
|
--json` (chant#2029), the tick records and the gate resolutions against them
|
|
352
462
|
merged oldest-first. It lives in a panel that grows from the strip, and two rules
|
|
353
463
|
hold it there. It is **pulled**, on the click that opens it and never on a timer.
|
|
354
464
|
And it is **bounded** — `--limit` on every invocation, 50 by default and 200 at
|
|
355
|
-
most, `--since` when asked
|
|
465
|
+
most, `--since` when asked, because the ledger grows one line per tick forever;
|
|
356
466
|
the answer reports the window it used, so a full one can't read as the end of
|
|
357
467
|
history. The route checks the project's chant version *before* it spawns: below
|
|
358
468
|
0.53.1, `chant operator log` resolves to `chant operator`, the tick daemon, and
|
|
@@ -361,12 +471,12 @@ behold reading a history must never become behold running an operator.
|
|
|
361
471
|
Since chant 0.53.1 (chant#2027) a tick record carries an `id` and the
|
|
362
472
|
per-component verdicts it derived, and both ride through to
|
|
363
473
|
`[].lastTick.{id,components}`. The strip line names the tick (truncated), and
|
|
364
|
-
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
|
|
365
475
|
live `chant components status` read is joined by. They join **under** that read:
|
|
366
476
|
a tick only ever feeds the last tier of `componentStatusColor` (the
|
|
367
477
|
reconciliation verdict), only on a node the live read left unpainted, and only
|
|
368
478
|
while the tick is younger than fifteen of chant's own operator rounds. Past
|
|
369
|
-
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
|
|
370
480
|
nowhere to put "as of an hour ago". A chant older than 0.53.1 sends neither
|
|
371
481
|
field and every one of these paths is a no-op.
|
|
372
482
|
|
|
@@ -386,13 +496,13 @@ design (a local tick with no PR behind it has none) and never synthesized, so a
|
|
|
386
496
|
fact without one renders no link and no placeholder for one, and a url behold
|
|
387
497
|
would not put behind an `href` reads as no address at all.
|
|
388
498
|
|
|
389
|
-
A gate someone already resolved simply leaves `pendingGates
|
|
390
|
-
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
|
|
391
501
|
than attributing an approval to nobody. The timeline is where resolutions are
|
|
392
502
|
named, because there they are records rather than an inference.
|
|
393
503
|
|
|
394
504
|
An `OperatorStack` (chant#1940) declares the loop as a k8s estate, so it already
|
|
395
|
-
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
|
|
396
506
|
home, each CronJob as a converge tick — off chant's own
|
|
397
507
|
`app.kubernetes.io/component: converge-tick` labels, never a naming convention.
|
|
398
508
|
|
|
@@ -405,7 +515,7 @@ executor.
|
|
|
405
515
|
### The exceptions, and their exact size
|
|
406
516
|
|
|
407
517
|
`POST /api/layout` (#228) writes **one** file in the served project:
|
|
408
|
-
`.behold/layout.json
|
|
518
|
+
`.behold/layout.json`, the hand-layout sidecar, `{version, lenses: {<lens>:
|
|
409
519
|
{<node id>: {dx,dy,dw,dh}}}}`. That is the whole of behold's write surface
|
|
410
520
|
inside a project, and it does not weaken the invariant above:
|
|
411
521
|
|
|
@@ -414,7 +524,7 @@ inside a project, and it does not weaken the invariant above:
|
|
|
414
524
|
chant's, and a delta for a node that left the estate is dropped on read.
|
|
415
525
|
- It **never touches the cloud and never touches your source**. No `.ts`, no
|
|
416
526
|
`chant.config.ts`, no `.behold.json`. The path is `cfg.projectDir` + two
|
|
417
|
-
constants
|
|
527
|
+
constants; nothing from the request reaches the filesystem.
|
|
418
528
|
- It refuses politely when it shouldn't write: preview mode, a static-export
|
|
419
529
|
capture, a read-only project directory, an oversized or malformed body.
|
|
420
530
|
- It is **per-user state**, unlike `.behold.json` (config, meant to be tracked).
|
|
@@ -430,7 +540,7 @@ copy, they write only into `<copy>/app/carveout/`, and the directory they write
|
|
|
430
540
|
into is a scratch dir behold created inside a directory it copied for you a
|
|
431
541
|
minute earlier. `carve bridge` runs without `--apply-rewrites`, so the demo's
|
|
432
542
|
own Terraform is not edited either. The only request-derived value is `select`,
|
|
433
|
-
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
|
|
434
544
|
reaches the spawn's argv comes from a closed set read off disk. No cloud write,
|
|
435
545
|
no Terraform mutation, no edit to anyone's chant source.
|
|
436
546
|
|
|
@@ -447,7 +557,7 @@ src/choudoufu-route.test.ts asserts it stays absent, for the same reason
|
|
|
447
557
|
## Changing behold (for agents working on this repo)
|
|
448
558
|
|
|
449
559
|
The sections above are about driving a running behold. This one is about
|
|
450
|
-
changing it
|
|
560
|
+
changing it, the working rules that used to live in a handoff note and now
|
|
451
561
|
live here, with the enforceable ones enforced.
|
|
452
562
|
|
|
453
563
|
- **Branches and merges.** Work on a branch off `main` (an isolated worktree
|
|
@@ -458,7 +568,7 @@ live here, with the enforceable ones enforced.
|
|
|
458
568
|
next onto what landed.
|
|
459
569
|
- **The local gate** is `just check` (tsc, tests, build). Every vitest run
|
|
460
570
|
also writes `.vitest-last.json` (the json reporter, PR #351), and `just
|
|
461
|
-
test` keeps the reporter's full output in `vitest.log
|
|
571
|
+
test` keeps the reporter's full output in `vitest.log`, both gitignored —
|
|
462
572
|
because #334's one-shot first-run failures kept losing the failing file's
|
|
463
573
|
name. If the suite fails once and passes on rerun, those two files from the
|
|
464
574
|
first run are the evidence; attach them to #334 (`jq '.testResults[] |
|
|
@@ -466,7 +576,7 @@ live here, with the enforceable ones enforced.
|
|
|
466
576
|
- **Releasing** is `just release`, after the version-bump PR merged: it tags
|
|
467
577
|
`behold-v<version>`, pushes the tag (which is what runs `release.yml`), and
|
|
468
578
|
waits for npm to show the version. It refuses in every state where a tag
|
|
469
|
-
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
|
|
470
580
|
re-fires publish. Tags absent from origin (behold-v0.10.0, 0.10.1) stay
|
|
471
581
|
absent for that reason.
|
|
472
582
|
- **Scratch infrastructure** is governed by `src/scratch.ts`, asserted by
|
|
@@ -489,8 +599,8 @@ tool answers reads for it, and how it becomes a `GraphIR`. `src/member-kind.ts`
|
|
|
489
599
|
is the one table; `chant` is the first row and every member was that row
|
|
490
600
|
until the table existed. A new kind is:
|
|
491
601
|
|
|
492
|
-
1. A `registerMemberKind({ kind, probe, expects, via })` call in
|
|
493
|
-
`src/member-kind.ts
|
|
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
|
|
494
604
|
file's presence, a regex over a root file, or either: the choudoufu probe
|
|
495
605
|
takes the `estate.chdf.hcl` sidecar, choudoufu's leading form, OR a `live {`
|
|
496
606
|
block in a root *.tf, #387); `via.tool` stamps the binary and
|
|
@@ -505,7 +615,24 @@ until the table existed. A new kind is:
|
|
|
505
615
|
`estateNamespaceScopes` already dispatch through `memberKindOf(dir)`.
|
|
506
616
|
The two routes that render an estate — `/api/graph`'s estate branch and
|
|
507
617
|
`/api/overlay`'s — run the same passes in the same order and must not
|
|
508
|
-
fork per kind; a kind's differences live inside its `read`.
|
|
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.
|
|
509
636
|
3. A `registerPack({ lexicon, iconFor, fields })` in `src/render.ts`, or the
|
|
510
637
|
kind's cards lead with the alphabetically first two short attrs. A `fields`
|
|
511
638
|
function takes the box the card is drawn in as its second argument, so a
|
|
@@ -522,7 +649,8 @@ until the table existed. A new kind is:
|
|
|
522
649
|
`carveStatusReader` does; a PATH probe the way `src/demos.ts` does).
|
|
523
650
|
6. Tests: the probe and the object form in `src/project.test.ts`; dispatch
|
|
524
651
|
in `src/estate.test.ts`'s "#368" block, which registers a fake kind and
|
|
525
|
-
asserts chant members still go through exactly the calls they did
|
|
652
|
+
asserts chant members still go through exactly the calls they did, and its
|
|
653
|
+
"#427" block for the render passes; the
|
|
526
654
|
kind's own reader off recorded documents in `src/__fixtures__/`, with
|
|
527
655
|
provenance in prose above the load.
|
|
528
656
|
|
|
@@ -534,7 +662,7 @@ string is `chant`. Anything behold boots for a kind goes through
|
|
|
534
662
|
things used to be offered to every member whatever it was, and a new kind gets
|
|
535
663
|
all three answered for free:
|
|
536
664
|
|
|
537
|
-
- `/api/project` publishes `memberKinds
|
|
665
|
+
- `/api/project` publishes `memberKinds`, the served members' kinds, in
|
|
538
666
|
composition order. The SPA opens on the `resources` zoom when none of them is
|
|
539
667
|
`chant`, because `components` is a projection of a chant project's own
|
|
540
668
|
component DAG and a member without one opens on "the components lens doesn't
|
|
@@ -551,7 +679,7 @@ all three answered for free:
|
|
|
551
679
|
already returns and the SPA already reads as "no resource facet here".
|
|
552
680
|
|
|
553
681
|
A kind that grows components, or a substrate with an owner chain, changes those
|
|
554
|
-
answers where they are decided
|
|
682
|
+
answers where they are decided rather than per kind in a route.
|
|
555
683
|
|
|
556
684
|
### The workbench catalog
|
|
557
685
|
|
|
@@ -561,7 +689,7 @@ estates this checkout is developed against (#386, #388). Four rules, and
|
|
|
561
689
|
src/demos.ts holds them:
|
|
562
690
|
|
|
563
691
|
1. A third source, `local`, and a second file. A local entry's `path` is
|
|
564
|
-
relative to the directory of the catalog file that named it
|
|
692
|
+
relative to the directory of the catalog file that named it, the intentius
|
|
565
693
|
checkouts are siblings, so the workbench writes `../choudoufu`,
|
|
566
694
|
`../waterpark`, `../chant`, and the file stays committed and reproducible.
|
|
567
695
|
An entry whose path is not checked out is unsatisfiable exactly as a
|
|
@@ -598,11 +726,11 @@ its own graph and overlay counts as it runs.
|
|
|
598
726
|
|
|
599
727
|
| entry | what it serves | needs |
|
|
600
728
|
|---|---|---|
|
|
601
|
-
| `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
|
|
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 |
|
|
602
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 |
|
|
603
|
-
| `fountain-ops` | `../fountain-ops` in place with `--env local
|
|
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 |
|
|
604
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 |
|
|
605
|
-
| `choudoufu-cohort-s3` | `estate-gen`'s `s3` cohort rendered into the target
|
|
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 |
|
|
606
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 |
|
|
607
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 |
|
|
608
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 |
|
|
@@ -612,7 +740,7 @@ its own graph and overlay counts as it runs.
|
|
|
612
740
|
|
|
613
741
|
**The scratch, by name and port.** Each emulator is the entry's own, booted by
|
|
614
742
|
its up script and removed by the `scripts/down.sh` that script wrote into the
|
|
615
|
-
target
|
|
743
|
+
target rather than by pattern.
|
|
616
744
|
|
|
617
745
|
| entry | container | host port |
|
|
618
746
|
|---|---|---|
|
|
@@ -623,7 +751,7 @@ target — never by pattern.
|
|
|
623
751
|
| `terralith-1` | `behold-wb-terralith-1` | 4655 |
|
|
624
752
|
| `terralith-4` | `behold-wb-terralith-4` | 4656 |
|
|
625
753
|
| `terralith-4-adopt` | `behold-wb-terralith-4-adopt` | 4657 |
|
|
626
|
-
| `chant-*`, `waterpark` | none
|
|
754
|
+
| `chant-*`, `waterpark` | none (source reads, no substrate |) |
|
|
627
755
|
| `fountain-ops` | the k3d cluster `fountain-local`, which is the checkout's, not behold's | — |
|
|
628
756
|
|
|
629
757
|
The port is the floci's host binding and the entry's `serve.spawnEnv`
|
|
@@ -634,10 +762,22 @@ behold spawns talk to the same emulator.
|
|
|
634
762
|
loaded through `behold demo <entry> <tmp target> --port <p>` on its own port
|
|
635
763
|
from 4720 up, `/api/graph` asserted to carry nodes and a live entry's
|
|
636
764
|
`/api/overlay?env=live` to answer with its bound/unowned/neutral split, timings
|
|
637
|
-
per entry; eight entries and
|
|
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
|
+
|
|
638
778
|
`terralith-4-adopt` is asserted twice — 85 UNOWNED, then the
|
|
639
779
|
`live-import` line the up script printed, run by the script itself in the
|
|
640
|
-
target the way a person would, then 301 bound
|
|
780
|
+
target the way a person would, then 301 bound, because that write is the
|
|
641
781
|
person's, never behold's (#372). `waterpark` is checked for an untouched
|
|
642
782
|
checkout afterwards. `missingRequirements` decides what runs: a missing binary,
|
|
643
783
|
an unchecked-out sibling, an uninstalled chant example or the absent Terraform
|
|
@@ -646,7 +786,7 @@ run exits 0. `fountain-ops` is skipped by name everywhere, for the reason in
|
|
|
646
786
|
the table. A `behold-wb-*` container left standing at the end fails the run.
|
|
647
787
|
|
|
648
788
|
The seeded catalog (#389) found one thing missing in src/: a LONE choudoufu
|
|
649
|
-
estate
|
|
789
|
+
estate (every generated entry is one) served the no-project card, because the
|
|
650
790
|
single-project read is `chant graph <dir>` and such a directory has no lexicon
|
|
651
791
|
for it to read. `servesAsEstate` (src/member-kind.ts) is the predicate that
|
|
652
792
|
routes one directory of a non-chant kind through the estate compose path, where
|
|
@@ -660,28 +800,28 @@ A Terraform estate reaches behold through chant.
|
|
|
660
800
|
`@intentius/chant-lexicon-terraform` reads the HCL an estate already has and
|
|
661
801
|
emits one entity per block, so **a Terraform estate is a chant project whose
|
|
662
802
|
only lexicon is a reader** and `chant graph --format ir` serves it like any
|
|
663
|
-
other. behold parses no HCL and ships no HCL parser
|
|
803
|
+
other. behold parses no HCL and ships no HCL parser, the same posture
|
|
664
804
|
`src/carve-lens.ts` states for the carve report (#378).
|
|
665
805
|
|
|
666
806
|
Three passes turn what arrives into a picture (`src/terraform-lens.ts`), in this
|
|
667
807
|
order, guarded on the IR carrying terraform entities so every other estate gets
|
|
668
808
|
the identical object back:
|
|
669
809
|
|
|
670
|
-
1. **`normalizeTerraformNodes
|
|
810
|
+
1. **`normalizeTerraformNodes`.** A node's `kind` arrives as the entity class
|
|
671
811
|
(`Terraform::Resource`), not the resource type, so every card would be titled
|
|
672
812
|
and iconed the same. The type moves out of `attrs.address` into `kind` and
|
|
673
813
|
the block class lands in `attrs.block`. That is the shape a carve node
|
|
674
814
|
already has, which is why one presentation pack serves both.
|
|
675
|
-
2. **`groupTerraformByRoot
|
|
815
|
+
2. **`groupTerraformByRoot`.** Roots are a Terraform project's only grouping.
|
|
676
816
|
It retires itself when chant#2266 groups upstream. A node sits in exactly one
|
|
677
817
|
box, so a box every node has left is dropped rather than drawn empty (#393):
|
|
678
818
|
a served directory arrives composed, `composeStacks` boxes the whole member,
|
|
679
|
-
and the root boxes then take those same nodes
|
|
819
|
+
and the root boxes then take those same nodes, which is what put an empty
|
|
680
820
|
box named `access` on water park's canvas. What the member box was there to
|
|
681
821
|
say moves into the root box's title, which is `<member>/<root>` whenever the
|
|
682
822
|
estate holds more than one member and the bare root name when it does not
|
|
683
823
|
(two members with a root apiece named `prod` would otherwise merge silently).
|
|
684
|
-
3. **`filterTerraformCards
|
|
824
|
+
3. **`filterTerraformCards`.** What is a card, below.
|
|
685
825
|
|
|
686
826
|
**What is a card (#382).** Measured on a real estate: 247 nodes for 43
|
|
687
827
|
resources, four fifths of it not infrastructure.
|
|
@@ -709,7 +849,7 @@ of root names and drop the counts.
|
|
|
709
849
|
were recorded when a Terraform IR carried none, and lexicon 0.61.0 (chant#2265,
|
|
710
850
|
which resolves a block's `"${…}"` references) draws 390 over water park's five
|
|
711
851
|
roots with no change on this side. The one relationship that looked derivable
|
|
712
|
-
without it
|
|
852
|
+
without it (a cross-root read by name) was measured and refused (#381): both
|
|
713
853
|
ends carry the same unresolved interpolation, so a match would be a coincidence
|
|
714
854
|
of variable naming. A data source says what it reads as a row instead.
|
|
715
855
|
|
|
@@ -725,11 +865,11 @@ across a read. Three decisions, each a trade #384 left open and each measured on
|
|
|
725
865
|
water park's `access/` before it was taken:
|
|
726
866
|
|
|
727
867
|
1. **The lexicon is opt-in.** chant resolves the lexicon from the config file's
|
|
728
|
-
own location, so the generated project has to see it
|
|
868
|
+
own location, so the generated project has to see it, and making it a
|
|
729
869
|
dependency would put `@cdktf/hcl2json`, a ~1.8 MB wasm blob, in the install
|
|
730
870
|
of every user who serves a chant project. `@intentius/chant-lexicon-terraform`
|
|
731
871
|
and `@cdktf/hcl2json` are therefore **optional peers**: declared in
|
|
732
|
-
package.json (the only place their versions are named
|
|
872
|
+
package.json (the only place their versions are named, the refusal reads
|
|
733
873
|
them from there), never installed by behold, probed at serve and doctor time,
|
|
734
874
|
and refused with the one install line and where behold looked. The same gate
|
|
735
875
|
`behold demo` puts on a binary it does not ship. The lexicon's own chant peer
|
|
@@ -737,12 +877,12 @@ water park's `access/` before it was taken:
|
|
|
737
877
|
loads no published version of it (`applyLineage is not a function`).
|
|
738
878
|
2. **Roots are discovered, and the skips are reported.** A root is a directory
|
|
739
879
|
with a `.tf` declaring a line-start `terraform {` or `provider "` block
|
|
740
|
-
beside a `resource`, `data` or `module` block
|
|
880
|
+
beside a `resource`, `data` or `module` block, a regex probe at the depth
|
|
741
881
|
the choudoufu probe uses, no HCL parsed. #384 proposed the first half alone
|
|
742
882
|
("what a root has and a called module does not") and the estate refuted it:
|
|
743
883
|
water park's `modules/persona` is a shared module called by three roots and
|
|
744
884
|
its `versions.tf` is `baseline`'s byte for byte. So two exclusions stand
|
|
745
|
-
beside the probe
|
|
885
|
+
beside the probe, a directory under a `modules/` segment (Terraform's own
|
|
746
886
|
standard module structure; the roots that call it draw its blocks already)
|
|
747
887
|
and one with nothing to draw (`access/backends` is two backend fragments) —
|
|
748
888
|
and both are named in the graph's note with their reason, the way
|
|
@@ -755,7 +895,7 @@ water park's `access/` before it was taken:
|
|
|
755
895
|
render goes through chant. A kind whose `read` shells `chant graph` against a
|
|
756
896
|
generated config is a scaffold, not a second reader, and it inherits the
|
|
757
897
|
probe, the cache stamp, the doctor line and estate composition (#368) for
|
|
758
|
-
free
|
|
898
|
+
free, so a Terraform root composes in an estate beside a chant project and a
|
|
759
899
|
choudoufu estate at no extra cost. `src/terraform-member.ts` is the whole of
|
|
760
900
|
it; `detectProjectShape` answers a directory that is its own member with
|
|
761
901
|
`membersFrom: "probe"`, which is what retired #384's `no chant.config.ts
|
|
@@ -783,19 +923,19 @@ the listing. Neither of them ever compares an attribute VALUE. A resource whose
|
|
|
783
923
|
tag, path or mutability was changed out of band still carries its markers, so
|
|
784
924
|
it is still bound, and it is still green.
|
|
785
925
|
|
|
786
|
-
The other half of the terminal's answer
|
|
926
|
+
The other half of the terminal's answer ("would a plan change anything?") is
|
|
787
927
|
a **separate, opt-in read**: `choudoufu plan -input=false -out=<tmp>` then
|
|
788
928
|
`choudoufu show -json <tmp>`, whose `resource_changes[].change.{actions,before,
|
|
789
929
|
after}` name the attributes and both their values. (`choudoufu plan -json` is
|
|
790
930
|
NOT this: on 0.16.0 it prints choudoufu's own ownership document, the same
|
|
791
931
|
shape as `live-plan -json`, with no attribute values in it at all. The
|
|
792
932
|
measurement is in src/choudoufu-plan.ts's header.) The plan file is written to
|
|
793
|
-
a scratch directory of behold's own and removed after
|
|
933
|
+
a scratch directory of behold's own and removed after rather than inside the served
|
|
794
934
|
member, which would be a write into someone's source.
|
|
795
935
|
|
|
796
936
|
It is opt-in because it costs differently: the ownership read is answered out
|
|
797
937
|
of the tagging index and is flat in the estate's size, while `plan` refreshes
|
|
798
|
-
every resource
|
|
938
|
+
every resource, one provider read per card, 301 of them on `terralith-4`. So:
|
|
799
939
|
|
|
800
940
|
- **Never on an ordinary overlay read.** `GET /api/overlay?plan=1` and
|
|
801
941
|
`GET /api/diff?plan=1` are the only things that spawn one, and the palette's
|
|
@@ -811,12 +951,88 @@ every resource — one provider read per card, 301 of them on `terralith-4`. So:
|
|
|
811
951
|
is still painted bound and still counted `bound`; it gains `attrs._planDrift`
|
|
812
952
|
and wears a dashed `--degraded` edge plus a `~ n attributes` corner glyph
|
|
813
953
|
(web/app.js `markDriftedCards`, the same post-render stamp as the carve and
|
|
814
|
-
operator marks). A card that is not bound is never marked
|
|
954
|
+
operator marks). A card that is not bound is never marked: an unowned
|
|
815
955
|
object's planned `create` is an ownership fact the overlay already paints,
|
|
816
956
|
and `planDrift` drops creates for that reason.
|
|
817
957
|
- **`/api/overlay`'s meta carries `drift`**: `{read: false}` or `{read: true,
|
|
818
958
|
drifted: n}`. The two are different answers — "nobody looked" versus "looked,
|
|
819
|
-
nothing drifted"
|
|
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`.
|
|
820
1036
|
|
|
821
1037
|
### A choudoufu estate's own references (#393)
|
|
822
1038
|
|
|
@@ -828,8 +1044,8 @@ estate references anything else", which is false about every one of them.
|
|
|
828
1044
|
|
|
829
1045
|
The references were never missing; nothing was reading them. **A choudoufu
|
|
830
1046
|
member's read now also runs the terraform kind's reader over its own
|
|
831
|
-
directory
|
|
832
|
-
project machinery above (nothing written under the estate)
|
|
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
|
|
833
1049
|
documents by address. behold still parses no HCL: `src/choudoufu-refs.ts` is
|
|
834
1050
|
two lists of strings and the rules that match them, and its header is the
|
|
835
1051
|
argument for each. In short: the lexicon names *blocks* in a path of module
|
|
@@ -846,7 +1062,7 @@ Every edge is `inferred` and carries the lexicon's own attribute name (`role`,
|
|
|
846
1062
|
`policy_arn`), so the card says what made the reference. **Without the
|
|
847
1063
|
lexicon there are no edges and the note says so** — carrying the terraform
|
|
848
1064
|
kind's own install line rather than the sentence behold has no reader to
|
|
849
|
-
assert
|
|
1065
|
+
assert, and the lexicon's version is in the member's cache stamp, so
|
|
850
1066
|
installing it invalidates the edgeless IR rather than serving it forever.
|
|
851
1067
|
|
|
852
1068
|
The layout half is `packBoxComponents` (src/render.ts). src/edgeless.ts wraps a
|
|
@@ -856,7 +1072,7 @@ dagre lays 42 connected components side by side on three ranks. The pass packs
|
|
|
856
1072
|
a box's components into shelves and resizes the box around them, re-laying each
|
|
857
1073
|
component on its own first (inside a cluster dagre interleaves them: a six-card
|
|
858
1074
|
cluster's bounding box spanned 46598 units) and wrapping a component that is a
|
|
859
|
-
strip either way
|
|
1075
|
+
strip either way: the DNS fan is 19564 x 400 upright and 900 x 10046 on its
|
|
860
1076
|
side. A box under 4:1 is left exactly as it laid out. Measured: `terralith-4`
|
|
861
1077
|
7660 x 5162 with 266 edges, waterpark 3536 x 3138 at detail 2 and 7142 x 6974
|
|
862
1078
|
at detail 3 (from 4.2:1 and 15.1:1, the case #393's wrap did not answer).
|