@ultimat3/cli 1.1.0 → 2.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 (138) hide show
  1. package/CLAUDE.md +724 -0
  2. package/README.md +41 -9
  3. package/package.json +25 -23
  4. package/src/api-routes.ts +16 -0
  5. package/src/app-auth.ts +32 -0
  6. package/src/app-entities.ts +18 -0
  7. package/src/app-env.ts +103 -0
  8. package/src/app-load.ts +20 -3
  9. package/src/bin.ts +4 -3
  10. package/src/budgets.ts +114 -9
  11. package/src/cmd-build.ts +69 -21
  12. package/src/cmd-db-branch.ts +215 -0
  13. package/src/cmd-db.ts +332 -155
  14. package/src/cmd-deploy.ts +59 -6
  15. package/src/cmd-dev.ts +87 -17
  16. package/src/cmd-docs.ts +167 -0
  17. package/src/cmd-doctor.ts +64 -9
  18. package/src/cmd-env.ts +95 -0
  19. package/src/cmd-errors.ts +33 -13
  20. package/src/cmd-fix.ts +5 -1
  21. package/src/cmd-generate.ts +146 -111
  22. package/src/cmd-help.ts +16 -5
  23. package/src/cmd-i18n.ts +2 -0
  24. package/src/cmd-jobs.ts +47 -33
  25. package/src/cmd-mcp.ts +11 -2
  26. package/src/cmd-new.ts +13 -7
  27. package/src/cmd-planned.ts +55 -10
  28. package/src/cmd-policy.ts +1 -0
  29. package/src/cmd-registries.ts +3 -0
  30. package/src/cmd-secrets.ts +368 -0
  31. package/src/cmd-tasks.ts +1 -0
  32. package/src/cmd-test.ts +17 -23
  33. package/src/cmd-verify.ts +177 -23
  34. package/src/db-backfill.ts +401 -0
  35. package/src/db-branch.ts +251 -0
  36. package/src/db-destructive.ts +29 -0
  37. package/src/db-finding.ts +28 -0
  38. package/src/db-generate.ts +112 -0
  39. package/src/db-snapshot.ts +24 -0
  40. package/src/dev-assets.ts +86 -20
  41. package/src/dev-cache.ts +122 -0
  42. package/src/dev-dashboard.ts +19 -4
  43. package/src/dev-hooks.ts +27 -2
  44. package/src/dev-n-plus-one.ts +191 -0
  45. package/src/dev-queue.ts +105 -19
  46. package/src/dev-render.ts +158 -26
  47. package/src/dev-roles-fixture.ts +67 -0
  48. package/src/dev-roles.ts +186 -78
  49. package/src/dev-runtime.ts +117 -40
  50. package/src/dev-services.ts +15 -0
  51. package/src/dev-storage.ts +245 -0
  52. package/src/dev-sync.ts +107 -0
  53. package/src/dev-traces.ts +11 -3
  54. package/src/dispatch.ts +4 -2
  55. package/src/document-styles.ts +54 -0
  56. package/src/drift.ts +37 -9
  57. package/src/error-catalog.ts +7 -18
  58. package/src/error-codes.ts +186 -0
  59. package/src/error-contract.ts +29 -7
  60. package/src/error-fixes.ts +114 -0
  61. package/src/errors.ts +205 -140
  62. package/src/fix-command.ts +268 -0
  63. package/src/flag-number.ts +56 -0
  64. package/src/framework-scope.ts +49 -0
  65. package/src/generate-kinds.ts +97 -0
  66. package/src/guards.ts +186 -0
  67. package/src/index.ts +87 -14
  68. package/src/island-bundle.ts +166 -0
  69. package/src/island-routes.ts +50 -0
  70. package/src/jobs-driver.ts +33 -0
  71. package/src/jobs-json.ts +24 -0
  72. package/src/jobs-report.ts +17 -4
  73. package/src/mcp-db-target.ts +52 -27
  74. package/src/mcp-errors.ts +120 -19
  75. package/src/mcp-host.ts +44 -25
  76. package/src/messages.ts +81 -2
  77. package/src/metrics-endpoint.ts +73 -0
  78. package/src/migrations.ts +37 -4
  79. package/src/otlp-export.ts +64 -0
  80. package/src/output.ts +46 -16
  81. package/src/parse.ts +41 -3
  82. package/src/policy-facts.ts +38 -6
  83. package/src/policy-fixture.ts +14 -7
  84. package/src/prerender.ts +111 -2
  85. package/src/registry.ts +21 -3
  86. package/src/runtime-overrides.ts +66 -0
  87. package/src/safe-url-label.ts +24 -0
  88. package/src/scaffold-fixture.ts +10 -0
  89. package/src/scaffold-typecheck.ts +16 -38
  90. package/src/serve.ts +202 -18
  91. package/src/source-files.ts +4 -0
  92. package/src/statement-loop.ts +74 -0
  93. package/src/style-csp.ts +18 -0
  94. package/src/sync-authenticator.ts +59 -0
  95. package/src/templates/action.ts +15 -30
  96. package/src/templates/admin-page.ts +103 -0
  97. package/src/templates/admin.ts +11 -7
  98. package/src/templates/backfill.ts +212 -0
  99. package/src/templates/entity.ts +72 -31
  100. package/src/templates/guard.ts +143 -0
  101. package/src/templates/index.ts +12 -1
  102. package/src/templates/island.ts +67 -0
  103. package/src/templates/job.ts +53 -13
  104. package/src/templates/naming.ts +17 -1
  105. package/src/templates/policy.ts +35 -28
  106. package/src/templates/query.ts +24 -5
  107. package/src/templates/resource.ts +19 -11
  108. package/src/templates/route.ts +90 -15
  109. package/src/templates/scaffold-app.ts +142 -45
  110. package/src/templates/scaffold-claude-agents.ts +149 -0
  111. package/src/templates/scaffold-claude-commands.ts +221 -0
  112. package/src/templates/scaffold-claude.ts +134 -0
  113. package/src/templates/scaffold-container.ts +46 -2
  114. package/src/templates/scaffold-db-package.ts +91 -0
  115. package/src/templates/scaffold-docs.ts +24 -5
  116. package/src/templates/scaffold-domain-package.ts +90 -0
  117. package/src/templates/scaffold-env.ts +87 -0
  118. package/src/templates/scaffold-i18n.ts +4 -1
  119. package/src/templates/scaffold-mcp-package.ts +49 -0
  120. package/src/templates/scaffold-package-shape.ts +25 -4
  121. package/src/templates/scaffold-repo.ts +116 -257
  122. package/src/templates/scaffold-roles.ts +68 -0
  123. package/src/templates/scaffold-ui-package.ts +56 -0
  124. package/src/templates/slice-foundation.ts +88 -0
  125. package/src/templates/wrap.ts +95 -0
  126. package/src/test-counts.ts +35 -0
  127. package/src/test-select.ts +30 -15
  128. package/src/test-shards.ts +21 -3
  129. package/src/test-workers.ts +47 -0
  130. package/src/ts-scan.ts +271 -13
  131. package/src/tsconfig-references.ts +78 -0
  132. package/src/verify-floor.ts +133 -0
  133. package/src/verify-step.ts +19 -0
  134. package/src/verify-test-run.ts +72 -0
  135. package/src/verify-tests.ts +160 -71
  136. package/src/version-loader.ts +20 -3
  137. package/src/workspace-checks.ts +87 -16
  138. package/src/write-line.ts +34 -0
@@ -0,0 +1,56 @@
1
+ // One integer-flag reader for every command that takes one. `Number.parseInt` alone accepts a
2
+ // prefix and answers `NaN` for the rest, and three commands took it bare: `x doctor --port abc`
3
+ // probed `NaN`, which `portFree` reports as free — a check that CANNOT FAIL — `x dev --port abc`
4
+ // handed `NaN` to `Bun.serve` and bound an arbitrary port, and `x test --workers 4abc` ran four.
5
+
6
+ import { BadFlagError } from './errors';
7
+ import type { ParsedArgs } from './parse';
8
+ import { flagString } from './parse';
9
+
10
+ export interface IntFlag {
11
+ readonly name: string;
12
+ /** The command as it appears in the cause line, e.g. `doctor` for `x doctor`. */
13
+ readonly command: string;
14
+ readonly min: number;
15
+ readonly max?: number;
16
+ /** A runnable invocation carrying a good value — never a `<placeholder>`. */
17
+ readonly example: string;
18
+ }
19
+
20
+ const bound = (flag: IntFlag): string =>
21
+ flag.max === undefined ? `>= ${flag.min}` : `from ${flag.min} to ${flag.max}`;
22
+
23
+ /** `/^\d+$/` first: it is the only test that refuses `4abc`, `4.9`, `0x10`, `+4` and ` 4`. */
24
+ export function parseIntFlag(raw: string, flag: IntFlag): number {
25
+ const value = /^\d+$/.test(raw) ? Number.parseInt(raw, 10) : Number.NaN;
26
+ if (
27
+ !Number.isInteger(value) ||
28
+ value < flag.min ||
29
+ (flag.max !== undefined && value > flag.max)
30
+ ) {
31
+ throw new BadFlagError({
32
+ flag: flag.name,
33
+ command: flag.command,
34
+ reason: `expects an integer ${bound(flag)}, got "${raw}"`,
35
+ fix: flag.example,
36
+ });
37
+ }
38
+ return value;
39
+ }
40
+
41
+ /** The flag's value, or `undefined` when it was not given. Throws `X_CLI_BAD_FLAG` on a bad one. */
42
+ export function readIntFlag(args: ParsedArgs, flag: IntFlag): number | undefined {
43
+ const raw = flagString(args, flag.name);
44
+ return raw === undefined ? undefined : parseIntFlag(raw, flag);
45
+ }
46
+
47
+ /** The same, with a default — for a flag whose spec already declares one. */
48
+ export const intFlagOr = (args: ParsedArgs, flag: IntFlag, fallback: number): number =>
49
+ readIntFlag(args, flag) ?? fallback;
50
+
51
+ /**
52
+ * Every port flag answers to the same bounds `serve.ts`'s `portValue` already enforces on `PORT`,
53
+ * 0 included — 0 is "let the kernel pick", which is how `x dev --port 0` boots a test server on a
54
+ * free port. Two ranges for one concept is the drift this constant exists to prevent.
55
+ */
56
+ export const PORT_RANGE = { min: 0, max: 65_535 } as const;
@@ -0,0 +1,49 @@
1
+ // Where the `@ultimat3` packages THIS `x` is pinned against live on disk. One resolver, because
2
+ // two answers to "which framework is installed" is two answers to every question read off it —
3
+ // the docs `x docs` quotes and the `fix:` lines `x errors explain` projects come from the same
4
+ // directory or they describe different builds.
5
+
6
+ // `node:fs`/`node:path` because Bun ships neither: `dirname` walks a resolved module up to the
7
+ // directory that owns it, and `existsSync` is what says which directory that is.
8
+ import { existsSync } from 'node:fs';
9
+ import { dirname, join } from 'node:path';
10
+
11
+ /**
12
+ * Deep enough for `src/index.ts` and for any entry an `exports` map could point at, shallow enough
13
+ * that a resolver answering something unexpected stops rather than walking to `/`.
14
+ */
15
+ const MAX_DEPTH = 6;
16
+
17
+ /**
18
+ * Resolved from the CLI's own dependency on `@ultimat3/core` rather than from the user's cwd:
19
+ * these are the packages this `x` would actually run. Resolution follows the symlink, so a
20
+ * workspace-linked app lands on the same `packages/` this monorepo does — one code path for both,
21
+ * and a directory listing rather than a hardcoded package list, because an app installs the
22
+ * subset it uses.
23
+ *
24
+ * The **exported entry** is what gets resolved, never `@ultimat3/core/package.json`: every package
25
+ * here declares `"exports": { ".": "./src/index.ts" }` and nothing else, so a subpath specifier is
26
+ * asking a resolver for something the package does not publish. Bun 1.3 happens to answer it
27
+ * anyway; a resolver that enforced `exports` would answer `undefined`, and the failure would be
28
+ * silent — `x docs` reporting no installed packages and `x errors explain` reporting that no
29
+ * framework raises the code. Walking up from the entry to the directory that owns its
30
+ * `package.json` depends on nothing but the entry that is already imported.
31
+ *
32
+ * `undefined` means the CLI cannot see its own dependency, which is a broken install and not
33
+ * merely an undocumented one; every caller reports that rather than answering emptily.
34
+ */
35
+ export function frameworkScopeDir(): string | undefined {
36
+ let dir: string;
37
+ try {
38
+ dir = dirname(Bun.resolveSync('@ultimat3/core', import.meta.dir));
39
+ } catch {
40
+ return undefined;
41
+ }
42
+ for (let depth = 0; depth < MAX_DEPTH; depth += 1) {
43
+ if (existsSync(join(dir, 'package.json'))) return dirname(dir);
44
+ const parent = dirname(dir);
45
+ if (parent === dir) return undefined;
46
+ dir = parent;
47
+ }
48
+ return undefined;
49
+ }
@@ -0,0 +1,97 @@
1
+ // Which generators exist, and how one is named on a command line. Split from `cmd-generate.ts`
2
+ // because "what a generator emits" and "which spelling reaches it" are two jobs — and the file
3
+ // that held both had reached the 500-line ceiling, one generator short of failing its own gate.
4
+
5
+ import { BadFlagError, MissingPositionalError, UnknownCommandError } from './errors';
6
+ import type { Surface } from './templates';
7
+
8
+ export const GENERATORS = [
9
+ 'resource',
10
+ 'action',
11
+ 'mutator',
12
+ 'backfill',
13
+ 'job',
14
+ 'route',
15
+ 'policy',
16
+ 'entity',
17
+ 'query',
18
+ 'task',
19
+ 'island',
20
+ 'admin:page',
21
+ 'guard',
22
+ ] as const;
23
+
24
+ export type Generator = (typeof GENERATORS)[number];
25
+
26
+ /**
27
+ * A resource slice is app-surface by construction: it ships a live query, a form with a signal,
28
+ * two actions and a policy, and `site/` is the 0kb, never-hydrated surface that may not import
29
+ * `app/`. The documented shape is the slice in `app/` plus a separate public route
30
+ * (`docs/architecture/15-adding-a-feature.md`), so the flag is refused before anything is written
31
+ * rather than emitting a slice that fails the app's own budget and boundary gates.
32
+ */
33
+ export function assertSurfaceSupported(kind: Generator, surface: Surface, name: string): void {
34
+ if (kind !== 'resource' || surface !== 'site') return;
35
+ throw new BadFlagError({
36
+ flag: 'surface',
37
+ command: 'g resource',
38
+ reason: 'a resource slice is app-surface — site/ ships 0kb JS and may not import app/',
39
+ // The caller's own name, not `<name>`: a `fix:` is copied and run verbatim, and `x g resource
40
+ // <name>` is a shell redirect (`bash: name: No such file or directory`), not a command.
41
+ fix: `x g resource ${name} && x g route ${name} --surface site`,
42
+ });
43
+ }
44
+
45
+ export function readKind(raw: string | undefined): Generator {
46
+ const kinds: readonly string[] = GENERATORS;
47
+ if (raw !== undefined && kinds.includes(raw)) return raw as Generator;
48
+ throw new UnknownCommandError({
49
+ path: `g ${raw ?? ''}`.trim(),
50
+ known: GENERATORS,
51
+ suggestion: 'g resource',
52
+ });
53
+ }
54
+
55
+ /** Two surfaces, spelled exactly. A typo that fell through to `app` would scaffold the wrong one. */
56
+ export function readSurface(raw: string | undefined, kind: Generator, name: string): Surface {
57
+ if (raw === undefined || raw === 'app') return 'app';
58
+ if (raw === 'site') return 'site';
59
+ throw new BadFlagError({
60
+ flag: 'surface',
61
+ command: 'g',
62
+ reason: `"${raw}" is not a surface (site, app)`,
63
+ fix: `x g ${kind} ${name} --surface app`,
64
+ });
65
+ }
66
+
67
+ /**
68
+ * The missing `<name>` positional. It used to throw `X_CLI_UNKNOWN_COMMAND` — for a command form
69
+ * that IS known — with `fix: "x g route <name>"`, which pasted into a shell is a redirect
70
+ * (`bash: name: No such file or directory`). The code now says what is actually wrong, and the fix
71
+ * is a command that runs.
72
+ */
73
+ export function readName(raw: string | undefined, kind: Generator): string {
74
+ if (raw !== undefined) return raw;
75
+ throw new MissingPositionalError({
76
+ command: `g ${kind}`,
77
+ positional: 'name',
78
+ example: `x g ${kind} ${EXAMPLE_NAME[kind]}`,
79
+ });
80
+ }
81
+
82
+ /** One runnable example per generator, so the `fix:` is a command and not a shape. */
83
+ const EXAMPLE_NAME: Readonly<Record<Generator, string>> = {
84
+ resource: 'invoice',
85
+ action: 'publish-post',
86
+ mutator: 'rename-post',
87
+ backfill: 'backfill-slugs',
88
+ job: 'send-digest',
89
+ route: 'posts',
90
+ policy: 'post',
91
+ entity: 'post',
92
+ query: 'recent-posts',
93
+ task: 'nightly-digest',
94
+ island: 'counter',
95
+ 'admin:page': 'ops',
96
+ guard: 'migration-safety',
97
+ };
package/src/guards.ts ADDED
@@ -0,0 +1,186 @@
1
+ // An app's own convention, made into a build error (axiom 3). A file in `guards/` exports one
2
+ // `guard`; the gate discovers the directory and runs each one inside the `boundaries` step. The
3
+ // framework decides what a guard IS — a function returning findings, held to the error contract —
4
+ // and nothing about what a guard may check.
5
+
6
+ // Bun ships no equivalent for either: `existsSync` answers whether this app has a guards
7
+ // directory, `join` builds the host-separator path, and `pathToFileURL` is the only spelling of an
8
+ // absolute path `import()` accepts on every host.
9
+ import { existsSync } from 'node:fs';
10
+ import { join } from 'node:path';
11
+ import { pathToFileURL } from 'node:url';
12
+ import { renderCauseValue, renderThrowable } from '@ultimat3/core';
13
+ import { fixProblem } from './error-contract';
14
+ import type { Finding } from './output';
15
+ import type { HostCheck } from './verify-step';
16
+
17
+ /** The directory IS the registration. An app-side list is a list an app can forget to add to. */
18
+ export const GUARD_DIR = 'guards';
19
+
20
+ export interface Guard {
21
+ /** What this app refuses, in one line. It names the rule when the guard itself is the problem. */
22
+ readonly summary: string;
23
+ /**
24
+ * The rule, over the app root. Returns findings — it never prints, never decides an exit code
25
+ * and never throws for a normal result, because `--json`, the step table and the exit code are
26
+ * all projections of what it returns (axiom 2).
27
+ */
28
+ check(root: string): Promise<readonly Finding[]> | readonly Finding[];
29
+ }
30
+
31
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
32
+ typeof value === 'object' && value !== null;
33
+
34
+ const isGuard = (value: unknown): value is Guard =>
35
+ isRecord(value) &&
36
+ typeof value['summary'] === 'string' &&
37
+ value['summary'].trim() !== '' &&
38
+ typeof value['check'] === 'function';
39
+
40
+ const CODE = /^X_[A-Z0-9_]+$/;
41
+
42
+ /**
43
+ * A value named in a cause, without ever throwing to name it — the one thing this validator may
44
+ * never do is fail while it is explaining a failure, because then a bug in an app's guard reaches
45
+ * its author as a stack trace out of framework internals. The rendering itself is
46
+ * `@ultimat3/core`'s `renderCauseValue`: the local copy this used to hold called `String(value)` on
47
+ * an unnarrowed `unknown`, so a guard returning an object with a throwing `toString` destroyed the
48
+ * refusal — the case a scan over `String(` cannot see, because the call is one helper away.
49
+ * The `typeof` prefix stays: "object null" and "number 42" say what a bare literal does not.
50
+ */
51
+ const shown = (value: unknown): string =>
52
+ typeof value === 'string' ? `"${value}"` : `${typeof value} ${renderCauseValue(value)}`;
53
+
54
+ /**
55
+ * Why a returned value is not a finding, or `undefined` when it is one. The `fix:` half is
56
+ * `fixProblem` — the identical rule `x verify`'s `errors` step applies to every shipped `fix:` in
57
+ * the framework — because a mechanism for producing errors that are not instructions is worse than
58
+ * no mechanism. It runs on the returned value rather than on the source, which is the half a
59
+ * static scan cannot reach: a `fix` assembled at run time has no literal to read.
60
+ */
61
+ export function findingProblem(value: unknown): string | undefined {
62
+ if (!isRecord(value)) return `${shown(value)} is not a finding object`;
63
+ const code = value['code'];
64
+ if (typeof code !== 'string' || !CODE.test(code)) {
65
+ return `${shown(code)} is not an X_SCREAMING_SNAKE code, so nothing can explain it`;
66
+ }
67
+ const cause = value['cause'];
68
+ if (typeof cause !== 'string' || cause.trim() === '') return `${code} states no cause`;
69
+ const fix = value['fix'];
70
+ if (typeof fix !== 'string') return `${code} carries no fix line`;
71
+ return fixProblem(fix);
72
+ }
73
+
74
+ /** Only what a finding may carry, so a guard cannot smuggle fields the renderers never show. */
75
+ function findingOf(value: Record<string, unknown>, at: string): Finding {
76
+ const docs = value['docs'];
77
+ const located = value['at'];
78
+ return {
79
+ code: value['code'] as string,
80
+ cause: value['cause'] as string,
81
+ fix: value['fix'] as string,
82
+ ...(typeof docs === 'string' ? { docs } : {}),
83
+ at: typeof located === 'string' && located !== '' ? located : at,
84
+ };
85
+ }
86
+
87
+ /**
88
+ * Every guard file, app-root-relative and sorted, so two machines report the same findings in the
89
+ * same order. A `*.test.ts` beside a guard is its test, never a second guard — the generator emits
90
+ * one, and importing it would run the suite inside the gate.
91
+ */
92
+ export async function guardPaths(root: string): Promise<readonly string[]> {
93
+ const dir = join(root, GUARD_DIR);
94
+ if (!existsSync(dir)) return [];
95
+ const paths: string[] = [];
96
+ for await (const entry of new Bun.Glob('*.{ts,tsx}').scan({ cwd: dir, absolute: false })) {
97
+ const path = entry.split('\\').join('/');
98
+ if (/\.(?:test|d)\.tsx?$/.test(path)) continue;
99
+ paths.push(`${GUARD_DIR}/${path}`);
100
+ }
101
+ return paths.sort();
102
+ }
103
+
104
+ const failed = (path: string, cause: string): Finding => ({
105
+ code: 'X_GUARD_FAILED',
106
+ cause,
107
+ fix: `return a finding from ${path} instead of throwing, then: x verify`,
108
+ docs: 'https://ultimate.dev/errors/X_GUARD_FAILED',
109
+ at: path,
110
+ });
111
+
112
+ const invalid = (path: string, cause: string): Finding => ({
113
+ code: 'X_GUARD_INVALID',
114
+ cause,
115
+ fix: `export a \`guard\` object — { summary, check } — from ${path}, then: x verify`,
116
+ docs: 'https://ultimate.dev/errors/X_GUARD_INVALID',
117
+ at: path,
118
+ });
119
+
120
+ const findingInvalid = (path: string, cause: string): Finding => ({
121
+ code: 'X_GUARD_FINDING_INVALID',
122
+ cause: `${path} returned a finding that is not one: ${cause}`,
123
+ fix: `rewrite what ${path} returns as a code, a cause and a fix naming a command or a file, then: x verify`,
124
+ docs: 'https://ultimate.dev/errors/X_GUARD_FINDING_INVALID',
125
+ at: path,
126
+ });
127
+
128
+ /** Same reason as `shown`: an app's guard may throw a value that fights every way of reading it. */
129
+ const messageOf = (error: unknown): string => renderThrowable(error);
130
+
131
+ /** One guard: import it, run it, and hold what it returns to the contract. Never throws. */
132
+ async function runGuard(root: string, path: string): Promise<readonly Finding[]> {
133
+ let loaded: unknown;
134
+ try {
135
+ loaded = await import(pathToFileURL(join(root, path)).href);
136
+ } catch (error) {
137
+ return [failed(path, `${path} could not be imported: ${messageOf(error)}`)];
138
+ }
139
+ const exported = isRecord(loaded) ? loaded['guard'] : undefined;
140
+ if (!isGuard(exported)) {
141
+ return [
142
+ invalid(
143
+ path,
144
+ exported === undefined
145
+ ? `${path} exports no \`guard\`, so a file in ${GUARD_DIR}/ enforces nothing`
146
+ : `${path} exports a \`guard\` with no summary and no check()`,
147
+ ),
148
+ ];
149
+ }
150
+ let returned: unknown;
151
+ try {
152
+ returned = await exported.check(root);
153
+ } catch (error) {
154
+ return [failed(path, `${path} ("${exported.summary}") threw: ${messageOf(error)}`)];
155
+ }
156
+ if (!Array.isArray(returned)) {
157
+ return [findingInvalid(path, `check() answered ${typeof returned}, not a list of findings`)];
158
+ }
159
+ const findings: Finding[] = [];
160
+ for (const candidate of returned as readonly unknown[]) {
161
+ // Reading a candidate can throw on its own — a getter that raises, a proxy that refuses — and
162
+ // `findingProblem` is total only for values it can read. Per candidate, so one unreadable
163
+ // entry costs its own line and not the readable findings beside it.
164
+ try {
165
+ const problem = findingProblem(candidate);
166
+ if (problem !== undefined) findings.push(findingInvalid(path, problem));
167
+ else findings.push(findingOf(candidate as Record<string, unknown>, path));
168
+ } catch (error) {
169
+ findings.push(findingInvalid(path, `it could not be read: ${messageOf(error)}`));
170
+ }
171
+ }
172
+ return findings;
173
+ }
174
+
175
+ /**
176
+ * Every guard this app declares, as findings the step it rides on adds to its own. Typed as a
177
+ * `HostCheck` because that is exactly the seam's shape — a rule the repo enforces on itself,
178
+ * contributed to a step that already exists. A guard can never add, remove, reorder or skip a
179
+ * step, which is what keeps "green" meaning one thing (axiom 5) while the app still gets to make
180
+ * its own convention a build error.
181
+ */
182
+ export const guardFindings: HostCheck = async (root) => {
183
+ const findings: Finding[] = [];
184
+ for (const path of await guardPaths(root)) findings.push(...(await runGuard(root, path)));
185
+ return findings;
186
+ };
package/src/index.ts CHANGED
@@ -2,6 +2,8 @@
2
2
  // on these, and a barrel that re-exports everything would make every internal a compatibility
3
3
  // promise.
4
4
 
5
+ /** The app's own API over HTTP — the one table `x dev` and a container both mount. */
6
+ export { apiRoutes } from './api-routes';
5
7
  export type { BoundaryCode, SourceFile } from './app-boundaries';
6
8
  export {
7
9
  appImportGraph,
@@ -32,7 +34,8 @@ export {
32
34
  readTarget,
33
35
  requireEntry,
34
36
  } from './cmd-build';
35
- export { branchDatabaseName, branchSql, dbCommand, previewUrl } from './cmd-db';
37
+ export { dbCommand } from './cmd-db';
38
+ export { runBranchCommand } from './cmd-db-branch';
36
39
  export type { DeployPlan } from './cmd-deploy';
37
40
  export { deployCommand, planDeploy } from './cmd-deploy';
38
41
  export type { DevServer, StartDevOptions } from './cmd-dev';
@@ -50,14 +53,30 @@ export type { McpHttpServer } from './cmd-mcp';
50
53
  export { mcpCommand, startMcpHttp } from './cmd-mcp';
51
54
  export type { NewAppOptions, WrittenApp } from './cmd-new';
52
55
  export { newCommand, planNewApp, writeNewApp } from './cmd-new';
53
- export type { PlannedCommand } from './cmd-planned';
54
- export { PLANNED_COMMANDS, plannedCommands } from './cmd-planned';
56
+ export type { PlannedCommand, PlannedSubcommand } from './cmd-planned';
57
+ export {
58
+ PLANNED_COMMANDS,
59
+ PLANNED_SUBCOMMANDS,
60
+ plannedCommands,
61
+ plannedSubcommand,
62
+ } from './cmd-planned';
55
63
  export { actionsCommand, entitiesCommand, queriesCommand } from './cmd-registries';
56
64
  export { renderRouteTable, routesCommand } from './cmd-routes';
57
- export { availableCpus, testCommand } from './cmd-test';
65
+ export { testCommand } from './cmd-test';
58
66
  export { runVerify, VERIFY_STEPS, verifyCommand, verifyStepNames } from './cmd-verify';
59
67
  export type { CliCommand, CommandContext } from './command';
60
68
  export { failed, ok } from './command';
69
+ export type { BranchRow, BranchSubcommand } from './db-branch';
70
+ export {
71
+ BRANCH_SUBCOMMANDS,
72
+ branchDatabaseName,
73
+ branchNameOf,
74
+ isBranchSubcommand,
75
+ pgliteBranchName,
76
+ previewUrl,
77
+ } from './db-branch';
78
+ export type { GeneratedFiles, GenerateMigrationOptions } from './db-generate';
79
+ export { generateAppMigration, migrationSql } from './db-generate';
61
80
  export type { AssetRoutesOptions } from './dev-assets';
62
81
  export {
63
82
  assetRoutes,
@@ -80,7 +99,8 @@ export type { DevServices, ServiceBinding } from './dev-services';
80
99
  export { describeServices, resolveServices } from './dev-services';
81
100
  export type { DispatchOptions } from './dispatch';
82
101
  export { dispatch } from './dispatch';
83
- export { checkDrift, recordedHashes, schemaHash, writeSchemaHash } from './drift';
102
+ export type { DeclaredEntityCount } from './drift';
103
+ export { checkSourceDrift, recordedHashes, schemaHash, writeSchemaHash } from './drift';
84
104
  export type { ErrorCatalog } from './error-catalog';
85
105
  export {
86
106
  buildErrorCatalog,
@@ -89,6 +109,8 @@ export {
89
109
  registeredErrorCodes,
90
110
  resetErrorCatalog,
91
111
  } from './error-catalog';
112
+ export type { CliErrorCode } from './error-codes';
113
+ export { CLI_ERROR_CODES, CLI_ERROR_TITLES } from './error-codes';
92
114
  export {
93
115
  BANNED_PHRASES,
94
116
  COMMAND_TOKENS,
@@ -102,19 +124,26 @@ export {
102
124
  RESERVED_HEADING,
103
125
  staticFix,
104
126
  } from './error-contract';
105
- export type { CliErrorCode } from './errors';
127
+ export type { CodeFixIndex, CodeFixScan } from './error-fixes';
128
+ export {
129
+ codeFixes,
130
+ codeFixScan,
131
+ loadCodeFixes,
132
+ resetCodeFixes,
133
+ scanScopeFixes,
134
+ } from './error-fixes';
106
135
  export {
107
136
  BadFlagError,
108
137
  BuildEntryMissingError,
109
138
  BunVersionError,
110
139
  CatalogExistsError,
111
- CLI_ERROR_CODES,
112
- CLI_ERROR_TITLES,
113
140
  CliNotImplementedError,
114
141
  DeclarationUnknownError,
115
142
  ErrorCodeUnknownError,
116
143
  FixTargetUnknownError,
117
144
  JobUnknownError,
145
+ MissingPositionalError,
146
+ MissingSubcommandError,
118
147
  NoTestFilesError,
119
148
  NotInAppError,
120
149
  PortInvalidError,
@@ -124,6 +153,16 @@ export {
124
153
  } from './errors';
125
154
  export type { ExecOptions, ExecResult, Runner } from './exec';
126
155
  export { exec, execOutput } from './exec';
156
+ export type { CitationFault, CitationRules, CommandCatalog, FixCitation } from './fix-command';
157
+ export {
158
+ citationFault,
159
+ citationProblem,
160
+ citedCommandProblem,
161
+ fixCitations,
162
+ loadCommandCatalog,
163
+ } from './fix-command';
164
+ export type { Guard } from './guards';
165
+ export { findingProblem, GUARD_DIR, guardFindings, guardPaths } from './guards';
127
166
  export type { DrainFailure, DrainOutcome, DrainSkip } from './jobs-drain';
128
167
  export { drainJobs } from './jobs-drain';
129
168
  export type { JobsListFilter, JobsListResult } from './jobs-report';
@@ -132,7 +171,16 @@ export { renderJobTable } from './jobs-table';
132
171
  export type { CliMcpServer, DevHostInput } from './mcp-host';
133
172
  export { createDevMcpServer, DEV_TOOL_SCOPES, localCaller } from './mcp-host';
134
173
  export { messageKeys, msg } from './messages';
135
- export { MIGRATIONS_DIR, migrationName, parseMigrationSql, readMigrations } from './migrations';
174
+ export type { MetricsEndpoint, MetricsEndpointOptions } from './metrics-endpoint';
175
+ export { DEFAULT_METRICS_PORT, startMetricsEndpoint } from './metrics-endpoint';
176
+ export {
177
+ hashFileName,
178
+ MIGRATIONS_DIR,
179
+ migrationName,
180
+ parseMigrationSql,
181
+ readMigrations,
182
+ snapshotFileName,
183
+ } from './migrations';
136
184
  export type { CommandResult, Finding, JsonValue, StepResult } from './output';
137
185
  export {
138
186
  exitCodeFor,
@@ -146,13 +194,19 @@ export {
146
194
  } from './output';
147
195
  export type { CommandSpec, FlagSpec, ParsedArgs } from './parse';
148
196
  export { flagBool, flagList, flagString, GLOBAL_FLAGS, nearest, parseArgs } from './parse';
149
- export type { PrerenderedPage, PrerenderOptions, PrerenderReport } from './prerender';
197
+ export type {
198
+ PrerenderedPage,
199
+ PrerenderOptions,
200
+ PrerenderReport,
201
+ UnmeasuredRoute,
202
+ } from './prerender';
150
203
  export { DEFAULT_ORIGIN, isPrerenderable, prerenderSite } from './prerender';
151
- export { CLI_VERSION, COMMANDS, commandFor, SPECS } from './registry';
204
+ export { COMMANDS, cliVersion, commandFor, SPECS } from './registry';
152
205
  export type { MigratedApp, ServedApp, ServeOptions, StartedApp } from './serve';
153
206
  export {
154
207
  CONTAINER_BINDING,
155
208
  DEFAULT_PORT,
209
+ metricsPortFromEnv,
156
210
  portFromEnv,
157
211
  roleFromEnv,
158
212
  runMigrations,
@@ -166,19 +220,36 @@ export {
166
220
  isVendored,
167
221
  SOURCE_GLOBS,
168
222
  } from './source-files';
223
+ export type { TestCounts } from './test-counts';
224
+ export { countsOf } from './test-counts';
169
225
  export type { TestFile } from './test-select';
170
226
  export { belongsToType, discoverTests, sampleFiles } from './test-select';
171
227
  export type { ReproduceOptions, RunShardsOptions, Shard } from './test-shards';
172
228
  export { planShards, quoteArg, reproduceFor, runShards, shardArgs } from './test-shards';
173
- export type { CodeSite, FixSite, SourceSite } from './ts-scan';
229
+ export { availableCpus, defaultWorkers, WORKER_CEILING } from './test-workers';
230
+ export type { CodeFixSite, CodeSite, FixSite, SourceSite } from './ts-scan';
174
231
  export {
175
232
  isCodeRegistry,
176
233
  maskLiterals,
177
234
  scanBorrowedCodes,
235
+ scanCodeFixSites,
178
236
  scanCodes,
179
237
  scanFixes,
180
238
  stripComments,
181
239
  } from './ts-scan';
240
+ // The one spelling rule for a `references` entry. Exported because the two gate scripts ask the
241
+ // same question this package's `package-shape` step does, and three answers is a duplicate entry.
242
+ export { normalizeReferencePath } from './tsconfig-references';
243
+ export type { VerifyFloor } from './verify-floor';
244
+ export {
245
+ floorProblemFindings,
246
+ floorRequires,
247
+ parseVerifyFloor,
248
+ readVerifyFloor,
249
+ skippedSuiteFinding,
250
+ VERIFY_FLOOR_FILE,
251
+ vanishedSuiteFinding,
252
+ } from './verify-floor';
182
253
  export type {
183
254
  HostCheck,
184
255
  StepOutcome,
@@ -188,8 +259,8 @@ export type {
188
259
  } from './verify-step';
189
260
  export { VERIFY_STEP_NAMES } from './verify-step';
190
261
  export type { TestType } from './verify-tests';
191
- export { TEST_STEPS, TEST_TYPES, testStepCommand, typeFilterOf } from './verify-tests';
192
- export type { ManifestFacts } from './workspace-checks';
262
+ export { TEST_STEPS, TEST_TYPES, testStepCommand, typeFiltersOf } from './verify-tests';
263
+ export type { ManifestFacts, PackageShapeOptions } from './workspace-checks';
193
264
  export {
194
265
  checkFileSizes,
195
266
  checkLockstep,
@@ -198,5 +269,7 @@ export {
198
269
  hasWorkspacePackages,
199
270
  LINE_CEILING,
200
271
  PACKAGE_FILES,
272
+ SEMVER,
201
273
  workspacePackages,
202
274
  } from './workspace-checks';
275
+ export { writeLine } from './write-line';