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
@@ -10,7 +10,7 @@ import { readFileSync, existsSync, mkdirSync, writeFileSync, renameSync, copyFil
10
10
  import { createHash } from "node:crypto";
11
11
  import { join, dirname, basename, resolve } from "node:path"; // resolve: the resume line must work from any cwd
12
12
  import { driverDir, driverRel, ensureDriverDir } from "../shared/driver-dir.mjs";
13
- import { REVIEWER_OPEN_QUESTIONS_FILE, reviewerOpenPointsForEmail } from "./reviewer-open-points.mjs"; // the run-record file the reviewer's open points go to // — one definition of where `_driver/` is
13
+ import { REVIEWER_OPEN_QUESTIONS_FILE } from "./reviewer-open-points.mjs"; // the run-record file the reviewer's open points go to, and the only thing that reads them now
14
14
  import { goodsOf } from "./queue-markers.mjs"; // — one reading of "does this job name goods", shared with the intake gate
15
15
  import { terminalClampDecision, orderClausesForLede, clientConditions, clauseForDefect } from "./terminal-clamp.mjs"; // — deliver and clamp, never withhold
16
16
  import { recordSpan } from "./attributed-span.mjs"; // — driver work the decomposition can attribute
@@ -47,6 +47,7 @@ import { readRegisterTaint, readActiveTaintAxes } from "./register-taint.mjs";
47
47
  import { parseNamedBand, mergeNamedBands, findCollapsedBands, quarantineUnknownStates, taintQuarantineCleanBlocks, bandRecords } from "./named-band.mjs";
48
48
  import { recordOriginsFor } from "./record-origins.mjs";
49
49
  import { REGISTER_PROVIDER } from "./driver.config.mjs";
50
+ import { noteRegisterServed, registersServedFrom, providerUsageCaveat } from "./register-served.mjs"; // which registers actually served this run — noted eagerly from the dispatch's own resolver, and reconciled at publish against the ledger rows, which carry the vendor per call
50
51
  import { FACTS_FILE as DIGEST_FACTS_FILE, ACCOUNTING_STAMP as DIGEST_ACCOUNTING_STAMP, recordedFindingUris,
51
52
  digestAccountingGap, digestBatchBrief, batchesOf } from "./register-digest-record.mjs"; // conversion 11 — the render's facts sidecar and the accounting era stamp
52
53
  import { buildBandShape, dominantElementComposites, deriveRegisterPositions, floorTierByMark, floorMarkKey } from "./band-shape.mjs"; // PR-8 — the deterministic reading layer; P2-A — candidates + positions
@@ -94,7 +95,7 @@ import { deriveScopeFacts } from "./scope-facts.mjs";
94
95
  import { documentGrowth } from "./gate-metrics.mjs";
95
96
  import { editRepairTail, abbrev } from "./repair-contract.mjs";
96
97
  import { repairFollowup } from "./repair-composers.mjs";
97
- import { seedRunStatus, recordTransition, writeRunStatus, rollupStatus, atomicWrite, finalStepFields, terminalRunState, signoffPatch, readSignoff } from "./progress.mjs";
98
+ import { seedRunStatus, recordTransition, writeRunStatus, readRunStatus, rollupStatus, atomicWrite, finalStepFields, terminalRunState, signoffPatch, readSignoff } from "./progress.mjs";
98
99
  import { batchMarkName } from "./mark-name.mjs";
99
100
  import { writeOutboxPacket } from "./outbox.mjs";
100
101
  import { publishReport, composeEmailHtml, deliverySubject } from "./publish/index.mjs";
@@ -208,8 +209,21 @@ const CL_SUPP_TAGS = ["closure"];
208
209
  // body) under the un-namespaced sessionKey we pass, so the row prefix-matches this run and the assembled
209
210
  // record set picks it up on re-assembly. Credential = the provider's own env var (already in the driver's
210
211
  // systemd EnvironmentFile); absence is a named mechanical cause. One provider is active per run.
212
+ /**
213
+ * Note the adapter's own id against this run, and hand the adapter straight back.
214
+ *
215
+ * RECORDED WHERE IT IS RESOLVED, so the run's record says what SERVED it rather than what was configured
216
+ * at launch. Noting is idempotent per run and writes only when the set changes, so a run fetching a
217
+ * thousand records pays one write. A null run directory notes nothing: a call outside a run has no record
218
+ * to carry the field, and inventing one is the guess this field exists to replace.
219
+ */
220
+ function noteAdapter(ctx, adapter) {
221
+ noteRegisterServed(ctx?.run?.runDir ?? ctx?.paths?.runDir ?? null, adapter?.id);
222
+ return adapter;
223
+ }
224
+
211
225
  async function defaultRecordFetcher(uri, ctx) {
212
- return activeProvider().recordFetch(uri, ctx);
226
+ return noteAdapter(ctx, activeProvider()).recordFetch(uri, ctx);
213
227
  }
214
228
 
215
229
  /**
@@ -2399,7 +2413,7 @@ async function verifyAndRecordHouseElement(ctx, opts = {}) {
2399
2413
  const caps = registerCapabilities();
2400
2414
  const { regions } = resolvePlanRegions(registerJurisdictions(ctx.job, ctx.profile), caps);
2401
2415
  const rec = resolveRecordExecutor({
2402
- lister: opts?.recordLister ?? null, adapter: activeProvider(),
2416
+ lister: opts?.recordLister ?? null, adapter: noteAdapter(ctx, activeProvider()),
2403
2417
  agentId: ctx.agentId ?? null, sessionKey: `clearance-${ctx.run.slug}-${ctx.run.codename}`,
2404
2418
  recordLog: runRecordLogPath(P.runDir),
2405
2419
  fixtureDir: ctx.job?.registerFixtures?.records ?? null,
@@ -7671,9 +7685,27 @@ export function buildOnlyYouSection(actions, findings, { nowMs = Date.now(), wit
7671
7685
  // addressed TO the reader; monitoring and filing-routine are standing items. Same tags as before, so
7672
7686
  // a reader who knows the old list reads the new one unchanged, one level down.
7673
7687
  const ASK_KINDS = new Set(["client-fact", "commercial-decision"]);
7688
+ // ── THE KINDS THAT DO NOT REACH A CLIENT SURFACE ────────────────────────────────────────────────
7689
+ //
7690
+ // Owner ruling, 2026-10-01, on the clearance emails production sent in the preceding day: the
7691
+ // "[Open question]" rows are non-critical and confuse, and they come OUT of the client's report and
7692
+ // email. They are not reworded and nothing replaces them.
7693
+ //
7694
+ // THE FILTER IS HERE AND NOT ON `advisories` ABOVE, which is the whole care in this change. That list
7695
+ // also feeds `actionDates`, so dropping a kind from it would silently take a declared deadline out of
7696
+ // the date set three predelivery checks key on — a client-facing removal quietly changing a date
7697
+ // check. Filtering at the point of RENDERING removes the rows from the document and touches nothing
7698
+ // else: the register in findings.json is unchanged, the kind stays valid, an advisory still never
7699
+ // conditions reliance, and the verdict and its bound read from the register rather than from these
7700
+ // lines.
7701
+ //
7702
+ // So the row is gone from what the client reads and the fact is still on the run, which is what the
7703
+ // ruling asks for.
7704
+ const NOT_FOR_CLIENT = new Set(["client-fact"]);
7674
7705
  const advisoryLine = (a) => `- **${ADVISORY_TAG[a.kind]}** ${askLine(a)}${a.deadline?.date ? ` (by ${a.deadline.date})` : ""}`;
7675
- const askLines = advisories.filter((a) => ASK_KINDS.has(a.kind)).map(advisoryLine);
7676
- const watchLines = advisories.filter((a) => !ASK_KINDS.has(a.kind)).map(advisoryLine);
7706
+ const forClient = (a) => !NOT_FOR_CLIENT.has(a.kind);
7707
+ const askLines = advisories.filter((a) => ASK_KINDS.has(a.kind)).filter(forClient).map(advisoryLine);
7708
+ const watchLines = advisories.filter((a) => !ASK_KINDS.has(a.kind)).filter(forClient).map(advisoryLine);
7677
7709
  const groups = [
7678
7710
  ["Before you can rely on this result", conditionLines],
7679
7711
  ["We need an answer from you", askLines],
@@ -9173,7 +9205,7 @@ async function pipelineInner(job, opts = {}) {
9173
9205
  // which is the same bug in the other direction.
9174
9206
  const planExec = opts.planExecutor
9175
9207
  ?? (envGateOn("CLEAROTRON_PLAN_DISPATCH") && activeProvider().executePlan
9176
- ? (args, c) => activeProvider().executePlan(args, c) : null);
9208
+ ? (args, c) => noteAdapter(c ?? ctx, activeProvider()).executePlan(args, c) : null);
9177
9209
  // Kill switch CLEAROTRON_SATPROBE_CODESIDE=0 (the inline `!== "0"` pattern): legacy mock harnesses
9178
9210
  // opt out — their scenarios pre-date the code-side member and script the AGENT path (and an armed
9179
9211
  // real executor lane would dial the provider from a test). Production default is ON.
@@ -10841,6 +10873,13 @@ async function pipelineInner(job, opts = {}) {
10841
10873
  let regArm = null;
10842
10874
  const regSwept = new Set();
10843
10875
  const regDeferReason = new Map();
10876
+ // — WHAT THE FOLD REFUSED, AND WHICH KIND OF REFUSAL IT WAS. Read by the remedy accounting
10877
+ // below, which otherwise requires every minted qid to land and so records a term as
10878
+ // dispatch-failed when the fold refused its slice as a duplicate of a question the plan had
10879
+ // already asked and answered. A fold-refused qid can never land, by construction — the
10880
+ // re-attempt above deliberately filters it out — so only the accounting was still waiting
10881
+ // for it. qid -> { kind, twin }.
10882
+ const regFoldRefusals = new Map();
10844
10883
  // Partial mints must not read as full sweeps: when SOME of a directive's remedy proposals mint
10845
10884
  // and a sibling is refused by the shape lint, the directive can still verify-closed on the
10846
10885
  // minted subset — the refusal is disclosed per directive (receipt `partials` row on a close,
@@ -10974,9 +11013,25 @@ async function pipelineInner(job, opts = {}) {
10974
11013
  // belief about two screens agreeing, and the day they stop agreeing this is the only place
10975
11014
  // that would say so.
10976
11015
  if (refused.length) {
11016
+ for (const r of refused) if (r?.qid) regFoldRefusals.set(r.qid, { kind: r.kind ?? null, twin: r.twin ?? null });
11017
+ // THE REASON THE FOLD GAVE IS LOGGED, not just the qid it refused. The fold is the only
11018
+ // place that knows WHY, and for a duplicate it knows WHICH plan row already holds the
11019
+ // answer. Logging the qid alone threw that away: recovering it on one run meant
11020
+ // re-deriving the twin from a shared identity hash by hand, and a receipt that cannot
11021
+ // say why a slice is missing reads as an engine fault whatever the cause was.
10977
11022
  runLog(run.runDir, { event: "frame-reopen-fold-refused", refused: refused.length,
10978
- qids: refused.map((r) => r.qid).slice(0, 6) });
10979
- note(`frame-reopen: ${refused.length} minted entr${refused.length === 1 ? "y" : "ies"} REFUSED at the fold — the mint screen and the fold screen disagree, which is a wiring finding`);
11023
+ qids: refused.map((r) => r.qid).slice(0, 6),
11024
+ kinds: refused.reduce((a, r) => { const k = r.kind ?? "unstated"; a[k] = (a[k] ?? 0) + 1; return a; }, {}),
11025
+ rows: refused.slice(0, 6).map((r) => ({ qid: r.qid, kind: r.kind ?? null, twin: r.twin ?? null, issue: r.issue ?? null })) });
11026
+ // ONE NOTE PER KIND, because they are three different findings and only one of them is a
11027
+ // wiring fault. A duplicate-question refusal is the fold working: the question is already
11028
+ // in the plan and its existing row carries the coverage. An identity collision or a
11029
+ // malformed term IS the mint screen and the fold screen disagreeing, because the mint
11030
+ // runs entryTermIssues and should have caught it.
11031
+ const dupes = refused.filter((r) => r.kind === "duplicate-question");
11032
+ const faults = refused.filter((r) => r.kind !== "duplicate-question");
11033
+ if (dupes.length) note(`frame-reopen: ${dupes.length} minted entr${dupes.length === 1 ? "y" : "ies"} refused at the fold as already-asked — the plan row${dupes.length === 1 ? "" : "s"} ${dupes.map((r) => `"${r.twin ?? "(twin unstated)"}"`).join(", ")} carr${dupes.length === 1 ? "ies" : "y"} that coverage; the term is answered, not unsearched`);
11034
+ if (faults.length) note(`frame-reopen: ${faults.length} minted entr${faults.length === 1 ? "y" : "ies"} REFUSED at the fold (${[...new Set(faults.map((r) => r.kind ?? "unstated"))].join(", ")}) — the mint screen and the fold screen disagree, which is a wiring finding`);
10980
11035
  }
10981
11036
  // Dispatch → derive band → VERIFY per directive (qid-landed + non-collapse + class-scope).
10982
11037
  // A byte-changed band with only a wrong-scope/empty/error block closes NOTHING.
@@ -11306,6 +11361,7 @@ async function pipelineInner(job, opts = {}) {
11306
11361
  terms: termRows,
11307
11362
  blocksByQid: regLastJoin?.blocksByQid ?? new Map(),
11308
11363
  executedQids: regLastJoin?.executedQids ?? new Set(),
11364
+ foldRefusals: regFoldRefusals,
11309
11365
  outOfScope,
11310
11366
  });
11311
11367
  } catch (e) {
@@ -15136,8 +15192,37 @@ async function pipelineInner(job, opts = {}) {
15136
15192
  try {
15137
15193
  const provider = activeProvider().id;
15138
15194
  const usage = tallyRegisterCalls(DEFAULT_LEDGER_PATH, `clearance-${run.slug}-${run.codename}-`);
15139
- runLog(run.runDir, { event: "provider-usage", provider, ...usage });
15140
- writeRunStatus(ctx, { providerUsage: { [provider]: usage } });
15195
+ // ── A WHOLE-RUN TALLY FILED UNDER ONE REGISTER SAYS SO WHEN THE RUN SERVED MORE THAN ONE ──────
15196
+ //
15197
+ // This label is resolved HERE, at publish, while the tally it labels spans the whole run. On a run
15198
+ // whose register changed part-way that key is whichever register happened to be active at publish,
15199
+ // and every call is filed under it — the confident-and-wrong shape the served field exists to end.
15200
+ //
15201
+ // The key's SHAPE is untouched because a ledger reads this map. What is added is the caveat, in the
15202
+ // same record, so the two register fields in one status cannot silently disagree: the served list
15203
+ // says two, and this says the tally covers both and could only be filed under one. Splitting the
15204
+ // tally by register is a second question and is not answered here.
15205
+ // ── THE LEDGER IS WHAT KNOWS, SO ASK IT BEFORE READING THE RECORD ───────────────────────────
15206
+ //
15207
+ // `registersServed` was written from ONE place: the adapter wrapper the record fetcher, the record
15208
+ // lister and a gated plan executor pass through. A clearance that takes counts and never fetches a
15209
+ // record went through none of them and recorded no register at all — the run could not say which
15210
+ // vendor answered it, and `registersServedFrom` returned [] for a run that had called a register
15211
+ // hundreds of times.
15212
+ //
15213
+ // Every one of those calls wrote a ledger row, and every row carries the vendor that produced it, so
15214
+ // the tally just computed already holds the answer per call. Noting them here is not a second source
15215
+ // of truth competing with the first: it is the same memoised setter, which unions and writes only on
15216
+ // change, so the eager note during the run and this reconciliation at publish cannot disagree.
15217
+ //
15218
+ // WHY NOT `activeProvider()` HERE. That resolves the register active at publish, which is the
15219
+ // confident-and-wrong answer this whole field exists to end — and on a run that changed register it
15220
+ // would name one vendor while the rows name two. The rows are per call; the resolver is per moment.
15221
+ for (const id of usage.providers) noteRegisterServed(run.runDir, id);
15222
+ const served = registersServedFrom(readRunStatus(run.runDir));
15223
+ const spans = providerUsageCaveat(served);
15224
+ runLog(run.runDir, { event: "provider-usage", provider, ...(served.length > 1 ? { spans: served } : {}), ...usage });
15225
+ writeRunStatus(ctx, { providerUsage: { [provider]: usage }, ...spans });
15141
15226
  providerTally = { [provider]: usage };
15142
15227
  // AD-4: the line prints the complete by_tool census, not a hand-picked subset — the R2 run printed
15143
15228
  // "search=0" while 286 execute_plan calls carried the whole register workload, and the subset read
@@ -15201,7 +15286,14 @@ async function pipelineInner(job, opts = {}) {
15201
15286
  // rule still forbids is the ENGINE'S OWN WORDS getting there: composeEmailHtml enumerates
15202
15287
  // predelivery-lint's code-owned projection (deliveryFlagLines), never the checks' raw `detail` —
15203
15288
  // this mail is addressed to job.forwarderEmail, which on a client-principal run is the client.
15204
- emailVerdictOpts.reviewerOpenPointsMd = reviewerOpenPointsForEmail(job, dirname(P.report)); writeFileSync(P.emailBody, composeEmailHtml(P.report, published.url, published.auditFile, emailNames, deliveryForRun(ctx), emailVerdictOpts));
15289
+ // THE REVIEWER'S OPEN POINTS NO LONGER RIDE THE EMAIL (owner ruling, 2026-10-01). This line read them
15290
+ // out of the run record and handed them to the cover, which is how the sentence naming the independent
15291
+ // reviewer reached a client after the 2026-09-24 ruling had already taken it off the report page: the
15292
+ // section stopped being spliced into the body and started being posted to the email instead. Measured
15293
+ // on the last thirty days of production, the report carries neither the heading nor that sentence since
15294
+ // that ruling, and the email carries both. The record is still written beside the report for the
15295
+ // reviewing lawyer; nothing reads it onto a client surface.
15296
+ writeFileSync(P.emailBody, composeEmailHtml(P.report, published.url, published.auditFile, emailNames, deliveryForRun(ctx), emailVerdictOpts));
15205
15297
  // ctx.verdict is set BEFORE the packet is composed, because the packet's copy reads it.
15206
15298
  ctx.verdict = verdict;
15207
15299
  // DELIVERY (Phase 2). The driver writes a self-contained delivery packet and leaves
@@ -153,7 +153,12 @@ export function atomicWrite(file, text) {
153
153
 
154
154
  function statusPath(runDir) { return join(runDir, "status.json"); }
155
155
 
156
- function readRunStatus(runDir) {
156
+ /**
157
+ * The run's own status as last written. `{}` when it cannot be read, which callers must treat as an
158
+ * absence rather than as an empty run — exported so a lane can read the run's own `startedAt` without
159
+ * a second copy of where status.json lives.
160
+ */
161
+ export function readRunStatus(runDir) {
157
162
  try { return JSON.parse(readFileSync(statusPath(runDir), "utf8")); }
158
163
  catch { return {}; }
159
164
  }
@@ -115,6 +115,14 @@ function emptyTally() {
115
115
  // confused with "the ledger used a name this module doesn't know" again.
116
116
  by_tool: {},
117
117
  unclassified: 0,
118
+ // WHICH VENDORS ACTUALLY ANSWERED THIS RUN, read off the rows rather than resolved from the
119
+ // environment. Every ledger row carries a `provider` discriminator (providers/_shared/ledger.mjs), so
120
+ // the set of vendors a run used is a fact the ledger already holds — and it is the ONLY place that
121
+ // holds it per call. A label resolved at publish instead names whichever register happened to be
122
+ // active then, which is wrong for a run whose register changed part-way; this cannot be, because each
123
+ // row says who produced it. Empty is a real answer and means the ledger carried no row for this run,
124
+ // which the four `ledger.*` provenance facts below then explain.
125
+ providers: [],
118
126
  // …and the PROVENANCE of the zeros above. Same house rule, the last place in this module still broken
119
127
  // by it: a ledger that is missing, mis-pointed or unreadable returned a clean all-zero tally that was
120
128
  // indistinguishable from "the run made no provider calls", and the note line printed `total=0 ((none))`
@@ -163,6 +171,7 @@ export function tallyRegisterCalls(ledgerPath = DEFAULT_LEDGER_PATH, runPrefix)
163
171
  out.ledger.readable = true;
164
172
 
165
173
  const firstFetchSession = new Map(); // record_fetch target → the session that first (network-)fetched it
174
+ const providersSeen = new Set(); // the vendors this run's rows name, in the order the ledger names them
166
175
 
167
176
  for (const line of text.split("\n")) {
168
177
  if (!line.trim()) continue;
@@ -179,6 +188,11 @@ export function tallyRegisterCalls(ledgerPath = DEFAULT_LEDGER_PATH, runPrefix)
179
188
  const isCacheHit = row.cache_hit === true;
180
189
  if (isCacheHit) out.cache_hits++;
181
190
 
191
+ // Collected for EVERY matching row, whatever tool it rode — a count-only run rides `count`-shaped
192
+ // rows and no record fetch, and that is exactly the run whose register went unrecorded before.
193
+ const rowProvider = typeof row.provider === "string" ? row.provider.trim().toLowerCase() : "";
194
+ if (rowProvider) providersSeen.add(rowProvider);
195
+
182
196
  const toolName = typeof row.tool === "string" && row.tool ? row.tool : "(none)";
183
197
  out.by_tool[toolName] = (out.by_tool[toolName] ?? 0) + 1;
184
198
  if (KINDS.includes(row.tool)) out[row.tool]++;
@@ -221,6 +235,8 @@ export function tallyRegisterCalls(ledgerPath = DEFAULT_LEDGER_PATH, runPrefix)
221
235
  }
222
236
  }
223
237
  }
238
+ // Sorted so two runs with the same vendors compare equal whatever order the ledger happened to append in.
239
+ out.providers = [...providersSeen].sort();
224
240
  return out;
225
241
  }
226
242
 
@@ -1712,9 +1712,13 @@ export function composeEmailHtml(reportMdPath, url, auditFile, names = [], deliv
1712
1712
  const { fm, secs } = parseReport(reportMdPath);
1713
1713
  const auditUrl = auditUrlFor(url, auditFile);
1714
1714
 
1715
- // 1) internal review headline — short: the bottom line + any "Reviewer's open questions" already in # Summary.
1716
- // Heading-neutral match: accept the new "Reviewer's open questions" and the legacy "Open questions for the reviewer".
1717
- const oq = (secs['Summary'] || '').match(/\*\*(?:Reviewer's open questions|Open questions for the reviewer)[\s\S]*?(?=\n\n[^*\d])/i);
1715
+ // 1) internal review headline — short: the bottom line, and nothing of the reviewer's own notes.
1716
+ //
1717
+ // THE SCRAPE THAT STOOD HERE IS GONE (owner ruling, 2026-10-01). It lifted a "Reviewer's open questions"
1718
+ // block out of the report's own # Summary and reprinted it on the cover. Two ways that reached a client:
1719
+ // a report that still carried an authored section of that name, and any archived report re-rendered
1720
+ // later. Removing the producer is not enough while something downstream goes looking for the text, so
1721
+ // this goes with it.
1718
1722
  // The report link rides HIGH — right under the bottom line in the headline, not buried below the table.
1719
1723
  const reportLink = hrefAttr(url)
1720
1724
  ? `<p style="margin:0 0 10px"><a href="${hrefAttr(url)}" style="color:#1a4fd6;font-weight:bold;font-size:12pt;text-decoration:none">▶ Open the full report</a>`
@@ -1826,8 +1830,10 @@ export function composeEmailHtml(reportMdPath, url, auditFile, names = [], deliv
1826
1830
  // second rung on any shipped build. Nothing renders a failover note into a report.
1827
1831
  // B5b checkpoint 4 — a customer named after the analysis was written ships as a delivery note, never silently.
1828
1832
  + (fm.late_bind_note ? `<p style="margin:0 0 8px;color:#7a2b12"><b>Applicant named mid-run:</b> ${cell(fm.late_bind_note)}</p>` : '')
1829
- + (oq ? `<div style="margin:0 0 8px">${mdBlock(oq[0])}</div>` : '') // …and the reviewer's open points from the run record (opts.reviewerOpenPointsMd), never from the report
1830
- + (opts.reviewerOpenPointsMd ? `<div style="margin:0 0 8px">${mdBlock(String(opts.reviewerOpenPointsMd).replace(/^#+\s*(.+)$/m, '**$1**'))}</div>` : '')
1833
+ // The reviewer's open points stood here, from the run record. They do not ride the cover any more
1834
+ // (owner ruling, 2026-10-01): the sentence naming the independent reviewer reads as a human declining
1835
+ // to sign, and the points themselves are the engine's own vocabulary. Both stay on the run, beside the
1836
+ // report, for the reviewing lawyer. Nothing is reworded and nothing replaces them.
1831
1837
  // wp50: the two-bucket # Actions list no longer rides the email — it renders on the report itself
1832
1838
  // (the single master document); the cover keeps only the headline, link, and surviving flags.
1833
1839
  + `</div>`;
@@ -475,16 +475,26 @@ export async function publishKnockout({ runId, codename, runDir, findings, plan,
475
475
  // Depth 2's sidecar, read from the RUN DIR rather than taken on trust from the caller: the
476
476
  // publisher renders what the driver measured and wrote, and a republish of an archived run picks up
477
477
  // the same file. An absent sidecar publishes exactly today's knockout.
478
+ // AN UNREADABLE SIDECAR IS NOT AN ABSENT ONE, and until now both arrived here as null. The
479
+ // instructed-scope load eight lines down has told the two apart since it was written; these two had
480
+ // the same catch and neither flag. A run whose counts file is present but corrupt therefore published
481
+ // the sentence for a run that never took any counts.
482
+ //
483
+ // `registerCountsUnreadable` HAS NO CONSUMER YET, deliberately. The page's line for an absent counts
484
+ // file says none could be taken, and there is no shipped sentence for a file that could not be read —
485
+ // writing one is a decision about what a client is told, not a defect fix. The distinction is recorded
486
+ // here so the decision has something to key on when it is made.
487
+ let registerCountsUnreadable = false;
478
488
  if (!registerCounts) {
479
489
  try { registerCounts = JSON.parse(readFileSync(driverDir(runDir, 'register-counts.json'), 'utf8')); }
480
- catch { registerCounts = null; }
490
+ catch { registerCounts = null; registerCountsUnreadable = existsSync(driverDir(runDir, 'register-counts.json')); }
481
491
  }
482
492
  // part 5 — the filings sidecar, read the same way and for the same reason: the publisher renders
483
493
  // what the driver measured and wrote, and a republish of an archived run picks up the same file. An
484
494
  // absent sidecar publishes exactly the counts-only knockout, with no filings section anywhere.
485
- let registerRecords = null;
495
+ let registerRecords = null, registerRecordsUnreadable = false;
486
496
  try { registerRecords = JSON.parse(readFileSync(driverDir(runDir, 'register-records.json'), 'utf8')); }
487
- catch { registerRecords = null; }
497
+ catch { registerRecords = null; registerRecordsUnreadable = existsSync(driverDir(runDir, 'register-records.json')); }
488
498
  // The owner lookups this run made, read the same tolerant way as the records above:
489
499
  // an archived run that predates the lane has no file, and its cards then render exactly as they were
490
500
  // delivered. The source line the report prints comes from HERE, not from anything the seat typed.
@@ -699,6 +709,9 @@ export async function publishKnockout({ runId, codename, runDir, findings, plan,
699
709
  const markBand = single ? overall : worstBand(framework, [m]);
700
710
  writeRO(file, renderKnockoutHtml(one, framework, {
701
711
  runId, overall: markBand, issued, auditFile, probeRan, registerCounts, registerRecords, ownerChecks, instructedScope, identity, matter: runId,
712
+ // Carried as its own value rather than folded into `registerRecords`, because three sites PRINT
713
+ // `registerRecords.unavailable` and a marker invented here would become client-facing wording.
714
+ registerRecordsUnreadable,
702
715
  // — an invented mark says so on its own report. Resolved ONCE above the loop:
703
716
  // the answer is a property of the run, and asking per mark would let a multi-mark demo mark some
704
717
  // documents and not others if the roster moved mid-publish.
@@ -1377,16 +1377,16 @@ function firstRef(marks, framework) {
1377
1377
  return '';
1378
1378
  }
1379
1379
 
1380
- // — THE REGISTER SENTENCE IS THE RENDERER'S, and it is the whole reason this function exists.
1380
+ // — THE REGISTER SENTENCE IS THE RENDERER'S, and it is the whole reason the two clauses below exist.
1381
1381
  //
1382
1382
  // A live run printed "the register overlay has not been run" in model prose directly under a table of
1383
1383
  // register counts the same run had taken. The model was not lying: it cannot see the count lane, it is
1384
1384
  // deliberately never shown the figures (register-count.mjs rule 1), and it filled the gap with the only
1385
1385
  // thing it had. The fix is not a better prompt — it is that nobody who cannot see the machinery gets to
1386
- // describe it. The validator forbids the model from saying anything about register coverage; this
1387
- // function says it, from the artifacts.
1386
+ // describe it. The validator forbids the model from saying anything about register coverage; these
1387
+ // clauses say it, from the artifacts.
1388
1388
  //
1389
- // It reads the SIDECARS, never the prose: the counts entry for this mark, and whether the product bought
1389
+ // They read the SIDECARS, never the prose: the counts entry for this mark, and whether the product bought
1390
1390
  // the probe at all. Four states, four sentences, and none of them is a judgment.
1391
1391
  //
1392
1392
  // — AND IT NOW STATES THE POSITION, NOT ONLY THE COVERAGE. Coverage alone ("hit-counts were taken
@@ -1394,10 +1394,12 @@ function firstRef(marks, framework) {
1394
1394
  // STANDS on the register for this name. That sentence is appended by registerPositionClause below and
1395
1395
  // it is present in every state, including the state where the answer is "nothing does" — an absence is
1396
1396
  // a finding and must be said out loud, never left as a silence over a table of numbers.
1397
- function registerLine(mark, registerCounts, probeRan, registerRecords = null, cards = []) {
1398
- return [coverageClause(mark, registerCounts, probeRan),
1399
- registerPositionClause(mark, registerCounts, registerRecords, cards)].filter(Boolean).join(' ');
1400
- }
1397
+ //
1398
+ // THE FUNCTION THAT COMPOSED THE PAIR IS GONE, and removing it was the point rather than tidiness. It
1399
+ // was called from nowhere in the tree, and its call to `registerPositionClause` was the one that did NOT
1400
+ // pass the unreadable-sidecar flag — so anybody reviving it would have revived exactly the defect that
1401
+ // flag closes, silently, with nothing at the call site to say so. The live composition is inline in
1402
+ // `renderKnockoutHtml`, which passes it. Found independently twice before being removed once.
1401
1403
 
1402
1404
  /**
1403
1405
  * Where the name STANDS on the register that was searched. Six states, and they are six because
@@ -1421,12 +1423,22 @@ function registerLine(mark, registerCounts, probeRan, registerRecords = null, ca
1421
1423
  // unconditionally, which was true while the clause was drawn under the cards. It is drawn under the
1422
1424
  // COUNTS now — the numbers it qualifies — and the cards are below it, so a fixed word would send a
1423
1425
  // reader the wrong way up the page. The caller knows the order; this function does not guess it.
1424
- function registerPositionClause(mark, registerCounts, registerRecords, cards = [], where = 'above') {
1426
+ // The one sentence for a listing whose answer this run cannot state, whether the lister refused or the
1427
+ // sidecar itself would not parse. Held once so the two callers below cannot drift: they are the same fact
1428
+ // to a reader, and two copies of a sentence are two sentences waiting to differ.
1429
+ const FILINGS_NOT_LISTED = 'The filings behind the counts could not be listed on this run, so nothing here says whether one stands.';
1430
+
1431
+ function registerPositionClause(mark, registerCounts, registerRecords, cards = [], where = 'above', recordsUnreadable = false) {
1425
1432
  if (!registerRecords) {
1433
+ // AN UNREADABLE SIDECAR IS NOT AN ABSENT ONE. Absent means the listing was never taken, and the page
1434
+ // says so. Unreadable means it WAS taken and this run cannot say what it found. The second sentence
1435
+ // was already written, for the lister's own refusal, and was simply unreachable from a republish:
1436
+ // a file that would not parse arrived as null, and null reads here as absent.
1437
+ if (recordsUnreadable) return FILINGS_NOT_LISTED;
1426
1438
  return registerCounts ? 'The filings behind those counts were not listed on this run.' : '';
1427
1439
  }
1428
1440
  if (registerRecords.unavailable) {
1429
- return 'The filings behind the counts could not be listed on this run, so nothing here says whether one stands.';
1441
+ return FILINGS_NOT_LISTED;
1430
1442
  }
1431
1443
  const entry = recordsForMark(registerRecords, mark?.name);
1432
1444
  const provider = registerRecords.providerLabel ?? registerRecords.provider ?? 'the register';
@@ -1775,6 +1787,11 @@ const CAVEAT_LEAD = 'This screen also carries the following limits:';
1775
1787
  export function renderKnockoutHtml(findings, framework, {
1776
1788
  runId, overall, issued = null, auditFile = null, probeRan = false, registerCounts = null,
1777
1789
  registerRecords = null,
1790
+ // — was the filings sidecar PRESENT but unreadable? Defaults to false, so every existing caller — the
1791
+ // unit fixtures and both render-check scripts, none of which pass one — renders exactly as it did. It
1792
+ // is its own value rather than a marker on `registerRecords`, because three sites PRINT
1793
+ // `registerRecords.unavailable` and anything invented for it would become client-facing wording.
1794
+ registerRecordsUnreadable = false,
1778
1795
  // The driver's own record of the owner lookups it ran. Defaults to [] so an
1779
1796
  // archived run that predates the lane renders exactly as it was delivered.
1780
1797
  ownerChecks = [],
@@ -1853,7 +1870,7 @@ export function renderKnockoutHtml(findings, framework, {
1853
1870
  // promoted, and the clause would name refs no card carries.
1854
1871
  const cardsByMark = new Map(marks.map((m) => [m?.name, registerCardViews(m, framework, registerRecords).cards]));
1855
1872
  const positions = marks.map((m) => {
1856
- const clause = registerPositionClause(m, registerCounts, registerRecords, cardsByMark.get(m?.name) ?? [], 'below');
1873
+ const clause = registerPositionClause(m, registerCounts, registerRecords, cardsByMark.get(m?.name) ?? [], 'below', registerRecordsUnreadable);
1857
1874
  if (!clause) return '';
1858
1875
  return marks.length > 1 && m?.name ? `${m.name}: ${clause}` : clause;
1859
1876
  }).filter(Boolean).join(' ');
@@ -2220,6 +2237,16 @@ export function knockoutReportData(findings, framework, { runId, codename, overa
2220
2237
  line: recordsLine(listed),
2221
2238
  fetched: listed.fetched ?? (listed.records ?? []).length,
2222
2239
  capped: Boolean(listed.capped),
2240
+ // THE CAP THE RUN RECORDED, beside the flag that says it bit. `capped: true` on its own cannot
2241
+ // tell a listing truncated at fifty from one a cap of zero stopped before it began: the first
2242
+ // is a register outcome and the second is a configuration, and a consumer reading the flag
2243
+ // alone reports them the same way. The run has always written `cap` per mark; nothing read it.
2244
+ //
2245
+ // NULL IS NOT ZERO and the two are kept apart all the way out, the way the portal's allowance
2246
+ // contract keeps them. Null is a sidecar that recorded no cap at all, which is every run
2247
+ // archived before the field existed; zero is a cap that was set to zero. Folding them together
2248
+ // would put an archived run and a misconfigured one on the same line.
2249
+ cap: Number.isFinite(listed.cap) ? listed.cap : null,
2223
2250
  records: (listed.records ?? []).map((r) => ({
2224
2251
  mark: r.mark, owner: r.owner, status: r.status, classes: r.classes ?? [],
2225
2252
  territory: r.territory, matchedForm: r.matchedForm, matchedBasis: r.matchedBasis,
@@ -567,6 +567,21 @@ export function traceRecordCarry({ bandRecords = [], placements = [], registerFi
567
567
  // and the one that run shipped without stating, so it is counted separately and never folded
568
568
  // into an ordinary drop total.
569
569
  const upstreamAbsent = rows.filter((r) => /:stage-incomplete$/.test(String(r.reason ?? "")));
570
+ // Records the step PASSED OVER: it ran, it reached them, and it recorded no ground of its own, so the
571
+ // driver's own literal stands as the reason. A reason, so not `unreasoned` — the same distinction
572
+ // `upstreamAbsent` above is drawn on, and counted separately for the same reason.
573
+ //
574
+ // WHY IT HAS TO BE IN `totals` AND NOT ONLY IN `by_reason_source`. This module's own header says a
575
+ // reader who opens the file and sees `unreasoned: 0` must be able to tell that from "nothing was
576
+ // dropped anywhere in this run". On R18 of 2026-09-27 the totals block read `unreasoned: 0` beside
577
+ // 5,760 records the step passed over — the count was in `by_reason_source` and not in the block a
578
+ // reader opens first, so the loudest fact about that run was one level down from the answer.
579
+ //
580
+ // `unreasoned` IS NOT REDEFINED, deliberately. A shipped Machine QC check passes only on
581
+ // `unreasoned === 0` and that row reaches a client's workbook; folding these in would turn a green
582
+ // into a red on every run with a silent exit, which is a decision about what a client is told rather
583
+ // than a correction to a record.
584
+ const stepSilent = rows.filter((r) => r.reason_source === "step-silent");
570
585
  const incompleteStages = Object.values(outcomes ?? {}).filter((o) => o && o.completed !== true).map((o) => o.stage);
571
586
  const slices = untraceableSlices({ crowds, planExecution });
572
587
  const deliveredFindings = Array.isArray(findings) ? findings.length : 0;
@@ -601,6 +616,7 @@ export function traceRecordCarry({ bandRecords = [], placements = [], registerFi
601
616
  finding: byReach.finding ?? 0,
602
617
  dropped: rows.length - (byReach.finding ?? 0),
603
618
  unreasoned: unreasoned.length,
619
+ step_silent: stepSilent.length,
604
620
  upstream_absent: upstreamAbsent.length,
605
621
  untraceable_slices: slices.length,
606
622
  // — THE SUM STATES ITS OWN INCOMPLETENESS. `n + s.untraced` coerced a null to 0, so a
@@ -886,7 +902,7 @@ export function statedDivergenceFindings({ reconciliation = null, carryRows = nu
886
902
  // statedDivergenceFindings checked=5 matched=5 diverged=0 ← should have named two marks
887
903
  //
888
904
  // The reconciliation names five finding-ended positions and they are five OTHER marks — VELTRIN
889
- // bioenergetische Kosmetik, KORPHIC HSE, KORPHI, DELPHIN & EMERENCE, KORPHI DIAGNOSTICS. The two that
905
+ // bioenergetische Kosmetik, KORPHIC HSE, KORPHI, KORPHIN & ACME, KORPHI DIAGNOSTICS. The two that
890
906
  // were lost sit in the CARRY rows and the reconciliation never mentions them:
891
907
  //
892
908
  // HALVER KORPHI reach=placed stopped_at=digest reason_source=step-stated reason=digest:reasoned-negative
@@ -66,8 +66,18 @@ export const RULE_DEFINITIONS = [
66
66
  ];
67
67
 
68
68
  /** Files worth scanning: prose-bearing, tracked, not generated, and not this rule's own definition. */
69
+ // `.js` IS IN THE LIST BECAUSE IT WAS THE HOLE. The extensions here were the ones the driver is written
70
+ // in, and the register adapters are not: 26 tracked `.js` files, every one carrying the same kind of
71
+ // header prose as the rest of the tree, scanned by nothing. The strip ran over them like everywhere
72
+ // else, and one of its casualties sat in an adapter header for as long as this check has existed,
73
+ // under a table reading zero. A floor that cannot see a whole file extension reports a repaired tree.
74
+ //
75
+ // It costs one row, and the row is NOT residue: an adapter header names a list separator as
76
+ // `(, ; / etc)`, which is a parenthesis opening on a comma and is also correct English about
77
+ // punctuation. `notes` in the backlog table records why that row is there, so the next reader meets an
78
+ // explanation rather than a number that will not fall.
69
79
  export const isScannable = (f) =>
70
- /\.(mjs|md|yml|ts)$/.test(f) && !f.startsWith("portal-ui/dist/") && !RULE_DEFINITIONS.includes(f);
80
+ /\.(mjs|js|md|yml|ts)$/.test(f) && !f.startsWith("portal-ui/dist/") && !RULE_DEFINITIONS.includes(f);
71
81
 
72
82
  /**
73
83
  * Count both signatures per file across `files`.
@@ -2079,7 +2079,7 @@ export function foldSupplementalEntries(plan, entries) {
2079
2079
  const eTerm = String(e.term ?? e.terms?.[0] ?? "");
2080
2080
  if (inBatch.has(e.qid) && inBatch.get(e.qid) === eTerm) continue; // the same row twice — one refusal
2081
2081
  if (inBatch.has(e.qid)) {
2082
- refused.push({ qid: e.qid, term: String(e.term ?? e.terms?.[0] ?? ""),
2082
+ refused.push({ qid: e.qid, term: String(e.term ?? e.terms?.[0] ?? ""), kind: "identity-collision",
2083
2083
  issue: planRowRefusal({ qid: e.qid, issue:
2084
2084
  `a different term ("${inBatch.get(e.qid)}") already minted this identity in the same batch, so `
2085
2085
  + `one of the two would be dropped with nothing recorded. Two DIFFERENT terms sharing one qid is `
@@ -2098,7 +2098,7 @@ export function foldSupplementalEntries(plan, entries) {
2098
2098
  e = kept;
2099
2099
  const issues = entryTermIssues(e);
2100
2100
  if (issues.length) {
2101
- refused.push({ qid: e.qid, term: String(e.term ?? e.terms?.[0] ?? ""),
2101
+ refused.push({ qid: e.qid, term: String(e.term ?? e.terms?.[0] ?? ""), kind: "malformed-term",
2102
2102
  issue: planRowRefusal({ qid: e.qid, issue: issues[0].issue }) });
2103
2103
  continue;
2104
2104
  }
@@ -2107,7 +2107,11 @@ export function foldSupplementalEntries(plan, entries) {
2107
2107
  const key = entryQuestionKey(e, plan);
2108
2108
  const twin = key ? asked.get(key) : undefined;
2109
2109
  if (twin) {
2110
- refused.push({ qid: e.qid, term: String(e.term ?? e.terms?.[0] ?? ""),
2110
+ // THE TWIN IS A FIELD, NOT ONLY A PHRASE IN THE PROSE. This row is the only place that knows the
2111
+ // question is already asked, and by which plan row. A reader could parse it back out of the
2112
+ // sentence; a caller cannot be asked to. The remedy accounting reads it to tell a duplicate
2113
+ // refusal (answered elsewhere) from the other two kinds (genuinely unasked).
2114
+ refused.push({ qid: e.qid, term: String(e.term ?? e.terms?.[0] ?? ""), kind: "duplicate-question", twin,
2111
2115
  issue: planRowRefusal({ qid: e.qid, issue:
2112
2116
  `it asks the same question as plan row "${twin}" once regions are resolved (an entry with no `
2113
2117
  + `regions of its own inherits the plan's, so an empty list is the WIDEST scope, not a narrower `