@skyramp/mcp 0.3.5 → 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 (157) 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 +80 -34
  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 +411 -114
  60. package/build/tools/test-management/analyzeChangesTool.d.ts +24 -1
  61. package/build/tools/test-management/analyzeChangesTool.js +71 -10
  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 +70 -12
  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 +10 -1
  73. package/build/types/index.d.ts +2 -2
  74. package/build/types/index.js +1 -1
  75. package/build/utils/AnalysisStateManager.d.ts +57 -1
  76. package/build/utils/AnalysisStateManager.js +54 -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/reportVerification.d.ts +64 -4
  101. package/build/utils/reportVerification.js +228 -3
  102. package/build/utils/reuseRouting.d.ts +3 -0
  103. package/build/utils/reuseRouting.js +50 -0
  104. package/build/utils/routeParsers.d.ts +2 -0
  105. package/build/utils/routeParsers.js +65 -8
  106. package/build/utils/scenarioDrafting.d.ts +1 -1
  107. package/build/utils/scenarioDrafting.js +57 -45
  108. package/build/utils/subjectEndpoints.d.ts +19 -0
  109. package/build/utils/subjectEndpoints.js +98 -0
  110. package/build/utils/testFileClassification.d.ts +11 -0
  111. package/build/utils/testFileClassification.js +47 -0
  112. package/build/utils/uiPageEnumerator.d.ts +45 -19
  113. package/build/utils/uiPageEnumerator.js +95 -51
  114. package/build/utils/utils-verify/allow.d.ts +16 -0
  115. package/build/utils/utils-verify/allow.js +68 -0
  116. package/build/utils/utils-verify/call-sites.d.ts +34 -0
  117. package/build/utils/utils-verify/call-sites.js +154 -0
  118. package/build/utils/utils-verify/index.d.ts +7 -0
  119. package/build/utils/utils-verify/index.js +7 -0
  120. package/build/utils/utils-verify/language-spec.d.ts +91 -0
  121. package/build/utils/utils-verify/language-spec.js +210 -0
  122. package/build/utils/utils-verify/locate.d.ts +39 -0
  123. package/build/utils/utils-verify/locate.js +199 -0
  124. package/build/utils/utils-verify/parse.d.ts +34 -0
  125. package/build/utils/utils-verify/parse.js +177 -0
  126. package/build/utils/utils-verify/stage.d.ts +24 -0
  127. package/build/utils/utils-verify/stage.js +107 -0
  128. package/build/utils/utils-verify/verify.d.ts +63 -0
  129. package/build/utils/utils-verify/verify.js +168 -0
  130. package/build/utils/utils.d.ts +3 -1
  131. package/build/utils/utils.js +3 -1
  132. package/build/workspace/workspace.d.ts +32 -32
  133. package/node_modules/playwright/lib/mcp/skyramp/assertTool.js +9 -5
  134. package/node_modules/playwright/lib/mcp/skyramp/loadTraceTool.js +16 -0
  135. package/node_modules/playwright/lib/mcp/skyramp/skyRampImport.js +2 -0
  136. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +115 -14
  137. package/node_modules/playwright/lib/mcp/test/skyRampExport.js +13 -1
  138. package/node_modules/playwright/node_modules/playwright-core/.DS_Store +0 -0
  139. package/node_modules/playwright/node_modules/playwright-core/lib/server/codegen/skyramp/jsonlReader.js +2 -0
  140. package/node_modules/playwright/node_modules/playwright-core/lib/vite/htmlReport/index.html +27 -253
  141. package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/{codeMirrorModule-DtudTj_v.js → codeMirrorModule-DJMC4zNo.js} +1 -1
  142. package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/index-BW82eAUI.js +196 -0
  143. package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/index.html +1 -1
  144. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-FNMuBzX1.js → codeMirrorModule-CZfp96qZ.js} +1 -1
  145. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-gpLo02E0.js +809 -0
  146. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.Bq1r1URj.js +2 -0
  147. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +2 -2
  148. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.VEfqi1qN.js +5 -0
  149. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +2 -2
  150. package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
  151. package/node_modules/playwright/node_modules/playwright-core/src/server/codegen/skyramp/jsonlReader.ts +1 -1
  152. package/node_modules/playwright/package.json +1 -1
  153. package/package.json +2 -2
  154. package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/index-BpDwp16L.js +0 -422
  155. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-Co9upU5h.js +0 -1035
  156. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.DXNIQ_dx.js +0 -2
  157. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.CIKB3XSv.js +0 -5
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Convert a plural resource name to its singular form for use in field names
3
+ * and step descriptions.
4
+ *
5
+ * Handles the most common irregular plurals found in REST API paths:
6
+ * -ies → -y (categories → category, companies → company)
7
+ * -ses → -s (statuses → status, classes → class)
8
+ * Falls back to removing the trailing "s" for regular plurals.
9
+ */
10
+ export function singularize(word) {
11
+ if (word.endsWith("ies") && word.length > 3) {
12
+ return word.slice(0, -3) + "y";
13
+ }
14
+ if (word.endsWith("ses") && word.length > 4) {
15
+ return word.slice(0, -2); // statuses → status
16
+ }
17
+ return word.endsWith("s") && word.length > 1 ? word.slice(0, -1) : word;
18
+ }
@@ -0,0 +1,52 @@
1
+ import type { ReuseMissedEntry } from "../types/ReuseOutcome.js";
2
+ /**
3
+ * Parses the agent-written POM catalog (`.skyramp/skyramp-pom-catalog.md`, path owned
4
+ * by `pom-catalog.ts`) and joins it against the raw locators a delivered spec still
5
+ * carries inline.
6
+ *
7
+ * Why the catalog and not `selectScopedPoms`: that join is a whole-token substring
8
+ * test (`pomContent.includes(specToken)`), which is structurally blind to
9
+ * prefix-parameterized selectors — the exact thing a good page object does. Measured
10
+ * on openobserve run 31662701508 it misses the covering file entirely and returns two
11
+ * unrelated pages; widening it to prefix matching finds the right file but pulls in 32
12
+ * more. The catalog is already scoped and records the wildcard form, so the same match
13
+ * is precise there.
14
+ *
15
+ * Everything here is informational — no caller gates on it — so the bias throughout is
16
+ * to under-report rather than assert. A malformed catalog yields no members, and a
17
+ * member with no recorded selector simply covers nothing.
18
+ */
19
+ /** One catalogued property or method, with its selectors as wildcard patterns. */
20
+ export interface CatalogMember {
21
+ className: string;
22
+ /** As the catalog names it: `propertyName` or `methodName(a, b)`. */
23
+ member: string;
24
+ kind: "property" | "method";
25
+ /** Basename of the class's `Import:` path — extension-less, see ReuseMissedEntry. */
26
+ pageObject: string;
27
+ /** Wildcard patterns; `*` matches any run of characters. */
28
+ selectors: string[];
29
+ }
30
+ /**
31
+ * Every property and method in the catalog. Returns `[]` for anything that is not a
32
+ * catalog — this runs on a file the agent wrote, so unparseable input is an expected
33
+ * state, not an error.
34
+ */
35
+ export declare function parsePomCatalog(markdown: string): CatalogMember[];
36
+ /**
37
+ * Raw locators the delivered spec still carries, and which of them a catalogued member
38
+ * covers.
39
+ *
40
+ * The spec side is `extractRawLocatorTokens`, a sibling of the POM-detection
41
+ * `extractSelectors` that excludes `getByRole`'s role token and the bare `name:`
42
+ * field — those are detection evidence, not locators, and `name:` in particular can
43
+ * capture a POM method's own data argument (e.g. `fp.createFunction({ name: "myFunction" })`).
44
+ * Both numbers come from one pass so the denominator and the misses cannot disagree.
45
+ *
46
+ * First match wins per locator. A locator covered by two members is still one missed
47
+ * reuse, and naming one of them is enough to send a reviewer to the right file.
48
+ */
49
+ export declare function findMissedReuse(specContent: string, members: CatalogMember[]): {
50
+ rawLocatorsInline: number;
51
+ missed: ReuseMissedEntry[];
52
+ };
@@ -0,0 +1,141 @@
1
+ import * as path from "path";
2
+ import { extractRawLocatorTokens } from "./pom-scope/selector-extractor.js";
3
+ import { escapeRegExp } from "./regex.js";
4
+ const CLASS_HEADING_RE = /^##\s+(.+?)\s*$/;
5
+ const IMPORT_RE = /^-\s+\*\*Import:\*\*\s+`([^`]+)`/;
6
+ const PROPERTIES_RE = /^-\s+\*\*Properties:\*\*/;
7
+ const METHODS_RE = /^-\s+\*\*Methods:\*\*/;
8
+ const OTHER_FIELD_RE = /^-\s+\*\*/;
9
+ /** A member bullet: indented, backticked name, dash separator, then the description. */
10
+ const MEMBER_RE = /^\s+-\s+`([^`]+)`\s*[—–-]\s*(.*)$/;
11
+ const BACKTICKED_RE = /`([^`]+)`/g;
12
+ /** `[data-test="value"]`, including prefix/suffix matchers — the value is the token. */
13
+ const ATTR_VALUE_RE = /\[[A-Za-z-]+[$^*~|]?="([^"]+)"\]/g;
14
+ /** A bare token that looks like a selector value rather than prose. */
15
+ const BARE_TOKEN_RE = /^[A-Za-z0-9][A-Za-z0-9._:-]*\*?$/;
16
+ /** Below this, a bare token is too generic to claim coverage of anything. Attribute
17
+ * values are exempt: `[data-test="x"]` is unambiguous evidence regardless of length. */
18
+ const MIN_BARE_TOKEN_LENGTH = 6;
19
+ /** `${...}` and `*` both mean "any run of characters" — normalize to one form. */
20
+ function normalizeWildcards(token) {
21
+ return token.replace(/\$\{[^}]*\}/g, "*");
22
+ }
23
+ /** Selector patterns in a member's DESCRIPTION — the text after the dash, never the
24
+ * member name itself. Scanning the whole bullet would make a property register its own
25
+ * name as a selector (`addFunctionButton` is a valid bare token), so a spec locator that
26
+ * happened to equal a property name would read as covered by that property, which is
27
+ * circular. */
28
+ function selectorsFrom(description) {
29
+ const out = new Set();
30
+ for (const [, backticked] of description.matchAll(BACKTICKED_RE)) {
31
+ const token = normalizeWildcards(backticked);
32
+ let sawAttr = false;
33
+ for (const [, value] of token.matchAll(ATTR_VALUE_RE)) {
34
+ out.add(value);
35
+ sawAttr = true;
36
+ }
37
+ // A bare token is only a selector if the whole backticked run is one — otherwise
38
+ // it is prose, or an attribute selector already harvested above.
39
+ if (!sawAttr && BARE_TOKEN_RE.test(token) && token.length >= MIN_BARE_TOKEN_LENGTH) {
40
+ out.add(token);
41
+ }
42
+ }
43
+ return [...out];
44
+ }
45
+ /**
46
+ * Every property and method in the catalog. Returns `[]` for anything that is not a
47
+ * catalog — this runs on a file the agent wrote, so unparseable input is an expected
48
+ * state, not an error.
49
+ */
50
+ export function parsePomCatalog(markdown) {
51
+ const members = [];
52
+ let className;
53
+ let pageObject = "";
54
+ let section;
55
+ for (const line of markdown.split("\n")) {
56
+ const heading = CLASS_HEADING_RE.exec(line);
57
+ if (heading) {
58
+ // Free-function module headings carry a trailing gloss — `bookingHelpers
59
+ // (module functions, not a class)` (SKYR-4157 finding 4) — strip it so
60
+ // `className` is the bare module/class name, not prose.
61
+ className = heading[1].replace(/\s*\([^)]*\)\s*$/, "");
62
+ pageObject = "";
63
+ section = undefined;
64
+ continue;
65
+ }
66
+ if (!className)
67
+ continue;
68
+ const imported = IMPORT_RE.exec(line);
69
+ if (imported) {
70
+ // Extension-less by construction: the catalog records an import specifier, and
71
+ // `path.basename` of `../../pages/functionsPages/functionsPage` is what a reader
72
+ // needs to find the file.
73
+ pageObject = path.basename(imported[1]).replace(/\.(ts|js|mjs|cjs)$/, "");
74
+ continue;
75
+ }
76
+ if (PROPERTIES_RE.test(line)) {
77
+ section = "property";
78
+ continue;
79
+ }
80
+ if (METHODS_RE.test(line)) {
81
+ section = "method";
82
+ continue;
83
+ }
84
+ // Any other `- **Field:**` line (Iframe, depth markers) closes the current section.
85
+ if (OTHER_FIELD_RE.test(line)) {
86
+ section = undefined;
87
+ continue;
88
+ }
89
+ if (!section)
90
+ continue;
91
+ const member = MEMBER_RE.exec(line);
92
+ if (!member)
93
+ continue;
94
+ members.push({
95
+ className,
96
+ member: member[1],
97
+ kind: section,
98
+ pageObject,
99
+ // Description only — see selectorsFrom on why the member name is excluded.
100
+ selectors: selectorsFrom(member[2]),
101
+ });
102
+ }
103
+ return members;
104
+ }
105
+ /** Anchored matcher for one wildcard pattern. */
106
+ function toMatcher(pattern) {
107
+ return new RegExp(`^${pattern.split("*").map(escapeRegExp).join(".*")}$`);
108
+ }
109
+ /** The member name without its parameter list, for the `Class.member` label. */
110
+ function bareMemberName(member) {
111
+ return member.replace(/\(.*$/, "");
112
+ }
113
+ /**
114
+ * Raw locators the delivered spec still carries, and which of them a catalogued member
115
+ * covers.
116
+ *
117
+ * The spec side is `extractRawLocatorTokens`, a sibling of the POM-detection
118
+ * `extractSelectors` that excludes `getByRole`'s role token and the bare `name:`
119
+ * field — those are detection evidence, not locators, and `name:` in particular can
120
+ * capture a POM method's own data argument (e.g. `fp.createFunction({ name: "myFunction" })`).
121
+ * Both numbers come from one pass so the denominator and the misses cannot disagree.
122
+ *
123
+ * First match wins per locator. A locator covered by two members is still one missed
124
+ * reuse, and naming one of them is enough to send a reviewer to the right file.
125
+ */
126
+ export function findMissedReuse(specContent, members) {
127
+ const locators = extractRawLocatorTokens(specContent);
128
+ const compiled = members.map((m) => ({ m, matchers: m.selectors.map(toMatcher) }));
129
+ const missed = [];
130
+ for (const locator of locators) {
131
+ const hit = compiled.find(({ matchers }) => matchers.some((re) => re.test(locator)));
132
+ if (!hit)
133
+ continue;
134
+ missed.push({
135
+ locator,
136
+ pageObject: hit.m.pageObject,
137
+ member: `${hit.m.className}.${bareMemberName(hit.m.member)}`,
138
+ });
139
+ }
140
+ return { rawLocatorsInline: locators.length, missed };
141
+ }
@@ -4,4 +4,16 @@
4
4
  * these strings to find which page objects cover the test's actions.
5
5
  */
6
6
  export declare const DEFAULT_TESTID_ATTRS: string[];
7
+ /** POM-detection token set (SKYR-4157 finding 1: also used for scoring, unchanged
8
+ * behaviour) — includes role names and bare `name:` values as evidence, even
9
+ * though those are not themselves raw locators. Used by the POM scoping path
10
+ * (`pom-scope/index.ts`); do not repurpose for coverage counting. */
7
11
  export declare function extractSelectors(specContent: string, extraTestIdAttrs?: string[]): string[];
12
+ /** Genuine raw LOCATOR tokens only: test-id attribute values, `getByTestId`,
13
+ * `getByText`/`getByLabel`/`getByPlaceholder`, and raw `.locator()`/
14
+ * `.frameLocator()` strings. Excludes `getByRole`'s role token and the bare
15
+ * `name:` field — neither is a locator, and `name:` in particular can capture a
16
+ * POM method's own data argument (SKYR-4157 finding 1). Used for coverage
17
+ * counting (`pom-catalog-parse.ts`), where the denominator must be actual
18
+ * locators still inline, not detection evidence. */
19
+ export declare function extractRawLocatorTokens(specContent: string, extraTestIdAttrs?: string[]): string[];
@@ -16,12 +16,19 @@ const attrPattern = (attrs) => new RegExp(`\\[(?:${attrs.map(esc).join("|")})[$^
16
16
  // string is actually a family-attr selector, vs. one that merely CONTAINS the
17
17
  // substring (e.g. ".data-testing-notes" contains "data-test" but isn't one).
18
18
  const attrAnchor = (attrs) => new RegExp(`\\[(?:${attrs.map(esc).join("|")})[$^*~|]?=`);
19
- const OTHER_PATTERNS = [
20
- /getByTestId\(\s*["']([^"']+)["']/g, // getByTestId("X")
21
- /getByRole\(\s*["']([^"']+)["']/g, // getByRole("role", ...)
22
- /name:\s*["']([^"']+)["']/g, // { name: "Accessible Name" }
23
- /getBy(?:Text|Label|Placeholder)\(\s*["']([^"']+)["']/g, // getByText("T") etc.
24
- ];
19
+ // getByTestId("X"), getByText/Label/Placeholder("T") — these capture genuine raw
20
+ // locator values, shared by both extractSelectors and extractRawLocatorTokens.
21
+ const GET_BY_TEST_ID = /getByTestId\(\s*["']([^"']+)["']/g;
22
+ const GET_BY_TEXT_LABEL_PLACEHOLDER = /getBy(?:Text|Label|Placeholder)\(\s*["']([^"']+)["']/g;
23
+ const LOCATOR_PATTERNS = [GET_BY_TEST_ID, GET_BY_TEXT_LABEL_PLACEHOLDER];
24
+ // getByRole("role", ...) and bare `name: "..."` object keys are useful evidence for
25
+ // POM *detection* (scoring which page objects a test's actions touch), but they are
26
+ // NOT raw locator values: a role name isn't a selector, and `name:` matches ANY bare
27
+ // object key so named — including a POM method's own data argument (SKYR-4157
28
+ // finding 1). Only extractSelectors (the detection path) includes these.
29
+ const GET_BY_ROLE = /getByRole\(\s*["']([^"']+)["']/g;
30
+ const BARE_NAME_FIELD = /name:\s*["']([^"']+)["']/g;
31
+ const OTHER_PATTERNS = [GET_BY_TEST_ID, GET_BY_ROLE, BARE_NAME_FIELD, GET_BY_TEXT_LABEL_PLACEHOLDER];
25
32
  // Raw .locator("...")/.frameLocator("...") strings that are NOT test-id-attribute based
26
33
  // (e.g. ".step-deduplication", "iframe#asset-list").
27
34
  // Quote-type-aware alternates: a double-quoted arg may contain unescaped single quotes
@@ -35,11 +42,13 @@ function normalize(token) {
35
42
  function isUseful(token) {
36
43
  return token.length >= 3 && !/^\d+$/.test(token);
37
44
  }
38
- export function extractSelectors(specContent, extraTestIdAttrs = []) {
45
+ /** Shared core: test-id-attribute values, the given value-bearing patterns, and raw
46
+ * `.locator()`/`.frameLocator()` strings not already covered by an attribute match. */
47
+ function collectTokens(specContent, patterns, extraTestIdAttrs) {
39
48
  const testIdAttrs = [...DEFAULT_TESTID_ATTRS, ...extraTestIdAttrs];
40
49
  const anchor = attrAnchor(testIdAttrs);
41
50
  const tokens = new Set();
42
- for (const re of [attrPattern(testIdAttrs), ...OTHER_PATTERNS]) {
51
+ for (const re of [attrPattern(testIdAttrs), ...patterns]) {
43
52
  for (const m of specContent.matchAll(re)) {
44
53
  const t = normalize(m[1]);
45
54
  if (isUseful(t))
@@ -55,3 +64,20 @@ export function extractSelectors(specContent, extraTestIdAttrs = []) {
55
64
  }
56
65
  return [...tokens];
57
66
  }
67
+ /** POM-detection token set (SKYR-4157 finding 1: also used for scoring, unchanged
68
+ * behaviour) — includes role names and bare `name:` values as evidence, even
69
+ * though those are not themselves raw locators. Used by the POM scoping path
70
+ * (`pom-scope/index.ts`); do not repurpose for coverage counting. */
71
+ export function extractSelectors(specContent, extraTestIdAttrs = []) {
72
+ return collectTokens(specContent, OTHER_PATTERNS, extraTestIdAttrs);
73
+ }
74
+ /** Genuine raw LOCATOR tokens only: test-id attribute values, `getByTestId`,
75
+ * `getByText`/`getByLabel`/`getByPlaceholder`, and raw `.locator()`/
76
+ * `.frameLocator()` strings. Excludes `getByRole`'s role token and the bare
77
+ * `name:` field — neither is a locator, and `name:` in particular can capture a
78
+ * POM method's own data argument (SKYR-4157 finding 1). Used for coverage
79
+ * counting (`pom-catalog-parse.ts`), where the denominator must be actual
80
+ * locators still inline, not detection evidence. */
81
+ export function extractRawLocatorTokens(specContent, extraTestIdAttrs = []) {
82
+ return collectTokens(specContent, LOCATOR_PATTERNS, extraTestIdAttrs);
83
+ }
@@ -7,13 +7,14 @@ export type ReuseViolation = {
7
7
  export type VerifyResult = {
8
8
  ok: boolean;
9
9
  violations: ReuseViolation[];
10
- unverifiable: string[];
10
+ unverifiableCalls: string[];
11
+ unrecognizedBindings: string[];
11
12
  parseError?: string;
12
13
  checkedCalls: number;
13
- /** Every POM call found in the spec, verifiable or not. NOT checkedCalls +
14
- * unverifiable.length: `unverifiable` also absorbs `unrecognized` bindings,
15
- * which are not calls at all, so that sum overcounts. This is the count of
16
- * POM members the delivered spec actually reuses. */
14
+ /** Every POM call found in the spec, verifiable or not. Equals
15
+ * `checkedCalls + unverifiableCalls.length`: `unrecognizedBindings` is kept
16
+ * separate precisely because those are not calls and would overcount. This is
17
+ * the count of POM members the delivered spec actually reuses. */
17
18
  reusedCalls: number;
18
19
  };
19
20
  export declare function verifyReuse(testFile: string, language: string): Promise<VerifyResult>;
@@ -126,18 +126,19 @@ export async function verifyReuse(testFile, language) {
126
126
  const calls = extractPomCalls(spec, bindings);
127
127
  const bindingsByName = new Map(bindings.map((b) => [b.name, b]));
128
128
  const violations = [];
129
- const unverifiable = [];
129
+ const unverifiableCalls = [];
130
+ const unrecognizedBindings = [];
130
131
  let checkedCalls = 0;
131
132
  for (const call of calls) {
132
133
  const binding = bindingsByName.get(call.binding);
133
134
  if (!binding) {
134
135
  // Shouldn't happen -- extractPomCalls only matches against the bindings it was given --
135
136
  // but degrade to unverifiable rather than throw if the invariant ever breaks.
136
- unverifiable.push([call.binding, ...call.chain].join("."));
137
+ unverifiableCalls.push([call.binding, ...call.chain].join("."));
137
138
  continue;
138
139
  }
139
140
  if (!call.strict) {
140
- unverifiable.push([call.binding, ...call.chain].join("."));
141
+ unverifiableCalls.push([call.binding, ...call.chain].join("."));
141
142
  continue;
142
143
  }
143
144
  checkedCalls++;
@@ -156,7 +157,7 @@ export async function verifyReuse(testFile, language) {
156
157
  // remap/demote loop the agent cannot win (no candidates to remap to → forced demote
157
158
  // of possibly-correct reuse). Degrade to unverifiable — the fail-open contract at
158
159
  // the reachability level, matching resolve.ts's permissive stance on shapes.
159
- unverifiable.push([call.binding, ...call.chain].join("."));
160
+ unverifiableCalls.push([call.binding, ...call.chain].join("."));
160
161
  checkedCalls--;
161
162
  continue;
162
163
  }
@@ -164,7 +165,7 @@ export async function verifyReuse(testFile, language) {
164
165
  }
165
166
  }
166
167
  for (const u of unrecognized)
167
- unverifiable.push(u);
168
+ unrecognizedBindings.push(u);
168
169
  let parseError;
169
170
  if (language === "javascript") {
170
171
  try {
@@ -179,7 +180,8 @@ export async function verifyReuse(testFile, language) {
179
180
  return {
180
181
  ok: violations.length === 0 && parseError === undefined,
181
182
  violations,
182
- unverifiable,
183
+ unverifiableCalls,
184
+ unrecognizedBindings,
183
185
  parseError,
184
186
  checkedCalls,
185
187
  reusedCalls: calls.length,
@@ -25,7 +25,7 @@ import { DriftAction } from "../types/TestAnalysis.js";
25
25
  */
26
26
  export declare function listChangedFilesAcross(roots: Array<string | undefined>): Promise<string[]>;
27
27
  export declare function listChangedFiles(repoRoot: string): Promise<string[]>;
28
- export interface UnbackedClaimsInput {
28
+ export interface UnchangedFileClaimsInput {
29
29
  /** Absolute path of the primary repo checkout the report pertains to. */
30
30
  repoRoot: string;
31
31
  /** Repo-relative paths that have a working-tree change (from listChangedFiles). */
@@ -49,8 +49,14 @@ export interface UnbackedClaimsInput {
49
49
  }
50
50
  /**
51
51
  * Cross-check report claims against files that actually changed in the working
52
- * tree, returning a human-readable description for each claim NOT backed by a
53
- * 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".
54
60
  *
55
61
  * The delivery path can only ship what the agent actually edited (SKYR-3883), so
56
62
  * a report must never claim file work the tree doesn't reflect. This is the
@@ -73,4 +79,58 @@ export interface UnbackedClaimsInput {
73
79
  * - Claims for files outside this checkout (cross-repo `repository`, or an
74
80
  * absolute path resolving outside `repoRoot`) — a different checkout owns them.
75
81
  */
76
- 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>;