clearotron 0.2.3 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +36 -37
- package/CONTRIBUTING.md +8 -4
- package/INSTALL.md +24 -4
- package/README.md +7 -6
- package/bin/example.mjs +6 -5
- package/bin/onboard.mjs +175 -9
- package/bin/start.mjs +66 -4
- package/build-info.json +2 -2
- package/demo/README.md +1 -1
- package/docs/GLOSSARY.md +85 -0
- package/docs/README.md +1 -0
- package/docs/architecture/01-product-overview.md +21 -9
- package/docs/architecture/05-config-governance.md +5 -0
- package/docs/decisions/0006-what-the-public-repository-carries.md +30 -5
- package/driver/CHANGELOG.md +34 -0
- package/driver/README.md +25 -6
- package/driver/connotation-search.mjs +1 -1
- package/driver/contract-audit.mjs +5 -1
- package/driver/contract-e3-baseline.json +11 -11
- package/driver/doubt-selection.mjs +1 -1
- package/driver/drainer-identity.mjs +1 -1
- package/driver/effort-model.mjs +2 -2
- package/driver/engine/probe.mjs +45 -5
- package/driver/gateway.mjs +2 -2
- package/driver/outbox-backoff.mjs +1 -1
- package/driver/package.json +1 -1
- package/driver/pipeline.mjs +206 -101
- package/driver/plain-register.mjs +16 -2
- package/driver/portal-config-view.mjs +30 -1
- package/driver/portal-local-auth.mjs +5 -1
- package/driver/portal-service.mjs +53 -2
- package/driver/predelivery-lint.mjs +54 -25
- package/driver/publish/render.mjs +109 -14
- package/driver/search-policy.mjs +1 -1
- package/driver/stage-context.mjs +13 -0
- package/driver/stages.mjs +51 -4
- package/driver/suite-census.json +97 -31
- package/driver/systemd/clearotron-worker.service +3 -3
- package/driver/tokens.mjs +1 -1
- package/driver/unit-inventory.mjs +34 -4
- package/mcp-server/CHANGELOG.md +2 -0
- package/mcp-server/package.json +1 -1
- package/package.json +4 -10
- package/portal-ui/dist/assets/{index-CEYvXdqH.js → index-KFAHMgdT.js} +73 -34
- package/portal-ui/dist/index.html +1 -1
- package/portal-ui/package.json +1 -1
- package/providers/oauth-mcp-bridge/CHANGELOG.md +2 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/scripts/added-reference-check.mjs +45 -89
- package/scripts/e2e.mjs +2 -2
- package/scripts/generated-files-are-current.mjs +125 -0
- package/scripts/mint-names-in-force.mjs +4 -2
- package/scripts/mint-public-residue.mjs +74 -0
- package/scripts/mint-reference-strip-backlog.mjs +12 -1
- package/scripts/mint-suite-census.mjs +5 -2
- package/scripts/render-check.mjs +13 -2
- package/scripts/report-frame-check.mjs +8 -1
- package/scripts/report-screenshot.mjs +6 -3
- package/scripts/revisit-render-check.mjs +6 -3
- package/scripts/score.mjs +1 -1
- package/scripts/strip-tracker-citations.mjs +9 -1
- package/scripts/test-full.mjs +226 -0
- package/shared/README.md +2 -1
- package/shared/browser-temp-root.mjs +142 -0
- package/shared/checkout-move.mjs +1 -1
- package/shared/client-door.mjs +4 -3
- package/shared/identifier-scan.mjs +1 -1
- package/shared/install-auth.mjs +35 -0
- package/shared/invocation.mjs +5 -1
- package/shared/listen.mjs +55 -3
- package/shared/names-in-force.mjs +4 -1
- package/shared/os-advice.mjs +91 -0
- package/shared/reap-on-exit.mjs +42 -0
- package/shared/reference-guard-classes.mjs +351 -0
- package/shared/suite-census.mjs +31 -3
- package/shared/withheld-paths-access.mjs +36 -18
|
@@ -49,6 +49,21 @@ export const PLAIN_FORMS = Object.freeze([
|
|
|
49
49
|
/** The longest visible sentence a reader should meet. The issue's number, not a derived one. */
|
|
50
50
|
export const SENTENCE_WORD_LIMIT = 25;
|
|
51
51
|
|
|
52
|
+
/**
|
|
53
|
+
* How a term in `PLAIN_FORMS` is looked for in prose — ONE definition, because two of them drift.
|
|
54
|
+
*
|
|
55
|
+
* THE INFLECTIONS ARE THE POINT, and they were the reason a second copy of this rule survived. The
|
|
56
|
+
* pre-delivery lint carried its own hand-tuned patterns — `\bproprietors?\b`, `\bprevails?\b|\bprevailing\b`
|
|
57
|
+
* — while this file built `\bproprietor\b` and matched neither plural. So the pinned source was the
|
|
58
|
+
* WEAKER of the two, and reading terms from it without this would have quietly narrowed what the live
|
|
59
|
+
* check catches: a consolidation that loses coverage is a regression wearing a tidy-up's clothes.
|
|
60
|
+
*
|
|
61
|
+
* A trailing `s`, `es`, `ed` or `ing` after the term, and a hyphen matching a space, which is how the
|
|
62
|
+
* same phrase is written in two documents by two people.
|
|
63
|
+
*/
|
|
64
|
+
export const termMatcher = (term) => new RegExp(
|
|
65
|
+
`\\b${term.replace(/[-]/g, "[- ]").replace(/\s+/g, "\\s+")}(?:e?s|ed|ing)?\\b`, "i");
|
|
66
|
+
|
|
52
67
|
/** Everything the run is ABOUT — the mark, its variants, the owners named. Never flagged. */
|
|
53
68
|
const ownTerms = (about = {}) => {
|
|
54
69
|
const out = [];
|
|
@@ -85,8 +100,7 @@ export function plainRegisterFlags(text, about = {}) {
|
|
|
85
100
|
|
|
86
101
|
const flags = [];
|
|
87
102
|
for (const [term, plain] of PLAIN_FORMS) {
|
|
88
|
-
|
|
89
|
-
if (!re.test(scan)) continue;
|
|
103
|
+
if (!termMatcher(term).test(scan)) continue;
|
|
90
104
|
flags.push({
|
|
91
105
|
kind: "vocabulary",
|
|
92
106
|
term,
|
|
@@ -49,6 +49,11 @@ import { readFlagSnapshot, engineFor, providersFor, postureDisagreement } from "
|
|
|
49
49
|
// reading is, because the question it was standing in for — does this still describe the box — now has
|
|
50
50
|
// a direct answer in `lastRun.disagrees`.
|
|
51
51
|
import { engineMode } from "./config-inventory.mjs"; // — the mode is DERIVED at read time, never stored
|
|
52
|
+
// THE ENGINE TABLE, READ FOR TWO WORDS. A row saying an engine cannot run has to name the program it
|
|
53
|
+
// could not find and the command that installs it, or the reader is told they have a problem and not
|
|
54
|
+
// what to do about it — and this table is already where the wizard and the run-door preflight read
|
|
55
|
+
// both of those, so naming them here adds no second description of an engine.
|
|
56
|
+
import { ENGINE_BINARIES } from "./driver.config.mjs";
|
|
52
57
|
|
|
53
58
|
/**
|
|
54
59
|
* The flag view.
|
|
@@ -60,6 +65,30 @@ import { engineMode } from "./config-inventory.mjs"; // — the mode is DERIVE
|
|
|
60
65
|
// One projection, used for whichever posture is the answer. Extracted when the live posture became that
|
|
61
66
|
// answer, so the LIVE reading and the LAST-RUN capture cannot be shaped differently and quietly invite a
|
|
62
67
|
// reader to compare two things that were built by two rules.
|
|
68
|
+
/**
|
|
69
|
+
* The engine block, plus the two words a reader needs when it cannot run.
|
|
70
|
+
*
|
|
71
|
+
* DERIVED FROM THE ID AT READ TIME, never stored in the capture. A capture written by an older build
|
|
72
|
+
* carries neither field, and a page that read them out of the capture would go quiet about the engine
|
|
73
|
+
* on exactly the deployments most likely to be misconfigured. The id is in every capture there has
|
|
74
|
+
* ever been, and the table is in this build.
|
|
75
|
+
*
|
|
76
|
+
* NULL FOR AN ENGINE THIS BUILD DOES NOT SHIP, which the row already has its own sentence for. Naming
|
|
77
|
+
* a program for an engine that does not exist here would be an invented fact.
|
|
78
|
+
*/
|
|
79
|
+
function withProgram(engine) {
|
|
80
|
+
if (!engine) return engine;
|
|
81
|
+
const spec = ENGINE_BINARIES[engine.id] ?? null;
|
|
82
|
+
return {
|
|
83
|
+
...engine,
|
|
84
|
+
// A BARE NAME, NEVER A RESOLVED PATH. `fallback` is what the table calls the program when nothing
|
|
85
|
+
// overrides it — "claude" — and it is what a reader types. The resolved path is this machine's
|
|
86
|
+
// layout and is deliberately kept out of anything a browser renders.
|
|
87
|
+
program: spec?.fallback ?? null,
|
|
88
|
+
install: spec?.install ?? null,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
63
92
|
function postureView(snap) {
|
|
64
93
|
return {
|
|
65
94
|
flags: Object.entries(snap.flags ?? {}).map(([name, f]) => ({
|
|
@@ -72,7 +101,7 @@ function postureView(snap) {
|
|
|
72
101
|
killSwitch: (snap.killSwitches ?? []).includes(name),
|
|
73
102
|
})),
|
|
74
103
|
built: snap.built ?? null,
|
|
75
|
-
engine: engineFor(snap),
|
|
104
|
+
engine: withProgram(engineFor(snap)),
|
|
76
105
|
engineMode: engineFor(snap) ? engineMode(engineFor(snap)) : null,
|
|
77
106
|
providers: providersFor(snap),
|
|
78
107
|
};
|
|
@@ -47,6 +47,7 @@ import { randomBytes, scryptSync, timingSafeEqual, createHmac } from "node:crypt
|
|
|
47
47
|
import { readFileSync, writeFileSync, mkdirSync, chmodSync } from "node:fs";
|
|
48
48
|
import { dirname, join } from "node:path";
|
|
49
49
|
import { homedir } from "node:os";
|
|
50
|
+
import { envPrefix } from "../shared/os-advice.mjs";
|
|
50
51
|
|
|
51
52
|
// ── the credential record ────────────────────────────────────────────────────────────────────────
|
|
52
53
|
//
|
|
@@ -164,7 +165,10 @@ export function passphraseResetCommand({ prefix = "", credentialPath = null, env
|
|
|
164
165
|
const base = `${prefix}clearotron passphrase --reset`;
|
|
165
166
|
const path = credentialPath ?? env.PORTAL_LOCAL_CREDENTIAL ?? null;
|
|
166
167
|
if (!path || path === credentialPathFor({}, home)) return base;
|
|
167
|
-
|
|
168
|
+
// `VAR=value cmd` IS POSIX-ONLY. PowerShell has no such juxtaposition — the assignment is its own
|
|
169
|
+
// statement there — so this line told a Windows reader their variable name was not a cmdlet, naming
|
|
170
|
+
// the wrong half of the command as the fault. Reported from a real run.
|
|
171
|
+
return `${envPrefix("PORTAL_LOCAL_CREDENTIAL", path)}${base}`;
|
|
168
172
|
}
|
|
169
173
|
|
|
170
174
|
/**
|
|
@@ -1333,6 +1333,48 @@ export function makePortalService({
|
|
|
1333
1333
|
};
|
|
1334
1334
|
const selectorOf = (body) => (body.recipeKey ? `recipe:${body.recipeKey}` : `product:${body.product || "the account's default"}`);
|
|
1335
1335
|
|
|
1336
|
+
// ── The engine program, read once, for every surface that reports it ───────────────────────────
|
|
1337
|
+
//
|
|
1338
|
+
// WHY /me NEEDS THIS AT ALL. The settings page can ask `/portal/admin/config`, which already computes
|
|
1339
|
+
// the live-versus-capture comparison. New clearance cannot: that route is staff-only, and the reader
|
|
1340
|
+
// who gets stuck here is as often a client. A screen that asked it would get a 404 and fall back to
|
|
1341
|
+
// the generic advice — which is the exact defect this exists to end, delivered to the exact reader
|
|
1342
|
+
// who reported it.
|
|
1343
|
+
//
|
|
1344
|
+
// WHAT IT COSTS ON A NORMAL PAGE VIEW: NOTHING, and that is structural rather than a promise. The
|
|
1345
|
+
// question only has a wrong answer in one state — the capture says there is no engine program — so
|
|
1346
|
+
// the live reading is taken only in that state. An install with a working engine never reaches the
|
|
1347
|
+
// probe, and /me is the hottest endpoint in the portal.
|
|
1348
|
+
//
|
|
1349
|
+
// AND IT IS THE SAME MECHANISM THE SETTINGS PAGE USES, deliberately: `flagView` with a live posture,
|
|
1350
|
+
// and the same `engine program` row out of `lastRun.disagrees`. Two surfaces answering this question
|
|
1351
|
+
// by two routes is how they came to contradict each other in the first place.
|
|
1352
|
+
//
|
|
1353
|
+
// NOT CACHED, AND THAT IS THE POINT OF THE CHANGE. A held reading was written first and removed: it
|
|
1354
|
+
// would have meant a reader who restarted the engine service went on being told to restart it for
|
|
1355
|
+
// as long as the window lasted — the staleness this whole change exists to end, in miniature, in
|
|
1356
|
+
// the one place a reader is standing when they act on it. Measured in testing, 2026-09-08: 0.28ms
|
|
1357
|
+
// for a reading taken cold with the imports warm, and a 0.29ms median over 50 requests in the state
|
|
1358
|
+
// that takes one on every request, against 0.04ms in the state that takes none. It is taken only
|
|
1359
|
+
// where a search is already refusing, so there is nothing here worth trading a wrong answer for.
|
|
1360
|
+
async function engineProgramDisputed(mode) {
|
|
1361
|
+
// NOT DEMO, NOTHING TO DISPUTE. The capture already sees the program, so the only disagreement
|
|
1362
|
+
// left is the mirror — this box cannot see what the engine could — and that one does not change
|
|
1363
|
+
// what this screen says: the screen is not refusing a search in that state.
|
|
1364
|
+
if (mode !== "demo") return false;
|
|
1365
|
+
try {
|
|
1366
|
+
const live = await livePosture();
|
|
1367
|
+
// `disagrees` is [] on agreement, rows on disagreement, and null when there is no capture to
|
|
1368
|
+
// compare against — three facts, and only the middle one is this. A null must not read as false.
|
|
1369
|
+
const rows = flagView(poolRoot, { live }).lastRun?.disagrees;
|
|
1370
|
+
return Array.isArray(rows) ? rows.some((d) => d.what === "engine program") : null;
|
|
1371
|
+
} catch {
|
|
1372
|
+
// COULD NOT LOOK, AND IT SAYS SO. Null travels to the screen as "unknown" and the screen prints
|
|
1373
|
+
// the general advice, which is what it printed before this existed.
|
|
1374
|
+
return null;
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1336
1378
|
async function route(method, path, identity, body = {}, query = {}) {
|
|
1337
1379
|
const principal = makePrincipal({ email: identity?.email, grants: grantsNow(), staffDomains });
|
|
1338
1380
|
const parts = path.replace(/\/+$/, "").split("/").filter(Boolean); // ["portal", ...]
|
|
@@ -1389,8 +1431,17 @@ export function makePortalService({
|
|
|
1389
1431
|
// writes at boot and derives the mode from `binaryPresent`, which that snapshot already
|
|
1390
1432
|
// carries. NULL when there is no snapshot to read, and null means THIS CANNOT ANSWER — the UI
|
|
1391
1433
|
// must leave the button alone rather than infer demo from an absent file.
|
|
1434
|
+
// READ ONCE. The payload names it and the program reading below is gated on it; two calls to
|
|
1435
|
+
// `flagView` here would be two reads of the same file that could disagree with each other.
|
|
1436
|
+
const meEngineMode = flagView(poolRoot).engineMode;
|
|
1392
1437
|
return { status: 200, json: { role: principal.role, email: principal.email, accounts: principal.accounts, accountNames,
|
|
1393
|
-
concurrentRuns: concurrentRunsCap(), brand: BRAND.name, engineMode:
|
|
1438
|
+
concurrentRuns: concurrentRunsCap(), brand: BRAND.name, engineMode: meEngineMode,
|
|
1439
|
+
// WHETHER THE PROGRAM IS ON THIS BOX WHILE THE ENGINE CANNOT SEE IT — true, false, or null
|
|
1440
|
+
// for "this could not be checked". The screen above renders one of three remedies from it,
|
|
1441
|
+
// and they are different remedies: install the CLI, restart the service that cannot see it,
|
|
1442
|
+
// or the general advice when nothing could be read. Only `engineMode: "demo"` can make this
|
|
1443
|
+
// anything but false; see `engineProgramDisputed`.
|
|
1444
|
+
engineProgramDisputed: await engineProgramDisputed(meEngineMode),
|
|
1394
1445
|
// HOW THIS INSTALL ARRIVED, so a screen can name the setup command the reader can actually
|
|
1395
1446
|
// type. `npm run setup` and `npx clearotron install` are the same wizard and each one is
|
|
1396
1447
|
// unrunnable on the other route; the no-engine notice named one of them and was wrong for
|
|
@@ -2142,7 +2193,7 @@ export function makePortalService({
|
|
|
2142
2193
|
// command is a true fact about THIS INSTALL'S OWN DISK, useful to anyone with a shell on the
|
|
2143
2194
|
// box and useless to a hosted client who has no checkout. On a local install the reader IS the
|
|
2144
2195
|
// operator, which is why the split that already exists does the work an "is this deployment
|
|
2145
|
-
// local" inference would have done badly. Agreed
|
|
2196
|
+
// local" inference would have done badly. Agreed before building, because it
|
|
2146
2197
|
// changes what a signed-in staff user is shown.
|
|
2147
2198
|
//
|
|
2148
2199
|
// COMPOSED IN ONE PLACE and handed over as a string. The browser cannot know this install's
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
// report-derived cover text alone, because a gate that simply stops running is not a gate that passes.
|
|
24
24
|
|
|
25
25
|
import { REGION_NAMES } from "./publish/regions.mjs";
|
|
26
|
+
import { PLAIN_FORMS, SENTENCE_WORD_LIMIT, termMatcher } from "./plain-register.mjs"; // the pinned rule, not a fourth copy
|
|
26
27
|
import { canonicalJurisdictionCode } from "./jurisdiction-codes.mjs"; // one spelling of a territory code
|
|
27
28
|
import { searchedCovers } from "./frame-diff-model.mjs"; // one copy of the EU-reach rule
|
|
28
29
|
import { partyFactSources, partyFactViolations, partyFactMessage, canJudgePartyFacts } from "./party-facts.mjs"; //
|
|
@@ -2375,26 +2376,25 @@ const knockoutSurfaces = (findings) => {
|
|
|
2375
2376
|
// FLAG-ONLY, AND NEVER PROJECTED. 333: "A hit is a rewrite of that line, never a disclosure and never a
|
|
2376
2377
|
// run failure." These carry surface "findings", which runKnockoutLint's caller does not project onto
|
|
2377
2378
|
// the cover note or the workbook — so a flag reaches whoever is fixing the run and nobody else.
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
const PLAIN_SENTENCE_WORDS = 25;
|
|
2379
|
+
// ── THE RULE IS NOT WRITTEN HERE ANY MORE ───────────────────────────────────────────────────────────
|
|
2380
|
+
//
|
|
2381
|
+
// This file carried its own vocabulary list and its own sentence limit, imported nothing, and was the
|
|
2382
|
+
// copy that actually ran on every knockout delivery — a fourth copy of a rule the other three are pinned
|
|
2383
|
+
// to each other by a test. It had already drifted: it flagged `senior right`, `limb` and `lane`, which
|
|
2384
|
+
// appear in no pinned copy, so a seat was corrected against a rule it was never taught; and it did not
|
|
2385
|
+
// flag `instructed`, which the doctrine does teach. It also carried no plain form at all, naming the
|
|
2386
|
+
// term and deferring the replacement to a skill — and the replacement is the half the doctrine calls
|
|
2387
|
+
// load-bearing.
|
|
2388
|
+
//
|
|
2389
|
+
// It reads the pinned source now. The three untaught terms go with it: teaching a term is a doctrine
|
|
2390
|
+
// change, and `PLAIN_FORMS` is pinned to both documents by
|
|
2391
|
+
// `the-two-register-rule-says-the-same-thing-to-both-products.test.mjs`, so a term added here without
|
|
2392
|
+
// its worked swap in both would red that test rather than silently widening what a seat is corrected on.
|
|
2393
|
+
//
|
|
2394
|
+
// WHAT WAS NOT LOST IN THE CONSOLIDATION. The patterns here handled inflections and the pinned source
|
|
2395
|
+
// did not — `proprietors`, `prevailing`. Reading terms from the weaker matcher would have narrowed the
|
|
2396
|
+
// live check while looking like a tidy-up, so the inflections moved INTO the pinned source as
|
|
2397
|
+
// `termMatcher`, and both sides use it.
|
|
2398
2398
|
|
|
2399
2399
|
/**
|
|
2400
2400
|
* The fields a reader of the knockout meets before opening anything, named one by one rather than
|
|
@@ -2424,15 +2424,39 @@ function knockoutVisibleProse(findings) {
|
|
|
2424
2424
|
* sentence carrying more than one idea. Each names the field, so a hit is a line somebody can rewrite
|
|
2425
2425
|
* rather than a score.
|
|
2426
2426
|
*/
|
|
2427
|
-
|
|
2427
|
+
/**
|
|
2428
|
+
* The checks that are internal BY DESIGN, named once so the rule lives beside the thing it governs.
|
|
2429
|
+
*
|
|
2430
|
+
* A hit on any of these is a line for whoever is fixing the run to rewrite. It is never something a
|
|
2431
|
+
* client is shown and it never fails a run — see the header above and the doctrine these implement.
|
|
2432
|
+
* Every other check on this lane is relabelled `report` as it is pushed, one line at a time, and these
|
|
2433
|
+
* three sit in the middle of that list: making them projectable is a one-line edit that looks exactly
|
|
2434
|
+
* like its neighbours and that no test would have caught, because the arms enumerating failure ids
|
|
2435
|
+
* filter these three out before asserting — correctly, for what those arms check.
|
|
2436
|
+
*/
|
|
2437
|
+
export const NEVER_PROJECTED = Object.freeze(new Set([
|
|
2438
|
+
"reviewer-note-subject", "plain-language-vocabulary", "plain-language-sentence-length",
|
|
2439
|
+
]));
|
|
2440
|
+
|
|
2441
|
+
/** The surface a check that must never reach a reader carries. Not `report`, and not a caller's choice. */
|
|
2442
|
+
export const INTERNAL_SURFACE = "findings";
|
|
2443
|
+
|
|
2444
|
+
/**
|
|
2445
|
+
* THE SURFACE IS NOT THE CALLER'S TO CHOOSE. It used to be a parameter with a default, so the property
|
|
2446
|
+
* held because every caller happened to pass nothing — a run that satisfies a rule rather than a rule.
|
|
2447
|
+
* A future caller passing `report` would have put a plain-language hit on the page a client reads, with
|
|
2448
|
+
* nothing going red.
|
|
2449
|
+
*/
|
|
2450
|
+
export function plainLanguageChecks({ findings } = {}) {
|
|
2451
|
+
const surface = INTERNAL_SURFACE;
|
|
2428
2452
|
const fields = knockoutVisibleProse(findings);
|
|
2429
2453
|
const vocab = [];
|
|
2430
2454
|
const longSentences = [];
|
|
2431
2455
|
for (const { where, text } of fields) {
|
|
2432
|
-
for (const [
|
|
2456
|
+
for (const [term] of PLAIN_FORMS) if (termMatcher(term).test(text)) vocab.push(`${where}: "${term}"`);
|
|
2433
2457
|
for (const sentence of text.split(/(?<=[.!?])\s+|\n+/)) {
|
|
2434
2458
|
const n = sentence.trim().split(/\s+/).filter(Boolean).length;
|
|
2435
|
-
if (n >
|
|
2459
|
+
if (n > SENTENCE_WORD_LIMIT) longSentences.push(`${where}: ${n} words`);
|
|
2436
2460
|
}
|
|
2437
2461
|
}
|
|
2438
2462
|
// ── A NOTE THAT WILL PRINT IN THE PLACE ITS WRITER DID NOT MEAN ────────────────────────────────────
|
|
@@ -2465,7 +2489,7 @@ export function plainLanguageChecks({ findings, surface = "findings" } = {}) {
|
|
|
2465
2489
|
check("plain-language-vocabulary", "voice", surface, vocab.length === 0,
|
|
2466
2490
|
vocab.length ? `the lawyer's vocabulary on lines a reader meets before opening anything — rewrite the line in the words the reader already owns (the skill carries the swaps): ${say(vocab)}` : ""),
|
|
2467
2491
|
check("plain-language-sentence-length", "voice", surface, longSentences.length === 0,
|
|
2468
|
-
longSentences.length ? `a default-visible sentence carrying more than one idea (over ${
|
|
2492
|
+
longSentences.length ? `a default-visible sentence carrying more than one idea (over ${SENTENCE_WORD_LIMIT} words) — split it, conclusion first: ${say(longSentences)}` : ""),
|
|
2469
2493
|
];
|
|
2470
2494
|
}
|
|
2471
2495
|
|
|
@@ -2529,7 +2553,12 @@ export function runKnockoutLint({ findings }) {
|
|
|
2529
2553
|
// "all", because on the clearance lane it asserts agreement BETWEEN surfaces. This lane has one
|
|
2530
2554
|
// surface, so "report" is also the truer label here. The filter is deliberately not `!== "findings"`:
|
|
2531
2555
|
// that would default a check added next year to PROJECTING onto a surface a client principal reads.
|
|
2532
|
-
|
|
2556
|
+
// AND IT REFUSES TO PROMOTE THE THREE THAT ARE INTERNAL BY DESIGN. Pinning the surface inside
|
|
2557
|
+
// `plainLanguageChecks` stops a caller ASKING for a projectable one; it does not stop this line
|
|
2558
|
+
// relabelling the answer afterwards, which is the same edit one step later and looks like every other
|
|
2559
|
+
// line around it. So the rule is enforced where the surface is actually chosen. Anything added to
|
|
2560
|
+
// NEVER_PROJECTED is covered here by construction rather than by whoever adds it remembering.
|
|
2561
|
+
const onReport = (list) => list.map((c) => (NEVER_PROJECTED.has(c.id) ? c : { ...c, surface: "report" }));
|
|
2533
2562
|
checks.push(...permissionProseChecks({ text: report, surface: "report", idSuffix: ":knockout", structural: true, cards: false }));
|
|
2534
2563
|
checks.push(...onReport(scopeNumberProseChecks({ reportMd: report })));
|
|
2535
2564
|
checks.push(...onReport(countingChecks({ report })));
|
|
@@ -416,7 +416,7 @@ const useEvidence = (m) => [USE_EVIDENCE_LABEL[m?._status], USE_SOURCE_LABEL[m?.
|
|
|
416
416
|
// EXACT EQUALITY, exactly as EVIDENCE_LABEL maps `_status`. The sentinel itself does not move: archived
|
|
417
417
|
// runs carry the old value forever and a fourth spelling of it would have to be accepted everywhere.
|
|
418
418
|
const USE_CHECK_NO_RESULT = 'perplexity_research — no result';
|
|
419
|
-
const USE_CHECK_NO_RESULT_CITE = '
|
|
419
|
+
const USE_CHECK_NO_RESULT_CITE = 'Nothing found in the marketplaces searched.';
|
|
420
420
|
const USE_CHECK_NO_RESULT_SHORT = 'marketplace search — no result found';
|
|
421
421
|
// — MATCHED ON NORMALISED PUNCTUATION, NOT ONE SPELLING. The constant itself does not
|
|
422
422
|
// move (archived runs carry it forever, the validators name it), but the SEAT emitted a hyphen where
|
|
@@ -852,7 +852,7 @@ function plainScopeNote(text) {
|
|
|
852
852
|
if (!t) return '';
|
|
853
853
|
return stripTelemetry(t).trim(); // trim: an all-telemetry note leaves only newlines, and '' is falsy
|
|
854
854
|
}
|
|
855
|
-
function scopeSection(ranBucket, coverage, coverageJudgment, methodologyText, contextNotes, fm = {}, hasRecordSet = false, hasCards = false) {
|
|
855
|
+
function scopeSection(ranBucket, coverage, coverageJudgment, methodologyText, contextNotes, fm = {}, hasRecordSet = false, hasCards = false, hasIndexEntry = false) {
|
|
856
856
|
const parts = [];
|
|
857
857
|
// B3 (spec 2026-07-30 §4) — record provenance, stated ONCE, here, instead of a hedge stamped on
|
|
858
858
|
// every card. This is the single home of what "fetched", "register-index entry" and "inferred"
|
|
@@ -868,7 +868,11 @@ function scopeSection(ranBucket, coverage, coverageJudgment, methodologyText, co
|
|
|
868
868
|
// conditions: a copy is a thing that drifts, and over-including costs one explanatory paragraph in a
|
|
869
869
|
// collapsed section while under-including costs a reader an unexplained label. The fetched-records
|
|
870
870
|
// sentence stays conditional on hasRecordSet, so a run WITH a record set is byte-identical to B3.
|
|
871
|
-
|
|
871
|
+
// THREE SENTENCES EXPLAINING ONE WORD, and the middle one printed on every report whether or not the
|
|
872
|
+
// page had a register-index entry on it — a definition of a label the reader could not see. It renders
|
|
873
|
+
// now only where such an entry does, and the remaining two say what "inferred" means in the words a
|
|
874
|
+
// reader would use for it rather than in the renderer's.
|
|
875
|
+
if (hasRecordSet || hasCards) parts.push(`<p class="scoperead" style="margin:0 0 4px;font-weight:600">Record provenance</p><p class="provnote" style="margin:0 0 6px;font-size:13px">${hasRecordSet ? 'Registration numbers on the cards were read from the register records. ' : ''}${hasIndexEntry ? 'A registration shown as a register-index entry was seen in the register index; its full record was not pulled. ' : ''}“Inferred” beside an owner’s likelihood to object means we judged it from what the owner sells and holds; we had no enforcement history to read.</p>`);
|
|
872
876
|
// — this is the one part of §4 that does NOT fold. Same markup, same heading, same marker; it is
|
|
873
877
|
// emitted beside the <details> instead of inside it, wrapped in the panel the only-you section already
|
|
874
878
|
// uses so it reads as a region of the page rather than a stray heading.
|
|
@@ -896,7 +900,11 @@ function scopeSection(ranBucket, coverage, coverageJudgment, methodologyText, co
|
|
|
896
900
|
+ `<p class="covnone" style="margin:0 0 6px;font-size:13px">No coverage record was produced for this run. `
|
|
897
901
|
+ `This section normally lists what each search covered and what is still open; its absence here is a gap `
|
|
898
902
|
+ `in the record, not a finding that nothing is open. Ask us before relying on it.</p>`);
|
|
899
|
-
|
|
903
|
+
// THE INTERNAL COVERAGE READ IS NOT RENDERED. It concatenated the engine's own search-unit names into
|
|
904
|
+
// about a thousand characters of prose — and on the measured run it ended mid-word, because it is a
|
|
905
|
+
// machine's working note and nothing was reading it as a sentence. Every fact in it is already in the
|
|
906
|
+
// coverage cells directly above, in plain words. It stays in the run's artifacts and in the workbook,
|
|
907
|
+
// where the reader is someone who wants it.
|
|
900
908
|
const meth = plainScopeNote(methodologyText);
|
|
901
909
|
if (meth) parts.push(`<p class="scoperead" style="margin:10px 0 4px;font-weight:600">How this search was run</p><div class="methnote" style="font-size:13px">${renderProse(meth)}</div>`);
|
|
902
910
|
const cn = contextNotesBlock(contextNotes);
|
|
@@ -1230,11 +1238,16 @@ function fullDetail(f, card, recordsByUri = new Map()) {
|
|
|
1230
1238
|
// ("Download full audit (Excel)"), because that is the string the reader hunts for on
|
|
1231
1239
|
// the page. NOT an inline .xlsx link: portal-report.mjs strips those, correctly — the
|
|
1232
1240
|
// portal REPLACES them with its own download control (portal-ui Result.tsx).
|
|
1233
|
-
// placeholder — a register UI exists and we do not know its per-record address.
|
|
1234
|
-
//
|
|
1241
|
+
// placeholder — a register UI exists and we do not know its per-record address. It CARRIES NO NOTE:
|
|
1242
|
+
// the number stands on its own, because "(placeholder)" beside twenty-four
|
|
1243
|
+
// registrations reads to a client as a broken report rather than as a missing link.
|
|
1244
|
+
// THE PLACEHOLDER NOTE IS GONE. It printed " — no record link available yet (placeholder)" beside
|
|
1245
|
+
// every registration a register UI has no per-record address for — twenty-four times on the measured
|
|
1246
|
+
// page — and a client reads "placeholder" as a broken report. The number is the fact; when there is a
|
|
1247
|
+
// link the number IS the link, and when there is not, the number still stands on its own. The
|
|
1248
|
+
// workbook note stays: it tells a reader where the full record actually is.
|
|
1235
1249
|
const NO_LINK_NOTE = {
|
|
1236
1250
|
workbook: ' — full record in the audit workbook (“Download full audit (Excel)”)',
|
|
1237
|
-
placeholder: ' — no record link available yet (placeholder)',
|
|
1238
1251
|
};
|
|
1239
1252
|
const regUri = (u, fb) => {
|
|
1240
1253
|
const h = regHref(u);
|
|
@@ -1350,7 +1363,11 @@ function fullDetail(f, card, recordsByUri = new Map()) {
|
|
|
1350
1363
|
// D7 — the code-owned "searched, nothing found" sentinel becomes client words HERE, by exact
|
|
1351
1364
|
// equality against the one constant. Any other value is a source string and rides through untouched.
|
|
1352
1365
|
const useSrc = isUseCheckNoResult(f.use_check?.source) ? USE_CHECK_NO_RESULT_CITE : f.use_check?.source;
|
|
1353
|
-
|
|
1366
|
+
// NO EVIDENCE TAG ON AN EMPTY RESULT. The line read "Use checked. Marketplace search run — no result
|
|
1367
|
+
// found. Evidence: inferred", and "inferred" beside "no result" reads as a contradiction: it qualifies
|
|
1368
|
+
// how a FINDING was established, and there is no finding here. Nothing was found, and that is the
|
|
1369
|
+
// whole of what the line has to say.
|
|
1370
|
+
const useChk = cite(useSrc, 'Use checked.', isUseCheckNoResult(f.use_check?.source) ? null : useStatus);
|
|
1354
1371
|
const ownR = cite(f.own_rights?.source, 'Own-portfolio sweep.', EVIDENCE_LABEL[f.own_rights?._status]);
|
|
1355
1372
|
const proseFull = cardBlock(card, /^full detail/i);
|
|
1356
1373
|
const proseFullShown = proseFull; // one report: the full prose; portal-report strips serve-time chrome, never analysis
|
|
@@ -1627,7 +1644,67 @@ const COV_STATE = {
|
|
|
1627
1644
|
'not-searched': { cls: 'todo', ic: '→', word: 'Not run this run' },
|
|
1628
1645
|
note: { cls: 'info', ic: 'i', word: 'Note' },
|
|
1629
1646
|
};
|
|
1647
|
+
/**
|
|
1648
|
+
* ONE ROW PER GAP, where the driver's follow-up row and the model's own row are the same search.
|
|
1649
|
+
*
|
|
1650
|
+
* A run deferred the English word DOLPHIN and the page said so twice: once as the model wrote it — "the
|
|
1651
|
+
* English word DOLPHIN as a dedicated exact search · Open item" — and once as the driver composes it
|
|
1652
|
+
* from the envelope, "Follow-up / dolphin · Open item: dolphin — not completed this run — the search for
|
|
1653
|
+
* it was planned and never reached the register…". The run's own reviewer flagged the duplicate and it
|
|
1654
|
+
* shipped anyway, because the second row is composed HERE and the reviewer reads what the model wrote.
|
|
1655
|
+
*
|
|
1656
|
+
* RENDER-SIDE ONLY. Both rows stay in the record and in the workbook; this decides what the page draws.
|
|
1657
|
+
* Dropping the driver's row from `coverage[]` would change what the run recorded, and this issue is
|
|
1658
|
+
* presentation.
|
|
1659
|
+
*
|
|
1660
|
+
* IT ERRS TOWARD KEEPING BOTH. A surplus row is today's behaviour; a wrongly-suppressed one hides a gap
|
|
1661
|
+
* from the reader, which is the failure worth avoiding. So the driver's row goes only when another row
|
|
1662
|
+
* carries EVERY significant word of the directive it names — a near-match keeps both.
|
|
1663
|
+
*/
|
|
1664
|
+
const FOLLOW_UP_PREFIX = 'Follow-up / ';
|
|
1665
|
+
const COV_STOPWORDS = new Set(['the', 'a', 'an', 'as', 'for', 'of', 'in', 'on', 'and', 'or', 'to', 'is',
|
|
1666
|
+
'was', 'it', 'its', 'this', 'that', 'with', 'by', 'at', 'be', 'been', 'run', 'search', 'searched']);
|
|
1667
|
+
const covWords = (t) => new Set(String(t || '').toLowerCase().match(/[a-z0-9]+/g)?.filter((w) => !COV_STOPWORDS.has(w)) ?? []);
|
|
1668
|
+
|
|
1669
|
+
function dedupeFollowUps(coverage) {
|
|
1670
|
+
const composed = (c) => String(c?.area || '').startsWith(FOLLOW_UP_PREFIX);
|
|
1671
|
+
const written = coverage.filter((c) => !composed(c));
|
|
1672
|
+
if (!written.length) return coverage;
|
|
1673
|
+
return coverage.filter((c) => {
|
|
1674
|
+
if (!composed(c)) return true;
|
|
1675
|
+
// The directive is the note's opening clause — the same string the area was clipped from, unclipped.
|
|
1676
|
+
const directive = covWords(String(c.note || '').split('—')[0]);
|
|
1677
|
+
if (!directive.size) return true;
|
|
1678
|
+
// A COMPLETED SEARCH NEVER STANDS IN FOR AN UNCOMPLETED ONE. This is the discriminator the first
|
|
1679
|
+
// version lacked, and it hid a real gap from a client on a delivered page.
|
|
1680
|
+
//
|
|
1681
|
+
// The composed row exists to say a planned search never ran. Suppressing it requires another row
|
|
1682
|
+
// saying THE SAME THING about the same search — so the row that suppresses must itself be open. A
|
|
1683
|
+
// row reporting a search that RAN is reporting the opposite, and the two are not interchangeable
|
|
1684
|
+
// however many words they share.
|
|
1685
|
+
//
|
|
1686
|
+
// Found on a real run, driven on the shipped renderer: a completed saturation probe into
|
|
1687
|
+
// third-party dolphin-word rights suppressed the uncompleted exact-word register search for DOLPHIN,
|
|
1688
|
+
// because both areas contain "dolphin". The client then read a coverage section that mentioned the
|
|
1689
|
+
// word and disclosed nothing left undone, and the suppressed row was the page's only disclosure of
|
|
1690
|
+
// it. That is exactly the failure this function's header calls the one worth avoiding, and the
|
|
1691
|
+
// header was right while the code was not.
|
|
1692
|
+
//
|
|
1693
|
+
// WHAT THIS STILL CANNOT DO, said rather than implied: it is word containment, so two genuinely
|
|
1694
|
+
// different OPEN searches sharing every word of a short directive would still collapse to one. The
|
|
1695
|
+
// discriminator a reader would want is which search a row names, and the rows carry no identity to
|
|
1696
|
+
// join on — that is why the issue's own wording is "name the same search" rather than a rule. The
|
|
1697
|
+
// narrowing here is the strongest one the data supports, and it errs toward keeping both.
|
|
1698
|
+
return !written.some((w) => {
|
|
1699
|
+
if (COV_STATE[w?.state]?.cls === 'ok') return false; // a searched-and-clean row reports the opposite
|
|
1700
|
+
const theirs = covWords(w.area);
|
|
1701
|
+
return [...directive].every((word) => theirs.has(word));
|
|
1702
|
+
});
|
|
1703
|
+
});
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1630
1706
|
function coverageGrid(coverage) {
|
|
1707
|
+
coverage = dedupeFollowUps(coverage);
|
|
1631
1708
|
if (!coverage.length) return '';
|
|
1632
1709
|
const cell = (c) => {
|
|
1633
1710
|
const s = COV_STATE[c.state] || COV_STATE.note;
|
|
@@ -1753,7 +1830,7 @@ document.addEventListener('keydown',function(e){if(e.key==='Escape'){var pop=doc
|
|
|
1753
1830
|
function markAssessmentBlock(ma) {
|
|
1754
1831
|
if (ma == null) return '';
|
|
1755
1832
|
const structured = typeof ma.distinctiveness === 'object' || typeof ma.connotation === 'object';
|
|
1756
|
-
const SEC = `<div class="sec"><span class="num">✦</span><h2>The mark itself</h2><span class="note">
|
|
1833
|
+
const SEC = `<div class="sec"><span class="num">✦</span><h2>The mark itself</h2><span class="note">how strong the name is on its own</span></div>`;
|
|
1757
1834
|
if (!structured) {
|
|
1758
1835
|
const dist = String(ma?.distinctiveness ?? '').trim(), conn = String(ma?.connotation ?? '').trim();
|
|
1759
1836
|
if (!dist && !conn) return '';
|
|
@@ -2130,7 +2207,7 @@ export function renderHtml(parsed, findings = [], coverage = [], opts = {}) {
|
|
|
2130
2207
|
const clNotice = (clNoticeText && CASE_LAW_BY_ORD.size)
|
|
2131
2208
|
? `<div class="panel" style="padding:12px 16px;margin:0 0 12px"><p style="margin:0 0 4px;font-weight:700;font-size:13px">Session-wide notice</p><div style="font-size:13px">${renderProse(clNoticeText)}</div></div>`
|
|
2132
2209
|
: '';
|
|
2133
|
-
const CL_SEC = (n) => `<div class="sec" id="common-law"><span class="num">${n}</span><h2>Common-law & marketplace</h2><span class="note">
|
|
2210
|
+
const CL_SEC = (n) => `<div class="sec" id="common-law"><span class="num">${n}</span><h2>Common-law & marketplace</h2><span class="note">who is using similar names, registered or not</span></div>
|
|
2134
2211
|
${clNotice}${clBody}`;
|
|
2135
2212
|
const hasCL = Boolean(clBody || clNotice);
|
|
2136
2213
|
let findingsSections, covNum;
|
|
@@ -2269,7 +2346,7 @@ ${opts.nav || ''}
|
|
|
2269
2346
|
${ruledOutSection(ruledOut, recordsByUri)}
|
|
2270
2347
|
|
|
2271
2348
|
<!-- doc-52 §3 WHAT ONLY YOU CAN CLOSE — forward decisions, plain English, after the findings. -->
|
|
2272
|
-
${buckets.you ? `<div class="sec" id="only-you"><span class="num">✔</span><h2>What only you can close</h2><span class="note">
|
|
2349
|
+
${buckets.you ? `<div class="sec" id="only-you"><span class="num">✔</span><h2>What only you can close</h2><span class="note">decisions that need you</span></div>
|
|
2273
2350
|
<div class="panel actions"><div class="actgrp act-you">${renderProse(buckets.you.body)
|
|
2274
2351
|
.replace(/\[Time-critical\]\s*/gi, '<span class="src cl" style="margin-right:6px">Time-critical</span> ')
|
|
2275
2352
|
.replace(/\[Open question\]\s*/gi, '<span class="src" style="margin-right:6px">Open question</span> ')
|
|
@@ -2287,14 +2364,32 @@ ${opts.nav || ''}
|
|
|
2287
2364
|
|
|
2288
2365
|
<!-- doc-52 §4 SCOPE & WHAT WE DIDN'T SEARCH — one collapsible section, last; replaces "Checks we ran"
|
|
2289
2366
|
+ "Methodology" + the coverage grid. Nothing here leads. -->
|
|
2290
|
-
${scopeSection(buckets.ran, coverage, opts.coverageJudgment, secs['Methodology'], DISPOSITION_MODE ? [] : contextNotes, fm, recordsByUri.size > 0, findings.length > 0
|
|
2367
|
+
${scopeSection(buckets.ran, coverage, opts.coverageJudgment, secs['Methodology'], DISPOSITION_MODE ? [] : contextNotes, fm, recordsByUri.size > 0, findings.length > 0,
|
|
2368
|
+
// WHETHER A REGISTER-INDEX ENTRY IS ACTUALLY ON THIS PAGE, mirroring the registration render's own
|
|
2369
|
+
// second disjunct rather than restating it loosely: a cited registration with no fetched body, which
|
|
2370
|
+
// is the state that draws the "(register-index entry)" label. The provenance paragraph explains that
|
|
2371
|
+
// label, so it renders where the label can and stays off every page where it cannot.
|
|
2372
|
+
findings.some((f) => (f?.owner?.registrations ?? []).some((r) => r?.uri
|
|
2373
|
+
&& (recordsByUri.size > 0 || !(r.status || r.filed || r.expiry || (r.classes && r.classes.length))))))}
|
|
2291
2374
|
|
|
2292
2375
|
${askAiHtml}
|
|
2293
2376
|
|
|
2294
2377
|
<footer>
|
|
2295
2378
|
<span>${productName ? `${esc(productName)}. ` : ''}${FRAMEWORK
|
|
2296
|
-
|
|
2297
|
-
|
|
2379
|
+
// TWO SENTENCES, and that count is the ruled shape rather than a consequence of trimming.
|
|
2380
|
+
//
|
|
2381
|
+
// Two things left. The band note — "the framework in force's own vocabulary, one word per
|
|
2382
|
+
// finding on every surface" — is a note about how the renderer works, printed on every report a
|
|
2383
|
+
// client receives; the framework's NAME is on the "Rated under" line below, once, which is
|
|
2384
|
+
// where a reader who wants it will look.
|
|
2385
|
+
//
|
|
2386
|
+
// AND "Working draft for legal review.", which is a separate decision and is recorded as one.
|
|
2387
|
+
// A delivered clearance is not a draft, and a document that calls itself one on every page is
|
|
2388
|
+
// describing its own status inaccurately to the person paying for it. Raised in review because
|
|
2389
|
+
// the first version of this comment argued only the band note and left the reader to infer that
|
|
2390
|
+
// the status line had gone along for the ride.
|
|
2391
|
+
? `Risk bands: <span class="mono">${esc(FRAMEWORK.bands.map(b => b.label).join(' / '))}</span>. Purple notes are for the reviewing lawyer and are removed on export.`
|
|
2392
|
+
: 'Risk bands: <span class="mono">LOW / MANAGEABLE / MEDIUM / HIGH / VERY HIGH</span>. Purple notes are for the reviewing lawyer and are removed on export.'}<br>Matter ${esc(fm.matter || '')}${fm.run ? ` · ${esc(fm.run)}` : ''}.${fm.rated_under ? `<br>Rated under: <span class="mono">${esc(fm.rated_under)}</span>.` : ''}${fm.run_under_project ? `<br>Run under project: <span class="mono">${esc(fm.run_under_project)}</span>.` : ''}</span>
|
|
2298
2393
|
${logoLockup({ mark: 16 })}
|
|
2299
2394
|
</footer>
|
|
2300
2395
|
</div>
|
package/driver/search-policy.mjs
CHANGED
|
@@ -207,7 +207,7 @@ export const PRODUCT_POLICIES = {
|
|
|
207
207
|
// 199 of those same 420 carry one. The driver now reads this row and can cut on that tier
|
|
208
208
|
// (doubt-selection.mjs).
|
|
209
209
|
//
|
|
210
|
-
//, owner-ruled 2026-08-23, product by product (reached this branch
|
|
210
|
+
//, owner-ruled 2026-08-23, product by product (reached this branch on
|
|
211
211
|
// that thread, not as a comment from the owner's own hand — the box has one `gh` identity).
|
|
212
212
|
// It is the P2 column of that table's `placement-inquiry trace` row — the row whose typed key is
|
|
213
213
|
// the placement tier. This stage's OWN row reads `bands 1+2 / band 1` on FINDING CLASS, which is
|
package/driver/stage-context.mjs
CHANGED
|
@@ -369,6 +369,19 @@ export const DISPATCH_EXTRAS = [
|
|
|
369
369
|
{ path: join(P.runDir, "_records"), dir: true, why: "assembleRunRecords — the fetched official records each claimed field is checked against; without them the check finds zero mismatches and the block silently vanishes" },
|
|
370
370
|
],
|
|
371
371
|
},
|
|
372
|
+
{
|
|
373
|
+
// Rule 1 of the two-register rule, measured over the record's default-visible lines and handed to
|
|
374
|
+
// the pass that already rewrites. Advisory by construction: it reaches the writer, never the reader.
|
|
375
|
+
//
|
|
376
|
+
// It reads the RECORD rather than the narrative, and that is the point — the lines a client meets
|
|
377
|
+
// first are typed fields (each conflict's one sentence, the coverage notes, the actions, the mark
|
|
378
|
+
// assessment's two reads), not prose to be re-parsed out of a document. A sandbox without this file
|
|
379
|
+
// composes no block, which is correct and visible: the composer stamps what it built.
|
|
380
|
+
id: "refute-plain-register", stage: "narrative-refutation",
|
|
381
|
+
reads: (P) => [
|
|
382
|
+
{ path: P.findings, why: "plainRegisterExtra reads the typed default-visible fields — net, coverage notes, actions, mark assessment — and the owners it must blank before reading" },
|
|
383
|
+
],
|
|
384
|
+
},
|
|
372
385
|
// ── — synthesis's two, and the asymmetry they close ─────────────────────────────────────────
|
|
373
386
|
//
|
|
374
387
|
// The reviewer received the plan-execution receipt as a code-derived table; the stage it reviews
|