clearotron 0.2.2 → 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 +64 -37
- package/.nvmrc +1 -1
- package/CONTRIBUTING.md +8 -4
- package/INSTALL.md +36 -14
- package/README.md +18 -22
- package/THIRD-PARTY-NOTICES.md +1 -1
- package/bin/clearotron.mjs +22 -0
- package/bin/example.mjs +10 -5
- package/bin/onboard.mjs +370 -28
- package/bin/start.mjs +220 -12
- package/build-info.json +2 -2
- package/demo/README.md +1 -1
- package/demo/full-country-search/run/_driver/framework.json +1 -1
- package/demo/global-preliminary-search/run/_driver/framework.json +1 -1
- package/demo/knockout-search/run/_driver/framework.json +1 -1
- package/demo/multi-country-focus-search/run/_driver/framework.json +1 -1
- package/docs/E2E.md +5 -5
- package/docs/GLOSSARY.md +85 -0
- package/docs/PORTAL.md +2 -2
- package/docs/README.md +6 -4
- package/docs/SECURITY.md +17 -0
- package/docs/architecture/01-product-overview.md +21 -9
- package/docs/architecture/04-configuration-reference.md +2 -0
- package/docs/architecture/05-config-governance.md +19 -0
- package/docs/architecture/05-customer-profiles.md +2 -2
- package/docs/configuration.md +4 -3
- package/docs/decisions/0006-what-the-public-repository-carries.md +30 -5
- package/driver/CHANGELOG.md +90 -0
- package/driver/README.md +25 -6
- package/driver/common-law-receipts.mjs +1 -1
- package/driver/config-staleness.mjs +1 -1
- package/driver/connotation-search.mjs +1 -1
- package/driver/contract-audit.mjs +5 -1
- package/driver/contract-e3-baseline.json +11 -11
- package/driver/dev-portal.mjs +2 -2
- package/driver/doubt-selection.mjs +1 -1
- package/driver/drainer-identity.mjs +1 -1
- package/driver/driver.config.mjs +1 -1
- package/driver/effort-model.mjs +2 -2
- package/driver/engine/cli-version.mjs +103 -0
- package/driver/engine/probe.mjs +45 -5
- package/driver/enqueue-schema.mjs +8 -8
- package/driver/feedback-store.mjs +1 -1
- package/driver/findings-model.mjs +52 -2
- package/driver/flag-snapshot.mjs +20 -0
- package/driver/framework.mjs +1 -1
- package/driver/gateway.mjs +20 -3
- package/driver/outbox-backoff.mjs +1 -1
- package/driver/package.json +3 -3
- package/driver/party-facts.mjs +2 -2
- package/driver/phase0.mjs +1 -1
- package/driver/pipeline.mjs +211 -106
- package/driver/plain-register.mjs +134 -0
- package/driver/portal-access.mjs +1 -1
- package/driver/portal-config-view.mjs +70 -2
- package/driver/portal-families.mjs +1 -1
- package/driver/portal-local-auth.mjs +5 -1
- package/driver/portal-report.mjs +1 -1
- package/driver/portal-service.mjs +79 -9
- package/driver/portal-upstream.mjs +2 -2
- package/driver/predelivery-lint.mjs +151 -2
- package/driver/profile-page.html +10 -2
- package/driver/profile-service.mjs +4 -4
- package/driver/profiles/README.md +3 -3
- package/driver/profiles.mjs +144 -12
- package/driver/publish/index.mjs +2 -2
- package/driver/publish/knockout.mjs +7 -1
- package/driver/publish/render-knockout.mjs +453 -118
- package/driver/publish/render.mjs +111 -16
- package/driver/reference-strip-signatures.mjs +20 -0
- package/driver/search-policy.mjs +3 -3
- package/driver/skills/blind-frame/SKILL.md +1 -1
- package/driver/skills/knockout-assess/SKILL.md +84 -0
- package/driver/skills/matter-frame/SKILL.md +6 -6
- package/driver/skills/matter-frame/watchlist-reference.md +2 -2
- package/driver/skills/prelim-common-law/SKILL.md +1 -1
- package/driver/skills/prelim-search/SKILL.md +1 -1
- package/driver/skills/prelim-search/report-prose.md +82 -0
- package/driver/skills/prelim-search/risk-framework-demo.manifest.json +1 -1
- package/driver/skills/prelim-search/risk-framework-triage.manifest.json +1 -1
- package/driver/skills/prelim-search/risk-framework.manifest.json +1 -1
- package/driver/skills/prelim-search/synthesis-rules.md +1 -1
- package/driver/skills/prelim-variants/SKILL.md +3 -3
- package/driver/stage-context.mjs +13 -0
- package/driver/stages.mjs +54 -7
- package/driver/suite-census.json +220 -58
- package/driver/systemd/clearotron-worker.service +3 -3
- package/driver/tokens.mjs +1 -1
- package/driver/unit-inventory.mjs +214 -27
- package/driver/verify.mjs +1 -1
- package/driver/whatif-memo-run.mjs +26 -4
- package/driver/whatif-memo.mjs +19 -1
- package/examples/README.md +2 -2
- package/examples/grants.example.json +6 -7
- package/mcp-server/CHANGELOG.md +4 -0
- package/mcp-server/lib/scrub.mjs +1 -1
- package/mcp-server/lib/whatif.mjs +2 -2
- package/mcp-server/package.json +1 -1
- package/mcp-server/packs/ops/EXAMPLES.md +2 -2
- package/mcp-server/server.mjs +1 -1
- package/package.json +31 -19
- package/portal-ui/dist/assets/{index-Ct3iQtZ2.css → index-1ziUJX1E.css} +43 -2
- package/portal-ui/dist/assets/{index-BUGgiYtF.js → index-KFAHMgdT.js} +289 -73
- package/portal-ui/dist/index.html +2 -2
- package/portal-ui/package.json +1 -1
- package/providers/_shared/term-shape.mjs +1 -1
- package/providers/oauth-mcp-bridge/CHANGELOG.md +4 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/providers/uspto-local/src/index-store.js +2 -2
- package/scripts/added-reference-check.mjs +45 -89
- package/scripts/citation-line-check.mjs +121 -2
- package/scripts/e2e.mjs +2 -2
- package/scripts/env-classify.mjs +67 -3
- package/scripts/generated-files-are-current.mjs +125 -0
- package/scripts/live-surface-check.mjs +38 -1
- 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/nightly-notice.mjs +0 -2
- package/scripts/preinstall-node-check.mjs +24 -0
- 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 +38 -7
- package/scripts/test-full.mjs +226 -0
- package/scripts/test-run.mjs +16 -0
- package/shared/README.md +2 -1
- package/shared/anon-overlay.mjs +3 -3
- package/shared/browser-temp-root.mjs +142 -0
- package/shared/checkout-move.mjs +1 -1
- package/shared/client-door.mjs +4 -3
- package/shared/customer-store.mjs +1 -1
- package/shared/identifier-scan.mjs +1 -1
- package/shared/install-auth.mjs +35 -0
- package/shared/invocation.mjs +33 -1
- package/shared/listen.mjs +55 -3
- package/shared/names-in-force.mjs +6 -1
- package/shared/node-floor.mjs +97 -0
- 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/staff-domain.mjs +154 -0
- package/shared/store-in-repo.mjs +23 -2
- package/shared/suite-census.mjs +31 -3
- package/shared/withheld-paths-access.mjs +36 -18
- package/skills/clearotron-ops/SKILL.md +1 -1
- package/driver/recipes/README.md +0 -49
- package/driver/skills/prelim-search/risk-framework-aurora.manifest.json +0 -19
- package/driver/skills/prelim-search/risk-framework-aurora.md +0 -77
- package/driver/skills/prelim-search/risk-framework-zephyr.manifest.json +0 -14
- package/driver/skills/prelim-search/risk-framework-zephyr.md +0 -33
- package/driver/skills/prelim-search/worked-examples-aurora.md +0 -10
- package/driver/skills/prelim-search/worked-examples-zephyr.md +0 -10
- package/scripts/ai-page-render-check.mjs +0 -532
- package/scripts/clearances-render-check.mjs +0 -480
- package/scripts/composer-render-check.mjs +0 -1027
- package/scripts/home-render-check.mjs +0 -526
- package/scripts/portal-lifecycle-check.mjs +0 -639
|
@@ -283,6 +283,8 @@ because the rule is about what PRODUCT CODE reads, not about what a run reads.
|
|
|
283
283
|
|---|---|---|
|
|
284
284
|
| `DEMO_PORT` | `18900` | Port `npx clearotron demo` serves the replayed report on. `--port` overrides it. |
|
|
285
285
|
| `CLEAROTRON_DEMO` | unset | `1` puts this install in the DEMO posture. **Ordering is real**: the four products are listed and orderable, the form, the plan and the confirmation are the product's own, and the confirmation resolves to a finished report that already exists rather than dispatching — no engine turn, no register call, no queue entry, no run directory (owner ruling 2026-08-31, superseding the greyed-control ruling of the same day). A product the demo carries no finished report for refuses and names which one. It also re-aims two boot warnings written for an operator of a real deployment at the visitor who is not one, from one place (`driver/demo-posture.mjs`). **Set by `npx clearotron demo`, not by an operator** — it is passed explicitly to the two processes that have a reason to know (the portal and the MCP door; the worker is not told, because a demo never queues anything for it to drain), and those run with `CLEAROTRON_NO_ENV_FILE=1`, so a stray `.env` can neither put a live install into demo mode nor take a demo out of one. Anything but the literal `1` is not a demo. Replaces `PORTAL_DEMO`, which named only one of the processes that has to know. |
|
|
286
|
+
| `CLEAROTRON_TEST_FIXTURE_PROFILES` | unset | `1` makes the profile loader return the three suite fixtures, which are refused from every roster otherwise. Set by `scripts/test-run.mjs`, never by an operator; an explicit `includeTestFixtures` argument beats it. Effect class `harness`; the full contract is its row in `.env.example`. |
|
|
287
|
+
| `CLEAROTRON_DEMO_PROFILES` | unset | `1` makes the profile loader return the bundled demo account, which a fresh install does not resolve (owner ruling 2026-09-08). Set by `clearotron demo`, `start --demo` and the suite runner, never by an operator. The gate is on the bundled layer, so a deployment's own configured store keeps its `demoData` accounts either way. Effect class `harness`; the full contract is its row in `.env.example`. |
|
|
286
288
|
| `PORTAL_LOCAL_CREDENTIAL` | `~/.cordillera/portal-local-credential.json` | Where local sign-in keeps its passphrase DIGEST. `npx clearotron demo` points it inside the demo's own base directory, so a demo mints its own passphrase instead of inheriting a digest minted for another address — and removing the demo stays one `rm -rf`. |
|
|
287
289
|
| `PORTAL_LOCAL_PASSPHRASE` | unset | **NEVER set this in a file.** An internal one-shot handoff, not an operator control: on a first FOREGROUND start the supervisor mints the passphrase and hands it to the portal it spawns *at the spawn call*, so the closing summary can print the value beside the address rather than sending a first-time reader back into eleven startup log lines for the one value in this product that cannot be read back. It is deliberately absent from the composed child environments, because that composition is what `--background` writes into the units' env file — a passphrase there would be a permanent plaintext copy on disk and the product's own sentence, "it is stored only as a digest", would stop being true. Setting it in any env file recreates exactly that. Lost passphrase: `clearotron passphrase --reset`. |
|
|
288
290
|
| `PORTAL_URL` | `http://127.0.0.1:18802`, or built from `PORTAL_SERVICE_HOST`/`PORTAL_SERVICE_PORT` | Where the deploy tick's live-surface check expects to reach the portal. |
|
|
@@ -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
|
|
|
@@ -303,6 +308,20 @@ Staff MCP: `TRADEMARK_MCP_HTTP_PORT` (18790), `TRADEMARK_MCP_HTTP_HOST`,
|
|
|
303
308
|
`TRADEMARK_MCP_EMAIL_CLAIM`, `TRADEMARK_MCP_AUTH_HEADER`, `TRADEMARK_MCP_URL`,
|
|
304
309
|
`TRADEMARK_MCP_AUTH_MODE` (see below),
|
|
305
310
|
`TRADEMARK_MCP_AUTH_DISABLED` / `TRADEMARK_MCP_DEV` (dev seams — never set in prod).
|
|
311
|
+
|
|
312
|
+
`TRADEMARK_MCP_KEY_SOCKET` is the local key door, and it is a PATH rather than a port on purpose. This
|
|
313
|
+
interface serves two populations that authenticate differently: people arriving through a tunnel, who
|
|
314
|
+
prove themselves with a proxy identity, and programs on the same machine holding a scoped access key,
|
|
315
|
+
which can never produce one. "Only accept a key from loopback" does not separate them — the tunnel daemon
|
|
316
|
+
runs on the same machine, so a request forwarded from the internet and one from the local portal arrive
|
|
317
|
+
with the same peer address. A tunnel forwards to a port and cannot reach a unix socket, so the transport
|
|
318
|
+
is the discriminator and the two doors are separate objects with opposite rules.
|
|
319
|
+
|
|
320
|
+
Unset means no key door, and a deployment gets exactly the door it had before. The socket is created
|
|
321
|
+
`0660` — owner and group only — with the mode set after `listen`, because `listen` creates the file and
|
|
322
|
+
the umask decides what it starts as. It refuses to open at all with no grants file, alongside
|
|
323
|
+
`TRADEMARK_MCP_AUTH_DISABLED`, or when `TRADEMARK_MCP_AUTH_MODE` is `token`: that mode makes the NETWORK
|
|
324
|
+
door take a key too, which is the thing this exists to prevent. Read by `mcp-server/key-socket.mjs`.
|
|
306
325
|
Client MCP: `CLIENT_MCP_HTTP_PORT` (code default 18811, matching the deployed unit; it was 18795,
|
|
307
326
|
which collided with a co-hosted warm-MCP block, so keep any unit override that names a port),
|
|
308
327
|
`CLIENT_MCP_HTTP_HOST`, `CLIENT_MCP_ALLOWED_HOSTS`,
|
|
@@ -3,8 +3,8 @@
|
|
|
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
|
> This chapter describes the profile *mechanism*. Real customer bundles load from an external store
|
|
6
|
-
> via `CLEAROTRON_CUSTOMERS_DIR` and no customer is named here; the
|
|
7
|
-
> synthetic
|
|
6
|
+
> via `CLEAROTRON_CUSTOMERS_DIR` and no customer is named here; the package ships `generic` and the demo
|
|
7
|
+
> brand owner, and the repository holds three further synthetic profiles for the test suite — see
|
|
8
8
|
> [`driver/profiles/`](../../driver/profiles/).
|
|
9
9
|
|
|
10
10
|
One engine, never forked — three layers. The reasoning core is shared by every client; the client
|
package/docs/configuration.md
CHANGED
|
@@ -115,9 +115,10 @@ under that framework. Absent, the generic set applies.
|
|
|
115
115
|
|
|
116
116
|
## 3. Client profiles
|
|
117
117
|
|
|
118
|
-
One JSON file per client under [`driver/profiles/`](../driver/profiles/).
|
|
119
|
-
examples: `generic` (the Generic default)
|
|
120
|
-
|
|
118
|
+
One JSON file per client under [`driver/profiles/`](../driver/profiles/). Two are published as working
|
|
119
|
+
examples: `generic` (the Generic default) and the demo brand owner. Three further synthetic profiles —
|
|
120
|
+
gaming, functional drinks and animal health — exist in the repository for the test suite and are left
|
|
121
|
+
out of the package.
|
|
121
122
|
|
|
122
123
|
A job picks its profile by **forwarding domain** — `matchDomains[]`, exact host or dot-suffix. The
|
|
123
124
|
applicant named in a request never selects a profile. Resolution happens once at run start and is
|
|
@@ -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,95 @@
|
|
|
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
|
+
|
|
37
|
+
## 0.2.3
|
|
38
|
+
|
|
39
|
+
### Patch Changes
|
|
40
|
+
|
|
41
|
+
- 54ea03a: Fixed: The configuration check no longer reports a problem when the client connector's address is reached with its own token. Putting a single sign-on front before it is the client's choice, and running it token-based is supported. The check still says the sign-in audience was not compared against that address, so it never implies the two agree.
|
|
42
|
+
- 4b6be02: Fixed: A bundled risk framework now states where it came from in words a customer can read. The note used to carry a confidentiality marking, a filename for a document not included, an internal reference number and revision history. It says whose framework it is, who stands behind it and which revision, and nothing else.
|
|
43
|
+
- 63f916c: Fixed: A fresh install's brand-owner list now offers Generic and, with the demo, the demo account. It offered three of our test accounts as well, on the install route that clones the repository.
|
|
44
|
+
- 5d12baa: Fixed: A run record now says which version of the engine's command-line tool served it. A change in results can be traced to a tool upgrade rather than guessed at.
|
|
45
|
+
|
|
46
|
+
Fixed: A version the tool could not report is recorded as unreadable. An absent field could not be told apart from a probe that never ran.
|
|
47
|
+
- 888de5d: Fixed: The deployment check now says whether each scheduled job's timer is still armed. A timer-driven service reads "inactive" between runs and when its timer has been stopped. So a check that asked only about the service could report nothing wrong while the scheduled work had quietly stopped happening.
|
|
48
|
+
- 3a75e22: Fixed: A supplementary memo now succeeds on its first attempt. It cost two turns instead of one, and left a retried-stage mark on a report that had been delivered cleanly.
|
|
49
|
+
|
|
50
|
+
Fixed: A supplementary memo states the rating framework it was reasoned under. A report assessed under a customer's own framework said so; a memo written from it did not.
|
|
51
|
+
- fe22384: Fixed: Installing on a Node version the engine cannot run on now stops at once. It names the version you have, the version needed, and the command that fixes it. Before, the install finished and the first US register search failed with an error that never mentioned Node. The supported floor is Node 22.13 or newer.
|
|
52
|
+
- 74b2cdd: Fixed: Setup now asks which address signs in, instead of turning it into an access rule covering everyone who shares its email domain.
|
|
53
|
+
|
|
54
|
+
For operators: The People & access page now names the setting behind a staff rule, and the file to edit to undo it.
|
|
55
|
+
- 9053919: Fixed: Five things a first-time reader could not act on.
|
|
56
|
+
|
|
57
|
+
A screen that needs a brand owner chosen no longer points at the top left when the menu is collapsed and there is nothing there.
|
|
58
|
+
|
|
59
|
+
The profile editor's Save button now states the one thing that is blocking it. That reason was always there, but it looked the same as the states where nothing is wrong.
|
|
60
|
+
|
|
61
|
+
The default jurisdictions field now says what it does with what you type. Entries are checked against the list and kept, never refused. A region counts as one entry.
|
|
62
|
+
|
|
63
|
+
The trademark categories field explains what those numbers are, with examples, before naming the standard they come from.
|
|
64
|
+
|
|
65
|
+
The row of coloured ratings under "Risk framework in force" now says it belongs to that framework. It used to read as a list of several frameworks. Both editors say it the same way.
|
|
66
|
+
- fe22384: For operators: Clearotron runs on Node 22.13 or newer again, down from 22.19, because its HTTP client moves back to version 7. Anyone who upgraded Node for the last release has nothing to undo.
|
|
67
|
+
- bb899a7: New: A screening report now leads with the read. Each conflict shows its name, band, source and a one-sentence verdict. The paragraph arguing that verdict is one click away. Register filings appear as conflicts only where the reviewer rated them above the lowest band; the rest stay in the filings table.
|
|
68
|
+
|
|
69
|
+
New: What was asked is now at the top of a screening report, with any question about whether it was the right thing to ask. Both used to sit at the bottom.
|
|
70
|
+
|
|
71
|
+
New: The long per-name assessment now opens from the read. The engine has always written it and the page never showed it.
|
|
72
|
+
|
|
73
|
+
New: Register counts say what they counted in their column headers. Territories are named rather than printed as two-letter codes.
|
|
74
|
+
|
|
75
|
+
New: The lines a reader meets before opening anything are written in plain language. That is the summary, the basis line, each conflict's one sentence and the reviewer's notes. Legal vocabulary stays where the detail is.
|
|
76
|
+
|
|
77
|
+
Fixed: Reviewer notes no longer appear on a screening report exported to PDF. They are for the reviewing lawyer, and a report forwarded to a client used to carry them.
|
|
78
|
+
- 2a812d6: Fixed: The New clearance screen now has a **Start a search** button. The button that ran a search used to say "Review clearance", and people could not tell it was the way to begin.
|
|
79
|
+
|
|
80
|
+
Fixed: When a search cannot start yet, the screen says what is still missing, both on the form and beside the button. It used to grey the button out and give no reason at all.
|
|
81
|
+
|
|
82
|
+
Fixed: **Save as search** now confirms the save beside the button that was pressed, names what was saved, and links to it. It gave no sign at the point of the click.
|
|
83
|
+
|
|
84
|
+
Fixed: The unsaved-changes warning no longer fires on a search you have just saved. It compared the form against a blank one and nothing ever reset it.
|
|
85
|
+
|
|
86
|
+
Fixed: The message shown when no search engine is attached now names the setup command for the way you installed. It used to name the one that only works in a copy of the source.
|
|
87
|
+
- 58af0b7: Fixed: The settings page no longer shows the engine as healthy when the engine program cannot be found.
|
|
88
|
+
|
|
89
|
+
That page reports which engine is configured. The New clearance screen reports whether a search can start right now. When those two readings disagreed, neither screen said so.
|
|
90
|
+
|
|
91
|
+
An install could therefore show a green engine while no search would start, and nothing explained the gap. The settings page now names the disagreement and says what to do about it. Running `clearotron doctor` reports the same thing in the same words.
|
|
92
|
+
|
|
3
93
|
## 0.2.2
|
|
4
94
|
|
|
5
95
|
### Patch Changes
|
package/driver/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# clearotron
|
|
1
|
+
# clearotron driver — deterministic orchestration for the trademark clearance workflow
|
|
2
2
|
|
|
3
3
|
Orchestration in **code**; the LLM does only judgment leaves. Each pipeline stage is **one blocking
|
|
4
4
|
engine turn** (default engine: `anthropic-agent`, shelling `claude -p` per stage); fan-out/fan-in/
|
|
@@ -18,8 +18,7 @@ the gather MCP servers live.
|
|
|
18
18
|
```
|
|
19
19
|
integrator ──job JSON (enqueue CLI / start_run / queue file)──┐
|
|
20
20
|
▼
|
|
21
|
-
|
|
22
|
-
systemd .timer (~90s fallback re-drain) ─▶ (same oneshot)
|
|
21
|
+
clearotron-worker.service (the built-in worker, watches every queue)
|
|
23
22
|
└─ node runner.mjs ─▶ for each queue: claim ─▶ pipeline(job, {agent}) ─▶ runStage() ─▶
|
|
24
23
|
engine turn (`claude -p`, blocking) ─▶ leaf turn
|
|
25
24
|
```
|
|
@@ -42,8 +41,28 @@ identity ran it. Headless deployments with no per-agent workspaces use one expli
|
|
|
42
41
|
delivered report, run failure, intake rejection, duplicate skip, late-bind ack — is a
|
|
43
42
|
self-contained packet in the outbox (`outbox.mjs`). Integrators consume events over the ops MCP
|
|
44
43
|
(`list_outbox_events` → `get_delivery_packet` → send → `mark_sent`/`ack_event`) and never touch the
|
|
45
|
-
driver's filesystem. `deliver-trigger.sh`
|
|
46
|
-
|
|
44
|
+
driver's filesystem. `deliver-trigger.sh` is the reference wake-up for an
|
|
45
|
+
agent-based integrator. The outbox units beside it are retired — see the units row below.
|
|
46
|
+
|
|
47
|
+
## What a file name tells you
|
|
48
|
+
|
|
49
|
+
Eleven suffixes carry a meaning across the whole directory. They are a naming convention rather than
|
|
50
|
+
a framework — nothing enforces them — but they are consistent enough that the suffix tells you what
|
|
51
|
+
kind of thing a module is before you open it.
|
|
52
|
+
|
|
53
|
+
| suffix | what the module is |
|
|
54
|
+
|---|---|
|
|
55
|
+
| `-model` | a pure parser or shape: text or JSON in, a validated structure out, no I/O |
|
|
56
|
+
| `-record` | a recording transport — the typed call a stage makes and the row it writes |
|
|
57
|
+
| `-form` | a driver-written form a stage fills in, with its own validation |
|
|
58
|
+
| `-io` | the reader and writer for a form's file, kept apart from the form's shape |
|
|
59
|
+
| `-union` | the accumulator that merges many writes into one form |
|
|
60
|
+
| `-call` | the model-call half of a pair: what the stage is asked to send |
|
|
61
|
+
| `-tool` | the driver-write half of the same pair: what the driver does with what came back |
|
|
62
|
+
| `-duty` | an obligation check — was something the run owed actually done |
|
|
63
|
+
| `-gate` | a refusal: a condition that stops a run or a stage rather than reporting on it |
|
|
64
|
+
| `-ledger` | an append-only account of what happened, read back for coverage and audit |
|
|
65
|
+
| `-verdict` | a pure function from evidence to a stated answer, with its reason |
|
|
47
66
|
|
|
48
67
|
## Files
|
|
49
68
|
|
|
@@ -63,7 +82,7 @@ identity ran it. Headless deployments with no per-agent workspaces use one expli
|
|
|
63
82
|
| `coverage-ledger.mjs` | Machine coverage-ledger contract: strict JSON-mirror parser (token-first throws), prose parser, `REGISTER_AXES`/`decideAxes`. |
|
|
64
83
|
| `enqueue-schema.mjs` | Job-file shape + `validateJob`. |
|
|
65
84
|
| `dev-portal.mjs` | Loopback-only dev-instance UI (static pool + `/profiles/*` proxy) for dry-run testing — see `../docs/E2E.md` Tier 1b. |
|
|
66
|
-
| `systemd/*` |
|
|
85
|
+
| `systemd/*` | `clearotron-worker.service` is the live worker and what a deployment runs. The path/timer/oneshot drain units and the outbox-wake units beside them are RETIRED: the built-in worker replaced that posture. Their files stay until the production rebuild — `unit-inventory.mjs` records the ruling and the reason, and reports a retired unit's absence as expected rather than as a fault. They keep their original names because a deployed box still holds them under those names. |
|
|
67
86
|
|
|
68
87
|
## Key engine facts
|
|
69
88
|
|
|
@@ -154,7 +154,7 @@ export function countMatrixCells(findingsContent) {
|
|
|
154
154
|
* "丝绸与铁 / 席尔克": each alternate is its own search term with its own grid — the copper-conduit worker
|
|
155
155
|
* keyed the split forms with full receipts while the validator demanded the compound key, 2026-06-12);
|
|
156
156
|
* or WORD-BOUNDARY substring coverage (a fully-receipted variant covers a LONGER variant containing it
|
|
157
|
-
* as a whole-word phrase — "
|
|
157
|
+
* as a whole-word phrase — "Foxglade Legends II: Oak & Ember" ⊃ "Oak & Ember"; a marketplace phrase
|
|
158
158
|
* search for the short form surfaces every listing of the long form. Mid-word containment does NOT
|
|
159
159
|
* count: "venzyy" ⊅ "venzy" — storefront search tokenizes, a doubled-letter variant is its own term).
|
|
160
160
|
*/
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// ── THE OUTCOME THIS EXISTS TO STOP ─────────────────────────────────────────────────────────────────
|
|
6
6
|
//
|
|
7
7
|
// After the F41 repair the owner restarted the worker, as instructed, and refreshed the portal. It
|
|
8
|
-
// showed the BUNDLED DEMO ROSTER
|
|
8
|
+
// showed the BUNDLED DEMO ROSTER and not his own account.
|
|
9
9
|
//
|
|
10
10
|
// ~/.env repaired 10:51:16
|
|
11
11
|
// worker 10:51:21 ← new config
|
|
@@ -769,7 +769,7 @@ export function segmentBinding({ segment_index, fragment } = {}, candidate) {
|
|
|
769
769
|
//
|
|
770
770
|
// SO THE MEASUREMENT SURVIVES THE ENFORCEMENT. A fragment that arrives is still weighed and still
|
|
771
771
|
// matched, and the verdict rides out on `fragmentState` for the receipts histogram — which is how the
|
|
772
|
-
// transcription-quality signal
|
|
772
|
+
// transcription-quality signal testing used to settle this stays measurable after the duty that
|
|
773
773
|
// produced it is gone. Never charge for it: an unbound fragment is a fact about the model, and the
|
|
774
774
|
// seat's ruling is not worse for it.
|
|
775
775
|
const frag = String(fragment ?? "").trim();
|
|
@@ -276,7 +276,11 @@ export const E3_BACKLOG_KINDS = [...E3_KINDS, "other"];
|
|
|
276
276
|
|
|
277
277
|
const E3_PATTERNS = [
|
|
278
278
|
{ kind: "literal-json-skeleton", re: /```json|\{\s*"[a-z_]+"\s*:/i },
|
|
279
|
-
|
|
279
|
+
// `EXACTLY the` USED TO STAND ALONE, and it is ordinary English: "puts its noise on exactly the
|
|
280
|
+
// report that matters most" is prose about a report, not a dictated key set, and the pattern is
|
|
281
|
+
// case-insensitive so the lower-case form matched too. The alternative now names what may follow it,
|
|
282
|
+
// which is the only form that dictates a set. Everything else in this pattern is byte-identical.
|
|
283
|
+
{ kind: "exactly-these-keys", re: /EXACTLY (?:these|one of)|EXACTLY the (?:following|keys|fields)|keys EXACTLY|EXACTLY \{|closed enum|key set is closed|additionalProperties/i },
|
|
280
284
|
// A quoted or backticked template carrying a <placeholder>, close to an instruction to emit it. The
|
|
281
285
|
// placeholder class is deliberately loose (`<comma-separated DOMAINS>`, `<N>`, `<one record URI>`) —
|
|
282
286
|
// the tell is a shape dictated for a parser to read back, not the casing inside the angle brackets.
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
},
|
|
18
18
|
"common-law": {
|
|
19
19
|
"literal-json-skeleton": 0,
|
|
20
|
-
"exactly-these-keys":
|
|
20
|
+
"exactly-these-keys": 2,
|
|
21
21
|
"dictated-line-shape": 0
|
|
22
22
|
},
|
|
23
23
|
"common-law-half": {
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
},
|
|
53
53
|
"synthesis": {
|
|
54
54
|
"literal-json-skeleton": 10,
|
|
55
|
-
"exactly-these-keys":
|
|
55
|
+
"exactly-these-keys": 8,
|
|
56
56
|
"dictated-line-shape": 5
|
|
57
57
|
},
|
|
58
58
|
"case-law": {
|
|
@@ -114,7 +114,7 @@
|
|
|
114
114
|
},
|
|
115
115
|
"skills/prelim-common-law/SKILL.md": {
|
|
116
116
|
"literal-json-skeleton": 0,
|
|
117
|
-
"exactly-these-keys":
|
|
117
|
+
"exactly-these-keys": 1,
|
|
118
118
|
"dictated-line-shape": 2
|
|
119
119
|
},
|
|
120
120
|
"skills/prelim-register/SKILL.md": {
|
|
@@ -129,7 +129,7 @@
|
|
|
129
129
|
},
|
|
130
130
|
"skills/placement-inquiry/SKILL.md": {
|
|
131
131
|
"literal-json-skeleton": 1,
|
|
132
|
-
"exactly-these-keys":
|
|
132
|
+
"exactly-these-keys": 3,
|
|
133
133
|
"dictated-line-shape": 1
|
|
134
134
|
},
|
|
135
135
|
"skills/prelim-register/digest.md": {
|
|
@@ -139,7 +139,7 @@
|
|
|
139
139
|
},
|
|
140
140
|
"skills/prelim-search/phase2-execution.md": {
|
|
141
141
|
"literal-json-skeleton": 0,
|
|
142
|
-
"exactly-these-keys":
|
|
142
|
+
"exactly-these-keys": 0,
|
|
143
143
|
"dictated-line-shape": 3
|
|
144
144
|
},
|
|
145
145
|
"skills/frame-diff/SKILL.md": {
|
|
@@ -149,12 +149,12 @@
|
|
|
149
149
|
},
|
|
150
150
|
"skills/prelim-search/synthesis-rules.md": {
|
|
151
151
|
"literal-json-skeleton": 1,
|
|
152
|
-
"exactly-these-keys":
|
|
152
|
+
"exactly-these-keys": 1,
|
|
153
153
|
"dictated-line-shape": 2
|
|
154
154
|
},
|
|
155
155
|
"skills/prelim-search/firm-wide-reasoning.md": {
|
|
156
156
|
"literal-json-skeleton": 0,
|
|
157
|
-
"exactly-these-keys":
|
|
157
|
+
"exactly-these-keys": 0,
|
|
158
158
|
"dictated-line-shape": 0
|
|
159
159
|
},
|
|
160
160
|
"skills/prelim-search/risk-framework.md": {
|
|
@@ -164,7 +164,7 @@
|
|
|
164
164
|
},
|
|
165
165
|
"skills/prelim-search/worked-examples.md": {
|
|
166
166
|
"literal-json-skeleton": 0,
|
|
167
|
-
"exactly-these-keys":
|
|
167
|
+
"exactly-these-keys": 0,
|
|
168
168
|
"dictated-line-shape": 1
|
|
169
169
|
},
|
|
170
170
|
"skills/prelim-search/report-prose.md": {
|
|
@@ -179,7 +179,7 @@
|
|
|
179
179
|
},
|
|
180
180
|
"skills/narrative-refutation/SKILL.md": {
|
|
181
181
|
"literal-json-skeleton": 0,
|
|
182
|
-
"exactly-these-keys":
|
|
182
|
+
"exactly-these-keys": 2,
|
|
183
183
|
"dictated-line-shape": 1
|
|
184
184
|
},
|
|
185
185
|
"skills/prelim-search/delivery-contract.md": {
|
|
@@ -189,7 +189,7 @@
|
|
|
189
189
|
}
|
|
190
190
|
},
|
|
191
191
|
"totals": {
|
|
192
|
-
"stages":
|
|
193
|
-
"files":
|
|
192
|
+
"stages": 45,
|
|
193
|
+
"files": 64
|
|
194
194
|
}
|
|
195
195
|
}
|
package/driver/dev-portal.mjs
CHANGED
|
@@ -301,7 +301,7 @@ async function loadOutbox(){const r=await(await fetch("/dev/outbox")).json();
|
|
|
301
301
|
let REG=null;
|
|
302
302
|
const esc=(s)=>String(s??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'");
|
|
303
303
|
async function loadSearches(){
|
|
304
|
-
const cust=$("#s_cust").value.trim()||"
|
|
304
|
+
const cust=$("#s_cust").value.trim()||"demo-brand-owner";
|
|
305
305
|
try{
|
|
306
306
|
const all=await(await fetch("/recipes")).json();
|
|
307
307
|
if(all.error){$("#searches").innerHTML='<span class="err">'+esc(all.error)+'</span>';return;}
|
|
@@ -320,7 +320,7 @@ async function loadSearches(){
|
|
|
320
320
|
function useProduct(k){const f=$("#f");f.product.value=k;$("#f_recipe").value="";f.mark.focus();}
|
|
321
321
|
function useRecipe(cust,slug){const f=$("#f");f.product.value="";$("#f_recipe").value=slug;f.profile.value=cust;f.mark.focus();}
|
|
322
322
|
async function composeSubmit(save){
|
|
323
|
-
const cust=$("#s_cust").value.trim()||"
|
|
323
|
+
const cust=$("#s_cust").value.trim()||"demo-brand-owner";
|
|
324
324
|
const fd=new FormData($("#sf"));
|
|
325
325
|
const recipe={label:(fd.get("label")||"").trim(),base:fd.get("base")};
|
|
326
326
|
if(fd.get("registerProbe")!=null)recipe.components={registerProbe:true};
|
|
@@ -100,7 +100,7 @@ export function doubtsForClosure({ doubts = null, doubtClosure = null } = {}) {
|
|
|
100
100
|
// The event exists to answer whether a cut is WORTH giving a value, before any row has one: if most
|
|
101
101
|
// doubts carry no tier the cut saves nothing. A count written only when the cut is live needs the cut
|
|
102
102
|
// to exist in order to measure whether the cut should exist — the circularity pipeline.mjs's own
|
|
103
|
-
// comment says this event prevents. Found
|
|
103
|
+
// comment says this event prevents. Found in review on the R2 artifacts.
|
|
104
104
|
//
|
|
105
105
|
// `keepSet` null means "keep everything": nothing is ever dropped, `ids` is discarded below, and the
|
|
106
106
|
// dispatch stays byte-identical. Only the COUNTS change.
|
|
@@ -136,7 +136,7 @@ export function drainerVerdict({ stamp, headCommit, isAlive, processes, ppidOf =
|
|
|
136
136
|
: "";
|
|
137
137
|
|
|
138
138
|
if (!alive) {
|
|
139
|
-
// ── THE ONE BRANCH THE POSTURE DECIDES (
|
|
139
|
+
// ── THE ONE BRANCH THE POSTURE DECIDES (decided 2026-09-06).
|
|
140
140
|
//
|
|
141
141
|
// The same empty answer means two opposite things. Where a worker holds the queue open, a gone
|
|
142
142
|
// drainer is the outage: nothing is executing runs and nothing will start one. Where the box drains
|
package/driver/driver.config.mjs
CHANGED
|
@@ -248,7 +248,7 @@ export const config = {
|
|
|
248
248
|
// 2026-07-19: these were joined against DRIVER_DIR, so the driver read framework manifests out of
|
|
249
249
|
// its BUNDLED driver/skills while CLEAROTRON_INSTRUCTIONS_DIR pointed the agent at the config store. A
|
|
250
250
|
// customer whose framework lives only in the config store therefore hard-failed at attachFramework
|
|
251
|
-
// with `framework_manifest_missing` —
|
|
251
|
+
// with `framework_manifest_missing` — a test account's first run died there before any stage ran.
|
|
252
252
|
get skillsRoot() { return dirname(this.skillsDir); },
|
|
253
253
|
|
|
254
254
|
// ── Per-agent paths ───────────────────────────────────────────────────────
|
package/driver/effort-model.mjs
CHANGED
|
@@ -218,7 +218,7 @@ export const costBand = (i) => Math.max(1, Math.min(5, Math.ceil(effortUnits(i)
|
|
|
218
218
|
// range is the thing this section was rebuilt to remove; do not reintroduce one without a new ruling.
|
|
219
219
|
|
|
220
220
|
/**
|
|
221
|
-
* THE QUOTED BOUNDS. ONE SOURCE — owner ruling, 2026-08-23
|
|
221
|
+
* THE QUOTED BOUNDS. ONE SOURCE — owner ruling, 2026-08-23.
|
|
222
222
|
*
|
|
223
223
|
* WHAT THIS REPLACED, and why a constant beat a model. The old quote was a base plus one adder per lane:
|
|
224
224
|
* 1.5h, +0.5 for case law, +0.5 for a native-language lane, +0.5 for a single territory. It missed the
|
|
@@ -264,7 +264,7 @@ export const quoteBoundsFor = (l) => TURNAROUND_QUOTE[deriveMode(l) === "knockou
|
|
|
264
264
|
/**
|
|
265
265
|
* The bounds for THIS job. The ruled range, and NOTHING is done to it.
|
|
266
266
|
*
|
|
267
|
-
* — OWNER RULING, 2026-08-26
|
|
267
|
+
* — OWNER RULING, 2026-08-26: "No compute. We just say 1.5–2.5 hours for
|
|
268
268
|
* big reports, period. Keep it simple." The question put to him was which run-slot cap the quote should
|
|
269
269
|
* divide by, and he removed the division instead of answering it.
|
|
270
270
|
*
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
|
|
3
|
+
//
|
|
4
|
+
// THE VERSION OF THE BINARY THAT SERVED A RUN, captured at dispatch.
|
|
5
|
+
//
|
|
6
|
+
// The record already says whether the model id the provider reported names a pinned build or an alias it
|
|
7
|
+
// may repoint. That answers "did the model move". It cannot answer "did the TOOL move", and the two are
|
|
8
|
+
// different questions with the same symptom: a run whose judgment differs from last week's.
|
|
9
|
+
//
|
|
10
|
+
// Nothing recorded it. Three archived runs were walked for every spelling of a version field and carried
|
|
11
|
+
// none — so what served them is not recoverable, and the box could have answered at any time.
|
|
12
|
+
//
|
|
13
|
+
// ── UNREADABLE IS A VALUE, NOT AN OMISSION ──────────────────────────────────────────────────────────
|
|
14
|
+
//
|
|
15
|
+
// The whole point of the field is telling three states apart, and only one of them is "we know":
|
|
16
|
+
//
|
|
17
|
+
// { version: "2.1.241", probe: "ok" } the binary answered
|
|
18
|
+
// { version: null, probe: "unreadable", why } it was asked and could not say
|
|
19
|
+
// (no field at all) this record predates the gauge
|
|
20
|
+
//
|
|
21
|
+
// An omission on failure collapses the middle into the third, and a reader comparing two runs cannot
|
|
22
|
+
// tell a tool that would not answer from a record written before anybody asked. That distinction is the
|
|
23
|
+
// reason for the field, so failing to write it is failing at the thing rather than at the edge of it.
|
|
24
|
+
//
|
|
25
|
+
// ── ONE SPAWN PER BUILD, NOT PER PROCESS ────────────────────────────────────────────────────────────
|
|
26
|
+
//
|
|
27
|
+
// A dispatch is many stages and every stage would otherwise pay. But the cache lives as long as the
|
|
28
|
+
// process, and the process is NOT one run: the drainer's watch loop calls the pipeline for job after job
|
|
29
|
+
// without exiting. A first version cached there would be reported as fact for every later run in that
|
|
30
|
+
// process — including runs served by a binary somebody upgraded in place underneath it.
|
|
31
|
+
//
|
|
32
|
+
// That is the exact silence this field exists to end, reintroduced by the cache meant to make it cheap,
|
|
33
|
+
// and it would have been invisible: the record would carry a version, confidently, and be wrong.
|
|
34
|
+
//
|
|
35
|
+
// So the key is the path AND what the filesystem says about the file — an in-place upgrade changes the
|
|
36
|
+
// modification time and the size, so it misses the cache and is probed again. The path alone is not
|
|
37
|
+
// enough (two engines can point at one binary, an engine can be repointed) and the engine id is not
|
|
38
|
+
// enough for the same reason.
|
|
39
|
+
//
|
|
40
|
+
// A file the filesystem cannot describe is NOT CACHED at all. Caching an unreadable probe under a key
|
|
41
|
+
// derived from a failed stat would pin the failure for the life of the process, so a binary that
|
|
42
|
+
// appeared a moment later would keep reading as absent.
|
|
43
|
+
// ── WHAT THIS DEPENDS ON, WHICH IS NOT ENFORCEABLE FROM HERE ────────────────────────────────────────
|
|
44
|
+
//
|
|
45
|
+
// A probe that can change what it probes is not a probe. This one spawns the engine binary, so it rests
|
|
46
|
+
// on `--version` being side-effect-free — true of every real CLI and not something this module can make
|
|
47
|
+
// true. It bit immediately: the suite's engine stand-ins fell through to their stage path, and one of
|
|
48
|
+
// them counts invocations to decide when to fail, so the probe consumed the failure a retry test was
|
|
49
|
+
// measuring and the retry never happened. The symptom was an attempt count off by one, three files away
|
|
50
|
+
// from the cause.
|
|
51
|
+
//
|
|
52
|
+
// The stand-ins now answer `--version` and exit, which is what the binaries they stand in for do. A new
|
|
53
|
+
// one that forgets will produce the same off-by-one, so `driver/test/a-run-records-the-tool-that-served-it`
|
|
54
|
+
// asserts every engine stand-in answers — the cheap ratchet under a condition that cannot be checked at
|
|
55
|
+
// the call site.
|
|
56
|
+
import { execFileSync } from "node:child_process";
|
|
57
|
+
import { statSync } from "node:fs";
|
|
58
|
+
|
|
59
|
+
/** Live for the process, keyed by resolved path. A run is one process; a probe is one spawn. */
|
|
60
|
+
const CACHE = new Map();
|
|
61
|
+
|
|
62
|
+
/** The first version-shaped token in the output, or the whole first line when nothing matches. */
|
|
63
|
+
export function parseVersion(out) {
|
|
64
|
+
const line = String(out ?? "").split("\n").map((l) => l.trim()).filter(Boolean)[0] ?? "";
|
|
65
|
+
if (!line) return null;
|
|
66
|
+
// These CLIs answer `2.1.241`, `codex-cli 0.5.0`, `claude 2.1.241 (Claude Code)`. Take the first
|
|
67
|
+
// dotted number and keep it; a build that answers in prose is recorded verbatim rather than dropped,
|
|
68
|
+
// because a string somebody can compare beats a null.
|
|
69
|
+
return (line.match(/\b\d+\.\d+(?:\.\d+)?(?:[-+][0-9A-Za-z.-]+)?\b/) ?? [line])[0];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Probe one binary. PURE apart from the spawn, which is injected so an arm can drive both branches
|
|
74
|
+
* without a binary on the box — the unreadable branch is the one that matters and it cannot be produced
|
|
75
|
+
* on demand from a real install.
|
|
76
|
+
*
|
|
77
|
+
* Never throws. A probe that could take down a dispatch would be a worse defect than the gap it closes.
|
|
78
|
+
*/
|
|
79
|
+
export function probeCliVersion(bin, { run = null, timeoutMs = 5000, cache = CACHE, stat = statSync } = {}) {
|
|
80
|
+
if (!bin) return { version: null, probe: "unreadable", why: "no engine binary was resolved" };
|
|
81
|
+
let key = null;
|
|
82
|
+
try { const st = stat(bin); key = `${bin}\u0000${st.mtimeMs}:${st.size}`; } catch { /* not cacheable */ }
|
|
83
|
+
if (key && cache.has(key)) return cache.get(key);
|
|
84
|
+
const spawn = run ?? ((b) => execFileSync(b, ["--version"], {
|
|
85
|
+
encoding: "utf8", timeout: timeoutMs, stdio: ["ignore", "pipe", "ignore"],
|
|
86
|
+
}));
|
|
87
|
+
let result;
|
|
88
|
+
try {
|
|
89
|
+
const version = parseVersion(spawn(bin));
|
|
90
|
+
result = version
|
|
91
|
+
? { version, probe: "ok" }
|
|
92
|
+
// It ran and said nothing a version could be read from. That is not the same as failing to run,
|
|
93
|
+
// and a reader chasing a tool change needs to know which happened.
|
|
94
|
+
: { version: null, probe: "unreadable", why: "the binary answered with no version-shaped token" };
|
|
95
|
+
} catch (e) {
|
|
96
|
+
result = { version: null, probe: "unreadable", why: String(e?.message ?? e).slice(0, 160) };
|
|
97
|
+
}
|
|
98
|
+
if (key) cache.set(key, result);
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Drop the cache. For arms, and for a caller that has just repointed an engine deliberately. */
|
|
103
|
+
export function forgetCliVersions(cache = CACHE) { cache.clear(); }
|