clearotron 0.4.0-beta.2 → 0.4.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/THIRD-PARTY-NOTICES.md +5 -5
- package/build-info.json +2 -2
- package/driver/CHANGELOG.md +9 -0
- package/driver/coverage-form.mjs +6 -0
- package/driver/coverage-ledger.mjs +14 -2
- package/driver/driver.config.mjs +24 -7
- package/driver/engine/mcp/stdio-server.mjs +12 -1
- package/driver/package.json +1 -1
- package/driver/pipeline-knockout.mjs +29 -8
- package/driver/pipeline.mjs +58 -8
- package/driver/progress.mjs +6 -1
- package/driver/publish/knockout.mjs +16 -3
- package/driver/publish/render-knockout.mjs +38 -11
- package/driver/record-carry.mjs +16 -0
- package/driver/reference-strip-signatures.mjs +11 -1
- package/driver/register-plan.mjs +7 -3
- package/driver/register-served.mjs +91 -0
- package/driver/remedy-accounting.mjs +38 -9
- package/driver/score-redaction.mjs +389 -20
- package/driver/skills/clearance-register/SKILL.md +0 -8
- package/driver/skills/clearance-register/digest.md +1 -1
- package/driver/skills/clearance-register/providers/corsearch.md +1 -1
- package/driver/skills/clearance-register/register-recipes.md +3 -9
- package/driver/skills/clearance-search/SKILL.md +2 -2
- package/driver/skills/clearance-search/phase2-execution.md +1 -1
- package/driver/skills/knockout-assess/SKILL.md +2 -0
- package/driver/stages-knockout.mjs +22 -0
- package/driver/suite-census.json +120 -18
- package/driver/unit-inventory.mjs +38 -43
- package/mcp-server/CHANGELOG.md +4 -0
- package/mcp-server/package.json +1 -1
- package/node_modules/brace-expansion/index.js +78 -22
- package/node_modules/brace-expansion/package.json +1 -1
- package/node_modules/readdir-glob/node_modules/brace-expansion/index.js +78 -22
- package/node_modules/readdir-glob/node_modules/brace-expansion/package.json +1 -1
- package/package.json +1 -1
- package/portal-ui/package.json +2 -2
- package/providers/jx/src/core.js +0 -1
- package/providers/jx/src/judge.js +0 -1
- package/providers/jx/src/nativeread.js +0 -1
- package/providers/oauth-mcp-bridge/CHANGELOG.md +4 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/providers/signa/src/core.js +37 -14
- package/scripts/e2e-first-time.mjs +12 -4
- package/scripts/e2e-scenario-ops.mjs +25 -2
- package/scripts/e2e.mjs +113 -15
- package/scripts/mint-reference-strip-backlog.mjs +36 -2
- package/scripts/score.mjs +102 -29
- package/shared/identifier-scan.mjs +31 -1
package/scripts/e2e.mjs
CHANGED
|
@@ -57,6 +57,7 @@ import { isLiveQueueMarker, isQueueSidecar, liveQueueState, LIVE_QUEUE_STATES, T
|
|
|
57
57
|
// leave the real one to park tomorrow's re-run as a duplicate. usage-ledger.mjs is a pure leaf too
|
|
58
58
|
// (node:fs + node:path + queue-markers.mjs), so it drags no driver machinery in either.
|
|
59
59
|
import { matterLedgerPath } from "../driver/usage-ledger.mjs";
|
|
60
|
+
import { registerServedLine } from "../driver/register-served.mjs"; // which register a run recorded as having served it
|
|
60
61
|
import { probeWorker } from "../driver/queue-watch-probe.mjs"; // the drain this deployment actually has
|
|
61
62
|
// Teardown asks whether a process is actually producing a run before it rewrites the record that says so.
|
|
62
63
|
import { claimLivenessForCodename, claimForbidsDestruction } from "../driver/claim-liveness.mjs";
|
|
@@ -1213,6 +1214,11 @@ export function outboxPackets({ runId = null, runIds = [], queueBase = null } =
|
|
|
1213
1214
|
return { packets: [...hit].sort(), unreadable: null };
|
|
1214
1215
|
}
|
|
1215
1216
|
|
|
1217
|
+
// WITHHELD UNLESS ASKED, and the default is off so nothing has to remember to turn it on. Set once from
|
|
1218
|
+
// argv; `evalAssertion` takes it as an option so a test can drive both paths without touching argv.
|
|
1219
|
+
export let REPORT_NAMES = false;
|
|
1220
|
+
export const setReportNames = (v) => { REPORT_NAMES = Boolean(v); };
|
|
1221
|
+
|
|
1216
1222
|
// ── assertions ───────────────────────────────────────────────────────────────────────────────────────
|
|
1217
1223
|
// Every op is implemented or explicitly reported UNIMPLEMENTED. An op that silently passes because
|
|
1218
1224
|
// nobody wrote it is the exact failure mode this whole suite exists to prevent.
|
|
@@ -1339,7 +1345,70 @@ function bandShapeUnder(full, file, field) {
|
|
|
1339
1345
|
return { doc, unread, shallow, verdict };
|
|
1340
1346
|
}
|
|
1341
1347
|
|
|
1342
|
-
|
|
1348
|
+
/**
|
|
1349
|
+
* A quotation from a RUN, or where to find it.
|
|
1350
|
+
*
|
|
1351
|
+
* WHY THIS EXISTS. `report` is the cheap instrument — free, re-runnable, and the doctrine tells a lane to
|
|
1352
|
+
* re-run it rather than re-run the clearance — so it is the one most often pointed at a real matter, in a
|
|
1353
|
+
* session that records whatever it prints. Its INVESTIGATE rows earned their usefulness by showing the
|
|
1354
|
+
* sentence they object to, and that sentence is the engine's own words about a client's matter.
|
|
1355
|
+
*
|
|
1356
|
+
* A LOCATION IS AS ACTIONABLE AS A QUOTE AND CARRIES NOTHING. "sentence 14 of knockout-findings.md" sends
|
|
1357
|
+
* a reader to the same words, in the file that already holds them, without copying them anywhere else.
|
|
1358
|
+
* The one job that needs them on the page — reading a surprising row against the text — asks with
|
|
1359
|
+
* `--names`, the same flag the scorer takes for the same reason.
|
|
1360
|
+
*
|
|
1361
|
+
* The count is never withheld. How many claims exceeded their evidence is the finding; which words they
|
|
1362
|
+
* used is the detail.
|
|
1363
|
+
*/
|
|
1364
|
+
/**
|
|
1365
|
+
* One door's refusal line.
|
|
1366
|
+
*
|
|
1367
|
+
* A DOOR'S REFUSAL TEXT IS THE DOOR'S, NOT OURS, and that is the reason to gate it rather than a reason
|
|
1368
|
+
* not to. Measured across every receipt on this box — 397 door answers, 213 refusals, 113 distinct
|
|
1369
|
+
* reasons — not one echoes the order: they carry an HTTP status and the server's own error envelope, or
|
|
1370
|
+
* a socket error. So this guards a case that has not happened, and it is worth one line, because the
|
|
1371
|
+
* half of these that CAN echo is structural: an `initialize` refusal is sent before any order exists and
|
|
1372
|
+
* cannot carry one, a `tools/call` refusal is sent after and can. Which it does is the door's own
|
|
1373
|
+
* error-message discipline, which nothing here governs and no clipping saves — an echoing server puts
|
|
1374
|
+
* the mark near the front, well inside the 160 characters this used to print.
|
|
1375
|
+
*
|
|
1376
|
+
* A MISSING REASON IS NOT A WITHHELD ONE. "(no reason recorded)" is a fact about the receipt and says so
|
|
1377
|
+
* plainly; offering to print it with `--names` would promise words that do not exist.
|
|
1378
|
+
*/
|
|
1379
|
+
export function doorReasonLine(r, names = REPORT_NAMES) {
|
|
1380
|
+
if (r?.reason == null || String(r.reason).trim() === "") return "(no reason recorded)";
|
|
1381
|
+
return fromTheRun({ names, quotes: [String(r.reason)],
|
|
1382
|
+
where: "the door's own words are in the round receipt, at rounds[].cases[].answers[].reason" });
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
function fromTheRun({ names, quotes, where }) {
|
|
1386
|
+
// THE LOCATION IS KEPT WHEN THE WORDS ARE SHOWN. It costs one clause beside the quotation it stands in
|
|
1387
|
+
// for, and a reader chasing a surprising row wants to go and read around it rather than only at it.
|
|
1388
|
+
if (names) return `${quotes.map((q) => `"${String(q).slice(0, 160)}"`).join(" · ")} (${where})`;
|
|
1389
|
+
return `${where} — run again with --names to read them here`;
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
/**
|
|
1393
|
+
* The row that fires when a listing does not hold the mark an assertion asked for.
|
|
1394
|
+
*
|
|
1395
|
+
* It is the WIDEST row in either floor op: it printed the name asked for and every name the listing did
|
|
1396
|
+
* hold, in clear, with no flag involved, and it fires precisely when somebody is investigating — a mark
|
|
1397
|
+
* is absent, so a reader is reading the report. It exists twice, once per floor op, with one verb each.
|
|
1398
|
+
*
|
|
1399
|
+
* The COUNT is never withheld. A listing that held nothing is a different defect from one that held four
|
|
1400
|
+
* other marks, and that distinction is the finding.
|
|
1401
|
+
*/
|
|
1402
|
+
function noSuchMark({ names, field, marks, file, verb }) {
|
|
1403
|
+
const listed = (marks ?? []).map((x) => x?.name).filter((x) => typeof x === "string");
|
|
1404
|
+
return { ok: false, saw: `the asked-for mark is not in ${file}, which ${verb} ${listed.length} mark(s)`
|
|
1405
|
+
+ (listed.length
|
|
1406
|
+
? ` — ${fromTheRun({ names, quotes: [field, ...listed],
|
|
1407
|
+
where: `the name asked for is the field of this assertion's path, and the names ${verb} are the "marks" array of ${file}` })}`
|
|
1408
|
+
: "") };
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
function evalAssertion(a, runDir, { names = REPORT_NAMES } = {}) {
|
|
1343
1412
|
const [file, field] = String(a.path ?? "").split(":");
|
|
1344
1413
|
const full = join(runDir, file || "");
|
|
1345
1414
|
|
|
@@ -1566,13 +1635,19 @@ function evalAssertion(a, runDir) {
|
|
|
1566
1635
|
const held = recordsFile
|
|
1567
1636
|
? (recordsFile.marks ?? []).reduce((n, m) => n + (m.records ?? []).length, 0)
|
|
1568
1637
|
: null;
|
|
1569
|
-
|
|
1638
|
+
// Carried WITH ITS POSITION, because the index is what a withheld row prints instead of the words.
|
|
1639
|
+
// `indexOf` on the sentence would be wrong the moment a document repeats one: both rows would send
|
|
1640
|
+
// the reader to the first copy, and a run that says the same thing twice is exactly the run somebody
|
|
1641
|
+
// is reading the report about.
|
|
1642
|
+
const badAt = sentences.map((s, i) => ({ s, i })).filter(({ s }) => aboutRegister.test(s) && !staysInside.test(s)
|
|
1570
1643
|
&& (exceedsACount.test(s) || (singularAbsence.test(s) && !(held > 0))));
|
|
1644
|
+
const bad = badAt.map(({ s }) => s);
|
|
1571
1645
|
const basis = held === null
|
|
1572
1646
|
? "_driver/register-records.json is absent or unreadable, so this is as strict as a run holding zero records — identical verdict, different reason: one is a fact about the search, this is a fact about the check's own evidence"
|
|
1573
1647
|
: `the run holds ${held} register record(s), so a status read off one is supported and only a claim over the field is not`;
|
|
1574
1648
|
return { ok: bad.length === 0,
|
|
1575
|
-
saw: bad.length ? `${bad.length} register claim(s) wider than the records this run holds —
|
|
1649
|
+
saw: bad.length ? `${bad.length} register claim(s) wider than the records this run holds — `
|
|
1650
|
+
+ `${fromTheRun({ names, quotes: bad.slice(0, 2), where: `sentence ${badAt.slice(0, 2).map(({ i }) => i + 1).join(" and ")} of ${file}` })} (${basis})`
|
|
1576
1651
|
: `${sentences.filter((s) => aboutRegister.test(s)).length} register sentence(s), each a count, a labelled expectation, or a status the records support — ${basis}` };
|
|
1577
1652
|
}
|
|
1578
1653
|
// `survivor-not-clear` — a mark this lane did not knock out is a SURVIVOR, never a clear. The two words
|
|
@@ -1712,7 +1787,7 @@ function evalAssertion(a, runDir) {
|
|
|
1712
1787
|
const want = field ? String(field).trim().toLowerCase() : null;
|
|
1713
1788
|
if (!want) return { ok: false, saw: "no mark given (path must be <file>:<MARK NAME>)" };
|
|
1714
1789
|
const m = (doc.marks ?? []).find((x) => String(x?.name ?? "").trim().toLowerCase() === want);
|
|
1715
|
-
if (!m) return {
|
|
1790
|
+
if (!m) return noSuchMark({ names, field, marks: doc.marks, file, verb: "counted" });
|
|
1716
1791
|
const floors = a.value && typeof a.value === "object" ? a.value : null;
|
|
1717
1792
|
if (!floors) return { ok: false, saw: "value must be an object of predicate floors, e.g. {\"identical\": 45}" };
|
|
1718
1793
|
const saw = [], short = [], untaken = [];
|
|
@@ -1720,7 +1795,12 @@ function evalAssertion(a, runDir) {
|
|
|
1720
1795
|
const cell = m.counts?.[pred];
|
|
1721
1796
|
if (!cell) { untaken.push(`${pred}: no such predicate on this run's sidecar`); continue; }
|
|
1722
1797
|
if (!Number.isFinite(cell.total)) {
|
|
1723
|
-
|
|
1798
|
+
// THE SAME FIELD AND THE SAME PROVENANCE as the `unavailable` twenty lines below, which is
|
|
1799
|
+
// withheld — this one is the engine's own prose about why a count could not be taken, out of the
|
|
1800
|
+
// run's sidecar. Withheld with it, and located by the predicate it belongs to.
|
|
1801
|
+
untaken.push(`${pred}: NOT TAKEN — ${cell.unavailable === undefined || cell.unavailable === null
|
|
1802
|
+
? "no reason recorded"
|
|
1803
|
+
: fromTheRun({ names, quotes: [String(cell.unavailable)], where: `the reason is counts.${pred}.unavailable of ${file}` })}`);
|
|
1724
1804
|
continue;
|
|
1725
1805
|
}
|
|
1726
1806
|
saw.push(`${pred}=${cell.total} (floor ${floor})`);
|
|
@@ -1744,11 +1824,14 @@ function evalAssertion(a, runDir) {
|
|
|
1744
1824
|
if (!existsSync(full)) return { ok: false, saw: `${file} absent — the filings lane wrote nothing, not even its refusal` };
|
|
1745
1825
|
const doc = readJson(full);
|
|
1746
1826
|
if (!doc) return { ok: false, saw: `${file} present but unparseable` };
|
|
1747
|
-
if (doc.unavailable) return { ok: false, saw: `the filings were never listed —
|
|
1827
|
+
if (doc.unavailable) return { ok: false, saw: `the filings were never listed — `
|
|
1828
|
+
+ `${fromTheRun({ names, quotes: [String(doc.unavailable)], where: `the reason is the "unavailable" field of ${file}` })}. `
|
|
1829
|
+
+ `A listing that was refused is not a register that holds nothing.` };
|
|
1748
1830
|
const want = field ? String(field).trim().toLowerCase() : null;
|
|
1749
1831
|
if (!want) return { ok: false, saw: "no mark given (path must be <file>:<MARK NAME>)" };
|
|
1750
|
-
const
|
|
1751
|
-
|
|
1832
|
+
const markIndex = (doc.marks ?? []).findIndex((x) => String(x?.name ?? "").trim().toLowerCase() === want);
|
|
1833
|
+
const m = markIndex === -1 ? null : doc.marks[markIndex];
|
|
1834
|
+
if (!m) return noSuchMark({ names, field, marks: doc.marks, file, verb: "listed" });
|
|
1752
1835
|
const floors = a.value && typeof a.value === "object" ? a.value : {};
|
|
1753
1836
|
const minRecords = Number.isFinite(floors.records) ? floors.records : 1;
|
|
1754
1837
|
const minOffices = Number.isFinite(floors.offices) ? floors.offices : 1;
|
|
@@ -1756,8 +1839,15 @@ function evalAssertion(a, runDir) {
|
|
|
1756
1839
|
const offices = [...new Set(records.map((r) => String(r?.territory ?? "").trim().toLowerCase()).filter(Boolean))];
|
|
1757
1840
|
// A term the register refused is REDUCED COVERAGE, and it is named whichever way the floor goes: met,
|
|
1758
1841
|
// it qualifies what was proved; missed, it says the shortfall may be the fetch rather than the register.
|
|
1759
|
-
|
|
1760
|
-
|
|
1842
|
+
// A TERM IS A SPELLING OF A MARK and the reason is the engine's own prose, so both are withheld by
|
|
1843
|
+
// default; the INDEX still says which of the run's terms to go and read.
|
|
1844
|
+
const failedRows = (m.terms ?? []).map((t, i) => ({ t, i })).filter(({ t }) => t?.ok !== true);
|
|
1845
|
+
const failed = names
|
|
1846
|
+
? failedRows.map(({ t }) => `${t?.term ?? "?"}: ${String(t?.reason ?? "no reason recorded").slice(0, 120)}`)
|
|
1847
|
+
// NAMED BY POSITION IN THE FILE, both coordinates. `term 2 of knockout-filings.json` is ambiguous
|
|
1848
|
+
// the moment the listing holds more than one mark, and it always does — so the row would send a
|
|
1849
|
+
// reader to a term belonging to a different mark and read just as confidently.
|
|
1850
|
+
: failedRows.map(({ i }) => `term ${i + 1} of mark ${markIndex + 1} of ${file}`);
|
|
1761
1851
|
const met = records.length >= minRecords && offices.length >= minOffices;
|
|
1762
1852
|
const body = `${records.length} record(s) (floor ${minRecords}) across ${offices.length} office(s) [${offices.join(", ") || "none"}] (floor ${minOffices})`;
|
|
1763
1853
|
if (met) return { ok: true, saw: failed.length ? `${body} — floor met, but ${failed.length} term(s) were refused and this run covered less than it asked for: ${failed.join(" · ")}` : body };
|
|
@@ -3091,6 +3181,10 @@ async function cmdReport(id, { round: requestedToken = null } = {}) {
|
|
|
3091
3181
|
// — the handover's "commit SHA on test" field, READ rather than reconstructed. An absence
|
|
3092
3182
|
// is printed as itself: a run that predates the stamp has none, and inventing one from a reflog
|
|
3093
3183
|
// is the reconstruction this replaces.
|
|
3184
|
+
// — WHICH REGISTER SERVED IT, beside the engine build because it is the same class of fact: what
|
|
3185
|
+
// this run actually ran against, read as a field rather than searched for as a substring. An absence
|
|
3186
|
+
// prints as itself, because "not recorded" is not "none served" and a reader must not have to guess.
|
|
3187
|
+
console.log(` register: ${registerServedLine(readJson(join(run.runDir, "status.json")))}`);
|
|
3094
3188
|
const eb = engineBuildOf(run.runDir);
|
|
3095
3189
|
if (!eb) console.log(` engine build: NOT RECORDED — this run predates the run-start stamp (#1423); any attribution for it is a reflog reconstruction`);
|
|
3096
3190
|
else {
|
|
@@ -3177,7 +3271,7 @@ async function cmdReport(id, { round: requestedToken = null } = {}) {
|
|
|
3177
3271
|
notProbed.push(`${ref}: the doors refused, but this receipt records no REASON — re-run with the current e2e.mjs to check expect.reasonMatches`);
|
|
3178
3272
|
console.log(` reason: NOT RECORDED by the round that wrote this receipt — not a pass`);
|
|
3179
3273
|
} else {
|
|
3180
|
-
for (const r of refusal.reasons) console.log(` [${r.door}] ${
|
|
3274
|
+
for (const r of refusal.reasons) console.log(` [${r.door}] ${doorReasonLine(r)}`);
|
|
3181
3275
|
if (refusal.wantReason && refusal.missed.length)
|
|
3182
3276
|
toInvestigate.push(`${ref}: refused, but ${refusal.missed.join(", ")} did not say "${refusal.wantReason}" — a refusal for the wrong reason reads exactly like the right one`);
|
|
3183
3277
|
else if (refusal.wantReason) console.log(` [ ok ] every door's reason carries "${refusal.wantReason}"`);
|
|
@@ -4040,7 +4134,7 @@ function cmdTeardown(id) {
|
|
|
4040
4134
|
// stamp can never disagree about whether a round is done.
|
|
4041
4135
|
// adds `SCENARIO_FILE` — the ONE filename pattern, exported so the test that pins the widening
|
|
4042
4136
|
// reads the same regex `list` and the sweep read, rather than a copy that agrees today.
|
|
4043
|
-
export { evalAssertion, OPS, queueDrainState, DOORS, runLedger, investigate, brief, secs, queueOutcomes,
|
|
4137
|
+
export { evalAssertion, fromTheRun, OPS, queueDrainState, DOORS, runLedger, investigate, brief, secs, queueOutcomes,
|
|
4044
4138
|
TERMINAL_BY_SUFFIX, TERMINAL_BY_SUFFIX_RAN, SCENARIO_FILE };
|
|
4045
4139
|
|
|
4046
4140
|
// ── main ─────────────────────────────────────────────────────────────────────────────────────────────
|
|
@@ -4064,6 +4158,10 @@ if (invokedDirectly) {
|
|
|
4064
4158
|
if (a === "--round") {
|
|
4065
4159
|
requestedRound = raw[++i] ?? null;
|
|
4066
4160
|
if (!requestedRound || requestedRound.startsWith("--")) die("--round needs a round TOKEN: e2e.mjs report <ID> --round <token>\n The token is on `run`'s output and in the ROUNDS block of any report.");
|
|
4161
|
+
} else if (a === "--names") {
|
|
4162
|
+
// The words the run wrote, on the page. Off by default because `report` is the cheap instrument and
|
|
4163
|
+
// is pointed at real matters in sessions that record what they print.
|
|
4164
|
+
setReportNames(true);
|
|
4067
4165
|
} else if (a.startsWith("--round=")) {
|
|
4068
4166
|
requestedRound = a.slice("--round=".length);
|
|
4069
4167
|
if (!requestedRound) die("--round= needs a round TOKEN: e2e.mjs report <ID> --round=<token>");
|
|
@@ -4071,13 +4169,13 @@ if (invokedDirectly) {
|
|
|
4071
4169
|
}
|
|
4072
4170
|
const [cmd, arg, ...extra] = positional;
|
|
4073
4171
|
if (extra.length) die(`unexpected argument "${extra[0]}"\n`
|
|
4074
|
-
+ "usage: e2e.mjs list | run <ID> [--stale] | status | report <ID> [--round <token>] | teardown <ID> [--waive-unread]");
|
|
4172
|
+
+ "usage: e2e.mjs list | run <ID> [--stale] | status | report <ID> [--round <token>] [--names] | teardown <ID> [--waive-unread]");
|
|
4075
4173
|
switch (cmd) {
|
|
4076
4174
|
case "list": cmdList(); break;
|
|
4077
4175
|
case "run": if (!arg) die("usage: e2e.mjs run <ID> [--stale]"); await cmdRun(arg); break;
|
|
4078
4176
|
case "status": cmdStatus(); break;
|
|
4079
|
-
case "report": if (!arg) die("usage: e2e.mjs report <ID> [--round <token>]"); await cmdReport(arg, { round: requestedRound }); break;
|
|
4177
|
+
case "report": if (!arg) die("usage: e2e.mjs report <ID> [--round <token>] [--names]"); await cmdReport(arg, { round: requestedRound }); break;
|
|
4080
4178
|
case "teardown": if (!arg) die("usage: e2e.mjs teardown <ID> [--waive-unread]"); cmdTeardown(arg); break;
|
|
4081
|
-
default: die("usage: e2e.mjs list | run <ID> [--stale] | status | report <ID> [--round <token>] | teardown <ID> [--waive-unread]");
|
|
4179
|
+
default: die("usage: e2e.mjs list | run <ID> [--stale] | status | report <ID> [--round <token>] [--names] | teardown <ID> [--waive-unread]");
|
|
4082
4180
|
}
|
|
4083
4181
|
}
|
|
@@ -36,17 +36,51 @@ if (p.laid) console.log(`mint-reference-strip-backlog: ${p.laid} tracked path(s)
|
|
|
36
36
|
const tracked = p.files;
|
|
37
37
|
const minted = censusOf(ROOT, tracked, publishedReader(ROOT, (f) => readFileSync(join(ROOT, f), "utf8")));
|
|
38
38
|
|
|
39
|
+
// ── EVERY ROW IS EXPLAINED, OR THE TABLE IS A NUMBER NOBODY CAN ACT ON ────────────────────────────
|
|
40
|
+
//
|
|
41
|
+
// A count on its own tells a reader a file has residue in it. It does not tell them whether the line is
|
|
42
|
+
// residue at all — and once the table reached zero, the next row to appear is either a regression to
|
|
43
|
+
// repair or a known-good the scanner cannot tell from one. The first `.js` row is the second kind: an
|
|
44
|
+
// adapter header names a list separator as `(, ; / etc)`, which matches the third signature and is also
|
|
45
|
+
// correct English about punctuation. Without a sentence beside it that row is a number that will never
|
|
46
|
+
// fall, and a floor with one of those in it stops being read.
|
|
47
|
+
//
|
|
48
|
+
// So notes are carried by the minter rather than typed into the artifact: a re-mint keeps the note for a
|
|
49
|
+
// file that still has rows, drops it for a file that no longer does (a repair takes its explanation with
|
|
50
|
+
// it), and says which rows are still unexplained. It never invents one — an unexplained row is reported,
|
|
51
|
+
// and the arm that reads this table refuses it.
|
|
52
|
+
const NOTES = {
|
|
53
|
+
"providers/clarivate/src/core.js":
|
|
54
|
+
"NOT residue — the header names a list separator as `(, ; / etc)`. It matches the third signature "
|
|
55
|
+
+ "and is correct English about punctuation, so it is recorded rather than repaired.",
|
|
56
|
+
};
|
|
57
|
+
const notesFor = (files) => Object.fromEntries(
|
|
58
|
+
Object.keys(files).filter((f) => NOTES[f]).map((f) => [f, NOTES[f]]).sort(([a], [b]) => a < b ? -1 : 1));
|
|
59
|
+
const unexplained = (files) => Object.keys(files).filter((f) => !NOTES[f]).sort();
|
|
60
|
+
|
|
39
61
|
if (process.argv.includes("--check")) {
|
|
40
62
|
const have = JSON.parse(readFileSync(TABLE, "utf8"));
|
|
41
63
|
const a = JSON.stringify(have.files), b = JSON.stringify(minted.files);
|
|
64
|
+
if (JSON.stringify(have.notes ?? {}) !== JSON.stringify(notesFor(minted.files))) {
|
|
65
|
+
console.error("reference-strip backlog: the committed notes do not match this tree's rows.");
|
|
66
|
+
console.error(" re-mint with: node scripts/mint-reference-strip-backlog.mjs");
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
42
69
|
if (a !== b || have.total !== minted.total) {
|
|
43
70
|
console.error("reference-strip backlog is STALE against the tree.");
|
|
44
71
|
console.error(` committed total ${have.total}, tree has ${minted.total}`);
|
|
45
72
|
console.error(" re-mint with: node scripts/mint-reference-strip-backlog.mjs");
|
|
46
73
|
process.exit(1);
|
|
47
74
|
}
|
|
48
|
-
|
|
75
|
+
const open = unexplained(minted.files);
|
|
76
|
+
const held = Object.keys(minted.files).length - open.length;
|
|
77
|
+
console.log(`reference-strip backlog: current — ${minted.total} matching line(s); `
|
|
78
|
+
+ `${open.length} file(s) to repair, ${held} recorded as not residue`);
|
|
49
79
|
} else {
|
|
50
|
-
writeFileSync(TABLE, JSON.stringify({ signatures: SIGNATURES.map((s) => s.name), ...minted
|
|
80
|
+
writeFileSync(TABLE, JSON.stringify({ signatures: SIGNATURES.map((s) => s.name), ...minted,
|
|
81
|
+
notes: notesFor(minted.files) }, null, 2) + "\n");
|
|
51
82
|
console.log(`minted ${TABLE}: ${minted.total} line(s) across ${Object.keys(minted.files).length} file(s)`);
|
|
83
|
+
const open = unexplained(minted.files);
|
|
84
|
+
if (open.length) console.log(`mint-reference-strip-backlog: ${open.length} file(s) carry rows with no note — `
|
|
85
|
+
+ `each is a line to repair, or a note to add here if it is not residue:\n ${open.join("\n ")}`);
|
|
52
86
|
}
|
package/scripts/score.mjs
CHANGED
|
@@ -64,7 +64,7 @@ import { recordQids } from "../driver/named-band.mjs";
|
|
|
64
64
|
import { previousRunDir, scenarioRefs } from "../driver/e2e-rounds.mjs";
|
|
65
65
|
// The names rule, kept out of this file so it is testable without a run directory — the same reason
|
|
66
66
|
// reference-score.mjs holds the scoring rules rather than this script.
|
|
67
|
-
import { protectedStrings, redactor, installRedaction, unclassifiedNotice, REDACTION_NOTICE } from "../driver/score-redaction.mjs";
|
|
67
|
+
import { protectedStrings, redactor, authoredRedactor, printAuthored, printPreRedacted, redactDeep, installRedaction, unclassifiedNotice, REDACTION_NOTICE } from "../driver/score-redaction.mjs";
|
|
68
68
|
import { readSettleStamp } from "../driver/settle-stamp.mjs";
|
|
69
69
|
import { envFrom } from "../shared/env-aliases.mjs"; // — resolves EITHER spelling; names the retired one because that is the live-writable half
|
|
70
70
|
|
|
@@ -517,8 +517,20 @@ function print(id, ref, run, s, delta, refPath) {
|
|
|
517
517
|
const B = s.buckets;
|
|
518
518
|
const scored = B.found.length + B.withheld.length + B.lost.length;
|
|
519
519
|
|
|
520
|
-
|
|
520
|
+
// A RULE IS A RULE. Nothing in it came from anywhere; routed so a party's ordinary long word cannot
|
|
521
|
+
// rewrite the page's own furniture.
|
|
522
|
+
printAuthored(`\n${"═".repeat(78)}`);
|
|
521
523
|
console.log(`${id} — ${ref.mark ?? "(mark unnamed in the reference)"}`);
|
|
524
|
+
// NOT ROUTED, AND THIS IS THE ONE THAT PROVED WHY. A path looks authored — the config repository's
|
|
525
|
+
// name and the working directory's name are furniture, and the derived layer was tokenising them, so
|
|
526
|
+
// a reader was shown a path they could not use. Routing it put a real client name on the page on the
|
|
527
|
+
// first run measured after the change: a run directory is NAMED AFTER THE MATTER, and that segment is
|
|
528
|
+
// a distinctive word of a party rather than a whole name, so it is exactly what the derived layer
|
|
529
|
+
// catches and exactly what the authored path drops.
|
|
530
|
+
//
|
|
531
|
+
// A path is not authored text. It is furniture with a data-derived segment in the middle of it, and
|
|
532
|
+
// the two cannot be separated by choosing an instrument for the whole line. Both stay on the full
|
|
533
|
+
// layer until the run directories themselves stop carrying matter.
|
|
522
534
|
console.log(`reference: ${refPath}`);
|
|
523
535
|
console.log(` ${ref.source}`);
|
|
524
536
|
console.log(`run: ${run.dir}`);
|
|
@@ -534,7 +546,9 @@ function print(id, ref, run, s, delta, refPath) {
|
|
|
534
546
|
// 28). The tool already had the honest pattern four lines down — `withheld` names what it could not
|
|
535
547
|
// read and declines to answer — and the delivery line invented a verdict from the same kind of
|
|
536
548
|
// absence. This is that asymmetry closed, in the direction of the honest half.
|
|
537
|
-
|
|
549
|
+
// `deliveryLine` composes authored words, a state token and a timestamp, read at its producer in
|
|
550
|
+
// reference-score.mjs — no part of it comes from the matter.
|
|
551
|
+
printAuthored(deliveryLine(run));
|
|
538
552
|
// — THE INSTRUMENT, BESIDE THE NUMBER. `--json` has carried `scorer_version` since this file
|
|
539
553
|
// shipped; the human path did not, and the human path is the one whose numbers get pasted into an
|
|
540
554
|
// issue. That body states 6/9 for a run that re-scores 5/2/2 today across two scorer changes
|
|
@@ -547,13 +561,17 @@ function print(id, ref, run, s, delta, refPath) {
|
|
|
547
561
|
console.log(`engine: ${run.engineCommit
|
|
548
562
|
? `${run.engineCommit.slice(0, 12)}${run.engineCommitFrom === "meta.json" ? " (from the pool's meta.json — this dir has no status.json)" : ""}`
|
|
549
563
|
: "(not recorded — no status.json engine stamp and no pool meta.json carrying one)"}`);
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
564
|
+
printAuthored(`scorer: v${SCORER_VERSION} (a score with no version predates this stamp and is not comparable to one)`);
|
|
565
|
+
printAuthored(`lane: ${run.lane}${run.hasDriver ? "" : " (no _driver/ — pool dir, not a workspace archive)"}`);
|
|
566
|
+
// Class numbers and territory codes. Neither is a name, and both were being broken up by the derived
|
|
567
|
+
// layer on references whose parties carry an ordinary long word.
|
|
568
|
+
printAuthored(`scope: classes ${run.scopeClasses.join(", ") || "(none recorded)"} territories ${run.scopeTerritories.join(", ") || "(none recorded)"}`);
|
|
553
569
|
// — WHICH SUBJECT MARKS THIS REFERENCE ANSWERS, on the line beside the classes and territories it
|
|
554
570
|
// already scopes by, because it is the same kind of fact. Never silent: a gold set that declares
|
|
555
571
|
// nothing says NOT DECLARED and names the marks the run searched.
|
|
556
|
-
|
|
572
|
+
// `coverage.why` is the SCORER's own sentence about what it could measure, not a sentence about
|
|
573
|
+
// anybody — the same reading that put `coverage` in the safe key set.
|
|
574
|
+
printAuthored(`coverage: ${s.coverage.state} — ${s.coverage.why}`);
|
|
557
575
|
// Never a bare "(unreadable)". An unread verdict is an absence, and an absence that prints as an empty
|
|
558
576
|
// parenthesis is the one a reader skims past — so it states the reason, and where a reading DID come
|
|
559
577
|
// from it names the artifact, because the two lanes answer this from different files.
|
|
@@ -616,7 +634,9 @@ function print(id, ref, run, s, delta, refPath) {
|
|
|
616
634
|
console.log(` entries are unreachable by construction. The counts axis above is this scenario's score.`);
|
|
617
635
|
console.log(` What follows folds over the run's OWN findings only.\n`);
|
|
618
636
|
}
|
|
619
|
-
|
|
637
|
+
// CLASS: THE TABLE'S OWN COLUMNS. "marks" is this tool's word for its own header, and on a reference
|
|
638
|
+
// whose parties carry it as a distinctive word the header read `of the «name N» the lawyer named`.
|
|
639
|
+
printAuthored(row("bucket", "n", "of the marks the lawyer named"));
|
|
620
640
|
console.log(row("found", B.found.length, `${pct(B.found.length, scored)} of ${scored} in-scope reference marks`));
|
|
621
641
|
if (s.registerOnly) {
|
|
622
642
|
// ONE source for the reason, so the summary and the rows can never name different causes. Missing
|
|
@@ -745,11 +765,11 @@ function print(id, ref, run, s, delta, refPath) {
|
|
|
745
765
|
for (const note of M.notes) console.log(` ${note}`);
|
|
746
766
|
|
|
747
767
|
console.log(`\n── axis B · field ${"─".repeat(59)}`);
|
|
748
|
-
if (!s.field.length)
|
|
768
|
+
if (!s.field.length) printAuthored(" the reference flags no entry as on-field — not scored, not passed");
|
|
749
769
|
for (const f of s.field) console.log(` ${String(f.state).padEnd(12)} ${f.mark} — ${f.detail}`);
|
|
750
770
|
|
|
751
771
|
console.log(`\n── axis C · sources ${"─".repeat(57)}`);
|
|
752
|
-
if (!s.sources.length)
|
|
772
|
+
if (!s.sources.length) printAuthored(" the reference names no channels — not scored, not passed");
|
|
753
773
|
for (const c of s.sources) console.log(` ${(c.searched ? "searched" : "ABSENT").padEnd(12)} ${c.channel}`);
|
|
754
774
|
|
|
755
775
|
console.log(`\n── axis D · gap discipline ${"─".repeat(50)}`);
|
|
@@ -761,9 +781,13 @@ function print(id, ref, run, s, delta, refPath) {
|
|
|
761
781
|
// instructed territory, or does the lane thin out as the count rises. Read `sub-query` first, then
|
|
762
782
|
// `returned` — a sub-query that ran and came back over the provider's ceiling is not depth holding.
|
|
763
783
|
const T = s.territories;
|
|
764
|
-
|
|
784
|
+
// AUTHORED, not data: this heading and the column ruler below it carry the words "territory" and
|
|
785
|
+
// "depth", which a party name can put into the protected set. Measured before this change: a reference
|
|
786
|
+
// whose proprietor was "Depth Charge" printed `per-territory «name 2»`, and one called "Territory
|
|
787
|
+
// Holdings" printed `per-«name 1» depth` — a reader cannot tell a redaction from a word.
|
|
788
|
+
printAuthored(`\n── axis E · per-territory depth ${"─".repeat(45)}`);
|
|
765
789
|
if (!T.subQueriesResolved) console.log(` sub-queries NOT MEASURABLE — ${T.why}`);
|
|
766
|
-
|
|
790
|
+
printAuthored(` ${"territory".padEnd(10)} ${"sub-query".padEnd(22)} ${"own".padEnd(4)} ${"grouped".padEnd(8)} ${"returned".padEnd(9)} ${"recall".padEnd(7)} reference entries`);
|
|
767
791
|
for (const r of T.rows) {
|
|
768
792
|
// `—` for recall is deliberate and is the whole design point of this row: a territory whose
|
|
769
793
|
// reference carries nothing to score prints NEITHER 0% (which reads as total failure) NOR 100%
|
|
@@ -899,15 +923,18 @@ function print(id, ref, run, s, delta, refPath) {
|
|
|
899
923
|
else for (const d of delta) console.log(` ${d.mark}: ${d.from} → ${d.to}`);
|
|
900
924
|
|
|
901
925
|
console.log(`\n${"═".repeat(78)}`);
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
926
|
+
// CLASS: THE CLOSING LEGEND. Eight lines, every one a literal of this file's own with nothing
|
|
927
|
+
// interpolated from a reference or a run — the class most obviously ours and the least defensible to
|
|
928
|
+
// leave on the data path, because a party's ordinary long word rewrites the page's own instructions.
|
|
929
|
+
printAuthored(`This is a measurement, not a verdict. There is no PASS here and the exit code is always 0.`);
|
|
930
|
+
printAuthored(`Reproducing the reference proves nothing — it is a regression tripwire, never a target.`);
|
|
931
|
+
printAuthored(`What to read: every WITHHELD row is a gather-to-judgment seam defect, not a recall one.`);
|
|
932
|
+
printAuthored(`Axis E: "own" counts sub-queries naming ONE territory and nothing else — the deep-dive itself.`);
|
|
933
|
+
printAuthored(`A territory with no reference entry prints "—", never 0% and never 100%. Both are conclusions.`);
|
|
907
934
|
// The instrument changed. A round comparing its noise against a round scored before `uncovered`
|
|
908
935
|
// existed is comparing two different measurements, and the drop will otherwise read as an improvement.
|
|
909
|
-
|
|
910
|
-
|
|
936
|
+
printAuthored(`"uncovered" is a finding of a mark this reference does not answer — a noise count from before`);
|
|
937
|
+
printAuthored(`that bucket existed is not comparable with one after it. The gold set must declare covers_marks.\n`);
|
|
911
938
|
}
|
|
912
939
|
|
|
913
940
|
// ── main ─────────────────────────────────────────────────────────────────────────────────────────────
|
|
@@ -977,17 +1004,40 @@ const delta = prev ? bucketDelta(scored.buckets, prev.buckets) : null;
|
|
|
977
1004
|
// that adds a print. Here `renderCarryThrough`'s lines and anything added after this was written are
|
|
978
1005
|
// covered without anyone remembering to.
|
|
979
1006
|
//
|
|
980
|
-
//
|
|
981
|
-
// lawyer never named is still somebody's name
|
|
1007
|
+
// THE INPUTS ARE THE REFERENCE, THE SCORED BUCKETS **AND THE RUN'S OWN FINDINGS**, and the third was
|
|
1008
|
+
// missing. A proprietor this run surfaced that the lawyer never named is still somebody's name; the
|
|
1009
|
+
// buckets carry the ones the scorer could join to a reference entry, and the FINDINGS carry every other.
|
|
1010
|
+
//
|
|
1011
|
+
// Measured on R18 `f5764a2f`: the `verdict:` line printed a proprietor in clear while tokenising the mark
|
|
1012
|
+
// beside it. The name sits at `findings[0].owner.name` — a key already in the protected set — and was
|
|
1013
|
+
// simply never walked, because the findings were not handed over. It also appears inside five prose
|
|
1014
|
+
// fields of the same document (`net`, `practical_position`, `read`, `condition`, `text`), so collecting it
|
|
1015
|
+
// as a name substitutes it in all of them rather than needing each one withheld whole. That matters:
|
|
1016
|
+
// `text` carries the reviewer's verdict word, and withholding it entire would blank a value the reader
|
|
1017
|
+
// came for.
|
|
982
1018
|
let unclassifiedKeys = [];
|
|
1019
|
+
// The two instruments the JSON payload needs, live only when names are withheld. With `--names` they
|
|
1020
|
+
// stay identity, which is the same "nothing to withhold" case `printAuthored` already has.
|
|
1021
|
+
let redactValue = (x) => x, redactKey = (x) => x;
|
|
983
1022
|
if (!opts.names) {
|
|
984
|
-
const { names, prose, unclassified } = protectedStrings({ reference: ref, scored });
|
|
1023
|
+
const { names, prose, derived, unclassified } = protectedStrings({ reference: ref, scored, run: run.findings ?? null });
|
|
985
1024
|
unclassifiedKeys = unclassified;
|
|
986
|
-
|
|
1025
|
+
// The second redactor is for lines this tool WROTE. It drops the derived-word layer
|
|
1026
|
+
// only, so a party's ordinary long word stops rewriting our own headings while its full name is still
|
|
1027
|
+
// taken out of them. A structural line nobody routed through `printAuthored` is redacted as before,
|
|
1028
|
+
// which is the old behaviour and the safe side.
|
|
1029
|
+
redactValue = redactor({ names, prose });
|
|
1030
|
+
redactKey = authoredRedactor({ names, prose, derived });
|
|
1031
|
+
installRedaction(redactValue, undefined, redactKey);
|
|
987
1032
|
}
|
|
988
1033
|
|
|
989
1034
|
if (opts.json) {
|
|
990
|
-
|
|
1035
|
+
// REDACTED AS A STRUCTURE, THEN WRITTEN RAW. Stringifying first and sending the text through the
|
|
1036
|
+
// boundary redacts a document with no prose in it: every string is either a field NAME this file
|
|
1037
|
+
// wrote or a VALUE out of the data, and they need opposite treatment. On one real score that turned
|
|
1038
|
+
// the keys `owner` and `additional` into tokens, because both are distinctive words of multi-word
|
|
1039
|
+
// parties in that reference — two fields no consumer could address, or discover the new name of.
|
|
1040
|
+
printPreRedacted(`${JSON.stringify(redactDeep({
|
|
991
1041
|
// — the instrument that produced these numbers, so a reader comparing two archived scores can
|
|
992
1042
|
// tell whether the comparison is valid. A score with no `scorer_version` predates this stamp.
|
|
993
1043
|
scorer_version: SCORER_VERSION,
|
|
@@ -1012,14 +1062,19 @@ if (opts.json) {
|
|
|
1012
1062
|
// so leaving it out would hide it from exactly the readers most likely to automate on it, and a
|
|
1013
1063
|
// consumer would have no way to tell an absent measure from a clean one.
|
|
1014
1064
|
carry_through: (() => { const ct = carryThrough(run.dir); return { ...ct, coverage: coverageConflicts(run.dir, ct.lost) }; })(),
|
|
1015
|
-
}, null, 2));
|
|
1065
|
+
}, { redactValue, redactKey }), null, 2)}\n`);
|
|
1016
1066
|
} else {
|
|
1017
1067
|
// Said before the first number, not after the last: a reader who stops at the top screen must know
|
|
1018
1068
|
// which of the two readings they are holding.
|
|
1019
|
-
|
|
1069
|
+
// THE NOTICE THAT EXPLAINS THE TOKENS HAD TOKENS IN IT. A party's distinctive word collided with
|
|
1070
|
+
// "marks", so the sentence defining «name N» was itself redacted — useless exactly where it matters.
|
|
1071
|
+
if (!opts.names) printAuthored(`\n ${REDACTION_NOTICE}`);
|
|
1020
1072
|
// A reference that has grown a field this module does not classify is unprotected in exactly that
|
|
1021
1073
|
// field, so the reader learns it before anything below, not after.
|
|
1022
|
-
|
|
1074
|
+
// AND THE WARNING PRINTED ONE OF ITS OWN KEY NAMES AS A TOKEN, so it could not name the key it
|
|
1075
|
+
// exists to name. It fires when a new reference field is unclassified, which is the one moment it has
|
|
1076
|
+
// to be readable.
|
|
1077
|
+
if (unclassifiedKeys.length) printAuthored(` ${unclassifiedNotice(unclassifiedKeys)}`);
|
|
1023
1078
|
print(String(id).toUpperCase(), ref, run, scored, delta, refPath);
|
|
1024
1079
|
// — THE LAWYER'S OWN STATEMENTS OF WHAT THE RUN MUST DEMONSTRATE. The buckets cannot carry
|
|
1025
1080
|
// these: an assertion says WHY a mark matters, and that reasoning is what tells a reader which lane to
|
|
@@ -1028,11 +1083,29 @@ if (opts.json) {
|
|
|
1028
1083
|
if (statements.length) {
|
|
1029
1084
|
console.log(`\n THE REFERENCE'S OWN ASSERTIONS AND CONTROLS (${statements.length}) — the scorer does not read English, so`);
|
|
1030
1085
|
console.log(" these are the run's own facts about the marks each one names, never a verdict on the sentence:");
|
|
1086
|
+
// A MARK LIFTED OUT OF A WITHHELD SENTENCE IS STILL A MARK.
|
|
1087
|
+
//
|
|
1088
|
+
// `st.text` is a reference sentence, so it is in the prose set and prints as withheld. These rows are
|
|
1089
|
+
// the SAME sentence, parsed: `scoreStatements` runs the mark extractor over it and reports each mark
|
|
1090
|
+
// it names with the bucket that mark landed in. A mark that appears nowhere but inside that sentence
|
|
1091
|
+
// was never a value of a name field, so it is in no name set, and the boundary redactor has nothing
|
|
1092
|
+
// to match — it printed in clear, beside a mark that was correctly withheld, on a real score.
|
|
1093
|
+
//
|
|
1094
|
+
// WITHHELD AT THE PRINT SITE RATHER THAN BY WIDENING THE PROTECTED SET. Collecting what the extractor
|
|
1095
|
+
// finds into the protected set was tried first and measured: that extractor is a floor, not a sound
|
|
1096
|
+
// extraction — it reads capitals, lawyers emphasise in capitals, and the redactor is case-insensitive,
|
|
1097
|
+
// so an emphasised `REFERENCE` in one gold sentence tokenised every lowercase "reference" on the
|
|
1098
|
+
// page. 34 more tokens, 17 from that one word, and a bucket table reading `88% of 8 in-scope «name»
|
|
1099
|
+
// marks`. The cure was worse than the leak, so it is not there; the state is the finding, and the
|
|
1100
|
+
// mark's identity is the detail that belongs behind the flag.
|
|
1101
|
+
const markShown = (mark) => (opts.names ? mark : "[withheld — run again with --names to read it]");
|
|
1031
1102
|
for (const st of statements) {
|
|
1032
1103
|
console.log(`\n [${st.kind}] ${st.text}`);
|
|
1033
1104
|
if (st.halves.length)
|
|
1034
|
-
for (const h of st.halves) console.log(` ${h.mark} — ${h.state}`);
|
|
1035
|
-
|
|
1105
|
+
for (const h of st.halves) console.log(` ${markShown(h.mark)} — ${h.state}`);
|
|
1106
|
+
// `why` names the same marks in a sentence of its own, so it is withheld the same way or it is a
|
|
1107
|
+
// second door onto the first defect.
|
|
1108
|
+
if (st.why) console.log(` UNEVALUATED: ${opts.names ? st.why : st.why.replace(/names .*?, none of which/, "names marks this reference holds, none of which")}`);
|
|
1036
1109
|
}
|
|
1037
1110
|
} else if (ref) {
|
|
1038
1111
|
// Absence, stated. A reference with no assertions and a reference the scorer failed to read are
|
|
@@ -129,7 +129,37 @@ export const ALLOWED_CONTEXT =
|
|
|
129
129
|
* The escaped form (`C:\\`) is not a raw path, so an escape beside a path in a string is still read.
|
|
130
130
|
*/
|
|
131
131
|
const BACKSLASH_ESCAPE = /\\(?:[\\nrtfv0]|u[0-9a-fA-F]{4}|x[0-9a-fA-F]{2})/g;
|
|
132
|
-
|
|
132
|
+
|
|
133
|
+
// A RAW PATH IS NOT ONLY AN ABSOLUTE ONE. The drive-letter form was the only one recognised, so the same
|
|
134
|
+
// misreading came back on the relative forms: `.\node_modules\.bin` and `<prefix>\node_modules` were read
|
|
135
|
+
// as holding an escaped newline, which ate the `n` and left the three letters after it standing at the
|
|
136
|
+
// front of a word. Three occurrences on the public tip, in a CI workflow and a test.
|
|
137
|
+
//
|
|
138
|
+
// The anchors below are what makes a backslash a separator rather than an escape, and each is a shape a
|
|
139
|
+
// path is written in and a string literal is not. A backslash after a LETTER is deliberately not an anchor:
|
|
140
|
+
// that is the escape the rule exists for, and the arms pin it.
|
|
141
|
+
//
|
|
142
|
+
// EACH ANCHOR MATCHES ONLY THE SHAPE ITS COMMENT CLAIMS, and the first version of this did not. `[>%]` alone
|
|
143
|
+
// read ANY closing angle or percent before an escape as a path — `rose to 50%\nNorvale`, `cmd >\nNorvale` —
|
|
144
|
+
// and `\.{1,2}` read the last two dots of an ellipsis as `..\`. In each of those the escape was left
|
|
145
|
+
// unescaped, the `n` stayed glued to the name, and A REAL NAME WENT UNFOUND. That is the worse of this
|
|
146
|
+
// rule's two failure directions, and it is the one a widened anchor buys.
|
|
147
|
+
//
|
|
148
|
+
// THE RESIDUE, said here so it is not a surprise: `a<b>\nNorvale` still reads as a path, because `<b>` is
|
|
149
|
+
// indistinguishable by shape from `<root>`. A closing HTML tag immediately before an escape, with a name
|
|
150
|
+
// straight after it, is not caught. A minimum length inside the angles would be an arbitrary line through
|
|
151
|
+
// real stand-ins, so this is recorded rather than fixed.
|
|
152
|
+
const PATH_TAIL = "[^\"'`<>|?*\\r\\n]*";
|
|
153
|
+
const RAW_WINDOWS_PATH = new RegExp([
|
|
154
|
+
// C:\… — a drive letter
|
|
155
|
+
"(?<![A-Za-z0-9])[A-Za-z]:\\\\(?!\\\\)",
|
|
156
|
+
// .\… and ..\… — relative. The dot in the lookbehind is what stops an ellipsis reading as `..\`.
|
|
157
|
+
"(?<![A-Za-z0-9.])\\.{1,2}\\\\(?!\\\\)",
|
|
158
|
+
// <prefix>\… — a stand-in for the first segment, as a comment or a doc writes it
|
|
159
|
+
"(?<=<[A-Za-z_][A-Za-z0-9_]*>)\\\\(?!\\\\)",
|
|
160
|
+
// %TEMP%\… — an environment variable, as a script writes it
|
|
161
|
+
"(?<=%[A-Za-z_][A-Za-z0-9_]*%)\\\\(?!\\\\)",
|
|
162
|
+
].map((anchor) => anchor + PATH_TAIL).join("|"), "g");
|
|
133
163
|
|
|
134
164
|
export const unescapeBoundaries = (line) => {
|
|
135
165
|
let out = "", at = 0;
|