@ultimat3/cli 11.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.
- package/CLAUDE.md +27 -0
- package/package.json +28 -28
- package/src/error-codes.ts +5 -0
- package/src/error-contract.ts +36 -2
- package/src/index.ts +10 -1
- package/src/mcp-errors.ts +2 -0
- package/src/ts-scan.ts +117 -6
- package/src/verify-checks.ts +10 -2
package/CLAUDE.md
CHANGED
|
@@ -256,6 +256,33 @@ that named the code in its `<PKG>_BORROWED_ERROR_CODES`. The docs check reads it
|
|
|
256
256
|
framework's own `framework.manifest.json`, because a second scanner over a narrower file set is a
|
|
257
257
|
manifest that claims completeness it does not have.
|
|
258
258
|
|
|
259
|
+
**A `code:` is a literal, a module-scope const in the same file, or a finding** — `As of 2026-08-23`,
|
|
260
|
+
and until then it was a literal or silence. `scanCodes` matched `code\s*[:=]\s*'X_…'`, so
|
|
261
|
+
`const STALE = 'X_DOC_PACKAGE_GRAPH_STALE'` followed by `code: STALE` — the DRY thing to write, and
|
|
262
|
+
what `scripts/package-map-graph.ts` really wrote — was a declaration to nobody: no manifest row, no
|
|
263
|
+
row demanded on `wiki/Error-Codes.md`, no entry for `bun run gate-codes`, and `x errors explain`
|
|
264
|
+
answering `X_ERROR_CODE_UNKNOWN` for a code the build throws. Silent, and in the **permissive**
|
|
265
|
+
direction: the DRYer the author, the less the gate saw (#277).
|
|
266
|
+
|
|
267
|
+
`scanCodeDeclarations` is that one pass, and it returns both halves. It resolves the identifier
|
|
268
|
+
against the module-scope consts of the **same file** — anchored at column 0, which is what makes it
|
|
269
|
+
module scope without a parser — and reports every name it cannot resolve as `X_ERROR_CODE_UNRESOLVED`
|
|
270
|
+
rather than skipping it, which is the whole point: a scanner that reads only what it likes enforces
|
|
271
|
+
only what it sees. `scanCodes` is its `.sites`, so the manifest, the docs check, `bun run gate-codes`
|
|
272
|
+
and `x errors explain` (through `scanCodeFixSites`, which resolves the same way) cannot see different
|
|
273
|
+
sets. Cross-file resolution was **refused** even though `fix-imports.ts` already does the harder
|
|
274
|
+
version for `fix:`: it would make the scan async for every caller, and the finding is the better
|
|
275
|
+
answer anyway — one file holds both the code and its only spelling.
|
|
276
|
+
|
|
277
|
+
Three shapes are deliberately not judged, each measured over the framework and both tracked apps
|
|
278
|
+
before the rule shipped. A name that resolves to something that is **not** a code is an answer, not
|
|
279
|
+
a gap (`const STATUS_NOT_FOUND = 404` in `@ultimat3/realtime`'s NATS fake, the one live instance). A
|
|
280
|
+
**table read** is not judged — `SEO_ERROR_CODES.metaMissing` is how `@ultimat3/seo` and
|
|
281
|
+
`@ultimat3/ui` raise all 18 of their codes, and the registry those literals live in already declares
|
|
282
|
+
them. A **lowercase** name is not judged: 164 sit at a `code:` position in this tree and every one is
|
|
283
|
+
a type annotation (`readonly code: string`) or a re-raise (`code: opts.code`). Measured on all three
|
|
284
|
+
roots: **0 findings**, so it enforces outright with no pin table.
|
|
285
|
+
|
|
259
286
|
An empty `fix`, or a `fix` that says `check` / `make sure` / `try` / `see the docs` and names no
|
|
260
287
|
command, call or file path, is `X_ERROR_FIX_INVALID`. A declared code the host's error reference
|
|
261
288
|
does not name is `X_ERROR_CODE_UNDOCUMENTED` — `wiki/Error-Codes.md` here, nothing in a generated
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/cli",
|
|
3
|
-
"version": "11.
|
|
3
|
+
"version": "11.1.0",
|
|
4
4
|
"description": "The `x` binary: new, dev, build, verify, generate, db, mcp, doctor, deploy",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -37,33 +37,33 @@
|
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@babel/core": "^7.28.4",
|
|
40
|
-
"@ultimat3/action": "11.
|
|
41
|
-
"@ultimat3/admin": "11.
|
|
42
|
-
"@ultimat3/ai": "11.
|
|
43
|
-
"@ultimat3/auth": "11.
|
|
44
|
-
"@ultimat3/cache": "11.
|
|
45
|
-
"@ultimat3/core": "11.
|
|
46
|
-
"@ultimat3/db": "11.
|
|
47
|
-
"@ultimat3/entity": "11.
|
|
48
|
-
"@ultimat3/flags": "11.
|
|
49
|
-
"@ultimat3/http": "11.
|
|
50
|
-
"@ultimat3/i18n": "11.
|
|
51
|
-
"@ultimat3/jobs": "11.
|
|
52
|
-
"@ultimat3/mail": "11.
|
|
53
|
-
"@ultimat3/manifest": "11.
|
|
54
|
-
"@ultimat3/mcp": "11.
|
|
55
|
-
"@ultimat3/money": "11.
|
|
56
|
-
"@ultimat3/policy": "11.
|
|
57
|
-
"@ultimat3/pwa": "11.
|
|
58
|
-
"@ultimat3/query": "11.
|
|
59
|
-
"@ultimat3/realtime": "11.
|
|
60
|
-
"@ultimat3/render": "11.
|
|
61
|
-
"@ultimat3/schema": "11.
|
|
62
|
-
"@ultimat3/scraping": "11.
|
|
63
|
-
"@ultimat3/seo": "11.
|
|
64
|
-
"@ultimat3/storage": "11.
|
|
65
|
-
"@ultimat3/testing": "11.
|
|
66
|
-
"@ultimat3/time": "11.
|
|
40
|
+
"@ultimat3/action": "11.1.0",
|
|
41
|
+
"@ultimat3/admin": "11.1.0",
|
|
42
|
+
"@ultimat3/ai": "11.1.0",
|
|
43
|
+
"@ultimat3/auth": "11.1.0",
|
|
44
|
+
"@ultimat3/cache": "11.1.0",
|
|
45
|
+
"@ultimat3/core": "11.1.0",
|
|
46
|
+
"@ultimat3/db": "11.1.0",
|
|
47
|
+
"@ultimat3/entity": "11.1.0",
|
|
48
|
+
"@ultimat3/flags": "11.1.0",
|
|
49
|
+
"@ultimat3/http": "11.1.0",
|
|
50
|
+
"@ultimat3/i18n": "11.1.0",
|
|
51
|
+
"@ultimat3/jobs": "11.1.0",
|
|
52
|
+
"@ultimat3/mail": "11.1.0",
|
|
53
|
+
"@ultimat3/manifest": "11.1.0",
|
|
54
|
+
"@ultimat3/mcp": "11.1.0",
|
|
55
|
+
"@ultimat3/money": "11.1.0",
|
|
56
|
+
"@ultimat3/policy": "11.1.0",
|
|
57
|
+
"@ultimat3/pwa": "11.1.0",
|
|
58
|
+
"@ultimat3/query": "11.1.0",
|
|
59
|
+
"@ultimat3/realtime": "11.1.0",
|
|
60
|
+
"@ultimat3/render": "11.1.0",
|
|
61
|
+
"@ultimat3/schema": "11.1.0",
|
|
62
|
+
"@ultimat3/scraping": "11.1.0",
|
|
63
|
+
"@ultimat3/seo": "11.1.0",
|
|
64
|
+
"@ultimat3/storage": "11.1.0",
|
|
65
|
+
"@ultimat3/testing": "11.1.0",
|
|
66
|
+
"@ultimat3/time": "11.1.0",
|
|
67
67
|
"babel-preset-solid": "^1.9.15"
|
|
68
68
|
}
|
|
69
69
|
}
|
package/src/error-codes.ts
CHANGED
|
@@ -26,6 +26,10 @@ export const CLI_OWNED_ERROR_CODES = [
|
|
|
26
26
|
'X_ERROR_FIX_PATH_MISSING',
|
|
27
27
|
'X_ERROR_CODE_UNDOCUMENTED',
|
|
28
28
|
'X_ERROR_CODE_UNREGISTERED',
|
|
29
|
+
// The third: a `code:` the scan cannot read at all. `const STALE = 'X_…'` in another file and
|
|
30
|
+
// `code: STALE` here is invisible to every reader of the code set, and silence there is
|
|
31
|
+
// permissive — the DRYer the author, the less the gate sees (#277).
|
|
32
|
+
'X_ERROR_CODE_UNRESOLVED',
|
|
29
33
|
// Reported as `Finding`s rather than thrown, and unregistered until now because of it — so
|
|
30
34
|
// `x errors explain X_TYPECHECK_FAILED` refused a code `x verify` had just printed. A finding
|
|
31
35
|
// carries an `X_*` code to the same reader a throw does; the registry is what makes that code
|
|
@@ -171,6 +175,7 @@ export const CLI_ERROR_TITLES: Readonly<Record<CliOwnedErrorCode, string>> = {
|
|
|
171
175
|
X_ERROR_FIX_PATH_MISSING: "an error's fix line cites a file this repository does not have",
|
|
172
176
|
X_ERROR_CODE_UNDOCUMENTED: 'a shipped error code has no row in the error reference',
|
|
173
177
|
X_ERROR_CODE_UNREGISTERED: 'the error reference documents a code no package registers',
|
|
178
|
+
X_ERROR_CODE_UNRESOLVED: 'an error code is written as a name this repository cannot resolve',
|
|
174
179
|
X_STORAGE_UNWRITABLE: 'the storage disk this process needs cannot be written to',
|
|
175
180
|
X_STORAGE_SECRET_DEV: 'upload grants would be signed with the shipped development key',
|
|
176
181
|
X_CLI_UNEXPECTED: 'the CLI itself failed',
|
package/src/error-contract.ts
CHANGED
|
@@ -13,8 +13,8 @@ import { citedPathProblem, FILE_TOKEN_PATTERN } from './fix-path';
|
|
|
13
13
|
import { scanFixSites } from './fix-scan';
|
|
14
14
|
import type { Finding } from './output';
|
|
15
15
|
import { eachSourceFile, isGenerated, isTest } from './source-files';
|
|
16
|
-
import type { CodeSite, FixSite } from './ts-scan';
|
|
17
|
-
import { isCodeRegistry, scanBorrowedCodes, scanCodes } from './ts-scan';
|
|
16
|
+
import type { CodeSite, FixSite, UnresolvedCodeSite } from './ts-scan';
|
|
17
|
+
import { isCodeRegistry, scanBorrowedCodes, scanCodeDeclarations, scanCodes } from './ts-scan';
|
|
18
18
|
|
|
19
19
|
/** Advice, not instruction. The list is the one in `docs/architecture/04-error-contract.md`. */
|
|
20
20
|
export const BANNED_PHRASES: readonly RegExp[] = [
|
|
@@ -259,6 +259,40 @@ export async function collectDeclaredCodes(root: string): Promise<readonly CodeS
|
|
|
259
259
|
return [...sites.values()].map(([site]) => site).sort((a, b) => a.code.localeCompare(b.code));
|
|
260
260
|
}
|
|
261
261
|
|
|
262
|
+
/**
|
|
263
|
+
* A `code:` this scan could not turn into a code. Its own finding rather than a silent skip, which
|
|
264
|
+
* is the whole of #277: `const STALE = 'X_DOC_PACKAGE_GRAPH_STALE'` and then `code: STALE` is the
|
|
265
|
+
* DRY thing to write, `scripts/package-map-graph.ts` wrote it, and the code went into no manifest,
|
|
266
|
+
* demanded no row on the reference, was exempt from `bun run gate-codes` and could not be explained
|
|
267
|
+
* — every reader silent, and every one of them permissive. Resolution closes the same-file case;
|
|
268
|
+
* this closes the rest, because a scanner that reads only what it likes enforces only what it sees.
|
|
269
|
+
*/
|
|
270
|
+
const unresolvedCodeFinding = (site: UnresolvedCodeSite): Finding => ({
|
|
271
|
+
code: 'X_ERROR_CODE_UNRESOLVED',
|
|
272
|
+
cause: `the code at ${site.at}:${site.line} is the name ${site.name}, and no module-scope const in that file gives it a value — so the manifest, the reference page and "x errors explain" are all blind to whatever code it holds`,
|
|
273
|
+
fix: `write the X_* code as a string literal at ${site.at}:${site.line}, or declare it as a module-scope const in that same file`,
|
|
274
|
+
docs: ERROR_DOCS_URL,
|
|
275
|
+
at: `${site.at}:${site.line}`,
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* The other half of `collectDeclaredCodes`, on its own walk rather than folded into that one: this
|
|
280
|
+
* asks whether a file's codes are READABLE, and the collector's callers — `x manifest`, the gate's
|
|
281
|
+
* `errors` step, `bun run gate-codes` — want the codes and not the findings. It runs wherever
|
|
282
|
+
* source does, with no reference page to check against, which is why it is not a host check.
|
|
283
|
+
*/
|
|
284
|
+
export async function checkErrorCodeResolution(root: string): Promise<readonly Finding[]> {
|
|
285
|
+
const findings: Finding[] = [];
|
|
286
|
+
for await (const source of eachSourceFile(root)) {
|
|
287
|
+
if (isTest(source) || isGenerated(source)) continue;
|
|
288
|
+
const text = await Bun.file(join(root, source)).text();
|
|
289
|
+
for (const site of scanCodeDeclarations(text, source).unresolved) {
|
|
290
|
+
findings.push(unresolvedCodeFinding(site));
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return findings;
|
|
294
|
+
}
|
|
295
|
+
|
|
262
296
|
/**
|
|
263
297
|
* Every `X_*` code shipped source declares must appear on the repo's error reference. The page is
|
|
264
298
|
* the host repo's to name — a framework monorepo publishes one, a generated app does not — which
|
package/src/index.ts
CHANGED
|
@@ -130,6 +130,7 @@ export {
|
|
|
130
130
|
COMMAND_TOKENS,
|
|
131
131
|
checkErrorCodeDocs,
|
|
132
132
|
checkErrorCodeRegistry,
|
|
133
|
+
checkErrorCodeResolution,
|
|
133
134
|
checkErrorFixes,
|
|
134
135
|
checkErrorFixReport,
|
|
135
136
|
collectDeclaredCodes,
|
|
@@ -283,11 +284,19 @@ export { belongsToType, discoverTests, sampleFiles } from './test-select';
|
|
|
283
284
|
export type { ReproduceOptions, RunShardsOptions, Shard } from './test-shards';
|
|
284
285
|
export { planShards, reproduceFor, runShards, shardArgs } from './test-shards';
|
|
285
286
|
export { availableCpus, defaultWorkers, WORKER_CEILING } from './test-workers';
|
|
286
|
-
export type {
|
|
287
|
+
export type {
|
|
288
|
+
CodeFixSite,
|
|
289
|
+
CodeScan,
|
|
290
|
+
CodeSite,
|
|
291
|
+
FixSite,
|
|
292
|
+
SourceSite,
|
|
293
|
+
UnresolvedCodeSite,
|
|
294
|
+
} from './ts-scan';
|
|
287
295
|
export {
|
|
288
296
|
isCodeRegistry,
|
|
289
297
|
maskLiterals,
|
|
290
298
|
scanBorrowedCodes,
|
|
299
|
+
scanCodeDeclarations,
|
|
291
300
|
scanCodeFixSites,
|
|
292
301
|
scanCodes,
|
|
293
302
|
stripComments,
|
package/src/mcp-errors.ts
CHANGED
|
@@ -62,6 +62,8 @@ const CLI_FIXES: Readonly<Record<CliErrorCode, string>> = {
|
|
|
62
62
|
X_ERROR_CODE_UNDOCUMENTED: 'x verify --json # the finding names the code and the missing page',
|
|
63
63
|
X_ERROR_CODE_UNREGISTERED:
|
|
64
64
|
'x errors list --json # register the code in its package src/errors.ts, or move its row under "Reserved codes"',
|
|
65
|
+
X_ERROR_CODE_UNRESOLVED:
|
|
66
|
+
'x verify --json # the finding names the file, the line and the name it could not resolve',
|
|
65
67
|
X_CLI_UNEXPECTED: 'x doctor --json',
|
|
66
68
|
X_TYPECHECK_FAILED: 'bunx tsc -b --pretty false',
|
|
67
69
|
X_LINT_FAILED: 'bunx biome check --write .',
|
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
|
|
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
|
-
|
|
300
|
-
|
|
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]) => {
|
package/src/verify-checks.ts
CHANGED
|
@@ -24,7 +24,7 @@ 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
30
|
import { liveRouteFindings } from './live-routes';
|
|
@@ -125,7 +125,15 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
|
|
|
125
125
|
// it does not have. "checked 412, could not read 27" is what a reader can act on.
|
|
126
126
|
async run(ctx) {
|
|
127
127
|
const report = await checkErrorFixReport(ctx.root);
|
|
128
|
-
|
|
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
|
+
];
|
|
129
137
|
return {
|
|
130
138
|
...fromFindings(findings),
|
|
131
139
|
output: msg('cli.verify.fixCoverage', {
|