@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.
Files changed (267) hide show
  1. package/CLAUDE.md +36 -13
  2. package/README.md +31 -8
  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/affected.ts +27 -35
  7. package/src/api-registration.ts +125 -11
  8. package/src/app-artifacts.ts +1 -1
  9. package/src/app-auth.ts +4 -27
  10. package/src/app-boundaries.ts +6 -20
  11. package/src/app-config-load.ts +51 -0
  12. package/src/app-env.ts +5 -11
  13. package/src/app-load.ts +55 -11
  14. package/src/app-manifest.ts +16 -8
  15. package/src/app-mcp.ts +10 -23
  16. package/src/app-permissions-borrowed.ts +129 -0
  17. package/src/app-permissions-site.ts +24 -0
  18. package/src/app-permissions.ts +0 -0
  19. package/src/app-reload-graph.ts +7 -30
  20. package/src/app-transport.ts +219 -0
  21. package/src/boundary-findings.ts +16 -4
  22. package/src/browser-transport.ts +160 -0
  23. package/src/budgets.ts +27 -34
  24. package/src/cdp-shot-driver.ts +3 -1
  25. package/src/cmd-auth-spec.ts +16 -0
  26. package/src/cmd-auth.ts +65 -0
  27. package/src/cmd-build-spec.ts +10 -3
  28. package/src/cmd-build.ts +107 -11
  29. package/src/cmd-db-backfill.ts +25 -12
  30. package/src/cmd-db.ts +66 -16
  31. package/src/cmd-deploy-helm.ts +7 -6
  32. package/src/cmd-deploy.ts +4 -4
  33. package/src/cmd-dev.ts +9 -8
  34. package/src/cmd-doctor-spec.ts +2 -9
  35. package/src/cmd-doctor.ts +75 -24
  36. package/src/cmd-env.ts +26 -2
  37. package/src/cmd-generate.ts +195 -47
  38. package/src/cmd-help.ts +19 -6
  39. package/src/cmd-i18n.ts +10 -9
  40. package/src/cmd-jobs-spec.ts +19 -3
  41. package/src/cmd-jobs.ts +136 -7
  42. package/src/cmd-manifest-spec.ts +1 -1
  43. package/src/cmd-manifest.ts +6 -25
  44. package/src/cmd-mcp.ts +46 -15
  45. package/src/cmd-new.ts +15 -1
  46. package/src/cmd-planned.ts +15 -8
  47. package/src/cmd-routes.ts +27 -3
  48. package/src/cmd-secrets-ring.ts +76 -0
  49. package/src/cmd-secrets-spec.ts +10 -2
  50. package/src/cmd-secrets.ts +95 -18
  51. package/src/cmd-shot-matrix.ts +1 -3
  52. package/src/cmd-shot.ts +12 -3
  53. package/src/cmd-tasks.ts +84 -15
  54. package/src/cmd-test-spec.ts +1 -7
  55. package/src/cmd-test.ts +13 -29
  56. package/src/cmd-verify.ts +13 -5
  57. package/src/comment-safe.ts +16 -0
  58. package/src/coverage-floor.ts +334 -0
  59. package/src/coverage-lcov.ts +193 -0
  60. package/src/coverage-source.ts +136 -0
  61. package/src/db-object-drift.ts +30 -0
  62. package/src/db-replay-engine.ts +115 -0
  63. package/src/db-schema-dump.ts +150 -0
  64. package/src/db-schema-refresh.ts +122 -0
  65. package/src/db-subscribes.ts +12 -4
  66. package/src/dev-dashboard-guard.ts +76 -0
  67. package/src/dev-dashboard.ts +51 -29
  68. package/src/dev-lock.ts +4 -7
  69. package/src/dev-route-table.ts +9 -0
  70. package/src/dispatch.ts +3 -6
  71. package/src/doctor-app-url.ts +39 -0
  72. package/src/doctor-auth.ts +95 -0
  73. package/src/doctor-coverage.ts +39 -0
  74. package/src/doctor-guards.ts +54 -0
  75. package/src/doctor-sealed.ts +132 -0
  76. package/src/document-graph.ts +82 -0
  77. package/src/drift-replica-identity.ts +42 -0
  78. package/src/error-catalog.ts +6 -9
  79. package/src/error-codes.ts +55 -3
  80. package/src/error-unthrown.ts +29 -11
  81. package/src/errors.ts +54 -83
  82. package/src/framework-schema-apply.ts +78 -0
  83. package/src/framework-schema-stamp.ts +96 -0
  84. package/src/framework-schema.ts +28 -2
  85. package/src/generate-files.ts +79 -12
  86. package/src/generate-grant-findings.ts +76 -0
  87. package/src/generate-grants.ts +77 -3
  88. package/src/generate-kinds.ts +52 -1
  89. package/src/generate-shadow.ts +46 -0
  90. package/src/generate-write.ts +62 -40
  91. package/src/generated-imports.ts +59 -0
  92. package/src/gh-target.ts +12 -7
  93. package/src/guard-sources.ts +104 -0
  94. package/src/guards.ts +23 -7
  95. package/src/handle-registration.ts +217 -0
  96. package/src/i18n-audit.ts +4 -1
  97. package/src/i18n-registration.ts +36 -0
  98. package/src/image-prepare.ts +51 -8
  99. package/src/import-closure.ts +165 -0
  100. package/src/index.ts +12 -5
  101. package/src/island-bundle.ts +3 -15
  102. package/src/island-store.ts +24 -16
  103. package/src/jobs-drain.ts +95 -21
  104. package/src/jobs-json.ts +50 -0
  105. package/src/jobs-report.ts +7 -4
  106. package/src/json-merge.ts +46 -0
  107. package/src/loopback-host.ts +13 -0
  108. package/src/manifest-staleness.ts +62 -0
  109. package/src/mcp-errors.ts +41 -4
  110. package/src/mcp-host.ts +16 -9
  111. package/src/mcp-test-run.ts +38 -0
  112. package/src/messages.ts +35 -0
  113. package/src/metrics-endpoint.ts +95 -24
  114. package/src/migration-extensions.ts +29 -0
  115. package/src/module-imports.ts +47 -0
  116. package/src/output.ts +24 -9
  117. package/src/page-navigation.ts +7 -23
  118. package/src/page-speculation.ts +5 -20
  119. package/src/permission-grants.ts +41 -9
  120. package/src/prerender-out.ts +34 -7
  121. package/src/prerender.ts +8 -5
  122. package/src/publish-closure.ts +101 -0
  123. package/src/pwa-artifacts.ts +26 -94
  124. package/src/registry.ts +2 -0
  125. package/src/revalidated-response.ts +4 -3
  126. package/src/role-load.ts +143 -0
  127. package/src/role-replicator.ts +16 -0
  128. package/src/role-start.ts +27 -5
  129. package/src/role-sync.ts +55 -20
  130. package/src/role-wake.ts +45 -0
  131. package/src/runtime-admin.ts +141 -0
  132. package/src/runtime-assets.ts +11 -7
  133. package/src/runtime-bindings.ts +4 -4
  134. package/src/runtime-cache.ts +38 -47
  135. package/src/runtime-hooks.ts +5 -2
  136. package/src/runtime-idempotency-scope.ts +23 -0
  137. package/src/runtime-isr.ts +36 -0
  138. package/src/runtime-jobs.ts +8 -33
  139. package/src/runtime-mfa-warning.ts +29 -0
  140. package/src/runtime-notify-retention.ts +10 -46
  141. package/src/runtime-overrides.ts +1 -1
  142. package/src/runtime-purge.ts +38 -16
  143. package/src/runtime-queue.ts +39 -11
  144. package/src/runtime-realtime.ts +9 -45
  145. package/src/runtime-render.ts +68 -35
  146. package/src/runtime-services.ts +22 -18
  147. package/src/runtime-storage.ts +10 -5
  148. package/src/scaffold-typecheck.ts +1 -0
  149. package/src/schema-diff.ts +16 -0
  150. package/src/schema-drift.ts +45 -15
  151. package/src/schema-dump-drift.ts +64 -0
  152. package/src/seo-routes.ts +80 -9
  153. package/src/serve-boot.ts +114 -158
  154. package/src/serve-drain.ts +9 -33
  155. package/src/serve-env.ts +9 -6
  156. package/src/serve-prebuilt-paths.ts +19 -0
  157. package/src/serve-prebuilt.ts +84 -0
  158. package/src/serve-web.ts +169 -0
  159. package/src/serve.ts +6 -9
  160. package/src/server-barrels.ts +54 -0
  161. package/src/shot-locale.ts +5 -23
  162. package/src/site-assets.ts +3 -5
  163. package/src/site-config.ts +9 -48
  164. package/src/site-seo.ts +1 -1
  165. package/src/sitemap-lastmod.ts +5 -0
  166. package/src/solid-loader.ts +12 -3
  167. package/src/source-edit.ts +49 -0
  168. package/src/source-files.ts +6 -0
  169. package/src/stored-object-headers.ts +43 -0
  170. package/src/sync-url.ts +2 -2
  171. package/src/tasks-facts.ts +3 -26
  172. package/src/templates/action.ts +147 -11
  173. package/src/templates/admin-catalog.ts +58 -0
  174. package/src/templates/admin-page.ts +36 -8
  175. package/src/templates/admin.ts +57 -17
  176. package/src/templates/backfill.ts +121 -87
  177. package/src/templates/entity.ts +140 -44
  178. package/src/templates/guard-animated-layout-property.ts +4 -11
  179. package/src/templates/guard-bare-error.ts +9 -13
  180. package/src/templates/guard-focus-visible.ts +4 -11
  181. package/src/templates/guard-image-dimensions.ts +6 -6
  182. package/src/templates/guard-island-without-states.ts +2 -4
  183. package/src/templates/guard-raw-breakpoint.ts +210 -0
  184. package/src/templates/guard-raw-colour.ts +4 -11
  185. package/src/templates/guard-raw-length.ts +375 -0
  186. package/src/templates/guard-raw-motion.ts +213 -0
  187. package/src/templates/guard-raw-shadow.ts +187 -0
  188. package/src/templates/guard-raw-z-index.ts +166 -0
  189. package/src/templates/guard-repo-raw-sql.ts +345 -0
  190. package/src/templates/guard-semantic-interactive.ts +6 -6
  191. package/src/templates/guard-undeclared-custom-property.ts +217 -0
  192. package/src/templates/guard-undefined-style-class.ts +215 -0
  193. package/src/templates/guard-untranslated-string.ts +6 -6
  194. package/src/templates/guard-unzoned-date.ts +9 -13
  195. package/src/templates/index.ts +3 -2
  196. package/src/templates/island.ts +34 -51
  197. package/src/templates/job-body-test.ts +121 -0
  198. package/src/templates/job.ts +23 -8
  199. package/src/templates/naming.ts +28 -0
  200. package/src/templates/policy.ts +16 -9
  201. package/src/templates/query.ts +242 -26
  202. package/src/templates/resource-create.ts +62 -18
  203. package/src/templates/resource-form-island.ts +58 -80
  204. package/src/templates/resource-page.ts +269 -0
  205. package/src/templates/resource-service.ts +96 -0
  206. package/src/templates/resource.ts +86 -61
  207. package/src/templates/route.ts +41 -35
  208. package/src/templates/scaffold-api.ts +55 -1
  209. package/src/templates/scaffold-app.ts +177 -53
  210. package/src/templates/scaffold-auth.ts +83 -19
  211. package/src/templates/scaffold-browser-client.ts +90 -0
  212. package/src/templates/scaffold-container-compose.ts +10 -1
  213. package/src/templates/scaffold-container.ts +20 -2
  214. package/src/templates/scaffold-dashboard-bare.ts +64 -2
  215. package/src/templates/scaffold-dashboard-example.ts +124 -31
  216. package/src/templates/scaffold-dashboard-shared.ts +1 -20
  217. package/src/templates/scaffold-dashboard.ts +1 -2
  218. package/src/templates/scaffold-db-client.ts +144 -0
  219. package/src/templates/scaffold-db-package.ts +90 -8
  220. package/src/templates/scaffold-docs.ts +50 -5
  221. package/src/templates/scaffold-guards.ts +54 -11
  222. package/src/templates/scaffold-helm-fences.ts +76 -0
  223. package/src/templates/scaffold-helm-templates.ts +32 -7
  224. package/src/templates/scaffold-helm.ts +31 -1
  225. package/src/templates/scaffold-http.ts +9 -7
  226. package/src/templates/scaffold-repo.ts +61 -3
  227. package/src/templates/scaffold-roles.ts +56 -9
  228. package/src/templates/scaffold-shell.ts +74 -105
  229. package/src/templates/scaffold-site.ts +45 -19
  230. package/src/templates/scaffold-ui-package.ts +18 -0
  231. package/src/templates/slice-foundation.ts +46 -8
  232. package/src/test-counts.ts +14 -2
  233. package/src/test-ignores.ts +25 -0
  234. package/src/test-passes.ts +0 -15
  235. package/src/test-select.ts +5 -14
  236. package/src/test-shards.ts +41 -93
  237. package/src/test-workers.ts +26 -4
  238. package/src/theme-boot.ts +6 -31
  239. package/src/transport-calls.ts +105 -0
  240. package/src/trusted-hops-error.ts +17 -0
  241. package/src/unscanned-admin.ts +54 -0
  242. package/src/unthrown-codes.ts +26 -0
  243. package/src/verify-checks.ts +43 -53
  244. package/src/verify-coverage-run.ts +190 -0
  245. package/src/verify-deadline.ts +353 -0
  246. package/src/verify-floor.ts +36 -9
  247. package/src/verify-merge.ts +171 -45
  248. package/src/verify-part-step.ts +90 -0
  249. package/src/verify-progress.ts +25 -0
  250. package/src/verify-role-load-probe.ts +54 -0
  251. package/src/verify-role-load.ts +148 -0
  252. package/src/verify-run.ts +116 -37
  253. package/src/verify-shard.ts +33 -11
  254. package/src/verify-stalled.ts +152 -0
  255. package/src/verify-step.ts +42 -1
  256. package/src/verify-tests.ts +83 -8
  257. package/src/workspace-checks.ts +50 -8
  258. package/src/workspace-dep-edit.ts +127 -0
  259. package/src/build-errors.ts +0 -54
  260. package/src/cmd-dev-fixture.ts +0 -204
  261. package/src/dev-live-fixture.ts +0 -122
  262. package/src/policy-fixture.ts +0 -78
  263. package/src/realtime-browser-probe-fixture.ts +0 -9
  264. package/src/role-start-fixture.ts +0 -78
  265. package/src/scaffold-fixture.ts +0 -160
  266. package/src/templates/island-fixture.ts +0 -76
  267. package/src/verify-errors.ts +0 -49
@@ -14,14 +14,19 @@ import type { TestType } from '@ultimat3/testing';
14
14
  import { TEST_TYPES } from '@ultimat3/testing';
15
15
  import { checkEvalBaselines, checkEvalCoverage, checkEvalRecording } from './app-evals';
16
16
  import { APP_CONFIG_FILE } from './app-root';
17
+ import { judgeCoverage } from './coverage-floor';
18
+ import { msg } from './messages';
17
19
  import { countsOf } from './test-counts';
18
20
  import { testEnvOverrides } from './test-dotenv';
21
+ import { IGNORED_TEST_PATTERNS } from './test-ignores';
19
22
  import type { TestFile } from './test-select';
20
23
  import { discoverTests } from './test-select';
21
24
  import { machineLease } from './test-slots';
22
25
  import type { WorkerPlan } from './test-workers';
23
26
  import { availableCpus, SERIAL_TYPES, totalMemory, workerPlan } from './test-workers';
27
+ import { runCovered } from './verify-coverage-run';
24
28
  import { withE2eApp } from './verify-e2e';
29
+ import { readVerifyFloor } from './verify-floor';
25
30
  import { shardFiles } from './verify-shard';
26
31
  import type { StepOutcome, VerifyContext, VerifyStep } from './verify-step';
27
32
  import { fromExec, fromFindings } from './verify-step';
@@ -120,7 +125,9 @@ const isSerial = (type: TestType): boolean => SERIAL_TYPES.includes(type);
120
125
  * Build output, and nested projects that carry their own `x verify`. `examples/**` and
121
126
  * `dummy/**` are the second kind: each holds a whole app gated by its own run of this same step
122
127
  * list, so collecting them here would report one app failure on two different gates. The patterns
123
- * are relative to the run's root, so this excludes nothing when the app itself is the root.
128
+ * are relative to the run's root, so this excludes nothing when the app itself is the root. The
129
+ * policy — which directories, and which are anchored at the root — is `test-ignores.ts`'s, the same
130
+ * one `discoverTests` filters the parallel steps by; this is its glob projection.
124
131
  *
125
132
  * `dummy/**` was added after the framework gate started running a demo app's tests and disagreeing
126
133
  * with `bun run test` — which already excluded it — for no reason a reader could see.
@@ -129,7 +136,7 @@ const isSerial = (type: TestType): boolean => SERIAL_TYPES.includes(type);
129
136
  * those apps and blocks on a per-app ratchet. Until 2026-08 that was only true of `examples/**`,
130
137
  * and `dummy/social-media-clone` was excluded by this list and gated by nothing.
131
138
  */
132
- const NEVER_A_TEST = ['**/dist/**', '**/build/**', '**/examples/**', '**/dummy/**'];
139
+ const NEVER_A_TEST = IGNORED_TEST_PATTERNS;
133
140
 
134
141
  const ignoreFlags = (patterns: readonly string[]): readonly string[] =>
135
142
  patterns.map((pattern) => `--path-ignore-patterns=${pattern}`);
@@ -240,6 +247,21 @@ const runType = async (ctx: VerifyContext, type: TestType): Promise<StepOutcome>
240
247
  ctx.workers === undefined ? workerPlan(availableCpus(), totalMemory(), env) : undefined;
241
248
  const workers = ctx.workers ?? (plan as WorkerPlan).workers;
242
249
  const lease = plan === undefined ? undefined : machineLease(plan.workers, env);
250
+ const widthReason =
251
+ plan === undefined
252
+ ? `${String(Math.min(workers, files.length))} workers (--workers)`
253
+ : files.length < plan.workers
254
+ ? `${String(files.length)} of ${plan.reason}`
255
+ : plan.reason;
256
+ if (type === 'unit' && isApp(ctx.root)) {
257
+ return coveredUnit(ctx, {
258
+ files: files.map((file) => file.path),
259
+ workers,
260
+ widthReason,
261
+ shard,
262
+ lease,
263
+ });
264
+ }
243
265
  const outcome = await runParallel({
244
266
  root: ctx.root,
245
267
  runner: ctx.runner,
@@ -247,18 +269,71 @@ const runType = async (ctx: VerifyContext, type: TestType): Promise<StepOutcome>
247
269
  workers,
248
270
  type,
249
271
  isolate: ctx.isolate === true,
250
- widthReason:
251
- plan === undefined
252
- ? `${String(Math.min(workers, files.length))} workers (--workers)`
253
- : files.length < plan.workers
254
- ? `${String(files.length)} of ${plan.reason}`
255
- : plan.reason,
272
+ widthReason,
256
273
  ...(lease === undefined ? {} : { lease }),
257
274
  ...(ctx.env === undefined ? {} : { env: ctx.env }),
258
275
  });
259
276
  return shard === undefined ? outcome : { ...outcome, shard };
260
277
  };
261
278
 
279
+ /**
280
+ * An APP's unit step: the suite, and what it covered of the app's own source held to the floor in
281
+ * `x.verify.json` (`coverage-floor.ts`). Only the unit suite counts — it is the one every run
282
+ * executes, and an opt-in suite cannot hold a floor a default run must meet. Only an app: the
283
+ * framework's packages are held to the same bar one process each, by `scripts/coverage-gate.ts`,
284
+ * because one run over a monorepo reads every package diluted by the ones it imports.
285
+ *
286
+ * A red suite is not judged — a slice that died wrote no lcov, and a coverage finding beside
287
+ * `X_TEST_FAILED` would send the reader to the wrong fix. A `--shard` slice is not judged either:
288
+ * it hands its facts to `x verify merge`, which folds every shard and judges once.
289
+ */
290
+ const coveredUnit = async (
291
+ ctx: VerifyContext,
292
+ run: {
293
+ readonly files: readonly string[];
294
+ readonly workers: number;
295
+ readonly widthReason: string;
296
+ readonly shard: ReturnType<typeof shardFiles> | undefined;
297
+ readonly lease: ReturnType<typeof machineLease> | undefined;
298
+ },
299
+ ): Promise<StepOutcome> => {
300
+ const { coverage, ...outcome } = await runCovered({
301
+ root: ctx.root,
302
+ runner: ctx.runner,
303
+ files: run.files,
304
+ workers: run.workers,
305
+ isolate: ctx.isolate === true,
306
+ widthReason: run.widthReason,
307
+ ...(run.lease === undefined ? {} : { lease: run.lease }),
308
+ ...(ctx.env === undefined ? {} : { env: ctx.env }),
309
+ });
310
+ if (!outcome.ok) return run.shard === undefined ? outcome : { ...outcome, shard: run.shard };
311
+ if (run.shard !== undefined) {
312
+ return {
313
+ ...outcome,
314
+ shard: run.shard,
315
+ coverage,
316
+ output: msg('cli.verify.coverageDeferred'),
317
+ };
318
+ }
319
+ const verdict = await judgeCoverage(
320
+ ctx.root,
321
+ coverage,
322
+ (await readVerifyFloor(ctx.root))?.coverage,
323
+ );
324
+ return {
325
+ ...outcome,
326
+ ok: verdict.findings.length === 0,
327
+ findings: verdict.findings,
328
+ output: verdict.output,
329
+ measured: {
330
+ lines: verdict.measure.lines,
331
+ funcs: verdict.measure.funcs,
332
+ files: verdict.measure.files,
333
+ },
334
+ };
335
+ };
336
+
262
337
  const isApp = (root: string): boolean => existsSync(join(root, APP_CONFIG_FILE));
263
338
 
264
339
  /**
@@ -10,10 +10,17 @@
10
10
  // public subject. `isReExportManifest` is the whole carve-out — one statement of logic in such a
11
11
  // file re-arms the ceiling on the same save, which is what keeps it from being a hole.
12
12
 
13
- import { existsSync } from 'node:fs';
13
+ import { existsSync, readFileSync } from 'node:fs';
14
14
  import { join } from 'node:path';
15
15
  import { ERROR_DOCS_URL, renderCauseValue } from '@ultimat3/core';
16
16
  import type { Finding } from './output';
17
+ import {
18
+ entryFilesOf,
19
+ FIXTURE_SUFFIX,
20
+ fixtureReachFinding,
21
+ fixturesReachedFrom,
22
+ mayReachFixture,
23
+ } from './publish-closure';
17
24
  import { isReExportManifest } from './reexport-manifest';
18
25
  import { eachSourceFile, isGenerated } from './source-files';
19
26
  import { checkRootReferences } from './tsconfig-references';
@@ -177,6 +184,16 @@ export function checkLockstep(
177
184
  */
178
185
  export const TEST_EXCLUSION = '!src/**/*.test.ts';
179
186
 
187
+ /**
188
+ * The same rule for the other suffix test code is written under. A `-fixture.ts` is a harness a
189
+ * test imports — a fake driver, a recorded page, a scaffolded tree — and `src` sweeps it into the
190
+ * tarball exactly as it sweeps a `*.test.ts`: six packages shipped 18 of them, 2026-10.
191
+ */
192
+ export const FIXTURE_EXCLUSION = '!src/**/*-fixture.ts';
193
+
194
+ /** Every exclusion a published package's `files` carries, in the order it is written. */
195
+ export const TEST_CODE_EXCLUSIONS: readonly string[] = [TEST_EXCLUSION, FIXTURE_EXCLUSION];
196
+
180
197
  export const missingPublishedFileFinding = (dir: string, entry: string): Finding => ({
181
198
  code: 'X_PACKAGE_SHAPE',
182
199
  cause: `packages/${dir}/package.json ships "${entry}" in "files", but packages/${dir}/${entry} does not exist`,
@@ -185,10 +202,10 @@ export const missingPublishedFileFinding = (dir: string, entry: string): Finding
185
202
  at: `packages/${dir}/package.json`,
186
203
  });
187
204
 
188
- export const publishesTestsFinding = (dir: string): Finding => ({
205
+ export const publishesTestsFinding = (dir: string, exclusion: string): Finding => ({
189
206
  code: 'X_PACKAGE_SHAPE',
190
- cause: `packages/${dir}/package.json does not exclude ${TEST_EXCLUSION} from "files"`,
191
- fix: `add "${TEST_EXCLUSION}" to "files" in packages/${dir}/package.json, after "src"`,
207
+ cause: `packages/${dir}/package.json does not exclude ${exclusion} from "files"`,
208
+ fix: `add "${exclusion}" to "files" in packages/${dir}/package.json, after "src"`,
192
209
  docs: ERROR_DOCS_URL,
193
210
  at: `packages/${dir}/package.json`,
194
211
  });
@@ -222,7 +239,7 @@ export const buildArtifactsFinding = (dir: string, count: number): Finding => ({
222
239
  export const noFilesAllowlistFinding = (dir: string): Finding => ({
223
240
  code: 'X_PACKAGE_SHAPE',
224
241
  cause: `packages/${dir}/package.json publishes with no "files" allowlist, so the tarball carries whatever is in the directory`,
225
- fix: `add "files": ["src", "${TEST_EXCLUSION}", "README.md", "LICENSE"] to packages/${dir}/package.json`,
242
+ fix: `add "files": ["src", ${TEST_CODE_EXCLUSIONS.map((entry) => `"${entry}"`).join(', ')}, "README.md", "LICENSE"] to packages/${dir}/package.json`,
226
243
  docs: ERROR_DOCS_URL,
227
244
  at: `packages/${dir}/package.json`,
228
245
  });
@@ -239,7 +256,7 @@ export const noFilesAllowlistFinding = (dir: string): Finding => ({
239
256
  * The second half is `src` sweeping in every `*.test.ts` beside it. Tests are the framework's own,
240
257
  * they run against a preloaded frozen clock and a sealed network, and a consumer's test runner
241
258
  * collecting them is a failure nobody asked for — 393 files, over half of `@ultimat3/cli`'s
242
- * tarball.
259
+ * tarball. A `*-fixture.ts` is the same code under its other suffix (`TEST_CODE_EXCLUSIONS`).
243
260
  *
244
261
  * Private packages are exempt: a generated app's `packages/*` never reach a registry, carry no
245
262
  * `files` and need no license of their own.
@@ -258,8 +275,10 @@ export function checkPublishShape(root: string, manifests: readonly ManifestFact
258
275
  if (existsSync(join(root, 'packages', manifest.dir, entry))) continue;
259
276
  findings.push(missingPublishedFileFinding(manifest.dir, entry));
260
277
  }
261
- if (!manifest.files.includes(TEST_EXCLUSION)) {
262
- findings.push(publishesTestsFinding(manifest.dir));
278
+ for (const exclusion of TEST_CODE_EXCLUSIONS) {
279
+ if (!manifest.files.includes(exclusion)) {
280
+ findings.push(publishesTestsFinding(manifest.dir, exclusion));
281
+ }
263
282
  }
264
283
  }
265
284
  return findings;
@@ -312,6 +331,28 @@ export interface PackageShapeOptions {
312
331
  readonly release?: string;
313
332
  }
314
333
 
334
+ /**
335
+ * The fixtures a published package's entry points reach. A package with no fixture costs one glob
336
+ * — 19 of 31 — and one with some is read once: `mayReachFixture` answers from the text, and only a
337
+ * package it cannot clear pays for the import walk.
338
+ */
339
+ async function fixtureReach(root: string, dir: string, manifest: unknown): Promise<Finding[]> {
340
+ const base = join(root, 'packages', dir);
341
+ const fixtures = new Bun.Glob(`src/**/*${FIXTURE_SUFFIX}`).scan({ cwd: base, absolute: false });
342
+ if ((await fixtures.next()).done === true) return [];
343
+ const entries = entryFilesOf(manifest);
344
+ // What the tarball carries of `src`, plus the fixtures themselves: a test's imports are not a
345
+ // consumer's, so no `*.test.ts` is read.
346
+ const sources = new Map<string, string>();
347
+ for await (const path of new Bun.Glob('src/**/*.{ts,tsx}').scan({ cwd: base, absolute: false })) {
348
+ if (!/\.test\.tsx?$/.test(path)) sources.set(path, readFileSync(join(base, path), 'utf8'));
349
+ }
350
+ if (!mayReachFixture(sources, entries)) return [];
351
+ return fixturesReachedFrom({ read: (path) => sources.get(path) }, entries).map((fixture) =>
352
+ fixtureReachFinding(dir, fixture, entries),
353
+ );
354
+ }
355
+
315
356
  /** Every package ships the same contract files; a missing one is a build error, not a chore. */
316
357
  export async function checkPackageShape(
317
358
  root: string,
@@ -348,6 +389,7 @@ export async function checkPackageShape(
348
389
  findings.push(badVersionFinding(dir, version));
349
390
  continue;
350
391
  }
392
+ if (record.private !== true) findings.push(...(await fixtureReach(root, dir, manifest)));
351
393
  facts.push({
352
394
  dir,
353
395
  name: typeof record.name === 'string' ? record.name : `@ultimat3/${dir}`,
@@ -0,0 +1,127 @@
1
+ // One workspace declaring another in its `package.json`, as a TEXT edit. A generator that makes
2
+ // `packages/db` import the web app owes the manifest line `package-shape` would otherwise demand
3
+ // (`X_WORKSPACE_DEP_UNDECLARED`), and re-serialising the whole file would reformat arrays and
4
+ // spacing the author — or their formatter — chose.
5
+
6
+ import { containedPath } from './generate-write';
7
+
8
+ const MANIFEST = 'package.json';
9
+
10
+ /** The fields a manifest edit reads. Anything else in the file is carried through untouched. */
11
+ interface Manifest {
12
+ readonly name?: unknown;
13
+ readonly version?: unknown;
14
+ readonly dependencies?: unknown;
15
+ readonly devDependencies?: unknown;
16
+ readonly peerDependencies?: unknown;
17
+ }
18
+
19
+ function parseManifest(text: string): Manifest | undefined {
20
+ try {
21
+ const parsed: unknown = JSON.parse(text);
22
+ return typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)
23
+ ? (parsed as Manifest)
24
+ : undefined;
25
+ } catch {
26
+ // A manifest that does not parse is `bun install`'s finding, and `package-shape`'s.
27
+ return undefined;
28
+ }
29
+ }
30
+
31
+ const stringRecord = (value: unknown): Readonly<Record<string, string>> =>
32
+ typeof value === 'object' && value !== null
33
+ ? Object.fromEntries(
34
+ Object.entries(value).filter(
35
+ (entry): entry is [string, string] => typeof entry[1] === 'string',
36
+ ),
37
+ )
38
+ : {};
39
+
40
+ const declares = (manifest: Manifest, name: string): boolean =>
41
+ [manifest.dependencies, manifest.devDependencies, manifest.peerDependencies].some((block) =>
42
+ Object.hasOwn(stringRecord(block), name),
43
+ );
44
+
45
+ /**
46
+ * `manifest` with `name` declared under `dependencies`, or `undefined` when it already is — or
47
+ * when the text is not a manifest this can edit safely. The block is rewritten from its parsed
48
+ * entries, sorted the way `bun add` sorts them; every byte outside it is the author's.
49
+ */
50
+ export function withDependency(
51
+ manifest: string,
52
+ name: string,
53
+ version: string,
54
+ ): string | undefined {
55
+ const parsed = parseManifest(manifest);
56
+ if (parsed === undefined || declares(parsed, name)) return undefined;
57
+ const block = /^([ \t]*)"dependencies":\s*\{/m.exec(manifest);
58
+ if (block === null) {
59
+ const close = manifest.lastIndexOf('}');
60
+ const body = manifest.slice(0, close).trimEnd();
61
+ // `{}` has no property to put a comma after; nothing a scaffold writes is that.
62
+ if (close === -1 || body.endsWith('{')) return undefined;
63
+ return `${body},\n "dependencies": {\n "${name}": "${version}"\n }\n}\n`;
64
+ }
65
+ const indent = block[1] ?? '';
66
+ const open = block.index + block[0].length;
67
+ const close = manifest.indexOf('}', open);
68
+ if (close === -1) return undefined;
69
+ const entries = Object.entries({ ...stringRecord(parsed.dependencies), [name]: version })
70
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
71
+ .map(([key, value]) => `${indent} ${JSON.stringify(key)}: ${JSON.stringify(value)}`);
72
+ return `${manifest.slice(0, open)}\n${entries.join(',\n')}\n${indent}${manifest.slice(close)}`;
73
+ }
74
+
75
+ /** A workspace's `name` and `version`, or `undefined` when its manifest supplies no name. */
76
+ export async function readWorkspace(
77
+ root: string,
78
+ dir: string,
79
+ ): Promise<{ readonly name: string; readonly version: string } | undefined> {
80
+ const file = Bun.file(containedPath(root, `${dir}/${MANIFEST}`));
81
+ if (!(await file.exists())) return undefined;
82
+ const parsed = parseManifest(await file.text());
83
+ if (parsed === undefined || typeof parsed.name !== 'string' || parsed.name === '') {
84
+ return undefined;
85
+ }
86
+ return {
87
+ name: parsed.name,
88
+ version: typeof parsed.version === 'string' ? parsed.version : '0.0.0',
89
+ };
90
+ }
91
+
92
+ /** One file a caller is about to write: its app-root-relative path and its whole new text. */
93
+ export interface PlannedEdit {
94
+ readonly path: string;
95
+ readonly contents: string;
96
+ }
97
+
98
+ /**
99
+ * The edit that declares the workspace at `toDir` in `fromDir`'s manifest, NOT yet written — a
100
+ * registrar plans every file it touches and writes all of them or none. `undefined` when the edge
101
+ * is already declared or either manifest is unreadable; the `package-shape` step then names the
102
+ * line, which is the same edit by hand.
103
+ */
104
+ export async function planWorkspaceDependency(
105
+ root: string,
106
+ fromDir: string,
107
+ toDir: string,
108
+ ): Promise<PlannedEdit | undefined> {
109
+ const target = await readWorkspace(root, toDir);
110
+ const path = `${fromDir}/${MANIFEST}`;
111
+ const file = Bun.file(containedPath(root, path));
112
+ if (target === undefined || !(await file.exists())) return undefined;
113
+ const contents = withDependency(await file.text(), target.name, target.version);
114
+ return contents === undefined ? undefined : { path, contents };
115
+ }
116
+
117
+ /** Declares it now. Answers the path it rewrote, or `undefined` when there was nothing to write. */
118
+ export async function declareWorkspaceDependency(
119
+ root: string,
120
+ fromDir: string,
121
+ toDir: string,
122
+ ): Promise<string | undefined> {
123
+ const edit = await planWorkspaceDependency(root, fromDir, toDir);
124
+ if (edit === undefined) return undefined;
125
+ await Bun.write(containedPath(root, edit.path), edit.contents);
126
+ return edit.path;
127
+ }
@@ -1,54 +0,0 @@
1
- // The CLI's build and bundle refusals: an entry an app does not have, an island that will not
2
- // compile, a framework browser script that will not bundle. Split from `errors.ts` at its 500-line
3
- // ceiling; `errors.ts` re-exports every name, so no import site moves.
4
-
5
- import { UltimateError } from '@ultimat3/core';
6
- import { quoteArg } from './shell-quote';
7
-
8
- /**
9
- * A build target names an entry file the app does not have. `x build` refuses before it spawns the
10
- * builder: `bun build`'s own "module not found" says nothing about which file an Ultimate app is
11
- * supposed to own, and `docker build`'s says nothing about which target wanted it.
12
- */
13
- export class BuildEntryMissingError extends UltimateError {
14
- constructor(input: { target: string; entry: string }) {
15
- super({
16
- code: 'X_BUILD_ENTRY_MISSING',
17
- cause: `x build --target ${input.target} builds from ${input.entry}, and the app does not have it`,
18
- fix: `x new scratch-app --dry-run --json # its file list carries ${input.entry}; copy that file into this app`,
19
- });
20
- }
21
- }
22
-
23
- /**
24
- * A client entry would not compile. `X_BUILD_FAILED`, not a code of its own: an island is a bundle
25
- * entry point like any other, and the target's own logs are what says which line. The fix builds
26
- * exactly that one file, so the next message an author reads is the compiler's and not the CLI's.
27
- */
28
- export class IslandBuildFailedError extends UltimateError {
29
- constructor(input: { file: string; logs: string }) {
30
- super({
31
- code: 'X_BUILD_FAILED',
32
- cause: `${input.file} is an island entry point and would not bundle: ${input.logs}`,
33
- fix: `bun build --target browser ${input.file}`,
34
- });
35
- }
36
- }
37
-
38
- /** Which framework script a page ships: the sync worker, the page boot, or the client router. */
39
- export type FrameworkScriptKind = 'sync worker' | 'page boot' | 'client router';
40
-
41
- /**
42
- * One of the page's framework scripts would not bundle. Same code as an island: "a browser entry
43
- * the framework builds did not build" is one condition, and the entry here is framework code, not
44
- * the app's — the cause names WHICH script, so the reader knows what the page is now missing.
45
- */
46
- export class FrameworkScriptBuildFailedError extends UltimateError {
47
- constructor(input: { what: FrameworkScriptKind; entry: string; logs: string }) {
48
- super({
49
- code: 'X_BUILD_FAILED',
50
- cause: `the ${input.what} (${input.entry}) would not bundle for the browser: ${input.logs}`,
51
- fix: `bun build --target browser --format iife ${quoteArg(input.entry)}`,
52
- });
53
- }
54
- }
@@ -1,204 +0,0 @@
1
- // The app `x dev` boots in its tests, and the one place it is declared: every assertion that
2
- // needs a booted app lives in `cmd-dev.test.ts`, because a process has ONE lifecycle — the second
3
- // in-process boot is refused (X_LIFECYCLE_DRAINED) — and the coverage gate runs a package in one
4
- // process. So the fixture is a module of its own, and that file keeps its line budget for tests.
5
- //
6
- // What the app declares, and what each declaration is here to prove:
7
- // app.config.ts the root marker a real `x dev` cannot start without; `ai.mcp` by default
8
- // apps/web/mcp.ts the app's own MCP endpoint, mounted by the web role
9
- // apps/web/runtime.ts the app's middleware and disks, reaching a development process
10
- // .git/HEAD the fixture is its own repository — see the entry below
11
- // apps/web/app/hello/* an SSR page with a component, edited on disk while `x dev` runs
12
- // apps/web/app/notes/* a memory-backed entity and a live query, fed by the in-process bridge
13
- // apps/web/app/posts/* an action, a policy and a query, mounted as HTTP routes
14
- // apps/web/site/pricing/* a static page with its own stylesheet, under the CSP `x dev` sends
15
- import { resetRegistry as resetActions } from '@ultimat3/action';
16
- import { clearRegistry as clearEntities } from '@ultimat3/entity';
17
- import { resetJobs, resetTasks } from '@ultimat3/jobs';
18
- import { clearPermissions, clearRoles } from '@ultimat3/policy';
19
- import { resetRegistry as resetQueries } from '@ultimat3/query';
20
- import type { Frame } from '@ultimat3/realtime';
21
- import { decode } from '@ultimat3/realtime';
22
- import type { WsLike } from '@ultimat3/realtime/server';
23
- import { clearRoutes } from '@ultimat3/render';
24
- import { resetAppLoad } from './app-load';
25
-
26
- export const DEV_FIXTURE_FILES: Readonly<Record<string, string>> = {
27
- 'package.json': JSON.stringify({ name: 'dev-fixture', version: '1.4.0' }),
28
-
29
- // Its own repository, so the ignore walk stops HERE. The framework's root `.gitignore` lists
30
- // `packages/cli/.dev-fixture/`, and `devIgnore` honours every ancestor up to a `.git` — without
31
- // this marker the watcher admitted the fixture root and nothing under it, so no save in this
32
- // tree ever reached `rebuild`, and the reload path was booted by every run and exercised by none.
33
- '.git/HEAD': 'ref: refs/heads/main\n',
34
-
35
- // The root marker a real `x dev` cannot start without, and where `ai.mcp` is declared — by
36
- // default `{ expose: true, path: '/mcp' }`, which is what the MCP mount reads.
37
- 'app.config.ts': `import { defineConfig } from '@ultimat3/core';
38
- export const config = defineConfig({ name: 'dev-fixture' });
39
- `,
40
-
41
- // The app's own MCP endpoint, in the contract `app-mcp.ts` reads. `resolveToken` answering
42
- // `null` rejects every bearer, which is enough to prove the ROUTE is mounted (401, not 404).
43
- 'apps/web/mcp.ts': `import { defineAppMcp } from '@ultimat3/mcp';
44
- export const mcp = defineAppMcp({ include: 'exposed', resolveToken: () => null });
45
- `,
46
-
47
- // The app's own middleware, in the contract `app-runtime.ts` reads: a header on every response
48
- // is the cheapest proof that the chain `x dev` composed is the app's and not only the replica's.
49
- // Its disks too: `x dev` built its own embedded disk and ignored this one until 22.4.
50
- 'apps/web/runtime.ts': `import { defineStorage, localDriver } from '@ultimat3/storage';
51
- const stamp = async (request, ctx, next) => {
52
- const response = await next(request, ctx);
53
- const headers = new Headers(response.headers);
54
- headers.set('x-dev-runtime', 'app');
55
- return new Response(response.body, { status: response.status, headers });
56
- };
57
- const storage = defineStorage({
58
- disks: { appdisk: localDriver({ root: new URL('../../.app-disk', import.meta.url).pathname }) },
59
- default: 'appdisk',
60
- });
61
- export const runtime = { middleware: [stamp], storage };
62
- `,
63
-
64
- // A memory-backed entity, so the fixture needs no migration: the row observer sits on
65
- // `database()`'s repo wrapper, the same seam a Postgres-backed repo writes through.
66
- 'apps/web/app/notes/entity.ts': `import { database, entity, memoryDriver, text, uuid } from '@ultimat3/entity';
67
- export const notes = entity('notes', { columns: { id: uuid().primaryKey(), title: text() } });
68
- export const db = database({ notes }, { driver: memoryDriver() });
69
- `,
70
- 'apps/web/app/notes/live.ts': `import { allow } from '@ultimat3/policy';
71
- import { from, query, t } from '@ultimat3/query';
72
- import { db } from './entity';
73
- export const liveNotes = query({
74
- input: t.object({}),
75
- policy: allow('public'),
76
- live: true,
77
- subscribes: ['notes'],
78
- sql: () =>
79
- from<{ id: string; title: string }>('notes', () => db.notes.where({}).all())
80
- .orderBy('id')
81
- .limit(50),
82
- });
83
- `,
84
-
85
- 'apps/web/app/posts/policy.ts': `import { allow, can, definePermissions, defineRoles } from '@ultimat3/policy';
86
- export const permissions = definePermissions(['post:publish'] as const);
87
- export const roles = defineRoles({
88
- author: { grants: ['post:publish'] },
89
- reader: { grants: [] },
90
- });
91
- export const canPostWrite = can('post:publish');
92
- export const anyone = allow();
93
- `,
94
-
95
- 'apps/web/app/posts/actions.ts': `import { action, t } from '@ultimat3/action';
96
- import { anyone, canPostWrite } from './policy';
97
-
98
- export const publishPost = action({
99
- input: t.object({ id: t.uuid }),
100
- output: t.object({ id: t.uuid }),
101
- policy: canPostWrite,
102
- async handle({ input }) {
103
- return { id: input.id };
104
- },
105
- });
106
-
107
- export const echoPost = action({
108
- input: t.object({ word: t.string }),
109
- output: t.object({ word: t.string }),
110
- policy: anyone,
111
- async handle({ input }) {
112
- return { word: input.word };
113
- },
114
- });
115
- `,
116
-
117
- // A page with a body, on the surface an author edits all day. What the reload test rewrites: the
118
- // module must be imported again for the new body to be served, which `import()` alone never does.
119
- 'apps/web/app/hello/page.tsx': `import { defineRoute } from '@ultimat3/render';
120
-
121
- export const config = defineRoute({
122
- render: 'ssr',
123
- hydrate: 'visible',
124
- offline: 'runtime',
125
- budget: { js: '60kb' },
126
- meta: () => ({ title: 'Hello', description: 'A page that is edited while x dev runs' }),
127
- });
128
-
129
- export function Page() {
130
- return <p>generation one</p>;
131
- }
132
- `,
133
-
134
- // An ISR page whose body is a COMPONENT in another file — the shape every real page has. What the
135
- // component-reload test rewrites: the page module's own bytes never change, so only evicting what
136
- // imports the component re-evaluates it, and only clearing the ISR store stops the first render
137
- // from answering for the rest of the process (notificado.co, 2026-09-27).
138
- 'apps/web/site/news/headline.tsx': `export function Headline() {
139
- return <h1>headline one</h1>;
140
- }
141
- `,
142
-
143
- 'apps/web/site/news/page.tsx': `import { defineRoute } from '@ultimat3/render';
144
- import { Headline } from './headline';
145
-
146
- export const config = defineRoute({
147
- render: 'isr',
148
- revalidate: { ttl: '1h' },
149
- offline: 'runtime',
150
- hydrate: 'never',
151
- budget: { js: '0kb' },
152
- meta: () => ({ title: 'News', description: 'A page whose component is edited while x dev runs' }),
153
- });
154
-
155
- export function Page() {
156
- return <main><Headline /></main>;
157
- }
158
- `,
159
-
160
- // A stylesheet the page imports, because that import is what registers it — and the document's
161
- // inline `<style>` is what the CSP has to name. Without one this file served no styled page and
162
- // could not have caught the policy that blanked every deployed app.
163
- 'apps/web/site/pricing/page.module.scss': `.price { color: #123456; }
164
- `,
165
-
166
- 'apps/web/site/pricing/page.tsx': `import { defineRoute } from '@ultimat3/render';
167
- import './page.module.scss';
168
-
169
- export const config = defineRoute({
170
- render: 'static',
171
- offline: 'precache',
172
- hydrate: 'never',
173
- budget: { js: '0kb' },
174
- meta: () => ({ title: 'Pricing', description: 'What it costs' }),
175
- });
176
- `,
177
- };
178
-
179
- export const resetRegistries = (): void => {
180
- resetActions();
181
- resetQueries();
182
- clearEntities();
183
- clearRoutes();
184
- resetJobs();
185
- resetTasks();
186
- clearPermissions();
187
- clearRoles();
188
- resetAppLoad();
189
- };
190
-
191
- /** A `WsLike` that keeps every frame the node sends, decoded — what a live assertion reads. */
192
- export class FakeWs implements WsLike {
193
- readonly frames: Frame[] = [];
194
- send(data: string): number {
195
- this.frames.push(decode(data));
196
- return data.length;
197
- }
198
- close(): void {}
199
- subscribe(): void {}
200
- unsubscribe(): void {}
201
- getBufferedAmount(): number {
202
- return 0;
203
- }
204
- }