@jterrazz/test 16.0.0 → 16.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.
Files changed (210) hide show
  1. package/dist/appium.adapter.js +59 -4
  2. package/dist/browser/index.js +88 -40
  3. package/dist/checker.js +272 -63
  4. package/dist/index.d.ts +33 -4
  5. package/dist/index.js +47 -24
  6. package/dist/oxlint.cjs +100 -27
  7. package/dist/oxlint.js +100 -27
  8. package/dist/playwright.adapter.js +14 -65
  9. package/dist/{ambiguity.js → substring-warning.js} +99 -1
  10. package/dist/vitest.js +71 -11
  11. package/docs/01-architecture.md +95 -0
  12. package/docs/02-developing.md +351 -0
  13. package/docs/03-testing.md +495 -0
  14. package/docs/04-operating.md +101 -0
  15. package/docs/05-module-tests.md +152 -0
  16. package/docs/06-integration.md +180 -0
  17. package/docs/07-component.md +246 -0
  18. package/docs/08-website.md +404 -0
  19. package/docs/09-mobile.md +270 -0
  20. package/docs/10-api.md +264 -0
  21. package/docs/11-jobs.md +157 -0
  22. package/docs/12-cli.md +677 -0
  23. package/docs/13-elements.md +242 -0
  24. package/docs/14-assertions.md +314 -0
  25. package/docs/15-tokens.md +253 -0
  26. package/docs/16-contracts.md +438 -0
  27. package/docs/17-services.md +188 -0
  28. package/docs/18-conventions.md +305 -0
  29. package/docs/19-linting.md +510 -0
  30. package/docs/README.md +36 -0
  31. package/docs/decisions/001-v9-conventions-on-three-channels.md +50 -0
  32. package/docs/decisions/002-the-statique-channel-holds-only-what-upstream-does-not.md +49 -0
  33. package/docs/decisions/003-a-rendered-component-is-a-facet-on-vitest-browser-mode.md +84 -0
  34. package/docs/decisions/004-a-module-against-real-services-or-a-golden-is-the-integration-facet.md +81 -0
  35. package/docs/decisions/005-the-doubles-ladder-is-closed.md +86 -0
  36. package/docs/decisions/006-the-catalogue-reaches-a-file-by-its-role.md +245 -0
  37. package/docs/decisions/007-compose-mode-leaves-the-package.md +53 -0
  38. package/docs/decisions/008-vitest-5-node-24-and-the-native-seams-as-optional-peers.md +75 -0
  39. package/docs/decisions/_template.md +22 -0
  40. package/docs/reference/classes/Matcher.md +120 -0
  41. package/docs/reference/classes/ProcessHandle.md +368 -0
  42. package/docs/reference/functions/defineContract.md +39 -0
  43. package/docs/reference/functions/defineContracts.md +36 -0
  44. package/docs/reference/functions/disabled.md +28 -0
  45. package/docs/reference/functions/enabled.md +20 -0
  46. package/docs/reference/functions/focused.md +26 -0
  47. package/docs/reference/functions/mockOf.md +79 -0
  48. package/docs/reference/functions/postgres.md +31 -0
  49. package/docs/reference/functions/process.md +33 -0
  50. package/docs/reference/functions/redis.md +28 -0
  51. package/docs/reference/functions/required.md +41 -0
  52. package/docs/reference/functions/selected.md +26 -0
  53. package/docs/reference/functions/sqlite.md +33 -0
  54. package/docs/reference/functions/testId.md +20 -0
  55. package/docs/reference/functions/text.md +40 -0
  56. package/docs/reference/functions/valued.md +28 -0
  57. package/docs/reference/functions/waitUntil.md +46 -0
  58. package/docs/reference/functions/within.md +29 -0
  59. package/docs/reference/index.md +196 -0
  60. package/docs/reference/interfaces/BaseResult.md +87 -0
  61. package/docs/reference/interfaces/CallResult.md +160 -0
  62. package/docs/reference/interfaces/CaptureScope.md +82 -0
  63. package/docs/reference/interfaces/CliResult.md +253 -0
  64. package/docs/reference/interfaces/ContainerAccessor.md +166 -0
  65. package/docs/reference/interfaces/DirectoryAccessor.md +33 -0
  66. package/docs/reference/interfaces/FetchResult.md +180 -0
  67. package/docs/reference/interfaces/FilesystemAccessor.md +46 -0
  68. package/docs/reference/interfaces/HttpResult.md +126 -0
  69. package/docs/reference/interfaces/JsonAccessor.md +46 -0
  70. package/docs/reference/interfaces/PageResult.md +356 -0
  71. package/docs/reference/interfaces/RenderResult.md +72 -0
  72. package/docs/reference/interfaces/RenderedText.md +110 -0
  73. package/docs/reference/interfaces/ResponseAccessor.md +47 -0
  74. package/docs/reference/interfaces/ScreenResult.md +128 -0
  75. package/docs/reference/interfaces/TableAccessor.md +26 -0
  76. package/docs/reference/interfaces/TextAccessor.md +96 -0
  77. package/docs/reference/type-aliases/ApiHandle.md +68 -0
  78. package/docs/reference/type-aliases/ApiSpecification.md +213 -0
  79. package/docs/reference/type-aliases/ApiSpecificationOptions.md +68 -0
  80. package/docs/reference/type-aliases/BrowserConsoleMessage.md +30 -0
  81. package/docs/reference/type-aliases/BrowserLinkElement.md +50 -0
  82. package/docs/reference/type-aliases/BrowserMetaElement.md +40 -0
  83. package/docs/reference/type-aliases/BrowserOpenOptions.md +87 -0
  84. package/docs/reference/type-aliases/BrowserPage.md +132 -0
  85. package/docs/reference/type-aliases/BrowserPort.md +51 -0
  86. package/docs/reference/type-aliases/CliEnv.md +11 -0
  87. package/docs/reference/type-aliases/CliHandle.md +80 -0
  88. package/docs/reference/type-aliases/CliInput.md +36 -0
  89. package/docs/reference/type-aliases/CliOutput.md +46 -0
  90. package/docs/reference/type-aliases/CliPort.md +58 -0
  91. package/docs/reference/type-aliases/CliSpecification.md +144 -0
  92. package/docs/reference/type-aliases/CliSpecificationOptions.md +134 -0
  93. package/docs/reference/type-aliases/ComponentChain.md +156 -0
  94. package/docs/reference/type-aliases/ComponentScenario.md +20 -0
  95. package/docs/reference/type-aliases/ComponentUi.md +47 -0
  96. package/docs/reference/type-aliases/ComponentVisitor.md +10 -0
  97. package/docs/reference/type-aliases/ContainerPort.md +113 -0
  98. package/docs/reference/type-aliases/Contract.md +68 -0
  99. package/docs/reference/type-aliases/ContractInput.md +13 -0
  100. package/docs/reference/type-aliases/ContractRequest.md +95 -0
  101. package/docs/reference/type-aliases/ContractResponder.md +23 -0
  102. package/docs/reference/type-aliases/ContractResponse.md +78 -0
  103. package/docs/reference/type-aliases/ContractResponseValue.md +13 -0
  104. package/docs/reference/type-aliases/Contracts.md +48 -0
  105. package/docs/reference/type-aliases/DatabaseKeys.md +16 -0
  106. package/docs/reference/type-aliases/DatabasePort.md +72 -0
  107. package/docs/reference/type-aliases/DeviceOpenOptions.md +46 -0
  108. package/docs/reference/type-aliases/DevicePort.md +51 -0
  109. package/docs/reference/type-aliases/DeviceScreen.md +32 -0
  110. package/docs/reference/type-aliases/DeviceTimeouts.md +39 -0
  111. package/docs/reference/type-aliases/DockerSpecConfig.md +43 -0
  112. package/docs/reference/type-aliases/DomMount.md +21 -0
  113. package/docs/reference/type-aliases/ElementKind.md +27 -0
  114. package/docs/reference/type-aliases/ElementMatch.md +74 -0
  115. package/docs/reference/type-aliases/ElementOptions.md +29 -0
  116. package/docs/reference/type-aliases/ElementRef.md +128 -0
  117. package/docs/reference/type-aliases/ExecOptions.md +37 -0
  118. package/docs/reference/type-aliases/FileAccessor.md +54 -0
  119. package/docs/reference/type-aliases/HonoApp.md +31 -0
  120. package/docs/reference/type-aliases/HttpContractFilter.md +49 -0
  121. package/docs/reference/type-aliases/HttpResponseInit.md +46 -0
  122. package/docs/reference/type-aliases/HttpStreamInit.md +58 -0
  123. package/docs/reference/type-aliases/IntegrationHandle.md +46 -0
  124. package/docs/reference/type-aliases/IntegrationSpecification.md +111 -0
  125. package/docs/reference/type-aliases/IntegrationSpecificationOptions.md +42 -0
  126. package/docs/reference/type-aliases/Intercept.md +19 -0
  127. package/docs/reference/type-aliases/InterceptOptions.md +28 -0
  128. package/docs/reference/type-aliases/InterceptScope.md +11 -0
  129. package/docs/reference/type-aliases/IsolationStrategy.md +75 -0
  130. package/docs/reference/type-aliases/JobHandle.md +34 -0
  131. package/docs/reference/type-aliases/JobsHandle.md +57 -0
  132. package/docs/reference/type-aliases/JobsResult.md +16 -0
  133. package/docs/reference/type-aliases/JobsSpecification.md +94 -0
  134. package/docs/reference/type-aliases/JobsSpecificationOptions.md +56 -0
  135. package/docs/reference/type-aliases/LandmarkKind.md +20 -0
  136. package/docs/reference/type-aliases/LiterateRunFlags.md +27 -0
  137. package/docs/reference/type-aliases/LiterateServeRegistration.md +70 -0
  138. package/docs/reference/type-aliases/MatchFixtureOptions.md +27 -0
  139. package/docs/reference/type-aliases/MatchableRequest.md +61 -0
  140. package/docs/reference/type-aliases/MatcherKind.md +34 -0
  141. package/docs/reference/type-aliases/MobileBackendOptions.md +28 -0
  142. package/docs/reference/type-aliases/MobileElementKind.md +14 -0
  143. package/docs/reference/type-aliases/MobileElementMatch.md +60 -0
  144. package/docs/reference/type-aliases/MobileElementRef.md +20 -0
  145. package/docs/reference/type-aliases/MobileHandle.md +65 -0
  146. package/docs/reference/type-aliases/MobileScenario.md +20 -0
  147. package/docs/reference/type-aliases/MobileSpecification.md +51 -0
  148. package/docs/reference/type-aliases/MobileSpecificationOptions.md +126 -0
  149. package/docs/reference/type-aliases/MobileVisitor.md +81 -0
  150. package/docs/reference/type-aliases/MockOfOptions.md +26 -0
  151. package/docs/reference/type-aliases/MockPort.md +10 -0
  152. package/docs/reference/type-aliases/PinnedClock.md +10 -0
  153. package/docs/reference/type-aliases/PostgresOptions.md +32 -0
  154. package/docs/reference/type-aliases/ProcessOptions.md +112 -0
  155. package/docs/reference/type-aliases/RedisOptions.md +20 -0
  156. package/docs/reference/type-aliases/RenderSubject.md +12 -0
  157. package/docs/reference/type-aliases/ScreenNode.md +67 -0
  158. package/docs/reference/type-aliases/ServerPort.md +36 -0
  159. package/docs/reference/type-aliases/ServerResponse.md +46 -0
  160. package/docs/reference/type-aliases/ServerSpec.md +21 -0
  161. package/docs/reference/type-aliases/ServiceHandle.md +216 -0
  162. package/docs/reference/type-aliases/ServiceRecord.md +12 -0
  163. package/docs/reference/type-aliases/SpecDocument.md +84 -0
  164. package/docs/reference/type-aliases/SpecEnvToken.md +21 -0
  165. package/docs/reference/type-aliases/SpecFileAssertion.md +27 -0
  166. package/docs/reference/type-aliases/SpecFixture.md +30 -0
  167. package/docs/reference/type-aliases/SpecKind.md +8 -0
  168. package/docs/reference/type-aliases/SpecRun.md +108 -0
  169. package/docs/reference/type-aliases/SpecServeEntry.md +40 -0
  170. package/docs/reference/type-aliases/SpecStream.md +32 -0
  171. package/docs/reference/type-aliases/SpecificationConfig.md +290 -0
  172. package/docs/reference/type-aliases/SqliteOptions.md +36 -0
  173. package/docs/reference/type-aliases/SseEvent.md +58 -0
  174. package/docs/reference/type-aliases/StreamBody.md +61 -0
  175. package/docs/reference/type-aliases/TextFilter.md +22 -0
  176. package/docs/reference/type-aliases/VisitScenario.md +20 -0
  177. package/docs/reference/type-aliases/Visitor.md +215 -0
  178. package/docs/reference/type-aliases/WaitUntilOptions.md +46 -0
  179. package/docs/reference/type-aliases/WebsiteBackendOptions.md +37 -0
  180. package/docs/reference/type-aliases/WebsiteHandle.md +51 -0
  181. package/docs/reference/type-aliases/WebsiteSpecification.md +118 -0
  182. package/docs/reference/type-aliases/WebsiteSpecificationOptions.md +40 -0
  183. package/docs/reference/variables/anthropic.md +19 -0
  184. package/docs/reference/variables/banner.md +10 -0
  185. package/docs/reference/variables/button.md +10 -0
  186. package/docs/reference/variables/clock.md +16 -0
  187. package/docs/reference/variables/complementary.md +10 -0
  188. package/docs/reference/variables/component.md +10 -0
  189. package/docs/reference/variables/content.md +10 -0
  190. package/docs/reference/variables/contentinfo.md +10 -0
  191. package/docs/reference/variables/dialog.md +10 -0
  192. package/docs/reference/variables/field.md +10 -0
  193. package/docs/reference/variables/form.md +10 -0
  194. package/docs/reference/variables/heading.md +10 -0
  195. package/docs/reference/variables/http.md +40 -0
  196. package/docs/reference/variables/intercept.md +11 -0
  197. package/docs/reference/variables/link.md +10 -0
  198. package/docs/reference/variables/listitem.md +10 -0
  199. package/docs/reference/variables/main.md +10 -0
  200. package/docs/reference/variables/match.md +8 -0
  201. package/docs/reference/variables/navigation.md +10 -0
  202. package/docs/reference/variables/openai.md +21 -0
  203. package/docs/reference/variables/option.md +17 -0
  204. package/docs/reference/variables/region.md +10 -0
  205. package/docs/reference/variables/row.md +10 -0
  206. package/docs/reference/variables/search.md +10 -0
  207. package/docs/reference/variables/specification.md +8 -0
  208. package/docs/reference/variables/status.md +10 -0
  209. package/docs/reference/variables/table.md +10 -0
  210. package/package.json +2 -1
package/dist/checker.js CHANGED
@@ -688,7 +688,7 @@ function checkPoolFixtures(rootDir) {
688
688
  violations.push({
689
689
  file: relEntry,
690
690
  line: 1,
691
- message: `${relEntry}: dead pool fixture — no spec under ${relative(rootDir, specsRoot)} references $FIXTURES/${entry.name} (C9 — see docs/19-linting.md)`,
691
+ message: `${relEntry}: dead pool fixture — no spec under ${relative(rootDir, specsRoot) || basename(specsRoot)} references $FIXTURES/${entry.name} (C9 — see docs/19-linting.md)`,
692
692
  rule: "c9",
693
693
  severity: "error"
694
694
  });
@@ -939,6 +939,13 @@ const SKIPPED$2 = /* @__PURE__ */ new Set([
939
939
  GROUND_FIXTURES,
940
940
  "node_modules"
941
941
  ]);
942
+ /**
943
+ * Does this facet-relative path cross ground? A LEADING underscore names it —
944
+ * the four ground directories and nothing else carry one (C13).
945
+ */
946
+ function isUnderGround(relativePath) {
947
+ return relativePath.split(/[/\\]/u).some((segment) => segment.startsWith("_"));
948
+ }
942
949
  /** `readdirSync` that answers `[]` for anything it cannot read. */
943
950
  function entriesOf(dir) {
944
951
  try {
@@ -1045,6 +1052,7 @@ function checkModuleTestUnderFacet(specsRoot) {
1045
1052
  if (specificationsIn(path).length === 0) continue;
1046
1053
  for (const file of walkFiles(path)) {
1047
1054
  if (!isTestFileName(file.slice(file.lastIndexOf("/") + 1))) continue;
1055
+ if (isUnderGround(relative(path, file))) continue;
1048
1056
  if (reachesRunner(textOf$1(file) ?? "")) continue;
1049
1057
  const rel = relative(specsRoot, file);
1050
1058
  violations.push({
@@ -1142,6 +1150,14 @@ function checkGroundOwnedByOne(specsRoot) {
1142
1150
  * (the package's own `specs/lint/`, a repository's consistency suites) is a
1143
1151
  * repository suite: it covers a tree rather than the assembled product, so
1144
1152
  * it keeps `.test.ts` and C1's declared depth is what judges its shape.
1153
+ *
1154
+ * A tree may also be ONE facet's whole, with no `specs/<facet>/` level: the
1155
+ * runner sits at the root of `specs/` and the folders below it are domains (the
1156
+ * `depth: 'mirror'` shape, where the tree follows a command tree instead of a
1157
+ * facet/domain one). Such a tree carries a `*.specification.ts` at its root,
1158
+ * and that is what says so — every `.test.ts` below it specifies the assembled
1159
+ * product, whatever folder it sits in. Without this clause the whole shape was
1160
+ * invisible to C12 and its specs had to be renamed by hand.
1145
1161
  */
1146
1162
  /** The six facet folders — a first-level name under `specs/` that IS a constructor. */
1147
1163
  const FACETS = /* @__PURE__ */ new Set([
@@ -1229,23 +1245,39 @@ function checkSpecOutsideSpecs(memberDir) {
1229
1245
  function checkTestUnderFacet(specsRoot) {
1230
1246
  return movesUnderFacet(specsRoot).map(({ from }) => {
1231
1247
  const rel = relative(specsRoot, from);
1248
+ const first = rel.split(/[/\\]/u)[0] ?? "";
1232
1249
  return {
1233
1250
  file: rel,
1234
1251
  line: 1,
1235
- message: `${rel}:1: a \`.test.ts\` under \`specs/${rel.split(/[/\\]/u)[0] ?? ""}/\` specifies the assembled product — rename it \`.spec.ts\` (fixable: \`jterrazz-test-check <root> --fix\`) (C12 — see docs/19-linting.md)`,
1252
+ message: `${rel}:1: a \`.test.ts\` under \`${FACETS.has(first) ? `specs/${first}/` : "specs/"}\` specifies the assembled product — rename it \`.spec.ts\` (fixable: \`jterrazz-test-check <root> --fix\`) (C12 — see docs/19-linting.md)`,
1236
1253
  rule: "c12-spec-file-name",
1237
1254
  severity: "error"
1238
1255
  };
1239
1256
  });
1240
1257
  }
1258
+ /**
1259
+ * Is this tree ONE facet's whole, rooted at `specs/`?
1260
+ *
1261
+ * A `*.specification.ts` at the ROOT of the tree is the statement: the runner
1262
+ * is the tree's, so every folder below it is a domain of that one facet and no
1263
+ * first level is a repository suite.
1264
+ */
1265
+ function isRootedFacetTree(specsRoot) {
1266
+ try {
1267
+ return readdirSync(specsRoot, { withFileTypes: true }).some((entry) => entry.isFile() && entry.name.includes(".specification."));
1268
+ } catch {
1269
+ return false;
1270
+ }
1271
+ }
1241
1272
  /** The renames clause two asks for, as absolute from/to pairs. */
1242
1273
  function movesUnderFacet(specsRoot) {
1243
1274
  const moves = [];
1275
+ const rooted = isRootedFacetTree(specsRoot);
1244
1276
  for (const path of walk(specsRoot)) {
1245
1277
  if (!path.endsWith(".test.ts")) continue;
1246
1278
  const parts = relative(specsRoot, path).split(/[/\\]/u);
1247
1279
  const facet = parts[0];
1248
- if (parts.length < 2 || facet === void 0 || !FACETS.has(facet)) continue;
1280
+ if (!rooted && (parts.length < 2 || facet === void 0 || !FACETS.has(facet))) continue;
1249
1281
  if (parts.some((part) => part.startsWith("_"))) continue;
1250
1282
  if (!reachesRunner(textOf(path))) continue;
1251
1283
  moves.push({
@@ -1273,6 +1305,16 @@ function movesUnderFacet(specsRoot) {
1273
1305
  * it for one member, and a path-less `--format json` run at the project root
1274
1306
  * runs it for every member the root manifest declares.
1275
1307
  */
1308
+ /** How deep a member may sit below the root — the toolchain's bound, to the segment. */
1309
+ const MEMBER_DEPTH = 6;
1310
+ /** Directories the member walk never enters. */
1311
+ const SKIPPED = /* @__PURE__ */ new Set([
1312
+ ".git",
1313
+ ".artifacts",
1314
+ "coverage",
1315
+ "dist",
1316
+ "node_modules"
1317
+ ]);
1276
1318
  /** The two simulated DOMs — the same pair the statique E5b refuses. */
1277
1319
  const SIMULATED_DOM = /\benvironment\s*:\s*['"](?<dom>happy-dom|jsdom)['"]/u;
1278
1320
  /**
@@ -1310,16 +1352,80 @@ const RETIRED_SCOPES = ["@testing-library/"];
1310
1352
  * React Native renders under jest until the react-native-web answer lands, and
1311
1353
  * where jest is the sanctioned runner `@testing-library/react-native` is the
1312
1354
  * only vocabulary there is: the rule would be asking for a facet that does not
1313
- * reach that runtime yet. The allowance is read from the member itself — a
1314
- * declared `jest`, a `jest` field, or a `test` script that calls it — so it
1315
- * lapses the day the member stops running jest.
1355
+ * reach that runtime yet. The allowance lapses the day jest stops running the
1356
+ * member's tests, and it has TWO sources, because jest is not always the
1357
+ * member's own: in a workspace where one app owns the only working RN
1358
+ * toolchain, the library declares the vocabulary and the app runs the tests
1359
+ * that use it.
1316
1360
  */
1317
1361
  const JEST_ONLY_SEAM = "@testing-library/react-native";
1318
- /** Does this member run jest? */
1319
- function runsJest(manifest) {
1362
+ /** Does the member's OWN manifest say jest runs here? */
1363
+ function declaresJest(manifest) {
1320
1364
  const declares = (name) => name in (manifest.dependencies ?? {}) || name in (manifest.devDependencies ?? {});
1321
1365
  return declares("jest") || declares("jest-expo") || manifest.jest !== void 0 || /(?:^|[\s/])jest(?:\s|$)/u.test(manifest.scripts?.test ?? "");
1322
1366
  }
1367
+ /** The config names jest answers to, whatever a project writes it in. */
1368
+ const JEST_CONFIG = /^jest\.config\.[cm]?[jt]s$/u;
1369
+ /** The keys of a jest config that name WHERE its tests are. */
1370
+ const JEST_REACH = /\b(?:roots|testMatch|testPathDirs|testPathIgnorePatterns)\s*:\s*\[(?<paths>[^\]]*)\]/gu;
1371
+ /** One quoted entry of such a list. */
1372
+ const JEST_PATH = /['"`](?<path>[^'"`\n]*)['"`]/gu;
1373
+ /** Every `jest.config.*` under `rootDir`, by absolute path — read once per root. */
1374
+ const jestConfigs = /* @__PURE__ */ new Map();
1375
+ /** Find them, bounded by the same skips and depth the member walk uses. */
1376
+ function findJestConfigs(rootDir) {
1377
+ const cached = jestConfigs.get(rootDir);
1378
+ if (cached !== void 0) return cached;
1379
+ const found = [];
1380
+ const walk = (dir, depth) => {
1381
+ if (depth > MEMBER_DEPTH) return;
1382
+ let entries;
1383
+ try {
1384
+ entries = readdirSync(dir, { withFileTypes: true });
1385
+ } catch {
1386
+ return;
1387
+ }
1388
+ for (const entry of entries) {
1389
+ const path = join(dir, entry.name);
1390
+ if (entry.isDirectory()) {
1391
+ if (!entry.name.startsWith(".") && !SKIPPED.has(entry.name)) walk(path, depth + 1);
1392
+ } else if (JEST_CONFIG.test(entry.name)) found.push(path);
1393
+ }
1394
+ };
1395
+ walk(rootDir, 0);
1396
+ jestConfigs.set(rootDir, found);
1397
+ return found;
1398
+ }
1399
+ /**
1400
+ * Does a jest config ELSEWHERE in the workspace collect this member's tests?
1401
+ *
1402
+ * jterrazz-design is the shape: the render tests are co-located in
1403
+ * `packages/native/src`, and `apps/playground` — which owns the only working
1404
+ * React Native toolchain — is what runs them, rooting into the library from its
1405
+ * own config. The library's manifest says `vitest --run`, so reading the
1406
+ * allowance from the member alone answered "no jest here" and F8 errored on
1407
+ * exactly the layout the exemption was written for.
1408
+ *
1409
+ * `<rootDir>` is jest's own token for the directory its config sits in, so a
1410
+ * path is resolved against that, and the member is reached when the result
1411
+ * lands inside it.
1412
+ */
1413
+ function jestReachesMember(memberDir, rootDir) {
1414
+ return findJestConfigs(rootDir).some((config) => {
1415
+ const from = dirname(config);
1416
+ let text;
1417
+ try {
1418
+ text = readFileSync(config, "utf8");
1419
+ } catch {
1420
+ return false;
1421
+ }
1422
+ return [...text.matchAll(JEST_REACH)].some((list) => [...(list.groups?.paths ?? "").matchAll(JEST_PATH)].some((entry) => {
1423
+ const stated = (entry.groups?.path ?? "").replace("<rootDir>", from);
1424
+ const absolute = resolve(from, stated);
1425
+ return absolute === memberDir || absolute.startsWith(`${memberDir}/`);
1426
+ }));
1427
+ });
1428
+ }
1323
1429
  /** Where a member states what it collects, whatever extension it writes it in. */
1324
1430
  const CONFIG_NAMES$1 = [
1325
1431
  "vitest.config.ts",
@@ -1329,14 +1435,6 @@ const CONFIG_NAMES$1 = [
1329
1435
  "vitest.config.mjs",
1330
1436
  "vitest.config.cjs"
1331
1437
  ];
1332
- /** Directories the member walk never enters. */
1333
- const SKIPPED = /* @__PURE__ */ new Set([
1334
- ".git",
1335
- ".artifacts",
1336
- "coverage",
1337
- "dist",
1338
- "node_modules"
1339
- ]);
1340
1438
  /** A member's manifest, or null when it has none (it is then not a member). */
1341
1439
  function readManifest(dir) {
1342
1440
  const path = join(dir, "package.json");
@@ -1394,9 +1492,8 @@ function holdsTests(dir) {
1394
1492
  */
1395
1493
  const DELEGATING_TEST = /--workspaces|(?:^|\s)-r(?:\s|$)|--recursive|turbo\s+run|nx\s+run-many|lerna\s+run/u;
1396
1494
  /** Every seam name this member declares that it should not, and why it should not. */
1397
- function declaredSeams(manifest) {
1495
+ function declaredSeams(manifest, jestRuns) {
1398
1496
  if (manifest.name === FRAMEWORK) return [];
1399
- const jestRuns = runsJest(manifest);
1400
1497
  const isRetired = (name) => {
1401
1498
  if (name === JEST_ONLY_SEAM && jestRuns) return false;
1402
1499
  return RETIRED.has(name) || RETIRED_SCOPES.some((scope) => name.startsWith(scope));
@@ -1452,8 +1549,9 @@ function simulatedDom(config, rootDir) {
1452
1549
  * The two are not the same sentence: removing a transitive changes nothing a
1453
1550
  * test can see, while dropping a retired seam means writing its facet.
1454
1551
  */
1455
- function seamDependencies(manifest, memberDir, label, subject) {
1456
- return declaredSeams(manifest).map((seam) => ({
1552
+ function seamDependencies(member) {
1553
+ const { label, manifest, memberDir, rootDir, subject } = member;
1554
+ return declaredSeams(manifest, declaresJest(manifest) || jestReachesMember(memberDir, rootDir)).map((seam) => ({
1457
1555
  file: join(label, "package.json"),
1458
1556
  line: lineOfKey(memberDir, seam.name),
1459
1557
  message: seam.kind === "carried" ? `${subject}: \`${seam.name}\` is a transitive of \`@jterrazz/test\` — remove the declaration (F8 — see docs/19-linting.md#f8-no-seam-dependency)` : `${subject}: \`${seam.name}\` is a seam \`@jterrazz/test\` replaced — the facet is \`component()\`, \`website()\`, \`clock\` or \`intercept()\`; remove the declaration (F8 — see docs/19-linting.md#f8-no-seam-dependency)`,
@@ -1488,7 +1586,13 @@ function checkMember(memberDir, rootDir) {
1488
1586
  return [
1489
1587
  ...config === null ? configPresent(manifest, memberDir, label, subject) : [],
1490
1588
  ...config === null ? [] : simulatedDom(config, rootDir),
1491
- ...seamDependencies(manifest, memberDir, label, subject),
1589
+ ...seamDependencies({
1590
+ label,
1591
+ manifest,
1592
+ memberDir,
1593
+ rootDir,
1594
+ subject
1595
+ }),
1492
1596
  ...anchoredToRoot(checkSpecOutsideSpecs(memberDir), label)
1493
1597
  ];
1494
1598
  }
@@ -1509,8 +1613,6 @@ function toPattern(glob) {
1509
1613
  const escaped = glob.replaceAll(/[.+^${}()|[\]\\]/gu, String.raw`\$&`).replaceAll("**", " ").replaceAll("*", "[^/]*").replaceAll(" ", ".*");
1510
1614
  return new RegExp(`^${escaped}$`, "u");
1511
1615
  }
1512
- /** How deep a member may sit below the root — the toolchain's bound, to the segment. */
1513
- const MEMBER_DEPTH = 6;
1514
1616
  /**
1515
1617
  * Every workspace member declared by the root manifest, plus the root itself —
1516
1618
  * a single-package repository IS its own member, and the pass has to reach it.
@@ -1573,7 +1675,8 @@ const SPECS_DEPTH = 6;
1573
1675
  */
1574
1676
  function discoverSpecRoots(rootDir) {
1575
1677
  const roots = /* @__PURE__ */ new Set();
1576
- for (const member of discoverMembers(rootDir)) {
1678
+ const members = discoverMembers(rootDir);
1679
+ for (const member of members.length === 0 ? [rootDir] : members) {
1577
1680
  if (basename(member) === "specs") roots.add(member);
1578
1681
  const walk = (dir, depth) => {
1579
1682
  if (depth > SPECS_DEPTH) return;
@@ -2219,7 +2322,7 @@ const CLOCK_READ = /'now'|\bCURRENT_(?:TIMESTAMP|DATE|TIME)\b|\bNOW\s*\(\s*\)/iu
2219
2322
  const SQL_LINE_COMMENT = /--[^\n]*/gu;
2220
2323
  const SQL_BLOCK_COMMENT = /\/\*[\s\S]*?\*\//gu;
2221
2324
  /** The file with its comments blanked out, line for line so a finding still points. */
2222
- function withoutComments(text) {
2325
+ function withoutComments$1(text) {
2223
2326
  return text.replaceAll(SQL_BLOCK_COMMENT, (block) => block.replaceAll(/[^\n]/gu, " ")).replaceAll(SQL_LINE_COMMENT, "");
2224
2327
  }
2225
2328
  /**
@@ -2394,7 +2497,7 @@ function checkSeedClockReads(path, rel, name) {
2394
2497
  const text = decodeText(path);
2395
2498
  if (text === null) return [];
2396
2499
  const violations = [];
2397
- for (const [index, line] of withoutComments(text).split("\n").entries()) {
2500
+ for (const [index, line] of withoutComments$1(text).split("\n").entries()) {
2398
2501
  const found = CLOCK_READ.exec(line);
2399
2502
  if (found === null) continue;
2400
2503
  violations.push({
@@ -2532,23 +2635,33 @@ function formatViolations(violations) {
2532
2635
  /**
2533
2636
  * CLI entry for the conventions checker (bundled as `dist/checker.js`).
2534
2637
  *
2535
- * node dist/checker.js [rootDir] [--fix] # a specs tree
2638
+ * node dist/checker.js [dir] [--fix] # a specs tree, or a project root
2536
2639
  * node dist/checker.js --format json # every root, every member
2537
2640
  * node dist/checker.js --member <dir> # one workspace member
2538
2641
  *
2539
- * Three runs, one reporter. With a PATH it walks that tree and runs every
2540
- * checker pass over it — the token/HTTP grammar (D4 / D4b / D10), the
2541
- * `<case>.spec.yaml` document conventions, and the cross-file passes (C9 dead
2642
+ * Three runs, one reporter. A PATH is read for what it IS. Name a specs tree
2643
+ * — a directory with a `specs` segment in its path — and it walks that tree
2644
+ * with every tree pass: the token/HTTP grammar (D4 / D4b / D10), the
2645
+ * `<case>.spec.yaml` document conventions, the cross-file passes (C9 dead
2542
2646
  * fixtures, C14/C15 fixture placement, B5 await-using inference, A7 database
2543
- * property). With `--member <dir>` it runs the member pass (E3 config-present,
2544
- * E5b no simulated DOM in a config, F8 no seam dependency) over one workspace
2545
- * member, whether or not that member has a `specs/` root.
2647
+ * property) and the facet passes (C12, C18, C20, C21w), which read the first
2648
+ * level of the tree and are the reason the anchor has to be right. Name
2649
+ * anything else and it is a PROJECT ROOT: the run is the path-less one,
2650
+ * anchored there. With `--member <dir>` it runs the member pass (E3
2651
+ * config-present, E5b no simulated DOM in a config, F8 no seam dependency)
2652
+ * over one workspace member, whether or not that member has a `specs/` root.
2546
2653
  *
2547
2654
  * With NO path it runs both over the whole project: every `specs/` root it
2548
2655
  * discovers from the root manifest's workspaces, and every member. That is the
2549
2656
  * run `@jterrazz/typescript`'s ratchet rests on, so the contract is exactly
2550
2657
  * "a path-less run reports what the per-root and per-member runs report".
2551
2658
  *
2659
+ * A NAMED root that turns out to hold no `specs/` tree at all is refused, loud
2660
+ * and non-zero. Pointed at a project root, the facet passes used to key on its
2661
+ * first level, find no facet folder there and report nothing — `<project>` and
2662
+ * `<project>/specs` disagreed on the same tree, and the run that reported
2663
+ * nothing was the one a CI line was likelier to be written with.
2664
+ *
2552
2665
  * `--format json` prints `[{ code, file, line, severity, message }]` on stdout
2553
2666
  * and nothing else, where `code` is `jterrazz-check(<id>)` — the namespace the
2554
2667
  * ratchet records under. Exit 1 on any ERROR-level violation either way;
@@ -2561,10 +2674,11 @@ function formatViolations(violations) {
2561
2674
  * fixes everything exits 0. With no path it applies them to every `specs/` root
2562
2675
  * it discovers: `--fix` means the same thing whichever run the operator typed.
2563
2676
  *
2564
- * After a C12 rename it NAMES every include glob of the owning member that
2565
- * still says `.test.ts`. A glob the rename left behind collects nothing, the
2566
- * run stays green with fewer files, and nothing else in the toolchain would
2567
- * have said a word.
2677
+ * After a C12 rename it NAMES the entries of the owning member's configs that
2678
+ * the rename BROKE — a glob whose prefix covers a file it moved, or a literal
2679
+ * naming one — and nothing else. An entry the rename left behind collects
2680
+ * nothing, the run stays green with fewer files, and nothing else in the
2681
+ * toolchain would have said a word.
2568
2682
  *
2569
2683
  * C12's rename goes through `git mv` where the tree is a git working tree, so
2570
2684
  * the history follows the file across a migration that touches hundreds of
@@ -2661,7 +2775,10 @@ function fixSpecSuffixes(root) {
2661
2775
  for (const { from, to } of movesUnderFacet(root)) {
2662
2776
  if (to === void 0 || existsSync(to)) continue;
2663
2777
  renameKeepingHistory(from, to);
2664
- renamed.push(`${relative(root, from)} → ${relative(root, to)}`);
2778
+ renamed.push({
2779
+ from,
2780
+ to
2781
+ });
2665
2782
  }
2666
2783
  return renamed;
2667
2784
  }
@@ -2674,53 +2791,145 @@ const CONFIG_NAMES = [
2674
2791
  "vitest.config.mjs",
2675
2792
  "vitest.config.cjs"
2676
2793
  ];
2677
- /** Any string in a config that ends in the suffix the mover just retired. */
2678
- const TEST_GLOB = /['"`](?<glob>[^'"`]*\*[^'"`]*\.test\.ts)['"`]/gu;
2794
+ /** What a config COLLECTS: the two keys whose entries name test files. */
2795
+ const COLLECTION = /\b(?:include|exclude)\s*:\s*\[(?<entries>[^\]]*)\]/gu;
2796
+ /** One quoted entry of such a list. */
2797
+ const ENTRY = /['"`](?<entry>[^'"`\n]*)['"`]/gu;
2798
+ /** The quotes a config's entries are written in. */
2799
+ const QUOTES = /* @__PURE__ */ new Set([
2800
+ "'",
2801
+ "\"",
2802
+ "`"
2803
+ ]);
2804
+ /** The index of the newline ending the `//` comment that starts at `index`. */
2805
+ function endOfLineComment(text, index) {
2806
+ const newline = text.indexOf("\n", index);
2807
+ return newline === -1 ? text.length : newline;
2808
+ }
2809
+ /** The index just past the block comment that starts at `index`. */
2810
+ function endOfBlockComment(text, index) {
2811
+ const close = text.indexOf("*/", index + 2);
2812
+ return close === -1 ? text.length : close + 2;
2813
+ }
2679
2814
  /**
2680
- * The include globs a rename leaves behind.
2815
+ * The config, with what is not code taken out.
2816
+ *
2817
+ * A notice that read a COMMENT as an include told signews-mobile to follow a
2818
+ * glob no project collected on. The scan is character by character because a
2819
+ * regex cannot tell the two apart: `specs/api/**` + `/*.test.ts` opens a block
2820
+ * comment to any pattern that does not know it is inside a string, and a `://`
2821
+ * opens a line comment to one that does not either.
2822
+ */
2823
+ function withoutComments(text) {
2824
+ let out = "";
2825
+ let quote = null;
2826
+ let index = 0;
2827
+ while (index < text.length) {
2828
+ const char = text[index] ?? "";
2829
+ const pair = text.slice(index, index + 2);
2830
+ if (quote !== null) {
2831
+ const escaped = char === "\\";
2832
+ out += escaped ? pair : char;
2833
+ quote = char === quote ? null : quote;
2834
+ index += escaped ? 2 : 1;
2835
+ } else if (pair === "//") {
2836
+ out += "\n";
2837
+ index = endOfLineComment(text, index);
2838
+ } else if (pair === "/*") index = endOfBlockComment(text, index);
2839
+ else {
2840
+ quote = QUOTES.has(char) ? char : null;
2841
+ out += char;
2842
+ index += 1;
2843
+ }
2844
+ }
2845
+ return out;
2846
+ }
2847
+ /**
2848
+ * Does this entry of a config sitting in `dir` reach `file`?
2849
+ *
2850
+ * A glob reaches what its literal PREFIX — everything before the first `*` —
2851
+ * contains; an entry with no `*` reaches the one file it names. Both are
2852
+ * resolved against the config's own directory, which is what vitest does.
2853
+ */
2854
+ function reaches(dir, entry, file) {
2855
+ const star = entry.indexOf("*");
2856
+ if (star === -1) return resolve(dir, entry) === file;
2857
+ const prefix = resolve(dir, entry.slice(0, star));
2858
+ return file === prefix || file.startsWith(`${prefix}/`);
2859
+ }
2860
+ /** Every entry of every `include`/`exclude` list a config states. */
2861
+ function collectedEntries(text) {
2862
+ const entries = [];
2863
+ for (const list of text.matchAll(COLLECTION)) for (const match of (list.groups?.entries ?? "").matchAll(ENTRY)) entries.push(match.groups?.entry ?? "");
2864
+ return entries;
2865
+ }
2866
+ /** What one broken entry says, glob or literal. */
2867
+ function noticeFor(where, entry) {
2868
+ return entry.includes("*") ? `${where}: ${entry} still names \`.test.ts\` — the files it collected are \`.spec.ts\` now, and the project collects nothing until the glob follows (C12)` : `${where}: ${entry} names a file this run renamed — it is \`${entry.slice(0, -8)}.spec.ts\` now, and the entry collects nothing until it follows (C12)`;
2869
+ }
2870
+ /** The entries of ONE config the renames broke. */
2871
+ function staleEntriesIn(path, owner, renamed) {
2872
+ const directory = dirname(path);
2873
+ return collectedEntries(withoutComments(readFileSync(path, "utf8"))).filter((entry) => entry.endsWith(".test.ts")).filter((entry) => renamed.some(({ from }) => reaches(directory, entry, from))).map((entry) => noticeFor(relative(owner, path), entry));
2874
+ }
2875
+ /**
2876
+ * The entries a rename leaves behind.
2681
2877
  *
2682
2878
  * A consumer that names its suffix in an `include` loses the whole suite the
2683
2879
  * moment the mover renames it: vitest collects fewer files, exits 0, and the
2684
2880
  * only trace is a number nobody compares. The mover owns that consequence —
2685
- * it cannot rewrite a config it does not parse, so it NAMES every glob that
2686
- * still says `.test.ts` in the configs of the member it just moved files
2687
- * under, and the author updates them in the same commit as the rename.
2688
- */
2689
- function staleIncludes(root) {
2881
+ * it cannot rewrite a config it does not parse, so it NAMES what it broke, and
2882
+ * the author updates it in the same commit as the rename.
2883
+ *
2884
+ * What it names is exactly what it broke: an entry of an `include`/`exclude`
2885
+ * list, never a comment, and only where the entry reaches a file this run
2886
+ * actually moved. A glob over `src/` collecting module tests the mover never
2887
+ * touched is not a stale glob, and four repositories of the wave were told it
2888
+ * was — one of them would have emptied its unit project by following the
2889
+ * notice.
2890
+ */
2891
+ function staleIncludes(root, renamed) {
2892
+ if (renamed.length === 0) return [];
2690
2893
  const owner = packageRootOf(root) ?? dirname(root);
2691
- const found = [];
2692
- for (const directory of /* @__PURE__ */ new Set([
2894
+ return [.../* @__PURE__ */ new Set([
2693
2895
  root,
2694
2896
  owner,
2695
2897
  dirname(root)
2696
- ])) for (const name of CONFIG_NAMES) {
2697
- const path = join(directory, name);
2698
- if (!existsSync(path)) continue;
2699
- const text = readFileSync(path, "utf8");
2700
- for (const match of text.matchAll(TEST_GLOB)) {
2701
- const glob = match.groups?.glob ?? "";
2702
- if (glob !== "") found.push(`${relative(owner, path)}: ${glob}`);
2703
- }
2704
- }
2705
- return found;
2898
+ ])].flatMap((directory) => CONFIG_NAMES.map((name) => join(directory, name)).filter((path) => existsSync(path))).flatMap((path) => staleEntriesIn(path, owner, renamed));
2706
2899
  }
2707
2900
  /** Apply every rewritable pass over one tree, printing what it did. */
2708
2901
  function applyFixes(root) {
2709
2902
  const renamed = fixSpecSuffixes(root);
2710
- for (const rename of renamed) console.log(`conventions checker: renamed ${rename} (C12)`);
2711
- for (const glob of renamed.length === 0 ? [] : staleIncludes(root)) console.log(`conventions checker: ${glob} still names \`.test.ts\` — the files it collected are \`.spec.ts\` now, and the project collects nothing until the glob follows (C12)`);
2903
+ for (const { from, to } of renamed) console.log(`conventions checker: renamed ${relative(root, from)} → ${relative(root, to)} (C12)`);
2904
+ for (const stale of staleIncludes(root, renamed)) console.log(`conventions checker: ${stale}`);
2712
2905
  const written = fixSpecFiles(root);
2713
2906
  if (written.length > 0) console.log(`conventions checker: rewrote ${written.length} spec document(s)`);
2714
2907
  for (const move of fixPoolFixtures(root)) console.log(`conventions checker: moved ${move} (C14) — stage the rename`);
2715
2908
  }
2716
- if (positional !== void 0) {
2909
+ /**
2910
+ * Is this directory a specs tree?
2911
+ *
2912
+ * The `specs` SEGMENT is the anchor every other pass reads (`specsAnchor`), so
2913
+ * the CLI reads it too: `specs`, `apps/web/specs` and `specs/api` are trees —
2914
+ * a subtree of one is still inside it — and everything else is a project root
2915
+ * holding zero or more of them.
2916
+ */
2917
+ function isSpecsTree(dir) {
2918
+ return dir.split(/[/\\]/u).includes("specs");
2919
+ }
2920
+ if (positional !== void 0 && isSpecsTree(requireDirectory(positional, "directory"))) {
2717
2921
  const root = requireDirectory(positional, "directory");
2718
2922
  if (fix) applyFixes(root);
2719
2923
  report(runAllChecks(root), `under ${root}`, TREE_PASSES);
2720
2924
  }
2721
- const root = resolve(".");
2925
+ const root = positional === void 0 ? resolve(".") : requireDirectory(positional, "directory");
2926
+ const specsRoots = discoverSpecRoots(root);
2927
+ if (positional !== void 0 && specsRoots.length === 0) {
2928
+ console.error(`conventions checker: no \`specs/\` tree under ${root} — name a specs tree, or a project root that holds one`);
2929
+ process.exit(1);
2930
+ }
2722
2931
  const found = [];
2723
- for (const specsRoot of discoverSpecRoots(root)) {
2932
+ for (const specsRoot of specsRoots) {
2724
2933
  const prefix = relative(root, specsRoot);
2725
2934
  if (fix) applyFixes(specsRoot);
2726
2935
  for (const violation of runAllChecks(specsRoot)) found.push({
package/dist/index.d.ts CHANGED
@@ -707,8 +707,25 @@ type CallResultOptions = BaseResultOptions & {
707
707
  * What `result.value` is, decided by what the call returned: a string reads as
708
708
  * a stream, anything else as JSON. `.call<T>()` already knows T, so the test
709
709
  * reads one field (`result.value.value.ok`) without narrowing a union first.
710
+ *
711
+ * Three answers, because a type has three ways of relating to `string`:
712
+ *
713
+ * - EXACTLY a string — a `TextAccessor`, and nothing to narrow;
714
+ * - it MAY be a string (`string | undefined`, `string | number`, `any`) — the
715
+ * UNION, which the test narrows. The type used to read the static type only
716
+ * while the accessor was built from the RUNTIME value, so a subject typed
717
+ * `string | undefined` was handed back as a `JsonAccessor`: `.value.value`
718
+ * compiled, the runtime had built a `TextAccessor`, and the read silently
719
+ * answered `undefined`;
720
+ * - never a string — a `JsonAccessor<Returned>`.
721
+ *
722
+ * `Returned` there is what the value CLAIMS, not what survived the crossing:
723
+ * the accessor is built from `JSON.stringify`, so a `Map`, a `Date` or a class
724
+ * instance arrives as the plain JSON it serialises to (`{}`, an ISO string, an
725
+ * object of its own fields). A subject whose answer is one of those is
726
+ * projected to plain data inside the call, where the test can see it happen.
710
727
  */
711
- type CallValue<Returned> = Returned extends string ? TextAccessor : JsonAccessor<Returned>;
728
+ type CallValue<Returned> = [Returned] extends [string] ? TextAccessor : [Extract<Returned, string>] extends [never] ? JsonAccessor<Returned> : JsonAccessor<Exclude<Returned, string>> | TextAccessor;
712
729
  /**
713
730
  * The result of an in-process call — the integration facet's whole surface.
714
731
  *
@@ -741,10 +758,14 @@ declare class CallResult<Returned = unknown> extends BaseResult {
741
758
  /**
742
759
  * What the call RETURNED. A string reads as a stream (`<name>.txt`),
743
760
  * anything else as JSON (`<name>.json`) — the same two subjects every
744
- * other facet's results are built from.
761
+ * other facet's results are built from, chosen on the value in hand and
762
+ * typed by {@link CallValue}, which answers for the three ways a type can
763
+ * relate to `string`.
745
764
  *
746
- * A call that threw returns the empty value: what it produced is
747
- * {@link CallResult.error}.
765
+ * A call that threw has no value to choose an accessor on, so `value` is
766
+ * the JSON reading of `null` whatever the declared type promises: what a
767
+ * refusal produced is {@link CallResult.error}, and that is where a spec
768
+ * reads it.
748
769
  */
749
770
  get value(): CallValue<Returned>;
750
771
  }
@@ -2883,6 +2904,14 @@ type WebsiteSpecificationOptions<Services extends ServiceRecord = ServiceRecord>
2883
2904
  * API it was started beside.
2884
2905
  */
2885
2906
  services?: Services;
2907
+ /**
2908
+ * Escape hatch: normaliser applied to every compared reading of a result
2909
+ * — the head projection and the json-ld among them — before the
2910
+ * comparison (CONVENTIONS D6). Does NOT mutate the raw `.text` accessor.
2911
+ * Prefer `{{token}}` placeholders in fixtures; this is for what a token
2912
+ * cannot name, a meta a FRAMEWORK owns and the page never asked for.
2913
+ */
2914
+ transform?: ((text: string) => string) | undefined;
2886
2915
  } & ({
2887
2916
  /**
2888
2917
  * Declared stub backend: started BEFORE the server command, its