@jterrazz/test 14.3.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.cjs CHANGED
@@ -198,21 +198,10 @@ function isTestCallee(callee) {
198
198
  if (callee.type === "CallExpression") return isTestCallee(callee.callee);
199
199
  return false;
200
200
  }
201
- /** Does the test callee chain carry a modifier member with the given name? */
202
- function testCalleeHasModifier(callee, modifier) {
203
- if (callee === void 0) return false;
204
- if (callee.type === "MemberExpression") return memberPropertyName(callee) === modifier || testCalleeHasModifier(callee.object, modifier);
205
- if (callee.type === "CallExpression") return testCalleeHasModifier(callee.callee, modifier);
206
- return false;
207
- }
208
201
  /** The callback argument of a test call (arrow or function expression), if any. */
209
202
  function findTestCallback(args) {
210
203
  return args.find((arg) => arg.type === "ArrowFunctionExpression" || arg.type === "FunctionExpression");
211
204
  }
212
- /** The static string name of a test call (`test('name', …)`), if a literal. */
213
- function testName(callNode) {
214
- return stringValue((callNode.arguments ?? [])[0]);
215
- }
216
205
  /**
217
206
  * Visitor fragment collecting every import-like source: static imports,
218
207
  * dynamic `import()`, and re-exports. Rules spread this into their visitor.
@@ -239,6 +228,26 @@ function importSourceVisitor(onSource) {
239
228
  * The **statique** channel — one entry per shipped `jterrazz/*` rule. Each rule
240
229
  * file attaches its entry as `meta.docs`, so a rule and its normative text can
241
230
  * never drift (the completeness meta-test asserts every rule carries one).
231
+ *
232
+ * **What this channel does NOT own.** A convention oxlint's own `vitest` plugin
233
+ * already enforces belongs to that plugin, not here — `@jterrazz/typescript` v10
234
+ * turns the plugin on over the test globs in every profile, so a consumer of the
235
+ * preset gets it without wiring. Four hygiene rules left the channel on that
236
+ * ground (J1, J3, J4, J5 → `vitest/no-focused-tests` + `vitest/no-disabled-tests`,
237
+ * `vitest/expect-expect`, `vitest/no-identical-title`, `vitest/prefer-lowercase-title`).
238
+ * `prefer-lowercase-title` is stricter than J5 was: a title opening on an all-caps
239
+ * identifier (`HTTP 404 …`, `DI …`) now fails, and the rulebook carries no
240
+ * `allowedPrefixes` — a title starts lowercase, whatever its first word. The
241
+ * rest are behaviour-identical on this package's own fixtures. Only J2 (the
242
+ * arbitrary-sleep ban, which has no upstream counterpart) stays statique.
243
+ *
244
+ * **Severity.** The estate's rulebook has no warn tier: a rule is on at `error`
245
+ * or off with a recorded reason, and that law binds `@jterrazz/typescript`'s
246
+ * rulebook. This channel is its one documented exception, and it is narrow — an
247
+ * id shaped `<family><n>w-…` is a REDUNDANCY heuristic (a `root` the walk would
248
+ * have found, a probe cluster that wants a golden), a reading of taste that
249
+ * would block a legitimate shape if it failed the build. Every hard convention
250
+ * here is an `error`; `plugin.test.ts` holds the split.
242
251
  */
243
252
  const RULE_DOCS = {
244
253
  "a1-specification-file": {
@@ -514,13 +523,6 @@ const RULE_DOCS = {
514
523
  id: "I4",
515
524
  rationale: "Dans les tests de module, mocks et données sont du CODE (`mockOf`, `*.fixtures.ts`) ; un vrai fichier appelle une spec."
516
525
  },
517
- "j1-no-only-skip": {
518
- channel: "statique",
519
- convention: "Aucun `.only` / `.skip` committé (`describe.only`, `test.only`, `test.skip`).",
520
- family: "J",
521
- id: "J1",
522
- rationale: "Un `.only`/`.skip` oublié désactive silencieusement une partie de la suite."
523
- },
524
526
  "j2-no-sleep-in-specs": {
525
527
  channel: "statique",
526
528
  convention: "Aucun sleep arbitraire (`setTimeout`/`setInterval`/`Atomics.wait`) sous `specs/**` — la synchronisation passe par `waitFor`.",
@@ -528,27 +530,6 @@ const RULE_DOCS = {
528
530
  id: "J2",
529
531
  rationale: "Un sleep fixe rend les tests lents et instables ; attendre une condition est déterministe."
530
532
  },
531
- "j3-no-expectless-test": {
532
- channel: "statique",
533
- convention: "Un `test(...)` avec callback contient au moins un `expect(…)` ; `test.todo` (sans callback) est ignoré.",
534
- family: "J",
535
- id: "J3",
536
- rationale: "Un test sans assertion est mort ou muet — il passe toujours sans rien vérifier."
537
- },
538
- "j4-unique-test-names": {
539
- channel: "statique",
540
- convention: "Deux tests d’un même fichier ne partagent pas un nom littéral (`.each` ignoré).",
541
- family: "J",
542
- id: "J4",
543
- rationale: "Le nom du test est son unique description — deux noms identiques rendent un échec ambigu."
544
- },
545
- "j5-lowercase-title": {
546
- channel: "statique",
547
- 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.",
548
- family: "J",
549
- id: "J5",
550
- rationale: "Un titre est un fragment de prose, pas une phrase — la casse minuscule le garde fragmentaire ; minusculiser un symbole nommé le mal-orthographierait."
551
- },
552
533
  "w1-scenario-pure": {
553
534
  channel: "statique",
554
535
  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é.",
@@ -907,7 +888,7 @@ const a1SpecificationFile = {
907
888
  },
908
889
  meta: {
909
890
  docs: RULE_DOCS["a1-specification-file"],
910
- messages: { outsideSpecification: "specification.{{member}}() must be called from a `*.specification.ts` file under specs/ (A1 — see docs/10-linting.md)." },
891
+ messages: { outsideSpecification: "specification.{{member}}() must be called from a `*.specification.ts` file under specs/ (A1 — see docs/13-linting.md)." },
911
892
  type: "problem"
912
893
  }
913
894
  };
@@ -984,13 +965,13 @@ const a10DuplicateBinding = {
984
965
  },
985
966
  meta: {
986
967
  docs: RULE_DOCS["a10-duplicate-binding"],
987
- 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)." },
968
+ 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)." },
988
969
  type: "problem"
989
970
  }
990
971
  };
991
972
  //#endregion
992
973
  //#region src/lint/rules/a2-known-constructors.ts
993
- /** The five constructors, and only five (A2 — see docs/10-linting.md). */
974
+ /** The five constructors, and only five (A2 — see docs/13-linting.md). */
994
975
  const KNOWN_CONSTRUCTORS = /* @__PURE__ */ new Set([
995
976
  "api",
996
977
  "cli",
@@ -1019,7 +1000,7 @@ const a2KnownConstructors = {
1019
1000
  },
1020
1001
  meta: {
1021
1002
  docs: RULE_DOCS["a2-known-constructors"],
1022
- 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)." },
1003
+ 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)." },
1023
1004
  type: "problem"
1024
1005
  }
1025
1006
  };
@@ -1064,7 +1045,7 @@ const a3NoDestructureAlias = {
1064
1045
  },
1065
1046
  meta: {
1066
1047
  docs: RULE_DOCS["a3-no-destructure-alias"],
1067
- 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." },
1048
+ 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." },
1068
1049
  type: "problem"
1069
1050
  }
1070
1051
  };
@@ -1116,7 +1097,7 @@ const a4CleanupAfterall = {
1116
1097
  },
1117
1098
  meta: {
1118
1099
  docs: RULE_DOCS["a4-cleanup-afterall"],
1119
- messages: { unregistered: "`cleanup` is destructured but never passed to afterAll — add `afterAll(cleanup)` to the specification file (A4 — see docs/10-linting.md)." },
1100
+ messages: { unregistered: "`cleanup` is destructured but never passed to afterAll — add `afterAll(cleanup)` to the specification file (A4 — see docs/13-linting.md)." },
1120
1101
  type: "problem"
1121
1102
  }
1122
1103
  };
@@ -1146,7 +1127,7 @@ const a5ModeWithServer = {
1146
1127
  },
1147
1128
  meta: {
1148
1129
  docs: RULE_DOCS["a5-mode-with-server"],
1149
- 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)." },
1130
+ 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)." },
1150
1131
  type: "problem"
1151
1132
  }
1152
1133
  };
@@ -1187,7 +1168,7 @@ const a6wRedundantComposeService = {
1187
1168
  },
1188
1169
  meta: {
1189
1170
  docs: RULE_DOCS["a6w-redundant-compose-service"],
1190
- 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)." },
1171
+ 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)." },
1191
1172
  type: "suggestion"
1192
1173
  }
1193
1174
  };
@@ -1222,7 +1203,7 @@ function findRoot(startDir, exists) {
1222
1203
  //#endregion
1223
1204
  //#region src/lint/rules/a9w-redundant-root.ts
1224
1205
  /**
1225
- * The root the convention would derive (A9 — see docs/10-linting.md): the
1206
+ * The root the convention would derive (A9 — see docs/13-linting.md): the
1226
1207
  * framework's own walk-up, run against this layer's cached fs probe. The rule
1227
1208
  * flags a `root` that the runner would have found by itself, so the two MUST
1228
1209
  * be the same function — a private copy of the walk drifted from it before.
@@ -1254,7 +1235,7 @@ const a9wRedundantRoot = {
1254
1235
  },
1255
1236
  meta: {
1256
1237
  docs: RULE_DOCS["a9w-redundant-root"],
1257
- messages: { redundant: "root: \"{{root}}\" is redundant — walking up from the specification file already resolves to that directory (A9 — see docs/10-linting.md)." },
1238
+ messages: { redundant: "root: \"{{root}}\" is redundant — walking up from the specification file already resolves to that directory (A9 — see docs/13-linting.md)." },
1258
1239
  type: "suggestion"
1259
1240
  }
1260
1241
  };
@@ -1360,7 +1341,7 @@ const b2KnownFixtureMarker = {
1360
1341
  },
1361
1342
  meta: {
1362
1343
  docs: RULE_DOCS["b2-known-fixture-marker"],
1363
- messages: { unknownMarker: "Unknown fixture marker \"{{marker}}\" — known markers: {{known}}. A path without a marker is feature-local (B2 — see docs/10-linting.md)." },
1344
+ messages: { unknownMarker: "Unknown fixture marker \"{{marker}}\" — known markers: {{known}}. A path without a marker is feature-local (B2 — see docs/13-linting.md)." },
1364
1345
  type: "problem"
1365
1346
  }
1366
1347
  };
@@ -1425,8 +1406,8 @@ const b4GivenThen = {
1425
1406
  meta: {
1426
1407
  docs: RULE_DOCS["b4-given-then"],
1427
1408
  messages: {
1428
- givenAfterThen: "The `// Given -` marker comes after `// Then -` — Given describes the setup and must precede Then (B4 — see docs/10-linting.md).",
1429
- missing: "Test is missing a `// {{marker}} -` comment (B4 — see docs/10-linting.md — every test needs both `// Given -` and `// Then -`)."
1409
+ givenAfterThen: "The `// Given -` marker comes after `// Then -` — Given describes the setup and must precede Then (B4 — see docs/13-linting.md).",
1410
+ missing: "Test is missing a `// {{marker}} -` comment (B4 — see docs/13-linting.md — every test needs both `// Given -` and `// Then -`)."
1430
1411
  },
1431
1412
  type: "problem"
1432
1413
  }
@@ -1467,7 +1448,7 @@ const b5AwaitUsing = {
1467
1448
  meta: {
1468
1449
  docs: RULE_DOCS["b5-await-using"],
1469
1450
  defaultOptions: [{ runners: [] }],
1470
- 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)." },
1451
+ 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)." },
1471
1452
  schema: [{
1472
1453
  additionalProperties: false,
1473
1454
  properties: { runners: {
@@ -1510,7 +1491,7 @@ const b6wRedundantEnvUrl = {
1510
1491
  },
1511
1492
  meta: {
1512
1493
  docs: RULE_DOCS["b6w-redundant-env-url"],
1513
- 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)." },
1494
+ 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)." },
1514
1495
  type: "suggestion"
1515
1496
  }
1516
1497
  };
@@ -1539,7 +1520,7 @@ const b8KebabTrigger = {
1539
1520
  },
1540
1521
  meta: {
1541
1522
  docs: RULE_DOCS["b8-kebab-trigger"],
1542
- 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)." },
1523
+ 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)." },
1543
1524
  type: "problem"
1544
1525
  }
1545
1526
  };
@@ -1594,7 +1575,7 @@ const b9wProductCommand = {
1594
1575
  },
1595
1576
  meta: {
1596
1577
  docs: RULE_DOCS["b9w-product-command"],
1597
- 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." },
1578
+ 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." },
1598
1579
  type: "suggestion"
1599
1580
  }
1600
1581
  };
@@ -1747,14 +1728,14 @@ const c1DomainStructure = {
1747
1728
  defaultOptions: [{ depth: DEFAULT_DEPTH }],
1748
1729
  docs: RULE_DOCS["c1-domain-structure"],
1749
1730
  messages: {
1750
- 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.",
1751
- specNotAtFacetRoot: "A `*.specification.ts` must sit at the facet root: `specs/<facet>/<name>.specification.ts` (C1 — see docs/10-linting.md).",
1752
- 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).",
1753
- 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).",
1754
- 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).",
1755
- 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).",
1756
- 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).",
1757
- 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)."
1731
+ 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.",
1732
+ specNotAtFacetRoot: "A `*.specification.ts` must sit at the facet root: `specs/<facet>/<name>.specification.ts` (C1 — see docs/13-linting.md).",
1733
+ 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).",
1734
+ 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).",
1735
+ 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).",
1736
+ 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).",
1737
+ 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).",
1738
+ 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)."
1758
1739
  },
1759
1740
  schema: [{
1760
1741
  additionalProperties: false,
@@ -1799,7 +1780,7 @@ const c10ContractsBoundary = {
1799
1780
  },
1800
1781
  meta: {
1801
1782
  docs: RULE_DOCS["c10-contracts-boundary"],
1802
- 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)." },
1783
+ 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)." },
1803
1784
  type: "problem"
1804
1785
  }
1805
1786
  };
@@ -1854,7 +1835,7 @@ const c11ContractDataPairing = {
1854
1835
  },
1855
1836
  meta: {
1856
1837
  docs: RULE_DOCS["c11-contract-data-pairing"],
1857
- 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)." },
1838
+ 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)." },
1858
1839
  type: "problem"
1859
1840
  }
1860
1841
  };
@@ -1907,7 +1888,7 @@ const c13UnderscoredGround = {
1907
1888
  },
1908
1889
  meta: {
1909
1890
  docs: RULE_DOCS["c13-underscored-ground"],
1910
- 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)." },
1891
+ 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)." },
1911
1892
  type: "problem"
1912
1893
  }
1913
1894
  };
@@ -1936,7 +1917,7 @@ const c2HttpOnlyRequests = {
1936
1917
  },
1937
1918
  meta: {
1938
1919
  docs: RULE_DOCS["c2-http-only-requests"],
1939
- messages: { notHttp: "_requests/{{entry}} is not a .http file — _requests/ contains only complete .http request files (C2 — see docs/10-linting.md)." },
1920
+ messages: { notHttp: "_requests/{{entry}} is not a .http file — _requests/ contains only complete .http request files (C2 — see docs/13-linting.md)." },
1940
1921
  type: "problem"
1941
1922
  }
1942
1923
  };
@@ -2160,18 +2141,18 @@ const c4ContractShape = {
2160
2141
  meta: {
2161
2142
  docs: RULE_DOCS["c4-contract-shape"],
2162
2143
  messages: {
2163
- badName: "Contract unit \"{{base}}\" must be named <kebab-name>.ts — the folder already carries the provider (C4 — see docs/10-linting.md).",
2164
- 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).",
2165
- missingComposite: "Contract facade \"{{base}}\" has no default export — it must default-export the composed world (C4 — see docs/10-linting.md).",
2166
- missingDefault: "Contract unit \"{{base}}\" has no default export — a unit default-exports `defineContract(...)` or a factory returning one (C4 — see docs/10-linting.md).",
2167
- notDefineContract: "The default export of a contract unit must be `defineContract(...)` or a factory returning a contract (C4 — see docs/10-linting.md).",
2168
- notDefineContracts: "The default export of \"{{base}}\" must be built from `defineContracts(...)` (or a composition re-export) (C4 — see docs/10-linting.md).",
2169
- providerEntry: "contracts/{{provider}}/{{child}} is neither a *.ts contract nor a *.response.json payload (C4 — see docs/10-linting.md).",
2170
- 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).",
2171
- providerSubfolder: "contracts/{{provider}}/{{child}}/ is nested — a provider folder is flat (C4 — see docs/10-linting.md).",
2172
- 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).",
2173
- 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).",
2174
- tooDeep: "Contract file \"{{base}}\" is nested deeper than contracts/<provider>/ (C4 — see docs/10-linting.md)."
2144
+ badName: "Contract unit \"{{base}}\" must be named <kebab-name>.ts — the folder already carries the provider (C4 — see docs/13-linting.md).",
2145
+ 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).",
2146
+ missingComposite: "Contract facade \"{{base}}\" has no default export — it must default-export the composed world (C4 — see docs/13-linting.md).",
2147
+ missingDefault: "Contract unit \"{{base}}\" has no default export — a unit default-exports `defineContract(...)` or a factory returning one (C4 — see docs/13-linting.md).",
2148
+ notDefineContract: "The default export of a contract unit must be `defineContract(...)` or a factory returning a contract (C4 — see docs/13-linting.md).",
2149
+ notDefineContracts: "The default export of \"{{base}}\" must be built from `defineContracts(...)` (or a composition re-export) (C4 — see docs/13-linting.md).",
2150
+ providerEntry: "contracts/{{provider}}/{{child}} is neither a *.ts contract nor a *.response.json payload (C4 — see docs/13-linting.md).",
2151
+ 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).",
2152
+ providerSubfolder: "contracts/{{provider}}/{{child}}/ is nested — a provider folder is flat (C4 — see docs/13-linting.md).",
2153
+ 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).",
2154
+ 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).",
2155
+ tooDeep: "Contract file \"{{base}}\" is nested deeper than contracts/<provider>/ (C4 — see docs/13-linting.md)."
2175
2156
  },
2176
2157
  type: "problem"
2177
2158
  }
@@ -2222,7 +2203,7 @@ const c6ToMatchExtension = {
2222
2203
  },
2223
2204
  meta: {
2224
2205
  docs: RULE_DOCS["c6-tomatch-extension"],
2225
- 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)." },
2206
+ 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)." },
2226
2207
  type: "problem"
2227
2208
  }
2228
2209
  };
@@ -2250,7 +2231,7 @@ const c7SeedsSqlOnly = {
2250
2231
  },
2251
2232
  meta: {
2252
2233
  docs: RULE_DOCS["c7-seeds-sql-only"],
2253
- 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)." },
2234
+ 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)." },
2254
2235
  type: "problem"
2255
2236
  }
2256
2237
  };
@@ -2327,7 +2308,7 @@ const c8ReferencedFixtureExists = {
2327
2308
  },
2328
2309
  meta: {
2329
2310
  docs: RULE_DOCS["c8-referenced-fixture-exists"],
2330
- 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." },
2311
+ 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." },
2331
2312
  type: "problem"
2332
2313
  }
2333
2314
  };
@@ -2396,7 +2377,7 @@ const d12wResponseBodyProbe = {
2396
2377
  meta: {
2397
2378
  docs: RULE_DOCS["d12w-response-body-probe"],
2398
2379
  defaultOptions: [{ threshold: DEFAULT_THRESHOLD }],
2399
- 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)." },
2380
+ 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)." },
2400
2381
  schema: [{
2401
2382
  additionalProperties: false,
2402
2383
  properties: { threshold: {
@@ -2479,7 +2460,7 @@ const d13wUnfrozenNegativeFixture = {
2479
2460
  },
2480
2461
  meta: {
2481
2462
  docs: RULE_DOCS["d13w-unfrozen-negative-fixture"],
2482
- 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)." },
2463
+ 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)." },
2483
2464
  type: "suggestion"
2484
2465
  }
2485
2466
  };
@@ -2555,7 +2536,7 @@ const d15wStatusOnlyProbe = {
2555
2536
  },
2556
2537
  meta: {
2557
2538
  docs: RULE_DOCS["d15w-status-only-probe"],
2558
- 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)." },
2539
+ 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)." },
2559
2540
  type: "suggestion"
2560
2541
  }
2561
2542
  };
@@ -2610,7 +2591,7 @@ const d2AwaitIoMatcher = {
2610
2591
  },
2611
2592
  meta: {
2612
2593
  docs: RULE_DOCS["d2-await-io-matcher"],
2613
- 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)." },
2594
+ 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)." },
2614
2595
  type: "problem"
2615
2596
  }
2616
2597
  };
@@ -2664,7 +2645,7 @@ const d2wAwaitSyncMatcher = {
2664
2645
  },
2665
2646
  meta: {
2666
2647
  docs: RULE_DOCS["d2w-await-sync-matcher"],
2667
- 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)." },
2648
+ 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)." },
2668
2649
  type: "suggestion"
2669
2650
  }
2670
2651
  };
@@ -2741,7 +2722,7 @@ const d6wTransformTokenEquivalent = {
2741
2722
  },
2742
2723
  meta: {
2743
2724
  docs: RULE_DOCS["d6w-transform-token-equivalent"],
2744
- 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)." },
2725
+ 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)." },
2745
2726
  type: "suggestion"
2746
2727
  }
2747
2728
  };
@@ -2775,7 +2756,7 @@ const d8wTextBypass = {
2775
2756
  },
2776
2757
  meta: {
2777
2758
  docs: RULE_DOCS["d8w-text-bypass"],
2778
- 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)." },
2759
+ 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)." },
2779
2760
  type: "suggestion"
2780
2761
  }
2781
2762
  };
@@ -2858,7 +2839,7 @@ const d9wSingleUseRef = {
2858
2839
  },
2859
2840
  meta: {
2860
2841
  docs: RULE_DOCS["d9w-single-use-ref"],
2861
- 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)." },
2842
+ 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)." },
2862
2843
  type: "suggestion"
2863
2844
  }
2864
2845
  };
@@ -2952,7 +2933,7 @@ const f1NoSubpathImport = {
2952
2933
  },
2953
2934
  meta: {
2954
2935
  docs: RULE_DOCS["f1-no-subpath-import"],
2955
- messages: { subpath: "Import from \"@jterrazz/test\", not \"{{source}}\" — internal subpaths do not exist (F1 — see docs/10-linting.md). Published subpaths, exempt everywhere: {{published}}." },
2936
+ messages: { subpath: "Import from \"@jterrazz/test\", not \"{{source}}\" — internal subpaths do not exist (F1 — see docs/13-linting.md). Published subpaths, exempt everywhere: {{published}}." },
2956
2937
  type: "problem"
2957
2938
  }
2958
2939
  };
@@ -3000,7 +2981,7 @@ const f2NoTestImportsInProd = {
3000
2981
  },
3001
2982
  meta: {
3002
2983
  docs: RULE_DOCS["f2-no-test-imports-in-prod"],
3003
- 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)." },
2984
+ 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)." },
3004
2985
  type: "problem"
3005
2986
  }
3006
2987
  };
@@ -3063,7 +3044,7 @@ const f3SpecsPublicEntry = {
3063
3044
  },
3064
3045
  meta: {
3065
3046
  docs: RULE_DOCS["f3-specs-public-entry"],
3066
- 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)." },
3047
+ 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)." },
3067
3048
  type: "problem"
3068
3049
  }
3069
3050
  };
@@ -3089,7 +3070,7 @@ const f4NoTestToTestImport = {
3089
3070
  },
3090
3071
  meta: {
3091
3072
  docs: RULE_DOCS["f4-no-test-to-test-import"],
3092
- 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)." },
3073
+ 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)." },
3093
3074
  type: "problem"
3094
3075
  }
3095
3076
  };
@@ -3115,7 +3096,7 @@ const f5FixturesOnlyFromTests = {
3115
3096
  },
3116
3097
  meta: {
3117
3098
  docs: RULE_DOCS["f5-fixtures-only-from-tests"],
3118
- messages: { fixturesImport: "A *.fixtures.ts module (\"{{source}}\") is only importable from *.test.ts files (F5 — see docs/10-linting.md)." },
3099
+ messages: { fixturesImport: "A *.fixtures.ts module (\"{{source}}\") is only importable from *.test.ts files (F5 — see docs/13-linting.md)." },
3119
3100
  type: "problem"
3120
3101
  }
3121
3102
  };
@@ -3207,8 +3188,8 @@ const i1LayerBoundaries = {
3207
3188
  defaultOptions: [{ layers: {} }],
3208
3189
  docs: RULE_DOCS["i1-layer-boundaries"],
3209
3190
  messages: {
3210
- crossLayer: "Layer \"{{layer}}\" must not import \"{{source}}\" — outside the edges its layer map declares (I1 — see docs/10-linting.md).",
3211
- foreignDependency: "\"{{source}}\" is not a dependency layer \"{{layer}}\" declares — one layer states the packages it may import (I1 — see docs/10-linting.md)."
3191
+ crossLayer: "Layer \"{{layer}}\" must not import \"{{source}}\" — outside the edges its layer map declares (I1 — see docs/13-linting.md).",
3192
+ foreignDependency: "\"{{source}}\" is not a dependency layer \"{{layer}}\" declares — one layer states the packages it may import (I1 — see docs/13-linting.md)."
3212
3193
  },
3213
3194
  schema: [{
3214
3195
  additionalProperties: false,
@@ -3296,9 +3277,9 @@ const i2SiblingTestNaming = {
3296
3277
  meta: {
3297
3278
  docs: RULE_DOCS["i2-sibling-test-naming"],
3298
3279
  messages: {
3299
- 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/.",
3300
- 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).",
3301
- testsDir: "__tests__/ directories are banned under src/ — the test of <file>.ts is its neighbour <file>.test.ts (I2 — see docs/10-linting.md)."
3280
+ 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/.",
3281
+ 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).",
3282
+ testsDir: "__tests__/ directories are banned under src/ — the test of <file>.ts is its neighbour <file>.test.ts (I2 — see docs/13-linting.md)."
3302
3283
  },
3303
3284
  type: "problem"
3304
3285
  }
@@ -3398,57 +3379,14 @@ const i4NoViMockInSrc = {
3398
3379
  meta: {
3399
3380
  docs: RULE_DOCS["i4-no-vi-mock-in-src"],
3400
3381
  messages: {
3401
- 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).",
3402
- 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).",
3403
- viMock: "`vi.mock` is banned under src/ — use mockOf/mockOfDate (I4 — see docs/10-linting.md)."
3382
+ 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).",
3383
+ 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).",
3384
+ viMock: "`vi.mock` is banned under src/ — use mockOf/mockOfDate (I4 — see docs/13-linting.md)."
3404
3385
  },
3405
3386
  type: "problem"
3406
3387
  }
3407
3388
  };
3408
3389
  //#endregion
3409
- //#region src/lint/rules/j1-no-only-skip.ts
3410
- /** Test runners whose `.only` / `.skip` modifiers must never be committed. */
3411
- const RUNNERS = /* @__PURE__ */ new Set([
3412
- "describe",
3413
- "it",
3414
- "test"
3415
- ]);
3416
- /** The focus/skip modifiers CONVENTIONS J1 forbids. */
3417
- const FORBIDDEN = /* @__PURE__ */ new Set(["only", "skip"]);
3418
- /**
3419
- * CONVENTIONS J1 — no committed `.only` / `.skip`.
3420
- *
3421
- * Flags the member form used across the specs: `describe.only`, `test.only`,
3422
- * `it.only` and their `.skip` counterparts. Modifiers that legitimately gate a
3423
- * test (`test.skipIf`, `test.runIf`) are untouched — only the bare `only`/`skip`
3424
- * that pin or disable a test in a commit are reported.
3425
- */
3426
- const j1NoOnlySkip = {
3427
- create(context) {
3428
- return { MemberExpression(node) {
3429
- if (node.computed === true) return;
3430
- const object = node.object;
3431
- const property = node.property;
3432
- if (object?.type !== "Identifier" || property?.type !== "Identifier") return;
3433
- const runner = object.name;
3434
- const modifier = property.name;
3435
- if (RUNNERS.has(runner) && FORBIDDEN.has(modifier)) context.report({
3436
- data: {
3437
- modifier,
3438
- runner
3439
- },
3440
- messageId: "forbidden",
3441
- node
3442
- });
3443
- } };
3444
- },
3445
- meta: {
3446
- docs: RULE_DOCS["j1-no-only-skip"],
3447
- messages: { forbidden: "Committed `{{runner}}.{{modifier}}` is not allowed (J1 — see docs/10-linting.md). Remove it before committing." },
3448
- type: "problem"
3449
- }
3450
- };
3451
- //#endregion
3452
3390
  //#region src/lint/rules/j2-no-sleep-in-specs.ts
3453
3391
  /**
3454
3392
  * CONVENTIONS J2 — spec files (`specs/**`) contain no arbitrary sleeps:
@@ -3487,138 +3425,13 @@ const j2NoSleepInSpecs = {
3487
3425
  meta: {
3488
3426
  docs: RULE_DOCS["j2-no-sleep-in-specs"],
3489
3427
  messages: {
3490
- sleep: "No arbitrary sleeps in specs — synchronise via `waitFor` or the framework (J2 — see docs/10-linting.md).",
3491
- timersImport: "No timer-based sleeps in specs — synchronise via `waitFor` or the framework (J2 — see docs/10-linting.md)."
3428
+ sleep: "No arbitrary sleeps in specs — synchronise via `waitFor` or the framework (J2 — see docs/13-linting.md).",
3429
+ timersImport: "No timer-based sleeps in specs — synchronise via `waitFor` or the framework (J2 — see docs/13-linting.md)."
3492
3430
  },
3493
3431
  type: "problem"
3494
3432
  }
3495
3433
  };
3496
3434
  //#endregion
3497
- //#region src/lint/rules/j3-no-expectless-test.ts
3498
- /** Does `callback` contain at least one `expect(…)` call? */
3499
- function hasExpect(callback) {
3500
- let found = false;
3501
- walk(callback, (node) => {
3502
- if (found || node.type !== "CallExpression") return;
3503
- const callee = node.callee;
3504
- if (callee?.type === "Identifier" && callee.name === "expect") found = true;
3505
- });
3506
- return found;
3507
- }
3508
- /**
3509
- * CONVENTIONS J3 — a test asserts something. A `test('…', () => { … })` whose
3510
- * body contains no `expect(…)` is either dead or a silent no-op.
3511
- *
3512
- * Reuses the shared B4 test-shape helpers. `test.todo(...)` (and any other
3513
- * callback-less form) is skipped — there is nothing to assert yet.
3514
- */
3515
- const j3NoExpectlessTest = {
3516
- create(context) {
3517
- return { CallExpression(node) {
3518
- const callee = node.callee;
3519
- if (!isTestCallee(callee)) return;
3520
- if (testCalleeHasModifier(callee, "todo")) return;
3521
- const callback = findTestCallback(node.arguments ?? []);
3522
- if (callback === void 0) return;
3523
- if (!hasExpect(callback)) context.report({
3524
- messageId: "noExpect",
3525
- node
3526
- });
3527
- } };
3528
- },
3529
- meta: {
3530
- docs: RULE_DOCS["j3-no-expectless-test"],
3531
- messages: { noExpect: "Test has no `expect(…)` — a test must assert something (J3 — see docs/10-linting.md). Use `test.todo` for a pending test." },
3532
- type: "problem"
3533
- }
3534
- };
3535
- //#endregion
3536
- //#region src/lint/rules/j4-unique-test-names.ts
3537
- /**
3538
- * CONVENTIONS J4 — test names are the sole description of behaviour (B3), so two
3539
- * tests sharing a literal name in one file are a copy-paste smell: one shadows
3540
- * the other in the reporter. Flags the second (and later) occurrence.
3541
- *
3542
- * `.each` / parametrized tests are skipped — their name is a template expanded
3543
- * per row, collisions there are by construction.
3544
- */
3545
- const j4UniqueTestNames = {
3546
- create(context) {
3547
- const seen = /* @__PURE__ */ new Set();
3548
- return { CallExpression(node) {
3549
- const callee = node.callee;
3550
- if (!isTestCallee(callee)) return;
3551
- if (testCalleeHasModifier(callee, "each")) return;
3552
- const name = testName(node);
3553
- if (name === void 0) return;
3554
- if (seen.has(name)) {
3555
- context.report({
3556
- data: { name },
3557
- messageId: "duplicate",
3558
- node
3559
- });
3560
- return;
3561
- }
3562
- seen.add(name);
3563
- } };
3564
- },
3565
- meta: {
3566
- docs: RULE_DOCS["j4-unique-test-names"],
3567
- 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)." },
3568
- type: "problem"
3569
- }
3570
- };
3571
- //#endregion
3572
- //#region src/lint/rules/j5-lowercase-title.ts
3573
- /** The block/test introducers whose title literal C1' constrains. */
3574
- const TITLED = /* @__PURE__ */ new Set([
3575
- "describe",
3576
- "it",
3577
- "test"
3578
- ]);
3579
- /** An identifier-shaped, all-caps/underscored first word: `VALID_CATEGORIES`,
3580
- * `HTTP`, `DI`. Such a word names a symbol verbatim — lowercasing it would
3581
- * misspell it — so it is exempt. A prose word (`Rejects`, `Builds`) is not. */
3582
- const ALL_CAPS_IDENTIFIER = /^[A-Z][A-Z0-9_]*$/u;
3583
- /**
3584
- * CONVENTIONS J5 — the first character of a `test('…')` / `describe('…')` title
3585
- * literal must be lowercase.
3586
- *
3587
- * The test name is the sole description of behaviour (B3): a sentence fragment,
3588
- * lowercase-led like prose (`'rejects an unknown job'`). Exempt are titles
3589
- * whose first WORD is an identifier-shaped all-caps/underscored token
3590
- * (`'VALID_CATEGORIES is rejected'`, `'HTTP is upgraded'`, `'DI wires …'`) —
3591
- * lowercasing a named symbol would misspell it — and titles opening on a
3592
- * non-letter (a `$`, a `{`, a `%s` placeholder). Only lowercase-able prose
3593
- * first words are enforced. Covers the modifier and parametrized forms
3594
- * (`test.only`, `describe.each(...)(...)`) via the chain root.
3595
- */
3596
- const j5LowercaseTitle = {
3597
- create(context) {
3598
- return { CallExpression(node) {
3599
- const callee = node.callee;
3600
- const root = chainRootName(callee);
3601
- if (root === void 0 || !TITLED.has(root)) return;
3602
- const title = stringValue(node.arguments?.[0]);
3603
- if (title === void 0) return;
3604
- const first = [...title][0];
3605
- if (first === void 0 || !/\p{L}/u.test(first)) return;
3606
- const firstWord = title.split(/\s+/u)[0];
3607
- if (ALL_CAPS_IDENTIFIER.test(firstWord)) return;
3608
- if (first !== first.toLocaleLowerCase()) context.report({
3609
- data: { runner: root },
3610
- messageId: "uppercase",
3611
- node
3612
- });
3613
- } };
3614
- },
3615
- meta: {
3616
- docs: RULE_DOCS["j5-lowercase-title"],
3617
- messages: { uppercase: "A {{runner}}() title must start lowercase — the test name is a prose fragment, not a sentence (J5 — see docs/10-linting.md)." },
3618
- type: "problem"
3619
- }
3620
- };
3621
- //#endregion
3622
3435
  //#region src/lint/rules/w1-scenario-pure.ts
3623
3436
  /** The scenario-carrying terminal actions: `.visit()` (website) and `.open()` (mobile). */
3624
3437
  const SCENARIO_ACTIONS$1 = /* @__PURE__ */ new Set(["open", "visit"]);
@@ -3648,7 +3461,7 @@ const w1ScenarioPure = {
3648
3461
  },
3649
3462
  meta: {
3650
3463
  docs: RULE_DOCS["w1-scenario-pure"],
3651
- 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)." },
3464
+ 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)." },
3652
3465
  type: "problem"
3653
3466
  }
3654
3467
  };
@@ -3664,7 +3477,7 @@ const SCENARIO_ACTIONS = /* @__PURE__ */ new Set(["open", "visit"]);
3664
3477
  *
3665
3478
  * Registered in a consumer's (or this repo's own) `oxlint.config.ts` via
3666
3479
  * `jsPlugins: ['@jterrazz/test/oxlint']` and referenced as `jterrazz/<rule>` in
3667
- * the `rules` map, e.g. `'jterrazz/j1-no-only-skip': 'error'` — or enabled
3480
+ * the `rules` map, e.g. `'jterrazz/j2-no-sleep-in-specs': 'error'` — or enabled
3668
3481
  * wholesale by spreading {@link recommendedRules}.
3669
3482
  *
3670
3483
  * This entry is bundled by tsdown (`dist/oxlint.js`); rules import nothing from
@@ -3714,11 +3527,7 @@ const plugin = {
3714
3527
  "i1-layer-boundaries": i1LayerBoundaries,
3715
3528
  "i2-sibling-test-naming": i2SiblingTestNaming,
3716
3529
  "i4-no-vi-mock-in-src": i4NoViMockInSrc,
3717
- "j1-no-only-skip": j1NoOnlySkip,
3718
3530
  "j2-no-sleep-in-specs": j2NoSleepInSpecs,
3719
- "j3-no-expectless-test": j3NoExpectlessTest,
3720
- "j4-unique-test-names": j4UniqueTestNames,
3721
- "j5-lowercase-title": j5LowercaseTitle,
3722
3531
  "w1-scenario-pure": w1ScenarioPure,
3723
3532
  "w2w-user-facing-elements": {
3724
3533
  create(context) {
@@ -3740,7 +3549,7 @@ const plugin = {
3740
3549
  },
3741
3550
  meta: {
3742
3551
  docs: RULE_DOCS["w2w-user-facing-elements"],
3743
- 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)." },
3552
+ 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)." },
3744
3553
  type: "suggestion"
3745
3554
  }
3746
3555
  }