clearotron 0.2.4 → 0.3.0-beta.1

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 (235) hide show
  1. package/.env.example +13 -2
  2. package/CONTRIBUTING.md +1 -1
  3. package/INSTALL.md +62 -38
  4. package/bin/brandowner.mjs +18 -169
  5. package/bin/clearotron.mjs +3 -1
  6. package/bin/connect.mjs +28 -19
  7. package/bin/disconnect.mjs +3 -3
  8. package/bin/example.mjs +7 -7
  9. package/bin/framework-preflight.mjs +49 -0
  10. package/bin/grant.mjs +151 -93
  11. package/bin/onboard.mjs +220 -138
  12. package/bin/start.mjs +146 -137
  13. package/bin/stop.mjs +2 -2
  14. package/bin/update.mjs +1 -1
  15. package/build-info.json +2 -2
  16. package/docs/CLIENT-MCP.md +2 -2
  17. package/docs/E2E.md +12 -2
  18. package/docs/ONBOARDING.md +1 -1
  19. package/docs/PORTAL.md +14 -13
  20. package/docs/SECURITY.md +23 -24
  21. package/docs/architecture/04-configuration-reference.md +12 -5
  22. package/docs/architecture/05-config-governance.md +7 -7
  23. package/docs/architecture/07-quality-and-audit.md +1 -1
  24. package/docs/architecture/08-development-guide.md +5 -0
  25. package/docs/configuration.md +118 -0
  26. package/docs/decisions/0004-documentation-structure.md +2 -2
  27. package/docs/decisions/0006-what-the-public-repository-carries.md +2 -2
  28. package/driver/CHANGELOG.md +70 -0
  29. package/driver/ask-ledger.mjs +2 -2
  30. package/driver/cancel.mjs +27 -0
  31. package/driver/case-law-sources.mjs +3 -3
  32. package/driver/company-bundle.mjs +261 -0
  33. package/driver/compare.mjs +1 -1
  34. package/driver/compose-read.mjs +2 -2
  35. package/driver/config-inventory.mjs +2 -2
  36. package/driver/contract-audit.mjs +1 -1
  37. package/driver/contract-e3-backlog.mjs +1 -1
  38. package/driver/contract-vocabulary.mjs +1 -1
  39. package/driver/declination-call.mjs +1 -1
  40. package/driver/deliver-trigger.sh +3 -3
  41. package/driver/dev-portal.mjs +3 -1
  42. package/driver/digest-queue.mjs +1 -1
  43. package/driver/disposition-tool.mjs +1 -1
  44. package/driver/doc-constants.mjs +1 -1
  45. package/driver/drain-posture.mjs +2 -2
  46. package/driver/drainer-identity.mjs +1 -1
  47. package/driver/driver.config.mjs +44 -9
  48. package/driver/effective-scope.mjs +30 -1
  49. package/driver/effort-model.mjs +6 -6
  50. package/driver/engine/CONTRACT.md +2 -2
  51. package/driver/engine/anthropic-agent.mjs +11 -11
  52. package/driver/engine/jx-turn.mjs +1 -1
  53. package/driver/engine/mcp/gather-config.mjs +29 -4
  54. package/driver/engine/mcp/recording-server.mjs +73 -1
  55. package/driver/engine/openai-agent.mjs +1 -1
  56. package/driver/engine/probe.mjs +28 -4
  57. package/driver/enqueue-schema.mjs +23 -3
  58. package/driver/findings-model.mjs +2 -2
  59. package/driver/flag-snapshot.mjs +2 -2
  60. package/driver/floor-duty.mjs +2 -2
  61. package/driver/frame-diff-model.mjs +1 -1
  62. package/driver/framework-preflight.mjs +143 -0
  63. package/driver/gateway.mjs +9 -1
  64. package/driver/hit-list.mjs +1 -1
  65. package/driver/jx-lanes.mjs +1 -1
  66. package/driver/jx.mjs +1 -1
  67. package/driver/knockout-assess-record.mjs +1 -1
  68. package/driver/knockout-review-record.mjs +435 -0
  69. package/driver/order-probe.mjs +1 -1
  70. package/driver/outbox-backoff.mjs +2 -2
  71. package/driver/owner-use-check.mjs +2 -2
  72. package/driver/package.json +1 -1
  73. package/driver/pipeline-knockout.mjs +105 -8
  74. package/driver/pipeline.mjs +81 -30
  75. package/driver/plain-register.mjs +77 -3
  76. package/driver/portal-access.mjs +141 -74
  77. package/driver/portal-config-view.mjs +59 -70
  78. package/driver/portal-report.mjs +4 -4
  79. package/driver/portal-service.mjs +348 -141
  80. package/driver/portal-upstream.mjs +105 -17
  81. package/driver/predelivery-lint.mjs +43 -18
  82. package/driver/product-rows.mjs +1 -1
  83. package/driver/products.mjs +1 -1
  84. package/driver/profile-page.html +30 -5
  85. package/driver/profile-service.mjs +197 -26
  86. package/driver/profiles.mjs +48 -1
  87. package/driver/publish/index.mjs +31 -13
  88. package/driver/publish/knockout.mjs +9 -5
  89. package/driver/publish/office-record-links.mjs +189 -0
  90. package/driver/publish/parse.mjs +3 -3
  91. package/driver/publish/publish-inputs.mjs +26 -0
  92. package/driver/publish/render-knockout.mjs +42 -42
  93. package/driver/publish/render.mjs +29 -4
  94. package/driver/publish/report-data.mjs +2 -2
  95. package/driver/publish/seed-pool.mjs +1 -1
  96. package/driver/publish/templates/report.css +8 -8
  97. package/driver/publish/xlsx.mjs +49 -7
  98. package/driver/queue-watch-verdict.mjs +2 -2
  99. package/driver/recipe-service.mjs +1 -1
  100. package/driver/record-carry.mjs +1 -1
  101. package/driver/reference-score.mjs +1 -1
  102. package/driver/reference-strip-signatures.mjs +1 -1
  103. package/driver/register-availability.mjs +4 -3
  104. package/driver/register-count.mjs +3 -3
  105. package/driver/register-records.mjs +1 -1
  106. package/driver/repair-composers.mjs +1 -1
  107. package/driver/repairs.mjs +3 -3
  108. package/driver/replay-archive.mjs +1 -1
  109. package/driver/report-card-record.mjs +1 -1
  110. package/driver/result-noun-fields.mjs +5 -0
  111. package/driver/roster-verdict.mjs +48 -5
  112. package/driver/run-activity.mjs +1 -1
  113. package/driver/run-requirements.mjs +18 -5
  114. package/driver/runner.mjs +24 -15
  115. package/driver/search-policy.mjs +8 -8
  116. package/driver/senior-rights.mjs +1 -1
  117. package/driver/skills/prelim-search/delivery-contract.md +1 -1
  118. package/driver/skills/prelim-search/risk-framework-triage.md +10 -7
  119. package/driver/stages-knockout.mjs +72 -6
  120. package/driver/stages.mjs +10 -10
  121. package/driver/suite-census.json +238 -70
  122. package/driver/synthesis-record.mjs +2 -2
  123. package/driver/systemd/clearotron-client-mcp.service +3 -3
  124. package/driver/systemd/clearotron-deploy.service +2 -2
  125. package/driver/systemd/clearotron-mcp-face.service +1 -1
  126. package/driver/systemd/clearotron-portal.service +3 -3
  127. package/driver/systemd/clearotron-worker.service +5 -5
  128. package/driver/systemd/install-census.mjs +1 -1
  129. package/driver/systemd/render-units.mjs +9 -9
  130. package/driver/terminal-clamp.mjs +1 -1
  131. package/driver/trigger-cap.mjs +18 -2
  132. package/driver/unit-inventory.mjs +8 -8
  133. package/driver/usage-ledger.mjs +5 -3
  134. package/driver/verify-knockout.mjs +7 -7
  135. package/driver/verify.mjs +5 -5
  136. package/driver/whatif-memo-run.mjs +1 -1
  137. package/driver/whatif-queue.mjs +3 -3
  138. package/driver/whatif-worker.mjs +2 -2
  139. package/examples/README.md +1 -1
  140. package/examples/grants.example.json +25 -24
  141. package/mcp-server/CHANGELOG.md +10 -0
  142. package/mcp-server/http-server.mjs +3 -3
  143. package/mcp-server/key-socket.mjs +1 -1
  144. package/mcp-server/lib/audit-view.mjs +3 -3
  145. package/mcp-server/lib/brief.mjs +3 -3
  146. package/mcp-server/lib/driver.mjs +1 -1
  147. package/mcp-server/lib/events.mjs +1 -1
  148. package/mcp-server/lib/http-handler.mjs +2 -2
  149. package/mcp-server/lib/instructions.mjs +2 -2
  150. package/mcp-server/lib/knockout.mjs +1 -1
  151. package/mcp-server/lib/ops.mjs +6 -3
  152. package/mcp-server/lib/options.mjs +15 -4
  153. package/mcp-server/lib/plan.mjs +7 -6
  154. package/mcp-server/lib/runs.mjs +10 -0
  155. package/mcp-server/lib/whatif.mjs +5 -5
  156. package/mcp-server/package.json +1 -1
  157. package/mcp-server/packs/README.md +1 -1
  158. package/mcp-server/remote/client-mcp-apikey.service +1 -1
  159. package/mcp-server/remote/client-mcp.service +2 -2
  160. package/mcp-server/remote/trademark-artifacts-http.service +1 -1
  161. package/mcp-server/server.mjs +38 -24
  162. package/package.json +2 -2
  163. package/portal-ui/dist/assets/{index-KFAHMgdT.js → index-CWTHP0sH.js} +3471 -1901
  164. package/portal-ui/dist/assets/{index-1ziUJX1E.css → index-KpytsmNH.css} +79 -26
  165. package/portal-ui/dist/index.html +2 -2
  166. package/portal-ui/package.json +1 -1
  167. package/providers/_shared/lane-probe.mjs +9 -3
  168. package/providers/jx-subclass/lookup.mjs +1 -1
  169. package/providers/oauth-mcp-bridge/CHANGELOG.md +4 -0
  170. package/providers/oauth-mcp-bridge/package.json +1 -1
  171. package/providers/oauth-mcp-bridge/systemd/courtlistener-mcp.service +1 -1
  172. package/scripts/citation-drift-report.mjs +1 -1
  173. package/scripts/citation-line-check.mjs +2 -2
  174. package/scripts/drive-env-check.mjs +1 -1
  175. package/scripts/e2e.mjs +5 -5
  176. package/scripts/env-audit.mjs +13 -1
  177. package/scripts/headless-page.mjs +5 -5
  178. package/scripts/live-surface-check.mjs +26 -2
  179. package/scripts/mint-names-in-force.mjs +19 -5
  180. package/scripts/mint-reference-strip-backlog.mjs +1 -1
  181. package/scripts/mint-suite-census.mjs +37 -10
  182. package/scripts/pack-publishable.mjs +1 -1
  183. package/scripts/preinstall-node-check.mjs +1 -1
  184. package/scripts/release-await-cut.mjs +3 -3
  185. package/scripts/release-cut-decision.mjs +1 -1
  186. package/scripts/release-dist-tag.mjs +1 -1
  187. package/scripts/release-install-check.mjs +1 -1
  188. package/scripts/release-notes-lint.mjs +1 -1
  189. package/scripts/release-publish-guard.mjs +1 -1
  190. package/scripts/release-version-pr-checks.mjs +2 -2
  191. package/scripts/release-version.mjs +61 -5
  192. package/scripts/render-brand-banner.mjs +1 -1
  193. package/scripts/render-check.mjs +2 -2
  194. package/scripts/repo-writes.mjs +1 -1
  195. package/scripts/report-frame-check.mjs +1 -1
  196. package/scripts/report-screenshot.mjs +2 -2
  197. package/scripts/retire-bare-refs.mjs +1 -1
  198. package/scripts/revisit-render-check.mjs +1 -1
  199. package/scripts/score.mjs +1 -1
  200. package/scripts/strip-titles-and-attributions.mjs +389 -0
  201. package/scripts/strip-tracker-citations.mjs +122 -5
  202. package/scripts/test-run.mjs +4 -4
  203. package/scripts/third-party-notices.mjs +1 -1
  204. package/scripts/verify-publishable.mjs +1 -1
  205. package/shared/access-audience.mjs +2 -2
  206. package/shared/anon-overlay.mjs +1 -1
  207. package/shared/brand.mjs +15 -1
  208. package/shared/bundle-freshness.mjs +1 -1
  209. package/shared/bundle-rebuild.mjs +1 -1
  210. package/shared/checkout-move.mjs +2 -2
  211. package/shared/client-door.mjs +8 -8
  212. package/shared/connect-clients.mjs +7 -7
  213. package/shared/connector-signin-probe.mjs +1 -1
  214. package/shared/env-aliases.mjs +1 -1
  215. package/shared/env-local.mjs +5 -5
  216. package/shared/grants-edit.mjs +76 -0
  217. package/shared/install-auth.mjs +1 -1
  218. package/shared/listen.mjs +3 -3
  219. package/shared/mcp-challenge.mjs +1 -1
  220. package/shared/names-in-force.mjs +2 -1
  221. package/shared/onboarding-store.mjs +19 -2
  222. package/shared/reference-guard-classes.mjs +44 -2
  223. package/shared/register-selection.mjs +1 -1
  224. package/shared/scope.mjs +223 -56
  225. package/shared/secret-file.mjs +1 -1
  226. package/shared/server-units.mjs +1 -1
  227. package/shared/staff-domain.mjs +45 -78
  228. package/shared/summary-blocks.mjs +2 -2
  229. package/shared/systemd-failure.mjs +3 -3
  230. package/shared/tracked-files.mjs +1 -1
  231. package/shared/trigger-lane.mjs +1 -1
  232. package/shared/tty-style.mjs +1 -1
  233. package/shared/usage-block.mjs +1 -1
  234. package/shared/vacuous-pass.mjs +1 -1
  235. package/shared/verb-shim.mjs +1 -1
@@ -2441,7 +2441,7 @@ export function knockoutFindingViews(mark, { manifest = null } = {}) {
2441
2441
  * Derived through knockoutFindingViews rather than re-deriving that fallback here — one projection
2442
2442
  * decides what a finding's number is, and this cell cannot disagree with the sheet it points into.
2443
2443
  */
2444
- // ── THE REVIEWER'S NOTES: WHAT EACH ONE IS ABOUT (tracker issues 331 A.4, 333 rule 5) ───────────────
2444
+ // ── THE REVIEWER'S NOTES: WHAT EACH ONE IS ABOUT (A.4, rule 5) ──────────────────────────────────────
2445
2445
  //
2446
2446
  // The knockout page prints a note about the REQUEST at the top, above the conflicts, and a note about
2447
2447
  // the NAME under that name's cards. A note saying the screen may have been scoped to the wrong market
@@ -2454,7 +2454,7 @@ export function knockoutFindingViews(mark, { manifest = null } = {}) {
2454
2454
  //
2455
2455
  // IT SORTS ON WHAT THE NOTE TALKS ABOUT, and the doctrine is written to match: a note about the request
2456
2456
  // NAMES the request. The word set spans both vocabularies deliberately — "dispatch" and "instructed"
2457
- // are what runs written before tracker issue 333 say, "the request" and "was asked" are what runs
2457
+ // are what earlier runs say, "the request" and "was asked" are what runs
2458
2458
  // written after it say — so one reader serves the archive and the new doctrine at once.
2459
2459
  export const REQUEST_NOTE_WORDS = /\b(?:dispatch|the request|the requester|instructed|was asked)\b/i;
2460
2460
 
@@ -277,7 +277,7 @@ export function postureDelta(snapshot, expected = PRODUCTION_POSTURE) {
277
277
  * hardcoded expectation. This answers how it differs from the box it is being read on, which is the
278
278
  * question a staff member actually has in front of the capability page.
279
279
  *
280
- * WHY AN AGE IS NOT THE GUARD IT LOOKS LIKE (tracker issue 170). The snapshot's only writers were a run
280
+ * WHY AN AGE IS NOT THE GUARD IT LOOKS LIKE. The snapshot's only writers were a run
281
281
  * and the launcher, so a deployment being CONFIGURED — which runs nothing by definition — holds a
282
282
  * capture that is stale for exactly as long as somebody is working on it, which is exactly when they
283
283
  * are looking at the page. Found live: the register provider was moved from signa to clarivate and every
@@ -526,7 +526,7 @@ export function isStale(snapshot, { now, maxAgeMs = 24 * 60 * 60 * 1000 }) {
526
526
  * the class of defect the snapshot exists to remove rather than to add to.
527
527
  *
528
528
  * Callable from the portal because the portal HAS the engine environment: one configuration per server
529
- * box, owner ruling 2026-08-26, `EnvironmentFile=%h/.env` on every unit. Its result is for COMPARISON
529
+ * box, ruling 2026-08-26, `EnvironmentFile=%h/.env` on every unit. Its result is for COMPARISON
530
530
  * only — see portal-config-view.mjs on why the page still renders the capture and never this.
531
531
  */
532
532
  export async function livePosture({ env = process.env } = {}) {
@@ -14,7 +14,7 @@
14
14
  // WHY THIS READS THE FORM AND NOT THE PROSE. The obligation used to read "placed or explicitly reasoned
15
15
  // away", and *reasoned away* is prose — unfalsifiable by machine. On the first of those runs 39 of the 45
16
16
  // missing marks ARE discussed somewhere in the recommendations and 0 of the 45 record ids are named, so
17
- // "discussed" and "reasoned away" cannot be told apart from the text. The owner ruling of 2026-08-20
17
+ // "discussed" and "reasoned away" cannot be told apart from the text. The ruling of 2026-08-20
18
18
  // settled it: the duty survives, narrowed to the must-check list, discharged by NAMING THE RECORD ID
19
19
  // with a one-line ground. That is a row on a form, and a row is checkable.
20
20
  //
@@ -65,7 +65,7 @@ export function armFloorDuty(runDir, { now = () => new Date().toISOString() } =
65
65
  ensureDriverDir(String(runDir ?? ""));
66
66
  writeFileSync(driverDir(String(runDir ?? ""), FLOOR_DUTY_STAMP), JSON.stringify({
67
67
  _provenance: "the placement pass on this run ran under code that orders the floors by record id "
68
- + "(#1117), so its floor account may be held to that order (tracker issue 1955)",
68
+ + "(#1117), so its floor account may be held to that order",
69
69
  ts: now(),
70
70
  }, null, 2) + "\n");
71
71
  return true;
@@ -436,7 +436,7 @@ export function effectiveInScope(hit, scopeJurisdictions = []) {
436
436
  // does a SEARCHED token mechanically cover a scoped jurisdiction j? (exact, or an EU search covering EU-scope).
437
437
  // A member scoped but only EU searched is NOT covered (national rights need a national search) → stays a gap.
438
438
  //
439
- // EXPORTED for the delivered-narrative coverage check (predelivery-lint.mjs, tracker issue 134), which asks
439
+ // EXPORTED for the delivered-narrative coverage check (predelivery-lint.mjs), which asks
440
440
  // the same question of a different claimant: not "did the plan cover the scope" but "does the prose claim
441
441
  // coverage of a territory the run did not search". One copy of the EU-reach rule, because two copies drifting
442
442
  // apart would be a worse defect than the disagreement this is being exported to catch. Callers pass
@@ -0,0 +1,143 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
3
+ //
4
+ // THE FRAMEWORK PRE-FLIGHT — read a deck and its manifest and say what they declare, before either
5
+ // rates a real matter.
6
+ //
7
+ // A customer's counsel writes a risk framework as two files: a prose deck they can argue with, and a
8
+ // small manifest that gives code the band words. Until now the only way to find out whether the pair
9
+ // worked was to point a company at it and run a matter. Three things went wrong in that order, and each
10
+ // of them looked like something else:
11
+ //
12
+ // the manifest is refused — the run fails by a token, hours in, naming a JSON key
13
+ // the deck and the manifest differ — nothing fails; the profile screen renders the framework's title
14
+ // and its coloured band pills and silently omits the box saying what
15
+ // the bands mean, so the page looks built and is missing half of it
16
+ // the deck resolves somewhere else — nothing fails; another framework rates the matter
17
+ //
18
+ // IT REPORTS AND DOES NOT GRADE. The pre-flight says what the framework declares — the ladder in its own
19
+ // order, the entity the deck names, the shape it is, which file answered — and where the two files
20
+ // disagree it says which band and what the deck did not do. It has no opinion on whether the rubric is a
21
+ // good one: a rubric is a legal document, and a check that scored one would be a content lint in a
22
+ // different costume. Nothing here reads what a band MEANS, only that the deck defines it.
23
+ //
24
+ // AGREEMENT IS THE RENDERER'S OWN READ, not a second one. `bandMeaningRows` in profile-service.mjs is the
25
+ // walk the profile screen makes; this calls it. A pre-flight with its own matcher would pass a deck the
26
+ // page then drops, which is the defect it exists to remove rather than a smaller version of it.
27
+
28
+ import { readFileSync } from "node:fs";
29
+ import { config } from "./driver.config.mjs";
30
+ import { loadFrameworkManifest, manifestPathFor, DEFAULT_FRAMEWORK } from "./framework.mjs";
31
+ import { bandMeaningRows } from "./profile-service.mjs";
32
+
33
+ /**
34
+ * WHY A DECK SERVED FROM THE REPO IS WORTH A SENTENCE, and why only sometimes.
35
+ *
36
+ * Skill resolution is layered: the config store wins, the repo answers when the store is silent. That
37
+ * fallback is the migration design and every generic methodology file relies on it. For a FRAMEWORK it is
38
+ * different in kind, because the shipped tree carries decks under the same filenames customers use for
39
+ * their own — `risk-framework.md`, `risk-framework-demo.md`, `risk-framework-triage.md`, which are the
40
+ * three `files[]` re-includes after excluding `risk-framework-*`. Take a customer's deck out of the
41
+ * config store and the repo's copy answers: readable, valid, and not theirs. Nothing throws and the
42
+ * matter is rated under another company's rubric.
43
+ *
44
+ * So: reported when an overlay is configured and did not hold the file. Silent on a single-tree install,
45
+ * where there is no other copy for it to have been.
46
+ */
47
+ export const SUBSTITUTION_SAY = (r) =>
48
+ `served from the shipped tree, not the configured store — the store has no ${r.rel}, so ${r.basePath} answered. `
49
+ + `A customer deck removed from the store is replaced by the shipped file of the same name rather than missed.`;
50
+
51
+ const layerSay = {
52
+ overlay: "the configured store",
53
+ base: "the shipped tree (the configured store does not hold it)",
54
+ "base-only": "the shipped tree",
55
+ missing: "nowhere — neither the configured store nor the shipped tree holds it",
56
+ };
57
+
58
+ /**
59
+ * Everything the pre-flight found about one framework, as data. `ok` is the whole verdict; `refusals`
60
+ * says why when it is false, in sentences written for the person who wrote the files.
61
+ *
62
+ * `resolve` and `read` are injected so the lint can drive a planted tree — the CLI passes neither and
63
+ * gets the engine's own resolution, which is the point of the check.
64
+ */
65
+ export function preflightFramework(fwPath, { resolve, read } = {}) {
66
+ const resolveOne = resolve ?? ((rel) => config.resolveSkillPathReport(rel));
67
+ const readOne = read ?? ((p) => readFileSync(p, "utf8"));
68
+ const manifestRel = manifestPathFor(fwPath);
69
+ const out = {
70
+ fwPath, manifestRel, ok: false, refusals: [], substitutions: [],
71
+ deck: null, manifest: null, declares: null, bands: null,
72
+ };
73
+
74
+ for (const [name, rel] of [["deck", fwPath], ["manifest", manifestRel]]) {
75
+ let r;
76
+ try { r = resolveOne(rel); }
77
+ catch (e) { out.refusals.push({ code: "skills_overlay_unreadable", say: String(e?.message ?? e) }); return out; }
78
+ out[name] = { rel, path: r.path, layer: r.layer, from: layerSay[r.layer] ?? r.layer };
79
+ if (r.layer === "base") out.substitutions.push({ ...r, say: SUBSTITUTION_SAY(r) });
80
+ }
81
+
82
+ let manifest;
83
+ try { manifest = loadFrameworkManifest((rel) => resolveOne(rel).path, fwPath); }
84
+ catch (e) {
85
+ out.refusals.push({ code: "manifest", say: String(e?.message ?? e) });
86
+ return out;
87
+ }
88
+ out.declares = {
89
+ key: manifest.framework_key, title: manifest.title, entity: manifest.entity_label,
90
+ sourceDeck: manifest.source_deck, shape: manifest.structure.kind,
91
+ ladder: manifest.bands.map((b) => ({ label: b.label, tone: b.tone })),
92
+ };
93
+
94
+ let deckText;
95
+ try { deckText = readOne(out.deck.path); }
96
+ catch (e) {
97
+ out.refusals.push({ code: "deck_unreadable", say: `the deck could not be read at ${out.deck.path}: ${String(e?.message ?? e)}` });
98
+ return out;
99
+ }
100
+
101
+ // The entity label has to appear in the prose, because the deck speaks about the company by name and a
102
+ // manifest naming a company the deck never mentions is a pair from two different frameworks.
103
+ if (!deckText.toLowerCase().includes(String(manifest.entity_label).toLowerCase()))
104
+ out.refusals.push({ code: "entity_absent",
105
+ say: `the manifest names "${manifest.entity_label}" as the company, and the deck prose never uses that name.` });
106
+
107
+ out.bands = bandMeaningRows(deckText, manifest) ?? [];
108
+ const missed = out.bands.filter((b) => b.miss);
109
+ if (missed.length)
110
+ out.refusals.push({ code: "bands_disagree",
111
+ say: `${missed.length} of ${out.bands.length} bands are named in the manifest and not defined in the deck. `
112
+ + "The profile screen shows what the bands mean only when EVERY band is defined, so one miss empties the whole box.",
113
+ bands: missed.map((b) => ({ band: b.band, miss: b.miss })) });
114
+
115
+ out.ok = out.refusals.length === 0;
116
+ return out;
117
+ }
118
+
119
+ /** The report as lines for a reader. No colour, no verdict word the caller has not earned. */
120
+ export function formatPreflight(r) {
121
+ const L = [];
122
+ L.push(`Framework: ${r.fwPath}${r.fwPath === DEFAULT_FRAMEWORK ? " (this installation's default)" : ""}`);
123
+ if (r.deck) L.push(` deck ${r.deck.path}`, ` read from ${r.deck.from}`);
124
+ if (r.manifest) L.push(` manifest ${r.manifest.path}`, ` read from ${r.manifest.from}`);
125
+ if (r.declares) {
126
+ const d = r.declares;
127
+ L.push("", `It declares itself "${d.title}" (${d.key}), a ${d.shape}-shaped framework rating ${d.entity}.`,
128
+ `Transcribed from: ${d.sourceDeck}`, "", "The ladder, highest risk first:");
129
+ for (const [i, b] of d.ladder.entries()) L.push(` ${i + 1}. ${b.label.padEnd(16)} ${b.tone}`);
130
+ }
131
+ if (r.bands?.length) {
132
+ L.push("", "What the deck defines:");
133
+ for (const b of r.bands) L.push(b.miss ? ` ✗ ${b.band.padEnd(16)} ${b.miss}` : ` ✓ ${b.band.padEnd(16)} ${b.meaning}`);
134
+ }
135
+ for (const s of r.substitutions) L.push("", `WARNING ${s.rel}: ${s.say}`);
136
+ if (r.refusals.length) {
137
+ L.push("", "Not ready:");
138
+ for (const f of r.refusals) L.push(` ${f.say}`);
139
+ } else {
140
+ L.push("", "The deck and the manifest agree. Nothing was created and nothing was rated.");
141
+ }
142
+ return L.join("\n");
143
+ }
@@ -65,6 +65,13 @@ export const TOOL_WRITTEN_ARTIFACTS = new Map([
65
65
  refusals: (runDir) => frameRefusalsFor(runDir) }],
66
66
  ["knockout-frame.md", { tool: "record_knockout_frame", what: "the batch scope note",
67
67
  refusals: (runDir) => frameRefusalsFor(runDir) }],
68
+ // The knockout lane's reviewing pass. ONE EXACT BASENAME, and it sits under `_driver/` rather than the
69
+ // run root because the artifact is the pass's account of what it rewrote, not a surface a client is
70
+ // shown — the lookup keys on the basename either way. The row is what makes a repair on this stage a
71
+ // CALL: the seat's grant carries no Write, so a ladder handing it the write form would order a
72
+ // hand-write it cannot perform, on a stage whose failure is deliberately quiet.
73
+ ["knockout-review.json", { tool: "record_knockout_review", what: "your rewrites of the lines a reader sees first",
74
+ refusals: (runDir) => knockoutReviewRefusalsFor(runDir) }],
68
75
  [BLIND_FRAME_MODEL_FILE, { tool: "record_blind_frame", what: "the threat model" }],
69
76
  [SKEPTIC_FLAGS_FILE, { tool: "record_skeptic", what: "your flags and escalation decisions" }],
70
77
  ["senior-eye-review.md", { tool: "record_narrative_refutation", what: "your verdict and typed flags" }],
@@ -227,6 +234,7 @@ import { KNOCKOUT_ASSESS_CHUNK_RE, knockoutAssessChunkFile } from "./knockout-as
227
234
  // The frame transport's refusal reader, so a missing plan or note can be told apart from a stage that
228
235
  // never called its tool — the distinction, which its sibling stage still lacks.
229
236
  import { frameRefusalsFor } from "./knockout-frame-record.mjs";
237
+ import { knockoutReviewRefusalsFor } from "./knockout-review-record.mjs";
230
238
  import { profilesStoreDir } from "./profiles.mjs";
231
239
  // The supplemental lane's single source (stages.mjs). The import is acyclic — stages.mjs reads
232
240
  // verify/coverage-ledger/config/framework and never reaches back here.
@@ -577,7 +585,7 @@ export function selectEngine() {
577
585
  // name: an enrolled-token veto is an enumerated check, and a fifth token forgetting to enrol is the
578
586
  // move-blindness this repo has counted seven instances of.
579
587
  //
580
- // What died here with the form path (2026-08-17 owner ruling, delete-not-gate): reading the seat's file
588
+ // What died here with the form path (2026-08-17 ruling, delete-not-gate): reading the seat's file
581
589
  // as a SUBMISSION, preserving it, refereeing whose bytes it held (the isDriverOwnBytes machinery) and
582
590
  // rewriting the seat-facing copy. The seat writes no file, so there is no submission to read, no bytes
583
591
  // to referee, and no seat-facing copy to keep in step. The capture duty moved to the receiver
@@ -178,7 +178,7 @@ export function slimLine(rec, reading = null) {
178
178
  return line;
179
179
  }
180
180
 
181
- // ── CROWDS RIDE THE LIST AS A SIBLING ARRAY (Option A, ruled on tracker issue 95, 2026-08-31) ──────
181
+ // ── CROWDS RIDE THE LIST AS A SIBLING ARRAY (Option A, ruled 2026-08-31) ──────────────────────────
182
182
  //
183
183
  // WHY THEY HAVE TO RIDE AT ALL. The locked body's acceptance 4 is that no stage reads the fat band or
184
184
  // an unpicked record. `band_lookup` answers from the list — but a lookup into a zone that was CROWDED
@@ -256,7 +256,7 @@ export { romanizationRefusal, romanizationSpellings };
256
256
  // The jx model calls (fold completions, serp-judge, nativeread) are the ONLY dispatches in the driver
257
257
  // that produce a per-token provider invoice. This comment used to name their executor as a
258
258
  // `@anthropic-ai/sdk` Messages call; that has not been true since the lane lost its own destination
259
- // and credential, and the SDK is no longer a dependency of this product at all (tracker issue 99).
259
+ // and credential, and the SDK is no longer a dependency of this product at all.
260
260
  // The lane goes wherever the run's resolved program goes — the CLI opens every connection, and no
261
261
  // driver code calls a provider — so one run never mixes a subscription login with an API key. What
262
262
  // survives unchanged is the BILLING distinction this block is about: these calls land on a per-token
package/driver/jx.mjs CHANGED
@@ -164,7 +164,7 @@ export function deriveJxSliceStatement({ sidecar, units = null, env = process.en
164
164
  // A reader concludes the deep lane ran. It does not follow: a profile set to `full` on an unarmed
165
165
  // deployment and one set to `candidates` on an armed deployment execute identically.
166
166
  //
167
- // OWNER RULING (2026-08-17): flag, do not gate. "If we can't run deep dive on serpAPI we need to flag
167
+ // RULING (2026-08-17): flag, do not gate. "If we can't run deep dive on serpAPI we need to flag
168
168
  // it." So `depth` is not deleted and it does not start gating anything — the run derives what the lane
169
169
  // ACTUALLY got from the same durable record `executes` already comes from, and says so beside the ask.
170
170
  // Gating would change what a run executes and therefore what it bills; a flag changes only what the
@@ -164,7 +164,7 @@ const DECLARED = Object.freeze({
164
164
 
165
165
  /**
166
166
  * Refuse an undeclared key by path. THE SHARED IMPLEMENTATION (preserve-merge.mjs), which six transports
167
- * now use — reused rather than rebuilt, per this issue's own owner ruling.
167
+ * now use — reused rather than rebuilt, per this issue's own ruling.
168
168
  *
169
169
  * THE HAND-ROLLED FIRST CUT OF THIS WAS A PRODUCT-FAILURE RISK, and the shared module is why it did not
170
170
  * ship. Mine refused undeclared keys at the TOP LEVEL as well as inside typed sub-objects. The shared one