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.
- package/THIRD-PARTY-NOTICES.md +1 -1
- package/bin/README.md +1 -1
- package/bin/clearotron.mjs +2 -0
- package/bin/mcp.mjs +80 -0
- package/bin/start.mjs +14 -1
- package/build-info.json +2 -2
- package/demo/README.md +1 -1
- package/docs/architecture/04-configuration-reference.md +0 -1
- package/docs/architecture/06-operations-runbook.md +4 -3
- package/docs/architecture/08-development-guide.md +3 -1
- package/driver/CHANGELOG.md +17 -0
- package/driver/citation-census.json +3 -3
- package/driver/compare.mjs +1 -1
- package/driver/contract-audit.mjs +6 -6
- package/driver/contract-dictation-registry.mjs +1 -1
- package/driver/contract-e3-backlog.mjs +17 -16
- package/driver/contract-vocabulary.mjs +138 -125
- package/driver/declination-call.mjs +5 -1
- package/driver/dev-portal.mjs +2 -1
- package/driver/engine/mcp/codex-config.mjs +7 -2
- package/driver/engine/mcp/gather-config.mjs +4 -0
- package/driver/engine/mcp/recording-server.mjs +1 -1
- package/driver/engine/mcp/supplemental.mjs +26 -1
- package/driver/engine/openai-agent.mjs +13 -3
- package/driver/findings-model.mjs +80 -13
- package/driver/flag-snapshot.mjs +2 -1
- package/driver/foreign-record-links.mjs +50 -0
- package/driver/frame-diff-model.mjs +1 -1
- package/driver/gateway.mjs +4 -4
- package/driver/jx.mjs +1 -1
- package/driver/knockout-review-record.mjs +55 -1
- package/driver/named-band.mjs +2 -0
- package/driver/owner-screen.mjs +7 -4
- package/driver/package.json +1 -1
- package/driver/pipeline-knockout.mjs +14 -12
- package/driver/pipeline.mjs +118 -20
- package/driver/portal-service.mjs +5 -4
- package/driver/products.mjs +1 -1
- package/driver/profiles.mjs +4 -3
- package/driver/publish/publish-inputs.mjs +2 -2
- package/driver/publish/xlsx.mjs +1 -1
- package/driver/register-count.mjs +2 -2
- package/driver/register-coverage.mjs +1 -1
- package/driver/register-plan.mjs +13 -2
- package/driver/result-noun-fields.mjs +3 -3
- package/driver/scope-rules.mjs +2 -2
- package/driver/stage-context.mjs +9 -0
- package/driver/stages-knockout.mjs +6 -5
- package/driver/stages.mjs +23 -23
- package/driver/suite-census.json +97 -31
- package/driver/synthesis-record.mjs +14 -5
- package/driver/verify.mjs +1 -1
- package/mcp-server/CHANGELOG.md +8 -0
- package/mcp-server/CONNECT.md +9 -3
- package/mcp-server/demo-mode.mjs +10 -0
- package/mcp-server/lib/runs.mjs +7 -1
- package/mcp-server/lib/scrub.mjs +1 -1
- package/mcp-server/package.json +1 -1
- package/mcp-server/packs/README.md +1 -1
- package/mcp-server/server.mjs +16 -2
- package/package.json +3 -2
- package/portal-ui/package.json +3 -3
- package/providers/_shared/count.mjs +5 -1
- package/providers/_shared/enumerate.mjs +66 -21
- package/providers/_shared/execute-plan.mjs +13 -4
- package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/providers/signa/src/capabilities.js +15 -0
- package/providers/signa/src/core.js +55 -26
- package/scripts/e2e-first-time.mjs +3 -3
- package/scripts/env-audit.mjs +3 -2
- package/scripts/freeze-example-run.mjs +8 -8
- package/scripts/mcp-registry-listing.mjs +199 -0
- package/scripts/mint-plugin-folder.mjs +65 -0
- package/scripts/release-version.mjs +44 -0
- package/scripts/score.mjs +1 -1
- package/shared/register-floor.mjs +19 -0
- package/shared/trigger-lane.mjs +1 -1
- 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.
|
|
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 ?
|
|
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
|
|
657
|
-
//
|
|
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:
|
|
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 (
|
|
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.
|
|
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
|
+
}
|
package/driver/flag-snapshot.mjs
CHANGED
|
@@ -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
|
|
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
|
|
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}].
|
package/driver/gateway.mjs
CHANGED
|
@@ -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
|
|
3196
|
-
// the undispatchable directives
|
|
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
|
|
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
|
|
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
|
*
|
package/driver/named-band.mjs
CHANGED
|
@@ -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
|
package/driver/owner-screen.mjs
CHANGED
|
@@ -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
|
-
|
|
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(" ");
|
package/driver/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "clearotron-driver",
|
|
3
3
|
"private": true,
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "0.4.1-beta.
|
|
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 {
|
|
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
|
-
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
* 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
|
|
198
|
-
const
|
|
199
|
-
if (!
|
|
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:
|
|
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:
|
|
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:
|
|
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 });
|