@jterrazz/test 17.0.0 → 18.0.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 (53) hide show
  1. package/dist/browser/index.js +1 -2
  2. package/dist/browser/services.js +1 -2
  3. package/dist/browser/stubs.js +1 -2
  4. package/dist/{shared-index.browser.js → browser-shared-index.browser.js} +82 -40
  5. package/dist/checker.js +15 -824
  6. package/dist/in-place.global-setup.d.ts +1 -0
  7. package/dist/in-place.global-setup.js +1 -1
  8. package/dist/index.d.ts +2 -14
  9. package/dist/index.js +83 -204
  10. package/dist/mobile-session.global-setup.d.ts +1 -0
  11. package/dist/oxlint.cjs +2 -2
  12. package/dist/oxlint.js +67 -2321
  13. package/dist/route-coverage.global-setup.d.ts +1 -0
  14. package/dist/route-coverage.global-setup.js +1 -2
  15. package/dist/services.global-setup.d.ts +1 -1
  16. package/dist/services.global-setup.js +1 -2
  17. package/dist/services.js +1 -1
  18. package/dist/shared-appium.adapter.js +14 -14
  19. package/dist/shared-artifacts.js +7 -5
  20. package/dist/shared-derived-projects.js +23 -8
  21. package/dist/shared-in-place.js +1 -1
  22. package/dist/shared-ledger.js +1 -1
  23. package/dist/shared-listened-port.js +19 -0
  24. package/dist/shared-literate-plugin.js +2 -1
  25. package/dist/shared-pause.js +12 -1
  26. package/dist/shared-playwright.adapter.js +17 -19
  27. package/dist/shared-process-alive.js +13 -0
  28. package/dist/shared-record-key.js +13 -0
  29. package/dist/shared-serve.js +180 -69
  30. package/dist/shared-services.global-setup.js +4 -9
  31. package/dist/shared-session-ledger.js +10 -6
  32. package/dist/shared-spec-document.js +2 -64
  33. package/dist/shared-spec-grammar.js +66 -0
  34. package/dist/shared-specs-modules.js +1033 -0
  35. package/dist/{shared-stream-body.js → shared-stub-reply.js} +40 -3
  36. package/dist/shared-stub-server.js +30 -78
  37. package/dist/shared-temp-root.global-setup.js +3 -10
  38. package/dist/shared-testcontainers.adapter.js +111 -8
  39. package/dist/temp-root.global-setup.js +1 -2
  40. package/dist/vitest.d.ts +17 -22
  41. package/dist/vitest.js +37 -53
  42. package/docs/concepts/architecture.md +20 -20
  43. package/docs/concepts/conventions.md +2 -2
  44. package/docs/guides/cli-specs.md +2 -0
  45. package/docs/guides/develop.md +20 -17
  46. package/docs/guides/mobile-specs.md +3 -3
  47. package/docs/guides/release.md +4 -3
  48. package/docs/guides/website-specs.md +1 -1
  49. package/docs/reference/linting.md +2 -2
  50. package/docs/reference/services.md +13 -9
  51. package/docs/reference/stubs.md +4 -2
  52. package/docs/start/set-up-a-project.md +31 -17
  53. package/package.json +10 -10
package/dist/vitest.js CHANGED
@@ -1,20 +1,20 @@
1
- import { a as callerFrames, r as writeInPlace } from "./shared-in-place.js";
1
+ import { r as writeInPlace } from "./shared-in-place.js";
2
2
  import { t as isRecord } from "./shared-json-shape.js";
3
3
  import { n as refusal$1, t as invariant } from "./shared-refusal.js";
4
4
  import { i as renderText, n as literate, o as resolveSpecPath } from "./shared-literate-plugin.js";
5
5
  import { u as shouldUpdateSnapshots } from "./shared-ledger.js";
6
- import { i as COVERAGE_DIR, l as VITEST_ARTIFACTS_DIR, n as ATTACHMENTS_DIR, o as REPORTER_OUTPUT_FILES, r as BLOB_DIR, s as SCREENSHOTS_DIR, t as ARTIFACTS_DIR } from "./shared-artifacts.js";
6
+ import { c as SCREENSHOTS_DIR, i as COVERAGE_DIR, n as ATTACHMENTS_DIR, o as NODE_COMPILE_CACHE_DIR, r as BLOB_DIR, s as REPORTER_OUTPUT_FILES, t as ARTIFACTS_DIR, u as VITEST_ARTIFACTS_DIR } from "./shared-artifacts.js";
7
7
  import { t as derivedProjects } from "./shared-derived-projects.js";
8
8
  import { t as TEST_SECTION } from "./shared-test-section.js";
9
- import { r as servicesSetup } from "./shared-services.global-setup.js";
9
+ import { n as servicesSetup } from "./shared-services.global-setup.js";
10
10
  import { r as tempRootSetup } from "./shared-temp-root.global-setup.js";
11
11
  import { inPlaceSetup } from "./in-place.global-setup.js";
12
12
  import { t as routeCoverageSetup } from "./shared-route-coverage.global-setup.js";
13
13
  import { mobileSessionSetup } from "./mobile-session.global-setup.js";
14
14
  import { createRequire } from "node:module";
15
- import { GROUND_GLOB, readSettings } from "@jterrazz/typescript/rules";
15
+ import { GROUND_GLOB, readSettings, slotsFor } from "@jterrazz/typescript/rules";
16
16
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
17
- import { basename, dirname, isAbsolute, resolve } from "node:path";
17
+ import { dirname, isAbsolute, resolve } from "node:path";
18
18
  import { fileURLToPath } from "node:url";
19
19
  import { configDefaults, mergeConfig } from "vitest/config";
20
20
  //#region src/vitest/project.ts
@@ -66,7 +66,9 @@ function onProjectDefaults(stated) {
66
66
  test: statedOnce(merged.test)
67
67
  };
68
68
  }
69
- /** A stated `include` or `exclude` replaces the canonical one; the group is 0 until a specification states its own. */
69
+ const PARALLEL = { groupOrder: 0 };
70
+ const SERIAL = { groupOrder: 1 };
71
+ /** A stated `include` or `exclude` replaces the canonical one; a project shares group 0 with every other, a serial one runs after them. */
70
72
  function projectTest(name, options, canonical) {
71
73
  const exclude = options.exclude ?? canonical.exclude;
72
74
  return {
@@ -74,7 +76,7 @@ function projectTest(name, options, canonical) {
74
76
  include: options.include ?? canonical.include,
75
77
  ...options.serial === true ? { fileParallelism: false } : {},
76
78
  name,
77
- sequence: { groupOrder: 0 },
79
+ sequence: options.serial === true ? SERIAL : PARALLEL,
78
80
  ...options.timeout === void 0 ? {} : {
79
81
  hookTimeout: options.timeout,
80
82
  testTimeout: options.timeout
@@ -84,28 +86,34 @@ function projectTest(name, options, canonical) {
84
86
  function specificationProject(specification, options) {
85
87
  return onProjectDefaults({ test: projectTest(specification, options, { include: [`specs/${specification}/**/*.spec.ts`] }) });
86
88
  }
87
- function inProcessProject(specification, options) {
89
+ /** The modules a stated runner names, one or several. */
90
+ const runnersOf = (runner) => typeof runner === "string" ? [runner] : [...runner];
91
+ function runnerProject(specification, options) {
88
92
  const project = specificationProject(specification, options);
89
93
  return options.runner === void 0 ? project : mergeConfig(project, { test: {
90
94
  isolate: false,
91
- setupFiles: [options.runner]
95
+ setupFiles: runnersOf(options.runner)
92
96
  } });
93
97
  }
94
98
  //#endregion
95
99
  //#region src/specifications/api/api.project.ts
96
100
  function api(options = {}) {
97
- return mergeConfig(inProcessProject("api", options), { test: { globalSetup: [servicesSetup(), routeCoverageSetup()] } });
101
+ return mergeConfig(runnerProject("api", options), { test: { globalSetup: [servicesSetup(), routeCoverageSetup()] } });
98
102
  }
99
103
  //#endregion
100
104
  //#region src/specifications/cli/cli.project.ts
105
+ function runnerBeside(common, documents) {
106
+ const { runner, ...isolated } = common;
107
+ return runner !== void 0 && runnersOf(runner).some((module) => resolve(module) === resolve(documents)) ? common : isolated;
108
+ }
101
109
  /** A cli project: the specification's `.spec.ts` files and its documents (docs/guides/spec-documents.md); the plugin's glob joins this project's include, never the root config. */
102
110
  function cli(options = {}) {
103
111
  const { literate: door, ...common } = options;
104
- const project = specificationProject("cli", common);
105
- if (door === false) return project;
106
- return mergeConfig(project, { plugins: [literate({
112
+ if (door === false) return runnerProject("cli", common);
113
+ const documents = door?.runner ?? "./specs/cli/cli.specification.ts";
114
+ return mergeConfig(runnerProject("cli", runnerBeside(common, documents)), { plugins: [literate({
107
115
  include: door?.include ?? ["specs/cli/**/*.spec.yaml"],
108
- runner: door?.runner ?? "./specs/cli/cli.specification.ts"
116
+ runner: documents
109
117
  })] });
110
118
  }
111
119
  //#endregion
@@ -220,27 +228,6 @@ const PAGE_DEPENDENCIES = [
220
228
  "vitest-mock-extended"
221
229
  ];
222
230
  const requireFrom = createRequire(import.meta.url);
223
- const HELPERS_FILE = import.meta.filename;
224
- const BUNDLED_CONFIG = /^(?<name>.+)\.timestamp-[^.]+\.mjs$/u;
225
- function callerFile() {
226
- return callerFrames().find(({ file }) => file !== HELPERS_FILE)?.file;
227
- }
228
- function nearestHolding(name) {
229
- let directory = process.cwd();
230
- for (;;) {
231
- if (existsSync(resolve(directory, name))) return directory;
232
- const parent = dirname(directory);
233
- if (parent === directory) return;
234
- directory = parent;
235
- }
236
- }
237
- function callerDirectory() {
238
- const file = callerFile();
239
- if (file === void 0) return process.cwd();
240
- const bundled = BUNDLED_CONFIG.exec(basename(file))?.groups?.name;
241
- if (bundled === void 0 || basename(dirname(file)) !== ".vite-temp") return dirname(file);
242
- return nearestHolding(bundled) ?? process.cwd();
243
- }
244
231
  function resolves(specifier) {
245
232
  try {
246
233
  requireFrom.resolve(specifier);
@@ -344,8 +331,8 @@ function viewportOf(options) {
344
331
  };
345
332
  }
346
333
  /** A component project: renders in a real Chromium; the default `exclude` keeps it out of `specs/`. */
347
- async function component(options = {}) {
348
- const root = options.root ?? callerDirectory();
334
+ async function component(options) {
335
+ const { root } = options;
349
336
  assertProviderPin(versionOf("vitest"), installedVersion("@vitest/browser-playwright"));
350
337
  const { playwright } = await import("@vitest/browser-playwright");
351
338
  const pipeline = options.vite === void 0 ? {} : await reroot(options.vite, root);
@@ -374,20 +361,19 @@ async function component(options = {}) {
374
361
  include: ["**/*.test.tsx"]
375
362
  }),
376
363
  provide: providedToPage(options),
377
- ...options.wrap === void 0 ? {} : { setupFiles: [writeSetupFile(options.wrap, root)] },
378
- sequence: { groupOrder: 2 }
364
+ ...options.wrap === void 0 ? {} : { setupFiles: [writeSetupFile(options.wrap, root)] }
379
365
  }
380
366
  });
381
367
  }
382
368
  //#endregion
383
369
  //#region src/specifications/integration/integration.project.ts
384
370
  function integration(options = {}) {
385
- return mergeConfig(inProcessProject("integration", options), { test: { globalSetup: [servicesSetup()] } });
371
+ return mergeConfig(runnerProject("integration", options), { test: { globalSetup: [servicesSetup()] } });
386
372
  }
387
373
  //#endregion
388
374
  //#region src/specifications/job/job.project.ts
389
375
  function job(options = {}) {
390
- return mergeConfig(inProcessProject("job", options), { test: { globalSetup: [servicesSetup()] } });
376
+ return mergeConfig(runnerProject("job", options), { test: { globalSetup: [servicesSetup()] } });
391
377
  }
392
378
  //#endregion
393
379
  //#region src/specifications/lint/lint.project.ts
@@ -396,23 +382,24 @@ function lint(options = {}) {
396
382
  }
397
383
  //#endregion
398
384
  //#region src/specifications/mobile/mobile.project.ts
399
- /** A simulator cannot share a machine with itself: serial. A global setup holds one driver session for the run. */
385
+ /** A simulator cannot share a machine with itself: serial, unless `serial: false` states a server that gives every session an app of its own. A global setup holds the run's driver sessions, one per worker. */
400
386
  function mobile(options = {}) {
401
387
  const { device, root, ...common } = options;
402
- return mergeConfig(specificationProject("mobile", common), { test: {
403
- fileParallelism: false,
388
+ return mergeConfig(specificationProject("mobile", {
389
+ ...common,
390
+ serial: common.serial !== false
391
+ }), { test: {
404
392
  globalSetup: [mobileSessionSetup()],
405
393
  ...device === void 0 ? {} : { provide: { mobileAppium: {
406
394
  device,
407
395
  root: root ?? "."
408
- } } },
409
- sequence: { groupOrder: 3 }
396
+ } } }
410
397
  } });
411
398
  }
412
399
  //#endregion
413
400
  //#region src/specifications/website/website.project.ts
414
401
  function website(options = {}) {
415
- return mergeConfig(specificationProject("website", options), { test: { sequence: { groupOrder: 1 } } });
402
+ return runnerProject("website", options);
416
403
  }
417
404
  //#endregion
418
405
  //#region src/vitest/preset.ts
@@ -454,6 +441,8 @@ function reporterOutputFiles(argv) {
454
441
  function rootDefaults() {
455
442
  return mergeConfig(projectDefaults(), { test: {
456
443
  coverage: COVERAGE,
444
+ env: { NODE_COMPILE_CACHE: resolve(NODE_COMPILE_CACHE_DIR) },
445
+ maxWorkers: slotsFor(resolve("."), "vitest"),
457
446
  outputFile: reporterOutputFiles(process.argv)
458
447
  } });
459
448
  }
@@ -499,11 +488,6 @@ function unit(options = {}) {
499
488
  }
500
489
  //#endregion
501
490
  //#region src/index.vitest.ts
502
- /** The shared vitest config: what you pass is merged over the defaults. */
503
- function defineSpecConfig(config = {}) {
504
- const { literate: literateOptions, ...userConfig } = config;
505
- return presetConfig(literateOptions ? mergeConfig({ plugins: [literate(literateOptions)] }, userConfig) : userConfig);
506
- }
507
491
  const BUILDERS = /* @__PURE__ */ new Map([
508
492
  ["unit", unit],
509
493
  ["lint", lint],
@@ -595,6 +579,6 @@ async function derivedFrom(root) {
595
579
  }
596
580
  const vitestConfig = async (configUrl) => await derivedFrom(dirname(fileURLToPath(configUrl)));
597
581
  //#endregion
598
- export { api, cli, component, defineSpecConfig, integration, job, lint, literate, mobile, unit, vitestConfig, website };
582
+ export { api, cli, component, integration, job, lint, literate, mobile, unit, vitestConfig, website };
599
583
 
600
584
  //# sourceMappingURL=vitest.js.map
@@ -36,22 +36,22 @@ Every seam is opened lazily rather than imported, and each one is an optional pe
36
36
 
37
37
  The code is trees with declared, one-directional edges: the product under `src/`, and `lint/` at the repository root beside it, the rules the package ships (the spine: `src/`, `lint/`, `specs/`, `docs/`, `skills/`). The map is the project's to declare, so this package declares its own as `FRAMEWORK_LAYERS` in `lint/self/checks/layers.ts`; `lint/self/checks/architecture.ts` compiles it, with the library owners, the specification faces and the direction of `lint/`, into the dependency-cruiser rules `jterrazz.config.ts` hands the toolchain's Architecture pass, and that declaration is the enforced statement of what follows, each crossing named at its import.
38
38
 
39
- The root reads the sentence of a test. A test opens a **specification** on its subject, sets up what **surrounds** it (the stubs, the services, the disk, the clock, the doubles), has a **visitor** act on it, and judges the result with **expectations**; the **chain** is the machinery that composes all of it, and **vitest** the runner that runs it. Each word is one tree at the root of `src/`, beside the three entries and their tests in `__tests__/`, and a lock of this repository's own holds the list closed.
40
-
41
- | Tree | May import | Holds |
42
- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
43
- | `chain/` | itself, `lint/kit/`; `paths/` reaches `vitest` | The machinery that composes a specification and knows none: at its root `defineSpecification`, the setup and action contracts, `perform`, the chain state and config, the computed chain type and `pause.ts` (the one wait); `context/` (`.headers()` and `.with()`, what a file shares), `paths/` (the stack reader, where a global setup sits, scratch and temp roots), `refusals/` |
44
- | `visitor/` | `chain/`, `lint/kit/`, itself | What acts on a screen, one part per section of [The visitor](../reference/visitor.md): `verbs.ts` (`VisitorVerbs`, the verbs every surface shares), `targets/` (the `Target` type, the roles, the landmarks with `within()` and the scope walk, the modifiers, how a target is spelled in messages), `matching/` (each candidate the refusal lists, the tag-to-landmark table, and `ambiguous.ts`, the refusal of a target matching several), `matrix.ts` (which verb and target each surface offers) |
45
- | `expectations/` | `chain/`, `lint/kit/`, itself, `vitest`; `result.ts` reaches `services/database-option`, the goldens reach how `disk/` names and resolves a path | How a result is judged: the base result and its accessors (`text/`, `disk/`, `table/`, `http/`, `command/`), the goldens and matchers, the `{{token}}` engine (`tokens/`), update mode (`in-place/`), `required` and `waitUntil` (`conditions/`) |
46
- | `stubs/` | `chain/`, `lint/kit/`, itself, `msw`; a stub's request filter reaches `expectations/tokens/`, one `match.*` grammar | What the outside world replies: public at its top, `stub.ts`, `exchange.ts`, the builders (`kinds/`), the intercept setup and scope (`intercept/`); internal below, the queue and the route grammar (`matching/`), the three deliveries and what they share (`delivery/`: `same-process/` msw's server, `browser-page/` msw's worker, `other-process/` the stub backend; at its root msw's handlers and the stream body on the wire) |
47
- | `services/` | `chain/`, `lint/kit/`, itself, each service its own library, `vitest`; `.seed()` reaches how `disk/` names and resolves a path | The real infrastructure a run starts: the services record, postgres, redis, sqlite, processes, containers and their startup, seeding, and `database-option.ts` (`database-named-only-when-several-at-runtime`'s check, shared by `.seed()` and `.table()`) |
48
- | `disk/` | `chain/`, `lint/kit/`, itself | How a test names the disk and what a chain lays on it before its action: `file()` and `directory()` (`spec-path.ts`) and their resolution from the spec file (`resolved-spec-path.ts`), `.fixture()` and `.link()`, their ports and the file-system adapters behind them |
49
- | `clock/` | `chain/`, `lint/kit/`, itself, `vitest` | The pinned clock and `.clock()`, the instant grammar, the process clock adapter |
50
- | `mocks/` | `chain/`, `lint/kit/`, itself, `vitest`, `vitest-mock-extended` | `mockOf()`, the one double of a port |
51
- | `specifications/` | `chain/`, every notion above, `lint/kit/`, each specification its own library; a specification's project reaches the runner config (`vitest/project`) | What is tested, one slice per constructor: `<specification>.face.ts` (the names it publishes), `<specification>.composition.ts` (the composition), `<specification>.handle.ts` (the constructor), `<specification>.project.ts` (its Vitest project), and its concept folders (`request/`, `server/`, `routes/`, `documents/`, `render/`, `device/` ...); the lint specification holds `rule-tester.ts` beside them, `ruleTester()`, the other half of the kit to test a rule; at its root, `specification.ts`, the seven as one record |
52
- | `vitest/` | itself, `chain/`, `lint/kit/`, update mode's setup, `vitest`, never a specification | What every Vitest project receives, knowing no specification: `preset.ts` (the root config), `project.ts` (the defaults every project is built on, `specificationProject`) and `unit.project.ts` (`unit()`), which wire the global setups their owners provide and resolve (`expectations/in-place/`, `chain/paths/temp-root`); a specification's own setups (`services/containers/`, `specifications/mobile/session/`) ride on its project |
53
- | `standard/` | `@jterrazz/typescript/config`, nothing of the tree | The defaults of the `test` section, data only: `standard.ts`, which the `test()` preset hands on as written ([`@jterrazz/typescript`'s Configuration § The standard](https://github.com/jterrazz/package-typescript/blob/main/docs/reference/configuration.md#the-standard)) |
54
- | `lint/` | itself and `src/standard/`, never the rest of `src/`: `self/` alone reads the product it holds to account | At the root, not under `src/`: the tool-facing channel, laid out below. The layer map names its files `lint/**`, and cruises it beside `src/` |
39
+ The root reads the sentence of a test. A test opens a **specification** on its subject, sets up what **surrounds** it (the stubs, the services, the disk, the clock, the doubles), has a **visitor** act on it, and judges the result with **expectations**; the **chain** is the machinery that composes all of it, and **vitest** the runner that runs it. Each word is one tree at the root of `src/`, beside the seven entry files (`index.ts`, `index.services.ts` and `index.stubs.ts`, each with its `.browser.ts` twin, and `index.vitest.ts`) and their tests in `__tests__/`, and a lock of this repository's own holds the list closed.
40
+
41
+ | Tree | May import | Holds |
42
+ | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
43
+ | `chain/` | itself, `lint/kit/`; `paths/` reaches `vitest` | The machinery that composes a specification and knows none: at its root `defineSpecification`, the setup and action contracts, `perform`, the chain state and config, the computed chain type, `pause.ts` (the one wait, and `untilDeadline`, the one deadline poll), `process-alive.ts` (whether a pid still runs) and `listened-port.ts` (the port a server got); `context/` (`.headers()` and `.with()`, what a file shares), `paths/` (the stack reader, where a global setup sits, scratch and temp roots), `refusals/` |
44
+ | `visitor/` | `chain/`, `lint/kit/`, itself | What acts on a screen, one part per section of [The visitor](../reference/visitor.md): `verbs.ts` (`VisitorVerbs`, the verbs every surface shares), `targets/` (the `Target` type, the roles, the landmarks with `within()` and the scope walk, the modifiers, how a target is spelled in messages), `matching/` (each candidate the refusal lists, the tag-to-landmark table, and `ambiguous.ts`, the refusal of a target matching several), `matrix.ts` (which verb and target each surface offers) |
45
+ | `expectations/` | `chain/`, `lint/kit/`, itself, `vitest`; `result.ts` reaches `services/database-option`, the command accessors `services/containers/docker-cli`, the goldens reach how `disk/` names and resolves a path | How a result is judged: the base result and its accessors (`text/`, `disk/`, `table/`, `http/`, `command/`), the goldens and matchers, the `{{token}}` engine (`tokens/`), update mode (`in-place/`), `required` and `waitUntil` (`conditions/`) |
46
+ | `stubs/` | `chain/`, `lint/kit/`, itself, `msw`; a stub's request filter reaches `expectations/tokens/`, one `match.*` grammar | What the outside world replies: public at its top, `stub.ts`, `exchange.ts`, the builders (`kinds/`), the intercept setup and scope (`intercept/`); internal below, the queue and the route grammar (`matching/`), the three deliveries and what they share (`delivery/`: `same-process/` msw's server, `browser-page/` msw's worker, `other-process/` the stub backend; at its root msw's handlers, the stream body on the wire, and the reply every delivery shares: a request's body as a filter reads it, a stub's reply after its delay, the one 501 a refused request is answered) |
47
+ | `services/` | `chain/`, `lint/kit/`, itself, each service its own library, `vitest`; `.seed()` reaches how `disk/` names and resolves a path | The real infrastructure a run starts: the services record, postgres, redis, sqlite, processes, containers and their startup with `docker-cli.ts`, the one module that runs `docker` (whether a daemon answers, and the reads of the containers a subject started), seeding, and `database-option.ts` (`database-named-only-when-several-at-runtime`'s check, shared by `.seed()` and `.table()`) |
48
+ | `disk/` | `chain/`, `lint/kit/`, itself | How a test names the disk and what a chain lays on it before its action: `file()` and `directory()` (`spec-path.ts`) and their resolution from the spec file (`resolved-spec-path.ts`), `.fixture()` and `.link()`, their ports and the file-system adapters behind them |
49
+ | `clock/` | `chain/`, `lint/kit/`, itself, `vitest` | The pinned clock and `.clock()`, the instant grammar, the process clock adapter |
50
+ | `mocks/` | `chain/`, `lint/kit/`, itself, `vitest`, `vitest-mock-extended` | `mockOf()`, the one double of a port |
51
+ | `specifications/` | `chain/`, every notion above, `lint/kit/`, each specification its own library; a specification's project reaches the runner config (`vitest/project`) | What is tested, one slice per constructor: `<specification>.face.ts` (the names it publishes), `<specification>.composition.ts` (the composition), `<specification>.handle.ts` (the constructor), `<specification>.project.ts` (its Vitest project), and its concept folders (`request/`, `server/`, `routes/`, `documents/`, `render/`, `device/` ...); the lint specification holds `rule-tester.ts` beside them, `ruleTester()`, the other half of the kit to test a rule; at its root, `specification.ts`, the seven as one record |
52
+ | `vitest/` | itself, `chain/`, `lint/kit/`, update mode's setup, `vitest`, never a specification | What every Vitest project receives, knowing no specification: `preset.ts` (the root config), `project.ts` (the defaults every project is built on, `specificationProject`) and `unit.project.ts` (`unit()`), which wire the global setups their owners provide and resolve (`expectations/in-place/`, `chain/paths/temp-root`); a specification's own setups (`services/containers/`, `specifications/mobile/session/`) ride on its project |
53
+ | `standard/` | `@jterrazz/typescript/config`, nothing of the tree | The defaults of the `test` section, data only: `standard.ts`, which the `test()` preset hands on as written ([`@jterrazz/typescript`'s Configuration § The standard](https://github.com/jterrazz/package-typescript/blob/main/docs/reference/configuration.md#the-standard)) |
54
+ | `lint/` | itself and `src/standard/`, never the rest of `src/`: `self/` alone reads the product it holds to account | At the root, not under `src/`: the tool-facing channel, laid out below. The layer map names its files `lint/**`, and cruises it beside `src/` |
55
55
 
56
56
  **`lint/` is split by audience first.**
57
57
 
@@ -60,7 +60,7 @@ The root reads the sentence of a test. A test opens a **specification** on its s
60
60
  | `index.oxlint.ts` | what ships at `./oxlint`, where `consumer/` meets `kit/` for the plugin: `recommendedRules`, the plugin as the default export, and `materialAt`/`testPackageOf` |
61
61
  | `index.config.ts` | what ships at `./config`: `test()`, the one preset this package hands a project (its section's defaults from `src/standard/`, its oxlint rules, the import and naming rules it hands the toolchain, the sections of `.jterrazz/baseline.json` it owns), and the `TestSettings` type its section merges into `@jterrazz/typescript/config` |
62
62
  | `index.checker.ts` | the `#checker` private import, where `consumer/` meets `kit/` for the conventions: the `Contribution` the `test()` preset names as its checks (the conventions pass `kit/checker/` builds from the registry of `consumer/checks/checker.ts`, the `test` section it holds, and its `baseline` command), which `@jterrazz/typescript`'s checker program runs on `typescript check`, `fix` and `baseline` |
63
- | `consumer/` | the declarations every consumer is held to, and nothing else: `catalogue/` (one row per rule in `manifest.ts`, its family in `families.ts`, the chapter's markers in `chapter.ts`, the fork), `rulebook/` (the `test()` preset, and the overrides it sets on upstream rules, one file per upstream plugin), `rules/` (our oxlint rules, `rules/<family>/<name>.ts` beside its test, assembled by `plugin.ts`), `checks/` (one file per checker pass, listed by `checker.ts`, the registry of the tree, ground, document and member passes and of the fixes a `--fix` applies) |
63
+ | `consumer/` | the declarations every consumer is held to, and nothing else: `catalogue/` (one row per rule in `manifest.json`, read by `manifest.ts`, its family in `families.ts`, the chapter's markers in `chapter.ts`, the fork), `rulebook/` (the `test()` preset, and the overrides it sets on upstream rules, one file per upstream plugin), `rules/` (our oxlint rules, `rules/<family>/<name>.ts` beside its test, assembled by `plugin.ts`), `checks/` (one file per checker pass, listed by `checker.ts`, the registry of the tree, ground, document and member passes and of the fixes a `--fix` applies) |
64
64
  | `self/` | the declarations only this repository is held to, never shipped: `catalogue/` (the self rows, the self chapter), `rules/`, `checks/` (the layer map and the rules it compiles to, the library owners, the source locks, and `checker.ts`, their registry), `corpus/` (the meta-tests), and `self.cli.ts`, which composes them with the kit (the self checks the `self()` preset names and the generated chapters), beside `preset.ts`, the `self()` preset, and `plugin.ts`, its plugin's entry |
65
65
  | `kit/` | what this package alone runs, on top of the rules kit it reads from `@jterrazz/typescript/rules`. The lesson, the rule rows, the chapter tables, the baseline sections and their judges, the git tree, the members, the ground folders and the suppression directives are the kit's: `project/` (how a rule or a pass reads a spec tree: the module parser, the file role, the subject, the siblings, the spec documents, the specs roots), `baseline/` (the `test` section of `.jterrazz/baseline.json`: the checker's counts, the route key the run keeps), `checker/` (what a check spots, taught into the kit's finding by its row, the oxlint run, the rewrites a fix makes, and the passes handed a registry for the kit's `runChecker`: the tree, ground, document and member runs, the conventions pass and its `baseline` command, the self pass held at zero), `catalogue/` (the row's channels, specifications and reaches, the catalogue chapter those render into, and its page) |
66
66
 
@@ -93,7 +93,7 @@ A convention this package states is held by a **channel**, and every row of the
93
93
  - **meta:** what the framework's own suite holds about itself: the catalogue's freshness, its completeness, the reach of its rules. [Test](../guides/test.md) owns the channel.
94
94
  - **process:** the judgement no single channel settles: asset-driven grouping, golden-file discipline, retro-propagation.
95
95
 
96
- The channel a rule sits on is not prose here: it is a field of `lint/consumer/catalogue/manifest.ts`, the single source of truth from which the catalogue in [Linting](../reference/linting.md) is generated. So is its audience: a `consumer` rule is what a project using the package meets and is listed there, a `self` rule holds only this package's own suite, has its row in `lint/self/catalogue/manifest.ts`, and is listed in [Test § The package's own rules](../guides/test.md#the-packages-own-rules). A rule and its normative sentence live together in the code, so the two cannot drift.
96
+ The channel a rule sits on is not prose here: it is a field of a rule's row in `lint/consumer/catalogue/manifest.json`, which `lint/consumer/catalogue/manifest.ts` reads, the single source of truth from which the catalogue in [Linting](../reference/linting.md) is generated. So is its audience: a `consumer` rule is what a project using the package meets and is listed there, a `self` rule holds only this package's own suite, has its row in `lint/self/catalogue/manifest.ts`, and is listed in [Test § The package's own rules](../guides/test.md#the-packages-own-rules). A rule and its normative sentence live together in the code, so the two cannot drift.
97
97
 
98
98
  ## What the tree publishes
99
99
 
@@ -103,7 +103,7 @@ The bundle is built by `typescript build`, from the manifest and the names of th
103
103
 
104
104
  **Each spec entry has two runtimes.** A component test renders in a real browser, and the node entry cannot load there: it imports node builtins at load (`node:fs` and `node:child_process` among them), while the native seams (`pg`, `better-sqlite3`, `testcontainers`) are lazy peers, opened on first use. So `exports["."]`, `exports["./services"]` and `exports["./stubs"]` each carry a `browser` condition, resolving to `dist/browser/index.js`, `dist/browser/services.js` and `dist/browser/stubs.js`: the same public surface, built for a page, where every node-only name (the seven constructors, `ruleTester()`, the services, the docker accessors, the results that walk a disk) is a stub that throws where it was called, naming the specification and saying it runs under node. The split is the RUNTIME's and never the specifications': one specifier per entry, so no spec has to remember which build it may import.
105
105
 
106
- `types` is stated once, and it is the node build's, so the two runtimes are ONE type surface and a name cannot exist on one side only. Each page entry takes its types from its node entry (`export type *`), so they cannot drift, and `specs/cli/node/names.spec.ts`, over the packed tarball, records every build's names and fails a value a page build lacks.
106
+ `types` is stated once, and it is the node build's, so the two runtimes are ONE type surface. Each page entry takes its types from its node entry (`export type *`), so they cannot drift, and `specs/cli/node/names.spec.ts`, over the packed tarball, records every build's names and fails a value a page build lacks. The check runs that one way: a page build may carry a value its node build does not export, and the names it records show which.
107
107
 
108
108
  The API is the types the package publishes, and nothing re-states it. What the code does project into the corpus is a set of generated tables and `schema/spec.schema.json`, generated from the document grammar's own constants; which tables, and the one gesture that writes them, is [Develop § What a change owes](../guides/develop.md#what-a-change-owes)'s list. Both `docs/` and `schema/` ship in the tarball, `docs/` less the two pages that are this repository's own, so a consumer reads the manual where the messages send them, inside the install ([Release](../guides/release.md#what-leaves-the-tree) names the two). All of them are held fresh by meta-tests.
109
109
 
@@ -111,7 +111,7 @@ The API is the types the package publishes, and nothing re-states it. What the c
111
111
 
112
112
  - **Reaching for a dependency from `chain/`.** A new external package belongs to the ONE slice that needs it (a notion (`stubs/`, `services/` ...) or a specification) and nowhere else. Only a library every layer truly shares sits behind the chain, in the one folder `FRAMEWORK_LAYERS` grants it to: vitest's test hooks in `paths/`; the `yaml` library and the toolchain's config types sit in `lint/kit/project/`, which the chain reaches. Anything else the chain imports is the boundary this package cannot afford to blur, and the layer map and the library-ownership rules refuse it.
113
113
  - **Adding an eighth constructor.** The seven are the closed vocabulary the conventions, the linter and the documentation are all shaped around. A new SUBJECT to specify is a design decision, and it earns a record in [`decisions/`](../decisions/) before it earns a constructor, as `integration` did in [ADR-004](../decisions/004-a-module-against-real-services-or-a-golden-is-the-integration-specification.md) and `lint` in [ADR-015](../decisions/015-a-rule-a-project-ships-is-the-lint-specification.md).
114
- - **Writing a rule's normative sentence into a chapter.** It belongs in `lint/consumer/catalogue/manifest.ts` beside the implementation; a chapter that restates it is the second copy the whole design exists to prevent.
114
+ - **Writing a rule's normative sentence into a chapter.** It belongs in `lint/consumer/catalogue/manifest.json` beside the implementation; a chapter that restates it is the second copy the whole design exists to prevent.
115
115
 
116
116
  ## Related
117
117
 
@@ -319,14 +319,14 @@ A rule is written when its defect class has been SEEN once and its criterion is
319
319
 
320
320
  ## Changing the conventions
321
321
 
322
- - A rule a machine can check is added to the code (its row in `lint/consumer/catalogue/manifest.ts` beside its implementation), and `npm run docs` regenerates the catalogue. It is never written here: the code is its one source.
322
+ - A rule a machine can check is added to the code (its row in `lint/consumer/catalogue/manifest.json` beside its implementation), and `npm run docs` regenerates the catalogue. It is never written here: the code is its one source.
323
323
  - A principle, or a criterion no machine can settle, is added here, as a process rule or in [What stays human](#what-stays-human-and-why).
324
324
  - A defect class seen once and decidable becomes a rule; one nobody has seen yet is a line of [the backlog](#the-backlog-the-criterion-written-coded-on-its-first-occurrence).
325
325
 
326
326
  ## Pitfalls
327
327
 
328
328
  - **Treating these docs as the spec.** They explain and illustrate; this constitution and the generated catalogue ([Linting](../reference/linting.md)) decide. When the docs and the catalogue disagree, the docs are fixed in the same change.
329
- - **Editing the generated catalogue by hand.** The [Linting](../reference/linting.md) catalogue is GENERATED from `lint/consumer/catalogue/manifest.ts`; a hand edit is overwritten by the next `npm run docs` and fails the freshness meta-test. Change the rule's text in the code; regenerate.
329
+ - **Editing the generated catalogue by hand.** The [Linting](../reference/linting.md) catalogue is GENERATED from the rows of `lint/consumer/catalogue/manifest.json`; a hand edit is overwritten by the next `npm run docs` and fails the freshness meta-test. Change the rule's text in the code; regenerate.
330
330
 
331
331
  ## Related
332
332
 
@@ -51,6 +51,8 @@ const result = await cli.exec(); // invoke the binary with no arguments
51
51
 
52
52
  The binary is spawned, never run with `spawnSync`: the test's event loop keeps turning while it runs, so a server the test itself started (an OTLP collector, a stub API on an ephemeral port) answers the binary, one-shot or long-running alike.
53
53
 
54
+ A one-shot run's whole process group ends with the test that ran it, whether the binary exited or not: `SIGTERM`, then `SIGKILL` after a 2 s grace, and the test is held until the group is gone. A process deaf to `SIGTERM` (a Playwright launcher's handler only closes its browsers) is killed before the worker can be torn down, so nothing the binary forked outlives the run.
55
+
54
56
  Note the asymmetry with the array form: `.exec()` (no args) is a bare invocation, but `.exec([])` (empty array) throws: a command _sequence_ must name at least one command.
55
57
 
56
58
  ```typescript
@@ -12,8 +12,10 @@ The repository answers six `make` targets, the same six every repository of its
12
12
  | `make build` | Build the bundle, before `lint` and before the `unit` project; an unchanged tree restores it from the build cache instead |
13
13
  | `make lint` | Typecheck, lint, format check, knip, then the conventions checker and the self checker (the source tree's structure locks) |
14
14
  | `make fix` | Build, then `npx typescript fix`, which runs the conventions' fixes and the self checks first, then oxlint's and the formatter, so the formatter formats what the checks wrote |
15
- | `make test` | Every suite |
16
- | `make check` | The whole verdict: build, lint, test, then the mutation verdict (`make mutation`, incremental) |
15
+ | `make test` | Every suite, through `npx typescript test`, which answers a tree it already proved green from that proof |
16
+ | `make check` | The whole verdict: build, lint, test, then the mutation verdict (`make mutation`, incremental); a stage whose inputs it already proved green answers from its proof |
17
+
18
+ A stage keeps its green as a proof keyed by the content of everything it reads, under `.artifacts/proven/`, and answers the same key from it: `make lint`, `make test` and `make mutation` run nothing on a tree they already proved green, on a laptop as on a runner ([the toolchain's Quality checks § A tree proven green](https://github.com/jterrazz/package-typescript/blob/main/docs/reference/quality-checks.md#a-tree-proven-green)). `check` asks no stage cold; a run that must ignore its proofs says so itself, `make mutation MUTATION_ARGS=--cold`.
17
19
 
18
20
  Every target installs first. CI calls `build`, `lint`, `test`, then `make mutation` ([Test § What CI runs](test.md#what-ci-runs)).
19
21
 
@@ -28,9 +30,10 @@ The everyday loop, and the targets beyond the six:
28
30
  | `npx vitest --run --project unit` | The fast suite, with no infrastructure |
29
31
  | `npm run docs` | Regenerate the generated tables |
30
32
  | `make test-changed` | Only the tests the uncommitted changes reach (`vitest --changed`): the local loop, never a verdict. Like any partial run, it judges no route ratchet ([API specs](api-specs.md#when-a-run-is-not-judged)) |
31
- | `make test-mobile` | The mobile specs on a real iOS simulator; CI runs it on macOS when a change reaches the mobile specs, and [Test](test.md) says when else it is owed |
33
+ | `make test-mobile` | The mobile specs on a real iOS simulator; no CI job runs it, and [Test](test.md) says when it is owed |
32
34
  | `make check-links` | Every link of `README.md`, `docs/` and `skills/`, remote ones included, with [lychee](https://lychee.cli.rs). It needs the network and a lychee on the machine, and fails where there is none. Never part of `check`: the gate reads local links offline, as `@jterrazz/typescript`'s Markdown pass states |
33
35
  | `make test-merge` | Read the blob reports of a sharded run back as one, then judge the route ratchet on it ([Test § What CI runs](test.md#what-ci-runs) says how a run is split) |
36
+ | `make rehearse` | The release gate on Linux, on the commit to tag, before the tag ([Releasing](release.md) says when). It needs docker and the network; never part of `check` |
34
37
 
35
38
  The `sqlite()` specs of the `unit` project need no rebuild after `npm ci`: this repository runs the `better-sqlite3` line whose native binding ships prebuilt. [Set up a project § What to install](../start/set-up-a-project.md#what-to-install) says which line that is, and what an older one costs.
36
39
 
@@ -46,19 +49,19 @@ This repository's `jterrazz.config.ts` declares it a `library` on `node`, lists
46
49
 
47
50
  ## Which file a change opens
48
51
 
49
- | Changing... | Opens |
50
- | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
51
- | A specification's chain, setups or terminal actions | `src/specifications/<specification>/<specification>.composition.ts` and the `*.capability.ts` or `*.action.ts` it composes, in the specification's concept folders, or in the notion it sets up (`src/stubs/`, `src/services/`, `src/disk/`, `src/clock/`) and `src/chain/context/` for a setup every specification shares |
52
- | What a result exposes | `src/expectations/` for an accessor, or the `*.result.ts` in the specification's concept folder; accessors stay READ-ONLY |
53
- | A matcher, or update-mode behaviour | `src/expectations/goldens/` and `src/expectations/in-place/`; `src/vitest/` only for the hook that wires them |
54
- | An external dependency's adapter | the ONE slice that owns the library: a notion (`src/services/<db>/`, `src/stubs/delivery/` ...) or a specification's concept folder (`src/specifications/website/browser/` ...), and no other |
55
- | A `{{token}}` or the structural comparison | `src/expectations/tokens/`, the list of kinds in `lint/kit/project/tokens.ts` |
56
- | A verb, a target, a landmark, or how one matches | `src/visitor/`: `verbs.ts` (`VisitorVerbs`, the verbs every surface shares), `targets/` (the `Target` type in `target.ts`, `roles.ts` and `landmarks.ts` (`within()` and the scope walk with it), each with the private factory it builds targets from, `modifiers.ts`, how a target is spelled in `as-written.ts`), `matching/` (the `target-matches-one-element` refusal in `ambiguous.ts`, what it lists of each candidate in `candidates.ts`, the tag-to-landmark table in `landmark-of-tag.ts`), `matrix.ts` (which verb and target each surface offers) |
57
- | A runner-config default, or a project helper | `src/vitest/preset.ts`, `src/vitest/project.ts` and `src/vitest/unit.project.ts`, or the specification's own `<specification>.project.ts` for its project, published by `src/index.vitest.ts` |
58
- | The `<case>.spec.yaml` grammar | `lint/kit/project/spec-document.ts`, read by BOTH the runner and the checker |
59
- | How a cli document runs | `src/specifications/cli/documents/literate.ts` |
60
- | A mechanized rule | its row in `lint/consumer/catalogue/manifest.ts` (`lint/self/catalogue/manifest.ts` for a self rule), its audience stated, **and** its implementation by channel: a static rule at `lint/consumer/rules/<family>/<name>.ts`, wired in `lint/consumer/rules/plugin.ts`; a checker pass at `lint/consumer/checks/<name>.ts`, listed in the registry `lint/consumer/checks/checker.ts`, which `lint/index.checker.ts` hands the kit; an option on an upstream rule in `lint/consumer/rulebook/`; a self rule or pass under `lint/self/rules/<family>/` or `lint/self/checks/`, listed in the `plugin.ts` or `checker.ts` beside them, which `lint/self/self.cli.ts` composes |
61
- | A principle, or a criterion no machine can settle | [Conventions](../concepts/conventions.md), the constitution |
52
+ | Changing... | Opens |
53
+ | --------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
54
+ | A specification's chain, setups or terminal actions | `src/specifications/<specification>/<specification>.composition.ts` and the `*.capability.ts` or `*.action.ts` it composes, in the specification's concept folders, or in the notion it sets up (`src/stubs/`, `src/services/`, `src/disk/`, `src/clock/`) and `src/chain/context/` for a setup every specification shares |
55
+ | What a result exposes | `src/expectations/` for an accessor, or the `*.result.ts` in the specification's concept folder; accessors stay READ-ONLY |
56
+ | A matcher, or update-mode behaviour | `src/expectations/goldens/` and `src/expectations/in-place/`; `src/vitest/` only for the hook that wires them |
57
+ | An external dependency's adapter | the ONE slice that owns the library: a notion (`src/services/<db>/`, `src/stubs/delivery/` ...) or a specification's concept folder (`src/specifications/website/browser/` ...), and no other |
58
+ | A `{{token}}` or the structural comparison | `src/expectations/tokens/`, the list of kinds in `lint/kit/project/tokens.ts` |
59
+ | A verb, a target, a landmark, or how one matches | `src/visitor/`: `verbs.ts` (`VisitorVerbs`, the verbs every surface shares), `targets/` (the `Target` type in `target.ts`, `roles.ts` and `landmarks.ts` (`within()` and the scope walk with it), each with the private factory it builds targets from, `modifiers.ts`, how a target is spelled in `as-written.ts`), `matching/` (the `target-matches-one-element` refusal in `ambiguous.ts`, what it lists of each candidate in `candidates.ts`, the tag-to-landmark table in `landmark-of-tag.ts`), `matrix.ts` (which verb and target each surface offers) |
60
+ | A runner-config default, or a project helper | `src/vitest/preset.ts`, `src/vitest/project.ts` and `src/vitest/unit.project.ts`, or the specification's own `<specification>.project.ts` for its project, published by `src/index.vitest.ts` |
61
+ | The `<case>.spec.yaml` grammar | `lint/kit/project/spec-document.ts`, read by BOTH the runner and the checker |
62
+ | How a cli document runs | `src/specifications/cli/documents/literate.ts` |
63
+ | A mechanized rule | its row in `lint/consumer/catalogue/manifest.json` (`lint/self/catalogue/manifest.ts` for a self rule), its audience stated, **and** its implementation by channel: a static rule at `lint/consumer/rules/<family>/<name>.ts`, wired in `lint/consumer/rules/plugin.ts`; a checker pass at `lint/consumer/checks/<name>.ts`, listed in the registry `lint/consumer/checks/checker.ts`, which `lint/index.checker.ts` hands the kit; an option on an upstream rule in `lint/consumer/rulebook/`; a self rule or pass under `lint/self/rules/<family>/` or `lint/self/checks/`, listed in the `plugin.ts` or `checker.ts` beside them, which `lint/self/self.cli.ts` composes |
64
+ | A principle, or a criterion no machine can settle | [Conventions](../concepts/conventions.md), the constitution |
62
65
 
63
66
  ## What a change owes
64
67
 
@@ -85,7 +88,7 @@ The manual is cut into the four sections the [map](../README.md) lists; how a pa
85
88
 
86
89
  - **One owner per fact.** Each fact is stated on one page; the others link to it.
87
90
  - **The kind guides share one outline** (what it specifies, the constructor, the chain, the result, unique here, pitfalls), so the reader who learned one kind knows where to look in the next.
88
- - **Rules live in the code.** A rule's normative sentence is written in `lint/consumer/catalogue/manifest.ts` and generated into [Linting](../reference/linting.md); the pages explain and illustrate, and the catalogue decides.
91
+ - **Rules live in the code.** A rule's normative sentence is written in `lint/consumer/catalogue/manifest.json` and generated into [Linting](../reference/linting.md); the pages explain and illustrate, and the catalogue decides.
89
92
  - **Every example compiles.** A meta-test typechecks each block that imports only the framework, so a sample cannot drift from the API.
90
93
  - **A block that starts at `test(...)` is an excerpt.** In its file, that test sits inside the file's one `describe`, which a block showing a whole file writes out.
91
94
 
@@ -76,7 +76,7 @@ The booted simulator itself is the machine's: it is reused by the next run, and
76
76
 
77
77
  ### A declared Appium server
78
78
 
79
- `appium: { url }` points the runner at a server someone else runs: a device farm, a Mac across the network, the protocol double this package specifies its own mobile specification against. The runner connects to it instead of spawning the project's own, and it hands the server the device by capabilities (`appium:deviceName`, `appium:platformVersion` from `os`, `appium:udid` when declared) instead of resolving and booting it through `simctl`: the machine the server runs on owns its simulators. `cleanup` stops nothing it did not start, and under `mobile()` the run's one session outlives it ([One session per run](#one-session-per-run)).
79
+ `appium: { url }` points the runner at a server someone else runs: a device farm, a Mac across the network, the protocol double this package specifies its own mobile specification against. The runner connects to it instead of spawning the project's own, and it hands the server the device by capabilities (`appium:deviceName`, `appium:platformVersion` from `os`, `appium:udid` when declared) instead of resolving and booting it through `simctl`: the machine the server runs on owns its simulators. `cleanup` stops nothing it did not start, and under `mobile()` the session its worker opened for the run outlives it ([One session per run](#one-session-per-run)).
80
80
 
81
81
  ```typescript
82
82
  export const { cleanup, mobile } = await specification.mobile({
@@ -88,7 +88,7 @@ export const { cleanup, mobile } = await specification.mobile({
88
88
 
89
89
  ### One session per run
90
90
 
91
- Under a server that outlives the file (a declared `appium` server, or the run's own), a `mobile()` project opens **one driver session for the whole run**, not one per spec file. The project's global setup hands every file an empty ledger under `.artifacts/vitest/mobile-sessions/`. The first `.open()` of the run creates the session and records it there, and every later file asking the same server for the same capabilities attaches to it instead of creating its own. The session asks the server to hold it through five idle minutes (`appium:newCommandTimeout: 300`), and a later file probes it (`GET /session/:id/timeouts`, answered without WebDriverAgent) before attaching: a session the server no longer holds (expired, or lost with a restarted server) is replaced by a new one, recorded and ended the same way. A probe that fails without saying the session is gone (a timeout, a 5xx) still replaces it, after a best-effort `DELETE /session/:id`: a session the probe could not confirm is ended, never left holding the device. `cleanup` leaves that session alone; the setup ends it once the last file has run. Global setups tear down in the reverse of their order, so a setup of the project's own that SERVES the declared Appium server (a protocol double, a server started for the run) is listed before `mobile()`'s: the run's session is then ended while its server still answers.
91
+ Under a server that outlives the file (a declared `appium` server, or the run's own), a `mobile()` project opens **one driver session per worker for the whole run**, not one per spec file: one in all on a serial project. The project's global setup hands every file an empty ledger under `.artifacts/vitest/mobile-sessions/`. The first `.open()` of the run creates the session and records it there, and every later file of that worker asking the same server for the same capabilities attaches to it instead of creating its own. The session asks the server to hold it through five idle minutes (`appium:newCommandTimeout: 300`), and a later file probes it (`GET /session/:id/timeouts`, answered without WebDriverAgent) before attaching: a session the server no longer holds (expired, or lost with a restarted server) is replaced by a new one, recorded and ended the same way. A probe that fails without saying the session is gone (a timeout, a 5xx) still replaces it, after a best-effort `DELETE /session/:id`: a session the probe could not confirm is ended, never left holding the device. `cleanup` leaves that session alone; the setup ends it once the last file has run. Global setups tear down in the reverse of their order, so a setup of the project's own that SERVES the declared Appium server (a protocol double, a server started for the run) is listed before `mobile()`'s: the run's session is then ended while its server still answers.
92
92
 
93
93
  **The run's own server** is the project's to declare: `mobile({ device, root })` names the simulator and where the `appium` binary is resolved from (`<root>/node_modules/.bin/appium`, `root` read against the config's root, `.` when absent). The project's global setup resolves and boots that simulator, spawns the server once for the run, and hands both to the workers. A runner with no `appium` of its own drives that server on the project's device and spawns nothing. The device is the project's to own, so a runner leaves `device` out and drives the project's; one that states it restates the project's, and the booted simulator's UDID stands in when it names none. A `device` that names another simulator, another runtime (`'iOS 26.5'` and `'26.5'` are one, as they are to resolution) or another UDID is refused, naming both devices: the setup booted the project's device alone. The setup stops the server, and its WebDriverAgent, once it has ended the run's sessions.
94
94
 
@@ -101,7 +101,7 @@ const mobile = {
101
101
  };
102
102
  ```
103
103
 
104
- The session is keyed on the server's URL and the capabilities asked for, so a second specification naming another app (or, under a declared `appium` server, another device) gets a session of its own, recorded and ended the same way. Files still run one at a time (`fileParallelism: false` is `mobile()`'s default): the app is terminated and relaunched by every `.open()`, and two files driving it at once would fight over it.
104
+ The session is keyed on the server's URL and the capabilities asked for, so a second specification naming another app (or, under a declared `appium` server, another device) gets a session of its own, recorded and ended the same way. Files still run one at a time (`mobile()` is serial by default): the app is terminated and relaunched by every `.open()`, and two files driving it at once would fight over it. A server that gives every session an app of its own, such as a protocol double, lets a project state `serial: false`: its files then run in parallel beside every other project, each worker over the session it opened, so no two files drive one app.
105
105
 
106
106
  Two runs keep a session per runner instead, which `cleanup` ends:
107
107
 
@@ -4,11 +4,12 @@ This repository ships one thing: the npm package `@jterrazz/test`. Nothing here
4
4
 
5
5
  ## Releasing
6
6
 
7
- A release is three steps, in this order.
7
+ A release is four steps, in this order.
8
8
 
9
9
  1. **Bump and land the version.** The version in `package.json` moves, alone, in a commit whose subject is `chore: <version> — <what changed>`. The commit log is the changelog: this repository carries no `CHANGELOG.md`, by decision, and the release notes plus that subject line are the record. A major also adds what it makes a project rewrite to [Set up a project § Upgrading](../start/set-up-a-project.md#upgrading), the one page a consumer reads before bumping.
10
- 2. **Create the GitHub release**, tagged at that commit. This is the trigger and the only one: `.github/workflows/release.yaml` listens on `release: types: [created]`.
11
- 3. **Watch the gate.** The workflow delegates to the shared `jterrazz/jterrazz-actions/.github/workflows/release-npm.yaml`, which runs the FULL validate job first (the same targets the push gate runs, chromium included: [Test § What CI runs](test.md#what-ci-runs)) and publishes only if it is green.
10
+ 2. **Rehearse the gate.** `make rehearse` runs it on that commit as the runner will, on Linux ([A rehearsal on Linux](https://github.com/jterrazz/package-typescript/blob/main/docs/reference/commands.md#a-rehearsal-on-linux)), and exits 0.
11
+ 3. **Create the GitHub release**, tagged at that commit. This is the trigger and the only one: `.github/workflows/release.yaml` listens on `release: types: [created]`.
12
+ 4. **Watch the gate.** The workflow delegates to the shared `jterrazz/jterrazz-actions/.github/workflows/release-npm.yaml`, which runs the FULL validate job first (the same targets the push gate runs, chromium included: [Test § What CI runs](test.md#what-ci-runs)) and publishes only if it is green.
12
13
 
13
14
  The publish step is `npm publish --access public --provenance`, on Node 24, with `id-token: write` granted so npm records a provenance attestation linking the tarball to the workflow run that built it. The registry is npm's, stated as `publishConfig` in the manifest.
14
15
 
@@ -99,7 +99,7 @@ test('serves robots.txt as plain text', async () => {
99
99
 
100
100
  #### `.visit(path, scenario?)`: a rendered page
101
101
 
102
- `.visit()` renders the page in a real chromium and resolves with the captured document. There is **one browser process per vitest worker**, shared by every runner the worker hosts and launched lazily on the first `.visit()`, so a spec file that only calls `.fetch()` never pays the browser-launch cost. It is Playwright's headless shell, named explicitly, the lighter of its two Chromium builds. Each visit gets a fresh, isolated browser context, and records no video and no trace: a scenario that fails leaves one screenshot ([Evidence on failure](#evidence-on-failure)).
102
+ `.visit()` renders the page in a real chromium and resolves with the captured document. There is **one browser process per vitest worker**, shared by every runner the worker hosts and launched lazily on the first `.visit()`, so a spec file that only calls `.fetch()` never pays the browser-launch cost. A worker hosts every file it runs when the project names its runner ([Set up a project](../start/set-up-a-project.md#the-project-helpers)), and one file otherwise. It is Playwright's headless shell, named explicitly, the lighter of its two Chromium builds. Each visit gets a fresh, isolated browser context, and records no video and no trace: a scenario that fails leaves one screenshot ([Evidence on failure](#evidence-on-failure)).
103
103
 
104
104
  ```typescript
105
105
  test('captures the full head surface of a rendered page', async () => {
@@ -129,7 +129,7 @@ const before = await cli.exec('seed'),
129
129
 
130
130
  ## The rule catalogue
131
131
 
132
- The catalogue below is **generated** from `lint/consumer/catalogue/manifest.ts`, where each rule carries its normative text beside its implementation. Never edit it by hand; `npm run docs` regenerates it, and a meta-test fails when the committed table is not what the code says. It lists every `consumer` rule (what a project using the package meets) across the channels a consumer meets (static, the oxlint plugin's rules, then toolchain, upstream, checker, runtime, type, process), grouped by convention family; the meta channel holds only this package's own suite. A `self` rule holds only this package's own suite, and is listed where the package says how it tests itself: [Test § The package's own rules](https://github.com/jterrazz/package-test/blob/main/docs/guides/test.md#the-packages-own-rules).
132
+ The catalogue below is **generated** from the rows of `lint/consumer/catalogue/manifest.json`, which `lint/consumer/catalogue/manifest.ts` reads, where each rule carries its normative text beside its implementation. Never edit it by hand; `npm run docs` regenerates it, and a meta-test fails when the committed table is not what the code says. It lists every `consumer` rule (what a project using the package meets) across the channels a consumer meets (static, the oxlint plugin's rules, then toolchain, upstream, checker, runtime, type, process), grouped by convention family; the meta channel holds only this package's own suite. A `self` rule holds only this package's own suite, and is listed where the package says how it tests itself: [Test § The package's own rules](https://github.com/jterrazz/package-test/blob/main/docs/guides/test.md#the-packages-own-rules).
133
133
 
134
134
  A rule answers to ONE name, the same everywhere: the id you address in a config (`jterrazz/<name>`), the code a checker finding prints (`jterrazz-check(<name>)`, keyed `jterrazz-check/<name>` by [the ratchet](#the-ratchet-the-test-section-of-jterrazzbaselinejson)), the name a suppression gives, and the anchor a message's `read:` part lands on (`docs/reference/linting.md#<name>`). The name states the requirement: a ban reads `no-<forbidden thing>`, a requirement names its subject and what holds of it (`seeds-are-sql`, `result-compared-whole`), a quantity reads `one-<thing>-per-<scope>`. A convention held over two channels is two rows, and so two names: the runtime twin alone carries `-at-runtime`. A name carries no severity, and neither does a row: every rule errors.
135
135
 
@@ -137,7 +137,7 @@ Seven columns. **Rule** is the name, and carries the row's anchor. **Specificati
137
137
 
138
138
  The catalogue is grouped by **family**, a word naming what the rules judge (the sections below, generated from `lint/consumer/catalogue/families.ts`), and a static rule's implementation sits in the folder of its family, `lint/consumer/rules/<family>/<name>.ts`.
139
139
 
140
- <!-- GENERATED:catalogue. Do not edit by hand; run `npm run docs`. Source: lint/consumer/catalogue/manifest.ts -->
140
+ <!-- GENERATED:catalogue. Do not edit by hand; run `npm run docs`. Source: lint/consumer/catalogue/manifest.json -->
141
141
 
142
142
  Every rule a project using the framework meets, across its enforcement channels (**static**: 55 (oxlint rules), **upstream**: 3 (options set on an upstream rule), **toolchain**: 7 (the preset @jterrazz/typescript runs), **checker**: 36 (bundled passes), **runtime**: 15 (execution-time refusals), **type**: 10 (what the compiler refuses), **process**: 2 (review-borne rules)), sourced from one manifest so the code and the catalogue can never drift.
143
143