clearotron 0.4.1-beta.5 → 0.4.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.
Files changed (45) hide show
  1. package/.env.example +7 -0
  2. package/build-info.json +2 -2
  3. package/docs/architecture/04-configuration-reference.md +1 -0
  4. package/driver/CHANGELOG.md +42 -0
  5. package/driver/ask-ledger.mjs +6 -1
  6. package/driver/band-shape.mjs +12 -4
  7. package/driver/close-verify.mjs +27 -8
  8. package/driver/engine/anthropic-agent.mjs +7 -3
  9. package/driver/engine/common.mjs +10 -3
  10. package/driver/engine/mcp/band-server.mjs +55 -12
  11. package/driver/engine/mcp/codex-config.mjs +24 -4
  12. package/driver/engine/mcp/gather-config.mjs +4 -3
  13. package/driver/gateway.mjs +13 -5
  14. package/driver/package.json +1 -1
  15. package/driver/pipeline.mjs +87 -31
  16. package/driver/predelivery-lint.mjs +1 -1
  17. package/driver/record-goods.mjs +30 -0
  18. package/driver/remedy-accounting.mjs +2 -6
  19. package/driver/skills/knockout-assess/SKILL.md +2 -2
  20. package/driver/stages-knockout.mjs +0 -13
  21. package/driver/stages.mjs +11 -2
  22. package/driver/suite-census.json +81 -3
  23. package/driver/verify-knockout.mjs +27 -10
  24. package/mcp-server/CHANGELOG.md +8 -0
  25. package/mcp-server/http-smoke.mjs +2 -0
  26. package/mcp-server/lib/audit-view.mjs +10 -3
  27. package/mcp-server/lib/events.mjs +7 -0
  28. package/mcp-server/lib/knockout.mjs +7 -5
  29. package/mcp-server/package.json +1 -1
  30. package/package.json +1 -1
  31. package/portal-ui/package.json +1 -1
  32. package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
  33. package/providers/oauth-mcp-bridge/package.json +1 -1
  34. package/scripts/ask-ai-render-check.mjs +1 -0
  35. package/scripts/authority-boundary-probe.mjs +2 -0
  36. package/scripts/report-header-render-check.mjs +1 -0
  37. package/scripts/report-offline-render-check.mjs +1 -0
  38. package/scripts/report-screenshot.mjs +1 -0
  39. package/scripts/report-sections-render-check.mjs +1 -0
  40. package/scripts/report-theme-render-check.mjs +1 -0
  41. package/scripts/revisit-render-check.mjs +1 -0
  42. package/scripts/settings-render-check.mjs +1 -0
  43. package/scripts/undeclared-key-census.mjs +1 -0
  44. package/scripts/verify-publishable.mjs +2 -0
  45. package/shared/names-in-force.mjs +1 -0
@@ -58,6 +58,7 @@
58
58
  // So `scrubBlocks` below keeps the drop, and the raw list a client account reads is the LIVE chain.
59
59
 
60
60
  import { scrubBody, scrubCards } from "./scrub.mjs";
61
+ import { INTERPRETED_EVENT_KINDS } from "./events.mjs";
61
62
  import { accountMayReadArtifact } from "../../shared/scope.mjs";
62
63
  import { clientFailureNote } from "../../shared/client-failure-note.mjs";
63
64
 
@@ -279,8 +280,14 @@ export function accountTrace(result) {
279
280
  //
280
281
  // The entry shape is a UNION over ~15 event kinds and classify() ends in a generic passthrough, so an
281
282
  // event nobody has projected yet arrives here with whatever keys it was written with. Hence an allowlist
282
- // over the union rather than a delete-list: a new event kind is thinned to its decision word until
283
- // someone widens this deliberately, which is the direction that fails safe.
283
+ // over the union rather than a delete-list.
284
+ //
285
+ // AND THE PASSTHROUGH ROWS DO NOT TRAVEL AT ALL. Thinned to its decision word, a passthrough row still
286
+ // carried the engine's own name for the event as both `kind` and `decision`: on one test run, 133 of the
287
+ // 158 rows a client account read were 73 such names (measured 2026-10-14). The owner's standing rule for
288
+ // engineering text on a client surface is to take it out and write nothing in its place, so a client
289
+ // account keeps only the kinds classify() interprets (INTERPRETED_EVENT_KINDS). `seq` was assigned over
290
+ // every event before this drop, so a poller's cursor never renumbers; the engineering view is unchanged.
284
291
  const TIMELINE_FIELDS = ["ts", "seq", "kind", "phase", "stage", "decision", "trigger", "ok", "outputSha",
285
292
  "changedFromPrevious", "attempt", "axes", "axis", "escalated", "verdict", "display", "cause",
286
293
  "recovered", "count", "uris", "findings", "negatives", "audit", "snapshot", "resume"];
@@ -306,7 +313,7 @@ export function accountTimeline(result, { brandName = "The firm" } = {}) {
306
313
  };
307
314
  return {
308
315
  ...pick(result, ["runId", "state", "tier", "_note"]),
309
- timeline: Array.isArray(result.timeline) ? result.timeline.map(entry) : [],
316
+ timeline: Array.isArray(result.timeline) ? result.timeline.filter((t) => INTERPRETED_EVENT_KINDS.has(t.kind)).map(entry) : [],
310
317
  verdictHistory: Array.isArray(result.verdictHistory)
311
318
  ? result.verdictHistory.map((v) => pick(v, ["ts", "kind", "verdict", "stage"]))
312
319
  : result.verdictHistory ?? null,
@@ -157,6 +157,13 @@ function classify(e, st, rat) {
157
157
  }
158
158
  }
159
159
 
160
+ // THE KINDS classify() INTERPRETS — one per `case` above, pinned to them by events.test.mjs. Every other event
161
+ // reaches the timeline through the passthrough, under the engine's own name for it. That is the engineering
162
+ // view's to keep; a client account's timeline keeps these kinds alone (audit-view.mjs accountTimeline).
163
+ export const INTERPRETED_EVENT_KINDS = new Set(["start", "axes", "stage", "skip", "failover", "attempt", "skeptic-escalation",
164
+ "escalation-skipped", "escalation-noop", "verdict", "verdict-2", "delivered-with-open-questions", "screen-gate-clean",
165
+ "screen-gate-violation", "audit", "provider-usage", "manual-rerun", "failed"]);
166
+
160
167
  // A SUCCEEDED attempt is not a decision. put one `attempt` row on run.jsonl per model dispatch, and
161
168
  // with no kind filter here every stage began emitting BOTH `attempt-succeeded` and `stage-completed` —
162
169
  // +61 entries on 269 (+23%) on the 2026-07-29 delivered run, roughly double on a correction-heavy one, on
@@ -361,9 +361,10 @@ export function knockoutCoverage(run) {
361
361
  * enumerated fifteen stages and none of them was a knockout stage — the tool could resolve nothing on this
362
362
  * product, including the word "verdict".
363
363
  *
364
- * `knockout-assess` is chunked (`knockout-assess#N`), so a caller may name the bare stage or one chunk.
364
+ * `knockout-assess` is chunked (`knockout-assess#N`), so a caller may name the bare stage or one chunk. The
365
+ * reviewing pass that follows it is a stage of its own, and its rewrites reach the record a reader receives.
365
366
  */
366
- export const KNOCKOUT_STAGES = ["knockout-frame", "knockout-sweep", "knockout-register", "knockout-assess"];
367
+ export const KNOCKOUT_STAGES = ["knockout-frame", "knockout-sweep", "knockout-register", "knockout-assess", "knockout-review"];
367
368
 
368
369
  export function knockoutStageFor(target) {
369
370
  const t = String(target ?? "").trim();
@@ -404,7 +405,7 @@ export function traceKnockout(run, target, events = []) {
404
405
  events: rows,
405
406
  // What the stage wrote, by presence — the honest answer to "did this stage produce anything".
406
407
  produced: knockoutArtifacts(run).filter((a) => a.exists && STAGE_OUTPUTS[bare]?.includes(a.name)),
407
- note: STAGE_NOTES[bare] ?? null,
408
+ note: KNOCKOUT_STAGE_NOTES[bare] ?? null,
408
409
  };
409
410
  }
410
411
 
@@ -436,9 +437,10 @@ const STAGE_OUTPUTS = {
436
437
  "knockout-assess": ["findings", "assessment"],
437
438
  };
438
439
 
439
- const STAGE_NOTES = {
440
+ // A stage's note is read by the connector's model, which answers clients from it. A stage with no note says
441
+ // nothing rather than something untrue: the sweep's note said two research calls per mark, and it makes one.
442
+ export const KNOCKOUT_STAGE_NOTES = {
440
443
  "knockout-frame": "Reads the order and writes the plan: which names, which classes, at what depth.",
441
- "knockout-sweep": "Two research calls per mark against the marketplace/common-law provider: the broad question, and the kinds of use the frame named for the client's field; the payloads land in research/.",
442
444
  "knockout-register": "Fetches and counts register filings for the searched names. Absent on a run with no register component.",
443
445
  "knockout-assess": "Rates each mark from its own payload and the fetched filings, and writes knockout-findings.json.",
444
446
  };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trademark-artifacts-mcp",
3
- "version": "0.4.1-beta.5",
3
+ "version": "0.4.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/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "clearotron",
3
3
  "mcpName": "io.github.CordilleraSarl/clearotron",
4
4
  "type": "module",
5
- "version": "0.4.1-beta.5",
5
+ "version": "0.4.1",
6
6
  "license": "AGPL-3.0-only",
7
7
  "repository": {
8
8
  "type": "git",
@@ -2,7 +2,7 @@
2
2
  "name": "portal-ui",
3
3
  "private": true,
4
4
  "type": "module",
5
- "version": "0.4.1-beta.5",
5
+ "version": "0.4.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": {
@@ -1,5 +1,13 @@
1
1
  # trademark-oauth-mcp-bridge
2
2
 
3
+ ## 0.4.1
4
+
5
+ No changes in this release.
6
+
7
+ ## 0.4.1-beta.6
8
+
9
+ No changes in this release.
10
+
3
11
  ## 0.4.1-beta.5
4
12
 
5
13
  No changes in this release.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trademark-oauth-mcp-bridge",
3
- "version": "0.4.1-beta.5",
3
+ "version": "0.4.1",
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).",
@@ -202,6 +202,7 @@ const wsUrl = await new Promise((resolve, reject) => {
202
202
  () => reject(new Error(`chrome did not report a devtools endpoint within 60s. Its stderr was:\n${devtools || '(nothing)'}`)),
203
203
  60000,
204
204
  )
205
+ chrome.stderr.setEncoding("utf8");
205
206
  chrome.stderr.on('data', (c) => {
206
207
  devtools += c
207
208
  const m = devtools.match(/ws:\/\/[^\s]+/)
@@ -78,7 +78,9 @@ const program = resolveEngineProgram("anthropic-agent");
78
78
  const child = spawn(program.resolved ?? program.bin, [...args, "--include-hook-events"],
79
79
  { stdio: ["pipe", "pipe", "pipe"], cwd: runDir, env: spawnEnv() });
80
80
  let out = "", err = "";
81
+ child.stdout.setEncoding("utf8");
81
82
  child.stdout.on("data", (d) => { out += d; });
83
+ child.stderr.setEncoding("utf8");
82
84
  child.stderr.on("data", (d) => { err += d; });
83
85
  child.stdin.on("error", () => {});
84
86
  child.stdin.write(input); child.stdin.end();
@@ -85,6 +85,7 @@ reapOnExit(chrome)
85
85
  const wsUrl = await new Promise((resolve, reject) => {
86
86
  let devtools = ''
87
87
  const t = setTimeout(() => reject(new Error(`chrome reported no devtools endpoint in 60s:\n${devtools || '(nothing)'}`)), 60000)
88
+ chrome.stderr.setEncoding("utf8");
88
89
  chrome.stderr.on('data', (c) => { devtools += c; const m = devtools.match(/ws:\/\/[^\s]+/); if (m) { clearTimeout(t); resolve(m[0]) } })
89
90
  })
90
91
  const ws = new WebSocket(wsUrl)
@@ -81,6 +81,7 @@ reapOnExit(chrome);
81
81
  const wsUrl = await new Promise((resolve, reject) => {
82
82
  let devtools = "";
83
83
  const t = setTimeout(() => reject(new Error(`chrome reported no devtools endpoint in 60s:\n${devtools || "(nothing)"}`)), 60000);
84
+ chrome.stderr.setEncoding("utf8");
84
85
  chrome.stderr.on("data", (c) => { devtools += c; const m = devtools.match(/ws:\/\/[^\s]+/); if (m) { clearTimeout(t); resolve(m[0]); } });
85
86
  });
86
87
  const ws = new WebSocket(wsUrl);
@@ -56,6 +56,7 @@ const chrome = spawn("google-chrome", [
56
56
  let stderr = "";
57
57
  const wsUrl = await new Promise((res, rej) => {
58
58
  const t = setTimeout(() => rej(new Error(`chrome reported no devtools endpoint in 60s. Its stderr:\n${stderr || "(nothing)"}`)), 60000);
59
+ chrome.stderr.setEncoding("utf8");
59
60
  chrome.stderr.on("data", (c) => { stderr += c; const m = stderr.match(/ws:\/\/[^\s]+/); if (m) { clearTimeout(t); res(m[0]); } });
60
61
  });
61
62
 
@@ -108,6 +108,7 @@ reapOnExit(chrome)
108
108
  const wsUrl = await new Promise((resolve, reject) => {
109
109
  let devtools = ''
110
110
  const t = setTimeout(() => reject(new Error(`chrome reported no devtools endpoint in 60s:\n${devtools || '(nothing)'}`)), 60000)
111
+ chrome.stderr.setEncoding("utf8");
111
112
  chrome.stderr.on('data', (c) => { devtools += c; const m = devtools.match(/ws:\/\/[^\s]+/); if (m) { clearTimeout(t); resolve(m[0]) } })
112
113
  })
113
114
  const ws = new WebSocket(wsUrl)
@@ -116,6 +116,7 @@ reapOnExit(chrome)
116
116
  const wsUrl = await new Promise((resolve, reject) => {
117
117
  let devtools = ''
118
118
  const t = setTimeout(() => reject(new Error(`chrome reported no devtools endpoint in 60s:\n${devtools || '(nothing)'}`)), 60000)
119
+ chrome.stderr.setEncoding("utf8");
119
120
  chrome.stderr.on('data', (c) => { devtools += c; const m = devtools.match(/ws:\/\/[^\s]+/); if (m) { clearTimeout(t); resolve(m[0]) } })
120
121
  })
121
122
  const ws = new WebSocket(wsUrl)
@@ -210,6 +210,7 @@ const wsUrl = await new Promise((resolve, reject) => {
210
210
  () => reject(new Error(`chrome did not report a devtools endpoint within 60s. Its stderr was:\n${devtools || '(nothing)'}`)),
211
211
  60000,
212
212
  )
213
+ chrome.stderr.setEncoding("utf8");
213
214
  chrome.stderr.on('data', (c) => {
214
215
  devtools += c
215
216
  const m = devtools.match(/ws:\/\/[^\s]+/)
@@ -129,6 +129,7 @@ reapOnExit(chrome)
129
129
  let devtools = ''
130
130
  const wsUrl = await new Promise((resolve, reject) => {
131
131
  const t = setTimeout(() => reject(new Error(`chrome did not report a devtools endpoint within 60s. Its stderr was:\n${devtools || '(nothing)'}`)), 60000)
132
+ chrome.stderr.setEncoding("utf8");
132
133
  chrome.stderr.on('data', (c) => {
133
134
  devtools += c
134
135
  const m = devtools.match(/ws:\/\/[^\s]+/)
@@ -26,6 +26,7 @@ const MCP = new URL("../driver/engine/mcp/", import.meta.url).pathname;
26
26
  const SERVERS=["recording-server.mjs","coverage-server.mjs","dispositions-server.mjs","declination-server.mjs","unit-note-server.mjs"];
27
27
  const ask=(s)=>new Promise((res)=>{const c=spawn(process.execPath,[join(MCP,s)],{stdio:["pipe","pipe","pipe"],env:{...process.env,PERPLEXITY_API_KEY:"x"}});
28
28
  let b="",o=null;const d=()=>{try{c.kill("SIGKILL")}catch{};res(o||[])};const t=setTimeout(d,8000);
29
+ c.stdout.setEncoding("utf8");
29
30
  c.stdout.on("data",x=>{b+=x;let n;while((n=b.indexOf("\n"))>=0){const l=b.slice(0,n).trim();b=b.slice(n+1);if(!l)continue;
30
31
  try{const m=JSON.parse(l);if(m.id===2){o=m.result?.tools??[];clearTimeout(t);d();}}catch{}}});
31
32
  c.on("error",()=>d());
@@ -174,7 +174,9 @@ try {
174
174
  // this deliberately no longer starts.
175
175
  if (/report:\s+\S/.test(out) && /published:/.test(out)) { clearTimeout(t); finish(true); }
176
176
  };
177
+ child.stdout.setEncoding("utf8");
177
178
  child.stdout.on("data", read);
179
+ child.stderr.setEncoding("utf8");
178
180
  child.stderr.on("data", read);
179
181
  child.on("error", (e) => { clearTimeout(t); finish(false, `demo could not start: ${e.message}`); });
180
182
  });
@@ -23,6 +23,7 @@ export const NAMES_IN_FORCE = Object.freeze([
23
23
  "CLEAROTRON_AI",
24
24
  "CLEAROTRON_AI_BILLING",
25
25
  "CLEAROTRON_AI_PATH",
26
+ "CLEAROTRON_BAND_GROUPS",
26
27
  "CLEAROTRON_BAND_RESPONSE_CHARS",
27
28
  "CLEAROTRON_BAND_RUN_DIR",
28
29
  "CLEAROTRON_BAND_TRUTH_GATE",