@jterrazz/test 18.2.0 → 18.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/oxlint.cjs CHANGED
@@ -172,7 +172,6 @@ function testPackageOf(specifier) {
172
172
  const SPECIFICATIONS = /* @__PURE__ */ new Set([
173
173
  "api",
174
174
  "cli",
175
- "deployment",
176
175
  "integration",
177
176
  "job",
178
177
  "lint",
@@ -213,10 +212,10 @@ var manifest_default = {
213
212
  {
214
213
  "audience": "consumer",
215
214
  "channel": "static",
216
- "sentence": "Eight constructors and only eight: `specification.api()`, `specification.job()`, `specification.cli(bin)`, `specification.integration()`, `specification.website()`, `specification.deployment()`, `specification.mobile()`, `specification.lint()`. Any other member (`.app`, `.http`, `.stack`...) is an error. A rendered component starts nothing: it is a chain, not a member of this record.",
215
+ "sentence": "Seven constructors and only seven: `specification.api()`, `specification.job()`, `specification.cli(bin)`, `specification.integration()`, `specification.website()`, `specification.mobile()`, `specification.lint()`. Any other member (`.app`, `.http`, `.stack`...) is an error. A rendered component starts nothing: it is a chain, not a member of this record.",
217
216
  "specification": "model",
218
217
  "family": "setup",
219
- "fix": "Name one of the eight, or reach for the `component` chain: a ninth subject is a decision before it is a constructor.",
218
+ "fix": "Name one of the seven, or reach for the `component` chain: an eighth subject is a decision before it is a constructor.",
220
219
  "name": "no-unknown-constructor",
221
220
  "rationale": "A closed surface stops parallel constructors being invented and keeps the API memorable.",
222
221
  "reach": "all"
@@ -954,7 +953,7 @@ var manifest_default = {
954
953
  {
955
954
  "audience": "consumer",
956
955
  "channel": "checker",
957
- "sentence": "A first-level folder named for one of the eight specifications holds a `*.specification.ts(x)` calling `specification.<specification>(`. Positive-only: any other first-level name is a repository suite, judged by `specs-at-declared-depth`'s declared depth alone.",
956
+ "sentence": "A first-level folder named for one of the seven specifications holds a `*.specification.ts(x)` calling `specification.<specification>(`. Positive-only: any other first-level name is a repository suite, judged by `specs-at-declared-depth`'s declared depth alone.",
958
957
  "specification": "model",
959
958
  "family": "layout",
960
959
  "fix": "Create the specification the folder promises, or rename the folder.",
@@ -1053,7 +1052,7 @@ var manifest_default = {
1053
1052
  {
1054
1053
  "audience": "consumer",
1055
1054
  "channel": "checker",
1056
- "sentence": "The first line of a depth-1 `.http` follows its grammar: a request line (`METHOD /path`, or `METHOD https://host/path` for a deployment) under `__requests__/`, a status line (`HTTP/1.1 <status>`, or `unreachable: <reason>` for an exchange that never answered) under `__expected__/`.",
1055
+ "sentence": "The first line of a depth-1 `.http` follows its grammar: a request line (`METHOD /path`, or `METHOD https://host/path` for a deployed api) under `__requests__/`, a status line (`HTTP/1.1 <status>`, or `unreachable: <reason>` for an exchange that never answered) under `__expected__/`.",
1057
1056
  "specification": "api",
1058
1057
  "family": "expectations",
1059
1058
  "fix": "Write the opening line the folder calls for, or move the file to the other one.",
@@ -2252,7 +2251,6 @@ function subjectsOf(source, readRequest, reading = NO_READING) {
2252
2251
  const SETTINGS = {
2253
2252
  api: ["clock", "headers"],
2254
2253
  cli: ["env"],
2255
- deployment: ["headers"],
2256
2254
  integration: ["clock"],
2257
2255
  job: ["clock"],
2258
2256
  website: ["clock", "headers"]
@@ -5999,7 +5997,6 @@ const noSimulatedDom = (0, _jterrazz_typescript_rules.defineRule)({
5999
5997
  const KNOWN_CONSTRUCTORS = /* @__PURE__ */ new Set([
6000
5998
  "api",
6001
5999
  "cli",
6002
- "deployment",
6003
6000
  "integration",
6004
6001
  "job",
6005
6002
  "lint",
@@ -6011,7 +6008,7 @@ const noUnknownConstructor = (0, _jterrazz_typescript_rules.defineRule)({
6011
6008
  docs: row$7,
6012
6009
  messages: { unknownConstructor: {
6013
6010
  found: "`specification.{{member}}` is no constructor",
6014
- fix: "call `specification.api()`, `.job()`, `.cli(bin)`, `.integration()`, `.website()`, `.deployment()`, `.mobile()` or `.lint()`; a rendered unit is the `component` chain"
6011
+ fix: "call `specification.api()`, `.job()`, `.cli(bin)`, `.integration()`, `.website()`, `.mobile()` or `.lint()`; a rendered unit is the `component` chain"
6015
6012
  } },
6016
6013
  read: (0, _jterrazz_typescript_rules.readAt)(row$7, CATALOGUE_CHAPTER),
6017
6014
  type: "problem"
package/dist/oxlint.d.cts CHANGED
@@ -12,7 +12,7 @@ type RowDocs<Family extends string = string> = Omit<RuleDocs, 'family' | 'name'>
12
12
  /** One per row: a convention enforced two ways is two rows, so a reader knows which pass a finding comes from. */
13
13
  channel: 'checker' | 'meta' | 'process' | 'runtime' | 'static' | 'toolchain' | 'type' | 'upstream';
14
14
  /** Required: `model` is a statement (this reaches every specification), never a default. */
15
- specification: 'api' | 'cli' | 'component' | 'deployment' | 'integration' | 'job' | 'lint' | 'mobile' | 'model' | 'website';
15
+ specification: 'api' | 'cli' | 'component' | 'integration' | 'job' | 'lint' | 'mobile' | 'model' | 'website';
16
16
  family: Family;
17
17
  rationale: string;
18
18
  /** An `upstream` row names the rule whose option carries it; the completeness meta-test reads it. */
package/dist/oxlint.d.ts CHANGED
@@ -12,7 +12,7 @@ type RowDocs<Family extends string = string> = Omit<RuleDocs, 'family' | 'name'>
12
12
  /** One per row: a convention enforced two ways is two rows, so a reader knows which pass a finding comes from. */
13
13
  channel: 'checker' | 'meta' | 'process' | 'runtime' | 'static' | 'toolchain' | 'type' | 'upstream';
14
14
  /** Required: `model` is a statement (this reaches every specification), never a default. */
15
- specification: 'api' | 'cli' | 'component' | 'deployment' | 'integration' | 'job' | 'lint' | 'mobile' | 'model' | 'website';
15
+ specification: 'api' | 'cli' | 'component' | 'integration' | 'job' | 'lint' | 'mobile' | 'model' | 'website';
16
16
  family: Family;
17
17
  rationale: string;
18
18
  /** An `upstream` row names the rule whose option carries it; the completeness meta-test reads it. */
package/dist/oxlint.js CHANGED
@@ -368,7 +368,6 @@ const noRedundantServiceUrl = defineRule({
368
368
  const SETTINGS = {
369
369
  api: ["clock", "headers"],
370
370
  cli: ["env"],
371
- deployment: ["headers"],
372
371
  integration: ["clock"],
373
372
  job: ["clock"],
374
373
  website: ["clock", "headers"]
@@ -3739,7 +3738,6 @@ const noSimulatedDom = defineRule({
3739
3738
  const KNOWN_CONSTRUCTORS = /* @__PURE__ */ new Set([
3740
3739
  "api",
3741
3740
  "cli",
3742
- "deployment",
3743
3741
  "integration",
3744
3742
  "job",
3745
3743
  "lint",
@@ -3751,7 +3749,7 @@ const noUnknownConstructor = defineRule({
3751
3749
  docs: row$7,
3752
3750
  messages: { unknownConstructor: {
3753
3751
  found: "`specification.{{member}}` is no constructor",
3754
- fix: "call `specification.api()`, `.job()`, `.cli(bin)`, `.integration()`, `.website()`, `.deployment()`, `.mobile()` or `.lint()`; a rendered unit is the `component` chain"
3752
+ fix: "call `specification.api()`, `.job()`, `.cli(bin)`, `.integration()`, `.website()`, `.mobile()` or `.lint()`; a rendered unit is the `component` chain"
3755
3753
  } },
3756
3754
  read: readAt(row$7, CATALOGUE_CHAPTER),
3757
3755
  type: "problem"
@@ -1,5 +1,5 @@
1
- import { t as isRecord } from "./shared-json-shape.js";
2
1
  import { n as refusal } from "./shared-refusal.js";
2
+ import { t as isRecord } from "./shared-json-shape.js";
3
3
  import { t as TOKEN_KINDS } from "./shared-tokens.js";
4
4
  //#region src/expectations/tokens/capture-scope.ts
5
5
  /** Named captures of `match.ref()` / `{{type#ref}}`, one scope per spec result. */
@@ -1,6 +1,6 @@
1
1
  import { i as safeRealpath } from "./shared-resolve.js";
2
- import { t as isRecord } from "./shared-json-shape.js";
3
2
  import { n as refusal } from "./shared-refusal.js";
3
+ import { t as isRecord } from "./shared-json-shape.js";
4
4
  import { r as closingOf, t as argumentSpans } from "./shared-source-scan.js";
5
5
  import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
6
6
  import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
@@ -33,10 +33,10 @@ var manifest_default = {
33
33
  {
34
34
  "audience": "consumer",
35
35
  "channel": "static",
36
- "sentence": "Eight constructors and only eight: `specification.api()`, `specification.job()`, `specification.cli(bin)`, `specification.integration()`, `specification.website()`, `specification.deployment()`, `specification.mobile()`, `specification.lint()`. Any other member (`.app`, `.http`, `.stack`...) is an error. A rendered component starts nothing: it is a chain, not a member of this record.",
36
+ "sentence": "Seven constructors and only seven: `specification.api()`, `specification.job()`, `specification.cli(bin)`, `specification.integration()`, `specification.website()`, `specification.mobile()`, `specification.lint()`. Any other member (`.app`, `.http`, `.stack`...) is an error. A rendered component starts nothing: it is a chain, not a member of this record.",
37
37
  "specification": "model",
38
38
  "family": "setup",
39
- "fix": "Name one of the eight, or reach for the `component` chain: a ninth subject is a decision before it is a constructor.",
39
+ "fix": "Name one of the seven, or reach for the `component` chain: an eighth subject is a decision before it is a constructor.",
40
40
  "name": "no-unknown-constructor",
41
41
  "rationale": "A closed surface stops parallel constructors being invented and keeps the API memorable.",
42
42
  "reach": "all"
@@ -774,7 +774,7 @@ var manifest_default = {
774
774
  {
775
775
  "audience": "consumer",
776
776
  "channel": "checker",
777
- "sentence": "A first-level folder named for one of the eight specifications holds a `*.specification.ts(x)` calling `specification.<specification>(`. Positive-only: any other first-level name is a repository suite, judged by `specs-at-declared-depth`'s declared depth alone.",
777
+ "sentence": "A first-level folder named for one of the seven specifications holds a `*.specification.ts(x)` calling `specification.<specification>(`. Positive-only: any other first-level name is a repository suite, judged by `specs-at-declared-depth`'s declared depth alone.",
778
778
  "specification": "model",
779
779
  "family": "layout",
780
780
  "fix": "Create the specification the folder promises, or rename the folder.",
@@ -873,7 +873,7 @@ var manifest_default = {
873
873
  {
874
874
  "audience": "consumer",
875
875
  "channel": "checker",
876
- "sentence": "The first line of a depth-1 `.http` follows its grammar: a request line (`METHOD /path`, or `METHOD https://host/path` for a deployment) under `__requests__/`, a status line (`HTTP/1.1 <status>`, or `unreachable: <reason>` for an exchange that never answered) under `__expected__/`.",
876
+ "sentence": "The first line of a depth-1 `.http` follows its grammar: a request line (`METHOD /path`, or `METHOD https://host/path` for a deployed api) under `__requests__/`, a status line (`HTTP/1.1 <status>`, or `unreachable: <reason>` for an exchange that never answered) under `__expected__/`.",
877
877
  "specification": "api",
878
878
  "family": "expectations",
879
879
  "fix": "Write the opening line the folder calls for, or move the file to the other one.",
@@ -1,5 +1,5 @@
1
- import { t as isRecord } from "./shared-json-shape.js";
2
1
  import { n as refusal } from "./shared-refusal.js";
2
+ import { t as isRecord } from "./shared-json-shape.js";
3
3
  import { a as readLedgers, i as measure, n as clearLedgers, r as markPartial, s as shardLedgersDir, t as ROUTES_DIR, u as shouldUpdateSnapshots } from "./shared-ledger.js";
4
4
  import { u as parseYamlSource } from "./shared-yaml-document.js";
5
5
  import { r as testSettingsAt } from "./shared-test-section.js";
@@ -1,6 +1,6 @@
1
1
  import { t as ServedProcess } from "./shared-serve.js";
2
- import { t as isRecord } from "./shared-json-shape.js";
3
2
  import { n as refusal } from "./shared-refusal.js";
3
+ import { t as isRecord } from "./shared-json-shape.js";
4
4
  import { d as VITEST_ARTIFACTS_DIR } from "./shared-artifacts.js";
5
5
  import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
6
6
  import { join, resolve } from "node:path";
@@ -3,7 +3,6 @@ import { basename } from "node:path";
3
3
  const SPECIFICATIONS = /* @__PURE__ */ new Set([
4
4
  "api",
5
5
  "cli",
6
- "deployment",
7
6
  "integration",
8
7
  "job",
9
8
  "lint",
@@ -97,6 +97,7 @@ async function loadMsw() {
97
97
  };
98
98
  }
99
99
  let serverInstance = null;
100
+ let registrations = 0;
100
101
  let refuseOffTheWire = null;
101
102
  function offTheWireError(request) {
102
103
  return new Error(refusal("network-strict-after-first-stub", {
@@ -128,14 +129,26 @@ async function registerStubs(stubs) {
128
129
  const { handlers, refuse, violation } = buildStubHandlers(msw, stubs);
129
130
  server.use(...handlers);
130
131
  refuseOffTheWire = (request) => refuse(offTheWireError(request), `unreadable request ${request.method} ${request.url}`);
132
+ registrations += 1;
133
+ let released = false;
131
134
  return {
132
135
  cleanup: () => {
136
+ if (released) return;
137
+ released = true;
133
138
  refuseOffTheWire = null;
134
139
  server.resetHandlers();
140
+ registrations -= 1;
141
+ if (registrations === 0) closeServer();
135
142
  },
136
143
  violation
137
144
  };
138
145
  }
146
+ function closeServer() {
147
+ if (serverInstance !== null) {
148
+ serverInstance.close();
149
+ serverInstance = null;
150
+ }
151
+ }
139
152
  //#endregion
140
153
  export { registerStubs };
141
154
 
@@ -1,5 +1,5 @@
1
- import { t as isRecord } from "./shared-json-shape.js";
2
1
  import { n as refusal, t as invariant } from "./shared-refusal.js";
2
+ import { t as isRecord } from "./shared-json-shape.js";
3
3
  import { t as listenedPort } from "./shared-listened-port.js";
4
4
  import { t as loadPeer } from "./shared-peer.js";
5
5
  import { execSync } from "node:child_process";
package/dist/stubs.js CHANGED
@@ -1,5 +1,5 @@
1
- import { t as isRecord } from "./shared-json-shape.js";
2
1
  import { n as refusal } from "./shared-refusal.js";
2
+ import { t as isRecord } from "./shared-json-shape.js";
3
3
  import { a as defineStubs, d as structuralEquals, f as structuralSubset, i as defineStub, o as describeRoute, r as declared, t as STREAM_BODY, y as CaptureScope } from "./shared-exchange.js";
4
4
  //#region src/stubs/kinds/http-filter.ts
5
5
  function rawOf(body) {
package/dist/vitest.d.ts CHANGED
@@ -86,11 +86,6 @@ type ComponentProjectOptions = ComponentOptions & SpecificationProjectOptions;
86
86
  /** A component project: renders in a real Chromium; the default `exclude` keeps it out of `specs/`. */
87
87
  export declare function component(options: ComponentProjectOptions): Promise<TestProjectInlineConfiguration>;
88
88
  //#endregion
89
- //#region src/specifications/deployment/deployment.project.d.ts
90
- /** `deployment()`: a running deployment, met over the network through complete HTTP exchanges. */
91
- type DeploymentProjectOptions = SpecificationProjectOptions;
92
- export declare function deployment(options?: DeploymentProjectOptions): TestProjectInlineConfiguration;
93
- //#endregion
94
89
  //#region src/specifications/integration/integration.project.d.ts
95
90
  /** `integration()`: a module against real services, or against a golden. */
96
91
  type IntegrationProjectOptions = InProcessProjectOptions;
@@ -140,5 +135,5 @@ export declare function unit(options?: UnitProjectOptions): TestProjectInlineCon
140
135
  //#region src/index.vitest.d.ts
141
136
  export declare const vitestConfig: (configUrl: string) => Promise<ViteUserConfig>;
142
137
  //#endregion
143
- export type { ApiProjectOptions, CliProjectOptions, ComponentProjectOptions, DeploymentProjectOptions, InProcessProjectOptions, IntegrationProjectOptions, JobProjectOptions, LintProjectOptions, LiterateDoor, LiterateOptions, LiteratePlugin, MobileProjectOptions, SpecificationProjectOptions, UnitProjectOptions, WebsiteProjectOptions };
138
+ export type { ApiProjectOptions, CliProjectOptions, ComponentProjectOptions, InProcessProjectOptions, IntegrationProjectOptions, JobProjectOptions, LintProjectOptions, LiterateDoor, LiterateOptions, LiteratePlugin, MobileProjectOptions, SpecificationProjectOptions, UnitProjectOptions, WebsiteProjectOptions };
144
139
  //# sourceMappingURL=vitest.d.ts.map
package/dist/vitest.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { r as writeInPlace } from "./shared-in-place.js";
2
- import { t as isRecord } from "./shared-json-shape.js";
3
2
  import { n as refusal$1, t as invariant } from "./shared-refusal.js";
3
+ import { t as isRecord } from "./shared-json-shape.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
6
  import { c as REPORTER_OUTPUT_FILES, d as VITEST_ARTIFACTS_DIR, i as COVERAGE_DIR, l as SCREENSHOTS_DIR, n as ATTACHMENTS_DIR, o as MODULE_CACHE_DIR, r as BLOB_DIR, s as NODE_COMPILE_CACHE_DIR, t as ARTIFACTS_DIR } from "./shared-artifacts.js";
@@ -366,11 +366,6 @@ async function component(options) {
366
366
  });
367
367
  }
368
368
  //#endregion
369
- //#region src/specifications/deployment/deployment.project.ts
370
- function deployment(options = {}) {
371
- return specificationProject("deployment", options);
372
- }
373
- //#endregion
374
369
  //#region src/specifications/integration/integration.project.ts
375
370
  function integration(options = {}) {
376
371
  return mergeConfig(runnerProject("integration", options), { test: { globalSetup: [servicesSetup()] } });
@@ -509,7 +504,6 @@ const BUILDERS = /* @__PURE__ */ new Map([
509
504
  ["job", job],
510
505
  ["integration", integration],
511
506
  ["website", website],
512
- ["deployment", deployment],
513
507
  ["mobile", mobile],
514
508
  ["component", component]
515
509
  ]);
@@ -593,6 +587,6 @@ async function derivedFrom(root) {
593
587
  }
594
588
  const vitestConfig = async (configUrl) => await derivedFrom(dirname(fileURLToPath(configUrl)));
595
589
  //#endregion
596
- export { api, cli, component, deployment, integration, job, lint, literate, mobile, unit, vitestConfig, website };
590
+ export { api, cli, component, integration, job, lint, literate, mobile, unit, vitestConfig, website };
597
591
 
598
592
  //# sourceMappingURL=vitest.js.map
package/docs/README.md CHANGED
@@ -22,8 +22,7 @@ The pages follow the package's four export families (what is tested, what surrou
22
22
  | [Component specs](guides/component-specs.md) | A rendered unit in a real Chromium, in its folder's `__tests__/`: the `component` chain |
23
23
  | [Website specs](guides/website-specs.md) | A served page, fetched or driven in a browser: `specification.website()` |
24
24
  | [Mobile specs](guides/mobile-specs.md) | A native app on an iOS simulator: `specification.mobile()` |
25
- | [Deployment specs](guides/deployment-specs.md) | A running deployment, one HTTP exchange over the network: `specification.deployment()` |
26
- | [API specs](guides/api-specs.md) | An HTTP app, in-process, through complete requests: `specification.api()` |
25
+ | [API specs](guides/api-specs.md) | An HTTP app, in-process or deployed at a `url`, through requests: `specification.api()` |
27
26
  | [Job specs](guides/job-specs.md) | A pipeline triggered by name: `specification.job()` |
28
27
  | [CLI specs](guides/cli-specs.md) | A binary in a fresh working directory: `specification.cli()` |
29
28
  | [Spec documents](guides/spec-documents.md) | A CLI session written as data: the `<case>.spec.yaml` format |
@@ -8,20 +8,19 @@ The rules themselves (what they say and why) are [Conventions](conventions.md);
8
8
 
9
9
  Every specification (the code behind one kind of test) is the same four steps. A **constructor** takes options and returns a **handle**; the handle opens a **chain** of zero or more setups closed by exactly one terminal action; the action executes and resolves to a **typed result**; the result is asserted through vitest's `expect()`. Nothing else is public: there is no imperative escape hatch, because a spec that can do anything proves nothing in particular.
10
10
 
11
- Eight constructors exist and the list is closed (`src/specifications/specification.ts`):
11
+ Seven constructors exist and the list is closed (`src/specifications/specification.ts`):
12
12
 
13
- | Constructor | Handle destructures to | Subject under test |
14
- | ----------------------------- | --------------------------- | ---------------------------------------------- |
15
- | `specification.api()` | `{ api, cleanup, docker }` | An HTTP API, built and served in this process |
16
- | `specification.job()` | `{ job, cleanup }` | A background pipeline, triggered by name |
17
- | `specification.cli()` | `{ cli, cleanup, docker }` | A command binary in a fresh temp directory |
18
- | `specification.integration()` | `{ integration, cleanup }` | A module against real services, or a golden |
19
- | `specification.website()` | `{ website, cleanup, url }` | A rendered page, fetched or driven in chromium |
20
- | `specification.deployment()` | `{ deployment, cleanup }` | A running deployment, over the network |
21
- | `specification.mobile()` | `{ mobile, cleanup, udid }` | A native screen on an iOS simulator |
22
- | `specification.lint()` | `{ lint, cleanup }` | The rules a project ships, over a fixture tree |
13
+ | Constructor | Handle destructures to | Subject under test |
14
+ | ----------------------------- | --------------------------- | -------------------------------------------------- |
15
+ | `specification.api()` | `{ api, cleanup, docker }` | An HTTP API, served in this process, or at a `url` |
16
+ | `specification.job()` | `{ job, cleanup }` | A background pipeline, triggered by name |
17
+ | `specification.cli()` | `{ cli, cleanup, docker }` | A command binary in a fresh temp directory |
18
+ | `specification.integration()` | `{ integration, cleanup }` | A module against real services, or a golden |
19
+ | `specification.website()` | `{ website, cleanup, url }` | A rendered page, fetched or driven in chromium |
20
+ | `specification.mobile()` | `{ mobile, cleanup, udid }` | A native screen on an iOS simulator |
21
+ | `specification.lint()` | `{ lint, cleanup }` | The rules a project ships, over a fixture tree |
23
22
 
24
- The asymmetry in that column is the model, not an oversight: `docker` reads a container the subject itself started, and neither a pipeline triggered by name nor a module called in-process is specified by the containers it starts, so `job` and `integration` are handed none; `website` and `mobile` drive a browser and a simulator rather than an orchestrated stack, so they carry neither; and `lint` and `deployment` start nothing at all, their `cleanup` releasing nothing: a deployment is running before the suite and after it, and its exchanges hold a socket for their own length only.
23
+ The asymmetry in that column is the model, not an oversight: `docker` reads a container the subject itself started, and neither a pipeline triggered by name nor a module called in-process is specified by the containers it starts, so `job` and `integration` are handed none; `website` and `mobile` drive a browser and a simulator rather than an orchestrated stack, so they carry neither; and `lint` starts nothing at all, its `cleanup` releasing nothing. An `api` met at a `url` is the same subject on another target ([ADR-022](../decisions/022-a-deployed-api-is-an-api-spec-with-a-url.md)): it starts nothing either, so its handle is `{ api, cleanup }`, and its chain is a second composition of the api specification, `apiDeployedDefinition` in `specifications/api/deployed/deployed.composition.ts`, which offers only what a request over the network can do.
25
24
 
26
25
  **One specification has no constructor**, and the same reason explains it: a rendered component starts nothing. `component` ([Component specs](../guides/component-specs.md)) is a chain the package exports directly (`component.intercept(...).render(<X />)`), so there is no handle to destructure and no `cleanup` to pass to `afterAll`. What every render of a project shares (the providers, the Vite pipeline, the viewport) is the PROJECT's, stated once by `component()` in `vitest.config.ts`.
27
26
 
@@ -29,7 +28,7 @@ A runner is created **once per suite**, in a `*.specification.ts` file, and impo
29
28
 
30
29
  ### The seam under the chain
31
30
 
32
- A specification's chain is COMPOSED: `src/specifications/<specification>/<specification>.composition.ts` lists the setups (each a `*.capability.ts`) and the terminal actions (each a `*.action.ts`) it is made of and the adapter each one is armed through, `defineSpecification` opens the chain, and every action runs through the one pipeline of `src/chain/perform.ts`. Every specification (`job`, `api`, `website`, `deployment`, `integration`, `cli`, `mobile`, `lint` and `component`) is composed, and there is no other chain. The infrastructure a chain needs is reached through **ports**, each a `*.port.ts` beside the area or the specification that owns it (`database`, `service`, `container`, `isolation` under `src/services/`; `fixture`, `link` under `src/disk/`; `server`, `cli`, `browser`, `device` in their specification), and an adapter implements one. So the chain knows "a database exists"; it never knows Postgres.
31
+ A specification's chain is COMPOSED: `src/specifications/<specification>/<specification>.composition.ts` lists the setups (each a `*.capability.ts`) and the terminal actions (each a `*.action.ts`) it is made of and the adapter each one is armed through, `defineSpecification` opens the chain, and every action runs through the one pipeline of `src/chain/perform.ts`. Every specification (`job`, `api`, `website`, `integration`, `cli`, `mobile` and `component`) is composed, and there is no other chain. The infrastructure a chain needs is reached through **ports**, each a `*.port.ts` beside the area or the specification that owns it (`database`, `service`, `container`, `isolation` under `src/services/`; `fixture`, `link` under `src/disk/`; `server`, `cli`, `browser`, `device` in their specification), and an adapter implements one. So the chain knows "a database exists"; it never knows Postgres.
33
32
 
34
33
  Every seam is opened lazily rather than imported, and each one is an optional peer loaded by the one module that owns it, so a project that tests no page, no component and no database installs none of them. Which peer each specification and each service needs, and what its refusal says, is [Set up a project § What to install](../start/set-up-a-project.md#what-to-install)'s list; a service's refusal is built by `src/services/peer.ts`, and a specification's by the adapter that loads its peer.
35
34
 
@@ -49,7 +48,7 @@ The root reads the sentence of a test. A test opens a **specification** on its s
49
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 |
50
49
  | `clock/` | `chain/`, `lint/kit/`, itself, `vitest` | The pinned clock and `.clock()`, the instant grammar, the process clock adapter |
51
50
  | `mocks/` | `chain/`, `lint/kit/`, itself, `vitest`, `vitest-mock-extended` | `mockOf()`, the one double of a port |
52
- | `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/`, `exchange/` ...); 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 eight as one record |
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/`, `deployed/`, `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 |
53
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 |
54
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)) |
55
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/` |
@@ -102,7 +101,7 @@ The bundle is built by `typescript build`, from the manifest and the names of th
102
101
 
103
102
  **There are SEVEN entries and no more**: `@jterrazz/test` for what writes a spec (the constructors and their faces, `ruleTester()`, the targets, `match` and the tokens, `clock`, `mockOf()`, `packed()`), `@jterrazz/test/services` for the services a specification starts, `@jterrazz/test/stubs` for what the network replies, `@jterrazz/test/vitest` for the project helpers and the `vitestConfig` the derived `vitest.config.ts` awaits, `@jterrazz/test/oxlint` for the lint plugin, `@jterrazz/test/config` for the `test()` preset a `jterrazz.config.ts` lists, `@jterrazz/test/schema` for an editor validating a `<case>.spec.yaml`. That is rule `no-unpublished-subpath` rather than a convention of taste, and the exemption is derived from the manifest's own `exports` map (the kit's `declaredSubpaths`), so a subpath is exempt the moment it is published and stops being exempt the moment it is withdrawn. A service or a stub imported from the root is rule `service-or-stub-from-its-subpath`, whose fix moves the import: the root then loads no service constructor and no stub builder, though it still carries the startup a specification runs over the services it is handed.
104
103
 
105
- **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 eight 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.
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.
106
105
 
107
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.
108
107
 
@@ -111,7 +110,7 @@ The API is the types the package publishes, and nothing re-states it. What the c
111
110
  ## Pitfalls
112
111
 
113
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.
114
- - **Adding a ninth constructor.** The eight 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), `lint` in [ADR-015](../decisions/015-a-rule-a-project-ships-is-the-lint-specification.md) and `deployment` in [ADR-022](../decisions/022-a-running-deployment-is-the-deployment-specification.md).
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).
115
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.
116
115
 
117
116
  ## Related
@@ -166,9 +166,9 @@ Two more hold what each path says. **The verb and the folder agree**: `seed()` r
166
166
 
167
167
  | Thing | Rule |
168
168
  | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
169
- | Specs root | `specs/` (`api/`, `job/`, `cli/`, `integration/`, `website/`, `deployment/`, `mobile/`, `lint/`): one folder per constructor, beside the special folders of what the specifications share: `__setup__/`, `__fixtures__/`, `__seeds__/`, `__requests__/`, `__expected__/`, and any repository suite ([the third door](#the-third-door-a-repository-suite)) |
169
+ | Specs root | `specs/` (`api/`, `job/`, `cli/`, `integration/`, `website/`, `mobile/`, `lint/`): one folder per constructor, beside the special folders of what the specifications share: `__setup__/`, `__fixtures__/`, `__seeds__/`, `__requests__/`, `__expected__/`, and any repository suite ([the third door](#the-third-door-a-repository-suite)) |
170
170
  | Specification | `specs/<specification>/<name>.specification.ts` (at the specification root) |
171
- | Instances | `api`, `job`, `cli`, `integration`, `website`, `deployment`, `mobile`, `lint`, enforced by the destructuring (`no-runner-alias`) |
171
+ | Instances | `api`, `job`, `cli`, `integration`, `website`, `mobile`, `lint`, enforced by the destructuring (`no-runner-alias`) |
172
172
  | Spec file | `specs/<specification>/<subject>/<aspect>.spec.ts`, the assembled product's word (`file-suffix-matches-place`); the subject's folder derived where the specification has one ([One file, one subject](#one-file-one-subject)) |
173
173
  | Spec document | `<case>.spec.yaml`, beside the spec it belongs to, never under `__expected__/` ([Spec documents](../guides/spec-documents.md)) |
174
174
  | Module test | `__tests__/<file>.test.ts`, in the folder of `<file>.ts` (under `src/`) |