@skyramp/mcp 0.3.4 → 0.3.6-rc.1

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 (159) hide show
  1. package/build/playwright/registerPlaywrightTools.js +92 -30
  2. package/build/playwright/traceRecordingPrompt.d.ts +6 -0
  3. package/build/playwright/traceRecordingPrompt.js +6 -2
  4. package/build/prompts/code-reuse.d.ts +1 -2
  5. package/build/prompts/code-reuse.js +182 -77
  6. package/build/prompts/modularization/integration-test-modularization.d.ts +2 -0
  7. package/build/prompts/modularization/integration-test-modularization.js +83 -41
  8. package/build/prompts/modularization/render.d.ts +18 -0
  9. package/build/prompts/modularization/render.js +12 -0
  10. package/build/prompts/modularization/ui-test-modularization.d.ts +3 -1
  11. package/build/prompts/modularization/ui-test-modularization.js +89 -47
  12. package/build/prompts/pom-aware-code-reuse.js +3 -1
  13. package/build/prompts/shared-helper-policy.d.ts +57 -0
  14. package/build/prompts/shared-helper-policy.js +135 -0
  15. package/build/prompts/test-recommendation/diffExecutionPlan.js +62 -56
  16. package/build/prompts/test-recommendation/fullRepoCatalog.js +19 -8
  17. package/build/prompts/test-recommendation/recommendationShared.d.ts +28 -6
  18. package/build/prompts/test-recommendation/recommendationShared.js +90 -16
  19. package/build/prompts/test-recommendation/registerRecommendTestsPrompt.js +22 -0
  20. package/build/prompts/test-recommendation/test-recommendation-prompt.d.ts +2 -2
  21. package/build/prompts/test-recommendation/test-recommendation-prompt.js +3 -3
  22. package/build/prompts/testbot/testbot-prompts.js +88 -33
  23. package/build/recommendation/budgeters/shared.js +105 -27
  24. package/build/recommendation/discriminators.js +13 -2
  25. package/build/recommendation/planRanker.d.ts +6 -6
  26. package/build/recommendation/planRanker.js +6 -61
  27. package/build/services/AnalyticsService.d.ts +7 -0
  28. package/build/services/AnalyticsService.js +7 -1
  29. package/build/services/ModularizationService.js +1 -3
  30. package/build/services/TestDiscoveryService.d.ts +0 -2
  31. package/build/services/TestDiscoveryService.js +2 -37
  32. package/build/services/TestGenerationService.d.ts +16 -0
  33. package/build/services/TestGenerationService.js +86 -10
  34. package/build/services/containerEnv.js +13 -12
  35. package/build/tools/code-refactor/codeReuseTool.js +279 -93
  36. package/build/tools/code-refactor/enhance-state.d.ts +49 -0
  37. package/build/tools/code-refactor/enhance-state.js +109 -0
  38. package/build/tools/code-refactor/enhanceAssertionsTool.js +34 -1
  39. package/build/tools/code-refactor/modularizationTool.js +9 -2
  40. package/build/tools/code-refactor/reuse-outcome.d.ts +23 -1
  41. package/build/tools/code-refactor/reuse-outcome.js +14 -4
  42. package/build/tools/code-refactor/reuse-state.d.ts +127 -5
  43. package/build/tools/code-refactor/reuse-state.js +628 -16
  44. package/build/tools/code-refactor/utils-verify-gates.d.ts +26 -0
  45. package/build/tools/code-refactor/utils-verify-gates.js +100 -0
  46. package/build/tools/code-refactor/verify-gates.d.ts +2 -1
  47. package/build/tools/code-refactor/verify-gates.js +90 -25
  48. package/build/tools/executeSkyrampTestTool.d.ts +19 -0
  49. package/build/tools/executeSkyrampTestTool.js +158 -8
  50. package/build/tools/generate-tests/generateBatchScenarioRestTool.js +2 -2
  51. package/build/tools/generate-tests/generateE2ERestTool.js +16 -0
  52. package/build/tools/generate-tests/generateUIRestTool.d.ts +1 -0
  53. package/build/tools/generate-tests/generateUIRestTool.js +22 -0
  54. package/build/tools/generate-tests/scenarioLint.d.ts +2 -0
  55. package/build/tools/generate-tests/scenarioLint.js +127 -19
  56. package/build/tools/generate-tests/trace-reuse-guard.d.ts +20 -0
  57. package/build/tools/generate-tests/trace-reuse-guard.js +93 -0
  58. package/build/tools/submitReportTool.d.ts +38 -38
  59. package/build/tools/submitReportTool.js +487 -104
  60. package/build/tools/test-management/analyzeChangesTool.d.ts +24 -1
  61. package/build/tools/test-management/analyzeChangesTool.js +75 -12
  62. package/build/tools/test-management/analyzeTestHealthTool.js +7 -7
  63. package/build/tools/test-management/registerTestPlanTool.d.ts +203 -0
  64. package/build/tools/test-management/registerTestPlanTool.js +149 -23
  65. package/build/types/Recommendation.d.ts +34 -5
  66. package/build/types/RepositoryAnalysis.d.ts +133 -114
  67. package/build/types/RepositoryAnalysis.js +1 -1
  68. package/build/types/ReuseOutcome.d.ts +102 -6
  69. package/build/types/ReuseOutcome.js +16 -2
  70. package/build/types/TestRecommendation.js +21 -3
  71. package/build/types/TestTypes.js +14 -8
  72. package/build/types/TestbotReport.d.ts +25 -3
  73. package/build/types/index.d.ts +2 -2
  74. package/build/types/index.js +1 -1
  75. package/build/utils/AnalysisStateManager.d.ts +69 -1
  76. package/build/utils/AnalysisStateManager.js +69 -5
  77. package/build/utils/branchDiff.d.ts +10 -0
  78. package/build/utils/branchDiff.js +28 -0
  79. package/build/utils/changedRoutes.d.ts +29 -0
  80. package/build/utils/changedRoutes.js +87 -0
  81. package/build/utils/featureFlags.d.ts +21 -0
  82. package/build/utils/featureFlags.js +23 -0
  83. package/build/utils/frontendIntegration.js +34 -4
  84. package/build/utils/importerHop.d.ts +2 -8
  85. package/build/utils/importerHop.js +15 -53
  86. package/build/utils/pathMatching.d.ts +38 -0
  87. package/build/utils/pathMatching.js +71 -0
  88. package/build/utils/pathSignatures.d.ts +22 -0
  89. package/build/utils/pathSignatures.js +57 -0
  90. package/build/utils/planMatchKeys.d.ts +16 -3
  91. package/build/utils/planMatchKeys.js +26 -10
  92. package/build/utils/pluralization.d.ts +10 -0
  93. package/build/utils/pluralization.js +18 -0
  94. package/build/utils/pom-catalog-parse.d.ts +52 -0
  95. package/build/utils/pom-catalog-parse.js +141 -0
  96. package/build/utils/pom-scope/selector-extractor.d.ts +12 -0
  97. package/build/utils/pom-scope/selector-extractor.js +34 -8
  98. package/build/utils/pom-verify/verify.d.ts +6 -5
  99. package/build/utils/pom-verify/verify.js +8 -6
  100. package/build/utils/reportLanguage.d.ts +43 -0
  101. package/build/utils/reportLanguage.js +125 -0
  102. package/build/utils/reportVerification.d.ts +74 -4
  103. package/build/utils/reportVerification.js +259 -3
  104. package/build/utils/reuseRouting.d.ts +3 -0
  105. package/build/utils/reuseRouting.js +50 -0
  106. package/build/utils/routeParsers.d.ts +2 -0
  107. package/build/utils/routeParsers.js +65 -8
  108. package/build/utils/scenarioDrafting.d.ts +1 -1
  109. package/build/utils/scenarioDrafting.js +57 -45
  110. package/build/utils/subjectEndpoints.d.ts +19 -0
  111. package/build/utils/subjectEndpoints.js +98 -0
  112. package/build/utils/testFileClassification.d.ts +11 -0
  113. package/build/utils/testFileClassification.js +47 -0
  114. package/build/utils/uiPageEnumerator.d.ts +45 -19
  115. package/build/utils/uiPageEnumerator.js +95 -51
  116. package/build/utils/utils-verify/allow.d.ts +16 -0
  117. package/build/utils/utils-verify/allow.js +68 -0
  118. package/build/utils/utils-verify/call-sites.d.ts +34 -0
  119. package/build/utils/utils-verify/call-sites.js +154 -0
  120. package/build/utils/utils-verify/index.d.ts +7 -0
  121. package/build/utils/utils-verify/index.js +7 -0
  122. package/build/utils/utils-verify/language-spec.d.ts +91 -0
  123. package/build/utils/utils-verify/language-spec.js +210 -0
  124. package/build/utils/utils-verify/locate.d.ts +39 -0
  125. package/build/utils/utils-verify/locate.js +199 -0
  126. package/build/utils/utils-verify/parse.d.ts +34 -0
  127. package/build/utils/utils-verify/parse.js +177 -0
  128. package/build/utils/utils-verify/stage.d.ts +24 -0
  129. package/build/utils/utils-verify/stage.js +107 -0
  130. package/build/utils/utils-verify/verify.d.ts +63 -0
  131. package/build/utils/utils-verify/verify.js +168 -0
  132. package/build/utils/utils.d.ts +3 -1
  133. package/build/utils/utils.js +3 -1
  134. package/build/workspace/workspace.d.ts +32 -32
  135. package/node_modules/playwright/lib/mcp/skyramp/assertTool.js +9 -5
  136. package/node_modules/playwright/lib/mcp/skyramp/loadTraceTool.js +16 -0
  137. package/node_modules/playwright/lib/mcp/skyramp/skyRampImport.js +2 -0
  138. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +115 -14
  139. package/node_modules/playwright/lib/mcp/test/skyRampExport.js +13 -1
  140. package/node_modules/playwright/node_modules/playwright-core/.DS_Store +0 -0
  141. package/node_modules/playwright/node_modules/playwright-core/lib/server/codegen/skyramp/jsonlReader.js +2 -0
  142. package/node_modules/playwright/node_modules/playwright-core/lib/vite/htmlReport/index.html +27 -253
  143. package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/{codeMirrorModule-DtudTj_v.js → codeMirrorModule-DJMC4zNo.js} +1 -1
  144. package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/index-BW82eAUI.js +196 -0
  145. package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/index.html +1 -1
  146. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-FNMuBzX1.js → codeMirrorModule-CZfp96qZ.js} +1 -1
  147. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-gpLo02E0.js +809 -0
  148. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.Bq1r1URj.js +2 -0
  149. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +2 -2
  150. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.VEfqi1qN.js +5 -0
  151. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +2 -2
  152. package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
  153. package/node_modules/playwright/node_modules/playwright-core/src/server/codegen/skyramp/jsonlReader.ts +1 -1
  154. package/node_modules/playwright/package.json +1 -1
  155. package/package.json +2 -2
  156. package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/index-BpDwp16L.js +0 -422
  157. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-Co9upU5h.js +0 -1035
  158. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.DXNIQ_dx.js +0 -2
  159. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.CIKB3XSv.js +0 -5
@@ -14,8 +14,18 @@ import { DriftAction } from "../types/TestAnalysis.js";
14
14
  * `--untracked-files=all` lists each untracked file individually (otherwise git
15
15
  * collapses a brand-new directory to "dir/", which would defeat path matching).
16
16
  */
17
+ /**
18
+ * Union of listChangedFiles over every tree a run legitimately writes to —
19
+ * the primary checkout, the tests-repo checkout (testRepoPath), and related
20
+ * repos (SKYR-4204 reopen: the backing check scanned only the primary, so a
21
+ * UI test generated into the tests-repo checkout was rejected as unbacked and
22
+ * demoted out of the report — run 32510028428). Undefined and duplicate roots
23
+ * are skipped; a root where git fails (missing dir, not a repo) contributes
24
+ * nothing rather than failing the whole check.
25
+ */
26
+ export declare function listChangedFilesAcross(roots: Array<string | undefined>): Promise<string[]>;
17
27
  export declare function listChangedFiles(repoRoot: string): Promise<string[]>;
18
- export interface UnbackedClaimsInput {
28
+ export interface UnchangedFileClaimsInput {
19
29
  /** Absolute path of the primary repo checkout the report pertains to. */
20
30
  repoRoot: string;
21
31
  /** Repo-relative paths that have a working-tree change (from listChangedFiles). */
@@ -39,8 +49,14 @@ export interface UnbackedClaimsInput {
39
49
  }
40
50
  /**
41
51
  * Cross-check report claims against files that actually changed in the working
42
- * tree, returning a human-readable description for each claim NOT backed by a
43
- * real change. An empty result means every claim is backed.
52
+ * tree, returning a human-readable description for each claim whose file has no
53
+ * change. An empty result means every claim is backed.
54
+ *
55
+ * Not to be confused with `findInvalidSourceCitations` below. This one is about
56
+ * files the agent says IT wrote — a created test, a maintained test — so the test
57
+ * is "did this file change in this run". That one is about application code the
58
+ * agent POINTS AT as the home of a defect, which it never edits, so the test is
59
+ * "does this file and symbol exist in the checkout at all".
44
60
  *
45
61
  * The delivery path can only ship what the agent actually edited (SKYR-3883), so
46
62
  * a report must never claim file work the tree doesn't reflect. This is the
@@ -63,4 +79,58 @@ export interface UnbackedClaimsInput {
63
79
  * - Claims for files outside this checkout (cross-repo `repository`, or an
64
80
  * absolute path resolving outside `repoRoot`) — a different checkout owns them.
65
81
  */
66
- export declare function findUnbackedClaims(input: UnbackedClaimsInput): string[];
82
+ export declare function findUnchangedFileClaims(input: UnchangedFileClaimsInput): string[];
83
+ /** One checkout of the run, and the owner/repo it holds. */
84
+ export interface RepoCheckout {
85
+ /** Absolute checkout path. */
86
+ root: string;
87
+ /** owner/repo of this checkout, when the run recorded one. Undefined means the
88
+ * checkout cannot be named, so nothing resolved in it can be attributed. */
89
+ repository?: string;
90
+ /** True for the run's primary checkout — the repo an entry with no `repository`
91
+ * already means downstream, so its citations are never stamped. */
92
+ primary?: boolean;
93
+ }
94
+ export interface SourceCitationInput {
95
+ /** Checkouts to resolve a citation against: the primary repo plus every related
96
+ * repo in the run. Empty means no checkout is known — see below. */
97
+ checkouts: RepoCheckout[];
98
+ /** LLM-supplied issuesFound entries in submission order. The index is quoted back
99
+ * to the agent so it can find the entry it has to fix. */
100
+ issues: {
101
+ sourceFile?: string;
102
+ sourceSymbol?: string;
103
+ repository?: string;
104
+ }[];
105
+ }
106
+ export interface SourceCitationCheck {
107
+ /** One message per DEFINITE mismatch. Empty means the citations may ship. */
108
+ invalid: string[];
109
+ /** owner/repo to stamp on the entry at the same index of `issues`, or undefined
110
+ * to ship the attribution exactly as submitted. */
111
+ repository: (string | undefined)[];
112
+ }
113
+ /**
114
+ * Check every `sourceFile`/`sourceSymbol` citation on the submitted issuesFound
115
+ * entries against the repository, and say which repo each one belongs to.
116
+ * Returns one message per DEFINITE mismatch — an empty `invalid` means the report
117
+ * may be written — plus, per entry, the owner/repo the citation resolved in.
118
+ *
119
+ * The attribution is the reason this resolves against every checkout instead of
120
+ * the first hit: the consumer must not have to guess a cited file's repo from the
121
+ * path, which is what makes it link the primary repo's copy of a related repo's
122
+ * file. Where the citation resolves is the answer, so it is returned rather than
123
+ * discarded. Only a related repo is stamped: an absent `repository` already means
124
+ * the primary repo downstream, so a primary-repo citation ships as submitted.
125
+ *
126
+ * Rejects four things: a path that no known checkout contains, a symbol absent
127
+ * from the file that was found, an attribution that names a different repo than
128
+ * the one holding the code, and a file that resolves in several named checkouts
129
+ * with no attribution to choose between them. Everything uncertain is ACCEPTED —
130
+ * no checkout, an unreadable file, a checkout with no owner/repo, a directory
131
+ * listing that fails. The check must behave the same on any repo, in any
132
+ * language, at any size, and it must never become a loop the agent cannot exit.
133
+ * Symbol matching is a plain substring search for the same reason: no parser, so
134
+ * no language it silently mishandles.
135
+ */
136
+ export declare function findInvalidSourceCitations(input: SourceCitationInput): Promise<SourceCitationCheck>;
@@ -1,4 +1,5 @@
1
1
  import path from "path";
2
+ import * as fs from "fs/promises";
2
3
  import { execFile } from "child_process";
3
4
  import { promisify } from "util";
4
5
  import { testFileMatches } from "./utils.js";
@@ -19,6 +20,37 @@ const execFileAsync = promisify(execFile);
19
20
  * `--untracked-files=all` lists each untracked file individually (otherwise git
20
21
  * collapses a brand-new directory to "dir/", which would defeat path matching).
21
22
  */
23
+ /**
24
+ * Union of listChangedFiles over every tree a run legitimately writes to —
25
+ * the primary checkout, the tests-repo checkout (testRepoPath), and related
26
+ * repos (SKYR-4204 reopen: the backing check scanned only the primary, so a
27
+ * UI test generated into the tests-repo checkout was rejected as unbacked and
28
+ * demoted out of the report — run 32510028428). Undefined and duplicate roots
29
+ * are skipped; a root where git fails (missing dir, not a repo) contributes
30
+ * nothing rather than failing the whole check.
31
+ */
32
+ export async function listChangedFilesAcross(roots) {
33
+ const seenRoots = new Set();
34
+ const seenFiles = new Set();
35
+ const files = [];
36
+ for (const root of roots) {
37
+ if (!root || seenRoots.has(root))
38
+ continue;
39
+ seenRoots.add(root);
40
+ try {
41
+ for (const f of await listChangedFiles(root)) {
42
+ if (seenFiles.has(f))
43
+ continue;
44
+ seenFiles.add(f);
45
+ files.push(f);
46
+ }
47
+ }
48
+ catch {
49
+ // Non-git or missing tree — nothing to contribute.
50
+ }
51
+ }
52
+ return files;
53
+ }
22
54
  export async function listChangedFiles(repoRoot) {
23
55
  const { stdout } = await execFileAsync("git", ["status", "--porcelain", "-z", "--untracked-files=all"], { cwd: repoRoot });
24
56
  const files = [];
@@ -41,8 +73,14 @@ export async function listChangedFiles(repoRoot) {
41
73
  }
42
74
  /**
43
75
  * Cross-check report claims against files that actually changed in the working
44
- * tree, returning a human-readable description for each claim NOT backed by a
45
- * real change. An empty result means every claim is backed.
76
+ * tree, returning a human-readable description for each claim whose file has no
77
+ * change. An empty result means every claim is backed.
78
+ *
79
+ * Not to be confused with `findInvalidSourceCitations` below. This one is about
80
+ * files the agent says IT wrote — a created test, a maintained test — so the test
81
+ * is "did this file change in this run". That one is about application code the
82
+ * agent POINTS AT as the home of a defect, which it never edits, so the test is
83
+ * "does this file and symbol exist in the checkout at all".
46
84
  *
47
85
  * The delivery path can only ship what the agent actually edited (SKYR-3883), so
48
86
  * a report must never claim file work the tree doesn't reflect. This is the
@@ -65,7 +103,7 @@ export async function listChangedFiles(repoRoot) {
65
103
  * - Claims for files outside this checkout (cross-repo `repository`, or an
66
104
  * absolute path resolving outside `repoRoot`) — a different checkout owns them.
67
105
  */
68
- export function findUnbackedClaims(input) {
106
+ export function findUnchangedFileClaims(input) {
69
107
  const { repoRoot, changedFiles, newTests, verdicts, primaryRepository } = input;
70
108
  const unbacked = [];
71
109
  const isBacked = (claimedPath) => {
@@ -108,3 +146,221 @@ export function findUnbackedClaims(input) {
108
146
  }
109
147
  return unbacked;
110
148
  }
149
+ /** Both ways out of a citation rejection, appended to every message so the agent
150
+ * never has to guess that dropping the citation is allowed. */
151
+ const CITATION_REMEDY = "Read the file you mean, correct sourceFile/sourceSymbol, and resubmit — or remove the citation if you cannot confirm it in source.";
152
+ /** How many same-basename paths to offer for a missing file. Enough to converge in
153
+ * one retry, few enough that the message stays readable. */
154
+ const MAX_PATH_SUGGESTIONS = 3;
155
+ /** True when `abs` names something strictly below `root`. */
156
+ function isInsideRoot(root, abs) {
157
+ const rel = path.relative(root, abs);
158
+ return !!rel && !rel.startsWith("..") && !path.isAbsolute(rel);
159
+ }
160
+ /**
161
+ * Resolve a cited path under one checkout, or return null when it lands outside.
162
+ * A citation names a file INSIDE the repo, so `../../etc/passwd` and an absolute
163
+ * path from another tree are invalid citations — not files to go looking for.
164
+ */
165
+ function resolveInRoot(root, cited) {
166
+ // The contract is a repository-relative path. An absolute one would slip past
167
+ // the containment check whenever it happens to sit under root, because
168
+ // path.resolve() discards root — and it is unusable as a review anchor.
169
+ if (path.isAbsolute(cited))
170
+ return null;
171
+ const abs = path.resolve(root, cited);
172
+ return isInsideRoot(root, abs) ? abs : null;
173
+ }
174
+ /**
175
+ * Repo-relative paths whose basename equals the cited file's basename, so a
176
+ * rejection can name the file the agent probably meant. `git ls-files` does the
177
+ * filtering (a pathspec, not a full listing) to stay cheap on a large repo; a
178
+ * non-git directory or missing git yields no suggestions rather than an error.
179
+ */
180
+ async function suggestSimilarPaths(roots, cited) {
181
+ const base = path.basename(cited);
182
+ if (!base)
183
+ return [];
184
+ const matches = [];
185
+ for (const root of roots) {
186
+ try {
187
+ const { stdout } = await execFileAsync("git", ["ls-files", "-z", "--", `*${base}`, base], {
188
+ cwd: root,
189
+ });
190
+ for (const p of stdout.split("\0")) {
191
+ // The pathspec also matches "myproducts.py" for "products.py"; keep only
192
+ // an exact basename hit so a suggestion is a real alternative spelling.
193
+ if (p && path.basename(p) === base)
194
+ matches.push(p);
195
+ }
196
+ }
197
+ catch {
198
+ // Not a git checkout, or git unavailable — no suggestions to offer.
199
+ }
200
+ }
201
+ return matches.slice(0, MAX_PATH_SUGGESTIONS);
202
+ }
203
+ /**
204
+ * Check every `sourceFile`/`sourceSymbol` citation on the submitted issuesFound
205
+ * entries against the repository, and say which repo each one belongs to.
206
+ * Returns one message per DEFINITE mismatch — an empty `invalid` means the report
207
+ * may be written — plus, per entry, the owner/repo the citation resolved in.
208
+ *
209
+ * The attribution is the reason this resolves against every checkout instead of
210
+ * the first hit: the consumer must not have to guess a cited file's repo from the
211
+ * path, which is what makes it link the primary repo's copy of a related repo's
212
+ * file. Where the citation resolves is the answer, so it is returned rather than
213
+ * discarded. Only a related repo is stamped: an absent `repository` already means
214
+ * the primary repo downstream, so a primary-repo citation ships as submitted.
215
+ *
216
+ * Rejects four things: a path that no known checkout contains, a symbol absent
217
+ * from the file that was found, an attribution that names a different repo than
218
+ * the one holding the code, and a file that resolves in several named checkouts
219
+ * with no attribution to choose between them. Everything uncertain is ACCEPTED —
220
+ * no checkout, an unreadable file, a checkout with no owner/repo, a directory
221
+ * listing that fails. The check must behave the same on any repo, in any
222
+ * language, at any size, and it must never become a loop the agent cannot exit.
223
+ * Symbol matching is a plain substring search for the same reason: no parser, so
224
+ * no language it silently mishandles.
225
+ */
226
+ export async function findInvalidSourceCitations(input) {
227
+ const invalid = [];
228
+ const repository = [];
229
+ const checkouts = input.checkouts.filter((c) => c.root && c.root !== "unknown");
230
+ if (checkouts.length === 0)
231
+ return { invalid, repository }; // Cannot verify anything — accept.
232
+ const roots = checkouts.map((c) => c.root);
233
+ for (let i = 0; i < input.issues.length; i++) {
234
+ const cited = input.issues[i].sourceFile?.trim();
235
+ if (!cited)
236
+ continue; // No citation on this entry — nothing to check.
237
+ const candidates = [];
238
+ for (const checkout of checkouts) {
239
+ const abs = resolveInRoot(checkout.root, cited);
240
+ if (abs)
241
+ candidates.push({ checkout, abs });
242
+ }
243
+ if (candidates.length === 0) {
244
+ invalid.push(`issuesFound[${i}] cites ${cited}, which resolves outside every repository checkout in this run. ` +
245
+ `Cite a path inside the checkout, relative to the repository root. ${CITATION_REMEDY}`);
246
+ continue;
247
+ }
248
+ // One relative path can exist in several checkouts of a multi-repo run, and
249
+ // the citation does not say which one it means. Keep every hit, so the symbol
250
+ // below is looked for in all of them instead of only the first.
251
+ const found = [];
252
+ let unreadable = false;
253
+ for (const candidate of candidates) {
254
+ try {
255
+ if (!(await fs.stat(candidate.abs)).isFile())
256
+ continue;
257
+ // stat() and readFile() both follow symlinks, so a link out of the tree
258
+ // would let a citation resolve against content outside the checkout.
259
+ // Compare the real paths before the file counts as found.
260
+ if (isInsideRoot(await fs.realpath(candidate.checkout.root), await fs.realpath(candidate.abs))) {
261
+ found.push(candidate);
262
+ }
263
+ }
264
+ catch (err) {
265
+ // "Not there" is the answer we want. Any other error (permissions, I/O)
266
+ // means the check could not run, which is never grounds to reject.
267
+ if (err.code !== "ENOENT")
268
+ unreadable = true;
269
+ }
270
+ }
271
+ if (found.length === 0) {
272
+ if (unreadable)
273
+ continue; // Could not look — accept.
274
+ const suggestions = await suggestSimilarPaths(roots, cited);
275
+ invalid.push(`issuesFound[${i}] cites ${cited} but that file does not exist in the repository` +
276
+ (suggestions.length > 0 ? ` (closest matches: ${suggestions.join(", ")})` : "") +
277
+ `. ${CITATION_REMEDY}`);
278
+ continue;
279
+ }
280
+ // The citation resolves in every checkout of `found`; a symbol narrows that to
281
+ // the copies that actually contain it, which is what the attribution below is
282
+ // taken from.
283
+ let resolved = found;
284
+ const symbol = input.issues[i].sourceSymbol?.trim();
285
+ if (symbol) {
286
+ const withSymbol = [];
287
+ let readAny = false;
288
+ for (const hit of found) {
289
+ try {
290
+ const content = await fs.readFile(hit.abs, "utf8");
291
+ readAny = true;
292
+ if (content.includes(symbol))
293
+ withSymbol.push(hit);
294
+ }
295
+ catch {
296
+ // Could not read this copy — try the next one.
297
+ }
298
+ }
299
+ if (!readAny)
300
+ continue; // Could not read any of them — accept.
301
+ if (withSymbol.length === 0) {
302
+ invalid.push(`issuesFound[${i}] cites ${symbol} in ${cited}, but that text does not appear anywhere in the file. ` +
303
+ CITATION_REMEDY);
304
+ continue;
305
+ }
306
+ resolved = withSymbol;
307
+ }
308
+ const attribution = attributeCitation({
309
+ entry: `issuesFound[${i}]`,
310
+ cited,
311
+ symbol,
312
+ declared: input.issues[i].repository?.trim(),
313
+ resolved: resolved.map((hit) => hit.checkout),
314
+ });
315
+ if (attribution.message)
316
+ invalid.push(attribution.message);
317
+ repository[i] = attribution.repository;
318
+ }
319
+ return { invalid, repository };
320
+ }
321
+ /** Distinct owner/repo names of the checkouts a citation resolved in, in order.
322
+ * A checkout the run could not name contributes nothing: it can be neither
323
+ * stamped nor offered as a candidate. */
324
+ function namesOf(resolved) {
325
+ return [...new Set(resolved.map((c) => c.repository).filter((r) => !!r))];
326
+ }
327
+ /**
328
+ * Reconcile where a citation resolved with what the entry says about it, giving
329
+ * back the owner/repo to stamp, a rejection message, or neither.
330
+ */
331
+ function attributeCitation(args) {
332
+ const { entry, cited, symbol, declared, resolved } = args;
333
+ const names = namesOf(resolved);
334
+ const where = symbol ? `${cited} (${symbol})` : cited;
335
+ if (declared) {
336
+ // The entry names its repo, so the citation is checked against THAT checkout.
337
+ // owner/repo is case-insensitive on GitHub, so a differently-cased attribution
338
+ // is the same repo — accepted, and re-stamped in the run's own spelling rather
339
+ // than sent back for a rewrite that would change nothing.
340
+ const match = resolved.find((c) => c.repository && c.repository.toLowerCase() === declared.toLowerCase());
341
+ if (match)
342
+ return match.repository === declared ? {} : { repository: match.repository };
343
+ // It resolved somewhere else. Only say so when the run can name where —
344
+ // otherwise there is nothing to correct the attribution to.
345
+ if (names.length === 0)
346
+ return {};
347
+ return {
348
+ message: `${entry} is attributed to ${declared}, but ${where} resolves in ${names.join(", ")} — not in ${declared}. ` +
349
+ `Set repository to the repo that holds the code, or cite a file that exists in ${declared}.`,
350
+ };
351
+ }
352
+ if (resolved.length > 1) {
353
+ // Several checkouts hold this path, and the entry does not say which is meant.
354
+ if (names.length > 1) {
355
+ return {
356
+ message: `${entry} cites ${where}, which exists in more than one repository of this run (${names.join(", ")}), ` +
357
+ `and the entry does not say which one it is about. Set repository to the owner/repo whose file you read.`,
358
+ };
359
+ }
360
+ return {}; // Cannot name the alternatives — accept, and stamp nothing.
361
+ }
362
+ // Exactly one checkout. Stamp it unless it is the primary, whose findings are
363
+ // identified downstream by having NO repository — see SourceCitationCheck.
364
+ const [only] = resolved;
365
+ return only.primary || !only.repository ? {} : { repository: only.repository };
366
+ }
@@ -0,0 +1,3 @@
1
+ export declare function isBrowserTestType(testType?: string): boolean;
2
+ export declare function isModularizeFirstTarget(testType?: string, language?: string): boolean;
3
+ export declare function isPomAwareTarget(language: string, framework?: string, testType?: string): boolean;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Routing predicates for the code-reuse flow. Kept apart from the prompt
3
+ * modules so the generation services can route on them without importing the
4
+ * prompt text and its POM-catalog dependencies.
5
+ */
6
+ import { isPomReuseEnabled, isUtilsReuseEnabled } from "./featureFlags.js";
7
+ // Page objects model browser pages — mapping an API-level test onto them is never
8
+ // meaningful, so only browser test types (or an unspecified type, for backwards
9
+ // compatibility) may take the POM path regardless of the feature flag.
10
+ export function isBrowserTestType(testType) {
11
+ return testType === undefined || testType === "ui" || testType === "e2e";
12
+ }
13
+ // Modularize-first flows: fresh codegen output carries no helper functions, so
14
+ // skyramp_modularization runs BEFORE skyramp_reuse_code and the reuse pass seeds
15
+ // the shared utils file from the helpers it created. The utils flag is the
16
+ // single gate for every test type — the modularization prompts' per-step
17
+ // extraction reads the same predicate, so the hand-off, the extraction and the
18
+ // seeding step agree on every codeReuse flow. Integration qualifies whenever the flag is on;
19
+ // UI only while the POM path is off (POM refactoring supersedes modularization,
20
+ // and the testbot UI flow asks for codeReuse unconditionally, so the flag is what
21
+ // keeps the default-off behavior a no-op reuse pass). e2e and load are not
22
+ // routed here: load has no modularization routing at all, e2e is deferred.
23
+ // Deliberately keyed on the POM *flag*, not on isPomAwareTarget: with the flag
24
+ // on, a UI target the POM path does not cover keeps today's plain reuse pass.
25
+ // Composing utils reuse with the POM path is a separate change; this predicate
26
+ // only decides the flag-off UI flow. The modularization prompts read it too, so
27
+ // extraction and seeding agree for every codeReuse flow; a modularizeCode-only
28
+ // call (no codeReuse) still receives the shared-helper rules — harmless without
29
+ // a reuse pass, and the tool cannot see codeReuse.
30
+ export function isModularizeFirstTarget(testType, language) {
31
+ if (!isUtilsReuseEnabled())
32
+ return false;
33
+ if (testType === "integration")
34
+ return true;
35
+ if (testType !== "ui" || isPomReuseEnabled())
36
+ return false;
37
+ // Shared browser helpers are Playwright TS/JS; other UI languages have no
38
+ // utils-file convention here and would fall through to a Python file name.
39
+ const lang = language?.toLowerCase();
40
+ return lang === "typescript" || lang === "javascript";
41
+ }
42
+ export function isPomAwareTarget(language, framework, testType) {
43
+ if (!isPomReuseEnabled())
44
+ return false;
45
+ if (!isBrowserTestType(testType))
46
+ return false;
47
+ const lang = language.toLowerCase();
48
+ return ((lang === "typescript" || lang === "javascript") &&
49
+ framework?.toLowerCase() === "playwright");
50
+ }
@@ -35,6 +35,8 @@ export declare function parseRouteLine(line: string, sourceFile: string): Parsed
35
35
  * aggregator's `APIRouter(prefix="/api")` yields `/api/recipes`, not `/recipes`.
36
36
  */
37
37
  export declare function parseFileEndpoints(content: string, sourceFile: string, mountPrefixes?: Map<string, string>): ParsedDiffEndpoint[];
38
+ export declare function isParamSegment(segment: string): boolean;
39
+ export declare function isOpaqueIdSegment(segment: string): boolean;
38
40
  export declare const SKIP_PATH_SEGMENTS: Set<string>;
39
41
  /**
40
42
  * True for version/prefix path segments — "v1".."v99" and the literal "api".
@@ -127,10 +127,27 @@ export function parseRouteLine(line, sourceFile) {
127
127
  };
128
128
  }
129
129
  }
130
- // Go (Gin, Echo, Chi): <ident>.GET/POST/...("/path", handler)
131
- const ginMatch = stripped.match(/(?:\w+)\.(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s*\(\s*["']([^"'?#]+)/i);
132
- if (ginMatch && sourceFile.endsWith(".go")) {
133
- return { method: ginMatch[1].toUpperCase(), path: ginMatch[2], sourceFile };
130
+ // Go (Gin, Echo, Chi): <ident>.GET/POST/...("path", handler)
131
+ //
132
+ // An all-caps method token (Gin/Echo convention: `g.GET(...)`, `r.POST(...)`)
133
+ // is unambiguously a route registration — nobody writes `query.GET(...)` — so
134
+ // it's accepted even without a leading slash (real Gin routes like
135
+ // `g.GET("ping", ...)` omit it). A CamelCase token (Chi's `r.Get`/`r.Post`)
136
+ // shares call syntax with a plain getter — `query.Get("term")`,
137
+ // `Header.Get("Accept-Version")` — so it only counts as a route when the
138
+ // captured path starts with "/", which every Chi route does and a getter's
139
+ // argument never does.
140
+ const ginUpperMatch = stripped.match(/\w+\.(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s*\(\s*["']([^"'?#]+)/);
141
+ if (ginUpperMatch && sourceFile.endsWith(".go")) {
142
+ return { method: ginUpperMatch[1], path: ginUpperMatch[2], sourceFile };
143
+ }
144
+ const ginCamelMatch = stripped.match(/\w+\.(Get|Post|Put|Patch|Delete|Head|Options)\s*\(\s*["'](\/[^"'?#]*)/);
145
+ if (ginCamelMatch && sourceFile.endsWith(".go")) {
146
+ return {
147
+ method: ginCamelMatch[1].toUpperCase(),
148
+ path: ginCamelMatch[2],
149
+ sourceFile,
150
+ };
134
151
  }
135
152
  // Go stdlib: http.HandleFunc/Handle
136
153
  const goHandleMatch = stripped.match(/(?:HandleFunc|Handle)\s*\(\s*["']([^"'?#]+)/);
@@ -490,6 +507,45 @@ export function parseFileEndpoints(content, sourceFile, mountPrefixes) {
490
507
  path: ep.path && !ep.path.startsWith("/") ? `/${ep.path}` : ep.path,
491
508
  }));
492
509
  }
510
+ /**
511
+ * One path segment that names a parameter rather than a resource, in any route
512
+ * syntax the scanners read: OpenAPI "{id}", Express/NestJS ":id", and Next.js
513
+ * "[id]" / "[...id]" / "[[...id]]" (the double-bracket form needs its own
514
+ * alternative — it contains an inner "]", which the single-bracket form's
515
+ * [^\]]+ can never span).
516
+ */
517
+ // The colon form may carry an Express/Fastify constraint or modifier —
518
+ // `:id(\d+)`, `:id?`, `:id*`, `:id+`. Those are still parameters, and a
519
+ // resource name is never one, so the predicate has to accept them or
520
+ // extractResourceFromPath returns the whole segment as the resource.
521
+ const PARAM_SEGMENT_RE = /^\{[^}]+\}$|^:[A-Za-z_]\w*(?:\([^)]*\))?[?*+]?$|^\[\[[^\]]+\]\]$|^\[[^\]]+\]$/;
522
+ export function isParamSegment(segment) {
523
+ return PARAM_SEGMENT_RE.test(segment);
524
+ }
525
+ /**
526
+ * One path segment that is an id VALUE rather than a name: a UUID, digits only,
527
+ * or a long hex run (a Mongo ObjectId is 24). A caller writes a literal id in a
528
+ * path where the route declares a parameter, and an id can never be a resource
529
+ * name, so resource derivation must skip it exactly as it skips a parameter.
530
+ * SKYR-4214: an accepted UUID produced the key
531
+ * "DELETE::a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11::integration", which matches
532
+ * nothing, so the candidate escaped dedup entirely.
533
+ *
534
+ * The 16-character floor on the hex rule keeps real resource names safe: a name
535
+ * built only from the letters a-f and digits, 16 characters or longer, is not a
536
+ * word.
537
+ */
538
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
539
+ export function isOpaqueIdSegment(segment) {
540
+ // The sign belongs to the value: an edge-case step writes `/orders/-1` to
541
+ // probe a negative id, and without the sign that segment became the resource
542
+ // name, so the key could never match real order coverage.
543
+ return UUID_RE.test(segment) || /^-?\d+$/.test(segment) || /^[0-9a-f]{16,}$/i.test(segment);
544
+ }
545
+ /** A segment that names neither a resource nor a sub-resource. */
546
+ function isNonResourceSegment(segment) {
547
+ return isParamSegment(segment) || isOpaqueIdSegment(segment);
548
+ }
493
549
  export const SKIP_PATH_SEGMENTS = new Set(["api", "v1", "v2", "v3", "public"]);
494
550
  /**
495
551
  * True for version/prefix path segments — "v1".."v99" and the literal "api".
@@ -510,14 +566,15 @@ export function isVersionLikeSegment(segment) {
510
566
  */
511
567
  export function extractResourceFromPath(endpointPath) {
512
568
  const segments = endpointPath.split("/").filter(Boolean);
513
- const meaningful = segments.filter((s) => !s.startsWith("{") && !SKIP_PATH_SEGMENTS.has(s));
569
+ const meaningful = segments.filter((s) => !isNonResourceSegment(s) && !SKIP_PATH_SEGMENTS.has(s));
514
570
  if (meaningful.length === 0)
515
571
  return "unknown";
516
- // Sub-resource pattern: /…/<parent>/{param}/<child>
517
- // Detected when the last raw segment is non-param and the second-to-last is a param.
572
+ // Sub-resource pattern: /…/<parent>/<id>/<child>
573
+ // Detected when the last raw segment names a resource and the one before it
574
+ // does not — an id in any spelling, not only "{param}".
518
575
  const last = segments[segments.length - 1];
519
576
  const secondLast = segments.length >= 2 ? segments[segments.length - 2] : "";
520
- if (!last.startsWith("{") && secondLast.startsWith("{") && meaningful.length >= 2) {
577
+ if (!isNonResourceSegment(last) && isNonResourceSegment(secondLast) && meaningful.length >= 2) {
521
578
  return `${meaningful[meaningful.length - 2]}_${meaningful[meaningful.length - 1]}`;
522
579
  }
523
580
  return meaningful[meaningful.length - 1];
@@ -46,7 +46,7 @@ export declare function draftScenariosFromEndpoints(endpoints: Array<EndpointInp
46
46
  /**
47
47
  * Enforce a global cap on drafted scenarios while preserving category diversity.
48
48
  *
49
- * 1. CRITICAL (new_endpoint / diff-direct) scenarios prioritized first.
49
+ * 1. CRITICAL (bug_caught) scenarios prioritized first.
50
50
  * 2. One scenario per non-empty category guaranteed (breadth).
51
51
  * 3. Remaining budget filled by priority tier (HIGH > MEDIUM > LOW).
52
52
  * 4. Hard cap at MAX_TOTAL_SCENARIOS — applied to the combined output.