@skyramp/mcp 0.3.6 → 0.3.8

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 (112) hide show
  1. package/build/prompts/code-reuse.js +17 -2
  2. package/build/prompts/enhance-assertions/sharedAssertionRules.js +1 -1
  3. package/build/prompts/modularization/ui-test-modularization.js +9 -6
  4. package/build/prompts/pom-aware-code-reuse.js +1 -1
  5. package/build/prompts/shared-helper-policy.js +5 -5
  6. package/build/prompts/test-recommendation/diffExecutionPlan.js +40 -9
  7. package/build/prompts/test-recommendation/fullRepoCatalog.js +3 -2
  8. package/build/prompts/test-recommendation/recommendationSections.js +3 -3
  9. package/build/prompts/test-recommendation/scopeAssessment.d.ts +2 -2
  10. package/build/prompts/test-recommendation/scopeAssessment.js +58 -76
  11. package/build/prompts/test-recommendation/test-recommendation-prompt.js +14 -1
  12. package/build/prompts/testbot/testbot-prompts.js +25 -8
  13. package/build/recommendation/budgeters/diversityBalancedBudgeter.js +39 -5
  14. package/build/recommendation/budgeters/shared.d.ts +21 -8
  15. package/build/recommendation/budgeters/shared.js +134 -56
  16. package/build/recommendation/planRanker.d.ts +5 -3
  17. package/build/recommendation/planRanker.js +1 -1
  18. package/build/services/TestGenerationService.js +15 -1
  19. package/build/tools/code-refactor/assertion-state.d.ts +91 -0
  20. package/build/tools/code-refactor/assertion-state.js +375 -0
  21. package/build/tools/code-refactor/codeReuseTool.js +6 -4
  22. package/build/tools/code-refactor/enhanceAssertionsTool.js +73 -18
  23. package/build/tools/code-refactor/retrofit-state.d.ts +53 -0
  24. package/build/tools/code-refactor/retrofit-state.js +162 -0
  25. package/build/tools/code-refactor/reuse-outcome.d.ts +7 -0
  26. package/build/tools/code-refactor/reuse-state.d.ts +9 -0
  27. package/build/tools/code-refactor/reuse-state.js +42 -4
  28. package/build/tools/code-refactor/utils-verify-gates.js +69 -15
  29. package/build/tools/executeSkyrampTestTool.js +19 -14
  30. package/build/tools/submitReportTool.js +87 -9
  31. package/build/tools/test-management/registerTestPlanTool.d.ts +3 -3
  32. package/build/tools/test-management/registerTestPlanTool.js +29 -8
  33. package/build/types/AssertionOutcome.d.ts +68 -0
  34. package/build/types/AssertionOutcome.js +1 -0
  35. package/build/types/RepositoryAnalysis.d.ts +10 -10
  36. package/build/types/ReuseOutcome.d.ts +16 -0
  37. package/build/types/TestRecommendation.d.ts +11 -1
  38. package/build/types/TestRecommendation.js +34 -0
  39. package/build/types/TestTypes.d.ts +4 -0
  40. package/build/types/TestTypes.js +8 -0
  41. package/build/types/TestbotReport.d.ts +13 -0
  42. package/build/types/index.d.ts +1 -1
  43. package/build/utils/AnalysisStateManager.d.ts +20 -7
  44. package/build/utils/assertion-verify/api-shared-lints.d.ts +5 -0
  45. package/build/utils/assertion-verify/api-shared-lints.js +315 -0
  46. package/build/utils/assertion-verify/contract-lints.d.ts +3 -0
  47. package/build/utils/assertion-verify/contract-lints.js +87 -0
  48. package/build/utils/assertion-verify/format.d.ts +5 -0
  49. package/build/utils/assertion-verify/format.js +65 -0
  50. package/build/utils/assertion-verify/helper-imports.d.ts +6 -0
  51. package/build/utils/assertion-verify/helper-imports.js +178 -0
  52. package/build/utils/assertion-verify/index.d.ts +3 -0
  53. package/build/utils/assertion-verify/index.js +7 -0
  54. package/build/utils/assertion-verify/integration-lints.d.ts +3 -0
  55. package/build/utils/assertion-verify/integration-lints.js +36 -0
  56. package/build/utils/assertion-verify/js-regex-blank.d.ts +1 -0
  57. package/build/utils/assertion-verify/js-regex-blank.js +153 -0
  58. package/build/utils/assertion-verify/lint-types.d.ts +33 -0
  59. package/build/utils/assertion-verify/lint-types.js +57 -0
  60. package/build/utils/assertion-verify/marker.d.ts +27 -0
  61. package/build/utils/assertion-verify/marker.js +61 -0
  62. package/build/utils/assertion-verify/metrics.d.ts +30 -0
  63. package/build/utils/assertion-verify/metrics.js +341 -0
  64. package/build/utils/assertion-verify/python-strip.d.ts +6 -0
  65. package/build/utils/assertion-verify/python-strip.js +75 -0
  66. package/build/utils/assertion-verify/strip-dispatch.d.ts +19 -0
  67. package/build/utils/assertion-verify/strip-dispatch.js +42 -0
  68. package/build/utils/assertion-verify/ui-lints.d.ts +8 -0
  69. package/build/utils/assertion-verify/ui-lints.js +244 -0
  70. package/build/utils/assertion-verify/verify.d.ts +61 -0
  71. package/build/utils/assertion-verify/verify.js +215 -0
  72. package/build/utils/featureFlags.d.ts +12 -2
  73. package/build/utils/featureFlags.js +33 -3
  74. package/build/utils/pathSignatures.d.ts +4 -1
  75. package/build/utils/pathSignatures.js +14 -2
  76. package/build/utils/planOnlyMode.d.ts +33 -0
  77. package/build/utils/planOnlyMode.js +40 -0
  78. package/build/utils/reportVerification.d.ts +9 -0
  79. package/build/utils/reportVerification.js +39 -6
  80. package/build/utils/scenarioDrafting.d.ts +1 -1
  81. package/build/utils/scenarioDrafting.js +1 -1
  82. package/build/utils/utils-verify/allow.d.ts +22 -4
  83. package/build/utils/utils-verify/allow.js +8 -2
  84. package/build/utils/utils-verify/call-sites.d.ts +40 -1
  85. package/build/utils/utils-verify/call-sites.js +196 -30
  86. package/build/utils/utils-verify/importers.d.ts +31 -0
  87. package/build/utils/utils-verify/importers.js +78 -0
  88. package/build/utils/utils-verify/index.d.ts +1 -0
  89. package/build/utils/utils-verify/index.js +1 -0
  90. package/build/utils/utils-verify/language-spec.d.ts +13 -2
  91. package/build/utils/utils-verify/language-spec.js +12 -2
  92. package/build/utils/utils-verify/parse.d.ts +31 -3
  93. package/build/utils/utils-verify/parse.js +190 -9
  94. package/build/utils/utils-verify/retrofit-equivalence.d.ts +43 -0
  95. package/build/utils/utils-verify/retrofit-equivalence.js +218 -0
  96. package/build/utils/utils-verify/stage.d.ts +6 -0
  97. package/build/utils/utils-verify/stage.js +12 -2
  98. package/build/utils/utils-verify/verify.d.ts +54 -4
  99. package/build/utils/utils-verify/verify.js +224 -12
  100. package/node_modules/playwright/node_modules/playwright-core/lib/generated/injectedScriptSource.js +1 -1
  101. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-CZfp96qZ.js → codeMirrorModule-LNgEKtdV.js} +1 -1
  102. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{defaultSettingsView-gpLo02E0.js → defaultSettingsView-Bwr1eMKC.js} +135 -135
  103. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.Bq1r1URj.js → index.-Id052Lr.js} +1 -1
  104. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +2 -2
  105. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{uiMode.VEfqi1qN.js → uiMode.BPopbasy.js} +1 -1
  106. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +2 -2
  107. package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
  108. package/node_modules/playwright/node_modules/playwright-core/src/generated/injectedScriptSource.ts +1 -1
  109. package/node_modules/playwright/package.json +1 -1
  110. package/package.json +2 -2
  111. package/build/tools/code-refactor/enhance-state.d.ts +0 -49
  112. package/build/tools/code-refactor/enhance-state.js +0 -109
@@ -0,0 +1,78 @@
1
+ import * as path from "path";
2
+ import { generatedSiblings } from "./call-sites.js";
3
+ /** The strings a sibling's source would contain if it imports the module at
4
+ * `modulePath` (relative to the sibling's directory): the path form for TS/JS
5
+ * specifiers, and the dotted Python form — `../shared/X` → `..shared.X`,
6
+ * `shared/X` → `shared.X` / `.shared.X`. */
7
+ export function moduleMentionCandidates(modulePath) {
8
+ const posix = modulePath.split(path.sep).join("/");
9
+ const m = /^((?:\.\.\/)*)(.*)$/.exec(posix);
10
+ const ups = (m?.[1] ?? "").split("/").filter(Boolean).length;
11
+ const rest = (m?.[2] ?? posix).split("/").filter(Boolean).join(".");
12
+ const dotted = ".".repeat(ups + 1) + rest; // one leading dot = current package
13
+ return [posix, "./" + posix, rest, dotted, "." + rest];
14
+ }
15
+ /**
16
+ * Count, per utils helper, how many Skyramp-generated tests in the spec's directory
17
+ * import it — the spec under verification included.
18
+ *
19
+ * Findings:
20
+ * - 0 importers: nothing imports the helper, not even the spec that wrote it — a
21
+ * dead helper, actionable on any run.
22
+ * - 1 importer: not (yet) shared. Reported only when the directory holds at least
23
+ * TWO delivered tests: on a first run every helper has exactly one importer by
24
+ * construction (it seeds the utils file), so listing them would be guaranteed
25
+ * noise on the most common path.
26
+ * Fails open to "no findings" on any read error.
27
+ */
28
+ export async function singleImporterHelpers(testFile, utilsFiles, helpersByFile, spec, preReadSiblings) {
29
+ let dir;
30
+ let siblings;
31
+ try {
32
+ dir = path.dirname(testFile);
33
+ siblings =
34
+ preReadSiblings ??
35
+ (await generatedSiblings(testFile, spec, { includeSelf: true }));
36
+ }
37
+ catch {
38
+ return [];
39
+ }
40
+ // `importedNames` matches by basename stem, so a bare `from SkyrampUtils import …`
41
+ // in a sibling would also be credited to `../shared/SkyrampUtils`. A sibling's
42
+ // bare or `./` import resolves to the module BESIDE it, so an out-of-directory
43
+ // utils file is credited only when the sibling's content names its module path;
44
+ // a miss under-counts, which keeps the helper IN the advisory rather than
45
+ // hiding it.
46
+ const stems = utilsFiles.map((f) => ({
47
+ file: f,
48
+ stem: path.basename(f).replace(/\.[^.]+$/, ""),
49
+ inDir: path.dirname(f) === dir,
50
+ mentions: moduleMentionCandidates(path.relative(dir, f).replace(/\.[^.]+$/, "")),
51
+ }));
52
+ // helper key = `${utilsFile}\0${name}` → number of importing tests
53
+ const counts = new Map();
54
+ for (const [file, helpers] of helpersByFile)
55
+ for (const h of helpers)
56
+ counts.set(`${file}\0${h.name}`, 0);
57
+ for (const { content } of siblings) {
58
+ for (const { file: utilsFile, stem, inDir, mentions } of stems) {
59
+ if (!inDir && !mentions.some((c) => content.includes(c)))
60
+ continue;
61
+ const imported = new Set(spec.importedNames(content, stem));
62
+ for (const h of helpersByFile.get(utilsFile) ?? [])
63
+ if (imported.has(h.name)) {
64
+ const k = `${utilsFile}\0${h.name}`;
65
+ counts.set(k, (counts.get(k) ?? 0) + 1);
66
+ }
67
+ }
68
+ }
69
+ const secondRunExists = siblings.length >= 2;
70
+ const out = [];
71
+ for (const [file, helpers] of helpersByFile)
72
+ for (const h of helpers) {
73
+ const n = counts.get(`${file}\0${h.name}`) ?? 0;
74
+ if (n === 0 || (n === 1 && secondRunExists))
75
+ out.push({ file, helper: h.name, line: h.line, importers: n });
76
+ }
77
+ return out;
78
+ }
@@ -5,3 +5,4 @@ export * from "./locate.js";
5
5
  export * from "./verify.js";
6
6
  export * from "./stage.js";
7
7
  export * from "./call-sites.js";
8
+ export * from "./retrofit-equivalence.js";
@@ -5,3 +5,4 @@ export * from "./locate.js";
5
5
  export * from "./verify.js";
6
6
  export * from "./stage.js";
7
7
  export * from "./call-sites.js";
8
+ export * from "./retrofit-equivalence.js";
@@ -54,12 +54,13 @@ export interface HelperFamilySpec {
54
54
  family: HelperFamily;
55
55
  /** Whether two helpers wrapping the same method+path are a violation. */
56
56
  routeDuplicates: boolean;
57
- /** Picks the permitted-assertion pattern out of a language spec. */
58
- allowedAssertion(spec: UtilsLanguageSpec): RegExp;
59
57
  /** Whether one complete assertion expression is permitted for this family. */
60
58
  isAllowed(spec: UtilsLanguageSpec, expr: string): boolean;
61
59
  /** One clause for the failure text: what a helper of this family may assert. */
62
60
  assertionRule: string;
61
+ /** One clause for the fix-instruction sentence — composed by the gate, never
62
+ * restated there (the browser clause reads as a continuation of the api one). */
63
+ fixRule: string;
63
64
  }
64
65
  export declare const HELPER_FAMILIES: Record<HelperFamily, HelperFamilySpec>;
65
66
  /** The helper family a test type's shared helpers follow. Unlike `isBrowserTestType`,
@@ -67,6 +68,16 @@ export declare const HELPER_FAMILIES: Record<HelperFamily, HelperFamilySpec>;
67
68
  * exists, and the browser rules are the more permissive of the two on duplicates, so
68
69
  * guessing "browser" would silently switch a check off. */
69
70
  export declare function helperFamilyFor(testType: string | undefined): HelperFamilySpec;
71
+ /** The family ONE helper is judged by. A helper that issues an SDK request follows
72
+ * the api rules whatever the test type: UI flows routinely keep a sign-in or seed
73
+ * request beside their browser helpers, and judging that request by the browser
74
+ * rules made its status assertion a false violation. Keyed on the request itself,
75
+ * not the duplicate key — a two-request sign-in, a concatenated path, and a
76
+ * parameterised route are all API helpers with no keyable route. A helper with no
77
+ * request falls back to the test type's family. */
78
+ export declare function helperFamilyOf(helper: {
79
+ wrapsRequest?: boolean;
80
+ }, fallback: HelperFamilySpec): HelperFamilySpec;
70
81
  /** The spec for a language string as the tools receive it, or `undefined` for a
71
82
  * language this verifier does not cover (Java: utils reuse is disabled for it). */
72
83
  export declare function utilsSpecFor(language: string | undefined): UtilsLanguageSpec | undefined;
@@ -125,20 +125,20 @@ export const HELPER_FAMILIES = {
125
125
  api: {
126
126
  family: "api",
127
127
  routeDuplicates: true,
128
- allowedAssertion: (spec) => spec.allowedAssertion,
129
128
  isAllowed: (spec, expr) => spec.allowedAssertion.test(expr),
130
129
  assertionRule: "a shared API helper asserts only the status code; move this assertion to the test body, applied to the response the helper returns",
130
+ fixRule: "a helper that wraps an SDK request asserts only the status code — one helper per method+path, extended with defaulted parameters, named method + resource",
131
131
  },
132
132
  browser: {
133
133
  family: "browser",
134
134
  routeDuplicates: false,
135
- allowedAssertion: (spec) => spec.allowedBrowserAssertion,
136
135
  // A structural wait on a locator that carries a scenario LITERAL
137
136
  // (`getByText("Total: $899.98")`) is a value assertion in disguise: digits or
138
137
  // currency in any string inside the expect(...) arguments disqualify it.
139
138
  isAllowed: (spec, expr) => spec.allowedBrowserAssertion.test(expr) &&
140
139
  !SCENARIO_LITERAL_RE.test(expectArgs(expr)),
141
140
  assertionRule: "a shared browser helper holds actions and structural waits only (toBeVisible, toBeEnabled, …); move this value assertion to the test body immediately after the helper call, selector verbatim",
141
+ fixRule: "every other helper holds actions and structural waits only, named for the user intent",
142
142
  },
143
143
  };
144
144
  /** The helper family a test type's shared helpers follow. Unlike `isBrowserTestType`,
@@ -149,6 +149,16 @@ export function helperFamilyFor(testType) {
149
149
  const t = (testType ?? "").toLowerCase();
150
150
  return HELPER_FAMILIES[t === "ui" || t === "e2e" ? "browser" : "api"];
151
151
  }
152
+ /** The family ONE helper is judged by. A helper that issues an SDK request follows
153
+ * the api rules whatever the test type: UI flows routinely keep a sign-in or seed
154
+ * request beside their browser helpers, and judging that request by the browser
155
+ * rules made its status assertion a false violation. Keyed on the request itself,
156
+ * not the duplicate key — a two-request sign-in, a concatenated path, and a
157
+ * parameterised route are all API helpers with no keyable route. A helper with no
158
+ * request falls back to the test type's family. */
159
+ export function helperFamilyOf(helper, fallback) {
160
+ return helper.wrapsRequest ? HELPER_FAMILIES.api : fallback;
161
+ }
152
162
  /** The spec for a language string as the tools receive it, or `undefined` for a
153
163
  * language this verifier does not cover (Java: utils reuse is disabled for it). */
154
164
  export function utilsSpecFor(language) {
@@ -2,8 +2,6 @@ import { type UtilsLanguageSpec } from "./language-spec.js";
2
2
  export interface HelperAssertion {
3
3
  line: number;
4
4
  text: string;
5
- /** True for the one permitted shape — a status-code comparison. */
6
- allowed: boolean;
7
5
  }
8
6
  export interface UtilsHelper {
9
7
  name: string;
@@ -15,8 +13,23 @@ export interface UtilsHelper {
15
13
  path?: string;
16
14
  /** `path` with every path parameter collapsed to `{}`; the duplicate key with `method`. */
17
15
  normalizedPath?: string;
16
+ /** The helper issues at least one SDK request — what makes it an API helper,
17
+ * whatever the test type, and whether or not it has a keyable single route
18
+ * (a two-request sign-in, a concatenated path, and a parameterised route all
19
+ * wrap requests without one). */
20
+ wrapsRequest: boolean;
21
+ /** The body holds a live `page.on('pageerror', …)` registration. A guard moved
22
+ * into a shared helper is still delivered — see hasPageErrorGuard. */
23
+ hasGuard: boolean;
18
24
  assertions: HelperAssertion[];
25
+ /** TypeScript only: signature parameters with no type annotation (`page`, not
26
+ * `page: Page`). Empty for other languages. */
27
+ untypedParams: string[];
19
28
  }
29
+ /** The parameter names in a TS signature that carry no `: type`. Reads the signature
30
+ * from the definition line to its `{` (Prettier wraps long signatures), takes the
31
+ * first balanced `(...)`, and splits it on top-level commas. */
32
+ export declare function untypedParamsOf(sigLines: string[]): string[];
20
33
  /**
21
34
  * Structured extraction of the module-level helpers in a utils file.
22
35
  *
@@ -27,8 +40,23 @@ export interface UtilsHelper {
27
40
  * next non-blank column-0 line; TS/JS bodies end when the brace depth opened by
28
41
  * the definition returns to zero.
29
42
  */
30
- export declare function parseUtilsFile(content: string, spec: UtilsLanguageSpec, allowedAssertion?: RegExp | ((expr: string) => boolean)): UtilsHelper[];
43
+ export declare function parseUtilsFile(content: string, spec: UtilsLanguageSpec): UtilsHelper[];
31
44
  /** Comments and string CONTENTS blanked (quotes kept, line count preserved). Python
32
45
  * comments are `#…`; TS/JS go through the shared stripper. */
33
46
  /** String CONTENTS blanked (quotes kept, comments kept, line count preserved). */
34
47
  export declare function blankStrings(src: string, spec: UtilsLanguageSpec): string;
48
+ /** Assertions in a source file: lines (Python) / occurrences (TS) matching the
49
+ * language's assertion pattern, with comments and string contents blanked so a
50
+ * commented-out or quoted `expect(` never counts. */
51
+ export declare function countAssertions(content: string, spec: UtilsLanguageSpec): number;
52
+ /**
53
+ * A live `page.on('pageerror', …)` registration — the guard the enhancer adds and
54
+ * modularization must not drop. Exactly `pageerror`: a `console` listener is not an
55
+ * error collector, and accepting it would let an unrelated listener mask the removal.
56
+ * The call is located on a copy with comments AND string contents blanked (so a
57
+ * commented-out or quoted example never satisfies the check) and its argument is read
58
+ * from the comment-blanked copy, where the `'pageerror'` literal is still legible.
59
+ * Both copies preserve offsets, so a Prettier-wrapped registration matches too.
60
+ */
61
+ export declare function hasPageErrorGuard(content: string, spec: UtilsLanguageSpec): boolean;
62
+ export declare function blankCommentsAndStrings(src: string, spec: UtilsLanguageSpec): string;
@@ -1,5 +1,96 @@
1
1
  import { normalizePath } from "./language-spec.js";
2
- import { stripCommentsAndStrings, stripStrings } from "../pom-scope/strip.js";
2
+ import { stripComments, stripCommentsAndStrings, stripStrings, } from "../pom-scope/strip.js";
3
+ /** The parameter names in a TS signature that carry no `: type`. Reads the signature
4
+ * from the definition line to its `{` (Prettier wraps long signatures), takes the
5
+ * first balanced `(...)`, and splits it on top-level commas. */
6
+ export function untypedParamsOf(sigLines) {
7
+ const sig = sigLines.join(" ");
8
+ const open = sig.indexOf("(");
9
+ if (open === -1) {
10
+ // Bare-arrow form: `export const open = async page => …` — one parameter,
11
+ // untyped by construction (a typed single parameter requires parentheses).
12
+ const bare = /(?:^|=)\s*(?:async\s+)?([A-Za-z_$][\w$]*)\s*=>/.exec(sig);
13
+ return bare ? [bare[1]] : [];
14
+ }
15
+ // `>` closes a nesting level only when it is not the arrow token's tail:
16
+ // `(callback: (x: string) => void, page)` must not close at `=>`.
17
+ const closes = (ch, prev) => ch === ")" || ch === "]" || ch === "}" || (ch === ">" && prev !== "=");
18
+ let depth = 0;
19
+ let close = -1;
20
+ for (let i = open; i < sig.length; i++) {
21
+ const ch = sig[i];
22
+ if ("([{<".includes(ch))
23
+ depth++;
24
+ else if (closes(ch, sig[i - 1] ?? "")) {
25
+ depth--;
26
+ if (depth === 0) {
27
+ close = i;
28
+ break;
29
+ }
30
+ }
31
+ }
32
+ if (close === -1)
33
+ return [];
34
+ const params = [];
35
+ let cur = "";
36
+ depth = 0;
37
+ for (let i = open + 1; i < close; i++) {
38
+ const ch = sig[i];
39
+ if ("([{<".includes(ch))
40
+ depth++;
41
+ else if (closes(ch, sig[i - 1] ?? ""))
42
+ depth--;
43
+ if (ch === "," && depth === 0) {
44
+ params.push(cur);
45
+ cur = "";
46
+ }
47
+ else
48
+ cur += ch;
49
+ }
50
+ params.push(cur);
51
+ const out = [];
52
+ for (const raw of params) {
53
+ const param = raw.trim().replace(/^\.\.\./, "");
54
+ if (!param)
55
+ continue;
56
+ // A default value means TypeScript infers the type — not a finding, even
57
+ // unannotated (`expectedStatus = 200`). `=>` inside a function-type
58
+ // annotation is not a default.
59
+ if (/=(?!>)/.test(param.replace(/\([^)]*\)/g, "")))
60
+ continue;
61
+ // A binding pattern's inner colons are renames, not annotations:
62
+ // `{ page: browserPage }` is implicitly any unless a `:` follows the
63
+ // complete pattern.
64
+ if (param.startsWith("{") || param.startsWith("[")) {
65
+ let d = 0;
66
+ let after = -1;
67
+ for (let i = 0; i < param.length; i++) {
68
+ if ("([{<".includes(param[i]))
69
+ d++;
70
+ else if (closes(param[i], param[i - 1] ?? "")) {
71
+ d--;
72
+ if (d === 0) {
73
+ after = i + 1;
74
+ break;
75
+ }
76
+ }
77
+ }
78
+ if (after === -1 || !param.slice(after).trimStart().startsWith(":"))
79
+ out.push(param.replace(/\s+/g, " "));
80
+ continue;
81
+ }
82
+ if (param.includes(":")) {
83
+ // `page: any` compiles clean under noImplicitAny — the customer's compiler
84
+ // will NEVER report it, which makes it the one case this advisory can catch
85
+ // that tsc cannot. It is also the exact legacy convention SKYR-3324 wrote.
86
+ if (/:\s*any\s*$/.test(param))
87
+ out.push(param.split(":")[0].trim().replace(/\?$/, "") + ": any");
88
+ continue;
89
+ }
90
+ out.push(param.replace(/\?$/, ""));
91
+ }
92
+ return out;
93
+ }
3
94
  /**
4
95
  * Structured extraction of the module-level helpers in a utils file.
5
96
  *
@@ -10,10 +101,7 @@ import { stripCommentsAndStrings, stripStrings } from "../pom-scope/strip.js";
10
101
  * next non-blank column-0 line; TS/JS bodies end when the brace depth opened by
11
102
  * the definition returns to zero.
12
103
  */
13
- export function parseUtilsFile(content, spec, allowedAssertion = spec.allowedAssertion) {
14
- const isAllowed = typeof allowedAssertion === "function"
15
- ? allowedAssertion
16
- : (expr) => allowedAssertion.test(expr);
104
+ export function parseUtilsFile(content, spec) {
17
105
  const lines = content.split("\n");
18
106
  const helpers = [];
19
107
  let i = 0;
@@ -37,7 +125,14 @@ export function parseUtilsFile(content, spec, allowedAssertion = spec.allowedAss
37
125
  name,
38
126
  line: start + 1,
39
127
  endLine: Math.max(end, start + 1),
128
+ wrapsRequest: false,
129
+ hasGuard: hasPageErrorGuard(body.join("\n"), spec),
40
130
  assertions: [],
131
+ untypedParams: spec.language === "typescript"
132
+ ? // The blanked copy: a comment inside a wrapped signature must not
133
+ // reach the agent as a "parameter".
134
+ untypedParamsOf(signatureLines(scan))
135
+ : [],
41
136
  };
42
137
  let requests = 0;
43
138
  for (let k = 0; k < body.length; k++) {
@@ -77,13 +172,13 @@ export function parseUtilsFile(content, spec, allowedAssertion = spec.allowedAss
77
172
  helper.assertions.push({
78
173
  line: start + k + 1,
79
174
  text: original,
80
- allowed: isAllowed(original),
81
175
  });
82
176
  k = last;
83
177
  }
84
178
  }
85
179
  // A helper wrapping two requests is a sequence, not "one SDK call": it has no single
86
180
  // route to be a duplicate of, and the shared-helper rules do not apply to it.
181
+ helper.wrapsRequest = requests > 0;
87
182
  if (requests > 1) {
88
183
  helper.method = undefined;
89
184
  helper.path = undefined;
@@ -122,16 +217,88 @@ export function blankStrings(src, spec) {
122
217
  .map((line) => line.replace(/(["'])(?:\\.|(?!\1).)*\1/g, (q) => q[0] + " ".repeat(Math.max(0, q.length - 2)) + q[0]))
123
218
  .join("\n");
124
219
  }
125
- function blankCommentsAndStrings(src, spec) {
220
+ /** Assertions in a source file: lines (Python) / occurrences (TS) matching the
221
+ * language's assertion pattern, with comments and string contents blanked so a
222
+ * commented-out or quoted `expect(` never counts. */
223
+ export function countAssertions(content, spec) {
224
+ const scan = blankCommentsAndStrings(content, spec);
225
+ if (spec.language === "python")
226
+ return scan.split("\n").filter((l) => spec.assertionRe.test(l)).length;
227
+ return (scan.match(new RegExp(spec.assertionRe.source, "g")) ?? []).length;
228
+ }
229
+ /**
230
+ * A live `page.on('pageerror', …)` registration — the guard the enhancer adds and
231
+ * modularization must not drop. Exactly `pageerror`: a `console` listener is not an
232
+ * error collector, and accepting it would let an unrelated listener mask the removal.
233
+ * The call is located on a copy with comments AND string contents blanked (so a
234
+ * commented-out or quoted example never satisfies the check) and its argument is read
235
+ * from the comment-blanked copy, where the `'pageerror'` literal is still legible.
236
+ * Both copies preserve offsets, so a Prettier-wrapped registration matches too.
237
+ */
238
+ export function hasPageErrorGuard(content, spec) {
239
+ const noComments = blankComments(content, spec);
240
+ const code = blankStrings(noComments, spec);
241
+ for (const m of code.matchAll(/\bpage\.on\(/g)) {
242
+ const at = (m.index ?? 0) + m[0].length;
243
+ if (/^\s*["']pageerror["']/.test(noComments.slice(at)))
244
+ return true;
245
+ }
246
+ return false;
247
+ }
248
+ /** Comments blanked, strings preserved, offsets kept. TS/JS use the shared stripper;
249
+ * Python blanks each line from its first `#` outside a string. */
250
+ function blankComments(src, spec) {
251
+ if (spec.language !== "python")
252
+ return stripComments(src);
253
+ const blanked = blankStrings(src, spec).split("\n");
254
+ return src
255
+ .split("\n")
256
+ .map((line, i) => {
257
+ const at = blanked[i].indexOf("#");
258
+ return at === -1
259
+ ? line
260
+ : line.slice(0, at) + " ".repeat(line.length - at);
261
+ })
262
+ .join("\n");
263
+ }
264
+ export function blankCommentsAndStrings(src, spec) {
126
265
  if (spec.language !== "python")
127
266
  return stripCommentsAndStrings(src);
128
267
  return src
129
268
  .split("\n")
130
269
  .map((line) => line
131
270
  .replace(/(["'])(?:\\.|(?!\1).)*\1/g, (q) => q[0] + " ".repeat(Math.max(0, q.length - 2)) + q[0])
132
- .replace(/#.*$/, ""))
271
+ // Blanked, not deleted: callers scan this copy for OFFSETS and read the text
272
+ // back out of the original, so every line must keep its length.
273
+ .replace(/#.*$/, (c) => " ".repeat(c.length)))
133
274
  .join("\n");
134
275
  }
276
+ function signatureLines(body) {
277
+ // The signature runs until its parameter parentheses close: a `{` inside a
278
+ // wrapped object-type parameter (`opts: { a: string },`) is not the body
279
+ // opener, so a bare `/[{]|=>/` test truncated such signatures.
280
+ const out = [];
281
+ let depth = 0;
282
+ let opened = false;
283
+ for (const l of body) {
284
+ out.push(l);
285
+ for (let i = 0; i < l.length; i++) {
286
+ if (l[i] === "(") {
287
+ depth++;
288
+ opened = true;
289
+ }
290
+ else if (l[i] === ")")
291
+ depth--;
292
+ }
293
+ if (opened && depth <= 0)
294
+ break;
295
+ if (!opened && /=>/.test(l))
296
+ break; // bare-arrow form has no parentheses
297
+ if (out.length > 12)
298
+ break;
299
+ }
300
+ return out;
301
+ }
135
302
  /** Exclusive end index of a Python def starting at `start`: the next non-blank line at
136
303
  * column 0 (a following def, import, or module statement). */
137
304
  function pythonBodyEnd(lines, start) {
@@ -147,12 +314,26 @@ function pythonBodyEnd(lines, start) {
147
314
  /** Exclusive end index of a brace-delimited definition starting at `start`. Strings and
148
315
  * comments are not tracked — helper bodies contain request paths, not braces in
149
316
  * strings — and an unbalanced body runs to end of file rather than throwing. */
317
+ /** The definition line through the one that opens the body (`{` or `=>`). */
150
318
  function braceBodyEnd(lines, start) {
151
319
  let depth = 0;
152
320
  let opened = false;
321
+ // A `{` inside the signature's still-open parentheses is an object-type
322
+ // parameter (`opts: { a: string },` wrapped by Prettier), not the body opener.
323
+ let parens = 0;
153
324
  for (let i = start; i < lines.length; i++) {
154
325
  for (const ch of lines[i]) {
155
- if (ch === "{") {
326
+ // Parens matter only until the body opens: they exist to get past a wrapped
327
+ // signature. Tracking them afterwards let one unbalanced `(` in a string or
328
+ // comment blind the brace count for the rest of the file — swallowing every
329
+ // later helper and misattributing its violations (the gate then never clears).
330
+ if (!opened && ch === "(")
331
+ parens++;
332
+ else if (!opened && ch === ")")
333
+ parens = Math.max(0, parens - 1);
334
+ else if (!opened && parens > 0)
335
+ continue;
336
+ else if (ch === "{") {
156
337
  depth++;
157
338
  opened = true;
158
339
  }
@@ -0,0 +1,43 @@
1
+ import { type UtilsLanguageSpec } from "./language-spec.js";
2
+ export interface RetrofitMismatch {
3
+ /** Absolute path of the rewired pre-existing test. */
4
+ file: string;
5
+ /** The shared helper the file now calls in place of its own request block. */
6
+ helper: string;
7
+ method: string;
8
+ path: string;
9
+ /** What the substitution changed about the request. */
10
+ detail: string;
11
+ }
12
+ /**
13
+ * Whether a retrofit changed the request the rewired test sends.
14
+ *
15
+ * STEP 5c edits a PRE-EXISTING test — code the customer already owns and whose
16
+ * behaviour nothing in this run chose — to call a shared helper instead of its own
17
+ * inline request block. That is only safe if the helper sends the same request. It
18
+ * usually does, because the helper was extracted from an identical block; but nothing
19
+ * checked it, and a helper that hardcodes a field the block never sent (or omits one
20
+ * it did) silently changes what the customer's test exercises. The failure is quiet:
21
+ * the test still runs, and a red result reads like any other red.
22
+ *
23
+ * The advisory path already refuses to OFFER such a site (see `substitutable` in
24
+ * call-sites.ts). This is the same question asked after the fact, about the edit that
25
+ * was actually made, and the rule is stricter: offering tolerates a site with an extra
26
+ * field, because extending the helper with a defaulted parameter is the sanctioned
27
+ * move; a completed retrofit must leave the request byte-equivalent in shape, so the
28
+ * field sets must match exactly in both directions.
29
+ *
30
+ * LIMIT: this compares the request's SHAPE — which fields and options it carries —
31
+ * not the values passed. A baseline sending `status: "draft"` rewired to
32
+ * `createPost(..., "published")` has the same field set and is not reported. Value
33
+ * equivalence needs argument-to-parameter binding, default resolution and expression
34
+ * normalisation; a wrong answer here REFUSES the report, so it is deliberately left
35
+ * out rather than approximated. The value case is also visible in the rewired file's
36
+ * own diff, where a hardcoded helper field is not.
37
+ *
38
+ * Compares against the file's committed baseline (`git show HEAD:<path>`), which is
39
+ * the only record of what the block sent before the edit. Fails OPEN — an untracked
40
+ * file, a detached worktree, or an unreadable baseline yields no mismatches rather
41
+ * than a refusal the agent cannot act on.
42
+ */
43
+ export declare function retrofitMismatches(retrofitFile: string, utilsFiles: string[], spec: UtilsLanguageSpec): Promise<RetrofitMismatch[]>;