clearotron 0.4.1 → 0.5.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 (104) hide show
  1. package/README.md +1 -0
  2. package/THIRD-PARTY-NOTICES.md +1 -1
  3. package/bin/start.mjs +33 -8
  4. package/build-info.json +2 -2
  5. package/driver/CHANGELOG.md +32 -0
  6. package/driver/ask-ledger.mjs +90 -16
  7. package/driver/band-shape.mjs +65 -11
  8. package/driver/band-size.mjs +10 -0
  9. package/driver/close-verify.mjs +29 -2
  10. package/driver/common-law-coverage-status.mjs +1 -1
  11. package/driver/connotation-search.mjs +7 -9
  12. package/driver/contract-e3-backlog.mjs +34 -34
  13. package/driver/contract-e3-baseline.json +5 -5
  14. package/driver/contract-vocabulary.mjs +12 -12
  15. package/driver/corrections-delivered.mjs +113 -0
  16. package/driver/corrections-feedforward.mjs +16 -6
  17. package/driver/corrective-arm.mjs +11 -8
  18. package/driver/coverage-form-io.mjs +2 -1
  19. package/driver/coverage-form.mjs +17 -2
  20. package/driver/coverage-union.mjs +11 -2
  21. package/driver/digest-queue.mjs +1 -1
  22. package/driver/disposition-call.mjs +1 -1
  23. package/driver/document-coverage.mjs +3 -2
  24. package/driver/effective-scope.mjs +12 -7
  25. package/driver/engine/mcp/band-server.mjs +77 -13
  26. package/driver/engine/mcp/dispositions-server.mjs +2 -2
  27. package/driver/engine/mcp/held-answers.mjs +128 -0
  28. package/driver/engine/mcp/recording-server.mjs +10 -6
  29. package/driver/engine/mcp/supplemental.mjs +74 -12
  30. package/driver/findings-model.mjs +16 -4
  31. package/driver/floor-duty.mjs +9 -0
  32. package/driver/gate-metrics.mjs +7 -1
  33. package/driver/gateway.mjs +28 -14
  34. package/driver/goods-evidence.mjs +295 -0
  35. package/driver/knockout-assess-record.mjs +1 -1
  36. package/driver/narrative-refutation-record.mjs +40 -2
  37. package/driver/package.json +1 -1
  38. package/driver/pipeline.mjs +1994 -323
  39. package/driver/placement-decisions.mjs +243 -0
  40. package/driver/placement-form.mjs +42 -8
  41. package/driver/placement-union.mjs +14 -6
  42. package/driver/provider-usage.mjs +10 -7
  43. package/driver/publish/audit-from-spine.mjs +5 -2
  44. package/driver/publish/index.mjs +19 -4
  45. package/driver/publish/xlsx.mjs +6 -6
  46. package/driver/recall-reconciliation.mjs +15 -5
  47. package/driver/record-carry.mjs +70 -6
  48. package/driver/record-discard.mjs +7 -1
  49. package/driver/record-fold.mjs +14 -0
  50. package/driver/record-goods.mjs +101 -5
  51. package/driver/register-plan.mjs +221 -6
  52. package/driver/registry-fidelity.mjs +34 -3
  53. package/driver/remedy-accounting.mjs +52 -14
  54. package/driver/repair-composers.mjs +265 -105
  55. package/driver/repair-scope.mjs +327 -0
  56. package/driver/repair-targets.mjs +92 -0
  57. package/driver/result-noun-fields.mjs +3 -3
  58. package/driver/scope-facts.mjs +6 -2
  59. package/driver/screen-gate.mjs +96 -0
  60. package/driver/skills/clearance-common-law/SKILL.md +8 -0
  61. package/driver/skills/clearance-register/SKILL.md +29 -21
  62. package/driver/skills/clearance-register/digest.md +22 -17
  63. package/driver/skills/clearance-register/providers/clarivate.md +4 -4
  64. package/driver/skills/clearance-register/providers/corsearch.md +1 -1
  65. package/driver/skills/clearance-register/providers/euipo.md +4 -4
  66. package/driver/skills/clearance-register/providers/free-tier.md +4 -4
  67. package/driver/skills/clearance-register/providers/uspto-local.md +3 -3
  68. package/driver/skills/clearance-register/register-recipes.md +6 -6
  69. package/driver/skills/clearance-register/unit.md +29 -22
  70. package/driver/skills/clearance-search/delivery-contract.md +2 -58
  71. package/driver/skills/clearance-search/report-prose.md +12 -3
  72. package/driver/skills/clearance-search/risk-framework-triage.md +1 -1
  73. package/driver/skills/clearance-search/synthesis-rules.md +10 -13
  74. package/driver/skills/clearance-variants/SKILL.md +4 -4
  75. package/driver/skills/knockout-assess/SKILL.md +9 -14
  76. package/driver/skills/knockout-frame/SKILL.md +5 -5
  77. package/driver/skills/narrative-refutation/SKILL.md +1 -1
  78. package/driver/skills/placement-inquiry/SKILL.md +1 -1
  79. package/driver/skills/whatif-memo/SKILL.md +1 -1
  80. package/driver/stage-context.mjs +2 -1
  81. package/driver/stages-knockout.mjs +1 -1
  82. package/driver/stages.mjs +96 -61
  83. package/driver/suite-census.json +415 -25
  84. package/driver/synthesis-record.mjs +310 -8
  85. package/driver/terminal-clamp.mjs +53 -2
  86. package/driver/verify-knockout.mjs +22 -4
  87. package/driver/verify.mjs +23 -3
  88. package/mcp-server/CHANGELOG.md +8 -0
  89. package/mcp-server/lib/driver.mjs +5 -0
  90. package/mcp-server/lib/events.mjs +25 -4
  91. package/mcp-server/lib/findings.mjs +4 -2
  92. package/mcp-server/package.json +1 -1
  93. package/package.json +1 -1
  94. package/portal-ui/package.json +1 -1
  95. package/providers/_shared/enumerate.mjs +12 -6
  96. package/providers/_shared/execute-plan.mjs +25 -1
  97. package/providers/_shared/follow-up-searches.mjs +139 -0
  98. package/providers/jx/src/core.js +3 -4
  99. package/providers/jx/src/judge.js +2 -2
  100. package/providers/jx/src/nativeread.js +1 -2
  101. package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
  102. package/providers/oauth-mcp-bridge/package.json +1 -1
  103. package/scripts/e2e-first-time.mjs +30 -13
  104. package/scripts/release-await-cut.mjs +4 -2
@@ -0,0 +1,295 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
3
+ // goods-evidence.mjs — what each band record's goods rest on, written down by the run.
4
+ //
5
+ // WHY. The stages that judge goods read them through the band tools: a line's goods words, and on request the
6
+ // official record itself. Those tools say whether a record's goods could be read. They cannot say what the goods
7
+ // rest on, and the difference decides what a goods judgment may conclude:
8
+ //
9
+ // complete the run holds the text whole, on a basis the record names: a full record from an operation
10
+ // whose goods are whole (the record text, the register's own flag on a retrieved record, the
11
+ // bulk register statement), or a search answer's statement shorter than its documented cut;
12
+ // cut the register marked the statement as cut: a missing word proves nothing;
13
+ // unknown goods are held and their completeness is not established: a search answer's unflagged
14
+ // statement at or over the default answer's documented cut, a retrieved record's statement with
15
+ // no flag, a register whose full-record operation the tree documents nowhere, or a held record
16
+ // whose goods field is missing;
17
+ // absent the run holds a full record from such an operation, and it lists no goods;
18
+ // unavailable no goods are held and a fetch of the record failed this run;
19
+ // not-held no goods are held, and nothing was fetched.
20
+ //
21
+ // None of the last five means the goods are unrelated (ruled 2026-10-18). Every state comes from the shared
22
+ // goods reader's one rule (record-goods.mjs `rowGoods`, `recordGoods`, `recordGoodsProvenance`), so the record
23
+ // and the goods test the shape groups on cannot disagree. What cannot be established is not called complete.
24
+ //
25
+ // WHAT IT READS. Nothing a search row holds directly: the goods a record's evidence carries come from the goods
26
+ // reader the band shape is handed (`of`, and `held` once the reader distinguishes a held body), which marks a
27
+ // statement the register cut. The run's own files say which official records it holds and where each came from
28
+ // (the record ledger and `_records/`), and the register call ledger says which fetches failed. The record is
29
+ // written beside the band shape, in `_driver/`, where no band tool serves it: what a stage is shown does not
30
+ // change with it. It carries no clock, so a replay derives it byte for byte.
31
+ //
32
+ // PURE except `heldRecordProvenance` and `readRunCallRows`, which read the run's files.
33
+ import { readFileSync, existsSync } from "node:fs";
34
+ import { normalizeRecordUri, readRecordArtifacts, forEachLedgerLine, recordBodyHasContent } from "./registry-fidelity.mjs";
35
+ import { recordGoodsProvenance } from "./record-goods.mjs";
36
+ import { driverDir } from "../shared/driver-dir.mjs";
37
+ import { runPrefixSpellings } from "../shared/pre-rename-spellings.mjs";
38
+
39
+ export const GOODS_EVIDENCE_SCHEMA_VERSION = 1;
40
+ export const GOODS_EVIDENCE_FILE = "goods-evidence.json";
41
+ export const GOODS_STATES = Object.freeze(["complete", "cut", "unknown", "absent", "unavailable", "not-held"]);
42
+
43
+ const uriKey = (u) => normalizeRecordUri(u);
44
+ // A register's own id for a record, as a fetch row names it where the provider logs its own id rather than the
45
+ // record's address: the address's last segment.
46
+ const idOf = (u) => { const k = normalizeRecordUri(u); return k ? k.slice(k.lastIndexOf("/") + 1) : ""; };
47
+
48
+ /**
49
+ * Which official records the run holds, and where each came from: canonical uri → {via, provider, session, ts,
50
+ * goods}, where `goods` is the shared reader's account of the body (record-goods.mjs `recordGoodsProvenance`:
51
+ * the operation it came from, and whether it supplies the goods field). The record ledger first, then `_records/`,
52
+ * the later copy winning, as the goods reader and `band_record` read them. A body that carries no record is no
53
+ * record held (registry-fidelity.mjs `recordBodyHasContent`). A torn line or an unreadable file contributes
54
+ * nothing. IMPURE (reads the run).
55
+ */
56
+ export function heldRecordProvenance(runDir) {
57
+ const out = new Map();
58
+ const put = (uri, via, facts, body) => {
59
+ const key = uriKey(uri);
60
+ if (!key || !recordBodyHasContent(body)) return;
61
+ const prev = out.get(key);
62
+ out.set(key, { via: [...new Set([...(prev?.via ?? []), via])], provider: facts.provider ?? prev?.provider ?? null,
63
+ session: facts.session ?? prev?.session ?? null, ts: facts.ts ?? prev?.ts ?? null, goods: recordGoodsProvenance(body) });
64
+ };
65
+ let text = "";
66
+ try { text = readFileSync(driverDir(runDir, "register-record-bodies.jsonl"), "utf8"); } catch { text = ""; }
67
+ for (const line of text.split("\n")) {
68
+ if (!line.trim()) continue;
69
+ let row; try { row = JSON.parse(line); } catch { continue; }
70
+ if (!row?.body || typeof row.body !== "object") continue;
71
+ put(row.target, "register-record-bodies", { provider: row.provider ?? null, session: row.sessionKey ?? null, ts: row.ts ?? null }, row.body);
72
+ }
73
+ for (const [uri, body] of readRecordArtifacts(runDir)) {
74
+ const r = body?._receipt;
75
+ put(uri, "_records", { provider: r?.provider ?? null, session: r?.context ?? null, ts: r?.fetched_at ?? null }, body);
76
+ }
77
+ return out;
78
+ }
79
+
80
+ // The run test every reader of the call ledger applies (registry-fidelity.mjs, provider-usage.mjs): a row is this
81
+ // run's when its session key, without the gateway's `agent:<id>:` namespace, starts with the run's prefix.
82
+ const stripNs = (s) => (typeof s === "string" ? s.replace(/^agent:[^:]+:/, "") : "");
83
+ const inRun = (row, runPrefix) => !runPrefix || runPrefixSpellings(runPrefix).some((rp) => stripNs(row?.sessionKey).startsWith(rp)
84
+ || stripNs(row?.sessionId).startsWith(rp));
85
+
86
+ /** This run's rows of the register call ledger. A missing file is no rows; one that cannot be read says so. IMPURE. */
87
+ export function readRunCallRows(callLogPath, runPrefix) {
88
+ const rows = [];
89
+ const read = forEachLedgerLine(callLogPath, (line) => {
90
+ let row; try { row = JSON.parse(line); } catch { return; }
91
+ if (inRun(row, runPrefix)) rows.push(row);
92
+ });
93
+ return { rows, error: read.error };
94
+ }
95
+
96
+ /**
97
+ * The fetches this run made per record, from its call ledger rows: key → {sent, from_memory, failed, ok, last_status}.
98
+ * A record fetch names its record by address or by the register's own id; a batch of full records names each id
99
+ * it carried. Requests sent and answers served from the run's memory are counted apart: one cost the register a
100
+ * request, the other did not. PURE.
101
+ *
102
+ * WHICH RECORD A FETCH ROW NAMES, in the address a band record carries. The registers log it three ways. Most give
103
+ * the address itself. EUIPO gives its application number, which names an EU record, so it is read as one: as a
104
+ * bare id it would also match a record of the same number on another register. Signa gives its own record id,
105
+ * opaque and unique to it, which stays an id. USPTO-local gives the serial it looked up, and says whether the
106
+ * record was found rather than whether the call succeeded, so an unfound record is a failed fetch.
107
+ */
108
+ export function fetchRowKey(row) { // @internal — driven by its test
109
+ if (row?.tool !== "record_fetch") return null;
110
+ if (row.target) {
111
+ const key = uriKey(row.target);
112
+ if (key) return key;
113
+ const bare = String(row.target).trim().toLowerCase();
114
+ return !bare ? null : row.provider === "euipo" ? `/mark/eu/${bare}` : bare;
115
+ }
116
+ return row.provider === "uspto-local" && row.serial ? `/mark/us/${String(row.serial).trim().toLowerCase()}` : null;
117
+ }
118
+ const fetchRowOk = (row) => (typeof row?.ok === "boolean" ? row.ok : typeof row?.found === "boolean" ? row.found : undefined);
119
+ export function recordFetchOutcomes(rows, { runPrefix = null } = {}) {
120
+ const out = new Map();
121
+ for (const row of Array.isArray(rows) ? rows : []) {
122
+ if (!inRun(row, runPrefix)) continue;
123
+ const one = fetchRowKey(row);
124
+ const keys = one ? [one] : Array.isArray(row?.records)
125
+ ? row.records.map((raw) => uriKey(raw) || String(raw ?? "").trim().toLowerCase()) : [];
126
+ for (const key of keys) {
127
+ if (!key) continue;
128
+ const o = out.get(key) ?? { sent: 0, from_memory: 0, failed: 0, ok: false, last_status: null };
129
+ if (row.cache_hit === true) o.from_memory++; else o.sent++;
130
+ const ok = fetchRowOk(row);
131
+ if (ok === false) o.failed++;
132
+ if (ok === true) o.ok = true;
133
+ if (row.http_status !== undefined) o.last_status = row.http_status;
134
+ out.set(key, o);
135
+ }
136
+ }
137
+ return out;
138
+ }
139
+
140
+ const fetchOf = (fetches, uri) => fetches?.get?.(uriKey(uri)) ?? fetches?.get?.(idOf(uri)) ?? null;
141
+
142
+ /**
143
+ * One record's goods evidence: {uri, state, source, basis, readable, held, fetch}. PURE.
144
+ * goods the goods reader: `of(record)` gives the statements the record's evidence holds, a statement the
145
+ * register cut marked `cut: true`; `held(record)`, where given, says whether they are a held body's
146
+ * held heldRecordProvenance's map: which official records the run holds, and where from
147
+ * fetches recordFetchOutcomes's map, or null
148
+ */
149
+ export function recordGoodsEvidence(record, { goods = null, held = null, fetches = null } = {}) {
150
+ const uri = uriKey(record?.record_id) || String(record?.record_id ?? "");
151
+ const body = held?.get?.(uri) ?? null;
152
+ const statements = goods?.of ? goods.of(record) : null;
153
+ const fromBody = typeof goods?.held === "function" ? goods.held(record) : Boolean(statements);
154
+ const fetch = fetchOf(fetches, record?.record_id);
155
+ const readable = statements?.length ? statements.some((g) => g?.en !== false) : null;
156
+ const cut = (statements ?? []).some((g) => g?.cut === true);
157
+ const unknown = (statements ?? []).some((g) => g?.unknown === true);
158
+ const row = (state, source, basis) => ({ uri, state, source, basis, readable, held: body, fetch });
159
+ // The statements are a held body's where the reader says so; a reader that cannot tell reads bodies only.
160
+ if (statements?.length && fromBody) {
161
+ const op = body?.goods?.operation ?? "undocumented";
162
+ return row(cut ? "cut" : unknown ? "unknown" : "complete", "record-body",
163
+ cut ? "register-flag" : unknown ? (op === "retrieve" ? "retrieved-record-without-flag" : "full-record-operation-undocumented")
164
+ : op === "retrieve" ? "register-flag" : op === "bulk-register" ? "bulk-register-statement" : "full-record");
165
+ }
166
+ if (statements?.length) return row(cut ? "cut" : unknown ? "unknown" : "complete", "search-row",
167
+ cut ? "register-flag" : unknown ? "default-answer-at-or-over-its-cut" : "default-answer-under-its-cut");
168
+ if (body) {
169
+ // Lists no goods only where an operation whose goods are whole supplies the field and it is empty. A body with
170
+ // no goods field, or from an operation the tree documents nowhere, establishes nothing about its goods.
171
+ const g = body.goods ?? { operation: "undocumented", field: "none" };
172
+ if (g.field === "empty" && g.operation !== "undocumented") return row("absent", "record-body", "full-record-without-goods");
173
+ return row("unknown", "record-body", g.field === "none" ? "record-without-goods-field" : "full-record-operation-undocumented");
174
+ }
175
+ if (fetch?.failed && !fetch.ok) return row("unavailable", null, "record-fetch-failed");
176
+ return row("not-held", null, "no-goods-held");
177
+ }
178
+
179
+ /**
180
+ * WHAT A RECORD'S GOODS REST ON, IN THE WORDS THE STAGES READ (the band lookup's `goods_from`, the write-up's list):
181
+ * the supporting evidence the record holds and its completeness, never the last call made for it. "No goods listed"
182
+ * needs a full record from an operation whose goods are whole, with the field there and empty; a record whose goods
183
+ * field is missing is "completeness not established". null where there is no evidence row. PURE.
184
+ */
185
+ export function goodsFromLabel(e) {
186
+ if (!e) return null;
187
+ if (e.source === "record-body") return e.state === "absent" ? "official record, no goods listed"
188
+ : e.state === "cut" ? "official record, cut by the register"
189
+ : e.state === "unknown" ? "official record, completeness not established" : "official record";
190
+ if (e.source === "search-row") return e.state === "cut" ? "search answer, cut by the register"
191
+ : e.state === "unknown" ? "search answer, completeness not established" : "search answer";
192
+ return e.state === "unavailable" ? "fetch failed" : "nothing held";
193
+ }
194
+
195
+ /** Records counted by state, by source and by basis; `unreadable` counts goods held in no language the engine reads. PURE. */
196
+ export function goodsEvidenceTotals(rows) {
197
+ const list = Array.isArray(rows) ? rows : [];
198
+ const by_state = Object.fromEntries(GOODS_STATES.map((s) => [s, 0]));
199
+ const by_source = {}, by_basis = {};
200
+ let unreadable = 0;
201
+ for (const r of list) {
202
+ by_state[r.state] = (by_state[r.state] ?? 0) + 1;
203
+ const s = r.source ?? "none";
204
+ by_source[s] = (by_source[s] ?? 0) + 1;
205
+ by_basis[r.basis] = (by_basis[r.basis] ?? 0) + 1;
206
+ if (r.readable === false) unreadable++;
207
+ }
208
+ return { records: list.length, by_state, by_source, by_basis, unreadable };
209
+ }
210
+
211
+ /** The record for a band: one row per record, in the band's order, and the totals. No clock. PURE. */
212
+ export function deriveGoodsEvidence({ records = [], goods = null, held = null, fetches = null } = {}) {
213
+ const rows = (Array.isArray(records) ? records : []).filter((r) => r?.record_id).map((r) => recordGoodsEvidence(r, { goods, held, fetches }));
214
+ return { schema_version: GOODS_EVIDENCE_SCHEMA_VERSION, totals: goodsEvidenceTotals(rows), records: rows };
215
+ }
216
+
217
+ /** The record the run wrote, or null when there is none or it will not parse. IMPURE. */
218
+ export function readGoodsEvidence(runDir) {
219
+ const p = driverDir(runDir, GOODS_EVIDENCE_FILE);
220
+ if (!existsSync(p)) return null;
221
+ try { return JSON.parse(readFileSync(p, "utf8")); } catch { return null; }
222
+ }
223
+
224
+ /**
225
+ * WHAT THE STAGES READ, per record, from the band tools' reading log: canonical uri → {opened, shown, missed}, each
226
+ * a set of session keys. `opened` is a `band_record` that served the record; `missed`, one that could not; `shown`,
227
+ * a lookup that served the record's row with its goods words. Only the band tools' own rows count; the lines a part
228
+ * of the shape or a page of a group served are `linesRead`'s. PURE.
229
+ */
230
+ export function recordReads(readingLogText) {
231
+ const out = new Map();
232
+ const at = (uri) => {
233
+ const key = uriKey(uri);
234
+ if (!key) return null;
235
+ let e = out.get(key);
236
+ if (!e) { e = { opened: new Set(), shown: new Set(), answered: new Set(), missed: new Set() }; out.set(key, e); }
237
+ return e;
238
+ };
239
+ for (const line of String(readingLogText ?? "").split("\n")) {
240
+ if (!line.trim()) continue;
241
+ let row; try { row = JSON.parse(line); } catch { continue; }
242
+ const session = String(row?.session ?? "unattributed");
243
+ // A record tool's miss that served the goods the register's search answer listed is a read of those goods, not
244
+ // a miss: the stage was given their text, though no official record was on file.
245
+ if (row?.tool === "band_record") at(row?.args?.record_id)?.[row.ok === true ? "opened" : row.served === "search-answer-goods" ? "answered" : "missed"].add(session);
246
+ else if (row?.tool === "band_lookup" && row.ok === true) for (const id of Array.isArray(row.served) ? row.served : []) at(id)?.shown.add(session);
247
+ }
248
+ return out;
249
+ }
250
+
251
+ /**
252
+ * WHAT THE STAGES WERE SHOWN BY LINE, from the same log: band shape line key → {band_shape, group_page}, each a set
253
+ * of session keys. A part of the shape logs the lines printed in it, and a page of a group the lines it returned;
254
+ * both print each line's goods words. Joined to a record through the line it sits on. PURE.
255
+ */
256
+ export function linesRead(readingLogText) {
257
+ const out = new Map();
258
+ for (const line of String(readingLogText ?? "").split("\n")) {
259
+ if (!line.trim()) continue;
260
+ let row; try { row = JSON.parse(line); } catch { continue; }
261
+ if (row?.ok !== true || !Array.isArray(row.served_lines)) continue;
262
+ const via = row.tool === "band_shape" ? "band_shape" : row.tool === "band_lookup" ? "group_page" : null;
263
+ if (!via) continue;
264
+ const session = String(row?.session ?? "unattributed");
265
+ for (const key of row.served_lines) {
266
+ if (typeof key !== "string" || !key) continue;
267
+ let e = out.get(key);
268
+ if (!e) { e = { band_shape: new Set(), group_page: new Set() }; out.set(key, e); }
269
+ e[via].add(session);
270
+ }
271
+ }
272
+ return out;
273
+ }
274
+
275
+ /**
276
+ * Register calls counted four ways, never folded into one: requests sent to the register, answers served from the
277
+ * run's memory (asked, never sent), credits as the register stated them (a request with no figure is counted as
278
+ * such, never as free), and the characters each answer held, which is what the call ledger's `bytes` measures.
279
+ * PURE.
280
+ */
281
+ export function registerCallAccount(rows) {
282
+ const out = { requests_sent: 0, failed: 0, answered_from_memory: 0,
283
+ credits: { charged: 0, requests_with_a_figure: 0, requests_without_a_figure: 0 }, response_characters: { sent: 0, from_memory: 0 } };
284
+ for (const row of Array.isArray(rows) ? rows : []) {
285
+ const chars = Number(row?.bytes) || 0;
286
+ if (row?.cache_hit === true) { out.answered_from_memory++; out.response_characters.from_memory += chars; continue; }
287
+ out.requests_sent++;
288
+ out.response_characters.sent += chars;
289
+ if (row?.ok === false) out.failed++;
290
+ const credits = row?.credits_charged;
291
+ if (typeof credits === "number" && Number.isFinite(credits)) { out.credits.charged += credits; out.credits.requests_with_a_figure++; }
292
+ else out.credits.requests_without_a_figure++;
293
+ }
294
+ return out;
295
+ }
@@ -271,7 +271,7 @@ export function acceptKnockoutAssess(params, { boundOrdinal = null } = {}) {
271
271
 
272
272
  const chunkSummary = str(params?.chunkSummary);
273
273
  if (!chunkSummary)
274
- return { ok: false, reason: "knockoutassess_summary_missing: chunkSummary (2–5 sentences covering THIS chunk's marks) is required — code composes the batch executive summary from the chunks, so a chunk without one vanishes from the summary" };
274
+ return { ok: false, reason: "knockoutassess_summary_missing: chunkSummary (the cross-mark read covering THIS chunk's marks) is required — code composes the batch executive summary from the chunks, so a chunk without one vanishes from the summary" };
275
275
 
276
276
  if (chunkNo === 0 && !str(params?.batch?.productContext))
277
277
  return { ok: false, reason: "knockoutassess_batch_missing: chunk 0 carries the batch object (productContext at minimum) — it is the only chunk that does, and the merged artifact takes it from there" };
@@ -34,8 +34,8 @@
34
34
  // `n` is the render's, taken from array order. A seat-supplied number is a value that can disagree with
35
35
  // the list it labels — duplicated, skipped, or renumbered by a corrective pass — and nothing downstream
36
36
  // would notice. Removing the field removes the defect (the doubt-closure `file_index` rule).
37
- import { writeFileSync, mkdirSync, existsSync, readFileSync } from "node:fs";
38
- import { captureCall, mergeCapture } from "./call-capture.mjs";
37
+ import { writeFileSync, mkdirSync, existsSync, readFileSync, readdirSync } from "node:fs";
38
+ import { captureCall, mergeCapture, nextSeq } from "./call-capture.mjs";
39
39
  import { join } from "node:path";
40
40
  import { driverDir } from "../shared/driver-dir.mjs";
41
41
  import { parseVerdict, countCitedDefects, parseCorrectionKinds, correctionFlagContent, CORRECTION_KINDS } from "./verify.mjs";
@@ -276,6 +276,44 @@ export function readAcceptedFlags(runDir) {
276
276
  return Array.isArray(flags) ? flags : null;
277
277
  }
278
278
 
279
+ /**
280
+ * The sequence number the next review call will be captured under. A dispatch notes it BEFORE it runs, so the
281
+ * calls that dispatch makes are exactly those numbered from it on. Never throws.
282
+ */
283
+ export function nextRefutationSeq(runDir) {
284
+ const { dir } = refutationCallPaths(String(runDir ?? ""));
285
+ const nameFor = (seq) => join(dir, `call-${String(seq).padStart(3, "0")}.json`);
286
+ let names = [];
287
+ try { names = readdirSync(dir); } catch { names = []; }
288
+ return nextSeq(names, nameFor) ?? 1000;
289
+ }
290
+
291
+ /**
292
+ * THE LAST ACCEPTED REVIEW CALL FROM `seq0` ON, AS TYPED: `{seq, verdict, flags, planAudit}`, or null.
293
+ *
294
+ * The review after a delivery-check repair is a later call than the first review's, so `readAcceptedFlags`
295
+ * (which opens the first call) cannot read it. A refused or unfinished call is skipped, as it is there: null
296
+ * means there is no review from that dispatch the driver can act on, and its caller says so rather than
297
+ * reading anything else in its place. Never throws.
298
+ */
299
+ export function readAcceptedReviewSince(runDir, seq0) {
300
+ const { dir } = refutationCallPaths(String(runDir ?? ""));
301
+ let names = [];
302
+ try { names = readdirSync(dir); } catch { return null; }
303
+ const calls = names.map((n) => /^call-(\d+)\.json$/.exec(n)).filter(Boolean)
304
+ .map((m) => ({ seq: Number(m[1]), file: join(dir, m[0]) }))
305
+ .filter((c) => c.seq >= Number(seq0))
306
+ .sort((a, b) => b.seq - a.seq);
307
+ for (const c of calls) {
308
+ let doc;
309
+ try { doc = JSON.parse(readFileSync(c.file, "utf8")); } catch { continue; }
310
+ if (doc?.accepted !== true) continue;
311
+ const p = doc?.params ?? {};
312
+ return { seq: c.seq, verdict: p.verdict ?? null, flags: Array.isArray(p.flags) ? p.flags : [], planAudit: p.plan_audit ?? null };
313
+ }
314
+ return null;
315
+ }
316
+
279
317
  /** Read back what was written — the verdict view of the stored file, through the one parse. */
280
318
  export function readRecordedVerdict(runDir) {
281
319
  try { return parseVerdict(readFileSync(join(String(runDir ?? ""), REVIEW_FILE), "utf8")); }
@@ -2,7 +2,7 @@
2
2
  "name": "clearotron-driver",
3
3
  "private": true,
4
4
  "type": "module",
5
- "version": "0.4.1",
5
+ "version": "0.5.0-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": {