clearotron 0.2.1 → 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/INSTALL.md +5 -4
- package/bin/example.mjs +9 -5
- package/bin/onboard.mjs +19 -19
- package/bin/stop.mjs +65 -3
- package/build-info.json +2 -2
- package/driver/CHANGELOG.md +21 -0
- 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 +83 -2
- package/driver/predelivery-lint.mjs +22 -4
- 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/record-carry.mjs +2 -2
- package/driver/reference-score.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 +66 -18
- package/driver/unit-inventory.mjs +47 -0
- package/driver/unit-state-verdict.mjs +8 -8
- package/driver/verify-knockout.mjs +9 -1
- package/driver/whatif-memo-run.mjs +45 -4
- package/mcp-server/CHANGELOG.md +2 -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/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 +2 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/scripts/drain-preflight.mjs +2 -2
- package/scripts/freeze-example-run.mjs +3 -3
- package/scripts/headless-page.mjs +51 -2
- package/scripts/live-surface-check.mjs +86 -17
- package/scripts/render-check.mjs +61 -2
- package/scripts/deploy-test.sh +0 -309
package/INSTALL.md
CHANGED
|
@@ -41,10 +41,11 @@ run is [mcp-server/CONNECT.md](mcp-server/CONNECT.md), and why something is the
|
|
|
41
41
|
- **Node.js >= 22**, and npm. A hard floor: `package.json` declares it, `.nvmrc` pins it, and the free
|
|
42
42
|
US register runs on `node:sqlite`, which ships with FTS5 from Node 22. Node 20 installs and then fails
|
|
43
43
|
at the first US search. `nvm use` picks the pin up.
|
|
44
|
-
- **macOS, Linux, or Windows
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
- **macOS, Linux, or native Windows for the demo; WSL2 with Node 22 for a clearance.** `npx clearotron
|
|
45
|
+
demo` runs anywhere Node does, native Windows included. A real clearance does not: the engine resolves
|
|
46
|
+
the reasoning CLI the POSIX way, so a native-Windows clearance refuses at preflight even with the CLI
|
|
47
|
+
on `PATH`. On Windows, `wsl --install`, then install Node 22 **inside** the Linux distribution and work
|
|
48
|
+
through this page from there. A *hosted*
|
|
48
49
|
deployment needs Linux for one further thing, the systemd outbox trigger —
|
|
49
50
|
[driver/systemd/README.md](driver/systemd/README.md).
|
|
50
51
|
- **A reasoning CLI on your `PATH`, signed in.** This is the prerequisite people miss. Every stage runs
|
package/bin/example.mjs
CHANGED
|
@@ -41,7 +41,7 @@ import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync,
|
|
|
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
43
|
import { basename, dirname, join, resolve, sep } from "node:path";
|
|
44
|
-
import { fileURLToPath } from "node:url";
|
|
44
|
+
import { fileURLToPath, pathToFileURL } 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
|
|
47
47
|
import { envFrom } from "../shared/env-aliases.mjs"; // — resolves EITHER spelling; names the retired one because that is the live-writable half
|
|
@@ -68,7 +68,11 @@ if (has("--help") || has("-h")) {
|
|
|
68
68
|
const shipped = demoChildren(join(REPO, "demo"));
|
|
69
69
|
if (shipped.length) {
|
|
70
70
|
console.log(`\n --product takes one of:`);
|
|
71
|
-
|
|
71
|
+
// NO "DEFAULT" MARKER (tracker issue 277). This printed "(the default, when --product is not given)"
|
|
72
|
+
// beside the first id, and kept printing it after the default became ALL of them — so `--help` taught
|
|
73
|
+
// a reader the exact belief the change removed, in the command it was reported against.
|
|
74
|
+
for (const id of shipped) console.log(` ${id}`);
|
|
75
|
+
console.log(`\n With no --product, every one of them is published.`);
|
|
72
76
|
console.log("");
|
|
73
77
|
} else {
|
|
74
78
|
// An absence is a finding: a tree with no demo says so rather than printing an empty list.
|
|
@@ -96,8 +100,8 @@ const DEMO_ROOT = join(REPO, "demo");
|
|
|
96
100
|
// disagreed. That file records what a knockout demo carries instead of a report.md, and why this line
|
|
97
101
|
// once let `demo/knockout-search` ship and stay unopenable..
|
|
98
102
|
|
|
99
|
-
// --run-dir takes a directory outright. --product names a child. Neither given:
|
|
100
|
-
//
|
|
103
|
+
// --run-dir takes a directory outright. --product names a child. Neither given: EVERY child, and the
|
|
104
|
+
// names are PRINTED below rather than assumed, because "the demo" means several.
|
|
101
105
|
const wanted = flag("--product");
|
|
102
106
|
const children = demoChildren(DEMO_ROOT);
|
|
103
107
|
|
|
@@ -220,7 +224,7 @@ console.log(samples.length === 1
|
|
|
220
224
|
console.log(` pool: ${poolRoot}\n`);
|
|
221
225
|
|
|
222
226
|
mkdirSync(poolRoot, { recursive: true });
|
|
223
|
-
const { republishRun } = await import(join(REPO, "driver", "publish", "report-registry.mjs"));
|
|
227
|
+
const { republishRun } = await import(pathToFileURL(join(REPO, "driver", "publish", "report-registry.mjs")).href);
|
|
224
228
|
|
|
225
229
|
// ── ONE FAILURE MUST NOT COST THE OTHERS, AND MUST NOT BE A QUIET COUNT (tracker issue 277) ─────────
|
|
226
230
|
//
|
package/bin/onboard.mjs
CHANGED
|
@@ -693,8 +693,8 @@ export async function preflightCandidate(candidate) {
|
|
|
693
693
|
/** Ask EUIPO for a token. This is the check that makes "refuses to persist a bad secret" true. */
|
|
694
694
|
export async function validateEuipo({ clientId, clientSecret, environment }) {
|
|
695
695
|
try {
|
|
696
|
-
const { resolveConfig } = await import(join(REPO, "providers", "euipo", "src", "core.js"));
|
|
697
|
-
const { getAccessToken } = await import(join(REPO, "providers", "euipo", "src", "euipo-client.js"));
|
|
696
|
+
const { resolveConfig } = await import(pathToFileURL(join(REPO, "providers", "euipo", "src", "core.js")).href);
|
|
697
|
+
const { getAccessToken } = await import(pathToFileURL(join(REPO, "providers", "euipo", "src", "euipo-client.js")).href);
|
|
698
698
|
await getAccessToken(resolveConfig({ clientId, clientSecret, environment }), { force: true });
|
|
699
699
|
return { ok: true };
|
|
700
700
|
} catch (e) {
|
|
@@ -705,7 +705,7 @@ export async function validateEuipo({ clientId, clientSecret, environment }) {
|
|
|
705
705
|
/** One minimal Perplexity call. Costs a request, so it is never made without being asked for. */
|
|
706
706
|
export async function validatePerplexity(apiKey) {
|
|
707
707
|
try {
|
|
708
|
-
const { buildRequestBody, callAgentAPI } = await import(join(REPO, "providers", "perplexity", "src", "core.js"));
|
|
708
|
+
const { buildRequestBody, callAgentAPI } = await import(pathToFileURL(join(REPO, "providers", "perplexity", "src", "core.js")).href);
|
|
709
709
|
await callAgentAPI(apiKey, buildRequestBody({ task: "Reply with the single word: ok.", preset: "fast-search" }), { retries: 0 });
|
|
710
710
|
return { ok: true };
|
|
711
711
|
} catch (e) {
|
|
@@ -1053,7 +1053,7 @@ export async function runCheck() {
|
|
|
1053
1053
|
// deliberate — a second reader would drift from this one exactly as the composer and the checker did
|
|
1054
1054
|
// in F41, and the drift is invisible because both sides keep passing their own arms.
|
|
1055
1055
|
const unitDir = join(homedir(), ".config", "systemd", "user");
|
|
1056
|
-
const { BACKGROUND_UNITS } = await import(join(REPO, "bin", "start.mjs"));
|
|
1056
|
+
const { BACKGROUND_UNITS } = await import(pathToFileURL(join(REPO, "bin", "start.mjs")).href);
|
|
1057
1057
|
const hosted = BACKGROUND_UNITS.some((u) => existsSync(join(unitDir, u)));
|
|
1058
1058
|
const unitEnv = hosted
|
|
1059
1059
|
? unitEnvironment({
|
|
@@ -1908,8 +1908,8 @@ export async function runCheck() {
|
|
|
1908
1908
|
say("\n Register lane — proven, not inferred");
|
|
1909
1909
|
try {
|
|
1910
1910
|
const [{ activeProvider }, { makeLaneProbe, probeSpend, DEFAULT_CONTROLS, loadProviderCapabilities }] = await Promise.all([
|
|
1911
|
-
import(join(REPO, "driver", "driver.config.mjs")),
|
|
1912
|
-
import(join(REPO, "providers", "_shared", "lane-probe.mjs")),
|
|
1911
|
+
import(pathToFileURL(join(REPO, "driver", "driver.config.mjs")).href),
|
|
1912
|
+
import(pathToFileURL(join(REPO, "providers", "_shared", "lane-probe.mjs")).href),
|
|
1913
1913
|
]);
|
|
1914
1914
|
const adapter = activeProvider();
|
|
1915
1915
|
// NOT `adapter.capabilities` — that is null on every adapter, and reading it announced a LOCAL
|
|
@@ -1995,7 +1995,7 @@ export async function runCheck() {
|
|
|
1995
1995
|
// green over that state would bless the owner's fresh-install 500.
|
|
1996
1996
|
say("\n Profile store");
|
|
1997
1997
|
try {
|
|
1998
|
-
const { loadProfiles } = await import(join(REPO, "driver", "profiles.mjs"));
|
|
1998
|
+
const { loadProfiles } = await import(pathToFileURL(join(REPO, "driver", "profiles.mjs")).href);
|
|
1999
1999
|
const resolved = loadProfiles({ force: true });
|
|
2000
2000
|
const named = [...resolved.keys()];
|
|
2001
2001
|
ok(`resolves: ${named.length} profile(s) (${named.slice(0, 6).join(", ")}${named.length > 6 ? ", …" : ""}) — the universal fallback is present`);
|
|
@@ -2024,7 +2024,7 @@ export async function runCheck() {
|
|
|
2024
2024
|
// an absence of evidence is not evidence of absence, and this whole check exists because something
|
|
2025
2025
|
// invisible was being read as fine.
|
|
2026
2026
|
{
|
|
2027
|
-
const { retiredSpellingsIn, retiredSpellingLine } = await import(join(REPO, "shared", "env-aliases.mjs"));
|
|
2027
|
+
const { retiredSpellingsIn, retiredSpellingLine } = await import(pathToFileURL(join(REPO, "shared", "env-aliases.mjs")).href);
|
|
2028
2028
|
const sources = [
|
|
2029
2029
|
{ label: "your environment file", env: fileEnv, known: true },
|
|
2030
2030
|
{ label: "this shell's environment", env: process.env, known: true },
|
|
@@ -2077,7 +2077,7 @@ export async function runCheck() {
|
|
|
2077
2077
|
|
|
2078
2078
|
say("\n Submit lane");
|
|
2079
2079
|
try {
|
|
2080
|
-
const { triggerLaneVerdict, HOSTED, SUPERVISED } = await import(join(REPO, "shared", "trigger-lane.mjs"));
|
|
2080
|
+
const { triggerLaneVerdict, HOSTED, SUPERVISED } = await import(pathToFileURL(join(REPO, "shared", "trigger-lane.mjs")).href);
|
|
2081
2081
|
// POSTURE FROM THE BOX, NOT FROM A FLAG. Units installed means the units serve, and the units read
|
|
2082
2082
|
// %h/.env — so an absent value there is the incident. No units means `start` supervises and derives
|
|
2083
2083
|
// the value at runtime, where reading this process's environment says nothing either way.
|
|
@@ -2117,7 +2117,7 @@ export async function runCheck() {
|
|
|
2117
2117
|
// Stop control's availability on — one reader, two surfaces.
|
|
2118
2118
|
let verbs = null;
|
|
2119
2119
|
try {
|
|
2120
|
-
const { opsTokenPosture } = await import(join(REPO, "driver", "portal-service.mjs"));
|
|
2120
|
+
const { opsTokenPosture } = await import(pathToFileURL(join(REPO, "driver", "portal-service.mjs")).href);
|
|
2121
2121
|
verbs = opsTokenPosture(opsToken).verbs;
|
|
2122
2122
|
} catch { /* unreadable posture leaves verbs null, which reads as full ops — see the verdict */ }
|
|
2123
2123
|
let probe = null;
|
|
@@ -2158,7 +2158,7 @@ export async function runCheck() {
|
|
|
2158
2158
|
// measured on a default install, a revoked key completed a full handshake with nothing logged. Nothing
|
|
2159
2159
|
// surfaced that state anywhere, which is why it survived to be found by hand.
|
|
2160
2160
|
{
|
|
2161
|
-
const { defaultDenylistPath } = await import(join(REPO, "shared", "client-door.mjs"));
|
|
2161
|
+
const { defaultDenylistPath } = await import(pathToFileURL(join(REPO, "shared", "client-door.mjs")).href);
|
|
2162
2162
|
const dl = effective("TRADEMARK_MCP_TOKEN_DENYLIST")?.v || defaultDenylistPath(homedir());
|
|
2163
2163
|
try {
|
|
2164
2164
|
const n = readFileSync(dl, "utf8").split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#")).length;
|
|
@@ -2172,8 +2172,8 @@ export async function runCheck() {
|
|
|
2172
2172
|
}
|
|
2173
2173
|
}
|
|
2174
2174
|
try {
|
|
2175
|
-
const { clientDoorState: doorState, describeDoorState, connectKeyReport, CLIENT_DOOR_UNIT: doorUnit } = await import(join(REPO, "shared", "client-door.mjs"));
|
|
2176
|
-
const { loadGrants: readGrantsFile, isRevoked: revokedCheck } = await import(join(REPO, "shared", "scope.mjs"));
|
|
2175
|
+
const { clientDoorState: doorState, describeDoorState, connectKeyReport, CLIENT_DOOR_UNIT: doorUnit } = await import(pathToFileURL(join(REPO, "shared", "client-door.mjs")).href);
|
|
2176
|
+
const { loadGrants: readGrantsFile, isRevoked: revokedCheck } = await import(pathToFileURL(join(REPO, "shared", "scope.mjs")).href);
|
|
2177
2177
|
// ── CONFIGURED IS NOT RUNNING, AND THIS IS THE SURFACE IT WAS MEASURED ON ──
|
|
2178
2178
|
//
|
|
2179
2179
|
// `describeDoorState` was written for exactly this block and then never called from it: the split
|
|
@@ -2249,7 +2249,7 @@ export async function runCheck() {
|
|
|
2249
2249
|
// names the old one. A door that is up, listening, and refuses everything is the worst of the three
|
|
2250
2250
|
// states to debug, and nothing surfaced the mismatch.
|
|
2251
2251
|
{
|
|
2252
|
-
const { clientDoorPort: portOf, allowedHosts: hostsFor } = await import(join(REPO, "shared", "client-door.mjs"));
|
|
2252
|
+
const { clientDoorPort: portOf, allowedHosts: hostsFor } = await import(pathToFileURL(join(REPO, "shared", "client-door.mjs")).href);
|
|
2253
2253
|
const declared = String(effective("CLIENT_MCP_ALLOWED_HOSTS")?.v ?? "").trim();
|
|
2254
2254
|
if (declared) {
|
|
2255
2255
|
const port = portOf(doorEnv);
|
|
@@ -2272,7 +2272,7 @@ export async function runCheck() {
|
|
|
2272
2272
|
// null — a probe that could not be made is not a door that is absent.
|
|
2273
2273
|
let doorListening = null;
|
|
2274
2274
|
try {
|
|
2275
|
-
const { clientDoorPort: portOf } = await import(join(REPO, "shared", "client-door.mjs"));
|
|
2275
|
+
const { clientDoorPort: portOf } = await import(pathToFileURL(join(REPO, "shared", "client-door.mjs")).href);
|
|
2276
2276
|
const { createConnection } = await import("node:net");
|
|
2277
2277
|
doorListening = await new Promise((resolve) => {
|
|
2278
2278
|
const sock = createConnection({ host: "127.0.0.1", port: portOf(doorEnv), timeout: 700 });
|
|
@@ -2310,7 +2310,7 @@ export async function runCheck() {
|
|
|
2310
2310
|
// THE PUBLISHED ADDRESS, AND WHETHER IT ANSWERS (, acceptance 2). Reported here
|
|
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
|
-
const { clientDoorReachability } = await import(join(REPO, "shared", "client-door.mjs"));
|
|
2313
|
+
const { clientDoorReachability } = await import(pathToFileURL(join(REPO, "shared", "client-door.mjs")).href);
|
|
2314
2314
|
// FROM THE FILE THE UNITS LOAD, NOT THE SHELL THIS COMMAND WAS TYPED IN (the tracker issue 226
|
|
2315
2315
|
// family, met again here). On a hosted box the published address lives in the units' environment,
|
|
2316
2316
|
// and reading `process.env` reported "no client connector address is published — that is correct
|
|
@@ -2357,7 +2357,7 @@ export async function runCheck() {
|
|
|
2357
2357
|
// No new credential and no provider API: the audience is in the redirect handed to a caller with
|
|
2358
2358
|
// no session at all, which is the request just made.
|
|
2359
2359
|
{
|
|
2360
|
-
const { readAudience, audienceVerdict } = await import(join(REPO, "shared", "access-audience.mjs"));
|
|
2360
|
+
const { readAudience, audienceVerdict } = await import(pathToFileURL(join(REPO, "shared", "access-audience.mjs")).href);
|
|
2361
2361
|
const configuredAud = effectiveForService("CLEAROTRON_OIDC_AUDIENCE")?.v ?? "";
|
|
2362
2362
|
const read = readAudience(probe ?? { error: "the published address was never asked" });
|
|
2363
2363
|
// NOTHING IS CLAIMED ABOUT A BOX THAT USES NO EDGE. With no audience configured AND no Access
|
|
@@ -2383,7 +2383,7 @@ export async function runCheck() {
|
|
|
2383
2383
|
// announces the side effect first, and its control runs before any vendor.
|
|
2384
2384
|
if (PROBE_CONNECTOR && String(published ?? "").trim()) {
|
|
2385
2385
|
const { registrationEndpointFrom, probeRegistration, describeRegistration } =
|
|
2386
|
-
await import(join(REPO, "shared", "connector-signin-probe.mjs"));
|
|
2386
|
+
await import(pathToFileURL(join(REPO, "shared", "connector-signin-probe.mjs")).href);
|
|
2387
2387
|
say("");
|
|
2388
2388
|
say(" --probe-connector: asking this door's sign-in whether each assistant vendor could register.");
|
|
2389
2389
|
say(" Each attempt that SUCCEEDS creates a throwaway OAuth client on your account, which you may");
|
|
@@ -3134,7 +3134,7 @@ try {
|
|
|
3134
3134
|
prose("No default — it is your hostname. Skip it and this stays a local install: everything works",
|
|
3135
3135
|
"here, and the Use-your-AI page says so rather than handing out an address that fails.");
|
|
3136
3136
|
prose("The usual shape, and why: mcp-server/CONNECT.md.");
|
|
3137
|
-
const { clientDoorReachability } = await import(join(REPO, "shared", "client-door.mjs"));
|
|
3137
|
+
const { clientDoorReachability } = await import(pathToFileURL(join(REPO, "shared", "client-door.mjs")).href);
|
|
3138
3138
|
for (;;) {
|
|
3139
3139
|
const clientUrl = await askValue("Public connector address:", {
|
|
3140
3140
|
skippable: true,
|
package/bin/stop.mjs
CHANGED
|
@@ -19,12 +19,16 @@ import { existsSync, rmSync } from "node:fs";
|
|
|
19
19
|
import { join } from "node:path";
|
|
20
20
|
import { homedir } from "node:os";
|
|
21
21
|
import { execFileSync } from "node:child_process";
|
|
22
|
+
import { systemdSaid, looksLikeBusFailure, busRemedy, CAPTURE_STDERR } from "../shared/systemd-failure.mjs"; // tracker issue 270 — a stop that could not look must not report that it stopped
|
|
22
23
|
import { BACKGROUND_UNITS } from "./start.mjs";
|
|
23
24
|
import { CLIENT_DOOR_UNIT } from "../shared/client-door.mjs";
|
|
24
25
|
import { invoke } from "../shared/invocation.mjs";
|
|
25
26
|
|
|
26
27
|
const UNIT_DIR = join(homedir(), ".config", "systemd", "user");
|
|
27
28
|
const say = (s = "") => console.log(s);
|
|
29
|
+
// ON STDERR, because a refusal that scrolls past in the same stream as the success lines is a
|
|
30
|
+
// refusal a script cannot act on and a reader skims (tracker issue 270).
|
|
31
|
+
const err = (s = "") => console.error(s);
|
|
28
32
|
|
|
29
33
|
const argv = process.argv.slice(2);
|
|
30
34
|
if (argv.includes("--help") || argv.includes("-h")) {
|
|
@@ -39,20 +43,76 @@ if (argv.includes("--help") || argv.includes("-h")) {
|
|
|
39
43
|
}
|
|
40
44
|
|
|
41
45
|
let found = 0;
|
|
46
|
+
const failures = [];
|
|
42
47
|
for (const u of BACKGROUND_UNITS) {
|
|
43
48
|
if (u === CLIENT_DOOR_UNIT) continue; // structurally unreachable (the pin's census forbids it); belt anyway
|
|
44
49
|
const file = join(UNIT_DIR, u);
|
|
45
50
|
if (!existsSync(file)) continue;
|
|
46
51
|
found++;
|
|
47
|
-
|
|
52
|
+
// ── THE STOP IS VERIFIED, AND THE FILE GOES ONLY IF IT WORKED (tracker issue 270) ────────────────
|
|
53
|
+
//
|
|
54
|
+
// This was `catch { /* already down */ }` — a COMMENT standing in for a check. The comment guessed why
|
|
55
|
+
// the call failed, the next line deleted the unit file regardless, and the line after that announced
|
|
56
|
+
// "stopped and removed". Measured on a test box: `disable --now` failed for want of a session bus, all
|
|
57
|
+
// four services stayed up on unchanged pids holding all three ports, three unit files were deleted, and
|
|
58
|
+
// the command exited 0 saying the box ran nothing. The services were then unmanageable — running, with
|
|
59
|
+
// no unit file to stop them by — which is strictly worse than leaving both alone.
|
|
60
|
+
//
|
|
61
|
+
// `is-active` is what decides, not the exit code of the disable, because the exit code is what lied.
|
|
62
|
+
let stopped = false;
|
|
63
|
+
let why = null;
|
|
64
|
+
try {
|
|
65
|
+
execFileSync("systemctl", ["--user", "disable", "--now", u], CAPTURE_STDERR);
|
|
66
|
+
} catch (e) {
|
|
67
|
+
why = systemdSaid(e);
|
|
68
|
+
}
|
|
69
|
+
// ASKED AFTER, WHATEVER THE DISABLE SAID. A disable that reported failure may still have stopped it,
|
|
70
|
+
// and one that reported success may not have — only the state answers.
|
|
71
|
+
try {
|
|
72
|
+
const state = execFileSync("systemctl", ["--user", "is-active", u], { ...CAPTURE_STDERR, stdio: ["ignore", "pipe", "pipe"] });
|
|
73
|
+
stopped = String(state).trim() !== "active";
|
|
74
|
+
} catch (e) {
|
|
75
|
+
// `is-active` exits non-zero for an inactive unit, which is the answer we want — but it exits
|
|
76
|
+
// non-zero for "cannot reach systemd" too, and those must not read the same. The stdout is the
|
|
77
|
+
// discriminator: an unreachable systemd prints nothing there.
|
|
78
|
+
const said = String(e?.stdout ?? "").trim();
|
|
79
|
+
if (said && said !== "active") stopped = true;
|
|
80
|
+
else why = why ?? systemdSaid(e);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!stopped) {
|
|
84
|
+
// NOT DELETED. A running service with no unit file cannot be stopped by any ordinary means.
|
|
85
|
+
failures.push({ unit: u, why: why ?? "systemd still reports it active" });
|
|
86
|
+
say(` COULD NOT STOP ${u} — its unit file is left in place, so it can still be stopped`);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
48
89
|
try { rmSync(file, { force: true }); } catch { /* already gone */ }
|
|
49
90
|
say(` stopped and removed ${u}`);
|
|
50
91
|
}
|
|
51
|
-
|
|
92
|
+
// THE COMMENT HERE ALREADY NAMED THE CAUSE AND SHRUGGED AT IT. If there is no user bus, the disables
|
|
93
|
+
// above did not happen either — which is the whole of tracker issue 270 — so this is where that is said.
|
|
94
|
+
try {
|
|
95
|
+
execFileSync("systemctl", ["--user", "daemon-reload"], CAPTURE_STDERR);
|
|
96
|
+
} catch (e) {
|
|
97
|
+
const said = systemdSaid(e);
|
|
98
|
+
err(` could not ask systemd to reload its units — ${said}`);
|
|
99
|
+
if (looksLikeBusFailure(said)) err(`\n${busRemedy()}\n`);
|
|
100
|
+
}
|
|
52
101
|
|
|
53
102
|
if (!found) {
|
|
54
103
|
say(" Nothing was running in the background — no pinned unit is installed on this box.");
|
|
55
104
|
say(" Nothing to do, and nothing was changed.");
|
|
105
|
+
} else if (failures.length) {
|
|
106
|
+
// THE SENTENCE THAT WAS WRONG. "The background product is stopped and the box runs nothing again" was
|
|
107
|
+
// printed unconditionally — including on the run where four services stayed up. A reader who is told
|
|
108
|
+
// that has no reason to look, which is what made the state unmanageable rather than merely wrong.
|
|
109
|
+
err("");
|
|
110
|
+
err(` ${failures.length} of ${found} service(s) could NOT be stopped, and their unit files are left in place:`);
|
|
111
|
+
for (const f of failures) err(` ${f.unit}: ${f.why}`);
|
|
112
|
+
if (failures.some((f) => looksLikeBusFailure(f.why))) err(`\n${busRemedy()}\n`);
|
|
113
|
+
err(" Nothing was removed for these, so they can still be stopped once systemd can be reached.");
|
|
114
|
+
err(" The box is NOT idle. This exits non-zero.");
|
|
115
|
+
process.exitCode = 1;
|
|
56
116
|
} else {
|
|
57
117
|
say("");
|
|
58
118
|
say(" The background product is stopped and the box runs nothing again — plain `clearotron start`");
|
|
@@ -60,4 +120,6 @@ if (!found) {
|
|
|
60
120
|
const door = existsSync(join(UNIT_DIR, CLIENT_DOOR_UNIT));
|
|
61
121
|
if (door) say(` Your assistant connection is untouched and still up; \`${invoke("disconnect")}\` is what closes it and revokes its key.`);
|
|
62
122
|
}
|
|
63
|
-
|
|
123
|
+
// NOT A BARE ZERO. A stop that could not stop something sets `exitCode` above, and exiting 0 here would
|
|
124
|
+
// discard it — printing the refusal and then reporting success, which is the shape this change removes.
|
|
125
|
+
process.exit(process.exitCode ?? 0);
|
package/build-info.json
CHANGED
package/driver/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
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
|
+
|
|
3
24
|
## 0.2.1
|
|
4
25
|
|
|
5
26
|
### Patch Changes
|
|
@@ -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,7 +107,8 @@ 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
|
|
@@ -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
|
}
|
|
@@ -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;
|
|
@@ -13221,7 +13291,7 @@ async function pipelineInner(job, opts = {}) {
|
|
|
13221
13291
|
// one, because the sibling's population boundary is deliberate and correct. It covers a
|
|
13222
13292
|
// finding-ending followed by SILENCE; this covers one followed by a SENTENCE.
|
|
13223
13293
|
//
|
|
13224
|
-
// On R2
|
|
13294
|
+
// On a delivered R2 run the sibling logged `checked:5 matched:5 lost:0` — correctly — on a
|
|
13225
13295
|
// delivery missing two marks from the lawyer's final list, one rated HIGH. They were dropped with
|
|
13226
13296
|
// a reason, so they were outside its population by design. A drop with a reason nobody verifies
|
|
13227
13297
|
// is not accounted for; it is unexamined with a paper trail, and that delivery carried 66 of them.
|
|
@@ -15520,6 +15590,17 @@ export async function runExperiment(job, opts) {
|
|
|
15520
15590
|
// what RAN.
|
|
15521
15591
|
engine: experimentEngineName(),
|
|
15522
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,
|
|
15523
15604
|
dispatchTrigger: opts.dispatchTrigger ?? "fresh",
|
|
15524
15605
|
// — WHICH PASS THIS ARM REPRODUCED, in a word. null on a non-corrective arm;
|
|
15525
15606
|
// never "dispatched-warm", which is production's alone.
|