clearotron 0.3.2-beta.13 → 0.3.2-beta.15
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/bin/clearotron.mjs +7 -1
- package/bin/example.mjs +49 -22
- package/build-info.json +2 -2
- package/driver/CHANGELOG.md +22 -0
- package/driver/contract-vocabulary.mjs +5 -5
- package/driver/engine/mcp/codex-config.mjs +3 -11
- package/driver/named-band.mjs +1 -1
- package/driver/package.json +1 -1
- package/driver/pipeline-knockout.mjs +3 -1
- package/driver/pipeline.mjs +30 -7
- package/driver/publish/index.mjs +10 -1
- package/driver/publish/render-knockout.mjs +31 -6
- package/driver/publish/render.mjs +54 -2
- package/driver/publish/templates/report.css +16 -1
- package/driver/register-availability.mjs +2 -2
- package/driver/register-plan.mjs +229 -55
- package/driver/skills/clearance-register/unit.md +1 -1
- package/driver/skills/clearance-variants/SKILL.md +10 -4
- package/driver/stages.mjs +1 -1
- package/driver/suite-census.json +45 -3
- package/driver/variant-manifest-model.mjs +39 -1
- package/mcp-server/CHANGELOG.md +8 -0
- package/mcp-server/lib/audit-view.mjs +5 -3
- package/mcp-server/lib/brief.mjs +10 -4
- package/mcp-server/lib/runs.mjs +35 -1
- package/mcp-server/package.json +1 -1
- package/mcp-server/server.mjs +6 -3
- package/package.json +1 -1
- package/portal-ui/dist/assets/{index-7Lq-dXDV.css → index-5CCwiJG7.css} +10 -0
- package/portal-ui/dist/assets/{index-w8GFZftk.js → index-DMthc7PQ.js} +8 -1
- package/portal-ui/dist/index.html +2 -2
- package/portal-ui/package.json +1 -1
- package/providers/_shared/execute-plan.mjs +11 -1
- package/providers/_shared/term-shape.mjs +76 -0
- package/providers/clarivate/src/capabilities.js +36 -0
- package/providers/clarivate/src/core.js +119 -0
- package/providers/corsearch/src/capabilities.js +24 -0
- package/providers/corsearch/src/core.js +7 -0
- package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/providers/signa/src/capabilities.js +29 -0
- package/providers/signa/src/core.js +17 -0
- package/shared/demo-start-args.mjs +10 -0
- package/shared/stdio-connect.mjs +41 -10
- package/shared/toml-string.mjs +25 -0
package/bin/clearotron.mjs
CHANGED
|
@@ -25,6 +25,7 @@ import { isEntrypoint } from "../shared/is-entrypoint.mjs";
|
|
|
25
25
|
import { nodeFloorVerdict, nodeFloorRefusal } from "../shared/node-floor.mjs"; // — one floor, read from package.json
|
|
26
26
|
import { invocationPrefix } from "../shared/invocation.mjs"; // — print a command the reader can type
|
|
27
27
|
import { watchParent } from "../shared/parent-watch.mjs";
|
|
28
|
+
import { EXPERIMENTAL_WARNING_OFF } from "../shared/demo-start-args.mjs";
|
|
28
29
|
|
|
29
30
|
export const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
30
31
|
|
|
@@ -193,7 +194,12 @@ const [verb, ...rest] = process.argv.slice(2);
|
|
|
193
194
|
// — tell the child how the READER reached us, so its own advice names a command they can type.
|
|
194
195
|
// Without this every spawned verb sees argv[1] = its own implementation file and would print `npx`
|
|
195
196
|
// even for somebody who typed a bare `clearotron`.
|
|
196
|
-
|
|
197
|
+
// THE DEMO RUNS WITHOUT NODE'S EXPERIMENTAL-FEATURE WARNING. Node prints one the first time anything loads
|
|
198
|
+
// its built-in SQLite, and on the demo it landed on the first screen, above the sentence saying what the
|
|
199
|
+
// demo is. A flag on the demo's own process; `bin/example.mjs` hands it on to the services it starts
|
|
200
|
+
// (EXPERIMENTAL_WARNING_OFF). Every other warning still prints.
|
|
201
|
+
const quiet = verb === "demo" ? [EXPERIMENTAL_WARNING_OFF] : [];
|
|
202
|
+
const child = spawn(process.execPath, [...quiet, target, ...builtin, ...rest], {
|
|
197
203
|
stdio: "inherit",
|
|
198
204
|
env: { ...process.env, CLEAROTRON_INVOKED_AS: process.argv[1] ?? "" },
|
|
199
205
|
});
|
package/bin/example.mjs
CHANGED
|
@@ -38,7 +38,7 @@ import "../shared/env-local.mjs"; // step 4 / — FIRST: this program read a
|
|
|
38
38
|
// handed it nothing and the read fell through to a default. Proven both ways from one
|
|
39
39
|
// environment: without this import the value is invisible, with it the retired spelling is
|
|
40
40
|
// back-filled. Placed above every other import because a side-effecting import runs in order.
|
|
41
|
-
import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
41
|
+
import { appendFileSync, cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
42
42
|
import { homedir, tmpdir } from "node:os";
|
|
43
43
|
import { removeDirectory } from "../shared/os-advice.mjs";
|
|
44
44
|
import { invoke } from "../shared/invocation.mjs"; // — the printed command is resolved once, for the reader who is actually standing there
|
|
@@ -53,7 +53,7 @@ import { ensureDemoProgram, demoProgramEnv } from "../shared/permanent-install.m
|
|
|
53
53
|
const REPO = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
54
54
|
|
|
55
55
|
import { usageBlock } from "../shared/usage-block.mjs";
|
|
56
|
-
import { demoStartArgs } from "../shared/demo-start-args.mjs";
|
|
56
|
+
import { demoStartArgs, withWarningOff } from "../shared/demo-start-args.mjs";
|
|
57
57
|
import { bundleVerdict } from "../shared/bundle-freshness.mjs";
|
|
58
58
|
const argv = process.argv.slice(2);
|
|
59
59
|
const flag = (n, d = null) => { const i = argv.indexOf(n); return i >= 0 ? argv[i + 1] : d; };
|
|
@@ -258,6 +258,14 @@ if (existsSync(poolRoot) && !statSync(poolRoot).isDirectory()) die(`demo: ${pool
|
|
|
258
258
|
|
|
259
259
|
// ── 3. replay ────────────────────────────────────────────────────────────────────────────────────────
|
|
260
260
|
console.log(`\n ${BRAND.name} ${BRAND.product.toLowerCase()} — demo\n`);
|
|
261
|
+
// THE LABEL, FIRST. The reader is about to look at a document that reads like advice about a real mark. It
|
|
262
|
+
// is not, and the demo says so before anything else rather than in a footnote nobody reaches. It printed
|
|
263
|
+
// after the replay, below the engine's own diagnostics and a Node warning, so the first thing a newcomer
|
|
264
|
+
// read was internal tallies (measured on the published beta, 2026-09-19).
|
|
265
|
+
console.log(" Real engine output for the fictional mark VENQORI, captured against Clarivate Compumark.");
|
|
266
|
+
console.log(" Replaying it needs no account, no key and no network.");
|
|
267
|
+
console.log(" Every number, band and citation below was produced by that real run and is being");
|
|
268
|
+
console.log(" re-rendered from its artifacts. It is an example, not advice.\n");
|
|
261
269
|
console.log(samples.length === 1
|
|
262
270
|
? ` sample: ${samples[0].dir}`
|
|
263
271
|
: ` samples: ${samples.length}${failures.length ? ` of ${shipped}` : ""} — ${samples.map((x) => x.name).join(", ")}`);
|
|
@@ -276,14 +284,33 @@ const { republishRun } = await import(pathToFileURL(join(REPO, "driver", "publis
|
|
|
276
284
|
// The failures are collected and reported together at the end, and the process exits non-zero, because a
|
|
277
285
|
// demo that came up missing a quarter of itself is not a success however good the three look.
|
|
278
286
|
const results = [];
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
287
|
+
// THE PUBLISHER'S OWN DIAGNOSTICS GO TO A FILE. It prints its tallies as it works (`[record-links] …`,
|
|
288
|
+
// `knockout receipts: …`), which is right for an operator watching a delivery and wrong as the first
|
|
289
|
+
// screen a newcomer reads. The replay's output is written to `replay.log` in the folder this demo made,
|
|
290
|
+
// and restored when the replay ends, so everything below prints as before. A failure is not lost: it is
|
|
291
|
+
// collected here and reported after the list.
|
|
292
|
+
const replayLog = join(flag("--pool") ? poolRoot : demoBase, "replay.log");
|
|
293
|
+
const toLog = (chunk, encoding, cb) => {
|
|
294
|
+
try { appendFileSync(replayLog, chunk, typeof encoding === "string" ? encoding : undefined); } catch { /* a log that cannot be written must not stop the replay */ }
|
|
295
|
+
if (typeof encoding === "function") encoding(); else if (typeof cb === "function") cb();
|
|
296
|
+
return true;
|
|
297
|
+
};
|
|
298
|
+
const screen = { out: process.stdout.write, err: process.stderr.write };
|
|
299
|
+
process.stdout.write = toLog;
|
|
300
|
+
process.stderr.write = toLog;
|
|
301
|
+
try {
|
|
302
|
+
for (const s0 of samples) {
|
|
303
|
+
try {
|
|
304
|
+
// poolUrl "" on purpose: the report's own link block is for a deployment that serves the pool at a
|
|
305
|
+
// public URL. This one is served from this process, at a port picked below.
|
|
306
|
+
results.push({ ...s0, published: await republishRun({ runId: s0.meta.runId, meta: s0.meta, pool: poolRoot, poolUrl: "", runDir: join(s0.publishFrom, "run") }) });
|
|
307
|
+
} catch (e) {
|
|
308
|
+
failures.push({ name: s0.name, why: String(e?.message ?? e) });
|
|
309
|
+
}
|
|
286
310
|
}
|
|
311
|
+
} finally {
|
|
312
|
+
process.stdout.write = screen.out;
|
|
313
|
+
process.stderr.write = screen.err;
|
|
287
314
|
}
|
|
288
315
|
// PUBLISHED, SO THE COPIES THEY WERE PUBLISHED FROM GO NOW, whatever comes next.
|
|
289
316
|
releaseDemoCopies();
|
|
@@ -293,12 +320,6 @@ if (!results.length) {
|
|
|
293
320
|
}
|
|
294
321
|
const published = results[0].published;
|
|
295
322
|
|
|
296
|
-
// THE LABEL. The reader is about to look at a document that reads like advice about a real mark. It is
|
|
297
|
-
// not, and the demo says so before the browser opens rather than in a footnote nobody reaches.
|
|
298
|
-
console.log(" Real engine output for the fictional mark VENQORI, captured against Clarivate Compumark.");
|
|
299
|
-
console.log(" Replaying it needs no account, no key and no network.");
|
|
300
|
-
console.log(" Every number, band and citation below was produced by that real run and is being");
|
|
301
|
-
console.log(" re-rendered from its artifacts. It is an example, not advice.\n");
|
|
302
323
|
// NAMES THE POPULATION. This printed "13 finding(s)" beside a report showing
|
|
303
324
|
// twelve, in the first sentence a reader meets. The number is not wrong — it comes from the audit
|
|
304
325
|
// spine (`driver/publish/audit-from-spine.mjs`, `counts.findings`), which counts every finding the run
|
|
@@ -307,10 +328,8 @@ console.log(" re-rendered from its artifacts. It is an example, not advice.\n")
|
|
|
307
328
|
// EACH LANE'S PUBLISHER REPORTS ITS OWN NUMBER, AND THEY ARE NOT THE SAME NUMBER.
|
|
308
329
|
//
|
|
309
330
|
// The clearance branch returns `counts.findings` — every finding in the run's audit spine. The knockout
|
|
310
|
-
// branch returns no `counts` at all; it returns
|
|
311
|
-
//
|
|
312
|
-
// is printed in its own words rather than mapped onto the clearance sentence. They happen to agree on
|
|
313
|
-
// the demo in this tree, which is one member of a class and proves nothing about the metric.
|
|
331
|
+
// branch returns no `counts` at all; it returns the names it screened and the rating each was given, so
|
|
332
|
+
// its line states those rather than a count mapped onto the clearance sentence.
|
|
314
333
|
//
|
|
315
334
|
// The third branch is the point: a lane whose publisher reports no count says so. This line printed a
|
|
316
335
|
// bare "?" to the knockout — a could-not-look wearing the costume of a number.
|
|
@@ -322,9 +341,13 @@ const spineOf = (pub) =>
|
|
|
322
341
|
Number.isFinite(pub.counts?.findings)
|
|
323
342
|
? `${pub.counts.findings} finding(s) recorded in the run's audit trail; the report shows
|
|
324
343
|
the ones it retains`
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
344
|
+
// A KNOCKOUT STATES WHAT IT SCREENED AND HOW EACH NAME WAS RATED, in the words its own report opens
|
|
345
|
+
// with ("One name screened: VENQORI, rated Low."). It used to count citations traced to held evidence,
|
|
346
|
+
// which a knockout's register-only findings never carry, so the line read "0 finding(s)" and a stranger
|
|
347
|
+
// took it as "the demo found nothing". Chosen 2026-09-19.
|
|
348
|
+
: Array.isArray(pub.reports) && pub.reports.length
|
|
349
|
+
? `${pub.reports.length} name${pub.reports.length === 1 ? "" : "s"} screened: ${pub.reports
|
|
350
|
+
.map((r) => (r.band ? `${r.mark}, rated ${r.band}` : r.mark)).join("; ")}; the report shows why`
|
|
328
351
|
: `this lane's publisher reported no finding count — the report itself is the record`;
|
|
329
352
|
for (const r of results) console.log(` published: ${r.published.runId}\n ${r.name} — ${spineOf(r.published)}`);
|
|
330
353
|
// "ONE PER PRODUCT" ONLY WHEN IT IS TRUE: with a sample missing, the count below says how many of how many.
|
|
@@ -418,6 +441,10 @@ console.log("");
|
|
|
418
441
|
// made, it starts from here as before.
|
|
419
442
|
const programRoot = ensureDemoProgram({ base: demoBase, say: (line) => console.log(line) });
|
|
420
443
|
const startFrom = programRoot ?? REPO;
|
|
444
|
+
// The services inherit the demo's Node flag through NODE_OPTIONS, so none of them prints the SQLite warning
|
|
445
|
+
// either. Set on this process's own environment, which both branches below hand on; the reader's own
|
|
446
|
+
// NODE_OPTIONS is kept.
|
|
447
|
+
process.env.NODE_OPTIONS = withWarningOff(process.env.NODE_OPTIONS);
|
|
421
448
|
const child = spawn(process.execPath, [join(startFrom, "bin", "start.mjs"), ...startArgs], {
|
|
422
449
|
cwd: startFrom, stdio: ["ignore", "inherit", "inherit"],
|
|
423
450
|
env: programRoot ? demoProgramEnv(process.env) : process.env,
|
package/build-info.json
CHANGED
package/driver/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# clearotron-driver
|
|
2
2
|
|
|
3
|
+
## 0.3.2-beta.15
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Fixed: A run's summary and its delivery email now give the rating and the report's own sentence. The internal delivery-check word no longer appears.
|
|
8
|
+
- Fixed: A report's answers to your instructions now show one line each, with the rest of each answer behind a fold.
|
|
9
|
+
- New: a register search can now be narrowed to the goods and services a filing actually covers, not only the numbered grouping it was filed under.
|
|
10
|
+
|
|
11
|
+
A common word in a busy grouping returns more filings than any search can read through. Narrowing by what each filing covers reaches the ones that matter.
|
|
12
|
+
|
|
13
|
+
New: searches no longer spend queries on owner portfolios guessed before any filing had been read. Owners now come from the filings a search returns.
|
|
14
|
+
- Fixed: A delivered report's conditions now carry only the conditions written for the client. The reviewer's own notes stay in the run record.
|
|
15
|
+
- Fixed: the portal now says it is loading while your clearances arrive, instead of showing an empty page that reads as nothing found.
|
|
16
|
+
|
|
17
|
+
## 0.3.2-beta.14
|
|
18
|
+
|
|
19
|
+
### Patch Changes
|
|
20
|
+
|
|
21
|
+
- Fixed: The lines for connecting an AI assistant now work when a folder name contains spaces, and the Codex settings block now works on Windows.
|
|
22
|
+
- Fixed: The demo now opens by saying what it shows, without internal diagnostics or a Node warning, and its knockout sample states the rating it reached.
|
|
23
|
+
- Fixed: A knockout report ordered for named countries now lists those countries under Registers counted, instead of a count of the provider's registers.
|
|
24
|
+
|
|
3
25
|
## 0.3.2-beta.13
|
|
4
26
|
|
|
5
27
|
### Patch Changes
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
// D4 verify.mjs parseCoverageLedgerJson, same shape
|
|
22
22
|
// D5 verify.mjs:1504 fail(`${unaccounted[0].token}:…`) — token minted in a DATA ROW
|
|
23
23
|
// D6 verify.mjs:1567 fail(`${violations[0].token}…`) — validatePlanFeasibility in register-plan.mjs
|
|
24
|
-
// D7 verify.mjs:1558 fail(`${v2[0].token}${detail}…`) — register-plan.mjs:
|
|
24
|
+
// D7 verify.mjs:1558 fail(`${v2[0].token}${detail}…`) — register-plan.mjs:2424 disclosureTextByAxis
|
|
25
25
|
// D8 verify.mjs:2470 caseLawLedgerFail fail(caseLawLedgerFail(…)) — token built in case-law-ledger.mjs:195 caseLawLedgerFail
|
|
26
26
|
//
|
|
27
27
|
// A partition built on the 60 tokens a regex CAN see would run green while blind to the rest, which is
|
|
@@ -139,10 +139,10 @@ export const VOCABULARY = [
|
|
|
139
139
|
{ token: "coverage_form_missing", stages: ["register-digest"], site: "driver/verify.mjs" },
|
|
140
140
|
{ token: "coverage_form_empty", stages: ["register-digest"], site: "driver/verify.mjs" },
|
|
141
141
|
{ token: "coverage_status_offenum", stages: ["register-digest"], site: "driver/verify.mjs:2050" },
|
|
142
|
-
{ token: "coverage_deferred_unaccounted", stages: ["register-digest"], site: "driver/verify.mjs coverageFormFail", family: "driver/register-plan.mjs:
|
|
143
|
-
{ token: "coverage_clean_unexecuted", stages: ["register-digest"], site: "driver/verify.mjs, the matterContext validator", family: "driver/register-plan.mjs:
|
|
144
|
-
{ token: "coverage_clean_skipped", stages: ["register-digest"], site: "driver/verify.mjs, the matterContext validator", family: "driver/register-plan.mjs:
|
|
145
|
-
{ token: "coverage_clean_unverified_incomplete", stages: ["register-digest"], site: "driver/verify.mjs, the matterContext validator", family: "driver/register-plan.mjs:
|
|
142
|
+
{ token: "coverage_deferred_unaccounted", stages: ["register-digest"], site: "driver/verify.mjs coverageFormFail", family: "driver/register-plan.mjs:2101 PROVIDER_HARD_ERROR_PREFIX — token on a data row", dynamic: "D5" },
|
|
143
|
+
{ token: "coverage_clean_unexecuted", stages: ["register-digest"], site: "driver/verify.mjs, the matterContext validator", family: "driver/register-plan.mjs:1835 validatePlanFeasibility", dynamic: "D6" },
|
|
144
|
+
{ token: "coverage_clean_skipped", stages: ["register-digest"], site: "driver/verify.mjs, the matterContext validator", family: "driver/register-plan.mjs:2196 searchedJurisdictionsFromPlan", dynamic: "D6" },
|
|
145
|
+
{ token: "coverage_clean_unverified_incomplete", stages: ["register-digest"], site: "driver/verify.mjs, the matterContext validator", family: "driver/register-plan.mjs:2424 disclosureTextByAxis", dynamic: "D7" },
|
|
146
146
|
{ token: "coverage_clean_tainted", stages: ["register-digest"], site: "driver/verify.mjs" },
|
|
147
147
|
{ token: "coverage_ledger_", stages: ["register-digest"], site: "driver/verify.mjs", family: "driver/coverage-ledger.mjs (parseCoverageLedgerJson token-first throws)", dynamic: "D4" },
|
|
148
148
|
{ token: "coverage_key_unknown", stages: ["register-digest"], site: "driver/verify.mjs", family: "driver/coverage-ledger.mjs", dynamic: "D4" },
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// exact server/tool wiring. Two inputs, both produced today by the gateway gather block:
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { dirname, join } from "node:path";
|
|
8
|
+
import { tomlString } from "../../../shared/toml-string.mjs";
|
|
8
9
|
|
|
9
10
|
// • mcpConfig — the claude-shaped JSON string `{mcpServers:{name:{command,args,env,connectionTimeoutMs?}}}`
|
|
10
11
|
// (buildGatherMcpConfig → JSON.stringify)
|
|
@@ -36,17 +37,8 @@ export const CRED_ENV_FORWARD = [
|
|
|
36
37
|
];
|
|
37
38
|
|
|
38
39
|
// ── TOML value escaping (basic strings) ──────────────────────────────────────────────────────────────
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
// TOML basic-string escapes: backslash, double-quote, and the C0 controls TOML names (\b \t \n \f \r).
|
|
42
|
-
const esc = str
|
|
43
|
-
.replace(/\\/g, "\\\\").replace(/"/g, '\\"')
|
|
44
|
-
.replace(/\x08/g, "\\b").replace(/\t/g, "\\t").replace(/\n/g, "\\n")
|
|
45
|
-
.replace(/\f/g, "\\f").replace(/\r/g, "\\r")
|
|
46
|
-
// any remaining control char → \uXXXX (TOML-legal)
|
|
47
|
-
.replace(/[\x00-\x1f]/g, (c) => "\\u" + c.charCodeAt(0).toString(16).padStart(4, "0"));
|
|
48
|
-
return `"${esc}"`;
|
|
49
|
-
}
|
|
40
|
+
// One encoder for every TOML block the product writes; re-exported so this module's callers keep their import.
|
|
41
|
+
export { tomlString };
|
|
50
42
|
function tomlStringArray(arr) {
|
|
51
43
|
return "[" + (arr || []).map(tomlString).join(", ") + "]";
|
|
52
44
|
}
|
package/driver/named-band.mjs
CHANGED
|
@@ -137,7 +137,7 @@ export function parseNamedBand(raw) {
|
|
|
137
137
|
// byte-identical to a slice the plan deliberately counted without fetching. Measured on a real
|
|
138
138
|
// run: four capability-gap blocks carried `error:true, deferred:true` into this function and
|
|
139
139
|
// reached record-carry.json with both fields gone and a sentence claiming the run "has a hit
|
|
140
|
-
// COUNT for this slice". register-plan.mjs:
|
|
140
|
+
// COUNT for this slice". register-plan.mjs:1835 validatePlanFeasibility already enforces the same rule one layer up
|
|
141
141
|
// ("a transient must not ship indistinguishable from a sanctioned descriptor") — it reads the
|
|
142
142
|
// RAW blocks, which is why it could. Every consumer that reads THIS projection could not.
|
|
143
143
|
// Conditional like the four keys above, so old bands carry neither key and nothing shifts.
|
package/driver/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "clearotron-driver",
|
|
3
3
|
"private": true,
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "0.3.2-beta.
|
|
5
|
+
"version": "0.3.2-beta.15",
|
|
6
6
|
"license": "AGPL-3.0-only",
|
|
7
7
|
"description": "Deterministic driver for the trademark clearance workflow: orchestration in code (fan-out, fan-in barrier, gating, retries); the model does judgment leaves only, through a reasoning CLI spawned per stage.",
|
|
8
8
|
"engines": {
|
|
@@ -1090,7 +1090,9 @@ export async function knockoutInner(ctx, job, opts = {}) {
|
|
|
1090
1090
|
writeFileSync(join(config.outboxDir, `${packet.runId}.pending`), `${agent}\n`);
|
|
1091
1091
|
} catch (e) { note(`delivery: outbox marker write skipped (${String(e.message).slice(0, 100)})`); }
|
|
1092
1092
|
const deliveredAt = new Date().toISOString();
|
|
1093
|
-
|
|
1093
|
+
// `tier` beside `verdict` — the same band word under the name the clearance lane records it by, so a
|
|
1094
|
+
// reader of either lane's status finds the rating in one place. `verdict` stays as it was.
|
|
1095
|
+
writeRunStatus(ctx, { state: "delivered", verdict: overall, tier: overall, statement: published.statement, url: published.url, reports: published.reports.map((r) => ({ mark: r.mark, url: r.url })), deliveredAt, sendPending: true, stepIndex: STEPS.length - 1, stepLabel: STEPS[STEPS.length - 1], stepN: STEPS.length, stepTotal: STEPS.length });
|
|
1094
1096
|
// — the knockout lane's pool copy learns its terminal state the same way,
|
|
1095
1097
|
// for the same reason: publish returns the pool dir, and `state: "delivered"` is decided after it
|
|
1096
1098
|
// returns. Same seam, same best-effort contract, no lane-specific exception to write down.
|
package/driver/pipeline.mjs
CHANGED
|
@@ -13379,6 +13379,17 @@ async function pipelineInner(job, opts = {}) {
|
|
|
13379
13379
|
// rationale lives on the function so the arms can DRIVE it rather than pin this call site's text.
|
|
13380
13380
|
const { clauses: orderedClauses, reasons: orderedReasons } = orderClausesForLede(clampClauses, clampReasons, guardClauses);
|
|
13381
13381
|
const reasonsOut = blockingGrounds.length ? [...orderedReasons, ...blockingGrounds] : orderedReasons;
|
|
13382
|
+
// — AND EACH APPENDED GROUND CARRIES AN EXPLICIT NULL CLAUSE, which is what puts it in the
|
|
13383
|
+
// run record ALONE. The block above says "no client surface moves", and that was true of the
|
|
13384
|
+
// statement and the bound recommendation, both fixed strings on BLOCKING. It was never true of the
|
|
13385
|
+
// CONDITIONS LIST: `clientConditions` walks the reasons and prints any reason that has no clause
|
|
13386
|
+
// and no engine token, so on a delivered BLOCKING run the reviewer's own cited correction lines
|
|
13387
|
+
// printed to the client as their conditions — 8 of the 13 conditions on the run this was measured
|
|
13388
|
+
// on (2026-09-20). A stored null is the shipped way to say "recorded, not rendered", and the
|
|
13389
|
+
// arrays stay index-aligned because `orderClausesForLede` returns them the same length.
|
|
13390
|
+
const clausesOut = blockingGrounds.length
|
|
13391
|
+
? [...orderedClauses, ...blockingGrounds.map(() => null)]
|
|
13392
|
+
: orderedClauses;
|
|
13382
13393
|
const kindsOut = verdict === "BLOCKING" ? { ...clampKinds, reviewerCited } : clampKinds;
|
|
13383
13394
|
// The invariant itself, stated over the value that is WRITTEN rather than over the branch that fills
|
|
13384
13395
|
// it — a guard that only holds while the code above is remembered is not a guard. Unreachable today
|
|
@@ -13396,13 +13407,19 @@ async function pipelineInner(job, opts = {}) {
|
|
|
13396
13407
|
// CONDITIONAL statement reads "<Tier> — conditional on: <factual open-state> (and N more)." —
|
|
13397
13408
|
// clampClauses carries the fact-voice clauses; the structured `stance` field is what every
|
|
13398
13409
|
// consumer keys on (never the statement's wording — the magic-string coupling is retired).
|
|
13399
|
-
const statement = riskStatement({ tier: derived.tier, verdict, reasons: reasonsOut, clauses:
|
|
13410
|
+
const statement = riskStatement({ tier: derived.tier, verdict, reasons: reasonsOut, clauses: clausesOut,
|
|
13400
13411
|
basis: isRegisterOnly(ctx.searchPolicy) ? "register-only" : null });
|
|
13401
13412
|
const tmp = driverDir(run.runDir, "verdict.json.tmp");
|
|
13402
|
-
writeFileSync(tmp, JSON.stringify({ ts: new Date().toISOString(), verdict, reasons: reasonsOut, clauses:
|
|
13413
|
+
writeFileSync(tmp, JSON.stringify({ ts: new Date().toISOString(), verdict, reasons: reasonsOut, clauses: clausesOut, kinds: kindsOut,
|
|
13403
13414
|
tier: derived.tier, badge: derived.badge, gaugeIndex: derived.gaugeIndex, maxComposite: derived.maxComposite,
|
|
13404
13415
|
band: derived.band ?? null, statement, stance: verdictStance(verdict) }, null, 2));
|
|
13405
13416
|
renameSync(tmp, driverDir(run.runDir, "verdict.json"));
|
|
13417
|
+
// — THE STATUS RECORD CARRIES THE BAND AND THE SENTENCE, so a reader of the run does not
|
|
13418
|
+
// have to reach for `verdict` to say something about the outcome. `verdict` is the gate's decision
|
|
13419
|
+
// (CLEAR / CONDITIONAL / BLOCKING) and is engine vocabulary; the band and the composed statement are
|
|
13420
|
+
// what every client surface already speaks. Measured 2026-09-20: an assistant summarising a run read
|
|
13421
|
+
// `verdict` and told the client "BLOCKING" beside a Medium rating, because status.json held no band.
|
|
13422
|
+
writeRunStatus(ctx, { tier: derived.tier ?? null, statement: statement ?? null });
|
|
13406
13423
|
return derived;
|
|
13407
13424
|
};
|
|
13408
13425
|
try { writeVerdictSidecar(); }
|
|
@@ -15902,11 +15919,17 @@ export function reconstructCtx(job, opts) { // @internal
|
|
|
15902
15919
|
// axes drive which register-unit files placement/digest/skeptic/synthesis reference. Mirror the cold path
|
|
15903
15920
|
// (pipeline) exactly: decideAxes("") defaults ALL axes ON, so a missing manifest never silently UNDER-spawns.
|
|
15904
15921
|
ctx.axes = decideAxes(existsSync(P.variantManifest) ? readFileSync(P.variantManifest, "utf8") : "");
|
|
15905
|
-
// …including the cold path's axis-from-plan UNION ( B3)
|
|
15906
|
-
//
|
|
15907
|
-
//
|
|
15908
|
-
//
|
|
15909
|
-
//
|
|
15922
|
+
// …including the cold path's axis-from-plan UNION ( B3), and the reason is now the plainer one:
|
|
15923
|
+
// THE FROZEN PLAN IS THE SEARCH AUTHORITY, and what it carries need not match what prose activation
|
|
15924
|
+
// would have chosen. `decideAxes` reads the manifest; the plan is what actually ran.
|
|
15925
|
+
//
|
|
15926
|
+
// The two diverge in both directions and each has a live case. A plan frozen before the guessed
|
|
15927
|
+
// owner lane was removed still carries its owner entries on incumbent-class, and a resumed run must
|
|
15928
|
+
// execute the plan it froze rather than the axes today's manifest would activate. Going the other
|
|
15929
|
+
// way, any future lane that mints entries on an axis the manifest does not announce lands here too.
|
|
15930
|
+
//
|
|
15931
|
+
// Without the union a stale-repair/--experiment of register-digest/placement would exclude the
|
|
15932
|
+
// executed lane from the per-axis unit list and the declared inputs/freshness stamps.
|
|
15910
15933
|
// Best-effort read — a legacy run without a frozen plan reconstructs exactly as before.
|
|
15911
15934
|
try {
|
|
15912
15935
|
const plan = JSON.parse(readFileSync(P.registerPlan, "utf8"));
|
package/driver/publish/index.mjs
CHANGED
|
@@ -1739,8 +1739,17 @@ export function composeEmailHtml(reportMdPath, url, auditFile, names = [], deliv
|
|
|
1739
1739
|
// the engine clamp reasons (opts.conditions = verdict.json reasons). Machinery-only clamps (no client
|
|
1740
1740
|
// item authored) degrade to one generic plain line — never raw engine jargon, never truncated mid-sentence.
|
|
1741
1741
|
const emailConditions = opts?.verdict === 'CONDITIONAL' ? actYouConditions(parseActionBuckets(secs['Actions']).you) : [];
|
|
1742
|
+
// THE BANNER SPEAKS THE RUN'S OWN SENTENCE. It used to print the delivery gate's word — "Delivered as
|
|
1743
|
+
// BLOCKING" — which is engine vocabulary and, beside a Medium rating on the report, reads as a
|
|
1744
|
+
// contradiction the reader cannot resolve (measured 2026-09-20). The statement is composed once by the
|
|
1745
|
+
// sidecar writer and is what the report and the index already show; a run recorded before statements
|
|
1746
|
+
// were persisted keeps the old line, which is all such a run has.
|
|
1747
|
+
const bound = String(opts?.statement ?? '').trim();
|
|
1748
|
+
const boundHead = bound
|
|
1749
|
+
? esc(bound)
|
|
1750
|
+
: `Delivered as ${esc(opts?.verdict ?? '')}${opts?.verdict === 'CONDITIONAL' ? ' — subject to:' : '.'}`;
|
|
1742
1751
|
const verdictBound = (opts?.verdict && opts.verdict !== 'CLEAR')
|
|
1743
|
-
? `<p style="margin:0 0 8px;padding:8px 10px;background:#fdeeee;border:1px solid #c98a86;color:#6e1512"><b
|
|
1752
|
+
? `<p style="margin:0 0 8px;padding:8px 10px;background:#fdeeee;border:1px solid #c98a86;color:#6e1512"><b>${boundHead}</b>${opts.verdict === 'CONDITIONAL' ? `<br>${(emailConditions.length ? emailConditions : ['the open items set out in the report, before relying on a clean result']).map((r) => `• ${cell(String(r))}`).join('<br>')}` : ''}</p>`
|
|
1744
1753
|
: '';
|
|
1745
1754
|
|
|
1746
1755
|
const reviewHeadline = `<div style="${FONT};font-size:11pt;color:#1a1a2e;margin:0 0 14px">`
|
|
@@ -63,6 +63,7 @@ import { EXPORT_TOGGLE, exportPopover, EXPORT_MENU_JS } from './report-topbar.mj
|
|
|
63
63
|
import { makeClassifyStatus, isAllClass } from '../../providers/_shared/screen.mjs';
|
|
64
64
|
import { saysSomethingNew } from '../../shared/says-something-new.mjs';
|
|
65
65
|
import { isNextStepHeading } from '../knockout-next-step.mjs';
|
|
66
|
+
import OFFERED from '../../shared/offered-territories.json' with { type: 'json' }; // the order form's own territory names
|
|
66
67
|
|
|
67
68
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
68
69
|
|
|
@@ -481,6 +482,14 @@ function territoryName(code, { bare = false } = {}) {
|
|
|
481
482
|
} catch { return ''; }
|
|
482
483
|
}
|
|
483
484
|
|
|
485
|
+
/** A territory the order names, in the order form's own words, or '' when that list has no such code. */
|
|
486
|
+
const ORDER_FORM_NAMES = new Map([...OFFERED.regions, ...OFFERED.countries].map((t) => [t.code, t.name]));
|
|
487
|
+
const orderedTerritoryName = (code) => {
|
|
488
|
+
const c = String(code ?? '').trim().toUpperCase();
|
|
489
|
+
// EM is the European Union register's own code, and some records carry it for an EU order.
|
|
490
|
+
return ORDER_FORM_NAMES.get(c === 'EM' ? 'EU' : c) ?? '';
|
|
491
|
+
};
|
|
492
|
+
|
|
484
493
|
/** "a, b and c" — the reader's list, not a join on commas. */
|
|
485
494
|
function listWords(items) {
|
|
486
495
|
const xs = (items ?? []).filter(Boolean);
|
|
@@ -1244,12 +1253,28 @@ function aboutRequestBlock(scope, requestNotes, depthNote = '', productContext =
|
|
|
1244
1253
|
// the middle of the panel is the thing a reader skips; the count and its source are what they read.
|
|
1245
1254
|
const regions = (registerCounts?.scope?.regions ?? []).filter(Boolean);
|
|
1246
1255
|
const provider = registerCounts?.providerLabel ?? registerCounts?.provider ?? '';
|
|
1247
|
-
// THE
|
|
1248
|
-
//
|
|
1249
|
-
//
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1256
|
+
// THE ORDER'S TERRITORIES, NOT THE PROVIDER'S REGISTERS (owner, 2026-09-19). The row counted the
|
|
1257
|
+
// registers the provider searched, and for a one-country order that is not one: a United States order is
|
|
1258
|
+
// counted over the United States and the international register, because an international filing can
|
|
1259
|
+
// designate the United States. A client who ordered one country read "2 registers" as a mistake. So an
|
|
1260
|
+
// order that names its territories is stated as it named them, in the order form's own names; the
|
|
1261
|
+
// registers actually counted stay in the line under the counts table. A worldwide order keeps the board's
|
|
1262
|
+
// row, "186 registers, on Clarivate Compumark.", because there the count is the scope (the record says
|
|
1263
|
+
// worldwide by carrying no jurisdictions). A territory the provider does not cover was not counted, so it
|
|
1264
|
+
// is not listed here; it is disclosed where deferrals are. A territory the form's list cannot name, or an
|
|
1265
|
+
// office this install could not reach, falls back to the count rather than printing a code or claiming
|
|
1266
|
+
// a territory nothing was counted in.
|
|
1267
|
+
const on = provider ? `, on ${provider}` : '';
|
|
1268
|
+
const sc = registerCounts?.scope ?? {};
|
|
1269
|
+
const deferred = new Set((sc.deferredJurisdictions ?? []).map((c) => String(c ?? '').trim().toUpperCase()));
|
|
1270
|
+
const ordered = [...new Set((sc.jurisdictions ?? [])
|
|
1271
|
+
.filter((c) => !deferred.has(String(c ?? '').trim().toUpperCase())).map(orderedTerritoryName))];
|
|
1272
|
+
const namesTheOrder = sc.worldwide !== true && !(sc.unreachableOffices ?? []).length
|
|
1273
|
+
&& ordered.length > 0 && ordered.every(Boolean);
|
|
1274
|
+
const counted = namesTheOrder ? `${listWords(ordered)}${on}.`
|
|
1275
|
+
: regions.length
|
|
1276
|
+
? `${regions.length} ${regions.length === 1 ? 'register' : 'registers'}${on}.`
|
|
1277
|
+
: (registerCounts ? `Counted worldwide${on}` : '');
|
|
1253
1278
|
if (!asked && !where && !classLine && !stage && !context && !counted) return '';
|
|
1254
1279
|
// ONE PANEL, THE SAME ON BOTH REPORTS (the 2026-09-16 report redesign). The rows carry what was asked for; the
|
|
1255
1280
|
// counts row says what was counted and hides the territory list behind a fold rather than running a
|
|
@@ -874,7 +874,7 @@ function actionsPanel(actionsText) {
|
|
|
874
874
|
if (!body) continue;
|
|
875
875
|
const kind = /answers to your instructions/i.test(label) ? 'ans' : /checks we ran|what we found/i.test(label) ? 'ran' : 'you';
|
|
876
876
|
const ic = kind === 'ans' ? '✎' : kind === 'ran' ? '✓' : '→';
|
|
877
|
-
parts.push(`<div class="actgrp act-${kind}"><h3><span class="actic">${ic}</span> ${inline(label)}</h3>${renderProse(body)}</div>`);
|
|
877
|
+
parts.push(`<div class="actgrp act-${kind}"><h3><span class="actic">${ic}</span> ${inline(label)}</h3>${kind === 'ans' ? answersHtml(body) : renderProse(body)}</div>`);
|
|
878
878
|
}
|
|
879
879
|
return parts.length ? `<div class="panel actions" id="actions">${parts.join('')}</div>` : '';
|
|
880
880
|
}
|
|
@@ -900,10 +900,62 @@ export function parseActionBuckets(actionsText) {
|
|
|
900
900
|
}
|
|
901
901
|
return out;
|
|
902
902
|
}
|
|
903
|
+
// ── ONE LINE PER ANSWER, THE REST FOLDED (owner, on the delivered report, 2026-09-20) ────────────────
|
|
904
|
+
//
|
|
905
|
+
// Every answer to a client's instruction printed at full length, one after another, and the section read
|
|
906
|
+
// as a wall of text. The acceptance brief had already ruled the shape — the question and the answer's
|
|
907
|
+
// opening visible, the explanation behind a fold — and it was never built, because no demo run carries
|
|
908
|
+
// intake asks and so no demo and no mock ever rendered this section.
|
|
909
|
+
//
|
|
910
|
+
// NOTHING IS CUT AND NO WORDS ARE WRITTEN. The line is the answer's FIRST SENTENCE, by the same
|
|
911
|
+
// splitFirstSentence the hero caption folds on (one rule, one set of abbreviations), and the remainder is
|
|
912
|
+
// the rest of the same string. `report.md`, `report-data.json` and the workbook are untouched.
|
|
913
|
+
//
|
|
914
|
+
// WHERE THE FOLD MAY NOT GO, and this is the whole care in the function. An internal note runs from its
|
|
915
|
+
// label to the END OF ITS LINE, and a client surface removes it by cutting to the close of the element
|
|
916
|
+
// that contains it (portal-report.mjs). So:
|
|
917
|
+
// · a wholly internal answer is dropped, exactly as renderProse drops one;
|
|
918
|
+
// · an answer whose remainder is ONLY an internal tail folds inside a wrapper the client strip takes
|
|
919
|
+
// whole, so that reader gets the line and no empty disclosure;
|
|
920
|
+
// · an answer that is internal from its first word takes the whole row with it: the strip would
|
|
921
|
+
// otherwise leave the question standing over a dangling arrow, which portal-report.mjs names as the
|
|
922
|
+
// empty-labelled-row defect;
|
|
923
|
+
// · otherwise the remainder folds, tail included, and the client cut trims it inside the fold's own
|
|
924
|
+
// paragraph while the public remainder stays.
|
|
925
|
+
// Splitting on the public head alone is what makes that true: the visible line can never carry a label.
|
|
926
|
+
function answersHtml(body) {
|
|
927
|
+
const lines = stripInternal(String(body ?? ''), { client: false }).split('\n').map((l) => l.trim());
|
|
928
|
+
const items = [];
|
|
929
|
+
for (const line of lines) {
|
|
930
|
+
if (!line) continue;
|
|
931
|
+
if (/^[-*]\s/.test(line)) items.push(line.replace(/^[-*]\s+/, ''));
|
|
932
|
+
else if (items.length) items[items.length - 1] += ` ${line}`;
|
|
933
|
+
else return renderProse(body); // not the bulleted register — render it as it always was
|
|
934
|
+
}
|
|
935
|
+
if (!items.length) return renderProse(body);
|
|
936
|
+
const rows = [];
|
|
937
|
+
for (const item of items) {
|
|
938
|
+
if (isInternalLabelled(item)) continue;
|
|
939
|
+
const at = item.indexOf('→');
|
|
940
|
+
const label = at < 0 ? '' : item.slice(0, at + 1).trim();
|
|
941
|
+
const answer = at < 0 ? item : item.slice(at + 1).trim();
|
|
942
|
+
const mark = answer.search(/\[internal\]/i);
|
|
943
|
+
const { first, rest } = splitFirstSentence(mark < 0 ? answer : answer.slice(0, mark));
|
|
944
|
+
const tail = mark < 0 ? '' : answer.slice(mark).trim();
|
|
945
|
+
const head = `${label ? `${inline(label)} ` : ''}${inline(first)}`;
|
|
946
|
+
if (!rest && !tail) { rows.push(`<li>${head}</li>`); continue; }
|
|
947
|
+
if (!first && !rest) { rows.push(`<li class="int-note">${head}${inline(tail)}</li>`); continue; }
|
|
948
|
+
const fold = `<details class="ans-more"><summary>More</summary><p>${inline(rest && tail ? `${rest} ${tail}` : rest || tail)}</p></details>`;
|
|
949
|
+
rows.push(`<li>${head}${rest ? fold : `<div class="int-note">${fold}</div>`}</li>`);
|
|
950
|
+
}
|
|
951
|
+
return rows.length ? `<ul class="answers">${rows.join('')}</ul>` : '';
|
|
952
|
+
}
|
|
953
|
+
|
|
903
954
|
function actionGroup(bucket, kind) {
|
|
904
955
|
if (!bucket || !bucket.body) return '';
|
|
905
956
|
const ic = kind === 'ans' ? '✎' : kind === 'ran' ? '✓' : '→';
|
|
906
|
-
|
|
957
|
+
const body = kind === 'ans' ? answersHtml(bucket.body) : renderProse(bucket.body);
|
|
958
|
+
return `<div class="actgrp act-${kind}"><h3><span class="actic">${ic}</span> ${inline(bucket.label)}</h3>${body}</div>`;
|
|
907
959
|
}
|
|
908
960
|
|
|
909
961
|
// doc-52 — the "Conditional — subject to:" banner speaks the SAME plain wording as "Only you can close
|
|
@@ -112,6 +112,18 @@
|
|
|
112
112
|
.panel.actions .act-you .actic{background:var(--med-soft);color:var(--med-tx)}
|
|
113
113
|
.panel.actions p,.panel.actions li{font-size:13.5px;color:var(--slate);line-height:1.55}
|
|
114
114
|
.panel.actions ul{margin:0;padding-left:20px} .panel.actions li{margin:3px 0}
|
|
115
|
+
/* Each answer shows one line; the rest folds. Styled here and not in the frozen renderer, and the
|
|
116
|
+
@media print rule that forces every <details> open already covers it — an exported PDF carries the
|
|
117
|
+
whole answer. The disclosure takes the caption fold's look, which is the only other fold of a
|
|
118
|
+
sentence into its remainder on this page. */
|
|
119
|
+
.panel.actions ul.answers li{margin:7px 0}
|
|
120
|
+
.panel.actions details.ans-more{margin:3px 0 0}
|
|
121
|
+
.panel.actions details.ans-more>summary{cursor:pointer;list-style:none;font-size:11px;font-weight:700;
|
|
122
|
+
letter-spacing:.06em;text-transform:uppercase;color:var(--faint)}
|
|
123
|
+
.panel.actions details.ans-more>summary::-webkit-details-marker{display:none}
|
|
124
|
+
.panel.actions details.ans-more>summary::before{content:"\25b8";margin-right:6px;display:inline-block;transition:.15s;color:var(--rose)}
|
|
125
|
+
.panel.actions details.ans-more[open]>summary::before{transform:rotate(90deg)}
|
|
126
|
+
.panel.actions details.ans-more p{margin:5px 0 0}
|
|
115
127
|
|
|
116
128
|
/* ── THE BOARD'S OWN SECTION STRIP, lifted from the approved mock rather than written here ────────
|
|
117
129
|
Five entries and the board's own labels, marked not to print because the board marks it so: a strip
|
|
@@ -388,7 +400,10 @@
|
|
|
388
400
|
in the hero, directly above text that was already fully expanded. Measured in Chrome: the caption's
|
|
389
401
|
own content was carried correctly and only the control leaked, which is why the commit that added
|
|
390
402
|
the fold could check the half it thought about and ship the half it did not. */
|
|
391
|
-
details.drill>summary,details.method>summary,details.sub-more>summary
|
|
403
|
+
details.drill>summary,details.method>summary,details.sub-more>summary,
|
|
404
|
+
/* An answer's "More" is a pure toggle: it carries no content of its own, so on paper it would be an
|
|
405
|
+
inert label over text the rule below has already expanded — the defect #470 shipped. */
|
|
406
|
+
details.ans-more>summary{display:none!important}
|
|
392
407
|
/* The caption fold made this line load-bearing for the document's CONTENT, not just its completeness. The
|
|
393
408
|
legal/practical reads now live inside .drillbody, so without this rule the exported PDF would
|
|
394
409
|
carry the one-line verdicts and none of the reasoning behind them. The export prints everything a
|
|
@@ -41,9 +41,9 @@
|
|
|
41
41
|
// CORRECTED 2026-08-11 — THIS PARAGRAPH USED TO SAY "one qid per office … which is the shape
|
|
42
42
|
// joinPlanToBands already has", AND THAT WAS FALSE ABOUT THE COMPILER IT DOCUMENTS. There is no
|
|
43
43
|
// per-office qid. compileRegisterPlan narrows ONE shared `regions` array and hands it to every entry
|
|
44
|
-
// (register-plan.mjs:
|
|
44
|
+
// (register-plan.mjs:846 compileRegisterPlan); the unreachable office produces no entry, so no qid, so no band block, so
|
|
45
45
|
// nothing ever reaches joinPlanToBands' deferred bucket — whose only source is a block stamped
|
|
46
|
-
// `error:true && deferred:true` (register-plan.mjs:
|
|
46
|
+
// `error:true && deferred:true` (register-plan.mjs:1489 extendRegisterPlan).
|
|
47
47
|
//
|
|
48
48
|
// The consequence was not academic. `deferred_coverage` rode the plan and nothing that a reader sees
|
|
49
49
|
// read it: coverage-form.mjs seeded its deferred rows from skeleton qids alone, so an EU+US matter on a
|