clearotron 0.2.2 → 0.2.3

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 (118) hide show
  1. package/.env.example +28 -0
  2. package/.nvmrc +1 -1
  3. package/INSTALL.md +13 -11
  4. package/README.md +11 -16
  5. package/THIRD-PARTY-NOTICES.md +1 -1
  6. package/bin/clearotron.mjs +22 -0
  7. package/bin/example.mjs +4 -0
  8. package/bin/onboard.mjs +195 -19
  9. package/bin/start.mjs +154 -8
  10. package/build-info.json +2 -2
  11. package/demo/full-country-search/run/_driver/framework.json +1 -1
  12. package/demo/global-preliminary-search/run/_driver/framework.json +1 -1
  13. package/demo/knockout-search/run/_driver/framework.json +1 -1
  14. package/demo/multi-country-focus-search/run/_driver/framework.json +1 -1
  15. package/docs/E2E.md +5 -5
  16. package/docs/PORTAL.md +2 -2
  17. package/docs/README.md +5 -4
  18. package/docs/SECURITY.md +17 -0
  19. package/docs/architecture/04-configuration-reference.md +2 -0
  20. package/docs/architecture/05-config-governance.md +14 -0
  21. package/docs/architecture/05-customer-profiles.md +2 -2
  22. package/docs/configuration.md +4 -3
  23. package/driver/CHANGELOG.md +56 -0
  24. package/driver/common-law-receipts.mjs +1 -1
  25. package/driver/config-staleness.mjs +1 -1
  26. package/driver/dev-portal.mjs +2 -2
  27. package/driver/driver.config.mjs +1 -1
  28. package/driver/engine/cli-version.mjs +103 -0
  29. package/driver/enqueue-schema.mjs +8 -8
  30. package/driver/feedback-store.mjs +1 -1
  31. package/driver/findings-model.mjs +52 -2
  32. package/driver/flag-snapshot.mjs +20 -0
  33. package/driver/framework.mjs +1 -1
  34. package/driver/gateway.mjs +18 -1
  35. package/driver/package.json +3 -3
  36. package/driver/party-facts.mjs +2 -2
  37. package/driver/phase0.mjs +1 -1
  38. package/driver/pipeline.mjs +5 -5
  39. package/driver/plain-register.mjs +120 -0
  40. package/driver/portal-access.mjs +1 -1
  41. package/driver/portal-config-view.mjs +40 -1
  42. package/driver/portal-families.mjs +1 -1
  43. package/driver/portal-report.mjs +1 -1
  44. package/driver/portal-service.mjs +26 -7
  45. package/driver/portal-upstream.mjs +2 -2
  46. package/driver/predelivery-lint.mjs +121 -1
  47. package/driver/profile-page.html +10 -2
  48. package/driver/profile-service.mjs +4 -4
  49. package/driver/profiles/README.md +3 -3
  50. package/driver/profiles.mjs +144 -12
  51. package/driver/publish/index.mjs +2 -2
  52. package/driver/publish/knockout.mjs +7 -1
  53. package/driver/publish/render-knockout.mjs +453 -118
  54. package/driver/publish/render.mjs +2 -2
  55. package/driver/reference-strip-signatures.mjs +20 -0
  56. package/driver/search-policy.mjs +2 -2
  57. package/driver/skills/blind-frame/SKILL.md +1 -1
  58. package/driver/skills/knockout-assess/SKILL.md +84 -0
  59. package/driver/skills/matter-frame/SKILL.md +6 -6
  60. package/driver/skills/matter-frame/watchlist-reference.md +2 -2
  61. package/driver/skills/prelim-common-law/SKILL.md +1 -1
  62. package/driver/skills/prelim-search/SKILL.md +1 -1
  63. package/driver/skills/prelim-search/report-prose.md +82 -0
  64. package/driver/skills/prelim-search/risk-framework-demo.manifest.json +1 -1
  65. package/driver/skills/prelim-search/risk-framework-triage.manifest.json +1 -1
  66. package/driver/skills/prelim-search/risk-framework.manifest.json +1 -1
  67. package/driver/skills/prelim-search/synthesis-rules.md +1 -1
  68. package/driver/skills/prelim-variants/SKILL.md +3 -3
  69. package/driver/stages.mjs +4 -4
  70. package/driver/suite-census.json +134 -38
  71. package/driver/unit-inventory.mjs +184 -27
  72. package/driver/verify.mjs +1 -1
  73. package/driver/whatif-memo-run.mjs +26 -4
  74. package/driver/whatif-memo.mjs +19 -1
  75. package/examples/README.md +2 -2
  76. package/examples/grants.example.json +6 -7
  77. package/mcp-server/CHANGELOG.md +2 -0
  78. package/mcp-server/lib/scrub.mjs +1 -1
  79. package/mcp-server/lib/whatif.mjs +2 -2
  80. package/mcp-server/package.json +1 -1
  81. package/mcp-server/packs/ops/EXAMPLES.md +2 -2
  82. package/mcp-server/server.mjs +1 -1
  83. package/package.json +28 -10
  84. package/portal-ui/dist/assets/{index-Ct3iQtZ2.css → index-1ziUJX1E.css} +43 -2
  85. package/portal-ui/dist/assets/{index-BUGgiYtF.js → index-CEYvXdqH.js} +226 -49
  86. package/portal-ui/dist/index.html +2 -2
  87. package/portal-ui/package.json +1 -1
  88. package/providers/_shared/term-shape.mjs +1 -1
  89. package/providers/oauth-mcp-bridge/CHANGELOG.md +2 -0
  90. package/providers/oauth-mcp-bridge/package.json +1 -1
  91. package/providers/uspto-local/src/index-store.js +2 -2
  92. package/scripts/citation-line-check.mjs +121 -2
  93. package/scripts/env-classify.mjs +67 -3
  94. package/scripts/live-surface-check.mjs +38 -1
  95. package/scripts/nightly-notice.mjs +0 -2
  96. package/scripts/preinstall-node-check.mjs +24 -0
  97. package/scripts/strip-tracker-citations.mjs +29 -6
  98. package/scripts/test-run.mjs +16 -0
  99. package/shared/anon-overlay.mjs +3 -3
  100. package/shared/customer-store.mjs +1 -1
  101. package/shared/invocation.mjs +28 -0
  102. package/shared/names-in-force.mjs +2 -0
  103. package/shared/node-floor.mjs +97 -0
  104. package/shared/staff-domain.mjs +154 -0
  105. package/shared/store-in-repo.mjs +23 -2
  106. package/skills/clearotron-ops/SKILL.md +1 -1
  107. package/driver/recipes/README.md +0 -49
  108. package/driver/skills/prelim-search/risk-framework-aurora.manifest.json +0 -19
  109. package/driver/skills/prelim-search/risk-framework-aurora.md +0 -77
  110. package/driver/skills/prelim-search/risk-framework-zephyr.manifest.json +0 -14
  111. package/driver/skills/prelim-search/risk-framework-zephyr.md +0 -33
  112. package/driver/skills/prelim-search/worked-examples-aurora.md +0 -10
  113. package/driver/skills/prelim-search/worked-examples-zephyr.md +0 -10
  114. package/scripts/ai-page-render-check.mjs +0 -532
  115. package/scripts/clearances-render-check.mjs +0 -480
  116. package/scripts/composer-render-check.mjs +0 -1027
  117. package/scripts/home-render-check.mjs +0 -526
  118. package/scripts/portal-lifecycle-check.mjs +0 -639
package/bin/start.mjs CHANGED
@@ -130,6 +130,9 @@ 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";
133
136
 
134
137
  const REPO = join(dirname(fileURLToPath(import.meta.url)), "..");
135
138
  const ENV_PATH = envLocalPath({ repoRoot: REPO }); // resolved, never composed: one resolver, so moving this file later is one line
@@ -366,6 +369,11 @@ export function installPaths(base) {
366
369
  };
367
370
  }
368
371
 
372
+ /** A staff rule this command will not build. Carries the sentence the operator reads, and why. */
373
+ export class StaffDomainRefused extends Error {
374
+ constructor(domain, message) { super(message); this.name = "StaffDomainRefused"; this.domain = domain; }
375
+ }
376
+
369
377
  /**
370
378
  * The one address that signs in, and the staff domain derived from it.
371
379
  *
@@ -374,10 +382,61 @@ export function installPaths(base) {
374
382
  * it safe here and would not be safe on a hosted instance. Without it the sign-in succeeds and every
375
383
  * page 403s, because signing in is not being enrolled (portal-access.mjs decides that, and it is
376
384
  * deliberately blind to which door you came through).
385
+ *
386
+ * ── AND THAT PARAGRAPH IS ONLY TRUE OF `<account>@localhost` ─────────────────────────────────────────
387
+ *
388
+ * "admits exactly that address and nobody else" is a property of the DOMAIN, not of the mode. It holds
389
+ * for `localhost`. It does not hold for a domain other people have addresses at: the rule this line
390
+ * produces from `alex@a-firm.example-tld` says *anyone at that domain*, the settings page then reports
391
+ * it back in those words, and on a deployment where identity arrives from an external login system the
392
+ * domain is the whole of the check. Nobody was asked, and there is nothing on the box recording that a
393
+ * grant was made.
394
+ *
395
+ * So the derivation now refuses the domains that can never be a staff rule — a webmail provider, a
396
+ * documentation domain — and hands anything wider than one machine back to the caller to state and
397
+ * confirm. `shared/staff-domain.mjs` is the one classifier, shared with the setup wizard, so the
398
+ * question the wizard asks and the rule this command builds cannot drift apart.
399
+ *
400
+ * An UNPARSEABLE address still returns `""`, and that is not the same answer as a refusal: nothing was
401
+ * classified, so nothing was decided. The caller already refuses a non-address one line earlier.
377
402
  */
378
403
  export function staffDomainFor(email) {
379
404
  const at = String(email ?? "").lastIndexOf("@");
380
- return at > 0 ? String(email).slice(at + 1).toLowerCase() : "";
405
+ const domain = at > 0 ? String(email).slice(at + 1).toLowerCase() : "";
406
+ if (!domain) return "";
407
+ const refusal = staffDomainRefusal(domain);
408
+ if (refusal) throw new StaffDomainRefused(domain, refusal);
409
+ return domain;
410
+ }
411
+
412
+ /**
413
+ * What to say when the derived domain is a real one — wider than this machine, and never assumed.
414
+ *
415
+ * A REFUSAL RATHER THAN A PROMPT, and the reason is that this command has no reader. `start` runs from
416
+ * a script, from `--background`, and from a service manager as often as from a terminal; a question
417
+ * asked there either hangs or is answered by whatever happens to be on stdin. Every other decision on
418
+ * this boundary already refuses instead — an unset access file, a multi-`@` identity, local mode off
419
+ * loopback — so this is the idiom the rest of the door speaks.
420
+ *
421
+ * The consent itself lives where a person is definitionally present: `clearotron install` asks for the
422
+ * address, shows this same sentence, and writes the answer down as an explicit `PORTAL_STAFF_DOMAINS`.
423
+ * After that the `||` above this call short-circuits and the derivation is never reached again.
424
+ */
425
+ export function wideStaffDomainRefusal({ user, domain, envPath }) {
426
+ return `${user} would make every address at ${domain} an administrator of this install.\n`
427
+ + "\n"
428
+ + ` The rule that would be written is: ${staffGrantSentence(domain, { staffLabel: `${BRAND.name} staff` })}\n`
429
+ + "\n"
430
+ + " That is a grant to a group, and this command will not make one on your behalf. Pick the one\n"
431
+ + " that is true here:\n"
432
+ + "\n"
433
+ + ` · only you use this machine — start without --user, or use ${String(user).slice(0, String(user).lastIndexOf("@"))}@localhost.\n`
434
+ + ` The rule is then this machine and nobody else, and nothing is granted to anyone.\n`
435
+ + ` · your colleagues at ${domain} should all be administrators — say so once, in writing:\n`
436
+ + ` PORTAL_STAFF_DOMAINS=${domain}\n`
437
+ + ` in ${envPath}, or in the environment. The settings page then names that file as where the\n`
438
+ + " rule came from, so whoever reads it later can undo it.\n"
439
+ + ` · run \`${invocationPrefix()}clearotron install\`, which asks for the address and this question with it.`;
381
440
  }
382
441
 
383
442
  /**
@@ -777,6 +836,10 @@ if (isMain) {
777
836
  // bound anywhere else in any mode. Sign-in is untouched — the demo signs in like any first start, and
778
837
  // the portal mints and prints its passphrase exactly as it does for a real one.
779
838
  const DEMO = argv.includes("--demo");
839
+ // THE DEMO BRINGS ITS OWN ACCOUNT. A fresh install resolves `generic` and nothing else (owner
840
+ // ruling, 2026-09-08), so the demo account is refused from the roster unless somebody asked for it.
841
+ // Asked here, once and visibly, rather than at each site that happens to read a roster.
842
+ if (DEMO) process.env.CLEAROTRON_DEMO_PROFILES ??= "1";
780
843
  // ── `--port` MOVES EVERY DOOR IT OPENS (tracker issue 166) ───────────────────────────────────────
781
844
  //
782
845
  // It used to move ONE of the three. `resolvePorts` reads three independent variables with three
@@ -812,16 +875,47 @@ if (isMain) {
812
875
  run: (cmd, args) => spawnSync(cmd, args, { cwd: REPO, stdio: "inherit", shell: process.platform === "win32" }).status ?? 1,
813
876
  });
814
877
 
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.
878
+ // The ADDRESS is not asked for here: a local install has one user, this machine already knows their
879
+ // name, and the address never leaves the machine — `--user` is there for a reader who wants their real
880
+ // one, and whatever is resolved here is written to `.env` so the question is never put twice. What IS
881
+ // asked for, and used not to be, is the staff rule the address implies once it is a real one; that is
882
+ // the block below the address, and `clearotron install` is where the question is actually put.
818
883
  let whoami = "user";
819
884
  try { whoami = userInfo().username || "user"; } catch { /* a container with no passwd entry */ }
820
885
  // In a demo the address is the demo's own and is never written anywhere: see the DEMO block above.
821
886
  const user = String(flag("--user", DEMO ? "demo@localhost" : (process.env.PORTAL_LOCAL_USER || `${whoami}@localhost`))).trim().toLowerCase();
822
887
  if (!user.includes("@") || user.indexOf("@") !== user.lastIndexOf("@"))
823
888
  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);
889
+ // ── THE STAFF RULE THIS ADDRESS IMPLIES, STATED BEFORE IT IS WRITTEN ──────────────────────────────
890
+ //
891
+ // An explicit `PORTAL_STAFF_DOMAINS` is a decision somebody already made in writing, and it wins
892
+ // untouched — including a domain the classifier would otherwise refuse, because an operator who
893
+ // typed it has said what they mean and this command does not overrule that.
894
+ //
895
+ // Absent one, the rule is DERIVED from the single address, and that derivation is the defect this
896
+ // block exists to close. Three outcomes:
897
+ //
898
+ // `localhost` (or any bare hostname) — one machine, no second person, nothing to ask. Silent, as
899
+ // it has always been. This is the default path and the only
900
+ // one a laptop ever reaches.
901
+ // a webmail or documentation domain — refused by `staffDomainFor` itself; there is no yes that
902
+ // makes it right.
903
+ // any other real domain — refused HERE, with the rule quoted in the words the
904
+ // settings page uses and the three ways out named.
905
+ //
906
+ // BEFORE THIS RUN CHANGES THE BOX, deliberately — it sits above the state-written divide further
907
+ // down, with the units gate, the auth-mode gate and the port probe. A refused start has written no
908
+ // `.env`, minted no secret and placed no unit, so the reader's only question — is my install
909
+ // half-made — has one answer.
910
+ let staffDomains = process.env.PORTAL_STAFF_DOMAINS;
911
+ if (!staffDomains) {
912
+ let derived = "";
913
+ try { derived = staffDomainFor(user); }
914
+ catch (e) { fatal(e.message); }
915
+ if (derived && classifyStaffDomain(derived) === "wide")
916
+ fatal(wideStaffDomainRefusal({ user, domain: derived, envPath: ENV_PATH }));
917
+ staffDomains = derived;
918
+ }
825
919
 
826
920
  say("");
827
921
  say(` ${BRAND.name} ${BRAND.product.toLowerCase()} — local install`);
@@ -1574,10 +1668,40 @@ if (isMain) {
1574
1668
  // sequence rather than a race between the terminal's signal and ours. It also means the group
1575
1669
  // kill below reaches anything a child spawns, not just the child.
1576
1670
  detached: true,
1577
- stdio: ["ignore", "inherit", "inherit"],
1671
+ // STDERR IS TEED RATHER THAN INHERITED, AND ONLY STDERR.
1672
+ //
1673
+ // These children announce a refusal as one FATAL line on stderr and exit 1. With `inherit` the
1674
+ // parent holds no copy, so the failure message below could only point at output it had never seen —
1675
+ // and on one report that output never reached the reader's terminal at all. It said "its own output
1676
+ // above says why" about a line that was not above, which is worse than saying nothing: it sends
1677
+ // somebody scrolling for a sentence they will not find.
1678
+ //
1679
+ // stdout stays inherited. It carries no refusal — measured: on a refusing door stdout is empty and
1680
+ // stderr holds the whole of it — and leaving it alone keeps the child's terminal detection and any
1681
+ // progress rendering intact. Piping a stream costs the child its TTY, so only the stream that has to
1682
+ // be read is piped.
1683
+ stdio: ["ignore", "inherit", "pipe"],
1578
1684
  env: { ...process.env, ...env },
1579
1685
  });
1580
- const rec = { name, script, child, alive: true };
1686
+ // The last lines of that child's stderr, forwarded on as they arrive so nothing is delayed or
1687
+ // swallowed, and kept so the failure can QUOTE them rather than refer to them.
1688
+ const tail = [];
1689
+ // A CHUNK IS NOT A LINE. `data` arrives on whatever boundary the pipe gives us, so a stream split
1690
+ // mid-sentence would push two half-lines and the quoted last words would reach the reader cut in
1691
+ // half — on precisely the message that exists because they could not see the original. The partial
1692
+ // remainder is held over and completed by the next chunk; whatever is left when the stream ends is
1693
+ // flushed, because a process that dies mid-line still said the thing it was saying.
1694
+ let pending = "";
1695
+ const keep = (line) => { if (line.trim()) tail.push(line); while (tail.length > 12) tail.shift(); };
1696
+ child.stderr?.setEncoding("utf8");
1697
+ child.stderr?.on("data", (chunk) => {
1698
+ process.stderr.write(chunk);
1699
+ const parts = (pending + String(chunk)).split("\n");
1700
+ pending = parts.pop() ?? "";
1701
+ for (const line of parts) keep(line);
1702
+ });
1703
+ child.stderr?.on("end", () => { keep(pending); pending = ""; });
1704
+ const rec = { name, script, child, alive: true, tail };
1581
1705
  children.push(rec);
1582
1706
  child.on("exit", (code, signal) => {
1583
1707
  rec.alive = false;
@@ -1590,7 +1714,7 @@ if (isMain) {
1590
1714
  + ` but nothing is draining the queue now. Start one with:\n node ${script} --watch\n`);
1591
1715
  return;
1592
1716
  }
1593
- err(`\nstart: ${name} (${script}) exited ${signal ? `on ${signal}` : `with code ${code}`} — its own output above says why. Stopping the rest.\n`);
1717
+ err(childExitReport({ name, script, code, signal, tail }));
1594
1718
  void shutdown(1);
1595
1719
  });
1596
1720
  child.on("error", (e) => {
@@ -1861,3 +1985,25 @@ if (isMain) {
1861
1985
  say(" — same product, managed by systemd, and it survives logout.");
1862
1986
  say("");
1863
1987
  }
1988
+
1989
+ /**
1990
+ * What a reader is told when a child this install cannot run without exits.
1991
+ *
1992
+ * QUOTED, NOT POINTED AT. This used to end "its own output above says why" while the parent inherited
1993
+ * the child's stderr and therefore held no copy of it. On one report that output never reached the
1994
+ * reader's terminal at all, so the sentence sent somebody scrolling for a line that was not there —
1995
+ * worse than saying nothing, because it reads as a working instruction.
1996
+ *
1997
+ * An empty tail is reported as an empty tail. A child that exits silently is a finding about the child,
1998
+ * and printing a heading with nothing under it would hide exactly that.
1999
+ *
2000
+ * PURE, and exported, so this can be driven without starting a supervisor — the failure path of a
2001
+ * process manager is the one nobody exercises by hand.
2002
+ */
2003
+ export function childExitReport({ name, script, code, signal, tail = [] }) {
2004
+ const how = signal ? `on ${signal}` : `with code ${code}`;
2005
+ const said = tail.length
2006
+ ? `Its last line${tail.length > 1 ? "s" : ""}:\n${tail.map((l) => ` ${l}`).join("\n")}\n`
2007
+ : "It exited without printing anything, which is itself the thing to report.\n";
2008
+ return `\nstart: ${name} (${script}) exited ${how}. Stopping the rest.\n${said}`;
2009
+ }
package/build-info.json CHANGED
@@ -1,4 +1,4 @@
1
1
  {
2
- "commit": "66760dc4f4b83293e80c8422e0633cdee2aabb41",
3
- "version": "0.2.2"
2
+ "commit": "5c8eca66a5f226dde9e1d01affd63da4eda61bbd",
3
+ "version": "0.2.3"
4
4
  }
@@ -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
 
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
@@ -76,10 +76,11 @@ The other half is structural: it fails on any undeclared identity inside a matte
76
76
  which is the half that catches something new. Without the private table the guard runs on synthetic
77
77
  sentinels: the machinery is exercised and there is nothing real to find.
78
78
 
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.
79
+ **Demo clients are synthetic.** The published package carries a Generic default (`generic`) and one
80
+ demo brand owner. The repository holds three further invented accounts — gaming, functional drinks and
81
+ animal health which exercise the per-client machinery and the test suite and are never published.
82
+ Real client bundles load at runtime from a private store (`CLEAROTRON_CUSTOMERS_DIR`) and are never
83
+ committed here.
83
84
 
84
85
  **Real third-party names are deliberate.** Registers, marketplaces, regulators, research providers and
85
86
  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
@@ -283,6 +283,8 @@ because the rule is about what PRODUCT CODE reads, not about what a run reads.
283
283
  |---|---|---|
284
284
  | `DEMO_PORT` | `18900` | Port `npx clearotron demo` serves the replayed report on. `--port` overrides it. |
285
285
  | `CLEAROTRON_DEMO` | unset | `1` puts this install in the DEMO posture. **Ordering is real**: the four products are listed and orderable, the form, the plan and the confirmation are the product's own, and the confirmation resolves to a finished report that already exists rather than dispatching — no engine turn, no register call, no queue entry, no run directory (owner ruling 2026-08-31, superseding the greyed-control ruling of the same day). A product the demo carries no finished report for refuses and names which one. It also re-aims two boot warnings written for an operator of a real deployment at the visitor who is not one, from one place (`driver/demo-posture.mjs`). **Set by `npx clearotron demo`, not by an operator** — it is passed explicitly to the two processes that have a reason to know (the portal and the MCP door; the worker is not told, because a demo never queues anything for it to drain), and those run with `CLEAROTRON_NO_ENV_FILE=1`, so a stray `.env` can neither put a live install into demo mode nor take a demo out of one. Anything but the literal `1` is not a demo. Replaces `PORTAL_DEMO`, which named only one of the processes that has to know. |
286
+ | `CLEAROTRON_TEST_FIXTURE_PROFILES` | unset | `1` makes the profile loader return the three suite fixtures, which are refused from every roster otherwise. Set by `scripts/test-run.mjs`, never by an operator; an explicit `includeTestFixtures` argument beats it. Effect class `harness`; the full contract is its row in `.env.example`. |
287
+ | `CLEAROTRON_DEMO_PROFILES` | unset | `1` makes the profile loader return the bundled demo account, which a fresh install does not resolve (owner ruling 2026-09-08). Set by `clearotron demo`, `start --demo` and the suite runner, never by an operator. The gate is on the bundled layer, so a deployment's own configured store keeps its `demoData` accounts either way. Effect class `harness`; the full contract is its row in `.env.example`. |
286
288
  | `PORTAL_LOCAL_CREDENTIAL` | `~/.cordillera/portal-local-credential.json` | Where local sign-in keeps its passphrase DIGEST. `npx clearotron demo` points it inside the demo's own base directory, so a demo mints its own passphrase instead of inheriting a digest minted for another address — and removing the demo stays one `rm -rf`. |
287
289
  | `PORTAL_LOCAL_PASSPHRASE` | unset | **NEVER set this in a file.** An internal one-shot handoff, not an operator control: on a first FOREGROUND start the supervisor mints the passphrase and hands it to the portal it spawns *at the spawn call*, so the closing summary can print the value beside the address rather than sending a first-time reader back into eleven startup log lines for the one value in this product that cannot be read back. It is deliberately absent from the composed child environments, because that composition is what `--background` writes into the units' env file — a passphrase there would be a permanent plaintext copy on disk and the product's own sentence, "it is stored only as a digest", would stop being true. Setting it in any env file recreates exactly that. Lost passphrase: `clearotron passphrase --reset`. |
288
290
  | `PORTAL_URL` | `http://127.0.0.1:18802`, or built from `PORTAL_SERVICE_HOST`/`PORTAL_SERVICE_PORT` | Where the deploy tick's live-surface check expects to reach the portal. |
@@ -303,6 +303,20 @@ Staff MCP: `TRADEMARK_MCP_HTTP_PORT` (18790), `TRADEMARK_MCP_HTTP_HOST`,
303
303
  `TRADEMARK_MCP_EMAIL_CLAIM`, `TRADEMARK_MCP_AUTH_HEADER`, `TRADEMARK_MCP_URL`,
304
304
  `TRADEMARK_MCP_AUTH_MODE` (see below),
305
305
  `TRADEMARK_MCP_AUTH_DISABLED` / `TRADEMARK_MCP_DEV` (dev seams — never set in prod).
306
+
307
+ `TRADEMARK_MCP_KEY_SOCKET` is the local key door, and it is a PATH rather than a port on purpose. This
308
+ interface serves two populations that authenticate differently: people arriving through a tunnel, who
309
+ prove themselves with a proxy identity, and programs on the same machine holding a scoped access key,
310
+ which can never produce one. "Only accept a key from loopback" does not separate them — the tunnel daemon
311
+ runs on the same machine, so a request forwarded from the internet and one from the local portal arrive
312
+ with the same peer address. A tunnel forwards to a port and cannot reach a unix socket, so the transport
313
+ is the discriminator and the two doors are separate objects with opposite rules.
314
+
315
+ Unset means no key door, and a deployment gets exactly the door it had before. The socket is created
316
+ `0660` — owner and group only — with the mode set after `listen`, because `listen` creates the file and
317
+ the umask decides what it starts as. It refuses to open at all with no grants file, alongside
318
+ `TRADEMARK_MCP_AUTH_DISABLED`, or when `TRADEMARK_MCP_AUTH_MODE` is `token`: that mode makes the NETWORK
319
+ door take a key too, which is the thing this exists to prevent. Read by `mcp-server/key-socket.mjs`.
306
320
  Client MCP: `CLIENT_MCP_HTTP_PORT` (code default 18811, matching the deployed unit; it was 18795,
307
321
  which collided with a co-hosted warm-MCP block, so keep any unit override that names a port),
308
322
  `CLIENT_MCP_HTTP_HOST`, `CLIENT_MCP_ALLOWED_HOSTS`,
@@ -3,8 +3,8 @@
3
3
  > Part of the architecture pack (`docs/architecture/`). The driver's module tree and the headless
4
4
  > integrator contract are in [`driver/README.md`](../../driver/README.md).
5
5
  > This chapter describes the profile *mechanism*. Real customer bundles load from an external store
6
- > via `CLEAROTRON_CUSTOMERS_DIR` and no customer is named here; the repository ships `generic` plus the
7
- > synthetic demo customers `aurora`, `zephyr` and `petcary` — see
6
+ > via `CLEAROTRON_CUSTOMERS_DIR` and no customer is named here; the package ships `generic` and the demo
7
+ > brand owner, and the repository holds three further synthetic profiles for the test suite — see
8
8
  > [`driver/profiles/`](../../driver/profiles/).
9
9
 
10
10
  One engine, never forked — three layers. The reasoning core is shared by every client; the client
@@ -115,9 +115,10 @@ under that framework. Absent, the generic set applies.
115
115
 
116
116
  ## 3. Client profiles
117
117
 
118
- One JSON file per client under [`driver/profiles/`](../driver/profiles/). Four ship as working
119
- examples: `generic` (the Generic default) plus three synthetic demo clients `aurora` (gaming),
120
- `zephyr` (functional drinks) and `petcary` (animal health).
118
+ One JSON file per client under [`driver/profiles/`](../driver/profiles/). Two are published as working
119
+ examples: `generic` (the Generic default) and the demo brand owner. Three further synthetic profiles —
120
+ gaming, functional drinks and animal health — exist in the repository for the test suite and are left
121
+ out of the package.
121
122
 
122
123
  A job picks its profile by **forwarding domain** — `matchDomains[]`, exact host or dot-suffix. The
123
124
  applicant named in a request never selects a profile. Resolution happens once at run start and is
@@ -1,5 +1,61 @@
1
1
  # clearotron-driver
2
2
 
3
+ ## 0.2.3
4
+
5
+ ### Patch Changes
6
+
7
+ - 54ea03a: Fixed: The configuration check no longer reports a problem when the client connector's address is reached with its own token. Putting a single sign-on front before it is the client's choice, and running it token-based is supported. The check still says the sign-in audience was not compared against that address, so it never implies the two agree.
8
+ - 4b6be02: Fixed: A bundled risk framework now states where it came from in words a customer can read. The note used to carry a confidentiality marking, a filename for a document not included, an internal reference number and revision history. It says whose framework it is, who stands behind it and which revision, and nothing else.
9
+ - 63f916c: Fixed: A fresh install's brand-owner list now offers Generic and, with the demo, the demo account. It offered three of our test accounts as well, on the install route that clones the repository.
10
+ - 5d12baa: Fixed: A run record now says which version of the engine's command-line tool served it. A change in results can be traced to a tool upgrade rather than guessed at.
11
+
12
+ Fixed: A version the tool could not report is recorded as unreadable. An absent field could not be told apart from a probe that never ran.
13
+ - 888de5d: Fixed: The deployment check now says whether each scheduled job's timer is still armed. A timer-driven service reads "inactive" between runs and when its timer has been stopped. So a check that asked only about the service could report nothing wrong while the scheduled work had quietly stopped happening.
14
+ - 3a75e22: Fixed: A supplementary memo now succeeds on its first attempt. It cost two turns instead of one, and left a retried-stage mark on a report that had been delivered cleanly.
15
+
16
+ Fixed: A supplementary memo states the rating framework it was reasoned under. A report assessed under a customer's own framework said so; a memo written from it did not.
17
+ - fe22384: Fixed: Installing on a Node version the engine cannot run on now stops at once. It names the version you have, the version needed, and the command that fixes it. Before, the install finished and the first US register search failed with an error that never mentioned Node. The supported floor is Node 22.13 or newer.
18
+ - 74b2cdd: Fixed: Setup now asks which address signs in, instead of turning it into an access rule covering everyone who shares its email domain.
19
+
20
+ For operators: The People & access page now names the setting behind a staff rule, and the file to edit to undo it.
21
+ - 9053919: Fixed: Five things a first-time reader could not act on.
22
+
23
+ A screen that needs a brand owner chosen no longer points at the top left when the menu is collapsed and there is nothing there.
24
+
25
+ The profile editor's Save button now states the one thing that is blocking it. That reason was always there, but it looked the same as the states where nothing is wrong.
26
+
27
+ The default jurisdictions field now says what it does with what you type. Entries are checked against the list and kept, never refused. A region counts as one entry.
28
+
29
+ The trademark categories field explains what those numbers are, with examples, before naming the standard they come from.
30
+
31
+ The row of coloured ratings under "Risk framework in force" now says it belongs to that framework. It used to read as a list of several frameworks. Both editors say it the same way.
32
+ - fe22384: For operators: Clearotron runs on Node 22.13 or newer again, down from 22.19, because its HTTP client moves back to version 7. Anyone who upgraded Node for the last release has nothing to undo.
33
+ - bb899a7: New: A screening report now leads with the read. Each conflict shows its name, band, source and a one-sentence verdict. The paragraph arguing that verdict is one click away. Register filings appear as conflicts only where the reviewer rated them above the lowest band; the rest stay in the filings table.
34
+
35
+ New: What was asked is now at the top of a screening report, with any question about whether it was the right thing to ask. Both used to sit at the bottom.
36
+
37
+ New: The long per-name assessment now opens from the read. The engine has always written it and the page never showed it.
38
+
39
+ New: Register counts say what they counted in their column headers. Territories are named rather than printed as two-letter codes.
40
+
41
+ New: The lines a reader meets before opening anything are written in plain language. That is the summary, the basis line, each conflict's one sentence and the reviewer's notes. Legal vocabulary stays where the detail is.
42
+
43
+ Fixed: Reviewer notes no longer appear on a screening report exported to PDF. They are for the reviewing lawyer, and a report forwarded to a client used to carry them.
44
+ - 2a812d6: Fixed: The New clearance screen now has a **Start a search** button. The button that ran a search used to say "Review clearance", and people could not tell it was the way to begin.
45
+
46
+ Fixed: When a search cannot start yet, the screen says what is still missing, both on the form and beside the button. It used to grey the button out and give no reason at all.
47
+
48
+ Fixed: **Save as search** now confirms the save beside the button that was pressed, names what was saved, and links to it. It gave no sign at the point of the click.
49
+
50
+ Fixed: The unsaved-changes warning no longer fires on a search you have just saved. It compared the form against a blank one and nothing ever reset it.
51
+
52
+ Fixed: The message shown when no search engine is attached now names the setup command for the way you installed. It used to name the one that only works in a copy of the source.
53
+ - 58af0b7: Fixed: The settings page no longer shows the engine as healthy when the engine program cannot be found.
54
+
55
+ That page reports which engine is configured. The New clearance screen reports whether a search can start right now. When those two readings disagreed, neither screen said so.
56
+
57
+ An install could therefore show a green engine while no search would start, and nothing explained the gap. The settings page now names the disagreement and says what to do about it. Running `clearotron doctor` reports the same thing in the same words.
58
+
3
59
  ## 0.2.2
4
60
 
5
61
  ### Patch Changes
@@ -154,7 +154,7 @@ export function countMatrixCells(findingsContent) {
154
154
  * "丝绸与铁 / 席尔克": each alternate is its own search term with its own grid — the copper-conduit worker
155
155
  * keyed the split forms with full receipts while the validator demanded the compound key, 2026-06-12);
156
156
  * or WORD-BOUNDARY substring coverage (a fully-receipted variant covers a LONGER variant containing it
157
- * as a whole-word phrase — "Aurora Legends II: Oak & Ember" ⊃ "Oak & Ember"; a marketplace phrase
157
+ * as a whole-word phrase — "Foxglade Legends II: Oak & Ember" ⊃ "Oak & Ember"; a marketplace phrase
158
158
  * search for the short form surfaces every listing of the long form. Mid-word containment does NOT
159
159
  * count: "venzyy" ⊅ "venzy" — storefront search tokenizes, a doubled-letter variant is its own term).
160
160
  */
@@ -5,7 +5,7 @@
5
5
  // ── THE OUTCOME THIS EXISTS TO STOP ─────────────────────────────────────────────────────────────────
6
6
  //
7
7
  // After the F41 repair the owner restarted the worker, as instructed, and refreshed the portal. It
8
- // showed the BUNDLED DEMO ROSTER — aurora, petcary, zephyr — and not his own account.
8
+ // showed the BUNDLED DEMO ROSTER and not his own account.
9
9
  //
10
10
  // ~/.env repaired 10:51:16
11
11
  // worker 10:51:21 ← new config
@@ -301,7 +301,7 @@ async function loadOutbox(){const r=await(await fetch("/dev/outbox")).json();
301
301
  let REG=null;
302
302
  const esc=(s)=>String(s??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;");
303
303
  async function loadSearches(){
304
- const cust=$("#s_cust").value.trim()||"aurora";
304
+ const cust=$("#s_cust").value.trim()||"demo-brand-owner";
305
305
  try{
306
306
  const all=await(await fetch("/recipes")).json();
307
307
  if(all.error){$("#searches").innerHTML='<span class="err">'+esc(all.error)+'</span>';return;}
@@ -320,7 +320,7 @@ async function loadSearches(){
320
320
  function useProduct(k){const f=$("#f");f.product.value=k;$("#f_recipe").value="";f.mark.focus();}
321
321
  function useRecipe(cust,slug){const f=$("#f");f.product.value="";$("#f_recipe").value=slug;f.profile.value=cust;f.mark.focus();}
322
322
  async function composeSubmit(save){
323
- const cust=$("#s_cust").value.trim()||"aurora";
323
+ const cust=$("#s_cust").value.trim()||"demo-brand-owner";
324
324
  const fd=new FormData($("#sf"));
325
325
  const recipe={label:(fd.get("label")||"").trim(),base:fd.get("base")};
326
326
  if(fd.get("registerProbe")!=null)recipe.components={registerProbe:true};
@@ -248,7 +248,7 @@ export const config = {
248
248
  // 2026-07-19: these were joined against DRIVER_DIR, so the driver read framework manifests out of
249
249
  // its BUNDLED driver/skills while CLEAROTRON_INSTRUCTIONS_DIR pointed the agent at the config store. A
250
250
  // customer whose framework lives only in the config store therefore hard-failed at attachFramework
251
- // with `framework_manifest_missing` — the first Aurora Interactive run died there before any stage ran.
251
+ // with `framework_manifest_missing` — a test account's first run died there before any stage ran.
252
252
  get skillsRoot() { return dirname(this.skillsDir); },
253
253
 
254
254
  // ── Per-agent paths ───────────────────────────────────────────────────────