clearotron 0.3.2-beta.11 → 0.3.2-beta.13
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 +1 -1
- package/CONTRIBUTING.md +6 -5
- package/INSTALL.md +3 -4
- package/README.md +2 -1
- package/bin/clearotron.mjs +14 -0
- package/bin/connect.mjs +68 -3
- package/bin/example.mjs +12 -1
- package/bin/key.mjs +6 -1
- package/bin/onboard.mjs +23 -3
- package/bin/passphrase.mjs +4 -2
- package/bin/start.mjs +18 -6
- package/build-info.json +2 -2
- package/docs/CLIENT-MCP.md +6 -6
- package/docs/DELIVERY.md +3 -3
- package/docs/ONBOARDING.md +1 -1
- package/docs/PORTAL.md +3 -3
- package/docs/RELEASES.md +1 -1
- package/docs/SECURITY.md +1 -1
- package/docs/architecture/04-configuration-reference.md +4 -4
- package/docs/architecture/05-config-governance.md +10 -10
- package/docs/architecture/06-operations-runbook.md +3 -3
- package/docs/architecture/07-quality-and-audit.md +1 -1
- package/docs/architecture/08-development-guide.md +2 -2
- package/docs/architecture/09-security-and-data.md +1 -1
- package/docs/decisions/0002-no-dark-functionality.md +1 -1
- package/docs/decisions/0006-what-the-public-repository-carries.md +3 -3
- package/driver/CHANGELOG.md +29 -0
- package/driver/ask-ledger.mjs +1 -1
- package/driver/band-shape.mjs +1 -1
- package/driver/bundled-demos.mjs +2 -2
- package/driver/card-budget.mjs +2 -2
- package/driver/case-law-ledger.mjs +2 -2
- package/driver/connotation-search.mjs +5 -5
- package/driver/contract-e3-backlog.mjs +13 -13
- package/driver/contract-vocabulary.mjs +5 -5
- package/driver/coverage-form.mjs +2 -2
- package/driver/demo-container.mjs +26 -2
- package/driver/disposition-tool.mjs +2 -2
- package/driver/engine/CONTRACT.md +3 -3
- package/driver/engine/mcp/gather-config.mjs +1 -1
- package/driver/engine/mcp/recording-server.mjs +1 -1
- package/driver/engine/mcp/supplemental.mjs +22 -5
- package/driver/engine/openai-agent.mjs +2 -2
- package/driver/findings-model.mjs +21 -6
- package/driver/gateway.mjs +1 -1
- package/driver/knockout-next-step.mjs +72 -0
- package/driver/named-band.mjs +1 -1
- package/driver/package.json +1 -1
- package/driver/pipeline-knockout.mjs +27 -1
- package/driver/pipeline.mjs +4 -4
- package/driver/placement-union.mjs +1 -1
- package/driver/portal-mcp-client.mjs +1 -1
- package/driver/portal-report.mjs +25 -3
- package/driver/portal-service.mjs +25 -13
- package/driver/predelivery-lint.mjs +9 -4
- package/driver/progress.mjs +37 -1
- package/driver/publish/render-knockout.mjs +14 -9
- package/driver/publish/render.mjs +6 -6
- package/driver/record-discard.mjs +1 -1
- package/driver/register-availability.mjs +1 -1
- package/driver/register-count.mjs +1 -1
- package/driver/register-digest-record.mjs +1 -1
- package/driver/register-plan.mjs +81 -11
- package/driver/report-card-record.mjs +2 -2
- package/driver/result-noun-fields.mjs +3 -1
- package/driver/roster-verdict.mjs +2 -2
- package/driver/search-policy.mjs +1 -1
- package/driver/skeptic-record.mjs +1 -1
- package/driver/stages-knockout.mjs +1 -1
- package/driver/stages.mjs +1 -1
- package/driver/suite-census.json +72 -24
- package/driver/systemd/README.md +1 -1
- package/driver/unit-inventory.mjs +2 -2
- package/driver/verify-knockout.mjs +0 -27
- package/driver/verify.mjs +1 -1
- package/mcp-server/CHANGELOG.md +8 -0
- package/mcp-server/CONNECT.md +8 -8
- package/mcp-server/lib/runs.mjs +1 -1
- package/mcp-server/package.json +1 -1
- package/mcp-server/serve.mjs +27 -0
- package/package.json +1 -1
- package/portal-ui/dist/assets/{index-CtvwLCti.css → index-7Lq-dXDV.css} +12 -9
- package/portal-ui/dist/assets/{index-DXSRxPV_.js → index-w8GFZftk.js} +110 -69
- package/portal-ui/dist/index.html +2 -2
- package/portal-ui/package.json +1 -1
- package/providers/free-tier/src/capabilities.js +2 -2
- package/providers/jx/src/core.js +3 -3
- package/providers/jx/src/turn-envelope.mjs +1 -1
- package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/providers/perplexity/README.md +1 -1
- package/providers/signa/src/capabilities.js +2 -2
- package/providers/signa/src/core.js +2 -2
- package/providers/uspto-local/src/core.js +1 -1
- package/providers/uspto-local/src/sync.js +1 -1
- package/scripts/README.md +2 -6
- package/scripts/ask-ai-render-check.mjs +26 -1
- package/scripts/citation-anchor-report.mjs +1 -1
- package/scripts/citation-line-check.mjs +3 -3
- package/scripts/dead-names.mjs +17 -16
- package/scripts/e2e.mjs +1 -1
- package/scripts/env-audit.mjs +7 -1
- package/scripts/env-classify.mjs +1 -1
- package/scripts/pack-publishable.mjs +1 -1
- package/scripts/release-artifact-seal.mjs +2 -2
- package/scripts/repo-writes.mjs +42 -0
- package/scripts/report-sections-render-check.mjs +245 -0
- package/scripts/report-theme-render-check.mjs +31 -3
- package/scripts/settings-render-check.mjs +15 -8
- package/scripts/strip-tracker-citations.mjs +4 -4
- package/scripts/test-run.mjs +108 -6
- package/shared/browser-temp-root.mjs +10 -3
- package/shared/client-door.mjs +38 -6
- package/shared/connect-clients.mjs +2 -0
- package/shared/identifier-scan.mjs +2 -2
- package/shared/invocation.mjs +1 -1
- package/shared/parent-watch.mjs +33 -0
- package/shared/reference-guard-classes.mjs +5 -3
- package/shared/running-start.mjs +14 -3
- package/shared/scope.mjs +22 -3
- package/shared/stdio-connect.mjs +39 -18
- package/shared/writing-standard-classes.mjs +2 -3
package/driver/progress.mjs
CHANGED
|
@@ -97,6 +97,30 @@ export function stepForStage(rawStageKey) {
|
|
|
97
97
|
return { index, label: DISPLAY_STEPS[index], n: index + 1, total: DISPLAY_STEPS.length };
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
/**
|
|
101
|
+
* THE STAGE A LIVE RUN IS IN NOW, for the card and the row a person watches (owner, 2026-09-19).
|
|
102
|
+
*
|
|
103
|
+
* status.json keeps two readings. The step fields hold the furthest display step ever reached, for a
|
|
104
|
+
* stepper that never runs backwards. `lastStage` is the stage the run ENTERED last, written at the one
|
|
105
|
+
* choke point every dispatch passes, corrective re-entries included. The card named the first, so a run
|
|
106
|
+
* sent back into synthesis by a correction pass went on reading "Case law & refutation" (measured on a
|
|
107
|
+
* beta, 2026-09-18). This names the stage in hand, a step back included, with its own step number.
|
|
108
|
+
*
|
|
109
|
+
* `currentStep` FIRST, where the run wrote one: the step it is in now, in the stepper's own words, moved by
|
|
110
|
+
* every transition in either direction and left in place through a stage with no display step. A status
|
|
111
|
+
* written before that field existed falls back to mapping `lastStage`, and one with neither keeps the
|
|
112
|
+
* furthest step, which is the best reading there is. PURE.
|
|
113
|
+
*/
|
|
114
|
+
export function stageNow(status) {
|
|
115
|
+
const c = status?.currentStep;
|
|
116
|
+
if (c && typeof c.label === "string" && c.label.trim())
|
|
117
|
+
return { step: c.label, stepN: Number.isFinite(c.n) ? c.n : null, stepTotal: Number.isFinite(c.total) ? c.total : null };
|
|
118
|
+
const now = stepForStage(status?.lastStage);
|
|
119
|
+
return now
|
|
120
|
+
? { step: now.label, stepN: now.n, stepTotal: now.total }
|
|
121
|
+
: { step: status?.stepLabel ?? null, stepN: status?.stepN ?? null, stepTotal: status?.stepTotal ?? null };
|
|
122
|
+
}
|
|
123
|
+
|
|
100
124
|
// Lifecycle honesty (charter P1 §4): the status patch a TERMINAL delivered write must carry. Nothing runs
|
|
101
125
|
// after the report: delivery is a packet and publish is code, so no
|
|
102
126
|
// recordTransition ever advances the stepper past "Drafting the report" — a delivered run (with
|
|
@@ -386,6 +410,7 @@ export function seedRunStatus(ctx, { resume = false } = {}) {
|
|
|
386
410
|
// — the identity is the SHARED rule, not this stepper's. See identitySeed.
|
|
387
411
|
...identitySeed(),
|
|
388
412
|
stepIndex: first.index, stepLabel: first.label, stepN: first.n, stepTotal: first.total,
|
|
413
|
+
currentStep: currentStepOf(first),
|
|
389
414
|
lastStage: null,
|
|
390
415
|
verdict: null,
|
|
391
416
|
url: null,
|
|
@@ -407,15 +432,26 @@ export function seedRunStatus(ctx, { resume = false } = {}) {
|
|
|
407
432
|
// so for the whole of any of them every surface went on naming the PREVIOUS stage, which is the same
|
|
408
433
|
// defect as a stale step wearing a different field. The step fields are still withheld — that part of
|
|
409
434
|
// the early return was right, and an unmapped stage must never touch the displayed step.
|
|
435
|
+
//
|
|
436
|
+
// `currentStep` IS THE STEP THE RUN IS IN NOW, and it moves both ways. The step fields above keep the
|
|
437
|
+
// furthest step reached (writeRunStatus), so a corrective pass that re-enters synthesis after case law
|
|
438
|
+
// left them reading case law while `lastStage` read synthesis (measured 2026-09-18). `lastStage` is the
|
|
439
|
+
// raw key; this is the same moment in the stepper's own words, for a surface that shows where the run is.
|
|
440
|
+
// A stage with no display step leaves it where it was: those stages run inside the step already shown.
|
|
410
441
|
export function recordTransition(ctx, rawStageKey) {
|
|
411
442
|
const step = stepForStage(rawStageKey);
|
|
412
443
|
writeRunStatus(ctx, {
|
|
413
|
-
...(step ? { stepIndex: step.index, stepLabel: step.label, stepN: step.n, stepTotal: step.total } : {}),
|
|
444
|
+
...(step ? { stepIndex: step.index, stepLabel: step.label, stepN: step.n, stepTotal: step.total, currentStep: currentStepOf(step) } : {}),
|
|
414
445
|
lastStage: rawStageKey,
|
|
415
446
|
});
|
|
416
447
|
rollupStatus(ctx?.run?.studioRoot);
|
|
417
448
|
}
|
|
418
449
|
|
|
450
|
+
/** The stepper's own words for one step, as `currentStep` carries it. PURE. */
|
|
451
|
+
function currentStepOf(step) {
|
|
452
|
+
return { index: step.index, label: step.label, n: step.n, total: step.total };
|
|
453
|
+
}
|
|
454
|
+
|
|
419
455
|
// ---- STATUS.md rollup --------------------------------------------------------------------------------
|
|
420
456
|
// The archive lives UNDER studioRoot (studioRoot/archive/...), so a recursive walk of studioRoot finds
|
|
421
457
|
// both in-flight and delivered/failed runs. Cap depth (status.json only sits at the run-dir level) and
|
|
@@ -62,6 +62,7 @@ import { EXPORT_TOGGLE, exportPopover, EXPORT_MENU_JS } from './report-topbar.mj
|
|
|
62
62
|
// which is the thing providers/_shared/screen.mjs exists to prevent.
|
|
63
63
|
import { makeClassifyStatus, isAllClass } from '../../providers/_shared/screen.mjs';
|
|
64
64
|
import { saysSomethingNew } from '../../shared/says-something-new.mjs';
|
|
65
|
+
import { isNextStepHeading } from '../knockout-next-step.mjs';
|
|
65
66
|
|
|
66
67
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
67
68
|
|
|
@@ -1513,21 +1514,25 @@ function koScale(framework, band) {
|
|
|
1513
1514
|
// It MOVES, it is not copied: `splitOutcome` returns the assessment without it, and the fold renders
|
|
1514
1515
|
// that remainder, so the words appear once.
|
|
1515
1516
|
//
|
|
1517
|
+
// A RECORD WRITTEN SINCE 2026-09-19 NEVER REACHES THIS WITH ONE: the pipeline removes a next-step section
|
|
1518
|
+
// before it writes the record (`knockout-next-step.mjs`), and the heading test below is that module's, so
|
|
1519
|
+
// the two cannot come to disagree about what a next step is. What still draws the section is a record
|
|
1520
|
+
// written before then, re-rendered.
|
|
1521
|
+
//
|
|
1516
1522
|
// THE HEADING IS THE MODEL'S OWN, so this hoists only the LAST block of the assessment. The assessing
|
|
1517
|
-
// skill fixes no heading vocabulary — it asks for sub-headers "where the content divides" and
|
|
1518
|
-
//
|
|
1519
|
-
//
|
|
1520
|
-
//
|
|
1521
|
-
//
|
|
1522
|
-
//
|
|
1523
|
-
//
|
|
1524
|
-
const OUTCOME_HEADING_RE = /^#{1,6}\s*(?:what to do with it|what happens next|next steps?|recommendations?)\b\s*$/i;
|
|
1523
|
+
// skill fixes no heading vocabulary — it asks for sub-headers "where the content divides", and the
|
|
1524
|
+
// wording is written fresh each run. Matching a list of phrases anywhere in the document would let a
|
|
1525
|
+
// mark whose read happens to argue under "Recommendation" in the MIDDLE have that middle lifted to the
|
|
1526
|
+
// bottom, handing the client the engine's argument in an order the engine did not write, with nothing
|
|
1527
|
+
// going red. A trailing block is the one position where a move cannot reorder what is left. So the
|
|
1528
|
+
// phrase list only decides whether the last block is the outcome; anything earlier stays where it was
|
|
1529
|
+
// written, and the page renders the assessment whole.
|
|
1525
1530
|
export function splitOutcome(assessment) {
|
|
1526
1531
|
const whole = String(assessment ?? '').trim();
|
|
1527
1532
|
const lines = whole.split('\n');
|
|
1528
1533
|
const heads = lines.map((l, i) => [l.trim(), i]).filter(([l]) => /^#{1,6}\s/.test(l));
|
|
1529
1534
|
const last = heads[heads.length - 1];
|
|
1530
|
-
if (!last || !
|
|
1535
|
+
if (!last || !isNextStepHeading(last[0])) return { body: whole, outcome: '' };
|
|
1531
1536
|
const outcome = lines.slice(last[1] + 1).join('\n').trim();
|
|
1532
1537
|
if (!outcome) return { body: whole, outcome: '' };
|
|
1533
1538
|
return { body: lines.slice(0, last[1]).join('\n').trim(), outcome };
|
|
@@ -90,7 +90,7 @@ let SENIOR_RIGHTS = new Map();
|
|
|
90
90
|
let DISPOSITION_MODE = false;
|
|
91
91
|
// — per-render flag: true when the record declares the v6 findings contract, which is the one that
|
|
92
92
|
// guarantees a position on every negative and a TYPED off-field ground. Gates the grouped
|
|
93
|
-
// reasoned-negative rendering; false ⇒ the pre- section, byte-identical, for every archived run.
|
|
93
|
+
// reasoned-negative rendering; false ⇒ the pre-change section, byte-identical, for every archived run.
|
|
94
94
|
let NEGATIVES_GROUPED = false;
|
|
95
95
|
// doc 50 — the run's FROZEN framework manifest (opts.framework, from _driver/framework.json). When
|
|
96
96
|
// present the report speaks ITS band words: chips/one-liners read f.band, the gauge ticks show its
|
|
@@ -1825,7 +1825,7 @@ function fullDetail(f, card, recordsByUri = new Map()) {
|
|
|
1825
1825
|
// too. It never did — the grouping's own note promises "each member is a <details> whose body is the
|
|
1826
1826
|
// same fullDetail block the compact card carries", and until now that was untrue of the one thing v6
|
|
1827
1827
|
// guarantees on every negative. Suppressing them there would take a conditional whose only job is
|
|
1828
|
-
// reproducing the pre- shape, which is the legacy code path this program forbids.
|
|
1828
|
+
// reproducing the pre-change shape, which is the legacy code path this program forbids.
|
|
1829
1829
|
//
|
|
1830
1830
|
// Absent fields ⇒ both helpers return '' ⇒ zero bytes, and the interpolation is attached to the
|
|
1831
1831
|
// template's opening so it leaves no whitespace-only line (review 2026-07-31, problem 8).
|
|
@@ -1859,7 +1859,7 @@ function fullDetail(f, card, recordsByUri = new Map()) {
|
|
|
1859
1859
|
//
|
|
1860
1860
|
// WHERE THERE IS NO PROSE THE TYPED PAIR IS STILL THE ACCOUNT, and that is most of the surfaces
|
|
1861
1861
|
// fullDetail feeds: a structured-only finding (no card file), a reasoned-negative row, a compact
|
|
1862
|
-
// card whose report-card stage produced nothing.
|
|
1862
|
+
// card whose report-card stage produced nothing. The promise that a negative's drawer carries its
|
|
1863
1863
|
// positions is untouched — a negative has no Full-detail prose to duplicate.
|
|
1864
1864
|
//
|
|
1865
1865
|
// manageableLine is NOT gated: the manageable category is a closed code-owned vocabulary the prose has
|
|
@@ -1988,7 +1988,7 @@ function manageableLine(f) {
|
|
|
1988
1988
|
//
|
|
1989
1989
|
// TRIM-TO-EMPTY IS LOAD-BEARING AND IS PRESERVED VERBATIM. `foldClause` returned '' for null, undefined,
|
|
1990
1990
|
// '' and whitespace-only input, and that empty string is what makes `clause(f.net) || card?.meta?.one ||
|
|
1991
|
-
// oneFallback(...)` fall through to exactly the pre- value on an archived run, which carries no
|
|
1991
|
+
// oneFallback(...)` fall through to exactly the pre-change value on an archived run, which carries no
|
|
1992
1992
|
// `net`. Deleting the function without keeping that behaviour would move every archived card whose
|
|
1993
1993
|
// `net` key exists but is blank. `clause` is `foldClause` with the budget arm removed and nothing else.
|
|
1994
1994
|
const clause = (value) => String(value ?? '').trim();
|
|
@@ -2314,7 +2314,7 @@ function contextNotesBlock(notes = []) {
|
|
|
2314
2314
|
}
|
|
2315
2315
|
|
|
2316
2316
|
// charter ruling 1 (2026-07-30, name-led): the masthead depth strip LEADS with the product's registry
|
|
2317
|
-
// name in bold — the same name
|
|
2317
|
+
// name in bold — the same name the read pills speak, never a rung on our ladder — then the coverage
|
|
2318
2318
|
// clauses (what this search covers / omits, from the run's frozen components via productCoverageNote).
|
|
2319
2319
|
// The seam is the LAST " — ": a registry name may itself carry one ("Preliminary clearance — register
|
|
2320
2320
|
// only") while the coverage clauses never do (they join on ";" and ","). A note with no seam at all
|
|
@@ -2323,7 +2323,7 @@ function contextNotesBlock(notes = []) {
|
|
|
2323
2323
|
/**
|
|
2324
2324
|
* THE HERO VERDICT CAPTION, folded to its first sentence.
|
|
2325
2325
|
*
|
|
2326
|
-
*
|
|
2326
|
+
* The design ruling: above any fold, only a statement, a labelled row, a count or a one-line card —
|
|
2327
2327
|
* "prose never appears until someone opens something, and once opened nothing is ever cut". The finding
|
|
2328
2328
|
* cards were moved onto that rule by `cf8dd43`; the hero caption was named in the same ruling and left
|
|
2329
2329
|
* behind, and this is the bullet that PR recorded as still owed.
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
//
|
|
6
6
|
// ── why this exists ──────────────────────────────────────────────────────────────────────────────────
|
|
7
7
|
//
|
|
8
|
-
// The shipped trace
|
|
8
|
+
// The shipped trace asked ONE pass to answer a question that spans two moments that never
|
|
9
9
|
// coexist. A record discarded at screening is gone by the time `findings.json` is written; the findings
|
|
10
10
|
// do not exist at the moment the record is discarded. Whichever end a single pass runs at, it guesses
|
|
11
11
|
// about the other — and `deriveRecordCarry` ran inside `register-digest`, which is BEFORE `synthesis`
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
// CORRECTED 2026-08-11 — THIS PARAGRAPH USED TO SAY "one qid per office … which is the shape
|
|
42
42
|
// joinPlanToBands already has", AND THAT WAS FALSE ABOUT THE COMPILER IT DOCUMENTS. There is no
|
|
43
43
|
// per-office qid. compileRegisterPlan narrows ONE shared `regions` array and hands it to every entry
|
|
44
|
-
// (register-plan.mjs:
|
|
44
|
+
// (register-plan.mjs:805 compileRegisterPlan); the unreachable office produces no entry, so no qid, so no band block, so
|
|
45
45
|
// nothing ever reaches joinPlanToBands' deferred bucket — whose only source is a block stamped
|
|
46
46
|
// `error:true && deferred:true` (register-plan.mjs:1317 extendRegisterPlan).
|
|
47
47
|
//
|
|
@@ -447,7 +447,7 @@ export async function countRegisterHits({
|
|
|
447
447
|
counts,
|
|
448
448
|
// — which registers these figures cover, present ONLY when one was dropped. countLine renders
|
|
449
449
|
// it; the scope block below records it. Both, deliberately: `scope.deferredJurisdictions` has sat
|
|
450
|
-
// on this artifact since it was written and NOTHING reads it, which is the same shape as
|
|
450
|
+
// on this artifact since it was written and NOTHING reads it, which is the same shape as
|
|
451
451
|
// `deferred_coverage` riding the plan with no consumer and shipping a false clean. A field a
|
|
452
452
|
// reader never sees is not a disclosure, so this one lands on the rendered line first.
|
|
453
453
|
...(officeScope ? { officeScope } : {}),
|
|
@@ -366,7 +366,7 @@ export function negativeMarkCell(row) {
|
|
|
366
366
|
const MIN_JUDGED_ROWS = 1;
|
|
367
367
|
|
|
368
368
|
/** A pipe row, cells escaped so a value carrying `|` cannot open a column. */
|
|
369
|
-
const row = (cells) => `| ${cells.map((c) => str(c).replace(/\|/g, "\\|").replace(/\n+/g, " ") || "—").join(" | ")} |`;
|
|
369
|
+
const row = (cells) => `| ${cells.map((c) => str(c).replace(/\\/g, "\\\\").replace(/\|/g, "\\|").replace(/\n+/g, " ") || "—").join(" | ")} |`;
|
|
370
370
|
const table = (columns, rows) => [row(columns), `|${columns.map(() => "---").join("|")}|`, ...rows.map(row)].join("\n");
|
|
371
371
|
|
|
372
372
|
/**
|
package/driver/register-plan.mjs
CHANGED
|
@@ -705,12 +705,7 @@ export function excludeHouseElement(manifest, house) {
|
|
|
705
705
|
|
|
706
706
|
const lcEl = element.toLowerCase();
|
|
707
707
|
const lcWords = words.map((w) => w.toLowerCase());
|
|
708
|
-
|
|
709
|
-
// remainder word "on" inside an unrelated mutation and keep a query this exists to drop.
|
|
710
|
-
const namesRemainder = (v) => {
|
|
711
|
-
const t = String(v ?? "").toLowerCase();
|
|
712
|
-
return lcWords.some((w) => new RegExp(`(^|[^\\p{L}\\p{N}])${w.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}([^\\p{L}\\p{N}]|$)`, "u").test(t));
|
|
713
|
-
};
|
|
708
|
+
const namesRemainder = (v) => namesAnyWord(v, lcWords);
|
|
714
709
|
|
|
715
710
|
// THE DOMINANT WORD OF THE REMAINDER: longest, ties broken by the LAST of them. The lawyer's own
|
|
716
711
|
// phrasing was "the last word alone", and on the matter that produced this the two agree; longest is
|
|
@@ -738,11 +733,75 @@ export function excludeHouseElement(manifest, house) {
|
|
|
738
733
|
// REQUIREMENT 3: the element is still checked ONCE, as a confirmation of the client's own live
|
|
739
734
|
// registrations rather than as a conflict search — so the exclusion is evidenced on the report by a
|
|
740
735
|
// query that ran, not by a sentence saying one would have.
|
|
736
|
+
// The remainder rides on it so a writer AFTER the compile can apply the same rule (`houseElementOf`).
|
|
741
737
|
const confirmation = { axis: "incumbent-class", predicate: "owner", term: element,
|
|
742
|
-
expected_kind: "enumerate", provenance: "mark", house_element_confirmation: true
|
|
738
|
+
expected_kind: "enumerate", provenance: "mark", house_element_confirmation: true,
|
|
739
|
+
house_element_remainder: remainder };
|
|
743
740
|
return { manifest: next, confirmation, refused: null };
|
|
744
741
|
}
|
|
745
742
|
|
|
743
|
+
/**
|
|
744
|
+
* Does `text` carry any of `words` (lower-case) as a word of its own? By word boundary rather than by
|
|
745
|
+
* containment: `includes` would count the remainder word "on" inside an unrelated mutation.
|
|
746
|
+
*/
|
|
747
|
+
function namesAnyWord(text, words) {
|
|
748
|
+
const t = String(text ?? "").toLowerCase();
|
|
749
|
+
return words.some((w) => new RegExp(`(^|[^\\p{L}\\p{N}])${w.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}([^\\p{L}\\p{N}]|$)`, "u").test(t));
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* The house element a plan was compiled without, read off the plan itself, or null.
|
|
754
|
+
*
|
|
755
|
+
* WHY THE PLAN CARRIES IT. The compile is not the only writer. After it, the common-law cross-check,
|
|
756
|
+
* the recall lane, the frame reopen and the jurisdiction candidates fold entries in, and the register
|
|
757
|
+
* session proposes its own. On the first live run that proposed an exclusion (2026-09-19) the compile
|
|
758
|
+
* dropped the element and two later writers put it straight back: a contains query from the cross-check
|
|
759
|
+
* and an exact query the register session proposed, both on the bare element. Those writers hold the
|
|
760
|
+
* plan and nothing else, so the plan is where the exclusion has to be readable. Its confirmation row
|
|
761
|
+
* names the element, and the remainder rides beside it because the rule needs both.
|
|
762
|
+
*/
|
|
763
|
+
export function houseElementOf(plan) {
|
|
764
|
+
const c = (plan?.entries ?? []).find((e) => e?.house_element_confirmation === true);
|
|
765
|
+
const element = String(c?.term ?? "").trim();
|
|
766
|
+
if (!element) return null;
|
|
767
|
+
const remainder = String(c?.house_element_remainder ?? "").trim().toLowerCase();
|
|
768
|
+
return { element, remainderWords: remainder.split(/\s+/).filter(Boolean) };
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/**
|
|
772
|
+
* Is this term a search on the excluded house element? The compile's own rule (`excludeHouseElement`
|
|
773
|
+
* keeps a variant only when it names a word of the remainder), applied to a term that arrives after
|
|
774
|
+
* the compile: a term carrying the element and no word of the remainder is a search on the element,
|
|
775
|
+
* whether it is the bare element, the element with a suffix, or the element beside other words. A term
|
|
776
|
+
* carrying a word of the remainder, like the whole mark, is kept, as the compile keeps it.
|
|
777
|
+
*
|
|
778
|
+
* A plan frozen before the remainder was recorded knows the element only, and then only the bare
|
|
779
|
+
* element counts: "carries no word of the remainder" cannot be decided without the remainder.
|
|
780
|
+
*/
|
|
781
|
+
export function isHouseElementTerm(term, house) {
|
|
782
|
+
if (!house) return false;
|
|
783
|
+
const t = String(term ?? "").trim().toLowerCase();
|
|
784
|
+
const el = house.element.toLowerCase();
|
|
785
|
+
if (t === el || (norm(el) !== "" && norm(t) === norm(el) && formKey(t) === formKey(el))) return true;
|
|
786
|
+
if (!house.remainderWords.length) return false;
|
|
787
|
+
const carries = t.includes(el) || (norm(el) !== "" && norm(t).includes(norm(el)));
|
|
788
|
+
return carries && !namesAnyWord(t, house.remainderWords);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/**
|
|
792
|
+
* The entry without its house-element terms, or null when none is left. An owner sweep is not mark
|
|
793
|
+
* text and comes back untouched: the one owner query on the element is the confirmation itself. PURE.
|
|
794
|
+
*/
|
|
795
|
+
export function withoutHouseElementTerms(entry, house) {
|
|
796
|
+
if (!house || !entry || entry.predicate === "owner") return entry;
|
|
797
|
+
if (Array.isArray(entry.terms)) {
|
|
798
|
+
const kept = entry.terms.filter((t) => !isHouseElementTerm(t, house));
|
|
799
|
+
if (!kept.length) return null;
|
|
800
|
+
return kept.length === entry.terms.length ? entry : { ...entry, terms: kept };
|
|
801
|
+
}
|
|
802
|
+
return isHouseElementTerm(entry.term, house) ? null : entry;
|
|
803
|
+
}
|
|
804
|
+
|
|
746
805
|
export function compileRegisterPlan({ manifest, job, form = null, skillVersion = "", capabilities = null, unavailableOffices = [], houseElement = null }) {
|
|
747
806
|
// ── AN EXCLUDED ELEMENT'S FORM BAND MUST BE UNREACHABLE, NOT MERELY UNASKED-FOR ─────────────────
|
|
748
807
|
//
|
|
@@ -1495,10 +1554,13 @@ export function entryQuestionKey(entry, plan) {
|
|
|
1495
1554
|
* an absence, and an absence reported as success is the failure this whole issue is about. Every
|
|
1496
1555
|
* caller writes them somewhere a reader will meet them.
|
|
1497
1556
|
*
|
|
1498
|
-
* Returns { plan, added: [qid], refused: [{qid, term, issue}] }
|
|
1557
|
+
* Returns { plan, added: [qid], refused: [{qid, term, issue}], excluded: [qid] } — `excluded` are rows on
|
|
1558
|
+
* the house element the compile set aside, left out quietly (see houseElementOf).
|
|
1499
1559
|
*/
|
|
1500
1560
|
export function foldSupplementalEntries(plan, entries) {
|
|
1501
1561
|
const have = new Set(plan.entries.map((e) => e.qid));
|
|
1562
|
+
const house = houseElementOf(plan);
|
|
1563
|
+
const excluded = [];
|
|
1502
1564
|
// — first qid wins, so the refusal names the row already IN the plan rather than a later
|
|
1503
1565
|
// arrival. The compiler's own rows are in here too: a supplemental re-proposal of a compiler slice
|
|
1504
1566
|
// is the case that actually fired on the round in evidence.
|
|
@@ -1530,8 +1592,9 @@ export function foldSupplementalEntries(plan, entries) {
|
|
|
1530
1592
|
// one identity, which is the fault. Keyed on the qid alone this refused the harmless case too, and
|
|
1531
1593
|
// the suite's own arm said so.
|
|
1532
1594
|
const inBatch = new Map(); // qid -> the term that claimed it
|
|
1533
|
-
for (const
|
|
1534
|
-
if (!
|
|
1595
|
+
for (const offered of entries ?? []) {
|
|
1596
|
+
if (!offered || typeof offered.qid !== "string" || !offered.qid) continue;
|
|
1597
|
+
let e = offered;
|
|
1535
1598
|
const eTerm = String(e.term ?? e.terms?.[0] ?? "");
|
|
1536
1599
|
if (inBatch.has(e.qid) && inBatch.get(e.qid) === eTerm) continue; // the same row twice — one refusal
|
|
1537
1600
|
if (inBatch.has(e.qid)) {
|
|
@@ -1546,6 +1609,12 @@ export function foldSupplementalEntries(plan, entries) {
|
|
|
1546
1609
|
inBatch.set(e.qid, eTerm);
|
|
1547
1610
|
if (have.has(e.qid)) continue; // already in the PLAN — a re-proposal, expected and quiet
|
|
1548
1611
|
have.add(e.qid); // a refused qid is claimed too — the same row twice is one refusal
|
|
1612
|
+
// THE CLIENT'S OWN ELEMENT, SET ASIDE BY THE COMPILE, IS NOT FOLDED BACK IN (see houseElementOf).
|
|
1613
|
+
// QUIET, NOT REFUSED: a refused row renders to the client as an OPEN ask, and this is no unanswered
|
|
1614
|
+
// question. The plan's confirmation row answers it and the report states the exclusion.
|
|
1615
|
+
const kept = withoutHouseElementTerms(e, house);
|
|
1616
|
+
if (!kept) { excluded.push(e.qid); continue; }
|
|
1617
|
+
e = kept;
|
|
1549
1618
|
const issues = entryTermIssues(e);
|
|
1550
1619
|
if (issues.length) {
|
|
1551
1620
|
refused.push({ qid: e.qid, term: String(e.term ?? e.terms?.[0] ?? ""),
|
|
@@ -1570,11 +1639,12 @@ export function foldSupplementalEntries(plan, entries) {
|
|
|
1570
1639
|
if (key && e.unsupported !== true) asked.set(key, e.qid); // dedupes the batch too, on the question
|
|
1571
1640
|
added.push(e);
|
|
1572
1641
|
}
|
|
1573
|
-
if (!added.length) return { plan, added: [], refused };
|
|
1642
|
+
if (!added.length) return { plan, added: [], refused, excluded };
|
|
1574
1643
|
return {
|
|
1575
1644
|
plan: { ...plan, plan_version: (plan.plan_version ?? 1) + 1, entries: [...plan.entries, ...added] },
|
|
1576
1645
|
added: added.map((e) => e.qid),
|
|
1577
1646
|
refused,
|
|
1647
|
+
excluded,
|
|
1578
1648
|
};
|
|
1579
1649
|
}
|
|
1580
1650
|
|
|
@@ -117,9 +117,9 @@ export function findingsDocFor(runDir, ordinal) {
|
|
|
117
117
|
// the record and under what number. publish/index.mjs quotes the correct shape verbatim in a comment of
|
|
118
118
|
// its own, which is what makes the divergence provable rather than a matter of taste.
|
|
119
119
|
//
|
|
120
|
-
// IT SHIPPED BECAUSE THE CONVERSION WAS NEVER CHECKED AGAINST THE ARTIFACT. asked for exactly that
|
|
120
|
+
// IT SHIPPED BECAUSE THE CONVERSION WAS NEVER CHECKED AGAINST THE ARTIFACT. The issue asked for exactly that
|
|
121
121
|
// check — "recompose the bullet for every card in demo and assert byte-equality with the
|
|
122
|
-
// delivered report.md" — and writing it is what caught this. The guard now lives beside
|
|
122
|
+
// delivered report.md" — and writing it is what caught this. The guard now lives beside the code it checks, and
|
|
123
123
|
// this is the argument for building the guard an issue asks for even when the code already looks done.
|
|
124
124
|
//
|
|
125
125
|
// THE NUMBER comes off the URI's last segment: `/mark/eu/018575624` → `018575624`. That is a parse, and
|
|
@@ -95,7 +95,9 @@ export const RESULT_NOUN_FIELDS = Object.freeze([
|
|
|
95
95
|
// writing site like the other two — it is a returned CLASSIFICATION of a checkout, not a field named
|
|
96
96
|
// for a result whose value is what a call returned, so the verdict is unchanged.
|
|
97
97
|
{ file: "driver/engine-build.mjs", noun: "outcome", sites: 3, atWriteSite: 0, verdict: "out-of-scope" },
|
|
98
|
-
|
|
98
|
+
// Read at the writing site: the third is the reply when every proposal was the house element the
|
|
99
|
+
// compile set aside — `executed: false` because nothing ran, the same statement as the error reply.
|
|
100
|
+
{ file: "driver/engine/mcp/supplemental.mjs", noun: "executed", sites: 3, atWriteSite: 0, verdict: "out-of-scope" },
|
|
99
101
|
{ file: "driver/outbox-backoff.mjs", noun: "outcome", sites: 5, atWriteSite: 0, verdict: "out-of-scope" },
|
|
100
102
|
{ file: "driver/portal-service.mjs", noun: "outcome", sites: 2, atWriteSite: 0, verdict: "out-of-scope" },
|
|
101
103
|
{ file: "driver/remedy-accounting.mjs", noun: "executed", sites: 1, atWriteSite: 0, verdict: "out-of-scope" },
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
//
|
|
19
19
|
// no configured store
|
|
20
20
|
// → the bundled roster governs, exactly as it always did — derived by driver/bundled-demos.mjs
|
|
21
|
-
// from the directory that ships it, never a list written down. On a test box it is CORRECT
|
|
22
|
-
|
|
21
|
+
// from the directory that ships it, never a list written down. On a test box it is CORRECT;
|
|
22
|
+
// anywhere else it means CLEAROTRON_CUSTOMERS_DIR is not reaching the service.
|
|
23
23
|
//
|
|
24
24
|
// Pure: no fs, no env, no network. Every input is a parameter so a test can state the whole world.
|
|
25
25
|
|
package/driver/search-policy.mjs
CHANGED
|
@@ -152,7 +152,7 @@ export const COMPONENTS = {
|
|
|
152
152
|
// so `basis` under-reports and could not carry the distinction even if we wanted it to.)
|
|
153
153
|
//
|
|
154
154
|
// The one mechanical set that stays load-bearing is the FLOORS — a tier-`identical` floor row, live and
|
|
155
|
-
// in an instructed class, gets a full card in EVERY product. That is an obligation
|
|
155
|
+
// in an instructed class, gets a full card in EVERY product. That is an obligation, not a
|
|
156
156
|
// priority, and the ladder does not grade it.
|
|
157
157
|
//
|
|
158
158
|
// `graded` means: the stage is told what kind of report it is writing and grades ITS OWN written output.
|
|
@@ -51,7 +51,7 @@ export function escalatedAxes(flagsText, axes) {
|
|
|
51
51
|
const flags = String(flagsText ?? "");
|
|
52
52
|
if (!flags) return [];
|
|
53
53
|
return axes.filter((a) =>
|
|
54
|
-
new RegExp(`(^|\\n)\\s*[-*]?\\s*ESCALATE:\\s*${a.replace(/[
|
|
54
|
+
new RegExp(`(^|\\n)\\s*[-*]?\\s*ESCALATE:\\s*${a.replace(/[.*+?^${}()|[\]\\-]/g, "\\$&")}\\b`, "i").test(flags));
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
/** Where the call's evidence lives — the driver's own record of what the seat handed it. */
|
|
@@ -418,7 +418,7 @@ export const KO_STAGES = {
|
|
|
418
418
|
// The depth rule is not stylistic. `# ` opens a SECTION of the delivered report — parseSections
|
|
419
419
|
// splits on /^# /m and batchSummaryOf terminates on it — so a single hash written here ends the
|
|
420
420
|
// client's summary at that line and drops everything after it, silently, on the entry point.
|
|
421
|
-
`EVERY mark ALSO carries "assessment": the opening read a reader of THIS MARK'S OWN report meets first, before any table. Write it for a client who ordered this one name and nothing else: what the name is, what the landscape around it looks like, what drives the rating
|
|
421
|
+
`EVERY mark ALSO carries "assessment": the opening read a reader of THIS MARK'S OWN report meets first, before any table. Write it for a client who ordered this one name and nothing else: what the name is, what the landscape around it looks like, what drives the rating.`,
|
|
422
422
|
`GIVE IT STRUCTURE A READER CAN SCAN — that is what changed. Sub-headers where the content divides, "- " bullets for the load-bearing points, a blank line between blocks. A sub-header is "## " or "### ". NEVER "# ": one hash opens a section of the report itself, and a summary that opens a section ends there and takes the rest of itself off the client's page. A single sub-header over one unbroken block is not structure — the blocks are what make the length readable, and unstructured length is the complaint this replaces.`,
|
|
423
423
|
`KEEP THE LENGTH. It is still the most useful part of a triage and it is still not rationed. What it now has to be is CONSISTENT: this mark's opening read is the scale of a single-mark report's opening read, and the marks of one batch sit within reach of each other. One assessment several times the size of its siblings is out of family. No word count and none is coming — judge it against the other marks in front of you.`,
|
|
424
424
|
`SYNTHESISE THE SOURCES, DO NOT SHELVE THEM SEPARATELY. Where filings were handed to you for this mark, weigh them in the SAME passage as the marketplace and common-law reading — what the screened field shows, what those filings do to that reading, and what the two together mean for the band. A reader must not be handed one block per source and left to reconcile them.`,
|
package/driver/stages.mjs
CHANGED
|
@@ -2154,7 +2154,7 @@ export const STAGES = {
|
|
|
2154
2154
|
const supplementalLane = !!registerPlan?.contract?.supplemental_lane;
|
|
2155
2155
|
return lines(
|
|
2156
2156
|
`First, read and follow exactly: skills/clearance-register/SKILL.md (the shared spine) then skills/clearance-register/unit.md (MODE A — UNIT). Do NOT read digest.md (digest-mode judgment a unit must never run).`,
|
|
2157
|
-
// WHAT THE KEY ALSO CARRIES — COMPOSED, NOT DOCTRINE
|
|
2157
|
+
// WHAT THE KEY ALSO CARRIES — COMPOSED, NOT DOCTRINE.
|
|
2158
2158
|
// `unit.md` used to name three tools flat, and on a deployment withholding two of them the seat was
|
|
2159
2159
|
// told it holds tools its grant does not carry. The composer derives the list from the same table
|
|
2160
2160
|
// that does the excluding, and returns NULL — not an empty sentence — when the provider cannot be
|