clearotron 0.2.3 → 0.2.4
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 +36 -37
- package/CONTRIBUTING.md +8 -4
- package/INSTALL.md +24 -4
- package/README.md +7 -6
- package/bin/example.mjs +6 -5
- package/bin/onboard.mjs +175 -9
- package/bin/start.mjs +66 -4
- package/build-info.json +2 -2
- package/demo/README.md +1 -1
- package/docs/GLOSSARY.md +85 -0
- package/docs/README.md +1 -0
- package/docs/architecture/01-product-overview.md +21 -9
- package/docs/architecture/05-config-governance.md +5 -0
- package/docs/decisions/0006-what-the-public-repository-carries.md +30 -5
- package/driver/CHANGELOG.md +34 -0
- package/driver/README.md +25 -6
- package/driver/connotation-search.mjs +1 -1
- package/driver/contract-audit.mjs +5 -1
- package/driver/contract-e3-baseline.json +11 -11
- package/driver/doubt-selection.mjs +1 -1
- package/driver/drainer-identity.mjs +1 -1
- package/driver/effort-model.mjs +2 -2
- package/driver/engine/probe.mjs +45 -5
- package/driver/gateway.mjs +2 -2
- package/driver/outbox-backoff.mjs +1 -1
- package/driver/package.json +1 -1
- package/driver/pipeline.mjs +206 -101
- package/driver/plain-register.mjs +16 -2
- package/driver/portal-config-view.mjs +30 -1
- package/driver/portal-local-auth.mjs +5 -1
- package/driver/portal-service.mjs +53 -2
- package/driver/predelivery-lint.mjs +54 -25
- package/driver/publish/render.mjs +109 -14
- package/driver/search-policy.mjs +1 -1
- package/driver/stage-context.mjs +13 -0
- package/driver/stages.mjs +51 -4
- package/driver/suite-census.json +97 -31
- package/driver/systemd/clearotron-worker.service +3 -3
- package/driver/tokens.mjs +1 -1
- package/driver/unit-inventory.mjs +34 -4
- package/mcp-server/CHANGELOG.md +2 -0
- package/mcp-server/package.json +1 -1
- package/package.json +4 -10
- package/portal-ui/dist/assets/{index-CEYvXdqH.js → index-KFAHMgdT.js} +73 -34
- package/portal-ui/dist/index.html +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/added-reference-check.mjs +45 -89
- package/scripts/e2e.mjs +2 -2
- package/scripts/generated-files-are-current.mjs +125 -0
- package/scripts/mint-names-in-force.mjs +4 -2
- package/scripts/mint-public-residue.mjs +74 -0
- package/scripts/mint-reference-strip-backlog.mjs +12 -1
- package/scripts/mint-suite-census.mjs +5 -2
- package/scripts/render-check.mjs +13 -2
- package/scripts/report-frame-check.mjs +8 -1
- package/scripts/report-screenshot.mjs +6 -3
- package/scripts/revisit-render-check.mjs +6 -3
- package/scripts/score.mjs +1 -1
- package/scripts/strip-tracker-citations.mjs +9 -1
- package/scripts/test-full.mjs +226 -0
- package/shared/README.md +2 -1
- package/shared/browser-temp-root.mjs +142 -0
- package/shared/checkout-move.mjs +1 -1
- package/shared/client-door.mjs +4 -3
- package/shared/identifier-scan.mjs +1 -1
- package/shared/install-auth.mjs +35 -0
- package/shared/invocation.mjs +5 -1
- package/shared/listen.mjs +55 -3
- package/shared/names-in-force.mjs +4 -1
- package/shared/os-advice.mjs +91 -0
- package/shared/reap-on-exit.mjs +42 -0
- package/shared/reference-guard-classes.mjs +351 -0
- package/shared/suite-census.mjs +31 -3
- package/shared/withheld-paths-access.mjs +36 -18
package/bin/onboard.mjs
CHANGED
|
@@ -671,24 +671,114 @@ export async function askSignIn(io, { localAccount = "user", staffLabel = "Staff
|
|
|
671
671
|
* Was `resolveClaudeBin`. The body never had anything claude-specific in it; the NAME was the last place
|
|
672
672
|
* this file still assumed one engine, and a name that lies is how the second adapter stayed invisible.
|
|
673
673
|
*/
|
|
674
|
-
export function resolveEngineBin(bin) {
|
|
674
|
+
export function resolveEngineBin(bin, { env = process.env, wsl = null, onWindowsDrive = null } = {}) {
|
|
675
|
+
// The predicate, not just the pattern, because /mnt/c cannot be created on a Linux runner without
|
|
676
|
+
// root — so an arm that could only supply a PATH could never drive the skip, and the branch would
|
|
677
|
+
// ship asserted by nobody. `ON_A_WINDOWS_DRIVE` is held to real paths by its own arm.
|
|
678
|
+
const onDrive = onWindowsDrive ?? ((x) => ON_A_WINDOWS_DRIVE.test(x));
|
|
675
679
|
// driver/engine/anthropic-agent.mjs — `CLEAROTRON_CLAUDE_PATH || "claude"`; openai-agent.mjs — the same
|
|
676
680
|
// shape on `CLEAROTRON_CODEX_PATH || "codex"`. A RELATIVE path is the trap for BOTH: stage subprocesses are
|
|
677
681
|
// spawned with cwd set to the RUN DIRECTORY (driver/engine/common.mjs resolveSpawnCwd, shared by the
|
|
678
682
|
// two adapters), so a relative binary resolves against a directory that did not exist at setup time.
|
|
683
|
+
const underWsl = wsl ?? isWsl({ env });
|
|
679
684
|
if (bin.includes("/")) {
|
|
680
685
|
const abs = resolve(bin);
|
|
681
|
-
|
|
682
|
-
|
|
686
|
+
// A PATH SOMEBODY TYPED IS NOT OVERRULED, only reported. The reader stated this one, and silently
|
|
687
|
+
// resolving somewhere else would be the launcher moving a door off a port that was asked for.
|
|
688
|
+
return { path: abs, executable: isExec(abs), relative: !isAbsolute(bin), windowsShim: underWsl && onDrive(abs), skipped: [] };
|
|
683
689
|
}
|
|
684
|
-
|
|
690
|
+
// ── UNDER WSL, THE WINDOWS PATH IS APPENDED TO THIS ONE ────────────────────────────────────────
|
|
691
|
+
//
|
|
692
|
+
// So `claude` on a fresh WSL2 Ubuntu resolves to /mnt/c/…/claude — the WINDOWS shim — before any
|
|
693
|
+
// Linux install is reached, and it is executable by every test this makes. It then fails the proof
|
|
694
|
+
// turn as "not signed in", because the credential it is looking for is the Linux one, and a reader
|
|
695
|
+
// is sent to fix a sign-in that was never the problem. Reported from a real WSL2 attempt.
|
|
696
|
+
//
|
|
697
|
+
// SKIPPED, AND NAMED. Passing over a candidate silently would leave the reader with "no binary
|
|
698
|
+
// found" on a machine where `which claude` prints one, so the skips travel back for the caller to
|
|
699
|
+
// say out loud. A missing Linux install then reports as missing, which is the true answer.
|
|
700
|
+
const skipped = [];
|
|
701
|
+
for (const dir of (env.PATH || "").split(delimiter).filter(Boolean)) {
|
|
685
702
|
const p = join(dir, bin);
|
|
686
|
-
if (isExec(p))
|
|
703
|
+
if (!isExec(p)) continue;
|
|
704
|
+
if (underWsl && onDrive(p)) { skipped.push(p); continue; }
|
|
705
|
+
return { path: p, executable: true, relative: false, windowsShim: false, skipped };
|
|
687
706
|
}
|
|
688
|
-
return { path: null, executable: false, relative: false };
|
|
707
|
+
return { path: null, executable: false, relative: false, windowsShim: false, skipped };
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/** A path on a Windows drive as WSL mounts it. */
|
|
711
|
+
export const ON_A_WINDOWS_DRIVE = /^\/mnt\/[a-z]\//i;
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* Whether this is a Linux running under Windows.
|
|
715
|
+
*
|
|
716
|
+
* BOTH SIGNALS INJECTABLE, for the reason `platformEngineRefusal` gives: the readers this protects
|
|
717
|
+
* are the ones who cannot run this suite to find out, so a Linux runner has to be able to drive both
|
|
718
|
+
* answers rather than read the source and agree with it.
|
|
719
|
+
*
|
|
720
|
+
* A READ THAT FAILS ANSWERS "NOT WSL", and that is the direction that changes nothing: it leaves the
|
|
721
|
+
* resolution exactly as it was before this existed. Claiming WSL on a could-not-read would start
|
|
722
|
+
* refusing candidates under /mnt on an ordinary Linux box with an ordinary mount.
|
|
723
|
+
*/
|
|
724
|
+
export function isWsl({ env = process.env, procVersion = null } = {}) {
|
|
725
|
+
if (String(env.WSL_DISTRO_NAME ?? "").trim()) return true;
|
|
726
|
+
if (String(env.WSL_INTEROP ?? "").trim()) return true;
|
|
727
|
+
const v = procVersion ?? (() => { try { return readFileSync("/proc/version", "utf8"); } catch { return ""; } })();
|
|
728
|
+
return /microsoft|wsl/i.test(v);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* What to say about candidates passed over because they sit on a Windows drive — or `null` when none
|
|
733
|
+
* were. Plural because a reader can have both a shim and a wrapper on PATH.
|
|
734
|
+
*/
|
|
735
|
+
export function windowsShimNote(skipped, bin) {
|
|
736
|
+
if (!skipped?.length) return null;
|
|
737
|
+
return `ignored ${skipped.length === 1 ? "a `" + bin + "` on a Windows drive" : skipped.length + " `" + bin + "` binaries on Windows drives"} `
|
|
738
|
+
+ `(${skipped.join(", ")}): WSL appends the Windows PATH to this one, and a Windows build cannot run a `
|
|
739
|
+
+ `stage here — it fails the proof turn as "not signed in" while holding a credential this side never `
|
|
740
|
+
+ `wrote. Install it inside the Linux distribution instead.`;
|
|
689
741
|
}
|
|
690
742
|
const isExec = (p) => { try { accessSync(p, constants.X_OK); return statSync(p).isFile(); } catch { return false; } };
|
|
691
743
|
|
|
744
|
+
/**
|
|
745
|
+
* What doctor says about the engine on a platform the run door refuses outright, or `null` where the
|
|
746
|
+
* ordinary binary checks apply.
|
|
747
|
+
*
|
|
748
|
+
* `platform` IS INJECTABLE, and for the reason `preflightEngineBinary` gives at its own refusal: the
|
|
749
|
+
* population this protects is the one that cannot run this suite to find out. An arm on a Linux runner
|
|
750
|
+
* has to be able to ask what a Windows reader is shown, or the Windows text is asserted by nobody —
|
|
751
|
+
* and reading `process.platform` inside the caller would make that arm read source instead of driving
|
|
752
|
+
* the answer.
|
|
753
|
+
*/
|
|
754
|
+
export function platformEngineRefusal({ platform = process.platform } = {}) {
|
|
755
|
+
if (platform !== "win32") return null;
|
|
756
|
+
return "this engine does not run on native Windows — stage subprocesses are spawned with POSIX path "
|
|
757
|
+
+ "and process semantics, so the run door refuses before it reads PATH. Run it under WSL2, or in "
|
|
758
|
+
+ "the devcontainer. The demo works here as it is: it replays finished reports and needs no engine.";
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/**
|
|
762
|
+
* How to leave demo mode, which is not the same instruction everywhere.
|
|
763
|
+
*
|
|
764
|
+
* On the platform the run door refuses, the standard advice is a loop: install a CLI the reader may
|
|
765
|
+
* already have, then restart a service — neither of which can change the answer, because the refusal
|
|
766
|
+
* is about the platform rather than the program. Naming WSL2 is the only instruction that ends it.
|
|
767
|
+
*/
|
|
768
|
+
export function leaveDemoAdvice(engSpec, { platform = process.platform } = {}) {
|
|
769
|
+
if (platform === "win32") {
|
|
770
|
+
return [`To leave demo on Windows: run the product under WSL2, or in the devcontainer. Installing `
|
|
771
|
+
+ `${engSpec.vendor}'s CLI natively will not change this — the run door refuses on the platform, `
|
|
772
|
+
+ "not on the program."];
|
|
773
|
+
}
|
|
774
|
+
return [
|
|
775
|
+
`To leave demo: install ${engSpec.vendor}'s CLI (\`${engSpec.fallback}\`)`
|
|
776
|
+
+ `${engSpec.install ? ` with \`${engSpec.install}\`` : ""}, then ${engSpec.signIn}.`,
|
|
777
|
+
"Restart any running engine service afterwards so it re-reads its PATH: the portal reports what the "
|
|
778
|
+
+ "engine saw when it last started, and it will not notice a new install until then.",
|
|
779
|
+
];
|
|
780
|
+
}
|
|
781
|
+
|
|
692
782
|
/**
|
|
693
783
|
* The engine menu, built from the driver's registry so the wizard cannot offer an adapter that does not
|
|
694
784
|
* exist — or hide one that does. Same guarantee the register-provider list has.
|
|
@@ -1201,7 +1291,28 @@ export async function runCheck() {
|
|
|
1201
1291
|
const binSet = !!binEff;
|
|
1202
1292
|
const binSetting = binEff?.v || engSpec.fallback;
|
|
1203
1293
|
const bin = resolveEngineBin(binSetting);
|
|
1204
|
-
|
|
1294
|
+
// ── NATIVE WINDOWS IS ANSWERED HERE, BEFORE ANY PATH IS RESOLVED OR REPORTED ──────────────────
|
|
1295
|
+
//
|
|
1296
|
+
// `resolveEngineBin` tests a candidate with `accessSync(X_OK)` and `isFile()`. Windows has no
|
|
1297
|
+
// execute bit, so X_OK is satisfied by any file that exists — and an npm global install writes
|
|
1298
|
+
// BOTH `claude`, an extensionless shell script for Git Bash, and `claude.cmd`. The POSIX test
|
|
1299
|
+
// passes on the shell script, so this line reported the engine FOUND at a path CreateProcess
|
|
1300
|
+
// cannot start, and the probe then failed with `spawn claude ENOENT`. Reported from a real
|
|
1301
|
+
// Windows run, 2026-09-09.
|
|
1302
|
+
//
|
|
1303
|
+
// RESOLVING `claude.cmd` INSTEAD WOULD MOVE THE CONTRADICTION, NOT REMOVE IT.
|
|
1304
|
+
// `preflightEngineBinary` refuses native Windows by name, and the run door calls it
|
|
1305
|
+
// unconditionally — so a clearance cannot run here whatever this line finds. Making the probe
|
|
1306
|
+
// succeed would produce a doctor that says ready, a probe that passes, and a run that refuses
|
|
1307
|
+
// anyway: the same disagreement one step later, and more convincing for having spawned something.
|
|
1308
|
+
//
|
|
1309
|
+
// So doctor says what the run door says. That is the property — the two agree — and the reader
|
|
1310
|
+
// gets one refusal they can act on instead of a found-then-failed sequence that sends them
|
|
1311
|
+
// looking for a PATH problem they do not have. The demo is unaffected: it replays finished runs
|
|
1312
|
+
// and needs no engine, which is why four reports published on that same Windows box.
|
|
1313
|
+
const platformRefusal = platformEngineRefusal();
|
|
1314
|
+
if (platformRefusal) problem(platformRefusal);
|
|
1315
|
+
else if (bin.executable && !bin.relative) ok(`${bin.path}`);
|
|
1205
1316
|
// The FACT only. It used to carry "install it for a real run (`npm run example` needs no engine)",
|
|
1206
1317
|
// which is the absence framing was filed about — and it now says half of what the MODE line
|
|
1207
1318
|
// below says, in worse words. One statement of a state, in the place that states states.
|
|
@@ -1212,6 +1323,15 @@ export async function runCheck() {
|
|
|
1212
1323
|
else if (bin.relative) problem(`${engSpec.env}="${binSetting}" is RELATIVE — stage subprocesses run with cwd set to the run directory, so it will not resolve there. Use an absolute path (${bin.path} from here)`);
|
|
1213
1324
|
else if (!bin.path) problem(`${engSpec.env}="${binSetting}" resolves to nothing on PATH`);
|
|
1214
1325
|
else problem(`${engSpec.env}="${binSetting}" → ${bin.path} is not an executable file`);
|
|
1326
|
+
// SAID AFTER THE CHAIN ABOVE, AND OUTSIDE IT. This block is one if/else-if ladder, so a statement
|
|
1327
|
+
// placed between two of its clauses re-parents every clause below onto the new `if` — measured:
|
|
1328
|
+
// it made doctor report an executable mock binary as "not an executable file", because the ladder's
|
|
1329
|
+
// tail became the else of THIS condition. Said whether or not a binary was found: under WSL a
|
|
1330
|
+
// Windows build on the appended PATH is passed over, and if a Linux one was found the reader still
|
|
1331
|
+
// needs to know which of the two they have, while if none was, "no binary" over a machine whose own
|
|
1332
|
+
// `which` prints one is a refusal nobody can act on.
|
|
1333
|
+
const shimNote = windowsShimNote(bin.skipped, engSpec.fallback);
|
|
1334
|
+
if (shimNote) info(shimNote);
|
|
1215
1335
|
|
|
1216
1336
|
// ── — WHICH MODE THIS INSTALL IS IN, said as a mode rather than as a list of absences ──────
|
|
1217
1337
|
//
|
|
@@ -1242,7 +1362,15 @@ export async function runCheck() {
|
|
|
1242
1362
|
if (installMode === ENGINE_MODES.DEMO) {
|
|
1243
1363
|
info("MODE: demo — everything works except starting a NEW search. The example report, its audit trail "
|
|
1244
1364
|
+ "and the MCP connection are live right now; `npm run example` needs no engine.");
|
|
1245
|
-
|
|
1365
|
+
// NAMES THE COMMAND, AND THE RESTART. The settings page says both now, and a doctor that named
|
|
1366
|
+
// the program but not how to install it — or that left out the restart, which is what actually
|
|
1367
|
+
// unsticks a reader who has just installed it — would be the third opinion this issue exists to
|
|
1368
|
+
// remove. Same words on all three surfaces, taken from the same table.
|
|
1369
|
+
// THE WAY OUT IS NOT THE SAME ON EVERY PLATFORM, and on the one where it differs the standard
|
|
1370
|
+
// advice is a loop: a Windows reader is told to install a CLI they may already have, and then to
|
|
1371
|
+
// restart a service — neither of which can change the answer, because the refusal is about the
|
|
1372
|
+
// platform rather than the binary. Naming WSL2 is the only instruction that ends this state.
|
|
1373
|
+
for (const line of leaveDemoAdvice(engSpec)) info(line);
|
|
1246
1374
|
} else {
|
|
1247
1375
|
info("The engine program is installed. Whether it is signed in cannot be read from disk: run "
|
|
1248
1376
|
+ "clearotron doctor --probe-engine to find out.");
|
|
@@ -2257,7 +2385,7 @@ export async function runCheck() {
|
|
|
2257
2385
|
//
|
|
2258
2386
|
// `describeDoorState` was written for exactly this block and then never called from it: the split
|
|
2259
2387
|
// it encodes sat in `shared/client-door.mjs` with no caller in `bin/` or `driver/`, while doctor
|
|
2260
|
-
// went on printing the three sentences the split replaces. The defect
|
|
2388
|
+
// went on printing the three sentences the split replaces. The defect testing measured is one of
|
|
2261
2389
|
// them — a `connect` that died at `daemon-reload` had already written the fence and placed both
|
|
2262
2390
|
// units, and doctor said "the client door is on" over a unit that was inactive with nothing on its
|
|
2263
2391
|
// port. Every angle read as configured, because configured is all anything asked.
|
|
@@ -2730,7 +2858,32 @@ try {
|
|
|
2730
2858
|
if (!pick.id) { sayNoEngine(); break; }
|
|
2731
2859
|
const eng = ENGINE_BINARIES[pick.id];
|
|
2732
2860
|
|
|
2861
|
+
// ── NATIVE WINDOWS IS ANSWERED HERE TOO, AND THE ESCAPE IS OFFERED RATHER THAN LEFT ON A MENU ──
|
|
2862
|
+
//
|
|
2863
|
+
// Doctor learned this and the wizard did not, and the gap is worse than it sounds: the two "no
|
|
2864
|
+
// engine" escapes below both sit behind "no usable binary", and on Windows that is FALSE. X_OK is
|
|
2865
|
+
// satisfied by any file that exists, so the wizard says "found C:\...\claude", walks past both
|
|
2866
|
+
// escapes, asks which lane pays, spends a proof turn, and meets `spawn claude ENOENT`. The only
|
|
2867
|
+
// prompt left then defaults to No and returns to the engine menu — with nothing changed between
|
|
2868
|
+
// iterations, on either lane. Reported from a real Windows run: the menu's last row was the one
|
|
2869
|
+
// way out and nothing on screen said so.
|
|
2870
|
+
//
|
|
2871
|
+
// So the refusal is stated before a candidate is resolved, and finishing with no engine is OFFERED
|
|
2872
|
+
// rather than being a row the reader has to notice. Declining leaves them on the menu exactly as
|
|
2873
|
+
// before, which is the branch that was already there.
|
|
2874
|
+
const wizardRefusal = platformEngineRefusal();
|
|
2875
|
+
if (wizardRefusal) {
|
|
2876
|
+
problem(wizardRefusal);
|
|
2877
|
+
if (await confirm("Finish setup with no engine configured?", true)) { sayNoEngine(); break engine; }
|
|
2878
|
+
continue;
|
|
2879
|
+
}
|
|
2880
|
+
|
|
2733
2881
|
let bin = resolveEngineBin(process.env[eng.env] || eng.fallback);
|
|
2882
|
+
// Under WSL the Windows build on the appended PATH has been passed over. Said before the install
|
|
2883
|
+
// offer below, because otherwise a reader whose Linux install is genuinely missing is asked to
|
|
2884
|
+
// install a binary their own `which` already prints — and would decline for the wrong reason.
|
|
2885
|
+
const shimNote = windowsShimNote(bin.skipped, eng.fallback);
|
|
2886
|
+
if (shimNote) info(shimNote);
|
|
2734
2887
|
if (!(bin.executable && !bin.relative) && eng.install) {
|
|
2735
2888
|
// ── — INSTALLING IT IS ONE COMMAND, AND WE USED TO STOP AT A SENTENCE ───────────────────
|
|
2736
2889
|
//
|
|
@@ -2929,7 +3082,20 @@ try {
|
|
|
2929
3082
|
break engine;
|
|
2930
3083
|
}
|
|
2931
3084
|
problem(probeFailureText(v));
|
|
3085
|
+
// ── THE ENGINE'S OWN LAST LINES, UNDER EVERY VERDICT, ON BOTH LANES ──────────────────────────
|
|
3086
|
+
//
|
|
3087
|
+
// This used to be `if (v.detail)`, so a verdict carrying no detail printed a headline and nothing
|
|
3088
|
+
// else — and the reader had a classification with no evidence to check it against. That is the
|
|
3089
|
+
// worst shape for a heuristic: the signed-out test is a text match over the engine's output, it
|
|
3090
|
+
// matches broadly on purpose, and a reader shown only its conclusion cannot tell a genuinely
|
|
3091
|
+
// signed-out CLI from an unrelated failure whose words happened to match.
|
|
3092
|
+
//
|
|
3093
|
+
// SAY WHICH IT IS, INCLUDING WHEN THERE IS NOTHING. "The engine printed nothing" is itself the
|
|
3094
|
+
// diagnosis on a binary that died before it opened its mouth, and printing no line at all leaves
|
|
3095
|
+
// a reader unable to tell that from a wizard that decided not to show them.
|
|
2932
3096
|
if (v.detail) info(`engine said: ${v.detail}`);
|
|
3097
|
+
else info("engine said: nothing — it produced no output on either stream before it stopped.");
|
|
3098
|
+
if (v.basis) info(`(that reading is ${v.basis === "text-match" ? "matched out of the text above, not a signal from the provider" : v.basis})`);
|
|
2933
3099
|
// — THE HAND-OFF. Signing in is the one step of this sequence nobody here can perform for
|
|
2934
3100
|
// someone, so the wizard names the command, waits, and re-probes rather than ending at a
|
|
2935
3101
|
// description of what is wrong. The text comes from ENGINE_BINARIES so the two adapters cannot
|
package/bin/start.mjs
CHANGED
|
@@ -116,7 +116,7 @@ import { SERVER_INSTALL_SET, unitsToRestartOnRefresh, unitHealthVerdict } from "
|
|
|
116
116
|
// tolerate and never manage"; settled point 2 superseded that.)
|
|
117
117
|
import { defaultDenylistPath, denylistPathFor, denylistFor, ensureDenylistFile, CLIENT_DOOR_UNIT, enablePlan, clientDoorPort } from "../shared/client-door.mjs"; // — one owner for the revocation list's path
|
|
118
118
|
import { createServer } from "node:net";
|
|
119
|
-
import { listenErrorMessage } from "../shared/listen.mjs";
|
|
119
|
+
import { listenErrorMessage, nextFreePort } from "../shared/listen.mjs";
|
|
120
120
|
import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
121
121
|
import { randomBytes } from "node:crypto";
|
|
122
122
|
import { invocationPrefix, invoke } from "../shared/invocation.mjs"; // — the banner names the verb
|
|
@@ -133,6 +133,8 @@ import { rebuildIfStale } from "../shared/bundle-rebuild.mjs"; // never serve
|
|
|
133
133
|
// ONE CLASSIFIER FOR WHAT A STAFF RULE ADMITS, shared with the setup wizard. Two copies of this
|
|
134
134
|
// judgement would be a wizard that asks about one rule and a launcher that writes another.
|
|
135
135
|
import { classifyStaffDomain, staffDomainRefusal, staffGrantSentence } from "../shared/staff-domain.mjs";
|
|
136
|
+
import { backgroundManager } from "../shared/os-advice.mjs";
|
|
137
|
+
import { frontingVariablesSet } from "../shared/install-auth.mjs"; // — one owner for what counts as a proxy in front of a door
|
|
136
138
|
|
|
137
139
|
const REPO = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
138
140
|
const ENV_PATH = envLocalPath({ repoRoot: REPO }); // resolved, never composed: one resolver, so moving this file later is one line
|
|
@@ -962,7 +964,24 @@ if (isMain) {
|
|
|
962
964
|
// loop because both paths below have to act on it: the background path must RESTART the door it
|
|
963
965
|
// adopted, and the foreground path must not spawn a second one beside it.
|
|
964
966
|
let adoptedClientDoor = false;
|
|
965
|
-
|
|
967
|
+
// ── WHETHER ANYTHING OUTSIDE THIS PROCESS IS ADDRESSED TO THESE NUMBERS ────────────────────────
|
|
968
|
+
//
|
|
969
|
+
// A door may only be moved off a port nobody chose when nothing fronts it. A proxy, an Access team
|
|
970
|
+
// or an OIDC issuer means something external resolves to these numbers, and a door that quietly
|
|
971
|
+
// moved would be up and unreachable — which looks like success and is the worst of the three
|
|
972
|
+
// outcomes. The auth-mode half of this is already closed further up: a foreground start refuses
|
|
973
|
+
// outright when PORTAL_AUTH_MODE names a hosted door, and again when the units are installed and
|
|
974
|
+
// serving. What is left to check is the settings that can be present with the mode unset.
|
|
975
|
+
//
|
|
976
|
+
// PORTAL_AUTH_MODE IS ABSENT FROM THAT LIST ON PURPOSE, and only a rule in another file makes that
|
|
977
|
+
// safe: `driver/portal-service.mjs` refuses to start in auth-proxy without CF_ACCESS_TEAM or
|
|
978
|
+
// PORTAL_OIDC_ISSUER, so a portal fronted by the mode alone cannot come up at all. Relaxing that
|
|
979
|
+
// refusal without adding the mode here would let this under-report — which is why the list has one
|
|
980
|
+
// owner in `shared/install-auth.mjs` with an arm holding it to the doors themselves.
|
|
981
|
+
const fronted = frontingVariablesSet(process.env);
|
|
982
|
+
const claimedPorts = new Set([ports.portal, ports.mcp, ports.client]);
|
|
983
|
+
const movedDoors = [];
|
|
984
|
+
for (const [what, port, portVar, doorUnit = null, key = null] of [["portal", ports.portal, "PORTAL_SERVICE_PORT", null, "portal"], ["engine door", ports.mcp, "TRADEMARK_MCP_HTTP_PORT", null, "mcp"], ["client door", ports.client, "CLIENT_MCP_HTTP_PORT", CLIENT_DOOR_UNIT, "client"]]) {
|
|
966
985
|
// A --background REFRESH runs over its own healthy units, which hold these ports on purpose;
|
|
967
986
|
// systemd's restart is the handover. Probing would refuse the flag exactly once it has worked.
|
|
968
987
|
// The narrow carve above already proved every installed unit is ours.
|
|
@@ -987,8 +1006,38 @@ if (isMain) {
|
|
|
987
1006
|
// on a privileged port and from an address this host does not have, and names the way out of each;
|
|
988
1007
|
// the launcher having its own shorter sentence for one of the three would mean a user meets two
|
|
989
1008
|
// different answers to the same question depending on which door refused first.
|
|
1009
|
+
// ── A PORT NOBODY CHOSE IS MOVED RATHER THAN REFUSED (owner ruling, 2026-09-09) ─────────────
|
|
1010
|
+
//
|
|
1011
|
+
// Three conditions, and each is a different reason:
|
|
1012
|
+
// · the address is genuinely taken — anything else is not this case;
|
|
1013
|
+
// · the reader did not state this port, so no one is addressed to it. An explicitly set port is
|
|
1014
|
+
// a stated address and is refused exactly as before;
|
|
1015
|
+
// · nothing fronts these doors, so nothing outside resolves to the old number.
|
|
1016
|
+
// With any of those false the refusal below fires unchanged, which is the whole of the previous
|
|
1017
|
+
// behaviour kept intact underneath this.
|
|
1018
|
+
if (code === "EADDRINUSE" && key && !fronted.length && !String(process.env[portVar] ?? "").trim()) {
|
|
1019
|
+
const next = await nextFreePort(port, async (p) => (await probe(p)) === null, { claimed: claimedPorts });
|
|
1020
|
+
// NULL IS NOT A FALLBACK. Nothing free in range means the reader is told the truth about the
|
|
1021
|
+
// port they asked for, rather than sent to one this could not prove was free either.
|
|
1022
|
+
if (next) {
|
|
1023
|
+
claimedPorts.delete(port);
|
|
1024
|
+
claimedPorts.add(next);
|
|
1025
|
+
ports[key] = next;
|
|
1026
|
+
movedDoors.push({ what, portVar, from: port, to: next });
|
|
1027
|
+
continue;
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
990
1030
|
if (code) fatal(listenErrorMessage({ code }, { what, host: HOST, port, portVar, portFile }));
|
|
991
1031
|
}
|
|
1032
|
+
// SAID OUT LOUD, EVERY TIME. A door that moved is at an address the reader did not ask for and will
|
|
1033
|
+
// not find by memory — and the portal's own URL is printed from `ports.portal` further down, so a
|
|
1034
|
+
// silent move would leave the two disagreeing with nothing explaining it.
|
|
1035
|
+
if (movedDoors.length) {
|
|
1036
|
+
say("");
|
|
1037
|
+
say(` ${movedDoors.length === 1 ? "One door was" : `${movedDoors.length} doors were`} already in use, so ${movedDoors.length === 1 ? "it" : "they"} moved:`);
|
|
1038
|
+
for (const d of movedDoors) say(` the ${d.what}: ${d.from} → ${d.to} (set ${d.portVar} to pin it)`);
|
|
1039
|
+
say(" Nothing outside this machine is addressed to these, so nothing else needed changing.");
|
|
1040
|
+
}
|
|
992
1041
|
|
|
993
1042
|
// ── 2. the two secrets, generated once and kept ────────────────────────────────────────────────────
|
|
994
1043
|
|
|
@@ -1981,8 +2030,21 @@ if (isMain) {
|
|
|
1981
2030
|
// run". Saying it here, before the commands go by, is the whole fix.
|
|
1982
2031
|
say(" This terminal is now the product: it runs only while this command does, and Ctrl-C — or closing");
|
|
1983
2032
|
say(" the window — stops everything it started. So the commands above need a SECOND terminal.");
|
|
1984
|
-
|
|
1985
|
-
|
|
2033
|
+
// ── THE BACKGROUND ROUTE IS NOT OFFERED WHERE IT CANNOT WORK ────────────────────────────────────
|
|
2034
|
+
//
|
|
2035
|
+
// `--background` installs and enables service units. There are none on Windows, so both the offer
|
|
2036
|
+
// and the sentence naming what manages them were wrong there — a reader was told to run a flag that
|
|
2037
|
+
// cannot succeed and given a service manager that is not on the machine and cannot be put there.
|
|
2038
|
+
// Reported from a real run. Same rule as the engine refusal above: do not name a route this platform
|
|
2039
|
+
// does not have.
|
|
2040
|
+
const manager = backgroundManager();
|
|
2041
|
+
if (manager) {
|
|
2042
|
+
say(` To get your prompt back instead, stop this and run ${invoke("start")} --background`);
|
|
2043
|
+
say(` — same product, managed by ${manager}, and it survives logout.`);
|
|
2044
|
+
} else {
|
|
2045
|
+
say(" There is no background form on this platform: the product runs as long as this window does.");
|
|
2046
|
+
say(" Leave it open and use a second terminal for the commands above.");
|
|
2047
|
+
}
|
|
1986
2048
|
say("");
|
|
1987
2049
|
}
|
|
1988
2050
|
|
package/build-info.json
CHANGED
package/demo/README.md
CHANGED
|
@@ -37,7 +37,7 @@ they are machine output with a fixed shape, described here instead:
|
|
|
37
37
|
the search policy, the receipts, the pre-delivery lint. These are what the report is assembled from
|
|
38
38
|
and what the audit surfaces read.
|
|
39
39
|
- `<product-id>/run/_records/` — the register records the run actually retrieved, one file per record,
|
|
40
|
-
named by register and number. Real
|
|
40
|
+
named by register and number. Real Clarivate Compumark records for a fictional mark.
|
|
41
41
|
|
|
42
42
|
`<product-id>/PROVENANCE.md` says how each run was frozen, what was dropped, and how to regenerate it.
|
|
43
43
|
|
package/docs/GLOSSARY.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Glossary
|
|
2
|
+
|
|
3
|
+
Words this codebase uses in a particular way. They are here because they are already in the tree —
|
|
4
|
+
in file names, comments and test titles — and a contributor meeting one should not have to reverse
|
|
5
|
+
it out of the code. Product vocabulary a client would meet is in [`../README.md`](../README.md); the
|
|
6
|
+
tenant, account and project model is in [`../INSTALL.md`](../INSTALL.md) under "The four things, and
|
|
7
|
+
what contains what".
|
|
8
|
+
|
|
9
|
+
Nothing here is a rule. Each line says what the word points at, and names the file that owns it.
|
|
10
|
+
|
|
11
|
+
## The run
|
|
12
|
+
|
|
13
|
+
**Seat** — one model turn with one job, inside a stage that has several. The common-law grid runs
|
|
14
|
+
halves `a` and `b` and a meaning seat `m`; each has its own model tier and its own ledger, and one
|
|
15
|
+
seat failing is not the stage failing.
|
|
16
|
+
|
|
17
|
+
**Dispatch** — handing a seat its prompt and waiting for the turn. Recorded per attempt in
|
|
18
|
+
`_driver/<stage>.jsonl`, so a stage that ran three times has three dispatches and one result.
|
|
19
|
+
|
|
20
|
+
**Matter frame** — what the run understands the request to be: the mark, the goods, the sector, the
|
|
21
|
+
territories. Everything downstream derives from it, and it is told not to widen past the territories
|
|
22
|
+
the job named.
|
|
23
|
+
|
|
24
|
+
**Blind frame** — a second reading of the raw request that never sees the matter frame. It runs
|
|
25
|
+
beside the investigation as a non-fatal sibling, so a framing mistake shows up as a disagreement
|
|
26
|
+
rather than propagating quietly.
|
|
27
|
+
|
|
28
|
+
**Feedforward** — the reviewer's flags reaching the corrective pass as data rather than as prose.
|
|
29
|
+
`corrections-feedforward.mjs` decides which flags count as still open.
|
|
30
|
+
|
|
31
|
+
**Carry-through** — of every subject the run's findings surface, which ones reach none of the
|
|
32
|
+
delivered documents. A finding that is recorded and never carried is the thing this measures.
|
|
33
|
+
|
|
34
|
+
**Declination** — a record the run enumerated and then decided not to pursue, with the position of
|
|
35
|
+
its reason in `_driver/declination-spec.json`. A declination is a decision, not a gap.
|
|
36
|
+
|
|
37
|
+
**Refusal** — the product declining rather than failing. `terminalKind: "designed-refusal"`. Nothing
|
|
38
|
+
about a refusal is retried, parked or recovered; the remedy is always the operator's.
|
|
39
|
+
|
|
40
|
+
## What a run leaves behind
|
|
41
|
+
|
|
42
|
+
**Ledger** — an append-only record of what was covered, kept so that "unsearched" can never be
|
|
43
|
+
reported as "clean". The coverage ledger appears in the report as prose and as JSON, from one source.
|
|
44
|
+
|
|
45
|
+
**Receipt** — a record that something happened, kept for observability and deliberately not used as a
|
|
46
|
+
gate. A receipt read as a gate is a Goodhart problem, and the reasoning-integrity receipt says so on
|
|
47
|
+
its own face.
|
|
48
|
+
|
|
49
|
+
**Register-digest** — the condensed register result a downstream stage reads instead of the raw
|
|
50
|
+
records: the same evidence, at the size a model turn can hold.
|
|
51
|
+
|
|
52
|
+
**Hit list** — one line per enumerated record and the fate it was given. It is not a read: a record
|
|
53
|
+
can be on the hit list and never opened, and the sign-off condition is about documents actually read.
|
|
54
|
+
|
|
55
|
+
**Predelivery** — the checks that run after the documents exist and before anyone receives them
|
|
56
|
+
(`predelivery-lint.mjs`, `_driver/predelivery-lint.json`). A failing predelivery withholds the
|
|
57
|
+
artifact; it never annotates it.
|
|
58
|
+
|
|
59
|
+
**Courier** — delivery as a self-contained packet handed to the integrator. The engine does not send;
|
|
60
|
+
what it produces is carried.
|
|
61
|
+
|
|
62
|
+
**Drift** — a value that is mirrored in two places by design, and the two disagreeing. The catalogue
|
|
63
|
+
of what is mirrored, and which copy owns it, is in the architecture docs.
|
|
64
|
+
|
|
65
|
+
## Guards and tests
|
|
66
|
+
|
|
67
|
+
**Arm** — one `test(...)` block. A file has many; each is named for the property it holds, and a red
|
|
68
|
+
arm is read by its output rather than by its name.
|
|
69
|
+
|
|
70
|
+
**Plant** — deliberately breaking something to prove a check would catch it. A guard that has never
|
|
71
|
+
been shown failing has not been shown to work, so the hard version plants a NEW instance of the class
|
|
72
|
+
rather than the one the check was written against.
|
|
73
|
+
|
|
74
|
+
**Contract dictation** — the rule that a contract is stated once and everything else derives from it.
|
|
75
|
+
`contract-dictation.mjs` computes over a corpus; `scripts/contract-dictation-scan.mjs` builds that
|
|
76
|
+
corpus from the tracked tree, so a new authoring layer is caught structurally rather than because
|
|
77
|
+
somebody remembered to list it.
|
|
78
|
+
|
|
79
|
+
**Assert census** — counting the assertions that never executed. An assertion inside a branch nothing
|
|
80
|
+
reaches is a test that passes and guards nothing (`scripts/unexecuted-asserts.mjs`).
|
|
81
|
+
|
|
82
|
+
**Authority probe** — asking the live CLI whether it honours a boundary the driver sets, and whether
|
|
83
|
+
the turn survives being refused. CI has no CLI and no subscription, so it can only prove the decision
|
|
84
|
+
is made correctly; whether it is obeyed is a property of the real binary
|
|
85
|
+
(`scripts/authority-boundary-probe.mjs`).
|
package/docs/README.md
CHANGED
|
@@ -38,6 +38,7 @@ the vendor flags approximate is UNKNOWN, never a number.
|
|
|
38
38
|
| [`PORTAL.md`](PORTAL.md) | The portal: what it serves, and who sees what |
|
|
39
39
|
| [`CLIENT-MCP.md`](CLIENT-MCP.md) | Publishing a connector your customers sign in to, and how their access is scoped. To connect *your own* app to *your own* runs, use [`../mcp-server/CONNECT.md`](../mcp-server/CONNECT.md) instead |
|
|
40
40
|
| [`E2E.md`](E2E.md) | Proving a deployment works end to end |
|
|
41
|
+
| [`GLOSSARY.md`](GLOSSARY.md) | The words this codebase uses in a particular way — one line each, for a contributor meeting them for the first time |
|
|
41
42
|
| [`SECURITY.md`](SECURITY.md) | The security envelope — what protects what, and where it is enforced in code. To report a vulnerability, use [`../SECURITY.md`](../SECURITY.md) |
|
|
42
43
|
|
|
43
44
|
Access control is stated once, in [`SECURITY.md`](SECURITY.md): who may see which runs, what an unset
|
|
@@ -3,18 +3,30 @@
|
|
|
3
3
|
> Part of the architecture pack (`docs/architecture/`). The driver's module tree and the headless
|
|
4
4
|
> integrator contract are in [`driver/README.md`](../../driver/README.md).
|
|
5
5
|
|
|
6
|
-
The clearotron driver turns a
|
|
7
|
-
|
|
8
|
-
the
|
|
9
|
-
|
|
6
|
+
The clearotron driver turns a clearance request into a delivered preliminary trademark clearance
|
|
7
|
+
report. One matter in, one decision out — with the investigation, the reasoning, the challenge, and
|
|
8
|
+
the paper trail in between run by deterministic code that treats the model as a reasoning step,
|
|
9
|
+
never as the orchestrator.
|
|
10
10
|
|
|
11
11
|
## What it does
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
**Who this is for.** Anyone who needs to know whether a name is free to use and is willing to run the
|
|
14
|
+
search themselves: a lawyer, a brand team, or an individual clearing their own mark. You need three
|
|
15
|
+
self-serve accounts: a reasoning CLI, a register, and web research.
|
|
16
|
+
|
|
17
|
+
**What you get.** Four searches at different depths. A knockout screens up to eight names in minutes.
|
|
18
|
+
A global preliminary, a multi-country focus, or a full country search takes one name deeper, and the
|
|
19
|
+
full country search adds case law. Each delivers a written report with the records behind every
|
|
20
|
+
finding. What you do with it is your call.
|
|
21
|
+
|
|
22
|
+
The four searches are declared in `driver/products.mjs`, and that declaration is what the rest of the
|
|
23
|
+
system reads: a knockout screens up to eight names and skips case law, and only the full country
|
|
24
|
+
search turns case law on. A knockout takes 5 to 10 minutes; the other three take 1.5 to 2.5 hours.
|
|
25
|
+
|
|
26
|
+
Under any of them the system resolves the matter (mark, scope, deadline), investigates the trademark
|
|
27
|
+
registers and the live marketplace in the variations a searcher would try, reasons the risk in the
|
|
28
|
+
matter's own framework, challenges its own draft twice, and delivers a report with every factual
|
|
29
|
+
claim traceable to a fetched source record.
|
|
18
30
|
|
|
19
31
|
The driver is the machine that runs that whole distance. It is **not an agent**: it is a plain
|
|
20
32
|
Node.js process, launched by systemd, that executes a fixed pipeline of stages. Each stage that
|
|
@@ -136,6 +136,11 @@ boxes rebuild.
|
|
|
136
136
|
|
|
137
137
|
## 5. Env-var register (by domain → tier)
|
|
138
138
|
|
|
139
|
+
**[`04-configuration-reference.md`](04-configuration-reference.md) is the variable table**, and it is
|
|
140
|
+
the one two instruments read — `driver/doc-constants.mjs` and `scripts/env-audit.mjs`. What follows
|
|
141
|
+
here is the governance view: which tier a name sits in and who may change it. When the two disagree
|
|
142
|
+
about what a variable *does*, 04 is right and this page is stale.
|
|
143
|
+
|
|
139
144
|
Tiers: T3 = operator-tunable (candidate for read-only display); T4 = backend-only (secret,
|
|
140
145
|
structural, or dev seam); [dev] = dev/test seam, never set in prod.
|
|
141
146
|
|
|
@@ -29,11 +29,36 @@ cut, not before: `cut/rules.mjs` treats a rule matching nothing as a refusal, so
|
|
|
29
29
|
does not yet exist cannot be pre-added, and `driver/test/release-pipeline.test.mjs` reds the moment the
|
|
30
30
|
file exists undecided.
|
|
31
31
|
|
|
32
|
-
**The drop list is `shared/withheld-paths.mjs`, and it is the only place a cut decision is recorded.**
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
32
|
+
**The drop list is `shared/withheld-paths.mjs`, and it is the only place a cut decision is recorded.** A
|
|
33
|
+
decision recorded anywhere else — an issue, a chat, a comment — is not recorded.
|
|
34
|
+
|
|
35
|
+
**Amended 2026-09-09.** This paragraph named two tests as the enforcement: `publication-scrub` and
|
|
36
|
+
`no-caveat-repair`. Neither exists. Measured across both repositories that day: neither file is tracked,
|
|
37
|
+
neither is on disk, and neither was ever added or deleted in either repository's history. A decision
|
|
38
|
+
record that names enforcement which was never written is worse than one that names none, because a
|
|
39
|
+
reader stops looking. What follows is what actually holds the line.
|
|
40
|
+
|
|
41
|
+
**The drop list itself does not ship.** It stays with the archive by owner ruling, 2026-08-31, so the
|
|
42
|
+
published tree carries no copy — and neither, today, does any other tree. `CUT_RECORD_PRESENT` is false
|
|
43
|
+
everywhere the code runs.
|
|
44
|
+
|
|
45
|
+
**What enforces the cut today is `shared/withheld-paths-access.mjs` and its three readers.** The accessor
|
|
46
|
+
is the only thing that reads the drop list, and it is written to work where the list is absent:
|
|
47
|
+
|
|
48
|
+
| Reader | With the list | Without it, which is every tree today |
|
|
49
|
+
|---|---|---|
|
|
50
|
+
| `scripts/citation-line-check.mjs` | a withheld file's citations need not resolve | every file crosses the cut, so every citation must resolve |
|
|
51
|
+
| `scripts/mint-suite-census.mjs` | a withheld test file is a stated absence | a removed test file is a LOSS |
|
|
52
|
+
| `shared/reference-guard-classes.mjs` | withheld paths are skipped | nothing is skipped, so the whole tree is counted |
|
|
53
|
+
|
|
54
|
+
Every one degrades **stricter**, never weaker, which is the property that makes the absence safe. A
|
|
55
|
+
separate record of what was cut is kept with the archive rather than published, so this repository can be
|
|
56
|
+
checked without it.
|
|
57
|
+
|
|
58
|
+
**The gap, stated rather than left to be found.** Because no tree carries the list, the with-the-list
|
|
59
|
+
column above has never run. What is enforced today is the strict fallback, not the record — and a check
|
|
60
|
+
that has never executed its other branch is a check whose other branch is unproven. The accessor
|
|
61
|
+
announces which mode it is in, once, so a reader of any run can tell which column applies.
|
|
37
62
|
|
|
38
63
|
**What survives a withheld document is the fact itself, moved to where the code enforces it:**
|
|
39
64
|
|
package/driver/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
# clearotron-driver
|
|
2
2
|
|
|
3
|
+
## 0.2.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- d35dd9b: Fixed: A search that was planned and never run is disclosed on the report again, even when another search mentions the same word.
|
|
8
|
+
|
|
9
|
+
One row on the coverage section says a planned search never reached the register. That row was removed whenever another row's heading carried the same words. A row for a search that had run and found nothing could remove it.
|
|
10
|
+
|
|
11
|
+
So a report could mention a term in its coverage section and say nothing was left undone. The only disclosure of the gap had been dropped. A completed search no longer stands in for an uncompleted one.
|
|
12
|
+
|
|
13
|
+
Re-rendering an archived report restores the row where this had removed it.
|
|
14
|
+
- b47be41: Fixed: Seven fixed sentences on the clearance report are now written for the person reading it.
|
|
15
|
+
|
|
16
|
+
These lines print on every report and none of them was written for a client. The footer explained the risk-band vocabulary to a developer. A paragraph defined a label most readers never saw on their page. An internal coverage note ran to about a thousand characters of the engine's own search names and ended mid-word.
|
|
17
|
+
|
|
18
|
+
Registration numbers no longer carry "(placeholder)" where the register has no per-record link — the number stands on its own. A gap that was disclosed twice, once in the model's words and once in the engine's, is disclosed once. Three section captions say what the section is rather than how it was produced. And a use check that found nothing no longer prints an evidence tag beside it, which read as a contradiction.
|
|
19
|
+
|
|
20
|
+
The page also no longer calls itself a working draft for legal review. That sentence printed on every report and went with the footer rewrite; it is a deliberate removal, not a casualty of one.
|
|
21
|
+
|
|
22
|
+
Nothing here changes what was searched, counted, rated or judged. Every fact about where a record came from is still there: the registration numbers, the dates they were read, the year each was registered.
|
|
23
|
+
- a4c9045: Fixed: The demo now names the register its example run was captured against.
|
|
24
|
+
|
|
25
|
+
`clearotron demo` said the run came from a production EU register. It did not. All four frozen runs were captured against Clarivate Compumark, which is what their own records and the reports say. The label also carried a capture date that matches none of the four runs, so it has gone.
|
|
26
|
+
|
|
27
|
+
The line now says what it is: a real run against Clarivate Compumark, and replaying it needs no account, no key and no network. That last part was always true and is worth saying where a reader meets it.
|
|
28
|
+
- f3bab16: Fixed: The lines a client reads first are now checked for the profession's vocabulary and for sentences carrying more than one idea. The reviewing pass rewrites them before delivery. Nothing about the check is shown to the client, and no run fails over it. A report clearing a name like PREVAIL is unaffected, because the mark being cleared is never read as a legal term.
|
|
29
|
+
- 862f76a: Fixed: The screen that will not start a search now gives advice that fits your machine.
|
|
30
|
+
|
|
31
|
+
An install with the engine program present, but invisible to the engine service, was told to install a program it already had. That advice cannot work. Following it changes nothing, because the engine reads its PATH when it starts. Until it is restarted, every screen reports what it saw at startup. Nothing said so.
|
|
32
|
+
|
|
33
|
+
The New clearance notice now tells those two states apart. Where the program is absent it gives the install advice as before. It adds that the service has to be restarted afterwards before it will notice. Where the program is present and the engine cannot see it, the notice says that instead, and names the restart as the remedy. Staff also get a link from that notice to the configuration page.
|
|
34
|
+
|
|
35
|
+
The configuration page's engine row now names the program it could not find. It names the command that installs it too, and `clearotron doctor` says the same, in the same words.
|
|
36
|
+
|
|
3
37
|
## 0.2.3
|
|
4
38
|
|
|
5
39
|
### Patch Changes
|