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.
Files changed (161) hide show
  1. package/.env.example +64 -37
  2. package/.nvmrc +1 -1
  3. package/CONTRIBUTING.md +8 -4
  4. package/INSTALL.md +36 -14
  5. package/README.md +18 -22
  6. package/THIRD-PARTY-NOTICES.md +1 -1
  7. package/bin/clearotron.mjs +22 -0
  8. package/bin/example.mjs +10 -5
  9. package/bin/onboard.mjs +370 -28
  10. package/bin/start.mjs +220 -12
  11. package/build-info.json +2 -2
  12. package/demo/README.md +1 -1
  13. package/demo/full-country-search/run/_driver/framework.json +1 -1
  14. package/demo/global-preliminary-search/run/_driver/framework.json +1 -1
  15. package/demo/knockout-search/run/_driver/framework.json +1 -1
  16. package/demo/multi-country-focus-search/run/_driver/framework.json +1 -1
  17. package/docs/E2E.md +5 -5
  18. package/docs/GLOSSARY.md +85 -0
  19. package/docs/PORTAL.md +2 -2
  20. package/docs/README.md +6 -4
  21. package/docs/SECURITY.md +17 -0
  22. package/docs/architecture/01-product-overview.md +21 -9
  23. package/docs/architecture/04-configuration-reference.md +2 -0
  24. package/docs/architecture/05-config-governance.md +19 -0
  25. package/docs/architecture/05-customer-profiles.md +2 -2
  26. package/docs/configuration.md +4 -3
  27. package/docs/decisions/0006-what-the-public-repository-carries.md +30 -5
  28. package/driver/CHANGELOG.md +90 -0
  29. package/driver/README.md +25 -6
  30. package/driver/common-law-receipts.mjs +1 -1
  31. package/driver/config-staleness.mjs +1 -1
  32. package/driver/connotation-search.mjs +1 -1
  33. package/driver/contract-audit.mjs +5 -1
  34. package/driver/contract-e3-baseline.json +11 -11
  35. package/driver/dev-portal.mjs +2 -2
  36. package/driver/doubt-selection.mjs +1 -1
  37. package/driver/drainer-identity.mjs +1 -1
  38. package/driver/driver.config.mjs +1 -1
  39. package/driver/effort-model.mjs +2 -2
  40. package/driver/engine/cli-version.mjs +103 -0
  41. package/driver/engine/probe.mjs +45 -5
  42. package/driver/enqueue-schema.mjs +8 -8
  43. package/driver/feedback-store.mjs +1 -1
  44. package/driver/findings-model.mjs +52 -2
  45. package/driver/flag-snapshot.mjs +20 -0
  46. package/driver/framework.mjs +1 -1
  47. package/driver/gateway.mjs +20 -3
  48. package/driver/outbox-backoff.mjs +1 -1
  49. package/driver/package.json +3 -3
  50. package/driver/party-facts.mjs +2 -2
  51. package/driver/phase0.mjs +1 -1
  52. package/driver/pipeline.mjs +211 -106
  53. package/driver/plain-register.mjs +134 -0
  54. package/driver/portal-access.mjs +1 -1
  55. package/driver/portal-config-view.mjs +70 -2
  56. package/driver/portal-families.mjs +1 -1
  57. package/driver/portal-local-auth.mjs +5 -1
  58. package/driver/portal-report.mjs +1 -1
  59. package/driver/portal-service.mjs +79 -9
  60. package/driver/portal-upstream.mjs +2 -2
  61. package/driver/predelivery-lint.mjs +151 -2
  62. package/driver/profile-page.html +10 -2
  63. package/driver/profile-service.mjs +4 -4
  64. package/driver/profiles/README.md +3 -3
  65. package/driver/profiles.mjs +144 -12
  66. package/driver/publish/index.mjs +2 -2
  67. package/driver/publish/knockout.mjs +7 -1
  68. package/driver/publish/render-knockout.mjs +453 -118
  69. package/driver/publish/render.mjs +111 -16
  70. package/driver/reference-strip-signatures.mjs +20 -0
  71. package/driver/search-policy.mjs +3 -3
  72. package/driver/skills/blind-frame/SKILL.md +1 -1
  73. package/driver/skills/knockout-assess/SKILL.md +84 -0
  74. package/driver/skills/matter-frame/SKILL.md +6 -6
  75. package/driver/skills/matter-frame/watchlist-reference.md +2 -2
  76. package/driver/skills/prelim-common-law/SKILL.md +1 -1
  77. package/driver/skills/prelim-search/SKILL.md +1 -1
  78. package/driver/skills/prelim-search/report-prose.md +82 -0
  79. package/driver/skills/prelim-search/risk-framework-demo.manifest.json +1 -1
  80. package/driver/skills/prelim-search/risk-framework-triage.manifest.json +1 -1
  81. package/driver/skills/prelim-search/risk-framework.manifest.json +1 -1
  82. package/driver/skills/prelim-search/synthesis-rules.md +1 -1
  83. package/driver/skills/prelim-variants/SKILL.md +3 -3
  84. package/driver/stage-context.mjs +13 -0
  85. package/driver/stages.mjs +54 -7
  86. package/driver/suite-census.json +220 -58
  87. package/driver/systemd/clearotron-worker.service +3 -3
  88. package/driver/tokens.mjs +1 -1
  89. package/driver/unit-inventory.mjs +214 -27
  90. package/driver/verify.mjs +1 -1
  91. package/driver/whatif-memo-run.mjs +26 -4
  92. package/driver/whatif-memo.mjs +19 -1
  93. package/examples/README.md +2 -2
  94. package/examples/grants.example.json +6 -7
  95. package/mcp-server/CHANGELOG.md +4 -0
  96. package/mcp-server/lib/scrub.mjs +1 -1
  97. package/mcp-server/lib/whatif.mjs +2 -2
  98. package/mcp-server/package.json +1 -1
  99. package/mcp-server/packs/ops/EXAMPLES.md +2 -2
  100. package/mcp-server/server.mjs +1 -1
  101. package/package.json +31 -19
  102. package/portal-ui/dist/assets/{index-Ct3iQtZ2.css → index-1ziUJX1E.css} +43 -2
  103. package/portal-ui/dist/assets/{index-BUGgiYtF.js → index-KFAHMgdT.js} +289 -73
  104. package/portal-ui/dist/index.html +2 -2
  105. package/portal-ui/package.json +1 -1
  106. package/providers/_shared/term-shape.mjs +1 -1
  107. package/providers/oauth-mcp-bridge/CHANGELOG.md +4 -0
  108. package/providers/oauth-mcp-bridge/package.json +1 -1
  109. package/providers/uspto-local/src/index-store.js +2 -2
  110. package/scripts/added-reference-check.mjs +45 -89
  111. package/scripts/citation-line-check.mjs +121 -2
  112. package/scripts/e2e.mjs +2 -2
  113. package/scripts/env-classify.mjs +67 -3
  114. package/scripts/generated-files-are-current.mjs +125 -0
  115. package/scripts/live-surface-check.mjs +38 -1
  116. package/scripts/mint-names-in-force.mjs +4 -2
  117. package/scripts/mint-public-residue.mjs +74 -0
  118. package/scripts/mint-reference-strip-backlog.mjs +12 -1
  119. package/scripts/mint-suite-census.mjs +5 -2
  120. package/scripts/nightly-notice.mjs +0 -2
  121. package/scripts/preinstall-node-check.mjs +24 -0
  122. package/scripts/render-check.mjs +13 -2
  123. package/scripts/report-frame-check.mjs +8 -1
  124. package/scripts/report-screenshot.mjs +6 -3
  125. package/scripts/revisit-render-check.mjs +6 -3
  126. package/scripts/score.mjs +1 -1
  127. package/scripts/strip-tracker-citations.mjs +38 -7
  128. package/scripts/test-full.mjs +226 -0
  129. package/scripts/test-run.mjs +16 -0
  130. package/shared/README.md +2 -1
  131. package/shared/anon-overlay.mjs +3 -3
  132. package/shared/browser-temp-root.mjs +142 -0
  133. package/shared/checkout-move.mjs +1 -1
  134. package/shared/client-door.mjs +4 -3
  135. package/shared/customer-store.mjs +1 -1
  136. package/shared/identifier-scan.mjs +1 -1
  137. package/shared/install-auth.mjs +35 -0
  138. package/shared/invocation.mjs +33 -1
  139. package/shared/listen.mjs +55 -3
  140. package/shared/names-in-force.mjs +6 -1
  141. package/shared/node-floor.mjs +97 -0
  142. package/shared/os-advice.mjs +91 -0
  143. package/shared/reap-on-exit.mjs +42 -0
  144. package/shared/reference-guard-classes.mjs +351 -0
  145. package/shared/staff-domain.mjs +154 -0
  146. package/shared/store-in-repo.mjs +23 -2
  147. package/shared/suite-census.mjs +31 -3
  148. package/shared/withheld-paths-access.mjs +36 -18
  149. package/skills/clearotron-ops/SKILL.md +1 -1
  150. package/driver/recipes/README.md +0 -49
  151. package/driver/skills/prelim-search/risk-framework-aurora.manifest.json +0 -19
  152. package/driver/skills/prelim-search/risk-framework-aurora.md +0 -77
  153. package/driver/skills/prelim-search/risk-framework-zephyr.manifest.json +0 -14
  154. package/driver/skills/prelim-search/risk-framework-zephyr.md +0 -33
  155. package/driver/skills/prelim-search/worked-examples-aurora.md +0 -10
  156. package/driver/skills/prelim-search/worked-examples-zephyr.md +0 -10
  157. package/scripts/ai-page-render-check.mjs +0 -532
  158. package/scripts/clearances-render-check.mjs +0 -480
  159. package/scripts/composer-render-check.mjs +0 -1027
  160. package/scripts/home-render-check.mjs +0 -526
  161. package/scripts/portal-lifecycle-check.mjs +0 -639
package/bin/start.mjs CHANGED
@@ -116,7 +116,7 @@ import { SERVER_INSTALL_SET, unitsToRestartOnRefresh, unitHealthVerdict } from "
116
116
  // tolerate and never manage"; settled point 2 superseded that.)
117
117
  import { defaultDenylistPath, denylistPathFor, denylistFor, ensureDenylistFile, CLIENT_DOOR_UNIT, enablePlan, clientDoorPort } from "../shared/client-door.mjs"; // — one owner for the revocation list's path
118
118
  import { createServer } from "node:net";
119
- import { listenErrorMessage } from "../shared/listen.mjs";
119
+ import { listenErrorMessage, nextFreePort } from "../shared/listen.mjs";
120
120
  import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
121
121
  import { randomBytes } from "node:crypto";
122
122
  import { invocationPrefix, invoke } from "../shared/invocation.mjs"; // — the banner names the verb
@@ -130,6 +130,11 @@ import { productIdentity } from "../shared/product-identity.mjs"; // AGPL §13
130
130
  import { pinEnvAll } from "../shared/env-aliases.mjs"; // — a pin that names one spelling has set nothing that wins
131
131
  import { BRAND } from "../shared/brand.mjs"; // — the installer's own name, from the tenant seam
132
132
  import { rebuildIfStale } from "../shared/bundle-rebuild.mjs"; // never serve a bundle older than its sources
133
+ // ONE CLASSIFIER FOR WHAT A STAFF RULE ADMITS, shared with the setup wizard. Two copies of this
134
+ // judgement would be a wizard that asks about one rule and a launcher that writes another.
135
+ import { classifyStaffDomain, staffDomainRefusal, staffGrantSentence } from "../shared/staff-domain.mjs";
136
+ import { backgroundManager } from "../shared/os-advice.mjs";
137
+ import { frontingVariablesSet } from "../shared/install-auth.mjs"; // — one owner for what counts as a proxy in front of a door
133
138
 
134
139
  const REPO = join(dirname(fileURLToPath(import.meta.url)), "..");
135
140
  const ENV_PATH = envLocalPath({ repoRoot: REPO }); // resolved, never composed: one resolver, so moving this file later is one line
@@ -366,6 +371,11 @@ export function installPaths(base) {
366
371
  };
367
372
  }
368
373
 
374
+ /** A staff rule this command will not build. Carries the sentence the operator reads, and why. */
375
+ export class StaffDomainRefused extends Error {
376
+ constructor(domain, message) { super(message); this.name = "StaffDomainRefused"; this.domain = domain; }
377
+ }
378
+
369
379
  /**
370
380
  * The one address that signs in, and the staff domain derived from it.
371
381
  *
@@ -374,10 +384,61 @@ export function installPaths(base) {
374
384
  * it safe here and would not be safe on a hosted instance. Without it the sign-in succeeds and every
375
385
  * page 403s, because signing in is not being enrolled (portal-access.mjs decides that, and it is
376
386
  * deliberately blind to which door you came through).
387
+ *
388
+ * ── AND THAT PARAGRAPH IS ONLY TRUE OF `<account>@localhost` ─────────────────────────────────────────
389
+ *
390
+ * "admits exactly that address and nobody else" is a property of the DOMAIN, not of the mode. It holds
391
+ * for `localhost`. It does not hold for a domain other people have addresses at: the rule this line
392
+ * produces from `alex@a-firm.example-tld` says *anyone at that domain*, the settings page then reports
393
+ * it back in those words, and on a deployment where identity arrives from an external login system the
394
+ * domain is the whole of the check. Nobody was asked, and there is nothing on the box recording that a
395
+ * grant was made.
396
+ *
397
+ * So the derivation now refuses the domains that can never be a staff rule — a webmail provider, a
398
+ * documentation domain — and hands anything wider than one machine back to the caller to state and
399
+ * confirm. `shared/staff-domain.mjs` is the one classifier, shared with the setup wizard, so the
400
+ * question the wizard asks and the rule this command builds cannot drift apart.
401
+ *
402
+ * An UNPARSEABLE address still returns `""`, and that is not the same answer as a refusal: nothing was
403
+ * classified, so nothing was decided. The caller already refuses a non-address one line earlier.
377
404
  */
378
405
  export function staffDomainFor(email) {
379
406
  const at = String(email ?? "").lastIndexOf("@");
380
- return at > 0 ? String(email).slice(at + 1).toLowerCase() : "";
407
+ const domain = at > 0 ? String(email).slice(at + 1).toLowerCase() : "";
408
+ if (!domain) return "";
409
+ const refusal = staffDomainRefusal(domain);
410
+ if (refusal) throw new StaffDomainRefused(domain, refusal);
411
+ return domain;
412
+ }
413
+
414
+ /**
415
+ * What to say when the derived domain is a real one — wider than this machine, and never assumed.
416
+ *
417
+ * A REFUSAL RATHER THAN A PROMPT, and the reason is that this command has no reader. `start` runs from
418
+ * a script, from `--background`, and from a service manager as often as from a terminal; a question
419
+ * asked there either hangs or is answered by whatever happens to be on stdin. Every other decision on
420
+ * this boundary already refuses instead — an unset access file, a multi-`@` identity, local mode off
421
+ * loopback — so this is the idiom the rest of the door speaks.
422
+ *
423
+ * The consent itself lives where a person is definitionally present: `clearotron install` asks for the
424
+ * address, shows this same sentence, and writes the answer down as an explicit `PORTAL_STAFF_DOMAINS`.
425
+ * After that the `||` above this call short-circuits and the derivation is never reached again.
426
+ */
427
+ export function wideStaffDomainRefusal({ user, domain, envPath }) {
428
+ return `${user} would make every address at ${domain} an administrator of this install.\n`
429
+ + "\n"
430
+ + ` The rule that would be written is: ${staffGrantSentence(domain, { staffLabel: `${BRAND.name} staff` })}\n`
431
+ + "\n"
432
+ + " That is a grant to a group, and this command will not make one on your behalf. Pick the one\n"
433
+ + " that is true here:\n"
434
+ + "\n"
435
+ + ` · only you use this machine — start without --user, or use ${String(user).slice(0, String(user).lastIndexOf("@"))}@localhost.\n`
436
+ + ` The rule is then this machine and nobody else, and nothing is granted to anyone.\n`
437
+ + ` · your colleagues at ${domain} should all be administrators — say so once, in writing:\n`
438
+ + ` PORTAL_STAFF_DOMAINS=${domain}\n`
439
+ + ` in ${envPath}, or in the environment. The settings page then names that file as where the\n`
440
+ + " rule came from, so whoever reads it later can undo it.\n"
441
+ + ` · run \`${invocationPrefix()}clearotron install\`, which asks for the address and this question with it.`;
381
442
  }
382
443
 
383
444
  /**
@@ -777,6 +838,10 @@ if (isMain) {
777
838
  // bound anywhere else in any mode. Sign-in is untouched — the demo signs in like any first start, and
778
839
  // the portal mints and prints its passphrase exactly as it does for a real one.
779
840
  const DEMO = argv.includes("--demo");
841
+ // THE DEMO BRINGS ITS OWN ACCOUNT. A fresh install resolves `generic` and nothing else (owner
842
+ // ruling, 2026-09-08), so the demo account is refused from the roster unless somebody asked for it.
843
+ // Asked here, once and visibly, rather than at each site that happens to read a roster.
844
+ if (DEMO) process.env.CLEAROTRON_DEMO_PROFILES ??= "1";
780
845
  // ── `--port` MOVES EVERY DOOR IT OPENS (tracker issue 166) ───────────────────────────────────────
781
846
  //
782
847
  // It used to move ONE of the three. `resolvePorts` reads three independent variables with three
@@ -812,16 +877,47 @@ if (isMain) {
812
877
  run: (cmd, args) => spawnSync(cmd, args, { cwd: REPO, stdio: "inherit", shell: process.platform === "win32" }).status ?? 1,
813
878
  });
814
879
 
815
- // Nobody is asked for this. A local install has one user, this machine already knows their name, and
816
- // the address never leaves the machine — `--user` is there for a reader who wants their real one, and
817
- // whatever is resolved here is written to `.env` so the question is never put twice.
880
+ // The ADDRESS is not asked for here: a local install has one user, this machine already knows their
881
+ // name, and the address never leaves the machine — `--user` is there for a reader who wants their real
882
+ // one, and whatever is resolved here is written to `.env` so the question is never put twice. What IS
883
+ // asked for, and used not to be, is the staff rule the address implies once it is a real one; that is
884
+ // the block below the address, and `clearotron install` is where the question is actually put.
818
885
  let whoami = "user";
819
886
  try { whoami = userInfo().username || "user"; } catch { /* a container with no passwd entry */ }
820
887
  // In a demo the address is the demo's own and is never written anywhere: see the DEMO block above.
821
888
  const user = String(flag("--user", DEMO ? "demo@localhost" : (process.env.PORTAL_LOCAL_USER || `${whoami}@localhost`))).trim().toLowerCase();
822
889
  if (!user.includes("@") || user.indexOf("@") !== user.lastIndexOf("@"))
823
890
  fatal(`--user "${user}" is not a single email address. It is the one identity that signs in here, and the portal refuses a multi-@ identity outright.`);
824
- const staffDomains = process.env.PORTAL_STAFF_DOMAINS || staffDomainFor(user);
891
+ // ── THE STAFF RULE THIS ADDRESS IMPLIES, STATED BEFORE IT IS WRITTEN ──────────────────────────────
892
+ //
893
+ // An explicit `PORTAL_STAFF_DOMAINS` is a decision somebody already made in writing, and it wins
894
+ // untouched — including a domain the classifier would otherwise refuse, because an operator who
895
+ // typed it has said what they mean and this command does not overrule that.
896
+ //
897
+ // Absent one, the rule is DERIVED from the single address, and that derivation is the defect this
898
+ // block exists to close. Three outcomes:
899
+ //
900
+ // `localhost` (or any bare hostname) — one machine, no second person, nothing to ask. Silent, as
901
+ // it has always been. This is the default path and the only
902
+ // one a laptop ever reaches.
903
+ // a webmail or documentation domain — refused by `staffDomainFor` itself; there is no yes that
904
+ // makes it right.
905
+ // any other real domain — refused HERE, with the rule quoted in the words the
906
+ // settings page uses and the three ways out named.
907
+ //
908
+ // BEFORE THIS RUN CHANGES THE BOX, deliberately — it sits above the state-written divide further
909
+ // down, with the units gate, the auth-mode gate and the port probe. A refused start has written no
910
+ // `.env`, minted no secret and placed no unit, so the reader's only question — is my install
911
+ // half-made — has one answer.
912
+ let staffDomains = process.env.PORTAL_STAFF_DOMAINS;
913
+ if (!staffDomains) {
914
+ let derived = "";
915
+ try { derived = staffDomainFor(user); }
916
+ catch (e) { fatal(e.message); }
917
+ if (derived && classifyStaffDomain(derived) === "wide")
918
+ fatal(wideStaffDomainRefusal({ user, domain: derived, envPath: ENV_PATH }));
919
+ staffDomains = derived;
920
+ }
825
921
 
826
922
  say("");
827
923
  say(` ${BRAND.name} ${BRAND.product.toLowerCase()} — local install`);
@@ -868,7 +964,24 @@ if (isMain) {
868
964
  // loop because both paths below have to act on it: the background path must RESTART the door it
869
965
  // adopted, and the foreground path must not spawn a second one beside it.
870
966
  let adoptedClientDoor = false;
871
- for (const [what, port, portVar, doorUnit = null] of [["portal", ports.portal, "PORTAL_SERVICE_PORT"], ["engine door", ports.mcp, "TRADEMARK_MCP_HTTP_PORT"], ["client door", ports.client, "CLIENT_MCP_HTTP_PORT", CLIENT_DOOR_UNIT]]) {
967
+ // ── WHETHER ANYTHING OUTSIDE THIS PROCESS IS ADDRESSED TO THESE NUMBERS ────────────────────────
968
+ //
969
+ // A door may only be moved off a port nobody chose when nothing fronts it. A proxy, an Access team
970
+ // or an OIDC issuer means something external resolves to these numbers, and a door that quietly
971
+ // moved would be up and unreachable — which looks like success and is the worst of the three
972
+ // outcomes. The auth-mode half of this is already closed further up: a foreground start refuses
973
+ // outright when PORTAL_AUTH_MODE names a hosted door, and again when the units are installed and
974
+ // serving. What is left to check is the settings that can be present with the mode unset.
975
+ //
976
+ // PORTAL_AUTH_MODE IS ABSENT FROM THAT LIST ON PURPOSE, and only a rule in another file makes that
977
+ // safe: `driver/portal-service.mjs` refuses to start in auth-proxy without CF_ACCESS_TEAM or
978
+ // PORTAL_OIDC_ISSUER, so a portal fronted by the mode alone cannot come up at all. Relaxing that
979
+ // refusal without adding the mode here would let this under-report — which is why the list has one
980
+ // owner in `shared/install-auth.mjs` with an arm holding it to the doors themselves.
981
+ const fronted = frontingVariablesSet(process.env);
982
+ const claimedPorts = new Set([ports.portal, ports.mcp, ports.client]);
983
+ const movedDoors = [];
984
+ for (const [what, port, portVar, doorUnit = null, key = null] of [["portal", ports.portal, "PORTAL_SERVICE_PORT", null, "portal"], ["engine door", ports.mcp, "TRADEMARK_MCP_HTTP_PORT", null, "mcp"], ["client door", ports.client, "CLIENT_MCP_HTTP_PORT", CLIENT_DOOR_UNIT, "client"]]) {
872
985
  // A --background REFRESH runs over its own healthy units, which hold these ports on purpose;
873
986
  // systemd's restart is the handover. Probing would refuse the flag exactly once it has worked.
874
987
  // The narrow carve above already proved every installed unit is ours.
@@ -893,8 +1006,38 @@ if (isMain) {
893
1006
  // on a privileged port and from an address this host does not have, and names the way out of each;
894
1007
  // the launcher having its own shorter sentence for one of the three would mean a user meets two
895
1008
  // different answers to the same question depending on which door refused first.
1009
+ // ── A PORT NOBODY CHOSE IS MOVED RATHER THAN REFUSED (owner ruling, 2026-09-09) ─────────────
1010
+ //
1011
+ // Three conditions, and each is a different reason:
1012
+ // · the address is genuinely taken — anything else is not this case;
1013
+ // · the reader did not state this port, so no one is addressed to it. An explicitly set port is
1014
+ // a stated address and is refused exactly as before;
1015
+ // · nothing fronts these doors, so nothing outside resolves to the old number.
1016
+ // With any of those false the refusal below fires unchanged, which is the whole of the previous
1017
+ // behaviour kept intact underneath this.
1018
+ if (code === "EADDRINUSE" && key && !fronted.length && !String(process.env[portVar] ?? "").trim()) {
1019
+ const next = await nextFreePort(port, async (p) => (await probe(p)) === null, { claimed: claimedPorts });
1020
+ // NULL IS NOT A FALLBACK. Nothing free in range means the reader is told the truth about the
1021
+ // port they asked for, rather than sent to one this could not prove was free either.
1022
+ if (next) {
1023
+ claimedPorts.delete(port);
1024
+ claimedPorts.add(next);
1025
+ ports[key] = next;
1026
+ movedDoors.push({ what, portVar, from: port, to: next });
1027
+ continue;
1028
+ }
1029
+ }
896
1030
  if (code) fatal(listenErrorMessage({ code }, { what, host: HOST, port, portVar, portFile }));
897
1031
  }
1032
+ // SAID OUT LOUD, EVERY TIME. A door that moved is at an address the reader did not ask for and will
1033
+ // not find by memory — and the portal's own URL is printed from `ports.portal` further down, so a
1034
+ // silent move would leave the two disagreeing with nothing explaining it.
1035
+ if (movedDoors.length) {
1036
+ say("");
1037
+ say(` ${movedDoors.length === 1 ? "One door was" : `${movedDoors.length} doors were`} already in use, so ${movedDoors.length === 1 ? "it" : "they"} moved:`);
1038
+ for (const d of movedDoors) say(` the ${d.what}: ${d.from} → ${d.to} (set ${d.portVar} to pin it)`);
1039
+ say(" Nothing outside this machine is addressed to these, so nothing else needed changing.");
1040
+ }
898
1041
 
899
1042
  // ── 2. the two secrets, generated once and kept ────────────────────────────────────────────────────
900
1043
 
@@ -1574,10 +1717,40 @@ if (isMain) {
1574
1717
  // sequence rather than a race between the terminal's signal and ours. It also means the group
1575
1718
  // kill below reaches anything a child spawns, not just the child.
1576
1719
  detached: true,
1577
- stdio: ["ignore", "inherit", "inherit"],
1720
+ // STDERR IS TEED RATHER THAN INHERITED, AND ONLY STDERR.
1721
+ //
1722
+ // These children announce a refusal as one FATAL line on stderr and exit 1. With `inherit` the
1723
+ // parent holds no copy, so the failure message below could only point at output it had never seen —
1724
+ // and on one report that output never reached the reader's terminal at all. It said "its own output
1725
+ // above says why" about a line that was not above, which is worse than saying nothing: it sends
1726
+ // somebody scrolling for a sentence they will not find.
1727
+ //
1728
+ // stdout stays inherited. It carries no refusal — measured: on a refusing door stdout is empty and
1729
+ // stderr holds the whole of it — and leaving it alone keeps the child's terminal detection and any
1730
+ // progress rendering intact. Piping a stream costs the child its TTY, so only the stream that has to
1731
+ // be read is piped.
1732
+ stdio: ["ignore", "inherit", "pipe"],
1578
1733
  env: { ...process.env, ...env },
1579
1734
  });
1580
- const rec = { name, script, child, alive: true };
1735
+ // The last lines of that child's stderr, forwarded on as they arrive so nothing is delayed or
1736
+ // swallowed, and kept so the failure can QUOTE them rather than refer to them.
1737
+ const tail = [];
1738
+ // A CHUNK IS NOT A LINE. `data` arrives on whatever boundary the pipe gives us, so a stream split
1739
+ // mid-sentence would push two half-lines and the quoted last words would reach the reader cut in
1740
+ // half — on precisely the message that exists because they could not see the original. The partial
1741
+ // remainder is held over and completed by the next chunk; whatever is left when the stream ends is
1742
+ // flushed, because a process that dies mid-line still said the thing it was saying.
1743
+ let pending = "";
1744
+ const keep = (line) => { if (line.trim()) tail.push(line); while (tail.length > 12) tail.shift(); };
1745
+ child.stderr?.setEncoding("utf8");
1746
+ child.stderr?.on("data", (chunk) => {
1747
+ process.stderr.write(chunk);
1748
+ const parts = (pending + String(chunk)).split("\n");
1749
+ pending = parts.pop() ?? "";
1750
+ for (const line of parts) keep(line);
1751
+ });
1752
+ child.stderr?.on("end", () => { keep(pending); pending = ""; });
1753
+ const rec = { name, script, child, alive: true, tail };
1581
1754
  children.push(rec);
1582
1755
  child.on("exit", (code, signal) => {
1583
1756
  rec.alive = false;
@@ -1590,7 +1763,7 @@ if (isMain) {
1590
1763
  + ` but nothing is draining the queue now. Start one with:\n node ${script} --watch\n`);
1591
1764
  return;
1592
1765
  }
1593
- err(`\nstart: ${name} (${script}) exited ${signal ? `on ${signal}` : `with code ${code}`} — its own output above says why. Stopping the rest.\n`);
1766
+ err(childExitReport({ name, script, code, signal, tail }));
1594
1767
  void shutdown(1);
1595
1768
  });
1596
1769
  child.on("error", (e) => {
@@ -1857,7 +2030,42 @@ if (isMain) {
1857
2030
  // run". Saying it here, before the commands go by, is the whole fix.
1858
2031
  say(" This terminal is now the product: it runs only while this command does, and Ctrl-C — or closing");
1859
2032
  say(" the window — stops everything it started. So the commands above need a SECOND terminal.");
1860
- say(` To get your prompt back instead, stop this and run ${invoke("start")} --background`);
1861
- say(" — same product, managed by systemd, and it survives logout.");
2033
+ // ── THE BACKGROUND ROUTE IS NOT OFFERED WHERE IT CANNOT WORK ────────────────────────────────────
2034
+ //
2035
+ // `--background` installs and enables service units. There are none on Windows, so both the offer
2036
+ // and the sentence naming what manages them were wrong there — a reader was told to run a flag that
2037
+ // cannot succeed and given a service manager that is not on the machine and cannot be put there.
2038
+ // Reported from a real run. Same rule as the engine refusal above: do not name a route this platform
2039
+ // does not have.
2040
+ const manager = backgroundManager();
2041
+ if (manager) {
2042
+ say(` To get your prompt back instead, stop this and run ${invoke("start")} --background`);
2043
+ say(` — same product, managed by ${manager}, and it survives logout.`);
2044
+ } else {
2045
+ say(" There is no background form on this platform: the product runs as long as this window does.");
2046
+ say(" Leave it open and use a second terminal for the commands above.");
2047
+ }
1862
2048
  say("");
1863
2049
  }
2050
+
2051
+ /**
2052
+ * What a reader is told when a child this install cannot run without exits.
2053
+ *
2054
+ * QUOTED, NOT POINTED AT. This used to end "its own output above says why" while the parent inherited
2055
+ * the child's stderr and therefore held no copy of it. On one report that output never reached the
2056
+ * reader's terminal at all, so the sentence sent somebody scrolling for a line that was not there —
2057
+ * worse than saying nothing, because it reads as a working instruction.
2058
+ *
2059
+ * An empty tail is reported as an empty tail. A child that exits silently is a finding about the child,
2060
+ * and printing a heading with nothing under it would hide exactly that.
2061
+ *
2062
+ * PURE, and exported, so this can be driven without starting a supervisor — the failure path of a
2063
+ * process manager is the one nobody exercises by hand.
2064
+ */
2065
+ export function childExitReport({ name, script, code, signal, tail = [] }) {
2066
+ const how = signal ? `on ${signal}` : `with code ${code}`;
2067
+ const said = tail.length
2068
+ ? `Its last line${tail.length > 1 ? "s" : ""}:\n${tail.map((l) => ` ${l}`).join("\n")}\n`
2069
+ : "It exited without printing anything, which is itself the thing to report.\n";
2070
+ return `\nstart: ${name} (${script}) exited ${how}. Stopping the rest.\n${said}`;
2071
+ }
package/build-info.json CHANGED
@@ -1,4 +1,4 @@
1
1
  {
2
- "commit": "66760dc4f4b83293e80c8422e0633cdee2aabb41",
3
- "version": "0.2.2"
2
+ "commit": "3c2277f7fab7452490c86d19802dd60a886a2b06",
3
+ "version": "0.2.4"
4
4
  }
package/demo/README.md CHANGED
@@ -37,7 +37,7 @@ they are machine output with a fixed shape, described here instead:
37
37
  the search policy, the receipts, the pre-delivery lint. These are what the report is assembled from
38
38
  and what the audit surfaces read.
39
39
  - `<product-id>/run/_records/` — the register records the run actually retrieved, one file per record,
40
- named by register and number. Real EUIPO records for a fictional mark.
40
+ named by register and number. Real Clarivate Compumark records for a fictional mark.
41
41
 
42
42
  `<product-id>/PROVENANCE.md` says how each run was frozen, what was dropped, and how to regenerate it.
43
43
 
@@ -2,7 +2,7 @@
2
2
  "schema_version": 1,
3
3
  "framework_key": "house-default",
4
4
  "title": "Generic default risk framework",
5
- "source_deck": "Generic house default IP Risk Assessment Framework.pptx (Privileged & Confidential), completed by the reviewing lawyer 2026-08-31; supersedes the 2026-07-05 transcription (doc 50), which was partial",
5
+ "source_deck": "Cordillera house risk framework, authored in-house and reviewed by counsel; 2026-08-31 revision",
6
6
  "entity_label": "the company",
7
7
  "bands": [
8
8
  {
@@ -2,7 +2,7 @@
2
2
  "schema_version": 1,
3
3
  "framework_key": "house-default",
4
4
  "title": "Generic default risk framework",
5
- "source_deck": "Generic house default IP Risk Assessment Framework.pptx (Privileged & Confidential), completed by the reviewing lawyer 2026-08-31; supersedes the 2026-07-05 transcription (doc 50), which was partial",
5
+ "source_deck": "Cordillera house risk framework, authored in-house and reviewed by counsel; 2026-08-31 revision",
6
6
  "entity_label": "the company",
7
7
  "bands": [
8
8
  {
@@ -2,7 +2,7 @@
2
2
  "schema_version": 1,
3
3
  "framework_key": "house-triage",
4
4
  "title": "House knockout triage ladder (Stage 0)",
5
- "source_deck": "Ported from the interactive knockout-searches skill's 5-tier rating system (SKILL.md rating table + calibration rules), doc 50 shape",
5
+ "source_deck": "Cordillera house triage scale for knockout screening, authored in-house; five rating tiers",
6
6
  "entity_label": "the client",
7
7
  "bands": [
8
8
  {
@@ -2,7 +2,7 @@
2
2
  "schema_version": 1,
3
3
  "framework_key": "house-default",
4
4
  "title": "Generic default risk framework",
5
- "source_deck": "Generic house default IP Risk Assessment Framework.pptx (Privileged & Confidential), completed by the reviewing lawyer 2026-08-31; supersedes the 2026-07-05 transcription (doc 50), which was partial",
5
+ "source_deck": "Cordillera house risk framework, authored in-house and reviewed by counsel; 2026-08-31 revision",
6
6
  "entity_label": "the company",
7
7
  "bands": [
8
8
  {
package/docs/E2E.md CHANGED
@@ -45,7 +45,7 @@ CLEAROTRON_DATABASE=corsearch # REQUIRED, no default (track
45
45
  CORSEARCH_SESSION_KEY=dev-offline # the credential preflight wants it set; never fetches under the mock
46
46
  CLEAROTRON_SATPROBE_CODESIDE=0 # the probe dials the provider; a mock run cannot
47
47
  CLEAROTRON_BAND_TRUTH_GATE=0 # the gate evidences bands against the production call ledger
48
- CLEAROTRON_CUSTOMERS_DIR= # unset ⇒ the in-repo demo customers (aurora/zephyr/petcary)
48
+ CLEAROTRON_CUSTOMERS_DIR= # unset ⇒ the demo customer bundled with the repo
49
49
  # MCP face (optional): TRADEMARK_MCP_DEV=1 TRADEMARK_MCP_AUTH_DISABLED=1 + loopback host + an ABSOLUTE
50
50
  # CLEAROTRON_ACCESS_FILE=/abs/path/grants.json. All four, or it refuses to start: with auth off and no
51
51
  # grants file every token-less caller resolves to internal read-all across every customer.
@@ -59,8 +59,8 @@ and the refusal prints the absolute path to write.
59
59
  Drive one run through the whole spine by hand:
60
60
 
61
61
  ```bash
62
- node driver/enqueue.mjs --mark "AURORA PROBE" --classes 9,41 --goods "game software" \
63
- --forwarder ops --profile aurora
62
+ node driver/enqueue.mjs --mark "NORTHWIND PROBE" --classes 9,41 --goods "water-quality monitoring software" \
63
+ --forwarder ops --profile demo-brand-owner
64
64
  node driver/runner.mjs # claims, runs all stages on the mock, publishes, writes the packets
65
65
  ls /home/you/trademark-dev/outbox # → <runId>.pending (+ failure/intake packets on the sad paths)
66
66
  cat <archived run>/_driver/delivery.json
@@ -75,7 +75,7 @@ outbox packet kind — on a real filesystem with real process boundaries, for fr
75
75
  this, from a clean `env -i` shell: enqueue → runner → `DELIVERED (verdict CLEAR)` → archived run dir
76
76
  + `<runId>.pending` outbox marker + `_driver/delivery.json` (runId, forwarder, subject,
77
77
  `emailBodyHtml`, url, verdict — no profile field; that is `_driver/profile.json` beside it, the run's
78
- frozen `aurora` demo profile, and the file to open to prove which profile resolved).
78
+ frozen demo profile, and the file to open to prove which profile resolved).
79
79
 
80
80
  ### Tier 1b — the UI PORTAL (browse the dev instance; develop UI features against it)
81
81
 
@@ -105,7 +105,7 @@ so a dev instance beside a live one must be given its own (`PORTAL_PORT`, `PROFI
105
105
  silently — each is a proxy to a port, and the port is all it knows. `/recipes/*` is the worse half:
106
106
  its save endpoint writes and git-commits into whichever recipe store it reached.
107
107
 
108
- A pass here looks like: index, run report, the aurora customer page and the profile-editor UI all
108
+ A pass here looks like: index, run report, the demo customer page and the profile-editor UI all
109
109
  render against the Tier-1 pool; the `/profiles/*` proxy round-trips; traversal and non-loopback binds
110
110
  are refused (unit-tested).
111
111
 
@@ -0,0 +1,85 @@
1
+ # Glossary
2
+
3
+ Words this codebase uses in a particular way. They are here because they are already in the tree —
4
+ in file names, comments and test titles — and a contributor meeting one should not have to reverse
5
+ it out of the code. Product vocabulary a client would meet is in [`../README.md`](../README.md); the
6
+ tenant, account and project model is in [`../INSTALL.md`](../INSTALL.md) under "The four things, and
7
+ what contains what".
8
+
9
+ Nothing here is a rule. Each line says what the word points at, and names the file that owns it.
10
+
11
+ ## The run
12
+
13
+ **Seat** — one model turn with one job, inside a stage that has several. The common-law grid runs
14
+ halves `a` and `b` and a meaning seat `m`; each has its own model tier and its own ledger, and one
15
+ seat failing is not the stage failing.
16
+
17
+ **Dispatch** — handing a seat its prompt and waiting for the turn. Recorded per attempt in
18
+ `_driver/<stage>.jsonl`, so a stage that ran three times has three dispatches and one result.
19
+
20
+ **Matter frame** — what the run understands the request to be: the mark, the goods, the sector, the
21
+ territories. Everything downstream derives from it, and it is told not to widen past the territories
22
+ the job named.
23
+
24
+ **Blind frame** — a second reading of the raw request that never sees the matter frame. It runs
25
+ beside the investigation as a non-fatal sibling, so a framing mistake shows up as a disagreement
26
+ rather than propagating quietly.
27
+
28
+ **Feedforward** — the reviewer's flags reaching the corrective pass as data rather than as prose.
29
+ `corrections-feedforward.mjs` decides which flags count as still open.
30
+
31
+ **Carry-through** — of every subject the run's findings surface, which ones reach none of the
32
+ delivered documents. A finding that is recorded and never carried is the thing this measures.
33
+
34
+ **Declination** — a record the run enumerated and then decided not to pursue, with the position of
35
+ its reason in `_driver/declination-spec.json`. A declination is a decision, not a gap.
36
+
37
+ **Refusal** — the product declining rather than failing. `terminalKind: "designed-refusal"`. Nothing
38
+ about a refusal is retried, parked or recovered; the remedy is always the operator's.
39
+
40
+ ## What a run leaves behind
41
+
42
+ **Ledger** — an append-only record of what was covered, kept so that "unsearched" can never be
43
+ reported as "clean". The coverage ledger appears in the report as prose and as JSON, from one source.
44
+
45
+ **Receipt** — a record that something happened, kept for observability and deliberately not used as a
46
+ gate. A receipt read as a gate is a Goodhart problem, and the reasoning-integrity receipt says so on
47
+ its own face.
48
+
49
+ **Register-digest** — the condensed register result a downstream stage reads instead of the raw
50
+ records: the same evidence, at the size a model turn can hold.
51
+
52
+ **Hit list** — one line per enumerated record and the fate it was given. It is not a read: a record
53
+ can be on the hit list and never opened, and the sign-off condition is about documents actually read.
54
+
55
+ **Predelivery** — the checks that run after the documents exist and before anyone receives them
56
+ (`predelivery-lint.mjs`, `_driver/predelivery-lint.json`). A failing predelivery withholds the
57
+ artifact; it never annotates it.
58
+
59
+ **Courier** — delivery as a self-contained packet handed to the integrator. The engine does not send;
60
+ what it produces is carried.
61
+
62
+ **Drift** — a value that is mirrored in two places by design, and the two disagreeing. The catalogue
63
+ of what is mirrored, and which copy owns it, is in the architecture docs.
64
+
65
+ ## Guards and tests
66
+
67
+ **Arm** — one `test(...)` block. A file has many; each is named for the property it holds, and a red
68
+ arm is read by its output rather than by its name.
69
+
70
+ **Plant** — deliberately breaking something to prove a check would catch it. A guard that has never
71
+ been shown failing has not been shown to work, so the hard version plants a NEW instance of the class
72
+ rather than the one the check was written against.
73
+
74
+ **Contract dictation** — the rule that a contract is stated once and everything else derives from it.
75
+ `contract-dictation.mjs` computes over a corpus; `scripts/contract-dictation-scan.mjs` builds that
76
+ corpus from the tracked tree, so a new authoring layer is caught structurally rather than because
77
+ somebody remembered to list it.
78
+
79
+ **Assert census** — counting the assertions that never executed. An assertion inside a branch nothing
80
+ reaches is a test that passes and guards nothing (`scripts/unexecuted-asserts.mjs`).
81
+
82
+ **Authority probe** — asking the live CLI whether it honours a boundary the driver sets, and whether
83
+ the turn survives being refused. CI has no CLI and no subscription, so it can only prove the decision
84
+ is made correctly; whether it is obeyed is a property of the real binary
85
+ (`scripts/authority-boundary-probe.mjs`).
package/docs/PORTAL.md CHANGED
@@ -133,14 +133,14 @@ rather than quietly opening a passphrase door instead; and local mode refuses a
133
133
  `PORTAL_SERVICE_HOST`, because off loopback the passphrase and the session cookie are on the wire in
134
134
  clear. Put a TLS-terminating proxy in front if it has to be reachable.
135
135
 
136
- Grants fixture: `{"tenants":{"demo":{"accounts":["aurora"],"users":{"cli@celta.example":["aurora"]}}}}`.
136
+ Grants fixture: `{"tenants":{"demo":{"accounts":["foxglade"],"users":{"cli@celta.example":["foxglade"]}}}}`.
137
137
 
138
138
  The trigger lane needs the MCP HTTP face and an accounts-scoped ops token; without both, the run step
139
139
  reports the trigger lane unwired and the plan step still works. **The face no longer has to be run in
140
140
  its dev bypass to provide that**:`TRADEMARK_MCP_AUTH_MODE=token` runs it with a mandatory scoped
141
141
  access key and no auth proxy — loopback only, and refused outright alongside
142
142
  `TRADEMARK_MCP_AUTH_DISABLED`, which authenticates nobody. Mint the key with
143
- `mint-token.mjs --scope ops --sub portal --verbs start_run,stop_run --accounts aurora`, or let
143
+ `mint-token.mjs --scope ops --sub portal --verbs start_run,stop_run --accounts foxglade`, or let
144
144
  `npx clearotron start` mint one in memory at every start and never write it down.
145
145
 
146
146
  ## Putting your own login provider in front
package/docs/README.md CHANGED
@@ -38,6 +38,7 @@ the vendor flags approximate is UNKNOWN, never a number.
38
38
  | [`PORTAL.md`](PORTAL.md) | The portal: what it serves, and who sees what |
39
39
  | [`CLIENT-MCP.md`](CLIENT-MCP.md) | Publishing a connector your customers sign in to, and how their access is scoped. To connect *your own* app to *your own* runs, use [`../mcp-server/CONNECT.md`](../mcp-server/CONNECT.md) instead |
40
40
  | [`E2E.md`](E2E.md) | Proving a deployment works end to end |
41
+ | [`GLOSSARY.md`](GLOSSARY.md) | The words this codebase uses in a particular way — one line each, for a contributor meeting them for the first time |
41
42
  | [`SECURITY.md`](SECURITY.md) | The security envelope — what protects what, and where it is enforced in code. To report a vulnerability, use [`../SECURITY.md`](../SECURITY.md) |
42
43
 
43
44
  Access control is stated once, in [`SECURITY.md`](SECURITY.md): who may see which runs, what an unset
@@ -76,10 +77,11 @@ The other half is structural: it fails on any undeclared identity inside a matte
76
77
  which is the half that catches something new. Without the private table the guard runs on synthetic
77
78
  sentinels: the machinery is exercised and there is nothing real to find.
78
79
 
79
- **Demo clients are synthetic.** A Generic default (`generic`) plus three inventions — `aurora`
80
- (gaming), `zephyr` (functional drinks), `petcary` (animal health). They exercise the per-client
81
- machinery and the test suite. Real client bundles load at runtime from a private store
82
- (`CLEAROTRON_CUSTOMERS_DIR`) and are never committed here.
80
+ **Demo clients are synthetic.** The published package carries a Generic default (`generic`) and one
81
+ demo brand owner. The repository holds three further invented accounts — gaming, functional drinks and
82
+ animal health which exercise the per-client machinery and the test suite and are never published.
83
+ Real client bundles load at runtime from a private store (`CLEAROTRON_CUSTOMERS_DIR`) and are never
84
+ committed here.
83
85
 
84
86
  **Real third-party names are deliberate.** Registers, marketplaces, regulators, research providers and
85
87
  the parties to published decisions are named throughout the code, tests and fixtures — as facts about
package/docs/SECURITY.md CHANGED
@@ -39,6 +39,23 @@ Exactly two roles exist: **staff**, admitted by an email-domain rule (`PORTAL_ST
39
39
  created in the guest-list file by whoever administers the box — `npm run grant` is the editor for it —
40
40
  never from a browser. The file is re-read per request, so a grant lands without a restart.
41
41
 
42
+ **A staff domain is a grant to everyone at that domain, so nothing derives one for you.** Every address
43
+ at a listed domain that gets past the sign-in door is staff, and staff sees every brand owner on the
44
+ instance. The install therefore derives `PORTAL_STAFF_DOMAINS` only from a sign-in address that names no
45
+ second person — the `<account>@localhost` form a single-user machine gets by default. Given a real
46
+ address it states the rule it would create, in the words the People & access screen will later use,
47
+ and refuses to write it: set `PORTAL_STAFF_DOMAINS` yourself, or answer the question `clearotron install`
48
+ asks. A webmail or shared provider is refused outright, because a rule built from one admits the public.
49
+ The People & access screen names the setting the rule came from and the file it is written in, so a
50
+ rule can be traced and undone by whoever finds it.
51
+
52
+ **That check is on the DERIVATION, not on the value.** A `PORTAL_STAFF_DOMAINS` you set yourself is
53
+ taken as written and never classified: `PORTAL_STAFF_DOMAINS=gmail.com` is accepted, and it admits
54
+ everyone at that provider who gets past the sign-in door. This is deliberate — a value someone typed is
55
+ a decision already taken, and the defect being fixed was a rule created with nobody asked — but it means
56
+ the protection is against the accident and not against the configuration. Read what you set, or set
57
+ nothing and answer the question the install asks.
58
+
42
59
 
43
60
  - **On the proxy door**, every HTTP request re-validates a JWT from the fronting auth proxy. The
44
61
  proxy is yours to choose — issuer, JWKS URL, claim and header are config
@@ -3,18 +3,30 @@
3
3
  > Part of the architecture pack (`docs/architecture/`). The driver's module tree and the headless
4
4
  > integrator contract are in [`driver/README.md`](../../driver/README.md).
5
5
 
6
- The clearotron driver turns a plain-language clearance request into a delivered, lawyer-vetted
7
- preliminary trademark clearance report. One matter in, one decision out — with the investigation,
8
- the reasoning, the challenge, and the paper trail in between run by deterministic code that treats
9
- the model as a reasoning step, never as the orchestrator.
6
+ The clearotron driver turns a clearance request into a delivered preliminary trademark clearance
7
+ report. One matter in, one decision out — with the investigation, the reasoning, the challenge, and
8
+ the paper trail in between run by deterministic code that treats the model as a reasoning step,
9
+ never as the orchestrator.
10
10
 
11
11
  ## What it does
12
12
 
13
- A lawyer forwards a clearance request by email. The system resolves the matter (mark, client,
14
- scope, deadline), investigates worldwide trademark registers and the live marketplace, in the
15
- variations a lawyer would try — reasons the risk in the client's own framework, challenges its own
16
- draft twice, and delivers a client-formatted report with every factual claim traceable to a fetched
17
- source record. A lawyer vets the result, then it moves.
13
+ **Who this is for.** Anyone who needs to know whether a name is free to use and is willing to run the
14
+ search themselves: a lawyer, a brand team, or an individual clearing their own mark. You need three
15
+ self-serve accounts: a reasoning CLI, a register, and web research.
16
+
17
+ **What you get.** Four searches at different depths. A knockout screens up to eight names in minutes.
18
+ A global preliminary, a multi-country focus, or a full country search takes one name deeper, and the
19
+ full country search adds case law. Each delivers a written report with the records behind every
20
+ finding. What you do with it is your call.
21
+
22
+ The four searches are declared in `driver/products.mjs`, and that declaration is what the rest of the
23
+ system reads: a knockout screens up to eight names and skips case law, and only the full country
24
+ search turns case law on. A knockout takes 5 to 10 minutes; the other three take 1.5 to 2.5 hours.
25
+
26
+ Under any of them the system resolves the matter (mark, scope, deadline), investigates the trademark
27
+ registers and the live marketplace in the variations a searcher would try, reasons the risk in the
28
+ matter's own framework, challenges its own draft twice, and delivers a report with every factual
29
+ claim traceable to a fetched source record.
18
30
 
19
31
  The driver is the machine that runs that whole distance. It is **not an agent**: it is a plain
20
32
  Node.js process, launched by systemd, that executes a fixed pipeline of stages. Each stage that