clearotron 0.4.1 → 0.5.0-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.
- package/README.md +1 -0
- package/THIRD-PARTY-NOTICES.md +1 -1
- package/bin/start.mjs +33 -8
- package/build-info.json +2 -2
- package/driver/CHANGELOG.md +23 -0
- package/driver/ask-ledger.mjs +86 -15
- package/driver/band-shape.mjs +61 -11
- package/driver/band-size.mjs +10 -0
- package/driver/close-verify.mjs +29 -2
- package/driver/common-law-coverage-status.mjs +1 -1
- package/driver/connotation-search.mjs +7 -9
- package/driver/contract-e3-backlog.mjs +34 -34
- package/driver/contract-e3-baseline.json +5 -5
- package/driver/contract-vocabulary.mjs +12 -12
- package/driver/corrections-delivered.mjs +113 -0
- package/driver/corrections-feedforward.mjs +16 -6
- package/driver/corrective-arm.mjs +11 -8
- package/driver/coverage-form.mjs +3 -0
- package/driver/coverage-union.mjs +11 -2
- package/driver/digest-queue.mjs +1 -1
- package/driver/disposition-call.mjs +1 -1
- package/driver/document-coverage.mjs +3 -2
- package/driver/effective-scope.mjs +2 -2
- package/driver/engine/mcp/band-server.mjs +77 -13
- package/driver/engine/mcp/dispositions-server.mjs +2 -2
- package/driver/engine/mcp/recording-server.mjs +10 -6
- package/driver/findings-model.mjs +16 -4
- package/driver/floor-duty.mjs +9 -0
- package/driver/gate-metrics.mjs +7 -1
- package/driver/gateway.mjs +28 -14
- package/driver/goods-evidence.mjs +295 -0
- package/driver/knockout-assess-record.mjs +1 -1
- package/driver/narrative-refutation-record.mjs +40 -2
- package/driver/package.json +1 -1
- package/driver/pipeline.mjs +1945 -314
- package/driver/placement-decisions.mjs +243 -0
- package/driver/placement-form.mjs +42 -8
- package/driver/placement-union.mjs +14 -6
- package/driver/provider-usage.mjs +10 -7
- package/driver/publish/audit-from-spine.mjs +5 -2
- package/driver/publish/index.mjs +19 -4
- package/driver/publish/xlsx.mjs +6 -6
- package/driver/recall-reconciliation.mjs +15 -5
- package/driver/record-carry.mjs +70 -6
- package/driver/record-discard.mjs +7 -1
- package/driver/record-fold.mjs +14 -0
- package/driver/record-goods.mjs +101 -5
- package/driver/register-plan.mjs +89 -0
- package/driver/registry-fidelity.mjs +34 -3
- package/driver/remedy-accounting.mjs +52 -14
- package/driver/repair-composers.mjs +265 -105
- package/driver/repair-scope.mjs +327 -0
- package/driver/repair-targets.mjs +92 -0
- package/driver/result-noun-fields.mjs +3 -3
- package/driver/screen-gate.mjs +96 -0
- package/driver/skills/clearance-common-law/SKILL.md +8 -0
- package/driver/skills/clearance-register/SKILL.md +5 -5
- package/driver/skills/clearance-register/digest.md +14 -14
- package/driver/skills/clearance-register/unit.md +3 -3
- package/driver/skills/clearance-search/delivery-contract.md +2 -58
- package/driver/skills/clearance-search/report-prose.md +12 -3
- package/driver/skills/clearance-search/risk-framework-triage.md +1 -1
- package/driver/skills/clearance-search/synthesis-rules.md +10 -13
- package/driver/skills/clearance-variants/SKILL.md +4 -4
- package/driver/skills/knockout-assess/SKILL.md +9 -14
- package/driver/skills/knockout-frame/SKILL.md +5 -5
- package/driver/skills/narrative-refutation/SKILL.md +1 -1
- package/driver/skills/placement-inquiry/SKILL.md +1 -1
- package/driver/skills/whatif-memo/SKILL.md +1 -1
- package/driver/stage-context.mjs +2 -1
- package/driver/stages-knockout.mjs +1 -1
- package/driver/stages.mjs +95 -60
- package/driver/suite-census.json +367 -19
- package/driver/synthesis-record.mjs +310 -8
- package/driver/terminal-clamp.mjs +53 -2
- package/driver/verify-knockout.mjs +22 -4
- package/driver/verify.mjs +23 -3
- package/mcp-server/CHANGELOG.md +4 -0
- package/mcp-server/lib/driver.mjs +5 -0
- package/mcp-server/lib/events.mjs +25 -4
- package/mcp-server/lib/findings.mjs +4 -2
- package/mcp-server/package.json +1 -1
- package/package.json +1 -1
- package/portal-ui/package.json +1 -1
- package/providers/jx/src/core.js +3 -4
- package/providers/jx/src/judge.js +2 -2
- package/providers/jx/src/nativeread.js +1 -2
- package/providers/oauth-mcp-bridge/CHANGELOG.md +4 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/scripts/e2e-first-time.mjs +29 -12
|
@@ -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 (
|
|
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")); }
|
package/driver/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "clearotron-driver",
|
|
3
3
|
"private": true,
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.5.0-beta.0",
|
|
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": {
|