clearotron 0.2.4 → 0.3.0-beta.1

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 (235) hide show
  1. package/.env.example +13 -2
  2. package/CONTRIBUTING.md +1 -1
  3. package/INSTALL.md +62 -38
  4. package/bin/brandowner.mjs +18 -169
  5. package/bin/clearotron.mjs +3 -1
  6. package/bin/connect.mjs +28 -19
  7. package/bin/disconnect.mjs +3 -3
  8. package/bin/example.mjs +7 -7
  9. package/bin/framework-preflight.mjs +49 -0
  10. package/bin/grant.mjs +151 -93
  11. package/bin/onboard.mjs +220 -138
  12. package/bin/start.mjs +146 -137
  13. package/bin/stop.mjs +2 -2
  14. package/bin/update.mjs +1 -1
  15. package/build-info.json +2 -2
  16. package/docs/CLIENT-MCP.md +2 -2
  17. package/docs/E2E.md +12 -2
  18. package/docs/ONBOARDING.md +1 -1
  19. package/docs/PORTAL.md +14 -13
  20. package/docs/SECURITY.md +23 -24
  21. package/docs/architecture/04-configuration-reference.md +12 -5
  22. package/docs/architecture/05-config-governance.md +7 -7
  23. package/docs/architecture/07-quality-and-audit.md +1 -1
  24. package/docs/architecture/08-development-guide.md +5 -0
  25. package/docs/configuration.md +118 -0
  26. package/docs/decisions/0004-documentation-structure.md +2 -2
  27. package/docs/decisions/0006-what-the-public-repository-carries.md +2 -2
  28. package/driver/CHANGELOG.md +70 -0
  29. package/driver/ask-ledger.mjs +2 -2
  30. package/driver/cancel.mjs +27 -0
  31. package/driver/case-law-sources.mjs +3 -3
  32. package/driver/company-bundle.mjs +261 -0
  33. package/driver/compare.mjs +1 -1
  34. package/driver/compose-read.mjs +2 -2
  35. package/driver/config-inventory.mjs +2 -2
  36. package/driver/contract-audit.mjs +1 -1
  37. package/driver/contract-e3-backlog.mjs +1 -1
  38. package/driver/contract-vocabulary.mjs +1 -1
  39. package/driver/declination-call.mjs +1 -1
  40. package/driver/deliver-trigger.sh +3 -3
  41. package/driver/dev-portal.mjs +3 -1
  42. package/driver/digest-queue.mjs +1 -1
  43. package/driver/disposition-tool.mjs +1 -1
  44. package/driver/doc-constants.mjs +1 -1
  45. package/driver/drain-posture.mjs +2 -2
  46. package/driver/drainer-identity.mjs +1 -1
  47. package/driver/driver.config.mjs +44 -9
  48. package/driver/effective-scope.mjs +30 -1
  49. package/driver/effort-model.mjs +6 -6
  50. package/driver/engine/CONTRACT.md +2 -2
  51. package/driver/engine/anthropic-agent.mjs +11 -11
  52. package/driver/engine/jx-turn.mjs +1 -1
  53. package/driver/engine/mcp/gather-config.mjs +29 -4
  54. package/driver/engine/mcp/recording-server.mjs +73 -1
  55. package/driver/engine/openai-agent.mjs +1 -1
  56. package/driver/engine/probe.mjs +28 -4
  57. package/driver/enqueue-schema.mjs +23 -3
  58. package/driver/findings-model.mjs +2 -2
  59. package/driver/flag-snapshot.mjs +2 -2
  60. package/driver/floor-duty.mjs +2 -2
  61. package/driver/frame-diff-model.mjs +1 -1
  62. package/driver/framework-preflight.mjs +143 -0
  63. package/driver/gateway.mjs +9 -1
  64. package/driver/hit-list.mjs +1 -1
  65. package/driver/jx-lanes.mjs +1 -1
  66. package/driver/jx.mjs +1 -1
  67. package/driver/knockout-assess-record.mjs +1 -1
  68. package/driver/knockout-review-record.mjs +435 -0
  69. package/driver/order-probe.mjs +1 -1
  70. package/driver/outbox-backoff.mjs +2 -2
  71. package/driver/owner-use-check.mjs +2 -2
  72. package/driver/package.json +1 -1
  73. package/driver/pipeline-knockout.mjs +105 -8
  74. package/driver/pipeline.mjs +81 -30
  75. package/driver/plain-register.mjs +77 -3
  76. package/driver/portal-access.mjs +141 -74
  77. package/driver/portal-config-view.mjs +59 -70
  78. package/driver/portal-report.mjs +4 -4
  79. package/driver/portal-service.mjs +348 -141
  80. package/driver/portal-upstream.mjs +105 -17
  81. package/driver/predelivery-lint.mjs +43 -18
  82. package/driver/product-rows.mjs +1 -1
  83. package/driver/products.mjs +1 -1
  84. package/driver/profile-page.html +30 -5
  85. package/driver/profile-service.mjs +197 -26
  86. package/driver/profiles.mjs +48 -1
  87. package/driver/publish/index.mjs +31 -13
  88. package/driver/publish/knockout.mjs +9 -5
  89. package/driver/publish/office-record-links.mjs +189 -0
  90. package/driver/publish/parse.mjs +3 -3
  91. package/driver/publish/publish-inputs.mjs +26 -0
  92. package/driver/publish/render-knockout.mjs +42 -42
  93. package/driver/publish/render.mjs +29 -4
  94. package/driver/publish/report-data.mjs +2 -2
  95. package/driver/publish/seed-pool.mjs +1 -1
  96. package/driver/publish/templates/report.css +8 -8
  97. package/driver/publish/xlsx.mjs +49 -7
  98. package/driver/queue-watch-verdict.mjs +2 -2
  99. package/driver/recipe-service.mjs +1 -1
  100. package/driver/record-carry.mjs +1 -1
  101. package/driver/reference-score.mjs +1 -1
  102. package/driver/reference-strip-signatures.mjs +1 -1
  103. package/driver/register-availability.mjs +4 -3
  104. package/driver/register-count.mjs +3 -3
  105. package/driver/register-records.mjs +1 -1
  106. package/driver/repair-composers.mjs +1 -1
  107. package/driver/repairs.mjs +3 -3
  108. package/driver/replay-archive.mjs +1 -1
  109. package/driver/report-card-record.mjs +1 -1
  110. package/driver/result-noun-fields.mjs +5 -0
  111. package/driver/roster-verdict.mjs +48 -5
  112. package/driver/run-activity.mjs +1 -1
  113. package/driver/run-requirements.mjs +18 -5
  114. package/driver/runner.mjs +24 -15
  115. package/driver/search-policy.mjs +8 -8
  116. package/driver/senior-rights.mjs +1 -1
  117. package/driver/skills/prelim-search/delivery-contract.md +1 -1
  118. package/driver/skills/prelim-search/risk-framework-triage.md +10 -7
  119. package/driver/stages-knockout.mjs +72 -6
  120. package/driver/stages.mjs +10 -10
  121. package/driver/suite-census.json +238 -70
  122. package/driver/synthesis-record.mjs +2 -2
  123. package/driver/systemd/clearotron-client-mcp.service +3 -3
  124. package/driver/systemd/clearotron-deploy.service +2 -2
  125. package/driver/systemd/clearotron-mcp-face.service +1 -1
  126. package/driver/systemd/clearotron-portal.service +3 -3
  127. package/driver/systemd/clearotron-worker.service +5 -5
  128. package/driver/systemd/install-census.mjs +1 -1
  129. package/driver/systemd/render-units.mjs +9 -9
  130. package/driver/terminal-clamp.mjs +1 -1
  131. package/driver/trigger-cap.mjs +18 -2
  132. package/driver/unit-inventory.mjs +8 -8
  133. package/driver/usage-ledger.mjs +5 -3
  134. package/driver/verify-knockout.mjs +7 -7
  135. package/driver/verify.mjs +5 -5
  136. package/driver/whatif-memo-run.mjs +1 -1
  137. package/driver/whatif-queue.mjs +3 -3
  138. package/driver/whatif-worker.mjs +2 -2
  139. package/examples/README.md +1 -1
  140. package/examples/grants.example.json +25 -24
  141. package/mcp-server/CHANGELOG.md +10 -0
  142. package/mcp-server/http-server.mjs +3 -3
  143. package/mcp-server/key-socket.mjs +1 -1
  144. package/mcp-server/lib/audit-view.mjs +3 -3
  145. package/mcp-server/lib/brief.mjs +3 -3
  146. package/mcp-server/lib/driver.mjs +1 -1
  147. package/mcp-server/lib/events.mjs +1 -1
  148. package/mcp-server/lib/http-handler.mjs +2 -2
  149. package/mcp-server/lib/instructions.mjs +2 -2
  150. package/mcp-server/lib/knockout.mjs +1 -1
  151. package/mcp-server/lib/ops.mjs +6 -3
  152. package/mcp-server/lib/options.mjs +15 -4
  153. package/mcp-server/lib/plan.mjs +7 -6
  154. package/mcp-server/lib/runs.mjs +10 -0
  155. package/mcp-server/lib/whatif.mjs +5 -5
  156. package/mcp-server/package.json +1 -1
  157. package/mcp-server/packs/README.md +1 -1
  158. package/mcp-server/remote/client-mcp-apikey.service +1 -1
  159. package/mcp-server/remote/client-mcp.service +2 -2
  160. package/mcp-server/remote/trademark-artifacts-http.service +1 -1
  161. package/mcp-server/server.mjs +38 -24
  162. package/package.json +2 -2
  163. package/portal-ui/dist/assets/{index-KFAHMgdT.js → index-CWTHP0sH.js} +3471 -1901
  164. package/portal-ui/dist/assets/{index-1ziUJX1E.css → index-KpytsmNH.css} +79 -26
  165. package/portal-ui/dist/index.html +2 -2
  166. package/portal-ui/package.json +1 -1
  167. package/providers/_shared/lane-probe.mjs +9 -3
  168. package/providers/jx-subclass/lookup.mjs +1 -1
  169. package/providers/oauth-mcp-bridge/CHANGELOG.md +4 -0
  170. package/providers/oauth-mcp-bridge/package.json +1 -1
  171. package/providers/oauth-mcp-bridge/systemd/courtlistener-mcp.service +1 -1
  172. package/scripts/citation-drift-report.mjs +1 -1
  173. package/scripts/citation-line-check.mjs +2 -2
  174. package/scripts/drive-env-check.mjs +1 -1
  175. package/scripts/e2e.mjs +5 -5
  176. package/scripts/env-audit.mjs +13 -1
  177. package/scripts/headless-page.mjs +5 -5
  178. package/scripts/live-surface-check.mjs +26 -2
  179. package/scripts/mint-names-in-force.mjs +19 -5
  180. package/scripts/mint-reference-strip-backlog.mjs +1 -1
  181. package/scripts/mint-suite-census.mjs +37 -10
  182. package/scripts/pack-publishable.mjs +1 -1
  183. package/scripts/preinstall-node-check.mjs +1 -1
  184. package/scripts/release-await-cut.mjs +3 -3
  185. package/scripts/release-cut-decision.mjs +1 -1
  186. package/scripts/release-dist-tag.mjs +1 -1
  187. package/scripts/release-install-check.mjs +1 -1
  188. package/scripts/release-notes-lint.mjs +1 -1
  189. package/scripts/release-publish-guard.mjs +1 -1
  190. package/scripts/release-version-pr-checks.mjs +2 -2
  191. package/scripts/release-version.mjs +61 -5
  192. package/scripts/render-brand-banner.mjs +1 -1
  193. package/scripts/render-check.mjs +2 -2
  194. package/scripts/repo-writes.mjs +1 -1
  195. package/scripts/report-frame-check.mjs +1 -1
  196. package/scripts/report-screenshot.mjs +2 -2
  197. package/scripts/retire-bare-refs.mjs +1 -1
  198. package/scripts/revisit-render-check.mjs +1 -1
  199. package/scripts/score.mjs +1 -1
  200. package/scripts/strip-titles-and-attributions.mjs +389 -0
  201. package/scripts/strip-tracker-citations.mjs +122 -5
  202. package/scripts/test-run.mjs +4 -4
  203. package/scripts/third-party-notices.mjs +1 -1
  204. package/scripts/verify-publishable.mjs +1 -1
  205. package/shared/access-audience.mjs +2 -2
  206. package/shared/anon-overlay.mjs +1 -1
  207. package/shared/brand.mjs +15 -1
  208. package/shared/bundle-freshness.mjs +1 -1
  209. package/shared/bundle-rebuild.mjs +1 -1
  210. package/shared/checkout-move.mjs +2 -2
  211. package/shared/client-door.mjs +8 -8
  212. package/shared/connect-clients.mjs +7 -7
  213. package/shared/connector-signin-probe.mjs +1 -1
  214. package/shared/env-aliases.mjs +1 -1
  215. package/shared/env-local.mjs +5 -5
  216. package/shared/grants-edit.mjs +76 -0
  217. package/shared/install-auth.mjs +1 -1
  218. package/shared/listen.mjs +3 -3
  219. package/shared/mcp-challenge.mjs +1 -1
  220. package/shared/names-in-force.mjs +2 -1
  221. package/shared/onboarding-store.mjs +19 -2
  222. package/shared/reference-guard-classes.mjs +44 -2
  223. package/shared/register-selection.mjs +1 -1
  224. package/shared/scope.mjs +223 -56
  225. package/shared/secret-file.mjs +1 -1
  226. package/shared/server-units.mjs +1 -1
  227. package/shared/staff-domain.mjs +45 -78
  228. package/shared/summary-blocks.mjs +2 -2
  229. package/shared/systemd-failure.mjs +3 -3
  230. package/shared/tracked-files.mjs +1 -1
  231. package/shared/trigger-lane.mjs +1 -1
  232. package/shared/tty-style.mjs +1 -1
  233. package/shared/usage-block.mjs +1 -1
  234. package/shared/vacuous-pass.mjs +1 -1
  235. package/shared/verb-shim.mjs +1 -1
@@ -118,7 +118,7 @@ export function returnedPhrase(info) {
118
118
  }
119
119
 
120
120
  /**
121
- * — MAY AN EXECUTED RECALL PROBE DISCHARGE ON ITS OWN EXECUTION? Owner ruling 2026-08-19: "no".
121
+ * — MAY AN EXECUTED RECALL PROBE DISCHARGE ON ITS OWN EXECUTION? Ruling 2026-08-19: "no".
122
122
  *
123
123
  * A recall ask discharges only when what came back is DEALT WITH. `kind:executed` alone discharges
124
124
  * nothing — and the failure it hid is the one this issue was filed on: a HIGH-graded live US
@@ -597,7 +597,7 @@ export function deliveredMarks(findingsJsonText) {
597
597
  const normMark = (s) => String(s ?? "").toLowerCase().replace(/[^a-z0-9]/g, "");
598
598
 
599
599
  /**
600
- * Owner ruling 2026-09-07 (tracker issue 246), Option A, worded ABOUT THE MARK: a recall ask may close
600
+ * Ruling 2026-09-07, Option A, worded ABOUT THE MARK: a recall ask may close
601
601
  * as immaterial only when the mark it went looking for is in the client's findings. Otherwise the mark
602
602
  * is owed a finding and the ask stays open.
603
603
  *
package/driver/cancel.mjs CHANGED
@@ -56,6 +56,33 @@ export function requestCancel(runDir, { via = "unknown", by = undefined } = {})
56
56
  return rec;
57
57
  }
58
58
 
59
+ /**
60
+ * THE LAST READ BEFORE A REPORT IS PUBLISHED, on both lanes.
61
+ *
62
+ * The gateway's read is inside the attempt loop, before a turn is dispatched, and a dispatched turn
63
+ * always finishes. So the flag is consulted when a stage is about to spend and at no other moment —
64
+ * which means a stop pressed during the FINAL stage has no later boundary to be seen at, and the run
65
+ * publishes. Measured 2026-09-09 on a knockout: the stop was recorded, the last stage's turn ran to
66
+ * completion, and a full report was delivered 100 seconds later. Both the dialog and the API had told
67
+ * the operator twice that nothing would be delivered.
68
+ *
69
+ * ONE DEFINITION FOR BOTH LANES, because the promise is made in one set of words to every operator and
70
+ * two readings of "past the point of stopping" is how one lane keeps it and the other does not.
71
+ *
72
+ * IT GOES AFTER AN ALREADY-PUBLISHED SHORT-CIRCUIT, NEVER BEFORE. A run whose report is already out has
73
+ * delivered; refusing at that point would mark a delivered run cancelled and tell the operator the
74
+ * opposite of what happened, which is the same defect pointed the other way.
75
+ *
76
+ * This does not make boundary mode able to stop everything — a turn in flight still finishes and its
77
+ * spend is still spent. It makes the one promise the copy actually makes true: that nothing is
78
+ * delivered.
79
+ */
80
+ export function assertNotCancelledBeforePublish(runDir, lane = "run") {
81
+ if (!isCancelled(runDir)) return null;
82
+ const rec = readCancel(runDir);
83
+ throw new RunCancelled(`publish (${lane})`, rec);
84
+ }
85
+
59
86
  /**
60
87
  * Thrown by the gateway when a run is asked to stop, and caught by both pipelines' run-level handlers.
61
88
  *
@@ -21,7 +21,7 @@
21
21
  // A FIX THAT MAKES THE GAP QUIETER WOULD BE WORSE THAN THE DEFECT. The report never claimed there was no
22
22
  // adverse case law; every surface disclosed the gap. That discipline is untouched here — this changes
23
23
  // the REASON attached to an honest gap, never whether the gap is stated.
24
- // ── AND A SOURCE NOBODY COULD CHECK IS NOT A SOURCE WE DO NOT HAVE (tracker issue 173) ──────────────
24
+ // ── AND A SOURCE NOBODY COULD CHECK IS NOT A SOURCE WE DO NOT HAVE ──────────────────────────────────
25
25
  //
26
26
  // The credential reader has four states — absent, usable, unusable, unreadable — and the caller used to
27
27
  // flatten them into one boolean before this composer saw them. Three of those states then shared one
@@ -30,7 +30,7 @@
30
30
  // ground truth. A deployment with a `chmod 000` on one file would tell a client it does not have US
31
31
  // federal case law. Nobody knows that. We know only that we could not look.
32
32
  //
33
- // OWNER RULING, 2026-09-05, on the three wordings put to him: report it as a source whose availability
33
+ // RULING, 2026-09-05, on the three wordings put to him: report it as a source whose availability
34
34
  // COULD NOT BE CONFIRMED — never as one this deployment does not have, and never as an outage — and stop
35
35
  // there. Deliberately it does not say whether to treat the coverage as available: that is a second
36
36
  // question, it applies to every product rather than this one source, and settling it as a side effect of
@@ -46,7 +46,7 @@
46
46
  *
47
47
  * `pipeline.mjs` built this shape inline and an arm rebuilt it by hand to drive the composer. Two copies
48
48
  * of one mapping, and the arm's copy could not fail when the real one changed: adding `checked` here for
49
- * tracker issue 173 left the arm still composing the old three-field row and still passing. A test that
49
+ * The repair left the arm still composing the old three-field row and still passing. A test that
50
50
  * restates the code it is checking agrees with itself.
51
51
  *
52
52
  * It is exported for that reason and not as a convenience: the arm and the pipeline must read the same
@@ -0,0 +1,261 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
3
+ // company-bundle.mjs — what a valid company bundle is, and what the roster will accept.
4
+ //
5
+ // ── EXTRACTED FROM bin/brandowner.mjs WHEN THE SECOND DOOR ARRIVED ─────────────────────────────────
6
+ //
7
+ // The command line was the only way to create a company, so this logic lived beside it. The browser is
8
+ // now the second door, and the alternative to moving it was a second opinion about what a valid company
9
+ // is — which is the thing this codebase keeps paying to remove. There is one definition and both doors
10
+ // ask it.
11
+ //
12
+ // It lives here rather than in shared/ because every question below is answered by driver code — the
13
+ // profile validator, the framework loader, the skill-path resolver — and shared/ does not depend on
14
+ // driver/. And it lives outside bin/ for the reason shared/onboarding-store.mjs already states: a bin
15
+ // entry importing another bin entry is a dependency between two things a user types.
16
+ //
17
+ // ── WHAT IS HERE AND WHAT IS NOT ──────────────────────────────────────────────────────────────────
18
+ //
19
+ // Everything here is filesystem-and-roster pure: it composes a bundle and says whether the roster would
20
+ // accept it. What differs between the two doors stays with each — where the store is, how a context pack
21
+ // is read, who the author is (the command reads the OS username, the service takes it from the verified
22
+ // sign-in), and how the write is recorded.
23
+ //
24
+ // The refusal that matters most is the domain collision. The roster loader throws for the WHOLE roster
25
+ // when two companies claim one domain, so a bad create does not fail one company — it stops the
26
+ // deployment resolving ANY of them at the next start. That check is not reachable from the per-profile
27
+ // validator, so it has to be made here, against the proposed roster, before anything is written.
28
+
29
+ import { existsSync } from "node:fs";
30
+ import { join } from "node:path";
31
+
32
+ import { validateProfileEdit } from "./profiles.mjs";
33
+ import { DEFAULT_FRAMEWORK, loadFrameworkManifest } from "./framework.mjs";
34
+ import { config } from "./driver.config.mjs";
35
+ import { Refusal } from "../shared/onboarding-store.mjs";
36
+
37
+ // ── the framework, which is the point of the setup ─────────────────────────────────────────────────
38
+ /**
39
+ * Resolve the framework this brand owner will be rated under, as the ruling words it: the client's own
40
+ * when supplied, the Generic default otherwise, and always stated.
41
+ *
42
+ * ABSENT AND BROKEN ARE NOT THE SAME EVENT and the ruling separates them deliberately. Absent means the
43
+ * client has not given us their framework yet — onboarding proceeds under the Generic default, named.
44
+ * Broken means someone TRIED to select one and it does not resolve, and falling back there would rate a
45
+ * client's matters under a framework nobody chose while the output said everything was fine.
46
+ *
47
+ * The profile validator checks the SHAPE of this string only (`skills/prelim-search/<file>.md`, no
48
+ * escape) and never whether the file is there — so a shape-valid path to a document that does not exist
49
+ * validates cleanly and fails at rating time, which is the wrong place to find out.
50
+ */
51
+ export function resolveFramework(requested, {
52
+ resolveSkill = (rel) => config.resolveSkillPath(rel),
53
+ loadManifest = loadFrameworkManifest,
54
+ } = {}) {
55
+ if (requested == null || String(requested).trim() === "")
56
+ return { path: DEFAULT_FRAMEWORK, source: "default" };
57
+
58
+ const path = String(requested).trim();
59
+ // Shape first, and by the profile validator's own rule rather than a second copy of it: a path that
60
+ // would be refused at load must be refused here, in the same words, before anything is written.
61
+ if (!/^skills\/prelim-search\/[^/]+\.md$/.test(path) || path.includes(".."))
62
+ throw new Refusal(
63
+ `--framework must name a document of the form "skills/prelim-search/<file>.md" (got ${JSON.stringify(path)}). `
64
+ + `A profile selects a SHIPPED framework, never an arbitrary path.`);
65
+
66
+ // RESOLVED THE WAY THE RATING STAGE RESOLVES IT, never by joining the repo root. `skills/...` paths
67
+ // are relative to the DRIVER's skills directory, and a deployment may serve them from a doctrine
68
+ // overlay (CLEAROTRON_INSTRUCTIONS_DIR) instead. Joining the repo root would have looked in a
69
+ // directory that does not exist, refused every valid framework on every install, and — worse in the
70
+ // other direction — been blind to the overlay a deployment actually reads.
71
+ const deck = resolveSkill(path);
72
+ if (!existsSync(deck))
73
+ throw new Refusal(
74
+ `--framework names ${path}, and there is no such document on this install (resolved to ${deck}). `
75
+ + `Refusing rather than rating this brand owner's matters under the Generic default: a framework `
76
+ + `somebody chose and that does not resolve is a mistake, not an absence.`,
77
+ { code: "framework_missing", path });
78
+
79
+ // The manifest is DERIVED from the deck path, never a separate knob — and it is what the validators,
80
+ // the renderer and the profile page read to know the framework's band vocabulary. A deck whose
81
+ // manifest will not load produces a customer whose page cannot state their own ladder.
82
+ //
83
+ // ASKED, NOT RE-CHECKED. An `existsSync` on the manifest here would be a second opinion about the
84
+ // same fact: `loadFrameworkManifest` already refuses a missing sidecar by name
85
+ // (`framework_manifest_missing:<path>`) and is the exact read the rating stage makes. Two checks
86
+ // means two messages to keep in step, and the one this file could write would be the one that goes
87
+ // stale.
88
+ let manifest;
89
+ try {
90
+ manifest = loadManifest(resolveSkill, path);
91
+ } catch (e) {
92
+ throw new Refusal(
93
+ `--framework names ${path}, and the framework will not load: ${String(e?.message ?? e)}. `
94
+ + `Refusing rather than falling back: a framework somebody chose and that does not resolve is a `
95
+ + `mistake, not an absence.`);
96
+ }
97
+ return { path, source: "supplied", manifest };
98
+ }
99
+
100
+ // ── the profile this writes ────────────────────────────────────────────────────────────────────────
101
+ /**
102
+ * Which marketplaces this brand owner's searches cover — supplied, or the Generic default, SAID OUT LOUD.
103
+ *
104
+ * A customer bundle is a COMPLETE document in this design, not an overlay on generic: every shipped
105
+ * profile carries its own `platforms`, and the loader requires a non-empty array on every file. The
106
+ * command had no way to supply one and set none, so every bundle it wrote failed to load on this field
107
+ * as well as on the `key` field above — two independent invalidities, and onboarding could not produce
108
+ * a loadable brand owner at all.
109
+ *
110
+ * Defaulting rather than refusing, and naming it, is this command's own established idiom: the same
111
+ * ruling governs the framework one function down. Which marketplaces a client's clearance searches is
112
+ * not a detail to decide silently, so an operator who supplies nothing is TOLD what they got and can
113
+ * refine it in the portal.
114
+ */
115
+ export function resolvePlatforms(supplied, roster) {
116
+ if (supplied?.length) return { platforms: supplied, source: "supplied" };
117
+ const house = roster?.get?.("generic")?.platforms ?? [];
118
+ if (!house.length)
119
+ throw new Refusal(
120
+ "no --platforms was given and the Generic default carries none, so there is nothing to onboard this "
121
+ + "brand owner with. Pass --platforms, or repair the generic profile in the store.",
122
+ { code: "no_marketplaces" });
123
+ return { platforms: [...house], source: "house default" };
124
+ }
125
+
126
+ export function buildProfile({ key, name, domains, platforms, framework, industry,
127
+ tradingNames, classes, territories }) {
128
+ // NO `key` IN THE DOCUMENT. The loader derives it from the FILENAME and injects it — readProfilesLayer
129
+ // composes `{ key, ...p }` — so a `key` written here is redundant on the way in and fatal on the way
130
+ // out: it is not in KNOWN_PROFILE_KEYS, and the deny-unknown-key gate hard-fails the whole roster over
131
+ // it. Every bundle this command wrote carried one, so the first successful onboarding made the store
132
+ // unloadable and the next command to read profiles threw. The parameter stays — the filename and the
133
+ // roster checks are addressed by key — it simply does not travel into the file.
134
+ const profile = { name, platforms };
135
+ if (domains?.length) profile.matchDomains = domains;
136
+ if (industry) profile.industry = industry;
137
+ // THE OPTIONAL THREE, OMITTED WHEN EMPTY RATHER THAN WRITTEN AS `[]`. An empty array is a real
138
+ // instruction to the reader of a profile, not the absence of one, so writing it for a field nobody
139
+ // filled in states something nobody said. The whole create path treats absent and empty as different
140
+ // and this is where that has to hold.
141
+ if (tradingNames?.length) profile.selfExclusionOwners = tradingNames;
142
+ if (classes?.length) profile.defaultClasses = classes;
143
+ if (territories?.length) profile.defaultJurisdictions = territories;
144
+ // ALWAYS SET, per the ruling. `frameworkFor` would fall back to the same value if this were absent —
145
+ // but "the tool sets it" is the point of the setup, and an explicit selection is what makes the
146
+ // receipt below mean anything.
147
+ profile.frameworkPath = framework.path;
148
+ return profile;
149
+ }
150
+
151
+ // ── the proposed roster, validated whole ───────────────────────────────────────────────────────────
152
+ /**
153
+ * Validate the candidate the way the deployment will read it: as one member of the roster, not alone.
154
+ *
155
+ * `loadProfiles` composes every profile in the store and throws on the FIRST cross-profile conflict it
156
+ * finds — a domain claimed twice, most of all. That throw is not scoped to the offending file: it stops
157
+ * the roster loading at all. So the only honest check is over the proposed state, which is what
158
+ * bin/grant.mjs does with `accessView` for exactly the same reason.
159
+ */
160
+ /**
161
+ * The roster as it stands, where AN EMPTY STORE IS AN EMPTY ROSTER — F42.
162
+ *
163
+ * `loadProfiles` with an explicit `dir` reads that directory ALONE and refuses a store with no
164
+ * `generic.json`, which is right for what that refusal is for: `generic` is the universal fallback every
165
+ * unprofiled job resolves to, and a RUN against a store without it would silently reprofile a client.
166
+ * The explicit-dir form deliberately has no fall-through, because the fixtures that build a roster
167
+ * assert on precisely that roster and layering the bundled set underneath would widen three set-level
168
+ * guards until none still tested its own name.
169
+ *
170
+ * BUT THIS COMMAND IS NOT RUNNING A CLEARANCE. It reads the store to answer one question — does this key
171
+ * or one of its domains already exist — and on a fresh install the honest answer is "no, there is
172
+ * nothing here yet". Instead it stack-traced on the FIRST day-one command a new operator types, with a
173
+ * refusal about a file they had never heard of and did not need: the runtime resolves `generic` from the
174
+ * product's bundled set through the OVERLAY path, so a deployment store never needs its own copy.
175
+ *
176
+ * So the missing-generic refusal is caught BY NAME and answered as the empty roster it describes. Caught
177
+ * by name rather than broadly, because every other thing that loader throws — an overlapping domain, an
178
+ * unknown key, an unreadable store — is a real refusal this command must still relay.
179
+ */
180
+ /**
181
+ * The key a company gets from its name — the filename, and the key every screen and route uses.
182
+ *
183
+ * ONE SPELLING FOR BOTH DOORS. The command line takes the key as an argument and the browser derives it
184
+ * from the name, so without this the two doors would file "Acme Ltd" under different keys depending on
185
+ * which one you used, and neither would be wrong.
186
+ *
187
+ * Capped well under the validator's 39 characters and stripped of leading and trailing hyphens, so a
188
+ * name made entirely of punctuation produces nothing rather than a key of dashes — the caller then
189
+ * refuses and asks for the key, which is honest, where inventing one would file a company under a name
190
+ * nobody chose.
191
+ */
192
+ export function companyKeyFrom(name) {
193
+ return String(name ?? "")
194
+ .toLowerCase()
195
+ .normalize("NFKD")
196
+ // COMBINING MARKS ARE DROPPED, NOT SEPARATED. NFKD splits "ü" into "u" + a combining diaeresis, and
197
+ // the class below would turn that mark into a hyphen — so "Zürich Präzision" filed as
198
+ // "zu-rich-pra-zision". Accented company names are the normal case in the languages this product
199
+ // works in, not an edge.
200
+ .replace(/[\u0300-\u036f]/g, "")
201
+ .replace(/[^a-z0-9]+/g, "-")
202
+ .replace(/^-+|-+$/g, "")
203
+ .slice(0, 39)
204
+ .replace(/-+$/, "");
205
+ }
206
+
207
+ export function rosterAsItStands(store, loadProfiles) {
208
+ try { return loadProfiles({ dir: store, force: true }); }
209
+ catch (e) {
210
+ if (/generic\.json is REQUIRED/.test(String(e?.message ?? ""))) return new Map();
211
+ throw e;
212
+ }
213
+ }
214
+
215
+ export function assertRosterAccepts({ store, key, profile, loadProfiles }) {
216
+ // THE LOADER'S OWN VALIDATOR, over the CANDIDATE — the discipline `project add` already applies to an
217
+ // overlay, in the same words, for the same reason: whatever the tree would refuse at load is refused
218
+ // here, before anything is written. Everything below this line reads the store AS IT STANDS, which is
219
+ // why none of it could ever see a bad field in the file about to be added: the candidate never met the
220
+ // loader until the next command did, and by then the write had landed.
221
+ // `generic` IS RESERVED AND THE KEY RULE DOES NOT SAY SO. `PROFILE_KEY_RE` matches it, while the
222
+ // comment above that regex claims the create path never allows it — so the rule was a sentence rather
223
+ // than a check. On a store with no `generic.json` of its own, creating one here writes a file that
224
+ // shadows the bundled universal fallback for every job that names no company, which is silent and
225
+ // affects every clearance rather than one. Refused at the gate both doors pass through.
226
+ if (String(key).toLowerCase() === "generic")
227
+ throw new Refusal(
228
+ `"generic" is the house default every unprofiled clearance falls back to, so it cannot be created `
229
+ + `as a company. Nothing has been written. Pick another key.`);
230
+
231
+ const v = validateProfileEdit(key, profile);
232
+ if (!v.ok)
233
+ throw new Refusal(`the company bundle is not valid, so nothing was written:\n ${v.errors.join("\n ")}`,
234
+ { code: "invalid_bundle", errors: v.errors });
235
+
236
+ const existing = rosterAsItStands(store, loadProfiles);
237
+ if (existing.has(key))
238
+ throw new Refusal(
239
+ `a brand owner "${key}" already exists in ${store}. This command creates; it does not overwrite an `
240
+ + `existing bundle. Edit it in the portal, or remove the file deliberately first.`,
241
+ { code: "key_exists", key });
242
+
243
+ for (const d of profile.matchDomains ?? []) {
244
+ const dl = String(d).toLowerCase();
245
+ for (const [otherKey, other] of existing) {
246
+ if ((other.matchDomains ?? []).some((o) => String(o).toLowerCase() === dl))
247
+ throw new Refusal(
248
+ `domain "${dl}" is already claimed by the brand owner "${otherKey}". Two owners claiming one `
249
+ + `domain makes the WHOLE roster refuse to load on the next start — not just this bundle — so `
250
+ + `nothing has been written.`,
251
+ { code: "domain_claimed", domain: dl, heldBy: otherKey });
252
+ }
253
+ }
254
+ }
255
+
256
+
257
+ // ── argument parsing ───────────────────────────────────────────────────────────────────────────────
258
+ // Long flags only, and an unknown one REFUSES. A permissive parser on a command that writes into the
259
+ // customers directory is how a typo'd flag becomes a silently missing field — `--fraemwork` would
260
+ // otherwise onboard a client under the Generic default while the operator believed they had set theirs.
261
+ const FLAGS = ["--name", "--domains", "--platforms", "--framework", "--industry", "--context"];
@@ -95,7 +95,7 @@ export function refuseToCompare(a = {}, b = {}) {
95
95
  const modelOf = (x) => String(x?.modelUsed ?? x?.model ?? "").trim();
96
96
  if (modelOf(a) !== modelOf(b)) return null;
97
97
  return "REFUSING TO COMPARE — neither record names an engine and both name the same model "
98
- + `(${modelOf(a) || "none"}), so there is nothing here that tells these two apart (tracker issue 1967). `
98
+ + `(${modelOf(a) || "none"}), so there is nothing here that tells these two apart. `
99
99
  + "A model id resolved from a tier is not the answer: it maps through the Anthropic table whichever "
100
100
  + "engine ran, so two arms on different engines read identically.\n"
101
101
  + " The per-attempt `_driver/<stage>.jsonl` records `engine` and `modelUsed` — compare from there.";
@@ -266,7 +266,7 @@ export function makeReadBudget({ perHour = 20 } = {}) {
266
266
  *
267
267
  * It used to say the client was built from `@anthropic-ai/sdk`, in the present tense, long after the
268
268
  * change recorded below moved this route through the engine door. The SDK has now been removed from
269
- * the manifests entirely (tracker issue 99) — nothing in the tree imports it.
269
+ * the manifests entirely — nothing in the tree imports it.
270
270
  */
271
271
  /**
272
272
  * Every way a parsed payload can fail to be a read, named.
@@ -336,7 +336,7 @@ export function makeComposeReader({ turn, now = () => new Date() } = {}) {
336
336
  message: `That is ${text.length.toLocaleString("en-GB")} characters — paste up to ${MAX_BRIEF.toLocaleString("en-GB")}, or set the search up below.`,
337
337
  };
338
338
  }
339
- // NO `thinking` BLOCK, DELIBERATELY. Owner ruling 2026-07-22: sonnet, thinking off. This is
339
+ // NO `thinking` BLOCK, DELIBERATELY. Ruling 2026-07-22: sonnet, thinking off. This is
340
340
  // extraction, not judgment — the brief already contains every fact the answer needs, and the
341
341
  // reasoning that matters (is this mark clear?) happens later, in the run, where it is paid for.
342
342
  // Off also keeps the button feeling like a button.
@@ -279,7 +279,7 @@ const CASELAW_SITES = Object.freeze({
279
279
  * a byte of it.
280
280
  */
281
281
  /**
282
- * Is this credential file something the bridge could actually use? — tracker issue 173.
282
+ * Is this credential file something the bridge could actually use?
283
283
  *
284
284
  * `existsSync` was the whole test, and a zero-byte file therefore reported as an enrolled case-law
285
285
  * source. That is not merely an operator-facing inaccuracy: `driver/case-law-sources.mjs` hands the
@@ -347,7 +347,7 @@ export function caseLawInventory(env = process.env) {
347
347
  // in to from one that is simply part of the build.
348
348
  enrolment: "oauth",
349
349
  configured,
350
- // THE THIRD STATE, WHICH `configured` CANNOT HOLD (tracker issue 173). `configured` is a boolean
350
+ // THE THIRD STATE, WHICH `configured` CANNOT HOLD. `configured` is a boolean
351
351
  // and the world has three cases in it: never enrolled, enrolled and usable, enrolled and NOT
352
352
  // usable. Collapsing the third into either of the others is the defect — into the first it
353
353
  // reads as "not set up" and hides a credential the operator believes in; into the second it
@@ -11,7 +11,7 @@
11
11
  // each, from a closed enum, plus the validator tokens that speak about that element.
12
12
  // E2 declarations ∪ validator vocabulary is a closed partition, in two arms of different strength:
13
13
  // arm 1 a token no element accounts for → hard red, now
14
- // arm 2 an element no token speaks about → a per-stage RATCHET (owner ruling 2026-08-13)
14
+ // arm 2 an element no token speaks about → a per-stage RATCHET (ruling 2026-08-13)
15
15
  // E3 structure is returned, never emitted as text — with a named, shrinking backlog for the sites
16
16
  // that exist today, because a lint that greenlights every existing hole certifies the problem.
17
17
 
@@ -195,7 +195,7 @@ export const E3_BACKLOG = [
195
195
  // quote — M2 LANDED. The live field is `anchor` (prelim-common-law/SKILL.md:197).
196
196
  // Neither was reworded. Both were removed, and this row is discharged of them.
197
197
  //
198
- // ── (b) THE NEW, NARROWER CLAIM — RE-EVIDENCED for B (the form path is DELETED, owner ruling
198
+ // ── (b) THE NEW, NARROWER CLAIM — RE-EVIDENCED for B (the form path is DELETED, ruling
199
199
  // 2026-08-17). The seat is no longer told to open any file: the surviving position-dictation now
200
200
  // orders a `record_dispositions` CALL carrying `row_index` / `receipt_index` / `ruling` / `note` per
201
201
  // row, and the receiver resolves BOTH positions — made the row an ordinal too, on the finding
@@ -82,7 +82,7 @@ export const VOCABULARY = [
82
82
  { token: "half_path_unrecognized", stages: ["common-law-half"], site: "driver/verify.mjs:395" },
83
83
  { token: "no_coverage_status_row", stages: [...CL, "register-digest"], site: "driver/verify.mjs:452, 1416" },
84
84
  // B — `connotation_form_unparseable` and `connotation_form_untouched` are GONE: they
85
- // were states only a hand-authored document could be in, and the form path is deleted (owner ruling
85
+ // were states only a hand-authored document could be in, and the form path is deleted (ruling
86
86
  // 2026-08-17). Their subject matter is carried by the call tokens under the D1 family row below.
87
87
  { token: "connotation_form_damaged", stages: CL, site: "driver/verify.mjs:889" },
88
88
  { token: "connotation_", stages: CL, site: "driver/verify.mjs:898", family: "driver/connotation-search.mjs:894 (CONNOTATION_FORM_REASONS, exported at :819)", dynamic: "D1" },
@@ -210,7 +210,7 @@ export function contradictionFor(reason, row, scope) {
210
210
  + `own-right or duplicate-of-delivered.`;
211
211
  }
212
212
 
213
- // ── ECONOMY IS NOT A REASON ABOUT A LIVE IN-CLASS MARK (owner ruling 2026-09-07) ─────────────────
213
+ // ── ECONOMY IS NOT A REASON ABOUT A LIVE IN-CLASS MARK (ruling 2026-09-07) ─────────────────
214
214
  //
215
215
  // `not-worth-the-line` means "it fits none of the four manageable categories and is not relevant
216
216
  // enough to drive the read". That is a statement about the REPORT'S BUDGET, not about the record. On a
@@ -21,7 +21,7 @@
21
21
  # the prelim-outbox.timer rescan owns the delivery at a sane cadence).
22
22
  set -u
23
23
 
24
- # tracker issue 774 — this fallback chain MIRRORS driver.config.mjs (`outboxDir` = CLEAROTRON_OUTBOX_DIR, else
24
+ # this fallback chain MIRRORS driver.config.mjs (`outboxDir` = CLEAROTRON_OUTBOX_DIR, else
25
25
  # <workspaceRoot>/prelim-outbox; `workspaceRoot` = CLEAROTRON_WORK_DIR, else $HOME/trademark/workspace).
26
26
  # It is a second derivation of the same answer in a language that cannot import the first, so it moves
27
27
  # whenever that one does. If they disagree, the driver writes markers where this script never looks and
@@ -37,7 +37,7 @@ DRAIN_WAIT="${CLEAROTRON_OUTBOX_DRAIN_WAIT:-180}"
37
37
  WAKE_MODEL="${CLEAROTRON_OUTBOX_WAKE_MODEL-anthropic/claude-haiku-4-5}"
38
38
  MSG="The trademark engine's outbox has pending events. Run the clearotron-deliver skill now: list_outbox_events, route each event (delivered → get_delivery_packet + send VERBATIM + mark_sent; every other kind → relay its text + ack_event)."
39
39
 
40
- # THE WALL, RESOLVED ONCE AND NAMED WHEN IT IS ABSENT (tracker issue 820).
40
+ # THE WALL, RESOLVED ONCE AND NAMED WHEN IT IS ABSENT.
41
41
  #
42
42
  # Every wake below runs under timeout(1) — GNU coreutils, not POSIX and not in a stock macOS or a
43
43
  # slim container. Without it `timeout …` is simply a command that is not there: bash answers 127, and
@@ -165,7 +165,7 @@ done
165
165
  # Bounded drain-wait after a successful wake: exit early once the courier consumed everything
166
166
  # (mark_sent/ack_event), else fall through to the pacing sleep / path-unit re-fire. NEVER delete here.
167
167
  #
168
- # POLL FIRST, THEN WAIT (tracker issue 830). `remaining` used to be assigned ONLY inside the loop body, so
168
+ # POLL FIRST, THEN WAIT. `remaining` used to be assigned ONLY inside the loop body, so
169
169
  # CLEAROTRON_OUTBOX_DRAIN_WAIT=0 — the value an operator sets to make delivery synchronous, which is to
170
170
  # say the value they set while already chasing a late delivery — ran the body zero times and left the
171
171
  # post-loop `${#remaining[@]}` reading an unset array. Under `set -u` that is an error: the echo never
@@ -76,7 +76,7 @@ export const DEV_COCKPIT_JOB_FIELDS = Object.freeze({
76
76
  notCarried: Object.freeze({
77
77
  registerFixtures: "a run that reads canned register payloads instead of calling a register. The cockpit "
78
78
  + "is a form for composing REAL dev runs; a fixture run is declared by the job file that wants one, so "
79
- + "the fact travels with the run rather than with whoever filled in a form (tracker issue 2038).",
79
+ + "the fact travels with the run rather than with whoever filled in a form.",
80
80
  promptParts: "the requester's declaration that the prose rides as SIDECAR files. The cockpit composes a job "
81
81
  + "from its own form and writes no sidecars, so it may not claim that shape (#1085).",
82
82
  forwarderDomain: "the cockpit's assembler defaults it; a dev form has no forwarding domain to state.",
@@ -87,6 +87,8 @@ export const DEV_COCKPIT_JOB_FIELDS = Object.freeze({
87
87
  projectKey: "spec-62 engagements are a customer-configuration surface; this form has no project picker "
88
88
  + "and inventing one here would let a dev job claim an overlay nobody chose.",
89
89
  parentRunId: "escalation lineage. Nothing on this form can name a parent run.",
90
+ tenant: "which organisation's Generic a run is filed under. The portal and the connector stamp it from a "
91
+ + "verified person; this dev form has no person to resolve it from, so a dev Generic run is filed under none.",
90
92
  customerUnknown: "arms candidate-self classification. A dev form cannot honestly say the applicant was "
91
93
  + "neither stated nor implied — the Customer box above states it or leaves it blank.",
92
94
  brief: "the intake confirmation brief, written by the email door's own gate. There is no such gate here.",
@@ -108,7 +108,7 @@ export function markFlushed(queue, ids, flushedAt) {
108
108
  // fired its own pass. An invariant with an undeclared violation is worse than a narrower invariant
109
109
  // stated honestly, because the next reader trusts the wrong one — so the exemption is DECLARED here,
110
110
  // with the reason, and an arm censuses every dispatch site against this list. Silence between an
111
- // invariant and a violation is the defect (tracker issue 116).
111
+ // invariant and a violation is the defect.
112
112
  //
113
113
  // WHY RECALL-RECONCILE CANNOT MINT — measured on the tree, not reasoned from the design:
114
114
  //
@@ -400,7 +400,7 @@ export function recordDispositions(spec, received, { now = () => new Date().toIS
400
400
  try {
401
401
  mkdirSync(dirname(accum), { recursive: true });
402
402
  // The accumulator in `_driver/` is the ONLY copy. The seat-facing mirror at `dispositions_path`
403
- // died with the form path (owner ruling 2026-08-17, delete-not-gate): the seat never reads or
403
+ // died with the form path (ruling 2026-08-17, delete-not-gate): the seat never reads or
404
404
  // writes it, the validator reads the accumulator, and a mirror nothing reads is a second writer
405
405
  // waiting to drift. The path itself survives in the spec as the accumulator's name anchor.
406
406
  writeFileSync(accum, json);
@@ -279,7 +279,7 @@ export const DOC_CONSTANTS = [
279
279
  doc: "INSTALL.md",
280
280
  pattern: /# TRADEMARK_MCP_HTTP_PORT=(\d+)/,
281
281
  },
282
- // ── TWO BINDINGS REMOVED: THEIR DOCUMENTS ARE NOT ON THIS TREE (tracker issue 83) ────────────────
282
+ // ── TWO BINDINGS REMOVED: THEIR DOCUMENTS ARE NOT ON THIS TREE ───────────────────────────────────
283
283
  //
284
284
  // `.env.deployment.example` and `.env.prod.example` each carried a `TRADEMARK_MCP_HTTP_PORT default`
285
285
  // binding. Both files are withheld from the public tree by the cut, so on this tree the bindings
@@ -4,8 +4,8 @@
4
4
  //
5
5
  // ── WHY THIS IS A MODULE AND NOT A LINE IN EACH ARM ─────────────────────────────────────────────────
6
6
  //
7
- // Two arms of the same deploy check reached opposite conclusions about one box (tracker issue 206,
8
- // measured on `c6e183d`). The queue arm read the worker unit, found it enabled, and said the .path/timer
7
+ // Two arms of the same deploy check reached opposite conclusions about one box, measured on
8
+ // `c6e183d`. The queue arm read the worker unit, found it enabled, and said the .path/timer
9
9
  // posture it was written for is retired here. The drainer arm did not read anything, applied the
10
10
  // timer-era rule, and called an absent drainer a fault. Both sentences shipped in the same report.
11
11
  //
@@ -86,7 +86,7 @@ export const DRAINER_CMD = /\b(runner\.mjs|clearotron[^\s]*\s+start)\b/;
86
86
  * @returns {{state: "pass"|"fail"|"warn", message: string}}
87
87
  */
88
88
  export function drainerVerdict({ stamp, headCommit, isAlive, processes, ppidOf = null, posture = null }) {
89
- // ── WHICH POSTURE IS THIS BOX IN, and say it out loud whatever the answer (tracker issue 206).
89
+ // ── WHICH POSTURE IS THIS BOX IN, and say it out loud whatever the answer.
90
90
  //
91
91
  // Only ONE branch below changes state on it — a stamped drainer whose process is gone. That is the
92
92
  // state the ruling is about: between ticks on a scheduled box, and nothing is executing runs on a