clearotron 0.4.0 → 0.4.1-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/THIRD-PARTY-NOTICES.md +2 -2
- package/build-info.json +2 -2
- package/docs/SECURITY-OWASP.md +21 -13
- package/docs/SECURITY.md +20 -14
- package/docs/architecture/04-configuration-reference.md +3 -2
- package/docs/architecture/05-config-governance.md +1 -1
- package/docs/architecture/06-operations-runbook.md +1 -1
- package/docs/architecture/09-security-and-data.md +16 -11
- package/driver/CHANGELOG.md +18 -0
- package/driver/binding-layers.mjs +28 -6
- package/driver/citation-census.json +3 -3
- package/driver/common-law-coverage-status.mjs +52 -0
- package/driver/common-law-receipts.mjs +1 -1
- package/driver/connotation-search.mjs +5 -5
- package/driver/contract-dictation-registry.mjs +1 -1
- package/driver/contract-e3-backlog.mjs +1 -1
- package/driver/contract-vocabulary.mjs +18 -18
- package/driver/degraded-parts.mjs +10 -3
- package/driver/dispatch-record.mjs +19 -0
- package/driver/doubt-closure-tool.mjs +2 -2
- package/driver/driver.config.mjs +39 -6
- package/driver/engagement-receipt.mjs +1 -1
- package/driver/engine/CONTRACT.md +41 -1
- package/driver/engine/anthropic-agent.mjs +56 -4
- package/driver/engine/mcp/codex-config.mjs +1 -1
- package/driver/engine/mcp/coverage-server.mjs +1 -1
- package/driver/engine/mcp/gather-config.mjs +1 -1
- package/driver/engine/mcp/recording-server.mjs +31 -4
- package/driver/engine/mcp/stdio-server.mjs +45 -7
- package/driver/engine/openai-agent.mjs +100 -17
- package/driver/engine/session-record.mjs +223 -0
- package/driver/enqueue-schema.mjs +2 -2
- package/driver/findings-model.mjs +3 -3
- package/driver/form-neighbourhood.mjs +7 -7
- package/driver/frame-diff-model.mjs +3 -3
- package/driver/framework.mjs +22 -0
- package/driver/gateway.mjs +94 -12
- package/driver/named-band.mjs +2 -0
- package/driver/package.json +1 -1
- package/driver/phase0.mjs +13 -2
- package/driver/pipeline-knockout.mjs +1 -1
- package/driver/pipeline.mjs +46 -33
- package/driver/portal-families.mjs +3 -3
- package/driver/portal-service.mjs +1 -1
- package/driver/predelivery-lint.mjs +19 -19
- package/driver/publish/index.mjs +1 -1
- package/driver/publish/knockout.mjs +5 -4
- package/driver/publish/office-record-links.mjs +17 -2
- package/driver/publish/publish-inputs.mjs +1 -1
- package/driver/publish/render-knockout.mjs +17 -5
- package/driver/publish/render.mjs +4 -2
- package/driver/record-fold.mjs +177 -0
- package/driver/register-count.mjs +18 -1
- package/driver/register-plan.mjs +71 -17
- package/driver/register-records.mjs +7 -0
- package/driver/registry-fidelity.mjs +2 -2
- package/driver/repairs.mjs +1 -1
- package/driver/report-card-record.mjs +4 -4
- package/driver/scope-facts.mjs +11 -3
- package/driver/search-policy.mjs +20 -1
- package/driver/skills/clearance-common-law/SKILL.md +22 -22
- package/driver/skills/clearance-register/digest.md +3 -3
- package/driver/skills/clearance-register/register-recipes.md +11 -11
- package/driver/skills/clearance-register/status-rules.md +1 -1
- package/driver/skills/clearance-register/unit.md +1 -1
- package/driver/skills/clearance-search/firm-wide-reasoning.md +3 -2
- package/driver/skills/clearance-search/phase2-execution.md +6 -5
- package/driver/skills/clearance-search/synthesis-rules.md +4 -4
- package/driver/skills/clearance-search/worked-examples.md +39 -39
- package/driver/skills/clearance-variants/SKILL.md +16 -16
- package/driver/skills/frame-diff/SKILL.md +2 -2
- package/driver/stage-context.mjs +27 -1
- package/driver/stages.mjs +12 -12
- package/driver/suite-census.json +175 -31
- package/driver/synthesis-record.mjs +5 -0
- package/driver/unit-inventory.mjs +17 -28
- package/driver/verify.mjs +6 -6
- package/mcp-server/CHANGELOG.md +4 -0
- package/mcp-server/lib/evidence.mjs +8 -8
- package/mcp-server/lib/knockout.mjs +1 -1
- package/mcp-server/package.json +2 -2
- package/package.json +2 -2
- package/portal-ui/package.json +1 -1
- package/providers/_shared/count.mjs +4 -1
- package/providers/_shared/enumerate.mjs +61 -22
- package/providers/_shared/execute-plan.mjs +2 -0
- package/providers/corsearch/src/capabilities.js +1 -1
- package/providers/oauth-mcp-bridge/CHANGELOG.md +4 -0
- package/providers/oauth-mcp-bridge/package.json +2 -2
- package/providers/signa/src/capabilities.js +17 -2
- package/providers/signa/src/core.js +234 -37
- package/scripts/band-shape-probe.mjs +2 -2
- package/scripts/deprecate-below.mjs +28 -4
- package/scripts/e2e-first-time.mjs +16 -1
- package/scripts/freeze-example-run.mjs +2 -2
- package/shared/scope.mjs +18 -2
|
@@ -1205,29 +1205,25 @@ export function narrativeWriteUpChecks({ narrativeMd, findings, depth, manifest,
|
|
|
1205
1205
|
maxWords: depth?.narrativeWriteUpWords ?? null });
|
|
1206
1206
|
// AN UNGRADED PRODUCT EXAMINES NOTHING, AND THAT IS THE ONLY SILENCE THIS CHECK MAY KEEP.
|
|
1207
1207
|
if (!r.graded) return [];
|
|
1208
|
-
// — A GRADED NARRATIVE
|
|
1208
|
+
// — A GRADED NARRATIVE WITH NO WRITE-UP BLOCK GETS A ROW, NEVER SILENCE. The old line here was
|
|
1209
1209
|
// `if (!r.total && !r.violations.length) return []`, which returned the SAME empty array for product
|
|
1210
|
-
// 4's correct inertness and for a graded run whose narrative carries no
|
|
1211
|
-
// 6 of 22 graded runs on the box are the second kind, and every one of them read as compliant.
|
|
1210
|
+
// 4's correct inertness and for a graded run whose narrative carries no write-up block at all.
|
|
1212
1211
|
//
|
|
1213
|
-
//
|
|
1214
|
-
//
|
|
1215
|
-
//
|
|
1216
|
-
//
|
|
1217
|
-
//
|
|
1212
|
+
// AND THE ROW SAYS THE RULE DOES NOT APPLY, because it does not (the owner's ruling of 2026-10-01). The
|
|
1213
|
+
// depth rule governs per-finding write-ups: which findings get one, and how long each may be. A
|
|
1214
|
+
// sectional narrative — the spine, the verdict, coverage, calibration — carries none, measured on 36
|
|
1215
|
+
// of 43 saved clearance narratives, so the rule has nothing to govern there. This row used to fail
|
|
1216
|
+
// every such clearance for a heading the directive never asks for, which put an INVESTIGATE line on
|
|
1217
|
+
// nearly every delivery that nobody could act on. Whether the directive should ask for per-finding
|
|
1218
|
+
// write-ups at all is a separate question, held for the synthesis redesign.
|
|
1218
1219
|
if (!r.total && !r.violations.length) {
|
|
1219
1220
|
if (!r.findingsTotal)
|
|
1220
1221
|
return [check("narrative-write-ups", "narrative-depth", surface, true,
|
|
1221
1222
|
"no findings on this run, so there is nothing to write up")];
|
|
1222
|
-
return [
|
|
1223
|
-
`this run has ${r.findingsTotal} finding(s)
|
|
1224
|
-
+ "
|
|
1225
|
-
+ "
|
|
1226
|
-
+ "states which findings get a prose write-up and how long it may be, and never asks for the "
|
|
1227
|
-
+ "heading this check keys on. It means the rule is unenforced on this run."),
|
|
1228
|
-
// The flag rides the FAILURE, as everywhere else here, and it is what stops a warm redo handing
|
|
1229
|
-
// the seat a correction no directive lets it satisfy.
|
|
1230
|
-
structural: true }];
|
|
1223
|
+
return [check("narrative-write-ups", "narrative-depth", surface, true,
|
|
1224
|
+
`the depth rule does not apply to a sectional narrative: this run has ${r.findingsTotal} finding(s) `
|
|
1225
|
+
+ "and the narrative carries no per-finding write-up block, so the band-rank cut and the word cap "
|
|
1226
|
+
+ "have nothing to govern")];
|
|
1231
1227
|
}
|
|
1232
1228
|
const coverage = r.unjoined
|
|
1233
1229
|
? ` (${r.examined} of ${r.total} write-ups joined to a finding; ${r.unjoined} could not be, and were not judged for membership)`
|
|
@@ -2200,7 +2196,7 @@ export function cardBudgetChecks({ cardFolds }) {
|
|
|
2200
2196
|
`assembly folded ${folds.length} surface(s) to the level budgets (moved, never deleted): ${folds.map((f) => `${f.surface} → +${f.movedSentences} sentence(s)/${f.movedWords} word(s) into depth`).join("; ")}`)];
|
|
2201
2197
|
}
|
|
2202
2198
|
|
|
2203
|
-
export function runLint({ depth, commonLawGrid, matterContext, clientPartyName, reportMd, clientSummaryMd, narrativeMd, auditMd, recordsByUri, searchedNames, headerName, ratedNames, actionsText, fetchFailures, extraPlatformNames, findings, findingsRaw, actionsRegister, actionsExpected, intakeAsks, askAnswers, cardFolds, verdictDoc, manifest, seniorRights, seniorRightsExpected, markAssessment, markAssessmentExpected, fourAnswers, contentModelExpected, findingsSchemaVersion, placements, ownerScreen, recordCarry, commonLawCarries, searchedJurisdictions = null }) {
|
|
2199
|
+
export function runLint({ depth, commonLawGrid, matterContext, clientPartyName, reportMd, clientSummaryMd, narrativeMd, auditMd, recordsByUri, searchedNames, headerName, ratedNames, actionsText, fetchFailures, extraPlatformNames, findings, narrativeFindings = null, findingsRaw, actionsRegister, actionsExpected, intakeAsks, askAnswers, cardFolds, verdictDoc, manifest, seniorRights, seniorRightsExpected, markAssessment, markAssessmentExpected, fourAnswers, contentModelExpected, findingsSchemaVersion, placements, ownerScreen, recordCarry, commonLawCarries, searchedJurisdictions = null }) {
|
|
2204
2200
|
// WS-B: the run's profile platforms join the vocabulary for this run. Profiles carry store
|
|
2205
2201
|
// DOMAINS by contract, so derive the name tokens a report would actually print: the raw norm
|
|
2206
2202
|
// ("thomasnetcom"), the separator-spaced phrase ("thomasnet com" / "made in china com"), and the
|
|
@@ -2290,7 +2286,9 @@ export function runLint({ depth, commonLawGrid, matterContext, clientPartyName,
|
|
|
2290
2286
|
if (clientSummaryMd) checks.push(...permissionProseChecks({ text: clientSummaryMd, surface: "client-summary", idSuffix: ":client" }));
|
|
2291
2287
|
if (narrativeMd) checks.push(...permissionProseChecks({ text: narrativeMd, surface: "narrative", idSuffix: ":narrative", structural: true }));
|
|
2292
2288
|
// — the narrative's own depth rules, on the narrative surface so they route to the synthesis redo.
|
|
2293
|
-
|
|
2289
|
+
// Its finding headings are in the model's numbering, so they join the record the model wrote when the
|
|
2290
|
+
// delivered record is folded (`narrativeFindings`), and the delivered record otherwise.
|
|
2291
|
+
if (narrativeMd) checks.push(...narrativeWriteUpChecks({ narrativeMd, findings: narrativeFindings ?? findings, depth, manifest }));
|
|
2294
2292
|
// — the same rule on both surfaces the client's words come from. The narrative is where the
|
|
2295
2293
|
// shipped defect was authored; the report is what a client reads, and a repair to one that left the
|
|
2296
2294
|
// other standing is how this family's previous cures kept being re-applied by hand.
|
|
@@ -2669,6 +2667,8 @@ const DELIVERY_LINES = {
|
|
|
2669
2667
|
// The sentence states the CONSEQUENCE, in the receipt's own terms: not "a check failed" but "the rule
|
|
2670
2668
|
// was applied to nothing". Eleven graded runs delivered carrying this, two of them orderable product
|
|
2671
2669
|
// demos, and the only place it was written was one row of a fifty-odd-row receipt nobody routes.
|
|
2670
|
+
// No new run emits it now (a sectional narrative is a passing row that says the rule does not apply);
|
|
2671
|
+
// the line stays so a republished receipt from before reads as it did.
|
|
2672
2672
|
"narrative-write-ups:could-not-read":
|
|
2673
2673
|
"The depth rules for the written-up findings were applied to nothing on this run — the narrative "
|
|
2674
2674
|
+ "carries no block this check can read, so neither the ranking cut nor the length cap was verified.",
|
package/driver/publish/index.mjs
CHANGED
|
@@ -598,7 +598,7 @@ export async function regenSurfaces(poolRoot) {}
|
|
|
598
598
|
* rather than discovered later.
|
|
599
599
|
*
|
|
600
600
|
* WHAT THIS DOES NOT REACH, so the next reader does not assume it does:
|
|
601
|
-
* · the run's PROSE. `- Source: [EUIPO ·
|
|
601
|
+
* · the run's PROSE. `- Source: [EUIPO · 099999992](https://euipo.europa.eu/…)` is markdown the synthesis
|
|
602
602
|
* wrote and renderProse links it like any other markdown. No field distinguishes a prose register
|
|
603
603
|
* citation from a prose evidence citation, so the only way to catch it is to pattern-match the URL —
|
|
604
604
|
* which is the move that produced this defect class in the first place.
|
|
@@ -15,7 +15,7 @@ import { riskTier, TONE_TIER, regenIndex, regenSurfaces, auditRouteFor, markRepo
|
|
|
15
15
|
import { runKnockoutLint, deliveryFlagLines } from '../predelivery-lint.mjs';
|
|
16
16
|
import { note } from '../log.mjs';
|
|
17
17
|
import { addSheet } from './xlsx.mjs';
|
|
18
|
-
import { COUNT_PREDICATES, COUNT_BASIS, countsForMark, countLine, countedMarks, variantFormsLine, disclosedFloor, moreThan } from '../register-count.mjs';
|
|
18
|
+
import { COUNT_PREDICATES, COUNT_BASIS, countsForMark, countLine, countedMarks, variantFormsLine, disclosedFloor, moreThan, countNote } from '../register-count.mjs';
|
|
19
19
|
import { RECORD_BASIS, recordsForMark, recordsLine, listedMarks, normalizeRegisterRecordLinks } from '../register-records.mjs';
|
|
20
20
|
import { reportIdentityFor, productCoverageNote, kebab } from '../search-policy.mjs';
|
|
21
21
|
import { batchMarkName } from '../mark-name.mjs';
|
|
@@ -235,10 +235,11 @@ export async function buildKnockoutWorkbook(findings, receipts, outPath, registe
|
|
|
235
235
|
row['Close variation forms'] = variantFormsLine(e) ?? 'not recorded — this run predates the close-variation column';
|
|
236
236
|
row['Register'] = registerCounts.providerLabel ?? registerCounts.provider ?? '';
|
|
237
237
|
row['Basis'] = COUNT_BASIS;
|
|
238
|
-
// The
|
|
238
|
+
// The reader's line for anything missing. The provider's own reason, or an exception's message, stays
|
|
239
|
+
// in the counts record where it was written (register-count.mjs, countNote).
|
|
239
240
|
row['Notes'] = COUNT_PREDICATES
|
|
240
241
|
.filter((p) => !Number.isFinite(e?.counts?.[p.key]?.total) && disclosedFloor(e?.counts?.[p.key]) === null)
|
|
241
|
-
.map((p) => `${p.label}: ${e?.counts?.[p.key]
|
|
242
|
+
.map((p) => `${p.label}: ${countNote(e?.counts?.[p.key]) ?? 'no count recorded'}`)
|
|
242
243
|
.join(' · ');
|
|
243
244
|
return row;
|
|
244
245
|
});
|
|
@@ -369,7 +370,7 @@ export function composeKnockoutEmail({ findings, framework, overall, reports = [
|
|
|
369
370
|
// says how to read the list instead: a "full report" link on a batch could only be one name standing
|
|
370
371
|
// for all of them. A single-mark run keeps its head link exactly as before — same bytes, same place.
|
|
371
372
|
const only = n === 1 ? urlByMark.get((findings.marks ?? [])[0]?.name) ?? null : null;
|
|
372
|
-
const head = `<p style="${FONT}">Knockout trademark review${job?.ref ? ` — ${esc(job.ref)}` : ''}: <b>${n} mark${n === 1 ? '' : 's'}</b>,
|
|
373
|
+
const head = `<p style="${FONT}">Knockout trademark review${job?.ref ? ` — ${esc(job.ref)}` : ''}: <b>${n} mark${n === 1 ? '' : 's'}</b>, ${n === 1 ? 'rating' : 'highest rating'} <b style="color:${t.txt}">${esc(overall ?? '')}</b>.`
|
|
373
374
|
+ (only ? ` <a href="${esc(only)}">Open the full report</a>${auditUrl ? ` · <a href="${esc(auditUrl)}">audit workbook</a>` : ''}.` : '')
|
|
374
375
|
+ (!only && n > 1 ? ` One report per name below${auditUrl ? `, and one <a href="${esc(auditUrl)}">audit workbook</a> for the batch` : ''}.` : '') + `</p>`;
|
|
375
376
|
// — THE FIRST NON-BLANK, never element 0. `?? fallback` catches null and undefined
|
|
@@ -122,10 +122,25 @@ export function officeRecordLink(rec, uri = "") {
|
|
|
122
122
|
const page = OFFICE_RECORD_PAGES[office];
|
|
123
123
|
const own = designation ? clean(rec.irNumber) : clean(rec.applicationNumber) || clean(rec.registrationNumber);
|
|
124
124
|
const unlinked = (reason) => ({ office, label: labelOf(office, own), href: null, reason });
|
|
125
|
+
const a = page?.address ? (designation ? madridMonitor([rec.irNumber]) : page.address(rec)) : null;
|
|
126
|
+
if (a) return { office, label: labelOf(office, a.number), href: a.href, reason: null };
|
|
127
|
+
// THE REGISTER'S OWN LINK, ONLY WHERE THE TABLE GIVES NONE. Some registers hand over the office's page
|
|
128
|
+
// for a record (`officeUrl`). The table above stays first: each of its addresses was checked against a
|
|
129
|
+
// real record, and on the records measured the register's link pointed at the same office's site in a
|
|
130
|
+
// different form, or at the designated office's page where the table opens the international
|
|
131
|
+
// registration. So the register's link fills only a gap — an office with no address here, or a number
|
|
132
|
+
// the address cannot take — and never replaces an address the table builds. Only an https address, and
|
|
133
|
+
// only beside a number the reader can be shown.
|
|
134
|
+
const given = registerGivenUrl(rec.officeUrl);
|
|
135
|
+
if (given && own) return { office, label: labelOf(office, own), href: given, reason: null };
|
|
125
136
|
if (!page) return unlinked("unknown-office");
|
|
126
137
|
if (!page.address) return unlinked("no-page");
|
|
127
|
-
|
|
128
|
-
|
|
138
|
+
return unlinked("unaddressable");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function registerGivenUrl(v) {
|
|
142
|
+
if (typeof v !== "string") return null;
|
|
143
|
+
try { return new URL(v).protocol === "https:" ? v : null; } catch { return null; }
|
|
129
144
|
}
|
|
130
145
|
|
|
131
146
|
/**
|
|
@@ -133,7 +133,7 @@ export const CALLER_SUPPLIED = {
|
|
|
133
133
|
* publish/index.mjs's source — reading a 1472-line sibling on every import to answer a question that
|
|
134
134
|
* only changes when someone edits the file is the wrong place for it; the dead-key/undeclared-store
|
|
135
135
|
* half runs in publish-input-coverage.test.mjs against the real source, on the
|
|
136
|
-
* dependency-repair.test.mjs
|
|
136
|
+
* dependency-repair.test.mjs precedent (its 15a and 15b tests).
|
|
137
137
|
*
|
|
138
138
|
* `tables` is injectable so the gate can be exercised against a partition nobody shipped, which is
|
|
139
139
|
* otherwise unreachable from a test.
|
|
@@ -51,7 +51,7 @@ import { SUMMARY_BLOCK_LINE, parseSummaryBlocks } from '../../shared/summary-blo
|
|
|
51
51
|
const STRIP = Object.freeze([
|
|
52
52
|
['summary', 'Summary'], ['findings', 'Findings'], ['filings', 'Also considered'], ['next', 'Next steps'],
|
|
53
53
|
]);
|
|
54
|
-
import { COUNT_BASIS, COUNT_PREDICATES, countsForMark, countLine, variantFormsLine, disclosedFloor, moreThan } from '../register-count.mjs';
|
|
54
|
+
import { COUNT_BASIS, COUNT_PREDICATES, countsForMark, countLine, variantFormsLine, disclosedFloor, moreThan, countNote } from '../register-count.mjs';
|
|
55
55
|
import { RECORD_BASIS, recordsForMark, recordsLine } from '../register-records.mjs';
|
|
56
56
|
import { officeLinkSentences } from './office-record-links.mjs';
|
|
57
57
|
import { knockoutFindingViews, splitKnockoutNotes } from '../findings-model.mjs';
|
|
@@ -66,6 +66,7 @@ import { makeClassifyStatus, isAllClass } from '../../providers/_shared/screen.m
|
|
|
66
66
|
import { saysSomethingNew } from '../../shared/says-something-new.mjs';
|
|
67
67
|
import { isNextStepHeading } from '../knockout-next-step.mjs';
|
|
68
68
|
import OFFERED from '../../shared/offered-territories.json' with { type: 'json' }; // the order form's own territory names
|
|
69
|
+
import { normalizeTerritory } from '../../providers/_shared/territory-codes.mjs'; // a territory as written → its code
|
|
69
70
|
|
|
70
71
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
71
72
|
|
|
@@ -492,6 +493,17 @@ const orderedTerritoryName = (code) => {
|
|
|
492
493
|
return ORDER_FORM_NAMES.get(c === 'EM' ? 'EU' : c) ?? '';
|
|
493
494
|
};
|
|
494
495
|
|
|
496
|
+
/**
|
|
497
|
+
* A territory as the order wrote it, as a code. An order carries a code ("EU", "UK") or a country's name
|
|
498
|
+
* ("Norway"), and both name lookups on this page take codes: a name reached them unconverted, came back
|
|
499
|
+
* empty and left the page without a trace (a delivered knockout, 2026-10-08, named two of the four
|
|
500
|
+
* territories it searched). The shared normaliser turns a name into its code; UK is the order form's GB.
|
|
501
|
+
*/
|
|
502
|
+
const orderCode = (t) => {
|
|
503
|
+
const c = normalizeTerritory(t) || String(t ?? '').trim().toUpperCase();
|
|
504
|
+
return c === 'UK' ? 'GB' : c;
|
|
505
|
+
};
|
|
506
|
+
|
|
495
507
|
/** "a, b and c" — the reader's list, not a join on commas. */
|
|
496
508
|
function listWords(items) {
|
|
497
509
|
const xs = (items ?? []).filter(Boolean);
|
|
@@ -561,7 +573,7 @@ function countsSection(marks, registerCounts, positions = '') {
|
|
|
561
573
|
// archived run re-rendering through today's renderer has no close-variation cell because the
|
|
562
574
|
// column did not exist when it was counted, and "no count recorded" would read as a defect in a
|
|
563
575
|
// report that has none. The hover says which.
|
|
564
|
-
const why = c
|
|
576
|
+
const why = countNote(c) ?? (c
|
|
565
577
|
? 'no count recorded'
|
|
566
578
|
: 'not counted on this run — it predates this column, and an archived report re-renders as what it was');
|
|
567
579
|
if (Number.isFinite(c?.total)) return `<td class="num">${esc(String(c.total))}</td>`;
|
|
@@ -1252,7 +1264,7 @@ function ownerCheckFor(ownerChecks, recordId) {
|
|
|
1252
1264
|
function aboutRequestBlock(scope, requestNotes, depthNote = '', productContext = '', searched = '', registerCounts = null) {
|
|
1253
1265
|
const goods = String(scope?.goods ?? '').trim();
|
|
1254
1266
|
const classes = (Array.isArray(scope?.classes) ? scope.classes : []).filter((c) => c || c === 0);
|
|
1255
|
-
const jx = (Array.isArray(scope?.jurisdictions) ? scope.jurisdictions : []).map((t) => territoryName(t)).filter(Boolean);
|
|
1267
|
+
const jx = (Array.isArray(scope?.jurisdictions) ? scope.jurisdictions : []).map((t) => territoryName(orderCode(t))).filter(Boolean);
|
|
1256
1268
|
const asked = goods;
|
|
1257
1269
|
const classLine = classes.length ? classes.join(', ') : '';
|
|
1258
1270
|
const where = jx.length ? listWords(jx) : '';
|
|
@@ -1279,9 +1291,9 @@ function aboutRequestBlock(scope, requestNotes, depthNote = '', productContext =
|
|
|
1279
1291
|
// a territory nothing was counted in.
|
|
1280
1292
|
const on = provider ? `, on ${provider}` : '';
|
|
1281
1293
|
const sc = registerCounts?.scope ?? {};
|
|
1282
|
-
const deferred = new Set((sc.deferredJurisdictions ?? []).map(
|
|
1294
|
+
const deferred = new Set((sc.deferredJurisdictions ?? []).map(orderCode));
|
|
1283
1295
|
const ordered = [...new Set((sc.jurisdictions ?? [])
|
|
1284
|
-
.filter((c) => !deferred.has(
|
|
1296
|
+
.filter((c) => !deferred.has(orderCode(c))).map((c) => orderedTerritoryName(orderCode(c))))];
|
|
1285
1297
|
const namesTheOrder = sc.worldwide !== true && !(sc.unreachableOffices ?? []).length
|
|
1286
1298
|
&& ordered.length > 0 && ordered.every(Boolean);
|
|
1287
1299
|
const counted = namesTheOrder ? `${listWords(ordered)}${on}.`
|
|
@@ -1705,9 +1705,11 @@ function fullDetail(f, card, recordsByUri = new Map()) {
|
|
|
1705
1705
|
// 404-card caveat (2026-07-22) — a cited record the V4-2 closure fetch DEFINITIVELY could not
|
|
1706
1706
|
// retrieve (joinEvidenceStatus stamped `_recordFetchFailure` from the persisted failure list). ONE
|
|
1707
1707
|
// deterministic code-owned line on the card, client render included: the reader must never take this
|
|
1708
|
-
// card's registry values as record-verified. Absent stamp (no join ran / archived runs) ⇒ nothing.
|
|
1708
|
+
// card's registry values as record-verified. Absent stamp (no join ran / archived runs) ⇒ nothing. The
|
|
1709
|
+
// failure's cause is the record's (the persisted failure list and the stamp keep it), never the card's:
|
|
1710
|
+
// it can be an exception's message.
|
|
1709
1711
|
const fetchFailLine = f._recordFetchFailure
|
|
1710
|
-
? `<li class="openitem"><b>Official register record could not be retrieved
|
|
1712
|
+
? `<li class="openitem"><b>Official register record could not be retrieved</b> — registry details in this card are unverified.</li>`
|
|
1711
1713
|
: '';
|
|
1712
1714
|
const regs = (f.owner?.registrations || []).map(r => {
|
|
1713
1715
|
const uri = String(r.uri || '').toLowerCase();
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
|
|
3
|
+
//
|
|
4
|
+
// THE FOLDED RECORD IS THE RECORD FROM THE DELIVERY SEAM ON (design, 2026-10-03).
|
|
5
|
+
//
|
|
6
|
+
// Before the overview and the cards are built, the driver folds a second filing of the same conflict into
|
|
7
|
+
// one finding and renumbers the rest. A synthesis save after that seam — the pre-delivery lint's narrative
|
|
8
|
+
// repair, the stale repair, the fix pass — wrote the model's own record back over the folded one, and
|
|
9
|
+
// nothing folded it again. Measured on a test run: the folded filing stood as its own finding again, every
|
|
10
|
+
// finding after it moved down one place, 19 cards were rebuilt for findings that had only moved, and the
|
|
11
|
+
// last finding shipped without the prose card it was owed, because no card had ever been built at its
|
|
12
|
+
// ordinal.
|
|
13
|
+
//
|
|
14
|
+
// So once the seam is passed, every accepted save folds again on acceptance, and findings.json — what the
|
|
15
|
+
// reviewer after the seam, the cards and the delivery read — is always the folded record. Two things stay
|
|
16
|
+
// in the model's numbering, because the model wrote them in it: its own record, which stays the base for
|
|
17
|
+
// its patches (the call records the save keeps), and whatever is named against that record — a flag's
|
|
18
|
+
// ordinals, the ordinals a save records it touched, the narrative's finding headings. So the record the
|
|
19
|
+
// save wrote is kept beside the folded file while a fold is in force, with the fold's map both ways, and a
|
|
20
|
+
// reader that joins something in the model's numbering reads that record (`modelRecordPath`).
|
|
21
|
+
//
|
|
22
|
+
// THE STATE, in _driver/fold.json:
|
|
23
|
+
// seamPassed — this pass has passed the seam. A pass opens with it cleared (`openPass`) and the seam's
|
|
24
|
+
// own fold sets it, so a save before the seam on a resumed pass writes the model's record,
|
|
25
|
+
// as every save before the seam always has.
|
|
26
|
+
// map — {modelToFolded, foldedToModel} for findings.json as it stands; null when the file is
|
|
27
|
+
// the record as the save wrote it.
|
|
28
|
+
// and _driver/findings-unfolded.json — that record, in the model's numbering, kept only while a fold is
|
|
29
|
+
// in force.
|
|
30
|
+
import { readFileSync, writeFileSync, existsSync, renameSync, rmSync, mkdirSync } from "node:fs";
|
|
31
|
+
import { dirname } from "node:path";
|
|
32
|
+
import { driverDir } from "../shared/driver-dir.mjs";
|
|
33
|
+
import { consolidateFindings, remapActionOrdinals, parseFindingsJson } from "./findings-model.mjs";
|
|
34
|
+
import { parseFrameworkManifest } from "./framework.mjs";
|
|
35
|
+
|
|
36
|
+
export const FOLD_FILE = "fold.json";
|
|
37
|
+
export const UNFOLDED_FILE = "findings-unfolded.json";
|
|
38
|
+
|
|
39
|
+
const bothWays = (ordinalMap) => {
|
|
40
|
+
const modelToFolded = {}, foldedToModel = {};
|
|
41
|
+
for (const [m, f] of ordinalMap) { modelToFolded[m] = f; (foldedToModel[f] ??= []).push(m); }
|
|
42
|
+
return { modelToFolded, foldedToModel };
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Fold a findings document as the delivery seam does: a second filing of the same conflict folds into one
|
|
47
|
+
* finding, the findings renumber, and the actions follow the renumber. `map` is null when nothing folds,
|
|
48
|
+
* and the document is then returned as it came. PURE.
|
|
49
|
+
*/
|
|
50
|
+
export function foldRecord(doc, manifest = null) {
|
|
51
|
+
const list = Array.isArray(doc?.findings) ? doc.findings : [];
|
|
52
|
+
if (list.length < 2) return { doc, merges: [], map: null };
|
|
53
|
+
const { findings, merges, ordinalMap } = consolidateFindings(list, (doc.schema_version ?? 1) >= 4 ? manifest : null);
|
|
54
|
+
if (!merges.length) return { doc, merges: [], map: null };
|
|
55
|
+
const next = { ...doc, findings };
|
|
56
|
+
// spec 64 — a merged-away ordinal remaps to its kept finding's new ordinal
|
|
57
|
+
if (Array.isArray(doc.actions)) next.actions = remapActionOrdinals(doc.actions, ordinalMap);
|
|
58
|
+
return { doc: next, merges, map: bothWays(ordinalMap) };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The fold state of a run, or null before any seam. Never throws. */
|
|
62
|
+
export function readFold(runDir) {
|
|
63
|
+
try { return JSON.parse(readFileSync(driverDir(String(runDir ?? ""), FOLD_FILE), "utf8")); } catch { return null; }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function writeFold(runDir, { seamPassed, map = null, merges = [], error = null }) {
|
|
67
|
+
const at = driverDir(String(runDir ?? ""), FOLD_FILE);
|
|
68
|
+
mkdirSync(dirname(at), { recursive: true });
|
|
69
|
+
const tmp = `${at}.${process.pid}.tmp`;
|
|
70
|
+
writeFileSync(tmp, JSON.stringify({
|
|
71
|
+
_provenance: "the fold of findings.json as it stands: whether this pass has passed the delivery seam, and the map from the model's ordinals to the folded ones and back",
|
|
72
|
+
at: new Date().toISOString(), seamPassed: Boolean(seamPassed), map,
|
|
73
|
+
// ordinals only: the names stay in the record itself
|
|
74
|
+
merges: (merges ?? []).map((m) => ({ kept: m.kept, dropped: m.dropped })),
|
|
75
|
+
...(error ? { error } : {}),
|
|
76
|
+
}, null, 2) + "\n");
|
|
77
|
+
renameSync(tmp, at);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function writeAtomic(file, text) {
|
|
81
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
82
|
+
writeFileSync(tmp, text);
|
|
83
|
+
renameSync(tmp, file);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Fold findings.json in place and record the fold. The seam calls it with `seam: true`; every accepted save
|
|
88
|
+
* calls it after writing the record, and it folds only once this pass has passed the seam. Before the seam
|
|
89
|
+
* the save wrote the model's record as it always has, and the state then says the file is unfolded.
|
|
90
|
+
*
|
|
91
|
+
* Returns `{merges}`, empty when nothing folded, and `error` when a fold could not be made. Never throws: a
|
|
92
|
+
* record that cannot be folded stays as the save wrote it, which is what every run did before this.
|
|
93
|
+
*/
|
|
94
|
+
export function foldFindingsFile(runDir, findingsPath, manifest, { seam = false } = {}) {
|
|
95
|
+
const dir = String(runDir ?? "");
|
|
96
|
+
const state = readFold(dir);
|
|
97
|
+
const unfoldedAt = driverDir(dir, UNFOLDED_FILE);
|
|
98
|
+
let folded = null; // the fold, once findings.json holds it
|
|
99
|
+
try {
|
|
100
|
+
if (!seam && state?.seamPassed !== true) {
|
|
101
|
+
if (state?.map) { writeFold(dir, { seamPassed: false }); rmSync(unfoldedAt, { force: true }); }
|
|
102
|
+
return { merges: [] };
|
|
103
|
+
}
|
|
104
|
+
if (!existsSync(findingsPath)) { writeFold(dir, { seamPassed: true }); return { merges: [] }; }
|
|
105
|
+
const raw = readFileSync(findingsPath, "utf8");
|
|
106
|
+
const { doc, merges, map } = foldRecord(JSON.parse(raw), manifest);
|
|
107
|
+
if (!merges.length) {
|
|
108
|
+
// Nothing folds. On a resumed pass the file can be the earlier pass's fold with no save since: the
|
|
109
|
+
// record and the map it was folded by both stand.
|
|
110
|
+
const held = seam && state?.map && existsSync(unfoldedAt);
|
|
111
|
+
writeFold(dir, held ? { seamPassed: true, map: state.map, merges: state.merges } : { seamPassed: true });
|
|
112
|
+
if (!held) rmSync(unfoldedAt, { force: true });
|
|
113
|
+
return { merges: [] };
|
|
114
|
+
}
|
|
115
|
+
parseFindingsJson(JSON.stringify(doc)); // the folded record must still validate; a throw keeps the record as written
|
|
116
|
+
writeAtomic(unfoldedAt, raw);
|
|
117
|
+
writeAtomic(findingsPath, `${JSON.stringify(doc, null, 2)}\n`);
|
|
118
|
+
folded = { map, merges };
|
|
119
|
+
writeFold(dir, { seamPassed: true, map, merges });
|
|
120
|
+
return { merges };
|
|
121
|
+
} catch (e) {
|
|
122
|
+
// A fold that cannot be made leaves the record as the save wrote it, and the state must say so: a map
|
|
123
|
+
// and a kept record from an earlier fold would send every reader of the model's numbering to a record
|
|
124
|
+
// this save has replaced. The failure is recorded, because nothing else reads this function's return.
|
|
125
|
+
const error = String(e?.message ?? e).replace(/\s+/g, " ").slice(0, 160);
|
|
126
|
+
try {
|
|
127
|
+
const seamPassed = seam || state?.seamPassed === true;
|
|
128
|
+
if (folded) writeFold(dir, { seamPassed, ...folded, error });
|
|
129
|
+
else { rmSync(unfoldedAt, { force: true }); writeFold(dir, { seamPassed, error }); }
|
|
130
|
+
} catch { /* best-effort */ }
|
|
131
|
+
return { merges: [], error };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** The fold an accepted save makes, with the run's frozen framework manifest. Never throws. */
|
|
136
|
+
export function foldAfterSave(runDir, findingsPath) {
|
|
137
|
+
let manifest = null;
|
|
138
|
+
try { manifest = parseFrameworkManifest(readFileSync(driverDir(String(runDir ?? ""), "framework.json"), "utf8")); }
|
|
139
|
+
catch { manifest = null; } // a run with no frozen manifest folds as the seam does for it
|
|
140
|
+
return foldFindingsFile(runDir, findingsPath, manifest);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** A pass opens before the seam. The map stands: the file on disk is still what the last writer left. */
|
|
144
|
+
export function openPass(runDir) {
|
|
145
|
+
const state = readFold(runDir);
|
|
146
|
+
if (!state?.seamPassed) return;
|
|
147
|
+
try { writeFold(runDir, { ...state, seamPassed: false }); } catch { /* best-effort: the seam rewrites it */ }
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** The record in the model's numbering: the one the last save wrote while a fold is in force, else findings.json. */
|
|
151
|
+
export function modelRecordPath(runDir, findingsPath) {
|
|
152
|
+
const at = driverDir(String(runDir ?? ""), UNFOLDED_FILE);
|
|
153
|
+
return readFold(runDir)?.map && existsSync(at) ? at : findingsPath;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** The fold as it stands, to put back if a pass that changed it is rolled back. Never throws. */
|
|
157
|
+
export function snapshotFold(runDir) {
|
|
158
|
+
const state = readFold(runDir);
|
|
159
|
+
let unfolded = null;
|
|
160
|
+
try { if (state?.map) unfolded = readFileSync(driverDir(String(runDir ?? ""), UNFOLDED_FILE), "utf8"); } catch { unfolded = null; }
|
|
161
|
+
return { state, unfolded };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Put a snapshot's fold back beside the findings.json it described. This pass's seam stays as it is. */
|
|
165
|
+
export function restoreFold(runDir, snap) {
|
|
166
|
+
const dir = String(runDir ?? "");
|
|
167
|
+
try {
|
|
168
|
+
const seamPassed = readFold(dir)?.seamPassed === true;
|
|
169
|
+
if (snap?.state?.map && snap.unfolded != null) {
|
|
170
|
+
writeAtomic(driverDir(dir, UNFOLDED_FILE), snap.unfolded);
|
|
171
|
+
writeFold(dir, { seamPassed, map: snap.state.map, merges: snap.state.merges });
|
|
172
|
+
} else {
|
|
173
|
+
rmSync(driverDir(dir, UNFOLDED_FILE), { force: true });
|
|
174
|
+
if (readFold(dir)) writeFold(dir, { seamPassed });
|
|
175
|
+
}
|
|
176
|
+
} catch { /* best-effort: the next save or seam rewrites the state */ }
|
|
177
|
+
}
|
|
@@ -95,7 +95,7 @@ import { resolveRegions } from "./register-plan.mjs";
|
|
|
95
95
|
import { reachableRegions } from "./register-availability.mjs"; // — pure; the env binding is injected
|
|
96
96
|
import { variantForms, VARIANT_RULES, VARIANT_CAP } from "./register-variants.mjs";
|
|
97
97
|
import { isCapabilityGap } from "../providers/_shared/execute-plan.mjs";
|
|
98
|
-
import { plainDeferralReason } from "./deferral-row.mjs"; // — the reader's line for a count left open
|
|
98
|
+
import { plainDeferralReason, plainCause } from "./deferral-row.mjs"; // — the reader's line for a count left open
|
|
99
99
|
|
|
100
100
|
/** The three questions, in report order. `matchMode` is the provider-neutral predicate name.
|
|
101
101
|
*
|
|
@@ -594,6 +594,23 @@ export function disclosedFloor(c) {
|
|
|
594
594
|
/** A floor as every page prints it: the register's own figure, and no sentence around it. */
|
|
595
595
|
export const moreThan = (floor) => `more than ${floor.toLocaleString("en-US")}`;
|
|
596
596
|
|
|
597
|
+
/**
|
|
598
|
+
* THE READER'S LINE FOR A COUNT THAT DID NOT LAND. A count's `unavailable` text is the counts record's: it
|
|
599
|
+
* can carry the provider's own refusal, or an exception's message (`count threw: …`), and that text
|
|
600
|
+
* belongs to the record, never to a cell a client reads. Two lines the counter writes for a reader stay as
|
|
601
|
+
* they are: a name with no near-form, and how many of the close-variation forms went uncounted (without the
|
|
602
|
+
* raw causes that follow it). Anything else reads as the shipped line for a count left open
|
|
603
|
+
* (deferral-row.mjs, plainCause). `null` when the cell has no text. PURE.
|
|
604
|
+
*/
|
|
605
|
+
export function countNote(cell) {
|
|
606
|
+
const t = String(cell?.unavailable ?? "").trim();
|
|
607
|
+
if (!t) return null;
|
|
608
|
+
if (/^no close-variation forms could be generated from ".*" — the name has no near-form under any rule in the table, so there was nothing to count$/.test(t)) return t;
|
|
609
|
+
const partial = /^\d+ of \d+ variant form\(s\) could not be counted, so the total would understate(?= — |$)/.exec(t);
|
|
610
|
+
if (partial) return partial[0];
|
|
611
|
+
return plainCause(t);
|
|
612
|
+
}
|
|
613
|
+
|
|
597
614
|
export function countLine(entry) {
|
|
598
615
|
if (!entry?.counts) return null;
|
|
599
616
|
const parts = COUNT_PREDICATES.map((p) => {
|