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.
- package/THIRD-PARTY-NOTICES.md +5 -5
- package/build-info.json +2 -2
- package/driver/CHANGELOG.md +9 -0
- package/driver/coverage-form.mjs +6 -0
- package/driver/coverage-ledger.mjs +14 -2
- package/driver/driver.config.mjs +24 -7
- package/driver/engine/mcp/stdio-server.mjs +12 -1
- package/driver/package.json +1 -1
- package/driver/pipeline-knockout.mjs +29 -8
- package/driver/pipeline.mjs +58 -8
- package/driver/progress.mjs +6 -1
- package/driver/publish/knockout.mjs +16 -3
- package/driver/publish/render-knockout.mjs +38 -11
- package/driver/record-carry.mjs +16 -0
- package/driver/reference-strip-signatures.mjs +11 -1
- package/driver/register-plan.mjs +7 -3
- package/driver/register-served.mjs +91 -0
- package/driver/remedy-accounting.mjs +38 -9
- package/driver/score-redaction.mjs +389 -20
- package/driver/skills/clearance-register/SKILL.md +0 -8
- package/driver/skills/clearance-register/digest.md +1 -1
- package/driver/skills/clearance-register/providers/corsearch.md +1 -1
- package/driver/skills/clearance-register/register-recipes.md +3 -9
- package/driver/skills/clearance-search/SKILL.md +2 -2
- package/driver/skills/clearance-search/phase2-execution.md +1 -1
- package/driver/skills/knockout-assess/SKILL.md +2 -0
- package/driver/stages-knockout.mjs +22 -0
- package/driver/suite-census.json +120 -18
- package/driver/unit-inventory.mjs +38 -43
- package/mcp-server/CHANGELOG.md +4 -0
- package/mcp-server/package.json +1 -1
- package/node_modules/brace-expansion/index.js +78 -22
- package/node_modules/brace-expansion/package.json +1 -1
- package/node_modules/readdir-glob/node_modules/brace-expansion/index.js +78 -22
- package/node_modules/readdir-glob/node_modules/brace-expansion/package.json +1 -1
- package/package.json +1 -1
- package/portal-ui/package.json +2 -2
- package/providers/jx/src/core.js +0 -1
- package/providers/jx/src/judge.js +0 -1
- package/providers/jx/src/nativeread.js +0 -1
- package/providers/oauth-mcp-bridge/CHANGELOG.md +4 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/providers/signa/src/core.js +37 -14
- package/scripts/e2e-first-time.mjs +12 -4
- package/scripts/e2e-scenario-ops.mjs +25 -2
- package/scripts/e2e.mjs +113 -15
- package/scripts/mint-reference-strip-backlog.mjs +36 -2
- package/scripts/score.mjs +102 -29
- package/shared/identifier-scan.mjs +31 -1
package/THIRD-PARTY-NOTICES.md
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
package/driver/CHANGELOG.md
CHANGED
|
@@ -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
|
package/driver/coverage-form.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
package/driver/driver.config.mjs
CHANGED
|
@@ -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
|
-
//
|
|
2061
|
-
//
|
|
2062
|
-
//
|
|
2063
|
-
//
|
|
2064
|
-
//
|
|
2065
|
-
|
|
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:
|
|
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
|
-
|
|
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 `
|
package/driver/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "clearotron-driver",
|
|
3
3
|
"private": true,
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "0.4.0-beta.
|
|
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
|
-
|
|
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,
|
|
1000
|
-
|
|
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 });
|
package/driver/pipeline.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
15140
|
-
|
|
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
|
package/driver/progress.mjs
CHANGED
|
@@ -153,7 +153,12 @@ export function atomicWrite(file, text) {
|
|
|
153
153
|
|
|
154
154
|
function statusPath(runDir) { return join(runDir, "status.json"); }
|
|
155
155
|
|
|
156
|
-
|
|
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
|
|
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;
|
|
1387
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
package/driver/record-carry.mjs
CHANGED
|
@@ -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`.
|