@jterrazz/test 13.0.2 → 14.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
@@ -10,7 +10,7 @@ let node_url = require("node:url");
10
10
  * Cached filesystem probes for the fs-anchored rules (a9w, c2, c6, c7, i2).
11
11
  *
12
12
  * Rules only ever look at directories NEXT TO files oxlint is already
13
- * visiting (a feature's `requests/`, `seeds/`, `expected/`, a sibling module
13
+ * visiting (a feature's `_requests/`, `_seeds/`, `_expected/`, a sibling module
14
14
  * file), so the probe set is small — but several rules probe the same feature
15
15
  * directory, and oxlint lints many files per process. A module-level cache
16
16
  * keeps each `readdir`/`stat` to one syscall per lint run.
@@ -341,7 +341,7 @@ const RULE_DOCS = {
341
341
  },
342
342
  "c1-domain-structure": {
343
343
  channel: "statique",
344
- convention: "La forme de l’arbre `specs/` se déclare via l’option `depth` : `facet-domain` (défaut — `*.test.ts` à la profondeur facet/domain, `*.specification.ts` au root de la facette), `facet` (test au root de la facette OU un dossier domaine plus bas, jamais plus profond), `mirror` (test à toute profondeur ≥ 1, nommé d’après son dossier), `off`.",
344
+ convention: "La forme de l’arbre `specs/` se déclare via l’option `depth` : `facet-domain` (défaut — `*.test.ts` à la profondeur facet/domain, `*.specification.ts` au root de la facette), `facet` (test au root de la facette OU un dossier domaine plus bas, jamais plus profond), `mirror` (test à toute profondeur ≥ 1, nommé d’après son dossier), `off`. Dans tous les modes, `off` compris : un dossier à underscore initial est du SOL, jamais un domaine — aucune spec ne vit dedans.",
345
345
  family: "C",
346
346
  id: "C1",
347
347
  rationale: "Une profondeur fixe rend la place de chaque fichier prévisible et détectable statiquement."
@@ -360,9 +360,16 @@ const RULE_DOCS = {
360
360
  id: "C11",
361
361
  rationale: "Une donnée n’existe que servie par un contrat — un fichier sans propriétaire est du poids mort qu’aucun test ne charge."
362
362
  },
363
+ "c13-underscored-ground": {
364
+ channel: "statique",
365
+ convention: "Sous `specs/`, le sol d’une spec porte un underscore initial : `_fixtures/`, `_expected/`, `_requests/`, `_seeds/`. Un dossier `fixtures/`, `expected/`, `requests/` ou `seeds/` est un nom d’avant la 14 — erreur nommant le renommage.",
366
+ family: "C",
367
+ id: "C13",
368
+ rationale: "Aucun résolveur ne lit plus le nom sans underscore : gardé tel quel, l’arbre devient invisible plutôt que faux, et l’échec arrive une étape plus loin que sa cause."
369
+ },
363
370
  "c2-http-only-requests": {
364
371
  channel: "statique",
365
- convention: "`requests/` ne contient que des fichiers `.http` ; toute autre extension est une erreur.",
372
+ convention: "`_requests/` ne contient que des fichiers `.http` ; toute autre extension est une erreur.",
366
373
  family: "C",
367
374
  id: "C2",
368
375
  rationale: "Une entrée de requête est un `.http` complet — homogénéiser le dossier interdit les formats ad hoc."
@@ -379,11 +386,11 @@ const RULE_DOCS = {
379
386
  convention: "L’argument de `toMatch` porte son extension (`'help.txt'`), sauf pour les snapshots d’arborescence (dossiers) ; un sujet fichier sans extension est une erreur.",
380
387
  family: "C",
381
388
  id: "C6",
382
- rationale: "L’extension fait partie du nom du fichier attendu — l’omettre casse la résolution `expected/`."
389
+ rationale: "L’extension fait partie du nom du fichier attendu — l’omettre casse la résolution `_expected/`."
383
390
  },
384
391
  "c7-seeds-sql-only": {
385
392
  channel: "statique",
386
- convention: "`seeds/` ne contient que des `*.sql` ; tout autre fichier est une erreur.",
393
+ convention: "`_seeds/` ne contient que des `*.sql` ; tout autre fichier est une erreur.",
387
394
  family: "C",
388
395
  id: "C7",
389
396
  rationale: "`.seed()` porte l’état des bases uniquement — pas de seed-handler ni de dispatch par préfixe."
@@ -425,7 +432,7 @@ const RULE_DOCS = {
425
432
  },
426
433
  "d9w-single-use-ref": {
427
434
  channel: "statique",
428
- convention: "Une ref de capture (`match.ref`, `{{kind#ref}}`) qui n’apparaît qu’une seule fois dans tout le fichier (code + fixtures `expected/` référencées) porte un nom inutilement → warning.",
435
+ convention: "Une ref de capture (`match.ref`, `{{kind#ref}}`) qui n’apparaît qu’une seule fois dans tout le fichier (code + fixtures `_expected/` référencées) porte un nom inutilement → warning.",
429
436
  family: "D",
430
437
  id: "D9",
431
438
  rationale: "Une ref ne se justifie que si elle asserte l’égalité sur au moins deux occurrences."
@@ -567,7 +574,7 @@ const RULE_DOCS = {
567
574
  const CHECKER_PASSES = [
568
575
  {
569
576
  channel: "checker",
570
- convention: "Tout `{{token}}` dans une fixture `expected/` — ou dans un flux attendu d’un document `<cas>.spec.yaml` — appartient au vocabulaire figé ; un token inconnu est une erreur.",
577
+ convention: "Tout `{{token}}` dans une fixture `_expected/` — ou dans un flux attendu d’un document `<cas>.spec.yaml` — appartient au vocabulaire figé ; un token inconnu est une erreur.",
571
578
  family: "D",
572
579
  id: "D4",
573
580
  name: "d4-unknown-token",
@@ -575,7 +582,7 @@ const CHECKER_PASSES = [
575
582
  },
576
583
  {
577
584
  channel: "checker",
578
- convention: "Une ref malformée d’un kind connu (`{{iso8601#}}`, `{{uuid #id}}`) dans un fichier texte sous `expected/` est une erreur.",
585
+ convention: "Une ref malformée d’un kind connu (`{{iso8601#}}`, `{{uuid #id}}`) dans un fichier texte sous `_expected/` est une erreur.",
579
586
  family: "D",
580
587
  id: "D4",
581
588
  name: "d4-malformed-ref",
@@ -583,7 +590,7 @@ const CHECKER_PASSES = [
583
590
  },
584
591
  {
585
592
  channel: "checker",
586
- convention: "La première ligne d’un `.http` de profondeur 1 suit sa grammaire : requête (`MÉTHODE /path`) sous `requests/`, statut (`HTTP/1.1 <status>`) sous `expected/`.",
593
+ convention: "La première ligne d’un `.http` de profondeur 1 suit sa grammaire : requête (`MÉTHODE /path`) sous `_requests/`, statut (`HTTP/1.1 <status>`) sous `_expected/`.",
587
594
  family: "D",
588
595
  id: "D4b",
589
596
  name: "d4b-http-first-line",
@@ -679,7 +686,7 @@ const CHECKER_PASSES = [
679
686
  },
680
687
  {
681
688
  channel: "checker",
682
- convention: "Un token connu dans un fichier sous `requests/` → warning : les requêtes sont des entrées, jamais matchées.",
689
+ convention: "Un token connu dans un fichier sous `_requests/` → warning : les requêtes sont des entrées, jamais matchées.",
683
690
  family: "D",
684
691
  id: "D10",
685
692
  name: "d10w-tokens-in-requests",
@@ -687,12 +694,28 @@ const CHECKER_PASSES = [
687
694
  },
688
695
  {
689
696
  channel: "checker",
690
- convention: "Aucune fixture morte : tout fichier sous `seeds/`/`requests/`/`fixtures/` et toute entrée de premier niveau de `expected/` doit être référencée ; un dossier de feature sans `*.test.ts` ni `*.spec.yaml` est orphelin (warning si argument non littéral). Un `<cas>.spec.yaml` référence les fixtures nommées par ses entrées `fixture:`.",
697
+ convention: "Aucune fixture morte : tout fichier sous `_seeds/`/`_requests/`/`_fixtures/` et toute entrée de premier niveau de `_expected/` doit être référencée ; un dossier de feature sans `*.test.ts` ni `*.spec.yaml` est orphelin (warning si argument non littéral). Un `<cas>.spec.yaml` référence les fixtures nommées par ses entrées `fixture:`.",
691
698
  family: "C",
692
699
  id: "C9",
693
700
  name: "c9-dead-fixtures",
694
701
  rationale: "Le miroir de C8 — une fixture que rien ne référence est du poids mort qui trompe le lecteur."
695
702
  },
703
+ {
704
+ channel: "checker",
705
+ convention: "Une fixture du pool est PARTAGÉE, ou elle est locale : un dossier de `<specs>/_fixtures/` référencé (`$FIXTURES/<nom>`) depuis un seul dossier de spec — deux documents d’une même feuille comptent pour un — doit vivre à côté de cette feuille, en `<feuille>/_fixtures/<nom>/`, référencé par la forme relative. Zéro référence reste l’erreur de fixture morte (C9). Corrigé par `--fix` : le dossier est déplacé et les littéraux réécrits.",
706
+ family: "C",
707
+ id: "C14",
708
+ name: "c14-pool-fixture-shared",
709
+ rationale: "Une fixture d’un seul scénario garée dans le pool se lit comme du sol partagé : chacun suppose qu’un autre en dépend et personne n’ose y toucher."
710
+ },
711
+ {
712
+ channel: "checker",
713
+ convention: "Une fixture locale ne se lit que depuis son propre dossier : un chemin `fixture:`/`.fixture()` contenant `..`, ou dont la cible sort du `_fixtures/` de la spec qui le nomme, est une erreur — le sol partagé par plusieurs feuilles va dans le pool `$FIXTURES`.",
714
+ family: "C",
715
+ id: "C15",
716
+ name: "c15-local-fixture-reach",
717
+ rationale: "Une feuille qui pioche dans les fixtures d’une voisine, c’est le pool par une autre porte, sans la visibilité du pool : la voisine casse une spec à deux dossiers de là sans le savoir."
718
+ },
696
719
  {
697
720
  channel: "checker",
698
721
  convention: "Canal principal de B5 : les runners docker-aware sont inférés de l’option `docker:` du fichier de specification importé, puis chaque résultat `.exec()` lié sans `await using` est signalé.",
@@ -1236,16 +1259,58 @@ const a9wRedundantRoot = {
1236
1259
  }
1237
1260
  };
1238
1261
  //#endregion
1262
+ //#region src/core/specification/shared/ground.ts
1263
+ /**
1264
+ * The GROUND a row of specs stands on — the naming law of a spec tree.
1265
+ *
1266
+ * In a row of specs, a MEMBER is a spec: `<aspect>.test.ts`, `<case>.spec.yaml`,
1267
+ * `<name>.specification.ts`, and the facet/domain folders that hold them.
1268
+ * Everything else in that row is what the members STAND ON — inert material the
1269
+ * framework resolves BY PATH, never imported, never executed — and it carries a
1270
+ * LEADING UNDERSCORE. A spec's own folder never does.
1271
+ *
1272
+ * Four names, and only four: `_fixtures/` (file state), `_expected/` (goldens),
1273
+ * `_requests/` (inputs), `_seeds/` (database state). `contracts/` is NOT ground —
1274
+ * a contract is TypeScript a spec imports, behaviour composed with the spec's own,
1275
+ * so it stays a member. `docker/` is not ground either: it sits at the PROJECT
1276
+ * root beside `package.json`, outside any row of specs.
1277
+ *
1278
+ * This module is the single home of those names. Every resolver, matcher, lint
1279
+ * rule and checker pass reads them from here, so the runtime and the static
1280
+ * channels cannot disagree about where a spec's ground lives.
1281
+ */
1282
+ /** Goldens: every expected fixture, flat, extension included. */
1283
+ const GROUND_EXPECTED = "_expected";
1284
+ /** File state copied into a working directory by `.fixture()`. */
1285
+ const GROUND_FIXTURES = "_fixtures";
1286
+ /** Inputs: complete `.http` request documents. */
1287
+ const GROUND_REQUESTS = "_requests";
1288
+ /** Database state: `.sql` fragments loaded by `.seed()`. */
1289
+ const GROUND_SEEDS = "_seeds";
1290
+ /**
1291
+ * The pre-14 name of each ground directory → the name it carries now. Kept for
1292
+ * ONE purpose: naming the rename in a diagnostic (`c13-underscored-ground`).
1293
+ * Nothing resolves through it — an un-underscored directory is invisible to
1294
+ * every resolver, which is exactly why it must be reported rather than tolerated.
1295
+ */
1296
+ const RENAMED_GROUND_DIRS = {
1297
+ expected: GROUND_EXPECTED,
1298
+ fixtures: GROUND_FIXTURES,
1299
+ requests: GROUND_REQUESTS,
1300
+ seeds: GROUND_SEEDS
1301
+ };
1302
+ //#endregion
1239
1303
  //#region src/core/specification/shared/fixtures.ts
1240
1304
  /**
1241
1305
  * Fixture path resolution + copy semantics for the `cli` facet's `.fixture()`.
1242
1306
  *
1243
1307
  * A fixture path is one of two shapes:
1244
1308
  *
1245
- * - `$FIXTURES/<rest>` — the shared pool at `<specs-root>/fixtures/<rest>`,
1309
+ * - `$FIXTURES/<rest>` — the shared pool at `<specs-root>/_fixtures/<rest>`,
1246
1310
  * where `<specs-root>` is the nearest ancestor directory named `specs`.
1247
- * - `<path>` (no marker) — feature-local, at `<test-dir>/fixtures/<path>`.
1311
+ * - `<path>` (no marker) — feature-local, at `<test-dir>/_fixtures/<path>`.
1248
1312
  *
1313
+ * The leading underscore is the ground rule of a spec tree ({@link GROUND_FIXTURES}).
1249
1314
  * Any other `$`-prefixed marker is a usage error (listed against the known
1250
1315
  * markers). Copy semantics mirror rsync's trailing-slash rule (see
1251
1316
  * {@link copyPlan}).
@@ -1580,7 +1645,12 @@ const DEFAULT_DEPTH = "facet-domain";
1580
1645
  * specs root. Specification files are unconstrained: a mirror has no facet
1581
1646
  * level to anchor them to.
1582
1647
  *
1583
- * `depth: 'off'` — no placement check; the tree's shape is guarded elsewhere.
1648
+ * `depth: 'off'` — no DEPTH check; the tree's shape is guarded elsewhere.
1649
+ *
1650
+ * The ground clause holds in every mode, `off` included: a directory whose name
1651
+ * carries a LEADING UNDERSCORE is ground — what the specs of a row stand on —
1652
+ * and never a domain, so no spec may live inside one. That is not a question of
1653
+ * depth, which is why no project's declared shape switches it off.
1584
1654
  *
1585
1655
  * Module tests under `src/` follow the neighbour rule (I2) and are out of scope
1586
1656
  * in every mode.
@@ -1588,12 +1658,23 @@ const DEFAULT_DEPTH = "facet-domain";
1588
1658
  const c1DomainStructure = {
1589
1659
  create(context) {
1590
1660
  const depth = context.options[0]?.depth ?? DEFAULT_DEPTH;
1591
- if (depth === "off") return {};
1592
1661
  return { Program(node) {
1593
1662
  const anchor = specsAnchor(context.filename);
1594
1663
  if (anchor === void 0) return;
1595
1664
  const base = anchor.relative.at(-1) ?? "";
1596
1665
  const nesting = anchor.relative.length - 1;
1666
+ if (base.endsWith(TEST_SUFFIX) || base.endsWith(SPECIFICATION_SUFFIX)) {
1667
+ const ground = anchor.relative.slice(0, -1).find((segment) => segment.startsWith("_"));
1668
+ if (ground !== void 0) {
1669
+ context.report({
1670
+ data: { ground },
1671
+ messageId: "specInGround",
1672
+ node
1673
+ });
1674
+ return;
1675
+ }
1676
+ }
1677
+ if (depth === "off") return;
1597
1678
  if (depth === "facet") {
1598
1679
  if (base.endsWith(TEST_SUFFIX)) {
1599
1680
  if (nesting < 1) context.report({
@@ -1652,6 +1733,7 @@ const c1DomainStructure = {
1652
1733
  defaultOptions: [{ depth: DEFAULT_DEPTH }],
1653
1734
  docs: RULE_DOCS["c1-domain-structure"],
1654
1735
  messages: {
1736
+ 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).",
1655
1737
  specNotAtFacetRoot: "A `*.specification.ts` must sit at the facet root: `specs/<facet>/<name>.specification.ts` (C1 — see docs/10-linting.md).",
1656
1738
  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).",
1657
1739
  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).",
@@ -1709,7 +1791,7 @@ const c10ContractsBoundary = {
1709
1791
  };
1710
1792
  //#endregion
1711
1793
  //#region src/lint/rules/c11-contract-data-pairing.ts
1712
- const TEST_FILE$11 = /\.test\.[cm]?[jt]sx?$/;
1794
+ const TEST_FILE$12 = /\.test\.[cm]?[jt]sx?$/;
1713
1795
  /** The provider folders that may carry data files. */
1714
1796
  const PROVIDERS$1 = [
1715
1797
  "anthropic",
@@ -1733,7 +1815,7 @@ function ownerOf(entry) {
1733
1815
  const c11ContractDataPairing = {
1734
1816
  create(context) {
1735
1817
  const file = context.physicalFilename;
1736
- if (!TEST_FILE$11.test(file) || !segments(file).includes("specs")) return {};
1818
+ if (!TEST_FILE$12.test(file) || !segments(file).includes("specs")) return {};
1737
1819
  return { Program(node) {
1738
1820
  const contractsDir = (0, node_path.join)((0, node_path.dirname)(file), "contracts");
1739
1821
  for (const provider of PROVIDERS$1) {
@@ -1763,13 +1845,66 @@ const c11ContractDataPairing = {
1763
1845
  }
1764
1846
  };
1765
1847
  //#endregion
1848
+ //#region src/lint/rules/c13-underscored-ground.ts
1849
+ const TEST_FILE$11 = /\.test\.[cm]?[jt]sx?$/;
1850
+ /**
1851
+ * CONVENTIONS C13 — the ground of a spec carries a leading underscore. A
1852
+ * directory named `fixtures/`, `expected/`, `requests/` or `seeds/` under
1853
+ * `specs/` is a pre-14 name: since `@jterrazz/test` 14.0.0 the four are
1854
+ * `_fixtures/`, `_expected/`, `_requests/` and `_seeds/`, with no compatibility
1855
+ * fallback.
1856
+ *
1857
+ * The catch this rule exists for: a consumer that upgrades and keeps the old
1858
+ * names loses nothing loudly. Every resolver — `.fixture()`, `.seed()`,
1859
+ * `.request()`, `toMatch()` — looks under the underscored name, finds nothing,
1860
+ * and the tree becomes invisible rather than wrong. The failure that follows is
1861
+ * "fixture does not exist" on every spec at once, one step removed from the
1862
+ * cause; this rule states the cause and the rename.
1863
+ *
1864
+ * Anchored on the feature's visited test file, exactly as C2/C7/C11 are: the
1865
+ * spec's own sibling directories are probed, plus the `$FIXTURES` pool at the
1866
+ * nearest specs root. A DOMAIN folder named after the ground it holds
1867
+ * (`specs/api/requests/_requests/`) is never a probe target — only what sits
1868
+ * beside a spec is.
1869
+ */
1870
+ const c13UnderscoredGround = {
1871
+ create(context) {
1872
+ const file = context.physicalFilename;
1873
+ const anchor = specsAnchor(file);
1874
+ if (!TEST_FILE$11.test(file) || anchor === void 0) return {};
1875
+ return { Program(node) {
1876
+ const seen = /* @__PURE__ */ new Set();
1877
+ const probe = (parent, legacy) => {
1878
+ const path = (0, node_path.join)(parent, legacy);
1879
+ if (seen.has(path) || !isDirectory(path)) return;
1880
+ seen.add(path);
1881
+ context.report({
1882
+ data: {
1883
+ ground: RENAMED_GROUND_DIRS[legacy],
1884
+ legacy
1885
+ },
1886
+ messageId: "notUnderscored",
1887
+ node
1888
+ });
1889
+ };
1890
+ for (const legacy of Object.keys(RENAMED_GROUND_DIRS)) probe((0, node_path.dirname)(file), legacy);
1891
+ probe(anchor.directory, "fixtures");
1892
+ } };
1893
+ },
1894
+ meta: {
1895
+ docs: RULE_DOCS["c13-underscored-ground"],
1896
+ 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)." },
1897
+ type: "problem"
1898
+ }
1899
+ };
1900
+ //#endregion
1766
1901
  //#region src/lint/rules/c2-http-only-requests.ts
1767
1902
  const TEST_FILE$10 = /\.test\.[cm]?[jt]sx?$/;
1768
1903
  /**
1769
- * CONVENTIONS C2 — `requests/` contains only `.http` files (a request file is
1904
+ * CONVENTIONS C2 — `_requests/` contains only `.http` files (a request file is
1770
1905
  * a COMPLETE request: method, path, headers, body). Anchored on the feature's
1771
1906
  * visited test file: when `<feature>/<feature>.test.ts` is linted, its sibling
1772
- * `requests/` directory is probed (cached readdir) and any non-`.http` entry
1907
+ * `_requests/` directory is probed (cached readdir) and any non-`.http` entry
1773
1908
  * is reported on the test file.
1774
1909
  */
1775
1910
  const c2HttpOnlyRequests = {
@@ -1777,7 +1912,7 @@ const c2HttpOnlyRequests = {
1777
1912
  const file = context.physicalFilename;
1778
1913
  if (!TEST_FILE$10.test(file) || !segments(file).includes("specs")) return {};
1779
1914
  return { Program(node) {
1780
- const requestsDir = (0, node_path.join)((0, node_path.dirname)(file), "requests");
1915
+ const requestsDir = (0, node_path.join)((0, node_path.dirname)(file), GROUND_REQUESTS);
1781
1916
  for (const entry of listDirectory(requestsDir) ?? []) if (!entry.endsWith(".http")) context.report({
1782
1917
  data: { entry },
1783
1918
  messageId: "notHttp",
@@ -1787,7 +1922,7 @@ const c2HttpOnlyRequests = {
1787
1922
  },
1788
1923
  meta: {
1789
1924
  docs: RULE_DOCS["c2-http-only-requests"],
1790
- messages: { notHttp: "requests/{{entry}} is not a .http file — requests/ contains only complete .http request files (C2 — see docs/10-linting.md)." },
1925
+ messages: { notHttp: "_requests/{{entry}} is not a .http file — _requests/ contains only complete .http request files (C2 — see docs/10-linting.md)." },
1791
1926
  type: "problem"
1792
1927
  }
1793
1928
  };
@@ -2045,11 +2180,11 @@ function subjectIsDirectory(toMatchCallee) {
2045
2180
  /**
2046
2181
  * CONVENTIONS C6 — the extension is part of the fixture name and mandatory in
2047
2182
  * `toMatch` (`'help.txt'`, never `'help'`) — except for directory-tree
2048
- * snapshots, which are directories under `expected/`.
2183
+ * snapshots, which are directories under `_expected/`.
2049
2184
  *
2050
2185
  * The directory exception is resolved two ways: statically, when the expect
2051
2186
  * subject visibly chains `.directory(…)`; else on disk, when
2052
- * `expected/<name>/` exists next to the spec (cached stat).
2187
+ * `_expected/<name>/` exists next to the spec (cached stat).
2053
2188
  */
2054
2189
  const c6ToMatchExtension = {
2055
2190
  create(context) {
@@ -2063,7 +2198,7 @@ const c6ToMatchExtension = {
2063
2198
  if (name === void 0 || name.length === 0) return;
2064
2199
  if ((name.split("/").at(-1) ?? "").includes(".")) return;
2065
2200
  if (subjectIsDirectory(callee)) return;
2066
- if (isDirectory((0, node_path.join)((0, node_path.dirname)(file), "expected", name))) return;
2201
+ if (isDirectory((0, node_path.join)((0, node_path.dirname)(file), "_expected", name))) return;
2067
2202
  context.report({
2068
2203
  data: { name },
2069
2204
  messageId: "missingExtension",
@@ -2073,7 +2208,7 @@ const c6ToMatchExtension = {
2073
2208
  },
2074
2209
  meta: {
2075
2210
  docs: RULE_DOCS["c6-tomatch-extension"],
2076
- 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)." },
2211
+ 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)." },
2077
2212
  type: "problem"
2078
2213
  }
2079
2214
  };
@@ -2081,17 +2216,17 @@ const c6ToMatchExtension = {
2081
2216
  //#region src/lint/rules/c7-seeds-sql-only.ts
2082
2217
  const TEST_FILE$7 = /\.test\.[cm]?[jt]sx?$/;
2083
2218
  /**
2084
- * CONVENTIONS C7 — `.seed()` carries DATABASE state only: `seeds/` contains
2219
+ * CONVENTIONS C7 — `.seed()` carries DATABASE state only: `_seeds/` contains
2085
2220
  * nothing but `*.sql` fragments. File state goes through `.fixture()`; there
2086
2221
  * is no seed handler and no prefix dispatch. Anchored on the feature's visited
2087
- * test file (cached readdir of the sibling `seeds/`).
2222
+ * test file (cached readdir of the sibling `_seeds/`).
2088
2223
  */
2089
2224
  const c7SeedsSqlOnly = {
2090
2225
  create(context) {
2091
2226
  const file = context.physicalFilename;
2092
2227
  if (!TEST_FILE$7.test(file) || !segments(file).includes("specs")) return {};
2093
2228
  return { Program(node) {
2094
- const seedsDir = (0, node_path.join)((0, node_path.dirname)(file), "seeds");
2229
+ const seedsDir = (0, node_path.join)((0, node_path.dirname)(file), GROUND_SEEDS);
2095
2230
  for (const entry of listDirectory(seedsDir) ?? []) if (!entry.endsWith(".sql")) context.report({
2096
2231
  data: { entry },
2097
2232
  messageId: "notSql",
@@ -2101,18 +2236,18 @@ const c7SeedsSqlOnly = {
2101
2236
  },
2102
2237
  meta: {
2103
2238
  docs: RULE_DOCS["c7-seeds-sql-only"],
2104
- 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)." },
2239
+ 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)." },
2105
2240
  type: "problem"
2106
2241
  }
2107
2242
  };
2108
2243
  //#endregion
2109
2244
  //#region src/lint/rules/c8-referenced-fixture-exists.ts
2110
2245
  const TEST_FILE$6 = /\.test\.[cm]?[jt]sx?$/;
2111
- /** The conventional sibling directory each fixture-referencing verb reads from. */
2246
+ /** The ground directory each fixture-referencing verb reads from. */
2112
2247
  const VERB_ROOTS = {
2113
- request: "requests",
2114
- seed: "seeds",
2115
- toMatch: "expected"
2248
+ request: GROUND_REQUESTS,
2249
+ seed: GROUND_SEEDS,
2250
+ toMatch: GROUND_EXPECTED
2116
2251
  };
2117
2252
  /**
2118
2253
  * A fixture reference is a filename/path, never inline content. `.seed()` also
@@ -2124,15 +2259,15 @@ function looksLikePath(value) {
2124
2259
  }
2125
2260
  /** Resolve the on-disk target a `.fixture()` literal points at, if determinable. */
2126
2261
  function resolveFixture(argument, featureDir, specsDir) {
2127
- if (argument.startsWith("$FIXTURES/")) return (0, node_path.join)(specsDir, "fixtures", argument.slice(10));
2262
+ if (argument.startsWith("$FIXTURES/")) return (0, node_path.join)(specsDir, GROUND_FIXTURES, argument.slice(10));
2128
2263
  if (argument.startsWith("$")) return;
2129
- return (0, node_path.join)(featureDir, "fixtures", argument);
2264
+ return (0, node_path.join)(featureDir, GROUND_FIXTURES, argument);
2130
2265
  }
2131
2266
  /**
2132
2267
  * CONVENTIONS C8 — a fixture referenced by a literal must exist on disk under
2133
- * its conventional root: `.request(x)`→`requests/x`, `.seed(x)`→`seeds/x`,
2134
- * `.fixture(x)`→feature `fixtures/` (or the `$FIXTURES/` pool at the nearest
2135
- * `specs/fixtures/`), `toMatch(x)`→`expected/x` (file or tree snapshot). A typo
2268
+ * its ground root: `.request(x)`→`_requests/x`, `.seed(x)`→`_seeds/x`,
2269
+ * `.fixture(x)`→the leaf's `_fixtures/` (or the `$FIXTURES/` pool at the nearest
2270
+ * `specs/_fixtures/`), `toMatch(x)`→`_expected/x` (file or tree snapshot). A typo
2136
2271
  * that would fail only at runtime is caught statically. Non-literal arguments
2137
2272
  * are out of static reach and skipped.
2138
2273
  */
@@ -2154,7 +2289,7 @@ const c8ReferencedFixtureExists = {
2154
2289
  if (target !== void 0 && !isFile(target) && !isDirectory(target)) context.report({
2155
2290
  data: {
2156
2291
  path: value,
2157
- root: "fixtures"
2292
+ root: GROUND_FIXTURES
2158
2293
  },
2159
2294
  messageId: "missing",
2160
2295
  node: args[0]
@@ -2178,7 +2313,7 @@ const c8ReferencedFixtureExists = {
2178
2313
  },
2179
2314
  meta: {
2180
2315
  docs: RULE_DOCS["c8-referenced-fixture-exists"],
2181
- messages: { missing: "Referenced fixture \"{{path}}\" does not exist on disk under its conventional {{root}}/ root (C8 — see docs/10-linting.md). Create it or fix the reference." },
2316
+ 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." },
2182
2317
  type: "problem"
2183
2318
  }
2184
2319
  };
@@ -2592,7 +2727,7 @@ const d6wTransformTokenEquivalent = {
2592
2727
  },
2593
2728
  meta: {
2594
2729
  docs: RULE_DOCS["d6w-transform-token-equivalent"],
2595
- 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)." },
2730
+ 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)." },
2596
2731
  type: "suggestion"
2597
2732
  }
2598
2733
  };
@@ -2661,7 +2796,7 @@ function refsInFixture(target) {
2661
2796
  * `{{uuid#order}}`) earns its keep only when it is asserted more than once (the
2662
2797
  * point of a capture is the equality check on later occurrences). Aggregating
2663
2798
  * per file — code `match.ref` literals plus the `{{kind#ref}}` tokens of the
2664
- * `expected/` fixtures the file references — a ref that appears exactly once
2799
+ * `_expected/` fixtures the file references — a ref that appears exactly once
2665
2800
  * everywhere is a plain matcher wearing a name.
2666
2801
  */
2667
2802
  const d9wSingleUseRef = {
@@ -2689,7 +2824,7 @@ const d9wSingleUseRef = {
2689
2824
  if (memberPropertyName(node.callee) === "toMatch") {
2690
2825
  const name = stringValue(node.arguments?.[0]);
2691
2826
  if (name !== void 0) {
2692
- const target = (0, node_path.join)(featureDir, "expected", name.replace(/\/+$/, ""));
2827
+ const target = (0, node_path.join)(featureDir, GROUND_EXPECTED, name.replace(/\/+$/, ""));
2693
2828
  for (const ref of refsInFixture(target)) {
2694
2829
  bump(ref);
2695
2830
  if (!fixtureNodes.has(ref)) fixtureNodes.set(ref, node);
@@ -3543,6 +3678,7 @@ const plugin = {
3543
3678
  "c1-domain-structure": c1DomainStructure,
3544
3679
  "c10-contracts-boundary": c10ContractsBoundary,
3545
3680
  "c11-contract-data-pairing": c11ContractDataPairing,
3681
+ "c13-underscored-ground": c13UnderscoredGround,
3546
3682
  "c2-http-only-requests": c2HttpOnlyRequests,
3547
3683
  "c4-contract-shape": c4ContractShape,
3548
3684
  "c6-tomatch-extension": c6ToMatchExtension,