clearotron 0.3.2 → 0.3.3-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRIBUTING.md +12 -0
- package/INSTALL.md +8 -0
- package/bin/onboard.mjs +109 -13
- package/bin/start.mjs +1 -1
- package/build-info.json +2 -2
- package/demo/MANIFEST.json +27 -0
- package/docs/INTAKE.md +8 -0
- package/docs/architecture/04-configuration-reference.md +29 -11
- package/driver/CHANGELOG.md +49 -0
- package/driver/citation-census.json +3 -3
- package/driver/clearance-variants-record.mjs +12 -1
- package/driver/common-law-coverage-status.mjs +113 -0
- package/driver/config-inventory.mjs +1 -1
- package/driver/contract-audit.mjs +1 -1
- package/driver/contract-e3-backlog.mjs +37 -37
- package/driver/contract-vocabulary.mjs +8 -8
- package/driver/coverage-form-io.mjs +3 -1
- package/driver/coverage-form.mjs +38 -11
- package/driver/coverage-ledger.mjs +37 -7
- package/driver/coverage-union.mjs +2 -2
- package/driver/crowd-context.mjs +19 -6
- package/driver/dev-portal.mjs +3 -3
- package/driver/drainer-identity.mjs +1 -1
- package/driver/driver.config.mjs +80 -9
- package/driver/engine/CONTRACT.md +3 -2
- package/driver/engine/anthropic-agent.mjs +34 -7
- package/driver/engine/mcp/clarivate-server.mjs +4 -2
- package/driver/engine/mcp/corsearch-server.mjs +3 -1
- package/driver/engine/mcp/coverage-server.mjs +1 -1
- package/driver/engine/mcp/dispositions-server.mjs +47 -5
- package/driver/engine/mcp/euipo-server.mjs +2 -0
- package/driver/engine/mcp/free-tier-server.mjs +2 -0
- package/driver/engine/mcp/gather-config.mjs +8 -2
- package/driver/engine/mcp/probe-server.mjs +37 -0
- package/driver/engine/mcp/proposal-fields.mjs +45 -0
- package/driver/engine/mcp/recording-server.mjs +30 -0
- package/driver/engine/mcp/signa-server.mjs +2 -0
- package/driver/engine/mcp/supplemental.mjs +89 -12
- package/driver/engine/mcp/unit-note-server.mjs +50 -0
- package/driver/engine/mcp/uspto-local-server.mjs +2 -0
- package/driver/engine/openai-agent.mjs +7 -0
- package/driver/engine/probe.mjs +67 -14
- package/driver/engine/tool-refusal.mjs +16 -0
- package/driver/enqueue-schema.mjs +2 -2
- package/driver/envelope-settle.mjs +82 -13
- package/driver/findings-model.mjs +4 -4
- package/driver/gateway.mjs +18 -2
- package/driver/manager-groups-verdict.mjs +1 -1
- package/driver/matter-frame-record.mjs +24 -7
- package/driver/named-band.mjs +1 -1
- package/driver/package.json +1 -1
- package/driver/partial-payload-baseline.json +12 -3
- package/driver/pipeline-knockout.mjs +3 -3
- package/driver/pipeline.mjs +154 -50
- package/driver/plan-run-agreement-verdict.mjs +49 -0
- package/driver/portal-service.mjs +8 -4
- package/driver/progress.mjs +14 -3
- package/driver/publish/index.mjs +41 -26
- package/driver/publish/report-data.mjs +4 -3
- package/driver/publish/xlsx.mjs +26 -4
- package/driver/queue-markers.mjs +44 -0
- package/driver/queue-watch-verdict.mjs +2 -2
- package/driver/reference-score.mjs +10 -2
- package/driver/register-availability.mjs +2 -2
- package/driver/register-plan.mjs +313 -21
- package/driver/roster-verdict.mjs +1 -1
- package/driver/runner.mjs +26 -2
- package/driver/settle-stamp.mjs +10 -3
- package/driver/skills/clearance-common-law/SKILL.md +2 -0
- package/driver/skills/clearance-register/SKILL.md +44 -3
- package/driver/skills/clearance-register/digest.md +5 -5
- package/driver/skills/clearance-register/providers/clarivate.md +1 -1
- package/driver/skills/clearance-register/unit.md +39 -0
- package/driver/skills/clearance-variants/SKILL.md +1 -1
- package/driver/skills/matter-frame/SKILL.md +4 -2
- package/driver/stages.mjs +12 -5
- package/driver/status-snapshot.mjs +2 -2
- package/driver/suite-census.json +293 -29
- package/driver/synthesis-record.mjs +80 -2
- package/driver/unit-file-drift.mjs +3 -3
- package/driver/unit-inventory.mjs +2 -2
- package/driver/unit-state-verdict.mjs +1 -1
- package/driver/updater-identity.mjs +2 -3
- package/driver/variant-manifest-model.mjs +11 -1
- package/driver/verify.mjs +5 -5
- package/driver/withheld-families.mjs +104 -0
- package/mcp-server/CHANGELOG.md +8 -0
- package/mcp-server/lib/brief.mjs +16 -12
- package/mcp-server/lib/runs.mjs +1 -1
- package/mcp-server/package.json +1 -1
- package/mcp-server/server.mjs +3 -2
- package/package.json +2 -2
- package/portal-ui/dist/assets/{index-DMthc7PQ.js → index-GBbbyQxc.js} +22 -4
- package/portal-ui/dist/index.html +1 -1
- package/portal-ui/package.json +1 -1
- package/providers/_shared/count.mjs +2 -2
- package/providers/_shared/enumerate.mjs +15 -2
- package/providers/_shared/execute-plan.mjs +19 -1
- package/providers/_shared/plan-guards.mjs +40 -0
- package/providers/clarivate/src/capabilities.js +15 -5
- package/providers/clarivate/src/core.js +41 -5
- package/providers/corsearch/src/capabilities.js +4 -0
- package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/providers/signa/src/capabilities.js +22 -8
- package/providers/signa/src/core.js +12 -1
- package/scripts/demo-evidence.mjs +114 -0
- package/scripts/e2e.mjs +1 -1
- package/scripts/engine-probe.mjs +6 -5
- package/scripts/env-audit.mjs +1 -1
- package/scripts/freeze-example-run.mjs +3 -3
- package/scripts/live-surface-check.mjs +32 -33
- package/scripts/mint-suite-census.mjs +66 -0
- package/scripts/package-size-budget.mjs +117 -0
- package/scripts/register-plan-shape.mjs +259 -0
- package/scripts/release-note-required.mjs +38 -1
- package/scripts/repo-writes.mjs +1 -1
- package/scripts/report-sections-render-check.mjs +7 -3
- package/scripts/score.mjs +7 -1
- package/scripts/settings-render-check.mjs +36 -0
- package/scripts/travelling-predicates.mjs +1 -1
- package/shared/identifier-scan.mjs +22 -5
- package/shared/scroll-settle.mjs +67 -0
|
@@ -181,7 +181,13 @@ export function uncarriedCoverageLimits(rows, ledger) {
|
|
|
181
181
|
if (!Array.isArray(ledger) || !ledger.length) return null;
|
|
182
182
|
const limited = ledger.filter((r) => {
|
|
183
183
|
const st = String(r?.status ?? "").trim();
|
|
184
|
-
|
|
184
|
+
// `withheld-by-judgment` sits beside `confirmed-clean` here, and the reason is the opposite one.
|
|
185
|
+
// A clean row has nothing to carry. A withheld row is a family the reading turn chose NOT to open,
|
|
186
|
+
// having read the identical question as a list and found what it needed — that is where the work
|
|
187
|
+
// was spent, not a slice the run could not clear, and the rule that nothing is added to the report keeps it out
|
|
188
|
+
// entirely. Demanding it be carried would put a "we did not search this" line in front of a lawyer
|
|
189
|
+
// about a decision that made the search better.
|
|
190
|
+
return st !== "" && st !== "confirmed-clean" && st !== "note" && st !== "withheld-by-judgment";
|
|
185
191
|
});
|
|
186
192
|
if (!limited.length) return null; // the run records no limit: nothing to carry
|
|
187
193
|
const carried = (rows ?? []).some((r) => {
|
|
@@ -201,6 +207,66 @@ export function uncarriedCoverageLimits(rows, ledger) {
|
|
|
201
207
|
* could assert either could waive its own contract — the same reason the reviewer's transport takes
|
|
202
208
|
* `receiptPresent` from the driver rather than from the call.
|
|
203
209
|
*/
|
|
210
|
+
/**
|
|
211
|
+
* The coverage rows a client may see, with every withheld family's row removed. PURE.
|
|
212
|
+
*
|
|
213
|
+
* `withheld-by-judgment` is the reading turn saying it chose not to open a family — it read the
|
|
214
|
+
* identical question as a list, found what it needed, and spent the work there. That is a decision
|
|
215
|
+
* about where effort went, not a gap in the client's search, and the rule that nothing is added to the report (2026-09-18) keeps it in the run record
|
|
216
|
+
* and the coverage ledger alone.
|
|
217
|
+
*
|
|
218
|
+
* Applied here rather than trusted to the model, because the model is the surface that can get it
|
|
219
|
+
* wrong: it authors these rows, it has been told the family was withheld, and a row reading "we did
|
|
220
|
+
* not search this" in front of a lawyer is the sentence that rule forbids — about a decision that
|
|
221
|
+
* made the search better. The LEDGER is the authority on which families were withheld.
|
|
222
|
+
*
|
|
223
|
+
* A row for a family the ledger holds as anything else is untouched, and that direction matters more
|
|
224
|
+
* than this one: a documented limit dropped by accident is a disclosure the reader is owed and does
|
|
225
|
+
* not get.
|
|
226
|
+
*/
|
|
227
|
+
export function withoutWithheldRows(rows, ledger) {
|
|
228
|
+
// A FAMILY IS ONLY WITHHELD IF THE LEDGER HOLDS NOTHING ELSE ABOUT IT. A family can be withheld at
|
|
229
|
+
// the axis level AND still carry a documented limit on one of its slices — measured: an axis whose
|
|
230
|
+
// whole-axis row is withheld while `primary-sweep (exact: <mark> [cl 25])` is a disclosed limit the
|
|
231
|
+
// synthesis gate then demands be carried. Dropping by family name alone deleted the carrying row and
|
|
232
|
+
// the run failed, which is the worse error stated twice over: a disclosure the reader is owed, gone,
|
|
233
|
+
// and a run that does not deliver.
|
|
234
|
+
//
|
|
235
|
+
// So a family qualifies only when EVERY ledger row for it is withheld. One row saying anything else
|
|
236
|
+
// keeps the whole family's rows, and that is the direction to fail in.
|
|
237
|
+
const byFamily = new Map();
|
|
238
|
+
for (const r of (Array.isArray(ledger) ? ledger : [])) {
|
|
239
|
+
const axis = String(r?.axis ?? "").trim().toLowerCase();
|
|
240
|
+
if (!axis) continue;
|
|
241
|
+
const isWithheld = String(r?.status ?? "").trim() === "withheld-by-judgment";
|
|
242
|
+
byFamily.set(axis, (byFamily.get(axis) ?? true) && isWithheld);
|
|
243
|
+
}
|
|
244
|
+
const withheld = new Set([...byFamily.entries()].filter(([, only]) => only).map(([axis]) => axis));
|
|
245
|
+
if (!withheld.size) return Array.isArray(rows) ? rows : [];
|
|
246
|
+
|
|
247
|
+
// ── THE AXIS IS NAMED IN A SEGMENT, NOT ALWAYS THE FIRST ONE ──────────────────────────────────
|
|
248
|
+
//
|
|
249
|
+
// This read `area.split("/")[0]` and was inert for the two shapes that matter most, failing OPEN —
|
|
250
|
+
// toward the client, which is the wrong direction for a gate whose whole job is to keep a row off
|
|
251
|
+
// the page. Measured against the real strings:
|
|
252
|
+
//
|
|
253
|
+
// `incumbent-class (entire axis)` no slash at all — the whole-axis row a client reads
|
|
254
|
+
// `Register / incumbent-class` the axis is the SECOND segment; the first is a layer
|
|
255
|
+
// `incumbent-class / extra script group` the only shape the first read caught
|
|
256
|
+
//
|
|
257
|
+
// So every `/`-separated segment is considered, with a trailing parenthetical stripped, and each is
|
|
258
|
+
// compared by EQUALITY against the axis names the ledger actually holds.
|
|
259
|
+
//
|
|
260
|
+
// Equality on a segment, never `includes`: a substring test over an open vocabulary is how one axis
|
|
261
|
+
// name once matched two unrelated areas, and here a false match DROPS a row — a documented limit the
|
|
262
|
+
// reader is owed, gone silently. The failure this gate prevents is a withheld row appearing; the
|
|
263
|
+
// failure it must not cause is a disclosure disappearing, and that one is worse.
|
|
264
|
+
const namesIn = (area) => String(area ?? "").toLowerCase().split("/")
|
|
265
|
+
.map((seg) => seg.replace(/\([^)]*\)/g, " ").trim())
|
|
266
|
+
.filter(Boolean);
|
|
267
|
+
return (Array.isArray(rows) ? rows : []).filter((r) => !namesIn(r?.area).some((n) => withheld.has(n)));
|
|
268
|
+
}
|
|
269
|
+
|
|
204
270
|
export function acceptSynthesis(params, { asks = [], ledger = null, manifest = null, owed = null, declined = null } = {}) {
|
|
205
271
|
const doc = params?.findings;
|
|
206
272
|
if (!doc || typeof doc !== "object" || Array.isArray(doc)) {
|
|
@@ -228,7 +294,19 @@ export function acceptSynthesis(params, { asks = [], ledger = null, manifest = n
|
|
|
228
294
|
// Two copies that must agree is a second-authoring defect, so there are not two. The record is the
|
|
229
295
|
// machine contract and the narrative's coverage list is RENDERED from it. Disagreement is not detected;
|
|
230
296
|
// it is impossible.
|
|
231
|
-
|
|
297
|
+
// ── NOTHING ADDED TO THE REPORT: A FAMILY THE READING TURN DID NOT OPEN STAYS OFF IT, WHATEVER THE MODEL WROTE ──
|
|
298
|
+
//
|
|
299
|
+
// `withheld-by-judgment` is the reading turn saying it chose not to open a family — it read the
|
|
300
|
+
// identical question as a list, found what it needed, and spent the work there instead. That is a
|
|
301
|
+
// decision about where effort went, not a gap in the client's search, and it belongs to the run
|
|
302
|
+
// record and the coverage ledger alone.
|
|
303
|
+
//
|
|
304
|
+
// Dropped HERE rather than trusted to the model, because the model is the one surface that can get
|
|
305
|
+
// it wrong: it writes the coverage rows, it has been told the family was withheld, and a row saying
|
|
306
|
+
// "we did not search this" in front of a lawyer is exactly the sentence that rule forbids — about a
|
|
307
|
+
// decision that made the search better. The ledger is the authority on which families were withheld,
|
|
308
|
+
// so a row whose family it holds as withheld goes, however the row was authored.
|
|
309
|
+
const rows = withoutWithheldRows(Array.isArray(doc.coverage) ? doc.coverage : [], ledger);
|
|
232
310
|
if (n.coverage?.rows !== undefined) {
|
|
233
311
|
return { ok: false, reason: "synthesis_coverage_rows_misplaced: coverage rows belong in `findings.coverage`, not on the narrative — the driver renders the narrative's coverage list from the record, so there is one authored set and no way for the client's readable statement and the machine record to disagree. Send `narrative.coverage.read` for the prose" };
|
|
234
312
|
}
|
|
@@ -63,7 +63,7 @@ export function unitBody(text) {
|
|
|
63
63
|
* driver/unit-files.mjs wherever it sits, or null when the repo tracks none
|
|
64
64
|
* dropIns — array of drop-in paths (may be empty)
|
|
65
65
|
* @param {{ok: boolean, why?: string}} probe — could the caller enumerate at all?
|
|
66
|
-
* @returns {{state: "pass"|"fail"|"skip", message: string, drifted: string[], templated: string[]}}
|
|
66
|
+
* @returns {{state: "pass"|"fail"|"skip", blocked?: true, message: string, drifted: string[], templated: string[]}}
|
|
67
67
|
*
|
|
68
68
|
* THREE OUTCOMES, and the third is the one this file exists for. "Could not look" is never "nothing is
|
|
69
69
|
* wrong" — the lesson, applied to the arm that has the same failure mode one layer down: a unit
|
|
@@ -72,7 +72,7 @@ export function unitBody(text) {
|
|
|
72
72
|
*/
|
|
73
73
|
export function unitFileDriftVerdict({ units = [], probe = { ok: true } } = {}) {
|
|
74
74
|
if (!probe.ok) {
|
|
75
|
-
return { state: "skip", drifted: [],
|
|
75
|
+
return { state: "skip", blocked: true, drifted: [],
|
|
76
76
|
message: `could not enumerate systemd units, so no unit file was COMPARED — ${probe.why ?? "no reason given"}. `
|
|
77
77
|
+ "This is a failure to look, not a finding about the deployment" };
|
|
78
78
|
}
|
|
@@ -80,7 +80,7 @@ export function unitFileDriftVerdict({ units = [], probe = { ok: true } } = {})
|
|
|
80
80
|
const unreadable = units.filter((u) => u && u.live == null);
|
|
81
81
|
const untracked = units.filter((u) => u && u.live != null && u.tracked == null);
|
|
82
82
|
if (!comparable.length) {
|
|
83
|
-
return { state: "skip", drifted: [],
|
|
83
|
+
return { state: "skip", blocked: true, drifted: [],
|
|
84
84
|
message: `systemd answered and no unit could be compared to a tracked file (${units.length} unit(s) seen, `
|
|
85
85
|
+ `${unreadable.length} with no readable fragment, ${untracked.length} with no tracked unit file `
|
|
86
86
|
+ "anywhere in the tree) — nothing was checked" };
|
|
@@ -676,7 +676,7 @@ export const CHECKED_TIMERS = Object.freeze(
|
|
|
676
676
|
* that reports on the service keeps saying `inactive`, which is what it says when the timer is working.
|
|
677
677
|
*/
|
|
678
678
|
export function timerVerdict(timers, { probeFailed = null } = {}) {
|
|
679
|
-
if (probeFailed) return { state: "
|
|
679
|
+
if (probeFailed) return { state: "skip", blocked: true, stopped: [], absent: [], message: `could not ask systemd about the timers: ${probeFailed}. That is this check failing to look, not a report about them.` };
|
|
680
680
|
const rows = timers ?? [];
|
|
681
681
|
if (!rows.length) return { state: "pass", stopped: [], absent: [], message: "no timer is declared for this box" };
|
|
682
682
|
const absent = rows.filter((t) => t.load === "not-found").map((t) => t.unit).sort();
|
|
@@ -726,7 +726,7 @@ export function unitInventoryVerdict({
|
|
|
726
726
|
inventory = UNIT_INVENTORY,
|
|
727
727
|
} = {}) {
|
|
728
728
|
if (!probe.ok) {
|
|
729
|
-
return { state: "skip", undeclared: [], orphaned: [], absent: [], misdeclared: [],
|
|
729
|
+
return { state: "skip", blocked: true, undeclared: [], orphaned: [], absent: [], misdeclared: [],
|
|
730
730
|
message: `could not enumerate systemd units, so the inventory was NOT checked — ${probe.why ?? "no reason given"}. `
|
|
731
731
|
+ "A failure to look is not a finding about the deployment." };
|
|
732
732
|
}
|
|
@@ -100,7 +100,7 @@ export function classifyActiveState(active) {
|
|
|
100
100
|
export function unitsActiveVerdict({ units, probe }) {
|
|
101
101
|
// — a failure to look is never a finding about the deployment.
|
|
102
102
|
if (!probe?.ok)
|
|
103
|
-
return { state: "skip", message: `could not enumerate systemd --user units — ${probe?.why ?? "no reason reported"}` };
|
|
103
|
+
return { state: "skip", blocked: true, message: `could not enumerate systemd --user units — ${probe?.why ?? "no reason reported"}` };
|
|
104
104
|
|
|
105
105
|
const label = (u) => `${u.unit}=${u.active}`
|
|
106
106
|
+ (u.type ? ` (${u.type}${u.since ? `, since ${u.since}` : ""})` : (u.since ? ` (since ${u.since})` : ""));
|
|
@@ -111,8 +111,7 @@ export function updaterVerdict({ stamp, now = null, deployClone = null, maxAgeSe
|
|
|
111
111
|
return { state: "fail", message:
|
|
112
112
|
`no updater identity stamp at ${UPDATER_STAMP_BASENAME}: the copy of the updater that deploys this `
|
|
113
113
|
+ "box did NOT say what it is, so whether it is the current one was not established. A copy old "
|
|
114
|
-
+ "enough to predate the stamp writes none, which is itself the stale-updater case.
|
|
115
|
-
+ "failure to look, never a pass." };
|
|
114
|
+
+ "enough to predate the stamp writes none, which is itself the stale-updater case." };
|
|
116
115
|
}
|
|
117
116
|
|
|
118
117
|
// — AN UNREADABLE SIDE IS NOT A MISMATCH. A digest that could not be taken arrives as null, and
|
|
@@ -128,7 +127,7 @@ export function updaterVerdict({ stamp, now = null, deployClone = null, maxAgeSe
|
|
|
128
127
|
: !wsha ? "the running copy of the updater could NOT be read as a digest"
|
|
129
128
|
: "the master could NOT be read as a digest";
|
|
130
129
|
const why = stamp.masterCommitError ? ` The updater reported: ${stamp.masterCommitError}` : "";
|
|
131
|
-
return { state: "
|
|
130
|
+
return { state: "skip", blocked: true, message:
|
|
132
131
|
`the updater stamp is present but ${which}, so the running copy was NOT `
|
|
133
132
|
+ `compared against its master.${why} This is a failure to look, never a pass.` };
|
|
134
133
|
}
|
|
@@ -176,8 +176,18 @@ export function parseVariantManifestModel(raw) {
|
|
|
176
176
|
// and refuses it. Each says so in its own capability contract, and the compiler declines to build an
|
|
177
177
|
// entry a register cannot express. Deciding it here would freeze one register's behaviour into a
|
|
178
178
|
// rule about every register.
|
|
179
|
-
|
|
179
|
+
// ── AN ABSENT KEY AND AN EMPTY LIST ARE DIFFERENT ANSWERS ───────────────────────────────────────
|
|
180
|
+
//
|
|
181
|
+
// `null` means the stage was asked and did not answer. `[]` means it answered: it considered the
|
|
182
|
+
// goods and there are no words worth narrowing by. They are the same value to a reader who only
|
|
183
|
+
// checks emptiness, and that is how a production run shipped a narrowing that never ran — the model
|
|
184
|
+
// could not send the key at all, the manifest carried an empty list, the compiler minted nothing,
|
|
185
|
+
// and the run looked exactly like a matter that genuinely had no goods words.
|
|
186
|
+
//
|
|
187
|
+
// Nothing downstream may treat the two the same. A skipped question is a fact about the run.
|
|
188
|
+
let goods_words = null;
|
|
180
189
|
if (m.goods_words != null) {
|
|
190
|
+
goods_words = [];
|
|
181
191
|
if (!Array.isArray(m.goods_words) || !m.goods_words.every((w) => typeof w === "string"))
|
|
182
192
|
throw new Error("variantmodel_goods_words_invalid (an array of single-word strings, or omitted)");
|
|
183
193
|
const seen = new Set();
|
package/driver/verify.mjs
CHANGED
|
@@ -16,7 +16,7 @@ import { matterFrameWasRecorded, frameRatifiedForms } from "./matter-frame-recor
|
|
|
16
16
|
import { findConnotationViolations, parsePrRiskResults, prRiskPopulation,
|
|
17
17
|
CONNOTATION_UNMATCHED_MARK, CONNOTATION_NO_RESEMBLANCE_MARK, MEANING_ANGLES_RE,
|
|
18
18
|
parseDispositionForm, CONNOTATION_UNRULED_REASONS, queryKey } from "./connotation-search.mjs";
|
|
19
|
-
import { formSidecarName, formSidecarPath } from "./disposition-union.mjs";
|
|
19
|
+
import { formSidecarName, formSidecarPath } from "./disposition-union.mjs"; import { coverageStatusAsData } from "./common-law-coverage-status.mjs";
|
|
20
20
|
// B — the transport's own four failure states. The audit reads the run's records; this file locates them.
|
|
21
21
|
import { auditDispositionCalls, CALL_FAILURE_REASONS } from "./disposition-call-audit.mjs";
|
|
22
22
|
import { callRecordPaths } from "./disposition-tool.mjs";
|
|
@@ -481,7 +481,7 @@ function commonLawMeaningSeat(p, c) {
|
|
|
481
481
|
// — the EVIDENCE chain for `validators.commonLaw`, lifted out verbatim so the unavailability
|
|
482
482
|
// veto can wrap it instead of pre-empting it. Every return token is unchanged.
|
|
483
483
|
function commonLawEvidence(p, c) {
|
|
484
|
-
const structural = commonLawStructural(c);
|
|
484
|
+
const structural = commonLawStructural(c, p);
|
|
485
485
|
if (!structural.ok) return structural;
|
|
486
486
|
// The dictated grid spec is the SINGLE source of the grid contract when present (the driver wrote it,
|
|
487
487
|
// the plugin ran it, the plugin wrote the ledger from it): the receipts gate joins the ledger against
|
|
@@ -596,7 +596,7 @@ function commonLawHalfEvidence(p, c) {
|
|
|
596
596
|
if (!half) return fail("half_path_unrecognized");
|
|
597
597
|
// — the meaning seat is judged on the meaning work, because that is all it was dictated.
|
|
598
598
|
if (half === MEANING_SEAT) return commonLawMeaningSeat(p, c);
|
|
599
|
-
const structural = commonLawStructural(c);
|
|
599
|
+
const structural = commonLawStructural(c, p);
|
|
600
600
|
if (!structural.ok) return structural;
|
|
601
601
|
const gs = readGridSpecHalf(p, half);
|
|
602
602
|
if (gs.invalid) return fail(`grid_spec_unreadable:_driver/grid-spec.half-${half}.json is corrupt or misshapen (driver-written — this is a bug, not a model defect)`);
|
|
@@ -664,7 +664,7 @@ function commonLawHalfEvidence(p, c) {
|
|
|
664
664
|
// adjacent with exactly one separator between them; it stops rejecting a document that HAS the section
|
|
665
665
|
// under the hyphenated compound the skill itself uses. The failure this removes is a false negative, and
|
|
666
666
|
// its cost was a clearance that had already spent an hour.
|
|
667
|
-
function commonLawStructural(c) {
|
|
667
|
+
function commonLawStructural(c, p = null) {
|
|
668
668
|
return all(
|
|
669
669
|
nonEmpty(c),
|
|
670
670
|
needsSection(c, "findings", [/^#{1,4}\s+[^\n]*\bfindings\b/im], "findings-heading"),
|
|
@@ -672,7 +672,7 @@ function commonLawStructural(c) {
|
|
|
672
672
|
needsSection(c, "coverage-ledger", [/coverage[\s-]ledger/i], "coverage-ledger"),
|
|
673
673
|
needsSection(c, "audit-trail", [/audit[\s-]trail/i], "audit-trail"),
|
|
674
674
|
needs(c, [/\|/], "platform matrix"),
|
|
675
|
-
hasCoverageLedgerRow(c) ? ok() : fail("no_coverage_status_row"),
|
|
675
|
+
coverageStatusAsData(p) || hasCoverageLedgerRow(c) ? ok() : fail("no_coverage_status_row"), // the recorded status first, the word as fallback: coverageStatusAsData() in common-law-coverage-status.mjs
|
|
676
676
|
);
|
|
677
677
|
}
|
|
678
678
|
|
|
@@ -0,0 +1,104 @@
|
|
|
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
|
+
// withheld-families.mjs — the reading turn's record of the waiting families it chose not to ask.
|
|
4
|
+
//
|
|
5
|
+
// On a crowded field the wider register families wait in the plan for the reading turn, which asks the
|
|
6
|
+
// ones worth asking. A family it does not ask was never searched, and until this record nothing said
|
|
7
|
+
// so: the turn's reason, when it gave one, sat in its prose note, where nothing reads it. So the turn
|
|
8
|
+
// records each family it leaves unasked as `withheld-by-judgment` with its reason, the coverage form
|
|
9
|
+
// carries a row per waiting family pre-settled from this record, and a family nobody judged is an
|
|
10
|
+
// unsettled row the digest's gate refuses to pass.
|
|
11
|
+
//
|
|
12
|
+
// The reason is the run's record and the audit workbook's, never the report's: the coverage form keeps
|
|
13
|
+
// these rows out of the ledger the report is built from (coverage-form.mjs, `family`).
|
|
14
|
+
//
|
|
15
|
+
// One file per axis, because the reading turn fans out one seat per axis and two seats writing one file
|
|
16
|
+
// would lose each other's calls. The axis is bound by the server, never taken from the payload.
|
|
17
|
+
import { readFileSync, writeFileSync, renameSync, existsSync, readdirSync, mkdirSync } from "node:fs";
|
|
18
|
+
import { dirname, join } from "node:path";
|
|
19
|
+
import { driverDir } from "../shared/driver-dir.mjs";
|
|
20
|
+
import { awaitsReadingTurn } from "../providers/_shared/plan-guards.mjs";
|
|
21
|
+
import { entryQuestionKey } from "./register-plan.mjs";
|
|
22
|
+
import { seatBannedTokens } from "./coverage-form.mjs";
|
|
23
|
+
|
|
24
|
+
export const WITHHELD_REASON_MAX = 600;
|
|
25
|
+
const FILE_RE = /^withheld-families-(.+)\.json$/;
|
|
26
|
+
|
|
27
|
+
export const withheldFamiliesPath = (runDir, axis) => driverDir(runDir, `withheld-families-${axis}.json`);
|
|
28
|
+
|
|
29
|
+
const readJson = (p) => { try { return existsSync(p) ? JSON.parse(readFileSync(p, "utf8")) : null; } catch { return null; } };
|
|
30
|
+
|
|
31
|
+
/** Every axis's record, merged: `{ [qid]: { axis, reason } }`. An unreadable file contributes nothing. */
|
|
32
|
+
export function readWithheldFamilies(runDir) {
|
|
33
|
+
const out = {};
|
|
34
|
+
let names = [];
|
|
35
|
+
try { names = readdirSync(driverDir(runDir)); } catch { return out; }
|
|
36
|
+
for (const name of names) {
|
|
37
|
+
const m = FILE_RE.exec(name);
|
|
38
|
+
if (!m) continue;
|
|
39
|
+
const doc = readJson(driverDir(runDir, name));
|
|
40
|
+
for (const [qid, v] of Object.entries(doc?.families ?? {})) {
|
|
41
|
+
const reason = String(v?.reason ?? "").trim();
|
|
42
|
+
if (qid && reason) out[qid] = { axis: String(doc?.axis ?? m[1]), reason };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The waiting families on one axis, and which of them the reading turn has asked: a waiting row is asked
|
|
50
|
+
* when a question it did not wait for — a plan entry or one of this axis's supplementals — carries the
|
|
51
|
+
* same question key. PURE over its inputs.
|
|
52
|
+
*/
|
|
53
|
+
export function waitingFamiliesOn(plan, axis, supplementals = []) {
|
|
54
|
+
const entries = Array.isArray(plan?.entries) ? plan.entries : [];
|
|
55
|
+
const askedKeys = new Set([...entries.filter((e) => !awaitsReadingTurn(e?.when)), ...supplementals]
|
|
56
|
+
.filter((e) => e && !e.unsupported).map((e) => entryQuestionKey(e, plan)).filter(Boolean));
|
|
57
|
+
const waiting = entries.filter((e) => e?.axis === axis && awaitsReadingTurn(e?.when));
|
|
58
|
+
return {
|
|
59
|
+
waiting,
|
|
60
|
+
unasked: waiting.filter((e) => !askedKeys.has(entryQuestionKey(e, plan))),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Record families withheld on the bound axis. `families` is `[{ qids: [qid…] | qid, reason }]` — one
|
|
66
|
+
* reason may cover several families. Each qid must be a waiting family of this axis in the frozen plan;
|
|
67
|
+
* a reason must be the reader's words, since the audit workbook prints it. Accepts what is valid, names
|
|
68
|
+
* what it refused, and never throws.
|
|
69
|
+
*/
|
|
70
|
+
export function recordWithheldFamilies(runDir, { axis, families } = {}) {
|
|
71
|
+
const plan = readJson(driverDir(runDir, "register-plan.json"));
|
|
72
|
+
if (!plan || !Array.isArray(plan.entries)) return { refused: "no frozen register plan in this run — there are no waiting families to record against" };
|
|
73
|
+
if (!Array.isArray(families) || !families.length) return { refused: "families must list at least one { qids, reason }" };
|
|
74
|
+
const supp = readJson(join(runDir, "register-units", `${axis}-supplemental-plan.json`));
|
|
75
|
+
const { waiting, unasked } = waitingFamiliesOn(plan, axis, Array.isArray(supp?.entries) ? supp.entries : []);
|
|
76
|
+
const waitingQids = new Set(waiting.map((e) => e.qid));
|
|
77
|
+
const path = withheldFamiliesPath(runDir, axis);
|
|
78
|
+
const doc = readJson(path) ?? { axis, families: {} };
|
|
79
|
+
const recorded = [], rejected = [];
|
|
80
|
+
for (const item of families) {
|
|
81
|
+
const qids = (Array.isArray(item?.qids) ? item.qids : [item?.qids ?? item?.qid]).map((q) => String(q ?? "").trim()).filter(Boolean);
|
|
82
|
+
const reason = String(item?.reason ?? "").replace(/\s+/g, " ").trim();
|
|
83
|
+
if (!qids.length) { rejected.push({ qid: "", issue: "an item names no qid" }); continue; }
|
|
84
|
+
if (!reason) { for (const qid of qids) rejected.push({ qid, issue: "no reason — a withheld family is recorded with why it was not asked" }); continue; }
|
|
85
|
+
if (reason.length > WITHHELD_REASON_MAX) { for (const qid of qids) rejected.push({ qid, issue: `the reason runs to ${reason.length} characters; at most ${WITHHELD_REASON_MAX}` }); continue; }
|
|
86
|
+
const banned = seatBannedTokens(reason);
|
|
87
|
+
if (banned.length) { for (const qid of qids) rejected.push({ qid, issue: `the reason names ${banned.join(", ")} — the audit workbook prints it; say it in a lawyer's words` }); continue; }
|
|
88
|
+
for (const qid of qids) {
|
|
89
|
+
if (!waitingQids.has(qid)) { rejected.push({ qid, issue: `not a waiting family on axis "${axis}" in the frozen plan` }); continue; }
|
|
90
|
+
doc.families[qid] = { reason };
|
|
91
|
+
recorded.push(qid);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (recorded.length) {
|
|
95
|
+
try {
|
|
96
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
97
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
98
|
+
writeFileSync(tmp, JSON.stringify({ axis, families: doc.families }, null, 2) + "\n");
|
|
99
|
+
renameSync(tmp, path);
|
|
100
|
+
} catch (e) { return { write_failed: String(e?.message ?? e).slice(0, 200) }; }
|
|
101
|
+
}
|
|
102
|
+
const still = unasked.filter((e) => !doc.families[e.qid]).map((e) => e.qid);
|
|
103
|
+
return { axis, recorded, rejected, still_to_judge: still, waiting: waiting.length };
|
|
104
|
+
}
|
package/mcp-server/CHANGELOG.md
CHANGED
package/mcp-server/lib/brief.mjs
CHANGED
|
@@ -44,6 +44,11 @@ function plainClause(s) {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
const titleCase = (w) => (w ? w.charAt(0) + w.slice(1).toLowerCase() : w);
|
|
47
|
+
// A band arrives as its word ("High") or as the derived record publish writes for the run's verdict
|
|
48
|
+
// ({ label, rankFromTop, scale }). Stringifying the record printed "Overall risk: [object object]."
|
|
49
|
+
// on the assistant's summary; every band read here goes through this, so either shape gives the word.
|
|
50
|
+
const bandLabel = (b) => (b && typeof b === "object" ? (typeof b.label === "string" && b.label.trim() ? b.label : null)
|
|
51
|
+
: (b == null || String(b).trim() === "" ? null : String(b)));
|
|
47
52
|
|
|
48
53
|
// One clearance finding → one line. NO SECOND FILTER: report-data.json already carries only the live
|
|
49
54
|
// findings (a withdrawn one is not in the file), so the brief's conflicts and the report's findings are
|
|
@@ -51,7 +56,7 @@ const titleCase = (w) => (w ? w.charAt(0) + w.slice(1).toLowerCase() : w);
|
|
|
51
56
|
// shorter list than the document the client was holding.
|
|
52
57
|
function clearanceLine(f) {
|
|
53
58
|
const who = [f.mark, f.owner?.name].filter(Boolean).join(" — ") || "(unnamed finding)";
|
|
54
|
-
const band = f.band ? ` — ${titleCase(
|
|
59
|
+
const band = bandLabel(f.band) ? ` — ${titleCase(bandLabel(f.band))} risk.` : "";
|
|
55
60
|
return `- **${who}**${band}${f.net ? ` ${f.net}` : ""}`.trimEnd();
|
|
56
61
|
}
|
|
57
62
|
|
|
@@ -77,7 +82,8 @@ export function buildBrief(run) {
|
|
|
77
82
|
// THE BAND, AND NEVER THE GATE'S WORD. This chain used to fall through to the delivery verdict — the
|
|
78
83
|
// sidecar's `verdict`, then the run's — so a run whose report reads Medium could be briefed as BLOCKING.
|
|
79
84
|
// The band is what the report shows; where no band is recorded the line is not drawn at all.
|
|
80
|
-
const
|
|
85
|
+
const rating = clearance?.rating ?? clearance?.verdict ?? null; // `verdict` on a report-data file written before the rename
|
|
86
|
+
const overall = bandLabel(rating?.band) ?? rating?.tier
|
|
81
87
|
?? (koDocs.length === 1 ? (koDocs[0].overall ?? null) : null)
|
|
82
88
|
?? fm.overall_label ?? run.tier ?? null;
|
|
83
89
|
|
|
@@ -92,9 +98,8 @@ export function buildBrief(run) {
|
|
|
92
98
|
const tail = [product, dateStr ? `run ${dateStr}` : null].filter(Boolean).join(", ");
|
|
93
99
|
lines.push(`**${subject}**${tail ? ` — ${tail}` : ""}.`);
|
|
94
100
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
: plainClause(fm.overall_caption ?? "");
|
|
101
|
+
// The report's own conclusion, verbatim, after the rating (ruled 2026-09-22: the report is the master).
|
|
102
|
+
const caption = clearance ? (clearance.caption ?? "") : plainClause(fm.overall_caption ?? "");
|
|
98
103
|
if (overall) lines.push(`**Overall risk: ${titleCase(String(overall))}.** ${caption}`.trim());
|
|
99
104
|
if (run.state) {
|
|
100
105
|
// a park is paused, not finished — say so plainly, and name the clock only where one exists
|
|
@@ -104,9 +109,8 @@ export function buildBrief(run) {
|
|
|
104
109
|
: run.state === "recovering" ? ` — auto-recovery backoff, resumes ${run.recoveryResumesAt ? `at ${String(run.recoveryResumesAt).replace("T", " ").slice(0, 16)} UTC` : "on its own"}`
|
|
105
110
|
: run.state === "parked-for-human" ? ` — parked by a runner stop (deploy/restart), resumes on the next runner activation` : "";
|
|
106
111
|
lines.push(`Status: ${run.state}${paused}.`);
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
if (run.statement) lines.push(String(run.statement));
|
|
112
|
+
// No second summary here: the rating and the report's conclusion above are the run's answer. The
|
|
113
|
+
// composed statement said "on hold" on runs nothing held.
|
|
110
114
|
}
|
|
111
115
|
|
|
112
116
|
let source = "none";
|
|
@@ -120,7 +124,7 @@ export function buildBrief(run) {
|
|
|
120
124
|
}
|
|
121
125
|
// Conditions gate a clean result; advisories never do, so only the conditions ride the briefing.
|
|
122
126
|
const conditions = [
|
|
123
|
-
...(
|
|
127
|
+
...(rating?.conditions ?? []),
|
|
124
128
|
...(clearance.actions?.conditions ?? []).map((a) => a?.text),
|
|
125
129
|
].filter(Boolean);
|
|
126
130
|
if (conditions.length) {
|
|
@@ -137,7 +141,7 @@ export function buildBrief(run) {
|
|
|
137
141
|
lines.push("", "**Each name screened:**");
|
|
138
142
|
for (const d of koDocs) {
|
|
139
143
|
for (const m of (d.marks ?? [])) {
|
|
140
|
-
const band = m.band ? `${titleCase(
|
|
144
|
+
const band = bandLabel(m.band) ? `${titleCase(bandLabel(m.band))}${m.qualifier ? ` (${m.qualifier})` : ""}` : "unrated";
|
|
141
145
|
lines.push(`- **${m.name}** — ${band}.${d.url ? ` Report: ${d.url}` : ""}`);
|
|
142
146
|
for (const f of (m.findings ?? [])) {
|
|
143
147
|
const who = [f.name, f.owner].filter(Boolean).join(" — ");
|
|
@@ -151,7 +155,7 @@ export function buildBrief(run) {
|
|
|
151
155
|
// was: a typed conflict already leads with its own band on the report, and widening this to all
|
|
152
156
|
// findings would change what this briefing says about runs that have no register layer at all.
|
|
153
157
|
if (f.shape === "register") {
|
|
154
|
-
const rating = f.band ? ` — ${titleCase(
|
|
158
|
+
const rating = bandLabel(f.band) ? ` — ${titleCase(bandLabel(f.band))} risk.` : "";
|
|
155
159
|
const read = f.basis && f.basis !== f.net ? ` ${f.basis}` : "";
|
|
156
160
|
lines.push(` - ${who}${rating}${f.net ? ` ${f.net}` : ""}${read}`.trimEnd());
|
|
157
161
|
continue;
|
|
@@ -204,7 +208,7 @@ export function buildBrief(run) {
|
|
|
204
208
|
return {
|
|
205
209
|
runId: run.runId, markName: run.markName ?? clearance?.markName ?? fm.title ?? null,
|
|
206
210
|
product,
|
|
207
|
-
overall, tier: run.tier ?? null,
|
|
211
|
+
overall, tier: run.tier ?? null, caption: run.caption ?? null, state: run.state ?? null, date: run.date ?? null,
|
|
208
212
|
source, brief: lines.join("\n"),
|
|
209
213
|
};
|
|
210
214
|
}
|
package/mcp-server/lib/runs.mjs
CHANGED
|
@@ -102,7 +102,7 @@ function runFromStatusFile(statusFile, agent) {
|
|
|
102
102
|
// assistant saw the gate's word and nothing beside it, which is the shape this whole item is about.
|
|
103
103
|
// The verdict record holds the band for those runs, so it is read from there.
|
|
104
104
|
state: s.state ?? null,
|
|
105
|
-
tier: s.tier ?? bandWord(s.verdict) ?? tierFromRecord(runDir),
|
|
105
|
+
tier: s.tier ?? bandWord(s.verdict) ?? tierFromRecord(runDir), caption: s.caption ?? null, url: s.url ?? null, // the report's own conclusion, never a second summary (ruled 2026-09-22)
|
|
106
106
|
markName: s.markName ?? null, ref: s.ref ?? null, classes: s.classes ?? null,
|
|
107
107
|
stepN: s.stepN ?? null, stepLabel: s.stepLabel ?? null, stepTotal: s.stepTotal ?? null,
|
|
108
108
|
failedStage: s.failedStage ?? null, reason: s.reason ?? null,
|
package/mcp-server/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trademark-artifacts-mcp",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3-beta.1",
|
|
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/mcp-server/server.mjs
CHANGED
|
@@ -207,8 +207,9 @@ function runSummary(run) {
|
|
|
207
207
|
// once. null where the registry cannot name it — the row says nothing rather than guessing, because
|
|
208
208
|
// a hardcoded fallback is how a knockout once announced itself as a product it provably was not.
|
|
209
209
|
product: productIdentityFor(run),
|
|
210
|
-
// The band and the
|
|
211
|
-
|
|
210
|
+
// The band and the report's own conclusion; the gate's word stays in the run record (824), and no
|
|
211
|
+
// second summary rides beside them (ruled 2026-09-22).
|
|
212
|
+
state: run.state, location: run.location, tier: run.tier, caption: run.caption, url: run.url,
|
|
212
213
|
markName: run.markName, ref: run.ref, classes: run.classes,
|
|
213
214
|
step: s.stepN ? `${s.stepN}/${s.stepTotal} ${s.stepLabel ?? ""}`.trim() : null,
|
|
214
215
|
startedAt: run.startedAt, updatedAt: run.updatedAt, deliveredAt: run.deliveredAt,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clearotron",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.3-beta.1",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
"assert-census": "node scripts/unexecuted-asserts.mjs"
|
|
67
67
|
},
|
|
68
68
|
"devDependencies": {
|
|
69
|
-
"@changesets/cli": "3.0.
|
|
69
|
+
"@changesets/cli": "3.0.3",
|
|
70
70
|
"@types/node": "^26",
|
|
71
71
|
"acorn": "^8.18.0",
|
|
72
72
|
"eslint": "^10.8.1",
|
|
@@ -12751,7 +12751,18 @@ function Logo({ markOnly = false }) {
|
|
|
12751
12751
|
var ACTIVE_MS = 5e3;
|
|
12752
12752
|
var IDLE_MS = 3e4;
|
|
12753
12753
|
var BACKOFF_MS = 6e4;
|
|
12754
|
-
|
|
12754
|
+
var startedEarly = new Map();
|
|
12755
|
+
function startEarly(key, fetcher) {
|
|
12756
|
+
if (!startedEarly.has(key)) startedEarly.set(key, fetcher());
|
|
12757
|
+
}
|
|
12758
|
+
function claimEarly(key) {
|
|
12759
|
+
if (!key) return null;
|
|
12760
|
+
const p = startedEarly.get(key);
|
|
12761
|
+
if (!p) return null;
|
|
12762
|
+
startedEarly.delete(key);
|
|
12763
|
+
return p;
|
|
12764
|
+
}
|
|
12765
|
+
function useLoad(fetcher, deps, early) {
|
|
12755
12766
|
const [result, setResult] = (0, import_react.useState)(null);
|
|
12756
12767
|
const [loading, setLoading] = (0, import_react.useState)(true);
|
|
12757
12768
|
const [nonce, setNonce] = (0, import_react.useState)(0);
|
|
@@ -12764,7 +12775,7 @@ function useLoad(fetcher, deps) {
|
|
|
12764
12775
|
if (seenKey.current !== null && seenKey.current !== depsKey) setResult(null);
|
|
12765
12776
|
seenKey.current = depsKey;
|
|
12766
12777
|
setLoading(true);
|
|
12767
|
-
fetcherRef.current().then((r) => {
|
|
12778
|
+
(claimEarly(early) ?? fetcherRef.current()).then((r) => {
|
|
12768
12779
|
if (!live) return;
|
|
12769
12780
|
setResult(r);
|
|
12770
12781
|
setLoading(false);
|
|
@@ -15487,7 +15498,7 @@ function lastFinishedOf(row) {
|
|
|
15487
15498
|
}
|
|
15488
15499
|
var COMPACT_FROM = 5;
|
|
15489
15500
|
function Home({ ctx }) {
|
|
15490
|
-
const { result, reload } = useLoad(() => api.runsMine(), []);
|
|
15501
|
+
const { result, reload } = useLoad(() => api.runsMine(), [], "runs:mine");
|
|
15491
15502
|
const allowanceOwner = ctx.owner ?? (ctx.me.accounts.length === 1 ? ctx.me.accounts[0] : null);
|
|
15492
15503
|
const { result: usageRes } = useLoad(() => allowanceOwner ? api.usage(allowanceOwner) : Promise.resolve({ kind: "pickAccount" }), [allowanceOwner]);
|
|
15493
15504
|
const allRuns = result?.kind === "ok" ? result.value : [];
|
|
@@ -16920,7 +16931,7 @@ function Clearances({ ctx }) {
|
|
|
16920
16931
|
const [query, setQuery] = (0, import_react.useState)("");
|
|
16921
16932
|
const [open, setOpen] = (0, import_react.useState)(new Set());
|
|
16922
16933
|
const [page, setPage] = (0, import_react.useState)(0);
|
|
16923
|
-
const { result, reload } = useLoad(() => api.runsMine(), []);
|
|
16934
|
+
const { result, reload } = useLoad(() => api.runsMine(), [], "runs:mine");
|
|
16924
16935
|
const allRuns = result?.kind === "ok" ? result.value : [];
|
|
16925
16936
|
const ownerFilter = ctx.owner;
|
|
16926
16937
|
const runs = (0, import_react.useMemo)(() => ownerFilter ? allRuns.filter((r) => runKey(r) === ownerFilter) : allRuns, [allRuns, ownerFilter]);
|
|
@@ -26744,6 +26755,13 @@ function screen(id, ctx) {
|
|
|
26744
26755
|
});
|
|
26745
26756
|
}
|
|
26746
26757
|
}
|
|
26758
|
+
var EARLY_RUNS_PATHS = [
|
|
26759
|
+
"/portal/home",
|
|
26760
|
+
"/portal/clearances",
|
|
26761
|
+
"/portal"
|
|
26762
|
+
];
|
|
26763
|
+
var here = window.location.pathname.replace(/[?#].*$/, "").replace(/\/+$/, "") || "/portal";
|
|
26764
|
+
if (EARLY_RUNS_PATHS.includes(here)) startEarly("runs:mine", () => api.runsMine());
|
|
26747
26765
|
var root = document.getElementById("root");
|
|
26748
26766
|
if (!root) throw new Error("#root is missing from index.html");
|
|
26749
26767
|
(0, import_client.createRoot)(root).render((0, import_jsx_runtime.jsx)(import_react.StrictMode, { children: (0, import_jsx_runtime.jsx)(AppShell, { render: screen }) }));
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
-->
|
|
50
50
|
<link rel="preconnect" href="https://api.fontshare.com" crossorigin />
|
|
51
51
|
<link href="https://api.fontshare.com/v2/css?f[]=satoshi@400,500,700,900&display=swap" rel="stylesheet" />
|
|
52
|
-
<script type="module" crossorigin src="/portal/assets/index-
|
|
52
|
+
<script type="module" crossorigin src="/portal/assets/index-GBbbyQxc.js"></script>
|
|
53
53
|
<link rel="stylesheet" crossorigin href="/portal/assets/index-5CCwiJG7.css">
|
|
54
54
|
</head>
|
|
55
55
|
<body>
|
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.3.
|
|
5
|
+
"version": "0.3.3-beta.1",
|
|
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": {
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
//
|
|
14
14
|
// SEAM (capabilities.countProbe) — WHERE the number comes from. Same three values enumerate.mjs
|
|
15
15
|
// documents, same meanings:
|
|
16
|
-
// "endpoint" Clarivate. A real POST /count: cheap, works at ANY magnitude
|
|
17
|
-
//
|
|
16
|
+
// "endpoint" Clarivate. A real POST /count: cheap, works at ANY magnitude, fetches nothing. A true
|
|
17
|
+
// count-only call.
|
|
18
18
|
// "cheap" Corsearch. The count rides page 0 of a normal search (totalHitCount) — so it is one
|
|
19
19
|
// BILLABLE search with `limit:1 fields:["uri"]`, the smallest response the API will give.
|
|
20
20
|
// Cheap, not free: every count here is a metered call.
|