clearotron 0.4.0-beta.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/THIRD-PARTY-NOTICES.md +5 -5
  2. package/build-info.json +2 -2
  3. package/driver/CHANGELOG.md +90 -0
  4. package/driver/ask-ledger.mjs +2 -2
  5. package/driver/coverage-form.mjs +6 -0
  6. package/driver/coverage-ledger.mjs +14 -2
  7. package/driver/driver.config.mjs +24 -7
  8. package/driver/engine/mcp/band-server.mjs +3 -3
  9. package/driver/engine/mcp/stdio-server.mjs +12 -1
  10. package/driver/gateway.mjs +27 -3
  11. package/driver/package.json +1 -1
  12. package/driver/pipeline-knockout.mjs +41 -10
  13. package/driver/pipeline.mjs +104 -12
  14. package/driver/progress.mjs +6 -1
  15. package/driver/provider-usage.mjs +16 -0
  16. package/driver/publish/index.mjs +11 -5
  17. package/driver/publish/knockout.mjs +16 -3
  18. package/driver/publish/render-knockout.mjs +38 -11
  19. package/driver/record-carry.mjs +17 -1
  20. package/driver/reference-strip-signatures.mjs +11 -1
  21. package/driver/register-plan.mjs +7 -3
  22. package/driver/register-served.mjs +91 -0
  23. package/driver/remedy-accounting.mjs +38 -9
  24. package/driver/reviewer-open-points.mjs +20 -23
  25. package/driver/score-redaction.mjs +416 -20
  26. package/driver/screen-gate.mjs +3 -3
  27. package/driver/skills/clearance-register/SKILL.md +0 -8
  28. package/driver/skills/clearance-register/digest.md +1 -1
  29. package/driver/skills/clearance-register/providers/corsearch.md +1 -1
  30. package/driver/skills/clearance-register/register-recipes.md +3 -9
  31. package/driver/skills/clearance-search/SKILL.md +2 -2
  32. package/driver/skills/clearance-search/phase2-execution.md +1 -1
  33. package/driver/skills/knockout-assess/SKILL.md +2 -0
  34. package/driver/stages-knockout.mjs +22 -0
  35. package/driver/suite-census.json +152 -26
  36. package/driver/unit-inventory.mjs +38 -43
  37. package/mcp-server/CHANGELOG.md +8 -0
  38. package/mcp-server/package.json +1 -1
  39. package/node_modules/brace-expansion/index.js +78 -22
  40. package/node_modules/brace-expansion/package.json +1 -1
  41. package/node_modules/readdir-glob/node_modules/brace-expansion/index.js +78 -22
  42. package/node_modules/readdir-glob/node_modules/brace-expansion/package.json +1 -1
  43. package/package.json +1 -1
  44. package/portal-ui/package.json +2 -2
  45. package/providers/_shared/term-shape.mjs +1 -1
  46. package/providers/jx/src/core.js +0 -1
  47. package/providers/jx/src/judge.js +0 -1
  48. package/providers/jx/src/nativeread.js +0 -1
  49. package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
  50. package/providers/oauth-mcp-bridge/package.json +1 -1
  51. package/providers/signa/src/core.js +125 -17
  52. package/scripts/e2e-first-time.mjs +12 -4
  53. package/scripts/e2e-scenario-ops.mjs +25 -2
  54. package/scripts/e2e.mjs +246 -24
  55. package/scripts/mint-reference-strip-backlog.mjs +36 -2
  56. package/scripts/score.mjs +102 -29
  57. package/shared/identifier-scan.mjs +31 -1
@@ -0,0 +1,91 @@
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
+ //
4
+ // register-served.mjs — which register actually served this run, as a field in the run's own record.
5
+ //
6
+ // THE KNOCKOUT LANE ALREADY RECORDS THIS and the clearance lane recorded nothing equivalent, so the same
7
+ // question about a clearance run had no answer to look up. What stood in for the field was searching for a
8
+ // register's NAME as a substring inside three artifacts that happen to mention it — and measured on the
9
+ // archived runs that instrument could not attribute more than half of two scenarios' runs to any register
10
+ // at all. Those are runs whose register is unknown to the record, not runs that used none, and nothing in
11
+ // the record said which.
12
+ //
13
+ // WHAT SERVED, NOT WHAT WAS CONFIGURED. The value is taken from the same resolver the dispatch itself goes
14
+ // through, at the moment a register is used, rather than from the frozen settings the run started on. The
15
+ // per-run settings sidecar is deliberately NOT the home for it: that file exists to freeze what the run
16
+ // was sold, so a resume runs on what it started on, and a register recorded there would be the launch
17
+ // value presented with a frozen file's authority. A run whose provider changed under it would read wrong
18
+ // and confident, which is the failure this field exists to end rather than to relocate.
19
+ //
20
+ // A LIST, BECAUSE ONE IS A LIST OF ONE. A run that used two registers says two rather than naming the
21
+ // first, and a reader never has to know whether the field is a value or a set.
22
+ //
23
+ // BEST-EFFORT AND SILENT, like every other telemetry write in this lane: a field that cannot be recorded
24
+ // must never fail a register call. An absence is then the honest answer — this run's register is unknown
25
+ // to the record — which is exactly the state the archive is full of and the reason nothing here guesses.
26
+ import { readRunStatus, writeRunStatus } from "./progress.mjs";
27
+
28
+ /** The status key. One name, so the writer and every reader cannot drift. */
29
+ export const REGISTERS_SERVED = "registersServed";
30
+
31
+ // Per-process memo so the common case — every call of a run resolving the same register — costs one read
32
+ // and one write for the whole run rather than one of each per call. Keyed by run directory because a
33
+ // process can carry more than one (a resume, a batch).
34
+ const seen = new Map();
35
+
36
+ /**
37
+ * Note that `id` served this run, and return the run's full list. Writes only when the set changes.
38
+ * `id` is the resolver's own answer, never a raw environment read. PURE apart from the run's record.
39
+ */
40
+ export function noteRegisterServed(runDir, id) {
41
+ const register = String(id ?? "").trim().toLowerCase();
42
+ if (!runDir || !register) return null;
43
+ let set = seen.get(runDir);
44
+ if (!set) {
45
+ // A RESUME READS WHAT THE EARLIER PROCESS RECORDED, so a run that changed register across a resume
46
+ // says both rather than only the half this process saw.
47
+ set = new Set(registersServedFrom(readRunStatus(runDir)));
48
+ seen.set(runDir, set);
49
+ }
50
+ if (set.has(register)) return [...set];
51
+ set.add(register);
52
+ const registers = [...set];
53
+ try { writeRunStatus(null, { [REGISTERS_SERVED]: registers }, runDir); }
54
+ catch { /* telemetry must never break a register call */ }
55
+ return registers;
56
+ }
57
+
58
+ /** The registers a run's status records, or [] when it records none. PURE. */
59
+ export function registersServedFrom(status) {
60
+ const v = status?.[REGISTERS_SERVED];
61
+ return Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.trim()) : [];
62
+ }
63
+
64
+ /**
65
+ * One line for a reader, three-valued rather than two: the register, the registers, or the absence NAMED
66
+ * as an absence. "not recorded" is not "none served" and a reader must never have to guess which.
67
+ * PURE.
68
+ */
69
+ export function registerServedLine(status) {
70
+ const r = registersServedFrom(status);
71
+ if (!r.length) return "NOT RECORDED — this run's record carries no register, which is not the same fact as using none";
72
+ return r.length === 1 ? r[0] : `${r.length} registers served this run: ${r.join(", ")}`;
73
+ }
74
+
75
+ /**
76
+ * The caveat a whole-run provider tally needs when the run served more than one register: `{}` when the
77
+ * tally's single key cannot be wrong, and the spanning list when it can. PURE.
78
+ *
79
+ * A TALLY'S KEY IS RESOLVED AT PUBLISH and the tally spans the whole run, so on a run whose register
80
+ * changed part-way every call is filed under whichever was active at the end. That is the shape the
81
+ * served field exists to end, and until the field existed nothing could contradict it. This does not
82
+ * split the tally — attributing a whole-run count to one of two registers is a different question — it
83
+ * stops the two register fields in one record disagreeing in silence.
84
+ */
85
+ export function providerUsageCaveat(served) {
86
+ const r = Array.isArray(served) ? served.filter((x) => typeof x === "string" && x.trim()) : [];
87
+ return r.length > 1 ? { providerUsageSpans: r } : {};
88
+ }
89
+
90
+ /** Test seam: forget the per-process memo, so an arm can drive a second run in the same process. */
91
+ export function forgetRegistersServed() { seen.clear(); }
@@ -136,15 +136,40 @@ function material(block) {
136
136
  * `unaccounted` when there is no reason either — a legacy receipt states neither, and inventing
137
137
  * "not dispatched" from silence would be the same guess in a new place);
138
138
  * 2. ANY slice that did not land clean ⇒ `dispatch-failed`, naming the first such qid. Every slice
139
- * must land, exactly as verifyRegisterDirectiveClose requires of a directive — one convention;
139
+ * must land, exactly as verifyRegisterDirectiveClose requires of a directive — one convention. A
140
+ * slice the FOLD refused as already-asked is read through the twin it duplicates first, because that
141
+ * slice cannot land and the twin is where its question was answered;
140
142
  * 3. all landed, ANY slice material ⇒ `found`;
141
143
  * 4. all landed, EVERY slice a counted zero ⇒ `searched-empty`;
142
144
  * 5. otherwise ⇒ `unaccounted`, with the shape that defeated the join stated.
143
145
  * PURE.
144
146
  */
145
- export function classifyRemedyTerm(row, { blocksByQid = new Map(), executedQids = new Set() } = {}) {
147
+ export function classifyRemedyTerm(row, { blocksByQid = new Map(), executedQids = new Set(), foldRefusals = new Map() } = {}) {
146
148
  const qids = (Array.isArray(row?.qids) ? row.qids : []).filter(Boolean);
147
- const slices = qids.map((q) => sliceRow(q, blocksByQid.get(q), executedQids.has(q)));
149
+ // — A SLICE THE FOLD REFUSED AS ALREADY-ASKED IS ANSWERED BY THE ROW IT DUPLICATES.
150
+ //
151
+ // The fold refuses a minted qid when the plan already holds a row asking the same question, and hands
152
+ // back the twin's qid. Such a slice can NEVER land: it was never frozen into the plan, and the reopen's
153
+ // own re-attempt filters it out on purpose. Requiring it to land anyway made rule 2 below fire on it and
154
+ // return before rules 3-5 could look at the rest, so a term whose question HAD been asked and answered
155
+ // was recorded `dispatch-failed` — which reads as an engine fault a re-run would fix, where the truth is
156
+ // that the coverage is already on the plan's own row. Measured on a delivered breadth run: one term, its
157
+ // twin enumerated with 71 records, the dominant-element gap left open and clamped on that basis.
158
+ //
159
+ // ONLY THE DUPLICATE KIND RESOLVES. An identity collision or a malformed term is a genuine fault and
160
+ // stays `dispatch-failed`: nothing else asked those questions, so nothing else answers them.
161
+ const resolve = (q) => {
162
+ const r = foldRefusals.get?.(q);
163
+ return r && r.kind === "duplicate-question" && r.twin ? String(r.twin) : q;
164
+ };
165
+ const landsAs = new Map(qids.map((q) => [q, resolve(q)]));
166
+ const blockFor = (q) => blocksByQid.get(landsAs.get(q) ?? q);
167
+ const ranAs = (q) => executedQids.has(landsAs.get(q) ?? q);
168
+ const slices = qids.map((q) => {
169
+ const via = landsAs.get(q);
170
+ const slice = sliceRow(q, blockFor(q), ranAs(q));
171
+ return via && via !== q ? { ...slice, answered_by: via, refused_at_fold: "duplicate-question" } : slice;
172
+ });
148
173
  if (!qids.length) {
149
174
  const reason = String(row?.dispatch_reason ?? "").trim();
150
175
  return reason
@@ -152,16 +177,16 @@ export function classifyRemedyTerm(row, { blocksByQid = new Map(), executedQids
152
177
  : { class: "unaccounted", basis: "no-mapping", reason: "the receipt records no slice and no reason for this term — the join cannot say whether it was searched", slices };
153
178
  }
154
179
  for (const q of qids) {
155
- const b = blocksByQid.get(q);
156
- if (sliceLanded(b, executedQids.has(q))) continue;
157
- const why = !executedQids.has(q) ? "slice-not-landed"
180
+ const b = blockFor(q);
181
+ if (sliceLanded(b, ranAs(q))) continue;
182
+ const why = !ranAs(q) ? "slice-not-landed"
158
183
  : !b ? "no-band-block"
159
184
  : b.deferred === true ? "capability-gap-deferral"
160
185
  : b.error === true ? "provider-error"
161
186
  : "collapsed-slice";
162
187
  return { class: "dispatch-failed", basis: why, reason: `${why}:${q}${b?.reason ? ` — ${clip(b.reason, 200)}` : ""}`, slices };
163
188
  }
164
- const blocks = qids.map((q) => blocksByQid.get(q));
189
+ const blocks = qids.map((q) => blockFor(q));
165
190
  if (blocks.some(material)) return { class: "found", basis: "band-block", reason: null, slices };
166
191
  if (blocks.every(countedZero)) return { class: "searched-empty", basis: "counted-zero", reason: null, slices };
167
192
  const odd = blocks.find((b) => !countedZero(b));
@@ -186,14 +211,18 @@ const emptyTotals = () => {
186
211
  * `out_of_scope` carries the directives that have no term unit (source-layer channels), with the reason
187
212
  * — counted and named, never silently absent.
188
213
  *
214
+ * `foldRefusals` maps a minted qid the FOLD refused to `{ kind, twin }`, as `foldSupplementalEntries`
215
+ * recorded it. It is how a term whose slice was refused as already-asked reads through the plan row that
216
+ * answered it instead of counting as a slice that failed to land.
217
+ *
189
218
  * Deterministic; no timestamps (the caller stamps `ts`), no IO, no judgment. PURE.
190
219
  */
191
- export function accountRemedyTerms({ terms = [], blocksByQid = new Map(), executedQids = new Set(), outOfScope = [] } = {}) {
220
+ export function accountRemedyTerms({ terms = [], blocksByQid = new Map(), executedQids = new Set(), foldRefusals = new Map(), outOfScope = [] } = {}) {
192
221
  const totals = emptyTotals();
193
222
  const byDirective = {};
194
223
  const rows = [];
195
224
  for (const t of Array.isArray(terms) ? terms : []) {
196
- const c = classifyRemedyTerm(t, { blocksByQid, executedQids });
225
+ const c = classifyRemedyTerm(t, { blocksByQid, executedQids, foldRefusals });
197
226
  const directive = String(t?.directive ?? "");
198
227
  totals.terms++;
199
228
  totals[key(c.class)]++;
@@ -3,31 +3,28 @@
3
3
  //
4
4
  // reviewer-open-points.mjs — where the reviewer's open points are kept, and how the audit workbook reads them.
5
5
  //
6
- // Owner ruling, 2026-09-24: reviewer notes never reach the client page. The pipeline still builds the
7
- // section from the review and the corrective observation (buildReviewerOpenPointsSection) and writes it to
8
- // the run record under `_driver/`, for the reviewing lawyer. Nothing a client can open carries it: not the
9
- // report, and not the audit workbook, whose link rides the cover note a client principal receives.
6
+ // Owner ruling, 2026-09-24: reviewer notes never reach the client page. Owner ruling, 2026-10-01: they do
7
+ // not reach the email either, on any run. The pipeline still builds the section from the review and the
8
+ // corrective observation (buildReviewerOpenPointsSection) and writes it to the run record under `_driver/`,
9
+ // beside the report, for the reviewing lawyer who opens the run. Nothing sends it anywhere.
10
10
  //
11
- // One module owns the file's name and its reading, so the pipeline that writes it and the email that reads
12
- // it cannot disagree about where it is, and the reader does not import the pipeline to learn a file name.
13
-
14
- import { readFileSync, existsSync } from "node:fs";
15
- import { driverDir } from "../shared/driver-dir.mjs";
11
+ // WHAT THE SECOND RULING REMOVED, and why it is not a narrowing of the first. The first took the section off
12
+ // the report page and left it on the email, gated on whether the run's forwarder was the client. The email
13
+ // then carried the sentence naming the independent reviewer — which reads as a human declining to sign a
14
+ // report — and carried the points in the engine's own vocabulary. Measured over the thirty days to
15
+ // 2026-10-01: the report page carries neither since the first ruling, and the email carried both. So the
16
+ // gate was holding a door open that is now shut: nothing reads this file onto a surface that is sent.
17
+ //
18
+ // One module owns the file's name, so the pipeline that writes it and the workbook reader that skips it
19
+ // cannot disagree about where it is, and a reader does not import the pipeline to learn a file name.
16
20
 
17
21
  /** The run-record file the reviewer's open points are written to, under the run's `_driver/`. */
18
22
  export const REVIEWER_OPEN_QUESTIONS_FILE = "reviewer-open-questions.md";
19
23
 
20
- /**
21
- * The recorded open points for the email the run sends, or null where that email could reach a client.
22
- *
23
- * The run's email goes to the job's forwarder, and on a client-started run (`clientPrincipal`) that address
24
- * is the client itself. There the open points stay in the run record alone. Everywhere else the recipient
25
- * is the reviewing lawyer, who reads them in the email's review headline, in the record's own words.
26
- * Null too when the run recorded none: the reviewer signed and nothing is open.
27
- */
28
- export function reviewerOpenPointsForEmail(job, runDir) {
29
- if (job?.clientPrincipal === true || !runDir) return null;
30
- const file = driverDir(runDir, REVIEWER_OPEN_QUESTIONS_FILE);
31
- if (!existsSync(file)) return null;
32
- try { return readFileSync(file, "utf8").trim() || null; } catch { return null; }
33
- }
24
+ // THE EMAIL READER IS GONE (owner ruling, 2026-10-01). It returned this file's text for a run whose
25
+ // forwarder was not the client, and the pipeline handed that to the email cover. Both halves are removed:
26
+ // the reader here, and the hand-off there. A gate on the recipient is not what the ruling asked for — the
27
+ // points leave every surface that is sent, so there is no recipient to test.
28
+ //
29
+ // NOT LEFT IN PLACE UNUSED. An exported reader with no caller is the shape somebody wires back up, and the
30
+ // thing that made this reachable in the first place was a function that existed and looked safe to call.