clearotron 0.3.2 → 0.3.3-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 (123) hide show
  1. package/CONTRIBUTING.md +12 -0
  2. package/INSTALL.md +8 -0
  3. package/bin/onboard.mjs +109 -13
  4. package/bin/start.mjs +1 -1
  5. package/build-info.json +2 -2
  6. package/demo/MANIFEST.json +27 -0
  7. package/docs/INTAKE.md +8 -0
  8. package/docs/architecture/04-configuration-reference.md +29 -11
  9. package/driver/CHANGELOG.md +49 -0
  10. package/driver/citation-census.json +3 -3
  11. package/driver/clearance-variants-record.mjs +12 -1
  12. package/driver/common-law-coverage-status.mjs +113 -0
  13. package/driver/config-inventory.mjs +1 -1
  14. package/driver/contract-audit.mjs +1 -1
  15. package/driver/contract-e3-backlog.mjs +37 -37
  16. package/driver/contract-vocabulary.mjs +8 -8
  17. package/driver/coverage-form-io.mjs +3 -1
  18. package/driver/coverage-form.mjs +38 -11
  19. package/driver/coverage-ledger.mjs +37 -7
  20. package/driver/coverage-union.mjs +2 -2
  21. package/driver/crowd-context.mjs +19 -6
  22. package/driver/dev-portal.mjs +3 -3
  23. package/driver/drainer-identity.mjs +1 -1
  24. package/driver/driver.config.mjs +80 -9
  25. package/driver/engine/CONTRACT.md +3 -2
  26. package/driver/engine/anthropic-agent.mjs +34 -7
  27. package/driver/engine/mcp/clarivate-server.mjs +4 -2
  28. package/driver/engine/mcp/corsearch-server.mjs +3 -1
  29. package/driver/engine/mcp/coverage-server.mjs +1 -1
  30. package/driver/engine/mcp/dispositions-server.mjs +47 -5
  31. package/driver/engine/mcp/euipo-server.mjs +2 -0
  32. package/driver/engine/mcp/free-tier-server.mjs +2 -0
  33. package/driver/engine/mcp/gather-config.mjs +8 -2
  34. package/driver/engine/mcp/probe-server.mjs +37 -0
  35. package/driver/engine/mcp/proposal-fields.mjs +45 -0
  36. package/driver/engine/mcp/recording-server.mjs +30 -0
  37. package/driver/engine/mcp/signa-server.mjs +2 -0
  38. package/driver/engine/mcp/supplemental.mjs +89 -12
  39. package/driver/engine/mcp/unit-note-server.mjs +50 -0
  40. package/driver/engine/mcp/uspto-local-server.mjs +2 -0
  41. package/driver/engine/openai-agent.mjs +7 -0
  42. package/driver/engine/probe.mjs +67 -14
  43. package/driver/engine/tool-refusal.mjs +16 -0
  44. package/driver/enqueue-schema.mjs +2 -2
  45. package/driver/envelope-settle.mjs +82 -13
  46. package/driver/findings-model.mjs +4 -4
  47. package/driver/gateway.mjs +18 -2
  48. package/driver/manager-groups-verdict.mjs +1 -1
  49. package/driver/matter-frame-record.mjs +24 -7
  50. package/driver/named-band.mjs +1 -1
  51. package/driver/package.json +1 -1
  52. package/driver/partial-payload-baseline.json +12 -3
  53. package/driver/pipeline-knockout.mjs +3 -3
  54. package/driver/pipeline.mjs +154 -50
  55. package/driver/plan-run-agreement-verdict.mjs +49 -0
  56. package/driver/portal-service.mjs +8 -4
  57. package/driver/progress.mjs +14 -3
  58. package/driver/publish/index.mjs +41 -26
  59. package/driver/publish/report-data.mjs +4 -3
  60. package/driver/publish/xlsx.mjs +26 -4
  61. package/driver/queue-markers.mjs +44 -0
  62. package/driver/queue-watch-verdict.mjs +2 -2
  63. package/driver/reference-score.mjs +10 -2
  64. package/driver/register-availability.mjs +2 -2
  65. package/driver/register-plan.mjs +313 -21
  66. package/driver/roster-verdict.mjs +1 -1
  67. package/driver/runner.mjs +26 -2
  68. package/driver/settle-stamp.mjs +10 -3
  69. package/driver/skills/clearance-common-law/SKILL.md +2 -0
  70. package/driver/skills/clearance-register/SKILL.md +44 -3
  71. package/driver/skills/clearance-register/digest.md +5 -5
  72. package/driver/skills/clearance-register/providers/clarivate.md +1 -1
  73. package/driver/skills/clearance-register/unit.md +39 -0
  74. package/driver/skills/clearance-variants/SKILL.md +1 -1
  75. package/driver/skills/matter-frame/SKILL.md +4 -2
  76. package/driver/stages.mjs +12 -5
  77. package/driver/status-snapshot.mjs +2 -2
  78. package/driver/suite-census.json +293 -29
  79. package/driver/synthesis-record.mjs +80 -2
  80. package/driver/unit-file-drift.mjs +3 -3
  81. package/driver/unit-inventory.mjs +2 -2
  82. package/driver/unit-state-verdict.mjs +1 -1
  83. package/driver/updater-identity.mjs +2 -3
  84. package/driver/variant-manifest-model.mjs +11 -1
  85. package/driver/verify.mjs +5 -5
  86. package/driver/withheld-families.mjs +104 -0
  87. package/mcp-server/CHANGELOG.md +8 -0
  88. package/mcp-server/lib/brief.mjs +16 -12
  89. package/mcp-server/lib/runs.mjs +1 -1
  90. package/mcp-server/package.json +1 -1
  91. package/mcp-server/server.mjs +3 -2
  92. package/package.json +2 -2
  93. package/portal-ui/dist/assets/{index-DMthc7PQ.js → index-GBbbyQxc.js} +22 -4
  94. package/portal-ui/dist/index.html +1 -1
  95. package/portal-ui/package.json +1 -1
  96. package/providers/_shared/count.mjs +2 -2
  97. package/providers/_shared/enumerate.mjs +15 -2
  98. package/providers/_shared/execute-plan.mjs +19 -1
  99. package/providers/_shared/plan-guards.mjs +40 -0
  100. package/providers/clarivate/src/capabilities.js +15 -5
  101. package/providers/clarivate/src/core.js +41 -5
  102. package/providers/corsearch/src/capabilities.js +4 -0
  103. package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
  104. package/providers/oauth-mcp-bridge/package.json +1 -1
  105. package/providers/signa/src/capabilities.js +22 -8
  106. package/providers/signa/src/core.js +12 -1
  107. package/scripts/demo-evidence.mjs +114 -0
  108. package/scripts/e2e.mjs +1 -1
  109. package/scripts/engine-probe.mjs +6 -5
  110. package/scripts/env-audit.mjs +1 -1
  111. package/scripts/freeze-example-run.mjs +3 -3
  112. package/scripts/live-surface-check.mjs +32 -33
  113. package/scripts/mint-suite-census.mjs +66 -0
  114. package/scripts/package-size-budget.mjs +117 -0
  115. package/scripts/register-plan-shape.mjs +259 -0
  116. package/scripts/release-note-required.mjs +38 -1
  117. package/scripts/repo-writes.mjs +1 -1
  118. package/scripts/report-sections-render-check.mjs +7 -3
  119. package/scripts/score.mjs +7 -1
  120. package/scripts/settings-render-check.mjs +36 -0
  121. package/scripts/travelling-predicates.mjs +1 -1
  122. package/shared/identifier-scan.mjs +22 -5
  123. package/shared/scroll-settle.mjs +67 -0
@@ -36,6 +36,7 @@
36
36
 
37
37
  import { existsSync, readFileSync, writeFileSync, renameSync } from "node:fs";
38
38
  import { isCapabilityGapReason } from "./coverage-ledger.mjs";
39
+ import { fullyDeferredAxes } from "./register-plan.mjs";
39
40
 
40
41
  export const SETTLE_SCHEMA_VERSION = 1;
41
42
 
@@ -52,22 +53,28 @@ export const SETTLE_SCHEMA_VERSION = 1;
52
53
  * receipt is final. So `suspect` catches a MIS-STAMPED deferral — an executor bug flagging a transient as
53
54
  * deterministic — and exists so that shape gets one attempt instead of silently becoming permanent.
54
55
  *
55
- * PURE.
56
+ * STICKY — a qid this run has already accepted as a capability gap, under any plan version, is accepted
57
+ * again whatever its current reason text says (stickyGaps below). PURE.
56
58
  */
57
- export function partitionReceiptDeferrals(plan, receipt) {
59
+ export function partitionReceiptDeferrals(plan, receipt, { sticky = new Set() } = {}) {
58
60
  const axisOf = new Map((plan?.entries ?? []).map((e) => [String(e?.qid ?? ""), String(e?.axis ?? "").toLowerCase()]));
59
61
  // An axis the frozen plan says is deferred end to end is accepted whatever its per-qid reason text says:
60
62
  // there is no slice of it left for a dispatch to reach.
61
- const fullyDeferred = new Set((receipt?.skeleton ?? [])
62
- .filter((s) => s?.state === "deferred")
63
- .map((s) => String(s?.axis ?? "").toLowerCase()));
63
+ //
64
+ // THE PLAN SAYS SO, NOT THE COVERAGE SKELETON. This read the skeleton's `deferred` state, which
65
+ // deriveCoverageSkeleton sets for an axis carrying ANY deferred qid and nothing missing, and at the
66
+ // fan-in nothing is missing. So every deferral on every axis was accepted: a provider hard error was
67
+ // logged "ACCEPTED as provider capability gaps … never retried", skipped the one bounded attempt the
68
+ // hard-error path is designed to get, and was then re-opened by the envelope, which reads the reason.
69
+ // fullyDeferredAxes asks the plan: every entry on the axis `unsupported`.
70
+ const fullyDeferred = new Set(fullyDeferredAxes(plan).map((a) => String(a.axis ?? "").toLowerCase()));
64
71
  const accepted = [], suspect = [];
65
72
  for (const d of receipt?.deferred ?? []) {
66
73
  const qid = String(d?.qid ?? "");
67
74
  const axis = axisOf.get(qid) ?? "";
68
75
  const reason = String(d?.reason ?? "");
69
76
  const row = { qid, axis, reason };
70
- (isCapabilityGapReason(reason) || fullyDeferred.has(axis) ? accepted : suspect).push(row);
77
+ (isCapabilityGapReason(reason) || fullyDeferred.has(axis) || sticky.has(qid) ? accepted : suspect).push(row);
71
78
  }
72
79
  return { accepted, suspect };
73
80
  }
@@ -129,7 +136,7 @@ const shallowRow = (d) => ({ qid: String(d?.qid ?? ""), reason: String(d?.reason
129
136
  * the one case the file exists to make durable. `supersede()` builds the entry; it is deliberately a
130
137
  * summary rather than a full copy, because the qid-level truth is always re-derivable from the receipt.
131
138
  */
132
- export function buildDecisionDoc({ planVersion, deferredTotal, accepted, closed, closeFailed, decidedAt, history = [] }) {
139
+ export function buildDecisionDoc({ planVersion, deferredTotal, accepted, closed, closeFailed, decidedAt, history = [], stickyGaps = [], settleTried = [] }) {
133
140
  return {
134
141
  schema_version: SETTLE_SCHEMA_VERSION,
135
142
  plan_version: planVersion ?? null,
@@ -138,10 +145,60 @@ export function buildDecisionDoc({ planVersion, deferredTotal, accepted, closed,
138
145
  accepted: accepted ?? [],
139
146
  closed: closed ?? [],
140
147
  close_failed: closeFailed ?? [],
148
+ sticky_gaps: stickyGaps ?? [],
149
+ settle_tried: settleTried ?? [],
141
150
  history: history ?? [],
142
151
  };
143
152
  }
144
153
 
154
+ // ── A CAPABILITY GAP IS DECIDED ONCE PER RUN, NOT ONCE PER PLAN VERSION ────────────────────────────────
155
+ //
156
+ // The live `accepted[]` is rebuilt from the current receipt on every settle, and a settle happens again
157
+ // whenever the plan version moves — every supplemental fold bumps it. So an acceptance lasted exactly as
158
+ // long as the version it was made under. On a production run on 2026-09-22 one slice the provider refused
159
+ // was accepted ("never retried"), re-opened, re-proposed, re-executed and accepted again, several times
160
+ // in one run, each pass paying the provider for an answer that could not change.
161
+ //
162
+ // `sticky_gaps` is the run's memory of them: every qid accepted with a reason that states a capability gap
163
+ // (isCapabilityGapReason), with the reason and the plan version it was first accepted under. It is carried
164
+ // forward on every settle and never dropped: not when the plan version moves, not when the receipt stops
165
+ // listing the qid. Only a REASON-MATCHED gap enters it. An axis-level acceptance (the skeleton branch in
166
+ // partitionReceiptDeferrals) and every suspect or close_failed row stay out, so a deferral a later attempt
167
+ // could close keeps today's behaviour.
168
+ export function stickyGapsAfter(prior, accepted, planVersion) {
169
+ const out = new Map((prior?.sticky_gaps ?? []).map((g) => [String(g?.qid ?? ""), g]));
170
+ for (const a of accepted ?? []) {
171
+ const qid = String(a?.qid ?? "");
172
+ if (!qid || out.has(qid) || !isCapabilityGapReason(a?.reason)) continue;
173
+ out.set(qid, { qid, axis: String(a?.axis ?? ""), reason: String(a?.reason ?? "").slice(0, 400), since_plan_version: planVersion ?? null });
174
+ }
175
+ out.delete("");
176
+ return [...out.values()];
177
+ }
178
+
179
+ // ── A SUSPECT DEFERRAL GETS ITS ONE ATTEMPT ONCE PER RUN ─────────────────────────────────────────────
180
+ //
181
+ // The settle step spends one executor attempt on a suspect deferral: a hard error the run could not
182
+ // classify as permanent, on the chance the provider has recovered. Every supplemental fold bumps the plan
183
+ // version and re-settles, and the repair ledger's budget is keyed on the plan version, so "one attempt"
184
+ // was one per plan version. `settle_tried` records each qid the settle step has sent, carried forward like
185
+ // `sticky_gaps`; a suspect already in it is recorded close_failed without another call.
186
+ export function settleTriedAfter(prior, dispatched, planVersion) {
187
+ const out = new Map((prior?.settle_tried ?? []).map((t) => [String(t?.qid ?? ""), t]));
188
+ for (const d of dispatched ?? []) {
189
+ const qid = String(d?.qid ?? "");
190
+ if (!qid || out.has(qid)) continue;
191
+ out.set(qid, { qid, axis: String(d?.axis ?? ""), plan_version: planVersion ?? null, outcome: String(d?.outcome ?? "").slice(0, 160) });
192
+ }
193
+ out.delete("");
194
+ return [...out.values()];
195
+ }
196
+
197
+ /** The run's sticky capability gaps, by qid → row. Absent or unreadable decision ⇒ none. */
198
+ export function readStickyGaps(P) {
199
+ return new Map((readEnvelopeDecision(P)?.sticky_gaps ?? []).map((g) => [String(g?.qid ?? ""), g]).filter(([q]) => q));
200
+ }
201
+
145
202
  /** One history entry per superseded decision. Returns the prior history with the old decision appended. */
146
203
  export function supersede(prior, source) {
147
204
  if (!prior) return [];
@@ -167,30 +224,42 @@ export function supersede(prior, source) {
167
224
  */
168
225
  export async function settleReceipt({ P, plan, receipt, dispatch = null, rejoin = null, now = null }) {
169
226
  const deferred = receipt?.deferred ?? [];
170
- let { accepted, suspect } = partitionReceiptDeferrals(plan, receipt);
227
+ const prior = readEnvelopeDecision(P);
228
+ const sticky = new Set((prior?.sticky_gaps ?? []).map((g) => String(g?.qid ?? "")));
229
+ let { accepted, suspect } = partitionReceiptDeferrals(plan, receipt, { sticky });
171
230
  const closed = [], closeFailed = [];
231
+ const tried = new Map((prior?.settle_tried ?? []).map((t) => [String(t?.qid ?? ""), t]));
232
+ for (const s of suspect.filter((x) => tried.has(x.qid)))
233
+ closeFailed.push({ qid: s.qid, axis: s.axis, outcome: `already tried once this run (${String(tried.get(s.qid)?.outcome ?? "").slice(0, 100)}) — not sent again` });
234
+ suspect = suspect.filter((x) => !tried.has(x.qid));
235
+ const dispatched = [];
172
236
  if (suspect.length && dispatch && rejoin) {
173
237
  const byAxis = new Map();
174
238
  for (const s of suspect) { if (!byAxis.has(s.axis)) byAxis.set(s.axis, []); byAxis.get(s.axis).push(s.qid); }
175
239
  for (const [axis, qids] of byAxis) { if (axis) await dispatch(axis, qids); }
176
240
  const after = await rejoin();
177
241
  const stillDeferred = new Map((after?.deferred ?? []).map((d) => [String(d.qid), String(d.reason ?? "")]));
178
- const re = partitionReceiptDeferrals(plan, after);
242
+ const re = partitionReceiptDeferrals(plan, after, { sticky });
179
243
  accepted = re.accepted;
180
244
  for (const s of suspect) {
181
- if (!stillDeferred.has(s.qid)) closed.push({ qid: s.qid, axis: s.axis, outcome: "ok" });
182
- else if (!re.accepted.some((a) => a.qid === s.qid))
183
- closeFailed.push({ qid: s.qid, axis: s.axis, outcome: `still deferred: ${stillDeferred.get(s.qid).slice(0, 140)}` });
245
+ // Only a qid that was actually sent (dispatch skips one with no axis) counts as tried.
246
+ if (!stillDeferred.has(s.qid)) { closed.push({ qid: s.qid, axis: s.axis, outcome: "ok" }); if (s.axis) dispatched.push({ ...s, outcome: "ok" }); }
247
+ else if (!re.accepted.some((a) => a.qid === s.qid)) {
248
+ const outcome = `still deferred: ${stillDeferred.get(s.qid).slice(0, 140)}`;
249
+ closeFailed.push({ qid: s.qid, axis: s.axis, outcome });
250
+ if (s.axis) dispatched.push({ ...s, outcome });
251
+ }
184
252
  }
185
253
  } else if (suspect.length) {
186
254
  for (const s of suspect) closeFailed.push({ qid: s.qid, axis: s.axis, outcome: "no executor lane at this seam" });
187
255
  }
188
- const prior = readEnvelopeDecision(P);
189
256
  return writeEnvelopeDecision(P, buildDecisionDoc({
190
257
  planVersion: receipt?.plan_version, deferredTotal: deferred.length,
191
258
  accepted: accepted.map((a) => ({ ...a, decision: "accepted-capability-gap" })),
192
259
  closed, closeFailed, decidedAt: now ?? new Date().toISOString(),
193
260
  history: supersede(prior, prior ? "re-settled" : null),
261
+ stickyGaps: stickyGapsAfter(prior, accepted, receipt?.plan_version),
262
+ settleTried: settleTriedAfter(prior, dispatched, receipt?.plan_version),
194
263
  }));
195
264
  }
196
265
 
@@ -335,14 +335,14 @@ export function isUnconditionalProceed(rec) {
335
335
 
336
336
  /**
337
337
  * Deterministically BIND a recommendation line to the verdict: on a CONDITIONAL verdict an
338
- * unconditional "proceed" is rewritten to carry the conditions; on BLOCKING any recommendation is
339
- * replaced (a BLOCKING run does not reach delivery post- — belt-and-braces for legacy paths).
338
+ * unconditional "proceed" is rewritten to carry the conditions. BLOCKING is the reviewer's sign-off on
339
+ * the draft, not a hold: a BLOCKING run delivers, so its recommendation stands as the report wrote it.
340
340
  * Code derives the bound from the model's OWN verdict + reasons; it never invents a verdict. PURE.
341
341
  */
342
342
  export function bindRecommendation(rec, verdict, reasons = [], { maxReasons, maxLen } = {}) {
343
343
  const r = String(rec ?? "").trim();
344
344
  const v = String(verdict || "").toUpperCase();
345
- if (v === "BLOCKING") return "On hold — the reviewing lawyer's open questions must be resolved before any recommendation.";
345
+ // BLOCKING returns the recommendation as written: nothing holds delivery on it, so "on hold" was untrue (ruled 2026-09-22).
346
346
  if (v !== "CONDITIONAL" || !r || !isUnconditionalProceed(r)) return r;
347
347
  const conds = (reasons ?? []).filter(Boolean);
348
348
  if (!conds.length) return r;
@@ -600,7 +600,7 @@ export function riskStatement({ tier, verdict, reasons, basis, clauses } = {}) {
600
600
  // CLEAR, so long as it is clear on register findings ALONE).
601
601
  const registerOnly = basis === "register-only";
602
602
  const basisNote = registerOnly ? " Register findings only — no common-law or marketplace search was run." : "";
603
- if (v === "BLOCKING") return `On hold — the reviewing lawyer's open questions must be resolved before any recommendation.${basisNote}`;
603
+ if (v === "BLOCKING") return `${t}${basisNote ? `.${basisNote}` : ""}`; // the reviewer's sign-off is not the clearance's answer and nothing is held on it: the band stands alone, as below
604
604
  if (v === "CONDITIONAL") {
605
605
  // A clause stored as explicit null is a condition ruled to the run record alone: it is not the lede,
606
606
  // it is not counted, and its reason is never the fallback text. `undefined` (legacy/short) is not null.
@@ -225,6 +225,8 @@ export function toolWrittenArtifact(p) {
225
225
  ?? null;
226
226
  }
227
227
  import { buildGatherMcpConfig, allowedToolsFor, toolGroupsForStage, recordAxisFor, seatWritesForGroups } from "./engine/mcp/gather-config.mjs";
228
+ import { everyToolCallRefused } from "./engine/tool-refusal.mjs"; // one definition, shared with the engine probe
229
+ export { everyToolCallRefused };
228
230
  // The profiles STORE root, for the write boundary only — read from the module that owns it, never
229
231
  // re-derived from CLEAROTRON_CUSTOMERS_DIR here. Acyclic: profiles.mjs imports node builtins + config.
230
232
  import { unitRefusalsFor } from "./register-unit-record.mjs";
@@ -1528,6 +1530,11 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
1528
1530
  // appear: a postponed run resumes on its own, and a signed-out one cannot. The sentence rides after
1529
1531
  // the colon, as `model_mismatch:` carries its detail, so no run-level classifier needs a new token.
1530
1532
  else if (turn.signals?.signedOut && fail) fail = `engine_signed_out: ${turn.signals.signedOut}`;
1533
+ // AND ONE WHOSE TOOLS WERE ALL REFUSED IS NAMED, not left as the missing file the refusal caused. The
1534
+ // turn reported success and the stage failed for want of what its tools would have produced; a
1535
+ // `missing_file` reads as a model that did not write, and sends the reader to the wrong place. The
1536
+ // sentence says what fixes this host. After the sign-in, which is the more basic fault when both appear.
1537
+ else if (turn.signals?.toolsRefused && fail) fail = `engine_tools_refused: ${turn.signals.toolsRefused}`;
1531
1538
  // A6 (addendum 2026-07-30): stop_reason max_tokens with ZERO usable output is a DETECTED FAULT with a
1532
1539
  // name — never a silent paid retry. The turn ran to its output-token ceiling and the artifact never
1533
1540
  // landed (a content fail on a "successful" turn), or the turn itself died at the ceiling (transport
@@ -2027,6 +2034,15 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
2027
2034
  note(`[${name}] the engine's sign-in could not be refreshed — breaking the ladder (a retry re-sends the same credential)`);
2028
2035
  break;
2029
2036
  }
2037
+ // EVERY TOOL CALL THE TURN MADE WAS REFUSED. On some hosts codex's own sandbox refuses the stage's
2038
+ // tool servers while the turn reports success, and the stage then fails for want of what the tools
2039
+ // would have produced. The next attempt spawns the same sandbox and is refused the same way, so a
2040
+ // retry re-buys the refusal: measured at three paid attempts per stage on a production run before the
2041
+ // identical-failure break stopped it. Break on the first. A turn where any call completed is not this.
2042
+ if (everyToolCallRefused(turn)) {
2043
+ note(`[${name}] every tool call this turn made was refused (${turn.mcpToolCallsRefused}) — breaking the ladder (a retry is refused the same way)`);
2044
+ break;
2045
+ }
2030
2046
  // D3: overload (529 / status_overloaded) — stop the ladder here: an in-ladder re-attempt hammers an
2031
2047
  // API that just said it is overloaded, seconds apart, on the SAME model. Breaking hands the failure
2032
2048
  // to the existing machinery: the chain cascades models (FALLBACK_ELIGIBLE) and an exhausted chain
@@ -2816,7 +2832,7 @@ export function correctionHint(lastFail, { gridLedgerName = "common-law-grid.jso
2816
2832
  `The driver computed every obligation and every identifier in it — the coverage unit, the query id, the hit ` +
2817
2833
  `count, the unaccounted classes and terms, each deferred slice's own receipt reason. Record the named row(s) ` +
2818
2834
  `through the \`record_coverage\` tool — {"row_id","status","reason"} per row, never by writing or editing any ` +
2819
- `file: "status" EXACTLY one bare token of confirmed-clean / coverage-limited / deferred, "reason" the sentence ` +
2835
+ `file: "status" EXACTLY one bare token of confirmed-clean / coverage-limited / deferred / withheld-by-judgment, "reason" the sentence ` +
2820
2836
  `the lawyer reads (qualifiers go in the reason, never in the status). ` +
2821
2837
  `A row marked "open" cannot be confirmed-clean, and its own "open_because" says which of the two kinds ` +
2822
2838
  `it is. A NEVER-SEARCHED slice — the active register provider cannot express it, so nothing can make it run — is ` +
@@ -2983,7 +2999,7 @@ export function correctionHint(lastFail, { gridLedgerName = "common-law-grid.jso
2983
2999
  // run the driver stamped as form-required, and the stamp is conditional. On an unstamped run
2984
3000
  // validators.registerFindings demands the table exactly as it did before and emits this label,
2985
3001
  // so dropping the arm left the one lane that can still fire it with a generic hint.
2986
- hint = "the file has a findings heading plus a Coverage ledger with a status row (confirmed-clean / coverage-limited / deferred)";
3002
+ hint = "the file has a findings heading plus a Coverage ledger with a status row (confirmed-clean / coverage-limited / deferred)" + (/common-law-findings/.test(lastFail) ? ", or each ledger row's status is recorded by calling `record_coverage_status` with `grid_spec_path`, the same spec path the grid tool was given" : "");
2987
3003
  } else if (/negative-results|coverage-ledger|audit-trail|findings-heading/.test(lastFail)) {
2988
3004
  hint = "the findings file carries ALL required sections: a findings heading, the Negative results matrix " +
2989
3005
  "(every variant × platform row), the Coverage ledger with a status row, and the Audit trail call log";
@@ -47,7 +47,7 @@ export function managerGroupsVerdict({ idGroups, managerGroups, user, uid, why =
47
47
  // was not established", and saying so is the whole point — the deployment that HAD this fault also
48
48
  // had a check that reported nothing wrong.
49
49
  if (!Array.isArray(idGroups) || !idGroups.length) {
50
- return { state: "skip", message: `could not read the groups of ${user}${why ? ` — ${why}` : ""}. Not checked, not passed.` };
50
+ return { state: "skip", blocked: true, message: `could not read the groups of ${user}${why ? ` — ${why}` : ""}. Not checked, not passed.` };
51
51
  }
52
52
  if (!Array.isArray(managerGroups)) {
53
53
  return { state: "skip", message: `no readable systemd --user manager for ${user}${why ? ` — ${why}` : ""}. `
@@ -195,22 +195,39 @@ export const refuseUndeclared = (params) => refuseUndeclaredShared(params, DECLA
195
195
  * The Nice classes the frame judged necessary beyond the instructed ones, as strings. IMPURE (reads
196
196
  * the run's own accepted call).
197
197
  *
198
- * THE PLAN COMPILE UNIONS THIS WITH THE INSTRUCTED CLASSES so every variant axis carries both. Before
199
- * it existed the axes carried the instructed classes alone and a class the frame had identified reached
200
- * the sweep only if something proposed it as supplemental work — where it competed for capped slots
201
- * with model-minted extras. A cap was deciding coverage the frame had already judged necessary, and on
202
- * the run this came from the delivered report said two such classes were "covered for the name and open
203
- * for its variants" while the reviewing lawyer's scope included one of them throughout.
198
+ * THE PLAN COMPILE DOES NOT READ THIS. It takes `frameIdentifiedClassRows` below, where each added
199
+ * class keeps its reason and costs one identical-mark question rather than riding every entry (decision
200
+ * 18). The one caller left is the house-element check, which unions this list with the instructed
201
+ * classes because the client's own filings are best looked for across the widest class set.
204
202
  *
205
203
  * EMPTY IS THE ORDINARY ANSWER and must stay cheap: no frame yet, a legacy or replayed run whose
206
204
  * accepted call predates the field, a frame that identified nothing — all of them return `[]`, the
207
- * union is a no-op, and the plan is exactly what it was.
205
+ * union is a no-op, and the check looks in the instructed classes alone.
208
206
  */
209
207
  export function frameIdentifiedClasses(runDir) {
210
208
  const rows = lastAcceptedMatterFrame(runDir)?.identified_classes;
211
209
  return (Array.isArray(rows) ? rows : []).map((r) => String(r?.class ?? "").trim()).filter(Boolean);
212
210
  }
213
211
 
212
+ /**
213
+ * The same rows WITH THEIR REASONS, for the register plan (decision 18). IMPURE, like its sibling.
214
+ *
215
+ * The reason is why the class is here and it is half the row: the bound on this widening is that a
216
+ * class is added only for goods the CLIENT'S OWN business plainly reaches, and a class carrying no
217
+ * stated reason cannot be checked against that by anyone. The plan compiler drops such a row rather
218
+ * than searching it, so the reason is load-bearing and not documentation.
219
+ *
220
+ * Kept beside `frameIdentifiedClasses` rather than replacing it: the other caller verifies a proposed
221
+ * house element against an owner-scoped lookup, where the widest class set is the right one to look in
222
+ * and a reason would mean nothing.
223
+ */
224
+ export function frameIdentifiedClassRows(runDir) {
225
+ const rows = lastAcceptedMatterFrame(runDir)?.identified_classes;
226
+ return (Array.isArray(rows) ? rows : [])
227
+ .map((r) => ({ class: String(r?.class ?? "").trim(), reason: String(r?.reason ?? "").trim() }))
228
+ .filter((r) => r.class);
229
+ }
230
+
214
231
  /**
215
232
  * The forms of the name this run's client ratified, as strings. IMPURE (reads the run's own accepted
216
233
  * call). Empty or one form is the ordinary answer.
@@ -137,7 +137,7 @@ export function parseNamedBand(raw) {
137
137
  // byte-identical to a slice the plan deliberately counted without fetching. Measured on a real
138
138
  // run: four capability-gap blocks carried `error:true, deferred:true` into this function and
139
139
  // reached record-carry.json with both fields gone and a sentence claiming the run "has a hit
140
- // COUNT for this slice". register-plan.mjs:1835 validatePlanFeasibility already enforces the same rule one layer up
140
+ // COUNT for this slice". register-plan.mjs validatePlanFeasibility already enforces the same rule one layer up
141
141
  // ("a transient must not ship indistinguishable from a sanctioned descriptor") — it reads the
142
142
  // RAW blocks, which is why it could. Every consumer that reads THIS projection could not.
143
143
  // Conditional like the four keys above, so old bands carry neither key and nothing shifts.
@@ -2,7 +2,7 @@
2
2
  "name": "clearotron-driver",
3
3
  "private": true,
4
4
  "type": "module",
5
- "version": "0.3.2",
5
+ "version": "0.3.3-beta.1",
6
6
  "license": "AGPL-3.0-only",
7
7
  "description": "Deterministic driver for the trademark clearance workflow: orchestration in code (fan-out, fan-in barrier, gating, retries); the model does judgment leaves only, through a reasoning CLI spawned per stage.",
8
8
  "engines": {
@@ -27,6 +27,14 @@
27
27
  "tool": "record_dispositions",
28
28
  "why": "no fixture yet; accumulating semantics claimed in the tool description ('Everything already recorded is kept') and not yet planted"
29
29
  },
30
+ {
31
+ "tool": "record_coverage_status",
32
+ "why": "both declared fields are required, so a call has no top-level key to omit; its accumulation by unit (a later call keeps the units it omits) is pinned in a-common-law-coverage-status-is-read-as-data.test.mjs"
33
+ },
34
+ {
35
+ "tool": "record_withheld_families",
36
+ "why": "its one declared field is required, so a call has no top-level key to omit; its accumulation by qid (a later call keeps the families it omits) is pinned in every-waiting-family-is-asked-or-withheld.test.mjs"
37
+ },
30
38
  {
31
39
  "tool": "record_unit_note",
32
40
  "why": "no fixture yet; every field optional, which is the exposed shape — this row is the highest-priority one to close"
@@ -61,11 +69,12 @@
61
69
  {
62
70
  "tool": "record_clearance_variants",
63
71
  "fields": [
72
+ "goods_words",
64
73
  "incumbent_classes",
65
- "watchlist_owners",
66
- "search_floor"
74
+ "search_floor",
75
+ "watchlist_owners"
67
76
  ],
68
- "why": "a partial silently empties the register-axis search floor, narrowing the run's own definition of coverage"
77
+ "why": "a partial silently empties the register-axis search floor, narrowing the run's own definition of coverage. `goods_words` joins them for the same reason and one worse: an emptied goods list reads as \"this matter named no goods\" rather than as a partial call, so the crowded sweep it exists to narrow goes back to returning more filings than anyone can read, and nothing says why. The MERGE preserves all four (mergeClearanceVariantsCall, keep-if-absent) — these rows record that the ACCEPTOR alone does not."
69
78
  },
70
79
  {
71
80
  "tool": "record_blind_frame",
@@ -639,7 +639,7 @@ export async function knockoutInner(ctx, job, opts = {}) {
639
639
  // for reconcile-runs' exact liveness test. The stepper and the identity are separate calls now.
640
640
  ...identitySeed(),
641
641
  stepIndex: 0, stepLabel: STEPS[0], stepN: 1, stepTotal: STEPS.length,
642
- verdict: null, url: null, failedStage: null, reason: null, deliveredAt: null,
642
+ review: null, url: null, failedStage: null, reason: null, deliveredAt: null,
643
643
  // A5/A3: a re-run of a previously-terminal knockout may reopen the state ONLY because the resume
644
644
  // guard cleared the sentinel (ctx.stateReset threads that authority); startedAt is no longer
645
645
  // seeded anywhere — writeRunStatus backfills it first-write-wins.
@@ -1092,11 +1092,11 @@ export async function knockoutInner(ctx, job, opts = {}) {
1092
1092
  const deliveredAt = new Date().toISOString();
1093
1093
  // `tier` beside `verdict` — the same band word under the name the clearance lane records it by, so a
1094
1094
  // reader of either lane's status finds the rating in one place. `verdict` stays as it was.
1095
- writeRunStatus(ctx, { state: "delivered", verdict: overall, tier: overall, statement: published.statement, url: published.url, reports: published.reports.map((r) => ({ mark: r.mark, url: r.url })), deliveredAt, sendPending: true, stepIndex: STEPS.length - 1, stepLabel: STEPS[STEPS.length - 1], stepN: STEPS.length, stepTotal: STEPS.length });
1095
+ writeRunStatus(ctx, { state: "delivered", tier: overall, statement: published.statement, url: published.url, reports: published.reports.map((r) => ({ mark: r.mark, url: r.url })), deliveredAt, sendPending: true, stepIndex: STEPS.length - 1, stepLabel: STEPS[STEPS.length - 1], stepN: STEPS.length, stepTotal: STEPS.length });
1096
1096
  // — the knockout lane's pool copy learns its terminal state the same way,
1097
1097
  // for the same reason: publish returns the pool dir, and `state: "delivered"` is decided after it
1098
1098
  // returns. Same seam, same best-effort contract, no lane-specific exception to write down.
1099
- const stamp = writeSettleStamp(published.poolRunDir, { state: "delivered", verdict: overall, deliveredAt, runId: published.runId ?? run.runId, lane: "knockout" });
1099
+ const stamp = writeSettleStamp(published.poolRunDir, { state: "delivered", tier: overall, deliveredAt, runId: published.runId ?? run.runId, lane: "knockout" });
1100
1100
  if (!stamp.written) note(`delivery: settle stamp not written (${stamp.reason})`);
1101
1101
  const archived = archive(run);
1102
1102
  rollupStatus(run.studioRoot);