@ultimat3/cli 1.1.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/CLAUDE.md +724 -0
  2. package/README.md +41 -9
  3. package/package.json +25 -23
  4. package/src/api-routes.ts +16 -0
  5. package/src/app-auth.ts +32 -0
  6. package/src/app-entities.ts +18 -0
  7. package/src/app-env.ts +103 -0
  8. package/src/app-load.ts +20 -3
  9. package/src/bin.ts +4 -3
  10. package/src/budgets.ts +114 -9
  11. package/src/cmd-build.ts +69 -21
  12. package/src/cmd-db-branch.ts +215 -0
  13. package/src/cmd-db.ts +332 -155
  14. package/src/cmd-deploy.ts +59 -6
  15. package/src/cmd-dev.ts +87 -17
  16. package/src/cmd-docs.ts +167 -0
  17. package/src/cmd-doctor.ts +64 -9
  18. package/src/cmd-env.ts +95 -0
  19. package/src/cmd-errors.ts +33 -13
  20. package/src/cmd-fix.ts +5 -1
  21. package/src/cmd-generate.ts +146 -111
  22. package/src/cmd-help.ts +16 -5
  23. package/src/cmd-i18n.ts +2 -0
  24. package/src/cmd-jobs.ts +47 -33
  25. package/src/cmd-mcp.ts +11 -2
  26. package/src/cmd-new.ts +13 -7
  27. package/src/cmd-planned.ts +55 -10
  28. package/src/cmd-policy.ts +1 -0
  29. package/src/cmd-registries.ts +3 -0
  30. package/src/cmd-secrets.ts +368 -0
  31. package/src/cmd-tasks.ts +1 -0
  32. package/src/cmd-test.ts +17 -23
  33. package/src/cmd-verify.ts +177 -23
  34. package/src/db-backfill.ts +401 -0
  35. package/src/db-branch.ts +251 -0
  36. package/src/db-destructive.ts +29 -0
  37. package/src/db-finding.ts +28 -0
  38. package/src/db-generate.ts +112 -0
  39. package/src/db-snapshot.ts +24 -0
  40. package/src/dev-assets.ts +86 -20
  41. package/src/dev-cache.ts +122 -0
  42. package/src/dev-dashboard.ts +19 -4
  43. package/src/dev-hooks.ts +27 -2
  44. package/src/dev-n-plus-one.ts +191 -0
  45. package/src/dev-queue.ts +105 -19
  46. package/src/dev-render.ts +158 -26
  47. package/src/dev-roles-fixture.ts +67 -0
  48. package/src/dev-roles.ts +186 -78
  49. package/src/dev-runtime.ts +117 -40
  50. package/src/dev-services.ts +15 -0
  51. package/src/dev-storage.ts +245 -0
  52. package/src/dev-sync.ts +107 -0
  53. package/src/dev-traces.ts +11 -3
  54. package/src/dispatch.ts +4 -2
  55. package/src/document-styles.ts +54 -0
  56. package/src/drift.ts +37 -9
  57. package/src/error-catalog.ts +7 -18
  58. package/src/error-codes.ts +186 -0
  59. package/src/error-contract.ts +29 -7
  60. package/src/error-fixes.ts +114 -0
  61. package/src/errors.ts +205 -140
  62. package/src/fix-command.ts +268 -0
  63. package/src/flag-number.ts +56 -0
  64. package/src/framework-scope.ts +49 -0
  65. package/src/generate-kinds.ts +97 -0
  66. package/src/guards.ts +186 -0
  67. package/src/index.ts +87 -14
  68. package/src/island-bundle.ts +166 -0
  69. package/src/island-routes.ts +50 -0
  70. package/src/jobs-driver.ts +33 -0
  71. package/src/jobs-json.ts +24 -0
  72. package/src/jobs-report.ts +17 -4
  73. package/src/mcp-db-target.ts +52 -27
  74. package/src/mcp-errors.ts +120 -19
  75. package/src/mcp-host.ts +44 -25
  76. package/src/messages.ts +81 -2
  77. package/src/metrics-endpoint.ts +73 -0
  78. package/src/migrations.ts +37 -4
  79. package/src/otlp-export.ts +64 -0
  80. package/src/output.ts +46 -16
  81. package/src/parse.ts +41 -3
  82. package/src/policy-facts.ts +38 -6
  83. package/src/policy-fixture.ts +14 -7
  84. package/src/prerender.ts +111 -2
  85. package/src/registry.ts +21 -3
  86. package/src/runtime-overrides.ts +66 -0
  87. package/src/safe-url-label.ts +24 -0
  88. package/src/scaffold-fixture.ts +10 -0
  89. package/src/scaffold-typecheck.ts +16 -38
  90. package/src/serve.ts +202 -18
  91. package/src/source-files.ts +4 -0
  92. package/src/statement-loop.ts +74 -0
  93. package/src/style-csp.ts +18 -0
  94. package/src/sync-authenticator.ts +59 -0
  95. package/src/templates/action.ts +15 -30
  96. package/src/templates/admin-page.ts +103 -0
  97. package/src/templates/admin.ts +11 -7
  98. package/src/templates/backfill.ts +212 -0
  99. package/src/templates/entity.ts +72 -31
  100. package/src/templates/guard.ts +143 -0
  101. package/src/templates/index.ts +12 -1
  102. package/src/templates/island.ts +67 -0
  103. package/src/templates/job.ts +53 -13
  104. package/src/templates/naming.ts +17 -1
  105. package/src/templates/policy.ts +35 -28
  106. package/src/templates/query.ts +24 -5
  107. package/src/templates/resource.ts +19 -11
  108. package/src/templates/route.ts +90 -15
  109. package/src/templates/scaffold-app.ts +142 -45
  110. package/src/templates/scaffold-claude-agents.ts +149 -0
  111. package/src/templates/scaffold-claude-commands.ts +221 -0
  112. package/src/templates/scaffold-claude.ts +134 -0
  113. package/src/templates/scaffold-container.ts +46 -2
  114. package/src/templates/scaffold-db-package.ts +91 -0
  115. package/src/templates/scaffold-docs.ts +24 -5
  116. package/src/templates/scaffold-domain-package.ts +90 -0
  117. package/src/templates/scaffold-env.ts +87 -0
  118. package/src/templates/scaffold-i18n.ts +4 -1
  119. package/src/templates/scaffold-mcp-package.ts +49 -0
  120. package/src/templates/scaffold-package-shape.ts +25 -4
  121. package/src/templates/scaffold-repo.ts +116 -257
  122. package/src/templates/scaffold-roles.ts +68 -0
  123. package/src/templates/scaffold-ui-package.ts +56 -0
  124. package/src/templates/slice-foundation.ts +88 -0
  125. package/src/templates/wrap.ts +95 -0
  126. package/src/test-counts.ts +35 -0
  127. package/src/test-select.ts +30 -15
  128. package/src/test-shards.ts +21 -3
  129. package/src/test-workers.ts +47 -0
  130. package/src/ts-scan.ts +271 -13
  131. package/src/tsconfig-references.ts +78 -0
  132. package/src/verify-floor.ts +133 -0
  133. package/src/verify-step.ts +19 -0
  134. package/src/verify-test-run.ts +72 -0
  135. package/src/verify-tests.ts +160 -71
  136. package/src/version-loader.ts +20 -3
  137. package/src/workspace-checks.ts +87 -16
  138. package/src/write-line.ts +34 -0
@@ -0,0 +1,88 @@
1
+ // The modules a feature slice owns — `entity.ts`, `repo.ts`, `policy.ts`, `errors.ts` — composed by
2
+ // every generator that imports one. `x g resource` already wrote them by composing `entityFiles` +
3
+ // `policyFiles`; the five generators that write *into* a slice imported the same files and wrote
4
+ // none of them, so each emitted TS2307 in any slice a resource had not been run in first.
5
+
6
+ import type { FeatureTarget } from './entity';
7
+ import { entityFiles } from './entity';
8
+ import type { GeneratedFile, NameSet } from './naming';
9
+ import { names } from './naming';
10
+ import { policyFiles } from './policy';
11
+
12
+ /**
13
+ * Which slice modules a generator's own source imports. Named per generator rather than emitted as
14
+ * one fixed set: a job imports `../repo` and evaluates no policy, and a generated `policy.ts` it
15
+ * never reads is a file an author has to read before deleting.
16
+ *
17
+ * `'entity'` is the pair, not the file: `repo.ts` imports `./entity` for its row type, so emitting
18
+ * one without the other moves the unresolved import rather than closing it.
19
+ */
20
+ export type SliceModule = 'entity' | 'policy' | 'errors';
21
+
22
+ /** The feature's own code, derived once. The `docs:` URL used to be the literal
23
+ * `.../X_NOT_FOUND` beside a `code:` of `X_INVOICE_NOT_FOUND`, so following the link from a real
24
+ * failure landed on a different code's page — the same interpolation `error-codes.ts`'s `docsFor`
25
+ * already does for every framework code. */
26
+ const notFoundCode = (feature: NameSet): string =>
27
+ `X_${feature.kebab.toUpperCase().split('-').join('_')}_NOT_FOUND`;
28
+
29
+ /**
30
+ * The emitted `fix:` cites `x queries list`, and which command it cites is the whole point: a
31
+ * scaffolded app runs the same `errors` step this repo does, so a fix naming a command the build
32
+ * does not ship writes a fresh X_ERROR_FIX_INVALID into the app on every `x g action`. It cited
33
+ * `x db studio`, which is in `PLANNED_SUBCOMMANDS` and exits X_NOT_IMPLEMENTED — the generator was
34
+ * breaking the one rule it exists to demonstrate. `x queries list` ships, and a read is where a
35
+ * caller gets an id that exists.
36
+ */
37
+ const errorsSource = (feature: NameSet): string => {
38
+ const errorCode = notFoundCode(feature);
39
+ return `// The ${feature.kebab} feature's X_* codes. Never throw a bare Error: an agent reading the failure
40
+ // needs the code, the cause and the exact command that fixes it.
41
+
42
+ import { UltimateError } from '@ultimat3/core';
43
+
44
+ export class ${feature.pascal}NotFoundError extends UltimateError {
45
+ constructor(input: { id: string }) {
46
+ super({
47
+ code: '${errorCode}',
48
+ cause: \`no ${feature.kebab} with id \${input.id}\`,
49
+ fix: 'x queries list --json, then pass an id the ${feature.kebab} read returns',
50
+ docs: 'https://ultimate.dev/errors/${errorCode}',
51
+ });
52
+ }
53
+ }
54
+ `;
55
+ };
56
+
57
+ /**
58
+ * Re-tags a slice module as one the writer may skip. The byte-carrying variant is passed through
59
+ * untouched rather than cast: only the scaffolded app icon carries bytes and no slice module is a
60
+ * PNG, so a future one would be a visible hard write instead of a silent claim it was checked.
61
+ */
62
+ const ifAbsent = (files: readonly GeneratedFile[]): readonly GeneratedFile[] =>
63
+ files.map((file) =>
64
+ typeof file.contents === 'string'
65
+ ? { path: file.path, contents: file.contents, merge: 'if-absent' as const }
66
+ : file,
67
+ );
68
+
69
+ /**
70
+ * The slice modules `needs` names, in the order a reader meets them: the table, then the authz,
71
+ * then the failures. Named from `target.feature` and never from the primitive's own name — the
72
+ * generated `import { InvoiceNotFoundError } from '../errors'` is the feature's type, not
73
+ * `send-invoice`'s.
74
+ */
75
+ export function sliceFoundation(
76
+ target: FeatureTarget,
77
+ needs: readonly SliceModule[],
78
+ ): readonly GeneratedFile[] {
79
+ const feature = names(target.feature);
80
+ const dir = `${target.surfaceDir}/${target.feature}`;
81
+ return ifAbsent([
82
+ ...(needs.includes('entity') ? entityFiles(target.feature, target) : []),
83
+ ...(needs.includes('policy') ? policyFiles(target.feature, target) : []),
84
+ ...(needs.includes('errors')
85
+ ? [{ path: `${dir}/errors.ts`, contents: errorsSource(feature) }]
86
+ : []),
87
+ ]);
88
+ }
@@ -0,0 +1,95 @@
1
+ // Emitting a line the way Biome would print it. A template cannot run a formatter, so generated
2
+ // source is written pre-formatted — and a FIXED shape is wrong for one name length or the other:
3
+ // Biome joins a short wrapped call back onto one line and breaks a long joined one, and the app's
4
+ // own `lint` step fails on whichever half the template guessed wrong.
5
+
6
+ /** The scaffold's own `biome.json` says `lineWidth: 100`, and this is the same number. */
7
+ export const LINE_WIDTH = 100;
8
+
9
+ /**
10
+ * `<open><a>, <b><close>` when it fits, one entry per line with a trailing comma when it does not.
11
+ * The one shape behind every generated argument list, array literal and parameter list — a second
12
+ * copy of this rule is how `x g entity credit-note-attachment` shipped a `$view([…])` line that the
13
+ * app's formatter rewrote on sight.
14
+ */
15
+ export function wrapList(
16
+ indent: string,
17
+ open: string,
18
+ entries: readonly string[],
19
+ close: string,
20
+ ): string {
21
+ const joined = `${indent}${open}${entries.join(', ')}${close}`;
22
+ if (joined.length <= LINE_WIDTH) return joined;
23
+ const body = entries.map((entry) => `${indent} ${entry},`).join('\n');
24
+ return `${indent}${open}\n${body}\n${indent}${close}`;
25
+ }
26
+
27
+ const isDigit = (char: string): boolean => char >= '0' && char <= '9';
28
+
29
+ /** How many digits run from `at`. A run is compared as a number, so `b2` precedes `b10`. */
30
+ const digitRun = (text: string, at: number): number => {
31
+ let end = at;
32
+ while (end < text.length && isDigit(text[end] ?? '')) end += 1;
33
+ return end - at;
34
+ };
35
+
36
+ /**
37
+ * Biome's own order for the names inside `{ … }`, measured against 2.5.8 rather than assumed —
38
+ * three behaviours, and no single-pass compare gives all three:
39
+ *
40
+ * `{ post, PostView }` → `{ PostView, post }` upper first on a pure case tie
41
+ * `{ Zeta, alpha }` → `{ alpha, Zeta }` but case is only ever the TIEBREAK
42
+ * `{ b10, b2 }` → `{ b2, b10 }` a digit run compares as a number
43
+ *
44
+ * The first two together are why folding to lower case and comparing is wrong: `post` is a prefix
45
+ * of `postview`, so a folded compare puts `post` first and Biome does not.
46
+ */
47
+ const compareSpecifiers = (left: string, right: string): number => {
48
+ let a = 0;
49
+ let b = 0;
50
+ while (a < left.length && b < right.length) {
51
+ const runA = digitRun(left, a);
52
+ const runB = digitRun(right, b);
53
+ if (runA > 0 && runB > 0) {
54
+ const valueA = Number(left.slice(a, a + runA));
55
+ const valueB = Number(right.slice(b, b + runB));
56
+ if (valueA !== valueB) return valueA - valueB;
57
+ a += runA;
58
+ b += runB;
59
+ continue;
60
+ }
61
+ const charA = left[a] ?? '';
62
+ const charB = right[b] ?? '';
63
+ const foldedA = charA.toLowerCase();
64
+ const foldedB = charB.toLowerCase();
65
+ if (foldedA !== foldedB) return foldedA < foldedB ? -1 : 1;
66
+ // Same letter, different case: uppercase wins HERE rather than after the whole string, which
67
+ // is what makes `PostView` precede `post` instead of following it.
68
+ if (charA !== charB) return charA < charB ? -1 : 1;
69
+ a += 1;
70
+ b += 1;
71
+ }
72
+ return left.length - right.length;
73
+ };
74
+
75
+ /**
76
+ * The names of an import, in the order Biome would leave them. `assist/source/organizeImports` is
77
+ * an ERROR under the scaffold's config, and the order is decided by the FEATURE NAME, not by the
78
+ * template: `x g action audit-log --feature billing` emitted `{ canBillingWrite, billingTag }`, so
79
+ * every feature starting `a` or `b` wrote a failing `lint` step into the app. Sorted here rather
80
+ * than spelled in each template, because the correct spelling is not knowable at authoring time.
81
+ */
82
+ export const sortSpecifiers = (names: readonly string[]): readonly string[] =>
83
+ [...names].sort(compareSpecifiers);
84
+
85
+ /**
86
+ * A named import, sorted and wrapped. Its own function because the braces are spaced on one line
87
+ * (`import { a, b } from …`) and not spaced when broken — `wrapList` would emit `import {a, b}`
88
+ * joined, or a stray space before the closing brace when broken.
89
+ */
90
+ export function wrapImport(names: readonly string[], from: string): string {
91
+ const sorted = sortSpecifiers(names);
92
+ const joined = `import { ${sorted.join(', ')} } from '${from}';`;
93
+ if (joined.length <= LINE_WIDTH) return joined;
94
+ return `import {\n${sorted.map((name) => ` ${name},`).join('\n')}\n} from '${from}';`;
95
+ }
@@ -0,0 +1,35 @@
1
+ // What a test step actually executed, read back out of `bun test`'s own summary. Separate from
2
+ // the two runners because both of them need it and neither owns it: a step's exit code answers
3
+ // "did anything fail", and only the counts answer "did anything run".
4
+
5
+ import type { ExecResult } from './exec';
6
+ import { execOutput } from './exec';
7
+ import { parseBunTest } from './mcp-test-output';
8
+
9
+ export interface TestCounts {
10
+ /** Tests that executed — passed plus failed. A failed test is a test that ran. */
11
+ readonly ran: number;
12
+ /** Tests bun reported as skipped or todo, which is the same thing here: they did not run. */
13
+ readonly skipped: number;
14
+ }
15
+
16
+ /**
17
+ * Summed across every process the step spawned, because a step is one line in the gate whether it
18
+ * ran on one worker or eight.
19
+ *
20
+ * `parseBunTest` is the reader, not a second regex: it is already the one place this repo turns
21
+ * bun's summary into numbers (`x mcp`'s `test.run`), and two readers of one format is exactly the
22
+ * drift the CLI's own boundary rule forbids. It reports output it cannot recognise as one FAILED
23
+ * test rather than as zeros — which is what keeps a runner that died before printing a summary
24
+ * from reading as a suite that ran nothing.
25
+ */
26
+ export const countsOf = (results: readonly ExecResult[]): TestCounts => {
27
+ let ran = 0;
28
+ let skipped = 0;
29
+ for (const result of results) {
30
+ const run = parseBunTest(execOutput(result), result.durationMs);
31
+ ran += run.passed + run.failed;
32
+ skipped += run.skipped;
33
+ }
34
+ return { ran, skipped };
35
+ };
@@ -9,27 +9,41 @@ import { BadFlagError } from './errors';
9
9
  import type { ParsedArgs } from './parse';
10
10
  import { flagString, nearest } from './parse';
11
11
  import type { TestType } from './verify-tests';
12
- import { TEST_TYPES, typeFilterOf } from './verify-tests';
12
+ import { ownerOf, TEST_TYPES } from './verify-tests';
13
13
 
14
14
  export interface TestFile {
15
15
  readonly path: string;
16
16
  readonly bytes: number;
17
17
  }
18
18
 
19
- const TEST_GLOB = '**/*.test.ts';
19
+ /**
20
+ * `.tsx` too. A JSX test was silently outside the gate's parallel steps — `discoverTests` never
21
+ * yielded it, so `runParallel` spawned `bun test` over an explicit file list that did not include
22
+ * it and the step reported green over a file that never executed, while `bun run test` at the root
23
+ * ran it and failed. `testStepCommand`'s own ignore patterns already say `.test.*`.
24
+ */
25
+ const TEST_GLOB = '**/*.test.{ts,tsx}';
20
26
 
21
27
  /**
22
28
  * The root `test` script's ignore list, kept identical so `x test` and `bun run test` see one
23
29
  * suite. `e2e/` is NOT on it: an opt-in suite that the gate runs but `x test` silently drops is
24
30
  * a suite nobody runs until CI says so. `examples/` is, because the reference app is a separate
25
31
  * project with its own gate — `x verify` there, not `x test` here.
32
+ *
33
+ * `dummy/` and `build/` complete the list `verify-tests.ts` already excluded (`NEVER_A_TEST`). The
34
+ * comment above claimed the two agreed and they did not: `x test unit` discovered 464 files where
35
+ * the gate's `unit` step ran 441, so the gate's own test steps — which now select through this
36
+ * function — would have started running a nested demo app's suite on the framework's gate.
37
+ *
38
+ * Both directories are gated where they belong, by `scripts/reference-app-gate.ts` running
39
+ * `x verify` inside each app — see `NEVER_A_TEST` for why that is the only place they run.
26
40
  */
27
- const IGNORED = ['/dist/', '/node_modules/', '/examples/'];
41
+ const IGNORED = ['/dist/', '/build/', '/node_modules/', '/examples/', '/dummy/'];
28
42
 
29
43
  /**
30
44
  * File size stands in for duration: cheap to read, and it correlates far better than file count.
31
- * `type`, when given, narrows to exactly the files verify-tests.ts would run for that suite — see
32
- * `belongsToType` below, the one place that rule is decided.
45
+ * `type`, when given, narrows to exactly the files verify-tests.ts would run for that suite — one
46
+ * owner per path, decided by `ownerOf` there and never a second time here.
33
47
  */
34
48
  export async function discoverTests(
35
49
  root: string,
@@ -65,18 +79,19 @@ export function sampleFiles(files: readonly TestFile[], sample: number): readonl
65
79
  }
66
80
 
67
81
  /**
68
- * verify-tests.ts owns the one definition of what a file's test type is; `typeFilterOf` is that
69
- * table's own accessor. Re-declaring the suffixes here would be a second definition, and the two
70
- * would drift the first time a suite's naming rule changed.
82
+ * verify-tests.ts owns the one definition of what a file's test type is, and `ownerOf` IS that
83
+ * definition — one owner per path, `unit` for anything no typed rule claims. Re-deciding it here
84
+ * would be a second answer, and the two would disagree the first time a suite's naming rule
85
+ * changed: a file both of them claimed would run in two steps of one gate.
86
+ *
87
+ * Asked lazily, and that is load-bearing: verify-tests.ts imports this module (the gate's test
88
+ * steps select their files through `discoverTests`), so the two form a cycle. Anything here that
89
+ * READ that module at import time would evaluate inside its temporal dead zone, and whichever side
90
+ * is imported first dies with "Cannot access 'TEST_TYPES' before initialization" — measured by
91
+ * `bun run manifest`, which imports the CLI and took the crash.
71
92
  */
72
- const TYPE_FILTERS: readonly (readonly [Exclude<TestType, 'unit'>, string])[] = TEST_TYPES.filter(
73
- (type): type is Exclude<TestType, 'unit'> => type !== 'unit',
74
- ).map((type) => [type, typeFilterOf(type)] as const);
75
-
76
- /** unit is everything the five typed suites do not claim, so no file falls between two types. */
77
93
  export function belongsToType(path: string, type: TestType): boolean {
78
- if (type === 'unit') return TYPE_FILTERS.every(([, filter]) => !path.includes(filter));
79
- return TYPE_FILTERS.some(([typed, filter]) => typed === type && path.includes(filter));
94
+ return ownerOf(path) === type;
80
95
  }
81
96
 
82
97
  /**
@@ -3,7 +3,7 @@
3
3
  // cmd-test.ts because a printed reproduction is only true if it carries every input to the split —
4
4
  // that rule is this file's, and argv parsing is that one's.
5
5
 
6
- import { docsFor } from './errors';
6
+ import { docsFor } from './error-codes';
7
7
  import type { Runner } from './exec';
8
8
  import { execOutput } from './exec';
9
9
  import { msg } from './messages';
@@ -43,8 +43,26 @@ export function planShards(files: readonly TestFile[], workers: number): readonl
43
43
  }));
44
44
  }
45
45
 
46
- /** Explicit file list, so the child never re-globs and can never pick up another shard's files. */
47
- export const shardArgs = (shard: Shard): readonly string[] => ['bun', 'test', ...shard.files];
46
+ /**
47
+ * Explicit file list, so the child never re-globs and can never pick up another shard's files.
48
+ *
49
+ * `--isolate` is what makes an arbitrary partition safe, and it is not optional. Half a dozen
50
+ * registries in this framework are process-global by design — the permission set, the roles, the
51
+ * entity/action/query tables, the error-code titles, the fixture bag — and a serial `bun test`
52
+ * only passes because glob order happens to put every declaring file before every file that reads
53
+ * what it left behind. Re-partition the same files and that accident is gone: measured on this
54
+ * repo, an 8-way split turned 0 failures into 36, all of them `X_PERMISSION_UNKNOWN` in
55
+ * `@ultimat3/query` because the `packages/cli` file that had been declaring `feed:read` for it
56
+ * landed in another shard. A fresh module registry per FILE removes the channel entirely, so the
57
+ * split can be any split. The database is isolated per WORKER, not per file — one cloned template
58
+ * per process, which is `ULTIMATE_TEST_WORKER` below.
59
+ */
60
+ export const SHARD_COMMAND_PREFIX = ['bun', 'test', '--isolate'] as const;
61
+
62
+ export const shardArgs = (shard: Shard): readonly string[] => [
63
+ ...SHARD_COMMAND_PREFIX,
64
+ ...shard.files,
65
+ ];
48
66
 
49
67
  const SHELL_SAFE = /^[\w@%+=:,./-]+$/;
50
68
 
@@ -0,0 +1,47 @@
1
+ // How wide a parallel test run goes, decided in one place. `x test --workers`, `x verify
2
+ // --workers` and every parallel step of the gate read this — a second default would split the same
3
+ // suite two different ways, and the `--worker N` reproduction a shard failure prints would then
4
+ // name a shard the gate never ran.
5
+
6
+ // Bun ships no CPU-count primitive; `cpus()` is the fallback when navigator cannot answer.
7
+ import { cpus } from 'node:os';
8
+
9
+ /** navigator first: it is the runtime's own answer, and it respects a container's CPU limit. */
10
+ export function availableCpus(): number {
11
+ const hinted = typeof navigator === 'undefined' ? Number.NaN : navigator.hardwareConcurrency;
12
+ return Math.max(1, Number.isFinite(hinted) && hinted > 0 ? Math.trunc(hinted) : cpus().length);
13
+ }
14
+
15
+ /**
16
+ * Deliberately MORE workers than cores, with a ceiling.
17
+ *
18
+ * `cpus - 1` is the intuitive default and it was measured to be worthless exactly where it has to
19
+ * pay off. On a 4-core `ubuntu-latest` — the runner this repo commits to — the `unit` step:
20
+ *
21
+ * | workers | wall |
22
+ * |---------|-------|
23
+ * | serial | 43.2s |
24
+ * | 3 (cpus - 1) | 44.8s | <- the old default: slower than not sharding at all
25
+ * | 4 | 41.6s |
26
+ * | 6 | 34.8s |
27
+ *
28
+ * Three workers on four cores loses to serial because sharding is not free — each worker reloads
29
+ * the framework's module graph — and three of them cannot cover that cost. The reason more-than-
30
+ * cores wins is that a test worker is not CPU-bound end to end: it spends real time on module
31
+ * resolution, on `--isolate` rebuilding a registry per file, and on waiting for its database.
32
+ * Oversubscribing fills those stalls.
33
+ *
34
+ * The ceiling is memory, not cores. A worker is a whole Bun process with the framework's module
35
+ * graph loaded and — in the typed suites — its own cloned Postgres or an in-process PGlite, so
36
+ * width costs hundreds of MB per step. It binds on a developer's 12- or 32-core machine, which is
37
+ * exactly where an unbounded count would swap.
38
+ *
39
+ * The floor of 2 keeps a 1-core box sharding rather than silently reverting to serial.
40
+ */
41
+ export const WORKER_CEILING = 8;
42
+
43
+ /** Oversubscription factor. See the table above — it is measured, not chosen for roundness. */
44
+ export const WORKER_OVERSUBSCRIBE = 1.5;
45
+
46
+ export const defaultWorkers = (available: number = availableCpus()): number =>
47
+ Math.max(2, Math.min(WORKER_CEILING, Math.round(available * WORKER_OVERSUBSCRIBE)));