clearotron 0.2.0 → 0.3.0-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/.env.example +52 -0
- package/INSTALL.md +9 -7
- package/README.md +2 -1
- package/bin/example.mjs +97 -32
- package/bin/onboard.mjs +58 -20
- package/bin/start.mjs +7 -0
- package/bin/stop.mjs +65 -3
- package/build-info.json +2 -2
- package/docs/RELEASES.md +6 -4
- package/docs/architecture/04-configuration-reference.md +1 -1
- package/driver/CHANGELOG.md +51 -0
- package/driver/ask-ledger.mjs +69 -1
- package/driver/declination-call.mjs +32 -0
- package/driver/driver.config.mjs +20 -0
- package/driver/engine/mcp/recording-server.mjs +4 -0
- package/driver/gateway.mjs +8 -3
- package/driver/knockout-assess-record.mjs +5 -1
- package/driver/package.json +1 -1
- package/driver/pipeline.mjs +123 -3
- package/driver/predelivery-lint.mjs +23 -5
- package/driver/publish/knockout.mjs +12 -6
- package/driver/publish/render-knockout.mjs +133 -23
- package/driver/publish/report-data.mjs +13 -3
- package/driver/publish/seed-pool.mjs +24 -9
- package/driver/record-carry.mjs +139 -0
- package/driver/reference-score.mjs +53 -3
- package/driver/reference-strip-signatures.mjs +68 -0
- package/driver/register-digest-record.mjs +31 -1
- package/driver/repairs.mjs +1 -1
- package/driver/result-noun-fields.mjs +7 -0
- package/driver/skills/knockout-assess/SKILL.md +10 -4
- package/driver/stages-knockout.mjs +1 -1
- package/driver/stages.mjs +1 -1
- package/driver/suite-census.json +108 -30
- package/driver/unit-inventory.mjs +47 -0
- package/driver/unit-state-verdict.mjs +8 -8
- package/driver/verify-knockout.mjs +9 -1
- package/driver/verify.mjs +2 -2
- package/driver/whatif-memo-run.mjs +45 -4
- package/mcp-server/CHANGELOG.md +10 -0
- package/mcp-server/lib/brief.mjs +15 -0
- package/mcp-server/lib/driver.mjs +6 -0
- package/mcp-server/lib/knockout.mjs +435 -0
- package/mcp-server/lib/scrub.mjs +1 -1
- package/mcp-server/lib/whatif.mjs +10 -1
- package/mcp-server/package.json +1 -1
- package/mcp-server/server.mjs +69 -4
- package/package.json +1 -1
- package/portal-ui/package.json +1 -1
- package/providers/oauth-mcp-bridge/CHANGELOG.md +10 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/scripts/ai-page-render-check.mjs +2 -1
- package/scripts/clearances-render-check.mjs +2 -1
- package/scripts/drain-preflight.mjs +2 -2
- package/scripts/env-audit.mjs +20 -0
- package/scripts/freeze-example-run.mjs +3 -3
- package/scripts/headless-page.mjs +274 -0
- package/scripts/home-render-check.mjs +2 -1
- package/scripts/live-surface-check.mjs +86 -17
- package/scripts/mint-reference-strip-backlog.mjs +41 -0
- package/scripts/release-await-cut.mjs +95 -7
- package/scripts/release-version-pr-checks.mjs +25 -1
- package/scripts/render-check.mjs +61 -2
- package/scripts/report-frame-check.mjs +12 -0
- package/scripts/report-screenshot.mjs +62 -2
- package/scripts/revisit-render-check.mjs +3 -2
- package/scripts/score.mjs +14 -0
- package/shared/access-audience.mjs +215 -0
- package/shared/tracked-files.mjs +31 -0
- package/scripts/deploy-test.sh +0 -309
package/docs/RELEASES.md
CHANGED
|
@@ -8,7 +8,7 @@ nothing else.
|
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
10
|
npm install -g clearotron # stable — the tested one
|
|
11
|
-
npm install -g clearotron@beta # newest —
|
|
11
|
+
npm install -g clearotron@beta # newest — cut when there is something worth testing
|
|
12
12
|
```
|
|
13
13
|
|
|
14
14
|
## What each channel promises
|
|
@@ -16,7 +16,7 @@ npm install -g clearotron@beta # newest — every merge, minutes after it la
|
|
|
16
16
|
| | `latest` (stable) | `beta` |
|
|
17
17
|
|---|---|---|
|
|
18
18
|
| **Version looks like** | `0.2.0` | `0.2.1-beta.4` |
|
|
19
|
-
| **Cut when** | a beta has passed a full clearance run and a from-scratch install by somebody who has never seen the product, and the owner says go |
|
|
19
|
+
| **Cut when** | a beta has passed a full clearance run and a from-scratch install by somebody who has never seen the product, and the owner says go | when a change lands that is worth testing, or while a stable is being prepared |
|
|
20
20
|
| **Promises** | it installed and ran a real clearance end to end before it was published | it built, and the automated suite passed |
|
|
21
21
|
| **Use it if** | you are running this for real work | you want a fix that landed today, or you are helping test |
|
|
22
22
|
|
|
@@ -35,8 +35,10 @@ suggest for yours.
|
|
|
35
35
|
|
|
36
36
|
## How often
|
|
37
37
|
|
|
38
|
-
A stable is cut when a beta earns it, which we aim at roughly weekly. Betas
|
|
39
|
-
|
|
38
|
+
A stable is cut when a beta earns it, which we aim at roughly weekly. **Betas are cut deliberately, days
|
|
39
|
+
apart** — when a change lands that somebody should test, or while a stable is being prepared. They are not
|
|
40
|
+
published on every merge, so `beta` is a version somebody chose to publish rather than whatever merged
|
|
41
|
+
last. Some stables have several betas before them and some have none.
|
|
40
42
|
|
|
41
43
|
Numbering follows the change, not the calendar: `0.2.x` for fixes, `0.3.0` when a feature lands, `1.0`
|
|
42
44
|
when the hit-list redesign ships.
|
|
@@ -200,7 +200,7 @@ deployment may override (verify live values per deployment).
|
|
|
200
200
|
| `CLEAROTRON_OAUTH_BRIDGE` | module-relative `providers/oauth-mcp-bridge/bridge.mjs` | Case-law MCP bridge script. (Portable since the module-relative default; set explicitly only for a bridge outside the repo tree.) |
|
|
201
201
|
| `CLEAROTRON_REGISTER_CALL_LOG` | `~/trademark/telemetry/register-calls.jsonl`, or the existing file wherever it already is | Billing-grade provider-call ledger, shared by whichever ONE register provider is wired — not a vendor artifact. Every read site derives the default from`homedir()` at call time (2026-07-19: two sites had hardcoded a literal account home, splitting the ledger under any other service account — guarded by `test/deployment-hostnames.test.mjs`). |
|
|
202
202
|
| `CLEAROTRON_REGISTER_RECORD_LOG` | **runtime-injected per run**: `<runDir>/_driver/register-record-bodies.jsonl` | Citation-fidelity log: the BODY of every fetched official record. ** moved it INTO the run** — created with the run, unioned into the run's`_records/`, archived and purged with it. There is no retention setting and no cleanup job, because it no longer grows on the box: held globally it reached 432 MB in 61 days on production and needed a rotation timer on every install. **Do not set this by hand** — a fixed value pins every run's bodies to one file and restores the problem. A box upgraded across still holds its old global file; nothing writes or reads it, the driver names it once per process on stderr, and archiving it is one`mv`. An empty log cannot read as verified: the run's successful `record_fetch` rows in the (still global) call ledger are compared against the assembled record set, and a gap is reported as a failure. |
|
|
203
|
-
| `CORSEARCH_CALL_LOG` / `CORSEARCH_RECORD_LOG` | — | **Deprecated, honoured for one release.**
|
|
203
|
+
| `CORSEARCH_CALL_LOG` / `CORSEARCH_RECORD_LOG` | — | **Deprecated, honoured for one release.** These are the names these two variables carried before the rename. Unset on every deployed box (all three ran the homedir default), so what actually protects an upgrade is the filename fallback: a`corsearch-calls.jsonl` / `corsearch-records.jsonl` already on disk keeps being read where it sits. Resolution order is in `providers/_shared/ledger-path.mjs`. |
|
|
204
204
|
| `CLEAROTRON_BAND_RUN_DIR` | set per dispatch | The run dir the band MCP server writes into, injected per stage — unset means the server has no run to write to and says so rather than guessing one. |
|
|
205
205
|
| `CLEAROTRON_FEEDBACK_DIR` | `<poolRoot>/_feedback` | Where report feedback flags are stored. Beside the pool by default so a deployment that moves the pool moves the flags with it. |
|
|
206
206
|
| `USPTO_LOCAL_DB` | **none — set it to use `uspto-local`** | The local USPTO index (`node:sqlite` + FTS5) that `bin/uspto-sync.mjs` builds and the free US register reads. Named in `.env.example`; this is the reference row. |
|
package/driver/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,56 @@
|
|
|
1
1
|
# prelim-driver
|
|
2
2
|
|
|
3
|
+
## 0.3.0-beta.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- bb1a409: New: A Knockout report now carries the whole assessment behind its ratings. You get the reviewer's notes and the reviewer's own opening read of each name. You also get the reasoning that holds a name at its rating, and what would move it. This was written during every search and reached only the audit workbook, so the report showed a rating without the thinking under it. The notes are marked as reference material rather than mixed into the findings.
|
|
8
|
+
|
|
9
|
+
New: A registered filing the search formed a view on now shows that rating and the read behind it. Before, the one registered right on a page was the only entry carrying no rating, beside softer uses that all carried one. That inverted what matters legally. A filing the search did not weigh still says so plainly.
|
|
10
|
+
- bb1a409: New: The tools that show how a search reached its answer now work on a Knockout search. You can ask what it found, what it looked at, where it searched and came back empty, and read the delivered report itself. Until now they returned nothing at all for a Knockout search, so anyone asking how one of these results was reached saw a blank record.
|
|
11
|
+
|
|
12
|
+
New: The proof-of-search record answers on these searches. It lists what was searched and found nothing, which is what answers a challenge to a result. Where a tool has nothing to show for this kind of search, it now says so in words. An empty list reads as "we looked and found nothing".
|
|
13
|
+
|
|
14
|
+
### Patch Changes
|
|
15
|
+
|
|
16
|
+
- df6277c: Fixed: A live registration covering the goods you asked about is no longer left out of a report for want of room. It is reported, or the report says why it is distant.
|
|
17
|
+
- 9ba8cd0: Fixed: A what-if memo is now refused if it writes anywhere in the delivered run except its own memo folder, and the refusal names the file.
|
|
18
|
+
- 8c8e200: Fixed: A what-if question left queued when its run is archived now comes back with a reason, instead of never being answered.
|
|
19
|
+
- bb1a409: Fixed: The delivery record now states plainly when a report's write-up length and ranking rules could not be checked against the delivered text. It says the rules were applied to nothing on that run. Before, this was recorded as an unlabelled failed check. It looked like any other, so a run could deliver with those rules unverified and nobody would see it.
|
|
20
|
+
- 9ba8cd0: For operators: Each run record now says whether the model id it observed was a pinned snapshot or an alias the provider can repoint.
|
|
21
|
+
- aea4a4e: Fixed: The deployment check now says which checkout a service is running from even when its unit file does not declare one.
|
|
22
|
+
- 14d6d28: Fixed: The demo and the install steps now start on native Windows, where they previously crashed on the first internal module they loaded.
|
|
23
|
+
|
|
24
|
+
## 0.2.1
|
|
25
|
+
|
|
26
|
+
### Patch Changes
|
|
27
|
+
|
|
28
|
+
- 3cc154a: Fixed: A report now keeps a mark the search confirmed, instead of dropping it because it was already noted on an internal working sheet. Where a mark is still missing, the run records it by name rather than closing the question.
|
|
29
|
+
- 3cc154a: Fixed: Asking a what-if question about a delivered report now returns a memo, instead of failing to find the run it was asked about.
|
|
30
|
+
- 0f7b44a: New: `clearotron demo` now publishes all four example reports — one per product — instead of only the first.
|
|
31
|
+
- 2138a3c: For operators: The `beta` channel now gets a release when there is something worth testing, days apart, instead of one on every merge.
|
|
32
|
+
- 8b5ab91: Fixed: `doctor` no longer reports a working Cloudflare Access door as unprotected. An API-style door and a failing origin are now told apart, each with its own message. Neither is reported as a pass.
|
|
33
|
+
- 77cf56d: For operators: The configuration reference now explains the two deprecated search-log variables in full, instead of stopping mid-sentence.
|
|
34
|
+
- 0dcd05a: Fixed: the settings catalogue now lists `CLEAROTRON_CHECKOUT_DIR`, the path every service file points at. The installer still fills it in for you. It is written down so that anyone whose service will not start can look it up.
|
|
35
|
+
|
|
36
|
+
## 0.2.1-beta.2
|
|
37
|
+
|
|
38
|
+
### Patch Changes
|
|
39
|
+
|
|
40
|
+
- 8b5ab91: Fixed: `doctor` no longer reports a working Cloudflare Access door as unprotected. An API-style door and a failing origin are now told apart, each with its own message. Neither is reported as a pass.
|
|
41
|
+
|
|
42
|
+
## 0.2.1-beta.1
|
|
43
|
+
|
|
44
|
+
### Patch Changes
|
|
45
|
+
|
|
46
|
+
- 0dcd05a: Fixed: the settings catalogue now lists `CLEAROTRON_CHECKOUT_DIR`, the path every service file points at. The installer still fills it in for you. It is written down so that anyone whose service will not start can look it up.
|
|
47
|
+
|
|
48
|
+
## 0.2.1-beta.0
|
|
49
|
+
|
|
50
|
+
### Patch Changes
|
|
51
|
+
|
|
52
|
+
- 77cf56d: For operators: The configuration reference now explains the two deprecated search-log variables in full, instead of stopping mid-sentence.
|
|
53
|
+
|
|
3
54
|
## 0.2.0
|
|
4
55
|
|
|
5
56
|
### Minor Changes
|
package/driver/ask-ledger.mjs
CHANGED
|
@@ -574,6 +574,64 @@ export function parseAskClosureLines(text) {
|
|
|
574
574
|
return out;
|
|
575
575
|
}
|
|
576
576
|
|
|
577
|
+
/**
|
|
578
|
+
* The marks the CLIENT was actually shown — `findings[].mark` from findings.json, normalized.
|
|
579
|
+
*
|
|
580
|
+
* Returns null when the file is absent or unparseable, and null is NOT an empty set: an absence is a
|
|
581
|
+
* could-not-look, and the caller below fails toward leaving the ask OPEN rather than closing it on a
|
|
582
|
+
* file it could not read.
|
|
583
|
+
*
|
|
584
|
+
* ✕ NEVER a substring search of the serialized document. `"DELFIN" in JSON.stringify(findings)` is true
|
|
585
|
+
* when the findings name DELFIN TECHNOLOGIES OY and nothing else — a membership test that matches every
|
|
586
|
+
* longer name inflates whatever it is counting and reads as a clean result. The field, or nothing.
|
|
587
|
+
* PURE.
|
|
588
|
+
*/
|
|
589
|
+
export function deliveredMarks(findingsJsonText) {
|
|
590
|
+
let doc;
|
|
591
|
+
try { doc = JSON.parse(String(findingsJsonText ?? "")); } catch { return null; }
|
|
592
|
+
const rows = Array.isArray(doc?.findings) ? doc.findings : null;
|
|
593
|
+
if (!rows) return null;
|
|
594
|
+
return new Set(rows.map((f) => normMark(f?.mark)).filter(Boolean));
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const normMark = (s) => String(s ?? "").toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Owner ruling 2026-09-07 (tracker issue 246), Option A, worded ABOUT THE MARK: a recall ask may close
|
|
601
|
+
* as immaterial only when the mark it went looking for is in the client's findings. Otherwise the mark
|
|
602
|
+
* is owed a finding and the ask stays open.
|
|
603
|
+
*
|
|
604
|
+
* The defect this answers: a recall probe found OSLER DELPHI, the closure stage pointed at
|
|
605
|
+
* `register-findings.md` — the run's own working sheet, where the mark genuinely IS reasoned — and the
|
|
606
|
+
* ask closed. The sheet is a citable file and the citation was true. It is simply not the document the
|
|
607
|
+
* client reads, and nothing checked that.
|
|
608
|
+
*
|
|
609
|
+
* ✕ SCOPED TO `ask:recall:` ASKS CARRYING A `mark_text`, deliberately, and this is the whole difference
|
|
610
|
+
* between the two readings of the ruling. `ask:recall-overflow:` rows carry a `term` — an owner name, a
|
|
611
|
+
* law firm, a probe never dispatched (Lewis Silkin LLP, NORDWEST Handel AG). Measured across three runs
|
|
612
|
+
* they are 28-29 per run and constant whether the run was healthy or not; reopening them would put
|
|
613
|
+
* names no lawyer asked for into the findings. A `mark_text` is the test because a mark is what a
|
|
614
|
+
* finding is about.
|
|
615
|
+
*
|
|
616
|
+
* Returns the owed row, or null when the ask is out of scope or the mark was delivered. PURE.
|
|
617
|
+
*/
|
|
618
|
+
export function recallMarkOwed(ask, delivered) {
|
|
619
|
+
if (!String(ask?.ask_id ?? "").startsWith("ask:recall:")) return null;
|
|
620
|
+
const st = ask?.ask?.structured ?? {};
|
|
621
|
+
const mark = String(st.mark_text ?? "").trim();
|
|
622
|
+
if (!mark) return null; // an owner/term probe is not a mark ask
|
|
623
|
+
if (delivered instanceof Set && delivered.has(normMark(mark))) return null;
|
|
624
|
+
return {
|
|
625
|
+
ask_id: ask.ask_id,
|
|
626
|
+
mark,
|
|
627
|
+
owner: String(st.owner ?? "").trim() || null,
|
|
628
|
+
uri: String(st.uri ?? "").trim() || null,
|
|
629
|
+
// null delivered = findings.json unreadable. Recorded so a reader can tell "the client was not shown
|
|
630
|
+
// this mark" from "nobody could tell what the client was shown" — they need different repairs.
|
|
631
|
+
basis: delivered instanceof Set ? "absent-from-findings" : "findings-unreadable",
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
|
|
577
635
|
/**
|
|
578
636
|
* The anti-confabulation guard, ask side (doubt-ledger applyClosure reused shape-for-shape): an
|
|
579
637
|
* IMMATERIAL line ends its ask IFF the ask is still open AND the quote appears VERBATIM
|
|
@@ -586,7 +644,9 @@ export function applyAskClosure(asks, closureLines, fileTexts = {}, { ts = null
|
|
|
586
644
|
const byId = new Map();
|
|
587
645
|
for (const l of closureLines ?? []) if (l?.id && !byId.has(l.id)) byId.set(l.id, l);
|
|
588
646
|
const unverified = [];
|
|
647
|
+
const carryIntoFindings = [];
|
|
589
648
|
let immaterialByStage = 0;
|
|
649
|
+
const delivered = deliveredMarks(fileTexts?.["findings.json"]);
|
|
590
650
|
const out = (asks ?? []).map((a) => {
|
|
591
651
|
if (a?.ending) return a; // the stage may never touch an ended ask
|
|
592
652
|
const l = byId.get(a.ask_id);
|
|
@@ -595,13 +655,21 @@ export function applyAskClosure(asks, closureLines, fileTexts = {}, { ts = null
|
|
|
595
655
|
const hay = squash(fileTexts?.[l.file]);
|
|
596
656
|
const q = squash(l.quote);
|
|
597
657
|
if (q && hay && hay.includes(q)) {
|
|
658
|
+
// THE CITATION IS VERIFIED AND THAT IS NO LONGER ENOUGH FOR A RECALL ASK ABOUT A MARK.
|
|
659
|
+
const owed = recallMarkOwed(a, delivered);
|
|
660
|
+
if (owed) {
|
|
661
|
+
carryIntoFindings.push(owed);
|
|
662
|
+
return { ...a, handoff: clip(`the recall probe found ${owed.mark} and the delivered findings do not name it — `
|
|
663
|
+
+ `the closure cited ${l.file}, which is the run's own working sheet, not what the client was shown. `
|
|
664
|
+
+ `This mark is owed a finding.`, 300) };
|
|
665
|
+
}
|
|
598
666
|
immaterialByStage++;
|
|
599
667
|
return { ...a, ending: mkEnding("judged-immaterial", "doubt-closure-stage", { evidence: `${l.file}: "${clip(l.quote)}"`, reasons: [l.reason], ts }) };
|
|
600
668
|
}
|
|
601
669
|
unverified.push({ ask_id: a.ask_id, file: l.file, quote: l.quote });
|
|
602
670
|
return a;
|
|
603
671
|
});
|
|
604
|
-
return { asks: out, immaterialByStage, unverified };
|
|
672
|
+
return { asks: out, immaterialByStage, unverified, carryIntoFindings };
|
|
605
673
|
}
|
|
606
674
|
|
|
607
675
|
/** Stamp the default handoff on every still-open ask (after closure) — an OPEN ask always names
|
|
@@ -210,6 +210,38 @@ export function contradictionFor(reason, row, scope) {
|
|
|
210
210
|
+ `own-right or duplicate-of-delivered.`;
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
+
// ── ECONOMY IS NOT A REASON ABOUT A LIVE IN-CLASS MARK (owner ruling 2026-09-07) ─────────────────
|
|
214
|
+
//
|
|
215
|
+
// `not-worth-the-line` means "it fits none of the four manageable categories and is not relevant
|
|
216
|
+
// enough to drive the read". That is a statement about the REPORT'S BUDGET, not about the record. On a
|
|
217
|
+
// live registration inside one of the matter's own instructed classes it is the one thing the seat may
|
|
218
|
+
// not say: the reader is paying for a judgment about that mark, and "there was no room" is not one.
|
|
219
|
+
//
|
|
220
|
+
// Measured on the run that prompted the ruling: 26 of 49 declinations at this seam were this token,
|
|
221
|
+
// and two of the lawyer's own reference marks were among them — one carried on every earlier run.
|
|
222
|
+
//
|
|
223
|
+
// ✕ WHY THIS IS NOT THE REFUSAL THAT WAS REMOVED BELOW, and the difference is the whole reason it can
|
|
224
|
+
// stand. That one refused `unrelated-goods` and `off-field-not-major` on class overlap, fired on 170
|
|
225
|
+
// of 170 records, and made the ENGINE decide relatedness — the lawyer's call. This one refuses ONE
|
|
226
|
+
// token and leaves every substantive reason available. The seat may still decline this record: it may
|
|
227
|
+
// say the goods are unrelated, that it is off-field and not a major brand, that it is the applicant's
|
|
228
|
+
// own right, or that it is already delivered. What it may no longer do is decline it for taking up
|
|
229
|
+
// space. The refusal is about the SHAPE of the reason, not about whether the record is relevant, so
|
|
230
|
+
// the relatedness judgment stays exactly where it was.
|
|
231
|
+
//
|
|
232
|
+
// It does effectively retire this token for live in-class records, because the sweep is already scoped
|
|
233
|
+
// to the instructed classes. That was put to the owner as the consequence and ruled anyway.
|
|
234
|
+
if (reason === "not-worth-the-line" && isLive(row?.status) && shared.length) {
|
|
235
|
+
return `this record is ${String(row?.status ?? "").trim() || "live"} and sits in class `
|
|
236
|
+
+ `${shared.join(", ")} — one of the matter's own instructed classes. "not worth the line" is a `
|
|
237
|
+
+ `statement about the report's budget, not about this mark, and a live registration in an `
|
|
238
|
+
+ `instructed class is never omitted for want of room. Either carry it, or decline it on something `
|
|
239
|
+
+ `true of THIS MARK: unrelated-goods if its goods do not meet the matter's, off-field-not-major `
|
|
240
|
+
+ `if it is off-field and not a major brand, an active dispute or a well-known enforcer, `
|
|
241
|
+
+ `own-right if it belongs to the applicant, duplicate-of-delivered if the same right is already `
|
|
242
|
+
+ `reported. Say what makes this mark distant, or give it its line.`;
|
|
243
|
+
}
|
|
244
|
+
|
|
213
245
|
// THERE IS NO CLASS-OVERLAP REFUSAL, AND THE FIRST DRAFT HAD ONE. It refused `unrelated-goods` and
|
|
214
246
|
// `off-field-not-major` over any record sharing one of the matter's filed classes, on the reasoning
|
|
215
247
|
// that the driver holds both class lists and they disagree with the stated reason.
|
package/driver/driver.config.mjs
CHANGED
|
@@ -612,6 +612,26 @@ const MODEL_FAMILY_RE = /(?:^|\/)(?:claude-)?(opus|sonnet|haiku)(?:[-.]|$)/i;
|
|
|
612
612
|
// mini one would then read as agreement, which is exactly the corruption this gauge exists to catch.
|
|
613
613
|
const OPENAI_ID_RE = /^(?:openai\/)?(gpt-[\w.-]+|o\d[\w.-]*)$/i;
|
|
614
614
|
const DATE_SUFFIX_RE = /-(?:\d{8}|\d{4}-\d{2}-\d{2})$/;
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Is this model id a DATED SNAPSHOT, or an undated alias that a provider may repoint?
|
|
618
|
+
*
|
|
619
|
+
* `modelBasis: "actual"` says the provider answered rather than that we guessed — it does not say the
|
|
620
|
+
* answer names a fixed build. Measured across three archived runs: haiku came back
|
|
621
|
+
* `claude-haiku-4-5-20251001`, opus and sonnet came back `claude-opus-5` and `claude-sonnet-5`. All
|
|
622
|
+
* three were recorded identically as observed, and two of them name something the provider can move
|
|
623
|
+
* underneath us. A snapshot rotation behind either alias between two runs leaves EXACTLY that record and
|
|
624
|
+
* is invisible, which is how an A/B across time ends up eliminating the seat on evidence that could not
|
|
625
|
+
* have shown it either way.
|
|
626
|
+
*
|
|
627
|
+
* Returns "snapshot", "alias", or null when there is no id to judge — null is a could-not-look and is
|
|
628
|
+
* never collapsed into "alias". PURE.
|
|
629
|
+
*/
|
|
630
|
+
export function modelSnapshotKind(model) {
|
|
631
|
+
const id = String(model ?? "").trim();
|
|
632
|
+
if (!id) return null;
|
|
633
|
+
return DATE_SUFFIX_RE.test(id) ? "snapshot" : "alias";
|
|
634
|
+
}
|
|
615
635
|
export function modelFamily(model) {
|
|
616
636
|
if (!model) return null;
|
|
617
637
|
const resolved = resolveModel(String(model));
|
|
@@ -1120,6 +1120,10 @@ serve({
|
|
|
1120
1120
|
properties: {
|
|
1121
1121
|
recordId: { type: "string", description: "Copied VERBATIM from the filings you were given." },
|
|
1122
1122
|
read: { type: "string", description: "What you concluded about THIS filing." },
|
|
1123
|
+
// Declared here for the reason the comment above gives: `additionalProperties: false`
|
|
1124
|
+
// means an undeclared key is a contradiction between the prose and the schema, and the
|
|
1125
|
+
// seat resolves it whichever way it happens to trust.
|
|
1126
|
+
band: { type: "string", description: "OPTIONAL — how you rate THIS filing, in the framework's own band words. Send it when you formed a view on the filing itself; the card prints it as that filing's rating. Omit it and your read still prints, with no rating claimed." },
|
|
1123
1127
|
},
|
|
1124
1128
|
},
|
|
1125
1129
|
},
|
package/driver/gateway.mjs
CHANGED
|
@@ -13,7 +13,7 @@ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, wri
|
|
|
13
13
|
import { join, dirname, basename } from "node:path";
|
|
14
14
|
import { driverDir } from "../shared/driver-dir.mjs"; //
|
|
15
15
|
import { tmpdir } from "node:os";
|
|
16
|
-
import { config, resolveModel, modelFamily, envOn, envGateOn } from "./driver.config.mjs";
|
|
16
|
+
import { config, resolveModel, modelFamily, modelSnapshotKind, envOn, envGateOn } from "./driver.config.mjs";
|
|
17
17
|
import { stageLog, runLog, note, outputMeta } from "./log.mjs";
|
|
18
18
|
// — the closed disposition set has ONE author; this file dictates it and must not retype it.
|
|
19
19
|
import { DISPOSITIONS, POSITION_REQUIRED_DISPOSITIONS } from "./findings-model.mjs";
|
|
@@ -1142,6 +1142,11 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
|
|
|
1142
1142
|
const modelRequested = engine.resolveModelId ? engine.resolveModelId(model) : resolveModel(model);
|
|
1143
1143
|
const modelActual = (typeof turn.modelWire === "string" && turn.modelWire) ? turn.modelWire : null;
|
|
1144
1144
|
const modelBasis = modelActual ? "actual" : "unknown";
|
|
1145
|
+
// WHETHER THE OBSERVED ID NAMES A FIXED BUILD. `modelBasis: "actual"` says the provider answered,
|
|
1146
|
+
// not that the answer is pinned: two of the three tiers come back as undated aliases the provider
|
|
1147
|
+
// may repoint, and recorded beside a dated one they read identically. null when there is nothing to
|
|
1148
|
+
// judge, never collapsed into "alias".
|
|
1149
|
+
const modelSnapshot = modelSnapshotKind(modelActual);
|
|
1145
1150
|
if (modelActual) lastModelWire = modelActual; // — never overwritten with null
|
|
1146
1151
|
// The comparison is by FAMILY (driver.config modelFamily), because `--model haiku` legitimately comes
|
|
1147
1152
|
// back as `claude-haiku-4-5-20251001`. THREE-VALUED: null when either side names no family this
|
|
@@ -1570,7 +1575,7 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
|
|
|
1570
1575
|
// modelMismatch — true/false when both sides name a family, null when either does not.
|
|
1571
1576
|
// Written even on the rows where they are null, so "this engine cannot report" stays visibly
|
|
1572
1577
|
// different from "this record predates the gauge".
|
|
1573
|
-
modelActual, modelBasis, modelMismatch,
|
|
1578
|
+
modelActual, modelBasis, modelSnapshot, modelMismatch,
|
|
1574
1579
|
// W3 billing telemetry: which engine ran + the RESOLVED billing mode (subscription vs api-key). This
|
|
1575
1580
|
// records INTENT (the mode the engine was configured to bill under), not independent billing evidence
|
|
1576
1581
|
// — the actual proof is the provider console (claude's stream also reports apiKeySource; codex does
|
|
@@ -1705,7 +1710,7 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
|
|
|
1705
1710
|
event: "attempt", stage: name, attempt, of: maxRetries + 1, ok: !fail, fail: fail ?? null,
|
|
1706
1711
|
//: the spine carries the same pair as the per-stage log, or the two disagree about what
|
|
1707
1712
|
// ran. `model` stays the requested resolution (its existing readers); `modelActual` is the wire.
|
|
1708
|
-
model: modelRequested, modelActual, modelBasis, modelMismatch,
|
|
1713
|
+
model: modelRequested, modelActual, modelBasis, modelSnapshot, modelMismatch,
|
|
1709
1714
|
wrote, warm: warm || undefined, warmEscalated: attempt === warmEscalatedAt || undefined,
|
|
1710
1715
|
rescued: rescued ?? undefined, killed: killed || undefined,
|
|
1711
1716
|
quiescentMs: Number.isFinite(quiescentMs) ? Math.round(quiescentMs) : undefined, // — see the per-stage row
|
|
@@ -154,7 +154,11 @@ const DECLARED = Object.freeze({
|
|
|
154
154
|
// "the lane that produced the record is a driver fact". The chip is derived from this joined list and
|
|
155
155
|
// from the finding's own receipted evidence — never from a word the seat typed about itself.
|
|
156
156
|
"marks.findings": ["ordinal", "name", "owner", "band", "net", "type", "evidence", "basis", "weighedFilings"],
|
|
157
|
-
|
|
157
|
+
// `band` is the rater's rating OF THAT FILING, optional, in the framework's own ladder words. It is
|
|
158
|
+
// declared here as well as in the tool schema because this allowlist — not the schema — is what the
|
|
159
|
+
// driver validates against: a key the seat sends and this list omits is refused, so the read would
|
|
160
|
+
// arrive banded and land unbanded with nothing saying why.
|
|
161
|
+
"marks.registerReads": ["recordId", "read", "band"],
|
|
158
162
|
"marks.negatives": ["term", "source", "note"],
|
|
159
163
|
});
|
|
160
164
|
|
package/driver/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "prelim-driver",
|
|
3
3
|
"private": true,
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.3.0-beta.0",
|
|
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": {
|
package/driver/pipeline.mjs
CHANGED
|
@@ -107,14 +107,15 @@ import { mintCrossCheckDoubts, mintContradictionDoubts, stitchDoubts, applyClosu
|
|
|
107
107
|
import { readAcceptedClosures } from "./doubt-closure-tool.mjs";
|
|
108
108
|
import { CLOSURE_EVIDENCE_FILES } from "./doubt-closure-call.mjs";
|
|
109
109
|
import { doubtsForClosure, doubtSelectionNote } from "./doubt-selection.mjs"; // doubt-closure selection
|
|
110
|
-
import { deriveAsks, applyAskClosure, finalizeOpenHandoffs, summarizeAsks } from "./ask-ledger.mjs";
|
|
110
|
+
import { deriveAsks, applyAskClosure, finalizeOpenHandoffs, summarizeAsks } from "./ask-ledger.mjs";
|
|
111
|
+
import { pendingWhatIf, claimWhatIf, finishWhatIf, whatIfRefusal } from "./whatif-queue.mjs"; // tracker issue 240 — a queued job is settled before its run archives // PR-6 — every ask ends (2026-07-29)
|
|
111
112
|
import { mintPresenceDoubts } from "./presence-reconciliation.mjs"; // presence-or-reason (2026-07-22 — the unjoined-Sheet-2 leak)
|
|
112
113
|
import { escalatedAxes } from "./skeptic-record.mjs"; // THE escalation parse — shared with the record_skeptic transport so the rendered shape and this read cannot drift
|
|
113
114
|
// — every placed candidate ends somewhere a reader can see; the ones that do not are counted by name
|
|
114
115
|
import { PLACEMENT_CARRY_SCHEMA_VERSION, reconcilePlacementCarry, placementCarryEvent, mintPlacementCarryDoubts, entryUris } from "./placement-carry.mjs";
|
|
115
116
|
import { FLOOR_DUTY_SCHEMA_VERSION, reconcileFloorDuty, floorDutyEvent, armFloorDuty, floorDutyArmed, floorDutyBlock, floorDutyBlocksSkip } from "./floor-duty.mjs"; // — the era stamp that turns disclosure into a delivery floor
|
|
116
117
|
import { synthesisDutyForRun } from "./synthesis-record.mjs"; // — the duty checked against the DELIVERED document
|
|
117
|
-
import { RECORD_CARRY_SCHEMA_VERSION, traceRecordCarry, parseStageOutcomes, recordCarryEvent, mintRecordCarryDoubts, bandRecordUri, placementIndex, findingUris , silentlyLostFindings } from "./record-carry.mjs";
|
|
118
|
+
import { RECORD_CARRY_SCHEMA_VERSION, traceRecordCarry, parseStageOutcomes, recordCarryEvent, mintRecordCarryDoubts, bandRecordUri, placementIndex, findingUris , silentlyLostFindings, statedDivergenceFindings } from "./record-carry.mjs";
|
|
118
119
|
import { reconcileSurfaceDuty, surfaceDutyNote } from "./surface-duty.mjs"; // item 3 — silence at the findings surface, read off the rows above
|
|
119
120
|
import { DISCARD_LEDGER_NAME, seamRows, appendDiscardRows, foldDiscardLedger } from "./record-discard.mjs";
|
|
120
121
|
import { readDeclinations } from "./declination-tool.mjs"; // — synthesis's own stated declines
|
|
@@ -1532,6 +1533,13 @@ function attachProfile(ctx, job, { write = true } = {}) {
|
|
|
1532
1533
|
const current = resolveProfile(job);
|
|
1533
1534
|
if (current && current.key !== sidecar.profileKey)
|
|
1534
1535
|
runLog(ctx.paths.runDir, { event: "profile-mismatch", sidecar: sidecar.profileKey, resolved: current.key });
|
|
1536
|
+
// THE MATCH IS RECORDED TOO, and that is the point rather than symmetry for its own sake.
|
|
1537
|
+
// `profile-mismatch` existed and its counterpart did not, so "the profile was right" was carried
|
|
1538
|
+
// by the ABSENCE of a row — and an absence cannot tell "it matched" from "the probe never ran"
|
|
1539
|
+
// (it throws below on a broken profiles/ state) or from "this run predates the probe". Three
|
|
1540
|
+
// different facts, one empty grep. The positive row says which.
|
|
1541
|
+
else if (current)
|
|
1542
|
+
runLog(ctx.paths.runDir, { event: "profile-resolved", sidecar: sidecar.profileKey, resolved: current.key });
|
|
1535
1543
|
} catch (e) {
|
|
1536
1544
|
runLog(ctx.paths.runDir, { event: "profile-mismatch-probe-failed", error: String(e.message).slice(0, 120) });
|
|
1537
1545
|
}
|
|
@@ -4651,7 +4659,7 @@ export function buildFailurePacket({ runId, agent, job = {}, failedStage, shortR
|
|
|
4651
4659
|
// was healthy and answering, re-triggering now hits the same cap, and "overloaded" points the reader
|
|
4652
4660
|
// at an availability problem they cannot act on.
|
|
4653
4661
|
//
|
|
4654
|
-
// The owner pre-committed the wording watching
|
|
4662
|
+
// The owner pre-committed the wording watching a capped run: a cap death is reported as a
|
|
4655
4663
|
// SUBSCRIPTION OUTCOME, NEVER AS AN ENGINE FINDING. So this names the cap, states how long the run
|
|
4656
4664
|
// actually held out before giving up, and says plainly that nothing about the run or the mark
|
|
4657
4665
|
// produced it.
|
|
@@ -6135,8 +6143,70 @@ function sentinel(runDir, name, obj) {
|
|
|
6135
6143
|
atomicWrite(join(runDir, name), JSON.stringify({ ts: new Date().toISOString(), ...obj }, null, 2) + "\n");
|
|
6136
6144
|
}
|
|
6137
6145
|
|
|
6146
|
+
/**
|
|
6147
|
+
* Settle every queued what-if BEFORE the run dir moves into the archive — tracker issue 240.
|
|
6148
|
+
*
|
|
6149
|
+
* A queued job lives under the run directory, and the worker enumerates candidates with `liveRunDirs`,
|
|
6150
|
+
* which skips `archive` outright. So archiving carried a pending job out of the worker's reach: not
|
|
6151
|
+
* claimed, not settled, not refused. The client who asked for it got no answer and no explanation, and
|
|
6152
|
+
* no row anywhere said why. That is an absence reported as nothing at all — the one shape
|
|
6153
|
+
* `whatIfRefusal` exists to prevent.
|
|
6154
|
+
*
|
|
6155
|
+
* BEFORE the rename, deliberately: the settlement is written into the run dir and travels with it, so
|
|
6156
|
+
* the answer is on disk in the archive where the read tool will look for it.
|
|
6157
|
+
*
|
|
6158
|
+
* ✕ THE SENTENCE IS PER KIND, and a memo does not get the stage one. `whatIfRefusal`'s standing
|
|
6159
|
+
* refusal — "this run is delivered or archived, what-if runs on live runs only" — is TRUE of a stage
|
|
6160
|
+
* and FALSE of a memo, which runs on archived evidence by design. Telling a client their memo was
|
|
6161
|
+
* refused because the run is archived would be a false sentence in the one place they go to find out
|
|
6162
|
+
* what happened. A memo is closed on the true reason instead: it was queued before the run archived,
|
|
6163
|
+
* and the drain does not reach an archived run.
|
|
6164
|
+
*
|
|
6165
|
+
* ✕ A JOB ANOTHER WORKER IS ACTIVELY RUNNING IS LEFT ALONE. `pendingWhatIf` returns queued jobs and
|
|
6166
|
+
* claims older than an hour, never a fresh claim, so a live worker's job is not settled underneath it.
|
|
6167
|
+
* A fresh claim at the moment of archiving is a race this does not close, and it is written down
|
|
6168
|
+
* rather than papered over.
|
|
6169
|
+
*
|
|
6170
|
+
* Best-effort throughout: a failure to settle must never stop the archive, because a run that cannot
|
|
6171
|
+
* archive is a much larger problem than a job with no row.
|
|
6172
|
+
*/
|
|
6173
|
+
export function settlePendingWhatIfsBeforeArchive(run) {
|
|
6174
|
+
let pending = [];
|
|
6175
|
+
// NOT a silent catch. This function exists because a job went unanswered with no row anywhere; a
|
|
6176
|
+
// failure to enumerate that returned quietly would reproduce exactly that, one level up, and the
|
|
6177
|
+
// archive would still succeed so nothing downstream would look wrong.
|
|
6178
|
+
try { pending = pendingWhatIf(run.runDir); }
|
|
6179
|
+
catch (e) { note(`what-if settle before archive could not enumerate the queue (${String(e?.message ?? e).slice(0, 120)}) — pending jobs may be unanswered`); return; }
|
|
6180
|
+
for (const entry of pending) {
|
|
6181
|
+
try {
|
|
6182
|
+
// CLAIM FIRST, exactly as the worker does. `finishWhatIf` writes the terminal file and does not
|
|
6183
|
+
// remove the `.json`; the claim is what renames it out of the queue. Settling without claiming
|
|
6184
|
+
// writes a `.failed` beside a `.json` that still reads as QUEUED to every reader — the job would
|
|
6185
|
+
// look pending and answered at the same time, which is worse than the silence being fixed here.
|
|
6186
|
+
const job = claimWhatIf(entry);
|
|
6187
|
+
if (!job) continue; // another worker won it, or it vanished — not ours to settle
|
|
6188
|
+
const kind = job?.op?.kind === "memo" ? "memo" : "stage";
|
|
6189
|
+
// ✕ THE SENTENCE IS PER KIND. The standing refusal — "what-if runs on live runs only" — is TRUE
|
|
6190
|
+
// of a stage and FALSE of a memo, which reasons over archived evidence by design. A memo is
|
|
6191
|
+
// closed on the true reason instead.
|
|
6192
|
+
const error = kind === "memo"
|
|
6193
|
+
? "this what-if was queued before its run was archived, and the drain does not reach an archived run — ask it again against the archived run, which a memo may read"
|
|
6194
|
+
: (whatIfRefusal({ location: "archive", state: run.state ?? null, kind }) ?? "this run is delivered or archived — what-if runs on live runs only");
|
|
6195
|
+
finishWhatIf(run.runDir, entry.id, { ok: false, op: job.op ?? null, error });
|
|
6196
|
+
try { runLog(run.runDir, { event: "whatif-settled-on-archive", id: entry.id, kind, stale: entry.stale }); }
|
|
6197
|
+
catch { /* the row is written; the log line is a convenience and must not undo it */ }
|
|
6198
|
+
} catch (e) {
|
|
6199
|
+
// One job's settlement must not cost the others, or the archive — but it must not be SILENT
|
|
6200
|
+
// either. A swallowed failure here leaves exactly the unanswered job this function exists to
|
|
6201
|
+
// prevent, and the archive still succeeds, so nothing downstream looks wrong.
|
|
6202
|
+
note(`what-if ${entry.id} could not be settled before archive (${String(e?.message ?? e).slice(0, 120)}) — it may be unanswered`);
|
|
6203
|
+
}
|
|
6204
|
+
}
|
|
6205
|
+
}
|
|
6206
|
+
|
|
6138
6207
|
function archive(run) {
|
|
6139
6208
|
try {
|
|
6209
|
+
settlePendingWhatIfsBeforeArchive(run);
|
|
6140
6210
|
mkdirSync(dirname(run.archiveDir), { recursive: true });
|
|
6141
6211
|
renameSync(run.runDir, run.archiveDir);
|
|
6142
6212
|
return run.archiveDir;
|
|
@@ -13215,6 +13285,30 @@ async function pipelineInner(job, opts = {}) {
|
|
|
13215
13285
|
note(`recall: ${silent.lost.length} position(s) the digest ended as FINDINGS reached no client `
|
|
13216
13286
|
+ `surface and no step said why — ${silent.lost.map((l) => `${l.mark ?? l.uri} (${l.reason})`).join("; ")}`);
|
|
13217
13287
|
}
|
|
13288
|
+
// ── AND THE STATED CASE, WHICH IS THE ONE THAT REACHED A CLIENT (tracker issue 248) ────────
|
|
13289
|
+
//
|
|
13290
|
+
// Same inputs, same seam, same best-effort contract — a second call rather than a widened first
|
|
13291
|
+
// one, because the sibling's population boundary is deliberate and correct. It covers a
|
|
13292
|
+
// finding-ending followed by SILENCE; this covers one followed by a SENTENCE.
|
|
13293
|
+
//
|
|
13294
|
+
// On a delivered R2 run the sibling logged `checked:5 matched:5 lost:0` — correctly — on a
|
|
13295
|
+
// delivery missing two marks from the lawyer's final list, one rated HIGH. They were dropped with
|
|
13296
|
+
// a reason, so they were outside its population by design. A drop with a reason nobody verifies
|
|
13297
|
+
// is not accounted for; it is unexamined with a paper trail, and that delivery carried 66 of them.
|
|
13298
|
+
const stated = statedDivergenceFindings({
|
|
13299
|
+
reconciliation: safeReadJson(P.recallReconciliation),
|
|
13300
|
+
carryRows: safeReadJson(P.recordCarry)?.rows ?? null,
|
|
13301
|
+
digestFindingUris: recordedFindingUris(P.runDir),
|
|
13302
|
+
});
|
|
13303
|
+
runLog(P.runDir, { event: "stated-divergence-findings", computable: stated.computable,
|
|
13304
|
+
reason: stated.reason, population_empty: stated.population_empty === true,
|
|
13305
|
+
checked: stated.checked, matched: stated.matched, diverged: stated.diverged.length,
|
|
13306
|
+
marks: stated.diverged.map((d) => d.mark).filter(Boolean).slice(0, 10) });
|
|
13307
|
+
if (stated.diverged.length) {
|
|
13308
|
+
note(`recall: ${stated.diverged.length} position(s) the digest ended as FINDINGS are absent from `
|
|
13309
|
+
+ "the findings and were dropped with a STATED reason pointing at another artifact — "
|
|
13310
|
+
+ stated.diverged.map((d) => `${d.mark ?? d.uri} (${d.reason}${d.cites_artifact ? ` → ${d.cites_artifact.join(", ")}` : ""})`).join("; "));
|
|
13311
|
+
}
|
|
13218
13312
|
} catch (e) { /* never mask a delivery */ }
|
|
13219
13313
|
// — the common-law path, and the jx zh slice on the same tracer. `consumed` used to read
|
|
13220
13314
|
// CLEAROTRON_JX_CONSUME, which decided whether a zh candidate could reach a finding at all: with it
|
|
@@ -13486,6 +13580,21 @@ async function pipelineInner(job, opts = {}) {
|
|
|
13486
13580
|
const askApplied = applyAskClosure(runAsks, acc.ask, fileTexts, { ts: askTs });
|
|
13487
13581
|
runAsks = askApplied.asks;
|
|
13488
13582
|
for (const u of askApplied.unverified) runLog(run.runDir, { event: "ask-closure-unverified", ...u });
|
|
13583
|
+
// Option A (tracker issue 246): a recall ask whose mark is not in the delivered findings
|
|
13584
|
+
// did NOT close. Each one is a mark the run found and the client was not shown, so it is
|
|
13585
|
+
// recorded by name rather than left to be inferred from an ask that merely stayed open.
|
|
13586
|
+
// ✕ NOT written into findings.json here. See the sidecar's own note: a row appended after
|
|
13587
|
+
// the drafting stage would carry no band and no reasoning.
|
|
13588
|
+
for (const c of askApplied.carryIntoFindings ?? [])
|
|
13589
|
+
runLog(run.runDir, { event: "ask-closure-mark-owed", ...c });
|
|
13590
|
+
if (askApplied.carryIntoFindings?.length)
|
|
13591
|
+
writeFileSync(driverDir(run.runDir, "marks-owed-a-finding.json"),
|
|
13592
|
+
JSON.stringify({
|
|
13593
|
+
_provenance: "recall asks that could not close because the mark they found is absent from findings.json — owner ruling 2026-09-07, tracker issue 246 Option A, worded about the MARK",
|
|
13594
|
+
_notWritten: "these are NOT appended to findings.json: a finding carries a band, a net line and a legal position, and a row minted here would carry none of them. The repair belongs where the findings are authored.",
|
|
13595
|
+
count: askApplied.carryIntoFindings.length,
|
|
13596
|
+
marks: askApplied.carryIntoFindings,
|
|
13597
|
+
}, null, 2) + "\n");
|
|
13489
13598
|
}
|
|
13490
13599
|
} else {
|
|
13491
13600
|
note(`doubt-closure failed (non-fatal — the open doubts/asks ship OPEN, as without the stage): ${dc.fail}`);
|
|
@@ -15481,6 +15590,17 @@ export async function runExperiment(job, opts) {
|
|
|
15481
15590
|
// what RAN.
|
|
15482
15591
|
engine: experimentEngineName(),
|
|
15483
15592
|
modelTier: model ?? null,
|
|
15593
|
+
// — THE RATING AUTHORITY THIS ARM RAN UNDER, on the arm's own record.
|
|
15594
|
+
//
|
|
15595
|
+
// `whatIfRun` resolves this correctly and returned it in memory only: nothing in the experiment
|
|
15596
|
+
// directory named the profile it rated under, so a reader coming to the arm tomorrow could not
|
|
15597
|
+
// confirm which framework produced it, and a future regression would be as silent as the one this
|
|
15598
|
+
// was opened on. The frozen sidecar is the authority — never a fresh resolve, which is the mid-run
|
|
15599
|
+
// drift the freeze exists to forbid.
|
|
15600
|
+
//
|
|
15601
|
+
// `null` means the run carries no frozen profile (a legacy run), and it is written rather than
|
|
15602
|
+
// omitted: absent and "there was none" are different facts and only one of them is a defect.
|
|
15603
|
+
ratedUnder: ctx.profile?.profileKey ?? null,
|
|
15484
15604
|
dispatchTrigger: opts.dispatchTrigger ?? "fresh",
|
|
15485
15605
|
// — WHICH PASS THIS ARM REPRODUCED, in a word. null on a non-corrective arm;
|
|
15486
15606
|
// never "dispatched-warm", which is production's alone.
|
|
@@ -701,7 +701,7 @@ export function competitorClaimChecks({ text, ownerScreen, recordsByUri, markVoc
|
|
|
701
701
|
// ── COVERAGE CLAIMS IN PROSE vs WHAT THE RUN ACTUALLY SEARCHED (tracker issue 134) ──────────────────
|
|
702
702
|
//
|
|
703
703
|
// THE DEFECT. `coverage_line:` is code-stamped from scope-facts.json; the narrative is model-written
|
|
704
|
-
// prose. Nothing bound them to one searched-territory set. On
|
|
704
|
+
// prose. Nothing bound them to one searched-territory set. On one recorded run the masthead read
|
|
705
705
|
// `registers: JP, WO` while the narrative said "Register searches covered Japan and Korea" — one of
|
|
706
706
|
// them was wrong and nothing detected it until a human compared the two surfaces by eye. They agree
|
|
707
707
|
// on today's runs because a prompt fix stopped the input contradicting itself, which is evidence the
|
|
@@ -2454,6 +2454,13 @@ export function flagLines(failures) {
|
|
|
2454
2454
|
// vendor vocabulary, no ids, no paths, no field names, no instructions to whoever regenerates the run,
|
|
2455
2455
|
// never "certified"/"signed". deliveryVocabViolations() below enforces that mechanically.
|
|
2456
2456
|
const DELIVERY_LINES = {
|
|
2457
|
+
// Keyed on the FULL check id, which deliveryFlagLines resolves before the base — see the note there.
|
|
2458
|
+
// The sentence states the CONSEQUENCE, in the receipt's own terms: not "a check failed" but "the rule
|
|
2459
|
+
// was applied to nothing". Eleven graded runs delivered carrying this, two of them orderable product
|
|
2460
|
+
// demos, and the only place it was written was one row of a fifty-odd-row receipt nobody routes.
|
|
2461
|
+
"narrative-write-ups:could-not-read":
|
|
2462
|
+
"The depth rules for the written-up findings were applied to nothing on this run — the narrative "
|
|
2463
|
+
+ "carries no block this check can read, so neither the ranking cut nor the length cap was verified.",
|
|
2457
2464
|
// template — the summary table against the names actually searched
|
|
2458
2465
|
"names-cell-populated": "The summary table does not name the mark that was searched.",
|
|
2459
2466
|
"assessment-row": "A searched name has no assessment of its own in the summary table.",
|
|
@@ -2555,15 +2562,26 @@ export function deliveryVocabViolations(line) {
|
|
|
2555
2562
|
export function deliveryFlagLines(failures) {
|
|
2556
2563
|
const groups = new Map();
|
|
2557
2564
|
for (const f of failures ?? []) {
|
|
2558
|
-
const
|
|
2565
|
+
const id = String(f?.id ?? "");
|
|
2566
|
+
const base = id.split(":")[0];
|
|
2559
2567
|
const family = String(f?.family ?? "");
|
|
2560
|
-
|
|
2561
|
-
|
|
2568
|
+
// A CHECK WHOSE DISTINCTION LIVES IN ITS SUFFIX COULD NOT BE SAID HERE (tracker issue 267).
|
|
2569
|
+
//
|
|
2570
|
+
// Grouping on `base` alone is right for the common case — a word-cap violation on nine write-ups is
|
|
2571
|
+
// one delivery line, not nine. But it also collapsed `narrative-write-ups:could-not-read` into the
|
|
2572
|
+
// same bucket as an ordinary depth violation, and those are different facts: one says a rule was
|
|
2573
|
+
// broken, the other says the rule was ENFORCED ON NOTHING. The second had no sentence of its own to
|
|
2574
|
+
// reach, so it degraded to the generic "a machine check did not pass" and became invisible.
|
|
2575
|
+
//
|
|
2576
|
+
// So: an entry keyed on the FULL id wins where one exists, and everything else groups by base
|
|
2577
|
+
// exactly as before. This adds no line to any run that did not already produce one.
|
|
2578
|
+
const key = DELIVERY_LINES[id] ? id : (base || `family/${family}`);
|
|
2579
|
+
const g = groups.get(key) ?? { base, family, id: DELIVERY_LINES[id] ? id : null, n: 0 };
|
|
2562
2580
|
g.n += 1;
|
|
2563
2581
|
groups.set(key, g);
|
|
2564
2582
|
}
|
|
2565
2583
|
return [...groups.values()].map((g) => {
|
|
2566
|
-
const sentence = DELIVERY_LINES[g.base] ?? DELIVERY_FAMILY_LINES[g.family] ?? DELIVERY_GENERIC;
|
|
2584
|
+
const sentence = (g.id && DELIVERY_LINES[g.id]) ?? DELIVERY_LINES[g.base] ?? DELIVERY_FAMILY_LINES[g.family] ?? DELIVERY_GENERIC;
|
|
2567
2585
|
// Belt and braces: a table entry that trips the house rules degrades to the generic instead of
|
|
2568
2586
|
// shipping. This is what makes "no engine vocabulary reaches a reader" structural rather than a
|
|
2569
2587
|
// matter of everyone remembering the rule when they add a check.
|
|
@@ -68,9 +68,13 @@ export const knockoutStatement = (framework, marks) =>
|
|
|
68
68
|
// 11pt, no <!DOCTYPE>, no <head>, no <meta charset> — and they rendered a Depth 2 report as a grey
|
|
69
69
|
// spreadsheet with the register hit-counts the customer paid for buried in a table cell. The report now
|
|
70
70
|
// renders through publish/render-knockout.mjs in the product's own design language, off the same shared
|
|
71
|
-
// stylesheet and brand tokens as the clearance report.
|
|
72
|
-
//
|
|
73
|
-
//
|
|
71
|
+
// stylesheet and brand tokens as the clearance report.
|
|
72
|
+
//
|
|
73
|
+
// THE REVIEWER'S NOTES ARE ON THE REPORT SINCE 2026-09-07 (owner ruling, tracker issue 274). This
|
|
74
|
+
// paragraph used to end "internal working material (the purple staff notes, the model's registerEstimate)
|
|
75
|
+
// is not IN the report; it lives in the audit workbook". That is now true of `registerEstimate` only: the
|
|
76
|
+
// notes render on the page, labelled, and the workbook keeps its copy. See render-knockout.mjs's header
|
|
77
|
+
// for the ruling and reviewerNotesBlock for the shape.
|
|
74
78
|
|
|
75
79
|
// ── The workbook: the skill's three sheet names + columns, plus the conditional sheets below ────────
|
|
76
80
|
// ── ONE DRILL-THROUGH KEY: `<MARK> #<ordinal>`, per mark ──────────────────────────────────────
|
|
@@ -250,9 +254,11 @@ export async function buildKnockoutWorkbook(findings, receipts, outPath, registe
|
|
|
250
254
|
// ── The email: a COVER NOTE. Headline band, one line per mark, the report link. Nothing else. ────────
|
|
251
255
|
// The report is the deliverable and the email points at it — the same doctrine the clearance lane
|
|
252
256
|
// follows. The old `delivery.email === 'table'` overlay inlined the full review table into the mail
|
|
253
|
-
// body,
|
|
254
|
-
//
|
|
255
|
-
//
|
|
257
|
+
// body, which put the findings in front of whoever the mail reached rather than whoever opened the
|
|
258
|
+
// report, and made a second rendering of them in a second dialect — a second thing to keep true.
|
|
259
|
+
// (That overlay's other charge, that it shipped an INTERNAL variant, no longer describes anything: the
|
|
260
|
+
// 2026-09-07 ruling put the reviewer's notes on the one report. The reason above is the load-bearing one
|
|
261
|
+
// and it is unaffected — the mail stays a cover note.) Per-lawyer client formatting is drafted by the assistant from the
|
|
256
262
|
// run's report-data.json, not by a template knob in here.
|
|
257
263
|
// `reports` is publishKnockout's own list — `[{mark, url}]`, one per published document — and
|
|
258
264
|
// `auditUrl` is composed by the publisher from the pool URL it already holds. Neither is derived here.
|