clearotron 0.3.2 → 0.3.3-beta.1
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 +109 -13
- 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 +29 -11
- package/driver/CHANGELOG.md +49 -0
- package/driver/citation-census.json +3 -3
- package/driver/clearance-variants-record.mjs +12 -1
- package/driver/common-law-coverage-status.mjs +113 -0
- package/driver/config-inventory.mjs +1 -1
- 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/dev-portal.mjs +3 -3
- package/driver/drainer-identity.mjs +1 -1
- package/driver/driver.config.mjs +80 -9
- package/driver/engine/CONTRACT.md +3 -2
- package/driver/engine/anthropic-agent.mjs +34 -7
- 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 +37 -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-knockout.mjs +3 -3
- package/driver/pipeline.mjs +154 -50
- package/driver/plan-run-agreement-verdict.mjs +49 -0
- package/driver/portal-service.mjs +8 -4
- package/driver/progress.mjs +14 -3
- package/driver/publish/index.mjs +41 -26
- package/driver/publish/report-data.mjs +4 -3
- package/driver/publish/xlsx.mjs +26 -4
- package/driver/queue-markers.mjs +44 -0
- package/driver/queue-watch-verdict.mjs +2 -2
- package/driver/reference-score.mjs +10 -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/settle-stamp.mjs +10 -3
- 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 +2 -2
- package/driver/suite-census.json +293 -29
- 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 +8 -0
- package/mcp-server/lib/brief.mjs +16 -12
- 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 +2 -2
- 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 +8 -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/e2e.mjs +1 -1
- 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 +32 -33
- 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/repo-writes.mjs +1 -1
- package/scripts/report-sections-render-check.mjs +7 -3
- package/scripts/score.mjs +7 -1
- package/scripts/settings-render-check.mjs +36 -0
- package/scripts/travelling-predicates.mjs +1 -1
- package/shared/identifier-scan.mjs +22 -5
- package/shared/scroll-settle.mjs +67 -0
package/driver/progress.mjs
CHANGED
|
@@ -194,6 +194,16 @@ const TERMINAL_STATES = new Set(["delivered", "failed", "cancelled"]);
|
|
|
194
194
|
* preflight before an unattended run remains the operational guard for that case; this closes the
|
|
195
195
|
* invisibility for every failure short of it, which is the honest claim.
|
|
196
196
|
*/
|
|
197
|
+
// ── THE REVIEWER'S SIGN-OFF IS NOT THE CLEARANCE'S ANSWER (ruled 2026-09-22) ─────────────────────────
|
|
198
|
+
//
|
|
199
|
+
// The narrative-refutation stage signs the draft off CLEAR, CONDITIONAL or BLOCKING. At the top of the run
|
|
200
|
+
// record as `verdict` it read as the clearance's answer, competed with the rating, and a BLOCKING one was
|
|
201
|
+
// printed to clients as "on hold" although nothing holds delivery on it. It now lives under the stage
|
|
202
|
+
// that produced it, named for what it is: `review.signoff`. The rating (`tier`) is the run's headline.
|
|
203
|
+
// A record written before the move carries the old field; `readSignoff` reads either.
|
|
204
|
+
export const signoffPatch = (signoff) => ({ review: { signoff: signoff ?? null } });
|
|
205
|
+
export const readSignoff = (s) => s?.review?.signoff ?? s?.verdict ?? null;
|
|
206
|
+
|
|
197
207
|
export function writeRunStatus(ctx, patch = {}, runDirOverride = null, { critical = false } = {}) {
|
|
198
208
|
const runDir = runDirOverride ?? ctx?.run?.runDir;
|
|
199
209
|
// — A STATE WRITE THAT CANNOT FIND ITS RUN DIRECTORY SAYS SO. It used to `return` here, in
|
|
@@ -222,6 +232,7 @@ export function writeRunStatus(ctx, patch = {}, runDirOverride = null, { critica
|
|
|
222
232
|
const { __stateReset, ...rest } = patch;
|
|
223
233
|
const old = readRunStatus(runDir);
|
|
224
234
|
const merged = { ...old, ...rest, updatedAt: nowISO() };
|
|
235
|
+
delete merged.verdict; // the retired top-level field: a record rewritten here carries `review.signoff` alone
|
|
225
236
|
if (typeof rest.stepIndex === "number" && typeof old.stepIndex === "number" && old.stepIndex > rest.stepIndex) {
|
|
226
237
|
// keep the furthest step ever reached (label/n/total move together with the index)
|
|
227
238
|
merged.stepIndex = old.stepIndex;
|
|
@@ -378,7 +389,7 @@ export function identitySeed() {
|
|
|
378
389
|
// first-write-wins makes it the honest wall-clock start across any number of resumes. A resume instead
|
|
379
390
|
// records itself: resumedAt (this resume's clock) + attempts (fresh run = 1, each resume +1), and
|
|
380
391
|
// threads __stateReset because the resume guard has deliberately cleared a terminal sentinel. The
|
|
381
|
-
//
|
|
392
|
+
// review/failedStage/reason resets stay (a resumed run owes a fresh outcome);
|
|
382
393
|
// recoveryAttempts/recoveryHistory stay OUT of the seed (they are the park budget's memory).
|
|
383
394
|
export function seedRunStatus(ctx, { resume = false } = {}) {
|
|
384
395
|
const { job, run, agent } = ctx;
|
|
@@ -412,7 +423,7 @@ export function seedRunStatus(ctx, { resume = false } = {}) {
|
|
|
412
423
|
stepIndex: first.index, stepLabel: first.label, stepN: first.n, stepTotal: first.total,
|
|
413
424
|
currentStep: currentStepOf(first),
|
|
414
425
|
lastStage: null,
|
|
415
|
-
|
|
426
|
+
review: null,
|
|
416
427
|
url: null,
|
|
417
428
|
failedStage: null,
|
|
418
429
|
reason: null,
|
|
@@ -489,7 +500,7 @@ export function lineFor(s) {
|
|
|
489
500
|
// The rollup now carries the send state loudly; clearotron-deliver flips sendPending:false on send.
|
|
490
501
|
const pending = s.sendPending === true ? " — 📮 SEND PENDING (email/WhatsApp NOT yet out — run clearotron-deliver)" : "";
|
|
491
502
|
if (s.state === "delivered") {
|
|
492
|
-
const v = s.
|
|
503
|
+
const v = s.tier ? ` (${s.tier})` : ""; // the rating, not the reviewer's sign-off word
|
|
493
504
|
return `- ${head} — delivered${v}${s.url ? ` — ${s.url}` : ""}${pending}`;
|
|
494
505
|
}
|
|
495
506
|
if (s.state === "failed") {
|
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,7 +1351,8 @@ 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,
|
|
1345
|
-
|
|
1354
|
+
caption: fm.overall_caption || undefined, // the report's own conclusion, which the run list quotes
|
|
1355
|
+
review: verdictInfo?.verdict ? { signoff: verdictInfo.verdict } : undefined, // the reviewer's sign-off, not the clearance's answer (ruled 2026-09-22)
|
|
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.
|
|
1348
1358
|
framework: framework ? { key: framework.framework_key, title: framework.title,
|
|
@@ -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
|
+
}
|
|
@@ -98,9 +98,10 @@ export function clearanceReportData({
|
|
|
98
98
|
auditFile: auditFile || null,
|
|
99
99
|
level: { searchLevel: searchLevel ?? null, stageLabel: stageLabel ?? null },
|
|
100
100
|
framework: framework ? { key: framework.framework_key, title: framework.title, bands: framework.bands.map((b) => ({ label: b.label, tone: b.tone })) } : null,
|
|
101
|
-
// The one risk statement
|
|
102
|
-
|
|
103
|
-
|
|
101
|
+
// The rating + the one risk statement — the exact record every other surface joins (spec 64). Named
|
|
102
|
+
// `rating` (ruled 2026-09-22): as `verdict` it read as the clearance's answer and carried the reviewer's
|
|
103
|
+
// sign-off word beside the band. The sign-off stays in the review stage's own record, not the client's.
|
|
104
|
+
rating: verdictInfo ? {
|
|
104
105
|
tier: verdictInfo.tier ?? null,
|
|
105
106
|
badge: verdictInfo.badge ?? null,
|
|
106
107
|
band: verdictInfo.band ?? null,
|
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 }; }
|
package/driver/queue-markers.mjs
CHANGED
|
@@ -78,6 +78,50 @@ export const PROSE_PARTS = {
|
|
|
78
78
|
campaignShape: ".campaignShape.txt", // P2-C (Round-2 §8a): campaign-shape facts — stated launch shape
|
|
79
79
|
};
|
|
80
80
|
|
|
81
|
+
// ── ONE GOODS DESCRIPTION, TWO SPELLINGS, AND EVERY READER GETS THE SAME ANSWER ────────────────────
|
|
82
|
+
//
|
|
83
|
+
// Intake accepts a goods description under the current field or under the older spelling a forwarding
|
|
84
|
+
// agent may still send. That acceptance was the gate's private knowledge: the scope stamp read the
|
|
85
|
+
// current field alone, so a job written the older way passed intake and recorded that the matter named
|
|
86
|
+
// no goods. Both sites were individually right and nothing compared them.
|
|
87
|
+
//
|
|
88
|
+
// Fixing the scope stamp alone moved the seam rather than closing it. Ten other readers — the clearance
|
|
89
|
+
// and knockout prompts, the pharmaceutical test, the product-context derivation, the portal's own row
|
|
90
|
+
// and the plan preview — still read the current field, so the scope file would carry a value the
|
|
91
|
+
// prompts never saw, and the validator comparing the two would report the disagreement as a fault in
|
|
92
|
+
// the frame.
|
|
93
|
+
//
|
|
94
|
+
// SO THE FOLD HAPPENS ONCE, WHERE THE JOB IS ASSEMBLED FOR A RUN, and every reader downstream sees one
|
|
95
|
+
// field. Nothing is written back to the queue file: the manifest stays exactly as it was filed, which is
|
|
96
|
+
// the same rule the intake gate follows about stamping derived values onto a job.
|
|
97
|
+
//
|
|
98
|
+
// Here, in the module that already owns the queue's own vocabulary, because the gate and the assembly
|
|
99
|
+
// must never hold two opinions about what counts. A hand-copied predicate is how they drifted the first
|
|
100
|
+
// time.
|
|
101
|
+
|
|
102
|
+
/** The field names a goods description may arrive under, newest first. */
|
|
103
|
+
export const GOODS_FIELDS = ["goods", "use"];
|
|
104
|
+
|
|
105
|
+
/** The goods description a job carries under any accepted spelling, or null. Pure. */
|
|
106
|
+
export function goodsOf(job) {
|
|
107
|
+
for (const f of GOODS_FIELDS) {
|
|
108
|
+
const v = job?.[f];
|
|
109
|
+
if (v != null && String(v).trim()) return v;
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The same job with its goods description under the current field, whichever spelling it arrived in.
|
|
116
|
+
* Returns the job unchanged when there is nothing to fold, so a job already on the current field is
|
|
117
|
+
* byte-identical and no caller has to ask which it got.
|
|
118
|
+
*/
|
|
119
|
+
export function withFoldedGoods(job) {
|
|
120
|
+
const goods = goodsOf(job);
|
|
121
|
+
if (goods == null || job?.goods === goods) return job;
|
|
122
|
+
return { ...job, goods };
|
|
123
|
+
}
|
|
124
|
+
|
|
81
125
|
/** A consumed claim's sidecars, swept together by runner.mjs cleanupClaimSidecars. */
|
|
82
126
|
export const CLAIM_SIDECAR_SUFFIXES = [".pid", ".meta", ".skips"];
|
|
83
127
|
|
|
@@ -91,8 +91,8 @@ export function queueWatchVerdict({ queueDirs, watched, unitPath, unitError = nu
|
|
|
91
91
|
// anywhere else; a privilege-limited read that answers "fine" is the exact failure this family of
|
|
92
92
|
// checks exists to refuse, and it is the one that would make this guard decoration on the box that
|
|
93
93
|
// matters most.
|
|
94
|
-
if (unitError) return { state: "skip", message: `the .path unit could not be read — ${unitPath}: ${unitError}` };
|
|
95
|
-
if (resolveError) return { state: "skip", message: `the queue dirs could not be resolved — ${resolveError}` };
|
|
94
|
+
if (unitError) return { state: "skip", blocked: true, message: `the .path unit could not be read — ${unitPath}: ${unitError}` };
|
|
95
|
+
if (resolveError) return { state: "skip", blocked: true, message: `the queue dirs could not be resolved — ${resolveError}` };
|
|
96
96
|
|
|
97
97
|
const q = Array.isArray(queueDirs) ? queueDirs : null;
|
|
98
98
|
const w = Array.isArray(watched) ? watched : [];
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
|
|
46
46
|
import { normalizeElement, consonantSkeleton } from "./form-neighbourhood.mjs";
|
|
47
47
|
import { canonicalJurisdictionCode } from "./jurisdiction-codes.mjs";
|
|
48
|
+
import { territoryTier } from "./territory-tiers.mjs";
|
|
48
49
|
|
|
49
50
|
export const REFERENCE_SCHEMA_VERSION = 1;
|
|
50
51
|
|
|
@@ -378,6 +379,12 @@ export function inScope(entry, scopeClasses = [], scopeTerritories = []) {
|
|
|
378
379
|
// that scenario it is five of fifteen entries, which is the difference between a bad round and a fine
|
|
379
380
|
// one. Same shape as the class rule: an entry naming no territory is in scope, and a run that recorded
|
|
380
381
|
// no scope cannot exclude anything.
|
|
382
|
+
// A WORLDWIDE SCOPE EXCLUDES NOTHING ON TERRITORY, for the reason a missing scope excludes nothing: it
|
|
383
|
+
// names no territory an entry could fall outside. Without this a worldwide run compared every entry
|
|
384
|
+
// with the word itself (`GLOBAL`), no entry names that, and the whole reference landed in `excluded`,
|
|
385
|
+
// an empty score that read as a clean one. Which words mean worldwide is the intake's own list
|
|
386
|
+
// (`territoryTier`), not a pattern restated here; `canonTerritory` still guesses nothing about `intl`.
|
|
387
|
+
if (scopeTerritories.some((t) => territoryTier(t) === "worldwide")) return true;
|
|
381
388
|
const terr = (entry?.jurisdictions ?? []).map(canonTerritory).filter(Boolean);
|
|
382
389
|
const scopeTerr = scopeTerritories.map(canonTerritory).filter(Boolean);
|
|
383
390
|
if (scopeTerr.length && terr.length && !terr.some((t) => scopeTerr.includes(t))) return false;
|
|
@@ -911,7 +918,8 @@ export function readVerdict({ verdictDoc = null, knockoutFindings = null, status
|
|
|
911
918
|
const rows = marks
|
|
912
919
|
.map((m) => [m.name, [m.rating, m.ratingQualifier ? `(${m.ratingQualifier})` : null].filter(Boolean).join(" ")].filter(Boolean).join(": "))
|
|
913
920
|
.filter((s) => s.includes(":"));
|
|
914
|
-
const
|
|
921
|
+
const worst = status?.tier ?? status?.verdict ?? null; // `verdict` on a quick-search record written before the move
|
|
922
|
+
const text = [worst ? `worst band ${worst}` : null, ...rows].filter(Boolean).join(" · ");
|
|
915
923
|
if (text) return { clean: clean(text), text, source: "knockout-findings.json + status.json — this lane writes no _driver/verdict.json", why: null };
|
|
916
924
|
}
|
|
917
925
|
return { clean: null, text: null, source: null,
|
|
@@ -2194,7 +2202,7 @@ export function deliveryLine(run) {
|
|
|
2194
2202
|
}
|
|
2195
2203
|
if (run.poolMeta) {
|
|
2196
2204
|
const issued = run.poolMeta.issuedAt ? ` published ${run.poolMeta.issuedAt}` : " publication time not recorded";
|
|
2197
|
-
const verdict = run.poolMeta.verdict ?? run.poolMeta.overall;
|
|
2205
|
+
const verdict = run.poolMeta.review?.signoff ?? run.poolMeta.verdict ?? run.poolMeta.overall;
|
|
2198
2206
|
// NOT PRESERVED stays the honest answer for a pool copy with no stamp — a run archived before the
|
|
2199
2207
|
// stamp existed, or one whose best-effort write failed. An absent stamp is unknown, not a refusal.
|
|
2200
2208
|
return `delivered: NOT PRESERVED — this is a pool copy carrying no settle stamp, so the terminal`
|
|
@@ -41,9 +41,9 @@
|
|
|
41
41
|
// CORRECTED 2026-08-11 — THIS PARAGRAPH USED TO SAY "one qid per office … which is the shape
|
|
42
42
|
// joinPlanToBands already has", AND THAT WAS FALSE ABOUT THE COMPILER IT DOCUMENTS. There is no
|
|
43
43
|
// per-office qid. compileRegisterPlan narrows ONE shared `regions` array and hands it to every entry
|
|
44
|
-
// (register-plan.mjs
|
|
44
|
+
// (register-plan.mjs compileRegisterPlan); the unreachable office produces no entry, so no qid, so no band block, so
|
|
45
45
|
// nothing ever reaches joinPlanToBands' deferred bucket — whose only source is a block stamped
|
|
46
|
-
// `error:true && deferred:true` (register-plan.mjs
|
|
46
|
+
// `error:true && deferred:true` (register-plan.mjs extendRegisterPlan).
|
|
47
47
|
//
|
|
48
48
|
// The consequence was not academic. `deferred_coverage` rode the plan and nothing that a reader sees
|
|
49
49
|
// read it: coverage-form.mjs seeded its deferred rows from skeleton qids alone, so an EU+US matter on a
|