@gate-forge/pack-playwright 0.7.1 → 0.8.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 (202) 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 +100 -3
  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 +9 -0
  39. package/dist/discovery/index.d.ts.map +1 -1
  40. package/dist/discovery/index.js +9 -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 +80 -11
  58. package/dist/discovery/reconcile.d.ts.map +1 -1
  59. package/dist/discovery/reconcile.js +395 -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/static-discovery.d.ts +38 -3
  66. package/dist/discovery/static-discovery.d.ts.map +1 -1
  67. package/dist/discovery/static-discovery.js +454 -15
  68. package/dist/discovery/static-discovery.js.map +1 -1
  69. package/dist/discovery/supervised-run.d.ts +199 -10
  70. package/dist/discovery/supervised-run.d.ts.map +1 -1
  71. package/dist/discovery/supervised-run.js +350 -39
  72. package/dist/discovery/supervised-run.js.map +1 -1
  73. package/dist/discovery/trusted-config.d.ts +180 -6
  74. package/dist/discovery/trusted-config.d.ts.map +1 -1
  75. package/dist/discovery/trusted-config.js +279 -10
  76. package/dist/discovery/trusted-config.js.map +1 -1
  77. package/dist/discovery/vitest-runner-adapter.d.ts +172 -0
  78. package/dist/discovery/vitest-runner-adapter.d.ts.map +1 -0
  79. package/dist/discovery/vitest-runner-adapter.js +607 -0
  80. package/dist/discovery/vitest-runner-adapter.js.map +1 -0
  81. package/dist/fixture/consumer-runner.d.ts +4 -0
  82. package/dist/fixture/consumer-runner.d.ts.map +1 -0
  83. package/dist/fixture/consumer-runner.js +38 -0
  84. package/dist/fixture/consumer-runner.js.map +1 -0
  85. package/dist/fixture/evidence.d.ts +27 -0
  86. package/dist/fixture/evidence.d.ts.map +1 -1
  87. package/dist/fixture/evidence.js +12 -0
  88. package/dist/fixture/evidence.js.map +1 -1
  89. package/dist/fixture/fixture.d.ts +2 -1
  90. package/dist/fixture/fixture.d.ts.map +1 -1
  91. package/dist/fixture/fixture.js +1 -17
  92. package/dist/fixture/fixture.js.map +1 -1
  93. package/dist/fixture/witness-client.d.ts +6 -174
  94. package/dist/fixture/witness-client.d.ts.map +1 -1
  95. package/dist/fixture/witness-client.js +6 -267
  96. package/dist/fixture/witness-client.js.map +1 -1
  97. package/dist/index.d.ts +9 -6
  98. package/dist/index.d.ts.map +1 -1
  99. package/dist/index.js +5 -4
  100. package/dist/index.js.map +1 -1
  101. package/dist/json.d.ts +6 -12
  102. package/dist/json.d.ts.map +1 -1
  103. package/dist/json.js +6 -17
  104. package/dist/json.js.map +1 -1
  105. package/dist/reporter/project-graph-reporter.d.ts +92 -0
  106. package/dist/reporter/project-graph-reporter.d.ts.map +1 -0
  107. package/dist/reporter/project-graph-reporter.js +92 -0
  108. package/dist/reporter/project-graph-reporter.js.map +1 -0
  109. package/dist/reporter/reporter.cjs.map +1 -1
  110. package/dist/reporter/reporter.d.ts +93 -4
  111. package/dist/reporter/reporter.d.ts.map +1 -1
  112. package/dist/reporter/reporter.js +174 -18
  113. package/dist/reporter/reporter.js.map +1 -1
  114. package/dist/runner-claims.d.ts +13 -0
  115. package/dist/runner-claims.d.ts.map +1 -0
  116. package/dist/runner-claims.js +46 -0
  117. package/dist/runner-claims.js.map +1 -0
  118. package/dist/runner-resolution.d.ts +3 -0
  119. package/dist/runner-resolution.d.ts.map +1 -0
  120. package/dist/runner-resolution.js +14 -0
  121. package/dist/runner-resolution.js.map +1 -0
  122. package/dist/supervisor/client.d.ts +23 -1
  123. package/dist/supervisor/client.d.ts.map +1 -1
  124. package/dist/supervisor/client.js +49 -0
  125. package/dist/supervisor/client.js.map +1 -1
  126. package/dist/supervisor/drain.d.ts +65 -0
  127. package/dist/supervisor/drain.d.ts.map +1 -1
  128. package/dist/supervisor/drain.js +353 -9
  129. package/dist/supervisor/drain.js.map +1 -1
  130. package/dist/supervisor/index.d.ts +7 -3
  131. package/dist/supervisor/index.d.ts.map +1 -1
  132. package/dist/supervisor/index.js +7 -3
  133. package/dist/supervisor/index.js.map +1 -1
  134. package/dist/supervisor/spool.d.ts +65 -2
  135. package/dist/supervisor/spool.d.ts.map +1 -1
  136. package/dist/supervisor/spool.js +47 -0
  137. package/dist/supervisor/spool.js.map +1 -1
  138. package/dist/surface.d.ts +6 -207
  139. package/dist/surface.d.ts.map +1 -1
  140. package/dist/surface.js +6 -264
  141. package/dist/surface.js.map +1 -1
  142. package/dist/vitest/index.d.ts +23 -0
  143. package/dist/vitest/index.d.ts.map +1 -0
  144. package/dist/vitest/index.js +356 -0
  145. package/dist/vitest/index.js.map +1 -0
  146. package/dist/vitest/reporter.d.ts +83 -0
  147. package/dist/vitest/reporter.d.ts.map +1 -0
  148. package/dist/vitest/reporter.js +211 -0
  149. package/dist/vitest/reporter.js.map +1 -0
  150. package/dist/vitest/worker-slot.d.ts +36 -0
  151. package/dist/vitest/worker-slot.d.ts.map +1 -0
  152. package/dist/vitest/worker-slot.js +51 -0
  153. package/dist/vitest/worker-slot.js.map +1 -0
  154. package/dist/witness/adapter-registry.d.ts +6 -50
  155. package/dist/witness/adapter-registry.d.ts.map +1 -1
  156. package/dist/witness/adapter-registry.js +6 -257
  157. package/dist/witness/adapter-registry.js.map +1 -1
  158. package/dist/witness/behavior-request.d.ts +6 -76
  159. package/dist/witness/behavior-request.d.ts.map +1 -1
  160. package/dist/witness/behavior-request.js +6 -273
  161. package/dist/witness/behavior-request.js.map +1 -1
  162. package/dist/witness/behavior.d.ts +6 -48
  163. package/dist/witness/behavior.d.ts.map +1 -1
  164. package/dist/witness/behavior.js +6 -114
  165. package/dist/witness/behavior.js.map +1 -1
  166. package/dist/witness/bin.d.ts +6 -17
  167. package/dist/witness/bin.d.ts.map +1 -1
  168. package/dist/witness/bin.js +6 -168
  169. package/dist/witness/bin.js.map +1 -1
  170. package/dist/witness/browser.d.ts +14 -206
  171. package/dist/witness/browser.d.ts.map +1 -1
  172. package/dist/witness/browser.js +15 -654
  173. package/dist/witness/browser.js.map +1 -1
  174. package/dist/witness/classifications.d.ts +6 -38
  175. package/dist/witness/classifications.d.ts.map +1 -1
  176. package/dist/witness/classifications.js +6 -83
  177. package/dist/witness/classifications.js.map +1 -1
  178. package/dist/witness/env-attestation.d.ts +6 -94
  179. package/dist/witness/env-attestation.d.ts.map +1 -1
  180. package/dist/witness/env-attestation.js +6 -198
  181. package/dist/witness/env-attestation.js.map +1 -1
  182. package/dist/witness/fixture-provider.d.ts +6 -82
  183. package/dist/witness/fixture-provider.d.ts.map +1 -1
  184. package/dist/witness/fixture-provider.js +6 -105
  185. package/dist/witness/fixture-provider.js.map +1 -1
  186. package/dist/witness/loopback-pins.d.ts +6 -75
  187. package/dist/witness/loopback-pins.d.ts.map +1 -1
  188. package/dist/witness/loopback-pins.js +6 -240
  189. package/dist/witness/loopback-pins.js.map +1 -1
  190. package/dist/witness/server.d.ts +8 -26
  191. package/dist/witness/server.d.ts.map +1 -1
  192. package/dist/witness/server.js +24 -3761
  193. package/dist/witness/server.js.map +1 -1
  194. package/dist/witness/types.d.ts +6 -882
  195. package/dist/witness/types.d.ts.map +1 -1
  196. package/dist/witness/types.js +9 -1
  197. package/dist/witness/types.js.map +1 -1
  198. package/examples/overlay-proof.spec.js +75 -0
  199. package/package.json +8 -2
  200. package/python/__pycache__/gateforge_persistence_intents.cpython-312.pyc +0 -0
  201. package/python/__pycache__/gateforge_persistence_intents.cpython-314.pyc +0 -0
  202. package/python/gateforge_pytest_plugin.py +462 -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,14 +966,21 @@ 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(),
@@ -784,7 +988,7 @@ export function scanTestFiles(options) {
784
988
  maxTraversedFiles: options.budget?.maxTraversedFiles ?? DEFAULT_MAX_TRAVERSED_FILES,
785
989
  maxImportDepth: options.budget?.maxImportDepth ?? DEFAULT_MAX_IMPORT_DEPTH,
786
990
  };
787
- const seededFiles = collectCandidateFiles(options.cwd, options.include, options.exclude);
991
+ const seededFiles = collectCandidateFiles(options.cwd, options.include, options.exclude, options.excludeFile);
788
992
  for (const file of seededFiles)
789
993
  state.seeded.add(file);
790
994
  for (const file of seededFiles) {
@@ -796,6 +1000,10 @@ export function scanTestFiles(options) {
796
1000
  const gateforgeImport = findGateforgeFixtureImport(model.source, file);
797
1001
  scanFileForTests(state, options.cwd, model, fileHttpClient, fileMock, gateforgeImport);
798
1002
  }
1003
+ state.result.registrationWarnings.sort((a, b) => (a.file < b.file ? -1 : a.file > b.file ? 1 : 0) ||
1004
+ a.location.line - b.location.line ||
1005
+ (a.environmentVariable < b.environmentVariable ? -1 : a.environmentVariable > b.environmentVariable ? 1 : 0) ||
1006
+ (a.titlePath.join('>') < b.titlePath.join('>') ? -1 : a.titlePath.join('>') > b.titlePath.join('>') ? 1 : 0));
799
1007
  state.result.entries.sort((a, b) => compareEntry(a, b));
800
1008
  state.result.unresolved.sort((a, b) => (a.file < b.file ? -1 : a.file > b.file ? 1 : 0) ||
801
1009
  a.location.line - b.location.line ||
@@ -951,6 +1159,7 @@ function scanFileForTests(state, cwd, model, fileHttpClient, fileMock, gateforge
951
1159
  gateforgeImport,
952
1160
  });
953
1161
  state.result.entries.push(entry);
1162
+ state.result.registrationWarnings.push(...registrationWarningsForCall(file, source, node, entry.titlePath));
954
1163
  // Walk the body so inner zero-arg `test.skip()` / `test.fixme()`
955
1164
  // calls attach to THIS entry's signals.
956
1165
  if (callback !== undefined) {
@@ -991,6 +1200,7 @@ function scanFileForTests(state, cwd, model, fileHttpClient, fileMock, gateforge
991
1200
  gateforgeImport,
992
1201
  });
993
1202
  state.result.entries.push(entry);
1203
+ state.result.registrationWarnings.push(...registrationWarningsForCall(file, source, outer, entry.titlePath));
994
1204
  if (outerCallback !== undefined) {
995
1205
  const enclosing = { facts: entry.facts, signals: entry.signals };
996
1206
  ts.forEachChild(outerCallback, (child) => visit(child, describeStack, enclosing));
@@ -1034,9 +1244,234 @@ function scanFileForTests(state, cwd, model, fileHttpClient, fileMock, gateforge
1034
1244
  };
1035
1245
  visit(source, [], null);
1036
1246
  }
1247
+ /**
1248
+ * Finds one top-level local constant initializer by name.
1249
+ *
1250
+ * Args:
1251
+ * source: parsed test source file.
1252
+ * name: local identifier to resolve.
1253
+ *
1254
+ * Returns:
1255
+ * ts.Expression | null: a unique const initializer, or null when it
1256
+ * is absent, ambiguous, mutable, or destructured.
1257
+ */
1258
+ function localConstantInitializer(source, name) {
1259
+ const matches = [];
1260
+ for (const statement of source.statements) {
1261
+ if (!ts.isVariableStatement(statement) || (statement.declarationList.flags & ts.NodeFlags.Const) === 0)
1262
+ continue;
1263
+ for (const declaration of statement.declarationList.declarations) {
1264
+ if (ts.isIdentifier(declaration.name) && declaration.name.text === name && declaration.initializer !== undefined) {
1265
+ matches.push(declaration.initializer);
1266
+ }
1267
+ }
1268
+ }
1269
+ return matches.length === 1 ? (matches[0] ?? null) : null;
1270
+ }
1271
+ /**
1272
+ * Finds a pure local helper declared as a function or const arrow.
1273
+ *
1274
+ * Args:
1275
+ * source: parsed test source file.
1276
+ * name: helper identifier to resolve.
1277
+ *
1278
+ * Returns:
1279
+ * ts.FunctionDeclaration | ts.ArrowFunction | ts.FunctionExpression | null:
1280
+ * one unambiguous top-level function helper.
1281
+ */
1282
+ function localStaticFunction(source, name) {
1283
+ const matches = [];
1284
+ for (const statement of source.statements) {
1285
+ if (ts.isFunctionDeclaration(statement) && statement.name?.text === name) {
1286
+ matches.push(statement);
1287
+ continue;
1288
+ }
1289
+ if (!ts.isVariableStatement(statement) || (statement.declarationList.flags & ts.NodeFlags.Const) === 0)
1290
+ continue;
1291
+ for (const declaration of statement.declarationList.declarations) {
1292
+ if (!ts.isIdentifier(declaration.name) || declaration.name.text !== name || declaration.initializer === undefined)
1293
+ continue;
1294
+ if (ts.isArrowFunction(declaration.initializer) || ts.isFunctionExpression(declaration.initializer)) {
1295
+ matches.push(declaration.initializer);
1296
+ }
1297
+ }
1298
+ }
1299
+ return matches.length === 1 ? (matches[0] ?? null) : null;
1300
+ }
1301
+ /**
1302
+ * Resolves a bounded JSON-like expression without executing consumer code.
1303
+ *
1304
+ * Args:
1305
+ * expression: source expression to resolve.
1306
+ * source: parsed file that may contain local consts and pure helpers.
1307
+ * bindings: literal arguments bound to a local helper's parameters.
1308
+ * depth: recursion depth used to reject cycles.
1309
+ *
1310
+ * Returns:
1311
+ * StaticJsonResult: a literal value when fully resolvable, else failure.
1312
+ */
1313
+ function staticJsonValue(expression, source, bindings = new Map(), depth = 0) {
1314
+ if (depth > 8)
1315
+ return { ok: false };
1316
+ if (ts.isAsExpression(expression) ||
1317
+ ts.isTypeAssertionExpression(expression) ||
1318
+ ts.isSatisfiesExpression(expression) ||
1319
+ ts.isNonNullExpression(expression)) {
1320
+ return staticJsonValue(expression.expression, source, bindings, depth + 1);
1321
+ }
1322
+ if (ts.isStringLiteralLike(expression))
1323
+ return { ok: true, value: expression.text };
1324
+ if (ts.isNumericLiteral(expression)) {
1325
+ const value = Number(expression.text);
1326
+ return Number.isFinite(value) ? { ok: true, value } : { ok: false };
1327
+ }
1328
+ if (expression.kind === ts.SyntaxKind.TrueKeyword)
1329
+ return { ok: true, value: true };
1330
+ if (expression.kind === ts.SyntaxKind.FalseKeyword)
1331
+ return { ok: true, value: false };
1332
+ if (expression.kind === ts.SyntaxKind.NullKeyword)
1333
+ return { ok: true, value: null };
1334
+ if (ts.isIdentifier(expression)) {
1335
+ const bound = bindings.get(expression.text);
1336
+ if (bound !== undefined)
1337
+ return { ok: true, value: bound };
1338
+ const initializer = localConstantInitializer(source, expression.text);
1339
+ return initializer === null ? { ok: false } : staticJsonValue(initializer, source, bindings, depth + 1);
1340
+ }
1341
+ if (ts.isArrayLiteralExpression(expression)) {
1342
+ const values = [];
1343
+ for (const element of expression.elements) {
1344
+ if (!ts.isExpression(element))
1345
+ return { ok: false };
1346
+ const resolved = staticJsonValue(element, source, bindings, depth + 1);
1347
+ if (!resolved.ok)
1348
+ return { ok: false };
1349
+ values.push(resolved.value);
1350
+ }
1351
+ return { ok: true, value: values };
1352
+ }
1353
+ if (ts.isObjectLiteralExpression(expression)) {
1354
+ const value = {};
1355
+ for (const property of expression.properties) {
1356
+ if (!ts.isPropertyAssignment(property))
1357
+ return { ok: false };
1358
+ const key = property.name;
1359
+ if (!(ts.isIdentifier(key) || ts.isStringLiteralLike(key) || ts.isNumericLiteral(key)))
1360
+ return { ok: false };
1361
+ const resolved = staticJsonValue(property.initializer, source, bindings, depth + 1);
1362
+ if (!resolved.ok)
1363
+ return { ok: false };
1364
+ value[key.text] = resolved.value;
1365
+ }
1366
+ return { ok: true, value };
1367
+ }
1368
+ if (ts.isCallExpression(expression) && ts.isIdentifier(expression.expression)) {
1369
+ const helper = localStaticFunction(source, expression.expression.text);
1370
+ if (helper === null || helper.parameters.length !== expression.arguments.length)
1371
+ return { ok: false };
1372
+ const values = [];
1373
+ for (const argument of expression.arguments) {
1374
+ if (ts.isSpreadElement(argument))
1375
+ return { ok: false };
1376
+ const resolved = staticJsonValue(argument, source, bindings, depth + 1);
1377
+ if (!resolved.ok)
1378
+ return { ok: false };
1379
+ values.push(resolved.value);
1380
+ }
1381
+ const helperBindings = new Map();
1382
+ for (const [index, parameter] of helper.parameters.entries()) {
1383
+ if (!ts.isIdentifier(parameter.name))
1384
+ return { ok: false };
1385
+ const value = values[index];
1386
+ if (value === undefined)
1387
+ return { ok: false };
1388
+ helperBindings.set(parameter.name.text, value);
1389
+ }
1390
+ const helperBody = helper.body;
1391
+ let returned;
1392
+ if (helperBody === undefined)
1393
+ return { ok: false };
1394
+ if (!ts.isBlock(helperBody)) {
1395
+ returned = helperBody;
1396
+ }
1397
+ else {
1398
+ const statement = helperBody.statements[0];
1399
+ if (helperBody.statements.length === 1 &&
1400
+ statement !== undefined &&
1401
+ ts.isReturnStatement(statement) &&
1402
+ statement.expression !== undefined) {
1403
+ returned = statement.expression;
1404
+ }
1405
+ }
1406
+ return returned === undefined
1407
+ ? { ok: false }
1408
+ : staticJsonValue(returned, source, helperBindings, depth + 1);
1409
+ }
1410
+ return { ok: false };
1411
+ }
1412
+ /**
1413
+ * Extracts Gateforge claim annotations from one test call using literals only.
1414
+ *
1415
+ * Args:
1416
+ * node: parsed test-call node.
1417
+ * source: parsed test source file for local const/helper resolution.
1418
+ *
1419
+ * Returns:
1420
+ * { claims, issue }: deduplicated claim ids, or a visible reason when a
1421
+ * possible annotation cannot be resolved statically.
1422
+ */
1423
+ function gateforgeAnnotationClaims(node, source) {
1424
+ if (!ts.isCallExpression(node))
1425
+ return { claims: [], issue: null };
1426
+ const options = node.arguments[1];
1427
+ if (options === undefined || ts.isArrowFunction(options) || ts.isFunctionExpression(options)) {
1428
+ return { claims: [], issue: null };
1429
+ }
1430
+ if (ts.isObjectLiteralExpression(options)) {
1431
+ const hasAnnotation = options.properties.some((property) => ts.isPropertyAssignment(property) && ts.isIdentifier(property.name) && property.name.text === 'annotation');
1432
+ if (!hasAnnotation) {
1433
+ return {
1434
+ claims: [],
1435
+ issue: options.properties.some(ts.isSpreadAssignment)
1436
+ ? 'test options contain a spread that may hide a Gateforge annotation'
1437
+ : null,
1438
+ };
1439
+ }
1440
+ }
1441
+ const resolved = staticJsonValue(options, source);
1442
+ if (!resolved.ok) {
1443
+ return { claims: [], issue: 'test options may contain a Gateforge annotation but are not a local literal or pure helper result' };
1444
+ }
1445
+ if (resolved.value === null || Array.isArray(resolved.value) || typeof resolved.value !== 'object') {
1446
+ return { claims: [], issue: null };
1447
+ }
1448
+ const annotation = resolved.value['annotation'];
1449
+ if (annotation === undefined)
1450
+ return { claims: [], issue: null };
1451
+ const items = Array.isArray(annotation) ? annotation : [annotation];
1452
+ const claims = [];
1453
+ for (const item of items) {
1454
+ if (item === null || Array.isArray(item) || typeof item !== 'object') {
1455
+ return { claims: [], issue: 'annotation value is not a statically resolved object' };
1456
+ }
1457
+ const type = item['type'];
1458
+ if (typeof type !== 'string') {
1459
+ return { claims: [], issue: 'annotation type is not a statically resolved string' };
1460
+ }
1461
+ if (type !== CLAIM_ANNOTATION_TYPE)
1462
+ continue;
1463
+ const description = item['description'];
1464
+ if (typeof description !== 'string' || description.length === 0) {
1465
+ return { claims: [], issue: 'Gateforge annotation description is not a non-empty static string' };
1466
+ }
1467
+ claims.push(description);
1468
+ }
1469
+ return { claims: [...new Set(claims)].sort(), issue: null };
1470
+ }
1037
1471
  /** Builds one static entry: facts from the callback body + signals. */
1038
1472
  function buildEntry(input) {
1039
1473
  const { file, source, node, callback, titlePath, location } = input;
1474
+ const annotations = gateforgeAnnotationClaims(node, source);
1040
1475
  const params = callback === undefined ? [] : signatureParamsOf(callback);
1041
1476
  const signals = [...input.inheritedSignals];
1042
1477
  for (const kind of input.suppression) {
@@ -1044,6 +1479,7 @@ function buildEntry(input) {
1044
1479
  }
1045
1480
  let pageRoute = null;
1046
1481
  let httpClientCall = null;
1482
+ const interceptedTargets = callback === undefined ? [] : pageRouteTargets(callback);
1047
1483
  if (callback !== undefined) {
1048
1484
  pageRoute = findPageRoute(callback, source, file);
1049
1485
  httpClientCall = findHttpClientCall(callback, source, file);
@@ -1058,11 +1494,14 @@ function buildEntry(input) {
1058
1494
  facts: {
1059
1495
  signatureParams: params,
1060
1496
  pageRoute,
1497
+ ...(interceptedTargets.length > 0 ? { pageRouteTargets: interceptedTargets } : {}),
1061
1498
  httpClientCall,
1062
1499
  fileHttpClientCall: input.fileHttpClient,
1063
1500
  fileMockImport: input.fileMock,
1064
1501
  gateforgeFixtureImport: input.gateforgeImport,
1065
1502
  },
1503
+ ...(annotations.claims.length > 0 ? { annotationClaims: annotations.claims } : {}),
1504
+ ...(annotations.issue !== null ? { annotationIssue: annotations.issue } : {}),
1066
1505
  };
1067
1506
  }
1068
1507
  //# sourceMappingURL=static-discovery.js.map