@ultimat3/cli 19.1.3 → 19.3.1

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 (65) hide show
  1. package/CLAUDE.md +125 -8
  2. package/package.json +29 -29
  3. package/src/app-boundaries.ts +11 -2
  4. package/src/app-load.ts +5 -1
  5. package/src/app-openapi.ts +13 -5
  6. package/src/app-permissions.ts +0 -0
  7. package/src/browser-launcher.ts +53 -4
  8. package/src/budgets.ts +60 -7
  9. package/src/cmd-dev.ts +49 -39
  10. package/src/cmd-doctor.ts +61 -23
  11. package/src/cmd-generate.ts +5 -2
  12. package/src/cmd-i18n.ts +10 -3
  13. package/src/cmd-jobs.ts +56 -10
  14. package/src/cmd-shot.ts +3 -1
  15. package/src/cmd-test.ts +15 -10
  16. package/src/db-seed.ts +2 -1
  17. package/src/dev-queue.ts +16 -2
  18. package/src/dev-reload.ts +46 -0
  19. package/src/dev-render.ts +28 -7
  20. package/src/dev-roles.ts +9 -8
  21. package/src/dev-runtime.ts +4 -1
  22. package/src/dev-sync.ts +17 -3
  23. package/src/dev-watch-tree.ts +226 -0
  24. package/src/dev-watch.ts +75 -0
  25. package/src/doctor-offline.ts +122 -0
  26. package/src/duplicate-packages.ts +278 -0
  27. package/src/error-catalog.ts +4 -5
  28. package/src/error-codes.ts +6 -0
  29. package/src/fix-command.ts +40 -1
  30. package/src/fix-path.ts +10 -11
  31. package/src/flag-number.ts +15 -0
  32. package/src/generate-kinds.ts +54 -4
  33. package/src/generate-write.ts +25 -2
  34. package/src/gitignore.ts +145 -0
  35. package/src/hold.ts +50 -17
  36. package/src/i18n-registration.ts +34 -5
  37. package/src/index.ts +3 -1
  38. package/src/island-bundle.ts +123 -10
  39. package/src/island-harness.ts +11 -4
  40. package/src/island-states-load.ts +2 -1
  41. package/src/jobs-driver.ts +4 -1
  42. package/src/mcp-errors.ts +2 -0
  43. package/src/mcp-host.ts +21 -9
  44. package/src/parse.ts +17 -0
  45. package/src/path-segments.ts +14 -0
  46. package/src/prerender.ts +68 -16
  47. package/src/retry-memo.ts +37 -0
  48. package/src/serve.ts +17 -2
  49. package/src/shot-browser.ts +23 -4
  50. package/src/source-files.ts +3 -1
  51. package/src/static-report.ts +21 -1
  52. package/src/style-bundle.ts +124 -0
  53. package/src/style-csp.ts +14 -12
  54. package/src/style-routes.ts +56 -0
  55. package/src/sw-artifacts.ts +84 -12
  56. package/src/templates/admin-page.ts +49 -1
  57. package/src/templates/resource-form-island.ts +13 -3
  58. package/src/templates/scaffold-container.ts +12 -0
  59. package/src/templates/scaffold-repo.ts +13 -2
  60. package/src/test-passes.ts +79 -0
  61. package/src/test-shards.ts +110 -36
  62. package/src/verify-checks.ts +13 -7
  63. package/src/verify-step.ts +4 -4
  64. package/src/verify-tests.ts +33 -8
  65. package/src/web-binding.ts +22 -0
@@ -40,6 +40,7 @@ import { execOutput } from './exec';
40
40
  import { msg } from './messages';
41
41
  import type { CommandResult, Finding, JsonValue, StepResult } from './output';
42
42
  import { quoteArg } from './shell-quote';
43
+ import { testPasses } from './test-passes';
43
44
  import type { TestFile } from './test-select';
44
45
  import type { TestType } from './verify-tests';
45
46
 
@@ -65,15 +66,26 @@ export function testArgs(input: {
65
66
  readonly workers: number;
66
67
  /** 0-based, matching `--worker`. Absent runs the whole selection across `workers` processes. */
67
68
  readonly shard?: number;
69
+ /**
70
+ * Everything after a bare `--`, handed to `bun test` verbatim and BEFORE the file list, which is
71
+ * where bun reads its flags. `ParsedArgs.passthrough` had no reader anywhere until 2026-09, so
72
+ * `x test unit -- --coverage --bail` parsed both flags, carried them through the command and
73
+ * dropped them on the floor — a run that reported exactly what a coverage run reports, with no
74
+ * coverage measured. `CommandSpec.passthrough` is what keeps the other commands from doing the
75
+ * same in silence: they refuse the `--` instead.
76
+ */
77
+ readonly passthrough?: readonly string[];
68
78
  }): readonly string[] {
69
79
  const files = [...input.files].sort();
80
+ const extra = input.passthrough ?? [];
70
81
  return input.shard === undefined
71
- ? ['bun', 'test', `--parallel=${String(input.workers)}`, ...files]
82
+ ? ['bun', 'test', `--parallel=${String(input.workers)}`, ...extra, ...files]
72
83
  : [
73
84
  'bun',
74
85
  'test',
75
86
  '--isolate',
76
87
  `--shard=${String(input.shard + 1)}/${String(input.workers)}`,
88
+ ...extra,
77
89
  ...files,
78
90
  ];
79
91
  }
@@ -102,6 +114,8 @@ export interface ReproduceOptions {
102
114
  readonly affected?: AffectedSelection;
103
115
  /** 0-based, when reproducing ONE shard. Absent reproduces the whole selection. */
104
116
  readonly shard?: number;
117
+ /** What the caller put after `--`. It reaches `bun test`, so a rerun without it runs differently. */
118
+ readonly passthrough?: readonly string[];
105
119
  }
106
120
 
107
121
  /**
@@ -124,6 +138,11 @@ export function reproduceFor(options: ReproduceOptions): string {
124
138
  '--workers',
125
139
  String(options.workers),
126
140
  ...(options.shard === undefined ? [] : ['--worker', String(options.shard)]),
141
+ // Last, and after a `--` of its own, because that is where the caller typed it and where the
142
+ // parser will find it again. Quoted for `shell-quote.ts`'s reason: a reproduce line is pasted.
143
+ ...(options.passthrough === undefined || options.passthrough.length === 0
144
+ ? []
145
+ : ['--', ...options.passthrough.map(quoteArg)]),
127
146
  ].join(' ');
128
147
  }
129
148
 
@@ -144,16 +163,28 @@ export interface RunShardsOptions {
144
163
  readonly sample?: { readonly kept: number; readonly total: number };
145
164
  /** Passed straight to `reproduceFor`: see `ReproduceOptions.affected`. */
146
165
  readonly affected?: AffectedSelection;
166
+ /** Everything after the caller's `--`, forwarded to every pass and printed in the reproduce. */
167
+ readonly passthrough?: readonly string[];
147
168
  }
148
169
 
149
- /** The reproduction's inputs, resolved once: `workers` is the run's real width, not the ask. */
150
- const planOf = (options: RunShardsOptions, workers: number): ReproduceOptions => ({
151
- workers,
170
+ /**
171
+ * The reproduction's inputs for ONE pass: `workers` is that pass's real width, not the ask, and
172
+ * `type` is the pass's own when the split gave it one — `x test live --workers 1` reruns exactly
173
+ * the files that failed, where the whole invocation's flags would rerun the corpus around them.
174
+ */
175
+ const planOf = (
176
+ options: RunShardsOptions,
177
+ pass: { readonly workers: number; readonly type?: TestType },
178
+ ): ReproduceOptions => ({
179
+ workers: pass.workers,
152
180
  ...(options.filter === undefined ? {} : { filter: options.filter }),
153
- ...(options.type === undefined ? {} : { type: options.type }),
181
+ ...(pass.type === undefined ? {} : { type: pass.type }),
154
182
  ...(options.sample === undefined ? {} : { sample: options.sample.kept }),
155
183
  ...(options.affected === undefined ? {} : { affected: options.affected }),
156
184
  ...(options.only === undefined ? {} : { shard: options.only }),
185
+ ...(options.passthrough === undefined || options.passthrough.length === 0
186
+ ? {}
187
+ : { passthrough: options.passthrough }),
157
188
  });
158
189
 
159
190
  /**
@@ -173,68 +204,111 @@ export const failureOf = (code: number, files: number, plan: ReproduceOptions):
173
204
  });
174
205
 
175
206
  /**
176
- * ONE `bun test`, not one per worker. Bun owns the pool and hands each free worker the next file,
177
- * so nothing here decides which file runs where — see this file's header for what that measured.
207
+ * ONE `bun test` PER PASS, and one pass unless the selection mixes serial files with the rest —
208
+ * `test-passes.ts` decides that, and this spends it. Bun owns the pool inside a pass and hands
209
+ * each free worker the next file, so nothing here decides which file runs where; see this file's
210
+ * header for what that measured.
211
+ *
212
+ * Sequential, never `Promise.all`: the whole point of a serial pass is that nothing runs beside
213
+ * it. And every pass runs even after one fails — the caller asked for a suite, and a report that
214
+ * stops at the first red step hides the rest of the answer.
178
215
  *
179
216
  * `ULTIMATE_TEST_WORKER` is still set for a `--worker` rerun and only then: that run is one
180
217
  * process, so naming its database is this file's to do. A `--parallel` run has N of them and Bun
181
218
  * numbers each with `BUN_TEST_WORKER_ID`, which `@ultimat3/testing`'s `workerId` already reads.
182
219
  */
183
220
  export async function runShards(options: RunShardsOptions): Promise<CommandResult> {
184
- const files = options.files.map((file) => file.path);
185
- const workers = Math.max(1, Math.min(Math.trunc(options.workers), files.length || 1));
186
221
  const only = options.only;
222
+ const passes = testPasses({
223
+ files: options.files,
224
+ workers: options.workers,
225
+ ...(options.type === undefined ? {} : { type: options.type }),
226
+ ...(only === undefined ? {} : { shard: only }),
227
+ });
187
228
  const started = performance.now();
188
- const result = await options.runner(
189
- testArgs({ files, workers, ...(only === undefined ? {} : { shard: only }) }),
190
- {
191
- cwd: options.root,
192
- ...(only === undefined ? {} : { env: { ULTIMATE_TEST_WORKER: String(only) } }),
193
- },
194
- );
195
- const durationMs = Math.round(performance.now() - started);
196
- const plan = planOf({ ...options, workers }, workers);
197
- const type = options.type;
198
- const typeParam = type === undefined ? {} : { type };
199
- const sample = options.sample;
200
- const label = only === undefined ? `${workers} worker(s)` : `shard ${only} of ${workers}`;
201
- const steps: readonly StepResult[] = [
202
- {
203
- name: `${label} · ${files.length} files`,
229
+ const steps: StepResult[] = [];
230
+ const spent: JsonValue[] = [];
231
+ let ok = true;
232
+ let exitCode = 0;
233
+ for (const pass of passes) {
234
+ const files = pass.files.map((file) => file.path);
235
+ const result = await options.runner(
236
+ testArgs({
237
+ files,
238
+ workers: pass.workers,
239
+ ...(only === undefined ? {} : { shard: only }),
240
+ ...(options.passthrough === undefined ? {} : { passthrough: options.passthrough }),
241
+ }),
242
+ {
243
+ cwd: options.root,
244
+ ...(only === undefined ? {} : { env: { ULTIMATE_TEST_WORKER: String(only) } }),
245
+ },
246
+ );
247
+ const plan = planOf(options, pass);
248
+ const label =
249
+ only === undefined ? `${pass.workers} worker(s)` : `shard ${only} of ${pass.workers}`;
250
+ steps.push({
251
+ name: `${pass.type === undefined ? label : `${pass.type} · ${label}`} · ${files.length} files`,
204
252
  ok: result.ok,
205
253
  durationMs: result.durationMs,
206
254
  // `output.ts` documents this field as absent for a NON-test step, so omitting it here made
207
255
  // `renderJson` describe the test step as one — recoverable only by parsing `name`.
208
- workers,
256
+ workers: pass.workers,
209
257
  findings: result.ok ? [] : [failureOf(result.code, files.length, plan)],
210
258
  output: execOutput(result),
211
- },
212
- ];
259
+ });
260
+ spent.push({
261
+ ...(pass.type === undefined ? {} : { type: pass.type }),
262
+ files: files.length,
263
+ workers: pass.workers,
264
+ ok: result.ok,
265
+ exitCode: result.code,
266
+ reproduce: reproduceFor(plan),
267
+ });
268
+ ok = ok && result.ok;
269
+ if (exitCode === 0) exitCode = result.code;
270
+ }
271
+ const durationMs = Math.round(performance.now() - started);
272
+ const fileCount = options.files.length;
273
+ // The width the RUN reached, which is the widest pass: a mixed selection whose serial half ran
274
+ // one at a time did not become a one-worker run, and reporting it as one would misname the
275
+ // reproduce a reader is handed.
276
+ const workers = Math.max(1, ...passes.map((pass) => pass.workers));
277
+ const plan = planOf(options, {
278
+ workers,
279
+ ...(options.type === undefined ? {} : { type: options.type }),
280
+ });
281
+ const type = options.type;
282
+ const typeParam = type === undefined ? {} : { type };
283
+ const sample = options.sample;
213
284
  const data: JsonValue = {
214
285
  ...typeParam,
215
286
  workers,
216
- files: files.length,
287
+ files: fileCount,
217
288
  durationMs,
218
289
  ...(options.filter === undefined ? {} : { filter: options.filter }),
219
290
  ...(sample === undefined ? {} : { sample: { kept: sample.kept, total: sample.total } }),
220
291
  ...(only === undefined ? {} : { shard: only }),
221
- ok: result.ok,
222
- exitCode: result.code,
292
+ // Only when the split made more than one, so a single-pass run's JSON is byte-identical to
293
+ // what it has always been — and a mixed one can never be read as if it were a single run.
294
+ ...(spent.length > 1 ? { passes: spent } : {}),
295
+ ok,
296
+ exitCode,
223
297
  reproduce: reproduceFor(plan),
224
298
  };
225
299
  return {
226
- ok: result.ok,
300
+ ok,
227
301
  command: 'test',
228
- summary: result.ok
302
+ summary: ok
229
303
  ? msg(type === undefined ? 'cli.test.pass' : 'cli.test.type.pass', {
230
304
  ...typeParam,
231
- files: files.length,
305
+ files: fileCount,
232
306
  workers,
233
307
  ms: durationMs,
234
308
  })
235
309
  : msg(type === undefined ? 'cli.test.fail' : 'cli.test.type.fail', {
236
310
  ...typeParam,
237
- failed: 1,
311
+ failed: steps.filter((step) => !step.ok).length,
238
312
  workers,
239
313
  }),
240
314
  steps,
@@ -242,6 +316,6 @@ export async function runShards(options: RunShardsOptions): Promise<CommandResul
242
316
  ? {}
243
317
  : { lines: [msg('cli.test.sampled', { ...sample, type: type ?? 'all' })] }),
244
318
  data,
245
- exitCode: result.ok ? 0 : 1,
319
+ exitCode: ok ? 0 : 1,
246
320
  };
247
321
  }
@@ -34,6 +34,7 @@ import type { Finding } from './output';
34
34
  import { findingFrom } from './output';
35
35
  import { checkMigrationDrift } from './schema-drift';
36
36
  import { scanSiteMeta } from './seo-meta';
37
+ import { readStaticReport } from './static-report';
37
38
  import { floorProblemFindings, readVerifyFloor } from './verify-floor';
38
39
  import type { VerifyStep } from './verify-step';
39
40
  import { fromExec, fromFindings, hostFindings } from './verify-step';
@@ -79,7 +80,7 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
79
80
  {
80
81
  name: 'boundaries',
81
82
  summary: "surface, layer and package-tier imports, and the app's own guards",
82
- // An app's `guards/` rides here rather than becoming an eighteenth step, for the reason the
83
+ // An app's `guards/` rides here rather than becoming a step of its own, for the reason the
83
84
  // seam already states: a host adds findings to a step, it can never add, remove, reorder or
84
85
  // skip one — so "green" keeps meaning exactly what it meant. This is the step whose host slot
85
86
  // already carries "rules this repo makes about itself that the framework cannot know" (the
@@ -105,7 +106,7 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
105
106
  name: 'package-shape',
106
107
  summary: 'every package ships the same contract files',
107
108
  applies: (ctx) => hasWorkspacePackages(ctx.root),
108
- // The dependency rule rides here rather than becoming a twentieth step because it is this
109
+ // The dependency rule rides here rather than becoming a step of its own because it is this
109
110
  // step's own question — what does a workspace owe the repo it lives in? — asked of the
110
111
  // manifest's `dependencies` instead of its `files`. It is deliberately NOT inside
111
112
  // `checkPackageShape`: `scripts/release.ts --check` calls that one to ask whether the tree is
@@ -154,7 +155,7 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
154
155
  // Source, not database: the gate runs in CI with nothing listening, and the database half is
155
156
  // the post-migrate verification `runMigrations` performs where a connection is already open.
156
157
  //
157
- // The destructive rail rides here rather than becoming an eighteenth step because it asks this
158
+ // The destructive rail rides here rather than becoming a step of its own because it asks this
158
159
  // step's own question — do the committed migrations still describe what the app is doing to its
159
160
  // schema? — off the same directory, in the same pass, with no database either.
160
161
  applies: async (ctx) => existsSync(join(ctx.root, APP_CONFIG_FILE)),
@@ -165,7 +166,7 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
165
166
  // The third rail, and the one the other two cannot see: a hand-written statement is
166
167
  // recorded by no snapshot and hashed by no source, so both halves above are green over SQL
167
168
  // a squash silently drops. Same directory, same reader, no database — this step's own
168
- // question, which is why it is not an eighteenth step.
169
+ // question, which is why it is not a step of its own.
169
170
  ...(await checkUngeneratableMigrations(ctx.root)),
170
171
  ]),
171
172
  },
@@ -190,7 +191,7 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
190
191
  name: 'budgets',
191
192
  summary:
192
193
  'per-route JS bytes and LCP, the global style layer every document carries, and the routes that boot nothing to receive their live rows',
193
- // The global-style assertion rides here rather than becoming an eighteenth step, because this
194
+ // The global-style assertion rides here rather than becoming a step of its own, because this
194
195
  // step already asks the one question it asks: what does the document this build emits actually
195
196
  // contain? It is also the same app load — `appManifest` fills render's stylesheet registry on
196
197
  // its way through — so a separate step would pay for a second one to answer half a question.
@@ -211,6 +212,11 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
211
212
  // to a hard `X_PRERENDER_FAILED` on `examples/dummy`), and it would be a second builder
212
213
  // beside `apps/web/prerender.ts`, which is where an app reads `SITE_ORIGIN`.
213
214
  const stats = await readBuildStats(ctx.root);
215
+ // The report beside the stats, for the one thing `checkBudgets` reads off it: a route the
216
+ // build rendered and could not weigh because its island was handed props over the cap is
217
+ // reported under `X_ISLAND_PROPS_INVALID` — the build's own sentence, naming the prop and
218
+ // its bytes — and not as an `X_BUDGET_UNMEASURED` whose fix is to go and read this file.
219
+ const report = await readStaticReport(ctx.root);
214
220
  // The load's own findings, FIRST and never dropped. A module that would not import registers
215
221
  // no route, so its budget is missing from the manifest and every route it declared reads as
216
222
  // `X_BUDGET_UNMEASURED` — the symptom, pointing the reader at `x build` for a file that will
@@ -224,7 +230,7 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
224
230
  // JavaScript does this route's document boot? A live read with no island is a route
225
231
  // whose answer is "none", which no suite can fail on — the page renders, at 200.
226
232
  ...(await liveRouteFindings(ctx.root)),
227
- ...checkBudgets(manifest, stats),
233
+ ...checkBudgets(manifest, stats, report?.unmeasured),
228
234
  ]);
229
235
  },
230
236
  },
@@ -289,7 +295,7 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
289
295
  // once, and says so by finding nothing — but `AGENTS.md` is required of every repo the gate
290
296
  // runs in, so the step always has a question to answer and must never report as skipped.
291
297
  //
292
- // `.env.example` joins this step rather than becoming an eighteenth: the question is the same
298
+ // `.env.example` joins this step rather than becoming one of its own: the question is the same
293
299
  // one — "does a committed, generated file still describe the code?" — and the step list is the
294
300
  // definition of shippable, so it grows only when a genuinely new question needs asking.
295
301
  //
@@ -29,19 +29,19 @@ export const VERIFY_STEP_NAMES = [
29
29
  'drift',
30
30
  'contract-diff',
31
31
  'budgets',
32
- // Eighteenth, and a deliberate widening of a closed list rather than a `HostCheck`: an SEO gate
32
+ // A deliberate widening of a closed list rather than a `HostCheck`: an SEO gate
33
33
  // is a MECHANISM every app with a `site/` surface wants (axiom 8), not a rule one host repo
34
34
  // enforces — and `verifyCommand.run` passes no host checks at all, so the app path could not
35
35
  // have carried it. It runs beside `budgets` because both read the app the same load produced.
36
36
  'seo',
37
- // Nineteenth, by the same test the SEO step above passed and for the same reason it is not a
38
- // rider: `boundaries` asks whether an import was LEGAL and this asks whether a declaration
37
+ // Here by the same test the SEO step above passed, and not a rider for the same reason:
38
+ // `boundaries` asks whether an import was LEGAL and this asks whether a declaration
39
39
  // REACHED the running app, which is a different question with a different fix (axiom 4). It
40
40
  // costs no second app load — `budgets` already imported every module, and this reads the
41
41
  // registries that load filled. Until it existed, an app could ship every user-facing string as
42
42
  // `⟦key⟧` with `x verify` green, because nothing in the gate ever asked (issue #249).
43
43
  'i18n',
44
- // Twentieth, by the same test `seo` and `i18n` each passed: a rider must ask the SAME question
44
+ // Here by the same test `seo` and `i18n` each passed: a rider must ask the SAME question
45
45
  // off the same data, and "was this import legal?" is not "does the permission this app grants
46
46
  // and requires exist?". Reported under `budgets` it would hand the reader a byte budget for an
47
47
  // authz defect (axiom 4). Until it existed, `x new` shipped an app that answered HTTP 500 with
@@ -36,13 +36,28 @@ type TypedTest = Exclude<TestType, 'unit'>;
36
36
 
37
37
  const TYPED_SUFFIXES = '{contract,live,job,e2e,eval}';
38
38
 
39
- const SUMMARIES: Readonly<Record<TypedTest, string>> = {
40
- contract: 'action/query schemas, policy denials, emitted OpenAPI and MCP shapes',
41
- live: 'live-query snapshots, incremental patches, reconnect deltas',
42
- job: 'step replay, idempotency dedupe, retry/backoff, outbox atomicity',
43
- e2e: 'the built output, incl. offline and SW update',
44
- eval: 'LLM output scored against thresholds',
45
- };
39
+ /**
40
+ * `Object.create(null)`, not a `{}` literal, because `stepFor` reads it with a COMPUTED key
41
+ * (`SUMMARIES[type]`). On a normal object literal every `Object.prototype` member reads back as
42
+ * present, so a table read that way answers a function instead of `undefined` for a key nobody
43
+ * declared — the defect `scripts/proto-index.ts` exists to keep out, thirteen instances across
44
+ * four sweeps. `type` is a closed union here and cannot be `'constructor'` today, which is
45
+ * exactly the argument every one of those thirteen had before it stopped being true.
46
+ *
47
+ * Same repair, and the same reason, as `packages/i18n/src/catalog.ts`. The read itself is guarded
48
+ * too (`summaryOf`), because a construction three screens above a read is not something a static
49
+ * rule should have to reason about.
50
+ */
51
+ const SUMMARIES: Readonly<Record<TypedTest, string>> = Object.assign(
52
+ Object.create(null) as Record<TypedTest, string>,
53
+ {
54
+ contract: 'action/query schemas, policy denials, emitted OpenAPI and MCP shapes',
55
+ live: 'live-query snapshots, incremental patches, reconnect deltas',
56
+ job: 'step replay, idempotency dedupe, retry/backoff, outbox atomicity',
57
+ e2e: 'the built output, incl. offline and SW update',
58
+ eval: 'LLM output scored against thresholds',
59
+ },
60
+ );
46
61
 
47
62
  /**
48
63
  * Every rule that decides a file's type, MOST SPECIFIC FIRST: the first entry a path matches owns
@@ -242,6 +257,16 @@ const evalStep: VerifyStep = {
242
257
  },
243
258
  };
244
259
 
260
+ /**
261
+ * The one computed read of `SUMMARIES`, guarded. The table is already prototype-free
262
+ * (`Object.create(null)`), which is what makes the READ safe — and `scripts/proto-index.ts`
263
+ * recognises a guard on the read itself, not a construction three screens above it. So the guard
264
+ * is here: a rule that has to reason about how a table was built is one tightening away from
265
+ * reporting this line, and a pin is a rule with a hole in it.
266
+ */
267
+ const summaryOf = (type: TypedTest): string =>
268
+ Object.hasOwn(SUMMARIES, type) ? SUMMARIES[type] : `${type} tests`;
269
+
245
270
  const stepFor = (type: TestType): VerifyStep => {
246
271
  if (type === 'unit') {
247
272
  return {
@@ -253,7 +278,7 @@ const stepFor = (type: TestType): VerifyStep => {
253
278
  if (type === 'eval') return evalStep;
254
279
  return {
255
280
  name: type,
256
- summary: SUMMARIES[type],
281
+ summary: summaryOf(type),
257
282
  applies: async (ctx) => (await filesFor(ctx.root, type)).length > 0,
258
283
  run: (ctx) => runType(ctx, type),
259
284
  };
@@ -0,0 +1,22 @@
1
+ // How a process binds its sockets, and what it admits about itself. A LEAF: it imports nothing,
2
+ // so every role can read it without pulling `dev-roles` — which is what made this its own file.
3
+ // `dev-sync` needs the default and `dev-roles` already imports `dev-sync`, so reading it from
4
+ // there would be a runtime import cycle in the framework's own boot path.
5
+
6
+ export interface WebBinding {
7
+ readonly dev: boolean;
8
+ /**
9
+ * The interface every socket this process opens binds to — the web role, the metrics endpoint
10
+ * and the `sync` node alike. ONE value, because they are one decision: a process that serves
11
+ * its app on loopback and its live-query patch stream on `0.0.0.0` has not bound to loopback,
12
+ * it has just moved which port the exposure is on.
13
+ */
14
+ readonly hostname: string;
15
+ }
16
+
17
+ /**
18
+ * Loopback and dev-mode. What `x dev` means, and what a container must override — a process bound
19
+ * to `localhost` inside a container is unreachable from the port mapping, the load balancer and
20
+ * every PaaS health probe, which is the same failure in four costumes.
21
+ */
22
+ export const DEV_BINDING: WebBinding = { dev: true, hostname: 'localhost' };