@skyramp/mcp 0.4.2 → 0.4.3-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 (119) hide show
  1. package/build/execution/wrapperConfig.d.ts +4 -0
  2. package/build/execution/wrapperConfig.js +14 -2
  3. package/build/prompts/code-reuse.d.ts +5 -1
  4. package/build/prompts/code-reuse.js +16 -10
  5. package/build/prompts/modularization/ui-test-modularization.js +9 -2
  6. package/build/prompts/pom-aware-code-reuse.d.ts +4 -1
  7. package/build/prompts/pom-aware-code-reuse.js +38 -6
  8. package/build/prompts/reuse-hand-off.d.ts +4 -0
  9. package/build/prompts/reuse-hand-off.js +10 -5
  10. package/build/prompts/shared-helper-policy.d.ts +5 -0
  11. package/build/prompts/shared-helper-policy.js +5 -0
  12. package/build/prompts/test-maintenance/driftAnalysisShared.js +2 -0
  13. package/build/prompts/test-maintenance/uiDriftAnalysisSections.js +2 -1
  14. package/build/prompts/testbot/testbot-prompts.js +33 -16
  15. package/build/recommendation/fakeVerifyContext.d.ts +3 -0
  16. package/build/recommendation/fakeVerifyContext.js +16 -0
  17. package/build/recommendation/runVerifiers.js +2 -0
  18. package/build/recommendation/types.d.ts +12 -0
  19. package/build/recommendation/verifierContracts.d.ts +24 -2
  20. package/build/recommendation/verifierContracts.js +32 -4
  21. package/build/recommendation/verifiers/changedLinesClaimed.d.ts +17 -0
  22. package/build/recommendation/verifiers/changedLinesClaimed.js +88 -0
  23. package/build/recommendation/verifiers/expectedValueSourced.js +8 -0
  24. package/build/recommendation/verifiers/requirementSourced.js +141 -11
  25. package/build/services/TestGenerationService.js +4 -0
  26. package/build/tools/code-refactor/caller-gate.d.ts +105 -0
  27. package/build/tools/code-refactor/caller-gate.js +174 -0
  28. package/build/tools/code-refactor/changed-helper-report.d.ts +62 -0
  29. package/build/tools/code-refactor/changed-helper-report.js +114 -0
  30. package/build/tools/code-refactor/codeReuseTool.js +131 -62
  31. package/build/tools/code-refactor/enhanceAssertionsTool.js +2 -2
  32. package/build/tools/code-refactor/helper-callers.d.ts +65 -0
  33. package/build/tools/code-refactor/helper-callers.js +85 -0
  34. package/build/tools/code-refactor/modularizationTool.js +10 -0
  35. package/build/tools/code-refactor/pom-pass-state.d.ts +51 -0
  36. package/build/tools/code-refactor/pom-pass-state.js +115 -0
  37. package/build/tools/code-refactor/retrofit-state.d.ts +3 -2
  38. package/build/tools/code-refactor/retrofit-state.js +11 -12
  39. package/build/tools/code-refactor/reuse-outcome.d.ts +6 -0
  40. package/build/tools/code-refactor/reuse-record-store.d.ts +52 -0
  41. package/build/tools/code-refactor/reuse-record-store.js +126 -0
  42. package/build/tools/code-refactor/reuse-state.d.ts +18 -6
  43. package/build/tools/code-refactor/reuse-state.js +78 -131
  44. package/build/tools/code-refactor/utils-verify-gates.js +3 -3
  45. package/build/tools/code-refactor/verify-gates.d.ts +4 -0
  46. package/build/tools/code-refactor/verify-gates.js +8 -1
  47. package/build/tools/generate-tests/generateContractRestTool.js +7 -7
  48. package/build/tools/generate-tests/generateIntegrationRestTool.js +8 -8
  49. package/build/tools/generate-tests/generateUIRestTool.js +3 -3
  50. package/build/tools/submitReportTool.js +91 -29
  51. package/build/tools/test-management/actionsTool.js +7 -0
  52. package/build/tools/test-management/registerTestPlanTool.d.ts +1 -0
  53. package/build/tools/test-management/registerTestPlanTool.js +70 -22
  54. package/build/types/RepositoryAnalysis.d.ts +20 -20
  55. package/build/types/ReuseOutcome.d.ts +96 -0
  56. package/build/types/TestExecution.d.ts +11 -0
  57. package/build/types/TestExecution.js +19 -0
  58. package/build/types/index.d.ts +1 -1
  59. package/build/utils/branchDiff.d.ts +1 -1
  60. package/build/utils/branchDiff.js +1 -1
  61. package/build/utils/changedRuns.d.ts +13 -0
  62. package/build/utils/changedRuns.js +56 -0
  63. package/build/utils/featureFlags.d.ts +8 -10
  64. package/build/utils/featureFlags.js +23 -21
  65. package/build/utils/normalizeSkyrampImports.d.ts +4 -1
  66. package/build/utils/normalizeSkyrampImports.js +24 -17
  67. package/build/utils/pom-scope/ownership.d.ts +19 -0
  68. package/build/utils/pom-scope/ownership.js +31 -0
  69. package/build/utils/pom-scope/pom-files.d.ts +8 -0
  70. package/build/utils/pom-scope/pom-files.js +8 -0
  71. package/build/utils/pom-scope/scoring.js +13 -1
  72. package/build/utils/pom-scope/strip.d.ts +10 -0
  73. package/build/utils/pom-scope/strip.js +9 -0
  74. package/build/utils/pom-verify/verify.js +13 -2
  75. package/build/utils/reuseRouting.d.ts +30 -2
  76. package/build/utils/reuseRouting.js +36 -15
  77. package/build/utils/testDependencyPolicy.js +4 -16
  78. package/build/utils/utils-verify/action-sites.d.ts +2 -2
  79. package/build/utils/utils-verify/body-reach.d.ts +2 -2
  80. package/build/utils/utils-verify/body-reach.js +4 -1
  81. package/build/utils/utils-verify/call-sites.d.ts +16 -6
  82. package/build/utils/utils-verify/call-sites.js +12 -6
  83. package/build/utils/utils-verify/changed-helpers.d.ts +38 -0
  84. package/build/utils/utils-verify/changed-helpers.js +91 -0
  85. package/build/utils/utils-verify/head.d.ts +48 -0
  86. package/build/utils/utils-verify/head.js +115 -0
  87. package/build/utils/utils-verify/importers.d.ts +59 -10
  88. package/build/utils/utils-verify/importers.js +121 -31
  89. package/build/utils/utils-verify/in-house.js +26 -31
  90. package/build/utils/utils-verify/index.d.ts +3 -0
  91. package/build/utils/utils-verify/index.js +3 -0
  92. package/build/utils/utils-verify/language-spec.js +10 -2
  93. package/build/utils/utils-verify/locate.d.ts +3 -0
  94. package/build/utils/utils-verify/locate.js +6 -6
  95. package/build/utils/utils-verify/parse.d.ts +13 -1
  96. package/build/utils/utils-verify/parse.js +122 -49
  97. package/build/utils/utils-verify/retrofit-equivalence.d.ts +0 -5
  98. package/build/utils/utils-verify/retrofit-equivalence.js +2 -34
  99. package/build/utils/utils-verify/stage.js +4 -16
  100. package/build/utils/utils-verify/verify.d.ts +9 -3
  101. package/build/utils/utils-verify/verify.js +26 -12
  102. package/node_modules/playwright/lib/dom-analyzer/blueprint.js +1 -0
  103. package/node_modules/playwright/lib/dom-analyzer/dynamicId.js +1 -0
  104. package/node_modules/playwright/lib/dom-analyzer/dynamicId.test.js +6 -0
  105. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +303 -150
  106. package/node_modules/playwright/node_modules/playwright-core/lib/generated/pollingRecorderSource.js +1 -1
  107. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-LNgEKtdV.js → codeMirrorModule-Bou8kKzE.js} +1 -1
  108. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{defaultSettingsView-Bwr1eMKC.js → defaultSettingsView-DTp2-8Si.js} +3 -3
  109. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.BAkLd5DX.js → index.ZYuEFHsi.js} +1 -1
  110. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +2 -2
  111. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{uiMode.BPopbasy.js → uiMode.GLqsx5cI.js} +1 -1
  112. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +2 -2
  113. package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
  114. package/node_modules/playwright/node_modules/playwright-core/src/generated/pollingRecorderSource.ts +1 -1
  115. package/node_modules/playwright/package.json +1 -1
  116. package/package.json +2 -2
  117. package/plugin/prompts/code-reuse/hand-off.md +13 -1
  118. package/plugin/prompts/plan-tests.md +2 -2
  119. package/plugin/skills/enhance-assertions/reference/shared-rules.md +1 -0
@@ -4,11 +4,13 @@ import * as path from "path";
4
4
  import { logger } from "../../utils/logger.js";
5
5
  import { toolError } from "../../utils/utils.js";
6
6
  import { getCodeReusePrompt } from "../../prompts/code-reuse.js";
7
- import { isPomAwareTarget } from "../../utils/reuseRouting.js";
7
+ import { reuseRouteFor, isPomFirstTarget, } from "../../utils/reuseRouting.js";
8
+ import { pageObjectCandidates } from "../../utils/pom-scope/ownership.js";
8
9
  import { selectScopedPoms, } from "../../utils/pom-scope/index.js";
9
10
  import { verifyReuse } from "../../utils/pom-verify/index.js";
10
11
  import { infraGateFailure, zeroReuseGateFailure, composeVerifyText, } from "./verify-gates.js";
11
12
  import { recordCandidates, recordNoPomLayer, recordVerifyOutcome, recordUtilsReuseIssued, recordUtilsVerifyOutcome, recordUtilsVerifyError, pendingModularization, utilsBaselineFor, } from "./reuse-state.js";
13
+ import { pomPassOutcome, recordPomPassFinished } from "./pom-pass-state.js";
12
14
  import { formatUtilsVerifyText } from "./utils-verify-gates.js";
13
15
  import { utilsSpecFor, verifyUtils } from "../../utils/utils-verify/index.js";
14
16
  import { codeRefactoringSchema, languageSchema, TestType, } from "../../types/TestTypes.js";
@@ -57,7 +59,7 @@ const codeReuseSchema = z.object({
57
59
  .describe("Helpers the search read and did not call, one entry each, with the reason."),
58
60
  })
59
61
  .optional()
60
- .describe("Verify mode, UI tests only (the SkyrampUtils path with page-object reuse off): what the search for the repository's own browser helper modules read and declined. These are DECLARATIONS — the report records them as what the run states it read, labelled as such, not as a measurement. Pass them on the `verify: true` call once the reuse edits are written."),
62
+ .describe("Verify mode, UI tests only (the SkyrampUtils path, which follows the page-object pass when page-object reuse is on): what the search for the repository's own browser helper modules read and declined. These are DECLARATIONS — the report records them as what the run states it read, labelled as such, not as a measurement. Pass them on the `verify: true` call once the reuse edits are written."),
61
63
  });
62
64
  const TOOL_NAME = "skyramp_reuse_code";
63
65
  /**
@@ -103,12 +105,23 @@ const DISCOVER_FIRST_NOTICE = `VERIFICATION NOT RUN — this test file has not b
103
105
  Discovery is per test file: it scopes page-object candidates to this file's own selectors, so discovery done for another spec does not cover this one. Verification confirms only that the page-object calls a spec ALREADY contains resolve against their source — it cannot name the calls the spec should have made. That is why verifying first reports a spec that reuses almost nothing as PASSED.
104
106
 
105
107
  Follow the result below for this test file; it states what to do next.`;
106
- // Only advertised when SKYRAMP_FEATURE_POM_REUSE=1 with the flag off this tool
107
- // never takes the POM path, so describing it would promise behavior it won't do.
108
- const POM_AWARE_MODE_DESCRIPTION = `
108
+ /** The page-object paragraph of the tool description ONE text per configuration,
109
+ * read at register time like the flag that gates it (nothing here is decided at
110
+ * import). Only advertised with SKYRAMP_FEATURE_POM_REUSE=1: with the flag off this
111
+ * tool never takes the POM path, so describing it would promise behavior it won't do.
112
+ * With shared-helper reuse on as well, a UI test takes both passes, and the text says
113
+ * that instead of a rule and an exception that contradict each other. */
114
+ function pomAwareModeDescription() {
115
+ if (!isPomReuseEnabled())
116
+ return "";
117
+ const head = `
109
118
 
110
119
  **POM-AWARE MODE (TypeScript/JavaScript + Playwright):**
111
- For projects with \`language: "typescript" or "javascript"\` and \`framework: "playwright"\`, this tool first checks whether the project has an existing Page Object Model (POM) library (\`pageobjects/\` directories or \`*.page.ts\` / \`*.page.js\` files). If POMs are found, the test is refactored to use those POM classes and methods — replacing raw inline locators with the existing abstractions. SkyrampUtils consolidation is skipped on this path. If no POMs are found, the tool falls back to the standard SkyrampUtils path. New POM creation is out of scope — existing POMs are reused only. After POM-aware code reuse, do NOT call skyramp_modularization — on this path that supersedes WORKFLOW SUMMARY step 6.`;
120
+ For projects with \`language: "typescript" or "javascript"\` and \`framework: "playwright"\`, this tool first checks whether the project has an existing Page Object Model (POM) library (\`pageobjects/\` directories or \`*.page.ts\` / \`*.page.js\` files). If POMs are found, the test is refactored to use those POM classes and methods — replacing raw inline locators with the existing abstractions. New POM creation is out of scope — existing POMs are reused only.`;
121
+ return isUtilsReuseEnabled()
122
+ ? `${head} For a UI test (\`testType: "ui"\`) this is the FIRST of two passes: once its verification reports PASSED — or it answers "No reusable POM layer detected" — the generation result's next steps call skyramp_modularization and then this tool again, which returns the SkyrampUtils workflow for the steps the POM pass left inline, and whose verify: true call checks both the POM calls and the shared helpers. For any other test type the POM pass is the only pass: do NOT call skyramp_modularization after it. SkyrampUtils consolidation happens only in that second pass, never during the POM pass.`
123
+ : `${head} SkyrampUtils consolidation is skipped on this path. If no POM layer is found, the tool says so and the test is left as generated. After POM-aware code reuse, do NOT call skyramp_modularization — on this path that supersedes WORKFLOW SUMMARY step 6.`;
124
+ }
112
125
  /** Whether a `verify: true` call must be turned into the discovery pass it skipped.
113
126
  *
114
127
  * The two modes return very different things — discovery returns the whole refactoring
@@ -119,11 +132,26 @@ const POM_AWARE_MODE_DESCRIPTION = `
119
132
  * `isPomAwareTarget` also carries the feature flag and the browser-test-type check, so a
120
133
  * call that could never have reached the POM procedure is never refused for lacking it.
121
134
  * Enforced only inside a run — see `markKeyFor`. */
122
- function mustDiscoverFirst(params, markKey) {
135
+ function mustDiscoverFirst(markKey, route) {
123
136
  return (markKey !== undefined &&
124
- isPomAwareTarget(params.language, params.framework, params.testType) &&
137
+ route.pass === "pom" &&
125
138
  !discoveredSpecs.has(markKey));
126
139
  }
140
+ /** The route for this call, resolved ONCE and handed to every branch, so discovery,
141
+ * verify, the prompt and the modularization check cannot disagree. A page-object
142
+ * target takes the page-object pass — except with both reuse settings on once that
143
+ * pass has finished for the spec: from then on the spec is in the SkyrampUtils flow,
144
+ * which runs over what the page-object pass left inline. */
145
+ async function resolveReuseRoute(params) {
146
+ const finished = isPomFirstTarget(params.language, params.testType) &&
147
+ (await pomPassOutcome(params.testFile)) !== undefined;
148
+ return reuseRouteFor(params.language, params.framework, params.testType, finished);
149
+ }
150
+ function verifyToolResult(v) {
151
+ return v.error
152
+ ? toolError(v.text)
153
+ : { content: [{ type: "text", text: v.text }] };
154
+ }
127
155
  /** Verify mode for the SkyrampUtils path: stage the shared utils file(s) this spec
128
156
  * wrote or imports — located by header, in code, after the edits exist — then check
129
157
  * their invariants and record the verdict that clears the utils debt.
@@ -134,23 +162,15 @@ function mustDiscoverFirst(params, markKey) {
134
162
  async function runUtilsVerify(params) {
135
163
  if (!isUtilsReuseEnabled()) {
136
164
  return {
137
- content: [
138
- {
139
- type: "text",
140
- text: "VERIFICATION SKIPPED — SkyrampUtils code reuse is disabled (SKYRAMP_FEATURE_UTILS_REUSE). Nothing to verify; continue.",
141
- },
142
- ],
165
+ text: "VERIFICATION SKIPPED — SkyrampUtils code reuse is disabled (SKYRAMP_FEATURE_UTILS_REUSE). Nothing to verify; continue.",
166
+ passed: true,
143
167
  };
144
168
  }
145
169
  if (!utilsSpecFor(params.language)) {
146
170
  // Say so rather than record a NoUtilsFile verdict nothing established.
147
171
  return {
148
- content: [
149
- {
150
- type: "text",
151
- text: `VERIFICATION SKIPPED — shared-helper verification does not cover ${params.language}; nothing was staged or checked. Continue.`,
152
- },
153
- ],
172
+ text: `VERIFICATION SKIPPED — shared-helper verification does not cover ${params.language}; nothing was staged or checked. Continue.`,
173
+ passed: true,
154
174
  };
155
175
  }
156
176
  try {
@@ -168,18 +188,14 @@ async function runUtilsVerify(params) {
168
188
  r.ok = false;
169
189
  const recorded = await recordUtilsVerifyOutcome(params.testFile, r, params.language, params.framework, params.testType, undefined, params.inHouse);
170
190
  return {
171
- content: [
172
- {
173
- type: "text",
174
- text: formatUtilsVerifyText(r, {
175
- staged: staging.staged,
176
- // A refused in-house module is the customer's file, not ours to move.
177
- unstaged: staging.failed.filter((f) => !staging.inHouse.includes(f)),
178
- unstagedInHouse: staging.failed.filter((f) => staging.inHouse.includes(f)),
179
- recorded,
180
- }),
181
- },
182
- ],
191
+ text: formatUtilsVerifyText(r, {
192
+ staged: staging.staged,
193
+ // A refused in-house module is the customer's file, not ours to move.
194
+ unstaged: staging.failed.filter((f) => !staging.inHouse.includes(f)),
195
+ unstagedInHouse: staging.failed.filter((f) => staging.inHouse.includes(f)),
196
+ recorded,
197
+ }),
198
+ passed: r.ok,
183
199
  };
184
200
  }
185
201
  catch (err) {
@@ -188,30 +204,28 @@ async function runUtilsVerify(params) {
188
204
  error: String(err),
189
205
  });
190
206
  await recordUtilsVerifyError(params.testFile);
191
- return toolError(`VERIFICATION ERROR — the shared-helper verifier could not run: ${String(err)}. This is not something to retry blindly; ` +
192
- `it does not block skyramp_enhance_assertions or skyramp_execute_test. If the cause is in your files (unreadable utils file), fix it and verify once more.`);
207
+ return {
208
+ text: `VERIFICATION ERROR the shared-helper verifier could not run: ${String(err)}. This is not something to retry blindly; ` +
209
+ `it does not block skyramp_enhance_assertions or skyramp_execute_test. If the cause is in your files (unreadable utils file), fix it and verify once more.`,
210
+ passed: false,
211
+ error: true,
212
+ };
193
213
  }
194
214
  }
195
- /** Verify mode: check the page-object calls the delivered spec already contains. */
196
- async function runVerify(params) {
197
- // Route on the same predicate the prompt path routes on. A target the POM path
198
- // never took must not reach the POM verifier — it is the sole writer of the POM
199
- // reuse summary, and a POM-less run would otherwise emit POM numbers — so every
200
- // non-POM target, including browser tests with the POM flag off, takes the
201
- // SkyrampUtils verifier, which is what the prompt path handed it.
202
- if (!isPomAwareTarget(params.language, params.framework, params.testType)) {
203
- return runUtilsVerify(params);
204
- }
215
+ /** The page-object verifier and its gates, recording what the pass established. */
216
+ async function runPomVerify(params, composed) {
205
217
  try {
206
218
  const r = await verifyReuse(params.testFile, params.language);
207
- const gate = (await infraGateFailure(params, r)) ??
208
- (await zeroReuseGateFailure(params, r));
219
+ const target = { ...params, composed };
220
+ const gate = (await infraGateFailure(target, r)) ??
221
+ (await zeroReuseGateFailure(target, r));
209
222
  // Record what this pass established before returning the text report:
210
223
  // every number in it is already in hand here, so the agent is never
211
224
  // asked to read one back out and retype it later.
212
225
  await recordVerifyOutcome(params.testFile, r, gate !== undefined, params.language);
213
226
  return {
214
- content: [{ type: "text", text: composeVerifyText(gate, r) }],
227
+ text: composeVerifyText(gate, r),
228
+ passed: gate === undefined && r.ok,
215
229
  };
216
230
  }
217
231
  catch (err) {
@@ -219,14 +233,55 @@ async function runVerify(params) {
219
233
  testFile: params.testFile,
220
234
  error: String(err),
221
235
  });
222
- return toolError(`VERIFICATION ERROR (treat as unverified, do not retry more than once): ${String(err)}`);
236
+ return {
237
+ text: `VERIFICATION ERROR (treat as unverified, do not retry more than once): ${String(err)}`,
238
+ passed: false,
239
+ error: true,
240
+ };
241
+ }
242
+ }
243
+ /** Verify mode: check the page-object calls the delivered spec already contains. */
244
+ async function runVerify(params, route) {
245
+ // Route on the same decision the prompt path routes on. A call outside the POM
246
+ // pass must not reach the POM verifier alone — it is the sole writer of the POM
247
+ // reuse summary, and a POM-less run would otherwise emit POM numbers — so every
248
+ // other call, including browser tests with the POM flag off, takes the
249
+ // SkyrampUtils verifier, which is what the prompt path handed it.
250
+ if (route.pass === "utils") {
251
+ return verifyToolResult(route.composed
252
+ ? await runComposedVerify(params)
253
+ : await runUtilsVerify(params));
223
254
  }
255
+ const pom = await runPomVerify(params, route.composed);
256
+ // Both settings on: a PASSED page-object verify finishes that pass, and the next
257
+ // reuse call on the spec serves the SkyrampUtils flow over what it left inline.
258
+ if (pom.passed && route.composed)
259
+ await recordPomPassFinished(params.testFile);
260
+ return verifyToolResult(pom);
261
+ }
262
+ /** Both reuse settings on, after the page-object pass: one `verify: true` call checks
263
+ * both halves of a spec that calls page objects AND shared helpers. The shared-helper
264
+ * verifier writes the utils record; the page-object verifier — still the only writer
265
+ * of the page-object record — re-checks the page-object calls the helper pass may
266
+ * have moved. Skipped when the page-object pass found no layer: there is nothing to
267
+ * re-check, and a verdict here would publish page-object numbers for a repository
268
+ * without page objects. */
269
+ async function runComposedVerify(params) {
270
+ const utils = await runUtilsVerify(params);
271
+ if ((await pomPassOutcome(params.testFile)) !== "finished")
272
+ return utils;
273
+ const pom = await runPomVerify(params, true);
274
+ return {
275
+ text: `## Page-object calls (the page-object pass)\n\n${pom.text}\n\n## Shared helpers\n\n${utils.text}\n\nThis call checks both. Re-run it after every fix; finish only when both parts report PASSED.`,
276
+ passed: utils.passed && pom.passed,
277
+ ...(utils.error || pom.error ? { error: true } : {}),
278
+ };
224
279
  }
225
280
  /** Discovery mode: the POM-aware refactoring procedure, scoped to this one spec.
226
281
  *
227
282
  * Both the ordinary discovery call and the refused-verify path go through here, so the
228
283
  * workflow a refused verify receives cannot drift from the one a first call receives. */
229
- async function runDiscovery(params, markKey) {
284
+ async function runDiscovery(params, markKey, route) {
230
285
  logger.info("Analyzing code for reuse opportunities", {
231
286
  testFile: params.testFile,
232
287
  language: params.language,
@@ -237,7 +292,7 @@ async function runDiscovery(params, markKey) {
237
292
  // Deterministic POM pre-scoping (TS/JS+Playwright browser tests only).
238
293
  // Never blocks: any error or empty result falls back to the agent-driven STEP 1.
239
294
  let scopedPoms;
240
- const pomAware = isPomAwareTarget(params.language, params.framework, params.testType);
295
+ const pomAware = route.pass === "pom";
241
296
  // The SkyrampUtils path records its debt here, where the instructions are issued:
242
297
  // from this point the tools that follow reuse on this spec refuse until a
243
298
  // `verify: true` pass has staged and checked the utils file (SKYR-4220).
@@ -253,10 +308,19 @@ async function runDiscovery(params, markKey) {
253
308
  if (pomAware) {
254
309
  try {
255
310
  const t0 = Date.now();
256
- const { tier1, tier2, diagnostics } = await selectScopedPoms({
311
+ const scoped = await selectScopedPoms({
257
312
  testFile: params.testFile,
258
313
  specContent: params.prompt,
259
314
  });
315
+ const { diagnostics } = scoped;
316
+ // Both settings on: the page-object path owns class modules only; a
317
+ // free-function helper module is the in-house path's (pom-scope/ownership.ts).
318
+ const tier1 = route.composed
319
+ ? await pageObjectCandidates(scoped.tier1)
320
+ : scoped.tier1;
321
+ const tier2 = route.composed
322
+ ? await pageObjectCandidates(scoped.tier2)
323
+ : scoped.tier2;
260
324
  if (tier1.length + tier2.length > 0) {
261
325
  scopedPoms = { tier1, tier2 };
262
326
  // Only report candidates found through the conventional POM globs.
@@ -276,12 +340,17 @@ async function runDiscovery(params, markKey) {
276
340
  await recordCandidates(params.testFile, tier1.length, params.language);
277
341
  }
278
342
  }
279
- else if (diagnostics.selectors > 0 &&
280
- diagnostics.discovery !== "skipped-too-large") {
343
+ else if ((diagnostics.selectors > 0 &&
344
+ diagnostics.discovery !== "skipped-too-large") ||
345
+ // Every candidate was a free-function module, which is not this path's.
346
+ scoped.tier1.length + scoped.tier2.length > 0) {
281
347
  // A clean scan ran (glob or selector-grep) and found zero overlap — distinct from
282
348
  // "scoping wasn't attempted" (no selectors) or "scan was skipped" (repo too large).
283
349
  scopedPoms = { tier1: [], tier2: [], scannedNoOverlap: true };
284
350
  await recordNoPomLayer(params.testFile);
351
+ // Both settings on: no page-object layer finishes that pass at once.
352
+ if (route.composed)
353
+ await recordPomPassFinished(params.testFile);
285
354
  }
286
355
  logger.info("POM pre-scoping", {
287
356
  ...diagnostics,
@@ -294,7 +363,7 @@ async function runDiscovery(params, markKey) {
294
363
  logger.warning("POM pre-scoping failed — falling back to agent-driven detection", { error: String(err) });
295
364
  }
296
365
  }
297
- const codeReusePrompt = getCodeReusePrompt(params.testFile, params.language, params.framework, scopedPoms, params.testType);
366
+ const codeReusePrompt = getCodeReusePrompt(params.testFile, params.language, params.framework, scopedPoms, params.testType, route);
298
367
  // Marked on the last line before the return, so the mark cannot be observed until the
299
368
  // workflow it stands for has been built. An earlier mark let a verify arriving mid-scope
300
369
  // check a spec that had not been handed the procedure yet.
@@ -336,7 +405,7 @@ export function registerCodeReuseTool(server) {
336
405
  - No actual function definitions found in other test files
337
406
 
338
407
  Applicability is decided by the caller: when a generation result was produced with \`codeReuse: true\`, it directs you here — ALWAYS follow through (a no-op result is a normal outcome, never a failure). Multi-step tests (UI, E2E, INTEGRATION, LOAD) are the ones that benefit; simple single-request tests (smoke, contract, fuzz) do not request code reuse.
339
- The tool will provide step-by-step instructions that MUST be followed exactly.${isPomReuseEnabled() ? POM_AWARE_MODE_DESCRIPTION : ""}`,
408
+ The tool will provide step-by-step instructions that MUST be followed exactly.${pomAwareModeDescription()}`,
340
409
  inputSchema: codeReuseSchema.shape,
341
410
  _meta: {
342
411
  keywords: [
@@ -350,11 +419,12 @@ export function registerCodeReuseTool(server) {
350
419
  }, async (params) => {
351
420
  let errorResult;
352
421
  try {
422
+ const route = await resolveReuseRoute(params);
353
423
  if (params.verify) {
354
424
  const markKey = markKeyFor(params.testFile);
355
- if (mustDiscoverFirst(params, markKey)) {
425
+ if (mustDiscoverFirst(markKey, route)) {
356
426
  logger.info("Verify called before the discovery pass — returning the discovery workflow", { testFile: params.testFile });
357
- const workflow = await runDiscovery(params, markKey);
427
+ const workflow = await runDiscovery(params, markKey, route);
358
428
  return {
359
429
  content: [
360
430
  {
@@ -364,13 +434,12 @@ export function registerCodeReuseTool(server) {
364
434
  ],
365
435
  };
366
436
  }
367
- return await runVerify(params);
437
+ return await runVerify(params, route);
368
438
  }
369
439
  // Modularize-first targets owe skyramp_modularization before reuse is issued —
370
440
  // the other half of the pair the hand-off names. Only ever owed for a file
371
- // generation handed off, so NL callers and POM targets are untouched.
372
- if (isUtilsReuseEnabled() &&
373
- !isPomAwareTarget(params.language, params.framework, params.testType)) {
441
+ // generation handed off, so NL callers and the page-object pass are untouched.
442
+ if (isUtilsReuseEnabled() && route.pass === "utils") {
374
443
  const owed = await pendingModularization(params.testFile);
375
444
  if (owed) {
376
445
  errorResult = toolError(owed);
@@ -381,7 +450,7 @@ export function registerCodeReuseTool(server) {
381
450
  content: [
382
451
  {
383
452
  type: "text",
384
- text: await runDiscovery(params, markKeyFor(params.testFile)),
453
+ text: await runDiscovery(params, markKeyFor(params.testFile), route),
385
454
  },
386
455
  ],
387
456
  };
@@ -12,7 +12,7 @@ function buildAutoApplyInstructions(testFile, testType, enhanceType) {
12
12
  `Test type: ${testType} | Context: ${enhanceType}`,
13
13
  ``,
14
14
  `Read the file, apply the type-specific assertion guidance below, and write the file back directly.`,
15
- `Add assertions after each send_request/sendRequest status-code assertion.`,
15
+ `Add assertions after each send_request/sendRequest status-code assertion, except on a setup request.`,
16
16
  `Use SDK helper: Python \`skyramp.get_response_value(response, "json.path")\`, JS \`getValue(response, "json.path")\`.`,
17
17
  `Do NOT restructure, reformat, add comments, or change imports. Only add assertion lines.`,
18
18
  ``,
@@ -99,7 +99,7 @@ export function registerEnhanceAssertionsTool(server) {
99
99
  `Enhance response body assertions in: \`${testFile}\``,
100
100
  `Test type: ${testType} | Context: ${enhanceType}`,
101
101
  ``,
102
- `Read the file, add assertions after each send_request/sendRequest status-code assertion, write it back.`,
102
+ `Read the file, add assertions after each send_request/sendRequest status-code assertion, except on a setup request, write it back.`,
103
103
  `Use SDK helper: Python \`skyramp.get_response_value(response, "json.path")\`, JS \`getValue(response, "json.path")\`.`,
104
104
  ``,
105
105
  `Shared assertion rules (apply all that fit):`,
@@ -0,0 +1,65 @@
1
+ import type { HelperChangeKind } from "../../types/ReuseOutcome.js";
2
+ /**
3
+ * A helper this run changed under the callers it already had — the shared-helper
4
+ * blast radius nothing in the product measured.
5
+ *
6
+ * A shared helper's change reaches every caller at once. The run executes what it
7
+ * DELIVERS, and an untouched caller is not delivered, so a break in one reached the
8
+ * customer's own continuous integration instead of the report. Worse for a browser
9
+ * helper: a changed selector alters what every caller does and appears in no calling
10
+ * test's diff.
11
+ *
12
+ * TWO PREDICATES, never one. `change` is the claim that the helper differs from its
13
+ * incumbent, and rests only on reading the module at HEAD. `callers` present is the
14
+ * separate claim that the caller list can be believed, and rests on the importer walk
15
+ * having read the module's directory. The gate acts on POSITIVE evidence — a file
16
+ * that was read, that imports this module, and that names this helper — and never on
17
+ * the absence of one; the disclosure states the list only when it is present, and
18
+ * says the callers are unknown otherwise. One predicate driving both would make a
19
+ * read error either demand a run for a helper nobody calls or assert a caller set
20
+ * that is wrong.
21
+ */
22
+ export interface ChangedHelperView {
23
+ /** Absolute path of the shared module. */
24
+ utilsFile: string;
25
+ helper: string;
26
+ change: HelperChangeKind;
27
+ /** ABSOLUTE PATHS of the Skyramp-generated tests that IMPORT this helper — a
28
+ * superset of those that call it, since an imported binding need not be used.
29
+ * Detection stops at the import on purpose: a call can reach the helper through an
30
+ * alias, a re-export, or a local wrapper, and a syntactic call check would miss
31
+ * those. A missed caller is an unexecuted test that breaks in the customer's
32
+ * continuous integration; an unused import costs one extra execution of a test
33
+ * that is runnable by construction. This over-runs rather than under-runs.
34
+ *
35
+ * ABSENT when the importer walk did not run — the one case where a list would be
36
+ * an artefact rather than a lower bound. Present, it is a lower bound: the walk
37
+ * covers ONE directory, so a generated test importing this module from anywhere
38
+ * else is not here. An empty list is the positive statement that the walk ran and
39
+ * found none beside the module. */
40
+ callers?: string[];
41
+ }
42
+ /** A shared module of one directory as this run left it, with what it was. */
43
+ export interface ChangedModule {
44
+ /** Absolute path. A module that is gone is named by the path it had. */
45
+ file: string;
46
+ /** The module at HEAD — read once by the caller, which also used it to decide the
47
+ * file IS a module (the header it carried there). */
48
+ head: string;
49
+ }
50
+ /**
51
+ * The helpers this run changed in `modules`, each with the tests beside it that
52
+ * import it.
53
+ *
54
+ * PER DIRECTORY, not per spec. The importer walk reads the modules' directory and
55
+ * nothing else, so every view here is a fact about the directory: which delivered
56
+ * spec happens to sit beside a module does not change it, and two specs beside one
57
+ * module would compute the same views twice. A module no spec names — deleted by a
58
+ * run that also dropped its import — is still a changed module of its directory,
59
+ * which is what stops un-sharing the helpers from being the way past the gate.
60
+ *
61
+ * No git here. The caller established from git which files were modules at HEAD
62
+ * and what they held; this compares that against the working tree and walks the
63
+ * directory. A module whose working copy cannot be read contributes nothing.
64
+ */
65
+ export declare function changedHelperCallers(modules: ChangedModule[]): Promise<ChangedHelperView[]>;
@@ -0,0 +1,85 @@
1
+ import * as fs from "fs/promises";
2
+ import { logger } from "../../utils/logger.js";
3
+ import { changedHelpersBetween, helperImporters, parseUtilsFile, utilsSpecForFile, } from "../../utils/utils-verify/index.js";
4
+ /**
5
+ * The helpers this run changed in `modules`, each with the tests beside it that
6
+ * import it.
7
+ *
8
+ * PER DIRECTORY, not per spec. The importer walk reads the modules' directory and
9
+ * nothing else, so every view here is a fact about the directory: which delivered
10
+ * spec happens to sit beside a module does not change it, and two specs beside one
11
+ * module would compute the same views twice. A module no spec names — deleted by a
12
+ * run that also dropped its import — is still a changed module of its directory,
13
+ * which is what stops un-sharing the helpers from being the way past the gate.
14
+ *
15
+ * No git here. The caller established from git which files were modules at HEAD
16
+ * and what they held; this compares that against the working tree and walks the
17
+ * directory. A module whose working copy cannot be read contributes nothing.
18
+ */
19
+ export async function changedHelperCallers(modules) {
20
+ const anchor = modules[0]?.file;
21
+ if (!anchor)
22
+ return [];
23
+ const spec = utilsSpecForFile(anchor);
24
+ if (!spec)
25
+ return [];
26
+ const helpersByFile = new Map();
27
+ const changedByFile = new Map();
28
+ for (const { file, head } of modules) {
29
+ // A module that is GONE reads as empty, which makes every helper it had at HEAD a
30
+ // removal — the same finding as deleting them one at a time. Only absence means
31
+ // that. Any other read failure — a permission error, a transient fault, a
32
+ // directory in its place — is the module not being readable, which is not
33
+ // evidence of anything: treating it as empty would call every incumbent helper
34
+ // removed and demand a run of every caller, on a malfunction.
35
+ let current;
36
+ try {
37
+ current = await fs.readFile(file, "utf8");
38
+ }
39
+ catch (err) {
40
+ if (err?.code !== "ENOENT") {
41
+ logger.warning("Could not read a shared module — the helpers it changed will not be reported", { file, error: String(err) });
42
+ continue;
43
+ }
44
+ current = "";
45
+ }
46
+ const changes = changedHelpersBetween(head, current, spec);
47
+ if (changes.length === 0)
48
+ continue;
49
+ changedByFile.set(file, changes);
50
+ // A REMOVED helper is not in the current module, so a caller set read off that
51
+ // module alone would find none — and report "nothing calls it" about the one
52
+ // change that breaks every caller's import outright. The incumbent's definitions
53
+ // of the removed names go into the walk beside the current ones.
54
+ const helpers = parseUtilsFile(current, spec);
55
+ const gone = new Set(changes.filter((c) => c.change === "removed").map((c) => c.helper));
56
+ helpersByFile.set(file, gone.size === 0
57
+ ? helpers
58
+ : [
59
+ ...helpers,
60
+ ...parseUtilsFile(head, spec).filter((h) => gone.has(h.name)),
61
+ ]);
62
+ }
63
+ if (changedByFile.size === 0)
64
+ return [];
65
+ // Anchored on the module: the walk reads the anchor's directory, which is the
66
+ // module's own. A deleted anchor still has a directory.
67
+ const scan = await helperImporters(anchor, [...changedByFile.keys()], helpersByFile, spec);
68
+ const importersOf = new Map(scan.helpers.map((h) => [`${h.file}\0${h.helper}`, h.importers]));
69
+ const out = [];
70
+ for (const [utilsFile, changes] of changedByFile)
71
+ for (const c of changes)
72
+ out.push({
73
+ utilsFile,
74
+ helper: c.helper,
75
+ change: c.change,
76
+ ...(scan.read
77
+ ? {
78
+ callers: [
79
+ ...(importersOf.get(`${utilsFile}\0${c.helper}`) ?? []),
80
+ ].sort(),
81
+ }
82
+ : {}),
83
+ });
84
+ return out;
85
+ }
@@ -2,6 +2,7 @@ import { z } from "zod";
2
2
  import { fixErrorsInstruction } from "../../skills/fixTestImportErrorsSkill.js";
3
3
  import fs from "fs";
4
4
  import { logger } from "../../utils/logger.js";
5
+ import { toolError } from "../../utils/utils.js";
5
6
  import { ProgrammingLanguage, TestType } from "../../types/TestTypes.js";
6
7
  import { ModularizationService, } from "../../services/ModularizationService.js";
7
8
  import { AnalyticsService } from "../../services/AnalyticsService.js";
@@ -9,6 +10,7 @@ import { normalizeLanguageParams, resolveParamAliases, } from "../../utils/norma
9
10
  import { normalizeSkyrampImportsInFile } from "../../utils/normalizeSkyrampImports.js";
10
11
  import { stageGeneratedPaths } from "../../utils/gitStaging.js";
11
12
  import { recordModularized } from "./reuse-state.js";
13
+ import { pendingPomPass } from "./pom-pass-state.js";
12
14
  const modularizationSchema = {
13
15
  testFile: z
14
16
  .string()
@@ -82,6 +84,14 @@ After modularization, if errors remain, ${fixErrorsInstruction()}.
82
84
  if (!params.isTraceBased &&
83
85
  [TestType.UI, TestType.E2E, TestType.INTEGRATION].includes(params.testType))
84
86
  params.isTraceBased = true;
87
+ // Both reuse settings on: the page-object pass maps the raw steps first, and
88
+ // its gates judge the spec while what it did not substitute is still inline.
89
+ // Checked before anything is recorded, so a refused call leaves no mark.
90
+ const owedPomPass = await pendingPomPass(params.testFile);
91
+ if (owedPomPass) {
92
+ errorResult = toolError(owedPomPass);
93
+ return errorResult;
94
+ }
85
95
  normalizeSkyrampImportsInFile(params.testFile);
86
96
  // Stage so testbot includes the generated files in its output commit.
87
97
  await stageGeneratedPaths(params.testFile);
@@ -0,0 +1,51 @@
1
+ import type { ReuseHandOff } from "../../utils/AnalysisStateManager.js";
2
+ import type { ReuseRederivation } from "./reuse-state.js";
3
+ /**
4
+ * The page-object pass as run state, for a target both reuse settings serve
5
+ * (isPomFirstTarget): whether the pass has finished for a spec, the mark that
6
+ * finishes it, the checkpoint that holds modularization until it has, and the
7
+ * merge of the two halves one such spec's report row carries.
8
+ */
9
+ /** Whether a hand-off's chain starts with the page-object pass. Derived from what the
10
+ * hand-off already stores, never recorded beside it: generation writes the hand-off
11
+ * from exactly these three values, and maintenance arms none for such a target. */
12
+ export declare function pomFirstHandOff(handOff: ReuseHandOff): boolean;
13
+ /**
14
+ * With both reuse settings on (isPomFirstTarget), where this spec's page-object pass
15
+ * stands. `undefined` while it is open; `"finished"` once its verify pass PASSED;
16
+ * `"no-layer"` when discovery found no page-object layer to reuse. From then on the
17
+ * reuse tool serves the SkyrampUtils flow for the spec, and the page-object gates
18
+ * have already judged the spec while every step they did not substitute was still raw
19
+ * and inline — the state they were written for.
20
+ *
21
+ * A FAILED verdict keeps the pass open: the page-object report says to fix and
22
+ * re-verify, and `pendingReuseVerification` refuses the tools after it until that
23
+ * happens. A record owned by another spec, or no run state, reads as open; the caller
24
+ * then serves the page-object pass, which is what the spec gets without composition.
25
+ */
26
+ export declare function pomPassOutcome(testFile: string, explicitStateFile?: string): Promise<"finished" | "no-layer" | undefined>;
27
+ /** Mark the page-object pass finished for a spec (see `pomPassOutcome`). Written by
28
+ * the reuse tool at the two points that finish it: a PASSED page-object verify, and
29
+ * discovery that found no page-object layer. */
30
+ export declare function recordPomPassFinished(testFile: string, explicitStateFile?: string): Promise<void>;
31
+ /**
32
+ * Does this spec owe its page-object pass before `skyramp_modularization`? Only a spec
33
+ * generation handed off with the page-object pass first. Modularizing first would move
34
+ * the raw steps the page-object pass maps into helpers it cannot see, and its gates
35
+ * would then judge a spec with nothing left inline. Fails open without a hand-off, on
36
+ * an unreadable or unwritable state file.
37
+ */
38
+ export declare function pendingPomPass(testFile: string, explicitStateFile?: string): Promise<string | undefined>;
39
+ /**
40
+ * One row, both halves. The outcome fields are disjoint by construction (the
41
+ * page-object fields sit at the top level, the shared-helper ones under `helpers` and
42
+ * `inHouse`). The two blocking verdicts become ONE, because the report refuses per
43
+ * spec and one `verify: true` call re-checks both halves (the reuse tool's combined
44
+ * verify). Declines are retired only where the half that owns them was measured: the
45
+ * page-object half measures a `kept inline` decline only when its zero-reuse gate
46
+ * walked the candidates, which it does not at non-zero reuse.
47
+ */
48
+ export declare function combineRederivations(pom: ReuseRederivation, helper: ReuseRederivation, carriesKeptInline: boolean,
49
+ /** The call that re-checks both halves — the shared-helper verify call, which the
50
+ * reuse tool routes to its combined verify for such a spec. */
51
+ combinedVerifyCall: string | undefined): ReuseRederivation;