clearotron 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +52 -0
- package/INSTALL.md +4 -3
- package/README.md +2 -1
- package/bin/example.mjs +88 -27
- package/bin/onboard.mjs +40 -2
- package/bin/start.mjs +7 -0
- 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 +30 -0
- package/driver/ask-ledger.mjs +69 -1
- package/driver/package.json +1 -1
- package/driver/pipeline.mjs +41 -2
- package/driver/predelivery-lint.mjs +1 -1
- 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/suite-census.json +46 -16
- package/driver/verify.mjs +2 -2
- package/mcp-server/CHANGELOG.md +8 -0
- package/mcp-server/lib/whatif.mjs +10 -1
- package/mcp-server/package.json +1 -1
- package/package.json +1 -1
- package/portal-ui/package.json +1 -1
- package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -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/env-audit.mjs +20 -0
- package/scripts/headless-page.mjs +225 -0
- package/scripts/home-render-check.mjs +2 -1
- 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/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/.env.example
CHANGED
|
@@ -403,3 +403,55 @@ CLEAROTRON_DEMO=
|
|
|
403
403
|
# Read by driver/whatif-worker.mjs. Refs tracker issue 1953.
|
|
404
404
|
# effect: tuning
|
|
405
405
|
CLEAROTRON_WHATIF_MAX_CONCURRENT=1
|
|
406
|
+
|
|
407
|
+
# The checkout every shipped systemd unit interpolates — and the one name in this file you are most
|
|
408
|
+
# likely to meet before you meet this file. Ten units name it in their `ExecStart`
|
|
409
|
+
# (clearotron-portal, -worker, -deploy, -client-mcp, -mcp-face, prelim-driver, prelim-outbox, the two
|
|
410
|
+
# mcp-server/remote units and the courtlistener bridge), and production's units all depend on it.
|
|
411
|
+
#
|
|
412
|
+
# YOU ALMOST CERTAINLY DO NOT SET THIS. `clearotron connect` writes it into the install's env file, set
|
|
413
|
+
# to the checkout it just installed (bin/connect.mjs, shared/checkout-move.mjs), and it rewrites it when
|
|
414
|
+
# the checkout moves. So this is not a decision waiting for you at install time, and the row does not
|
|
415
|
+
# exist because a deployment cannot be configured without it.
|
|
416
|
+
#
|
|
417
|
+
# IT EXISTS BECAUSE OF WHO READS IT AT 2AM. Somebody whose unit will not start reads
|
|
418
|
+
# `${CLEAROTRON_CHECKOUT_DIR}` in an ExecStart, comes to the file INSTALL.md §8 calls the catalogue, and
|
|
419
|
+
# finds nothing. `shared/names-in-force.mjs` already knew the name; neither governance document did.
|
|
420
|
+
# A row that says "the installer writes this" is the answer to their question — the absence was not.
|
|
421
|
+
#
|
|
422
|
+
# Surfaced by the cut-over overlay control once the withheld `ops/` bucket was laid back over the public
|
|
423
|
+
# tree: the ratchets that would have caught it had had no subject since the cut, so this is a gap of
|
|
424
|
+
# long standing rather than a regression. Refs tracker issue 213.
|
|
425
|
+
#
|
|
426
|
+
# Left EMPTY here on purpose. A path in this file would read as a default; there is none, and the
|
|
427
|
+
# installer supplies the real value.
|
|
428
|
+
# effect: deployment
|
|
429
|
+
CLEAROTRON_CHECKOUT_DIR=
|
|
430
|
+
|
|
431
|
+
# ── The release pipeline's own two names ─────────────────────────────────────────────────────────
|
|
432
|
+
# Both are set by `.github/workflows/release.yml` and read by the release scripts. NEITHER is an
|
|
433
|
+
# operator's decision, and on an install both are unset and the defaults are the right answer. They are
|
|
434
|
+
# catalogued for the same reason CLEAROTRON_CHECKOUT_DIR above is: they are OUR names, read by code that
|
|
435
|
+
# ships, and a reader who meets one in a workflow log needs somewhere to look it up.
|
|
436
|
+
#
|
|
437
|
+
# They are NOT in the system-owned list beside GITHUB_OUTPUT, and the line between them is worth stating:
|
|
438
|
+
# GitHub sets GITHUB_*, so a row would document somebody else's decision. We set these, so a row
|
|
439
|
+
# documents ours.
|
|
440
|
+
#
|
|
441
|
+
# Found by the audit while closing tracker issue 213, not by that issue's diagnosis — they arrived with
|
|
442
|
+
# tonight's release-pipeline work and the catalogue ratchet caught them on the next run, which is the
|
|
443
|
+
# ratchet doing its job. Raised with the release lane rather than silently absorbed.
|
|
444
|
+
|
|
445
|
+
# The commit the cut decision is ABOUT. The workflow sets it to `github.sha` — the push — because the
|
|
446
|
+
# checkout may have moved on since, and a decision made about "whatever HEAD is now" tried to republish
|
|
447
|
+
# an already-released version. Unset falls back to HEAD, which is correct for the cron and for a hand
|
|
448
|
+
# run, where nothing has moved. Read by scripts/release-cut-decision.mjs.
|
|
449
|
+
# effect: deployment
|
|
450
|
+
CLEAROTRON_CUT_REF=
|
|
451
|
+
|
|
452
|
+
# How long publishing waits for a parked version run, in whole milliseconds. Unset uses the built-in
|
|
453
|
+
# budget; the workflow sets it to 0 on a `workflow_dispatch` rehearsal so a dry run does not sit out the
|
|
454
|
+
# full wait. A malformed value REFUSES rather than defaulting — taken as 0 it would skip the wait
|
|
455
|
+
# entirely, which is that script not running at all. Read by scripts/release-await-cut.mjs.
|
|
456
|
+
# effect: tuning
|
|
457
|
+
CLEAROTRON_RELEASE_WAIT_MS=
|
package/INSTALL.md
CHANGED
|
@@ -115,12 +115,13 @@ Nothing here requires an agent platform or a database — reports are written to
|
|
|
115
115
|
|
|
116
116
|
```
|
|
117
117
|
npm install -g clearotron # stable — the tested one
|
|
118
|
-
npm install -g clearotron@beta # newest —
|
|
118
|
+
npm install -g clearotron@beta # newest — cut when there is something worth testing
|
|
119
119
|
```
|
|
120
120
|
|
|
121
121
|
A **stable** has run a real clearance end to end and had a from-scratch install driven by somebody who has
|
|
122
|
-
never seen the product, before it was published. A **beta** is published
|
|
123
|
-
|
|
122
|
+
never seen the product, before it was published. A **beta** is published when a change lands that is worth
|
|
123
|
+
testing, or while a stable is being prepared — days apart, not on every merge; it built and the suite
|
|
124
|
+
passed, and nothing has driven a live register through it.
|
|
124
125
|
|
|
125
126
|
**Upgrade production to stables only.** What each channel promises and how often one is cut:
|
|
126
127
|
**[docs/RELEASES.md](docs/RELEASES.md)**.
|
package/README.md
CHANGED
|
@@ -39,7 +39,8 @@ Node 22 or newer, on macOS, Linux, or Windows via WSL2. That puts `clearotron` o
|
|
|
39
39
|
command below works in that short form.
|
|
40
40
|
|
|
41
41
|
That command gives you the **stable** release — the one that has run a real clearance end to end before it
|
|
42
|
-
was published. If you want the newest code instead,
|
|
42
|
+
was published. If you want the newest code instead, a beta is published whenever there is something worth
|
|
43
|
+
testing — deliberately, days apart, not on every merge:
|
|
43
44
|
|
|
44
45
|
```bash
|
|
45
46
|
npm install -g clearotron@beta
|
package/bin/example.mjs
CHANGED
|
@@ -40,7 +40,7 @@ import "../shared/env-local.mjs"; // step 4 / — FIRST: this program read a
|
|
|
40
40
|
import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
41
41
|
import { homedir, tmpdir } from "node:os";
|
|
42
42
|
import { invoke } from "../shared/invocation.mjs"; // — the printed command is resolved once, for the reader who is actually standing there
|
|
43
|
-
import { join, resolve,
|
|
43
|
+
import { basename, dirname, join, resolve, sep } from "node:path";
|
|
44
44
|
import { fileURLToPath } from "node:url";
|
|
45
45
|
import { spawn } from "node:child_process";
|
|
46
46
|
import { BRAND } from "../shared/brand.mjs"; // — the installer's own name, from the tenant seam
|
|
@@ -100,11 +100,26 @@ const DEMO_ROOT = join(REPO, "demo");
|
|
|
100
100
|
// name is PRINTED below rather than assumed, because "the demo" is about to mean one of several.
|
|
101
101
|
const wanted = flag("--product");
|
|
102
102
|
const children = demoChildren(DEMO_ROOT);
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
103
|
+
|
|
104
|
+
// ── ALL OF THEM, UNLESS THE CALLER NARROWED IT (tracker issue 277) ──────────────────────────────────
|
|
105
|
+
//
|
|
106
|
+
// This used to replay `children[0]` and stop. The package ships one finished report per product, and a
|
|
107
|
+
// first-time reader met one of them with nothing on screen saying the other three existed — so three
|
|
108
|
+
// quarters of what was shipped was reachable only by someone who already knew to ask for it. The owner's
|
|
109
|
+
// ruling is that they auto-load: "it should auto-load since it is NOT obvious these runs are there unless
|
|
110
|
+
// you know about it."
|
|
111
|
+
//
|
|
112
|
+
// `--product` and `--run-dir` still narrow to one, because "show me just this one" is a real thing to
|
|
113
|
+
// want. Neither given now means every product this tree ships, in the container's own order.
|
|
114
|
+
const sampleDirs = flag("--run-dir")
|
|
115
|
+
? [resolve(flag("--run-dir"))]
|
|
116
|
+
: wanted
|
|
117
|
+
? [resolve(join(DEMO_ROOT, wanted))]
|
|
118
|
+
: children.map((c) => resolve(join(DEMO_ROOT, c)));
|
|
119
|
+
// The refusal below is about ONE directory, and with nothing shipped there is no directory to name — so
|
|
120
|
+
// the container itself is what it looks at, which is what it always did when `demo/` was empty.
|
|
121
|
+
const sampleDir = sampleDirs[0] ?? resolve(DEMO_ROOT);
|
|
122
|
+
if (!sampleDirs.length || !isFrozen(sampleDir)) {
|
|
108
123
|
// AN ABSENCE IS A FINDING, AND IT NAMES WHAT IT LOOKED AT. This exits 1 and always has; tracker issue
|
|
109
124
|
// 2193 reported it exiting 0, which did not reproduce at v0.1.0 or at main's tip. An arm pins it.
|
|
110
125
|
die(
|
|
@@ -128,10 +143,17 @@ if (!isFrozen(sampleDir)) {
|
|
|
128
143
|
// PUBLISHING WRITES A RECEIPT INTO THE RUN DIRECTORY, and `demo/` is tracked — so a reader who only READ
|
|
129
144
|
// the demo came back to a dirty checkout. `publishSource` is the one definition of that rule, shared with
|
|
130
145
|
// the launcher, which seeds the pool from the same container on every `--demo` start.
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const
|
|
134
|
-
|
|
146
|
+
// EVERY ONE THAT WAS ASKED FOR IS READ BEFORE ANY IS PUBLISHED, so a manifest missing a runId is a
|
|
147
|
+
// refusal about that demo by name rather than a partial pool nobody can account for.
|
|
148
|
+
const samples = sampleDirs.map((dir) => {
|
|
149
|
+
const manifest = join(dir, "meta.json");
|
|
150
|
+
if (!isFrozen(dir)) die(`demo: ${dir} is not a frozen demo — it holds no meta.json and lane entry file.`);
|
|
151
|
+
const m = JSON.parse(readFileSync(manifest, "utf8"));
|
|
152
|
+
if (!m?.runId) die(`example: ${manifest} names no runId — it is not a frozen example manifest.`);
|
|
153
|
+
return { dir, meta: m, publishFrom: publishSource(dir, { repoRoot: REPO }), name: basename(dir) };
|
|
154
|
+
});
|
|
155
|
+
const publishFrom = samples[0].publishFrom;
|
|
156
|
+
const meta = samples[0].meta;
|
|
135
157
|
|
|
136
158
|
// ── 2. the pool guard ────────────────────────────────────────────────────────────────────────────────
|
|
137
159
|
// Resolve through symlinks. A $HOME that resolves inside the archive is exactly the shape a `===` test
|
|
@@ -192,19 +214,38 @@ if (existsSync(poolRoot) && !statSync(poolRoot).isDirectory()) die(`demo: ${pool
|
|
|
192
214
|
|
|
193
215
|
// ── 3. replay ────────────────────────────────────────────────────────────────────────────────────────
|
|
194
216
|
console.log(`\n ${BRAND.name} ${BRAND.product.toLowerCase()} — demo\n`);
|
|
195
|
-
console.log(
|
|
217
|
+
console.log(samples.length === 1
|
|
218
|
+
? ` sample: ${samples[0].dir}`
|
|
219
|
+
: ` samples: ${samples.length} — ${samples.map((x) => x.name).join(", ")}`);
|
|
196
220
|
console.log(` pool: ${poolRoot}\n`);
|
|
197
221
|
|
|
198
222
|
mkdirSync(poolRoot, { recursive: true });
|
|
199
223
|
const { republishRun } = await import(join(REPO, "driver", "publish", "report-registry.mjs"));
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
224
|
+
|
|
225
|
+
// ── ONE FAILURE MUST NOT COST THE OTHERS, AND MUST NOT BE A QUIET COUNT (tracker issue 277) ─────────
|
|
226
|
+
//
|
|
227
|
+
// Publishing stopped at the first error, which was right when there was one demo and is wrong now: a
|
|
228
|
+
// reader whose knockout capture is unreadable should still get the other three, and should be TOLD which
|
|
229
|
+
// one is missing. "3 published" with no other line is the shape this repository calls a silent count —
|
|
230
|
+
// the number is true and the reader cannot tell it is short.
|
|
231
|
+
//
|
|
232
|
+
// The failures are collected and reported together at the end, and the process exits non-zero, because a
|
|
233
|
+
// demo that came up missing a quarter of itself is not a success however good the three look.
|
|
234
|
+
const results = [];
|
|
235
|
+
const failures = [];
|
|
236
|
+
for (const s0 of samples) {
|
|
237
|
+
try {
|
|
238
|
+
// poolUrl "" on purpose: the report's own link block is for a deployment that serves the pool at a
|
|
239
|
+
// public URL. This one is served from this process, at a port picked below.
|
|
240
|
+
results.push({ ...s0, published: await republishRun({ runId: s0.meta.runId, meta: s0.meta, pool: poolRoot, poolUrl: "", runDir: join(s0.publishFrom, "run") }) });
|
|
241
|
+
} catch (e) {
|
|
242
|
+
failures.push({ name: s0.name, why: String(e?.message ?? e) });
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (!results.length) {
|
|
246
|
+
die(`demo: no demo could be replayed.`, "", ...failures.map((f) => ` ${f.name}: ${f.why}`));
|
|
207
247
|
}
|
|
248
|
+
const published = results[0].published;
|
|
208
249
|
|
|
209
250
|
// THE LABEL. The reader is about to look at a document that reads like advice about a real mark. It is
|
|
210
251
|
// not, and the demo says so before the browser opens rather than in a footnote nobody reaches.
|
|
@@ -227,18 +268,34 @@ console.log(" re-rendered from its artifacts. It is an example, not advice.\n")
|
|
|
227
268
|
//
|
|
228
269
|
// The third branch is the point: a lane whose publisher reports no count says so. This line printed a
|
|
229
270
|
// bare "?" to the knockout — a could-not-look wearing the costume of a number.
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
271
|
+
// PER LANE, FOR EVERY DEMO — not for the first one with the rest reduced to a product name. The three
|
|
272
|
+
// branches below are the whole point of this sentence: two lanes count different populations and a third
|
|
273
|
+
// counts none, and printing one lane's number beside four reports would state the wrong population three
|
|
274
|
+
// times.
|
|
275
|
+
const spineOf = (pub) =>
|
|
276
|
+
Number.isFinite(pub.counts?.findings)
|
|
277
|
+
? `${pub.counts.findings} finding(s) recorded in the run's audit spine; the report shows
|
|
233
278
|
the ones it retains`
|
|
234
|
-
: Number.isFinite(
|
|
235
|
-
? `${
|
|
236
|
-
evidence, on ${
|
|
279
|
+
: Number.isFinite(pub.receipts?.findings)
|
|
280
|
+
? `${pub.receipts.findings} finding(s) with citations traced to this run's own held
|
|
281
|
+
evidence, on ${pub.receipts.citing}/${pub.receipts.marks} mark(s)`
|
|
237
282
|
: `this lane's publisher reported no finding count — the report itself is the record`;
|
|
238
|
-
console.log(` published: ${published.runId}
|
|
283
|
+
for (const r of results) console.log(` published: ${r.published.runId}\n ${r.name} — ${spineOf(r.published)}`);
|
|
284
|
+
if (results.length > 1) console.log(`\n ${results.length} demo reports are published and listed — one per product.`);
|
|
285
|
+
// LOUD, AND ON STDERR, AND NON-ZERO. Said after the successes so a reader sees what they DID get first,
|
|
286
|
+
// and cannot mistake the run for a clean one.
|
|
287
|
+
if (failures.length) {
|
|
288
|
+
console.error(`\n ${failures.length} of ${samples.length} demo(s) could NOT be replayed:`);
|
|
289
|
+
for (const f of failures) console.error(` ${f.name}: ${f.why}`);
|
|
290
|
+
console.error(` The portal below lists the ${results.length} that published. This exits non-zero.`);
|
|
291
|
+
process.exitCode = 1;
|
|
292
|
+
}
|
|
239
293
|
|
|
240
294
|
if (has("--once")) {
|
|
241
|
-
|
|
295
|
+
// EVERY REPORT THAT WAS PUBLISHED, not the first one. Naming one of four here is the same defect as
|
|
296
|
+
// publishing one of four: the reader is handed a path and has no way to learn the others exist.
|
|
297
|
+
console.log("");
|
|
298
|
+
for (const r of results) console.log(` report: ${join(poolRoot, r.published.runId, "report.html")}`);
|
|
242
299
|
//, criterion 5 — SAY WHAT WAS CREATED, ON EVERY PATH THAT CREATES SOMETHING.
|
|
243
300
|
//
|
|
244
301
|
// The portal path below prints this and `--once` did not, so the one invocation a reader is most
|
|
@@ -251,7 +308,11 @@ if (has("--once")) {
|
|
|
251
308
|
// does — worse than silence, because it reads as an answer.
|
|
252
309
|
const created = flag("--pool") ? poolRoot : demoBase;
|
|
253
310
|
console.log(` Removing it later is one directory: rm -rf ${created}\n`);
|
|
254
|
-
|
|
311
|
+
// NOT A BARE ZERO. A demo that failed to replay one of its four sets `exitCode` above, and exiting 0
|
|
312
|
+
// here would discard it — printing the failure and then reporting success, which is the shape this
|
|
313
|
+
// change exists to remove. `--once` is also the invocation a script is most likely to use, so it is
|
|
314
|
+
// the one where a swallowed code does the most damage.
|
|
315
|
+
process.exit(process.exitCode ?? 0);
|
|
255
316
|
}
|
|
256
317
|
|
|
257
318
|
/**
|
package/bin/onboard.mjs
CHANGED
|
@@ -2311,7 +2311,12 @@ export async function runCheck() {
|
|
|
2311
2311
|
// rather than beside the unit, because the unit running and the address being reachable are
|
|
2312
2312
|
// different facts and the second is the one a client depends on.
|
|
2313
2313
|
const { clientDoorReachability } = await import(join(REPO, "shared", "client-door.mjs"));
|
|
2314
|
-
|
|
2314
|
+
// FROM THE FILE THE UNITS LOAD, NOT THE SHELL THIS COMMAND WAS TYPED IN (the tracker issue 226
|
|
2315
|
+
// family, met again here). On a hosted box the published address lives in the units' environment,
|
|
2316
|
+
// and reading `process.env` reported "no client connector address is published — that is correct
|
|
2317
|
+
// for a local install" about a deployment that publishes one. Every verdict below rests on this
|
|
2318
|
+
// value, including the audience comparison, so a wrong reading here is not one wrong line.
|
|
2319
|
+
const published = effectiveForService("CLEAROTRON_CLIENT_MCP_URL")?.v ?? null;
|
|
2315
2320
|
// A SHORT, CHEAP ASK, and a failure to reach IS the finding rather than a failure to look — the
|
|
2316
2321
|
// property under test is reachability itself. What would be dishonest is calling an address green
|
|
2317
2322
|
// because nothing asked, which is why `probe` stays null when there is nothing to ask about.
|
|
@@ -2327,8 +2332,13 @@ export async function runCheck() {
|
|
|
2327
2332
|
// returns null when the header is absent — a looked-and-none answer, not a did-not-look — and
|
|
2328
2333
|
// the readers separate those, so a probe that omits the field reads as never-looked rather
|
|
2329
2334
|
// than silently as "no challenge".
|
|
2335
|
+
// THE REDIRECT TARGET TRAVELS WITH IT (tracker issue 241). This request is already made with
|
|
2336
|
+
// `redirect: "manual"`, so the Location an unauthenticated caller is handed is right here —
|
|
2337
|
+
// and it is where Cloudflare Access puts the audience. Reading it from THIS response rather
|
|
2338
|
+
// than asking a second time keeps `doctor` to one request per address, and keeps both answers
|
|
2339
|
+
// about the same moment.
|
|
2330
2340
|
probe = { ok: res.status < 500, status: res.status, error: null,
|
|
2331
|
-
challenge: res.headers.get("www-authenticate") };
|
|
2341
|
+
challenge: res.headers.get("www-authenticate"), location: res.headers.get("location") };
|
|
2332
2342
|
} catch (e) { probe = { ok: false, status: null, error: String(e?.cause?.code ?? e?.name ?? e?.message ?? e) }; }
|
|
2333
2343
|
}
|
|
2334
2344
|
const reach = clientDoorReachability({ url: published, probe });
|
|
@@ -2336,6 +2346,34 @@ export async function runCheck() {
|
|
|
2336
2346
|
else if (reach.state === "fail") problem(reach.message);
|
|
2337
2347
|
else info(reach.message);
|
|
2338
2348
|
|
|
2349
|
+
// ── AND IS IT THE AUDIENCE THIS INSTALL CHECKS AGAINST? (tracker issue 241) ───────────────────
|
|
2350
|
+
//
|
|
2351
|
+
// The other half of the recreation trap, and the half with no symptom of its own. Deleting and
|
|
2352
|
+
// recreating an Access application changes the audience; the existing warning fires on the
|
|
2353
|
+
// CHALLENGE being wrong, so on a box where somebody recreated the application and then fixed the
|
|
2354
|
+
// sign-in, everything above reads healthy and the stale audience stays invisible until a real
|
|
2355
|
+
// request is rejected.
|
|
2356
|
+
//
|
|
2357
|
+
// No new credential and no provider API: the audience is in the redirect handed to a caller with
|
|
2358
|
+
// no session at all, which is the request just made.
|
|
2359
|
+
{
|
|
2360
|
+
const { readAudience, audienceVerdict } = await import(join(REPO, "shared", "access-audience.mjs"));
|
|
2361
|
+
const configuredAud = effectiveForService("CLEAROTRON_OIDC_AUDIENCE")?.v ?? "";
|
|
2362
|
+
const read = readAudience(probe ?? { error: "the published address was never asked" });
|
|
2363
|
+
// NOTHING IS CLAIMED ABOUT A BOX THAT USES NO EDGE. With no audience configured AND no Access
|
|
2364
|
+
// challenge in front of the address, there is no question here — a local install is not a
|
|
2365
|
+
// misconfigured hosted one, and saying so every time is how a reader learns to skim this.
|
|
2366
|
+
if (configuredAud || read.kind !== "not-fronted") {
|
|
2367
|
+
const v = audienceVerdict({ configured: configuredAud, read });
|
|
2368
|
+
if (v.ok) ok(v.message);
|
|
2369
|
+
// A COULD-NOT-LOOK IS SAID, NOT RAISED. The line above has already reported the address as
|
|
2370
|
+
// unreachable; repeating it as a second failure teaches the reader that this section
|
|
2371
|
+
// double-counts. It still states what was NOT established, which is the whole job.
|
|
2372
|
+
else if (v.kind === "could-not-look") info(v.message);
|
|
2373
|
+
else problem(v.message);
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2339
2377
|
// ── CAN AN ASSISTANT ACTUALLY SIGN IN? (tracker issue 149, opt-in) ────────────────────────────
|
|
2340
2378
|
//
|
|
2341
2379
|
// The check above reads the CHALLENGE FORM, which is the first of two provider settings that decide
|
package/bin/start.mjs
CHANGED
|
@@ -1057,6 +1057,12 @@ if (isMain) {
|
|
|
1057
1057
|
// — `clearotron demo` hands over to this — so fixing the player alone left the defect where it was.
|
|
1058
1058
|
const { publishSource } = await import("../driver/demo-container.mjs");
|
|
1059
1059
|
const seed = await seedPool({ pool: paths.pool, examplesDir: publishSource(join(REPO, "demo"), { repoRoot: REPO }), republish: republishRun });
|
|
1060
|
+
// WHAT WAS ALREADY THERE IS SAID TOO (tracker issue 277). This branch used to run only when the pool
|
|
1061
|
+
// was empty; it now tops a stale pool up to the package's set, so "seeded 1" on an upgrade is a fact
|
|
1062
|
+
// about what was MISSING and says nothing on its own about how many are now listed.
|
|
1063
|
+
if (seed.already?.length) {
|
|
1064
|
+
say(` archive ${seed.already.length} example report(s) already published here`);
|
|
1065
|
+
}
|
|
1060
1066
|
if (seed.seeded.length) {
|
|
1061
1067
|
say(` seeded ${seed.seeded.length} example report(s) into ${paths.pool}`);
|
|
1062
1068
|
// THE LABEL. is delivered: the report now carries the owner's own sample sentence on its
|
|
@@ -1070,6 +1076,7 @@ if (isMain) {
|
|
|
1070
1076
|
}
|
|
1071
1077
|
// Never a silent nothing. "The archive is empty" and "the archive is empty and nobody noticed why"
|
|
1072
1078
|
// look identical in the browser, so both other outcomes are said out loud.
|
|
1079
|
+
if (seed.skipped) say(` archive ${seed.skipped}`);
|
|
1073
1080
|
for (const p of seed.problems) err(` WARNING: sample seeding — ${p}`);
|
|
1074
1081
|
} catch (e) {
|
|
1075
1082
|
err(` WARNING: the example report could not be seeded (${String(e?.message ?? e)}) — the archive will come up empty. Everything else works; \`npm run example\` shows a sample without touching this install.`);
|
package/build-info.json
CHANGED
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,35 @@
|
|
|
1
1
|
# prelim-driver
|
|
2
2
|
|
|
3
|
+
## 0.2.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 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.
|
|
8
|
+
- 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.
|
|
9
|
+
- 0f7b44a: New: `clearotron demo` now publishes all four example reports — one per product — instead of only the first.
|
|
10
|
+
- 2138a3c: For operators: The `beta` channel now gets a release when there is something worth testing, days apart, instead of one on every merge.
|
|
11
|
+
- 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.
|
|
12
|
+
- 77cf56d: For operators: The configuration reference now explains the two deprecated search-log variables in full, instead of stopping mid-sentence.
|
|
13
|
+
- 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.
|
|
14
|
+
|
|
15
|
+
## 0.2.1-beta.2
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- 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.
|
|
20
|
+
|
|
21
|
+
## 0.2.1-beta.1
|
|
22
|
+
|
|
23
|
+
### Patch Changes
|
|
24
|
+
|
|
25
|
+
- 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.
|
|
26
|
+
|
|
27
|
+
## 0.2.1-beta.0
|
|
28
|
+
|
|
29
|
+
### Patch Changes
|
|
30
|
+
|
|
31
|
+
- 77cf56d: For operators: The configuration reference now explains the two deprecated search-log variables in full, instead of stopping mid-sentence.
|
|
32
|
+
|
|
3
33
|
## 0.2.0
|
|
4
34
|
|
|
5
35
|
### 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
|
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.2.
|
|
5
|
+
"version": "0.2.1",
|
|
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
|
@@ -114,7 +114,7 @@ import { escalatedAxes } from "./skeptic-record.mjs"; // THE escalation parse
|
|
|
114
114
|
import { PLACEMENT_CARRY_SCHEMA_VERSION, reconcilePlacementCarry, placementCarryEvent, mintPlacementCarryDoubts, entryUris } from "./placement-carry.mjs";
|
|
115
115
|
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
116
|
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";
|
|
117
|
+
import { RECORD_CARRY_SCHEMA_VERSION, traceRecordCarry, parseStageOutcomes, recordCarryEvent, mintRecordCarryDoubts, bandRecordUri, placementIndex, findingUris , silentlyLostFindings, statedDivergenceFindings } from "./record-carry.mjs";
|
|
118
118
|
import { reconcileSurfaceDuty, surfaceDutyNote } from "./surface-duty.mjs"; // item 3 — silence at the findings surface, read off the rows above
|
|
119
119
|
import { DISCARD_LEDGER_NAME, seamRows, appendDiscardRows, foldDiscardLedger } from "./record-discard.mjs";
|
|
120
120
|
import { readDeclinations } from "./declination-tool.mjs"; // — synthesis's own stated declines
|
|
@@ -4651,7 +4651,7 @@ export function buildFailurePacket({ runId, agent, job = {}, failedStage, shortR
|
|
|
4651
4651
|
// was healthy and answering, re-triggering now hits the same cap, and "overloaded" points the reader
|
|
4652
4652
|
// at an availability problem they cannot act on.
|
|
4653
4653
|
//
|
|
4654
|
-
// The owner pre-committed the wording watching
|
|
4654
|
+
// The owner pre-committed the wording watching a capped run: a cap death is reported as a
|
|
4655
4655
|
// SUBSCRIPTION OUTCOME, NEVER AS AN ENGINE FINDING. So this names the cap, states how long the run
|
|
4656
4656
|
// actually held out before giving up, and says plainly that nothing about the run or the mark
|
|
4657
4657
|
// produced it.
|
|
@@ -13215,6 +13215,30 @@ async function pipelineInner(job, opts = {}) {
|
|
|
13215
13215
|
note(`recall: ${silent.lost.length} position(s) the digest ended as FINDINGS reached no client `
|
|
13216
13216
|
+ `surface and no step said why — ${silent.lost.map((l) => `${l.mark ?? l.uri} (${l.reason})`).join("; ")}`);
|
|
13217
13217
|
}
|
|
13218
|
+
// ── AND THE STATED CASE, WHICH IS THE ONE THAT REACHED A CLIENT (tracker issue 248) ────────
|
|
13219
|
+
//
|
|
13220
|
+
// Same inputs, same seam, same best-effort contract — a second call rather than a widened first
|
|
13221
|
+
// one, because the sibling's population boundary is deliberate and correct. It covers a
|
|
13222
|
+
// finding-ending followed by SILENCE; this covers one followed by a SENTENCE.
|
|
13223
|
+
//
|
|
13224
|
+
// On R2 `russet-kestrel` the sibling logged `checked:5 matched:5 lost:0` — correctly — on a
|
|
13225
|
+
// delivery missing two marks from the lawyer's final list, one rated HIGH. They were dropped with
|
|
13226
|
+
// a reason, so they were outside its population by design. A drop with a reason nobody verifies
|
|
13227
|
+
// is not accounted for; it is unexamined with a paper trail, and that delivery carried 66 of them.
|
|
13228
|
+
const stated = statedDivergenceFindings({
|
|
13229
|
+
reconciliation: safeReadJson(P.recallReconciliation),
|
|
13230
|
+
carryRows: safeReadJson(P.recordCarry)?.rows ?? null,
|
|
13231
|
+
digestFindingUris: recordedFindingUris(P.runDir),
|
|
13232
|
+
});
|
|
13233
|
+
runLog(P.runDir, { event: "stated-divergence-findings", computable: stated.computable,
|
|
13234
|
+
reason: stated.reason, population_empty: stated.population_empty === true,
|
|
13235
|
+
checked: stated.checked, matched: stated.matched, diverged: stated.diverged.length,
|
|
13236
|
+
marks: stated.diverged.map((d) => d.mark).filter(Boolean).slice(0, 10) });
|
|
13237
|
+
if (stated.diverged.length) {
|
|
13238
|
+
note(`recall: ${stated.diverged.length} position(s) the digest ended as FINDINGS are absent from `
|
|
13239
|
+
+ "the findings and were dropped with a STATED reason pointing at another artifact — "
|
|
13240
|
+
+ stated.diverged.map((d) => `${d.mark ?? d.uri} (${d.reason}${d.cites_artifact ? ` → ${d.cites_artifact.join(", ")}` : ""})`).join("; "));
|
|
13241
|
+
}
|
|
13218
13242
|
} catch (e) { /* never mask a delivery */ }
|
|
13219
13243
|
// — the common-law path, and the jx zh slice on the same tracer. `consumed` used to read
|
|
13220
13244
|
// CLEAROTRON_JX_CONSUME, which decided whether a zh candidate could reach a finding at all: with it
|
|
@@ -13486,6 +13510,21 @@ async function pipelineInner(job, opts = {}) {
|
|
|
13486
13510
|
const askApplied = applyAskClosure(runAsks, acc.ask, fileTexts, { ts: askTs });
|
|
13487
13511
|
runAsks = askApplied.asks;
|
|
13488
13512
|
for (const u of askApplied.unverified) runLog(run.runDir, { event: "ask-closure-unverified", ...u });
|
|
13513
|
+
// Option A (tracker issue 246): a recall ask whose mark is not in the delivered findings
|
|
13514
|
+
// did NOT close. Each one is a mark the run found and the client was not shown, so it is
|
|
13515
|
+
// recorded by name rather than left to be inferred from an ask that merely stayed open.
|
|
13516
|
+
// ✕ NOT written into findings.json here. See the sidecar's own note: a row appended after
|
|
13517
|
+
// the drafting stage would carry no band and no reasoning.
|
|
13518
|
+
for (const c of askApplied.carryIntoFindings ?? [])
|
|
13519
|
+
runLog(run.runDir, { event: "ask-closure-mark-owed", ...c });
|
|
13520
|
+
if (askApplied.carryIntoFindings?.length)
|
|
13521
|
+
writeFileSync(driverDir(run.runDir, "marks-owed-a-finding.json"),
|
|
13522
|
+
JSON.stringify({
|
|
13523
|
+
_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",
|
|
13524
|
+
_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.",
|
|
13525
|
+
count: askApplied.carryIntoFindings.length,
|
|
13526
|
+
marks: askApplied.carryIntoFindings,
|
|
13527
|
+
}, null, 2) + "\n");
|
|
13489
13528
|
}
|
|
13490
13529
|
} else {
|
|
13491
13530
|
note(`doubt-closure failed (non-fatal — the open doubts/asks ship OPEN, as without the stage): ${dc.fail}`);
|
|
@@ -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
|