@ultimat3/cli 10.0.0 → 11.1.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.
@@ -2,6 +2,7 @@
2
2
  // action registry rather than re-listed, plus the test that pins every projected tool describing
3
3
  // itself.
4
4
 
5
+ import { sortedImports } from './imports';
5
6
  import type { GeneratedFile, NameSet } from './naming';
6
7
  import { packageShapeFiles } from './scaffold-package-shape';
7
8
 
@@ -44,9 +45,11 @@ const mcpIndex = (
44
45
  app: NameSet,
45
46
  ): string => `// The app's own MCP tools. Every action with mcp.expose is already a tool; add app-specific
46
47
  // read-only helpers here. Authorization is the action's policy, unchanged.
47
- import * as api from '@${app.kebab}/web/api/health';
48
- import { registerActions } from '@ultimat3/action';
49
- import { defineAppMcp } from '@ultimat3/mcp';
48
+ ${sortedImports([
49
+ `import * as api from '@${app.kebab}/web/api/health';`,
50
+ `import { registerActions } from '@ultimat3/action';`,
51
+ `import { defineAppMcp } from '@ultimat3/mcp';`,
52
+ ])}
50
53
 
51
54
  // Names come from export names, so the registry agrees with the module the app already wrote.
52
55
  registerActions(api);
@@ -63,6 +63,7 @@ const rootPackage = (app: NameSet, version: string): string => `{
63
63
  "@ultimat3/core": "^${version}",
64
64
  "@ultimat3/db": "^${version}",
65
65
  "@ultimat3/entity": "^${version}",
66
+ "@ultimat3/http": "^${version}",
66
67
  "@ultimat3/i18n": "^${version}",
67
68
  "@ultimat3/jobs": "^${version}",
68
69
  "@ultimat3/mcp": "^${version}",
package/src/ts-scan.ts CHANGED
@@ -19,6 +19,17 @@ export interface CodeSite extends SourceSite {
19
19
  readonly code: string;
20
20
  }
21
21
 
22
+ export interface UnresolvedCodeSite extends SourceSite {
23
+ /** The identifier exactly as written at the `code:` position. */
24
+ readonly name: string;
25
+ }
26
+
27
+ /** One file's codes, and the names at a `code:` position this scan could not turn into one. */
28
+ export interface CodeScan {
29
+ readonly sites: readonly CodeSite[];
30
+ readonly unresolved: readonly UnresolvedCodeSite[];
31
+ }
32
+
22
33
  // `ReadonlySet`, so a consumer cannot mutate what every scan in this package reads.
23
34
  export const QUOTES: ReadonlySet<string> = new Set(["'", '"', '`']);
24
35
  export const OPENERS: ReadonlySet<string> = new Set(['(', '[', '{']);
@@ -202,27 +213,111 @@ const CODE_AT_KEY = /\bcode\s*[:=]\s*(['"`])(X_[A-Z0-9_]+)\1/g;
202
213
  const CODE_LITERAL = /(['"`])(X_[A-Z0-9_]+)\1/g;
203
214
  const CODE_KEY = /^[\t ]*(X_[A-Z0-9_]+)\s*:/gm;
204
215
 
216
+ /**
217
+ * A `code` KEY, and never a member assignment: `found.code = SOMETHING` projects somebody else's
218
+ * code and declares none. The literal form above keeps its looser `\b` deliberately — a scanner
219
+ * that stopped collecting a code it has collected for four majors would shrink the manifest.
220
+ */
221
+ const CODE_KEY_POSITION = /(?<![.\w$])code\s*[:=]\s*/g;
222
+
223
+ /** Cheap enough to run on every file, so the masking pass below is paid only where it can pay. */
224
+ const HAS_CODE_IDENTIFIER = /(?<![.\w$])code\s*[:=]\s*[A-Za-z_$]/;
225
+
226
+ /** Sticky: the value expression is read at an exact offset, never out of a slice that may cut. */
227
+ const VALUE_IDENTIFIER = /([A-Za-z_$][\w$]*)\s*([.([]?)/y;
228
+
229
+ /**
230
+ * A module-scope `const NAME = 'X_…'`, and the names of every other module-scope const. Anchored
231
+ * at column 0, which is what makes it module scope without a parser: a `const` inside a function
232
+ * can be shadowed by another in a sibling scope, and a resolver that picked one of them would be
233
+ * guessing. The second set is the answer "that name IS declared here, and it is not a code" —
234
+ * `const STATUS_NOT_FOUND = 404` in `@ultimat3/realtime`'s NATS fake is the live instance, and a
235
+ * rule that reported it would be a rule the reader has to argue with.
236
+ */
237
+ const CODE_CONST =
238
+ /^(?:export\s+)?const\s+([A-Za-z_$][\w$]*)\s*(?::[^=\n]*)?=\s*(['"`])(X_[A-Z0-9_]+)\2/gm;
239
+ const MODULE_CONST = /^(?:export\s+)?const\s+([A-Za-z_$][\w$]*)\s*[:=]/gm;
240
+
241
+ /** House shape for a constant. A lowercase name at a `code:` is a type annotation or a re-raise. */
242
+ const CODE_CONSTANT_NAME = /^[A-Z][A-Z0-9_]+$/;
243
+
244
+ interface ModuleConstants {
245
+ /** Name → the code it holds. */
246
+ readonly codes: ReadonlyMap<string, string>;
247
+ /** Every module-scope const name, code-valued or not. */
248
+ readonly names: ReadonlySet<string>;
249
+ }
250
+
251
+ function moduleConstants(text: string): ModuleConstants {
252
+ const codes = new Map<string, string>();
253
+ const names = new Set<string>();
254
+ for (const match of text.matchAll(MODULE_CONST)) names.add(match[1] as string);
255
+ for (const match of text.matchAll(CODE_CONST)) codes.set(match[1] as string, match[3] as string);
256
+ return { codes, names };
257
+ }
258
+
259
+ /**
260
+ * The bare identifier a key's value is, or `undefined` when the value is anything else. A member
261
+ * read, an index and a call are all refused: `SEO_ERROR_CODES.metaMissing` is how two packages
262
+ * raise every code they own, the registry branch below already collects those literals, and
263
+ * judging the read would report eighteen working sites as broken.
264
+ */
265
+ function valueIdentifier(masked: string, from: number): string | undefined {
266
+ VALUE_IDENTIFIER.lastIndex = from;
267
+ const match = VALUE_IDENTIFIER.exec(masked);
268
+ return match === null || match[2] !== '' ? undefined : match[1];
269
+ }
270
+
205
271
  /**
206
272
  * Codes this file declares: every `code:` / `code =` throw site, plus — in a package's own code
207
273
  * registry — every entry of its code list or title table, whichever shape it uses. A registry is
208
274
  * the only place a bare `X_*` literal is a declaration; anywhere else it is a reference (an env
209
275
  * var named `X_BUILD_ID`, an HTTP status map keyed by code) and collecting it would invent a code.
276
+ *
277
+ * A `code:` written as an IDENTIFIER is resolved against the module-scope consts of the same file,
278
+ * and reported as `unresolved` when nothing there gives it a value (#277). Both halves matter and
279
+ * neither is optional: `const STALE = 'X_DOC_PACKAGE_GRAPH_STALE'` is what a DRY author writes, and
280
+ * a scan that skipped it silently left the code out of the manifest, out of `wiki/Error-Codes.md`'s
281
+ * demanded rows, out of `bun run gate-codes` and out of `x errors explain` — permissive, and quiet.
282
+ * The identifier half reads the MASKED text: `packages/cli/src/templates/` emits app source by the
283
+ * dozen inside template literals, and a `code: STALE` in one of those is text, not a declaration.
210
284
  */
211
- export function scanCodes(source: string, at: string): readonly CodeSite[] {
285
+ export function scanCodeDeclarations(source: string, at: string): CodeScan {
212
286
  const text = stripComments(source);
213
287
  const lineAt = lineIndex(text);
214
288
  const sites = new Map<string, CodeSite>();
289
+ const unresolved: UnresolvedCodeSite[] = [];
215
290
  const add = (code: string, index: number): void => {
216
291
  if (!sites.has(code)) sites.set(code, { at, line: lineAt(index), code });
217
292
  };
218
293
  for (const match of text.matchAll(CODE_AT_KEY)) add(match[2] as string, match.index);
294
+ if (HAS_CODE_IDENTIFIER.test(text)) {
295
+ const masked = maskLiterals(source);
296
+ const constants = moduleConstants(text);
297
+ for (const key of masked.matchAll(CODE_KEY_POSITION)) {
298
+ const name = valueIdentifier(masked, key.index + key[0].length);
299
+ if (name === undefined) continue;
300
+ const code = constants.codes.get(name);
301
+ if (code !== undefined) add(code, key.index);
302
+ else if (!constants.names.has(name) && CODE_CONSTANT_NAME.test(name)) {
303
+ unresolved.push({ at, line: lineAt(key.index), name });
304
+ }
305
+ }
306
+ }
219
307
  if (isCodeRegistry(text)) {
220
308
  for (const match of text.matchAll(CODE_LITERAL)) add(match[2] as string, match.index);
221
309
  for (const match of text.matchAll(CODE_KEY)) add(match[1] as string, match.index);
222
310
  }
223
- return [...sites.values()];
311
+ return { sites: [...sites.values()], unresolved };
224
312
  }
225
313
 
314
+ /**
315
+ * The codes alone, for every caller that has no report to attach a finding to. One scanner, one
316
+ * answer: the manifest, the docs check, `bun run gate-codes` and `x errors explain` all read this.
317
+ */
318
+ export const scanCodes = (source: string, at: string): readonly CodeSite[] =>
319
+ scanCodeDeclarations(source, at).sites;
320
+
226
321
  export interface CodeFixSite extends CodeSite {
227
322
  /**
228
323
  * The fix literal exactly as written, `${…}` included. Absent when the throw site builds its
@@ -250,6 +345,8 @@ function soleLiteral(
250
345
  return found.length === 1 ? found[0] : undefined;
251
346
  }
252
347
 
348
+ const CODE_NAME = /^X_[A-Z0-9_]+$/;
349
+
253
350
  /**
254
351
  * Every `X_*` code paired with the `fix:` written beside it — in the SAME object literal, which is
255
352
  * the whole rule. `new UltimateError({ code, cause, fix })` is the one shape this framework raises
@@ -263,6 +360,15 @@ function soleLiteral(
263
360
  export function scanCodeFixSites(source: string, at: string): readonly CodeFixSite[] {
264
361
  const masked = maskLiterals(source);
265
362
  const lineAt = lineIndex(masked);
363
+ // Lazily, because most files hold no `code:` at all and stripping is a whole extra pass over
364
+ // the text. Same resolver `scanCodeDeclarations` reads, so `x errors explain` can never see a
365
+ // smaller set of throw sites than the manifest does.
366
+ let constants: ModuleConstants | undefined;
367
+ const constantCode = (name: string | undefined): string | undefined => {
368
+ if (name === undefined) return undefined;
369
+ constants ??= moduleConstants(stripComments(source));
370
+ return constants.codes.get(name);
371
+ };
266
372
  const keys = new Map<number, { readonly kind: 'code' | 'fix'; readonly from: number }>();
267
373
  for (const key of masked.matchAll(CODE_OR_FIX_KEY)) {
268
374
  keys.set(key.index, {
@@ -293,11 +399,16 @@ export function scanCodeFixSites(source: string, at: string): readonly CodeFixSi
293
399
  const scope = stack.at(-1);
294
400
  if (key === undefined || scope === undefined) continue;
295
401
  const literal = soleLiteral(masked, source, key.from, lineAt);
296
- if (literal === undefined) continue;
297
402
  if (key.kind === 'fix') {
298
- if (!fixes.has(scope)) fixes.set(scope, literal.fix);
299
- } else if (/^X_[A-Z0-9_]+$/.test(literal.fix) && !codes.has(scope)) {
300
- codes.set(scope, { at, line: literal.line, code: literal.fix });
403
+ if (literal !== undefined && !fixes.has(scope)) fixes.set(scope, literal.fix);
404
+ continue;
405
+ }
406
+ // A fix has no second reading, so it stays literal-only; a code has exactly one, which is the
407
+ // module-scope const its own file declares it in.
408
+ const code =
409
+ literal === undefined ? constantCode(valueIdentifier(masked, key.from)) : literal.fix;
410
+ if (code !== undefined && CODE_NAME.test(code) && !codes.has(scope)) {
411
+ codes.set(scope, { at, line: literal?.line ?? lineAt(key.from), code });
301
412
  }
302
413
  }
303
414
  return [...codes].map(([scope, site]) => {
@@ -24,9 +24,10 @@ import { checkBudgets, readBuildStats } from './budgets';
24
24
  import { checkDestructiveMigrations } from './db-destructive';
25
25
  import { checkDocumentStyles, documentSurfaces } from './document-styles';
26
26
  import { checkSourceDrift } from './drift';
27
- import { checkErrorFixReport } from './error-contract';
27
+ import { checkErrorCodeResolution, checkErrorFixReport } from './error-contract';
28
28
  import { guardFindings } from './guards';
29
29
  import { catalogFindings } from './i18n-registration';
30
+ import { liveRouteFindings } from './live-routes';
30
31
  import { msg } from './messages';
31
32
  import type { Finding } from './output';
32
33
  import { findingFrom } from './output';
@@ -124,7 +125,15 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
124
125
  // it does not have. "checked 412, could not read 27" is what a reader can act on.
125
126
  async run(ctx) {
126
127
  const report = await checkErrorFixReport(ctx.root);
127
- const findings = [...report.findings, ...(await hostFindings(ctx, 'errors'))];
128
+ // The third rule on this step, and the one that is about the code rather than the fix: a
129
+ // `code:` reached through a name nothing in its own file declares is a code no reader of the
130
+ // set can see — not the manifest, not the reference page's coverage rule, not
131
+ // `x errors explain`. It runs here because it needs source and nothing else (#277).
132
+ const findings = [
133
+ ...report.findings,
134
+ ...(await checkErrorCodeResolution(ctx.root)),
135
+ ...(await hostFindings(ctx, 'errors')),
136
+ ];
128
137
  return {
129
138
  ...fromFindings(findings),
130
139
  output: msg('cli.verify.fixCoverage', {
@@ -171,7 +180,8 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
171
180
  },
172
181
  {
173
182
  name: 'budgets',
174
- summary: 'per-route JS bytes and LCP, and the global style layer every document carries',
183
+ summary:
184
+ 'per-route JS bytes and LCP, the global style layer every document carries, and the routes that boot nothing to receive their live rows',
175
185
  // The global-style assertion rides here rather than becoming an eighteenth step, because this
176
186
  // step already asks the one question it asks: what does the document this build emits actually
177
187
  // contain? It is also the same app load — `appManifest` fills render's stylesheet registry on
@@ -202,6 +212,10 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
202
212
  return fromFindings([
203
213
  ...findings,
204
214
  ...checkDocumentStyles(documentSurfaces()),
215
+ // The third rider, and the same question this step already asks one level down: what
216
+ // JavaScript does this route's document boot? A live read with no island is a route
217
+ // whose answer is "none", which no suite can fail on — the page renders, at 200.
218
+ ...(await liveRouteFindings(ctx.root)),
205
219
  ...checkBudgets(manifest, stats),
206
220
  ]);
207
221
  },