@ultimat3/cli 21.0.0 → 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.
- package/CLAUDE.md +84 -1650
- package/README.md +40 -2
- package/package.json +30 -30
- package/src/api-registration.ts +124 -0
- package/src/app-artifacts.ts +39 -0
- package/src/app-boundaries.ts +5 -10
- package/src/app-load.ts +37 -5
- package/src/app-openapi.ts +27 -0
- package/src/app-permissions.ts +0 -0
- package/src/app-root.ts +1 -1
- package/src/async-pages.ts +32 -0
- package/src/bin.ts +7 -1
- package/src/boundary-findings.ts +36 -0
- package/src/browser-launcher-fake-html.ts +80 -0
- package/src/browser-launcher-fake.ts +165 -0
- package/src/browser-launcher-port.ts +159 -0
- package/src/browser-launcher.ts +26 -133
- package/src/budgets.ts +11 -1
- package/src/cdp-shot-a11y.ts +77 -0
- package/src/cdp-shot-clock.ts +14 -0
- package/src/cdp-shot-driver.ts +150 -0
- package/src/cdp-shot-element.ts +147 -0
- package/src/cdp-shot-errors.ts +62 -0
- package/src/cdp-shot-keys.ts +152 -0
- package/src/cdp-shot-page.ts +230 -0
- package/src/cdp-shot-watch.ts +241 -0
- package/src/cmd-affected-spec.ts +24 -0
- package/src/cmd-affected.ts +3 -19
- package/src/cmd-build-spec.ts +16 -0
- package/src/cmd-build.ts +46 -20
- package/src/cmd-ci-spec.ts +24 -0
- package/src/cmd-ci.ts +17 -21
- package/src/cmd-db-branch.ts +2 -2
- package/src/cmd-db-spec.ts +99 -0
- package/src/cmd-db.ts +15 -94
- package/src/cmd-deploy-helm.ts +136 -0
- package/src/cmd-deploy-spec.ts +40 -0
- package/src/cmd-deploy.ts +111 -43
- package/src/cmd-dev-spec.ts +25 -0
- package/src/cmd-dev.ts +55 -36
- package/src/cmd-docs-spec.ts +16 -0
- package/src/cmd-docs.ts +2 -11
- package/src/cmd-doctor-spec.ts +21 -0
- package/src/cmd-doctor.ts +4 -18
- package/src/cmd-env-spec.ts +18 -0
- package/src/cmd-env.ts +4 -13
- package/src/cmd-errors-spec.ts +23 -0
- package/src/cmd-errors.ts +3 -17
- package/src/cmd-fix-spec.ts +18 -0
- package/src/cmd-fix.ts +4 -13
- package/src/cmd-generate-spec.ts +31 -0
- package/src/cmd-generate.ts +46 -42
- package/src/cmd-i18n-spec.ts +16 -0
- package/src/cmd-i18n.ts +7 -14
- package/src/cmd-jobs-spec.ts +56 -0
- package/src/cmd-jobs.ts +3 -50
- package/src/cmd-manifest-spec.ts +15 -0
- package/src/cmd-manifest.ts +19 -20
- package/src/cmd-mcp-spec.ts +21 -0
- package/src/cmd-mcp.ts +2 -15
- package/src/cmd-new-spec.ts +33 -0
- package/src/cmd-new.ts +38 -33
- package/src/cmd-policy-spec.ts +13 -0
- package/src/cmd-policy.ts +2 -9
- package/src/cmd-pr-spec.ts +39 -0
- package/src/cmd-pr.ts +5 -65
- package/src/cmd-registries-spec.ts +32 -0
- package/src/cmd-registries.ts +4 -25
- package/src/cmd-routes-spec.ts +12 -0
- package/src/cmd-routes.ts +2 -7
- package/src/cmd-secrets-spec.ts +19 -0
- package/src/cmd-secrets.ts +22 -34
- package/src/cmd-shot-island.ts +12 -13
- package/src/cmd-shot-spec.ts +53 -0
- package/src/cmd-shot.ts +11 -60
- package/src/cmd-tasks-spec.ts +21 -0
- package/src/cmd-tasks.ts +2 -16
- package/src/cmd-test-spec.ts +54 -0
- package/src/cmd-test.ts +4 -49
- package/src/cmd-verify-spec.ts +28 -0
- package/src/cmd-verify.ts +3 -23
- package/src/db-seed.ts +4 -2
- package/src/dev-dashboard.ts +4 -4
- package/src/dev-lock.ts +18 -2
- package/src/dev-port.ts +21 -0
- package/src/dev-route-table.ts +3 -3
- package/src/dispatch.ts +12 -5
- package/src/document-styles.ts +1 -1
- package/src/drift.ts +4 -18
- package/src/error-catalog.ts +16 -9
- package/src/error-codes.ts +28 -30
- package/src/error-pages.ts +19 -7
- package/src/errors.ts +14 -0
- package/src/favicon.ts +2 -2
- package/src/fix-imports.ts +1 -1
- package/src/fix-scan.ts +2 -9
- package/src/flag-reads.ts +1 -2
- package/src/foreign-text.ts +36 -0
- package/src/framework-schema.ts +5 -6
- package/src/generate-feature.ts +42 -0
- package/src/generate-files.ts +7 -3
- package/src/generate-grants.ts +83 -0
- package/src/generate-kinds.ts +56 -4
- package/src/i18n-index.ts +59 -10
- package/src/icon-assets.ts +1 -1
- package/src/image-prepare.ts +14 -0
- package/src/import-scan.ts +63 -0
- package/src/index.ts +67 -348
- package/src/invocation-flags.ts +26 -0
- package/src/island-bundle.ts +6 -1
- package/src/island-capture.ts +6 -6
- package/src/island-harness-script.ts +6 -2
- package/src/island-realtime.ts +15 -8
- package/src/island-shot.ts +5 -0
- package/src/island-store.ts +131 -0
- package/src/island-verdict.ts +1 -1
- package/src/job-registration.ts +42 -0
- package/src/jobs-driver.ts +2 -2
- package/src/load-findings.ts +51 -0
- package/src/mcp-db-target.ts +1 -1
- package/src/mcp-errors.ts +28 -27
- package/src/mcp-host.ts +27 -12
- package/src/mcp-ui-diff.ts +27 -0
- package/src/mcp-ui-inspect.ts +4 -4
- package/src/mcp-ui-interact.ts +21 -15
- package/src/mcp-ui.ts +19 -15
- package/src/measure-database.ts +73 -0
- package/src/measure-paths.ts +74 -0
- package/src/measure-scope.ts +74 -0
- package/src/messages.ts +1 -3
- package/src/metrics-endpoint.ts +1 -1
- package/src/otlp-export.ts +1 -1
- package/src/output.ts +6 -0
- package/src/permission-grants.ts +86 -0
- package/src/prerender-out.ts +25 -0
- package/src/prerender.ts +121 -102
- package/src/pwa-artifacts.ts +3 -3
- package/src/reexport-manifest.ts +2 -1
- package/src/registry.ts +80 -56
- package/src/role-realtime.ts +36 -0
- package/src/{dev-replicator.ts → role-replicator.ts} +1 -1
- package/src/{dev-roles-fixture.ts → role-start-fixture.ts} +9 -5
- package/src/role-start-types.ts +112 -0
- package/src/{dev-roles.ts → role-start.ts} +42 -115
- package/src/{dev-sync.ts → role-sync.ts} +18 -8
- package/src/root-env.ts +67 -0
- package/src/{dev-assets.ts → runtime-assets.ts} +7 -7
- package/src/{dev-services.ts → runtime-bindings.ts} +39 -16
- package/src/{dev-cache.ts → runtime-cache.ts} +2 -2
- package/src/runtime-jobs.ts +87 -0
- package/src/{dev-live-feed.ts → runtime-live-feed.ts} +18 -5
- package/src/{dev-notify-retention.ts → runtime-notify-retention.ts} +1 -1
- package/src/{dev-purge.ts → runtime-purge.ts} +2 -2
- package/src/{dev-queue.ts → runtime-queue.ts} +4 -4
- package/src/runtime-realtime.ts +55 -0
- package/src/{dev-replica.ts → runtime-replica.ts} +2 -2
- package/src/{dev-runtime.ts → runtime-services.ts} +41 -17
- package/src/{dev-storage.ts → runtime-storage.ts} +4 -4
- package/src/scaffold-fixture.ts +28 -6
- package/src/scaffold-typecheck.ts +6 -3
- package/src/schema-drift.ts +7 -1
- package/src/script-csp.ts +5 -2
- package/src/secrets-rotation.ts +59 -0
- package/src/serve-boot.ts +192 -0
- package/src/serve-drain.ts +24 -0
- package/src/serve-entry.ts +6 -0
- package/src/serve-env.ts +116 -0
- package/src/serve-types.ts +55 -0
- package/src/serve.ts +44 -347
- package/src/shot-server.ts +2 -2
- package/src/shot-settle.ts +10 -1
- package/src/shot-theme.ts +3 -3
- package/src/shot-verdict.ts +16 -7
- package/src/signal-shred.ts +27 -0
- package/src/solid-loader.ts +26 -2
- package/src/static-report.ts +8 -1
- package/src/templates/action.ts +30 -16
- package/src/templates/entity.ts +12 -7
- package/src/templates/index.ts +1 -1
- package/src/templates/job.ts +10 -7
- package/src/templates/policy.ts +20 -2
- package/src/templates/resource-create.ts +127 -0
- package/src/templates/resource-form-island.ts +49 -12
- package/src/templates/resource.ts +10 -4
- package/src/templates/scaffold-app.ts +4 -1
- package/src/templates/scaffold-auth.ts +3 -1
- package/src/templates/scaffold-container-compose.ts +184 -0
- package/src/templates/scaffold-container.ts +24 -159
- package/src/templates/scaffold-dashboard-example.ts +2 -2
- package/src/templates/scaffold-db-package.ts +16 -3
- package/src/templates/scaffold-demo-org.ts +41 -0
- package/src/templates/scaffold-entries.ts +1 -1
- package/src/templates/scaffold-helm-templates.ts +66 -7
- package/src/templates/scaffold-helm.ts +27 -5
- package/src/templates/scaffold-i18n.ts +15 -10
- package/src/templates/scaffold-repo.ts +13 -10
- package/src/templates/scaffold-roles.ts +38 -10
- package/src/templates/slice-foundation.ts +1 -1
- package/src/templates/wrap.ts +4 -1
- package/src/test-passes.ts +2 -1
- package/src/test-workers.ts +26 -0
- package/src/ts-scan.ts +3 -6
- package/src/tsconfig-references.ts +1 -2
- package/src/verify-checks.ts +41 -39
- package/src/verify-e2e.ts +10 -7
- package/src/verify-run.ts +11 -1
- package/src/verify-step.ts +3 -3
- package/src/verify-tests.ts +1 -26
- package/src/verify-typecheck.ts +28 -0
- package/src/web-binding.ts +2 -2
- package/src/workspace-graph.ts +10 -33
- package/src/cdp-browser.ts +0 -94
- package/src/cdp-connection.ts +0 -247
- package/src/cdp-e2e-page.ts +0 -180
- package/src/cdp-e2e-session.ts +0 -199
- package/src/cdp-errors.ts +0 -56
- package/src/cdp-launch.ts +0 -193
- package/src/cdp-offline-script.ts +0 -73
- package/src/cdp-pipe.ts +0 -77
- package/src/e2e-app.ts +0 -103
- package/src/e2e-browser-handle.ts +0 -55
- package/src/e2e-dom-fixture.ts +0 -117
- package/src/e2e-driver.ts +0 -117
- package/src/e2e-errors.ts +0 -117
- package/src/e2e-evaluate.ts +0 -156
- package/src/e2e-locator.ts +0 -86
- package/src/e2e-page.ts +0 -153
- package/src/e2e-preload.ts +0 -64
- package/src/e2e-probe.ts +0 -23
- package/src/e2e-selection.ts +0 -182
- package/src/e2e-spawn.ts +0 -169
- package/src/measurement-actor.ts +0 -26
- /package/src/{dev-hooks.ts → runtime-hooks.ts} +0 -0
- /package/src/{dev-render.ts → runtime-render.ts} +0 -0
package/CLAUDE.md
CHANGED
|
@@ -1,1677 +1,111 @@
|
|
|
1
1
|
# @ultimat3/cli — boundary
|
|
2
2
|
|
|
3
|
-
Tier 5. May import tiers 0–4.
|
|
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
|
-
|
|
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.
|
|
8
|
+
Commands: `bun test packages/cli` (from the repo root — the test preload lives there),
|
|
9
|
+
`bunx tsc --noEmit -p packages/cli/tsconfig.json`.
|
|
230
10
|
|
|
231
|
-
|
|
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.
|
|
11
|
+
## Rules
|
|
237
12
|
|
|
238
|
-
|
|
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 |
|
|
13
|
+
| Rule | Detail |
|
|
276
14
|
|---|---|
|
|
277
|
-
| `
|
|
278
|
-
| `
|
|
279
|
-
|
|
|
280
|
-
|
|
|
281
|
-
| `
|
|
282
|
-
| `
|
|
283
|
-
|
|
|
284
|
-
|
|
|
285
|
-
| `
|
|
286
|
-
| `
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
`
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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.
|
|
358
|
-
|
|
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.
|
|
367
|
-
|
|
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`.
|
|
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`)
|
|
404
42
|
|
|
405
43
|
| File | Job |
|
|
406
44
|
|---|---|
|
|
407
|
-
| `
|
|
408
|
-
| `
|
|
409
|
-
| `
|
|
410
|
-
| `
|
|
411
|
-
| `
|
|
412
|
-
| `
|
|
413
|
-
| `
|
|
414
|
-
| `
|
|
415
|
-
| `
|
|
416
|
-
| `
|
|
417
|
-
| `
|
|
418
|
-
| `
|
|
419
|
-
| `
|
|
420
|
-
| `
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
**Absent by default, and that is a requirement rather than a state.** Nothing here runs until
|
|
425
|
-
`installE2eDriver` is called, so `hasE2eDriver()` still answers `false` and the gate's `e2e` step
|
|
426
|
-
still refuses instead of passing over a browser it does not have. This paragraph also said "CI has
|
|
427
|
-
no Chrome" until 2026-08-27, and that is false and was the reason issue #390's fourth requirement
|
|
428
|
-
— a real browser check — was recorded as out of reach: GitHub-hosted `ubuntu-latest` ships one at
|
|
429
|
-
`/usr/bin/google-chrome`, preinstalled, with no download step and no new dependency.
|
|
430
|
-
|
|
431
|
-
**The browser is RAW CDP over Bun's own `WebSocket`, and carries no dependency.**
|
|
432
|
-
`packages/scraping/src/cdp-port.ts` declares a ~25-method port because `ScrapePage` is a full
|
|
433
|
-
scraping surface and its intended implementation is `puppeteer-core`. `E2eBrowserPage` is FIVE
|
|
434
|
-
methods, and CDP's wire format is one JSON object with an `id` — so the whole thing an e2e driver
|
|
435
|
-
needs is four small modules, which is why `x test e2e` needs nothing installed that `bun install`
|
|
436
|
-
did not already put there. `e2e/cdp-browser.e2e.test.ts` drives a real Chrome against a real
|
|
437
|
-
`Bun.serve` and asserts all five methods; `openE2eBrowserIfAvailable()` answering `undefined` is
|
|
438
|
-
what makes it a SKIP on a laptop without one rather than a red step.
|
|
439
|
-
|
|
440
|
-
**The load EVENT is the completion signal, never `Page.navigate`'s reply.** Measured on Chrome 150:
|
|
441
|
-
a navigation that swaps the render process — `about:blank` → `http://localhost:<port>/`, the most
|
|
442
|
-
ordinary one there is — loads the page, hits the server and answers a later `Runtime.evaluate` from
|
|
443
|
-
the new document, and the navigate frame **never comes back at all**. A driver that awaited the
|
|
444
|
-
reply waited out its full deadline on every first navigation. So `cdpConnect().once()` registers a
|
|
445
|
-
`Page.loadEventFired` waiter BEFORE the send, and the reply is raced against it — still read, but
|
|
446
|
-
only for `errorText`, which is the one place a refused navigation is named.
|
|
447
|
-
|
|
448
|
-
**A CDP call is deadlined and a close settles every call in flight.** Without that, a suite whose
|
|
449
|
-
browser died waits out one full deadline per call and reports a timeout, where the true fault is a
|
|
450
|
-
dead browser. The four codes are four repairs, which is why they are not one:
|
|
451
|
-
`X_CDP_BROWSER_MISSING` (install one), `X_CDP_LAUNCH_FAILED` (read the browser's own stderr, which
|
|
452
|
-
the cause carries), `X_CDP_CALL_FAILED` (look at the page), `X_CDP_TIMEOUT` (raise the deadline).
|
|
453
|
-
|
|
454
|
-
**`evaluate` is the edge that cannot be lossless.** `PageLike.evaluate` takes a closure and every
|
|
455
|
-
browser port in this framework takes a string, so what crosses is `Function.prototype.toString()`
|
|
456
|
-
and nothing else. A zero-parameter closure naming only page globals is supported; a native or bound
|
|
457
|
-
function, a declared parameter and a method shorthand are refused STATICALLY, before a byte leaves;
|
|
458
|
-
a binding the page does not have comes back named, from the page's own `ReferenceError`. Measured on
|
|
459
|
-
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
|
|
460
|
-
runs**, so a captured PRIMITIVE can vanish from the source and never fail at all, while a captured
|
|
461
|
-
reference always survives as its name. No static rule in this process can see the difference — which
|
|
462
|
-
is why the refusal is raised from the page's answer rather than from a scan of the source.
|
|
463
|
-
|
|
464
|
-
**One of `E2eFixtures`' four members still refuses, and it is the one that is not a port gap.**
|
|
465
|
-
`update()` needs a second build served under a new immutable build id, which is a fact about the
|
|
466
|
-
SERVER, and no page port has ever been able to speak for one. `offline()`/`online()` FORWARD — to
|
|
467
|
-
`E2eBrowserPage.offline`, which `cdp-e2e-page.ts` implements as
|
|
468
|
-
`Network.emulateNetworkConditions` and `@ultimat3/scraping` implements through
|
|
469
|
-
`CdpPageLike.setOfflineMode`. They refused until 2026-08-27 on a reason the tree contradicted on
|
|
470
|
-
the day it was written. A fixture that silently no-opped would make the assertion after it read as
|
|
471
|
-
proof — `offline()` followed by "the fallback rendered" is the app's ONLINE page passing an offline
|
|
472
|
-
test — so an `E2eBrowserPage` that declares no `offline` still gets the refusal, now naming the
|
|
473
|
-
method the double is missing rather than a capability the framework does not have.
|
|
474
|
-
|
|
475
|
-
## The service worker is emitted here, because the emitter needs facts only a build has
|
|
476
|
-
|
|
477
|
-
`@ultimat3/pwa` shipped `generateServiceWorker`, `buildPrecacheManifest`, `offlineFallbackSource`,
|
|
478
|
-
`backgroundSyncSource` and `pushSource` since it existed, and every one had **zero callers** outside
|
|
479
|
-
its own package. So `pwa.offline`, `pwa.backgroundSync`, `pwa.push` and every route's own `offline:`
|
|
480
|
-
were declarations with no build behind them, and no Ultimate app worked offline however its config
|
|
481
|
-
was written (#390). `sw-artifacts.ts` is the caller.
|
|
482
|
-
|
|
483
|
-
**Why here and not beside the manifest.** `loadPwaArtifacts(root)` needs a root and a config file;
|
|
484
|
-
the worker needs the ROUTE TABLE and the ISLAND BUNDLE as well — facts only a booted app and a
|
|
485
|
-
finished build have. Splitting them keeps `loadPwaArtifacts` callable before either exists, which
|
|
486
|
-
`x doctor` and the icon writer rely on. The route table is `describeRoutes()`, the one projection
|
|
487
|
-
`x.manifest.json`, `/_x`, the sitemap and `sw.js` are all built from, so a route added to the app
|
|
488
|
-
cannot be missing from the precache manifest.
|
|
489
|
-
|
|
490
|
-
| Surface | What it does with the worker |
|
|
491
|
-
|---|---|
|
|
492
|
-
| `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 |
|
|
493
|
-
| `serve.ts` | the same two routes in the container, from the same call |
|
|
494
|
-
| `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 |
|
|
495
|
-
|
|
496
|
-
**A precache revision is the DOCUMENT's content hash, and until 2026-09-06 it was the build id.**
|
|
497
|
-
`pwaRoutes` projected four of `PwaRoute`'s eight fields, so every route entry read
|
|
498
|
-
`{"url":"/","revision":"<buildId>","bytes":0}`: two deploys of a byte-identical site re-fetched
|
|
499
|
-
every precached page, which is the one thing `packages/pwa/src/precache.ts`' own header says must
|
|
500
|
-
never happen, and `DEFAULT_PRECACHE_WARN_BYTES` was a 5 MB budget over a total that could not count
|
|
501
|
-
one byte of HTML. The cause was ORDER — `prerender.ts` emitted the worker before the render loop —
|
|
502
|
-
so the emission moved after it and `ServiceWorkerInput.documents` carries `renderStatic`'s own
|
|
503
|
-
`hash` and byte count per rendered path. `x dev` and `serve.ts` pass none and keep the build id:
|
|
504
|
-
neither has rendered a page at boot, and inventing a hash for bytes that do not exist is a revision
|
|
505
|
-
that never changes when the page does. **The offline document is fed through its own pair**, because
|
|
506
|
-
`buildPrecacheManifest` adds it ITSELF as `reason: 'fallback'` ahead of every route and `add()`
|
|
507
|
-
keeps the first entry per url — so the route entry of the same url is shadowed and only
|
|
508
|
-
`offlineFallbackRevision` / `offlineFallbackBytes` (`ServiceWorkerConfig`, `@ultimat3/pwa`) can
|
|
509
|
-
decide it. That pair was declared and fed by nobody for one commit; `serviceWorkerArtifacts` reads
|
|
510
|
-
the fallback path out of the same `documents` map, so the one page an offline navigation depends on
|
|
511
|
-
is content-addressed like every other. A fallback this pass did not render — no route serves it,
|
|
512
|
-
which `x doctor` reports as `X_PWA_NO_OFFLINE_FALLBACK` — is absent from the map and keeps the
|
|
513
|
-
build id.
|
|
514
|
-
|
|
515
|
-
**Registration is an EXTERNAL script, never inline**, and that is a CSP fact rather than a
|
|
516
|
-
preference: `startWeb` computes a `script-src` sha256 per inline script, so an unhashed one is
|
|
517
|
-
blocked in the container while passing report-only under `x dev` — which is how the hydration
|
|
518
|
-
runtime shipped broken once already.
|
|
519
|
-
|
|
520
|
-
**`sw.js` is served `no-store` with `Service-Worker-Allowed: /`.** A cached `sw.js` is a worker that
|
|
521
|
-
cannot be replaced: the browser re-fetches it to decide whether an update exists, and an
|
|
522
|
-
intermediary answering the old bytes pins every client to the deploy that shipped them. Without the
|
|
523
|
-
header the browser refuses to let a worker served from `/` control `/` — the failure `assertScope`
|
|
524
|
-
cannot see, because the scope a REGISTRATION asks for has to be allowed by the script's own response
|
|
525
|
-
and not only by its path.
|
|
526
|
-
|
|
527
|
-
**`api/` and `shared/` never cross.** An API response is a JSON document whose freshness is the
|
|
528
|
-
app's business, and precaching one serves a stale answer to a client that had a network; `shared/`
|
|
529
|
-
is not a URL at all. The filter is a `flatMap` rather than `filter().map()` because the predicate
|
|
530
|
-
does not narrow `surface` for the map that follows it, and a cast would hide the day a fifth surface
|
|
531
|
-
arrives.
|
|
532
|
-
|
|
533
|
-
**`pwa.push` is read and still wires nothing, and it says so.** `generateServiceWorker` emits a push
|
|
534
|
-
handler only when a VAPID key comes with the capability, there is no `pwa.vapid` config key, and it
|
|
535
|
-
drops the handler in SILENCE otherwise. `pushWarning` is this module's own finding, reported through
|
|
536
|
-
`x build --json`'s `serviceWorkerWarnings` — `jobs.driver`'s shape one package over, refused the same way.
|
|
537
|
-
|
|
538
|
-
**The browser check is what let any of this ship.** #390's fourth requirement was *"a real browser
|
|
539
|
-
check that the emitted worker installs, activates and serves the fallback offline. Until it exists,
|
|
540
|
-
do not ship the worker"* — a bad `sw.js` is sticky in a way a manifest is not.
|
|
541
|
-
`e2e/service-worker.e2e.test.ts` registers the emitted file in a real Chrome, waits for it to take
|
|
542
|
-
control, takes the network away, and asserts that a runtime route with nothing cached renders the
|
|
543
|
-
offline document.
|
|
544
|
-
|
|
545
|
-
**And it found the driver bug first.** `E2eFixtures.offline()` did not take the SERVICE WORKER
|
|
546
|
-
offline: a worker fetches on its own CDP target, the condition was only ever set on the page's, and
|
|
547
|
-
a `networkFirst` route the cache had never seen still answered from the network. So an offline
|
|
548
|
-
assertion made on a PWA tested nothing. `cdp-e2e-page.ts` now auto-attaches worker targets and
|
|
549
|
-
carries the condition onto each, including one that attaches AFTER `offline(true)` — the ordinary
|
|
550
|
-
case for a PWA.
|
|
551
|
-
|
|
552
|
-
## The `errors` step enforces the error contract
|
|
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`)
|
|
553
61
|
|
|
554
62
|
| File | Job |
|
|
555
63
|
|---|---|
|
|
556
|
-
| `
|
|
557
|
-
| `
|
|
558
|
-
| `
|
|
559
|
-
| `
|
|
560
|
-
| `
|
|
561
|
-
| `
|
|
562
|
-
| `source-files.ts` | which files are shipped source — shared with `filesize`, never a second list |
|
|
563
|
-
|
|
564
|
-
**A `fix:` may not cite a command this build does not ship.** Six shipped fix lines named
|
|
565
|
-
`x db status`, `x logs tail`, `x trace`, `x metrics`, `x auth whoami` and `x ai prompts`, and every
|
|
566
|
-
one passed — the text rule checks that a fix NAMES a command, never that the registry holds it.
|
|
567
|
-
`fix-command.ts` resolves the citation, and a PLANNED command fails too: `x logs` parses, `x help`
|
|
568
|
-
lists it, and running it hands the reader `X_NOT_IMPLEMENTED` instead of the fix.
|
|
569
|
-
|
|
570
|
-
**A `fix:` may not cite a file this repo does not have, either.** That was the other half, and
|
|
571
|
-
nothing resolved it: `X_UI_RUNTIME_MISSING` told its reader to paste a line no generator ever wrote,
|
|
572
|
-
through every gate since it shipped (#274, #246). A file token is one of the four things that make a
|
|
573
|
-
fix an instruction at all (`COMMAND_TOKENS`), so `fix-path.ts` is built from the SAME extension list
|
|
574
|
-
— a token that satisfies the instruction rule is exactly the token this one has to resolve, and two
|
|
575
|
-
lists would be a citation the second rule cannot see. `X_ERROR_FIX_PATH_MISSING` is its own code:
|
|
576
|
-
`X_ERROR_FIX_INVALID` means the fix is not an instruction, this one means it is one and points at
|
|
577
|
-
nothing, and the repairs differ.
|
|
578
|
-
|
|
579
|
-
It is narrow so a finding never has to be argued with — three shapes are not judged at all, because
|
|
580
|
-
each resolves against something other than the root the gate is running in: a scoped specifier
|
|
581
|
-
(`@ultimat3/ui/global.scss`, which resolves through `node_modules`), a dot-relative path
|
|
582
|
-
(`./global.scss`, which resolves against the reader's own file) and any path whose **parent
|
|
583
|
-
directory** this root does not have (`src/errors.ts`, `apps/web/server.ts`,
|
|
584
|
-
`packages/i18n/catalogs/en.json` — all three name a directory a generated app has and this repo does
|
|
585
|
-
not). What is left is the citation a root really can answer: a directory that exists, named as
|
|
586
|
-
holding a file it does not hold. A glob must match at least one file. Measured over all three roots
|
|
587
|
-
the gate runs in — the framework, `examples/dummy`, `dummy/social-media-clone` — **117 path citations
|
|
588
|
-
read, 0 findings**, so it enforces outright with no pin table.
|
|
589
|
-
|
|
590
|
-
The rule is **conditional, and that is load-bearing**: *if* a fix cites `x <command>`, it must
|
|
591
|
-
resolve. It does not require every fix to name one — `set OTEL_EXPORTER_OTLP_ENDPOINT=…` and
|
|
592
|
-
`counter('orders_total', { maxSeries: 4000 })` are executable and correctly cite nothing, and a
|
|
593
|
-
universal rule would push an author into citing a command that does not really fix it. A second
|
|
594
|
-
word is judged as a subcommand only when the spec declares subcommands, or `x new my-app` reports
|
|
595
|
-
`my-app` as one. The registry arrives through `await import('./registry')` — `registry → cmd-verify
|
|
596
|
-
→ error-contract` closes a cycle back to the caller, and the precedent for the break is
|
|
597
|
-
`cmd-build.ts`.
|
|
598
|
-
|
|
599
|
-
**It reads a THIRD word, under the same condition.** `x db branch ls --json` resolved — `db` is a
|
|
600
|
-
command, `branch` is one of its subcommands — and the word that decided what actually ran was never
|
|
601
|
-
looked at, so a fix line that created a stray database passed every check the repo had. A third
|
|
602
|
-
word is judged only where the subcommand declares a closed set (`CommandSpec.subcommandPositionals`,
|
|
603
|
-
declared from the constant the command validates against), because `x jobs show <id>` and
|
|
604
|
-
`x db gen "add publish_at"` take open positionals and a universal rule would report findings about
|
|
605
|
-
working invocations. `positionalChoices` cannot express it: `fix-command.ts` reads that field only
|
|
606
|
-
where a command declares no subcommands at all.
|
|
607
|
-
|
|
608
|
-
**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.
|
|
609
|
-
|
|
610
|
-
**And in that one slot, a `<placeholder>` is a finding too.** `x db branch <name>` is what two
|
|
611
|
-
`@ultimat3/mcp` fix lines said; the citation reader does not read `<name>` as a word, so the slot
|
|
612
|
-
was never examined and the line resolved clean while running it answers `X_CLI_UNKNOWN_COMMAND` —
|
|
613
|
-
the same blind spot in a second disguise. A closed set means the slot is a verb, so there is
|
|
614
|
-
nothing a reader could substitute that would make it run. `CITATION` therefore matches a
|
|
615
|
-
placeholder in the third slot **only**: `x jobs show <id>` and `x db branch drop <name>` are correct
|
|
616
|
-
fix lines and must stay invisible to this rule.
|
|
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 |
|
|
617
70
|
|
|
618
|
-
`
|
|
619
|
-
walk, one entry per code, the owning registry preferred over any throw site and over a registry
|
|
620
|
-
that named the code in its `<PKG>_BORROWED_ERROR_CODES`. The docs check reads it and so does the
|
|
621
|
-
framework's own `framework.manifest.json`, because a second scanner over a narrower file set is a
|
|
622
|
-
manifest that claims completeness it does not have.
|
|
623
|
-
|
|
624
|
-
**A `code:` is a literal, a module-scope const in the same file, or a finding** — `As of 2026-08-23`,
|
|
625
|
-
and until then it was a literal or silence. `scanCodes` matched `code\s*[:=]\s*'X_…'`, so
|
|
626
|
-
`const STALE = 'X_DOC_PACKAGE_GRAPH_STALE'` followed by `code: STALE` — the DRY thing to write, and
|
|
627
|
-
what `scripts/package-map-graph.ts` really wrote — was a declaration to nobody: no manifest row, no
|
|
628
|
-
row demanded on `wiki/Error-Codes.md`, no entry for `bun run gate-codes`, and `x errors explain`
|
|
629
|
-
answering `X_ERROR_CODE_UNKNOWN` for a code the build throws. Silent, and in the **permissive**
|
|
630
|
-
direction: the DRYer the author, the less the gate saw (#277).
|
|
631
|
-
|
|
632
|
-
`scanCodeDeclarations` is that one pass, and it returns both halves. It resolves the identifier
|
|
633
|
-
against the module-scope consts of the **same file** — anchored at column 0, which is what makes it
|
|
634
|
-
module scope without a parser — and reports every name it cannot resolve as `X_ERROR_CODE_UNRESOLVED`
|
|
635
|
-
rather than skipping it, which is the whole point: a scanner that reads only what it likes enforces
|
|
636
|
-
only what it sees. `scanCodes` is its `.sites`, so the manifest, the docs check, `bun run gate-codes`
|
|
637
|
-
and `x errors explain` (through `scanCodeFixSites`, which resolves the same way) cannot see different
|
|
638
|
-
sets. Cross-file resolution was **refused** even though `fix-imports.ts` already does the harder
|
|
639
|
-
version for `fix:`: it would make the scan async for every caller, and the finding is the better
|
|
640
|
-
answer anyway — one file holds both the code and its only spelling.
|
|
641
|
-
|
|
642
|
-
Three shapes are deliberately not judged, each measured over the framework and both tracked apps
|
|
643
|
-
before the rule shipped. A name that resolves to something that is **not** a code is an answer, not
|
|
644
|
-
a gap (`const STATUS_NOT_FOUND = 404` in `@ultimat3/realtime`'s NATS fake, the one live instance). A
|
|
645
|
-
**table read** is not judged — `SEO_ERROR_CODES.metaMissing` is how `@ultimat3/seo` and
|
|
646
|
-
`@ultimat3/ui` raise all 18 of their codes, and the registry those literals live in already declares
|
|
647
|
-
them. A **lowercase** name is not judged: 164 sit at a `code:` position in this tree and every one is
|
|
648
|
-
a type annotation (`readonly code: string`) or a re-raise (`code: opts.code`). Measured on all three
|
|
649
|
-
roots: **0 findings**, so it enforces outright with no pin table.
|
|
650
|
-
|
|
651
|
-
An empty `fix`, or a `fix` that says `check` / `make sure` / `try` / `see the docs` and names no
|
|
652
|
-
command, call or file path, is `X_ERROR_FIX_INVALID`. A declared code the host's error reference
|
|
653
|
-
does not name is `X_ERROR_CODE_UNDOCUMENTED` — `wiki/Error-Codes.md` here, nothing in a generated
|
|
654
|
-
app, which is why that half arrives as a host check (`scripts/verify.ts`) rather than a hardcoded
|
|
655
|
-
path in this package.
|
|
656
|
-
|
|
657
|
-
`ts-scan.ts` masks comments and string contents before it looks for structure. The contract's own
|
|
658
|
-
3-line rendering appears verbatim in doc blocks and interpolated messages, and a scanner that read
|
|
659
|
-
those as declarations would report findings nobody can fix. What it cannot see is a `fix` with no
|
|
660
|
-
literal — a parameter, or a table lookup with no fallback. Those are out of a static scan's reach,
|
|
661
|
-
and the step says so rather than guessing.
|
|
662
|
-
|
|
663
|
-
**A fix does not always arrive under a key**, and until `As of 2026-08` the scanner assumed it did.
|
|
664
|
-
`@ultimat3/mcp`'s `readonly-sql.ts` hands every fix positionally to a local `rejected(cause, fix)`
|
|
665
|
-
helper, so the file held no `fix:` at all and `scanFixes` returned `[]` for all of it — the
|
|
666
|
-
citation resolver was never given a string to judge, and two stale `x db branch <name>` lines
|
|
667
|
-
shipped through the hole. `scanFixes` now also reads the argument in the `fix: string` position of
|
|
668
|
-
a **local** helper, under four rules, each with its own case in `fix-scan.test.ts`: the helper must
|
|
669
|
-
BUILD an error (`code` key or `new …Error(` in its body), or `citedCommandProblem(fix, catalog)` —
|
|
670
|
-
which takes a fix to *judge* it — would have its call sites read as declarations; the parameter
|
|
671
|
-
list may hold no rest or destructured parameter, because neither has a reliable position; the call
|
|
672
|
-
may not be a member access; and the argument must BE one literal, stricter than the key path,
|
|
673
|
-
because `prefix + 'x doctor'` reads as one literal there and publishing half a fix is worse than
|
|
674
|
-
publishing none. Measured over the whole tree: 16 files gained readable fixes, `readonly-sql.ts`
|
|
675
|
-
went from 0 to 7, and **zero** new findings.
|
|
676
|
-
|
|
677
|
-
**And a fix does not always arrive in the file that declares its builder**, which is where four
|
|
678
|
-
bad `fix:` lines in `packages/ui/src/icons/build-icons.ts` shipped: `invalidIconDataError` is
|
|
679
|
-
declared in `packages/ui/src/errors.ts`, and a per-package `errors.ts` full of factories is the
|
|
680
|
-
house pattern, so the same-file rule left the most common shape of all unchecked. `fix-imports.ts`
|
|
681
|
-
resolves it — the specifier is relative, the candidate paths are `<base>.ts{,x}` and
|
|
682
|
-
`<base>/index.ts{,x}`, and the parameter position is the callee's. An alias is renamed to what the
|
|
683
|
-
CALLER writes; a local declaration of the same name wins, because that is the function the call
|
|
684
|
-
actually reaches. One module cache per run: `errors.ts` is imported by every file in its package.
|
|
685
|
-
|
|
686
|
-
**An error CLASS is the same helper one keyword away**, and is now read too: the name is the
|
|
687
|
-
class's, the parameter list its `constructor`'s, `new X(…)` is a call like any other. It was
|
|
688
|
-
measured as dead code in the same-file rule — zero same-file call sites — and cross-file it is
|
|
689
|
-
`@ultimat3/render`'s fourteen classes plus `@ultimat3/core`'s three image ones.
|
|
690
|
-
|
|
691
|
-
Measured over the whole tree, `As of 2026-08`: **791 → 877** fix literals read, 37 files gained
|
|
692
|
-
one, and **3 findings** the gate had never been able to see — `x verify --contract` and
|
|
693
|
-
`x build --route` (two flags no command declares) and one `check …` line with no command token.
|
|
694
|
-
|
|
695
|
-
What it still cannot see is a builder imported from another **package**: `candidatePaths` refuses a
|
|
696
|
-
non-relative specifier, because resolving one means guessing which of 29 packages a bare name came
|
|
697
|
-
from and a wrong guess reads an unrelated function's argument as a fix. Measured: 3 call sites in
|
|
698
|
-
this repo, none of them a finding. It is **not** left silent — the step's `output` carries
|
|
699
|
-
`checked {n} fix line(s), could not read {m}`, counted at `FixScan.unreadable`: an argument in a
|
|
700
|
-
KNOWN fix position that is not one literal. Deliberately not "imports I could not open", which is
|
|
701
|
-
1504 names here and 1310 of them are `join` and `UltimateError` — a number nobody can act on.
|
|
702
|
-
|
|
703
|
-
`cli → admin` is a declared sideways edge (`scripts/lib/tiers.ts`): `x dev` **mounts** the
|
|
704
|
-
dashboard, it never grows a second one. The CLI's only contribution is the facts no registry
|
|
705
|
-
holds — a SQL runner, the caught outbox, the committed manifest, the process's own services, the
|
|
706
|
-
spans it recorded — supplied as `defaultDevSources({ hooks })`.
|
|
707
|
-
|
|
708
|
-
Wired means answerable: all eleven panels answer in a `x dev` process, and a hook the CLI does
|
|
709
|
-
not supply is a panel that refuses with a wiring line, never one that renders empty. `timeline`
|
|
710
|
-
is core's tracer (`x dev` is what calls `configureTelemetry`), `cache` is
|
|
711
|
-
`recentInvalidations()`, `policy` is `@ultimat3/policy`'s own `policyMatrix()` over the app's
|
|
712
|
-
roles — a verdict re-derived here would be the second authz the framework exists to prevent.
|
|
713
|
-
`subscribers` is the one source left unwired: `@ultimat3/realtime` retains no matcher trace, and
|
|
714
|
-
that trace is the live panel's question, so the panel degrades to its own note instead.
|
|
715
|
-
|
|
716
|
-
`dev-traces.ts` reads a span's panel kind off its **name prefix** — a subsystem that starts emitting
|
|
717
|
-
spans adds its prefix to `KIND_BY_PREFIX` or its work is filed under `action`. `db.` is there
|
|
718
|
-
because `@ultimat3/db`'s two funnels open one span per statement (`db.select`, `db.begin`), and a
|
|
719
|
-
statement is the one span that states its own identity — `STATEMENT_ATTRIBUTE`, **imported** from
|
|
720
|
-
`@ultimat3/db` by both `dev-traces.ts` and its test rather than spelled as a literal, which the
|
|
721
|
-
recorder prefers over the name, so the timeline's `repeatedSql` groups SQL texts and not span names. Those spans
|
|
722
|
-
exist only where a `StatementObserver` is installed, so a trace with no DB children is a process
|
|
723
|
-
with no statement diagnostic, not a broken recorder.
|
|
724
|
-
|
|
725
|
-
`dev-n-plus-one.ts` is that observer, and `cmd-dev.ts` is the **only** place that installs it —
|
|
726
|
-
`serve.ts` installs neither it nor the in-process trace RECORDER (`createTraceRecorder`, which is
|
|
727
|
-
`/_x/timeline`'s source), the same line that file already draws for `/_x`. **It is not "no
|
|
728
|
-
exporter"**, `As of 2026-08`: `serve.ts` calls `startOtlpExport(options.env)`, because a collector
|
|
729
|
-
named in the chart has to receive spans from the container and not only from a laptop. What a
|
|
730
|
-
production process does without is the *statement* diagnostic and the in-memory timeline — the
|
|
731
|
-
ledger and the recorder go in together and come out together in `stop()`, because the timeline's
|
|
732
|
-
SQL rows and the repeat counts are one feature with one toggle, and uninstalled the seam costs the
|
|
733
|
-
one `undefined` branch it already pays (axiom 6).
|
|
734
|
-
|
|
735
|
-
Three rules hold the ledger, each load-bearing. **Per request, keyed by the `Ctx` object** — a
|
|
736
|
-
`WeakMap` whose entry dies with the request, so nothing sweeps and nothing accumulates across a dev
|
|
737
|
-
session; a statement issued outside a request is not counted at all, because "five of one shape"
|
|
738
|
-
only means something inside one unit of work. The price of keying on identity is that a
|
|
739
|
-
`withChildContext` scope is its own tally. **A shape is `entity.op` when attributed**, the
|
|
740
|
-
statement's own text with whitespace collapsed when it is not — `members.findById` fifty times is
|
|
741
|
-
what an author can act on, and grouping fifty point lookups by their SQL would report bind values.
|
|
742
|
-
That rule is **not written here**: `statementFingerprint`/`statementKind` are `@ultimat3/db`'s and
|
|
743
|
-
the threshold is `@ultimat3/entity`'s `N_PLUS_ONE_THRESHOLD`, because `@ultimat3/testing`'s
|
|
744
|
-
`statements` fixture is a second detector and a copy of either would let a loop that fails a test be
|
|
745
|
-
a different loop from the one this ledger warns about. What stays here is what only a dev *server*
|
|
746
|
-
knows: the request as the unit of work, the bound report list, one log line per request per code.
|
|
747
|
-
**An expected statement is not counted** — `expectedQueryLoop` suppresses a verdict and this ledger
|
|
748
|
-
is the verdict, so the span and the timeline still show the loop while the thing that warns is told
|
|
749
|
-
the author already answered. A shape is promoted to a verdict exactly once, on the statement that
|
|
750
|
-
crosses the threshold, and its count keeps rising: a loop of fifty is one report reading fifty. The
|
|
751
|
-
report list is bounded and drops its oldest.
|
|
752
|
-
|
|
753
|
-
`statement-loop.ts` is the **one** projection those verdicts reach four surfaces through, and the
|
|
754
|
-
reason there is only one is that four renderings of one loop must be one sentence. It hands a
|
|
755
|
-
verdict to `@ultimat3/entity`'s `nPlusOne()` — the `fix:` speaks that package's vocabulary and is
|
|
756
|
-
derived from the relations the schema already declared — and each surface takes a field of what
|
|
757
|
-
comes back: `cmd-dev.ts` appends `loopFinding` to the `findings` getter (text and `--json` render it
|
|
758
|
-
for free), `dev-dashboard.ts` supplies `statementLoops` so `/_x/timeline` shows `nPlusOne` for the
|
|
759
|
-
request on screen, `cmd-dev.ts` again passes `devNotices` down `startRoles` so the browser overlay
|
|
760
|
-
renders the loop under the error, and the ledger itself emits `warnLoop` — one `logger.warn` per
|
|
761
|
-
request per code, the ids riding along from core's `setLoggerContextFields`.
|
|
762
|
-
|
|
763
|
-
Two rules about *when* a count is read. **A surface reads it live**: the finding, the panel row and
|
|
764
|
-
the notice all say `ran 50 times` because they ask after the loop finished, while the log line says
|
|
765
|
-
`ran 5 times` because it was written the moment the threshold was crossed — same verdict, two
|
|
766
|
-
honest moments. **A verdict belongs to its request**: `repeatsFor(ctx)` reads the request's own
|
|
767
|
-
tally rather than filtering the bounded global list, so the overlay still names a loop the bound
|
|
768
|
-
already dropped. `serve.ts` supplies no `devNotices`, so the seam it boots through is a key that is
|
|
769
|
-
absent, not a hook answering an empty list.
|
|
770
|
-
|
|
771
|
-
`dev-n-plus-one.test.ts` and `statement-loop.test.ts` drive the ledger and the projection with
|
|
772
|
-
hand-built `StatementEvent`s — fast, and enough to pin every rule above. `n-plus-one-detector.test.ts`
|
|
773
|
-
proves the loop those events stand in for: real `posts`/`authors` entities, `postgresRepo` and
|
|
774
|
-
`createPgliteClient` (an injected fake driver so no `@electric-sql/pglite` build is needed, but a
|
|
775
|
-
real client — `createRecordingClient` implements `DbClient` on its own and never reaches the
|
|
776
|
-
observer, so it cannot stand in here) — a naive per-row `findById` loop trips `X_N_PLUS_ONE_QUERY`
|
|
777
|
-
with the exact `preload('author')` line, the `preload()` form of the same read stays quiet,
|
|
778
|
-
`expectedQueryLoop` silences the naive form without stopping it from running, and a naive per-row
|
|
779
|
-
`delete` loop trips `X_N_PLUS_ONE_WRITE`. Its describe block spells the pattern `n1`, matching
|
|
780
|
-
`packages/entity/src/n-plus-one.test.ts`'s own fixture prefix, because `bun test -t 'n+1'` is a
|
|
781
|
-
regex and `+` is a quantifier — `n1` is what actually selects these tests.
|
|
782
|
-
|
|
783
|
-
## One migration engine, four environments
|
|
71
|
+
### Boot (`x dev`, the container)
|
|
784
72
|
|
|
785
73
|
| File | Job |
|
|
786
74
|
|---|---|
|
|
787
|
-
| `
|
|
788
|
-
| `
|
|
789
|
-
| `
|
|
790
|
-
| `
|
|
791
|
-
| `
|
|
792
|
-
| `
|
|
793
|
-
| `
|
|
794
|
-
| `
|
|
795
|
-
| `drift.ts` | `checkSourceDrift`: the `.hash` sidecar the `drift` step compares, no database needed |
|
|
796
|
-
| `schema-diff.ts` | what two GENERATED snapshots disagree about, as data — the pure half |
|
|
797
|
-
| `schema-drift.ts` | `checkMigrationDrift`: entity declarations against the newest `.snapshot.json`, and the composition the `drift` step and `x doctor` both read |
|
|
798
|
-
| `db-destructive.ts` | `checkDestructiveMigrations`: the same step's second half — every committed `up` that drops, truncates or retypes must carry `-- destructive: true` |
|
|
799
|
-
| `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>` |
|
|
800
|
-
| `db-backfill.ts` | `x db backfill --list`: the flag parsing, the ledger read and the table |
|
|
801
|
-
| `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 |
|
|
802
|
-
|
|
803
|
-
`jobs-driver.ts` is the ONE place a CLI command gets hold of the app's queue — `withJobDriver`,
|
|
804
|
-
which `x jobs` and `x db backfill` both call. It reuses an ambient `jobDriver()` when a process
|
|
805
|
-
already installed one (inside `x dev` or `x mcp serve`, booting a second queue talks to the wrong
|
|
806
|
-
database) and otherwise boots `startQueue` and releases it in a `finally`, or a CLI that exits
|
|
807
|
-
holding the PGlite lock breaks the next command run against this app. A second copy of that boot
|
|
808
|
-
would be two answers to "which queue is this command talking to".
|
|
809
|
-
|
|
810
|
-
**`x db branch` takes a VERB, and a branch name can never be one.** `ls`, `create <name>`,
|
|
811
|
-
`drop <name>` — a closed set, declared once in `BRANCH_SUBCOMMANDS` and read three ways: the
|
|
812
|
-
command validates against it, `dbCommand.spec.subcommandPositionals` declares it so the `errors`
|
|
813
|
-
step can resolve a citation against it, and the refusal for an unknown word lists it. The bare-name
|
|
814
|
-
form it replaces is why: the argument *was* the name, so `x db branch ls --json` — the `fix:` on
|
|
815
|
-
the planned `x branch`, on `X_DB_BRANCH_FAILED`, and (as `create`/`drop`) on `@ultimat3/db`'s own
|
|
816
|
-
`X_BRANCH_EXISTS` and `X_SQL_UNSAFE` — cloned a database called `ls` and returned no listing. All
|
|
817
|
-
four passed every check the repo had, because `fix-command.ts` resolved two words and the third was
|
|
818
|
-
the one that decided what ran.
|
|
819
|
-
|
|
820
|
-
**`drop` has no confirmation flag, and that is the design.** It may only drop what `ls` shows: an
|
|
821
|
-
external branch is a database carrying the marker comment `createBranch` writes **and** this
|
|
822
|
-
database's own `<source>_branch_` prefix, an embedded one is a `pgdata-<name>` directory, so the
|
|
823
|
-
shared database this session is connected to is in neither set.
|
|
824
|
-
The typo is impossible rather than the keystroke tedious — and `@ultimat3/db` already ships
|
|
825
|
-
`x db branch drop <name>` as `X_BRANCH_EXISTS`'s `fix:` with no flag on it, so a flag here would
|
|
826
|
-
break a shipped instruction.
|
|
827
|
-
|
|
828
|
-
**The prefix half is not decoration, and it is no longer the only source guard.** The marker records
|
|
829
|
-
the base `As of 2026-08-19` — `ultimate:branch:<base>:<iso>`, read back as `BranchInfo.base` — so
|
|
830
|
-
`reapBranches` can skip another app's clones on its own. The prefix guard still stands and is what
|
|
831
|
-
`ls`/`drop` read, because an **older** marker records no base at all: it is skipped by the reaper
|
|
832
|
-
rather than dropped, which leaves `drop` needing an answer that does not depend on a field half the
|
|
833
|
-
branches lack. One Postgres server hosting two Ultimate apps answers `listBranches()` with both
|
|
834
|
-
apps' clones, and `branchNameOf` reduced `postly_branch_feat` and `analytics_branch_feat` to the
|
|
835
|
-
same branch name — so `x db branch drop feat`, run against `postly`, was authorised by
|
|
836
|
-
`analytics`'s row and then issued `drop database if exists "postly_branch_feat"` against a database
|
|
837
|
-
carrying no marker at all: a `DROP DATABASE` the guard had never approved, and nothing recoverable
|
|
838
|
-
about it. `branchNameIn(source, database)` is the source-scoped inverse of `branchDatabaseName` and
|
|
839
|
-
the one `ls` and `drop` both read; `branchNameOf` survives for `mcp-db-target.ts` alone, which has
|
|
840
|
-
a URL and no connection to ask `current_database()` with.
|
|
841
|
-
|
|
842
|
-
**The membership check lives inside `dropExternalBranch`, not in the wiring above it.** One
|
|
843
|
-
connection, one listing, one statement before the `DROP` — a listing taken by the caller and acted
|
|
844
|
-
on afterwards is two connections and a window wide enough to hold a whole `create`. It is still not
|
|
845
|
-
atomic and cannot be: `DROP DATABASE` runs in no transaction, so no single statement both verifies
|
|
846
|
-
the marker and deletes. Closing the last gap means a lock around both halves inside
|
|
847
|
-
`@ultimat3/db`'s `dropBranch` — which a `psql` at the next terminal would not hold either.
|
|
848
|
-
|
|
849
|
-
**`ls` is the reason `create` no longer shells out to `psql`.** `listBranches()` finds branches by
|
|
850
|
-
`createBranch`'s marker comment; the `psql` path wrote the `CREATE DATABASE` and no comment, so
|
|
851
|
-
every branch the CLI made was invisible to the only lister the framework has. External branching
|
|
852
|
-
now runs through `@ultimat3/db` on one `role: 'migrate'` client — `max: 1`, no statement timeout,
|
|
853
|
-
both load-bearing: `CREATE DATABASE … TEMPLATE` is refused while any *other* session holds the
|
|
854
|
-
template, and cloning a real database outlives a `web` profile's 10s.
|
|
855
|
-
|
|
856
|
-
**`DatabaseTarget.production` is a fact this package supplies, and it was the literal `false`.**
|
|
857
|
-
`mcp-db-target.ts` is the only place one is ever built, so `assertBranchDatabase`'s first refusal —
|
|
858
|
-
"production is never migratable from MCP at all" — could not run for any database the CLI produced;
|
|
859
|
-
a production database was refused only incidentally, because its name lacked `_branch_`, and one
|
|
860
|
-
named `shop_branch_hotfix` read as a branch and was migratable. It is now core's one key, read the
|
|
861
|
-
way `x doctor` reads it. An **unreadable** `ULTIMATE_ENV` counts as production: `tryResolveEnvironment`
|
|
862
|
-
answers `undefined` for exactly one input — a value that is not an environment — and a guard that
|
|
863
|
-
read a typo as "not production" would be defeated by the misconfiguration it exists to survive.
|
|
864
|
-
`staging` stays false; `branch: null` is already what refuses it, and widening the flag would make
|
|
865
|
-
the refusal say something untrue.
|
|
866
|
-
|
|
867
|
-
`x db backfill` has four shapes and a **dry run is the default**: `--list` reports the ledger,
|
|
868
|
-
`--pending` reports declared-minus-completed and exits non-zero when there is drift, `<name>` plans
|
|
869
|
-
one sweep, and `--all` plans every pending one. `--write` is never implied — the inspection forms
|
|
870
|
-
and the acting form are the same command, and the flag is the only thing that separates them.
|
|
871
|
-
`--all --write` isolates per name and continues past a failure, exiting non-zero naming each, so one
|
|
872
|
-
wedged cleanup cannot block every later one forever.
|
|
873
|
-
|
|
874
|
-
Until 1.2.0 a bare `x db backfill <name>` threw `X_NOT_IMPLEMENTED`, and the ledger was the only
|
|
875
|
-
half that existed: `x_backfills` recorded what had run, and **nothing recorded what was pending**, so
|
|
876
|
-
a scaffolded backfill could be merged and deployed and silently never run. `--pending` is the alarm
|
|
877
|
-
that closes it; `registeredBackfills()` is what makes a declaration visible before its first pass.
|
|
878
|
-
|
|
879
|
-
`x db migrate` and `ROLE=migrate` are the same function call. That is the whole design: until
|
|
880
|
-
1.2.0 the CLI shelled out to `bunx drizzle-kit` — a second engine, a second journal, declared in no
|
|
881
|
-
`package.json` and fetched unpinned at run time — while the release phase used the framework's
|
|
882
|
-
ledger, so "what has been applied" had two answers that only agreed by luck. `cmd-db.test.ts`
|
|
883
|
-
holds the line from both ends: no shipped source spawns a second migrator, and this file still
|
|
884
|
-
imports `runMigrations` from `./serve`.
|
|
885
|
-
|
|
886
|
-
**The post-condition is one check too, and it is the database one.** `runMigrations` runs
|
|
887
|
-
`@ultimat3/db`'s `checkDrift()` inside the queue's lifetime — the connection it opened for the
|
|
888
|
-
migrator is the only one there is — and returns the report on `MigratedApp.drift`, so a developer
|
|
889
|
-
and a release phase verify the same thing. `x db migrate` renders it through `driftFindings` and
|
|
890
|
-
exits non-zero; `runRole` throws the first difference for `ROLE=migrate`, so the release phase
|
|
891
|
-
exits non-zero too. Both entrypoints call the same `runMigrations` and both fail — the difference
|
|
892
|
-
is only the channel each has. `ROLE=migrate` logged and exited 0 until it did not: a release phase
|
|
893
|
-
whose only signal is the exit code reported success over a schema nobody can reconstruct, which is
|
|
894
|
-
the failure the post-migrate check exists to catch.
|
|
895
|
-
|
|
896
|
-
**`x db gen` emits `REPLICA IDENTITY FULL`, and the set is DECLARED rather than derived**,
|
|
897
|
-
`As of 2026-08-26` (#357). `@ultimat3/realtime` refuses a live subscription to a table without it —
|
|
898
|
-
logical replication carries no old row on an UPDATE, so no patch can be computed — and for two
|
|
899
|
-
years nothing in the framework emitted one. It could not be derived, and that is the load-bearing
|
|
900
|
-
fact: the relation name lives inside the query's `sql:` callback, which no generator can invoke
|
|
901
|
-
without valid input (`describeSql` says so itself — "`null` when no sample input was supplied").
|
|
902
|
-
So a live query DECLARES it (`subscribes:`, `@ultimat3/query`), the declaration is machine-checked
|
|
903
|
-
against the resolved `shape.entity` on the first subscribe (`X_QUERY_SUBSCRIBES_DRIFT`), and
|
|
904
|
-
`db-subscribes.ts` reads it off `describeQueries()` — the same source `frameworkSources` copies onto
|
|
905
|
-
`QueryFact.subscribes`, one hop earlier, because building the manifest here would re-load the app
|
|
906
|
-
and demand a `package.json` that `x db gen` has never needed.
|
|
907
|
-
|
|
908
|
-
**The third `subscribes:` refusal is this package's, because no other tier can ask it.**
|
|
909
|
-
`@ultimat3/db` keeps only the declared names an entity's table matches and DROPS the rest — it has
|
|
910
|
-
no way to tell a typo from a table another migration owns — and `@ultimat3/query` holds no table
|
|
911
|
-
catalog at all. So `subscribes: ['posts', 'user']` granted the identity to `posts`, dropped `user`
|
|
912
|
-
in silence, and read as granted. `X_QUERY_SUBSCRIBES_UNKNOWN` refuses it BEFORE anything is
|
|
913
|
-
written, naming the query and offering the tables the app does declare. It is checked after
|
|
914
|
-
`loadApp`'s findings, never before: a module that would not import leaves the registry short, and
|
|
915
|
-
every name whose entity lives in it would then look like a typo.
|
|
916
|
-
|
|
917
|
-
**And it accepts a table the migrations it just applied demonstrably created**, `As of 2026-08-26`
|
|
918
|
-
(issue #345). A snapshot records only what ENTITIES declare, so a table created by a HAND-WRITTEN
|
|
919
|
-
migration reached no sidecar and was `unexpected-table` on every deploy forever — with a `fix:`
|
|
920
|
-
that generated an empty migration, because `x db gen` diffs the entity registry against the newest
|
|
921
|
-
snapshot and the table is on neither side. `@ultimat3/db` fixed the wording; `acceptCreatedTables`
|
|
922
|
-
(`db-accept-created.ts`) is the half that file's `unexpectedTable` names, and it is composed around
|
|
923
|
-
`checkDrift` inside `runMigrations`, so `x db migrate` and `ROLE=migrate` accept the same set.
|
|
924
|
-
**Only `unexpected-table`, and only for a name a migration's SQL creates** — which is what keeps it
|
|
925
|
-
an acceptance rather than the check switched off: a table absent from the snapshot produces exactly
|
|
926
|
-
one difference (`diffSchema` reports it and never compares its columns), and a table nobody
|
|
927
|
-
declared and no migration created is still reported, cause and `fix:` intact. The evidence is the
|
|
928
|
-
applied list itself: `migrate()` runs first, so every file on disk has been applied by the time the
|
|
929
|
-
question is asked. The verb phrase is read ANCHORED off the raw statement, which is the whole
|
|
930
|
-
protection — a `create table` can only be at position 0 by being one, so `values ('create table
|
|
931
|
-
ghost')` opens with `insert` and a comment-only chunk is not a statement at all. A `stripSqlNoise`
|
|
932
|
-
pass was written first and deleted: it could not change one answer, and a defence that cannot fail
|
|
933
|
-
is one nobody can test. Everything the anchor admits and the name grammar does not — a comment
|
|
934
|
-
between the keywords, a `temp` table, a qualifier naming a schema `checkDrift` never introspected —
|
|
935
|
-
contributes nothing, which reports drift that could have been accepted and never the reverse.
|
|
936
|
-
|
|
937
|
-
**The `drift` step asks a third thing, off the same directory and with no database either: is every
|
|
938
|
-
destructive statement declared?** `db-destructive.ts` reads each committed migration through
|
|
939
|
-
`migrations.ts` — the reader `x db migrate` applies from, because a rail checking a list the
|
|
940
|
-
migrator does not run enforces nothing — and refuses an `up` that drops a table, drops a column,
|
|
941
|
-
truncates or retypes without a `-- destructive: true` line, as `X_MIGRATION_DESTRUCTIVE`. It decides
|
|
942
|
-
none of that itself: `@ultimat3/db`'s `destructive.ts` owns the classifier `db-generate.ts` already
|
|
943
|
-
wrote the marker from, so the generator and the gate cannot disagree about one file. One finding per
|
|
944
|
-
file, never one per statement — the marker declares the whole migration. It rides on `drift` rather
|
|
945
|
-
than becoming an eighteenth step because it is this step's own question over this step's own files;
|
|
946
|
-
a new step is for a genuinely new question.
|
|
947
|
-
|
|
948
|
-
**The `drift` step reads the SNAPSHOT, not only the hash, `As of 2026-08-25`.** `checkSourceDrift`
|
|
949
|
-
compares a schema-source hash to a `.hash` sidecar and never reads what the migration RECORDED, so
|
|
950
|
-
`dummy/social-media-clone` sat green while **nine declared CHECK constraints had never reached any
|
|
951
|
-
database** — a comment body could be whitespace, a like count could go negative, an email needed no
|
|
952
|
-
`@` — and a squash that dropped ten invariants and nine defaults would have been green too. The
|
|
953
|
-
source had not moved, so nothing that hashes source could see it. `checkSnapshotDrift`
|
|
954
|
-
(`schema-drift.ts`) diffs `snapshotOf(describeEntities())` against `declaredSchema(readMigrations())`
|
|
955
|
-
— both sides are `snapshotOf`'s own spelling, which is what makes a check, a default and a column
|
|
956
|
-
type comparable at all. Measured against that app rolled back to the state its gate was green in:
|
|
957
|
-
**20 findings**, 9 checks and 11 defaults.
|
|
958
|
-
|
|
959
|
-
**Two directions, two codes, because they are two repairs.** `X_DB_SCHEMA_UNMIGRATED` is a
|
|
960
|
-
declaration the migrations do not carry — the database will never get it. `X_DB_SCHEMA_UNDECLARED`
|
|
961
|
-
is a migration carrying what nothing declares any more, whose fix names BOTH branches, because the
|
|
962
|
-
declaration may have been lost rather than removed and `x db gen` would emit the DROP. One "drift"
|
|
963
|
-
verdict over both teaches a reader neither.
|
|
964
|
-
|
|
965
|
-
**Absent is not empty, and reading it as "recorded none" would fail every existing app on its first
|
|
966
|
-
run.** `TableDescription.checks` is absent — never `[]` — on a table declaring none, exactly like
|
|
967
|
-
`IndexDescription.using` and `ColumnDescription.generated`. Both sides normalise to empty
|
|
968
|
-
(`schema-diff.ts`), `using` reads through `indexMethodOf` and `order` through `?? 'asc'`, or an app
|
|
969
|
-
whose sidecar predates any of the three reports a difference on every index it has.
|
|
970
|
-
|
|
971
|
-
**The hash half stays, and runs second.** It catches what the snapshot comparison cannot see at all
|
|
972
|
-
— a seed, a helper or a TS-only invariant moving under `packages/db/src` with no statement behind
|
|
973
|
-
it, which is what `reconcileSchemaHash` exists to re-record. It is SUPPRESSED when the snapshot half
|
|
974
|
-
found something: both then answer one condition and only one of them is an instruction, since
|
|
975
|
-
`schema hashes to 3f2a, newest migration recorded 91bc` names no constraint, no column and no table.
|
|
976
|
-
|
|
977
|
-
**An app whose modules will not import is not judged here.** `appEntities` answers `undefined`
|
|
978
|
-
rather than a short registry, which would read as "every table was dropped" and hand out a DROP per
|
|
979
|
-
table for one file's syntax error — the same stance `generateAppMigration` takes with its `blocked`
|
|
980
|
-
outcome. The cost is that a schema check can be silently skipped under an already-red gate; the
|
|
981
|
-
alternative is a false red whose fix destroys data.
|
|
982
|
-
|
|
983
|
-
**The FOURTH thing the `drift` step asks is what neither hash nor snapshot can see: SQL no
|
|
984
|
-
declaration carries at all.** `ALTER TABLE posts REPLICA IDENTITY FULL;` sits in
|
|
985
|
-
`examples/dummy/packages/db/migrations/0001_init.sql`, no generator emits it, no snapshot records
|
|
986
|
-
it, and a squash drops it in silence — and no declaration-based check can ever see it, because a
|
|
987
|
-
regenerated sidecar equals the declaration by construction. `db-ungeneratable.ts` reports it,
|
|
988
|
-
`As of 2026-08-25`. It classifies nothing itself: `@ultimat3/db`'s `ungeneratableStatements`
|
|
989
|
-
matches each statement's leading verb phrase against `GENERATABLE_FORMS` — everything
|
|
990
|
-
`generateMigration` emits, held honest in both directions by that package's own test — because
|
|
991
|
-
every SQL classifier in the tree is db's (`sql-scan.ts`, `statement-split.ts`, `sql-noise.ts`,
|
|
992
|
-
`destructive.ts`) and a second one here is the reimplementation this file's own rule forbids.
|
|
993
|
-
Measured: **7 statements in `examples/dummy`** (five `CREATE TYPE … AS ENUM`, two
|
|
994
|
-
`REPLICA IDENTITY FULL`), **0 in all four `dummy/social-media-clone` migrations** and 0 in
|
|
995
|
-
`examples/dummy`'s own generated `0002_money_scale.sql` — real generator output reports nothing,
|
|
996
|
-
which is the half a rail like this lives or dies on.
|
|
997
|
-
|
|
998
|
-
**The declaration is a header line, and it carries a COUNT: `-- ungeneratable: 7`.** Not
|
|
999
|
-
`@ultimat3/db`'s code but this package's (`X_MIGRATION_UNGENERATABLE`), because the only remedy
|
|
1000
|
-
available for every statement it reports is a line in the migration file, and where an app keeps
|
|
1001
|
-
its migrations is this package's fact — db classifies and deliberately declares no code. Three
|
|
1002
|
-
decisions behind that shape:
|
|
1003
|
-
|
|
1004
|
-
| Decision | Why |
|
|
1005
|
-
|---|---|
|
|
1006
|
-
| 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 |
|
|
1007
|
-
| 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 |
|
|
1008
|
-
| 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 |
|
|
1009
|
-
|
|
1010
|
-
**The `fix:` names the marker first and `x db gen` second, and that order is the point.**
|
|
1011
|
-
Regenerating is exactly what *discards* these statements, so the command every other db code
|
|
1012
|
-
answers with is the one this one must not lead with — `X_MIGRATION_UNGENERATABLE`'s `CLI_FIXES` row
|
|
1013
|
-
is `x verify --only drift`, and the re-declare branch (an enum is a text column plus a check
|
|
1014
|
-
invariant) rides behind an em-dash because it is available for some of the statements and not all.
|
|
1015
|
-
**`REPLICA IDENTITY FULL` was the statement with no second branch, and stopped being one on
|
|
1016
|
-
2026-08-26** (#357): a live query declares the relations it is patched from (`subscribes:`),
|
|
1017
|
-
`db-subscribes.ts` reads them off the same registry the manifest is projected from, `x db gen`
|
|
1018
|
-
emits the ALTER and `@ultimat3/db` records it on the snapshot so it is emitted once. The re-declare
|
|
1019
|
-
branch covers it now: declare `subscribes:` and regenerate. A statement already committed is a
|
|
1020
|
-
different question and still counts — `GENERATABLE_FORMS` (`@ultimat3/db`) matches a leading verb
|
|
1021
|
-
phrase and does not carry this one, measured at 7 found / 7 declared on `examples/dummy`'s
|
|
1022
|
-
`0001_init.sql`, `As of 2026-08-26` — so the marker branch remains the only remedy for SQL on disk.
|
|
1023
|
-
|
|
1024
|
-
**`x db gen` reports what it could not write, and exits 0.** `GeneratedMigration.unrendered` reached
|
|
1025
|
-
the committed `.sql` as a `-- UNRENDERED` comment and nothing else read it; `db-generate.ts` now
|
|
1026
|
-
carries it on every branch (a REQUIRED field, so forgetting to project it is a type error) and
|
|
1027
|
-
`cmd-db.ts` prints the count plus each entry's own remedy and carries the list under
|
|
1028
|
-
`data.unrendered`. Not a non-zero exit: `x db gen` is the `fix:` on `X_DB_DRIFT` and four other
|
|
1029
|
-
shipped errors, and a fix that always exits 1 is not an instruction — the `x i18n add fr` failure,
|
|
1030
|
-
repeated. The red belongs at the gate, and the `drift` step reads the SAME list to decide that
|
|
1031
|
-
`x db gen` is not the fix it should be handing out.
|
|
1032
|
-
|
|
1033
|
-
The *source* half is a different question with a different answer: `checkSourceDrift` hashes the
|
|
1034
|
-
entity source against what `x db gen` recorded, answers the same before and after a migration, and
|
|
1035
|
-
opens nothing — which is what lets the gate run it in a CI with no database. It stays on `x verify`
|
|
1036
|
-
and `x doctor` and is deliberately **not** repeated on `x db migrate`; two reporters of one
|
|
1037
|
-
condition is the duplication this package's own rule forbids. Both were called `checkDrift` until
|
|
1038
|
-
1.2.0, and the one that was wired everywhere was the one that cannot see a column added by hand.
|
|
1039
|
-
|
|
1040
|
-
Generation opens no database. It diffs `describeEntities()` against `declaredSchema(readMigrations(root))`
|
|
1041
|
-
— the snapshot the newest migration wrote down — so `x db gen` answers the same in CI, on a laptop
|
|
1042
|
-
with nothing running, and against a database three migrations behind. An app whose modules will not
|
|
1043
|
-
load generates **nothing**: a short registry is indistinguishable from deleted entities, and the
|
|
1044
|
-
diff would be a DROP nobody asked for.
|
|
1045
|
-
|
|
1046
|
-
**An empty diff re-records the `.hash` sidecar, and that is what makes `X_DB_DRIFT` followable.**
|
|
1047
|
-
The hash `checkSourceDrift` compares covers every non-test file under `packages/db/src` — a seed, a
|
|
1048
|
-
helper, a decorator — not only the ones that imply DDL, and narrowing that glob would trade a loud
|
|
1049
|
-
error for a silent gap in the one check that catches "entities changed and no migration was
|
|
1050
|
-
generated". So detection stays broad and the REMEDY carries the weight: `x db gen "describe the
|
|
1051
|
-
change"` — the exact `fix:` the error hands out — records the current hash against the newest
|
|
1052
|
-
migration when the diff is empty, instead of writing nothing and leaving the gate red forever with
|
|
1053
|
-
hand-editing a generated file as the only way out. `GeneratedFiles.outcome` is the four things a run
|
|
1054
|
-
can be — `generated`, `hash-recorded`, `unchanged`, `blocked` — and `runGen` projects it onto
|
|
1055
|
-
`--json` on **every** branch: reporting `hash-recorded` as `generated` would name a migration nobody
|
|
1056
|
-
can apply, and reporting it as `unchanged` would hide a file this command wrote. That second one
|
|
1057
|
-
shipped: the no-migration branch hardcoded `data: { migration: null, files: [] }`, so the run that
|
|
1058
|
-
wrote the sidecar reported writing nothing to the machine reading the output.
|
|
1059
|
-
|
|
1060
|
-
Nothing is masked, and the branch proves it rather than promising it: `loadApp` reported no findings
|
|
1061
|
-
(the registry is whole, never short), `declaredSchema` returned a real snapshot (`X_MIGRATION_SNAPSHOT_MISSING`
|
|
1062
|
-
otherwise), and the emptiness is `generateMigration`'s own verdict — the same call the written path
|
|
1063
|
-
takes. A migration with no migration id to record against writes nothing, which is the
|
|
1064
|
-
`x new --no-example` case: an entity against zero migrations is `create table` for all of it and
|
|
1065
|
-
never an empty diff. `reconcileSchemaHash` also declines to write when an OLDER migration already
|
|
1066
|
-
recorded the hash, because `checkSourceDrift` already answers clean there and restamping the newest
|
|
1067
|
-
sidecar would claim it produced a schema it did not — one predicate, `isRecorded`, read by both.
|
|
1068
|
-
|
|
1069
|
-
One migration is one file, split by a lone `-- down` line. `<id>.down.sql` is a pre-1.2.0
|
|
1070
|
-
hand-written layout and `readMigrations` skips it — read as a migration it sorts next to its own
|
|
1071
|
-
`up` and drops every table the pair exists to reverse.
|
|
1072
|
-
|
|
1073
|
-
## `x dev` boots the app; it does not simulate one
|
|
1074
|
-
|
|
1075
|
-
| File | Job |
|
|
1076
|
-
|---|---|
|
|
1077
|
-
| `api-routes.ts` | the app's API over HTTP: every registered action AND every registered query |
|
|
1078
|
-
| `dev-services.ts` | resolve which service each binding points at — embedded or external |
|
|
1079
|
-
| `dev-queue.ts` | the db + queue pair alone, and the one place that takes every ambient accessor back |
|
|
1080
|
-
| `dev-runtime.ts` | start the rest on top of it and install the remaining accessors (storage, mail, transport) |
|
|
1081
|
-
| `dev-cache.ts` | which cache tiers this process reads through, and the cross-instance invalidation hop |
|
|
1082
|
-
| `dev-purge.ts` | the hourly retention sweep: which framework tables this boot owns, the `purge()` job over them and the `task` that fires it |
|
|
1083
|
-
| `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` |
|
|
1084
|
-
| `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 |
|
|
1085
83
|
| `runtime-overrides.ts` | the one field a host hands the framework a driver through |
|
|
1086
|
-
| `sync-
|
|
1087
|
-
| `
|
|
1088
|
-
| `dev
|
|
1089
|
-
| `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 |
|
|
1090
|
-
| `script-csp.ts` | the `script-src` sha256 of every inline `<script>` it serves — the hydration runtime, from `@ultimat3/render`'s own `HYDRATE_RUNTIME_BODIES` |
|
|
1091
|
-
| `dev-assets.ts` | the image pipeline's only HTTP surface: `/icons/*` and `/media/*` |
|
|
1092
|
-
| `favicon.ts` | `/favicon.ico`: the app's own file, and the bytes the framework answers with when there is none |
|
|
1093
|
-
| `dev-hooks.ts` | the pipeline's `authorize` seam, decided from the app's own `Policy` objects |
|
|
1094
|
-
| `dev-replica.ts` | which boot gets a standby, and the one middleware frame that opens the read scope |
|
|
1095
|
-
| `dev-replicator.ts` | the `replicator` role: the feed selected, locked and pumped — and `replicatedRelations()`, the entity TABLES it filters on |
|
|
1096
|
-
| `dev-roles.ts` | `--role` selection plus start/stop for `web`, `sync`, `worker`, `scheduler` |
|
|
1097
|
-
| `dev-dashboard.ts` | the `DevSources` hooks only this process can answer, and the two CLI panels |
|
|
1098
|
-
| `dev-traces.ts` | core's spans → the `/_x` timeline's request traces |
|
|
1099
|
-
| `dev-n-plus-one.ts` | statement shapes counted per request, and the ones that repeat past the threshold |
|
|
1100
|
-
| `statement-loop.ts` | one verdict → the finding, the panel fact, the overlay notice and the log line |
|
|
1101
|
-
| `dev-policy.ts` | which actors to ask about, and which capability each policy gates |
|
|
1102
|
-
| `cmd-dev.ts` | boot order, mounting `/_x`, installing the span exporter, the file watcher |
|
|
1103
|
-
| `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 |
|
|
1104
|
-
| `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 |
|
|
1105
|
-
| `dev-reload.ts` | one rebuild at a time — a tick arriving mid-build coalesces into ONE trailing rebuild, for the newest file |
|
|
1106
|
-
| `gitignore.ts` | what git ignores, as data: parse, the ancestor chain up to the repository, and last-match-wins |
|
|
1107
|
-
| `path-segments.ts` | `hasPathSegment` — a directory of that exact name, never a substring of one |
|
|
1108
|
-
| `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 |
|
|
1109
|
-
| `mcp-host.ts` | the `DevCapabilities` half of `@ultimat3/mcp`'s `DevHost` — db, tests, logs, verify |
|
|
1110
|
-
| `mcp-db-target.ts` | which database the host is pointed at: whether it is a branch, and whether it is production |
|
|
1111
|
-
| `mcp-errors.ts` | `errors.explain`: one runnable command per code, typed over `CliErrorCode` |
|
|
1112
|
-
| `error-catalog.ts` | imports every `@ultimat3/*` package so `x errors` answers for codes no command loads |
|
|
1113
|
-
| `mcp-test-output.ts` | reading `bun test`'s own summary back into a `TestRun` |
|
|
1114
|
-
| `cmd-mcp.ts` | `x mcp serve`: the two transports, and the local developer's caller |
|
|
1115
|
-
|
|
1116
|
-
**The watch set is a REGISTRATION decision, not a filter, `As of 2026-09-06`.** `watch(root, {
|
|
1117
|
-
recursive: true })` takes one inotify descriptor per directory in the tree — measured on `x dev`
|
|
1118
|
-
against `examples/dummy`: **110 descriptors, 39 of them (35%) under `.x/` and `node_modules/`**, and
|
|
1119
|
-
on a monorepo root **1901, of which 1490 (78%) were `.git/` and `node_modules/`**, where one
|
|
1120
|
-
`git status` delivered 5 `.git/index` events into the JS callback. `isIgnoredPath` answered
|
|
1121
|
-
correctly every time and answered too late: the kernel queue entry, the callback and the slot out of
|
|
1122
|
-
`max_user_watches` (8192 on many distributions) were already spent. Bun 1.4.0's `fs.watch` has no
|
|
1123
|
-
ignore option, so `dev-watch-tree.ts` walks the root itself and registers one non-recursive watcher
|
|
1124
|
-
per admitted directory, pruning at descent.
|
|
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 |
|
|
1125
87
|
|
|
1126
|
-
|
|
1127
|
-
in both directions. Nothing read the ignore file, so `touch tsconfig.tsbuildinfo` — the file every
|
|
1128
|
-
`bun run typecheck` rewrites, named by `x new`'s own scaffolded `.gitignore` — logged
|
|
1129
|
-
`reloaded tsconfig.tsbuildinfo in 113ms`, a full `appManifest()` plus `buildIslands()`; on the
|
|
1130
|
-
framework root 54 git-ignored directories were unfiltered. And `dist` and `coverage` were matched at
|
|
1131
|
-
ANY depth, so an app's own `/dist` or `/coverage` ROUTE never reloaded — silently, which the file's
|
|
1132
|
-
own header calls worse than a spurious rebuild. `gitignore.ts` reads it with git's own anchoring
|
|
1133
|
-
(unanchored at any depth, a leading `/` or an inner slash where it is written, `!` re-including,
|
|
1134
|
-
a trailing `/` directory-only) and walks ANCESTOR ignore files up to the directory holding `.git` —
|
|
1135
|
-
`examples/dummy` carries none of its own and every rule about it lives in the repository root's. The
|
|
1136
|
-
file is re-read, and the whole watch set re-walked, on any write naming `.gitignore`; nothing ever
|
|
1137
|
-
spawns `git check-ignore`. `x new`'s scaffold writes `/dist/` and `/coverage/` **root-anchored** for
|
|
1138
|
-
this reason, plus `packages/*/dist/`. It is `fix-path.ts`'s parser, lifted: two readers of one file
|
|
1139
|
-
are two answers to what an app committed.
|
|
1140
|
-
|
|
1141
|
-
**A watcher event carries no filename when the WATCHED directory itself moves.** Bun's recursive
|
|
1142
|
-
watcher delivers `undefined` — not `null` — on `mv myapp myapp2`, a re-clone or a volume remount,
|
|
1143
|
-
and `isIgnoredPath(undefined)` threw a `TypeError` inside an fs callback, outside any `try`, with no
|
|
1144
|
-
`uncaughtException` handler: `x dev` died with a stack trace. The listener is total over
|
|
1145
|
-
`string | Buffer | null | undefined` and logs `dev.watch.unnamed_event` once.
|
|
1146
|
-
|
|
1147
|
-
**The reload has an in-flight guard, and it is the state that needed it.** A 45ms drip — a slow
|
|
1148
|
-
`git checkout`, a formatter walking files, `x db gen` — measured **40 reloads for 40 files**, each
|
|
1149
|
-
launching `Promise.all([appManifest, buildIslands])` while the previous still ran and assigning
|
|
1150
|
-
`state.manifest` / `state.islands` in COMPLETION order, so a slower earlier tick could land on top
|
|
1151
|
-
of a newer one. `coalesceReloads` (`dev-reload.ts`) keeps the LAST tick that arrived during a
|
|
1152
|
-
rebuild and starts exactly one more.
|
|
1153
|
-
|
|
1154
|
-
`api-routes.ts` is the app's own API surface, composed **once** and mounted by both `cmd-dev.ts`
|
|
1155
|
-
and `serve.ts`: `listActions().map(toRoute)` from `@ultimat3/action` plus
|
|
1156
|
-
`listQueries().map(toQueryRoute)` from `@ultimat3/query`. Two lists is how `query.client()`
|
|
1157
|
-
shipped deriving `/_x/query/<kebab>` against a route neither file mounted — a typed read that
|
|
1158
|
-
compiled everywhere and 404'd everywhere — and a surface that answers in `x dev` and not in the
|
|
1159
|
-
container is the same failure one release later. It reads the registries at call time, never at
|
|
1160
|
-
import: importing the app IS the registration, and it happens after this module loads.
|
|
1161
|
-
|
|
1162
|
-
`startWeb` warns when the route table declares `auth: 'required'` and the app configured no
|
|
1163
|
-
authenticator: `hooks.authenticate` is the only place an actor can come from, so such a process
|
|
1164
|
-
boots clean, reports healthy, and refuses every valid session. A warning and not a throw, because
|
|
1165
|
-
`x new` scaffolds guarded routes before it scaffolds an authenticator.
|
|
1166
|
-
|
|
1167
|
-
The roles live in `@ultimat3/core` (`ROLES`, `isRole`), never in a second list here. A dev-only
|
|
1168
|
-
driver, a dev-only authorizer or a dev-only queue is the bug this design exists to prevent — the
|
|
1169
|
-
only thing dev changes is which driver is behind an interface.
|
|
1170
|
-
|
|
1171
|
-
### `HOST` is the interface, read the way `PORT` is
|
|
1172
|
-
|
|
1173
|
-
`serve.ts`'s `hostnameFromEnv` — `HOST`, trimmed, empty is `0.0.0.0` — and `ServeOptions.hostname`
|
|
1174
|
-
overrides it as `port` overrides `PORT`; `containerBinding(env, hostname)` is the one `WebBinding`
|
|
1175
|
-
`serveApp` hands `startRoles`, so `web`, `sync` and the metrics endpoint bind the same interface.
|
|
1176
|
-
Before 2026-09-07 `CONTAINER_BINDING` was the only production binding and an app whose auth mode
|
|
1177
|
-
admits one implicit actor without a login — which must refuse a public interface — could not run in
|
|
1178
|
-
a container at all. A loopback bind in a container is unreachable through `-p`; the wiki row says
|
|
1179
|
-
where it IS reachable. Not `HOSTNAME`: Docker sets that to the container id.
|
|
1180
|
-
|
|
1181
|
-
### `RuntimeOverrides` is the only way to hand the framework a driver
|
|
1182
|
-
|
|
1183
|
-
`ServeOptions` was `{ root, env, role?, port?, metricsPort? }`, so the ONLY way an app could
|
|
1184
|
-
install a driver was an ambient setter at module scope — and `loadApp` imports the app's modules
|
|
1185
|
-
*after* `startServices` has captured its own. The slot moved and the capture did not: every
|
|
1186
|
-
`handle.enqueue()` went to the app's queue while the worker claimed from Postgres, and `/_x` read
|
|
1187
|
-
the ambient one, so the dashboard agreed with the enqueue side and disagreed with reality.
|
|
1188
|
-
|
|
1189
|
-
Every field REPLACES the env-selected default rather than sitting beside it — `overrides?.x ?? <the
|
|
1190
|
-
env switch>`, one expression, one answer (axiom 1). A field nothing consumes is not there: the
|
|
1191
|
-
entity `Driver` in particular, because `@ultimat3/entity` exposes no installer for one
|
|
1192
|
-
(`database(entities, { driver })` is the app's own call), and a slot the boot cannot honour is the
|
|
1193
|
-
class of defect this seam exists to end.
|
|
1194
|
-
|
|
1195
|
-
**The split is refused, not reconciled.** `assertOneJobDriver` runs first in `startRoles` and
|
|
1196
|
-
throws `X_RUNTIME_DRIVER_SPLIT` when `jobDriver()` is not the object this process serves. Reading
|
|
1197
|
-
through the accessor instead would make the split invisible rather than impossible — and the app
|
|
1198
|
-
would still have installed a driver the boot never saw, with no outbox store bound to it and no
|
|
1199
|
-
relay draining it.
|
|
1200
|
-
|
|
1201
|
-
### What the boot now calls that nothing called before
|
|
1202
|
-
|
|
1203
|
-
| Mechanism | Where | Was |
|
|
1204
|
-
|---|---|---|
|
|
1205
|
-
| the transactional outbox | `dev-queue.ts` installs the store + facade, `worker` runs the relay | staged rows nothing published |
|
|
1206
|
-
| the durable scheduler | `pgSchedulerState` + `createPgLeaseLeader` in `startRoles` | a watermark forgotten on restart, and every replica its own leader |
|
|
1207
|
-
| the Postgres event bus | `dev-queue.ts` | `step.waitForEvent` forgot every correlation on restart |
|
|
1208
|
-
| the shared idempotency store | `dev-queue.ts` | a retry on another replica charged the card twice |
|
|
1209
|
-
| the shared auth limiter | `configureAuthLimiters` in `startServices` | account lockouts counted per POD, so N replicas granted `maxAttempts × N` guesses |
|
|
1210
|
-
| 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 |
|
|
1211
|
-
| the cache tiers | `dev-cache.ts` | only the CDN tier was registered; memo, LRU and Redis had zero callers |
|
|
1212
|
-
| WebSocket authentication | `dev-sync.ts` | `actorId: null` on every socket — realtime was single-tenant by wiring |
|
|
1213
|
-
| OTLP export | `otlp-export.ts` | the chart set the variable and no code read it |
|
|
1214
|
-
|
|
1215
|
-
`createPgLeaseLeader`, never `createPgLeader`: the latter's `pg_try_advisory_lock` is
|
|
1216
|
-
session-scoped and the grant dies when the connection returns to the pool, so every node reads
|
|
1217
|
-
itself as leader and a rolling update double-fires every task.
|
|
1218
|
-
|
|
1219
|
-
The relay runs on `worker` and only `worker` — the role that exists wherever jobs run at all.
|
|
1220
|
-
Duplicating it is safe — the claim is a **lease** taken in the statement that locks the row
|
|
1221
|
-
(`@ultimat3/jobs`' `outbox-pg.ts`, fenced on `claimed_by`), so two relays never hold one batch —
|
|
1222
|
-
but pointless. The idempotency key is not the reason and never was: its conflict target is a
|
|
1223
|
-
partial index over live states, so it collapses a repeat only while the first job is still live.
|
|
1224
|
-
|
|
1225
|
-
`SQL_IDEMPOTENCY_TABLE` is applied beside `SQL_JOBS_TABLE`, and the store is installed by the boot
|
|
1226
|
-
rather than by the app, even though `@ultimat3/action` documents
|
|
1227
|
-
`postgresIdempotencyStore({ executor: Bun.sql })`: **`Bun.sql` has no `.query(text, values)`** — it
|
|
1228
|
-
is a tagged template whose positional form is `unsafe` — so that line does not satisfy `PgExecutor`,
|
|
1229
|
-
and a second executor would open a second pool against a URL this boot already resolved. The app
|
|
1230
|
-
owes only the declaration, `configureIdempotency({ scope: 'shared' })`, which `x new` names in
|
|
1231
|
-
`apps/web/server.ts`.
|
|
1232
|
-
|
|
1233
|
-
The per-TENANT subscription cap is deliberately unset, and **both halves of it are**:
|
|
1234
|
-
`assertCapacity` returns early unless `maxPerTenant` AND `tenantOf` are given, so passing one arms
|
|
1235
|
-
nothing — and no default is defensible when one tenant is a person and the next is five thousand
|
|
1236
|
-
seats. The per-socket 128 stands because a socket is one browser tab.
|
|
1237
|
-
|
|
1238
|
-
**The change feed is filtered by TABLE, never by entity name**, `As of 2026-08-26`.
|
|
1239
|
-
`replicatedRelations()` (`dev-replicator.ts`) is the one projection, and both of its readers are
|
|
1240
|
-
catalog readers: `PgReplicationStream` keeps a change only when `#entities.has(relation.name)` and a
|
|
1241
|
-
pgoutput Relation message names the table, while `warnPartialIdentity` matches the same list against
|
|
1242
|
-
`pg_class.relname`. An entity NAME is the framework's own registry key — a cache tag, a policy and
|
|
1243
|
-
`x entities describe` are all keyed by it — and `entity('user', { table: 'users' })` makes the two
|
|
1244
|
-
different strings. It passed `.name`, so a renamed table matched on neither side: **every change
|
|
1245
|
-
skipped** and a replica-identity warning that could never fire, with no error anywhere. Invisible to
|
|
1246
|
-
every fixture in the tree, because `table` defaults to the name verbatim and all six entities in
|
|
1247
|
-
`examples/dummy` have `name === table` — `dev-replicator.test.ts` uses `billingAccount` on
|
|
1248
|
-
`billing_accounts` for exactly that reason, and proves the value through the real call chain:
|
|
1249
|
-
`assertIdentifier` refuses `billingAccount` before any connection and accepts `billing_accounts`.
|
|
1250
|
-
|
|
1251
|
-
`trustProxy` is read from `TRUSTED_PROXY_HOPS` in `startWeb`, the way `PORT` and `ROLE` are read: it
|
|
1252
|
-
is a fact about the deployment, not an app config choice, and one image runs behind an ingress in
|
|
1253
|
-
one cluster and behind nothing on a laptop. Without it `ctx.ip` is the ingress's socket address on
|
|
1254
|
-
every request, so the limiter keys the whole fleet's anonymous traffic into one bucket.
|
|
1255
|
-
|
|
1256
|
-
### The document carries the page's client scope — on private documents only
|
|
1257
|
-
|
|
1258
|
-
`dev-render.ts` puts `@ultimat3/render`'s `clientScopeTag(clientScopeOf(ctx.actor))` (`@ultimat3/auth`) in the head of a
|
|
1259
|
-
gated `ssr` page and every `stream` — the documents whose own headers say `private`
|
|
1260
|
-
(`documentCarriesScope`). A shareable document carries none, because a CDN would hand one visitor's
|
|
1261
|
-
scope to the next. `dev-render-scope.test.ts` drives `static`, `isr`, `ssr` (gated and not) and `stream` over the real pipeline. There is
|
|
1262
|
-
no island bootstrap: the page client is created lazily by the first transport call or realtime hook
|
|
1263
|
-
(plan 101, decided 2026-09-22; `island-bundle.ts`'s header records what an eager one cost).
|
|
1264
|
-
|
|
1265
|
-
### The page's sync target is the framework's, and so is the worker that hosts the socket
|
|
1266
|
-
|
|
1267
|
-
`page-sync.ts` is ONE call `cmd-dev.ts` and `serve.ts` both make (plan 101 slice 11, `As of
|
|
1268
|
-
2026-09-22`), so the two boots cannot serve different targets:
|
|
88
|
+
### Build and data
|
|
1269
89
|
|
|
1270
90
|
| File | Job |
|
|
1271
91
|
|---|---|
|
|
1272
|
-
| `
|
|
1273
|
-
| `
|
|
1274
|
-
| `
|
|
1275
|
-
| `
|
|
1276
|
-
| `dev-render.ts` | `DocumentOptions.sync` → render's `clientSyncTags`: `ultimate-sync`, `ultimate-build`, `ultimate-sync-worker` on EVERY document (principal-free, unlike the scope tag). The page boot's `<script defer>` (render's `clientBootTags`) is rendered after the body, only when the document carries the SCOPE tag (restoring and replaying are per principal) AND an island this render emitted reaches `@ultimat3/realtime` (`island-realtime.ts` records the answer every island build computes; a page whose islands never touch a record — `examples/dummy`'s settings — pays nothing). `dev-render-scope.test.ts`. Both framework scripts resolve realtime from the root, then from `apps/*` — a workspace app's realtime is `apps/web`'s dependency, and resolving from the root alone silently built no worker and no boot for `examples/dummy`. The static export carries none — it has no sync node |
|
|
1277
|
-
|
|
1278
|
-
### Realtime is installed FOR the author, only where it is used
|
|
1279
|
-
|
|
1280
|
-
`island-realtime.ts` (plan 101 slice 14, `As of 2026-09-22`): an island whose own import graph
|
|
1281
|
-
reaches `@ultimat3/realtime` — walked with `live-routes.ts`'s `firstInGraph`, relative specifiers
|
|
1282
|
-
only, the transpiler reading re-exports and erasing `import type` — is built from a virtual entry
|
|
1283
|
-
that calls `installRealtime({ signal: createSignal })` with that bundle's solid-js before the
|
|
1284
|
-
island's module body runs. Every other island is built from its own file, byte for byte. +103 B on
|
|
1285
|
-
a Solid island reading `useConnection`, +0 elsewhere. A PACKAGE importing realtime for an island is
|
|
1286
|
-
the walk's blind spot, and a hook there throws `X_REALTIME_UNINSTALLED` by name.
|
|
1287
|
-
|
|
1288
|
-
### `x verify` runs the static steps beside the serial suites
|
|
1289
|
-
|
|
1290
|
-
`verify-run.ts`'s `BESIDE_SERIAL_SUITES` — `lint`, `boundaries`, `filesize`, `package-shape`,
|
|
1291
|
-
`errors` — start when `live` does and are joined before the first step after `eval` (DX ledger #14).
|
|
1292
|
-
They read the tree and write nothing a later step reads; `typecheck` stays first and alone, `unit`
|
|
1293
|
-
alone. The table, `--json` and every step name keep the declared order; `data.durationMs` is WALL
|
|
1294
|
-
time now, not the sum of step times. Measured locally on the framework root, 12 cores, one run
|
|
1295
|
-
each, `As of 2026-09-22`: **395 s → 270 s** wall. `verify-run-overlap.test.ts` pins both halves.
|
|
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` |
|
|
1296
96
|
|
|
1297
|
-
###
|
|
1298
|
-
|
|
1299
|
-
`live-routes.ts`, `As of 2026-09-22`: the rule walks each route's SERVER graph (the page's relative
|
|
1300
|
-
imports — an island is a `src` string, never an import) and reports every module there that reads
|
|
1301
|
-
`useQuery`, `useConnection`, `useMutation`, `useMutationQueue`, `useRecord`, `useChannel` **or
|
|
1302
|
-
`hasPageSocket`**, unless some island's own graph imports that module too. Once per module, however
|
|
1303
|
-
many routes reach it. `hasPageSocket()` was an EXEMPTION until this date, on the argument that a
|
|
1304
|
-
module asking had written its fallback; on the server it answers false every time, so a guarded
|
|
1305
|
-
module no browser runs renders nothing forever — `examples/dummy`'s update banner, in a layout no
|
|
1306
|
-
island imported, never showed "A new version is ready.". The fix line starts "move it into an
|
|
1307
|
-
island".
|
|
1308
|
-
|
|
1309
|
-
### A registered code nothing throws must say so
|
|
1310
|
-
|
|
1311
|
-
`error-unthrown.ts`'s `checkErrorCodesThrown(root, page)` (DX ledger #9, `X_ERROR_CODE_UNTHROWN`):
|
|
1312
|
-
a code a package registry names, used nowhere else in shipped `packages/*/src` — not at a `code:`,
|
|
1313
|
-
not as a fallback literal, not through a table member something reads — is reported unless its
|
|
1314
|
-
reference row says `thrown by nothing` / `not thrown` or sits under `## Reserved codes`. The fix is
|
|
1315
|
-
the row, never the registration: a shipped code is stable forever. A host check, like the other
|
|
1316
|
-
reference-page rules — in a generated app every framework code would read as unthrown. Its limit:
|
|
1317
|
-
an exported class holding `code: 'X_…'` counts as a thrower whether or not anything constructs it,
|
|
1318
|
-
which is why `X_RPC_FAILED`'s `RpcFailedError` is not reported.
|
|
1319
|
-
|
|
1320
|
-
### `island-bundle.ts` is the bundler half of `hydrate`
|
|
1321
|
-
|
|
1322
|
-
`@ultimat3/render` shipped `island()`, the collector, `emitIslandAttributes`, `hydrateRuntime`,
|
|
1323
|
-
`RouteEntry.islands` and `routeJsBytes` — and **nothing constructed or populated any of them**.
|
|
1324
|
-
`hydrate` was a documented capability with no implementation, to the point that `render-static.ts`
|
|
1325
|
-
told authors to "move the request-dependent part into an island", naming a mechanism the framework
|
|
1326
|
-
could not express. This package is the half that can see a file on disk, so it is the half that was
|
|
1327
|
-
missing.
|
|
1328
|
-
|
|
1329
|
-
| File | Job |
|
|
1330
|
-
|---|---|
|
|
1331
|
-
| `island-bundle.ts` | discover `*.island.tsx`, build each as its own entry point, hash it, resolve a page's specifier to its URL |
|
|
1332
|
-
| `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 |
|
|
1333
|
-
| `island-routes.ts` | serve those chunks, at `ISLAND_BASE_PATH`, immutable |
|
|
1334
|
-
| `dev-render.ts` | one collector **per render**, and `hydrateRuntime` after the body |
|
|
1335
|
-
| `prerender.ts` | build first, write the chunks into the export, then measure |
|
|
1336
|
-
| `budgets.ts` | `measureDocumentJs` weighs `data-x-entry` as well as `<script src>` |
|
|
1337
|
-
|
|
1338
|
-
**A stats row is keyed by the route's DECLARED path, and holds its heaviest page**
|
|
1339
|
-
(`As of 2026-08-23`). `checkBudgets` looks a route up by `route.url` off the manifest, which is the
|
|
1340
|
-
pattern (`/blog/:slug`), and `prerenderSite` pushed `artifact.path` — the filled one
|
|
1341
|
-
(`/blog/hello`). So no dynamic static route had ever been weighed: each was `X_BUDGET_UNMEASURED`
|
|
1342
|
-
and `X_BUDGET_EXCEEDED` could not fire for the whole class. The heaviest page and not the first,
|
|
1343
|
-
because a budget is a ceiling and the page that breaks it is the one the route answers for; the
|
|
1344
|
-
report's `emitted` list still names every filled path.
|
|
1345
|
-
|
|
1346
|
-
**Both halves of the CSP are computed at boot, `As of 2026-08-23`.** `style-csp.ts` was alone, and
|
|
1347
|
-
the hydration runtime is emitted INLINE in every document carrying an island — so with
|
|
1348
|
-
`script-src 'self' 'wasm-unsafe-eval'` no island booted in any container. `x dev` sends the policy
|
|
1349
|
-
report-only (`dev: true`), which is exactly why nobody saw it: the page hydrated on a laptop and
|
|
1350
|
-
never in the image. `startWeb` extends both directives; the property is pinned end to end by
|
|
1351
|
-
`dev-roles-script-csp.test.ts`, which parses a served document, hashes every executable inline
|
|
1352
|
-
script in it and asserts the response's own `script-src` names each one — with `dev: false`, the
|
|
1353
|
-
only mode in which the policy can block anything.
|
|
1354
|
-
|
|
1355
|
-
**One `Bun.build` per island, never one call with N entry points**, and `splitting: false`. The
|
|
1356
|
-
island's `src` is a string, so no import edge reaches it and the page's graph stays the page's
|
|
1357
|
-
(axiom 6) — a shared chunk would put that number back behind a graph walk, and the budget compares
|
|
1358
|
-
against bytes. Two islands that both import the same helper each carry a copy; that is the honest
|
|
1359
|
-
number for what booting either one costs.
|
|
1360
|
-
|
|
1361
|
-
**The chunk URL is SOURCE-addressed, not byte-addressed, `As of 2026-09-06`.** `graphHash` hashes
|
|
1362
|
-
the build's inputs — the source map's `sourcesContent`, sorted, plus the entry's app-relative path,
|
|
1363
|
-
the framework version and `Bun.version` — with render's own `contentHash`, the function that already
|
|
1364
|
-
stamps an ETag and a precache revision. Hashing the OUTPUT is what it did, and `Bun.build` is not
|
|
1365
|
-
byte-deterministic under `minify`: measured on 1.4.0, one entry point, no source file touched, a
|
|
1366
|
-
131,589-byte island alternated between two outputs of identical length differing only in minified
|
|
1367
|
-
identifier names, roughly one build in ten. The URL then flapped — ten names in ten minutes in
|
|
1368
|
-
ai-maxxing — so the service worker precached a chunk that 404ed and the `immutable` cache never hit.
|
|
1369
|
-
The cost of the alternative was measured and refused: `minify: { identifiers: false }` IS
|
|
1370
|
-
deterministic and is +47% raw, +20% gzipped, on every island of every app. `sourcemap: 'external'`
|
|
1371
|
-
pays for the input list (277ms against 276ms) and its `//# debugId=` line is stripped, so the
|
|
1372
|
-
shipped bytes are unchanged. Two processes can therefore serve two byte strings at one URL — the
|
|
1373
|
-
same program under different local names — and a per-file cache keeps ONE process serving one.
|
|
1374
|
-
Delete `graphHash` the day `Bun.build` is deterministic.
|
|
1375
|
-
|
|
1376
|
-
**`x dev`, the container and the static export all mount the same table.** `serve.ts` builds the
|
|
1377
|
-
islands at boot for the same reason it mounts `apiRoutes()`: a seam that works in dev and not in the
|
|
1378
|
-
image is the same failure one release later. `x dev` rebuilds them on the watcher tick — an island
|
|
1379
|
-
is the single module this process never imports, so there is no Bun module cache to invalidate —
|
|
1380
|
-
and the SAME tick re-imports the route module beside them when its source changed
|
|
1381
|
-
(`app-load.ts`'s `reloadRoute`: `<path>?x-reload=<hash>` is the one cache key Bun honours,
|
|
1382
|
-
`registerRoute` replaces the entry for the same file, and `dev-render.ts` reads the entry back from
|
|
1383
|
-
the table on every request rather than closing over the one it was built from). Until 2026-09-07
|
|
1384
|
-
the island was the only reload that took effect, so a save served a new island under an old page —
|
|
1385
|
-
the old props, the placeholder the new island renders when they are missing. A route module and
|
|
1386
|
-
nothing else: an action, a query or an entity is held by every module that imported it, and no
|
|
1387
|
-
re-import can rebind those. `@ultimat3/render`'s loader admits the query
|
|
1388
|
-
(`/\.tsx(?:\?[^/]*)?$/`) and strips it before reading the file — anchored on `.tsx$`, the
|
|
1389
|
-
re-import fell through to Bun's own JSX loader and every reloaded page died on `__xh`.
|
|
1390
|
-
|
|
1391
|
-
**The dev fixture is its own repository** (`.git/HEAD` in `DEV_FIXTURE_FILES`). The framework's root
|
|
1392
|
-
`.gitignore` lists `packages/cli/.dev-fixture/`, `devIgnore` honours every ancestor up to a `.git`,
|
|
1393
|
-
and so the watcher admitted the fixture root and nothing under it: every run booted the reload path
|
|
1394
|
-
and none exercised it. The marker is what lets `cmd-dev.test.ts` save a page and await the tick.
|
|
1395
|
-
|
|
1396
|
-
**`app-load.ts` skips `*.island.tsx` deliberately.** It registers no primitive, and importing it
|
|
1397
|
-
would put the one module guaranteed to be outside the server's graph inside this process's, where a
|
|
1398
|
-
top-level `document` reference takes the whole scan down.
|
|
1399
|
-
|
|
1400
|
-
**The budget is charged from the emitted document, and it names the island.** An island's chunk is
|
|
1401
|
-
reached by `import()` from inside the hydration runtime, so it is never a `<script src>` — weighing
|
|
1402
|
-
script tags alone charged a page for the runtime and never for the code that runtime boots.
|
|
1403
|
-
`measureDocumentJs` reads `data-x-entry` as what it is, dedupes it (two instances of one island are
|
|
1404
|
-
one module), and `prerenderSite` maps the heaviest URL back through the bundle so
|
|
1405
|
-
`X_BUDGET_EXCEEDED` names `apps/web/site/pricing/calculator.island.tsx` and not a hash.
|
|
1406
|
-
|
|
1407
|
-
`X_ISLAND_INVALID` is **borrowed** from `@ultimat3/render`, not twinned: "this src cannot become a
|
|
1408
|
-
client entry" is what that code already means, and the bundler is simply the half that can see
|
|
1409
|
-
whether the file exists. A failed compile is `X_BUILD_FAILED` — an island is a bundle entry point
|
|
1410
|
-
like any other, and `Bun.build` *rejects* rather than answering `success: false`, so the catch is
|
|
1411
|
-
the real path.
|
|
1412
|
-
|
|
1413
|
-
### `dev-assets.ts` is where the image pipeline meets HTTP
|
|
1414
|
-
|
|
1415
|
-
Three packages declare what an image is and none of them serves one: `@ultimat3/seo` says what a
|
|
1416
|
-
variant URL means (`parseImageQuery`) and produces the bytes (`builtinImageDriver`),
|
|
1417
|
-
`@ultimat3/storage` says what a variant is called and where it is cached (`variantKey`), and
|
|
1418
|
-
`@ultimat3/pwa` says which icons a web manifest promises (`planIcons`, `BuiltinImagePipeline`).
|
|
1419
|
-
Pixels are `@ultimat3/core`'s pipeline, only ever. This file picks two base paths — `ICON_BASE_PATH`
|
|
1420
|
-
and `MEDIA_BASE_PATH` — and decides nothing else; a resize, a format table or a second cache key
|
|
1421
|
-
here is the drift the split exists to prevent.
|
|
1422
|
-
|
|
1423
|
-
**`/media` and `/_storage` are one authz decision, not two.** Both serve objects off the app's only
|
|
1424
|
-
disk, so `/media/*key` declares what `dev-storage.ts` declares — `auth: 'required'` +
|
|
1425
|
-
`STORAGE_READ_PERMISSION` + `enforcedBy: 'handler'` — and calls the same two functions, in the same
|
|
1426
|
-
order: `authorizeStorageRead` then `assertReadableKey`. It shipped `auth: 'public'` with no policy
|
|
1427
|
-
and no tenant check while its twin required both, which made every tenant's uploads one URL away in
|
|
1428
|
-
production (`serve.ts` mounts it), and `?w=` made it an unauthenticated `put` besides. The tenant
|
|
1429
|
-
test lives in ONE function both routes call, and `storage-surfaces.test.ts` pins the pair against
|
|
1430
|
-
each other — every case names the verdict absolutely as well as comparing the two, because equality
|
|
1431
|
-
alone is satisfied by both surfaces failing open together. Cacheability follows the key, not the
|
|
1432
|
-
route: a tenant-scoped key takes `AUTHORIZED_OBJECT_CACHE` (`private, max-age=0`, varying on
|
|
1433
|
-
`authorization`/`cookie`), and only a key no tenant owns keeps `immutable`. A genuinely public image
|
|
1434
|
-
belongs under `apps/web/site/`, which is a static asset and never touches that disk.
|
|
1435
|
-
|
|
1436
|
-
**A variant is CACHED only at a width the framework can mint.** The cache key is built entirely
|
|
1437
|
-
from caller-supplied query values, so `?w=1`, `?w=2`, … each wrote a new object to the app's only
|
|
1438
|
-
disk, on a route every signed-in tenant may reach for their own keys. `@ultimat3/seo`'s
|
|
1439
|
-
`MAX_IMAGE_WIDTH` (8192) bounds that and does not close it. `isMintableWidth` is the bound:
|
|
1440
|
-
`DEFAULT_WIDTHS` **plus the source's own intrinsic width**, which is exactly the set `usableWidths`
|
|
1441
|
-
puts in a `srcset` — the constant alone would refuse the widest entry of any image whose intrinsic
|
|
1442
|
-
width is not one of the eight. Anything outside it is still served; only the `put` is refused, so
|
|
1443
|
-
no caller gains a new 4xx. `?q=` is deliberately still unbounded here — the closed set for quality
|
|
1444
|
-
is `@ultimat3/seo`'s to declare, not this file's.
|
|
1445
|
-
|
|
1446
|
-
**`/favicon.ico` is a mechanism, not a scaffolded file.** Every browser requests it unprompted, the
|
|
1447
|
-
scaffold wrote none and neither served surface mounted a route, so a permanent 404 sat in the console
|
|
1448
|
-
of every app the framework produces — noise that trains the reader to ignore console errors, which is
|
|
1449
|
-
the opposite of what `--json` and an executable `fix:` are for (#272). Two rungs and one path: the
|
|
1450
|
-
app's own `apps/web/site/favicon.ico` wins, and `favicon.ts` answers a 32x32 PNG encoded through
|
|
1451
|
-
`@ultimat3/core`'s own pipeline when there is none — the same encoder `x new`'s icon goes through, so
|
|
1452
|
-
there is no second image format in the tree and no base64 blob nobody can verify. It is deliberately
|
|
1453
|
-
NOT derived from `ICON_SOURCE`: resizing the install icon needs `@ultimat3/pwa`'s pipeline and would
|
|
1454
|
-
make the answer depend on a file that may be absent, which is a third rung under a mechanism that has
|
|
1455
|
-
exactly two. The file is read per REQUEST, so dropping one into a running `x dev` takes effect
|
|
1456
|
-
without a restart. It mounts through `assetRoutes`, which is the one route set `serve.ts` and
|
|
1457
|
-
`cmd-dev.ts` both compose — a favicon added to one of them alone is a 404 that comes back in
|
|
1458
|
-
production only — and `prerenderSite` writes the same bytes into the static export, because an
|
|
1459
|
-
artifact served with no process behind it has to carry every byte the browser will ask for.
|
|
1460
|
-
|
|
1461
|
-
`ICON_SOURCE` lives here, not in `cmd-doctor.ts`, because this is the module that reads it: the
|
|
1462
|
-
diagnostic checks what `x dev` serves, so one constant cannot pass the check and serve nothing.
|
|
1463
|
-
It is a **PNG** — core decodes PNG and JPEG only, and the SVG this used to name could never
|
|
1464
|
-
become an icon.
|
|
1465
|
-
|
|
1466
|
-
The routes mount whether or not the source icon exists, and a missing one is refused with
|
|
1467
|
-
`X_PWA_ICON_MISSING` and its fix — a route that silently disappears is a 404 whose meaning an agent
|
|
1468
|
-
has to guess. Deliberately **not** also a boot finding: `x doctor` already reports this condition,
|
|
1469
|
-
with this code, and two reporters of one condition is the duplication this package's own rule
|
|
1470
|
-
forbids. `x dev` owns the runtime half; the diagnostic owns the other.
|
|
1471
|
-
|
|
1472
|
-
### `hold.ts` is why a long-running command outlives its own result
|
|
1473
|
-
|
|
1474
|
-
`dispatch` renders a `CommandResult` and `bin.ts` exits on the code — so a command whose server is
|
|
1475
|
-
still listening when `run` resolves is a command the exit code takes down, between the line that
|
|
1476
|
-
announced the url and the first request to it. `x dev` and `x mcp serve --transport http` both did.
|
|
1477
|
-
|
|
1478
|
-
The one answer is `CommandResult.hold`: report first, then `dispatch` awaits the hold before the
|
|
1479
|
-
exit code. `holdUntilShutdown` installs core's signal handlers (`installSignalHandlers` — until
|
|
1480
|
-
this it had no callers anywhere, which is why `cmd-mcp.ts`'s `onShutdown` registration was never
|
|
1481
|
-
reached), waits on the **drain's first phase** rather than on a signal list of its own, and
|
|
1482
|
-
releases what core's lifecycle never learned about — the embedded Postgres, the worker, the
|
|
1483
|
-
watcher — *after* the drain, so an in-flight request still has the database it opened against.
|
|
1484
|
-
Ctrl-C is therefore the same three phases production runs, not a kill that leaves `.x/pgdata`
|
|
1485
|
-
locked by a process that no longer exists.
|
|
1486
|
-
|
|
1487
|
-
**The release runs INSIDE the drain's own deadline, `As of 2026-08-23`, and it is the same
|
|
1488
|
-
deadline.** `drain()` ABANDONS a hook that overruns `ShutdownReason.deadlineAt` — the process is
|
|
1489
|
-
meant to exit without it — and `release` re-enters the very same teardown one call later:
|
|
1490
|
-
`app.stop()` → `startRoles().stop()` → `worker.stop()`, memoised in the package that owns it, so
|
|
1491
|
-
awaiting it is awaiting the promise the drain just walked away from. Unbounded, that hung past
|
|
1492
|
-
`terminationGracePeriodSeconds` and the kubelet SIGKILLed a process that had already drained
|
|
1493
|
-
cleanly. The budget is the hook's own `reason.deadlineAt`, not a stopwatch of ours, so there is one
|
|
1494
|
-
number and not two; an overrun is logged as `X_SHUTDOWN_TIMEOUT` and a REJECTION still rejects,
|
|
1495
|
-
because `dispatch` awaits the hold inside its own `try`.
|
|
1496
|
-
|
|
1497
|
-
**`options.exit` has exactly one caller: `runRole` in `serve.ts`.** `bin.ts` ends in
|
|
1498
|
-
`process.exit(code)`, so `x dev` and `x mcp` need nothing; `apps/web/server.ts` — which is what a
|
|
1499
|
-
container runs — has no such line, and one non-unref'd interval anywhere in the app then holds an
|
|
1500
|
-
event loop with nothing left to do. A function rather than a boolean because `process.exit` inside
|
|
1501
|
-
a library is untestable, and the caller is the one that knows.
|
|
1502
|
-
|
|
1503
|
-
Commands: `bun test`, `bunx tsc --noEmit -p tsconfig.json`.
|
|
1504
|
-
|
|
1505
|
-
## A declared flag with no reader is a promise `x help` makes and nothing keeps
|
|
1506
|
-
|
|
1507
|
-
`x deploy --critical` said *"security deploy: forces clients to reload"* and forced nothing: the
|
|
1508
|
-
value is written into the plan JSON (`cmd-deploy.ts`) and **no package reads that field**. The
|
|
1509
|
-
parser accepts every declared flag, so this is neither a parse error nor a type error — the flag
|
|
1510
|
-
worked perfectly and meant nothing, to the operator most likely to be shipping a security patch.
|
|
1511
|
-
|
|
1512
|
-
`flag-reads.ts` is the rule that can see the class of defect: **every flag a command declares is
|
|
1513
|
-
read by something in the CLI's own source**, as `X_CLI_FLAG_UNREAD`. The four global flags are
|
|
1514
|
-
excluded — `--json`, `--help`, `--cwd` and `--verbose` are the parser's, read once for every
|
|
1515
|
-
command, and a per-command rule would report all thirty declarations of `--json`. The read test is
|
|
1516
|
-
deliberately generous: a bare `'name'` literal anywhere outside a `name:`/`short:` spec field
|
|
1517
|
-
counts, so a flag echoed only into `--json`, or read through a shared constant, is read. A gate
|
|
1518
|
-
that guessed at intent would report findings about working commands.
|
|
1519
|
-
|
|
1520
|
-
It is enforced by `flag-reads.test.ts`, in the `unit` step — the same shape `cmd-planned.test.ts`
|
|
1521
|
-
and `error-catalog.test.ts` use for a rule about the CLI's own declarations, and the reason its
|
|
1522
|
-
`fix:` is a `bun test` line rather than an `x` command: the rule can only ever fire in this repo.
|
|
1523
|
-
Promoting it to `x verify`'s `boundaries` host check is one line in `scripts/verify.ts`.
|
|
1524
|
-
|
|
1525
|
-
**It does not catch `--critical`, and that is the honest limit.** The flag IS read —
|
|
1526
|
-
`flagBool(ctx.args, 'critical')` — and what had no consumer was the plan FIELD, one level below any
|
|
1527
|
-
rule over names. Two stronger rules were measured and rejected: "the read must not be a property
|
|
1528
|
-
initializer" reports six flags, five of which work (`x db --allow-destructive`, `x jobs --queue`);
|
|
1529
|
-
"the summary must match the behaviour" is undecidable. So the flag's summary now says what it does,
|
|
1530
|
-
and forcing a reload is **not a thing this framework does**, `As of 2026-08`. `updateSignal`
|
|
1531
|
-
had no runtime caller for four majors and 9.0.0 deleted it rather than wiring it: `pwa` is tier 4
|
|
1532
|
-
and the two runtimes holding both build ids — `http` (2) and `sync` (3) — are below it, so no
|
|
1533
|
-
legal import could ever have reached the function. A deploy command has no channel to a running
|
|
1534
|
-
client regardless; the plan is `docker compose up` or `helm upgrade`. What ships is notification:
|
|
1535
|
-
`useConnection().updateAvailable` from `@ultimat3/realtime`.
|
|
1536
|
-
|
|
1537
|
-
## Planned commands are commands
|
|
1538
|
-
|
|
1539
|
-
Every command in `wiki/CLI-Reference.md`'s planned table is in the registry, built from
|
|
1540
|
-
`PLANNED_COMMANDS` in `cmd-planned.ts`, and exits `X_NOT_IMPLEMENTED` with a `fix:` naming the
|
|
1541
|
-
closest **shipped** command. `X_CLI_UNKNOWN_COMMAND` would say "you typed something that does not
|
|
1542
|
-
exist", which is false and sends an agent hunting a typo. `cmd-planned.test.ts` enforces both
|
|
1543
|
-
halves: every row is reachable through the parser, and no `fix` points at another planned command.
|
|
1544
|
-
|
|
1545
|
-
`PLANNED_SUBCOMMANDS` is the same promise one level down, and `x db studio` is its only entry.
|
|
1546
|
-
A subcommand stays in its command's `subcommands` list — the parser reaches it, `x help db` lists
|
|
1547
|
-
it — and the owning `run` does `throw plannedSubcommand('db', 'studio')`. Dropping it from the list
|
|
1548
|
-
instead would answer `X_CLI_UNKNOWN_SUBCOMMAND`, which is the same lie the table above closes.
|
|
1549
|
-
|
|
1550
|
-
## `guards/` is how an app makes its own convention a build error
|
|
1551
|
-
|
|
1552
|
-
Axiom 3 says a convention that is not a build error does not exist, and until 1.2.0 the framework
|
|
1553
|
-
gave an app no way to create one: `VERIFY_STEP_NAMES` is a closed literal list with no extension
|
|
1554
|
-
point. A file in `guards/` closes it.
|
|
97
|
+
### Introspection and tools
|
|
1555
98
|
|
|
1556
99
|
| File | Job |
|
|
1557
100
|
|---|---|
|
|
1558
|
-
| `
|
|
1559
|
-
| `
|
|
1560
|
-
| `cmd-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
says the shape — *a host adds findings to a step; it can never add, remove, reorder or skip one* —
|
|
1564
|
-
so "green" keeps meaning exactly what it meant, whatever an app writes. `boundaries` is the step
|
|
1565
|
-
whose host slot already carries "rules this repo makes about itself that the framework cannot
|
|
1566
|
-
know" (the monorepo's tier table arrives through it), and it runs third, before any suite, so a
|
|
1567
|
-
convention failure comes back in seconds. `guardFindings` is *typed* as a `HostCheck` and is
|
|
1568
|
-
composed by the step rather than registered as one: the slot is `Partial<Record<VerifyStepName,
|
|
1569
|
-
HostCheck>>`, one function per step, so an app registering there would evict the framework's own
|
|
1570
|
-
tier check — and `verifyCommand.run` passes no `hostChecks` at all, which is why an app-supplied
|
|
1571
|
-
check could not have reached the gate through that field in the first place.
|
|
1572
|
-
|
|
1573
|
-
**Discovered, never registered.** `guards/*.ts`, minus `*.test.ts`, sorted. Nothing imports a
|
|
1574
|
-
guard, nothing lists one, and there is no `defineGuard` to call — a guard that has to announce
|
|
1575
|
-
itself is a guard an app can forget to announce, which is the coupling axiom 8's extension model
|
|
1576
|
-
rejects. A `*.test.ts` beside a guard is its test: importing it would run a suite inside the gate.
|
|
1577
|
-
|
|
1578
|
-
**A guard returns `Finding[]`, so it inherits everything.** `--json`, the step table, the summary
|
|
1579
|
-
counts and the exit code are all projections of what it returns (axiom 2); a guard that printed or
|
|
1580
|
-
chose an exit code would be a second gate. It never throws for a normal result — a throw is
|
|
1581
|
-
`X_GUARD_FAILED`, reported as a finding rather than taking the run down.
|
|
1582
|
-
|
|
1583
|
-
**And what it returns is held to the error contract.** `findingProblem` demands an
|
|
1584
|
-
`X_SCREAMING_SNAKE` code, a non-empty cause, and a `fix:` that passes `fixProblem` — the *same*
|
|
1585
|
-
rule `x verify`'s `errors` step applies to every shipped `fix:` in this repo. It runs on the
|
|
1586
|
-
returned value, which is the half a static scan cannot reach: a `fix` assembled at run time has no
|
|
1587
|
-
literal to read. Three codes, one per way a guard can fail to be one — `X_GUARD_INVALID` (no
|
|
1588
|
-
usable export), `X_GUARD_FAILED` (it threw), `X_GUARD_FINDING_INVALID` (what it returned is not a
|
|
1589
|
-
finding). Anything else about a guard is the app's business: no size ceiling, no budget, no rule
|
|
1590
|
-
about what it may check.
|
|
1591
|
-
|
|
1592
|
-
**The validator may never be the thing that crashes.** `findingProblem` names an offending value
|
|
1593
|
-
through `shown()` and not `JSON.stringify` — which refuses a BigInt — and every candidate is read
|
|
1594
|
-
inside a `try`, because reading one can throw on its own (a getter that raises, a proxy that
|
|
1595
|
-
refuses). A guard returning `[1n]` is `X_GUARD_FINDING_INVALID`, per candidate, so one unreadable
|
|
1596
|
-
entry costs its own line and not the real findings beside it. The mechanism whose job is producing
|
|
1597
|
-
structured failures handing back a stack trace is the one outcome it exists to prevent.
|
|
1598
|
-
|
|
1599
|
-
**`x new` ships NINE guards, `As of 2026-09-08`** — the four below plus five interface rules
|
|
1600
|
-
(`semantic-interactive`, `focus-visible`, `image-dimensions`, `animated-layout-property`,
|
|
1601
|
-
`island-without-states`), each statically decidable and each carrying a legitimate-lookalike test
|
|
1602
|
-
that must NOT be reported. **Two of the original four were broken the whole time and nobody could
|
|
1603
|
-
see it**, because no guard had ever run against a real app: `raw-colour` reported all 87 uses of
|
|
1604
|
-
`rgb(var(--color-…))` — which IS the token form `_colors.scss` emits, so its own cause line ("a
|
|
1605
|
-
value no theme can restate") was false of every one — and `untranslated-string`'s JSX mask
|
|
1606
|
-
`/\{[^{}]*\}/g` does not nest, so `{t('k', { org: x })}` lost its inner brace group first and the
|
|
1607
|
-
remnant read as prose, flagging every `t()` call with an interpolation object or a template-literal
|
|
1608
|
-
key. Both are fixed and both now report zero against `examples/dummy`, which is the first tracked
|
|
1609
|
-
app to carry a `guards/` directory at all. Read the count, never this sentence:
|
|
1610
|
-
`bun -e "import {scaffoldGuardFiles} from './packages/cli/src/templates/scaffold-guards'; console.log(scaffoldGuardFiles().length)"`.
|
|
1611
|
-
|
|
1612
|
-
The scaffolded `AGENTS.md` states nine
|
|
1613
|
-
non-negotiables, and five of them used to be prose — each proven green on `x verify`: a hardcoded
|
|
1614
|
-
JSX string beside a `t()` call, `color: #ff0000` in a stylesheet whose own scaffolded header called
|
|
1615
|
-
it "a lint failure", `toLocaleDateString('en-US')` with no `timeZone`, `t.number` money, and a bare
|
|
1616
|
-
`throw new Error` in a repo. Four of the five are now guards the scaffold writes —
|
|
1617
|
-
`guard-raw-colour`, `guard-unzoned-date`, `guard-bare-error`, `guard-untranslated-string` — so the
|
|
1618
|
-
rule is a build error the day the app is created rather than a sentence an agent may skip. The
|
|
1619
|
-
fifth, money-as-float, has **no static signature**; the scaffolded `AGENTS.md` row now points at the
|
|
1620
|
-
`MoneyInput` type error that already fires, because shipping a guard that cannot work is worse than
|
|
1621
|
-
naming the mechanism that does. Their codes are app codes derived from the guard name, so none of
|
|
1622
|
-
them appears in `wiki/Error-Codes.md` or the manifest.
|
|
1623
|
-
|
|
1624
|
-
`x g guard <name>` writes `guards/<name>.ts` and its test, and nothing else — no index, no
|
|
1625
|
-
registry row, no manifest entry. The emitted rule is the class of failure a guard exists for: a
|
|
1626
|
-
migration that adds a `NOT NULL` column with no `DEFAULT` applies cleanly to an empty local
|
|
1627
|
-
database and fails on the first production table that already holds rows. The `drift` step reads
|
|
1628
|
-
those same files and asks a different question, and a test suite runs against a database the
|
|
1629
|
-
statement has never met — which is exactly when an app needs a rule of its own. Its code is
|
|
1630
|
-
DERIVED from the guard's name (`guardCode`), never written as a literal: an `X_*` literal in
|
|
1631
|
-
framework source is a framework code and `error-catalog.test.ts` requires it to be registered.
|
|
1632
|
-
|
|
1633
|
-
That rule is held to a real bar, because it is the worked example every app starts from and a
|
|
1634
|
-
demonstration that is wrong on realistic input teaches the wrong shape. Block comments are
|
|
1635
|
-
stripped before line comments and both before statements are split, so a commented-out
|
|
1636
|
-
`ALTER TABLE` is a note and not a finding that blocks `x verify` over nothing; and `DEFAULT NULL`
|
|
1637
|
-
counts as **no** default, because it is one in syntax and none in effect — every existing row still
|
|
1638
|
-
takes NULL and still violates `NOT NULL`. Both cases are in the emitted test, which is what proves
|
|
1639
|
-
an app's copy still works, and both run through the real seam in `guards.test.ts`.
|
|
1640
|
-
|
|
1641
|
-
It is in `FIXTURE_GENERATORS` like the other two, and it is the only generated file that imports
|
|
1642
|
-
`@ultimat3/cli` for its types — so the scaffold gate compiling it is what proves a scaffolded app
|
|
1643
|
-
can write one at all. The root `tsconfig.json` `x new` scaffolds has no `include`, so `guards/` is
|
|
1644
|
-
typechecked there by default; an app whose tsconfig names an explicit `include` list has to add
|
|
1645
|
-
`guards/**/*` to it, or its guards compile nowhere.
|
|
1646
|
-
|
|
1647
|
-
## Two generators that scaffold something other than a primitive
|
|
1648
|
-
|
|
1649
|
-
`x g island <name> [--at <dir>]` writes a **client entry point**, not a component: the filename is
|
|
1650
|
-
how the bundler discovers it and `mount` is how the hydration runtime calls it, so the filename,
|
|
1651
|
-
the `mount` export and that `mount` RENDERS are what `templates/island.test.ts` pins — it builds
|
|
1652
|
-
the emitted entry with `buildIslands` and drives it with `mountIsland`, so a template that
|
|
1653
|
-
typechecks and does not mount is a failing test. It runs the mutation too, rather than describing
|
|
1654
|
-
it: the same island with `{count()}` replaced by `{0}` must fail the assertion the live one passes.
|
|
1655
|
-
`--at` takes the directory directly rather than deriving one, because the caller that cannot guess
|
|
1656
|
-
is `X_ISLAND_INVALID` — its cause already holds the exact path a page's `src` resolved to, so its
|
|
1657
|
-
`fix:` hands that path straight back.
|
|
1658
|
-
|
|
1659
|
-
`x g admin:page <name> --permission <perm> [--at <dir>]` writes an ordinary TSX component and **no
|
|
1660
|
-
`defineRoute` call**, deliberately. `@ultimat3/admin`'s `pages:` is the one thing that puts a page in the route
|
|
1661
|
-
table and `guardedPage()` is the one thing that decides it; a generator that emitted a route
|
|
1662
|
-
declaration would hand back the unguarded second way in that seam exists to close. The emitted test
|
|
1663
|
-
asserts the absence. `--permission` defaults to `<name>:read` rather than to nothing, because an
|
|
1664
|
-
empty permission list is `X_ADMIN_PAGE_UNGUARDED` at declaration time. `--at` is the same flag
|
|
1665
|
-
`x g island` takes and for the same reason — an app's admin is wherever its `defineAdmin` is, which
|
|
1666
|
-
no generator can derive, and the hardcoded `apps/admin/src/pages` sent every other layout (the
|
|
1667
|
-
demo's is `apps/admin/app/admin`) to `git mv` after every run.
|
|
1668
|
-
|
|
1669
|
-
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 |
|
|
1670
106
|
|
|
1671
|
-
|
|
1672
|
-
`(planned)` suffix disappears with it, and `x help` follows automatically.
|
|
107
|
+
## Adding a command
|
|
1673
108
|
|
|
1674
|
-
|
|
1675
|
-
add
|
|
1676
|
-
|
|
1677
|
-
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`.
|