@ultimat3/cli 20.2.1 → 22.0.0

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 (237) hide show
  1. package/CLAUDE.md +84 -1582
  2. package/README.md +40 -2
  3. package/package.json +31 -31
  4. package/src/api-registration.ts +124 -0
  5. package/src/app-artifacts.ts +39 -0
  6. package/src/app-boundaries.ts +5 -10
  7. package/src/app-env.ts +2 -2
  8. package/src/app-load.ts +37 -5
  9. package/src/app-openapi.ts +27 -0
  10. package/src/app-permissions.ts +0 -0
  11. package/src/app-root.ts +1 -1
  12. package/src/async-pages.ts +32 -0
  13. package/src/bin.ts +7 -1
  14. package/src/boundary-findings.ts +36 -0
  15. package/src/browser-launcher-fake-html.ts +80 -0
  16. package/src/browser-launcher-fake.ts +165 -0
  17. package/src/browser-launcher-port.ts +159 -0
  18. package/src/browser-launcher.ts +26 -133
  19. package/src/budgets.ts +56 -13
  20. package/src/build-errors.ts +54 -0
  21. package/src/cdp-shot-a11y.ts +77 -0
  22. package/src/cdp-shot-clock.ts +14 -0
  23. package/src/cdp-shot-driver.ts +150 -0
  24. package/src/cdp-shot-element.ts +147 -0
  25. package/src/cdp-shot-errors.ts +62 -0
  26. package/src/cdp-shot-keys.ts +152 -0
  27. package/src/cdp-shot-page.ts +230 -0
  28. package/src/cdp-shot-watch.ts +241 -0
  29. package/src/cmd-affected-spec.ts +24 -0
  30. package/src/cmd-affected.ts +3 -19
  31. package/src/cmd-build-spec.ts +16 -0
  32. package/src/cmd-build.ts +46 -20
  33. package/src/cmd-ci-spec.ts +24 -0
  34. package/src/cmd-ci.ts +17 -21
  35. package/src/cmd-db-branch.ts +2 -2
  36. package/src/cmd-db-spec.ts +99 -0
  37. package/src/cmd-db.ts +15 -94
  38. package/src/cmd-deploy-helm.ts +136 -0
  39. package/src/cmd-deploy-spec.ts +40 -0
  40. package/src/cmd-deploy.ts +117 -42
  41. package/src/cmd-dev-spec.ts +25 -0
  42. package/src/cmd-dev.ts +78 -122
  43. package/src/cmd-docs-spec.ts +16 -0
  44. package/src/cmd-docs.ts +2 -11
  45. package/src/cmd-doctor-spec.ts +21 -0
  46. package/src/cmd-doctor.ts +4 -18
  47. package/src/cmd-env-spec.ts +18 -0
  48. package/src/cmd-env.ts +4 -13
  49. package/src/cmd-errors-spec.ts +23 -0
  50. package/src/cmd-errors.ts +3 -17
  51. package/src/cmd-fix-spec.ts +18 -0
  52. package/src/cmd-fix.ts +4 -13
  53. package/src/cmd-generate-spec.ts +31 -0
  54. package/src/cmd-generate.ts +46 -42
  55. package/src/cmd-i18n-spec.ts +16 -0
  56. package/src/cmd-i18n.ts +7 -14
  57. package/src/cmd-jobs-spec.ts +56 -0
  58. package/src/cmd-jobs.ts +3 -50
  59. package/src/cmd-manifest-spec.ts +15 -0
  60. package/src/cmd-manifest.ts +19 -20
  61. package/src/cmd-mcp-spec.ts +21 -0
  62. package/src/cmd-mcp.ts +2 -15
  63. package/src/cmd-new-spec.ts +33 -0
  64. package/src/cmd-new.ts +38 -33
  65. package/src/cmd-policy-spec.ts +13 -0
  66. package/src/cmd-policy.ts +2 -9
  67. package/src/cmd-pr-spec.ts +39 -0
  68. package/src/cmd-pr.ts +5 -65
  69. package/src/cmd-registries-spec.ts +32 -0
  70. package/src/cmd-registries.ts +4 -25
  71. package/src/cmd-routes-spec.ts +12 -0
  72. package/src/cmd-routes.ts +2 -7
  73. package/src/cmd-secrets-spec.ts +19 -0
  74. package/src/cmd-secrets.ts +22 -34
  75. package/src/cmd-shot-island.ts +12 -13
  76. package/src/cmd-shot-spec.ts +53 -0
  77. package/src/cmd-shot.ts +11 -60
  78. package/src/cmd-tasks-spec.ts +21 -0
  79. package/src/cmd-tasks.ts +2 -16
  80. package/src/cmd-test-spec.ts +54 -0
  81. package/src/cmd-test.ts +4 -49
  82. package/src/cmd-verify-spec.ts +28 -0
  83. package/src/cmd-verify.ts +3 -23
  84. package/src/db-seed.ts +4 -2
  85. package/src/dev-dashboard.ts +4 -4
  86. package/src/dev-lock.ts +18 -2
  87. package/src/dev-port.ts +21 -0
  88. package/src/dev-route-table.ts +119 -0
  89. package/src/dispatch.ts +12 -5
  90. package/src/document-styles.ts +1 -1
  91. package/src/drift.ts +4 -18
  92. package/src/error-catalog.ts +16 -9
  93. package/src/error-codes.ts +33 -28
  94. package/src/error-pages.ts +19 -7
  95. package/src/error-unthrown.ts +130 -0
  96. package/src/errors.ts +22 -29
  97. package/src/favicon.ts +2 -2
  98. package/src/fix-imports.ts +1 -1
  99. package/src/fix-scan.ts +2 -9
  100. package/src/flag-reads.ts +1 -2
  101. package/src/foreign-text.ts +36 -0
  102. package/src/framework-schema.ts +5 -6
  103. package/src/generate-feature.ts +42 -0
  104. package/src/generate-files.ts +7 -3
  105. package/src/generate-grants.ts +83 -0
  106. package/src/generate-kinds.ts +56 -4
  107. package/src/i18n-index.ts +59 -10
  108. package/src/icon-assets.ts +1 -1
  109. package/src/image-prepare.ts +14 -0
  110. package/src/import-scan.ts +63 -0
  111. package/src/index.ts +67 -334
  112. package/src/invocation-flags.ts +26 -0
  113. package/src/island-bundle.ts +38 -11
  114. package/src/island-capture.ts +6 -6
  115. package/src/island-harness-script.ts +6 -2
  116. package/src/island-realtime.ts +98 -0
  117. package/src/island-shot.ts +5 -0
  118. package/src/island-store.ts +131 -0
  119. package/src/island-verdict.ts +2 -2
  120. package/src/job-registration.ts +42 -0
  121. package/src/jobs-driver.ts +2 -2
  122. package/src/live-routes.ts +82 -42
  123. package/src/load-findings.ts +51 -0
  124. package/src/mcp-db-target.ts +1 -1
  125. package/src/mcp-errors.ts +30 -26
  126. package/src/mcp-host.ts +27 -12
  127. package/src/mcp-ui-diff.ts +27 -0
  128. package/src/mcp-ui-inspect.ts +4 -4
  129. package/src/mcp-ui-interact.ts +21 -15
  130. package/src/mcp-ui.ts +19 -15
  131. package/src/measure-database.ts +73 -0
  132. package/src/measure-paths.ts +74 -0
  133. package/src/measure-scope.ts +74 -0
  134. package/src/messages.ts +1 -3
  135. package/src/metrics-endpoint.ts +1 -1
  136. package/src/otlp-export.ts +1 -1
  137. package/src/output.ts +6 -0
  138. package/src/page-sync.ts +54 -0
  139. package/src/permission-grants.ts +86 -0
  140. package/src/prerender-out.ts +25 -0
  141. package/src/prerender.ts +121 -102
  142. package/src/pwa-artifacts.ts +3 -3
  143. package/src/realtime-browser-probe-fixture.ts +2 -2
  144. package/src/reexport-manifest.ts +2 -1
  145. package/src/registry.ts +80 -56
  146. package/src/role-realtime.ts +36 -0
  147. package/src/{dev-replicator.ts → role-replicator.ts} +1 -1
  148. package/src/{dev-roles-fixture.ts → role-start-fixture.ts} +9 -5
  149. package/src/role-start-types.ts +112 -0
  150. package/src/{dev-roles.ts → role-start.ts} +42 -115
  151. package/src/{dev-sync.ts → role-sync.ts} +23 -11
  152. package/src/root-env.ts +67 -0
  153. package/src/{dev-assets.ts → runtime-assets.ts} +7 -7
  154. package/src/{dev-services.ts → runtime-bindings.ts} +43 -17
  155. package/src/{dev-cache.ts → runtime-cache.ts} +2 -2
  156. package/src/runtime-jobs.ts +87 -0
  157. package/src/{dev-live-feed.ts → runtime-live-feed.ts} +20 -5
  158. package/src/{dev-notify-retention.ts → runtime-notify-retention.ts} +1 -1
  159. package/src/{dev-purge.ts → runtime-purge.ts} +2 -2
  160. package/src/{dev-queue.ts → runtime-queue.ts} +4 -4
  161. package/src/runtime-realtime.ts +55 -0
  162. package/src/{dev-render.ts → runtime-render.ts} +119 -20
  163. package/src/{dev-replica.ts → runtime-replica.ts} +2 -2
  164. package/src/{dev-runtime.ts → runtime-services.ts} +41 -17
  165. package/src/{dev-storage.ts → runtime-storage.ts} +4 -4
  166. package/src/scaffold-fixture.ts +28 -6
  167. package/src/scaffold-typecheck.ts +6 -3
  168. package/src/schema-drift.ts +7 -1
  169. package/src/script-csp.ts +5 -2
  170. package/src/secrets-rotation.ts +59 -0
  171. package/src/serve-boot.ts +192 -0
  172. package/src/serve-drain.ts +24 -0
  173. package/src/serve-entry.ts +6 -0
  174. package/src/serve-env.ts +116 -0
  175. package/src/serve-types.ts +55 -0
  176. package/src/serve.ts +44 -338
  177. package/src/shot-server.ts +2 -2
  178. package/src/shot-settle.ts +10 -1
  179. package/src/shot-theme.ts +3 -3
  180. package/src/shot-verdict.ts +16 -7
  181. package/src/signal-shred.ts +27 -0
  182. package/src/solid-loader.ts +26 -2
  183. package/src/static-report.ts +8 -1
  184. package/src/sw-artifacts.ts +13 -3
  185. package/src/sync-url.ts +31 -0
  186. package/src/templates/action.ts +30 -16
  187. package/src/templates/entity.ts +12 -7
  188. package/src/templates/index.ts +1 -1
  189. package/src/templates/job.ts +10 -7
  190. package/src/templates/policy.ts +20 -2
  191. package/src/templates/resource-create.ts +127 -0
  192. package/src/templates/resource-form-island.ts +76 -30
  193. package/src/templates/resource.ts +10 -4
  194. package/src/templates/route.ts +3 -0
  195. package/src/templates/scaffold-app.ts +4 -1
  196. package/src/templates/scaffold-auth.ts +3 -1
  197. package/src/templates/scaffold-container-compose.ts +184 -0
  198. package/src/templates/scaffold-container.ts +25 -145
  199. package/src/templates/scaffold-dashboard-example.ts +2 -2
  200. package/src/templates/scaffold-db-package.ts +16 -3
  201. package/src/templates/scaffold-demo-org.ts +41 -0
  202. package/src/templates/scaffold-entries.ts +1 -1
  203. package/src/templates/scaffold-env.ts +6 -0
  204. package/src/templates/scaffold-helm-templates.ts +66 -7
  205. package/src/templates/scaffold-helm.ts +27 -5
  206. package/src/templates/scaffold-i18n.ts +15 -10
  207. package/src/templates/scaffold-repo.ts +13 -10
  208. package/src/templates/scaffold-roles.ts +38 -10
  209. package/src/templates/slice-foundation.ts +1 -1
  210. package/src/templates/wrap.ts +4 -1
  211. package/src/test-passes.ts +2 -1
  212. package/src/test-workers.ts +26 -0
  213. package/src/ts-scan.ts +3 -6
  214. package/src/tsconfig-references.ts +1 -2
  215. package/src/verify-checks.ts +41 -39
  216. package/src/verify-e2e.ts +41 -0
  217. package/src/verify-run.ts +115 -50
  218. package/src/verify-step.ts +3 -3
  219. package/src/verify-tests.ts +22 -30
  220. package/src/verify-typecheck.ts +28 -0
  221. package/src/web-binding.ts +2 -2
  222. package/src/worker-bundle.ts +192 -0
  223. package/src/workspace-graph.ts +10 -33
  224. package/src/cdp-browser.ts +0 -100
  225. package/src/cdp-connection.ts +0 -211
  226. package/src/cdp-e2e-page.ts +0 -209
  227. package/src/cdp-errors.ts +0 -56
  228. package/src/cdp-launch.ts +0 -139
  229. package/src/e2e-dom-fixture.ts +0 -117
  230. package/src/e2e-driver.ts +0 -97
  231. package/src/e2e-errors.ts +0 -103
  232. package/src/e2e-evaluate.ts +0 -156
  233. package/src/e2e-locator.ts +0 -86
  234. package/src/e2e-page.ts +0 -150
  235. package/src/e2e-selection.ts +0 -182
  236. package/src/measurement-actor.ts +0 -26
  237. /package/src/{dev-hooks.ts → runtime-hooks.ts} +0 -0
package/CLAUDE.md CHANGED
@@ -1,1609 +1,111 @@
1
1
  # @ultimat3/cli — boundary
2
2
 
3
- Tier 5. May import tiers 0–4. Nothing imports this except `create-ultimate`.
3
+ Tier 5. May import tiers 0–4. Declared sideways edges: `cli → admin` (`x dev` mounts the `/_x`
4
+ dashboard), `cli → testing` (islands, the e2e step, `x shot`'s `launchChrome`). Nothing imports
5
+ this except `create-ultimate`. The reasoning behind every rule below, verbatim and dated, is
6
+ [`docs/history/cli.md`](../../docs/history/cli.md); where the two disagree this file wins.
4
7
 
5
- | Rule | Detail |
6
- |---|---|
7
- | Entry | `src/bin.ts` (`#!/usr/bin/env bun`) — argv, stdout, exit code only. Before `dispatch`, `local-cli.ts` decides whether THIS process is the app's own CLI: a global `x` inside an app re-executes `node_modules/@ultimat3/cli/src/bin.ts` when that is a different realpath, because a second module instance of `@ultimat3/entity` is an empty registry (a zero-entity manifest, green, 2026-09-05). Same realpath (a workspace symlink), no app, a compiled binary, or `ULTIMATE_KEEP_GLOBAL_CLI` set: no hand-over |
8
- | stdout | `write-line.ts`'s `writeLine` — synchronous fd 1, never `process.stdout.write`, which truncates at the 64KB pipe buffer when `process.exit` follows. Exported, because `create-ultimate`'s entry point needs the same one |
9
- | stderr | `write-line.ts`'s `writeErrorLine` — the same loop on fd 2, for a line that is not the command's answer. A `CommandResult` declaring `stream: 'stderr'` is routed there by `dispatch.ts`'s `sinkFor`, and `x mcp serve --transport stdio` is the one case: its fd 1 carries JSON-RPC frames, so the `✓ mcp stdio serving 13 tools` line rendered after the loop was a malformed frame. Neither renderer carries `stream`, exactly like `hold` |
10
- | Boot logs under `--json` | `dispatch.ts` calls core's `setLogStream('stderr')` when `args.json` is set, once, for all thirty commands. `x db migrate --json` printed the boot logger's `ultimate migrate applied` and then the command's own object, so `json.load` raised on the second document. A server's stdout stays its log stream; this is the CLI process only |
11
- | Test execution | `test-shards.ts`'s `testArgs` — ONE `bun test --parallel=N` per PASS (`test-passes.ts`; a selection mixing `live`/`e2e` files with the rest is two passes, the serial one at `--parallel=1`), never N processes this repo packs itself. It did pack them, largest-first greedy over file SIZE, and the packer was deleted for buying **nothing**: four interleaved runs each on the 1296-file unit corpus gave 58.2/60.0/65.0/66.5s hand-packed against 54.5/57.8/61.7/64.5s under `--parallel=8`, within noise, because both are work-bound — 436.7s of file time is a 54.6s floor on 8 workers and the slowest single file is 20.5s. A greedy pack of 1296 small items lands near-optimal by accident. `--timings` is refused on the same evidence (#342). `--parallel` implies `--isolate`, so the per-file module registry is unchanged, and the per-worker database is too: `@ultimat3/testing`'s `workerId` already read `BUN_TEST_WORKER_ID`, which Bun sets 1..N. `ULTIMATE_TEST_WORKER` is set only for a single-shard `x test --worker I` rerun, which is one process |
12
- | Numeric flags | `flag-number.ts` — one reader for `--port` / `--workers` / `--shard`. A bare `Number.parseInt` accepts `4abc` and answers `NaN`, which turned three checks into ones that cannot fail |
13
- | Shell quoting | `shell-quote.ts`'s `quoteArg` — every value the CLI pastes into a `fix:` or a reproduce line, `exec.ts`'s missing-program refusal and `test-shards.ts`'s reproduce command both. A name holding a space or a `;` interpolated bare is an instruction that runs something else |
14
- | Missing positionals | `MissingPositionalError`, never `BadFlagError` (names a flag that does not exist) and never `UnknownCommandError` (says a known command form is not one). Its `example` is a REAL invocation — `x g route <name>` in a shell is a redirect |
15
- | Bare subcommands | `CommandSpec.defaultSubcommand`, **declared**. The parser answered `subcommands[0]` until 1.2.0, so `x db` ran `gen` — the migration GENERATOR — because it sorted first, and `x mcp` started a server. A command with no defensible default declares none and `MissingSubcommandError` refuses the bare form; `parse.test.ts` pins the set at exactly `db` and `mcp`. Its fix is `x help <command>`. Both forms answer now: `--help` is read off the flag loop and `readSubcommand` is SKIPPED when it is set, so `x db --help`, `x mcp --help` and `x pr --help` print usage instead of exiting 1 with this same refusal — which is what they did on every command taking a subcommand until 2026-08 (`parse.test.ts` pins it across the shipped registry) |
16
- | Closed flag values | a flag whose values are a closed set is READ through a function that refuses the rest — `cmd-build.ts`'s `readTarget`, `cmd-deploy.ts`'s `readMethod`, `cmd-routes.ts`'s `readSurfaceFilter`, `cmd-mcp.ts`'s `isTransport`. `=== 'helm' ? 'helm' : 'compose'` made `x deploy --method helmm` a COMPOSE deploy reporting `method: "compose"`, and `--surface App` reported `0 routes` and exit 0 — a typo and an empty table rendering identically. The set is the framework's own where one exists (`SURFACES` from `@ultimat3/render`), never a list restated here |
17
- | Passthrough | `CommandSpec.passthrough`, **declared**. `ParsedArgs.passthrough` says "handed to the underlying tool verbatim" and had no reader anywhere, so `x test unit -- --coverage --bail` parsed both flags and dropped both — a run that measured nothing reporting what a coverage run reports. `x test` forwards them to `bun test` (before the file list) and prints them back in its reproduce line; every other command refuses a non-empty `--` tail with `X_CLI_BAD_FLAG` rather than dropping it. `help`/`version` short-circuit ahead of the command and are exempt |
18
- | App root | `CommandSpec.requiresApp`, **enforced by `dispatch.ts`** before `target.run` — the field's doc said so for 17 commands and nothing read it, so the promise was kept only by each command remembering to call `requireAppRoot` itself. Those 17 calls stay (they hand the command its root, and name subcommands the dispatcher cannot see), but the DECLARATION is what decides, ahead of any check a command makes about its own arguments; `--help` is exempt, because `target` is the help command by then |
19
- | Result helpers | `command.ts`'s `ok()` / `failed()` write `ok` **after** the `extra` spread: the function's name is the verdict and nothing a caller passes can overturn it. Spread last, `failed('verify', '1 of 20 steps failed', { ok: true })` answered `ok: true` and `exitCodeFor` exited **0** on it — a green CI over a red command (`command.test.ts`) |
20
- | I/O | only `dispatch.ts` renders or exits; commands return `CommandResult` |
21
- | Staying up | a command still listening when `run` resolves returns `hold` (`hold.ts`), or `bin.ts` exits out from under it |
22
- | `--json` | every command, no exceptions — same data as the human render |
23
- | Errors | codes + titles in `src/error-codes.ts`, classes in `src/errors.ts`, subclass `UltimateError`, never a bare `Error`. A class may sit beside its one thrower when `errors.ts` has no room under the 500-line ceiling — `db-seed.ts` and `metrics-endpoint.ts` do |
24
- | Subprocesses | only through `exec.ts`, so a test can inject a fake `Runner` |
25
- | Templates | `templates/*.ts` return strings; no fixture files on disk |
26
- | Strings | rendered output through `messages.ts`, missing key renders `⟦key⟧` — see below for what is *not* rendered output |
27
- | Facts | load the app (`app-load.ts`), then project it — never parse source for primitives |
28
-
29
- Every fact the CLI reports comes from a framework package: the manifest from
30
- `@ultimat3/manifest`, `openapi.json` from `@ultimat3/action`, the route table from
31
- `@ultimat3/render`, budget units from `@ultimat3/render`, the `/_x` panels from
32
- `@ultimat3/admin`, the MCP tool catalog from `@ultimat3/mcp`, eval coverage from
33
- `@ultimat3/ai`. A check that reimplements one of those here is the bug, not the fix.
34
-
35
- `app-evals.ts` is why the `eval` step can apply with no eval suite at all: a prompt no eval
36
- names is `X_EVAL_MISSING`, an eval whose baseline was never recorded is `X_EVAL_BASELINE_MISSING`,
37
- and a skipped step would read as a green gate over untested code. Its third rule runs *before* the
38
- suite rather than beside it — `ULTIMATE_EVAL_RECORD` makes every eval write its own numbers and
39
- pass, so a gate that inherited the flag would rewrite the committed baselines during the run, and
40
- a finding after the fact does not put them back.
41
-
42
- `verify-floor.ts` is the suite ratchet, and it is split across two owners on purpose. `runVerify`
43
- judges the **suites**: a step the committed `x.verify.json` names that reports nothing to check is
44
- recorded failed and not skipped, so the failure count, `data.failed` and every step table another
45
- gate parses all carry it. The `manifest` step judges the **file**: a floor that does not parse, or
46
- that names a step the gate does not run, enforces nothing — and a ratchet nobody notices is off is
47
- the false green it exists to close. Nothing writes the file; a gate that edits its own floor
48
- ratchets in both directions.
49
-
50
- **"Nothing to check" is two conditions and one code.** `applies` sees the first — no files — and
51
- cannot see the second, because `describe.skipIf` is decided inside the child process: measured with
52
- no `TEST_DATABASE_URL`, `live` is `4 pass, 114 skip` and the step reported green over a suite whose
53
- whole subject is the database. `test-counts.ts` reads bun's own summary back (`parseBunTest`, the
54
- same reader `x mcp`'s `test.run` uses — a second regex over one format is drift), each runner
55
- attaches `StepOutcome.tests`, and a floor step whose `ran` is zero is `X_VERIFY_SUITE_VANISHED`
56
- with `skippedSuiteFinding`'s cause. **Zero, not a ratio**: one real assertion is a suite that runs,
57
- and a threshold would be a number nobody can defend. An absent `tests` is a step that spawned no
58
- test process at all (`eval` answering with declarations alone), which is not the same claim.
59
-
60
- `x new` writes an `x.verify.json` (`templates/scaffold-repo.ts`), or the code above is unreachable
61
- in every generated app — the repo shape that grows suites fastest. It names the eleven steps the
62
- scaffold has proved apply, and deliberately not `e2e`: the scaffolded `page.e2e.test.ts` is an
63
- `e2eTest`, which is `test.skip` until the app registers a browser driver, so pinning it would fail
64
- the app's first gate on the scaffold's own placeholder.
65
-
66
- `tsconfig-references.ts` is `package-shape`'s fourth rule: **every published workspace is in the
67
- root `references`**. `bun run typecheck` is `tsc -b`, which builds referenced projects and nothing
68
- else, so a package no reference names is one the gate's own `typecheck` step passes over without
69
- reading a line of it — `X_PACKAGE_UNREFERENCED`, whose `fix:` is the exact `{ "path": … }` entry
70
- and the `tsc -b` that proves it took. Private packages are exempt (a generated app's are all
71
- private), and a root that declares **no** `references` array is not judged at all: project
72
- references are opt-in, and a scaffolded app builds through `extends` + `include`.
73
-
74
- `workspace-graph.ts` is `package-shape`'s fifth rule: **every cross-workspace import is declared
75
- in the importing workspace's own manifest**. Without it a scaffolded repo's dependency graph exists
76
- only inside `tsc` — imports resolve through the root `tsconfig.json` `paths`, so affected-package
77
- detection, `bun --filter` ordering and "what breaks if I change this" all read manifests and all
78
- answer too small a set (issue #239, found in a real app where a change reaching five packages
79
- reported one). `X_WORKSPACE_DEP_UNDECLARED` names the manifest and the exact line to add. Shipped
80
- source only: a test file's import is not judged, because `packages/*` here declares no
81
- `devDependencies` by design and the root's hoist is what resolves them. A manifest the scan cannot
82
- read is its own finding rather than a silent skip — a skipped workspace is a hiding place for the
83
- very edge the rule is looking for.
84
-
85
- `app-permissions.ts` is the `policy` step. Two references in the whole
86
- framework are bare strings nothing checks — `RoleDef.grants` and `RouteGuard.permission` — while
87
- `can()` calls `assertPermission` and throws `X_PERMISSION_UNKNOWN` on the first request that
88
- reaches the route. So `x new` shipped an app that granted `dashboard:read`, required it on
89
- `/dashboard` and declared it nowhere: HTTP 500 on two of its three routes, from the first `x dev`,
90
- under a green gate. It reads `roleDefinitions()` and `routeEntries()` after `loadApp` and reports
91
- each reference `isKnownPermission` refuses — **that predicate and no other**, because it is the one
92
- `assertPermission` uses, including its rule that an app which has declared NOTHING is not checked
93
- at all. A gate that disagreed with the process it gates would be worse than none. The cause and the
94
- `fix:` are `permissionUnknown`'s, so `@ultimat3/policy` owns both wordings; `X_PERMISSION_UNKNOWN`
95
- is in `CLI_BORROWED_ERROR_CODES`. Its own step rather than a rider on `budgets`, by that step's own
96
- test: reported there, an authz defect would hand the reader a byte budget (axiom 4). It costs no
97
- second app load.
98
-
99
- `dev-replica.ts` is where read-replica routing is WIRED, and it had to be wired in two places
100
- because it was opt-in twice. `@ultimat3/db`'s `defaultClient()` is the one composer of
101
- `replicatedClient(primary, replica)` from `DATABASE_REPLICA_URL`, and it runs only from
102
- `baseClient()` — "the client an app installed none for" — while every process the framework boots
103
- calls `setDbClient` in `dev-queue.ts`, so no booted process had ever read that variable. Routing
104
- also needs an open `withReplicaReads` scope, and nothing opened one. `startDb` now installs the
105
- replicated pair as the AMBIENT client while keeping the primary for everything this boot does
106
- itself (`applySchema`, the queue's `PgExecutor`, `ping`, `close` — DDL and a claim are writes), and
107
- `cmd-dev.ts` / `serve.ts` prepend one middleware frame that opens the scope per request. Both
108
- halves are `undefined`/empty with no replica configured, and an EMBEDDED binding never gets one:
109
- PGlite has no standby. Not `@ultimat3/http`'s pipeline, which would make the HTTP tier know what a
110
- database is; the boot is the only tier that may know about a request and a pool.
111
-
112
- `flag-number.ts`'s `portPairAfter` is the one suggestion for a caller that binds a PAIR, and
113
- `neighbouringPort` is for a caller that binds one port. `x dev` takes `PORT` **and** `PORT + 1`, so
114
- the neighbour of a taken web port is the sync port — both `x doctor`'s `X_PORT_IN_USE` and
115
- `dev-sync.ts`'s handed back the port the finding had just named as taken, each under a test that
116
- said it ended the failure. It answers downward at the top of the range for the reason
117
- `neighbouringPort` does, and one further: the suggestion's own neighbour has to exist, or
118
- `syncPortFor` refuses it with `X_PORT_INVALID`.
119
-
120
- `port-probe.ts` is the one `portFree`, because two commands ask it and must not disagree:
121
- `x doctor` reports it as a finding for BOTH ports `x dev` binds — the web port and the sync port,
122
- which it derives with `syncPortFor` (the boot's own rule: a `--port` at the top of the range has no
123
- sync port at all, so the probe reports `X_PORT_INVALID` instead of probing the port BELOW it, which
124
- is what `neighbouringPort` answered there) — each labelled with the role that wants it — and `startSync` asks it after a failed
125
- `listenSyncNode` so a taken neighbour is `X_PORT_IN_USE` rather than `X_CLI_UNEXPECTED` over
126
- `Bun.serve`'s own English rendered into a `cause:`. It is ASKED, never read off the caught value,
127
- which is what `scripts/catch-render.ts` refuses; anything else the listener failed on is re-thrown
128
- untouched. `x doctor` also probes `DATABASE_URL` with a real `select 1` through
129
- `@ultimat3/db`'s `checkDb` — a TCP connect answers "reachable" for a running server with wrong
130
- credentials, which is the case an operator most needs told about — and reports `X_DB_UNAVAILABLE`
131
- with that package's own two-branch fix. An EMBEDDED binding is not probed: that lock is `x dev`'s.
132
-
133
- `doctor-offline.ts` is `x doctor`'s offline-fallback check, and it is the shape of a diagnostic
134
- that could not be closed. The probe was the literal `apps/web/app/offline.tsx` — a filename
135
- `assertRouteFilename` REFUSES (`X_ROUTE_FILE_INVALID`: the directory is the URL, so a page is
136
- `page.tsx`) — while `x new` scaffolds `apps/web/site/offline/page.tsx` and the finding's own `fix:`
137
- writes `apps/web/app/offline/page.tsx`. Three different paths, so every app the framework has ever
138
- produced reported `X_PWA_NO_OFFLINE_FALLBACK` from its first `x doctor`, running the fix changed
139
- nothing, and the app that HAD the route was told it did not. It now reads the DECLARED
140
- `pwa.offline.fallback` through `loadPwaArtifacts` and matches it against `describeRoutes()` —
141
- **`site/` only**, `As of 2026-09`. It accepted `app/` too, on the argument that both surfaces answer
142
- the same URL; true, and not the question. `SURFACE_SPECS` allows `app/` exactly `stream | ssr`, only
143
- a `static` route is prerendered, and `serviceWorkerArtifacts` precaches a rendered DOCUMENT
144
- (`documents.get(fallback)`) — so an `app/` fallback has nothing to precache and the offline
145
- navigation reaches the network it exists to survive without. The check and its own `fix:` disagreed
146
- about one code. Not closed by this: a `site/` route declaring `render: 'ssr'` is not prerendered
147
- either, and `NavigableRoute` carries no render mode. An app whose modules will not import is not
148
- judged at all, which is
149
- `appEntities`' rule (`schema-drift.ts`) one registry over. The fix is `x g route <name>
150
- --surface site` only where the fallback is one path segment the generator can really produce: a
151
- nested path slugifies to a DIFFERENT url, so there it is the config edit instead — a `fix:` that
152
- runs and leaves the finding where it was is the defect this file exists to end. **`site`, not
153
- `app`**: the document that answers a lost network has to render with no network, no session and no
154
- database, which `app/` (`ssr | stream`) cannot promise, and it is the line `@ultimat3/pwa`'s own
155
- `X_PWA_NO_OFFLINE_FALLBACK` hands out for the same code — two fixes for one code are two answers.
156
-
157
- `X_ENV_MISSING`'s fix is the file write, for the same reason. It was `x new --force`, which cannot
158
- run where the reader is standing: `x new` takes a `<name>` positional (`X_CLI_BAD_FLAG` without
159
- one) and with one it scaffolds a SECOND app beside the broken one. `cp .env.example
160
- .env.development` — the committed projection of `envSchema`, which is what `x env example` writes
161
- and `X_ENV_EXAMPLE_DRIFT` keeps honest.
162
-
163
- `i18n-index.ts` is the one writer of an app's `packages/i18n/src/index.ts`, shared by `x g` and
164
- `x i18n add|sync`. A catalog on disk and a SELECTABLE locale were two different sets: `x i18n add
165
- fr` wrote the file, exited 0, and left `x verify --only i18n` red with `X_CATALOG_UNREGISTERED`
166
- whose `fix:` named an edit that had already been made — an agent following it verbatim changes
167
- nothing and loops forever, on the command whose whole job is adding a locale. `unregisteredFix`
168
- (`i18n-registration.ts`) is the other half: one code over two causes, so where the index EXISTS and
169
- does not name the locale's own `catalogs/<tag>.json` import, the CLI substitutes a fix that
170
- performs the registration. The package's own "move the `defineCatalogs()` call" line still stands
171
- for the cause it was written for.
172
-
173
- `app-agents-md.ts` is why the `manifest` step declares no `applies` at all. The drift half needs
174
- a committed `x.manifest.json` to compare against, but `AGENTS.md` is required of every repo the
175
- gate runs in — so the step always has a question to answer, and gating both halves on the file
176
- that only the first one needs is how `X_AGENTS_MD_MISSING` stayed unreachable while its wiki row
177
- said it fails builds.
178
-
179
- ## What goes in `messages.ts`, and what does not
180
-
181
- `messages.ts` holds the strings a command *renders* — `CommandResult.summary`, `lines`, anything
182
- the human renderer prints. Three things stay inline, deliberately, and a review asking to move
183
- them is answered by this table rather than by a second convention:
184
-
185
- | Not in the catalog | Why |
186
- |---|---|
187
- | `CommandSpec.summary` / `.usage` / `FlagSpec.summary` | the spec is the command's declaration, next to the `run` it describes; parsing and `x help` both derive from it. All command modules declare it inline — moving a subset creates two places to look for one command's help |
188
- | `Finding.cause` / `Finding.fix`, and `BadFlagError`'s `reason` | stable machine-readable diagnostics. A `fix:` is copied and run verbatim; a translated one is a broken command |
189
- | Fixed-width table headers (`renderJobTable`, `renderRouteTable`) | column keys, not prose — the widths are computed from them and `--json` carries the same names |
190
-
191
- ## The introspection commands project registries, they never re-derive facts
192
-
193
- | Command | Files | Reads |
194
- |---|---|---|
195
- | `x actions` / `x queries` / `x entities` | `cmd-registries.ts` | the three declaration registries |
196
- | `x jobs` | `cmd-jobs.ts`, `jobs-{driver,report,drain,json,table}.ts` | `@ultimat3/jobs`' own introspection |
197
- | `x tasks` | `cmd-tasks.ts`, `tasks-facts.ts` | `registeredTasks()` + `@ultimat3/time`'s cron resolution |
198
- | `x policy` | `cmd-policy.ts`, `policy-facts.ts` | `@ultimat3/policy`'s `policyMatrix()` over the app's own `Policy` objects |
199
- | `x i18n` | `cmd-i18n.ts`, `i18n-audit.ts`, `i18n-registration.ts` | `@ultimat3/i18n`'s `extractFromFiles` + `auditCatalogs`, then the live catalog registry |
200
- | `x i18n check` and the `policy` step, before either registry | `duplicate-packages.ts` | `Bun.resolveSync` of `@ultimat3/i18n` / `@ultimat3/policy` / `@ultimat3/entity` from the app root, every workspace and the CLI's own directory, keyed by REALPATH. Two real directories is `X_PACKAGE_DUPLICATED`: a registry is per module instance, so the app registers into one copy and the CLI reads the other — `x i18n check` said "move `defineCatalogs()`" about a call that was already where it belongs (ai-maxxing, 2026-09-05), and the policy step was green over an undeclared grant. A workspace symlink to one checkout is one copy; two store entries at ONE version are two |
201
-
202
- Each pairs a `cmd-*.ts` of CLI wiring with a facts module that takes plain inputs and returns plain
203
- data, so the projection is testable without a `ParsedArgs` — the `cmd-jobs.ts` / `jobs-report.ts`
204
- split, repeated. Tables go through `table.ts`; a second padding helper is the drift it prevents.
205
-
206
- `x policy explain` exists because five packages already print it as the `fix:` on an authz denial
207
- (`policy`, `action`, `query`, `http`, `auth`), and `x i18n` because all three of `@ultimat3/i18n`'s
208
- own error fixes name it. A `fix:` line naming a command this build does not ship is the failure
209
- mode `cmd-planned.ts` closes for planned commands and these close for real ones.
210
-
211
- `x i18n check` scans source, which the "never parse source for primitives" rule below does not
212
- forbid: a `t()` call is not a primitive and no registry holds it. It uses `source-files.ts`, the
213
- same walk `errors` and `filesize` use, so the three cannot disagree on what the app's source is.
214
-
215
- **And then it asks the question the scan cannot answer.** A catalog complete on disk, its keys used
216
- everywhere in source, and an audit of one against the other were all green for an app that rendered
217
- `⟦key⟧` on every page — registration is a side effect of importing the module that calls
218
- `defineCatalogs()`, and nothing imported it (issue #249). `i18n-registration.ts` loads the app
219
- through `loadApp` — the same call `serveApp` makes at boot, so it is the boot's own answer and not a
220
- simulation of one — and compares the catalogs on disk against the live registry, per locale
221
- (`X_CATALOG_UNREGISTERED`). Two conditions, one code: a shipped catalog no module registered, and
222
- no catalog anywhere while source calls `t()` — the second is the vacuous green an app with an
223
- `app.config.ts` and no `packages/i18n/catalogs/` used to get. `loadApp`'s own findings ride along
224
- ONLY when something is unregistered, because "packages/i18n/src/index.ts: SyntaxError" is the
225
- evidence for the gap above it and noise on a pass.
226
-
227
- `catalogFindings(root)` is the one composition both callers report: `x i18n check` renders it as a
228
- table with a `registered` column, `x verify`'s **`i18n` step** returns it as findings. One
229
- implementation, so the command and the gate can never disagree about an app.
230
-
231
- **The generators emit `useT()` from the app's own catalog module, never `t` from `@ultimat3/i18n`.**
232
- The specifier is `resolveCatalogModule(root)` — `packages/i18n/package.json`'s `name`, read off
233
- disk, because a template is a pure string function and only a package name resolves as an import.
234
- An app with no such package keeps the framework import: emitting one that cannot resolve trades a
235
- wrong idiom for a file that does not compile. This is where the reported bug's idiom came from —
236
- every generated page imported `t` directly, so no page depended on the module that registers.
237
-
238
- **A catalog is authored nested and read flat.** `Catalog` (`{ 'nav.home': 'Home' }`) is the
239
- translator's form; the file on disk holds `{ nav: { home: 'Home' } }`, and `parseNestedCatalog`
240
- refuses a dot inside a key — so anything writing a catalog goes through `nestCatalog`
241
- (`serializeCatalog` for `x i18n add|sync`, `templates/catalog-json.ts` for every generator) or it
242
- emits a file `defineCatalogs` rejects at the app's first boot. `merge: 'json'` unions **deeply**
243
- (`json-merge.ts`) for the same reason: `x new` and `x g resource` both contribute under `app`, and
244
- a shallow spread keeps one of them.
245
-
246
- ## Three commands that reach outside the process, and none of them is a gate step
247
-
248
- `x shot`, `x pr` and `x ci` exist because of the one line in the root `CLAUDE.md` that shapes this
249
- whole package: **the primary developer is an AI agent.** An agent cannot open a browser, cannot look
250
- at a running dev server and cannot read the GitHub web UI. It can read a file, and it can run a
251
- command that prints. These three turn each of those into a file and a print.
252
-
253
- They are also the only three commands that need something the process does not have — a browser, a
254
- network, a GitHub token — which is why **none of them is a step of `x verify`**, and why that is not
255
- an oversight to be corrected later. A gate that needs a browser goes red for reasons unrelated to the
256
- change, and CI does not install one.
257
-
258
- | | Reaches for | Never |
259
- |---|---|---|
260
- | `x shot <route>` | `x dev` on a scratch port, plus the app's own `puppeteer-core` through `@ultimat3/scraping` — launching Chrome here, or **attaching** to one over `--cdp-url` / `SCRAPE_CDP_URL`, which is what every stealth provider sells and what `remoteBrowser()` has called its primary path since it shipped | the static build — `--target static` prerenders `site/` only, so an `app/` route would photograph the landing page |
261
- | `x shot --island <name>` | the same server and the same browser, plus the app's own `*.island.states.ts` | a second command — photographing a route and photographing a component are one job with two subjects, and `--island` with a route positional is refused by name |
262
- | `x shot --all-islands` | every `*.island.states.ts` in the app, one boot, one browser per declared VIEWPORT across all of them | a second capture loop — one island and every island are `runIslandSweep` with one argument, because two loops are two answers to what a run produced |
263
- | `x pr review\|resolve\|reply` | `gh api graphql`, through the injected `Runner` | `gh pr view --comments`, which shows *issue* comments and not the line-anchored threads that carry the findings |
264
- | `x ci` | `gh run view --log-failed`, one call | a per-job log fetch — the run and all its jobs come back together |
265
-
266
- **`verdict.json` names its own blind spots, and that is the design.** `x shot` reports what it could
267
- not observe alongside what it did. A capture tool that silently omits what it cannot see is worse
268
- than one that says so, because the omission reads as a clean result.
269
-
270
- ### `--island` photographs ONE component in a state nobody can click to
271
-
272
- A failed read, an empty list, over-quota, read-only: the states a reviewer most needs to see are the
273
- ones a running app will not produce on request. `--island` takes them, one address at a time.
274
-
275
- | File | Job |
276
- |---|---|
277
- | `island-states-load.ts` | discover `*.island.states.ts`, prove each pure, import it, check the set |
278
- | `island-harness.ts` | the document that mounts ONE island over `data-x-entry` / `data-x-props` |
279
- | `island-harness-script.ts` | what runs before the chunk does: the sealed network, the pinned clock, the readiness watch |
280
- | `island-harness-route.ts` | `GET /_x/island`, mounted by `x dev` |
281
- | `island-shot.ts` | the RUN: which islands, in which order, which artifacts — and the missing-shot gate |
282
- | `island-capture.ts` | ONE picture: the assertions before the shutter, the crop rectangle, the one session |
283
- | `island-shot-index.ts` | `index.md` as a PURE function of (manifests, targets, verdicts) — no I/O in the rule |
284
- | `shot-browser.ts` | which browser a run gets — launch one here, or attach over `--cdp-url` / `SCRAPE_CDP_URL` — as three rules over plain inputs |
285
- | `island-verdict.ts` | the per-state verdict — a PNG cannot say the component threw or logged |
286
- | `cmd-shot-island.ts` | the flags, the refusals, and the one browser per declared viewport |
287
-
288
- The vocabulary is **`@ultimat3/testing`'s**, not this package's: `defineIslandStates`,
289
- `islandShotTargets`, `islandAddress` / `parseIslandAddress`, `findIslandStates`,
290
- `assertIslandStatesPure`. `cli → testing` is a declared sideways edge and `cli → scraping` is
291
- another, which is what makes `@ultimat3/cli` the only package that can hold both the mount half and
292
- the screenshot half.
293
-
294
- **The expected picture list exists before a browser does.** `loadIslandStates` → `findIslandStates`
295
- → `islandShotTargets` is a pure expansion off files on disk, and the run ends by diffing it against
296
- what actually landed (`missingShots`, `X_SHOT_ISLAND_MISSING`). That diff is the point of the whole
297
- design: a loop that swallowed every failure would otherwise report a clean run with no pictures in
298
- it, and "produced nothing and exited 0" is the one outcome a reader cannot tell from success.
299
-
300
- **An unstubbed request FAILS the run.** The page's own seal replaces `fetch`, `WebSocket`,
301
- `EventSource` and `XMLHttpRequest` before the island's chunk is imported, answers the state's
302
- `routes` and publishes everything else on `window.__xShot.unstubbed`; the capture refuses on a
303
- non-empty list with `X_SHOT_ISLAND_UNSTUBBED_REQUEST`, naming each method and path. A component
304
- whose fetch quietly hangs paints its own loading branch, and the picture then shows a fixture gap
305
- dressed up as a real component state. `@ultimat3/testing`'s `sealNetwork()` is not reusable here:
306
- it patches THIS process's `globalThis.fetch` and the component runs in the page's realm.
307
-
308
- **Readiness is quiet, not idle.** Fonts ready, then N consecutive animation frames with an unchanged
309
- network-ACTIVITY counter — never "nothing in flight", which never comes for a state whose fixture is
310
- deliberately `pending`, and never a fixed sleep, which photographs whatever a slow machine painted.
311
-
312
- **Eight assertions before a shutter opens** (`photographFault`), each naming a fact the picture would
313
- have hidden rather than shown: no probe, not the harness, no host element, a mount that REJECTED, a
314
- mount that never finished, a page that never went quiet, a zero-sized box, a box with no children
315
- and no text. Then a byte floor as a backstop. Every one of them otherwise comes out as a plausible
316
- image of the wrong thing.
317
-
318
- **`--all-islands` is a spelling, never `--island` with no value.** The parser refuses a bare
319
- `--island` ("expects a value") and `--island=` is an empty NAME, so "every island" had no form a
320
- reader could type that could not be read as a mistyped one. A boolean cannot be confused with a
321
- name. Every contradictory pair is refused BY NAME and before a value is read — beside `--island`,
322
- beside a route positional, and beside `--state`, which belongs to ONE manifest's vocabulary
323
- (`empty` in two manifests is two unrelated states). An app declaring no states at all is refused
324
- too, rather than answered with an empty gallery: "produced nothing and exited 0" is the one outcome
325
- a reader cannot tell from success. Its `fix:` names `x g island`, and deliberately not a states
326
- generator — a `fix:` may only cite a command this build ships (`fix-command.ts`), and there is none.
327
-
328
- **A sweep never aborts on a failure.** Every state the app CAN photograph is captured, every
329
- per-island `verdict.json` and the index are written, and only then does the missing-picture gate
330
- turn the reasons into a non-zero exit. One island that will not mount must not cost a reader the
331
- other nineteen — which is the same rule the per-state loop already followed, one level up.
332
-
333
- **`.x/shot/island/index.md` is the file an agent opens**, and it is written for a SINGLE-island run
334
- too: the file that says what a picture IS cannot be a property of how many islands were asked for.
335
- `island-shot-index.ts` is a pure renderer over (manifests, targets, verdicts) — no disk, no browser
336
- — and the caller does the writing, which is the split every check in this repo uses. It carries the
337
- counts, the re-run commands, the verdict's own `blind` list (handed in, never reworded: two
338
- wordings drift), and per state the `id`, the `title`, the `note` — the *"you cannot reach this by
339
- clicking, because …"* line, which is the whole reason a reviewer knows what they are looking at.
340
-
341
- **A console WARNING and an overflowing box are recorded and gate NOTHING.** `stateShotOk` reads
342
- neither, on purpose: a signal that fails a run is a signal an author switches off. The warnings were
343
- already in `page.console()` and already in `--json`; what was missing is that nobody counted them.
344
- The overflow is new and is the readiness probe's own answer — `scrollWidth > clientWidth` on the
345
- crop target, measured in the same round trip, because a fact measured after the shutter is a fact
346
- about a different page.
347
-
348
- **The clip carries a margin, clamped to the document.** It was the readiness box EXACTLY, and a
349
- pixel-tight rectangle shaves off everything a component paints outside its border box — a
350
- `box-shadow`, an outline, a focus ring, a hairline border on a subpixel — so a reviewer reads a
351
- component with no elevation as flat. `ISLAND_CROP_MARGIN_PX` is the one constant; the clamp reads
352
- the document's own extent off the probe (`page`), never the viewport, and it may only ever make the
353
- frame BIGGER than the component's box — a clamp that could shrink it would crop the subject.
354
-
355
- **One session per picture, and that is not an optimisation to collapse.** `page.console()` and
356
- `page.pageErrors()` are bounded rings over the whole SESSION, so a shared one files state A's
357
- console errors under state B — and per-state attribution is the half of the artifact that gates.
8
+ Commands: `bun test packages/cli` (from the repo root — the test preload lives there),
9
+ `bunx tsc --noEmit -p packages/cli/tsconfig.json`.
358
10
 
359
- **The picture is the CROP TARGET, `As of 2026-08-26`** — the readiness probe's own box, which is
360
- the selector the manifest declared or the island's host element. Measured on `examples/dummy`
361
- before it: 720x560 for a component whose box the verdict reported, in the same run, as 688x104.
362
- `CaptureClip` had been on the port since #336 and `island-shot.ts` passed none, and this paragraph
363
- said the port "takes no clip rectangle" — so did the verdict's own `blind` list, which is a blind
364
- spot naming a capability the tool has, the same lie as one hiding a gap. The state's `viewport` is
365
- still what the page is LAID OUT in, so there is still one browser per declared viewport, memoised;
366
- it is no longer what the picture is.
11
+ ## Rules
367
12
 
368
- **The clip is translated, not copied.** `getBoundingClientRect()` answers VIEWPORT coordinates and
369
- a capture clip is in PAGE coordinates; they agree only at the origin, which is the one case a
370
- harness happens to be in and is a rule nothing enforces. So the probe returns `scroll` beside `box`
371
- and `clipFor` adds them — a component below the fold would otherwise crop a band it is not in, with
372
- a picture that looks like a picture and nothing anywhere to report it. `box` keeps meaning the DOM's
373
- own answer, because that is what the verdict publishes.
374
-
375
- **Both themes are photographed by emulating the PREFERENCE.** `page.colorScheme(target.theme)`
376
- before the navigation, so the first paint already has it. The harness's `data-theme` attribute stays
377
- — it is right for a component that READS a theme it does not own — but it is the OUTCOME of a theme
378
- decision, and a component that resolves `'system'` itself deletes it on mount: `x shot --island`
379
- reported four pictures and wrote two, byte-identical, same md5 (#338). Re-setting the attribute
380
- after readiness is not the repair; it photographs a state the component would never reach.
381
-
382
- **`loadApp` does not import a states file**, for the reason it does not import an island: it
383
- registers no primitive, and importing it would put `@ultimat3/testing` in the server module graph of
384
- every `x dev`, every `x build` and every gate step that loads the app (axiom 6).
385
-
386
- **`x shot` reuses a running `x dev` rather than booting a second one.** Embedded Postgres is
387
- single-writer, so a second boot is `X_DEV_ALREADY_RUNNING` and no picture is ever taken. A reused
388
- server's `stop()` deliberately does not clear the other process's lock.
389
-
390
- **`gh` is invoked through `ctx.runner`, never `Bun.spawn` directly** — that is what lets every test
391
- supply a reply table and assert the exact argv with no network and no `gh` installed. `GhOptions.fix`
392
- is a **required** field, so shelling out to GitHub without stating a remedy is a type error rather
393
- than a review comment. A GraphQL response is untrusted input and is parsed against a schema, never
394
- cast: a `null` where an id was expected would otherwise become a mutation against `undefined`.
395
-
396
- ## The browser-backed e2e driver lives here, because the adapter has nowhere else to be
397
-
398
- `@ultimat3/testing` declares `PageLike` and has never had a driver for it; `@ultimat3/scraping` owns
399
- the only real browser in the tree and speaks `ScrapePage`. Both are tier 5, so neither may import
400
- the other, and `testing -> scraping` would be a NEW sideways edge. This package already holds
401
- declared edges to **both** (`SIDEWAYS_ALLOW`, `scripts/lib/tiers.ts`) and is the one package allowed
402
- to know about everything — so the join is here, and it is the same rule
403
- `docs/architecture/01-package-map.md` states for wiring a route table into `pwa`.
404
-
405
- | File | Job |
406
- |---|---|
407
- | `e2e-driver.ts` | `installE2eDriver({ page, baseUrl })` — the ONE call an app's test preload makes. Registers `page` over its declaration and installs the `e2eTest` seam; returns the undo |
408
- | `e2e-page.ts` | `PageLike` over four members of `ScrapePage`, declared structurally so a test stands one up in six lines |
409
- | `e2e-locator.ts` | `LocatorLike` — a handle that resolves nothing until asked, one round trip per question |
410
- | `e2e-selection.ts` | what a locator SELECTS, as data, and the one in-page expression that resolves it |
411
- | `e2e-evaluate.ts` | the closure→string crossing, which is the only lossy edge in the adapter |
412
- | `e2e-errors.ts` | one constructor per refusal |
413
- | `e2e-dom-fixture.ts` | a document small enough to hold in a test and real enough to RUN the expressions above |
414
- | `cdp-browser.ts` | the two doors: `openE2eBrowserIfAvailable()` (undefined when there is no browser) and `openE2eBrowser()` (refuses by name), and the close that undoes both halves |
415
- | `cdp-launch.ts` | which Chrome, and starting it — the candidate list, the flags, and the endpoint read off its stderr |
416
- | `cdp-connection.ts` | CDP over Bun's own `WebSocket`: request framing, reply correlation by `id`, one-shot event waiters, the per-call deadline |
417
- | `cdp-e2e-page.ts` | `E2eBrowserPage`'s five methods over an attached, flattened session |
418
- | `cdp-errors.ts` | one constructor per way the browser half refuses |
419
-
420
- **Absent by default, and that is a requirement rather than a state.** Nothing here runs until
421
- `installE2eDriver` is called, so `hasE2eDriver()` still answers `false` and the gate's `e2e` step
422
- still refuses instead of passing over a browser it does not have. This paragraph also said "CI has
423
- no Chrome" until 2026-08-27, and that is false and was the reason issue #390's fourth requirement
424
- — a real browser check — was recorded as out of reach: GitHub-hosted `ubuntu-latest` ships one at
425
- `/usr/bin/google-chrome`, preinstalled, with no download step and no new dependency.
426
-
427
- **The browser is RAW CDP over Bun's own `WebSocket`, and carries no dependency.**
428
- `packages/scraping/src/cdp-port.ts` declares a ~25-method port because `ScrapePage` is a full
429
- scraping surface and its intended implementation is `puppeteer-core`. `E2eBrowserPage` is FIVE
430
- methods, and CDP's wire format is one JSON object with an `id` — so the whole thing an e2e driver
431
- needs is four small modules, which is why `x test e2e` needs nothing installed that `bun install`
432
- did not already put there. `e2e/cdp-browser.e2e.test.ts` drives a real Chrome against a real
433
- `Bun.serve` and asserts all five methods; `openE2eBrowserIfAvailable()` answering `undefined` is
434
- what makes it a SKIP on a laptop without one rather than a red step.
435
-
436
- **The load EVENT is the completion signal, never `Page.navigate`'s reply.** Measured on Chrome 150:
437
- a navigation that swaps the render process — `about:blank` → `http://localhost:<port>/`, the most
438
- ordinary one there is — loads the page, hits the server and answers a later `Runtime.evaluate` from
439
- the new document, and the navigate frame **never comes back at all**. A driver that awaited the
440
- reply waited out its full deadline on every first navigation. So `cdpConnect().once()` registers a
441
- `Page.loadEventFired` waiter BEFORE the send, and the reply is raced against it — still read, but
442
- only for `errorText`, which is the one place a refused navigation is named.
443
-
444
- **A CDP call is deadlined and a close settles every call in flight.** Without that, a suite whose
445
- browser died waits out one full deadline per call and reports a timeout, where the true fault is a
446
- dead browser. The four codes are four repairs, which is why they are not one:
447
- `X_CDP_BROWSER_MISSING` (install one), `X_CDP_LAUNCH_FAILED` (read the browser's own stderr, which
448
- the cause carries), `X_CDP_CALL_FAILED` (look at the page), `X_CDP_TIMEOUT` (raise the deadline).
449
-
450
- **`evaluate` is the edge that cannot be lossless.** `PageLike.evaluate` takes a closure and every
451
- browser port in this framework takes a string, so what crosses is `Function.prototype.toString()`
452
- and nothing else. A zero-parameter closure naming only page globals is supported; a native or bound
453
- function, a declared parameter and a method shorthand are refused STATICALLY, before a byte leaves;
454
- a binding the page does not have comes back named, from the page's own `ReferenceError`. Measured on
455
- Bun 1.3.14 and 1.4.0 alike — re-measured on both when the repo moved back to the 1.3 series, because a version-stamped claim that names one runtime is unread evidence on the other — and load-bearing: **Bun's transpiler folds `wanted === 3` to `!0` before `toString()` ever
456
- runs**, so a captured PRIMITIVE can vanish from the source and never fail at all, while a captured
457
- reference always survives as its name. No static rule in this process can see the difference — which
458
- is why the refusal is raised from the page's answer rather than from a scan of the source.
459
-
460
- **One of `E2eFixtures`' four members still refuses, and it is the one that is not a port gap.**
461
- `update()` needs a second build served under a new immutable build id, which is a fact about the
462
- SERVER, and no page port has ever been able to speak for one. `offline()`/`online()` FORWARD — to
463
- `E2eBrowserPage.offline`, which `cdp-e2e-page.ts` implements as
464
- `Network.emulateNetworkConditions` and `@ultimat3/scraping` implements through
465
- `CdpPageLike.setOfflineMode`. They refused until 2026-08-27 on a reason the tree contradicted on
466
- the day it was written. A fixture that silently no-opped would make the assertion after it read as
467
- proof — `offline()` followed by "the fallback rendered" is the app's ONLINE page passing an offline
468
- test — so an `E2eBrowserPage` that declares no `offline` still gets the refusal, now naming the
469
- method the double is missing rather than a capability the framework does not have.
470
-
471
- ## The service worker is emitted here, because the emitter needs facts only a build has
472
-
473
- `@ultimat3/pwa` shipped `generateServiceWorker`, `buildPrecacheManifest`, `offlineFallbackSource`,
474
- `backgroundSyncSource` and `pushSource` since it existed, and every one had **zero callers** outside
475
- its own package. So `pwa.offline`, `pwa.backgroundSync`, `pwa.push` and every route's own `offline:`
476
- were declarations with no build behind them, and no Ultimate app worked offline however its config
477
- was written (#390). `sw-artifacts.ts` is the caller.
478
-
479
- **Why here and not beside the manifest.** `loadPwaArtifacts(root)` needs a root and a config file;
480
- the worker needs the ROUTE TABLE and the ISLAND BUNDLE as well — facts only a booted app and a
481
- finished build have. Splitting them keeps `loadPwaArtifacts` callable before either exists, which
482
- `x doctor` and the icon writer rely on. The route table is `describeRoutes()`, the one projection
483
- `x.manifest.json`, `/_x`, the sitemap and `sw.js` are all built from, so a route added to the app
484
- cannot be missing from the precache manifest.
485
-
486
- | Surface | What it does with the worker |
13
+ | Rule | Detail |
487
14
  |---|---|
488
- | `cmd-dev.ts` | mounts `/sw.js` and `/x-sw-register.js`; built ONCE at boot and deliberately not rebuilt on the watcher tick — a worker that changes under a page it already controls is the update path, and re-emitting one per keystroke exercises it on every save |
489
- | `serve.ts` | the same two routes in the container, from the same call |
490
- | `prerender.ts` | writes both as FILES into the export — a static host runs no route table, so a `<script src="/x-sw-register.js">` in every document is a 404 unless the bytes are in the artifact. In TWO halves, `As of 2026-09-06`: `serviceWorkerHead(pwa)` before the render loop, because every document has to name the script, and `serviceWorkerArtifacts` AFTER it, because a precache revision is the content hash of a document that does not exist yet |
491
-
492
- **A precache revision is the DOCUMENT's content hash, and until 2026-09-06 it was the build id.**
493
- `pwaRoutes` projected four of `PwaRoute`'s eight fields, so every route entry read
494
- `{"url":"/","revision":"<buildId>","bytes":0}`: two deploys of a byte-identical site re-fetched
495
- every precached page, which is the one thing `packages/pwa/src/precache.ts`' own header says must
496
- never happen, and `DEFAULT_PRECACHE_WARN_BYTES` was a 5 MB budget over a total that could not count
497
- one byte of HTML. The cause was ORDER — `prerender.ts` emitted the worker before the render loop —
498
- so the emission moved after it and `ServiceWorkerInput.documents` carries `renderStatic`'s own
499
- `hash` and byte count per rendered path. `x dev` and `serve.ts` pass none and keep the build id:
500
- neither has rendered a page at boot, and inventing a hash for bytes that do not exist is a revision
501
- that never changes when the page does. **The offline document is fed through its own pair**, because
502
- `buildPrecacheManifest` adds it ITSELF as `reason: 'fallback'` ahead of every route and `add()`
503
- keeps the first entry per url — so the route entry of the same url is shadowed and only
504
- `offlineFallbackRevision` / `offlineFallbackBytes` (`ServiceWorkerConfig`, `@ultimat3/pwa`) can
505
- decide it. That pair was declared and fed by nobody for one commit; `serviceWorkerArtifacts` reads
506
- the fallback path out of the same `documents` map, so the one page an offline navigation depends on
507
- is content-addressed like every other. A fallback this pass did not render — no route serves it,
508
- which `x doctor` reports as `X_PWA_NO_OFFLINE_FALLBACK` — is absent from the map and keeps the
509
- build id.
510
-
511
- **Registration is an EXTERNAL script, never inline**, and that is a CSP fact rather than a
512
- preference: `startWeb` computes a `script-src` sha256 per inline script, so an unhashed one is
513
- blocked in the container while passing report-only under `x dev` — which is how the hydration
514
- runtime shipped broken once already.
515
-
516
- **`sw.js` is served `no-store` with `Service-Worker-Allowed: /`.** A cached `sw.js` is a worker that
517
- cannot be replaced: the browser re-fetches it to decide whether an update exists, and an
518
- intermediary answering the old bytes pins every client to the deploy that shipped them. Without the
519
- header the browser refuses to let a worker served from `/` control `/` — the failure `assertScope`
520
- cannot see, because the scope a REGISTRATION asks for has to be allowed by the script's own response
521
- and not only by its path.
522
-
523
- **`api/` and `shared/` never cross.** An API response is a JSON document whose freshness is the
524
- app's business, and precaching one serves a stale answer to a client that had a network; `shared/`
525
- is not a URL at all. The filter is a `flatMap` rather than `filter().map()` because the predicate
526
- does not narrow `surface` for the map that follows it, and a cast would hide the day a fifth surface
527
- arrives.
528
-
529
- **`pwa.push` is read and still wires nothing, and it says so.** `generateServiceWorker` emits a push
530
- handler only when a VAPID key comes with the capability, there is no `pwa.vapid` config key, and it
531
- drops the handler in SILENCE otherwise. `pushWarning` is this module's own finding, reported through
532
- `x build --json`'s `serviceWorkerWarnings` — `jobs.driver`'s shape one package over, refused the same way.
533
-
534
- **The browser check is what let any of this ship.** #390's fourth requirement was *"a real browser
535
- check that the emitted worker installs, activates and serves the fallback offline. Until it exists,
536
- do not ship the worker"* — a bad `sw.js` is sticky in a way a manifest is not.
537
- `e2e/service-worker.e2e.test.ts` registers the emitted file in a real Chrome, waits for it to take
538
- control, takes the network away, and asserts that a runtime route with nothing cached renders the
539
- offline document.
540
-
541
- **And it found the driver bug first.** `E2eFixtures.offline()` did not take the SERVICE WORKER
542
- offline: a worker fetches on its own CDP target, the condition was only ever set on the page's, and
543
- a `networkFirst` route the cache had never seen still answered from the network. So an offline
544
- assertion made on a PWA tested nothing. `cdp-e2e-page.ts` now auto-attaches worker targets and
545
- carries the condition onto each, including one that attaches AFTER `offline(true)` — the ordinary
546
- case for a PWA.
547
-
548
- ## The `errors` step enforces the error contract
15
+ | Entry | `src/bin.ts` — argv, stdout, exit code only. `local-cli.ts` re-executes the app's own `node_modules/@ultimat3/cli` when a global `x` is a different realpath: a second `@ultimat3/entity` instance is an empty registry |
16
+ | Registry | `registry.ts`: every declaration static (`cmd-<name>-spec.ts`), every body `await import()`ed when that command runs. A command body is never imported to answer the parser or `x help` — `registry-lazy.test.ts`. A spec's constants live in the spec file or a leaf module, never in the body |
17
+ | Startup cost | `sass` loads on the first stylesheet compile (`@ultimat3/render`'s `compileStylesheet`), Babel on the first island transform (`solid-loader.ts`); both pinned by `*-lazy.test.ts` in a child process |
18
+ | Module names | `role-*` / `runtime-*` are booted by `serve.ts` in production; `dev-*` is `x dev`'s alone. Derived from the import graph by `module-naming.test.ts`, never listed |
19
+ | stdout / stderr | `write-line.ts`'s `writeLine` / `writeErrorLine` — synchronous fd writes, never `process.stdout.write` (truncates at the pipe buffer before `process.exit`). A `CommandResult` with `stream: 'stderr'` goes to fd 2 (`x mcp serve --transport stdio`) |
20
+ | `--json` | every command; `dispatch.ts` sets core's log stream to stderr under it. Same data as the human render |
21
+ | I/O | only `dispatch.ts` renders or exits; commands return `CommandResult`. `ok()` / `failed()` write `ok` after the `extra` spread — the verdict cannot be overturned |
22
+ | Staying up | a command still listening when `run` resolves returns `hold` (`hold.ts`); the release runs inside the drain's own deadline |
23
+ | Test execution | `test-shards.ts`'s `testArgs` — one `bun test --parallel=N` per pass (`test-passes.ts`); `live`/`e2e` run serially (`SERIAL_TYPES`, `test-workers.ts`) |
24
+ | Numeric flags | `flag-number.ts` — one reader for `--port` / `--workers` / `--shard` |
25
+ | Shell quoting | `shell-quote.ts`'s `quoteArg` for every value pasted into a `fix:` or a reproduce line |
26
+ | Missing input | `MissingPositionalError` (its `example` is a real invocation); a bare subcommand is refused unless the spec DECLARES `defaultSubcommand` (`MissingSubcommandError`); `--help` short-circuits both |
27
+ | Closed flag values | read through a function that refuses the rest (`readTarget`, `readMethod`, `readSurfaceFilter`, `isTransport`), from the framework's own set where one exists |
28
+ | Passthrough | declared per command (`CommandSpec.passthrough`); every other command refuses a `--` tail with `X_CLI_BAD_FLAG` |
29
+ | App root | `CommandSpec.requiresApp`, enforced by `dispatch.ts` before `run` |
30
+ | Declared flags | every flag a command declares is read in this package's source — `flag-reads.test.ts` (`X_CLI_FLAG_UNREAD`) |
31
+ | Planned commands | `PLANNED_COMMANDS` / `PLANNED_SUBCOMMANDS` (`cmd-planned.ts`): in the registry, exit `X_NOT_IMPLEMENTED` with a fix naming a shipped command |
32
+ | Errors | codes + titles in `error-codes.ts`, a runnable line per code in `mcp-errors.ts` (typed over `CliErrorCode`), classes in `errors.ts` or beside their one thrower when `errors.ts` is at its ceiling. Never a bare `Error` |
33
+ | Subprocesses | only through `exec.ts`'s injectable `Runner`; `gh` through `ctx.runner` with a required `fix` |
34
+ | Templates | `templates/*.ts` return strings, pre-formatted for Biome (`wrap.ts`'s `wrapList` / `wrapImport` / `sortSpecifiers`); no fixture files on disk |
35
+ | Strings | rendered output through `messages.ts` (missing key renders `⟦key⟧`). NOT in the catalog: `CommandSpec` summaries and usage, `Finding.cause` / `fix`, fixed-width table headers |
36
+ | Facts | load the app (`app-load.ts`), then project a framework package's registry — never parse source for primitives, never re-derive a fact another package owns |
37
+ | Public API | `src/index.ts`, explicit re-exports only. Every name there is a semver promise; a name nothing outside this package reads is not exported (22.0.0 pruned 236) |
38
+
39
+ ## File map
40
+
41
+ ### Gate steps (`x verify`)
549
42
 
550
43
  | File | Job |
551
44
  |---|---|
552
- | `ts-scan.ts` | the masking every scan shares, the `X_*` codes a file declares, and the ones it says it borrows |
553
- | `fix-scan.ts` | the strings a `fix:` can evaluate to: under a key, at a factory's argument, at a class constructor's |
554
- | `fix-imports.ts` | which of those factories a file can call that it did not declare — one relative specifier, one file read |
555
- | `error-contract.ts` | the rules, the two checks that turn them into findings, and `collectDeclaredCodes` |
556
- | `fix-command.ts` | resolving an `x <command>` a `fix:` cites against the registry |
557
- | `fix-path.ts` | resolving a PATH or a glob a `fix:` cites against the root the gate is running in |
558
- | `source-files.ts` | which files are shipped source — shared with `filesize`, never a second list |
559
-
560
- **A `fix:` may not cite a command this build does not ship.** Six shipped fix lines named
561
- `x db status`, `x logs tail`, `x trace`, `x metrics`, `x auth whoami` and `x ai prompts`, and every
562
- one passed — the text rule checks that a fix NAMES a command, never that the registry holds it.
563
- `fix-command.ts` resolves the citation, and a PLANNED command fails too: `x logs` parses, `x help`
564
- lists it, and running it hands the reader `X_NOT_IMPLEMENTED` instead of the fix.
565
-
566
- **A `fix:` may not cite a file this repo does not have, either.** That was the other half, and
567
- nothing resolved it: `X_UI_RUNTIME_MISSING` told its reader to paste a line no generator ever wrote,
568
- through every gate since it shipped (#274, #246). A file token is one of the four things that make a
569
- fix an instruction at all (`COMMAND_TOKENS`), so `fix-path.ts` is built from the SAME extension list
570
- — a token that satisfies the instruction rule is exactly the token this one has to resolve, and two
571
- lists would be a citation the second rule cannot see. `X_ERROR_FIX_PATH_MISSING` is its own code:
572
- `X_ERROR_FIX_INVALID` means the fix is not an instruction, this one means it is one and points at
573
- nothing, and the repairs differ.
574
-
575
- It is narrow so a finding never has to be argued with — three shapes are not judged at all, because
576
- each resolves against something other than the root the gate is running in: a scoped specifier
577
- (`@ultimat3/ui/global.scss`, which resolves through `node_modules`), a dot-relative path
578
- (`./global.scss`, which resolves against the reader's own file) and any path whose **parent
579
- directory** this root does not have (`src/errors.ts`, `apps/web/server.ts`,
580
- `packages/i18n/catalogs/en.json` — all three name a directory a generated app has and this repo does
581
- not). What is left is the citation a root really can answer: a directory that exists, named as
582
- holding a file it does not hold. A glob must match at least one file. Measured over all three roots
583
- the gate runs in — the framework, `examples/dummy`, `dummy/social-media-clone` — **117 path citations
584
- read, 0 findings**, so it enforces outright with no pin table.
585
-
586
- The rule is **conditional, and that is load-bearing**: *if* a fix cites `x <command>`, it must
587
- resolve. It does not require every fix to name one — `set OTEL_EXPORTER_OTLP_ENDPOINT=…` and
588
- `counter('orders_total', { maxSeries: 4000 })` are executable and correctly cite nothing, and a
589
- universal rule would push an author into citing a command that does not really fix it. A second
590
- word is judged as a subcommand only when the spec declares subcommands, or `x new my-app` reports
591
- `my-app` as one. The registry arrives through `await import('./registry')` — `registry → cmd-verify
592
- → error-contract` closes a cycle back to the caller, and the precedent for the break is
593
- `cmd-build.ts`.
594
-
595
- **It reads a THIRD word, under the same condition.** `x db branch ls --json` resolved — `db` is a
596
- command, `branch` is one of its subcommands — and the word that decided what actually ran was never
597
- looked at, so a fix line that created a stray database passed every check the repo had. A third
598
- word is judged only where the subcommand declares a closed set (`CommandSpec.subcommandPositionals`,
599
- declared from the constant the command validates against), because `x jobs show <id>` and
600
- `x db gen "add publish_at"` take open positionals and a universal rule would report findings about
601
- working invocations. `positionalChoices` cannot express it: `fix-command.ts` reads that field only
602
- where a command declares no subcommands at all.
603
-
604
- **A command with no subcommands must still declare `positionalChoices`, or its second word is unjudged.** `x g migration` shipped in two `@ultimat3/admin` fix lines — a generator that has never existed, answered with `X_CLI_UNKNOWN_COMMAND` when run — because the `g` spec declared none and `fix-command.ts` returns `undefined` for an absent set. The third instance of this class (#131, then `x db branch <name>`). `cmd-generate.ts` now declares `positionalChoices: GENERATORS`, from the SAME constant `readKind` validates against, exactly as `cmd-test.ts` declares `TEST_TYPES` — `fix-command.test.ts` pins `x g migration` as a finding against the real catalog. A new command whose first positional is a closed set and does not declare it is a hole this rule cannot see.
605
-
606
- **And in that one slot, a `<placeholder>` is a finding too.** `x db branch <name>` is what two
607
- `@ultimat3/mcp` fix lines said; the citation reader does not read `<name>` as a word, so the slot
608
- was never examined and the line resolved clean while running it answers `X_CLI_UNKNOWN_COMMAND` —
609
- the same blind spot in a second disguise. A closed set means the slot is a verb, so there is
610
- nothing a reader could substitute that would make it run. `CITATION` therefore matches a
611
- placeholder in the third slot **only**: `x jobs show <id>` and `x db branch drop <name>` are correct
612
- fix lines and must stay invisible to this rule.
613
-
614
- `collectDeclaredCodes` is the only answer to "which codes exist, and where is each declared?" — one
615
- walk, one entry per code, the owning registry preferred over any throw site and over a registry
616
- that named the code in its `<PKG>_BORROWED_ERROR_CODES`. The docs check reads it and so does the
617
- framework's own `framework.manifest.json`, because a second scanner over a narrower file set is a
618
- manifest that claims completeness it does not have.
619
-
620
- **A `code:` is a literal, a module-scope const in the same file, or a finding** — `As of 2026-08-23`,
621
- and until then it was a literal or silence. `scanCodes` matched `code\s*[:=]\s*'X_…'`, so
622
- `const STALE = 'X_DOC_PACKAGE_GRAPH_STALE'` followed by `code: STALE` — the DRY thing to write, and
623
- what `scripts/package-map-graph.ts` really wrote — was a declaration to nobody: no manifest row, no
624
- row demanded on `wiki/Error-Codes.md`, no entry for `bun run gate-codes`, and `x errors explain`
625
- answering `X_ERROR_CODE_UNKNOWN` for a code the build throws. Silent, and in the **permissive**
626
- direction: the DRYer the author, the less the gate saw (#277).
627
-
628
- `scanCodeDeclarations` is that one pass, and it returns both halves. It resolves the identifier
629
- against the module-scope consts of the **same file** — anchored at column 0, which is what makes it
630
- module scope without a parser — and reports every name it cannot resolve as `X_ERROR_CODE_UNRESOLVED`
631
- rather than skipping it, which is the whole point: a scanner that reads only what it likes enforces
632
- only what it sees. `scanCodes` is its `.sites`, so the manifest, the docs check, `bun run gate-codes`
633
- and `x errors explain` (through `scanCodeFixSites`, which resolves the same way) cannot see different
634
- sets. Cross-file resolution was **refused** even though `fix-imports.ts` already does the harder
635
- version for `fix:`: it would make the scan async for every caller, and the finding is the better
636
- answer anyway — one file holds both the code and its only spelling.
637
-
638
- Three shapes are deliberately not judged, each measured over the framework and both tracked apps
639
- before the rule shipped. A name that resolves to something that is **not** a code is an answer, not
640
- a gap (`const STATUS_NOT_FOUND = 404` in `@ultimat3/realtime`'s NATS fake, the one live instance). A
641
- **table read** is not judged — `SEO_ERROR_CODES.metaMissing` is how `@ultimat3/seo` and
642
- `@ultimat3/ui` raise all 18 of their codes, and the registry those literals live in already declares
643
- them. A **lowercase** name is not judged: 164 sit at a `code:` position in this tree and every one is
644
- a type annotation (`readonly code: string`) or a re-raise (`code: opts.code`). Measured on all three
645
- roots: **0 findings**, so it enforces outright with no pin table.
646
-
647
- An empty `fix`, or a `fix` that says `check` / `make sure` / `try` / `see the docs` and names no
648
- command, call or file path, is `X_ERROR_FIX_INVALID`. A declared code the host's error reference
649
- does not name is `X_ERROR_CODE_UNDOCUMENTED` — `wiki/Error-Codes.md` here, nothing in a generated
650
- app, which is why that half arrives as a host check (`scripts/verify.ts`) rather than a hardcoded
651
- path in this package.
652
-
653
- `ts-scan.ts` masks comments and string contents before it looks for structure. The contract's own
654
- 3-line rendering appears verbatim in doc blocks and interpolated messages, and a scanner that read
655
- those as declarations would report findings nobody can fix. What it cannot see is a `fix` with no
656
- literal — a parameter, or a table lookup with no fallback. Those are out of a static scan's reach,
657
- and the step says so rather than guessing.
658
-
659
- **A fix does not always arrive under a key**, and until `As of 2026-08` the scanner assumed it did.
660
- `@ultimat3/mcp`'s `readonly-sql.ts` hands every fix positionally to a local `rejected(cause, fix)`
661
- helper, so the file held no `fix:` at all and `scanFixes` returned `[]` for all of it — the
662
- citation resolver was never given a string to judge, and two stale `x db branch <name>` lines
663
- shipped through the hole. `scanFixes` now also reads the argument in the `fix: string` position of
664
- a **local** helper, under four rules, each with its own case in `fix-scan.test.ts`: the helper must
665
- BUILD an error (`code` key or `new …Error(` in its body), or `citedCommandProblem(fix, catalog)` —
666
- which takes a fix to *judge* it — would have its call sites read as declarations; the parameter
667
- list may hold no rest or destructured parameter, because neither has a reliable position; the call
668
- may not be a member access; and the argument must BE one literal, stricter than the key path,
669
- because `prefix + 'x doctor'` reads as one literal there and publishing half a fix is worse than
670
- publishing none. Measured over the whole tree: 16 files gained readable fixes, `readonly-sql.ts`
671
- went from 0 to 7, and **zero** new findings.
672
-
673
- **And a fix does not always arrive in the file that declares its builder**, which is where four
674
- bad `fix:` lines in `packages/ui/src/icons/build-icons.ts` shipped: `invalidIconDataError` is
675
- declared in `packages/ui/src/errors.ts`, and a per-package `errors.ts` full of factories is the
676
- house pattern, so the same-file rule left the most common shape of all unchecked. `fix-imports.ts`
677
- resolves it — the specifier is relative, the candidate paths are `<base>.ts{,x}` and
678
- `<base>/index.ts{,x}`, and the parameter position is the callee's. An alias is renamed to what the
679
- CALLER writes; a local declaration of the same name wins, because that is the function the call
680
- actually reaches. One module cache per run: `errors.ts` is imported by every file in its package.
681
-
682
- **An error CLASS is the same helper one keyword away**, and is now read too: the name is the
683
- class's, the parameter list its `constructor`'s, `new X(…)` is a call like any other. It was
684
- measured as dead code in the same-file rule — zero same-file call sites — and cross-file it is
685
- `@ultimat3/render`'s fourteen classes plus `@ultimat3/core`'s three image ones.
686
-
687
- Measured over the whole tree, `As of 2026-08`: **791 → 877** fix literals read, 37 files gained
688
- one, and **3 findings** the gate had never been able to see — `x verify --contract` and
689
- `x build --route` (two flags no command declares) and one `check …` line with no command token.
690
-
691
- What it still cannot see is a builder imported from another **package**: `candidatePaths` refuses a
692
- non-relative specifier, because resolving one means guessing which of 29 packages a bare name came
693
- from and a wrong guess reads an unrelated function's argument as a fix. Measured: 3 call sites in
694
- this repo, none of them a finding. It is **not** left silent — the step's `output` carries
695
- `checked {n} fix line(s), could not read {m}`, counted at `FixScan.unreadable`: an argument in a
696
- KNOWN fix position that is not one literal. Deliberately not "imports I could not open", which is
697
- 1504 names here and 1310 of them are `join` and `UltimateError` — a number nobody can act on.
698
-
699
- `cli → admin` is a declared sideways edge (`scripts/lib/tiers.ts`): `x dev` **mounts** the
700
- dashboard, it never grows a second one. The CLI's only contribution is the facts no registry
701
- holds — a SQL runner, the caught outbox, the committed manifest, the process's own services, the
702
- spans it recorded — supplied as `defaultDevSources({ hooks })`.
703
-
704
- Wired means answerable: all eleven panels answer in a `x dev` process, and a hook the CLI does
705
- not supply is a panel that refuses with a wiring line, never one that renders empty. `timeline`
706
- is core's tracer (`x dev` is what calls `configureTelemetry`), `cache` is
707
- `recentInvalidations()`, `policy` is `@ultimat3/policy`'s own `policyMatrix()` over the app's
708
- roles — a verdict re-derived here would be the second authz the framework exists to prevent.
709
- `subscribers` is the one source left unwired: `@ultimat3/realtime` retains no matcher trace, and
710
- that trace is the live panel's question, so the panel degrades to its own note instead.
711
-
712
- `dev-traces.ts` reads a span's panel kind off its **name prefix** — a subsystem that starts emitting
713
- spans adds its prefix to `KIND_BY_PREFIX` or its work is filed under `action`. `db.` is there
714
- because `@ultimat3/db`'s two funnels open one span per statement (`db.select`, `db.begin`), and a
715
- statement is the one span that states its own identity — `STATEMENT_ATTRIBUTE`, **imported** from
716
- `@ultimat3/db` by both `dev-traces.ts` and its test rather than spelled as a literal, which the
717
- recorder prefers over the name, so the timeline's `repeatedSql` groups SQL texts and not span names. Those spans
718
- exist only where a `StatementObserver` is installed, so a trace with no DB children is a process
719
- with no statement diagnostic, not a broken recorder.
720
-
721
- `dev-n-plus-one.ts` is that observer, and `cmd-dev.ts` is the **only** place that installs it —
722
- `serve.ts` installs neither it nor the in-process trace RECORDER (`createTraceRecorder`, which is
723
- `/_x/timeline`'s source), the same line that file already draws for `/_x`. **It is not "no
724
- exporter"**, `As of 2026-08`: `serve.ts` calls `startOtlpExport(options.env)`, because a collector
725
- named in the chart has to receive spans from the container and not only from a laptop. What a
726
- production process does without is the *statement* diagnostic and the in-memory timeline — the
727
- ledger and the recorder go in together and come out together in `stop()`, because the timeline's
728
- SQL rows and the repeat counts are one feature with one toggle, and uninstalled the seam costs the
729
- one `undefined` branch it already pays (axiom 6).
730
-
731
- Three rules hold the ledger, each load-bearing. **Per request, keyed by the `Ctx` object** — a
732
- `WeakMap` whose entry dies with the request, so nothing sweeps and nothing accumulates across a dev
733
- session; a statement issued outside a request is not counted at all, because "five of one shape"
734
- only means something inside one unit of work. The price of keying on identity is that a
735
- `withChildContext` scope is its own tally. **A shape is `entity.op` when attributed**, the
736
- statement's own text with whitespace collapsed when it is not — `members.findById` fifty times is
737
- what an author can act on, and grouping fifty point lookups by their SQL would report bind values.
738
- That rule is **not written here**: `statementFingerprint`/`statementKind` are `@ultimat3/db`'s and
739
- the threshold is `@ultimat3/entity`'s `N_PLUS_ONE_THRESHOLD`, because `@ultimat3/testing`'s
740
- `statements` fixture is a second detector and a copy of either would let a loop that fails a test be
741
- a different loop from the one this ledger warns about. What stays here is what only a dev *server*
742
- knows: the request as the unit of work, the bound report list, one log line per request per code.
743
- **An expected statement is not counted** — `expectedQueryLoop` suppresses a verdict and this ledger
744
- is the verdict, so the span and the timeline still show the loop while the thing that warns is told
745
- the author already answered. A shape is promoted to a verdict exactly once, on the statement that
746
- crosses the threshold, and its count keeps rising: a loop of fifty is one report reading fifty. The
747
- report list is bounded and drops its oldest.
748
-
749
- `statement-loop.ts` is the **one** projection those verdicts reach four surfaces through, and the
750
- reason there is only one is that four renderings of one loop must be one sentence. It hands a
751
- verdict to `@ultimat3/entity`'s `nPlusOne()` — the `fix:` speaks that package's vocabulary and is
752
- derived from the relations the schema already declared — and each surface takes a field of what
753
- comes back: `cmd-dev.ts` appends `loopFinding` to the `findings` getter (text and `--json` render it
754
- for free), `dev-dashboard.ts` supplies `statementLoops` so `/_x/timeline` shows `nPlusOne` for the
755
- request on screen, `cmd-dev.ts` again passes `devNotices` down `startRoles` so the browser overlay
756
- renders the loop under the error, and the ledger itself emits `warnLoop` — one `logger.warn` per
757
- request per code, the ids riding along from core's `setLoggerContextFields`.
758
-
759
- Two rules about *when* a count is read. **A surface reads it live**: the finding, the panel row and
760
- the notice all say `ran 50 times` because they ask after the loop finished, while the log line says
761
- `ran 5 times` because it was written the moment the threshold was crossed — same verdict, two
762
- honest moments. **A verdict belongs to its request**: `repeatsFor(ctx)` reads the request's own
763
- tally rather than filtering the bounded global list, so the overlay still names a loop the bound
764
- already dropped. `serve.ts` supplies no `devNotices`, so the seam it boots through is a key that is
765
- absent, not a hook answering an empty list.
766
-
767
- `dev-n-plus-one.test.ts` and `statement-loop.test.ts` drive the ledger and the projection with
768
- hand-built `StatementEvent`s — fast, and enough to pin every rule above. `n-plus-one-detector.test.ts`
769
- proves the loop those events stand in for: real `posts`/`authors` entities, `postgresRepo` and
770
- `createPgliteClient` (an injected fake driver so no `@electric-sql/pglite` build is needed, but a
771
- real client — `createRecordingClient` implements `DbClient` on its own and never reaches the
772
- observer, so it cannot stand in here) — a naive per-row `findById` loop trips `X_N_PLUS_ONE_QUERY`
773
- with the exact `preload('author')` line, the `preload()` form of the same read stays quiet,
774
- `expectedQueryLoop` silences the naive form without stopping it from running, and a naive per-row
775
- `delete` loop trips `X_N_PLUS_ONE_WRITE`. Its describe block spells the pattern `n1`, matching
776
- `packages/entity/src/n-plus-one.test.ts`'s own fixture prefix, because `bun test -t 'n+1'` is a
777
- regex and `+` is a quantifier — `n1` is what actually selects these tests.
778
-
779
- ## One migration engine, four environments
45
+ | `verify-checks.ts` / `verify-step.ts` / `verify-run.ts` | the step list, the outcome shape, the run order (`BESIDE_SERIAL_SUITES`) |
46
+ | `verify-floor.ts` | `x.verify.json`: a floor step that ran zero tests is `X_VERIFY_SUITE_VANISHED` |
47
+ | `load-findings.ts` | module-load failures are reported once, by `manifest`; other steps point there |
48
+ | `boundary-findings.ts` / `app-boundaries.ts` / `boundary-cuts.ts` | surface + layer rules; a finding's `fix:` is the concrete cut |
49
+ | `guards.ts` | an app's `guards/*.ts`, discovered, run on `boundaries`, held to the error contract |
50
+ | `error-contract.ts` / `ts-scan.ts` / `fix-scan.ts` / `fix-imports.ts` / `fix-command.ts` / `fix-path.ts` | the `errors` step: every `fix:` names a runnable command, call or existing file |
51
+ | `workspace-checks.ts` / `workspace-graph.ts` / `tsconfig-references.ts` | `package-shape`, `filesize` |
52
+ | `app-permissions.ts` / `permission-grants.ts` | `policy`: every permission granted or required is declared, and every one required is granted by some role (`X_PERMISSION_UNGRANTED`) |
53
+ | `job-registration.ts` | `manifest`: no job or task under a positional `anonymous-*` name (`X_JOB_UNREGISTERED`) |
54
+ | `async-pages.ts` / `live-routes.ts` / `budgets.ts` | `budgets` riders: an async `Page` with no `load`, a live read no island imports, bytes per route |
55
+ | `app-agents-md.ts` / `app-env.ts` / `app-openapi.ts` / `app-manifest.ts` | `manifest` and `contract-diff` |
56
+ | `schema-drift.ts` / `drift.ts` / `db-destructive.ts` / `db-ungeneratable.ts` | `drift`: snapshot vs declarations, the source hash (core's `canonicalJson`), the header markers |
57
+ | `i18n-registration.ts` / `i18n-audit.ts` | `i18n` |
58
+ | `error-unthrown.ts` | host check: a registered code nothing throws must say so |
59
+
60
+ ### Generators (`x g`, `x new`)
780
61
 
781
62
  | File | Job |
782
63
  |---|---|
783
- | `migrations.ts` | the app's `packages/db/migrations` read into `@ultimat3/db`'s `Migration` shape — the **one** reader |
784
- | `db-generate.ts` | `x db gen`: entities diffed against what the migrations declare, written as `.sql` + `.snapshot.json` + `.hash` |
785
- | `cmd-db.ts` | the subcommands, and nothing else — `gen` calls `db-generate.ts`, `migrate`/`reset` call `serve.ts`'s `runMigrations` |
786
- | `db-branch.ts` | what a branch IS: the closed verb set, the name it takes on disk and in `pg_database`, and list/create/drop per mode |
787
- | `cmd-db-branch.ts` | `x db branch`'s wiring alone — which verb, which refusal, and the one connection an external clone runs on |
788
- | `db-finding.ts` | one thrown value → one `Finding`, shared by `cmd-db.ts` and `cmd-db-branch.ts` |
789
- | `db-accept-created.ts` | `acceptCreatedTables`: the post-migrate report minus the tables the applied migrations' own SQL creates — the half `@ultimat3/db`'s `unexpectedTable` names |
790
- | `db-subscribes.ts` | `replicaIdentityTables`: the tables `x db gen` grants `REPLICA IDENTITY FULL`, read off each live query's declared `subscribes:` — and `X_QUERY_SUBSCRIBES_UNKNOWN` for a name no entity's table matches |
791
- | `drift.ts` | `checkSourceDrift`: the `.hash` sidecar the `drift` step compares, no database needed |
792
- | `schema-diff.ts` | what two GENERATED snapshots disagree about, as data — the pure half |
793
- | `schema-drift.ts` | `checkMigrationDrift`: entity declarations against the newest `.snapshot.json`, and the composition the `drift` step and `x doctor` both read |
794
- | `db-destructive.ts` | `checkDestructiveMigrations`: the same step's second half — every committed `up` that drops, truncates or retypes must carry `-- destructive: true` |
795
- | `db-ungeneratable.ts` | `checkUngeneratableMigrations`: the same step's fourth rail — every committed `up` holding SQL `x db gen` could not have written must say how many, as `-- ungeneratable: <n>` |
796
- | `db-backfill.ts` | `x db backfill --list`: the flag parsing, the ledger read and the table |
797
- | `db-seed.ts` | `x db seed`, everything except the argv: `SEED_GLOBS` (where a seed is declared), `discoverSeeds`, `parseSeedTierFlag`, `selectSeeds` (which seeds this invocation runs, and its two refusals — `X_DECLARATION_UNKNOWN` and `X_SEED_ENVIRONMENT`), `runSeeds` (one transaction **per seed**, never one around the run) and the two renderers. The `db-backfill.ts` split repeated: a driver plus plain strings in, plain rows out, so every rule is testable with no `ParsedArgs` and no boot. Which tiers an environment takes is `@ultimat3/entity`'s `seedTiersFor` — two copies of "may this seed run" would be two answers |
798
-
799
- `jobs-driver.ts` is the ONE place a CLI command gets hold of the app's queue — `withJobDriver`,
800
- which `x jobs` and `x db backfill` both call. It reuses an ambient `jobDriver()` when a process
801
- already installed one (inside `x dev` or `x mcp serve`, booting a second queue talks to the wrong
802
- database) and otherwise boots `startQueue` and releases it in a `finally`, or a CLI that exits
803
- holding the PGlite lock breaks the next command run against this app. A second copy of that boot
804
- would be two answers to "which queue is this command talking to".
805
-
806
- **`x db branch` takes a VERB, and a branch name can never be one.** `ls`, `create <name>`,
807
- `drop <name>` — a closed set, declared once in `BRANCH_SUBCOMMANDS` and read three ways: the
808
- command validates against it, `dbCommand.spec.subcommandPositionals` declares it so the `errors`
809
- step can resolve a citation against it, and the refusal for an unknown word lists it. The bare-name
810
- form it replaces is why: the argument *was* the name, so `x db branch ls --json` — the `fix:` on
811
- the planned `x branch`, on `X_DB_BRANCH_FAILED`, and (as `create`/`drop`) on `@ultimat3/db`'s own
812
- `X_BRANCH_EXISTS` and `X_SQL_UNSAFE` — cloned a database called `ls` and returned no listing. All
813
- four passed every check the repo had, because `fix-command.ts` resolved two words and the third was
814
- the one that decided what ran.
815
-
816
- **`drop` has no confirmation flag, and that is the design.** It may only drop what `ls` shows: an
817
- external branch is a database carrying the marker comment `createBranch` writes **and** this
818
- database's own `<source>_branch_` prefix, an embedded one is a `pgdata-<name>` directory, so the
819
- shared database this session is connected to is in neither set.
820
- The typo is impossible rather than the keystroke tedious — and `@ultimat3/db` already ships
821
- `x db branch drop <name>` as `X_BRANCH_EXISTS`'s `fix:` with no flag on it, so a flag here would
822
- break a shipped instruction.
823
-
824
- **The prefix half is not decoration, and it is no longer the only source guard.** The marker records
825
- the base `As of 2026-08-19` — `ultimate:branch:<base>:<iso>`, read back as `BranchInfo.base` — so
826
- `reapBranches` can skip another app's clones on its own. The prefix guard still stands and is what
827
- `ls`/`drop` read, because an **older** marker records no base at all: it is skipped by the reaper
828
- rather than dropped, which leaves `drop` needing an answer that does not depend on a field half the
829
- branches lack. One Postgres server hosting two Ultimate apps answers `listBranches()` with both
830
- apps' clones, and `branchNameOf` reduced `postly_branch_feat` and `analytics_branch_feat` to the
831
- same branch name — so `x db branch drop feat`, run against `postly`, was authorised by
832
- `analytics`'s row and then issued `drop database if exists "postly_branch_feat"` against a database
833
- carrying no marker at all: a `DROP DATABASE` the guard had never approved, and nothing recoverable
834
- about it. `branchNameIn(source, database)` is the source-scoped inverse of `branchDatabaseName` and
835
- the one `ls` and `drop` both read; `branchNameOf` survives for `mcp-db-target.ts` alone, which has
836
- a URL and no connection to ask `current_database()` with.
837
-
838
- **The membership check lives inside `dropExternalBranch`, not in the wiring above it.** One
839
- connection, one listing, one statement before the `DROP` — a listing taken by the caller and acted
840
- on afterwards is two connections and a window wide enough to hold a whole `create`. It is still not
841
- atomic and cannot be: `DROP DATABASE` runs in no transaction, so no single statement both verifies
842
- the marker and deletes. Closing the last gap means a lock around both halves inside
843
- `@ultimat3/db`'s `dropBranch` — which a `psql` at the next terminal would not hold either.
844
-
845
- **`ls` is the reason `create` no longer shells out to `psql`.** `listBranches()` finds branches by
846
- `createBranch`'s marker comment; the `psql` path wrote the `CREATE DATABASE` and no comment, so
847
- every branch the CLI made was invisible to the only lister the framework has. External branching
848
- now runs through `@ultimat3/db` on one `role: 'migrate'` client — `max: 1`, no statement timeout,
849
- both load-bearing: `CREATE DATABASE … TEMPLATE` is refused while any *other* session holds the
850
- template, and cloning a real database outlives a `web` profile's 10s.
851
-
852
- **`DatabaseTarget.production` is a fact this package supplies, and it was the literal `false`.**
853
- `mcp-db-target.ts` is the only place one is ever built, so `assertBranchDatabase`'s first refusal —
854
- "production is never migratable from MCP at all" — could not run for any database the CLI produced;
855
- a production database was refused only incidentally, because its name lacked `_branch_`, and one
856
- named `shop_branch_hotfix` read as a branch and was migratable. It is now core's one key, read the
857
- way `x doctor` reads it. An **unreadable** `ULTIMATE_ENV` counts as production: `tryResolveEnvironment`
858
- answers `undefined` for exactly one input — a value that is not an environment — and a guard that
859
- read a typo as "not production" would be defeated by the misconfiguration it exists to survive.
860
- `staging` stays false; `branch: null` is already what refuses it, and widening the flag would make
861
- the refusal say something untrue.
862
-
863
- `x db backfill` has four shapes and a **dry run is the default**: `--list` reports the ledger,
864
- `--pending` reports declared-minus-completed and exits non-zero when there is drift, `<name>` plans
865
- one sweep, and `--all` plans every pending one. `--write` is never implied — the inspection forms
866
- and the acting form are the same command, and the flag is the only thing that separates them.
867
- `--all --write` isolates per name and continues past a failure, exiting non-zero naming each, so one
868
- wedged cleanup cannot block every later one forever.
869
-
870
- Until 1.2.0 a bare `x db backfill <name>` threw `X_NOT_IMPLEMENTED`, and the ledger was the only
871
- half that existed: `x_backfills` recorded what had run, and **nothing recorded what was pending**, so
872
- a scaffolded backfill could be merged and deployed and silently never run. `--pending` is the alarm
873
- that closes it; `registeredBackfills()` is what makes a declaration visible before its first pass.
64
+ | `cmd-generate.ts` / `generate-files.ts` / `generate-kinds.ts` / `generate-write.ts` | argv → pure file list → writes (conflict-checked, `merge: 'json'` / `'if-absent'`) |
65
+ | `generate-feature.ts` | a `--feature` naming no slice is `X_FEATURE_UNKNOWN`; nothing invents an entity |
66
+ | `generate-grants.ts` | a written `policy.ts` grants `:read` to `member`, `:write` to `admin` in `apps/web/shared/roles.ts` |
67
+ | `api-registration.ts` | a written job or task is listed in `apps/web/api/index.ts` |
68
+ | `app-artifacts.ts` | `x.manifest.json` + `openapi.json`, written together by `x g` and `x manifest` |
69
+ | `templates/` | every emitted file; `scaffold-*.ts` for `x new`, one file per generator otherwise |
874
70
 
875
- `x db migrate` and `ROLE=migrate` are the same function call. That is the whole design: until
876
- 1.2.0 the CLI shelled out to `bunx drizzle-kit` — a second engine, a second journal, declared in no
877
- `package.json` and fetched unpinned at run time — while the release phase used the framework's
878
- ledger, so "what has been applied" had two answers that only agreed by luck. `cmd-db.test.ts`
879
- holds the line from both ends: no shipped source spawns a second migrator, and this file still
880
- imports `runMigrations` from `./serve`.
881
-
882
- **The post-condition is one check too, and it is the database one.** `runMigrations` runs
883
- `@ultimat3/db`'s `checkDrift()` inside the queue's lifetime — the connection it opened for the
884
- migrator is the only one there is — and returns the report on `MigratedApp.drift`, so a developer
885
- and a release phase verify the same thing. `x db migrate` renders it through `driftFindings` and
886
- exits non-zero; `runRole` throws the first difference for `ROLE=migrate`, so the release phase
887
- exits non-zero too. Both entrypoints call the same `runMigrations` and both fail — the difference
888
- is only the channel each has. `ROLE=migrate` logged and exited 0 until it did not: a release phase
889
- whose only signal is the exit code reported success over a schema nobody can reconstruct, which is
890
- the failure the post-migrate check exists to catch.
891
-
892
- **`x db gen` emits `REPLICA IDENTITY FULL`, and the set is DECLARED rather than derived**,
893
- `As of 2026-08-26` (#357). `@ultimat3/realtime` refuses a live subscription to a table without it —
894
- logical replication carries no old row on an UPDATE, so no patch can be computed — and for two
895
- years nothing in the framework emitted one. It could not be derived, and that is the load-bearing
896
- fact: the relation name lives inside the query's `sql:` callback, which no generator can invoke
897
- without valid input (`describeSql` says so itself — "`null` when no sample input was supplied").
898
- So a live query DECLARES it (`subscribes:`, `@ultimat3/query`), the declaration is machine-checked
899
- against the resolved `shape.entity` on the first subscribe (`X_QUERY_SUBSCRIBES_DRIFT`), and
900
- `db-subscribes.ts` reads it off `describeQueries()` — the same source `frameworkSources` copies onto
901
- `QueryFact.subscribes`, one hop earlier, because building the manifest here would re-load the app
902
- and demand a `package.json` that `x db gen` has never needed.
903
-
904
- **The third `subscribes:` refusal is this package's, because no other tier can ask it.**
905
- `@ultimat3/db` keeps only the declared names an entity's table matches and DROPS the rest — it has
906
- no way to tell a typo from a table another migration owns — and `@ultimat3/query` holds no table
907
- catalog at all. So `subscribes: ['posts', 'user']` granted the identity to `posts`, dropped `user`
908
- in silence, and read as granted. `X_QUERY_SUBSCRIBES_UNKNOWN` refuses it BEFORE anything is
909
- written, naming the query and offering the tables the app does declare. It is checked after
910
- `loadApp`'s findings, never before: a module that would not import leaves the registry short, and
911
- every name whose entity lives in it would then look like a typo.
912
-
913
- **And it accepts a table the migrations it just applied demonstrably created**, `As of 2026-08-26`
914
- (issue #345). A snapshot records only what ENTITIES declare, so a table created by a HAND-WRITTEN
915
- migration reached no sidecar and was `unexpected-table` on every deploy forever — with a `fix:`
916
- that generated an empty migration, because `x db gen` diffs the entity registry against the newest
917
- snapshot and the table is on neither side. `@ultimat3/db` fixed the wording; `acceptCreatedTables`
918
- (`db-accept-created.ts`) is the half that file's `unexpectedTable` names, and it is composed around
919
- `checkDrift` inside `runMigrations`, so `x db migrate` and `ROLE=migrate` accept the same set.
920
- **Only `unexpected-table`, and only for a name a migration's SQL creates** — which is what keeps it
921
- an acceptance rather than the check switched off: a table absent from the snapshot produces exactly
922
- one difference (`diffSchema` reports it and never compares its columns), and a table nobody
923
- declared and no migration created is still reported, cause and `fix:` intact. The evidence is the
924
- applied list itself: `migrate()` runs first, so every file on disk has been applied by the time the
925
- question is asked. The verb phrase is read ANCHORED off the raw statement, which is the whole
926
- protection — a `create table` can only be at position 0 by being one, so `values ('create table
927
- ghost')` opens with `insert` and a comment-only chunk is not a statement at all. A `stripSqlNoise`
928
- pass was written first and deleted: it could not change one answer, and a defence that cannot fail
929
- is one nobody can test. Everything the anchor admits and the name grammar does not — a comment
930
- between the keywords, a `temp` table, a qualifier naming a schema `checkDrift` never introspected —
931
- contributes nothing, which reports drift that could have been accepted and never the reverse.
932
-
933
- **The `drift` step asks a third thing, off the same directory and with no database either: is every
934
- destructive statement declared?** `db-destructive.ts` reads each committed migration through
935
- `migrations.ts` — the reader `x db migrate` applies from, because a rail checking a list the
936
- migrator does not run enforces nothing — and refuses an `up` that drops a table, drops a column,
937
- truncates or retypes without a `-- destructive: true` line, as `X_MIGRATION_DESTRUCTIVE`. It decides
938
- none of that itself: `@ultimat3/db`'s `destructive.ts` owns the classifier `db-generate.ts` already
939
- wrote the marker from, so the generator and the gate cannot disagree about one file. One finding per
940
- file, never one per statement — the marker declares the whole migration. It rides on `drift` rather
941
- than becoming an eighteenth step because it is this step's own question over this step's own files;
942
- a new step is for a genuinely new question.
943
-
944
- **The `drift` step reads the SNAPSHOT, not only the hash, `As of 2026-08-25`.** `checkSourceDrift`
945
- compares a schema-source hash to a `.hash` sidecar and never reads what the migration RECORDED, so
946
- `dummy/social-media-clone` sat green while **nine declared CHECK constraints had never reached any
947
- database** — a comment body could be whitespace, a like count could go negative, an email needed no
948
- `@` — and a squash that dropped ten invariants and nine defaults would have been green too. The
949
- source had not moved, so nothing that hashes source could see it. `checkSnapshotDrift`
950
- (`schema-drift.ts`) diffs `snapshotOf(describeEntities())` against `declaredSchema(readMigrations())`
951
- — both sides are `snapshotOf`'s own spelling, which is what makes a check, a default and a column
952
- type comparable at all. Measured against that app rolled back to the state its gate was green in:
953
- **20 findings**, 9 checks and 11 defaults.
954
-
955
- **Two directions, two codes, because they are two repairs.** `X_DB_SCHEMA_UNMIGRATED` is a
956
- declaration the migrations do not carry — the database will never get it. `X_DB_SCHEMA_UNDECLARED`
957
- is a migration carrying what nothing declares any more, whose fix names BOTH branches, because the
958
- declaration may have been lost rather than removed and `x db gen` would emit the DROP. One "drift"
959
- verdict over both teaches a reader neither.
960
-
961
- **Absent is not empty, and reading it as "recorded none" would fail every existing app on its first
962
- run.** `TableDescription.checks` is absent — never `[]` — on a table declaring none, exactly like
963
- `IndexDescription.using` and `ColumnDescription.generated`. Both sides normalise to empty
964
- (`schema-diff.ts`), `using` reads through `indexMethodOf` and `order` through `?? 'asc'`, or an app
965
- whose sidecar predates any of the three reports a difference on every index it has.
966
-
967
- **The hash half stays, and runs second.** It catches what the snapshot comparison cannot see at all
968
- — a seed, a helper or a TS-only invariant moving under `packages/db/src` with no statement behind
969
- it, which is what `reconcileSchemaHash` exists to re-record. It is SUPPRESSED when the snapshot half
970
- found something: both then answer one condition and only one of them is an instruction, since
971
- `schema hashes to 3f2a, newest migration recorded 91bc` names no constraint, no column and no table.
972
-
973
- **An app whose modules will not import is not judged here.** `appEntities` answers `undefined`
974
- rather than a short registry, which would read as "every table was dropped" and hand out a DROP per
975
- table for one file's syntax error — the same stance `generateAppMigration` takes with its `blocked`
976
- outcome. The cost is that a schema check can be silently skipped under an already-red gate; the
977
- alternative is a false red whose fix destroys data.
978
-
979
- **The FOURTH thing the `drift` step asks is what neither hash nor snapshot can see: SQL no
980
- declaration carries at all.** `ALTER TABLE posts REPLICA IDENTITY FULL;` sits in
981
- `examples/dummy/packages/db/migrations/0001_init.sql`, no generator emits it, no snapshot records
982
- it, and a squash drops it in silence — and no declaration-based check can ever see it, because a
983
- regenerated sidecar equals the declaration by construction. `db-ungeneratable.ts` reports it,
984
- `As of 2026-08-25`. It classifies nothing itself: `@ultimat3/db`'s `ungeneratableStatements`
985
- matches each statement's leading verb phrase against `GENERATABLE_FORMS` — everything
986
- `generateMigration` emits, held honest in both directions by that package's own test — because
987
- every SQL classifier in the tree is db's (`sql-scan.ts`, `statement-split.ts`, `sql-noise.ts`,
988
- `destructive.ts`) and a second one here is the reimplementation this file's own rule forbids.
989
- Measured: **7 statements in `examples/dummy`** (five `CREATE TYPE … AS ENUM`, two
990
- `REPLICA IDENTITY FULL`), **0 in all four `dummy/social-media-clone` migrations** and 0 in
991
- `examples/dummy`'s own generated `0002_money_scale.sql` — real generator output reports nothing,
992
- which is the half a rail like this lives or dies on.
993
-
994
- **The declaration is a header line, and it carries a COUNT: `-- ungeneratable: 7`.** Not
995
- `@ultimat3/db`'s code but this package's (`X_MIGRATION_UNGENERATABLE`), because the only remedy
996
- available for every statement it reports is a line in the migration file, and where an app keeps
997
- its migrations is this package's fact — db classifies and deliberately declares no code. Three
998
- decisions behind that shape:
999
-
1000
- | Decision | Why |
1001
- |---|---|
1002
- | in the migration file, not a pin table here | the gate runs in every generated app, and a table in `packages/cli/src` can hold no row for an app it has never seen. `-- destructive: true` in the same directory, read by the same reader, is the precedent |
1003
- | a count, not a boolean | the first hand-written statement would otherwise buy the file an unlimited allowance, and statements a reader never sees again are this rail's whole subject. A ratchet in the `README_FENCE_BACKLOG` sense: `found > declared` reports, a declared count that is too high is a pin nobody lowered |
1004
- | the **header** — before the first statement | `hasDestructiveMarker` had to become a lexical scan because a regex over the raw file matched its marker inside a block comment and inside a dollar-quoted body, and `noiseAt` is db's and unexported. A run anchored at index 0 needs no scanner to be exact: before the first statement there is no string, no quoted identifier and no dollar body for a marker to hide in |
1005
-
1006
- **The `fix:` names the marker first and `x db gen` second, and that order is the point.**
1007
- Regenerating is exactly what *discards* these statements, so the command every other db code
1008
- answers with is the one this one must not lead with — `X_MIGRATION_UNGENERATABLE`'s `CLI_FIXES` row
1009
- is `x verify --only drift`, and the re-declare branch (an enum is a text column plus a check
1010
- invariant) rides behind an em-dash because it is available for some of the statements and not all.
1011
- **`REPLICA IDENTITY FULL` was the statement with no second branch, and stopped being one on
1012
- 2026-08-26** (#357): a live query declares the relations it is patched from (`subscribes:`),
1013
- `db-subscribes.ts` reads them off the same registry the manifest is projected from, `x db gen`
1014
- emits the ALTER and `@ultimat3/db` records it on the snapshot so it is emitted once. The re-declare
1015
- branch covers it now: declare `subscribes:` and regenerate. A statement already committed is a
1016
- different question and still counts — `GENERATABLE_FORMS` (`@ultimat3/db`) matches a leading verb
1017
- phrase and does not carry this one, measured at 7 found / 7 declared on `examples/dummy`'s
1018
- `0001_init.sql`, `As of 2026-08-26` — so the marker branch remains the only remedy for SQL on disk.
1019
-
1020
- **`x db gen` reports what it could not write, and exits 0.** `GeneratedMigration.unrendered` reached
1021
- the committed `.sql` as a `-- UNRENDERED` comment and nothing else read it; `db-generate.ts` now
1022
- carries it on every branch (a REQUIRED field, so forgetting to project it is a type error) and
1023
- `cmd-db.ts` prints the count plus each entry's own remedy and carries the list under
1024
- `data.unrendered`. Not a non-zero exit: `x db gen` is the `fix:` on `X_DB_DRIFT` and four other
1025
- shipped errors, and a fix that always exits 1 is not an instruction — the `x i18n add fr` failure,
1026
- repeated. The red belongs at the gate, and the `drift` step reads the SAME list to decide that
1027
- `x db gen` is not the fix it should be handing out.
1028
-
1029
- The *source* half is a different question with a different answer: `checkSourceDrift` hashes the
1030
- entity source against what `x db gen` recorded, answers the same before and after a migration, and
1031
- opens nothing — which is what lets the gate run it in a CI with no database. It stays on `x verify`
1032
- and `x doctor` and is deliberately **not** repeated on `x db migrate`; two reporters of one
1033
- condition is the duplication this package's own rule forbids. Both were called `checkDrift` until
1034
- 1.2.0, and the one that was wired everywhere was the one that cannot see a column added by hand.
1035
-
1036
- Generation opens no database. It diffs `describeEntities()` against `declaredSchema(readMigrations(root))`
1037
- — the snapshot the newest migration wrote down — so `x db gen` answers the same in CI, on a laptop
1038
- with nothing running, and against a database three migrations behind. An app whose modules will not
1039
- load generates **nothing**: a short registry is indistinguishable from deleted entities, and the
1040
- diff would be a DROP nobody asked for.
1041
-
1042
- **An empty diff re-records the `.hash` sidecar, and that is what makes `X_DB_DRIFT` followable.**
1043
- The hash `checkSourceDrift` compares covers every non-test file under `packages/db/src` — a seed, a
1044
- helper, a decorator — not only the ones that imply DDL, and narrowing that glob would trade a loud
1045
- error for a silent gap in the one check that catches "entities changed and no migration was
1046
- generated". So detection stays broad and the REMEDY carries the weight: `x db gen "describe the
1047
- change"` — the exact `fix:` the error hands out — records the current hash against the newest
1048
- migration when the diff is empty, instead of writing nothing and leaving the gate red forever with
1049
- hand-editing a generated file as the only way out. `GeneratedFiles.outcome` is the four things a run
1050
- can be — `generated`, `hash-recorded`, `unchanged`, `blocked` — and `runGen` projects it onto
1051
- `--json` on **every** branch: reporting `hash-recorded` as `generated` would name a migration nobody
1052
- can apply, and reporting it as `unchanged` would hide a file this command wrote. That second one
1053
- shipped: the no-migration branch hardcoded `data: { migration: null, files: [] }`, so the run that
1054
- wrote the sidecar reported writing nothing to the machine reading the output.
1055
-
1056
- Nothing is masked, and the branch proves it rather than promising it: `loadApp` reported no findings
1057
- (the registry is whole, never short), `declaredSchema` returned a real snapshot (`X_MIGRATION_SNAPSHOT_MISSING`
1058
- otherwise), and the emptiness is `generateMigration`'s own verdict — the same call the written path
1059
- takes. A migration with no migration id to record against writes nothing, which is the
1060
- `x new --no-example` case: an entity against zero migrations is `create table` for all of it and
1061
- never an empty diff. `reconcileSchemaHash` also declines to write when an OLDER migration already
1062
- recorded the hash, because `checkSourceDrift` already answers clean there and restamping the newest
1063
- sidecar would claim it produced a schema it did not — one predicate, `isRecorded`, read by both.
1064
-
1065
- One migration is one file, split by a lone `-- down` line. `<id>.down.sql` is a pre-1.2.0
1066
- hand-written layout and `readMigrations` skips it — read as a migration it sorts next to its own
1067
- `up` and drops every table the pair exists to reverse.
1068
-
1069
- ## `x dev` boots the app; it does not simulate one
71
+ ### Boot (`x dev`, the container)
1070
72
 
1071
73
  | File | Job |
1072
74
  |---|---|
1073
- | `api-routes.ts` | the app's API over HTTP: every registered action AND every registered query |
1074
- | `dev-services.ts` | resolve which service each binding points at — embedded or external |
1075
- | `dev-queue.ts` | the db + queue pair alone, and the one place that takes every ambient accessor back |
1076
- | `dev-runtime.ts` | start the rest on top of it and install the remaining accessors (storage, mail, transport) |
1077
- | `dev-cache.ts` | which cache tiers this process reads through, and the cross-instance invalidation hop |
1078
- | `dev-purge.ts` | the hourly retention sweep: which framework tables this boot owns, the `purge()` job over them and the `task` that fires it |
1079
- | `dev-notify-retention.ts` | `notify.inboxReadRetentionMs` / `inboxUnreadRetentionMs` off the app's own `app.config.ts` — the sibling of `loadSignInPath` and `loadCacheTiers`, because `startServices` holds no `AppConfig` |
1080
- | `dev-sync.ts` | the `sync` role: its live-query registry, who is dialling it, and the socket it owns |
75
+ | `serve.ts` / `serve-boot.ts` / `serve-entry.ts` / `serve-env.ts` | the production entry: roles from `ROLE`, bindings from env, `ROLE=migrate` = `x db migrate` |
76
+ | `cmd-dev.ts` / `dev-route-table.ts` | `x dev`: every role in one process, `/_x`, the watcher |
77
+ | `runtime-bindings.ts` | which service each binding points at (embedded or external); events follow `realtime.transport` / `urlEnv` |
78
+ | `runtime-queue.ts` / `runtime-services.ts` | the db + queue pair; everything else and every ambient accessor |
79
+ | `runtime-jobs.ts` / `runtime-realtime.ts` / `runtime-notify-retention.ts` / `runtime-cache.ts` / `runtime-purge.ts` / `runtime-replica.ts` | `app.config.ts` sections the boot obeys, and what each wires |
80
+ | `role-start.ts` / `role-start-types.ts` / `role-realtime.ts` | `--role` selection and start/stop for `web`, `sync`, `worker`, `scheduler`; `realtime.enabled: false` drops `sync` and `replicator` |
81
+ | `role-sync.ts` / `role-replicator.ts` / `runtime-live-feed.ts` | the sync node, the change feed |
82
+ | `runtime-render.ts` / `runtime-assets.ts` / `runtime-storage.ts` / `runtime-hooks.ts` / `api-routes.ts` | the HTTP surface: pages, `/icons` + `/media`, `/_storage`, authz, the app's API |
1081
83
  | `runtime-overrides.ts` | the one field a host hands the framework a driver through |
1082
- | `sync-authenticator.ts` | the app's HTTP authenticator, seen as the sync node's |
1083
- | `otlp-export.ts` | the exporters `OTEL_EXPORTER_OTLP_ENDPOINT` switches on, and their drain hooks |
1084
- | `dev-render.ts` | one HTTP route per registered `route`, through render's own mode function |
1085
- | `style-csp.ts` | the `style-src` sha256 of every inline `<style>` the web role **still** serves — the app's own surface CSS is a file under `/styles/` (`style-bundle.ts`) admitted by `'self'`, so a production boot extends the directive with nothing |
1086
- | `script-csp.ts` | the `script-src` sha256 of every inline `<script>` it serves — the hydration runtime, from `@ultimat3/render`'s own `HYDRATE_RUNTIME_BODIES` |
1087
- | `dev-assets.ts` | the image pipeline's only HTTP surface: `/icons/*` and `/media/*` |
1088
- | `favicon.ts` | `/favicon.ico`: the app's own file, and the bytes the framework answers with when there is none |
1089
- | `dev-hooks.ts` | the pipeline's `authorize` seam, decided from the app's own `Policy` objects |
1090
- | `dev-replica.ts` | which boot gets a standby, and the one middleware frame that opens the read scope |
1091
- | `dev-replicator.ts` | the `replicator` role: the feed selected, locked and pumped — and `replicatedRelations()`, the entity TABLES it filters on |
1092
- | `dev-roles.ts` | `--role` selection plus start/stop for `web`, `sync`, `worker`, `scheduler` |
1093
- | `dev-dashboard.ts` | the `DevSources` hooks only this process can answer, and the two CLI panels |
1094
- | `dev-traces.ts` | core's spans → the `/_x` timeline's request traces |
1095
- | `dev-n-plus-one.ts` | statement shapes counted per request, and the ones that repeat past the threshold |
1096
- | `statement-loop.ts` | one verdict → the finding, the panel fact, the overlay notice and the log line |
1097
- | `dev-policy.ts` | which actors to ask about, and which capability each policy gates |
1098
- | `cmd-dev.ts` | boot order, mounting `/_x`, installing the span exporter, the file watcher |
1099
- | `dev-watch.ts` | which paths under the app root `x dev` may not watch: the app's own `.gitignore` (`gitignore.ts`) plus a floor of five directory names an ignore file cannot be relied on to carry — `.git`, `.x`, `node_modules`, `.personal`, `.claude` — every one of them dotted or an install, matched as a path SEGMENT at any depth |
1100
- | `dev-watch-tree.ts` | the watch SET: one `watch(dir, { recursive: false })` per ADMITTED directory, a new directory picked up on `rename`, a removed one giving its descriptor back, and the 30ms trailing debounce |
1101
- | `dev-reload.ts` | one rebuild at a time — a tick arriving mid-build coalesces into ONE trailing rebuild, for the newest file |
1102
- | `gitignore.ts` | what git ignores, as data: parse, the ancestor chain up to the repository, and last-match-wins |
1103
- | `path-segments.ts` | `hasPathSegment` — a directory of that exact name, never a substring of one |
1104
- | `style-bundle.ts` / `style-routes.ts` | a surface's CSS as one content-hashed file under `/styles/`, served `immutable` — `island-bundle.ts` / `island-routes.ts`' shape one asset over. It was an inline `<style>` until 2026-09-06: 156,738 bytes, identical on every page, inside a `private, no-store` document. The URL is the hash alone, no surface in the name: a surface is not a property of the bytes, and an app whose only CSS is its global layer would otherwise write three identical files into its static export and three entries into a precache manifest that has a budget |
1105
- | `mcp-host.ts` | the `DevCapabilities` half of `@ultimat3/mcp`'s `DevHost` — db, tests, logs, verify |
1106
- | `mcp-db-target.ts` | which database the host is pointed at: whether it is a branch, and whether it is production |
1107
- | `mcp-errors.ts` | `errors.explain`: one runnable command per code, typed over `CliErrorCode` |
1108
- | `error-catalog.ts` | imports every `@ultimat3/*` package so `x errors` answers for codes no command loads |
1109
- | `mcp-test-output.ts` | reading `bun test`'s own summary back into a `TestRun` |
1110
- | `cmd-mcp.ts` | `x mcp serve`: the two transports, and the local developer's caller |
1111
-
1112
- **The watch set is a REGISTRATION decision, not a filter, `As of 2026-09-06`.** `watch(root, {
1113
- recursive: true })` takes one inotify descriptor per directory in the tree — measured on `x dev`
1114
- against `examples/dummy`: **110 descriptors, 39 of them (35%) under `.x/` and `node_modules/`**, and
1115
- on a monorepo root **1901, of which 1490 (78%) were `.git/` and `node_modules/`**, where one
1116
- `git status` delivered 5 `.git/index` events into the JS callback. `isIgnoredPath` answered
1117
- correctly every time and answered too late: the kernel queue entry, the callback and the slot out of
1118
- `max_user_watches` (8192 on many distributions) were already spent. Bun 1.4.0's `fs.watch` has no
1119
- ignore option, so `dev-watch-tree.ts` walks the root itself and registers one non-recursive watcher
1120
- per admitted directory, pruning at descent.
1121
-
1122
- **And the ignore set is the app's own `.gitignore`.** Seven hand-listed directory names were wrong
1123
- in both directions. Nothing read the ignore file, so `touch tsconfig.tsbuildinfo` — the file every
1124
- `bun run typecheck` rewrites, named by `x new`'s own scaffolded `.gitignore` — logged
1125
- `reloaded tsconfig.tsbuildinfo in 113ms`, a full `appManifest()` plus `buildIslands()`; on the
1126
- framework root 54 git-ignored directories were unfiltered. And `dist` and `coverage` were matched at
1127
- ANY depth, so an app's own `/dist` or `/coverage` ROUTE never reloaded — silently, which the file's
1128
- own header calls worse than a spurious rebuild. `gitignore.ts` reads it with git's own anchoring
1129
- (unanchored at any depth, a leading `/` or an inner slash where it is written, `!` re-including,
1130
- a trailing `/` directory-only) and walks ANCESTOR ignore files up to the directory holding `.git` —
1131
- `examples/dummy` carries none of its own and every rule about it lives in the repository root's. The
1132
- file is re-read, and the whole watch set re-walked, on any write naming `.gitignore`; nothing ever
1133
- spawns `git check-ignore`. `x new`'s scaffold writes `/dist/` and `/coverage/` **root-anchored** for
1134
- this reason, plus `packages/*/dist/`. It is `fix-path.ts`'s parser, lifted: two readers of one file
1135
- are two answers to what an app committed.
1136
-
1137
- **A watcher event carries no filename when the WATCHED directory itself moves.** Bun's recursive
1138
- watcher delivers `undefined` — not `null` — on `mv myapp myapp2`, a re-clone or a volume remount,
1139
- and `isIgnoredPath(undefined)` threw a `TypeError` inside an fs callback, outside any `try`, with no
1140
- `uncaughtException` handler: `x dev` died with a stack trace. The listener is total over
1141
- `string | Buffer | null | undefined` and logs `dev.watch.unnamed_event` once.
1142
-
1143
- **The reload has an in-flight guard, and it is the state that needed it.** A 45ms drip — a slow
1144
- `git checkout`, a formatter walking files, `x db gen` — measured **40 reloads for 40 files**, each
1145
- launching `Promise.all([appManifest, buildIslands])` while the previous still ran and assigning
1146
- `state.manifest` / `state.islands` in COMPLETION order, so a slower earlier tick could land on top
1147
- of a newer one. `coalesceReloads` (`dev-reload.ts`) keeps the LAST tick that arrived during a
1148
- rebuild and starts exactly one more.
1149
-
1150
- `api-routes.ts` is the app's own API surface, composed **once** and mounted by both `cmd-dev.ts`
1151
- and `serve.ts`: `listActions().map(toRoute)` from `@ultimat3/action` plus
1152
- `listQueries().map(toQueryRoute)` from `@ultimat3/query`. Two lists is how `query.client()`
1153
- shipped deriving `/_x/query/<kebab>` against a route neither file mounted — a typed read that
1154
- compiled everywhere and 404'd everywhere — and a surface that answers in `x dev` and not in the
1155
- container is the same failure one release later. It reads the registries at call time, never at
1156
- import: importing the app IS the registration, and it happens after this module loads.
1157
-
1158
- `startWeb` warns when the route table declares `auth: 'required'` and the app configured no
1159
- authenticator: `hooks.authenticate` is the only place an actor can come from, so such a process
1160
- boots clean, reports healthy, and refuses every valid session. A warning and not a throw, because
1161
- `x new` scaffolds guarded routes before it scaffolds an authenticator.
1162
-
1163
- The roles live in `@ultimat3/core` (`ROLES`, `isRole`), never in a second list here. A dev-only
1164
- driver, a dev-only authorizer or a dev-only queue is the bug this design exists to prevent — the
1165
- only thing dev changes is which driver is behind an interface.
1166
-
1167
- ### `HOST` is the interface, read the way `PORT` is
1168
-
1169
- `serve.ts`'s `hostnameFromEnv` — `HOST`, trimmed, empty is `0.0.0.0` — and `ServeOptions.hostname`
1170
- overrides it as `port` overrides `PORT`; `containerBinding(env, hostname)` is the one `WebBinding`
1171
- `serveApp` hands `startRoles`, so `web`, `sync` and the metrics endpoint bind the same interface.
1172
- Before 2026-09-07 `CONTAINER_BINDING` was the only production binding and an app whose auth mode
1173
- admits one implicit actor without a login — which must refuse a public interface — could not run in
1174
- a container at all. A loopback bind in a container is unreachable through `-p`; the wiki row says
1175
- where it IS reachable. Not `HOSTNAME`: Docker sets that to the container id.
1176
-
1177
- ### `RuntimeOverrides` is the only way to hand the framework a driver
1178
-
1179
- `ServeOptions` was `{ root, env, role?, port?, metricsPort? }`, so the ONLY way an app could
1180
- install a driver was an ambient setter at module scope — and `loadApp` imports the app's modules
1181
- *after* `startServices` has captured its own. The slot moved and the capture did not: every
1182
- `handle.enqueue()` went to the app's queue while the worker claimed from Postgres, and `/_x` read
1183
- the ambient one, so the dashboard agreed with the enqueue side and disagreed with reality.
1184
-
1185
- Every field REPLACES the env-selected default rather than sitting beside it — `overrides?.x ?? <the
1186
- env switch>`, one expression, one answer (axiom 1). A field nothing consumes is not there: the
1187
- entity `Driver` in particular, because `@ultimat3/entity` exposes no installer for one
1188
- (`database(entities, { driver })` is the app's own call), and a slot the boot cannot honour is the
1189
- class of defect this seam exists to end.
1190
-
1191
- **The split is refused, not reconciled.** `assertOneJobDriver` runs first in `startRoles` and
1192
- throws `X_RUNTIME_DRIVER_SPLIT` when `jobDriver()` is not the object this process serves. Reading
1193
- through the accessor instead would make the split invisible rather than impossible — and the app
1194
- would still have installed a driver the boot never saw, with no outbox store bound to it and no
1195
- relay draining it.
1196
-
1197
- ### What the boot now calls that nothing called before
1198
-
1199
- | Mechanism | Where | Was |
1200
- |---|---|---|
1201
- | the transactional outbox | `dev-queue.ts` installs the store + facade, `worker` runs the relay | staged rows nothing published |
1202
- | the durable scheduler | `pgSchedulerState` + `createPgLeaseLeader` in `startRoles` | a watermark forgotten on restart, and every replica its own leader |
1203
- | the Postgres event bus | `dev-queue.ts` | `step.waitForEvent` forgot every correlation on restart |
1204
- | the shared idempotency store | `dev-queue.ts` | a retry on another replica charged the card twice |
1205
- | the shared auth limiter | `configureAuthLimiters` in `startServices` | account lockouts counted per POD, so N replicas granted `maxAttempts × N` guesses |
1206
- | the retention sweep | `dev-purge.ts`, declared in `startServices` | three `purgeExpired()` with no caller — every row `x_idempotency`, `x_rate_limit` and `x_auth_*` ever took was kept |
1207
- | the cache tiers | `dev-cache.ts` | only the CDN tier was registered; memo, LRU and Redis had zero callers |
1208
- | WebSocket authentication | `dev-sync.ts` | `actorId: null` on every socket — realtime was single-tenant by wiring |
1209
- | OTLP export | `otlp-export.ts` | the chart set the variable and no code read it |
1210
-
1211
- `createPgLeaseLeader`, never `createPgLeader`: the latter's `pg_try_advisory_lock` is
1212
- session-scoped and the grant dies when the connection returns to the pool, so every node reads
1213
- itself as leader and a rolling update double-fires every task.
84
+ | `script-csp.ts` / `style-csp.ts` / `style-bundle.ts` / `page-sync.ts` / `worker-bundle.ts` | CSP hashes, the CSS file, the page's sync target and worker |
85
+ | `island-bundle.ts` / `island-store.ts` / `island-realtime.ts` / `solid-loader.ts` | islands: one `Bun.build` each, source-addressed; `x build --target docker` writes a verified store the container loads |
86
+ | `dev-*.ts` | `x dev` only: dashboard sources, traces, the N+1 ledger, the watcher, the reload, the lock, the port |
1214
87
 
1215
- The relay runs on `worker` and only `worker` — the role that exists wherever jobs run at all.
1216
- Duplicating it is safe — the claim is a **lease** taken in the statement that locks the row
1217
- (`@ultimat3/jobs`' `outbox-pg.ts`, fenced on `claimed_by`), so two relays never hold one batch —
1218
- but pointless. The idempotency key is not the reason and never was: its conflict target is a
1219
- partial index over live states, so it collapses a repeat only while the first job is still live.
1220
-
1221
- `SQL_IDEMPOTENCY_TABLE` is applied beside `SQL_JOBS_TABLE`, and the store is installed by the boot
1222
- rather than by the app, even though `@ultimat3/action` documents
1223
- `postgresIdempotencyStore({ executor: Bun.sql })`: **`Bun.sql` has no `.query(text, values)`** — it
1224
- is a tagged template whose positional form is `unsafe` — so that line does not satisfy `PgExecutor`,
1225
- and a second executor would open a second pool against a URL this boot already resolved. The app
1226
- owes only the declaration, `configureIdempotency({ scope: 'shared' })`, which `x new` names in
1227
- `apps/web/server.ts`.
1228
-
1229
- The per-TENANT subscription cap is deliberately unset, and **both halves of it are**:
1230
- `assertCapacity` returns early unless `maxPerTenant` AND `tenantOf` are given, so passing one arms
1231
- nothing — and no default is defensible when one tenant is a person and the next is five thousand
1232
- seats. The per-socket 128 stands because a socket is one browser tab.
1233
-
1234
- **The change feed is filtered by TABLE, never by entity name**, `As of 2026-08-26`.
1235
- `replicatedRelations()` (`dev-replicator.ts`) is the one projection, and both of its readers are
1236
- catalog readers: `PgReplicationStream` keeps a change only when `#entities.has(relation.name)` and a
1237
- pgoutput Relation message names the table, while `warnPartialIdentity` matches the same list against
1238
- `pg_class.relname`. An entity NAME is the framework's own registry key — a cache tag, a policy and
1239
- `x entities describe` are all keyed by it — and `entity('user', { table: 'users' })` makes the two
1240
- different strings. It passed `.name`, so a renamed table matched on neither side: **every change
1241
- skipped** and a replica-identity warning that could never fire, with no error anywhere. Invisible to
1242
- every fixture in the tree, because `table` defaults to the name verbatim and all six entities in
1243
- `examples/dummy` have `name === table` — `dev-replicator.test.ts` uses `billingAccount` on
1244
- `billing_accounts` for exactly that reason, and proves the value through the real call chain:
1245
- `assertIdentifier` refuses `billingAccount` before any connection and accepts `billing_accounts`.
1246
-
1247
- `trustProxy` is read from `TRUSTED_PROXY_HOPS` in `startWeb`, the way `PORT` and `ROLE` are read: it
1248
- is a fact about the deployment, not an app config choice, and one image runs behind an ingress in
1249
- one cluster and behind nothing on a laptop. Without it `ctx.ip` is the ingress's socket address on
1250
- every request, so the limiter keys the whole fleet's anonymous traffic into one bucket.
1251
-
1252
- ### `island-bundle.ts` is the bundler half of `hydrate`
1253
-
1254
- `@ultimat3/render` shipped `island()`, the collector, `emitIslandAttributes`, `hydrateRuntime`,
1255
- `RouteEntry.islands` and `routeJsBytes` — and **nothing constructed or populated any of them**.
1256
- `hydrate` was a documented capability with no implementation, to the point that `render-static.ts`
1257
- told authors to "move the request-dependent part into an island", naming a mechanism the framework
1258
- could not express. This package is the half that can see a file on disk, so it is the half that was
1259
- missing.
88
+ ### Build and data
1260
89
 
1261
90
  | File | Job |
1262
91
  |---|---|
1263
- | `island-bundle.ts` | discover `*.island.tsx`, build each as its own entry point, hash it, resolve a page's specifier to its URL |
1264
- | `island-solid-dedupe.ts` | the plugin installed FIRST: every `solid-js` specifier in an island's graph resolves to the APP's copy. `Bun.build` resolves from a module's real path, so a package reached through a symlink (`file:` overrides, `bun link`) brought its own `solid-js` — the scaffold's theme toggle shipped two runtimes under CI's own links, 62,463 B against 50,042 B with one (issue #490), and two runtimes are two reactive graphs |
1265
- | `island-routes.ts` | serve those chunks, at `ISLAND_BASE_PATH`, immutable |
1266
- | `dev-render.ts` | one collector **per render**, and `hydrateRuntime` after the body |
1267
- | `prerender.ts` | build first, write the chunks into the export, then measure |
1268
- | `budgets.ts` | `measureDocumentJs` weighs `data-x-entry` as well as `<script src>` |
1269
-
1270
- **A stats row is keyed by the route's DECLARED path, and holds its heaviest page**
1271
- (`As of 2026-08-23`). `checkBudgets` looks a route up by `route.url` off the manifest, which is the
1272
- pattern (`/blog/:slug`), and `prerenderSite` pushed `artifact.path` — the filled one
1273
- (`/blog/hello`). So no dynamic static route had ever been weighed: each was `X_BUDGET_UNMEASURED`
1274
- and `X_BUDGET_EXCEEDED` could not fire for the whole class. The heaviest page and not the first,
1275
- because a budget is a ceiling and the page that breaks it is the one the route answers for; the
1276
- report's `emitted` list still names every filled path.
1277
-
1278
- **Both halves of the CSP are computed at boot, `As of 2026-08-23`.** `style-csp.ts` was alone, and
1279
- the hydration runtime is emitted INLINE in every document carrying an island — so with
1280
- `script-src 'self' 'wasm-unsafe-eval'` no island booted in any container. `x dev` sends the policy
1281
- report-only (`dev: true`), which is exactly why nobody saw it: the page hydrated on a laptop and
1282
- never in the image. `startWeb` extends both directives; the property is pinned end to end by
1283
- `dev-roles-script-csp.test.ts`, which parses a served document, hashes every executable inline
1284
- script in it and asserts the response's own `script-src` names each one — with `dev: false`, the
1285
- only mode in which the policy can block anything.
1286
-
1287
- **One `Bun.build` per island, never one call with N entry points**, and `splitting: false`. The
1288
- island's `src` is a string, so no import edge reaches it and the page's graph stays the page's
1289
- (axiom 6) — a shared chunk would put that number back behind a graph walk, and the budget compares
1290
- against bytes. Two islands that both import the same helper each carry a copy; that is the honest
1291
- number for what booting either one costs.
1292
-
1293
- **The chunk URL is SOURCE-addressed, not byte-addressed, `As of 2026-09-06`.** `graphHash` hashes
1294
- the build's inputs — the source map's `sourcesContent`, sorted, plus the entry's app-relative path,
1295
- the framework version and `Bun.version` — with render's own `contentHash`, the function that already
1296
- stamps an ETag and a precache revision. Hashing the OUTPUT is what it did, and `Bun.build` is not
1297
- byte-deterministic under `minify`: measured on 1.4.0, one entry point, no source file touched, a
1298
- 131,589-byte island alternated between two outputs of identical length differing only in minified
1299
- identifier names, roughly one build in ten. The URL then flapped — ten names in ten minutes in
1300
- ai-maxxing — so the service worker precached a chunk that 404ed and the `immutable` cache never hit.
1301
- The cost of the alternative was measured and refused: `minify: { identifiers: false }` IS
1302
- deterministic and is +47% raw, +20% gzipped, on every island of every app. `sourcemap: 'external'`
1303
- pays for the input list (277ms against 276ms) and its `//# debugId=` line is stripped, so the
1304
- shipped bytes are unchanged. Two processes can therefore serve two byte strings at one URL — the
1305
- same program under different local names — and a per-file cache keeps ONE process serving one.
1306
- Delete `graphHash` the day `Bun.build` is deterministic.
1307
-
1308
- **`x dev`, the container and the static export all mount the same table.** `serve.ts` builds the
1309
- islands at boot for the same reason it mounts `apiRoutes()`: a seam that works in dev and not in the
1310
- image is the same failure one release later. `x dev` rebuilds them on the watcher tick — an island
1311
- is the single module this process never imports, so there is no Bun module cache to invalidate —
1312
- and the SAME tick re-imports the route module beside them when its source changed
1313
- (`app-load.ts`'s `reloadRoute`: `<path>?x-reload=<hash>` is the one cache key Bun honours,
1314
- `registerRoute` replaces the entry for the same file, and `dev-render.ts` reads the entry back from
1315
- the table on every request rather than closing over the one it was built from). Until 2026-09-07
1316
- the island was the only reload that took effect, so a save served a new island under an old page —
1317
- the old props, the placeholder the new island renders when they are missing. A route module and
1318
- nothing else: an action, a query or an entity is held by every module that imported it, and no
1319
- re-import can rebind those. `@ultimat3/render`'s loader admits the query
1320
- (`/\.tsx(?:\?[^/]*)?$/`) and strips it before reading the file — anchored on `.tsx$`, the
1321
- re-import fell through to Bun's own JSX loader and every reloaded page died on `__xh`.
1322
-
1323
- **The dev fixture is its own repository** (`.git/HEAD` in `DEV_FIXTURE_FILES`). The framework's root
1324
- `.gitignore` lists `packages/cli/.dev-fixture/`, `devIgnore` honours every ancestor up to a `.git`,
1325
- and so the watcher admitted the fixture root and nothing under it: every run booted the reload path
1326
- and none exercised it. The marker is what lets `cmd-dev.test.ts` save a page and await the tick.
1327
-
1328
- **`app-load.ts` skips `*.island.tsx` deliberately.** It registers no primitive, and importing it
1329
- would put the one module guaranteed to be outside the server's graph inside this process's, where a
1330
- top-level `document` reference takes the whole scan down.
1331
-
1332
- **The budget is charged from the emitted document, and it names the island.** An island's chunk is
1333
- reached by `import()` from inside the hydration runtime, so it is never a `<script src>` — weighing
1334
- script tags alone charged a page for the runtime and never for the code that runtime boots.
1335
- `measureDocumentJs` reads `data-x-entry` as what it is, dedupes it (two instances of one island are
1336
- one module), and `prerenderSite` maps the heaviest URL back through the bundle so
1337
- `X_BUDGET_EXCEEDED` names `apps/web/site/pricing/calculator.island.tsx` and not a hash.
1338
-
1339
- `X_ISLAND_INVALID` is **borrowed** from `@ultimat3/render`, not twinned: "this src cannot become a
1340
- client entry" is what that code already means, and the bundler is simply the half that can see
1341
- whether the file exists. A failed compile is `X_BUILD_FAILED` — an island is a bundle entry point
1342
- like any other, and `Bun.build` *rejects* rather than answering `success: false`, so the catch is
1343
- the real path.
1344
-
1345
- ### `dev-assets.ts` is where the image pipeline meets HTTP
1346
-
1347
- Three packages declare what an image is and none of them serves one: `@ultimat3/seo` says what a
1348
- variant URL means (`parseImageQuery`) and produces the bytes (`builtinImageDriver`),
1349
- `@ultimat3/storage` says what a variant is called and where it is cached (`variantKey`), and
1350
- `@ultimat3/pwa` says which icons a web manifest promises (`planIcons`, `BuiltinImagePipeline`).
1351
- Pixels are `@ultimat3/core`'s pipeline, only ever. This file picks two base paths — `ICON_BASE_PATH`
1352
- and `MEDIA_BASE_PATH` — and decides nothing else; a resize, a format table or a second cache key
1353
- here is the drift the split exists to prevent.
1354
-
1355
- **`/media` and `/_storage` are one authz decision, not two.** Both serve objects off the app's only
1356
- disk, so `/media/*key` declares what `dev-storage.ts` declares — `auth: 'required'` +
1357
- `STORAGE_READ_PERMISSION` + `enforcedBy: 'handler'` — and calls the same two functions, in the same
1358
- order: `authorizeStorageRead` then `assertReadableKey`. It shipped `auth: 'public'` with no policy
1359
- and no tenant check while its twin required both, which made every tenant's uploads one URL away in
1360
- production (`serve.ts` mounts it), and `?w=` made it an unauthenticated `put` besides. The tenant
1361
- test lives in ONE function both routes call, and `storage-surfaces.test.ts` pins the pair against
1362
- each other — every case names the verdict absolutely as well as comparing the two, because equality
1363
- alone is satisfied by both surfaces failing open together. Cacheability follows the key, not the
1364
- route: a tenant-scoped key takes `AUTHORIZED_OBJECT_CACHE` (`private, max-age=0`, varying on
1365
- `authorization`/`cookie`), and only a key no tenant owns keeps `immutable`. A genuinely public image
1366
- belongs under `apps/web/site/`, which is a static asset and never touches that disk.
1367
-
1368
- **A variant is CACHED only at a width the framework can mint.** The cache key is built entirely
1369
- from caller-supplied query values, so `?w=1`, `?w=2`, … each wrote a new object to the app's only
1370
- disk, on a route every signed-in tenant may reach for their own keys. `@ultimat3/seo`'s
1371
- `MAX_IMAGE_WIDTH` (8192) bounds that and does not close it. `isMintableWidth` is the bound:
1372
- `DEFAULT_WIDTHS` **plus the source's own intrinsic width**, which is exactly the set `usableWidths`
1373
- puts in a `srcset` — the constant alone would refuse the widest entry of any image whose intrinsic
1374
- width is not one of the eight. Anything outside it is still served; only the `put` is refused, so
1375
- no caller gains a new 4xx. `?q=` is deliberately still unbounded here — the closed set for quality
1376
- is `@ultimat3/seo`'s to declare, not this file's.
1377
-
1378
- **`/favicon.ico` is a mechanism, not a scaffolded file.** Every browser requests it unprompted, the
1379
- scaffold wrote none and neither served surface mounted a route, so a permanent 404 sat in the console
1380
- of every app the framework produces — noise that trains the reader to ignore console errors, which is
1381
- the opposite of what `--json` and an executable `fix:` are for (#272). Two rungs and one path: the
1382
- app's own `apps/web/site/favicon.ico` wins, and `favicon.ts` answers a 32x32 PNG encoded through
1383
- `@ultimat3/core`'s own pipeline when there is none — the same encoder `x new`'s icon goes through, so
1384
- there is no second image format in the tree and no base64 blob nobody can verify. It is deliberately
1385
- NOT derived from `ICON_SOURCE`: resizing the install icon needs `@ultimat3/pwa`'s pipeline and would
1386
- make the answer depend on a file that may be absent, which is a third rung under a mechanism that has
1387
- exactly two. The file is read per REQUEST, so dropping one into a running `x dev` takes effect
1388
- without a restart. It mounts through `assetRoutes`, which is the one route set `serve.ts` and
1389
- `cmd-dev.ts` both compose — a favicon added to one of them alone is a 404 that comes back in
1390
- production only — and `prerenderSite` writes the same bytes into the static export, because an
1391
- artifact served with no process behind it has to carry every byte the browser will ask for.
1392
-
1393
- `ICON_SOURCE` lives here, not in `cmd-doctor.ts`, because this is the module that reads it: the
1394
- diagnostic checks what `x dev` serves, so one constant cannot pass the check and serve nothing.
1395
- It is a **PNG** — core decodes PNG and JPEG only, and the SVG this used to name could never
1396
- become an icon.
1397
-
1398
- The routes mount whether or not the source icon exists, and a missing one is refused with
1399
- `X_PWA_ICON_MISSING` and its fix — a route that silently disappears is a 404 whose meaning an agent
1400
- has to guess. Deliberately **not** also a boot finding: `x doctor` already reports this condition,
1401
- with this code, and two reporters of one condition is the duplication this package's own rule
1402
- forbids. `x dev` owns the runtime half; the diagnostic owns the other.
1403
-
1404
- ### `hold.ts` is why a long-running command outlives its own result
1405
-
1406
- `dispatch` renders a `CommandResult` and `bin.ts` exits on the code — so a command whose server is
1407
- still listening when `run` resolves is a command the exit code takes down, between the line that
1408
- announced the url and the first request to it. `x dev` and `x mcp serve --transport http` both did.
1409
-
1410
- The one answer is `CommandResult.hold`: report first, then `dispatch` awaits the hold before the
1411
- exit code. `holdUntilShutdown` installs core's signal handlers (`installSignalHandlers` — until
1412
- this it had no callers anywhere, which is why `cmd-mcp.ts`'s `onShutdown` registration was never
1413
- reached), waits on the **drain's first phase** rather than on a signal list of its own, and
1414
- releases what core's lifecycle never learned about — the embedded Postgres, the worker, the
1415
- watcher — *after* the drain, so an in-flight request still has the database it opened against.
1416
- Ctrl-C is therefore the same three phases production runs, not a kill that leaves `.x/pgdata`
1417
- locked by a process that no longer exists.
1418
-
1419
- **The release runs INSIDE the drain's own deadline, `As of 2026-08-23`, and it is the same
1420
- deadline.** `drain()` ABANDONS a hook that overruns `ShutdownReason.deadlineAt` — the process is
1421
- meant to exit without it — and `release` re-enters the very same teardown one call later:
1422
- `app.stop()` → `startRoles().stop()` → `worker.stop()`, memoised in the package that owns it, so
1423
- awaiting it is awaiting the promise the drain just walked away from. Unbounded, that hung past
1424
- `terminationGracePeriodSeconds` and the kubelet SIGKILLed a process that had already drained
1425
- cleanly. The budget is the hook's own `reason.deadlineAt`, not a stopwatch of ours, so there is one
1426
- number and not two; an overrun is logged as `X_SHUTDOWN_TIMEOUT` and a REJECTION still rejects,
1427
- because `dispatch` awaits the hold inside its own `try`.
92
+ | `cmd-build.ts` / `image-prepare.ts` | `x build`: static gate first; docker stamps `BUILD_ID` and writes the island store |
93
+ | `prerender.ts` / `measure-scope.ts` / `static-report.ts` | the static export; routes rendered only to weigh run inside a request as core's `measurementActor()`, with the app's API answered in process (`withInProcessFetch`) |
94
+ | `sw-artifacts.ts` / `pwa-artifacts.ts` / `favicon.ts` | the service worker, the web manifest, `/favicon.ico` |
95
+ | `cmd-db.ts` / `migrations.ts` / `db-generate.ts` / `db-branch.ts` / `db-seed.ts` / `db-backfill.ts` / `db-subscribes.ts` / `db-accept-created.ts` | one migration engine for `x db` and `ROLE=migrate` |
1428
96
 
1429
- **`options.exit` has exactly one caller: `runRole` in `serve.ts`.** `bin.ts` ends in
1430
- `process.exit(code)`, so `x dev` and `x mcp` need nothing; `apps/web/server.ts` — which is what a
1431
- container runs — has no such line, and one non-unref'd interval anywhere in the app then holds an
1432
- event loop with nothing left to do. A function rather than a boolean because `process.exit` inside
1433
- a library is untestable, and the caller is the one that knows.
1434
-
1435
- Commands: `bun test`, `bunx tsc --noEmit -p tsconfig.json`.
1436
-
1437
- ## A declared flag with no reader is a promise `x help` makes and nothing keeps
1438
-
1439
- `x deploy --critical` said *"security deploy: forces clients to reload"* and forced nothing: the
1440
- value is written into the plan JSON (`cmd-deploy.ts`) and **no package reads that field**. The
1441
- parser accepts every declared flag, so this is neither a parse error nor a type error — the flag
1442
- worked perfectly and meant nothing, to the operator most likely to be shipping a security patch.
1443
-
1444
- `flag-reads.ts` is the rule that can see the class of defect: **every flag a command declares is
1445
- read by something in the CLI's own source**, as `X_CLI_FLAG_UNREAD`. The four global flags are
1446
- excluded — `--json`, `--help`, `--cwd` and `--verbose` are the parser's, read once for every
1447
- command, and a per-command rule would report all thirty declarations of `--json`. The read test is
1448
- deliberately generous: a bare `'name'` literal anywhere outside a `name:`/`short:` spec field
1449
- counts, so a flag echoed only into `--json`, or read through a shared constant, is read. A gate
1450
- that guessed at intent would report findings about working commands.
1451
-
1452
- It is enforced by `flag-reads.test.ts`, in the `unit` step — the same shape `cmd-planned.test.ts`
1453
- and `error-catalog.test.ts` use for a rule about the CLI's own declarations, and the reason its
1454
- `fix:` is a `bun test` line rather than an `x` command: the rule can only ever fire in this repo.
1455
- Promoting it to `x verify`'s `boundaries` host check is one line in `scripts/verify.ts`.
1456
-
1457
- **It does not catch `--critical`, and that is the honest limit.** The flag IS read —
1458
- `flagBool(ctx.args, 'critical')` — and what had no consumer was the plan FIELD, one level below any
1459
- rule over names. Two stronger rules were measured and rejected: "the read must not be a property
1460
- initializer" reports six flags, five of which work (`x db --allow-destructive`, `x jobs --queue`);
1461
- "the summary must match the behaviour" is undecidable. So the flag's summary now says what it does,
1462
- and forcing a reload is **not a thing this framework does**, `As of 2026-08`. `updateSignal`
1463
- had no runtime caller for four majors and 9.0.0 deleted it rather than wiring it: `pwa` is tier 4
1464
- and the two runtimes holding both build ids — `http` (2) and `sync` (3) — are below it, so no
1465
- legal import could ever have reached the function. A deploy command has no channel to a running
1466
- client regardless; the plan is `docker compose up` or `helm upgrade`. What ships is notification:
1467
- `useConnection().updateAvailable` from `@ultimat3/realtime`.
1468
-
1469
- ## Planned commands are commands
1470
-
1471
- Every command in `wiki/CLI-Reference.md`'s planned table is in the registry, built from
1472
- `PLANNED_COMMANDS` in `cmd-planned.ts`, and exits `X_NOT_IMPLEMENTED` with a `fix:` naming the
1473
- closest **shipped** command. `X_CLI_UNKNOWN_COMMAND` would say "you typed something that does not
1474
- exist", which is false and sends an agent hunting a typo. `cmd-planned.test.ts` enforces both
1475
- halves: every row is reachable through the parser, and no `fix` points at another planned command.
1476
-
1477
- `PLANNED_SUBCOMMANDS` is the same promise one level down, and `x db studio` is its only entry.
1478
- A subcommand stays in its command's `subcommands` list — the parser reaches it, `x help db` lists
1479
- it — and the owning `run` does `throw plannedSubcommand('db', 'studio')`. Dropping it from the list
1480
- instead would answer `X_CLI_UNKNOWN_SUBCOMMAND`, which is the same lie the table above closes.
1481
-
1482
- ## `guards/` is how an app makes its own convention a build error
1483
-
1484
- Axiom 3 says a convention that is not a build error does not exist, and until 1.2.0 the framework
1485
- gave an app no way to create one: `VERIFY_STEP_NAMES` is a closed literal list with no extension
1486
- point. A file in `guards/` closes it.
97
+ ### Introspection and tools
1487
98
 
1488
99
  | File | Job |
1489
100
  |---|---|
1490
- | `guards.ts` | what a guard IS, how the directory is read, and what a guard is held to |
1491
- | `templates/guard.ts` | `x g guard <name>` — the emitted rule, its pure half and its test |
1492
- | `cmd-verify.ts` | one line in the `boundaries` step: `guardFindings(ctx.root)` |
1493
-
1494
- **It rides on `boundaries`, and it is not an eighteenth step.** The `HostCheck` contract already
1495
- says the shape — *a host adds findings to a step; it can never add, remove, reorder or skip one* —
1496
- so "green" keeps meaning exactly what it meant, whatever an app writes. `boundaries` is the step
1497
- whose host slot already carries "rules this repo makes about itself that the framework cannot
1498
- know" (the monorepo's tier table arrives through it), and it runs third, before any suite, so a
1499
- convention failure comes back in seconds. `guardFindings` is *typed* as a `HostCheck` and is
1500
- composed by the step rather than registered as one: the slot is `Partial<Record<VerifyStepName,
1501
- HostCheck>>`, one function per step, so an app registering there would evict the framework's own
1502
- tier check — and `verifyCommand.run` passes no `hostChecks` at all, which is why an app-supplied
1503
- check could not have reached the gate through that field in the first place.
1504
-
1505
- **Discovered, never registered.** `guards/*.ts`, minus `*.test.ts`, sorted. Nothing imports a
1506
- guard, nothing lists one, and there is no `defineGuard` to call — a guard that has to announce
1507
- itself is a guard an app can forget to announce, which is the coupling axiom 8's extension model
1508
- rejects. A `*.test.ts` beside a guard is its test: importing it would run a suite inside the gate.
1509
-
1510
- **A guard returns `Finding[]`, so it inherits everything.** `--json`, the step table, the summary
1511
- counts and the exit code are all projections of what it returns (axiom 2); a guard that printed or
1512
- chose an exit code would be a second gate. It never throws for a normal result — a throw is
1513
- `X_GUARD_FAILED`, reported as a finding rather than taking the run down.
1514
-
1515
- **And what it returns is held to the error contract.** `findingProblem` demands an
1516
- `X_SCREAMING_SNAKE` code, a non-empty cause, and a `fix:` that passes `fixProblem` — the *same*
1517
- rule `x verify`'s `errors` step applies to every shipped `fix:` in this repo. It runs on the
1518
- returned value, which is the half a static scan cannot reach: a `fix` assembled at run time has no
1519
- literal to read. Three codes, one per way a guard can fail to be one — `X_GUARD_INVALID` (no
1520
- usable export), `X_GUARD_FAILED` (it threw), `X_GUARD_FINDING_INVALID` (what it returned is not a
1521
- finding). Anything else about a guard is the app's business: no size ceiling, no budget, no rule
1522
- about what it may check.
1523
-
1524
- **The validator may never be the thing that crashes.** `findingProblem` names an offending value
1525
- through `shown()` and not `JSON.stringify` — which refuses a BigInt — and every candidate is read
1526
- inside a `try`, because reading one can throw on its own (a getter that raises, a proxy that
1527
- refuses). A guard returning `[1n]` is `X_GUARD_FINDING_INVALID`, per candidate, so one unreadable
1528
- entry costs its own line and not the real findings beside it. The mechanism whose job is producing
1529
- structured failures handing back a stack trace is the one outcome it exists to prevent.
1530
-
1531
- **`x new` ships NINE guards, `As of 2026-09-08`** — the four below plus five interface rules
1532
- (`semantic-interactive`, `focus-visible`, `image-dimensions`, `animated-layout-property`,
1533
- `island-without-states`), each statically decidable and each carrying a legitimate-lookalike test
1534
- that must NOT be reported. **Two of the original four were broken the whole time and nobody could
1535
- see it**, because no guard had ever run against a real app: `raw-colour` reported all 87 uses of
1536
- `rgb(var(--color-…))` — which IS the token form `_colors.scss` emits, so its own cause line ("a
1537
- value no theme can restate") was false of every one — and `untranslated-string`'s JSX mask
1538
- `/\{[^{}]*\}/g` does not nest, so `{t('k', { org: x })}` lost its inner brace group first and the
1539
- remnant read as prose, flagging every `t()` call with an interpolation object or a template-literal
1540
- key. Both are fixed and both now report zero against `examples/dummy`, which is the first tracked
1541
- app to carry a `guards/` directory at all. Read the count, never this sentence:
1542
- `bun -e "import {scaffoldGuardFiles} from './packages/cli/src/templates/scaffold-guards'; console.log(scaffoldGuardFiles().length)"`.
1543
-
1544
- The scaffolded `AGENTS.md` states nine
1545
- non-negotiables, and five of them used to be prose — each proven green on `x verify`: a hardcoded
1546
- JSX string beside a `t()` call, `color: #ff0000` in a stylesheet whose own scaffolded header called
1547
- it "a lint failure", `toLocaleDateString('en-US')` with no `timeZone`, `t.number` money, and a bare
1548
- `throw new Error` in a repo. Four of the five are now guards the scaffold writes —
1549
- `guard-raw-colour`, `guard-unzoned-date`, `guard-bare-error`, `guard-untranslated-string` — so the
1550
- rule is a build error the day the app is created rather than a sentence an agent may skip. The
1551
- fifth, money-as-float, has **no static signature**; the scaffolded `AGENTS.md` row now points at the
1552
- `MoneyInput` type error that already fires, because shipping a guard that cannot work is worse than
1553
- naming the mechanism that does. Their codes are app codes derived from the guard name, so none of
1554
- them appears in `wiki/Error-Codes.md` or the manifest.
1555
-
1556
- `x g guard <name>` writes `guards/<name>.ts` and its test, and nothing else — no index, no
1557
- registry row, no manifest entry. The emitted rule is the class of failure a guard exists for: a
1558
- migration that adds a `NOT NULL` column with no `DEFAULT` applies cleanly to an empty local
1559
- database and fails on the first production table that already holds rows. The `drift` step reads
1560
- those same files and asks a different question, and a test suite runs against a database the
1561
- statement has never met — which is exactly when an app needs a rule of its own. Its code is
1562
- DERIVED from the guard's name (`guardCode`), never written as a literal: an `X_*` literal in
1563
- framework source is a framework code and `error-catalog.test.ts` requires it to be registered.
1564
-
1565
- That rule is held to a real bar, because it is the worked example every app starts from and a
1566
- demonstration that is wrong on realistic input teaches the wrong shape. Block comments are
1567
- stripped before line comments and both before statements are split, so a commented-out
1568
- `ALTER TABLE` is a note and not a finding that blocks `x verify` over nothing; and `DEFAULT NULL`
1569
- counts as **no** default, because it is one in syntax and none in effect — every existing row still
1570
- takes NULL and still violates `NOT NULL`. Both cases are in the emitted test, which is what proves
1571
- an app's copy still works, and both run through the real seam in `guards.test.ts`.
1572
-
1573
- It is in `FIXTURE_GENERATORS` like the other two, and it is the only generated file that imports
1574
- `@ultimat3/cli` for its types — so the scaffold gate compiling it is what proves a scaffolded app
1575
- can write one at all. The root `tsconfig.json` `x new` scaffolds has no `include`, so `guards/` is
1576
- typechecked there by default; an app whose tsconfig names an explicit `include` list has to add
1577
- `guards/**/*` to it, or its guards compile nowhere.
1578
-
1579
- ## Two generators that scaffold something other than a primitive
1580
-
1581
- `x g island <name> [--at <dir>]` writes a **client entry point**, not a component: the filename is
1582
- how the bundler discovers it and `mount` is how the hydration runtime calls it, so the filename,
1583
- the `mount` export and that `mount` RENDERS are what `templates/island.test.ts` pins — it builds
1584
- the emitted entry with `buildIslands` and drives it with `mountIsland`, so a template that
1585
- typechecks and does not mount is a failing test. It runs the mutation too, rather than describing
1586
- it: the same island with `{count()}` replaced by `{0}` must fail the assertion the live one passes.
1587
- `--at` takes the directory directly rather than deriving one, because the caller that cannot guess
1588
- is `X_ISLAND_INVALID` — its cause already holds the exact path a page's `src` resolved to, so its
1589
- `fix:` hands that path straight back.
1590
-
1591
- `x g admin:page <name> --permission <perm> [--at <dir>]` writes an ordinary TSX component and **no
1592
- `defineRoute` call**, deliberately. `@ultimat3/admin`'s `pages:` is the one thing that puts a page in the route
1593
- table and `guardedPage()` is the one thing that decides it; a generator that emitted a route
1594
- declaration would hand back the unguarded second way in that seam exists to close. The emitted test
1595
- asserts the absence. `--permission` defaults to `<name>:read` rather than to nothing, because an
1596
- empty permission list is `X_ADMIN_PAGE_UNGUARDED` at declaration time. `--at` is the same flag
1597
- `x g island` takes and for the same reason — an app's admin is wherever its `defineAdmin` is, which
1598
- no generator can derive, and the hardcoded `apps/admin/src/pages` sent every other layout (the
1599
- demo's is `apps/admin/app/admin`) to `git mv` after every run.
1600
-
1601
- Both are in `FIXTURE_GENERATORS`, so both are compiled by the scaffold gate.
101
+ | `cmd-registries.ts` / `cmd-jobs.ts` / `cmd-tasks.ts` / `cmd-policy.ts` / `cmd-i18n.ts` | project a framework registry; each pairs CLI wiring with a facts module |
102
+ | `cmd-mcp.ts` / `mcp-host.ts` / `mcp-errors.ts` / `mcp-db-target.ts` | `x mcp serve`: 18 tools, two transports |
103
+ | `cmd-shot*.ts` / `cdp-shot-*.ts` / `browser-launcher*.ts` / `island-*` | `x shot` over raw CDP; `verdict.json` names its own blind spots. Not a gate step |
104
+ | `cmd-pr.ts` / `cmd-ci.ts` | GitHub through `gh`, parsed against a schema. Not gate steps |
105
+ | `error-catalog.ts` | imports every `@ultimat3/*` package so `x errors` answers for any code |
1602
106
 
1603
- Implementing one means deleting its row and adding a real `cmd-<name>.ts` — the summary's
1604
- `(planned)` suffix disappears with it, and `x help` follows automatically.
107
+ ## Adding a command
1605
108
 
1606
- Adding a command: write `cmd-<name>.ts` exporting a `CliCommand`, register it in `registry.ts`,
1607
- add its message keys to `messages.ts`. Help and parsing derive from the spec automatically. A
1608
- command's `run` must be `async`: a synchronous throw escapes every caller that awaits the promise
1609
- the signature promises, `dispatch`'s own error path included.
109
+ Write `cmd-<name>-spec.ts` (the declaration) and `cmd-<name>.ts` (a `CliCommand` whose `spec` is
110
+ that declaration), add one `lazy(<name>Spec, …)` row to `registry.ts`, add its message keys to
111
+ `messages.ts`. Help and parsing derive from the spec. `run` must be `async`.