@ultimat3/cli 22.15.0 → 23.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/CLAUDE.md +23 -8
  2. package/README.md +28 -5
  3. package/package.json +29 -28
  4. package/src/admin-catalog-keys.ts +16 -0
  5. package/src/admin-registration.ts +192 -0
  6. package/src/api-registration.ts +36 -11
  7. package/src/app-boundaries.ts +6 -20
  8. package/src/app-load.ts +55 -11
  9. package/src/app-manifest.ts +14 -0
  10. package/src/app-permissions.ts +0 -0
  11. package/src/app-reload-graph.ts +7 -30
  12. package/src/app-transport.ts +219 -0
  13. package/src/boundary-findings.ts +16 -4
  14. package/src/browser-transport.ts +160 -0
  15. package/src/budgets.ts +25 -1
  16. package/src/cmd-build-spec.ts +10 -3
  17. package/src/cmd-build.ts +56 -9
  18. package/src/cmd-db.ts +60 -11
  19. package/src/cmd-doctor.ts +35 -11
  20. package/src/cmd-generate.ts +118 -17
  21. package/src/cmd-jobs-spec.ts +19 -3
  22. package/src/cmd-jobs.ts +120 -3
  23. package/src/cmd-new.ts +15 -1
  24. package/src/cmd-routes.ts +15 -1
  25. package/src/cmd-secrets-ring.ts +76 -0
  26. package/src/cmd-secrets-spec.ts +10 -2
  27. package/src/cmd-secrets.ts +48 -12
  28. package/src/cmd-tasks.ts +84 -15
  29. package/src/cmd-verify.ts +10 -2
  30. package/src/comment-safe.ts +16 -0
  31. package/src/coverage-floor.ts +334 -0
  32. package/src/coverage-lcov.ts +193 -0
  33. package/src/coverage-source.ts +136 -0
  34. package/src/db-object-drift.ts +30 -0
  35. package/src/db-replay-engine.ts +115 -0
  36. package/src/db-schema-dump.ts +150 -0
  37. package/src/db-schema-refresh.ts +122 -0
  38. package/src/dev-dashboard.ts +2 -2
  39. package/src/dev-route-table.ts +9 -0
  40. package/src/doctor-coverage.ts +39 -0
  41. package/src/doctor-guards.ts +54 -0
  42. package/src/doctor-sealed.ts +132 -0
  43. package/src/document-graph.ts +82 -0
  44. package/src/error-codes.ts +32 -0
  45. package/src/framework-schema.ts +15 -0
  46. package/src/generate-files.ts +76 -10
  47. package/src/generate-grant-findings.ts +76 -0
  48. package/src/generate-grants.ts +77 -3
  49. package/src/generate-kinds.ts +19 -0
  50. package/src/generate-write.ts +15 -27
  51. package/src/generated-imports.ts +59 -0
  52. package/src/guard-sources.ts +104 -0
  53. package/src/guards.ts +23 -7
  54. package/src/handle-registration.ts +217 -0
  55. package/src/i18n-registration.ts +36 -0
  56. package/src/image-prepare.ts +51 -8
  57. package/src/import-closure.ts +165 -0
  58. package/src/index.ts +11 -2
  59. package/src/island-store.ts +24 -16
  60. package/src/jobs-drain.ts +14 -7
  61. package/src/jobs-json.ts +50 -0
  62. package/src/jobs-report.ts +3 -0
  63. package/src/json-merge.ts +46 -0
  64. package/src/mcp-errors.ts +29 -1
  65. package/src/messages.ts +33 -0
  66. package/src/metrics-endpoint.ts +71 -24
  67. package/src/migration-extensions.ts +29 -0
  68. package/src/module-imports.ts +47 -0
  69. package/src/output.ts +18 -9
  70. package/src/permission-grants.ts +41 -9
  71. package/src/publish-closure.ts +101 -0
  72. package/src/role-load.ts +143 -0
  73. package/src/role-start.ts +18 -1
  74. package/src/role-wake.ts +45 -0
  75. package/src/runtime-admin.ts +141 -0
  76. package/src/runtime-hooks.ts +5 -2
  77. package/src/runtime-queue.ts +8 -1
  78. package/src/runtime-render.ts +6 -0
  79. package/src/runtime-services.ts +2 -1
  80. package/src/scaffold-typecheck.ts +1 -0
  81. package/src/schema-dump-drift.ts +64 -0
  82. package/src/serve-boot.ts +85 -157
  83. package/src/serve-prebuilt-paths.ts +19 -0
  84. package/src/serve-prebuilt.ts +84 -0
  85. package/src/serve-web.ts +164 -0
  86. package/src/serve.ts +6 -9
  87. package/src/server-barrels.ts +54 -0
  88. package/src/source-edit.ts +49 -0
  89. package/src/tasks-facts.ts +4 -1
  90. package/src/templates/action.ts +144 -11
  91. package/src/templates/admin-catalog.ts +58 -0
  92. package/src/templates/admin-page.ts +36 -8
  93. package/src/templates/admin.ts +57 -17
  94. package/src/templates/backfill.ts +121 -87
  95. package/src/templates/entity.ts +140 -44
  96. package/src/templates/guard-animated-layout-property.ts +4 -11
  97. package/src/templates/guard-bare-error.ts +9 -13
  98. package/src/templates/guard-focus-visible.ts +4 -11
  99. package/src/templates/guard-image-dimensions.ts +6 -6
  100. package/src/templates/guard-island-without-states.ts +2 -4
  101. package/src/templates/guard-raw-breakpoint.ts +210 -0
  102. package/src/templates/guard-raw-colour.ts +4 -11
  103. package/src/templates/guard-raw-length.ts +375 -0
  104. package/src/templates/guard-raw-motion.ts +213 -0
  105. package/src/templates/guard-raw-shadow.ts +187 -0
  106. package/src/templates/guard-raw-z-index.ts +166 -0
  107. package/src/templates/guard-repo-raw-sql.ts +345 -0
  108. package/src/templates/guard-semantic-interactive.ts +6 -6
  109. package/src/templates/guard-undeclared-custom-property.ts +217 -0
  110. package/src/templates/guard-undefined-style-class.ts +215 -0
  111. package/src/templates/guard-untranslated-string.ts +6 -6
  112. package/src/templates/guard-unzoned-date.ts +9 -13
  113. package/src/templates/index.ts +3 -2
  114. package/src/templates/island.ts +34 -51
  115. package/src/templates/job-body-test.ts +121 -0
  116. package/src/templates/job.ts +23 -8
  117. package/src/templates/policy.ts +16 -9
  118. package/src/templates/query.ts +242 -26
  119. package/src/templates/resource-create.ts +62 -18
  120. package/src/templates/resource-form-island.ts +58 -80
  121. package/src/templates/resource-page.ts +269 -0
  122. package/src/templates/resource-service.ts +96 -0
  123. package/src/templates/resource.ts +86 -61
  124. package/src/templates/route.ts +38 -29
  125. package/src/templates/scaffold-api.ts +55 -1
  126. package/src/templates/scaffold-app.ts +171 -51
  127. package/src/templates/scaffold-auth.ts +83 -19
  128. package/src/templates/scaffold-browser-client.ts +90 -0
  129. package/src/templates/scaffold-container.ts +16 -0
  130. package/src/templates/scaffold-dashboard-bare.ts +64 -2
  131. package/src/templates/scaffold-dashboard-example.ts +124 -31
  132. package/src/templates/scaffold-dashboard-shared.ts +1 -20
  133. package/src/templates/scaffold-dashboard.ts +1 -2
  134. package/src/templates/scaffold-db-client.ts +142 -0
  135. package/src/templates/scaffold-db-package.ts +90 -8
  136. package/src/templates/scaffold-docs.ts +50 -5
  137. package/src/templates/scaffold-guards.ts +54 -11
  138. package/src/templates/scaffold-http.ts +9 -7
  139. package/src/templates/scaffold-repo.ts +52 -2
  140. package/src/templates/scaffold-roles.ts +56 -9
  141. package/src/templates/scaffold-shell.ts +36 -70
  142. package/src/templates/scaffold-site.ts +45 -19
  143. package/src/templates/scaffold-ui-package.ts +18 -0
  144. package/src/templates/slice-foundation.ts +46 -8
  145. package/src/test-counts.ts +18 -2
  146. package/src/transport-calls.ts +105 -0
  147. package/src/unscanned-admin.ts +54 -0
  148. package/src/verify-checks.ts +26 -7
  149. package/src/verify-coverage-run.ts +190 -0
  150. package/src/verify-deadline.ts +256 -0
  151. package/src/verify-errors.ts +15 -6
  152. package/src/verify-floor.ts +36 -9
  153. package/src/verify-merge.ts +117 -24
  154. package/src/verify-progress.ts +25 -0
  155. package/src/verify-role-load-probe.ts +54 -0
  156. package/src/verify-role-load.ts +148 -0
  157. package/src/verify-run.ts +93 -35
  158. package/src/verify-shard.ts +20 -9
  159. package/src/verify-step.ts +42 -1
  160. package/src/verify-tests.ts +78 -6
  161. package/src/workspace-checks.ts +50 -8
  162. package/src/workspace-dep-edit.ts +127 -0
  163. package/src/cmd-dev-fixture.ts +0 -204
  164. package/src/dev-live-fixture.ts +0 -122
  165. package/src/policy-fixture.ts +0 -78
  166. package/src/realtime-browser-probe-fixture.ts +0 -9
  167. package/src/role-start-fixture.ts +0 -78
  168. package/src/scaffold-fixture.ts +0 -160
  169. package/src/templates/island-fixture.ts +0 -76
@@ -0,0 +1,122 @@
1
+ // The schema dump, regenerated on behalf of a command: `x db gen` and `x db migrate` both end by
2
+ // making `packages/db/schema/` equal to what the migrations produce. Split from
3
+ // `db-schema-dump.ts`, which replays and writes; this decides what a command REPORTS when the dump
4
+ // was written, was already right, was not owed, or could not be produced — and holds the two
5
+ // findings that are about the dump as a whole rather than about one file in it.
6
+
7
+ import { ERROR_DOCS_URL, renderThrowable } from '@ultimat3/core';
8
+ import type { Migration } from '@ultimat3/db';
9
+ import { PGLITE_MISSING } from '@ultimat3/db';
10
+ import type { CatalogDescription } from '@ultimat3/db/schema-dump';
11
+ import {
12
+ hasSchemaDump,
13
+ type ReplayedSchema,
14
+ replayFor,
15
+ SCHEMA_DUMP_DIR,
16
+ type SchemaDumpWrite,
17
+ writeSchemaDump,
18
+ } from './db-schema-dump';
19
+ import { MIGRATIONS_DIR, readMigrations } from './migrations';
20
+ import type { Finding, JsonValue } from './output';
21
+ import { findingFrom, isUltimateErrorShape } from './output';
22
+
23
+ /**
24
+ * The migrations would not replay, so there is no dump to write and nothing to compare one with.
25
+ * A framework error keeps its own cause; the fix is replaced, because the engine's own (`set
26
+ * DATABASE_URL …`) answers a question nobody asked — this database is a scratch one booted here.
27
+ *
28
+ * Two causes are not about the migrations at all, and each keeps the fix that repairs it: a
29
+ * refusal this pipeline raised itself (`X_SCHEMA_DUMP_DRIFT` — no server for an extension the
30
+ * embedded database cannot link) arrives whole, and a missing optional peer gets the install,
31
+ * because `x db migrate` would succeed and teach nothing.
32
+ */
33
+ export function replayFailure(error: unknown): Finding {
34
+ if (isUltimateErrorShape(error) && error.code === 'X_SCHEMA_DUMP_DRIFT') {
35
+ return { ...findingFrom(error), docs: ERROR_DOCS_URL, at: MIGRATIONS_DIR };
36
+ }
37
+ const detail = isUltimateErrorShape(error) ? findingFrom(error).cause : renderThrowable(error);
38
+ const missing = detail.startsWith(PGLITE_MISSING);
39
+ return {
40
+ code: 'X_SCHEMA_DUMP_DRIFT',
41
+ cause: missing
42
+ ? `the schema dump is replayed on the embedded database and ${detail}`
43
+ : `the migrations do not replay on the scratch database, so the schema dump cannot be produced: ${detail}`,
44
+ fix: missing
45
+ ? // A literal, as `@ultimat3/db`'s own `PGLITE_FIX` is: a `fix:` splices nothing into its
46
+ // command position, and `db-schema-refresh.test.ts` holds it equal to `PGLITE_PACKAGE`.
47
+ 'bun add -d @electric-sql/pglite # then: x db gen'
48
+ : 'x db migrate --json # applies the same migrations to the dev database; a statement refused there too is the one to repair — then: x db gen',
49
+ docs: ERROR_DOCS_URL,
50
+ at: missing ? SCHEMA_DUMP_DIR : MIGRATIONS_DIR,
51
+ };
52
+ }
53
+
54
+ /**
55
+ * The same finding, for a run of `x db gen` that DID write its migration before the dump failed.
56
+ * The exit code is 1 and the migration is on disk, and a reader who sees only the first fact
57
+ * generates a second migration for a schema that is already recorded. So the cause leads with
58
+ * the file that exists, and says what the next `x db gen` will and will not do.
59
+ */
60
+ export const afterWrittenMigration = (finding: Finding, id: string): Finding => ({
61
+ ...finding,
62
+ cause: `migration ${id} WAS written to ${MIGRATIONS_DIR} and is not to be generated again; only the schema dump failed, and once that is repaired a bare x db gen writes the dump and no second migration — ${finding.cause}`,
63
+ });
64
+
65
+ /** An app with a migration and no dump directory: one finding, and no database booted to say so. */
66
+ export const dumpNeverGenerated = (): Finding => ({
67
+ code: 'X_SCHEMA_DUMP_DRIFT',
68
+ cause: `${MIGRATIONS_DIR} holds migrations and ${SCHEMA_DUMP_DIR} does not exist: the schema dump was never generated`,
69
+ fix: 'x db gen',
70
+ docs: ERROR_DOCS_URL,
71
+ at: SCHEMA_DUMP_DIR,
72
+ });
73
+
74
+ export interface SchemaDumpRefresh extends SchemaDumpWrite {
75
+ /**
76
+ * `skipped` — no migration and no dump, so nothing is owed and no database is booted.
77
+ * `failed` — the migrations did not replay; `finding` says why, and the dump on disk is left
78
+ * exactly as it was.
79
+ */
80
+ readonly status: 'written' | 'unchanged' | 'skipped' | 'failed';
81
+ readonly finding?: Finding | undefined;
82
+ /** The replayed catalog, for a caller that compares a live database against it. */
83
+ readonly catalog?: CatalogDescription | undefined;
84
+ }
85
+
86
+ const NOTHING: SchemaDumpWrite = { written: [], removed: [], total: 0 };
87
+
88
+ /**
89
+ * Regenerate the dump after `x db gen` or `x db migrate`. Never throws: a replay that fails is
90
+ * reported as the finding it is, after the migration the command was asked for is already written.
91
+ */
92
+ export async function refreshSchemaDump(
93
+ root: string,
94
+ env: Readonly<Record<string, string | undefined>> = Bun.env,
95
+ // Injected so the reporting rules are testable without booting a database per case.
96
+ replay: (migrations: readonly Migration[]) => Promise<ReplayedSchema> = replayFor(
97
+ root,
98
+ env,
99
+ false,
100
+ ),
101
+ ): Promise<SchemaDumpRefresh> {
102
+ const migrations = await readMigrations(root);
103
+ if (migrations.length === 0 && !hasSchemaDump(root)) return { ...NOTHING, status: 'skipped' };
104
+ let replayed: ReplayedSchema;
105
+ try {
106
+ replayed = await replay(migrations);
107
+ } catch (error) {
108
+ return { ...NOTHING, status: 'failed', finding: replayFailure(error) };
109
+ }
110
+ const write = await writeSchemaDump(root, replayed.files);
111
+ const changed = write.written.length + write.removed.length > 0;
112
+ return { ...write, status: changed ? 'written' : 'unchanged', catalog: replayed.catalog };
113
+ }
114
+
115
+ /** The `--json` shape both commands report under `data.schemaDump`. */
116
+ export const schemaDumpJson = (refresh: SchemaDumpRefresh): JsonValue => ({
117
+ directory: SCHEMA_DUMP_DIR,
118
+ status: refresh.status,
119
+ files: refresh.total,
120
+ written: [...refresh.written],
121
+ removed: [...refresh.removed],
122
+ });
@@ -55,8 +55,8 @@ export interface DevDashboardInput {
55
55
 
56
56
  /**
57
57
  * Read-only is already enforced by `assertReadOnly` inside `dbPanel`, before `runSql` is ever
58
- * reached. A second gate here would be a second authz: two places to update when `x db psql
59
- * --write` changes what is allowed, and one of them would eventually disagree.
58
+ * reached. A second gate here would be a second authz: two places to update when what the panel
59
+ * allows changes, and one of them would eventually disagree.
60
60
  */
61
61
  async function runSql(input: DevDashboardInput, sql: string): Promise<SqlResult> {
62
62
  const started = performance.now();
@@ -21,6 +21,7 @@ import type { PageSpeculation } from './page-speculation';
21
21
  import { loadSpeculation, pageSpeculation } from './page-speculation';
22
22
  import { pageSync } from './page-sync';
23
23
  import { loadPwaArtifacts } from './pwa-artifacts';
24
+ import { adminMountRoutes } from './runtime-admin';
24
25
  import { assetRoutes } from './runtime-assets';
25
26
  import { appRoutes } from './runtime-render';
26
27
  import { servedStorage, storageRoutes } from './runtime-storage';
@@ -150,6 +151,14 @@ export async function devRouteTable(input: DevRouteTableInput): Promise<DevRoute
150
151
  ...sync.routes,
151
152
  ...navigation.routes,
152
153
  ...pagePostRoutes(),
154
+ // The app's generated admin, when it declared one (`defineAdmin`): every screen under its base
155
+ // path, served through the same document builder as the pages below. Nothing for an app with
156
+ // no admin — the registry is asked, the package is not imported.
157
+ ...adminMountRoutes({
158
+ buildId: input.buildId,
159
+ themeHead: theme.head,
160
+ ...(origin === undefined ? {} : { origin }),
161
+ }),
153
162
  ...appRoutes({
154
163
  buildId: input.buildId,
155
164
  ...(input.isr === undefined ? {} : { isr: input.isr }),
@@ -0,0 +1,39 @@
1
+ // `x doctor`'s coverage listing: every file the app's coverage floor does not see, and the reason
2
+ // `x.verify.json` gives for each. A LISTING, never a finding: an `exclude` with its `why` is a
3
+ // decision the app made in a committed file — printing it is what keeps the decision in view, and
4
+ // a red `x doctor` over it would be the framework overruling what it asked to be written down.
5
+
6
+ import type { CoverageExclude } from './coverage-floor';
7
+ import { msg } from './messages';
8
+ import type { CommandResult, JsonValue } from './output';
9
+ import { readVerifyFloor } from './verify-floor';
10
+
11
+ /** The I/O half: the excludes this app's `x.verify.json` states. None when it states no floor. */
12
+ export async function coverageExcludesProbe(root: string): Promise<readonly CoverageExclude[]> {
13
+ return (await readVerifyFloor(root))?.coverage?.exclude ?? [];
14
+ }
15
+
16
+ /**
17
+ * The listing, on a result `x doctor` already decided: `data.coverage.exclude` for a machine, one
18
+ * line per entry for a person. `ok`, the summary and the findings are returned as they came.
19
+ */
20
+ export function withCoverageExcludes(
21
+ result: CommandResult,
22
+ exclude: readonly CoverageExclude[],
23
+ ): CommandResult {
24
+ const data = typeof result.data === 'object' && result.data !== null ? result.data : {};
25
+ const listed: JsonValue = exclude.map(({ glob, why }) => ({ glob, why }));
26
+ return {
27
+ ...result,
28
+ data: { ...(data as Record<string, JsonValue>), coverage: { exclude: listed } },
29
+ ...(exclude.length === 0
30
+ ? {}
31
+ : {
32
+ lines: [
33
+ ...(result.lines ?? []),
34
+ msg('cli.doctor.coverageExclude', { count: exclude.length }),
35
+ ...exclude.map(({ glob, why }) => ` ${glob} — ${why}`),
36
+ ],
37
+ }),
38
+ };
39
+ }
@@ -0,0 +1,54 @@
1
+ // `x doctor`'s guard listing: the guards the framework ships that this app's `guards/` does not
2
+ // hold, each with the command that adds it. A LISTING, never a finding: every shipped guard says
3
+ // "delete this file to drop the rule", so an app without one may have decided — and a red
4
+ // `x doctor` over a decision would be the framework overruling the convention it handed over.
5
+
6
+ import { GUARD_DIR, guardPaths } from './guards';
7
+ import { msg } from './messages';
8
+ import type { CommandResult, JsonValue } from './output';
9
+ import { SHIPPED_GUARD_NAMES } from './templates/scaffold-guards';
10
+
11
+ export interface MissingGuard {
12
+ readonly name: string;
13
+ /** The command that writes it, with its test. Then `x verify` says what it finds. */
14
+ readonly add: string;
15
+ }
16
+
17
+ /** The rule, pure: which shipped names have no file among the guards an app holds. */
18
+ export function missingShippedGuards(present: readonly string[]): readonly MissingGuard[] {
19
+ const held = new Set(present.map((path) => path.replace(/\.tsx?$/, '')));
20
+ return SHIPPED_GUARD_NAMES.filter((name) => !held.has(`${GUARD_DIR}/${name}`)).map((name) => ({
21
+ name,
22
+ add: `x g guard ${name}`,
23
+ }));
24
+ }
25
+
26
+ /** The I/O half: the same enumeration the `boundaries` step runs, so the two cannot disagree. */
27
+ export async function shippedGuardsProbe(root: string): Promise<readonly MissingGuard[]> {
28
+ return missingShippedGuards(await guardPaths(root));
29
+ }
30
+
31
+ /**
32
+ * The listing, on a result `x doctor` already decided: `data.guards.missing` for a machine, one
33
+ * line per command for a person. `ok`, the summary and the findings are returned as they came.
34
+ */
35
+ export function withGuardListing(
36
+ result: CommandResult,
37
+ missing: readonly MissingGuard[],
38
+ ): CommandResult {
39
+ const data = typeof result.data === 'object' && result.data !== null ? result.data : {};
40
+ const listed: JsonValue = missing.map(({ name, add }) => ({ name, add }));
41
+ return {
42
+ ...result,
43
+ data: { ...(data as Record<string, JsonValue>), guards: { missing: listed } },
44
+ ...(missing.length === 0
45
+ ? {}
46
+ : {
47
+ lines: [
48
+ ...(result.lines ?? []),
49
+ msg('cli.doctor.guards', { count: missing.length }),
50
+ ...missing.map(({ add }) => ` ${add}`),
51
+ ],
52
+ }),
53
+ };
54
+ }
@@ -0,0 +1,132 @@
1
+ // `x doctor`'s sealed-column checks: the columns the app declared `.sealed()`, against the master
2
+ // keys this environment declares. Its own file for `doctor-offline.ts`'s reason — answering needs
3
+ // the app's entities and the committed secrets, and `cmd-doctor.ts`'s job is the probe. The split
4
+ // is the same too: reaching the disk is `sealedKeysProbe`, deciding is `sealedKeyFindings`.
5
+
6
+ import {
7
+ ERROR_DOCS_URL,
8
+ findMasterKey,
9
+ readSecretsFile,
10
+ SECRETS_RETIRED_KEYS_ENV,
11
+ sealKeyIds,
12
+ secretsFileExists,
13
+ } from '@ultimat3/core';
14
+ import { registeredEntities, sealedFields } from '@ultimat3/entity';
15
+ import { loadApp } from './app-load';
16
+ import type { Finding } from './output';
17
+ import { quoteArg } from './shell-quote';
18
+
19
+ export interface SealedColumnFact {
20
+ readonly entity: string;
21
+ /** Property key, as the entity declares it. */
22
+ readonly column: string;
23
+ readonly lookup: boolean;
24
+ }
25
+
26
+ export interface SealedKeysFact {
27
+ /**
28
+ * Every sealed column the app declares, or `undefined` when the app would not load — a short
29
+ * registry reads as "no sealed columns", which is the false green `appEntities` exists to refuse.
30
+ */
31
+ readonly columns: readonly SealedColumnFact[] | undefined;
32
+ /** The key ids in force, or `undefined` when no usable master key was found. */
33
+ readonly keys: { readonly current: string; readonly retired: readonly string[] } | undefined;
34
+ }
35
+
36
+ const named = (columns: readonly SealedColumnFact[]): string =>
37
+ columns.map((one) => `${one.entity}.${one.column}`).join(', ');
38
+
39
+ /**
40
+ * The rule, pure. Two conditions, and an app with no sealed column is judged by neither.
41
+ *
42
+ * No key: every write to a sealed column is `X_SEAL_KEY_MISSING` — core's own code and its own
43
+ * fix, reported here before the first request finds out.
44
+ *
45
+ * A retired key still declared: a rotation whose re-seal has not finished. That is a live-system
46
+ * state and not a fault, but it has a cost that ends only when someone acts — a `lookup` column
47
+ * matches through `in (…)` over every key and its `.unique()` holds per key — so it is a finding
48
+ * until the retired key is dropped.
49
+ */
50
+ export function sealedKeyFindings(fact: SealedKeysFact): readonly Finding[] {
51
+ const columns = fact.columns;
52
+ if (columns === undefined || columns.length === 0) return [];
53
+ if (fact.keys === undefined) {
54
+ return [
55
+ {
56
+ code: 'X_SEAL_KEY_MISSING',
57
+ cause: `${named(columns)} ${columns.length === 1 ? 'is' : 'are'} sealed and this environment has no usable master key, so every write to ${columns.length === 1 ? 'it' : 'them'} is refused`,
58
+ fix: 'x secrets init # or, where the key already exists: export ULTIMATE_SECRETS_KEY="$(cat .secrets.key)"',
59
+ docs: ERROR_DOCS_URL,
60
+ },
61
+ ];
62
+ }
63
+ const [oldest] = fact.keys.retired;
64
+ if (oldest === undefined) return [];
65
+ const lookups = columns.filter((one) => one.lookup);
66
+ const weakened =
67
+ lookups.length === 0
68
+ ? ''
69
+ : ` — ${named(lookups)} ${lookups.length === 1 ? 'is a lookup column' : 'are lookup columns'}, so until then equality reads through in (…) over every key and .unique() holds per key`;
70
+ return [
71
+ {
72
+ code: 'X_SEAL_RESEAL_PENDING',
73
+ cause: `${named(columns)} may still hold values sealed under retired master key(s) ${fact.keys.retired.join(', ')}: a rotation whose re-seal has not finished${weakened}`,
74
+ // The id is 16 hex characters computed here from a key, never text read from a file.
75
+ fix: `x secrets rotate --drop ${quoteArg(oldest)} --json # AFTER a backfill() has rewritten every row — update(id, { column: row.column }) seals under the current key`,
76
+ docs: ERROR_DOCS_URL,
77
+ },
78
+ ];
79
+ }
80
+
81
+ /**
82
+ * The retired ring as a booted process would see it: the real environment first, the committed
83
+ * file second — `installSecrets()`'s own order, without installing anything into this process.
84
+ */
85
+ async function retiredRing(
86
+ root: string,
87
+ env: Readonly<Record<string, string | undefined>>,
88
+ ): Promise<string | undefined> {
89
+ const declared = env[SECRETS_RETIRED_KEYS_ENV];
90
+ if (declared !== undefined && declared !== '') return declared;
91
+ const key = findMasterKey(root, { ...env });
92
+ if (key === undefined || !secretsFileExists(root)) return undefined;
93
+ return (await readSecretsFile(root, key))[SECRETS_RETIRED_KEYS_ENV];
94
+ }
95
+
96
+ /** The app's sealed columns, or `undefined` when it would not load. */
97
+ export type DeclaredSealedColumns = (
98
+ root: string,
99
+ ) => Promise<readonly SealedColumnFact[] | undefined>;
100
+
101
+ /** `appEntities`' rule (`schema-drift.ts`): a registry short one module is not "none declared". */
102
+ const appSealedColumns: DeclaredSealedColumns = async (root) => {
103
+ const app = await loadApp(root);
104
+ if (app.findings.length > 0) return undefined;
105
+ return registeredEntities().flatMap((entry) =>
106
+ // `core` is absent on a hand-registered entry, which declares no columns to seal.
107
+ (entry.core === undefined ? [] : sealedFields(entry.core)).map((field) => ({
108
+ entity: entry.name,
109
+ column: field.property,
110
+ lookup: field.lookup,
111
+ })),
112
+ );
113
+ };
114
+
115
+ export async function sealedKeysProbe(
116
+ root: string,
117
+ env: Readonly<Record<string, string | undefined>> = process.env,
118
+ declared: DeclaredSealedColumns = appSealedColumns,
119
+ ): Promise<SealedKeysFact> {
120
+ const columns = await declared(root);
121
+ // No sealed column means no question about keys, and no reason to open the secrets file.
122
+ if (columns === undefined || columns.length === 0) return { columns, keys: undefined };
123
+ try {
124
+ const ring = await retiredRing(root, env);
125
+ const source = { ...env, ...(ring === undefined ? {} : { [SECRETS_RETIRED_KEYS_ENV]: ring }) };
126
+ return { columns, keys: await sealKeyIds({ root, env: source }) };
127
+ } catch {
128
+ // Whatever refused — no key, a malformed one, a secrets file this key does not open — the
129
+ // answer for a sealed column is the same: there is no key ring to seal under.
130
+ return { columns, keys: undefined };
131
+ }
132
+ }
@@ -0,0 +1,82 @@
1
+ // Whether importing an app module would bring a DOCUMENT module into this process: a component
2
+ // (`.tsx`) or a stylesheet. A role that renders nothing asks this before each import
3
+ // (`role-load.ts`), so a worker never compiles the app's Sass or evaluates its pages to learn that
4
+ // they register no job.
5
+
6
+ import { resolvedImports } from './module-imports';
7
+ import { hasPathSegment } from './path-segments';
8
+
9
+ /** A component or a stylesheet: what only a process that renders documents evaluates. */
10
+ const DOCUMENT = /\.(?:[jt]sx|s[ac]ss|css)$/;
11
+
12
+ export const isDocumentModule = (path: string): boolean => DOCUMENT.test(path);
13
+
14
+ /** An installed framework package, under either linker: `…/node_modules/@ultimat3/<pkg>/…`. */
15
+ const FRAMEWORK_SCOPE = '/node_modules/@ultimat3/';
16
+
17
+ export interface DocumentGraph {
18
+ /**
19
+ * True when importing `absolute` would evaluate a document module this process has not already
20
+ * loaded. Errs toward `false`: a specifier that will not resolve and a file that will not parse
21
+ * contribute no edge, so the caller imports the module and the import itself reports the fault.
22
+ */
23
+ reachesDocument(absolute: string): Promise<boolean>;
24
+ }
25
+
26
+ /**
27
+ * `loaded` answers whether a module is already in this process — a module the API index imported
28
+ * costs nothing more, whatever it reaches, so the walk stops there. Injected, so a test decides it.
29
+ */
30
+ export function createDocumentGraph(loaded: (absolute: string) => boolean): DocumentGraph {
31
+ const edges = new Map<string, readonly string[]>();
32
+ const verdicts = new Map<string, boolean>();
33
+
34
+ /**
35
+ * The files importing `path` evaluates, when the walk follows them: the app's own modules and
36
+ * the framework's — `@ultimat3/ui` is a barrel of components, and an app module importing it
37
+ * brings every one of them and their Sass. A third-party package is not walked: it ships no
38
+ * component this framework's loader compiles. A lazy import's target is no edge at all.
39
+ */
40
+ async function importsOf(path: string): Promise<readonly string[]> {
41
+ const known = edges.get(path);
42
+ if (known !== undefined) return known;
43
+ // Unreadable: no edge, and the caller's own import is what reports it.
44
+ const source = await Bun.file(path)
45
+ .text()
46
+ .catch(() => '');
47
+ const found = resolvedImports(path, source, 'evaluated').filter(
48
+ (target) => !hasPathSegment(target, 'node_modules') || target.includes(FRAMEWORK_SCOPE),
49
+ );
50
+ edges.set(path, found);
51
+ return found;
52
+ }
53
+
54
+ return {
55
+ async reachesDocument(absolute: string): Promise<boolean> {
56
+ const known = verdicts.get(absolute);
57
+ if (known !== undefined) return known;
58
+ // The whole reachable set, never a per-node recursion: inside an import cycle a node's own
59
+ // answer depends on one still being computed, and a `false` memoised there is wrong.
60
+ const seen = new Set<string>();
61
+ const stack = [absolute];
62
+ let reaches = false;
63
+ while (stack.length > 0 && !reaches) {
64
+ const path = stack.pop();
65
+ if (path === undefined || seen.has(path)) continue;
66
+ seen.add(path);
67
+ if (verdicts.get(path) === true || (isDocumentModule(path) && !loaded(path))) {
68
+ reaches = true;
69
+ break;
70
+ }
71
+ // Already here, or already cleared: nothing below it is new to this process.
72
+ if (isDocumentModule(path) || loaded(path) || verdicts.get(path) === false) continue;
73
+ stack.push(...(await importsOf(path)));
74
+ }
75
+ // A clean walk clears everything it visited — each one's reachable set is inside this one's.
76
+ // A walk that found a document proves it for the start alone.
77
+ if (reaches) verdicts.set(absolute, true);
78
+ else for (const path of seen) verdicts.set(path, false);
79
+ return reaches;
80
+ },
81
+ };
82
+ }
@@ -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,6 +210,16 @@ 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',
208
223
  ] as const;
209
224
 
210
225
  /**
@@ -314,6 +329,10 @@ export const CLI_ERROR_TITLES: Readonly<Record<CliOwnedErrorCode, string>> = {
314
329
  X_GUARD_INVALID: 'a file in guards/ exports no usable guard',
315
330
  X_GUARD_FAILED: 'an app guard threw instead of returning findings',
316
331
  X_GUARD_FINDING_INVALID: "an app guard's finding breaks the error contract",
332
+ X_BROWSER_TRANSPORT_BYPASS:
333
+ 'browser-reachable code opens its own connection instead of using the one seam',
334
+ X_BROWSER_SERVER_BARREL:
335
+ "browser-reachable code imports a package's server barrel where the package publishes a browser entry",
317
336
  X_CLI_FLAG_UNREAD: 'a command declares a flag no code reads',
318
337
  X_SECRETS_EDITOR_MISSING: 'no $EDITOR to open the decrypted secrets in',
319
338
  X_SECRETS_EDIT_FAILED: 'the editor exited non-zero, so nothing was resealed',
@@ -360,6 +379,19 @@ export const CLI_ERROR_TITLES: Readonly<Record<CliOwnedErrorCode, string>> = {
360
379
  X_DEV_RESTART_REQUIRED:
361
380
  'a save reached a module that defines a primitive; only a new process serves it',
362
381
  X_DEV_ROOT_GONE: 'the app root x dev serves was deleted or moved',
382
+ X_SEAL_RESEAL_PENDING: 'sealed columns still hold values under a retired master key',
383
+ X_VERIFY_STEP_TIMEOUT: 'a gate step ran past its deadline and was stopped',
384
+ X_COVERAGE_BELOW_FLOOR: 'the unit suite covers less of the app than x.verify.json states',
385
+ X_COVERAGE_FLOOR_UNSTATED:
386
+ 'x.verify.json states no coverage floor, or one under 95 with no reason',
387
+ X_COVERAGE_FLOOR_STALE: 'the app now covers more than the floor in x.verify.json states',
388
+ X_DB_HANDLE_UNREGISTERED: "an entity x g wrote is not in the app's typed database handle",
389
+ X_ROLE_LOAD_INCOMPLETE: 'a worker or scheduler did not register a job or task the manifest names',
390
+ X_ADMIN_RESOURCE_UNWIRED: "an admin override x g wrote is not in the app's defineAdmin() call",
391
+ X_IMAGE_NOT_PREBUILT:
392
+ 'a container role built at boot what the image build should have: island chunks or stylesheets',
393
+ X_ADMIN_UNSCANNED:
394
+ 'a defineAdmin() declaration sits outside the app scan, so the admin is never mounted',
363
395
  };
364
396
 
365
397
  // One unconditional call, so a second package claiming one of the CLI's codes throws
@@ -7,6 +7,7 @@
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';
@@ -49,6 +50,9 @@ export const FRAMEWORK_SCHEMA: readonly FrameworkSchema[] = Object.freeze([
49
50
  'x_scheduler_leader',
50
51
  'x_job_leases',
51
52
  'x_job_events',
53
+ 'x_job_pauses',
54
+ 'x_job_workers',
55
+ 'x_job_counters',
52
56
  ]),
53
57
  ddl: Object.freeze([SQL_JOBS_TABLE]),
54
58
  }),
@@ -109,6 +113,17 @@ export const FRAMEWORK_SCHEMA: readonly FrameworkSchema[] = Object.freeze([
109
113
  tables: Object.freeze(['x_notify_inbox']),
110
114
  ddl: Object.freeze([SQL_NOTIFY_INBOX_TABLE]),
111
115
  }),
116
+ /**
117
+ * The admin's durable audit trail — what `postgresAuditLog()` writes and every detail page's
118
+ * history card reads. Without this row an app that chose the durable log met `42P01` on the
119
+ * first audited request. Imported through `@ultimat3/admin/schema`, a leaf module: the DDL is
120
+ * on every role's boot path, and not one admin screen is (`serve-graph.test.ts`).
121
+ */
122
+ Object.freeze({
123
+ pkg: '@ultimat3/admin',
124
+ tables: Object.freeze([ADMIN_AUDIT_TABLE]),
125
+ ddl: Object.freeze([SQL_ADMIN_AUDIT_TABLE]),
126
+ }),
112
127
  ]);
113
128
 
114
129
  /** Every relation this boot creates, flattened. */