@ultimat3/cli 19.2.0 → 19.3.2

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 (59) hide show
  1. package/CLAUDE.md +135 -9
  2. package/README.md +1 -1
  3. package/package.json +29 -29
  4. package/src/app-agents-md.ts +14 -3
  5. package/src/app-boundaries.ts +11 -2
  6. package/src/app-load.ts +96 -25
  7. package/src/budgets.ts +17 -6
  8. package/src/cmd-dev-fixture.ts +25 -0
  9. package/src/cmd-dev.ts +48 -46
  10. package/src/cmd-doctor.ts +61 -23
  11. package/src/cmd-generate.ts +25 -3
  12. package/src/cmd-i18n.ts +10 -3
  13. package/src/cmd-jobs.ts +56 -10
  14. package/src/cmd-test.ts +15 -10
  15. package/src/db-seed.ts +2 -1
  16. package/src/dev-queue.ts +16 -2
  17. package/src/dev-reload.ts +46 -0
  18. package/src/dev-render.ts +35 -9
  19. package/src/dev-runtime.ts +4 -1
  20. package/src/dev-sync.ts +11 -3
  21. package/src/dev-watch-tree.ts +226 -0
  22. package/src/dev-watch.ts +59 -37
  23. package/src/doctor-offline.ts +122 -0
  24. package/src/error-catalog.ts +4 -5
  25. package/src/fix-command.ts +40 -1
  26. package/src/fix-path.ts +10 -11
  27. package/src/flag-number.ts +15 -0
  28. package/src/generate-files.ts +24 -2
  29. package/src/generate-kinds.ts +54 -4
  30. package/src/generate-write.ts +25 -2
  31. package/src/gitignore.ts +145 -0
  32. package/src/hold.ts +50 -17
  33. package/src/index.ts +1 -1
  34. package/src/island-bundle.ts +2 -1
  35. package/src/island-states-load.ts +2 -1
  36. package/src/jobs-driver.ts +4 -1
  37. package/src/mcp-host.ts +18 -9
  38. package/src/parse.ts +17 -0
  39. package/src/path-segments.ts +14 -0
  40. package/src/prerender.ts +46 -20
  41. package/src/retry-memo.ts +37 -0
  42. package/src/scaffold-fixture.ts +17 -0
  43. package/src/serve.ts +40 -5
  44. package/src/source-files.ts +3 -1
  45. package/src/sw-artifacts.ts +71 -7
  46. package/src/templates/action.ts +47 -16
  47. package/src/templates/admin-page.ts +49 -1
  48. package/src/templates/island.ts +4 -2
  49. package/src/templates/scaffold-container.ts +12 -0
  50. package/src/templates/scaffold-docs.ts +7 -0
  51. package/src/templates/scaffold-entries.ts +4 -2
  52. package/src/templates/scaffold-repo.ts +7 -2
  53. package/src/templates/slice-foundation.ts +36 -0
  54. package/src/test-passes.ts +79 -0
  55. package/src/test-shards.ts +110 -36
  56. package/src/verify-checks.ts +11 -8
  57. package/src/verify-floor.ts +59 -3
  58. package/src/verify-step.ts +4 -4
  59. package/src/verify-tests.ts +14 -2
@@ -50,15 +50,41 @@ export interface ServiceWorkerInput {
50
50
  * the offline fallback with no CSS is a page the visitor cannot read.
51
51
  */
52
52
  readonly styles: StyleBundle;
53
+ /**
54
+ * What the build RENDERED, keyed by the route path it was rendered for. Optional because only a
55
+ * static export has documents to hash: `x dev` and the container emit the worker at boot, where
56
+ * no page has been built yet, and `buildPrecacheManifest` falls back to the build id for a route
57
+ * this map does not name.
58
+ */
59
+ readonly documents?: ReadonlyMap<string, RenderedDocument>;
60
+ }
61
+
62
+ /**
63
+ * One rendered document, as the precache manifest needs it.
64
+ *
65
+ * `revision` is `contentHash(html)` — `@ultimat3/render`'s own, the function that already stamps
66
+ * an ETag — and never the build id. `precache.ts`' header states the rule and nothing kept it:
67
+ * `pwaRoutes` projected four of `PwaRoute`'s eight fields, so every route entry read
68
+ * `{"url":"/","revision":"build-aaa","bytes":0}` and two deploys of a byte-identical site
69
+ * re-fetched every precached document. The zero was the second half — `DEFAULT_PRECACHE_WARN_BYTES`
70
+ * is a 5 MB budget over a total that could not count one byte of HTML.
71
+ */
72
+ export interface RenderedDocument {
73
+ readonly revision: string;
74
+ readonly bytes: number;
53
75
  }
54
76
 
55
77
  /**
56
78
  * The route table, as the service worker sees it. `api/` is dropped: an API response is a JSON
57
79
  * document whose freshness is the app's business, and precaching one serves a stale answer to a
58
- * client that had a network. Only the four fields `PwaRoute` reads cross — a descriptor carries
59
- * budgets and policy flags that a browser has no use for.
80
+ * client that had a network. Only the fields a browser can act on cross — a descriptor carries
81
+ * budgets and policy flags it has no use for — plus, for a route this build rendered, the content
82
+ * hash and the byte count of the document it produced.
60
83
  */
61
- const pwaRoutes = (routes: readonly RouteDescriptor[]): readonly PwaRoute[] =>
84
+ const pwaRoutes = (
85
+ routes: readonly RouteDescriptor[],
86
+ documents: ReadonlyMap<string, RenderedDocument>,
87
+ ): readonly PwaRoute[] =>
62
88
  // `flatMap` rather than `filter().map()`: the filter's predicate does not narrow `surface` for
63
89
  // the map that follows it, and `PwaRoute` declares the two navigable surfaces only. A cast would
64
90
  // hide the day a fifth surface arrives.
@@ -66,6 +92,8 @@ const pwaRoutes = (routes: readonly RouteDescriptor[]): readonly PwaRoute[] =>
66
92
  // `shared/` is dropped with `api/`, and for a stronger reason: it is not a URL at all — the
67
93
  // surface exists so two routes can import one module, and a browser can never navigate to it.
68
94
  if (route.surface !== 'site' && route.surface !== 'app') return [];
95
+ // A `Map`, so a route path that happens to spell a prototype member cannot answer with one.
96
+ const document = documents.get(route.path);
69
97
  return [
70
98
  {
71
99
  path: route.path,
@@ -73,6 +101,10 @@ const pwaRoutes = (routes: readonly RouteDescriptor[]): readonly PwaRoute[] =>
73
101
  mode: route.mode,
74
102
  offline: route.offline,
75
103
  dynamic: route.dynamic,
104
+ // Absent rather than invented for a route no build rendered — an `ssr` page, or one this
105
+ // pass could not produce. `buildPrecacheManifest` then falls back to the build id, which
106
+ // is the honest answer when there are no bytes to hash.
107
+ ...(document === undefined ? {} : { revision: document.revision, bytes: document.bytes }),
76
108
  },
77
109
  ];
78
110
  });
@@ -128,14 +160,34 @@ export function serviceWorkerArtifacts(
128
160
  input: ServiceWorkerInput,
129
161
  ): ServiceWorkerArtifacts | undefined {
130
162
  const pwa = input.pwa;
131
- if (pwa.offline.fallback === null) return undefined;
163
+ const head = serviceWorkerHead(pwa);
164
+ const fallback = pwa.offline.fallback;
165
+ // One predicate DECIDES — `serviceWorkerHead`, so a document can never name a script this
166
+ // function then declines to emit — and the `null` check is what NARROWS `fallback` below:
167
+ // TypeScript cannot learn a `string` from the other's answer, and a cast would hide the day the
168
+ // two stop agreeing.
169
+ if (head === undefined || fallback === null) return undefined;
170
+ const documents = input.documents ?? new Map<string, RenderedDocument>();
171
+ // The offline document is the one entry `buildPrecacheManifest` adds ITSELF, as
172
+ // `reason: 'fallback'`, ahead of every route — and `add()` keeps the first entry per url, so its
173
+ // revision is the one that decides. Without this pair it was the build id whatever the build
174
+ // knew, which re-downloaded the single page an offline navigation depends on on every deploy.
175
+ // Absent when this pass did not render the fallback (no route serves it — `x doctor` reports
176
+ // that as `X_PWA_NO_OFFLINE_FALLBACK`), and then `@ultimat3/pwa` falls back to the build id.
177
+ const fallbackDocument = documents.get(fallback);
132
178
  const output = generateServiceWorker(
133
- pwaRoutes(input.routes),
179
+ pwaRoutes(input.routes, documents),
134
180
  {
135
181
  scope: SW_SCOPE,
136
182
  swPath: SERVICE_WORKER_PATH,
183
+ ...(fallbackDocument === undefined
184
+ ? {}
185
+ : {
186
+ offlineFallbackRevision: fallbackDocument.revision,
187
+ offlineFallbackBytes: fallbackDocument.bytes,
188
+ }),
137
189
  offline: {
138
- fallback: pwa.offline.fallback,
190
+ fallback,
139
191
  ...(pwa.offline.image === null ? {} : { image: pwa.offline.image }),
140
192
  ...(pwa.offline.font === null ? {} : { font: pwa.offline.font }),
141
193
  neverCache: pwa.offline.neverCache,
@@ -148,7 +200,7 @@ export function serviceWorkerArtifacts(
148
200
  return {
149
201
  source: output.source,
150
202
  register: registerSource(),
151
- head: `<script src="${SW_REGISTER_PATH}" defer></script>`,
203
+ head,
152
204
  precache: output.precache,
153
205
  // `output.warnings` IS `output.precache.warnings` — the generator returns the manifest's list
154
206
  // verbatim — so it is read once, not twice. The push line is this module's own, and it is the
@@ -160,6 +212,18 @@ export function serviceWorkerArtifacts(
160
212
  };
161
213
  }
162
214
 
215
+ /**
216
+ * The one `<script src>` a document needs, or `undefined` for an app that gets no worker.
217
+ *
218
+ * Separate from `serviceWorkerArtifacts` because the two are wanted at different moments: a static
219
+ * export has to put this tag in every document it renders, and the WORKER cannot be emitted until
220
+ * those documents exist — its precache manifest is built from their content hashes. One predicate
221
+ * for both (`offline.fallback === null` is `generateServiceWorker`'s refusal, spent early), so a
222
+ * document can never name a script the export does not carry.
223
+ */
224
+ export const serviceWorkerHead = (pwa: PwaArtifacts): string | undefined =>
225
+ pwa.offline.fallback === null ? undefined : `<script src="${SW_REGISTER_PATH}" defer></script>`;
226
+
163
227
  /**
164
228
  * `pwa.push: true` with nothing to sign a subscription with. There is no `pwa.vapid` config key
165
229
  * yet, so today this fires for EVERY app that sets the flag — deliberately: a switch that silently
@@ -5,12 +5,25 @@
5
5
  import type { FeatureTarget } from './entity';
6
6
  import type { GeneratedFile, NameSet } from './naming';
7
7
  import { names } from './naming';
8
- import { sliceFoundation } from './slice-foundation';
8
+ import { sliceExports, sliceFoundation } from './slice-foundation';
9
9
  import { wrapImport } from './wrap';
10
10
 
11
+ /**
12
+ * The handler's lookup-by-id, or the comment that says what a slice needs before it can have one.
13
+ * `x g resource` writes a slice whose `errors.ts` declares `<Feature>NotFoundError`, and so does
14
+ * the foundation this generator lays under an empty directory; a slice an author wrote by hand
15
+ * (ai-maxxing's `fleet`, with `HostNotFoundError` and `SessionNotFoundError`) declares the errors
16
+ * it has and not this one. Importing it anyway is a file that fails at import.
17
+ */
18
+ const missingLookup = (feature: NameSet): string =>
19
+ ` // No lookup by id: ../errors declares no ${feature.pascal}NotFoundError, and a row that is not
20
+ // there needs one to be thrown for it. Declare it there — the shape \`x g resource\` writes —
21
+ // then read the row through ../repo and throw it when the read answers nothing.`;
22
+
11
23
  const actionSource = (
12
24
  name: NameSet,
13
25
  feature: NameSet,
26
+ lookup: boolean,
14
27
  ): string => `// ${name.camel}: one mutation, server-authoritative. Input is validated before the handler runs
15
28
  // and the policy is the same object the MCP tool and the HTTP route evaluate.
16
29
  // \`t\` comes from @ultimat3/action, not @ultimat3/schema: an action file imports one package.
@@ -18,23 +31,25 @@ const actionSource = (
18
31
  import { action, t } from '@ultimat3/action';
19
32
  // One directory up: actions live in \`actions/\`, the feature's errors, policy and repo are the
20
33
  // slice's own files and are shared by every action in it.
21
-
22
- import { ${feature.pascal}NotFoundError } from '../errors';
23
- ${wrapImport([`can${feature.pascal}Write`, `${feature.camel}Tag`], '../policy')}
24
- import * as repo from '../repo';
25
-
34
+ ${lookup ? `\nimport { ${feature.pascal}NotFoundError } from '../errors';\n` : ''}${wrapImport([`can${feature.pascal}Write`, `${feature.camel}Tag`], '../policy')}
35
+ ${lookup ? "import * as repo from '../repo';\n" : ''}
26
36
  export const ${name.camel} = action({
27
37
  // orgId is part of the input because the policy decides on it — authz reads the declaration,
28
38
  // never the database.
29
39
  input: t.object({ id: t.uuid, orgId: t.uuid }),
30
- output: t.object({ id: t.uuid, title: t.string }),
40
+ output: t.object({ id: t.uuid${lookup ? ', title: t.string' : ''} }),
31
41
  policy: can${feature.pascal}Write,
32
42
  cache: { invalidates: [${feature.camel}Tag] },
33
43
  mcp: { expose: true, description: '${name.raw} — edit this description' },
34
44
  async handle({ input }) {
35
- const row = await repo.byId(input.id);
45
+ ${
46
+ lookup
47
+ ? ` const row = await repo.byId(input.id);
36
48
  if (row === undefined) throw new ${feature.pascal}NotFoundError({ id: input.id });
37
- return { id: row.id, title: row.title };
49
+ return { id: row.id, title: row.title };`
50
+ : `${missingLookup(feature)}
51
+ return { id: input.id };`
52
+ }
38
53
  },
39
54
  });
40
55
  `;
@@ -42,14 +57,13 @@ export const ${name.camel} = action({
42
57
  const mutatorSource = (
43
58
  name: NameSet,
44
59
  feature: NameSet,
60
+ lookup: boolean,
45
61
  ): string => `// ${name.camel}: an action with an optimistic local twin. The local half runs against the client
46
62
  // store immediately; the server half is authoritative and reconciles on conflict.
47
63
 
48
64
  import { mutator, t } from '@ultimat3/action';
49
- import { ${feature.pascal}NotFoundError } from '../errors';
50
- import { can${feature.pascal}Write } from '../policy';
51
- import * as repo from '../repo';
52
-
65
+ ${lookup ? `import { ${feature.pascal}NotFoundError } from '../errors';\n` : ''}import { can${feature.pascal}Write } from '../policy';
66
+ ${lookup ? "import * as repo from '../repo';\n" : ''}
53
67
  interface Local${feature.pascal} {
54
68
  readonly id: string;
55
69
  readonly title: string;
@@ -71,9 +85,14 @@ export const ${name.camel} = mutator({
71
85
  });
72
86
  },
73
87
  async server(_ctx, input) {
74
- const row = await repo.byId(input.id);
88
+ ${
89
+ lookup
90
+ ? ` const row = await repo.byId(input.id);
75
91
  if (row === undefined) throw new ${feature.pascal}NotFoundError({ id: input.id });
76
- return { id: row.id, title: input.title };
92
+ return { id: row.id, title: input.title };`
93
+ : `${missingLookup(feature)}
94
+ return { id: input.id, title: input.title };`
95
+ }
77
96
  },
78
97
  conflict: 'server-wins',
79
98
  });
@@ -181,6 +200,13 @@ contractTest('${name.camel} projects one tool and one operation', () => {
181
200
 
182
201
  export interface ActionOptions extends FeatureTarget {
183
202
  readonly mutator?: boolean;
203
+ /**
204
+ * The slice's `errors.ts` as it stands on disk, or absent when the slice has none yet. Absent,
205
+ * the foundation writes one declaring `<Feature>NotFoundError` and the action may throw it;
206
+ * present, the file is the author's and is never rewritten, so the action throws it only when
207
+ * `sliceExports` finds it there.
208
+ */
209
+ readonly sliceErrors?: string;
184
210
  }
185
211
 
186
212
  export function actionFiles(rawName: string, target: ActionOptions): readonly GeneratedFile[] {
@@ -188,6 +214,9 @@ export function actionFiles(rawName: string, target: ActionOptions): readonly Ge
188
214
  const feature = names(target.feature);
189
215
  const dir = `${target.surfaceDir}/${target.feature}/actions`;
190
216
  const isMutator = target.mutator === true;
217
+ const lookup =
218
+ target.sliceErrors === undefined ||
219
+ sliceExports(target.sliceErrors, `${feature.pascal}NotFoundError`);
191
220
  return [
192
221
  // The three slice modules this action's source imports — `../errors`, `../policy`, `../repo`
193
222
  // (which comes with `../entity`, its row type). Composed rather than assumed: `x g action`
@@ -195,7 +224,9 @@ export function actionFiles(rawName: string, target: ActionOptions): readonly Ge
195
224
  ...sliceFoundation(target, ['entity', 'policy', 'errors']),
196
225
  {
197
226
  path: `${dir}/${name.kebab}.ts`,
198
- contents: isMutator ? mutatorSource(name, feature) : actionSource(name, feature),
227
+ contents: isMutator
228
+ ? mutatorSource(name, feature, lookup)
229
+ : actionSource(name, feature, lookup),
199
230
  },
200
231
  // TWO test files, because the gate types a test by its FILENAME and this declaration owes two
201
232
  // suites: the input parse is a `unit` assertion and the three projections are `contract` ones.
@@ -9,6 +9,7 @@ import { sortedImports } from './imports';
9
9
  import { catalogPath, resolveLocales } from './locales';
10
10
  import type { GeneratedFile } from './naming';
11
11
  import { camel, kebab, pascal } from './naming';
12
+ import { LINE_WIDTH } from './wrap';
12
13
 
13
14
  /** Where an admin lives when the caller does not say. `x new` scaffolds this layout. */
14
15
  export const DEFAULT_ADMIN_PAGE_DIR = 'apps/admin/src/pages';
@@ -58,9 +59,45 @@ const catalogImport = (module: string | undefined): string =>
58
59
  const pageImports = (module: string | undefined): string =>
59
60
  sortedImports([
60
61
  `import type { AdminCustomPage, AdminPageProps } from '@ultimat3/admin';`,
62
+ `import { definePermissions } from '@ultimat3/policy';`,
61
63
  catalogImport(module),
62
64
  ]);
63
65
 
66
+ /**
67
+ * The page DECLARES the permission it requires, in both registries that decide about it.
68
+ *
69
+ * `x g admin:page ops` emitted `permissions: ['ops:read']` and nothing anywhere declared
70
+ * `ops:read`, so `assertPermission` threw X_PERMISSION_UNKNOWN on the first request that reached
71
+ * the page: a screen the generator built and no actor can open. Nothing catches it either — the
72
+ * gate's `policy` step reads `roleDefinitions()` and `routeEntries()`, and an admin page is
73
+ * neither a role nor a route, so `x verify` is green over it.
74
+ *
75
+ * Here rather than in a `policy.ts` beside it, because registration is a side effect of IMPORT and
76
+ * `pages:` already imports this module: a declaration in a file the admin does not import is a
77
+ * declaration that never runs. `declare module` is the type half — `can()`'s key type reads the
78
+ * registry, not the `definePermissions` call — and merging an identical member is what lets an app
79
+ * that already declares this permission keep its own declaration.
80
+ */
81
+ const declarePermissions = (name: string, permission: string): string => {
82
+ const line = `export const ${camel(name)}PagePermissions = definePermissions(['${permission}']);`;
83
+ // Emitted PRE-formatted, because a template cannot run one: past 100 columns biome rewrites this
84
+ // call across four lines, and `x new zebra`-class names reach it (`emitted-contract.test.ts`
85
+ // varies both the length and the first letter for exactly this reason).
86
+ return line.length <= LINE_WIDTH
87
+ ? line
88
+ : `export const ${camel(name)}PagePermissions = definePermissions([\n '${permission}',\n]);`;
89
+ };
90
+
91
+ const permissionDeclaration = (name: string, permission: string): string => `
92
+ declare module '@ultimat3/policy' {
93
+ interface PermissionRegistry {
94
+ '${permission}': true;
95
+ }
96
+ }
97
+
98
+ ${declarePermissions(name, permission)}
99
+ `;
100
+
64
101
  /** `useT()` is per render, so the component binds it in its own body. */
65
102
  const translatorBinding = (module: string | undefined): string =>
66
103
  module === undefined ? '' : '\n const t = useT();\n';
@@ -81,9 +118,12 @@ const pageSource = (
81
118
  // so the specifier is relative to wherever \`defineAdmin\` lives:
82
119
  // import { ${declaration}Page } from './${name}';
83
120
  // defineAdmin({ …, pages: […, ${declaration}Page] })
121
+ //
122
+ // The permission below is declared here AND has to be decided: an authz map built from a fixed
123
+ // list must name '${permission}' too, or a declared permission is still denied for everyone.
84
124
 
85
125
  ${pageImports(module)}
86
-
126
+ ${permissionDeclaration(name, permission)}
87
127
  export function ${Name}Page(props: AdminPageProps) {${translatorBinding(module)}
88
128
  return (
89
129
  <section>
@@ -108,6 +148,7 @@ const pageTest = (name: string, permission: string): string => {
108
148
  const declaration = camel(name);
109
149
  return `// The ${name} admin page is guarded and owns no route of its own — the two facts that separate an
110
150
  // admin screen from a page, and the two an edit here is most likely to break.
151
+ import { knownPermissions } from '@ultimat3/policy';
111
152
  import { expect, unitTest } from '@ultimat3/testing';
112
153
  import { ${declaration}Page } from './${name}';
113
154
 
@@ -119,6 +160,13 @@ unitTest('the ${name} admin page is rooted and guarded', () => {
119
160
  expect(${declaration}Page.permissions).toContain('${permission}');
120
161
  });
121
162
 
163
+ // A \`permissions:\` entry no \`definePermissions()\` declares is X_PERMISSION_UNKNOWN on the first
164
+ // request that reaches the page — a screen this generator built and nobody can open. Importing the
165
+ // page above is what registers it, which is why the declaration lives in that file and not beside it.
166
+ unitTest('the ${name} admin page declares its permission', () => {
167
+ expect(knownPermissions()).toContain('${permission}');
168
+ });
169
+
122
170
  unitTest('the ${name} admin page declares no route of its own', () => {
123
171
  // \`pages:\` is the only way in. A \`config\` export here would be a route the frame never guards.
124
172
  expect('config' in ${declaration}Page).toBe(false);
@@ -71,9 +71,11 @@ function ${Name}(props: ${Name}Props): JSX.Element {
71
71
  * container already has children, so without it the server's markup stays on screen above a
72
72
  * second, live copy of the same thing.
73
73
  */
74
- export function mount(el: HTMLElement, props: ${Name}Props): void {
74
+ export function mount(el: HTMLElement, props: ${Name}Props): () => void {
75
75
  el.textContent = '';
76
- render(() => <${Name} {...props} />, el);
76
+ // Solid's \`render\` answers its disposer; returning it is what lets \`mountIsland\` in
77
+ // \`@ultimat3/testing\` stop this island — its timers included — when a test is done with it.
78
+ return render(() => <${Name} {...props} />, el);
77
79
  }
78
80
  `;
79
81
  };
@@ -6,6 +6,7 @@
6
6
  // run-to-completion migrate step are conventions every container platform shares — a Heroku
7
7
  // buildpack, a Render blueprint or a fly.toml would be the primitive that never ships.
8
8
 
9
+ import { SECRETS_KEY_FILE } from '@ultimat3/core';
9
10
  import type { GeneratedFile, NameSet } from './naming';
10
11
  import { helmFiles } from './scaffold-helm';
11
12
 
@@ -81,6 +82,12 @@ ENTRYPOINT ["bun", "apps/web/server.ts"]
81
82
  * tells the operator to create, in its own `env_file:` — nor `.env.development`, and both landed in
82
83
  * an image layer that `cache-to=mode=max` then pushes to a shared cache. Same four lines the
83
84
  * framework's own `docker/Dockerfile.dockerignore` carries, and for the same reason.
85
+ *
86
+ * The key file is interpolated from `SECRETS_KEY_FILE` rather than written out, because the
87
+ * constant is what `findMasterKey` reads: a rename would otherwise leave every app this generator
88
+ * has ever produced ignoring a filename nothing writes, which reads as a rule still in force.
89
+ * `scripts/image-contract.ts` holds every ignore file IN THIS TREE to the same line; a generated
90
+ * app's copy is this template's, and `scaffold-container.test.ts` is where it is judged.
84
91
  */
85
92
  const dockerignore = (): string => `**/.env
86
93
  **/.env.*
@@ -88,6 +95,11 @@ const dockerignore = (): string => `**/.env
88
95
  # Same shape, different file: an .npmrc carries a registry auth token, so any that exists in a
89
96
  # build context is somebody's local credential and has no business in a layer.
90
97
  **/.npmrc
98
+ # Same shape, and this one is the key itself: ${SECRETS_KEY_FILE} decrypts the committed
99
+ # secrets.enc.json beside it, and findMasterKey falls back to the file whenever
100
+ # ULTIMATE_SECRETS_KEY is unset — so a baked copy boots the image on it and the platform's key is
101
+ # never exercised.
102
+ **/${SECRETS_KEY_FILE}
91
103
 
92
104
  node_modules
93
105
  **/node_modules
@@ -4,6 +4,7 @@
4
4
  // ignore file, the production topology and the deploy page are `scaffold-container.ts`; the
5
5
  // `.claude/` harness that reads AGENTS.md is `scaffold-claude.ts`.
6
6
 
7
+ import { LINE_CEILING } from '../workspace-checks';
7
8
  import type { GeneratedFile, NameSet } from './naming';
8
9
  import { claudeFiles } from './scaffold-claude';
9
10
  import { containerFiles } from './scaffold-container';
@@ -27,10 +28,16 @@ not exist — five of these had an empty column and were each measured green on
27
28
  | Time | store UTC, format with an explicit IANA time zone | \`guards/unzoned-date.ts\` |
28
29
  | Strings | every user-facing string goes through \`t()\` | \`guards/untranslated-string.ts\` |
29
30
  | Colour | semantic tokens only, never a raw hex | \`guards/raw-colour.ts\` |
31
+ | Size | one file, one job — ${LINE_CEILING} lines of reviewable logic, and split past it | \`X_FILE_TOO_LONG\` |
30
32
 
31
33
  \`guards/\` is yours: each file is one rule, discovered by \`x verify\` and run inside its
32
34
  \`boundaries\` step. Delete one to drop the rule, and \`x g guard <name>\` writes the next.
33
35
 
36
+ Size is a hard line and not a style note: past ${LINE_CEILING} lines a file has stopped being the
37
+ unit of review, and \`x verify\` refuses it. The one exemption is a file that is nothing but re-exports —
38
+ it has one job by construction, and its length tracks the API's size rather than its complexity;
39
+ one statement of logic in such a file re-arms the ceiling on the same save.
40
+
34
41
  Money is the one row with no guard, deliberately: a float has no static signature a text rule can
35
42
  see, and the type already fires — measured, \`price: 19.99\` in a seed is
36
43
  \`TS2322: Type 'number' is not assignable to type 'MoneyInput'\`. A guard that pretended to check
@@ -9,8 +9,10 @@ import type { GeneratedFile } from './naming';
9
9
  const server =
10
10
  (): string => `// The production entry. \`docker/Dockerfile\` starts this, and \`x build --target binary\` compiles it.
11
11
  // ROLE selects what this process is — web, sync, worker, scheduler, replicator, or migrate, which
12
- // applies the migrations and exits. PORT is bound on every interface, because a container bound to
13
- // localhost is unreachable through its own port mapping.
12
+ // applies the migrations and exits. PORT is bound on HOST, and HOST defaults to every interface,
13
+ // because a container bound to loopback is unreachable through its own port mapping. HOST=127.0.0.1
14
+ // is for a process that must never answer a public interface: reachable only where the container
15
+ // shares the host's network namespace (\`--network host\`), or through a sidecar and \`ssh -L\`.
14
16
 
15
17
  import { join } from 'node:path';
16
18
  import { runRole } from '@ultimat3/cli';
@@ -333,13 +333,18 @@ declare module '*.scss' {
333
333
  }
334
334
  `;
335
335
 
336
+ // Build output is ROOT-ANCHORED, and that is not cosmetic: an unanchored `dist/` or `coverage/`
337
+ // matches a directory of that name at ANY depth, so `apps/web/site/dist/page.tsx` — an app's own
338
+ // `/dist` route, the directory IS the URL — was a file git refused to commit and `x dev` refused to
339
+ // reload. `packages/*/dist/` keeps a workspace's build output ignored without reaching a surface.
336
340
  const gitignore = (): string => `node_modules/
337
341
  .x/
338
- dist/
342
+ /dist/
343
+ packages/*/dist/
339
344
  *.tsbuildinfo
340
345
  .env
341
346
  .env.*.local
342
- coverage/
347
+ /coverage/
343
348
  playwright-report/
344
349
  test-results/
345
350
  `;
@@ -3,6 +3,7 @@
3
3
  // `policyFiles`; the five generators that write *into* a slice imported the same files and wrote
4
4
  // none of them, so each emitted TS2307 in any slice a resource had not been run in first.
5
5
 
6
+ import { stripComments } from '../ts-scan';
6
7
  import type { FeatureTarget } from './entity';
7
8
  import { entityFiles } from './entity';
8
9
  import type { GeneratedFile, NameSet } from './naming';
@@ -84,3 +85,38 @@ export function sliceFoundation(
84
85
  : []),
85
86
  ]);
86
87
  }
88
+
89
+ /** The exported names an `export { a, b as c }` list declares — `c`, never `b`. */
90
+ const listedExports = (code: string): readonly string[] =>
91
+ [...code.matchAll(/\bexport\s*\{([^}]*)\}/g)].flatMap((match) =>
92
+ (match[1] ?? '')
93
+ .split(',')
94
+ .map(
95
+ (entry) =>
96
+ entry
97
+ .trim()
98
+ .split(/\s+as\s+/)
99
+ .at(-1)
100
+ ?.trim() ?? '',
101
+ )
102
+ .filter((entry) => entry !== ''),
103
+ );
104
+
105
+ /**
106
+ * Whether `source` — a slice module as it stands on the app's disk — exports the VALUE `name`.
107
+ * Read at generation time because assuming it was measured: `x g action` into ai-maxxing's
108
+ * `fleet` slice wrote `import { FleetNotFoundError } from '../errors'` into a slice whose
109
+ * `errors.ts` declared `HostNotFoundError` and `SessionNotFoundError` and no `FleetNotFoundError`.
110
+ * The file failed at import — and because `x db gen` and `x manifest` load every module, one
111
+ * generated-and-not-yet-edited action made both refuse to run.
112
+ *
113
+ * Comments are masked first: `// TODO: add FleetNotFoundError` is not an export. A `type` or an
114
+ * `interface` of that name is not one either — the generated code constructs it.
115
+ */
116
+ export function sliceExports(source: string, name: string): boolean {
117
+ const code = stripComments(source);
118
+ const declared = new RegExp(
119
+ `\\bexport\\s+(?:abstract\\s+)?(?:class|const|let|var|function|enum)\\s+${name}\\b`,
120
+ );
121
+ return declared.test(code) || listedExports(code).includes(name);
122
+ }
@@ -0,0 +1,79 @@
1
+ // Which of a selection's files may share a worker pool, and which may not. `--parallel=N` is the
2
+ // width of the WHOLE run, so a selection holding a `live` or an `e2e` file is more than one run —
3
+ // this file decides how many, and `test-shards.ts` spends them.
4
+
5
+ import type { TestFile } from './test-select';
6
+ import type { TestType } from './verify-tests';
7
+ import { ownerOf, SERIAL_TYPES } from './verify-tests';
8
+
9
+ /** One `bun test` invocation: which files, how wide, and the type its reproduce line names. */
10
+ export interface TestPass {
11
+ readonly files: readonly TestFile[];
12
+ readonly workers: number;
13
+ /**
14
+ * The type this pass is exactly the selection of, when it is one — so a failure's `fix:` names
15
+ * `x test live --workers 1` and reruns THESE files, not the whole corpus at this width.
16
+ */
17
+ readonly type?: TestType;
18
+ }
19
+
20
+ export interface PassInput {
21
+ readonly files: readonly TestFile[];
22
+ /** What the caller asked for, already bounded by `--workers`' own reader. */
23
+ readonly workers: number;
24
+ /** The positional, when there was one. A pass over one type keeps it; the split never adds one. */
25
+ readonly type?: TestType;
26
+ /** Set for a `--worker I` rerun, which is one process by construction. */
27
+ readonly shard?: number;
28
+ }
29
+
30
+ const widthFor = (files: readonly TestFile[], workers: number): number =>
31
+ Math.max(1, Math.min(Math.trunc(workers), files.length || 1));
32
+
33
+ /**
34
+ * The passes one invocation becomes. One, for every selection that holds no serial file — which
35
+ * is every `x test unit`, every `--filter` over a feature's contract tests, and the whole corpus
36
+ * of an app that has neither.
37
+ *
38
+ * `live` and `e2e` are the exception, and `verify-tests.ts` owns both the list and the reasons: a
39
+ * logical replication slot is named at the Postgres CLUSTER level so a per-worker database does
40
+ * not isolate it, and `e2e` shares one `dist/` and one browser profile. `x verify` has routed them
41
+ * through `runSerial` since 2026-08 while `x test` clamped on the POSITIONAL alone — so a bare
42
+ * `x test --workers 8` ran the very files the gate runs one at a time, eight at a time, and only
43
+ * a real `TEST_DATABASE_URL` makes that visible.
44
+ *
45
+ * Each serial type is its own pass rather than one pass over both, because the pass's `type` is
46
+ * what its failure reproduces with: `x test live --workers 1` selects exactly the files that ran.
47
+ *
48
+ * A `--worker I` rerun is left whole: it is a single `bun test --isolate --shard=i/N` process, so
49
+ * nothing inside it runs beside anything else, and splitting it would make shard i of the rerun a
50
+ * different set of files from shard i of the run it reproduces.
51
+ */
52
+ export function testPasses(input: PassInput): readonly TestPass[] {
53
+ const serialTypes = new Set<TestType>(SERIAL_TYPES);
54
+ if (input.shard !== undefined) {
55
+ return [
56
+ {
57
+ files: input.files,
58
+ workers: widthFor(input.files, input.workers),
59
+ ...(input.type === undefined ? {} : { type: input.type }),
60
+ },
61
+ ];
62
+ }
63
+ const shared = input.files.filter((file) => !serialTypes.has(ownerOf(file.path)));
64
+ const passes: TestPass[] = [];
65
+ // Cheapest first, and the widest first: the pool run is most of the corpus and most of the
66
+ // signal, and a serial suite that needs a database is the one a laptop is least likely to have.
67
+ if (shared.length > 0) {
68
+ passes.push({
69
+ files: shared,
70
+ workers: widthFor(shared, input.workers),
71
+ ...(input.type === undefined ? {} : { type: input.type }),
72
+ });
73
+ }
74
+ for (const type of SERIAL_TYPES) {
75
+ const files = input.files.filter((file) => ownerOf(file.path) === type);
76
+ if (files.length > 0) passes.push({ files, workers: 1, type });
77
+ }
78
+ return passes;
79
+ }