clearotron 0.3.2-beta.12 → 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/CONTRIBUTING.md +6 -5
- package/INSTALL.md +3 -4
- package/README.md +2 -1
- package/bin/example.mjs +12 -1
- package/bin/onboard.mjs +16 -2
- package/bin/start.mjs +6 -3
- 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 +13 -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/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/openai-agent.mjs +2 -2
- package/driver/findings-model.mjs +21 -6
- package/driver/gateway.mjs +1 -1
- package/driver/package.json +1 -1
- package/driver/pipeline-knockout.mjs +1 -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 +4 -1
- package/driver/portal-service.mjs +17 -5
- package/driver/predelivery-lint.mjs +9 -4
- package/driver/progress.mjs +37 -1
- package/driver/publish/render.mjs +6 -6
- package/driver/record-discard.mjs +1 -1
- package/driver/register-count.mjs +1 -1
- package/driver/register-digest-record.mjs +1 -1
- package/driver/report-card-record.mjs +2 -2
- package/driver/roster-verdict.mjs +2 -2
- package/driver/search-policy.mjs +1 -1
- package/driver/skeptic-record.mjs +1 -1
- package/driver/stages.mjs +1 -1
- package/driver/suite-census.json +27 -9
- package/driver/systemd/README.md +1 -1
- package/driver/unit-inventory.mjs +2 -2
- package/driver/verify.mjs +1 -1
- package/mcp-server/CHANGELOG.md +4 -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/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 +4 -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/citation-anchor-report.mjs +1 -1
- package/scripts/citation-line-check.mjs +3 -3
- package/scripts/e2e.mjs +1 -1
- package/scripts/env-audit.mjs +1 -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/settings-render-check.mjs +5 -1
- package/scripts/strip-tracker-citations.mjs +4 -4
- package/scripts/test-run.mjs +46 -3
- package/shared/browser-temp-root.mjs +10 -3
- package/shared/client-door.mjs +18 -6
- package/shared/connect-clients.mjs +2 -0
- package/shared/identifier-scan.mjs +2 -2
- package/shared/invocation.mjs +1 -1
- package/shared/reference-guard-classes.mjs +5 -3
- package/shared/stdio-connect.mjs +39 -18
- package/shared/writing-standard-classes.mjs +2 -3
|
@@ -1363,7 +1363,7 @@ export function parseDispositionForm(raw) {
|
|
|
1363
1363
|
// EVERY OBLIGATION SHIPS. No head(), no slice(), no "…and N more". The register-digest sibling
|
|
1364
1364
|
// (gateway.mjs deferredSlicesRequiredRows) printed the first six qids per axis and elided the rest, and its
|
|
1365
1365
|
// own doc block records what that cost: "Under advice that is an economy; under a requirement it makes
|
|
1366
|
-
// compliance impossible." Same rule here
|
|
1366
|
+
// compliance impossible." Same rule here: a value a model must copy is complete, or it is
|
|
1367
1367
|
// visibly marked as cut. Nothing below is cut, so nothing below is marked.
|
|
1368
1368
|
/**
|
|
1369
1369
|
* The obligation set as the seat's own to-do list, for the tool result. Returns "" when nothing is owed —
|
|
@@ -1375,7 +1375,7 @@ export function renderConnotationObligations(ob, { ledgerPath = "your half ledge
|
|
|
1375
1375
|
// ── — THE PAGE IS NUMBERED OFF THE SAME FUNCTION THAT RESOLVES THE NUMBER ─────────────────
|
|
1376
1376
|
//
|
|
1377
1377
|
// `obligationRows` is the driver's row list, and a `row_index` is a position into it. Numbering this
|
|
1378
|
-
// page with its own counter would be two derivations of one ordering, which is the mistake
|
|
1378
|
+
// page with its own counter would be two derivations of one ordering, which is the mistake the
|
|
1379
1379
|
// segment display already names in its own comment: "the display and the check cannot disagree, which
|
|
1380
1380
|
// they would the moment either recomputed its own split". So the numbers come from the list itself.
|
|
1381
1381
|
//
|
|
@@ -1610,7 +1610,7 @@ export function renderDispositionTable(rows, ob) {
|
|
|
1610
1610
|
for (const e of ob?.queries ?? []) for (const r of e.results) if (r.id) byId.set(normId(r.id), r);
|
|
1611
1611
|
// — the rows are the FORM's, and a row the seat never ruled on has nothing to render: it is
|
|
1612
1612
|
// outstanding work, which the gate reports as a count, not a table row reading "—".
|
|
1613
|
-
const cell = (s) => String(s ?? "").replace(/\|/g, "\\|").replace(/\s+/g, " ").trim();
|
|
1613
|
+
const cell = (s) => String(s ?? "").replace(/\\/g, "\\\\").replace(/\|/g, "\\|").replace(/\s+/g, " ").trim();
|
|
1614
1614
|
const usable = (rows ?? []).filter((d) => d?.receipt_id && d?.ruling);
|
|
1615
1615
|
// ── — A PARKED ROW IS NOT AN OUTSTANDING ROW, AND THE FILTER ABOVE STOPPED BEING SAFE ─────────
|
|
1616
1616
|
//
|
|
@@ -1683,7 +1683,7 @@ const NO_POLICED_SECTION = "(no PR / reputational / connotation section)";
|
|
|
1683
1683
|
// id that is not among THAT row's own candidates — the driver wrote those ids, so this is a damaged
|
|
1684
1684
|
// form, not outstanding work, and its remedy is different.
|
|
1685
1685
|
// undisposed_cite_too_short deleted — cannot occur; nothing is retyped.
|
|
1686
|
-
// recurrent_uncited_form deleted — the form has no shape for the seat to get wrong.
|
|
1686
|
+
// recurrent_uncited_form deleted — the form has no shape for the seat to get wrong. The old
|
|
1687
1687
|
// cited-but-unbound state was a property of PROSE (right page, wrong query string on the line); a
|
|
1688
1688
|
// form row IS its binding, so the state has no representative.
|
|
1689
1689
|
// no_source_cite deleted — advisory, prose-only, never reached a token.
|
|
@@ -1786,7 +1786,7 @@ export const CONNOTATION_FORM_REASONS = Object.freeze(
|
|
|
1786
1786
|
*
|
|
1787
1787
|
* `quote_unbound` and `form_damaged` are deliberately absent: those rows ARE ruled.
|
|
1788
1788
|
*
|
|
1789
|
-
* `parked`
|
|
1789
|
+
* `parked` is absent for the opposite reason and it is the load-bearing one: a parked row
|
|
1790
1790
|
* is NOT ruled, and it is not outstanding either. Outstanding means "the seat still owes this", and the
|
|
1791
1791
|
* whole point of the park is that the seat has been told to stop — leaving it here would keep the stage
|
|
1792
1792
|
* failing forever on a row nobody may re-send, which is the deterministic death the park introduced.
|
|
@@ -456,7 +456,7 @@ export const E3_BACKLOG = [
|
|
|
456
456
|
where: "driver/stages.mjs:3182",
|
|
457
457
|
surface: "stage-message",
|
|
458
458
|
evidence: "MACHINE FINDINGS (MANDATORY): … a JSON OBJECT {\"schema_version\":<FINDINGS_SCHEMA_VERSION>,\"rated_under_framework\":\"…\",\"findings\":[...],\"coverage\":[...],\"context_notes\":[...],\"actions\":[...],\"ask_answers\":[...]} … Each finding object has EXACTLY these keys: {\"ordinal\",\"mark\",\"owner\",\"band\",\"net\",\"bor",
|
|
459
|
-
reparsedBy: "driver/findings-model.mjs:
|
|
459
|
+
reparsedBy: "driver/findings-model.mjs:844 parseFindingsJson via validators.narrative",
|
|
460
460
|
removedByMove: "NOTHING ON THE #850 PLAN REMOVES THIS",
|
|
461
461
|
},
|
|
462
462
|
// RETIRED 2026-08-16 — synthesis's dictated disposition list, and I am the one who left it standing.
|
|
@@ -479,7 +479,7 @@ export const E3_BACKLOG = [
|
|
|
479
479
|
where: "driver/stages.mjs:3219",
|
|
480
480
|
surface: "stage-message",
|
|
481
481
|
evidence: "- manageable …: {\"category\":\"<EXACTLY one of large-competitor / commercial-partner / troll / well-known-enforcer>\",\"reason\":\"<one-two lines…>\"}",
|
|
482
|
-
reparsedBy: "driver/findings-model.mjs:
|
|
482
|
+
reparsedBy: "driver/findings-model.mjs:844 parseFindingsJson",
|
|
483
483
|
removedByMove: "NOTHING ON THE #850 PLAN REMOVES THIS",
|
|
484
484
|
},
|
|
485
485
|
{
|
|
@@ -488,7 +488,7 @@ export const E3_BACKLOG = [
|
|
|
488
488
|
where: "driver/stages.mjs:3235",
|
|
489
489
|
surface: "stage-message",
|
|
490
490
|
evidence: "- meters: {\"mark_similarity\":{...},\"goods_proximity\":{...},\"use\":{...},\"enforcer\":{...}} — all four present, each {\"token\",\"basis\",\"source\"}. … mark_similarity = high | medium | low. goods_proximity = high | medium | low. enforcer = high | medium | low | unknown. use = confirmed | not-confirmed | un",
|
|
491
|
-
reparsedBy: "driver/findings-model.mjs:
|
|
491
|
+
reparsedBy: "driver/findings-model.mjs:844 parseFindingsJson; driver/verify.mjs:1171 checkFindingsSibling gates meters.*.source; finding_basis_source_missing",
|
|
492
492
|
removedByMove: "NOTHING ON THE #850 PLAN REMOVES THIS",
|
|
493
493
|
},
|
|
494
494
|
{
|
|
@@ -497,7 +497,7 @@ export const E3_BACKLOG = [
|
|
|
497
497
|
where: "driver/stages.mjs:3236",
|
|
498
498
|
surface: "stage-message",
|
|
499
499
|
evidence: "- quadrant: {\"x\",\"y\"} numbers in [0,1]. x = goods/services proximity (0 = distant, 1 = identical). y = mark similarity (0 = distinct, 1 = identical).",
|
|
500
|
-
reparsedBy: "driver/findings-model.mjs:
|
|
500
|
+
reparsedBy: "driver/findings-model.mjs:844 parseFindingsJson",
|
|
501
501
|
removedByMove: "NOTHING ON THE #850 PLAN REMOVES THIS",
|
|
502
502
|
},
|
|
503
503
|
{
|
|
@@ -506,7 +506,7 @@ export const E3_BACKLOG = [
|
|
|
506
506
|
where: "driver/stages.mjs:3237",
|
|
507
507
|
surface: "stage-message",
|
|
508
508
|
evidence: "- source: {\"source_type\",\"resolved_link\"}. source_type EXACTLY one of: register-vendor / register-euipo / common-law-marketplace / common-law-web / case-law",
|
|
509
|
-
reparsedBy: "driver/findings-model.mjs:
|
|
509
|
+
reparsedBy: "driver/findings-model.mjs:844 parseFindingsJson",
|
|
510
510
|
removedByMove: "M1 removes the opaque `resolved_link` URI (ordinal / record-handle selection); the object shape and the source_type enum survive",
|
|
511
511
|
},
|
|
512
512
|
{
|
|
@@ -515,7 +515,7 @@ export const E3_BACKLOG = [
|
|
|
515
515
|
where: "driver/stages.mjs:3238",
|
|
516
516
|
surface: "stage-message",
|
|
517
517
|
evidence: "coverage[]: ONE object per coverage AREA, EXACTLY {\"area\",\"state\",\"note\"}. … state EXACTLY one of: confirmed-clean / coverage-limited / open / not-searched / note.",
|
|
518
|
-
reparsedBy: "driver/findings-model.mjs:
|
|
518
|
+
reparsedBy: "driver/findings-model.mjs:844 parseFindingsJson; the render owns the coverage panel from these typed states",
|
|
519
519
|
removedByMove: "NOTHING ON THE #850 PLAN REMOVES THIS",
|
|
520
520
|
},
|
|
521
521
|
{
|
|
@@ -533,7 +533,7 @@ export const E3_BACKLOG = [
|
|
|
533
533
|
where: "driver/stages.mjs:3304",
|
|
534
534
|
surface: "stage-message",
|
|
535
535
|
evidence: "MARK ASSESSMENT … STRUCTURED FORM …: either field may instead be an OBJECT {\"read\":\"…\",\"spectrum\":\"…\",\"per_class\":[{\"class\":\"5\",\"note\":\"…\"}],\"per_market\":[{\"market\":\"CN\",\"note\":\"…\"}],\"counter_registrations\":[{\"mark\":\"…\",\"uri\":\"/mark/…\",\"note\":\"…\"}],\"acquired\":\"<optional>\",\"note\":\"<optional residual>",
|
|
536
|
-
reparsedBy: "driver/findings-model.mjs:
|
|
536
|
+
reparsedBy: "driver/findings-model.mjs:844 parseFindingsJson; the report collapses the rows behind toggles and the audit workbook renders them",
|
|
537
537
|
removedByMove: "NOTHING ON THE #850 PLAN REMOVES THIS",
|
|
538
538
|
},
|
|
539
539
|
{
|
|
@@ -542,7 +542,7 @@ export const E3_BACKLOG = [
|
|
|
542
542
|
where: "driver/stages.mjs:3310",
|
|
543
543
|
surface: "stage-message",
|
|
544
544
|
evidence: "FOUR ANSWERS …: \"four_answers\": {\"third_party_rights\":{...},\"objection_likelihood\":{...},\"registrability\":{...},\"client_enforceability\":{...}} … Each answer … is {\"read\":\"…\",\"token\":\"…\",\"basis\":\"…\",\"ordinals\":[…]}. Tokens (closed enums …): third_party_rights = strong|moderate|weak; objection_likelih",
|
|
545
|
-
reparsedBy: "driver/findings-model.mjs:
|
|
545
|
+
reparsedBy: "driver/findings-model.mjs:844 parseFindingsJson",
|
|
546
546
|
removedByMove: "NOTHING ON THE #850 PLAN REMOVES THIS",
|
|
547
547
|
},
|
|
548
548
|
{
|
|
@@ -551,7 +551,7 @@ export const E3_BACKLOG = [
|
|
|
551
551
|
where: "driver/stages.mjs:3221",
|
|
552
552
|
surface: "stage-message",
|
|
553
553
|
evidence: "ACTIONS REGISTER …: emit \"actions\": [...] — ONE object per forward step …, each EXACTLY {\"id\",\"kind\",\"text\",\"ordinals\"} plus an OPTIONAL \"deadline\" and an OPTIONAL \"condition\". … kind: EXACTLY one of — consent / coexistence-agreement / territorial-delimitation / goods-amendment / mark-modification /",
|
|
554
|
-
reparsedBy: "driver/findings-model.mjs:
|
|
554
|
+
reparsedBy: "driver/findings-model.mjs:844 parseFindingsJson; pipeline applyCoverageFloor legalActions arm derives the delivered disposition from the closed kind enum",
|
|
555
555
|
removedByMove: "NOTHING ON THE #850 PLAN REMOVES THIS",
|
|
556
556
|
},
|
|
557
557
|
{
|
|
@@ -560,7 +560,7 @@ export const E3_BACKLOG = [
|
|
|
560
560
|
where: "driver/stages.mjs:3317",
|
|
561
561
|
surface: "stage-message",
|
|
562
562
|
evidence: "COVERAGE JUDGMENT …: emit \"coverage_judgment\": {\"sufficient\":<bool>, \"reason\":\"<one line…>\"} — EXACTLY those two keys. Do NOT emit \"rows\": the driver writes that register itself … anything you type there is replaced wholesale.",
|
|
563
|
-
reparsedBy: "driver/findings-model.mjs:
|
|
563
|
+
reparsedBy: "driver/findings-model.mjs:844 parseFindingsJson. The \"Do NOT emit rows\" clause is the purest E3 case in the tree — the prompt names a field, dictates its shape and states in the same breath that code overwrites it",
|
|
564
564
|
removedByMove: "NOTHING ON THE #850 PLAN REMOVES THIS",
|
|
565
565
|
},
|
|
566
566
|
{
|
|
@@ -575,7 +575,7 @@ export const E3_BACKLOG = [
|
|
|
575
575
|
// in E3_EVIDENCE_UNRESOLVED instead would have grown the not-checked slice for a row that is
|
|
576
576
|
// perfectly checkable, which is the avoidable coverage loss conversion 9's note names one row up.
|
|
577
577
|
evidence: "SEND ONE \"ask_answers\" ENTRY PER ASK, as a TOP-LEVEL field of the findings record you hand to the call (never as a narrative section — the driver renders the labelled line",
|
|
578
|
-
reparsedBy: "driver/findings-model.mjs:
|
|
578
|
+
reparsedBy: "driver/findings-model.mjs:844 parseFindingsJson (finding_ask_answer_answer_missing); assembleReportMd · buildAskAnswersSection joins on the retyped ask string",
|
|
579
579
|
removedByMove: "NOTHING ON THE #850 PLAN REMOVES THIS",
|
|
580
580
|
},
|
|
581
581
|
// DELETED at conversion 10, recorded rather than absorbed — E3's own rule for a
|
|
@@ -611,7 +611,7 @@ export const E3_BACKLOG = [
|
|
|
611
611
|
where: "driver/stages.mjs:3296",
|
|
612
612
|
surface: "stage-message",
|
|
613
613
|
evidence: "add it to the top-level \"context_notes\" array — each object EXACTLY {\"type\":\"famous-neighbour-ungrounded\",\"mark\",\"owner\",\"context\"}",
|
|
614
|
-
reparsedBy: "driver/findings-model.mjs:
|
|
614
|
+
reparsedBy: "driver/findings-model.mjs:844 parseFindingsJson",
|
|
615
615
|
removedByMove: "NOTHING ON THE #850 PLAN REMOVES THIS",
|
|
616
616
|
},
|
|
617
617
|
{
|
|
@@ -620,7 +620,7 @@ export const E3_BACKLOG = [
|
|
|
620
620
|
where: "driver/stages.mjs:3184",
|
|
621
621
|
surface: "stage-message",
|
|
622
622
|
evidence: "- owner: {\"name\",\"country\",\"registrations\":[...]}. … Each registration: {\"uri\", optionally \"classes\":[\"9\",\"41\"],\"status\",\"filed\",\"expiry\",\"jurisdiction\"}. The \"uri\" is the ONLY field that matters: the driver BINDS classes/status/filed/expiry/jurisdiction AND the owner name from the FETCHED record ke",
|
|
623
|
-
reparsedBy: "driver/findings-model.mjs:
|
|
623
|
+
reparsedBy: "driver/findings-model.mjs:844 parseFindingsJson + the record-binding join. Six of the seven keys are stated in the prompt and overwritten by code in the same sentence",
|
|
624
624
|
removedByMove: "M1 removes the opaque `uri` (ordinal / record-handle selection against the band); the object skeleton and the five overwritten keys survive",
|
|
625
625
|
},
|
|
626
626
|
{
|
package/driver/coverage-form.mjs
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// PASS; ~86% of emitted tokens landed in no artifact, and the attempt that passed is the one that
|
|
14
14
|
// PATCHED). A cold ladder never patches.
|
|
15
15
|
//
|
|
16
|
-
// THE CURE IS
|
|
16
|
+
// THE CURE IS THE ONE APPLIED ONE GATE OVER: the driver writes the row and the model fills in the judgment. The
|
|
17
17
|
// axis, the coverage unit, the open crowd blocks with their qids and hit counts, and the deferred slices
|
|
18
18
|
// with their per-qid receipt reasons are all facts the driver holds BEFORE the stage dispatches. It
|
|
19
19
|
// writes them. The seat sets `status` and `reason`, and nothing else in the file is the seat's to write.
|
|
@@ -856,7 +856,7 @@ export function findCoverageFormViolations(rows, formError = null) {
|
|
|
856
856
|
// kind and nothing in it is a string the model had to copy. parseCoverageLedgerFull survives, but only as
|
|
857
857
|
// the ARCHIVED-RUN reader (loadCoverageLedger's prose fallback): no gate parses this table any more.
|
|
858
858
|
|
|
859
|
-
const cell = (s) => String(s ?? "").replace(/\|/g, "\\|").replace(/\s+/g, " ").trim();
|
|
859
|
+
const cell = (s) => String(s ?? "").replace(/\\/g, "\\\\").replace(/\|/g, "\\|").replace(/\s+/g, " ").trim();
|
|
860
860
|
|
|
861
861
|
/** The `## Coverage ledger` section, rendered from the form's rows. "" when nothing is settled. PURE. */
|
|
862
862
|
export function renderCoverageLedgerSection(rows) {
|
|
@@ -97,6 +97,30 @@ export function prepareSample(dir, { repoRoot, tmp } = {}) {
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
|
|
100
|
+
/**
|
|
101
|
+
* THE COPIES GO WHEN THEIR PUBLISH IS DONE, AND AT EXIT WHATEVER HAPPENED.
|
|
102
|
+
*
|
|
103
|
+
* Every copy below is made in the system temp directory and nothing removed it: four per `demo --once`,
|
|
104
|
+
* one per demo start, some a full replayed run of about 1,850 files, and 36 GB of them on one machine
|
|
105
|
+
* (measured 2026-09-19). Each root is recorded as it is made. A caller releases them the moment its
|
|
106
|
+
* publish is done, and an exit hook removes whatever is left, on success and on failure. A caller that
|
|
107
|
+
* can be stopped by a signal turns the signal into an exit, so the hook runs then too.
|
|
108
|
+
*/
|
|
109
|
+
const copies = new Set();
|
|
110
|
+
let exitHooked = false;
|
|
111
|
+
function tracked(root) {
|
|
112
|
+
copies.add(root);
|
|
113
|
+
if (!exitHooked) { exitHooked = true; process.once("exit", releaseDemoCopies); }
|
|
114
|
+
return root;
|
|
115
|
+
}
|
|
116
|
+
/** Remove every copy made so far. Safe to call more than once. */
|
|
117
|
+
export function releaseDemoCopies() {
|
|
118
|
+
for (const d of [...copies]) {
|
|
119
|
+
try { rmSync(d, { recursive: true, force: true }); } catch { /* going anyway; the exit hook tries again */ }
|
|
120
|
+
copies.delete(d);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
100
124
|
/**
|
|
101
125
|
* The directory a frozen demo should be PUBLISHED from — itself, or a copy when it is part of this tree.
|
|
102
126
|
*
|
|
@@ -117,7 +141,7 @@ export function publishSource(dir, { repoRoot, tmp = tmpdir() } = {}) {
|
|
|
117
141
|
const root = resolve(repoRoot ?? "");
|
|
118
142
|
const here = resolve(dir);
|
|
119
143
|
if (!root || !(here === root || here.startsWith(root + sep))) return dir;
|
|
120
|
-
const copy = join(mkdtempSync(join(tmp, "clearotron-demo-")), "sample");
|
|
144
|
+
const copy = join(tracked(mkdtempSync(join(tmp, "clearotron-demo-"))), "sample");
|
|
121
145
|
cpSync(here, copy, { recursive: true });
|
|
122
146
|
return copy;
|
|
123
147
|
}
|
|
@@ -135,7 +159,7 @@ export function publishContainer(root, { repoRoot, tmp = tmpdir() } = {}) {
|
|
|
135
159
|
const repo = resolve(repoRoot ?? "");
|
|
136
160
|
const here = resolve(root);
|
|
137
161
|
if (!repo || !(here === repo || here.startsWith(repo + sep))) return { dir: root, unusable: demoInventory(root).unusable };
|
|
138
|
-
const copy = join(mkdtempSync(join(tmp, "clearotron-demo-")), "sample");
|
|
162
|
+
const copy = join(tracked(mkdtempSync(join(tmp, "clearotron-demo-"))), "sample");
|
|
139
163
|
mkdirSync(copy, { recursive: true });
|
|
140
164
|
const { children, unusable } = demoInventory(here);
|
|
141
165
|
const left = [...unusable];
|
|
@@ -288,8 +288,8 @@ export function callsSoFar(dispositionsPath) {
|
|
|
288
288
|
export { PARK_AFTER_REFUSALS };
|
|
289
289
|
|
|
290
290
|
/**
|
|
291
|
-
* Which rows have been refused past the bound. PURE, and counted PER ROW ACROSS CALLS — the unit
|
|
292
|
-
* own banner records an earlier investigation dying on, when it joined per-row-final-state instead.
|
|
291
|
+
* Which rows have been refused past the bound. PURE, and counted PER ROW ACROSS CALLS — the unit this
|
|
292
|
+
* module's own banner records an earlier investigation dying on, when it joined per-row-final-state instead.
|
|
293
293
|
*
|
|
294
294
|
* A row that is ruled later is never re-parked, because the union only parks rows that are not ruled.
|
|
295
295
|
* @returns {string[]} row_ids, sorted, so the same ledger always yields the same answer.
|
|
@@ -145,7 +145,7 @@ Stages must name **abstract tiers**, not provider aliases. Per-engine maps:
|
|
|
145
145
|
| `sweep` | register-unit, case-law, skeptic, report-overview, report-card | `sonnet` | `$CLEAROTRON_OPENAI_MODEL_SWEEP` |
|
|
146
146
|
| `cheap` | saturation-probe | `haiku` | `$CLEAROTRON_OPENAI_MODEL_CHEAP` |
|
|
147
147
|
|
|
148
|
-
**AN UNHONOURED OVERRIDE IS AN ERROR, NOT A SUBSTITUTION** (
|
|
148
|
+
**AN UNHONOURED OVERRIDE IS AN ERROR, NOT A SUBSTITUTION** (2026-08-03). This
|
|
149
149
|
section used to declare two further tiers — `skeptic` → `google/gemini-3-flash-preview` and
|
|
150
150
|
`refutation` → `together/deepseek-ai/DeepSeek-V4-Pro` — each substituting an anthropic model on the
|
|
151
151
|
anthropic engine, "grade-moving, validated only in the paid A/B". The substitution never was validated
|
|
@@ -191,7 +191,7 @@ disarms the refusal and never the record.
|
|
|
191
191
|
| `max` | `max` | `xhigh` |
|
|
192
192
|
|
|
193
193
|
`off` used to map to `minimal` on codex and `low` on claude — a whole rung apart at the bottom, so a
|
|
194
|
-
cross-engine effort comparison at `off` was off by one
|
|
194
|
+
cross-engine effort comparison at `off` was off by one. `low` is the anthropic
|
|
195
195
|
floor, so codex came up to it and codex's `minimal` is deliberately unreachable from the driver's tier
|
|
196
196
|
vocabulary. `max` is the one sanctioned divergence: it means "this engine's top rung", and codex has no
|
|
197
197
|
`max`. `engine.anthropic.test.mjs` pins the two tables together and asserts that single exception.
|
|
@@ -200,7 +200,7 @@ vocabulary. `max` is the one sanctioned divergence: it means "this engine's top
|
|
|
200
200
|
(`assertEffectiveTier`), on the EFFECTIVE tier rather than the declared one: Haiku 4.5 rejects adaptive
|
|
201
201
|
thinking and the request bounces to sonnet, so the pairing measures sonnet and records haiku. Both ways
|
|
202
202
|
it can arise are runtime overrides that never touch the stage table (`CLEAROTRON_STAGE_THINKING`, an
|
|
203
|
-
`--experiment --model`), which is why the start-of-run scan alone could not see it
|
|
203
|
+
`--experiment --model`), which is why the start-of-run scan alone could not see it.
|
|
204
204
|
|
|
205
205
|
## 4. `claude -p` invocation (anthropic-agent)
|
|
206
206
|
|
|
@@ -212,7 +212,7 @@ const RECORDING = Object.freeze({
|
|
|
212
212
|
// SECOND — skeptic, same shape ( is the template). Its OWN key, so blind-frame's seat is never
|
|
213
213
|
// handed record_skeptic and skeptic is never handed record_blind_frame.
|
|
214
214
|
//
|
|
215
|
-
// skeptic's key carries a SECOND tool: search_run_artifacts, the sanctioned read surface (
|
|
215
|
+
// skeptic's key carries a SECOND tool: search_run_artifacts, the sanctioned read surface (the
|
|
216
216
|
// ratification hold, unlock path 1). A READ over the run's own tree, not retrieval — it dials nothing,
|
|
217
217
|
// writes nothing, and is bounded to CLEAROTRON_BAND_RUN_DIR by construction (skeptic-search.mjs). It
|
|
218
218
|
// replaces the artifact half of the Bash reads O3c measured; the skill-doc half was already the seeded
|
|
@@ -60,8 +60,8 @@ const codexBin = () => { const r = resolveEngineProgram("openai-agent"); return
|
|
|
60
60
|
// 3 stage families, and BYTE-IDENTICALLY on retry. That was true of that code and that CLI.
|
|
61
61
|
//
|
|
62
62
|
// What has changed is not the measurement but everything around it: three weeks of stage-contract
|
|
63
|
-
// work, and a codex CLI that has moved several minor versions. The ruling above supersedes 's
|
|
64
|
-
// conclusion; it does not erase its evidence. A reader who finds first must be able to get here.
|
|
63
|
+
// work, and a codex CLI that has moved several minor versions. The ruling above supersedes the measurement's
|
|
64
|
+
// conclusion; it does not erase its evidence. A reader who finds the measurement first must be able to get here.
|
|
65
65
|
//
|
|
66
66
|
// ── HOW A REGRESSION ANNOUNCES ITSELF — no experiment is scheduled, and none is needed ────────────
|
|
67
67
|
//
|
|
@@ -578,6 +578,18 @@ const clipClause = (s, max) => {
|
|
|
578
578
|
* count authority and the fallback. Returns null when either axis is missing (legacy sidecars —
|
|
579
579
|
* callers fall back to today's bare word). PURE.
|
|
580
580
|
*/
|
|
581
|
+
/**
|
|
582
|
+
* The conditions a client is actually told a CONDITIONAL verdict rests on: the client-voice clauses, and
|
|
583
|
+
* the reasons as their fallback. A clause stored as explicit null is a condition ruled to the run record
|
|
584
|
+
* alone, so its reason is not stated either; `undefined` (legacy/short) is not null. PURE.
|
|
585
|
+
*/
|
|
586
|
+
export function statedConditions({ reasons, clauses } = {}) {
|
|
587
|
+
const cs = Array.isArray(clauses) ? clauses : [];
|
|
588
|
+
const conds = (Array.isArray(reasons) ? reasons : []).filter((r, i) => cs[i] !== null).map((r) => String(r ?? "").trim()).filter(Boolean);
|
|
589
|
+
const cls = cs.map((c) => String(c ?? "").trim()).filter(Boolean);
|
|
590
|
+
return { conds, cls };
|
|
591
|
+
}
|
|
592
|
+
|
|
581
593
|
export function riskStatement({ tier, verdict, reasons, basis, clauses } = {}) {
|
|
582
594
|
const v = String(verdict || "").toUpperCase();
|
|
583
595
|
const t = String(tier || "").trim();
|
|
@@ -592,10 +604,13 @@ export function riskStatement({ tier, verdict, reasons, basis, clauses } = {}) {
|
|
|
592
604
|
if (v === "CONDITIONAL") {
|
|
593
605
|
// A clause stored as explicit null is a condition ruled to the run record alone: it is not the lede,
|
|
594
606
|
// it is not counted, and its reason is never the fallback text. `undefined` (legacy/short) is not null.
|
|
595
|
-
const
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
607
|
+
const { conds, cls } = statedConditions({ reasons, clauses });
|
|
608
|
+
// NOTHING STATED, NO "CONDITIONAL ON:" LINE (owner, 2026-09-19). When every condition is ruled to
|
|
609
|
+
// the run record, the line had nothing behind it and fell back to "the open conditions carried in the
|
|
610
|
+
// report": a conditional with no condition. The band word stands alone, and `stance` still says
|
|
611
|
+
// conditional to every consumer that keys on it.
|
|
612
|
+
if (!cls.length && !conds.length) return `${t}${basisNote ? `.${basisNote}` : ""}`;
|
|
613
|
+
const lede = cls[0] ?? conds[0];
|
|
599
614
|
const first = clipClause(sentenceCaseLead(lede), STATEMENT_CLAUSE_MAX);
|
|
600
615
|
const n = conds.length || cls.length;
|
|
601
616
|
const more = n > 1 ? ` (and ${n - 1} more)` : "";
|
|
@@ -1228,7 +1243,7 @@ export function parseFindingsJsonLenient(raw, opts = {}) {
|
|
|
1228
1243
|
// and drops the others, so a run's entire non-Latin field can collapse into one finding whose prose
|
|
1229
1244
|
// describes one of them.
|
|
1230
1245
|
//
|
|
1231
|
-
// The fix
|
|
1246
|
+
// The fix, at a third site: keep the Latin normalisation exactly as it was where it produces
|
|
1232
1247
|
// anything, and fall back to the RAW value (NFKC-folded, so full-width and compatibility forms of the
|
|
1233
1248
|
// same characters still key alike) where it does not. Latin keys are byte-identical to before — pinned
|
|
1234
1249
|
// by test, because a consolidation key that moved would re-merge every archived finding differently.
|
|
@@ -1721,7 +1736,7 @@ function validateNet(f, ord, mode) {
|
|
|
1721
1736
|
// (publish/index.mjs strict-parses the archived findings.json), and a delivered matter would stop
|
|
1722
1737
|
// republishing. So it engages at v7 and archived records parse byte-identically, forever.
|
|
1723
1738
|
//
|
|
1724
|
-
// THE LENIENT PATH IS EXEMPT, for validateNetRequired's reason
|
|
1739
|
+
// THE LENIENT PATH IS EXEMPT, for validateNetRequired's reason and one of its own. Dropping a
|
|
1725
1740
|
// finding because its sentence is shaped wrong would delete a real conflict over a punctuation mark —
|
|
1726
1741
|
// silence arrived at by enforcing a rule about clarity. It also keeps the token out of pipeline.mjs's A3
|
|
1727
1742
|
// per-finding salvage lane, which has nothing to salvage here (see the token's own note below).
|
package/driver/gateway.mjs
CHANGED
|
@@ -1146,7 +1146,7 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
|
|
|
1146
1146
|
// `resolveModel(model)` / `engine.resolveModelId(model)`, computed here without ever looking at the
|
|
1147
1147
|
// provider's answer. So `--model gemini` logged `google/gemini-3.1-pro-preview` and ran sonnet (the
|
|
1148
1148
|
// engine's alias table substituted, silently), a haiku stage that bounced to sonnet logged haiku,
|
|
1149
|
-
// and every attribution downstream — the A/B arm's manifest, tokens.mjs' per-model rollup,
|
|
1149
|
+
// and every attribution downstream — the A/B arm's manifest, tokens.mjs' per-model rollup, the
|
|
1150
1150
|
// billing classes — was keyed to a model that had not run. Patching the log would not have fixed
|
|
1151
1151
|
// it; the log was already saying what the code believed.
|
|
1152
1152
|
//
|
package/driver/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "clearotron-driver",
|
|
3
3
|
"private": true,
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "0.3.2-beta.
|
|
5
|
+
"version": "0.3.2-beta.13",
|
|
6
6
|
"license": "AGPL-3.0-only",
|
|
7
7
|
"description": "Deterministic driver for the trademark clearance workflow: orchestration in code (fan-out, fan-in barrier, gating, retries); the model does judgment leaves only, through a reasoning CLI spawned per stage.",
|
|
8
8
|
"engines": {
|
|
@@ -540,7 +540,7 @@ export async function knockoutInner(ctx, job, opts = {}) {
|
|
|
540
540
|
//
|
|
541
541
|
// ONLY when the counter is the real provider. On fixtures or an injected counter the offices this
|
|
542
542
|
// deployment can reach are not what the lane is talking to, and splitting on them would defer a
|
|
543
|
-
// register the fixture holds — a $0 lane reporting a gap it does not have (the shape
|
|
543
|
+
// register the fixture holds — a $0 lane reporting a gap it does not have (the shape a
|
|
544
544
|
// sibling defect had, and the reason `missing` carries the same guard).
|
|
545
545
|
const unreachable = countExec.source === "provider" ? registerUnavailableOffices(caps) : [];
|
|
546
546
|
ctx.registerUnreachable = unreachable;
|
package/driver/pipeline.mjs
CHANGED
|
@@ -4818,7 +4818,7 @@ export function terminalReasonFields(reason, { cap = 200, fullCap = 4000 } = {})
|
|
|
4818
4818
|
// `_driver/failure.json` carried reason/reasonVerbatim/failClass/terminalKind and no payload at all, so the
|
|
4819
4819
|
// operator was told one query had dropped and never which one — E2E had to replay
|
|
4820
4820
|
// findDroppedConnotationQueries against the preserved artifacts to name it. `reasonDetail` already reached
|
|
4821
|
-
// status.json
|
|
4821
|
+
// status.json; `reasonQuantity` reached neither file.
|
|
4822
4822
|
//
|
|
4823
4823
|
// Both are written UNCONDITIONALLY as string|null and number|null. Never omitted, never 0: "no key" and
|
|
4824
4824
|
// "this failure carries no count" are different facts, the same distinction `reasonTruncated` is written on
|
|
@@ -7929,7 +7929,7 @@ export function buildAskAnswersSection(askAnswers, intakeAsks) { // @internal
|
|
|
7929
7929
|
* change to the delivered report nobody asked for. So the divergence is now one-country-only and owned,
|
|
7930
7930
|
* rather than general and deferred.
|
|
7931
7931
|
*
|
|
7932
|
-
* This section itself is untouched by the ladder: the grouping is not extended and not re-keyed.
|
|
7932
|
+
* This section itself is untouched by the ladder: the grouping is not extended and not re-keyed. The
|
|
7933
7933
|
* graded entries render in a SIBLING section (buildGradedEntriesSection) using the same line grammar.
|
|
7934
7934
|
*/
|
|
7935
7935
|
export function buildReasonedNegativesSection(findings) { // @internal
|
|
@@ -15696,8 +15696,8 @@ async function pipelineInner(job, opts = {}) {
|
|
|
15696
15696
|
// — `reasonDetail` rides beside `reason`, never inside it. abbrev, not a bare slice: a cut that
|
|
15697
15697
|
// reads as a word is worse than one that reads as a cut, and this one is read by an engineer
|
|
15698
15698
|
// chasing a defect.
|
|
15699
|
-
// — one spelling of the payload for BOTH sinks. status.json already carried `reasonDetail
|
|
15700
|
-
//
|
|
15699
|
+
// — one spelling of the payload for BOTH sinks. status.json already carried `reasonDetail`;
|
|
15700
|
+
// `_driver/failure.json` carried neither it nor the count, and `reasonQuantity` reached
|
|
15701
15701
|
// no file at all — so the operator read "dropped 1 dictated connotation query" and had to replay the
|
|
15702
15702
|
// gate against the preserved artifacts to learn which one. Derived once here and passed to both
|
|
15703
15703
|
// writers, so the notice and the status file can never disagree about the payload.
|
|
@@ -68,7 +68,7 @@ function indexRows(rows, { bySelection = true, uriLeg = true } = {}) {
|
|
|
68
68
|
/**
|
|
69
69
|
* Union the seat's tiers into what the driver already holds.
|
|
70
70
|
*
|
|
71
|
-
* THE RULE, and why it is that way round — identical to
|
|
71
|
+
* THE RULE, and why it is that way round — identical to coverage-union.mjs and disposition-union.mjs, for identical reasons:
|
|
72
72
|
* · a SUBMITTED row the gate would accept wins, so the seat can CORRECT itself;
|
|
73
73
|
* · otherwise a PRIOR row the gate would accept is kept, so a killed or cold attempt cannot destroy
|
|
74
74
|
* work already done;
|
|
@@ -76,7 +76,7 @@ function post(urlStr, { headers = {}, body = "", wantId = null, timeoutMs = 3000
|
|
|
76
76
|
data += c;
|
|
77
77
|
// SSE early-exit: once the frame with our id is complete, we have the answer — stop reading
|
|
78
78
|
if (wantId != null && String(res.headers["content-type"] ?? "").includes("text/event-stream")
|
|
79
|
-
&& new RegExp(`"id"
|
|
79
|
+
&& new RegExp(`"id"\\s*:\\s*${wantId}`).test(data) && /\n\n/.test(data)) { res.destroy(); done(); }
|
|
80
80
|
});
|
|
81
81
|
res.on("end", done);
|
|
82
82
|
res.on("close", done);
|
package/driver/portal-report.mjs
CHANGED
|
@@ -850,7 +850,10 @@ export function sectionsOf(html) {
|
|
|
850
850
|
if (!nav) return [];
|
|
851
851
|
const out = [];
|
|
852
852
|
for (const m of nav[1].matchAll(STRIP_LINK_RE)) {
|
|
853
|
-
|
|
853
|
+
// Stripped until nothing changes, so a tag that a first pass reassembles cannot survive into the label.
|
|
854
|
+
let label = m[2];
|
|
855
|
+
for (let prev = null; prev !== label;) { prev = label; label = label.replace(/<[^>]*>/g, ""); }
|
|
856
|
+
label = label.replace(/\s+/g, " ").trim();
|
|
854
857
|
if (m[1] && label) out.push({ id: m[1], label });
|
|
855
858
|
}
|
|
856
859
|
return out;
|
|
@@ -179,6 +179,9 @@ import { accessAudience, audienceLabel } from "../shared/access-audience.mjs";
|
|
|
179
179
|
import { resolveNumericSetting } from "./numeric-setting.mjs"; // — the same table the engine enforces, without the throw a rendering surface must not take
|
|
180
180
|
import { isEntrypoint } from "../shared/is-entrypoint.mjs"; // — one entry-point test, all spellings
|
|
181
181
|
import { deploymentBox } from "../shared/deployment-box.mjs"; // — the box names itself; one rule
|
|
182
|
+
import { stageNow } from "./progress.mjs"; // the stage a live run is in now, for the card and the row
|
|
183
|
+
// A run in one of these has finished; its step is the one its terminal write set, not the stage it last entered.
|
|
184
|
+
const TERMINAL_STATES = new Set(["delivered", "failed", "cancelled"]);
|
|
182
185
|
|
|
183
186
|
// The offering the portal menus build from — product-rows.mjs's row, unchanged.
|
|
184
187
|
//
|
|
@@ -384,7 +387,7 @@ export function scanAccountRuns({ poolRoot, workspaceRoot, account = null, gener
|
|
|
384
387
|
stageLabel: stageLabelOf(meta.searchLevel) ?? meta.stageLabel ?? null,
|
|
385
388
|
productName: productNameOf(meta.searchLevel),
|
|
386
389
|
state: "delivered", date: meta.date ?? null,
|
|
387
|
-
// WHEN IT FINISHED, to the second
|
|
390
|
+
// WHEN IT FINISHED, to the second. `date` is DAY precision and always was — it is
|
|
388
391
|
// parsed out of the run directory name. Two reads of one mark delivered on the same day
|
|
389
392
|
// therefore TIE, Array.prototype.sort is stable, and which one the parent row spoke for was
|
|
390
393
|
// decided by readdirSync order rather than by recency.
|
|
@@ -515,7 +518,11 @@ export function scanAccountRuns({ poolRoot, workspaceRoot, account = null, gener
|
|
|
515
518
|
resetsAt: typeof s.resetsAt === "string" ? s.resetsAt : null,
|
|
516
519
|
// Elapsed, so a card can say how long this has been going without inventing a finish time.
|
|
517
520
|
startedAt: typeof s.startedAt === "string" ? s.startedAt : null,
|
|
518
|
-
|
|
521
|
+
// THE STAGE IT IS IN NOW, a step back into an earlier stage included (owner, 2026-09-19): see
|
|
522
|
+
// stageNow. Only while it is live; a run that has finished keeps the step its terminal write set.
|
|
523
|
+
...(TERMINAL_STATES.has(s.state)
|
|
524
|
+
? { step: s.stepLabel ?? null, stepN: s.stepN ?? null, stepTotal: s.stepTotal ?? null }
|
|
525
|
+
: stageNow(s)),
|
|
519
526
|
// — a requested stop is a state the screen shows. Stamped by stop_run,
|
|
520
527
|
// preserved by writeRunStatus's spread-merge, replaced by the terminal when the honour check
|
|
521
528
|
// fires. The UI derives "Stopping…" from this beside a non-terminal state.
|
|
@@ -867,7 +874,7 @@ export const SERVER_ROUTE_HEADS = Object.freeze(["api", "report", "admin", "heal
|
|
|
867
874
|
* `msgId` and `conversationId` sat in this list while `jobFor` hardcoded both to null and the trigger
|
|
868
875
|
* hop stripped them off again. The totality test beside it could not see that: it asks whether every
|
|
869
876
|
* declared field is classified, which a declared-and-nulled field satisfies perfectly. So the sentence
|
|
870
|
-
* above asserted a behaviour and the test next to it checked a different one — the same shape as
|
|
877
|
+
* above asserted a behaviour and the test next to it checked a different one — the same shape as
|
|
871
878
|
* "an invitation nothing honours", one level up.
|
|
872
879
|
*
|
|
873
880
|
* `stamped` exists so the new guard can be DERIVED rather than hand-listed. It splits `carries` into the
|
|
@@ -2928,7 +2935,7 @@ async function connectorDoorKind(url) {
|
|
|
2928
2935
|
// row, and a retired-endpoint row is a different fact from a not-yours row. If an old open tab is
|
|
2929
2936
|
// still clicking Flag, that is a thing worth being able to count.
|
|
2930
2937
|
//
|
|
2931
|
-
// WHAT IS DELIBERATELY STILL BELOW THIS LINE: the whole resolver — the two-lane shape,
|
|
2938
|
+
// WHAT IS DELIBERATELY STILL BELOW THIS LINE: the whole resolver — the two-lane shape, the
|
|
2932
2939
|
// disposition read, the server-side-locator rule. The owner ruled "disable services and any code",
|
|
2933
2940
|
// and the acceptance criterion says re-enabling must be a switch and not a rebuild. Deleting 130
|
|
2934
2941
|
// lines of hard-won doctrine would have made it a rebuild.
|
|
@@ -3759,7 +3766,12 @@ function doctrineStore(now = Date.now) {
|
|
|
3759
3766
|
} catch (e) {
|
|
3760
3767
|
// NOT `{head: null}` alone. A null head with no reason reads as "no store", which is a legal and
|
|
3761
3768
|
// healthy state (`no-overlay`); this is a different thing and has to say so.
|
|
3762
|
-
|
|
3769
|
+
//
|
|
3770
|
+
// THE ERROR'S OWN WORDS GO TO THE SERVICE LOG, NOT THE WIRE. This answer is served by the health
|
|
3771
|
+
// route, and an error message can carry the store's filesystem path. `unreadable` says what the
|
|
3772
|
+
// caller needs; the operator reads why in the log.
|
|
3773
|
+
console.error(`[portal] the instructions store could not be read: ${String(e?.message ?? e).slice(0, 200)}`);
|
|
3774
|
+
value = { head: null, situation: "unreadable", outcome: "blocked" };
|
|
3763
3775
|
}
|
|
3764
3776
|
storeCache = { at: t, value };
|
|
3765
3777
|
return value;
|
|
@@ -30,7 +30,7 @@ import { partyFactSources, partyFactViolations, partyFactMessage, canJudgePartyF
|
|
|
30
30
|
import { writeUpViolations, writeUpMessage } from "./narrative-write-ups.mjs"; //
|
|
31
31
|
|
|
32
32
|
import { findRegistryArithmeticIssues, findRegistryViolations, splitBlocks } from "./registry-fidelity.mjs";
|
|
33
|
-
import { CLIENT_TIER_BY_COMPOSITE, joinFindingToBlock, parseBlockOrd, worstLiveBand, NO_RATED_CONFLICTS, deriveActionConditions, isUnconditionalProceed, verdictStance, joinAskToAnswer, projectAssessmentField, POSITION_REQUIRED_DISPOSITIONS, OFF_FIELD_GROUNDS, FINDINGS_SCHEMA_VERSION, netChainMarkers, STATEMENT_CLAUSE_MAX } from "./findings-model.mjs";
|
|
33
|
+
import { CLIENT_TIER_BY_COMPOSITE, joinFindingToBlock, parseBlockOrd, worstLiveBand, NO_RATED_CONFLICTS, deriveActionConditions, isUnconditionalProceed, verdictStance, statedConditions, joinAskToAnswer, projectAssessmentField, POSITION_REQUIRED_DISPOSITIONS, OFF_FIELD_GROUNDS, FINDINGS_SCHEMA_VERSION, netChainMarkers, STATEMENT_CLAUSE_MAX } from "./findings-model.mjs";
|
|
34
34
|
import { normalizeBand } from "./framework.mjs";
|
|
35
35
|
import { clientConditions, unrenderableConditions, ENGINE_TOKEN_RE } from "./terminal-clamp.mjs"; // the reader's clause per condition, and the token shape it may never carry
|
|
36
36
|
import { knockoutNoteView, REQUEST_NOTE_WORDS, REQUEST_SUBJECT_WORDS } from "./findings-model.mjs"; // one reader for where a note prints
|
|
@@ -87,7 +87,7 @@ const REF_ALLOWLIST = new Set([
|
|
|
87
87
|
const TERRITORY_VOCAB = new Set(Object.values(REGION_NAMES).map((n) => norm(n)));
|
|
88
88
|
|
|
89
89
|
function stripHtml(s) {
|
|
90
|
-
return String(s ?? "").replace(/<[^>]+>/g, " ").replace(/&
|
|
90
|
+
return String(s ?? "").replace(/<[^>]+>/g, " ").replace(/ /g, " ").replace(/&/g, "&");
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
// "April 2014", "MAY 2026" — dates pass the Title-Case-multi-word shape but are never entities.
|
|
@@ -2023,7 +2023,12 @@ export function statementCoherenceChecks({ verdictDoc }) {
|
|
|
2023
2023
|
stanceOk ? "" : `verdict.json stance "${verdictDoc.stance}" does not match the verdict ${v || "(missing)"} (expected "${expected ?? "?"}") — the sidecar fields diverged; re-derive it (writeVerdictSidecar composes both from one record)`);
|
|
2024
2024
|
if (!c3.pass) c3.structural = true;
|
|
2025
2025
|
out.push(c3);
|
|
2026
|
-
|
|
2026
|
+
// The form is owed only where something is stated: a conditional whose every condition was ruled to
|
|
2027
|
+
// the run record carries the band word alone (riskStatement), and a "conditional on:" there would be
|
|
2028
|
+
// the empty line the owner ruled off the page.
|
|
2029
|
+
const stated = statedConditions({ reasons: verdictDoc.reasons, clauses: verdictDoc.clauses });
|
|
2030
|
+
const nothingStated = !stated.cls.length && !stated.conds.length;
|
|
2031
|
+
const formOk = verdictDoc.stance !== "conditional" || nothingStated || /—\s*conditional on:/.test(st);
|
|
2027
2032
|
const c4 = check("statement-conditional-form", "verdict", "report", formOk,
|
|
2028
2033
|
formOk ? "" : `a conditional-stance statement must state what conditions reliance ("<Tier> — conditional on: <facts>"), but reads "${st.slice(0, 80)}" — re-derive the sidecar`);
|
|
2029
2034
|
if (!c4.pass) c4.structural = true;
|
|
@@ -2339,7 +2344,7 @@ export function runLint({ depth, commonLawGrid, matterContext, clientPartyName,
|
|
|
2339
2344
|
// ── The KNOCKOUT lane's applicable subset (2026-07-31) ──────────────────────────────────────────────
|
|
2340
2345
|
// The knockout (Stage 0/0.5) lane wrote NO predelivery-lint.json at all, by a decision recorded in
|
|
2341
2346
|
// docs/DELIVERY.md (2026-07-28). That decision rested on two premises this tranche has since moved:
|
|
2342
|
-
// the lint artifact is now the WORKBOOK's QC record
|
|
2347
|
+
// the lint artifact is now the WORKBOOK's QC record, so a lane that writes none produces an
|
|
2343
2348
|
// EMPTY QC record rather than a deliberately-absent one; and the A10 projects recorded defects onto
|
|
2344
2349
|
// the cover note that reaches the reviewing lawyer, so a knockout defect reached nobody. What has NOT
|
|
2345
2350
|
// moved is the other half of the memo: this lane's deliverable is store-rendered, and most clearance
|