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.
@@ -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
- gb: { name: "United Kingdom", address: (r) => addressFrom([r.applicationNumber, r.registrationNumber], /^(UK\d{11})$/i, (m) => ({
72
- number: m[1].toUpperCase(), href: `https://trademarks.ipo.gov.uk/ipo-tmcase/page/Results/1/${m[1].toUpperCase()}` })) },
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
- /** The registers whose records carry no page of the vendor's own, so the office's page is the link. */
122
- export const VENDORS_WITHOUT_RECORD_PAGES = Object.freeze(["signa"]);
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 is
126
- * `provider`. Returns null for any other register, so those runs render exactly as before: their records
127
- * carry links of their own. `tally` is what publish records, per office, so that a register whose numbers
128
- * never fit shows up as a count instead of as a report that quietly looks the way it always did.
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, provider) {
131
- if (!VENDORS_WITHOUT_RECORD_PAGES.includes(clean(provider).toLowerCase())) return null;
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
- for (const l of byUri instanceof Map ? byUri.values() : []) if (l && !l.href) keys.add(`${l.office} ${l.reason}`);
176
- return [...keys].sort().map((k) => {
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, and each filing that carries an office
219
- * number gains an `officeLink` in place, as normalizeRegisterRecordLinks rewrites links in place, so the
220
- * report, the workbook and report-data.json all state the same link. Returns null for any other
221
- * register, whose filings render exactly as before.
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 (!VENDORS_WITHOUT_RECORD_PAGES.includes(clean(doc?.provider).toLowerCase())) return null;
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 REAL customer on the
28
- // profile roster (never "generic"). The routing core (`makeRecipeService`) is fs-only + injected
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
- return profiles.has(customer) && customer !== "generic";
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 real customer (create the profile first; "generic" cannot own recipes)` } };
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
+ }
@@ -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": 7,
152
- "asserts": 36,
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": 12,
374
- "asserts": 56,
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": 24,
1808
- "asserts": 67,
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": 60,
2030
- "asserts": 215,
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": 20,
2192
- "asserts": 79,
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": 122,
3062
- "asserts": 656,
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": 12,
3326
- "asserts": 67,
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": 16,
3482
- "asserts": 44,
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": 26,
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": 9,
4862
- "asserts": 37,
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": 22,
5401
- "asserts": 94,
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": 55,
5766
+ "asserts": 57,
5761
5767
  "skips": 0,
5762
5768
  "todos": 0
5763
5769
  },
@@ -1,5 +1,9 @@
1
1
  # trademark-artifacts-mcp
2
2
 
3
+ ## 0.3.0-beta.3
4
+
5
+ No changes in this release.
6
+
3
7
  ## 0.3.0-beta.2
4
8
 
5
9
  ### Patch Changes
@@ -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 }); } catch { return { savedSearches: [], savedSearchesNote: null }; }
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)
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trademark-artifacts-mcp",
3
- "version": "0.3.0-beta.2",
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "clearotron",
3
3
  "type": "module",
4
- "version": "0.3.0-beta.2",
4
+ "version": "0.3.0-beta.3",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": {
7
7
  "type": "git",
@@ -11018,14 +11018,14 @@ var NAV = [
11018
11018
  label: "Admin settings",
11019
11019
  path: "/portal/admin",
11020
11020
  icon: "settings",
11021
- needs: "manage",
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: "manage"
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 visible = (e, who) => !e.needs || who.permissions[e.needs];
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 account = ownerFilter ?? (ctx.me.allAccounts ? "*" : ctx.me.accounts.length === 1 ? ctx.me.accounts[0] : null);
13020
- const { result: famResult, reload: reloadFamilies } = useLoad(() => api.families(account), [account]);
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: canManage(ctx.me) ? () => ctx.go("/portal/admin/config") : null,
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 { result: observed } = useLoad(() => api.adminObserved(), []);
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-CcFjgM78.js"></script>
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>
@@ -2,7 +2,7 @@
2
2
  "name": "portal-ui",
3
3
  "private": true,
4
4
  "type": "module",
5
- "version": "0.3.0-beta.2",
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,5 +1,9 @@
1
1
  # trademark-oauth-mcp-bridge
2
2
 
3
+ ## 0.3.0-beta.3
4
+
5
+ No changes in this release.
6
+
3
7
  ## 0.3.0-beta.2
4
8
 
5
9
  No changes in this release.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trademark-oauth-mcp-bridge",
3
- "version": "0.3.0-beta.2",
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:2256 says the round closes on
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;