clearotron 0.3.2-beta.1 → 0.3.2-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.env.example CHANGED
@@ -480,6 +480,25 @@ CLEAROTRON_CUT_REF=
480
480
  # effect: tuning
481
481
  CLEAROTRON_RELEASE_WAIT_MS=
482
482
 
483
+ # A GitHub token carrying Actions read and write. The release workflow supplies it from a repository
484
+ # secret so that a cut does not wait for a person to approve the version pull request's parked CI run:
485
+ # that run is authored by the repository's own Actions bot, and GitHub parks bot-authored runs as
486
+ # `action_required`. The built-in token cannot approve a run — self-approval is blocked deliberately —
487
+ # which is why this is a second, separate credential.
488
+ #
489
+ # NEVER SET THIS ON A DEPLOYMENT. No installed instance reads it and no clearance run reaches it. It is
490
+ # written down here because shipped code reads it, and every name shipped code reads has a row; the
491
+ # catalogue is the contract, not a list of things you are expected to set.
492
+ #
493
+ # UNSET IS THE ORDINARY CASE AND NOT A MISCONFIGURATION. The script names the absent token on stdout
494
+ # and exits successfully, and the version run then waits for a person exactly as it did before the
495
+ # token existed. Nothing fails and no run changes its conclusion.
496
+ #
497
+ # It carries no effect declaration because the closed vocabulary has no class that is true of it: the
498
+ # credential class claims a run refuses at preflight when the name is absent, and this one does not
499
+ # refuse at all. Read by scripts/release-approve-parked.mjs.
500
+ ACTIONS_APPROVE_TOKEN=
501
+
483
502
  # The trickle floor: the fewest output tokens per second of ACTIVE time (elapsed minus tool wait) a
484
503
  # model turn may produce before it is stopped as a stall. Unset uses 1. A stage streaming a token every
485
504
  # few seconds holds off both other clocks — the stall clock resets on any byte, and the no-progress
package/build-info.json CHANGED
@@ -1,4 +1,4 @@
1
1
  {
2
- "commit": "4a4575724492736d2333d3bf2faf8027578427f2",
3
- "version": "0.3.2-beta.1"
2
+ "commit": "8393489759f73ec4177b2fce74005780b7b4a567",
3
+ "version": "0.3.2-beta.2"
4
4
  }
@@ -296,6 +296,7 @@ because the rule is about what PRODUCT CODE reads, not about what a run reads.
296
296
  | `CLEAROTRON_UPDATER_STAMP` | `_updater-identity.json` beside the update script, in the directory the updater runs from | The full path of the file in which the updater that deploys this box records which copy of itself ran. The updater writes it and deploy health reads it under this one name, so a box that moves the stamp sets it once for both. On a box with no updater unit, setting it says an updater exists elsewhere and is to be judged. Effect class `deployment`. |
297
297
  | `CLEAROTRON_CUT_REF` | `HEAD` | Which ref the cut decision reads the version from. **Read only by the release workflow, never set on a deployment.** The jobs that ask about `main` set it to `origin/main` explicitly, because their checkout is pinned to the run's own ref and `HEAD` there is that ref rather than the branch they are deciding about. A job that asks the wrong subject gets a confident wrong answer. |
298
298
  | `CLEAROTRON_RELEASE_WAIT_MS` | 25 minutes | How long a requested cut waits for the version pull request to merge itself before giving up. **Read only by the release workflow.** A rehearsal sets it to `0` so the wiring is exercised without holding a runner. Giving up is a quiet success by design — the scheduled run underneath catches a cut whose wait expired — so this budget failing shows up as a slow job rather than a red one. |
299
+ | `ACTIONS_APPROVE_TOKEN` | unset | A GitHub token with Actions read and write, used to approve the version pull request's parked CI run so a cut does not wait for a person. **Read only by the release workflow, never set on a deployment.** The built-in token cannot approve a run — GitHub blocks self-approval — so this is a second, separate credential. Unset is the ordinary case and not an error: the script names the absent token and exits successfully, and the version run waits for a person as it did before. Actions write is broader than approval alone — it also dispatches workflows, cancels any run in the repository and deletes run logs. |
299
300
  | `CLEAROTRON_AGENT_MCP_URL` | unset (⇒ `null`) | The API-key MCP door advertised to a signed-in client. Null until that door is deployed, and the UI keeps its honest empty state rather than inventing a URL. |
300
301
  | `CLEAROTRON_AGENTS` | derived | Comma-separated agent ids for `scripts/purge-runs.mjs` to sweep. |
301
302
  | `CLEAROTRON_E2E_DIR` | **none — the script refuses without it** | The config repo's `e2e/` directory. There is one suite and it is not in this repo (ruling 2026-08-07), so the comparison script names the variable rather than defaulting anywhere. |
@@ -495,7 +495,15 @@ origin; all portal config lives server-side in portal-service.
495
495
 
496
496
  These are read by the release workflow and by nothing a deployment runs. They are listed here because a
497
497
  name absent from this register is a name nobody can look up, not because an operator has any reason to set
498
- one — and setting either on a box does nothing at all.
498
+ one — and setting any of them on a box does nothing at all.
499
+
500
+ `ACTIONS_APPROVE_TOKEN` (unset) — a GitHub token with Actions read and write, used to approve the
501
+ version pull request's parked CI run so that a cut does not wait for someone to click. That run is
502
+ authored by the repository's own Actions bot and GitHub parks bot-authored runs; the built-in token
503
+ cannot release one, because self-approval is blocked deliberately. Unset is the ordinary case: the
504
+ script names the absent token and exits successfully, and the run waits for a person as before.
505
+ Actions write is wider than approving — it also dispatches workflows, cancels any run in the
506
+ repository and deletes run logs — so it is worth rotating on the same schedule as a deploy key.
499
507
 
500
508
  `CLEAROTRON_CUT_REF` (default `HEAD`) — which ref the cut decision reads the version from. The jobs that
501
509
  ask about `main` set it to `origin/main` explicitly, because their checkout is pinned to the run's own ref
@@ -1,5 +1,11 @@
1
1
  # clearotron-driver
2
2
 
3
+ ## 0.3.2-beta.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 8642064: Fixed: On one register the "Filings containing the name" figure counted only identical filings. A report could therefore show a field as far less crowded than it really is. That column now asks the register the question its label promises. Where a register cannot answer a given kind of search, the figure is reported as unavailable rather than filled in from a narrower one.
8
+
3
9
  ## 0.3.2-beta.1
4
10
 
5
11
  ### Patch Changes
@@ -1184,8 +1184,16 @@ export const PROVIDERS = {
1184
1184
  // modes, not strategies, and the API rejects them in the strategies array. These wrappers
1185
1185
  // hand-built the vendor shape and so were untouched by that fix — the translator is the one
1186
1186
  // place that knows which mode rides which request shape, and every caller must go through it.
1187
- const r = await core.doCountHits(process.env.SIGNA_API_KEY, base,
1188
- core.toSignaParams({ name, match_mode: matchMode || "exact", nice_classes: classes, regions }),
1187
+ // A MODE THIS PROVIDER CANNOT EXPRESS REFUSES THE CELL, rather than being answered by a
1188
+ // different predicate's number. The translator names it; this turns it into an honest unknown,
1189
+ // which the count kernel already treats as a disclosed gap. The alternative is what this
1190
+ // issue was: a narrower query answering under the wider query's label, invisibly.
1191
+ const signaParams = core.toSignaParams({ name, match_mode: matchMode || "exact", nice_classes: classes, regions });
1192
+ if (signaParams.unsupported_match_mode) {
1193
+ return { ok: false, total: null, unsupported: true,
1194
+ cause: `this register provider cannot express the "${signaParams.unsupported_match_mode}" match mode, so this count is UNKNOWN — it is not answered with a different predicate's number` };
1195
+ }
1196
+ const r = await core.doCountHits(process.env.SIGNA_API_KEY, base, signaParams,
1189
1197
  { kind: "count", agentId, sessionKey, sessionId: null, recordLog });
1190
1198
  const text = typeof r?.text === "string" ? r.text : "";
1191
1199
  if (text.startsWith("ERROR")) return { ok: false, cause: text.slice(0, 200) };
@@ -1207,8 +1215,16 @@ export const PROVIDERS = {
1207
1215
  catch (e) { return { ok: false, records: null, reason: `plugin core unavailable: ${e.message}` }; }
1208
1216
  const base = process.env.SIGNA_BASE_URL || core.DEFAULT_BASE;
1209
1217
  try {
1218
+ // The listing takes the same refusal as the count above, and for the same reason one level on:
1219
+ // a narrower search here returns FEWER records under the wider query's name, so the listing
1220
+ // would under-report and read as complete.
1221
+ const signaListParams = core.toSignaParams({ name, match_mode: matchMode || "exact", nice_classes: classes, regions });
1222
+ if (signaListParams.unsupported_match_mode) {
1223
+ return { ok: false, records: null,
1224
+ reason: `this register provider cannot express the "${signaListParams.unsupported_match_mode}" match mode, so this listing was not taken — it is not answered with a narrower search` };
1225
+ }
1210
1226
  const r = await core.doSearch(process.env.SIGNA_API_KEY, base,
1211
- { ...core.toSignaParams({ name, match_mode: matchMode || "exact", nice_classes: classes, regions }), limit },
1227
+ { ...signaListParams, limit },
1212
1228
  { kind: "search", agentId, sessionKey, sessionId: null, recordLog });
1213
1229
  const text = typeof r?.text === "string" ? r.text : "";
1214
1230
  if (text.startsWith("ERROR")) return { ok: false, records: null, reason: text.slice(0, 200) };
@@ -2,7 +2,7 @@
2
2
  "name": "clearotron-driver",
3
3
  "private": true,
4
4
  "type": "module",
5
- "version": "0.3.2-beta.1",
5
+ "version": "0.3.2-beta.2",
6
6
  "license": "AGPL-3.0-only",
7
7
  "description": "Deterministic driver for the trademark clearance workflow: orchestration in code (fan-out, fan-in barrier, gating, retries); the model does judgment leaves only, through a reasoning CLI spawned per stage.",
8
8
  "engines": {
@@ -3694,8 +3694,8 @@
3694
3694
  "todos": 0
3695
3695
  },
3696
3696
  "release-pipeline.test.mjs": {
3697
- "tests": 93,
3698
- "asserts": 356,
3697
+ "tests": 99,
3698
+ "asserts": 376,
3699
3699
  "skips": 0,
3700
3700
  "todos": 0
3701
3701
  },
@@ -6377,6 +6377,12 @@
6377
6377
  "skips": 0,
6378
6378
  "todos": 0
6379
6379
  },
6380
+ "providers/signa/test/match-mode-is-not-approximated.test.mjs": {
6381
+ "tests": 4,
6382
+ "asserts": 11,
6383
+ "skips": 0,
6384
+ "todos": 0
6385
+ },
6380
6386
  "providers/uspto-local/test/backfile-window.test.mjs": {
6381
6387
  "tests": 6,
6382
6388
  "asserts": 11,
@@ -1,5 +1,9 @@
1
1
  # trademark-artifacts-mcp
2
2
 
3
+ ## 0.3.2-beta.2
4
+
5
+ No changes in this release.
6
+
3
7
  ## 0.3.2-beta.1
4
8
 
5
9
  No changes in this release.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trademark-artifacts-mcp",
3
- "version": "0.3.2-beta.1",
3
+ "version": "0.3.2-beta.2",
4
4
  "license": "AGPL-3.0-only",
5
5
  "private": true,
6
6
  "description": "MCP server to interrogate clearotron trademark-clearance runs — list/read artifacts, trace the full decision flow, telemetry/cost, coverage, single-run search, and a gated single-step what-if. Imports the clearotron-driver read-only; touches no driver/template/deploy files.",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "clearotron",
3
3
  "type": "module",
4
- "version": "0.3.2-beta.1",
4
+ "version": "0.3.2-beta.2",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": {
7
7
  "type": "git",
@@ -2,7 +2,7 @@
2
2
  "name": "portal-ui",
3
3
  "private": true,
4
4
  "type": "module",
5
- "version": "0.3.2-beta.1",
5
+ "version": "0.3.2-beta.2",
6
6
  "license": "AGPL-3.0-only",
7
7
  "description": "The unified trademark portal UI. One address, one login: who you are decides what you see. Built as a static bundle, served by driver/portal-service.mjs — the browser never reaches profile-service or recipe-service.",
8
8
  "engines": {
@@ -1,5 +1,9 @@
1
1
  # trademark-oauth-mcp-bridge
2
2
 
3
+ ## 0.3.2-beta.2
4
+
5
+ No changes in this release.
6
+
3
7
  ## 0.3.2-beta.1
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.2-beta.1",
3
+ "version": "0.3.2-beta.2",
4
4
  "license": "AGPL-3.0-only",
5
5
  "private": true,
6
6
  "description": "OAuth 2.1 MCP stdio bridge used by the engine's case-law gather stage (courtlistener / legaldatahunter).",
@@ -576,7 +576,35 @@ export function toSignaParams(p = {}) {
576
576
  const mode = String(p.match_mode ?? "").trim();
577
577
  if (mode === "exact" || mode === "phonetic" || mode === "prefix") out.strategies = [mode];
578
578
  else if (mode === "starts_with" || mode === "ends_with" || mode === "contains") out.match = mode;
579
- else if (!mode && !p.match && !(Array.isArray(p.strategies) && p.strategies.length)) {
579
+ // ── `default` IS THE COUNT LANE'S WORD FOR THE SAME UNANCHORED QUERY THE PLAN LANE CALLS `{}` ────
580
+ //
581
+ // THE DEFECT. The count lane hands `match_mode: "default"` straight through — that is the word
582
+ // `COUNT_PREDICATES` gives the CONTAINING predicate, the one whose figure a client reads under
583
+ // "Filings containing the name". `default` matched none of the branches here, so neither request
584
+ // shape was selected, and `buildSearchRequest`'s else branch sent `strategies: ["exact"]`. The
585
+ // containing column therefore printed an exact count, on the axis that most signals how crowded a
586
+ // field is, and a client was told the field was less busy than it is in a number the report states
587
+ // as fact. Nothing failed and nothing was logged: a narrower query answers perfectly well, it just
588
+ // answers a different question.
589
+ //
590
+ // The branch below already does exactly this for the plan lane, which reaches here with NO mode at
591
+ // all. The two lanes mean the same thing by different words; they now take the same shape.
592
+ else if (mode === "default") {
593
+ if (typeof out.query === "string" && !out.query.includes("*")) out.match = "contains";
594
+ }
595
+ // ── AN UNRECOGNISED MODE REFUSES; IT DOES NOT QUIETLY BECOME A NARROWER SEARCH ──────────────────
596
+ //
597
+ // THE CLASS, and it has now bitten twice. This function's own header records `starts_with` falling
598
+ // through to a plain exact search — "a NARROWER query than the plan asked for, answering as though it
599
+ // were the one requested" — and `default` has just done the same on a client-facing figure. Both were
600
+ // fixed by adding the missing word. Adding words one defect at a time leaves the next unmapped mode
601
+ // to do it a third time, silently, in whichever direction happens to be wrong.
602
+ //
603
+ // So a mode this provider cannot express is now NAMED rather than approximated. The caller turns it
604
+ // into a refused cell, and a refused cell is a disclosed gap a reader can see; a wrong number is not
605
+ // visible at all. This can only fire where a number would otherwise have been silently wrong.
606
+ else if (mode) out.unsupported_match_mode = mode;
607
+ if (!mode && !p.match && !(Array.isArray(p.strategies) && p.strategies.length)) {
580
608
  // ── THE `default` PREDICATE, WHICH JUST MADE EXECUTABLE ────────────────────────────────
581
609
  // `planPredicateParams` returns {} for the plan's `default` predicate — no match_mode at all —
582
610
  // and this branch is the only thing standing between that and `strategies: ["exact"]`. With
@@ -0,0 +1,111 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
3
+ //
4
+ // release-pre-gate.mjs — let a stable cut reach the version script when every note has already been
5
+ // consumed by a pre-release.
6
+ //
7
+ // THE PROBLEM THIS EXISTS FOR, MEASURED. `changesets/action` decides whether to run `version-script` by
8
+ // counting the release notes it can see, and it does that ONCE, at the top of the run, before it touches
9
+ // the working tree. Its reader is `readChangesetState`:
10
+ //
11
+ // let preState = await readPreState(cwd);
12
+ // let changesets = await readChangesets(cwd);
13
+ // if (preState !== undefined && preState.mode === "pre") {
14
+ // return { preState, changesets: changesets.filter((c) => !c.id.startsWith("pre/")) };
15
+ // }
16
+ // return { preState: undefined, changesets };
17
+ //
18
+ // `readChangesets` DOES enumerate `.changeset/pre/`, giving each note an id beginning `pre/`. The filter
19
+ // that hides them applies only while the tree says `mode: "pre"`.
20
+ //
21
+ // A pre-release consumes a note by MOVING it into `.changeset/pre/`, where it waits so the eventual
22
+ // stable can list every change since the last stable. So the ordinary state after cutting a beta is: no
23
+ // notes at the top level, every note under `pre/`, and the flag still saying `pre`. In that state the
24
+ // action counts zero, skips the version script, emits no pull request number, and the stable cut refuses
25
+ // with "Nothing to cut" — a refusal about the wrong thing, because the tree holds a whole release.
26
+ //
27
+ // WHAT THIS DOES, AND WHY IT IS NOT A TRICK. It writes the channel the dispatch asked for into the file
28
+ // the action reads, before the action reads it. A stable cut IS a tree leaving pre-release mode, and the
29
+ // notes under `pre/` ARE the notes that stable will carry; saying so is a true statement about this cut,
30
+ // not a device to get past a check. `release-version.mjs` already states the same principle for the
31
+ // other reader: "the mode a cut needs is a fact about the channel asked for, not about what the last cut
32
+ // happened to do." There are two readers of that fact and they are reached separately.
33
+ //
34
+ // THE WRITE IS DISCARDED, ON PURPOSE, AND THAT IS THE POINT. The action's own first act inside
35
+ // `runVersion` is `git checkout changeset-release/main` followed by `git reset --hard <the run's
36
+ // commit>`, which puts `pre.json` back to `pre`. The version script then runs in that rebuilt tree, finds
37
+ // the tree still in pre mode, and performs the real transition itself with `changeset pre exit` — which
38
+ // is where the transition has to happen, and the reason it was moved there. So this changes what the
39
+ // action COUNTS and nothing about what the cut DOES. Nothing here versions anything, and a tree this
40
+ // touched still produces the same version it would have produced had the gate never been in the way.
41
+ //
42
+ // WHAT IT REFUSES TO DO. It is silent on a beta, on a tree that is not in pre mode, on a tree whose notes
43
+ // are at the top level where the action can already see them, and on a tree with no notes anywhere. That
44
+ // last one matters most: a cut with genuinely nothing to cut must still refuse, and it still does,
45
+ // because this writes nothing when `.changeset/pre/` is empty.
46
+ import { readFileSync, writeFileSync, existsSync, readdirSync } from "node:fs";
47
+ import { join, dirname } from "node:path";
48
+ import { fileURLToPath } from "node:url";
49
+ import { isEntrypoint } from "../shared/is-entrypoint.mjs";
50
+
51
+ const ROOT = join(dirname(dirname(fileURLToPath(import.meta.url))));
52
+
53
+ /** A note is a `.md` file that is not the directory's own README. */
54
+ export function countNotes(dir) {
55
+ if (!existsSync(dir)) return 0;
56
+ return readdirSync(dir).filter((f) => f.endsWith(".md") && f.toLowerCase() !== "readme.md").length;
57
+ }
58
+
59
+ /**
60
+ * PURE. Whether this cut has to restate its channel for the action's counter, and nothing else.
61
+ *
62
+ * `null` means leave the tree alone, and every `null` here is a case the action already reads correctly
63
+ * — including the two that must keep refusing. Only the one state the action cannot see returns a write.
64
+ *
65
+ * Kept separate from the file it writes so the decision can be driven against every member of the class
66
+ * rather than only against the tree that happens to be checked out.
67
+ */
68
+ export function preGateDecision({ cut, mode, topLevelNotes, preNotes }) {
69
+ // A beta never restates anything. A beta with no pending notes MUST still refuse, and that refusal is
70
+ // this function returning null.
71
+ if (cut !== "stable") return null;
72
+ // Not in pre mode: the action's filter is not engaged, so whatever is there is already counted.
73
+ if (mode !== "pre") return null;
74
+ // The action can see these. Touching the flag here would change the pull request's title for a cut
75
+ // that never needed help.
76
+ if (topLevelNotes > 0) return null;
77
+ // Nothing anywhere. "Nothing to cut" is then the truth, and the refusal downstream is correct.
78
+ if (preNotes === 0) return null;
79
+ return { mode: "exit" };
80
+ }
81
+
82
+ export function main(root = ROOT, argv = process.argv.slice(2)) {
83
+ const cut = (argv.find((a) => a.startsWith("--cut=")) ?? "").slice("--cut=".length);
84
+ // `--root` exists so this can be driven against a built tree rather than only against the checkout it
85
+ // happens to live in. The workflow never passes it: there the script sits in the repository being cut,
86
+ // which is what ROOT already means.
87
+ const rootFlag = argv.find((a) => a.startsWith("--root="));
88
+ if (rootFlag) root = rootFlag.slice("--root=".length);
89
+ const preJson = join(root, ".changeset", "pre.json");
90
+ const flag = existsSync(preJson) ? JSON.parse(readFileSync(preJson, "utf8")) : null;
91
+ const mode = flag?.mode ?? "none";
92
+ const topLevelNotes = countNotes(join(root, ".changeset"));
93
+ const preNotes = countNotes(join(root, ".changeset", "pre"));
94
+
95
+ const decision = preGateDecision({ cut, mode, topLevelNotes, preNotes });
96
+ const state = `cut=${cut || "none"}, mode=${mode}, ${topLevelNotes} note(s) pending, ${preNotes} already consumed by the pre-release`;
97
+ if (!decision) {
98
+ // SAYS SO EITHER WAY. A step that is silent when it does nothing cannot be told from a step that did
99
+ // not run, and this one is skipped far more often than it acts.
100
+ console.log(`release-pre-gate: ${state} — leaving the flag alone.`);
101
+ return 0;
102
+ }
103
+ writeFileSync(preJson, `${JSON.stringify({ ...flag, ...decision }, null, 2)}\n`);
104
+ console.log(`release-pre-gate: ${state} — the notes this stable carries are all under .changeset/pre/,`
105
+ + " where the action does not count them while the flag says pre. Writing mode=exit so it counts them."
106
+ + " The version script performs the real transition after the action rebuilds the tree; this write is"
107
+ + " discarded by that rebuild and changes no version.");
108
+ return 0;
109
+ }
110
+
111
+ if (isEntrypoint(import.meta.url)) process.exit(main());