clearotron 0.4.0-beta.2 → 0.4.0-beta.3

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 (49) hide show
  1. package/THIRD-PARTY-NOTICES.md +5 -5
  2. package/build-info.json +2 -2
  3. package/driver/CHANGELOG.md +9 -0
  4. package/driver/coverage-form.mjs +6 -0
  5. package/driver/coverage-ledger.mjs +14 -2
  6. package/driver/driver.config.mjs +24 -7
  7. package/driver/engine/mcp/stdio-server.mjs +12 -1
  8. package/driver/package.json +1 -1
  9. package/driver/pipeline-knockout.mjs +29 -8
  10. package/driver/pipeline.mjs +58 -8
  11. package/driver/progress.mjs +6 -1
  12. package/driver/publish/knockout.mjs +16 -3
  13. package/driver/publish/render-knockout.mjs +38 -11
  14. package/driver/record-carry.mjs +16 -0
  15. package/driver/reference-strip-signatures.mjs +11 -1
  16. package/driver/register-plan.mjs +7 -3
  17. package/driver/register-served.mjs +91 -0
  18. package/driver/remedy-accounting.mjs +38 -9
  19. package/driver/score-redaction.mjs +389 -20
  20. package/driver/skills/clearance-register/SKILL.md +0 -8
  21. package/driver/skills/clearance-register/digest.md +1 -1
  22. package/driver/skills/clearance-register/providers/corsearch.md +1 -1
  23. package/driver/skills/clearance-register/register-recipes.md +3 -9
  24. package/driver/skills/clearance-search/SKILL.md +2 -2
  25. package/driver/skills/clearance-search/phase2-execution.md +1 -1
  26. package/driver/skills/knockout-assess/SKILL.md +2 -0
  27. package/driver/stages-knockout.mjs +22 -0
  28. package/driver/suite-census.json +120 -18
  29. package/driver/unit-inventory.mjs +38 -43
  30. package/mcp-server/CHANGELOG.md +4 -0
  31. package/mcp-server/package.json +1 -1
  32. package/node_modules/brace-expansion/index.js +78 -22
  33. package/node_modules/brace-expansion/package.json +1 -1
  34. package/node_modules/readdir-glob/node_modules/brace-expansion/index.js +78 -22
  35. package/node_modules/readdir-glob/node_modules/brace-expansion/package.json +1 -1
  36. package/package.json +1 -1
  37. package/portal-ui/package.json +2 -2
  38. package/providers/jx/src/core.js +0 -1
  39. package/providers/jx/src/judge.js +0 -1
  40. package/providers/jx/src/nativeread.js +0 -1
  41. package/providers/oauth-mcp-bridge/CHANGELOG.md +4 -0
  42. package/providers/oauth-mcp-bridge/package.json +1 -1
  43. package/providers/signa/src/core.js +37 -14
  44. package/scripts/e2e-first-time.mjs +12 -4
  45. package/scripts/e2e-scenario-ops.mjs +25 -2
  46. package/scripts/e2e.mjs +113 -15
  47. package/scripts/mint-reference-strip-backlog.mjs +36 -2
  48. package/scripts/score.mjs +102 -29
  49. package/shared/identifier-scan.mjs +31 -1
@@ -110,7 +110,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
110
110
  SOFTWARE.
111
111
  ```
112
112
 
113
- ## @modelcontextprotocol/sdk@1.30.0
113
+ ## @modelcontextprotocol/sdk@1.30.1
114
114
 
115
115
  - **Licence declared:** `MIT`
116
116
  - **Repository:** https://github.com/modelcontextprotocol/typescript-sdk
@@ -566,7 +566,7 @@ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
566
566
  SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
567
567
  ```
568
568
 
569
- ## brace-expansion@1.1.18
569
+ ## brace-expansion@1.1.21
570
570
 
571
571
  - **Licence declared:** `MIT`
572
572
  - **Repository:** git://github.com/juliangruber/brace-expansion
@@ -596,7 +596,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
596
596
  SOFTWARE.
597
597
  ```
598
598
 
599
- ## brace-expansion@2.1.4
599
+ ## brace-expansion@2.1.7
600
600
 
601
601
  - **Licence declared:** `MIT`
602
602
  - **Repository:** git://github.com/juliangruber/brace-expansion
@@ -1968,7 +1968,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
1968
1968
  SOFTWARE.
1969
1969
  ```
1970
1970
 
1971
- ## fast-uri@3.1.7
1971
+ ## fast-uri@3.1.8
1972
1972
 
1973
1973
  - **Licence declared:** `BSD-3-Clause`
1974
1974
  - **Repository:** https://github.com/fastify/fast-uri
@@ -2626,7 +2626,7 @@ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
2626
2626
  PERFORMANCE OF THIS SOFTWARE.
2627
2627
  ```
2628
2628
 
2629
- ## ip-address@10.5.0
2629
+ ## ip-address@10.7.2
2630
2630
 
2631
2631
  - **Licence declared:** `MIT`
2632
2632
  - **Repository:** https://github.com/beaugunderson/ip-address
package/build-info.json CHANGED
@@ -1,4 +1,4 @@
1
1
  {
2
- "commit": "9724d686ef597055a7d7f205632ca7b77bc4efef",
3
- "version": "0.4.0-beta.2"
2
+ "commit": "c8159f8575f90587b1c78d5151d9acf9235977e1",
3
+ "version": "0.4.0-beta.3"
4
4
  }
@@ -1,5 +1,14 @@
1
1
  # clearotron-driver
2
2
 
3
+ ## 0.4.0-beta.3
4
+
5
+ ### Patch Changes
6
+
7
+ - Fixed: Reports no longer show a gap in coverage when a search was skipped because an identical one had already run.
8
+ - Fixed: Opposition windows appear again on register records, and renewal dates are read from where the register now publishes them.
9
+ - Before you upgrade: setup and `clearotron doctor` now ask for a newer Claude Code, because the older one quietly serves an earlier model generation.
10
+ - Fixed: Expiry dates appear again on US trademark records, which had been left blank since the register moved where it publishes them.
11
+
3
12
  ## 0.4.0-beta.2
4
13
 
5
14
  ### Minor Changes
@@ -1005,6 +1005,12 @@ export function renderCoverageLedgerJsonFromForm(rows, classTokens) {
1005
1005
  axis: String(r.axis ?? "").trim().toLowerCase(),
1006
1006
  scope, status: String(r.status).trim().toLowerCase(), reason,
1007
1007
  ...(classes.length ? { classes } : {}),
1008
+ // THE COUNT AND THE PLAN KEY THE SOURCE ROW ALREADY HOLDS (ruling 551). The table two functions up
1009
+ // prints `total_hits` as "N hits" and this render dropped it, so the two renders of one form
1010
+ // disagreed about whether the run knew the size of what it left. A row without either — a seat-added
1011
+ // row — emits neither, which says "this row has no count" rather than "the count was lost here".
1012
+ ...(Number.isInteger(r.total_hits) ? { total_hits: r.total_hits } : {}),
1013
+ ...(r.qid ? { qid: String(r.qid) } : {}),
1008
1014
  };
1009
1015
  }));
1010
1016
  }
@@ -450,7 +450,19 @@ export function parseCoverageLedger(md) {
450
450
  // OPTIONAL structured column: the Nice classes a row is about, as an array of class-number strings — the
451
451
  // join key scope-facts.mjs uses to attribute a coverage gap to an instructed class without re-parsing
452
452
  // prose. Archived ledgers (no classes key) parse byte-identically to before.
453
- const ROW_KEYS = ["axis", "scope", "status", "reason", "classes"];
453
+ // TWO MORE OPTIONAL COLUMNS, AND THEY CLOSE A DRIFT THE RENDERER'S OWN DOCTRINE FORBIDS. The form is the
454
+ // source and the prose table and this JSON are both renders of it, "so they agree by construction and
455
+ // neither can be the thing that drifts" — and they did not agree: a form row carries `total_hits` and the
456
+ // table prints it as "N hits", while this JSON dropped it. Measured on R18 of 2026-09-27: 50 rows of four
457
+ // fields, and not one of the 49 coverage-limited rows carried a count, so a reader could not tell a slice
458
+ // holding 200 records from one holding 300,000. Ruling 551 puts the count on the row in as many words.
459
+ //
460
+ // `qid` rides for the same reason the form keeps it in its own column: it is what joins a coverage row to
461
+ // the plan's question, and without it the count can only be re-attached by matching prose. A seat-added
462
+ // row has no qid and no count, which is a fact about that row rather than a gap in this contract.
463
+ //
464
+ // Archived ledgers parse byte-identically: both are optional and absence is unchanged behaviour.
465
+ const ROW_KEYS = ["axis", "scope", "status", "reason", "classes", "total_hits", "qid"];
454
466
 
455
467
  // Extract Nice-class tokens from a ledger row's free-text scope/reason cell ("[cl 5,32]", "Class 30 leg",
456
468
  // "nice classes 5/32"). DETERMINISTIC + conservative: only numbers 1–45 introduced by an explicit
@@ -494,7 +506,7 @@ export function parseCoverageLedgerJson(raw, { allowedAxes = REGISTER_AXES, acti
494
506
  if (!r || typeof r !== "object" || Array.isArray(r))
495
507
  throw new Error("coverage_ledger_unparseable: every row must be a plain object");
496
508
  for (const k of Object.keys(r)) {
497
- if (!ROW_KEYS.includes(k)) throw new Error(`coverage_key_unknown:${short(k)} (keys are EXACTLY: axis, scope, status, reason — plus OPTIONAL classes)`);
509
+ if (!ROW_KEYS.includes(k)) throw new Error(`coverage_key_unknown:${short(k)} (keys are EXACTLY: axis, scope, status, reason — plus OPTIONAL classes, total_hits, qid)`);
498
510
  }
499
511
  // normalize-then-validate (B): coerce cosmetic formatting (markdown/qualifier/transposition) to the
500
512
  // canonical token, but keep the guard STRICT — an unknown axis still fails, with the ORIGINAL token in
@@ -2057,17 +2057,34 @@ export const ENGINE_BINARIES = {
2057
2057
  // newer", with no ceiling. Setup installs it into the engines folder (enginesFolder, below the table)
2058
2058
  // when the reader picks this engine, and the resolver uses it only when the machine has no copy of its
2059
2059
  // own. The package's own `bin` field names the program, so no path inside it is written down here.
2060
- // 2.1.280 is the floor because it is the oldest release that can run the current generation of this
2061
- // vendor's top tier: below it the API refuses the model id outright ("version 2.1.280 or newer is
2062
- // required"), and the tier alias quietly goes on serving the previous generation. Measured 2026-09-22
2063
- // on 2.1.263 — the alias returned the older model and the pinned id was refused — so a floor that
2064
- // only asks for a program that starts is a floor that passes a machine this engine cannot run on.
2065
- package: "@anthropic-ai/claude-code", floor: "2.1.280",
2060
+ // THE FLOOR IS THE OLDEST RELEASE WHOSE TIER ALIASES REACH THE CURRENT MODEL GENERATION, and each
2061
+ // tier crossed on its own release rather than together. Measured 2026-09-30 by driving each version's
2062
+ // `-p` turn at a local recorder and reading the `model` it put on the wire:
2063
+ //
2064
+ // version --model opus --model sonnet
2065
+ // 2.1.277 claude-opus-5 claude-sonnet-5
2066
+ // 2.1.278 claude-opus-5 claude-sonnet-5
2067
+ // 2.1.280 claude-opus-5-5 claude-sonnet-5
2068
+ // 2.1.281 claude-opus-5-5 claude-sonnet-5
2069
+ // 2.1.282 claude-opus-5-5 claude-sonnet-5
2070
+ // 2.1.283 claude-opus-5-5 claude-sonnet-5
2071
+ // 2.1.284 claude-opus-5-5 claude-sonnet-5-5
2072
+ //
2073
+ // So the old floor of 2.1.280 was exactly right for the top tier and left the sonnet tier a
2074
+ // generation behind for four releases, silently: the alias resolves inside the program, the request
2075
+ // is well formed either way, and nothing refuses. The floor moves to the release where the SECOND
2076
+ // tier crosses, because a stage that asks for sonnet is asking for the current sonnet.
2077
+ //
2078
+ // Below the floor the previous behaviour still holds: on 2.1.263 the alias returned the older model
2079
+ // and a pinned id was refused outright ("version 2.1.280 or newer is required"), measured 2026-09-22.
2080
+ // A floor that only asks for a program that starts is a floor that passes a machine this engine
2081
+ // cannot run the current generation on.
2082
+ package: "@anthropic-ai/claude-code", floor: "2.1.284",
2066
2083
  // WHAT THE INSTALL TAKES ON DISK, in MB, which setup states before it asks to install. MEASURED, not
2067
2084
  // declared by the vendor: the engines folder after a fresh install of this package into an empty
2068
2085
  // folder, on npm 10.9.8 and on 11.19.1, 2026-09-14. A later release can be larger or smaller, so setup
2069
2086
  // says "about". Re-measure when the floor moves.
2070
- installMB: 214,
2087
+ installMB: 233,
2071
2088
  // The licence setup states wherever it tells a reader what they are about to install or use, as the
2072
2089
  // vendor's package declares it: "SEE LICENSE IN README.md", Anthropic's own terms.
2073
2090
  licence: "proprietary third-party software",
@@ -156,7 +156,18 @@ export function serve({ name, version = "0.1.0", tools = [] }) {
156
156
  // a seat can act on it in the turn it happens.
157
157
  const unmet = requiredFieldViolations(tool.inputSchema, params?.arguments);
158
158
  if (unmet.length) {
159
- logToolEvent({ event: "settled", seq, server: name, tool: tool.name, ...(axis ? { axis } : {}), ok: false });
159
+ // THE REASON THE ROW HAD IN SCOPE AND THREW AWAY. A settled row with
160
+ // `ok: false` and nothing else asserts a failure and withholds everything that would let a
161
+ // reader weigh it — and a verdict turns on that, because a failed tool call counts against a
162
+ // run's cleanliness. Measured on the round of 2026-09-26: this gate settled a frame call in the
163
+ // SAME MILLISECOND it started, wrote no call file, and the record could not say why.
164
+ //
165
+ // THE FIELD NAMES, NEVER THE MESSAGE. `unmet` is a list of schema keys, so it carries no matter
166
+ // content; the sentence below carries none today either, but a tool's refusal text is free to
167
+ // interpolate a mark, and this row's own contract is the call and never its content. A row that
168
+ // copied the message would leak the day one of them does; a row that copies `unmet` cannot.
169
+ logToolEvent({ event: "settled", seq, server: name, tool: tool.name, ...(axis ? { axis } : {}), ok: false,
170
+ reason: `missing_required:${unmet.join(",")}` });
160
171
  return ok(id, { isError: true, content: [{ type: "text", text:
161
172
  `${tool.name}_missing_required:${unmet.join(",")} — the schema you were handed declares `
162
173
  + `${unmet.length === 1 ? "this field" : "these fields"} REQUIRED and the call did not carry `
@@ -2,7 +2,7 @@
2
2
  "name": "clearotron-driver",
3
3
  "private": true,
4
4
  "type": "module",
5
- "version": "0.4.0-beta.2",
5
+ "version": "0.4.0-beta.3",
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": {
@@ -47,7 +47,7 @@ import {
47
47
  import { RunCancelled, assertNotCancelledBeforePublish } from "./cancel.mjs"; // stop-by-user: never the failure lane below
48
48
  import { loadFrameworkManifest, parseFrameworkManifest, frameworkFor, DEFAULT_FRAMEWORK } from "./framework.mjs";
49
49
  import { attachFrameworkMethod, methodPathFor, FROZEN_METHOD_FILE } from "./framework-method.mjs";
50
- import { KO_STAGES, KO_STEPS, KO_STEP_REGISTER_COUNT, koSteps, koPaths, kebab, knockoutGridSpec, KNOCKOUT_WEB, KNOCKOUT_MINUTES_BAR, koChunks } from "./stages-knockout.mjs";
50
+ import { KO_STAGES, KO_STEPS, KO_STEP_REGISTER_COUNT, koSteps, koPaths, kebab, knockoutGridSpec, KNOCKOUT_WEB, KNOCKOUT_MINUTES_BAR, knockoutBarVerdict, koChunks } from "./stages-knockout.mjs";
51
51
  import { kebabCollisions, reportIdentityFor, CAPABILITY_SKIPPED_CAUSE, CAPABILITY_SKIPPED_NOTE } from "./search-policy.mjs";
52
52
  import { countPreflight, countRegisterHits, countedMarks, resolveCountExecutor } from "./register-count.mjs";
53
53
  import { recordsPreflight, listRegisterRecords, listedMarks, resolveRecordExecutor } from "./register-records.mjs";
@@ -60,7 +60,7 @@ import { validators as koValidators, validateMergedFindings, worstBand, register
60
60
  import { reviewAbout, reviewEvidence, reviewEvidenceLines, applyKnockoutReview, knockoutReviewFile } from "./knockout-review-record.mjs";
61
61
  import { stripNextStepSections } from "./knockout-next-step.mjs";
62
62
  import { publishKnockout, composeKnockoutEmail } from "./publish/knockout.mjs";
63
- import { writeRunStatus, rollupStatus, atomicWrite, identitySeed } from "./progress.mjs"; // — the identity seed is shared; the stepper is not
63
+ import { writeRunStatus, readRunStatus, rollupStatus, atomicWrite, identitySeed } from "./progress.mjs"; // — the identity seed is shared; the stepper is not
64
64
  import { batchMarkName } from "./mark-name.mjs";
65
65
  import { runLog, note, outputMeta } from "./log.mjs";
66
66
  import { defaultTerritoryState } from "./effective-scope.mjs"; // the stored-defaults reading — one producer, shared with the clearance lane
@@ -994,13 +994,16 @@ export async function knockoutInner(ctx, job, opts = {}) {
994
994
  const rows = readFileSync(K.sweepLedger, "utf8").split("\n").filter(Boolean).map((l) => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
995
995
  const cells = rows.reduce((n, r) => n + (Number.isFinite(r?.cells) ? r.cells : 0), 0);
996
996
  const places = rows.reduce((n, r) => Math.max(n, Array.isArray(r?.places) ? r.places.length : 0), 0);
997
- const minutes = Number(((Date.now() - sweepStartedAt) / 60000).toFixed(1));
997
+ // THE SWEEP'S OWN ELAPSED TIME, UNDER A NAME THAT SAYS SO, and the bar is no longer asserted here.
998
+ // The bar is about the RUN (ruling 591) and this span is one stage of it: both knockouts of
999
+ // 2026-09-26 recorded `overBar: false` at 0.6 and 0.8 minutes while the runs took 14.4 minutes
1000
+ // wall, which e2e scored as over. A bar recorded against the wrong quantity is worse than an
1001
+ // unrecorded one, because the field's name says it answered the question. It is recorded at
1002
+ // delivery instead, where the run's own start is readable.
1003
+ const sweepMinutes = Number(((Date.now() - sweepStartedAt) / 60000).toFixed(1));
998
1004
  runLog(run.runDir, { event: "knockout-sweep-total", marks: planMarks.length, calls: rows.length,
999
- cells, mostPlacesForOneName: places, minutes, barMinutes: KNOCKOUT_MINUTES_BAR,
1000
- // Stated so a reader does not have to do the comparison, and so "over" is a fact about this run
1001
- // rather than a judgment about the build. It changes nothing this run did.
1002
- overBar: minutes > KNOCKOUT_MINUTES_BAR });
1003
- note(`knockout sweep: ${cells} cell(s) across ${rows.length} call(s) in ${minutes} min (the bar is ${KNOCKOUT_MINUTES_BAR})`);
1005
+ cells, mostPlacesForOneName: places, sweepMinutes });
1006
+ note(`knockout sweep: ${cells} cell(s) across ${rows.length} call(s) in ${sweepMinutes} min`);
1004
1007
  } catch { /* the receipts are the record; a summary that could not be read never fails a sweep */ }
1005
1008
  }
1006
1009
 
@@ -1216,6 +1219,24 @@ export async function knockoutInner(ctx, job, opts = {}) {
1216
1219
  writeFileSync(join(config.outboxDir, `${packet.runId}.pending`), `${agent}\n`);
1217
1220
  } catch (e) { note(`delivery: outbox marker write skipped (${String(e.message).slice(0, 100)})`); }
1218
1221
  const deliveredAt = new Date().toISOString();
1222
+ // ── THE TEN-MINUTE BAR, AGAINST THE RUN (ruling 591) ────────────────────────────────────────────
1223
+ //
1224
+ // Recorded here because this is the first point at which the run's own span is known: `startedAt` is
1225
+ // in the run's status and `deliveredAt` is the line above. It used to be recorded at the web sweep,
1226
+ // against the sweep's own elapsed time, so both knockouts of 2026-09-26 stated `overBar: false` at
1227
+ // 0.6 and 0.8 minutes while their runs took 14.4 minutes wall — the number beside the bar was not the
1228
+ // number the bar is about.
1229
+ //
1230
+ // RECORDED, NEVER APPLIED, exactly as before: nothing reads `overBar` and nothing about this run
1231
+ // changes on it.
1232
+ //
1233
+ // AND AN UNREADABLE START SAYS SO RATHER THAN VANISHING. `readRunStatus` answers `{}` when it cannot
1234
+ // read, and a bar quietly omitted because a file was unreadable is an absence that reads as "not
1235
+ // over". So the event always carries the field, and names the reason when it has no span.
1236
+ try {
1237
+ const verdict = knockoutBarVerdict({ startedAt: readRunStatus(run.runDir)?.startedAt, deliveredAt });
1238
+ runLog(run.runDir, { event: "knockout-run-total", barMinutes: KNOCKOUT_MINUTES_BAR, ...verdict });
1239
+ } catch { /* the bar is a record, never a gate: a summary that could not be written never fails a delivery */ }
1219
1240
  // `tier` beside `verdict` — the same band word under the name the clearance lane records it by, so a
1220
1241
  // reader of either lane's status finds the rating in one place. `verdict` stays as it was.
1221
1242
  writeRunStatus(ctx, { state: "delivered", tier: overall, statement: published.statement, url: published.url, reports: published.reports.map((r) => ({ mark: r.mark, url: r.url })), deliveredAt, sendPending: true, stepIndex: STEPS.length - 1, stepLabel: STEPS[STEPS.length - 1], stepN: STEPS.length, stepTotal: STEPS.length });
@@ -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 register actually served this run, from the resolver the dispatch itself uses
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,
@@ -9173,7 +9187,7 @@ async function pipelineInner(job, opts = {}) {
9173
9187
  // which is the same bug in the other direction.
9174
9188
  const planExec = opts.planExecutor
9175
9189
  ?? (envGateOn("CLEAROTRON_PLAN_DISPATCH") && activeProvider().executePlan
9176
- ? (args, c) => activeProvider().executePlan(args, c) : null);
9190
+ ? (args, c) => noteAdapter(c ?? ctx, activeProvider()).executePlan(args, c) : null);
9177
9191
  // Kill switch CLEAROTRON_SATPROBE_CODESIDE=0 (the inline `!== "0"` pattern): legacy mock harnesses
9178
9192
  // opt out — their scenarios pre-date the code-side member and script the AGENT path (and an armed
9179
9193
  // real executor lane would dial the provider from a test). Production default is ON.
@@ -10841,6 +10855,13 @@ async function pipelineInner(job, opts = {}) {
10841
10855
  let regArm = null;
10842
10856
  const regSwept = new Set();
10843
10857
  const regDeferReason = new Map();
10858
+ // — WHAT THE FOLD REFUSED, AND WHICH KIND OF REFUSAL IT WAS. Read by the remedy accounting
10859
+ // below, which otherwise requires every minted qid to land and so records a term as
10860
+ // dispatch-failed when the fold refused its slice as a duplicate of a question the plan had
10861
+ // already asked and answered. A fold-refused qid can never land, by construction — the
10862
+ // re-attempt above deliberately filters it out — so only the accounting was still waiting
10863
+ // for it. qid -> { kind, twin }.
10864
+ const regFoldRefusals = new Map();
10844
10865
  // Partial mints must not read as full sweeps: when SOME of a directive's remedy proposals mint
10845
10866
  // and a sibling is refused by the shape lint, the directive can still verify-closed on the
10846
10867
  // minted subset — the refusal is disclosed per directive (receipt `partials` row on a close,
@@ -10974,9 +10995,25 @@ async function pipelineInner(job, opts = {}) {
10974
10995
  // belief about two screens agreeing, and the day they stop agreeing this is the only place
10975
10996
  // that would say so.
10976
10997
  if (refused.length) {
10998
+ for (const r of refused) if (r?.qid) regFoldRefusals.set(r.qid, { kind: r.kind ?? null, twin: r.twin ?? null });
10999
+ // THE REASON THE FOLD GAVE IS LOGGED, not just the qid it refused. The fold is the only
11000
+ // place that knows WHY, and for a duplicate it knows WHICH plan row already holds the
11001
+ // answer. Logging the qid alone threw that away: recovering it on one run meant
11002
+ // re-deriving the twin from a shared identity hash by hand, and a receipt that cannot
11003
+ // say why a slice is missing reads as an engine fault whatever the cause was.
10977
11004
  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`);
11005
+ qids: refused.map((r) => r.qid).slice(0, 6),
11006
+ kinds: refused.reduce((a, r) => { const k = r.kind ?? "unstated"; a[k] = (a[k] ?? 0) + 1; return a; }, {}),
11007
+ rows: refused.slice(0, 6).map((r) => ({ qid: r.qid, kind: r.kind ?? null, twin: r.twin ?? null, issue: r.issue ?? null })) });
11008
+ // ONE NOTE PER KIND, because they are three different findings and only one of them is a
11009
+ // wiring fault. A duplicate-question refusal is the fold working: the question is already
11010
+ // in the plan and its existing row carries the coverage. An identity collision or a
11011
+ // malformed term IS the mint screen and the fold screen disagreeing, because the mint
11012
+ // runs entryTermIssues and should have caught it.
11013
+ const dupes = refused.filter((r) => r.kind === "duplicate-question");
11014
+ const faults = refused.filter((r) => r.kind !== "duplicate-question");
11015
+ 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`);
11016
+ 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
11017
  }
10981
11018
  // Dispatch → derive band → VERIFY per directive (qid-landed + non-collapse + class-scope).
10982
11019
  // A byte-changed band with only a wrong-scope/empty/error block closes NOTHING.
@@ -11306,6 +11343,7 @@ async function pipelineInner(job, opts = {}) {
11306
11343
  terms: termRows,
11307
11344
  blocksByQid: regLastJoin?.blocksByQid ?? new Map(),
11308
11345
  executedQids: regLastJoin?.executedQids ?? new Set(),
11346
+ foldRefusals: regFoldRefusals,
11309
11347
  outOfScope,
11310
11348
  });
11311
11349
  } catch (e) {
@@ -15136,8 +15174,20 @@ async function pipelineInner(job, opts = {}) {
15136
15174
  try {
15137
15175
  const provider = activeProvider().id;
15138
15176
  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 } });
15177
+ // ── A WHOLE-RUN TALLY FILED UNDER ONE REGISTER SAYS SO WHEN THE RUN SERVED MORE THAN ONE ──────
15178
+ //
15179
+ // This label is resolved HERE, at publish, while the tally it labels spans the whole run. On a run
15180
+ // whose register changed part-way that key is whichever register happened to be active at publish,
15181
+ // and every call is filed under it — the confident-and-wrong shape the served field exists to end.
15182
+ //
15183
+ // The key's SHAPE is untouched because a ledger reads this map. What is added is the caveat, in the
15184
+ // same record, so the two register fields in one status cannot silently disagree: the served list
15185
+ // says two, and this says the tally covers both and could only be filed under one. Splitting the
15186
+ // tally by register is a second question and is not answered here.
15187
+ const served = registersServedFrom(readRunStatus(run.runDir));
15188
+ const spans = providerUsageCaveat(served);
15189
+ runLog(run.runDir, { event: "provider-usage", provider, ...(served.length > 1 ? { spans: served } : {}), ...usage });
15190
+ writeRunStatus(ctx, { providerUsage: { [provider]: usage }, ...spans });
15141
15191
  providerTally = { [provider]: usage };
15142
15192
  // AD-4: the line prints the complete by_tool census, not a hand-picked subset — the R2 run printed
15143
15193
  // "search=0" while 286 execute_plan calls carried the whole register workload, and the subset read
@@ -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
  }
@@ -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
@@ -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`.