clearotron 0.4.0 → 0.4.1-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 (96) hide show
  1. package/THIRD-PARTY-NOTICES.md +2 -2
  2. package/build-info.json +2 -2
  3. package/docs/SECURITY-OWASP.md +21 -13
  4. package/docs/SECURITY.md +20 -14
  5. package/docs/architecture/04-configuration-reference.md +3 -2
  6. package/docs/architecture/05-config-governance.md +1 -1
  7. package/docs/architecture/06-operations-runbook.md +1 -1
  8. package/docs/architecture/09-security-and-data.md +16 -11
  9. package/driver/CHANGELOG.md +18 -0
  10. package/driver/binding-layers.mjs +28 -6
  11. package/driver/citation-census.json +3 -3
  12. package/driver/common-law-coverage-status.mjs +52 -0
  13. package/driver/common-law-receipts.mjs +1 -1
  14. package/driver/connotation-search.mjs +5 -5
  15. package/driver/contract-dictation-registry.mjs +1 -1
  16. package/driver/contract-e3-backlog.mjs +1 -1
  17. package/driver/contract-vocabulary.mjs +18 -18
  18. package/driver/degraded-parts.mjs +10 -3
  19. package/driver/dispatch-record.mjs +19 -0
  20. package/driver/doubt-closure-tool.mjs +2 -2
  21. package/driver/driver.config.mjs +39 -6
  22. package/driver/engagement-receipt.mjs +1 -1
  23. package/driver/engine/CONTRACT.md +41 -1
  24. package/driver/engine/anthropic-agent.mjs +56 -4
  25. package/driver/engine/mcp/codex-config.mjs +1 -1
  26. package/driver/engine/mcp/coverage-server.mjs +1 -1
  27. package/driver/engine/mcp/gather-config.mjs +1 -1
  28. package/driver/engine/mcp/recording-server.mjs +31 -4
  29. package/driver/engine/mcp/stdio-server.mjs +45 -7
  30. package/driver/engine/openai-agent.mjs +100 -17
  31. package/driver/engine/session-record.mjs +223 -0
  32. package/driver/enqueue-schema.mjs +2 -2
  33. package/driver/findings-model.mjs +3 -3
  34. package/driver/form-neighbourhood.mjs +7 -7
  35. package/driver/frame-diff-model.mjs +3 -3
  36. package/driver/framework.mjs +22 -0
  37. package/driver/gateway.mjs +94 -12
  38. package/driver/named-band.mjs +2 -0
  39. package/driver/package.json +1 -1
  40. package/driver/phase0.mjs +13 -2
  41. package/driver/pipeline-knockout.mjs +1 -1
  42. package/driver/pipeline.mjs +46 -33
  43. package/driver/portal-families.mjs +3 -3
  44. package/driver/portal-service.mjs +1 -1
  45. package/driver/predelivery-lint.mjs +19 -19
  46. package/driver/publish/index.mjs +1 -1
  47. package/driver/publish/knockout.mjs +5 -4
  48. package/driver/publish/office-record-links.mjs +17 -2
  49. package/driver/publish/publish-inputs.mjs +1 -1
  50. package/driver/publish/render-knockout.mjs +17 -5
  51. package/driver/publish/render.mjs +4 -2
  52. package/driver/record-fold.mjs +177 -0
  53. package/driver/register-count.mjs +18 -1
  54. package/driver/register-plan.mjs +71 -17
  55. package/driver/register-records.mjs +7 -0
  56. package/driver/registry-fidelity.mjs +2 -2
  57. package/driver/repairs.mjs +1 -1
  58. package/driver/report-card-record.mjs +4 -4
  59. package/driver/scope-facts.mjs +11 -3
  60. package/driver/search-policy.mjs +20 -1
  61. package/driver/skills/clearance-common-law/SKILL.md +22 -22
  62. package/driver/skills/clearance-register/digest.md +3 -3
  63. package/driver/skills/clearance-register/register-recipes.md +11 -11
  64. package/driver/skills/clearance-register/status-rules.md +1 -1
  65. package/driver/skills/clearance-register/unit.md +1 -1
  66. package/driver/skills/clearance-search/firm-wide-reasoning.md +3 -2
  67. package/driver/skills/clearance-search/phase2-execution.md +6 -5
  68. package/driver/skills/clearance-search/synthesis-rules.md +4 -4
  69. package/driver/skills/clearance-search/worked-examples.md +39 -39
  70. package/driver/skills/clearance-variants/SKILL.md +16 -16
  71. package/driver/skills/frame-diff/SKILL.md +2 -2
  72. package/driver/stage-context.mjs +27 -1
  73. package/driver/stages.mjs +12 -12
  74. package/driver/suite-census.json +175 -31
  75. package/driver/synthesis-record.mjs +5 -0
  76. package/driver/unit-inventory.mjs +17 -28
  77. package/driver/verify.mjs +6 -6
  78. package/mcp-server/CHANGELOG.md +4 -0
  79. package/mcp-server/lib/evidence.mjs +8 -8
  80. package/mcp-server/lib/knockout.mjs +1 -1
  81. package/mcp-server/package.json +2 -2
  82. package/package.json +2 -2
  83. package/portal-ui/package.json +1 -1
  84. package/providers/_shared/count.mjs +4 -1
  85. package/providers/_shared/enumerate.mjs +61 -22
  86. package/providers/_shared/execute-plan.mjs +2 -0
  87. package/providers/corsearch/src/capabilities.js +1 -1
  88. package/providers/oauth-mcp-bridge/CHANGELOG.md +4 -0
  89. package/providers/oauth-mcp-bridge/package.json +2 -2
  90. package/providers/signa/src/capabilities.js +17 -2
  91. package/providers/signa/src/core.js +234 -37
  92. package/scripts/band-shape-probe.mjs +2 -2
  93. package/scripts/deprecate-below.mjs +28 -4
  94. package/scripts/e2e-first-time.mjs +16 -1
  95. package/scripts/freeze-example-run.mjs +2 -2
  96. package/shared/scope.mjs +18 -2
@@ -80,7 +80,7 @@ export const VOCABULARY = [
80
80
  { token: "connotation_search_missing", stages: ["common-law"], site: "driver/verify.mjs" },
81
81
  { token: "receipts_short", stages: ["common-law"], site: "driver/verify.mjs" },
82
82
  { token: "half_path_unrecognized", stages: ["common-law-half"], site: "driver/verify.mjs:395" },
83
- { token: "no_coverage_status_row", stages: [...CL, "register-digest"], site: "driver/verify.mjs:452, 1416" },
83
+ { token: "no_coverage_status_row", stages: [...CL, "register-digest"], site: "driver/verify.mjs:452 commonLawMeaningSeat, 1416" },
84
84
  // B — `connotation_form_unparseable` and `connotation_form_untouched` are GONE: they
85
85
  // were states only a hand-authored document could be in, and the form path is deleted (ruling
86
86
  // 2026-08-17). Their subject matter is carried by the call tokens under the D1 family row below.
@@ -139,7 +139,7 @@ export const VOCABULARY = [
139
139
  { token: "coverage_no_status", stages: ["register-digest"], site: "driver/verify.mjs" },
140
140
  { token: "coverage_form_missing", stages: ["register-digest"], site: "driver/verify.mjs" },
141
141
  { token: "coverage_form_empty", stages: ["register-digest"], site: "driver/verify.mjs" },
142
- { token: "coverage_status_offenum", stages: ["register-digest"], site: "driver/verify.mjs:2050" },
142
+ { token: "coverage_status_offenum", stages: ["register-digest"], site: "driver/verify.mjs validators.registerFindings" },
143
143
  { token: "coverage_deferred_unaccounted", stages: ["register-digest"], site: "driver/verify.mjs coverageFormFail", family: "driver/register-plan.mjs PROVIDER_HARD_ERROR_PREFIX — token on a data row", dynamic: "D5" },
144
144
  { token: "coverage_clean_unexecuted", stages: ["register-digest"], site: "driver/verify.mjs, the matterContext validator", family: "driver/register-plan.mjs validatePlanFeasibility", dynamic: "D6" },
145
145
  { token: "coverage_clean_skipped", stages: ["register-digest"], site: "driver/verify.mjs, the matterContext validator", family: "driver/register-plan.mjs searchedJurisdictionsFromPlan", dynamic: "D6" },
@@ -150,7 +150,7 @@ export const VOCABULARY = [
150
150
  { token: "coverage_axis_", stages: ["register-digest"], site: "driver/verify.mjs", family: "driver/coverage-ledger.mjs", dynamic: "D4" },
151
151
  { token: "coverage_status_invalid", stages: ["register-digest"], site: "driver/verify.mjs", family: "driver/coverage-ledger.mjs", dynamic: "D4" },
152
152
  { token: "coverage_classes_invalid", stages: ["register-digest"], site: "driver/verify.mjs", family: "driver/coverage-ledger.mjs", dynamic: "D4" },
153
- { token: "plan_execution_unreadable", stages: ["register-digest", "narrative-refutation"], site: "driver/verify.mjs:1496, 1712" },
153
+ { token: "plan_execution_unreadable", stages: ["register-digest", "narrative-refutation"], site: "driver/verify.mjs:1496 rowLabel, 1712" },
154
154
 
155
155
  // ── matter-frame / clearance-variants / blind-frame / frame-diff ──────────────────────────────────────
156
156
  { token: "stagecontracts_invalid", stages: ["matter-frame", "clearance-variants", "placement-inquiry", "case-law"], site: "driver/verify.mjs:1539 coverageEntryList, 1170, 1366, 1677" },
@@ -197,7 +197,7 @@ export const VOCABULARY = [
197
197
  // verify.mjs reaches the module, so a family that could grow a member no row covers must be readable.
198
198
  { token: "matterframe_", stages: ["matter-frame"], site: "driver/matter-frame-record.mjs acceptMatterFrame", family: "driver/matter-frame-record.mjs", dynamic: "D3" },
199
199
  { token: "framediff_model_missing", stages: ["frame-diff"], site: "driver/verify.mjs" },
200
- { token: "framediff_", stages: ["frame-diff"], site: "driver/verify.mjs:1224 → 742", family: "driver/frame-diff-model.mjs", dynamic: "D3" },
200
+ { token: "framediff_", stages: ["frame-diff"], site: "driver/verify.mjs:1224 checkFindingsSibling → 742", family: "driver/frame-diff-model.mjs", dynamic: "D3" },
201
201
 
202
202
  // ── register-unit ──────────────────────────────────────────────────────────────────────────────────
203
203
  { token: "declared_not_executed", stages: ["register-unit"], site: "driver/verify.mjs" },
@@ -206,17 +206,17 @@ export const VOCABULARY = [
206
206
  { token: "named_band_invalid", stages: ["register-unit"], site: "driver/verify.mjs" },
207
207
  { token: "named_band_collapsed", stages: ["register-unit"], site: "driver/verify.mjs" },
208
208
  { token: "band_block_unplanned", stages: ["register-unit"], site: "driver/verify.mjs" },
209
- { token: "named_band_", stages: ["register-unit"], site: "driver/verify.mjs:1736 → 742", family: "driver/named-band.mjs", dynamic: "D3" },
209
+ { token: "named_band_", stages: ["register-unit"], site: "driver/verify.mjs validators.registerUnit", family: "driver/named-band.mjs", dynamic: "D3" },
210
210
 
211
211
  // ── placement-inquiry ──────────────────────────────────────────────────────────────────────────────
212
- { token: "placementmodel_missing", stages: ["placement-inquiry"], site: "driver/verify.mjs:1352, 1379" },
212
+ { token: "placementmodel_missing", stages: ["placement-inquiry"], site: "driver/verify.mjs:1352 connotationViolations, 1379" },
213
213
  { token: "placement_unjudged", stages: ["placement-inquiry"], site: "driver/verify.mjs placementAccountVerdict" },
214
214
  { token: "placement_register_unrendered", stages: ["placement-inquiry"], site: "driver/verify.mjs placementAccountVerdict" },
215
215
  { token: "placement_form_unreadable", stages: ["placement-inquiry"], site: "driver/verify.mjs placementAccountVerdict" },
216
- { token: "placement", stages: ["placement-inquiry"], site: "driver/verify.mjs:1352 → 742", family: "driver/placement-model.mjs (`placement_*` and `placements_*`)", dynamic: "D3" },
216
+ { token: "placement", stages: ["placement-inquiry"], site: "driver/verify.mjs:1352 connotationViolations → 742", family: "driver/placement-model.mjs (`placement_*` and `placements_*`)", dynamic: "D3" },
217
217
 
218
218
  // ── case-law / narrative-refutation ────────────────────────────────────────────────────────────────
219
- { token: "caselaw_ledger_missing", stages: ["case-law"], site: "driver/verify.mjs:1682, 1685" },
219
+ { token: "caselaw_ledger_missing", stages: ["case-law"], site: "driver/verify.mjs validators.caseLaw" },
220
220
  { token: "caselaw_ledger_unparseable", stages: ["case-law"], site: "driver/verify.mjs" },
221
221
  { token: "caselaw_ledger", stages: ["case-law"], site: "driver/verify.mjs:2425 caseLawLedgerFail", family: "driver/case-law-ledger.mjs caseLawLedgerFail (census reasons)", dynamic: "D8" },
222
222
  { token: "no_verdict_line", stages: ["narrative-refutation"], site: "driver/verify.mjs" },
@@ -234,8 +234,8 @@ export const VOCABULARY = [
234
234
  /**
235
235
  * WRAPPERS — the emitted failure string is not always the bare token.
236
236
  *
237
- * invalid_file:<rel(gradedArtifact)>:<validator reason> gateway.mjs:788
238
- * max_tokens_no_output:<the whole string above> gateway.mjs:1113
237
+ * invalid_file:<rel(gradedArtifact)>:<validator reason> runStageLadder() in gateway.mjs
238
+ * max_tokens_no_output:<the whole string above> runStageLadder() in gateway.mjs
239
239
  *
240
240
  * The file segment is variable and `max_tokens_no_output:` PREFIXES an otherwise ordinary token, so a
241
241
  * matcher keyed on the bare token misses the emitted string and reports a false unattached token. These
@@ -281,7 +281,7 @@ export const ARM1_EXEMPTIONS = [
281
281
  {
282
282
  token: "profile_unparseable",
283
283
  stages: ["common-law"],
284
- reason: "The run's profile is config the driver loaded before the stage ran (verify.mjs:301). A corrupt profile is a config fault; no element of the common-law contract is about it.",
284
+ reason: "The run's profile is config the driver loaded before the stage ran (verify.mjs:301 COMMONLAW_UNAVAILABLE_RE). A corrupt profile is a config fault; no element of the common-law contract is about it.",
285
285
  },
286
286
  {
287
287
  token: "framework_manifest_unreadable",
@@ -303,7 +303,7 @@ export const ARM1_EXEMPTIONS = [
303
303
  {
304
304
  token: "plan_execution_unreadable",
305
305
  stages: ["register-digest", "narrative-refutation"],
306
- reason: "The plan-execution receipt is written by the register tool lane, not by either stage (verify.mjs:1496, 1712). Unreadable is a tool/driver fault; both stages are only READERS of it.",
306
+ reason: "The plan-execution receipt is written by the register tool lane, not by either stage (verify.mjs:1496 rowLabel, 1712). Unreadable is a tool/driver fault; both stages are only READERS of it.",
307
307
  },
308
308
  {
309
309
  token: "grid_spec_unreadable",
@@ -318,7 +318,7 @@ export const ARM1_EXEMPTIONS = [
318
318
  {
319
319
  token: "coverage_form_empty",
320
320
  stages: ["register-digest"],
321
- reason: "verify.mjs:1449, same artifact and same verdict in its own string: a form that parsed and carries no rows is \"(driver-written — this is a bug, not a model defect)\".",
321
+ reason: "verify.mjs:1449 connotationDispositionFail, same artifact and same verdict in its own string: a form that parsed and carries no rows is \"(driver-written — this is a bug, not a model defect)\".",
322
322
  },
323
323
  {
324
324
  token: "half_path_unrecognized",
@@ -331,7 +331,7 @@ export const ARM1_EXEMPTIONS = [
331
331
  * STAGE-UNREACHABLE VALIDATORS — not exemptions, a SCOPE statement.
332
332
  *
333
333
  * verify.mjs wires four validators no STAGES entry reaches: findings(1661), report(1719), audit(1728),
334
- * clientSummary(1736). They are NOT dead code — replay-archive.mjs:128 and mcp-server/lib/coverage.mjs:78
334
+ * clientSummary(1736). They are NOT dead code — replay-archive.mjs:128 replayRun and check() in mcp-server/lib/coverage.mjs
335
335
  * index validators[key] dynamically and the MCP server declares `audit` required — but no stage dispatches
336
336
  * them, so their tokens are unaccountable BY CONSTRUCTION with respect to a stage partition. The
337
337
  * client-summary STAGE was deleted 2026-08-01 (no line to cite: the stage is gone) and its validator outlived it.
@@ -343,7 +343,7 @@ export const STAGE_UNREACHABLE_VALIDATORS = [
343
343
  { validator: "findings", site: "driver/verify.mjs validators.findings", reason: "no STAGES entry names it; synthesis validates findings.json through validators.narrative's sibling checks instead" },
344
344
  { validator: "report", site: "driver/verify.mjs validators.report", reason: "report.md is ASSEMBLED by the driver (assembleReportMd); the validator survives as a post-assembly structural gate outside the stage table" },
345
345
  { validator: "audit", site: "driver/verify.mjs validators.audit", reason: "reached only through the MCP server's dynamic validators[key] lookup, which declares it required" },
346
- { validator: "clientSummary", site: "driver/verify.mjs:1736", reason: "the client-summary STAGE was deleted 2026-08-01 (no line to cite: the stage is gone); the validator was not" },
346
+ { validator: "clientSummary", site: "driver/verify.mjs validators.clientSummary", reason: "the client-summary STAGE was deleted 2026-08-01 (no line to cite: the stage is gone); the validator was not" },
347
347
  ];
348
348
 
349
349
  /**
@@ -423,7 +423,7 @@ export const TRIPWIRE_OUT_OF_SCOPE = [
423
423
  {
424
424
  prefix: "register_plan_",
425
425
  site: "driver/register-plan.mjs",
426
- reason: "parseRegisterPlan is called once from a validator (verify.mjs:2102) inside a try whose catch swallows it — `catch { /* no plan in reach — gate inactive */ }` at verify.mjs:2146. No throw of this family becomes a stage failure token.",
426
+ reason: "parseRegisterPlan is called once from a validator (validators.registerFindings in verify.mjs) inside a try whose catch swallows it — `catch { /* no plan in reach — gate inactive */ }` in the same validator. No throw of this family becomes a stage failure token.",
427
427
  },
428
428
  ];
429
429
 
@@ -526,12 +526,12 @@ export const INNER_CODES = Object.freeze([
526
526
  // `form_damaged` is minted in connotation-search.mjs AND coverage-form.mjs and becomes a different
527
527
  // token in each lane. The static census records the FIRST site it matched and no more, so its own
528
528
  // citation names one of the three mints — which is why `mints` is a list, and why a ruling copied from
529
- // the census output would have been half a ruling. verify.mjs:1075 already states the rule this row
529
+ // the census output would have been half a ruling. verify.mjs:1075 parseCorrectionKinds already states the rule this row
530
530
  // records: `coverage_form_damaged`, never a bare `form_damaged`.
531
531
  { code: "form_damaged",
532
532
  mints: ["driver/connotation-search.mjs:2004", "driver/connotation-search.mjs:2080", "driver/coverage-form.mjs:876"],
533
533
  rollsUpTo: ["connotation_form_damaged", "coverage_form_damaged"],
534
- why: "Minted in two lanes and namespaced per lane: verify.mjs:1019 for the meaning sweep, verify.mjs:1119 for the register digest. The namespacing is what keeps them apart — see verify.mjs:1075." },
534
+ why: "Minted in two lanes and namespaced per lane: verify.mjs:1019 parseCorrections for the meaning sweep, verify.mjs:1119 findReviewerCoherenceFlags for the register digest. The namespacing is what keeps them apart — see verify.mjs:1075 parseCorrectionKinds." },
535
535
  ]);
536
536
 
537
537
  /**
@@ -69,7 +69,14 @@ const readJsonl = (p) => {
69
69
  } catch { return []; }
70
70
  };
71
71
 
72
- /** The stage's own last attempt row, or null when the stage wrote none. */
72
+ /**
73
+ * The stage's own last attempt row, or null when the stage wrote none.
74
+ *
75
+ * A PART IS DEGRADED WHEN ITS STAGE FAILED, and that is the row's `fail`, never its `ok`. The two said the
76
+ * same thing until the attempt row learned to record a session that reached an error result and was
77
+ * recovered (engine/session-record.mjs): such an attempt is not ok, and its stage still delivered. Whether
78
+ * that should reach a client's report is a ruling nobody has made, so this reads what it always meant.
79
+ */
73
80
  export function lastAttempt(runDir, stage) {
74
81
  const rows = readJsonl(driverDir(runDir, `${stage}.jsonl`))
75
82
  .filter((r) => Number.isFinite(Number(r?.attempt)) && typeof r.ok === "boolean");
@@ -93,7 +100,7 @@ function courtDecisions(runDir, log) {
93
100
  if (lastEvent(log, ["case-law-decision"])?.run !== true) return null;
94
101
  const last = lastAttempt(runDir, "case-law");
95
102
  const part = { part: "court-decisions", name: PART_NAMES.courtDecisions };
96
- if (last?.ok === false) return { ...part, reason: reasonFor(last.fail), cause: `the case-law stage ended failed: ${last.fail ?? "no cause recorded"}` };
103
+ if (last?.fail) return { ...part, reason: reasonFor(last.fail), cause: `the case-law stage ended failed: ${last.fail}` };
97
104
  let text;
98
105
  try { text = readFileSync(join(runDir, "case-law-findings.md"), "utf8"); } catch { text = null; }
99
106
  if (text === null) return { ...part, reason: NOT_COMPLETED, cause: "case-law-findings.md is absent" };
@@ -119,7 +126,7 @@ const failedStep = (log, ok, failed) => {
119
126
  };
120
127
  const stageEnded = (runDir, stage) => {
121
128
  const last = lastAttempt(runDir, stage);
122
- return last?.ok === false ? last : null;
129
+ return last?.fail ? last : null;
123
130
  };
124
131
 
125
132
  // The register's own steps, each of which the run survives without: the band's shape (the floors and the
@@ -46,6 +46,25 @@ export function dispatchFileName(stage, attempt, repair = 0) {
46
46
  return `${stage}.attempt${attempt}${repair ? `.repair${repair}` : ""}.${DISPATCH_SUFFIX}`;
47
47
  }
48
48
 
49
+ export const STREAMS_DIR = "streams";
50
+
51
+ /**
52
+ * `_driver/streams/<label>.attempt<N>.jsonl` (or `…repair<M>…`): the raw stream of the session that
53
+ * dispatch started, named as its dispatch record is. The engines write it (engine/session-record.mjs,
54
+ * `streamSink`); this is the one place the name is made. Absolute, through `driverDir`, so a label's
55
+ * colon is written the way the platform can hold it.
56
+ *
57
+ * IN ITS OWN FOLDER, NEVER BESIDE THE JOURNALS. Every `*.jsonl` directly under `_driver/` is read as a
58
+ * stage's attempt journal by name: the token rollup sums its usage rows, the degraded-parts check reads
59
+ * its label off the file name, the integrity check leaves it out as append-only. A raw stream there
60
+ * would be counted as a stage and its usage summed twice. One folder down, no reader of `_driver/`
61
+ * lists it, and the integrity snapshot reads files only.
62
+ */
63
+ export function streamFilePath(runDir, stage, attempt, repair = 0) {
64
+ if (!runDir) return null;
65
+ return join(driverDir(runDir), STREAMS_DIR, driverFileName(`${stage}.attempt${attempt}${repair ? `.repair${repair}` : ""}.jsonl`));
66
+ }
67
+
49
68
  const shaOf = (buf) => createHash("sha256").update(buf).digest("hex").slice(0, 12);
50
69
 
51
70
  // ── — WHAT THE STAGE WAS GRANTED, BESIDE THE SHA THAT PROVES WHAT IT WAS ASKED ────────────────
@@ -119,8 +119,8 @@ export function captureCall(runDir, seq, received, { now = () => new Date().toIS
119
119
  //
120
120
  // THAT PREMISE IS WHY THIS IS SAFE, SO CHECK IT BEFORE MOVING THE SPEC WRITE. Five of the six
121
121
  // doubt-minting sites build an id from an ordinal over a FILTERED list
122
- // (`presence-reconciliation.mjs:182` mints `doubt:presence:<sheet>:<n>` after three `continue` filters,
123
- // with `status: "open"` hardcoded beside it); `doubt-ledger.mjs:232` is the exception and the template,
122
+ // (`presence-reconciliation.mjs:182 mintPresenceDoubts` mints `doubt:presence:<sheet>:<n>` after three `continue` filters,
123
+ // with `status: "open"` hardcoded beside it); `doubt-ledger.mjs:232 mintContradictionDoubts` is the exception and the template,
124
124
  // putting the candidate name in the id and using the ordinal only as a tiebreaker. If the spec is ever
125
125
  // minted per attempt instead of per stage, those ordinals can shift between attempts and this serve
126
126
  // starts handing back a verdict about a different subject. Nothing else in this file would notice.
@@ -1337,6 +1337,10 @@ export const PROVIDERS = {
1337
1337
  return { ok: true,
1338
1338
  total: Number.isFinite(p.total_hits) ? p.total_hits : null,
1339
1339
  ...(p.total_approximate === true ? { approximate: true, floor: Number.isFinite(p.total_floor) ? p.total_floor : null } : {}),
1340
+ // The register's own notes on this answer, carried whole as the clearance sweep carries them —
1341
+ // `expanded_fallback` among them, which says the total counts records rather than marks. The
1342
+ // knockout listing dropped them, so a knockout run could not show what a clearance run shows.
1343
+ ...(Array.isArray(p.warnings) && p.warnings.length ? { warnings: p.warnings } : {}),
1340
1344
  records: (Array.isArray(p.results) ? p.results : []).map((row) => {
1341
1345
  const rec = row?.raw && typeof row.raw === "object" ? core.normalizeRecord(row.raw, row.office || null) : null;
1342
1346
  return {
@@ -1355,6 +1359,9 @@ export const PROVIDERS = {
1355
1359
  filing_route: rec?.filingRoute ?? null,
1356
1360
  // No page per record at Signa (hasPublicRecordUrl: false above), and none is made up here.
1357
1361
  record_url: null,
1362
+ // The office's own page as the register gives it, for publish to fall back on where it cannot
1363
+ // address the office's page from the numbers above (office-record-links.mjs).
1364
+ office_url: rec?.officeUrl ?? null,
1358
1365
  };
1359
1366
  }) };
1360
1367
  } catch (e) { return { ok: false, records: null, reason: `listRecords threw: ${e.message}` }; }
@@ -2079,12 +2086,31 @@ export const ENGINE_BINARIES = {
2079
2086
  // and a pinned id was refused outright ("version 2.1.280 or newer is required"), measured 2026-09-22.
2080
2087
  // A floor that only asks for a program that starts is a floor that passes a machine this engine
2081
2088
  // cannot run the current generation on.
2082
- package: "@anthropic-ai/claude-code", floor: "2.1.284",
2089
+ //
2090
+ // THE HAIKU TIER CROSSED LATER, when Claude Haiku 5.5 joined the generation. Measured 2026-10-09 by a
2091
+ // one-word `-p` turn per version on the test box's own sign-in, reading the model the turn reports in
2092
+ // `modelUsage`:
2093
+ //
2094
+ // version --model haiku
2095
+ // 2.1.285 claude-haiku-4-5-20251001
2096
+ // 2.1.287 claude-haiku-4-5-20251001
2097
+ // 2.1.288 claude-haiku-4-5-20251001
2098
+ // 2.1.289 claude-haiku-4-5-20251001
2099
+ // 2.1.290 claude-haiku-4-5-20251001
2100
+ // 2.1.292 claude-haiku-4-5-20251001
2101
+ // 2.1.293 claude-haiku-5-5
2102
+ // 2.1.294 claude-haiku-5-5
2103
+ // 2.1.295 claude-haiku-5-5
2104
+ //
2105
+ // 2.1.295 still serves claude-opus-5-5 and claude-sonnet-5-5. The floor moves to 2.1.293 for the reason
2106
+ // it moved to 2.1.284: a stage that asks for haiku is asking for the current haiku.
2107
+ package: "@anthropic-ai/claude-code", floor: "2.1.293",
2083
2108
  // WHAT THE INSTALL TAKES ON DISK, in MB, which setup states before it asks to install. MEASURED, not
2084
2109
  // declared by the vendor: the engines folder after a fresh install of this package into an empty
2085
- // folder, on npm 10.9.8 and on 11.19.1, 2026-09-14. A later release can be larger or smaller, so setup
2086
- // says "about". Re-measure when the floor moves.
2087
- installMB: 233,
2110
+ // folder. Re-measured when the floor moved to 2.1.293: 242 MB for a fresh install of 2.1.293 itself,
2111
+ // on npm 10.9.9, 2026-10-09. A later release can be larger or smaller, so setup says "about".
2112
+ // Re-measure when the floor moves.
2113
+ installMB: 242,
2088
2114
  // The licence setup states wherever it tells a reader what they are about to install or use, as the
2089
2115
  // vendor's package declares it: "SEE LICENSE IN README.md", Anthropic's own terms.
2090
2116
  licence: "proprietary third-party software",
@@ -2110,8 +2136,15 @@ export const ENGINE_BINARIES = {
2110
2136
  vendor: "OpenAI",
2111
2137
  product: "Codex",
2112
2138
  env: "CLEAROTRON_CODEX_PATH", fallback: "codex",
2113
- package: "@openai/codex", floor: "0.154.0",
2114
- installMB: 324, // measured the same way and on the same day as Claude's, above
2139
+ // THE FLOOR IS THE OLDEST RELEASE THAT ACCEPTS THE JUDGMENT TIER'S MODEL on a ChatGPT sign-in, which
2140
+ // is how a subscription bills. Measured 2026-10-09 with a one-word `codex exec` turn per version on the
2141
+ // test box: `gpt-6.1-sol` is refused with a 400 on 0.156.1, 0.157.0 and 0.158.0 ("not supported when
2142
+ // using Codex with a ChatGPT account") and accepted on 0.159.0, 0.160.0, 0.161.0 and 0.162.0;
2143
+ // `gpt-6-luna` is accepted from 0.156.1. Below the floor every clearance stops at its first
2144
+ // judgment-tier stage (the tiers are in engine/openai-agent.mjs). The old floor, 0.154.0, was the
2145
+ // permissions feature's (engine/mcp/codex-config.mjs).
2146
+ package: "@openai/codex", floor: "0.159.0",
2147
+ installMB: 424, // re-measured the same way as Claude's, above: a fresh install of 0.159.0 itself, 2026-10-09
2115
2148
  licence: "third-party software under the Apache-2.0 licence", // the package's own "license" field
2116
2149
  label: "OpenAI — each stage runs as a headless `codex exec` turn",
2117
2150
  module: "engine/openai-agent.mjs", adapter: "openaiAgentEngine",
@@ -37,7 +37,7 @@ export function isFinding(heading, block) {
37
37
 
38
38
  // Distinctive word anchors (length ≥4, non-stopword) from the owner / element / goods / sector /
39
39
  // jurisdiction / mark vocabulary; multi-word anchors contribute their significant words (so a finding
40
- // mentioning "Plesner" anchors against owner "Plesner Advokatpartnerselskab").
40
+ // mentioning "Varnholt" anchors against owner "Varnholt Advokatpartnerselskab").
41
41
  function anchorTerms(anchors = {}) {
42
42
  const raw = [
43
43
  ...(anchors.owners ?? []), ...(anchors.dominantElements ?? []), ...(anchors.goodsServices ?? []),
@@ -36,7 +36,9 @@ ladder consumes it without knowing which engine produced it ([gateway.mjs](../ga
36
36
  laneWaitMs: number,
37
37
  json: object | null, // synthesized envelope in the classifier's shape; every downstream
38
38
  // classifier (payloadText, status, embedded-fallback, timeout,
39
- // lane-wedge) reads this and works unchanged across engines
39
+ // lane-wedge) reads this and works unchanged across engines.
40
+ // It also carries how the session ended: terminalReason,
41
+ // apiErrorStatus and errorText (see `session` below)
40
42
  usage: Usage | null, // canonical shape below; null = no tokens accounted (e.g. stall)
41
43
  sessionRef: string | null, // opaque resume handle (claude session_id | codex thread_id)
42
44
  modelWire: string | null, // MODEL GAUGE — the served model id this turn observed (§3);
@@ -57,9 +59,47 @@ ladder consumes it without knowing which engine produced it ([gateway.mjs](../ga
57
59
  // opened; [] = recorded "read nothing"; key absent = engine cannot
58
60
  // observe reads (openai-agent) → gateway journals `reads: null`
59
61
  readsTruncated: boolean | absent, // true = the path cap dropped at least one further distinct file
62
+ toolCallsByName: object | null, // the turn's tool calls by tool name; an MCP call is
63
+ // `mcp__<server>__<tool>` on both engines; null = no session ran
64
+ session: Session | null, // what the session went through (below); null = no session ran
65
+ stream: { file, present, bytes, reason? } | null,
66
+ // the raw stream, written to the file the caller named
67
+ // (the gateway names _driver/streams/<label>.attempt<N>.jsonl)
60
68
  }
61
69
  ```
62
70
 
71
+ ### `session` — what the session went through
72
+
73
+ Both engines used to keep only the LAST result of a session, so a session the vendor's classifier cut
74
+ off mid-answer, or refused and a restart recovered, ended in a successful result and was recorded as
75
+ clean. `session` keeps every event of that kind, as the program wrote it, with milliseconds since the
76
+ spawn ([engine/session-record.mjs](session-record.mjs)):
77
+
78
+ ```
79
+ Session { interrupted, starts, results: [{atMs, index, subtype, isError, stopReason, terminalReason, origin}],
80
+ classifierCuts: [{atMs, synthetic}], refusals: [{atMs, category, model}], refusalStops }
81
+ ```
82
+
83
+ It carries kinds, codes and times, never the program's words. The retry ladder still reads the last
84
+ result alone; the gateway journals the attempt as `ok: false` when any result carried `is_error`, while
85
+ `fail` — what every reader of "did the stage fail" reads — is unchanged. Codex states no cut or refusal
86
+ on its stream, so on that engine `results` lists every `turn.completed`, `turn.failed` and `error`.
87
+
88
+ How the session ended rides the envelope, and the gateway writes it on every attempt and repair row,
89
+ null where the engine said nothing:
90
+
91
+ ```
92
+ terminalReason the kind of ending as the program states it (claude `terminal_reason`); null on codex
93
+ apiErrorStatus the vendor's status when it gave one (claude `api_error_status`); null on codex
94
+ errorText the program's own words for the error, only when the session ended in one, cut to
95
+ 300 characters: claude's result text when that result has `is_error`; codex's
96
+ `turn.failed` message, or its stream error when the turn never completed
97
+ ```
98
+
99
+ These are the one place the record holds the program's words: an expired sign-in otherwise reads as
100
+ `stop_sequence` and nothing else. `stopReason` stays verbatim beside them, and none of the three moves a
101
+ verdict.
102
+
63
103
  `runStage()` folds that into what the pipeline consumes — `{ok, json, attempts, text, sessionKey, …}` —
64
104
  and journals usage/wall/status on the way past.
65
105
 
@@ -26,6 +26,7 @@ import { authorityTrees } from "../authority-trees.mjs";
26
26
  import { recordEngineChild, clearEngineChild } from "./child-record.mjs"; //
27
27
  import { engineEnv } from "./engine-env.mjs"; // — the program's environment, by list (see spawnEnv)
28
28
  import { engineSpawn, spawnsDetached, killWindowsTreeNow } from "./engine-spawn.mjs"; // one answer to how a turn starts and ends, on every platform
29
+ import { newSessionRecord, noteClaudeEvent, sessionSummary, countTool, streamSink, claudeEnding } from "./session-record.mjs"; // what the session went through, beside how it ended
29
30
 
30
31
  // Read per-call (not module-level) so tests can drive a short stall timeout / a mock binary.
31
32
  // ONE place knows how to find the program (driver.config.mjs resolveEngineProgram): the explicit setting,
@@ -514,6 +515,9 @@ export function synthesizeEnvelope({ resultEvent, usage, killed }) {
514
515
  status: ok ? "ok" : (killed ? "timeout" : "error"),
515
516
  result: { meta: { agentMeta: { usage } }, payloads: [{ text }] },
516
517
  summary: r?.subtype, runId: r?.session_id, stopReason: r?.stop_reason,
518
+ // …and beside the stop reason, what ended the session and, on an error, the program's own words for it
519
+ // (session-record.mjs, claudeEnding). Recorded only: `ok` above reads exactly what it read before.
520
+ ...claudeEnding(r),
517
521
  };
518
522
  }
519
523
 
@@ -524,7 +528,7 @@ export const anthropicAgentEngine = {
524
528
  // doctrine tree, the profile store and `<runDir>/_driver/` at the moment of the write. That is a real
525
529
  // boundary and this engine has it.
526
530
  writeBoundary: "enforced",
527
- async runTurn({ message, model, thinking, timeoutSec, resumeRef, mcpConfig, allowedTools, cwd, skillsDir, skillsGrantRoots, profilesDir, resolveSkill, runDir, seatWrites = null, grantDispatch = null, stallSec, progressFiles } = {}) {
531
+ async runTurn({ message, model, thinking, timeoutSec, resumeRef, mcpConfig, allowedTools, cwd, skillsDir, skillsGrantRoots, profilesDir, resolveSkill, runDir, seatWrites = null, grantDispatch = null, stallSec, progressFiles, streamFile = null } = {}) {
528
532
  if (!message) throw new Error("anthropic-agent.runTurn: message is required");
529
533
  const { args, input, grantNote } = buildClaudeArgs({ message, model, thinking, resumeRef, mcpConfig, allowedTools, cwd, skillsDir, skillsGrantRoots, profilesDir, resolveSkill, runDir, seatWrites, grantDispatch });
530
534
  // the cross-check speaks. Driver stderr is what an operator reads, and it is where the other
@@ -690,6 +694,24 @@ export const anthropicAgentEngine = {
690
694
  // ("a completed tool result = the agent loop advanced", below). The gap between them is the turn
691
695
  // waiting on tools rather than generating.
692
696
  let toolCalls = 0;
697
+ // …AND THE SAME CALLS BY TOOL NAME, so a tool server's own log can be set against what the model
698
+ // asked for. A name is the program's identifier for a tool (`Read`, `mcp__band__band_lookup`), never
699
+ // an input, so it carries no client text. The values sum to `toolCalls` by construction: one fold
700
+ // per tool_use block, at the one site that counts them.
701
+ const toolCallsByName = Object.create(null);
702
+ // …AND WHICH OF THEM A TOOL SERVER ANSWERED WITH AN ERROR. A refused call the model then re-made used to
703
+ // leave the stage row reading ok on one attempt, as though nothing had happened. Each ask's id names its
704
+ // tool, and its result says whether it came back as an error; nothing else is kept.
705
+ const toolNameById = new Map();
706
+ const toolErrorIds = new Set();
707
+ // WHAT THE SESSION WENT THROUGH, beside how it ended: every result event, every classifier cut,
708
+ // every refusal and every restart, each with when it arrived. See session-record.mjs; the retry
709
+ // policy below still reads the LAST result alone, so this records and decides nothing.
710
+ const session = newSessionRecord();
711
+ // The raw stream, one file per session beside its dispatch (the caller names it). Opened before the
712
+ // first byte can arrive; closed in settle(). A sink that could not open is a null writer whose
713
+ // close() says why.
714
+ const sink = streamSink(streamFile);
693
715
  // HOW MANY OF THOSE ASKED FOR A COMMAND TOOL. The command tools are removed from every stage
694
716
  // (COMMAND_TOOLS), so the number a test round expects here is zero, and a count is what proves it: a
695
717
  // flag in the argv shows what was asked of the program, this shows what the model did. Still a count
@@ -814,6 +836,7 @@ export const anthropicAgentEngine = {
814
836
  let ev;
815
837
  try { ev = JSON.parse(line); } catch { return; /* partial/non-json line */ }
816
838
  sawAnyEvent = true;
839
+ noteClaudeEvent(session, ev, Date.now() - t0);
817
840
  if (ev.type === "result") resultEvent = ev;
818
841
  else if (ev.type === "rate_limit_event") rateLimitEvent = ev;
819
842
  else if (ev.type === "system" && ev.subtype === "init") {
@@ -839,7 +862,8 @@ export const anthropicAgentEngine = {
839
862
  // classification and no recovery park, or the turn never settles with its watchdog already gone.
840
863
  // Same belt-and-braces as the thinking gauge's `?.some?.()` on the line above.
841
864
  for (const b of Array.isArray(ev.message?.content) ? ev.message.content : []) {
842
- if (b?.type === "tool_use") toolCalls++; // #1111 — every tool, not only Read; a COUNT, never a name
865
+ if (b?.type === "tool_use") { toolCalls++; countTool(toolCallsByName, b?.name); } // #1111 — every tool, not only Read; the count and its split by name
866
+ if (b?.type === "tool_use" && b?.id != null) toolNameById.set(String(b.id), String(b?.name ?? "?"));
843
867
  if (b?.type === "tool_use" && COMMAND_TOOLS.includes(b?.name)) commandToolCalls++;
844
868
  if (b?.type === "tool_use" && FILE_WRITE_TOOLS.includes(b?.name) && b?.id != null) writeAsks.add(String(b.id));
845
869
  if (b?.type === "tool_use" && b?.name === "Read" && typeof b?.input?.file_path === "string") {
@@ -873,8 +897,10 @@ export const anthropicAgentEngine = {
873
897
  progress();
874
898
  }
875
899
  else if (ev.type === "user") {
876
- for (const b of Array.isArray(ev.message?.content) ? ev.message.content : [])
900
+ for (const b of Array.isArray(ev.message?.content) ? ev.message.content : []) {
877
901
  if (b?.type === "tool_result" && b?.is_error === true && writeAsks.has(String(b?.tool_use_id))) writesFailed++;
902
+ if (b?.type === "tool_result" && b?.is_error === true && b?.tool_use_id != null) toolErrorIds.add(String(b.tool_use_id));
903
+ }
878
904
  // the result of the ask above. Only counted when an ask is outstanding, so a `user`
879
905
  // event with no preceding tool_use adds nothing rather than charging the turn for a gap it
880
906
  // never spent waiting.
@@ -1052,6 +1078,7 @@ export const anthropicAgentEngine = {
1052
1078
  chunkGapMs = prevChunkAt === null ? 0 : Date.now() - prevChunkAt;
1053
1079
  prevChunkAt = Date.now();
1054
1080
  lastMove = Date.now(); // ANY streamed byte = liveness → resets the stall clock
1081
+ sink.write(d); // the raw stream, byte for byte, before anything parses it
1055
1082
  buf += d.toString();
1056
1083
  let nl;
1057
1084
  while ((nl = buf.indexOf("\n")) >= 0) { parseLine(buf.slice(0, nl)); buf = buf.slice(nl + 1); }
@@ -1074,7 +1101,7 @@ export const anthropicAgentEngine = {
1074
1101
  if (stderr.length > maxBuffer) { stderr = stderr.slice(0, maxBuffer); overflowKill(); }
1075
1102
  });
1076
1103
  // error + close can BOTH fire for a single failure (Node) → settle EXACTLY once (no double clearInterval/resolve).
1077
- child.on("error", (e) => { if (settled) return; settled = true; clearInterval(watchdog); resolve(errResult(t0, e, resumeRef)); });
1104
+ child.on("error", (e) => { if (settled) return; settled = true; clearInterval(watchdog); sink.close(); resolve(errResult(t0, e, resumeRef)); });
1078
1105
  child.on("close", (code) => settle(code));
1079
1106
  function settle(code) {
1080
1107
  // the turn is over; the record must not outlive it, and is cleared only if
@@ -1087,6 +1114,7 @@ export const anthropicAgentEngine = {
1087
1114
  try { child.stdout.destroy(); } catch { /* stream gone */ }
1088
1115
  try { child.stderr.destroy(); } catch { /* stream gone */ }
1089
1116
  if (!overflow) { parseLine(buf); } // B1: flush the final, un-terminated line (the result event) — but NEVER the truncated overflow tail
1117
+ const stream = sink.close(); // after the flush: every byte the program wrote is in it, or the record says why not
1090
1118
  buf = "";
1091
1119
  // ONE SAMPLE OF THE CLOCK, and both quantities below are derived from it. Two `Date.now()` calls
1092
1120
  // a few milliseconds apart are enough to break the identity on the very turn it matters for: on a
@@ -1176,11 +1204,18 @@ export const anthropicAgentEngine = {
1176
1204
  // from this list is unreliable and the consumer must downgrade it to "not observed").
1177
1205
  reads: [...reads], readsTruncated,
1178
1206
  toolCalls, toolWaitMs: settledToolWaitMs, // #1111 — two integers, no content
1207
+ // The same calls by tool name (values sum to toolCalls), and what the session went through: every
1208
+ // result, cut, refusal and restart (session-record.mjs). `stream` is the raw stream's file and size,
1209
+ // or why it was not kept. Recording only — `failed` above still reads the last result alone.
1210
+ toolCallsByName: { ...toolCallsByName }, session: sessionSummary(session), stream,
1179
1211
  // The two counts a test round reads per stage: calls to a command tool, and calls the program
1180
1212
  // refused. The refusals are the result event's own `permission_denials`, counted; null when the
1181
1213
  // turn settled with no result event, so "not reported" never reads as "none refused".
1182
1214
  commandToolCalls, writesFailed,
1183
1215
  toolCallsRefused: Array.isArray(resultEvent?.permission_denials) ? resultEvent.permission_denials.length : null,
1216
+ // The tool-server calls that came back as an error, by tool name: the program sent them and the
1217
+ // server refused or failed them. A call the program itself refused is counted above and not here.
1218
+ toolCallsErroredByName: toolServerErrors(toolErrorIds, toolNameById, resultEvent?.permission_denials),
1184
1219
  // ms from spawn to the child's FIRST byte, or null when it never spoke. RECORDING
1185
1220
  // ONLY: nothing branches on it. A recording field is protected by a test or by nothing, so
1186
1221
  // engine.anthropic.test.mjs pins it — deleting it must red something.
@@ -1253,9 +1288,26 @@ function errResult(t0, e, resumeRef) {
1253
1288
  // modelWire: null for the opposite reason — no turn ran, so the wire said nothing about a model, and
1254
1289
  // the record must say UNKNOWN rather than inherit the alias that was asked for.
1255
1290
  json: null, usage: null, reads: [], readsTruncated: false, modelWire: null, providerWire: null, sessionRef: resumeRef ?? null,
1291
+ // No session ran, so there is nothing to have been interrupted and no stream: null, never an empty record.
1292
+ toolCallsByName: null, toolCallsErroredByName: null, session: null, stream: null,
1256
1293
  };
1257
1294
  }
1258
1295
 
1296
+ /**
1297
+ * The tool-server calls of one turn that came back as an error, by tool name: every `mcp__` ask whose
1298
+ * result carried `is_error`, less the asks the program refused itself (`permission_denials`, which
1299
+ * `toolCallsRefused` counts). An OBJECT is a measurement (`{}`: none). PURE.
1300
+ */
1301
+ export function toolServerErrors(errorIds, nameById, denials) {
1302
+ const denied = new Set((Array.isArray(denials) ? denials : []).map((d) => String(d?.tool_use_id ?? "")).filter(Boolean));
1303
+ const byName = Object.create(null);
1304
+ for (const id of errorIds ?? []) {
1305
+ const name = nameById?.get(id);
1306
+ if (typeof name === "string" && name.startsWith("mcp__") && !denied.has(id)) countTool(byName, name);
1307
+ }
1308
+ return { ...byName };
1309
+ }
1310
+
1259
1311
  /**
1260
1312
  * The program's own word for which provider served a turn, from the result event's per-model usage:
1261
1313
  * `modelUsage[<model>].provider`, "firstParty" on Anthropic's own API and "foundry" on Azure Foundry
@@ -218,7 +218,7 @@ export function commandEnvToml(withheld = []) {
218
218
  // ── the stage's permission profile: what its shell commands may read and write ──────────────────────
219
219
  // codex's `workspace-write` sandbox confines where a command WRITES and lets it read the whole disk, the
220
220
  // install's settings files and the program's own sign-in included. A permission profile confines reading
221
- // too (the vendor's Permissions guide, a beta feature since 0.138.0; the floor is 0.154.0): a stage's
221
+ // too (the vendor's Permissions guide, a beta feature since 0.138.0; the floor was 0.154.0 when this was written and is 0.159.0 now, for the judgment tier's model — driver.config.mjs): a stage's
222
222
  // commands read the platform's own paths, the instruction trees and the temp folders, and write the run
223
223
  // folder, their working folder and the temp folders. Nothing else is readable, so a page a stage fetched
224
224
  // cannot get a command to print the account's files. The temp folders stay writable because
@@ -19,7 +19,7 @@
19
19
  // wired per run by the driver; there is no parameter for it and this tool never guesses one
20
20
  // (recording-server.mjs's record_doubt_closure records why the parameter must not exist). The form name comes off the run's own
21
21
  // era stamp, the row ids are the driver's, and every other identifier is computed. Read at CALL TIME,
22
- // never captured at module load (stdio-server.mjs:29-33 states why for exactly this variable).
22
+ // never captured at module load (stdio-server.mjs:29-33 toolLogPath states why for exactly this variable).
23
23
  import { serve } from "./stdio-server.mjs";
24
24
  import { recordCoverage } from "../../coverage-tool.mjs";
25
25
  import { MAX_ROWS_PER_CALL } from "../../coverage-call.mjs";
@@ -107,7 +107,7 @@ export const REGISTER_SERVERS = {
107
107
  // public page, and this provider publishes neither. Read 2026-08-22 across providers/signa/src: no
108
108
  // vienna or figurative field anywhere in the core, the normaliser carries `imageAvailable`
109
109
  // (`rec.has_media`) and nothing else about the mark's figure, `hasPublicRecordUrl: false` and
110
- // `resolved_link: null` with "Signa exposes no per-record public URL". Serving it would return a
110
+ // `resolved_link: null` (no page per record of its own). Serving it would return a
111
111
  // bare boolean under a name that promises comparable figurative data. NOT A PROBE: this is a read of
112
112
  // the code and the vendor doc in this repo, and whether the live API has an image endpoint nobody
113
113
  // wired is UNTESTED from here — euipo's `markImage` looked equally absent until `GET
@@ -5,7 +5,7 @@
5
5
  // ── WHAT THIS CATEGORY IS FOR, AND WHY IT IS NOT A RETRIEVAL GRANT ──────────────────────────────────
6
6
  //
7
7
  // The two-box model conflates "may reach the outside world" with "may hand back structure"
8
- // (gather-config.mjs:246-268). A stage converted to a typed return needs the second and not the first.
8
+ // (gather-config.mjs:246-268 RECORDING). A stage converted to a typed return needs the second and not the first.
9
9
  // This server carries ONLY the second: it writes the calling stage's own artifact into the calling
10
10
  // stage's own run directory, and it dials nothing.
11
11
  //
@@ -55,7 +55,7 @@ import { PROVIDERS, REGISTER_PROVIDER } from "../../driver.config.mjs";
55
55
  // already written against them; this conversion is the consumption their headers said was still owed.
56
56
  import { recordClosures } from "../../doubt-closure-tool.mjs";
57
57
  import { MAX_CLOSURES_PER_CALL, CLOSURE_KINDS } from "../../doubt-closure-call.mjs";
58
- import { readFileSync } from "node:fs";
58
+ import { readFileSync, existsSync } from "node:fs"; import { FROZEN_METHOD_FILE } from "../../framework-method.mjs";
59
59
  import { join } from "node:path";
60
60
  import { driverDir } from "../../../shared/driver-dir.mjs"; //
61
61
  import { VARIANT_CATEGORIES } from "../../variant-manifest-model.mjs";
@@ -398,7 +398,7 @@ serve({
398
398
  item: {
399
399
  type: "string",
400
400
  description:
401
- "For a FIRING variant directive this must be a mark-shaped search term (TAKIS, CORAL " +
401
+ "For a FIRING variant directive this must be a mark-shaped search term (WAVA, CORAL " +
402
402
  "MAGIC) or carry a remedy. A label — a parenthetical, an enumeration, more than about " +
403
403
  "four words — dispatches as a nil search that reads CLEAN, and is refused here.",
404
404
  },
@@ -1134,6 +1134,9 @@ serve({
1134
1134
  handler: record_synthesis,
1135
1135
  }, {
1136
1136
  name: "record_knockout_assess",
1137
+ // The framework's `inputs` fields are offered only on a run whose framework states a method: a session
1138
+ // offered a field it may not use can fill it, and the check that refuses it runs after the whole step.
1139
+ schemaNow: (schema) => knockoutAssessSchemaFor(schema, process.env.CLEAROTRON_BAND_RUN_DIR),
1137
1140
  description:
1138
1141
  "Hand back THIS CHUNK's rated assessment as VALUES. The driver serializes knockout-assess-<n>.json, " +
1139
1142
  "so you never format JSON and never write the file. WHICH CHUNK IS NOT YOURS TO SAY — the driver " +
@@ -1392,7 +1395,7 @@ serve({
1392
1395
  },
1393
1396
  batch: {
1394
1397
  type: "object",
1395
- required: ["productContext", "inUseAs", "places"],
1398
+ required: ["productContext", "inUseAs"], // places are per name (ruling 570): the batch list is optional, as in the acceptor and the manual
1396
1399
  properties: {
1397
1400
  productContext: { type: "string", description: "One sentence: what the batch is for. Every mark's contextFraming is read against it." },
1398
1401
  inUseAs: { type: "string", description: "The kinds of use off the register that could conflict in this client's field, as one phrase completing \"Is this name already in use as …?\". The places the batch is searched on include the sites where these uses are listed." },
@@ -1439,3 +1442,27 @@ serve({
1439
1442
  handler: record_knockout_frame,
1440
1443
  }],
1441
1444
  });
1445
+
1446
+ /**
1447
+ * The knockout save tool's schema for the run this server serves. The framework's `inputs` fields are
1448
+ * offered only where the run's framework states a method, which is when its frozen copy
1449
+ * (`_driver/framework-method.json`) exists: a run with no method has none, since the mint removes it. A
1450
+ * session offered a field it may not use can fill it, and the check that refuses it runs only after the
1451
+ * whole step. With no run to read, the schema is the one declared. Read when the tools are listed.
1452
+ */
1453
+ export function knockoutAssessSchemaFor(schema, runDir) {
1454
+ if (!runDir || existsSync(driverDir(String(runDir), FROZEN_METHOD_FILE))) return schema;
1455
+ const strip = (s) => {
1456
+ if (Array.isArray(s)) return s.map(strip);
1457
+ if (!s || typeof s !== "object") return s;
1458
+ const out = {};
1459
+ for (const [k, v] of Object.entries(s)) {
1460
+ if (k === "properties" && v && typeof v === "object" && !Array.isArray(v)) {
1461
+ out.properties = Object.fromEntries(Object.entries(v).filter(([name]) => name !== "inputs").map(([name, sub]) => [name, strip(sub)]));
1462
+ } else if (k === "required" && Array.isArray(v)) out.required = v.filter((name) => name !== "inputs");
1463
+ else out[k] = strip(v);
1464
+ }
1465
+ return out;
1466
+ };
1467
+ return strip(schema);
1468
+ }