clearotron 0.3.3 → 0.4.0-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.
Files changed (142) hide show
  1. package/.env.example +9 -0
  2. package/INSTALL.md +1 -14
  3. package/bin/brandowner.mjs +5 -5
  4. package/bin/connect.mjs +4 -4
  5. package/bin/onboard.mjs +5 -7
  6. package/bin/start.mjs +20 -5
  7. package/bin/update.mjs +6 -1
  8. package/build-info.json +2 -2
  9. package/docs/architecture/04-configuration-reference.md +2 -6
  10. package/driver/CHANGELOG.md +28 -0
  11. package/driver/ask-ledger.mjs +2 -2
  12. package/driver/band-shape.mjs +8 -8
  13. package/driver/blind-frame-model.mjs +1 -1
  14. package/driver/common-law-receipts.mjs +2 -2
  15. package/driver/commonlaw-carry.mjs +2 -2
  16. package/driver/company-bundle.mjs +11 -18
  17. package/driver/connotation-search.mjs +4 -4
  18. package/driver/contract-e3-backlog.mjs +3 -3
  19. package/driver/declination-call.mjs +1 -1
  20. package/driver/declination-tool.mjs +1 -1
  21. package/driver/dev-portal.mjs +1 -1
  22. package/driver/door-call-verdict.mjs +27 -0
  23. package/driver/driver.config.mjs +17 -11
  24. package/driver/e2e/README.md +1 -1
  25. package/driver/engine/mcp/clarivate-server.mjs +2 -1
  26. package/driver/engine/mcp/corsearch-server.mjs +1 -0
  27. package/driver/engine/mcp/euipo-server.mjs +1 -0
  28. package/driver/engine/mcp/free-tier-server.mjs +1 -1
  29. package/driver/engine/mcp/gather-config.mjs +1 -1
  30. package/driver/engine/mcp/perplexity-server.mjs +1 -1
  31. package/driver/engine/mcp/recording-server.mjs +1 -1
  32. package/driver/engine/mcp/signa-server.mjs +1 -0
  33. package/driver/engine/mcp/supplemental.mjs +1 -1
  34. package/driver/engine/mcp/uspto-local-server.mjs +1 -0
  35. package/driver/enqueue-schema.mjs +2 -2
  36. package/driver/feedback-issues.mjs +1 -1
  37. package/driver/feedback-store.mjs +1 -1
  38. package/driver/findings-model.mjs +3 -3
  39. package/driver/flag-snapshot.mjs +1 -1
  40. package/driver/floor-duty.mjs +2 -2
  41. package/driver/form-neighbourhood.mjs +47 -15
  42. package/driver/frame-diff-model.mjs +3 -3
  43. package/driver/gateway.mjs +5 -5
  44. package/driver/jx-lanes.mjs +1 -1
  45. package/driver/known-conflicts.mjs +18 -0
  46. package/driver/log.mjs +2 -2
  47. package/driver/package.json +1 -1
  48. package/driver/pipeline-knockout.mjs +129 -95
  49. package/driver/pipeline.mjs +69 -32
  50. package/driver/placement-carry.mjs +2 -2
  51. package/driver/placement-form.mjs +1 -1
  52. package/driver/portal-mcp-client.mjs +1 -1
  53. package/driver/portal-request-origin.mjs +79 -0
  54. package/driver/portal-service.mjs +45 -17
  55. package/driver/predelivery-lint.mjs +10 -10
  56. package/driver/profile-page.html +9 -13
  57. package/driver/profile-service.mjs +25 -13
  58. package/driver/profiles.mjs +17 -4
  59. package/driver/progress.mjs +1 -1
  60. package/driver/provider-usage.mjs +24 -1
  61. package/driver/publish/index.mjs +17 -5
  62. package/driver/publish/knockout.mjs +3 -2
  63. package/driver/publish/render-knockout.mjs +1 -1
  64. package/driver/publish/render.mjs +14 -3
  65. package/driver/publish/search-depth.mjs +4 -2
  66. package/driver/recall-reconciliation.mjs +1 -1
  67. package/driver/record-carry.mjs +6 -6
  68. package/driver/recording-agreement.mjs +2 -2
  69. package/driver/reference-score.mjs +27 -27
  70. package/driver/register-count.mjs +56 -1
  71. package/driver/register-digest-record.mjs +1 -1
  72. package/driver/register-plan.mjs +5 -5
  73. package/driver/register-records.mjs +10 -1
  74. package/driver/registry-fidelity.mjs +4 -4
  75. package/driver/repair-composers.mjs +6 -6
  76. package/driver/run-economics.mjs +8 -19
  77. package/driver/screen-gate.mjs +1 -1
  78. package/driver/skills/blind-frame/SKILL.md +2 -2
  79. package/driver/skills/clearance-common-law/SKILL.md +2 -2
  80. package/driver/skills/clearance-common-law/perplexity-prompts.md +4 -4
  81. package/driver/skills/clearance-register/digest.md +1 -1
  82. package/driver/skills/clearance-register/unit.md +1 -1
  83. package/driver/skills/clearance-search/report-prose.md +5 -5
  84. package/driver/skills/clearance-search/synthesis-rules.md +4 -4
  85. package/driver/skills/clearance-variants/SKILL.md +2 -2
  86. package/driver/skills/clearance-variants/transliteration-scripts.md +1 -1
  87. package/driver/skills/frame-diff/SKILL.md +2 -2
  88. package/driver/skills/knockout-assess/SKILL.md +9 -9
  89. package/driver/skills/matter-frame/watchlist-reference.md +1 -1
  90. package/driver/skills/narrative-refutation/SKILL.md +2 -2
  91. package/driver/skills/placement-inquiry/SKILL.md +1 -1
  92. package/driver/stage-context.mjs +4 -4
  93. package/driver/stages.mjs +23 -15
  94. package/driver/suite-census.json +138 -42
  95. package/driver/systemd/clearotron-client-mcp.service +24 -0
  96. package/driver/systemd/clearotron-mcp-face.service +24 -0
  97. package/driver/systemd/clearotron-portal.service +24 -0
  98. package/driver/systemd/clearotron-worker.service +24 -0
  99. package/driver/tokens.mjs +26 -17
  100. package/driver/turnaround-bands.mjs +1 -1
  101. package/driver/unit-inventory.mjs +3 -3
  102. package/driver/variant-manifest-model.mjs +1 -1
  103. package/driver/verify.mjs +2 -2
  104. package/driver/whatif-memo-run.mjs +1 -1
  105. package/mcp-server/CHANGELOG.md +8 -0
  106. package/mcp-server/lib/audit.mjs +9 -2
  107. package/mcp-server/lib/http-handler.mjs +7 -3
  108. package/mcp-server/mint-token.mjs +8 -6
  109. package/mcp-server/package.json +1 -1
  110. package/mcp-server/server.mjs +11 -1
  111. package/package.json +1 -1
  112. package/portal-ui/dist/assets/{index-GBbbyQxc.js → index-D_O_55vK.js} +59 -9
  113. package/portal-ui/dist/index.html +1 -1
  114. package/portal-ui/package.json +1 -1
  115. package/providers/_shared/README.md +1 -1
  116. package/providers/_shared/answer-memory.mjs +199 -0
  117. package/providers/_shared/ledger-path.mjs +1 -1
  118. package/providers/_shared/ledger.mjs +47 -5
  119. package/providers/_shared/script-form.mjs +24 -5
  120. package/providers/_shared/term-shape.mjs +5 -5
  121. package/providers/clarivate/src/capabilities.js +11 -0
  122. package/providers/clarivate/src/core.js +140 -13
  123. package/providers/jx-subclass/lookup.mjs +1 -1
  124. package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
  125. package/providers/oauth-mcp-bridge/package.json +1 -1
  126. package/providers/signa/src/capabilities.js +24 -0
  127. package/providers/signa/src/core.js +66 -0
  128. package/scripts/deprecate-below.mjs +114 -2
  129. package/scripts/freeze-example-run.mjs +1 -1
  130. package/scripts/live-surface-check.mjs +11 -2
  131. package/scripts/release-entry-catch-up.mjs +211 -0
  132. package/scripts/release-note-required.mjs +102 -6
  133. package/scripts/release-rehearsal-version.mjs +60 -0
  134. package/scripts/release-sbom.mjs +104 -0
  135. package/scripts/release-visible-check.mjs +7 -5
  136. package/scripts/score.mjs +3 -3
  137. package/shared/brand.mjs +1 -1
  138. package/shared/client-door.mjs +15 -8
  139. package/shared/driver-dir.mjs +20 -9
  140. package/shared/names-in-force.mjs +2 -0
  141. package/shared/scope.mjs +25 -10
  142. package/shared/store-in-repo.mjs +38 -17
@@ -54,6 +54,7 @@ import { capabilitiesFor } from "./register-capabilities.mjs";
54
54
  // — the ONE binding of the office split to this box's env, shared with the plan lane.
55
55
  import { registerUnavailableOffices } from "./register-unreachable.mjs";
56
56
  import { runRecordLogPath } from "../providers/_shared/ledger-path.mjs"; // — this run's record log
57
+ import { beginAnswerMemory, endAnswerMemory } from "../providers/_shared/answer-memory.mjs";
57
58
  import { validators as koValidators, validateMergedFindings, worstBand, registerSurfacedFilings, raterCaveats, SURVIVOR_BOUNDARY_RE } from "./verify-knockout.mjs";
58
59
  import { reviewAbout, reviewEvidence, reviewEvidenceLines, applyKnockoutReview, knockoutReviewFile } from "./knockout-review-record.mjs";
59
60
  import { stripNextStepSections } from "./knockout-next-step.mjs";
@@ -646,6 +647,12 @@ export async function knockoutInner(ctx, job, opts = {}) {
646
647
  ...(ctx.stateReset ? { __stateReset: true } : {}),
647
648
  });
648
649
  rollupStatus(run.studioRoot);
650
+ // THE SIGNA RUN MEMORY'S ATTEMPT (providers/_shared/answer-memory.mjs): whatever an earlier attempt
651
+ // held is dropped, so a re-run asks the register again, and the mode is fixed for this attempt.
652
+ ctx.answerMemory = beginAnswerMemory(run.runDir, REGISTER_PROVIDER);
653
+ if (ctx.answerMemory.applies)
654
+ runLog(run.runDir, { event: "answer-memory", mode: ctx.answerMemory.mode, ...(ctx.answerMemory.unknown ? { unrecognised: ctx.answerMemory.unknown } : {}) });
655
+ if (ctx.answerMemory.unknown) note(`answer memory: ${ctx.answerMemory.switch}="${ctx.answerMemory.unknown}" is not off, watch or on, so this register's default applies (${ctx.answerMemory.mode})`);
649
656
  note(`=== KNOCKOUT batch (${policy.stageLabel}) — ${markNames.length} mark(s), framework ${ctx.framework.framework_key} ===`);
650
657
 
651
658
  // 1 — frame
@@ -661,6 +668,125 @@ export async function knockoutInner(ctx, job, opts = {}) {
661
668
  if (probeWanted) {
662
669
  koStep(ctx, KO_STEP_REGISTER_COUNT);
663
670
  ensureDriverDir(run.runDir);
671
+ // The listing lane, as a step the count lane can run before or after. It returns the listing it
672
+ // wrote, or null when it was refused or failed; either way it has already recorded why.
673
+ const listFilings = async () => {
674
+ let listedDoc = null;
675
+ // ── 1.6 — THE FILINGS BEHIND THE NARROW COUNTS ( part 5) ─────────────────────────────────
676
+ //
677
+ // NEVER TERMINAL, and that is the difference between this lane and the count lane. The counts
678
+ // ARE the product — none of them means no product, so the run dies. The listing EXPLAINS the
679
+ // counts: a batch that got its numbers and could not fetch the faces behind them is a smaller
680
+ // deliverable, not a wrong one, and killing the run would throw away the part that was paid for.
681
+ // Every failure lands as a recorded reason on the surfaces instead.
682
+ const recExec = resolveRecordExecutor({
683
+ lister: opts?.recordLister ?? null, adapter: activeProvider(),
684
+ agentId: agent, sessionKey: `clearance-${run.slug}-${run.codename}`,
685
+ // — this run's record log, not the box's. The knockout lane never reads the record bodies
686
+ // its batch-screen hydration writes; before this they were pure growth on a global file.
687
+ recordLog: runRecordLogPath(run.runDir),
688
+ // The $0 guarantee, threaded rather than re-derived: if the COUNTS ran on fixtures, this lane
689
+ // may not reach a live register. resolveRecordExecutor refuses instead of falling through.
690
+ offline: countExec.source.startsWith("fixtures:"),
691
+ fixtureDir: job?.registerFixtures?.records ?? null,
692
+ });
693
+ const recRefusal = countExec.source.startsWith("fixtures:") && recExec.source === "none"
694
+ // NAMES WHAT THE READER CAN ACT ON. This said `CLEAROTRON_KNOCKOUT_RECORD_FIXTURES` — a variable that
695
+ // no longer exists — so the sentence now names the job field that does.
696
+ ? "this run counted from fixtures and its job declares no `registerFixtures.records` directory, so the "
697
+ + "filings behind the counts were not listed — a fixture run never reaches a live register."
698
+ : recordsPreflight({ capabilities: ctx.registerCaps, hasAdapter: recExec.source !== "none" });
699
+ if (recRefusal) {
700
+ // A structural refusal is RECORDED, not swallowed. Without a sidecar the surfaces cannot tell
701
+ // "this provider does not list filings" from "nobody asked", and the second reads as an
702
+ // omission — the same silence part 2 closed one section up.
703
+ atomicWrite(K.registerRecords, JSON.stringify({
704
+ schema: 1, provider: REGISTER_PROVIDER, providerLabel: ctx.registerCaps?.label ?? REGISTER_PROVIDER,
705
+ takenAt: new Date().toISOString(), unavailable: recRefusal, marks: [],
706
+ }, null, 2) + "\n");
707
+ note(`register filings not listed: ${recRefusal}`);
708
+ runLog(run.runDir, { event: "knockout-register-records-refused", provider: REGISTER_PROVIDER, reason: recRefusal });
709
+ } else {
710
+ try {
711
+ const recDoc = await listRegisterRecords({
712
+ marks: markRows.map((m) => ({ name: String(m.name), classes: Array.isArray(m.classes) ? m.classes : null })),
713
+ classes: job.classes ?? null, jurisdictions: job.jurisdictions ?? null,
714
+ provider: REGISTER_PROVIDER, capabilities: ctx.registerCaps,
715
+ lister: recExec.list, ledgerPath: K.countLedger,
716
+ // — the SAME split the counts used. Re-deriving it here would let the two halves of one
717
+ // report narrow to different scopes; `recExec` can also be a fixture lister while the counts
718
+ // ran live, and the offices a fixture holds are not this box's.
719
+ unreachable: recExec.source === "provider" ? (ctx.registerUnreachable ?? []) : [],
720
+ concurrency: 3, // step 3 — the same constant as the sibling call above
721
+ ...(process.env.CLEAROTRON_KNOCKOUT_RECORD_CAP ? { cap: Number(process.env.CLEAROTRON_KNOCKOUT_RECORD_CAP) } : {}),
722
+ ...(process.env.CLEAROTRON_KNOCKOUT_VARIANT_CAP ? { variantCap: Number(process.env.CLEAROTRON_KNOCKOUT_VARIANT_CAP) } : {}),
723
+ });
724
+ atomicWrite(K.registerRecords, JSON.stringify(recDoc, null, 2) + "\n");
725
+ listedDoc = recDoc;
726
+ runLog(run.runDir, { event: "knockout-register-records", provider: REGISTER_PROVIDER, executor: recExec.source,
727
+ marks: recDoc.marks.length, listed: listedMarks(recDoc), records: recDoc.marks.reduce((n, m) => n + m.records.length, 0) });
728
+ } catch (e) {
729
+ // The lane does not throw per mark, so reaching here means something structural broke. It
730
+ // still must not take the counts down with it — but it must not vanish either.
731
+ note(`register filings listing failed (non-fatal): ${e.message}`);
732
+ runLog(run.runDir, { event: "knockout-register-records-failed", provider: REGISTER_PROVIDER, cause: String(e?.message ?? e).slice(0, 300) });
733
+ }
734
+ }
735
+ return listedDoc;
736
+ };
737
+
738
+ // ── THE OWNER LOOKUP, ONCE THE LISTING HAS NAMED THE OWNER AND A COUNT HAS LANDED ──────────────
739
+ //
740
+ // Run after the count gate below, whichever lane went first. A batch where not one mark got a
741
+ // number stops at that gate; on a register that lists first the lookups used to run before it,
742
+ // research calls the batch paid for and then threw away.
743
+ const checkOwners = async (recDoc) => {
744
+ if (!recDoc) return;
745
+ // On the run that produced the issue, the owner's name was on disk 48 seconds before the sweep
746
+ // started and no pass ever searched it: every sweep keys on the TERM, and nothing re-swept on an
747
+ // OWNER once the register named one. The assessment then inferred what the owner sold from the
748
+ // owner's name, and deferred the real answer to a document the run did not hold.
749
+ //
750
+ // Bounded to promoted filings and deduplicated per owner — on the issue's own run that is ONE
751
+ // extra query. Never throws: a failure produces rows saying the lookup did not answer, and the
752
+ // run publishes (ruling A, 2026-09-07).
753
+ try {
754
+ const owed = ownersOwedACheck(recDoc);
755
+ if (owed.length) {
756
+ const checks = await runOwnerChecks({
757
+ owners: owed, exec: sweep.exec, runDir: run.runDir, ledgerPath: K.ownerCheckLedger,
758
+ preset: process.env.CLEAROTRON_KNOCKOUT_PRESET || "pro-search",
759
+ });
760
+ atomicWrite(K.ownerChecks, JSON.stringify({ schema: 1, checks }, null, 2) + "\n");
761
+ runLog(run.runDir, {
762
+ event: "knockout-owner-checks", owners: owed.length,
763
+ answered: checks.filter((c) => c.ok).length,
764
+ // The count is logged because the ruling is about COST: one promoted filing on the
765
+ // issue's run means one query. A number climbing here is the bound having widened.
766
+ unanswered: checks.filter((c) => !c.ok).length,
767
+ });
768
+ } else {
769
+ runLog(run.runDir, { event: "knockout-owner-checks", owners: 0, answered: 0, unanswered: 0,
770
+ detail: "no promoted filing names a proprietor to search" });
771
+ }
772
+ } catch (e) {
773
+ // Structural only — runOwnerChecks itself never throws. Non-fatal by the same rule as the
774
+ // listing above: a report is never withheld for a check that could not run.
775
+ note(`owner use-check failed (non-fatal): ${e.message}`);
776
+ runLog(run.runDir, { event: "knockout-owner-checks-failed", cause: String(e?.message ?? e).slice(0, 300) });
777
+ }
778
+ };
779
+
780
+ // ── THE LISTING FIRST, ON A REGISTER WHOSE LISTING HOLDS THE COUNT ──────────────────────────────
781
+ //
782
+ // The identical and close columns are the exact predicate over a term, and the listing asks exactly
783
+ // that. On a register that declares its listing carries the register's own total for that question
784
+ // (`listingAnswersCount`), the listing runs first and the count lane takes those two columns from it
785
+ // (register-count.mjs listingAnswers) instead of paying for them again. It still asks what the
786
+ // listing never asks — `containing` — and any term the listing did not answer. Every other register
787
+ // counts first and lists second, as it always has.
788
+ const listFirst = ctx.registerCaps?.listingAnswersCount === true;
789
+ const listedFirst = listFirst ? await listFilings() : null;
664
790
  // resume: a settled prior sidecar is reused cell by cell, so a re-run never re-bills a count that
665
791
  // already landed — including across a build that ADDED a predicate, where only the new column is
666
792
  // bought (register-count.mjs cellSettled).
@@ -683,6 +809,7 @@ export async function knockoutInner(ctx, job, opts = {}) {
683
809
  // to VARIANT_CAP, so this knob can only ever narrow. Unset ⇒ the code cap, which is the answer
684
810
  // for every deployment that has not thought about it.
685
811
  ...(process.env.CLEAROTRON_KNOCKOUT_VARIANT_CAP ? { variantCap: Number(process.env.CLEAROTRON_KNOCKOUT_VARIANT_CAP) } : {}),
812
+ ...(listedFirst ? { listed: listedFirst } : {}),
686
813
  });
687
814
  const counted = countedMarks(doc);
688
815
  // Not one mark got a number. The count IS this product — a batch published now would be a plain
@@ -698,101 +825,7 @@ export async function knockoutInner(ctx, job, opts = {}) {
698
825
  runLog(run.runDir, { event: "knockout-register-counts", provider: REGISTER_PROVIDER, executor: countExec.source, marks: doc.marks.length, counted, regions: doc.scope.regions ?? "worldwide" });
699
826
  if (counted < doc.marks.length) note(`register counts: ${doc.marks.length - counted}/${doc.marks.length} mark(s) unavailable — the batch continues (they publish as "not available", never as zero)`);
700
827
 
701
- // ── 1.6 — THE FILINGS BEHIND THE NARROW COUNTS ( part 5) ─────────────────────────────────
702
- //
703
- // NEVER TERMINAL, and that is the difference between this lane and the one above it. The counts
704
- // ARE the product — none of them means no product, so the run dies. The listing EXPLAINS the
705
- // counts: a batch that got its numbers and could not fetch the faces behind them is a smaller
706
- // deliverable, not a wrong one, and killing the run would throw away the part that was paid for.
707
- // Every failure lands as a recorded reason on the surfaces instead.
708
- const recExec = resolveRecordExecutor({
709
- lister: opts?.recordLister ?? null, adapter: activeProvider(),
710
- agentId: agent, sessionKey: `clearance-${run.slug}-${run.codename}`,
711
- // — this run's record log, not the box's. The knockout lane never reads the record bodies
712
- // its batch-screen hydration writes; before this they were pure growth on a global file.
713
- recordLog: runRecordLogPath(run.runDir),
714
- // The $0 guarantee, threaded rather than re-derived: if the COUNTS ran on fixtures, this lane
715
- // may not reach a live register. resolveRecordExecutor refuses instead of falling through.
716
- offline: countExec.source.startsWith("fixtures:"),
717
- fixtureDir: job?.registerFixtures?.records ?? null,
718
- });
719
- const recRefusal = countExec.source.startsWith("fixtures:") && recExec.source === "none"
720
- // NAMES WHAT THE READER CAN ACT ON. This said `CLEAROTRON_KNOCKOUT_RECORD_FIXTURES` — a variable that
721
- // no longer exists — so the sentence now names the job field that does.
722
- ? "this run counted from fixtures and its job declares no `registerFixtures.records` directory, so the "
723
- + "filings behind the counts were not listed — a fixture run never reaches a live register."
724
- : recordsPreflight({ capabilities: ctx.registerCaps, hasAdapter: recExec.source !== "none" });
725
- if (recRefusal) {
726
- // A structural refusal is RECORDED, not swallowed. Without a sidecar the surfaces cannot tell
727
- // "this provider does not list filings" from "nobody asked", and the second reads as an
728
- // omission — the same silence part 2 closed one section up.
729
- atomicWrite(K.registerRecords, JSON.stringify({
730
- schema: 1, provider: REGISTER_PROVIDER, providerLabel: ctx.registerCaps?.label ?? REGISTER_PROVIDER,
731
- takenAt: new Date().toISOString(), unavailable: recRefusal, marks: [],
732
- }, null, 2) + "\n");
733
- note(`register filings not listed: ${recRefusal}`);
734
- runLog(run.runDir, { event: "knockout-register-records-refused", provider: REGISTER_PROVIDER, reason: recRefusal });
735
- } else {
736
- try {
737
- const recDoc = await listRegisterRecords({
738
- marks: markRows.map((m) => ({ name: String(m.name), classes: Array.isArray(m.classes) ? m.classes : null })),
739
- classes: job.classes ?? null, jurisdictions: job.jurisdictions ?? null,
740
- provider: REGISTER_PROVIDER, capabilities: ctx.registerCaps,
741
- lister: recExec.list, ledgerPath: K.countLedger,
742
- // — the SAME split the counts used. Re-deriving it here would let the two halves of one
743
- // report narrow to different scopes; `recExec` can also be a fixture lister while the counts
744
- // ran live, and the offices a fixture holds are not this box's.
745
- unreachable: recExec.source === "provider" ? (ctx.registerUnreachable ?? []) : [],
746
- concurrency: 3, // step 3 — the same constant as the sibling call above
747
- ...(process.env.CLEAROTRON_KNOCKOUT_RECORD_CAP ? { cap: Number(process.env.CLEAROTRON_KNOCKOUT_RECORD_CAP) } : {}),
748
- ...(process.env.CLEAROTRON_KNOCKOUT_VARIANT_CAP ? { variantCap: Number(process.env.CLEAROTRON_KNOCKOUT_VARIANT_CAP) } : {}),
749
- });
750
- atomicWrite(K.registerRecords, JSON.stringify(recDoc, null, 2) + "\n");
751
- runLog(run.runDir, { event: "knockout-register-records", provider: REGISTER_PROVIDER, executor: recExec.source,
752
- marks: recDoc.marks.length, listed: listedMarks(recDoc), records: recDoc.marks.reduce((n, m) => n + m.records.length, 0) });
753
-
754
- // ── THE OWNER LOOKUP, HERE BECAUSE HERE IS WHERE THE OWNER BECOMES KNOWN ──────────────────────
755
- //
756
- // On the run that produced the issue, the owner's name was on disk 48 seconds before the sweep
757
- // started and no pass ever searched it: every sweep keys on the TERM, and nothing re-swept on an
758
- // OWNER once the register named one. The assessment then inferred what the owner sold from the
759
- // owner's name, and deferred the real answer to a document the run did not hold.
760
- //
761
- // Bounded to promoted filings and deduplicated per owner — on the issue's own run that is ONE
762
- // extra query. Never throws: a failure produces rows saying the lookup did not answer, and the
763
- // run publishes (ruling A, 2026-09-07).
764
- try {
765
- const owed = ownersOwedACheck(recDoc);
766
- if (owed.length) {
767
- const checks = await runOwnerChecks({
768
- owners: owed, exec: sweep.exec, runDir: run.runDir, ledgerPath: K.ownerCheckLedger,
769
- preset: process.env.CLEAROTRON_KNOCKOUT_PRESET || "pro-search",
770
- });
771
- atomicWrite(K.ownerChecks, JSON.stringify({ schema: 1, checks }, null, 2) + "\n");
772
- runLog(run.runDir, {
773
- event: "knockout-owner-checks", owners: owed.length,
774
- answered: checks.filter((c) => c.ok).length,
775
- // The count is logged because the ruling is about COST: one promoted filing on the
776
- // issue's run means one query. A number climbing here is the bound having widened.
777
- unanswered: checks.filter((c) => !c.ok).length,
778
- });
779
- } else {
780
- runLog(run.runDir, { event: "knockout-owner-checks", owners: 0, answered: 0, unanswered: 0,
781
- detail: "no promoted filing names a proprietor to search" });
782
- }
783
- } catch (e) {
784
- // Structural only — runOwnerChecks itself never throws. Non-fatal by the same rule as the
785
- // listing above: a report is never withheld for a check that could not run.
786
- note(`owner use-check failed (non-fatal): ${e.message}`);
787
- runLog(run.runDir, { event: "knockout-owner-checks-failed", cause: String(e?.message ?? e).slice(0, 300) });
788
- }
789
- } catch (e) {
790
- // The lane does not throw per mark, so reaching here means something structural broke. It
791
- // still must not take the counts down with it — but it must not vanish either.
792
- note(`register filings listing failed (non-fatal): ${e.message}`);
793
- runLog(run.runDir, { event: "knockout-register-records-failed", provider: REGISTER_PROVIDER, cause: String(e?.message ?? e).slice(0, 300) });
794
- }
795
- }
828
+ await checkOwners(listFirst ? listedFirst : await listFilings());
796
829
  }
797
830
 
798
831
  // 2 — the sweep: ONE broad code-side research call per mark, receipted, per-mark degrade
@@ -1098,6 +1131,7 @@ export async function knockoutInner(ctx, job, opts = {}) {
1098
1131
  // returns. Same seam, same best-effort contract, no lane-specific exception to write down.
1099
1132
  const stamp = writeSettleStamp(published.poolRunDir, { state: "delivered", tier: overall, deliveredAt, runId: published.runId ?? run.runId, lane: "knockout" });
1100
1133
  if (!stamp.written) note(`delivery: settle stamp not written (${stamp.reason})`);
1134
+ endAnswerMemory(run.runDir); // the held answers do not travel into the archive
1101
1135
  const archived = archive(run);
1102
1136
  rollupStatus(run.studioRoot);
1103
1137
  sentinel(archived ?? run.runDir, ".delivered", { verdict: overall, url: published.url, reports: published.reports.map((r) => ({ mark: r.mark, url: r.url })), notified: "pending", sendPending: true, archived, lane: "knockout" });
@@ -76,7 +76,7 @@ import { armCoverageForm, coverageFormInput, coverageFormPaths, coverageFormStam
76
76
  import { unionPlacementForm } from "./placement-union.mjs";
77
77
  import { readPlacementForm, readPlacementFormInput, writePlacementForm } from "./placement-form-io.mjs";
78
78
  import { dictatedPaths, findStrayArtifacts, treeSnapshot, findStrayInTree, matterSiblings, findStrayMatterSiblings } from "./stray-artifacts.mjs"; // — a run dir holds no document no stage dictated; — nor does the doctrine tree
79
- import { resolveProfile, resolveEffectiveProfile, derivedFloor, derivedBatchSize, applicantMatchesProfile, NEUTRAL_DELIVERY, deliveryForRun, recipeProseGuard, withRunPlatforms, profileStoreResolution } from "./profiles.mjs"; // adds profileStoreResolution — the CONFIG store's identity, beside the doctrine tree's
79
+ import { resolveProfile, resolveEffectiveProfile, derivedFloor, derivedBatchSize, gridBatchFor, applicantMatchesProfile, NEUTRAL_DELIVERY, deliveryForRun, recipeProseGuard, withRunPlatforms, profileStoreResolution } from "./profiles.mjs"; // adds profileStoreResolution — the CONFIG store's identity, beside the doctrine tree's
80
80
  import { resolveSearchPolicy, gateResolvedPolicy, loadRecipes, policyFor, isRegisterOnly, reportIdentityFor, depthFor } from "./search-policy.mjs";
81
81
  import { profileOrdinals } from "./profile-selection.mjs"; // lever 3 — driver selection
82
82
  // THE OFFERING'S own sentence about where the native-language investigation can be bought. It reaches a
@@ -138,6 +138,7 @@ import { tallyRegisterCalls, DEFAULT_LEDGER_PATH, fetchedRecordUris, countLaneCa
138
138
  // this path, because this process runs several runs' pipelines at once and a global address would file
139
139
  // one run's evidence under another.
140
140
  import { runRecordLogPath } from "../providers/_shared/ledger-path.mjs";
141
+ import { beginAnswerMemory, endAnswerMemory } from "../providers/_shared/answer-memory.mjs";
141
142
  import { findScreenGateViolations, findScreenGateParseGaps, screenGateZeroCause } from "./screen-gate.mjs";
142
143
  import { emptyQueue, coerceQueue, mintItem, pendingItems, markFlushed, receiptKeyFor,
143
144
  buildFlushFollowup, runPostFlushGateRepair } from "./digest-queue.mjs"; // (t1cd) — the digest-trigger funnel
@@ -149,7 +150,7 @@ import { stampTokenRollup } from "./tokens.mjs";
149
150
  import { recordRunConsumption } from "./consumption-ledger.mjs";
150
151
  import { quoteForJob, reconcileTurnaround } from "./run-quote.mjs";
151
152
  import { assembleRunRecords, readRecordArtifacts, findRegistryArithmeticIssues, findRegistryViolations, applyRegistryCorrections, extractEnforcerSignals, collectOppositionDeadlines, normalizeRecordUri } from "./registry-fidelity.mjs";
152
- import { readKnownConflictsFor, writeKnownConflictsFor, markKey, acceptedConflicts } from "./known-conflicts.mjs"; // spec 64 — the workspace-level per-mark recall store
153
+ import { readKnownConflictsFor, writeKnownConflictsFor, markKey, acceptedConflicts, recallReceiptForOwnCompany } from "./known-conflicts.mjs"; // spec 64 — the workspace-level per-mark recall store
153
154
  import { runLint, flagLines, properNameCandidates } from "./predelivery-lint.mjs";
154
155
  import { parsePlacementsJson } from "./placement-model.mjs"; // B2 — the structured tier mirror; the lint reads it only to flag an EMPTY one
155
156
  import { readAnchors } from "./anchor-reader.mjs";
@@ -979,7 +980,12 @@ function deriveGridSpec(ctx) {
979
980
  terms: null,
980
981
  spec_inputs: { registerOnly: Boolean(ctx.registerOnly), gridVariants: ctx.gridVariants?.length ?? 0, profilePlatforms: ctx.profile?.platforms?.length ?? 0 },
981
982
  };
982
- if (!ctx.registerOnly && ctx.gridVariants?.length && ctx.profile?.platforms?.length) {
983
+ // AN EMPTY MARKETPLACE LIST STILL WRITES A GRID. A company may pick no marketplaces (the owner's ruling of
984
+ // 2026-09-23), and the general-web cell and the meaning sweep ride in this spec: gating it on a non-empty
985
+ // list switched both off with the stores, and the downgrade clamp then read the missing spec as a failed
986
+ // sweep. The spec below is then the web cell plus whatever channels the matter frame names. Only a profile
987
+ // with no list at all (legacy) takes the spec-less path.
988
+ if (!ctx.registerOnly && ctx.gridVariants?.length && Array.isArray(ctx.profile?.platforms)) {
983
989
  const gridSpecPath = P.gridSpec;
984
990
  // #5 — required channels: a NAMED profile's curated platforms are authoritative. The GENERIC fallback
985
991
  // derives the channels from the MATTER FRAME's industry/goods reasoning (its "Search channels:" line) so
@@ -1041,7 +1047,8 @@ function deriveGridSpec(ctx) {
1041
1047
  terms: ctx.gridVariants,
1042
1048
  platforms: [...channels, "web"], // the dictated channels + the general-web cell
1043
1049
  output_path: P.commonLawGrid,
1044
- batch: ctx.profile?.batchSize ?? 14,
1050
+ // SIZED BY THE CELLS THIS GRID RUNS, never larger than the profile's own figure (gridBatchFor).
1051
+ batch: gridBatchFor(ctx.profile, channels.length + 1),
1045
1052
  // disposition_required (P2-C §8b leg 2): the receipt-presence stamp arming the commonLaw validator's
1046
1053
  // receipts-disposition arm (the D1 ledger_required pattern — every fresh spec carries it; pre-P2-C
1047
1054
  // archived specs lack it, so replay verdicts never flip). splitGridSpec spreads the connotation
@@ -2179,7 +2186,7 @@ function deriveScopeLedgerJson(ctx) {
2179
2186
  // 2026-08-03 — the SEED SET is the union of every distinctive element the manifest names, and falls
2180
2187
  // back to the job's own mark when it names none, so the floor stops moving with the model's choice of anchor
2181
2188
  // and can no longer be empty. See the VARIANT FLOOR section of form-neighbourhood.mjs.
2182
- function deriveFormNeighbourhood(ctx) {
2189
+ export function deriveFormNeighbourhood(ctx) { // @internal — exported for its call-site test
2183
2190
  const P = ctx.paths;
2184
2191
  // — an ABSENT prose manifest no longer returns without an artifact. It used to, and that was the
2185
2192
  // zero-semantics hole at its widest: no manifest ⇒ no form-neighbourhood.json ⇒ compileRegisterPlan's
@@ -2207,14 +2214,19 @@ function deriveFormNeighbourhood(ctx) {
2207
2214
  // The MARK is the floor's fallback seed and the only input to it that is not model output —
2208
2215
  // resolved exactly as every other job-mark consumer resolves it (stages.mjs blind-frame).
2209
2216
  const mark = ctx.job.marks ?? ctx.job.markName ?? ctx.job.name ?? "";
2210
- const ow = loadOrdinaryWords("en"), json = renderFormNeighbourhoodJson(manifestMd, { markets, droppedAxes, model, mark, ordinaryWords: ow.words }); // an unloadable word list removes nothing
2217
+ // The active register's own declaration: `false` leaves the mixed-alphabet look-alike spellings out of
2218
+ // the band and lists them as not searched, with the reason its index gives. Undeclared changes nothing.
2219
+ const caps = registerCapabilities();
2220
+ const mixedScriptQuery = caps?.mixedScriptQuery ?? null;
2221
+ const ow = loadOrdinaryWords("en"), json = renderFormNeighbourhoodJson(manifestMd, { markets, droppedAxes, model, mark, ordinaryWords: ow.words, mixedScriptQuery, nativeScriptIndex: caps?.nativeScriptIndex ?? null }); // an unloadable word list removes nothing
2211
2222
  const tmp = P.formNeighbourhood + ".tmp";
2212
2223
  writeFileSync(tmp, json);
2213
2224
  renameSync(tmp, P.formNeighbourhood);
2214
- let exact = 0, floor = 0, added = 0, seededFrom = "", seeds = [], notSearched = 0;
2225
+ let exact = 0, floor = 0, added = 0, seededFrom = "", seeds = [], notSearched = 0, mixedNotSearched = 0;
2215
2226
  try {
2216
2227
  const o = JSON.parse(json);
2217
2228
  exact = o.elements.reduce((a, e) => a + (e.band?.exactQueries?.length ?? 0), 0); notSearched = o.elements.reduce((a, e) => a + (e.band?.ordinaryWordDifferentSound?.length ?? 0), 0);
2229
+ mixedNotSearched = o.elements.reduce((a, e) => a + (e.band?.mixedScriptNotSearched?.length ?? 0), 0);
2218
2230
  floor = o.variant_floor?.counts?.floor ?? 0;
2219
2231
  added = o.variant_floor?.counts?.model_added ?? 0;
2220
2232
  seededFrom = String(o.seeded_from ?? "");
@@ -2222,7 +2234,7 @@ function deriveFormNeighbourhood(ctx) {
2222
2234
  } catch { /* counts are telemetry only */ }
2223
2235
  // The SEED SET is logged by name, because it is the input that decides whether two runs of the same
2224
2236
  // matter can produce the same floor — a floor count alone cannot tell a stable run from a re-anchored one.
2225
- runLog(P.runDir, { event: "form-neighbourhood-derived", exact, floor, modelAdded: added, droppedAxes, seeds, seededFrom, ordinaryWordNotSearched: notSearched, ...(ow.error ? { ordinaryWordListError: ow.error } : {}) });
2237
+ runLog(P.runDir, { event: "form-neighbourhood-derived", exact, floor, modelAdded: added, droppedAxes, seeds, seededFrom, ordinaryWordNotSearched: notSearched, ...(mixedScriptQuery === false ? { mixedScriptNotSearched: mixedNotSearched } : {}), ...(ow.error ? { ordinaryWordListError: ow.error } : {}) });
2226
2238
  // The fallback is not a quiet degrade: it means this run's variant floor rests on the mark alone
2227
2239
  // because the stage named no usable element. Loud, so the absence is a finding and not a shrug.
2228
2240
  if (seededFrom.startsWith("job mark"))
@@ -3134,7 +3146,7 @@ function recordSynthesisSeam(ctx, r, trigger = null) {
3134
3146
  evidence: r?.fail ? String(r.fail).slice(0, 80) : "",
3135
3147
  reasonFor: (rec, uri) => {
3136
3148
  // — THE 102. This callback used to be zero-arity and constant: every record this pass
3137
- // discarded got the same sentence and `step-silent`, whatever it was. One of them was DELPHIC,
3149
+ // discarded got the same sentence and `step-silent`, whatever it was. One of them was KORPHIC,
3138
3150
  // a lawyer-named gold mark placed on sheet-2 and stamped `carry: "carried"`, and the trace could
3139
3151
  // say only that synthesis had not delivered it. The sibling seam one stage earlier
3140
3152
  // (`recordDigestSeam`) has read its stage's own output per record since it was written; this one
@@ -3324,7 +3336,7 @@ export function deriveRecordCarry(ctx, trigger, { findings = null } = {}) { //
3324
3336
  // DIFFERENT Play Store apps. That collision concentrates in the non-Latin population this exists for,
3325
3337
  // because a Han-script marketplace query returns `?q=…` search URLs.
3326
3338
  //
3327
- // This is the path that lost TIKI TWIST and TIKI TROPICS on R3 — both sat in the run's own records and
3339
+ // This is the path that lost WAVO TWIST and WAVO TROPICS on R3 — both sat in the run's own records and
3328
3340
  // reached no findings list. It has already cost a round.
3329
3341
  //
3330
3342
  // Derived at publish, like the register trace and for the same reason: everything it joins is final by
@@ -9070,6 +9082,12 @@ async function pipelineInner(job, opts = {}) {
9070
9082
  ctx.forceFrom = opts.fromStage != null ? stageOrdinal(opts.fromStage) : null;
9071
9083
  if (opts.fromStage != null && ctx.forceFrom < 0) throw new Error(`--from: unknown stage "${opts.fromStage}"`);
9072
9084
  runLog(run.runDir, { event: "start", agent, resume: isResume, fromStage: opts.fromStage ?? null, job: { id: job.id, slug: run.slug, codename: run.codename, customer: run.customer, forwarder: job.forwarder } });
9085
+ // THE SIGNA RUN MEMORY'S ATTEMPT (providers/_shared/answer-memory.mjs): whatever an earlier attempt held
9086
+ // is dropped, so a resume asks the register again, and the mode is fixed for this attempt.
9087
+ ctx.answerMemory = beginAnswerMemory(run.runDir, REGISTER_PROVIDER);
9088
+ if (ctx.answerMemory.applies)
9089
+ runLog(run.runDir, { event: "answer-memory", mode: ctx.answerMemory.mode, ...(ctx.answerMemory.unknown ? { unrecognised: ctx.answerMemory.unknown } : {}) });
9090
+ if (ctx.answerMemory.unknown) note(`answer memory: ${ctx.answerMemory.switch}="${ctx.answerMemory.unknown}" is not off, watch or on, so this register's default applies (${ctx.answerMemory.mode})`);
9073
9091
  // item 11 — WHICH ARM DID THIS RUN RUN UNDER? Unconditional and three-valued in the AD-4 sense: the row
9074
9092
  // is always written, `seed: null` meaning the ordinary production ordering. A probe arm that is not on
9075
9093
  // the record is an arm whose result cannot be attributed later, and the probe exists to attribute.
@@ -9161,6 +9179,9 @@ async function pipelineInner(job, opts = {}) {
9161
9179
  const candidates = [], candDirectives = [], overflow = [];
9162
9180
  const seen = new Set();
9163
9181
  const mintedQids = new Set(); // qid uniqueness inside the lane — the fold below dedupes on it
9182
+ // term → every company whose delivery remembered it. The searches read every company's rows; the
9183
+ // receipt names whose they were, so an audit lists only its own (recallReceiptForOwnCompany).
9184
+ const companiesOf = new Map();
9164
9185
  for (const r of ranked) {
9165
9186
  const mark = String(r.mark_text).trim();
9166
9187
  if (searchedKeys.has(markKey(mark))) continue; // the primary sweep already owns the searched mark itself
@@ -9176,13 +9197,17 @@ async function pipelineInner(job, opts = {}) {
9176
9197
  // characters", and it answered both with a silent `continue`. The first is a genuine repeat
9177
9198
  // and is still skipped; the second is a distinct mark and now gets a distinct qid.
9178
9199
  const termKey = `${predicate}:${term.toLowerCase()}`;
9200
+ if (!companiesOf.has(termKey)) companiesOf.set(termKey, new Set());
9201
+ companiesOf.get(termKey).add(r.customer || null);
9179
9202
  if (seen.has(termKey)) continue;
9180
9203
  seen.add(termKey);
9181
9204
  const qid = mintSupplementalQid({ prefix, term, used: mintedQids });
9182
9205
  candidates.push({ qid, axis: "primary-sweep", predicate, term, nice_classes: inScope, regions: [], expected_kind: "enumerate" });
9183
- candDirectives.push({ qid, uri: r.uri ?? null, mark_text: mark, owner: r.owner ?? null });
9206
+ candDirectives.push({ qid, uri: r.uri ?? null, mark_text: mark, owner: r.owner ?? null, customers: companiesOf.get(termKey) });
9184
9207
  }
9185
9208
  }
9209
+ for (const d of candDirectives) d.customers = [...d.customers];
9210
+ const customersOfQid = new Map(candDirectives.map((d) => [d.qid, d.customers]));
9186
9211
  // ── — THE SAME DEFECT, AND THE OWNER BUDGET IS WHERE IT BITES ───────────
9187
9212
  //
9188
9213
  // The cross-check lane's fix, applied here because this lane has the identical shape: the caps
@@ -9201,7 +9226,7 @@ async function pipelineInner(job, opts = {}) {
9201
9226
  for (const e of screened.entries) {
9202
9227
  const isOwner = e.predicate === "owner";
9203
9228
  if (isOwner ? ownerUsed >= RECALL_CAP_OWNER : markUsed >= RECALL_CAP_MARK) {
9204
- overflow.push({ qid: e.qid, term: String(e.term).slice(0, 60) }); continue;
9229
+ overflow.push({ qid: e.qid, term: String(e.term).slice(0, 60), customers: customersOfQid.get(e.qid) ?? [] }); continue;
9205
9230
  }
9206
9231
  if (isOwner) ownerUsed++; else markUsed++;
9207
9232
  entries.push(e);
@@ -9234,7 +9259,7 @@ async function pipelineInner(job, opts = {}) {
9234
9259
  note(`recall probes: ${refusedDirectives.length} probe(s) REFUSED as un-searchable terms — recorded in _driver/register-recall.json refused[], never dispatched`);
9235
9260
  }
9236
9261
  if (kept.length || overflow.length || refusedDirectives.length) {
9237
- writeFileSync(`${receiptPath}.tmp`, JSON.stringify({ schema_version: 2, ts: new Date().toISOString(), cap: { mark: RECALL_CAP_MARK, owner: RECALL_CAP_OWNER }, directives: kept, overflow, ...(refusedDirectives.length ? { refused: refusedDirectives } : {}) }, null, 2) + "\n");
9262
+ writeFileSync(`${receiptPath}.tmp`, JSON.stringify({ schema_version: 2, ts: new Date().toISOString(), customer: ctx.profile?.profileKey || null, cap: { mark: RECALL_CAP_MARK, owner: RECALL_CAP_OWNER }, directives: kept, overflow, ...(refusedDirectives.length ? { refused: refusedDirectives } : {}) }, null, 2) + "\n");
9238
9263
  renameSync(`${receiptPath}.tmp`, receiptPath);
9239
9264
  }
9240
9265
  // The RECEIPT partitions; what the seat is TOLD does not. This feeds the primary-sweep prompt's
@@ -10410,7 +10435,7 @@ async function pipelineInner(job, opts = {}) {
10410
10435
  // plan-unexecuted StageFailure below still holds the line for a genuine hole.
10411
10436
  runLog(run.runDir, { event: "plan-qids-missing", axis: a, qids: entries.map((e) => e.qid), action: "fresh-execute-plan" });
10412
10437
  note(`register-unit ${a}: ${entries.length} dictated plan entr${entries.length === 1 ? "y" : "ies"} unexecuted on a resumed-past axis — one fresh execute_plan-only call`);
10413
- const freshMsg = repairFollowup("register-unit:plan-join-fresh", { axis: a, registerPlan: P.registerPlan, bandPath: P.registerBand(a) });
10438
+ const freshMsg = repairFollowup("register-unit:plan-join-fresh", { axis: a, registerPlan: P.registerPlan, bandPath: P.registerBand(a), entries });
10414
10439
  const rf = await stage("register-unit", { ...ctx, axis: a }, { force: true, freshMessage: freshMsg, sessionKey: `clearance-${ctx.run.slug}-${ctx.run.codename}-register-unit-${a}-plan-join-fresh`, trigger: "plan-join-fresh" });
10415
10440
  if (!rf.ok) note(`register-unit ${a}: fresh execute_plan call failed (${rf.fail}) — the plan-unexecuted StageFailure below holds the line`);
10416
10441
  continue;
@@ -10591,10 +10616,13 @@ async function pipelineInner(job, opts = {}) {
10591
10616
  // back-compat: a pre-doc-35 receipt keyed only on the cell set (no source channels)
10592
10617
  (priorReceipt.sig == null && !sourceChannels.length &&
10593
10618
  JSON.stringify((priorReceipt.requested ?? []).map(cellKey).sort()) === JSON.stringify(closable.map(cellKey).sort())));
10619
+ // SIZED BY THE CELLS A CLOSURE GRID RUNS, as the main grid is (gridBatchFor): its spec carries only the
10620
+ // platforms of the cells it closes.
10621
+ const closureBatch = (cells) => gridBatchFor(ctx.profile, new Set(cells.map((c) => c.platform)).size);
10594
10622
  if ((closable.length || sourceChannels.length) && !alreadyAttempted) {
10595
10623
  const requested = closable;
10596
10624
  const variants = [...new Set(closable.map((c) => c.variant))];
10597
- const batchSize = ctx.profile?.batchSize ?? 14; // WS-B: derived from the profile's platform count
10625
+ const batchSize = closureBatch(closable);
10598
10626
  const batches = Math.max(1, Math.ceil(variants.length / batchSize));
10599
10627
  const gridCalls = batches + (sourceChannels.length ? 1 : 0); // grid call(s) + a channel-sweep call (tokens-only: no $ estimate)
10600
10628
  note(`coverage closure: ${closable.length} machine-closable cell(s)${sourceChannels.length ? ` + ${sourceChannels.length} un-swept in-scope channel(s) [${sourceChannels.join(", ")}]` : ""} — one supplementary grid pass (${gridCalls} grid call(s))`);
@@ -10707,7 +10735,7 @@ async function pipelineInner(job, opts = {}) {
10707
10735
  }
10708
10736
  if (closable.length || exempt.length) {
10709
10737
  // Tokens-only (owner directive 2026-07-11): the offer names the work (grid calls), never a $ figure.
10710
- const batchesLeft = Math.max(1, Math.ceil([...new Set(closable.map((c) => c.variant))].length / (ctx.profile?.batchSize ?? 14)));
10738
+ const batchesLeft = Math.max(1, Math.ceil([...new Set(closable.map((c) => c.variant))].length / closureBatch(closable)));
10711
10739
  const offer = ` — closable on instruction (${batchesLeft} supplementary grid call(s))`;
10712
10740
  ctx.coverageNote = `marketplace cells not executed: ${[...closable, ...exempt].map(cellKey).join("; ")}` +
10713
10741
  (closable.length ? ` — attempted in-loop and still unreachable${offer}` : "") +
@@ -11060,7 +11088,7 @@ async function pipelineInner(job, opts = {}) {
11060
11088
  directiveQids.set(key, []);
11061
11089
  // Per-case wording (A2): the variant fallback now also DISCLOSES a label-shaped item
11062
11090
  // (term-shape lint) — dispatching the display label verbatim was the 2026-07-28
11063
- // nil-search class ("Reverse-order TIKI composites (…)" searched as exact → 0 → read
11091
+ // nil-search class ("Reverse-order WAVO composites (…)" searched as exact → 0 → read
11064
11092
  // as clean). The remedy contract (SKILL.md) is the fix the wording points at.
11065
11093
  regDeferReason.set(key, String(d.layer ?? "").toLowerCase() === "field"
11066
11094
  ? "no-code-remedy: a field class-gap with no searchable term×class pair (closing it in the matter's own classes only re-runs the primary sweep — disclosed)"
@@ -11152,8 +11180,8 @@ async function pipelineInner(job, opts = {}) {
11152
11180
  }
11153
11181
  // Dispatch → derive band → VERIFY per directive (qid-landed + non-collapse + class-scope).
11154
11182
  // A byte-changed band with only a wrong-scope/empty/error block closes NOTHING.
11155
- const dispatchAndVerify = async () => {
11156
- const outcome = await dispatchPlanQids(a, added, "frame-reopen-sweep", 2);
11183
+ const dispatchAndVerify = async (qids) => {
11184
+ const outcome = await dispatchPlanQids(a, qids, "frame-reopen-sweep", 2);
11157
11185
  refreshSupplementalExecution(ctx);
11158
11186
  try { deriveNamedBand(ctx); } catch (e) { regMechFail = `named-band:${String(e.message).slice(0, 80)}`; }
11159
11187
  const bands = readRegisterBands(P, axes);
@@ -11166,7 +11194,7 @@ async function pipelineInner(job, opts = {}) {
11166
11194
  executedQids: ev.executedQids, blocksByQid: ev.blocksByQid,
11167
11195
  });
11168
11196
  const mintedDirectives = regDirectives.filter((d) => (directiveQids.get(reopenKey(d)) ?? []).length);
11169
- let ev = await dispatchAndVerify();
11197
+ let ev = await dispatchAndVerify(added);
11170
11198
  regLastJoin = ev;
11171
11199
  let outcome = ev.outcome;
11172
11200
  const retryTargets = [];
@@ -11179,16 +11207,24 @@ async function pipelineInner(job, opts = {}) {
11179
11207
  // correct scope from attempt 1, so this recovers a transient (an error / collapsed / unlanded
11180
11208
  // slice), never re-runs a wrong query. dispatchPlanQids' own repair ledger (max 2) refuses a
11181
11209
  // third — so this cannot loop or thrash the paid pass.
11210
+ //
11211
+ // ONLY THE FAILING DIRECTIVES' OWN ENTRIES GO AGAIN. A directive that verified closed on the first
11212
+ // attempt is not asked twice: re-sending its qids re-fetches records the run already holds, and
11213
+ // on a register that bills per request it pays for them again.
11182
11214
  if (retryTargets.length) {
11183
- note(`frame-reopen: ${retryTargets.length} directive(s) not verified-closed — ONE re-attempt with the same structured remedy`);
11184
- runLog(run.runDir, { event: "frame-reopen-reattempt", directives: retryTargets.map(reopenKey) });
11185
- ev = await dispatchAndVerify();
11186
- regLastJoin = ev;
11187
- outcome = ev.outcome ?? outcome;
11188
- for (const d of retryTargets) {
11189
- const v = verifyOne(d, ev);
11190
- if (v.closed) { regSwept.add(reopenKey(d)); regDeferReason.delete(reopenKey(d)); }
11191
- else regDeferReason.set(reopenKey(d), v.reason);
11215
+ const retryQids = [...new Set(retryTargets.flatMap((d) => directiveQids.get(reopenKey(d)) ?? []))]
11216
+ .filter((q) => added.includes(q));
11217
+ note(`frame-reopen: ${retryTargets.length} directive(s) not verified-closed — ONE re-attempt of their own ${retryQids.length} entr${retryQids.length === 1 ? "y" : "ies"} with the same structured remedy`);
11218
+ runLog(run.runDir, { event: "frame-reopen-reattempt", directives: retryTargets.map(reopenKey), qids: retryQids });
11219
+ if (retryQids.length) {
11220
+ ev = await dispatchAndVerify(retryQids);
11221
+ regLastJoin = ev;
11222
+ outcome = ev.outcome ?? outcome;
11223
+ for (const d of retryTargets) {
11224
+ const v = verifyOne(d, ev);
11225
+ if (v.closed) { regSwept.add(reopenKey(d)); regDeferReason.delete(reopenKey(d)); }
11226
+ else regDeferReason.set(reopenKey(d), v.reason);
11227
+ }
11192
11228
  }
11193
11229
  }
11194
11230
  const afterBand = existsSync(P.registerBand(a)) ? readFileSync(P.registerBand(a), "utf8") : null;
@@ -12557,7 +12593,7 @@ async function pipelineInner(job, opts = {}) {
12557
12593
  jurisdictions: (() => { try { return JSON.parse(readFileSync(P.instructedScope, "utf8"))?.jurisdictions ?? null; } catch { return null; } })(),
12558
12594
  });
12559
12595
  if (caseLaw.declined)
12560
- note(`[case-law] the draft narrative turns on "${caseLaw.trigger}", and a ${caseLaw.product ?? "run with no resolved product"} does not carry the case-law reading — recorded, not run (#519)`);
12596
+ note(`[case-law] the draft narrative turns on "${caseLaw.trigger}", and a ${caseLaw.product ?? "run with no resolved product"} does not carry the case-law reading — recorded, not run`);
12561
12597
  if (needCaseLaw) runLog(run.runDir, { event: "case-law-trigger", requested: caseLaw.requested, detected: caseLaw.detected });
12562
12598
  // T7 (E5): feed the finding index so the case-law stage stamps "- ord:" on each grounded
12563
12599
  // profile — the render + report-card joins become deterministic (copper-spire's prompt-fished join
@@ -13500,7 +13536,7 @@ async function pipelineInner(job, opts = {}) {
13500
13536
  // by construction, which is the intended shape: a BLOCKING with an empty reasons array is
13501
13537
  // structurally impossible, and any future path that reintroduces one dies here instead of shipping.
13502
13538
  if (verdict === "BLOCKING" && !reasonsOut.length)
13503
- throw new Error("verdict BLOCKING with no reasons — a blocking decision without its grounds; verdict.json is the artifact whose whole purpose is carrying them (#1065)");
13539
+ throw new Error("verdict BLOCKING with no reasons — a blocking decision without its grounds; verdict.json is the artifact whose whole purpose is carrying them");
13504
13540
  // doc 50 — a v4 record derives off the run's FROZEN framework (band words); deriveDisplayVerdict
13505
13541
  // fail-louds if banded findings arrive with no manifest (never silently badge a rated matter LOW).
13506
13542
  const derived = deriveDisplayVerdict({ verdict, reasons: reasonsOut, kinds: kindsOut, findings: findingsArr,
@@ -14096,7 +14132,7 @@ async function pipelineInner(job, opts = {}) {
14096
14132
  screenGateUnresolved: readJson(driverDir(run.runDir, "screen-gate-unresolved.json"))?.unresolved ?? null,
14097
14133
  supplementalPlans,
14098
14134
  xcheck: readJson(driverDir(run.runDir, "register-xcheck.json")),
14099
- recall: readJson(driverDir(run.runDir, "register-recall.json")),
14135
+ recall: recallReceiptForOwnCompany(readJson(driverDir(run.runDir, "register-recall.json"))), // never another company's recall line
14100
14136
  planExecution: ctx.planExecution ?? readJson(P.planExecution),
14101
14137
  }, { ts: askTs });
14102
14138
  } catch (e) {
@@ -15476,6 +15512,7 @@ async function pipelineInner(job, opts = {}) {
15476
15512
  // and the run is already settled, so a failure to stamp is logged and the delivery proceeds.
15477
15513
  const stamp = writeSettleStamp(published.poolRunDir, { state: "delivered", signoff: verdict, deliveredAt, runId: published.runId ?? run.runId, lane: "clearance" });
15478
15514
  if (!stamp.written) note(`delivery: settle stamp not written (${stamp.reason})`);
15515
+ endAnswerMemory(run.runDir); // the held answers do not travel into the archive
15479
15516
  const archived = archive(run);
15480
15517
  rollupStatus(run.studioRoot);
15481
15518
  // `notified: "pending"` and `sendPending: true` unconditionally, which is what the KNOCKOUT lane has
@@ -71,7 +71,7 @@ const isSepRow = (t) => /^\|[\s:|-]+\|$/.test(t);
71
71
  * BOTH reach every line of the section, prose included, and that is deliberate. The first cut held
72
72
  * the text path to pipe-TABLE rows only, on the theory that digest.md puts carried candidates in
73
73
  * tables. Run against the real 2026-07-29 findings it manufactured a loss: the watchlist annex there
74
- * is written as `- **W-1 · TIKI Brand outdoor-living portfolio — …**` bullets, so a candidate the
74
+ * is written as `- **W-1 · WAVO Brand outdoor-living portfolio — …**` bullets, so a candidate the
75
75
  * digest had plainly carried came back `uncarried`. A join that invents losses is worth nothing, and
76
76
  * the section shape is the digest's to choose. So the reach is the same as parseFindingsEndings
77
77
  * (recall-reconciliation.mjs), which has always counted a prose mention as an ending — one
@@ -216,7 +216,7 @@ export function adjudicationOf(entry, surfaces) {
216
216
  // Measured on `placement-carry-2026-07-29`, whose delivered digest discharges its one borderline
217
217
  // entry in as many words —
218
218
  //
219
- // | R-8 (my override) — placement-inquiry placed `TIKI LOVERS` at sheet-2 (S2-C) while flagging
219
+ // | R-8 (my override) — placement-inquiry placed `WAVO LOVERS` at sheet-2 (S2-C) while flagging
220
220
  // it as a borderline headline call | OVERRODE — promoted to headline. …
221
221
  //
222
222
  // — and names no URI anywhere in that row. A URI-only branch reports that discharged obligation as
@@ -103,7 +103,7 @@ export const SELECT_ROW_CONTRACT = Object.freeze({
103
103
  unresolved: "A `select` the band does not hold is reported back to you by id, never silently dropped.",
104
104
  });
105
105
 
106
- const PROVENANCE = "driver-written form (#562). You SELECT and you JUDGE; the driver COPIES. For each "
106
+ const PROVENANCE = "driver-written form. You SELECT and you JUDGE; the driver COPIES. For each "
107
107
  + "register candidate you are placing, add a row naming one of its record ids in `select` plus your "
108
108
  + "`tier` and `reason` (and `borderline` if the call is close) — the driver resolves the id against the "
109
109
  + "register's own exact-identity fold and fills mark, owner, records, territories and classes from it, "
@@ -83,7 +83,7 @@ function post(urlStr, { headers = {}, body = "", wantId = null, timeoutMs = 3000
83
83
  });
84
84
  // — a timeout got no status and therefore no answer; it is marked at the throw rather than
85
85
  // recognised later by its message, so nothing has to keep a regex in step with a sentence.
86
- req.setTimeout(timeoutMs, () => { req.destroy(transportError(`MCP request timed out after ${timeoutMs}ms`, null)); });
86
+ req.setTimeout(timeoutMs, () => { req.destroy(Object.assign(transportError(`MCP request timed out after ${timeoutMs}ms`, null), { timedOut: true })); });
87
87
  req.on("error", (e) => reject(isSocketFailure(e) ? transportError(e.message, null) : e));
88
88
  req.end(body);
89
89
  });