@ultimat3/cli 22.15.0 → 24.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 +36 -13
- package/README.md +31 -8
- package/package.json +29 -28
- package/src/admin-catalog-keys.ts +16 -0
- package/src/admin-registration.ts +192 -0
- package/src/affected.ts +27 -35
- package/src/api-registration.ts +125 -11
- package/src/app-artifacts.ts +1 -1
- package/src/app-auth.ts +4 -27
- package/src/app-boundaries.ts +6 -20
- package/src/app-config-load.ts +51 -0
- package/src/app-env.ts +5 -11
- package/src/app-load.ts +55 -11
- package/src/app-manifest.ts +16 -8
- package/src/app-mcp.ts +10 -23
- package/src/app-permissions-borrowed.ts +129 -0
- package/src/app-permissions-site.ts +24 -0
- package/src/app-permissions.ts +0 -0
- package/src/app-reload-graph.ts +7 -30
- package/src/app-transport.ts +219 -0
- package/src/boundary-findings.ts +16 -4
- package/src/browser-transport.ts +160 -0
- package/src/budgets.ts +27 -34
- package/src/cdp-shot-driver.ts +3 -1
- package/src/cmd-auth-spec.ts +16 -0
- package/src/cmd-auth.ts +65 -0
- package/src/cmd-build-spec.ts +10 -3
- package/src/cmd-build.ts +107 -11
- package/src/cmd-db-backfill.ts +25 -12
- package/src/cmd-db.ts +66 -16
- package/src/cmd-deploy-helm.ts +7 -6
- package/src/cmd-deploy.ts +4 -4
- package/src/cmd-dev.ts +9 -8
- package/src/cmd-doctor-spec.ts +2 -9
- package/src/cmd-doctor.ts +75 -24
- package/src/cmd-env.ts +26 -2
- package/src/cmd-generate.ts +195 -47
- package/src/cmd-help.ts +19 -6
- package/src/cmd-i18n.ts +10 -9
- package/src/cmd-jobs-spec.ts +19 -3
- package/src/cmd-jobs.ts +136 -7
- package/src/cmd-manifest-spec.ts +1 -1
- package/src/cmd-manifest.ts +6 -25
- package/src/cmd-mcp.ts +46 -15
- package/src/cmd-new.ts +15 -1
- package/src/cmd-planned.ts +15 -8
- package/src/cmd-routes.ts +27 -3
- package/src/cmd-secrets-ring.ts +76 -0
- package/src/cmd-secrets-spec.ts +10 -2
- package/src/cmd-secrets.ts +95 -18
- package/src/cmd-shot-matrix.ts +1 -3
- package/src/cmd-shot.ts +12 -3
- package/src/cmd-tasks.ts +84 -15
- package/src/cmd-test-spec.ts +1 -7
- package/src/cmd-test.ts +13 -29
- package/src/cmd-verify.ts +13 -5
- package/src/comment-safe.ts +16 -0
- package/src/coverage-floor.ts +334 -0
- package/src/coverage-lcov.ts +193 -0
- package/src/coverage-source.ts +136 -0
- package/src/db-object-drift.ts +30 -0
- package/src/db-replay-engine.ts +115 -0
- package/src/db-schema-dump.ts +150 -0
- package/src/db-schema-refresh.ts +122 -0
- package/src/db-subscribes.ts +12 -4
- package/src/dev-dashboard-guard.ts +76 -0
- package/src/dev-dashboard.ts +51 -29
- package/src/dev-lock.ts +4 -7
- package/src/dev-route-table.ts +9 -0
- package/src/dispatch.ts +3 -6
- package/src/doctor-app-url.ts +39 -0
- package/src/doctor-auth.ts +95 -0
- package/src/doctor-coverage.ts +39 -0
- package/src/doctor-guards.ts +54 -0
- package/src/doctor-sealed.ts +132 -0
- package/src/document-graph.ts +82 -0
- package/src/drift-replica-identity.ts +42 -0
- package/src/error-catalog.ts +6 -9
- package/src/error-codes.ts +55 -3
- package/src/error-unthrown.ts +29 -11
- package/src/errors.ts +54 -83
- package/src/framework-schema-apply.ts +78 -0
- package/src/framework-schema-stamp.ts +96 -0
- package/src/framework-schema.ts +28 -2
- package/src/generate-files.ts +79 -12
- package/src/generate-grant-findings.ts +76 -0
- package/src/generate-grants.ts +77 -3
- package/src/generate-kinds.ts +52 -1
- package/src/generate-shadow.ts +46 -0
- package/src/generate-write.ts +62 -40
- package/src/generated-imports.ts +59 -0
- package/src/gh-target.ts +12 -7
- package/src/guard-sources.ts +104 -0
- package/src/guards.ts +23 -7
- package/src/handle-registration.ts +217 -0
- package/src/i18n-audit.ts +4 -1
- package/src/i18n-registration.ts +36 -0
- package/src/image-prepare.ts +51 -8
- package/src/import-closure.ts +165 -0
- package/src/index.ts +12 -5
- package/src/island-bundle.ts +3 -15
- package/src/island-store.ts +24 -16
- package/src/jobs-drain.ts +95 -21
- package/src/jobs-json.ts +50 -0
- package/src/jobs-report.ts +7 -4
- package/src/json-merge.ts +46 -0
- package/src/loopback-host.ts +13 -0
- package/src/manifest-staleness.ts +62 -0
- package/src/mcp-errors.ts +41 -4
- package/src/mcp-host.ts +16 -9
- package/src/mcp-test-run.ts +38 -0
- package/src/messages.ts +35 -0
- package/src/metrics-endpoint.ts +95 -24
- package/src/migration-extensions.ts +29 -0
- package/src/module-imports.ts +47 -0
- package/src/output.ts +24 -9
- package/src/page-navigation.ts +7 -23
- package/src/page-speculation.ts +5 -20
- package/src/permission-grants.ts +41 -9
- package/src/prerender-out.ts +34 -7
- package/src/prerender.ts +8 -5
- package/src/publish-closure.ts +101 -0
- package/src/pwa-artifacts.ts +26 -94
- package/src/registry.ts +2 -0
- package/src/revalidated-response.ts +4 -3
- package/src/role-load.ts +143 -0
- package/src/role-replicator.ts +16 -0
- package/src/role-start.ts +27 -5
- package/src/role-sync.ts +55 -20
- package/src/role-wake.ts +45 -0
- package/src/runtime-admin.ts +141 -0
- package/src/runtime-assets.ts +11 -7
- package/src/runtime-bindings.ts +4 -4
- package/src/runtime-cache.ts +38 -47
- package/src/runtime-hooks.ts +5 -2
- package/src/runtime-idempotency-scope.ts +23 -0
- package/src/runtime-isr.ts +36 -0
- package/src/runtime-jobs.ts +8 -33
- package/src/runtime-mfa-warning.ts +29 -0
- package/src/runtime-notify-retention.ts +10 -46
- package/src/runtime-overrides.ts +1 -1
- package/src/runtime-purge.ts +38 -16
- package/src/runtime-queue.ts +39 -11
- package/src/runtime-realtime.ts +9 -45
- package/src/runtime-render.ts +68 -35
- package/src/runtime-services.ts +22 -18
- package/src/runtime-storage.ts +10 -5
- package/src/scaffold-typecheck.ts +1 -0
- package/src/schema-diff.ts +16 -0
- package/src/schema-drift.ts +45 -15
- package/src/schema-dump-drift.ts +64 -0
- package/src/seo-routes.ts +80 -9
- package/src/serve-boot.ts +114 -158
- package/src/serve-drain.ts +9 -33
- package/src/serve-env.ts +9 -6
- package/src/serve-prebuilt-paths.ts +19 -0
- package/src/serve-prebuilt.ts +84 -0
- package/src/serve-web.ts +169 -0
- package/src/serve.ts +6 -9
- package/src/server-barrels.ts +54 -0
- package/src/shot-locale.ts +5 -23
- package/src/site-assets.ts +3 -5
- package/src/site-config.ts +9 -48
- package/src/site-seo.ts +1 -1
- package/src/sitemap-lastmod.ts +5 -0
- package/src/solid-loader.ts +12 -3
- package/src/source-edit.ts +49 -0
- package/src/source-files.ts +6 -0
- package/src/stored-object-headers.ts +43 -0
- package/src/sync-url.ts +2 -2
- package/src/tasks-facts.ts +3 -26
- package/src/templates/action.ts +147 -11
- package/src/templates/admin-catalog.ts +58 -0
- package/src/templates/admin-page.ts +36 -8
- package/src/templates/admin.ts +57 -17
- package/src/templates/backfill.ts +121 -87
- package/src/templates/entity.ts +140 -44
- package/src/templates/guard-animated-layout-property.ts +4 -11
- package/src/templates/guard-bare-error.ts +9 -13
- package/src/templates/guard-focus-visible.ts +4 -11
- package/src/templates/guard-image-dimensions.ts +6 -6
- package/src/templates/guard-island-without-states.ts +2 -4
- package/src/templates/guard-raw-breakpoint.ts +210 -0
- package/src/templates/guard-raw-colour.ts +4 -11
- package/src/templates/guard-raw-length.ts +375 -0
- package/src/templates/guard-raw-motion.ts +213 -0
- package/src/templates/guard-raw-shadow.ts +187 -0
- package/src/templates/guard-raw-z-index.ts +166 -0
- package/src/templates/guard-repo-raw-sql.ts +345 -0
- package/src/templates/guard-semantic-interactive.ts +6 -6
- package/src/templates/guard-undeclared-custom-property.ts +217 -0
- package/src/templates/guard-undefined-style-class.ts +215 -0
- package/src/templates/guard-untranslated-string.ts +6 -6
- package/src/templates/guard-unzoned-date.ts +9 -13
- package/src/templates/index.ts +3 -2
- package/src/templates/island.ts +34 -51
- package/src/templates/job-body-test.ts +121 -0
- package/src/templates/job.ts +23 -8
- package/src/templates/naming.ts +28 -0
- package/src/templates/policy.ts +16 -9
- package/src/templates/query.ts +242 -26
- package/src/templates/resource-create.ts +62 -18
- package/src/templates/resource-form-island.ts +58 -80
- package/src/templates/resource-page.ts +269 -0
- package/src/templates/resource-service.ts +96 -0
- package/src/templates/resource.ts +86 -61
- package/src/templates/route.ts +41 -35
- package/src/templates/scaffold-api.ts +55 -1
- package/src/templates/scaffold-app.ts +177 -53
- package/src/templates/scaffold-auth.ts +83 -19
- package/src/templates/scaffold-browser-client.ts +90 -0
- package/src/templates/scaffold-container-compose.ts +10 -1
- package/src/templates/scaffold-container.ts +20 -2
- package/src/templates/scaffold-dashboard-bare.ts +64 -2
- package/src/templates/scaffold-dashboard-example.ts +124 -31
- package/src/templates/scaffold-dashboard-shared.ts +1 -20
- package/src/templates/scaffold-dashboard.ts +1 -2
- package/src/templates/scaffold-db-client.ts +144 -0
- package/src/templates/scaffold-db-package.ts +90 -8
- package/src/templates/scaffold-docs.ts +50 -5
- package/src/templates/scaffold-guards.ts +54 -11
- package/src/templates/scaffold-helm-fences.ts +76 -0
- package/src/templates/scaffold-helm-templates.ts +32 -7
- package/src/templates/scaffold-helm.ts +31 -1
- package/src/templates/scaffold-http.ts +9 -7
- package/src/templates/scaffold-repo.ts +61 -3
- package/src/templates/scaffold-roles.ts +56 -9
- package/src/templates/scaffold-shell.ts +74 -105
- package/src/templates/scaffold-site.ts +45 -19
- package/src/templates/scaffold-ui-package.ts +18 -0
- package/src/templates/slice-foundation.ts +46 -8
- package/src/test-counts.ts +14 -2
- package/src/test-ignores.ts +25 -0
- package/src/test-passes.ts +0 -15
- package/src/test-select.ts +5 -14
- package/src/test-shards.ts +41 -93
- package/src/test-workers.ts +26 -4
- package/src/theme-boot.ts +6 -31
- package/src/transport-calls.ts +105 -0
- package/src/trusted-hops-error.ts +17 -0
- package/src/unscanned-admin.ts +54 -0
- package/src/unthrown-codes.ts +26 -0
- package/src/verify-checks.ts +43 -53
- package/src/verify-coverage-run.ts +190 -0
- package/src/verify-deadline.ts +353 -0
- package/src/verify-floor.ts +36 -9
- package/src/verify-merge.ts +171 -45
- package/src/verify-part-step.ts +90 -0
- package/src/verify-progress.ts +25 -0
- package/src/verify-role-load-probe.ts +54 -0
- package/src/verify-role-load.ts +148 -0
- package/src/verify-run.ts +116 -37
- package/src/verify-shard.ts +33 -11
- package/src/verify-stalled.ts +152 -0
- package/src/verify-step.ts +42 -1
- package/src/verify-tests.ts +83 -8
- package/src/workspace-checks.ts +50 -8
- package/src/workspace-dep-edit.ts +127 -0
- package/src/build-errors.ts +0 -54
- package/src/cmd-dev-fixture.ts +0 -204
- package/src/dev-live-fixture.ts +0 -122
- package/src/policy-fixture.ts +0 -78
- package/src/realtime-browser-probe-fixture.ts +0 -9
- package/src/role-start-fixture.ts +0 -78
- package/src/scaffold-fixture.ts +0 -160
- package/src/templates/island-fixture.ts +0 -76
- package/src/verify-errors.ts +0 -49
package/src/error-codes.ts
CHANGED
|
@@ -141,6 +141,11 @@ export const CLI_OWNED_ERROR_CODES = [
|
|
|
141
141
|
'X_GUARD_INVALID',
|
|
142
142
|
'X_GUARD_FAILED',
|
|
143
143
|
'X_GUARD_FINDING_INVALID',
|
|
144
|
+
// The browser-transport rule, on `boundaries` in every app (`app-transport.ts`). Both codes
|
|
145
|
+
// shipped as this repository's own script findings; the rule moved into the gate, so the CLI
|
|
146
|
+
// emits them and therefore owns them — the code strings and their meaning are unchanged.
|
|
147
|
+
'X_BROWSER_TRANSPORT_BYPASS',
|
|
148
|
+
'X_BROWSER_SERVER_BARREL',
|
|
144
149
|
// The CLI's own declarations, held to each other. A flag the parser accepts and no code reads
|
|
145
150
|
// is a promise in `x help` with nothing behind it — `x deploy --critical` said "forces clients
|
|
146
151
|
// to reload" and reached no reader outside the plan JSON it was written into.
|
|
@@ -205,11 +210,29 @@ export const CLI_OWNED_ERROR_CODES = [
|
|
|
205
210
|
'X_SITEMAP_EXTRA_INVALID',
|
|
206
211
|
'X_DEV_RESTART_REQUIRED',
|
|
207
212
|
'X_DEV_ROOT_GONE',
|
|
213
|
+
'X_SEAL_RESEAL_PENDING',
|
|
214
|
+
'X_VERIFY_STEP_TIMEOUT',
|
|
215
|
+
'X_COVERAGE_BELOW_FLOOR',
|
|
216
|
+
'X_COVERAGE_FLOOR_UNSTATED',
|
|
217
|
+
'X_COVERAGE_FLOOR_STALE',
|
|
218
|
+
'X_DB_HANDLE_UNREGISTERED',
|
|
219
|
+
'X_ROLE_LOAD_INCOMPLETE',
|
|
220
|
+
'X_ADMIN_RESOURCE_UNWIRED',
|
|
221
|
+
'X_IMAGE_NOT_PREBUILT',
|
|
222
|
+
'X_ADMIN_UNSCANNED',
|
|
223
|
+
'X_FRAMEWORK_TABLE_ORPHANED',
|
|
224
|
+
'X_BUILD_OUT_UNSAFE',
|
|
225
|
+
'X_FRAMEWORK_SCHEMA_UNAPPLIED',
|
|
226
|
+
'X_APP_URL_PORT_MISMATCH',
|
|
227
|
+
'X_DEV_HOST_REFUSED',
|
|
228
|
+
'X_TRUSTED_PROXY_HOPS_INVALID',
|
|
229
|
+
'X_PERMISSION_BORROWED',
|
|
230
|
+
'X_ERROR_CODE_UNTHROWN_STALE',
|
|
208
231
|
] as const;
|
|
209
232
|
|
|
210
233
|
/**
|
|
211
|
-
* `X_NOT_IMPLEMENTED` is `@ultimat3/core`'s —
|
|
212
|
-
*
|
|
234
|
+
* `X_NOT_IMPLEMENTED` is `@ultimat3/core`'s — every planned command throws it through core's
|
|
235
|
+
* own `NotImplementedError`, and none of them may declare a title for it. The CLI is the process that imports every
|
|
213
236
|
* package (`error-catalog.ts`), so a title declared twice here is the one that would win by load
|
|
214
237
|
* order rather than by ownership.
|
|
215
238
|
*
|
|
@@ -270,7 +293,7 @@ export const CLI_ERROR_TITLES: Readonly<Record<CliOwnedErrorCode, string>> = {
|
|
|
270
293
|
X_ERROR_CODE_UNREGISTERED: 'the error reference documents a code no package registers',
|
|
271
294
|
X_ERROR_CODE_UNRESOLVED: 'an error code is written as a name this repository cannot resolve',
|
|
272
295
|
X_ERROR_CODE_UNTHROWN:
|
|
273
|
-
'a registered error code is constructed by nothing and
|
|
296
|
+
'a registered error code is constructed by nothing and UNTHROWN_CODES in packages/cli/src/unthrown-codes.ts does not list it',
|
|
274
297
|
X_FRAMEWORK_SCHEMA_FAILED: 'a framework table could not be created at boot',
|
|
275
298
|
X_STORAGE_UNWRITABLE: 'the storage disk this process needs cannot be written to',
|
|
276
299
|
X_STORAGE_SECRET_DEV: 'upload grants would be signed with the shipped development key',
|
|
@@ -314,6 +337,10 @@ export const CLI_ERROR_TITLES: Readonly<Record<CliOwnedErrorCode, string>> = {
|
|
|
314
337
|
X_GUARD_INVALID: 'a file in guards/ exports no usable guard',
|
|
315
338
|
X_GUARD_FAILED: 'an app guard threw instead of returning findings',
|
|
316
339
|
X_GUARD_FINDING_INVALID: "an app guard's finding breaks the error contract",
|
|
340
|
+
X_BROWSER_TRANSPORT_BYPASS:
|
|
341
|
+
'browser-reachable code opens its own connection instead of using the one seam',
|
|
342
|
+
X_BROWSER_SERVER_BARREL:
|
|
343
|
+
"browser-reachable code imports a package's server barrel where the package publishes a browser entry",
|
|
317
344
|
X_CLI_FLAG_UNREAD: 'a command declares a flag no code reads',
|
|
318
345
|
X_SECRETS_EDITOR_MISSING: 'no $EDITOR to open the decrypted secrets in',
|
|
319
346
|
X_SECRETS_EDIT_FAILED: 'the editor exited non-zero, so nothing was resealed',
|
|
@@ -360,6 +387,31 @@ export const CLI_ERROR_TITLES: Readonly<Record<CliOwnedErrorCode, string>> = {
|
|
|
360
387
|
X_DEV_RESTART_REQUIRED:
|
|
361
388
|
'a save reached a module that defines a primitive; only a new process serves it',
|
|
362
389
|
X_DEV_ROOT_GONE: 'the app root x dev serves was deleted or moved',
|
|
390
|
+
X_SEAL_RESEAL_PENDING: 'sealed columns still hold values under a retired master key',
|
|
391
|
+
X_VERIFY_STEP_TIMEOUT: 'a gate step ran past its deadline and was stopped',
|
|
392
|
+
X_COVERAGE_BELOW_FLOOR: 'the unit suite covers less of the app than x.verify.json states',
|
|
393
|
+
X_COVERAGE_FLOOR_UNSTATED:
|
|
394
|
+
'x.verify.json states no coverage floor, or one under 95 with no reason',
|
|
395
|
+
X_COVERAGE_FLOOR_STALE: 'the app now covers more than the floor in x.verify.json states',
|
|
396
|
+
X_DB_HANDLE_UNREGISTERED: "an entity x g wrote is not in the app's typed database handle",
|
|
397
|
+
X_ROLE_LOAD_INCOMPLETE: 'a worker or scheduler did not register a job or task the manifest names',
|
|
398
|
+
X_ADMIN_RESOURCE_UNWIRED: "an admin override x g wrote is not in the app's defineAdmin() call",
|
|
399
|
+
X_IMAGE_NOT_PREBUILT:
|
|
400
|
+
'a container role built at boot what the image build should have: island chunks or stylesheets',
|
|
401
|
+
X_ADMIN_UNSCANNED:
|
|
402
|
+
'a defineAdmin() declaration sits outside the app scan, so the admin is never mounted',
|
|
403
|
+
X_FRAMEWORK_TABLE_ORPHANED:
|
|
404
|
+
'a framework table no release reads any more is still in the database',
|
|
405
|
+
X_BUILD_OUT_UNSAFE: 'the static export directory is not one a build may empty',
|
|
406
|
+
X_FRAMEWORK_SCHEMA_UNAPPLIED:
|
|
407
|
+
'a serving role booted over a database whose framework schema this build never applied',
|
|
408
|
+
X_APP_URL_PORT_MISMATCH: 'APP_URL names a loopback port x dev does not serve',
|
|
409
|
+
X_DEV_HOST_REFUSED: 'a /_x request named a host that is not this machine',
|
|
410
|
+
X_TRUSTED_PROXY_HOPS_INVALID:
|
|
411
|
+
'TRUSTED_PROXY_HOPS is not a whole number of proxies the boot accepts',
|
|
412
|
+
X_PERMISSION_BORROWED: 'an app rule requires a permission only an imported package declared',
|
|
413
|
+
X_ERROR_CODE_UNTHROWN_STALE:
|
|
414
|
+
'a code UNTHROWN_CODES lists as thrown by nothing is thrown again — delete it from UNTHROWN_CODES in packages/cli/src/unthrown-codes.ts',
|
|
363
415
|
};
|
|
364
416
|
|
|
365
417
|
// One unconditional call, so a second package claiming one of the CLI's codes throws
|
package/src/error-unthrown.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// A registered code nothing constructs. The registry and the reference keep a shipped code alive
|
|
2
2
|
// forever — which is right — and that is exactly why a code can outlive its last thrower with no
|
|
3
3
|
// gate noticing: `X_RPC_FAILED` sat registered and documented as live after the transport change
|
|
4
|
-
// took its only throw site away.
|
|
4
|
+
// took its only throw site away. It has to be LISTED as unthrown (`UNTHROWN_CODES`), or this
|
|
5
|
+
// reports it.
|
|
5
6
|
|
|
6
7
|
import { join } from 'node:path';
|
|
7
8
|
import { ERROR_DOCS_URL, maskLiterals, stripComments } from '@ultimat3/core';
|
|
@@ -9,6 +10,7 @@ import { RESERVED_HEADING } from './error-contract';
|
|
|
9
10
|
import type { Finding } from './output';
|
|
10
11
|
import { eachSourceFile, isGenerated, isTest } from './source-files';
|
|
11
12
|
import { isCodeRegistry } from './ts-scan';
|
|
13
|
+
import { UNTHROWN_CODES, UNTHROWN_CODES_FILE } from './unthrown-codes';
|
|
12
14
|
|
|
13
15
|
const CODE_LITERAL = /(['"`])(X_[A-Z0-9_]+)\1/g;
|
|
14
16
|
const TITLE_KEY = /^[\t ]*(X_[A-Z0-9_]+)\s*:/gm;
|
|
@@ -18,9 +20,6 @@ const LIST_LINE = /^[\t ]*(['"`])X_[A-Z0-9_]+\1\s*,?\s*$/;
|
|
|
18
20
|
const TABLE_ENTRY = /\b([a-z][A-Za-z0-9]*)\s*:\s*(['"`])(X_[A-Z0-9_]+)\2/g;
|
|
19
21
|
const MEMBER_READ = /\.([A-Za-z_$][\w$]*)/g;
|
|
20
22
|
|
|
21
|
-
/** Phrases a reference row uses to say, in words, that nothing throws the code any more. */
|
|
22
|
-
const DECLARED_UNTHROWN = /thrown by nothing|not thrown/i;
|
|
23
|
-
|
|
24
23
|
export interface CodeUse {
|
|
25
24
|
/** Every code a package registry names. */
|
|
26
25
|
readonly registered: ReadonlySet<string>;
|
|
@@ -78,6 +77,8 @@ export async function collectCodeUse(root: string): Promise<CodeUse> {
|
|
|
78
77
|
const members = new Set<string>();
|
|
79
78
|
for await (const source of eachSourceFile(root)) {
|
|
80
79
|
if (!/^packages\/[^/]+\/src\//.test(source) || isTest(source) || isGenerated(source)) continue;
|
|
80
|
+
// The list names every code it waives, and naming one is not throwing it.
|
|
81
|
+
if (source === UNTHROWN_CODES_FILE) continue;
|
|
81
82
|
const use = codeUseOf(await Bun.file(join(root, source)).text());
|
|
82
83
|
for (const code of use.registered) registered.add(code);
|
|
83
84
|
for (const code of use.used) used.add(code);
|
|
@@ -88,28 +89,36 @@ export async function collectCodeUse(root: string): Promise<CodeUse> {
|
|
|
88
89
|
return { registered, used };
|
|
89
90
|
}
|
|
90
91
|
|
|
91
|
-
/** Codes
|
|
92
|
+
/** Codes that sit under the reserved heading — declared unthrown by WHERE they are, not by words. */
|
|
92
93
|
export function declaredUnthrown(markdown: string): ReadonlySet<string> {
|
|
93
94
|
const out = new Set<string>();
|
|
94
95
|
let reserved = false;
|
|
95
96
|
for (const line of markdown.split('\n')) {
|
|
96
97
|
if (line.trim() === RESERVED_HEADING) reserved = true;
|
|
97
98
|
const row = /^\|\s*`(X_[A-Z0-9_]+)`\s*\|/.exec(line);
|
|
98
|
-
if (row !== null &&
|
|
99
|
+
if (row !== null && reserved) out.add(row[1] ?? '');
|
|
99
100
|
}
|
|
100
101
|
return out;
|
|
101
102
|
}
|
|
102
103
|
|
|
103
104
|
const unthrownFinding = (code: string, page: string): Finding => ({
|
|
104
105
|
code: 'X_ERROR_CODE_UNTHROWN',
|
|
105
|
-
cause: `${code} is registered and ${page} presents it as live, but no shipped source constructs it — a reader matching on it waits for an error that cannot arrive`,
|
|
106
|
+
cause: `${code} is registered and ${page} presents it as live, but no shipped source constructs it — a reader matching on it waits for an error that cannot arrive. Add it to UNTHROWN_CODES in ${UNTHROWN_CODES_FILE} and say so in its row, or throw it again where it belongs`,
|
|
106
107
|
// Never "delete the registration": a shipped code is stable forever, and an old log line must
|
|
107
108
|
// still explain. The row is what has to change.
|
|
108
|
-
fix:
|
|
109
|
+
fix: 'x errors explain X_ERROR_CODE_UNTHROWN --json',
|
|
109
110
|
docs: ERROR_DOCS_URL,
|
|
110
111
|
at: page,
|
|
111
112
|
});
|
|
112
113
|
|
|
114
|
+
const staleFinding = (code: string): Finding => ({
|
|
115
|
+
code: 'X_ERROR_CODE_UNTHROWN_STALE',
|
|
116
|
+
cause: `${code} is listed in UNTHROWN_CODES and shipped source constructs it again — the listing would excuse a code that is live, so delete it from UNTHROWN_CODES in ${UNTHROWN_CODES_FILE}`,
|
|
117
|
+
fix: 'x errors explain X_ERROR_CODE_UNTHROWN_STALE --json',
|
|
118
|
+
docs: ERROR_DOCS_URL,
|
|
119
|
+
at: UNTHROWN_CODES_FILE,
|
|
120
|
+
});
|
|
121
|
+
|
|
113
122
|
/**
|
|
114
123
|
* Registered, used by nothing, and not declared unthrown on the reference. A host check — the page
|
|
115
124
|
* is the host repo's to name, and only a monorepo's walk sees every package's source; in a
|
|
@@ -118,13 +127,22 @@ const unthrownFinding = (code: string, page: string): Finding => ({
|
|
|
118
127
|
export async function checkErrorCodesThrown(
|
|
119
128
|
root: string,
|
|
120
129
|
page: string,
|
|
130
|
+
listed: ReadonlySet<string> = UNTHROWN_CODES,
|
|
121
131
|
): Promise<readonly Finding[]> {
|
|
122
132
|
const reference = Bun.file(join(root, page));
|
|
123
133
|
if (!(await reference.exists())) return [];
|
|
124
|
-
const
|
|
134
|
+
const reserved = declaredUnthrown(await reference.text());
|
|
125
135
|
const { registered, used } = await collectCodeUse(root);
|
|
126
|
-
|
|
127
|
-
.filter((code) => !used.has(code) && !
|
|
136
|
+
const unthrown = [...registered]
|
|
137
|
+
.filter((code) => !used.has(code) && !listed.has(code) && !reserved.has(code))
|
|
128
138
|
.sort()
|
|
129
139
|
.map((code) => unthrownFinding(code, page));
|
|
140
|
+
// Only the THROWN-again direction: a listed name no registry holds waives nothing (the code it
|
|
141
|
+
// was meant to name is still reported), and a host repo's fixture registering none of this
|
|
142
|
+
// repo's codes would otherwise read every listing as stale.
|
|
143
|
+
const stale = [...listed]
|
|
144
|
+
.filter((code) => used.has(code))
|
|
145
|
+
.sort()
|
|
146
|
+
.map((code) => staleFinding(code));
|
|
147
|
+
return [...unthrown, ...stale];
|
|
130
148
|
}
|
package/src/errors.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// that resolves it — the codes themselves, their titles and their registration are `./error-codes`,
|
|
3
3
|
// so a package importing a class does not pull the table and vice versa.
|
|
4
4
|
import { UltimateError } from '@ultimat3/core';
|
|
5
|
+
import type { Finding } from './output';
|
|
5
6
|
import { quoteArg } from './shell-quote';
|
|
6
7
|
|
|
7
8
|
/** An unknown command or subcommand. Carries a suggestion so the retry is one keystroke away. */
|
|
@@ -301,14 +302,59 @@ export class FixTargetUnknownError extends UltimateError {
|
|
|
301
302
|
}
|
|
302
303
|
}
|
|
303
304
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
305
|
+
/**
|
|
306
|
+
* A client entry would not compile. `X_BUILD_FAILED`, not a code of its own: an island is a bundle
|
|
307
|
+
* entry point like any other, and the target's own logs are what says which line. The fix builds
|
|
308
|
+
* exactly that one file, so the next message an author reads is the compiler's and not the CLI's.
|
|
309
|
+
*/
|
|
310
|
+
export class IslandBuildFailedError extends UltimateError {
|
|
311
|
+
constructor(input: { file: string; logs: string }) {
|
|
312
|
+
super({
|
|
313
|
+
code: 'X_BUILD_FAILED',
|
|
314
|
+
cause: `${input.file} is an island entry point and would not bundle: ${input.logs}`,
|
|
315
|
+
fix: `bun build --target browser ${input.file}`,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** Which framework script a page ships: the sync worker, the page boot, or the client router. */
|
|
321
|
+
export type FrameworkScriptKind = 'sync worker' | 'page boot' | 'client router';
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* One of the page's framework scripts would not bundle. Same code as an island: "a browser entry
|
|
325
|
+
* the framework builds did not build" is one condition, and the entry here is framework code, not
|
|
326
|
+
* the app's — the cause names WHICH script, so the reader knows what the page is now missing.
|
|
327
|
+
*/
|
|
328
|
+
export class FrameworkScriptBuildFailedError extends UltimateError {
|
|
329
|
+
constructor(input: { what: FrameworkScriptKind; entry: string; logs: string }) {
|
|
330
|
+
super({
|
|
331
|
+
code: 'X_BUILD_FAILED',
|
|
332
|
+
cause: `the ${input.what} (${input.entry}) would not bundle for the browser: ${input.logs}`,
|
|
333
|
+
fix: `bun build --target browser --format iife ${quoteArg(input.entry)}`,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* A static build over an app whose modules did not all import. The route registry is filled BY the
|
|
340
|
+
* import, so a page whose module threw is not skipped but absent — and an export without it, with
|
|
341
|
+
* `x build` green, is a deploy that deletes a page. The `manifest` step owns load findings
|
|
342
|
+
* (`load-findings.ts`), so the fix is the command that lists every one with its own fix.
|
|
343
|
+
*/
|
|
344
|
+
export class PrerenderLoadFailedError extends UltimateError {
|
|
345
|
+
constructor(findings: readonly Finding[]) {
|
|
346
|
+
const listed = findings.map((finding) =>
|
|
347
|
+
finding.at === undefined
|
|
348
|
+
? `${finding.code} ${finding.cause}`
|
|
349
|
+
: `${finding.at}: ${finding.code} ${finding.cause}`,
|
|
350
|
+
);
|
|
351
|
+
super({
|
|
352
|
+
code: 'X_BUILD_FAILED',
|
|
353
|
+
cause: `the static export renders the routes the app's modules register, and ${findings.length} module load finding(s) left it short: ${listed.join('; ')}`,
|
|
354
|
+
fix: 'x verify --only manifest --json',
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
}
|
|
312
358
|
|
|
313
359
|
/**
|
|
314
360
|
* `ROLE` selects what a container is. One image runs every role, so a typo is a process that would
|
|
@@ -363,36 +409,6 @@ export class PortInvalidError extends UltimateError {
|
|
|
363
409
|
}
|
|
364
410
|
}
|
|
365
411
|
|
|
366
|
-
/**
|
|
367
|
-
* `x env` was run in an app whose `app.config.ts` exports no `envSchema`. Not a silent success:
|
|
368
|
-
* writing a `.env.example` with no variables in it, or reporting "0 declared variables, all
|
|
369
|
-
* present", both read as a working environment declaration to whoever runs the command next.
|
|
370
|
-
*
|
|
371
|
-
* `X_CONFIG_INVALID` is core's code for "a configuration this process cannot boot on — env or
|
|
372
|
-
* `app.config.ts`", which is exactly this; the CLI names it in `CLI_BORROWED_ERROR_CODES` rather
|
|
373
|
-
* than minting a synonym.
|
|
374
|
-
*/
|
|
375
|
-
export class EnvSchemaMissingError extends UltimateError {
|
|
376
|
-
constructor(input: { subcommand: string }) {
|
|
377
|
-
super({
|
|
378
|
-
code: 'X_CONFIG_INVALID',
|
|
379
|
-
cause: `x env ${input.subcommand} needs the env declaration, and app.config.ts exports no "envSchema"`,
|
|
380
|
-
fix: "add to app.config.ts: export const envSchema = { DATABASE_URL: { type: 'url', description: 'Postgres connection URL' } } satisfies EnvSchema; export const env = defineEnv(envSchema);",
|
|
381
|
-
});
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
/** An interface-complete command path whose remote/native half is not written yet. */
|
|
386
|
-
export class CliNotImplementedError extends UltimateError {
|
|
387
|
-
constructor(input: { feature: string; fix: string }) {
|
|
388
|
-
super({
|
|
389
|
-
code: 'X_NOT_IMPLEMENTED',
|
|
390
|
-
cause: `${input.feature} is not implemented in this build`,
|
|
391
|
-
fix: input.fix,
|
|
392
|
-
});
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
|
|
396
412
|
/**
|
|
397
413
|
* The process could not obtain a storage disk to write to.
|
|
398
414
|
*
|
|
@@ -432,48 +448,3 @@ export class LocalDiskUnsafeError extends UltimateError {
|
|
|
432
448
|
});
|
|
433
449
|
}
|
|
434
450
|
}
|
|
435
|
-
|
|
436
|
-
/**
|
|
437
|
-
* `x secrets edit` decrypts into a buffer and hands it to `$EDITOR`. There is no fallback editor:
|
|
438
|
-
* guessing one and opening a decrypted file in it is the last place a surprise belongs.
|
|
439
|
-
*/
|
|
440
|
-
export class SecretsEditorMissingError extends UltimateError {
|
|
441
|
-
constructor(input: { vars: readonly string[] }) {
|
|
442
|
-
super({
|
|
443
|
-
code: 'X_SECRETS_EDITOR_MISSING',
|
|
444
|
-
cause: `x secrets edit opens the decrypted secrets in an editor and none of ${input.vars.join(', ')} is set`,
|
|
445
|
-
fix: 'EDITOR=nano x secrets edit',
|
|
446
|
-
});
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
/**
|
|
451
|
-
* The editor exited non-zero — a crash, or a deliberate abort. The buffer is discarded either way
|
|
452
|
-
* and the committed file is left exactly as it was: resealing a buffer whose editor failed would
|
|
453
|
-
* commit whatever half-written state the crash left behind.
|
|
454
|
-
*/
|
|
455
|
-
export class SecretsEditFailedError extends UltimateError {
|
|
456
|
-
constructor(input: { editor: string; code: number }) {
|
|
457
|
-
super({
|
|
458
|
-
code: 'X_SECRETS_EDIT_FAILED',
|
|
459
|
-
cause: `"${input.editor}" exited ${input.code}, so the decrypted buffer was discarded and the committed secrets file was not rewritten`,
|
|
460
|
-
fix: 'x secrets edit',
|
|
461
|
-
});
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
/**
|
|
466
|
-
* `x secrets init` would overwrite a file that already exists. `X_GENERATE_CONFLICT` is this
|
|
467
|
-
* package's own code for exactly that, and a second name for "a generator would clobber something"
|
|
468
|
-
* is the duplication the code registry exists to prevent. Losing a master key is unrecoverable —
|
|
469
|
-
* the committed file it opens is then ciphertext nobody can read again.
|
|
470
|
-
*/
|
|
471
|
-
export class SecretsExistsError extends UltimateError {
|
|
472
|
-
constructor(input: { path: string; fix: string }) {
|
|
473
|
-
super({
|
|
474
|
-
code: 'X_GENERATE_CONFLICT',
|
|
475
|
-
cause: `${input.path} already exists, and x secrets init would replace it`,
|
|
476
|
-
fix: input.fix,
|
|
477
|
-
});
|
|
478
|
-
}
|
|
479
|
-
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// The two ways a boot meets the framework schema (plan 101, s1-con #5): `applyLockedSchema` runs the
|
|
2
|
+
// DDL in ONE transaction behind the migration advisory lock with a bounded `lock_timeout`, then
|
|
3
|
+
// stamps the build; `verifySchema` runs no DDL and refuses a database this build was never applied
|
|
4
|
+
// to. Which boot takes which is `runtime-queue.ts`'s `SchemaMode`.
|
|
5
|
+
|
|
6
|
+
import { frameworkVersion, logger } from '@ultimat3/core';
|
|
7
|
+
import type { DbClient } from '@ultimat3/db';
|
|
8
|
+
import {
|
|
9
|
+
literal,
|
|
10
|
+
MIGRATION_LOCK_KEY,
|
|
11
|
+
MIGRATION_LOCK_WAIT_MS,
|
|
12
|
+
poolProfileFor,
|
|
13
|
+
raw,
|
|
14
|
+
withTransaction,
|
|
15
|
+
} from '@ultimat3/db';
|
|
16
|
+
import { applyFrameworkSchema } from './framework-schema';
|
|
17
|
+
import {
|
|
18
|
+
FrameworkSchemaUnappliedError,
|
|
19
|
+
frameworkSchemaHash,
|
|
20
|
+
nextStamp,
|
|
21
|
+
parseStamp,
|
|
22
|
+
STAMP_RELATION,
|
|
23
|
+
type StampEntry,
|
|
24
|
+
stampVerdict,
|
|
25
|
+
} from './framework-schema-stamp';
|
|
26
|
+
|
|
27
|
+
const thisBuild = (): StampEntry => ({
|
|
28
|
+
version: frameworkVersion(),
|
|
29
|
+
schema: frameworkSchemaHash(),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
/** The stamp as the catalog holds it; `null` when the relation or its comment is absent. */
|
|
33
|
+
async function readStamp(client: DbClient): Promise<string | null> {
|
|
34
|
+
const rows = await client.query<{ readonly stamp: string | null }>(
|
|
35
|
+
raw(`select obj_description(to_regclass('${STAMP_RELATION}'), 'pg_class') as stamp`),
|
|
36
|
+
);
|
|
37
|
+
return rows[0]?.stamp ?? null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The migration lock, not a lock of its own: a second migrator — another `ROLE=migrate` pod, an
|
|
42
|
+
* `x db migrate` — waits behind this one instead of racing its `create table`s into `23505`. A
|
|
43
|
+
* transaction-scoped lock (`pg_advisory_xact_lock`), so no pinned session: COMMIT releases it.
|
|
44
|
+
*
|
|
45
|
+
* Two `lock_timeout`s: the lock wait is the migrator's own (`MIGRATION_LOCK_WAIT_MS`), and every
|
|
46
|
+
* DDL statement after it is the migrate pool's (`poolProfileFor('migrate')`), so an `alter table`
|
|
47
|
+
* behind an open transaction fails in seconds rather than queueing every enqueue behind it.
|
|
48
|
+
*/
|
|
49
|
+
export async function applyLockedSchema(client: DbClient): Promise<void> {
|
|
50
|
+
await withTransaction(
|
|
51
|
+
async (tx) => {
|
|
52
|
+
await tx.execute(raw(`set local lock_timeout = ${Math.round(MIGRATION_LOCK_WAIT_MS)}`));
|
|
53
|
+
await tx.execute(raw(`select pg_advisory_xact_lock(${MIGRATION_LOCK_KEY})`));
|
|
54
|
+
const ddlTimeoutMs = Math.round(poolProfileFor('migrate').lockTimeoutMs);
|
|
55
|
+
await tx.execute(raw(`set local lock_timeout = ${ddlTimeoutMs}`));
|
|
56
|
+
await applyFrameworkSchema((statement) => tx.execute(raw(statement)));
|
|
57
|
+
const stamp = nextStamp(await readStamp(tx), thisBuild());
|
|
58
|
+
await tx.execute(raw(`comment on table ${STAMP_RELATION} is ${literal(stamp).text}`));
|
|
59
|
+
},
|
|
60
|
+
{ client },
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* One catalog read. A build absent from the stamp is refused with the command that applies it; a
|
|
66
|
+
* fleet straddling a major is reported once and served — refusing it would crash-loop the old pods
|
|
67
|
+
* of every rolling deploy across a breaking release.
|
|
68
|
+
*/
|
|
69
|
+
export async function verifySchema(client: DbClient): Promise<void> {
|
|
70
|
+
const mine = thisBuild();
|
|
71
|
+
const verdict = stampVerdict(parseStamp(await readStamp(client)), mine);
|
|
72
|
+
if (!verdict.applied) throw new FrameworkSchemaUnappliedError({ mine, newest: verdict.newest });
|
|
73
|
+
if (verdict.skew === undefined) return;
|
|
74
|
+
logger.warn('ultimate framework major skew', {
|
|
75
|
+
cause: `this process runs ${mine.version} and the database's framework schema was last applied by ${verdict.skew.version}: a fleet across a major boundary is mid-rollout or half rolled back`,
|
|
76
|
+
fix: 'kubectl rollout status deployment --selector app.kubernetes.io/component=web',
|
|
77
|
+
});
|
|
78
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Which builds have applied the framework schema to this database: a stamp `ROLE=migrate` leaves
|
|
2
|
+
// in `x_jobs`' table comment, and the verdict a serving role reads from it instead of running DDL
|
|
3
|
+
// itself (plan 101, s1-con #5). Pure: the statements live in `framework-schema-apply.ts`.
|
|
4
|
+
|
|
5
|
+
import { UltimateError } from '@ultimat3/core';
|
|
6
|
+
import { FRAMEWORK_SCHEMA } from './framework-schema';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The relation that carries the stamp. A comment, not a table: a new framework table moves every
|
|
10
|
+
* app's committed schema dump (`packages/db/schema/framework/`), and a comment is in none of them.
|
|
11
|
+
*/
|
|
12
|
+
export const STAMP_RELATION = 'x_jobs';
|
|
13
|
+
|
|
14
|
+
/** The first line of a stamp — what tells it apart from a comment someone else wrote. */
|
|
15
|
+
const STAMP_HEADER = 'ultimate-framework-schema';
|
|
16
|
+
|
|
17
|
+
/** Enough for a rolling deploy and its rollback, small enough to stay one readable comment. */
|
|
18
|
+
const STAMP_KEEP = 16;
|
|
19
|
+
|
|
20
|
+
export interface StampEntry {
|
|
21
|
+
readonly version: string;
|
|
22
|
+
readonly schema: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** This build's DDL, as one short hash: two builds with the same DDL are the same schema. */
|
|
26
|
+
export function frameworkSchemaHash(): string {
|
|
27
|
+
const hasher = new Bun.CryptoHasher('sha256');
|
|
28
|
+
for (const entry of FRAMEWORK_SCHEMA) for (const ddl of entry.ddl) hasher.update(ddl);
|
|
29
|
+
return hasher.digest('hex').slice(0, 16);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Every build a stamp records, newest first. Anything that is not a stamp records none. */
|
|
33
|
+
export function parseStamp(comment: string | null | undefined): readonly StampEntry[] {
|
|
34
|
+
if (typeof comment !== 'string') return [];
|
|
35
|
+
const [header, ...lines] = comment.split('\n');
|
|
36
|
+
if (header !== STAMP_HEADER) return [];
|
|
37
|
+
return lines.flatMap((line) => {
|
|
38
|
+
const [version, schema] = line.trim().split(' ');
|
|
39
|
+
return version !== undefined &&
|
|
40
|
+
/^[0-9A-Za-z.+-]{1,64}$/.test(version) &&
|
|
41
|
+
schema !== undefined &&
|
|
42
|
+
/^[0-9a-f]{16}$/.test(schema)
|
|
43
|
+
? [{ version, schema }]
|
|
44
|
+
: [];
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The stamp after `mine` applied: it first, every other build kept, the oldest dropped. */
|
|
49
|
+
export function nextStamp(previous: string | null | undefined, mine: StampEntry): string {
|
|
50
|
+
const kept = parseStamp(previous).filter((entry) => entry.schema !== mine.schema);
|
|
51
|
+
const entries = [mine, ...kept].slice(0, STAMP_KEEP);
|
|
52
|
+
return [STAMP_HEADER, ...entries.map((entry) => `${entry.version} ${entry.schema}`)].join('\n');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const majorOf = (version: string): string => version.split('.')[0] ?? version;
|
|
56
|
+
|
|
57
|
+
export type StampVerdict =
|
|
58
|
+
| { readonly applied: true; readonly skew?: StampEntry }
|
|
59
|
+
| { readonly applied: false; readonly newest?: StampEntry };
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Applied when this build's hash is anywhere in the stamp: an old pod restarting mid-rollout finds
|
|
63
|
+
* its own build there, behind the new one. `skew` is the newest build when its MAJOR differs from
|
|
64
|
+
* this process's — a fleet straddling a breaking release, which a boot reports and never refuses.
|
|
65
|
+
*/
|
|
66
|
+
export function stampVerdict(entries: readonly StampEntry[], mine: StampEntry): StampVerdict {
|
|
67
|
+
const newest = entries[0];
|
|
68
|
+
if (!entries.some((entry) => entry.schema === mine.schema)) {
|
|
69
|
+
return newest === undefined ? { applied: false } : { applied: false, newest };
|
|
70
|
+
}
|
|
71
|
+
return newest !== undefined && majorOf(newest.version) !== majorOf(mine.version)
|
|
72
|
+
? { applied: true, skew: newest }
|
|
73
|
+
: { applied: true };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** A serving role over a database its build's framework tables were never applied to. */
|
|
77
|
+
export class FrameworkSchemaUnappliedError extends UltimateError {
|
|
78
|
+
constructor(input: { readonly mine: StampEntry; readonly newest: StampEntry | undefined }) {
|
|
79
|
+
super({
|
|
80
|
+
code: 'X_FRAMEWORK_SCHEMA_UNAPPLIED',
|
|
81
|
+
cause:
|
|
82
|
+
`this build (${input.mine.version}, framework schema ${input.mine.schema}) has not been applied ` +
|
|
83
|
+
`to this database — ${
|
|
84
|
+
input.newest === undefined
|
|
85
|
+
? `${STAMP_RELATION} carries no stamp, so no ROLE=migrate of this release ran`
|
|
86
|
+
: `the newest applied build is ${input.newest.version} (${input.newest.schema})`
|
|
87
|
+
}. A serving role verifies and never runs DDL: the migrate step must run first`,
|
|
88
|
+
fix: 'x db migrate',
|
|
89
|
+
meta: {
|
|
90
|
+
version: input.mine.version,
|
|
91
|
+
schema: input.mine.schema,
|
|
92
|
+
...(input.newest === undefined ? {} : { newest: { ...input.newest } }),
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
package/src/framework-schema.ts
CHANGED
|
@@ -7,10 +7,15 @@
|
|
|
7
7
|
// the one code path that needs it, and it surfaces as a Postgres `42P01` from inside a worker.
|
|
8
8
|
|
|
9
9
|
import { SQL_AUDIT_TABLE, SQL_IDEMPOTENCY_TABLE } from '@ultimat3/action';
|
|
10
|
+
import { ADMIN_AUDIT_TABLE, SQL_ADMIN_AUDIT_TABLE } from '@ultimat3/admin/schema';
|
|
10
11
|
import { AUTH_TABLE_NAMES, AUTH_TABLES, SQL_AUTH_LIMIT_TABLES } from '@ultimat3/auth';
|
|
11
12
|
import { SQL_RATE_LIMIT_TABLE } from '@ultimat3/http';
|
|
12
13
|
import { SQL_JOBS_TABLE } from '@ultimat3/jobs';
|
|
13
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
SQL_NOTIFY_DELIVERIES_TABLE,
|
|
16
|
+
SQL_NOTIFY_DIGESTS_TABLE,
|
|
17
|
+
SQL_NOTIFY_INBOX_TABLE,
|
|
18
|
+
} from '@ultimat3/notify';
|
|
14
19
|
import { FrameworkSchemaFailedError } from './schema-errors';
|
|
15
20
|
|
|
16
21
|
export interface FrameworkSchema {
|
|
@@ -49,6 +54,9 @@ export const FRAMEWORK_SCHEMA: readonly FrameworkSchema[] = Object.freeze([
|
|
|
49
54
|
'x_scheduler_leader',
|
|
50
55
|
'x_job_leases',
|
|
51
56
|
'x_job_events',
|
|
57
|
+
'x_job_pauses',
|
|
58
|
+
'x_job_workers',
|
|
59
|
+
'x_job_counters',
|
|
52
60
|
]),
|
|
53
61
|
ddl: Object.freeze([SQL_JOBS_TABLE]),
|
|
54
62
|
}),
|
|
@@ -72,7 +80,7 @@ export const FRAMEWORK_SCHEMA: readonly FrameworkSchema[] = Object.freeze([
|
|
|
72
80
|
}),
|
|
73
81
|
Object.freeze({
|
|
74
82
|
pkg: '@ultimat3/auth',
|
|
75
|
-
tables: Object.freeze(['
|
|
83
|
+
tables: Object.freeze(['x_auth_lockouts']),
|
|
76
84
|
ddl: Object.freeze([SQL_AUTH_LIMIT_TABLES]),
|
|
77
85
|
}),
|
|
78
86
|
/**
|
|
@@ -109,6 +117,24 @@ export const FRAMEWORK_SCHEMA: readonly FrameworkSchema[] = Object.freeze([
|
|
|
109
117
|
tables: Object.freeze(['x_notify_inbox']),
|
|
110
118
|
ddl: Object.freeze([SQL_NOTIFY_INBOX_TABLE]),
|
|
111
119
|
}),
|
|
120
|
+
// `createPgDigestStore`'s windows. Installed whether or not the app picks that store, for the
|
|
121
|
+
// reason every notify row is: `setNotifyStores` is the app's boot line and runs after this one.
|
|
122
|
+
Object.freeze({
|
|
123
|
+
pkg: '@ultimat3/notify',
|
|
124
|
+
tables: Object.freeze(['x_notify_digests']),
|
|
125
|
+
ddl: Object.freeze([SQL_NOTIFY_DIGESTS_TABLE]),
|
|
126
|
+
}),
|
|
127
|
+
/**
|
|
128
|
+
* The admin's durable audit trail — what `postgresAuditLog()` writes and every detail page's
|
|
129
|
+
* history card reads. Without this row an app that chose the durable log met `42P01` on the
|
|
130
|
+
* first audited request. Imported through `@ultimat3/admin/schema`, a leaf module: the DDL is
|
|
131
|
+
* on every role's boot path, and not one admin screen is (`serve-graph.test.ts`).
|
|
132
|
+
*/
|
|
133
|
+
Object.freeze({
|
|
134
|
+
pkg: '@ultimat3/admin',
|
|
135
|
+
tables: Object.freeze([ADMIN_AUDIT_TABLE]),
|
|
136
|
+
ddl: Object.freeze([SQL_ADMIN_AUDIT_TABLE]),
|
|
137
|
+
}),
|
|
112
138
|
]);
|
|
113
139
|
|
|
114
140
|
/** Every relation this boot creates, flattened. */
|