clearotron 0.2.1 → 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 (45) hide show
  1. package/INSTALL.md +5 -4
  2. package/bin/example.mjs +9 -5
  3. package/bin/onboard.mjs +19 -19
  4. package/bin/stop.mjs +65 -3
  5. package/build-info.json +2 -2
  6. package/driver/CHANGELOG.md +21 -0
  7. package/driver/declination-call.mjs +32 -0
  8. package/driver/driver.config.mjs +20 -0
  9. package/driver/engine/mcp/recording-server.mjs +4 -0
  10. package/driver/gateway.mjs +8 -3
  11. package/driver/knockout-assess-record.mjs +5 -1
  12. package/driver/package.json +1 -1
  13. package/driver/pipeline.mjs +83 -2
  14. package/driver/predelivery-lint.mjs +22 -4
  15. package/driver/publish/knockout.mjs +12 -6
  16. package/driver/publish/render-knockout.mjs +133 -23
  17. package/driver/publish/report-data.mjs +13 -3
  18. package/driver/record-carry.mjs +2 -2
  19. package/driver/reference-score.mjs +1 -1
  20. package/driver/result-noun-fields.mjs +7 -0
  21. package/driver/skills/knockout-assess/SKILL.md +10 -4
  22. package/driver/stages-knockout.mjs +1 -1
  23. package/driver/stages.mjs +1 -1
  24. package/driver/suite-census.json +66 -18
  25. package/driver/unit-inventory.mjs +47 -0
  26. package/driver/unit-state-verdict.mjs +8 -8
  27. package/driver/verify-knockout.mjs +9 -1
  28. package/driver/whatif-memo-run.mjs +45 -4
  29. package/mcp-server/CHANGELOG.md +2 -0
  30. package/mcp-server/lib/brief.mjs +15 -0
  31. package/mcp-server/lib/driver.mjs +6 -0
  32. package/mcp-server/lib/knockout.mjs +435 -0
  33. package/mcp-server/lib/scrub.mjs +1 -1
  34. package/mcp-server/package.json +1 -1
  35. package/mcp-server/server.mjs +69 -4
  36. package/package.json +1 -1
  37. package/portal-ui/package.json +1 -1
  38. package/providers/oauth-mcp-bridge/CHANGELOG.md +2 -0
  39. package/providers/oauth-mcp-bridge/package.json +1 -1
  40. package/scripts/drain-preflight.mjs +2 -2
  41. package/scripts/freeze-example-run.mjs +3 -3
  42. package/scripts/headless-page.mjs +51 -2
  43. package/scripts/live-surface-check.mjs +86 -17
  44. package/scripts/render-check.mjs +61 -2
  45. package/scripts/deploy-test.sh +0 -309
@@ -213,6 +213,12 @@
213
213
  "skips": 0,
214
214
  "todos": 0
215
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
+ },
216
222
  "a-fact-about-a-named-party.test.mjs": {
217
223
  "tests": 30,
218
224
  "asserts": 80,
@@ -298,8 +304,8 @@
298
304
  "todos": 0
299
305
  },
300
306
  "a-memo-over-a-delivered-report-leaves-it-alone.test.mjs": {
301
- "tests": 9,
302
- "asserts": 45,
307
+ "tests": 13,
308
+ "asserts": 52,
303
309
  "skips": 0,
304
310
  "todos": 0
305
311
  },
@@ -387,6 +393,12 @@
387
393
  "skips": 0,
388
394
  "todos": 0
389
395
  },
396
+ "a-queued-what-if-is-answered-before-its-run-archives.test.mjs": {
397
+ "tests": 3,
398
+ "asserts": 12,
399
+ "skips": 0,
400
+ "todos": 0
401
+ },
390
402
  "a-real-install-starts-with-an-empty-archive.test.mjs": {
391
403
  "tests": 6,
392
404
  "asserts": 16,
@@ -496,8 +508,8 @@
496
508
  "todos": 0
497
509
  },
498
510
  "a-screenshot-of-an-error-page-is-not-a-report.test.mjs": {
499
- "tests": 17,
500
- "asserts": 44,
511
+ "tests": 21,
512
+ "asserts": 55,
501
513
  "skips": 4,
502
514
  "todos": 0
503
515
  },
@@ -567,6 +579,12 @@
567
579
  "skips": 0,
568
580
  "todos": 0
569
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
+ },
570
588
  "a-verb-is-a-module-until-it-is-the-command.test.mjs": {
571
589
  "tests": 3,
572
590
  "asserts": 8,
@@ -700,8 +718,8 @@
700
718
  "todos": 0
701
719
  },
702
720
  "an-experiment-receipt-names-its-engine.test.mjs": {
703
- "tests": 6,
704
- "asserts": 22,
721
+ "tests": 8,
722
+ "asserts": 27,
705
723
  "skips": 0,
706
724
  "todos": 0
707
725
  },
@@ -711,12 +729,24 @@
711
729
  "skips": 1,
712
730
  "todos": 0
713
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
+ },
714
738
  "an-ordered-field-is-a-field-the-tool-can-express.test.mjs": {
715
739
  "tests": 1,
716
740
  "asserts": 6,
717
741
  "skips": 0,
718
742
  "todos": 0
719
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
+ },
720
750
  "anchor-and-script-aware-quote.test.mjs": {
721
751
  "tests": 12,
722
752
  "asserts": 40,
@@ -1348,8 +1378,8 @@
1348
1378
  "todos": 0
1349
1379
  },
1350
1380
  "declination-is-stated-not-inferred.test.mjs": {
1351
- "tests": 20,
1352
- "asserts": 60,
1381
+ "tests": 24,
1382
+ "asserts": 69,
1353
1383
  "skips": 0,
1354
1384
  "todos": 0
1355
1385
  },
@@ -1397,7 +1427,7 @@
1397
1427
  },
1398
1428
  "deployment-hostnames.test.mjs": {
1399
1429
  "tests": 13,
1400
- "asserts": 37,
1430
+ "asserts": 36,
1401
1431
  "skips": 0,
1402
1432
  "todos": 0
1403
1433
  },
@@ -2457,6 +2487,12 @@
2457
2487
  "skips": 2,
2458
2488
  "todos": 0
2459
2489
  },
2490
+ "no-run-codenames-in-tracked-files.test.mjs": {
2491
+ "tests": 6,
2492
+ "asserts": 13,
2493
+ "skips": 4,
2494
+ "todos": 0
2495
+ },
2460
2496
  "no-test-writes-inside-the-checkout.test.mjs": {
2461
2497
  "tests": 16,
2462
2498
  "asserts": 35,
@@ -3161,7 +3197,7 @@
3161
3197
  },
3162
3198
  "register-count.test.mjs": {
3163
3199
  "tests": 20,
3164
- "asserts": 145,
3200
+ "asserts": 148,
3165
3201
  "skips": 0,
3166
3202
  "todos": 0
3167
3203
  },
@@ -3262,8 +3298,8 @@
3262
3298
  "todos": 0
3263
3299
  },
3264
3300
  "release-pipeline.test.mjs": {
3265
- "tests": 83,
3266
- "asserts": 320,
3301
+ "tests": 87,
3302
+ "asserts": 334,
3267
3303
  "skips": 0,
3268
3304
  "todos": 0
3269
3305
  },
@@ -3880,8 +3916,8 @@
3880
3916
  "todos": 0
3881
3917
  },
3882
3918
  "start-background-units.test.mjs": {
3883
- "tests": 6,
3884
- "asserts": 20,
3919
+ "tests": 9,
3920
+ "asserts": 26,
3885
3921
  "skips": 0,
3886
3922
  "todos": 0
3887
3923
  },
@@ -4276,8 +4312,8 @@
4276
4312
  "todos": 0
4277
4313
  },
4278
4314
  "the-narrative-is-checked-against-its-depth-rules.test.mjs": {
4279
- "tests": 15,
4280
- "asserts": 55,
4315
+ "tests": 19,
4316
+ "asserts": 70,
4281
4317
  "skips": 0,
4282
4318
  "todos": 0
4283
4319
  },
@@ -4377,6 +4413,12 @@
4377
4413
  "skips": 1,
4378
4414
  "todos": 0
4379
4415
  },
4416
+ "the-report-carries-what-the-assessment-wrote.test.mjs": {
4417
+ "tests": 10,
4418
+ "asserts": 41,
4419
+ "skips": 0,
4420
+ "todos": 0
4421
+ },
4380
4422
  "the-report-shell-keeps-what-a-repair-does-not-resend.test.mjs": {
4381
4423
  "tests": 6,
4382
4424
  "asserts": 28,
@@ -4720,8 +4762,8 @@
4720
4762
  "todos": 0
4721
4763
  },
4722
4764
  "verify-knockout.test.mjs": {
4723
- "tests": 1,
4724
- "asserts": 5,
4765
+ "tests": 2,
4766
+ "asserts": 11,
4725
4767
  "skips": 0,
4726
4768
  "todos": 0
4727
4769
  },
@@ -4952,6 +4994,12 @@
4952
4994
  "skips": 0,
4953
4995
  "todos": 0
4954
4996
  },
4997
+ "knockout-audit-tools.test.mjs": {
4998
+ "tests": 16,
4999
+ "asserts": 73,
5000
+ "skips": 0,
5001
+ "todos": 0
5002
+ },
4955
5003
  "lexsearch.test.mjs": {
4956
5004
  "tests": 5,
4957
5005
  "asserts": 11,
@@ -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 : [])) {
@@ -31,7 +31,7 @@ import { createHash } from "node:crypto";
31
31
  import { existsSync, readFileSync, readdirSync, mkdirSync, writeFileSync, renameSync, statSync } from "node:fs";
32
32
  import { join, dirname } from "node:path";
33
33
  import { fileURLToPath } from "node:url";
34
- import { driverDir } from "../shared/driver-dir.mjs";
34
+ import { driverDir, DRIVER_DIR } from "../shared/driver-dir.mjs"; // the subtree name from its one definition, never a literal
35
35
  import { composeMemo } from "./whatif-memo.mjs";
36
36
 
37
37
  export const SKILL_ROOT = join(dirname(fileURLToPath(import.meta.url)), "skills");
@@ -85,6 +85,43 @@ export function movedArtifacts(before = {}, after = {}) {
85
85
  .map((p) => ({ path: p, before: before[p] ?? null, after: after[p] ?? null }));
86
86
  }
87
87
 
88
+ /**
89
+ * Split what moved into the memo's OWN RECORD and everything else.
90
+ *
91
+ * A memo legitimately leaves two kinds of trace on its parent. Its documents go under `_memos/`, which
92
+ * `digestRunDir` already skips. Its DISPATCH leaves driver telemetry — `_driver/whatif-memo.*` and an
93
+ * appended line in `_driver/run.jsonl` — because the memo rides the same gateway ladder as any seat and
94
+ * the gateway records what it dispatched. That is the parent gaining a RECORD of the memo, which is
95
+ * correct behaviour and the same rule the experiment path already lives by; it is not the archive being
96
+ * rewritten.
97
+ *
98
+ * ✕ EVERYTHING ELSE IS THE SEAT WRITING WHERE IT WAS NOT INVITED, and it is refused by name. The seat
99
+ * has a real file-write grant on the run directory, and on a delivered run it used it: an archived
100
+ * client run carries `memo-osler-coexistence.json` at its root, which is the seat's reply written to a
101
+ * path of its own choosing instead of to the `expectFile` it was given. That also explains the retry
102
+ * that preceded it — the harness looked for the reply where it had asked for it and did not find it.
103
+ *
104
+ * `run.jsonl` may only GROW. A shrink or a rewrite is not an append, and an append is the only thing a
105
+ * dispatch does to it. The check is on size because the digest carries `sha:size` and not the bytes; a
106
+ * rewrite that happened to keep the same length would pass, and that is written down rather than
107
+ * claimed against. PURE.
108
+ */
109
+ export function splitMemoFootprint(moved = []) {
110
+ const own = [];
111
+ const foreign = [];
112
+ const sizeOf = (v) => { const n = Number(String(v ?? "").split(":")[1]); return Number.isFinite(n) ? n : null; };
113
+ for (const m of moved) {
114
+ const p = String(m?.path ?? "");
115
+ if (p.startsWith(`${DRIVER_DIR}/whatif-memo.`)) { own.push(m); continue; }
116
+ if (p === `${DRIVER_DIR}/run.jsonl`) {
117
+ const b = sizeOf(m.before); const a = sizeOf(m.after);
118
+ if (b !== null && a !== null && a > b) { own.push(m); continue; }
119
+ }
120
+ foreign.push(m);
121
+ }
122
+ return { own, foreign };
123
+ }
124
+
88
125
  /** The profile key the PARENT rated under — read from its frozen sidecar, never re-resolved. */
89
126
  export function parentRatedUnder(runDir, { read = readFileSync } = {}) {
90
127
  try { return JSON.parse(read(driverDir(runDir, "profile.json"), "utf8"))?.profileKey ?? null; }
@@ -299,10 +336,14 @@ export async function askArchivedRun({ runId, question, requestedBy = null } = {
299
336
 
300
337
  // AFTER the write, deliberately: the check has to cover this call's own behaviour, not merely the
301
338
  // reasoning pass's. If writing the memo touched the parent, that is exactly what must be caught.
302
- const moved = movedArtifacts(before, digestRunDir(run.runDir));
303
- if (moved.length)
339
+ const { own, foreign } = splitMemoFootprint(movedArtifacts(before, digestRunDir(run.runDir)));
340
+ if (foreign.length)
304
341
  return { ok: false, fail: MEMO_FAILS.PARENT_MOVED,
305
- detail: `the parent run's artifacts moved during the memo: ${moved.slice(0, 5).map((m) => m.path).join(", ")}`, moved };
342
+ // NAMES WHAT IT FOUND AND WHY IT IS WRONG. "the parent's artifacts moved" was true of a correct
343
+ // memo too — the dispatch telemetry tripped it — so the sentence could not tell a reader whether
344
+ // the archive had been rewritten or the memo had merely run.
345
+ detail: `a memo may write only to ${MEMO_DIR}/ and its own reply file; these were written into the parent run instead: ${foreign.slice(0, 5).map((m) => m.path).join(", ")}`,
346
+ moved: foreign, memoFootprint: own };
306
347
 
307
348
  return {
308
349
  ok: true,
@@ -1,5 +1,7 @@
1
1
  # trademark-artifacts-mcp
2
2
 
3
+ ## 0.3.0-beta.0
4
+
3
5
  ## 0.2.1
4
6
 
5
7
  ## 0.2.1-beta.2
@@ -135,6 +135,21 @@ export function buildBrief(run) {
135
135
  lines.push(`- **${m.name}** — ${band}.${d.url ? ` Report: ${d.url}` : ""}`);
136
136
  for (const f of (m.findings ?? [])) {
137
137
  const who = [f.name, f.owner].filter(Boolean).join(" — ");
138
+ // A PROMOTED REGISTER FILING SHOWS THE RATING AND THE READ THE SEARCH ACTUALLY MADE (tracker
139
+ // issue 274). This line used to print `net` alone, and for a register card `net` carried the
140
+ // stated "no rating of its own" — so the one hard legal right on a page was described here as
141
+ // unrated even on runs where the assessment had written a full read of that exact filing and
142
+ // the report was already printing it. The page and this briefing disagreed.
143
+ //
144
+ // SCOPED TO `shape === 'register'` DELIBERATELY. Every other finding line is left exactly as it
145
+ // was: a typed conflict already leads with its own band on the report, and widening this to all
146
+ // findings would change what this briefing says about runs that have no register layer at all.
147
+ if (f.shape === "register") {
148
+ const rating = f.band ? ` — ${titleCase(String(f.band))} risk.` : "";
149
+ const read = f.basis && f.basis !== f.net ? ` ${f.basis}` : "";
150
+ lines.push(` - ${who}${rating}${f.net ? ` ${f.net}` : ""}${read}`.trimEnd());
151
+ continue;
152
+ }
138
153
  lines.push(` - ${who}${f.net ? `: ${f.net}` : ""}`.trimEnd());
139
154
  }
140
155
  }
@@ -14,6 +14,12 @@ export {
14
14
  REGISTER_AXES, decideAxes, axisTier,
15
15
  } from "../../driver/stages.mjs";
16
16
 
17
+ // The KNOCKOUT lane's own run-dir table. `paths` above is the CLEARANCE table and has no entry for any
18
+ // file this lane writes, which is why every audit projection read a delivered knockout as a run with
19
+ // nothing on disk (tracker issue 275). Re-exported rather than re-derived for the reason this whole file
20
+ // exists: a second copy of a path table drifts, and the drift shows up as an artifact reported missing.
21
+ export { koPaths } from "../../driver/stages-knockout.mjs";
22
+
17
23
  // stripInternal/stripEngineInternals/stripTelemetry are the driver's OWN client-safety transforms — the
18
24
  // same ones publish/render.mjs applies to the client HTML export. lib/scrub.mjs composes them so the MCP
19
25
  // client surface and the delivered report answer "what may a client see?" from ONE definition ( R1: