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
@@ -44,7 +44,7 @@ import {
44
44
  // implementation; a second copy here would be a second thing to keep true.
45
45
  decideRecovery, failureSignature, classifyFailureReason, countRecoveryLanes, weatherCeilingFor,
46
46
  } from "./repairs.mjs";
47
- import { RunCancelled } from "./cancel.mjs"; // stop-by-user: never the failure lane below
47
+ import { RunCancelled, assertNotCancelledBeforePublish } from "./cancel.mjs"; // stop-by-user: never the failure lane below
48
48
  import { loadFrameworkManifest, parseFrameworkManifest, frameworkFor, DEFAULT_FRAMEWORK } from "./framework.mjs";
49
49
  import { KO_STAGES, KO_STEPS, KO_STEP_REGISTER_COUNT, koSteps, koPaths, kebab, knockoutPrompt, koChunks } from "./stages-knockout.mjs";
50
50
  import { kebabCollisions, reportIdentityFor, CAPABILITY_SKIPPED_CAUSE, CAPABILITY_SKIPPED_NOTE } from "./search-policy.mjs";
@@ -55,10 +55,12 @@ import { capabilitiesFor } from "./register-capabilities.mjs";
55
55
  import { registerUnavailableOffices } from "./register-unreachable.mjs";
56
56
  import { runRecordLogPath } from "../providers/_shared/ledger-path.mjs"; // — this run's record log
57
57
  import { validators as koValidators, validateMergedFindings, worstBand, registerSurfacedFilings, raterCaveats, SURVIVOR_BOUNDARY_RE } from "./verify-knockout.mjs";
58
+ import { reviewAbout, reviewEvidence, reviewEvidenceLines, applyKnockoutReview, knockoutReviewFile } from "./knockout-review-record.mjs";
58
59
  import { publishKnockout, composeKnockoutEmail } from "./publish/knockout.mjs";
59
60
  import { writeRunStatus, rollupStatus, atomicWrite, identitySeed } from "./progress.mjs"; // — the identity seed is shared; the stepper is not
60
61
  import { batchMarkName } from "./mark-name.mjs";
61
62
  import { runLog, note, outputMeta } from "./log.mjs";
63
+ import { defaultTerritoryState } from "./effective-scope.mjs"; // the stored-defaults reading — one producer, shared with the clearance lane
62
64
  import { AGENT_WHATSAPP, whatsappRouting } from "./stages.mjs";
63
65
  import { writeOutboxPacket } from "./outbox.mjs";
64
66
  import { rollupTokens, stampTokenRollup } from "./tokens.mjs";
@@ -66,7 +68,7 @@ import { recordRunConsumption } from "./consumption-ledger.mjs";
66
68
  import { writeSettleStamp } from "./settle-stamp.mjs"; // — the pool copy's own terminal state
67
69
  import { stopReason } from "../shared/stop-reason.mjs"; //
68
70
  import { envFrom } from "../shared/env-aliases.mjs"; // — resolves EITHER spelling; names the retired one because that is the live-writable half
69
- // The scoped owner lookup a promoted register filing is owed (tracker issue 276). Bounded, deduplicated
71
+ // The scoped owner lookup a promoted register filing is owed. Bounded, deduplicated
70
72
  // per owner, and structurally unable to withhold a report.
71
73
  import { ownersOwedACheck, runOwnerChecks } from "./owner-use-check.mjs";
72
74
 
@@ -164,6 +166,62 @@ export function readBackLadder(ctx, sidecarPath, { minted }) { // exported for
164
166
 
165
167
  // ── koStage: the slim lane's stage runner — skip-if-output-valid (the stageOnce pattern), one
166
168
  // runStage call, StageFailure on exhaustion (rate_limited rides to pipeline()'s postpone catch).
169
+ /**
170
+ * Rewrite the lines a reader meets first, and NEVER cost a client a report doing it.
171
+ *
172
+ * Every failure path returns the record it was handed. A stage that fails, times out, returns nothing,
173
+ * returns something that will not parse, or produces a rewrite the merged gate then refuses, all end the
174
+ * same way: the rated record ships, unrewritten, exactly as it shipped before this stage existed. This
175
+ * pass is presentation. A refusal is never a pass, and "no report" is a product failure — so the last
176
+ * thing added to the delivery path is the last thing that should be able to stop it.
177
+ *
178
+ * THE EMPTY CASE IS NOT A FAILURE AND IS NOT SILENT. A batch whose default-visible lines are all already
179
+ * plain has nothing to dispatch, and paying for a turn to be told so is waste. It is logged with the
180
+ * count, so "nothing was flagged" and "the pass never ran" are different rows in the journal rather than
181
+ * one absent one.
182
+ */
183
+ async function knockoutReviewingPass({ ctx, run, K, merged, plan }) {
184
+ const about = reviewAbout(merged, plan);
185
+ const evidence = reviewEvidence(merged, about);
186
+ if (!evidence.rows.length) {
187
+ runLog(run.runDir, { event: "knockout-review", outcome: "nothing-flagged", flagged: 0,
188
+ marks: merged.marks.length, excludedMarks: about.marks.length, excludedOwners: about.owners.length });
189
+ return merged;
190
+ }
191
+
192
+ const keep = (outcome, detail) => {
193
+ runLog(run.runDir, { event: "knockout-review", outcome, flagged: evidence.rows.length, applied: 0,
194
+ delivered: "the rated record, unrewritten", ...detail });
195
+ note(`reviewing pass: ${outcome} — the rated record ships unrewritten`);
196
+ return merged;
197
+ };
198
+
199
+ try {
200
+ await koStage("knockout-review", ctx, {
201
+ msgCtx: { evidenceLines: reviewEvidenceLines(evidence), exclusionNote: evidence.exclusionNote },
202
+ });
203
+ } catch (e) {
204
+ return keep("stage-failed", { reason: String(e?.message ?? e).slice(0, 200) });
205
+ }
206
+
207
+ let review;
208
+ try { review = JSON.parse(readFileSync(knockoutReviewFile(run.runDir), "utf8")); }
209
+ catch (e) { return keep("artifact-unreadable", { reason: String(e?.message ?? e).slice(0, 200) }); }
210
+
211
+ const { doc, receipt } = applyKnockoutReview(merged, review);
212
+ // THE REWRITTEN RECORD GOES THROUGH THE SAME GATE. Not a lighter one and not none: this is the record
213
+ // a client receives, and the only reason it is trusted is that it passed what the rated one passed.
214
+ const mv = validateMergedFindings(run.runDir, doc, plan);
215
+ if (!mv.ok) return keep("rewrite-refused-by-the-merged-gate", { failures: mv.failures.slice(0, 5) });
216
+
217
+ atomicWrite(K.findings, JSON.stringify(doc, null, 2) + "\n");
218
+ runLog(run.runDir, { event: "knockout-review", outcome: "applied", flagged: evidence.rows.length,
219
+ applied: receipt.applied, declined: receipt.declined,
220
+ unresolved: receipt.unresolved.length, refused: receipt.refused.length,
221
+ excludedMarks: about.marks.length, excludedOwners: about.owners.length });
222
+ return doc;
223
+ }
224
+
167
225
  async function koStage(name, ctx, { chunkNo = null, msgCtx = {} } = {}) {
168
226
  // — the stage's own start, captured before ANY work: before the existsSync, before the validator
169
227
  // read, before the message composition. states why on the spine — capture it after the await and
@@ -478,8 +536,8 @@ export async function knockoutInner(ctx, job, opts = {}) {
478
536
  //
479
537
  // `failClass: "deterministic"` says the same thing to the OTHER reader: repairs.mjs's ladder. It
480
538
  // was stamped when this lane had no ladder for it to reach — "so the fact travels with the throw
481
- // rather than being re-guessed from prose by whichever catch the throw ends up in." As of tracker
482
- // issue 1889 that catch is this lane's own, the stamp is LIVE, and it is what buys this refusal
539
+ // rather than being re-guessed from prose by whichever catch the throw ends up in." That catch is
540
+ // now this lane's own, the stamp is LIVE, and it is what buys this refusal
483
541
  // zero parks without the ladder having to read a word of the prose.
484
542
  if (refusal) throw new StageFailure("knockout-register-count", refusal, null,
485
543
  { refusal: true, failClass: "deterministic" });
@@ -521,6 +579,18 @@ export async function knockoutInner(ctx, job, opts = {}) {
521
579
  marksDetailed: markRows.map((m) => ({ name: String(m.name), ...(Array.isArray(m.classes) && m.classes.length ? { classes: m.classes } : {}), ...(m.ref ? { ref: m.ref } : {}) })),
522
580
  }, null, 2) + "\n");
523
581
  } catch (e) { note(`instructed-scope write failed (non-fatal): ${e.message}`); }
582
+ // The stored defaults the engine cannot search — the clearance lane's record, on this lane too. A
583
+ // knockout reads the same account profile and dropped the same entries in the same silence.
584
+ try {
585
+ const dts = defaultTerritoryState(ctx.profile);
586
+ // two records of one fact, so each has its own try: a fault in one must not take the other
587
+ try { if (dts.unrecognized.length) runLog(run.runDir, { event: "default-territory-unrecognized", count: dts.unrecognized.length, entries: dts.unrecognized, lane: "knockout" }); } catch (e) { note(`default-territory-unrecognized log failed (non-fatal): ${e.message}`); }
588
+ writeFileSync(K.defaultTerritories, JSON.stringify({
589
+ profileKey: ctx.profile?.profileKey ?? null,
590
+ searchable: dts.kept,
591
+ unrecognized: dts.unrecognized,
592
+ }, null, 2) + "\n");
593
+ } catch (e) { note(`default-territories write failed (non-fatal): ${e.message}`); }
524
594
 
525
595
  // status seed — knockout's OWN step flow (never seedRunStatus's clearance stepper). Depth 2
526
596
  // walks one more step than a plain knockout, so the list is resolved once and frozen on ctx.
@@ -658,7 +728,7 @@ export async function knockoutInner(ctx, job, opts = {}) {
658
728
  runLog(run.runDir, { event: "knockout-register-records", provider: REGISTER_PROVIDER, executor: recExec.source,
659
729
  marks: recDoc.marks.length, listed: listedMarks(recDoc), records: recDoc.marks.reduce((n, m) => n + m.records.length, 0) });
660
730
 
661
- // ── THE OWNER LOOKUP, HERE BECAUSE HERE IS WHERE THE OWNER BECOMES KNOWN (tracker issue 276) ──
731
+ // ── THE OWNER LOOKUP, HERE BECAUSE HERE IS WHERE THE OWNER BECOMES KNOWN ──────────────────────
662
732
  //
663
733
  // On the run that produced the issue, the owner's name was on disk 48 seconds before the sweep
664
734
  // started and no pass ever searched it: every sweep keys on the TERM, and nothing re-swept on an
@@ -667,7 +737,7 @@ export async function knockoutInner(ctx, job, opts = {}) {
667
737
  //
668
738
  // Bounded to promoted filings and deduplicated per owner — on the issue's own run that is ONE
669
739
  // extra query. Never throws: a failure produces rows saying the lookup did not answer, and the
670
- // run publishes (owner ruling A, 2026-09-07).
740
+ // run publishes (ruling A, 2026-09-07).
671
741
  try {
672
742
  const owed = ownersOwedACheck(recDoc);
673
743
  if (owed.length) {
@@ -811,7 +881,9 @@ export async function knockoutInner(ctx, job, opts = {}) {
811
881
  for (let c = 0; c < chunks.length; c++) {
812
882
  await koStage("knockout-assess", ctx, { chunkNo: c, msgCtx: { chunkMarks: chunks[c], chunkTotal: chunks.length, framework: ctx.framework } });
813
883
  }
814
- const merged = { schema_version: 1, framework: null, batch: null, marks: [] };
884
+ // `let`, because the reviewing pass below returns a REWRITTEN record rather than editing this one:
885
+ // the original has to survive intact as the thing that ships if the pass cannot finish cleanly.
886
+ let merged = { schema_version: 1, framework: null, batch: null, marks: [] };
815
887
  const summaries = [];
816
888
  for (let c = 0; c < chunks.length; c++) {
817
889
  // READ prefers the new location and falls back to the pre-relocation one, so a run whose chunks
@@ -896,9 +968,34 @@ export async function knockoutInner(ctx, job, opts = {}) {
896
968
  if (!mv.ok) throw new StageFailure("knockout-assess", `merged findings failed the lint: ${mv.failures.join("; ")}`, null);
897
969
  runLog(run.runDir, { event: "knockout-receipts", ...mv.receipts });
898
970
  atomicWrite(K.findings, JSON.stringify(merged, null, 2) + "\n");
971
+ // ── THE REVIEWING PASS, AFTER THE RECORD IS ON DISK AND BEFORE ANYTHING READS IT ─────────────────
972
+ //
973
+ // ORDER MATTERS THREE WAYS. The record is written FIRST so the stage's validator has something to
974
+ // join every address against — an address that names no line is refused while the seat can still
975
+ // send a repair turn, rather than reported as unresolved after the stage was declared good. It also
976
+ // means a run that dies inside the pass has already left a complete, valid, deliverable record: the
977
+ // rated one, unrewritten, which is exactly what shipped before this stage existed. And the rewritten
978
+ // record goes back through `validateMergedFindings` before it replaces that one, so nothing reaches
979
+ // a client that has not been through the same gate.
980
+ //
981
+ // NO NEW STEP LABEL. `stepTotal` is derived from `koSteps()`, so adding one would renumber the
982
+ // progress screen a client watches for a pass that takes a minute. It runs inside "Knockout
983
+ // assessment", which is what it is part of.
984
+ merged = await knockoutReviewingPass({ ctx, run, K, merged, plan });
899
985
  try { writeFileSync(K.assessment, String(merged.batch.executiveSummary ?? "")); } catch { /* prose mirror, best-effort */ }
900
986
 
901
987
  // 4 — publish (report + workbook + meta + index)
988
+ //
989
+ // THE LAST READ OF THE STOP FLAG. Everything above dispatches through the gateway, which reads it
990
+ // before a turn — so on this lane, where publication follows the final stage directly, a stop
991
+ // pressed during that stage was never seen and the report went out anyway.
992
+ assertNotCancelledBeforePublish(run.runDir, "knockout");
993
+ // PAST THE LAST STOPPABLE POINT, RECORDED WHERE IT BECOMES TRUE. The read above is the final one, so
994
+ // from this line on a stop cannot prevent delivery — and the screen must stop promising it will.
995
+ // Written here rather than derived on the screen from a step number: "Report & publish" is step 4 of
996
+ // 5 on this lane, not the last, so any arithmetic over stepN would name the wrong step and go stale
997
+ // the day a lane's list changes.
998
+ writeRunStatus(ctx, { stoppable: false });
902
999
  koStep(ctx, "Report & publish");
903
1000
  const overall = worstBand(ctx.framework, merged.marks);
904
1001
  const published = await publishKnockout({
@@ -934,7 +1031,7 @@ export async function knockoutInner(ctx, job, opts = {}) {
934
1031
  conversationId: job.conversationId ?? null,
935
1032
  subject: `Knockout trademark review — ${job.ref ?? markNames[0] ?? "batch"} (${nMarks} mark${nMarks === 1 ? "" : "s"})`,
936
1033
  emailBodyHtml: emailHtml,
937
- // THE SAME ROUTING AS THE CLEARANCE PACKET (tracker issue 289 part b). This read
1034
+ // THE SAME ROUTING AS THE CLEARANCE PACKET (part b). This read
938
1035
  // `AGENT_WHATSAPP[agent]`, and every user of a deployment shares one agent id, so every knockout
939
1036
  // completion paged the operator and told the person who ordered it nothing. The clearance packet
940
1037
  // was moved off that and this one was not, which is the half that shipped: one call site fixed,
@@ -81,7 +81,7 @@ import { profileOrdinals } from "./profile-selection.mjs"; // lever 3 — driv
81
81
  // CLIENT REPORT through the script-scope coverage row, which is why it is imported rather than typed:
82
82
  // the row used to name "Depth 5 (prelim-jx)" — a retired key on a retired ladder — as its remedy.
83
83
  import { NATIVE_LANGUAGE_REMEDY } from "./products.mjs";
84
- import { resolveTerritories } from "./effective-scope.mjs"; // the ONE territory ladder (the geography stamp included)
84
+ import { resolveTerritories, defaultTerritoryState } from "./effective-scope.mjs"; // the ONE territory ladder (the geography stamp included) + the stored-defaults reading
85
85
  import { acquireSlot, releaseSlot } from "./slot-lock.mjs";
86
86
  import { mintSupplementalEntries, withRejected } from "./engine/mcp/supplemental.mjs";
87
87
  import { runLog, note, fileMeta, outputMeta, stageLog } from "./log.mjs"; import { armProduced, readArmSurfaces, producedNothingLine } from "./experiment-honesty.mjs"; import { correctiveReadiness, correctiveRefusalLine, correctivePassState } from "./corrective-arm.mjs";
@@ -98,7 +98,7 @@ import { buildAuditMd, parseSpineFindingBlocks } from "./publish/audit-from-spin
98
98
  import { deriveRegisterPresence } from "./publish/register-presence.mjs"; // — the audit stores every live in-scope record
99
99
  import { lastAcceptedMatterFrame } from "./matter-frame-record.mjs"; // — the frame's inferred scope, when nothing was instructed
100
100
  import { romanizedTermsFromPlan, mintSupplementalQid } from "./register-plan.mjs"; // — the stamp the late lanes never met
101
- import { slimLine, crowdLine } from "./hit-list.mjs"; // — the list the run works from; crowds ride it as a sibling array (tracker issue 95)
101
+ import { slimLine, crowdLine } from "./hit-list.mjs"; // — the list the run works from; crowds ride it as a sibling array
102
102
  import { mintCrossCheckDoubts, mintContradictionDoubts, stitchDoubts, applyClosure } from "./doubt-ledger.mjs"; // doubt-stitch + doubt-closure (2026-07-22)
103
103
  // Conversion 6: the two line-form parsers are no longer on the live path — the seat sends typed
104
104
  // rows and the driver applies THOSE. `parseClosureLines`/`parseAskClosureLines` are still exported and
@@ -178,7 +178,7 @@ import { caseLawInventory } from "./config-inventory.mjs"; // — the deployme
178
178
  import { caseLawSourceRows } from "./case-law-sources.mjs"; // one author for the shape the stage is handed
179
179
  import { writeSettleStamp } from "./settle-stamp.mjs"; // — the pool copy's own terminal state
180
180
  import { isEntrypoint } from "../shared/is-entrypoint.mjs"; // — one entry-point test, all spellings
181
- import { RunCancelled, isCancelled } from "./cancel.mjs"; // stop-by-user: a distinct class, never classified as a failure; isCancelled is the resume door's own refusal (2155)
181
+ import { RunCancelled, isCancelled, assertNotCancelledBeforePublish } from "./cancel.mjs"; // stop-by-user: a distinct class, never classified as a failure; isCancelled is the resume door's own refusal (2155)
182
182
  import { BRAND } from "../shared/brand.mjs"; // — the operator name a client is told to expect, from the tenant seam
183
183
  import { CLIENT_ACTION } from "../shared/client-failure-note.mjs"; // — the action, worded once; this surface names the mark itself
184
184
  import { stopReason } from "../shared/stop-reason.mjs"; // — one builder, four cancel sites
@@ -699,7 +699,7 @@ function deriveHitList(ctx, band) {
699
699
  // as a fallback so a provider that promotes the field is not silently ignored.
700
700
  const lines = (band?.enumerated ?? []).map((r) =>
701
701
  slimLine(r, r?.screen?.mark_transliteration ?? r?.mark_transliteration ?? null));
702
- // ── CROWDS RIDE BESIDE THE LINES (Option A, ruled on tracker issue 95) ────────────────────────
702
+ // ── CROWDS RIDE BESIDE THE LINES (Option A, as ruled) ─────────────────────────────────────────
703
703
  //
704
704
  // A crowd is a zone that could NOT be enumerated. Without it on the list, `band_lookup` answering
705
705
  // from the list returns nothing for a crowded zone — and nothing is indistinguishable from
@@ -1568,6 +1568,10 @@ function attachProfile(ctx, job, { write = true } = {}) {
1568
1568
  // job-derived field in that subset would make it unrecomputable. The swept set is visible in the frozen
1569
1569
  // platforms; where the extras came from is run telemetry, logged below.
1570
1570
  ctx.profile.profileSha = profileShaOf(ctx.profile); // T9 (K3)
1571
+ // WHICH ORGANISATION'S GENERIC this run is filed under — the door stamps it on a Generic job, from the
1572
+ // verified principal. Set AFTER the hash for the reason the provenance note above gives: it is job-
1573
+ // derived, and `profileSha` must stay recomputable from profiles/<key>.json. Absent on a company's run.
1574
+ if (typeof job?.tenant === "string" && job.tenant && ctx.profile.profileKey === "generic") ctx.profile.organisation = job.tenant;
1571
1575
  const tmp = `${sidecarPath}.tmp`;
1572
1576
  writeFileSync(tmp, JSON.stringify(ctx.profile, null, 2) + "\n");
1573
1577
  renameSync(tmp, sidecarPath);
@@ -2633,7 +2637,7 @@ const safeReadText = (p) => { try { return readFileSync(p, "utf8"); } catch { re
2633
2637
  // a named reason, never a silent skip.
2634
2638
  // ── — THE FLOOR DUTY, checked ────────────────────────────────────────────────────────────────
2635
2639
  //
2636
- // The floors are placement-inquiry's one uncapped obligation and nothing verified them. The owner ruling
2640
+ // The floors are placement-inquiry's one uncapped obligation and nothing verified them. The ruling
2637
2641
  // of 2026-08-20 made the duty machine-checkable — placed, or named by record id with a one-line ground —
2638
2642
  // and this is the check that ruling ordered. The dictation that tells the seat to name the ids ships in
2639
2643
  // the same commit, so a run predating it reports its WHOLE floor unmet, correctly.
@@ -4653,7 +4657,7 @@ export function buildFailurePacket({ runId, agent, job = {}, failedStage, shortR
4653
4657
  // verbatim reason below this line names which.
4654
4658
  [REFUSAL_TERMINAL_KIND]: "Nothing failed. This deployment does not serve the search that was ordered, and said so before any work was done — the reason below names what is missing and what to change. Re-running it unchanged returns the same answer.",
4655
4659
  };
4656
- // ── A CAP DEATH IS A SUBSCRIPTION OUTCOME, NOT AN OUTAGE (tracker issue 103, the fourth ask) ──────
4660
+ // ── A CAP DEATH IS A SUBSCRIPTION OUTCOME, NOT AN OUTAGE (the fourth ask) ─────────────────────────
4657
4661
  //
4658
4662
  // The weather lane carries BOTH an overloaded provider and a usage cap, and its one sentence
4659
4663
  // described the first: "stayed overloaded or unreachable … Re-trigger once the provider is healthy."
@@ -5885,7 +5889,7 @@ async function enforceCorrectionsReachFindings(ctx, P, pre, resume) {
5885
5889
  }
5886
5890
  }
5887
5891
 
5888
- // (T3a, owner ruling 2026-08-26 — "Deliver always, with open points printed. The refusal on a blocking
5892
+ // (T3a, ruling 2026-08-26 — "Deliver always, with open points printed. The refusal on a blocking
5889
5893
  // review goes." That REVERSES T3, which had retired "delivered-with-open-questions" and deleted
5890
5894
  // the extra this comment used to be a tombstone for. Both decisions are the owner's; this is the standing
5891
5895
  // one. The open points are no longer a dispatch extra a seat writes up: `buildReviewerOpenPointsSection`
@@ -6034,13 +6038,20 @@ function refuteRegistryCheckExtra(ctx) {
6034
6038
  * the one report where it matters most — the same defect one level in that turned "AXIS Bank filed in
6035
6039
  * class 36" into "group Bank filed in class 36" on a report clearing AXIS.
6036
6040
  *
6037
- * WHY THE FIELDS ARE THE RECORD'S OWN KEYS AND NOT `DEFAULT_VISIBLE_FIELDS.clearance`. That list's
6038
- * clearance half names five fields nothing in this tree reads — `oneLiner`, `freedomToOperate`,
6039
- * `thirdPartyRights`, `ownRights` and `batchOpener`, in either casing. Iterating it would open nothing
6040
- * and report a clean result over text it never read, which is the absence-as-pass this block exists to
6041
- * avoid. The keys below are the ones the record actually carries, and they are the surfaces the
6042
- * READER-OWNED NOUNS directive already names as reaching the client. Reconciling that list is its own
6043
- * change because it is shared with the knockout half, which IS real.
6041
+ * WHY THE FIELDS ARE THE RECORD'S OWN KEYS, AND WHAT THAT USED TO SAY. This block could not use
6042
+ * `DEFAULT_VISIBLE_FIELDS.clearance`, because that list named fields no record carried — iterating it
6043
+ * would have opened nothing and reported a clean result over text it never read, which is the
6044
+ * absence-as-pass this block exists to avoid.
6045
+ *
6046
+ * That list is repaired now: every entry is a path that resolves against a delivered record, and an arm
6047
+ * resolves each one. The keys below are still written out rather than read from it, because this block
6048
+ * needs the `where` label it puts in front of each hit and the list carries paths and not labels. The
6049
+ * two agree on the surfaces, and the arm that resolves the list is what keeps that true.
6050
+ *
6051
+ * The old wording here also stated the defect slightly wrong, and said so about the field it was most
6052
+ * likely to mislead on: `thirdPartyRights` was called dead "in either casing" when the record carries
6053
+ * `four_answers.third_party_rights`, in fourteen files. Checking a name in one casing and reporting it
6054
+ * absent in both is the same error one level down from the one this paragraph is about.
6044
6055
  *
6045
6056
  * An absent or malformed findings.json yields no block, and the composer records the id as not built —
6046
6057
  * so "nothing to say" and "could not look" are told apart on the dispatch receipt rather than inferred.
@@ -6249,7 +6260,7 @@ function sentinel(runDir, name, obj) {
6249
6260
  }
6250
6261
 
6251
6262
  /**
6252
- * Settle every queued what-if BEFORE the run dir moves into the archive — tracker issue 240.
6263
+ * Settle every queued what-if BEFORE the run dir moves into the archive.
6253
6264
  *
6254
6265
  * A queued job lives under the run directory, and the worker enumerates candidates with `liveRunDirs`,
6255
6266
  * which skips `archive` outright. So archiving carried a pending job out of the worker's reach: not
@@ -7044,7 +7055,7 @@ export function injectZhScopeCoverage(P, runDir, note, opts = {}) { // @intern
7044
7055
  * returns nothing the moment `laneRan` is true, so a `full`-configured lane that ran candidates only
7045
7056
  * fell through every disclosure this pipeline had.
7046
7057
  *
7047
- * OWNER RULING (2026-08-17), and it decides the shape rather than just authorising the work: "if we
7058
+ * RULING (2026-08-17), and it decides the shape rather than just authorising the work: "if we
7048
7059
  * can't run deep dive on serpAPI we need to flag it" — FLAG, NOT GATE. So nothing here changes what a
7049
7060
  * run executes or what it retrieves; it changes what the delivered document admits. `coverage-limited`
7050
7061
  * is the state for exactly that: a disclosed limit, never a verdict clamp.
@@ -7230,7 +7241,7 @@ export function fullProseOrdinals(findings) {
7230
7241
  // ADVISORY_KINDS member could never reach a client document at all. The 2026-07-16 comment described the
7231
7242
  // behaviour it shipped with and became stale prose sitting next to code that contradicted it.
7232
7243
  //
7233
- // Owner ruling 2026-08-19 (relayed), on a renewal-deadline example: it renders. That intent is the
7244
+ // Ruling 2026-08-19 (relayed), on a renewal-deadline example: it renders. That intent is the
7234
7245
  // ruled behaviour. The tag is REQUIRED, not decoration — `advisoryLine` interpolates ADVISORY_TAG[a.kind]
7235
7246
  // directly, so a kind that passes the filter without an entry here renders "- **undefined** …" into a
7236
7247
  // client document. Whoever adds a fifth kind adds it in both places, and render.mjs needs the matching
@@ -7327,8 +7338,8 @@ export function buildOnlyYouSection(actions, findings, { nowMs = Date.now(), wit
7327
7338
  // Terminal punctuation on the ask BEFORE the subject join. This was written to work around a cut —
7328
7339
  // actYouConditions ended its bound line at the first '.' or ':' before whitespace, and the subject's
7329
7340
  // own "(re:" qualified, so a period-less ask (the synthesis prompt demands one sentence but nothing
7330
- // enforces terminal punctuation) delivered as a mangled "…terms (re". THAT CUT IS GONE (owner
7331
- // ruling 2026-08-10): the conditions box now renders the ask and its subject whole. The punctuation
7341
+ // enforces terminal punctuation) delivered as a mangled "…terms (re". THAT CUT IS GONE (ruling
7342
+ // 2026-08-10): the conditions box now renders the ask and its subject whole. The punctuation
7332
7343
  // stays because it was always the right output — "Instruct counsel on the joined-script forms (re: X)"
7333
7344
  // reads as one run-on sentence, and the period is what separates the ask from the driver's own note.
7334
7345
  // Asks with no subject are untouched.
@@ -7419,7 +7430,7 @@ export function buildOnlyYouSection(actions, findings, { nowMs = Date.now(), wit
7419
7430
 
7420
7431
  // ── T3a — THE REVIEWER'S OPEN POINTS, CODE-BUILT FROM THE REVIEW ────────────────────────────────────
7421
7432
  //
7422
- // Owner ruling 2026-08-26, verbatim: "Deliver always, with open points printed. The refusal on a
7433
+ // Ruling 2026-08-26, verbatim: "Deliver always, with open points printed. The refusal on a
7423
7434
  // blocking review goes." That REVERSES T3, which retired "delivered-with-open-questions" and
7424
7435
  // is itself recorded as an owner-approved decision — both are his, and this is the standing one.
7425
7436
  //
@@ -8450,7 +8461,7 @@ async function pipelineInner(job, opts = {}) {
8450
8461
  // and carries the measured reason it cannot be queued (queueing it would orphan the mint the last
8451
8462
  // flush's own pass makes). That list is censused by an arm, so this sentence cannot go stale
8452
8463
  // silently again — it read "the ONLY path" while one mechanism had always fired its own pass, and an
8453
- // invariant with an undeclared violation teaches the next reader the wrong rule (tracker issue 116).
8464
+ // invariant with an undeclared violation teaches the next reader the wrong rule.
8454
8465
  // Everything else: escalation/envelope/screen-gate keep
8455
8466
  // their unit-level work but MINT durable queue items instead of firing their own opus digest pass, and
8456
8467
  // the queue settles in ONE consolidated flush at the frame-reopen seam (pre-synthesis), plus at most
@@ -8603,6 +8614,25 @@ async function pipelineInner(job, opts = {}) {
8603
8614
  geography: job.geography ?? null,
8604
8615
  }, null, 2) + "\n");
8605
8616
  } catch (e) { note(`instructed-scope write failed (non-fatal): ${e.message}`); }
8617
+ // THE STORED DEFAULTS THE ENGINE CANNOT SEARCH — recorded by the run, not only by the plan preview.
8618
+ //
8619
+ // `defaultTerritoryState` is the ONE producer of this reading (effective-scope.mjs) and it is CALLED
8620
+ // here rather than re-derived: the run's own territory ladder (`resolveTerritories`) passes an
8621
+ // unrecognised account default straight through, so before this the only surfaces that named one were
8622
+ // the plan response and the profile screen — both intake-side. A firm whose stored default is
8623
+ // misspelled got a narrower search than the configuration says, and no run artifact said so.
8624
+ // Written for EVERY run, empty list included: an empty array is the asserted zero, and a reader who
8625
+ // only ever sees this file on the runs that had a bad entry cannot tell it from a run nothing checked.
8626
+ try {
8627
+ const dts = defaultTerritoryState(ctx.profile);
8628
+ // two records of one fact, so each has its own try: a fault in one must not take the other
8629
+ try { if (dts.unrecognized.length) runLog(run.runDir, { event: "default-territory-unrecognized", count: dts.unrecognized.length, entries: dts.unrecognized }); } catch (e) { note(`default-territory-unrecognized log failed (non-fatal): ${e.message}`); }
8630
+ writeFileSync(P.defaultTerritories, JSON.stringify({
8631
+ profileKey: ctx.profile?.profileKey ?? null,
8632
+ searchable: dts.kept,
8633
+ unrecognized: dts.unrecognized,
8634
+ }, null, 2) + "\n");
8635
+ } catch (e) { note(`default-territories write failed (non-fatal): ${e.message}`); }
8606
8636
  // Change B5 — unknown-customer state + the late-bind consumer. The intake gate sets
8607
8637
  // job.customerUnknown when the applicant is neither stated nor forwarder-implied; the forwarding agent
8608
8638
  // may later drop customer-bind.json into this run dir when a thread reply names the applicant (B5b).
@@ -12010,7 +12040,7 @@ async function pipelineInner(job, opts = {}) {
12010
12040
  // an inventory that cannot be read hands the stage nothing, and the dictation then says nothing about
12011
12041
  // sources rather than asserting a readiness nobody measured.
12012
12042
  try {
12013
- // COMPOSED BY THE MODULE THAT READS IT (tracker issue 173). This mapping used to be inline here
12043
+ // COMPOSED BY THE MODULE THAT READS IT. This mapping used to be inline here
12014
12044
  // and rebuilt by hand in an arm — two authors for one shape, and the arm's copy went stale the
12015
12045
  // moment this one gained a field, while still passing. `caseLawSourceRows` is now the only place
12016
12046
  // it is written, and it carries `checked` so an UNREADABLE credential reaches the composer as a
@@ -12472,7 +12502,7 @@ async function pipelineInner(job, opts = {}) {
12472
12502
 
12473
12503
  // ── — THE TERMINAL GUARDS DELIVER AND CLAMP; THEY DO NOT WITHHOLD ───────────
12474
12504
  //
12475
- // Owner ruling, 2026-08-27, verbatim intent: **reports always ship**. When a terminal guard finds
12505
+ // Ruling, 2026-08-27, verbatim intent: **reports always ship**. When a terminal guard finds
12476
12506
  // the report incomplete at delivery, the engine sends it with the gap patched conservatively and
12477
12507
  // the defect named in the run record. It never withholds.
12478
12508
  //
@@ -12543,7 +12573,7 @@ async function pipelineInner(job, opts = {}) {
12543
12573
  // not an oversight: if the unanswerable count is ever the large one, it wants its own issue against
12544
12574
  // the band rather than a wider floor here.
12545
12575
  //
12546
- // NO REPAIR RUNG, deliberately and on a measurement (owner ruling relayed 2026-08-27). Every
12576
+ // NO REPAIR RUNG, deliberately and on a measurement (ruling relayed 2026-08-27). Every
12547
12577
  // registered repair composer belongs to another stage; placement has none, and building one is a
12548
12578
  // larger piece of work than this floor. The blast radius said it could wait: on the day this was
12549
12579
  // written today's run discharged all 60 of its floors, and the only two runs with undischarged rows
@@ -12556,7 +12586,7 @@ async function pipelineInner(job, opts = {}) {
12556
12586
  // argument was that nothing could then reach delivery unaccounted. The corrective pass is the state
12557
12587
  // that enumeration missed: `prepareDeclinationSpec` runs again before it over a surface that can
12558
12588
  // have grown, so it is ordered against a LARGER owed set; if that call is refused,
12559
- // `rollbackCorrectivePass` restores the pre-corrective findings and — by T3b's owner ruling,
12589
+ // `rollbackCorrectivePass` restores the pre-corrective findings and — by T3b's ruling,
12560
12590
  // deliberately — the run DELIVERS. What ships then satisfied the OLD owed set, and no acceptance
12561
12591
  // boundary ever saw a call about the records added since, because the only call ordered against them
12562
12592
  // was the one that was refused.
@@ -12910,7 +12940,7 @@ async function pipelineInner(job, opts = {}) {
12910
12940
  try { writeVerdictSidecar(); }
12911
12941
  catch (e) { throw new StageFailure("verdict", `verdict sidecar write failed (the single label authority): ${String(e.message).slice(0, 120)}`); }
12912
12942
 
12913
- // T3a, owner ruling 2026-08-26 — "Deliver always, with open points printed. The refusal on a blocking
12943
+ // T3a, ruling 2026-08-26 — "Deliver always, with open points printed. The refusal on a blocking
12914
12944
  // review goes." This REVERSES T3 (H3/H5), whose flip to fail-on-BLOCKING is itself recorded
12915
12945
  // itself an owner-approved decision. Both are his; this is the standing one, and the
12916
12946
  // history is kept here rather than deleted because a reader who finds only one of them will conclude
@@ -12979,7 +13009,7 @@ async function pipelineInner(job, opts = {}) {
12979
13009
  }
12980
13010
  }
12981
13011
  if (verdict === "BLOCKING") {
12982
- // ── T3a — THE RUN DELIVERS. Owner ruling 2026-08-26, verbatim: "Deliver always, with open
13012
+ // ── T3a — THE RUN DELIVERS. Ruling 2026-08-26, verbatim: "Deliver always, with open
12983
13013
  // points printed. The refusal on a blocking review goes." ──────────────────────────────────
12984
13014
  //
12985
13015
  // What stood here threw, and its reasoning was T3: "a report the reviewer will not
@@ -13390,7 +13420,7 @@ async function pipelineInner(job, opts = {}) {
13390
13420
  note(`recall: ${silent.lost.length} position(s) the digest ended as FINDINGS reached no client `
13391
13421
  + `surface and no step said why — ${silent.lost.map((l) => `${l.mark ?? l.uri} (${l.reason})`).join("; ")}`);
13392
13422
  }
13393
- // ── AND THE STATED CASE, WHICH IS THE ONE THAT REACHED A CLIENT (tracker issue 248) ────────
13423
+ // ── AND THE STATED CASE, WHICH IS THE ONE THAT REACHED A CLIENT ────────────────────────────
13394
13424
  //
13395
13425
  // Same inputs, same seam, same best-effort contract — a second call rather than a widened first
13396
13426
  // one, because the sibling's population boundary is deliberate and correct. It covers a
@@ -13685,7 +13715,7 @@ async function pipelineInner(job, opts = {}) {
13685
13715
  const askApplied = applyAskClosure(runAsks, acc.ask, fileTexts, { ts: askTs });
13686
13716
  runAsks = askApplied.asks;
13687
13717
  for (const u of askApplied.unverified) runLog(run.runDir, { event: "ask-closure-unverified", ...u });
13688
- // Option A (tracker issue 246): a recall ask whose mark is not in the delivered findings
13718
+ // Option A: a recall ask whose mark is not in the delivered findings
13689
13719
  // did NOT close. Each one is a mark the run found and the client was not shown, so it is
13690
13720
  // recorded by name rather than left to be inferred from an ask that merely stayed open.
13691
13721
  // ✕ NOT written into findings.json here. See the sidecar's own note: a row appended after
@@ -13695,7 +13725,7 @@ async function pipelineInner(job, opts = {}) {
13695
13725
  if (askApplied.carryIntoFindings?.length)
13696
13726
  writeFileSync(driverDir(run.runDir, "marks-owed-a-finding.json"),
13697
13727
  JSON.stringify({
13698
- _provenance: "recall asks that could not close because the mark they found is absent from findings.json — owner ruling 2026-09-07, tracker issue 246 Option A, worded about the MARK",
13728
+ _provenance: "recall asks that could not close because the mark they found is absent from findings.json — owner ruling 2026-09-07, Option A, worded about the MARK",
13699
13729
  _notWritten: "these are NOT appended to findings.json: a finding carries a band, a net line and a legal position, and a row minted here would carry none of them. The repair belongs where the findings are authored.",
13700
13730
  count: askApplied.carryIntoFindings.length,
13701
13731
  marks: askApplied.carryIntoFindings,
@@ -14647,6 +14677,13 @@ async function pipelineInner(job, opts = {}) {
14647
14677
  published = JSON.parse(readFileSync(join(run.runDir, ".published"), "utf8"));
14648
14678
  note(`publish skip — already published → ${published.url}`);
14649
14679
  } else {
14680
+ // THE LAST READ OF THE STOP FLAG, and it sits HERE rather than above the branch: a run that has
14681
+ // already published has delivered, and refusing at that point would record a delivered run as
14682
+ // cancelled — the same defect pointed the other way.
14683
+ assertNotCancelledBeforePublish(run.runDir, "clearance");
14684
+ // Same fact, same moment, same words as the knockout lane: past this line a stop cannot prevent
14685
+ // delivery, and the screen is told so rather than working it out.
14686
+ writeRunStatus(ctx, { stoppable: false });
14650
14687
  published = await publishReport({
14651
14688
  runId: `${run.slug}-${run.date}-${run.codename}`, codename: run.codename,
14652
14689
  reportMd: P.report, auditMd: P.audit, findingsJson: P.findings, poolRoot: config.poolRoot, poolUrl: config.poolUrl,
@@ -14659,6 +14696,20 @@ async function pipelineInner(job, opts = {}) {
14659
14696
  });
14660
14697
  sentinel(run.runDir, ".published", { runId: published.runId, url: published.url, counts: published.counts,
14661
14698
  clientGate: published.clientGate }); // machine-QC record (observability) — decides nothing
14699
+ // WHAT THE TWO GATES EACH SAID, in the run's own log, under names that say whose they are.
14700
+ // `.published` carries `clientGate.released` beside `counts.gateViolations`, which are the verdicts
14701
+ // of DIFFERENT gates: the first is the client-export refusal (evaluateClientGate), the second is the
14702
+ // audit workbook's own build check (validateAudit), which is advisory by design and decides nothing
14703
+ // about the release. Read as one gate they contradict each other — released, with a list of
14704
+ // violations beside it — and a delivered run shipped looking exactly like that. Named separately
14705
+ // here, with the advisory COUNT and the absences the gate did not close on, so the log a person
14706
+ // reads after a run answers "what did the checks say" without opening the sentinel and knowing
14707
+ // which field belongs to which gate.
14708
+ runLog(run.runDir, { event: "publish-gates",
14709
+ clientExport: published.clientGate?.released === false ? "closed" : "released",
14710
+ clientExportReasons: published.clientGate?.reasons ?? [],
14711
+ releasedDespiteAbsentInputs: (published.clientGate?.notClosing ?? []).map((n) => n.input),
14712
+ auditWorkbookAdvisory: published.counts?.gateViolations?.length ?? 0 });
14662
14713
  note(`published → ${published.url}`);
14663
14714
  }
14664
14715
  // Lifecycle honesty (charter P1 §4): publish is CODE, not a stage() call, so nothing ever advanced the
@@ -1,7 +1,7 @@
1
1
  // SPDX-License-Identifier: AGPL-3.0-only
2
2
  // Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
3
3
  //
4
- // THE TWO-REGISTER RULE, AS THE REVIEWER READS IT — tracker issue 333.
4
+ // THE TWO-REGISTER RULE, AS THE REVIEWER READS IT.
5
5
  //
6
6
  // The report goes to a lawyer who layers advice on top, and that lawyer's client reads the same page.
7
7
  // The band, the summary, the basis line and the one-liners are the whole product for the second reader,
@@ -127,8 +127,82 @@ export function plainRegisterFlags(text, about = {}) {
127
127
  /**
128
128
  * The fields a reader meets before opening anything. Named here rather than at each call site so the
129
129
  * two products answer to one list — the knockout and the clearance drifted apart once already.
130
+ *
131
+ * EVERY ENTRY IS A PATH INTO A DELIVERED RECORD, and an arm resolves each one against the runs under
132
+ * `demo/`. That is the whole repair. The clearance half named eight fields of which six did not exist
133
+ * in any casing, and the reason nobody noticed is that a list of names is only ever read BY a person:
134
+ * `thirdPartyRights` beside a record carrying `four_answers.third_party_rights` reads as correct, and a
135
+ * loop over it opens nothing and reports a clean result over text it never read. A path either resolves
136
+ * or the arm says so.
137
+ *
138
+ * THE PATHS ARE THE SURFACES THE LIVE CHECK ALREADY READS. `plainRegisterExtra` in pipeline.mjs walks
139
+ * the clearance record's own keys and had a comment explaining that it could not use this list. The list
140
+ * is now what that walk reads, plus the four answers, which render on the report as prose and were
141
+ * missing from both. One definition, and the walk is the thing that made it checkable.
142
+ *
143
+ * `[]` MARKS A LIST and `*` an object's own values: `findings[].net` is one sentence per finding, and
144
+ * `four_answers.*.read` one per answer — that register is keyed by answer name and is not an array.
145
+ *
146
+ * WHAT CAME OFF, AND WHY EACH. `oneLiner` and `freedomToOperate` exist in no record in either casing and
147
+ * name no surface anybody could point at — they are gone rather than renamed, because inventing a target
148
+ * for them would be a guess in the one place a guess reads as a fact. `ownRights` resolves to
149
+ * `findings[].own_rights.source`, which is a record URI and not prose: a citation has no register, and
150
+ * running a plain-words check over one would flag the profession's vocabulary inside a machine
151
+ * identifier. `batchOpener` was in the KNOCKOUT half and is dead in both casings there.
152
+ *
153
+ * KNOWN INCOMPLETE, DELIBERATELY. `marks[].registerReads[].read` is walked as knockout-visible prose by
154
+ * `knockoutVisibleProse` and is not on the knockout list here. Adding it would widen what the reviewing
155
+ * pass rewrites on a delivered report, which is a change to what a client receives and not this repair's
156
+ * to make. The acceptance here is that every entry names something real, not that the list is complete;
157
+ * the completeness question is recorded on the issue rather than settled in passing.
130
158
  */
131
159
  export const DEFAULT_VISIBLE_FIELDS = Object.freeze({
132
- knockout: ["summary", "batchOpener", "basis", "net", "factors", "counterFactors", "mitigation", "standardCaveats", "reviewerNotes"],
133
- clearance: ["summary", "oneLiner", "registrability", "thirdPartyRights", "ownRights", "freedomToOperate", "reviewerNotes", "coverage"],
160
+ knockout: [
161
+ "batch.executiveSummary", "batch.standardCaveats[]",
162
+ "marks[].basis", "marks[].factors[]", "marks[].counterFactors[]", "marks[].mitigation",
163
+ "marks[].purpleNotes[]", "marks[].findings[].net",
164
+ ],
165
+ clearance: [
166
+ "mark_assessment.distinctiveness", "mark_assessment.connotation",
167
+ "four_answers.*.read", "findings[].net", "coverage[].note", "actions[].text",
168
+ ],
134
169
  });
170
+
171
+ /**
172
+ * Every string a path in `DEFAULT_VISIBLE_FIELDS` reaches in one delivered record.
173
+ *
174
+ * THIS IS WHAT MAKES THE LIST CHECKABLE, and its absence is why six dead names sat there for months. A
175
+ * list of bare names can only be read by a person; a path can be resolved, so an arm can say which
176
+ * entries open nothing.
177
+ *
178
+ * `[]` walks a list, `*` walks an object's own values — `four_answers` is keyed by answer name rather
179
+ * than being an array, and a path that assumed a list there resolved to nothing while looking right.
180
+ *
181
+ * TWO FIELDS ARRIVE AS EITHER A STRING OR AN OBJECT, and this reads both because the code that renders
182
+ * them does. A reviewer's note is `p?.text ?? p` in `knockoutVisibleProse`; an assessment field is
183
+ * `typeof v === "string" ? v : v?.read` where the structured form carries typed rows beside the prose.
184
+ * Resolving only the string form would have reported both as dead on a record that carries them, which
185
+ * is the same absence-dressed-as-a-fact this repair exists to remove — measured, on the delivered runs.
186
+ *
187
+ * A missing key yields NOTHING rather than throwing. An absent optional field is a fact about that
188
+ * record, and the arm decides what an absence means across several of them.
189
+ */
190
+ export function resolveVisiblePath(record, path) {
191
+ let nodes = [record];
192
+ for (const seg of String(path).split(".")) {
193
+ const list = seg.endsWith("[]");
194
+ const key = list ? seg.slice(0, -2) : seg;
195
+ const next = [];
196
+ for (const n of nodes) {
197
+ if (n == null || typeof n !== "object") continue;
198
+ if (key === "*") { next.push(...Object.values(n)); continue; }
199
+ const v = key ? n[key] : n;
200
+ if (v == null) continue;
201
+ if (list && Array.isArray(v)) next.push(...v); else next.push(v);
202
+ }
203
+ nodes = next;
204
+ }
205
+ return nodes
206
+ .map((n) => (typeof n === "string" ? n : (typeof n?.text === "string" ? n.text : n?.read)))
207
+ .filter((s) => typeof s === "string" && s.trim());
208
+ }