@ultimat3/cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +100 -0
  3. package/package.json +60 -0
  4. package/src/app-agents-md.ts +27 -0
  5. package/src/app-boundaries.ts +206 -0
  6. package/src/app-evals.ts +74 -0
  7. package/src/app-load.ts +136 -0
  8. package/src/app-manifest.ts +137 -0
  9. package/src/app-openapi.ts +12 -0
  10. package/src/app-root.ts +57 -0
  11. package/src/bin.ts +17 -0
  12. package/src/boundary-cuts.ts +219 -0
  13. package/src/budgets.ts +92 -0
  14. package/src/cmd-build.ts +109 -0
  15. package/src/cmd-db.ts +187 -0
  16. package/src/cmd-deploy.ts +124 -0
  17. package/src/cmd-dev.ts +286 -0
  18. package/src/cmd-doctor.ts +178 -0
  19. package/src/cmd-errors.ts +99 -0
  20. package/src/cmd-fix.ts +126 -0
  21. package/src/cmd-generate.ts +434 -0
  22. package/src/cmd-help.ts +94 -0
  23. package/src/cmd-i18n.ts +212 -0
  24. package/src/cmd-jobs.ts +237 -0
  25. package/src/cmd-manifest.ts +97 -0
  26. package/src/cmd-mcp.ts +176 -0
  27. package/src/cmd-new.ts +133 -0
  28. package/src/cmd-planned.ts +119 -0
  29. package/src/cmd-policy.ts +136 -0
  30. package/src/cmd-registries.ts +195 -0
  31. package/src/cmd-routes.ts +73 -0
  32. package/src/cmd-tasks.ts +151 -0
  33. package/src/cmd-test.ts +109 -0
  34. package/src/cmd-verify.ts +265 -0
  35. package/src/command.ts +33 -0
  36. package/src/dev-assets.ts +177 -0
  37. package/src/dev-dashboard.ts +242 -0
  38. package/src/dev-hooks.ts +51 -0
  39. package/src/dev-policy.ts +82 -0
  40. package/src/dev-queue.ts +109 -0
  41. package/src/dev-render.ts +129 -0
  42. package/src/dev-replicator.ts +92 -0
  43. package/src/dev-roles.ts +246 -0
  44. package/src/dev-runtime.ts +203 -0
  45. package/src/dev-services.ts +75 -0
  46. package/src/dev-traces.ts +141 -0
  47. package/src/dispatch.ts +98 -0
  48. package/src/drift.ts +86 -0
  49. package/src/error-catalog.ts +156 -0
  50. package/src/error-contract.ts +212 -0
  51. package/src/errors.ts +367 -0
  52. package/src/exec.ts +70 -0
  53. package/src/hold.ts +48 -0
  54. package/src/i18n-audit.ts +183 -0
  55. package/src/index.ts +179 -0
  56. package/src/jobs-drain.ts +151 -0
  57. package/src/jobs-json.ts +134 -0
  58. package/src/jobs-report.ts +132 -0
  59. package/src/jobs-table.ts +34 -0
  60. package/src/json-merge.ts +40 -0
  61. package/src/mcp-db-target.ts +50 -0
  62. package/src/mcp-errors.ts +99 -0
  63. package/src/mcp-host.ts +282 -0
  64. package/src/mcp-test-output.ts +57 -0
  65. package/src/messages.ts +119 -0
  66. package/src/output.ts +174 -0
  67. package/src/parse.ts +243 -0
  68. package/src/policy-facts.ts +196 -0
  69. package/src/policy-fixture.ts +71 -0
  70. package/src/registry.ts +73 -0
  71. package/src/scaffold-fixture.ts +69 -0
  72. package/src/scaffold-typecheck.ts +240 -0
  73. package/src/source-files.ts +38 -0
  74. package/src/table.ts +19 -0
  75. package/src/tasks-facts.ts +113 -0
  76. package/src/templates/action.ts +193 -0
  77. package/src/templates/admin.ts +46 -0
  78. package/src/templates/catalog-json.ts +17 -0
  79. package/src/templates/entity.ts +157 -0
  80. package/src/templates/index.ts +23 -0
  81. package/src/templates/job.ts +148 -0
  82. package/src/templates/locales.ts +93 -0
  83. package/src/templates/naming.ts +97 -0
  84. package/src/templates/policy.ts +120 -0
  85. package/src/templates/query.ts +116 -0
  86. package/src/templates/resource.ts +199 -0
  87. package/src/templates/route.ts +138 -0
  88. package/src/templates/scaffold-app.ts +320 -0
  89. package/src/templates/scaffold-docs.ts +156 -0
  90. package/src/templates/scaffold-i18n.ts +149 -0
  91. package/src/templates/scaffold-icon.ts +54 -0
  92. package/src/templates/scaffold-package-shape.ts +49 -0
  93. package/src/templates/scaffold-repo.ts +427 -0
  94. package/src/test-select.ts +130 -0
  95. package/src/test-shards.ts +188 -0
  96. package/src/thrown-by.ts +24 -0
  97. package/src/ts-scan.ts +217 -0
  98. package/src/verify-step.ts +83 -0
  99. package/src/verify-tests.ts +166 -0
  100. package/src/version-loader.ts +16 -0
  101. package/src/workspace-checks.ts +288 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 developerz.ai
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,100 @@
1
+ # @ultimat3/cli
2
+
3
+ The `x` binary. One char during dev, one command per job, `--json` on every one of them.
4
+
5
+ ## What it owns
6
+
7
+ Commands and the `x verify` step count, `As of 2026-08`:
8
+
9
+ | Command | Does | Notes |
10
+ |---|---|---|
11
+ | `x new <name>` | scaffolds the monorepo | interactive-free; auth, seeded DB, example route |
12
+ | `x dev` | every role in one process | embedded Postgres/events/storage, `/_x` mounted |
13
+ | `x build --target docker\|binary\|static` | one artifact | `ROLE` selects behaviour at start |
14
+ | `x verify` | **the gate** | 16 named steps, each with pass/fail + duration |
15
+ | `x g <primitive> <name>` | scaffolds a primitive **with a passing test** | never a TODO stub |
16
+ | `x db gen\|migrate\|reset\|studio\|branch` | everything DB | `branch` = copy-on-write clone + preview URL |
17
+ | `x mcp serve` | `@ultimat3/mcp`'s 13 dev tools, over stdio or HTTP | one catalog, one scope set, both transports |
18
+ | `x doctor` | environment, ports, drift, PWA prerequisites | every finding carries a fix command |
19
+ | `x deploy` | container deploy plan | compose or helm; zero platform primitives |
20
+ | `x manifest` / `x routes` | generated facts | `x.manifest.json`, `openapi.json`, route table |
21
+ | `x actions` / `x queries` / `x entities` | the declaration registries | `list` and `describe <name>`, straight off the registries |
22
+ | `x jobs ls\|show\|retry\|drain` | the queue | depth, dead letters, step traces, `retry --from-step`, `drain --to` |
23
+ | `x test [type]` | one of the six test types, or all | same type rule as the gate; `--filter`, `--sample N` |
24
+ | `x errors explain <CODE>` | the error table, programmatically | refuses an unregistered code instead of inventing one |
25
+ | `x fix boundary <file>` | the minimal cut for a crossed surface boundary | prints the plan and the `git mv`; never rewrites a file |
26
+
27
+ Everything in [CLI reference](../../wiki/CLI-Reference.md)'s planned table is also in the registry
28
+ and exits `X_NOT_IMPLEMENTED` with a `fix:` naming the closest shipped command — "not built yet"
29
+ and "not a command" are different facts.
30
+
31
+ ## The output contract
32
+
33
+ Every command returns one `CommandResult`; the human renderer and the JSON renderer are
34
+ projections of it, so `--json` can never drift from the terminal.
35
+
36
+ ```
37
+ X_DB_DRIFT: schema differs from migrations
38
+ cause: table "posts" has column "publish_at" not present in any migration
39
+ fix: x db gen "add publish_at"
40
+ ```
41
+
42
+ ```sh
43
+ x verify --json
44
+ # {"ok":false,"command":"verify","summary":"1 of 16 steps failed","steps":[...]}
45
+ ```
46
+
47
+ ## `x verify` steps
48
+
49
+ `typecheck lint boundaries filesize package-shape errors unit contract live job e2e eval drift
50
+ contract-diff budgets manifest`
51
+
52
+ One list, in cost order, defined once in `cmd-verify.ts` — the framework repo's own gate
53
+ (`bun run verify`) runs exactly it. A step with nothing to check here reports as skipped, never as
54
+ passed. Never bails early: an agent fixing three things needs all three findings from one run.
55
+ There is no `--only` and no `--skip`; the exit code is non-zero if any step fails.
56
+
57
+ ## Layout
58
+
59
+ | File | Responsibility |
60
+ |---|---|
61
+ | `bin.ts` | argv, stdout, exit code — nothing else |
62
+ | `dispatch.ts` | parse → run → render → exit; the only I/O boundary |
63
+ | `parse.ts` | flags, subcommands, `--json`, `--help`, suggestions |
64
+ | `output.ts` | one data shape, two renderers, the 3-line error format |
65
+ | `registry.ts` | the one command list |
66
+ | `cmd-*.ts` | one command group each |
67
+ | `templates/` | scaffolding as typed string modules, not copied fixtures |
68
+ | `app-load.ts` | import an app's modules so the framework registries hold it |
69
+ | `app-manifest.ts` | `x.manifest.json`, projected by `@ultimat3/manifest` |
70
+ | `app-openapi.ts` | `openapi.json`, projected by `@ultimat3/action` |
71
+ | `app-boundaries.ts` | app import boundaries, over `@ultimat3/render`'s surface check |
72
+ | `app-agents-md.ts` | `AGENTS.md` exists and stays short, over `@ultimat3/manifest`'s check |
73
+ | `dev-*.ts` | what `x dev` boots: services, runtime, routes, hooks, roles, the `/_x` mount |
74
+ | `mcp-host.ts` | the shell-side half of `@ultimat3/mcp`'s dev server — db, tests, logs, verify |
75
+ | `verify-step.ts` | the step shape, the step names, the host-check hook |
76
+ | `verify-tests.ts` | one `bun test` invocation per test type |
77
+ | `workspace-checks.ts` | file-size ceiling and package contract files |
78
+ | `drift.ts` `budgets.ts` | the checks `x verify` composes |
79
+
80
+ The CLI describes an app by **loading** it, never by parsing it: `action()`, `entity()`,
81
+ `job()` and `defineRoute()` register themselves, and `x manifest`, `x routes` and `x verify`
82
+ read the same tables the running server reads. There is no second definition of a primitive,
83
+ no second OpenAPI builder and no second surface-boundary walk anywhere in this package.
84
+
85
+ ## Generated file layout
86
+
87
+ `x g` writes into the feature slice:
88
+
89
+ ```
90
+ apps/web/app/<feature>/{entity,repo,service,policy,errors,ui}.ts
91
+ apps/web/app/<feature>/{actions,queries,live,jobs,tasks}/<name>.ts
92
+ apps/web/{site,app}/<path>/page.tsx
93
+ ```
94
+
95
+ Every emitted source has a `<file>.test.ts` beside it that passes on the first run.
96
+
97
+ ## Errors
98
+
99
+ `X_CLI_UNKNOWN_COMMAND` `X_CLI_BAD_FLAG` `X_VERIFY_FAILED` `X_NOT_IN_APP` `X_BUN_VERSION`
100
+ `X_NOT_IMPLEMENTED`
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@ultimat3/cli",
3
+ "version": "1.0.0",
4
+ "description": "The `x` binary: new, dev, build, verify, generate, db, mcp, doctor, deploy",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/developerz-ai/ultimate.git",
10
+ "directory": "packages/cli"
11
+ },
12
+ "publishConfig": {
13
+ "access": "public",
14
+ "provenance": true
15
+ },
16
+ "exports": {
17
+ ".": "./src/index.ts"
18
+ },
19
+ "bin": {
20
+ "x": "./src/bin.ts"
21
+ },
22
+ "files": [
23
+ "src",
24
+ "!src/**/*.test.ts",
25
+ "README.md",
26
+ "LICENSE"
27
+ ],
28
+ "engines": {
29
+ "bun": ">=1.3.0"
30
+ },
31
+ "scripts": {
32
+ "typecheck": "tsc --noEmit -p tsconfig.json",
33
+ "test": "bun test",
34
+ "dev": "bun run src/bin.ts dev"
35
+ },
36
+ "dependencies": {
37
+ "@ultimat3/action": "1.0.0",
38
+ "@ultimat3/admin": "1.0.0",
39
+ "@ultimat3/ai": "1.0.0",
40
+ "@ultimat3/cache": "1.0.0",
41
+ "@ultimat3/core": "1.0.0",
42
+ "@ultimat3/db": "1.0.0",
43
+ "@ultimat3/entity": "1.0.0",
44
+ "@ultimat3/http": "1.0.0",
45
+ "@ultimat3/i18n": "1.0.0",
46
+ "@ultimat3/jobs": "1.0.0",
47
+ "@ultimat3/mail": "1.0.0",
48
+ "@ultimat3/manifest": "1.0.0",
49
+ "@ultimat3/mcp": "1.0.0",
50
+ "@ultimat3/policy": "1.0.0",
51
+ "@ultimat3/pwa": "1.0.0",
52
+ "@ultimat3/query": "1.0.0",
53
+ "@ultimat3/realtime": "1.0.0",
54
+ "@ultimat3/render": "1.0.0",
55
+ "@ultimat3/seo": "1.0.0",
56
+ "@ultimat3/storage": "1.0.0",
57
+ "@ultimat3/testing": "1.0.0",
58
+ "@ultimat3/time": "1.0.0"
59
+ }
60
+ }
@@ -0,0 +1,27 @@
1
+ // The hand-written half of what an agent reads. `@ultimat3/manifest` owns both context files —
2
+ // `x.manifest.json` carries the facts derived from code, `AGENTS.md` the conventions code cannot
3
+ // express — so the gate runs that package's own check rather than forming a second opinion about
4
+ // what a context file has to be. Warnings never fail: they are judgement calls a human makes.
5
+
6
+ // Bun ships no `Bun.*` path API: `join` builds the host-separator path the check reads.
7
+ import { join } from 'node:path';
8
+ import { AGENTS_MD_FILENAME, assertAgentsMd } from '@ultimat3/manifest';
9
+ import type { Finding } from './output';
10
+ import { findingFrom } from './output';
11
+
12
+ export interface AgentsMdOutcome {
13
+ readonly findings: readonly Finding[];
14
+ /** Non-fatal observations, carried into `--json` so a human can judge them. */
15
+ readonly warnings: readonly string[];
16
+ }
17
+
18
+ /** `assertAgentsMd` throws `X_AGENTS_MD_*`; a gate step reports, so the error becomes a finding. */
19
+ export async function checkAgentsMd(root: string): Promise<AgentsMdOutcome> {
20
+ const path = join(root, AGENTS_MD_FILENAME);
21
+ try {
22
+ const { warnings } = await assertAgentsMd({ path });
23
+ return { findings: [], warnings };
24
+ } catch (error) {
25
+ return { findings: [{ ...findingFrom(error), at: AGENTS_MD_FILENAME }], warnings: [] };
26
+ }
27
+ }
@@ -0,0 +1,206 @@
1
+ // App-level import boundaries: the rules that keep the static path from paying for the app path
2
+ // (axiom 6) and keep layers from collapsing into each other.
3
+ //
4
+ // The three surface rules are `@ultimat3/render`'s `checkSurfaceBoundary` — the same check the
5
+ // build runs, transitive, naming the whole chain. The two layer rules below stay here because no
6
+ // package owns them: `service.ts` is not a primitive, and a route's ban on the database is about
7
+ // app layout, not about rendering.
8
+ //
9
+ // Runtime imports only: `Bun.Transpiler.scanImports` sees what survives type erasure, which is
10
+ // exactly the distinction the `app/ -> api/` rule needs (`import type` is allowed, a value
11
+ // import is not).
12
+
13
+ // Bun ships no `Bun.*` path API: `joinPath` reaches a file on disk with the host's separator.
14
+ import { join as joinPath } from 'node:path';
15
+ // The POSIX variants resolve specifiers against import-graph keys, which are POSIX on every host.
16
+ import { dirname, join, normalize, relative } from 'node:path/posix';
17
+ import type { BoundaryRule, ImportGraph } from '@ultimat3/render';
18
+ import { checkSurfaceBoundary, importGraph } from '@ultimat3/render';
19
+ import type { Finding } from './output';
20
+
21
+ export const BOUNDARY_CODES = [
22
+ 'X_BOUNDARY_SITE_TO_APP',
23
+ 'X_BOUNDARY_SHARED_LEAF',
24
+ 'X_BOUNDARY_APP_TO_API',
25
+ 'X_BOUNDARY_ROUTE_TO_DB',
26
+ 'X_BOUNDARY_SERVICE_TO_HTTP',
27
+ ] as const;
28
+
29
+ export type BoundaryCode = (typeof BOUNDARY_CODES)[number];
30
+
31
+ export interface SourceFile {
32
+ /** POSIX path relative to the app root, e.g. `apps/web/site/pricing/page.tsx`. */
33
+ readonly path: string;
34
+ readonly source: string;
35
+ }
36
+
37
+ const CODE_OF: Readonly<Record<BoundaryRule, BoundaryCode>> = {
38
+ 'site-imports-app': 'X_BOUNDARY_SITE_TO_APP',
39
+ 'shared-is-a-leaf': 'X_BOUNDARY_SHARED_LEAF',
40
+ 'app-imports-api-at-runtime': 'X_BOUNDARY_APP_TO_API',
41
+ };
42
+
43
+ /**
44
+ * The one rule → diagnostic-code mapping. `x verify` reports a surface violation as a finding and
45
+ * `x fix boundary` re-reports the same violation as a cut; a second copy of this table is the two
46
+ * commands drifting onto different codes for one edge.
47
+ */
48
+ export const boundaryCodeOf = (rule: BoundaryRule): BoundaryCode => CODE_OF[rule];
49
+
50
+ const docs = (code: BoundaryCode): string => `https://ultimate.dev/errors/${code}`;
51
+
52
+ const isRoute = (path: string): boolean => /\/(page|layout|route)\.[cm]?tsx?$/.test(path);
53
+ const isService = (path: string): boolean => /\/service\.[cm]?ts$/.test(path);
54
+ const isDbSpecifier = (specifier: string): boolean =>
55
+ /(^|\/)packages\/db($|\/)/.test(specifier) ||
56
+ specifier.endsWith('/db') ||
57
+ /^@[^/]+\/db$/.test(specifier) ||
58
+ specifier === 'drizzle-orm';
59
+ const isHttpSpecifier = (specifier: string): boolean =>
60
+ specifier === '@ultimat3/http' || /(^|\/)http($|\/)/.test(specifier);
61
+
62
+ /** The transpiler rejects a shebang, and an app's `bin/` entry points legitimately have one. */
63
+ export const stripShebang = (source: string): string =>
64
+ source.startsWith('#!') ? source.slice(source.indexOf('\n') + 1) : source;
65
+
66
+ /** Bun's transpiler is the parser; a regex fallback would miss re-exports and dynamic imports. */
67
+ export function scanRuntimeImports(file: SourceFile): readonly string[] {
68
+ const loader = file.path.endsWith('x') ? 'tsx' : 'ts';
69
+ const transpiler = new Bun.Transpiler({ loader });
70
+ return transpiler.scanImports(stripShebang(file.source)).map((entry) => entry.path);
71
+ }
72
+
73
+ const CANDIDATE_SUFFIXES = ['', '.ts', '.tsx', '/index.ts', '/index.tsx'] as const;
74
+
75
+ /**
76
+ * Resolve a relative specifier onto a real graph key. Extensions matter here and only here: an
77
+ * edge that does not land on a key is a dead end, and the transitive walk stops one hop short.
78
+ */
79
+ export function resolveSpecifier(
80
+ fromFile: string,
81
+ specifier: string,
82
+ keys: ReadonlySet<string>,
83
+ ): string {
84
+ if (!specifier.startsWith('.')) return specifier;
85
+ const base = normalize(join(dirname(fromFile), specifier));
86
+ for (const suffix of CANDIDATE_SUFFIXES) {
87
+ if (keys.has(`${base}${suffix}`)) return `${base}${suffix}`;
88
+ }
89
+ return base;
90
+ }
91
+
92
+ /**
93
+ * The specifier `fromFile` must write to reach `target` — `resolveSpecifier` run backwards, and
94
+ * what every import of a file has to become once that file moves. Extensionless, because that is
95
+ * the form `CANDIDATE_SUFFIXES` resolves and the form every app source already writes.
96
+ */
97
+ export function relativeSpecifier(fromFile: string, target: string): string {
98
+ const path = relative(dirname(fromFile), target).replace(/\.[cm]?tsx?$/, '');
99
+ return path.startsWith('.') ? path : `./${path}`;
100
+ }
101
+
102
+ interface ScannedFile {
103
+ readonly path: string;
104
+ readonly imports: readonly string[];
105
+ }
106
+
107
+ function scan(files: readonly SourceFile[]): readonly ScannedFile[] {
108
+ const keys = new Set(files.map((file) => file.path));
109
+ return files.map((file) => ({
110
+ path: file.path,
111
+ imports: scanRuntimeImports(file).map((specifier) =>
112
+ resolveSpecifier(file.path, specifier, keys),
113
+ ),
114
+ }));
115
+ }
116
+
117
+ function graphOf(scanned: readonly ScannedFile[]): ImportGraph {
118
+ const record: Record<string, readonly string[]> = {};
119
+ for (const file of scanned) record[file.path] = file.imports;
120
+ return importGraph(record);
121
+ }
122
+
123
+ const surfaceFindings = (graph: ImportGraph): readonly Finding[] =>
124
+ checkSurfaceBoundary(graph).map((violation) => {
125
+ const code = boundaryCodeOf(violation.rule);
126
+ return {
127
+ code,
128
+ cause: violation.cause,
129
+ fix: violation.fix,
130
+ docs: docs(code),
131
+ at: violation.importer,
132
+ };
133
+ });
134
+
135
+ /** `apps/web/app/posts/service.ts` → `posts`: the primitive a generator would be told to make. */
136
+ const subjectOf = (path: string, fallback: string): string => path.split('/').at(-2) ?? fallback;
137
+
138
+ /**
139
+ * Both fixes are one runnable line, with the rest of the instruction behind a `#` — a fix a
140
+ * caller has to edit before it runs is prose, and prose is what "errors are instructions" bans.
141
+ */
142
+ function layerFindings(scanned: readonly ScannedFile[]): readonly Finding[] {
143
+ const findings: Finding[] = [];
144
+ for (const file of scanned) {
145
+ for (const specifier of file.imports) {
146
+ if (isRoute(file.path) && isDbSpecifier(specifier)) {
147
+ findings.push({
148
+ code: 'X_BOUNDARY_ROUTE_TO_DB',
149
+ cause: `route imports the database ("${specifier}") — routes call actions and queries`,
150
+ fix: `x g query ${subjectOf(file.path, 'rows')} # then call it from the route`,
151
+ docs: docs('X_BOUNDARY_ROUTE_TO_DB'),
152
+ at: file.path,
153
+ });
154
+ }
155
+ if (isService(file.path) && isHttpSpecifier(specifier)) {
156
+ findings.push({
157
+ code: 'X_BOUNDARY_SERVICE_TO_HTTP',
158
+ cause: `service imports HTTP ("${specifier}") — a service that knows about requests cannot be reused by a job`,
159
+ fix: `x g action ${subjectOf(file.path, 'service')} # read the request there, pass the service plain values`,
160
+ docs: docs('X_BOUNDARY_SERVICE_TO_HTTP'),
161
+ at: file.path,
162
+ });
163
+ }
164
+ }
165
+ }
166
+ return findings;
167
+ }
168
+
169
+ /**
170
+ * Check every app source file against both rule sets. Pure — callers do the I/O — so the dev
171
+ * server can run this on every save and `x verify` can run it over a full file list.
172
+ */
173
+ export function checkImportRules(files: readonly SourceFile[]): readonly Finding[] {
174
+ const scanned = scan(files);
175
+ return [...surfaceFindings(graphOf(scanned)), ...layerFindings(scanned)];
176
+ }
177
+
178
+ const APP_GLOBS = ['apps/*/{site,app,api,shared}/**/*.{ts,tsx}'];
179
+
180
+ /**
181
+ * Read every source file under an app's site, app, api and shared surfaces (`APP_GLOBS`). The
182
+ * only I/O in this module, and the one definition of "which files are the app's sources" —
183
+ * `checkAppBoundaries` reads through this, and so does `x fix boundary`, never a second glob.
184
+ */
185
+ export async function readAppSources(root: string): Promise<readonly SourceFile[]> {
186
+ const files: SourceFile[] = [];
187
+ for (const pattern of APP_GLOBS) {
188
+ const glob = new Bun.Glob(pattern);
189
+ for await (const path of glob.scan({ cwd: root, absolute: false })) {
190
+ if (path.includes('node_modules') || path.includes('.test.')) continue;
191
+ const posix = path.split('\\').join('/');
192
+ files.push({ path: posix, source: await Bun.file(joinPath(root, posix)).text() });
193
+ }
194
+ }
195
+ return files;
196
+ }
197
+
198
+ /** The resolved import graph over a file set — what `checkSurfaceBoundary` walks. */
199
+ export function appImportGraph(files: readonly SourceFile[]): ImportGraph {
200
+ return graphOf(scan(files));
201
+ }
202
+
203
+ /** Read an app's sources and check them. */
204
+ export async function checkAppBoundaries(root: string): Promise<readonly Finding[]> {
205
+ return checkImportRules(await readAppSources(root));
206
+ }
@@ -0,0 +1,74 @@
1
+ // The eval facts `x verify` reads: recording is off, every prompt has an eval, and every eval has
2
+ // a baseline it can actually gate against.
3
+ //
4
+ // All three come from the app's own registries and from `@ultimat3/ai`'s own reader — never from a
5
+ // filename, so renaming a file cannot silently un-gate a prompt, and never from a second baseline
6
+ // parser, so the gate and the suite can never disagree about what a recorded score is.
7
+
8
+ import {
9
+ baselinePath,
10
+ describeEvals,
11
+ EvalBaselineMissingError,
12
+ EvalMissingError,
13
+ EvalRecordingError,
14
+ promptsWithoutEvals,
15
+ RECORD_ENV,
16
+ readBaseline,
17
+ recordingBaselines,
18
+ } from '@ultimat3/ai';
19
+ import { loadApp } from './app-load';
20
+ import type { Finding } from './output';
21
+ import { findingFrom } from './output';
22
+
23
+ /**
24
+ * Recording makes every eval write the numbers it just measured and pass, so a gate run that
25
+ * inherited the flag is green over scores nothing compared — and rewrites the committed baselines
26
+ * on its way through. The step refuses before the suite runs, because a red step alone would not
27
+ * undo the rewrite.
28
+ */
29
+ export function checkEvalRecording(): readonly Finding[] {
30
+ if (!recordingBaselines()) return [];
31
+ return [{ ...findingFrom(new EvalRecordingError({ env: RECORD_ENV })), at: RECORD_ENV }];
32
+ }
33
+
34
+ /** Every prompt an app registers must be named by an eval. */
35
+ export async function checkEvalCoverage(root: string): Promise<readonly Finding[]> {
36
+ // Loading is idempotent per process: `x verify` loads the app once and every later step,
37
+ // including the manifest, reads the same registries.
38
+ await loadApp(root);
39
+ return promptsWithoutEvals().map((prompt) => ({
40
+ ...findingFrom(new EvalMissingError({ prompt: prompt.ref, id: prompt.id })),
41
+ at: prompt.ref,
42
+ }));
43
+ }
44
+
45
+ /**
46
+ * Every eval must have a baseline on disk that reads back. Without this the gate asks only whether
47
+ * a `defineEval` exists, and an eval whose numbers were never recorded — one no test asserts, one
48
+ * whose `baseline:` is a cwd-relative string — passes it while gating on nothing.
49
+ */
50
+ export async function checkEvalBaselines(root: string): Promise<readonly Finding[]> {
51
+ await loadApp(root);
52
+ const findings: Finding[] = [];
53
+ for (const fact of describeEvals()) {
54
+ // `baselinePath` refuses a spec that is not `import.meta.resolve('./…')`, and `readBaseline`
55
+ // refuses a file that is not a baseline; both say so as the eval's own X_* code.
56
+ try {
57
+ const path = baselinePath(fact.baseline, fact.name);
58
+ if ((await readBaseline(path)) !== undefined) continue;
59
+ findings.push({
60
+ ...findingFrom(
61
+ new EvalBaselineMissingError({
62
+ eval: fact.name,
63
+ path,
64
+ reason: 'has never been recorded',
65
+ }),
66
+ ),
67
+ at: fact.name,
68
+ });
69
+ } catch (error) {
70
+ findings.push({ ...findingFrom(error), at: fact.name });
71
+ }
72
+ }
73
+ return findings;
74
+ }
@@ -0,0 +1,136 @@
1
+ // Loading an app into the framework's own registries. The CLI owns no second definition of what
2
+ // a primitive is: `entity()`, `job()` and `task()` register on import, and `registerActions` /
3
+ // `registerQueries` / `registerRoute` name the rest — so `x manifest`, `x routes` and `x verify`
4
+ // read exactly the tables the running server reads.
5
+
6
+ // Bun ships no `Bun.*` path API: `relative`/`sep` turn an absolute scan hit into the app-root-
7
+ // relative POSIX path every finding and every manifest fact is keyed by.
8
+ import { relative, sep } from 'node:path';
9
+ import { registerActions } from '@ultimat3/action';
10
+ import { localeConfig } from '@ultimat3/i18n';
11
+ import type { ErrorCodeFact } from '@ultimat3/manifest';
12
+ import { registerQueries } from '@ultimat3/query';
13
+ import { isRouteConfig, registerRoute } from '@ultimat3/render';
14
+ import { collectDeclaredCodes } from './error-contract';
15
+ import type { Finding } from './output';
16
+ import { findingFrom } from './output';
17
+
18
+ /** Every place an app keeps code the framework has to see. */
19
+ const APP_GLOBS = [
20
+ 'apps/*/{site,app,api,shared}/**/*.{ts,tsx}',
21
+ 'apps/*/*.{ts,tsx}',
22
+ 'packages/*/src/**/*.ts',
23
+ ] as const;
24
+
25
+ export interface LoadedApp {
26
+ readonly root: string;
27
+ /** App-root-relative POSIX paths of every module that imported, sorted. */
28
+ readonly files: readonly string[];
29
+ /** Every `X_*` code the app's source declares, by code — the one fact no registry holds. */
30
+ readonly errorCodes: readonly ErrorCodeFact[];
31
+ /**
32
+ * The locale the app falls back to. `packages/i18n/src/index.ts` is inside the import loop, and
33
+ * `defineCatalogs()` configures `@ultimat3/i18n` on its way through — so this is the framework's
34
+ * own answer, read back from `localeConfig()`, and never a regex over the app's source, which
35
+ * only ever matched the one `defineCatalogs({ default: '…' })` spelling it anticipated. An app
36
+ * whose i18n module would not import leaves the framework default (`en`) and a finding saying so.
37
+ */
38
+ readonly defaultLocale: string;
39
+ /** Modules that would not import, and primitives that would not register. */
40
+ readonly findings: readonly Finding[];
41
+ }
42
+
43
+ // A module is imported and registered exactly once per PROCESS: `import()` caches, and a registry
44
+ // rejects a second registration of a name. So a rescan refreshes only the facts DERIVED from the
45
+ // registries — the manifest and its build id — and never the primitives themselves: an edited route
46
+ // config, action or query needs a restart. Clearing the registries would not change that. Bun
47
+ // exposes no way to invalidate a cached module, so the re-import hands back the same stale exports,
48
+ // and a cache-busting query string leaks a fresh module instance on every save.
49
+ const registered = new Set<string>();
50
+ // A registration failure is sticky: the file is never retried, so the finding is replayed.
51
+ const failures = new Map<string, Finding>();
52
+
53
+ /** Test seam, and what `x dev` would call if it ever restarted the registries in-process. */
54
+ export function resetAppLoad(): void {
55
+ registered.clear();
56
+ failures.clear();
57
+ }
58
+
59
+ export async function loadApp(root: string): Promise<LoadedApp> {
60
+ const files: string[] = [];
61
+ const findings: Finding[] = [];
62
+
63
+ for (const pattern of APP_GLOBS) {
64
+ for await (const absolute of new Bun.Glob(pattern).scan({ cwd: root, absolute: true })) {
65
+ if (absolute.includes('node_modules') || absolute.includes('.test.')) continue;
66
+ const file = relative(root, absolute).split(sep).join('/');
67
+ let module: Record<string, unknown>;
68
+ try {
69
+ module = (await import(absolute)) as Record<string, unknown>;
70
+ } catch (error) {
71
+ findings.push({ ...findingFrom(error), at: file });
72
+ continue;
73
+ }
74
+ files.push(file);
75
+ const finding = await register(absolute, file, module);
76
+ if (finding !== undefined) findings.push(finding);
77
+ }
78
+ }
79
+
80
+ files.sort();
81
+ // Read after the loop, never before it: `configureLocales` runs on the app's own import.
82
+ return {
83
+ root,
84
+ files,
85
+ errorCodes: await appErrorCodes(root),
86
+ defaultLocale: localeConfig().fallback,
87
+ findings,
88
+ };
89
+ }
90
+
91
+ /** Registers a module once; every later call replays whatever the first one reported. */
92
+ async function register(
93
+ absolute: string,
94
+ file: string,
95
+ module: Record<string, unknown>,
96
+ ): Promise<Finding | undefined> {
97
+ const previous = failures.get(absolute);
98
+ if (previous !== undefined) return previous;
99
+ if (registered.has(absolute)) return undefined;
100
+ registered.add(absolute);
101
+ try {
102
+ const config = module['config'];
103
+ if (isRouteConfig(config)) {
104
+ // The build counts boundaries from the compiled JSX; before a build there is only the
105
+ // source, and `render: 'stream'` is rejected without one — so count them in the text.
106
+ const source = await Bun.file(absolute).text();
107
+ registerRoute({ file, config, suspenseBoundaries: countSuspense(source) });
108
+ }
109
+ registerActions(module);
110
+ registerQueries(module);
111
+ return undefined;
112
+ } catch (error) {
113
+ const finding: Finding = { ...findingFrom(error), at: file };
114
+ failures.set(absolute, finding);
115
+ return finding;
116
+ }
117
+ }
118
+
119
+ const countSuspense = (source: string): number => source.match(/<Suspense[\s/>]/g)?.length ?? 0;
120
+
121
+ /** `packages/db/src/errors.ts` → `packages/db`; `apps/web/app/posts/errors.ts` → `apps/web`. */
122
+ const workspaceOf = (file: string): string => file.split('/').slice(0, 2).join('/');
123
+
124
+ /**
125
+ * The app's `X_*` codes, from the same walk and the same scanner the `errors` gate step uses.
126
+ * Deliberately not a second scan of the loaded modules' `*_ERROR_CODES` exports: that array is a
127
+ * convention some apps follow and most do not, so an app that declares every code at its throw
128
+ * site — the reference app included — published `"errorCodes": []`, a manifest claiming a
129
+ * completeness it never had. `collectDeclaredCodes` is the only answer to "which codes exist?",
130
+ * already sorted by code and one entry per code, so the projection is just the owning workspace.
131
+ */
132
+ const appErrorCodes = async (root: string): Promise<readonly ErrorCodeFact[]> =>
133
+ (await collectDeclaredCodes(root)).map((site) => ({
134
+ code: site.code,
135
+ package: workspaceOf(site.at),
136
+ }));