@skyramp/mcp 0.3.4 → 0.3.6-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/build/playwright/registerPlaywrightTools.js +92 -30
  2. package/build/playwright/traceRecordingPrompt.d.ts +6 -0
  3. package/build/playwright/traceRecordingPrompt.js +6 -2
  4. package/build/prompts/code-reuse.d.ts +1 -2
  5. package/build/prompts/code-reuse.js +182 -77
  6. package/build/prompts/modularization/integration-test-modularization.d.ts +2 -0
  7. package/build/prompts/modularization/integration-test-modularization.js +83 -41
  8. package/build/prompts/modularization/render.d.ts +18 -0
  9. package/build/prompts/modularization/render.js +12 -0
  10. package/build/prompts/modularization/ui-test-modularization.d.ts +3 -1
  11. package/build/prompts/modularization/ui-test-modularization.js +89 -47
  12. package/build/prompts/pom-aware-code-reuse.js +3 -1
  13. package/build/prompts/shared-helper-policy.d.ts +57 -0
  14. package/build/prompts/shared-helper-policy.js +135 -0
  15. package/build/prompts/test-recommendation/diffExecutionPlan.js +62 -56
  16. package/build/prompts/test-recommendation/fullRepoCatalog.js +19 -8
  17. package/build/prompts/test-recommendation/recommendationShared.d.ts +28 -6
  18. package/build/prompts/test-recommendation/recommendationShared.js +90 -16
  19. package/build/prompts/test-recommendation/registerRecommendTestsPrompt.js +22 -0
  20. package/build/prompts/test-recommendation/test-recommendation-prompt.d.ts +2 -2
  21. package/build/prompts/test-recommendation/test-recommendation-prompt.js +3 -3
  22. package/build/prompts/testbot/testbot-prompts.js +88 -33
  23. package/build/recommendation/budgeters/shared.js +105 -27
  24. package/build/recommendation/discriminators.js +13 -2
  25. package/build/recommendation/planRanker.d.ts +6 -6
  26. package/build/recommendation/planRanker.js +6 -61
  27. package/build/services/AnalyticsService.d.ts +7 -0
  28. package/build/services/AnalyticsService.js +7 -1
  29. package/build/services/ModularizationService.js +1 -3
  30. package/build/services/TestDiscoveryService.d.ts +0 -2
  31. package/build/services/TestDiscoveryService.js +2 -37
  32. package/build/services/TestGenerationService.d.ts +16 -0
  33. package/build/services/TestGenerationService.js +86 -10
  34. package/build/services/containerEnv.js +13 -12
  35. package/build/tools/code-refactor/codeReuseTool.js +279 -93
  36. package/build/tools/code-refactor/enhance-state.d.ts +49 -0
  37. package/build/tools/code-refactor/enhance-state.js +109 -0
  38. package/build/tools/code-refactor/enhanceAssertionsTool.js +34 -1
  39. package/build/tools/code-refactor/modularizationTool.js +9 -2
  40. package/build/tools/code-refactor/reuse-outcome.d.ts +23 -1
  41. package/build/tools/code-refactor/reuse-outcome.js +14 -4
  42. package/build/tools/code-refactor/reuse-state.d.ts +127 -5
  43. package/build/tools/code-refactor/reuse-state.js +628 -16
  44. package/build/tools/code-refactor/utils-verify-gates.d.ts +26 -0
  45. package/build/tools/code-refactor/utils-verify-gates.js +100 -0
  46. package/build/tools/code-refactor/verify-gates.d.ts +2 -1
  47. package/build/tools/code-refactor/verify-gates.js +90 -25
  48. package/build/tools/executeSkyrampTestTool.d.ts +19 -0
  49. package/build/tools/executeSkyrampTestTool.js +158 -8
  50. package/build/tools/generate-tests/generateBatchScenarioRestTool.js +2 -2
  51. package/build/tools/generate-tests/generateE2ERestTool.js +16 -0
  52. package/build/tools/generate-tests/generateUIRestTool.d.ts +1 -0
  53. package/build/tools/generate-tests/generateUIRestTool.js +22 -0
  54. package/build/tools/generate-tests/scenarioLint.d.ts +2 -0
  55. package/build/tools/generate-tests/scenarioLint.js +127 -19
  56. package/build/tools/generate-tests/trace-reuse-guard.d.ts +20 -0
  57. package/build/tools/generate-tests/trace-reuse-guard.js +93 -0
  58. package/build/tools/submitReportTool.d.ts +38 -38
  59. package/build/tools/submitReportTool.js +487 -104
  60. package/build/tools/test-management/analyzeChangesTool.d.ts +24 -1
  61. package/build/tools/test-management/analyzeChangesTool.js +75 -12
  62. package/build/tools/test-management/analyzeTestHealthTool.js +7 -7
  63. package/build/tools/test-management/registerTestPlanTool.d.ts +203 -0
  64. package/build/tools/test-management/registerTestPlanTool.js +149 -23
  65. package/build/types/Recommendation.d.ts +34 -5
  66. package/build/types/RepositoryAnalysis.d.ts +133 -114
  67. package/build/types/RepositoryAnalysis.js +1 -1
  68. package/build/types/ReuseOutcome.d.ts +102 -6
  69. package/build/types/ReuseOutcome.js +16 -2
  70. package/build/types/TestRecommendation.js +21 -3
  71. package/build/types/TestTypes.js +14 -8
  72. package/build/types/TestbotReport.d.ts +25 -3
  73. package/build/types/index.d.ts +2 -2
  74. package/build/types/index.js +1 -1
  75. package/build/utils/AnalysisStateManager.d.ts +69 -1
  76. package/build/utils/AnalysisStateManager.js +69 -5
  77. package/build/utils/branchDiff.d.ts +10 -0
  78. package/build/utils/branchDiff.js +28 -0
  79. package/build/utils/changedRoutes.d.ts +29 -0
  80. package/build/utils/changedRoutes.js +87 -0
  81. package/build/utils/featureFlags.d.ts +21 -0
  82. package/build/utils/featureFlags.js +23 -0
  83. package/build/utils/frontendIntegration.js +34 -4
  84. package/build/utils/importerHop.d.ts +2 -8
  85. package/build/utils/importerHop.js +15 -53
  86. package/build/utils/pathMatching.d.ts +38 -0
  87. package/build/utils/pathMatching.js +71 -0
  88. package/build/utils/pathSignatures.d.ts +22 -0
  89. package/build/utils/pathSignatures.js +57 -0
  90. package/build/utils/planMatchKeys.d.ts +16 -3
  91. package/build/utils/planMatchKeys.js +26 -10
  92. package/build/utils/pluralization.d.ts +10 -0
  93. package/build/utils/pluralization.js +18 -0
  94. package/build/utils/pom-catalog-parse.d.ts +52 -0
  95. package/build/utils/pom-catalog-parse.js +141 -0
  96. package/build/utils/pom-scope/selector-extractor.d.ts +12 -0
  97. package/build/utils/pom-scope/selector-extractor.js +34 -8
  98. package/build/utils/pom-verify/verify.d.ts +6 -5
  99. package/build/utils/pom-verify/verify.js +8 -6
  100. package/build/utils/reportLanguage.d.ts +43 -0
  101. package/build/utils/reportLanguage.js +125 -0
  102. package/build/utils/reportVerification.d.ts +74 -4
  103. package/build/utils/reportVerification.js +259 -3
  104. package/build/utils/reuseRouting.d.ts +3 -0
  105. package/build/utils/reuseRouting.js +50 -0
  106. package/build/utils/routeParsers.d.ts +2 -0
  107. package/build/utils/routeParsers.js +65 -8
  108. package/build/utils/scenarioDrafting.d.ts +1 -1
  109. package/build/utils/scenarioDrafting.js +57 -45
  110. package/build/utils/subjectEndpoints.d.ts +19 -0
  111. package/build/utils/subjectEndpoints.js +98 -0
  112. package/build/utils/testFileClassification.d.ts +11 -0
  113. package/build/utils/testFileClassification.js +47 -0
  114. package/build/utils/uiPageEnumerator.d.ts +45 -19
  115. package/build/utils/uiPageEnumerator.js +95 -51
  116. package/build/utils/utils-verify/allow.d.ts +16 -0
  117. package/build/utils/utils-verify/allow.js +68 -0
  118. package/build/utils/utils-verify/call-sites.d.ts +34 -0
  119. package/build/utils/utils-verify/call-sites.js +154 -0
  120. package/build/utils/utils-verify/index.d.ts +7 -0
  121. package/build/utils/utils-verify/index.js +7 -0
  122. package/build/utils/utils-verify/language-spec.d.ts +91 -0
  123. package/build/utils/utils-verify/language-spec.js +210 -0
  124. package/build/utils/utils-verify/locate.d.ts +39 -0
  125. package/build/utils/utils-verify/locate.js +199 -0
  126. package/build/utils/utils-verify/parse.d.ts +34 -0
  127. package/build/utils/utils-verify/parse.js +177 -0
  128. package/build/utils/utils-verify/stage.d.ts +24 -0
  129. package/build/utils/utils-verify/stage.js +107 -0
  130. package/build/utils/utils-verify/verify.d.ts +63 -0
  131. package/build/utils/utils-verify/verify.js +168 -0
  132. package/build/utils/utils.d.ts +3 -1
  133. package/build/utils/utils.js +3 -1
  134. package/build/workspace/workspace.d.ts +32 -32
  135. package/node_modules/playwright/lib/mcp/skyramp/assertTool.js +9 -5
  136. package/node_modules/playwright/lib/mcp/skyramp/loadTraceTool.js +16 -0
  137. package/node_modules/playwright/lib/mcp/skyramp/skyRampImport.js +2 -0
  138. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +115 -14
  139. package/node_modules/playwright/lib/mcp/test/skyRampExport.js +13 -1
  140. package/node_modules/playwright/node_modules/playwright-core/.DS_Store +0 -0
  141. package/node_modules/playwright/node_modules/playwright-core/lib/server/codegen/skyramp/jsonlReader.js +2 -0
  142. package/node_modules/playwright/node_modules/playwright-core/lib/vite/htmlReport/index.html +27 -253
  143. package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/{codeMirrorModule-DtudTj_v.js → codeMirrorModule-DJMC4zNo.js} +1 -1
  144. package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/index-BW82eAUI.js +196 -0
  145. package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/index.html +1 -1
  146. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-FNMuBzX1.js → codeMirrorModule-CZfp96qZ.js} +1 -1
  147. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-gpLo02E0.js +809 -0
  148. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.Bq1r1URj.js +2 -0
  149. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +2 -2
  150. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.VEfqi1qN.js +5 -0
  151. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +2 -2
  152. package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
  153. package/node_modules/playwright/node_modules/playwright-core/src/server/codegen/skyramp/jsonlReader.ts +1 -1
  154. package/node_modules/playwright/package.json +1 -1
  155. package/package.json +2 -2
  156. package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/index-BpDwp16L.js +0 -422
  157. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-Co9upU5h.js +0 -1035
  158. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.DXNIQ_dx.js +0 -2
  159. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.CIKB3XSv.js +0 -5
@@ -14,6 +14,7 @@
14
14
  * matching (contract tests, or a fallback when names drift between
15
15
  * register and generate).
16
16
  */
17
+ import { isParamSegment, isOpaqueIdSegment } from "./routeParsers.js";
17
18
  /** Lowercase, collapse non-alphanumeric runs to '-', trim leading/trailing '-'. */
18
19
  export function slugifyName(name) {
19
20
  return (name ?? "")
@@ -22,9 +23,22 @@ export function slugifyName(name) {
22
23
  .replace(/^-+|-+$/g, "");
23
24
  }
24
25
  /**
25
- * Normalize a path so param segments collapse: numeric ids, `{brace}` params,
26
- * `:colon` params, and long hex/uuid segments all become `:p`. Mirrors
27
- * discriminators.ts's normalizePath (duplicated here rather than imported —
26
+ * Normalize a path so param/id segments collapse: `{brace}`, `:colon`,
27
+ * `[bracket]` params in any of routeParsers.ts's syntaxes, plus numeric,
28
+ * UUID, and long-hex id VALUES, all become `:p`. Delegates to
29
+ * routeParsers.ts's `isParamSegment`/`isOpaqueIdSegment` (SKYR-4214 task 5)
30
+ * rather than a second inline copy — this module previously had its own
31
+ * looser hex rule (`/^[0-9a-fA-F-]{16,}$/`, accepting `-`) and no
32
+ * bracket-syntax rule at all, so a Next.js `[id]` segment never collapsed
33
+ * and kept a match key from matching its `{id}`-spelled counterpart (the
34
+ * SKYR-4123 failure mode: a generated test's key stops matching its plan
35
+ * item). The characterization test in
36
+ * `pathSegmentClassification.characterization.test.ts` confirms every
37
+ * previously-agreeing input (UUID, numeric, 16+/24-char hex) still
38
+ * collapses to `:p` after this change — safe within one run because the
39
+ * register-time key and the generation-time query are computed by the same
40
+ * build; a plan file is never persisted across builds. Mirrors
41
+ * discriminators.ts's normalizePath (duplicated rather than imported —
28
42
  * that function verifies discriminator claims against a diff, a different
29
43
  * concern from plan matching, and keeping them independent avoids coupling
30
44
  * this module's stability to phase 1's internal helper).
@@ -36,13 +50,12 @@ export function normalizeMatchPath(rawPath) {
36
50
  .map((seg) => {
37
51
  if (seg === "")
38
52
  return seg;
39
- if (/^\{.*\}$/.test(seg))
40
- return ":p";
41
- if (/^:.+/.test(seg))
42
- return ":p";
43
- if (/^\d+$/.test(seg))
44
- return ":p";
45
- if (/^[0-9a-fA-F-]{16,}$/.test(seg))
53
+ // `isParamSegment` covers the well-formed colon family — `:id`,
54
+ // `:id(\d+)`, `:id?`, `:id*`, `:id+`. The `/^:/` test in front of it is
55
+ // deliberate breadth for the colon segments that regex rejects, such as
56
+ // `:1abc` or `:id-x`: a malformed param must still normalize to `:p`,
57
+ // or a registered `/users/:1abc` stops endpoint-matching `/users/42`.
58
+ if (/^:/.test(seg) || isParamSegment(seg) || isOpaqueIdSegment(seg))
46
59
  return ":p";
47
60
  return seg.toLowerCase();
48
61
  })
@@ -74,6 +87,9 @@ export function buildApprovedPlanItem(candidate) {
74
87
  category: candidate.scenario.category,
75
88
  source: candidate.source,
76
89
  ...(candidate.verifiedDiscriminator ? { verifiedDiscriminator: candidate.verifiedDiscriminator } : {}),
90
+ ...(candidate.scenario.subjectEndpoints !== undefined
91
+ ? { subjectEndpoints: candidate.scenario.subjectEndpoints }
92
+ : {}),
77
93
  matchKeys: computeMatchKeys(candidate.scenario),
78
94
  };
79
95
  }
@@ -0,0 +1,10 @@
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 declare function singularize(word: string): string;
@@ -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,
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Report-language enforcement for `skyramp_submit_report` (SKYR-4185).
3
+ *
4
+ * The report language (SKYR-4023) reaches the MCP server only as a testbot
5
+ * prompt/resource arg, and the instruction to write free-text report fields in
6
+ * that language is prose the agent sometimes ignores — the customer-visible
7
+ * failure this module exists to close. The prompt render captures the language
8
+ * here (same server process serves the prompt and later handles the report
9
+ * tool), and the report tool rejects submissions whose free-text fields leaked
10
+ * back into English.
11
+ *
12
+ * Capture is fail-open by design: if the testbot prompt was never served
13
+ * (local/IDE flows, or a run broken enough that the agent has no instructions),
14
+ * the language stays unset and no report is ever falsely rejected.
15
+ */
16
+ /** Capture the report language at prompt-serve time. Last render wins:
17
+ * `undefined` clears any earlier capture, so a long-lived server (IDE use)
18
+ * that renders a ja prompt and later an English/argless one disarms the
19
+ * guardrail rather than falsely rejecting a legitimately-English report. */
20
+ export declare function setReportLanguage(language: string | undefined): void;
21
+ export declare function getReportLanguage(): string | undefined;
22
+ /** Test isolation only — module state persists across tests in one process. */
23
+ export declare function resetReportLanguage(): void;
24
+ export declare function isEnforcedReportLanguage(language: string | undefined): language is string;
25
+ /** English display name for a language code, falling back to the raw code when
26
+ * Intl rejects it as a structurally invalid tag (e.g. "ja_JP") — the code is
27
+ * ultimately caller-supplied, and a name lookup must never throw. */
28
+ export declare function reportLanguageDisplayName(language: string): string;
29
+ export interface ReportTextField {
30
+ /** Where the text came from, in the tool's input terms (e.g. "testResults[2].details"). */
31
+ path: string;
32
+ text: string;
33
+ }
34
+ /** Field paths whose text leaked English under an enforced report language.
35
+ * Empty for non-enforced languages — unknown languages are skipped, never guessed. */
36
+ export declare function findLanguageViolations(fields: ReportTextField[], language: string): string[];
37
+ /** Field paths in the deliberately-lenient band: some English prose, but under
38
+ * the rejection threshold (i.e. exactly one stray word at the current
39
+ * MIN_PROSE_WORDS of 2). These SHIP — the guardrail accepts them so a lone
40
+ * technical word ("flaky", "timeout") never blocks a report. Counted into the
41
+ * submit-report analytics event so the residual leak rate is measurable
42
+ * before anyone tightens the threshold. */
43
+ export declare function findLanguageNearMisses(fields: ReportTextField[], language: string): string[];
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Report-language enforcement for `skyramp_submit_report` (SKYR-4185).
3
+ *
4
+ * The report language (SKYR-4023) reaches the MCP server only as a testbot
5
+ * prompt/resource arg, and the instruction to write free-text report fields in
6
+ * that language is prose the agent sometimes ignores — the customer-visible
7
+ * failure this module exists to close. The prompt render captures the language
8
+ * here (same server process serves the prompt and later handles the report
9
+ * tool), and the report tool rejects submissions whose free-text fields leaked
10
+ * back into English.
11
+ *
12
+ * Capture is fail-open by design: if the testbot prompt was never served
13
+ * (local/IDE flows, or a run broken enough that the agent has no instructions),
14
+ * the language stays unset and no report is ever falsely rejected.
15
+ */
16
+ let sessionReportLanguage;
17
+ /** Capture the report language at prompt-serve time. Last render wins:
18
+ * `undefined` clears any earlier capture, so a long-lived server (IDE use)
19
+ * that renders a ja prompt and later an English/argless one disarms the
20
+ * guardrail rather than falsely rejecting a legitimately-English report. */
21
+ export function setReportLanguage(language) {
22
+ sessionReportLanguage = language;
23
+ }
24
+ export function getReportLanguage() {
25
+ return sessionReportLanguage;
26
+ }
27
+ /** Test isolation only — module state persists across tests in one process. */
28
+ export function resetReportLanguage() {
29
+ sessionReportLanguage = undefined;
30
+ }
31
+ // Languages the guardrail can enforce: detection is script-presence-based, so a
32
+ // language is enforceable only when its expected script is disjoint from Latin.
33
+ // English ('en') is deliberately absent — it is the default, not enforced.
34
+ // A Map, not a plain object: the key is caller-supplied, and object indexing
35
+ // would resolve prototype keys ("toString") to inherited members whose later
36
+ // .test() call crashes the tool.
37
+ const TARGET_SCRIPTS = new Map([
38
+ // Hiragana, Katakana (incl. halfwidth), CJK ideographs (incl. ext A / compat).
39
+ ["ja", /[぀-ヿ㐀-䶿一-鿿豈-﫿ヲ-゚]/],
40
+ ]);
41
+ export function isEnforcedReportLanguage(language) {
42
+ return language !== undefined && TARGET_SCRIPTS.has(language);
43
+ }
44
+ /** English display name for a language code, falling back to the raw code when
45
+ * Intl rejects it as a structurally invalid tag (e.g. "ja_JP") — the code is
46
+ * ultimately caller-supplied, and a name lookup must never throw. */
47
+ export function reportLanguageDisplayName(language) {
48
+ try {
49
+ return new Intl.DisplayNames(["en"], { type: "language" }).of(language) ?? language;
50
+ }
51
+ catch {
52
+ return language;
53
+ }
54
+ }
55
+ // Tokens that are legitimate untranslated content in any report language — the
56
+ // prompt's own "do NOT translate" list: enum values, HTTP verbs, test types.
57
+ const UNTRANSLATED_WORDS = new Set([
58
+ // Exact enum values only. NOT "passed"/"failed" — those are ordinary English
59
+ // prose and are exactly what leaks in pytest summaries ("1 passed in 0.88s",
60
+ // "failed: count was 13"; see letsramp/api-insight#215).
61
+ "pass", "fail", "skipped", "error", "unknown",
62
+ "critical", "high", "medium", "low",
63
+ "get", "post", "put", "patch", "delete", "head", "options",
64
+ "ui", "e2e", "api", "contract", "integration", "smoke", "fuzz", "load", "mock",
65
+ "http", "https", "json", "yaml", "ok",
66
+ ]);
67
+ /** A field must contain at least this many plain English prose words (after the
68
+ * stripping below) to be flagged — so identifier-and-timing-only details like
69
+ * "10.8s, products_contract_test.py" stay legal, and one stray word ("flaky")
70
+ * never rejects a report. Two is deliberate: the shortest real leak observed
71
+ * ("1 passed in 0.88s", api-insight#215) has exactly two prose words. */
72
+ const MIN_PROSE_WORDS = 2;
73
+ /** Count the plain English prose words in `text`, or return 0 when the target
74
+ * language's script is present (the field is compliant regardless of any
75
+ * English around it). Backticked spans, file names/paths, identifiers
76
+ * (snake_case/camelCase/ALL-CAPS), digit-bearing tokens, and the prompt's
77
+ * untranslated vocabulary are all excluded from the count. */
78
+ function countProseWords(text, script) {
79
+ if (script.test(text))
80
+ return 0;
81
+ const withoutCode = text.replace(/`[^`]*`/g, " ");
82
+ let proseWords = 0;
83
+ for (const raw of withoutCode.split(/\s+/)) {
84
+ const token = raw.replace(/^[^A-Za-z0-9]+/, "").replace(/[^A-Za-z0-9]+$/, "");
85
+ // Pure alphabetic words only: anything with digits or interior punctuation
86
+ // (paths, file names, snake_case, versions, "10.8s") is not prose.
87
+ if (token.length < 2 || !/^[A-Za-z]+$/.test(token))
88
+ continue;
89
+ if (/[a-z][A-Z]/.test(token))
90
+ continue; // camelCase identifier
91
+ if (token === token.toUpperCase())
92
+ continue; // acronym / enum shout (GET, FAILED)
93
+ if (UNTRANSLATED_WORDS.has(token.toLowerCase()))
94
+ continue;
95
+ proseWords++;
96
+ }
97
+ return proseWords;
98
+ }
99
+ /** Field paths whose text leaked English under an enforced report language.
100
+ * Empty for non-enforced languages — unknown languages are skipped, never guessed. */
101
+ export function findLanguageViolations(fields, language) {
102
+ const script = TARGET_SCRIPTS.get(language);
103
+ if (!script)
104
+ return [];
105
+ return fields
106
+ .filter((f) => countProseWords(f.text, script) >= MIN_PROSE_WORDS)
107
+ .map((f) => f.path);
108
+ }
109
+ /** Field paths in the deliberately-lenient band: some English prose, but under
110
+ * the rejection threshold (i.e. exactly one stray word at the current
111
+ * MIN_PROSE_WORDS of 2). These SHIP — the guardrail accepts them so a lone
112
+ * technical word ("flaky", "timeout") never blocks a report. Counted into the
113
+ * submit-report analytics event so the residual leak rate is measurable
114
+ * before anyone tightens the threshold. */
115
+ export function findLanguageNearMisses(fields, language) {
116
+ const script = TARGET_SCRIPTS.get(language);
117
+ if (!script)
118
+ return [];
119
+ return fields
120
+ .filter((f) => {
121
+ const words = countProseWords(f.text, script);
122
+ return words > 0 && words < MIN_PROSE_WORDS;
123
+ })
124
+ .map((f) => f.path);
125
+ }