@gate-forge/pack-playwright 0.7.1 → 0.9.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 (205) hide show
  1. package/README.md +31 -0
  2. package/dist/constants.d.ts +13 -140
  3. package/dist/constants.d.ts.map +1 -1
  4. package/dist/constants.js +13 -148
  5. package/dist/constants.js.map +1 -1
  6. package/dist/cypress/plugin.d.ts +53 -0
  7. package/dist/cypress/plugin.d.ts.map +1 -0
  8. package/dist/cypress/plugin.js +369 -0
  9. package/dist/cypress/plugin.js.map +1 -0
  10. package/dist/cypress/spec-scan.d.ts +39 -0
  11. package/dist/cypress/spec-scan.d.ts.map +1 -0
  12. package/dist/cypress/spec-scan.js +301 -0
  13. package/dist/cypress/spec-scan.js.map +1 -0
  14. package/dist/diagnosis.d.ts +39 -0
  15. package/dist/diagnosis.d.ts.map +1 -0
  16. package/dist/diagnosis.js +63 -0
  17. package/dist/diagnosis.js.map +1 -0
  18. package/dist/discovery/adapter-projection.d.ts +13 -0
  19. package/dist/discovery/adapter-projection.d.ts.map +1 -0
  20. package/dist/discovery/adapter-projection.js +84 -0
  21. package/dist/discovery/adapter-projection.js.map +1 -0
  22. package/dist/discovery/config-locations.d.ts +5 -0
  23. package/dist/discovery/config-locations.d.ts.map +1 -0
  24. package/dist/discovery/config-locations.js +19 -0
  25. package/dist/discovery/config-locations.js.map +1 -0
  26. package/dist/discovery/cypress-runner-adapter.d.ts +112 -0
  27. package/dist/discovery/cypress-runner-adapter.d.ts.map +1 -0
  28. package/dist/discovery/cypress-runner-adapter.js +501 -0
  29. package/dist/discovery/cypress-runner-adapter.js.map +1 -0
  30. package/dist/discovery/discover.d.ts +53 -1
  31. package/dist/discovery/discover.d.ts.map +1 -1
  32. package/dist/discovery/discover.js +290 -30
  33. package/dist/discovery/discover.js.map +1 -1
  34. package/dist/discovery/git-ignore.d.ts +13 -0
  35. package/dist/discovery/git-ignore.d.ts.map +1 -0
  36. package/dist/discovery/git-ignore.js +62 -0
  37. package/dist/discovery/git-ignore.js.map +1 -0
  38. package/dist/discovery/index.d.ts +10 -0
  39. package/dist/discovery/index.d.ts.map +1 -1
  40. package/dist/discovery/index.js +10 -0
  41. package/dist/discovery/index.js.map +1 -1
  42. package/dist/discovery/playwright-runner-adapter.d.ts +108 -0
  43. package/dist/discovery/playwright-runner-adapter.d.ts.map +1 -0
  44. package/dist/discovery/playwright-runner-adapter.js +185 -0
  45. package/dist/discovery/playwright-runner-adapter.js.map +1 -0
  46. package/dist/discovery/prepare-barrier.d.ts +522 -0
  47. package/dist/discovery/prepare-barrier.d.ts.map +1 -0
  48. package/dist/discovery/prepare-barrier.js +739 -0
  49. package/dist/discovery/prepare-barrier.js.map +1 -0
  50. package/dist/discovery/pytest-adapter.d.ts.map +1 -1
  51. package/dist/discovery/pytest-adapter.js +6 -2
  52. package/dist/discovery/pytest-adapter.js.map +1 -1
  53. package/dist/discovery/pytest-runner-adapter.d.ts +124 -0
  54. package/dist/discovery/pytest-runner-adapter.d.ts.map +1 -0
  55. package/dist/discovery/pytest-runner-adapter.js +454 -0
  56. package/dist/discovery/pytest-runner-adapter.js.map +1 -0
  57. package/dist/discovery/reconcile.d.ts +89 -11
  58. package/dist/discovery/reconcile.d.ts.map +1 -1
  59. package/dist/discovery/reconcile.js +440 -78
  60. package/dist/discovery/reconcile.js.map +1 -1
  61. package/dist/discovery/runner-env.d.ts +88 -2
  62. package/dist/discovery/runner-env.d.ts.map +1 -1
  63. package/dist/discovery/runner-env.js +142 -4
  64. package/dist/discovery/runner-env.js.map +1 -1
  65. package/dist/discovery/runner-file-scope.d.ts +114 -0
  66. package/dist/discovery/runner-file-scope.d.ts.map +1 -0
  67. package/dist/discovery/runner-file-scope.js +638 -0
  68. package/dist/discovery/runner-file-scope.js.map +1 -0
  69. package/dist/discovery/static-discovery.d.ts +49 -3
  70. package/dist/discovery/static-discovery.d.ts.map +1 -1
  71. package/dist/discovery/static-discovery.js +477 -18
  72. package/dist/discovery/static-discovery.js.map +1 -1
  73. package/dist/discovery/supervised-run.d.ts +199 -10
  74. package/dist/discovery/supervised-run.d.ts.map +1 -1
  75. package/dist/discovery/supervised-run.js +350 -39
  76. package/dist/discovery/supervised-run.js.map +1 -1
  77. package/dist/discovery/trusted-config.d.ts +180 -6
  78. package/dist/discovery/trusted-config.d.ts.map +1 -1
  79. package/dist/discovery/trusted-config.js +279 -10
  80. package/dist/discovery/trusted-config.js.map +1 -1
  81. package/dist/discovery/vitest-runner-adapter.d.ts +172 -0
  82. package/dist/discovery/vitest-runner-adapter.d.ts.map +1 -0
  83. package/dist/discovery/vitest-runner-adapter.js +607 -0
  84. package/dist/discovery/vitest-runner-adapter.js.map +1 -0
  85. package/dist/fixture/consumer-runner.d.ts +4 -0
  86. package/dist/fixture/consumer-runner.d.ts.map +1 -0
  87. package/dist/fixture/consumer-runner.js +38 -0
  88. package/dist/fixture/consumer-runner.js.map +1 -0
  89. package/dist/fixture/evidence.d.ts +27 -0
  90. package/dist/fixture/evidence.d.ts.map +1 -1
  91. package/dist/fixture/evidence.js +12 -0
  92. package/dist/fixture/evidence.js.map +1 -1
  93. package/dist/fixture/fixture.d.ts +2 -1
  94. package/dist/fixture/fixture.d.ts.map +1 -1
  95. package/dist/fixture/fixture.js +1 -17
  96. package/dist/fixture/fixture.js.map +1 -1
  97. package/dist/fixture/witness-client.d.ts +6 -174
  98. package/dist/fixture/witness-client.d.ts.map +1 -1
  99. package/dist/fixture/witness-client.js +6 -267
  100. package/dist/fixture/witness-client.js.map +1 -1
  101. package/dist/index.d.ts +9 -6
  102. package/dist/index.d.ts.map +1 -1
  103. package/dist/index.js +5 -4
  104. package/dist/index.js.map +1 -1
  105. package/dist/json.d.ts +6 -12
  106. package/dist/json.d.ts.map +1 -1
  107. package/dist/json.js +6 -17
  108. package/dist/json.js.map +1 -1
  109. package/dist/reporter/project-graph-reporter.d.ts +121 -0
  110. package/dist/reporter/project-graph-reporter.d.ts.map +1 -0
  111. package/dist/reporter/project-graph-reporter.js +132 -0
  112. package/dist/reporter/project-graph-reporter.js.map +1 -0
  113. package/dist/reporter/reporter.cjs.map +1 -1
  114. package/dist/reporter/reporter.d.ts +93 -4
  115. package/dist/reporter/reporter.d.ts.map +1 -1
  116. package/dist/reporter/reporter.js +174 -18
  117. package/dist/reporter/reporter.js.map +1 -1
  118. package/dist/runner-claims.d.ts +13 -0
  119. package/dist/runner-claims.d.ts.map +1 -0
  120. package/dist/runner-claims.js +46 -0
  121. package/dist/runner-claims.js.map +1 -0
  122. package/dist/runner-resolution.d.ts +3 -0
  123. package/dist/runner-resolution.d.ts.map +1 -0
  124. package/dist/runner-resolution.js +14 -0
  125. package/dist/runner-resolution.js.map +1 -0
  126. package/dist/supervisor/client.d.ts +23 -1
  127. package/dist/supervisor/client.d.ts.map +1 -1
  128. package/dist/supervisor/client.js +49 -0
  129. package/dist/supervisor/client.js.map +1 -1
  130. package/dist/supervisor/drain.d.ts +65 -0
  131. package/dist/supervisor/drain.d.ts.map +1 -1
  132. package/dist/supervisor/drain.js +353 -9
  133. package/dist/supervisor/drain.js.map +1 -1
  134. package/dist/supervisor/index.d.ts +7 -3
  135. package/dist/supervisor/index.d.ts.map +1 -1
  136. package/dist/supervisor/index.js +7 -3
  137. package/dist/supervisor/index.js.map +1 -1
  138. package/dist/supervisor/spool.d.ts +65 -2
  139. package/dist/supervisor/spool.d.ts.map +1 -1
  140. package/dist/supervisor/spool.js +47 -0
  141. package/dist/supervisor/spool.js.map +1 -1
  142. package/dist/surface.d.ts +6 -207
  143. package/dist/surface.d.ts.map +1 -1
  144. package/dist/surface.js +6 -264
  145. package/dist/surface.js.map +1 -1
  146. package/dist/vitest/index.d.ts +23 -0
  147. package/dist/vitest/index.d.ts.map +1 -0
  148. package/dist/vitest/index.js +356 -0
  149. package/dist/vitest/index.js.map +1 -0
  150. package/dist/vitest/reporter.d.ts +83 -0
  151. package/dist/vitest/reporter.d.ts.map +1 -0
  152. package/dist/vitest/reporter.js +211 -0
  153. package/dist/vitest/reporter.js.map +1 -0
  154. package/dist/vitest/worker-slot.d.ts +36 -0
  155. package/dist/vitest/worker-slot.d.ts.map +1 -0
  156. package/dist/vitest/worker-slot.js +51 -0
  157. package/dist/vitest/worker-slot.js.map +1 -0
  158. package/dist/witness/adapter-registry.d.ts +6 -50
  159. package/dist/witness/adapter-registry.d.ts.map +1 -1
  160. package/dist/witness/adapter-registry.js +6 -257
  161. package/dist/witness/adapter-registry.js.map +1 -1
  162. package/dist/witness/behavior-request.d.ts +6 -76
  163. package/dist/witness/behavior-request.d.ts.map +1 -1
  164. package/dist/witness/behavior-request.js +6 -273
  165. package/dist/witness/behavior-request.js.map +1 -1
  166. package/dist/witness/behavior.d.ts +6 -48
  167. package/dist/witness/behavior.d.ts.map +1 -1
  168. package/dist/witness/behavior.js +6 -114
  169. package/dist/witness/behavior.js.map +1 -1
  170. package/dist/witness/bin.d.ts +6 -17
  171. package/dist/witness/bin.d.ts.map +1 -1
  172. package/dist/witness/bin.js +6 -168
  173. package/dist/witness/bin.js.map +1 -1
  174. package/dist/witness/browser.d.ts +14 -206
  175. package/dist/witness/browser.d.ts.map +1 -1
  176. package/dist/witness/browser.js +15 -654
  177. package/dist/witness/browser.js.map +1 -1
  178. package/dist/witness/classifications.d.ts +6 -38
  179. package/dist/witness/classifications.d.ts.map +1 -1
  180. package/dist/witness/classifications.js +6 -83
  181. package/dist/witness/classifications.js.map +1 -1
  182. package/dist/witness/env-attestation.d.ts +6 -94
  183. package/dist/witness/env-attestation.d.ts.map +1 -1
  184. package/dist/witness/env-attestation.js +6 -198
  185. package/dist/witness/env-attestation.js.map +1 -1
  186. package/dist/witness/fixture-provider.d.ts +6 -82
  187. package/dist/witness/fixture-provider.d.ts.map +1 -1
  188. package/dist/witness/fixture-provider.js +6 -105
  189. package/dist/witness/fixture-provider.js.map +1 -1
  190. package/dist/witness/loopback-pins.d.ts +6 -75
  191. package/dist/witness/loopback-pins.d.ts.map +1 -1
  192. package/dist/witness/loopback-pins.js +6 -240
  193. package/dist/witness/loopback-pins.js.map +1 -1
  194. package/dist/witness/server.d.ts +8 -26
  195. package/dist/witness/server.d.ts.map +1 -1
  196. package/dist/witness/server.js +24 -3761
  197. package/dist/witness/server.js.map +1 -1
  198. package/dist/witness/types.d.ts +6 -882
  199. package/dist/witness/types.d.ts.map +1 -1
  200. package/dist/witness/types.js +9 -1
  201. package/dist/witness/types.js.map +1 -1
  202. package/examples/overlay-proof.spec.js +75 -0
  203. package/package.json +12 -3
  204. package/python/gateforge_pytest_plugin.py +462 -0
  205. package/python/__pycache__/gateforge_persistence_intents.cpython-314.pyc +0 -0
@@ -33,6 +33,8 @@ import { readFileSync, existsSync, readdirSync, statSync } from 'node:fs';
33
33
  import { join, posix } from 'node:path';
34
34
  import ts from 'typescript';
35
35
  import { pathInScope } from '@gate-forge/core';
36
+ import { CLAIM_ANNOTATION_TYPE } from '../constants.js';
37
+ import { gitIgnoredPaths } from './git-ignore.js';
36
38
  /** Default cap on files pulled in through import traversal. */
37
39
  export const DEFAULT_MAX_TRAVERSED_FILES = 200;
38
40
  /** Default bound on import-traversal depth (chain length). */
@@ -125,12 +127,127 @@ function locationOf(file, source, node) {
125
127
  const { line, character } = source.getLineAndCharacterOfPosition(node.getStart(source));
126
128
  return { file, line: line + 1, col: character };
127
129
  }
130
+ /**
131
+ * Tests whether a syntax node is nested inside another node's subtree.
132
+ *
133
+ * Args:
134
+ * container: candidate ancestor node.
135
+ * target: node whose ancestry is checked.
136
+ *
137
+ * Returns:
138
+ * boolean: true when target is inside container.
139
+ */
140
+ function isWithinNode(container, target) {
141
+ let current = target;
142
+ while (current !== undefined && current !== container)
143
+ current = current.parent;
144
+ return current === container;
145
+ }
146
+ /**
147
+ * Finds Gateforge environment reads in conditional ancestors of a test
148
+ * registration so environment-dependent registrations can be reviewed.
149
+ *
150
+ * Args:
151
+ * file: repo-relative file containing the registration.
152
+ * source: parsed TypeScript source file.
153
+ * node: registration call node.
154
+ * titlePath: static title path for the registration.
155
+ *
156
+ * Returns:
157
+ * StaticRegistrationWarning[]: one warning per relevant environment variable.
158
+ */
159
+ function registrationWarningsForCall(file, source, node, titlePath) {
160
+ const guards = [];
161
+ for (let parent = node.parent; parent !== undefined; parent = parent.parent) {
162
+ if (ts.isIfStatement(parent) &&
163
+ (isWithinNode(parent.thenStatement, node) ||
164
+ (parent.elseStatement !== undefined && isWithinNode(parent.elseStatement, node)))) {
165
+ guards.push(parent.expression);
166
+ }
167
+ else if (ts.isConditionalExpression(parent) &&
168
+ (isWithinNode(parent.whenTrue, node) || isWithinNode(parent.whenFalse, node))) {
169
+ guards.push(parent.condition);
170
+ }
171
+ else if (ts.isBinaryExpression(parent) &&
172
+ isWithinNode(parent.right, node) &&
173
+ (parent.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken ||
174
+ parent.operatorToken.kind === ts.SyntaxKind.BarBarToken ||
175
+ parent.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken)) {
176
+ guards.push(parent.left);
177
+ }
178
+ }
179
+ if (guards.length === 0)
180
+ return [];
181
+ const variables = new Map();
182
+ /**
183
+ * Recognizes property or literal-index access to process.env.
184
+ *
185
+ * Args:
186
+ * expression: expression to inspect.
187
+ *
188
+ * Returns:
189
+ * boolean: true when expression is process.env.
190
+ */
191
+ const isProcessEnv = (expression) => (ts.isPropertyAccessExpression(expression) &&
192
+ ts.isIdentifier(expression.expression) &&
193
+ expression.expression.text === 'process' &&
194
+ expression.name.text === 'env') ||
195
+ (ts.isElementAccessExpression(expression) &&
196
+ ts.isIdentifier(expression.expression) &&
197
+ expression.expression.text === 'process' &&
198
+ expression.argumentExpression !== undefined &&
199
+ ts.isStringLiteral(expression.argumentExpression) &&
200
+ expression.argumentExpression.text === 'env');
201
+ /**
202
+ * Records Gateforge environment reads from one registration guard.
203
+ *
204
+ * Args:
205
+ * guard: conditional expression to scan.
206
+ *
207
+ * Returns:
208
+ * void.
209
+ */
210
+ const visitGuard = (guard) => {
211
+ if (ts.isPropertyAccessExpression(guard) && isProcessEnv(guard.expression)) {
212
+ const name = guard.name.text;
213
+ if (name.startsWith('GATEFORGE_') && !variables.has(name)) {
214
+ variables.set(name, locationOf(file, source, guard));
215
+ }
216
+ }
217
+ else if (ts.isElementAccessExpression(guard) &&
218
+ isProcessEnv(guard.expression) &&
219
+ guard.argumentExpression !== undefined &&
220
+ ts.isStringLiteral(guard.argumentExpression)) {
221
+ const name = guard.argumentExpression.text;
222
+ if (name.startsWith('GATEFORGE_') && !variables.has(name)) {
223
+ variables.set(name, locationOf(file, source, guard));
224
+ }
225
+ }
226
+ ts.forEachChild(guard, visitGuard);
227
+ };
228
+ for (const guard of guards)
229
+ visitGuard(guard);
230
+ return [...variables.entries()].map(([environmentVariable, location]) => ({
231
+ file,
232
+ titlePath: [...titlePath],
233
+ environmentVariable,
234
+ location,
235
+ }));
236
+ }
128
237
  /**
129
238
  * Walks the repo (pruning VCS/deps/build dirs) and returns the sorted
130
- * repo-relative posix files matching the include globs and no exclude
131
- * glob. Pure filesystem listing — no content is read here.
239
+ * repo-relative posix files matching the include globs, no exclude glob,
240
+ * and no git ignore rule. Pure filesystem listing — no content is read
241
+ * here.
242
+ *
243
+ * A path the repository ignores (e.g. the built, git-ignored bundle a
244
+ * frontend build writes into the tree) can never be enumerated by the
245
+ * runner, so a row from it would be a permanent `unenumeratedReason`
246
+ * gap. Where git cannot answer — outside a work tree, or git
247
+ * unavailable — every candidate is kept exactly as before
248
+ * (see {@link gitIgnoredPaths}).
132
249
  */
133
- function collectCandidateFiles(cwd, include, exclude) {
250
+ function collectCandidateFiles(cwd, include, exclude, excludeFile) {
134
251
  const found = [];
135
252
  const walk = (dir, rel) => {
136
253
  let names;
@@ -157,13 +274,17 @@ function collectCandidateFiles(cwd, include, exclude) {
157
274
  }
158
275
  else if (PARSEABLE_EXTENSIONS.some((ext) => name.endsWith(ext)) &&
159
276
  pathInScope(relPath, include) &&
160
- !pathInScope(relPath, exclude)) {
277
+ !pathInScope(relPath, exclude) &&
278
+ excludeFile?.(relPath) !== true) {
161
279
  found.push(relPath);
162
280
  }
163
281
  }
164
282
  };
165
283
  walk(cwd, '');
166
- return found.sort();
284
+ const ignored = gitIgnoredPaths(cwd, found);
285
+ if (ignored === null)
286
+ return found.sort();
287
+ return found.filter((file) => !ignored.has(file)).sort();
167
288
  }
168
289
  /** The callee shape `{base, names}` of `a.b.c(...)`, else null. */
169
290
  function calleeChain(expression) {
@@ -206,10 +327,19 @@ function bodyReferencesTest(body) {
206
327
  visit(body);
207
328
  return found;
208
329
  }
209
- /** Reads a file's text; unreadable content is a parse error row. */
330
+ /**
331
+ * Reads a file's text; unreadable content is a parse error row. A path
332
+ * that is not a regular file (a directory that reached the read through
333
+ * a seeded or traversed row) is skipped silently — a directory is not
334
+ * a parse failure of the consumer's source, and reporting one used to
335
+ * flip `inventoryComplete` on a repo that parses fine.
336
+ */
210
337
  function readText(state, file) {
338
+ const absolute = join(state.cwd, file);
339
+ if (!isFile(absolute))
340
+ return null;
211
341
  try {
212
- return readFileSync(join(state.cwd, file), 'utf8');
342
+ return readFileSync(absolute, 'utf8');
213
343
  }
214
344
  catch (error) {
215
345
  state.result.parseErrors.push({
@@ -539,7 +669,25 @@ function isPlaywrightSpecifier(specifier) {
539
669
  function isTestModuleSpecifier(specifier) {
540
670
  return isPlaywrightSpecifier(specifier) || isPackSpecifier(specifier);
541
671
  }
542
- /** Resolves a relative specifier against the repo (candidate extensions). */
672
+ /**
673
+ * Resolves a relative specifier the way node/TS resolve it: a FILE
674
+ * first (the bare path when it names one, then each parseable
675
+ * extension), then a directory's `index.*`. A directory is never a
676
+ * resolution result — `existsSync` is true for one, and returning it
677
+ * made `readText` fail with EISDIR and report a false parse error
678
+ * against the consumer's own source whenever a file and a directory
679
+ * shared a name (install rehearsal F16).
680
+ *
681
+ * Args:
682
+ * state: the running scan (seeded/traversed files win outright).
683
+ * cwd: absolute repo root.
684
+ * fromFile: repo-relative file holding the import.
685
+ * specifier: the import specifier, as written.
686
+ *
687
+ * Returns:
688
+ * string | null: repo-relative FILE path, or null when nothing
689
+ * resolves.
690
+ */
543
691
  function resolveSpecifier(state, cwd, fromFile, specifier) {
544
692
  const base = posix.dirname(fromFile);
545
693
  const joined = posix.normalize(posix.join(base, specifier));
@@ -549,12 +697,33 @@ function resolveSpecifier(state, cwd, fromFile, specifier) {
549
697
  ...PARSEABLE_EXTENSIONS.map((ext) => `${joined}/index${ext}`),
550
698
  ];
551
699
  for (const candidate of candidates) {
552
- if (state.seeded.has(candidate) || state.traversed.has(candidate) || existsSync(join(cwd, candidate))) {
700
+ if (state.seeded.has(candidate) || state.traversed.has(candidate))
701
+ return candidate;
702
+ }
703
+ for (const candidate of candidates) {
704
+ if (isFile(join(cwd, candidate)))
553
705
  return candidate;
554
- }
555
706
  }
556
707
  return null;
557
708
  }
709
+ /**
710
+ * Whether an absolute path is an existing regular file (never a
711
+ * directory, a socket, or a device node).
712
+ *
713
+ * Args:
714
+ * absolute: absolute filesystem path.
715
+ *
716
+ * Returns:
717
+ * boolean: true only for an existing regular file.
718
+ */
719
+ function isFile(absolute) {
720
+ try {
721
+ return statSync(absolute).isFile();
722
+ }
723
+ catch {
724
+ return false;
725
+ }
726
+ }
558
727
  /**
559
728
  * Resolves whether `name` in `file` binds to a playwright test function,
560
729
  * through aliases and relative imports, bounded by depth + file budget.
@@ -696,6 +865,34 @@ function findPageRoute(node, source, file) {
696
865
  visit(node);
697
866
  return found;
698
867
  }
868
+ /**
869
+ * Collects literal URL patterns intercepted by `page.route` in one test.
870
+ *
871
+ * Args:
872
+ * node: the test callback subtree.
873
+ *
874
+ * Returns:
875
+ * string[]: unique, sorted static route patterns.
876
+ */
877
+ function pageRouteTargets(node) {
878
+ const targets = new Set();
879
+ const visit = (current) => {
880
+ if (ts.isCallExpression(current) &&
881
+ ts.isPropertyAccessExpression(current.expression) &&
882
+ current.expression.name.text === 'route' &&
883
+ ts.isIdentifier(current.expression.expression)) {
884
+ const target = current.arguments[0];
885
+ if (target !== undefined &&
886
+ ts.isStringLiteralLike(target) &&
887
+ target.text.length > 0) {
888
+ targets.add(target.text);
889
+ }
890
+ }
891
+ ts.forEachChild(current, visit);
892
+ };
893
+ visit(node);
894
+ return [...targets].sort();
895
+ }
699
896
  /** Whether the subtree calls fetch/axios (the app's HTTP boundary). */
700
897
  function findHttpClientCall(node, source, file) {
701
898
  let found = null;
@@ -769,22 +966,30 @@ function findModuleMock(source, file) {
769
966
  * options: cwd, include/exclude globs, and optional budgets.
770
967
  *
771
968
  * Returns:
772
- * StaticScanResult: entries, unresolved rows, parse errors, scanned
773
- * files, and the budget flag. Never throws for scanner-detectable
774
- * problems — those are rows (fail closed as data, not silence).
969
+ * StaticScanResult: entries, unresolved rows, parse errors, registration
970
+ * warnings, scanned files, and the budget flag. Never throws for
971
+ * scanner-detectable problems — those are rows (fail closed as data, not silence).
775
972
  */
776
973
  export function scanTestFiles(options) {
777
974
  const state = {
778
975
  cwd: options.cwd,
779
- result: { entries: [], unresolved: [], parseErrors: [], scannedFiles: [], budgetExceeded: false },
976
+ result: {
977
+ entries: [],
978
+ unresolved: [],
979
+ parseErrors: [],
980
+ registrationWarnings: [],
981
+ scannedFiles: [],
982
+ budgetExceeded: false,
983
+ },
780
984
  models: new Map(),
781
985
  seeded: new Set(),
782
986
  traversed: new Set(),
783
987
  resolving: new Set(),
784
988
  maxTraversedFiles: options.budget?.maxTraversedFiles ?? DEFAULT_MAX_TRAVERSED_FILES,
785
989
  maxImportDepth: options.budget?.maxImportDepth ?? DEFAULT_MAX_IMPORT_DEPTH,
990
+ testGlobals: options.testGlobals ?? (() => false),
786
991
  };
787
- const seededFiles = collectCandidateFiles(options.cwd, options.include, options.exclude);
992
+ const seededFiles = collectCandidateFiles(options.cwd, options.include, options.exclude, options.excludeFile);
788
993
  for (const file of seededFiles)
789
994
  state.seeded.add(file);
790
995
  for (const file of seededFiles) {
@@ -796,6 +1001,10 @@ export function scanTestFiles(options) {
796
1001
  const gateforgeImport = findGateforgeFixtureImport(model.source, file);
797
1002
  scanFileForTests(state, options.cwd, model, fileHttpClient, fileMock, gateforgeImport);
798
1003
  }
1004
+ state.result.registrationWarnings.sort((a, b) => (a.file < b.file ? -1 : a.file > b.file ? 1 : 0) ||
1005
+ a.location.line - b.location.line ||
1006
+ (a.environmentVariable < b.environmentVariable ? -1 : a.environmentVariable > b.environmentVariable ? 1 : 0) ||
1007
+ (a.titlePath.join('>') < b.titlePath.join('>') ? -1 : a.titlePath.join('>') > b.titlePath.join('>') ? 1 : 0));
799
1008
  state.result.entries.sort((a, b) => compareEntry(a, b));
800
1009
  state.result.unresolved.sort((a, b) => (a.file < b.file ? -1 : a.file > b.file ? 1 : 0) ||
801
1010
  a.location.line - b.location.line ||
@@ -813,6 +1022,9 @@ function compareEntry(a, b) {
813
1022
  /** Visits one modeled file, harvesting describe stacks and test calls. */
814
1023
  function scanFileForTests(state, cwd, model, fileHttpClient, fileMock, gateforgeImport) {
815
1024
  const { file, source } = model;
1025
+ // Once per file, not once per call site: the runner-globals predicate
1026
+ // matches globs, and a file has hundreds of call expressions.
1027
+ const runnerGlobals = state.testGlobals(file);
816
1028
  const visit = (node, describeStack, enclosing) => {
817
1029
  if (!ts.isCallExpression(node)) {
818
1030
  ts.forEachChild(node, (child) => visit(child, describeStack, enclosing));
@@ -826,7 +1038,13 @@ function scanFileForTests(state, cwd, model, fileHttpClient, fileMock, gateforge
826
1038
  const location = locationOf(file, source, node);
827
1039
  const resolution = resolveTestAlias(state, cwd, file, chain.base, 0);
828
1040
  const names = chain.names;
829
- const isDescribe = names.includes('describe');
1041
+ // A runner that injects the test GLOBALS for this file (vitest
1042
+ // `globals: true`) owns a bare `describe(...)` exactly as it owns a
1043
+ // bare `it(...)`: the runner itself would execute this suite. Without
1044
+ // it, an import-less describe reads as an unresolvable alias and the
1045
+ // whole file collapses into gaps.
1046
+ const isDescribe = names.includes('describe') ||
1047
+ (resolution === 'unknown' && names.length === 0 && chain.base === 'describe' && runnerGlobals);
830
1048
  const isExtend = names.includes('extend');
831
1049
  const lifecycle = names.some((name) => NON_TEST_SEGMENTS.has(name));
832
1050
  const suppression = names.filter((name) => SUPPRESSION_SEGMENTS.has(name));
@@ -836,9 +1054,19 @@ function scanFileForTests(state, cwd, model, fileHttpClient, fileMock, gateforge
836
1054
  // `pw.test(...)` / `base.test(...)` where pw/base is the required or
837
1055
  // namespaced test module object: the test function itself (not a
838
1056
  // wrapper) — grades exactly like a direct `test(...)` registration.
839
- const effectiveResolution = resolution === 'testmodule' && !isExtend && !lifecycle && !isDescribe && (names[0] === 'test' || names[0] === 'it')
1057
+ // A runner that provides GLOBALS for this file (vitest `globals: true`)
1058
+ // makes a bare `it('…')`/`test('…')` a registration the runner itself
1059
+ // owns: the runner proves those files by executing them, so the scan
1060
+ // must not file them as an unresolvable alias.
1061
+ const globalsRegistration = resolution === 'unknown' &&
1062
+ names.length === 0 &&
1063
+ (chain.base === 'test' || chain.base === 'it') &&
1064
+ runnerGlobals;
1065
+ const effectiveResolution = globalsRegistration
840
1066
  ? 'test'
841
- : resolution;
1067
+ : resolution === 'testmodule' && !isExtend && !lifecycle && !isDescribe && (names[0] === 'test' || names[0] === 'it')
1068
+ ? 'test'
1069
+ : resolution;
842
1070
  // Zero-arg conditional suppression INSIDE a test body: test.skip() / test.fixme()
843
1071
  if (effectiveResolution === 'test' &&
844
1072
  suppression.length > 0 &&
@@ -951,6 +1179,7 @@ function scanFileForTests(state, cwd, model, fileHttpClient, fileMock, gateforge
951
1179
  gateforgeImport,
952
1180
  });
953
1181
  state.result.entries.push(entry);
1182
+ state.result.registrationWarnings.push(...registrationWarningsForCall(file, source, node, entry.titlePath));
954
1183
  // Walk the body so inner zero-arg `test.skip()` / `test.fixme()`
955
1184
  // calls attach to THIS entry's signals.
956
1185
  if (callback !== undefined) {
@@ -991,6 +1220,7 @@ function scanFileForTests(state, cwd, model, fileHttpClient, fileMock, gateforge
991
1220
  gateforgeImport,
992
1221
  });
993
1222
  state.result.entries.push(entry);
1223
+ state.result.registrationWarnings.push(...registrationWarningsForCall(file, source, outer, entry.titlePath));
994
1224
  if (outerCallback !== undefined) {
995
1225
  const enclosing = { facts: entry.facts, signals: entry.signals };
996
1226
  ts.forEachChild(outerCallback, (child) => visit(child, describeStack, enclosing));
@@ -1034,9 +1264,234 @@ function scanFileForTests(state, cwd, model, fileHttpClient, fileMock, gateforge
1034
1264
  };
1035
1265
  visit(source, [], null);
1036
1266
  }
1267
+ /**
1268
+ * Finds one top-level local constant initializer by name.
1269
+ *
1270
+ * Args:
1271
+ * source: parsed test source file.
1272
+ * name: local identifier to resolve.
1273
+ *
1274
+ * Returns:
1275
+ * ts.Expression | null: a unique const initializer, or null when it
1276
+ * is absent, ambiguous, mutable, or destructured.
1277
+ */
1278
+ function localConstantInitializer(source, name) {
1279
+ const matches = [];
1280
+ for (const statement of source.statements) {
1281
+ if (!ts.isVariableStatement(statement) || (statement.declarationList.flags & ts.NodeFlags.Const) === 0)
1282
+ continue;
1283
+ for (const declaration of statement.declarationList.declarations) {
1284
+ if (ts.isIdentifier(declaration.name) && declaration.name.text === name && declaration.initializer !== undefined) {
1285
+ matches.push(declaration.initializer);
1286
+ }
1287
+ }
1288
+ }
1289
+ return matches.length === 1 ? (matches[0] ?? null) : null;
1290
+ }
1291
+ /**
1292
+ * Finds a pure local helper declared as a function or const arrow.
1293
+ *
1294
+ * Args:
1295
+ * source: parsed test source file.
1296
+ * name: helper identifier to resolve.
1297
+ *
1298
+ * Returns:
1299
+ * ts.FunctionDeclaration | ts.ArrowFunction | ts.FunctionExpression | null:
1300
+ * one unambiguous top-level function helper.
1301
+ */
1302
+ function localStaticFunction(source, name) {
1303
+ const matches = [];
1304
+ for (const statement of source.statements) {
1305
+ if (ts.isFunctionDeclaration(statement) && statement.name?.text === name) {
1306
+ matches.push(statement);
1307
+ continue;
1308
+ }
1309
+ if (!ts.isVariableStatement(statement) || (statement.declarationList.flags & ts.NodeFlags.Const) === 0)
1310
+ continue;
1311
+ for (const declaration of statement.declarationList.declarations) {
1312
+ if (!ts.isIdentifier(declaration.name) || declaration.name.text !== name || declaration.initializer === undefined)
1313
+ continue;
1314
+ if (ts.isArrowFunction(declaration.initializer) || ts.isFunctionExpression(declaration.initializer)) {
1315
+ matches.push(declaration.initializer);
1316
+ }
1317
+ }
1318
+ }
1319
+ return matches.length === 1 ? (matches[0] ?? null) : null;
1320
+ }
1321
+ /**
1322
+ * Resolves a bounded JSON-like expression without executing consumer code.
1323
+ *
1324
+ * Args:
1325
+ * expression: source expression to resolve.
1326
+ * source: parsed file that may contain local consts and pure helpers.
1327
+ * bindings: literal arguments bound to a local helper's parameters.
1328
+ * depth: recursion depth used to reject cycles.
1329
+ *
1330
+ * Returns:
1331
+ * StaticJsonResult: a literal value when fully resolvable, else failure.
1332
+ */
1333
+ function staticJsonValue(expression, source, bindings = new Map(), depth = 0) {
1334
+ if (depth > 8)
1335
+ return { ok: false };
1336
+ if (ts.isAsExpression(expression) ||
1337
+ ts.isTypeAssertionExpression(expression) ||
1338
+ ts.isSatisfiesExpression(expression) ||
1339
+ ts.isNonNullExpression(expression)) {
1340
+ return staticJsonValue(expression.expression, source, bindings, depth + 1);
1341
+ }
1342
+ if (ts.isStringLiteralLike(expression))
1343
+ return { ok: true, value: expression.text };
1344
+ if (ts.isNumericLiteral(expression)) {
1345
+ const value = Number(expression.text);
1346
+ return Number.isFinite(value) ? { ok: true, value } : { ok: false };
1347
+ }
1348
+ if (expression.kind === ts.SyntaxKind.TrueKeyword)
1349
+ return { ok: true, value: true };
1350
+ if (expression.kind === ts.SyntaxKind.FalseKeyword)
1351
+ return { ok: true, value: false };
1352
+ if (expression.kind === ts.SyntaxKind.NullKeyword)
1353
+ return { ok: true, value: null };
1354
+ if (ts.isIdentifier(expression)) {
1355
+ const bound = bindings.get(expression.text);
1356
+ if (bound !== undefined)
1357
+ return { ok: true, value: bound };
1358
+ const initializer = localConstantInitializer(source, expression.text);
1359
+ return initializer === null ? { ok: false } : staticJsonValue(initializer, source, bindings, depth + 1);
1360
+ }
1361
+ if (ts.isArrayLiteralExpression(expression)) {
1362
+ const values = [];
1363
+ for (const element of expression.elements) {
1364
+ if (!ts.isExpression(element))
1365
+ return { ok: false };
1366
+ const resolved = staticJsonValue(element, source, bindings, depth + 1);
1367
+ if (!resolved.ok)
1368
+ return { ok: false };
1369
+ values.push(resolved.value);
1370
+ }
1371
+ return { ok: true, value: values };
1372
+ }
1373
+ if (ts.isObjectLiteralExpression(expression)) {
1374
+ const value = {};
1375
+ for (const property of expression.properties) {
1376
+ if (!ts.isPropertyAssignment(property))
1377
+ return { ok: false };
1378
+ const key = property.name;
1379
+ if (!(ts.isIdentifier(key) || ts.isStringLiteralLike(key) || ts.isNumericLiteral(key)))
1380
+ return { ok: false };
1381
+ const resolved = staticJsonValue(property.initializer, source, bindings, depth + 1);
1382
+ if (!resolved.ok)
1383
+ return { ok: false };
1384
+ value[key.text] = resolved.value;
1385
+ }
1386
+ return { ok: true, value };
1387
+ }
1388
+ if (ts.isCallExpression(expression) && ts.isIdentifier(expression.expression)) {
1389
+ const helper = localStaticFunction(source, expression.expression.text);
1390
+ if (helper === null || helper.parameters.length !== expression.arguments.length)
1391
+ return { ok: false };
1392
+ const values = [];
1393
+ for (const argument of expression.arguments) {
1394
+ if (ts.isSpreadElement(argument))
1395
+ return { ok: false };
1396
+ const resolved = staticJsonValue(argument, source, bindings, depth + 1);
1397
+ if (!resolved.ok)
1398
+ return { ok: false };
1399
+ values.push(resolved.value);
1400
+ }
1401
+ const helperBindings = new Map();
1402
+ for (const [index, parameter] of helper.parameters.entries()) {
1403
+ if (!ts.isIdentifier(parameter.name))
1404
+ return { ok: false };
1405
+ const value = values[index];
1406
+ if (value === undefined)
1407
+ return { ok: false };
1408
+ helperBindings.set(parameter.name.text, value);
1409
+ }
1410
+ const helperBody = helper.body;
1411
+ let returned;
1412
+ if (helperBody === undefined)
1413
+ return { ok: false };
1414
+ if (!ts.isBlock(helperBody)) {
1415
+ returned = helperBody;
1416
+ }
1417
+ else {
1418
+ const statement = helperBody.statements[0];
1419
+ if (helperBody.statements.length === 1 &&
1420
+ statement !== undefined &&
1421
+ ts.isReturnStatement(statement) &&
1422
+ statement.expression !== undefined) {
1423
+ returned = statement.expression;
1424
+ }
1425
+ }
1426
+ return returned === undefined
1427
+ ? { ok: false }
1428
+ : staticJsonValue(returned, source, helperBindings, depth + 1);
1429
+ }
1430
+ return { ok: false };
1431
+ }
1432
+ /**
1433
+ * Extracts Gateforge claim annotations from one test call using literals only.
1434
+ *
1435
+ * Args:
1436
+ * node: parsed test-call node.
1437
+ * source: parsed test source file for local const/helper resolution.
1438
+ *
1439
+ * Returns:
1440
+ * { claims, issue }: deduplicated claim ids, or a visible reason when a
1441
+ * possible annotation cannot be resolved statically.
1442
+ */
1443
+ function gateforgeAnnotationClaims(node, source) {
1444
+ if (!ts.isCallExpression(node))
1445
+ return { claims: [], issue: null };
1446
+ const options = node.arguments[1];
1447
+ if (options === undefined || ts.isArrowFunction(options) || ts.isFunctionExpression(options)) {
1448
+ return { claims: [], issue: null };
1449
+ }
1450
+ if (ts.isObjectLiteralExpression(options)) {
1451
+ const hasAnnotation = options.properties.some((property) => ts.isPropertyAssignment(property) && ts.isIdentifier(property.name) && property.name.text === 'annotation');
1452
+ if (!hasAnnotation) {
1453
+ return {
1454
+ claims: [],
1455
+ issue: options.properties.some(ts.isSpreadAssignment)
1456
+ ? 'test options contain a spread that may hide a Gateforge annotation'
1457
+ : null,
1458
+ };
1459
+ }
1460
+ }
1461
+ const resolved = staticJsonValue(options, source);
1462
+ if (!resolved.ok) {
1463
+ return { claims: [], issue: 'test options may contain a Gateforge annotation but are not a local literal or pure helper result' };
1464
+ }
1465
+ if (resolved.value === null || Array.isArray(resolved.value) || typeof resolved.value !== 'object') {
1466
+ return { claims: [], issue: null };
1467
+ }
1468
+ const annotation = resolved.value['annotation'];
1469
+ if (annotation === undefined)
1470
+ return { claims: [], issue: null };
1471
+ const items = Array.isArray(annotation) ? annotation : [annotation];
1472
+ const claims = [];
1473
+ for (const item of items) {
1474
+ if (item === null || Array.isArray(item) || typeof item !== 'object') {
1475
+ return { claims: [], issue: 'annotation value is not a statically resolved object' };
1476
+ }
1477
+ const type = item['type'];
1478
+ if (typeof type !== 'string') {
1479
+ return { claims: [], issue: 'annotation type is not a statically resolved string' };
1480
+ }
1481
+ if (type !== CLAIM_ANNOTATION_TYPE)
1482
+ continue;
1483
+ const description = item['description'];
1484
+ if (typeof description !== 'string' || description.length === 0) {
1485
+ return { claims: [], issue: 'Gateforge annotation description is not a non-empty static string' };
1486
+ }
1487
+ claims.push(description);
1488
+ }
1489
+ return { claims: [...new Set(claims)].sort(), issue: null };
1490
+ }
1037
1491
  /** Builds one static entry: facts from the callback body + signals. */
1038
1492
  function buildEntry(input) {
1039
1493
  const { file, source, node, callback, titlePath, location } = input;
1494
+ const annotations = gateforgeAnnotationClaims(node, source);
1040
1495
  const params = callback === undefined ? [] : signatureParamsOf(callback);
1041
1496
  const signals = [...input.inheritedSignals];
1042
1497
  for (const kind of input.suppression) {
@@ -1044,6 +1499,7 @@ function buildEntry(input) {
1044
1499
  }
1045
1500
  let pageRoute = null;
1046
1501
  let httpClientCall = null;
1502
+ const interceptedTargets = callback === undefined ? [] : pageRouteTargets(callback);
1047
1503
  if (callback !== undefined) {
1048
1504
  pageRoute = findPageRoute(callback, source, file);
1049
1505
  httpClientCall = findHttpClientCall(callback, source, file);
@@ -1058,11 +1514,14 @@ function buildEntry(input) {
1058
1514
  facts: {
1059
1515
  signatureParams: params,
1060
1516
  pageRoute,
1517
+ ...(interceptedTargets.length > 0 ? { pageRouteTargets: interceptedTargets } : {}),
1061
1518
  httpClientCall,
1062
1519
  fileHttpClientCall: input.fileHttpClient,
1063
1520
  fileMockImport: input.fileMock,
1064
1521
  gateforgeFixtureImport: input.gateforgeImport,
1065
1522
  },
1523
+ ...(annotations.claims.length > 0 ? { annotationClaims: annotations.claims } : {}),
1524
+ ...(annotations.issue !== null ? { annotationIssue: annotations.issue } : {}),
1066
1525
  };
1067
1526
  }
1068
1527
  //# sourceMappingURL=static-discovery.js.map