clearotron 0.4.0 → 0.4.1-beta.0
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/THIRD-PARTY-NOTICES.md +2 -2
- package/build-info.json +2 -2
- package/docs/SECURITY-OWASP.md +21 -13
- package/docs/SECURITY.md +20 -14
- package/docs/architecture/04-configuration-reference.md +3 -2
- package/docs/architecture/05-config-governance.md +1 -1
- package/docs/architecture/06-operations-runbook.md +1 -1
- package/docs/architecture/09-security-and-data.md +16 -11
- package/driver/CHANGELOG.md +18 -0
- package/driver/binding-layers.mjs +28 -6
- package/driver/citation-census.json +3 -3
- package/driver/common-law-coverage-status.mjs +52 -0
- package/driver/common-law-receipts.mjs +1 -1
- package/driver/connotation-search.mjs +5 -5
- package/driver/contract-dictation-registry.mjs +1 -1
- package/driver/contract-e3-backlog.mjs +1 -1
- package/driver/contract-vocabulary.mjs +18 -18
- package/driver/degraded-parts.mjs +10 -3
- package/driver/dispatch-record.mjs +19 -0
- package/driver/doubt-closure-tool.mjs +2 -2
- package/driver/driver.config.mjs +39 -6
- package/driver/engagement-receipt.mjs +1 -1
- package/driver/engine/CONTRACT.md +41 -1
- package/driver/engine/anthropic-agent.mjs +56 -4
- package/driver/engine/mcp/codex-config.mjs +1 -1
- package/driver/engine/mcp/coverage-server.mjs +1 -1
- package/driver/engine/mcp/gather-config.mjs +1 -1
- package/driver/engine/mcp/recording-server.mjs +31 -4
- package/driver/engine/mcp/stdio-server.mjs +45 -7
- package/driver/engine/openai-agent.mjs +100 -17
- package/driver/engine/session-record.mjs +223 -0
- package/driver/enqueue-schema.mjs +2 -2
- package/driver/findings-model.mjs +3 -3
- package/driver/form-neighbourhood.mjs +7 -7
- package/driver/frame-diff-model.mjs +3 -3
- package/driver/framework.mjs +22 -0
- package/driver/gateway.mjs +94 -12
- package/driver/named-band.mjs +2 -0
- package/driver/package.json +1 -1
- package/driver/phase0.mjs +13 -2
- package/driver/pipeline-knockout.mjs +1 -1
- package/driver/pipeline.mjs +46 -33
- package/driver/portal-families.mjs +3 -3
- package/driver/portal-service.mjs +1 -1
- package/driver/predelivery-lint.mjs +19 -19
- package/driver/publish/index.mjs +1 -1
- package/driver/publish/knockout.mjs +5 -4
- package/driver/publish/office-record-links.mjs +17 -2
- package/driver/publish/publish-inputs.mjs +1 -1
- package/driver/publish/render-knockout.mjs +17 -5
- package/driver/publish/render.mjs +4 -2
- package/driver/record-fold.mjs +177 -0
- package/driver/register-count.mjs +18 -1
- package/driver/register-plan.mjs +71 -17
- package/driver/register-records.mjs +7 -0
- package/driver/registry-fidelity.mjs +2 -2
- package/driver/repairs.mjs +1 -1
- package/driver/report-card-record.mjs +4 -4
- package/driver/scope-facts.mjs +11 -3
- package/driver/search-policy.mjs +20 -1
- package/driver/skills/clearance-common-law/SKILL.md +22 -22
- package/driver/skills/clearance-register/digest.md +3 -3
- package/driver/skills/clearance-register/register-recipes.md +11 -11
- package/driver/skills/clearance-register/status-rules.md +1 -1
- package/driver/skills/clearance-register/unit.md +1 -1
- package/driver/skills/clearance-search/firm-wide-reasoning.md +3 -2
- package/driver/skills/clearance-search/phase2-execution.md +6 -5
- package/driver/skills/clearance-search/synthesis-rules.md +4 -4
- package/driver/skills/clearance-search/worked-examples.md +39 -39
- package/driver/skills/clearance-variants/SKILL.md +16 -16
- package/driver/skills/frame-diff/SKILL.md +2 -2
- package/driver/stage-context.mjs +27 -1
- package/driver/stages.mjs +12 -12
- package/driver/suite-census.json +175 -31
- package/driver/synthesis-record.mjs +5 -0
- package/driver/unit-inventory.mjs +17 -28
- package/driver/verify.mjs +6 -6
- package/mcp-server/CHANGELOG.md +4 -0
- package/mcp-server/lib/evidence.mjs +8 -8
- package/mcp-server/lib/knockout.mjs +1 -1
- package/mcp-server/package.json +2 -2
- package/package.json +2 -2
- package/portal-ui/package.json +1 -1
- package/providers/_shared/count.mjs +4 -1
- package/providers/_shared/enumerate.mjs +61 -22
- package/providers/_shared/execute-plan.mjs +2 -0
- package/providers/corsearch/src/capabilities.js +1 -1
- package/providers/oauth-mcp-bridge/CHANGELOG.md +4 -0
- package/providers/oauth-mcp-bridge/package.json +2 -2
- package/providers/signa/src/capabilities.js +17 -2
- package/providers/signa/src/core.js +234 -37
- package/scripts/band-shape-probe.mjs +2 -2
- package/scripts/deprecate-below.mjs +28 -4
- package/scripts/e2e-first-time.mjs +16 -1
- package/scripts/freeze-example-run.mjs +2 -2
- package/shared/scope.mjs +18 -2
|
@@ -115,8 +115,11 @@ export function serve({ name, version = "0.1.0", tools = [] }) {
|
|
|
115
115
|
// it must be read-only. Nothing here needs to claim a write is a read to get called, and a
|
|
116
116
|
// `readOnlyHint: true` on a tool that writes a ledger would be a lie told to a sandbox for
|
|
117
117
|
// convenience — the exact thing annotations exist to prevent.
|
|
118
|
-
|
|
119
|
-
|
|
118
|
+
// Listed when asked, not at start: a tool may give the schema for the run it serves (`schemaNow`), read
|
|
119
|
+
// at call time like every per-run read here. A tool without one lists its declared schema, as before.
|
|
120
|
+
const list = () => tools.map((t) => ({
|
|
121
|
+
name: t.name, description: t.description,
|
|
122
|
+
inputSchema: typeof t.schemaNow === "function" ? t.schemaNow(t.inputSchema) : t.inputSchema,
|
|
120
123
|
...(t.annotations ? { annotations: t.annotations } : {}),
|
|
121
124
|
}));
|
|
122
125
|
const send = (m) => process.stdout.write(JSON.stringify(m) + "\n");
|
|
@@ -137,7 +140,7 @@ export function serve({ name, version = "0.1.0", tools = [] }) {
|
|
|
137
140
|
} else if (method === "ping") {
|
|
138
141
|
ok(id, {});
|
|
139
142
|
} else if (method === "tools/list") {
|
|
140
|
-
ok(id, { tools: list });
|
|
143
|
+
ok(id, { tools: list() });
|
|
141
144
|
} else if (method === "tools/call") {
|
|
142
145
|
const tool = byName.get(params?.name);
|
|
143
146
|
if (!tool) return ok(id, { isError: true, content: [{ type: "text", text: `unknown tool: ${params?.name}` }] });
|
|
@@ -167,7 +170,7 @@ export function serve({ name, version = "0.1.0", tools = [] }) {
|
|
|
167
170
|
// interpolate a mark, and this row's own contract is the call and never its content. A row that
|
|
168
171
|
// copied the message would leak the day one of them does; a row that copies `unmet` cannot.
|
|
169
172
|
logToolEvent({ event: "settled", seq, server: name, tool: tool.name, ...(axis ? { axis } : {}), ok: false,
|
|
170
|
-
reason: `missing_required:${unmet.join(",")}` });
|
|
173
|
+
failure: "schema", reason: `missing_required:${unmet.join(",")}` });
|
|
171
174
|
return ok(id, { isError: true, content: [{ type: "text", text:
|
|
172
175
|
`${tool.name}_missing_required:${unmet.join(",")} — the schema you were handed declares `
|
|
173
176
|
+ `${unmet.length === 1 ? "this field" : "these fields"} REQUIRED and the call did not carry `
|
|
@@ -177,13 +180,19 @@ export function serve({ name, version = "0.1.0", tools = [] }) {
|
|
|
177
180
|
try { out = await tool.handler(params?.arguments || {}); }
|
|
178
181
|
catch (e) {
|
|
179
182
|
// A THROWN tool error SETTLES. It returned an answer — a wrong one, but the model saw it and
|
|
180
|
-
// could act on it. Recording it as unsettled would attribute a live failure to a timeout.
|
|
181
|
-
|
|
183
|
+
// could act on it. Recording it as unsettled would attribute a live failure to a timeout. The
|
|
184
|
+
// row names the error's class and never its message, which can carry whatever the tool was given.
|
|
185
|
+
logToolEvent({ event: "settled", seq, server: name, tool: tool.name, ...(axis ? { axis } : {}), ok: false,
|
|
186
|
+
failure: "threw", reason: `threw:${errorClass(e)}` });
|
|
182
187
|
return ok(id, { isError: true, content: [{ type: "text", text: `${tool.name} error: ${e?.message ?? e}` }] });
|
|
183
188
|
}
|
|
184
189
|
const isError = typeof out === "object" && out?.isError === true;
|
|
185
190
|
const text = typeof out === "string" ? out : (out?.text ?? JSON.stringify(out));
|
|
186
|
-
|
|
191
|
+
// A tool that answered with an error rejected the call itself, after the schema let it past. The row
|
|
192
|
+
// says so, and carries the complaint's own code when the text opens with one, never the text.
|
|
193
|
+
const code = isError ? complaintCode(text) : null;
|
|
194
|
+
logToolEvent({ event: "settled", seq, server: name, tool: tool.name, ...(axis ? { axis } : {}), ok: !isError,
|
|
195
|
+
...(isError ? { failure: "server", ...(code ? { reason: code } : {}) } : {}) });
|
|
187
196
|
ok(id, { isError: isError || undefined, content: [{ type: "text", text }] });
|
|
188
197
|
} else if (id != null) {
|
|
189
198
|
err(id, -32601, `method not found: ${method}`);
|
|
@@ -205,3 +214,32 @@ export function serve({ name, version = "0.1.0", tools = [] }) {
|
|
|
205
214
|
// so `claude --mcp-config` reports a connected server whose tool calls return a clear isError.
|
|
206
215
|
process.on("uncaughtException", (e) => { process.stderr.write(`[mcp:${name}] ${e?.stack ?? e}\n`); });
|
|
207
216
|
}
|
|
217
|
+
|
|
218
|
+
// ── WHAT REFUSED A CALL, as the settled row names it ───────────────────────────────────────────────────
|
|
219
|
+
//
|
|
220
|
+
// (At the end of this file so that adding it moved no line another file cites.)
|
|
221
|
+
//
|
|
222
|
+
// A settled row with `ok: false` carries `failure`, one of three words for where the call stopped:
|
|
223
|
+
// schema the call did not carry a field the tool's schema requires; refused before the tool ran
|
|
224
|
+
// threw the tool ran and threw
|
|
225
|
+
// server the tool ran and answered with an error of its own
|
|
226
|
+
// So a call refused before its tool ever ran is told apart from one the tool rejected without knowing
|
|
227
|
+
// that the first leaves no call file behind. `reason` is never the message: a message can quote what the
|
|
228
|
+
// tool was given. It is the missing fields, the error's class, or the complaint's own code.
|
|
229
|
+
|
|
230
|
+
/** An error's class name, never its message. PURE. */
|
|
231
|
+
export function errorClass(e) {
|
|
232
|
+
const n = typeof e?.name === "string" && /^[A-Za-z][A-Za-z0-9]*$/.test(e.name) ? e.name : "";
|
|
233
|
+
return n || "Error";
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* The code a tool's complaint opens with, as the acceptors write them (`knockoutframe_classes_plain: …`,
|
|
238
|
+
* `judgment_record_not_held:1:…`): a lower-case word with at least one underscore, read up to the first
|
|
239
|
+
* character that is not part of it. `null` when the text opens any other way, so prose never reaches the
|
|
240
|
+
* row. PURE.
|
|
241
|
+
*/
|
|
242
|
+
export function complaintCode(text) {
|
|
243
|
+
const m = /^([a-z][a-z0-9]*(?:_[a-z0-9]+)+)(?=$|[^a-z0-9_])/.exec(String(text ?? ""));
|
|
244
|
+
return m ? m[1] : null;
|
|
245
|
+
}
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
|
|
25
25
|
import { mkdtempSync, mkdirSync, writeFileSync, copyFileSync, existsSync, rmSync, readFileSync, readdirSync, statSync, symlinkSync, lstatSync, realpathSync } from "node:fs";
|
|
26
26
|
import { everyToolCallRefused } from "./tool-refusal.mjs";
|
|
27
|
+
import { newSessionRecord, noteCodexEvent, sessionSummary, countTool, streamSink, codexEnding } from "./session-record.mjs"; // what the session went through, beside how it ended
|
|
27
28
|
import { writeSecretFile } from "../../shared/secret-file.mjs"; // the rotated login goes back the way every credential is written
|
|
28
29
|
import { tmpdir, homedir } from "node:os";
|
|
29
30
|
import { join, dirname, sep } from "node:path";
|
|
@@ -38,11 +39,28 @@ import { resolveEngineProgram } from "../driver.config.mjs"; // — the one pl
|
|
|
38
39
|
const codexBin = () => { const r = resolveEngineProgram("openai-agent"); return r.resolved ?? r.bin; };
|
|
39
40
|
|
|
40
41
|
// tier/alias → codex `-m` model id. opus/sonnet/haiku are the driver's abstract tiers (CONTRACT §3). The
|
|
41
|
-
// GPT ids are ENV-OVERRIDABLE and default to
|
|
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
|
|
43
44
|
// uses REAL, current ids rather than invented guesses, and a stage's declared tier means the same thing
|
|
44
|
-
// on both engines.
|
|
45
|
-
// `model: gpt-5.6-sol,
|
|
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.
|
|
48
|
+
//
|
|
49
|
+
// ── THE GPT-6 LADDER (ruling 2026-10-09) ────────────────────────────────────────────────────────────────
|
|
50
|
+
//
|
|
51
|
+
// The owner asked for the latest models on both engines, cheaper and faster. The vendor's price list,
|
|
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.
|
|
56
|
+
//
|
|
57
|
+
// THE CODEX FLOOR MOVED WITH IT. On ChatGPT-account sign-in (subscription billing) codex 0.156.1, 0.157.0
|
|
58
|
+
// and 0.158.0 refuse `gpt-6.1-sol` with a 400 — "not supported when using Codex with a ChatGPT account" —
|
|
59
|
+
// 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
|
+
// 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
|
+
//
|
|
63
|
+
// The August measurement below still names the symptom to watch for when a tier moves to a cheaper model.
|
|
46
64
|
//
|
|
47
65
|
// ── THE THREE TIERS MAP ONTO THE CODEX LADDER (ruling 2026-09-02,) ──────────────────────────
|
|
48
66
|
//
|
|
@@ -89,9 +107,9 @@ const codexBin = () => { const r = resolveEngineProgram("openai-agent"); return
|
|
|
89
107
|
// runs SOMETHING is worse than one that refuses: the arm still produces a number and the number is a
|
|
90
108
|
// story. They fall through to the throw below, which is where they always should have been. Concrete
|
|
91
109
|
// gpt-*/o* ids still pass through.
|
|
92
|
-
const JUDGMENT = process.env.CLEAROTRON_OPENAI_MODEL_JUDGMENT || "gpt-
|
|
93
|
-
const SWEEP = process.env.CLEAROTRON_OPENAI_MODEL_SWEEP || "gpt-
|
|
94
|
-
const CHEAP = process.env.CLEAROTRON_OPENAI_MODEL_CHEAP || "gpt-
|
|
110
|
+
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
|
|
112
|
+
const CHEAP = process.env.CLEAROTRON_OPENAI_MODEL_CHEAP || "gpt-6-luna"; // haiku — was gpt-5.6-luna
|
|
95
113
|
const OPENAI_MODEL = { opus: JUDGMENT, sonnet: SWEEP, haiku: CHEAP };
|
|
96
114
|
export function openaiModel(model) {
|
|
97
115
|
if (!model) return undefined;
|
|
@@ -311,8 +329,10 @@ export function parseCodexEvent(line, ev) {
|
|
|
311
329
|
switch (e?.type) {
|
|
312
330
|
case "thread.started": if (e.thread_id) ev.threadId = e.thread_id; break;
|
|
313
331
|
case "turn.completed": ev.turnCompleted = true; if (e.usage) ev.usage = e.usage; break;
|
|
314
|
-
|
|
315
|
-
|
|
332
|
+
// The program's own words are kept apart from the placeholders, so the record never shows a word of
|
|
333
|
+
// ours where it said nothing (session-record.mjs, codexEnding).
|
|
334
|
+
case "turn.failed": ev.turnFailed = e.error?.message || "turn.failed"; ev.turnFailedText = typeof e.error?.message === "string" ? e.error.message : null; break;
|
|
335
|
+
case "error": ev.streamError = e.message || "stream error"; ev.streamErrorText = typeof e.message === "string" ? e.message : null; break;
|
|
316
336
|
case "item.completed":
|
|
317
337
|
if (e.item?.type === "agent_message" && typeof e.item.text === "string") ev.agentText = e.item.text;
|
|
318
338
|
// A FILE CHANGE CODEX ITSELF REPORTS AS FAILED. Measured on codex-cli 0.156.1 with its sandbox unable
|
|
@@ -330,6 +350,7 @@ export function parseCodexEvent(line, ev) {
|
|
|
330
350
|
ev.commandFailures.push({ command: String(e.item.command ?? ""), output: String(e.item.aggregated_output ?? "") });
|
|
331
351
|
}
|
|
332
352
|
noteMcpToolCall(e.item, ev);
|
|
353
|
+
noteToolItem(e.item, ev);
|
|
333
354
|
break;
|
|
334
355
|
// ── — AN MCP CALL THAT WAS REFUSED IS NOT A CALL NOBODY MADE ──────────
|
|
335
356
|
//
|
|
@@ -352,6 +373,7 @@ export function parseCodexEvent(line, ev) {
|
|
|
352
373
|
// would double every call.
|
|
353
374
|
case "item.started":
|
|
354
375
|
noteMcpToolCall(e.item, ev);
|
|
376
|
+
noteToolItem(e.item, ev);
|
|
355
377
|
break;
|
|
356
378
|
default: break;
|
|
357
379
|
}
|
|
@@ -377,6 +399,47 @@ export function noteMcpToolCall(item, ev) {
|
|
|
377
399
|
});
|
|
378
400
|
}
|
|
379
401
|
|
|
402
|
+
// The item kinds codex reports a tool call as, besides its MCP calls. Its own words, measured on
|
|
403
|
+
// codex-cli 0.150–0.158: a shell command, a file edit, a web search.
|
|
404
|
+
const TOOL_ITEM_KINDS = new Set(["command_execution", "file_change", "web_search"]);
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Remember one tool-shaped item by id, for the per-name count. Codex writes each item twice (started,
|
|
408
|
+
* then its terminal state) under one id, so the id is the unit, exactly as for the MCP gauge.
|
|
409
|
+
*/
|
|
410
|
+
export function noteToolItem(item, ev) {
|
|
411
|
+
if (!item?.id) return;
|
|
412
|
+
const name = item.type === "mcp_tool_call" ? `mcp__${item.server ?? "?"}__${item.tool ?? "?"}`
|
|
413
|
+
: TOOL_ITEM_KINDS.has(item.type) ? item.type : null;
|
|
414
|
+
if (!name) return;
|
|
415
|
+
(ev.toolItems ??= new Map()).set(item.id, name);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* The turn's tool calls by name, in the shape anthropic-agent records: an MCP call under
|
|
420
|
+
* `mcp__<server>__<tool>`, and codex's own tool items under its own word for them. Counted once per item
|
|
421
|
+
* id. An OBJECT is a measurement (`{}` = none observed).
|
|
422
|
+
*/
|
|
423
|
+
export function codexToolCallsByName(ev) {
|
|
424
|
+
const byName = Object.create(null);
|
|
425
|
+
for (const name of ev?.toolItems?.values() ?? []) countTool(byName, name);
|
|
426
|
+
return { ...byName };
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* The tool-server calls of this turn that came back as an error, by tool name, in the shape
|
|
431
|
+
* anthropic-agent records: a call that reached its server and `failed` there, which codex writes with no
|
|
432
|
+
* `error.message` (a call codex refused itself carries one, and `toolCallsRefused` counts those). Counted
|
|
433
|
+
* once per item id. An OBJECT is a measurement (`{}`: none). PURE.
|
|
434
|
+
*/
|
|
435
|
+
export function codexToolServerErrors(ev) {
|
|
436
|
+
const byName = Object.create(null);
|
|
437
|
+
for (const c of ev?.mcpCalls?.values() ?? []) {
|
|
438
|
+
if (c.status === "failed" && !c.message) countTool(byName, `mcp__${c.server ?? "?"}__${c.tool ?? "?"}`);
|
|
439
|
+
}
|
|
440
|
+
return { ...byName };
|
|
441
|
+
}
|
|
442
|
+
|
|
380
443
|
/**
|
|
381
444
|
* The MCP tool-call gauge for this turn: what completed, what was REFUSED, and why.
|
|
382
445
|
*
|
|
@@ -540,11 +603,16 @@ function settleTuple({ r, ev, resumeRef }) {
|
|
|
540
603
|
const failed = overflow || killed || !ev.turnCompleted || !!ev.turnFailed;
|
|
541
604
|
const usage = mapUsage(ev.usage);
|
|
542
605
|
const text = ev.agentText || "";
|
|
543
|
-
const json =
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
606
|
+
const json = {
|
|
607
|
+
...buildEnvelope({
|
|
608
|
+
text, ok: !failed, killed, usage,
|
|
609
|
+
summary: ev.turnFailed ? "failed" : (ev.turnCompleted ? "success" : undefined),
|
|
610
|
+
runId: ev.threadId,
|
|
611
|
+
}),
|
|
612
|
+
// What ended the turn, in the program's own words when it failed (session-record.mjs, codexEnding):
|
|
613
|
+
// the same three fields the Claude adapter writes. Recorded only; `ok` above is unchanged.
|
|
614
|
+
...codexEnding(ev),
|
|
615
|
+
};
|
|
548
616
|
const stderrOut = overflow
|
|
549
617
|
? (r.stderr + `\nopenai-agent output overflow: stdout/stderr exceeded ${r.maxBuffer} chars — killed the tree and failed the turn (truncated tail never parsed)`)
|
|
550
618
|
: (stallKill ? (r.stderr + "\nrequest timed out (openai-agent stall-watchdog: 0 streamed tokens)") : r.stderr);
|
|
@@ -589,6 +657,10 @@ function settleTuple({ r, ev, resumeRef }) {
|
|
|
589
657
|
// adapter reads its commands only for failures (below), so it reports no command-tool count: null, never zero.
|
|
590
658
|
toolCallsRefused: mcpToolGauge(ev).mcpToolCallsRefused,
|
|
591
659
|
commandToolCalls: null,
|
|
660
|
+
// The per-name split of the calls codex reports (see codexToolCallsByName), what the session went
|
|
661
|
+
// through (session-record.mjs: every turn ending and stream error, in order), and the raw stream.
|
|
662
|
+
toolCallsByName: codexToolCallsByName(ev), toolCallsErroredByName: codexToolServerErrors(ev),
|
|
663
|
+
session: sessionSummary(ev.session), stream: ev.stream ?? null,
|
|
592
664
|
writesFailed: ev.writesFailed ?? 0,
|
|
593
665
|
commandsFailed: ev.commandsFailed ?? 0,
|
|
594
666
|
commandFailures: ev.commandFailures ?? [],
|
|
@@ -624,6 +696,8 @@ function errResult(t0, e, resumeRef) {
|
|
|
624
696
|
code: 1, killed: false, wall: (Date.now() - t0) / 1000, stdout: "",
|
|
625
697
|
stderr: `openai-agent error: ${e?.message ?? e}`, laneWaitMs: 0,
|
|
626
698
|
json: null, usage: null, modelWire: null, sessionRef: resumeRef ?? null,
|
|
699
|
+
// No session ran: nothing to have been interrupted, no stream.
|
|
700
|
+
toolCallsByName: null, toolCallsErroredByName: null, session: null, stream: null,
|
|
627
701
|
};
|
|
628
702
|
}
|
|
629
703
|
|
|
@@ -652,7 +726,7 @@ export const openaiAgentEngine = {
|
|
|
652
726
|
// `thread/resume failed: no rollout found for thread id ... (code -32600)`, exit 1 — with the control
|
|
653
727
|
// (same id, same binary, its own home) resuming cleanly. So every warm-patch retry and every
|
|
654
728
|
// form-repair sub-turn on the codex arm has been failing.
|
|
655
|
-
async runTurn({ message, model, thinking, timeoutSec, resumeRef, mcpConfig, allowedTools, cwd, skillsDir, skillsGrantRoots, profilesDir, resolveSkill, runDir, stallSec, codexHome: providedHome } = {}) {
|
|
729
|
+
async runTurn({ message, model, thinking, timeoutSec, resumeRef, mcpConfig, allowedTools, cwd, skillsDir, skillsGrantRoots, profilesDir, resolveSkill, runDir, stallSec, codexHome: providedHome, streamFile = null } = {}) {
|
|
656
730
|
if (!message) throw new Error("openai-agent.runTurn: message is required");
|
|
657
731
|
const t0 = Date.now();
|
|
658
732
|
let codexHome;
|
|
@@ -703,16 +777,25 @@ export const openaiAgentEngine = {
|
|
|
703
777
|
// Stamped HERE, not at function entry: everything above is file writes that can take a moment, and
|
|
704
778
|
// a floor set too early would let a previous turn's rollout back in on a shared home.
|
|
705
779
|
const spawnedAtMs = Date.now();
|
|
706
|
-
const ev = { threadId: null, usage: null, agentText: "", turnCompleted: false, turnFailed: null, streamError: null, model: null, mcpCalls: new Map()
|
|
780
|
+
const ev = { threadId: null, usage: null, agentText: "", turnCompleted: false, turnFailed: null, streamError: null, model: null, mcpCalls: new Map(),
|
|
781
|
+
session: newSessionRecord() };
|
|
782
|
+
// The raw stream, one file per session beside its dispatch; every line codex wrote on stdout.
|
|
783
|
+
const sink = streamSink(streamFile);
|
|
707
784
|
const r = await runStreamingChild({
|
|
708
785
|
bin: codexBin(), args, input, runDir,
|
|
709
786
|
// — the run dir, not a shared tmpdir. codex makes cwd a workspace root, writable under the
|
|
710
787
|
// stage's profile, so this tightens the writable surface onto the run rather than widening it.
|
|
711
788
|
cwd: resolveSpawnCwd({ cwd, runDir }),
|
|
712
789
|
env, stallSec, timeoutSec,
|
|
713
|
-
onStdoutLine: (line) =>
|
|
790
|
+
onStdoutLine: (line) => {
|
|
791
|
+
sink.write(`${line}\n`);
|
|
792
|
+
parseCodexEvent(line, ev);
|
|
793
|
+
let e; try { e = JSON.parse(line); } catch { return; }
|
|
794
|
+
noteCodexEvent(ev.session, e, Date.now() - t0);
|
|
795
|
+
},
|
|
714
796
|
stderrIsLiveness: true, // codex streams progress on STDERR → it is liveness for the stall watchdog
|
|
715
797
|
});
|
|
798
|
+
ev.stream = sink.close();
|
|
716
799
|
if (r.spawnError) return errResult(t0, r.spawnError, resumeRef);
|
|
717
800
|
// The stream said no model (it never does — see readServedModel); the rollout under THIS run's
|
|
718
801
|
// CODEX_HOME did. Read here, inside the try, because the finally below deletes that dir.
|
|
@@ -0,0 +1,223 @@
|
|
|
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
|
+
// engine/session-record.mjs — what a model session went through, kept beside what it ended with.
|
|
4
|
+
//
|
|
5
|
+
// ── why this exists ──────────────────────────────────────────────────────────────────────────────────
|
|
6
|
+
//
|
|
7
|
+
// Both engines read the program's stream in memory and kept the LAST result. A session the vendor's
|
|
8
|
+
// classifier cut off mid-answer, or one the API refused and a restart recovered, ended in an ordinary
|
|
9
|
+
// successful result and was recorded as a clean session. A cut drops the tool calls that had not
|
|
10
|
+
// finished, so a stage that lost calls mid-answer looked exactly like one that finished.
|
|
11
|
+
//
|
|
12
|
+
// Measured on the bench's saved-data replays (2026-09-28, 50 Claude sessions): 23 were cut and carried
|
|
13
|
+
// on; in 8 the API then refused; one of the 8 still answered, because a background helper restarted the
|
|
14
|
+
// session and the last result was a success. The program's own words for each event, read off a saved
|
|
15
|
+
// stream with the content left out:
|
|
16
|
+
//
|
|
17
|
+
// cut a `user` event, `isSynthetic: true`, whose text block says the response "was stopped by
|
|
18
|
+
// a safety classifier"
|
|
19
|
+
// refusal a `system` event, subtype `model_refusal_no_fallback`, carrying `api_refusal_category`
|
|
20
|
+
// and `original_model`; then an `assistant` message with `stop_reason: "refusal"`
|
|
21
|
+
// restart a second `system` / `init` event in the same stream
|
|
22
|
+
// results one `result` event per answer, each with `result_index`, `is_error`, `stop_reason`,
|
|
23
|
+
// `terminal_reason` and, for one a helper's notice started, `origin.kind`
|
|
24
|
+
//
|
|
25
|
+
// So nothing here is inferred: every row is an event the program wrote, with when it arrived. The
|
|
26
|
+
// record answers "was this session interrupted, refused or restarted, and when" from the run directory,
|
|
27
|
+
// and it never decides anything — the retry policy reads the last result exactly as before.
|
|
28
|
+
//
|
|
29
|
+
// ── what is NOT recorded ─────────────────────────────────────────────────────────────────────────────
|
|
30
|
+
//
|
|
31
|
+
// No text in the session record. The cut message, the refusal's explanation and every result's answer
|
|
32
|
+
// carry the model's or the vendor's words about client matter; a row carries the event's kind, its time
|
|
33
|
+
// and its codes. The raw stream, which does carry the words, goes to its own file in the run directory
|
|
34
|
+
// (streamSink below), where the dispatch text already sits.
|
|
35
|
+
//
|
|
36
|
+
// ONE EXCEPTION, and it sits beside the record rather than in it: when a session ENDED IN AN ERROR, the
|
|
37
|
+
// program's own words for that error go on the attempt row (`errorText`, see "how the session ended"
|
|
38
|
+
// below). An expired sign-in recorded `stop_sequence` six times and nothing else, while the program had
|
|
39
|
+
// said in plain words that the session had expired. On a healthy ending that field is the model's answer
|
|
40
|
+
// and stays off the row.
|
|
41
|
+
|
|
42
|
+
import { mkdirSync, openSync, writeSync, closeSync, statfsSync } from "node:fs";
|
|
43
|
+
import { dirname } from "node:path";
|
|
44
|
+
|
|
45
|
+
/** The program's words for a classifier cut, as it writes them into the session. Matched, never quoted. */
|
|
46
|
+
export const CLASSIFIER_CUT_RE = /stopped by a safety classifier/i;
|
|
47
|
+
|
|
48
|
+
// A row list is a record, not a log: a session that is cut fifty times has said what it needs to say by
|
|
49
|
+
// the fiftieth, and the count beside the list keeps saying how many there were.
|
|
50
|
+
const ROWS_CAP = 50;
|
|
51
|
+
|
|
52
|
+
/** An empty record. `starts` counts the program's own session starts (`system`/`init`). */
|
|
53
|
+
export function newSessionRecord() {
|
|
54
|
+
return { starts: 0, results: [], classifierCuts: [], refusals: [], refusalStops: 0, dropped: 0 };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const push = (rec, list, row) => {
|
|
58
|
+
if (list.length < ROWS_CAP) list.push(row);
|
|
59
|
+
else rec.dropped += 1;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/** The text a `user` event carries outside its tool results: a string, or its `text` blocks. */
|
|
63
|
+
function userText(content) {
|
|
64
|
+
if (typeof content === "string") return content;
|
|
65
|
+
if (!Array.isArray(content)) return "";
|
|
66
|
+
return content.filter((b) => b?.type === "text" && typeof b.text === "string").map((b) => b.text).join("\n");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Fold one parsed Claude stream event into the record. `atMs` is milliseconds since the spawn. PURE
|
|
71
|
+
* over its arguments; the caller owns the clock.
|
|
72
|
+
*/
|
|
73
|
+
export function noteClaudeEvent(rec, ev, atMs) {
|
|
74
|
+
if (!rec || !ev || typeof ev !== "object") return;
|
|
75
|
+
if (ev.type === "system" && ev.subtype === "init") { rec.starts += 1; return; }
|
|
76
|
+
if (ev.type === "system" && ev.subtype === "model_refusal_no_fallback") {
|
|
77
|
+
push(rec, rec.refusals, {
|
|
78
|
+
atMs, category: typeof ev.api_refusal_category === "string" ? ev.api_refusal_category : null,
|
|
79
|
+
model: typeof ev.original_model === "string" ? ev.original_model : null,
|
|
80
|
+
});
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (ev.type === "assistant") {
|
|
84
|
+
if (ev.message?.stop_reason === "refusal") rec.refusalStops += 1;
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (ev.type === "user") {
|
|
88
|
+
if (CLASSIFIER_CUT_RE.test(userText(ev.message?.content))) push(rec, rec.classifierCuts, { atMs, synthetic: ev.isSynthetic === true });
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (ev.type === "result") {
|
|
92
|
+
push(rec, rec.results, {
|
|
93
|
+
atMs,
|
|
94
|
+
index: Number.isFinite(ev.result_index) ? ev.result_index : rec.results.length + rec.dropped,
|
|
95
|
+
subtype: typeof ev.subtype === "string" ? ev.subtype : null,
|
|
96
|
+
isError: ev.is_error === true,
|
|
97
|
+
stopReason: typeof ev.stop_reason === "string" ? ev.stop_reason : null,
|
|
98
|
+
terminalReason: typeof ev.terminal_reason === "string" ? ev.terminal_reason : null,
|
|
99
|
+
origin: typeof ev.origin?.kind === "string" ? ev.origin.kind : null,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Fold one parsed Codex `exec --json` event into the record. Codex states no refusal category and no
|
|
106
|
+
* classifier cut on its stream (measured, codex-cli 0.150–0.158), so what it can say is said: how the
|
|
107
|
+
* turn ended, every time it ended, and every stream error.
|
|
108
|
+
*/
|
|
109
|
+
export function noteCodexEvent(rec, ev, atMs) {
|
|
110
|
+
if (!rec || !ev || typeof ev !== "object") return;
|
|
111
|
+
if (ev.type === "thread.started") { rec.starts += 1; return; }
|
|
112
|
+
if (ev.type === "turn.completed" || ev.type === "turn.failed" || ev.type === "error") {
|
|
113
|
+
push(rec, rec.results, {
|
|
114
|
+
atMs, index: rec.results.length + rec.dropped, subtype: ev.type,
|
|
115
|
+
isError: ev.type !== "turn.completed", stopReason: null, terminalReason: null, origin: null,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** True when any result the session reached was an error, whatever the last one said. */
|
|
121
|
+
export const anyResultErrored = (rec) => Array.isArray(rec?.results) && rec.results.some((r) => r?.isError === true);
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The record as the attempt row carries it. `interrupted` is the one-word answer a reader filters on: the
|
|
125
|
+
* session was cut, refused, restarted, or reached an error result, at least once.
|
|
126
|
+
*/
|
|
127
|
+
export function sessionSummary(rec) {
|
|
128
|
+
if (!rec) return null;
|
|
129
|
+
const interrupted = rec.classifierCuts.length > 0 || rec.refusals.length > 0 || rec.refusalStops > 0
|
|
130
|
+
|| rec.starts > 1 || anyResultErrored(rec);
|
|
131
|
+
return {
|
|
132
|
+
interrupted, starts: rec.starts, results: rec.results.map((r) => ({ ...r })),
|
|
133
|
+
classifierCuts: rec.classifierCuts.map((r) => ({ ...r })), refusals: rec.refusals.map((r) => ({ ...r })),
|
|
134
|
+
refusalStops: rec.refusalStops, ...(rec.dropped ? { rowsDropped: rec.dropped } : {}),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ── how the session ended, in the program's own words ──────────────────────────────────────────────────
|
|
139
|
+
//
|
|
140
|
+
// The stop reason alone misleads on an error. Measured on an expired sign-in: the Claude program reported
|
|
141
|
+
// `subtype: "success"`, `is_error: true`, `stop_reason: "stop_sequence"` (the stop of its OWN synthetic
|
|
142
|
+
// message, whose model reads `<synthetic>`) and `terminal_reason: "api_error"`, with the reason in the
|
|
143
|
+
// result text: "Failed to authenticate: OAuth session expired and could not be refreshed". Six retries of
|
|
144
|
+
// one stage journalled six identical `stop_sequence` rows and said nothing about why. The stop reason stays
|
|
145
|
+
// verbatim, because on an error it describes the program's synthetic message and rewriting it would be
|
|
146
|
+
// inventing; these three go beside it, on both engines:
|
|
147
|
+
//
|
|
148
|
+
// terminalReason the kind of ending, as the program states it. null when it states none (Codex never does).
|
|
149
|
+
// apiErrorStatus the vendor's status when it gave one. null when it did not, which is not zero.
|
|
150
|
+
// errorText the program's words for the error, ONLY when the session ended in one, cut to a length a
|
|
151
|
+
// person reads. Never a word of ours in their place: an error that carried none reads null.
|
|
152
|
+
//
|
|
153
|
+
// The verdict does not move: `fail`, the retry ladder and the envelope's status read exactly what they
|
|
154
|
+
// read before. These record; they decide nothing.
|
|
155
|
+
export const ERROR_TEXT_CAP = 300;
|
|
156
|
+
|
|
157
|
+
const words = (t) => (typeof t === "string" && t.trim() ? t.slice(0, ERROR_TEXT_CAP) : null);
|
|
158
|
+
|
|
159
|
+
/** Claude: from the session's LAST result event (the one the verdict reads), or nulls when none arrived. */
|
|
160
|
+
export function claudeEnding(resultEvent) {
|
|
161
|
+
const r = resultEvent && typeof resultEvent === "object" ? resultEvent : null;
|
|
162
|
+
return {
|
|
163
|
+
terminalReason: typeof r?.terminal_reason === "string" ? r.terminal_reason : null,
|
|
164
|
+
apiErrorStatus: r?.api_error_status ?? null,
|
|
165
|
+
errorText: r?.is_error === true ? words(r.result) : null,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Codex: no terminal kind and no status on its stream. Its words are the failed turn's own message, or the
|
|
171
|
+
* stream error when the turn never completed; an `error` it recovered from (a reconnect, then a completed
|
|
172
|
+
* turn) is not why it ended. Reads the raw messages `parseCodexEvent` keeps, never its placeholders.
|
|
173
|
+
*/
|
|
174
|
+
export function codexEnding(ev) {
|
|
175
|
+
const failed = ev?.turnFailedText ?? null, stream = ev?.streamErrorText ?? null;
|
|
176
|
+
const said = ev?.turnFailed ? words(failed) : (ev?.turnCompleted ? null : words(stream));
|
|
177
|
+
return { terminalReason: null, apiErrorStatus: null, errorText: said };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** The three as an attempt row carries them: written every time, null where the engine said nothing. */
|
|
181
|
+
export function endingFields(json) {
|
|
182
|
+
return { terminalReason: json?.terminalReason ?? null, apiErrorStatus: json?.apiErrorStatus ?? null, errorText: json?.errorText ?? null };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** A per-tool-name call count, folded one name at a time. */
|
|
186
|
+
export function countTool(byName, name) {
|
|
187
|
+
const k = String(name ?? "?") || "?";
|
|
188
|
+
byName[k] = (byName[k] ?? 0) + 1;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ── the raw stream ───────────────────────────────────────────────────────────────────────────────────
|
|
192
|
+
//
|
|
193
|
+
// Every byte the program wrote on its protocol channel, one file per session, beside the dispatch that
|
|
194
|
+
// started it. Kept while the disk holding the run has room: below STREAM_FLOOR_BYTES free it is not
|
|
195
|
+
// written, and the record says so rather than leaving a file that stops halfway. A write that fails
|
|
196
|
+
// mid-session closes the file and says so too. Neither ever fails the turn.
|
|
197
|
+
export const STREAM_FLOOR_BYTES = 5 * 1024 ** 3;
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Open a sink for `file`. Returns `{ write(chunk), close() }`; `close()` returns what landed:
|
|
201
|
+
* `{ file, bytes, present: true }`, or `{ file, present: false, reason }`. A null file is a null sink.
|
|
202
|
+
*/
|
|
203
|
+
export function streamSink(file, { floorBytes = STREAM_FLOOR_BYTES, freeBytes = null } = {}) {
|
|
204
|
+
if (!file) return { write() {}, close: () => null };
|
|
205
|
+
let fd = null, bytes = 0, reason = null;
|
|
206
|
+
try {
|
|
207
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
208
|
+
const free = freeBytes ?? (() => { const s = statfsSync(dirname(file)); return Number(s.bavail) * Number(s.bsize); })();
|
|
209
|
+
if (free < floorBytes) reason = `not written: the disk holding the run had ${Math.round(free / 1024 ** 2)} MiB free, under the ${Math.round(floorBytes / 1024 ** 3)} GiB floor`;
|
|
210
|
+
else fd = openSync(file, "w", 0o600);
|
|
211
|
+
} catch (e) { reason = `not written: ${String(e?.message ?? e).slice(0, 120)}`; }
|
|
212
|
+
return {
|
|
213
|
+
write(chunk) {
|
|
214
|
+
if (fd === null) return;
|
|
215
|
+
try { bytes += writeSync(fd, typeof chunk === "string" ? chunk : Buffer.from(chunk)); }
|
|
216
|
+
catch (e) { try { closeSync(fd); } catch { /* already gone */ } fd = null; reason = `cut short after ${bytes} bytes: ${String(e?.message ?? e).slice(0, 120)}`; }
|
|
217
|
+
},
|
|
218
|
+
close() {
|
|
219
|
+
if (fd !== null) { try { closeSync(fd); } catch { /* the bytes are on disk or they are not; bytes says which */ } fd = null; }
|
|
220
|
+
return reason ? { file, present: bytes > 0, bytes, reason } : { file, present: true, bytes };
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
}
|
|
@@ -730,8 +730,8 @@ export function validateJob(job, { atClaim = false } = {}) {
|
|
|
730
730
|
warnings.push(...fit.warnings);
|
|
731
731
|
}
|
|
732
732
|
// platforms — marketplaces to sweep IN ADDITION to the account's own. ADDITIVE ONLY, and enforced as
|
|
733
|
-
// such where the union happens: a client's platforms are a client MANDATE (the 2026-07-18
|
|
734
|
-
//
|
|
733
|
+
// such where the union happens: a client's platforms are a client MANDATE (the 2026-07-18 Drivers
|
|
734
|
+
// Haven run searched house platforms instead of the client's), so a per-run list can widen the grid
|
|
735
735
|
// and can never shrink it. Same per-entry rules as a profile's, shared via platformEntryErrors.
|
|
736
736
|
if (job.platforms != null) {
|
|
737
737
|
if (!Array.isArray(job.platforms)) errs.push("platforms must be an array of bare store domains (e.g. [\"gnc.com\"]) — they are ADDED to the account's marketplaces, never a replacement for them");
|
|
@@ -1780,7 +1780,7 @@ function validateNet(f, ord, mode) {
|
|
|
1780
1780
|
// `findings?_` and would aim the repair turn at register-coverage-ledger.json — forbidding the model
|
|
1781
1781
|
// to rewrite the only file that could fix it.
|
|
1782
1782
|
// It reaches the wire as a validator `reason` (verify.mjs checkFindingsSibling), which is the only shape
|
|
1783
|
-
// gateway.mjs:
|
|
1783
|
+
// gateway.mjs:1200 judgeArtifacts mints as `invalid_file:<path>:<token>`; a bespoke throw would match nothing.
|
|
1784
1784
|
// The precedent is one line up the file: findings_mark_assessment_* was named plural for the same
|
|
1785
1785
|
// routing reason.
|
|
1786
1786
|
//
|
|
@@ -1942,14 +1942,14 @@ function recordUrlOrigin(value) {
|
|
|
1942
1942
|
*. `checkRecordUrlHost` asks whether a link points at the wrong register, and `recordUrlOrigin`
|
|
1943
1943
|
* returns null for anything with no host — so the host gate returns CLEAN for `#`. That is right for its
|
|
1944
1944
|
* own purpose and it means nothing in the findings path was asking whether the link is a link. A value of
|
|
1945
|
-
* `#` then composes `- Source: [EUIPO ·
|
|
1945
|
+
* `#` then composes `- Source: [EUIPO · 099999992](#)` — the delivered R5 shape is about, byte for
|
|
1946
1946
|
* byte, reached through a path nothing rejected.
|
|
1947
1947
|
*
|
|
1948
1948
|
* ABSENT IS NOT DEAD, and the distinction is the whole predicate. `null` and `""` are the sanctioned way
|
|
1949
1949
|
* to say "this provider publishes no per-record page" — validateSource documents `""` in its own message
|
|
1950
1950
|
* and renderSourceBullet composes nothing for it. Rejecting those would break the legitimate case this
|
|
1951
1951
|
* issue exists to protect. Anything else that is not an http(s) URL is a value a seat typed in place of a
|
|
1952
|
-
* link: `#`, `#details/trademarks/
|
|
1952
|
+
* link: `#`, `#details/trademarks/099999992`, a bare path, or whitespace that only looks like a value.
|
|
1953
1953
|
*
|
|
1954
1954
|
* IT ENUMERATES RATHER THAN ASKING "IS THIS AN http(s) URL", and a shipped test is why. ` an
|
|
1955
1955
|
* unparseable or non-http value is not a host claim and is left to the shape rules` deliberately ACCEPTS
|
|
@@ -288,8 +288,8 @@ export function formNeighbourhood(element, { markets = [], scripts = SUPPORTED_S
|
|
|
288
288
|
|
|
289
289
|
// exactQueries = the strings dispatched as OR-stacked exact/near name searches (deduped, original excluded).
|
|
290
290
|
//
|
|
291
|
-
// Deduped ON THE DIACRITIC FOLD, not the raw string. The registers fold diacritics, so `
|
|
292
|
-
// `
|
|
291
|
+
// Deduped ON THE DIACRITIC FOLD, not the raw string. The registers fold diacritics, so `havensidè`,
|
|
292
|
+
// `havensidé`, `havensidê`, `havensidë` and `havenšide` are ONE query, not five — and one already covered by the
|
|
293
293
|
// element itself. Drivers Haven 2026-07-17 dispatched exactly those five as distinct terms; each returned
|
|
294
294
|
// the same 424 hits, together they exhausted the run's record budget, all five stayed `unenumerated`, and
|
|
295
295
|
// `coverage_clean_unverified_incomplete:primary-sweep` then blocked delivery six times on a gate that could
|
|
@@ -399,8 +399,8 @@ export function coverageGaps(band, { dispatched = [], explained = [] } = {}) {
|
|
|
399
399
|
// `model` is the VALIDATED variant-manifest.json (variant-manifest-model.mjs). When supplied it is
|
|
400
400
|
// AUTHORITATIVE for the dominant element and the prose is used only for the formative root — because the prose
|
|
401
401
|
// parse is unsafe. `dominantElementFromManifest` captures to the first `.`/`;`/`(`/newline, so a manifest that
|
|
402
|
-
// writes "Dominant element:
|
|
403
|
-
// normalizeElement then compacts to `
|
|
402
|
+
// writes "Dominant element: AQUA — the stem a family of marks shares" yields the whole clause, which
|
|
403
|
+
// normalizeElement then compacts to `aquathestemafamilyofmarksshares`. That pseudo-element got a full
|
|
404
404
|
// exhaustive edit-1 neighbourhood: 1,736 junk exact queries on AquaPlus 2026-07-17, 4,524 on the 07-16 run.
|
|
405
405
|
// Measured 2026-07-18: 10 of 20 recent runs carried one of these. The JSON field is a validated scalar and
|
|
406
406
|
// cannot swallow a sentence.
|
|
@@ -478,7 +478,7 @@ export function renderFormNeighbourhoodJson(manifestMd, { markets = [], scripts
|
|
|
478
478
|
// line variantCompletenessGaps' per-script floor draws when it says it VALIDATES, NEVER GENERATES.
|
|
479
479
|
|
|
480
480
|
// A seed longer than this is a prose-parse artifact, not an element. The measured shape is the
|
|
481
|
-
// swallowed clause — "
|
|
481
|
+
// swallowed clause — "aquathestemafamilyofmarksshares", 31-35 characters against a 4-char element —
|
|
482
482
|
// which took a full exhaustive edit-1 neighbourhood and put 1,736 junk exact queries on the wire
|
|
483
483
|
// (AquaPlus 2026-07-17). isStemOfNamedElement already rejects that shape for the formative root by
|
|
484
484
|
// measuring it against the named elements; this is the same measure for a seed with nothing to
|
|
@@ -691,7 +691,7 @@ export function variantFloorFamilies(elements, { mark = "", droppedAxes = [], mi
|
|
|
691
691
|
// MEASURED, not assumed (2026-08-03): the register compiler's `norm` and the shared `formKey`
|
|
692
692
|
// both strip separators, so every member of this family keys IDENTICALLY to the mark and is
|
|
693
693
|
// already searched as the mark's own exact entry — which is why compileRegisterPlan does not
|
|
694
|
-
// re-dispatch them, and why doing so would be the
|
|
694
|
+
// re-dispatch them, and why doing so would be the Drivers Haven shape (five spellings, one result
|
|
695
695
|
// set, the run's record budget spent). Enumerated here because the family is genuinely
|
|
696
696
|
// mechanical and the surfaces that do NOT fold spacing — the common-law grid, the connotation
|
|
697
697
|
// sweep — are real; those consumers are NOT wired to it by this change.
|
|
@@ -794,7 +794,7 @@ export function mergeVariantFloor(floorFamilies, modelVariants, { rejectedSeeds
|
|
|
794
794
|
/**
|
|
795
795
|
* Is `root` a plausible formative root, or a prose-parse artifact?
|
|
796
796
|
*
|
|
797
|
-
* A stem is never LONGER than the element it stems from — VOLEM(5) for ZOLEMA(6),
|
|
797
|
+
* A stem is never LONGER than the element it stems from — VOLEM(5) for ZOLEMA(6), AQU(3) for AQUA(4),
|
|
798
798
|
* VELTRI(6) for VELTRIN(7). Substring containment is NOT the test: a real root can differ in its leading
|
|
799
799
|
* character (ZOLEMA → VOLEM reaches the VOLEMA family), which is exactly the widening the root exists for.
|
|
800
800
|
* The swallowed-sentence artifacts are always the element PLUS a clause — 32-36 characters against a 5-char
|