clearotron 0.4.1-beta.0 → 0.4.1-beta.2

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 (79) hide show
  1. package/THIRD-PARTY-NOTICES.md +1 -1
  2. package/bin/README.md +1 -1
  3. package/bin/clearotron.mjs +2 -0
  4. package/bin/mcp.mjs +80 -0
  5. package/bin/start.mjs +14 -1
  6. package/build-info.json +2 -2
  7. package/demo/README.md +1 -1
  8. package/docs/architecture/04-configuration-reference.md +0 -1
  9. package/docs/architecture/06-operations-runbook.md +4 -3
  10. package/docs/architecture/08-development-guide.md +3 -1
  11. package/driver/CHANGELOG.md +17 -0
  12. package/driver/citation-census.json +3 -3
  13. package/driver/compare.mjs +1 -1
  14. package/driver/contract-audit.mjs +6 -6
  15. package/driver/contract-dictation-registry.mjs +1 -1
  16. package/driver/contract-e3-backlog.mjs +17 -16
  17. package/driver/contract-vocabulary.mjs +138 -125
  18. package/driver/declination-call.mjs +5 -1
  19. package/driver/dev-portal.mjs +2 -1
  20. package/driver/engine/mcp/codex-config.mjs +7 -2
  21. package/driver/engine/mcp/gather-config.mjs +4 -0
  22. package/driver/engine/mcp/recording-server.mjs +1 -1
  23. package/driver/engine/mcp/supplemental.mjs +26 -1
  24. package/driver/engine/openai-agent.mjs +13 -3
  25. package/driver/findings-model.mjs +80 -13
  26. package/driver/flag-snapshot.mjs +2 -1
  27. package/driver/foreign-record-links.mjs +50 -0
  28. package/driver/frame-diff-model.mjs +1 -1
  29. package/driver/gateway.mjs +4 -4
  30. package/driver/jx.mjs +1 -1
  31. package/driver/knockout-review-record.mjs +55 -1
  32. package/driver/named-band.mjs +2 -0
  33. package/driver/owner-screen.mjs +7 -4
  34. package/driver/package.json +1 -1
  35. package/driver/pipeline-knockout.mjs +14 -12
  36. package/driver/pipeline.mjs +118 -20
  37. package/driver/portal-service.mjs +5 -4
  38. package/driver/products.mjs +1 -1
  39. package/driver/profiles.mjs +4 -3
  40. package/driver/publish/publish-inputs.mjs +2 -2
  41. package/driver/publish/xlsx.mjs +1 -1
  42. package/driver/register-count.mjs +2 -2
  43. package/driver/register-coverage.mjs +1 -1
  44. package/driver/register-plan.mjs +13 -2
  45. package/driver/result-noun-fields.mjs +3 -3
  46. package/driver/scope-rules.mjs +2 -2
  47. package/driver/stage-context.mjs +9 -0
  48. package/driver/stages-knockout.mjs +6 -5
  49. package/driver/stages.mjs +23 -23
  50. package/driver/suite-census.json +97 -31
  51. package/driver/synthesis-record.mjs +14 -5
  52. package/driver/verify.mjs +1 -1
  53. package/mcp-server/CHANGELOG.md +8 -0
  54. package/mcp-server/CONNECT.md +9 -3
  55. package/mcp-server/demo-mode.mjs +10 -0
  56. package/mcp-server/lib/runs.mjs +7 -1
  57. package/mcp-server/lib/scrub.mjs +1 -1
  58. package/mcp-server/package.json +1 -1
  59. package/mcp-server/packs/README.md +1 -1
  60. package/mcp-server/server.mjs +16 -2
  61. package/package.json +3 -2
  62. package/portal-ui/package.json +3 -3
  63. package/providers/_shared/count.mjs +5 -1
  64. package/providers/_shared/enumerate.mjs +66 -21
  65. package/providers/_shared/execute-plan.mjs +13 -4
  66. package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
  67. package/providers/oauth-mcp-bridge/package.json +1 -1
  68. package/providers/signa/src/capabilities.js +15 -0
  69. package/providers/signa/src/core.js +55 -26
  70. package/scripts/e2e-first-time.mjs +3 -3
  71. package/scripts/env-audit.mjs +3 -2
  72. package/scripts/freeze-example-run.mjs +8 -8
  73. package/scripts/mcp-registry-listing.mjs +199 -0
  74. package/scripts/mint-plugin-folder.mjs +65 -0
  75. package/scripts/release-version.mjs +44 -0
  76. package/scripts/score.mjs +1 -1
  77. package/shared/register-floor.mjs +19 -0
  78. package/shared/trigger-lane.mjs +1 -1
  79. package/skills/clearotron-ops/SKILL.md +81 -24
@@ -22,7 +22,7 @@
22
22
  //
23
23
  // ── THE RUN IS NOT THE SEAT'S TO NAME ───────────────────────────────────────────────────────────────
24
24
  //
25
- // `CLEAROTRON_BAND_RUN_DIR`, read at CALL TIME. `serverEnv()` (gather-config.mjs:162) sets it for every local
25
+ // `CLEAROTRON_BAND_RUN_DIR`, read at CALL TIME. serverEnv() in gather-config.mjs sets it for every local
26
26
  // server, not only band. There is no `run_dir` parameter and there must not be one — the lesson is
27
27
  // the earlier one, where an invented `CLEAROTRON_RUN_DIR` fallback sat two lines under a sentence promising the tool
28
28
  // never guesses a run. Call time rather than module load: stdio-server.mjs:29-33 states why for this
@@ -57,6 +57,31 @@ const compactProposal = (p) => ({
57
57
  const inPriority = (p, prio) =>
58
58
  (Array.isArray(p?.nice_classes) ? p.nice_classes : []).some((c) => prio.has(String(c).trim()));
59
59
 
60
+ // A SOUND-ALIKE BATCH GOES AS LISTS where the register takes one (`phoneticOrWidth`, ruled 2026-10-09): up to
61
+ // that many spellings per question, within its word budget, each long enough for the ranked search. A
62
+ // spelling below that floor is its own question, as every name of a batch was before. Each list is one
63
+ // question with one receipt row, so a list that crowds or fails takes no other list down with it. PURE.
64
+ function splitBatch(p, names, capabilities) {
65
+ const { terms: _batch, ...rest } = p;
66
+ const single = (t) => ({ ...rest, term: t });
67
+ const width = Math.floor(Number(capabilities?.phoneticOrWidth));
68
+ if (String(p?.predicate ?? "") !== "phonetic" || !(width >= 2)) return names.map(single);
69
+ const floor = Number(capabilities?.rankedMinLength) || 0;
70
+ const budget = Number(capabilities?.listWordBudget) || Infinity;
71
+ const words = (t) => t.split(/\s+/).filter(Boolean).length;
72
+ const folded = (t) => [...t.normalize("NFKD").replace(/\p{M}/gu, "").toLowerCase().replace(/\s+/g, "")].length;
73
+ const out = [];
74
+ let list = [];
75
+ const close = () => { if (list.length) out.push(list.length === 1 ? single(list[0]) : { ...rest, terms: list }); list = []; };
76
+ for (const t of names) {
77
+ if (folded(t) < floor) { out.push(single(t)); continue; }
78
+ if (list.length >= width || list.reduce((n, x) => n + words(x), 0) + words(t) > budget) close();
79
+ list.push(t);
80
+ }
81
+ close();
82
+ return out;
83
+ }
84
+
60
85
  export function mintSupplementalEntries(axis, proposals, { existingQids = new Set(), perCall = Infinity, axisMax = Infinity, existingCount = 0, capabilities = null, priorityClasses = [] } = {}) {
61
86
  const minted = [], reused = [], rejected = [], enriched = [], narrowed = [];
62
87
  let budget = Math.max(0, axisMax - existingCount);
@@ -82,7 +107,7 @@ export function mintSupplementalEntries(axis, proposals, { existingQids = new Se
82
107
  const names = perName && Array.isArray(p?.terms) ? p.terms.map((t) => String(t ?? "").trim()).filter(Boolean) : [];
83
108
  const splitOf = names.length > 1 && names.length <= PLAN_MAX_OR_WIDTH
84
109
  ? fingerprint({ axis, predicate: String(p?.predicate ?? "default"), names: [...names].sort(), i }) : null;
85
- const byName = splitOf ? names.map((t) => { const { terms: _batch, ...rest } = p; return { ...rest, term: t }; }) : [p ?? {}];
110
+ const byName = splitOf ? splitBatch(p, names, capabilities) : [p ?? {}];
86
111
  return byName.flatMap((q) => {
87
112
  const words = perWord && Array.isArray(q?.goods_words) ? q.goods_words.map((w) => String(w ?? "").trim()).filter(Boolean) : [];
88
113
  return words.length > 1
@@ -426,6 +426,16 @@ export function codexToolCallsByName(ev) {
426
426
  return { ...byName };
427
427
  }
428
428
 
429
+ /**
430
+ * The commands codex ran this turn: its `command_execution` items, counted once per item id, a failed one
431
+ * included, since a command that exited non-zero still ran. A NUMBER is a measurement (0: none ran). PURE.
432
+ */
433
+ export function codexCommandCalls(ev) {
434
+ let n = 0;
435
+ for (const name of ev?.toolItems?.values() ?? []) if (name === "command_execution") n += 1;
436
+ return n;
437
+ }
438
+
429
439
  /**
430
440
  * The tool-server calls of this turn that came back as an error, by tool name, in the shape
431
441
  * anthropic-agent records: a call that reached its server and `failed` there, which codex writes with no
@@ -653,10 +663,10 @@ function settleTuple({ r, ev, resumeRef }) {
653
663
  // still cannot report a whole-turn tool count, so `toolCalls` stays null, which is the house rule
654
664
  // for "this engine does not report" rather than "it called nothing".
655
665
  ...mcpToolGauge(ev),
656
- // The refused count under the name the attempt row reads on both engines. Codex keeps its shell, and this
657
- // adapter reads its commands only for failures (below), so it reports no command-tool count: null, never zero.
666
+ // The refused count under the name the attempt row reads on both engines. Codex keeps its shell, and every
667
+ // command it runs is a `command_execution` item: counted once per item id, failed ones included.
658
668
  toolCallsRefused: mcpToolGauge(ev).mcpToolCallsRefused,
659
- commandToolCalls: null,
669
+ commandToolCalls: codexCommandCalls(ev),
660
670
  // The per-name split of the calls codex reports (see codexToolCallsByName), what the session went
661
671
  // through (session-record.mjs: every turn ending and stream error, in order), and the raw stream.
662
672
  toolCallsByName: codexToolCallsByName(ev), toolCallsErroredByName: codexToolServerErrors(ev),
@@ -858,7 +858,7 @@ export function parseFindingsJson(raw, opts = {}) {
858
858
  method: "method" in opts ? (opts.method ?? null) : undefined,
859
859
  // — null means the caller named no provider, and the record-URL gate stays inactive. An empty
860
860
  // ARRAY is a real answer ("this provider publishes no per-record page") and is not the same thing.
861
- recordOrigins: Array.isArray(opts.recordOrigins) ? opts.recordOrigins : null };
861
+ recordOrigins: Array.isArray(opts.recordOrigins) ? opts.recordOrigins : null, foreignLinks: Array.isArray(opts.recordOrigins) ? [] : null };
862
862
  onlyKeys(doc, topKeysFor(mode), (k) => `findings_key_unknown:${short(k)}`);
863
863
  if (mode.v4) {
864
864
  if (typeof doc.rated_under_framework !== "string" || !doc.rated_under_framework.trim())
@@ -879,7 +879,7 @@ export function parseFindingsJson(raw, opts = {}) {
879
879
  throw new Error("findings_empty: the record carries no findings AND no coverage areas — at least one is required");
880
880
 
881
881
  const seenOrdinals = new Set();
882
- const outFindings = findings.map((f, idx) => validateFinding(f, idx, seenOrdinals, mode));
882
+ const outFindings = findings.map((f, idx) => validateFinding(f, idx, seenOrdinals, mode)); throwForeignLinks(mode);
883
883
  const outCoverage = coverage.map((c) => validateCoverage(c));
884
884
  const outContextNotes = contextNotes.map((n) => validateContextNote(n));
885
885
  const outActions = validateActionsList(doc.actions, outFindings);
@@ -1381,10 +1381,10 @@ function validateFinding(f, idx, seenOrdinals, mode = { v4: false, manifest: nul
1381
1381
  if (f.withdrawn_reason != null && f.disposition !== "withdrawn")
1382
1382
  throw new Error(`finding_withdrawn_reason_orphan:${ord} (withdrawn_reason is only valid when disposition is "withdrawn")`);
1383
1383
 
1384
- validateOwner(f.owner, ord, mode?.recordOrigins ?? null);
1384
+ validateOwner(f.owner, ord, mode?.recordOrigins ?? null, mode?.foreignLinks ?? null);
1385
1385
  validateMeters(f.meters);
1386
1386
  validateQuadrant(f.quadrant);
1387
- validateSource(f.source, ord, mode?.recordOrigins ?? null);
1387
+ validateSource(f.source, ord, mode?.recordOrigins ?? null, mode?.foreignLinks ?? null);
1388
1388
 
1389
1389
  // Instance #5 — per-finding use-check / own-rights cite. SHAPE-ONLY here (present ⇒ a { source } object
1390
1390
  // whose source is a string). The composite>=3 NON-EMPTY-source enforcement lives in verify.mjs
@@ -1969,13 +1969,13 @@ export function isDeadRecordLink(value) {
1969
1969
  return t === "" || t.startsWith("#") || t.startsWith("/");
1970
1970
  }
1971
1971
 
1972
- function checkRecordUrlHost(value, origins, ord, what) {
1972
+ function checkRecordUrlHost(value, origins, ord, what, collect = null) {
1973
1973
  if (origins == null) return; // gate inactive — see the header
1974
1974
  const origin = recordUrlOrigin(value);
1975
- if (origin == null) return;
1976
- if (origins.includes(origin)) return;
1975
+ if (origin == null || origins.includes(origin)) return;
1976
+ if (collect) { collect.push({ ord, what, value }); return; } // the strict parse names EVERY offender in one refusal
1977
1977
  throw new Error(
1978
- `finding_record_url_foreign_host:${ord} (${what} points at ${origin}, which is not a register this run searched — `
1978
+ `finding_record_url_foreign_host:${Array.isArray(ord) ? ord.join(",") : ord} (${what} points at ${origin}, which is not a register this run searched — `
1979
1979
  + (origins.length
1980
1980
  ? `this provider's record host is ${origins.join(" or ")}. Compose the URL from the record's own uri and THIS provider's base host, per providers/<name>.md.`
1981
1981
  : "this provider publishes no per-record page at all — cite the office register in the text and leave the URL as the record's own uri path.")
@@ -1983,7 +1983,7 @@ function checkRecordUrlHost(value, origins, ord, what) {
1983
1983
  );
1984
1984
  }
1985
1985
 
1986
- function validateOwner(owner, ord, origins = null) {
1986
+ function validateOwner(owner, ord, origins = null, collect = null) {
1987
1987
  if (!isPlainObject(owner)) throw new Error(`finding_owner_invalid:${ord} (owner must be an object { name, country, registrations })`);
1988
1988
  onlyKeys(owner, OWNER_KEYS, (k) => `finding_owner_key_unknown:${short(k)}`);
1989
1989
  if (typeof owner.name !== "string" || !owner.name.trim()) throw new Error(`finding_owner_invalid:${ord} (owner.name must be a non-empty string)`);
@@ -1999,7 +1999,7 @@ function validateOwner(owner, ord, origins = null) {
1999
1999
  if (!isPlainObject(r)) throw new Error(`finding_registration_invalid:${ord} (each registration must be a plain object)`);
2000
2000
  onlyKeys(r, REGISTRATION_KEYS, (k) => `finding_registration_key_unknown:${short(k)}`);
2001
2001
  if (typeof r.uri !== "string" || !r.uri.trim()) throw new Error(`finding_registration_invalid:${short(r.uri)} (registration.uri must be a non-empty string)`);
2002
- checkRecordUrlHost(r.uri, origins, ord, `registration.uri "${short(r.uri)}"`);
2002
+ checkRecordUrlHost(r.uri, origins, ord, `registration.uri "${short(r.uri)}"`, collect);
2003
2003
  if (r.classes != null && !(Array.isArray(r.classes) && r.classes.every((c) => typeof c === "string")))
2004
2004
  throw new Error(`finding_registration_invalid:${short(r.uri)} (registration.classes must be an array of strings)`);
2005
2005
  }
@@ -2042,7 +2042,7 @@ function validateQuadrant(q) {
2042
2042
  }
2043
2043
  }
2044
2044
 
2045
- function validateSource(source, ord, origins = null) {
2045
+ function validateSource(source, ord, origins = null, collect = null) {
2046
2046
  if (!isPlainObject(source)) throw new Error(`finding_source_invalid:${ord} (source must be { source_type, resolved_link })`);
2047
2047
  onlyKeys(source, SOURCE_KEYS, (k) => `finding_source_key_unknown:${short(k)}`);
2048
2048
  if (!SOURCE_TYPES.includes(source.source_type))
@@ -2061,7 +2061,7 @@ function validateSource(source, ord, origins = null) {
2061
2061
  + "dead anchor in the report. Give the record's real URL, or — if this provider publishes no "
2062
2062
  + 'per-record page — leave it "" and cite the office register in the text.)',
2063
2063
  );
2064
- checkRecordUrlHost(source.resolved_link, origins, ord, "source.resolved_link");
2064
+ checkRecordUrlHost(source.resolved_link, origins, ord, "source.resolved_link", collect);
2065
2065
  }
2066
2066
  }
2067
2067
 
@@ -2179,7 +2179,7 @@ export function projectCoverageJudgment(cj) {
2179
2179
  const rows = Array.isArray(cj.rows) ? cj.rows : [];
2180
2180
  if (!rows.length) return cj;
2181
2181
  // — the READER's name leads, and falls back to the identifier when the driver had nothing to
2182
- // add. This string is printed on report.html ("Coverage read"), which is why it reads the label.
2182
+ // add. renderHtml receives this string and leaves it off report.html, so no page prints it; the label leads anyway.
2183
2183
  const shown = rows.slice(0, COVERAGE_JUDGMENT_ROW_CAP);
2184
2184
  const hidden = rows.length - shown.length;
2185
2185
  const rowText = shown.map((r) => `${trimDot(r.areaLabel || r.area)}: ${trimDot(r.note)}`).join("; ");
@@ -2571,3 +2571,70 @@ export function knockoutFindingRange(mark) {
2571
2571
  if (!ords.length || !markName) return "";
2572
2572
  return ords.length === 1 ? `${markName} #${ords[0]}` : `${markName} #${ords[0]}–#${ords[ords.length - 1]}`;
2573
2573
  }
2574
+
2575
+ /**
2576
+ * ONE REFUSAL FOR EVERY FOREIGN RECORD LINK the strict parse collected.
2577
+ *
2578
+ * The gate used to throw at the first offending link, so each refusal named one finding and a corrective
2579
+ * attempt fixed one. A seat that had copied an office's page into eleven findings ran out of attempts at
2580
+ * the third. The token now carries every offending ordinal; the sentence after it is the gate's own,
2581
+ * unchanged, about the first offender.
2582
+ */
2583
+ function throwForeignLinks(mode) {
2584
+ const found = mode?.foreignLinks ?? [];
2585
+ if (!found.length) return;
2586
+ const ords = [...new Set(found.map((f) => f.ord))];
2587
+ checkRecordUrlHost(found[0].value, mode.recordOrigins, ords.length === 1 ? ords[0] : ords, found[0].what);
2588
+ }
2589
+
2590
+ /** The canonical record path inside an absolute URL (`/mark/<cc>/<rest>`), or null. PURE. */
2591
+ function canonicalRecordPath(value) {
2592
+ try {
2593
+ const path = new URL(String(value)).pathname;
2594
+ return /^\/mark\/[a-z]{2}\/[^/]+/i.test(path) ? path : null;
2595
+ } catch { return null; }
2596
+ }
2597
+
2598
+ /**
2599
+ * THE SALVAGE FOR A FOREIGN RECORD LINK: blank it, and deliver. PURE.
2600
+ *
2601
+ * A record link on a host the run's register does not publish is refused by the gate above. When a seat
2602
+ * keeps writing one, the report still goes out: a register-sourced `resolved_link` on a foreign host
2603
+ * becomes `""`, the value the findings model already reads as "this register publishes no page for the
2604
+ * record", and publish still links a register that publishes no pages to the office's own page from the
2605
+ * record itself. A `registration.uri` written as a foreign URL keeps the record's canonical path when
2606
+ * the URL carries one, since the path is the record's identity. One that does not is left as written and
2607
+ * returned in `kept`, because no identity can be read from it.
2608
+ *
2609
+ * `origins` is the gate's own list (recordOriginsFor); null turns this off, as it turns the gate off.
2610
+ * Returns the repaired document, every value it changed (`blanked`, with what it was), and `kept`.
2611
+ */
2612
+ export function blankForeignRecordLinks(doc, origins) {
2613
+ const blanked = [], kept = [];
2614
+ if (origins == null || !isPlainObject(doc) || !Array.isArray(doc.findings)) return { doc, blanked, kept };
2615
+ const foreign = (value) => { const o = recordUrlOrigin(value); return o != null && !origins.includes(o); };
2616
+ const findings = doc.findings.map((f) => {
2617
+ if (!isPlainObject(f)) return f;
2618
+ let out = f;
2619
+ const s = f.source;
2620
+ if (isPlainObject(s) && String(s.source_type ?? "").startsWith("register") && typeof s.resolved_link === "string" && foreign(s.resolved_link)) {
2621
+ out = { ...out, source: { ...s, resolved_link: "" } };
2622
+ blanked.push({ ordinal: f.ordinal ?? null, field: "source.resolved_link", was: s.resolved_link, now: "" });
2623
+ }
2624
+ const regs = isPlainObject(f.owner) && Array.isArray(f.owner.registrations) ? f.owner.registrations : null;
2625
+ if (regs) {
2626
+ let changed = false;
2627
+ const next = regs.map((r, index) => {
2628
+ if (!isPlainObject(r) || typeof r.uri !== "string" || !foreign(r.uri)) return r;
2629
+ const path = canonicalRecordPath(r.uri);
2630
+ if (!path) { kept.push({ ordinal: f.ordinal ?? null, field: "registration.uri", index, was: r.uri }); return r; }
2631
+ changed = true;
2632
+ blanked.push({ ordinal: f.ordinal ?? null, field: "registration.uri", index, was: r.uri, now: path });
2633
+ return { ...r, uri: path };
2634
+ });
2635
+ if (changed) out = { ...out, owner: { ...out.owner, registrations: next } };
2636
+ }
2637
+ return out;
2638
+ });
2639
+ return { doc: { ...doc, findings }, blanked, kept };
2640
+ }
@@ -425,7 +425,8 @@ export function registerCanCountFor(snapshot) {
425
425
  *
426
426
  * The trap is `covered ?? []`, which turns both "unrestricted" and "unknown" into "nothing" and offers
427
427
  * a client zero territories on a production deployment. It is written down at
428
- * providers/corsearch/src/capabilities.js:66-68 as well: "Never read as 'covers nothing'".
428
+ * the `covered` entry of CAPABILITIES in providers/corsearch/src/capabilities.js as well: "Never read as
429
+ * 'covers nothing'".
429
430
  */
430
431
  export function registerTerritoriesFor(snapshot) {
431
432
  if (!snapshot?.register || !("territories" in snapshot.register)) return undefined;
@@ -0,0 +1,50 @@
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
+ // foreign-record-links.mjs — a record link on a host the run's register does not publish: refused once, then
5
+ // blanked, and the run delivers.
6
+ //
7
+ // The findings gate refuses such a link (`finding_record_url_foreign_host`, findings-model.mjs). R2 on
8
+ // 0.4.1-beta.1 delivered no report over eleven of them: the seat copied each office's own page into a
9
+ // record link, every attempt was refused, and nothing could repair it. Two layers now act on it, and both
10
+ // record what they did under one event name and in one file, so a reader meets one fact however it arose:
11
+ // · the synthesis transport, at the call (synthesis-record.mjs): the first offending call in a run is
12
+ // refused, naming every finding, so the seat can fix them in the same turn; a repeat is accepted with
13
+ // the foreign links blanked;
14
+ // · the synthesis salvage lane (pipeline.mjs), for a findings file the transport did not write.
15
+ // Blanking is blankForeignRecordLinks (findings-model.mjs): a register link becomes "", the value that
16
+ // already means "this register publishes no page for the record".
17
+ import { writeFileSync } from "node:fs";
18
+ import { driverDir } from "../shared/driver-dir.mjs";
19
+ import { blankForeignRecordLinks } from "./findings-model.mjs";
20
+ import { activeRecordOrigins } from "./record-origins.mjs";
21
+ import { runLog } from "./log.mjs";
22
+
23
+ export const FOREIGN_HOST_TOKEN = "finding_record_url_foreign_host";
24
+ export const BLANKED_FILE = "foreign-record-links.json";
25
+
26
+ /** The gate's list of record hosts: the caller's when it names one, else the run's register. */
27
+ export const recordOriginsOr = (given) => (given === undefined ? activeRecordOrigins() : given);
28
+
29
+ /**
30
+ * The synthesis call, refused once and then blanked. PURE. `refusedBefore` is whether this run's transport
31
+ * already refused a call for a foreign record link; until it has, the call is returned unchanged and the
32
+ * gate refuses it.
33
+ */
34
+ export function blankOnRepeat(call, origins, refusedBefore) {
35
+ if (!refusedBefore || !Array.isArray(origins) || !call?.findings) return { call, blanked: [], kept: [] };
36
+ const { doc, blanked, kept } = blankForeignRecordLinks(call.findings, origins);
37
+ return { call: blanked.length ? { ...call, findings: doc } : call, blanked, kept };
38
+ }
39
+
40
+ /** What a blanking changed: the originals kept beside the run, and one run-log row. Never throws. */
41
+ export function recordBlanking(runDir, { stage, layer, blanked, kept, fail = null }) {
42
+ try {
43
+ writeFileSync(driverDir(runDir, BLANKED_FILE),
44
+ JSON.stringify({ stage, layer, ...(fail ? { fail: String(fail).slice(0, 300) } : {}), blanked, kept }, null, 2) + "\n");
45
+ } catch { /* the file is forensics; the run-log row below is the record */ }
46
+ try {
47
+ runLog(runDir, { event: "foreign-record-links-blanked", stage, layer, blanked: blanked.length, kept: kept.length,
48
+ ordinals: [...new Set(blanked.map((b) => b.ordinal))] });
49
+ } catch { /* a log that cannot be written must not stop the record it describes */ }
50
+ }
@@ -143,7 +143,7 @@ export function undispatchableThrow(offenders) {
143
143
  * Every FIRING directive in a frame-diff document that cannot be dispatched — read LENIENTLY, from
144
144
  * raw text or an already-parsed object. This is the enumeration the gateway hint and the pipeline's
145
145
  * exhausted-ladder disclosure both run against the artifact ON DISK, because the fail string they
146
- * would otherwise scrape is cut at 160 characters (verify.mjs:381 commonLawMeaningSeat) and cannot carry more than the
146
+ * would otherwise scrape is cut at 160 characters (checkJson() in verify.mjs) and cannot carry more than the
147
147
  * first offender. Lenient on purpose: the artifact reaching these two callers has ALREADY failed
148
148
  * validation, so it may also carry an unknown key or a bad gap flag — refusing to enumerate then
149
149
  * would lose exactly the disclosures this exists to keep. Returns [{item, layer, severity, why}].
@@ -445,7 +445,7 @@ export function attemptOk(fail, turn) {
445
445
  export function streamMeta(runDir, stream) {
446
446
  if (!stream || typeof stream !== "object" || !stream.file) return null;
447
447
  const dir = String(runDir ?? "");
448
- const file = dir && String(stream.file).startsWith(dir) ? String(stream.file).slice(dir.length).replace(/^[\\/]+/, "") : String(stream.file);
448
+ const file = dir && String(stream.file).startsWith(dir) ? String(stream.file).slice(dir.length).replace(/^[\\/]+/, "").replace(/\\/g, "/") : String(stream.file); // forward slashes, as every record path is, on Windows too
449
449
  return { file, present: stream.present === true, bytes: Number.isFinite(stream.bytes) ? stream.bytes : null,
450
450
  ...(stream.reason ? { reason: stream.reason } : {}) };
451
451
  }
@@ -3192,8 +3192,8 @@ export function isFormClassFail(fail) {
3192
3192
  return FORM_CLASS_RE.test(fail ?? "");
3193
3193
  }
3194
3194
  // How many in-dispatch repairs one stage run may buy. TWO, and the number has a cause: parseFrameDiff
3195
- // is FAIL-FAST — it throws on the first bad severity (frame-diff-model.mjs:66 parseFrameDiff) before it ever collects
3196
- // the undispatchable directives (:100) — so ONE artifact carrying both defects surfaces them
3195
+ // is FAIL-FAST — it throws on the first bad severity (parseFrameDiff() in frame-diff-model.mjs) before it ever collects
3196
+ // the undispatchable directives — so ONE artifact carrying both defects surfaces them
3197
3197
  // SEQUENTIALLY. That is exactly what the 08-02 frame-diff ladder did (severity on a1, undispatchable
3198
3198
  // on a2), and a cap of 1 would have handed the second one straight back to the ladder. The codebase
3199
3199
  // already learned this lesson once at directive granularity: undispatchableThrow batches ALL offenders
@@ -3682,7 +3682,7 @@ export function warmPatchMessage(lastFail, expectFile, { supplementalLane = fals
3682
3682
  * THE COUNTS A TEST ROUND READS PER STAGE, on both engines, spread into `toolGauge`. `toolCallsRefused`:
3683
3683
  * calls the program refused (Claude's own denials; Codex's tool-server calls refused before reaching their
3684
3684
  * server). `commandToolCalls`: calls to a tool that runs a command, which no Claude stage is offered, so
3685
- * zero is the expected reading; Codex keeps its shell and reports null. `mcpToolCalls`: the tool-server
3685
+ * zero is the expected reading there; Codex keeps its shell and counts the commands it ran. `mcpToolCalls`: the tool-server
3686
3686
  * calls Codex completed, its only count of calls made, since its `toolCalls` stays null.
3687
3687
  * `toolCallsErroredByName`: the tool-server calls that came back as an error, by tool name, so a stage that
3688
3688
  * had a call refused and made it again says so where its row otherwise reads ok on one attempt. Null, as in
package/driver/jx.mjs CHANGED
@@ -59,7 +59,7 @@ export const jxFoldLaneRecord = ({ cause = null, ...rest } = {}) => ({
59
59
  // silent in the fold. One record spoke and the other could not.
60
60
  //
61
61
  // TWO FIELDS, BECAUSE ONE CANNOT CARRY BOTH JOBS.
62
- // `executes` is a SCALAR STRING, because reference-score.mjs:1103 routes it into the same slot as the
62
+ // `executes` is a SCALAR STRING, because readJxLanes() in reference-score.mjs routes it into the same slot as the
63
63
  // per-lane `executes` string and scripts/score.mjs interpolates it bare — an object prints
64
64
  // [object Object] there. It names the slices that RAN, "+"-joined, and is NEVER "" (the e2e `exists`
65
65
  // op passes on an empty string, so an all-failed run would have gone green on a statement of nothing).
@@ -272,7 +272,7 @@ export function reviewEvidence(merged, about = {}) {
272
272
  if (at?.engineOwned || !Object.hasOwn(ADDRESSABLE, at?.field)) continue;
273
273
  const flags = plainRegisterFlags(text, about);
274
274
  if (!flags.length) continue;
275
- rows.push({ at, where, says: flags.map((f) => f.say) });
275
+ rows.push({ at: offeredAddress(at), where, says: flags.map((f) => f.say) });
276
276
  }
277
277
  const marks = (about.marks ?? []).filter(Boolean), owners = (about.owners ?? []).filter(Boolean);
278
278
  const ownerNote = owners.length ? ` The ${owners.length} owner name(s) from the record were also removed.` : "";
@@ -287,6 +287,60 @@ export function reviewEvidenceLines({ rows }) {
287
287
  return rows.map(({ at, where, says }) => `- ${where} — address ${JSON.stringify(at)} — ${says.join(" ")}`);
288
288
  }
289
289
 
290
+ /**
291
+ * The lines a reader meets first, each one the pass may write, with the address that names it.
292
+ *
293
+ * Handed to the reviewer on a record the driver's read flagged nowhere (owner ruling 2026-10-10). Its
294
+ * first question is the client's, and it can rewrite toward that answer, or decline, only at an address
295
+ * it was given: the tool refuses a composed one. The set is the one the flagged table is drawn from,
296
+ * without the flag, so the engine's own caveats and any field the applier cannot write stay out here too.
297
+ */
298
+ export function reviewFirstReadRows(merged) {
299
+ return knockoutVisibleProse(merged)
300
+ .filter(({ at }) => !at?.engineOwned && Object.hasOwn(ADDRESSABLE, at?.field))
301
+ .map(({ at, where }) => ({ at: offeredAddress(at), where }));
302
+ }
303
+
304
+ /**
305
+ * An address as the seat is told to copy it: the field and the keys that field takes, nothing else.
306
+ *
307
+ * The walk marks a standing caveat with `engineOwned`, true or false, to decide what is offered at all.
308
+ * The grammar refuses any key a field does not take, so an offered address that carried the mark would
309
+ * be refused when copied verbatim, as the tool tells the seat to copy it.
310
+ */
311
+ export function offeredAddress(at) {
312
+ const out = { field: at.field };
313
+ for (const k of ADDRESSABLE[at.field] ?? []) if (at[k] !== undefined) out[k] = at[k];
314
+ return out;
315
+ }
316
+
317
+ /** One line each, the address quoted for the seat to copy back. */
318
+ export function reviewFirstReadLines(rows) {
319
+ return rows.map(({ at, where }) => `- ${where} — address ${JSON.stringify(at)}`);
320
+ }
321
+
322
+ /**
323
+ * What the reviewing pass hands the reviewer, decided before any turn is spent. PURE.
324
+ *
325
+ * EVERY KNOCKOUT IS REVIEWED. Flagged lines go as the measured table. A record flagged nowhere hands
326
+ * over its first-read lines instead, so the client's question is put on every record. Only a record with
327
+ * no such line at all is not dispatched, because there would be nothing to read and no address to answer.
328
+ */
329
+ export function reviewDispatch(merged, plan) {
330
+ const about = reviewAbout(merged, plan);
331
+ const evidence = reviewEvidence(merged, about);
332
+ const firstRead = evidence.rows.length ? [] : reviewFirstReadRows(merged);
333
+ return {
334
+ about, evidence, flagged: evidence.rows.length, listed: firstRead.length,
335
+ dispatch: evidence.rows.length > 0 || firstRead.length > 0,
336
+ msgCtx: {
337
+ evidenceLines: reviewEvidenceLines(evidence),
338
+ firstReadLines: reviewFirstReadLines(firstRead),
339
+ exclusionNote: evidence.exclusionNote,
340
+ },
341
+ };
342
+ }
343
+
290
344
  /**
291
345
  * The `about` a knockout run is blind to: every name in the batch, and every owner on the record.
292
346
  *
@@ -131,6 +131,8 @@ export function parseNamedBand(raw) {
131
131
  ...(Array.isArray(b.covered_by) && b.covered_by.length ? { covered_by: b.covered_by } : {}),
132
132
  // what the total counts, when the register said — carried like the counts it qualifies
133
133
  ...(typeof b.total_counts === "string" ? { total_counts: b.total_counts } : {}),
134
+ // the register's floor when it answered "at least N": the total above stays null beside it
135
+ ...(Number.isFinite(b.total_floor) ? { total_floor: b.total_floor } : {}),
134
136
  // — THE REFUSAL STAMP SURVIVES THE PROJECTION. `execute-plan.mjs` writes an errored slice
135
137
  // as `{state:"incomplete", total_hits:0, fetched:0, error:true}` and says why in its own header:
136
138
  // "the error:true stamp (a provider error is never confusable with a sanctioned crowd)". The
@@ -31,7 +31,7 @@
31
31
  //
32
32
  // PURE (no node imports) so it tests offline, like coverage-ledger.mjs / named-band.mjs.
33
33
 
34
- import { recordQids } from "./named-band.mjs";
34
+ import { recordQids } from "./named-band.mjs"; import { moreThan } from "../shared/register-floor.mjs";
35
35
 
36
36
  /** Owner-slice states. `enumerated` is the only one that can carry a negative. */
37
37
  export const OWNER_SLICE_STATES = ["enumerated", "crowd", "not-run", "missing"];
@@ -95,7 +95,9 @@ export function deriveOwnerScreen(planEntries, blocks, bandRecords, {
95
95
  terms: Array.isArray(e.terms) ? e.terms.map(str) : (e.term != null ? [str(e.term)] : []),
96
96
  nice_classes: (e.nice_classes ?? []).map(str),
97
97
  state,
98
- total_hits: Number(block?.total_hits) || 0,
98
+ // a register's floor ("at least N") is no count: the total stays null and the floor rides beside it
99
+ total_hits: Number.isFinite(block?.total_floor) ? null : Number(block?.total_hits) || 0,
100
+ ...(Number.isFinite(block?.total_floor) ? { total_floor: block.total_floor } : {}),
99
101
  records: recs.length,
100
102
  record_ids: recs.slice(0, recordCap).map((r) => str(r.record_id)).filter(Boolean),
101
103
  reason: sliceReason(e, block, deferred.get(qid) ?? null, capabilities),
@@ -103,7 +105,8 @@ export function deriveOwnerScreen(planEntries, blocks, bandRecords, {
103
105
  // executor's own descriptor says so, and the screen keeps them apart on the same row.
104
106
  portfolio: countEntry ? {
105
107
  qid: str(countEntry.qid),
106
- total_hits: Number(countBlock?.total_hits) || 0,
108
+ total_hits: Number.isFinite(countBlock?.total_floor) ? null : Number(countBlock?.total_hits) || 0,
109
+ ...(Number.isFinite(countBlock?.total_floor) ? { total_floor: countBlock.total_floor } : {}),
107
110
  counted: !!countBlock && countBlock.error !== true,
108
111
  } : null,
109
112
  });
@@ -173,7 +176,7 @@ export function ownerScreenNegative(screen) {
173
176
  ];
174
177
  if (hit.length) parts.push(`Enumerated WITH records: ${hit.map((o) => `${name(o)} — ${o.records} record(s)`).join("; ")}.`);
175
178
  if (zero.length) parts.push(`Enumerated with zero records — a negative may be stated for these owners and ONLY these: ${zero.map(name).join("; ")}.`);
176
- if (crowd.length) parts.push(`Count-only (NOT a clean — the slice was never enumerated): ${crowd.map((o) => `${name(o)} — ${o.total_hits} hit(s)`).join("; ")}.`);
179
+ if (crowd.length) parts.push(`Count-only (NOT a clean — the slice was never enumerated): ${crowd.map((o) => `${name(o)} — ${Number.isFinite(o.total_floor) ? moreThan(o.total_floor) : o.total_hits} hit(s)`).join("; ")}.`);
177
180
  if (notRun.length) parts.push(`NOT RUN — no negative may be stated for these owners; the gap is disclosed, never a clean: ${notRun.map((o) => `${name(o)}: ${o.reason ?? "capability absent"}`).join("; ")}.`);
178
181
  if (missing.length) parts.push(`Unaccounted for: ${missing.map(name).join("; ")}.`);
179
182
  return parts.join(" ");
@@ -2,7 +2,7 @@
2
2
  "name": "clearotron-driver",
3
3
  "private": true,
4
4
  "type": "module",
5
- "version": "0.4.1-beta.0",
5
+ "version": "0.4.1-beta.2",
6
6
  "license": "AGPL-3.0-only",
7
7
  "description": "Deterministic driver for the trademark clearance workflow: orchestration in code (fan-out, fan-in barrier, gating, retries); the model does judgment leaves only, through a reasoning CLI spawned per stage.",
8
8
  "engines": {
@@ -57,7 +57,7 @@ import { registerUnavailableOffices } from "./register-unreachable.mjs";
57
57
  import { runRecordLogPath } from "../providers/_shared/ledger-path.mjs"; // — this run's record log
58
58
  import { beginAnswerMemory, endAnswerMemory } from "../providers/_shared/answer-memory.mjs";
59
59
  import { validators as koValidators, validateMergedFindings, worstBand, registerSurfacedFilings, raterCaveats, SURVIVOR_BOUNDARY_RE } from "./verify-knockout.mjs";
60
- import { reviewAbout, reviewEvidence, reviewEvidenceLines, applyKnockoutReview, knockoutReviewFile } from "./knockout-review-record.mjs";
60
+ import { reviewDispatch, applyKnockoutReview, knockoutReviewFile } from "./knockout-review-record.mjs";
61
61
  import { stripNextStepSections } from "./knockout-next-step.mjs";
62
62
  import { publishKnockout, composeKnockoutEmail } from "./publish/knockout.mjs";
63
63
  import { writeRunStatus, readRunStatus, rollupStatus, atomicWrite, identitySeed } from "./progress.mjs"; // — the identity seed is shared; the stepper is not
@@ -188,22 +188,24 @@ export function readBackLadder(ctx, sidecarPath, { minted }) { // exported for
188
188
  * pass is presentation. A refusal is never a pass, and "no report" is a product failure — so the last
189
189
  * thing added to the delivery path is the last thing that should be able to stop it.
190
190
  *
191
- * THE EMPTY CASE IS NOT A FAILURE AND IS NOT SILENT. A batch whose default-visible lines are all already
192
- * plain has nothing to dispatch, and paying for a turn to be told so is waste. It is logged with the
193
- * count, so "nothing was flagged" and "the pass never ran" are different rows in the journal rather than
194
- * one absent one.
191
+ * EVERY KNOCKOUT IS REVIEWED, by the owner's ruling of 2026-10-10. The reviewer's first question is the
192
+ * client's, and a record whose wording the driver's read flagged nowhere still gets it: the reviewer is
193
+ * handed the lines a reader meets first, with their addresses, and rewrites toward the client's question
194
+ * or declines at one of them. Only a record with no such line at all is not dispatched. That is logged
195
+ * with its counts, so "there was nothing to read" and "the pass never ran" are different rows in the
196
+ * journal rather than one absent one.
195
197
  */
196
198
  async function knockoutReviewingPass({ ctx, run, K, merged, plan }) {
197
- const about = reviewAbout(merged, plan);
198
- const evidence = reviewEvidence(merged, about);
199
- if (!evidence.rows.length) {
200
- runLog(run.runDir, { event: "knockout-review", outcome: "nothing-flagged", flagged: 0,
199
+ const d = reviewDispatch(merged, plan);
200
+ const { about } = d;
201
+ if (!d.dispatch) {
202
+ runLog(run.runDir, { event: "knockout-review", outcome: "nothing-flagged", flagged: 0, listed: 0,
201
203
  marks: merged.marks.length, excludedMarks: about.marks.length, excludedOwners: about.owners.length });
202
204
  return merged;
203
205
  }
204
206
 
205
207
  const keep = (outcome, detail) => {
206
- runLog(run.runDir, { event: "knockout-review", outcome, flagged: evidence.rows.length, applied: 0,
208
+ runLog(run.runDir, { event: "knockout-review", outcome, flagged: d.flagged, listed: d.listed, applied: 0,
207
209
  delivered: "the rated record, unrewritten", ...detail });
208
210
  note(`reviewing pass: ${outcome} — the rated record ships unrewritten`);
209
211
  return merged;
@@ -211,7 +213,7 @@ async function knockoutReviewingPass({ ctx, run, K, merged, plan }) {
211
213
 
212
214
  try {
213
215
  await koStage("knockout-review", ctx, {
214
- msgCtx: { evidenceLines: reviewEvidenceLines(evidence), exclusionNote: evidence.exclusionNote },
216
+ msgCtx: d.msgCtx,
215
217
  });
216
218
  } catch (e) {
217
219
  return keep("stage-failed", { reason: String(e?.message ?? e).slice(0, 200) });
@@ -228,7 +230,7 @@ async function knockoutReviewingPass({ ctx, run, K, merged, plan }) {
228
230
  if (!mv.ok) return keep("rewrite-refused-by-the-merged-gate", { failures: mv.failures.slice(0, 5) });
229
231
 
230
232
  atomicWrite(K.findings, JSON.stringify(doc, null, 2) + "\n");
231
- runLog(run.runDir, { event: "knockout-review", outcome: "applied", flagged: evidence.rows.length,
233
+ runLog(run.runDir, { event: "knockout-review", outcome: "applied", flagged: d.flagged, listed: d.listed,
232
234
  applied: receipt.applied, declined: receipt.declined,
233
235
  unresolved: receipt.unresolved.length, refused: receipt.refused.length,
234
236
  excludedMarks: about.marks.length, excludedOwners: about.owners.length });