clearotron 0.5.0-beta.4 → 0.5.0-beta.5

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 (38) hide show
  1. package/THIRD-PARTY-NOTICES.md +1 -1
  2. package/build-info.json +2 -2
  3. package/docs/architecture/04-configuration-reference.md +1 -1
  4. package/docs/architecture/05-config-governance.md +1 -1
  5. package/driver/CHANGELOG.md +12 -0
  6. package/driver/contract-e3-backlog.mjs +6 -6
  7. package/driver/coverage-form.mjs +1 -1
  8. package/driver/engine/mcp/perplexity-server.mjs +19 -6
  9. package/driver/engine/openai-agent.mjs +14 -11
  10. package/driver/findings-model.mjs +14 -1
  11. package/driver/gateway.mjs +18 -2
  12. package/driver/owner-use-check.mjs +15 -6
  13. package/driver/package.json +1 -1
  14. package/driver/pipeline.mjs +58 -22
  15. package/driver/predelivery-lint.mjs +46 -11
  16. package/driver/publish/render-knockout.mjs +23 -4
  17. package/driver/record-replay.mjs +40 -0
  18. package/driver/register-digest-record.mjs +21 -5
  19. package/driver/register-plan.mjs +6 -2
  20. package/driver/repair-targets.mjs +81 -6
  21. package/driver/research-answers.mjs +106 -12
  22. package/driver/research-checks.mjs +84 -0
  23. package/driver/skills/clearance-register/digest.md +1 -1
  24. package/driver/skills/clearance-search/phase2-execution.md +10 -7
  25. package/driver/skills/clearance-search/synthesis-rules.md +2 -2
  26. package/driver/skills/narrative-refutation/SKILL.md +1 -1
  27. package/driver/stage-context.mjs +14 -2
  28. package/driver/stages.mjs +8 -4
  29. package/driver/suite-census.json +48 -6
  30. package/driver/synthesis-record.mjs +123 -14
  31. package/mcp-server/CHANGELOG.md +4 -0
  32. package/mcp-server/package.json +1 -1
  33. package/package.json +1 -1
  34. package/portal-ui/package.json +1 -1
  35. package/providers/oauth-mcp-bridge/CHANGELOG.md +4 -0
  36. package/providers/oauth-mcp-bridge/package.json +1 -1
  37. package/scripts/deprecate-below.mjs +14 -3
  38. package/scripts/e2e-first-time.mjs +2 -1
@@ -80,7 +80,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
80
80
  THE SOFTWARE.
81
81
  ```
82
82
 
83
- ## @hono/node-server@1.19.17
83
+ ## @hono/node-server@2.1.5
84
84
 
85
85
  - **Licence declared:** `MIT`
86
86
  - **Repository:** https://github.com/honojs/node-server
package/build-info.json CHANGED
@@ -1,4 +1,4 @@
1
1
  {
2
- "commit": "7c6882de2e3c147992a6667bbce24de9fffb5206",
3
- "version": "0.5.0-beta.4"
2
+ "commit": "a475b2f2632ee5df27bc7248bd73d27d6fc6f5c3",
3
+ "version": "0.5.0-beta.5"
4
4
  }
@@ -191,7 +191,7 @@ timeout shot, warm patch, backoff), the lane-wedge re-dispatch, and the rate-lim
191
191
  | `CLEAROTRON_AI_BILLING` | `subscription` (default) \| `api-key` \| `cloud` | Billing mode for the selected engine. ONE variable for both, and only the LIVE engine's setting is read — it fills each engine's billing knob, and the engine that is not selected is never consulted. **`anthropic-agent`:** subscription **deletes `ANTHROPIC_API_KEY` from the child env** so `claude -p` uses OAuth subscription credentials (a present key would override them); `api-key` keeps the key — the scale setting and standing fallback; **`cloud`** bills Claude per use through the reader's own cloud account, named by the Claude program's own switch — `CLAUDE_CODE_USE_VERTEX`, `CLAUDE_CODE_USE_FOUNDRY` or `CLAUDE_CODE_USE_BEDROCK`, or `ANTHROPIC_BASE_URL` alone for a gateway (see the credentials table below) — and deletes `ANTHROPIC_API_KEY` as subscription does. **`openai-agent`:** subscription seeds `auth.json` into the per-run `CODEX_HOME` from `CLEAROTRON_OPENAI_AUTH_FILE` (default `~/.codex/auth.json`) and strips API keys; `api-key` keeps `CODEX_API_KEY`; `cloud` is refused. **Fail-loud, never a guess, never the subscription in its place:** `api-key` with no `ANTHROPIC_API_KEY` / `CODEX_API_KEY` throws; on `anthropic-agent`, `cloud` with two switches on, or with none and no `ANTHROPIC_BASE_URL`, throws, and so does a switch left on under `subscription` or `api-key`, because the program bills that cloud whatever this says; a value that is none of the three throws on both engines. `clearotron start` and `clearotron doctor` report each refusal against the settings the services read, naming a value that is not a billing mode by its setting and never quoting it, and the runner refuses the search when it is ordered, before anything is spent. The resolved mode is stamped on every stage telemetry row (`engine`, `authMode`, `apiBilled`, and `cloud`, null when no cloud bills), and the attempt row also carries `providerReported`, the program's own word for who served the turn. |
192
192
  | `CLEAROTRON_CLAUDE_PATH` · `CLEAROTRON_CODEX_PATH` | `claude` / `codex` on `PATH`, then the copy Clearotron installed | The engine binary to spawn, ONE PER ENGINE: `CLEAROTRON_CLAUDE_PATH` under `anthropic-agent`, `CLEAROTRON_CODEX_PATH` under `openai-agent`. Only the live engine's is read, and a machine that runs both sets both — they were one variable until it met a machine needing two different paths. A path set here must be **absolute**: stage subprocesses run with cwd set to the run directory, so a relative path does not resolve there, and `clearotron doctor` refuses one. **Unset**, or set to the bare word `claude` / `codex` (which means the same), the engine uses the program on `PATH` when the machine has one, and otherwise the copy Clearotron installed in `~/.local/share/clearotron/engines`: `clearotron install` offers to put the chosen engine's program there (`@anthropic-ai/claude-code` or `@openai/codex`, this platform's build only), and `clearotron update` refreshes it. Nothing is bundled into the package. A value naming anything else is used as given and never falls back. `clearotron doctor` says which copy it found, and its version when npm installed that copy. |
193
193
  | `CLEAROTRON_OPENAI_AUTH_FILE` | `~/.codex/auth.json` | The credentials file seeded into the per-run `CODEX_HOME` under subscription billing. Was exempted from the August 2026 rename as an `openai-agent` internal rather than an install-surface name; a decision of 2026-09-04 **reversed that exemption** and renamed the whole namespace, so it carries the house prefix like everything else. One spelling, no exceptions. |
194
- | `CLEAROTRON_OPENAI_MODEL_JUDGMENT` / `CLEAROTRON_OPENAI_MODEL_SWEEP` / `CLEAROTRON_OPENAI_MODEL_CHEAP` | `gpt-6.1-sol` / `gpt-6.1-sol` / `gpt-6-luna` (from 2026-10-09; before that `gpt-5.6-sol` / `gpt-5.6-terra` / `gpt-5.6-luna`). Codex `0.159.0` or newer is required on a ChatGPT sign-in: older releases refuse `gpt-6.1-sol` with a 400. | Maps the driver's abstract `opus`/`sonnet`/`haiku` tiers onto the codex ladder, the way the anthropic engine maps onto its own (ruling 2026-09-02, superseding the single-model mapping). A tier that cannot hold its stage contract announces itself at that stage — `missing:negative-results`, `no_coverage_status_row`, `named_band_missing` in `_driver/<stage>.jsonl` — not at delivery. |
194
+ | `CLEAROTRON_OPENAI_MODEL_JUDGMENT` / `CLEAROTRON_OPENAI_MODEL_SWEEP` / `CLEAROTRON_OPENAI_MODEL_CHEAP` | `gpt-6.1-sol` / `gpt-6.1-terra` / `gpt-6-luna` (from 2026-10-28; from 2026-10-09 the sweep tier shared `gpt-6.1-sol`, and before that they were `gpt-5.6-sol` / `gpt-5.6-terra` / `gpt-5.6-luna`). Codex `0.159.0` or newer is required on a ChatGPT sign-in: older releases refuse `gpt-6.1-sol` with a 400. | Maps the driver's abstract `opus`/`sonnet`/`haiku` tiers onto the codex ladder, the way the anthropic engine maps onto its own (ruling 2026-09-02, superseding the single-model mapping). A tier that cannot hold its stage contract announces itself at that stage — `missing:negative-results`, `no_coverage_status_row`, `named_band_missing` in `_driver/<stage>.jsonl` — not at delivery. |
195
195
  | ↳ why they are three, and what the old measurement still says | superseded 2026-09-02, not erased | The three defaulted to `sol` alone on a MEASUREMENT, never a placeholder: on 2026-08-11/12, with `SWEEP=gpt-5.6-terra` / `CHEAP=gpt-5.6-luna`, every codex clearance died on structural-output gates — `missing:negative-results`, `no_coverage_status_row`, `named_band_missing` — over 2 scenarios × 3 stage families, **byte-identically on retry**, so no retry budget rescued it. That measurement stands for the code and the codex CLI **of that date**, and it is why the judgment tier keeps `sol`. Three weeks of stage-contract work and several codex minor versions sit between it and the 2026-09-02 ruling that split the three. **The symptom to match against this cause is unchanged**: those same three gates, as a fail row in`_driver/<stage>.jsonl`, inside the first few dispatches of a clearance and identical on every retry — a model incapable of a contract is not flaky at it. It reads as an engine bug when the only fault is this configuration. A cheap-tier experiment still belongs in the three constants in `driver/engine/openai-agent.mjs` where a reviewer sees it, not in an untraceable env line. |
196
196
  | `CLEAROTRON_DATABASE` | **REQUIRED — no default.** `corsearch` \| `clarivate` \| `signa` \| `euipo` \| `uspto-local` \| `free-tier` | Active register provider — one per run, set in the environment the deploy carries, in every environment including production. There is **no default**: unset resolves to `null` and every use of it throws, so a run refuses at start rather than calling a vendor nobody chose (the removed `corsearch` fallback named a vendor the deployment did not choose). Three are paid global sweeps; `euipo` (EU) and `uspto-local` (US) are free single-office sources, and `free-tier` composes those two as one register. Choosing a free value makes every territory outside its coverage a disclosed *deferred* row. Unknown ids throw loudly, and the gather layer throws at stage time for any provider without a built MCP server. |
197
197
  | `CLEAROTRON_CODEX_SANDBOX_BYPASS` | unset | `1` runs `codex exec` with its own sandbox bypassed, for hosts where that sandbox cannot run (setup and `doctor --probe-engine` report it). It removes a defence: with it set, a stage's commands run with every permission of the account Clearotron runs as, not only in the run folder. Codex has no guard against a stage writing into the run's `_driver/` folder, with the sandbox or without it. Set it because the host forces it, never for convenience. |
@@ -161,7 +161,7 @@ structural, or dev seam); [dev] = dev/test seam, never set in prod.
161
161
  `CLEAROTRON_AI` (anthropic-agent | openai-agent; default anthropic-agent), `CLEAROTRON_AI_BILLING`
162
162
  (subscription|api-key|cloud; cloud is Claude only), `CLEAROTRON_CODEX_PATH`,
163
163
  `CLEAROTRON_OPENAI_AUTH_FILE`, `CLEAROTRON_OPENAI_MODEL_JUDGMENT` / `CLEAROTRON_OPENAI_MODEL_SWEEP` /
164
- `CLEAROTRON_OPENAI_MODEL_CHEAP` (gpt-6.1-sol / gpt-6.1-sol / gpt-6-luna),
164
+ `CLEAROTRON_OPENAI_MODEL_CHEAP` (gpt-6.1-sol / gpt-6.1-terra / gpt-6-luna),
165
165
  `CLEAROTRON_CLAUDE_PATH` (claude on PATH, then the copy Clearotron installed),
166
166
  `CLEAROTRON_SYNTHESIS_MODEL` (opus), `CLEAROTRON_KNOCKOUT_MODEL` (opus),
167
167
  `CLEAROTRON_MAX_BUDGET_USD` (unset).
@@ -1,5 +1,17 @@
1
1
  # clearotron-driver
2
2
 
3
+ ## 0.5.0-beta.5
4
+
5
+ ### Patch Changes
6
+
7
+ - Fixed: When a register search fails, the report's coverage still accounts for the unread searches beside it, instead of leaving them out.
8
+ - Fixed: When the search for an owner's use of a mark could not run, the report no longer says that search found nothing.
9
+ - Fixed: A register entry the engine placed on the watchlist now appears in the audit trail with its reason, instead of being left out.
10
+ - Fixed: A citation of a finding the review withdrew, in the report's summary panel or methodology, is now sent back for correction before delivery.
11
+ - Fixed: A clearance that searched common law or the United Kingdom is no longer flagged as claiming a search it did not run.
12
+ - New: Searches now use the latest models: Claude Haiku 5.5, Sonnet 5.5 and Opus 5.5, and OpenAI's GPT-6.1 Sol, GPT-6.1 Terra and GPT-6 Luna.
13
+ - Fixed: When two parties use the same name without a registration, the report keeps a finding for each, instead of keeping only the last one saved.
14
+
3
15
  ## 0.5.0-beta.4
4
16
 
5
17
  ### Patch Changes
@@ -171,7 +171,7 @@ export const E3_BACKLOG = [
171
171
  {
172
172
  stage: "common-law",
173
173
  kind: "dictated-line-shape",
174
- where: "driver/stages.mjs:1103 (emitted at 1891, 1920, 2100, 2320)",
174
+ where: "driver/stages.mjs:1104 (emitted at 1891, 1920, 2100, 2320)",
175
175
  surface: "stage-message",
176
176
  evidence: "CROSS-CHECK HAND-OFF: … record it on its OWN line in EXACTLY the form \"CROSS-CHECK REQUIRED: <what> — <why>\" (that exact prefix; an em-dash between what and why; name the mark in CAPS in <what>). The driver parses ONLY this exact line shape…",
177
177
  reparsedBy: "driver/doubt-ledger.mjs:183 CROSS_CHECK_RE = /^(?:[-*]\\s+)?CROSS-CHECK REQUIRED:\\s*(.+?)\\s+—\\s+(.+?)\\s*$/ → mintCrossCheckDoubts",
@@ -265,7 +265,7 @@ export const E3_BACKLOG = [
265
265
  {
266
266
  stage: "register-unit",
267
267
  kind: "exactly-these-keys",
268
- where: "driver/stages.mjs:2271",
268
+ where: "driver/stages.mjs:2272",
269
269
  surface: "stage-message",
270
270
  evidence: "Every block you append MUST carry \"state\":\"enumerated\" (ONLY if you paged it to has_more:false) or \"state\":\"incomplete\" — EXACTLY those two strings; there is no \"verified\"/\"checked\"/\"complete\"/\"clean\" state, and any other value fails the stage.",
271
271
  reparsedBy: "driver/named-band.mjs parseNamedBand (off-enum state fails validators.registerUnit)",
@@ -283,7 +283,7 @@ export const E3_BACKLOG = [
283
283
  {
284
284
  stage: "register-unit",
285
285
  kind: "exactly-these-keys",
286
- where: "driver/stages.mjs:4277 (the frame-reopen / scoped-retry message builder). A second number stood here and had been stale for some time: it pointed at a contract-element description rather than a builder, at its old line and at every mechanical shift of it. Two candidate builders sit beside 4164 and picking one would be a guess, so the wrong pointer is removed rather than moved a third time — one accurate citation beats one accurate and one invented.",
286
+ where: "driver/stages.mjs:4280 (the frame-reopen / scoped-retry message builder). A second number stood here and had been stale for some time: it pointed at a contract-element description rather than a builder, at its old line and at every mechanical shift of it. Two candidate builders sit beside 4164 and picking one would be a guess, so the wrong pointer is removed rather than moved a third time — one accurate citation beats one accurate and one invented.",
287
287
  surface: "stage-message",
288
288
  evidence: "Every block you append MUST carry \"state\":\"enumerated\" (ONLY if paged to has_more:false) or \"state\":\"incomplete\" — EXACTLY those two strings … (re-dispatch builders, which REPLACE def.message)",
289
289
  reparsedBy: "driver/named-band.mjs parseNamedBand. Scope warning: these builders replace def.message on every escalation / envelope-close / frame-reopen dispatch, so an E3 lint that walks STAGES[*].message only never sees them",
@@ -524,7 +524,7 @@ export const E3_BACKLOG = [
524
524
  kind: "literal-json-skeleton",
525
525
  where: "driver/stages.mjs:3294",
526
526
  surface: "stage-message",
527
- evidence: "use_check = {\"source\",\"quality\"}: … quality: OPTIONAL, EXACTLY one of owner-site / independent / register-mirror … own_rights = {\"source\"}",
527
+ evidence: "use_check = {\"source\",\"quality\",\"question\"}: … quality: OPTIONAL, EXACTLY one of owner-site / independent / register-mirror … own_rights = {\"source\"}",
528
528
  reparsedBy: "driver/verify.mjs checkFindingsSibling (finding_use_check_missing); driver/own-rights.mjs:19-22",
529
529
  removedByMove: "NOTHING ON THE #850 PLAN REMOVES THIS",
530
530
  },
@@ -670,7 +670,7 @@ export const E3_BACKLOG = [
670
670
  {
671
671
  stage: "narrative-refutation",
672
672
  kind: "dictated-line-shape",
673
- where: "driver/stages.mjs:4832",
673
+ where: "driver/stages.mjs:4835",
674
674
  surface: "stage-message",
675
675
  // RE-QUOTED BY CONVERSION 9. The LINE-TOKEN half is gone — no "anywhere on the line", no "[kind: …]
676
676
  // token", no "a line with no token is treated as fact", because a kind is a typed field now and an
@@ -769,7 +769,7 @@ export const E3_BACKLOG = [
769
769
  {
770
770
  stage: "report-card",
771
771
  kind: "literal-json-skeleton",
772
- where: "driver/stages.mjs:3966",
772
+ where: "driver/stages.mjs:3968",
773
773
  surface: "stage-message",
774
774
  evidence: "The finding's OWN record — the ONLY source for this card …:\\n```json\\n<JSON.stringify(finding, null, 2)>\\n```",
775
775
  reparsedBy: "none — this is the INPUT side, and that is why it belongs in the survey: a full JSON object rendered into the prompt is exactly the mechanism #850 proves produced R-RECEIPT (the model pattern-matches a shown shape). E3's clause 1 as written (\"a code fence or inline example showing the exact object shape the model must emit\") does not reach an injected record, so the lint needs an explicit rule for shown-but-not-owed structure",
@@ -373,7 +373,7 @@ export function coverageFormRows({ skeleton = [], activeAxes = null, plan = null
373
373
  return r ? { narrowing: r.narrowing, ...(r.lists?.length ? { lists: r.lists } : {}), ...(r.not_covered?.length ? { not_covered: r.not_covered } : {}) } : {};
374
374
  };
375
375
  const entriesByQid = new Map((plan?.entries ?? []).map((e) => [e.qid, e]));
376
- const open = openBlocksByAxis(skel, bandBlocksByAxis, plan);
376
+ const open = openBlocksByAxis(skel, bandBlocksByAxis, plan, { withDeferred: true });
377
377
  // Axis order is the run's, not a set's iteration order: the plan-execution skeleton first (the order
378
378
  // the plan dictated), then any active axis the skeleton does not name, then the canonical axis list as
379
379
  // the final tie-break. Deterministic ⇒ the same run regenerates a byte-identical form every pass.
@@ -30,7 +30,7 @@ import { parsePrRiskResults, connotationObligations, renderConnotationObligation
30
30
  import { obligationsSidecarPath } from "../../disposition-tool.mjs";
31
31
  import { writeGridProvenance } from "./grid-provenance.mjs"; // — what served the grid, beside the ledger
32
32
  import { insideRun } from "../../../shared/inside-run.mjs"; // a path a tool is handed is served only inside this run
33
- import { keepResearchAnswer, researchReplayOn, savedResearchAnswer } from "../../research-answers.mjs"; // each answer's text, kept for a later attempt of its stage; a replay answers from them
33
+ import { keepResearchAnswer, isResearchAnswer, replayAnswersFrom, researchReplayOn, savedResearchAnswer } from "../../research-answers.mjs"; // each answer's text, kept for a later attempt of its stage; a replay answers from them
34
34
 
35
35
  const API_KEY = process.env.PERPLEXITY_API_KEY || "";
36
36
 
@@ -222,7 +222,14 @@ function judgeBlock(cands, part, gridSpecPath, reserve = 0) {
222
222
  async function research(params) {
223
223
  // A REPLAY SENDS NOTHING OUT, so it is answered before the key is asked for: an offline replay needs none.
224
224
  if (researchReplayOn(RUN_DIR)) return replayResearch(params);
225
- if (!API_KEY) return "ERROR: PERPLEXITY_API_KEY not set — marketplace research unavailable.";
225
+ // A REFUSED QUESTION IS LOGGED as a failed one is. It left no line, so the run's record could not tell a question
226
+ // refused for want of a credential from one never asked, and a writer whose every question was refused wrote
227
+ // "no result" with nothing in the log to say otherwise. The reason names the refusal, never the key.
228
+ if (!API_KEY) {
229
+ logCall("perplexity_research", { task: params?.task ?? "", ...(params?.grid_spec_path ? { grid: true } : {}) },
230
+ { ok: false, error: "refused: no research credential" });
231
+ return "ERROR: PERPLEXITY_API_KEY not set — marketplace research unavailable.";
232
+ }
226
233
  const { task, depth, model: modelOverride, allow_fetch = false, domain_filter, enable_sandbox = false, response_schema, schema_name, grid_spec_path, part } = params;
227
234
 
228
235
  // ── Deterministic grid mode: the caller dictates the cells; the tool runs + captures them. ──
@@ -305,7 +312,11 @@ async function research(params) {
305
312
  const data = await callAgentAPI(API_KEY, body);
306
313
  keepRaw("question", { task, preset }, data);
307
314
  const text = enable_sandbox ? formatSandboxResponse(data, preset) : formatResponse(data, preset);
308
- logCall("perplexity_research", asked, { ok: true, bytes: String(text ?? "").length, citations: citationCount(data) });
315
+ // A REPLY THE FORMATTER COULD NOT READ IS NO ANSWER, and is logged as a failure: it was logged as answered, beside
316
+ // completed searches, though it is not kept and is asked again (research-answers.mjs).
317
+ logCall("perplexity_research", asked, isResearchAnswer(text)
318
+ ? { ok: true, bytes: String(text ?? "").length, citations: citationCount(data) }
319
+ : { ok: false, error: String(text ?? "").slice(0, 200) });
309
320
  keepResearchAnswer(RUN_DIR, { session: SESSION, agent: AGENT, task, preset, text });
310
321
  return text;
311
322
  } catch (err) {
@@ -321,16 +332,18 @@ async function research(params) {
321
332
  //
322
333
  // In the experiment sandbox under `--research replay` (research-answers.mjs), a question the run already asked
323
334
  // gets the answer the run got, and any other question, a grid among them, is refused: nothing goes out to the
324
- // research service. Both are logged as a lookup is, marked as a replay, so the arm's reading log says which.
335
+ // research service. Both are logged as a lookup is, marked as a replay, so the arm's reading log says which. The answers
336
+ // are read from the copy the marker names, outside the arm's folder, where the stage's own commands cannot reach them.
325
337
  function replayResearch(params) {
326
338
  const { task, grid_spec_path } = params ?? {};
327
339
  if (grid_spec_path) {
328
340
  logCall("perplexity_research", { task: task ?? "", grid: true, replay: true }, { ok: false, error: "replay: grid not replayed" });
329
341
  return "ERROR: replay — this replays the run's saved research answers and runs no grid; nothing was sent to the research service.";
330
342
  }
331
- const saved = savedResearchAnswer(RUN_DIR, task);
343
+ const from = replayAnswersFrom(RUN_DIR);
344
+ const saved = savedResearchAnswer(from, task);
332
345
  if (!saved) {
333
- logCall("perplexity_research", { task: task ?? "", replay: true }, { ok: false, error: "replay: no saved answer" });
346
+ logCall("perplexity_research", { task: task ?? "", replay: true }, { ok: false, error: from ? "replay: no saved answer" : "replay: the marker names no answers file" });
334
347
  return "ERROR: replay — the run asked no question worded exactly like this one, so there is no saved answer to give; nothing was sent to the research service.";
335
348
  }
336
349
  logCall("perplexity_research", { task, replay: true }, { ok: true, bytes: saved.text.length, replayed: true });
@@ -40,25 +40,28 @@ const codexBin = () => { const r = resolveEngineProgram("openai-agent"); return
40
40
 
41
41
  // tier/alias → codex `-m` model id. opus/sonnet/haiku are the driver's abstract tiers (CONTRACT §3). The
42
42
  // GPT ids are ENV-OVERRIDABLE and default to the vendor's current models — `gpt-6.1-sol` for the judgment
43
- // and sweep tiers, `gpt-6-luna` for the cheap tier (the GPT-6 ladder, below) — so an out-of-the-box run
44
- // uses REAL, current ids rather than invented guesses, and a stage's declared tier means the same thing
45
- // on both engines. Until 2026-10-09 they were `gpt-5.6-sol`, `gpt-5.6-terra` and `gpt-5.6-luna`; `sol`
46
- // was what codex itself reported as its own default (live-probed 2026-07-27: `model: gpt-5.6-sol,
47
- // provider: openai`), which is why the judgment tier kept it.
43
+ // tier, `gpt-6.1-terra` for the sweep tier and `gpt-6-luna` for the cheap tier (the GPT-6 ladder, below) —
44
+ // so an out-of-the-box run uses REAL, current ids rather than invented guesses, and a stage's declared tier
45
+ // means the same thing on both engines. Until 2026-10-09 they were `gpt-5.6-sol`, `gpt-5.6-terra` and
46
+ // `gpt-5.6-luna`; `sol` was what codex itself reported as its own default (live-probed 2026-07-27:
47
+ // `model: gpt-5.6-sol, provider: openai`), which is why the judgment tier kept it.
48
48
  //
49
- // ── THE GPT-6 LADDER (ruling 2026-10-09) ────────────────────────────────────────────────────────────────
49
+ // ── THE GPT-6 LADDER (2026-10-09; the sweep tier moved 2026-10-28) ──────────────────────────────────────
50
50
  //
51
51
  // The owner asked for the latest models on both engines, cheaper and faster. The vendor's price list,
52
52
  // read 2026-10-09 (standard tier, per million tokens in / out): `gpt-6.1-sol` $2 / $10, `gpt-6-luna`
53
- // $0.10 / $0.50, `gpt-6-astra` $10 / $50. The 5.6 ladder it replaces was sol $4 / $20, terra $2 / $12,
54
- // luna $0.20 / $1.20. There is no GPT-6 middle model and Sol 6.1 costs less than Terra 5.6, so the sweep
55
- // tier takes Sol as well; Astra is the premium model, neither cheaper nor faster, and is not a rung.
53
+ // $0.10 / $0.50, `gpt-6-astra` $10 / $50. The 5.6 ladder it replaced was sol $4 / $20, terra $2 / $12,
54
+ // luna $0.20 / $1.20. With no GPT-6 middle model then, the sweep tier shared Sol. `gpt-6.1-terra`
55
+ // ($1 / $5, released 2026-10-26) is that middle model, and the owner asked again for the latest models,
56
+ // so the sweep tier takes it. Astra is the premium model, neither cheaper nor faster, and is not a rung.
56
57
  //
57
58
  // THE CODEX FLOOR MOVED WITH IT. On ChatGPT-account sign-in (subscription billing) codex 0.156.1, 0.157.0
58
59
  // and 0.158.0 refuse `gpt-6.1-sol` with a 400 — "not supported when using Codex with a ChatGPT account" —
59
60
  // and 0.159.0 to 0.162.0 accept it; `gpt-6-luna` is accepted from 0.156.1. Measured 2026-10-09 with
60
61
  // one-word turns on the test box. Below 0.159.0 the judgment tier stops at its first stage, so the floor
61
- // in ENGINE_BINARIES (driver.config.mjs) is 0.159.0.
62
+ // in ENGINE_BINARIES (driver.config.mjs) is 0.159.0. `gpt-6.1-terra` needs 0.159.0 or later by the
63
+ // vendor's Codex model page, and 0.162.0 served it on a ChatGPT sign-in (measured 2026-10-28), so the
64
+ // floor did not move for it.
62
65
  //
63
66
  // The August measurement below still names the symptom to watch for when a tier moves to a cheaper model.
64
67
  //
@@ -108,7 +111,7 @@ const codexBin = () => { const r = resolveEngineProgram("openai-agent"); return
108
111
  // story. They fall through to the throw below, which is where they always should have been. Concrete
109
112
  // gpt-*/o* ids still pass through.
110
113
  const JUDGMENT = process.env.CLEAROTRON_OPENAI_MODEL_JUDGMENT || "gpt-6.1-sol"; // opus — was gpt-5.6-sol
111
- const SWEEP = process.env.CLEAROTRON_OPENAI_MODEL_SWEEP || "gpt-6.1-sol"; // sonnet — was gpt-5.6-terra
114
+ const SWEEP = process.env.CLEAROTRON_OPENAI_MODEL_SWEEP || "gpt-6.1-terra"; // sonnet — was gpt-6.1-sol, and gpt-5.6-terra before 2026-10-09
112
115
  const CHEAP = process.env.CLEAROTRON_OPENAI_MODEL_CHEAP || "gpt-6-luna"; // haiku — was gpt-5.6-luna
113
116
  const OPENAI_MODEL = { opus: JUDGMENT, sonnet: SWEEP, haiku: CHEAP };
114
117
  export function openaiModel(model) {
@@ -791,8 +791,21 @@ const ACTION_KEYS = ["id", "kind", "text", "ordinals", "deadline", "condition"];
791
791
  // without them parses clean. The parser is SHAPE-only here (object / key-allowlist / source-is-a-string);
792
792
  // the composite>=3 NON-EMPTY-source gate lives in verify.mjs checkFindingsSibling (it carries the ordinal
793
793
  // context and is a validator-fail, not a parser-throw, on the offline unit path).
794
- const USE_CHECK_KEYS = ["source", "quality"]; // A4 — optional model-attested source class, code-audited
794
+ // A4 — `quality`, the optional model-attested source class, code-audited. `question` names the research question whose
795
+ // completed answer a no-result rests on; the writer's record joins it to the answers the writer holds (synthesis-record.mjs).
796
+ const USE_CHECK_KEYS = ["source", "quality", "question"];
795
797
  export const USE_SOURCE_QUALITY = ["owner-site", "independent", "register-mirror"];
798
+ // D7 — `use_check.source` carries exactly ONE "we searched and found nothing" value, a code-owned sentinel the
799
+ // writer's message dictates. The writer's record refuses it when the run holds no completed search to have returned
800
+ // it, so the record must read it as the report does. MATCHED ON NORMALISED PUNCTUATION, NOT ONE SPELLING: the seat
801
+ // emitted a hyphen where the doctrine writes an em dash, and exact equality let the raw tool name through to a
802
+ // delivered report twice. Dash class and whitespace fold to one form on both sides; anything else is a source string.
803
+ // The frozen renderer (publish/render.mjs) keeps its own copy of this match, and a test holds the two the same.
804
+ export const USE_CHECK_NO_RESULT = "perplexity_research — no result";
805
+ export const isUseCheckNoResult = (s) => {
806
+ const norm = (x) => String(x ?? "").replace(/[‐-―−-]+/g, "-").replace(/\s+/g, " ").trim().toLowerCase();
807
+ return norm(s) === norm(USE_CHECK_NO_RESULT);
808
+ };
796
809
  const OWN_RIGHTS_KEYS = ["source"];
797
810
  const DEADLINE_KEYS = ["kind", "date"]; // #6 — a CLIENT-facing time-critical date (opposition / SOU / renewal)
798
811
 
@@ -12,6 +12,7 @@
12
12
  import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
13
13
  import { join, dirname, basename } from "node:path";
14
14
  import { driverDir } from "../shared/driver-dir.mjs"; //
15
+ import { NO_SEARCH_RESULT, RESEARCH_CORRECTION } from "./research-answers.mjs"; // a search that did not answer supplies no result
15
16
  import { tmpdir } from "node:os";
16
17
  import { config, resolveModel, modelFamily, modelSnapshotKind, envOn, envGateOn, preflightEngineBinary, olderThanFloor, ENGINE_BINARIES } from "./driver.config.mjs";
17
18
  import { probeCliVersion } from "./engine/cli-version.mjs";
@@ -2375,8 +2376,18 @@ const CLEARANCE_FINDINGS_TOKEN_RE = /(?<!knockout_|knockoutassess_)findings?_/;
2375
2376
  // of the written files, and the driver then holds the record that save left: the retry sends only the change, in the
2376
2377
  // patch the follow-ups above it ask for. A record nobody saved yet, or one whose version is being replaced, still
2377
2378
  // takes the complete save.
2378
- const HELD_RECORD_RETRY = "Fix exactly that in ONE `record_synthesis` PATCH call, sending only the change: `findings_patch` carrying the complete corrected finding object(s), each with the `ordinal` it replaces, and `findings` carrying any register you corrected, complete, and never a `findings[]` list; leave the narrative prose unchanged";
2379
+ // The patch a held record takes, in the tool's own words; the retry for most findings checks leaves the narrative as it is.
2380
+ const HELD_RECORD_PATCH = "ONE `record_synthesis` PATCH call, sending only the change: `findings_patch` carrying the complete corrected finding object(s), each with the `ordinal` it replaces, and `findings` carrying any register you corrected, complete, and never a `findings[]` list";
2381
+ const HELD_RECORD_RETRY = `Fix exactly that in ${HELD_RECORD_PATCH}; leave the narrative prose unchanged`;
2379
2382
  const VERSION_FAILURE_RE = /finding_legacy_scale_forbidden|schema_version/;
2383
+ // A USE CHECK'S CORRECTION corrects what depends on the finding too, so it never says to leave the narrative as it is
2384
+ // (design, 2026-10-28). A held record takes the patch, in the tool's own words, with the narrative sections it changes;
2385
+ // a record not yet accepted is completed by its first call, which the line after every hint names. The caller prints a
2386
+ // full stop after the hint, so the ruled sentences go without their last one.
2387
+ const USE_CHECK_TOKEN_RE = /finding_use_check_(missing|source_missing):/;
2388
+ const useCheckCorrection = (heldRecord) => heldRecord
2389
+ ? `${RESEARCH_CORRECTION} Send it in ${HELD_RECORD_PATCH}. In \`narrative\`, send only the sections you changed — the rest are held`
2390
+ : RESEARCH_CORRECTION.replace(/\.$/, "");
2380
2391
  /**
2381
2392
  * Does the driver hold an accepted synthesis record for the run these files belong to? Read from either of the files
2382
2393
  * the writer's call renders: the stage's own expected file is the narrative, and a caller may name the findings.
@@ -2433,6 +2444,10 @@ export function correctionHint(lastFail, { gridLedgerName = "common-law-grid.jso
2433
2444
  // The framework's method (framework-method.mjs). Each refusal already names its fix — the value list,
2434
2445
  // or the band the table gives — so no addendum: the generic band hint below would point elsewhere.
2435
2446
  extra = "";
2447
+ } else if (USE_CHECK_TOKEN_RE.test(lastFail)) {
2448
+ // A use status with no source behind it. Where no search completed, the honest record is an unknown use,
2449
+ // never a no-result, so the repair is handed the ruled sentence for a search that did not answer, unchanged.
2450
+ extra = ` ${NO_SEARCH_RESULT}`;
2436
2451
  } else if (/finding_band_|findings_rated_under_|finding_disposition_missing/.test(lastFail)) {
2437
2452
  extra = ` SPECIFIC FIX: every finding carries "disposition"; adversarial / coexistence-partner / ` +
2438
2453
  `distinguished findings are RATED and carry "band" = EXACTLY one of the framework's band words (as ` +
@@ -2482,7 +2497,8 @@ export function correctionHint(lastFail, { gridLedgerName = "common-law-grid.jso
2482
2497
  `registration — never overwrite or transpose another's facts; a finding with no fetched record → []), the four ` +
2483
2498
  `meters as {token, basis} (basis = verified-from-record | inferred-from-signal), quadrant {x,y} in [0,1], a ` +
2484
2499
  `typed source, a disposition, and a unique 1-based ordinal. The failed check was: ${tok}.${extra} ` +
2485
- (heldRecord && !VERSION_FAILURE_RE.test(lastFail) ? HELD_RECORD_RETRY
2500
+ (USE_CHECK_TOKEN_RE.test(lastFail) ? useCheckCorrection(heldRecord)
2501
+ : heldRecord && !VERSION_FAILURE_RE.test(lastFail) ? HELD_RECORD_RETRY
2486
2502
  : `Fix exactly that, re-save the COMPLETE findings.json, and leave the narrative prose unchanged`);
2487
2503
  } else if (/coverage_status_offenum/.test(lastFail)) {
2488
2504
  // D1 — the off-enum PROSE Status cell (distinct from coverage_status_invalid, which is the JSON
@@ -37,11 +37,19 @@
37
37
  // cite is enforced in that it is always PRESENT and always the driver's, never in the sense that a
38
38
  // missing one can withhold a report. Nothing in this module throws. A provider outage produces rows that
39
39
  // say the query did not answer, and the run publishes.
40
+ //
41
+ // ── 2026-10-28: A LOOKUP THAT DID NOT ANSWER IS NOT A "NO RESULT" ──────────────────────────────────────
42
+ //
43
+ // The row's source was that literal for a failed lookup and for a completed answer with no link alike, and the card
44
+ // printed it: neither shows a search that found nothing. A row now keeps the link it has and nothing in its place, and
45
+ // its `ok` says whether the lookup answered. The card's words for the two are the owner's (render-knockout.mjs). A
46
+ // reply the formatter could not read is a lookup that did not answer.
40
47
  import { existsSync, readFileSync, writeFileSync, appendFileSync, mkdirSync } from "node:fs";
41
48
  import { join } from "node:path";
42
49
  import { createHash } from "node:crypto";
43
50
 
44
- import { promotableRecords } from "./publish/render-knockout.mjs";
51
+ import { promotableRecords, ownerCheckOutcome, ownerCheckText, USE_CHECK_UNAVAILABLE } from "./publish/render-knockout.mjs";
52
+ import { isResearchAnswer } from "./research-answers.mjs"; // a reply the formatter could not read is no answer
45
53
 
46
54
  /** The literal the clearance lane uses for an honest non-answer. Copied, not re-worded. */
47
55
  export const NO_RESULT = "perplexity_research — no result";
@@ -124,8 +132,8 @@ export function firstSourceUrl(text) {
124
132
  * `exec` is the sweep's own executor — injected, so the fixture path that makes this product testable at
125
133
  * $0 covers this call too and a test never reaches a provider.
126
134
  *
127
- * Each row carries `source`: a URL when the payload gave one, otherwise the clearance lane's honest
128
- * literal. A row is written for EVERY owner owed a check, including the ones that failed — an owner with
135
+ * Each row carries `source`: a URL when the payload gave one, otherwise null, and `ok`: whether the lookup
136
+ * answered. A row is written for EVERY owner owed a check, including the ones that failed — an owner with
129
137
  * no row would be indistinguishable from an owner nobody owed a check to, which is the absence-reads-as-
130
138
  * a-pass shape this repository keeps paying for.
131
139
  *
@@ -154,7 +162,7 @@ export async function runOwnerChecks({ owners, exec, runDir, ledgerPath = null,
154
162
  writeFileSync(join(rawDir, ownerPayloadFile(o).replace(/\.md$/, ".json")), JSON.stringify({ mark: o.mark, owner: o.owner, query, items: r.raw }, null, 1) + "\n");
155
163
  } catch { /* the ledger row is the receipt; a raw-results write never fails a lookup */ }
156
164
  }
157
- const ok = Boolean(r?.ok && r?.text);
165
+ const ok = Boolean(r?.ok && isResearchAnswer(r?.text));
158
166
  let payloadFile = null;
159
167
  if (ok && runDir) {
160
168
  // The payload lands beside the mark payloads, under a name derived from the row rather than the
@@ -162,7 +170,7 @@ export async function runOwnerChecks({ owners, exec, runDir, ledgerPath = null,
162
170
  payloadFile = ownerPayloadFile(o);
163
171
  try { writeFileSync(join(runDir, "research", payloadFile), r.text); } catch { payloadFile = null; }
164
172
  }
165
- const source = ok ? (firstSourceUrl(r.text) ?? NO_RESULT) : NO_RESULT;
173
+ const source = ok ? firstSourceUrl(r.text) : null;
166
174
  const row = {
167
175
  mark: o.mark, owner: o.owner, recordIds: o.recordIds, classes: o.classes,
168
176
  query, preset, ok, source, payloadFile,
@@ -206,7 +214,8 @@ export function useCheckLineFor(ownerChecks, recordId) {
206
214
  const row = (Array.isArray(ownerChecks) ? ownerChecks : [])
207
215
  .find((c) => (Array.isArray(c?.recordIds) ? c.recordIds : []).some((x) => String(x).trim() === id));
208
216
  if (!row) return null;
209
- return `${USE_CHECK_LABEL} ${row.source}`;
217
+ const o = ownerCheckOutcome(row);
218
+ return o.kind === "unavailable" ? USE_CHECK_UNAVAILABLE : `${USE_CHECK_LABEL} ${ownerCheckText(o)}`;
210
219
  }
211
220
 
212
221
  /** Read the store a run wrote, tolerantly. `[]` when the lane did not run — never a throw. */
@@ -2,7 +2,7 @@
2
2
  "name": "clearotron-driver",
3
3
  "private": true,
4
4
  "type": "module",
5
- "version": "0.5.0-beta.4",
5
+ "version": "0.5.0-beta.5",
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": {