@skyramp/mcp 0.3.8 → 0.4.0-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 (229) hide show
  1. package/build/commands/commandLibrary.d.ts +1 -1
  2. package/build/commands/commandLibrary.js +3 -3
  3. package/build/commands/recommendTestsAndExecuteCommand.d.ts +1 -1
  4. package/build/commands/recommendTestsAndExecuteCommand.js +35 -20
  5. package/build/commands/testThisEndpointCommand.js +35 -19
  6. package/build/index.js +9 -3
  7. package/build/playwright/blueprintDigest.d.ts +15 -0
  8. package/build/playwright/blueprintDigest.js +152 -0
  9. package/build/playwright/blueprintDigestStore.d.ts +31 -0
  10. package/build/playwright/blueprintDigestStore.js +117 -0
  11. package/build/playwright/registerPlaywrightTools.js +60 -12
  12. package/build/playwright/traceRecordingPrompt.js +8 -7
  13. package/build/prompts/enhance-assertions/sharedAssertionRules.js +9 -8
  14. package/build/prompts/enhance-assertions/uiAssertionsPrompt.js +24 -2
  15. package/build/prompts/promptAssets.d.ts +20 -0
  16. package/build/prompts/promptAssets.js +55 -0
  17. package/build/prompts/sut-setup/modes/dockerComposePrompt.js +19 -5
  18. package/build/prompts/test-maintenance/actionsInstructions.d.ts +4 -0
  19. package/build/prompts/test-maintenance/actionsInstructions.js +14 -2
  20. package/build/prompts/test-maintenance/drift-analysis-prompt.d.ts +0 -10
  21. package/build/prompts/test-maintenance/drift-analysis-prompt.js +2 -11
  22. package/build/prompts/test-maintenance/uiDriftAnalysisSections.js +8 -4
  23. package/build/prompts/test-recommendation/diffExecutionPlan.d.ts +5 -22
  24. package/build/prompts/test-recommendation/diffExecutionPlan.js +37 -465
  25. package/build/prompts/test-recommendation/recommendationSections.d.ts +7 -17
  26. package/build/prompts/test-recommendation/recommendationSections.js +67 -309
  27. package/build/prompts/test-recommendation/recommendationShared.d.ts +19 -47
  28. package/build/prompts/test-recommendation/recommendationShared.js +49 -155
  29. package/build/prompts/test-recommendation/registerRecommendTestsPrompt.d.ts +0 -5
  30. package/build/prompts/test-recommendation/registerRecommendTestsPrompt.js +10 -153
  31. package/build/prompts/test-recommendation/test-recommendation-prompt.d.ts +2 -29
  32. package/build/prompts/test-recommendation/test-recommendation-prompt.js +32 -457
  33. package/build/prompts/testbot/planDeclarations.d.ts +6 -0
  34. package/build/prompts/testbot/planDeclarations.js +9 -0
  35. package/build/prompts/testbot/testbot-prompts.d.ts +8 -0
  36. package/build/prompts/testbot/testbot-prompts.js +256 -381
  37. package/build/recommendation/answers.d.ts +35 -0
  38. package/build/recommendation/answers.js +96 -0
  39. package/build/recommendation/registerPlan.d.ts +49 -0
  40. package/build/recommendation/registerPlan.js +117 -0
  41. package/build/recommendation/runVerifiers.d.ts +10 -0
  42. package/build/recommendation/runVerifiers.js +49 -0
  43. package/build/recommendation/subjectStep.d.ts +42 -0
  44. package/build/recommendation/subjectStep.js +86 -0
  45. package/build/recommendation/types.d.ts +163 -0
  46. package/build/recommendation/types.js +20 -0
  47. package/build/recommendation/verifierContracts.d.ts +382 -0
  48. package/build/recommendation/verifierContracts.js +263 -0
  49. package/build/recommendation/verifiers/changedFile.d.ts +2 -0
  50. package/build/recommendation/verifiers/changedFile.js +82 -0
  51. package/build/recommendation/verifiers/citedPath.d.ts +12 -0
  52. package/build/recommendation/verifiers/citedPath.js +35 -0
  53. package/build/recommendation/verifiers/coverage.d.ts +7 -0
  54. package/build/recommendation/verifiers/coverage.js +617 -0
  55. package/build/recommendation/verifiers/deliveredMatchesPlan.d.ts +11 -0
  56. package/build/recommendation/verifiers/deliveredMatchesPlan.js +33 -0
  57. package/build/recommendation/verifiers/endpointGrounded.d.ts +17 -0
  58. package/build/recommendation/verifiers/endpointGrounded.js +128 -0
  59. package/build/recommendation/verifiers/existingCoverage.d.ts +6 -0
  60. package/build/recommendation/verifiers/existingCoverage.js +51 -0
  61. package/build/recommendation/verifiers/expectedOutcome.d.ts +31 -0
  62. package/build/recommendation/verifiers/expectedOutcome.js +105 -0
  63. package/build/recommendation/verifiers/removedElementGuarded.d.ts +2 -0
  64. package/build/recommendation/verifiers/removedElementGuarded.js +57 -0
  65. package/build/recommendation/verifiers/reportedCategory.d.ts +26 -0
  66. package/build/recommendation/verifiers/reportedCategory.js +84 -0
  67. package/build/recommendation/verifiers/screenRoute.d.ts +10 -0
  68. package/build/recommendation/verifiers/screenRoute.js +118 -0
  69. package/build/recommendation/verifiers/statedDifference.d.ts +6 -0
  70. package/build/recommendation/verifiers/statedDifference.js +140 -0
  71. package/build/recommendation/verifiers/uiElementGrounded.d.ts +7 -0
  72. package/build/recommendation/verifiers/uiElementGrounded.js +318 -0
  73. package/build/resources/analysisResources.js +1 -114
  74. package/build/resources/testbotResource.js +23 -13
  75. package/build/services/ModularizationService.js +2 -1
  76. package/build/services/TestDiscoveryService.d.ts +3 -72
  77. package/build/services/TestDiscoveryService.js +10 -303
  78. package/build/services/containerEnv.d.ts +1 -1
  79. package/build/services/containerEnv.js +12 -0
  80. package/build/skills/fixTestImportErrorsSkill.d.ts +13 -0
  81. package/build/skills/fixTestImportErrorsSkill.js +20 -0
  82. package/build/toolNames.d.ts +1 -0
  83. package/build/toolNames.js +1 -0
  84. package/build/tools/code-refactor/enhanceAssertionsTool.js +3 -3
  85. package/build/tools/code-refactor/modularizationTool.js +2 -1
  86. package/build/tools/executeSkyrampTestTool.d.ts +80 -0
  87. package/build/tools/executeSkyrampTestTool.js +246 -19
  88. package/build/tools/generate-tests/generateBatchScenarioRestTool.js +6 -0
  89. package/build/tools/generate-tests/generateContractRestTool.js +3 -3
  90. package/build/tools/generate-tests/planGuard.d.ts +2 -2
  91. package/build/tools/generate-tests/planGuard.js +78 -18
  92. package/build/tools/one-click/oneClickTool.d.ts +0 -1
  93. package/build/tools/one-click/oneClickTool.js +0 -5
  94. package/build/tools/submitReportTool.d.ts +48 -42
  95. package/build/tools/submitReportTool.js +576 -193
  96. package/build/tools/test-management/actionsTool.js +72 -4
  97. package/build/tools/test-management/analyzeChangesTool.d.ts +144 -48
  98. package/build/tools/test-management/analyzeChangesTool.js +212 -1219
  99. package/build/tools/test-management/analyzeTestHealthTool.js +13 -24
  100. package/build/tools/test-management/index.d.ts +1 -0
  101. package/build/tools/test-management/index.js +1 -0
  102. package/build/tools/test-management/registerTestPlanTool.d.ts +795 -172
  103. package/build/tools/test-management/registerTestPlanTool.js +609 -542
  104. package/build/tools/test-management/resolveScreenTool.d.ts +75 -0
  105. package/build/tools/test-management/resolveScreenTool.js +289 -0
  106. package/build/types/BlueprintDigest.d.ts +34 -0
  107. package/build/types/BlueprintDigest.js +1 -0
  108. package/build/types/RepositoryAnalysis.d.ts +20 -1559
  109. package/build/types/RepositoryAnalysis.js +2 -58
  110. package/build/types/StepMethod.d.ts +40 -0
  111. package/build/types/StepMethod.js +77 -0
  112. package/build/types/TestAnalysis.d.ts +12 -0
  113. package/build/types/TestExecution.d.ts +4 -0
  114. package/build/types/TestRecommendation.d.ts +24 -24
  115. package/build/types/TestRecommendation.js +91 -89
  116. package/build/types/TestbotPromptOptions.d.ts +0 -4
  117. package/build/types/TestbotReport.d.ts +64 -2
  118. package/build/utils/AnalysisStateManager.d.ts +79 -113
  119. package/build/utils/AnalysisStateManager.js +147 -57
  120. package/build/utils/assertion-verify/api-shared-lints.js +1 -1
  121. package/build/utils/assertion-verify/metrics.js +85 -36
  122. package/build/utils/assertion-verify/ui-lints.d.ts +0 -5
  123. package/build/utils/assertion-verify/ui-lints.js +32 -0
  124. package/build/utils/branchDiff.d.ts +63 -31
  125. package/build/utils/branchDiff.js +242 -94
  126. package/build/utils/containedPath.d.ts +18 -0
  127. package/build/utils/containedPath.js +73 -0
  128. package/build/utils/dartRouteExtractor.d.ts +18 -34
  129. package/build/utils/dartRouteExtractor.js +101 -173
  130. package/build/utils/featureFlags.d.ts +12 -0
  131. package/build/utils/featureFlags.js +14 -0
  132. package/build/utils/frontendSelectors.d.ts +48 -27
  133. package/build/utils/frontendSelectors.js +241 -80
  134. package/build/utils/pathMatching.d.ts +2 -4
  135. package/build/utils/pathMatching.js +2 -4
  136. package/build/utils/planMatchKeys.d.ts +38 -47
  137. package/build/utils/planMatchKeys.js +143 -81
  138. package/build/utils/rebaselineSnapshots.d.ts +24 -0
  139. package/build/utils/rebaselineSnapshots.js +65 -0
  140. package/build/utils/removedUiElements.d.ts +22 -0
  141. package/build/utils/removedUiElements.js +106 -0
  142. package/build/utils/reportVerification.d.ts +2 -6
  143. package/build/utils/reportVerification.js +61 -2
  144. package/build/utils/screenRoutes.d.ts +66 -0
  145. package/build/utils/screenRoutes.js +727 -0
  146. package/build/utils/sourceRouteExtractor.js +320 -112
  147. package/build/utils/testFileClassification.d.ts +11 -2
  148. package/build/utils/testFileClassification.js +44 -2
  149. package/build/utils/testFixtures.d.ts +5 -0
  150. package/build/utils/testFixtures.js +13 -0
  151. package/build/utils/utils.d.ts +0 -1
  152. package/build/utils/utils.js +0 -11
  153. package/build/utils/versions.d.ts +3 -3
  154. package/build/utils/versions.js +1 -1
  155. package/build/workspace/workspace.d.ts +12 -12
  156. package/node_modules/playwright/lib/mcp/skyramp/assertHiddenTool.js +56 -0
  157. package/node_modules/playwright/lib/mcp/skyramp/assertTool.js +2 -1
  158. package/node_modules/playwright/lib/mcp/skyramp/loadTraceTool.js +10 -0
  159. package/node_modules/playwright/lib/mcp/skyramp/skyRampImport.js +4 -1
  160. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +160 -1
  161. package/node_modules/playwright/lib/mcp/test/skyRampExport.js +4 -2
  162. package/node_modules/playwright/node_modules/playwright-core/lib/server/codegen/skyramp/jsonlReader.js +1 -0
  163. package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/recorderSignalProcessor.js +2 -0
  164. package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder.js +5 -1
  165. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.-Id052Lr.js → index.B7KbSQcC.js} +1 -1
  166. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +1 -1
  167. package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
  168. package/node_modules/playwright/node_modules/playwright-core/src/server/codegen/skyramp/jsonlReader.ts +1 -1
  169. package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/recorderSignalProcessor.ts +7 -0
  170. package/node_modules/playwright/node_modules/playwright-core/src/server/recorder.ts +6 -1
  171. package/node_modules/playwright/package.json +1 -1
  172. package/package.json +4 -3
  173. package/plugin/.claude-plugin/plugin.json +8 -0
  174. package/plugin/plugin.json +6 -0
  175. package/plugin/prompts/declaring-a-plan.md +20 -0
  176. package/plugin/prompts/generate-tests/context-fetching.md +4 -0
  177. package/plugin/prompts/generate-tests/execution-plan.md +63 -0
  178. package/plugin/prompts/generate-tests/generation.md +108 -0
  179. package/plugin/prompts/generate-tests/path-parameters.md +1 -0
  180. package/plugin/prompts/generate-tests/reasoning-protocol.md +17 -0
  181. package/plugin/prompts/generate-tests/tool-workflow-variants.md +61 -0
  182. package/plugin/prompts/generate-tests/tool-workflows.md +65 -0
  183. package/plugin/prompts/plan-tests.md +42 -0
  184. package/plugin/prompts/testbot-task1.md +82 -0
  185. package/plugin/skills/fix-test-import-errors/SKILL.md +98 -0
  186. package/build/prompts/test-recommendation/analysisOutputPrompt.d.ts +0 -84
  187. package/build/prompts/test-recommendation/analysisOutputPrompt.js +0 -369
  188. package/build/prompts/test-recommendation/fullRepoCatalog.d.ts +0 -7
  189. package/build/prompts/test-recommendation/fullRepoCatalog.js +0 -283
  190. package/build/prompts/test-recommendation/scopeAssessment.d.ts +0 -81
  191. package/build/prompts/test-recommendation/scopeAssessment.js +0 -359
  192. package/build/recommendation/budgeters/diversityBalancedBudgeter.d.ts +0 -7
  193. package/build/recommendation/budgeters/diversityBalancedBudgeter.js +0 -105
  194. package/build/recommendation/budgeters/fixedNBudgeter.d.ts +0 -7
  195. package/build/recommendation/budgeters/fixedNBudgeter.js +0 -11
  196. package/build/recommendation/budgeters/shared.d.ts +0 -32
  197. package/build/recommendation/budgeters/shared.js +0 -246
  198. package/build/recommendation/discriminators.d.ts +0 -37
  199. package/build/recommendation/discriminators.js +0 -379
  200. package/build/recommendation/diversity.d.ts +0 -47
  201. package/build/recommendation/diversity.js +0 -101
  202. package/build/recommendation/planRanker.d.ts +0 -65
  203. package/build/recommendation/planRanker.js +0 -83
  204. package/build/recommendation/testFixtures.d.ts +0 -25
  205. package/build/recommendation/testFixtures.js +0 -45
  206. package/build/types/FrontendIntegration.d.ts +0 -28
  207. package/build/types/FrontendIntegration.js +0 -22
  208. package/build/types/Recommendation.d.ts +0 -146
  209. package/build/types/Recommendation.js +0 -74
  210. package/build/utils/changedRoutes.d.ts +0 -29
  211. package/build/utils/changedRoutes.js +0 -87
  212. package/build/utils/frontendIntegration.d.ts +0 -9
  213. package/build/utils/frontendIntegration.js +0 -243
  214. package/build/utils/importerHop.d.ts +0 -135
  215. package/build/utils/importerHop.js +0 -489
  216. package/build/utils/pathAffinityClassification.d.ts +0 -49
  217. package/build/utils/pathAffinityClassification.js +0 -180
  218. package/build/utils/pythonMountPrefixes.d.ts +0 -25
  219. package/build/utils/pythonMountPrefixes.js +0 -347
  220. package/build/utils/repoScanner.d.ts +0 -34
  221. package/build/utils/repoScanner.js +0 -300
  222. package/build/utils/routeParsers.d.ts +0 -95
  223. package/build/utils/routeParsers.js +0 -951
  224. package/build/utils/scenarioDrafting.d.ts +0 -92
  225. package/build/utils/scenarioDrafting.js +0 -951
  226. package/build/utils/subjectEndpoints.d.ts +0 -19
  227. package/build/utils/subjectEndpoints.js +0 -98
  228. package/build/utils/uiPageEnumerator.d.ts +0 -172
  229. package/build/utils/uiPageEnumerator.js +0 -474
@@ -1,28 +1,14 @@
1
1
  /**
2
- * Frontend selector extraction for UI-test-maintenance discovery (Pass 3, "selector-edge").
2
+ * The `data-*` attributes and selector literals a frontend diff touched.
3
3
  *
4
- * Playwright/Cypress page objects couple to a changed component by SELECTOR — a
5
- * `data-testid` value or a CSS class token — not by filename or by `import`. So the
6
- * filename-token (pass 1) and import-edge (pass 2) promotion signals in
7
- * TestDiscoveryService Step 2 both miss them: a page object `table.component.ts` shares
8
- * no name token with the changed `CascadeFields.tsx` and does not import it — it selects
9
- * `.t--table-filter-columns-dropdown` at runtime.
4
+ * One reader now: the removal guard (SKYR-4305), which reports an element the
5
+ * diff deleted from a page that survives so the agent can assert it is gone.
10
6
  *
11
- * This module extracts the selector literals that a frontend diff added OR removed, so
12
- * discovery can promote any existing test/page-object whose body still references one of
13
- * them. Both sides of a rename are captured deliberately: a page object that has NOT been
14
- * updated still contains the OLD value, so matching the removed literal is what surfaces
15
- * the test that needs maintenance.
16
- *
17
- * This is the frontend analog of importerHop's `extractChangedSymbols` (backend type
18
- * names) — kept in its own frontend-scoped module rather than in the backend-resource
19
- * importerHop, per the codebase convention of keeping domain vocabularies local.
7
+ * This module also used to lift every selector literal a diff added or removed,
8
+ * so discovery could promote a page object whose body referenced one. That was
9
+ * the server deciding which existing tests a change touches — the external
10
+ * tests are loaded uncut up to the discovery cap now, and the agent reads them.
20
11
  */
21
- // Bound the extracted set so a large diff can't produce an unbounded match list. The
22
- // downstream promotion is separately capped by MAX_UI_PROMOTED, so this is a belt on the
23
- // literal set itself. Exported so the caller can detect + log when a single file's
24
- // extraction is truncated by this cap.
25
- export const MAX_CHANGED_SELECTORS = 80;
26
12
  // Utility / design-token CSS classes (Tailwind/Bootstrap-style) are ALSO multi-hyphen kebab
27
13
  // tokens, but they are shared *styling* vocabulary, not component identity — matching them
28
14
  // floods Pass 3, because snapshot files (`__snapshots__/*.snap`) serialize `className`
@@ -53,71 +39,246 @@ function isDistinctiveSelector(token, trusted) {
53
39
  return hasBEM || (hyphens >= 2 && !COMMON_CSS_SELECTOR_TOKENS.test(token));
54
40
  }
55
41
  /**
56
- * Extract selector literals added or removed in a single file's diff slice.
57
- *
58
- * Two complementary passes over the changed (`+`/`-`) lines, excluding the `+++`/`---`
59
- * file headers:
60
- * 1. Explicit test-facing attribute/API values: `data-testid` / `data-test` /
61
- * `data-test-id` / `data-cy` / `data-qa` attribute values and `getByTestId(...)`
62
- * arguments (trusted — accepts camelCase or single-delimiter ids).
63
- * 2. Distinctive kebab / BEM class tokens anywhere on the line (>= 2 hyphens or `--`),
64
- * which catches CSS-class-based coupling (`t--table-filter-columns-dropdown`,
65
- * `ads-button--primary`).
42
+ * The test-facing selector grammar, shared by every extractor in this module so the
43
+ * two cannot drift: `data-testid` / `data-test` / `data-test-id` / `data-cy` / `data-qa`
44
+ * attribute values (string literals only) and `getByTestId(...)` arguments.
45
+ * Case-insensitive on purpose: JSX authors write `data-testId` (demoshop) and the
46
+ * browser lowercases attribute names, so `getByTestId` matches either spelling.
47
+ */
48
+ const TESTID_ATTR_RE = /\b(data-(?:test-?id|test|cy|qa))\b\s*=\s*\{?\s*[`'"]([A-Za-z][\w:-]{2,})[`'"]/gi;
49
+ const GET_BY_TEST_ID_RE = /getByTestId\(\s*[`'"]([A-Za-z][\w:-]{2,})[`'"]/gi;
50
+ /**
51
+ * Any other `data-*` attribute with a static string value (`data-section="session-id-debug"`).
52
+ * Untrusted: the value must pass the stricter distinctiveness rule, and attributes that carry
53
+ * component STATE rather than identity are excluded — they flip at runtime, so "absent" is
54
+ * meaningless for them.
55
+ */
56
+ const GENERIC_DATA_ATTR_RE = /\b(data-[a-z][a-z0-9]*(?:-[a-z0-9]+)*)\s*=\s*\{?\s*[`'"]([A-Za-z][\w:-]{2,})[`'"]/gi;
57
+ const STATE_DATA_ATTRS = new Set([
58
+ "data-state", "data-theme", "data-orientation", "data-side", "data-align", "data-value",
59
+ "data-index", "data-slot", "data-disabled", "data-checked", "data-active", "data-open",
60
+ "data-loading", "data-size", "data-variant", "data-selected", "data-highlighted",
61
+ "data-focus", "data-hover", "data-pressed", "data-expanded", "data-placeholder", "data-status",
62
+ ]);
63
+ /**
64
+ * Identifying `data-*` attributes on one source line: the trusted test-facing set (any
65
+ * casing), `getByTestId(...)` arguments (attributed to `data-testid`), and any other
66
+ * static `data-*` whose value is distinctive and whose attribute is not a state carrier.
67
+ */
68
+ function attributesOnLine(line) {
69
+ const out = [];
70
+ const seen = new Set();
71
+ const push = (attribute, value) => {
72
+ const key = `${attribute}=${value}`;
73
+ if (!seen.has(key)) {
74
+ seen.add(key);
75
+ out.push({ attribute, value });
76
+ }
77
+ };
78
+ let m;
79
+ TESTID_ATTR_RE.lastIndex = 0;
80
+ while ((m = TESTID_ATTR_RE.exec(line)) !== null) {
81
+ if (isDistinctiveSelector(m[2], true))
82
+ push(m[1].toLowerCase(), m[2]);
83
+ }
84
+ GET_BY_TEST_ID_RE.lastIndex = 0;
85
+ while ((m = GET_BY_TEST_ID_RE.exec(line)) !== null) {
86
+ if (isDistinctiveSelector(m[1], true))
87
+ push("data-testid", m[1]);
88
+ }
89
+ GENERIC_DATA_ATTR_RE.lastIndex = 0;
90
+ while ((m = GENERIC_DATA_ATTR_RE.exec(line)) !== null) {
91
+ const attribute = m[1].toLowerCase();
92
+ if (STATE_DATA_ATTRS.has(attribute))
93
+ continue;
94
+ if (seen.has(`${attribute}=${m[2]}`))
95
+ continue; // already captured by the trusted pass
96
+ if (isDistinctiveSelector(m[2], false))
97
+ push(attribute, m[2]);
98
+ }
99
+ return out;
100
+ }
101
+ /**
102
+ * Tag name of the JSX/HTML element a diff line opens, or "" — also "" for a comment
103
+ * line (`//`, `/*`, `*`, `{/*`, `<!--`), whose `<div …>` is text, not an element.
66
104
  */
67
- export function extractChangedSelectors(fileDiff) {
105
+ function openingTagOf(line) {
106
+ const t = line.trim();
107
+ if (/^(?:\/\/|\/\*|\*|\{\s*\/\*|<!--)/.test(t))
108
+ return "";
109
+ return /<\s*([A-Za-z][\w.-]*)/.exec(t)?.[1]?.toLowerCase() ?? "";
110
+ }
111
+ /** Identifying attribute VALUES that appear on `+` lines of a diff (any number of files). */
112
+ export function extractAddedAttributeValues(diff) {
68
113
  const out = new Set();
69
- // Require a quote immediately before the value so only string LITERALS are captured —
70
- // `data-testid="x"`, `data-testid={'x'}`, `data-testid={`x`}`. This deliberately skips
71
- // non-literal expressions like `data-testid={someVar}`, which would otherwise inject a
72
- // variable name into the selector set and cause noisy promotion matches.
73
- // Require a quote/backtick on BOTH sides so only FULLY-literal values are captured —
74
- // `data-testid="x"`, `data-testid={'x'}`, `data-testid={`x`}`. The closing delimiter
75
- // deliberately rejects interpolated template literals (`data-testid={`members-${id}`}`),
76
- // which would otherwise leak a partial prefix (`members-`) into the selector set.
77
- const ATTR = /\bdata-(?:test-?id|test|cy|qa)\b\s*=\s*\{?\s*[`'"]([A-Za-z][\w:-]{2,})[`'"]/g;
78
- const GET_BY_TEST_ID = /getByTestId\(\s*[`'"]([A-Za-z][\w:-]{2,})[`'"]/g;
79
- const KEBAB = /[a-z][a-z0-9]*(?:-{1,2}[a-z0-9]+)+/g;
80
- // BEM base whose modifier is composed DYNAMICALLY, so no concrete class literal exists
81
- // in the diff to grep: `ads-button--${kind}`, "ads-button--" + kind, clsx(base && `x--${y}`).
82
- // Match a kebab base + `--` followed by a NON-alphanumeric (interpolation `$`, quote,
83
- // paren, whitespace) and capture the `<base>--` prefix. The page objects' concrete
84
- // modifier classes (ads-button--primary, ads-button--secondary) all start with that
85
- // prefix, so a substring match on it promotes them — the shared/design-system component
86
- // case a static-literal scan (KEBAB) misses. (A STATIC modifier like `foo--bar` is left
87
- // to KEBAB, which captures the whole class.)
88
- const BEM_DYNAMIC = /([a-z][a-z0-9]*(?:-[a-z0-9]+)+--)(?![a-z0-9])/g;
89
- for (const raw of fileDiff.split("\n")) {
90
- if (!(raw.startsWith("+") || raw.startsWith("-")))
114
+ for (const raw of diff.split("\n")) {
115
+ if (!raw.startsWith("+") || raw.startsWith("+++"))
91
116
  continue;
92
- if (raw.startsWith("+++") || raw.startsWith("---"))
117
+ for (const a of attributesOnLine(raw.slice(1)))
118
+ out.add(a.value);
119
+ }
120
+ return [...out];
121
+ }
122
+ /**
123
+ * Identifying `data-*` attributes one file's diff REMOVED: literals on `-` lines, minus the
124
+ * cases where the element still renders somewhere:
125
+ *
126
+ * - **moved / extracted** — the same value appears on a `+` line of this file or, via
127
+ * `addedValuesElsewhere`, anywhere else in the diff (a component extracted into a new
128
+ * file is the common shape);
129
+ * - **not an element** — a `-` line counts only when it opens a JSX/HTML tag or is a
130
+ * wrapped attribute line of a tag opened above it, on a changed or an unchanged
131
+ * line (the tag is carried until the `>` that closes it; a `>` inside a quoted
132
+ * value, a `{…}` expression or an `=>` arrow is content and does not close it). A deleted comment, docs snippet, story arg or page-object
133
+ * `getByTestId(...)` call that merely mentions the attribute is not an element
134
+ * removal — the element itself may well survive on an unchanged context line;
135
+ * - **renamed** — a rename cannot be told apart by literal identity (old and new
136
+ * value differ by definition), so it is paired structurally: within the same hunk,
137
+ * each removed ELEMENT is paired with AT MOST ONE added element that opens the same
138
+ * tag, carries a NEW identifying attribute, and keeps every other attribute NAME
139
+ * the removed element set on its changed lines (a rename may add attributes or
140
+ * edit their values, never drop one). With the tag kept on a context line, the
141
+ * changed lines are exactly the attributes that differ, so an in-place rename
142
+ * compares only what it touched while an in-place replacement that drops the old
143
+ * element's handlers does not pair. An element whose changed lines carry nothing
144
+ * but the identifier pairs by tag alone: a relabelled rename (`clear-state` →
145
+ * `reset-session`, "Clear State" → "Reset Session") and a same-shape replacement
146
+ * are the same diff, and the tie goes to rename — a guard on a retired identifier
147
+ * passes vacuously, and the agent still reads the diff for what the list lacks. A paired added element is consumed, so one addition
148
+ * suppresses one removal, not every removal in the hunk. SKYR-4339: pairing by tag
149
+ * alone consumed a genuinely removed `<a href="#">` with a genuinely added
150
+ * `<a href="https://…">` in the same hunk (judgement eval RG03), so the guard was
151
+ * never listed.
152
+ *
153
+ * Feeds `uiContext.removedElements` (SKYR-4305) — the grounding for a removal guard
154
+ * (`toBeHidden()` on `getByTestId(value)` for `data-testid`, on
155
+ * `locator('[<attribute>="<value>"]')` for any other `data-*`).
156
+ */
157
+ export function extractRemovedAttributes(fileDiff, addedValuesElsewhere = []) {
158
+ const addedAnywhere = new Set(addedValuesElsewhere);
159
+ for (const v of extractAddedAttributeValues(fileDiff))
160
+ addedAnywhere.add(v);
161
+ // Split into hunks so rename pairing stays local.
162
+ const hunks = [];
163
+ for (const raw of fileDiff.split("\n")) {
164
+ if (raw.startsWith("@@")) {
165
+ hunks.push([]);
93
166
  continue;
94
- const line = raw.slice(1);
95
- for (const re of [ATTR, GET_BY_TEST_ID]) {
96
- re.lastIndex = 0;
97
- let m;
98
- while ((m = re.exec(line)) !== null) {
99
- if (isDistinctiveSelector(m[1], true))
100
- out.add(m[1]);
101
- if (out.size >= MAX_CHANGED_SELECTORS)
102
- return [...out];
103
- }
104
167
  }
105
- KEBAB.lastIndex = 0;
106
- let k;
107
- while ((k = KEBAB.exec(line)) !== null) {
108
- if (isDistinctiveSelector(k[0], false))
109
- out.add(k[0]);
110
- if (out.size >= MAX_CHANGED_SELECTORS)
111
- return [...out];
168
+ if (hunks.length === 0)
169
+ hunks.push([]);
170
+ hunks[hunks.length - 1].push(raw);
171
+ }
172
+ // Names of the attributes an element's changed lines set (`name=` at attribute
173
+ // position: line start or after whitespace, so `=>`, `a=b` inside a handler body and
174
+ // `id=` inside a regex do not count), minus the identifying data-* attributes. Names,
175
+ // not values: a rename may edit or reformat a neighbouring attribute, never drop one.
176
+ const otherAttrNames = (text, ids) => {
177
+ const out = new Set();
178
+ const re = /(?:^|\s)([A-Za-z][\w:.-]*)=/g;
179
+ let m;
180
+ while ((m = re.exec(text)) !== null) {
181
+ const name = m[1].toLowerCase();
182
+ if (ids.some((a) => name === a.attribute))
183
+ continue;
184
+ out.add(name);
112
185
  }
113
- BEM_DYNAMIC.lastIndex = 0;
114
- let b;
115
- while ((b = BEM_DYNAMIC.exec(line)) !== null) {
116
- if (isDistinctiveSelector(b[1], false))
117
- out.add(b[1]);
118
- if (out.size >= MAX_CHANGED_SELECTORS)
119
- return [...out];
186
+ return out;
187
+ };
188
+ // Does this line close the opening tag? A `>` inside a quoted value, inside a `{…}`
189
+ // expression (`onClick={() => x}`) or as part of `=>` is content, not the tag end.
190
+ const closesOpeningTag = (line) => {
191
+ let t = line.replace(/"[^"]*"|'[^']*'|`[^`]*`/g, '""').replace(/=>/g, "");
192
+ for (let prev = ""; prev !== t;) {
193
+ prev = t;
194
+ t = t.replace(/\{[^{}]*\}/g, "");
195
+ }
196
+ return t.includes(">");
197
+ };
198
+ const out = [];
199
+ const seen = new Set();
200
+ for (const hunk of hunks) {
201
+ // Group changed lines into elements per side: an element starts on the line that
202
+ // opens its tag and carries on until the `>` that closes the opening tag, so a
203
+ // wrapped attribute line (`- data-testid="x"`) belongs to the tag opened above it,
204
+ // and a later comment or unrelated line in the hunk does not inherit it.
205
+ // The tag may also be opened on an unchanged context line (`<a` kept, attributes
206
+ // edited underneath): then each side's changed lines form an element of that tag,
207
+ // until the context `>` that closes it.
208
+ const removed = [];
209
+ const added = [];
210
+ let curMinus = null;
211
+ let curPlus = null;
212
+ let contextTag = "";
213
+ for (const raw of hunk) {
214
+ if (raw.startsWith("+++") || raw.startsWith("---"))
215
+ continue;
216
+ if (!(raw.startsWith("+") || raw.startsWith("-"))) {
217
+ const line = raw.startsWith(" ") ? raw.slice(1) : raw;
218
+ const tag = openingTagOf(line);
219
+ if (tag) {
220
+ contextTag = tag;
221
+ curMinus = null;
222
+ curPlus = null;
223
+ }
224
+ if (closesOpeningTag(line)) {
225
+ contextTag = "";
226
+ curMinus = null;
227
+ curPlus = null;
228
+ }
229
+ continue;
230
+ }
231
+ const line = raw.slice(1);
232
+ const minus = raw.startsWith("-");
233
+ const ownTag = openingTagOf(line);
234
+ let cur = minus ? curMinus : curPlus;
235
+ if (ownTag) {
236
+ cur = { tag: ownTag, text: "", ids: [], consumed: false };
237
+ (minus ? removed : added).push(cur);
238
+ }
239
+ else if (!cur && contextTag) {
240
+ cur = { tag: contextTag, text: "", ids: [], consumed: false };
241
+ (minus ? removed : added).push(cur);
242
+ }
243
+ if (cur) {
244
+ cur.text += `${line}\n`;
245
+ cur.ids.push(...attributesOnLine(line));
246
+ }
247
+ // Only an element (a tag opened on this line or carried from above) can be a
248
+ // removed element; a bare mention in a comment / docs / page object is not.
249
+ if (closesOpeningTag(line))
250
+ cur = null;
251
+ if (minus)
252
+ curMinus = cur;
253
+ else
254
+ curPlus = cur;
255
+ }
256
+ const candidates = added.filter((a) => a.ids.length > 0);
257
+ for (const r of removed) {
258
+ if (r.ids.length === 0)
259
+ continue;
260
+ const rNames = otherAttrNames(r.text, r.ids);
261
+ const partner = candidates.find((a) => {
262
+ if (a.consumed || a.tag !== r.tag)
263
+ return false;
264
+ const aNames = otherAttrNames(a.text, a.ids);
265
+ for (const name of rNames)
266
+ if (!aNames.has(name))
267
+ return false;
268
+ return true;
269
+ });
270
+ if (partner) {
271
+ partner.consumed = true;
272
+ continue;
273
+ }
274
+ for (const a of r.ids) {
275
+ const key = `${a.attribute}=${a.value}`;
276
+ if (!addedAnywhere.has(a.value) && !seen.has(key)) {
277
+ seen.add(key);
278
+ out.push(a);
279
+ }
280
+ }
120
281
  }
121
282
  }
122
- return [...out];
283
+ return out;
123
284
  }
@@ -5,10 +5,8 @@
5
5
  * are both true even though the paths name different resources. These
6
6
  * helpers require the match to land on a "/" (or path-end) boundary.
7
7
  *
8
- * Lives in its own module, independent of `routeParsers.ts` and
9
- * `importerHop.ts`, so callers in either direction (`planRanker.ts` imports
10
- * `routeParsers.ts`; `scenarioDrafting.ts` imports `importerHop.ts`, which
11
- * imports `routeParsers.ts`) can both use it without risking an import cycle.
8
+ * Lives in its own module so any caller can use it without risking an import
9
+ * cycle through whichever module owns the route predicates.
12
10
  */
13
11
  /**
14
12
  * Returns true when `a` starts with `b` at a path-segment boundary.
@@ -5,10 +5,8 @@
5
5
  * are both true even though the paths name different resources. These
6
6
  * helpers require the match to land on a "/" (or path-end) boundary.
7
7
  *
8
- * Lives in its own module, independent of `routeParsers.ts` and
9
- * `importerHop.ts`, so callers in either direction (`planRanker.ts` imports
10
- * `routeParsers.ts`; `scenarioDrafting.ts` imports `importerHop.ts`, which
11
- * imports `routeParsers.ts`) can both use it without risking an import cycle.
8
+ * Lives in its own module so any caller can use it without risking an import
9
+ * cycle through whichever module owns the route predicates.
12
10
  */
13
11
  /**
14
12
  * Returns true when `a` starts with `b` at a path-segment boundary.
@@ -1,22 +1,19 @@
1
1
  /**
2
- * SKYR-3879 Path B — shared match-key computation for the register-plan
3
- * checkpoint. `computeMatchKeys` runs ONCE, at approval time (inside
4
- * registerTestPlanTool / the analyze_changes pre-seed), so the two
5
- * consumers — planGuard (generation-tool hard gate) and submitReportTool
6
- * (report cross-check) — only ever do a Set/array membership check, never
7
- * re-derive normalization logic against a live candidate.
2
+ * Shared match-key computation for the generation-tool plan gate. The generation
3
+ * tools are called with a scenario name and an endpoint, never a `plannedTestId`,
4
+ * so this joins a call back to a plan planned test; the report compares ids.
8
5
  *
9
6
  * Two kinds of key:
10
7
  * - `name:<slug>` — normalized scenarioName, for multi-step
11
- * candidates (batch-scenario / integration) where the agent's own name is
8
+ * planned tests (batch-scenario / integration) where the agent's own name is
12
9
  * the natural join key.
13
10
  * - `ep:<testType>:<METHOD>:<path>` — one per step, for single-endpoint
14
11
  * matching (contract tests, or a fallback when names drift between
15
12
  * register and generate).
16
13
  */
17
- import { DraftedScenario } from "../types/RepositoryAnalysis.js";
18
- import { ApprovedPlanItem, Candidate } from "../types/Recommendation.js";
19
- /** Query shape built from a generation-tool call or a submitted report entry. */
14
+ import { PlanScenario } from "../types/RepositoryAnalysis.js";
15
+ import type { PlannedTest } from "../recommendation/types.js";
16
+ /** Query shape built from a generation-tool call or a reported test entry. */
20
17
  export interface PlanMatchQuery {
21
18
  scenarioName?: string;
22
19
  testType?: string;
@@ -25,51 +22,45 @@ export interface PlanMatchQuery {
25
22
  }
26
23
  /** Lowercase, collapse non-alphanumeric runs to '-', trim leading/trailing '-'. */
27
24
  export declare function slugifyName(name: string | undefined): string;
25
+ export declare function isParamSegment(segment: string): boolean;
26
+ export declare function isOpaqueIdSegment(segment: string): boolean;
28
27
  /**
29
- * Normalize a path so param/id segments collapse: `{brace}`, `:colon`,
30
- * `[bracket]` params in any of routeParsers.ts's syntaxes, plus numeric,
31
- * UUID, and long-hex id VALUES, all become `:p`. Delegates to
32
- * routeParsers.ts's `isParamSegment`/`isOpaqueIdSegment` (SKYR-4214 task 5)
33
- * rather than a second inline copy — this module previously had its own
34
- * looser hex rule (`/^[0-9a-fA-F-]{16,}$/`, accepting `-`) and no
35
- * bracket-syntax rule at all, so a Next.js `[id]` segment never collapsed
36
- * and kept a match key from matching its `{id}`-spelled counterpart (the
37
- * SKYR-4123 failure mode: a generated test's key stops matching its plan
38
- * item). The characterization test in
39
- * `pathSegmentClassification.characterization.test.ts` confirms every
40
- * previously-agreeing input (UUID, numeric, 16+/24-char hex) still
41
- * collapses to `:p` after this change — safe within one run because the
42
- * register-time key and the generation-time query are computed by the same
43
- * build; a plan file is never persisted across builds. Mirrors
44
- * discriminators.ts's normalizePath (duplicated rather than imported —
45
- * that function verifies discriminator claims against a diff, a different
46
- * concern from plan matching, and keeping them independent avoids coupling
47
- * this module's stability to phase 1's internal helper).
28
+ * Normalize a path so param and id segments collapse to `:p`.
29
+ *
30
+ * The two predicates above are the only rule for it — an earlier inline copy had
31
+ * no bracket rule, so a Next.js `[id]` stopped matching its `{id}` spelling.
48
32
  */
49
33
  export declare function normalizeMatchPath(rawPath: string | undefined): string;
50
- /** Name key (if a name exists) plus one endpoint key per step. */
51
- export declare function computeMatchKeys(scenario: DraftedScenario): string[];
52
- /** Build the persisted plan item from a ranked Candidate. */
53
- export declare function buildApprovedPlanItem(candidate: Candidate): ApprovedPlanItem;
34
+ /** Name key (if a name exists) plus one endpoint key per step. BOTH kinds carry
35
+ * the planned test's test type, or a contract call matches an integration-only
36
+ * planned test sharing its name. A slug holds no `:`. */
37
+ export declare function computeMatchKeys(scenario: PlanScenario): string[];
54
38
  /**
55
- * True when `query` matches at least one of `items`' matchKeys:
56
- * - by scenarioName slug (`name:<slug>`), or
39
+ * True when `query` matches `keys`:
40
+ * - by testType + scenarioName slug (`name:<testType>:<slug>`), or
57
41
  * - by testType + path (`ep:<testType>:*:<path>`), method-exact when
58
42
  * `query.method` is given, method-agnostic (any method) otherwise — the
59
43
  * contract tool's `method: ""` ("test all methods") case. Path comparison
60
44
  * tolerates a base-path prefix on either side (see normalizedPathsMatch).
45
+ * The planned test's test type gates BOTH channels: gating the endpoint channel
46
+ * alone let a contract call pass against an integration-only planned test of the
47
+ * same name. Where the query carries an endpoint the name match must agree on it
48
+ * too, since `slugifyName` folds case and every run of non-alphanumerics.
61
49
  */
62
- export declare function matchesApprovedItem(items: ApprovedPlanItem[], query: PlanMatchQuery): boolean;
50
+ export declare function matchesMatchKeys(keys: string[], query: PlanMatchQuery): boolean;
63
51
  /**
64
- * True when `query` is admissible under the approved plan: it matches a
65
- * GENERATE item, or it matches an ADDITIONAL item while the plan has at least
66
- * one GENERATE item. ADDITIONAL is strictly a backfill pool for failed
67
- * GENERATE items — on a zero-budget plan (`generate: []`, e.g. maxGenerate=0
68
- * for a config-only PR) there is nothing to backfill, so ADDITIONAL must not
69
- * admit generation. Shared by planGuard and submitReportTool so the gate and
70
- * the report cross-check can never disagree on admissibility.
52
+ * The planned tests whose scenarioName and testType match but whose planned steps
53
+ * name no endpoint the query addresses — the near miss a boolean cannot report,
54
+ * where the real disagreement is one step's path spelling. Empty when the query
55
+ * names no path.
71
56
  */
72
- export declare function matchesApprovedPlan(plan: {
73
- generate: ApprovedPlanItem[];
74
- additional: ApprovedPlanItem[];
75
- }, query: PlanMatchQuery): boolean;
57
+ export declare function nameMatchedEndpointMismatches(plannedTests: PlannedTest[], query: PlanMatchQuery): PlannedTest[];
58
+ /** The planned tests whose scenarioName matches but whose testType does not — the
59
+ * plan holds this scenario and the call asked a different generator for it, so
60
+ * "not in the plan" is the wrong thing to say. Empty when the query names no
61
+ * type, since `nameAgrees` then matches on the name alone. */
62
+ export declare function nameMatchedTypeMismatches(plannedTests: PlannedTest[], query: PlanMatchQuery): PlannedTest[];
63
+ /** True when `query` matches at least one planned test of the registered plan.
64
+ * The keys are computed per call rather than stored with the plan, so there is
65
+ * one description of a planned test and no second shape to drift from it. */
66
+ export declare function matchesPlanCandidates(plannedTests: PlannedTest[], query: PlanMatchQuery): boolean;