@intentius/behold 0.18.0 → 0.20.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 +377 -76
- package/README.md +238 -190
- package/demos.json +17 -8
- package/dist/cli.js +13118 -11622
- package/example-argo-estate/README.md +8 -8
- package/example-argo-estate/app-a/package.json +2 -2
- package/example-argo-estate/app-b/package.json +2 -2
- package/example-argo-estate/chant.workspace.json +27 -0
- package/example-argo-estate/control-plane/package.json +2 -2
- package/example-argo-estate/package-lock.json +139 -62
- package/example-carve/README.md +14 -14
- package/example-carve/app/package-lock.json +75 -12
- package/example-carve/app/package.json +2 -2
- package/example-carve/legacy-tf/versions.tf +1 -1
- package/example-choudoufu-estate/README.md +4 -4
- package/example-choudoufu-estate/chant.workspace.json +32 -0
- package/example-choudoufu-estate/scripts/choudoufu-up.sh +21 -0
- package/example-flux-estate/README.md +5 -5
- package/example-flux-estate/app-a/package.json +2 -2
- package/example-flux-estate/app-b/package.json +2 -2
- package/example-flux-estate/chant.workspace.json +27 -0
- package/example-flux-estate/control-plane/package.json +2 -2
- package/example-flux-estate/package-lock.json +139 -62
- package/example-k8s/README.md +20 -20
- package/example-k8s/chant.config.ts +1 -1
- package/example-k8s/ops/k3d-apply.op.ts +1 -1
- package/example-k8s/package-lock.json +135 -217
- package/example-k8s/package.json +2 -3
- package/example-terraform-estate/README.md +109 -0
- package/example-terraform-estate/backends/backend.s3.tf +10 -0
- package/example-terraform-estate/chant.workspace.json +22 -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/README.md +5 -5
- package/example-writes/chant.config.ts +1 -1
- package/example-writes/ops/apply.op.ts +1 -1
- package/example-writes/ops/floci.op.ts +2 -2
- package/example-writes/ops/reconcile.op.ts +1 -1
- package/example-writes/package-lock.json +65 -178
- package/example-writes/package.json +2 -3
- package/package.json +7 -6
- package/web/app.js +439 -34
- package/web/behaviour-scale.js +32 -0
- package/web/behaviour-scale.test.js +39 -0
- 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/example-choudoufu-estate/.behold.json +0 -8
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
|
|
|
@@ -14,6 +14,23 @@ apply creds.
|
|
|
14
14
|
- Actions (delegated writes): `op-run` (start an `ApplyOp`/`ReconcileOp`),
|
|
15
15
|
`op-signal` (approve a gate), `op-status` / `op-report` (watch it).
|
|
16
16
|
|
|
17
|
+
## A declared workspace
|
|
18
|
+
|
|
19
|
+
A directory with a `chant.workspace.json` is a chant workspace (chant ≥ 0.81.0,
|
|
20
|
+
INTENTIUS/chant#2524). `behold serve <root>` serves it from chant's own member
|
|
21
|
+
list (`chant workspace ls --json`, read contract 1; any other contract is
|
|
22
|
+
refused), and reads each `chant` and `terraform` member through `chant
|
|
23
|
+
workspace graph --member <name>`. A `choudoufu` member keeps behold's own
|
|
24
|
+
reader, whose vocabulary (rungs, ownership, adoptable rows) chant's graph does
|
|
25
|
+
not carry. Node ids are `<member>/<id>` with the declared member name, the same
|
|
26
|
+
form chant composes. A member chant cannot read is drawn as one
|
|
27
|
+
`UnreadableMember` node carrying `attrs._unreadable: {code, message}` with
|
|
28
|
+
chant's reason code. An `other` member is listed on startup and in doctor and
|
|
29
|
+
draws nothing. `.behold.json`'s `members` are ignored beside a declaration;
|
|
30
|
+
`behold doctor --fix` writes the declaration for an estate that has only them.
|
|
31
|
+
Several directories on the command line (`behold serve a b c`) stay the loose
|
|
32
|
+
view behold composes itself.
|
|
33
|
+
|
|
17
34
|
## Getting a server
|
|
18
35
|
|
|
19
36
|
```sh
|
|
@@ -26,8 +43,8 @@ gets a structured `{code: "no-project"}` error from `/api/graph`, not a blank
|
|
|
26
43
|
graph.
|
|
27
44
|
|
|
28
45
|
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
|
|
46
|
+
one: the eleven workbench entries from `workbench.json` (this repo's own
|
|
47
|
+
catalog of the internal estates behold is developed against), the same
|
|
31
48
|
`behold demo <entry>` load, `just example name="<entry>"` to serve one, and
|
|
32
49
|
"The workbench catalog" below for what each is and what it boots.
|
|
33
50
|
|
|
@@ -43,7 +60,7 @@ preview-locked, and one load runs at a time (409 otherwise).
|
|
|
43
60
|
|
|
44
61
|
## The read loop
|
|
45
62
|
|
|
46
|
-
0. **discover
|
|
63
|
+
0. **discover.** GET `/api` lists every route with a one-line description,
|
|
47
64
|
plus the server's version and a link back to this guide. Before the server
|
|
48
65
|
exists (or when a route answers with an error you'd have to guess at), run
|
|
49
66
|
`npx @intentius/behold doctor <dir> --json`: a read-only diagnosis of the
|
|
@@ -52,7 +69,7 @@ preview-locked, and one load runs at a time (409 otherwise).
|
|
|
52
69
|
substrate readiness and committed Ops. Each check is
|
|
53
70
|
`{name, status: pass|warn|fail, detail, fix}`; the process exits non-zero
|
|
54
71
|
iff something failed. It starts no server and changes nothing.
|
|
55
|
-
1. **observe
|
|
72
|
+
1. **observe.** GET `/api/graph` (JSON: `{ ir, svg, meta }`). The mixed graph
|
|
56
73
|
of the project, every node with `id`/`kind`/`lexicon`/`attrs`/`sourceLoc`. Drift
|
|
57
74
|
status, when present, is `attrs._status` (`good`=managed, `warn`=foreign,
|
|
58
75
|
`accent`=pending, `neutral`=unobserved, `runtime`=runtime child). With
|
|
@@ -67,7 +84,26 @@ preview-locked, and one load runs at a time (409 otherwise).
|
|
|
67
84
|
itself carries an address (chant#2045); `originSource: "inferred"` means
|
|
68
85
|
behold read the id's spelling and nothing more, and a `forge: "unknown"` id
|
|
69
86
|
is never resolved to a link on behold's guess.
|
|
70
|
-
`?
|
|
87
|
+
`?stacks=1` is the apply-order lens (#426): the cross-stack ordering chant
|
|
88
|
+
computes at synthesis and prints from `chant graph --stacks --json`, which
|
|
89
|
+
behold read nowhere and re-derived a subset of. One card per stack (a
|
|
90
|
+
lexicon partition), one box per wave, edges consumer to producer;
|
|
91
|
+
`meta.stacks` carries `order`, `waves` and `cycles` verbatim. It is
|
|
92
|
+
source-only (no env, no credentials, no substrate) and asks chant for the
|
|
93
|
+
project ROOT, not `graphPath`, because `runStackGraph` only sees
|
|
94
|
+
`ops/*.op.ts` from there (measured: `example-writes` answers `[aws,
|
|
95
|
+
temporal]` from `.` and `[aws]` from `src`). Single project only: an estate
|
|
96
|
+
is refused with `422 stacks-estate`, since `composeStacks` pools members
|
|
97
|
+
into one bucket per bare lexicon name and two members' `aws` stacks are not
|
|
98
|
+
one stack. A cycle is NOT a refusal — chant exits 1 with valid JSON and the
|
|
99
|
+
entangled stacks absent from `order`/`waves`, so they are drawn unwaved in
|
|
100
|
+
their own box and the note says so; nothing invents a wave number for a
|
|
101
|
+
stack chant could not order. `/api/project`'s `stacksCapable` gates the
|
|
102
|
+
stop. Note that `chant graph --stacks --json` is malformed today on two of
|
|
103
|
+
chant's own examples (an edge with no `from`, a `null` in `order` and in a
|
|
104
|
+
wave); `sanitizeStackGraph` treats `nodes` as the roster and drops anything
|
|
105
|
+
named nowhere else.
|
|
106
|
+
`?ops=1` is the ops lens: the project's declared Ops as a phase track, read
|
|
71
107
|
from each emitted `dist/ops/<name>/op.json`, with the current run painted
|
|
72
108
|
over it (`meta.run`, `meta.gate`); `/api/ops/<name>/status` reads that Op's
|
|
73
109
|
durable run status and pending gate (`chant run status --temporal`). A
|
|
@@ -77,29 +113,120 @@ preview-locked, and one load runs at a time (409 otherwise).
|
|
|
77
113
|
every rule, verbatim. `then: run(<op>)` is an edge to that Op's first step
|
|
78
114
|
when the Op is in the rendered set, and `attrs.dangling` when it isn't.
|
|
79
115
|
`meta.operator` on that same lens names the ConvergeOps the project declares
|
|
80
|
-
(chant's own `searchAttributes.Converge === "true"`, read from op.json
|
|
116
|
+
(chant's own `searchAttributes.Converge === "true"`, read from op.json, no
|
|
81
117
|
subprocess), and `/api/operator/status` fills the strip in from
|
|
82
118
|
`chant operator status --json`.
|
|
83
|
-
2. **focus
|
|
119
|
+
2. **focus.** Narrow with chant graph options as query params: `?detail=0..3`,
|
|
84
120
|
`?components=1`, `?logical=1`, `?lens=blast:<id>&down=1`, `?lens=lexicon:aws`,
|
|
85
121
|
`?env=`, `?stack=`, `?tier=`, `?target=`, `?collapse=1`.
|
|
86
122
|
`?collapse=1` (#393) shuts every member box holding more than 40 cards: the
|
|
87
123
|
box is drawn as ONE card carrying the count it was badged with (`301
|
|
88
124
|
resources · 84 bound · 85 unowned · 132 neutral`, in the estate's own
|
|
89
125
|
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
|
-
|
|
126
|
+
one so every count and note speaks about the picture on screen — except its
|
|
127
|
+
NOTE, which is written about the expanded estate plus one collapse clause
|
|
128
|
+
(#396): a shut box hides what the estate references rather than changing it.
|
|
129
|
+
⌘K → "Collapse large boxes" / "Expand all". Below the limit it changes
|
|
130
|
+
nothing. **The toggle costs no live read** (#396): a member's overlay
|
|
131
|
+
document is cached under its source stamp and the read's own options, the
|
|
132
|
+
same key half `memberIr` and #404's plan cache use, so collapse and expand
|
|
133
|
+
are re-renders of a document behold already holds. `src/overlay-ir.ts` is
|
|
134
|
+
that cache and its header is the stated exception to src/member-ir.ts's
|
|
135
|
+
"a live read is never cached"; it names everything that drops an entry —
|
|
136
|
+
`POST /api/refresh` ("Re-check live"), `GET /api/overlay?plan=1`
|
|
137
|
+
("Re-check live with plan", which re-checks both halves), the capture that
|
|
138
|
+
ends an Op run, and the member's source moving. A change made to the account
|
|
139
|
+
by something that is not behold is what the re-check rows are for.
|
|
140
|
+
3. **inspect.** A node's `sourceLoc.file` is the typed source that declared it;
|
|
93
141
|
edit there to change the estate (chant is the source of truth, not behold).
|
|
94
|
-
4. **find
|
|
142
|
+
4. **find.** ⌘K also takes an ADDRESS (#393). Two characters in, the palette
|
|
95
143
|
matches the ids and addresses of the graph the page is currently showing and
|
|
96
144
|
offers up to twelve `node: <address>` rows, prefix matches first, each with
|
|
97
145
|
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
|
|
146
|
+
the same address). Enter takes the same path a click on the card does (the
|
|
147
|
+
inspect pane and the highlight) and pans the graph onto it: `revealNode` in
|
|
100
148
|
web/app.js drives the same viewBox the wheel, the drag and "⤢ fit" drive.
|
|
101
149
|
Nothing here fetches, so it works in a static export too.
|
|
102
150
|
|
|
151
|
+
### The picture arrives in pieces (#422)
|
|
152
|
+
|
|
153
|
+
`GET /api/overlay?progressive=1` answers at once with the estate composed from
|
|
154
|
+
every member's SOURCE, each node carrying `attrs._pendingRead`, and then
|
|
155
|
+
broadcasts one `member` SSE frame per member as its own live read settles —
|
|
156
|
+
`{member, statuses: {<composed id>: <status>}, unobserved?}`, or `{error}` if
|
|
157
|
+
the pass itself failed. `meta.mode` is `progressive` and `meta.pending` names
|
|
158
|
+
the members not yet read.
|
|
159
|
+
|
|
160
|
+
**Opt-in, and never the default.** `/api/overlay` without the flag is byte-for-
|
|
161
|
+
byte what it was: the blocking, complete live answer this guide's read loop
|
|
162
|
+
names, which is what an agent GETs and what `src/export.ts` captures into a
|
|
163
|
+
static bundle. A route that answered with members it had not read would hand an
|
|
164
|
+
agent a half-picture it could not know was half, and freeze a bundle with
|
|
165
|
+
members permanently still-reading.
|
|
166
|
+
|
|
167
|
+
**Pending is a third claim.** `_unobserved` is "behold looked and could not
|
|
168
|
+
see"; `neutral` is "the plan did not mention it"; `_pendingRead` is "the read
|
|
169
|
+
has not finished". It rides its own attr rather than a fifth `_status` because
|
|
170
|
+
a fifth `_status` is not paintable — pinhole's painter takes a closed set and
|
|
171
|
+
falls back to `neutral` for anything else, which is `unobserved`, the exact
|
|
172
|
+
wrong answer. The SPA draws it dashed and dimmed, never filled: the fill is the
|
|
173
|
+
status channel and a pending card has no status yet.
|
|
174
|
+
|
|
175
|
+
No live result is cached to build the first picture and none is reused. A
|
|
176
|
+
pending member becomes a real one only when its own read completes, and the
|
|
177
|
+
live pass a progressive request starts is the same one per member under the
|
|
178
|
+
same budget: a progressive load costs the estate no extra spawn.
|
|
179
|
+
|
|
180
|
+
### What a read cost (#420, #421)
|
|
181
|
+
|
|
182
|
+
`GET /api/doctor` serves `diagnose()`, the same report `behold doctor` prints,
|
|
183
|
+
which until #421 only the CLI could ask for — with two extra blocks on it:
|
|
184
|
+
|
|
185
|
+
- `reads`, the ledger from `src/read-stats.ts`. Every scheduled chant read
|
|
186
|
+
passes through `ReadScheduler`, so each one is filed there with its running
|
|
187
|
+
time, its outcome, and which side of the source/live split it sits on. A bare
|
|
188
|
+
`chant graph` is the member's own TypeScript evaluation; anything with
|
|
189
|
+
`--live`/`--overlay`, and every other verb `isScheduledRead` allows, reaches
|
|
190
|
+
past it. `shared` counts subscribers handed an in-flight spawn, work not
|
|
191
|
+
done, never a run.
|
|
192
|
+
- `cache`, `memberIrCacheStats()`.
|
|
193
|
+
|
|
194
|
+
`GET /api/doctor?reads=1` is the same two blocks WITHOUT the diagnosis, and it
|
|
195
|
+
is what the SPA asks for. The split is the one #421 states: the full report is
|
|
196
|
+
for a person diagnosing and the panel line is for a person waiting. It is also a
|
|
197
|
+
cost split too: `diagnose()` probes docker, gh, helm and k3d through
|
|
198
|
+
src/substrates.ts, whose `probe` has no timeout and no kill, and the SPA asks
|
|
199
|
+
after every load settles. A static export asks for neither; `refreshReadCost`
|
|
200
|
+
returns on `staticMode` rather than relying on the bundle's own 404.
|
|
201
|
+
|
|
202
|
+
`reads.inFlight` is `{running, queued}` off the scheduler itself. The ledger
|
|
203
|
+
cannot answer it: a read still in flight has finished nothing, so it has filed
|
|
204
|
+
nothing. "What is queued now" is what someone watching a slow read wants.
|
|
205
|
+
|
|
206
|
+
Three things to know before reading a number off it. `queuedMs` is contention
|
|
207
|
+
**in the scheduler** (the HTTP-versus-poll-versus-capture competition #367
|
|
208
|
+
bounded) rather than the whole of what a member waits: the estate fan-out bounds
|
|
209
|
+
upstream through `mapPool` (src/estate.ts), so a three-member estate measured
|
|
210
|
+
3033/3035/2218 ms of running time with `queuedMs` 0 against 5.2 s of wall clock.
|
|
211
|
+
The gap is the upstream wait and #422 is where it gets measured.
|
|
212
|
+
|
|
213
|
+
`reads.unmeasured` counts the served members the ledger is BLIND to, and the
|
|
214
|
+
panel line prints it rather than quietly excluding them. Only reads that pass
|
|
215
|
+
`ReadScheduler` are filed, which is every chant read and therefore a terraform
|
|
216
|
+
member too (its `via.read` shells `chant graph`, src/terraform-member.ts). A
|
|
217
|
+
choudoufu member spawns its own binary through its own helper and is filed
|
|
218
|
+
nowhere, so on a chant+choudoufu estate the slowest card may be one that never
|
|
219
|
+
reached `recent` — "slowest 3s · 1 not measured" is the honest form of that, and
|
|
220
|
+
an unqualified "slowest 3s" is not.
|
|
221
|
+
|
|
222
|
+
And preview mode keeps the totals but drops `recent`, whose samples are keyed by
|
|
223
|
+
member directory.
|
|
224
|
+
|
|
225
|
+
The SPA asks once after each load settles and shows the answer on the loading
|
|
226
|
+
scrim the *next* time one goes up (`web/read-cost.js`, `#loading-cost`): the
|
|
227
|
+
slowest recent member, because an estate read finishes when its last member
|
|
228
|
+
does, so a sum would report a number nobody waits for.
|
|
229
|
+
|
|
103
230
|
### The behaviour overlay (#398, M1 of #397)
|
|
104
231
|
|
|
105
232
|
What the estate costs and where it runs out of headroom, on `/api/overlay`
|
|
@@ -129,7 +256,8 @@ Two sources, in this order:
|
|
|
129
256
|
1. `attrs._behaviour` on the nodes, as `chant graph --live --overlay` painted
|
|
130
257
|
them, with `meta._behaviour` beside them for the graph-level half.
|
|
131
258
|
2. `behaviour.<env>.json` at a member's root — `{meta, entities: {<address>:
|
|
132
|
-
block}}` — read **only** when no node of that member carries the attr
|
|
259
|
+
block}}` — read **only** when no node of that member carries the attr and
|
|
260
|
+
the member's read carried no `meta._behaviour` either (#402), and
|
|
133
261
|
**only on the overlay** (`/api/graph` is the source graph; a prediction
|
|
134
262
|
about a live account has no business on it). The file's keys are the
|
|
135
263
|
member's own addresses; behold prefixes them to reach the composed id
|
|
@@ -147,10 +275,45 @@ configured.
|
|
|
147
275
|
`meta.behaviour.refusal` is `{reason, remedy}` in the lexicon's own words,
|
|
148
276
|
printed as it came, and it is present **instead of** everything above: a
|
|
149
277
|
refusal emits no entity block at all and strips any that had arrived. The drift
|
|
150
|
-
overlay is untouched either way
|
|
278
|
+
overlay is untouched either way: nothing in this pass reads or writes
|
|
151
279
|
`_status`. `?logical=1` projects a different picture and carries no behaviour
|
|
152
280
|
block in M1.
|
|
153
281
|
|
|
282
|
+
### Live versus declared (#402, M5)
|
|
283
|
+
|
|
284
|
+
With a traffic level, `/api/overlay` predicts each member twice and carries
|
|
285
|
+
both figures and their difference. The level is `?traffic=` on the request,
|
|
286
|
+
else `.behold.json`'s `{"behaviour": {"traffic": "100 rps, p50"}}`, else none;
|
|
287
|
+
with none nothing is asked for and the payload is exactly M1's. behold never
|
|
288
|
+
defaults a level, because every figure is *at* one.
|
|
289
|
+
|
|
290
|
+
`meta.behaviour` gains `live`, `declared` (each `{perHour, currency, basis}`,
|
|
291
|
+
where `basis` is `total` for a figure the engine stated and `sum` for behold
|
|
292
|
+
adding the engine's per-entity figures) and `delta` (`{perHour, currency}`,
|
|
293
|
+
live minus declared), plus the same three per member under `members[<name>]`.
|
|
294
|
+
A delta is only ever like with like: one currency, and two totals or two sums.
|
|
295
|
+
Anything else yields no delta and a line in `diagnostics` saying why. A node
|
|
296
|
+
also carries the file's block under `attrs._behaviourDeclared`, including a
|
|
297
|
+
node the account no longer holds, which has no `_behaviour` at all.
|
|
298
|
+
|
|
299
|
+
Who reads what is `src/behaviour-delta.ts`'s header. A chant member's live
|
|
300
|
+
half is its own overlay read with `--traffic` added (sent only to a chant at
|
|
301
|
+
0.75.0 or newer; an older one rejects the flag). A choudoufu member is not
|
|
302
|
+
read by chant, so behold predicts it through a generated reader project with
|
|
303
|
+
`binary: "choudoufu"` (the Terraform member's scratch project, under
|
|
304
|
+
`behold-chdf-*`), which needs the optional terraform lexicon. A terraform
|
|
305
|
+
member has no live half and so no delta.
|
|
306
|
+
|
|
307
|
+
The declared half goes through `memberIr` and is cached under the member's
|
|
308
|
+
source stamp plus the traffic level. The live half goes through `overlayIr`
|
|
309
|
+
and is dropped by whatever drops the overlay. With a level set, the
|
|
310
|
+
`behaviour.<env>.json` documents are not read at all: a fixture's figures
|
|
311
|
+
beside a real prediction would be two answers to one question.
|
|
312
|
+
|
|
313
|
+
`/api/graph?traffic=` (or the config default) paints the declared side alone
|
|
314
|
+
as `attrs._behaviour` with `meta.behaviour.declared`. The file is the only
|
|
315
|
+
estate that route has, so it carries no live half and no delta.
|
|
316
|
+
|
|
154
317
|
### Colour by drift, cost or headroom (#399, M2)
|
|
155
318
|
|
|
156
319
|
The SPA colours the graph by one of three modes. The mode is **client-side
|
|
@@ -172,13 +335,13 @@ the drift overlay's neutral (`pinTokensFor().neutralFill`) and is marked
|
|
|
172
335
|
Both ramps are derived from the active palette (`rampFor` in
|
|
173
336
|
`web/behaviour-scale.js`), the way #229 derives the chrome: cost is a single hue
|
|
174
337
|
off the accent walked in lightness away from the background — expensive is not a
|
|
175
|
-
verdict behold gets to paint
|
|
338
|
+
verdict behold gets to paint, and headroom rides the three status hues the
|
|
176
339
|
drift overlay already anchors, degraded → foreign → managed.
|
|
177
340
|
|
|
178
341
|
One legend per mode, in the Model tab. The Scope tab carries a totals row per
|
|
179
342
|
box and per estate for the active mode: `cost` quotes `meta.behaviour.boxes[key]`
|
|
180
343
|
and `.sum`, or `meta.behaviour.total` when the ENGINE stated one (labelled
|
|
181
|
-
"engine total"); `headroom` has no server-side aggregate
|
|
344
|
+
"engine total"); `headroom` has no server-side aggregate, since no engine states one —
|
|
182
345
|
so the SPA takes the scope's min and median itself and the row says "computed".
|
|
183
346
|
Every row carries `n priced · m unpriced`. In drift mode there is no row.
|
|
184
347
|
|
|
@@ -187,7 +350,7 @@ Everything either mode decides is pure in `web/behaviour-scale.js`, with
|
|
|
187
350
|
|
|
188
351
|
### Provenance on every figure, and the refusal (#401, M4)
|
|
189
352
|
|
|
190
|
-
Every figure the SPA shows
|
|
353
|
+
Every figure the SPA shows, the totals rows, the legend, and the inspect pane's
|
|
191
354
|
`behaviour` section — carries `{engine} {version} · {tolerance} · {basis}`
|
|
192
355
|
beside it, never once per page. `modeled` is spelled out on hover as "modeled,
|
|
193
356
|
not billed": #397's one prohibition is a prediction presented as a bill, and the
|
|
@@ -196,7 +359,7 @@ a badge over a set (a box, the estate) says `mixed` on any field the set
|
|
|
196
359
|
disagrees on rather than picking one.
|
|
197
360
|
|
|
198
361
|
`meta.behaviour.refusal` disables both behaviour modes in the View tab and in
|
|
199
|
-
⌘K
|
|
362
|
+
⌘K (visibly, with the lexicon's reason and remedy as their tooltip) and prints
|
|
200
363
|
those words verbatim where the legend would be. The drift overlay keeps
|
|
201
364
|
rendering; it never depended on an engine. `meta.behaviour.absent` disables the
|
|
202
365
|
same two modes with the absent line as their tooltip and prints **nothing**:
|
|
@@ -229,7 +392,7 @@ session.
|
|
|
229
392
|
{human, note}}`. Each entry carries `target`, `stage`
|
|
230
393
|
(`emitted`/`bridged`/`applied`), `graduated`, `note` and a retypeable
|
|
231
394
|
`applyCommand`. The same field appears on an ordinary `behold serve` whose
|
|
232
|
-
project directory carries a carveout
|
|
395
|
+
project directory carries a carveout (no report and no demo needed) and is
|
|
233
396
|
absent entirely when nothing has been carved.
|
|
234
397
|
|
|
235
398
|
In the graph, an `applied` address draws inside the chant member box (keeping
|
|
@@ -281,7 +444,7 @@ behold does not apply. To change the estate:
|
|
|
281
444
|
- `op-run <name>` — start the project's `ApplyOp` (code→cloud) or `ReconcileOp`
|
|
282
445
|
(cloud→code PR).
|
|
283
446
|
- `op-signal <name> <gate>` — approve a gate (e.g. a destructive apply).
|
|
284
|
-
- `op-status <name>` — watch phases; `op-report <name
|
|
447
|
+
- `op-status <name>` — watch phases; `op-report <name>`, the run report.
|
|
285
448
|
|
|
286
449
|
Every mutation is a gated, durable Temporal workflow with a human-confirmable gate
|
|
287
450
|
and saga rollback. There is no behold endpoint that mutates the cloud.
|
|
@@ -289,7 +452,7 @@ and saga rollback. There is no behold endpoint that mutates the cloud.
|
|
|
289
452
|
A run behold triggered is asked for structured per-step records (chant#1676:
|
|
290
453
|
`--progress-json` on the durable path, `--json` on the local one), so the ops
|
|
291
454
|
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
|
|
455
|
+
with an Approve button. That button is `op-signal`, the same delegated write —
|
|
293
456
|
and nothing else about the run is behold's to decide: a stream that dies leaves
|
|
294
457
|
the playhead at the last settled step and says so, and a gate paints as pending
|
|
295
458
|
only when chant's `gateState` query named it.
|
|
@@ -333,7 +496,7 @@ A dispatched run is followed honestly (#165 §6, PR #350): only GitHub's own
|
|
|
333
496
|
`completed` + conclusion paints `ok`/`failed`; a poll-failure budget or the
|
|
334
497
|
follow deadline promotes the run to `lost` (chips frozen at last-observed,
|
|
335
498
|
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
|
|
499
|
+
run id is persisted as one JSON per project under `~/.behold/ci-runs/`, the
|
|
337
500
|
operator's machine-state, outside the project tree, so the Invariant below
|
|
338
501
|
still names the whole in-project write surface. `GET /api/ci/run` reads the
|
|
339
502
|
record; `POST /api/ci/readopt` (also attempted once at boot) re-follows an
|
|
@@ -345,14 +508,14 @@ reader instead of nothing.
|
|
|
345
508
|
A project that declares a `ConvergeOp` gets an operator strip on the ops lens:
|
|
346
509
|
per loop, the last tick's own log line (verbatim, with its instant), the lease,
|
|
347
510
|
and how many gates are pending. **One** tick — `chant operator status` keeps only
|
|
348
|
-
`records.at(-1)
|
|
511
|
+
`records.at(-1)`, so the strip is never a timeline, and it says so.
|
|
349
512
|
|
|
350
513
|
The timeline is its own read: `GET /api/operator/log` → `chant operator log
|
|
351
514
|
--json` (chant#2029), the tick records and the gate resolutions against them
|
|
352
515
|
merged oldest-first. It lives in a panel that grows from the strip, and two rules
|
|
353
516
|
hold it there. It is **pulled**, on the click that opens it and never on a timer.
|
|
354
517
|
And it is **bounded** — `--limit` on every invocation, 50 by default and 200 at
|
|
355
|
-
most, `--since` when asked
|
|
518
|
+
most, `--since` when asked, because the ledger grows one line per tick forever;
|
|
356
519
|
the answer reports the window it used, so a full one can't read as the end of
|
|
357
520
|
history. The route checks the project's chant version *before* it spawns: below
|
|
358
521
|
0.53.1, `chant operator log` resolves to `chant operator`, the tick daemon, and
|
|
@@ -361,12 +524,12 @@ behold reading a history must never become behold running an operator.
|
|
|
361
524
|
Since chant 0.53.1 (chant#2027) a tick record carries an `id` and the
|
|
362
525
|
per-component verdicts it derived, and both ride through to
|
|
363
526
|
`[].lastTick.{id,components}`. The strip line names the tick (truncated), and
|
|
364
|
-
the verdicts join onto the component DAG by component name
|
|
527
|
+
the verdicts join onto the component DAG by component name, the same key the
|
|
365
528
|
live `chant components status` read is joined by. They join **under** that read:
|
|
366
529
|
a tick only ever feeds the last tier of `componentStatusColor` (the
|
|
367
530
|
reconciliation verdict), only on a node the live read left unpainted, and only
|
|
368
531
|
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
|
|
532
|
+
that it is named on the node, dated, and painted nothing: a graph fill has
|
|
370
533
|
nowhere to put "as of an hour ago". A chant older than 0.53.1 sends neither
|
|
371
534
|
field and every one of these paths is a no-op.
|
|
372
535
|
|
|
@@ -386,13 +549,13 @@ design (a local tick with no PR behind it has none) and never synthesized, so a
|
|
|
386
549
|
fact without one renders no link and no placeholder for one, and a url behold
|
|
387
550
|
would not put behind an `href` reads as no address at all.
|
|
388
551
|
|
|
389
|
-
A gate someone already resolved simply leaves `pendingGates
|
|
390
|
-
carries no `resolvedBy`/`when
|
|
552
|
+
A gate someone already resolved simply leaves `pendingGates`, the status read
|
|
553
|
+
carries no `resolvedBy`/`when`, so the strip shows no resolved-by line rather
|
|
391
554
|
than attributing an approval to nobody. The timeline is where resolutions are
|
|
392
555
|
named, because there they are records rather than an inference.
|
|
393
556
|
|
|
394
557
|
An `OperatorStack` (chant#1940) declares the loop as a k8s estate, so it already
|
|
395
|
-
appears in the entity graph. behold names it there
|
|
558
|
+
appears in the entity graph. behold names it there, the Namespace as the loop's
|
|
396
559
|
home, each CronJob as a converge tick — off chant's own
|
|
397
560
|
`app.kubernetes.io/component: converge-tick` labels, never a naming convention.
|
|
398
561
|
|
|
@@ -405,7 +568,7 @@ executor.
|
|
|
405
568
|
### The exceptions, and their exact size
|
|
406
569
|
|
|
407
570
|
`POST /api/layout` (#228) writes **one** file in the served project:
|
|
408
|
-
`.behold/layout.json
|
|
571
|
+
`.behold/layout.json`, the hand-layout sidecar, `{version, lenses: {<lens>:
|
|
409
572
|
{<node id>: {dx,dy,dw,dh}}}}`. That is the whole of behold's write surface
|
|
410
573
|
inside a project, and it does not weaken the invariant above:
|
|
411
574
|
|
|
@@ -414,7 +577,7 @@ inside a project, and it does not weaken the invariant above:
|
|
|
414
577
|
chant's, and a delta for a node that left the estate is dropped on read.
|
|
415
578
|
- It **never touches the cloud and never touches your source**. No `.ts`, no
|
|
416
579
|
`chant.config.ts`, no `.behold.json`. The path is `cfg.projectDir` + two
|
|
417
|
-
constants
|
|
580
|
+
constants; nothing from the request reaches the filesystem.
|
|
418
581
|
- It refuses politely when it shouldn't write: preview mode, a static-export
|
|
419
582
|
capture, a read-only project directory, an oversized or malformed body.
|
|
420
583
|
- It is **per-user state**, unlike `.behold.json` (config, meant to be tracked).
|
|
@@ -424,13 +587,23 @@ inside a project, and it does not weaken the invariant above:
|
|
|
424
587
|
render with the deltas baked into the SVG, which is how `behold export` and
|
|
425
588
|
static snapshots honour a hand layout.
|
|
426
589
|
|
|
427
|
-
|
|
590
|
+
A declared workspace (#464) keeps that file in the workspace root, the
|
|
591
|
+
directory holding `chant.workspace.json`, rather than in its first member.
|
|
592
|
+
|
|
593
|
+
`behold doctor --fix` (#464, ws-015) is the third exception, and it runs only
|
|
594
|
+
when you type the flag: for an estate root whose members are listed in
|
|
595
|
+
`.behold.json`, it writes `chant.workspace.json` in that root (never over an
|
|
596
|
+
existing one) and moves the saved `.behold/layout.json` from the first member to
|
|
597
|
+
the root with its node ids rewritten. It then runs `chant workspace check`. The
|
|
598
|
+
old layout file and `.behold.json` are left as they were.
|
|
599
|
+
|
|
600
|
+
The fourth exception is the carve walkthrough's two steps above (#254), and it
|
|
428
601
|
is narrower still: they exist only when behold booted a `behold demo carve`
|
|
429
602
|
copy, they write only into `<copy>/app/carveout/`, and the directory they write
|
|
430
603
|
into is a scratch dir behold created inside a directory it copied for you a
|
|
431
604
|
minute earlier. `carve bridge` runs without `--apply-rewrites`, so the demo's
|
|
432
605
|
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
|
|
606
|
+
and it must be an address the served report already ranks, the value that
|
|
434
607
|
reaches the spawn's argv comes from a closed set read off disk. No cloud write,
|
|
435
608
|
no Terraform mutation, no edit to anyone's chant source.
|
|
436
609
|
|
|
@@ -447,7 +620,7 @@ src/choudoufu-route.test.ts asserts it stays absent, for the same reason
|
|
|
447
620
|
## Changing behold (for agents working on this repo)
|
|
448
621
|
|
|
449
622
|
The sections above are about driving a running behold. This one is about
|
|
450
|
-
changing it
|
|
623
|
+
changing it, the working rules that used to live in a handoff note and now
|
|
451
624
|
live here, with the enforceable ones enforced.
|
|
452
625
|
|
|
453
626
|
- **Branches and merges.** Work on a branch off `main` (an isolated worktree
|
|
@@ -458,7 +631,7 @@ live here, with the enforceable ones enforced.
|
|
|
458
631
|
next onto what landed.
|
|
459
632
|
- **The local gate** is `just check` (tsc, tests, build). Every vitest run
|
|
460
633
|
also writes `.vitest-last.json` (the json reporter, PR #351), and `just
|
|
461
|
-
test` keeps the reporter's full output in `vitest.log
|
|
634
|
+
test` keeps the reporter's full output in `vitest.log`, both gitignored —
|
|
462
635
|
because #334's one-shot first-run failures kept losing the failing file's
|
|
463
636
|
name. If the suite fails once and passes on rerun, those two files from the
|
|
464
637
|
first run are the evidence; attach them to #334 (`jq '.testResults[] |
|
|
@@ -466,7 +639,7 @@ live here, with the enforceable ones enforced.
|
|
|
466
639
|
- **Releasing** is `just release`, after the version-bump PR merged: it tags
|
|
467
640
|
`behold-v<version>`, pushes the tag (which is what runs `release.yml`), and
|
|
468
641
|
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
|
|
642
|
+
push would be wrong. Never re-push a `behold-v*` tag by hand: a tag push
|
|
470
643
|
re-fires publish. Tags absent from origin (behold-v0.10.0, 0.10.1) stay
|
|
471
644
|
absent for that reason.
|
|
472
645
|
- **Scratch infrastructure** is governed by `src/scratch.ts`, asserted by
|
|
@@ -489,8 +662,8 @@ tool answers reads for it, and how it becomes a `GraphIR`. `src/member-kind.ts`
|
|
|
489
662
|
is the one table; `chant` is the first row and every member was that row
|
|
490
663
|
until the table existed. A new kind is:
|
|
491
664
|
|
|
492
|
-
1. A `registerMemberKind({ kind, probe, expects, via })` call in
|
|
493
|
-
`src/member-kind.ts
|
|
665
|
+
1. A `registerMemberKind({ kind, probe, expects, via, passes })` call in
|
|
666
|
+
`src/member-kind.ts`, the `probe` is sync, read-only and runs no code (a
|
|
494
667
|
file's presence, a regex over a root file, or either: the choudoufu probe
|
|
495
668
|
takes the `estate.chdf.hcl` sidecar, choudoufu's leading form, OR a `live {`
|
|
496
669
|
block in a root *.tf, #387); `via.tool` stamps the binary and
|
|
@@ -505,7 +678,24 @@ until the table existed. A new kind is:
|
|
|
505
678
|
`estateNamespaceScopes` already dispatch through `memberKindOf(dir)`.
|
|
506
679
|
The two routes that render an estate — `/api/graph`'s estate branch and
|
|
507
680
|
`/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`.
|
|
681
|
+
fork per kind; a kind's differences live inside its `read` or its `passes`.
|
|
682
|
+
|
|
683
|
+
`passes` (#427) is the render-time half, and it exists because the read-time
|
|
684
|
+
one could not hold it: the Terraform kind's third pass takes the request's
|
|
685
|
+
zoom, so it cannot run where the reader runs. `applyMemberPasses(ir,
|
|
686
|
+
{detail})` is the one call the six render sites make, and `memberPassNote(run,
|
|
687
|
+
dirs)` the line they return, each kind handed only the served dirs that are
|
|
688
|
+
its own, so a route never names a kind. Three rules a `passes` holds. It
|
|
689
|
+
mutates IN PLACE, because the existing passes do and their callers depend on
|
|
690
|
+
it. It self-guards on the IR's own CONTENT and never on the served members'
|
|
691
|
+
kinds. A chant project may declare a kind's lexicon; a chant project
|
|
692
|
+
declaring `terraform` is what `src/terraform-route.test.ts`'s first block
|
|
693
|
+
serves, and `memberKindOf` says `chant` there, so membership dispatch would
|
|
694
|
+
silently stop rendering it. And a run is carried forward, never re-run: a
|
|
695
|
+
pass that already elided its cards returns an empty elision the second time,
|
|
696
|
+
which drops the note on every composed estate. A test that re-registers a
|
|
697
|
+
kind to stub its reader spreads the real spec (`{ ...terraformSpec, via: … }`)
|
|
698
|
+
or it drops the passes with it.
|
|
509
699
|
3. A `registerPack({ lexicon, iconFor, fields })` in `src/render.ts`, or the
|
|
510
700
|
kind's cards lead with the alphabetically first two short attrs. A `fields`
|
|
511
701
|
function takes the box the card is drawn in as its second argument, so a
|
|
@@ -522,7 +712,8 @@ until the table existed. A new kind is:
|
|
|
522
712
|
`carveStatusReader` does; a PATH probe the way `src/demos.ts` does).
|
|
523
713
|
6. Tests: the probe and the object form in `src/project.test.ts`; dispatch
|
|
524
714
|
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
|
|
715
|
+
asserts chant members still go through exactly the calls they did, and its
|
|
716
|
+
"#427" block for the render passes; the
|
|
526
717
|
kind's own reader off recorded documents in `src/__fixtures__/`, with
|
|
527
718
|
provenance in prose above the load.
|
|
528
719
|
|
|
@@ -534,7 +725,7 @@ string is `chant`. Anything behold boots for a kind goes through
|
|
|
534
725
|
things used to be offered to every member whatever it was, and a new kind gets
|
|
535
726
|
all three answered for free:
|
|
536
727
|
|
|
537
|
-
- `/api/project` publishes `memberKinds
|
|
728
|
+
- `/api/project` publishes `memberKinds`, the served members' kinds, in
|
|
538
729
|
composition order. The SPA opens on the `resources` zoom when none of them is
|
|
539
730
|
`chant`, because `components` is a projection of a chant project's own
|
|
540
731
|
component DAG and a member without one opens on "the components lens doesn't
|
|
@@ -551,7 +742,7 @@ all three answered for free:
|
|
|
551
742
|
already returns and the SPA already reads as "no resource facet here".
|
|
552
743
|
|
|
553
744
|
A kind that grows components, or a substrate with an owner chain, changes those
|
|
554
|
-
answers where they are decided
|
|
745
|
+
answers where they are decided rather than per kind in a route.
|
|
555
746
|
|
|
556
747
|
### The workbench catalog
|
|
557
748
|
|
|
@@ -561,7 +752,7 @@ estates this checkout is developed against (#386, #388). Four rules, and
|
|
|
561
752
|
src/demos.ts holds them:
|
|
562
753
|
|
|
563
754
|
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
|
|
755
|
+
relative to the directory of the catalog file that named it, the intentius
|
|
565
756
|
checkouts are siblings, so the workbench writes `../choudoufu`,
|
|
566
757
|
`../waterpark`, `../chant`, and the file stays committed and reproducible.
|
|
567
758
|
An entry whose path is not checked out is unsatisfiable exactly as a
|
|
@@ -598,21 +789,21 @@ its own graph and overlay counts as it runs.
|
|
|
598
789
|
|
|
599
790
|
| entry | what it serves | needs |
|
|
600
791
|
|---|---|---|
|
|
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
|
|
792
|
+
| `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
793
|
| `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
|
|
794
|
+
| `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
795
|
| `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
|
|
796
|
+
| `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
797
|
| `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
798
|
| `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
799
|
| `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 |
|
|
609
800
|
| `terralith-4` | the same at scale 4: 301 cards, all 301 bound. The estate behold is sized against | the same |
|
|
610
801
|
| `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 |
|
|
611
|
-
| `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` + `@
|
|
802
|
+
| `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` + `@cdktn/hcl2json` beside behold — optional peers behold declares and does not install |
|
|
612
803
|
|
|
613
804
|
**The scratch, by name and port.** Each emulator is the entry's own, booted by
|
|
614
805
|
its up script and removed by the `scripts/down.sh` that script wrote into the
|
|
615
|
-
target
|
|
806
|
+
target rather than by pattern.
|
|
616
807
|
|
|
617
808
|
| entry | container | host port |
|
|
618
809
|
|---|---|---|
|
|
@@ -623,7 +814,7 @@ target — never by pattern.
|
|
|
623
814
|
| `terralith-1` | `behold-wb-terralith-1` | 4655 |
|
|
624
815
|
| `terralith-4` | `behold-wb-terralith-4` | 4656 |
|
|
625
816
|
| `terralith-4-adopt` | `behold-wb-terralith-4-adopt` | 4657 |
|
|
626
|
-
| `chant-*`, `waterpark` | none
|
|
817
|
+
| `chant-*`, `waterpark` | none (source reads, no substrate |) |
|
|
627
818
|
| `fountain-ops` | the k3d cluster `fountain-local`, which is the checkout's, not behold's | — |
|
|
628
819
|
|
|
629
820
|
The port is the floci's host binding and the entry's `serve.spawnEnv`
|
|
@@ -634,10 +825,22 @@ behold spawns talk to the same emulator.
|
|
|
634
825
|
loaded through `behold demo <entry> <tmp target> --port <p>` on its own port
|
|
635
826
|
from 4720 up, `/api/graph` asserted to carry nodes and a live entry's
|
|
636
827
|
`/api/overlay?env=live` to answer with its bound/unowned/neutral split, timings
|
|
637
|
-
per entry; eight entries and
|
|
828
|
+
per entry; eight entries and 553s on the machine this was written on.
|
|
829
|
+
|
|
830
|
+
Time is asserted, not only printed (#423). Each entry carries a ceiling at
|
|
831
|
+
roughly twice its own measured value, and the run carries one too, because
|
|
832
|
+
#419's whole subject was a number nobody was watching: the ratio could not
|
|
833
|
+
regress silently and the duration could. The measured figures the ceilings come
|
|
834
|
+
from are in the script beside them; `BEHOLD_E2E_SECS_<entry>` and
|
|
835
|
+
`BEHOLD_E2E_RUN_SECS` override. A live entry also asserts #422's shape: the
|
|
836
|
+
progressive overlay answers with every member marked pending and does so before
|
|
837
|
+
the blocking read returns, so the estate cannot quietly regress to
|
|
838
|
+
all-or-nothing. Where the blocking read is already sub-second there is nothing
|
|
839
|
+
to beat, and the run reports the two numbers rather than claiming a pass.
|
|
840
|
+
|
|
638
841
|
`terralith-4-adopt` is asserted twice — 85 UNOWNED, then the
|
|
639
842
|
`live-import` line the up script printed, run by the script itself in the
|
|
640
|
-
target the way a person would, then 301 bound
|
|
843
|
+
target the way a person would, then 301 bound, because that write is the
|
|
641
844
|
person's, never behold's (#372). `waterpark` is checked for an untouched
|
|
642
845
|
checkout afterwards. `missingRequirements` decides what runs: a missing binary,
|
|
643
846
|
an unchecked-out sibling, an uninstalled chant example or the absent Terraform
|
|
@@ -646,7 +849,7 @@ run exits 0. `fountain-ops` is skipped by name everywhere, for the reason in
|
|
|
646
849
|
the table. A `behold-wb-*` container left standing at the end fails the run.
|
|
647
850
|
|
|
648
851
|
The seeded catalog (#389) found one thing missing in src/: a LONE choudoufu
|
|
649
|
-
estate
|
|
852
|
+
estate (every generated entry is one) served the no-project card, because the
|
|
650
853
|
single-project read is `chant graph <dir>` and such a directory has no lexicon
|
|
651
854
|
for it to read. `servesAsEstate` (src/member-kind.ts) is the predicate that
|
|
652
855
|
routes one directory of a non-chant kind through the estate compose path, where
|
|
@@ -660,28 +863,28 @@ A Terraform estate reaches behold through chant.
|
|
|
660
863
|
`@intentius/chant-lexicon-terraform` reads the HCL an estate already has and
|
|
661
864
|
emits one entity per block, so **a Terraform estate is a chant project whose
|
|
662
865
|
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
|
|
866
|
+
other. behold parses no HCL and ships no HCL parser, the same posture
|
|
664
867
|
`src/carve-lens.ts` states for the carve report (#378).
|
|
665
868
|
|
|
666
869
|
Three passes turn what arrives into a picture (`src/terraform-lens.ts`), in this
|
|
667
870
|
order, guarded on the IR carrying terraform entities so every other estate gets
|
|
668
871
|
the identical object back:
|
|
669
872
|
|
|
670
|
-
1. **`normalizeTerraformNodes
|
|
873
|
+
1. **`normalizeTerraformNodes`.** A node's `kind` arrives as the entity class
|
|
671
874
|
(`Terraform::Resource`), not the resource type, so every card would be titled
|
|
672
875
|
and iconed the same. The type moves out of `attrs.address` into `kind` and
|
|
673
876
|
the block class lands in `attrs.block`. That is the shape a carve node
|
|
674
877
|
already has, which is why one presentation pack serves both.
|
|
675
|
-
2. **`groupTerraformByRoot
|
|
878
|
+
2. **`groupTerraformByRoot`.** Roots are a Terraform project's only grouping.
|
|
676
879
|
It retires itself when chant#2266 groups upstream. A node sits in exactly one
|
|
677
880
|
box, so a box every node has left is dropped rather than drawn empty (#393):
|
|
678
881
|
a served directory arrives composed, `composeStacks` boxes the whole member,
|
|
679
|
-
and the root boxes then take those same nodes
|
|
882
|
+
and the root boxes then take those same nodes, which is what put an empty
|
|
680
883
|
box named `access` on water park's canvas. What the member box was there to
|
|
681
884
|
say moves into the root box's title, which is `<member>/<root>` whenever the
|
|
682
885
|
estate holds more than one member and the bare root name when it does not
|
|
683
886
|
(two members with a root apiece named `prod` would otherwise merge silently).
|
|
684
|
-
3. **`filterTerraformCards
|
|
887
|
+
3. **`filterTerraformCards`.** What is a card, below.
|
|
685
888
|
|
|
686
889
|
**What is a card (#382).** Measured on a real estate: 247 nodes for 43
|
|
687
890
|
resources, four fifths of it not infrastructure.
|
|
@@ -709,7 +912,7 @@ of root names and drop the counts.
|
|
|
709
912
|
were recorded when a Terraform IR carried none, and lexicon 0.61.0 (chant#2265,
|
|
710
913
|
which resolves a block's `"${…}"` references) draws 390 over water park's five
|
|
711
914
|
roots with no change on this side. The one relationship that looked derivable
|
|
712
|
-
without it
|
|
915
|
+
without it (a cross-root read by name) was measured and refused (#381): both
|
|
713
916
|
ends carry the same unresolved interpolation, so a match would be a coincidence
|
|
714
917
|
of variable naming. A data source says what it reads as a row instead.
|
|
715
918
|
|
|
@@ -725,11 +928,11 @@ across a read. Three decisions, each a trade #384 left open and each measured on
|
|
|
725
928
|
water park's `access/` before it was taken:
|
|
726
929
|
|
|
727
930
|
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
|
|
729
|
-
dependency would put `@
|
|
931
|
+
own location, so the generated project has to see it, and making it a
|
|
932
|
+
dependency would put `@cdktn/hcl2json`, a ~1.8 MB wasm blob, in the install
|
|
730
933
|
of every user who serves a chant project. `@intentius/chant-lexicon-terraform`
|
|
731
|
-
and `@
|
|
732
|
-
package.json (the only place their versions are named
|
|
934
|
+
and `@cdktn/hcl2json` are therefore **optional peers**: declared in
|
|
935
|
+
package.json (the only place their versions are named, the refusal reads
|
|
733
936
|
them from there), never installed by behold, probed at serve and doctor time,
|
|
734
937
|
and refused with the one install line and where behold looked. The same gate
|
|
735
938
|
`behold demo` puts on a binary it does not ship. The lexicon's own chant peer
|
|
@@ -737,12 +940,12 @@ water park's `access/` before it was taken:
|
|
|
737
940
|
loads no published version of it (`applyLineage is not a function`).
|
|
738
941
|
2. **Roots are discovered, and the skips are reported.** A root is a directory
|
|
739
942
|
with a `.tf` declaring a line-start `terraform {` or `provider "` block
|
|
740
|
-
beside a `resource`, `data` or `module` block
|
|
943
|
+
beside a `resource`, `data` or `module` block, a regex probe at the depth
|
|
741
944
|
the choudoufu probe uses, no HCL parsed. #384 proposed the first half alone
|
|
742
945
|
("what a root has and a called module does not") and the estate refuted it:
|
|
743
946
|
water park's `modules/persona` is a shared module called by three roots and
|
|
744
947
|
its `versions.tf` is `baseline`'s byte for byte. So two exclusions stand
|
|
745
|
-
beside the probe
|
|
948
|
+
beside the probe, a directory under a `modules/` segment (Terraform's own
|
|
746
949
|
standard module structure; the roots that call it draw its blocks already)
|
|
747
950
|
and one with nothing to draw (`access/backends` is two backend fragments) —
|
|
748
951
|
and both are named in the graph's note with their reason, the way
|
|
@@ -755,7 +958,7 @@ water park's `access/` before it was taken:
|
|
|
755
958
|
render goes through chant. A kind whose `read` shells `chant graph` against a
|
|
756
959
|
generated config is a scaffold, not a second reader, and it inherits the
|
|
757
960
|
probe, the cache stamp, the doctor line and estate composition (#368) for
|
|
758
|
-
free
|
|
961
|
+
free, so a Terraform root composes in an estate beside a chant project and a
|
|
759
962
|
choudoufu estate at no extra cost. `src/terraform-member.ts` is the whole of
|
|
760
963
|
it; `detectProjectShape` answers a directory that is its own member with
|
|
761
964
|
`membersFrom: "probe"`, which is what retired #384's `no chant.config.ts
|
|
@@ -783,19 +986,19 @@ the listing. Neither of them ever compares an attribute VALUE. A resource whose
|
|
|
783
986
|
tag, path or mutability was changed out of band still carries its markers, so
|
|
784
987
|
it is still bound, and it is still green.
|
|
785
988
|
|
|
786
|
-
The other half of the terminal's answer
|
|
989
|
+
The other half of the terminal's answer ("would a plan change anything?") is
|
|
787
990
|
a **separate, opt-in read**: `choudoufu plan -input=false -out=<tmp>` then
|
|
788
991
|
`choudoufu show -json <tmp>`, whose `resource_changes[].change.{actions,before,
|
|
789
992
|
after}` name the attributes and both their values. (`choudoufu plan -json` is
|
|
790
993
|
NOT this: on 0.16.0 it prints choudoufu's own ownership document, the same
|
|
791
994
|
shape as `live-plan -json`, with no attribute values in it at all. The
|
|
792
995
|
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
|
|
996
|
+
a scratch directory of behold's own and removed after rather than inside the served
|
|
794
997
|
member, which would be a write into someone's source.
|
|
795
998
|
|
|
796
999
|
It is opt-in because it costs differently: the ownership read is answered out
|
|
797
1000
|
of the tagging index and is flat in the estate's size, while `plan` refreshes
|
|
798
|
-
every resource
|
|
1001
|
+
every resource, one provider read per card, 301 of them on `terralith-4`. So:
|
|
799
1002
|
|
|
800
1003
|
- **Never on an ordinary overlay read.** `GET /api/overlay?plan=1` and
|
|
801
1004
|
`GET /api/diff?plan=1` are the only things that spawn one, and the palette's
|
|
@@ -811,12 +1014,110 @@ every resource — one provider read per card, 301 of them on `terralith-4`. So:
|
|
|
811
1014
|
is still painted bound and still counted `bound`; it gains `attrs._planDrift`
|
|
812
1015
|
and wears a dashed `--degraded` edge plus a `~ n attributes` corner glyph
|
|
813
1016
|
(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
|
|
1017
|
+
operator marks). A card that is not bound is never marked: an unowned
|
|
815
1018
|
object's planned `create` is an ownership fact the overlay already paints,
|
|
816
1019
|
and `planDrift` drops creates for that reason.
|
|
817
1020
|
- **`/api/overlay`'s meta carries `drift`**: `{read: false}` or `{read: true,
|
|
818
1021
|
drifted: n}`. The two are different answers — "nobody looked" versus "looked,
|
|
819
|
-
nothing drifted"
|
|
1022
|
+
nothing drifted", and the legend prints a count only for the second.
|
|
1023
|
+
|
|
1024
|
+
### A lexicon behold draws but cannot aim (#430)
|
|
1025
|
+
|
|
1026
|
+
`behold doctor`'s **targets** line. `SUBSTRATE_TARGET_VARS` (src/targets.ts)
|
|
1027
|
+
names four lexicons against the nine behold draws, deliberately — it withholds
|
|
1028
|
+
one until chant can actually be pointed at an emulator for it. That module also
|
|
1029
|
+
names the cost: a landed chant change leaves a stale omission "until someone
|
|
1030
|
+
notices", which happened once (#125) and left behold showing a floci-gcp pill it
|
|
1031
|
+
could not aim a read at. This is the line someone reads instead.
|
|
1032
|
+
|
|
1033
|
+
`NO_AMBIENT_TARGET` (same module) is what stops it being noise: k8s and temporal
|
|
1034
|
+
are not withheld, they are finished — they bind from `chant.config` rather than
|
|
1035
|
+
an ambient variable, so there is nothing to override and nothing for a picker.
|
|
1036
|
+
Those report as settled, not as a gap.
|
|
1037
|
+
|
|
1038
|
+
What the line CANNOT say, stated because the gap is the point: the honest
|
|
1039
|
+
question is "chant can route this and behold does not list it", and behold
|
|
1040
|
+
cannot ask it. Since chant 0.61 the endpoint variable is declared by the LEXICON
|
|
1041
|
+
PLUGIN (`endpointEnvVarsFor`), not by a table in core, and behold installs no
|
|
1042
|
+
lexicon plugins; the served project does. No chant CLI reports them either
|
|
1043
|
+
(`lifecycle whoami --json` answers identity and says nothing about endpoints).
|
|
1044
|
+
So the check warns on the weaker question it can answer and says it may be
|
|
1045
|
+
over-reporting. The real fix is upstream, and #430 records the ask.
|
|
1046
|
+
|
|
1047
|
+
### A card with no live half is not an unobserved one (#429)
|
|
1048
|
+
|
|
1049
|
+
pinhole's painter takes a closed `_status` set and falls back to `neutral` for
|
|
1050
|
+
anything else, `undefined` included. So on a mixed overlay a member with no live
|
|
1051
|
+
half, a Terraform root, whose `live`/`overlay`/`env` are stripped in
|
|
1052
|
+
src/terraform-member.ts — painted exactly like a card behold looked at and could
|
|
1053
|
+
not see, while the legend counted only the latter. Measured on
|
|
1054
|
+
`serve example-writes example-terraform-estate --env prod`: 37 Terraform cards
|
|
1055
|
+
carrying no `_status`, 2 chant cards carrying `neutral`, one fill between them.
|
|
1056
|
+
|
|
1057
|
+
Those cards now carry `data-no-live` and the legend gives them their own row.
|
|
1058
|
+
Named rather than folded into `unobserved`, because "I looked and could not see"
|
|
1059
|
+
and "there is nothing here to look at" are different claims, the same argument
|
|
1060
|
+
#399 makes for an unpriced card, and the same answer: mark it, do not merely
|
|
1061
|
+
colour it. The mark is overlay-only; on the source graph no card has a status
|
|
1062
|
+
and marking every one of them would say nothing.
|
|
1063
|
+
|
|
1064
|
+
This is presentation, not vocabulary. `statusVocabulary` gets no terraform row:
|
|
1065
|
+
there is no status to name. See #429 for the measurement that established it.
|
|
1066
|
+
|
|
1067
|
+
### An adoptable row is painted like an unowned one, plus its evidence (#413)
|
|
1068
|
+
|
|
1069
|
+
A `NEEDS_DISCOVERY` omission paired with an `adoptable[]` row at the same
|
|
1070
|
+
address paints `warn` with `ownership: "foreign"`, the same verdict the UNOWNED
|
|
1071
|
+
branch gives and for the same reason: the live object carries no marker for
|
|
1072
|
+
this estate. On its own, `NEEDS_DISCOVERY` stays neutral — the declaration
|
|
1073
|
+
names no identity, and with nothing found that is a "could not answer" rather
|
|
1074
|
+
than an action.
|
|
1075
|
+
|
|
1076
|
+
The card leads with `attrs.adopt`, which is choudoufu's own `adopt_command`
|
|
1077
|
+
here rather than the marker pair an unowned row carries. `adoptLine`'s
|
|
1078
|
+
composed-command branch had no real caller until this. behold composes no
|
|
1079
|
+
command of its own: the API that writes a tag differs per resource type.
|
|
1080
|
+
|
|
1081
|
+
`attrs.matchedOn` is the difference in kind. An unowned row needs no evidence
|
|
1082
|
+
because the identity IS the match; here choudoufu asserts that a live object is
|
|
1083
|
+
a declaration because their identity-bearing arguments agree, and a person
|
|
1084
|
+
about to stamp two tags onto somebody's resource is owed the list.
|
|
1085
|
+
|
|
1086
|
+
`web/app.js` renders both through the one copyable row, and its tooltip says
|
|
1087
|
+
whether the line is a command or a pair of tags.
|
|
1088
|
+
|
|
1089
|
+
### The adoption sweep is off unless a serve asks (#412)
|
|
1090
|
+
|
|
1091
|
+
`live-plan -json` carries `adoptable[]` (live resources the estate-wide sweep
|
|
1092
|
+
matched to a declared instance by content) and `swept` (the types it listed in
|
|
1093
|
+
full). Both are empty on a run that did not ask the account-inventory question,
|
|
1094
|
+
and behold does not ask by default.
|
|
1095
|
+
|
|
1096
|
+
The opt-in is `TOFU_LIVE_COLLECT_UNCLAIMED=1` (`ADOPTION_SWEEP_VAR`,
|
|
1097
|
+
src/choudoufu-live.ts), carried through `serve.spawnEnv`, the seam
|
|
1098
|
+
`setChoudoufuSpawnEnv` already feeds every choudoufu spawn. The env var rather
|
|
1099
|
+
than `-adoption-only`, because that flag suppresses the resource diff the
|
|
1100
|
+
overlay needs. Off by default because the sweep is bounded by the ACCOUNT rather
|
|
1101
|
+
than the estate, and behold polls. `0` means no, which is choudoufu's own
|
|
1102
|
+
spelling, not a truthiness test.
|
|
1103
|
+
|
|
1104
|
+
**Content matching is a closed per-type table**, and that is what decides
|
|
1105
|
+
whether a declaration can ever produce an `adoptable[]` row. `matchTable`
|
|
1106
|
+
(choudoufu's internal/live/foreign/classify.go) lists eleven types against the
|
|
1107
|
+
identity-bearing arguments a match may use — `aws_vpc` on `cidr_block`,
|
|
1108
|
+
`aws_security_group`/`aws_lb`/`aws_sns_topic` and friends on `name`,
|
|
1109
|
+
`aws_subnet` on `cidr_block` + `availability_zone`, `aws_acm_certificate` on
|
|
1110
|
+
`domain_name`. A type absent from it, in the table's own words, "can never
|
|
1111
|
+
produce a bind candidate". `aws_iam_role` is absent, which is why #412's first
|
|
1112
|
+
attempt measured a shape that cannot match rather than a gap. A resource with a
|
|
1113
|
+
`count`/`for_each` key is skipped too, so a fixture wants a scalar.
|
|
1114
|
+
|
|
1115
|
+
`src/__fixtures__/choudoufu-live-plan-adoptable.json` is the real thing,
|
|
1116
|
+
recorded off floci: one scalar `aws_vpc` declaring a static `cidr_block`, its
|
|
1117
|
+
unmarked live twin created out of band, and the run asked with the opt-in. It
|
|
1118
|
+
carries the pairing M2 is about, a `NEEDS_DISCOVERY` omission and an
|
|
1119
|
+
`adoptable[]` row at the same address — plus `matched[]` naming what was
|
|
1120
|
+
compared and choudoufu's own `adopt_command`.
|
|
820
1121
|
|
|
821
1122
|
### A choudoufu estate's own references (#393)
|
|
822
1123
|
|
|
@@ -828,8 +1129,8 @@ estate references anything else", which is false about every one of them.
|
|
|
828
1129
|
|
|
829
1130
|
The references were never missing; nothing was reading them. **A choudoufu
|
|
830
1131
|
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)
|
|
1132
|
+
directory**, one root, the estate directory itself, through the same scratch
|
|
1133
|
+
project machinery above (nothing written under the estate), and joins the two
|
|
833
1134
|
documents by address. behold still parses no HCL: `src/choudoufu-refs.ts` is
|
|
834
1135
|
two lists of strings and the rules that match them, and its header is the
|
|
835
1136
|
argument for each. In short: the lexicon names *blocks* in a path of module
|
|
@@ -846,7 +1147,7 @@ Every edge is `inferred` and carries the lexicon's own attribute name (`role`,
|
|
|
846
1147
|
`policy_arn`), so the card says what made the reference. **Without the
|
|
847
1148
|
lexicon there are no edges and the note says so** — carrying the terraform
|
|
848
1149
|
kind's own install line rather than the sentence behold has no reader to
|
|
849
|
-
assert
|
|
1150
|
+
assert, and the lexicon's version is in the member's cache stamp, so
|
|
850
1151
|
installing it invalidates the edgeless IR rather than serving it forever.
|
|
851
1152
|
|
|
852
1153
|
The layout half is `packBoxComponents` (src/render.ts). src/edgeless.ts wraps a
|
|
@@ -856,7 +1157,7 @@ dagre lays 42 connected components side by side on three ranks. The pass packs
|
|
|
856
1157
|
a box's components into shelves and resizes the box around them, re-laying each
|
|
857
1158
|
component on its own first (inside a cluster dagre interleaves them: a six-card
|
|
858
1159
|
cluster's bounding box spanned 46598 units) and wrapping a component that is a
|
|
859
|
-
strip either way
|
|
1160
|
+
strip either way: the DNS fan is 19564 x 400 upright and 900 x 10046 on its
|
|
860
1161
|
side. A box under 4:1 is left exactly as it laid out. Measured: `terralith-4`
|
|
861
1162
|
7660 x 5162 with 266 edges, waterpark 3536 x 3138 at detail 2 and 7142 x 6974
|
|
862
1163
|
at detail 3 (from 4.2:1 and 15.1:1, the case #393's wrap did not answer).
|