clearotron 0.3.0-beta.2 → 0.3.0-beta.3
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/INSTALL.md +19 -0
- package/bin/onboard.mjs +65 -6
- package/bin/start.mjs +7 -1
- package/build-info.json +2 -2
- package/driver/CHANGELOG.md +22 -0
- package/driver/driver.config.mjs +5 -0
- package/driver/engine/anthropic-agent.mjs +34 -17
- package/driver/gateway.mjs +76 -15
- package/driver/package.json +1 -1
- package/driver/portal-service.mjs +1 -1
- package/driver/profile-service.mjs +31 -3
- package/driver/publish/index.mjs +12 -9
- package/driver/publish/knockout.mjs +4 -2
- package/driver/publish/office-record-links.mjs +56 -21
- package/driver/recipe-service.mjs +14 -5
- package/driver/record-origins.mjs +14 -0
- package/driver/suite-census.json +28 -22
- package/mcp-server/CHANGELOG.md +4 -0
- package/mcp-server/lib/driver.mjs +2 -0
- package/mcp-server/lib/knockout.mjs +2 -2
- package/mcp-server/lib/options.mjs +9 -2
- package/mcp-server/package.json +1 -1
- package/package.json +1 -1
- package/portal-ui/dist/assets/{index-CcFjgM78.js → index-C1gpdkyH.js} +17 -8
- package/portal-ui/dist/index.html +1 -1
- package/portal-ui/package.json +1 -1
- package/providers/clarivate/src/core.js +5 -0
- package/providers/oauth-mcp-bridge/CHANGELOG.md +4 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/scripts/e2e-unread-terminals.mjs +1 -1
- package/scripts/e2e.mjs +114 -9
- package/scripts/revisit-render-check.mjs +22 -4
- package/scripts/travelling-predicates.mjs +1 -1
- package/shared/store-in-repo.mjs +50 -2
|
@@ -67,9 +67,13 @@ export const OFFICE_RECORD_PAGES = Object.freeze({
|
|
|
67
67
|
fr: { name: "France", address: (r) => addressFrom([r.applicationNumber, r.registrationNumber], /^(?:FR)?(\d{7})$/i, (m) => ({
|
|
68
68
|
number: `FR${m[1]}`, href: `https://data.inpi.fr/marques/FR${m[1]}` })) },
|
|
69
69
|
// The office's record address as indexed; it challenges automated fetches, so none was opened from a
|
|
70
|
-
// server. UK numbers are UK plus eleven digits (UK000..., UK008..., UK009...).
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
// server. UK numbers are UK plus eleven digits (UK000..., UK008..., UK009...). A national number given
|
|
71
|
+
// bare, seven digits as one register hands it over, is the same number under the office's UK0000
|
|
72
|
+
// prefix: UK00003456789 is national mark 3456789.
|
|
73
|
+
gb: { name: "United Kingdom", address: (r) => addressFrom([r.applicationNumber, r.registrationNumber], /^(?:(UK\d{11})|(\d{7}))$/i, (m) => {
|
|
74
|
+
const n = m[1] ? m[1].toUpperCase() : `UK0000${m[2]}`;
|
|
75
|
+
return { number: n, href: `https://trademarks.ipo.gov.uk/ipo-tmcase/page/Results/1/${n}` };
|
|
76
|
+
}) },
|
|
73
77
|
// The register's own search opens records at exactly this address: the application number, then the
|
|
74
78
|
// registration number when there is one. Opened a real record. The older search.patentstyret.no
|
|
75
79
|
// addresses now answer 410 Gone.
|
|
@@ -91,6 +95,12 @@ export const OFFICE_RECORD_PAGES = Object.freeze({
|
|
|
91
95
|
|
|
92
96
|
const handleOffice = (uri) => (/^\/mark\/([a-z]{2,4})\//i.exec(clean(uri))?.[1] ?? "").toLowerCase();
|
|
93
97
|
|
|
98
|
+
// The same office under another register's code. The EUIPO's WIPO ST.3 code is EM, and a register that
|
|
99
|
+
// keeps it (Compumark does) names the office the table holds as EU. An alias, never a second table entry:
|
|
100
|
+
// the table is keyed by the offices, and one office has one address.
|
|
101
|
+
const OFFICE_ALIASES = Object.freeze({ em: "eu" });
|
|
102
|
+
const officeOf = (code) => OFFICE_ALIASES[code] ?? code;
|
|
103
|
+
|
|
94
104
|
// A number that already carries its office's letters (FR4123456, UK00003456789) stands alone; a bare
|
|
95
105
|
// one is shown with its office (CH 08133/2025), so the label always says whose number it is.
|
|
96
106
|
const labelOf = (office, number) => (number ? (/^[a-z]{2}/i.test(number) ? number : `${office.toUpperCase()} ${number}`) : null);
|
|
@@ -108,7 +118,7 @@ const labelOf = (office, number) => (number ? (/^[a-z]{2}/i.test(number) ? numbe
|
|
|
108
118
|
export function officeRecordLink(rec, uri = "") {
|
|
109
119
|
if (!rec || typeof rec !== "object") return null;
|
|
110
120
|
const designation = clean(rec.filingRoute) === "madrid_designation";
|
|
111
|
-
const office = designation ? "wo" : (clean(rec.office).toLowerCase() || handleOffice(uri));
|
|
121
|
+
const office = designation ? "wo" : officeOf(clean(rec.office).toLowerCase() || handleOffice(uri));
|
|
112
122
|
const page = OFFICE_RECORD_PAGES[office];
|
|
113
123
|
const own = designation ? clean(rec.irNumber) : clean(rec.applicationNumber) || clean(rec.registrationNumber);
|
|
114
124
|
const unlinked = (reason) => ({ office, label: labelOf(office, own), href: null, reason });
|
|
@@ -118,17 +128,28 @@ export function officeRecordLink(rec, uri = "") {
|
|
|
118
128
|
return a ? { office, label: labelOf(office, a.number), href: a.href, reason: null } : unlinked("unaddressable");
|
|
119
129
|
}
|
|
120
130
|
|
|
121
|
-
/**
|
|
122
|
-
|
|
131
|
+
/**
|
|
132
|
+
* Does the run's register publish no page per record, so that the office's page is the only link a reader
|
|
133
|
+
* can be given? `origins` is `recordOriginsFor`'s answer for the run's register: the hosts it may put in a
|
|
134
|
+
* record URL. An EMPTY list is that answer, and it is the same one normalizeRecordLinks enforces, which
|
|
135
|
+
* strips every record URL on such a register, so none of its records reaches publish with a link of its
|
|
136
|
+
* own. Null means no register was named, and nothing is addressed.
|
|
137
|
+
*
|
|
138
|
+
* DECIDED BY THE REGISTER'S DECLARATION, NEVER BY A LIST OF VENDOR NAMES. A list here once named one
|
|
139
|
+
* vendor, on the premise that every other register's records carry links of their own. Compumark's do
|
|
140
|
+
* not, and its runs reached the reader with a handle and no reason, while this code knew the address.
|
|
141
|
+
*/
|
|
142
|
+
const publishesNoRecordPages = (origins) => Array.isArray(origins) && origins.length === 0;
|
|
123
143
|
|
|
124
144
|
/**
|
|
125
|
-
* Every registration the findings cite, keyed by its lower-cased uri, for a run whose register
|
|
126
|
-
* `
|
|
127
|
-
*
|
|
128
|
-
* never fit shows up as a count instead of as a report that quietly looks
|
|
145
|
+
* Every registration the findings cite, keyed by its lower-cased uri, for a run whose register may put a
|
|
146
|
+
* record URL on `origins` (see publishesNoRecordPages). Returns null for a register that publishes pages
|
|
147
|
+
* of its own, so those runs render exactly as before. `tally` is what publish records, per office, so
|
|
148
|
+
* that a register whose numbers never fit shows up as a count instead of as a report that quietly looks
|
|
149
|
+
* the way it always did.
|
|
129
150
|
*/
|
|
130
|
-
export function recordLinksFor(findings, recordsByUri,
|
|
131
|
-
if (!
|
|
151
|
+
export function recordLinksFor(findings, recordsByUri, origins) {
|
|
152
|
+
if (!publishesNoRecordPages(origins)) return null;
|
|
132
153
|
const byUri = new Map();
|
|
133
154
|
const tally = { linked: {}, cited: {}, notRetrieved: 0 };
|
|
134
155
|
for (const f of Array.isArray(findings) ? findings : []) {
|
|
@@ -169,17 +190,30 @@ const nameOf = (office) => OFFICE_RECORD_PAGES[office]?.name ?? office.toUpperCa
|
|
|
169
190
|
* NO COUNT. The cards list only some findings' registrations (an off-field finding lists none), so a
|
|
170
191
|
* number here could name more registrations than the reader can find on the page. The workbook lists
|
|
171
192
|
* them all, each with its reason.
|
|
193
|
+
*
|
|
194
|
+
* ONE SENTENCE FOR EVERY OFFICE THE TABLE HOLDS NO ADDRESS FOR, after the per-office ones. A register
|
|
195
|
+
* covering the world cites filings from offices this table does not know: one Compumark knockout on the
|
|
196
|
+
* test instance listed sixteen of them. A sentence each would repeat one caveat sixteen times, which is
|
|
197
|
+
* the repeated note the owner ruled reads to a client as a broken report.
|
|
172
198
|
*/
|
|
173
199
|
export function officeReasonSentences(byUri) {
|
|
174
200
|
const keys = new Set();
|
|
175
|
-
|
|
176
|
-
|
|
201
|
+
const unknown = new Set();
|
|
202
|
+
for (const l of byUri instanceof Map ? byUri.values() : []) {
|
|
203
|
+
if (!l || l.href) continue;
|
|
204
|
+
if (l.reason === "unknown-office") unknown.add(l.office);
|
|
205
|
+
else keys.add(`${l.office} ${l.reason}`);
|
|
206
|
+
}
|
|
207
|
+
const each = [...keys].sort().map((k) => {
|
|
177
208
|
const [office, reason] = k.split(" ");
|
|
178
209
|
const name = nameOf(office);
|
|
179
210
|
if (reason === "no-page") return `${name}: the register publishes no page for a single record, so its registrations are cited by number.`;
|
|
180
|
-
if (reason === "unknown-office") return `${name}: we hold no page address for this register, so its registrations are cited by number.`;
|
|
181
211
|
return `${name}: registrations whose numbers are not in the form the register's page address takes are cited by number, not linked.`;
|
|
182
212
|
});
|
|
213
|
+
const codes = [...unknown].sort().map(nameOf);
|
|
214
|
+
if (codes.length === 1) each.push(`${codes[0]}: we hold no page address for this register, so its registrations are cited by number.`);
|
|
215
|
+
else if (codes.length > 1) each.push(`${codes.join(", ")}: we hold no page address for these registers, so their registrations are cited by number.`);
|
|
216
|
+
return each;
|
|
183
217
|
}
|
|
184
218
|
|
|
185
219
|
/**
|
|
@@ -215,20 +249,21 @@ export function linkCellFor(registrations, byUri) {
|
|
|
215
249
|
|
|
216
250
|
/**
|
|
217
251
|
* A knockout's listed filings, each addressed at its office's page, for a listing taken on a register
|
|
218
|
-
* with no record pages of its own. `doc` is register-records.json
|
|
219
|
-
*
|
|
220
|
-
*
|
|
221
|
-
*
|
|
252
|
+
* with no record pages of its own. `doc` is register-records.json and `origins` is `recordOriginsFor`'s
|
|
253
|
+
* answer for the register the sidecar names (see publishesNoRecordPages). Each filing that carries an
|
|
254
|
+
* office number gains an `officeLink` in place, as normalizeRegisterRecordLinks rewrites links in place, so
|
|
255
|
+
* the report, the workbook and report-data.json all state the same link. Returns null for a register that
|
|
256
|
+
* publishes pages of its own, whose filings render exactly as before.
|
|
222
257
|
*
|
|
223
258
|
* A FILING WITH NO OFFICE NUMBER IS LEFT AS IT WAS, and shows as it always did. Every listing taken
|
|
224
259
|
* before the numbers were kept is that case, so an archived knockout republishes unchanged. An
|
|
225
260
|
* `officeLink` found already on the sidecar is dropped first: the link is set here, from the numbers, and
|
|
226
261
|
* nowhere else.
|
|
227
262
|
*/
|
|
228
|
-
export function addressListedFilings(doc) {
|
|
263
|
+
export function addressListedFilings(doc, origins) {
|
|
229
264
|
const records = (doc?.marks ?? []).flatMap((m) => m?.records ?? []).filter((r) => r && typeof r === "object");
|
|
230
265
|
for (const r of records) delete r.officeLink;
|
|
231
|
-
if (!
|
|
266
|
+
if (!publishesNoRecordPages(origins)) return null;
|
|
232
267
|
const tally = { linked: {}, cited: {}, noNumber: 0 };
|
|
233
268
|
for (const r of records) {
|
|
234
269
|
const link = officeRecordLink(r, r.recordId);
|
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
// server-owned (monotonic per save); EVERY write re-runs the SAME load-time validator the driver uses
|
|
25
25
|
// (search-policy.mjs validateRecipe), so the UI can never persist a recipe the driver would later reject;
|
|
26
26
|
// free text gets the profiles.mjs anti-rule prose guards (a recipe must never smuggle a rating rule in as
|
|
27
|
-
// prose — levels select machinery, never rating authority); a recipe belongs to a
|
|
28
|
-
// profile roster
|
|
27
|
+
// prose — levels select machinery, never rating authority); a recipe belongs to a company on the
|
|
28
|
+
// profile roster, Generic included. The routing core (`makeRecipeService`) is fs-only + injected
|
|
29
29
|
// git/audit so it unit-tests offline; auth + rate-limit are wired in the bootstrap.
|
|
30
30
|
|
|
31
31
|
import "../shared/env-local.mjs"; // — FIRST: the CLEAROTRON_* translation must land before any
|
|
@@ -61,6 +61,13 @@ export const componentCatalog = () =>
|
|
|
61
61
|
export function makeRecipeService({
|
|
62
62
|
recipesDir,
|
|
63
63
|
profileDir = undefined, // undefined ⇒ profiles.mjs default dir
|
|
64
|
+
// THE ROSTER THE PORTAL SHOWS, read the way profile-service reads it (its `readLayered` note). The
|
|
65
|
+
// portal passes true: the deployment's store, overlay over base, with `generic` falling through from
|
|
66
|
+
// the product. Read as an explicit directory instead, a store holding no `generic.json` — every fresh
|
|
67
|
+
// install and every demo — throws "generic.json is REQUIRED", the catch below turned that into "no
|
|
68
|
+
// such company", and saved searches answered 404 for every company on the install, one created a
|
|
69
|
+
// minute earlier included, while the same company's projects answered 200.
|
|
70
|
+
readLayered = false,
|
|
64
71
|
loadRecipes = loadRecipesDefault,
|
|
65
72
|
loadProfiles = loadProfilesDefault,
|
|
66
73
|
writeRecipe = defaultWriteRecipe,
|
|
@@ -69,8 +76,10 @@ export function makeRecipeService({
|
|
|
69
76
|
} = {}) {
|
|
70
77
|
const rosterHas = (customer) => {
|
|
71
78
|
try {
|
|
72
|
-
const profiles = profileDir === undefined ? loadProfiles({ force: true }) : loadProfiles({ dir: profileDir, force: true });
|
|
73
|
-
|
|
79
|
+
const profiles = readLayered || profileDir === undefined ? loadProfiles({ force: true }) : loadProfiles({ dir: profileDir, force: true });
|
|
80
|
+
// GENERIC OWNS SAVED SEARCHES LIKE ANY COMPANY (owner ruling, 2026-09-11). It was refused here as not a
|
|
81
|
+
// real customer, and a fresh install, which holds Generic alone, opened Custom searches on an error.
|
|
82
|
+
return profiles.has(customer);
|
|
74
83
|
} catch { return false; } // an unreadable roster fails CLOSED here — writes need a verifiable owner
|
|
75
84
|
};
|
|
76
85
|
const listRow = ([key, r]) => {
|
|
@@ -122,7 +131,7 @@ export function makeRecipeService({
|
|
|
122
131
|
return { status: 404, json: { error: "not_found" } };
|
|
123
132
|
if (method !== "POST") return { status: 405, json: { error: "method_not_allowed" } };
|
|
124
133
|
if (!rosterHas(customer))
|
|
125
|
-
return { status: 400, json: { error: `customer "${customer}" is not on the profile roster — a saved search belongs to a
|
|
134
|
+
return { status: 400, json: { error: `customer "${customer}" is not on the profile roster — a saved search belongs to a company on the roster (create the company first)` } };
|
|
126
135
|
|
|
127
136
|
const incoming = body.recipe;
|
|
128
137
|
if (!incoming || typeof incoming !== "object" || Array.isArray(incoming))
|
|
@@ -43,3 +43,17 @@ export function activeRecordOrigins(env = process.env) {
|
|
|
43
43
|
const id = String(env?.CLEAROTRON_DATABASE ?? "").trim().toLowerCase();
|
|
44
44
|
return id ? recordOriginsFor(id) : null;
|
|
45
45
|
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The record origins of the register a stored artefact NAMES — a knockout's listing, a run's fetch
|
|
49
|
+
* receipts — or null when it names none.
|
|
50
|
+
*
|
|
51
|
+
* EMPTY IS "NOBODY SAID", and it turns the gate off, as normalizeRegisterRecordLinks reads the same
|
|
52
|
+
* field: an artefact that never recorded its register cannot be judged against one. An id this table
|
|
53
|
+
* does not know is an answer, and resolves as recordOriginsFor resolves it, to no origins, because no
|
|
54
|
+
* host of its can be established as legitimate.
|
|
55
|
+
*/
|
|
56
|
+
export function declaredRecordOrigins(provider) {
|
|
57
|
+
const id = String(provider ?? "").trim().toLowerCase();
|
|
58
|
+
return id ? recordOriginsFor(id) : null;
|
|
59
|
+
}
|
package/driver/suite-census.json
CHANGED
|
@@ -148,8 +148,8 @@
|
|
|
148
148
|
"todos": 0
|
|
149
149
|
},
|
|
150
150
|
"a-company-created-in-the-browser-is-a-whole-company.test.mjs": {
|
|
151
|
-
"tests":
|
|
152
|
-
"asserts":
|
|
151
|
+
"tests": 11,
|
|
152
|
+
"asserts": 61,
|
|
153
153
|
"skips": 0,
|
|
154
154
|
"todos": 0
|
|
155
155
|
},
|
|
@@ -370,8 +370,8 @@
|
|
|
370
370
|
"todos": 0
|
|
371
371
|
},
|
|
372
372
|
"a-knockout-filing-links-to-its-office.test.mjs": {
|
|
373
|
-
"tests":
|
|
374
|
-
"asserts":
|
|
373
|
+
"tests": 15,
|
|
374
|
+
"asserts": 76,
|
|
375
375
|
"skips": 0,
|
|
376
376
|
"todos": 0
|
|
377
377
|
},
|
|
@@ -1804,8 +1804,8 @@
|
|
|
1804
1804
|
"todos": 0
|
|
1805
1805
|
},
|
|
1806
1806
|
"doctor-reads-the-units-not-the-shell.test.mjs": {
|
|
1807
|
-
"tests":
|
|
1808
|
-
"asserts":
|
|
1807
|
+
"tests": 30,
|
|
1808
|
+
"asserts": 85,
|
|
1809
1809
|
"skips": 0,
|
|
1810
1810
|
"todos": 0
|
|
1811
1811
|
},
|
|
@@ -2026,8 +2026,8 @@
|
|
|
2026
2026
|
"todos": 0
|
|
2027
2027
|
},
|
|
2028
2028
|
"engine.anthropic.test.mjs": {
|
|
2029
|
-
"tests":
|
|
2030
|
-
"asserts":
|
|
2029
|
+
"tests": 61,
|
|
2030
|
+
"asserts": 222,
|
|
2031
2031
|
"skips": 0,
|
|
2032
2032
|
"todos": 0
|
|
2033
2033
|
},
|
|
@@ -2188,8 +2188,8 @@
|
|
|
2188
2188
|
"todos": 0
|
|
2189
2189
|
},
|
|
2190
2190
|
"exit1-artifact-rescue.test.mjs": {
|
|
2191
|
-
"tests":
|
|
2192
|
-
"asserts":
|
|
2191
|
+
"tests": 25,
|
|
2192
|
+
"asserts": 101,
|
|
2193
2193
|
"skips": 0,
|
|
2194
2194
|
"todos": 0
|
|
2195
2195
|
},
|
|
@@ -3058,8 +3058,8 @@
|
|
|
3058
3058
|
"todos": 0
|
|
3059
3059
|
},
|
|
3060
3060
|
"portal-service.test.mjs": {
|
|
3061
|
-
"tests":
|
|
3062
|
-
"asserts":
|
|
3061
|
+
"tests": 123,
|
|
3062
|
+
"asserts": 662,
|
|
3063
3063
|
"skips": 0,
|
|
3064
3064
|
"todos": 0
|
|
3065
3065
|
},
|
|
@@ -3322,8 +3322,8 @@
|
|
|
3322
3322
|
"todos": 0
|
|
3323
3323
|
},
|
|
3324
3324
|
"recipe-service.test.mjs": {
|
|
3325
|
-
"tests":
|
|
3326
|
-
"asserts":
|
|
3325
|
+
"tests": 16,
|
|
3326
|
+
"asserts": 77,
|
|
3327
3327
|
"skips": 0,
|
|
3328
3328
|
"todos": 0
|
|
3329
3329
|
},
|
|
@@ -3478,8 +3478,8 @@
|
|
|
3478
3478
|
"todos": 0
|
|
3479
3479
|
},
|
|
3480
3480
|
"register-findings-link-to-the-office-record.test.mjs": {
|
|
3481
|
-
"tests":
|
|
3482
|
-
"asserts":
|
|
3481
|
+
"tests": 19,
|
|
3482
|
+
"asserts": 61,
|
|
3483
3483
|
"skips": 0,
|
|
3484
3484
|
"todos": 0
|
|
3485
3485
|
},
|
|
@@ -3665,7 +3665,7 @@
|
|
|
3665
3665
|
},
|
|
3666
3666
|
"report-record-link-host.test.mjs": {
|
|
3667
3667
|
"tests": 3,
|
|
3668
|
-
"asserts":
|
|
3668
|
+
"asserts": 27,
|
|
3669
3669
|
"skips": 0,
|
|
3670
3670
|
"todos": 0
|
|
3671
3671
|
},
|
|
@@ -4479,6 +4479,12 @@
|
|
|
4479
4479
|
"skips": 0,
|
|
4480
4480
|
"todos": 0
|
|
4481
4481
|
},
|
|
4482
|
+
"the-doors-comparison-knows-an-admit-case.test.mjs": {
|
|
4483
|
+
"tests": 7,
|
|
4484
|
+
"asserts": 20,
|
|
4485
|
+
"skips": 0,
|
|
4486
|
+
"todos": 0
|
|
4487
|
+
},
|
|
4482
4488
|
"the-doubt-carries-its-placement-tier.test.mjs": {
|
|
4483
4489
|
"tests": 17,
|
|
4484
4490
|
"asserts": 56,
|
|
@@ -4858,8 +4864,8 @@
|
|
|
4858
4864
|
"todos": 0
|
|
4859
4865
|
},
|
|
4860
4866
|
"the-settings-surface-reads-the-customer-store.test.mjs": {
|
|
4861
|
-
"tests":
|
|
4862
|
-
"asserts":
|
|
4867
|
+
"tests": 10,
|
|
4868
|
+
"asserts": 43,
|
|
4863
4869
|
"skips": 0,
|
|
4864
4870
|
"todos": 0
|
|
4865
4871
|
},
|
|
@@ -5397,8 +5403,8 @@
|
|
|
5397
5403
|
"todos": 0
|
|
5398
5404
|
},
|
|
5399
5405
|
"describe-options.test.mjs": {
|
|
5400
|
-
"tests":
|
|
5401
|
-
"asserts":
|
|
5406
|
+
"tests": 23,
|
|
5407
|
+
"asserts": 98,
|
|
5402
5408
|
"skips": 0,
|
|
5403
5409
|
"todos": 0
|
|
5404
5410
|
},
|
|
@@ -5757,7 +5763,7 @@
|
|
|
5757
5763
|
},
|
|
5758
5764
|
"nav.test.ts": {
|
|
5759
5765
|
"tests": 12,
|
|
5760
|
-
"asserts":
|
|
5766
|
+
"asserts": 57,
|
|
5761
5767
|
"skips": 0,
|
|
5762
5768
|
"todos": 0
|
|
5763
5769
|
},
|
package/mcp-server/CHANGELOG.md
CHANGED
|
@@ -26,6 +26,8 @@ export { koPaths } from "../../driver/stages-knockout.mjs";
|
|
|
26
26
|
// this same function rather than from a second reading of the numbers. office-record-links.mjs imports
|
|
27
27
|
// nothing, so the read-only server stays light.
|
|
28
28
|
export { addressListedFilings } from "../../driver/publish/office-record-links.mjs";
|
|
29
|
+
// Which registers publish no page per record, as publish decides it: the sidecar's register, looked up.
|
|
30
|
+
export { declaredRecordOrigins } from "../../driver/record-origins.mjs";
|
|
29
31
|
|
|
30
32
|
// stripInternal/stripEngineInternals/stripTelemetry are the driver's OWN client-safety transforms — the
|
|
31
33
|
// same ones publish/render.mjs applies to the client HTML export. lib/scrub.mjs composes them so the MCP
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
28
28
|
import { basename, join } from "node:path";
|
|
29
29
|
|
|
30
|
-
import { koPaths, addressListedFilings } from "./driver.mjs";
|
|
30
|
+
import { koPaths, addressListedFilings, declaredRecordOrigins } from "./driver.mjs";
|
|
31
31
|
import { driverDir } from "../../shared/driver-dir.mjs";
|
|
32
32
|
|
|
33
33
|
const readJson = (p) => { try { return JSON.parse(readFileSync(p, "utf8")); } catch { return null; } };
|
|
@@ -213,7 +213,7 @@ export function knockoutEvidence(run) {
|
|
|
213
213
|
// office's own page, or the office and the number and why that is not a link. This view states the same,
|
|
214
214
|
// from the same function over the same sidecar, so the connector and the report cannot give one filing
|
|
215
215
|
// two links. On any other register the call sets nothing, and every row is exactly as it was.
|
|
216
|
-
if (Array.isArray(recordsDoc?.marks)) addressListedFilings(recordsDoc);
|
|
216
|
+
if (Array.isArray(recordsDoc?.marks)) addressListedFilings(recordsDoc, declaredRecordOrigins(recordsDoc?.provider));
|
|
217
217
|
const out = [];
|
|
218
218
|
|
|
219
219
|
for (const entry of (Array.isArray(recordsDoc?.marks) ? recordsDoc.marks : [])) {
|
|
@@ -238,11 +238,18 @@ function accountFor(key, { scope, now }) {
|
|
|
238
238
|
* wherever it resolves — so there is no shut state left to probe, and the invitation cannot disagree with
|
|
239
239
|
* the enforcement because neither consults an environment.
|
|
240
240
|
*
|
|
241
|
-
* An account that simply has no saved searches gets an empty list and no note.
|
|
241
|
+
* An account that simply has no saved searches gets an empty list and no note. A store that cannot be
|
|
242
|
+
* READ gets the empty list AND a note, because those are different facts, and an assistant told "none"
|
|
243
|
+
* repeats it to the client as the answer. The note names no variable and no path: its reader may be a
|
|
244
|
+
* client, and the reason belongs to whoever runs the installation, whose `clearotron doctor` names it.
|
|
242
245
|
*/
|
|
243
246
|
function savedSearchesFor(key) {
|
|
244
247
|
let recipes;
|
|
245
|
-
try { recipes = loadRecipes({ force: true, proseGuard: recipeProseGuard }); }
|
|
248
|
+
try { recipes = loadRecipes({ force: true, proseGuard: recipeProseGuard }); }
|
|
249
|
+
catch {
|
|
250
|
+
return { savedSearches: [], savedSearchesNote: "Saved searches could not be read on this installation, so none are "
|
|
251
|
+
+ "listed. That is not the same as having none, and the people who run the installation can see why." };
|
|
252
|
+
}
|
|
246
253
|
return {
|
|
247
254
|
savedSearches: [...recipes.entries()]
|
|
248
255
|
.filter(([k, r]) => k.startsWith(`${key}/`) && !r.archived)
|
package/mcp-server/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trademark-artifacts-mcp",
|
|
3
|
-
"version": "0.3.0-beta.
|
|
3
|
+
"version": "0.3.0-beta.3",
|
|
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
|
@@ -11018,14 +11018,14 @@ var NAV = [
|
|
|
11018
11018
|
label: "Admin settings",
|
|
11019
11019
|
path: "/portal/admin",
|
|
11020
11020
|
icon: "settings",
|
|
11021
|
-
needs: "
|
|
11021
|
+
needs: "everything",
|
|
11022
11022
|
hidden: true,
|
|
11023
11023
|
children: [{
|
|
11024
11024
|
id: "admin.config",
|
|
11025
11025
|
label: "Global config",
|
|
11026
11026
|
path: "/portal/admin/config",
|
|
11027
11027
|
icon: "server",
|
|
11028
|
-
needs: "
|
|
11028
|
+
needs: "everything"
|
|
11029
11029
|
}]
|
|
11030
11030
|
},
|
|
11031
11031
|
{
|
|
@@ -11036,7 +11036,8 @@ var NAV = [
|
|
|
11036
11036
|
hidden: true
|
|
11037
11037
|
}
|
|
11038
11038
|
];
|
|
11039
|
-
var
|
|
11039
|
+
var holds = (who, gate) => gate === "everything" ? who.allAccounts === true : who.permissions[gate];
|
|
11040
|
+
var visible = (e, who) => !e.needs || holds(who, e.needs);
|
|
11040
11041
|
function navFor(who, entries = NAV) {
|
|
11041
11042
|
return entries.filter((e) => visible(e, who) && !e.hidden).map((e) => e.children ? {
|
|
11042
11043
|
...e,
|
|
@@ -12258,6 +12259,9 @@ function canManage(who) {
|
|
|
12258
12259
|
function canRun(who) {
|
|
12259
12260
|
return who.permissions.run;
|
|
12260
12261
|
}
|
|
12262
|
+
function seesEverything(who) {
|
|
12263
|
+
return who.allAccounts === true;
|
|
12264
|
+
}
|
|
12261
12265
|
//#endregion
|
|
12262
12266
|
//#region src/screens/Home.tsx
|
|
12263
12267
|
var TERMINAL$1 = new Set([
|
|
@@ -13016,11 +13020,15 @@ function Clearances({ ctx }) {
|
|
|
13016
13020
|
const wanted = new URLSearchParams(window.location.search).get("owner");
|
|
13017
13021
|
if (wanted && wanted !== ctx.owner) ctx.setOwner(wanted);
|
|
13018
13022
|
}, [ctx]);
|
|
13019
|
-
const
|
|
13020
|
-
const
|
|
13023
|
+
const seesAll = ctx.me.allAccounts;
|
|
13024
|
+
const account = ownerFilter ?? (seesAll ? "*" : ctx.me.accounts.length === 1 ? ctx.me.accounts[0] : null);
|
|
13025
|
+
const { result: famResult, reload: reloadFamilies } = useLoad(() => seesAll ? api.families(account) : Promise.resolve({
|
|
13026
|
+
kind: "ok",
|
|
13027
|
+
value: NO_FAMILIES
|
|
13028
|
+
}), [account, seesAll]);
|
|
13021
13029
|
const families = famResult?.kind === "ok" ? famResult.value : NO_FAMILIES;
|
|
13022
13030
|
const [picked, setPicked] = (0, import_react.useState)(new Set());
|
|
13023
|
-
const canGroup = canManage(ctx.me);
|
|
13031
|
+
const canGroup = canManage(ctx.me) && seesAll;
|
|
13024
13032
|
const [showRetired, setShowRetired] = (0, import_react.useState)(false);
|
|
13025
13033
|
const { result: retiredResult, reload: reloadRetired } = useLoad(() => canGroup ? api.retiredRuns(account) : Promise.resolve({ kind: "notFound" }), [account, canGroup]);
|
|
13026
13034
|
const retired = retiredResult?.kind === "ok" ? retiredResult.value : [];
|
|
@@ -17392,7 +17400,7 @@ function NewClearance({ ctx }) {
|
|
|
17392
17400
|
demoMode: ctx.me.engineMode === "demo",
|
|
17393
17401
|
setupRoute: ctx.me.setupRoute,
|
|
17394
17402
|
programDisputed: ctx.me.engineProgramDisputed,
|
|
17395
|
-
onSettings:
|
|
17403
|
+
onSettings: seesEverything(ctx.me) ? () => ctx.go("/portal/admin/config") : null,
|
|
17396
17404
|
saveOpen,
|
|
17397
17405
|
saveName,
|
|
17398
17406
|
saveText,
|
|
@@ -22124,7 +22132,8 @@ function useSourceRepo() {
|
|
|
22124
22132
|
function PeopleAccess({ ctx }) {
|
|
22125
22133
|
const { result } = useLoad(() => api.adminAccess(), []);
|
|
22126
22134
|
const repo = useSourceRepo();
|
|
22127
|
-
const
|
|
22135
|
+
const seesAll = ctx.me.allAccounts;
|
|
22136
|
+
const { result: observed } = useLoad(() => seesAll ? api.adminObserved() : Promise.resolve({ kind: "notFound" }), [seesAll]);
|
|
22128
22137
|
if (result && result.kind !== "ok") return (0, import_jsx_runtime.jsx)("div", {
|
|
22129
22138
|
className: "screen",
|
|
22130
22139
|
children: (0, import_jsx_runtime.jsxs)("div", {
|
|
@@ -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-C1gpdkyH.js"></script>
|
|
53
53
|
<link rel="stylesheet" crossorigin href="/portal/assets/index-CsCuPshD.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.0-beta.
|
|
5
|
+
"version": "0.3.0-beta.3",
|
|
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": {
|
|
@@ -1214,6 +1214,11 @@ export async function doBatchScreen(apiKey, base, params, tctx) {
|
|
|
1214
1214
|
application_date: nr.applicationDate,
|
|
1215
1215
|
registration_date: nr.registrationDate,
|
|
1216
1216
|
expiry_date: nr.expiryDate,
|
|
1217
|
+
// The office's own numbers, which the record carries and a reader looks the record up by. A
|
|
1218
|
+
// knockout's filings list is built from these rows, and without them it could name no record a
|
|
1219
|
+
// reader can open.
|
|
1220
|
+
application_number: nr.applicationNumber,
|
|
1221
|
+
registration_number: nr.registrationNumber,
|
|
1217
1222
|
live_status: liveStatusOf(rec),
|
|
1218
1223
|
all_class: isAllClass(nr.niceClasses),
|
|
1219
1224
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trademark-oauth-mcp-bridge",
|
|
3
|
-
"version": "0.3.0-beta.
|
|
3
|
+
"version": "0.3.0-beta.3",
|
|
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).",
|
|
@@ -240,7 +240,7 @@ export function classify(doors, names, now, probe = runStateProbe) {
|
|
|
240
240
|
// dropped off this list while remaining exactly as unclosed as the ones that were never opened —
|
|
241
241
|
// so reading a round was enough to make it stop being counted, whatever the read found.
|
|
242
242
|
//
|
|
243
|
-
// The harness already knows this. previousRoundNotice in e2e.mjs
|
|
243
|
+
// The harness already knows this. previousRoundNotice in e2e.mjs says the round closes on
|
|
244
244
|
// `reportedState === "settled"`, NEVER on `reportedAt != null`, and gives the incident that
|
|
245
245
|
// taught it. This lister was the last reader keying on the weaker field.
|
|
246
246
|
if (String(r?.reportedState ?? "") === "settled") continue;
|