@ultimat3/cli 2.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 (75) hide show
  1. package/CLAUDE.md +109 -13
  2. package/README.md +1 -0
  3. package/package.json +24 -24
  4. package/src/budgets.ts +31 -8
  5. package/src/cmd-db-branch.ts +6 -2
  6. package/src/cmd-db.ts +138 -10
  7. package/src/cmd-deploy.ts +42 -14
  8. package/src/cmd-dev.ts +9 -2
  9. package/src/cmd-docs.ts +7 -3
  10. package/src/cmd-doctor.ts +16 -7
  11. package/src/cmd-fix.ts +15 -3
  12. package/src/cmd-generate.ts +29 -4
  13. package/src/cmd-help.ts +25 -4
  14. package/src/cmd-i18n.ts +8 -5
  15. package/src/cmd-jobs.ts +6 -5
  16. package/src/cmd-mcp.ts +16 -12
  17. package/src/cmd-new.ts +10 -14
  18. package/src/cmd-planned.ts +13 -0
  19. package/src/cmd-policy.ts +8 -6
  20. package/src/cmd-registries.ts +7 -6
  21. package/src/cmd-routes.ts +27 -4
  22. package/src/cmd-secrets.ts +6 -6
  23. package/src/cmd-test.ts +14 -3
  24. package/src/cmd-verify.ts +80 -10
  25. package/src/command.ts +10 -2
  26. package/src/db-branch.ts +18 -0
  27. package/src/db-generate.ts +38 -6
  28. package/src/db-seed.ts +294 -0
  29. package/src/dev-assets.ts +22 -3
  30. package/src/dev-cache.ts +9 -9
  31. package/src/dev-render.ts +6 -1
  32. package/src/dev-roles.ts +5 -3
  33. package/src/dev-runtime.ts +2 -2
  34. package/src/dev-storage.ts +6 -4
  35. package/src/dev-traces.ts +26 -4
  36. package/src/dispatch.ts +33 -4
  37. package/src/drift.ts +41 -1
  38. package/src/error-catalog.ts +1 -0
  39. package/src/error-codes.ts +11 -0
  40. package/src/error-contract.ts +31 -4
  41. package/src/exec.ts +42 -8
  42. package/src/fix-command.ts +9 -2
  43. package/src/fix-imports.ts +118 -0
  44. package/src/fix-scan.ts +251 -0
  45. package/src/flag-number.ts +11 -0
  46. package/src/flag-reads.ts +114 -0
  47. package/src/i18n-audit.ts +2 -1
  48. package/src/index.ts +19 -5
  49. package/src/jobs-drain.ts +6 -1
  50. package/src/mcp-errors.ts +13 -0
  51. package/src/mcp-host.ts +4 -2
  52. package/src/messages.ts +15 -0
  53. package/src/metrics-endpoint.ts +60 -13
  54. package/src/otlp-export.ts +14 -0
  55. package/src/parse.ts +6 -1
  56. package/src/seo-meta.ts +105 -0
  57. package/src/serve.ts +15 -3
  58. package/src/shell-quote.ts +15 -0
  59. package/src/templates/action.ts +39 -7
  60. package/src/templates/backfill.ts +3 -1
  61. package/src/templates/index.ts +10 -1
  62. package/src/templates/job.ts +6 -2
  63. package/src/templates/query.ts +6 -1
  64. package/src/templates/route.ts +18 -9
  65. package/src/templates/scaffold-api.ts +100 -0
  66. package/src/templates/scaffold-app.ts +8 -48
  67. package/src/templates/scaffold-container.ts +44 -9
  68. package/src/templates/scaffold-helm-templates.ts +327 -0
  69. package/src/templates/scaffold-helm.ts +144 -0
  70. package/src/templates/scaffold-repo.ts +25 -8
  71. package/src/test-shards.ts +1 -10
  72. package/src/test-workers.ts +4 -1
  73. package/src/ts-scan.ts +25 -176
  74. package/src/tsconfig-references.ts +27 -2
  75. package/src/verify-step.ts +5 -0
@@ -36,6 +36,7 @@ export const CATALOG_PACKAGES = [
36
36
  '@ultimat3/realtime',
37
37
  '@ultimat3/render',
38
38
  '@ultimat3/schema',
39
+ '@ultimat3/scraping',
39
40
  '@ultimat3/seo',
40
41
  '@ultimat3/storage',
41
42
  '@ultimat3/testing',
@@ -67,6 +67,11 @@ export const CLI_OWNED_ERROR_CODES = [
67
67
  'X_DB_MIGRATE_FAILED',
68
68
  'X_DB_BRANCH_FAILED',
69
69
  'X_DB_STUDIO_FAILED',
70
+ // Refusing to seed production is a REFUSAL, not a malformed invocation, so it is not
71
+ // `X_CLI_BAD_FLAG`: the argv was well formed and the answer is no. Its own code is what lets
72
+ // `x errors explain` hand back the one remedy — name the tier — instead of the flag code's
73
+ // "unknown flag, missing value, or a value the command refuses", which covers a dozen causes.
74
+ 'X_SEED_ENVIRONMENT',
70
75
  // The five app-surface boundary codes. `@ultimat3/render` owns the *rule* (`checkSurfaceBoundary`)
71
76
  // and the CLI owns the diagnostic, because `x verify` and `x fix boundary` are the two commands
72
77
  // that report it — see `app-boundaries.ts`, which holds the one rule-to-code table.
@@ -81,6 +86,10 @@ export const CLI_OWNED_ERROR_CODES = [
81
86
  'X_GUARD_INVALID',
82
87
  'X_GUARD_FAILED',
83
88
  'X_GUARD_FINDING_INVALID',
89
+ // The CLI's own declarations, held to each other. A flag the parser accepts and no code reads
90
+ // is a promise in `x help` with nothing behind it — `x deploy --critical` said "forces clients
91
+ // to reload" and reached no reader outside the plan JSON it was written into.
92
+ 'X_CLI_FLAG_UNREAD',
84
93
  // The two halves of `x secrets edit` that belong to the terminal rather than to the envelope.
85
94
  // `@ultimat3/core` owns every X_SECRETS_* code about the file and the key; an editor is the
86
95
  // CLI's problem alone, and core would have no `fix:` to offer for one.
@@ -165,6 +174,7 @@ export const CLI_ERROR_TITLES: Readonly<Record<CliOwnedErrorCode, string>> = {
165
174
  X_DB_MIGRATE_FAILED: 'x db migrate failed',
166
175
  X_DB_BRANCH_FAILED: 'an x db branch step failed',
167
176
  X_DB_STUDIO_FAILED: 'x db studio failed',
177
+ X_SEED_ENVIRONMENT: 'the seed tier is not one this environment runs',
168
178
  X_BOUNDARY_SITE_TO_APP: 'site/ imported app/',
169
179
  X_BOUNDARY_SHARED_LEAF: 'shared/ imported a surface',
170
180
  X_BOUNDARY_APP_TO_API: 'app/ imported api/ at runtime',
@@ -173,6 +183,7 @@ export const CLI_ERROR_TITLES: Readonly<Record<CliOwnedErrorCode, string>> = {
173
183
  X_GUARD_INVALID: 'a file in guards/ exports no usable guard',
174
184
  X_GUARD_FAILED: 'an app guard threw instead of returning findings',
175
185
  X_GUARD_FINDING_INVALID: "an app guard's finding breaks the error contract",
186
+ X_CLI_FLAG_UNREAD: 'a command declares a flag no code reads',
176
187
  X_SECRETS_EDITOR_MISSING: 'no $EDITOR to open the decrypted secrets in',
177
188
  X_SECRETS_EDIT_FAILED: 'the editor exited non-zero, so nothing was resealed',
178
189
  };
@@ -8,10 +8,12 @@
8
8
  import { join } from 'node:path';
9
9
  import { docsFor } from './error-codes';
10
10
  import { citedCommandProblem, loadCommandCatalog } from './fix-command';
11
+ import { createHelperResolver } from './fix-imports';
12
+ import { scanFixSites } from './fix-scan';
11
13
  import type { Finding } from './output';
12
14
  import { eachSourceFile, isGenerated, isTest } from './source-files';
13
15
  import type { CodeSite, FixSite } from './ts-scan';
14
- import { isCodeRegistry, scanBorrowedCodes, scanCodes, scanFixes } from './ts-scan';
16
+ import { isCodeRegistry, scanBorrowedCodes, scanCodes } from './ts-scan';
15
17
 
16
18
  /** Advice, not instruction. The list is the one in `docs/architecture/04-error-contract.md`. */
17
19
  export const BANNED_PHRASES: readonly RegExp[] = [
@@ -77,12 +79,33 @@ const fixFinding = (site: FixSite, problem: string): Finding => ({
77
79
  * The catalog is loaded ONCE per run rather than per fix line: it is a dynamic import (see
78
80
  * `fix-command.ts` for the cycle it breaks) and this walks every shipped source file.
79
81
  */
80
- export async function checkErrorFixes(root: string): Promise<readonly Finding[]> {
82
+ export interface ErrorFixReport {
83
+ readonly findings: readonly Finding[];
84
+ /** Fix literals actually read, and held to both rules. */
85
+ readonly checked: number;
86
+ /**
87
+ * Fix arguments at a known builder that hold no single literal — a parameter passed through, a
88
+ * concatenation, a table lookup. The step prints it, because a gate that says "checked 412,
89
+ * could not read 27" is honest and one that says nothing is the false green this check exists to
90
+ * close. It does NOT cover a builder imported from another PACKAGE: `candidatePaths` resolves
91
+ * relative specifiers only, and that gap is 3 call sites across this repo, measured 2026-08.
92
+ */
93
+ readonly unreadable: number;
94
+ }
95
+
96
+ export async function checkErrorFixReport(root: string): Promise<ErrorFixReport> {
81
97
  const findings: Finding[] = [];
82
98
  const catalog = await loadCommandCatalog();
99
+ const imports = createHelperResolver(root);
100
+ let checked = 0;
101
+ let unreadable = 0;
83
102
  for await (const path of eachSourceFile(root)) {
84
103
  if (isTest(path) || isGenerated(path)) continue;
85
- for (const site of scanFixes(await Bun.file(join(root, path)).text(), path)) {
104
+ const source = await Bun.file(join(root, path)).text();
105
+ const scan = scanFixSites(source, path, await imports(path, source));
106
+ checked += scan.sites.length;
107
+ unreadable += scan.unreadable;
108
+ for (const site of scan.sites) {
86
109
  // The interpolation-blanked form for both rules: `x ${name}` names no command this can
87
110
  // resolve, and reading `<value>` as one would be a finding nobody can act on.
88
111
  const fix = staticFix(site.fix);
@@ -90,9 +113,13 @@ export async function checkErrorFixes(root: string): Promise<readonly Finding[]>
90
113
  if (problem !== undefined) findings.push(fixFinding(site, problem));
91
114
  }
92
115
  }
93
- return findings;
116
+ return { findings, checked, unreadable };
94
117
  }
95
118
 
119
+ /** The findings alone, for every caller that reports no coverage line. */
120
+ export const checkErrorFixes = async (root: string): Promise<readonly Finding[]> =>
121
+ (await checkErrorFixReport(root)).findings;
122
+
96
123
  /**
97
124
  * A code is documented when the reference page names it. Deliberately not "owns a table row": the
98
125
  * page legitimately groups near-identical codes onto one row, and a rule that forbade that would
package/src/exec.ts CHANGED
@@ -5,7 +5,10 @@
5
5
  // `UltimateError` straight from core rather than a class in `./errors`: this module is imported by
6
6
  // every command, and `./errors` runs `registerErrorCodes` on import — a subprocess boundary must
7
7
  // not decide when the CLI's registry is populated. `X_CLI_UNEXPECTED` is owned there all the same.
8
- import { UltimateError } from '@ultimat3/core';
8
+ import { renderThrowable, UltimateError } from '@ultimat3/core';
9
+ // `shell-quote.ts` is a leaf — it imports nothing, so the subprocess boundary stays importable
10
+ // from anywhere, this file's header rule about a single boundary included.
11
+ import { quoteArg } from './shell-quote';
9
12
 
10
13
  export interface ExecResult {
11
14
  readonly command: readonly string[];
@@ -27,6 +30,43 @@ export type Runner = (command: readonly string[], options: ExecOptions) => Promi
27
30
  /** performance.now(), not Date.now(): the test preload freezes the wall clock on purpose. */
28
31
  const now = (): number => performance.now();
29
32
 
33
+ /**
34
+ * The failure mode this file's header already promised was identical everywhere, and the one it
35
+ * never coded. `x deploy` on a machine without `docker` threw Bun's own `Error: Executable not
36
+ * found in $PATH`; `dispatch.ts` rendered it as `X_CLI_UNEXPECTED` with `fix: x doctor --json`,
37
+ * and `runDoctor` checks nothing about an absent binary — an instruction that cannot close the
38
+ * error is axiom 4 inverted. The code stays `X_CLI_UNEXPECTED` (the CLI already owns it for a
39
+ * failure of its own machinery); what changes is that the fix names the program to install.
40
+ *
41
+ * The program name goes through `quoteArg` at BOTH references: `docker compose` or any name a
42
+ * shell would resplit produced a `fix:` that runs something else, which is axiom 4 inverted twice
43
+ * in one line.
44
+ *
45
+ * The thrown value goes through core's render helper and is never interpolated: an `unknown`
46
+ * reaching a `cause:` through `${…}` is what `bun run error-render` refuses, and this one is
47
+ * genuinely unknown — Bun raises `ENOENT` for a missing program, `EACCES` for an unrunnable one.
48
+ *
49
+ * The return type is inferred so this stays one statement of `Bun.spawn`'s own shape.
50
+ */
51
+ function spawnOrRefuse(command: readonly string[], options: ExecOptions) {
52
+ const [head = '', ...rest] = command;
53
+ try {
54
+ return Bun.spawn([head, ...rest], {
55
+ cwd: options.cwd,
56
+ env: options.env === undefined ? Bun.env : { ...Bun.env, ...options.env },
57
+ stdin: options.stdin === undefined ? 'ignore' : new TextEncoder().encode(options.stdin),
58
+ stdout: 'pipe',
59
+ stderr: 'pipe',
60
+ });
61
+ } catch (error) {
62
+ throw new UltimateError({
63
+ code: 'X_CLI_UNEXPECTED',
64
+ cause: `the CLI could not run "${head}" from ${options.cwd}: ${renderThrowable(error)}`,
65
+ fix: `install ${quoteArg(head)} and put it on PATH, then re-run — confirm with: command -v ${quoteArg(head)}`,
66
+ });
67
+ }
68
+ }
69
+
30
70
  export const exec: Runner = async (command, options) => {
31
71
  const started = now();
32
72
  const [head, ...rest] = command;
@@ -40,13 +80,7 @@ export const exec: Runner = async (command, options) => {
40
80
  fix: 'pass the program as the first element: exec(["bun", "test"], { cwd })',
41
81
  });
42
82
  }
43
- const proc = Bun.spawn([head, ...rest], {
44
- cwd: options.cwd,
45
- env: options.env === undefined ? Bun.env : { ...Bun.env, ...options.env },
46
- stdin: options.stdin === undefined ? 'ignore' : new TextEncoder().encode(options.stdin),
47
- stdout: 'pipe',
48
- stderr: 'pipe',
49
- });
83
+ const proc = spawnOrRefuse([head, ...rest], options);
50
84
  const [stdout, stderr, code] = await Promise.all([
51
85
  new Response(proc.stdout).text(),
52
86
  new Response(proc.stderr).text(),
@@ -28,8 +28,15 @@ import { GLOBAL_FLAGS } from './parse';
28
28
  // in `@ultimat3/mcp` — is `X_CLI_UNKNOWN_COMMAND` when run and resolved clean while a placeholder
29
29
  // was invisible to the reader. Second and fourth slots are open positionals (`x new my-app`,
30
30
  // `x db branch drop <name>`), where a placeholder is exactly right.
31
- const CITATION =
32
- /(?:^|[\s;|&("'`])x\s+([a-z][a-z\d-]*)(?:\s+([a-z][a-z\d-]*))?(?:\s+([a-z][a-z\d-]*|<[^>]*>))?/g;
31
+ // A `:` is part of a word only when a letter follows it, which is what separates the shipped
32
+ // positional `admin:page` from prose that ends a citation with a colon (`x verify: the gate`).
33
+ // Read without it, `x g admin:page` cites `x g admin` — a positional the CLI does not ship —
34
+ // and the one documented invocation of the admin-page generator was a standing false finding.
35
+ const WORD = String.raw`[a-z][a-z\d-]*(?::[a-z][a-z\d-]*)?`;
36
+ const CITATION = new RegExp(
37
+ String.raw`(?:^|[\s;|&("'\x60])x\s+(${WORD})(?:\s+(${WORD}))?(?:\s+(${WORD}|<[^>]*>))?`,
38
+ 'g',
39
+ );
33
40
 
34
41
  /**
35
42
  * A long flag, `--` stripped. `--no-<name>` is the parser's negation of a boolean, so it resolves
@@ -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;
@@ -54,3 +54,14 @@ export const intFlagOr = (args: ParsedArgs, flag: IntFlag, fallback: number): nu
54
54
  * free port. Two ranges for one concept is the drift this constant exists to prevent.
55
55
  */
56
56
  export const PORT_RANGE = { min: 0, max: 65_535 } as const;
57
+
58
+ /**
59
+ * A free-port suggestion the thing being fixed will actually accept. `port + 1` at the top of the
60
+ * range names 65536, which is not a port — so a `fix:` built that way reproduces a failure instead
61
+ * of ending one: `x doctor` emitted `x dev --port 65536`, which `x dev` refuses with
62
+ * `X_CLI_BAD_FLAG`. The neighbour below is a port; the one above does not exist. Here rather than
63
+ * beside either caller, because two ports-are-bounded rules is the drift `PORT_RANGE` above
64
+ * already exists to prevent.
65
+ */
66
+ export const neighbouringPort = (port: number): number =>
67
+ port < PORT_RANGE.max ? port + 1 : PORT_RANGE.max - 1;