clearotron 0.3.1-beta.0 → 0.3.1-beta.2

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.
Files changed (73) hide show
  1. package/.env.example +17 -0
  2. package/INSTALL.md +1 -1
  3. package/README.md +6 -1
  4. package/THIRD-PARTY-NOTICES.md +4 -4
  5. package/bin/connect.mjs +2 -2
  6. package/bin/onboard.mjs +96 -11
  7. package/bin/start.mjs +129 -23
  8. package/build-info.json +2 -2
  9. package/docs/architecture/05-config-governance.md +14 -0
  10. package/driver/CHANGELOG.md +52 -0
  11. package/driver/citation-census.json +2 -1
  12. package/driver/common-law-receipts.mjs +78 -1
  13. package/driver/contract-vocabulary.mjs +6 -6
  14. package/driver/engine/anthropic-agent.mjs +75 -0
  15. package/driver/form-neighbourhood.mjs +44 -2
  16. package/driver/matter-frame-record.mjs +68 -1
  17. package/driver/named-band.mjs +1 -1
  18. package/driver/package.json +1 -1
  19. package/driver/pipeline-knockout.mjs +13 -6
  20. package/driver/pipeline.mjs +48 -34
  21. package/driver/portal-config-view.mjs +6 -0
  22. package/driver/portal-mcp-client.mjs +54 -9
  23. package/driver/portal-service.mjs +69 -6
  24. package/driver/profile-page.html +34 -4
  25. package/driver/profiles.mjs +5 -1
  26. package/driver/register-availability.mjs +2 -2
  27. package/driver/register-plan.mjs +22 -0
  28. package/driver/scope-ledger.mjs +33 -0
  29. package/driver/skills/prelim-common-law/SKILL.md +3 -1
  30. package/driver/skills/prelim-search/synthesis-rules.md +3 -1
  31. package/driver/stages.mjs +10 -1
  32. package/driver/suite-census.json +82 -58
  33. package/mcp-server/CHANGELOG.md +16 -0
  34. package/mcp-server/http-server.mjs +27 -4
  35. package/mcp-server/lib/cf-access.mjs +16 -2
  36. package/mcp-server/lib/http-handler.mjs +37 -1
  37. package/mcp-server/lib/ops.mjs +22 -9
  38. package/mcp-server/lib/options.mjs +1 -1
  39. package/mcp-server/lib/plan.mjs +47 -2
  40. package/mcp-server/package.json +1 -1
  41. package/mcp-server/server.mjs +29 -4
  42. package/package.json +1 -1
  43. package/portal-ui/dist/assets/{index-Cv-E_agg.css → index-D5WAoLZI.css} +20 -6
  44. package/portal-ui/dist/assets/{index-CwPAS0we.js → index-D8ITW-aD.js} +2151 -744
  45. package/portal-ui/dist/index.html +2 -2
  46. package/portal-ui/package.json +6 -6
  47. package/providers/_shared/term-shape.mjs +6 -0
  48. package/providers/_shared/territory-codes.mjs +43 -0
  49. package/providers/clarivate/src/capabilities.js +13 -1
  50. package/providers/clarivate/src/core.js +21 -1
  51. package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
  52. package/providers/oauth-mcp-bridge/package.json +1 -1
  53. package/scripts/citation-line-check.mjs +67 -21
  54. package/scripts/e2e.mjs +4 -2
  55. package/scripts/import-cycle-check.mjs +34 -3
  56. package/scripts/live-surface-check.mjs +2 -2
  57. package/scripts/mint-names-in-force.mjs +21 -2
  58. package/scripts/purge-runs.mjs +114 -3
  59. package/scripts/revisit-render-check.mjs +77 -44
  60. package/scripts/test-run.mjs +7 -0
  61. package/scripts/travelling-predicates.mjs +1 -1
  62. package/shared/connect-clients.mjs +95 -16
  63. package/shared/invocation.mjs +25 -0
  64. package/shared/mcp-challenge.mjs +32 -0
  65. package/shared/names-in-force.mjs +2 -0
  66. package/shared/notice-owed.mjs +62 -0
  67. package/shared/scope.mjs +66 -5
  68. package/shared/stdio-connect.mjs +92 -24
  69. package/shared/trigger-lane.mjs +19 -0
  70. package/shared/wsl.mjs +15 -0
  71. package/skills/clearotron-account/SKILL.md +27 -0
  72. package/skills/clearotron-ops/COURIER.md +8 -1
  73. package/skills/clearotron-ops/SKILL.md +18 -2
package/.env.example CHANGED
@@ -480,6 +480,23 @@ CLEAROTRON_CUT_REF=
480
480
  # effect: tuning
481
481
  CLEAROTRON_RELEASE_WAIT_MS=
482
482
 
483
+ # The trickle floor: the fewest output tokens per second of ACTIVE time (elapsed minus tool wait) a
484
+ # model turn may produce before it is stopped as a stall. Unset uses 1. A stage streaming a token every
485
+ # few seconds holds off both other clocks — the stall clock resets on any byte, and the no-progress
486
+ # ceiling counts token movement as progress deliberately — so before this the only thing that stopped
487
+ # one was its wall, which throws the whole attempt away. Set 0 to disable the floor; that is read as "no
488
+ # floor", never as a floor of zero. A negative or unreadable value falls back to the default rather than
489
+ # switching the instrument off. Read by driver/engine/anthropic-agent.mjs.
490
+ # effect: tuning
491
+ CLEAROTRON_MIN_TOKENS_PER_SEC=
492
+
493
+ # How much ACTIVE time a turn gets before the floor above may fire at all, in whole milliseconds. Unset
494
+ # uses 300000 (five minutes). It exists because a rate measured over a few seconds is noise, and a turn
495
+ # that thinks before it writes would fail an immediate floor while working perfectly. A non-positive or
496
+ # unreadable value falls back to the default. Read by driver/engine/anthropic-agent.mjs.
497
+ # effect: tuning
498
+ CLEAROTRON_MIN_TOKENS_WARMUP_MS=
499
+
483
500
  # ── The local key door's socket ───────────────────────────────
484
501
  # Same shape as the two below: read by product code, in neither this file nor the governance table
485
502
  # until a private environment-contract ratchet caught it.
package/INSTALL.md CHANGED
@@ -1326,7 +1326,7 @@ answer on any of them:
1326
1326
  by that vendor's terms. AGPL-3.0 grants you nothing over it, and this repository redistributes no
1327
1327
  part of it.
1328
1328
  - **Register and research providers are your own agreements.** EUIPO, the USPTO bulk product,
1329
- `PERPLEXITY_API_KEY`, CourtListener, and the subscription registers (Corsearch, Clarivate, Signa)
1329
+ `PERPLEXITY_API_KEY`, CourtListener, and the subscription registers (Clarivate, Signa, Corsearch)
1330
1330
  each sit on terms you accept directly with that provider. The adapters in `providers/` are ours and
1331
1331
  are licensed with the rest of the code; what they connect to is not.
1332
1332
  - **npm dependencies carry their own licences.** `package-lock.json` is the resolved set, and each
package/README.md CHANGED
@@ -29,6 +29,11 @@ That fetches the published package — it will ask once before downloading — t
29
29
  clearances into a local portal and prints the portal's address and the passphrase to sign in with. Open
30
30
  the address in your browser. No account, no credentials, no network calls to us.
31
31
 
32
+ The demo runs for as long as that window stays open, and removes everything it made when you close it —
33
+ nothing of it is left on the machine, and running it again later starts clean. If you want to keep the
34
+ sample reports after closing the window, run `npx clearotron demo --keep`; it prints the one command
35
+ that removes the folder when you are done with it.
36
+
32
37
  **Then install it.**
33
38
 
34
39
  ```bash
@@ -102,7 +107,7 @@ clearotron run --job my-job.json
102
107
  ## How it fits together
103
108
 
104
109
  - **A reasoning CLI does the thinking.** Every stage runs as a headless turn of the [Claude CLI](https://claude.com/claude-code) (`claude`) or the Codex CLI (`codex`), which must be installed. `CLEAROTRON_AI_BILLING` chooses what pays for the turn: your signed-in subscription, or an API key. Either way the CLI is what runs — there is no path that calls the model directly.
105
- - **One register credential sets coverage and cost.** `CLEAROTRON_DATABASE` has no default — a run refuses rather than picking a vendor for you. EUIPO and a local USPTO index cost nothing; Signa, Clarivate and Corsearch are subscriptions. [The six, and what each reaches](providers/README.md).
110
+ - **One register credential sets coverage and cost.** `CLEAROTRON_DATABASE` has no default — a run refuses rather than picking a vendor for you. EUIPO and a local USPTO index cost nothing; Clarivate, Signa and Corsearch are subscriptions. [The six, and what each reaches](providers/README.md).
106
111
  - **One research key.** `PERPLEXITY_API_KEY` covers the open web and the marketplaces. A clearance refuses without it at the door, before a register stage has spent.
107
112
  - **A run takes hours, and survives interruption.** Every finished stage stays on disk; a resume re-runs only what is missing, and a run parked on a provider cap continues by itself.
108
113
  - **A finished run is queryable.** An MCP server lets Claude, ChatGPT or your editor read and question
@@ -5028,7 +5028,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
5028
5028
  THE SOFTWARE.
5029
5029
  ```
5030
5030
 
5031
- ## react@19.2.8
5031
+ ## react@19.3.0
5032
5032
 
5033
5033
  - **Licence declared:** `MIT`
5034
5034
  - **Repository:** https://github.com/react/react
@@ -5058,7 +5058,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
5058
5058
  SOFTWARE.
5059
5059
  ```
5060
5060
 
5061
- ## react-dom@19.2.8
5061
+ ## react-dom@19.3.0
5062
5062
 
5063
5063
  - **Licence declared:** `MIT`
5064
5064
  - **Repository:** https://github.com/react/react
@@ -5592,10 +5592,10 @@ SOFTWARE.
5592
5592
  - **Repository:** https://github.com/lddubeau/saxes
5593
5593
  - **Licence file:** none shipped
5594
5594
 
5595
- ## scheduler@0.27.0
5595
+ ## scheduler@0.28.0
5596
5596
 
5597
5597
  - **Licence declared:** `MIT`
5598
- - **Repository:** https://github.com/facebook/react
5598
+ - **Repository:** https://github.com/react/react
5599
5599
  - **Licence file:** `LICENSE`
5600
5600
 
5601
5601
  ```
package/bin/connect.mjs CHANGED
@@ -47,7 +47,7 @@ import { execFileSync } from "node:child_process";
47
47
  import { createServer } from "node:net";
48
48
  import { CONNECT_CLIENTS, WHERE_FLAG, clientById, leadRouteFor, plainStep, whatItNeeds } from "../shared/connect-clients.mjs";
49
49
  import { stdioConnectFor, STDIO_SHAPES } from "../shared/stdio-connect.mjs";
50
- import { isWsl } from "../shared/wsl.mjs";
50
+ import { isWsl, wslTarget } from "../shared/wsl.mjs"; // — and which distribution a row should start the server in
51
51
  import { defaultDenylistPath, clientDoorAddress, clientDoorPort, clientDoorState, enablePlan, applyEnablePlan, describeChange, recordConnectKey, CLIENT_DOOR_UNIT } from "../shared/client-door.mjs";
52
52
  import { mintToken, tokenId, resolvePerson, loadGrants } from "../shared/scope.mjs";
53
53
  import { envFrom } from "../shared/env-aliases.mjs";
@@ -206,7 +206,7 @@ function deploymentHas(env = process.env) {
206
206
  return {
207
207
  // EVERY SHAPE, RESOLVED ONCE. A row picks its own; nothing here knows a client's name.
208
208
  stdioRoutes: Object.fromEntries(Object.keys(STDIO_SHAPES).map((shape) =>
209
- [shape, stdioConnectFor(shape, { workDir: env.CLEAROTRON_WORK_DIR || null, reportsDir: env.CLEAROTRON_REPORTS_DIR || null })])),
209
+ [shape, stdioConnectFor(shape, { workDir: env.CLEAROTRON_WORK_DIR || null, reportsDir: env.CLEAROTRON_REPORTS_DIR || null, wsl: wslTarget({ env }) })])),
210
210
  wsl: isWsl({ env }),
211
211
  // WHERE THE DOOR BINDS — not an address handed to any assistant. It is the loopback address the
212
212
  // unit listens on, and `enablePlan` needs it to write the unit. It used to be passed to the
package/bin/onboard.mjs CHANGED
@@ -66,6 +66,12 @@ import { homedir, userInfo } from "node:os";
66
66
  import { invocationPrefix, installRoute, reachableCommand } from "../shared/invocation.mjs"; // — one rule for how the reader invokes us
67
67
  import { nodeFloorVerdict } from "../shared/node-floor.mjs"; // — the floor is package.json engines, not a constant here
68
68
  import { invocationForm } from "../shared/invocation.mjs"; // — and WHY that form
69
+ // One sentence about a page that is not ours, from the module that owns how a reader invokes us — NOT
70
+ // from bin/start.mjs, which is where it used to live. A command importing a command closes a loop that
71
+ // hangs at run time rather than failing at build: this file has a top-level await, so start.mjs asking
72
+ // for it back mid-evaluation never resolves and the wizard installs nothing. The import-cycle guard
73
+ // names that pair; the sentence now lives where both commands can read it.
74
+ import { foreignPageHint } from "../shared/invocation.mjs";
69
75
  import { standFrom } from "../shared/invocation.mjs"; // is this tree one npm replaces?
70
76
  import { installShim } from "../shared/verb-shim.mjs"; // — the verb goes on PATH
71
77
  import { relocationPlan } from "../shared/permanent-install.mjs"; // — and the program out of npx's cache
@@ -118,6 +124,7 @@ import { usageBlock } from "../shared/usage-block.mjs";
118
124
  import { invoke } from "../shared/invocation.mjs"; // — name a command the reader can actually type
119
125
  import { parseEnvFile } from "../shared/env-file-merge.mjs"; // — ONE KEY=value reader, taken from a leaf: the unit renderer re-exports this same function, and it is a COMMAND, so importing it from here would put a command in this binary's graph
120
126
  import { unitEnvironment, unitValue, couldNotDetermine } from "../driver/unit-environment.mjs"; // — F34: claim about the UNITS only from the units' own environment
127
+ import { DEPLOYMENT_BOXES } from "../shared/deployment-box.mjs"; // the allowlist, imported — never a second copy that drifts
121
128
 
122
129
  /**
123
130
  * A file's text, or null when it is not there or cannot be read.
@@ -1051,6 +1058,29 @@ export async function runCheck() {
1051
1058
  say("\n Trademark clearance engine — configuration check\n");
1052
1059
  say(` repo: ${REPO}`);
1053
1060
 
1061
+ // ── ONE READING OF THE UNITS' ENVIRONMENT, FOR EVERY SECTION THAT CLAIMS ANYTHING ABOUT THEM ─────
1062
+ //
1063
+ // F34: two sections asserted facts about the units while reading the operator's shell, and both of
1064
+ // their reported problems were false on a correctly-running install. Resolving this once is
1065
+ // deliberate — a second reader would drift from this one exactly as the composer and the checker did
1066
+ // in F41, and the drift is invisible because both sides keep passing their own arms.
1067
+ //
1068
+ // HOISTED TO THE TOP OF runCheck, from just above the Engine section, so that the FIRST section —
1069
+ // Deployment — can name the box from the file the units load. A box line sourced from this command's
1070
+ // own shell would be the defect it is there to catch: the shell and the units' file can disagree,
1071
+ // and it is the units' value that decides what the running service thinks it is.
1072
+ const fileEnv = readEnvFile(READ_ENV_PATH());
1073
+ const unitDir = join(homedir(), ".config", "systemd", "user");
1074
+ const { BACKGROUND_UNITS, startPaths } = await import(pathToFileURL(join(REPO, "bin", "start.mjs")).href);
1075
+ const hosted = BACKGROUND_UNITS.some((u) => existsSync(join(unitDir, u)));
1076
+ const unitEnv = hosted
1077
+ ? unitEnvironment({
1078
+ units: BACKGROUND_UNITS.map((u) => ({ name: u, text: readIfPresent(join(unitDir, u)) })),
1079
+ readEnvFile: readIfPresent,
1080
+ // These are USER units under ~/.config/systemd/user, so systemd's %h is this home.
1081
+ home: homedir() })
1082
+ : null;
1083
+
1054
1084
  // — CURRENT, not just configured. Printed before anything else because a stale deployment
1055
1085
  // makes every line below it a report about the wrong build.
1056
1086
  say("\n Deployment");
@@ -1080,6 +1110,56 @@ export async function runCheck() {
1080
1110
  info("this does not fetch — the count is against the last fetch, not against the remote right now");
1081
1111
  }
1082
1112
 
1113
+ // ── WHICH BOX THIS IS, NAMED OUT LOUD ───────────────────────────────────────────────────────────
1114
+ //
1115
+ // `CLEAROTRON_BOX` is how a deployment names itself; `shared/deployment-box.mjs` says it is never
1116
+ // inferred from the account, the checkout or the port, because all three correlate with the answer
1117
+ // and none of them IS the answer. But nothing here printed it, so an unset or misspelled value left
1118
+ // doctor exiting 0 and ending "Nothing wrong with what is configured" — the one check that would
1119
+ // catch a deployment lying about itself was the one check that did not report.
1120
+ //
1121
+ // FOUR STATES, NOT THREE. Set, unset and unrecognised are the three the requirement names. The
1122
+ // fourth is a hosted box whose unit environment COULD NOT BE READ, and calling that "unset" would
1123
+ // claim the file says nothing when it was never read — absence reported as a finding, in the section
1124
+ // whose entire job is to make the box visible. `couldNotDetermine` already exists for exactly this
1125
+ // and is what the token and URL checks below use.
1126
+ //
1127
+ // THE VALUE COMES FROM THE FILE THE UNITS LOAD, never from this command's shell, and the two can
1128
+ // disagree: an operator with CLEAROTRON_BOX exported in their own environment would otherwise read
1129
+ // their own shell back and call it the deployment's identity. The shell is reported only when it
1130
+ // CONTRADICTS the file, because that disagreement is itself a finding.
1131
+ //
1132
+ // The allowlist is imported rather than re-typed. Two inline copies agree on the day they are
1133
+ // written and drift afterwards, which is the reason that module exists at all.
1134
+ const boxSource = hosted ? "the units' environment" : "your environment file";
1135
+ const boxFileEnv = hosted ? (unitEnv?.known === true ? unitEnv.env : null) : fileEnv;
1136
+ if (hosted && unitEnv?.known !== true) {
1137
+ warn(couldNotDetermine("CLEAROTRON_BOX", unitEnv));
1138
+ } else {
1139
+ const raw = (boxFileEnv?.CLEAROTRON_BOX ?? "").trim();
1140
+ if (!raw) {
1141
+ // AN ABSENCE IS NOT A MISCONFIGURATION, and this command already separates the two — a machine
1142
+ // nobody has configured yet has no box, legitimately, and `--check` on one exits 0 by contract.
1143
+ // Reported at `!` so it is visible rather than silent, which is the whole requirement; raising it
1144
+ // to `✗` made every unconfigured machine fail its own acceptance arm, and said "fault" about a
1145
+ // state that is the normal beginning of an install.
1146
+ warn(`no CLEAROTRON_BOX in ${boxSource} — this deployment does not name itself, so nothing below `
1147
+ + `can be read as a statement about a particular box. Set it to one of: ${DEPLOYMENT_BOXES.join(", ")}`);
1148
+ } else if (!DEPLOYMENT_BOXES.includes(raw)) {
1149
+ problem(`CLEAROTRON_BOX is "${raw}" in ${boxSource}, which is not a box this build knows `
1150
+ + `(${DEPLOYMENT_BOXES.join(", ")}). An unrecognised name reads as NO box everywhere it is `
1151
+ + "consulted, which is quieter than a wrong one and just as wrong");
1152
+ } else {
1153
+ ok(`this box names itself "${raw}", from ${boxSource}`);
1154
+ const shell = (process.env.CLEAROTRON_BOX ?? "").trim();
1155
+ if (shell && shell !== raw) {
1156
+ warn(`your shell says CLEAROTRON_BOX="${shell}", which disagrees with ${boxSource}. The running `
1157
+ + "service goes by the file; your own commands go by the shell, so the two would report "
1158
+ + "different boxes for the same machine");
1159
+ }
1160
+ }
1161
+ }
1162
+
1083
1163
  // ── THE TWO FILES MAY DISAGREE ABOUT THE DOOR ────────────────────────────────
1084
1164
  //
1085
1165
  // They are disjoint by design and are SUPPOSED to differ about paths, ports and secrets. What must
@@ -1163,7 +1243,10 @@ export async function runCheck() {
1163
1243
  // Read the file up here rather than at the `.env` heading below: the engine section is the first that
1164
1244
  // needs `effective()`, and which ENGINE is configured decides which binary variable to check. Reading
1165
1245
  // is not applying — see readEnvFile's header.
1166
- const fileEnv = readEnvFile(READ_ENV_PATH());
1246
+ // `fileEnv` is read at the top of runCheck now — the Deployment section needs it to name the box on a
1247
+ // box that runs no units, and a `const` used above its declaration is a dead-zone crash, not a
1248
+ // fallback. It reached here as one on the first draft of that section: harmless on a hosted box,
1249
+ // where the ternary never evaluates this branch, and fatal on every install without units.
1167
1250
  // Environment wins over the file (the loader contract), so report the effective value and say which
1168
1251
  // source it came from — a value read from the wrong place is the whole class of bug here.
1169
1252
  // ── CHECK 1 — THE CHECKER READS EVERY SPELLING THE ENGINE ACCEPTS ─────────────────────────
@@ -1210,16 +1293,9 @@ export async function runCheck() {
1210
1293
  // their reported problems were false on a correctly-running install. Resolving this once, here, is
1211
1294
  // deliberate — a second reader would drift from this one exactly as the composer and the checker did
1212
1295
  // in F41, and the drift is invisible because both sides keep passing their own arms.
1213
- const unitDir = join(homedir(), ".config", "systemd", "user");
1214
- const { BACKGROUND_UNITS, startPaths } = await import(pathToFileURL(join(REPO, "bin", "start.mjs")).href);
1215
- const hosted = BACKGROUND_UNITS.some((u) => existsSync(join(unitDir, u)));
1216
- const unitEnv = hosted
1217
- ? unitEnvironment({
1218
- units: BACKGROUND_UNITS.map((u) => ({ name: u, text: readIfPresent(join(unitDir, u)) })),
1219
- readEnvFile: readIfPresent,
1220
- // These are USER units under ~/.config/systemd/user, so systemd's %h is this home.
1221
- home: homedir() })
1222
- : null;
1296
+ // The reading itself was hoisted AGAIN, to the top of runCheck, so the Deployment section can name
1297
+ // the box from the file the units load. `unitDir`, `hosted`, `unitEnv` and `startPaths` are all in
1298
+ // scope from there; nothing between here and there consumed them, which is why the move is safe.
1223
1299
 
1224
1300
  // ── AND THE DOOR SECTION IS THE THIRD SUCH SECTION ──────────────────────────────────────────────
1225
1301
  //
@@ -3987,6 +4063,15 @@ try {
3987
4063
  say(` ${invocationPrefix()}clearotron start\n`);
3988
4064
  say(" Starts the portal and the engine door and prints one address to open in your browser. That");
3989
4065
  say(" address is the product: you order a clearance from it and read the report there.\n");
4066
+ // THE ONE SENTENCE A READER NEEDS BEFORE THEY OPEN THAT ADDRESS, and setup was the only one of the
4067
+ // three commands that never said it. A port can be free where the product runs and taken where the
4068
+ // BROWSER runs — a forwarding listener outside this environment answers 127.0.0.1 first — and the
4069
+ // page that opens is then somebody else's, with nothing on screen saying so. It is printed here
4070
+ // rather than beside an address because setup starts nothing and has no address to print: the reader
4071
+ // meets it moments before the command that does. Composed by the same function `start` and `demo`
4072
+ // use, with no number, so the three commands cannot drift into three answers.
4073
+ for (const line of foreignPageHint("start")) say(` ${line}`);
4074
+ say("");
3990
4075
  say(` ${style.dim(`Also: \`${invocationPrefix()}clearotron demo\` replays a finished report with no keys and no model calls;`)}`);
3991
4076
  say(` ${style.dim(`\`${invocationPrefix()}clearotron run --job ${/\s/.test(EXAMPLE_JOB) ? `"${EXAMPLE_JOB}"` : EXAMPLE_JOB}\` runs a first real clearance on the EU register.`)}`);
3992
4077
  // THE OLD WAY IS A CHECKOUT'S. A package has no npm scripts where its reader stands.
package/bin/start.mjs CHANGED
@@ -121,9 +121,13 @@ import { SERVER_INSTALL_SET, unitsToRestartOnRefresh, unitHealthVerdict } from "
121
121
  import { defaultDenylistPath, denylistPathFor, denylistFor, ensureDenylistFile, CLIENT_DOOR_UNIT, enablePlan, clientDoorPort, demoTokenSecret, demoTokenSecretPath, keyIssueCommand } from "../shared/client-door.mjs"; // — one owner for the revocation list's path
122
122
  import { createServer } from "node:net";
123
123
  import { listenErrorMessage, nextFreePort } from "../shared/listen.mjs";
124
- import { chmodSync, copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
124
+ import { chmodSync, copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
125
125
  import { randomBytes } from "node:crypto";
126
- import { invocationPrefix, invoke, reachableCommand } from "../shared/invocation.mjs"; // — the banner names the verb
126
+ import { invocationPrefix, invoke, reachableCommand, foreignPageHint } from "../shared/invocation.mjs"; // — the banner names the verb
127
+ // RE-EXPORTED, NOT RE-DECLARED. The sentence moved to a module that imports nothing of ours, because
128
+ // setup imports it and setup has a top-level await: a command importing a command closes a loop that
129
+ // hangs at run time rather than failing at build. Its readers keep this spelling.
130
+ export { foreignPageHint };
127
131
  import { unitEnvPath, activeEnvPath } from "../shared/env-local.mjs"; // — the file the units read, named once
128
132
  import { parseEnvFile } from "../shared/env-file-merge.mjs"; // ONE KEY=value reader, in a leaf: render-units is a COMMAND, and importing it from here closed a cycle
129
133
  import { homedir, userInfo } from "node:os";
@@ -303,21 +307,6 @@ export function resolvePorts(env = {}) {
303
307
  client: one("CLIENT_MCP_HTTP_PORT", clientDoorPort({})) };
304
308
  }
305
309
 
306
- /**
307
- * WHAT TO DO WHEN THE PAGE THAT OPENS IS NOT OURS. Printed under every "Open" line.
308
- *
309
- * A port can be free where this runs and taken where the browser runs: on WSL, a Windows-side listener
310
- * (VS Code's Remote-SSH forwarding is the one measured, 2026-09-11) answers 127.0.0.1 before WSL does. The
311
- * doors bind cleanly, the in-use detection has nothing to see, and the browser shows somebody else's page
312
- * with nothing on this screen saying so. `--port` already moves all three doors; the reader has to be told
313
- * about it at the moment the address is handed over, which is here.
314
- */
315
- export function foreignPageHint(verb) {
316
- return [
317
- "If the page that opens is not this install's sign-in, another program on this machine holds that",
318
- `port from outside this environment. Run \`${invoke(verb)} --port 28802\` (or any free number) instead.`,
319
- ];
320
- }
321
310
 
322
311
  /**
323
312
  * Apply `--port <n>` to the three doors.
@@ -349,6 +338,37 @@ export function portsForFlag(portFlag, ports, env = {}) {
349
338
  };
350
339
  }
351
340
 
341
+ /**
342
+ * THE DEMO'S OWN DOORS — the numbers it opens when the reader has asked for none.
343
+ *
344
+ * A demo used to start on the install's default portal port. On WSL that put the browser's address in
345
+ * the hands of whatever holds that number on the WINDOWS side: a Remote-SSH forward answered 127.0.0.1
346
+ * first, and the reader opened a production portal's "not signed in" page believing it was the demo.
347
+ * A local free-port walk cannot help there — the port IS free inside WSL, which is why the bind
348
+ * succeeded — so the fix is not to look harder but to stop using the number anybody else would.
349
+ *
350
+ * 18860 has no meaning: it is unusual, it is clear of the three an install opens by default (18802,
351
+ * 18790, 18811) and of the range a moved door walks into, and a stranger's machine is unlikely to be
352
+ * running something there. The two doors that follow take the next two numbers, which is the same
353
+ * arithmetic `--port` already uses, so a reader who moves the demo and a reader who does not meet one
354
+ * layout rather than two.
355
+ *
356
+ * AN EXPLICIT VARIABLE STILL WINS, exactly as it does for the flag: somebody who set a port chose that
357
+ * number, and a demo is a convenience over the defaults rather than an override of a decision. So is
358
+ * `--port`, which is applied after this and replaces it. PURE.
359
+ */
360
+ export const DEMO_PORT_BASE = 18860;
361
+
362
+ export function demoPortDefaults(ports, env = {}) {
363
+ const explicit = (name) => String(env[name] ?? "").trim() !== "";
364
+ return {
365
+ ...ports,
366
+ portal: explicit("PORTAL_SERVICE_PORT") ? ports.portal : DEMO_PORT_BASE,
367
+ mcp: explicit("TRADEMARK_MCP_HTTP_PORT") ? ports.mcp : DEMO_PORT_BASE + 1,
368
+ client: explicit("CLIENT_MCP_HTTP_PORT") ? ports.client : DEMO_PORT_BASE + 2,
369
+ };
370
+ }
371
+
352
372
  /**
353
373
  * Where the grants file is, for a command that is NOT the supervisor.
354
374
  *
@@ -911,6 +931,14 @@ if (isMain) {
911
931
  // Decided before any path is, because in a demo every path below is the demo's own. The posture
912
932
  // itself is described at the DEMO block further down.
913
933
  const DEMO = argv.includes("--demo");
934
+ // THE DEMO CLEANS UP AFTER ITSELF (owner ruling, 2026-09-14). A reader who tries the product once
935
+ // should not be left with a folder they did not choose, a guest list and a signing secret in their
936
+ // home — and the next demo on that machine should not have to reason about the last one's leftovers.
937
+ // `--keep` is for somebody who wants the reports to outlive the window, and it prints the one command
938
+ // that removes them.
939
+ const DEMO_KEEP = argv.includes("--keep");
940
+ // Whether the reader chose this base, which decides whether leftovers in it are the demo's to reset.
941
+ const BASE_GIVEN = argv.includes("--base");
914
942
  // A DEMO HAS NO BACKGROUND FORM, and asking for one is refused before anything is written. The
915
943
  // background path installs units that run the reader's own install, so `--demo --background` would set
916
944
  // up an empty install in their home and call it the demo.
@@ -944,13 +972,36 @@ if (isMain) {
944
972
  // AN INSTALL NOBODY CONFIGURED still answers. `start --base <dir>` writes a guest list on every start
945
973
  // and no settings at all, so an install somewhere of its own passes every check above. A guest list
946
974
  // with no demo secret beside it is somebody's install; a demo's own base has both.
947
- if (!found.length && existsSync(paths.grants) && !existsSync(demoTokenSecretPath(paths.base)))
975
+ //
976
+ // ONLY WHERE THE READER CHOSE THE BASE. On the demo's own default directory this check refused the
977
+ // demo's OWN leftovers — a 0.3.0-era demo wrote a guest list there, and the next `demo` with no
978
+ // flag at all was told to "run the demo without --base", which is the command it had just run. The
979
+ // default directory is the demo's; what is in it is the demo's to reset.
980
+ if (BASE_GIVEN && !found.length && existsSync(paths.grants) && !existsSync(demoTokenSecretPath(paths.base)))
948
981
  found.push(`it holds a guest list, ${paths.grants}, and no demo of its own`);
949
982
  if (found.length)
950
983
  fatal(`--demo cannot run in ${paths.base}: ${found.join("; ")}.\n`
951
984
  + " The demo keeps its own data, and its own signing secret, in its base. Leaving those in an\n"
952
985
  + " install's directory would make keys issued for that install refuse at its door.\n"
953
- + " Run the demo without --base, or give it a directory of its own.");
986
+ + ` Give the demo a directory of its own, or remove ${paths.base} if nothing in it is wanted.`);
987
+
988
+ // ── AN OLDER DEMO'S BASE IS RESET, SILENTLY, AND ONLY WHEN IT IS THE DEMO'S OWN ─────────────────
989
+ //
990
+ // Demos before this one kept their base, so a machine that met the product once carries one. The
991
+ // next demo must neither refuse it nor start half inside it: a base holding one version's pool and
992
+ // another's program is a state nobody designed and nobody can read.
993
+ //
994
+ // THE RAILS ARE WHAT MAKE THIS SAFE TO WRITE AT ALL, because this line removes a directory:
995
+ // · a demo, never an ordinary start;
996
+ // · a base the reader did NOT name — the demo's own default, not a directory they pointed at;
997
+ // · every install signal above already checked and none found;
998
+ // · the path still compared against the default before the call, so a future edit that changes
999
+ // how the default is computed cannot silently widen what this removes.
1000
+ const reset = demoBaseResetTarget({ baseGiven: BASE_GIVEN, base, demoDefault: resolve(join(homedir(), "trademark-demo")) });
1001
+ if (reset && existsSync(reset)) {
1002
+ try { rmSync(reset, { recursive: true, force: true }); }
1003
+ catch (e) { fatal(`the demo could not reset its own folder ${reset} (${e.code ?? e.message}). Remove it and run the demo again.`); }
1004
+ }
954
1005
  }
955
1006
  // ── THIS INSTALL'S FIRST START, read before this start writes either file that answers it ────────────
956
1007
  //
@@ -1020,6 +1071,10 @@ if (isMain) {
1020
1071
  // the flag is a convenience over the defaults, not an override of a decision. Driven in the issue:
1021
1072
  // with all three exported, the demo already came up correctly — that path must not change.
1022
1073
  const portFlag = flag("--port");
1074
+ // THE DEMO'S DEFAULTS, BEFORE THE FLAG AND AFTER THE ENVIRONMENT. Applied only when the reader asked
1075
+ // for no port at all: `--port` below replaces whatever this chose, and a variable somebody set is
1076
+ // preserved inside the function itself. See demoPortDefaults for why the demo may not share 18802.
1077
+ if (DEMO && !portFlag) Object.assign(ports, demoPortDefaults(ports, process.env));
1023
1078
  if (portFlag) {
1024
1079
  let moved;
1025
1080
  try { moved = portsForFlag(portFlag, ports, process.env); }
@@ -1969,7 +2024,7 @@ if (isMain) {
1969
2024
  }
1970
2025
  say("");
1971
2026
  say(` Open: ${envs.url}`);
1972
- for (const line of foreignPageHint(DEMO ? "demo" : "start")) say(` ${line}`);
2027
+ for (const line of foreignPageHint(DEMO ? "demo" : "start", ports.portal)) say(` ${line}`);
1973
2028
  say(" This SURVIVES the terminal — close the window, the product keeps running.");
1974
2029
  say(` Stop it: ${invoke("stop")} (stops and removes the units; issued connect keys survive — \`${invoke("disconnect")}\` revokes those)`);
1975
2030
  say(` Is it up? ${invoke("status")}`);
@@ -2070,6 +2125,26 @@ if (isMain) {
2070
2125
  }
2071
2126
  await new Promise((r) => setTimeout(r, 200));
2072
2127
  }
2128
+ // ── THE DEMO TAKES ITS FOLDER WITH IT ───────────────────────────────────────────────────────────
2129
+ //
2130
+ // AFTER the children are down, not before: the runner and the two services write into this base
2131
+ // while they live, and removing it under them would turn an orderly stop into a page of ENOENT.
2132
+ //
2133
+ // The same rails as the reset at the top of the run, plus one more: the demo only removes a base it
2134
+ // MADE this run. A reader who asked for a directory of their own keeps it — they named it, so it is
2135
+ // theirs — and `--keep` is for the reader who wants the reports to outlive the window.
2136
+ if (DEMO && !BASE_GIVEN && !DEMO_KEEP) {
2137
+ try {
2138
+ rmSync(paths.base, { recursive: true, force: true });
2139
+ say(`\n The demo removed everything it created: ${paths.base} is gone, and nothing of it is left on this machine.`);
2140
+ } catch (e) {
2141
+ // A FAILED CLEANUP IS SAID, NOT SWALLOWED. The promise this prints is the one thing a reader
2142
+ // cannot check for themselves once the window closes.
2143
+ err(`\n The demo could not remove ${paths.base} (${e.code ?? e.message}) — remove it by hand if nothing in it is wanted.`);
2144
+ }
2145
+ } else if (DEMO) {
2146
+ say(`\n The demo's folder is kept at ${paths.base}. Remove it when you are done: rm -rf ${paths.base}`);
2147
+ }
2073
2148
  process.exit(code);
2074
2149
  }
2075
2150
 
@@ -2203,7 +2278,7 @@ if (isMain) {
2203
2278
 
2204
2279
  say("");
2205
2280
  say(` Open ${envs.url}`);
2206
- for (const line of foreignPageHint(DEMO ? "demo" : "start")) say(` ${line}`);
2281
+ for (const line of foreignPageHint(DEMO ? "demo" : "start", ports.portal)) say(` ${line}`);
2207
2282
  say("");
2208
2283
  // ── TWO DOORS, TWO AUDIENCES, BOTH NAMED ( — F26) ─────────────────────────
2209
2284
  //
@@ -2285,6 +2360,11 @@ if (isMain) {
2285
2360
  say(` │ WRITE THE PASSPHRASE DOWN NOW. It is stored only as a digest, so`);
2286
2361
  say(` │ nothing — not this product, not this terminal — can read it back.`);
2287
2362
  say(` │ Lost it? ${reset}`);
2363
+ // THE HINT BELONGS IN THE BOX TOO, and this was the reader the whole sentence was written for. The
2364
+ // frame exists because a first-time reader skips the log wall and acts on it — so the one address
2365
+ // they copy was the one address with nothing beside it saying what to do when the page that opens
2366
+ // is somebody else's. It was printed nine lines above, to a reader who by design did not read there.
2367
+ for (const line of foreignPageHint(DEMO ? "demo" : "start", ports.portal)) say(` │ ${line}`);
2288
2368
  say(` └${rule}┘`);
2289
2369
  } else {
2290
2370
  // THE WAY BACK IN FIRST, then which credential, when and for whom: laterStartLines says why. Read for
@@ -2340,10 +2420,30 @@ if (isMain) {
2340
2420
  // cannot succeed and given a service manager that is not on the machine and cannot be put there.
2341
2421
  // Reported from a real run. Same rule as the engine refusal above: do not name a route this platform
2342
2422
  // does not have.
2343
- for (const line of backgroundOfferLines({ demo: DEMO, manager: backgroundManager(), start: invoke("start") })) say(line);
2423
+ for (const line of backgroundOfferLines({ demo: DEMO, keep: DEMO_KEEP || BASE_GIVEN, manager: backgroundManager(), start: invoke("start") })) say(line);
2344
2424
  say("");
2345
2425
  }
2346
2426
 
2427
+ /**
2428
+ * WHICH DIRECTORY, IF ANY, A DEMO MAY RESET BEFORE IT STARTS. Null means none, and null is the answer
2429
+ * for everything except one case.
2430
+ *
2431
+ * The one case: the demo's OWN DEFAULT directory, which the reader did not name. Demos before this one
2432
+ * kept their base, so a machine that met the product once carries one; the next demo must neither
2433
+ * refuse it — which is what happened, on a run with no flags, with a remedy telling the reader to drop
2434
+ * a flag they had not passed — nor start half inside it.
2435
+ *
2436
+ * A DIRECTORY THE READER NAMED IS THEIRS. `--base` is a decision about where their data lives, and
2437
+ * nothing in a demo may remove a path somebody chose; the install checks at the call site refuse those
2438
+ * instead. Separated from the call site so the rule can be driven as a table rather than by starting a
2439
+ * product, and because a rule this consequential should be readable in one screen. PURE.
2440
+ */
2441
+ export function demoBaseResetTarget({ baseGiven = false, base = "", demoDefault = "" } = {}) {
2442
+ if (baseGiven) return null; // the reader chose it, so it is not the demo's to clear
2443
+ if (!base || !demoDefault) return null; // nothing to compare: say no
2444
+ return base === demoDefault ? base : null; // compared whole, never by prefix — a sibling path is not this
2445
+ }
2446
+
2347
2447
  /**
2348
2448
  * The closing offer of a foreground start: how to get the prompt back, where there is a way.
2349
2449
  *
@@ -2353,10 +2453,16 @@ if (isMain) {
2353
2453
  * offered one. Where the offer stands, it names what it needs BEFORE the reader stops what is running.
2354
2454
  * PURE.
2355
2455
  */
2356
- export function backgroundOfferLines({ demo = false, manager = null, start = "clearotron start" } = {}) {
2456
+ export function backgroundOfferLines({ demo = false, keep = false, manager = null, start = "clearotron start" } = {}) {
2357
2457
  if (demo) return [
2358
2458
  " The demo has no background form: it runs as long as this window does. Leave it open and use a",
2359
2459
  " second terminal for the commands above.",
2460
+ // SAID BEFORE THE READER PRESSES IT, not only afterwards. A person deciding whether to close the
2461
+ // window is deciding whether to keep the reports, and finding that out from the last line is too
2462
+ // late. `--keep` is named here because this is where the choice is made.
2463
+ keep
2464
+ ? " This demo keeps its folder: the reports stay after the window closes, and the last line says how to remove them."
2465
+ : " Everything the demo made goes when this window closes — pass --keep to leave the folder and its reports behind.",
2360
2466
  ];
2361
2467
  if (!manager) return [
2362
2468
  " There is no background form on this platform: the product runs as long as this window does.",
package/build-info.json CHANGED
@@ -1,4 +1,4 @@
1
1
  {
2
- "commit": "ee3ad6c3a561265588b8db72f153e3eb6694d9fe",
3
- "version": "0.3.1-beta.0"
2
+ "commit": "3e6266f5960e7990e767edbdb95ca50831156b55",
3
+ "version": "0.3.1-beta.2"
4
4
  }
@@ -180,6 +180,20 @@ and only where it exceeds the stall window), `CLEAROTRON_KILL_ESCALATE_MS`
180
180
  fills mid-run surfaces as a *missing artifact* at a later stage rather than as a disk error. `0`
181
181
  disables; a non-numeric value throws rather than silently disabling the guard.).
182
182
 
183
+ `CLEAROTRON_MIN_TOKENS_PER_SEC` (1) and `CLEAROTRON_MIN_TOKENS_WARMUP_MS` (5 minutes) — the trickle floor, and
184
+ how long a turn is left alone before it applies. A stage that streams a token every few seconds is not a
185
+ slow stage: it holds off the stall clock, which resets on any streamed byte, and the no-progress ceiling,
186
+ which counts token movement as progress on purpose, so the only thing that ever stopped one was the wall
187
+ — at which point the attempt is thrown away and done again from the start. The floor is counted in output
188
+ tokens per second of ACTIVE time (elapsed minus tool wait), for the same reason the hard ceiling is: a
189
+ turn waiting on a slow register lookup is producing nothing and must not be killed for it. A turn under
190
+ the floor is stopped there and recorded as a stall, so a retry gets the ordinary budget rather than an
191
+ extended one. `0` disables the floor entirely and is read as "no floor", never as a floor of zero; a
192
+ negative or unreadable value falls back to the default rather than switching the instrument off. The
193
+ default is a floor against a pathology rather than a budget for a stage — the readings it was written
194
+ from were three orders of magnitude apart, 0.08 tokens per second against 74 — and it is expected to
195
+ move once the same window has been read across more runs.
196
+
183
197
  ### 5.4 Pipeline feature gates & kill switches — T3 (defaults ON; `0` disables)
184
198
 
185
199
  `CLEAROTRON_PLAN_DISPATCH`, `CLEAROTRON_SATPROBE_CODESIDE`,
@@ -1,5 +1,57 @@
1
1
  # clearotron-driver
2
2
 
3
+ ## 0.3.1-beta.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 240673c: Fixed: A chat notice now carries the channel to send it on, so an assistant with several chat channels no longer drops it silently.
8
+ - 240673c: Fixed: Naming a country in words rather than by code now works for every country, including Belgium and Luxembourg. Before, some were carried as unrecognised.
9
+ - 240673c: Fixed: A family search on a name whose first or last word is a single letter or digit now runs. Before, one register refused it and the search was reported as an outage.
10
+
11
+ Fixed: A conflict whose owner could not be identified is no longer given a risk rating. It is carried as an open item naming who must be identified.
12
+ - f96c089: Fixed: Running the test suite from inside another test run no longer lets the outer run delete the inner run's temporary files.
13
+ - f96c089: Fixed: A clearance for a new client can be ordered through an assistant connector without setting up a company first.
14
+ - 240673c: Fixed: In the staff editor, a refused territory in a project now highlights the field it is about, as it already did when editing a customer. Before, the message appeared but no field was marked.
15
+ - 240673c: Fixed: A sign-in refusal now always says which instance answered — by organisation, by sign-in service, or by the address it runs on.
16
+ - 240673c: Fixed: The published list of settings this build reads no longer keeps a name after the code stops reading it. The list was derived from a scan that included the list itself, so a retired name kept itself alive.
17
+ - 240673c: Fixed: A search step that streams at a crawl is now stopped early and retried, instead of running to its time limit and losing the work.
18
+ - f96c089: Fixed: A mark whose main element contains no vowel — a consonant-only initialism, for example — can now be cleared. Before, the search plan refused to compile and the whole clearance ended without delivering anything.
19
+ - 240673c: Fixed: Use your AI now gives the steps your connector actually takes — sign-in where it signs you in, a key only where a key works.
20
+ - f96c089: Fixed: `doctor` now names the deployment it is checking, and refuses a name that is missing or not recognised. Before, a deployment that was misnamed — or not named at all — passed the check in silence.
21
+ - 240673c: Fixed: Setup and the framed first-run box now say what to do when the page that opens belongs to another program.
22
+
23
+ Fixed: The port that advice suggests is never the port already in use.
24
+ - f96c089: For operators: The repository's own comment-to-code references are now checked for having moved, not only for existing.
25
+ - 240673c: Fixed: On WSL, the "on this computer" rows now start the server inside WSL for you, so an assistant running on Windows can use them.
26
+ - 240673c: Fixed: Somebody you add on the People page can sign in straight away. Before, they were refused until the service restarted with a changed setting.
27
+ - 240673c: Fixed: The demo now removes everything it created when its window closes, and says so. Pass `--keep` to leave the folder and its reports.
28
+
29
+ Fixed: Trying the demo a second time on a machine that has run it before now works. Before, it refused its own folder and suggested dropping a flag that had not been given.
30
+ - 240673c: Fixed: The demo opens ports of its own rather than the ones an installation uses, so the page it points you at is the demo's.
31
+ - 240673c: Fixed: A search now covers every spelling and sound-alike of the name in each category of goods or services the engine judges relevant.
32
+
33
+ Fixed: Before, the added categories were searched for the name exactly and nothing else. The matter frame records each one with the reason it was added.
34
+ - 240673c: Fixed: An off-register search now also covers the channels the matter itself names, not only the account's usual marketplaces.
35
+
36
+ Fixed: A channel no pass ran is now recorded as open rather than described in a note.
37
+
38
+ Fixed: A finding reads what the platform's own record says before calling an owner unidentified.
39
+ - 240673c: For operators: The portal can now call the engine over a local socket instead of a network port, by naming it as its engine address. The deployment check reports that address as wired and says which socket it is.
40
+ - f96c089: Fixed: The run purge no longer deletes a clearance whose report or failure notice has not been sent yet.
41
+
42
+ Fixed: Those runs are marked in the table the purge prints, and removing one now takes a flag that says so.
43
+
44
+ For operators: Every applied purge leaves a record of what it removed, when, and whether any of it was still owed.
45
+ - 1905ea4: Fixed: Saving a territory the engine cannot search now says so, instead of suggesting the kind of entry that was just refused.
46
+
47
+ ## 0.3.1-beta.1
48
+
49
+ ### Patch Changes
50
+
51
+ - c3228f3: Fixed: A clearance that stops is recorded as owing a notice even when the folder its notice is queued in cannot be written to.
52
+ - c3228f3: Fixed: Check now reports the same problems Save would refuse, so a company setting can no longer pass the check and then fail to save.
53
+ - c3228f3: Fixed: On older Windows-Subsystem installations the engine now identifies the platform by its interop registration rather than by the kernel version string.
54
+
3
55
  ## 0.3.1-beta.0
4
56
 
5
57
  ### Patch Changes
@@ -1,4 +1,5 @@
1
1
  {
2
2
  "citations": 621,
3
- "files": 1892
3
+ "files": 1892,
4
+ "checkable": 102
4
5
  }