@jterrazz/test 14.2.0 → 15.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/oxlint.js CHANGED
@@ -194,21 +194,10 @@ function isTestCallee(callee) {
194
194
  if (callee.type === "CallExpression") return isTestCallee(callee.callee);
195
195
  return false;
196
196
  }
197
- /** Does the test callee chain carry a modifier member with the given name? */
198
- function testCalleeHasModifier(callee, modifier) {
199
- if (callee === void 0) return false;
200
- if (callee.type === "MemberExpression") return memberPropertyName(callee) === modifier || testCalleeHasModifier(callee.object, modifier);
201
- if (callee.type === "CallExpression") return testCalleeHasModifier(callee.callee, modifier);
202
- return false;
203
- }
204
197
  /** The callback argument of a test call (arrow or function expression), if any. */
205
198
  function findTestCallback(args) {
206
199
  return args.find((arg) => arg.type === "ArrowFunctionExpression" || arg.type === "FunctionExpression");
207
200
  }
208
- /** The static string name of a test call (`test('name', …)`), if a literal. */
209
- function testName(callNode) {
210
- return stringValue((callNode.arguments ?? [])[0]);
211
- }
212
201
  /**
213
202
  * Visitor fragment collecting every import-like source: static imports,
214
203
  * dynamic `import()`, and re-exports. Rules spread this into their visitor.
@@ -235,6 +224,26 @@ function importSourceVisitor(onSource) {
235
224
  * The **statique** channel — one entry per shipped `jterrazz/*` rule. Each rule
236
225
  * file attaches its entry as `meta.docs`, so a rule and its normative text can
237
226
  * never drift (the completeness meta-test asserts every rule carries one).
227
+ *
228
+ * **What this channel does NOT own.** A convention oxlint's own `vitest` plugin
229
+ * already enforces belongs to that plugin, not here — `@jterrazz/typescript` v10
230
+ * turns the plugin on over the test globs in every profile, so a consumer of the
231
+ * preset gets it without wiring. Four hygiene rules left the channel on that
232
+ * ground (J1, J3, J4, J5 → `vitest/no-focused-tests` + `vitest/no-disabled-tests`,
233
+ * `vitest/expect-expect`, `vitest/no-identical-title`, `vitest/prefer-lowercase-title`).
234
+ * `prefer-lowercase-title` is stricter than J5 was: a title opening on an all-caps
235
+ * identifier (`HTTP 404 …`, `DI …`) now fails, and the rulebook carries no
236
+ * `allowedPrefixes` — a title starts lowercase, whatever its first word. The
237
+ * rest are behaviour-identical on this package's own fixtures. Only J2 (the
238
+ * arbitrary-sleep ban, which has no upstream counterpart) stays statique.
239
+ *
240
+ * **Severity.** The estate's rulebook has no warn tier: a rule is on at `error`
241
+ * or off with a recorded reason, and that law binds `@jterrazz/typescript`'s
242
+ * rulebook. This channel is its one documented exception, and it is narrow — an
243
+ * id shaped `<family><n>w-…` is a REDUNDANCY heuristic (a `root` the walk would
244
+ * have found, a probe cluster that wants a golden), a reading of taste that
245
+ * would block a legitimate shape if it failed the build. Every hard convention
246
+ * here is an `error`; `plugin.test.ts` holds the split.
238
247
  */
239
248
  const RULE_DOCS = {
240
249
  "a1-specification-file": {
@@ -510,13 +519,6 @@ const RULE_DOCS = {
510
519
  id: "I4",
511
520
  rationale: "Dans les tests de module, mocks et données sont du CODE (`mockOf`, `*.fixtures.ts`) ; un vrai fichier appelle une spec."
512
521
  },
513
- "j1-no-only-skip": {
514
- channel: "statique",
515
- convention: "Aucun `.only` / `.skip` committé (`describe.only`, `test.only`, `test.skip`).",
516
- family: "J",
517
- id: "J1",
518
- rationale: "Un `.only`/`.skip` oublié désactive silencieusement une partie de la suite."
519
- },
520
522
  "j2-no-sleep-in-specs": {
521
523
  channel: "statique",
522
524
  convention: "Aucun sleep arbitraire (`setTimeout`/`setInterval`/`Atomics.wait`) sous `specs/**` — la synchronisation passe par `waitFor`.",
@@ -524,27 +526,6 @@ const RULE_DOCS = {
524
526
  id: "J2",
525
527
  rationale: "Un sleep fixe rend les tests lents et instables ; attendre une condition est déterministe."
526
528
  },
527
- "j3-no-expectless-test": {
528
- channel: "statique",
529
- convention: "Un `test(...)` avec callback contient au moins un `expect(…)` ; `test.todo` (sans callback) est ignoré.",
530
- family: "J",
531
- id: "J3",
532
- rationale: "Un test sans assertion est mort ou muet — il passe toujours sans rien vérifier."
533
- },
534
- "j4-unique-test-names": {
535
- channel: "statique",
536
- convention: "Deux tests d’un même fichier ne partagent pas un nom littéral (`.each` ignoré).",
537
- family: "J",
538
- id: "J4",
539
- rationale: "Le nom du test est son unique description — deux noms identiques rendent un échec ambigu."
540
- },
541
- "j5-lowercase-title": {
542
- channel: "statique",
543
- convention: "La première lettre d’un titre `test()`/`describe()`/`it()` littéral est en minuscule. Exemptés : les titres dont le premier MOT est un identifiant tout en majuscules/underscores (`VALID_CATEGORIES`, `HTTP`, `DI`) et ceux démarrant sur un non-lettre — seuls les premiers mots de prose minusculisables sont contraints.",
544
- family: "J",
545
- id: "J5",
546
- rationale: "Un titre est un fragment de prose, pas une phrase — la casse minuscule le garde fragmentaire ; minusculiser un symbole nommé le mal-orthographierait."
547
- },
548
529
  "w1-scenario-pure": {
549
530
  channel: "statique",
550
531
  convention: "Un scénario (`.visit()` website, `.open()` mobile) est le When : le visiteur agit, la capture reflète l’état final ; aucun `expect()` dans le callback — les assertions vivent dans le Then, sur le résultat retourné.",
@@ -903,7 +884,7 @@ const a1SpecificationFile = {
903
884
  },
904
885
  meta: {
905
886
  docs: RULE_DOCS["a1-specification-file"],
906
- messages: { outsideSpecification: "specification.{{member}}() must be called from a `*.specification.ts` file under specs/ (A1 — see docs/10-linting.md)." },
887
+ messages: { outsideSpecification: "specification.{{member}}() must be called from a `*.specification.ts` file under specs/ (A1 — see docs/13-linting.md)." },
907
888
  type: "problem"
908
889
  }
909
890
  };
@@ -980,13 +961,13 @@ const a10DuplicateBinding = {
980
961
  },
981
962
  meta: {
982
963
  docs: RULE_DOCS["a10-duplicate-binding"],
983
- 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)." },
964
+ 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/13-linting.md)." },
984
965
  type: "problem"
985
966
  }
986
967
  };
987
968
  //#endregion
988
969
  //#region src/lint/rules/a2-known-constructors.ts
989
- /** The five constructors, and only five (A2 — see docs/10-linting.md). */
970
+ /** The five constructors, and only five (A2 — see docs/13-linting.md). */
990
971
  const KNOWN_CONSTRUCTORS = /* @__PURE__ */ new Set([
991
972
  "api",
992
973
  "cli",
@@ -1015,7 +996,7 @@ const a2KnownConstructors = {
1015
996
  },
1016
997
  meta: {
1017
998
  docs: RULE_DOCS["a2-known-constructors"],
1018
- messages: { unknownConstructor: "specification.{{member}} does not exist — the only constructors are specification.api(), specification.jobs(), specification.cli(), specification.website() and specification.mobile() (A2 — see docs/10-linting.md)." },
999
+ messages: { unknownConstructor: "specification.{{member}} does not exist — the only constructors are specification.api(), specification.jobs(), specification.cli(), specification.website() and specification.mobile() (A2 — see docs/13-linting.md)." },
1019
1000
  type: "problem"
1020
1001
  }
1021
1002
  };
@@ -1060,7 +1041,7 @@ const a3NoDestructureAlias = {
1060
1041
  },
1061
1042
  meta: {
1062
1043
  docs: RULE_DOCS["a3-no-destructure-alias"],
1063
- 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." },
1044
+ messages: { aliased: "Destructure the specification result with its canonical name: `{{key}}`, not `{{key}}: {{alias}}` (A3 — see docs/13-linting.md). Rename at the import site if a different local name is needed." },
1064
1045
  type: "problem"
1065
1046
  }
1066
1047
  };
@@ -1112,7 +1093,7 @@ const a4CleanupAfterall = {
1112
1093
  },
1113
1094
  meta: {
1114
1095
  docs: RULE_DOCS["a4-cleanup-afterall"],
1115
- messages: { unregistered: "`cleanup` is destructured but never passed to afterAll — add `afterAll(cleanup)` to the specification file (A4 — see docs/10-linting.md)." },
1096
+ messages: { unregistered: "`cleanup` is destructured but never passed to afterAll — add `afterAll(cleanup)` to the specification file (A4 — see docs/13-linting.md)." },
1116
1097
  type: "problem"
1117
1098
  }
1118
1099
  };
@@ -1142,7 +1123,7 @@ const a5ModeWithServer = {
1142
1123
  },
1143
1124
  meta: {
1144
1125
  docs: RULE_DOCS["a5-mode-with-server"],
1145
- 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)." },
1126
+ 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/13-linting.md)." },
1146
1127
  type: "problem"
1147
1128
  }
1148
1129
  };
@@ -1183,7 +1164,7 @@ const a6wRedundantComposeService = {
1183
1164
  },
1184
1165
  meta: {
1185
1166
  docs: RULE_DOCS["a6w-redundant-compose-service"],
1186
- 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)." },
1167
+ messages: { redundant: "composeService: \"{{name}}\" is redundant — the key \"{{key}}\" already binds to it (exact name or kebab-case derivation, A6 — see docs/13-linting.md)." },
1187
1168
  type: "suggestion"
1188
1169
  }
1189
1170
  };
@@ -1218,7 +1199,7 @@ function findRoot(startDir, exists) {
1218
1199
  //#endregion
1219
1200
  //#region src/lint/rules/a9w-redundant-root.ts
1220
1201
  /**
1221
- * The root the convention would derive (A9 — see docs/10-linting.md): the
1202
+ * The root the convention would derive (A9 — see docs/13-linting.md): the
1222
1203
  * framework's own walk-up, run against this layer's cached fs probe. The rule
1223
1204
  * flags a `root` that the runner would have found by itself, so the two MUST
1224
1205
  * be the same function — a private copy of the walk drifted from it before.
@@ -1250,7 +1231,7 @@ const a9wRedundantRoot = {
1250
1231
  },
1251
1232
  meta: {
1252
1233
  docs: RULE_DOCS["a9w-redundant-root"],
1253
- messages: { redundant: "root: \"{{root}}\" is redundant — walking up from the specification file already resolves to that directory (A9 — see docs/10-linting.md)." },
1234
+ messages: { redundant: "root: \"{{root}}\" is redundant — walking up from the specification file already resolves to that directory (A9 — see docs/13-linting.md)." },
1254
1235
  type: "suggestion"
1255
1236
  }
1256
1237
  };
@@ -1356,7 +1337,7 @@ const b2KnownFixtureMarker = {
1356
1337
  },
1357
1338
  meta: {
1358
1339
  docs: RULE_DOCS["b2-known-fixture-marker"],
1359
- messages: { unknownMarker: "Unknown fixture marker \"{{marker}}\" — known markers: {{known}}. A path without a marker is feature-local (B2 — see docs/10-linting.md)." },
1340
+ messages: { unknownMarker: "Unknown fixture marker \"{{marker}}\" — known markers: {{known}}. A path without a marker is feature-local (B2 — see docs/13-linting.md)." },
1360
1341
  type: "problem"
1361
1342
  }
1362
1343
  };
@@ -1421,8 +1402,8 @@ const b4GivenThen = {
1421
1402
  meta: {
1422
1403
  docs: RULE_DOCS["b4-given-then"],
1423
1404
  messages: {
1424
- givenAfterThen: "The `// Given -` marker comes after `// Then -` — Given describes the setup and must precede Then (B4 — see docs/10-linting.md).",
1425
- missing: "Test is missing a `// {{marker}} -` comment (B4 — see docs/10-linting.md — every test needs both `// Given -` and `// Then -`)."
1405
+ givenAfterThen: "The `// Given -` marker comes after `// Then -` — Given describes the setup and must precede Then (B4 — see docs/13-linting.md).",
1406
+ missing: "Test is missing a `// {{marker}} -` comment (B4 — see docs/13-linting.md — every test needs both `// Given -` and `// Then -`)."
1426
1407
  },
1427
1408
  type: "problem"
1428
1409
  }
@@ -1463,7 +1444,7 @@ const b5AwaitUsing = {
1463
1444
  meta: {
1464
1445
  docs: RULE_DOCS["b5-await-using"],
1465
1446
  defaultOptions: [{ runners: [] }],
1466
- 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)." },
1447
+ messages: { requireAwaitUsing: "The result of docker-aware runner \"{{runner}}\" must be bound with `await using` so its containers are disposed (B5 — see docs/13-linting.md)." },
1467
1448
  schema: [{
1468
1449
  additionalProperties: false,
1469
1450
  properties: { runners: {
@@ -1506,7 +1487,7 @@ const b6wRedundantEnvUrl = {
1506
1487
  },
1507
1488
  meta: {
1508
1489
  docs: RULE_DOCS["b6w-redundant-env-url"],
1509
- 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)." },
1490
+ 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/13-linting.md)." },
1510
1491
  type: "suggestion"
1511
1492
  }
1512
1493
  };
@@ -1535,7 +1516,7 @@ const b8KebabTrigger = {
1535
1516
  },
1536
1517
  meta: {
1537
1518
  docs: RULE_DOCS["b8-kebab-trigger"],
1538
- 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)." },
1519
+ 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/13-linting.md)." },
1539
1520
  type: "problem"
1540
1521
  }
1541
1522
  };
@@ -1590,7 +1571,7 @@ const b9wProductCommand = {
1590
1571
  },
1591
1572
  meta: {
1592
1573
  docs: RULE_DOCS["b9w-product-command"],
1593
- 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." },
1574
+ 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/13-linting.md). Suppress with a reason if the product genuinely is this binary." },
1594
1575
  type: "suggestion"
1595
1576
  }
1596
1577
  };
@@ -1743,14 +1724,14 @@ const c1DomainStructure = {
1743
1724
  defaultOptions: [{ depth: DEFAULT_DEPTH }],
1744
1725
  docs: RULE_DOCS["c1-domain-structure"],
1745
1726
  messages: {
1746
- specInGround: "A spec must not live under \"{{ground}}/\" — a leading underscore marks GROUND (what the specs of a row stand on: _fixtures/, _expected/, _requests/, _seeds/), never a domain (C1 — see docs/10-linting.md). Ground that is CODE keeps its own unit test: `<module>.test.ts` NEXT to the `<module>.ts` it tests (I2), and nothing else.",
1747
- specNotAtFacetRoot: "A `*.specification.ts` must sit at the facet root: `specs/<facet>/<name>.specification.ts` (C1 — see docs/10-linting.md).",
1748
- 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).",
1749
- testAtSpecsRoot: "A `*.test.ts` must live in a directory under specs/ — with `depth: \"mirror\"` the tree mirrors a structure, and the specs root mirrors nothing (C1 — see docs/10-linting.md).",
1750
- testNotMirroringDirectory: "A `*.test.ts` must be named after the directory holding it — `{{directory}}/{{directory}}.test.ts` — with `depth: \"mirror\"` (C1 — see docs/10-linting.md).",
1751
- testOutsideFacet: "A `*.test.ts` must live in a facet folder — `specs/<facet>/<aspect>.test.ts`, or one domain deeper: with `depth: \"facet\"` the specs root holds facets, not tests (C1 — see docs/10-linting.md).",
1752
- 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).",
1753
- testTooDeepForFacet: "A `*.test.ts` must sit at the facet root or one domain folder down — `specs/<facet>/<aspect>.test.ts` or `specs/<facet>/<domain>/<aspect>.test.ts`: with `depth: \"facet\"` nothing nests deeper (C1 — see docs/10-linting.md)."
1727
+ specInGround: "A spec must not live under \"{{ground}}/\" — a leading underscore marks GROUND (what the specs of a row stand on: _fixtures/, _expected/, _requests/, _seeds/), never a domain (C1 — see docs/13-linting.md). Ground that is CODE keeps its own unit test: `<module>.test.ts` NEXT to the `<module>.ts` it tests (I2), and nothing else.",
1728
+ specNotAtFacetRoot: "A `*.specification.ts` must sit at the facet root: `specs/<facet>/<name>.specification.ts` (C1 — see docs/13-linting.md).",
1729
+ 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/13-linting.md).",
1730
+ testAtSpecsRoot: "A `*.test.ts` must live in a directory under specs/ — with `depth: \"mirror\"` the tree mirrors a structure, and the specs root mirrors nothing (C1 — see docs/13-linting.md).",
1731
+ testNotMirroringDirectory: "A `*.test.ts` must be named after the directory holding it — `{{directory}}/{{directory}}.test.ts` — with `depth: \"mirror\"` (C1 — see docs/13-linting.md).",
1732
+ testOutsideFacet: "A `*.test.ts` must live in a facet folder — `specs/<facet>/<aspect>.test.ts`, or one domain deeper: with `depth: \"facet\"` the specs root holds facets, not tests (C1 — see docs/13-linting.md).",
1733
+ testTooDeep: "A `*.test.ts` must sit at facet/domain depth: `specs/<facet>/<domain>/<aspect>.test.ts` — no deeper nesting (C1 — see docs/13-linting.md).",
1734
+ testTooDeepForFacet: "A `*.test.ts` must sit at the facet root or one domain folder down — `specs/<facet>/<aspect>.test.ts` or `specs/<facet>/<domain>/<aspect>.test.ts`: with `depth: \"facet\"` nothing nests deeper (C1 — see docs/13-linting.md)."
1754
1735
  },
1755
1736
  schema: [{
1756
1737
  additionalProperties: false,
@@ -1795,7 +1776,7 @@ const c10ContractsBoundary = {
1795
1776
  },
1796
1777
  meta: {
1797
1778
  docs: RULE_DOCS["c10-contracts-boundary"],
1798
- messages: { internal: "Import \"{{source}}\" reaches into a provider folder — only contracts/*.contracts.ts is importable from a test; add a named scenario export there instead (C10 — see docs/10-linting.md)." },
1779
+ messages: { internal: "Import \"{{source}}\" reaches into a provider folder — only contracts/*.contracts.ts is importable from a test; add a named scenario export there instead (C10 — see docs/13-linting.md)." },
1799
1780
  type: "problem"
1800
1781
  }
1801
1782
  };
@@ -1850,7 +1831,7 @@ const c11ContractDataPairing = {
1850
1831
  },
1851
1832
  meta: {
1852
1833
  docs: RULE_DOCS["c11-contract-data-pairing"],
1853
- messages: { orphan: "contracts/{{provider}}/{{entry}} has no owning contract — data pairs with the sibling {{owner}} (stem = name up to the first dot) (C11 — see docs/10-linting.md)." },
1834
+ messages: { orphan: "contracts/{{provider}}/{{entry}} has no owning contract — data pairs with the sibling {{owner}} (stem = name up to the first dot) (C11 — see docs/13-linting.md)." },
1854
1835
  type: "problem"
1855
1836
  }
1856
1837
  };
@@ -1903,7 +1884,7 @@ const c13UnderscoredGround = {
1903
1884
  },
1904
1885
  meta: {
1905
1886
  docs: RULE_DOCS["c13-underscored-ground"],
1906
- messages: { notUnderscored: "The directory \"{{legacy}}/\" is a pre-14 ground name — rename it to \"{{ground}}/\" (`git mv {{legacy}} {{ground}}`). What a spec stands on carries the underscore; a spec's own folder never does, and no resolver reads the un-underscored name (C13 — see docs/10-linting.md)." },
1887
+ messages: { notUnderscored: "The directory \"{{legacy}}/\" is a pre-14 ground name — rename it to \"{{ground}}/\" (`git mv {{legacy}} {{ground}}`). What a spec stands on carries the underscore; a spec's own folder never does, and no resolver reads the un-underscored name (C13 — see docs/13-linting.md)." },
1907
1888
  type: "problem"
1908
1889
  }
1909
1890
  };
@@ -1932,7 +1913,7 @@ const c2HttpOnlyRequests = {
1932
1913
  },
1933
1914
  meta: {
1934
1915
  docs: RULE_DOCS["c2-http-only-requests"],
1935
- messages: { notHttp: "_requests/{{entry}} is not a .http file — _requests/ contains only complete .http request files (C2 — see docs/10-linting.md)." },
1916
+ messages: { notHttp: "_requests/{{entry}} is not a .http file — _requests/ contains only complete .http request files (C2 — see docs/13-linting.md)." },
1936
1917
  type: "problem"
1937
1918
  }
1938
1919
  };
@@ -2156,18 +2137,18 @@ const c4ContractShape = {
2156
2137
  meta: {
2157
2138
  docs: RULE_DOCS["c4-contract-shape"],
2158
2139
  messages: {
2159
- badName: "Contract unit \"{{base}}\" must be named <kebab-name>.ts — the folder already carries the provider (C4 — see docs/10-linting.md).",
2160
- badProviderDir: "contracts/{{entry}}/ is not a provider directory — a contracts/ root holds only http/, openai/, anthropic/ and *.contracts.ts files (C4 — see docs/10-linting.md).",
2161
- missingComposite: "Contract facade \"{{base}}\" has no default export — it must default-export the composed world (C4 — see docs/10-linting.md).",
2162
- missingDefault: "Contract unit \"{{base}}\" has no default export — a unit default-exports `defineContract(...)` or a factory returning one (C4 — see docs/10-linting.md).",
2163
- notDefineContract: "The default export of a contract unit must be `defineContract(...)` or a factory returning a contract (C4 — see docs/10-linting.md).",
2164
- notDefineContracts: "The default export of \"{{base}}\" must be built from `defineContracts(...)` (or a composition re-export) (C4 — see docs/10-linting.md).",
2165
- providerEntry: "contracts/{{provider}}/{{child}} is neither a *.ts contract nor a *.response.json payload (C4 — see docs/10-linting.md).",
2166
- providerMismatch: "This contract lives in contracts/{{provider}}/ but its request is a `{{declared}}.*` builder — the folder carries the provider (C4 — see docs/10-linting.md).",
2167
- providerSubfolder: "contracts/{{provider}}/{{child}}/ is nested — a provider folder is flat (C4 — see docs/10-linting.md).",
2168
- rootEntry: "contracts/{{entry}} is not a *.contracts.ts facade — data files live in a provider folder next to their contract (C4 — see docs/10-linting.md).",
2169
- rootFile: "Contract file \"{{base}}\" sits at the contracts/ root, which holds only *.contracts.ts facades — a unit contract belongs in http/, openai/ or anthropic/ (C4 — see docs/10-linting.md).",
2170
- tooDeep: "Contract file \"{{base}}\" is nested deeper than contracts/<provider>/ (C4 — see docs/10-linting.md)."
2140
+ badName: "Contract unit \"{{base}}\" must be named <kebab-name>.ts — the folder already carries the provider (C4 — see docs/13-linting.md).",
2141
+ badProviderDir: "contracts/{{entry}}/ is not a provider directory — a contracts/ root holds only http/, openai/, anthropic/ and *.contracts.ts files (C4 — see docs/13-linting.md).",
2142
+ missingComposite: "Contract facade \"{{base}}\" has no default export — it must default-export the composed world (C4 — see docs/13-linting.md).",
2143
+ missingDefault: "Contract unit \"{{base}}\" has no default export — a unit default-exports `defineContract(...)` or a factory returning one (C4 — see docs/13-linting.md).",
2144
+ notDefineContract: "The default export of a contract unit must be `defineContract(...)` or a factory returning a contract (C4 — see docs/13-linting.md).",
2145
+ notDefineContracts: "The default export of \"{{base}}\" must be built from `defineContracts(...)` (or a composition re-export) (C4 — see docs/13-linting.md).",
2146
+ providerEntry: "contracts/{{provider}}/{{child}} is neither a *.ts contract nor a *.response.json payload (C4 — see docs/13-linting.md).",
2147
+ providerMismatch: "This contract lives in contracts/{{provider}}/ but its request is a `{{declared}}.*` builder — the folder carries the provider (C4 — see docs/13-linting.md).",
2148
+ providerSubfolder: "contracts/{{provider}}/{{child}}/ is nested — a provider folder is flat (C4 — see docs/13-linting.md).",
2149
+ rootEntry: "contracts/{{entry}} is not a *.contracts.ts facade — data files live in a provider folder next to their contract (C4 — see docs/13-linting.md).",
2150
+ rootFile: "Contract file \"{{base}}\" sits at the contracts/ root, which holds only *.contracts.ts facades — a unit contract belongs in http/, openai/ or anthropic/ (C4 — see docs/13-linting.md).",
2151
+ tooDeep: "Contract file \"{{base}}\" is nested deeper than contracts/<provider>/ (C4 — see docs/13-linting.md)."
2171
2152
  },
2172
2153
  type: "problem"
2173
2154
  }
@@ -2218,7 +2199,7 @@ const c6ToMatchExtension = {
2218
2199
  },
2219
2200
  meta: {
2220
2201
  docs: RULE_DOCS["c6-tomatch-extension"],
2221
- 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)." },
2202
+ 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/13-linting.md)." },
2222
2203
  type: "problem"
2223
2204
  }
2224
2205
  };
@@ -2246,7 +2227,7 @@ const c7SeedsSqlOnly = {
2246
2227
  },
2247
2228
  meta: {
2248
2229
  docs: RULE_DOCS["c7-seeds-sql-only"],
2249
- 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)." },
2230
+ messages: { notSql: "_seeds/{{entry}} is not a .sql file — _seeds/ carries database state only; file state goes through .fixture() (C7 — see docs/13-linting.md)." },
2250
2231
  type: "problem"
2251
2232
  }
2252
2233
  };
@@ -2323,7 +2304,7 @@ const c8ReferencedFixtureExists = {
2323
2304
  },
2324
2305
  meta: {
2325
2306
  docs: RULE_DOCS["c8-referenced-fixture-exists"],
2326
- messages: { missing: "Referenced fixture \"{{path}}\" does not exist on disk under its ground {{root}}/ root (C8 — see docs/10-linting.md). Create it or fix the reference." },
2307
+ messages: { missing: "Referenced fixture \"{{path}}\" does not exist on disk under its ground {{root}}/ root (C8 — see docs/13-linting.md). Create it or fix the reference." },
2327
2308
  type: "problem"
2328
2309
  }
2329
2310
  };
@@ -2392,7 +2373,7 @@ const d12wResponseBodyProbe = {
2392
2373
  meta: {
2393
2374
  docs: RULE_DOCS["d12w-response-body-probe"],
2394
2375
  defaultOptions: [{ threshold: DEFAULT_THRESHOLD }],
2395
- 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)." },
2376
+ messages: { bodyProbeCluster: "{{count}} body probes in one test — this wants a full golden: expect(result.response).toMatch('x.http') (D12 — see docs/13-linting.md)." },
2396
2377
  schema: [{
2397
2378
  additionalProperties: false,
2398
2379
  properties: { threshold: {
@@ -2475,7 +2456,7 @@ const d13wUnfrozenNegativeFixture = {
2475
2456
  },
2476
2457
  meta: {
2477
2458
  docs: RULE_DOCS["d13w-unfrozen-negative-fixture"],
2478
- 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)." },
2459
+ 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/13-linting.md)." },
2479
2460
  type: "suggestion"
2480
2461
  }
2481
2462
  };
@@ -2551,7 +2532,7 @@ const d15wStatusOnlyProbe = {
2551
2532
  },
2552
2533
  meta: {
2553
2534
  docs: RULE_DOCS["d15w-status-only-probe"],
2554
- 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)." },
2535
+ messages: { statusOnlyProbe: "The test's only assertion(s) are status probes — pin the full response: expect(result.response).toMatch('case.http') (D11 — see docs/13-linting.md)." },
2555
2536
  type: "suggestion"
2556
2537
  }
2557
2538
  };
@@ -2606,7 +2587,7 @@ const d2AwaitIoMatcher = {
2606
2587
  },
2607
2588
  meta: {
2608
2589
  docs: RULE_DOCS["d2-await-io-matcher"],
2609
- 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)." },
2590
+ 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/13-linting.md)." },
2610
2591
  type: "problem"
2611
2592
  }
2612
2593
  };
@@ -2660,7 +2641,7 @@ const d2wAwaitSyncMatcher = {
2660
2641
  },
2661
2642
  meta: {
2662
2643
  docs: RULE_DOCS["d2w-await-sync-matcher"],
2663
- 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)." },
2644
+ messages: { redundantAwait: "await on expect(…).{{matcher}}(…) is redundant — that matcher is synchronous; only IO-backed matchers (toMatchRows/toBeEmpty/toBeRunning) are awaited (D2 — see docs/13-linting.md)." },
2664
2645
  type: "suggestion"
2665
2646
  }
2666
2647
  };
@@ -2737,7 +2718,7 @@ const d6wTransformTokenEquivalent = {
2737
2718
  },
2738
2719
  meta: {
2739
2720
  docs: RULE_DOCS["d6w-transform-token-equivalent"],
2740
- 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)." },
2721
+ 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/13-linting.md)." },
2741
2722
  type: "suggestion"
2742
2723
  }
2743
2724
  };
@@ -2771,7 +2752,7 @@ const d8wTextBypass = {
2771
2752
  },
2772
2753
  meta: {
2773
2754
  docs: RULE_DOCS["d8w-text-bypass"],
2774
- 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)." },
2755
+ 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/13-linting.md)." },
2775
2756
  type: "suggestion"
2776
2757
  }
2777
2758
  };
@@ -2854,7 +2835,7 @@ const d9wSingleUseRef = {
2854
2835
  },
2855
2836
  meta: {
2856
2837
  docs: RULE_DOCS["d9w-single-use-ref"],
2857
- 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)." },
2838
+ 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/13-linting.md)." },
2858
2839
  type: "suggestion"
2859
2840
  }
2860
2841
  };
@@ -2948,7 +2929,7 @@ const f1NoSubpathImport = {
2948
2929
  },
2949
2930
  meta: {
2950
2931
  docs: RULE_DOCS["f1-no-subpath-import"],
2951
- messages: { subpath: "Import from \"@jterrazz/test\", not \"{{source}}\" — internal subpaths do not exist (F1 — see docs/10-linting.md). Published subpaths, exempt everywhere: {{published}}." },
2932
+ messages: { subpath: "Import from \"@jterrazz/test\", not \"{{source}}\" — internal subpaths do not exist (F1 — see docs/13-linting.md). Published subpaths, exempt everywhere: {{published}}." },
2952
2933
  type: "problem"
2953
2934
  }
2954
2935
  };
@@ -2996,7 +2977,7 @@ const f2NoTestImportsInProd = {
2996
2977
  },
2997
2978
  meta: {
2998
2979
  docs: RULE_DOCS["f2-no-test-imports-in-prod"],
2999
- 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)." },
2980
+ 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/13-linting.md)." },
3000
2981
  type: "problem"
3001
2982
  }
3002
2983
  };
@@ -3059,7 +3040,7 @@ const f3SpecsPublicEntry = {
3059
3040
  },
3060
3041
  meta: {
3061
3042
  docs: RULE_DOCS["f3-specs-public-entry"],
3062
- 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)." },
3043
+ 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/13-linting.md; only @jterrazz/test/oxlint, and src/integrations/** from specs/integrations/, are exempt)." },
3063
3044
  type: "problem"
3064
3045
  }
3065
3046
  };
@@ -3085,7 +3066,7 @@ const f4NoTestToTestImport = {
3085
3066
  },
3086
3067
  meta: {
3087
3068
  docs: RULE_DOCS["f4-no-test-to-test-import"],
3088
- 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)." },
3069
+ 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/13-linting.md)." },
3089
3070
  type: "problem"
3090
3071
  }
3091
3072
  };
@@ -3111,7 +3092,7 @@ const f5FixturesOnlyFromTests = {
3111
3092
  },
3112
3093
  meta: {
3113
3094
  docs: RULE_DOCS["f5-fixtures-only-from-tests"],
3114
- messages: { fixturesImport: "A *.fixtures.ts module (\"{{source}}\") is only importable from *.test.ts files (F5 — see docs/10-linting.md)." },
3095
+ messages: { fixturesImport: "A *.fixtures.ts module (\"{{source}}\") is only importable from *.test.ts files (F5 — see docs/13-linting.md)." },
3115
3096
  type: "problem"
3116
3097
  }
3117
3098
  };
@@ -3203,8 +3184,8 @@ const i1LayerBoundaries = {
3203
3184
  defaultOptions: [{ layers: {} }],
3204
3185
  docs: RULE_DOCS["i1-layer-boundaries"],
3205
3186
  messages: {
3206
- crossLayer: "Layer \"{{layer}}\" must not import \"{{source}}\" — outside the edges its layer map declares (I1 — see docs/10-linting.md).",
3207
- foreignDependency: "\"{{source}}\" is not a dependency layer \"{{layer}}\" declares — one layer states the packages it may import (I1 — see docs/10-linting.md)."
3187
+ crossLayer: "Layer \"{{layer}}\" must not import \"{{source}}\" — outside the edges its layer map declares (I1 — see docs/13-linting.md).",
3188
+ foreignDependency: "\"{{source}}\" is not a dependency layer \"{{layer}}\" declares — one layer states the packages it may import (I1 — see docs/13-linting.md)."
3208
3189
  },
3209
3190
  schema: [{
3210
3191
  additionalProperties: false,
@@ -3292,9 +3273,9 @@ const i2SiblingTestNaming = {
3292
3273
  meta: {
3293
3274
  docs: RULE_DOCS["i2-sibling-test-naming"],
3294
3275
  messages: {
3295
- 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/.",
3296
- 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).",
3297
- testsDir: "__tests__/ directories are banned under src/ — the test of <file>.ts is its neighbour <file>.test.ts (I2 — see docs/10-linting.md)."
3276
+ orphanTest: "A src/ module test must sit NEXT to the module it tests — no neighbour \"{{expected}}\" found (I2 — see docs/13-linting.md). A test needing more than its module is a specification: move it to specs/.",
3277
+ rootTests: "A root-level tests/ directory is banned — module tests are siblings under src/, product specifications live in specs/ (I2 — see docs/13-linting.md).",
3278
+ testsDir: "__tests__/ directories are banned under src/ — the test of <file>.ts is its neighbour <file>.test.ts (I2 — see docs/13-linting.md)."
3298
3279
  },
3299
3280
  type: "problem"
3300
3281
  }
@@ -3394,57 +3375,14 @@ const i4NoViMockInSrc = {
3394
3375
  meta: {
3395
3376
  docs: RULE_DOCS["i4-no-vi-mock-in-src"],
3396
3377
  messages: {
3397
- 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).",
3398
- 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).",
3399
- viMock: "`vi.mock` is banned under src/ — use mockOf/mockOfDate (I4 — see docs/10-linting.md)."
3378
+ 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/13-linting.md).",
3379
+ bannedDir: "`{{dir}}/` directories are banned under src/ — mocks and data are code: mockOf/mockOfDate inline, payloads in a *.fixtures.ts neighbour (I4 — see docs/13-linting.md).",
3380
+ viMock: "`vi.mock` is banned under src/ — use mockOf/mockOfDate (I4 — see docs/13-linting.md)."
3400
3381
  },
3401
3382
  type: "problem"
3402
3383
  }
3403
3384
  };
3404
3385
  //#endregion
3405
- //#region src/lint/rules/j1-no-only-skip.ts
3406
- /** Test runners whose `.only` / `.skip` modifiers must never be committed. */
3407
- const RUNNERS = /* @__PURE__ */ new Set([
3408
- "describe",
3409
- "it",
3410
- "test"
3411
- ]);
3412
- /** The focus/skip modifiers CONVENTIONS J1 forbids. */
3413
- const FORBIDDEN = /* @__PURE__ */ new Set(["only", "skip"]);
3414
- /**
3415
- * CONVENTIONS J1 — no committed `.only` / `.skip`.
3416
- *
3417
- * Flags the member form used across the specs: `describe.only`, `test.only`,
3418
- * `it.only` and their `.skip` counterparts. Modifiers that legitimately gate a
3419
- * test (`test.skipIf`, `test.runIf`) are untouched — only the bare `only`/`skip`
3420
- * that pin or disable a test in a commit are reported.
3421
- */
3422
- const j1NoOnlySkip = {
3423
- create(context) {
3424
- return { MemberExpression(node) {
3425
- if (node.computed === true) return;
3426
- const object = node.object;
3427
- const property = node.property;
3428
- if (object?.type !== "Identifier" || property?.type !== "Identifier") return;
3429
- const runner = object.name;
3430
- const modifier = property.name;
3431
- if (RUNNERS.has(runner) && FORBIDDEN.has(modifier)) context.report({
3432
- data: {
3433
- modifier,
3434
- runner
3435
- },
3436
- messageId: "forbidden",
3437
- node
3438
- });
3439
- } };
3440
- },
3441
- meta: {
3442
- docs: RULE_DOCS["j1-no-only-skip"],
3443
- messages: { forbidden: "Committed `{{runner}}.{{modifier}}` is not allowed (J1 — see docs/10-linting.md). Remove it before committing." },
3444
- type: "problem"
3445
- }
3446
- };
3447
- //#endregion
3448
3386
  //#region src/lint/rules/j2-no-sleep-in-specs.ts
3449
3387
  /**
3450
3388
  * CONVENTIONS J2 — spec files (`specs/**`) contain no arbitrary sleeps:
@@ -3483,138 +3421,13 @@ const j2NoSleepInSpecs = {
3483
3421
  meta: {
3484
3422
  docs: RULE_DOCS["j2-no-sleep-in-specs"],
3485
3423
  messages: {
3486
- sleep: "No arbitrary sleeps in specs — synchronise via `waitFor` or the framework (J2 — see docs/10-linting.md).",
3487
- timersImport: "No timer-based sleeps in specs — synchronise via `waitFor` or the framework (J2 — see docs/10-linting.md)."
3424
+ sleep: "No arbitrary sleeps in specs — synchronise via `waitFor` or the framework (J2 — see docs/13-linting.md).",
3425
+ timersImport: "No timer-based sleeps in specs — synchronise via `waitFor` or the framework (J2 — see docs/13-linting.md)."
3488
3426
  },
3489
3427
  type: "problem"
3490
3428
  }
3491
3429
  };
3492
3430
  //#endregion
3493
- //#region src/lint/rules/j3-no-expectless-test.ts
3494
- /** Does `callback` contain at least one `expect(…)` call? */
3495
- function hasExpect(callback) {
3496
- let found = false;
3497
- walk(callback, (node) => {
3498
- if (found || node.type !== "CallExpression") return;
3499
- const callee = node.callee;
3500
- if (callee?.type === "Identifier" && callee.name === "expect") found = true;
3501
- });
3502
- return found;
3503
- }
3504
- /**
3505
- * CONVENTIONS J3 — a test asserts something. A `test('…', () => { … })` whose
3506
- * body contains no `expect(…)` is either dead or a silent no-op.
3507
- *
3508
- * Reuses the shared B4 test-shape helpers. `test.todo(...)` (and any other
3509
- * callback-less form) is skipped — there is nothing to assert yet.
3510
- */
3511
- const j3NoExpectlessTest = {
3512
- create(context) {
3513
- return { CallExpression(node) {
3514
- const callee = node.callee;
3515
- if (!isTestCallee(callee)) return;
3516
- if (testCalleeHasModifier(callee, "todo")) return;
3517
- const callback = findTestCallback(node.arguments ?? []);
3518
- if (callback === void 0) return;
3519
- if (!hasExpect(callback)) context.report({
3520
- messageId: "noExpect",
3521
- node
3522
- });
3523
- } };
3524
- },
3525
- meta: {
3526
- docs: RULE_DOCS["j3-no-expectless-test"],
3527
- messages: { noExpect: "Test has no `expect(…)` — a test must assert something (J3 — see docs/10-linting.md). Use `test.todo` for a pending test." },
3528
- type: "problem"
3529
- }
3530
- };
3531
- //#endregion
3532
- //#region src/lint/rules/j4-unique-test-names.ts
3533
- /**
3534
- * CONVENTIONS J4 — test names are the sole description of behaviour (B3), so two
3535
- * tests sharing a literal name in one file are a copy-paste smell: one shadows
3536
- * the other in the reporter. Flags the second (and later) occurrence.
3537
- *
3538
- * `.each` / parametrized tests are skipped — their name is a template expanded
3539
- * per row, collisions there are by construction.
3540
- */
3541
- const j4UniqueTestNames = {
3542
- create(context) {
3543
- const seen = /* @__PURE__ */ new Set();
3544
- return { CallExpression(node) {
3545
- const callee = node.callee;
3546
- if (!isTestCallee(callee)) return;
3547
- if (testCalleeHasModifier(callee, "each")) return;
3548
- const name = testName(node);
3549
- if (name === void 0) return;
3550
- if (seen.has(name)) {
3551
- context.report({
3552
- data: { name },
3553
- messageId: "duplicate",
3554
- node
3555
- });
3556
- return;
3557
- }
3558
- seen.add(name);
3559
- } };
3560
- },
3561
- meta: {
3562
- docs: RULE_DOCS["j4-unique-test-names"],
3563
- 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)." },
3564
- type: "problem"
3565
- }
3566
- };
3567
- //#endregion
3568
- //#region src/lint/rules/j5-lowercase-title.ts
3569
- /** The block/test introducers whose title literal C1' constrains. */
3570
- const TITLED = /* @__PURE__ */ new Set([
3571
- "describe",
3572
- "it",
3573
- "test"
3574
- ]);
3575
- /** An identifier-shaped, all-caps/underscored first word: `VALID_CATEGORIES`,
3576
- * `HTTP`, `DI`. Such a word names a symbol verbatim — lowercasing it would
3577
- * misspell it — so it is exempt. A prose word (`Rejects`, `Builds`) is not. */
3578
- const ALL_CAPS_IDENTIFIER = /^[A-Z][A-Z0-9_]*$/u;
3579
- /**
3580
- * CONVENTIONS J5 — the first character of a `test('…')` / `describe('…')` title
3581
- * literal must be lowercase.
3582
- *
3583
- * The test name is the sole description of behaviour (B3): a sentence fragment,
3584
- * lowercase-led like prose (`'rejects an unknown job'`). Exempt are titles
3585
- * whose first WORD is an identifier-shaped all-caps/underscored token
3586
- * (`'VALID_CATEGORIES is rejected'`, `'HTTP is upgraded'`, `'DI wires …'`) —
3587
- * lowercasing a named symbol would misspell it — and titles opening on a
3588
- * non-letter (a `$`, a `{`, a `%s` placeholder). Only lowercase-able prose
3589
- * first words are enforced. Covers the modifier and parametrized forms
3590
- * (`test.only`, `describe.each(...)(...)`) via the chain root.
3591
- */
3592
- const j5LowercaseTitle = {
3593
- create(context) {
3594
- return { CallExpression(node) {
3595
- const callee = node.callee;
3596
- const root = chainRootName(callee);
3597
- if (root === void 0 || !TITLED.has(root)) return;
3598
- const title = stringValue(node.arguments?.[0]);
3599
- if (title === void 0) return;
3600
- const first = [...title][0];
3601
- if (first === void 0 || !/\p{L}/u.test(first)) return;
3602
- const firstWord = title.split(/\s+/u)[0];
3603
- if (ALL_CAPS_IDENTIFIER.test(firstWord)) return;
3604
- if (first !== first.toLocaleLowerCase()) context.report({
3605
- data: { runner: root },
3606
- messageId: "uppercase",
3607
- node
3608
- });
3609
- } };
3610
- },
3611
- meta: {
3612
- docs: RULE_DOCS["j5-lowercase-title"],
3613
- messages: { uppercase: "A {{runner}}() title must start lowercase — the test name is a prose fragment, not a sentence (J5 — see docs/10-linting.md)." },
3614
- type: "problem"
3615
- }
3616
- };
3617
- //#endregion
3618
3431
  //#region src/lint/rules/w1-scenario-pure.ts
3619
3432
  /** The scenario-carrying terminal actions: `.visit()` (website) and `.open()` (mobile). */
3620
3433
  const SCENARIO_ACTIONS$1 = /* @__PURE__ */ new Set(["open", "visit"]);
@@ -3644,7 +3457,7 @@ const w1ScenarioPure = {
3644
3457
  },
3645
3458
  meta: {
3646
3459
  docs: RULE_DOCS["w1-scenario-pure"],
3647
- messages: { expectInScenario: "No expect() inside a scenario — the scenario is the When; assert the final state on the returned result in the Then (W1 — see docs/10-linting.md)." },
3460
+ messages: { expectInScenario: "No expect() inside a scenario — the scenario is the When; assert the final state on the returned result in the Then (W1 — see docs/13-linting.md)." },
3648
3461
  type: "problem"
3649
3462
  }
3650
3463
  };
@@ -3660,7 +3473,7 @@ const SCENARIO_ACTIONS = /* @__PURE__ */ new Set(["open", "visit"]);
3660
3473
  *
3661
3474
  * Registered in a consumer's (or this repo's own) `oxlint.config.ts` via
3662
3475
  * `jsPlugins: ['@jterrazz/test/oxlint']` and referenced as `jterrazz/<rule>` in
3663
- * the `rules` map, e.g. `'jterrazz/j1-no-only-skip': 'error'` — or enabled
3476
+ * the `rules` map, e.g. `'jterrazz/j2-no-sleep-in-specs': 'error'` — or enabled
3664
3477
  * wholesale by spreading {@link recommendedRules}.
3665
3478
  *
3666
3479
  * This entry is bundled by tsdown (`dist/oxlint.js`); rules import nothing from
@@ -3710,11 +3523,7 @@ const plugin = {
3710
3523
  "i1-layer-boundaries": i1LayerBoundaries,
3711
3524
  "i2-sibling-test-naming": i2SiblingTestNaming,
3712
3525
  "i4-no-vi-mock-in-src": i4NoViMockInSrc,
3713
- "j1-no-only-skip": j1NoOnlySkip,
3714
3526
  "j2-no-sleep-in-specs": j2NoSleepInSpecs,
3715
- "j3-no-expectless-test": j3NoExpectlessTest,
3716
- "j4-unique-test-names": j4UniqueTestNames,
3717
- "j5-lowercase-title": j5LowercaseTitle,
3718
3527
  "w1-scenario-pure": w1ScenarioPure,
3719
3528
  "w2w-user-facing-elements": {
3720
3529
  create(context) {
@@ -3736,7 +3545,7 @@ const plugin = {
3736
3545
  },
3737
3546
  meta: {
3738
3547
  docs: RULE_DOCS["w2w-user-facing-elements"],
3739
- messages: { testIdElement: "Prefer a user-facing element (button, link, field, heading, content) over testId() — the escape hatch hides what the user actually sees (W2 — see docs/10-linting.md)." },
3548
+ messages: { testIdElement: "Prefer a user-facing element (button, link, field, heading, content) over testId() — the escape hatch hides what the user actually sees (W2 — see docs/13-linting.md)." },
3740
3549
  type: "suggestion"
3741
3550
  }
3742
3551
  }