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
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
import { readFileSync, writeFileSync, renameSync, existsSync, mkdirSync } from "node:fs";
|
|
31
31
|
import { dirname, join } from "node:path";
|
|
32
32
|
import { driverDir } from "../../../shared/driver-dir.mjs"; //
|
|
33
|
-
import { PLAN_PREDICATES, PLAN_MAX_OR_WIDTH, PLAN_MAX_NAME_LENGTH, fingerprint, ownerIntersectionGap, resolveRegions } from "../../register-plan.mjs";
|
|
33
|
+
import { PLAN_PREDICATES, PLAN_MAX_OR_WIDTH, PLAN_MAX_NAME_LENGTH, fingerprint, ownerIntersectionGap, resolveRegions, houseElementOf, withoutHouseElementTerms } from "../../register-plan.mjs";
|
|
34
34
|
import { entryTermIssues } from "../../../providers/_shared/term-shape.mjs";
|
|
35
35
|
import { isNonLatinTerm, romanizationRefusal, romanizationSpellings, nativeScriptIndexGap } from "../../../providers/_shared/script-form.mjs";
|
|
36
36
|
|
|
@@ -289,9 +289,10 @@ export async function proposeSupplemental(params, tctx, deps) {
|
|
|
289
289
|
// execute-plan kernel hands it to the provider's buildEntryQuery, which backfills only entries that
|
|
290
290
|
// declare none (makeRegionRequiredBuildEntryQuery). A proposal that DOES declare regions is
|
|
291
291
|
// untouched, qid fingerprints are unchanged, and providers that do not require regions ignore it.
|
|
292
|
-
let planRegions = [], planClasses = [];
|
|
292
|
+
let planRegions = [], planClasses = [], house = null;
|
|
293
293
|
try {
|
|
294
294
|
const frozen = JSON.parse(readFileSync(driverDir(dirname(dirname(outPath)), "register-plan.json"), "utf8"));
|
|
295
|
+
house = houseElementOf(frozen);
|
|
295
296
|
planRegions = (Array.isArray(frozen?.regions) ? frozen.regions : []).map((r) => String(r).trim()).filter(Boolean);
|
|
296
297
|
// C3 — the frozen plan's own class list is the priority set: proposals intersecting it compete
|
|
297
298
|
// for the per-call/per-axis caps first (mintSupplementalEntries stable-sorts; values unchanged).
|
|
@@ -307,8 +308,24 @@ export async function proposeSupplemental(params, tctx, deps) {
|
|
|
307
308
|
}
|
|
308
309
|
const perCall = 12; // step 3 — was a knob; no environment ever set it
|
|
309
310
|
const axisMax = 24; // step 3 — was a knob; no environment ever set it
|
|
311
|
+
// ── THE CLIENT'S OWN ELEMENT IS LEFT OUT BEFORE ANYTHING RUNS ───────────────────────────────────
|
|
312
|
+
//
|
|
313
|
+
// This tool EXECUTES what it mints, before the fold adds it to the plan, so the fold's own exclusion
|
|
314
|
+
// (`houseElementOf` in register-plan.mjs) comes too late here: the query would already have run. On
|
|
315
|
+
// the first live run that proposed an exclusion, this session proposed an exact search on the bare
|
|
316
|
+
// house element the compile had set aside. The same rule is applied here, before the mint. Not a
|
|
317
|
+
// rejection: rejected[] becomes an OPEN ask on the report, and the element is not an open question.
|
|
318
|
+
const excludedHouse = [];
|
|
319
|
+
const offered = [];
|
|
320
|
+
for (const p of proposals) {
|
|
321
|
+
const kept = house ? withoutHouseElementTerms({ ...(p ?? {}), predicate: String(p?.predicate ?? "default") }, house) : p;
|
|
322
|
+
if (!kept) { excludedHouse.push(String(p?.term ?? p?.terms?.[0] ?? "")); continue; }
|
|
323
|
+
offered.push(kept === p || !Array.isArray(kept.terms) ? p : { ...p, terms: kept.terms });
|
|
324
|
+
}
|
|
325
|
+
if (!offered.length)
|
|
326
|
+
return { type: "text", text: JSON.stringify({ minted: [], reused: [], rejected: [], excluded_house_element: excludedHouse, executed: false }, null, 2) };
|
|
310
327
|
const existingQids = new Set(supp.entries.map((e) => e.qid));
|
|
311
|
-
const { minted, reused, rejected, enriched, narrowed } = mintSupplementalEntries(axis,
|
|
328
|
+
const { minted, reused, rejected, enriched, narrowed } = mintSupplementalEntries(axis, offered,
|
|
312
329
|
{ existingQids, perCall, axisMax, existingCount: supp.entries.length, capabilities: deps.capabilities ?? null, priorityClasses: planClasses });
|
|
313
330
|
|
|
314
331
|
// Field-level romanisation enrichment of a REUSED qid (2026-07-30 review round): the natural retry —
|
|
@@ -373,7 +390,7 @@ export async function proposeSupplemental(params, tctx, deps) {
|
|
|
373
390
|
const r = await executePlan({ plan_path: suppPath, axis, output_path: outPath, qids }, tctx);
|
|
374
391
|
const text = r && typeof r === "object" ? (r.text ?? "") : String(r ?? "");
|
|
375
392
|
if (!text || text.startsWith("ERROR")) {
|
|
376
|
-
return { type: "text", text: JSON.stringify({ minted: minted.map((e) => e.qid), reused, ...(enrichedQids.length ? { enriched: enrichedQids } : {}), ...(conflicts.length ? { conflict: conflicts } : {}), rejected, ...(narrowed.length ? { narrowed } : {}), executed: false, error: text.slice(0, 300) || "executor returned nothing" }, null, 2) };
|
|
393
|
+
return { type: "text", text: JSON.stringify({ minted: minted.map((e) => e.qid), reused, ...(enrichedQids.length ? { enriched: enrichedQids } : {}), ...(conflicts.length ? { conflict: conflicts } : {}), rejected, ...(excludedHouse.length ? { excluded_house_element: excludedHouse } : {}), ...(narrowed.length ? { narrowed } : {}), executed: false, error: text.slice(0, 300) || "executor returned nothing" }, null, 2) };
|
|
377
394
|
}
|
|
378
395
|
try { summary = JSON.parse(text); } catch { summary = { raw: text.slice(0, 300) }; }
|
|
379
396
|
}
|
|
@@ -398,5 +415,5 @@ export async function proposeSupplemental(params, tctx, deps) {
|
|
|
398
415
|
}
|
|
399
416
|
} catch { /* band unreadable — the executor summary still crosses */ }
|
|
400
417
|
|
|
401
|
-
return { type: "text", text: JSON.stringify({ minted: minted.map((e) => e.qid), reused, ...(enrichedQids.length ? { enriched: enrichedQids } : {}), ...(conflicts.length ? { conflict: conflicts } : {}), rejected, ...(narrowed.length ? { narrowed } : {}), executed: qids.length > 0, summary, results }, null, 2) };
|
|
418
|
+
return { type: "text", text: JSON.stringify({ minted: minted.map((e) => e.qid), reused, ...(enrichedQids.length ? { enriched: enrichedQids } : {}), ...(conflicts.length ? { conflict: conflicts } : {}), rejected, ...(excludedHouse.length ? { excluded_house_element: excludedHouse } : {}), ...(narrowed.length ? { narrowed } : {}), executed: qids.length > 0, summary, results }, null, 2) };
|
|
402
419
|
}
|
|
@@ -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
|
//
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
|
|
3
|
+
// knockout-next-step.mjs — a next-step section the assessing model wrote into a name's read comes off
|
|
4
|
+
// before the report is published.
|
|
5
|
+
//
|
|
6
|
+
// A knockout states findings and a rating; what to do with the name is the reading lawyer's. The assessing
|
|
7
|
+
// instructions stopped asking for a next step, and the model still writes one now and then, under a heading
|
|
8
|
+
// of its own: "What to do with it", "Practical next step". Refusing the turn and re-asking cost a full
|
|
9
|
+
// re-dispatch each time, and the owner ruled for delivery over the loop (2026-09-19). So the section is
|
|
10
|
+
// removed here, in code, with no model call, and the run record names every heading removed.
|
|
11
|
+
//
|
|
12
|
+
// WHAT COMES OFF: a heading whose own words are a next step, and the section under it — nothing else. A
|
|
13
|
+
// hashed heading's section runs to the next heading at its level or above; a line that is only bold text
|
|
14
|
+
// runs to the next heading of either kind; a bold label opening a paragraph ("**Practical next step** —
|
|
15
|
+
// …") takes that paragraph. Position does not matter: removing a section cannot reorder what is left.
|
|
16
|
+
//
|
|
17
|
+
// WHAT STAYS, BY DESIGN: the same advice written as an ordinary sentence with no heading over it. Nothing
|
|
18
|
+
// bounds that sentence but judgement, and cutting a guess out of the client's read is worse than leaving
|
|
19
|
+
// it. The instruction not to write one is the only thing that reaches it.
|
|
20
|
+
|
|
21
|
+
// A heading's own words that make it a next step. Whole-heading: "What drives the rating" and "What is
|
|
22
|
+
// still open" are not next steps, and a word appearing somewhere inside a heading proves nothing.
|
|
23
|
+
export const NEXT_STEP_HEADING_RE =
|
|
24
|
+
/^(?:(?:practical|immediate|suggested|recommended|possible)\s+)?(?:next\s+steps?\b.*|what\s+to\s+do\b.*|what\s+happens\s+next|recommendations?\b.*|our\s+recommendations?\b.*|(?:the\s+)?way\s+forward)$/i;
|
|
25
|
+
|
|
26
|
+
const label = (s) => String(s).replace(/[*_`]/g, "").replace(/[\s:.–—-]+$/, "").trim();
|
|
27
|
+
|
|
28
|
+
/** What kind of heading this line is, and its words — or null for a line that heads nothing. PURE. */
|
|
29
|
+
function headingAt(line) {
|
|
30
|
+
const atx = /^\s{0,3}(#{1,6})\s+(.*?)\s*#*\s*$/.exec(line);
|
|
31
|
+
if (atx) return { kind: "atx", level: atx[1].length, text: label(atx[2]) };
|
|
32
|
+
const bold = /^\s*(\*\*|__)(.+?)\1\s*:?\s*$/.exec(line);
|
|
33
|
+
if (bold) return { kind: "bold", text: label(bold[2]) };
|
|
34
|
+
const lead = /^\s*(\*\*|__)(.+?)\1\s*[:.–—-]?\s+\S/.exec(line);
|
|
35
|
+
if (lead) return { kind: "lead", text: label(lead[2]) };
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Is this whole line a heading over a next step? PURE. */
|
|
40
|
+
export function isNextStepHeading(line) {
|
|
41
|
+
const h = headingAt(String(line ?? ""));
|
|
42
|
+
return Boolean(h && NEXT_STEP_HEADING_RE.test(h.text));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The text with every next-step section removed, and what was removed: `{ text, removed: [{ heading,
|
|
47
|
+
* chars }] }`. Text with nothing to remove comes back byte-identical. PURE.
|
|
48
|
+
*/
|
|
49
|
+
export function stripNextStepSections(text) {
|
|
50
|
+
const src = String(text ?? "");
|
|
51
|
+
const lines = src.split("\n");
|
|
52
|
+
const keep = [];
|
|
53
|
+
const removed = [];
|
|
54
|
+
for (let i = 0; i < lines.length;) {
|
|
55
|
+
const h = headingAt(lines[i]);
|
|
56
|
+
if (!h || !NEXT_STEP_HEADING_RE.test(h.text)) { keep.push(lines[i]); i++; continue; }
|
|
57
|
+
let j = i + 1;
|
|
58
|
+
if (h.kind === "lead") {
|
|
59
|
+
while (j < lines.length && lines[j].trim() !== "") j++;
|
|
60
|
+
} else {
|
|
61
|
+
for (; j < lines.length; j++) {
|
|
62
|
+
const n = headingAt(lines[j]);
|
|
63
|
+
if (!n || n.kind === "lead") continue;
|
|
64
|
+
if (h.kind === "atx" ? n.kind === "atx" && n.level <= h.level : true) break;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
removed.push({ heading: h.text, chars: lines.slice(i, j).join("\n").trim().length });
|
|
68
|
+
i = j;
|
|
69
|
+
}
|
|
70
|
+
if (!removed.length) return { text: src, removed };
|
|
71
|
+
return { text: keep.join("\n").replace(/\n[ \t]*\n(?:[ \t]*\n)+/g, "\n\n").trim(), removed };
|
|
72
|
+
}
|
package/driver/named-band.mjs
CHANGED
|
@@ -137,7 +137,7 @@ export function parseNamedBand(raw) {
|
|
|
137
137
|
// byte-identical to a slice the plan deliberately counted without fetching. Measured on a real
|
|
138
138
|
// run: four capability-gap blocks carried `error:true, deferred:true` into this function and
|
|
139
139
|
// reached record-carry.json with both fields gone and a sentence claiming the run "has a hit
|
|
140
|
-
// COUNT for this slice". register-plan.mjs:
|
|
140
|
+
// COUNT for this slice". register-plan.mjs:1681 validatePlanFeasibility already enforces the same rule one layer up
|
|
141
141
|
// ("a transient must not ship indistinguishable from a sanctioned descriptor") — it reads the
|
|
142
142
|
// RAW blocks, which is why it could. Every consumer that reads THIS projection could not.
|
|
143
143
|
// Conditional like the four keys above, so old bands carry neither key and nothing shifts.
|
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": {
|
|
@@ -56,6 +56,7 @@ import { registerUnavailableOffices } from "./register-unreachable.mjs";
|
|
|
56
56
|
import { runRecordLogPath } from "../providers/_shared/ledger-path.mjs"; // — this run's record log
|
|
57
57
|
import { validators as koValidators, validateMergedFindings, worstBand, registerSurfacedFilings, raterCaveats, SURVIVOR_BOUNDARY_RE } from "./verify-knockout.mjs";
|
|
58
58
|
import { reviewAbout, reviewEvidence, reviewEvidenceLines, applyKnockoutReview, knockoutReviewFile } from "./knockout-review-record.mjs";
|
|
59
|
+
import { stripNextStepSections } from "./knockout-next-step.mjs";
|
|
59
60
|
import { publishKnockout, composeKnockoutEmail } from "./publish/knockout.mjs";
|
|
60
61
|
import { writeRunStatus, rollupStatus, atomicWrite, identitySeed } from "./progress.mjs"; // — the identity seed is shared; the stepper is not
|
|
61
62
|
import { batchMarkName } from "./mark-name.mjs";
|
|
@@ -349,6 +350,28 @@ export function survivorBoundaryNote(policy) {
|
|
|
349
350
|
return `This is ${screenName}, not a clearance. A mark not knocked out here is not clear — it is not knocked out at this screen's depth, and proceeds to clearance. Nothing above is a finding of availability.`;
|
|
350
351
|
}
|
|
351
352
|
|
|
353
|
+
// ── a next-step section the model wrote comes off, in code ──────────────────────────────────────────
|
|
354
|
+
//
|
|
355
|
+
// Each name's read is published as its own report, and the screen states findings and a rating — what to
|
|
356
|
+
// do with the name is the reading lawyer's. A heading the model wrote over a next step, and the section
|
|
357
|
+
// under it, are removed here and named in the run record; `knockout-next-step.mjs` says what counts. This
|
|
358
|
+
// replaced a pre-delivery refusal that re-asked the whole chunk for the same edit (owner, 2026-09-19).
|
|
359
|
+
// EXPORTED so the record line is driven directly, not only through a whole pipeline.
|
|
360
|
+
export function removeNextStepSections(runDir, marks) {
|
|
361
|
+
const out = [];
|
|
362
|
+
for (const m of marks ?? []) {
|
|
363
|
+
if (typeof m?.assessment !== "string") continue;
|
|
364
|
+
const { text, removed } = stripNextStepSections(m.assessment);
|
|
365
|
+
if (!removed.length) continue;
|
|
366
|
+
m.assessment = text;
|
|
367
|
+
const row = { event: "knockout-next-step-removed", mark: m.name, headings: removed.map((r) => r.heading),
|
|
368
|
+
chars: removed.reduce((n, r) => n + r.chars, 0), lane: "knockout" };
|
|
369
|
+
runLog(runDir, row);
|
|
370
|
+
out.push(row);
|
|
371
|
+
}
|
|
372
|
+
return out;
|
|
373
|
+
}
|
|
374
|
+
|
|
352
375
|
// ── — the knockout lane's recovery park ───────────────────────────────────────
|
|
353
376
|
//
|
|
354
377
|
// EXPORTED so it can be armed directly. The clearance lane's equivalent is inline in a 6,000-line catch
|
|
@@ -517,7 +540,7 @@ export async function knockoutInner(ctx, job, opts = {}) {
|
|
|
517
540
|
//
|
|
518
541
|
// ONLY when the counter is the real provider. On fixtures or an injected counter the offices this
|
|
519
542
|
// deployment can reach are not what the lane is talking to, and splitting on them would defer a
|
|
520
|
-
// 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
|
|
521
544
|
// sibling defect had, and the reason `missing` carries the same guard).
|
|
522
545
|
const unreachable = countExec.source === "provider" ? registerUnavailableOffices(caps) : [];
|
|
523
546
|
ctx.registerUnreachable = unreachable;
|
|
@@ -964,6 +987,9 @@ export async function knockoutInner(ctx, job, opts = {}) {
|
|
|
964
987
|
// it runs BEFORE the artifact is written, not after. The counts are logged because a receipts pass
|
|
965
988
|
// over zero citations is a different fact from a receipts pass, and only the count can tell them
|
|
966
989
|
// apart afterwards.
|
|
990
|
+
// BEFORE THE GATE AND THE WRITE, so the record on disk is the record that ships, and the reviewing
|
|
991
|
+
// pass never addresses a line inside a section that is about to go.
|
|
992
|
+
removeNextStepSections(run.runDir, merged.marks);
|
|
967
993
|
const mv = validateMergedFindings(run.runDir, merged, plan);
|
|
968
994
|
if (!mv.ok) throw new StageFailure("knockout-assess", `merged findings failed the lint: ${mv.failures.join("; ")}`, null);
|
|
969
995
|
runLog(run.runDir, { event: "knockout-receipts", ...mv.receipts });
|
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
|
@@ -122,6 +122,13 @@ const CHROME_RES = [
|
|
|
122
122
|
// has stopped matching. Removing the wrapper first would take the nav with it, drive the count to
|
|
123
123
|
// zero, and turn a security assertion into a permanent false alarm.
|
|
124
124
|
{ tag: "div", open: /<div class="[^"]*\brep-stickyhead\b[^"]*"[^>]*>/ },
|
|
125
|
+
// THE SECTION MENU, WHEREVER THE RENDERER PUT IT. A report rendered today carries it inside the header
|
|
126
|
+
// above, and it goes with that. A report rendered before 2026-09-18 carries it just AFTER the header, so
|
|
127
|
+
// stripping the header alone left it in the frame: a second copy of the menu the portal draws, pinned
|
|
128
|
+
// over the report's title band, its current item red on red in the dark theme. Measured on an archived
|
|
129
|
+
// global preliminary report served by a published beta. Archived reports are served from their baked
|
|
130
|
+
// bytes, so this has to happen here rather than in the renderer. `sectionsOf` has already read it.
|
|
131
|
+
{ tag: "nav", open: /<nav class="[^"]*\bstrip\b[^"]*"[^>]*>/ },
|
|
125
132
|
// the "Internal review copy — stripped on export" bar, which hosted the quality-capture controls
|
|
126
133
|
{ tag: "div", open: /<div class="[^"]*\breview\b[^"]*\binternal\b[^"]*"[^>]*>/ },
|
|
127
134
|
// per-finding flag buttons and their popovers
|
|
@@ -529,7 +536,7 @@ const EMBED_JS = `
|
|
|
529
536
|
function schedule(){
|
|
530
537
|
if(queued)return;
|
|
531
538
|
queued=true;
|
|
532
|
-
requestAnimationFrame(function(){queued=false;post();});
|
|
539
|
+
requestAnimationFrame(function(){queued=false;post();sections();});
|
|
533
540
|
}
|
|
534
541
|
// WHICH CONTROLS THIS DOCUMENT ACTUALLY HAS.
|
|
535
542
|
//
|
|
@@ -559,12 +566,24 @@ const EMBED_JS = `
|
|
|
559
566
|
// script) and the shell draws it in its own header. Only the ids that are really in the document are
|
|
560
567
|
// announced: a report whose renderer named a section it did not draw would otherwise offer the reader
|
|
561
568
|
// a breadcrumb entry that jumps nowhere.
|
|
569
|
+
//
|
|
570
|
+
// AND WHERE EACH ONE STARTS, so the shell can show how far the reader has got (owner, 2026-09-19): every
|
|
571
|
+
// section reached so far is marked, the rest are not. The page scrolls, not this frame, so only the shell
|
|
572
|
+
// knows where the reader is. It needs each section's top in this document to compare. Said again whenever
|
|
573
|
+
// the layout moves: a panel opening pushes every section below it down.
|
|
574
|
+
var saidSections='';
|
|
562
575
|
function sections(){
|
|
563
576
|
try{
|
|
564
577
|
var list=window.__CORD_SECTIONS;
|
|
565
578
|
if(!list||!list.length)return;
|
|
566
579
|
var live=[];
|
|
567
|
-
for(var i=0;i<list.length;i++)
|
|
580
|
+
for(var i=0;i<list.length;i++){
|
|
581
|
+
var el=document.getElementById(list[i].id);
|
|
582
|
+
if(el) live.push({id:list[i].id,label:list[i].label,top:Math.max(0,Math.round(el.getBoundingClientRect().top+window.scrollY))});
|
|
583
|
+
}
|
|
584
|
+
var said=JSON.stringify(live);
|
|
585
|
+
if(said===saidSections)return;
|
|
586
|
+
saidSections=said;
|
|
568
587
|
parent.postMessage({source:TAG,type:'sections',sections:live},'*');
|
|
569
588
|
}catch(e){}
|
|
570
589
|
}
|
|
@@ -831,7 +850,10 @@ export function sectionsOf(html) {
|
|
|
831
850
|
if (!nav) return [];
|
|
832
851
|
const out = [];
|
|
833
852
|
for (const m of nav[1].matchAll(STRIP_LINK_RE)) {
|
|
834
|
-
|
|
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();
|
|
835
857
|
if (m[1] && label) out.push({ id: m[1], label });
|
|
836
858
|
}
|
|
837
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;
|
|
@@ -3931,7 +3943,9 @@ const escHtml = (t) => String(t).replace(/[&<>"']/g, (c) => ({ "&": "&", "<"
|
|
|
3931
3943
|
// and brand pack §01 fixes dark at #0f0e0c near-black + #f0e8d8 parchment. The pack wins.
|
|
3932
3944
|
// ── THE PLAIN LINE LEADS; THE ADMINISTRATOR'S TWO LINES STEP BACK INTO A FOLD ────────────────────────
|
|
3933
3945
|
//
|
|
3934
|
-
// A person arriving here needs the field, the button, and one fact: this install
|
|
3946
|
+
// A person arriving here needs the field, the button, and one fact: this install has one user, and which
|
|
3947
|
+
// address that is. The owner's words, 2026-09-19: "Clearotron portal, as <address>." read as "you are
|
|
3948
|
+
// <address>", and a person who found `key issue` first minted a key and could not get in.
|
|
3935
3949
|
// The reset command and the way to add people are an administrator's business, so they sit in a closed
|
|
3936
3950
|
// "Administrator help" fold, word for word as they were, with the same link People gives to putting a
|
|
3937
3951
|
// login system in front. A `<details>` needs no script, which this door must render without.
|
|
@@ -3976,9 +3990,7 @@ ${DOOR_THEME_INIT}
|
|
|
3976
3990
|
.hint { margin-top:18px; font-size:13px; }
|
|
3977
3991
|
code { font-family:var(--mono); font-size:12.5px; background:var(--code-bg);
|
|
3978
3992
|
padding:1px 5px; border-radius:4px; overflow-wrap:anywhere; }
|
|
3979
|
-
.
|
|
3980
|
-
.lead b { font-weight:600; }
|
|
3981
|
-
.fold { margin-top:10px; }
|
|
3993
|
+
.fold { margin-top:18px; padding-top:14px; border-top:1px solid var(--line); }
|
|
3982
3994
|
.fold > summary { display:inline-flex; align-items:center; gap:6px; list-style:none; cursor:pointer;
|
|
3983
3995
|
font-size:13px; color:var(--link); }
|
|
3984
3996
|
.fold > summary::-webkit-details-marker { display:none; }
|
|
@@ -3993,7 +4005,7 @@ ${signedIn
|
|
|
3993
4005
|
? `<p>You are signed in as <span class="who">${escHtml(email)}</span>.</p>
|
|
3994
4006
|
<div><a href="/portal">Go to the portal</a></div>
|
|
3995
4007
|
<form method="post" action="/portal/logout"><button type="submit">Sign out</button></form>`
|
|
3996
|
-
: `<p
|
|
4008
|
+
: `<p>This ${escHtml(BRAND.name)} has one user: <b class="who">${escHtml(email)}</b>. Enter its passphrase.</p>
|
|
3997
4009
|
${discarded ? `<p class="hint">A session this portal did not start, from another ${escHtml(BRAND.name)} on this address or an expired one, was set aside. Sign in below.</p>` : ""}
|
|
3998
4010
|
${error ? `<p class="err">${escHtml(error)}</p>` : ""}
|
|
3999
4011
|
<form method="post" action="/portal/login">
|
|
@@ -4001,10 +4013,10 @@ ${error ? `<p class="err">${escHtml(error)}</p>` : ""}
|
|
|
4001
4013
|
<input id="passphrase" name="passphrase" type="password" autocomplete="new-password" autofocus>
|
|
4002
4014
|
<button type="submit">Sign in</button>
|
|
4003
4015
|
</form>
|
|
4004
|
-
<p class="lead"><b>This ${escHtml(BRAND.name)} signs in one person: you.</b></p>
|
|
4005
4016
|
<details class="fold"><summary><span>Administrator help</span><svg class="chev" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m9 18 6-6-6-6"/></svg></summary>
|
|
4006
|
-
<p class="hint">
|
|
4007
|
-
running this portal. It
|
|
4017
|
+
<p class="hint">The passphrase was printed once when this ${escHtml(BRAND.name)} first started. Lost it? Run
|
|
4018
|
+
<code>${escHtml(reset)}</code> on the machine running this portal. It prints a new one, once, for the same user.</p>
|
|
4019
|
+
<p class="hint">A key from <code>${escHtml(bareInvocation("key"))} issue</code> is for an AI assistant, not for this page.</p>
|
|
4008
4020
|
<p class="hint">To add people, put it behind a login system such as your company single sign-on. <a href="${escHtml(LOGIN_IN_FRONT_DOC)}" target="_blank" rel="noreferrer">How to set that up</a></p>
|
|
4009
4021
|
</details>`}
|
|
4010
4022
|
</div></body></html>`;
|
|
@@ -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
|