clearotron 0.3.2 → 0.3.3-beta.0
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/CONTRIBUTING.md +12 -0
- package/INSTALL.md +8 -0
- package/bin/onboard.mjs +29 -7
- package/bin/start.mjs +1 -1
- package/build-info.json +2 -2
- package/demo/MANIFEST.json +27 -0
- package/docs/INTAKE.md +8 -0
- package/docs/architecture/04-configuration-reference.md +1 -1
- package/driver/CHANGELOG.md +37 -0
- package/driver/clearance-variants-record.mjs +12 -1
- package/driver/common-law-coverage-status.mjs +113 -0
- package/driver/contract-audit.mjs +1 -1
- package/driver/contract-e3-backlog.mjs +37 -37
- package/driver/contract-vocabulary.mjs +8 -8
- package/driver/coverage-form-io.mjs +3 -1
- package/driver/coverage-form.mjs +38 -11
- package/driver/coverage-ledger.mjs +37 -7
- package/driver/coverage-union.mjs +2 -2
- package/driver/crowd-context.mjs +19 -6
- package/driver/drainer-identity.mjs +1 -1
- package/driver/engine/mcp/clarivate-server.mjs +4 -2
- package/driver/engine/mcp/corsearch-server.mjs +3 -1
- package/driver/engine/mcp/coverage-server.mjs +1 -1
- package/driver/engine/mcp/dispositions-server.mjs +47 -5
- package/driver/engine/mcp/euipo-server.mjs +2 -0
- package/driver/engine/mcp/free-tier-server.mjs +2 -0
- package/driver/engine/mcp/gather-config.mjs +8 -2
- package/driver/engine/mcp/probe-server.mjs +28 -0
- package/driver/engine/mcp/proposal-fields.mjs +45 -0
- package/driver/engine/mcp/recording-server.mjs +30 -0
- package/driver/engine/mcp/signa-server.mjs +2 -0
- package/driver/engine/mcp/supplemental.mjs +89 -12
- package/driver/engine/mcp/unit-note-server.mjs +50 -0
- package/driver/engine/mcp/uspto-local-server.mjs +2 -0
- package/driver/engine/openai-agent.mjs +7 -0
- package/driver/engine/probe.mjs +67 -14
- package/driver/engine/tool-refusal.mjs +16 -0
- package/driver/enqueue-schema.mjs +2 -2
- package/driver/envelope-settle.mjs +82 -13
- package/driver/findings-model.mjs +4 -4
- package/driver/gateway.mjs +18 -2
- package/driver/manager-groups-verdict.mjs +1 -1
- package/driver/matter-frame-record.mjs +24 -7
- package/driver/named-band.mjs +1 -1
- package/driver/package.json +1 -1
- package/driver/partial-payload-baseline.json +12 -3
- package/driver/pipeline.mjs +141 -37
- package/driver/publish/index.mjs +40 -25
- package/driver/publish/xlsx.mjs +26 -4
- package/driver/queue-markers.mjs +44 -0
- package/driver/queue-watch-verdict.mjs +2 -2
- package/driver/register-availability.mjs +2 -2
- package/driver/register-plan.mjs +313 -21
- package/driver/roster-verdict.mjs +1 -1
- package/driver/runner.mjs +26 -2
- package/driver/skills/clearance-common-law/SKILL.md +2 -0
- package/driver/skills/clearance-register/SKILL.md +44 -3
- package/driver/skills/clearance-register/digest.md +5 -5
- package/driver/skills/clearance-register/providers/clarivate.md +1 -1
- package/driver/skills/clearance-register/unit.md +39 -0
- package/driver/skills/clearance-variants/SKILL.md +1 -1
- package/driver/skills/matter-frame/SKILL.md +4 -2
- package/driver/stages.mjs +12 -5
- package/driver/status-snapshot.mjs +1 -1
- package/driver/suite-census.json +236 -14
- package/driver/synthesis-record.mjs +80 -2
- package/driver/unit-file-drift.mjs +3 -3
- package/driver/unit-inventory.mjs +2 -2
- package/driver/unit-state-verdict.mjs +1 -1
- package/driver/updater-identity.mjs +2 -3
- package/driver/variant-manifest-model.mjs +11 -1
- package/driver/verify.mjs +5 -5
- package/driver/withheld-families.mjs +104 -0
- package/mcp-server/CHANGELOG.md +4 -0
- package/mcp-server/lib/brief.mjs +5 -7
- package/mcp-server/lib/runs.mjs +1 -1
- package/mcp-server/package.json +1 -1
- package/mcp-server/server.mjs +3 -2
- package/package.json +1 -1
- package/portal-ui/dist/assets/{index-DMthc7PQ.js → index-GBbbyQxc.js} +22 -4
- package/portal-ui/dist/index.html +1 -1
- package/portal-ui/package.json +1 -1
- package/providers/_shared/count.mjs +2 -2
- package/providers/_shared/enumerate.mjs +15 -2
- package/providers/_shared/execute-plan.mjs +19 -1
- package/providers/_shared/plan-guards.mjs +40 -0
- package/providers/clarivate/src/capabilities.js +15 -5
- package/providers/clarivate/src/core.js +41 -5
- package/providers/corsearch/src/capabilities.js +4 -0
- package/providers/oauth-mcp-bridge/CHANGELOG.md +4 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/providers/signa/src/capabilities.js +22 -8
- package/providers/signa/src/core.js +12 -1
- package/scripts/demo-evidence.mjs +114 -0
- package/scripts/engine-probe.mjs +6 -5
- package/scripts/env-audit.mjs +1 -1
- package/scripts/freeze-example-run.mjs +3 -3
- package/scripts/live-surface-check.mjs +26 -19
- package/scripts/mint-suite-census.mjs +66 -0
- package/scripts/package-size-budget.mjs +117 -0
- package/scripts/register-plan-shape.mjs +259 -0
- package/scripts/release-note-required.mjs +38 -1
- package/scripts/settings-render-check.mjs +36 -0
- package/scripts/travelling-predicates.mjs +1 -1
- package/shared/identifier-scan.mjs +22 -5
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
3
|
+
// Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
|
|
4
|
+
// package-size-budget.mjs — THE TARBALL CANNOT GROW WITHOUT SOMEBODY DECIDING THAT IT SHOULD.
|
|
5
|
+
//
|
|
6
|
+
// The package is large by design: most of it is bundled demo evidence, and that was ruled to be right.
|
|
7
|
+
// What was missing is not a smaller package, it is a MEASUREMENT — nothing recorded the size, so it
|
|
8
|
+
// could double across a few betas and no one would find out from the repository. A number nobody
|
|
9
|
+
// records is a number nobody can notice changing.
|
|
10
|
+
//
|
|
11
|
+
// WHAT THIS IS NOT. It is not a size limit chosen by taste, and it does not argue about what ships.
|
|
12
|
+
// It compares against a RECORDED baseline and allows a margin; anything inside the margin passes
|
|
13
|
+
// silently, anything past it fails and prints the ten largest paths so the growth has a name before
|
|
14
|
+
// anyone debates it.
|
|
15
|
+
//
|
|
16
|
+
// WHY A COMMITTED BASELINE RATHER THAN A LIVE LOOKUP of the last published version. A gate must give
|
|
17
|
+
// the same answer on every machine, including one with no network, and it must not start failing
|
|
18
|
+
// because a registry is slow or a version was unpublished. The baseline is minted into this repository
|
|
19
|
+
// the same way the suite census is, so the comparison is deterministic, reviewable in the diff that
|
|
20
|
+
// changes it, and honest about being a decision somebody made rather than a fact fetched at runtime.
|
|
21
|
+
//
|
|
22
|
+
// Usage:
|
|
23
|
+
// node scripts/package-size-budget.mjs measure and report against the baseline
|
|
24
|
+
// node scripts/package-size-budget.mjs --check …and exit non-zero when it is past the margin
|
|
25
|
+
// node scripts/package-size-budget.mjs --apply re-record the baseline as it is now
|
|
26
|
+
|
|
27
|
+
import { execFileSync } from "node:child_process";
|
|
28
|
+
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
29
|
+
import { dirname, join } from "node:path";
|
|
30
|
+
import { fileURLToPath } from "node:url";
|
|
31
|
+
|
|
32
|
+
const ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
33
|
+
const BASELINE = join(ROOT, "package-size-baseline.json");
|
|
34
|
+
|
|
35
|
+
// THE MARGIN, and why it is one number rather than per-field taste. A change that adds a demo run
|
|
36
|
+
// moves every field at once, so a single proportion catches the shape of growth that matters and
|
|
37
|
+
// leaves ordinary churn alone. 10% is roughly one more bundled run on today's package — big enough
|
|
38
|
+
// that nobody trips it by editing source, small enough that a doubling cannot arrive unannounced.
|
|
39
|
+
export const MARGIN = 0.10;
|
|
40
|
+
|
|
41
|
+
/** What `npm pack` says this tree would publish. */
|
|
42
|
+
export function measure({ root = ROOT } = {}) {
|
|
43
|
+
const out = execFileSync("npm", ["pack", "--dry-run", "--json"], {
|
|
44
|
+
cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], maxBuffer: 64 * 1024 * 1024,
|
|
45
|
+
});
|
|
46
|
+
const p = JSON.parse(out)[0];
|
|
47
|
+
return {
|
|
48
|
+
size: p.size,
|
|
49
|
+
unpackedSize: p.unpackedSize,
|
|
50
|
+
entryCount: p.entryCount,
|
|
51
|
+
files: (p.files ?? []).map((f) => ({ path: f.path, size: f.size })),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const mb = (n) => `${(n / 1_000_000).toFixed(1)} MB`;
|
|
56
|
+
const pct = (now, was) => `${now >= was ? "+" : ""}${(((now - was) / was) * 100).toFixed(1)}%`;
|
|
57
|
+
|
|
58
|
+
/** The ten largest paths, so growth has a name rather than a number. */
|
|
59
|
+
export function largest(files, n = 10) {
|
|
60
|
+
return [...files].sort((a, b) => b.size - a.size).slice(0, n);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Compare a measurement against a baseline. Returns `{ over: [...], rows: [...] }` — `over` names the
|
|
65
|
+
* fields past the margin and is empty when nothing is.
|
|
66
|
+
*/
|
|
67
|
+
export function compare(now, was, { margin = MARGIN } = {}) {
|
|
68
|
+
const rows = [];
|
|
69
|
+
const over = [];
|
|
70
|
+
for (const [field, label] of [["size", "packed"], ["unpackedSize", "unpacked"], ["entryCount", "entries"]]) {
|
|
71
|
+
const limit = Math.floor(was[field] * (1 + margin));
|
|
72
|
+
const breached = now[field] > limit;
|
|
73
|
+
if (breached) over.push(field);
|
|
74
|
+
rows.push({ field, label, now: now[field], was: was[field], limit, breached });
|
|
75
|
+
}
|
|
76
|
+
return { over, rows };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
80
|
+
const apply = process.argv.includes("--apply");
|
|
81
|
+
const check = process.argv.includes("--check");
|
|
82
|
+
const now = measure();
|
|
83
|
+
|
|
84
|
+
if (apply) {
|
|
85
|
+
writeFileSync(BASELINE, `${JSON.stringify({
|
|
86
|
+
note: "What `npm pack` produced when this was recorded. The budget allows a margin over these; see scripts/package-size-budget.mjs.",
|
|
87
|
+
margin: MARGIN,
|
|
88
|
+
size: now.size, unpackedSize: now.unpackedSize, entryCount: now.entryCount,
|
|
89
|
+
}, null, 2)}\n`);
|
|
90
|
+
console.log(`recorded: ${mb(now.size)} packed, ${mb(now.unpackedSize)} unpacked, ${now.entryCount} entries`);
|
|
91
|
+
process.exit(0);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!existsSync(BASELINE)) {
|
|
95
|
+
console.error("package-size-budget: no baseline recorded. `node scripts/package-size-budget.mjs --apply` writes one.");
|
|
96
|
+
process.exit(check ? 1 : 0);
|
|
97
|
+
}
|
|
98
|
+
const was = JSON.parse(readFileSync(BASELINE, "utf8"));
|
|
99
|
+
const { over, rows } = compare(now, was, { margin: was.margin ?? MARGIN });
|
|
100
|
+
|
|
101
|
+
for (const r of rows) {
|
|
102
|
+
const shown = r.field === "entryCount" ? `${r.now} against ${r.was}` : `${mb(r.now)} against ${mb(r.was)}`;
|
|
103
|
+
console.log(` ${r.label.padEnd(9)} ${shown} (${pct(r.now, r.was)}${r.breached ? " — PAST THE MARGIN" : ""})`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!over.length) {
|
|
107
|
+
console.log(`package-size-budget: inside the ${Math.round((was.margin ?? MARGIN) * 100)}% margin.`);
|
|
108
|
+
process.exit(0);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
console.error(`\npackage-size-budget: ${over.join(" and ")} past the ${Math.round((was.margin ?? MARGIN) * 100)}% margin.`);
|
|
112
|
+
console.error("The ten largest paths in what would be published:\n");
|
|
113
|
+
for (const f of largest(now.files)) console.error(` ${mb(f.size).padStart(8)} ${f.path}`);
|
|
114
|
+
console.error("\nIf the growth is intended, re-record the baseline in the same commit that causes it:");
|
|
115
|
+
console.error(" node scripts/package-size-budget.mjs --apply");
|
|
116
|
+
process.exit(check ? 1 : 0);
|
|
117
|
+
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
3
|
+
// Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
|
|
4
|
+
//
|
|
5
|
+
// register-plan-shape.mjs — recompile a recorded run's register plan from its own inputs and print
|
|
6
|
+
// the SHAPE of the result: how many questions run without being asked for, how many wait, and whether
|
|
7
|
+
// the waiting ones are waiting for the reading turn.
|
|
8
|
+
//
|
|
9
|
+
// node scripts/register-plan-shape.mjs <run-directory>
|
|
10
|
+
//
|
|
11
|
+
// WHY THIS PRINTS COUNTS AND NOTHING ELSE. It exists to be run against recorded client runs, where the
|
|
12
|
+
// mark, the goods words, the owners and the records are not ours to quote anywhere — and a plan entry's
|
|
13
|
+
// own qid contains the mark, so even an identifier list would be a disclosure. What comes out is
|
|
14
|
+
// arithmetic: numbers, axis names and predicate names, all of which are vocabulary this repository
|
|
15
|
+
// already publishes. Nothing here reads a record, and nothing here can print one.
|
|
16
|
+
//
|
|
17
|
+
// IT WRITES NOTHING, which is the other half of being safe to point at a real run. The ordinary compile
|
|
18
|
+
// path freezes the plan it built and writes run-journal rows; this reads the same inputs and compiles
|
|
19
|
+
// in memory. A recorded run is evidence, and a tool that measures it must leave it byte-identical.
|
|
20
|
+
//
|
|
21
|
+
// WHAT IT IS FOR. The plan a recorded run froze was compiled by the compiler of its day. Reading that
|
|
22
|
+
// artifact says what the engine DID ask; it cannot say what today's engine WOULD ask. Recompiling from
|
|
23
|
+
// the run's own inputs answers the second question, which is the one a rule change has to be judged on.
|
|
24
|
+
//
|
|
25
|
+
// AN ABSENT INPUT IS A FINDING, NOT A DEFAULT. Every input is named and checked, and a missing one
|
|
26
|
+
// stops the run with the path that was looked for. A compile over a half-read run directory would
|
|
27
|
+
// produce a number, and a number produced that way is worse than no number.
|
|
28
|
+
|
|
29
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
30
|
+
import { resolve, join } from "node:path";
|
|
31
|
+
import { paths } from "../driver/stages.mjs";
|
|
32
|
+
import { driverDir } from "../shared/driver-dir.mjs"; // — the run-directory layout has one namer; a literal here is how two spellings drift
|
|
33
|
+
import { compileRegisterPlan, awaitsReadingTurn, planMaxOrWidth } from "../driver/register-plan.mjs";
|
|
34
|
+
import { parseVariantManifestModel } from "../driver/variant-manifest-model.mjs";
|
|
35
|
+
import { registerCapabilities, registerUnavailableOffices } from "../driver/register-unreachable.mjs";
|
|
36
|
+
import { frameIdentifiedClassRows } from "../driver/matter-frame-record.mjs";
|
|
37
|
+
import { excludeHouseElement, mintSupplementalQid, HOUSE_ELEMENT_RECEIPT } from "../driver/register-plan.mjs";
|
|
38
|
+
import { inScopeClassList, registerJurisdictions } from "../driver/pipeline.mjs";
|
|
39
|
+
import { isEntrypoint } from "../shared/is-entrypoint.mjs";
|
|
40
|
+
|
|
41
|
+
const readJson = (p) => JSON.parse(readFileSync(p, "utf8"));
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The three kinds that run without being asked for (decision 10, as amended 2026-09-21).
|
|
45
|
+
*
|
|
46
|
+
* READ OFF THE ENTRY, deliberately, rather than re-deriving the compiler's own test: this is a
|
|
47
|
+
* measuring instrument, and an instrument that shares the mechanism it measures cannot disagree with
|
|
48
|
+
* it. The subtlety worth stating is the third line — the BARE contains entry on the mark waits, and
|
|
49
|
+
* only the goods-narrowed one is always on (ruled 2026-09-20), so `provenance === "mark"` alone is not the
|
|
50
|
+
* test and a reader who assumed it were would count a waiting entry as open.
|
|
51
|
+
*/
|
|
52
|
+
function openKind(e) {
|
|
53
|
+
if (e.axis === "saturation-probe") return "saturation probe";
|
|
54
|
+
if (Array.isArray(e.goods_text) && e.goods_text.length) return "goods-narrowed";
|
|
55
|
+
if (e.provenance === "mark" && String(e.predicate) !== "default") return "identical mark";
|
|
56
|
+
if (e.unsupported === true) return "unsupported (a disclosure, not a search)";
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function planShape(plan) {
|
|
61
|
+
const open = [], waiting = [], other = [], unexpected = [];
|
|
62
|
+
for (const e of plan.entries ?? []) {
|
|
63
|
+
if (awaitsReadingTurn(e.when)) { waiting.push(e); continue; }
|
|
64
|
+
if (e.when) { other.push(e); continue; }
|
|
65
|
+
const kind = openKind(e);
|
|
66
|
+
if (kind) open.push({ e, kind }); else unexpected.push(e);
|
|
67
|
+
}
|
|
68
|
+
const byKind = {};
|
|
69
|
+
for (const { kind } of open) byKind[kind] = (byKind[kind] ?? 0) + 1;
|
|
70
|
+
const byAxis = {};
|
|
71
|
+
for (const e of waiting) byAxis[e.axis] = (byAxis[e.axis] ?? 0) + 1;
|
|
72
|
+
// EVERY entry by axis, waiting or not — the comparison against a frozen plan needs the whole
|
|
73
|
+
// population per lane, not only the part that waits.
|
|
74
|
+
const byAxisAll = {};
|
|
75
|
+
for (const e of plan.entries ?? []) byAxisAll[e.axis] = (byAxisAll[e.axis] ?? 0) + 1;
|
|
76
|
+
return { total: (plan.entries ?? []).length, open: open.length, byKind,
|
|
77
|
+
waiting: waiting.length, byAxis, byAxisAll, parentGated: other.length,
|
|
78
|
+
unexpected: unexpected.map((e) => `${e.axis}/${e.predicate}`) };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function inputsFor(runDir) {
|
|
82
|
+
const P = paths(runDir);
|
|
83
|
+
const need = (label, p) => {
|
|
84
|
+
if (!existsSync(p)) {
|
|
85
|
+
console.error(`register-plan-shape: ${label} is not in this run directory — looked for ${p}.`);
|
|
86
|
+
console.error(" Without it the compile would be over a population this run never had. Refusing.");
|
|
87
|
+
process.exit(2);
|
|
88
|
+
}
|
|
89
|
+
return p;
|
|
90
|
+
};
|
|
91
|
+
let manifest = parseVariantManifestModel(readFileSync(need("the variant manifest", P.variantManifestModel), "utf8"));
|
|
92
|
+
// ── THE CLIENT'S OWN ELEMENT, IF THIS RUN VERIFIED THAT IT OWNS IT ──────────────────────────────
|
|
93
|
+
//
|
|
94
|
+
// MISSING FROM THE FIRST CUT, and it mattered on the first real run this tool was pointed at. Where
|
|
95
|
+
// a run verified that the client owns an element of its own mark, that element leaves the conflict
|
|
96
|
+
// analysis: the compile is handed the element so its form band becomes unreachable rather than
|
|
97
|
+
// merely unasked-for, and a single confirmation question is appended in its place. On a matter that
|
|
98
|
+
// is a house mark plus a tagline, that element can account for more than half the questions — so a
|
|
99
|
+
// recompile that skipped it reported a population the run could never have had, in the direction of
|
|
100
|
+
// too many. The condition is the receipt's own, exactly as the run applies it: an absent,
|
|
101
|
+
// unreadable or unverified receipt excludes nothing, which is the safe direction and the one a
|
|
102
|
+
// client is never harmed by.
|
|
103
|
+
let houseElement = null, houseConfirmation = null;
|
|
104
|
+
try {
|
|
105
|
+
const receipt = readJson(driverDir(runDir, HOUSE_ELEMENT_RECEIPT));
|
|
106
|
+
if (receipt?.verified === true) {
|
|
107
|
+
const r = excludeHouseElement(manifest, { element: receipt.element, remainder: receipt.remainder });
|
|
108
|
+
// REFUSED IS NOT APPLIED. The receipt answers who owns the element; the transform answers whether
|
|
109
|
+
// this manifest's mark survives the cut. The run requires both, so this does too.
|
|
110
|
+
if (!r.refused) { manifest = r.manifest; houseConfirmation = r.confirmation; houseElement = receipt.element; }
|
|
111
|
+
}
|
|
112
|
+
} catch { /* no receipt — nothing was verified, so nothing is excluded */ }
|
|
113
|
+
const scope = readJson(need("the instructed scope", driverDir(runDir, "instructed-scope.json")));
|
|
114
|
+
const profile = readJson(need("the customer profile", driverDir(runDir, "profile.json")));
|
|
115
|
+
// OPTIONAL INPUTS ARE REPORTED, NOT DEFAULTED IN SILENCE. Each one that is absent removes questions
|
|
116
|
+
// from the recompile, and an absence nobody is told about is indistinguishable from a compiler that
|
|
117
|
+
// stopped asking them. A preserved run directory that lost an input recompiles exactly like a
|
|
118
|
+
// compiler that retired a lane, and those are different facts.
|
|
119
|
+
//
|
|
120
|
+
// EACH LINE SAYS HOW MUCH, not that something is missing. The first cut said the form band "compiles
|
|
121
|
+
// to nothing" — true, and read by a careful reader as a large number on every register. It is a large
|
|
122
|
+
// number only where the register has no OR surface. That wording was acted on before it was measured.
|
|
123
|
+
const absent = [];
|
|
124
|
+
let form = null;
|
|
125
|
+
if (existsSync(P.formNeighbourhood)) form = readJson(P.formNeighbourhood);
|
|
126
|
+
else {
|
|
127
|
+
// QUANTIFIED, AND BY THIS REGISTER, because the qualitative version of this line was misleading and
|
|
128
|
+
// was acted on. The form band is OR-BATCHED at the provider's declared width, so on a register with
|
|
129
|
+
// a wide OR surface the WHOLE band — eight terms or forty — compiles to one question plus one
|
|
130
|
+
// wildcard fringe. Measured: 40 terms cost 2 questions on a 500-wide register and 41 on one with no
|
|
131
|
+
// OR surface. "A whole lane compiles to nothing" reads as a large number on every register and is
|
|
132
|
+
// one only on the narrow ones.
|
|
133
|
+
const w = planMaxOrWidth(registerCapabilities());
|
|
134
|
+
absent.push(`form-neighbourhood.json — the form band does not compile. On this register `
|
|
135
|
+
+ `(OR width ${w}) that is about ${w > 1 ? "2 questions however many terms the band held" : "one question per term in the band"}.`);
|
|
136
|
+
}
|
|
137
|
+
if (!existsSync(driverDir(runDir, HOUSE_ELEMENT_RECEIPT)))
|
|
138
|
+
absent.push(`${HOUSE_ELEMENT_RECEIPT} — no element was excluded, so the client's own element is searched as a conflict`);
|
|
139
|
+
if (!(manifest.goods_words ?? []).length)
|
|
140
|
+
absent.push("goods_words on the manifest — no goods-narrowed question can compile from it");
|
|
141
|
+
|
|
142
|
+
// THE RUN'S OWN RESOLVERS, NOT THE RAW FIELDS. An earlier cut read `scope.classes` and
|
|
143
|
+
// `scope.jurisdictions` straight off the file. Two of those reads are wrong:
|
|
144
|
+
//
|
|
145
|
+
// · A matter that instructed NO class falls back to the customer profile's defaults, so the raw
|
|
146
|
+
// read compiles an unscoped plan where the run compiled a scoped one — and a plan is always
|
|
147
|
+
// class-scoped, so the compiler would refuse outright rather than differ quietly.
|
|
148
|
+
// · Territories come off a shared ladder that reads the geography stamp, not off the field alone.
|
|
149
|
+
//
|
|
150
|
+
// MEASURED, because the obvious third claim is not true and was in this comment until it was
|
|
151
|
+
// checked: a worldwide matter records `jurisdictions: null` and the resolver answers `[]`, which is
|
|
152
|
+
// the SAME answer the raw read gives, and `[]` is correct — worldwide means no region clause at all.
|
|
153
|
+
// The resolvers earn their place on the two cases above, not on that one.
|
|
154
|
+
//
|
|
155
|
+
// The whole scope object is handed over rather than two fields, so the ladder sees the stamp it reads.
|
|
156
|
+
const job = { ...scope, jobKey: "shape-read" };
|
|
157
|
+
return {
|
|
158
|
+
manifest,
|
|
159
|
+
job: { ...job, classes: inScopeClassList(job, profile), jurisdictions: registerJurisdictions(job, profile) },
|
|
160
|
+
addedClasses: frameIdentifiedClassRows(runDir),
|
|
161
|
+
form,
|
|
162
|
+
houseElement,
|
|
163
|
+
__houseConfirmation: houseConfirmation,
|
|
164
|
+
__absent: absent,
|
|
165
|
+
capabilities: registerCapabilities(),
|
|
166
|
+
unavailableOffices: registerUnavailableOffices(),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function main() {
|
|
171
|
+
const runDir = process.argv[2];
|
|
172
|
+
if (!runDir) {
|
|
173
|
+
console.error("usage: node scripts/register-plan-shape.mjs <run-directory>");
|
|
174
|
+
console.error(" Prints how many register questions run unasked and how many wait. Counts only —");
|
|
175
|
+
console.error(" no marks, no terms, no identifiers. Writes nothing into the run directory.");
|
|
176
|
+
process.exit(2);
|
|
177
|
+
}
|
|
178
|
+
const dir = resolve(runDir);
|
|
179
|
+
if (!existsSync(dir)) { console.error(`register-plan-shape: no such run directory: ${dir}`); process.exit(2); }
|
|
180
|
+
|
|
181
|
+
const inputs = inputsFor(dir);
|
|
182
|
+
const plan = compileRegisterPlan(inputs);
|
|
183
|
+
// THE ONE QUESTION THE EXCLUDED ELEMENT STILL OWES, appended after the compile exactly as the run
|
|
184
|
+
// appends it: the element leaves the conflict analysis, and in its place the plan asks once whether
|
|
185
|
+
// the client's own registrations are there. It is not derived from the manifest, so it cannot come
|
|
186
|
+
// out of the compile — and a recompile that dropped it would under-count by one on every run that
|
|
187
|
+
// excluded an element.
|
|
188
|
+
if (inputs.__houseConfirmation && plan?.entries) {
|
|
189
|
+
const used = new Set(plan.entries.map((e) => e.qid));
|
|
190
|
+
plan.entries.push({ ...inputs.__houseConfirmation,
|
|
191
|
+
qid: mintSupplementalQid({ prefix: "house", term: inputs.__houseConfirmation.term, used }),
|
|
192
|
+
nice_classes: plan.nice_classes ?? [], regions: plan.regions ?? [] });
|
|
193
|
+
}
|
|
194
|
+
const s = planShape(plan);
|
|
195
|
+
|
|
196
|
+
// WHICH REGISTER THIS BOX IS CONFIGURED FOR, because the shape depends on it — a provider that cannot
|
|
197
|
+
// express a predicate stamps the entry unsupported, and unsupported entries are counted here. A
|
|
198
|
+
// number read against the wrong provider is not this run's shape. The vendor name is vocabulary this
|
|
199
|
+
// repository already publishes; nothing about the matter is.
|
|
200
|
+
// ── AND WHAT THIS RUN ACTUALLY FROZE, BESIDE IT ────────────────────────────────────────────────
|
|
201
|
+
//
|
|
202
|
+
// The recompile answers "what would today's engine ask". On its own that number is uninterpretable:
|
|
203
|
+
// the first real run this tool saw compiled 44 questions against 129 in the frozen plan, and nobody
|
|
204
|
+
// could say from one number whether that was a deliberate removal, a defect, or a difference in the
|
|
205
|
+
// inputs. Printed side by side and split by axis, the same two numbers say WHERE the population
|
|
206
|
+
// moved, which is the question a reader actually has. Still counts only, and the frozen plan is read,
|
|
207
|
+
// never rewritten.
|
|
208
|
+
// THE CATCH IS NARROWED TO THE FILE BEING ABSENT, deliberately. A blanket catch here swallowed a
|
|
209
|
+
// ReferenceError on the first cut — the path was read off a variable that is not in scope in this
|
|
210
|
+
// function — and the comparison silently never printed, reading exactly like a run with no frozen
|
|
211
|
+
// plan. An absent artifact is a fact about the run; anything else is a fault in this script and must
|
|
212
|
+
// say so rather than look like one.
|
|
213
|
+
let frozen = null;
|
|
214
|
+
const frozenPath = paths(dir).registerPlan;
|
|
215
|
+
if (existsSync(frozenPath)) frozen = planShape(JSON.parse(readFileSync(frozenPath, "utf8")));
|
|
216
|
+
|
|
217
|
+
if (inputs.__absent?.length) {
|
|
218
|
+
console.log(" INPUTS THIS RUN DIRECTORY DOES NOT CARRY — each one lowers the counts below:");
|
|
219
|
+
for (const a of inputs.__absent) console.log(` · ${a}`);
|
|
220
|
+
console.log("");
|
|
221
|
+
}
|
|
222
|
+
console.log(` register configured here ${inputs.capabilities?.id ?? "none — CLEAROTRON_DATABASE is unset"}`);
|
|
223
|
+
console.log(` questions compiled ${s.total}`);
|
|
224
|
+
console.log(` run without being asked for ${s.open}`);
|
|
225
|
+
for (const [kind, n] of Object.entries(s.byKind).sort()) console.log(` ${String(n).padStart(4)} ${kind}`);
|
|
226
|
+
console.log(` waiting for the reading turn ${s.waiting}`);
|
|
227
|
+
for (const [axis, n] of Object.entries(s.byAxis).sort()) console.log(` ${String(n).padStart(4)} ${axis}`);
|
|
228
|
+
if (s.parentGated) console.log(` waiting on a parent question ${s.parentGated} (the crowd-gated fringe)`);
|
|
229
|
+
if (plan.added_classes?.length) console.log(` classes the frame added ${plan.added_classes.length}`);
|
|
230
|
+
|
|
231
|
+
if (frozen) {
|
|
232
|
+
console.log(`\n THIS RUN'S OWN FROZEN PLAN, for comparison — what it asked, not what today would ask:`);
|
|
233
|
+
console.log(` questions frozen ${frozen.total} (recompiled now: ${s.total})`);
|
|
234
|
+
const axes = [...new Set([...Object.keys(frozen.byAxis), ...Object.keys(s.byAxis),
|
|
235
|
+
...(frozen.byAxisAll ? Object.keys(frozen.byAxisAll) : []), ...Object.keys(s.byAxisAll ?? {})])].sort();
|
|
236
|
+
for (const a of axes) {
|
|
237
|
+
const was = frozen.byAxisAll?.[a] ?? 0, now = s.byAxisAll?.[a] ?? 0;
|
|
238
|
+
if (was || now) console.log(` ${String(was).padStart(4)} -> ${String(now).padStart(4)} ${a}`);
|
|
239
|
+
}
|
|
240
|
+
if (frozen.total !== s.total) {
|
|
241
|
+
console.log(`\n The two populations differ by ${Math.abs(frozen.total - s.total)}. Gating marks an entry WAITING,`);
|
|
242
|
+
console.log(" it never removes one, so a difference here is the compiler's, not the gate's — the axis");
|
|
243
|
+
console.log(" rows above say which lane moved. Expected where a lane was deliberately retired.");
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// THE PROPERTY, STATED AS A VERDICT the reader does not have to derive from the numbers above.
|
|
248
|
+
// `unexpected` is the whole of it: an entry that neither waits nor is one of the three sanctioned
|
|
249
|
+
// kinds is a question this plan would ask in the same breath as the identical mark.
|
|
250
|
+
if (s.unexpected.length) {
|
|
251
|
+
console.log(`\n NOT HELD — ${s.unexpected.length} question(s) run unasked that are none of the three kinds:`);
|
|
252
|
+
for (const u of [...new Set(s.unexpected)].sort()) console.log(` ${u}`);
|
|
253
|
+
process.exitCode = 1;
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
console.log("\n HELD — every question outside the identical-mark, saturation and goods-narrowed set waits.");
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (isEntrypoint(import.meta.url)) main();
|
|
@@ -53,6 +53,23 @@
|
|
|
53
53
|
// reach a number, which is the failure this repository has already paid for once. The remaining half is
|
|
54
54
|
// a person reading the diff and asking what a lawyer would want told.
|
|
55
55
|
//
|
|
56
|
+
// ── ONLY A PULL REQUEST'S OWN COMMITS ARE READ, AND THAT IS THE ONLY READING THAT COUNTS ─────────────
|
|
57
|
+
//
|
|
58
|
+
// The per-commit question below does not survive a squash. A squash commit adds every note the branch
|
|
59
|
+
// carried, so it "carries its own note", and any prose release line it inherited from a folded commit is
|
|
60
|
+
// excused. Measured on a simulated squash of an integration branch whose one offending commit this guard
|
|
61
|
+
// refused commit by commit: over the squash it passed. So this guard is a gate only where it reads the
|
|
62
|
+
// commits a pull request brings, one at a time, before they are folded. That is how CI runs it, on the
|
|
63
|
+
// pull request's range; on a push to main the range is empty and it says so rather than passing quietly.
|
|
64
|
+
//
|
|
65
|
+
// AND POINTED AT MAIN'S OWN LINE IT SAYS SO, ON EVERY ANSWER. A range with a commit on `origin/main`'s
|
|
66
|
+
// first-parent history is reading the squashes themselves. That reading still catches a squash that
|
|
67
|
+
// shipped code with no note at all (an arm replays one), so it is not refused. It cannot catch a prose
|
|
68
|
+
// line a squash inherited, and its pass is therefore not the per-commit answer. So every answer over
|
|
69
|
+
// that line carries the sentence that says which of the two it can give. An audit of what shipped
|
|
70
|
+
// reads the pull requests that brought it, never the squashes they became. An arm pins
|
|
71
|
+
// both readings side by side, and that CI runs this on the pull request's range and nowhere else.
|
|
72
|
+
//
|
|
56
73
|
// ── A DECLINATION ANSWERS FOR ITS OWN COMMIT, NOT FOR THE RANGE ──────────────────────────────────────
|
|
57
74
|
//
|
|
58
75
|
// This read one `Release-note: none` anywhere in the range as the answer for all of it, so a pack of
|
|
@@ -270,7 +287,7 @@ function main() {
|
|
|
270
287
|
// have failed, and would have reported the acceptance met for the life of the branch.
|
|
271
288
|
const head = argAfter("--head") || "HEAD";
|
|
272
289
|
const git = (...a) => execFileSync("git", a, { encoding: "utf8", maxBuffer: 1 << 28 });
|
|
273
|
-
let changed, commits, atHead;
|
|
290
|
+
let changed, commits, atHead, onMainLine = [];
|
|
274
291
|
try {
|
|
275
292
|
changed = git("diff", "--name-only", `${base}...${head}`).split("\n").filter(Boolean);
|
|
276
293
|
// THE BRANCH'S OWN COMMITS. On a pull request the range can reach commits main already carries, from
|
|
@@ -282,6 +299,12 @@ function main() {
|
|
|
282
299
|
catch { /* no origin/main in this clone: the range as given */ }
|
|
283
300
|
}
|
|
284
301
|
const shas = git("rev-list", "--no-merges", "--reverse", `${base}..${head}`, ...exclude).split("\n").filter(Boolean);
|
|
302
|
+
// MAIN'S OWN LINE IS NAMED (see the header): a squash there excuses the prose it inherited. Asked
|
|
303
|
+
// only where main is known; a clone without it keeps the range as given.
|
|
304
|
+
let mainKnown = false;
|
|
305
|
+
try { git("rev-parse", "--verify", "-q", "origin/main^{commit}"); mainKnown = true; } catch { /* not known here */ }
|
|
306
|
+
const mainLine = mainKnown ? new Set(git("rev-list", "--first-parent", "origin/main").split("\n").filter(Boolean)) : new Set();
|
|
307
|
+
onMainLine = shas.filter((sha) => mainLine.has(sha));
|
|
285
308
|
commits = shas.map((sha) => ({
|
|
286
309
|
sha,
|
|
287
310
|
subject: git("log", "-1", "--format=%s", sha).trim(),
|
|
@@ -302,6 +325,14 @@ function main() {
|
|
|
302
325
|
catch (e) { console.error(`release-note-required: cannot read the shipped file list: ${e.message}`); process.exit(2); }
|
|
303
326
|
if (!files.length) { console.error("release-note-required: package.json names no shipped files, so this cannot look"); process.exit(2); }
|
|
304
327
|
|
|
328
|
+
if (!commits.length) {
|
|
329
|
+
// NOTHING TO READ IS SAID, NOT IMPLIED. On a push to main every commit is already on main, so the range
|
|
330
|
+
// is empty by construction: the pull request that brought them was read before they were squashed.
|
|
331
|
+
console.log(`release-note-required: no commit in ${base}..${head} that main does not already hold, so there `
|
|
332
|
+
+ "is nothing here to read. This guard answers for a pull request's own commits, one by one, and main's "
|
|
333
|
+
+ "history is never read.");
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
305
336
|
const { visible, notes, declined, withdrawals, owed } = commitVerdicts({ commits, files, atHead });
|
|
306
337
|
console.log(`release-note-required: ${changed.length} changed file(s) against ${base}`
|
|
307
338
|
+ `${head === "HEAD" ? "" : ` (head ${head})`}; `
|
|
@@ -309,6 +340,12 @@ function main() {
|
|
|
309
340
|
+ `${withdrawals.length ? `; ${withdrawals.length} answered by a note the range withdrew` : ""}`);
|
|
310
341
|
// PER COMMIT, SAID AS PER COMMIT. "no note, declared on purpose" read as a verdict on the range, and beside
|
|
311
342
|
// a range that carries a note it told a reader skimming the output that none went out. Found in review.
|
|
343
|
+
if (onMainLine.length) {
|
|
344
|
+
console.log(` NOT THE GATE'S READING: ${onMainLine.length} commit(s) read here are on main's own line (first `
|
|
345
|
+
+ `${onMainLine[0].slice(0, 7)}), so they are squashes. Over a squash this finds a change that shipped with no `
|
|
346
|
+
+ "note at all; it cannot find a prose release line the squash inherited, so a pass here is not the "
|
|
347
|
+
+ "per-commit answer. The gate is the pull request's own reading, before the squash.");
|
|
348
|
+
}
|
|
312
349
|
for (const d of declined) console.log(` ${d.sha.slice(0, 7)} declines a note of its own, on purpose: ${d.reason}`);
|
|
313
350
|
// AN EXCUSED COMMIT IS PRINTED, WITH THE SHA THAT EXCUSED IT. A pass this check reached by reading
|
|
314
351
|
// another commit's decision is one a person may want to disagree with, and they cannot if it is silent.
|
|
@@ -600,6 +600,27 @@ if (await open('/portal/people', "document.querySelector('table.data tbody tr')"
|
|
|
600
600
|
};
|
|
601
601
|
})()`
|
|
602
602
|
|
|
603
|
+
// The bar's reading, then the two ways a reader moves the table with it: the table moved by a finger
|
|
604
|
+
// (the thumb follows) and a press at the track's right end (the table follows). The table is put back
|
|
605
|
+
// at its start afterwards so the screenshots below show it as a reader first meets it.
|
|
606
|
+
const pinnedBarProbe = `(async () => {
|
|
607
|
+
window.scrollTo(0, 0); await new Promise((r) => setTimeout(r, 200));
|
|
608
|
+
const wrap = document.querySelector('.table-wrap'); const bar = document.querySelector('.pinned-bar');
|
|
609
|
+
const thumb = bar && bar.querySelector('.pinned-thumb'); const track = bar && bar.querySelector('.pinned-track');
|
|
610
|
+
if (!wrap || !bar || !thumb || !track) return { wrap: !!wrap, bar: !!bar, thumb: !!thumb, track: !!track };
|
|
611
|
+
const b = bar.getBoundingClientRect(), t0 = thumb.getBoundingClientRect();
|
|
612
|
+
const out = { bar: true, onScreen: b.top >= 0 && b.bottom <= innerHeight + 1, barH: Math.round(b.height),
|
|
613
|
+
thumbW: Math.round(t0.width), thumbH: Math.round(t0.height), nativeBarH: wrap.offsetHeight - wrap.clientHeight };
|
|
614
|
+
wrap.scrollLeft = 200; await new Promise((r) => setTimeout(r, 200));
|
|
615
|
+
out.thumbFollowed = Math.round(thumb.getBoundingClientRect().left - t0.left);
|
|
616
|
+
const tr = track.getBoundingClientRect();
|
|
617
|
+
track.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, clientX: tr.right - 2, clientY: tr.top + 4, pointerId: 1 }));
|
|
618
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
619
|
+
out.tableAtEnd = Math.round(wrap.scrollWidth - wrap.clientWidth - wrap.scrollLeft);
|
|
620
|
+
wrap.scrollLeft = 0; await new Promise((r) => setTimeout(r, 200));
|
|
621
|
+
return out;
|
|
622
|
+
})()`
|
|
623
|
+
|
|
603
624
|
for (const [label, width] of [['desktop', 1280], ['phone', 400]]) {
|
|
604
625
|
await cmd('Emulation.setDeviceMetricsOverride', { width, height: 900, deviceScaleFactor: 1, mobile: width < 700 })
|
|
605
626
|
await sleep(350)
|
|
@@ -615,6 +636,21 @@ if (await open('/portal/people', "document.querySelector('table.data tbody tr')"
|
|
|
615
636
|
ok(p.pageScrollsSideways === false,
|
|
616
637
|
`${label}: the page itself does not scroll sideways — the table's overflow stays in the table`)
|
|
617
638
|
console.log(` ${label}: wrapper ${p.wrapW}px, table ${p.tableW}px, wrapper scrollable: ${p.scrollable}`)
|
|
639
|
+
// — A TABLE THAT CONTINUES SHOWS ITS SCROLLBAR, on this screen as on the Clearances list. The same
|
|
640
|
+
// pinned bar is wired on both; the Clearances check asserts it and this one did not, so the bar here
|
|
641
|
+
// was drawn and unmeasured. Where the table fits there is no bar: a bar with nothing to scroll is a
|
|
642
|
+
// control that does nothing.
|
|
643
|
+
const pin = (await evalIn(pinnedBarProbe)) ?? {}
|
|
644
|
+
console.log(` ${label}: pinned bar ${JSON.stringify(pin)}`)
|
|
645
|
+
if (p.scrollable) {
|
|
646
|
+
ok(pin.bar && pin.onScreen, `${label}: the table continues past its edge, and its sideways bar is on screen`)
|
|
647
|
+
ok(pin.nativeBarH === 0, `${label}: only the pinned bar is drawn, not the table's own beside it`)
|
|
648
|
+
ok(pin.barH >= 9 && pin.thumbH >= 8 && pin.thumbW >= 24, `${label}: the pinned bar is drawn at a size a reader can see (${pin.barH}px, thumb ${pin.thumbW}×${pin.thumbH})`)
|
|
649
|
+
ok(pin.thumbFollowed > 0, `${label}: the thumb follows the table`)
|
|
650
|
+
ok(pin.tableAtEnd <= 1, `${label}: pressing the end of the track moves the table to its end`)
|
|
651
|
+
} else {
|
|
652
|
+
ok(pin.bar === false, `${label}: the table fits, and no sideways bar is drawn`)
|
|
653
|
+
}
|
|
618
654
|
await cmd('Emulation.clearDeviceMetricsOverride', {})
|
|
619
655
|
for (const theme of ['light', 'dark']) {
|
|
620
656
|
await setTheme(theme)
|
|
@@ -103,7 +103,7 @@ function isPlumbing(node, child) {
|
|
|
103
103
|
//
|
|
104
104
|
// const wrote = files.length ? files.some(…) : null; gateway.mjs (by name)
|
|
105
105
|
// const inScope = scope.size ? tokens.some(…) : (…); reasoning-tripwires.mjs:82
|
|
106
|
-
// const reached = b.layer === "national" ? (…) : regions.some(…); register-plan.mjs
|
|
106
|
+
// const reached = b.layer === "national" ? (…) : regions.some(…); resolveRegions() in register-plan.mjs
|
|
107
107
|
//
|
|
108
108
|
// The climb stopped at the ternary and reported "unresolved", which reads as a limit of the pattern
|
|
109
109
|
// and was a missing case. All three are `local` — the boolean is bound and read in the same function.
|
|
@@ -116,14 +116,31 @@ export const ALLOWED_CONTEXT =
|
|
|
116
116
|
* Escapes become spaces before matching, which fixes the class rather than that one line: `\n`, `\t`,
|
|
117
117
|
* `\r` and `\\` in any quoted source, HTML numeric and named entities, and `%20`-style percent escapes.
|
|
118
118
|
* A boundary in the SOURCE is a boundary in the TEXT, whatever the encoding.
|
|
119
|
+
*
|
|
120
|
+
* READ AS A LEXER READS THEM, and two misreadings of Windows paths to Node's executable are why
|
|
121
|
+
* (2026-09-22). Both reported a three-letter mark, spelled by the letters after the path's last `\n`,
|
|
122
|
+
* that was not there:
|
|
123
|
+
*
|
|
124
|
+
* - `\\` is consumed as a pair, in the same left-to-right pass as the others. Replaced in a separate
|
|
125
|
+
* pass after `\n`, the second backslash of a pair was read as the start of `\n`.
|
|
126
|
+
* - A Windows path written raw — drive letter, colon, ONE backslash, as in a comment, a doc or a TOML
|
|
127
|
+
* literal — holds no escapes, so its backslashes are left as separators. A separator is already a
|
|
128
|
+
* boundary, and reading `\n` there also broke a lowercase name that starts with n, r, t, f or v.
|
|
129
|
+
* The escaped form (`C:\\`) is not a raw path, so an escape beside a path in a string is still read.
|
|
119
130
|
*/
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
131
|
+
const BACKSLASH_ESCAPE = /\\(?:[\\nrtfv0]|u[0-9a-fA-F]{4}|x[0-9a-fA-F]{2})/g;
|
|
132
|
+
const RAW_WINDOWS_PATH = /(?<![A-Za-z0-9])[A-Za-z]:\\(?!\\)[^"'`<>|?*\r\n]*/g;
|
|
133
|
+
|
|
134
|
+
export const unescapeBoundaries = (line) => {
|
|
135
|
+
let out = "", at = 0;
|
|
136
|
+
for (const m of line.matchAll(RAW_WINDOWS_PATH)) {
|
|
137
|
+
out += line.slice(at, m.index).replace(BACKSLASH_ESCAPE, " ") + m[0];
|
|
138
|
+
at = m.index + m[0].length;
|
|
139
|
+
}
|
|
140
|
+
return (out + line.slice(at).replace(BACKSLASH_ESCAPE, " "))
|
|
125
141
|
.replace(/&(?:#\d+|#x[0-9a-fA-F]+|[a-zA-Z]+);/g, " ")
|
|
126
142
|
.replace(/%[0-9a-fA-F]{2}/g, " ");
|
|
143
|
+
};
|
|
127
144
|
|
|
128
145
|
/**
|
|
129
146
|
* Does this entry fire on this line?
|