clearotron 0.3.0-beta.4 → 0.3.0-beta.6
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 +9 -0
- package/INSTALL.md +13 -0
- package/bin/connect.mjs +3 -1
- package/bin/onboard.mjs +80 -28
- package/bin/start.mjs +82 -18
- package/bin/update.mjs +38 -6
- package/build-info.json +2 -2
- package/docs/architecture/04-configuration-reference.md +1 -0
- package/driver/CHANGELOG.md +36 -10
- package/driver/contract-e3-backlog.mjs +1 -1
- package/driver/demo-container.mjs +30 -2
- package/driver/engine/openai-agent.mjs +60 -2
- package/driver/enqueue-schema.mjs +3 -1
- package/driver/gateway.mjs +12 -0
- package/driver/package.json +1 -1
- package/driver/portal-local-auth.mjs +35 -3
- package/driver/portal-service.mjs +80 -18
- package/driver/portal-upstream.mjs +20 -1
- package/driver/search-policy.mjs +7 -2
- package/driver/suite-census.json +71 -23
- package/mcp-server/CHANGELOG.md +12 -2
- package/mcp-server/CONNECT.md +3 -2
- package/mcp-server/lib/options.mjs +13 -5
- package/mcp-server/package.json +1 -1
- package/package.json +1 -1
- package/portal-ui/dist/assets/{index-DWYCsOCJ.js → index-DBUVdQT-.js} +6 -0
- package/portal-ui/dist/index.html +1 -1
- package/portal-ui/package.json +1 -1
- package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/scripts/e2e.mjs +84 -12
- package/scripts/engine-probe.mjs +2 -2
- package/scripts/env-classify.mjs +3 -0
- package/shared/connect-clients.mjs +8 -0
- package/shared/names-in-force.mjs +1 -0
- package/shared/permanent-install.mjs +167 -0
- package/shared/stdio-connect.mjs +32 -14
- package/shared/verb-shim.mjs +10 -1
- package/shared/wsl.mjs +23 -0
package/driver/suite-census.json
CHANGED
|
@@ -135,6 +135,12 @@
|
|
|
135
135
|
"skips": 0,
|
|
136
136
|
"todos": 0
|
|
137
137
|
},
|
|
138
|
+
"a-codex-login-survives-its-own-refresh.test.mjs": {
|
|
139
|
+
"tests": 4,
|
|
140
|
+
"asserts": 18,
|
|
141
|
+
"skips": 0,
|
|
142
|
+
"todos": 0
|
|
143
|
+
},
|
|
138
144
|
"a-company-can-be-given-a-framework-after-it-exists.test.mjs": {
|
|
139
145
|
"tests": 8,
|
|
140
146
|
"asserts": 19,
|
|
@@ -153,6 +159,12 @@
|
|
|
153
159
|
"skips": 0,
|
|
154
160
|
"todos": 0
|
|
155
161
|
},
|
|
162
|
+
"a-connect-line-says-where-it-runs.test.mjs": {
|
|
163
|
+
"tests": 4,
|
|
164
|
+
"asserts": 14,
|
|
165
|
+
"skips": 0,
|
|
166
|
+
"todos": 0
|
|
167
|
+
},
|
|
156
168
|
"a-credential-file-is-written-one-way.test.mjs": {
|
|
157
169
|
"tests": 4,
|
|
158
170
|
"asserts": 13,
|
|
@@ -381,6 +393,12 @@
|
|
|
381
393
|
"skips": 0,
|
|
382
394
|
"todos": 0
|
|
383
395
|
},
|
|
396
|
+
"a-local-install-hands-its-stores-to-every-reader.test.mjs": {
|
|
397
|
+
"tests": 1,
|
|
398
|
+
"asserts": 10,
|
|
399
|
+
"skips": 0,
|
|
400
|
+
"todos": 0
|
|
401
|
+
},
|
|
384
402
|
"a-mark-name-cannot-lie-about-how-it-reads.test.mjs": {
|
|
385
403
|
"tests": 9,
|
|
386
404
|
"asserts": 37,
|
|
@@ -490,8 +508,8 @@
|
|
|
490
508
|
"todos": 0
|
|
491
509
|
},
|
|
492
510
|
"a-printed-command-runs-from-where-the-reader-stands.test.mjs": {
|
|
493
|
-
"tests":
|
|
494
|
-
"asserts":
|
|
511
|
+
"tests": 15,
|
|
512
|
+
"asserts": 62,
|
|
495
513
|
"skips": 0,
|
|
496
514
|
"todos": 0
|
|
497
515
|
},
|
|
@@ -747,6 +765,12 @@
|
|
|
747
765
|
"skips": 0,
|
|
748
766
|
"todos": 0
|
|
749
767
|
},
|
|
768
|
+
"a-store-refusal-reaches-the-person-who-can-fix-it.test.mjs": {
|
|
769
|
+
"tests": 5,
|
|
770
|
+
"asserts": 21,
|
|
771
|
+
"skips": 0,
|
|
772
|
+
"todos": 0
|
|
773
|
+
},
|
|
750
774
|
"a-stored-territory-that-does-nothing-is-named.test.mjs": {
|
|
751
775
|
"tests": 6,
|
|
752
776
|
"asserts": 12,
|
|
@@ -945,6 +969,12 @@
|
|
|
945
969
|
"skips": 0,
|
|
946
970
|
"todos": 0
|
|
947
971
|
},
|
|
972
|
+
"an-install-from-npx-moves-out-of-the-cache.test.mjs": {
|
|
973
|
+
"tests": 8,
|
|
974
|
+
"asserts": 45,
|
|
975
|
+
"skips": 4,
|
|
976
|
+
"todos": 0
|
|
977
|
+
},
|
|
948
978
|
"an-mcp-route-says-which-challenge-it-answers-with.test.mjs": {
|
|
949
979
|
"tests": 8,
|
|
950
980
|
"asserts": 25,
|
|
@@ -1804,9 +1834,9 @@
|
|
|
1804
1834
|
"todos": 0
|
|
1805
1835
|
},
|
|
1806
1836
|
"doctor-reads-the-units-not-the-shell.test.mjs": {
|
|
1807
|
-
"tests":
|
|
1808
|
-
"asserts":
|
|
1809
|
-
"skips":
|
|
1837
|
+
"tests": 33,
|
|
1838
|
+
"asserts": 96,
|
|
1839
|
+
"skips": 1,
|
|
1810
1840
|
"todos": 0
|
|
1811
1841
|
},
|
|
1812
1842
|
"doctor-refuses-what-cannot-run.test.mjs": {
|
|
@@ -2080,9 +2110,9 @@
|
|
|
2080
2110
|
"todos": 0
|
|
2081
2111
|
},
|
|
2082
2112
|
"enqueue-schema-class-inheritance.test.mjs": {
|
|
2083
|
-
"tests":
|
|
2084
|
-
"asserts":
|
|
2085
|
-
"skips":
|
|
2113
|
+
"tests": 12,
|
|
2114
|
+
"asserts": 31,
|
|
2115
|
+
"skips": 1,
|
|
2086
2116
|
"todos": 0
|
|
2087
2117
|
},
|
|
2088
2118
|
"enqueue-schema-unreadable-store.test.mjs": {
|
|
@@ -3022,8 +3052,8 @@
|
|
|
3022
3052
|
"todos": 0
|
|
3023
3053
|
},
|
|
3024
3054
|
"portal-local-login.test.mjs": {
|
|
3025
|
-
"tests":
|
|
3026
|
-
"asserts":
|
|
3055
|
+
"tests": 28,
|
|
3056
|
+
"asserts": 111,
|
|
3027
3057
|
"skips": 0,
|
|
3028
3058
|
"todos": 0
|
|
3029
3059
|
},
|
|
@@ -3058,9 +3088,9 @@
|
|
|
3058
3088
|
"todos": 0
|
|
3059
3089
|
},
|
|
3060
3090
|
"portal-service.test.mjs": {
|
|
3061
|
-
"tests":
|
|
3062
|
-
"asserts":
|
|
3063
|
-
"skips":
|
|
3091
|
+
"tests": 124,
|
|
3092
|
+
"asserts": 670,
|
|
3093
|
+
"skips": 1,
|
|
3064
3094
|
"todos": 0
|
|
3065
3095
|
},
|
|
3066
3096
|
"portal-static.test.mjs": {
|
|
@@ -4018,9 +4048,9 @@
|
|
|
4018
4048
|
"todos": 0
|
|
4019
4049
|
},
|
|
4020
4050
|
"search-policy.test.mjs": {
|
|
4021
|
-
"tests":
|
|
4022
|
-
"asserts":
|
|
4023
|
-
"skips":
|
|
4051
|
+
"tests": 35,
|
|
4052
|
+
"asserts": 180,
|
|
4053
|
+
"skips": 1,
|
|
4024
4054
|
"todos": 0
|
|
4025
4055
|
},
|
|
4026
4056
|
"seat-outputs-outside-driver-tree.test.mjs": {
|
|
@@ -4455,6 +4485,12 @@
|
|
|
4455
4485
|
"skips": 0,
|
|
4456
4486
|
"todos": 0
|
|
4457
4487
|
},
|
|
4488
|
+
"the-demo-hands-its-runs-to-the-assistant.test.mjs": {
|
|
4489
|
+
"tests": 2,
|
|
4490
|
+
"asserts": 11,
|
|
4491
|
+
"skips": 0,
|
|
4492
|
+
"todos": 0
|
|
4493
|
+
},
|
|
4458
4494
|
"the-demo-help-names-every-product-it-ships.test.mjs": {
|
|
4459
4495
|
"tests": 4,
|
|
4460
4496
|
"asserts": 8,
|
|
@@ -4480,8 +4516,8 @@
|
|
|
4480
4516
|
"todos": 0
|
|
4481
4517
|
},
|
|
4482
4518
|
"the-doors-comparison-knows-an-admit-case.test.mjs": {
|
|
4483
|
-
"tests":
|
|
4484
|
-
"asserts":
|
|
4519
|
+
"tests": 12,
|
|
4520
|
+
"asserts": 38,
|
|
4485
4521
|
"skips": 0,
|
|
4486
4522
|
"todos": 0
|
|
4487
4523
|
},
|
|
@@ -4659,6 +4695,12 @@
|
|
|
4659
4695
|
"skips": 1,
|
|
4660
4696
|
"todos": 0
|
|
4661
4697
|
},
|
|
4698
|
+
"the-open-line-says-what-to-do-when-the-page-is-not-ours.test.mjs": {
|
|
4699
|
+
"tests": 4,
|
|
4700
|
+
"asserts": 13,
|
|
4701
|
+
"skips": 0,
|
|
4702
|
+
"todos": 0
|
|
4703
|
+
},
|
|
4662
4704
|
"the-owner-of-a-promoted-filing-is-searched.test.mjs": {
|
|
4663
4705
|
"tests": 15,
|
|
4664
4706
|
"asserts": 43,
|
|
@@ -4725,6 +4767,12 @@
|
|
|
4725
4767
|
"skips": 0,
|
|
4726
4768
|
"todos": 0
|
|
4727
4769
|
},
|
|
4770
|
+
"the-proving-question-names-no-engines-model.test.mjs": {
|
|
4771
|
+
"tests": 2,
|
|
4772
|
+
"asserts": 5,
|
|
4773
|
+
"skips": 0,
|
|
4774
|
+
"todos": 0
|
|
4775
|
+
},
|
|
4728
4776
|
"the-public-residue-is-a-floor.test.mjs": {
|
|
4729
4777
|
"tests": 15,
|
|
4730
4778
|
"asserts": 59,
|
|
@@ -5403,9 +5451,9 @@
|
|
|
5403
5451
|
"todos": 0
|
|
5404
5452
|
},
|
|
5405
5453
|
"describe-options.test.mjs": {
|
|
5406
|
-
"tests":
|
|
5407
|
-
"asserts":
|
|
5408
|
-
"skips":
|
|
5454
|
+
"tests": 24,
|
|
5455
|
+
"asserts": 108,
|
|
5456
|
+
"skips": 1,
|
|
5409
5457
|
"todos": 0
|
|
5410
5458
|
},
|
|
5411
5459
|
"diff.test.mjs": {
|
|
@@ -5594,8 +5642,8 @@
|
|
|
5594
5642
|
"ext": "ts",
|
|
5595
5643
|
"perFile": {
|
|
5596
5644
|
"api.test.ts": {
|
|
5597
|
-
"tests":
|
|
5598
|
-
"asserts":
|
|
5645
|
+
"tests": 44,
|
|
5646
|
+
"asserts": 176,
|
|
5599
5647
|
"skips": 0,
|
|
5600
5648
|
"todos": 0
|
|
5601
5649
|
},
|
package/mcp-server/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# trademark-artifacts-mcp
|
|
2
2
|
|
|
3
|
+
## 0.3.0-beta.6
|
|
4
|
+
|
|
5
|
+
No changes in this release.
|
|
6
|
+
|
|
7
|
+
## 0.3.0-beta.5
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- e7cd9e1: Fixed: On an install run with `clearotron start`, a connected assistant now lists the same saved searches as the portal, including Generic's. `clearotron doctor` no longer says working saved searches are off, and it names one place profiles come from.
|
|
12
|
+
|
|
3
13
|
## 0.3.0-beta.4
|
|
4
14
|
|
|
5
15
|
No changes in this release.
|
|
@@ -18,11 +28,11 @@ No changes in this release.
|
|
|
18
28
|
|
|
19
29
|
### Patch Changes
|
|
20
30
|
|
|
21
|
-
- b8ec213: Fixed:
|
|
31
|
+
- b8ec213: Fixed: A company you create in the browser now appears in your assistant's list of companies straight away.
|
|
22
32
|
|
|
23
33
|
It used to appear only after the service restarted, although a search could already be started for it. Its projects were missing from the list in the same way, and so was its account in the search options.
|
|
24
34
|
|
|
25
|
-
The
|
|
35
|
+
The check comparing the running service with the configuration store no longer calls a company the portal's key does not cover a disagreement. It names that gap on a line of its own, with the command that widens the key.
|
|
26
36
|
|
|
27
37
|
## 0.2.4
|
|
28
38
|
|
package/mcp-server/CONNECT.md
CHANGED
|
@@ -44,14 +44,15 @@ touches no network. If that works, everything below is configuration.
|
|
|
44
44
|
One command. Substitute your clone's path and your workspace root:
|
|
45
45
|
|
|
46
46
|
```sh
|
|
47
|
-
claude mcp add trademark-artifacts \
|
|
47
|
+
claude mcp add trademark-artifacts --scope user \
|
|
48
48
|
-e CLEAROTRON_WORK_DIR=/path/to/your/workspace \
|
|
49
49
|
-- node /path/to/clearotron/mcp-server/server.mjs
|
|
50
50
|
```
|
|
51
51
|
|
|
52
52
|
Check it: `claude mcp list` prints `trademark-artifacts: … - √ Connected`.
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
`--scope user` makes it available in every project, not only the directory you ran it in. In Windows
|
|
55
|
+
PowerShell 5.1, write the separator as `"--"`: that shell drops a bare `--`. Remove it with
|
|
55
56
|
`claude mcp remove trademark-artifacts`.
|
|
56
57
|
|
|
57
58
|
## Claude Desktop
|
|
@@ -193,7 +193,10 @@ function accountFor(key, { scope, now }) {
|
|
|
193
193
|
profile = loadProfiles().get(key) ?? null;
|
|
194
194
|
if (!profile) { fresh = loadProfiles({ force: true }); profile = fresh.get(key) ?? null; }
|
|
195
195
|
} catch { return null; } // unreadable roster ⇒ say nothing
|
|
196
|
-
|
|
196
|
+
// GENERIC IS ANSWERED LIKE ANY COMPANY (owner ruling, 2026-09-11). It owns saved searches in the portal,
|
|
197
|
+
// and a fresh install holds Generic alone, so treating it as nobody's account told the assistant on
|
|
198
|
+
// every such install that none existed.
|
|
199
|
+
if (!profile) return null;
|
|
197
200
|
|
|
198
201
|
let projects = [];
|
|
199
202
|
try {
|
|
@@ -220,7 +223,7 @@ function accountFor(key, { scope, now }) {
|
|
|
220
223
|
? null
|
|
221
224
|
: "This account names no default search. A request that names none is whichever search its resolved territories make it — worldwide is a Global preliminary search, one country is a Full country search, anything else is a Multi-country focus search.",
|
|
222
225
|
},
|
|
223
|
-
...savedSearchesFor(key),
|
|
226
|
+
...savedSearchesFor(key, scope),
|
|
224
227
|
allowance: allowanceFor(profile, { scope, now }),
|
|
225
228
|
};
|
|
226
229
|
}
|
|
@@ -243,12 +246,17 @@ function accountFor(key, { scope, now }) {
|
|
|
243
246
|
* repeats it to the client as the answer. The note names no variable and no path: its reader may be a
|
|
244
247
|
* client, and the reason belongs to whoever runs the installation, whose `clearotron doctor` names it.
|
|
245
248
|
*/
|
|
246
|
-
function savedSearchesFor(key) {
|
|
249
|
+
function savedSearchesFor(key, scope) {
|
|
247
250
|
let recipes;
|
|
248
251
|
try { recipes = loadRecipes({ force: true, proseGuard: recipeProseGuard }); }
|
|
249
|
-
catch {
|
|
252
|
+
catch (e) {
|
|
253
|
+
// The store's own error, path included, goes only to a session POSITIVELY known to run the installation:
|
|
254
|
+
// an ops session, or a person with access to everything. Everyone else, a missing scope included, gets
|
|
255
|
+
// the sentence alone, because its reader may be a client.
|
|
256
|
+
const operator = scope?.kind === "ops" || scope?.everything === true;
|
|
257
|
+
const cause = operator ? ` (${String(e?.message ?? e).split("\n")[0].slice(0, 300)})` : "";
|
|
250
258
|
return { savedSearches: [], savedSearchesNote: "Saved searches could not be read on this installation, so none are "
|
|
251
|
-
+
|
|
259
|
+
+ `listed. That is not the same as having none, and the people who run the installation can see why${cause}.` };
|
|
252
260
|
}
|
|
253
261
|
return {
|
|
254
262
|
savedSearches: [...recipes.entries()]
|
package/mcp-server/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trademark-artifacts-mcp",
|
|
3
|
-
"version": "0.3.0-beta.
|
|
3
|
+
"version": "0.3.0-beta.6",
|
|
4
4
|
"license": "AGPL-3.0-only",
|
|
5
5
|
"private": true,
|
|
6
6
|
"description": "MCP server to interrogate clearotron trademark-clearance runs — list/read artifacts, trace the full decision flow, telemetry/cost, coverage, single-run search, and a gated single-step what-if. Imports the clearotron-driver read-only; touches no driver/template/deploy files.",
|
package/package.json
CHANGED
|
@@ -10309,6 +10309,12 @@ function decodeStatus(status, body) {
|
|
|
10309
10309
|
};
|
|
10310
10310
|
return { kind: "notFound" };
|
|
10311
10311
|
case 409: {
|
|
10312
|
+
const refused = refusalDetail(body);
|
|
10313
|
+
if (refused?.code.startsWith("store_")) return {
|
|
10314
|
+
kind: "reject",
|
|
10315
|
+
errors: errorsOf(body),
|
|
10316
|
+
detail: refused
|
|
10317
|
+
};
|
|
10312
10318
|
const msg = errorsOf(body)[0] ?? "This action could not be completed.";
|
|
10313
10319
|
return /version|conflict/i.test(msg) && !/confirmation|plan again|re-confirm/i.test(msg) ? {
|
|
10314
10320
|
kind: "conflict",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
-->
|
|
50
50
|
<link rel="preconnect" href="https://api.fontshare.com" crossorigin />
|
|
51
51
|
<link href="https://api.fontshare.com/v2/css?f[]=satoshi@400,500,700,900&display=swap" rel="stylesheet" />
|
|
52
|
-
<script type="module" crossorigin src="/portal/assets/index-
|
|
52
|
+
<script type="module" crossorigin src="/portal/assets/index-DBUVdQT-.js"></script>
|
|
53
53
|
<link rel="stylesheet" crossorigin href="/portal/assets/index-Cv-E_agg.css">
|
|
54
54
|
</head>
|
|
55
55
|
<body>
|
package/portal-ui/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "portal-ui",
|
|
3
3
|
"private": true,
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "0.3.0-beta.
|
|
5
|
+
"version": "0.3.0-beta.6",
|
|
6
6
|
"license": "AGPL-3.0-only",
|
|
7
7
|
"description": "The unified trademark portal UI. One address, one login: who you are decides what you see. Built as a static bundle, served by driver/portal-service.mjs — the browser never reaches profile-service or recipe-service.",
|
|
8
8
|
"engines": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trademark-oauth-mcp-bridge",
|
|
3
|
-
"version": "0.3.0-beta.
|
|
3
|
+
"version": "0.3.0-beta.6",
|
|
4
4
|
"license": "AGPL-3.0-only",
|
|
5
5
|
"private": true,
|
|
6
6
|
"description": "OAuth 2.1 MCP stdio bridge used by the engine's case-law gather stage (courtlistener / legaldatahunter).",
|
package/scripts/e2e.mjs
CHANGED
|
@@ -1057,6 +1057,57 @@ export function doorDisagreementLine(caseId, answers, { expectTerminal = null }
|
|
|
1057
1057
|
+ `and naming the accepter would blame the door that behaved. Declare \`doors\` on the case to say which doors can admit it.`;
|
|
1058
1058
|
}
|
|
1059
1059
|
|
|
1060
|
+
/**
|
|
1061
|
+
* The doors ONE case goes through: the scenario's doors, narrowed by the case's own `doors` list when it
|
|
1062
|
+
* declares one. Pure, and settled for EVERY case before anything is queued, so a mistyped declaration
|
|
1063
|
+
* refuses the round instead of quietly shrinking it.
|
|
1064
|
+
*
|
|
1065
|
+
* WHY A CASE MAY NARROW THEM. `door: "all"` exists to catch a rule one door enforces and another does not,
|
|
1066
|
+
* and that only works when every door can ASK the case's question. R0e asks what a request naming no
|
|
1067
|
+
* customer falls back to. The ops door rides an accounts-scoped key, and its gate refuses such a request
|
|
1068
|
+
* before that question is reached, correctly. So comparing that door's answer with the cli door's reported
|
|
1069
|
+
* a disagreement between two doors that both behaved. A declared list says which doors can answer; a door
|
|
1070
|
+
* left out is reduced coverage the case chose, recorded with its reason, and never a disagreement.
|
|
1071
|
+
*
|
|
1072
|
+
* Refused, each naming the case: a list that is empty or not a list of names, a name that is no door at
|
|
1073
|
+
* all, and a door this scenario does not drive. Each of those would run the case through fewer doors than
|
|
1074
|
+
* its author meant, with nothing saying so.
|
|
1075
|
+
*/
|
|
1076
|
+
export function caseDoors(scenarioDoors, kase = {}) {
|
|
1077
|
+
const declared = kase?.doors;
|
|
1078
|
+
if (declared == null) return { asked: [...scenarioDoors], notAsked: [] };
|
|
1079
|
+
const id = kase?.id ?? "(unnamed case)";
|
|
1080
|
+
if (!Array.isArray(declared) || !declared.length || declared.some((d) => typeof d !== "string" || !d))
|
|
1081
|
+
throw new Error(`${id}: \`doors\` must be a non-empty list of door names, got ${JSON.stringify(declared)}`);
|
|
1082
|
+
const unknown = declared.filter((d) => !Object.hasOwn(DOORS, d));
|
|
1083
|
+
if (unknown.length)
|
|
1084
|
+
throw new Error(`${id}: \`doors\` names ${unknown.map((d) => JSON.stringify(d)).join(", ")}, which is not a door (${Object.keys(DOORS).join(", ")})`);
|
|
1085
|
+
const outside = declared.filter((d) => !scenarioDoors.includes(d));
|
|
1086
|
+
if (outside.length)
|
|
1087
|
+
throw new Error(`${id}: \`doors\` names ${outside.join(", ")}, which this scenario does not drive (${scenarioDoors.join(", ")})`);
|
|
1088
|
+
const said = kase["why-doors"];
|
|
1089
|
+
const why = (Array.isArray(said) ? said.join(" ") : String(said ?? "")).replace(/\s+/g, " ").trim()
|
|
1090
|
+
|| "the case declares which doors can answer it";
|
|
1091
|
+
return { asked: scenarioDoors.filter((d) => declared.includes(d)), notAsked: scenarioDoors.filter((d) => !declared.includes(d)).map((door) => ({ door, why })) };
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
/**
|
|
1095
|
+
* One round's door coverage, read off its receipt: the cases whose doors disagreed, the cases that lost a
|
|
1096
|
+
* door to the transport, and the cases that left a door out by declaration. `reduced` counts a case once
|
|
1097
|
+
* however many of those it has. ONE reading for `report`, so the count on the summary line and the lines
|
|
1098
|
+
* under it cannot be two derivations of one fact.
|
|
1099
|
+
*/
|
|
1100
|
+
export function doorCoverage(rec) {
|
|
1101
|
+
const cases = rec?.cases ?? [];
|
|
1102
|
+
const dis = cases.filter((c) => !c.agreed);
|
|
1103
|
+
const lost = cases
|
|
1104
|
+
.map((c) => ({ c, out: (c.answers || []).filter((a) => (a.answerClass ?? doorAnswerClass(a)) === DOOR_ANSWER.INFRA_UNAVAILABLE) }))
|
|
1105
|
+
.filter((x) => x.out.length);
|
|
1106
|
+
const chose = cases.filter((c) => Array.isArray(c.notSubmitted) && c.notSubmitted.length);
|
|
1107
|
+
const reduced = new Set([...lost.map((x) => x.c.id), ...chose.map((c) => c.id)]).size;
|
|
1108
|
+
return { dis, lost, chose, reduced };
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1060
1111
|
const enqueue = (job, door) =>
|
|
1061
1112
|
door === "cli" ? enqueueViaCli(job)
|
|
1062
1113
|
: door === "ops-mcp" ? enqueueViaMcp(job)
|
|
@@ -1912,13 +1963,20 @@ async function cmdRun(id) {
|
|
|
1912
1963
|
// ever wants the launch to stop.
|
|
1913
1964
|
printPreviousRoundNotice(s);
|
|
1914
1965
|
|
|
1915
|
-
const jobs = s.job ? [{ id: s.id, job: s.job }] : (s.cases ?? []).map((c) => ({ id: c.id, job: c.job, what: c.what, oneMatterAcrossDoors: c.oneMatterAcrossDoors === true, expectTerminal: (c.expect ?? {}).terminal ?? null }));
|
|
1966
|
+
const jobs = s.job ? [{ id: s.id, job: s.job, kase: {} }] : (s.cases ?? []).map((c) => ({ id: c.id, job: c.job, what: c.what, oneMatterAcrossDoors: c.oneMatterAcrossDoors === true, expectTerminal: (c.expect ?? {}).terminal ?? null, kase: c }));
|
|
1916
1967
|
|
|
1917
1968
|
// door: "all" is the point of R0 — a rule enforced in one door and not another is exactly the #98
|
|
1918
1969
|
// asymmetry. Every case goes through EVERY drivable door and the answers are compared. runner.mjs's
|
|
1919
1970
|
// claimAndPrep is the wall they all land on, so they must agree; if they ever do not, the door that
|
|
1920
|
-
// admits is the bug, not the door that refuses.
|
|
1971
|
+
// admits is the bug, not the door that refuses. A case that declares `doors` goes through those alone
|
|
1972
|
+
// (caseDoors says why a case may).
|
|
1921
1973
|
const doors = s.door === "all" ? Object.entries(DOORS).filter(([, d]) => d.real).map(([k]) => k) : [s.door];
|
|
1974
|
+
// Settled for every case BEFORE the first enqueue: a declaration that cannot be honoured refuses the
|
|
1975
|
+
// round here, not after half of it has been queued.
|
|
1976
|
+
const doorsFor = new Map();
|
|
1977
|
+
for (const { id, kase } of jobs) {
|
|
1978
|
+
try { doorsFor.set(id, caseDoors(doors, kase)); } catch (e) { die(`e2e: ${s.id}: ${e.message}. Nothing was queued.`); }
|
|
1979
|
+
}
|
|
1922
1980
|
|
|
1923
1981
|
// `token` is what lets `report` — a later, separate process — read THIS round rather than every round
|
|
1924
1982
|
// that ever used these refs. Without it the second same-day round's report mixes both rounds' queue
|
|
@@ -1933,11 +1991,15 @@ async function cmdRun(id) {
|
|
|
1933
1991
|
// whether the doors are one matter or two. See refForRun and refForDoor.
|
|
1934
1992
|
const roundRef = refForRun(job.ref);
|
|
1935
1993
|
const answers = [];
|
|
1936
|
-
|
|
1994
|
+
// The scenario's `doors` still decide the ref suffix, so a case asked at one door keeps the ref it
|
|
1995
|
+
// would have had at that door in a round that asked every door.
|
|
1996
|
+
const { asked, notAsked } = doorsFor.get(caseId);
|
|
1997
|
+
for (const d of asked) {
|
|
1937
1998
|
const res = await enqueue({ ...job, ref: refForDoor(roundRef, d, { doors, oneMatterAcrossDoors }) }, d);
|
|
1938
1999
|
answers.push({ door: d, ...res });
|
|
1939
2000
|
}
|
|
1940
|
-
const { agreed, unavailable, reducedCoverage } = doorAsymmetry(answers);
|
|
2001
|
+
const { agreed, unavailable, reducedCoverage: lostADoor } = doorAsymmetry(answers);
|
|
2002
|
+
const reducedCoverage = lostADoor || notAsked.length > 0;
|
|
1941
2003
|
console.log(` ${agreed ? (answers[0].ok ? "queued " : "REFUSED") : "DISAGREE"} ${caseId}${what ? ` — ${what}` : ""}`);
|
|
1942
2004
|
// — ONE LINE PER JOB, ALWAYS. This printed one line per CASE and, when the doors agreed and
|
|
1943
2005
|
// accepted, nothing per door — so a 2-case round across 2 doors printed 2 lines while enqueuing 4
|
|
@@ -1979,7 +2041,12 @@ async function cmdRun(id) {
|
|
|
1979
2041
|
// `expect.reasonMatches`, and a case refused for the WRONG reason read exactly like one refused for
|
|
1980
2042
|
// the right one. Whitespace-collapsed and truncated because the two doors answer in different shapes
|
|
1981
2043
|
// (the CLI returns JSON, the MCP door an error string) and the receipt is a record, not a transcript.
|
|
2044
|
+
// A door the case left out by declaration, named with the case's reason. Not a disagreement and not a
|
|
2045
|
+
// lost door: the case chose it, and the round's reader still needs to know the case compared fewer doors.
|
|
2046
|
+
for (const n of notAsked)
|
|
2047
|
+
console.log(` ⓘ [${n.door}] not submitted — ${n.why}; this case has reduced door coverage by its own declaration`);
|
|
1982
2048
|
round.cases.push({ id: caseId, ref: job.ref, submittedRef: roundRef, agreed, reducedCoverage,
|
|
2049
|
+
...(notAsked.length ? { notSubmitted: notAsked } : {}),
|
|
1983
2050
|
answers: answers.map((a) => ({ door: a.door, accepted: a.ok,
|
|
1984
2051
|
// — the job id this door queued, on the RECEIPT as well as on the screen. `report` runs in
|
|
1985
2052
|
// a later process and could otherwise only re-derive the job set from refs; a watcher reading
|
|
@@ -3175,14 +3242,19 @@ async function cmdReport(id, { round: requestedToken = null } = {}) {
|
|
|
3175
3242
|
console.log(`\ndoors: NO RECEIPT at ${receiptPath(POOL_ROOT, s.id)} — cannot tell whether the doors agreed`);
|
|
3176
3243
|
toInvestigate.push(`no doors receipt — run this scenario with the current e2e.mjs to record what each door answered`);
|
|
3177
3244
|
} else {
|
|
3178
|
-
|
|
3179
|
-
//
|
|
3180
|
-
//
|
|
3181
|
-
//
|
|
3182
|
-
const unavailable = rec
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3245
|
+
// — a door lost to the transport is REDUCED COVERAGE, and it is reported as its own line; so is a
|
|
3246
|
+
// door a case left out by declaration. Older receipts carry no answerClass; `doorAnswerClass` falls
|
|
3247
|
+
// back to "answered" for them, which reproduces the previous reading rather than inventing a
|
|
3248
|
+
// retrospective one. One reading of the receipt, doorCoverage, for the count and the lines under it.
|
|
3249
|
+
const { dis, lost: unavailable, chose, reduced } = doorCoverage(rec);
|
|
3250
|
+
console.log(`\ndoors: ${rec.doors.join(", ") || "(not recorded)"} — ${rec.cases.length} case(s), ${dis.length} disagreement(s), ${reduced} case(s) with reduced door coverage`);
|
|
3251
|
+
for (const c of chose) {
|
|
3252
|
+
const who = c.notSubmitted.map((n) => `${n.door} (${n.why})`).join(", ");
|
|
3253
|
+
console.log(` ⓘ ${c.id}: not submitted to ${who}`);
|
|
3254
|
+
// NOT toInvestigate, for the same reason as a lost door below: the case chose to compare fewer
|
|
3255
|
+
// doors, and that is a limit on what this round proved, not a finding about the product.
|
|
3256
|
+
notProbed.push(`${c.id}: not submitted to ${who} — the case declares which doors can answer it, so its door agreement covers fewer doors than the scenario drives`);
|
|
3257
|
+
}
|
|
3186
3258
|
for (const c of dis) {
|
|
3187
3259
|
// — the RECEIPT does not carry the case's contract, so it is joined back to the scenario by
|
|
3188
3260
|
// id. A receipt written before this field existed reads null and gets the old sentence, which is
|
package/scripts/engine-probe.mjs
CHANGED
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
// env at module top — and every static import evaluates before this file's own body runs, so applying the
|
|
36
36
|
// aliases in the body would be too late for the capture it is meant to fix.
|
|
37
37
|
import "../shared/env-local.mjs";
|
|
38
|
-
import { probeEngineTurn, probeFailureText,
|
|
38
|
+
import { probeEngineTurn, probeFailureText, PROBE_TIMEOUT_SEC } from "../driver/engine/probe.mjs";
|
|
39
39
|
import { isEntrypoint } from "../shared/is-entrypoint.mjs"; // — realpath both sides, or a symlinked invocation exits 0 silently
|
|
40
40
|
|
|
41
41
|
const argv = process.argv.slice(2);
|
|
@@ -62,7 +62,7 @@ if (isEntrypoint(import.meta.url)) {
|
|
|
62
62
|
const env = engine ? { ...process.env, CLEAROTRON_AI: engine } : process.env;
|
|
63
63
|
const asJson = argv.includes("--json");
|
|
64
64
|
if (!asJson) {
|
|
65
|
-
console.error(`engine-probe: one
|
|
65
|
+
console.error(`engine-probe: one turn on the engine's cheapest model, up to ${PROBE_TIMEOUT_SEC}s, on this box's own `
|
|
66
66
|
+ `credential — real, and it costs whatever one cheap turn costs.`);
|
|
67
67
|
}
|
|
68
68
|
const verdict = await probeEngineTurn({ env });
|
package/scripts/env-classify.mjs
CHANGED
|
@@ -346,6 +346,9 @@ export const DEPLOYMENT_NAMES = new Set([
|
|
|
346
346
|
// by nothing else, so moving it to the house prefix dropped it through to `tuning` — the bucket step 3
|
|
347
347
|
// deletes from. The audience did not change; only the spelling did, and this list is what says so.
|
|
348
348
|
"CLEAROTRON_DEMO",
|
|
349
|
+
// Set by the install on the copy it starts after moving out of npx's cache, never by an operator. Its
|
|
350
|
+
// row declares `deployment`, and no shape matches it, so unlisted it would fall through to `tuning`.
|
|
351
|
+
"CLEAROTRON_RELOCATED",
|
|
349
352
|
// Three names read only through optional chaining, so no catalogue check saw them until the scanner
|
|
350
353
|
// learned `?.`. Each row declares `deployment`, and no shape matches them (`_PORT$` does not take
|
|
351
354
|
// `_PORTS`), so unlisted each fell through to `tuning`, the bucket step 3 deletes from. The first is set
|
|
@@ -289,6 +289,10 @@ export const leadRouteFor = (id) => {
|
|
|
289
289
|
* @param {{ stdioRoutes?: object, publicAddress?: string|null, operator?: string|null }} have
|
|
290
290
|
* @param {"disk"|"public-http"} [route] defaults to the row's `lead`
|
|
291
291
|
*/
|
|
292
|
+
/** The first step of every "on this computer" route when the install runs under WSL. */
|
|
293
|
+
export const WSL_STEP = "Do this inside your WSL terminal, the Linux one where Clearotron is installed, not in PowerShell. "
|
|
294
|
+
+ "An assistant running on Windows itself cannot start it from there.";
|
|
295
|
+
|
|
292
296
|
export function whatItNeeds(client, have = {}, route = client?.lead) {
|
|
293
297
|
if (!client) return null;
|
|
294
298
|
const author = client.routes?.[route];
|
|
@@ -320,6 +324,10 @@ export function whatItNeeds(client, have = {}, route = client?.lead) {
|
|
|
320
324
|
reason: "this copy of the software is incomplete, so there is nothing to hand your assistant",
|
|
321
325
|
fix: "whoever installed it will need to install it again" };
|
|
322
326
|
}
|
|
327
|
+
// ON WSL, "THIS COMPUTER" IS THE LINUX INSIDE WINDOWS. The line names WSL paths, so it runs in the WSL
|
|
328
|
+
// terminal where Clearotron is installed. Pasted into PowerShell it fails, and an assistant running on
|
|
329
|
+
// Windows itself could not start a program at a Linux path anyway.
|
|
330
|
+
if (have.wsl) steps.unshift({ text: WSL_STEP });
|
|
323
331
|
const first = steps.find((s) => s.copy)?.copy;
|
|
324
332
|
// `command` and `stdio` ride for the terminal, which prints a disk offer's copy by its shape.
|
|
325
333
|
return { client, served: true, route, steps, launch: client.launch ?? null, enables: null, ...evidence,
|
|
@@ -142,6 +142,7 @@ export const NAMES_IN_FORCE = Object.freeze([
|
|
|
142
142
|
"CLEAROTRON_REGISTER_PLAN",
|
|
143
143
|
"CLEAROTRON_REGISTER_RECORD_LOG",
|
|
144
144
|
"CLEAROTRON_RELEASE_WAIT_MS",
|
|
145
|
+
"CLEAROTRON_RELOCATED",
|
|
145
146
|
"CLEAROTRON_REOPEN_MAX_FETCH",
|
|
146
147
|
"CLEAROTRON_REPLAY_ROOTS",
|
|
147
148
|
"CLEAROTRON_REPLAY_SNAPSHOT",
|