clearotron 0.3.2-beta.6 → 0.3.2-beta.8
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 +24 -23
- package/INSTALL.md +142 -75
- package/README.md +3 -3
- package/bin/onboard.mjs +637 -216
- package/bin/start.mjs +133 -23
- package/bin/update.mjs +82 -11
- package/build-info.json +2 -2
- package/docs/architecture/04-configuration-reference.md +26 -11
- package/docs/architecture/05-config-governance.md +17 -7
- package/driver/CHANGELOG.md +83 -0
- package/driver/band-size.mjs +59 -0
- package/driver/config-inventory.mjs +112 -9
- package/driver/connotation-search.mjs +45 -0
- package/driver/contract-arm2-baseline.json +1 -3
- package/driver/contract-e3-backlog.mjs +29 -29
- package/driver/contract-vocabulary.mjs +59 -25
- package/driver/door-gates.mjs +41 -7
- package/driver/driver.config.mjs +272 -59
- 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/recording-server.mjs +13 -0
- package/driver/engine/openai-agent.mjs +4 -2
- package/driver/engine/probe.mjs +110 -23
- package/driver/findings-model.mjs +1 -1
- package/driver/flag-snapshot.mjs +28 -5
- package/driver/gateway.mjs +30 -21
- package/driver/jx-lanes.mjs +21 -2
- package/driver/jx-units.mjs +6 -3
- package/driver/jx.mjs +4 -2
- package/driver/matter-frame-record.mjs +90 -1
- package/driver/named-band.mjs +34 -2
- package/driver/package.json +1 -1
- package/driver/pipeline.mjs +391 -26
- package/driver/portal-config-view.mjs +30 -1
- package/driver/portal-report.mjs +15 -1
- package/driver/portal-service.mjs +46 -6
- package/driver/predelivery-lint.mjs +12 -2
- package/driver/publish/index.mjs +46 -5
- package/driver/publish/knockout.mjs +10 -1
- package/driver/publish/render-knockout.mjs +69 -7
- package/driver/publish/render.mjs +170 -59
- package/driver/publish/report-data.mjs +4 -1
- package/driver/publish/report-topbar.mjs +58 -0
- package/driver/publish/templates/report.css +28 -2
- package/driver/publish/xlsx.mjs +13 -1
- package/driver/register-availability.mjs +2 -2
- package/driver/register-coverage.mjs +94 -1
- package/driver/register-digest-record.mjs +236 -11
- package/driver/register-plan.mjs +170 -0
- package/driver/result-noun-fields.mjs +7 -4
- package/driver/run-economics.mjs +41 -10
- package/driver/run-requirements.mjs +173 -9
- package/driver/runner.mjs +3 -3
- package/driver/stages.mjs +12 -8
- package/driver/suite-census.json +162 -72
- package/driver/systemd/README.md +7 -4
- package/driver/terminal-clamp.mjs +107 -1
- package/driver/tokens.mjs +169 -3
- package/driver/unit-environment.mjs +42 -15
- package/driver/unit-inventory.mjs +19 -2
- package/driver/verify.mjs +50 -5
- package/mcp-server/CHANGELOG.md +8 -0
- package/mcp-server/http-server.mjs +4 -0
- package/mcp-server/lib/audit.mjs +11 -1
- package/mcp-server/lib/http-handler.mjs +6 -2
- package/mcp-server/package.json +1 -1
- package/mcp-server/server.mjs +16 -2
- package/package.json +1 -1
- package/portal-ui/dist/assets/{index-5UyqAyNM.js → index-6jzO9HiX.js} +155 -79
- package/portal-ui/dist/index.html +1 -1
- package/portal-ui/package.json +1 -1
- package/providers/clarivate/src/capabilities.js +5 -5
- package/providers/clarivate/src/core.js +1 -1
- package/providers/corsearch/src/core.js +2 -2
- 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 +3 -3
- package/providers/signa/src/capabilities.js +5 -6
- package/providers/signa/src/core.js +1 -1
- package/providers/uspto-local/README.md +1 -1
- package/scripts/authority-boundary-probe.mjs +4 -2
- package/scripts/env-audit.mjs +12 -6
- package/scripts/freeze-example-run.mjs +49 -16
- package/scripts/generated-files-are-current.mjs +69 -4
- package/scripts/release-duplicate-notes.mjs +246 -0
- package/scripts/release-publish-guard.mjs +64 -6
- package/scripts/report-print-check.mjs +194 -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/deployment-box.mjs +7 -2
- package/shared/driver-dir.mjs +1 -1
- package/shared/names-in-force.mjs +1 -1
package/providers/jx/README.md
CHANGED
|
@@ -9,7 +9,8 @@ validated at parse.
|
|
|
9
9
|
**One vendor, one billing mode, decided by the run and not by this directory.** The lanes go through
|
|
10
10
|
`engine.runTurn()` — the same door all fourteen agentic stages use — via
|
|
11
11
|
[`../../driver/engine/jx-turn.mjs`](../../driver/engine/jx-turn.mjs). Whatever program the customer configured
|
|
12
|
-
(`CLEAROTRON_AI`) and whatever billing mode the run is on (API key or
|
|
12
|
+
(`CLEAROTRON_AI`) and whatever billing mode the run is on (subscription, API key or, for Claude, a cloud account)
|
|
13
|
+
carries these calls too.
|
|
13
14
|
That is the owner's standing rule — *one LLM provider only ever, API or auth, no mix* — and until 2026-08-20
|
|
14
15
|
these three lanes were the one place in the product that broke it: they POSTed to the Anthropic Messages API on
|
|
15
16
|
`ANTHROPIC_API_KEY` at a hardcoded cheap tier no matter what the rest of the run was doing.
|
|
@@ -106,15 +106,20 @@ export function envelopeFromTurnText(text, toolName) {
|
|
|
106
106
|
* so the ORDER of those checks lives here once rather than three times.
|
|
107
107
|
*
|
|
108
108
|
* `turn` is supplied by the driver and is injectable for tests:
|
|
109
|
-
* async ({prompt, kind}) => { ok, text, truncated, usage:{input,output}, model, vendor, authMode, cause }
|
|
109
|
+
* async ({prompt, kind}) => { ok, text, truncated, usage:{input,output}, model, vendor, authMode, cloud, cause }
|
|
110
110
|
*
|
|
111
111
|
* ATTRIBUTION RIDES EVERY RETURN, including the failures. A degrade still spent tokens, and without the
|
|
112
112
|
* model, vendor and billing mode beside them they cannot be attributed in the run's rollup — which is
|
|
113
113
|
* the half of that says a run must be able to state who did the work.
|
|
114
|
+
*
|
|
115
|
+
* `cloud` is part of that attribution: under the cloud billing mode it names the account that paid
|
|
116
|
+
* ("foundry", "vertex", "bedrock", "gateway"), and it is null under every other mode. The ledger stamp
|
|
117
|
+
* reads it from THIS return, so a field left off here reached every native-language record as
|
|
118
|
+
* `cloud: null` while the main steps of the same run said which cloud paid.
|
|
114
119
|
*/
|
|
115
120
|
export async function runJxTurn({ body, turn, kind, started, truncatedCause, parse }) {
|
|
116
121
|
const t0 = Number.isFinite(started) ? started : Date.now();
|
|
117
|
-
const blank = { tookMs: 0, model: null, vendor: null, authMode: null, usage: null };
|
|
122
|
+
const blank = { tookMs: 0, model: null, vendor: null, authMode: null, cloud: null, usage: null };
|
|
118
123
|
if (typeof turn !== "function") return { ok: false, cause: `${kind}: no turn runner was supplied`, ...blank };
|
|
119
124
|
|
|
120
125
|
const { prompt, toolName } = promptFromRequest(body);
|
|
@@ -124,7 +129,7 @@ export async function runJxTurn({ body, turn, kind, started, truncatedCause, par
|
|
|
124
129
|
|
|
125
130
|
const attribution = {
|
|
126
131
|
tookMs: Date.now() - t0,
|
|
127
|
-
model: r?.model ?? null, vendor: r?.vendor ?? null, authMode: r?.authMode ?? null,
|
|
132
|
+
model: r?.model ?? null, vendor: r?.vendor ?? null, authMode: r?.authMode ?? null, cloud: r?.cloud ?? null,
|
|
128
133
|
// Passed through WHOLE, and `null` stays null. The driver hands over the engine contract's canonical
|
|
129
134
|
// Usage ({input, output, cacheRead, cacheWrite, total}); re-shaping it to two fields here would drop
|
|
130
135
|
// cache and total tokens from the rollup, and a zeroed object in place of null would report a
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trademark-oauth-mcp-bridge",
|
|
3
|
-
"version": "0.3.2-beta.
|
|
3
|
+
"version": "0.3.2-beta.8",
|
|
4
4
|
"license": "AGPL-3.0-only",
|
|
5
5
|
"private": true,
|
|
6
6
|
"description": "OAuth 2.1 MCP stdio bridge used by the engine's case-law gather stage (courtlistener / legaldatahunter).",
|
|
@@ -523,9 +523,9 @@ export function reconcileGridLedger(stdoutStr, spec) {
|
|
|
523
523
|
*
|
|
524
524
|
* Why identity and not a count: the sandbox program TRANSCRIBES the dictated queries into its own source,
|
|
525
525
|
* and a mutated string is a DIFFERENT SEARCH. The count stays right while the dictated query never ran, so
|
|
526
|
-
* every count-based check upstream and downstream reads clean.
|
|
527
|
-
*
|
|
528
|
-
*
|
|
526
|
+
* every count-based check upstream and downstream reads clean. The substitution this catches fuses one
|
|
527
|
+
* row's script into another row's transliteration: the two strings differ, the dictated and recorded
|
|
528
|
+
* counts still match exactly, and one dictated query is never searched by anybody.
|
|
529
529
|
*
|
|
530
530
|
* A query that THREW and said so is not this defect — it owns a gap row (`<query> | connotation | <error>`,
|
|
531
531
|
* or the reconciled object form) and the driver's merge gate weighs it separately. Only a silent
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
// it beats a number that is right for one shape and wrong for the other. See
|
|
35
35
|
// OWNER_SCOPED_WINDOW below, which is the machine-readable half.
|
|
36
36
|
//
|
|
37
|
-
//
|
|
37
|
+
// The total is a separate matter: it saturates at 10000 and flags itself
|
|
38
38
|
// approximate there — a fact about the count, not about the window.
|
|
39
39
|
//
|
|
40
40
|
// PURE: no node imports, no vendor HTTP.
|
|
@@ -129,7 +129,7 @@ export const CAPABILITIES = Object.freeze({
|
|
|
129
129
|
classFilter: "native",
|
|
130
130
|
// Search rows already carry status / nice_classes / owner_name → screening is inline, zero extra calls.
|
|
131
131
|
screenSource: "search-row",
|
|
132
|
-
// No
|
|
132
|
+
// No hard result ceiling, and no total to compare one against.
|
|
133
133
|
resultCeiling: null,
|
|
134
134
|
|
|
135
135
|
// ── the predicates, and WHICH REQUEST SHAPE each one rides ──────────────────────────────────────
|
|
@@ -247,10 +247,9 @@ export const CAPABILITIES = Object.freeze({
|
|
|
247
247
|
// `legacy_code`. asks for `code` to be primary, and it now is, in the one place it decides
|
|
248
248
|
// anything: `SIGNA_OFFICE_CODES` is what a reader and a future translate() should reach for.
|
|
249
249
|
//
|
|
250
|
-
// THE MIGRATION IS NOT URGENT AND THE REASON IS
|
|
251
|
-
//
|
|
252
|
-
//
|
|
253
|
-
// prv/SE 22, ukipo/GB 96, uspto/US 169, wipo/WO 68). Nothing on the wire moves if `translate` keeps
|
|
250
|
+
// THE MIGRATION IS NOT URGENT, AND THE REASON IS A PROPERTY OF THE REGISTER rather than a guess:
|
|
251
|
+
// each legacy key and its ISO code address the same office, so the two spellings are interchangeable
|
|
252
|
+
// for every office this deployment reaches. Nothing on the wire moves if `translate` keeps
|
|
254
253
|
// emitting keys, so it does — a vocabulary swap under the executor buys nothing and risks a live
|
|
255
254
|
// office lookup.
|
|
256
255
|
//
|
|
@@ -316,7 +316,7 @@ export function isSearchResponseBody(body) {
|
|
|
316
316
|
// 21, 101, 18) and — the case that matters — an empty band answered `total_count: 0, approximate:
|
|
317
317
|
// false`, an EXACT zero, which is the only kind this repository is allowed to render.
|
|
318
318
|
//
|
|
319
|
-
//
|
|
319
|
+
// An approximate total of exactly 10000 is a saturation marker rather than an estimate.
|
|
320
320
|
// The vendor is saying "at least ten thousand", and it says so on the broad sweeps (a bare owner
|
|
321
321
|
// filter, `match: similar`, an unanchored `contains`) — precisely the bands a clearance cannot
|
|
322
322
|
// enumerate anyway.
|
|
@@ -155,7 +155,7 @@ what an index actually holds, including whether the 1884 backfile is in it (a da
|
|
|
155
155
|
identical on every other number and is missing a century).
|
|
156
156
|
|
|
157
157
|
This replaces only the *register* half of a clearance
|
|
158
|
-
— the reasoning engine still needs its own subscription or
|
|
158
|
+
— the reasoning engine still needs its own subscription, API key or cloud account, and the unregistered-use half
|
|
159
159
|
still wants `PERPLEXITY_API_KEY`.
|
|
160
160
|
|
|
161
161
|
**It refuses rather than answering zero.** An absent index, a schema with no rows, or an index whose
|
|
@@ -28,7 +28,7 @@ import { join } from "node:path";
|
|
|
28
28
|
import { driverDir, ensureDriverDir } from "../shared/driver-dir.mjs"; // — one definition of where `_driver/` is
|
|
29
29
|
import { tmpdir } from "node:os";
|
|
30
30
|
import { buildClaudeArgs, spawnEnv } from "../driver/engine/anthropic-agent.mjs";
|
|
31
|
-
import {
|
|
31
|
+
import { resolveEngineProgram } from "../driver/driver.config.mjs"; // — the program a run would spawn, found the way a run finds it
|
|
32
32
|
|
|
33
33
|
const arg = (f, d) => { const i = process.argv.indexOf(f); return i > 0 ? process.argv[i + 1] : d; };
|
|
34
34
|
const model = arg("--model", "claude-sonnet-5");
|
|
@@ -73,7 +73,9 @@ console.log(`probe root: ${root}`);
|
|
|
73
73
|
console.log(`--add-dir roots: ${args.filter((a, i) => args[i - 1] === "--add-dir").join(", ")}`);
|
|
74
74
|
console.log(`--settings present: ${args.includes("--settings")}`);
|
|
75
75
|
|
|
76
|
-
|
|
76
|
+
// The program a run would spawn (driver.config.mjs resolveEngineProgram), so this probes the same copy.
|
|
77
|
+
const program = resolveEngineProgram("anthropic-agent");
|
|
78
|
+
const child = spawn(program.resolved ?? program.bin, [...args, "--include-hook-events"],
|
|
77
79
|
{ stdio: ["pipe", "pipe", "pipe"], cwd: runDir, env: spawnEnv() });
|
|
78
80
|
let out = "", err = "";
|
|
79
81
|
child.stdout.on("data", (d) => { out += d; });
|
package/scripts/env-audit.mjs
CHANGED
|
@@ -535,13 +535,18 @@ export function auditEnv(root = ROOT) {
|
|
|
535
535
|
// roster, while ADR-0003 had already ruled case-law setup an OAuth flow
|
|
536
536
|
// and not a variable at all. Evidence of a reader is not evidence of a
|
|
537
537
|
// READ: the roster is a list of names to look for, not a call site.
|
|
538
|
-
// 4 the AZURE_OPENAI_* block, an external contract
|
|
538
|
+
// 4 the AZURE_OPENAI_* block, then kept as an external contract. That call did
|
|
539
|
+
// not hold: nothing the product runs read them, and the rows were removed
|
|
540
|
+
// from `.env.example` on 2026-09-15 (below). They are no longer counted
|
|
541
|
+
// among the wrong deletions.
|
|
539
542
|
// 1 CLEAROTRON_SEND_TOOL_PREFIX — genuinely dead, and this direction does not
|
|
540
543
|
// catch it either: its one surviving mention is a governance-doc line, and
|
|
541
544
|
// a mention is enough to spare a row. Under-firing is the cost of the
|
|
542
545
|
// trade, taken deliberately. It is the prose-sweep class.
|
|
543
546
|
//
|
|
544
|
-
// So
|
|
547
|
+
// So FOUR of ten deletions would have been wrong, the four live reads above,
|
|
548
|
+
// one of them a credential row. (This said six until 2026-09-15; that count
|
|
549
|
+
// did not follow from the rows as listed, and it included the Azure block.)
|
|
545
550
|
// Evidence used instead: the bare NAME, on a name boundary, anywhere in the
|
|
546
551
|
// tracked tree. The accessor family is filed as and does NOT belong here —
|
|
547
552
|
// see below.
|
|
@@ -561,10 +566,11 @@ export function auditEnv(root = ROOT) {
|
|
|
561
566
|
// ── AND IT IS NOT A SUPPRESSION LIST ────────────────────────────────────────────────────────────
|
|
562
567
|
//
|
|
563
568
|
// forbids one, rightly: "if a row is deliberately readerless, the row goes, not the guard."
|
|
564
|
-
// Applied literally that ruling deletes
|
|
565
|
-
//
|
|
566
|
-
//
|
|
567
|
-
//
|
|
569
|
+
// Applied literally, that ruling deletes a row whose consumer is not this tree at all: a variable read
|
|
570
|
+
// by a program the product spawns, such as the Claude program's own sign-in token. The row documents a
|
|
571
|
+
// contract with that consumer, and there is no reader here to retire. (The four AZURE_OPENAI_* rows that
|
|
572
|
+
// first raised this were a different case. They named another platform's settings, nothing the product
|
|
573
|
+
// runs read them, and they were removed from `.env.example` on 2026-09-15.)
|
|
568
574
|
//
|
|
569
575
|
// So a row is ACCOUNTED FOR two ways, and this is one rule applied to every row rather than a list of
|
|
570
576
|
// exempt names: something in the tree names it, OR the row carries an inline `# external:` line
|
|
@@ -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
|
|
@@ -59,21 +61,21 @@ const FROZEN_FILES = [
|
|
|
59
61
|
{ path: "audit.md", why: "publish/index.mjs:1022 auditMd, the audit workbook source" },
|
|
60
62
|
{ path: "findings.json", why: "publish/index.mjs:715 readStore, the per-finding machine contract" },
|
|
61
63
|
{ path: "status.json", why: "publish/index.mjs:913 machineLedgerNote + markName" },
|
|
62
|
-
{ path: "case-law-findings.md", why: "publish/index.mjs:
|
|
63
|
-
{ path: "common-law-grid.json", why: "publish/index.mjs:
|
|
64
|
+
{ path: "case-law-findings.md", why: "publish/index.mjs:875 clPath, the case-law section" },
|
|
65
|
+
{ path: "common-law-grid.json", why: "publish/index.mjs:1006 commonLawJoinedTerms, common-law coverage" },
|
|
64
66
|
// publish/index.mjs — the _driver sidecars it reads by name
|
|
65
67
|
{ path: "_driver/receipts.json", why: "publish/index.mjs:761 fetchReceipts" },
|
|
66
68
|
{ path: "_driver/senior-rights.json", why: "publish/index.mjs:787 seniorRights" },
|
|
67
69
|
{ path: "_driver/verdict.json", why: "publish/index.mjs:792 verdictInfo" },
|
|
68
70
|
{ 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: "publish/index.mjs:
|
|
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: "publish/index.mjs:
|
|
75
|
-
{ path: "_driver/corrections-state.json", why: "publish/index.mjs:
|
|
76
|
-
{ path: "_driver/search-policy.json", why: "publish/index.mjs:
|
|
71
|
+
{ path: "_driver/register-plan.json", why: "publish/index.mjs:846 scopeBasis" },
|
|
72
|
+
{ path: "_driver/instructed-scope.json", why: "publish/index.mjs:847 searchedJurisdictions, the fallback for register-plan" },
|
|
73
|
+
{ path: "_driver/enforcer-signals.json", why: "publish/index.mjs:887 esPath" },
|
|
74
|
+
{ path: "_driver/predelivery-lint.json", why: "publish/index.mjs:172 lintSink" },
|
|
75
|
+
{ path: "_driver/escalation-state.json", why: "publish/index.mjs:173 escSink" },
|
|
76
|
+
{ path: "_driver/reasoning-integrity.json", why: "publish/index.mjs:924 integritySink" },
|
|
77
|
+
{ path: "_driver/corrections-state.json", why: "publish/index.mjs:174 correctionsSink" },
|
|
78
|
+
{ path: "_driver/search-policy.json", why: "publish/index.mjs:987 searchPolicy, level + stage label" },
|
|
77
79
|
{ path: "_driver/profile.json", why: "publish/index.mjs reads the frozen profile; report-registry.mjs:42 republishRun, customer key" },
|
|
78
80
|
];
|
|
79
81
|
|
|
@@ -106,7 +108,7 @@ const KNOCKOUT_FILES = [
|
|
|
106
108
|
// knockout report render empty (publish/knockout.mjs:140-155). Named by stages-knockout.mjs:32,41.
|
|
107
109
|
{ path: "_driver/register-counts.json", why: "publish/knockout.mjs:140-155 counted figures + the Register column" },
|
|
108
110
|
{ 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:
|
|
111
|
+
{ path: "_driver/instructed-scope.json", why: "publish/index.mjs:847 searchedJurisdictions, the fallback for register-plan" },
|
|
110
112
|
];
|
|
111
113
|
|
|
112
114
|
/** The allowlist for a template. One place, so a new template cannot half-exist. */
|
|
@@ -162,7 +164,7 @@ const SCRUB = [
|
|
|
162
164
|
// hides the next real difference.
|
|
163
165
|
const VOLATILE = [
|
|
164
166
|
{ 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:
|
|
167
|
+
{ 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
168
|
];
|
|
167
169
|
|
|
168
170
|
// ── REWRITES — what is CHANGED on the way out, as opposed to what is refused ───────────────────────
|
|
@@ -226,8 +228,19 @@ const substituteVendorKey = (key) => {
|
|
|
226
228
|
// meta.json keys the freeze is EXPECTED to change, with the reason. Anything else differing is a finding.
|
|
227
229
|
const EXPECTED_META_DELTA = {
|
|
228
230
|
tokens: "telemetry pruned — _driver/*.jsonl is the only source (driver/tokens.mjs:82)",
|
|
231
|
+
servedModels: "telemetry pruned — the attempt rows in _driver/*.jsonl are the only source (servedModels in driver/tokens.mjs)",
|
|
229
232
|
};
|
|
230
233
|
|
|
234
|
+
// THE SAME CAUSE, ON THE TWO OTHER SURFACES THAT SHOW IT. report-data.json carries `servedModels` beside the
|
|
235
|
+
// report's content, and the page renders it as the scope section's closing line (render.mjs
|
|
236
|
+
// servedModelsLine, class "servedby"). Only that key and that one paragraph are set aside, on both sides
|
|
237
|
+
// and out loud; a difference anywhere else in either file is still a finding. The paragraph holds escaped
|
|
238
|
+
// text and no markup, so the pattern cannot run past its own closing tag. It takes the whitespace before
|
|
239
|
+
// the paragraph with it: the clearance page joins its scope parts with a line break and an indent, which
|
|
240
|
+
// exists only because the line does.
|
|
241
|
+
const EXPECTED_DATA_DELTA = ["servedModels"];
|
|
242
|
+
const SERVED_LINE_RE = /\s*<p class="servedby"[^>]*>[^<]*<\/p>/g;
|
|
243
|
+
|
|
231
244
|
// ── args ─────────────────────────────────────────────────────────────────────────────────────────────
|
|
232
245
|
const argv = process.argv.slice(2);
|
|
233
246
|
const flag = (name) => { const i = argv.indexOf(name); return i >= 0 ? argv[i + 1] : null; };
|
|
@@ -594,7 +607,27 @@ if (proofOk) {
|
|
|
594
607
|
}
|
|
595
608
|
continue;
|
|
596
609
|
}
|
|
597
|
-
|
|
610
|
+
// The served-model record is set aside by name on both sides (EXPECTED_DATA_DELTA, SERVED_LINE_RE),
|
|
611
|
+
// and only when it is what differed does the note say so.
|
|
612
|
+
let sA = normalise(rawA), sB = normalise(rawB), aside = [];
|
|
613
|
+
if (/^report-data(?:-.+)?\.json$/.test(name)) {
|
|
614
|
+
let dA = null, dB = null;
|
|
615
|
+
try { dA = JSON.parse(rawA); dB = JSON.parse(rawB); } catch { dA = dB = null; }
|
|
616
|
+
if (dA && dB) {
|
|
617
|
+
aside = EXPECTED_DATA_DELTA.filter((k) => JSON.stringify(dA[k]) !== JSON.stringify(dB[k]));
|
|
618
|
+
for (const k of EXPECTED_DATA_DELTA) { delete dA[k]; delete dB[k]; }
|
|
619
|
+
sA = normalise(JSON.stringify(dA, null, 2)); sB = normalise(JSON.stringify(dB, null, 2));
|
|
620
|
+
}
|
|
621
|
+
} else if (name.endsWith(".html") && sA !== sB) {
|
|
622
|
+
const tA = sA.replace(SERVED_LINE_RE, ""), tB = sB.replace(SERVED_LINE_RE, "");
|
|
623
|
+
if (tA === tB) { aside = ["the footer's served-models line"]; sA = tA; sB = tB; }
|
|
624
|
+
}
|
|
625
|
+
if (sA === sB) {
|
|
626
|
+
note(aside.length
|
|
627
|
+
? `${name} identical apart from ${aside.join(", ")}, which differs as expected — ${EXPECTED_META_DELTA.servedModels}`
|
|
628
|
+
: `${name} identical (${rawB.length} bytes)`);
|
|
629
|
+
continue;
|
|
630
|
+
}
|
|
598
631
|
finding(`${name} differs between the source run and the frozen copy — the allowlist dropped an input the renderer reads`);
|
|
599
632
|
}
|
|
600
633
|
}
|
|
@@ -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);
|