clearotron 0.2.0 → 0.3.0-beta.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.
Files changed (70) hide show
  1. package/.env.example +52 -0
  2. package/INSTALL.md +9 -7
  3. package/README.md +2 -1
  4. package/bin/example.mjs +97 -32
  5. package/bin/onboard.mjs +58 -20
  6. package/bin/start.mjs +7 -0
  7. package/bin/stop.mjs +65 -3
  8. package/build-info.json +2 -2
  9. package/docs/RELEASES.md +6 -4
  10. package/docs/architecture/04-configuration-reference.md +1 -1
  11. package/driver/CHANGELOG.md +51 -0
  12. package/driver/ask-ledger.mjs +69 -1
  13. package/driver/declination-call.mjs +32 -0
  14. package/driver/driver.config.mjs +20 -0
  15. package/driver/engine/mcp/recording-server.mjs +4 -0
  16. package/driver/gateway.mjs +8 -3
  17. package/driver/knockout-assess-record.mjs +5 -1
  18. package/driver/package.json +1 -1
  19. package/driver/pipeline.mjs +123 -3
  20. package/driver/predelivery-lint.mjs +23 -5
  21. package/driver/publish/knockout.mjs +12 -6
  22. package/driver/publish/render-knockout.mjs +133 -23
  23. package/driver/publish/report-data.mjs +13 -3
  24. package/driver/publish/seed-pool.mjs +24 -9
  25. package/driver/record-carry.mjs +139 -0
  26. package/driver/reference-score.mjs +53 -3
  27. package/driver/reference-strip-signatures.mjs +68 -0
  28. package/driver/register-digest-record.mjs +31 -1
  29. package/driver/repairs.mjs +1 -1
  30. package/driver/result-noun-fields.mjs +7 -0
  31. package/driver/skills/knockout-assess/SKILL.md +10 -4
  32. package/driver/stages-knockout.mjs +1 -1
  33. package/driver/stages.mjs +1 -1
  34. package/driver/suite-census.json +108 -30
  35. package/driver/unit-inventory.mjs +47 -0
  36. package/driver/unit-state-verdict.mjs +8 -8
  37. package/driver/verify-knockout.mjs +9 -1
  38. package/driver/verify.mjs +2 -2
  39. package/driver/whatif-memo-run.mjs +45 -4
  40. package/mcp-server/CHANGELOG.md +10 -0
  41. package/mcp-server/lib/brief.mjs +15 -0
  42. package/mcp-server/lib/driver.mjs +6 -0
  43. package/mcp-server/lib/knockout.mjs +435 -0
  44. package/mcp-server/lib/scrub.mjs +1 -1
  45. package/mcp-server/lib/whatif.mjs +10 -1
  46. package/mcp-server/package.json +1 -1
  47. package/mcp-server/server.mjs +69 -4
  48. package/package.json +1 -1
  49. package/portal-ui/package.json +1 -1
  50. package/providers/oauth-mcp-bridge/CHANGELOG.md +10 -0
  51. package/providers/oauth-mcp-bridge/package.json +1 -1
  52. package/scripts/ai-page-render-check.mjs +2 -1
  53. package/scripts/clearances-render-check.mjs +2 -1
  54. package/scripts/drain-preflight.mjs +2 -2
  55. package/scripts/env-audit.mjs +20 -0
  56. package/scripts/freeze-example-run.mjs +3 -3
  57. package/scripts/headless-page.mjs +274 -0
  58. package/scripts/home-render-check.mjs +2 -1
  59. package/scripts/live-surface-check.mjs +86 -17
  60. package/scripts/mint-reference-strip-backlog.mjs +41 -0
  61. package/scripts/release-await-cut.mjs +95 -7
  62. package/scripts/release-version-pr-checks.mjs +25 -1
  63. package/scripts/render-check.mjs +61 -2
  64. package/scripts/report-frame-check.mjs +12 -0
  65. package/scripts/report-screenshot.mjs +62 -2
  66. package/scripts/revisit-render-check.mjs +3 -2
  67. package/scripts/score.mjs +14 -0
  68. package/shared/access-audience.mjs +215 -0
  69. package/shared/tracked-files.mjs +31 -0
  70. package/scripts/deploy-test.sh +0 -309
@@ -168,6 +168,36 @@ const FINDING_COLUMNS = Object.freeze(
168
168
  // finding it there together with screen_verdict / class / status.
169
169
  const NEGATIVE_COLUMNS = Object.freeze(["Mark", "Search Term / Variant", "Result", "Notes"]);
170
170
 
171
+ /**
172
+ * The "Mark" cell for one Negative-results row.
173
+ *
174
+ * A `duplicate-of-surfaced` row is not a statement about the MARK. It is a statement about one
175
+ * registration of a mark whose position is already reported above it — the digest's own rule is "one
176
+ * row per POSITION, never one per registration of the same right". Printing the bare mark under a
177
+ * column headed "Mark" says the opposite of what the row means, and the document then reads as both
178
+ * "keep this mark, here is the reasoning" in the incumbent table and "no separate row for this mark"
179
+ * here. Nine readers scan this file, and one of them is the drafting seat that decides what the client
180
+ * is shown.
181
+ *
182
+ * Naming the record removes the contradiction and changes no judgment the digest made: the row is
183
+ * still a drop, still on the same ground, still carrying the seat's own reason.
184
+ *
185
+ * ✕ EVERY OTHER GROUND IS LEFT ALONE, deliberately. `off-field`, `dead-status` and `out-of-class` are
186
+ * about the record on its own terms, where the bare mark is what a reader wants and is not ambiguous.
187
+ * A blanket change to this column would touch every negative row in every report for a defect that
188
+ * only exists on the duplicate class.
189
+ *
190
+ * The office comes from the record uri (`/mark/wo/…` -> `WO`); every uri in the archived corpus carries
191
+ * a two-letter office in that position. When it cannot be read the BARE MARK is printed rather than a
192
+ * broken qualifier — a missing qualifier is a smaller defect than "OSLER DELPHI — UNDEFINED record".
193
+ */
194
+ export function negativeMarkCell(row) {
195
+ const mark = String(row?.cells?.mark ?? "").trim();
196
+ if (row?.ground !== "duplicate-of-surfaced" || !mark) return mark;
197
+ const office = String(row?.cells?.uri ?? "").split("/")[2] ?? "";
198
+ return /^[a-z]{2}$/i.test(office) ? `${mark} \u2014 ${office.toUpperCase()} record` : mark;
199
+ }
200
+
171
201
  // ── THE FLOOR, AND WHAT ITS ZERO MEANS ────────────────────────────────────────────────────────────
172
202
  //
173
203
  // `validators.registerFindings` carried `nonEmpty(c)` with no character number, so there is no
@@ -353,7 +383,7 @@ export function renderRegisterFindings(model, facts = emptyFacts()) {
353
383
  out.push(DIGEST_SECTIONS.negative, "");
354
384
  out.push(model.negative_rows.length
355
385
  ? table(NEGATIVE_COLUMNS, model.negative_rows.map((r) => [
356
- r.cells.mark, r.variant || r.cells.mark, r.drop_reason,
386
+ negativeMarkCell(r), r.variant || r.cells.mark, r.drop_reason,
357
387
  [`URI ${r.cells.uri}`, r.screen_verdict ? `screen_verdict=${r.screen_verdict}` : "",
358
388
  r.cells.classes ? `class=${r.cells.classes}` : "", r.cells.status ? `status=${r.cells.status}` : ""]
359
389
  .filter(Boolean).join("; "),
@@ -555,7 +555,7 @@ export function countRecoveryLanes(history, { total = 0 } = {}) {
555
555
 
556
556
  // ── CAP PARKS: A PROVIDER SAYING "NOT YET" IS NOT A STAGE FAILING (tracker issue 103) ──────────────
557
557
  //
558
- // Owner, watching indigo-falcon spend 4 of its 6 recovery parks against one subscription cap:
558
+ // Owner, watching a run spend 4 of its 6 recovery parks against one subscription cap:
559
559
  //
560
560
  // "surely it can work out when the cap expires and try after that time and not just keep trying
561
561
  // and then die."
@@ -95,6 +95,13 @@ export const RESULT_NOUN_FIELDS = Object.freeze([
95
95
  { file: "driver/skills-store-provenance.mjs", noun: "outcome", sites: 6, atWriteSite: 0, verdict: "out-of-scope" },
96
96
  { file: "driver/status-snapshot.mjs", noun: "outcome", sites: 1, atWriteSite: 0, verdict: "out-of-scope" },
97
97
  { file: "mcp-server/lib/events.mjs", noun: "recovered", sites: 1, atWriteSite: 0, verdict: "out-of-scope" },
98
+ // Read at the writing site, as this table requires. All three sites are in `knockoutSearches`, a
99
+ // READ-ONLY projection that composes the rows `list_searches` answers with: it opens no artifact and
100
+ // writes nothing, so nothing here can reach `_driver/*.json` or `run.jsonl`. Each value is a literal
101
+ // chosen from the run's own stored record — "found" or "no-hit" from whether the mark has findings,
102
+ // and "recorded" for a register term whose stored row says it did NOT answer. None is a call's return
103
+ // read back as a verdict, which is the defect this table exists to find.
104
+ { file: "mcp-server/lib/knockout.mjs", noun: "outcome", sites: 3, atWriteSite: 0, verdict: "out-of-scope" },
98
105
  { file: "mcp-server/lib/ops.mjs", noun: "settled", sites: 1, atWriteSite: 0, verdict: "out-of-scope" },
99
106
  { file: "mcp-server/server.mjs", noun: "outcome", sites: 1, atWriteSite: 0, verdict: "out-of-scope" },
100
107
  { file: "scripts/backfill-started-at.mjs", noun: "outcome", sites: 5, atWriteSite: 0, verdict: "out-of-scope" },
@@ -162,9 +162,14 @@ your job. These two fields are how what you concluded reaches the reader instead
162
162
  every id against that store and refuses one it does not hold — so the report can state a filing's
163
163
  source as a fact rather than as a word you typed about your own sourcing.
164
164
 
165
- - **`registerReads`** — on the MARK. Rows of `{recordId, read}` for a filing you weighed that did **not**
166
- become a `findings[]` record. `read` is what you concluded about *that* filing: whether it bears on the
167
- rating, and why. It prints on that filing's card.
165
+ - **`registerReads`** — on the MARK. Rows of `{recordId, read, band?}` for a filing you weighed that did
166
+ **not** become a `findings[]` record. `read` is what you concluded about *that* filing: whether it bears
167
+ on the rating, and why. It prints on that filing's card. `band` is **optional** and is how you rate that
168
+ filing on its own — in the framework's band words, the same vocabulary as the mark's rating. Send it
169
+ whenever you formed a view on the filing itself: a registered right is often the one hard legal fact on
170
+ the page, and with no band its card is the only one carrying no rating while softer uses around it carry
171
+ one, which reads as though the registration mattered least. Omit it and the read still prints, claiming
172
+ no rating.
168
173
  - **`weighedFilings`** — on a FINDING. The `recordId`s whose evidence your reasoning for that conflict
169
174
  actually used. The report derives the finding's source labelling from this, so list one only if you
170
175
  reasoned from it.
@@ -205,7 +210,8 @@ line, which describes the card and claims nothing about the rating. So:
205
210
  "classesDriving": [8], "bullets": ["..."], "purpleNotes": ["..."],
206
211
  "registerEstimate": "...", "parodyNote": null, "crowdedField": false,
207
212
  "registerReads": [ { "recordId": "<verbatim from the filings you were given>",
208
- "read": "<what you concluded about THAT filing>" } ],
213
+ "read": "<what you concluded about THAT filing>",
214
+ "band": "<OPTIONAL — your rating of THAT filing, in the ladder's words>" } ],
209
215
  "basis": "<ONE sentence: why this band, for this name, in these classes>",
210
216
  "factors": ["<2-4 load-bearing observations, one line each>"],
211
217
  "counterFactors": ["<1-3: what holds it at this band rather than the next>"],
@@ -301,7 +301,7 @@ export const KO_STAGES = {
301
301
  // a finding with no register evidence. Omitting either field is always safe: the card keeps its
302
302
  // neutral line, which describes the card and is true.
303
303
  existsSync(K.registerRecords)
304
- ? `WHEN YOU WEIGH ONE OF THOSE FILINGS, SAY SO BY ITS OWN ID — copy "recordId" verbatim from the file above. "registerReads" on the MARK: rows of {recordId, read} for a filing you weighed that did NOT become a findings[] record; "read" is what you concluded about THAT filing — whether it bears on the rating and why — and it prints on that filing's card in the reader's own report. "weighedFilings" on a FINDING: the recordIds whose evidence your reasoning for that conflict actually used, because the report derives that finding's source labelling from it. Both are optional and both are joined against the filings you were given, so an id we do not hold is refused by name. A filing you did not weigh simply gets no row: do not invent a read to fill one, and never write "not weighed" as a read.`
304
+ ? `WHEN YOU WEIGH ONE OF THOSE FILINGS, SAY SO BY ITS OWN ID — copy "recordId" verbatim from the file above. "registerReads" on the MARK: rows of {recordId, read, band?} for a filing you weighed that did NOT become a findings[] record; "read" is what you concluded about THAT filing — whether it bears on the rating and why — and it prints on that filing's card in the reader's own report. "band" is OPTIONAL and is your rating of THAT filing on its own, in the framework's band words: send it whenever you formed a view on the filing itself, because a registered right left unbanded is the only card on the page with no rating while softer uses beside it carry one, which reads as though the registration mattered least. Omit it and your read still prints, claiming no rating. "weighedFilings" on a FINDING: the recordIds whose evidence your reasoning for that conflict actually used, because the report derives that finding's source labelling from it. Both are optional and both are joined against the filings you were given, so an id we do not hold is refused by name. A filing you did not weigh simply gets no row: do not invent a read to fill one, and never write "not weighed" as a read.`
305
305
  : "",
306
306
  `A DEGRADED mark's row must carry degraded:true and the purple "Manual verification recommended"`,
307
307
  `note; a mark WITH a payload must never claim degraded (the validator joins both against the disk).`,
package/driver/stages.mjs CHANGED
@@ -3267,7 +3267,7 @@ export const STAGES = {
3267
3267
  `DECLINATIONS: what you do not deliver, you decline BY NAME through the \`record_declination\` tool. A record that reached your findings surface leaves this stage as a finding in ${P.findings} or as a declination, and there is no third way out.`,
3268
3268
  ...(Array.isArray(findingsSurface) && findingsSurface.length ? [
3269
3269
  `DECLINATIONS (MANDATORY): the register digest carried ${findingsSurface.length} record(s) onto your findings surface. Each one leaves this stage by one of two routes and there is no third: it becomes a finding in ${P.findings}, or you decline it BY NAME with a reason and a ground. A record you simply do not mention is reported as a defect of this run, named individually in its trace and shipped as an open doubt, so silence costs you more than a declination ever will.`,
3270
- `Decline by calling the \`record_declination\` tool. Its schema names the fields it takes and what each is for — read them there, not here. The one thing to know before you call: you cite a record by its POSITION in the list below — there is no field for a mark name or a uri, so a record you were not handed cannot be expressed at all. The reason vocabulary is closed: ${DECLINATION_REASON_TOKENS.join(" / ")}, each an omission synthesis-rules.md already authorises, and if your ground is none of them the rules do not let you omit the record. The grounds you write are one or two lines in your OWN words on why THIS record does not earn a line — never machine-parsed, and what the reviewing lawyer reads. Send them in one batch where you can; a refused row never voids its neighbours, and the answer names what is still undecided so you can finish in this turn.`,
3270
+ `Decline by calling the \`record_declination\` tool. Its schema names the fields it takes and what each is for — read them there, not here. The one thing to know before you call: you cite a record by its POSITION in the list below — there is no field for a mark name or a uri, so a record you were not handed cannot be expressed at all. The reason vocabulary is closed: ${DECLINATION_REASON_TOKENS.join(" / ")}, each an omission synthesis-rules.md already authorises, and if your ground is none of them the rules do not let you omit the record. ONE OF THEM IS NOT AVAILABLE ON A LIVE RECORD IN AN INSTRUCTED CLASS: \`not-worth-the-line\` is a statement about the report's budget rather than about the mark, and a live registration inside one of the matter's own classes is never omitted for want of room — carry it, or decline it on something true of THAT MARK. The call is refused if you try, so it costs you a turn. The grounds you write are one or two lines in your OWN words on why THIS record does not earn a line — never machine-parsed, and what the reviewing lawyer reads. Send them in one batch where you can; a refused row never voids its neighbours, and the answer names what is still undecided so you can finish in this turn.`,
3271
3271
  `A REFUSAL FROM THAT TOOL IS ABOUT BOOKKEEPING, NEVER ABOUT YOUR LEGAL JUDGMENT. There is exactly ONE case: declining a mark IDENTICAL to the applied-for mark, live, in one of the matter's own filed classes, on a discretionary ground — because synthesis-rules.md orders that an on-point identical mark in the relevant class is never dropped, "regardless of filer profile", so such a declination contradicts the instruction you are already following. Nothing else is refused. If that record really is the applicant's own, or already delivered under another record, say exactly that with own-right or duplicate-of-delivered and it is accepted. Whether goods are related, whether a name is off-field, whether a conflict is worth the line — those are your calls and the tool does not have an opinion about any of them.`,
3272
3272
  `The records on your findings surface, by position:`,
3273
3273
  ...findingsSurface.map((r, i) => ` ${i}. ${r.mark ?? "(unnamed)"}${r.owner ? ` — ${r.owner}` : ""}${r.tier ? ` [${r.tier}]` : ""}${r.uri ? ` ${r.uri}` : ""}`),
@@ -189,6 +189,12 @@
189
189
  "skips": 1,
190
190
  "todos": 0
191
191
  },
192
+ "a-door-that-is-not-there-is-not-a-door-that-agrees.test.mjs": {
193
+ "tests": 19,
194
+ "asserts": 69,
195
+ "skips": 0,
196
+ "todos": 0
197
+ },
192
198
  "a-doubt-cannot-answer-itself.test.mjs": {
193
199
  "tests": 17,
194
200
  "asserts": 40,
@@ -201,6 +207,18 @@
201
207
  "skips": 0,
202
208
  "todos": 0
203
209
  },
210
+ "a-drop-with-a-stated-reason-is-checked.test.mjs": {
211
+ "tests": 13,
212
+ "asserts": 32,
213
+ "skips": 0,
214
+ "todos": 0
215
+ },
216
+ "a-dynamic-import-of-a-path-is-a-file-url.test.mjs": {
217
+ "tests": 3,
218
+ "asserts": 6,
219
+ "skips": 1,
220
+ "todos": 0
221
+ },
204
222
  "a-fact-about-a-named-party.test.mjs": {
205
223
  "tests": 30,
206
224
  "asserts": 80,
@@ -286,8 +304,8 @@
286
304
  "todos": 0
287
305
  },
288
306
  "a-memo-over-a-delivered-report-leaves-it-alone.test.mjs": {
289
- "tests": 9,
290
- "asserts": 45,
307
+ "tests": 13,
308
+ "asserts": 52,
291
309
  "skips": 0,
292
310
  "todos": 0
293
311
  },
@@ -375,9 +393,15 @@
375
393
  "skips": 0,
376
394
  "todos": 0
377
395
  },
378
- "a-real-install-starts-with-an-empty-archive.test.mjs": {
396
+ "a-queued-what-if-is-answered-before-its-run-archives.test.mjs": {
379
397
  "tests": 3,
380
- "asserts": 8,
398
+ "asserts": 12,
399
+ "skips": 0,
400
+ "todos": 0
401
+ },
402
+ "a-real-install-starts-with-an-empty-archive.test.mjs": {
403
+ "tests": 6,
404
+ "asserts": 16,
381
405
  "skips": 0,
382
406
  "todos": 0
383
407
  },
@@ -483,6 +507,12 @@
483
507
  "skips": 0,
484
508
  "todos": 0
485
509
  },
510
+ "a-screenshot-of-an-error-page-is-not-a-report.test.mjs": {
511
+ "tests": 21,
512
+ "asserts": 55,
513
+ "skips": 4,
514
+ "todos": 0
515
+ },
486
516
  "a-secret-file-is-compared-not-printed.test.mjs": {
487
517
  "tests": 5,
488
518
  "asserts": 16,
@@ -501,6 +531,12 @@
501
531
  "skips": 0,
502
532
  "todos": 0
503
533
  },
534
+ "a-shared-proprietor-is-not-a-bucket-collision.test.mjs": {
535
+ "tests": 6,
536
+ "asserts": 14,
537
+ "skips": 0,
538
+ "todos": 0
539
+ },
504
540
  "a-signal-immune-fixture-is-reaped-by-its-owner.test.mjs": {
505
541
  "tests": 10,
506
542
  "asserts": 20,
@@ -543,6 +579,12 @@
543
579
  "skips": 0,
544
580
  "todos": 0
545
581
  },
582
+ "a-unit-is-attributed-by-what-it-runs-not-only-by-what-it-declares.test.mjs": {
583
+ "tests": 10,
584
+ "asserts": 27,
585
+ "skips": 0,
586
+ "todos": 0
587
+ },
546
588
  "a-verb-is-a-module-until-it-is-the-command.test.mjs": {
547
589
  "tests": 3,
548
590
  "asserts": 8,
@@ -676,8 +718,8 @@
676
718
  "todos": 0
677
719
  },
678
720
  "an-experiment-receipt-names-its-engine.test.mjs": {
679
- "tests": 6,
680
- "asserts": 22,
721
+ "tests": 8,
722
+ "asserts": 27,
681
723
  "skips": 0,
682
724
  "todos": 0
683
725
  },
@@ -687,12 +729,24 @@
687
729
  "skips": 1,
688
730
  "todos": 0
689
731
  },
732
+ "an-observed-model-id-says-whether-it-is-pinned.test.mjs": {
733
+ "tests": 4,
734
+ "asserts": 10,
735
+ "skips": 0,
736
+ "todos": 0
737
+ },
690
738
  "an-ordered-field-is-a-field-the-tool-can-express.test.mjs": {
691
739
  "tests": 1,
692
740
  "asserts": 6,
693
741
  "skips": 0,
694
742
  "todos": 0
695
743
  },
744
+ "an-unread-probe-is-not-a-failed-measurement.test.mjs": {
745
+ "tests": 5,
746
+ "asserts": 11,
747
+ "skips": 0,
748
+ "todos": 0
749
+ },
696
750
  "anchor-and-script-aware-quote.test.mjs": {
697
751
  "tests": 12,
698
752
  "asserts": 40,
@@ -724,8 +778,8 @@
724
778
  "todos": 0
725
779
  },
726
780
  "ask-ledger.test.mjs": {
727
- "tests": 21,
728
- "asserts": 100,
781
+ "tests": 26,
782
+ "asserts": 119,
729
783
  "skips": 0,
730
784
  "todos": 0
731
785
  },
@@ -1324,8 +1378,8 @@
1324
1378
  "todos": 0
1325
1379
  },
1326
1380
  "declination-is-stated-not-inferred.test.mjs": {
1327
- "tests": 20,
1328
- "asserts": 60,
1381
+ "tests": 24,
1382
+ "asserts": 69,
1329
1383
  "skips": 0,
1330
1384
  "todos": 0
1331
1385
  },
@@ -1373,7 +1427,7 @@
1373
1427
  },
1374
1428
  "deployment-hostnames.test.mjs": {
1375
1429
  "tests": 13,
1376
- "asserts": 37,
1430
+ "asserts": 36,
1377
1431
  "skips": 0,
1378
1432
  "todos": 0
1379
1433
  },
@@ -1882,8 +1936,8 @@
1882
1936
  "todos": 0
1883
1937
  },
1884
1938
  "example-replay.test.mjs": {
1885
- "tests": 6,
1886
- "asserts": 24,
1939
+ "tests": 8,
1940
+ "asserts": 31,
1887
1941
  "skips": 0,
1888
1942
  "todos": 0
1889
1943
  },
@@ -2433,6 +2487,12 @@
2433
2487
  "skips": 2,
2434
2488
  "todos": 0
2435
2489
  },
2490
+ "no-run-codenames-in-tracked-files.test.mjs": {
2491
+ "tests": 6,
2492
+ "asserts": 13,
2493
+ "skips": 4,
2494
+ "todos": 0
2495
+ },
2436
2496
  "no-test-writes-inside-the-checkout.test.mjs": {
2437
2497
  "tests": 16,
2438
2498
  "asserts": 35,
@@ -2826,7 +2886,7 @@
2826
2886
  "production-dependencies.test.mjs": {
2827
2887
  "tests": 4,
2828
2888
  "asserts": 7,
2829
- "skips": 0,
2889
+ "skips": 3,
2830
2890
  "todos": 0
2831
2891
  },
2832
2892
  "products.test.mjs": {
@@ -2964,7 +3024,7 @@
2964
3024
  "reading-the-demo-does-not-edit-the-repository.test.mjs": {
2965
3025
  "tests": 2,
2966
3026
  "asserts": 8,
2967
- "skips": 0,
3027
+ "skips": 1,
2968
3028
  "todos": 0
2969
3029
  },
2970
3030
  "reason-cut-is-visible.test.mjs": {
@@ -3076,8 +3136,8 @@
3076
3136
  "todos": 0
3077
3137
  },
3078
3138
  "reference-score-owner-identity.test.mjs": {
3079
- "tests": 9,
3080
- "asserts": 30,
3139
+ "tests": 10,
3140
+ "asserts": 34,
3081
3141
  "skips": 0,
3082
3142
  "todos": 0
3083
3143
  },
@@ -3137,7 +3197,7 @@
3137
3197
  },
3138
3198
  "register-count.test.mjs": {
3139
3199
  "tests": 20,
3140
- "asserts": 145,
3200
+ "asserts": 148,
3141
3201
  "skips": 0,
3142
3202
  "todos": 0
3143
3203
  },
@@ -3160,8 +3220,8 @@
3160
3220
  "todos": 0
3161
3221
  },
3162
3222
  "register-digest-record.test.mjs": {
3163
- "tests": 30,
3164
- "asserts": 102,
3223
+ "tests": 32,
3224
+ "asserts": 114,
3165
3225
  "skips": 0,
3166
3226
  "todos": 0
3167
3227
  },
@@ -3238,8 +3298,8 @@
3238
3298
  "todos": 0
3239
3299
  },
3240
3300
  "release-pipeline.test.mjs": {
3241
- "tests": 52,
3242
- "asserts": 243,
3301
+ "tests": 87,
3302
+ "asserts": 334,
3243
3303
  "skips": 0,
3244
3304
  "todos": 0
3245
3305
  },
@@ -3856,8 +3916,8 @@
3856
3916
  "todos": 0
3857
3917
  },
3858
3918
  "start-background-units.test.mjs": {
3859
- "tests": 6,
3860
- "asserts": 20,
3919
+ "tests": 9,
3920
+ "asserts": 26,
3861
3921
  "skips": 0,
3862
3922
  "todos": 0
3863
3923
  },
@@ -4252,8 +4312,8 @@
4252
4312
  "todos": 0
4253
4313
  },
4254
4314
  "the-narrative-is-checked-against-its-depth-rules.test.mjs": {
4255
- "tests": 15,
4256
- "asserts": 55,
4315
+ "tests": 19,
4316
+ "asserts": 70,
4257
4317
  "skips": 0,
4258
4318
  "todos": 0
4259
4319
  },
@@ -4335,6 +4395,12 @@
4335
4395
  "skips": 0,
4336
4396
  "todos": 0
4337
4397
  },
4398
+ "the-reference-strip-left-sentences-unfinished.test.mjs": {
4399
+ "tests": 6,
4400
+ "asserts": 13,
4401
+ "skips": 3,
4402
+ "todos": 0
4403
+ },
4338
4404
  "the-repair-order-and-the-tool-schema-agree.test.mjs": {
4339
4405
  "tests": 6,
4340
4406
  "asserts": 27,
@@ -4347,6 +4413,12 @@
4347
4413
  "skips": 1,
4348
4414
  "todos": 0
4349
4415
  },
4416
+ "the-report-carries-what-the-assessment-wrote.test.mjs": {
4417
+ "tests": 10,
4418
+ "asserts": 41,
4419
+ "skips": 0,
4420
+ "todos": 0
4421
+ },
4350
4422
  "the-report-shell-keeps-what-a-repair-does-not-resend.test.mjs": {
4351
4423
  "tests": 6,
4352
4424
  "asserts": 28,
@@ -4690,8 +4762,8 @@
4690
4762
  "todos": 0
4691
4763
  },
4692
4764
  "verify-knockout.test.mjs": {
4693
- "tests": 1,
4694
- "asserts": 5,
4765
+ "tests": 2,
4766
+ "asserts": 11,
4695
4767
  "skips": 0,
4696
4768
  "todos": 0
4697
4769
  },
@@ -4922,6 +4994,12 @@
4922
4994
  "skips": 0,
4923
4995
  "todos": 0
4924
4996
  },
4997
+ "knockout-audit-tools.test.mjs": {
4998
+ "tests": 16,
4999
+ "asserts": 73,
5000
+ "skips": 0,
5001
+ "todos": 0
5002
+ },
4925
5003
  "lexsearch.test.mjs": {
4926
5004
  "tests": 5,
4927
5005
  "asserts": 11,
@@ -5043,8 +5121,8 @@
5043
5121
  "todos": 0
5044
5122
  },
5045
5123
  "whatif.test.mjs": {
5046
- "tests": 9,
5047
- "asserts": 29,
5124
+ "tests": 10,
5125
+ "asserts": 32,
5048
5126
  "skips": 0,
5049
5127
  "todos": 0
5050
5128
  }
@@ -641,6 +641,53 @@ export function unitInventoryVerdict({
641
641
  * @param {string|null} raw the value `systemctl show -p WorkingDirectory` printed
642
642
  * @returns {{path: string|null, prefixes: string, why: string|null}}
643
643
  */
644
+ /**
645
+ * Which checkout is a unit actually serving — and which piece of evidence said so.
646
+ *
647
+ * A unit can be attributed two ways and they are not equally good.
648
+ *
649
+ * `WorkingDirectory` is a DECLARATION. It is what the unit file asks for, and it is frequently not a
650
+ * checkout at all: a unit written as `WorkingDirectory=~` reports `!/home/<user>`, which resolves to a
651
+ * home directory that is not a git tree, and the unit then drops out of the comparison entirely. That is
652
+ * not a rare shape — it is what a unit looks like when its author put the checkout in `ExecStart=`
653
+ * instead, which is the normal thing to do, because systemd cannot expand a variable in
654
+ * `WorkingDirectory=` and can in `ExecStart=`.
655
+ *
656
+ * The RUNNING COMMAND LINE is an OBSERVATION: the absolute path the live process was actually started
657
+ * with. So it wins, and that order is the point rather than a preference. The incident this comparison
658
+ * exists for is a deployment that served its portal from one clone while running its runner from
659
+ * another — and in that state the declaration and the process disagree, with the process telling the
660
+ * truth. Preferring the declaration would take the wrong side of exactly the case the check is for.
661
+ *
662
+ * WHEN BOTH RESOLVE AND DISAGREE, that IS the finding, and it is returned rather than quietly resolved:
663
+ * a unit whose declared tree and running tree differ has been repointed since it started, and it is
664
+ * serving the older one until something restarts it.
665
+ *
666
+ * PURE — the caller does the reading (systemd, the unit file, the process table) and this decides.
667
+ *
668
+ * @param {object} a
669
+ * @param {string|null} a.declaredTree a git tree resolved from `WorkingDirectory`, or null
670
+ * @param {string|null} a.runningTree a git tree resolved from the live process's argv, or null
671
+ * @param {string|null} a.declaredWhy why the declaration gave nothing, if it gave nothing
672
+ * @param {string|null} a.runningWhy why the process gave nothing, if it gave nothing
673
+ * @returns {{clone: string|null, source: string|null, disagreement: string|null, why: string|null}}
674
+ */
675
+ export function unitClone({ declaredTree = null, runningTree = null, declaredWhy = null, runningWhy = null } = {}) {
676
+ const norm = (p) => String(p ?? "").replace(/\/+$/, "") || null;
677
+ const declared = norm(declaredTree);
678
+ const running = norm(runningTree);
679
+ const disagreement = declared && running && declared !== running
680
+ ? `the unit declares ${declared} and its running process was started from ${running} — it is serving the second until something restarts it`
681
+ : null;
682
+ if (running) return { clone: running, source: "the running command line", disagreement, why: null };
683
+ if (declared) return { clone: declared, source: "WorkingDirectory", disagreement, why: null };
684
+ // BOTH REASONS, never the first one that came to hand. A caller that reports only "no WorkingDirectory"
685
+ // sends the next reader to fix a unit file when the process table was the half that could not be read.
686
+ const parts = [declaredWhy, runningWhy].filter(Boolean);
687
+ return { clone: null, source: null, disagreement: null,
688
+ why: parts.length ? parts.join("; and ") : "neither the unit's WorkingDirectory nor its running command line named a checkout" };
689
+ }
690
+
644
691
  export function unitWorkingDirectory(raw) {
645
692
  const s = String(raw ?? "").trim();
646
693
  if (!s) return { path: null, prefixes: "", why: "the unit reported no WorkingDirectory" };
@@ -14,9 +14,9 @@
14
14
  //
15
15
  // Anything that is neither `active` nor `inactive` is a fault. `prelim-driver` is a `Type=oneshot` fired
16
16
  // by a 90-second timer, so EVERY drain passes through `activating` — and `activating` landed in that
17
- // list. The filter tolerated the oneshot at rest and failed it for working. deploy-test.sh gates the
18
- // hourly test-instance deploy on this script's exit code, so a deploy that happened to land inside a
19
- // drain window reported the instance unhealthy and exited 1 on a deployment that was fine.
17
+ // list. The filter tolerated the oneshot at rest and failed it for working. The hourly test-instance
18
+ // deploy gates on this script's exit code, so a deploy that happened to land inside a drain window
19
+ // reported the instance unhealthy and exited 1 on a deployment that was fine.
20
20
  //
21
21
  // The bug is not the enumeration — was right to give this three outcomes instead of two. The bug is
22
22
  // INVERSE ENUMERATION over an incomplete vocabulary: listing the two states you have seen and calling
@@ -39,7 +39,7 @@
39
39
  // The ruling on says "starting and stopping are both tolerated for a one-shot". That is descriptive
40
40
  // of the case that was observed, not a restriction: the issue's own Scope section carries no unit-type
41
41
  // qualifier, the arm has never been unit-type-aware, and it already tolerates `inactive` for everything.
42
- // It is also the wrong place to draw the line — deploy-test.sh RESTARTS the long-running services
42
+ // It is also the wrong place to draw the line — the hourly deploy RESTARTS the long-running services
43
43
  // immediately before running this check, so `trademark-portal=activating` is the same race with a
44
44
  // different unit, and a oneshot-only fix would leave it standing. The unit's Type is still carried into
45
45
  // the message so a reader can tell a oneshot mid-fire from a service mid-restart.
@@ -63,9 +63,9 @@
63
63
  // This is not caution for its own sake. An earlier draft of this fix also widened the count to treat
64
64
  // transitional units as up, on the reasoning that a oneshot mid-fire is "not nothing". The consequence:
65
65
  // {5 services activating, 3 inactive} returned `pass` with a message that began "0 active" — a green
66
- // tick on the deploy's final gate having confirmed that ZERO services were running. deploy-test.sh
66
+ // tick on the deploy's final gate having confirmed that ZERO services were running. The hourly deploy
67
67
  // restarts the long-running services immediately before running this check, so that is the ordinary
68
- // shape of the box, not a corner. Widening a count to make a message read better is how a guard dies.
68
+ // shape of the instance, not a corner. Widening a count to make a message read better is how a guard dies.
69
69
 
70
70
  /** The six documented ActiveState values of systemd 255, mapped to what the check must do about them. */
71
71
  export const ACTIVE_STATE_MEANING = Object.freeze({
@@ -134,8 +134,8 @@ export function unitsActiveVerdict({ units, probe }) {
134
134
  // CHANGES NOTHING HERE. The count below is origin/main's, predicate and message unchanged:
135
135
  // `active`, literally, and nothing else. An earlier draft of this fix widened it to count
136
136
  // transitional units as up, which defeated this guard outright — {5 activating, 3 inactive} then
137
- // returned `pass` with a message that began "0 active", on the deploy's FINAL gate, on a box where
138
- // deploy-test.sh has just restarted every long-running service. 's Scope names one thing, the
137
+ // returned `pass` with a message that began "0 active", on the deploy's FINAL gate, on an instance
138
+ // where the hourly deploy has just restarted every long-running service. Scope names one thing, the
139
139
  // state vocabulary of the FAULT branch above. Transitional units are named in the messages here so
140
140
  // they are never swallowed; they are counted as up nowhere.
141
141
  const activeCount = (units ?? []).filter((u) => u.active === "active").length;
@@ -442,7 +442,7 @@ export const validators = {
442
442
  // — the register reads and the per-finding weighed lists, both joined to the store.
443
443
  if (m.registerReads !== undefined && m.registerReads !== null) {
444
444
  if (!Array.isArray(m.registerReads))
445
- return { ok: false, reason: `mark "${m.name}": registerReads must be an ARRAY of { recordId, read } rows, or omitted entirely` };
445
+ return { ok: false, reason: `mark "${m.name}": registerReads must be an ARRAY of { recordId, read, band? } rows, or omitted entirely` };
446
446
  const held = registerRecordIdsFor(runDir, m.name);
447
447
  for (const row of m.registerReads) {
448
448
  const id = String(row?.recordId ?? "").trim();
@@ -451,6 +451,14 @@ export const validators = {
451
451
  if (!read) return { ok: false, reason: `mark "${m.name}": registerReads row "${id}" has an empty read. Omit the row rather than sending an empty one: a filing with no read keeps the card's neutral line, which is true` };
452
452
  if (!held.has(id))
453
453
  return { ok: false, reason: `mark "${m.name}": registerReads cites "${id}", which is not a record this run holds for that mark. The id must be copied from the filings you were handed — the driver joins it against the store and never takes your word for it` };
454
+ // The band is OPTIONAL and, when present, is the same closed vocabulary the mark's own rating
455
+ // uses — the card prints it as this filing's rating, so a word outside the frozen ladder would
456
+ // put a chip on the page in a dialect the framework does not define. Absence stays free: a read
457
+ // with no band prints the read and no chip, which is what every run before this one produced.
458
+ if (row?.band != null && String(row.band).trim()) {
459
+ if (ladder.length && bandIdx(ladder, row.band) < 0)
460
+ return { ok: false, reason: `knockout_band_unknown:${m.name}: registerReads row "${id}" carries band "${row.band}", which is not in the frozen ladder (${ladder.join(" / ")}) — rate the filing in the framework's own vocabulary, or omit the band and let the read stand alone` };
461
+ }
454
462
  }
455
463
  }
456
464
  for (const f of (Array.isArray(m.findings) ? m.findings : [])) {
package/driver/verify.mjs CHANGED
@@ -170,8 +170,8 @@ function needs(content, markers, label, names = []) {
170
170
  //
171
171
  // THE CLASS. Every gate below keys on prose the MODEL composes, so each is one phrasing drift away
172
172
  // from killing a run that produced the section perfectly. Measured across three runs and two engines:
173
- // pewter-lantern (codex) wrote "## Negative-results matrix" and was rejected twice, killing a client
174
- // run; umber-beacon (anthropic) wrote "## Negative results (per-cell detail)" — different spelling AND
173
+ // one run on codex wrote "## Negative-results matrix" and was rejected twice, killing the run; another
174
+ // on anthropic wrote "## Negative results (per-cell detail)" — different spelling AND
175
175
  // different trailing words, same skill. PR 336 widened the regex to `[\s-]`, which fixed those two
176
176
  // instances and left the class exactly where it was: the gate still asks the model to guess a spelling.
177
177
  //