clearotron 0.3.2-beta.7 → 0.3.2-beta.9
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/.env.example +58 -26
- package/CONTRIBUTING.md +8 -8
- package/INSTALL.md +148 -81
- package/README.md +3 -3
- package/SECURITY.md +3 -3
- package/bin/brandowner.mjs +3 -3
- package/bin/framework-preflight.mjs +1 -1
- package/bin/onboard.mjs +637 -216
- package/bin/start.mjs +151 -27
- package/bin/update.mjs +58 -11
- package/build-info.json +2 -2
- package/docs/DELIVERY.md +2 -1
- package/docs/INTAKE.md +1 -1
- package/docs/ONBOARDING.md +1 -1
- package/docs/architecture/03-run-lifecycle.md +6 -6
- package/docs/architecture/04-configuration-reference.md +32 -14
- package/docs/architecture/05-config-governance.md +23 -8
- package/docs/architecture/05-customer-profiles.md +2 -2
- package/docs/architecture/06-operations-runbook.md +3 -3
- package/docs/architecture/08-development-guide.md +6 -6
- package/docs/configuration.md +5 -5
- package/docs/decisions/0003-credential-model.md +1 -1
- package/docs/writing-standard.md +4 -0
- package/driver/CHANGELOG.md +124 -0
- package/driver/README.md +3 -3
- package/driver/band-size.mjs +59 -0
- package/driver/binding-layers.mjs +1 -1
- package/driver/citation-census.json +3 -3
- package/driver/{prelim-variants-record.mjs → clearance-variants-record.mjs} +24 -24
- package/driver/common-law-receipts.mjs +2 -2
- package/driver/company-bundle.mjs +3 -3
- package/driver/compose-read.mjs +8 -14
- package/driver/config-inventory.mjs +112 -9
- package/driver/consumption-ledger.mjs +2 -2
- package/driver/contract-arm2-baseline.json +2 -5
- package/driver/contract-dictation-registry.mjs +19 -19
- package/driver/contract-e3-backlog.mjs +43 -43
- package/driver/contract-e3-baseline.json +14 -14
- package/driver/contract-vocabulary.mjs +68 -27
- package/driver/deliver-trigger.sh +16 -16
- package/driver/demo-container.mjs +3 -3
- package/driver/dev-portal.mjs +3 -3
- package/driver/disposition-call.mjs +1 -1
- package/driver/door-gates.mjs +41 -7
- package/driver/doubt-ledger.mjs +2 -2
- package/driver/drainer-identity.mjs +34 -8
- package/driver/driver.config.mjs +367 -104
- package/driver/engine/CONTRACT.md +10 -3
- package/driver/engine/README.md +2 -2
- package/driver/engine/anthropic-agent.mjs +77 -21
- package/driver/engine/auth.mjs +129 -10
- package/driver/engine/jx-turn.mjs +7 -6
- package/driver/engine/mcp/README.md +1 -1
- package/driver/engine/mcp/dispositions-server.mjs +3 -3
- package/driver/engine/mcp/gather-config.mjs +9 -9
- package/driver/engine/mcp/perplexity-server.mjs +2 -2
- package/driver/engine/mcp/recording-server.mjs +18 -5
- package/driver/engine/openai-agent.mjs +4 -2
- package/driver/engine/probe.mjs +110 -23
- package/driver/enqueue-schema.mjs +6 -2
- package/driver/findings-model.mjs +6 -3
- package/driver/flag-snapshot.mjs +34 -8
- package/driver/form-neighbourhood.mjs +54 -7
- package/driver/framework.mjs +4 -4
- package/driver/gateway.mjs +36 -24
- package/driver/jx-lanes.mjs +23 -4
- package/driver/jx-units.mjs +7 -4
- package/driver/jx.mjs +34 -4
- package/driver/knockout-review-record.mjs +56 -4
- package/driver/known-conflicts.mjs +1 -1
- package/driver/matter-frame-record.mjs +90 -1
- package/driver/named-band.mjs +1 -1
- package/driver/ordinary-words.mjs +51 -0
- package/driver/outbox-backoff.mjs +31 -16
- package/driver/package.json +1 -1
- package/driver/partial-payload-baseline.json +2 -2
- package/driver/phase0.mjs +3 -3
- package/driver/pipeline-knockout.mjs +5 -5
- package/driver/pipeline.mjs +396 -81
- package/driver/placement-form.mjs +77 -1
- package/driver/placement-model.mjs +1 -1
- package/driver/portal-config-view.mjs +30 -1
- package/driver/portal-report.mjs +107 -6
- package/driver/portal-service.mjs +80 -14
- package/driver/portal-upstream.mjs +1 -1
- package/driver/predelivery-lint.mjs +12 -2
- package/driver/preserve-merge.mjs +3 -3
- package/driver/product-rows.mjs +2 -2
- package/driver/products.mjs +1 -1
- package/driver/profiles/README.md +3 -3
- package/driver/profiles/demo-brand-owner.json +2 -2
- package/driver/profiles.mjs +55 -17
- package/driver/progress.mjs +18 -8
- package/driver/provider-usage.mjs +8 -8
- package/driver/publish/index.mjs +154 -8
- package/driver/publish/knockout.mjs +39 -5
- package/driver/publish/pool-admin.mjs +1 -1
- package/driver/publish/publish-inputs.mjs +18 -2
- package/driver/publish/render-knockout.mjs +184 -31
- package/driver/publish/render.mjs +323 -93
- package/driver/publish/report-data.mjs +4 -1
- package/driver/publish/report-topbar.mjs +58 -0
- package/driver/publish/search-depth.mjs +133 -4
- package/driver/publish/templates/report.css +78 -4
- package/driver/publish/xlsx.mjs +20 -1
- package/driver/queue-order.mjs +2 -2
- package/driver/recording-agreement.mjs +1 -1
- package/driver/reference-score.mjs +1 -1
- package/driver/register-availability.mjs +2 -2
- package/driver/register-count.mjs +50 -5
- package/driver/register-coverage.mjs +161 -1
- package/driver/register-digest-record.mjs +236 -11
- package/driver/register-grant-vocabulary.mjs +1 -1
- package/driver/register-plan.mjs +189 -2
- package/driver/registry-fidelity.mjs +3 -3
- package/driver/repair-composers.mjs +1 -1
- package/driver/repair-contract.mjs +1 -1
- package/driver/replay-archive.mjs +6 -6
- package/driver/report-overview-record.mjs +2 -2
- package/driver/result-noun-fields.mjs +2 -2
- package/driver/run-economics.mjs +41 -10
- package/driver/run-requirements.mjs +173 -9
- package/driver/runner.mjs +5 -5
- package/driver/scope-facts.mjs +20 -5
- package/driver/scope-ledger.mjs +5 -5
- package/driver/search-policy.mjs +22 -12
- package/driver/skills/README.md +15 -15
- package/driver/skills/blind-frame/SKILL.md +2 -2
- package/driver/skills/case-law-citation/SKILL.md +4 -4
- package/driver/skills/case-law-citation/sources/eurlex.md +1 -1
- package/driver/skills/{prelim-common-law → clearance-common-law}/SKILL.md +22 -22
- package/driver/skills/{prelim-common-law → clearance-common-law}/perplexity-prompts.md +1 -1
- package/driver/skills/{prelim-register → clearance-register}/SKILL.md +10 -10
- package/driver/skills/{prelim-register → clearance-register}/digest.md +2 -2
- package/driver/skills/{prelim-register → clearance-register}/providers/README.md +1 -1
- package/driver/skills/{prelim-register → clearance-register}/providers/clarivate.md +37 -35
- package/driver/skills/{prelim-register → clearance-register}/providers/corsearch.md +20 -11
- package/driver/skills/{prelim-register → clearance-register}/providers/signa.md +5 -5
- package/driver/skills/{prelim-register → clearance-register}/register-recipes.md +3 -3
- package/driver/skills/{prelim-register → clearance-register}/status-rules.md +2 -2
- package/driver/skills/{prelim-register → clearance-register}/stealth-filer-indicators.md +1 -1
- package/driver/skills/{prelim-register → clearance-register}/unit.md +2 -2
- package/driver/skills/{prelim-search → clearance-search}/SKILL.md +31 -31
- package/driver/skills/{prelim-search → clearance-search}/delivery-contract.md +1 -1
- package/driver/skills/{prelim-search → clearance-search}/phase2-execution.md +18 -18
- package/driver/skills/{prelim-search → clearance-search}/synthesis-rules.md +7 -7
- package/driver/skills/{prelim-variants → clearance-variants}/SKILL.md +18 -18
- package/driver/skills/{prelim-variants → clearance-variants}/transliteration-scripts.md +5 -5
- package/driver/skills/frame-diff/SKILL.md +1 -1
- package/driver/skills/knockout-assess/SKILL.md +10 -7
- package/driver/skills/matter-frame/SKILL.md +3 -3
- package/driver/skills/narrative-refutation/SKILL.md +9 -9
- package/driver/skills/placement-inquiry/SKILL.md +5 -5
- package/driver/stage-context.mjs +1 -1
- package/driver/stages-knockout.mjs +4 -4
- package/driver/stages.mjs +65 -61
- package/driver/status-snapshot.mjs +2 -2
- package/driver/suite-census.json +340 -136
- package/driver/surface-exit-verdict.mjs +58 -0
- package/driver/systemd/README.md +9 -6
- package/driver/systemd/clearotron-worker.service +1 -1
- package/driver/terminal-clamp.mjs +109 -1
- package/driver/tokens.mjs +169 -3
- package/driver/unit-environment.mjs +42 -15
- package/driver/unit-inventory.mjs +19 -2
- package/driver/usage-ledger.mjs +1 -1
- package/driver/variant-manifest-model.mjs +4 -4
- package/driver/verify-knockout.mjs +27 -0
- package/driver/verify.mjs +94 -6
- package/driver/whatif-queue.mjs +1 -1
- package/driver/wordlists/en.txt +63906 -0
- package/mcp-server/CHANGELOG.md +8 -0
- package/mcp-server/README.md +1 -1
- package/mcp-server/lib/README.md +1 -1
- package/mcp-server/lib/options.mjs +8 -7
- package/mcp-server/lib/plan.mjs +18 -2
- package/mcp-server/lib/runs.mjs +1 -1
- package/mcp-server/lib/usage.mjs +3 -3
- package/mcp-server/lib/whatif.mjs +1 -1
- package/mcp-server/package.json +1 -1
- package/mcp-server/server.mjs +18 -1
- package/package.json +12 -11
- package/portal-ui/dist/assets/{index-CVOIvdhc.css → index-CtvwLCti.css} +207 -3
- package/portal-ui/dist/assets/{index-5UyqAyNM.js → index-EVaSo5-g.js} +1580 -527
- package/portal-ui/dist/index.html +2 -2
- package/portal-ui/package.json +1 -1
- package/providers/README.md +1 -1
- package/providers/_shared/enumerate.mjs +6 -6
- package/providers/_shared/execute-plan.mjs +3 -3
- package/providers/_shared/ledger.mjs +119 -5
- package/providers/_shared/provider-text.mjs +2 -2
- package/providers/_shared/screen.mjs +2 -2
- package/providers/_shared/script-form.mjs +3 -3
- package/providers/_shared/territory-codes.mjs +23 -3
- package/providers/clarivate/README.md +1 -1
- package/providers/clarivate/src/capabilities.js +12 -12
- package/providers/clarivate/src/core.js +37 -43
- package/providers/corsearch/README.md +1 -1
- package/providers/corsearch/src/capabilities.js +5 -5
- package/providers/corsearch/src/core.js +3 -3
- package/providers/jx/README.md +2 -1
- package/providers/jx/src/turn-envelope.mjs +8 -3
- package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/providers/perplexity/src/core.js +1 -1
- package/providers/signa/README.md +1 -1
- package/providers/signa/src/capabilities.js +42 -49
- package/providers/signa/src/core.js +106 -29
- package/providers/uspto-local/README.md +1 -1
- package/providers/uspto-local/src/sync.js +1 -1
- package/scripts/README.md +1 -0
- package/scripts/ask-ai-render-check.mjs +127 -1
- package/scripts/authority-boundary-probe.mjs +8 -6
- package/scripts/backfill-started-at.mjs +2 -2
- package/scripts/census-merge-driver.mjs +33 -2
- package/scripts/citation-anchor-report.mjs +181 -0
- package/scripts/dead-names.mjs +1 -1
- package/scripts/deprecate-below.mjs +66 -8
- package/scripts/drain-preflight.mjs +1 -1
- package/scripts/e2e.mjs +174 -0
- package/scripts/env-audit.mjs +39 -6
- package/scripts/env-classify.mjs +20 -2
- package/scripts/freeze-example-run.mjs +61 -18
- package/scripts/generated-files-are-current.mjs +69 -4
- package/scripts/live-surface-check.mjs +124 -41
- package/scripts/markdown-link-check.mjs +1 -1
- package/scripts/merge-shape-check.mjs +242 -0
- package/scripts/mint-names-in-force.mjs +5 -5
- package/scripts/mint-offered-territories.mjs +72 -0
- package/scripts/mint-public-residue.mjs +2 -2
- package/scripts/mint-reference-strip-backlog.mjs +2 -2
- package/scripts/mint-suite-census.mjs +75 -2
- package/scripts/mint-writing-standard-backlog.mjs +2 -2
- package/scripts/purge-runs.mjs +7 -7
- package/scripts/reconcile-runs.mjs +2 -2
- package/scripts/release-approve-parked.mjs +20 -2
- package/scripts/release-await-cut.mjs +120 -1
- package/scripts/release-note-required.mjs +76 -8
- package/scripts/report-header-render-check.mjs +164 -0
- package/scripts/settings-render-check.mjs +75 -2
- package/scripts/test-full.mjs +96 -3
- package/scripts/test-run.mjs +10 -0
- package/shared/brand.mjs +27 -0
- package/shared/connect-clients.mjs +39 -11
- package/shared/deployment-box.mjs +7 -2
- package/shared/driver-dir.mjs +1 -1
- package/shared/env-aliases.mjs +1 -1
- package/shared/identifier-scan.mjs +65 -9
- package/shared/identifier-sentinels.mjs +22 -0
- package/shared/names-in-force.mjs +4 -2
- package/shared/offered-territories.json +738 -0
- package/shared/pre-rename-spellings.mjs +53 -0
- package/shared/reference-guard-classes.mjs +40 -2
- package/shared/stdio-connect.mjs +39 -4
- package/shared/tree-commit.mjs +48 -0
- /package/driver/skills/{prelim-register → clearance-register}/providers/euipo.md +0 -0
- /package/driver/skills/{prelim-register → clearance-register}/providers/free-tier.md +0 -0
- /package/driver/skills/{prelim-register → clearance-register}/providers/uspto-local.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/field-doctrine-pharma.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/firm-wide-reasoning.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/report-prose.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/risk-framework-demo.manifest.json +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/risk-framework-demo.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/risk-framework-triage.manifest.json +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/risk-framework-triage.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/risk-framework.manifest.json +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/risk-framework.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/template-formatting.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/templates/email/generic.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/templates/search-request-form.html +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/worked-examples-demo.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/worked-examples.md +0 -0
package/scripts/e2e.mjs
CHANGED
|
@@ -68,6 +68,10 @@ import { doorGates, resolveForDoor } from "../driver/door-gates.mjs";
|
|
|
68
68
|
// — the plan-row term screen, shared with the driver rather than restated here, so the harness
|
|
69
69
|
// can never drift from what the plan freeze and the executor enforce. PURE (no node imports).
|
|
70
70
|
import { entryTermIssues } from "../providers/_shared/term-shape.mjs";
|
|
71
|
+
// — the mechanical tier vocabulary, imported from the module that classifies into it rather than
|
|
72
|
+
// restated here: a floor naming a tier that does not exist must fail loudly rather than compare against
|
|
73
|
+
// a missing key, and the list must not drift if a tier is ever added. PURE (no node imports) too.
|
|
74
|
+
import { SHAPE_TIERS } from "../driver/band-shape.mjs";
|
|
71
75
|
// The owner's turnaround benchmarks. The BAND owns the number and a scenario file cannot carry
|
|
72
76
|
// one — see the lint rule below. Another pure leaf with zero imports of its own, for the same reason
|
|
73
77
|
// queue-markers.mjs is: nothing in this file may reach driver.config.mjs.
|
|
@@ -1256,6 +1260,70 @@ export function pathsAnOpDoesNotRead(scenarios) {
|
|
|
1256
1260
|
return out;
|
|
1257
1261
|
}
|
|
1258
1262
|
|
|
1263
|
+
/**
|
|
1264
|
+
* The office a band-shape floor row belongs to, from the row's own fields and never from a wider table.
|
|
1265
|
+
*
|
|
1266
|
+
* TWO SOURCES, IN THE ORDER band-shape.mjs POPULATES THEM. `registry` is the projection's own field and
|
|
1267
|
+
* it is derived, so it can read `unknown`; the record id carries the office as its second path segment.
|
|
1268
|
+
* Measured on a preserved dense clearance run, 2026-09-18: 289 of 289 rows carried `registry`, and every
|
|
1269
|
+
* one agreed with its id's segment. The fallback exists for the row that does not, and a row that
|
|
1270
|
+
* answers neither way is counted separately by the caller rather than dropped.
|
|
1271
|
+
*/
|
|
1272
|
+
export const bandRowOffice = (r) => {
|
|
1273
|
+
const reg = String(r?.registry ?? "").trim().toLowerCase();
|
|
1274
|
+
if (reg && reg !== "unknown") return reg;
|
|
1275
|
+
const m = String(r?.record_id ?? "").match(/^\/[^/]+\/([^/]+)\//);
|
|
1276
|
+
return m ? m[1].toLowerCase() : "";
|
|
1277
|
+
};
|
|
1278
|
+
|
|
1279
|
+
/**
|
|
1280
|
+
* Open a clearance run's band shape for a floor op: the document, the mark check, and the two coverage
|
|
1281
|
+
* qualifications — or ONE sentence saying why no floor under it can be read.
|
|
1282
|
+
*
|
|
1283
|
+
* THE MARK IS CHECKED AGAINST `targets`, NOT AGAINST `targets[0]`. The driver shapes a band for the mark
|
|
1284
|
+
* under search PLUS its manifest variants, and it puts the job's own mark in that list unconditionally;
|
|
1285
|
+
* the first entry is the variant lane's to order. Membership is therefore the property that holds while
|
|
1286
|
+
* the variant lane moves, and it still catches the case this check is for — a shape derived for some
|
|
1287
|
+
* other mark entirely. A mismatch says so in its own words rather than folding into a missed floor,
|
|
1288
|
+
* because "the store's mark moved" and "the register thinned" need opposite answers.
|
|
1289
|
+
*/
|
|
1290
|
+
function bandShapeUnder(full, file, field) {
|
|
1291
|
+
const bad = (saw) => ({ bad: saw });
|
|
1292
|
+
if (!existsSync(full)) return bad(`${file} absent — the clearance lane derives this after every register band re-merge, so an absent `
|
|
1293
|
+
+ `shape is a derivation that did not happen or a lane that never merged a band. It is not a register that holds nothing.`);
|
|
1294
|
+
const doc = readJson(full);
|
|
1295
|
+
if (!doc) return bad(`${file} present but unparseable`);
|
|
1296
|
+
const want = field ? String(field).trim().toLowerCase() : null;
|
|
1297
|
+
if (!want) return bad("no mark given (path must be <file>:<MARK NAME>)");
|
|
1298
|
+
const targets = Array.isArray(doc.targets) ? doc.targets.filter((t) => String(t ?? "").trim()) : [];
|
|
1299
|
+
if (!targets.length) return bad(`${file} carries no targets — nothing in it says which mark the band was shaped for, so no floor under a mark can be read from it`);
|
|
1300
|
+
if (!targets.some((t) => String(t).trim().toLowerCase() === want)) {
|
|
1301
|
+
const shown = targets.slice(0, 6).map((t) => JSON.stringify(String(t))).join(", ");
|
|
1302
|
+
return bad(`this shape was derived for targets that do not include ${JSON.stringify(field)} — it was shaped for ${shown}`
|
|
1303
|
+
+ `${targets.length > 6 ? `, and ${targets.length - 6} more` : ""}. The store's mark moved, or this run is another mark's; either way no floor here is about the mark the scenario named.`);
|
|
1304
|
+
}
|
|
1305
|
+
const spots = Array.isArray(doc.blind_spots) ? doc.blind_spots : [];
|
|
1306
|
+
// The register would not answer: a refused slice, or one it would not state a total for. These make a
|
|
1307
|
+
// shortfall genuinely ambiguous, so they qualify a pass and may be the reason for a miss.
|
|
1308
|
+
const unread = spots.filter((b) => b?.kind === "refused-slice" || b?.kind === "uncountable-slice")
|
|
1309
|
+
.map((b) => `${b.count ?? "?"} ${b.kind}`);
|
|
1310
|
+
// The register DID answer, with a number larger than what was fetched. That is evidence of size, not a
|
|
1311
|
+
// gap in it — see the block at the ops — so it is named with the depth read and excuses nothing.
|
|
1312
|
+
const shallow = spots.filter((b) => b?.kind === "unenumerated-crowd")
|
|
1313
|
+
.map((b) => `${b.count ?? "?"} ${b.kind}${Array.isArray(b.read_depth) && b.read_depth.length ? ` (read ${b.read_depth.join("/")} deep)` : ""}`);
|
|
1314
|
+
const verdict = (ok, passBody, missBody) => {
|
|
1315
|
+
const parts = [ok ? passBody : missBody];
|
|
1316
|
+
if (unread.length) parts.push(ok
|
|
1317
|
+
? `floor met, but the register refused or would not total ${unread.join(", ")}, so this run covered less than it asked for`
|
|
1318
|
+
: `and the register refused or would not total ${unread.join(", ")}, so the shortfall may be what it would not answer rather than what it holds`);
|
|
1319
|
+
if (shallow.length) parts.push(ok
|
|
1320
|
+
? `${shallow.join(", ")} — the register stated more than the funnel fetched there`
|
|
1321
|
+
: `and ${shallow.join(", ")} — the register stated more than the funnel fetched there, which is a finding about the read depth and NOT a reason to lower the floor`);
|
|
1322
|
+
return { ok, saw: parts.join(" — ") };
|
|
1323
|
+
};
|
|
1324
|
+
return { doc, unread, shallow, verdict };
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1259
1327
|
function evalAssertion(a, runDir) {
|
|
1260
1328
|
const [file, field] = String(a.path ?? "").split(":");
|
|
1261
1329
|
const full = join(runDir, file || "");
|
|
@@ -1637,6 +1705,112 @@ function evalAssertion(a, runDir) {
|
|
|
1637
1705
|
saw: failed.length ? `${body}, and ${failed.length} term(s) FAILED to fetch, so the shortfall may be the fetch and not the register: ${failed.join(" · ")}`
|
|
1638
1706
|
: `${body} — the register returned fewer rows than the hit path needs, so screening, hydration and citation ran on less than this scenario exists to give them` };
|
|
1639
1707
|
}
|
|
1708
|
+
// ── · THE SAME TWO QUESTIONS, ASKED OF THE CLEARANCE LANE'S OWN ARTIFACT ──────────────────────
|
|
1709
|
+
//
|
|
1710
|
+
// WHY A SECOND PAIR RATHER THAN A SECOND PATH ON THE PAIR ABOVE. `register-counts.json` and
|
|
1711
|
+
// `register-records.json` are written by the KNOCKOUT lane and by nothing else. A clearance scenario
|
|
1712
|
+
// asserting the floors above therefore gets `absent` on both, and the sentence those absences print —
|
|
1713
|
+
// "a run whose register lane wrote nothing is not a run that found nothing" — is true of the file and
|
|
1714
|
+
// false of the run: the clearance lane read the band and recorded it under other names. The record it
|
|
1715
|
+
// keeps is `_driver/band-shape.json`, derived after every named-band re-merge, and it is a different
|
|
1716
|
+
// shape entirely rather than the same shape in another place, so it needs its own two ops.
|
|
1717
|
+
//
|
|
1718
|
+
// THREE POPULATIONS, THREE DIFFERENT NUMBERS, AND CONFLATING THEM IS THE TRAP THIS PAIR SITS IN.
|
|
1719
|
+
// Measured on a preserved dense clearance run, 2026-09-18, all three true of one band at one moment:
|
|
1720
|
+
//
|
|
1721
|
+
// totals.records 1554 every in-scope record the merged band holds
|
|
1722
|
+
// totals.by_tier.identical 245 tiered across the WHOLE band, class filter and all
|
|
1723
|
+
// floors.in_class_identical_or_near 289 identical AND near-identical, live, in-class only
|
|
1724
|
+
//
|
|
1725
|
+
// The band's own delivered depth record counted 2098, because it counts everything READ rather than
|
|
1726
|
+
// what stayed in scope. A floor written against one of these and read against another drifts with
|
|
1727
|
+
// nobody noticing, which is the failure the floors exist to catch happening to the floors themselves.
|
|
1728
|
+
// So every number these ops print names its population in words, and neither op mixes two.
|
|
1729
|
+
//
|
|
1730
|
+
// BOTH KEEP THE TWO RULES THE PAIR ABOVE IS BUILT ON. A size that was never taken is never a small
|
|
1731
|
+
// one: an absent shape, a shape with no `totals`, no `by_tier` or no floors array is reported as a
|
|
1732
|
+
// derivation that did not happen, never as a band that holds nothing. And a slice the register refused
|
|
1733
|
+
// or would not total is reduced coverage, named whichever way the floor goes.
|
|
1734
|
+
//
|
|
1735
|
+
// A CROWD THE FUNNEL COULD NOT ENUMERATE IS NOT THAT, AND MUST NEVER EXCUSE A SHORTFALL. Its
|
|
1736
|
+
// `total_hits` is LARGER than what was fetched, which is the register stating that the band is at
|
|
1737
|
+
// least that big — positive evidence of size, the opposite of a coverage gap. A dense band carries
|
|
1738
|
+
// these by construction (19 of them on the run measured above), so letting them soften a miss would
|
|
1739
|
+
// make every genuine thinning report as "the register may not have answered" for ever. They are named
|
|
1740
|
+
// with the depth actually read, and they change no verdict.
|
|
1741
|
+
if (a.op === "band-count-floor" || a.op === "band-records-floor") {
|
|
1742
|
+
const shape = bandShapeUnder(full, file, field);
|
|
1743
|
+
if (shape.bad) return { ok: false, saw: shape.bad };
|
|
1744
|
+
const value = a.value && typeof a.value === "object" && !Array.isArray(a.value) ? a.value : null;
|
|
1745
|
+
|
|
1746
|
+
if (a.op === "band-count-floor") {
|
|
1747
|
+
if (!value) return { ok: false, saw: "value must be an object of floors, e.g. {\"records\": 1000, \"by_tier\": {\"identical\": 120}}" };
|
|
1748
|
+
const unknown = Object.keys(value).filter((k) => k !== "records" && k !== "by_tier");
|
|
1749
|
+
if (unknown.length) return { ok: false, saw: `unknown floor key(s) ${unknown.map((k) => JSON.stringify(k)).join(", ")} — this op takes `
|
|
1750
|
+
+ `"records" (the whole in-scope band) and "by_tier" (one floor per mechanical tier). A key nothing reads is a floor nobody asserts.` };
|
|
1751
|
+
const totals = shape.doc.totals && typeof shape.doc.totals === "object" ? shape.doc.totals : null;
|
|
1752
|
+
if (!totals) return { ok: false, saw: `${file} carries no totals — the band was never sized, which is not a band whose size is zero` };
|
|
1753
|
+
|
|
1754
|
+
const saw = [], short = [], untaken = [];
|
|
1755
|
+
if (Object.hasOwn(value, "records")) {
|
|
1756
|
+
const want = value.records;
|
|
1757
|
+
if (!Number.isFinite(want)) return { ok: false, saw: `"records" must be a number, not ${JSON.stringify(want)}` };
|
|
1758
|
+
if (!Number.isFinite(totals.records)) untaken.push(`records: NOT TAKEN — totals.records is ${JSON.stringify(totals.records ?? null)}`);
|
|
1759
|
+
else {
|
|
1760
|
+
saw.push(`${totals.records} record(s) in the whole in-scope band (floor ${want})`);
|
|
1761
|
+
if (!(totals.records >= want)) short.push(`the in-scope band is ${totals.records} record(s), below the floor of ${want}`);
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
if (Object.hasOwn(value, "by_tier")) {
|
|
1765
|
+
const tiers = value.by_tier && typeof value.by_tier === "object" && !Array.isArray(value.by_tier) ? value.by_tier : null;
|
|
1766
|
+
if (!tiers) return { ok: false, saw: "\"by_tier\" must be an object of tier floors, e.g. {\"identical\": 120}" };
|
|
1767
|
+
const byTier = totals.by_tier && typeof totals.by_tier === "object" ? totals.by_tier : null;
|
|
1768
|
+
if (!byTier) return { ok: false, saw: `${file} carries no totals.by_tier — the band was never tiered, which is not a band holding no identical marks` };
|
|
1769
|
+
for (const [tier, want] of Object.entries(tiers)) {
|
|
1770
|
+
if (!SHAPE_TIERS.includes(tier)) { untaken.push(`${tier}: NO SUCH TIER — the mechanical tiers are ${SHAPE_TIERS.join(", ")}`); continue; }
|
|
1771
|
+
if (!Number.isFinite(want)) return { ok: false, saw: `the floor for ${tier} must be a number, not ${JSON.stringify(want)}` };
|
|
1772
|
+
const n = byTier[tier];
|
|
1773
|
+
if (!Number.isFinite(n)) { untaken.push(`${tier}: NOT TAKEN — totals.by_tier.${tier} is ${JSON.stringify(n ?? null)}`); continue; }
|
|
1774
|
+
saw.push(`${n} record(s) tiered ${tier} across the whole band (floor ${want})`);
|
|
1775
|
+
if (!(n >= want)) short.push(`${tier} is ${n}, below the floor of ${want}`);
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
if (!saw.length && !short.length && !untaken.length)
|
|
1779
|
+
return { ok: false, saw: "no floor stated — give \"records\", \"by_tier\" or both; an op asserting nothing is an assert that looked at nothing" };
|
|
1780
|
+
if (untaken.length) return { ok: false, saw: `the band was never sized that way, so this scenario proved nothing about the size it exists for — ${untaken.join(" · ")}` };
|
|
1781
|
+
return shape.verdict(short.length === 0, saw.join(" · "),
|
|
1782
|
+
`${short.join(" · ")} — either the register has thinned out under this mark or the query narrowed; re-measure before moving the floor`);
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
// The population behind the count, and the two properties the size-dependent paths need from it:
|
|
1786
|
+
// enough rows to work on, and rows from more than one office.
|
|
1787
|
+
//
|
|
1788
|
+
// BOTH NUMBERS COME OUT OF THE SAME LIST, DELIBERATELY. The shape also carries a `by_registry` table
|
|
1789
|
+
// over the whole band, and taking the office span from there while taking the row count from the
|
|
1790
|
+
// floor list would report a multi-office band whose every floor row was one office — which is the
|
|
1791
|
+
// exact fail-open the knockout records floor names: a total-only floor is met while the property it
|
|
1792
|
+
// exists for lapses with every assert green. One list, both properties, or neither is asserted.
|
|
1793
|
+
const floors = value ?? {};
|
|
1794
|
+
const unknown = Object.keys(floors).filter((k) => k !== "records" && k !== "offices");
|
|
1795
|
+
if (unknown.length) return { ok: false, saw: `unknown floor key(s) ${unknown.map((k) => JSON.stringify(k)).join(", ")} — this op takes `
|
|
1796
|
+
+ `"records" (rows on the floor list) and "offices" (how many the list spans).` };
|
|
1797
|
+
const minRecords = Number.isFinite(floors.records) ? floors.records : 1;
|
|
1798
|
+
const minOffices = Number.isFinite(floors.offices) ? floors.offices : 1;
|
|
1799
|
+
const rows = shape.doc.floors?.in_class_identical_or_near;
|
|
1800
|
+
if (!Array.isArray(rows)) return { ok: false, saw: `${file} carries no floors.in_class_identical_or_near array — the one list that is `
|
|
1801
|
+
+ `complete by construction was never written, which is not a band holding no identical marks` };
|
|
1802
|
+
const offices = [...new Set(rows.map(bandRowOffice).filter(Boolean))].sort();
|
|
1803
|
+
// A row whose office cannot be read is counted and named rather than dropped: it narrows the span
|
|
1804
|
+
// this op can see, and a span that is narrow because the field is missing is not a narrow band.
|
|
1805
|
+
const officeless = rows.filter((r) => !bandRowOffice(r)).length;
|
|
1806
|
+
const met = rows.length >= minRecords && offices.length >= minOffices;
|
|
1807
|
+
const body = `${rows.length} live in-class identical/near-identical floor row(s) (floor ${minRecords}) across `
|
|
1808
|
+
+ `${offices.length} office(s) [${offices.join(", ") || "none"}] (floor ${minOffices})`
|
|
1809
|
+
+ (officeless ? `, and ${officeless} row(s) carry no office, so the span read here is a floor on the span, not a count of it` : "");
|
|
1810
|
+
return shape.verdict(met, body,
|
|
1811
|
+
`${body} — the band the clearance lane merged is smaller or narrower than this scenario exists to hand the size-dependent paths`);
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1640
1814
|
// field ops
|
|
1641
1815
|
const fn = OPS[a.op];
|
|
1642
1816
|
if (!fn) return { ok: false, saw: `UNIMPLEMENTED op "${a.op}" — not passing by omission`, unimplemented: true };
|
package/scripts/env-audit.mjs
CHANGED
|
@@ -203,6 +203,22 @@ export function mergeEnvNameBindings(perFile) {
|
|
|
203
203
|
// read as `env[X]`, for the reason READ_RE gives.
|
|
204
204
|
const CONST_READ_RE = /(?<![.\w$])(?:process\??\.)?env(?:\?\.)?\[\s*([A-Z][A-Z0-9_]*)\s*\](?!\s*=[^=])/g;
|
|
205
205
|
|
|
206
|
+
// THE NAME HELD IN A TABLE AS A VALUE, which is a read the three readers above cannot see. A generic
|
|
207
|
+
// resolver takes the name out of a row and reads it — `{ env: "CLEAROTRON_CODEX_PATH", fallback: "codex" }`
|
|
208
|
+
// — so the variable is read on every run and spelled nowhere the audit was looking. Thirteen names are
|
|
209
|
+
// read exactly this way, and two of them are live deployment settings that no rule could ask for a row
|
|
210
|
+
// for, because every ratchet that would is keyed on the name being seen.
|
|
211
|
+
//
|
|
212
|
+
// MATCHED ON THE PROPERTY NAME, NEVER ON THE VALUE'S SHAPE, and that is the whole reason this is safe.
|
|
213
|
+
// A rule that took any uppercase string literal would take every message key, token and enum member in
|
|
214
|
+
// the tree and drown the audit in names nothing reads. The property names are the closed set the
|
|
215
|
+
// resolvers actually use; a new resolver that invents a fourth spelling is invisible again, which is a
|
|
216
|
+
// known limit of this shape rather than a defect in the rule — the same limit the accessor reader has.
|
|
217
|
+
//
|
|
218
|
+
// This is the third time this shape has hidden a read here: the accessor family was the first, `envFrom`
|
|
219
|
+
// the second, and a name held as a value is those two one remove further on.
|
|
220
|
+
const TABLE_NAME_RE = /(?<![.\w$])(?:env|envName|tokenEnv)\s*:\s*["']([A-Z][A-Z0-9_]*)["']/g;
|
|
221
|
+
|
|
206
222
|
export function namesRead(text, bindings = null) {
|
|
207
223
|
const found = new Set();
|
|
208
224
|
const stripped = stripCommentLines(text);
|
|
@@ -210,6 +226,8 @@ export function namesRead(text, bindings = null) {
|
|
|
210
226
|
for (let m; (m = READ_RE.exec(stripped));) found.add(m[1] || m[2]);
|
|
211
227
|
ACCESSOR_RE.lastIndex = 0;
|
|
212
228
|
for (let m; (m = ACCESSOR_RE.exec(stripped));) found.add(m[1] || m[2]);
|
|
229
|
+
TABLE_NAME_RE.lastIndex = 0;
|
|
230
|
+
for (let m; (m = TABLE_NAME_RE.exec(stripped));) found.add(m[1]);
|
|
213
231
|
// Resolved through the corpus map when one is supplied. Absent map ⇒ this half is simply off, which
|
|
214
232
|
// is what keeps the function pure and drivable on a single string.
|
|
215
233
|
if (bindings) {
|
|
@@ -357,6 +375,15 @@ export const EFFECT_CLASSES = Object.freeze({
|
|
|
357
375
|
credential: "authentication material; absent, the run refuses at preflight by name",
|
|
358
376
|
deployment: "where input and output live; the conclusion a run reaches is unchanged",
|
|
359
377
|
tuning: "how long or how hard a run tries; the conclusion a run reaches is unchanged",
|
|
378
|
+
// WHAT THE INSTALL WIZARD WROTE WHEN IT SET THIS MACHINE UP. Added because the classifier had computed
|
|
379
|
+
// this class for years and no declaration could say it: five names computed `setup` against a
|
|
380
|
+
// vocabulary with no word for it, so each was either undeclared or declared as the nearest wrong
|
|
381
|
+
// thing. A declaration that cannot be true is worse than none — it reads as considered.
|
|
382
|
+
//
|
|
383
|
+
// It is its own class rather than a flavour of `deployment` because the two answer different
|
|
384
|
+
// questions. `deployment` says where this machine keeps things; `setup` says what the installer
|
|
385
|
+
// found or made here, which is the fact an operator needs when a program has moved.
|
|
386
|
+
setup: "what the install wizard found or wrote when it set this machine up; the conclusion a run reaches is unchanged",
|
|
360
387
|
harness: "read only on a fixture, replay or self-test path; no production run reaches it",
|
|
361
388
|
});
|
|
362
389
|
|
|
@@ -535,13 +562,18 @@ export function auditEnv(root = ROOT) {
|
|
|
535
562
|
// roster, while ADR-0003 had already ruled case-law setup an OAuth flow
|
|
536
563
|
// and not a variable at all. Evidence of a reader is not evidence of a
|
|
537
564
|
// READ: the roster is a list of names to look for, not a call site.
|
|
538
|
-
// 4 the AZURE_OPENAI_* block, an external contract
|
|
565
|
+
// 4 the AZURE_OPENAI_* block, then kept as an external contract. That call did
|
|
566
|
+
// not hold: nothing the product runs read them, and the rows were removed
|
|
567
|
+
// from `.env.example` on 2026-09-15 (below). They are no longer counted
|
|
568
|
+
// among the wrong deletions.
|
|
539
569
|
// 1 CLEAROTRON_SEND_TOOL_PREFIX — genuinely dead, and this direction does not
|
|
540
570
|
// catch it either: its one surviving mention is a governance-doc line, and
|
|
541
571
|
// a mention is enough to spare a row. Under-firing is the cost of the
|
|
542
572
|
// trade, taken deliberately. It is the prose-sweep class.
|
|
543
573
|
//
|
|
544
|
-
// So
|
|
574
|
+
// So FOUR of ten deletions would have been wrong, the four live reads above,
|
|
575
|
+
// one of them a credential row. (This said six until 2026-09-15; that count
|
|
576
|
+
// did not follow from the rows as listed, and it included the Azure block.)
|
|
545
577
|
// Evidence used instead: the bare NAME, on a name boundary, anywhere in the
|
|
546
578
|
// tracked tree. The accessor family is filed as and does NOT belong here —
|
|
547
579
|
// see below.
|
|
@@ -561,10 +593,11 @@ export function auditEnv(root = ROOT) {
|
|
|
561
593
|
// ── AND IT IS NOT A SUPPRESSION LIST ────────────────────────────────────────────────────────────
|
|
562
594
|
//
|
|
563
595
|
// forbids one, rightly: "if a row is deliberately readerless, the row goes, not the guard."
|
|
564
|
-
// Applied literally that ruling deletes
|
|
565
|
-
//
|
|
566
|
-
//
|
|
567
|
-
//
|
|
596
|
+
// Applied literally, that ruling deletes a row whose consumer is not this tree at all: a variable read
|
|
597
|
+
// by a program the product spawns, such as the Claude program's own sign-in token. The row documents a
|
|
598
|
+
// contract with that consumer, and there is no reader here to retire. (The four AZURE_OPENAI_* rows that
|
|
599
|
+
// first raised this were a different case. They named another platform's settings, nothing the product
|
|
600
|
+
// runs read them, and they were removed from `.env.example` on 2026-09-15.)
|
|
568
601
|
//
|
|
569
602
|
// So a row is ACCOUNTED FOR two ways, and this is one rule applied to every row rather than a list of
|
|
570
603
|
// exempt names: something in the tree names it, OR the row carries an inline `# external:` line
|
package/scripts/env-classify.mjs
CHANGED
|
@@ -355,6 +355,19 @@ export const DEPLOYMENT_NAMES = new Set([
|
|
|
355
355
|
// Set by the install on the copy it starts after moving out of npx's cache, never by an operator. Its
|
|
356
356
|
// row declares `deployment`, and no shape matches it, so unlisted it would fall through to `tuning`.
|
|
357
357
|
"CLEAROTRON_RELOCATED",
|
|
358
|
+
// The two register ledgers, listed the day the scanner learned to read a name held in a table as a
|
|
359
|
+
// value — the same shape as the optional-chaining three below, one scanner widening later. Both name
|
|
360
|
+
// WHERE a file lives and resolve by an existence ladder over several directories, which is the
|
|
361
|
+
// definition `CLEAROTRON_JX_SUBCLASS_DB` was moved here on: a row whose own words name a place is
|
|
362
|
+
// `deployment`, and it reaches `tuning` only because `tuning` is the residual.
|
|
363
|
+
//
|
|
364
|
+
// GETTING THIS WRONG IS WORSE THAN THE BLINDNESS IT REPLACES. No deployed box sets either — the
|
|
365
|
+
// ledger module says so and explains why the default is resolved by existence rather than by name —
|
|
366
|
+
// so `tuning` with an empty set-site list is precisely the population step 3 deletes from. Making
|
|
367
|
+
// them visible without listing them here would move the billing-grade call ledger and the record
|
|
368
|
+
// ledger a "verified from the record" claim joins against from unseen to proposed for deletion.
|
|
369
|
+
"CLEAROTRON_REGISTER_CALL_LOG",
|
|
370
|
+
"CLEAROTRON_REGISTER_RECORD_LOG",
|
|
358
371
|
// Three names read only through optional chaining, so no catalogue check saw them until the scanner
|
|
359
372
|
// learned `?.`. Each row declares `deployment`, and no shape matches them (`_PORT$` does not take
|
|
360
373
|
// `_PORTS`), so unlisted each fell through to `tuning`, the bucket step 3 deletes from. The first is set
|
|
@@ -502,7 +515,12 @@ export function classify({ catalogue, sources, setup = setupNames(), readSites =
|
|
|
502
515
|
};
|
|
503
516
|
|
|
504
517
|
const cls = (name) => OVERRIDES[name] ?? (setup.has(name) ? "setup"
|
|
505
|
-
|
|
518
|
+
// `credential`, the word the declared vocabulary uses, NOT a second word for the same thing. This
|
|
519
|
+
// computed `vendor-credential` while no row could declare it, which is the same gap that left the
|
|
520
|
+
// install wizard's names undeclarable — one side of the contract saying a word the other side has
|
|
521
|
+
// no way to say. The rule settled with that one holds here: the classifier and the vocabulary say
|
|
522
|
+
// the SAME word, and a disagreement is fixed rather than frozen as an exception.
|
|
523
|
+
: VENDOR_RE.test(name) ? "credential"
|
|
506
524
|
// The listed audience is consulted BEFORE the shapes, so a renamed name keeps the class a human gave
|
|
507
525
|
// it rather than the one its new spelling happens to match.
|
|
508
526
|
: deploymentNames.has(name) ? "deployment"
|
|
@@ -678,7 +696,7 @@ function build() {
|
|
|
678
696
|
_what: "#1838 step 1 — every catalogued variable classified, and for every TUNING name the environments that have ever set it.",
|
|
679
697
|
_how: "Regenerate with: node scripts/env-classify.mjs --check (the production half is read from docs/architecture/env-set-in-production.txt, which needs `--gather-prod` on the production box to refresh).",
|
|
680
698
|
_values: "NO VALUE from any environment is read into this artifact. Names only.",
|
|
681
|
-
_counts: { rows: rows.length, setup: by("setup"), deployment: by("deployment"),
|
|
699
|
+
_counts: { rows: rows.length, setup: by("setup"), deployment: by("deployment"), credential: by("credential"), tuning: by("tuning") },
|
|
682
700
|
_stepThreePopulation: {
|
|
683
701
|
_what: "What step 3 may act on: a numeric default, and no environment anywhere sets it.",
|
|
684
702
|
_warning: "A candidate list, not a licence. Each still needs its read site read and its guard checked before deletion.",
|
|
@@ -29,9 +29,11 @@
|
|
|
29
29
|
// _driver/run.jsonl the event log
|
|
30
30
|
// _driver/stage-inputs/ what each stage was handed
|
|
31
31
|
// _history/ pre-reopen snapshots
|
|
32
|
-
// Dropping the telemetry drops `meta.tokens` (driver/publish/index.mjs
|
|
33
|
-
// rollupTokens)
|
|
34
|
-
//
|
|
32
|
+
// Dropping the telemetry drops `meta.tokens` (driver/publish/index.mjs rollupTokens — the only consumer of
|
|
33
|
+
// rollupTokens), and with it the record of which models served the run (servedModels in
|
|
34
|
+
// driver/tokens.mjs): `servedModels` on meta.json and report-data.json, and the one line that closes the
|
|
35
|
+
// report's footer. Those are the differences step 5 is told to expect, and it says so out loud rather
|
|
36
|
+
// than normalising them away in silence.
|
|
35
37
|
//
|
|
36
38
|
// WHAT THIS SCRIPT DOES NOT DO
|
|
37
39
|
// It does not decide the sample is publishable. It greps for the shapes that must never leave the VM
|
|
@@ -45,7 +47,8 @@ import {
|
|
|
45
47
|
} from "node:fs";
|
|
46
48
|
import { join, dirname, relative, basename } from "node:path";
|
|
47
49
|
import { createHash } from "node:crypto";
|
|
48
|
-
import { driverDir } from "../shared/driver-dir.mjs";
|
|
50
|
+
import { driverDir } from "../shared/driver-dir.mjs";
|
|
51
|
+
import { PUBLISH_INPUTS } from "../driver/publish/publish-inputs.mjs"; //
|
|
49
52
|
import { tmpdir } from "node:os";
|
|
50
53
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
51
54
|
|
|
@@ -59,24 +62,33 @@ const FROZEN_FILES = [
|
|
|
59
62
|
{ path: "audit.md", why: "publish/index.mjs:1022 auditMd, the audit workbook source" },
|
|
60
63
|
{ path: "findings.json", why: "publish/index.mjs:715 readStore, the per-finding machine contract" },
|
|
61
64
|
{ path: "status.json", why: "publish/index.mjs:913 machineLedgerNote + markName" },
|
|
62
|
-
{ path: "case-law-findings.md", why: "
|
|
63
|
-
{ path: "common-law-grid.json", why: "
|
|
65
|
+
{ path: "case-law-findings.md", why: "`clPath` declared in index.mjs, the case-law section" },
|
|
66
|
+
{ path: "common-law-grid.json", why: "`commonLawJoinedTerms` declared in index.mjs, common-law coverage" },
|
|
64
67
|
// publish/index.mjs — the _driver sidecars it reads by name
|
|
65
68
|
{ path: "_driver/receipts.json", why: "publish/index.mjs:761 fetchReceipts" },
|
|
66
69
|
{ path: "_driver/senior-rights.json", why: "publish/index.mjs:787 seniorRights" },
|
|
67
70
|
{ path: "_driver/verdict.json", why: "publish/index.mjs:792 verdictInfo" },
|
|
68
71
|
{ path: "_driver/framework.json", why: "publish/index.mjs, the frozen band vocabulary the run was rated under" },
|
|
69
|
-
{ path: "_driver/register-plan.json", why: "publish/index.mjs:
|
|
70
|
-
{ path: "_driver/instructed-scope.json", why: "publish/index.mjs:
|
|
71
|
-
{ path: "_driver/enforcer-signals.json", why: "
|
|
72
|
-
{ path: "_driver/predelivery-lint.json", why: "publish/index.mjs:
|
|
73
|
-
{ path: "_driver/escalation-state.json", why: "publish/index.mjs:
|
|
74
|
-
{ path: "_driver/reasoning-integrity.json", why: "
|
|
75
|
-
{ path: "_driver/corrections-state.json", why: "publish/index.mjs:
|
|
76
|
-
{ path: "_driver/search-policy.json", why: "
|
|
72
|
+
{ path: "_driver/register-plan.json", why: "publish/index.mjs:901 scopeBasis" },
|
|
73
|
+
{ path: "_driver/instructed-scope.json", why: "publish/index.mjs:902 searchedJurisdictions, the fallback for register-plan" },
|
|
74
|
+
{ path: "_driver/enforcer-signals.json", why: "`esPath` declared in index.mjs" },
|
|
75
|
+
{ path: "_driver/predelivery-lint.json", why: "publish/index.mjs:172 lintSink" },
|
|
76
|
+
{ path: "_driver/escalation-state.json", why: "publish/index.mjs:173 escSink" },
|
|
77
|
+
{ path: "_driver/reasoning-integrity.json", why: "`integritySink` declared in index.mjs" },
|
|
78
|
+
{ path: "_driver/corrections-state.json", why: "publish/index.mjs:174 correctionsSink" },
|
|
79
|
+
{ path: "_driver/search-policy.json", why: "`searchPolicy` declared in index.mjs, level + stage label" },
|
|
77
80
|
{ path: "_driver/profile.json", why: "publish/index.mjs reads the frozen profile; report-registry.mjs:42 republishRun, customer key" },
|
|
78
81
|
];
|
|
79
82
|
|
|
83
|
+
// EVERY INPUT THE PUBLISHER DECLARES TRAVELS, taken from its own closed table rather than restated. The
|
|
84
|
+
// hand-kept list above drifted from that table: register-named-band.json, the recall receipt and the
|
|
85
|
+
// local-language lane's two files were declared publish inputs and were left behind, so a frozen republish
|
|
86
|
+
// drew no register section and no local-language row where the source run drew both. A declared input
|
|
87
|
+
// added later now travels without an edit here; one the publisher does not declare still needs a line above.
|
|
88
|
+
for (const path of Object.keys(PUBLISH_INPUTS)) {
|
|
89
|
+
if (!FROZEN_FILES.some((f) => f.path === path)) FROZEN_FILES.push({ path, why: "declared in publish/publish-inputs.mjs" });
|
|
90
|
+
}
|
|
91
|
+
|
|
80
92
|
// ── THE KNOCKOUT LANE IS A DIFFERENT WORKSPACE, AND report.md IS NOT IN IT ─
|
|
81
93
|
//
|
|
82
94
|
// A knockout run never writes report.md: for that lane the markdown is an OUTPUT of publish, not an
|
|
@@ -106,7 +118,7 @@ const KNOCKOUT_FILES = [
|
|
|
106
118
|
// knockout report render empty (publish/knockout.mjs:140-155). Named by stages-knockout.mjs:32,41.
|
|
107
119
|
{ path: "_driver/register-counts.json", why: "publish/knockout.mjs:140-155 counted figures + the Register column" },
|
|
108
120
|
{ path: "_driver/register-records.json", why: "stages-knockout.mjs:41 the terms behind the close-variation axis" },
|
|
109
|
-
{ path: "_driver/instructed-scope.json", why: "publish/index.mjs:
|
|
121
|
+
{ path: "_driver/instructed-scope.json", why: "publish/index.mjs:902 searchedJurisdictions, the fallback for register-plan" },
|
|
110
122
|
];
|
|
111
123
|
|
|
112
124
|
/** The allowlist for a template. One place, so a new template cannot half-exist. */
|
|
@@ -162,7 +174,7 @@ const SCRUB = [
|
|
|
162
174
|
// hides the next real difference.
|
|
163
175
|
const VOLATILE = [
|
|
164
176
|
{ id: "issued", re: /\d{4}-\d{2}-\d{2} · \d{2}:\d{2} [A-Z]{2,5}/g, sub: "<issued>", why: "publish/index.mjs, the generation stamp in the firm locale" },
|
|
165
|
-
{ id: "iso-timestamp", re: /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z/g, sub: "<ts>", why: "publish/index.mjs:
|
|
177
|
+
{ id: "iso-timestamp", re: /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z/g, sub: "<ts>", why: "publish/index.mjs:668 asOf" },
|
|
166
178
|
];
|
|
167
179
|
|
|
168
180
|
// ── REWRITES — what is CHANGED on the way out, as opposed to what is refused ───────────────────────
|
|
@@ -174,7 +186,7 @@ const VOLATILE = [
|
|
|
174
186
|
//
|
|
175
187
|
// 1. THE BIRTH CODENAME, IN CONTENT. `--codename` rebuilds the run's IDENTITY — the leaf, the runId, the
|
|
176
188
|
// directory — and the content keeps the old pair: `_receipt.context` in every record artifact reads
|
|
177
|
-
// "
|
|
189
|
+
// "clearance-<matter>-<codename>-register-unit-primary-sweep", and audit.md and status.json carry it in
|
|
178
190
|
// prose. driver/test/no-client-identifiers.test.mjs check 2 sweeps CONTENT as well as paths, so a
|
|
179
191
|
// tree renamed but not rewritten can never be committed — which is exactly what the old echo check
|
|
180
192
|
// reported, correctly, and could do nothing about.
|
|
@@ -226,8 +238,19 @@ const substituteVendorKey = (key) => {
|
|
|
226
238
|
// meta.json keys the freeze is EXPECTED to change, with the reason. Anything else differing is a finding.
|
|
227
239
|
const EXPECTED_META_DELTA = {
|
|
228
240
|
tokens: "telemetry pruned — _driver/*.jsonl is the only source (driver/tokens.mjs:82)",
|
|
241
|
+
servedModels: "telemetry pruned — the attempt rows in _driver/*.jsonl are the only source (servedModels in driver/tokens.mjs)",
|
|
229
242
|
};
|
|
230
243
|
|
|
244
|
+
// THE SAME CAUSE, ON THE TWO OTHER SURFACES THAT SHOW IT. report-data.json carries `servedModels` beside the
|
|
245
|
+
// report's content, and the page renders it as the scope section's closing line (render.mjs
|
|
246
|
+
// servedModelsLine, class "servedby"). Only that key and that one paragraph are set aside, on both sides
|
|
247
|
+
// and out loud; a difference anywhere else in either file is still a finding. The paragraph holds escaped
|
|
248
|
+
// text and no markup, so the pattern cannot run past its own closing tag. It takes the whitespace before
|
|
249
|
+
// the paragraph with it: the clearance page joins its scope parts with a line break and an indent, which
|
|
250
|
+
// exists only because the line does.
|
|
251
|
+
const EXPECTED_DATA_DELTA = ["servedModels"];
|
|
252
|
+
const SERVED_LINE_RE = /\s*<p class="servedby"[^>]*>[^<]*<\/p>/g;
|
|
253
|
+
|
|
231
254
|
// ── args ─────────────────────────────────────────────────────────────────────────────────────────────
|
|
232
255
|
const argv = process.argv.slice(2);
|
|
233
256
|
const flag = (name) => { const i = argv.indexOf(name); return i >= 0 ? argv[i + 1] : null; };
|
|
@@ -594,7 +617,27 @@ if (proofOk) {
|
|
|
594
617
|
}
|
|
595
618
|
continue;
|
|
596
619
|
}
|
|
597
|
-
|
|
620
|
+
// The served-model record is set aside by name on both sides (EXPECTED_DATA_DELTA, SERVED_LINE_RE),
|
|
621
|
+
// and only when it is what differed does the note say so.
|
|
622
|
+
let sA = normalise(rawA), sB = normalise(rawB), aside = [];
|
|
623
|
+
if (/^report-data(?:-.+)?\.json$/.test(name)) {
|
|
624
|
+
let dA = null, dB = null;
|
|
625
|
+
try { dA = JSON.parse(rawA); dB = JSON.parse(rawB); } catch { dA = dB = null; }
|
|
626
|
+
if (dA && dB) {
|
|
627
|
+
aside = EXPECTED_DATA_DELTA.filter((k) => JSON.stringify(dA[k]) !== JSON.stringify(dB[k]));
|
|
628
|
+
for (const k of EXPECTED_DATA_DELTA) { delete dA[k]; delete dB[k]; }
|
|
629
|
+
sA = normalise(JSON.stringify(dA, null, 2)); sB = normalise(JSON.stringify(dB, null, 2));
|
|
630
|
+
}
|
|
631
|
+
} else if (name.endsWith(".html") && sA !== sB) {
|
|
632
|
+
const tA = sA.replace(SERVED_LINE_RE, ""), tB = sB.replace(SERVED_LINE_RE, "");
|
|
633
|
+
if (tA === tB) { aside = ["the footer's served-models line"]; sA = tA; sB = tB; }
|
|
634
|
+
}
|
|
635
|
+
if (sA === sB) {
|
|
636
|
+
note(aside.length
|
|
637
|
+
? `${name} identical apart from ${aside.join(", ")}, which differs as expected — ${EXPECTED_META_DELTA.servedModels}`
|
|
638
|
+
: `${name} identical (${rawB.length} bytes)`);
|
|
639
|
+
continue;
|
|
640
|
+
}
|
|
598
641
|
finding(`${name} differs between the source run and the frozen copy — the allowlist dropped an input the renderer reads`);
|
|
599
642
|
}
|
|
600
643
|
}
|
|
@@ -53,6 +53,29 @@ export function treeState(root = ROOT) {
|
|
|
53
53
|
* The alternative — dirtying a real generated file and restoring it — is a shared-file mutation, and
|
|
54
54
|
* the test runner runs files in parallel, so it would be a race that reddens somebody else's arm.
|
|
55
55
|
*/
|
|
56
|
+
/**
|
|
57
|
+
* Which paths differ between two `git status --porcelain` readings, named so a reader can see WHAT moved.
|
|
58
|
+
*
|
|
59
|
+
* A porcelain line is a two-character state, a space, and the path. Lines are compared as a multiset so
|
|
60
|
+
* a path whose STATE changed — staged to modified, say — is reported as having moved, and the paths are
|
|
61
|
+
* returned rather than a count, because two numbers agreeing is not the same as two sets agreeing.
|
|
62
|
+
*/
|
|
63
|
+
export function movedPaths(before, after) {
|
|
64
|
+
const bag = (s) => {
|
|
65
|
+
const m = new Map();
|
|
66
|
+
for (const line of String(s).split("\n")) {
|
|
67
|
+
if (!line.trim()) continue;
|
|
68
|
+
m.set(line, (m.get(line) ?? 0) + 1);
|
|
69
|
+
}
|
|
70
|
+
return m;
|
|
71
|
+
};
|
|
72
|
+
const [b, a] = [bag(before), bag(after)];
|
|
73
|
+
const out = new Set();
|
|
74
|
+
for (const [line, n] of a) if ((b.get(line) ?? 0) !== n) out.add(line.slice(3).trim() || line.trim());
|
|
75
|
+
for (const [line, n] of b) if ((a.get(line) ?? 0) !== n) out.add(line.slice(3).trim() || line.trim());
|
|
76
|
+
return [...out].sort();
|
|
77
|
+
}
|
|
78
|
+
|
|
56
79
|
export function checkAll({ dir = HERE, root = ROOT, log = console.log, readTree = () => treeState(root) } = {}) {
|
|
57
80
|
const found = minters(dir);
|
|
58
81
|
if (!found.length) return { found, stale: [], unreadable: [], wrote: [], empty: true };
|
|
@@ -60,6 +83,7 @@ export function checkAll({ dir = HERE, root = ROOT, log = console.log, readTree
|
|
|
60
83
|
const stale = [];
|
|
61
84
|
const unreadable = [];
|
|
62
85
|
const wrote = [];
|
|
86
|
+
const unattributable = [];
|
|
63
87
|
for (const m of found) {
|
|
64
88
|
// ── `--check` IS A CONTRACT, AND NOTHING WAS VERIFYING IT ──────────────────────────────────────
|
|
65
89
|
//
|
|
@@ -73,13 +97,41 @@ export function checkAll({ dir = HERE, root = ROOT, log = console.log, readTree
|
|
|
73
97
|
// one ran, it wrote. THE LIMIT, SAID RATHER THAN LEFT: this catches the harmful inert form, the
|
|
74
98
|
// one that silently repairs. A minter that ignores the flag and does nothing at all still reports
|
|
75
99
|
// `current`, and no probe from out here can tell that from a file that really is current.
|
|
100
|
+
// ── "DID THE TREE MOVE" IS NOT "DID THIS PROCESS WRITE" ────────────────────────────────────────
|
|
101
|
+
//
|
|
102
|
+
// Those are the same question only where nothing else can write, and this probe does not run
|
|
103
|
+
// there. Inside the suite it is a subprocess of one test file while every other file in its shard
|
|
104
|
+
// runs beside it, deliberately unserialised — dropping `--test-concurrency=1` is what made the
|
|
105
|
+
// suite 2.4 times faster. So a neighbour writing anywhere in the repository moved the snapshot and
|
|
106
|
+
// this reported it as the minter having written.
|
|
107
|
+
//
|
|
108
|
+
// Measured: a commit whose whole diff was one stylesheet's phone-width rules and a release note
|
|
109
|
+
// failed here naming TWO minters, and the same bytes passed on a rerun. Two is the tell — a minter
|
|
110
|
+
// that ignores `--check` and re-mints leaves its repair in the tree, so the NEXT minter's `before`
|
|
111
|
+
// already carries it and the next one is not flagged. Both being named cannot come from either.
|
|
112
|
+
//
|
|
113
|
+
// So the accusation is made only where it can be: from a tree that was CLEAN when this minter
|
|
114
|
+
// started, where a change appearing during its run has no other author available. A tree that was
|
|
115
|
+
// already dirty is one where somebody else is writing, and the honest answer is that this probe
|
|
116
|
+
// could not look — which is this file's own rule one level in, since it already refuses to read a
|
|
117
|
+
// minter that could not look as a pass.
|
|
118
|
+
//
|
|
119
|
+
// THE LIMIT, SAID RATHER THAN LEFT: a neighbour that begins writing after a clean reading and
|
|
120
|
+
// before the minter exits is still attributed here. Closing that needs the probe to own the tree,
|
|
121
|
+
// which is a change to where it runs rather than to what it asks.
|
|
76
122
|
const before = readTree();
|
|
77
123
|
const r = spawnSync(process.execPath, [join(dir, m), "--check"], { cwd: root, encoding: "utf8" });
|
|
78
124
|
const after = readTree();
|
|
79
125
|
const out = ((r.stdout || "") + (r.stderr || "")).trim();
|
|
80
126
|
if (before !== null && after !== null && before !== after) {
|
|
81
|
-
|
|
82
|
-
|
|
127
|
+
const moved = movedPaths(before, after);
|
|
128
|
+
if (before.trim() === "") {
|
|
129
|
+
wrote.push({ m, out, moved });
|
|
130
|
+
log(` WROTE ${m} (during --check): ${moved.join(", ") || "the tree moved"}`);
|
|
131
|
+
} else {
|
|
132
|
+
unattributable.push({ m, moved });
|
|
133
|
+
log(` ? ${m} — the tree moved and this probe cannot say who moved it: ${moved.join(", ") || "paths unknown"}`);
|
|
134
|
+
}
|
|
83
135
|
continue;
|
|
84
136
|
}
|
|
85
137
|
// 0 is current, 1 is stale, anything else is a minter that could not look — reported separately,
|
|
@@ -89,11 +141,11 @@ export function checkAll({ dir = HERE, root = ROOT, log = console.log, readTree
|
|
|
89
141
|
unreadable.push({ m, out, code: r.status });
|
|
90
142
|
log(` ? ${m} (exit ${r.status})`);
|
|
91
143
|
}
|
|
92
|
-
return { found, stale, unreadable, wrote, empty: false, contractChecked: readTree() !== null };
|
|
144
|
+
return { found, stale, unreadable, wrote, unattributable, empty: false, contractChecked: readTree() !== null };
|
|
93
145
|
}
|
|
94
146
|
|
|
95
147
|
function main() {
|
|
96
|
-
const { found, stale, unreadable, wrote, empty, contractChecked } = checkAll();
|
|
148
|
+
const { found, stale, unreadable, wrote, unattributable, empty, contractChecked } = checkAll();
|
|
97
149
|
if (empty) {
|
|
98
150
|
console.error("generated-files-are-current: no scripts/mint-*.mjs found. Either they moved or the "
|
|
99
151
|
+ "naming changed — and a pass over nothing is not a pass.");
|
|
@@ -111,6 +163,19 @@ function main() {
|
|
|
111
163
|
+ `commit without the repair, and reports current over a check that did not happen. Fix the minter.`);
|
|
112
164
|
process.exit(2);
|
|
113
165
|
}
|
|
166
|
+
// BEFORE the staleness verdict, and exit 2 rather than 1: a tree moving under the probe means the
|
|
167
|
+
// staleness answers were read off a tree that was changing while they were taken, so "out of date" is
|
|
168
|
+
// not a claim this run has the standing to make either. Could-not-look is the whole verdict.
|
|
169
|
+
if (unattributable.length) {
|
|
170
|
+
console.error(`\n${unattributable.length} minter(s) ran while the tree was ALREADY dirty and it moved `
|
|
171
|
+
+ `underneath them. This probe cannot say whether the minter wrote or something running beside it `
|
|
172
|
+
+ `did, so it names neither. That is a could-not-look, not a pass and not an accusation.\n`);
|
|
173
|
+
for (const { m, moved } of unattributable) {
|
|
174
|
+
console.error(` ${m} — moved: ${moved.join(", ") || "paths unknown"}`);
|
|
175
|
+
}
|
|
176
|
+
console.error(`\nRun it on a tree nobody else is writing to, and it will answer.`);
|
|
177
|
+
process.exit(2);
|
|
178
|
+
}
|
|
114
179
|
if (unreadable.length) {
|
|
115
180
|
console.error(`\n${unreadable.length} minter(s) could not look. That is not a pass; fix the minter first.`);
|
|
116
181
|
process.exit(2);
|