clearotron 0.3.2 → 0.3.3-beta.0
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 +12 -0
- package/INSTALL.md +8 -0
- package/bin/onboard.mjs +29 -7
- package/bin/start.mjs +1 -1
- package/build-info.json +2 -2
- package/demo/MANIFEST.json +27 -0
- package/docs/INTAKE.md +8 -0
- package/docs/architecture/04-configuration-reference.md +1 -1
- package/driver/CHANGELOG.md +37 -0
- package/driver/clearance-variants-record.mjs +12 -1
- package/driver/common-law-coverage-status.mjs +113 -0
- package/driver/contract-audit.mjs +1 -1
- package/driver/contract-e3-backlog.mjs +37 -37
- package/driver/contract-vocabulary.mjs +8 -8
- package/driver/coverage-form-io.mjs +3 -1
- package/driver/coverage-form.mjs +38 -11
- package/driver/coverage-ledger.mjs +37 -7
- package/driver/coverage-union.mjs +2 -2
- package/driver/crowd-context.mjs +19 -6
- package/driver/drainer-identity.mjs +1 -1
- package/driver/engine/mcp/clarivate-server.mjs +4 -2
- package/driver/engine/mcp/corsearch-server.mjs +3 -1
- package/driver/engine/mcp/coverage-server.mjs +1 -1
- package/driver/engine/mcp/dispositions-server.mjs +47 -5
- package/driver/engine/mcp/euipo-server.mjs +2 -0
- package/driver/engine/mcp/free-tier-server.mjs +2 -0
- package/driver/engine/mcp/gather-config.mjs +8 -2
- package/driver/engine/mcp/probe-server.mjs +28 -0
- package/driver/engine/mcp/proposal-fields.mjs +45 -0
- package/driver/engine/mcp/recording-server.mjs +30 -0
- package/driver/engine/mcp/signa-server.mjs +2 -0
- package/driver/engine/mcp/supplemental.mjs +89 -12
- package/driver/engine/mcp/unit-note-server.mjs +50 -0
- package/driver/engine/mcp/uspto-local-server.mjs +2 -0
- package/driver/engine/openai-agent.mjs +7 -0
- package/driver/engine/probe.mjs +67 -14
- package/driver/engine/tool-refusal.mjs +16 -0
- package/driver/enqueue-schema.mjs +2 -2
- package/driver/envelope-settle.mjs +82 -13
- package/driver/findings-model.mjs +4 -4
- package/driver/gateway.mjs +18 -2
- package/driver/manager-groups-verdict.mjs +1 -1
- package/driver/matter-frame-record.mjs +24 -7
- package/driver/named-band.mjs +1 -1
- package/driver/package.json +1 -1
- package/driver/partial-payload-baseline.json +12 -3
- package/driver/pipeline.mjs +141 -37
- package/driver/publish/index.mjs +40 -25
- package/driver/publish/xlsx.mjs +26 -4
- package/driver/queue-markers.mjs +44 -0
- package/driver/queue-watch-verdict.mjs +2 -2
- package/driver/register-availability.mjs +2 -2
- package/driver/register-plan.mjs +313 -21
- package/driver/roster-verdict.mjs +1 -1
- package/driver/runner.mjs +26 -2
- package/driver/skills/clearance-common-law/SKILL.md +2 -0
- package/driver/skills/clearance-register/SKILL.md +44 -3
- package/driver/skills/clearance-register/digest.md +5 -5
- package/driver/skills/clearance-register/providers/clarivate.md +1 -1
- package/driver/skills/clearance-register/unit.md +39 -0
- package/driver/skills/clearance-variants/SKILL.md +1 -1
- package/driver/skills/matter-frame/SKILL.md +4 -2
- package/driver/stages.mjs +12 -5
- package/driver/status-snapshot.mjs +1 -1
- package/driver/suite-census.json +236 -14
- package/driver/synthesis-record.mjs +80 -2
- package/driver/unit-file-drift.mjs +3 -3
- package/driver/unit-inventory.mjs +2 -2
- package/driver/unit-state-verdict.mjs +1 -1
- package/driver/updater-identity.mjs +2 -3
- package/driver/variant-manifest-model.mjs +11 -1
- package/driver/verify.mjs +5 -5
- package/driver/withheld-families.mjs +104 -0
- package/mcp-server/CHANGELOG.md +4 -0
- package/mcp-server/lib/brief.mjs +5 -7
- package/mcp-server/lib/runs.mjs +1 -1
- package/mcp-server/package.json +1 -1
- package/mcp-server/server.mjs +3 -2
- package/package.json +1 -1
- package/portal-ui/dist/assets/{index-DMthc7PQ.js → index-GBbbyQxc.js} +22 -4
- package/portal-ui/dist/index.html +1 -1
- package/portal-ui/package.json +1 -1
- package/providers/_shared/count.mjs +2 -2
- package/providers/_shared/enumerate.mjs +15 -2
- package/providers/_shared/execute-plan.mjs +19 -1
- package/providers/_shared/plan-guards.mjs +40 -0
- package/providers/clarivate/src/capabilities.js +15 -5
- package/providers/clarivate/src/core.js +41 -5
- package/providers/corsearch/src/capabilities.js +4 -0
- package/providers/oauth-mcp-bridge/CHANGELOG.md +4 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/providers/signa/src/capabilities.js +22 -8
- package/providers/signa/src/core.js +12 -1
- package/scripts/demo-evidence.mjs +114 -0
- package/scripts/engine-probe.mjs +6 -5
- package/scripts/env-audit.mjs +1 -1
- package/scripts/freeze-example-run.mjs +3 -3
- package/scripts/live-surface-check.mjs +26 -19
- package/scripts/mint-suite-census.mjs +66 -0
- package/scripts/package-size-budget.mjs +117 -0
- package/scripts/register-plan-shape.mjs +259 -0
- package/scripts/release-note-required.mjs +38 -1
- package/scripts/settings-render-check.mjs +36 -0
- package/scripts/travelling-predicates.mjs +1 -1
- package/shared/identifier-scan.mjs +22 -5
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.
|
|
5
|
+
"version": "0.3.3-beta.0",
|
|
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": {
|
|
@@ -27,6 +27,14 @@
|
|
|
27
27
|
"tool": "record_dispositions",
|
|
28
28
|
"why": "no fixture yet; accumulating semantics claimed in the tool description ('Everything already recorded is kept') and not yet planted"
|
|
29
29
|
},
|
|
30
|
+
{
|
|
31
|
+
"tool": "record_coverage_status",
|
|
32
|
+
"why": "both declared fields are required, so a call has no top-level key to omit; its accumulation by unit (a later call keeps the units it omits) is pinned in a-common-law-coverage-status-is-read-as-data.test.mjs"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"tool": "record_withheld_families",
|
|
36
|
+
"why": "its one declared field is required, so a call has no top-level key to omit; its accumulation by qid (a later call keeps the families it omits) is pinned in every-waiting-family-is-asked-or-withheld.test.mjs"
|
|
37
|
+
},
|
|
30
38
|
{
|
|
31
39
|
"tool": "record_unit_note",
|
|
32
40
|
"why": "no fixture yet; every field optional, which is the exposed shape — this row is the highest-priority one to close"
|
|
@@ -61,11 +69,12 @@
|
|
|
61
69
|
{
|
|
62
70
|
"tool": "record_clearance_variants",
|
|
63
71
|
"fields": [
|
|
72
|
+
"goods_words",
|
|
64
73
|
"incumbent_classes",
|
|
65
|
-
"
|
|
66
|
-
"
|
|
74
|
+
"search_floor",
|
|
75
|
+
"watchlist_owners"
|
|
67
76
|
],
|
|
68
|
-
"why": "a partial silently empties the register-axis search floor, narrowing the run's own definition of coverage"
|
|
77
|
+
"why": "a partial silently empties the register-axis search floor, narrowing the run's own definition of coverage. `goods_words` joins them for the same reason and one worse: an emptied goods list reads as \"this matter named no goods\" rather than as a partial call, so the crowded sweep it exists to narrow goes back to returning more filings than anyone can read, and nothing says why. The MERGE preserves all four (mergeClearanceVariantsCall, keep-if-absent) — these rows record that the ACCEPTOR alone does not."
|
|
69
78
|
},
|
|
70
79
|
{
|
|
71
80
|
"tool": "record_blind_frame",
|
package/driver/pipeline.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import { readFileSync, existsSync, mkdirSync, writeFileSync, renameSync, copyFil
|
|
|
10
10
|
import { createHash } from "node:crypto";
|
|
11
11
|
import { join, dirname, basename, resolve } from "node:path"; // resolve: the resume line must work from any cwd
|
|
12
12
|
import { driverDir, driverRel, ensureDriverDir } from "../shared/driver-dir.mjs"; // — one definition of where `_driver/` is
|
|
13
|
+
import { goodsOf } from "./queue-markers.mjs"; // — one reading of "does this job name goods", shared with the intake gate
|
|
13
14
|
import { terminalClampDecision, orderClausesForLede, clientConditions, clauseForDefect } from "./terminal-clamp.mjs"; // — deliver and clamp, never withhold
|
|
14
15
|
import { recordSpan } from "./attributed-span.mjs"; // — driver work the decomposition can attribute
|
|
15
16
|
import { fileURLToPath } from "node:url";
|
|
@@ -39,8 +40,8 @@ import { parseVerdict, countCitedDefects, parseCorrectionKinds, parseCorrections
|
|
|
39
40
|
import { readAcceptedFlags } from "./narrative-refutation-record.mjs"; // T3b — the typed flags, not the re-parse
|
|
40
41
|
import { evidenceClaimViolations, evidenceClaimTable } from "./evidence-claim-invariant.mjs"; //
|
|
41
42
|
import { buildCorrectionsApplied, correctionsWorklist, correctionsAppliedTable, correctionScope, scopeDrift, unresolvedFlags, reportLines, linesOf, REPORT_LINE_KEY, REPORT_LINE_LABEL } from "./corrections-feedforward.mjs";
|
|
42
|
-
import { parseCoverageLedgerJson, parseCoverageLedgerFull, deriveCoverageStatus, classTokensFromScopeText, coerceToolAbsenceDeferred, applyTaintDeferred, decideRegisterGap, splitDeferredByCloseability, coverageLedgerTableRows, coverageUnitLabel, NON_MATERIAL_AXES, COVERAGE_STATUSES } from "./coverage-ledger.mjs";
|
|
43
|
-
import { receiptSettled, readEnvelopeDecision, settleReceipt, settledDeferralsSection } from "./envelope-settle.mjs";
|
|
43
|
+
import { parseCoverageLedgerJson, parseCoverageLedgerFull, deriveCoverageStatus, classTokensFromScopeText, coerceToolAbsenceDeferred, applyTaintDeferred, decideRegisterGap, splitDeferredByCloseability, formRowUnitKey, coverageLedgerTableRows, coverageUnitLabel, NON_MATERIAL_AXES, COVERAGE_STATUSES } from "./coverage-ledger.mjs";
|
|
44
|
+
import { receiptSettled, readEnvelopeDecision, settleReceipt, settledDeferralsSection, readStickyGaps } from "./envelope-settle.mjs";
|
|
44
45
|
import { readRegisterTaint, readActiveTaintAxes } from "./register-taint.mjs";
|
|
45
46
|
import { parseNamedBand, mergeNamedBands, findCollapsedBands, quarantineUnknownStates, taintQuarantineCleanBlocks, bandRecords } from "./named-band.mjs";
|
|
46
47
|
import { recordOriginsFor } from "./record-origins.mjs";
|
|
@@ -97,7 +98,7 @@ import { publishReport, composeEmailHtml, deliverySubject } from "./publish/inde
|
|
|
97
98
|
import { parseCaseLawProfiles, joinCaseLawProfiles } from "./publish/parse.mjs";
|
|
98
99
|
import { buildAuditMd, parseSpineFindingBlocks } from "./publish/audit-from-spine.mjs";
|
|
99
100
|
import { deriveRegisterPresence } from "./publish/register-presence.mjs"; // — the audit stores every live in-scope record
|
|
100
|
-
import { lastAcceptedMatterFrame, frameIdentifiedClasses, frameHouseElementCandidate } from "./matter-frame-record.mjs"; // — the frame's inferred scope, when nothing was instructed; and the classes it judged necessary beyond the instructed ones, which the plan compile
|
|
101
|
+
import { lastAcceptedMatterFrame, frameIdentifiedClasses, frameIdentifiedClassRows, frameHouseElementCandidate } from "./matter-frame-record.mjs"; // — the frame's inferred scope, when nothing was instructed; and the classes it judged necessary beyond the instructed ones, each with its reason, which the plan compile gives one identical-mark question apiece (decision 18)
|
|
101
102
|
import { romanizedTermsFromPlan, mintSupplementalQid } from "./register-plan.mjs";
|
|
102
103
|
import { excludeHouseElement, verifyHouseElementOwnership, resolveRegions as resolvePlanRegions, HOUSE_ELEMENT_RECEIPT } from "./register-plan.mjs"; // 647 — the client's own element leaves the conflict analysis only on a verified receipt
|
|
103
104
|
import { resolveRecordExecutor } from "./register-records.mjs"; // — the stamp the late lanes never met
|
|
@@ -463,8 +464,35 @@ export function readPlanExecution(ctx) { // @internal
|
|
|
463
464
|
const p = ctx?.paths?.planExecution;
|
|
464
465
|
try { return p && existsSync(p) ? JSON.parse(readFileSync(p, "utf8")) : null; } catch { return null; }
|
|
465
466
|
}
|
|
467
|
+
/**
|
|
468
|
+
* A qid this run has accepted as a capability gap (envelope-settle.mjs stickyGapsAfter) leaves `missing`
|
|
469
|
+
* for `deferred`, with the reason it was accepted under, so no re-join can put it back on the ladder.
|
|
470
|
+
* Returns the join unchanged when nothing is held. PURE.
|
|
471
|
+
*/
|
|
472
|
+
export function holdStickyGapsIn(join, sticky) { // @internal
|
|
473
|
+
const held = (join?.missing ?? []).filter((q) => sticky?.has(q));
|
|
474
|
+
if (!held.length) return join;
|
|
475
|
+
return { ...join, missing: join.missing.filter((q) => !sticky.has(q)),
|
|
476
|
+
deferred: [...(join.deferred ?? []), ...held.map((q) => ({ qid: q, reason: String(sticky.get(q)?.reason ?? "").slice(0, 300) }))] };
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* The coverage units of this run's sticky capability gaps, as ledgerUnitKey()s, found through the coverage
|
|
481
|
+
* form's own deferred rows (the driver writes each with its qid). The envelope, the escalation and the
|
|
482
|
+
* skeptic's ledger split then hold those rows whatever reason the seat wrote for them. A run with no form,
|
|
483
|
+
* or no sticky gap, holds nothing extra, exactly as before.
|
|
484
|
+
*/
|
|
485
|
+
export function stickyGapUnits(P) { // @internal
|
|
486
|
+
const sticky = readStickyGaps(P);
|
|
487
|
+
if (!sticky.size) return new Set();
|
|
488
|
+
const stamp = coverageFormStamp(P.runDir);
|
|
489
|
+
const rows = stamp.required ? (readCoverageForm(P.runDir, stamp.formName).rows ?? []) : [];
|
|
490
|
+
return new Set(rows.filter((r) => r?.kind === "deferred" && sticky.has(String(r.qid ?? ""))).map(formRowUnitKey));
|
|
491
|
+
}
|
|
492
|
+
|
|
466
493
|
export function writePlanExecutionReceipt(ctx, joinRes) { // @internal
|
|
467
494
|
const P = ctx.paths;
|
|
495
|
+
joinRes = holdStickyGapsIn(joinRes, readStickyGaps(P));
|
|
468
496
|
// — the ONE place the receipt is written is the one place this decision is taken. The
|
|
469
497
|
// reclassification is keyed off the receipt already on disk (ladderExhaustedQids), so every writer
|
|
470
498
|
// agrees without any of them knowing about it: the fan-in, the envelope's re-join after its close
|
|
@@ -2384,7 +2412,12 @@ function attachRegisterPlan(ctx, { frozenOnly = false } = {}) {
|
|
|
2384
2412
|
// for the documented normal case. Harmless on corsearch (an absent region clause is a worldwide
|
|
2385
2413
|
// sweep); fatal on a provider whose regions[] is mandatory, where every entry then errored on its
|
|
2386
2414
|
// count probe and the whole plan joined MISSING at fan-in (review finding 11).
|
|
2387
|
-
|
|
2415
|
+
// DECISION 18: the frame's added classes are no longer UNIONED into the plan's class scope. They
|
|
2416
|
+
// ride one identical-mark entry each instead, which is what an added class is supposed to cost —
|
|
2417
|
+
// unioning put every added class on every variant and every family. The instructed scope is what
|
|
2418
|
+
// it always was, and `addedClasses` only ever appends.
|
|
2419
|
+
job: { jobKey: ctx.run.slug, classes: inScopeClassList(ctx.job, ctx.profile).map(String), jurisdictions: registerJurisdictions(ctx.job, ctx.profile) },
|
|
2420
|
+
addedClasses: frameIdentifiedClassRows(P.runDir),
|
|
2388
2421
|
form, skillVersion: "clearance-register@spec48",
|
|
2389
2422
|
// — WHICH ELEMENT THE EXCLUSION TOOK OUT, so the compile can make its form band unreachable
|
|
2390
2423
|
// rather than merely unasked-for. Null unless the ownership receipt verified, which is the same
|
|
@@ -6219,10 +6252,11 @@ export function skepticDeferralExtra(ctx) { // @internal
|
|
|
6219
6252
|
const gapAxes = capabilityGapAxes(ctx.registerPlan, receipt);
|
|
6220
6253
|
const fullyDeferred = new Set((ctx.registerPlan ? fullyDeferredAxes(ctx.registerPlan) : []).map((a) => String(a.axis).toLowerCase()));
|
|
6221
6254
|
const axes = [...new Set(rows.map((r) => String(r.axis ?? "").toLowerCase()).filter(Boolean))];
|
|
6255
|
+
const heldUnits = stickyGapUnits(P);
|
|
6222
6256
|
const ledgerLines = coverageLedgerTableRows(rows);
|
|
6223
6257
|
const held = [], closeable = [];
|
|
6224
6258
|
for (const a of axes) {
|
|
6225
|
-
const s = splitDeferredByCloseability(rows, a, gapAxes, { fullyDeferred: fullyDeferred.has(a) });
|
|
6259
|
+
const s = splitDeferredByCloseability(rows, a, gapAxes, { fullyDeferred: fullyDeferred.has(a), heldUnits });
|
|
6226
6260
|
for (const r of s.held) held.push(`${a} / ${r.unit} — ${String(r.reason ?? "").replace(/\s+/g, " ").slice(0, 160)}`);
|
|
6227
6261
|
for (const r of s.closeable) closeable.push(`${a} / ${r.unit} — ${String(r.reason ?? "").replace(/\s+/g, " ").slice(0, 160)}`);
|
|
6228
6262
|
}
|
|
@@ -6230,6 +6264,16 @@ export function skepticDeferralExtra(ctx) { // @internal
|
|
|
6230
6264
|
const deferredQids = deferredList.slice(0, 24)
|
|
6231
6265
|
.map((d) => `- ${d.qid} — ${String(d.reason ?? "").replace(/\s+/g, " ").slice(0, 200)}`);
|
|
6232
6266
|
const more = deferredList.length > 24 ? [`- …and ${deferredList.length - 24} more (read ${P.planExecution} for the rest)`] : [];
|
|
6267
|
+
// THE CLASSES THE FRAME ADDED, AND WHAT EACH RETURNED. This block names what was refused and what is
|
|
6268
|
+
// open, and nothing that ran, so a class the frame added beyond the instructed ones — asked and
|
|
6269
|
+
// answered with its records listed — was absent from it, and the skeptic reported it as never swept.
|
|
6270
|
+
const addedClassRows = (ctx.registerPlan?.entries ?? []).filter((e) => e?.added_class_reason).map((e) => {
|
|
6271
|
+
const ran = (receipt?.executed ?? []).find((x) => x.qid === e.qid);
|
|
6272
|
+
const answer = ran ? `${ran.state}${Number.isFinite(ran.records) ? `, ${ran.records} records` : ""}`
|
|
6273
|
+
: (receipt?.deferred ?? []).some((d) => d.qid === e.qid) ? "refused (listed above)"
|
|
6274
|
+
: (receipt?.missing ?? []).includes(e.qid) ? "not run (missing)" : "not in the receipt";
|
|
6275
|
+
return `- ${e.qid} (class ${(e.nice_classes ?? []).join(", ")}) — ${answer}`;
|
|
6276
|
+
});
|
|
6233
6277
|
const ownerNegative = ownerScreenNegative(readOwnerScreen(P));
|
|
6234
6278
|
return lines(
|
|
6235
6279
|
`COVERAGE + EXECUTION, DRIVER-COMPUTED — do NOT re-derive any of this from the findings prose. These rows come from ${P.registerCoverageLedger} and ${P.planExecution}, the machine artifacts the driver wrote; both are also yours to read directly, but the answer to "what is still open, and can a re-run close it" is already below.`,
|
|
@@ -6240,6 +6284,7 @@ export function skepticDeferralExtra(ctx) { // @internal
|
|
|
6240
6284
|
deferredQids.length
|
|
6241
6285
|
? lines(`Plan-execution receipt — queries the ACTIVE PROVIDER REFUSED deterministically (${deferredList.length} of ${(receipt.executed?.length ?? 0) + deferredList.length + (receipt.missing?.length ?? 0)} planned), with the mechanical reason per query:`, ...deferredQids, ...more)
|
|
6242
6286
|
: "Plan-execution receipt: no query was deterministically refused by the provider this run.",
|
|
6287
|
+
addedClassRows.length ? lines("", "Classes the frame added beyond the instructed ones — each question, and what it returned:", ...addedClassRows) : "",
|
|
6243
6288
|
"",
|
|
6244
6289
|
closeable.length
|
|
6245
6290
|
? `CLOSEABLE floor obligations (a warm re-run reaches these — escalate them if they are material): ${closeable.join("; ")}.`
|
|
@@ -6401,7 +6446,7 @@ function plainRegisterExtra(ctx) {
|
|
|
6401
6446
|
}
|
|
6402
6447
|
}
|
|
6403
6448
|
|
|
6404
|
-
// ── THE RECEIPT, AND THE
|
|
6449
|
+
// ── THE RECEIPT, AND THE GRADED CLASSES, STATED ONCE ────────────────────────────────────────────────
|
|
6405
6450
|
//
|
|
6406
6451
|
//. The receipt this block tabulates is the answer to "did that search run", and until now exactly
|
|
6407
6452
|
// one stage got it: the REVIEWER. `synthesis` — the stage that writes the claim the reviewer then
|
|
@@ -6419,7 +6464,7 @@ function plainRegisterExtra(ctx) {
|
|
|
6419
6464
|
// out twice, once per seat, is the shape — one rule in two places, drifting from the day the second
|
|
6420
6465
|
// copy is typed — and the classes are the part that must never drift, because class (1) is the blocking
|
|
6421
6466
|
// condition.
|
|
6422
|
-
// THE HEAD AND THE
|
|
6467
|
+
// THE HEAD AND THE GRADED CLASSES ARE NOT DECLARED HERE. They live in register-plan.mjs, beside the
|
|
6423
6468
|
// derivation that assigns the states, because the gateway's corrective hint is a THIRD reader of the
|
|
6424
6469
|
// same grading and `pipeline.mjs` imports `gateway.mjs` — so a constant declared here could never
|
|
6425
6470
|
// reach it.
|
|
@@ -6447,8 +6492,13 @@ function planAuditExtra(ctx, { stage = "narrative-refutation" } = {}) {
|
|
|
6447
6492
|
`- executed: ${exec.executed.length} entr${exec.executed.length === 1 ? "y" : "ies"} (${crowds.length} crowd/incomplete${crowds.length ? `: ${crowds.slice(0, 4).map((x) => x.qid).join("; ")}` : ""})`,
|
|
6448
6493
|
`- missing (no band block): ${exec.missing.length}${exec.missing.length ? ` — ${exec.missing.slice(0, 4).join("; ")}` : ""}`,
|
|
6449
6494
|
`- skipped (crowd-gated fringe): ${exec.skipped.length}`,
|
|
6495
|
+
// The families waiting for the reading turn, and those it asked. Without these lines the table's
|
|
6496
|
+
// own buckets summed to the whole plan less the waiting families, and a reviewer read it as
|
|
6497
|
+
// "no family waiting" while the receipt held 156.
|
|
6498
|
+
`- awaiting the reading turn's ask: ${exec.awaiting?.length ?? 0}`,
|
|
6499
|
+
exec.asked?.length ? `- asked by the reading turn (a waiting family's question, asked by another entry): ${exec.asked.length}` : "",
|
|
6450
6500
|
exec.unplanned?.length ? `- unplanned qid-stamped blocks: ${exec.unplanned.length}` : "",
|
|
6451
|
-
...(exec.skeleton ?? []).map((s) => `- axis ${s.axis}: ${s.state} (${s.executed}/${s.entries} executed, ${s.crowds} crowd)`),
|
|
6501
|
+
...(exec.skeleton ?? []).map((s) => `- axis ${s.axis}: ${s.state} (${s.executed}/${s.entries} executed, ${s.crowds} crowd${s.awaiting ? `, ${s.awaiting} awaiting` : ""})`),
|
|
6452
6502
|
];
|
|
6453
6503
|
} catch (e) { rows = [`- (receipt table unavailable — read + audit the receipt file directly: ${P.planExecution})`]; note(`plan-audit receipt table (non-fatal): ${e.message}`); }
|
|
6454
6504
|
return lines(
|
|
@@ -6716,7 +6766,7 @@ export function readFindingsForReport(P) {
|
|
|
6716
6766
|
// The matter's FULL in-scope Nice-class set (incl. services 42/44) — declared classes (top-level OR per-mark)
|
|
6717
6767
|
// else the profile defaults. The dangerous-band floor + its coverage gate must span ALL of these, never a
|
|
6718
6768
|
// goods-only subset (the VELTRIPHEN services-class miss). Strings, deduped.
|
|
6719
|
-
function inScopeClassList(job, profile) {
|
|
6769
|
+
export function inScopeClassList(job, profile) { // @internal — also read by scripts/register-plan-shape.mjs, which must resolve scope exactly as the run did
|
|
6720
6770
|
const fromMarks = Array.isArray(job?.marks) ? job.marks.flatMap((m) => (Array.isArray(m?.classes) ? m.classes : [])) : [];
|
|
6721
6771
|
const declared = [...(Array.isArray(job?.classes) ? job.classes : []), ...fromMarks];
|
|
6722
6772
|
return [...new Set((declared.length ? declared : (profile?.defaultClasses ?? [])).map(String))];
|
|
@@ -7035,7 +7085,14 @@ export function coverageRowAreaLabel(axis, unit) { // @internal
|
|
|
7035
7085
|
export function coverageJudgmentRows(ledgerRows, planExecution) { // @internal
|
|
7036
7086
|
const open = [];
|
|
7037
7087
|
for (const r of ledgerRows ?? []) {
|
|
7038
|
-
|
|
7088
|
+
// `withheld-by-judgment` joins `confirmed-clean` in NOT reaching the reader, and for the opposite
|
|
7089
|
+
// reason. A clean row has nothing to disclose. A withheld one has something to say, and it is
|
|
7090
|
+
// ruled to belong in the run record and the coverage ledger only: nothing is added to the report
|
|
7091
|
+
// (owner, 2026-09-18). A family the reading turn chose not to open, having read the identical question as
|
|
7092
|
+
// a list and found what it needed, is not a gap in the client's search — it is where the work was
|
|
7093
|
+
// spent — and a row saying otherwise would read to a lawyer as an incomplete job.
|
|
7094
|
+
const status = String(r?.status ?? "").toLowerCase();
|
|
7095
|
+
if (!r || status === "confirmed-clean" || status === "withheld-by-judgment") continue;
|
|
7039
7096
|
const reason = String(r.reason ?? "").replace(/\s+/g, " ").trim().slice(0, 160);
|
|
7040
7097
|
const axis = String(r.axis ?? "").toLowerCase();
|
|
7041
7098
|
open.push({ axis, area: coverageRowArea(axis, r.unit), areaLabel: coverageRowAreaLabel(axis, r.unit),
|
|
@@ -8455,6 +8512,42 @@ function postponeRun(e, run, meta = {}) {
|
|
|
8455
8512
|
return { ok: false, postponed: true, resetsAt, codename, fromStage: e.stage, runDir: run?.runDir ?? null };
|
|
8456
8513
|
}
|
|
8457
8514
|
|
|
8515
|
+
/**
|
|
8516
|
+
* The instructed scope a job asks for — what the MATTER named, before any model ran.
|
|
8517
|
+
*
|
|
8518
|
+
* Pulled out of the run so it can be driven straight from a job, because the defect it closes lived
|
|
8519
|
+
* exactly in the seam between the intake gate and this object: the gate counts a job as carrying a
|
|
8520
|
+
* goods description under EITHER spelling, and this stamped only the current one. A job written the
|
|
8521
|
+
* older way passed the gate and landed `goods: null` — the scope file saying the matter named no
|
|
8522
|
+
* goods while the request plainly did — and every reader that asks what the matter covers reads this
|
|
8523
|
+
* file. Nothing said so, because nothing compared the two sites.
|
|
8524
|
+
*
|
|
8525
|
+
* PURE: a job in, a plain object out, no IO.
|
|
8526
|
+
*/
|
|
8527
|
+
export function instructedScopeOf(job) {
|
|
8528
|
+
const markNames = Array.isArray(job?.marks)
|
|
8529
|
+
? job.marks.map((m) => (typeof m === "string" ? m : m?.name)).filter(Boolean)
|
|
8530
|
+
: (job?.markName ?? job?.name ?? null);
|
|
8531
|
+
return {
|
|
8532
|
+
marks: markNames,
|
|
8533
|
+
classes: job?.classes ?? null,
|
|
8534
|
+
jurisdictions: job?.jurisdictions ?? null,
|
|
8535
|
+
// THE GATE'S OWN READING, imported rather than restated. A run's job is folded onto one field at
|
|
8536
|
+
// assembly, so this is normally reading what is already there; it stays for a job handed to this
|
|
8537
|
+
// function directly, and because the gate and the scope disagreeing is the defect it closes.
|
|
8538
|
+
goods: goodsOf(job),
|
|
8539
|
+
customer: job?.customer ?? null,
|
|
8540
|
+
// the geography stamp (enqueue-schema.mjs, "the GEOGRAPHY STAMP": {mode, origin}) — copied
|
|
8541
|
+
// VERBATIM, never recomputed: foldRecipeScope mutates job.jurisdictions on later passes (and
|
|
8542
|
+
// re-stamps origin "saved-search" when it does), so by read time the stamp is the only surviving
|
|
8543
|
+
// record of where the territories came from. Without it here, the frame reconstructs that
|
|
8544
|
+
// provenance from the request prose — a reconstruction validators.matterContext cannot check.
|
|
8545
|
+
// null = the job predates the stamp ("unrecorded", effective-scope.mjs) — an explicit state,
|
|
8546
|
+
// never a missing key.
|
|
8547
|
+
geography: job?.geography ?? null,
|
|
8548
|
+
};
|
|
8549
|
+
}
|
|
8550
|
+
|
|
8458
8551
|
async function pipelineInner(job, opts = {}) {
|
|
8459
8552
|
assertTierSanity();
|
|
8460
8553
|
// The engine binary, first and UNCONDITIONALLY — before the register preflight, which two lanes skip.
|
|
@@ -8872,24 +8965,7 @@ async function pipelineInner(job, opts = {}) {
|
|
|
8872
8965
|
// frame validator compares against THIS file, never against the frame's own paraphrase) —
|
|
8873
8966
|
// paraphrase drift between the request and the frame is a defect, not a style choice.
|
|
8874
8967
|
try {
|
|
8875
|
-
|
|
8876
|
-
? job.marks.map((m) => (typeof m === "string" ? m : m?.name)).filter(Boolean)
|
|
8877
|
-
: (job.markName ?? job.name ?? null);
|
|
8878
|
-
writeFileSync(P.instructedScope, JSON.stringify({
|
|
8879
|
-
marks: markNames,
|
|
8880
|
-
classes: job.classes ?? null,
|
|
8881
|
-
jurisdictions: job.jurisdictions ?? null,
|
|
8882
|
-
goods: job.goods ?? null,
|
|
8883
|
-
customer: job.customer ?? null,
|
|
8884
|
-
// the geography stamp (enqueue-schema.mjs, "the GEOGRAPHY STAMP": {mode, origin}) — copied
|
|
8885
|
-
// VERBATIM, never recomputed: foldRecipeScope mutates job.jurisdictions on later passes (and
|
|
8886
|
-
// re-stamps origin "saved-search" when it does), so by read time the stamp is the only
|
|
8887
|
-
// surviving record of where the territories came from. Without it here, the frame reconstructs
|
|
8888
|
-
// that provenance from the request prose — a reconstruction validators.matterContext cannot
|
|
8889
|
-
// check. null = the job predates the stamp ("unrecorded", effective-scope.mjs) — an explicit
|
|
8890
|
-
// state, never a missing key.
|
|
8891
|
-
geography: job.geography ?? null,
|
|
8892
|
-
}, null, 2) + "\n");
|
|
8968
|
+
writeFileSync(P.instructedScope, JSON.stringify(instructedScopeOf(job), null, 2) + "\n");
|
|
8893
8969
|
} catch (e) { note(`instructed-scope write failed (non-fatal): ${e.message}`); }
|
|
8894
8970
|
// THE STORED DEFAULTS THE ENGINE CANNOT SEARCH — recorded by the run, not only by the plan preview.
|
|
8895
8971
|
//
|
|
@@ -9820,6 +9896,19 @@ async function pipelineInner(job, opts = {}) {
|
|
|
9820
9896
|
const fanInRepairs = [];
|
|
9821
9897
|
const dispatchPlanQids = async (a, qids, repairId, max = 1) => {
|
|
9822
9898
|
if (!planExec || !ctx.registerPlan) return null;
|
|
9899
|
+
// A qid this run has accepted as a capability gap is never sent to the provider again, under any plan
|
|
9900
|
+
// version (envelope-settle.mjs stickyGapsAfter). A whole-axis dispatch becomes the axis's other qids;
|
|
9901
|
+
// the executor's qid-ownership merge keeps the gap's own block as it stands.
|
|
9902
|
+
const sticky = readStickyGaps(P);
|
|
9903
|
+
if (sticky.size) {
|
|
9904
|
+
const wanted = qids?.length ? qids : (ctx.registerPlan.entries ?? []).filter((e) => e.axis === a).map((e) => e.qid);
|
|
9905
|
+
const held = wanted.filter((q) => sticky.has(q));
|
|
9906
|
+
if (held.length) {
|
|
9907
|
+
runLog(run.runDir, { event: "plan-qids-sticky-gap", axis: a, qids: held, action: repairId });
|
|
9908
|
+
qids = wanted.filter((q) => !sticky.has(q));
|
|
9909
|
+
if (!qids.length) return null;
|
|
9910
|
+
}
|
|
9911
|
+
}
|
|
9823
9912
|
if (!repairLedger.canAttempt(repairId, a, { max, epoch: repairEpoch })) return null;
|
|
9824
9913
|
note(`register-unit ${a}: ${repairId} — direct executor dispatch (code, no agent turn) for ${qids?.length ?? "all"} dictated slice(s)`);
|
|
9825
9914
|
let outcome;
|
|
@@ -10280,7 +10369,15 @@ async function pipelineInner(job, opts = {}) {
|
|
|
10280
10369
|
// been a log line. Settling here is idempotent and makes the decision durable.
|
|
10281
10370
|
if (!receiptSettled(P, priorReceipt).settled) await settleEnvelopeAtReceipt("receipt-reuse");
|
|
10282
10371
|
} else {
|
|
10283
|
-
|
|
10372
|
+
// A qid this run has accepted as a capability gap does not ride the ladder again. It leaves `missing`
|
|
10373
|
+
// for `deferred` with the reason it was accepted under, before any dispatch or followup reads the
|
|
10374
|
+
// join, so neither the direct executor nor the plan-join followup re-dictates it.
|
|
10375
|
+
const holdStickyGaps = (j) => {
|
|
10376
|
+
const held = holdStickyGapsIn(j, readStickyGaps(P));
|
|
10377
|
+
if (held !== j) runLog(run.runDir, { event: "plan-qids-sticky-gap", qids: j.missing.filter((q) => !held.missing.includes(q)), action: "held-from-missing" });
|
|
10378
|
+
return held;
|
|
10379
|
+
};
|
|
10380
|
+
let joinRes = holdStickyGaps(joinPlanToBands(ctx.registerPlan, readBands()));
|
|
10284
10381
|
const missingEntriesByAxis = () => {
|
|
10285
10382
|
const byQid = new Map(ctx.registerPlan.entries.map((e) => [e.qid, e]));
|
|
10286
10383
|
const m = new Map();
|
|
@@ -10302,7 +10399,7 @@ async function pipelineInner(job, opts = {}) {
|
|
|
10302
10399
|
runLog(run.runDir, { event: "plan-qids-missing", axis: a, qids, action: "plan-direct-execute" });
|
|
10303
10400
|
await dispatchPlanQids(a, qids, "plan-direct-execute", 2);
|
|
10304
10401
|
}
|
|
10305
|
-
joinRes = joinPlanToBands(ctx.registerPlan, readBands());
|
|
10402
|
+
joinRes = holdStickyGaps(joinPlanToBands(ctx.registerPlan, readBands()));
|
|
10306
10403
|
if (!joinRes.missing.length) deriveNamedBand(ctx); // dispatch landed blocks — re-merge so Layer B reads them
|
|
10307
10404
|
}
|
|
10308
10405
|
if (joinRes.missing.length) {
|
|
@@ -10327,7 +10424,7 @@ async function pipelineInner(job, opts = {}) {
|
|
|
10327
10424
|
const wf = await stage("register-unit", { ...ctx, axis: a }, { force: true, followup, sessionKey: unitKey[a], trigger: "plan-join" });
|
|
10328
10425
|
if (!wf.ok) note(`register-unit ${a}: warm plan-join followup failed (${wf.fail}) — the plan-unexecuted StageFailure below holds the line`);
|
|
10329
10426
|
}
|
|
10330
|
-
joinRes = joinPlanToBands(ctx.registerPlan, readBands());
|
|
10427
|
+
joinRes = holdStickyGaps(joinPlanToBands(ctx.registerPlan, readBands()));
|
|
10331
10428
|
deriveNamedBand(ctx); // the followup appended band blocks — re-merge so Layer B reads them
|
|
10332
10429
|
}
|
|
10333
10430
|
const skeleton = writeExecution(joinRes);
|
|
@@ -10402,6 +10499,7 @@ async function pipelineInner(job, opts = {}) {
|
|
|
10402
10499
|
{ failClass, repairs: fanInRepairs, quantity: joinRes.missing.length });
|
|
10403
10500
|
}
|
|
10404
10501
|
runLog(run.runDir, { event: "plan-execution", executed: joinRes.executed.length, skipped: joinRes.skipped.length, unplanned: joinRes.unplanned.length,
|
|
10502
|
+
awaiting: joinRes.awaiting?.length ?? 0, asked: joinRes.asked?.length ?? 0,
|
|
10405
10503
|
axes: skeleton.map((s) => `${s.axis}:${s.state}`) });
|
|
10406
10504
|
// Decide the deferrals now — before placement-inquiry, which on the evidence run started one second
|
|
10407
10505
|
// after this point on inputs the run had just recorded as unfinished.
|
|
@@ -11610,7 +11708,7 @@ async function pipelineInner(job, opts = {}) {
|
|
|
11610
11708
|
// that designates no floor.
|
|
11611
11709
|
if (owned.length > 0 && !floorBreachAxes.has(a.toLowerCase())) {
|
|
11612
11710
|
const split = splitDeferredByCloseability(ledger, a, escalationGapAxes,
|
|
11613
|
-
{ fullyDeferred: escalationFullyDeferred.has(a.toLowerCase()) });
|
|
11711
|
+
{ fullyDeferred: escalationFullyDeferred.has(a.toLowerCase()), heldUnits: stickyGapUnits(P) });
|
|
11614
11712
|
const openNonDeferred = owned.some((r) => r.status !== "deferred" && r.status !== "coverage-limited");
|
|
11615
11713
|
if (split.held.length > 0 && split.closeable.length === 0 && !openNonDeferred) {
|
|
11616
11714
|
note(`escalation skipped ${a} — capability-gap deferral: the active register provider cannot express those slices, so a re-run re-derives the same refusal (the gap stays open and disclosed)`);
|
|
@@ -11686,7 +11784,7 @@ async function pipelineInner(job, opts = {}) {
|
|
|
11686
11784
|
// while ALSO carrying held rows — and the resumed unit must not be left to think those are
|
|
11687
11785
|
// work it failed to do. Name them, exactly as the envelope's close followup does.
|
|
11688
11786
|
const escHeld = splitDeferredByCloseability(ledger, a, escalationGapAxes,
|
|
11689
|
-
{ fullyDeferred: escalationFullyDeferred.has(a.toLowerCase()) }).held;
|
|
11787
|
+
{ fullyDeferred: escalationFullyDeferred.has(a.toLowerCase()), heldUnits: stickyGapUnits(P) }).held;
|
|
11690
11788
|
if (escHeld.length) {
|
|
11691
11789
|
followup += `\n\nNOT YOURS TO CLOSE — the active register provider cannot express these slices at all, so no re-run can reach them. Leave these Coverage-ledger rows exactly as they are (\`deferred\`, same reason), and do not restate them as searched or clean:\n${escHeld.map((r) => `| ${r.unit} | deferred | ${r.reason} |`).join("\n")}`;
|
|
11692
11790
|
}
|
|
@@ -11762,8 +11860,9 @@ async function pipelineInner(job, opts = {}) {
|
|
|
11762
11860
|
// time, still deferred, still an open floor, still disclosed.
|
|
11763
11861
|
const gapAxes = capabilityGapAxes(ctx.registerPlan, readPlanExecution(ctx));
|
|
11764
11862
|
const fullyDeferred = new Set((ctx.registerPlan ? fullyDeferredAxes(ctx.registerPlan) : []).map((a) => String(a.axis).toLowerCase()));
|
|
11863
|
+
const stickyUnitsNow = stickyGapUnits(P); // a gap this run already accepted is held whatever its row's reason says
|
|
11765
11864
|
const closeabilityByAxis = new Map(deferredAxes.map((a) => [a,
|
|
11766
|
-
splitDeferredByCloseability(ledgerNow, a, gapAxes, { fullyDeferred: fullyDeferred.has(String(a).toLowerCase()) })]));
|
|
11865
|
+
splitDeferredByCloseability(ledgerNow, a, gapAxes, { fullyDeferred: fullyDeferred.has(String(a).toLowerCase()), heldUnits: stickyUnitsNow })]));
|
|
11767
11866
|
const heldAxes = deferredAxes.filter((a) => {
|
|
11768
11867
|
const s = closeabilityByAxis.get(a);
|
|
11769
11868
|
// The `!hasBreach &&` term is retired with the ⭐ floor: it kept an axis carrying a
|
|
@@ -11805,6 +11904,10 @@ async function pipelineInner(job, opts = {}) {
|
|
|
11805
11904
|
? `\n\nThese rows on this axis are NOT yours to close and must stay exactly as they are — the active register provider cannot express those slices at all, so no re-run can reach them. Leave them \`deferred\`, keep their reason, and do not restate them as searched or clean:\n${split.held.map((r) => `| ${r.unit} | deferred | ${r.reason} |`).join("\n")}`
|
|
11806
11905
|
: "";
|
|
11807
11906
|
note(`envelope: closing deferred coverage on ${a} (deadline permits — ${decision.reason})`);
|
|
11907
|
+
// What the re-opened unit is asked to close, and what it is told to leave: the record of the split
|
|
11908
|
+
// this close acted on, so a gap handed back as work is visible in the run's own log.
|
|
11909
|
+
runLog(run.runDir, { event: "envelope-close-rows", axis: a,
|
|
11910
|
+
closeable: split.closeable.map((r) => String(r.unit ?? "")), held: split.held.map((r) => String(r.unit ?? "")) });
|
|
11808
11911
|
const followup = repairFollowup("register-unit:envelope-close", { paths: P, axis: a, rows: rows + heldRows,
|
|
11809
11912
|
supplementalLane: !!ctx.registerPlan?.contract?.supplemental_lane });
|
|
11810
11913
|
const r = await stage("register-unit", { ...ctx, axis: a }, { force: true, followup, sessionKey: unitKey[a], trigger: "envelope" });
|
|
@@ -12241,6 +12344,7 @@ async function pipelineInner(job, opts = {}) {
|
|
|
12241
12344
|
ledger: loadCoverageLedger(run.runDir).rows, // fresh read — post-reopen/re-digest, same source as every gate
|
|
12242
12345
|
planContext: { entries: ctx.registerPlan.entries ?? [], niceClasses: ctx.registerPlan.nice_classes ?? [], regions: ctx.registerPlan.regions ?? [] },
|
|
12243
12346
|
executor: ccExecutor,
|
|
12347
|
+
capabilities: registerCapabilities(), // the contains floor, read the way the plan compile reads it
|
|
12244
12348
|
note: (m) => note(m),
|
|
12245
12349
|
log: (row) => runLog(run.runDir, row), // the orchestrator's own crowd-context-failed row lands in run.jsonl
|
|
12246
12350
|
});
|
|
@@ -15295,7 +15399,7 @@ async function pipelineInner(job, opts = {}) {
|
|
|
15295
15399
|
{
|
|
15296
15400
|
const mark = job.markName ?? job.name ?? job.ref ?? "the matter";
|
|
15297
15401
|
const ref = job.ref ? ` (${job.ref})` : "";
|
|
15298
|
-
const vtag =
|
|
15402
|
+
const vtag = emailVerdictOpts.tier ? ` Overall risk: ${emailVerdictOpts.tier}.` : ""; // the rating, never the reviewer's sign-off word (ruled 2026-09-22)
|
|
15299
15403
|
// self-contained packet (the email body HTML is embedded so the courier needs no path resolution
|
|
15300
15404
|
// across the archive move). The courier sends EXACTLY this — same subject, text and recipient the
|
|
15301
15405
|
// deleted send stages composed, now composed in code.
|
|
@@ -15317,7 +15421,7 @@ async function pipelineInner(job, opts = {}) {
|
|
|
15317
15421
|
// stated reason when no number is held. It used to go to AGENT_WHATSAPP[agent], which is the
|
|
15318
15422
|
// operator on every run because every user shares one agent id.
|
|
15319
15423
|
...whatsappRouting(job, agent),
|
|
15320
|
-
whatsappText: `✅ Clearotron search for ${mark}${ref}
|
|
15424
|
+
whatsappText: `✅ Clearotron search for ${mark}${ref} is done.${vtag} Report: ${published.url}`,
|
|
15321
15425
|
url: published.url, verdict, markName: job.markName ?? job.name ?? null,
|
|
15322
15426
|
};
|
|
15323
15427
|
// A NEW send supersedes any previous one: .sent is PER-SEND idempotence, not per-run-lifetime.
|
|
@@ -15364,7 +15468,7 @@ async function pipelineInner(job, opts = {}) {
|
|
|
15364
15468
|
// .published + .delivered on disk must never read "7/9" on any status surface (nothing runs after
|
|
15365
15469
|
// the packet, so no stage transition would ever finish the display sequence). finalStepFields ⇒ 9/9.
|
|
15366
15470
|
const deliveredAt = new Date().toISOString();
|
|
15367
|
-
writeRunStatus(ctx, { state: "delivered", verdict, statement: emailVerdictOpts.statement ?? undefined, url: published.url, deliveredAt, sendPending: true, ...finalStepFields() });
|
|
15471
|
+
writeRunStatus(ctx, { state: "delivered", verdict, statement: emailVerdictOpts.statement ?? undefined, caption: published.caption ?? undefined, url: published.url, deliveredAt, sendPending: true, ...finalStepFields() });
|
|
15368
15472
|
// — THE POOL COPY LEARNS ITS OWN TERMINAL STATE, HERE AND NOWHERE ELSE.
|
|
15369
15473
|
// `meta.json` cannot carry this: it is composed inside publish, before this line runs, so the state
|
|
15370
15474
|
// did not exist yet when it was written. This is the one moment where the terminal state and the
|
package/driver/publish/index.mjs
CHANGED
|
@@ -14,7 +14,7 @@ import { parseReport, parseAudit, parseSections, parseBlocks, stripInternal, par
|
|
|
14
14
|
import { renderHtml, parseActionBuckets, actYouConditions } from './render.mjs';
|
|
15
15
|
import { buildAudit } from './xlsx.mjs';
|
|
16
16
|
import { parseFindingsJson, parseFindingsJsonLenient, deriveDisplayVerdict, joinFindingToBlock, CLIENT_TIER_BY_COMPOSITE, projectCoverageJudgment } from '../findings-model.mjs';
|
|
17
|
-
import { readStore, requiredAbsent, nonClosingAbsences } from './publish-inputs.mjs'; // — and why an absence did not close
|
|
17
|
+
import { readStore, requiredAbsent, nonClosingAbsences } from './publish-inputs.mjs'; import { coverageFormStamp, readCoverageForm } from '../coverage-form-io.mjs'; import { coverageUnitLabel } from '../coverage-ledger.mjs'; // — and why an absence did not close
|
|
18
18
|
import { clearanceReportData } from './report-data.mjs';
|
|
19
19
|
import { searchDepthRecord, planTerritoriesOf } from './search-depth.mjs'; import { bandRecords } from '../named-band.mjs'; // how much was read to reach the answer, as counts and tokens
|
|
20
20
|
import { parseFrameworkManifest } from '../framework.mjs';
|
|
@@ -251,11 +251,11 @@ function indexRows(runs, { reportFile, linkPrefix = '', showAudit = true, client
|
|
|
251
251
|
<td>${anonMark(r.title, { key: ck, run: r.runId })}</td>
|
|
252
252
|
<td>${anonClient(r.client, ck)}</td>
|
|
253
253
|
<td><span class="b b-${attrValue(r.badge)}">${esc(r.overall)}</span>${qcFailed ? ' <span class="hold" title="machine QC checks failed — see the audit workbook">⚠ QC</span>' : ''}${!client && !qcFailed && r.clientGate?.inputsAbsent?.length ? ` <span class="disc" title="${escAttr(`published without ${r.clientGate.inputsAbsent.join(', ')} — each declared optional, so the release stands; the report was built without it`)}">◦ built without an input</span>` : ''}${
|
|
254
|
-
//
|
|
255
|
-
//
|
|
256
|
-
//
|
|
257
|
-
//
|
|
258
|
-
r.
|
|
254
|
+
// The report's own conclusion beside the band pill (ruled 2026-09-22: the report is the master),
|
|
255
|
+
// never a second summary. A meta.json written before the caption was kept shows the pill alone:
|
|
256
|
+
// its stored statement could say "on hold", which was never true. The pill carries the rating,
|
|
257
|
+
// the single headline on this short surface.
|
|
258
|
+
r.caption ? `<span class="stmt" title="${escAttr(r.caption)}">${esc(r.caption)}</span>` : ''}</td>
|
|
259
259
|
<td>${runCell}</td>${showAudit ? `
|
|
260
260
|
<td>${r.auditFile ? `<a ${runAttrs} href="${linkPrefix}${attrValue(r.runId)}/${attrValue(r.auditFile)}">audit.xlsx</a>` : '—'}</td>` : ''}
|
|
261
261
|
</tr>`;
|
|
@@ -872,6 +872,15 @@ export async function publishReport({ runId, codename, reportMd, auditMd, findin
|
|
|
872
872
|
} catch { /* the workbook row is the record that matters; this line is the second copy */ }
|
|
873
873
|
}
|
|
874
874
|
|
|
875
|
+
// ── THE WAITING FAMILIES THE READING TURN WITHHELD (withheld-families.mjs) ───────────────────────
|
|
876
|
+
//
|
|
877
|
+
// Ruled 2026-09-22: a family the reading turn chose not to ask is recorded with its reason in the run's
|
|
878
|
+
// record and in the audit workbook, and NOT in the report. The coverage form keeps these rows out of
|
|
879
|
+
// the ledger the report is built from, so this sheet is their one reader-facing place. Same builder,
|
|
880
|
+
// same four columns and the same state as the probes above; the area is the driver's own label and the
|
|
881
|
+
// words are the reading turn's reason.
|
|
882
|
+
const withheldFamilies = withheldFamilyRows(runDir ?? dirname(reportMd));
|
|
883
|
+
|
|
875
884
|
// doc 50 — the run's FROZEN framework manifest (band vocabulary). Present on band-doctrine runs;
|
|
876
885
|
// absent on every archived run (they render byte-identically on the legacy paths).
|
|
877
886
|
let framework = null;
|
|
@@ -1134,7 +1143,7 @@ export async function publishReport({ runId, codename, reportMd, auditMd, findin
|
|
|
1134
1143
|
// the same rule (the workbook's own BANNED gate had already started firing on the raw detail —
|
|
1135
1144
|
// advisory, so CI stayed green). reviewReceipts.lint keeps its raw detail for the internal
|
|
1136
1145
|
// readers above (fetchState reads registry-record-coverage's URIs out of it).
|
|
1137
|
-
counts = await buildAudit({ droppedConditions, undispatchedProbes, findings, coverage, contextNotes, coverageJudgment, markAssessment, corrections: correctionsDoc, fetchState, verdict: verdictInfo, jurisdiction, commonLawJoinedTerms, registerOnly, clientGate, lintFailures: deliveryFlagLines(reviewReceipts.lint), productName, registerPublishesRecordPages: runOrigins == null ? null : runOrigins.length > 0, recordLinks: officeLinks?.byUri ?? null }, auditParsed, join(poolRunDir, auditFile), fm.title, fm);
|
|
1146
|
+
counts = await buildAudit({ droppedConditions, undispatchedProbes, withheldFamilies, findings, coverage, contextNotes, coverageJudgment, markAssessment, corrections: correctionsDoc, fetchState, verdict: verdictInfo, jurisdiction, commonLawJoinedTerms, registerOnly, clientGate, lintFailures: deliveryFlagLines(reviewReceipts.lint), productName, registerPublishesRecordPages: runOrigins == null ? null : runOrigins.length > 0, recordLinks: officeLinks?.byUri ?? null }, auditParsed, join(poolRunDir, auditFile), fm.title, fm);
|
|
1138
1147
|
grpRead(join(poolRunDir, auditFile), 0o640);
|
|
1139
1148
|
if (counts?.gateViolations?.length) console.warn(`[audit-workbook] advisory: ${counts.gateViolations.join(' | ')}`);
|
|
1140
1149
|
} catch (e) {
|
|
@@ -1342,6 +1351,7 @@ export async function publishReport({ runId, codename, reportMd, auditMd, findin
|
|
|
1342
1351
|
// writer). Absent on legacy runs — regenIndex re-reads every historical meta.json, so consumers
|
|
1343
1352
|
// null-guard and old rows render byte-identically.
|
|
1344
1353
|
statement: verdictInfo?.statement ?? undefined,
|
|
1354
|
+
caption: fm.overall_caption || undefined, // the report's own conclusion, which the run list quotes
|
|
1345
1355
|
verdict: verdictInfo?.verdict ?? undefined,
|
|
1346
1356
|
// doc 50 — which framework rated this run (custom vs Generic default) + its ladder, for the archive
|
|
1347
1357
|
// card, the email table sort/colour and the per-customer index; absent on archived runs forever.
|
|
@@ -1369,7 +1379,8 @@ export async function publishReport({ runId, codename, reportMd, auditMd, findin
|
|
|
1369
1379
|
// `<origin>/<runId>/report.html` — where the documents sit on disk, which is not an application route.
|
|
1370
1380
|
// Composed rather than spelled here so the report link is built the same way the audit link always was.
|
|
1371
1381
|
const url = reportRouteFor(poolUrl, runId);
|
|
1372
|
-
return { runId, auditFile, counts, total, url, poolRunDir, auditError, findingsError, customerKey: customerKey || 'generic', clientGate
|
|
1382
|
+
return { runId, auditFile, counts, total, url, poolRunDir, auditError, findingsError, customerKey: customerKey || 'generic', clientGate,
|
|
1383
|
+
caption: fm.overall_caption || null }; // the report's own conclusion, for the run record's short surfaces
|
|
1373
1384
|
}
|
|
1374
1385
|
|
|
1375
1386
|
// Compose the notification email body — MARKDOWN (the mail tool renders markdown→HTML; raw HTML gets escaped).
|
|
@@ -1739,17 +1750,13 @@ export function composeEmailHtml(reportMdPath, url, auditFile, names = [], deliv
|
|
|
1739
1750
|
// the engine clamp reasons (opts.conditions = verdict.json reasons). Machinery-only clamps (no client
|
|
1740
1751
|
// item authored) degrade to one generic plain line — never raw engine jargon, never truncated mid-sentence.
|
|
1741
1752
|
const emailConditions = opts?.verdict === 'CONDITIONAL' ? actYouConditions(parseActionBuckets(secs['Actions']).you) : [];
|
|
1742
|
-
// THE
|
|
1743
|
-
//
|
|
1744
|
-
//
|
|
1745
|
-
//
|
|
1746
|
-
//
|
|
1747
|
-
const
|
|
1748
|
-
|
|
1749
|
-
? esc(bound)
|
|
1750
|
-
: `Delivered as ${esc(opts?.verdict ?? '')}${opts?.verdict === 'CONDITIONAL' ? ' — subject to:' : '.'}`;
|
|
1751
|
-
const verdictBound = (opts?.verdict && opts.verdict !== 'CLEAR')
|
|
1752
|
-
? `<p style="margin:0 0 8px;padding:8px 10px;background:#fdeeee;border:1px solid #c98a86;color:#6e1512"><b>${boundHead}</b>${opts.verdict === 'CONDITIONAL' ? `<br>${(emailConditions.length ? emailConditions : ['the open items set out in the report, before relying on a clean result']).map((r) => `• ${cell(String(r))}`).join('<br>')}` : ''}</p>`
|
|
1753
|
+
// THE REPORT IS THE MASTER (ruled 2026-09-22). The banner used to print a second summary composed
|
|
1754
|
+
// beside the report — "On hold — …" on a run the report called conditional, although nothing is ever
|
|
1755
|
+
// held — and before that the delivery gate's word. It now quotes the report: the rating, then the
|
|
1756
|
+
// report's own conclusion (below), and where the report names what only the client can close, those
|
|
1757
|
+
// items under the report's own heading. Nothing here composes a sentence of its own.
|
|
1758
|
+
const verdictBound = opts?.verdict === 'CONDITIONAL'
|
|
1759
|
+
? `<p style="margin:0 0 8px;padding:8px 10px;background:#fdeeee;border:1px solid #c98a86;color:#6e1512"><b>Only you can close these</b><br>${(emailConditions.length ? emailConditions : ['the open items set out in the report, before relying on a clean result']).map((r) => `• ${cell(String(r))}`).join('<br>')}</p>`
|
|
1753
1760
|
: '';
|
|
1754
1761
|
|
|
1755
1762
|
const reviewHeadline = `<div style="${FONT};font-size:11pt;color:#1a1a2e;margin:0 0 14px">`
|
|
@@ -1757,12 +1764,11 @@ export function composeEmailHtml(reportMdPath, url, auditFile, names = [], deliv
|
|
|
1757
1764
|
+ verdictBound
|
|
1758
1765
|
+ `<p style="margin:0 0 8px"><b>${[opts?.productName ? esc(String(opts.productName)) : '', esc(fm.title || '')].filter(Boolean).join(' — ')} (${esc(fm.matter || '')})</b></p>`
|
|
1759
1766
|
// T2 (H5): the derived tier (verdict sidecar, passed by the driver) outranks the
|
|
1760
|
-
// model-authored fm label — one authority on every surface.
|
|
1761
|
-
//
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
? `<p style="margin:0 0 6px"
|
|
1765
|
-
: ((opts?.tier ?? fm.overall_label) ? `<p style="margin:0 0 6px"><b>Overall risk: ${esc(opts?.tier ?? fm.overall_label)}.</b> ${cell(fm.overall_caption || '')}</p>` : ''))
|
|
1767
|
+
// model-authored fm label — one authority on every surface. The rating leads, then the report's own
|
|
1768
|
+
// conclusion verbatim (ruling 244); with no rating recorded, the conclusion stands alone.
|
|
1769
|
+
+ ((opts?.tier ?? fm.overall_label)
|
|
1770
|
+
? `<p style="margin:0 0 6px"><b>Overall risk: ${esc(opts?.tier ?? fm.overall_label)}.</b> ${cell(fm.overall_caption || '')}</p>`
|
|
1771
|
+
: (fm.overall_caption ? `<p style="margin:0 0 6px">${cell(fm.overall_caption)}</p>` : ''))
|
|
1766
1772
|
+ reportLink
|
|
1767
1773
|
+ (fm.handling_note ? `<p style="margin:0 0 8px;color:#7a2b12"><b>Handling note:</b> ${cell(fm.handling_note)}</p>` : '')
|
|
1768
1774
|
// T9 (K3): methodology identity — which profile/framework rated this run (+ verifiable sha).
|
|
@@ -1811,3 +1817,12 @@ function officeLinksFor(findings, recordsByUri, runOrigins) {
|
|
|
1811
1817
|
if (links) console.log(`[record-links] ${links.summary}`);
|
|
1812
1818
|
return links;
|
|
1813
1819
|
}
|
|
1820
|
+
|
|
1821
|
+
/** The coverage form's family rows judged withheld, as audit-workbook coverage rows. Never throws. */
|
|
1822
|
+
export function withheldFamilyRows(runDir) {
|
|
1823
|
+
try {
|
|
1824
|
+
const { rows } = readCoverageForm(runDir, coverageFormStamp(runDir).formName);
|
|
1825
|
+
return (rows ?? []).filter((r) => r?.kind === 'family' && r.status === 'withheld-by-judgment' && r.reason)
|
|
1826
|
+
.map((r) => ({ area: coverageUnitLabel(r.unit), state: 'not-searched', note: String(r.reason) }));
|
|
1827
|
+
} catch { return []; }
|
|
1828
|
+
}
|
package/driver/publish/xlsx.mjs
CHANGED
|
@@ -302,7 +302,7 @@ export function searchRows(auditParsed, { findings = [], joinedTerms = null, reg
|
|
|
302
302
|
const byTerm = new Map();
|
|
303
303
|
for (const n of cl) {
|
|
304
304
|
const key = (n.search_term || '').trim();
|
|
305
|
-
if (!byTerm.has(key)) { byTerm.set(key, { plats: new Set(), results: new Set(), notes: [], gaps: new Set() }); order.push(key); }
|
|
305
|
+
if (!byTerm.has(key)) { byTerm.set(key, { plats: new Set(), results: new Set(), notes: [], gaps: new Set(), gapReasons: new Map() }); order.push(key); }
|
|
306
306
|
const g = byTerm.get(key);
|
|
307
307
|
if (n.platform) g.plats.add(n.platform);
|
|
308
308
|
if (n.result) g.results.add(n.result);
|
|
@@ -310,7 +310,17 @@ export function searchRows(auditParsed, { findings = [], joinedTerms = null, reg
|
|
|
310
310
|
// — the surfaces that could NOT be searched are counted separately from the ones that came back
|
|
311
311
|
// empty. The dedup unions results across platforms, so one gapped surface among many used to vanish
|
|
312
312
|
// entirely into a term-level "0 — clean".
|
|
313
|
-
|
|
313
|
+
// — THE REGISTER'S OWN WORDS, KEPT. A surface that could not be searched arrives here with the
|
|
314
|
+
// reason it gave (`audit-from-spine` puts the gap's `error` into `notes`), and this row used to
|
|
315
|
+
// record only that it was a gap. So a query a provider REFUSED — "content policy (HTTP 400)" —
|
|
316
|
+
// and one that was simply never run rendered as the same sentence, and the reason was in the
|
|
317
|
+
// workbook's hand the whole time. `audit.md` kept it; this file dropped it.
|
|
318
|
+
if (notSearched(n, n.result, n.notes)) {
|
|
319
|
+
const surface = n.platform || '?';
|
|
320
|
+
g.gaps.add(surface);
|
|
321
|
+
const said = plainNote(n.notes || '').trim();
|
|
322
|
+
if (said) g.gapReasons.set(surface, said);
|
|
323
|
+
}
|
|
314
324
|
}
|
|
315
325
|
push({ 'Search term / variant': COMMON_LAW_SECTION, Scope: '', Result: '', Outcome: '', Note: '', _section: true });
|
|
316
326
|
for (const term of order) {
|
|
@@ -329,7 +339,19 @@ export function searchRows(auditParsed, { findings = [], joinedTerms = null, reg
|
|
|
329
339
|
// searched at all. The gap count is carried beside the hit flag, never folded into it.
|
|
330
340
|
const gaps = [...g.gaps];
|
|
331
341
|
const ran = Math.max(0, n - gaps.length);
|
|
332
|
-
|
|
342
|
+
// A REFUSAL AND A DROP ARE DIFFERENT FACTS, and only one of them is about the register. Where the
|
|
343
|
+
// surface said why, its words are quoted; where nothing was recorded, the row says that instead of
|
|
344
|
+
// implying a reason it does not have. A reader deciding whether to re-run needs to know which:
|
|
345
|
+
// a content-policy refusal will refuse again, a query that never ran may simply run.
|
|
346
|
+
const said = gaps.map((s) => [s, g.gapReasons.get(s)]).filter(([, r]) => r);
|
|
347
|
+
const silent = gaps.filter((s) => !g.gapReasons.has(s));
|
|
348
|
+
const reasonNote = said.length
|
|
349
|
+
? ` The surface${said.length === 1 ? '' : 's'} gave a reason: ${said.map(([s, r]) => `${s} — ${r}`).join('; ')}.`
|
|
350
|
+
: '';
|
|
351
|
+
const silentNote = silent.length && said.length
|
|
352
|
+
? ` ${silent.length} recorded no reason (${silent.slice(0, 4).join(', ')}${silent.length > 4 ? ', …' : ''}).`
|
|
353
|
+
: silent.length ? ' No reason was recorded for any of them.' : '';
|
|
354
|
+
const gapNote = gaps.length ? ` ${gaps.length} surface${gaps.length === 1 ? '' : 's'} could not be searched (${gaps.slice(0, 4).join(', ')}${gaps.length > 4 ? ', …' : ''}) — this term is NOT closed across them.${reasonNote}${silentNote}` : '';
|
|
333
355
|
const outcome = anyHit ? (joined ? '→ Findings' : 'Reviewed — not carried as a conflict')
|
|
334
356
|
: gaps.length && !ran ? NOT_SEARCHED_OUTCOME // nothing ran at all: never a closure claim
|
|
335
357
|
: gaps.length ? 'No conflict — partial' // some ran clean, some never ran: say both
|
|
@@ -637,7 +659,7 @@ export async function buildAudit(contract, auditParsed, outPath, mark = '', fm =
|
|
|
637
659
|
// shape and not a new sheet, and the words are the run receipt's own.
|
|
638
660
|
addSheet(wb, 'Coverage & gaps', COVERAGE_COLS,
|
|
639
661
|
[...coverageRows(coverage), ...coverageRows(contract?.droppedConditions || []),
|
|
640
|
-
...coverageRows(contract?.undispatchedProbes || [])], (row, _d, kept) => {
|
|
662
|
+
...coverageRows(contract?.undispatchedProbes || []), ...coverageRows(contract?.withheldFamilies || [])], (row, _d, kept) => {
|
|
641
663
|
if (!kept.has('State')) return;
|
|
642
664
|
const st = row.getCell('State'); const f = STATE_FILL[String(st.value).trim()];
|
|
643
665
|
if (f) { st.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF' + f } }; st.font = { bold: true }; }
|