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.
Files changed (96) hide show
  1. package/THIRD-PARTY-NOTICES.md +2 -2
  2. package/build-info.json +2 -2
  3. package/docs/SECURITY-OWASP.md +21 -13
  4. package/docs/SECURITY.md +20 -14
  5. package/docs/architecture/04-configuration-reference.md +3 -2
  6. package/docs/architecture/05-config-governance.md +1 -1
  7. package/docs/architecture/06-operations-runbook.md +1 -1
  8. package/docs/architecture/09-security-and-data.md +16 -11
  9. package/driver/CHANGELOG.md +18 -0
  10. package/driver/binding-layers.mjs +28 -6
  11. package/driver/citation-census.json +3 -3
  12. package/driver/common-law-coverage-status.mjs +52 -0
  13. package/driver/common-law-receipts.mjs +1 -1
  14. package/driver/connotation-search.mjs +5 -5
  15. package/driver/contract-dictation-registry.mjs +1 -1
  16. package/driver/contract-e3-backlog.mjs +1 -1
  17. package/driver/contract-vocabulary.mjs +18 -18
  18. package/driver/degraded-parts.mjs +10 -3
  19. package/driver/dispatch-record.mjs +19 -0
  20. package/driver/doubt-closure-tool.mjs +2 -2
  21. package/driver/driver.config.mjs +39 -6
  22. package/driver/engagement-receipt.mjs +1 -1
  23. package/driver/engine/CONTRACT.md +41 -1
  24. package/driver/engine/anthropic-agent.mjs +56 -4
  25. package/driver/engine/mcp/codex-config.mjs +1 -1
  26. package/driver/engine/mcp/coverage-server.mjs +1 -1
  27. package/driver/engine/mcp/gather-config.mjs +1 -1
  28. package/driver/engine/mcp/recording-server.mjs +31 -4
  29. package/driver/engine/mcp/stdio-server.mjs +45 -7
  30. package/driver/engine/openai-agent.mjs +100 -17
  31. package/driver/engine/session-record.mjs +223 -0
  32. package/driver/enqueue-schema.mjs +2 -2
  33. package/driver/findings-model.mjs +3 -3
  34. package/driver/form-neighbourhood.mjs +7 -7
  35. package/driver/frame-diff-model.mjs +3 -3
  36. package/driver/framework.mjs +22 -0
  37. package/driver/gateway.mjs +94 -12
  38. package/driver/named-band.mjs +2 -0
  39. package/driver/package.json +1 -1
  40. package/driver/phase0.mjs +13 -2
  41. package/driver/pipeline-knockout.mjs +1 -1
  42. package/driver/pipeline.mjs +46 -33
  43. package/driver/portal-families.mjs +3 -3
  44. package/driver/portal-service.mjs +1 -1
  45. package/driver/predelivery-lint.mjs +19 -19
  46. package/driver/publish/index.mjs +1 -1
  47. package/driver/publish/knockout.mjs +5 -4
  48. package/driver/publish/office-record-links.mjs +17 -2
  49. package/driver/publish/publish-inputs.mjs +1 -1
  50. package/driver/publish/render-knockout.mjs +17 -5
  51. package/driver/publish/render.mjs +4 -2
  52. package/driver/record-fold.mjs +177 -0
  53. package/driver/register-count.mjs +18 -1
  54. package/driver/register-plan.mjs +71 -17
  55. package/driver/register-records.mjs +7 -0
  56. package/driver/registry-fidelity.mjs +2 -2
  57. package/driver/repairs.mjs +1 -1
  58. package/driver/report-card-record.mjs +4 -4
  59. package/driver/scope-facts.mjs +11 -3
  60. package/driver/search-policy.mjs +20 -1
  61. package/driver/skills/clearance-common-law/SKILL.md +22 -22
  62. package/driver/skills/clearance-register/digest.md +3 -3
  63. package/driver/skills/clearance-register/register-recipes.md +11 -11
  64. package/driver/skills/clearance-register/status-rules.md +1 -1
  65. package/driver/skills/clearance-register/unit.md +1 -1
  66. package/driver/skills/clearance-search/firm-wide-reasoning.md +3 -2
  67. package/driver/skills/clearance-search/phase2-execution.md +6 -5
  68. package/driver/skills/clearance-search/synthesis-rules.md +4 -4
  69. package/driver/skills/clearance-search/worked-examples.md +39 -39
  70. package/driver/skills/clearance-variants/SKILL.md +16 -16
  71. package/driver/skills/frame-diff/SKILL.md +2 -2
  72. package/driver/stage-context.mjs +27 -1
  73. package/driver/stages.mjs +12 -12
  74. package/driver/suite-census.json +175 -31
  75. package/driver/synthesis-record.mjs +5 -0
  76. package/driver/unit-inventory.mjs +17 -28
  77. package/driver/verify.mjs +6 -6
  78. package/mcp-server/CHANGELOG.md +4 -0
  79. package/mcp-server/lib/evidence.mjs +8 -8
  80. package/mcp-server/lib/knockout.mjs +1 -1
  81. package/mcp-server/package.json +2 -2
  82. package/package.json +2 -2
  83. package/portal-ui/package.json +1 -1
  84. package/providers/_shared/count.mjs +4 -1
  85. package/providers/_shared/enumerate.mjs +61 -22
  86. package/providers/_shared/execute-plan.mjs +2 -0
  87. package/providers/corsearch/src/capabilities.js +1 -1
  88. package/providers/oauth-mcp-bridge/CHANGELOG.md +4 -0
  89. package/providers/oauth-mcp-bridge/package.json +2 -2
  90. package/providers/signa/src/capabilities.js +17 -2
  91. package/providers/signa/src/core.js +234 -37
  92. package/scripts/band-shape-probe.mjs +2 -2
  93. package/scripts/deprecate-below.mjs +28 -4
  94. package/scripts/e2e-first-time.mjs +16 -1
  95. package/scripts/freeze-example-run.mjs +2 -2
  96. 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 THE PARSER CANNOT KEY IS NOT A COMPLIANT ONE. The old line here was
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 recognisable write-up block.
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
- // STRUCTURAL, so it reports without sending a seat to fix it. The depth directive tells the seat
1214
- // WHICH findings get a prose write-up and HOW LONG it may be. It does not ask for the `Finding N —
1215
- // <mark>` heading this parser keys on, so a narrative without one breaks no rule the seat was given,
1216
- // and a warm redo would hand it a correction it has no instruction to satisfy. What is wrong here is
1217
- // that the DEPTH RULES WENT UNVERIFIED — a coverage fact, stated as one.
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 [{ ...check("narrative-write-ups:could-not-read", "narrative-depth", surface, false,
1223
- `this run has ${r.findingsTotal} finding(s) and the narrative carries NO recognisable prose `
1224
- + "write-up block, so the depth rules were not verified on it — neither the band-rank cut nor the "
1225
- + "word cap was applied to anything. This is NOT a fault in the narrative: the depth directive "
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
- if (narrativeMd) checks.push(...narrativeWriteUpChecks({ narrativeMd, findings, depth, manifest }));
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.",
@@ -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 · 018575624](https://euipo.europa.eu/…)` is markdown the synthesis
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 verbatim provider reason for anything missing — the auditable half of "not available".
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]?.unavailable ?? 'no count recorded'}`)
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>, worst band <b style="color:${t.txt}">${esc(overall ?? '')}</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
- const a = designation ? madridMonitor([rec.irNumber]) : page.address(rec);
128
- return a ? { office, label: labelOf(office, a.number), href: a.href, reason: null } : unlinked("unaddressable");
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:76-95 precedent.
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?.unavailable ?? (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((c) => String(c ?? '').trim().toUpperCase()));
1294
+ const deferred = new Set((sc.deferredJurisdictions ?? []).map(orderCode));
1283
1295
  const ordered = [...new Set((sc.jurisdictions ?? [])
1284
- .filter((c) => !deferred.has(String(c ?? '').trim().toUpperCase())).map(orderedTerritoryName))];
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 (${esc(f._recordFetchFailure.cause || 'fetch failed')})</b> — registry details in this card are unverified.</li>`
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) => {