@jterrazz/test 9.0.0 → 9.1.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/README.md CHANGED
@@ -297,7 +297,7 @@ import { mockOf, mockOfDate } from '@jterrazz/test';
297
297
 
298
298
  ## Conventions
299
299
 
300
- Normative rules live in [CONVENTIONS.md](CONVENTIONS.md). A facet (`specs/<facet>/`) carries its runner(s) at its root and holds domain folders; the folder follows the assets:
300
+ Normative rules live in the constitution ([docs/09-conventions.md](docs/09-conventions.md)); the generated per-rule catalogue is [docs/10-linting.md](docs/10-linting.md). A facet (`specs/<facet>/`) carries its runner(s) at its root and holds domain folders; the folder follows the assets:
301
301
 
302
302
  ```
303
303
  specs/<facet>/ # api | jobs | cli | integrations | lint
@@ -318,7 +318,7 @@ Every test contains `// Given -` and `// Then -` comments (always both; `// When
318
318
 
319
319
  ### Convention enforcement — the shipped lint plugin
320
320
 
321
- These conventions are not just prose: the package ships an oxlint plugin (`@jterrazz/test/oxlint`) with ~40 AST rules, plus a `jterrazz-test-check` binary (the conventions checker) that reads the data fixtures and cross-file relationships oxlint cannot. Wire the plugin into your `oxlint.config.ts` and run `jterrazz-test-check specs` in CI — the full catalogue (each rule, its channel and rationale) lives in [CONVENTIONS-CATALOG.md](CONVENTIONS-CATALOG.md).
321
+ These conventions are not just prose: the package ships an oxlint plugin (`@jterrazz/test/oxlint`) with ~40 AST rules, plus a `jterrazz-test-check` binary (the conventions checker) that reads the data fixtures and cross-file relationships oxlint cannot. Wire the plugin into your `oxlint.config.ts` and run `jterrazz-test-check specs` in CI — the full four-channel catalogue (each rule, its channel and rationale) is generated into [docs/10-linting.md](docs/10-linting.md).
322
322
 
323
323
  ## Requirements
324
324
 
@@ -329,6 +329,6 @@ These conventions are not just prose: the package ships an oxlint plugin (`@jter
329
329
 
330
330
  ## Docs
331
331
 
332
- - Guide (chapters): [docs/README.md](docs/README.md) — getting started, API/jobs/CLI specs, assertions, tokens, contracts, services, conventions
333
- - API reference + LLM-friendly docs: <https://jterrazz.github.io/package-test/>
334
- - Agent ingestion: <https://jterrazz.github.io/package-test/llms-full.txt>
332
+ - Guide (chapters): [docs/README.md](docs/README.md) — getting started, API/jobs/CLI specs, assertions, tokens, contracts, services, conventions, linting
333
+ - API reference: committed under [docs/reference/](docs/reference/) — compiled from source by `npm run docs`
334
+ - Agent skill: [skills/jterrazz-test/](skills/jterrazz-test/) — mental model, per-facet references, generated rule reference
package/dist/checker.js CHANGED
@@ -375,7 +375,7 @@ function checkDockerRunnerAwaitUsing(rootDir) {
375
375
  violations.push({
376
376
  file: rel,
377
377
  line,
378
- message: `${rel}:${line}: result of docker-aware runner "${runner}" must be bound with \`await using\` so its containers are disposed (CONVENTIONS B5)`,
378
+ message: `${rel}:${line}: result of docker-aware runner "${runner}" must be bound with \`await using\` so its containers are disposed (B5 — see docs/10-linting.md)`,
379
379
  severity: "error"
380
380
  });
381
381
  }
@@ -431,13 +431,13 @@ function checkDatabaseProperty(rootDir) {
431
431
  if (count >= 2 && !hasDatabase) violations.push({
432
432
  file: rel,
433
433
  line,
434
- message: `${rel}:${line}: .${verb}() must pass { database } — ${count} SQL databases are declared (CONVENTIONS A7)`,
434
+ message: `${rel}:${line}: .${verb}() must pass { database } — ${count} SQL databases are declared (A7 — see docs/10-linting.md)`,
435
435
  severity: "error"
436
436
  });
437
437
  else if (count === 1 && hasDatabase) violations.push({
438
438
  file: rel,
439
439
  line,
440
- message: `${rel}:${line}: .${verb}() must not pass { database } — a single SQL database is declared (CONVENTIONS A7)`,
440
+ message: `${rel}:${line}: .${verb}() must not pass { database } — a single SQL database is declared (A7 — see docs/10-linting.md)`,
441
441
  severity: "error"
442
442
  });
443
443
  }
@@ -485,7 +485,7 @@ function checkDeadFixtures(rootDir) {
485
485
  violations.push({
486
486
  file: rel,
487
487
  line: 1,
488
- message: `${rel}: domain directory has conventional subdirs (${convSubdirs.join(", ")}) but no *.test.ts (CONVENTIONS C9)`,
488
+ message: `${rel}: domain directory has conventional subdirs (${convSubdirs.join(", ")}) but no *.test.ts (C9 — see docs/10-linting.md)`,
489
489
  severity: "error"
490
490
  });
491
491
  continue;
@@ -507,7 +507,7 @@ function checkDeadFixtures(rootDir) {
507
507
  violations.push({
508
508
  file: relEntry,
509
509
  line: 1,
510
- message: `${relEntry}: dead fixture — no test literal in ${feature} references ${sub}/${entry.name} (CONVENTIONS C9)`,
510
+ message: `${relEntry}: dead fixture — no test literal in ${feature} references ${sub}/${entry.name} (C9 — see docs/10-linting.md)`,
511
511
  severity
512
512
  });
513
513
  }
@@ -531,7 +531,7 @@ function checkPoolFixtures(rootDir) {
531
531
  violations.push({
532
532
  file: relEntry,
533
533
  line: 1,
534
- message: `${relEntry}: dead pool fixture — no spec under ${relative(rootDir, specsRoot)} references $FIXTURES/${entry.name} (CONVENTIONS C9)`,
534
+ message: `${relEntry}: dead pool fixture — no spec under ${relative(rootDir, specsRoot)} references $FIXTURES/${entry.name} (C9 — see docs/10-linting.md)`,
535
535
  severity: "error"
536
536
  });
537
537
  }
@@ -663,13 +663,13 @@ function checkConventionFiles(rootDir) {
663
663
  if (depth1 && !REQUEST_LINE.test(firstLine(text))) violations.push({
664
664
  file: rel,
665
665
  line: 1,
666
- message: `${rel}:1: a requests/*.http file must start with a request line "METHOD /path" (CONVENTIONS D4b)`,
666
+ message: `${rel}:1: a requests/*.http file must start with a request line "METHOD /path" (D4b — see docs/10-linting.md)`,
667
667
  severity: "error"
668
668
  });
669
669
  for (const { line, token } of findKnownTokens(text)) violations.push({
670
670
  file: rel,
671
671
  line,
672
- message: `${rel}:${line}: token ${token} in a requests/ file — requests are inputs, never matched; tokens are not validated here (CONVENTIONS D10)`,
672
+ message: `${rel}:${line}: token ${token} in a requests/ file — requests are inputs, never matched; tokens are not validated here (D10 — see docs/10-linting.md)`,
673
673
  severity: "warn",
674
674
  token
675
675
  });
@@ -680,7 +680,7 @@ function checkConventionFiles(rootDir) {
680
680
  if (depth1 && entry.name.endsWith(".http") && !STATUS_LINE.test(firstLine(text))) violations.push({
681
681
  file: rel,
682
682
  line: 1,
683
- message: `${rel}:1: an expected/*.http file must start with a status line "HTTP/1.1 <status>" (CONVENTIONS D4b)`,
683
+ message: `${rel}:1: an expected/*.http file must start with a status line "HTTP/1.1 <status>" (D4b — see docs/10-linting.md)`,
684
684
  severity: "error"
685
685
  });
686
686
  for (const { line, token } of findUnknownTokens(text)) violations.push({
package/dist/oxlint.cjs CHANGED
@@ -657,7 +657,7 @@ const a1SpecificationFile = {
657
657
  },
658
658
  meta: {
659
659
  docs: RULE_DOCS["a1-specification-file"],
660
- messages: { outsideSpecification: "specification.{{member}}() must be called from a `*.specification.ts` file under specs/ (CONVENTIONS A1)." },
660
+ messages: { outsideSpecification: "specification.{{member}}() must be called from a `*.specification.ts` file under specs/ (A1 — see docs/10-linting.md)." },
661
661
  type: "problem"
662
662
  }
663
663
  };
@@ -734,13 +734,13 @@ const a10DuplicateBinding = {
734
734
  },
735
735
  meta: {
736
736
  docs: RULE_DOCS["a10-duplicate-binding"],
737
- messages: { duplicate: "Service keys \"{{first}}\" and \"{{key}}\" both bind to compose service \"{{binding}}\" — one shadows the other; rename a key or set distinct composeService values (CONVENTIONS A10)." },
737
+ messages: { duplicate: "Service keys \"{{first}}\" and \"{{key}}\" both bind to compose service \"{{binding}}\" — one shadows the other; rename a key or set distinct composeService values (A10 — see docs/10-linting.md)." },
738
738
  type: "problem"
739
739
  }
740
740
  };
741
741
  //#endregion
742
742
  //#region src/lint/rules/a2-known-constructors.ts
743
- /** The three constructors, and only three (CONVENTIONS A2). */
743
+ /** The three constructors, and only three (A2 — see docs/10-linting.md). */
744
744
  const KNOWN_CONSTRUCTORS = /* @__PURE__ */ new Set([
745
745
  "api",
746
746
  "cli",
@@ -766,7 +766,7 @@ const a2KnownConstructors = {
766
766
  },
767
767
  meta: {
768
768
  docs: RULE_DOCS["a2-known-constructors"],
769
- messages: { unknownConstructor: "specification.{{member}} does not exist — the only constructors are specification.api(), specification.jobs() and specification.cli() (CONVENTIONS A2)." },
769
+ messages: { unknownConstructor: "specification.{{member}} does not exist — the only constructors are specification.api(), specification.jobs() and specification.cli() (A2 — see docs/10-linting.md)." },
770
770
  type: "problem"
771
771
  }
772
772
  };
@@ -811,7 +811,7 @@ const a3NoDestructureAlias = {
811
811
  },
812
812
  meta: {
813
813
  docs: RULE_DOCS["a3-no-destructure-alias"],
814
- messages: { aliased: "Destructure the specification result with its canonical name: `{{key}}`, not `{{key}}: {{alias}}` (CONVENTIONS A3). Rename at the import site if a different local name is needed." },
814
+ messages: { aliased: "Destructure the specification result with its canonical name: `{{key}}`, not `{{key}}: {{alias}}` (A3 — see docs/10-linting.md). Rename at the import site if a different local name is needed." },
815
815
  type: "problem"
816
816
  }
817
817
  };
@@ -863,7 +863,7 @@ const a4CleanupAfterall = {
863
863
  },
864
864
  meta: {
865
865
  docs: RULE_DOCS["a4-cleanup-afterall"],
866
- messages: { unregistered: "`cleanup` is destructured but never passed to afterAll — add `afterAll(cleanup)` to the specification file (CONVENTIONS A4)." },
866
+ messages: { unregistered: "`cleanup` is destructured but never passed to afterAll — add `afterAll(cleanup)` to the specification file (A4 — see docs/10-linting.md)." },
867
867
  type: "problem"
868
868
  }
869
869
  };
@@ -893,7 +893,7 @@ const a5ModeWithServer = {
893
893
  },
894
894
  meta: {
895
895
  docs: RULE_DOCS["a5-mode-with-server"],
896
- messages: { hardcodedMode: "`mode` must not be hardcoded when `server` is defined — the node/compose switch lives in vitest.config.ts via `env: { TEST_MODE: \"compose\" }` (CONVENTIONS A5)." },
896
+ messages: { hardcodedMode: "`mode` must not be hardcoded when `server` is defined — the node/compose switch lives in vitest.config.ts via `env: { TEST_MODE: \"compose\" }` (A5 — see docs/10-linting.md)." },
897
897
  type: "problem"
898
898
  }
899
899
  };
@@ -934,7 +934,7 @@ const a6wRedundantComposeService = {
934
934
  },
935
935
  meta: {
936
936
  docs: RULE_DOCS["a6w-redundant-compose-service"],
937
- messages: { redundant: "composeService: \"{{name}}\" is redundant — the key \"{{key}}\" already binds to it (exact name or kebab-case derivation, CONVENTIONS A6)." },
937
+ messages: { redundant: "composeService: \"{{name}}\" is redundant — the key \"{{key}}\" already binds to it (exact name or kebab-case derivation, A6 — see docs/10-linting.md)." },
938
938
  type: "suggestion"
939
939
  }
940
940
  };
@@ -998,7 +998,7 @@ function readFileCached(path) {
998
998
  //#endregion
999
999
  //#region src/lint/rules/a9w-redundant-root.ts
1000
1000
  /**
1001
- * The root the convention would derive (CONVENTIONS A9): walk up from the
1001
+ * The root the convention would derive (A9 — see docs/10-linting.md): walk up from the
1002
1002
  * specification file to the first directory containing
1003
1003
  * `docker/compose.test.yaml`, else the first containing `package.json`.
1004
1004
  */
@@ -1037,7 +1037,7 @@ const a9wRedundantRoot = {
1037
1037
  },
1038
1038
  meta: {
1039
1039
  docs: RULE_DOCS["a9w-redundant-root"],
1040
- messages: { redundant: "root: \"{{root}}\" is redundant — walking up from the specification file already resolves to that directory (CONVENTIONS A9)." },
1040
+ messages: { redundant: "root: \"{{root}}\" is redundant — walking up from the specification file already resolves to that directory (A9 — see docs/10-linting.md)." },
1041
1041
  type: "suggestion"
1042
1042
  }
1043
1043
  };
@@ -1101,7 +1101,7 @@ const b2KnownFixtureMarker = {
1101
1101
  },
1102
1102
  meta: {
1103
1103
  docs: RULE_DOCS["b2-known-fixture-marker"],
1104
- messages: { unknownMarker: "Unknown fixture marker \"{{marker}}\" — known markers: {{known}}. A path without a marker is feature-local (CONVENTIONS B2)." },
1104
+ messages: { unknownMarker: "Unknown fixture marker \"{{marker}}\" — known markers: {{known}}. A path without a marker is feature-local (B2 — see docs/10-linting.md)." },
1105
1105
  type: "problem"
1106
1106
  }
1107
1107
  };
@@ -1166,8 +1166,8 @@ const b4GivenThen = {
1166
1166
  meta: {
1167
1167
  docs: RULE_DOCS["b4-given-then"],
1168
1168
  messages: {
1169
- givenAfterThen: "The `// Given -` marker comes after `// Then -` — Given describes the setup and must precede Then (CONVENTIONS B4).",
1170
- missing: "Test is missing a `// {{marker}} -` comment (CONVENTIONS B4 — every test needs both `// Given -` and `// Then -`)."
1169
+ givenAfterThen: "The `// Given -` marker comes after `// Then -` — Given describes the setup and must precede Then (B4 — see docs/10-linting.md).",
1170
+ missing: "Test is missing a `// {{marker}} -` comment (B4 — see docs/10-linting.md — every test needs both `// Given -` and `// Then -`)."
1171
1171
  },
1172
1172
  type: "problem"
1173
1173
  }
@@ -1208,7 +1208,7 @@ const b5AwaitUsing = {
1208
1208
  meta: {
1209
1209
  docs: RULE_DOCS["b5-await-using"],
1210
1210
  defaultOptions: [{ runners: [] }],
1211
- messages: { requireAwaitUsing: "The result of docker-aware runner \"{{runner}}\" must be bound with `await using` so its containers are disposed (CONVENTIONS B5)." },
1211
+ messages: { requireAwaitUsing: "The result of docker-aware runner \"{{runner}}\" must be bound with `await using` so its containers are disposed (B5 — see docs/10-linting.md)." },
1212
1212
  schema: [{
1213
1213
  additionalProperties: false,
1214
1214
  properties: { runners: {
@@ -1251,7 +1251,7 @@ const b6wRedundantEnvUrl = {
1251
1251
  },
1252
1252
  meta: {
1253
1253
  docs: RULE_DOCS["b6w-redundant-env-url"],
1254
- messages: { redundant: ".env({ {{key}}: ….connectionString }) is redundant — the framework already injects <KEY>_URL (and the DATABASE_URL/REDIS_URL aliases) for every service (CONVENTIONS B6)." },
1254
+ messages: { redundant: ".env({ {{key}}: ….connectionString }) is redundant — the framework already injects <KEY>_URL (and the DATABASE_URL/REDIS_URL aliases) for every service (B6 — see docs/10-linting.md)." },
1255
1255
  type: "suggestion"
1256
1256
  }
1257
1257
  };
@@ -1280,7 +1280,7 @@ const b8KebabTrigger = {
1280
1280
  },
1281
1281
  meta: {
1282
1282
  docs: RULE_DOCS["b8-kebab-trigger"],
1283
- messages: { notKebab: "Job name \"{{name}}\" must be a stable kebab-case identifier (e.g. \"nightly-report\") — it is a contract between the app and its tests (CONVENTIONS B8)." },
1283
+ messages: { notKebab: "Job name \"{{name}}\" must be a stable kebab-case identifier (e.g. \"nightly-report\") — it is a contract between the app and its tests (B8 — see docs/10-linting.md)." },
1284
1284
  type: "problem"
1285
1285
  }
1286
1286
  };
@@ -1335,7 +1335,7 @@ const b9wProductCommand = {
1335
1335
  },
1336
1336
  meta: {
1337
1337
  docs: RULE_DOCS["b9w-product-command"],
1338
- messages: { thirdPartyBinary: "specification.cli() points at a third-party binary in node_modules/.bin — a spec should exercise your product command (cli.exec(\"check\"), …), not the tool underneath (CONVENTIONS B9). Suppress with a reason if the product genuinely is this binary." },
1338
+ messages: { thirdPartyBinary: "specification.cli() points at a third-party binary in node_modules/.bin — a spec should exercise your product command (cli.exec(\"check\"), …), not the tool underneath (B9 — see docs/10-linting.md). Suppress with a reason if the product genuinely is this binary." },
1339
1339
  type: "suggestion"
1340
1340
  }
1341
1341
  };
@@ -1391,9 +1391,9 @@ const c1DomainStructure = {
1391
1391
  meta: {
1392
1392
  docs: RULE_DOCS["c1-domain-structure"],
1393
1393
  messages: {
1394
- specNotAtFacetRoot: "A `*.specification.ts` must sit at the facet root: `specs/<facet>/<name>.specification.ts` (CONVENTIONS C1).",
1395
- testAtFacetRoot: "A `*.test.ts` must live in a domain folder: `specs/<facet>/<domain>/<aspect>.test.ts` — tests directly at the facet root are forbidden (CONVENTIONS C1).",
1396
- testTooDeep: "A `*.test.ts` must sit at facet/domain depth: `specs/<facet>/<domain>/<aspect>.test.ts` — no deeper nesting (CONVENTIONS C1)."
1394
+ specNotAtFacetRoot: "A `*.specification.ts` must sit at the facet root: `specs/<facet>/<name>.specification.ts` (C1 — see docs/10-linting.md).",
1395
+ testAtFacetRoot: "A `*.test.ts` must live in a domain folder: `specs/<facet>/<domain>/<aspect>.test.ts` — tests directly at the facet root are forbidden (C1 — see docs/10-linting.md).",
1396
+ testTooDeep: "A `*.test.ts` must sit at facet/domain depth: `specs/<facet>/<domain>/<aspect>.test.ts` — no deeper nesting (C1 — see docs/10-linting.md)."
1397
1397
  },
1398
1398
  type: "problem"
1399
1399
  }
@@ -1423,13 +1423,13 @@ const c2HttpOnlyRequests = {
1423
1423
  },
1424
1424
  meta: {
1425
1425
  docs: RULE_DOCS["c2-http-only-requests"],
1426
- messages: { notHttp: "requests/{{entry}} is not a .http file — requests/ contains only complete .http request files (CONVENTIONS C2)." },
1426
+ messages: { notHttp: "requests/{{entry}} is not a .http file — requests/ contains only complete .http request files (C2 — see docs/10-linting.md)." },
1427
1427
  type: "problem"
1428
1428
  }
1429
1429
  };
1430
1430
  //#endregion
1431
1431
  //#region src/lint/rules/c4-contract-shape.ts
1432
- /** Providers a contract file may declare (CONVENTIONS C4). */
1432
+ /** Providers a contract file may declare (C4 — see docs/10-linting.md). */
1433
1433
  const PROVIDERS = /* @__PURE__ */ new Set([
1434
1434
  "anthropic",
1435
1435
  "http",
@@ -1495,12 +1495,12 @@ const c4ContractShape = {
1495
1495
  meta: {
1496
1496
  docs: RULE_DOCS["c4-contract-shape"],
1497
1497
  messages: {
1498
- badName: "Contract file \"{{base}}\" must be named <name>.<provider>.ts with provider ∈ openai | anthropic | http (CONVENTIONS C4).",
1499
- foreignImport: "Contract imports \"{{source}}\" — a contract imports only from the public entry (@jterrazz/test) (CONVENTIONS C4).",
1500
- missingDefault: "Contract file has no `export default defineContract(...)` (CONVENTIONS C4).",
1501
- namedExport: "Contract files have no named exports — only `export default defineContract(...)` (CONVENTIONS C4).",
1502
- notDefineContract: "The default export of a contract file must be `defineContract(...)` syntactically (CONVENTIONS C4).",
1503
- subfolder: "Contract file \"{{base}}\" is nested — contracts/ is flat, no subfolders (CONVENTIONS C4)."
1498
+ badName: "Contract file \"{{base}}\" must be named <name>.<provider>.ts with provider ∈ openai | anthropic | http (C4 — see docs/10-linting.md).",
1499
+ foreignImport: "Contract imports \"{{source}}\" — a contract imports only from the public entry (@jterrazz/test) (C4 — see docs/10-linting.md).",
1500
+ missingDefault: "Contract file has no `export default defineContract(...)` (C4 — see docs/10-linting.md).",
1501
+ namedExport: "Contract files have no named exports — only `export default defineContract(...)` (C4 — see docs/10-linting.md).",
1502
+ notDefineContract: "The default export of a contract file must be `defineContract(...)` syntactically (C4 — see docs/10-linting.md).",
1503
+ subfolder: "Contract file \"{{base}}\" is nested — contracts/ is flat, no subfolders (C4 — see docs/10-linting.md)."
1504
1504
  },
1505
1505
  type: "problem"
1506
1506
  }
@@ -1551,7 +1551,7 @@ const c6ToMatchExtension = {
1551
1551
  },
1552
1552
  meta: {
1553
1553
  docs: RULE_DOCS["c6-tomatch-extension"],
1554
- messages: { missingExtension: "toMatch(\"{{name}}\") is missing its extension — the extension is part of the fixture name (\"help.txt\", never \"help\"); only directory-tree snapshots (expected/<name>/) omit it (CONVENTIONS C6)." },
1554
+ messages: { missingExtension: "toMatch(\"{{name}}\") is missing its extension — the extension is part of the fixture name (\"help.txt\", never \"help\"); only directory-tree snapshots (expected/<name>/) omit it (C6 — see docs/10-linting.md)." },
1555
1555
  type: "problem"
1556
1556
  }
1557
1557
  };
@@ -1579,7 +1579,7 @@ const c7SeedsSqlOnly = {
1579
1579
  },
1580
1580
  meta: {
1581
1581
  docs: RULE_DOCS["c7-seeds-sql-only"],
1582
- messages: { notSql: "seeds/{{entry}} is not a .sql file — seeds/ carries database state only; file state goes through .fixture() (CONVENTIONS C7)." },
1582
+ messages: { notSql: "seeds/{{entry}} is not a .sql file — seeds/ carries database state only; file state goes through .fixture() (C7 — see docs/10-linting.md)." },
1583
1583
  type: "problem"
1584
1584
  }
1585
1585
  };
@@ -1668,7 +1668,7 @@ const c8ReferencedFixtureExists = {
1668
1668
  },
1669
1669
  meta: {
1670
1670
  docs: RULE_DOCS["c8-referenced-fixture-exists"],
1671
- messages: { missing: "Referenced fixture \"{{path}}\" does not exist on disk under its conventional {{root}}/ root (CONVENTIONS C8). Create it or fix the reference." },
1671
+ messages: { missing: "Referenced fixture \"{{path}}\" does not exist on disk under its conventional {{root}}/ root (C8 — see docs/10-linting.md). Create it or fix the reference." },
1672
1672
  type: "problem"
1673
1673
  }
1674
1674
  };
@@ -1737,7 +1737,7 @@ const d12wResponseBodyProbe = {
1737
1737
  meta: {
1738
1738
  docs: RULE_DOCS["d12w-response-body-probe"],
1739
1739
  defaultOptions: [{ threshold: DEFAULT_THRESHOLD }],
1740
- messages: { bodyProbeCluster: "{{count}} body probes in one test — this wants a full golden: expect(result.response).toMatch('x.http') (CONVENTIONS D12)." },
1740
+ messages: { bodyProbeCluster: "{{count}} body probes in one test — this wants a full golden: expect(result.response).toMatch('x.http') (D12 — see docs/10-linting.md)." },
1741
1741
  schema: [{
1742
1742
  additionalProperties: false,
1743
1743
  properties: { threshold: {
@@ -1820,7 +1820,7 @@ const d13wUnfrozenNegativeFixture = {
1820
1820
  },
1821
1821
  meta: {
1822
1822
  docs: RULE_DOCS["d13w-unfrozen-negative-fixture"],
1823
- messages: { unfrozenNegativeFixture: "This toMatch asserts a mismatch (wrapped in expect(…).toThrow/.rejects) — pass { frozen: true } so TEST_UPDATE=1 never overwrites the deliberately-wrong fixture (CONVENTIONS D13)." },
1823
+ messages: { unfrozenNegativeFixture: "This toMatch asserts a mismatch (wrapped in expect(…).toThrow/.rejects) — pass { frozen: true } so TEST_UPDATE=1 never overwrites the deliberately-wrong fixture (D13 — see docs/10-linting.md)." },
1824
1824
  type: "suggestion"
1825
1825
  }
1826
1826
  };
@@ -1896,7 +1896,7 @@ const d15wStatusOnlyProbe = {
1896
1896
  },
1897
1897
  meta: {
1898
1898
  docs: RULE_DOCS["d15w-status-only-probe"],
1899
- messages: { statusOnlyProbe: "The test's only assertion(s) are status probes — pin the full response: expect(result.response).toMatch('case.http') (CONVENTIONS D11)." },
1899
+ messages: { statusOnlyProbe: "The test's only assertion(s) are status probes — pin the full response: expect(result.response).toMatch('case.http') (D11 — see docs/10-linting.md)." },
1900
1900
  type: "suggestion"
1901
1901
  }
1902
1902
  };
@@ -1951,7 +1951,7 @@ const d2AwaitIoMatcher = {
1951
1951
  },
1952
1952
  meta: {
1953
1953
  docs: RULE_DOCS["d2-await-io-matcher"],
1954
- messages: { mustAwait: "expect(…).{{matcher}}(…) is IO-backed and returns a promise — it must be awaited or returned, else the assertion never runs (CONVENTIONS D2)." },
1954
+ messages: { mustAwait: "expect(…).{{matcher}}(…) is IO-backed and returns a promise — it must be awaited or returned, else the assertion never runs (D2 — see docs/10-linting.md)." },
1955
1955
  type: "problem"
1956
1956
  }
1957
1957
  };
@@ -2005,7 +2005,7 @@ const d2wAwaitSyncMatcher = {
2005
2005
  },
2006
2006
  meta: {
2007
2007
  docs: RULE_DOCS["d2w-await-sync-matcher"],
2008
- messages: { redundantAwait: "await on expect(…).{{matcher}}(…) is redundant — that matcher is synchronous; only IO-backed matchers (toMatchRows/toBeEmpty/toBeRunning) are awaited (CONVENTIONS D2)." },
2008
+ messages: { redundantAwait: "await on expect(…).{{matcher}}(…) is redundant — that matcher is synchronous; only IO-backed matchers (toMatchRows/toBeEmpty/toBeRunning) are awaited (D2 — see docs/10-linting.md)." },
2009
2009
  type: "suggestion"
2010
2010
  }
2011
2011
  };
@@ -2082,7 +2082,7 @@ const d6wTransformTokenEquivalent = {
2082
2082
  },
2083
2083
  meta: {
2084
2084
  docs: RULE_DOCS["d6w-transform-token-equivalent"],
2085
- messages: { tokenEquivalent: "This transform only rewrites output into known token literals — write the tokens in the expected/ fixture instead; transform is an escape hatch for uncovered noise (CONVENTIONS D6)." },
2085
+ messages: { tokenEquivalent: "This transform only rewrites output into known token literals — write the tokens in the expected/ fixture instead; transform is an escape hatch for uncovered noise (D6 — see docs/10-linting.md)." },
2086
2086
  type: "suggestion"
2087
2087
  }
2088
2088
  };
@@ -2116,7 +2116,7 @@ const d8wTextBypass = {
2116
2116
  },
2117
2117
  meta: {
2118
2118
  docs: RULE_DOCS["d8w-text-bypass"],
2119
- messages: { textBypass: "expect(x.text).{{matcher}}(…) asserts on the raw stream — prefer the typed accessor subject (expect(x)) so the token grammar and fixture resolution apply (CONVENTIONS D8)." },
2119
+ messages: { textBypass: "expect(x.text).{{matcher}}(…) asserts on the raw stream — prefer the typed accessor subject (expect(x)) so the token grammar and fixture resolution apply (D8 — see docs/10-linting.md)." },
2120
2120
  type: "suggestion"
2121
2121
  }
2122
2122
  };
@@ -2199,7 +2199,7 @@ const d9wSingleUseRef = {
2199
2199
  },
2200
2200
  meta: {
2201
2201
  docs: RULE_DOCS["d9w-single-use-ref"],
2202
- messages: { singleUse: "Capture ref \"{{ref}}\" is used only once in this spec — a ref earns its name by asserting equality across occurrences; use a plain matcher instead (CONVENTIONS D9)." },
2202
+ messages: { singleUse: "Capture ref \"{{ref}}\" is used only once in this spec — a ref earns its name by asserting equality across occurrences; use a plain matcher instead (D9 — see docs/10-linting.md)." },
2203
2203
  type: "suggestion"
2204
2204
  }
2205
2205
  };
@@ -2227,7 +2227,7 @@ const f1NoSubpathImport = {
2227
2227
  },
2228
2228
  meta: {
2229
2229
  docs: RULE_DOCS["f1-no-subpath-import"],
2230
- messages: { subpath: "Import from \"@jterrazz/test\", not \"{{source}}\" — subpaths do not exist (CONVENTIONS F1; only the tool-facing @jterrazz/test/oxlint is exempt)." },
2230
+ messages: { subpath: "Import from \"@jterrazz/test\", not \"{{source}}\" — subpaths do not exist (F1 — see docs/10-linting.md; only the tool-facing @jterrazz/test/oxlint is exempt)." },
2231
2231
  type: "problem"
2232
2232
  }
2233
2233
  };
@@ -2275,7 +2275,7 @@ const f2NoTestImportsInProd = {
2275
2275
  },
2276
2276
  meta: {
2277
2277
  docs: RULE_DOCS["f2-no-test-imports-in-prod"],
2278
- messages: { testImport: "Production code must not import the test artefact \"{{source}}\" — test imports are only legal from specs/, *.test.ts, *.fixtures.ts or *.specification.ts (CONVENTIONS F2)." },
2278
+ messages: { testImport: "Production code must not import the test artefact \"{{source}}\" — test imports are only legal from specs/, *.test.ts, *.fixtures.ts or *.specification.ts (F2 — see docs/10-linting.md)." },
2279
2279
  type: "problem"
2280
2280
  }
2281
2281
  };
@@ -2339,7 +2339,7 @@ const f3SpecsPublicEntry = {
2339
2339
  },
2340
2340
  meta: {
2341
2341
  docs: RULE_DOCS["f3-specs-public-entry"],
2342
- messages: { deepImport: "specs/ must not deep-import framework internals — reach the framework via its public entry (@jterrazz/test, or src/index.js in this repo), not \"{{source}}\" (CONVENTIONS F3; only @jterrazz/test/oxlint, and src/integrations/** from specs/integrations/, are exempt)." },
2342
+ messages: { deepImport: "specs/ must not deep-import framework internals — reach the framework via its public entry (@jterrazz/test, or src/index.js in this repo), not \"{{source}}\" (F3 — see docs/10-linting.md; only @jterrazz/test/oxlint, and src/integrations/** from specs/integrations/, are exempt)." },
2343
2343
  type: "problem"
2344
2344
  }
2345
2345
  };
@@ -2365,7 +2365,7 @@ const f4NoTestToTestImport = {
2365
2365
  },
2366
2366
  meta: {
2367
2367
  docs: RULE_DOCS["f4-no-test-to-test-import"],
2368
- messages: { testToTest: "A test file must not import another test file (\"{{source}}\") — share data via a *.fixtures.ts neighbour or a *.specification.ts runner (CONVENTIONS F4)." },
2368
+ messages: { testToTest: "A test file must not import another test file (\"{{source}}\") — share data via a *.fixtures.ts neighbour or a *.specification.ts runner (F4 — see docs/10-linting.md)." },
2369
2369
  type: "problem"
2370
2370
  }
2371
2371
  };
@@ -2391,14 +2391,14 @@ const f5FixturesOnlyFromTests = {
2391
2391
  },
2392
2392
  meta: {
2393
2393
  docs: RULE_DOCS["f5-fixtures-only-from-tests"],
2394
- messages: { fixturesImport: "A *.fixtures.ts module (\"{{source}}\") is only importable from *.test.ts files (CONVENTIONS F5)." },
2394
+ messages: { fixturesImport: "A *.fixtures.ts module (\"{{source}}\") is only importable from *.test.ts files (F5 — see docs/10-linting.md)." },
2395
2395
  type: "problem"
2396
2396
  }
2397
2397
  };
2398
2398
  //#endregion
2399
2399
  //#region src/lint/rules/i1-layer-boundaries.ts
2400
2400
  /**
2401
- * One folder = one external dependency (CONVENTIONS I1): the packages each
2401
+ * One folder = one external dependency (I1 — see docs/10-linting.md): the packages each
2402
2402
  * `src/integrations/<folder>/` may import.
2403
2403
  */
2404
2404
  const INTEGRATION_DEPS = {
@@ -2504,10 +2504,10 @@ const i1LayerBoundaries = {
2504
2504
  meta: {
2505
2505
  docs: RULE_DOCS["i1-layer-boundaries"],
2506
2506
  messages: {
2507
- coreExternal: "core/ imports nothing external — \"{{source}}\" is not allowed (CONVENTIONS I1).",
2508
- crossLayer: "Layer \"{{layer}}\" must not import \"{{source}}\" — outside the sanctioned layer edges (CONVENTIONS I1).",
2509
- foreignDependency: "\"{{source}}\" is not this folder's own dependency — one integrations folder = one external dependency (CONVENTIONS I1).",
2510
- lintRuntime: "The lint layer imports nothing from the framework runtime — \"{{source}}\" is outside its pure-helper whitelist (CONVENTIONS I1)."
2507
+ coreExternal: "core/ imports nothing external — \"{{source}}\" is not allowed (I1 — see docs/10-linting.md).",
2508
+ crossLayer: "Layer \"{{layer}}\" must not import \"{{source}}\" — outside the sanctioned layer edges (I1 — see docs/10-linting.md).",
2509
+ foreignDependency: "\"{{source}}\" is not this folder's own dependency — one integrations folder = one external dependency (I1 — see docs/10-linting.md).",
2510
+ lintRuntime: "The lint layer imports nothing from the framework runtime — \"{{source}}\" is outside its pure-helper whitelist (I1 — see docs/10-linting.md)."
2511
2511
  },
2512
2512
  type: "problem"
2513
2513
  }
@@ -2560,9 +2560,9 @@ const i2SiblingTestNaming = {
2560
2560
  meta: {
2561
2561
  docs: RULE_DOCS["i2-sibling-test-naming"],
2562
2562
  messages: {
2563
- orphanTest: "A src/ module test must sit NEXT to the module it tests — no neighbour \"{{expected}}\" found (CONVENTIONS I2). A test needing more than its module is a specification: move it to specs/.",
2564
- rootTests: "A root-level tests/ directory is banned — module tests are siblings under src/, product specifications live in specs/ (CONVENTIONS I2).",
2565
- testsDir: "__tests__/ directories are banned under src/ — the test of <file>.ts is its neighbour <file>.test.ts (CONVENTIONS I2)."
2563
+ orphanTest: "A src/ module test must sit NEXT to the module it tests — no neighbour \"{{expected}}\" found (I2 — see docs/10-linting.md). A test needing more than its module is a specification: move it to specs/.",
2564
+ rootTests: "A root-level tests/ directory is banned — module tests are siblings under src/, product specifications live in specs/ (I2 — see docs/10-linting.md).",
2565
+ testsDir: "__tests__/ directories are banned under src/ — the test of <file>.ts is its neighbour <file>.test.ts (I2 — see docs/10-linting.md)."
2566
2566
  },
2567
2567
  type: "problem"
2568
2568
  }
@@ -2621,9 +2621,9 @@ const i4NoViMockInSrc = {
2621
2621
  meta: {
2622
2622
  docs: RULE_DOCS["i4-no-vi-mock-in-src"],
2623
2623
  messages: {
2624
- assetImport: "A src/ module test must not import the data asset \"{{source}}\" — inline it as code or move the test to specs/ (CONVENTIONS I4).",
2625
- bannedDir: "`{{dir}}/` directories are banned under src/ — mocks and data are code: mockOf/mockOfDate inline, payloads in a *.fixtures.ts neighbour (CONVENTIONS I4).",
2626
- viMock: "`vi.mock` is banned under src/ — use mockOf/mockOfDate (CONVENTIONS I4)."
2624
+ assetImport: "A src/ module test must not import the data asset \"{{source}}\" — inline it as code or move the test to specs/ (I4 — see docs/10-linting.md).",
2625
+ bannedDir: "`{{dir}}/` directories are banned under src/ — mocks and data are code: mockOf/mockOfDate inline, payloads in a *.fixtures.ts neighbour (I4 — see docs/10-linting.md).",
2626
+ viMock: "`vi.mock` is banned under src/ — use mockOf/mockOfDate (I4 — see docs/10-linting.md)."
2627
2627
  },
2628
2628
  type: "problem"
2629
2629
  }
@@ -2667,7 +2667,7 @@ const j1NoOnlySkip = {
2667
2667
  },
2668
2668
  meta: {
2669
2669
  docs: RULE_DOCS["j1-no-only-skip"],
2670
- messages: { forbidden: "Committed `{{runner}}.{{modifier}}` is not allowed (CONVENTIONS J1). Remove it before committing." },
2670
+ messages: { forbidden: "Committed `{{runner}}.{{modifier}}` is not allowed (J1 — see docs/10-linting.md). Remove it before committing." },
2671
2671
  type: "problem"
2672
2672
  }
2673
2673
  };
@@ -2710,8 +2710,8 @@ const j2NoSleepInSpecs = {
2710
2710
  meta: {
2711
2711
  docs: RULE_DOCS["j2-no-sleep-in-specs"],
2712
2712
  messages: {
2713
- sleep: "No arbitrary sleeps in specs — synchronise via `waitFor` or the framework (CONVENTIONS J2).",
2714
- timersImport: "No timer-based sleeps in specs — synchronise via `waitFor` or the framework (CONVENTIONS J2)."
2713
+ sleep: "No arbitrary sleeps in specs — synchronise via `waitFor` or the framework (J2 — see docs/10-linting.md).",
2714
+ timersImport: "No timer-based sleeps in specs — synchronise via `waitFor` or the framework (J2 — see docs/10-linting.md)."
2715
2715
  },
2716
2716
  type: "problem"
2717
2717
  }
@@ -2751,7 +2751,7 @@ const j3NoExpectlessTest = {
2751
2751
  },
2752
2752
  meta: {
2753
2753
  docs: RULE_DOCS["j3-no-expectless-test"],
2754
- messages: { noExpect: "Test has no `expect(…)` — a test must assert something (CONVENTIONS J3). Use `test.todo` for a pending test." },
2754
+ messages: { noExpect: "Test has no `expect(…)` — a test must assert something (J3 — see docs/10-linting.md). Use `test.todo` for a pending test." },
2755
2755
  type: "problem"
2756
2756
  }
2757
2757
  };
@@ -2787,7 +2787,7 @@ const j4UniqueTestNames = {
2787
2787
  },
2788
2788
  meta: {
2789
2789
  docs: RULE_DOCS["j4-unique-test-names"],
2790
- messages: { duplicate: "Duplicate test name \"{{name}}\" in this file — the test name is the sole description of behaviour, so it must be unique (CONVENTIONS J4)." },
2790
+ messages: { duplicate: "Duplicate test name \"{{name}}\" in this file — the test name is the sole description of behaviour, so it must be unique (J4 — see docs/10-linting.md)." },
2791
2791
  type: "problem"
2792
2792
  }
2793
2793
  };
@@ -2883,7 +2883,7 @@ const plugin = {
2883
2883
  },
2884
2884
  meta: {
2885
2885
  docs: RULE_DOCS["j5-lowercase-title"],
2886
- messages: { uppercase: "A {{runner}}() title must start lowercase — the test name is a prose fragment, not a sentence (CONVENTIONS J5)." },
2886
+ messages: { uppercase: "A {{runner}}() title must start lowercase — the test name is a prose fragment, not a sentence (J5 — see docs/10-linting.md)." },
2887
2887
  type: "problem"
2888
2888
  }
2889
2889
  }
package/dist/oxlint.js CHANGED
@@ -653,7 +653,7 @@ const a1SpecificationFile = {
653
653
  },
654
654
  meta: {
655
655
  docs: RULE_DOCS["a1-specification-file"],
656
- messages: { outsideSpecification: "specification.{{member}}() must be called from a `*.specification.ts` file under specs/ (CONVENTIONS A1)." },
656
+ messages: { outsideSpecification: "specification.{{member}}() must be called from a `*.specification.ts` file under specs/ (A1 — see docs/10-linting.md)." },
657
657
  type: "problem"
658
658
  }
659
659
  };
@@ -730,13 +730,13 @@ const a10DuplicateBinding = {
730
730
  },
731
731
  meta: {
732
732
  docs: RULE_DOCS["a10-duplicate-binding"],
733
- messages: { duplicate: "Service keys \"{{first}}\" and \"{{key}}\" both bind to compose service \"{{binding}}\" — one shadows the other; rename a key or set distinct composeService values (CONVENTIONS A10)." },
733
+ messages: { duplicate: "Service keys \"{{first}}\" and \"{{key}}\" both bind to compose service \"{{binding}}\" — one shadows the other; rename a key or set distinct composeService values (A10 — see docs/10-linting.md)." },
734
734
  type: "problem"
735
735
  }
736
736
  };
737
737
  //#endregion
738
738
  //#region src/lint/rules/a2-known-constructors.ts
739
- /** The three constructors, and only three (CONVENTIONS A2). */
739
+ /** The three constructors, and only three (A2 — see docs/10-linting.md). */
740
740
  const KNOWN_CONSTRUCTORS = /* @__PURE__ */ new Set([
741
741
  "api",
742
742
  "cli",
@@ -762,7 +762,7 @@ const a2KnownConstructors = {
762
762
  },
763
763
  meta: {
764
764
  docs: RULE_DOCS["a2-known-constructors"],
765
- messages: { unknownConstructor: "specification.{{member}} does not exist — the only constructors are specification.api(), specification.jobs() and specification.cli() (CONVENTIONS A2)." },
765
+ messages: { unknownConstructor: "specification.{{member}} does not exist — the only constructors are specification.api(), specification.jobs() and specification.cli() (A2 — see docs/10-linting.md)." },
766
766
  type: "problem"
767
767
  }
768
768
  };
@@ -807,7 +807,7 @@ const a3NoDestructureAlias = {
807
807
  },
808
808
  meta: {
809
809
  docs: RULE_DOCS["a3-no-destructure-alias"],
810
- messages: { aliased: "Destructure the specification result with its canonical name: `{{key}}`, not `{{key}}: {{alias}}` (CONVENTIONS A3). Rename at the import site if a different local name is needed." },
810
+ messages: { aliased: "Destructure the specification result with its canonical name: `{{key}}`, not `{{key}}: {{alias}}` (A3 — see docs/10-linting.md). Rename at the import site if a different local name is needed." },
811
811
  type: "problem"
812
812
  }
813
813
  };
@@ -859,7 +859,7 @@ const a4CleanupAfterall = {
859
859
  },
860
860
  meta: {
861
861
  docs: RULE_DOCS["a4-cleanup-afterall"],
862
- messages: { unregistered: "`cleanup` is destructured but never passed to afterAll — add `afterAll(cleanup)` to the specification file (CONVENTIONS A4)." },
862
+ messages: { unregistered: "`cleanup` is destructured but never passed to afterAll — add `afterAll(cleanup)` to the specification file (A4 — see docs/10-linting.md)." },
863
863
  type: "problem"
864
864
  }
865
865
  };
@@ -889,7 +889,7 @@ const a5ModeWithServer = {
889
889
  },
890
890
  meta: {
891
891
  docs: RULE_DOCS["a5-mode-with-server"],
892
- messages: { hardcodedMode: "`mode` must not be hardcoded when `server` is defined — the node/compose switch lives in vitest.config.ts via `env: { TEST_MODE: \"compose\" }` (CONVENTIONS A5)." },
892
+ messages: { hardcodedMode: "`mode` must not be hardcoded when `server` is defined — the node/compose switch lives in vitest.config.ts via `env: { TEST_MODE: \"compose\" }` (A5 — see docs/10-linting.md)." },
893
893
  type: "problem"
894
894
  }
895
895
  };
@@ -930,7 +930,7 @@ const a6wRedundantComposeService = {
930
930
  },
931
931
  meta: {
932
932
  docs: RULE_DOCS["a6w-redundant-compose-service"],
933
- messages: { redundant: "composeService: \"{{name}}\" is redundant — the key \"{{key}}\" already binds to it (exact name or kebab-case derivation, CONVENTIONS A6)." },
933
+ messages: { redundant: "composeService: \"{{name}}\" is redundant — the key \"{{key}}\" already binds to it (exact name or kebab-case derivation, A6 — see docs/10-linting.md)." },
934
934
  type: "suggestion"
935
935
  }
936
936
  };
@@ -994,7 +994,7 @@ function readFileCached(path) {
994
994
  //#endregion
995
995
  //#region src/lint/rules/a9w-redundant-root.ts
996
996
  /**
997
- * The root the convention would derive (CONVENTIONS A9): walk up from the
997
+ * The root the convention would derive (A9 — see docs/10-linting.md): walk up from the
998
998
  * specification file to the first directory containing
999
999
  * `docker/compose.test.yaml`, else the first containing `package.json`.
1000
1000
  */
@@ -1033,7 +1033,7 @@ const a9wRedundantRoot = {
1033
1033
  },
1034
1034
  meta: {
1035
1035
  docs: RULE_DOCS["a9w-redundant-root"],
1036
- messages: { redundant: "root: \"{{root}}\" is redundant — walking up from the specification file already resolves to that directory (CONVENTIONS A9)." },
1036
+ messages: { redundant: "root: \"{{root}}\" is redundant — walking up from the specification file already resolves to that directory (A9 — see docs/10-linting.md)." },
1037
1037
  type: "suggestion"
1038
1038
  }
1039
1039
  };
@@ -1097,7 +1097,7 @@ const b2KnownFixtureMarker = {
1097
1097
  },
1098
1098
  meta: {
1099
1099
  docs: RULE_DOCS["b2-known-fixture-marker"],
1100
- messages: { unknownMarker: "Unknown fixture marker \"{{marker}}\" — known markers: {{known}}. A path without a marker is feature-local (CONVENTIONS B2)." },
1100
+ messages: { unknownMarker: "Unknown fixture marker \"{{marker}}\" — known markers: {{known}}. A path without a marker is feature-local (B2 — see docs/10-linting.md)." },
1101
1101
  type: "problem"
1102
1102
  }
1103
1103
  };
@@ -1162,8 +1162,8 @@ const b4GivenThen = {
1162
1162
  meta: {
1163
1163
  docs: RULE_DOCS["b4-given-then"],
1164
1164
  messages: {
1165
- givenAfterThen: "The `// Given -` marker comes after `// Then -` — Given describes the setup and must precede Then (CONVENTIONS B4).",
1166
- missing: "Test is missing a `// {{marker}} -` comment (CONVENTIONS B4 — every test needs both `// Given -` and `// Then -`)."
1165
+ givenAfterThen: "The `// Given -` marker comes after `// Then -` — Given describes the setup and must precede Then (B4 — see docs/10-linting.md).",
1166
+ missing: "Test is missing a `// {{marker}} -` comment (B4 — see docs/10-linting.md — every test needs both `// Given -` and `// Then -`)."
1167
1167
  },
1168
1168
  type: "problem"
1169
1169
  }
@@ -1204,7 +1204,7 @@ const b5AwaitUsing = {
1204
1204
  meta: {
1205
1205
  docs: RULE_DOCS["b5-await-using"],
1206
1206
  defaultOptions: [{ runners: [] }],
1207
- messages: { requireAwaitUsing: "The result of docker-aware runner \"{{runner}}\" must be bound with `await using` so its containers are disposed (CONVENTIONS B5)." },
1207
+ messages: { requireAwaitUsing: "The result of docker-aware runner \"{{runner}}\" must be bound with `await using` so its containers are disposed (B5 — see docs/10-linting.md)." },
1208
1208
  schema: [{
1209
1209
  additionalProperties: false,
1210
1210
  properties: { runners: {
@@ -1247,7 +1247,7 @@ const b6wRedundantEnvUrl = {
1247
1247
  },
1248
1248
  meta: {
1249
1249
  docs: RULE_DOCS["b6w-redundant-env-url"],
1250
- messages: { redundant: ".env({ {{key}}: ….connectionString }) is redundant — the framework already injects <KEY>_URL (and the DATABASE_URL/REDIS_URL aliases) for every service (CONVENTIONS B6)." },
1250
+ messages: { redundant: ".env({ {{key}}: ….connectionString }) is redundant — the framework already injects <KEY>_URL (and the DATABASE_URL/REDIS_URL aliases) for every service (B6 — see docs/10-linting.md)." },
1251
1251
  type: "suggestion"
1252
1252
  }
1253
1253
  };
@@ -1276,7 +1276,7 @@ const b8KebabTrigger = {
1276
1276
  },
1277
1277
  meta: {
1278
1278
  docs: RULE_DOCS["b8-kebab-trigger"],
1279
- messages: { notKebab: "Job name \"{{name}}\" must be a stable kebab-case identifier (e.g. \"nightly-report\") — it is a contract between the app and its tests (CONVENTIONS B8)." },
1279
+ messages: { notKebab: "Job name \"{{name}}\" must be a stable kebab-case identifier (e.g. \"nightly-report\") — it is a contract between the app and its tests (B8 — see docs/10-linting.md)." },
1280
1280
  type: "problem"
1281
1281
  }
1282
1282
  };
@@ -1331,7 +1331,7 @@ const b9wProductCommand = {
1331
1331
  },
1332
1332
  meta: {
1333
1333
  docs: RULE_DOCS["b9w-product-command"],
1334
- messages: { thirdPartyBinary: "specification.cli() points at a third-party binary in node_modules/.bin — a spec should exercise your product command (cli.exec(\"check\"), …), not the tool underneath (CONVENTIONS B9). Suppress with a reason if the product genuinely is this binary." },
1334
+ messages: { thirdPartyBinary: "specification.cli() points at a third-party binary in node_modules/.bin — a spec should exercise your product command (cli.exec(\"check\"), …), not the tool underneath (B9 — see docs/10-linting.md). Suppress with a reason if the product genuinely is this binary." },
1335
1335
  type: "suggestion"
1336
1336
  }
1337
1337
  };
@@ -1387,9 +1387,9 @@ const c1DomainStructure = {
1387
1387
  meta: {
1388
1388
  docs: RULE_DOCS["c1-domain-structure"],
1389
1389
  messages: {
1390
- specNotAtFacetRoot: "A `*.specification.ts` must sit at the facet root: `specs/<facet>/<name>.specification.ts` (CONVENTIONS C1).",
1391
- testAtFacetRoot: "A `*.test.ts` must live in a domain folder: `specs/<facet>/<domain>/<aspect>.test.ts` — tests directly at the facet root are forbidden (CONVENTIONS C1).",
1392
- testTooDeep: "A `*.test.ts` must sit at facet/domain depth: `specs/<facet>/<domain>/<aspect>.test.ts` — no deeper nesting (CONVENTIONS C1)."
1390
+ specNotAtFacetRoot: "A `*.specification.ts` must sit at the facet root: `specs/<facet>/<name>.specification.ts` (C1 — see docs/10-linting.md).",
1391
+ testAtFacetRoot: "A `*.test.ts` must live in a domain folder: `specs/<facet>/<domain>/<aspect>.test.ts` — tests directly at the facet root are forbidden (C1 — see docs/10-linting.md).",
1392
+ testTooDeep: "A `*.test.ts` must sit at facet/domain depth: `specs/<facet>/<domain>/<aspect>.test.ts` — no deeper nesting (C1 — see docs/10-linting.md)."
1393
1393
  },
1394
1394
  type: "problem"
1395
1395
  }
@@ -1419,13 +1419,13 @@ const c2HttpOnlyRequests = {
1419
1419
  },
1420
1420
  meta: {
1421
1421
  docs: RULE_DOCS["c2-http-only-requests"],
1422
- messages: { notHttp: "requests/{{entry}} is not a .http file — requests/ contains only complete .http request files (CONVENTIONS C2)." },
1422
+ messages: { notHttp: "requests/{{entry}} is not a .http file — requests/ contains only complete .http request files (C2 — see docs/10-linting.md)." },
1423
1423
  type: "problem"
1424
1424
  }
1425
1425
  };
1426
1426
  //#endregion
1427
1427
  //#region src/lint/rules/c4-contract-shape.ts
1428
- /** Providers a contract file may declare (CONVENTIONS C4). */
1428
+ /** Providers a contract file may declare (C4 — see docs/10-linting.md). */
1429
1429
  const PROVIDERS = /* @__PURE__ */ new Set([
1430
1430
  "anthropic",
1431
1431
  "http",
@@ -1491,12 +1491,12 @@ const c4ContractShape = {
1491
1491
  meta: {
1492
1492
  docs: RULE_DOCS["c4-contract-shape"],
1493
1493
  messages: {
1494
- badName: "Contract file \"{{base}}\" must be named <name>.<provider>.ts with provider ∈ openai | anthropic | http (CONVENTIONS C4).",
1495
- foreignImport: "Contract imports \"{{source}}\" — a contract imports only from the public entry (@jterrazz/test) (CONVENTIONS C4).",
1496
- missingDefault: "Contract file has no `export default defineContract(...)` (CONVENTIONS C4).",
1497
- namedExport: "Contract files have no named exports — only `export default defineContract(...)` (CONVENTIONS C4).",
1498
- notDefineContract: "The default export of a contract file must be `defineContract(...)` syntactically (CONVENTIONS C4).",
1499
- subfolder: "Contract file \"{{base}}\" is nested — contracts/ is flat, no subfolders (CONVENTIONS C4)."
1494
+ badName: "Contract file \"{{base}}\" must be named <name>.<provider>.ts with provider ∈ openai | anthropic | http (C4 — see docs/10-linting.md).",
1495
+ foreignImport: "Contract imports \"{{source}}\" — a contract imports only from the public entry (@jterrazz/test) (C4 — see docs/10-linting.md).",
1496
+ missingDefault: "Contract file has no `export default defineContract(...)` (C4 — see docs/10-linting.md).",
1497
+ namedExport: "Contract files have no named exports — only `export default defineContract(...)` (C4 — see docs/10-linting.md).",
1498
+ notDefineContract: "The default export of a contract file must be `defineContract(...)` syntactically (C4 — see docs/10-linting.md).",
1499
+ subfolder: "Contract file \"{{base}}\" is nested — contracts/ is flat, no subfolders (C4 — see docs/10-linting.md)."
1500
1500
  },
1501
1501
  type: "problem"
1502
1502
  }
@@ -1547,7 +1547,7 @@ const c6ToMatchExtension = {
1547
1547
  },
1548
1548
  meta: {
1549
1549
  docs: RULE_DOCS["c6-tomatch-extension"],
1550
- messages: { missingExtension: "toMatch(\"{{name}}\") is missing its extension — the extension is part of the fixture name (\"help.txt\", never \"help\"); only directory-tree snapshots (expected/<name>/) omit it (CONVENTIONS C6)." },
1550
+ messages: { missingExtension: "toMatch(\"{{name}}\") is missing its extension — the extension is part of the fixture name (\"help.txt\", never \"help\"); only directory-tree snapshots (expected/<name>/) omit it (C6 — see docs/10-linting.md)." },
1551
1551
  type: "problem"
1552
1552
  }
1553
1553
  };
@@ -1575,7 +1575,7 @@ const c7SeedsSqlOnly = {
1575
1575
  },
1576
1576
  meta: {
1577
1577
  docs: RULE_DOCS["c7-seeds-sql-only"],
1578
- messages: { notSql: "seeds/{{entry}} is not a .sql file — seeds/ carries database state only; file state goes through .fixture() (CONVENTIONS C7)." },
1578
+ messages: { notSql: "seeds/{{entry}} is not a .sql file — seeds/ carries database state only; file state goes through .fixture() (C7 — see docs/10-linting.md)." },
1579
1579
  type: "problem"
1580
1580
  }
1581
1581
  };
@@ -1664,7 +1664,7 @@ const c8ReferencedFixtureExists = {
1664
1664
  },
1665
1665
  meta: {
1666
1666
  docs: RULE_DOCS["c8-referenced-fixture-exists"],
1667
- messages: { missing: "Referenced fixture \"{{path}}\" does not exist on disk under its conventional {{root}}/ root (CONVENTIONS C8). Create it or fix the reference." },
1667
+ messages: { missing: "Referenced fixture \"{{path}}\" does not exist on disk under its conventional {{root}}/ root (C8 — see docs/10-linting.md). Create it or fix the reference." },
1668
1668
  type: "problem"
1669
1669
  }
1670
1670
  };
@@ -1733,7 +1733,7 @@ const d12wResponseBodyProbe = {
1733
1733
  meta: {
1734
1734
  docs: RULE_DOCS["d12w-response-body-probe"],
1735
1735
  defaultOptions: [{ threshold: DEFAULT_THRESHOLD }],
1736
- messages: { bodyProbeCluster: "{{count}} body probes in one test — this wants a full golden: expect(result.response).toMatch('x.http') (CONVENTIONS D12)." },
1736
+ messages: { bodyProbeCluster: "{{count}} body probes in one test — this wants a full golden: expect(result.response).toMatch('x.http') (D12 — see docs/10-linting.md)." },
1737
1737
  schema: [{
1738
1738
  additionalProperties: false,
1739
1739
  properties: { threshold: {
@@ -1816,7 +1816,7 @@ const d13wUnfrozenNegativeFixture = {
1816
1816
  },
1817
1817
  meta: {
1818
1818
  docs: RULE_DOCS["d13w-unfrozen-negative-fixture"],
1819
- messages: { unfrozenNegativeFixture: "This toMatch asserts a mismatch (wrapped in expect(…).toThrow/.rejects) — pass { frozen: true } so TEST_UPDATE=1 never overwrites the deliberately-wrong fixture (CONVENTIONS D13)." },
1819
+ messages: { unfrozenNegativeFixture: "This toMatch asserts a mismatch (wrapped in expect(…).toThrow/.rejects) — pass { frozen: true } so TEST_UPDATE=1 never overwrites the deliberately-wrong fixture (D13 — see docs/10-linting.md)." },
1820
1820
  type: "suggestion"
1821
1821
  }
1822
1822
  };
@@ -1892,7 +1892,7 @@ const d15wStatusOnlyProbe = {
1892
1892
  },
1893
1893
  meta: {
1894
1894
  docs: RULE_DOCS["d15w-status-only-probe"],
1895
- messages: { statusOnlyProbe: "The test's only assertion(s) are status probes — pin the full response: expect(result.response).toMatch('case.http') (CONVENTIONS D11)." },
1895
+ messages: { statusOnlyProbe: "The test's only assertion(s) are status probes — pin the full response: expect(result.response).toMatch('case.http') (D11 — see docs/10-linting.md)." },
1896
1896
  type: "suggestion"
1897
1897
  }
1898
1898
  };
@@ -1947,7 +1947,7 @@ const d2AwaitIoMatcher = {
1947
1947
  },
1948
1948
  meta: {
1949
1949
  docs: RULE_DOCS["d2-await-io-matcher"],
1950
- messages: { mustAwait: "expect(…).{{matcher}}(…) is IO-backed and returns a promise — it must be awaited or returned, else the assertion never runs (CONVENTIONS D2)." },
1950
+ messages: { mustAwait: "expect(…).{{matcher}}(…) is IO-backed and returns a promise — it must be awaited or returned, else the assertion never runs (D2 — see docs/10-linting.md)." },
1951
1951
  type: "problem"
1952
1952
  }
1953
1953
  };
@@ -2001,7 +2001,7 @@ const d2wAwaitSyncMatcher = {
2001
2001
  },
2002
2002
  meta: {
2003
2003
  docs: RULE_DOCS["d2w-await-sync-matcher"],
2004
- messages: { redundantAwait: "await on expect(…).{{matcher}}(…) is redundant — that matcher is synchronous; only IO-backed matchers (toMatchRows/toBeEmpty/toBeRunning) are awaited (CONVENTIONS D2)." },
2004
+ messages: { redundantAwait: "await on expect(…).{{matcher}}(…) is redundant — that matcher is synchronous; only IO-backed matchers (toMatchRows/toBeEmpty/toBeRunning) are awaited (D2 — see docs/10-linting.md)." },
2005
2005
  type: "suggestion"
2006
2006
  }
2007
2007
  };
@@ -2078,7 +2078,7 @@ const d6wTransformTokenEquivalent = {
2078
2078
  },
2079
2079
  meta: {
2080
2080
  docs: RULE_DOCS["d6w-transform-token-equivalent"],
2081
- messages: { tokenEquivalent: "This transform only rewrites output into known token literals — write the tokens in the expected/ fixture instead; transform is an escape hatch for uncovered noise (CONVENTIONS D6)." },
2081
+ messages: { tokenEquivalent: "This transform only rewrites output into known token literals — write the tokens in the expected/ fixture instead; transform is an escape hatch for uncovered noise (D6 — see docs/10-linting.md)." },
2082
2082
  type: "suggestion"
2083
2083
  }
2084
2084
  };
@@ -2112,7 +2112,7 @@ const d8wTextBypass = {
2112
2112
  },
2113
2113
  meta: {
2114
2114
  docs: RULE_DOCS["d8w-text-bypass"],
2115
- messages: { textBypass: "expect(x.text).{{matcher}}(…) asserts on the raw stream — prefer the typed accessor subject (expect(x)) so the token grammar and fixture resolution apply (CONVENTIONS D8)." },
2115
+ messages: { textBypass: "expect(x.text).{{matcher}}(…) asserts on the raw stream — prefer the typed accessor subject (expect(x)) so the token grammar and fixture resolution apply (D8 — see docs/10-linting.md)." },
2116
2116
  type: "suggestion"
2117
2117
  }
2118
2118
  };
@@ -2195,7 +2195,7 @@ const d9wSingleUseRef = {
2195
2195
  },
2196
2196
  meta: {
2197
2197
  docs: RULE_DOCS["d9w-single-use-ref"],
2198
- messages: { singleUse: "Capture ref \"{{ref}}\" is used only once in this spec — a ref earns its name by asserting equality across occurrences; use a plain matcher instead (CONVENTIONS D9)." },
2198
+ messages: { singleUse: "Capture ref \"{{ref}}\" is used only once in this spec — a ref earns its name by asserting equality across occurrences; use a plain matcher instead (D9 — see docs/10-linting.md)." },
2199
2199
  type: "suggestion"
2200
2200
  }
2201
2201
  };
@@ -2223,7 +2223,7 @@ const f1NoSubpathImport = {
2223
2223
  },
2224
2224
  meta: {
2225
2225
  docs: RULE_DOCS["f1-no-subpath-import"],
2226
- messages: { subpath: "Import from \"@jterrazz/test\", not \"{{source}}\" — subpaths do not exist (CONVENTIONS F1; only the tool-facing @jterrazz/test/oxlint is exempt)." },
2226
+ messages: { subpath: "Import from \"@jterrazz/test\", not \"{{source}}\" — subpaths do not exist (F1 — see docs/10-linting.md; only the tool-facing @jterrazz/test/oxlint is exempt)." },
2227
2227
  type: "problem"
2228
2228
  }
2229
2229
  };
@@ -2271,7 +2271,7 @@ const f2NoTestImportsInProd = {
2271
2271
  },
2272
2272
  meta: {
2273
2273
  docs: RULE_DOCS["f2-no-test-imports-in-prod"],
2274
- messages: { testImport: "Production code must not import the test artefact \"{{source}}\" — test imports are only legal from specs/, *.test.ts, *.fixtures.ts or *.specification.ts (CONVENTIONS F2)." },
2274
+ messages: { testImport: "Production code must not import the test artefact \"{{source}}\" — test imports are only legal from specs/, *.test.ts, *.fixtures.ts or *.specification.ts (F2 — see docs/10-linting.md)." },
2275
2275
  type: "problem"
2276
2276
  }
2277
2277
  };
@@ -2335,7 +2335,7 @@ const f3SpecsPublicEntry = {
2335
2335
  },
2336
2336
  meta: {
2337
2337
  docs: RULE_DOCS["f3-specs-public-entry"],
2338
- messages: { deepImport: "specs/ must not deep-import framework internals — reach the framework via its public entry (@jterrazz/test, or src/index.js in this repo), not \"{{source}}\" (CONVENTIONS F3; only @jterrazz/test/oxlint, and src/integrations/** from specs/integrations/, are exempt)." },
2338
+ messages: { deepImport: "specs/ must not deep-import framework internals — reach the framework via its public entry (@jterrazz/test, or src/index.js in this repo), not \"{{source}}\" (F3 — see docs/10-linting.md; only @jterrazz/test/oxlint, and src/integrations/** from specs/integrations/, are exempt)." },
2339
2339
  type: "problem"
2340
2340
  }
2341
2341
  };
@@ -2361,7 +2361,7 @@ const f4NoTestToTestImport = {
2361
2361
  },
2362
2362
  meta: {
2363
2363
  docs: RULE_DOCS["f4-no-test-to-test-import"],
2364
- messages: { testToTest: "A test file must not import another test file (\"{{source}}\") — share data via a *.fixtures.ts neighbour or a *.specification.ts runner (CONVENTIONS F4)." },
2364
+ messages: { testToTest: "A test file must not import another test file (\"{{source}}\") — share data via a *.fixtures.ts neighbour or a *.specification.ts runner (F4 — see docs/10-linting.md)." },
2365
2365
  type: "problem"
2366
2366
  }
2367
2367
  };
@@ -2387,14 +2387,14 @@ const f5FixturesOnlyFromTests = {
2387
2387
  },
2388
2388
  meta: {
2389
2389
  docs: RULE_DOCS["f5-fixtures-only-from-tests"],
2390
- messages: { fixturesImport: "A *.fixtures.ts module (\"{{source}}\") is only importable from *.test.ts files (CONVENTIONS F5)." },
2390
+ messages: { fixturesImport: "A *.fixtures.ts module (\"{{source}}\") is only importable from *.test.ts files (F5 — see docs/10-linting.md)." },
2391
2391
  type: "problem"
2392
2392
  }
2393
2393
  };
2394
2394
  //#endregion
2395
2395
  //#region src/lint/rules/i1-layer-boundaries.ts
2396
2396
  /**
2397
- * One folder = one external dependency (CONVENTIONS I1): the packages each
2397
+ * One folder = one external dependency (I1 — see docs/10-linting.md): the packages each
2398
2398
  * `src/integrations/<folder>/` may import.
2399
2399
  */
2400
2400
  const INTEGRATION_DEPS = {
@@ -2500,10 +2500,10 @@ const i1LayerBoundaries = {
2500
2500
  meta: {
2501
2501
  docs: RULE_DOCS["i1-layer-boundaries"],
2502
2502
  messages: {
2503
- coreExternal: "core/ imports nothing external — \"{{source}}\" is not allowed (CONVENTIONS I1).",
2504
- crossLayer: "Layer \"{{layer}}\" must not import \"{{source}}\" — outside the sanctioned layer edges (CONVENTIONS I1).",
2505
- foreignDependency: "\"{{source}}\" is not this folder's own dependency — one integrations folder = one external dependency (CONVENTIONS I1).",
2506
- lintRuntime: "The lint layer imports nothing from the framework runtime — \"{{source}}\" is outside its pure-helper whitelist (CONVENTIONS I1)."
2503
+ coreExternal: "core/ imports nothing external — \"{{source}}\" is not allowed (I1 — see docs/10-linting.md).",
2504
+ crossLayer: "Layer \"{{layer}}\" must not import \"{{source}}\" — outside the sanctioned layer edges (I1 — see docs/10-linting.md).",
2505
+ foreignDependency: "\"{{source}}\" is not this folder's own dependency — one integrations folder = one external dependency (I1 — see docs/10-linting.md).",
2506
+ lintRuntime: "The lint layer imports nothing from the framework runtime — \"{{source}}\" is outside its pure-helper whitelist (I1 — see docs/10-linting.md)."
2507
2507
  },
2508
2508
  type: "problem"
2509
2509
  }
@@ -2556,9 +2556,9 @@ const i2SiblingTestNaming = {
2556
2556
  meta: {
2557
2557
  docs: RULE_DOCS["i2-sibling-test-naming"],
2558
2558
  messages: {
2559
- orphanTest: "A src/ module test must sit NEXT to the module it tests — no neighbour \"{{expected}}\" found (CONVENTIONS I2). A test needing more than its module is a specification: move it to specs/.",
2560
- rootTests: "A root-level tests/ directory is banned — module tests are siblings under src/, product specifications live in specs/ (CONVENTIONS I2).",
2561
- testsDir: "__tests__/ directories are banned under src/ — the test of <file>.ts is its neighbour <file>.test.ts (CONVENTIONS I2)."
2559
+ orphanTest: "A src/ module test must sit NEXT to the module it tests — no neighbour \"{{expected}}\" found (I2 — see docs/10-linting.md). A test needing more than its module is a specification: move it to specs/.",
2560
+ rootTests: "A root-level tests/ directory is banned — module tests are siblings under src/, product specifications live in specs/ (I2 — see docs/10-linting.md).",
2561
+ testsDir: "__tests__/ directories are banned under src/ — the test of <file>.ts is its neighbour <file>.test.ts (I2 — see docs/10-linting.md)."
2562
2562
  },
2563
2563
  type: "problem"
2564
2564
  }
@@ -2617,9 +2617,9 @@ const i4NoViMockInSrc = {
2617
2617
  meta: {
2618
2618
  docs: RULE_DOCS["i4-no-vi-mock-in-src"],
2619
2619
  messages: {
2620
- assetImport: "A src/ module test must not import the data asset \"{{source}}\" — inline it as code or move the test to specs/ (CONVENTIONS I4).",
2621
- bannedDir: "`{{dir}}/` directories are banned under src/ — mocks and data are code: mockOf/mockOfDate inline, payloads in a *.fixtures.ts neighbour (CONVENTIONS I4).",
2622
- viMock: "`vi.mock` is banned under src/ — use mockOf/mockOfDate (CONVENTIONS I4)."
2620
+ assetImport: "A src/ module test must not import the data asset \"{{source}}\" — inline it as code or move the test to specs/ (I4 — see docs/10-linting.md).",
2621
+ bannedDir: "`{{dir}}/` directories are banned under src/ — mocks and data are code: mockOf/mockOfDate inline, payloads in a *.fixtures.ts neighbour (I4 — see docs/10-linting.md).",
2622
+ viMock: "`vi.mock` is banned under src/ — use mockOf/mockOfDate (I4 — see docs/10-linting.md)."
2623
2623
  },
2624
2624
  type: "problem"
2625
2625
  }
@@ -2663,7 +2663,7 @@ const j1NoOnlySkip = {
2663
2663
  },
2664
2664
  meta: {
2665
2665
  docs: RULE_DOCS["j1-no-only-skip"],
2666
- messages: { forbidden: "Committed `{{runner}}.{{modifier}}` is not allowed (CONVENTIONS J1). Remove it before committing." },
2666
+ messages: { forbidden: "Committed `{{runner}}.{{modifier}}` is not allowed (J1 — see docs/10-linting.md). Remove it before committing." },
2667
2667
  type: "problem"
2668
2668
  }
2669
2669
  };
@@ -2706,8 +2706,8 @@ const j2NoSleepInSpecs = {
2706
2706
  meta: {
2707
2707
  docs: RULE_DOCS["j2-no-sleep-in-specs"],
2708
2708
  messages: {
2709
- sleep: "No arbitrary sleeps in specs — synchronise via `waitFor` or the framework (CONVENTIONS J2).",
2710
- timersImport: "No timer-based sleeps in specs — synchronise via `waitFor` or the framework (CONVENTIONS J2)."
2709
+ sleep: "No arbitrary sleeps in specs — synchronise via `waitFor` or the framework (J2 — see docs/10-linting.md).",
2710
+ timersImport: "No timer-based sleeps in specs — synchronise via `waitFor` or the framework (J2 — see docs/10-linting.md)."
2711
2711
  },
2712
2712
  type: "problem"
2713
2713
  }
@@ -2747,7 +2747,7 @@ const j3NoExpectlessTest = {
2747
2747
  },
2748
2748
  meta: {
2749
2749
  docs: RULE_DOCS["j3-no-expectless-test"],
2750
- messages: { noExpect: "Test has no `expect(…)` — a test must assert something (CONVENTIONS J3). Use `test.todo` for a pending test." },
2750
+ messages: { noExpect: "Test has no `expect(…)` — a test must assert something (J3 — see docs/10-linting.md). Use `test.todo` for a pending test." },
2751
2751
  type: "problem"
2752
2752
  }
2753
2753
  };
@@ -2783,7 +2783,7 @@ const j4UniqueTestNames = {
2783
2783
  },
2784
2784
  meta: {
2785
2785
  docs: RULE_DOCS["j4-unique-test-names"],
2786
- messages: { duplicate: "Duplicate test name \"{{name}}\" in this file — the test name is the sole description of behaviour, so it must be unique (CONVENTIONS J4)." },
2786
+ messages: { duplicate: "Duplicate test name \"{{name}}\" in this file — the test name is the sole description of behaviour, so it must be unique (J4 — see docs/10-linting.md)." },
2787
2787
  type: "problem"
2788
2788
  }
2789
2789
  };
@@ -2879,7 +2879,7 @@ const plugin = {
2879
2879
  },
2880
2880
  meta: {
2881
2881
  docs: RULE_DOCS["j5-lowercase-title"],
2882
- messages: { uppercase: "A {{runner}}() title must start lowercase — the test name is a prose fragment, not a sentence (CONVENTIONS J5)." },
2882
+ messages: { uppercase: "A {{runner}}() title must start lowercase — the test name is a prose fragment, not a sentence (J5 — see docs/10-linting.md)." },
2883
2883
  type: "problem"
2884
2884
  }
2885
2885
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jterrazz/test",
3
- "version": "9.0.0",
3
+ "version": "9.1.0",
4
4
  "description": "Declarative testing framework for HTTP APIs, CLIs, and background jobs — one entry point, real infrastructure, golden-file assertions, plus an oxlint convention plugin.",
5
5
  "keywords": [
6
6
  "api-testing",
@@ -46,7 +46,7 @@
46
46
  },
47
47
  "scripts": {
48
48
  "build": "tsdown --config tsdown.config.ts",
49
- "docs": "typescript docs && npm run build && node dist/catalog.js",
49
+ "docs": "npm run build && node dist/catalog.js && typescript docs",
50
50
  "lint": "typescript check && node dist/checker.js specs",
51
51
  "lint:fix": "typescript fix",
52
52
  "test": "vitest --run"
@@ -63,7 +63,7 @@
63
63
  },
64
64
  "devDependencies": {
65
65
  "@hono/node-server": "^2.0.8",
66
- "@jterrazz/typescript": "^6.1.0",
66
+ "@jterrazz/typescript": "^7.0.0",
67
67
  "@types/better-sqlite3": "^7.6.13",
68
68
  "@types/node": "^26.1.1",
69
69
  "@types/pg": "^8.20.0",