@ultimat3/cli 3.0.0 → 4.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 (52) hide show
  1. package/CLAUDE.md +69 -10
  2. package/package.json +24 -24
  3. package/src/budgets.ts +8 -5
  4. package/src/cmd-deploy.ts +42 -14
  5. package/src/cmd-docs.ts +7 -3
  6. package/src/cmd-fix.ts +15 -3
  7. package/src/cmd-generate.ts +29 -4
  8. package/src/cmd-help.ts +25 -4
  9. package/src/cmd-i18n.ts +8 -5
  10. package/src/cmd-jobs.ts +6 -5
  11. package/src/cmd-mcp.ts +16 -12
  12. package/src/cmd-new.ts +9 -13
  13. package/src/cmd-planned.ts +13 -0
  14. package/src/cmd-policy.ts +8 -6
  15. package/src/cmd-registries.ts +7 -6
  16. package/src/cmd-routes.ts +27 -4
  17. package/src/cmd-secrets.ts +6 -6
  18. package/src/cmd-verify.ts +55 -3
  19. package/src/command.ts +10 -2
  20. package/src/dev-cache.ts +9 -9
  21. package/src/dev-render.ts +6 -1
  22. package/src/dev-runtime.ts +2 -2
  23. package/src/dispatch.ts +33 -4
  24. package/src/error-codes.ts +5 -0
  25. package/src/error-contract.ts +31 -4
  26. package/src/fix-command.ts +9 -2
  27. package/src/fix-imports.ts +118 -0
  28. package/src/fix-scan.ts +251 -0
  29. package/src/flag-reads.ts +114 -0
  30. package/src/i18n-audit.ts +2 -1
  31. package/src/index.ts +8 -1
  32. package/src/jobs-drain.ts +6 -1
  33. package/src/mcp-errors.ts +5 -0
  34. package/src/mcp-host.ts +4 -2
  35. package/src/messages.ts +3 -0
  36. package/src/otlp-export.ts +14 -0
  37. package/src/parse.ts +6 -1
  38. package/src/seo-meta.ts +105 -0
  39. package/src/templates/action.ts +39 -7
  40. package/src/templates/backfill.ts +3 -1
  41. package/src/templates/index.ts +10 -1
  42. package/src/templates/job.ts +6 -2
  43. package/src/templates/query.ts +6 -1
  44. package/src/templates/route.ts +18 -9
  45. package/src/templates/scaffold-api.ts +100 -0
  46. package/src/templates/scaffold-app.ts +8 -48
  47. package/src/templates/scaffold-container.ts +44 -9
  48. package/src/templates/scaffold-helm-templates.ts +327 -0
  49. package/src/templates/scaffold-helm.ts +144 -0
  50. package/src/templates/scaffold-repo.ts +25 -8
  51. package/src/ts-scan.ts +12 -174
  52. package/src/verify-step.ts +5 -0
@@ -0,0 +1,118 @@
1
+ // A fix-building helper the calling file did not declare: `invalidIconDataError` lives in
2
+ // `packages/ui/src/errors.ts` and every fix it is handed is written in `icons/build-icons.ts`.
3
+ // One specifier, one file read, one parameter position — deliberately still not `tsc`.
4
+
5
+ // `dirname`/`join` are `node:`-only by necessity: Bun exposes no path-join primitive.
6
+ import { dirname, join } from 'node:path';
7
+ import type { FixHelper } from './fix-scan';
8
+ import { scanFixHelpers } from './fix-scan';
9
+ import { endOfLiteral, maskLiterals } from './ts-scan';
10
+
11
+ /**
12
+ * A named import, matched over the MASKED source and anchored at the start of a line, so an
13
+ * `import …` written inside a template literal is not read as one — `packages/cli/src/templates/`
14
+ * emits a dozen of them as generated app source, and resolving those pointed the scan at a module
15
+ * that only exists in the app the template writes. Masking blanks a literal's contents and keeps
16
+ * its delimiters, so the specifier is read back out of the raw source at the quote's own offset.
17
+ *
18
+ * `import type` is skipped whole: a type has no call site. A default or namespace import is not
19
+ * matched at all — this repo ships no default exports, and `errors.raise(…)` is a member access
20
+ * that `helperFixSites` refuses by design.
21
+ */
22
+ const IMPORT_CLAUSE = /^import\s+(type\s+)?\{([^}]*)\}\s*from\s*['"]/gm;
23
+
24
+ /** `a`, `a as b`, and the inline `type a` that carries no value. */
25
+ const parseClause = (clause: string): { readonly exported: string; readonly local: string }[] =>
26
+ clause
27
+ .split(',')
28
+ .map((part) => part.trim())
29
+ .filter((part) => part !== '' && !/^type\s/.test(part))
30
+ .map((part) => {
31
+ const [exported, local] = part.split(/\s+as\s+/);
32
+ return { exported: (exported ?? '').trim(), local: (local ?? exported ?? '').trim() };
33
+ })
34
+ .filter((name) => /^[A-Za-z_$][\w$]*$/.test(name.exported) && name.local !== '');
35
+
36
+ interface LocalImport {
37
+ readonly specifier: string;
38
+ readonly names: readonly { readonly exported: string; readonly local: string }[];
39
+ }
40
+
41
+ /** Every value import in this file, specifier as written. */
42
+ export function scanImports(source: string): readonly LocalImport[] {
43
+ const masked = maskLiterals(source);
44
+ const imports: LocalImport[] = [];
45
+ for (const match of masked.matchAll(IMPORT_CLAUSE)) {
46
+ if (match[1] !== undefined) continue;
47
+ const names = parseClause(match[2] ?? '');
48
+ const quote = match.index + match[0].length - 1;
49
+ const specifier = source.slice(quote + 1, endOfLiteral(masked, quote) - 1);
50
+ if (names.length > 0 && specifier !== '') imports.push({ specifier, names });
51
+ }
52
+ return imports;
53
+ }
54
+
55
+ /**
56
+ * The repo-relative paths a relative specifier could name, in resolution order. Only relative
57
+ * ones: `@ultimat3/db` and `node:path` are somebody else's file set, and a scanner that guessed
58
+ * which package a bare name came from would read an unrelated function's argument as a fix. That
59
+ * gap is real and named — `x verify`'s `errors` step counts what it could not read rather than
60
+ * passing over it silently, which is the failure this file exists to end.
61
+ */
62
+ export function candidatePaths(from: string, specifier: string): readonly string[] {
63
+ if (!specifier.startsWith('./') && !specifier.startsWith('../')) return [];
64
+ const base = join(dirname(from), specifier);
65
+ // A path that climbed out of the repo root is not a file this scan may open.
66
+ if (base.startsWith('..')) return [];
67
+ return [`${base}.ts`, `${base}.tsx`, join(base, 'index.ts'), join(base, 'index.tsx')];
68
+ }
69
+
70
+ /**
71
+ * The helpers one file can call, named as that file names them.
72
+ *
73
+ * Deliberately NOT also a count of the imports it could not open: that number is 1504 in this
74
+ * repo and 1310 of them are `@ultimat3/*` names like `join` and `UltimateError` — a figure nobody
75
+ * can act on. What the gate reports instead is `FixScan.unreadable`, which counts only arguments
76
+ * in a KNOWN fix position, and is therefore a count of fixes rather than of imports.
77
+ */
78
+ export type HelperResolver = (path: string, source: string) => Promise<readonly FixHelper[]>;
79
+
80
+ /**
81
+ * One resolver per run, because the module cache is the whole point: `packages/ui/src/errors.ts`
82
+ * is imported by every file in the package, and re-reading and re-scanning it per importer turns
83
+ * a one-pass walk into a quadratic one.
84
+ *
85
+ * A name declared in the imported module wins by NAME alone — no export check. The tree
86
+ * typechecks, so a name this file imports is a name that module exports; a second rule reading
87
+ * `export` keywords would only be able to disagree with `tsc`, never to add anything.
88
+ */
89
+ export function createHelperResolver(root: string): HelperResolver {
90
+ const modules = new Map<string, readonly FixHelper[] | undefined>();
91
+
92
+ const helpersIn = async (path: string): Promise<readonly FixHelper[] | undefined> => {
93
+ const cached = modules.get(path);
94
+ if (cached !== undefined || modules.has(path)) return cached;
95
+ const file = Bun.file(join(root, path));
96
+ const found = (await file.exists())
97
+ ? scanFixHelpers(maskLiterals(await file.text()))
98
+ : undefined;
99
+ modules.set(path, found);
100
+ return found;
101
+ };
102
+
103
+ return async (path, source) => {
104
+ const helpers: FixHelper[] = [];
105
+ for (const declaration of scanImports(source)) {
106
+ let declared: readonly FixHelper[] | undefined;
107
+ for (const candidate of candidatePaths(path, declaration.specifier)) {
108
+ declared = await helpersIn(candidate);
109
+ if (declared !== undefined) break;
110
+ }
111
+ for (const name of declaration.names) {
112
+ const helper = (declared ?? []).find((one) => one.name === name.exported);
113
+ if (helper !== undefined) helpers.push({ name: name.local, index: helper.index });
114
+ }
115
+ }
116
+ return helpers;
117
+ };
118
+ }
@@ -0,0 +1,251 @@
1
+ // Every string a `fix:` can evaluate to, in the three shapes a fix arrives in: under a key, in the
2
+ // argument position of a factory that builds an error, and in the argument position of an error
3
+ // class's constructor. Split out of `ts-scan.ts` when the third shape and cross-file resolution
4
+ // (`fix-imports.ts`) took the file past the 500-line ceiling; the masking primitives stay there.
5
+
6
+ import type { FixSite } from './ts-scan';
7
+ import {
8
+ CLOSERS,
9
+ endOfLiteral,
10
+ lineIndex,
11
+ maskLiterals,
12
+ OPENERS,
13
+ QUOTES,
14
+ valueLiterals,
15
+ } from './ts-scan';
16
+
17
+ /** The lookbehind rejects member access: `cond ? e.fix : ''` is a ternary, not a declaration. */
18
+ const FIX_KEY = /(?<![.\w$])fix\s*:\s*/g;
19
+
20
+ /** Text between the bracket at `open` and its match, or `undefined` when it never closes. */
21
+ function bracketSpan(masked: string, open: number): string | undefined {
22
+ let depth = 0;
23
+ for (let i = open; i < masked.length; i += 1) {
24
+ const ch = masked[i] as string;
25
+ // Only `()[]{}`. An angle bracket is a generic in a parameter list and the tail of `=>` in the
26
+ // very same list, so counting it makes `(fn: () => void)` end the span in the wrong place.
27
+ if (OPENERS.has(ch)) depth += 1;
28
+ else if (CLOSERS.has(ch)) {
29
+ depth -= 1;
30
+ if (depth === 0) return masked.slice(open + 1, i);
31
+ }
32
+ }
33
+ return undefined;
34
+ }
35
+
36
+ /** Split at depth-0 commas. Safe on masked text, where a comma inside a literal is already gone. */
37
+ function topLevelParts(text: string): readonly string[] {
38
+ const parts: string[] = [];
39
+ let depth = 0;
40
+ let start = 0;
41
+ for (let i = 0; i < text.length; i += 1) {
42
+ const ch = text[i] as string;
43
+ if (OPENERS.has(ch)) depth += 1;
44
+ else if (CLOSERS.has(ch)) depth -= 1;
45
+ else if (ch === ',' && depth === 0) {
46
+ parts.push(text.slice(start, i));
47
+ start = i + 1;
48
+ }
49
+ }
50
+ parts.push(text.slice(start));
51
+ return parts;
52
+ }
53
+
54
+ /** A callable that builds an error and takes its fix positionally, and where in its list. */
55
+ export interface FixHelper {
56
+ readonly name: string;
57
+ readonly index: number;
58
+ }
59
+
60
+ const HELPER_DECL =
61
+ /(?<![.\w$])(?:function\s+([A-Za-z_$][\w$]*)\s*\(|(?:const|let)\s+([A-Za-z_$][\w$]*)\s*(?::[^=;]*)?=\s*(?:async\s+)?\()/g;
62
+
63
+ /**
64
+ * A class, and — because the helper's NAME is the class's and the parameter list is its
65
+ * constructor's — the two are found in two steps rather than one regex. `@ultimat3/render`'s
66
+ * `errors.ts` declares fourteen of these taking `(cause, fix)` positionally, and until this the
67
+ * form was unread everywhere: measured over the tree, it has ZERO same-file call sites, so the
68
+ * same-file rule was dead code for it and 15 of render's codes never had a fix line checked.
69
+ */
70
+ const CLASS_DECL = /(?<![.\w$])class\s+([A-Za-z_$][\w$]*)[^{;]*\{/g;
71
+ const CONSTRUCTOR = /(?<![.\w$])constructor\s*\(/;
72
+
73
+ const FIX_PARAM = /^\s*fix\s*:\s*string\s*$/;
74
+
75
+ /**
76
+ * What separates a helper that BUILDS a fix from one that CONSUMES one. `citedCommandProblem(fix:
77
+ * string, …)` in `fix-command.ts` takes a fix in order to judge it, and reading its call sites as
78
+ * declarations would report findings about strings that are already findings. A builder names a
79
+ * `code` or constructs an `…Error`; a consumer does neither.
80
+ */
81
+ const BUILDS_ERROR = /(?<![.\w$])code\s*[:=]|new\s+[A-Za-z_$][\w$]*Error\s*\(/;
82
+
83
+ /**
84
+ * The body of the declaration whose parameter list ends at `after`, and never a `{` belonging to
85
+ * something below it. An unbounded `indexOf('{')` reads the next object literal in the FILE when
86
+ * the body is a concise expression, so `const label = (fix: string) => fix.trim();` followed
87
+ * anywhere by a `{ code: … }` was read as an error builder and every `label(…)` call handed the
88
+ * gate a string to judge as a fix — a false gate failure over innocent source.
89
+ *
90
+ * The scan therefore ends at the `;` that ends the declaration, or at a bracket closing a scope
91
+ * this declaration is inside. Both directions of that bound answer `''`, which classifies the
92
+ * helper as a non-builder: a missed fix line costs one unchecked citation, a wrongly claimed one
93
+ * costs a build. A `{` inside a return-type annotation (`(): { ok: boolean } => …`) is read as the
94
+ * body and answers `''` for the same reason.
95
+ */
96
+ function bodyOf(masked: string, after: number): string {
97
+ for (let i = after; i < masked.length; i += 1) {
98
+ const ch = masked[i] as string;
99
+ if (ch === '{') return bracketSpan(masked, i) ?? '';
100
+ if (ch === ';' || CLOSERS.has(ch)) break;
101
+ }
102
+ return '';
103
+ }
104
+
105
+ /**
106
+ * One declaration judged: it must build an error, and its fix must sit at a position a call site
107
+ * can be read at. A rest parameter makes the position of everything after it unknowable, and a
108
+ * destructured one has no position at all — its `fix:` key at the CALL site is already read by
109
+ * `FIX_KEY`.
110
+ */
111
+ function helperAt(masked: string, name: string, open: number): FixHelper | undefined {
112
+ const params = bracketSpan(masked, open);
113
+ if (params === undefined || params.includes('...')) return undefined;
114
+ const parts = topLevelParts(params);
115
+ if (parts.some((part) => /^\s*[[{]/.test(part))) return undefined;
116
+ const index = parts.findIndex((part) => FIX_PARAM.test(part));
117
+ if (index === -1) return undefined;
118
+ if (!BUILDS_ERROR.test(bodyOf(masked, open + params.length + 2))) return undefined;
119
+ return { name, index };
120
+ }
121
+
122
+ /**
123
+ * Every fix-building callable this file declares — a function, an arrow bound to a const, or a
124
+ * class whose constructor takes the fix. Exported because `fix-imports.ts` asks the same question
125
+ * of a file this one merely imports FROM; there is no second reader of a declaration.
126
+ */
127
+ export function scanFixHelpers(masked: string): readonly FixHelper[] {
128
+ const helpers: FixHelper[] = [];
129
+ for (const declaration of masked.matchAll(HELPER_DECL)) {
130
+ const name = declaration[1] ?? declaration[2];
131
+ const open = declaration.index + declaration[0].length - 1;
132
+ if (name === undefined || masked[open] !== '(') continue;
133
+ const helper = helperAt(masked, name, open);
134
+ if (helper !== undefined) helpers.push(helper);
135
+ }
136
+ for (const declaration of masked.matchAll(CLASS_DECL)) {
137
+ const name = declaration[1];
138
+ const body = declaration.index + declaration[0].length - 1;
139
+ const inner = bracketSpan(masked, body);
140
+ const found = inner === undefined ? null : CONSTRUCTOR.exec(inner);
141
+ if (name === undefined || found === null) continue;
142
+ const helper = helperAt(masked, name, body + 1 + found.index + found[0].length - 1);
143
+ if (helper !== undefined) helpers.push(helper);
144
+ }
145
+ return helpers;
146
+ }
147
+
148
+ /**
149
+ * A fix argument this scan could not read, at a call site it could: the callee is a known helper
150
+ * and the fix position holds something other than one literal. Counted rather than dropped, so
151
+ * `x verify`'s `errors` step can say "checked 412, could not read 27" — a gate that stays silent
152
+ * about its own blind spot is the false green this file exists to close (axiom 4 applies to it too).
153
+ */
154
+ export interface FixScan {
155
+ readonly sites: readonly FixSite[];
156
+ readonly unreadable: number;
157
+ }
158
+
159
+ /**
160
+ * The argument in that position at every call to that helper in this file. `new X(…)` is a call
161
+ * like any other here: the lookbehind sees the space after `new`, so a class needs no second rule.
162
+ */
163
+ function helperFixSites(
164
+ masked: string,
165
+ source: string,
166
+ at: string,
167
+ helper: FixHelper,
168
+ lineAt: (index: number) => number,
169
+ unreadable: { count: number },
170
+ ): readonly FixSite[] {
171
+ const sites: FixSite[] = [];
172
+ // The lookbehind is `FIX_KEY`'s: `reporter.rejected(…)` is some other object's method.
173
+ const call = new RegExp(`(?<![.\\w$])${helper.name}\\s*\\(`, 'g');
174
+ for (const match of masked.matchAll(call)) {
175
+ const open = match.index + match[0].length - 1;
176
+ const args = bracketSpan(masked, open);
177
+ if (args === undefined) continue;
178
+ const parts = topLevelParts(args);
179
+ const argument = parts[helper.index];
180
+ // A call one argument short passes no fix at all — nothing was written here to read.
181
+ if (argument === undefined) continue;
182
+ // Stricter than the `fix:` path, and deliberately: the whole argument must BE one literal.
183
+ // `valueLiterals` alone reads `prefix + 'x doctor'` as one literal, because the identifier
184
+ // half contributes none — and publishing half a fix as the whole one is the failure
185
+ // `soleLiteral` already names. A key at least declares that what follows is the value.
186
+ const quote = argument.trim()[0];
187
+ if (quote === undefined || !QUOTES.has(quote)) {
188
+ unreadable.count += 1;
189
+ continue;
190
+ }
191
+ const literal = argument.indexOf(quote);
192
+ if (argument.slice(endOfLiteral(argument, literal)).trim() !== '') {
193
+ unreadable.count += 1;
194
+ continue;
195
+ }
196
+ const from = parts.slice(0, helper.index).reduce((n, part) => n + part.length + 1, open + 1);
197
+ const literals = valueLiterals(masked, source, from, lineAt);
198
+ if (literals.length === 1) sites.push({ ...(literals[0] as FixSite), at });
199
+ else unreadable.count += 1;
200
+ }
201
+ return sites;
202
+ }
203
+
204
+ /**
205
+ * Every string a `fix:` can evaluate to. Searched over the masked source, so a `fix:` written
206
+ * inside a doc comment or interpolated into a message is not mistaken for a declaration. A `fix`
207
+ * computed at runtime — a bare identifier, a parameter, a table lookup with no literal fallback —
208
+ * has nothing to read and is beyond a static scan; the gate says so rather than guessing.
209
+ *
210
+ * Three shapes, because a fix does not always arrive under a key. `@ultimat3/mcp`'s
211
+ * `readonly-sql.ts` hands every one of its fixes positionally to a local `rejected(cause, fix)`
212
+ * helper, so the key rule alone returned `[]` for the whole file — 20 non-test files in that
213
+ * package and the scanner saw fixes in three — and two stale `x db branch <name>` lines shipped
214
+ * through the hole.
215
+ *
216
+ * `imported` is the third: the helpers this file can call that it did not declare, resolved by
217
+ * `fix-imports.ts` and passed in, because a scanner over one string cannot open a second file.
218
+ * The four rules a call site is read under are `helperAt`'s and do not change with where the
219
+ * declaration was found.
220
+ */
221
+ export function scanFixSites(
222
+ source: string,
223
+ at: string,
224
+ imported: readonly FixHelper[] = [],
225
+ ): FixScan {
226
+ const unreadable = { count: 0 };
227
+ const masked = maskLiterals(source);
228
+ const lineAt = lineIndex(masked);
229
+ const sites: FixSite[] = [];
230
+ for (const key of masked.matchAll(FIX_KEY)) {
231
+ const start = key.index + key[0].length;
232
+ for (const literal of valueLiterals(masked, source, start, lineAt)) {
233
+ sites.push({ ...literal, at });
234
+ }
235
+ }
236
+ // A name declared here wins over one imported under the same name: the declaration is what a
237
+ // call in this file actually reaches, and reading both would report one argument twice.
238
+ const local = scanFixHelpers(masked);
239
+ const names = new Set(local.map((helper) => helper.name));
240
+ for (const helper of [...local, ...imported.filter((one) => !names.has(one.name))]) {
241
+ sites.push(...helperFixSites(masked, source, at, helper, lineAt, unreadable));
242
+ }
243
+ return { sites, unreadable: unreadable.count };
244
+ }
245
+
246
+ /** The sites alone, for every caller that has no second file to resolve an import against. */
247
+ export const scanFixes = (
248
+ source: string,
249
+ at: string,
250
+ imported: readonly FixHelper[] = [],
251
+ ): readonly FixSite[] => scanFixSites(source, at, imported).sites;
@@ -0,0 +1,114 @@
1
+ // A flag a command declares and nothing reads. The parser accepts every declared flag, so a flag
2
+ // with no reader is not a parse error and not a type error — it is a promise in the help text with
3
+ // no code behind it, and only a rule over the two halves together can see that.
4
+ //
5
+ // The bound of this rule, stated where it is enforced: it sees the flag NAME reaching a reader,
6
+ // not the value reaching an effect. `x deploy --critical` satisfied it by being written into the
7
+ // plan JSON, where nothing read the field; that flag is deleted rather than wired, and a second
8
+ // one of its shape would pass here too.
9
+
10
+ // `join`/`relative` are `node:`-only by necessity: Bun exposes no path-join primitive.
11
+ import { join, relative } from 'node:path';
12
+ import { docsFor } from './error-codes';
13
+ import type { Finding } from './output';
14
+ import type { CommandSpec, FlagSpec } from './parse';
15
+ import { GLOBAL_FLAGS } from './parse';
16
+ import { stripComments } from './ts-scan';
17
+
18
+ /** A flag as declared, with the command that declares it. */
19
+ export interface DeclaredFlag {
20
+ readonly command: string;
21
+ readonly flag: FlagSpec;
22
+ }
23
+
24
+ /**
25
+ * Every flag a command declares. The global four are excluded: `--json`, `--help`, `--cwd` and
26
+ * `--verbose` are the parser's and the dispatcher's, read once for every command rather than by
27
+ * the command that lists them, and a per-command rule would report all 30 of them as unread.
28
+ */
29
+ export function declaredFlags(specs: readonly CommandSpec[]): readonly DeclaredFlag[] {
30
+ const global = new Set(GLOBAL_FLAGS.map((flag) => flag.name));
31
+ return specs.flatMap((spec) =>
32
+ (spec.flags ?? [])
33
+ .filter((flag) => !global.has(flag.name))
34
+ .map((flag) => ({ command: spec.name, flag })),
35
+ );
36
+ }
37
+
38
+ /** A flag name is `[a-z][a-z-]*`, so nothing in it is a regex metacharacter to escape. */
39
+ const literalOf = (name: string): RegExp => new RegExp(`(['"\`])${name}\\1`, 'g');
40
+
41
+ /**
42
+ * The declaration itself, which is never a read. `{ name: 'critical', … }` and its `short:` twin
43
+ * are the two places the name appears as a spec field; every other occurrence of the bare literal
44
+ * is a reader — `flagBool(ctx.args, 'critical')`, a table key, a constant the reader indexes with.
45
+ */
46
+ const declarationOf = (name: string): RegExp =>
47
+ new RegExp(`(?:name|short)\\s*:\\s*(['"\`])${name}\\1`, 'g');
48
+
49
+ const countIn = (text: string, pattern: RegExp): number => [...text.matchAll(pattern)].length;
50
+
51
+ /**
52
+ * Whether this file reads the flag, as against merely declaring it. Deliberately generous: a flag
53
+ * consumed only by being echoed into `--json`, or read through a shared constant rather than by
54
+ * name at the call site, is still read — the rule exists to catch a flag NOTHING mentions, and a
55
+ * gate that guessed at intent would report findings about working commands.
56
+ */
57
+ export const readsFlag = (text: string, name: string): boolean =>
58
+ countIn(text, literalOf(name)) > countIn(text, declarationOf(name));
59
+
60
+ const declaresFlag = (text: string, name: string): boolean =>
61
+ countIn(text, declarationOf(name)) > 0;
62
+
63
+ const unreadFinding = (declared: DeclaredFlag, at: string): Finding => ({
64
+ code: 'X_CLI_FLAG_UNREAD',
65
+ cause: `x ${declared.command} declares --${declared.flag.name} ("${declared.flag.summary}") and no file in the CLI's source reads it, so the flag parses and changes nothing`,
66
+ fix: `read it in ${at} with flag${declared.flag.type === 'boolean' ? 'Bool' : 'String'}(ctx.args, '${declared.flag.name}'), or delete it from the spec's flags`,
67
+ docs: docsFor('X_CLI_FLAG_UNREAD'),
68
+ at,
69
+ });
70
+
71
+ /**
72
+ * Every declared flag held to one rule: something reads it.
73
+ *
74
+ * Scans source rather than the runtime, because "is this value ever consumed?" is not a question
75
+ * a `run` can be asked without running it — and running every command is not a check, it is the
76
+ * program. Comments are stripped first: a flag named only in the prose above the spec is not read,
77
+ * and a scanner that counted it would pass exactly the flags most likely to be dead.
78
+ */
79
+ export async function checkFlagReads(
80
+ specs: readonly CommandSpec[],
81
+ srcDir: string,
82
+ ): Promise<readonly Finding[]> {
83
+ const paths: string[] = [];
84
+ try {
85
+ for await (const path of new Bun.Glob('**/*.ts').scan({ cwd: srcDir, absolute: false })) {
86
+ if (!/\.test\.tsx?$/.test(path)) paths.push(path);
87
+ }
88
+ } catch {
89
+ // The directory is not there. `Bun.Glob.scan` raises rather than yielding nothing, so the
90
+ // absent case has to be caught here — see the `texts.size` guard below for why it answers [].
91
+ // The scan is ALL that is inside the `try`, deliberately: a file the scan found and this
92
+ // cannot read must propagate, or an unreadable source answers "no findings" and the rule
93
+ // reports green over the half it could not see.
94
+ return [];
95
+ }
96
+ const texts = new Map<string, string>();
97
+ for (const path of paths) {
98
+ texts.set(path, stripComments(await Bun.file(join(srcDir, path)).text()));
99
+ }
100
+ // No CLI source under this root: the rule holds two halves against each other and only one is
101
+ // here, so there is nothing it can decide. Derived, not "is this the framework repo" — the same
102
+ // condition `scripts/release-workflow.ts` uses for a tree with no publishable workspace. Scanning
103
+ // on would report EVERY declared flag as unread, which is the false-positive direction and the
104
+ // one that trains a reader to ignore the check.
105
+ if (texts.size === 0) return [];
106
+ const findings: Finding[] = [];
107
+ for (const declared of declaredFlags(specs)) {
108
+ const name = declared.flag.name;
109
+ if ([...texts.values()].some((text) => readsFlag(text, name))) continue;
110
+ const declaringFile = [...texts].find(([, text]) => declaresFlag(text, name))?.[0];
111
+ findings.push(unreadFinding(declared, join(relative('', srcDir), declaringFile ?? '')));
112
+ }
113
+ return findings;
114
+ }
package/src/i18n-audit.ts CHANGED
@@ -8,6 +8,7 @@
8
8
  // root-relative POSIX shape every CLI-reported path is keyed by.
9
9
  import { existsSync } from 'node:fs';
10
10
  import { join, relative, sep } from 'node:path';
11
+ import { renderThrowable } from '@ultimat3/core';
11
12
  import type { Catalog, Extraction, ExtractReport, Locale } from '@ultimat3/i18n';
12
13
  import {
13
14
  auditCatalogs,
@@ -51,7 +52,7 @@ function parseCatalogJson(path: string, raw: string): unknown {
51
52
  try {
52
53
  return JSON.parse(raw);
53
54
  } catch (error) {
54
- throw catalogInvalid(path, error instanceof Error ? error.message : String(error));
55
+ throw catalogInvalid(path, renderThrowable(error));
55
56
  }
56
57
  }
57
58
 
package/src/index.ts CHANGED
@@ -117,12 +117,14 @@ export {
117
117
  } from './error-catalog';
118
118
  export type { CliErrorCode } from './error-codes';
119
119
  export { CLI_ERROR_CODES, CLI_ERROR_TITLES } from './error-codes';
120
+ export type { ErrorFixReport } from './error-contract';
120
121
  export {
121
122
  BANNED_PHRASES,
122
123
  COMMAND_TOKENS,
123
124
  checkErrorCodeDocs,
124
125
  checkErrorCodeRegistry,
125
126
  checkErrorFixes,
127
+ checkErrorFixReport,
126
128
  collectDeclaredCodes,
127
129
  documentedCodes,
128
130
  fixProblem,
@@ -167,6 +169,12 @@ export {
167
169
  fixCitations,
168
170
  loadCommandCatalog,
169
171
  } from './fix-command';
172
+ export type { HelperResolver } from './fix-imports';
173
+ export { candidatePaths, createHelperResolver, scanImports } from './fix-imports';
174
+ export type { FixHelper, FixScan } from './fix-scan';
175
+ export { scanFixes, scanFixHelpers, scanFixSites } from './fix-scan';
176
+ export type { DeclaredFlag } from './flag-reads';
177
+ export { checkFlagReads, declaredFlags, readsFlag } from './flag-reads';
170
178
  export type { Guard } from './guards';
171
179
  export { findingProblem, GUARD_DIR, guardFindings, guardPaths } from './guards';
172
180
  export type { DrainFailure, DrainOutcome, DrainSkip } from './jobs-drain';
@@ -241,7 +249,6 @@ export {
241
249
  scanBorrowedCodes,
242
250
  scanCodeFixSites,
243
251
  scanCodes,
244
- scanFixes,
245
252
  stripComments,
246
253
  } from './ts-scan';
247
254
  // The one spelling rule for a `references` entry. Exported because the two gate scripts ask the
package/src/jobs-drain.ts CHANGED
@@ -75,7 +75,12 @@ async function copySteps(source: JobDriver, target: JobDriver, runId: string): P
75
75
  /**
76
76
  * Hand a leased job back exactly as the drain found it. `countsAsAttempt: false` is the point:
77
77
  * a transfer that failed is not a failed attempt, and burning one per `x jobs drain` retry would
78
- * dead-letter a job nobody ever ran. It parks as `suspended`, which every driver claims.
78
+ * dead-letter a job nobody ever ran.
79
+ *
80
+ * It returns to `ready`, which is what "as the drain found it" means — the drain leased a ready
81
+ * job and could not move it. It used to land in `suspended`, not by intent but because
82
+ * `countsAsAttempt: false` was the only bit the drivers had and `step.sleep` had claimed it;
83
+ * `NackOptions.park` now carries the suspension, so the two callers no longer share one meaning.
79
84
  */
80
85
  async function releaseLease(source: JobDriver, id: string): Promise<void> {
81
86
  try {
package/src/mcp-errors.ts CHANGED
@@ -22,6 +22,11 @@ const CLI_FIXES: Readonly<Record<CliErrorCode, string>> = {
22
22
  // Runnable first, the narrowing behind a `#`: `x help <command> --json` pasted into a shell
23
23
  // is a redirect, not a command, and this table is copied verbatim by whoever reads it.
24
24
  X_CLI_BAD_FLAG: 'x help --json # then narrow to the command the cause names',
25
+ // Not an `x` command: this rule is about the CLI's OWN declarations, it can only fire in this
26
+ // repo, and the suite that applies it is what reproduces the finding. A placeholder command
27
+ // would fail this table's own no-`<placeholder>` rule, and rightly — it would not run.
28
+ X_CLI_FLAG_UNREAD:
29
+ 'bun test packages/cli/src/flag-reads.test.ts # the finding names the flag and the file to read it in',
25
30
  X_VERIFY_FAILED: 'x verify --json',
26
31
  X_NOT_IN_APP: 'x new myapp --json && cd myapp',
27
32
  X_BUN_VERSION: 'bun upgrade',
package/src/mcp-host.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  // is a second catalog of routes, entities, actions, queries or jobs.
5
5
 
6
6
  import { join } from 'node:path';
7
- import { agentActor, isUltimateError, UltimateError } from '@ultimat3/core';
7
+ import { agentActor, isUltimateError, renderThrowable, UltimateError } from '@ultimat3/core';
8
8
  import type { DbClient } from '@ultimat3/db';
9
9
  import {
10
10
  ensureReadOnlyRole,
@@ -199,7 +199,9 @@ function capabilities(input: DevHostInput, lazy: LazyServices): DevCapabilities
199
199
  if (isUltimateError(error)) throw error;
200
200
  throw new UltimateError({
201
201
  code: 'X_DB_MIGRATE_FAILED',
202
- cause: error instanceof Error ? error.message : String(error),
202
+ // The blessed total renderer: `String(error)` runs the value's own `toString`, and
203
+ // this is the last hop before an agent is handed the three-line result.
204
+ cause: renderThrowable(error),
203
205
  fix: 'x db reset',
204
206
  });
205
207
  }
package/src/messages.ts CHANGED
@@ -170,6 +170,9 @@ const CATALOG = {
170
170
  'cli.verify.passSkipped':
171
171
  '{passed} of {count} steps passed in {ms}ms — {skipped} skipped: {names}',
172
172
  'cli.verify.failSkipped': '{failed} of {count} steps failed — {skipped} skipped: {names}',
173
+ // The `errors` step's own coverage, in `output`: a scan without a parser reads most fix lines
174
+ // and not all of them, and a step that reports findings alone claims a completeness it lacks.
175
+ 'cli.verify.fixCoverage': 'checked {checked} fix line(s), could not read {unreadable}',
173
176
  'cli.verify.serial': 'serial',
174
177
  'cli.verify.workers': '{workers} workers',
175
178
  'cli.env.checked': '{count} declared variable(s), all present and valid',
@@ -7,6 +7,8 @@ import {
7
7
  configureMetrics,
8
8
  configureTelemetry,
9
9
  logger,
10
+ noopExporter,
11
+ noopMetricExporter,
10
12
  onShutdown,
11
13
  otlpMetricExporter,
12
14
  otlpSpanExporter,
@@ -29,6 +31,14 @@ export const METRIC_EXPORT_INTERVAL_MS = 60_000;
29
31
  * Both are registered with `onShutdown(..., { phase: 'close' })`: the last spans of a drain are
30
32
  * the ones that explain the drain, and a process that exits with a full queue loses exactly the
31
33
  * window an operator went looking for.
34
+ *
35
+ * The release UNINSTALLS what it installed, per signal. `configureTelemetry`/`configureMetrics`
36
+ * merge into process-global state, so stopping the timer and dropping the drain hooks left the
37
+ * first boot's exporter configured: a second `serveApp` in the same process exported its spans
38
+ * into a released exporter — queued against a collector nothing will flush to, on a timer nothing
39
+ * clears. `cmd-dev.ts`'s `stop()` hands back `noopExporter` for exactly this reason. Per signal and
40
+ * never unconditionally: `x dev` configures a trace RECORDER before calling this, and a boot that
41
+ * installed no exporter must not uninstall one it never owned.
32
42
  */
33
43
  export function startOtlpExport(env: Env = process.env): () => void {
34
44
  const releases: (() => void)[] = [];
@@ -41,6 +51,9 @@ export function startOtlpExport(env: Env = process.env): () => void {
41
51
  if (traces !== undefined) {
42
52
  const exporter = otlpSpanExporter({ endpoint: traces });
43
53
  configureTelemetry({ exporter });
54
+ // Pushed first, so the reversed run below applies it LAST — after the drain hook is dropped,
55
+ // the same order `cmd-dev.ts` releases the recorder in.
56
+ releases.push(() => configureTelemetry({ exporter: noopExporter }));
44
57
  releases.push(onShutdown('otlp-traces', () => exporter.shutdown(), { phase: 'close' }));
45
58
  logger.info('ultimate otlp traces', { endpoint: traces });
46
59
  }
@@ -49,6 +62,7 @@ export function startOtlpExport(env: Env = process.env): () => void {
49
62
  if (metrics !== undefined) {
50
63
  const exporter = otlpMetricExporter({ endpoint: metrics });
51
64
  configureMetrics({ exporter });
65
+ releases.push(() => configureMetrics({ exporter: noopMetricExporter }));
52
66
  // The push loop, and not only the exporter: `configureMetrics` names where a snapshot goes
53
67
  // and nothing decides when one is taken, so without this the collector receives one export —
54
68
  // the drain's — for the whole life of the process.
package/src/parse.ts CHANGED
@@ -47,7 +47,12 @@ export interface CommandSpec {
47
47
  */
48
48
  readonly subcommandPositionals?: Readonly<Record<string, readonly string[]>>;
49
49
  readonly flags?: readonly FlagSpec[];
50
- /** Command needs an app root (`app.config.ts`) — the dispatcher enforces it. */
50
+ /**
51
+ * Command needs an app root (`app.config.ts`). The dispatcher enforces it — `dispatch.ts`, before
52
+ * `target.run` — and until 2026-08 nothing read this field at all: the guarantee was kept only by
53
+ * each of the 17 declaring commands remembering to call `requireAppRoot` itself, so a new command
54
+ * that declared it and forgot the call ran outside an app with no refusal.
55
+ */
51
56
  readonly requiresApp?: boolean;
52
57
  }
53
58