@jterrazz/test 15.0.0 → 15.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/oxlint.js CHANGED
@@ -1,6 +1,5 @@
1
1
  import { basename, dirname, join, resolve } from "node:path";
2
2
  import { readFileSync, readdirSync, statSync } from "node:fs";
3
- import { fileURLToPath } from "node:url";
4
3
  //#region src/lint/fs-cache.ts
5
4
  /**
6
5
  * Cached filesystem probes for the fs-anchored rules (a9w, c2, c6, c7, i2).
@@ -66,7 +65,7 @@ function readFileCached(path) {
66
65
  */
67
66
  /** Split a path into its non-empty segments (posix or win separators). */
68
67
  function segments(path) {
69
- return path.split(/[/\\]/).filter(Boolean);
68
+ return path.split(/[/\\]/u).filter(Boolean);
70
69
  }
71
70
  /**
72
71
  * The `specs/` directory a file belongs to: the NEAREST ancestor named `specs`,
@@ -108,7 +107,8 @@ function stringValue(node) {
108
107
  if (node.type === "TemplateLiteral") {
109
108
  const expressions = node.expressions;
110
109
  const quasis = node.quasis;
111
- if (expressions?.length === 0 && quasis?.length === 1) return quasis[0].value?.cooked;
110
+ const onlyQuasi = quasis?.[0];
111
+ if (expressions?.length === 0 && quasis?.length === 1 && onlyQuasi !== void 0) return onlyQuasi.value?.cooked;
112
112
  }
113
113
  }
114
114
  /** The property name of a non-computed member expression, if identifiable. */
@@ -479,7 +479,7 @@ const RULE_DOCS = {
479
479
  },
480
480
  "f3-specs-public-entry": {
481
481
  channel: "statique",
482
- convention: "Depuis `specs/`, seul l’import en profondeur des INTERNES du framework est interdit : un chemin relatif résolvant dans `src/{core,integrations,vitest,lint}/` du dépôt du framework, ou tout `@jterrazz/test/<subpath>` autre que `@jterrazz/test/oxlint`. Les imports de la source de SA PROPRE app par un consommateur sont toujours permis (c’est le motif documenté) ; seul `specs/integrations/` peut importer en profondeur `src/integrations/**`.",
482
+ convention: "Depuis `specs/`, seul l’import en profondeur des INTERNES du framework est interdit : un chemin relatif résolvant dans `src/{specification,integrations,vitest,lint}/` du dépôt du framework, ou tout `@jterrazz/test/<subpath>` autre que `@jterrazz/test/oxlint`. Les imports de la source de SA PROPRE app par un consommateur sont toujours permis (c’est le motif documenté) ; seul `specs/integrations/` peut importer en profondeur `src/integrations/**`.",
483
483
  family: "F",
484
484
  id: "F3",
485
485
  rationale: "Tester par la surface publique garde les specs découplées des chemins internes du framework, sans gêner le consommateur qui importe sa propre app."
@@ -889,83 +889,6 @@ const a1SpecificationFile = {
889
889
  }
890
890
  };
891
891
  //#endregion
892
- //#region src/core/specification/shared/binding.ts
893
- /**
894
- * Compose-binding resolution and the case conversions it shares with the
895
- * automatic env injection (CONVENTIONS A6 / B6).
896
- *
897
- * A services-record key is written in the natural TypeScript style
898
- * (`analyticsDb`), while a compose service name is kebab-case
899
- * (`analytics-db`). These helpers bridge the two vocabularies deterministically
900
- * so the record key stays the single source of truth and `composeService`
901
- * remains a rare escape hatch.
902
- */
903
- /**
904
- * Convert a record key to its kebab-case form: camelCase boundaries become
905
- * `-`, everything is lowercased. `analyticsDb` → `analytics-db`, `db` → `db`.
906
- */
907
- function toKebabCase(key) {
908
- return key.replace(/(?<lower>[a-z0-9])(?<upper>[A-Z])/g, "$<lower>-$<upper>").toLowerCase();
909
- }
910
- //#endregion
911
- //#region src/lint/rules/a10-duplicate-binding.ts
912
- /** The compose service an entry binds to: explicit `composeService`, else the key's kebab form. */
913
- function canonicalBinding(entry, key) {
914
- const factory = entry.value;
915
- if (factory?.type === "CallExpression") {
916
- const options = factory.arguments?.[0];
917
- if (options?.type === "ObjectExpression") {
918
- const explicit = stringValue(findProperty(options, "composeService")?.value);
919
- if (explicit !== void 0) return explicit;
920
- }
921
- }
922
- return toKebabCase(key);
923
- }
924
- /**
925
- * CONVENTIONS A10 — within one `services` record, two keys that derive (or
926
- * pin via `composeService`) the SAME compose service name are a silent
927
- * collision: the record is a map, and the second binding would shadow the
928
- * first. Detected on the same kebab derivation the runtime binding uses
929
- * (`analyticsDb` and `analytics-db` both → `analytics-db`), and across explicit
930
- * `composeService` values.
931
- */
932
- const a10DuplicateBinding = {
933
- create(context) {
934
- return { CallExpression(node) {
935
- if (specificationMember(node) === void 0) return;
936
- for (const argument of node.arguments ?? []) {
937
- const record = findProperty(argument, "services")?.value;
938
- if (record?.type !== "ObjectExpression") continue;
939
- const seen = /* @__PURE__ */ new Map();
940
- for (const entry of record.properties ?? []) {
941
- const key = propertyKeyName(entry);
942
- if (key === void 0) continue;
943
- const binding = canonicalBinding(entry, key);
944
- const first = seen.get(binding);
945
- if (first !== void 0) {
946
- context.report({
947
- data: {
948
- binding,
949
- first,
950
- key
951
- },
952
- messageId: "duplicate",
953
- node: entry
954
- });
955
- continue;
956
- }
957
- seen.set(binding, key);
958
- }
959
- }
960
- } };
961
- },
962
- meta: {
963
- docs: RULE_DOCS["a10-duplicate-binding"],
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)." },
965
- type: "problem"
966
- }
967
- };
968
- //#endregion
969
892
  //#region src/lint/rules/a2-known-constructors.ts
970
893
  /** The five constructors, and only five (A2 — see docs/13-linting.md). */
971
894
  const KNOWN_CONSTRUCTORS = /* @__PURE__ */ new Set([
@@ -1128,6 +1051,25 @@ const a5ModeWithServer = {
1128
1051
  }
1129
1052
  };
1130
1053
  //#endregion
1054
+ //#region src/specification/facets/_common/binding.ts
1055
+ /**
1056
+ * Compose-binding resolution and the case conversions it shares with the
1057
+ * automatic env injection (CONVENTIONS A6 / B6).
1058
+ *
1059
+ * A services-record key is written in the natural TypeScript style
1060
+ * (`analyticsDb`), while a compose service name is kebab-case
1061
+ * (`analytics-db`). These helpers bridge the two vocabularies deterministically
1062
+ * so the record key stays the single source of truth and `composeService`
1063
+ * remains a rare escape hatch.
1064
+ */
1065
+ /**
1066
+ * Convert a record key to its kebab-case form: camelCase boundaries become
1067
+ * `-`, everything is lowercased. `analyticsDb` → `analytics-db`, `db` → `db`.
1068
+ */
1069
+ function toKebabCase(key) {
1070
+ return key.replaceAll(/(?<lower>[a-z0-9])(?<upper>[A-Z])/gu, "$<lower>-$<upper>").toLowerCase();
1071
+ }
1072
+ //#endregion
1131
1073
  //#region src/lint/rules/a6w-redundant-compose-service.ts
1132
1074
  /**
1133
1075
  * CONVENTIONS A6 (warning) — `composeService:` is the escape hatch for
@@ -1169,7 +1111,7 @@ const a6wRedundantComposeService = {
1169
1111
  }
1170
1112
  };
1171
1113
  //#endregion
1172
- //#region src/core/specification/shared/resolve.ts
1114
+ //#region src/specification/facets/_common/resolve.ts
1173
1115
  /** What makes a directory a project root — a manifest, or a test stack. */
1174
1116
  const ROOT_MARKERS = ["package.json", "docker/compose.test.yaml"];
1175
1117
  /**
@@ -1236,7 +1178,65 @@ const a9wRedundantRoot = {
1236
1178
  }
1237
1179
  };
1238
1180
  //#endregion
1239
- //#region src/core/specification/shared/ground.ts
1181
+ //#region src/lint/rules/a10-duplicate-binding.ts
1182
+ /** The compose service an entry binds to: explicit `composeService`, else the key's kebab form. */
1183
+ function canonicalBinding(entry, key) {
1184
+ const factory = entry.value;
1185
+ if (factory?.type === "CallExpression") {
1186
+ const options = factory.arguments?.[0];
1187
+ if (options?.type === "ObjectExpression") {
1188
+ const explicit = stringValue(findProperty(options, "composeService")?.value);
1189
+ if (explicit !== void 0) return explicit;
1190
+ }
1191
+ }
1192
+ return toKebabCase(key);
1193
+ }
1194
+ /**
1195
+ * CONVENTIONS A10 — within one `services` record, two keys that derive (or
1196
+ * pin via `composeService`) the SAME compose service name are a silent
1197
+ * collision: the record is a map, and the second binding would shadow the
1198
+ * first. Detected on the same kebab derivation the runtime binding uses
1199
+ * (`analyticsDb` and `analytics-db` both → `analytics-db`), and across explicit
1200
+ * `composeService` values.
1201
+ */
1202
+ const a10DuplicateBinding = {
1203
+ create(context) {
1204
+ return { CallExpression(node) {
1205
+ if (specificationMember(node) === void 0) return;
1206
+ for (const argument of node.arguments ?? []) {
1207
+ const record = findProperty(argument, "services")?.value;
1208
+ if (record?.type !== "ObjectExpression") continue;
1209
+ const seen = /* @__PURE__ */ new Map();
1210
+ for (const entry of record.properties ?? []) {
1211
+ const key = propertyKeyName(entry);
1212
+ if (key === void 0) continue;
1213
+ const binding = canonicalBinding(entry, key);
1214
+ const first = seen.get(binding);
1215
+ if (first !== void 0) {
1216
+ context.report({
1217
+ data: {
1218
+ binding,
1219
+ first,
1220
+ key
1221
+ },
1222
+ messageId: "duplicate",
1223
+ node: entry
1224
+ });
1225
+ continue;
1226
+ }
1227
+ seen.set(binding, key);
1228
+ }
1229
+ }
1230
+ } };
1231
+ },
1232
+ meta: {
1233
+ docs: RULE_DOCS["a10-duplicate-binding"],
1234
+ 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)." },
1235
+ type: "problem"
1236
+ }
1237
+ };
1238
+ //#endregion
1239
+ //#region src/specification/facets/_common/ground.ts
1240
1240
  /**
1241
1241
  * The GROUND a row of specs stands on — the naming law of a spec tree.
1242
1242
  *
@@ -1277,7 +1277,7 @@ const RENAMED_GROUND_DIRS = {
1277
1277
  seeds: GROUND_SEEDS
1278
1278
  };
1279
1279
  //#endregion
1280
- //#region src/core/specification/shared/fixtures.ts
1280
+ //#region src/specification/facets/_common/fixtures.ts
1281
1281
  /**
1282
1282
  * Fixture path resolution + copy semantics for the `cli` facet's `.fixture()`.
1283
1283
  *
@@ -1324,7 +1324,7 @@ const b2KnownFixtureMarker = {
1324
1324
  const args = node.arguments ?? [];
1325
1325
  const path = leadingText(args[0]);
1326
1326
  if (path === void 0 || !path.startsWith("$")) return;
1327
- const marker = path.replace(/\/+$/, "").split("/")[0];
1327
+ const marker = path.replace(/\/+$/u, "").split("/")[0] ?? "";
1328
1328
  if (!KNOWN.has(marker)) context.report({
1329
1329
  data: {
1330
1330
  known: [...KNOWN_FIXTURE_MARKERS].join(", "),
@@ -1458,7 +1458,7 @@ const b5AwaitUsing = {
1458
1458
  };
1459
1459
  //#endregion
1460
1460
  //#region src/lint/rules/b6w-redundant-env-url.ts
1461
- const URL_KEY = /^[A-Z0-9]+(?:_[A-Z0-9]+)*_URL$/;
1461
+ const URL_KEY = /^[A-Z0-9]+(?:_[A-Z0-9]+)*_URL$/u;
1462
1462
  /**
1463
1463
  * CONVENTIONS B6 (warning) — in `cli` mode with services, the framework
1464
1464
  * already injects `<KEY>_URL` for every service (plus the unambiguous
@@ -1494,7 +1494,7 @@ const b6wRedundantEnvUrl = {
1494
1494
  //#endregion
1495
1495
  //#region src/lint/rules/b8-kebab-trigger.ts
1496
1496
  /** Stable kebab-case job identifier: `nightly-report`, `send-welcome-emails`. */
1497
- const KEBAB_CASE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
1497
+ const KEBAB_CASE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
1498
1498
  /**
1499
1499
  * CONVENTIONS B8 — a job `name` passed to `.trigger()` is a stable kebab-case
1500
1500
  * identifier; it is a contract between the app and its tests. Only string
@@ -1750,212 +1750,71 @@ const c1DomainStructure = {
1750
1750
  }
1751
1751
  };
1752
1752
  //#endregion
1753
- //#region src/lint/rules/c10-contracts-boundary.ts
1754
- /** A specifier reaching into a provider folder — the internal half of contracts/. */
1755
- const INTERNAL_UNIT = /(?:^|\/)contracts\/(?:anthropic|http|openai)\//u;
1753
+ //#region src/lint/rules/c2-http-only-requests.ts
1754
+ const TEST_FILE$12 = /\.test\.[cm]?[jt]sx?$/u;
1756
1755
  /**
1757
- * CONVENTIONS C10 — the contracts boundary. A feature's `contracts/` folder has
1758
- * a PUBLIC half (`*.contracts.ts` facades: the default export is the world, the
1759
- * named exports are its scenarios) and an INTERNAL half (the `http/`, `openai/`
1760
- * and `anthropic/` unit contracts the facades compose). Outside `contracts/`,
1761
- * importing a unit is an error — a test routes through the facade, so a
1762
- * scenario is named once, next to the world it derives from.
1756
+ * CONVENTIONS C2 — `_requests/` contains only `.http` files (a request file is
1757
+ * a COMPLETE request: method, path, headers, body). Anchored on the feature's
1758
+ * visited test file: when `<feature>/<feature>.test.ts` is linted, its sibling
1759
+ * `_requests/` directory is probed (cached readdir) and any non-`.http` entry
1760
+ * is reported on the test file.
1763
1761
  */
1764
- const c10ContractsBoundary = {
1762
+ const c2HttpOnlyRequests = {
1765
1763
  create(context) {
1766
- const parts = segments(context.physicalFilename);
1767
- if (!parts.includes("specs")) return {};
1768
- if (parts.slice(0, -1).includes("contracts")) return {};
1769
- return importSourceVisitor(({ node, source }) => {
1770
- if (INTERNAL_UNIT.test(source)) context.report({
1771
- data: { source },
1772
- messageId: "internal",
1764
+ const file = context.physicalFilename;
1765
+ if (!TEST_FILE$12.test(file) || !segments(file).includes("specs")) return {};
1766
+ return { Program(node) {
1767
+ const requestsDir = join(dirname(file), GROUND_REQUESTS);
1768
+ for (const entry of listDirectory(requestsDir) ?? []) if (!entry.endsWith(".http")) context.report({
1769
+ data: { entry },
1770
+ messageId: "notHttp",
1773
1771
  node
1774
1772
  });
1775
- });
1773
+ } };
1776
1774
  },
1777
1775
  meta: {
1778
- docs: RULE_DOCS["c10-contracts-boundary"],
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)." },
1776
+ docs: RULE_DOCS["c2-http-only-requests"],
1777
+ messages: { notHttp: "_requests/{{entry}} is not a .http file — _requests/ contains only complete .http request files (C2 — see docs/13-linting.md)." },
1780
1778
  type: "problem"
1781
1779
  }
1782
1780
  };
1783
1781
  //#endregion
1784
- //#region src/lint/rules/c11-contract-data-pairing.ts
1785
- const TEST_FILE$12 = /\.test\.[cm]?[jt]sx?$/;
1786
- /** The provider folders that may carry data files. */
1787
- const PROVIDERS$1 = [
1782
+ //#region src/lint/rules/c4-contract-shape.ts
1783
+ /** The only directories a `contracts/` root may hold — the provider carriers. */
1784
+ const PROVIDERS$1 = /* @__PURE__ */ new Set([
1788
1785
  "anthropic",
1789
1786
  "http",
1790
1787
  "openai"
1791
- ];
1792
- /** Data files: served payloads and matched inputs, both owned by a contract. */
1793
- const DATA_SUFFIXES = [".request.ts", ".response.json"];
1794
- /** The owning contract of a data file: its name up to the FIRST dot, `.ts`. */
1795
- function ownerOf(entry) {
1796
- return `${entry.slice(0, entry.indexOf("."))}.ts`;
1788
+ ]);
1789
+ const TEST_FILE$11 = /\.test\.[cm]?[jt]sx?$/u;
1790
+ /** A file oxlint itself visits — the AST half of the rule reports on those. */
1791
+ const SOURCE_FILE = /\.[cm]?[jt]sx?$/u;
1792
+ /** The public facade of a feature: `contracts/<kebab>.contracts.ts`. */
1793
+ const COMPOSITE_FILE = /^[a-z0-9]+(?:-[a-z0-9]+)*\.contracts\.[cm]?ts$/u;
1794
+ /** An internal unit contract: `contracts/<provider>/<kebab>.ts`. */
1795
+ const UNIT_FILE = /^[a-z0-9]+(?:-[a-z0-9]+)*\.[cm]?ts$/u;
1796
+ /** Matched data next to its contract: `contracts/<provider>/<stem>.request.ts`. */
1797
+ const REQUEST_DATA_FILE = /^[a-z0-9]+(?:-[a-z0-9]+)*\.request\.[cm]?ts$/u;
1798
+ /** The name a specifier/declaration exports under, when it is `default`. */
1799
+ function exportsDefault(node) {
1800
+ return (node.specifiers ?? []).some((specifier) => {
1801
+ const exported = specifier.exported;
1802
+ return exported?.type === "Identifier" && exported.name === "default" || stringValue(exported) === "default";
1803
+ });
1804
+ }
1805
+ /** Is this node a `defineContract(...)` call? */
1806
+ function isDefineContract(node) {
1807
+ const callee = node.callee;
1808
+ return node.type === "CallExpression" && callee?.type === "Identifier" && callee.name === "defineContract";
1809
+ }
1810
+ /** A default export that may legitimately produce a contract (value or factory). */
1811
+ function producesContract(declaration) {
1812
+ if (declaration === void 0) return false;
1813
+ return isDefineContract(declaration) || declaration.type === "ArrowFunctionExpression" || declaration.type === "FunctionDeclaration" || declaration.type === "FunctionExpression" || declaration.type === "Identifier";
1797
1814
  }
1798
1815
  /**
1799
- * CONVENTIONS C11 — data is owned by a contract. Inside a provider folder every
1800
- * `*.response.json` (served payload) and `*.request.ts` (matched prompt/body)
1801
- * pairs with the sibling `<stem>.ts` that serves it, where the stem is the name
1802
- * up to the FIRST dot: `events.fr.response.json` belongs to `events.ts`. A data
1803
- * file with no owner is dead weight no test can reach — reported on the feature's
1804
- * test file, since oxlint never visits a `.json`.
1805
- */
1806
- const c11ContractDataPairing = {
1807
- create(context) {
1808
- const file = context.physicalFilename;
1809
- if (!TEST_FILE$12.test(file) || !segments(file).includes("specs")) return {};
1810
- return { Program(node) {
1811
- const contractsDir = join(dirname(file), "contracts");
1812
- for (const provider of PROVIDERS$1) {
1813
- const entries = listDirectory(join(contractsDir, provider));
1814
- if (entries === null) continue;
1815
- const present = new Set(entries);
1816
- for (const entry of entries) {
1817
- if (!DATA_SUFFIXES.some((suffix) => entry.endsWith(suffix))) continue;
1818
- const owner = ownerOf(entry);
1819
- if (!present.has(owner)) context.report({
1820
- data: {
1821
- entry,
1822
- owner,
1823
- provider
1824
- },
1825
- messageId: "orphan",
1826
- node
1827
- });
1828
- }
1829
- }
1830
- } };
1831
- },
1832
- meta: {
1833
- docs: RULE_DOCS["c11-contract-data-pairing"],
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)." },
1835
- type: "problem"
1836
- }
1837
- };
1838
- //#endregion
1839
- //#region src/lint/rules/c13-underscored-ground.ts
1840
- const TEST_FILE$11 = /\.test\.[cm]?[jt]sx?$/;
1841
- /**
1842
- * CONVENTIONS C13 — the ground of a spec carries a leading underscore. A
1843
- * directory named `fixtures/`, `expected/`, `requests/` or `seeds/` under
1844
- * `specs/` is a pre-14 name: since `@jterrazz/test` 14.0.0 the four are
1845
- * `_fixtures/`, `_expected/`, `_requests/` and `_seeds/`, with no compatibility
1846
- * fallback.
1847
- *
1848
- * The catch this rule exists for: a consumer that upgrades and keeps the old
1849
- * names loses nothing loudly. Every resolver — `.fixture()`, `.seed()`,
1850
- * `.request()`, `toMatch()` — looks under the underscored name, finds nothing,
1851
- * and the tree becomes invisible rather than wrong. The failure that follows is
1852
- * "fixture does not exist" on every spec at once, one step removed from the
1853
- * cause; this rule states the cause and the rename.
1854
- *
1855
- * Anchored on the feature's visited test file, exactly as C2/C7/C11 are: the
1856
- * spec's own sibling directories are probed, plus the `$FIXTURES` pool at the
1857
- * nearest specs root. A DOMAIN folder named after the ground it holds
1858
- * (`specs/api/requests/_requests/`) is never a probe target — only what sits
1859
- * beside a spec is.
1860
- */
1861
- const c13UnderscoredGround = {
1862
- create(context) {
1863
- const file = context.physicalFilename;
1864
- const anchor = specsAnchor(file);
1865
- if (!TEST_FILE$11.test(file) || anchor === void 0) return {};
1866
- return { Program(node) {
1867
- const seen = /* @__PURE__ */ new Set();
1868
- const probe = (parent, legacy) => {
1869
- const path = join(parent, legacy);
1870
- if (seen.has(path) || !isDirectory(path)) return;
1871
- seen.add(path);
1872
- context.report({
1873
- data: {
1874
- ground: RENAMED_GROUND_DIRS[legacy],
1875
- legacy
1876
- },
1877
- messageId: "notUnderscored",
1878
- node
1879
- });
1880
- };
1881
- for (const legacy of Object.keys(RENAMED_GROUND_DIRS)) probe(dirname(file), legacy);
1882
- probe(anchor.directory, "fixtures");
1883
- } };
1884
- },
1885
- meta: {
1886
- docs: RULE_DOCS["c13-underscored-ground"],
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)." },
1888
- type: "problem"
1889
- }
1890
- };
1891
- //#endregion
1892
- //#region src/lint/rules/c2-http-only-requests.ts
1893
- const TEST_FILE$10 = /\.test\.[cm]?[jt]sx?$/;
1894
- /**
1895
- * CONVENTIONS C2 — `_requests/` contains only `.http` files (a request file is
1896
- * a COMPLETE request: method, path, headers, body). Anchored on the feature's
1897
- * visited test file: when `<feature>/<feature>.test.ts` is linted, its sibling
1898
- * `_requests/` directory is probed (cached readdir) and any non-`.http` entry
1899
- * is reported on the test file.
1900
- */
1901
- const c2HttpOnlyRequests = {
1902
- create(context) {
1903
- const file = context.physicalFilename;
1904
- if (!TEST_FILE$10.test(file) || !segments(file).includes("specs")) return {};
1905
- return { Program(node) {
1906
- const requestsDir = join(dirname(file), GROUND_REQUESTS);
1907
- for (const entry of listDirectory(requestsDir) ?? []) if (!entry.endsWith(".http")) context.report({
1908
- data: { entry },
1909
- messageId: "notHttp",
1910
- node
1911
- });
1912
- } };
1913
- },
1914
- meta: {
1915
- docs: RULE_DOCS["c2-http-only-requests"],
1916
- messages: { notHttp: "_requests/{{entry}} is not a .http file — _requests/ contains only complete .http request files (C2 — see docs/13-linting.md)." },
1917
- type: "problem"
1918
- }
1919
- };
1920
- //#endregion
1921
- //#region src/lint/rules/c4-contract-shape.ts
1922
- /** The only directories a `contracts/` root may hold — the provider carriers. */
1923
- const PROVIDERS = /* @__PURE__ */ new Set([
1924
- "anthropic",
1925
- "http",
1926
- "openai"
1927
- ]);
1928
- const TEST_FILE$9 = /\.test\.[cm]?[jt]sx?$/;
1929
- /** A file oxlint itself visits — the AST half of the rule reports on those. */
1930
- const SOURCE_FILE = /\.[cm]?[jt]sx?$/;
1931
- /** The public facade of a feature: `contracts/<kebab>.contracts.ts`. */
1932
- const COMPOSITE_FILE = /^[a-z0-9]+(?:-[a-z0-9]+)*\.contracts\.[cm]?ts$/u;
1933
- /** An internal unit contract: `contracts/<provider>/<kebab>.ts`. */
1934
- const UNIT_FILE = /^[a-z0-9]+(?:-[a-z0-9]+)*\.[cm]?ts$/u;
1935
- /** Matched data next to its contract: `contracts/<provider>/<stem>.request.ts`. */
1936
- const REQUEST_DATA_FILE = /^[a-z0-9]+(?:-[a-z0-9]+)*\.request\.[cm]?ts$/u;
1937
- /** Served data next to its contract: `contracts/<provider>/<stem>[.<qualifier>].response.json`. */
1938
- const RESPONSE_DATA_FILE = /\.response\.json$/;
1939
- /** The name a specifier/declaration exports under, when it is `default`. */
1940
- function exportsDefault(node) {
1941
- return (node.specifiers ?? []).some((specifier) => {
1942
- const exported = specifier.exported;
1943
- return exported?.type === "Identifier" && exported.name === "default" || stringValue(exported) === "default";
1944
- });
1945
- }
1946
- /** Is this node a `defineContract(...)` call? */
1947
- function isDefineContract(node) {
1948
- const callee = node.callee;
1949
- return node.type === "CallExpression" && callee?.type === "Identifier" && callee.name === "defineContract";
1950
- }
1951
- /** A default export that may legitimately produce a contract (value or factory). */
1952
- function producesContract(declaration) {
1953
- if (declaration === void 0) return false;
1954
- return isDefineContract(declaration) || declaration.type === "ArrowFunctionExpression" || declaration.type === "FunctionDeclaration" || declaration.type === "FunctionExpression" || declaration.type === "Identifier";
1955
- }
1956
- /**
1957
- * The provider a `request:` value implies, when the builder is written as
1958
- * `<provider>.<verb>(…)` — the only statically decidable form.
1816
+ * The provider a `request:` value implies, when the builder is written as
1817
+ * `<provider>.<verb>(…)` — the only statically decidable form.
1959
1818
  */
1960
1819
  function requestProvider(request) {
1961
1820
  if (request?.type !== "CallExpression") return;
@@ -1983,7 +1842,7 @@ function checkLayout(context, node, contractsDir) {
1983
1842
  });
1984
1843
  continue;
1985
1844
  }
1986
- if (!PROVIDERS.has(entry)) {
1845
+ if (!PROVIDERS$1.has(entry)) {
1987
1846
  context.report({
1988
1847
  data: { entry },
1989
1848
  messageId: "badProviderDir",
@@ -1999,7 +1858,7 @@ function checkLayout(context, node, contractsDir) {
1999
1858
  messageId: "providerSubfolder",
2000
1859
  node
2001
1860
  });
2002
- else if (!/\.[cm]?ts$/u.test(child) && !RESPONSE_DATA_FILE.test(child)) context.report({
1861
+ else if (!/\.[cm]?ts$/u.test(child) && !child.endsWith(".response.json")) context.report({
2003
1862
  data: {
2004
1863
  child,
2005
1864
  provider: entry
@@ -2068,7 +1927,7 @@ function checkUnit(context, program, base, provider) {
2068
1927
  if (isDefineContract(node)) {
2069
1928
  const argument = node.arguments?.[0];
2070
1929
  const declared = requestProvider(argument === void 0 ? void 0 : findProperty(argument, "request")?.value);
2071
- if (declared !== void 0 && PROVIDERS.has(declared) && declared !== provider) context.report({
1930
+ if (declared !== void 0 && PROVIDERS$1.has(declared) && declared !== provider) context.report({
2072
1931
  data: {
2073
1932
  declared,
2074
1933
  provider
@@ -2110,7 +1969,7 @@ const c4ContractShape = {
2110
1969
  if (!parts.includes("specs")) return {};
2111
1970
  const contractsIndex = parts.lastIndexOf("contracts");
2112
1971
  if (contractsIndex === -1) {
2113
- if (!TEST_FILE$9.test(file)) return {};
1972
+ if (!TEST_FILE$11.test(file)) return {};
2114
1973
  return { Program(node) {
2115
1974
  checkLayout(context, node, join(dirname(file), "contracts"));
2116
1975
  } };
@@ -2121,8 +1980,8 @@ const c4ContractShape = {
2121
1980
  checkComposite(context, node, base);
2122
1981
  } };
2123
1982
  if (depth === 2) {
2124
- const provider = parts[contractsIndex + 1];
2125
- return PROVIDERS.has(provider) ? { Program(node) {
1983
+ const provider = parts[contractsIndex + 1] ?? "";
1984
+ return PROVIDERS$1.has(provider) ? { Program(node) {
2126
1985
  checkUnit(context, node, base, provider);
2127
1986
  } } : {};
2128
1987
  }
@@ -2155,7 +2014,7 @@ const c4ContractShape = {
2155
2014
  };
2156
2015
  //#endregion
2157
2016
  //#region src/lint/rules/c6-tomatch-extension.ts
2158
- const TEST_FILE$8 = /\.test\.[cm]?[jt]sx?$/;
2017
+ const TEST_FILE$10 = /\.test\.[cm]?[jt]sx?$/u;
2159
2018
  /** Does the expect(…) subject chain contain a `.directory(…)` call? */
2160
2019
  function subjectIsDirectory(toMatchCallee) {
2161
2020
  const expectCall = toMatchCallee.object;
@@ -2180,7 +2039,7 @@ function subjectIsDirectory(toMatchCallee) {
2180
2039
  const c6ToMatchExtension = {
2181
2040
  create(context) {
2182
2041
  const file = context.physicalFilename;
2183
- if (!TEST_FILE$8.test(file) || !segments(file).includes("specs")) return {};
2042
+ if (!TEST_FILE$10.test(file) || !segments(file).includes("specs")) return {};
2184
2043
  return { CallExpression(node) {
2185
2044
  const callee = node.callee;
2186
2045
  if (callee === void 0 || memberPropertyName(callee) !== "toMatch") return;
@@ -2205,7 +2064,7 @@ const c6ToMatchExtension = {
2205
2064
  };
2206
2065
  //#endregion
2207
2066
  //#region src/lint/rules/c7-seeds-sql-only.ts
2208
- const TEST_FILE$7 = /\.test\.[cm]?[jt]sx?$/;
2067
+ const TEST_FILE$9 = /\.test\.[cm]?[jt]sx?$/u;
2209
2068
  /**
2210
2069
  * CONVENTIONS C7 — `.seed()` carries DATABASE state only: `_seeds/` contains
2211
2070
  * nothing but `*.sql` fragments. File state goes through `.fixture()`; there
@@ -2215,7 +2074,7 @@ const TEST_FILE$7 = /\.test\.[cm]?[jt]sx?$/;
2215
2074
  const c7SeedsSqlOnly = {
2216
2075
  create(context) {
2217
2076
  const file = context.physicalFilename;
2218
- if (!TEST_FILE$7.test(file) || !segments(file).includes("specs")) return {};
2077
+ if (!TEST_FILE$9.test(file) || !segments(file).includes("specs")) return {};
2219
2078
  return { Program(node) {
2220
2079
  const seedsDir = join(dirname(file), GROUND_SEEDS);
2221
2080
  for (const entry of listDirectory(seedsDir) ?? []) if (!entry.endsWith(".sql")) context.report({
@@ -2233,7 +2092,7 @@ const c7SeedsSqlOnly = {
2233
2092
  };
2234
2093
  //#endregion
2235
2094
  //#region src/lint/rules/c8-referenced-fixture-exists.ts
2236
- const TEST_FILE$6 = /\.test\.[cm]?[jt]sx?$/;
2095
+ const TEST_FILE$8 = /\.test\.[cm]?[jt]sx?$/u;
2237
2096
  /** The ground directory each fixture-referencing verb reads from. */
2238
2097
  const VERB_ROOTS = {
2239
2098
  request: GROUND_REQUESTS,
@@ -2266,7 +2125,7 @@ const c8ReferencedFixtureExists = {
2266
2125
  create(context) {
2267
2126
  const file = context.physicalFilename;
2268
2127
  const anchor = specsAnchor(file);
2269
- if (!TEST_FILE$6.test(file) || anchor === void 0) return {};
2128
+ if (!TEST_FILE$8.test(file) || anchor === void 0) return {};
2270
2129
  const featureDir = dirname(file);
2271
2130
  return { CallExpression(node) {
2272
2131
  const callee = node.callee;
@@ -2276,7 +2135,7 @@ const c8ReferencedFixtureExists = {
2276
2135
  if (verb === "fixture") {
2277
2136
  const value = stringValue(args[0]);
2278
2137
  if (value === void 0 || !looksLikePath(value)) return;
2279
- const target = resolveFixture(value.replace(/\/+$/, ""), featureDir, anchor.directory);
2138
+ const target = resolveFixture(value.replace(/\/+$/u, ""), featureDir, anchor.directory);
2280
2139
  if (target !== void 0 && !isFile(target) && !isDirectory(target)) context.report({
2281
2140
  data: {
2282
2141
  path: value,
@@ -2291,7 +2150,7 @@ const c8ReferencedFixtureExists = {
2291
2150
  if (root === void 0) return;
2292
2151
  const value = stringValue(args[0]);
2293
2152
  if (value === void 0 || !looksLikePath(value)) return;
2294
- const target = join(featureDir, root, value.replace(/\/+$/, ""));
2153
+ const target = join(featureDir, root, value.replace(/\/+$/u, ""));
2295
2154
  if (!(verb === "toMatch" ? isFile(target) || isDirectory(target) : isFile(target))) context.report({
2296
2155
  data: {
2297
2156
  path: `${root}/${value}`,
@@ -2309,231 +2168,142 @@ const c8ReferencedFixtureExists = {
2309
2168
  }
2310
2169
  };
2311
2170
  //#endregion
2312
- //#region src/lint/rules/d12w-response-body-probe.ts
2313
- /** A test reading the response body this many times (or more) is a cluster. */
2314
- const DEFAULT_THRESHOLD = 3;
2315
- /** Is `node` the `x.response.body` member access itself? */
2316
- function isResponseBody(node) {
2317
- if (node.type !== "MemberExpression" || memberPropertyName(node) !== "body") return false;
2318
- const object = node.object;
2319
- return object !== void 0 && memberPropertyName(object) === "response";
2320
- }
2321
- /** Peel `await`, `as T`, and `!` off an initializer to reach its core expression. */
2322
- function unwrap(node) {
2323
- let current = node ?? void 0;
2324
- while (current != null) if (current.type === "AwaitExpression") current = current.argument ?? void 0;
2325
- else if (current.type === "TSAsExpression" || current.type === "TSNonNullExpression") current = current.expression ?? void 0;
2326
- else if (current.type === "ParenthesizedExpression") current = current.expression ?? void 0;
2327
- else return current;
2328
- }
2171
+ //#region src/lint/rules/c10-contracts-boundary.ts
2172
+ /** A specifier reaching into a provider folder — the internal half of contracts/. */
2173
+ const INTERNAL_UNIT = /(?:^|\/)contracts\/(?:anthropic|http|openai)\//u;
2329
2174
  /**
2330
- * CONVENTIONS D12 (warning) — a full-response golden is the norm for API
2331
- * assertions (the D11 boundary, mechanized). A test that accumulates a CLUSTER
2332
- * of raw body probes — reading `.response.body` (or a variable cast from it)
2333
- * `threshold`+ times in one test callback — should collapse into one golden:
2334
- * `expect(result.response).toMatch('case.http')`. One or two probes stay silent
2335
- * (a legitimate scalpel: absence, a single-field delta, third-party output).
2336
- *
2337
- * A probe is a body-rooted read: a direct `.response.body` use, or a field
2338
- * access on the local binding a `const body = result.response.body as {…}`
2339
- * introduces. The count is the same threshold the golden transformation applies.
2175
+ * CONVENTIONS C10 — the contracts boundary. A feature's `contracts/` folder has
2176
+ * a PUBLIC half (`*.contracts.ts` facades: the default export is the world, the
2177
+ * named exports are its scenarios) and an INTERNAL half (the `http/`, `openai/`
2178
+ * and `anthropic/` unit contracts the facades compose). Outside `contracts/`,
2179
+ * importing a unit is an error — a test routes through the facade, so a
2180
+ * scenario is named once, next to the world it derives from.
2340
2181
  */
2341
- const d12wResponseBodyProbe = {
2182
+ const c10ContractsBoundary = {
2342
2183
  create(context) {
2343
- if (!segments(context.filename).includes("specs")) return {};
2344
- const threshold = context.options[0]?.threshold ?? DEFAULT_THRESHOLD;
2345
- return { CallExpression(node) {
2346
- if (!isTestCallee(node.callee)) return;
2347
- const callback = findTestCallback(node.arguments ?? []);
2348
- if (callback === void 0) return;
2349
- const aliases = /* @__PURE__ */ new Set();
2350
- let responseBodyNodes = 0;
2351
- walk(callback, (inner) => {
2352
- if (isResponseBody(inner)) responseBodyNodes++;
2353
- if (inner.type === "VariableDeclarator") {
2354
- const init = unwrap(inner.init);
2355
- const id = inner.id;
2356
- if (init !== void 0 && isResponseBody(init) && id?.type === "Identifier") aliases.add(id.name);
2357
- }
2358
- });
2359
- let aliasReads = 0;
2360
- walk(callback, (inner) => {
2361
- if (inner.type !== "MemberExpression") return;
2362
- const object = inner.object;
2363
- if (object?.type === "Identifier" && aliases.has(object.name)) aliasReads++;
2364
- });
2365
- const probes = responseBodyNodes - aliases.size + aliasReads;
2366
- if (probes >= threshold) context.report({
2367
- data: { count: String(probes) },
2368
- messageId: "bodyProbeCluster",
2184
+ const parts = segments(context.physicalFilename);
2185
+ if (!parts.includes("specs")) return {};
2186
+ if (parts.slice(0, -1).includes("contracts")) return {};
2187
+ return importSourceVisitor(({ node, source }) => {
2188
+ if (INTERNAL_UNIT.test(source)) context.report({
2189
+ data: { source },
2190
+ messageId: "internal",
2369
2191
  node
2370
2192
  });
2371
- } };
2193
+ });
2372
2194
  },
2373
2195
  meta: {
2374
- docs: RULE_DOCS["d12w-response-body-probe"],
2375
- defaultOptions: [{ threshold: DEFAULT_THRESHOLD }],
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)." },
2377
- schema: [{
2378
- additionalProperties: false,
2379
- properties: { threshold: {
2380
- minimum: 1,
2381
- type: "integer"
2382
- } },
2383
- type: "object"
2384
- }],
2385
- type: "suggestion"
2196
+ docs: RULE_DOCS["c10-contracts-boundary"],
2197
+ 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)." },
2198
+ type: "problem"
2386
2199
  }
2387
2200
  };
2388
2201
  //#endregion
2389
- //#region src/lint/rules/d13w-unfrozen-negative-fixture.ts
2390
- /**
2391
- * Does `arguments[1]` of a `toMatch(name, options)` call carry `{ frozen: true }`?
2392
- * A frozen fixture opts out of update-mode rewriting, so it is exempt.
2393
- */
2394
- function hasFrozenOption(args) {
2395
- const options = args[1];
2396
- if (options === void 0 || options.type !== "ObjectExpression") return false;
2397
- const value = findProperty(options, "frozen")?.value;
2398
- return value?.type === "Literal" && value.value === true;
2202
+ //#region src/lint/rules/c11-contract-data-pairing.ts
2203
+ const TEST_FILE$7 = /\.test\.[cm]?[jt]sx?$/u;
2204
+ /** The provider folders that may carry data files. */
2205
+ const PROVIDERS = [
2206
+ "anthropic",
2207
+ "http",
2208
+ "openai"
2209
+ ];
2210
+ /** Data files: served payloads and matched inputs, both owned by a contract. */
2211
+ const DATA_SUFFIXES = [".request.ts", ".response.json"];
2212
+ /** The owning contract of a data file: its name up to the FIRST dot, `.ts`. */
2213
+ function ownerOf(entry) {
2214
+ return `${entry.slice(0, entry.indexOf("."))}.ts`;
2399
2215
  }
2400
2216
  /**
2401
- * Is this `toMatch(…)` call nested inside a WRAPPING `expect(…)` — the shape of a
2402
- * negative assertion (`expect(() => …toMatch(…)).toThrow()` or
2403
- * `expect(…toMatch(…)).rejects.toThrow()`)? The subject's own `expect(x)` is the
2404
- * member OBJECT of `.toMatch` (a descendant), never an ancestor, so a positive
2405
- * `expect(x).toMatch('f')` never trips this. The walk stays inside the current
2406
- * expression: it stops at the first statement boundary or non-`expect` call, so
2407
- * a `toMatch` merely written inside `inUpdateMode(() => …)` is NOT flagged.
2217
+ * CONVENTIONS C11 — data is owned by a contract. Inside a provider folder every
2218
+ * `*.response.json` (served payload) and `*.request.ts` (matched prompt/body)
2219
+ * pairs with the sibling `<stem>.ts` that serves it, where the stem is the name
2220
+ * up to the FIRST dot: `events.fr.response.json` belongs to `events.ts`. A data
2221
+ * file with no owner is dead weight no test can reach — reported on the feature's
2222
+ * test file, since oxlint never visits a `.json`.
2408
2223
  */
2409
- const TRANSPARENT_ANCESTORS = /* @__PURE__ */ new Set([
2410
- "ArrowFunctionExpression",
2411
- "AwaitExpression",
2412
- "ChainExpression",
2413
- "MemberExpression",
2414
- "ParenthesizedExpression",
2415
- "TSAsExpression",
2416
- "TSNonNullExpression"
2417
- ]);
2418
- function isWrappedInExpect(node) {
2419
- let current = node.parent;
2420
- while (current !== void 0) {
2421
- if (current.type === "CallExpression") {
2422
- const callee = current.callee;
2423
- return callee?.type === "Identifier" && callee.name === "expect";
2424
- }
2425
- if (!TRANSPARENT_ANCESTORS.has(current.type)) return false;
2426
- current = current.parent;
2427
- }
2428
- return false;
2429
- }
2430
- /**
2431
- * CONVENTIONS D13 (warning) — a `toMatch` whose failure is the behaviour under
2432
- * test (wrapped in `expect(() => …).toThrow()` or `expect(…).rejects.toThrow()`)
2433
- * asserts a DELIBERATELY-WRONG or MISSING fixture. Under `TEST_UPDATE=1` an
2434
- * unfrozen fixture is silently rewritten with the actual output — the matcher
2435
- * writes instead of throwing — which destroys the negative case (the assertion
2436
- * then no longer throws). Pass `{ frozen: true }` so the fixture is never
2437
- * rewritten and the mismatch/error still throws in update mode.
2438
- *
2439
- * The structural signal is precise for the wrapped forms; a `toMatch` routed
2440
- * through a helper that owns the try/catch (`catchMessage(() => …toMatch(…))`)
2441
- * is out of static reach — see the D13 process note for that residue.
2442
- */
2443
- const d13wUnfrozenNegativeFixture = {
2224
+ const c11ContractDataPairing = {
2444
2225
  create(context) {
2445
- if (!segments(context.filename).includes("specs")) return {};
2446
- return { CallExpression(node) {
2447
- const callee = node.callee;
2448
- if (callee === void 0 || memberPropertyName(callee) !== "toMatch") return;
2449
- if (!isWrappedInExpect(node)) return;
2450
- if (hasFrozenOption(node.arguments ?? [])) return;
2451
- context.report({
2452
- messageId: "unfrozenNegativeFixture",
2453
- node
2454
- });
2226
+ const file = context.physicalFilename;
2227
+ if (!TEST_FILE$7.test(file) || !segments(file).includes("specs")) return {};
2228
+ return { Program(node) {
2229
+ const contractsDir = join(dirname(file), "contracts");
2230
+ for (const provider of PROVIDERS) {
2231
+ const entries = listDirectory(join(contractsDir, provider));
2232
+ if (entries === null) continue;
2233
+ const present = new Set(entries);
2234
+ for (const entry of entries) {
2235
+ if (!DATA_SUFFIXES.some((suffix) => entry.endsWith(suffix))) continue;
2236
+ const owner = ownerOf(entry);
2237
+ if (!present.has(owner)) context.report({
2238
+ data: {
2239
+ entry,
2240
+ owner,
2241
+ provider
2242
+ },
2243
+ messageId: "orphan",
2244
+ node
2245
+ });
2246
+ }
2247
+ }
2455
2248
  } };
2456
2249
  },
2457
2250
  meta: {
2458
- docs: RULE_DOCS["d13w-unfrozen-negative-fixture"],
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)." },
2460
- type: "suggestion"
2251
+ docs: RULE_DOCS["c11-contract-data-pairing"],
2252
+ 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)." },
2253
+ type: "problem"
2461
2254
  }
2462
2255
  };
2463
2256
  //#endregion
2464
- //#region src/lint/rules/d15w-status-only-probe.ts
2465
- /** An HTTP status code lives in this range — the numeric-literal gate. */
2466
- const MIN_STATUS = 100;
2467
- const MAX_STATUS = 599;
2468
- /** Matchers that pin a single value — the shape of a status probe. */
2469
- const EQUALITY_MATCHERS = /* @__PURE__ */ new Set(["toBe", "toEqual"]);
2470
- /** Is `node` an `expect(…)` call (bare `expect` identifier callee)? */
2471
- function isExpectCall(node) {
2472
- if (node?.type !== "CallExpression") return false;
2473
- const callee = node.callee;
2474
- return callee?.type === "Identifier" && callee.name === "expect";
2475
- }
2476
- /** Is `node` a numeric literal in the HTTP status range (100–599)? */
2477
- function isStatusLiteral(node) {
2478
- if (node?.type !== "Literal" || typeof node.value !== "number") return false;
2479
- const value = node.value;
2480
- return Number.isInteger(value) && value >= MIN_STATUS && value <= MAX_STATUS;
2481
- }
2482
- /**
2483
- * Is `node` a status probe — `expect(X.status).toBe(N)` / `.toEqual(N)` where
2484
- * `X.status` is any `.status` member access and `N` is a numeric literal in the
2485
- * HTTP status range? The numeric-literal gate keeps container-status STRINGS
2486
- * (`expect(neo.status).toBe('running')`) out.
2487
- */
2488
- function isStatusProbe(node) {
2489
- if (node.type !== "CallExpression") return false;
2490
- const callee = node.callee;
2491
- if (callee?.type !== "MemberExpression") return false;
2492
- const matcher = memberPropertyName(callee);
2493
- if (matcher === void 0 || !EQUALITY_MATCHERS.has(matcher)) return false;
2494
- const expectCall = callee.object;
2495
- if (!isExpectCall(expectCall)) return false;
2496
- const subject = (expectCall?.arguments ?? [])[0];
2497
- if (subject?.type !== "MemberExpression" || memberPropertyName(subject) !== "status") return false;
2498
- const expected = (node.arguments ?? [])[0];
2499
- return isStatusLiteral(expected);
2500
- }
2257
+ //#region src/lint/rules/c13-underscored-ground.ts
2258
+ const TEST_FILE$6 = /\.test\.[cm]?[jt]sx?$/u;
2501
2259
  /**
2502
- * CONVENTIONS D11/D15 (warning) — a spec test whose ONLY assertions are HTTP
2503
- * status probes (`expect(X.status).toBe(N)` / `.toEqual(N)`, N a numeric literal
2504
- * 100–599) throws away the full-response golden. Pin the whole response instead:
2505
- * `expect(result.response).toMatch('case.http')`.
2260
+ * CONVENTIONS C13 — the ground of a spec carries a leading underscore. A
2261
+ * directory named `fixtures/`, `expected/`, `requests/` or `seeds/` under
2262
+ * `specs/` is a pre-14 name: since `@jterrazz/test` 14.0.0 the four are
2263
+ * `_fixtures/`, `_expected/`, `_requests/` and `_seeds/`, with no compatibility
2264
+ * fallback.
2506
2265
  *
2507
- * This complements d12w (which needs a CLUSTER of ≥3 body probes): the lone
2508
- * `expect(result.status).toBe(422)` as a test's sole assertion falls below that
2509
- * threshold yet still hides everything the response carries. The rule fires only
2510
- * when EVERY `expect(…)` in the callback is a status probe — a status probe
2511
- * sitting NEXT TO a real assertion (a golden, `toMatchRows`, `toContain`, …) is
2512
- * the legitimate scalpel and stays silent.
2266
+ * The catch this rule exists for: a consumer that upgrades and keeps the old
2267
+ * names loses nothing loudly. Every resolver — `.fixture()`, `.seed()`,
2268
+ * `.request()`, `toMatch()` — looks under the underscored name, finds nothing,
2269
+ * and the tree becomes invisible rather than wrong. The failure that follows is
2270
+ * "fixture does not exist" on every spec at once, one step removed from the
2271
+ * cause; this rule states the cause and the rename.
2272
+ *
2273
+ * Anchored on the feature's visited test file, exactly as C2/C7/C11 are: the
2274
+ * spec's own sibling directories are probed, plus the `$FIXTURES` pool at the
2275
+ * nearest specs root. A DOMAIN folder named after the ground it holds
2276
+ * (`specs/api/requests/_requests/`) is never a probe target — only what sits
2277
+ * beside a spec is.
2513
2278
  */
2514
- const d15wStatusOnlyProbe = {
2279
+ const c13UnderscoredGround = {
2515
2280
  create(context) {
2516
- if (!segments(context.filename).includes("specs")) return {};
2517
- return { CallExpression(node) {
2518
- if (!isTestCallee(node.callee)) return;
2519
- const callback = findTestCallback(node.arguments ?? []);
2520
- if (callback === void 0) return;
2521
- let expectCalls = 0;
2522
- let statusProbes = 0;
2523
- walk(callback, (inner) => {
2524
- if (isExpectCall(inner)) expectCalls++;
2525
- if (isStatusProbe(inner)) statusProbes++;
2526
- });
2527
- if (expectCalls >= 1 && statusProbes === expectCalls) context.report({
2528
- messageId: "statusOnlyProbe",
2529
- node
2530
- });
2281
+ const file = context.physicalFilename;
2282
+ const anchor = specsAnchor(file);
2283
+ if (!TEST_FILE$6.test(file) || anchor === void 0) return {};
2284
+ return { Program(node) {
2285
+ const seen = /* @__PURE__ */ new Set();
2286
+ const probe = (parent, legacy) => {
2287
+ const path = join(parent, legacy);
2288
+ if (seen.has(path) || !isDirectory(path)) return;
2289
+ seen.add(path);
2290
+ context.report({
2291
+ data: {
2292
+ ground: RENAMED_GROUND_DIRS[legacy] ?? legacy,
2293
+ legacy
2294
+ },
2295
+ messageId: "notUnderscored",
2296
+ node
2297
+ });
2298
+ };
2299
+ for (const legacy of Object.keys(RENAMED_GROUND_DIRS)) probe(dirname(file), legacy);
2300
+ probe(anchor.directory, "fixtures");
2531
2301
  } };
2532
2302
  },
2533
2303
  meta: {
2534
- docs: RULE_DOCS["d15w-status-only-probe"],
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)." },
2536
- type: "suggestion"
2304
+ docs: RULE_DOCS["c13-underscored-ground"],
2305
+ 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)." },
2306
+ type: "problem"
2537
2307
  }
2538
2308
  };
2539
2309
  //#endregion
@@ -2669,7 +2439,7 @@ const KNOWN_TOKEN_LITERAL = new RegExp(`^\\{\\{(?:${[
2669
2439
  "url",
2670
2440
  "uuid",
2671
2441
  "workdir"
2672
- ].join("|")})(?:#[\\w.-]+)?\\}\\}$`);
2442
+ ].join("|")})(?:#[\\w.-]+)?\\}\\}$`, "u");
2673
2443
  /**
2674
2444
  * Is this function a pure chain of `.replace(…, '{{token}}')` normalisations?
2675
2445
  * Accepts an arrow with an expression body or a single-return body whose
@@ -2681,8 +2451,9 @@ function isTokenEquivalentTransform(fn) {
2681
2451
  let expression = fn.body;
2682
2452
  if (expression?.type === "BlockStatement") {
2683
2453
  const statements = expression.body ?? [];
2684
- if (statements.length !== 1 || statements[0].type !== "ReturnStatement") return false;
2685
- expression = statements[0].argument;
2454
+ const [only] = statements;
2455
+ if (statements.length !== 1 || only?.type !== "ReturnStatement") return false;
2456
+ expression = only.argument;
2686
2457
  }
2687
2458
  let sawReplace = false;
2688
2459
  while (expression?.type === "CallExpression") {
@@ -2758,9 +2529,9 @@ const d8wTextBypass = {
2758
2529
  };
2759
2530
  //#endregion
2760
2531
  //#region src/lint/rules/d9w-single-use-ref.ts
2761
- const TEST_FILE$5 = /\.test\.[cm]?[jt]sx?$/;
2532
+ const TEST_FILE$5 = /\.test\.[cm]?[jt]sx?$/u;
2762
2533
  /** A captured-ref token: `{{kind#ref}}`. Captures the ref name. */
2763
- const REF_TOKEN = /\{\{[A-Za-z][A-Za-z0-9]*#(?<ref>[\w.-]+)\}\}/g;
2534
+ const REF_TOKEN = /\{\{[A-Za-z][A-Za-z0-9]*#(?<ref>[\w.-]+)\}\}/gu;
2764
2535
  /** Is this call `match.ref(...)`? */
2765
2536
  function isMatchRef(node) {
2766
2537
  const callee = node.callee;
@@ -2815,7 +2586,7 @@ const d9wSingleUseRef = {
2815
2586
  if (memberPropertyName(node.callee) === "toMatch") {
2816
2587
  const name = stringValue(node.arguments?.[0]);
2817
2588
  if (name !== void 0) {
2818
- const target = join(featureDir, GROUND_EXPECTED, name.replace(/\/+$/, ""));
2589
+ const target = join(featureDir, GROUND_EXPECTED, name.replace(/\/+$/u, ""));
2819
2590
  for (const ref of refsInFixture(target)) {
2820
2591
  bump(ref);
2821
2592
  if (!fixtureNodes.has(ref)) fixtureNodes.set(ref, node);
@@ -2840,6 +2611,234 @@ const d9wSingleUseRef = {
2840
2611
  }
2841
2612
  };
2842
2613
  //#endregion
2614
+ //#region src/lint/rules/d12w-response-body-probe.ts
2615
+ /** A test reading the response body this many times (or more) is a cluster. */
2616
+ const DEFAULT_THRESHOLD = 3;
2617
+ /** Is `node` the `x.response.body` member access itself? */
2618
+ function isResponseBody(node) {
2619
+ if (node.type !== "MemberExpression" || memberPropertyName(node) !== "body") return false;
2620
+ const object = node.object;
2621
+ return object !== void 0 && memberPropertyName(object) === "response";
2622
+ }
2623
+ /** Peel `await`, `as T`, and `!` off an initializer to reach its core expression. */
2624
+ function unwrap(node) {
2625
+ let current = node ?? void 0;
2626
+ while (current != null) if (current.type === "AwaitExpression") current = current.argument ?? void 0;
2627
+ else if (current.type === "TSAsExpression" || current.type === "TSNonNullExpression") current = current.expression ?? void 0;
2628
+ else if (current.type === "ParenthesizedExpression") current = current.expression ?? void 0;
2629
+ else return current;
2630
+ }
2631
+ /**
2632
+ * CONVENTIONS D12 (warning) — a full-response golden is the norm for API
2633
+ * assertions (the D11 boundary, mechanized). A test that accumulates a CLUSTER
2634
+ * of raw body probes — reading `.response.body` (or a variable cast from it)
2635
+ * `threshold`+ times in one test callback — should collapse into one golden:
2636
+ * `expect(result.response).toMatch('case.http')`. One or two probes stay silent
2637
+ * (a legitimate scalpel: absence, a single-field delta, third-party output).
2638
+ *
2639
+ * A probe is a body-rooted read: a direct `.response.body` use, or a field
2640
+ * access on the local binding a `const body = result.response.body as {…}`
2641
+ * introduces. The count is the same threshold the golden transformation applies.
2642
+ */
2643
+ const d12wResponseBodyProbe = {
2644
+ create(context) {
2645
+ if (!segments(context.filename).includes("specs")) return {};
2646
+ const threshold = context.options[0]?.threshold ?? DEFAULT_THRESHOLD;
2647
+ return { CallExpression(node) {
2648
+ if (!isTestCallee(node.callee)) return;
2649
+ const callback = findTestCallback(node.arguments ?? []);
2650
+ if (callback === void 0) return;
2651
+ const aliases = /* @__PURE__ */ new Set();
2652
+ let responseBodyNodes = 0;
2653
+ walk(callback, (inner) => {
2654
+ if (isResponseBody(inner)) responseBodyNodes++;
2655
+ if (inner.type === "VariableDeclarator") {
2656
+ const init = unwrap(inner.init);
2657
+ const id = inner.id;
2658
+ if (init !== void 0 && isResponseBody(init) && id?.type === "Identifier") aliases.add(id.name);
2659
+ }
2660
+ });
2661
+ let aliasReads = 0;
2662
+ walk(callback, (inner) => {
2663
+ if (inner.type !== "MemberExpression") return;
2664
+ const object = inner.object;
2665
+ if (object?.type === "Identifier" && aliases.has(object.name)) aliasReads++;
2666
+ });
2667
+ const probes = responseBodyNodes - aliases.size + aliasReads;
2668
+ if (probes >= threshold) context.report({
2669
+ data: { count: String(probes) },
2670
+ messageId: "bodyProbeCluster",
2671
+ node
2672
+ });
2673
+ } };
2674
+ },
2675
+ meta: {
2676
+ docs: RULE_DOCS["d12w-response-body-probe"],
2677
+ defaultOptions: [{ threshold: DEFAULT_THRESHOLD }],
2678
+ 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)." },
2679
+ schema: [{
2680
+ additionalProperties: false,
2681
+ properties: { threshold: {
2682
+ minimum: 1,
2683
+ type: "integer"
2684
+ } },
2685
+ type: "object"
2686
+ }],
2687
+ type: "suggestion"
2688
+ }
2689
+ };
2690
+ //#endregion
2691
+ //#region src/lint/rules/d13w-unfrozen-negative-fixture.ts
2692
+ /**
2693
+ * Does `arguments[1]` of a `toMatch(name, options)` call carry `{ frozen: true }`?
2694
+ * A frozen fixture opts out of update-mode rewriting, so it is exempt.
2695
+ */
2696
+ function hasFrozenOption(args) {
2697
+ const options = args[1];
2698
+ if (options === void 0 || options.type !== "ObjectExpression") return false;
2699
+ const value = findProperty(options, "frozen")?.value;
2700
+ return value?.type === "Literal" && value.value === true;
2701
+ }
2702
+ /**
2703
+ * Is this `toMatch(…)` call nested inside a WRAPPING `expect(…)` — the shape of a
2704
+ * negative assertion (`expect(() => …toMatch(…)).toThrow()` or
2705
+ * `expect(…toMatch(…)).rejects.toThrow()`)? The subject's own `expect(x)` is the
2706
+ * member OBJECT of `.toMatch` (a descendant), never an ancestor, so a positive
2707
+ * `expect(x).toMatch('f')` never trips this. The walk stays inside the current
2708
+ * expression: it stops at the first statement boundary or non-`expect` call, so
2709
+ * a `toMatch` merely written inside `inUpdateMode(() => …)` is NOT flagged.
2710
+ */
2711
+ const TRANSPARENT_ANCESTORS = /* @__PURE__ */ new Set([
2712
+ "ArrowFunctionExpression",
2713
+ "AwaitExpression",
2714
+ "ChainExpression",
2715
+ "MemberExpression",
2716
+ "ParenthesizedExpression",
2717
+ "TSAsExpression",
2718
+ "TSNonNullExpression"
2719
+ ]);
2720
+ function isWrappedInExpect(node) {
2721
+ let current = node.parent;
2722
+ while (current !== void 0) {
2723
+ if (current.type === "CallExpression") {
2724
+ const callee = current.callee;
2725
+ return callee?.type === "Identifier" && callee.name === "expect";
2726
+ }
2727
+ if (!TRANSPARENT_ANCESTORS.has(current.type)) return false;
2728
+ current = current.parent;
2729
+ }
2730
+ return false;
2731
+ }
2732
+ /**
2733
+ * CONVENTIONS D13 (warning) — a `toMatch` whose failure is the behaviour under
2734
+ * test (wrapped in `expect(() => …).toThrow()` or `expect(…).rejects.toThrow()`)
2735
+ * asserts a DELIBERATELY-WRONG or MISSING fixture. Under `TEST_UPDATE=1` an
2736
+ * unfrozen fixture is silently rewritten with the actual output — the matcher
2737
+ * writes instead of throwing — which destroys the negative case (the assertion
2738
+ * then no longer throws). Pass `{ frozen: true }` so the fixture is never
2739
+ * rewritten and the mismatch/error still throws in update mode.
2740
+ *
2741
+ * The structural signal is precise for the wrapped forms; a `toMatch` routed
2742
+ * through a helper that owns the try/catch (`catchMessage(() => …toMatch(…))`)
2743
+ * is out of static reach — see the D13 process note for that residue.
2744
+ */
2745
+ const d13wUnfrozenNegativeFixture = {
2746
+ create(context) {
2747
+ if (!segments(context.filename).includes("specs")) return {};
2748
+ return { CallExpression(node) {
2749
+ const callee = node.callee;
2750
+ if (callee === void 0 || memberPropertyName(callee) !== "toMatch") return;
2751
+ if (!isWrappedInExpect(node)) return;
2752
+ if (hasFrozenOption(node.arguments ?? [])) return;
2753
+ context.report({
2754
+ messageId: "unfrozenNegativeFixture",
2755
+ node
2756
+ });
2757
+ } };
2758
+ },
2759
+ meta: {
2760
+ docs: RULE_DOCS["d13w-unfrozen-negative-fixture"],
2761
+ 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)." },
2762
+ type: "suggestion"
2763
+ }
2764
+ };
2765
+ //#endregion
2766
+ //#region src/lint/rules/d15w-status-only-probe.ts
2767
+ /** An HTTP status code lives in this range — the numeric-literal gate. */
2768
+ const MIN_STATUS = 100;
2769
+ const MAX_STATUS = 599;
2770
+ /** Matchers that pin a single value — the shape of a status probe. */
2771
+ const EQUALITY_MATCHERS = /* @__PURE__ */ new Set(["toBe", "toEqual"]);
2772
+ /** Is `node` an `expect(…)` call (bare `expect` identifier callee)? */
2773
+ function isExpectCall(node) {
2774
+ if (node?.type !== "CallExpression") return false;
2775
+ const callee = node.callee;
2776
+ return callee?.type === "Identifier" && callee.name === "expect";
2777
+ }
2778
+ /** Is `node` a numeric literal in the HTTP status range (100–599)? */
2779
+ function isStatusLiteral(node) {
2780
+ if (node?.type !== "Literal" || typeof node.value !== "number") return false;
2781
+ const { value } = node;
2782
+ return Number.isInteger(value) && value >= MIN_STATUS && value <= MAX_STATUS;
2783
+ }
2784
+ /**
2785
+ * Is `node` a status probe — `expect(X.status).toBe(N)` / `.toEqual(N)` where
2786
+ * `X.status` is any `.status` member access and `N` is a numeric literal in the
2787
+ * HTTP status range? The numeric-literal gate keeps container-status STRINGS
2788
+ * (`expect(neo.status).toBe('running')`) out.
2789
+ */
2790
+ function isStatusProbe(node) {
2791
+ if (node.type !== "CallExpression") return false;
2792
+ const callee = node.callee;
2793
+ if (callee?.type !== "MemberExpression") return false;
2794
+ const matcher = memberPropertyName(callee);
2795
+ if (matcher === void 0 || !EQUALITY_MATCHERS.has(matcher)) return false;
2796
+ const expectCall = callee.object;
2797
+ if (!isExpectCall(expectCall)) return false;
2798
+ const subject = (expectCall?.arguments ?? [])[0];
2799
+ if (subject?.type !== "MemberExpression" || memberPropertyName(subject) !== "status") return false;
2800
+ const expected = (node.arguments ?? [])[0];
2801
+ return isStatusLiteral(expected);
2802
+ }
2803
+ /**
2804
+ * CONVENTIONS D11/D15 (warning) — a spec test whose ONLY assertions are HTTP
2805
+ * status probes (`expect(X.status).toBe(N)` / `.toEqual(N)`, N a numeric literal
2806
+ * 100–599) throws away the full-response golden. Pin the whole response instead:
2807
+ * `expect(result.response).toMatch('case.http')`.
2808
+ *
2809
+ * This complements d12w (which needs a CLUSTER of ≥3 body probes): the lone
2810
+ * `expect(result.status).toBe(422)` as a test's sole assertion falls below that
2811
+ * threshold yet still hides everything the response carries. The rule fires only
2812
+ * when EVERY `expect(…)` in the callback is a status probe — a status probe
2813
+ * sitting NEXT TO a real assertion (a golden, `toMatchRows`, `toContain`, …) is
2814
+ * the legitimate scalpel and stays silent.
2815
+ */
2816
+ const d15wStatusOnlyProbe = {
2817
+ create(context) {
2818
+ if (!segments(context.filename).includes("specs")) return {};
2819
+ return { CallExpression(node) {
2820
+ if (!isTestCallee(node.callee)) return;
2821
+ const callback = findTestCallback(node.arguments ?? []);
2822
+ if (callback === void 0) return;
2823
+ let expectCalls = 0;
2824
+ let statusProbes = 0;
2825
+ walk(callback, (inner) => {
2826
+ if (isExpectCall(inner)) expectCalls++;
2827
+ if (isStatusProbe(inner)) statusProbes++;
2828
+ });
2829
+ if (expectCalls >= 1 && statusProbes === expectCalls) context.report({
2830
+ messageId: "statusOnlyProbe",
2831
+ node
2832
+ });
2833
+ } };
2834
+ },
2835
+ meta: {
2836
+ docs: RULE_DOCS["d15w-status-only-probe"],
2837
+ 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)." },
2838
+ type: "suggestion"
2839
+ }
2840
+ };
2841
+ //#endregion
2843
2842
  //#region src/lint/package-exports.ts
2844
2843
  /**
2845
2844
  * The subpaths this package actually publishes, read from its OWN
@@ -2893,7 +2892,7 @@ function findOwnManifest(startDir) {
2893
2892
  */
2894
2893
  function declaredSubpaths(startDir) {
2895
2894
  if (startDir === void 0 && cached !== null) return cached;
2896
- const exports = findOwnManifest(startDir ?? dirname(fileURLToPath(import.meta.url)))?.exports;
2895
+ const exports = findOwnManifest(startDir ?? import.meta.dirname)?.exports;
2897
2896
  const subpaths = exports !== null && typeof exports === "object" ? Object.keys(exports).filter((key) => key.startsWith("./") && key !== ".").map((key) => `${PACKAGE$2}/${key.slice(2)}`).sort() : [];
2898
2897
  if (startDir === void 0) cached = subpaths;
2899
2898
  return subpaths;
@@ -2935,9 +2934,9 @@ const f1NoSubpathImport = {
2935
2934
  };
2936
2935
  //#endregion
2937
2936
  //#region src/lint/rules/f2-no-test-imports-in-prod.ts
2938
- const TEST_FILE$4 = /\.(?:test|fixtures|specification)\.[cm]?[jt]sx?$/;
2939
- const CONFIG_FILE = /(?:^|[/\\])[\w.-]*\.config\.[cm]?[jt]s$/;
2940
- const TEST_IMPORT$1 = /\.(?:test|fixtures)(?:\.[cm]?[jt]sx?)?$/;
2937
+ const TEST_FILE$4 = /\.(?:test|fixtures|specification)\.[cm]?[jt]sx?$/u;
2938
+ const CONFIG_FILE = /(?:^|[/\\])[\w.-]*\.config\.[cm]?[jt]s$/u;
2939
+ const TEST_IMPORT$1 = /\.(?:test|fixtures)(?:\.[cm]?[jt]sx?)?$/u;
2941
2940
  const TOOL_SUBPATH$1 = "@jterrazz/test/oxlint";
2942
2941
  /**
2943
2942
  * CONVENTIONS F2 — production code never imports a test artefact. In a
@@ -2994,15 +2993,15 @@ const TOOL_SUBPATH = "@jterrazz/test/oxlint";
2994
2993
  * `server: () => createApp()` importing `../../src/app.js`).
2995
2994
  */
2996
2995
  const FRAMEWORK_LAYERS = /* @__PURE__ */ new Set([
2997
- "core",
2998
2996
  "integrations",
2999
2997
  "lint",
2998
+ "specification",
3000
2999
  "vitest"
3001
3000
  ]);
3002
3001
  /**
3003
3002
  * CONVENTIONS F3 — from `specs/`, deep-importing the FRAMEWORK's internals is
3004
3003
  * forbidden: a relative path resolving inside the framework repo's
3005
- * `src/{core,integrations,vitest,lint}/`, or any `@jterrazz/test/<subpath>`
3004
+ * `src/{specification,integrations,vitest,lint}/`, or any `@jterrazz/test/<subpath>`
3006
3005
  * other than the sanctioned tool-facing `@jterrazz/test/oxlint`. A consumer's
3007
3006
  * imports of its OWN app source are always allowed — that is the pattern.
3008
3007
  *
@@ -3028,7 +3027,7 @@ const f3SpecsPublicEntry = {
3028
3027
  }
3029
3028
  const marker = source.indexOf("/src/");
3030
3029
  if (marker === -1 && !source.startsWith("src/")) return;
3031
- const layer = (marker === -1 ? source.slice(4) : source.slice(marker + 5)).split("/")[0];
3030
+ const layer = (marker === -1 ? source.slice(4) : source.slice(marker + 5)).split("/")[0] ?? "";
3032
3031
  if (!FRAMEWORK_LAYERS.has(layer)) return;
3033
3032
  if (underIntegrations && layer === "integrations") return;
3034
3033
  context.report({
@@ -3046,8 +3045,8 @@ const f3SpecsPublicEntry = {
3046
3045
  };
3047
3046
  //#endregion
3048
3047
  //#region src/lint/rules/f4-no-test-to-test-import.ts
3049
- const TEST_FILE$3 = /\.test\.[cm]?[jt]sx?$/;
3050
- const TEST_IMPORT = /\.test(?:\.[cm]?[jt]sx?)?$/;
3048
+ const TEST_FILE$3 = /\.test\.[cm]?[jt]sx?$/u;
3049
+ const TEST_IMPORT = /\.test(?:\.[cm]?[jt]sx?)?$/u;
3051
3050
  /**
3052
3051
  * CONVENTIONS F4 — a `*.test.ts` never imports another `*.test.ts`. Shared
3053
3052
  * test data belongs in a `*.fixtures.ts` neighbour (F5), shared runners in a
@@ -3072,8 +3071,8 @@ const f4NoTestToTestImport = {
3072
3071
  };
3073
3072
  //#endregion
3074
3073
  //#region src/lint/rules/f5-fixtures-only-from-tests.ts
3075
- const TEST_FILE$2 = /\.test\.[cm]?[jt]sx?$/;
3076
- const FIXTURES_IMPORT = /\.fixtures(?:\.[cm]?[jt]sx?)?$/;
3074
+ const TEST_FILE$2 = /\.test\.[cm]?[jt]sx?$/u;
3075
+ const FIXTURES_IMPORT = /\.fixtures(?:\.[cm]?[jt]sx?)?$/u;
3077
3076
  /**
3078
3077
  * CONVENTIONS F5 — a `*.fixtures.ts` module is only importable from
3079
3078
  * `*.test.ts` files. Any other importer (prod code, a specification file,
@@ -3098,7 +3097,7 @@ const f5FixturesOnlyFromTests = {
3098
3097
  };
3099
3098
  //#endregion
3100
3099
  //#region src/lint/rules/i1-layer-boundaries.ts
3101
- const TEST_OR_FIXTURES = /\.(?:test|fixtures)\.[cm]?[jt]sx?$/;
3100
+ const TEST_OR_FIXTURES = /\.(?:test|fixtures)\.[cm]?[jt]sx?$/u;
3102
3101
  function matchesPackage(source, packages) {
3103
3102
  return packages.some((name) => source === name || source.startsWith(`${name}/`));
3104
3103
  }
@@ -3110,7 +3109,7 @@ function pathInsideSrc(path) {
3110
3109
  }
3111
3110
  /** Strip a `.js`/`.ts`-style extension for exact-path comparison. */
3112
3111
  function withoutExtension(path) {
3113
- return path.replace(/\.[cm]?[jt]sx?$/, "");
3112
+ return path.replace(/\.[cm]?[jt]sx?$/u, "");
3114
3113
  }
3115
3114
  /** Does `target` match one of the declared paths (prefix, or exact module)? */
3116
3115
  function matchesPath(target, patterns) {
@@ -3227,7 +3226,7 @@ const i1LayerBoundaries = {
3227
3226
  };
3228
3227
  //#endregion
3229
3228
  //#region src/lint/rules/i2-sibling-test-naming.ts
3230
- const TEST_FILE$1 = /\.test\.[cm]?[jt]sx?$/;
3229
+ const TEST_FILE$1 = /\.test\.[cm]?[jt]sx?$/u;
3231
3230
  /**
3232
3231
  * CONVENTIONS I2 — module tests are NEIGHBOURS: the test of `<file>.ts` is
3233
3232
  * `<file>.test.ts` next to it (Go's `foo_test.go` parity). Flags, on visited
@@ -3255,7 +3254,7 @@ const i2SiblingTestNaming = {
3255
3254
  ".js"
3256
3255
  ].map((extension) => file.replace(TEST_FILE$1, extension));
3257
3256
  if (!neighbours.some(isFile)) context.report({
3258
- data: { expected: neighbours[0].split(/[/\\]/).at(-1) ?? "" },
3257
+ data: { expected: neighbours[0]?.split(/[/\\]/u).at(-1) ?? "" },
3259
3258
  messageId: "orphanTest",
3260
3259
  node
3261
3260
  });
@@ -3282,7 +3281,7 @@ const i2SiblingTestNaming = {
3282
3281
  };
3283
3282
  //#endregion
3284
3283
  //#region src/lint/rules/i4-no-vi-mock-in-src.ts
3285
- const TEST_FILE = /\.test\.[cm]?[jt]sx?$/;
3284
+ const TEST_FILE = /\.test\.[cm]?[jt]sx?$/u;
3286
3285
  const DATA_ASSET = new RegExp(`\\.(?:${[
3287
3286
  "avif",
3288
3287
  "bin",
@@ -3320,10 +3319,10 @@ const DATA_ASSET = new RegExp(`\\.(?:${[
3320
3319
  "yaml",
3321
3320
  "yml",
3322
3321
  "zip"
3323
- ].join("|")})$`, "i");
3322
+ ].join("|")})$`, "iu");
3324
3323
  /** A bundler suffix (`./payload.json?raw`, `./doc.md#frag`) is not part of the extension. */
3325
3324
  function withoutSuffix(source) {
3326
- return source.replace(/[?#].*$/, "");
3325
+ return source.replace(/[?#].*$/u, "");
3327
3326
  }
3328
3327
  /**
3329
3328
  * CONVENTIONS I4 — in module tests under `src/`, mocks and data are CODE:
@@ -3563,7 +3562,7 @@ const plugin = {
3563
3562
  *
3564
3563
  * 'jterrazz/b5-await-using': ['error', { runners: ['dockerCli'] }]
3565
3564
  */
3566
- const recommendedRules = Object.fromEntries(Object.keys(plugin.rules).map((rule) => [`jterrazz/${rule}`, /^\w+w-/.test(rule) ? "warn" : "error"]));
3565
+ const recommendedRules = Object.fromEntries(Object.keys(plugin.rules).map((rule) => [`jterrazz/${rule}`, /^\w+w-/u.test(rule) ? "warn" : "error"]));
3567
3566
  /**
3568
3567
  * The composable testing fragment — wire the plugin, enable the whole
3569
3568
  * catalogue, and ship the one override every strict consumer needs. Designed