clearotron 0.4.1-beta.1 → 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/build-info.json +2 -2
- package/driver/CHANGELOG.md +6 -0
- package/driver/engine/mcp/gather-config.mjs +4 -0
- package/driver/findings-model.mjs +79 -12
- package/driver/foreign-record-links.mjs +50 -0
- package/driver/package.json +1 -1
- package/driver/pipeline.mjs +30 -3
- package/driver/suite-census.json +6 -0
- package/driver/synthesis-record.mjs +14 -5
- package/mcp-server/CHANGELOG.md +4 -0
- package/mcp-server/package.json +1 -1
- package/package.json +1 -1
- package/portal-ui/package.json +1 -1
- package/providers/oauth-mcp-bridge/CHANGELOG.md +4 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/scripts/e2e-first-time.mjs +2 -2
package/build-info.json
CHANGED
package/driver/CHANGELOG.md
CHANGED
|
@@ -849,6 +849,10 @@ function serverEnv({ sessionKey, agent, runDir, recordAxis }) {
|
|
|
849
849
|
// the server per turn so the acceptance boundary can refuse a payload naming any other index, which
|
|
850
850
|
// turns O3c's measured 224/0 habit into a structure. Absent for every non-fan-out stage.
|
|
851
851
|
if (recordAxis) e.CLEAROTRON_RECORD_AXIS = String(recordAxis);
|
|
852
|
+
// — THE RUN'S REGISTER, so a recording tool gates a record link at the call as verify.mjs gates the
|
|
853
|
+
// saved file (record-origins.mjs). Handed over explicitly: Codex gives a server only its own `env` and
|
|
854
|
+
// the names it forwards, so a value left to inheritance reaches a Claude server and never a Codex one.
|
|
855
|
+
if (process.env.CLEAROTRON_DATABASE) e.CLEAROTRON_DATABASE = process.env.CLEAROTRON_DATABASE;
|
|
852
856
|
// — the ledger paths are RESOLVED HERE and handed to the child explicitly, not forwarded as raw
|
|
853
857
|
// env. Two reasons, and the second is the one that kept this rename parked for months:
|
|
854
858
|
// · the old line was `if (process.env[v]) e[v] = …`, and NO box sets those vars — prod, test and dev
|
|
@@ -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
|
|
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|
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": {
|
package/driver/pipeline.mjs
CHANGED
|
@@ -45,7 +45,7 @@ import { parseCoverageLedgerJson, parseCoverageLedgerFull, deriveCoverageStatus,
|
|
|
45
45
|
import { receiptSettled, readEnvelopeDecision, settleReceipt, settledDeferralsSection, readStickyGaps } from "./envelope-settle.mjs";
|
|
46
46
|
import { readRegisterTaint, readActiveTaintAxes } from "./register-taint.mjs";
|
|
47
47
|
import { parseNamedBand, mergeNamedBands, findCollapsedBands, quarantineUnknownStates, taintQuarantineCleanBlocks, bandRecords } from "./named-band.mjs";
|
|
48
|
-
import { recordOriginsFor } from "./record-origins.mjs";
|
|
48
|
+
import { recordOriginsFor, activeRecordOrigins } from "./record-origins.mjs"; import { recordBlanking, FOREIGN_HOST_TOKEN } from "./foreign-record-links.mjs";
|
|
49
49
|
import { REGISTER_PROVIDER } from "./driver.config.mjs";
|
|
50
50
|
import { noteRegisterServed, registersServedFrom, providerUsageCaveat } from "./register-served.mjs"; // which registers actually served this run — noted eagerly from the dispatch's own resolver, and reconciled at publish against the ledger rows, which carry the vendor per call
|
|
51
51
|
import { FACTS_FILE as DIGEST_FACTS_FILE, ACCOUNTING_STAMP as DIGEST_ACCOUNTING_STAMP, recordedFindingUris,
|
|
@@ -163,7 +163,7 @@ import { parsePlacementsJson } from "./placement-model.mjs"; // B2 — the str
|
|
|
163
163
|
import { readAnchors } from "./anchor-reader.mjs";
|
|
164
164
|
import { findEngagementReceipts } from "./engagement-receipt.mjs";
|
|
165
165
|
import { plainRegisterFlags } from "./plain-register.mjs";
|
|
166
|
-
import { parseFindingsJson, parseFindingsJsonLenient, consolidateFindings, deriveDisplayVerdict, bindRecommendation, compareBlockingPower, inDispositionMode, DISPOSITION_GROUP, deriveActionConditions, cardedParties, actionPartyReferences, quarantinedConditionRows, salvageRepairTargets, riskStatement, verdictStance, remapActionOrdinals, joinAskToAnswer, stripAskLabel, bandBorderlineDeclarations, reasonedNegativeGroups } from "./findings-model.mjs";
|
|
166
|
+
import { parseFindingsJson, parseFindingsJsonLenient, consolidateFindings, deriveDisplayVerdict, bindRecommendation, compareBlockingPower, inDispositionMode, DISPOSITION_GROUP, deriveActionConditions, cardedParties, actionPartyReferences, quarantinedConditionRows, salvageRepairTargets, riskStatement, verdictStance, remapActionOrdinals, joinAskToAnswer, stripAskLabel, bandBorderlineDeclarations, reasonedNegativeGroups, blankForeignRecordLinks } from "./findings-model.mjs";
|
|
167
167
|
import { foldCaption, foldCardRead } from "./card-budget.mjs";
|
|
168
168
|
// S2 — the report card's mechanical frame, composed from the record instead of dictated (see below).
|
|
169
169
|
import { carriesOwnFrame, composeCard } from "./card-frame.mjs";
|
|
@@ -5035,7 +5035,7 @@ export function buildFailurePacket({ runId, agent, job = {}, failedStage, shortR
|
|
|
5035
5035
|
// a top-level defect, or nothing salvageable — fails the run exactly as before. One bad object never silently
|
|
5036
5036
|
// suppresses a real conflict, and never silently kills a ~$40 run.
|
|
5037
5037
|
async function quarantineSynth(r, ctx, name) {
|
|
5038
|
-
if (r.ok) return r;
|
|
5038
|
+
if (r.ok) return r; if (foreignRecordLinksBlanked(r, ctx, name)) return { ...r, ok: true, fail: undefined, repaired: "foreign-record-links-blanked" };
|
|
5039
5039
|
// A6: a max_tokens-named content fault wraps the underlying validator token — strip the wrapper so a
|
|
5040
5040
|
// per-finding shape defect on a ceiling-cut turn still reaches the lenient-salvage lane.
|
|
5041
5041
|
const fail = String(r.fail || "").replace(/^max_tokens_no_output:/, "");
|
|
@@ -16993,3 +16993,30 @@ function orphanAreaLabel(qid, axis, plan) {
|
|
|
16993
16993
|
export function instructedScopeToWrite(job, { jobRebuiltFromStatus = false, scopeOnDisk = false } = {}) { // @internal
|
|
16994
16994
|
return jobRebuiltFromStatus && scopeOnDisk ? null : instructedScopeOf(job);
|
|
16995
16995
|
}
|
|
16996
|
+
|
|
16997
|
+
// ── THE SALVAGE FOR A FOREIGN RECORD LINK: BLANK IT, RE-CHECK, DELIVER ──────────────────────────────────
|
|
16998
|
+
//
|
|
16999
|
+
// A seat that writes an office's page, or another register's, into a record link is refused by the
|
|
17000
|
+
// findings gate (`finding_record_url_foreign_host`). The salvage lane below takes that token, because it is
|
|
17001
|
+
// a `finding_…` token, but its lenient re-parse does not judge hosts, so it found nothing to repair and
|
|
17002
|
+
// failed the run: R2 on 0.4.1-beta.1 delivered no report over eleven copied office links. Here the driver
|
|
17003
|
+
// blanks every foreign link itself (blankForeignRecordLinks says how), keeps what it changed beside the run,
|
|
17004
|
+
// and re-checks the file with the stage's own validator. A pass delivers; anything still refused falls
|
|
17005
|
+
// through to the lanes below exactly as before, with fewer defects in the file.
|
|
17006
|
+
export function foreignRecordLinksBlanked(r, ctx, name) { // exported for its test
|
|
17007
|
+
const fail = String(r?.fail || "").replace(/^max_tokens_no_output:/, "");
|
|
17008
|
+
if (!fail.includes(FOREIGN_HOST_TOKEN) || !existsSync(ctx.paths.findings)) return false;
|
|
17009
|
+
let doc;
|
|
17010
|
+
try { doc = JSON.parse(readFileSync(ctx.paths.findings, "utf8")); } catch { return false; }
|
|
17011
|
+
const { doc: repaired, blanked, kept } = blankForeignRecordLinks(doc, activeRecordOrigins());
|
|
17012
|
+
if (!blanked.length) return false;
|
|
17013
|
+
atomicWrite(ctx.paths.findings, JSON.stringify(repaired, null, 2) + "\n");
|
|
17014
|
+
recordBlanking(ctx.paths.runDir, { stage: name, layer: "salvage", blanked, kept, fail });
|
|
17015
|
+
note(`[${name}] ${blanked.length} record link(s) on a host this register does not publish were blanked by the driver; the originals are in _driver/foreign-record-links.json`);
|
|
17016
|
+
const def = STAGES[name];
|
|
17017
|
+
const out = def?.out ? def.out(ctx.paths, ctx.axis) : null;
|
|
17018
|
+
const v = out && existsSync(out) && def.validate ? def.validate(out, readFileSync(out, "utf8")) : null;
|
|
17019
|
+
if (v?.ok) return true;
|
|
17020
|
+
runLog(ctx.paths.runDir, { event: "foreign-record-links-recheck", stage: name, ok: false, fail: String(v?.reason ?? "no validator").slice(0, 200) });
|
|
17021
|
+
return false;
|
|
17022
|
+
}
|
package/driver/suite-census.json
CHANGED
|
@@ -585,6 +585,12 @@
|
|
|
585
585
|
"skips": 0,
|
|
586
586
|
"todos": 0
|
|
587
587
|
},
|
|
588
|
+
"a-foreign-record-link-is-named-once-and-blanked-at-salvage.test.mjs": {
|
|
589
|
+
"tests": 8,
|
|
590
|
+
"asserts": 31,
|
|
591
|
+
"skips": 0,
|
|
592
|
+
"todos": 0
|
|
593
|
+
},
|
|
588
594
|
"a-frame-call-with-places-per-name-passes-the-gate.test.mjs": {
|
|
589
595
|
"tests": 2,
|
|
590
596
|
"asserts": 7,
|
|
@@ -53,7 +53,7 @@ import { declinationCallPaths, readDeclinations } from "./declination-tool.mjs";
|
|
|
53
53
|
import { reconcileDeclinationDuty, declinationDutyRefusal } from "./declination-duty.mjs";
|
|
54
54
|
import { findingUris } from "./record-carry.mjs"; // one derivation of "which records did the findings name", called not copied
|
|
55
55
|
import { foldAfterSave } from "./record-fold.mjs"; // once the delivery seam is passed, the saved record is folded again
|
|
56
|
-
|
|
56
|
+
import { recordOriginsOr, blankOnRepeat, recordBlanking, FOREIGN_HOST_TOKEN } from "./foreign-record-links.mjs"; // a foreign record link: refused once, then blanked
|
|
57
57
|
export const NARRATIVE_FILE = "narrative.md";
|
|
58
58
|
export const FINDINGS_FILE = "findings.json";
|
|
59
59
|
|
|
@@ -269,7 +269,7 @@ export function withoutWithheldRows(rows, ledger) {
|
|
|
269
269
|
return (Array.isArray(rows) ? rows : []).filter((r) => !namesIn(r?.area).some((n) => withheld.has(n)));
|
|
270
270
|
}
|
|
271
271
|
|
|
272
|
-
export function acceptSynthesis(params, { asks = [], ledger = null, manifest = null, method = undefined, methodInvalid = null, owed = null, declined = null } = {}) {
|
|
272
|
+
export function acceptSynthesis(params, { asks = [], ledger = null, manifest = null, method = undefined, methodInvalid = null, owed = null, declined = null, recordOrigins = null } = {}) {
|
|
273
273
|
const doc = params?.findings;
|
|
274
274
|
if (!doc || typeof doc !== "object" || Array.isArray(doc)) {
|
|
275
275
|
return { ok: false, reason: "synthesis_findings_missing: `findings` must be the findings document object { schema_version, rated_under_framework, findings, coverage, … } — this is the record the report and the workbook are built from, and there is no path that renders without it" };
|
|
@@ -383,7 +383,7 @@ export function acceptSynthesis(params, { asks = [], ledger = null, manifest = n
|
|
|
383
383
|
// own file gone wrong, and the seat cannot fix it — so it is refused by name, never read as "no method".
|
|
384
384
|
if (methodInvalid) return { ok: false, reason: `framework_method_unreadable: _driver/${FROZEN_METHOD_FILE} does not read back (${String(methodInvalid).slice(0, 160)}) — a driver fault, not the writer's` };
|
|
385
385
|
let parsedDoc;
|
|
386
|
-
try { parsedDoc = parseFindingsJson(findings, manifest ? { manifest, ...(method !== undefined ? { method } : {}) } : {}); }
|
|
386
|
+
try { parsedDoc = parseFindingsJson(findings, { ...(manifest ? { manifest, ...(method !== undefined ? { method } : {}) } : {}), ...(Array.isArray(recordOrigins) ? { recordOrigins } : {}) }); }
|
|
387
387
|
catch (e) {
|
|
388
388
|
return { ok: false, reason: `synthesis_findings_invalid: ${String(e?.message ?? e).slice(0, 300)}` };
|
|
389
389
|
}
|
|
@@ -608,7 +608,7 @@ export function recordSynthesis(runDir, received, opts = {}) {
|
|
|
608
608
|
const auto = driverReadsFor(dir0);
|
|
609
609
|
const { asks = auto.asks, ledger = auto.ledger, manifest = auto.manifest,
|
|
610
610
|
method = auto.method, methodInvalid = auto.methodInvalid ?? null,
|
|
611
|
-
owed = auto.owed, declined = auto.declined,
|
|
611
|
+
owed = auto.owed, declined = auto.declined, recordOrigins = undefined,
|
|
612
612
|
now = () => new Date().toISOString() } = opts;
|
|
613
613
|
const { dir, payload, accepted, refusals } = synthesisCallPaths(dir0);
|
|
614
614
|
// Its sibling rule — ONE FILE PER CALL, refusals included. This used to
|
|
@@ -644,7 +644,16 @@ export function recordSynthesis(runDir, received, opts = {}) {
|
|
|
644
644
|
call = merged.merged;
|
|
645
645
|
}
|
|
646
646
|
|
|
647
|
-
|
|
647
|
+
// A RECORD LINK ON A HOST THIS REGISTER DOES NOT PUBLISH is gated here as verify.mjs gates the saved
|
|
648
|
+
// file. The run's first such call is refused, naming every finding, so the seat can fix them in this
|
|
649
|
+
// turn; a repeat is accepted with those links blanked and the originals kept (foreign-record-links.mjs).
|
|
650
|
+
const origins = recordOriginsOr(recordOrigins);
|
|
651
|
+
const repeat = blankOnRepeat(call, origins, refusalsFor(dir0).some((r) => String(r?.reason ?? "").includes(FOREIGN_HOST_TOKEN)));
|
|
652
|
+
if (repeat.blanked.length) {
|
|
653
|
+
call = repeat.call;
|
|
654
|
+
recordBlanking(dir0, { stage: "synthesis", layer: "call", blanked: repeat.blanked, kept: repeat.kept });
|
|
655
|
+
}
|
|
656
|
+
const v = acceptSynthesis(call, { asks, ledger, manifest, method, methodInvalid, owed, declined, recordOrigins: origins });
|
|
648
657
|
if (!v.ok) {
|
|
649
658
|
noteRefusal(v.reason);
|
|
650
659
|
return { written: null, refused: v.reason,
|
package/mcp-server/CHANGELOG.md
CHANGED
package/mcp-server/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trademark-artifacts-mcp",
|
|
3
|
-
"version": "0.4.1-beta.
|
|
3
|
+
"version": "0.4.1-beta.2",
|
|
4
4
|
"license": "AGPL-3.0-only",
|
|
5
5
|
"private": true,
|
|
6
6
|
"description": "MCP server to interrogate clearotron trademark-clearance runs — list/read artifacts, trace the full decision flow, telemetry/cost, coverage, single-run search, and a gated single-step what-if. Imports the clearotron-driver read-only; touches no driver/template/deploy files.",
|
package/package.json
CHANGED
package/portal-ui/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "portal-ui",
|
|
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": "The unified trademark portal UI. One address, one login: who you are decides what you see. Built as a static bundle, served by driver/portal-service.mjs — the browser never reaches profile-service or recipe-service.",
|
|
8
8
|
"engines": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trademark-oauth-mcp-bridge",
|
|
3
|
-
"version": "0.4.1-beta.
|
|
3
|
+
"version": "0.4.1-beta.2",
|
|
4
4
|
"license": "AGPL-3.0-only",
|
|
5
5
|
"private": true,
|
|
6
6
|
"description": "OAuth 2.1 MCP stdio bridge used by the engine's case-law gather stage (courtlistener / legaldatahunter).",
|
|
@@ -86,7 +86,7 @@ export const COUNTED_EVENTS = {
|
|
|
86
86
|
"record-carry-failed": FAILED, "record-discard-failed": FAILED, "register-digest-facts-failed": FAILED,
|
|
87
87
|
"register-plan-infeasible": FAILED, "register-positions-failed": FAILED, "register-presence-failed": FAILED,
|
|
88
88
|
"remedy-accounting-failed": FAILED, "report-cards-rate-limited": FAILED, "restamp-miss": FAILED,
|
|
89
|
-
"reviewer-degenerate": FAILED, "salvage-lane-no-target": FAILED, "searched-jurisdictions-unresolved": FAILED,
|
|
89
|
+
"reviewer-degenerate": FAILED, "salvage-lane-no-target": FAILED, "searched-jurisdictions-unresolved": FAILED, "foreign-record-links-recheck": FAILED,
|
|
90
90
|
"skeptic-skipped": FAILED, "stage-input-over-ceiling": FAILED, "status-write-failed": FAILED,
|
|
91
91
|
"stale-repair-entry-done": { kind: FAILED, when: (e) => (Array.isArray(e.failed) ? e.failed.length : Number(e.failed)) > 0 },
|
|
92
92
|
// written only where the step threw
|
|
@@ -138,7 +138,7 @@ export const COUNTED_EVENTS = {
|
|
|
138
138
|
"commonlaw-channels-unstated": REPAIR, "commonlaw-channels-unusable": REPAIR,
|
|
139
139
|
"corrective-unnamed-removal-repaired": REPAIR, "coverage-floor-clamp": REPAIR, "jx-receipt-repaired": REPAIR,
|
|
140
140
|
"knockout-next-step-removed": REPAIR, "recall-reconciliation-positions-rederived": REPAIR, "registry-auto-correct": REPAIR,
|
|
141
|
-
"repair-attempted": REPAIR, "terminal-guard-clamp": REPAIR, "verdict-conditions-dropped": REPAIR,
|
|
141
|
+
"repair-attempted": REPAIR, "terminal-guard-clamp": REPAIR, "verdict-conditions-dropped": REPAIR, "foreign-record-links-blanked": REPAIR,
|
|
142
142
|
"verdict-hardened-by-repair": REPAIR, "verdict-rederive-repair": REPAIR,
|
|
143
143
|
"common-law-half-quarantined": QUARANTINE, "coverage-ledger-quarantined": QUARANTINE,
|
|
144
144
|
"findings-actions-quarantined": QUARANTINE, "named-band-state-quarantined": QUARANTINE, "quarantine": QUARANTINE,
|