@ultimat3/cli 1.1.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +724 -0
- package/README.md +41 -9
- package/package.json +25 -23
- package/src/api-routes.ts +16 -0
- package/src/app-auth.ts +32 -0
- package/src/app-entities.ts +18 -0
- package/src/app-env.ts +103 -0
- package/src/app-load.ts +20 -3
- package/src/bin.ts +4 -3
- package/src/budgets.ts +114 -9
- package/src/cmd-build.ts +69 -21
- package/src/cmd-db-branch.ts +215 -0
- package/src/cmd-db.ts +332 -155
- package/src/cmd-deploy.ts +59 -6
- package/src/cmd-dev.ts +87 -17
- package/src/cmd-docs.ts +167 -0
- package/src/cmd-doctor.ts +64 -9
- package/src/cmd-env.ts +95 -0
- package/src/cmd-errors.ts +33 -13
- package/src/cmd-fix.ts +5 -1
- package/src/cmd-generate.ts +146 -111
- package/src/cmd-help.ts +16 -5
- package/src/cmd-i18n.ts +2 -0
- package/src/cmd-jobs.ts +47 -33
- package/src/cmd-mcp.ts +11 -2
- package/src/cmd-new.ts +13 -7
- package/src/cmd-planned.ts +55 -10
- package/src/cmd-policy.ts +1 -0
- package/src/cmd-registries.ts +3 -0
- package/src/cmd-secrets.ts +368 -0
- package/src/cmd-tasks.ts +1 -0
- package/src/cmd-test.ts +17 -23
- package/src/cmd-verify.ts +177 -23
- package/src/db-backfill.ts +401 -0
- package/src/db-branch.ts +251 -0
- package/src/db-destructive.ts +29 -0
- package/src/db-finding.ts +28 -0
- package/src/db-generate.ts +112 -0
- package/src/db-snapshot.ts +24 -0
- package/src/dev-assets.ts +86 -20
- package/src/dev-cache.ts +122 -0
- package/src/dev-dashboard.ts +19 -4
- package/src/dev-hooks.ts +27 -2
- package/src/dev-n-plus-one.ts +191 -0
- package/src/dev-queue.ts +105 -19
- package/src/dev-render.ts +158 -26
- package/src/dev-roles-fixture.ts +67 -0
- package/src/dev-roles.ts +186 -78
- package/src/dev-runtime.ts +117 -40
- package/src/dev-services.ts +15 -0
- package/src/dev-storage.ts +245 -0
- package/src/dev-sync.ts +107 -0
- package/src/dev-traces.ts +11 -3
- package/src/dispatch.ts +4 -2
- package/src/document-styles.ts +54 -0
- package/src/drift.ts +37 -9
- package/src/error-catalog.ts +7 -18
- package/src/error-codes.ts +186 -0
- package/src/error-contract.ts +29 -7
- package/src/error-fixes.ts +114 -0
- package/src/errors.ts +205 -140
- package/src/fix-command.ts +268 -0
- package/src/flag-number.ts +56 -0
- package/src/framework-scope.ts +49 -0
- package/src/generate-kinds.ts +97 -0
- package/src/guards.ts +186 -0
- package/src/index.ts +87 -14
- package/src/island-bundle.ts +166 -0
- package/src/island-routes.ts +50 -0
- package/src/jobs-driver.ts +33 -0
- package/src/jobs-json.ts +24 -0
- package/src/jobs-report.ts +17 -4
- package/src/mcp-db-target.ts +52 -27
- package/src/mcp-errors.ts +120 -19
- package/src/mcp-host.ts +44 -25
- package/src/messages.ts +81 -2
- package/src/metrics-endpoint.ts +73 -0
- package/src/migrations.ts +37 -4
- package/src/otlp-export.ts +64 -0
- package/src/output.ts +46 -16
- package/src/parse.ts +41 -3
- package/src/policy-facts.ts +38 -6
- package/src/policy-fixture.ts +14 -7
- package/src/prerender.ts +111 -2
- package/src/registry.ts +21 -3
- package/src/runtime-overrides.ts +66 -0
- package/src/safe-url-label.ts +24 -0
- package/src/scaffold-fixture.ts +10 -0
- package/src/scaffold-typecheck.ts +16 -38
- package/src/serve.ts +202 -18
- package/src/source-files.ts +4 -0
- package/src/statement-loop.ts +74 -0
- package/src/style-csp.ts +18 -0
- package/src/sync-authenticator.ts +59 -0
- package/src/templates/action.ts +15 -30
- package/src/templates/admin-page.ts +103 -0
- package/src/templates/admin.ts +11 -7
- package/src/templates/backfill.ts +212 -0
- package/src/templates/entity.ts +72 -31
- package/src/templates/guard.ts +143 -0
- package/src/templates/index.ts +12 -1
- package/src/templates/island.ts +67 -0
- package/src/templates/job.ts +53 -13
- package/src/templates/naming.ts +17 -1
- package/src/templates/policy.ts +35 -28
- package/src/templates/query.ts +24 -5
- package/src/templates/resource.ts +19 -11
- package/src/templates/route.ts +90 -15
- package/src/templates/scaffold-app.ts +142 -45
- package/src/templates/scaffold-claude-agents.ts +149 -0
- package/src/templates/scaffold-claude-commands.ts +221 -0
- package/src/templates/scaffold-claude.ts +134 -0
- package/src/templates/scaffold-container.ts +46 -2
- package/src/templates/scaffold-db-package.ts +91 -0
- package/src/templates/scaffold-docs.ts +24 -5
- package/src/templates/scaffold-domain-package.ts +90 -0
- package/src/templates/scaffold-env.ts +87 -0
- package/src/templates/scaffold-i18n.ts +4 -1
- package/src/templates/scaffold-mcp-package.ts +49 -0
- package/src/templates/scaffold-package-shape.ts +25 -4
- package/src/templates/scaffold-repo.ts +116 -257
- package/src/templates/scaffold-roles.ts +68 -0
- package/src/templates/scaffold-ui-package.ts +56 -0
- package/src/templates/slice-foundation.ts +88 -0
- package/src/templates/wrap.ts +95 -0
- package/src/test-counts.ts +35 -0
- package/src/test-select.ts +30 -15
- package/src/test-shards.ts +21 -3
- package/src/test-workers.ts +47 -0
- package/src/ts-scan.ts +271 -13
- package/src/tsconfig-references.ts +78 -0
- package/src/verify-floor.ts +133 -0
- package/src/verify-step.ts +19 -0
- package/src/verify-test-run.ts +72 -0
- package/src/verify-tests.ts +160 -71
- package/src/version-loader.ts +20 -3
- package/src/workspace-checks.ts +87 -16
- package/src/write-line.ts +34 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// The X_* codes owned by @ultimat3/cli, and nothing else: the two lists, their titles, the one
|
|
2
|
+
// registration call and `docsFor`. Every code names the exact command that resolves it, because
|
|
3
|
+
// the CLI is the surface an agent reads first — a failure here has to be actionable without a doc
|
|
4
|
+
// lookup or a second round-trip. The classes that throw these codes live in `./errors`.
|
|
5
|
+
import { registerErrorCodes } from '@ultimat3/core';
|
|
6
|
+
|
|
7
|
+
/** Codes this package declares and owns. */
|
|
8
|
+
export const CLI_OWNED_ERROR_CODES = [
|
|
9
|
+
'X_CLI_UNKNOWN_COMMAND',
|
|
10
|
+
'X_CLI_BAD_FLAG',
|
|
11
|
+
'X_VERIFY_FAILED',
|
|
12
|
+
'X_NOT_IN_APP',
|
|
13
|
+
'X_BUN_VERSION',
|
|
14
|
+
'X_TEST_NO_FILES',
|
|
15
|
+
'X_TEST_SHARD_FAILED',
|
|
16
|
+
'X_SCAFFOLD_PATH_ESCAPE',
|
|
17
|
+
'X_GENERATE_JSON_INVALID',
|
|
18
|
+
'X_APP_PACKAGE_INVALID',
|
|
19
|
+
'X_ERROR_CODE_UNKNOWN',
|
|
20
|
+
'X_DECLARATION_UNKNOWN',
|
|
21
|
+
'X_JOB_UNKNOWN',
|
|
22
|
+
'X_FIX_TARGET_UNKNOWN',
|
|
23
|
+
'X_ERROR_FIX_INVALID',
|
|
24
|
+
'X_ERROR_CODE_UNDOCUMENTED',
|
|
25
|
+
'X_ERROR_CODE_UNREGISTERED',
|
|
26
|
+
// Reported as `Finding`s rather than thrown, and unregistered until now because of it — so
|
|
27
|
+
// `x errors explain X_TYPECHECK_FAILED` refused a code `x verify` had just printed. A finding
|
|
28
|
+
// carries an `X_*` code to the same reader a throw does; the registry is what makes that code
|
|
29
|
+
// explainable, unique and documented-or-fail, so a code the CLI emits is a code the CLI owns.
|
|
30
|
+
'X_CLI_UNEXPECTED',
|
|
31
|
+
'X_TYPECHECK_FAILED',
|
|
32
|
+
'X_LINT_FAILED',
|
|
33
|
+
'X_TEST_FAILED',
|
|
34
|
+
// The ratchet's own code. A skipped step is not a failure — unless `x.verify.json` says this
|
|
35
|
+
// repo already ran it, in which case the suite was deleted and the gate would otherwise print
|
|
36
|
+
// one more green line for it.
|
|
37
|
+
'X_VERIFY_SUITE_VANISHED',
|
|
38
|
+
'X_FILE_TOO_LONG',
|
|
39
|
+
'X_PACKAGE_SHAPE',
|
|
40
|
+
// The build graph's own membership rule. `tsc -b` compiles referenced projects and nothing
|
|
41
|
+
// else, so a workspace no root reference names is one the `typecheck` step passes over without
|
|
42
|
+
// reading — the hole that let `scripts/` hold seven type errors under a green gate.
|
|
43
|
+
'X_PACKAGE_UNREFERENCED',
|
|
44
|
+
'X_RELEASE_VERSION_SKEW',
|
|
45
|
+
'X_STORAGE_UNWRITABLE',
|
|
46
|
+
'X_STORAGE_SECRET_DEV',
|
|
47
|
+
'X_MANIFEST_STALE',
|
|
48
|
+
'X_BUDGET_UNMEASURED',
|
|
49
|
+
'X_BUILD_FAILED',
|
|
50
|
+
'X_BUILD_ENTRY_MISSING',
|
|
51
|
+
'X_DEPLOY_FAILED',
|
|
52
|
+
// The two the container's own environment can get wrong. A PaaS injects `PORT` and a supervisor
|
|
53
|
+
// injects `ROLE`; both arrive as strings from outside the app, so both are validated at boot
|
|
54
|
+
// rather than defaulted — a web role that quietly bound 3000 when the platform said 8080 fails
|
|
55
|
+
// its health check with nothing in the log that names the cause.
|
|
56
|
+
'X_ROLE_UNKNOWN',
|
|
57
|
+
'X_PORT_INVALID',
|
|
58
|
+
// The boot's own consistency check. `startServices` captures the drivers it built, and
|
|
59
|
+
// `loadApp` runs AFTER it — so an app module calling `setJobDriver(theirs)` moved the ambient
|
|
60
|
+
// slot and left the captured object alone: every `handle.enqueue()` went to their queue while
|
|
61
|
+
// the worker claimed from Postgres, and the `/_x` panel read the ambient one and agreed with
|
|
62
|
+
// the enqueue side. Nothing failed. Refused here rather than documented, per axiom 3.
|
|
63
|
+
'X_RUNTIME_DRIVER_SPLIT',
|
|
64
|
+
'X_GENERATE_CONFLICT',
|
|
65
|
+
'X_PORT_IN_USE',
|
|
66
|
+
'X_DB_GEN_FAILED',
|
|
67
|
+
'X_DB_MIGRATE_FAILED',
|
|
68
|
+
'X_DB_BRANCH_FAILED',
|
|
69
|
+
'X_DB_STUDIO_FAILED',
|
|
70
|
+
// The five app-surface boundary codes. `@ultimat3/render` owns the *rule* (`checkSurfaceBoundary`)
|
|
71
|
+
// and the CLI owns the diagnostic, because `x verify` and `x fix boundary` are the two commands
|
|
72
|
+
// that report it — see `app-boundaries.ts`, which holds the one rule-to-code table.
|
|
73
|
+
'X_BOUNDARY_SITE_TO_APP',
|
|
74
|
+
'X_BOUNDARY_SHARED_LEAF',
|
|
75
|
+
'X_BOUNDARY_APP_TO_API',
|
|
76
|
+
'X_BOUNDARY_ROUTE_TO_DB',
|
|
77
|
+
'X_BOUNDARY_SERVICE_TO_HTTP',
|
|
78
|
+
// The three ways an app's own guard can fail to be one. A guard is the app's convention made
|
|
79
|
+
// into a build error, so a guard the gate cannot trust has to be a finding rather than a skip:
|
|
80
|
+
// an app that believes its rule is enforced and is not is worse off than one with no rule.
|
|
81
|
+
'X_GUARD_INVALID',
|
|
82
|
+
'X_GUARD_FAILED',
|
|
83
|
+
'X_GUARD_FINDING_INVALID',
|
|
84
|
+
// The two halves of `x secrets edit` that belong to the terminal rather than to the envelope.
|
|
85
|
+
// `@ultimat3/core` owns every X_SECRETS_* code about the file and the key; an editor is the
|
|
86
|
+
// CLI's problem alone, and core would have no `fix:` to offer for one.
|
|
87
|
+
'X_SECRETS_EDITOR_MISSING',
|
|
88
|
+
'X_SECRETS_EDIT_FAILED',
|
|
89
|
+
] as const;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* `X_NOT_IMPLEMENTED` is `@ultimat3/core`'s — `CliNotImplementedError` and every planned command
|
|
93
|
+
* throw it, and none of them may declare a title for it. The CLI is the process that imports every
|
|
94
|
+
* package (`error-catalog.ts`), so a title declared twice here is the one that would win by load
|
|
95
|
+
* order rather than by ownership.
|
|
96
|
+
*
|
|
97
|
+
* The three env codes are core's for the same reason: `defineEnv`, `checkEnv` and the
|
|
98
|
+
* `.env.example` projection all live in `@ultimat3/core`, and `x env` is the surface that reports
|
|
99
|
+
* them. A fourth code meaning "the example is stale" would be this package inventing a second name
|
|
100
|
+
* for a condition core already named.
|
|
101
|
+
*
|
|
102
|
+
* `X_ISLAND_INVALID` is `@ultimat3/render`'s and is borrowed for the same reason: "this src cannot
|
|
103
|
+
* become a client entry" is what that code already means, and the bundler is simply the half that
|
|
104
|
+
* can see whether the file exists. A CLI-owned twin would be one condition with two names.
|
|
105
|
+
*/
|
|
106
|
+
export const CLI_BORROWED_ERROR_CODES = [
|
|
107
|
+
'X_NOT_IMPLEMENTED',
|
|
108
|
+
'X_CONFIG_INVALID',
|
|
109
|
+
'X_ENV_MISSING',
|
|
110
|
+
'X_ENV_EXAMPLE_DRIFT',
|
|
111
|
+
'X_ISLAND_INVALID',
|
|
112
|
+
] as const;
|
|
113
|
+
|
|
114
|
+
/** Every code the CLI can throw: the ones it owns plus the one it borrows. */
|
|
115
|
+
export const CLI_ERROR_CODES = [...CLI_OWNED_ERROR_CODES, ...CLI_BORROWED_ERROR_CODES] as const;
|
|
116
|
+
|
|
117
|
+
export type CliOwnedErrorCode = (typeof CLI_OWNED_ERROR_CODES)[number];
|
|
118
|
+
export type CliErrorCode = (typeof CLI_ERROR_CODES)[number];
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Registered titles, so `x errors list` enumerates the CLI's codes alongside every other
|
|
122
|
+
* package's instead of leaving a hole an agent has to read source to fill. Typed over
|
|
123
|
+
* `CliOwnedErrorCode`, so adding a code without a title is a build error.
|
|
124
|
+
*/
|
|
125
|
+
export const CLI_ERROR_TITLES: Readonly<Record<CliOwnedErrorCode, string>> = {
|
|
126
|
+
X_CLI_UNKNOWN_COMMAND: 'not a command in the registry',
|
|
127
|
+
X_CLI_BAD_FLAG: 'unknown flag, missing value, or a value the command refuses',
|
|
128
|
+
X_VERIFY_FAILED: 'at least one x verify step failed',
|
|
129
|
+
X_NOT_IN_APP: 'the command needs an app root and found none',
|
|
130
|
+
X_BUN_VERSION: 'Bun is older than the framework floor',
|
|
131
|
+
X_TEST_NO_FILES: 'the test selection matched no files',
|
|
132
|
+
X_TEST_SHARD_FAILED: 'a test shard exited non-zero',
|
|
133
|
+
X_SCAFFOLD_PATH_ESCAPE: 'a generated path resolves outside the directory it is written into',
|
|
134
|
+
X_GENERATE_JSON_INVALID: "a generator's own merge: 'json' output does not parse as a JSON object",
|
|
135
|
+
X_APP_PACKAGE_INVALID: "the app's package.json supplies no name and version",
|
|
136
|
+
X_ERROR_CODE_UNKNOWN: 'no package registered this error code',
|
|
137
|
+
X_DECLARATION_UNKNOWN: 'no declaration with this name is registered',
|
|
138
|
+
X_JOB_UNKNOWN: 'the queue holds no job with this id',
|
|
139
|
+
X_FIX_TARGET_UNKNOWN: 'the named file is not one of the app source files',
|
|
140
|
+
X_ERROR_FIX_INVALID: "an error's fix line is not a runnable instruction",
|
|
141
|
+
X_ERROR_CODE_UNDOCUMENTED: 'a shipped error code has no row in the error reference',
|
|
142
|
+
X_ERROR_CODE_UNREGISTERED: 'the error reference documents a code no package registers',
|
|
143
|
+
X_STORAGE_UNWRITABLE: 'the storage disk this process needs cannot be written to',
|
|
144
|
+
X_STORAGE_SECRET_DEV: 'upload grants would be signed with the shipped development key',
|
|
145
|
+
X_CLI_UNEXPECTED: 'the CLI itself failed',
|
|
146
|
+
X_TYPECHECK_FAILED: 'tsc failed',
|
|
147
|
+
X_LINT_FAILED: 'Biome failed',
|
|
148
|
+
X_TEST_FAILED: 'a test type failed',
|
|
149
|
+
X_VERIFY_SUITE_VANISHED: 'a step the committed floor requires had nothing left to check',
|
|
150
|
+
X_FILE_TOO_LONG: 'a source file is over 500 lines',
|
|
151
|
+
X_PACKAGE_SHAPE: 'a workspace package is missing a contract file',
|
|
152
|
+
X_PACKAGE_UNREFERENCED: 'a published workspace is not in the root tsconfig build graph',
|
|
153
|
+
X_RELEASE_VERSION_SKEW: 'a workspace is not at the lockstep version',
|
|
154
|
+
X_MANIFEST_STALE: 'openapi.json is stale',
|
|
155
|
+
X_BUDGET_UNMEASURED: 'a route declares a budget the build never measured',
|
|
156
|
+
X_BUILD_FAILED: 'x build failed',
|
|
157
|
+
X_BUILD_ENTRY_MISSING: "the build target's entry file is not in the app",
|
|
158
|
+
X_DEPLOY_FAILED: 'a deploy step failed',
|
|
159
|
+
X_ROLE_UNKNOWN: 'ROLE names something that is not a role',
|
|
160
|
+
X_PORT_INVALID: 'PORT is not a TCP port number',
|
|
161
|
+
X_RUNTIME_DRIVER_SPLIT: 'the ambient driver is not the one this process serves',
|
|
162
|
+
X_GENERATE_CONFLICT: 'a generator would overwrite a file',
|
|
163
|
+
X_PORT_IN_USE: 'the dev port is taken',
|
|
164
|
+
X_DB_GEN_FAILED: 'x db gen failed',
|
|
165
|
+
X_DB_MIGRATE_FAILED: 'x db migrate failed',
|
|
166
|
+
X_DB_BRANCH_FAILED: 'an x db branch step failed',
|
|
167
|
+
X_DB_STUDIO_FAILED: 'x db studio failed',
|
|
168
|
+
X_BOUNDARY_SITE_TO_APP: 'site/ imported app/',
|
|
169
|
+
X_BOUNDARY_SHARED_LEAF: 'shared/ imported a surface',
|
|
170
|
+
X_BOUNDARY_APP_TO_API: 'app/ imported api/ at runtime',
|
|
171
|
+
X_BOUNDARY_ROUTE_TO_DB: 'a route touched the database',
|
|
172
|
+
X_BOUNDARY_SERVICE_TO_HTTP: 'a service imported HTTP',
|
|
173
|
+
X_GUARD_INVALID: 'a file in guards/ exports no usable guard',
|
|
174
|
+
X_GUARD_FAILED: 'an app guard threw instead of returning findings',
|
|
175
|
+
X_GUARD_FINDING_INVALID: "an app guard's finding breaks the error contract",
|
|
176
|
+
X_SECRETS_EDITOR_MISSING: 'no $EDITOR to open the decrypted secrets in',
|
|
177
|
+
X_SECRETS_EDIT_FAILED: 'the editor exited non-zero, so nothing was resealed',
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// One unconditional call, so a second package claiming one of the CLI's codes throws
|
|
181
|
+
// X_ERROR_CODE_DUPLICATE instead of losing silently to whichever module imported first.
|
|
182
|
+
registerErrorCodes(
|
|
183
|
+
Object.fromEntries(Object.entries(CLI_ERROR_TITLES).map(([code, title]) => [code, { title }])),
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
export const docsFor = (code: CliErrorCode): string => `https://ultimate.dev/errors/${code}`;
|
package/src/error-contract.ts
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
// `join` is `node:`-only by necessity: Bun exposes no path-join primitive.
|
|
8
8
|
import { join } from 'node:path';
|
|
9
|
-
import { docsFor } from './
|
|
9
|
+
import { docsFor } from './error-codes';
|
|
10
|
+
import { citedCommandProblem, loadCommandCatalog } from './fix-command';
|
|
10
11
|
import type { Finding } from './output';
|
|
11
12
|
import { eachSourceFile, isGenerated, isTest } from './source-files';
|
|
12
13
|
import type { CodeSite, FixSite } from './ts-scan';
|
|
@@ -17,7 +18,12 @@ export const BANNED_PHRASES: readonly RegExp[] = [
|
|
|
17
18
|
/\bcheck(s|ed|ing)?\b/i,
|
|
18
19
|
/\bmake sure\b/i,
|
|
19
20
|
/\btry(ing)?\b/i,
|
|
20
|
-
|
|
21
|
+
// The whole family, not one spelling of it. This was `see the docs?` — one article longer than
|
|
22
|
+
// `see docs`, which `@ultimat3/mcp`'s `server.ts` shipped as a `fix:` and which passed the gate
|
|
23
|
+
// for that reason alone. Measured against all 740 shipped fix literals: the wider pattern
|
|
24
|
+
// matches none of them, so it costs nothing and closes `read the docs` and `see documentation`
|
|
25
|
+
// before either is written.
|
|
26
|
+
/\b(?:see|read|consult|refer to)\s+(?:the\s+)?(?:docs?|documentation)\b/i,
|
|
21
27
|
];
|
|
22
28
|
|
|
23
29
|
/**
|
|
@@ -59,13 +65,28 @@ const fixFinding = (site: FixSite, problem: string): Finding => ({
|
|
|
59
65
|
at: `${site.at}:${site.line}`,
|
|
60
66
|
});
|
|
61
67
|
|
|
62
|
-
/**
|
|
68
|
+
/**
|
|
69
|
+
* Every `fix:` an agent can be handed, read out of shipped source and held to BOTH rules: it must
|
|
70
|
+
* be an instruction, and any `x <command>` it cites must be one this build ships.
|
|
71
|
+
*
|
|
72
|
+
* The second rule is the one a text scan could never decide. Six fix lines shipped citing
|
|
73
|
+
* `x db status`, `x logs tail`, `x trace`, `x metrics`, `x auth whoami` and `x ai prompts` — all
|
|
74
|
+
* of them named a command, so all of them passed, and every one handed its reader
|
|
75
|
+
* `X_NOT_IMPLEMENTED` or `X_CLI_UNKNOWN_COMMAND` in place of the fix it promised.
|
|
76
|
+
*
|
|
77
|
+
* The catalog is loaded ONCE per run rather than per fix line: it is a dynamic import (see
|
|
78
|
+
* `fix-command.ts` for the cycle it breaks) and this walks every shipped source file.
|
|
79
|
+
*/
|
|
63
80
|
export async function checkErrorFixes(root: string): Promise<readonly Finding[]> {
|
|
64
81
|
const findings: Finding[] = [];
|
|
82
|
+
const catalog = await loadCommandCatalog();
|
|
65
83
|
for await (const path of eachSourceFile(root)) {
|
|
66
84
|
if (isTest(path) || isGenerated(path)) continue;
|
|
67
85
|
for (const site of scanFixes(await Bun.file(join(root, path)).text(), path)) {
|
|
68
|
-
|
|
86
|
+
// The interpolation-blanked form for both rules: `x ${name}` names no command this can
|
|
87
|
+
// resolve, and reading `<value>` as one would be a finding nobody can act on.
|
|
88
|
+
const fix = staticFix(site.fix);
|
|
89
|
+
const problem = fixProblem(site.fix) ?? citedCommandProblem(fix, catalog);
|
|
69
90
|
if (problem !== undefined) findings.push(fixFinding(site, problem));
|
|
70
91
|
}
|
|
71
92
|
}
|
|
@@ -148,8 +169,8 @@ export async function checkErrorCodeRegistry(
|
|
|
148
169
|
* it borrows in that same file — so a registry that owns the code outranks a throw site, and a
|
|
149
170
|
* registry that has said the code is somebody else's ranks below both.
|
|
150
171
|
*/
|
|
151
|
-
const claim = (site: CodeSite, borrowed: ReadonlySet<string>): number => {
|
|
152
|
-
if (!
|
|
172
|
+
const claim = (site: CodeSite, registry: boolean, borrowed: ReadonlySet<string>): number => {
|
|
173
|
+
if (!registry) return 1;
|
|
153
174
|
return borrowed.has(site.code) ? 0 : 2;
|
|
154
175
|
};
|
|
155
176
|
|
|
@@ -178,8 +199,9 @@ export async function collectDeclaredCodes(root: string): Promise<readonly CodeS
|
|
|
178
199
|
if (isTest(source) || isGenerated(source)) continue;
|
|
179
200
|
const text = await Bun.file(join(root, source)).text();
|
|
180
201
|
const borrowed = scanBorrowedCodes(text);
|
|
202
|
+
const registry = isCodeRegistry(text);
|
|
181
203
|
for (const site of scanCodes(text, source)) {
|
|
182
|
-
const found: [CodeSite, number] = [site, claim(site, borrowed)];
|
|
204
|
+
const found: [CodeSite, number] = [site, claim(site, registry, borrowed)];
|
|
183
205
|
const seen = sites.get(site.code);
|
|
184
206
|
sites.set(site.code, seen === undefined ? found : declarationOf(seen, found));
|
|
185
207
|
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// Every `X_*` code's REAL `fix:`, read off the throw site that raises it. `x errors explain` used
|
|
2
|
+
// to answer `x verify --json` for every code the CLI does not own — 327 of 378 — which is a shrug,
|
|
3
|
+
// not an instruction. The framework already writes an executable fix at each throw site and the
|
|
4
|
+
// `errors` gate step already proves each one runnable, so the answer is to project that text
|
|
5
|
+
// rather than to restate it in a second table nobody keeps current (axiom 2).
|
|
6
|
+
|
|
7
|
+
// Bun ships no path join: the scan needs an absolute path per globbed, scope-relative entry.
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
import { staticFix } from './error-contract';
|
|
10
|
+
import { frameworkScopeDir } from './framework-scope';
|
|
11
|
+
import { isGenerated, isTest, isVendored } from './source-files';
|
|
12
|
+
import type { CodeFixSite } from './ts-scan';
|
|
13
|
+
import { scanCodeFixSites } from './ts-scan';
|
|
14
|
+
|
|
15
|
+
/** Every throw site of one code, sorted by file then line so two machines answer identically. */
|
|
16
|
+
export type CodeFixIndex = ReadonlyMap<string, readonly CodeFixSite[]>;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Published packages ship `src` (`"exports": "./src/index.ts"` — the artifact IS the source), so
|
|
20
|
+
* this glob reaches the same files in `node_modules/@ultimat3` that it reaches in `packages/`.
|
|
21
|
+
*/
|
|
22
|
+
const PACKAGE_SOURCES = '*/src/**/*.{ts,tsx}';
|
|
23
|
+
|
|
24
|
+
const byPosition = (a: CodeFixSite, b: CodeFixSite): number =>
|
|
25
|
+
a.at === b.at ? a.line - b.line : a.at.localeCompare(b.at);
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* `x docs`'s `locate()` spelling, deliberately: `@ultimat3/render/src/errors.ts` is the one form
|
|
29
|
+
* that is both a resolvable specifier in an app and an unambiguous file in this monorepo, and two
|
|
30
|
+
* spellings of "where that is" is two things for a reader to learn.
|
|
31
|
+
*/
|
|
32
|
+
const located = (path: string): string => `@ultimat3/${path}`;
|
|
33
|
+
|
|
34
|
+
/** One scope directory in, one index out — pure enough for a test to point at a fixture tree. */
|
|
35
|
+
export async function scanScopeFixes(scope: string): Promise<CodeFixIndex> {
|
|
36
|
+
const index = new Map<string, CodeFixSite[]>();
|
|
37
|
+
// `followSymlinks` defaults to FALSE. `node_modules/@ultimat3/*` is a symlink per package under
|
|
38
|
+
// `bun link` and under any workspace an app resolves without realpath, and the default answer
|
|
39
|
+
// there is an empty index — the silent kind of empty, where every code falls back and nothing
|
|
40
|
+
// says a walk found nothing. The scope is a package directory, so there is no tree to run away
|
|
41
|
+
// into.
|
|
42
|
+
const walk = new Bun.Glob(PACKAGE_SOURCES).scan({ cwd: scope, followSymlinks: true });
|
|
43
|
+
for await (const path of walk) {
|
|
44
|
+
if (isTest(path) || isGenerated(path) || isVendored(path)) continue;
|
|
45
|
+
const text = await Bun.file(join(scope, path)).text();
|
|
46
|
+
for (const site of scanCodeFixSites(text, located(path))) {
|
|
47
|
+
// `${…}` holds a value only the throw site knows. Blanked to `<value>` — the same shape the
|
|
48
|
+
// `errors` step judges the line in — because a fix quoting a variable name an agent cannot
|
|
49
|
+
// resolve reads as a command it can paste, and it is not one.
|
|
50
|
+
const found: CodeFixSite =
|
|
51
|
+
site.fix === undefined ? site : { ...site, fix: staticFix(site.fix) };
|
|
52
|
+
index.set(site.code, [...(index.get(site.code) ?? []), found]);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
for (const sites of index.values()) sites.sort(byPosition);
|
|
56
|
+
return index;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Whether the walk happened, which an empty index cannot say on its own. `unread` and `read` both
|
|
61
|
+
* produce no entry for a code and they mean opposite things: "this framework does not raise it"
|
|
62
|
+
* against "this process never got to look". A fallback line that cannot tell them apart states the
|
|
63
|
+
* first as fact whenever the second is true.
|
|
64
|
+
*/
|
|
65
|
+
export type CodeFixScan = 'unread' | 'read';
|
|
66
|
+
|
|
67
|
+
let pending: Promise<CodeFixIndex> | undefined;
|
|
68
|
+
let scanned: CodeFixIndex = new Map();
|
|
69
|
+
let state: CodeFixScan = 'unread';
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Best-effort by design, and therefore **never** a throw. One unreadable file under a
|
|
73
|
+
* `node_modules` an installer left half-written would otherwise reject `loadCodeFixes()`, and its
|
|
74
|
+
* three callers await it unguarded — so a permission bit would take `x errors explain` and the
|
|
75
|
+
* whole of `x mcp serve`'s startup down for an index that is a nicety. `pending` caches the
|
|
76
|
+
* settled promise either way, so a rejection would have been permanent for the process too.
|
|
77
|
+
*/
|
|
78
|
+
async function build(): Promise<CodeFixIndex> {
|
|
79
|
+
const scope = frameworkScopeDir();
|
|
80
|
+
if (scope === undefined) return scanned;
|
|
81
|
+
try {
|
|
82
|
+
scanned = await scanScopeFixes(scope);
|
|
83
|
+
state = 'read';
|
|
84
|
+
} catch {
|
|
85
|
+
// Deliberately swallowed and deliberately not logged: `explainErrorCode` is the reporter, and
|
|
86
|
+
// `state` is what makes it say "could not be read" instead of "nothing raises this code".
|
|
87
|
+
scanned = new Map();
|
|
88
|
+
}
|
|
89
|
+
return scanned;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Memoised: one walk of the installed framework per process, for the same reason `loadErrorCatalog`
|
|
94
|
+
* imports every package once. Callers await this before anything reads `codeFixes()`, exactly as
|
|
95
|
+
* they await the catalog before reading `listErrorCodes()` — the seam is synchronous because
|
|
96
|
+
* `@ultimat3/mcp`'s `DevCapabilities.explainError` is.
|
|
97
|
+
*/
|
|
98
|
+
export function loadCodeFixes(): Promise<CodeFixIndex> {
|
|
99
|
+
pending ??= build();
|
|
100
|
+
return pending;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** What has been loaded, synchronously. Empty until `loadCodeFixes()` has resolved. */
|
|
104
|
+
export const codeFixes = (): CodeFixIndex => scanned;
|
|
105
|
+
|
|
106
|
+
/** Whether the index above is an answer or an absence. */
|
|
107
|
+
export const codeFixScan = (): CodeFixScan => state;
|
|
108
|
+
|
|
109
|
+
/** Test seam — the counterpart to `resetErrorCatalog`. */
|
|
110
|
+
export function resetCodeFixes(): void {
|
|
111
|
+
pending = undefined;
|
|
112
|
+
scanned = new Map();
|
|
113
|
+
state = 'unread';
|
|
114
|
+
}
|