@skyramp/mcp 0.4.0 → 0.4.1-rc.2

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 (83) hide show
  1. package/build/prompts/enhance-assertions/contractProviderAssertionsPrompt.js +2 -1
  2. package/build/prompts/enhance-assertions/integrationAssertionsPrompt.js +2 -1
  3. package/build/prompts/enhance-assertions/sharedAssertionRules.d.ts +1 -1
  4. package/build/prompts/enhance-assertions/sharedAssertionRules.js +57 -22
  5. package/build/prompts/enhance-assertions/uiAssertionsPrompt.js +17 -9
  6. package/build/prompts/test-recommendation/diffExecutionPlan.js +0 -2
  7. package/build/prompts/test-recommendation/test-recommendation-prompt.js +11 -6
  8. package/build/prompts/testbot/testbot-prompts.js +19 -12
  9. package/build/recommendation/answers.d.ts +11 -7
  10. package/build/recommendation/answers.js +14 -10
  11. package/build/recommendation/pullRequestText.d.ts +18 -0
  12. package/build/recommendation/pullRequestText.js +31 -0
  13. package/build/recommendation/registerPlan.d.ts +9 -1
  14. package/build/recommendation/registerPlan.js +8 -1
  15. package/build/recommendation/runVerifiers.js +6 -0
  16. package/build/recommendation/types.d.ts +58 -0
  17. package/build/recommendation/verifierContracts.d.ts +108 -13
  18. package/build/recommendation/verifierContracts.js +147 -29
  19. package/build/recommendation/verifiers/coverage.d.ts +10 -0
  20. package/build/recommendation/verifiers/coverage.js +144 -22
  21. package/build/recommendation/verifiers/defects.d.ts +9 -0
  22. package/build/recommendation/verifiers/defects.js +117 -0
  23. package/build/recommendation/verifiers/deliveredMatchesPlan.d.ts +22 -0
  24. package/build/recommendation/verifiers/deliveredMatchesPlan.js +43 -0
  25. package/build/recommendation/verifiers/existingCoverage.js +53 -0
  26. package/build/recommendation/verifiers/expectedValueSourced.d.ts +14 -0
  27. package/build/recommendation/verifiers/expectedValueSourced.js +246 -0
  28. package/build/recommendation/verifiers/issueTraceability.d.ts +52 -0
  29. package/build/recommendation/verifiers/issueTraceability.js +197 -0
  30. package/build/recommendation/verifiers/requirementSourced.d.ts +2 -0
  31. package/build/recommendation/verifiers/requirementSourced.js +168 -0
  32. package/build/services/TestGenerationService.js +3 -1
  33. package/build/tools/code-refactor/codeReuseTool.js +1 -1
  34. package/build/tools/code-refactor/reuse-outcome.d.ts +1 -1
  35. package/build/tools/code-refactor/reuse-state.d.ts +85 -7
  36. package/build/tools/code-refactor/reuse-state.js +239 -34
  37. package/build/tools/code-refactor/utils-verify-gates.d.ts +5 -0
  38. package/build/tools/code-refactor/utils-verify-gates.js +103 -11
  39. package/build/tools/generate-tests/generateBatchScenarioRestTool.js +2 -1
  40. package/build/tools/submitReportTool.js +276 -39
  41. package/build/tools/test-management/actionsTool.js +5 -0
  42. package/build/tools/test-management/analyzeChangesTool.d.ts +53 -0
  43. package/build/tools/test-management/analyzeChangesTool.js +55 -2
  44. package/build/tools/test-management/registerTestPlanTool.d.ts +32 -17
  45. package/build/tools/test-management/registerTestPlanTool.js +105 -16
  46. package/build/types/ReuseOutcome.d.ts +73 -7
  47. package/build/types/TestAnalysis.d.ts +6 -0
  48. package/build/types/TestbotReport.d.ts +22 -4
  49. package/build/utils/AnalysisStateManager.d.ts +7 -1
  50. package/build/utils/AnalysisStateManager.js +5 -1
  51. package/build/utils/assertion-verify/api-shared-lints.js +118 -11
  52. package/build/utils/assertion-verify/format.js +2 -2
  53. package/build/utils/assertion-verify/helper-imports.d.ts +7 -0
  54. package/build/utils/assertion-verify/helper-imports.js +119 -27
  55. package/build/utils/assertion-verify/lint-types.d.ts +31 -2
  56. package/build/utils/assertion-verify/lint-types.js +66 -0
  57. package/build/utils/assertion-verify/metrics.d.ts +13 -0
  58. package/build/utils/assertion-verify/metrics.js +16 -0
  59. package/build/utils/assertion-verify/verify.d.ts +11 -6
  60. package/build/utils/assertion-verify/verify.js +56 -15
  61. package/build/utils/canonicalJson.d.ts +11 -0
  62. package/build/utils/canonicalJson.js +17 -0
  63. package/build/utils/utils-verify/action-key.d.ts +27 -0
  64. package/build/utils/utils-verify/action-key.js +292 -0
  65. package/build/utils/utils-verify/allow.d.ts +8 -1
  66. package/build/utils/utils-verify/allow.js +14 -1
  67. package/build/utils/utils-verify/call-sites.d.ts +76 -8
  68. package/build/utils/utils-verify/call-sites.js +256 -70
  69. package/build/utils/utils-verify/language-spec.d.ts +3 -2
  70. package/build/utils/utils-verify/parse.d.ts +22 -3
  71. package/build/utils/utils-verify/parse.js +123 -52
  72. package/build/utils/utils-verify/verify.d.ts +33 -3
  73. package/build/utils/utils-verify/verify.js +126 -12
  74. package/build/utils/workspaceAuth.d.ts +59 -19
  75. package/build/utils/workspaceAuth.js +228 -31
  76. package/package.json +1 -1
  77. package/plugin/prompts/generate-tests/execution-plan.md +3 -3
  78. package/plugin/prompts/generate-tests/generation.md +1 -0
  79. package/plugin/prompts/plan-tests.md +41 -15
  80. package/plugin/prompts/testbot-task1.md +3 -9
  81. package/build/prompts/testbot/planDeclarations.d.ts +0 -6
  82. package/build/prompts/testbot/planDeclarations.js +0 -9
  83. package/plugin/prompts/declaring-a-plan.md +0 -20
@@ -0,0 +1,292 @@
1
+ /**
2
+ * The Playwright actions a browser helper can perform — the closed vocabulary that
3
+ * gives the action VERB, where an API helper's method comes from the request. A
4
+ * structural wait (`expect(...).toBeVisible()`) is not an action.
5
+ */
6
+ const ACTION_RE = /\.(click|dblclick|fill|type|press|check|uncheck|selectOption|hover|tap|clear|focus|setInputFiles|dragTo|goto)\s*\(/g;
7
+ /** Actions whose ARGUMENT is part of the identity: where a navigation goes, where a
8
+ * drag lands. Every other action's argument is a value (`fill("50")`), and a
9
+ * differing value is what the merge lifts into a parameter. */
10
+ const TARGET_ARG = new Set(["goto", "dragTo"]);
11
+ /**
12
+ * The comparison key of a browser helper: the analogue of `method + path` for a
13
+ * helper that sends no request. One step per action, in body order: the verb plus
14
+ * the LOCATOR CHAIN the action is called on, as written — the prompt's own
15
+ * definition of a browser duplicate is "same primitives in the same order,
16
+ * selectors identical", and that is a textual identity, not a set of tokens. So
17
+ * `getByRole("heading", { level: 1 })` and `{ level: 2 }` differ, `getByTestId("x")`
18
+ * and `getByText("x")` differ, `a.getByText("b")` and `b.getByText("a")` differ, and
19
+ * `.first()` / `.nth(2)` count. Formatting does not: whitespace and braces outside
20
+ * strings are dropped and quotes normalised, so a Prettier-wrapped chain and its
21
+ * one-line twin key the same. Argument VALUES are not part of the key, except a
22
+ * `goto` target and a `dragTo` destination, which are the identity of those steps.
23
+ * The legacy page-level form (`page.click("#save")`) is keyed on its selector
24
+ * argument.
25
+ *
26
+ * `undefined` when the helper performs no action, or when any action's locator is
27
+ * not built from plain string literals (a template literal, a variable, a regex, an
28
+ * f-string): a sequence with an unreadable step is not comparable. A `goto` target
29
+ * may be a template literal, keyed on its text with the interpolation kept: the
30
+ * generated `${baseUrl}/…` prefix names one module constant.
31
+ *
32
+ * `body` is the helper source with comments blanked and strings kept. Playwright for
33
+ * Python is read through the JavaScript spelling (see toPlaywrightJs), so the same
34
+ * helper in either language gets the same key.
35
+ */
36
+ export function actionKeyOf(body, language = "typescript") {
37
+ const src = resolveLocatorAliases(language === "python" ? toPlaywrightJs(body) : body);
38
+ // Structure is read on a copy with string CONTENTS blanked (same length), so a
39
+ // `)` or `.` inside a literal never ends a chain; text is taken from the original.
40
+ const structure = blankStringContents(src);
41
+ const steps = [];
42
+ for (const m of src.matchAll(ACTION_RE)) {
43
+ const verb = m[1];
44
+ const dot = m.index;
45
+ const argsOpen = dot + m[0].length - 1;
46
+ const argsClose = matchingClose(structure, argsOpen);
47
+ if (argsClose === -1)
48
+ return undefined;
49
+ const chainStart = chainStartBefore(structure, dot);
50
+ let chain = src.slice(chainStart, dot);
51
+ const args = src.slice(argsOpen + 1, argsClose);
52
+ let target = "";
53
+ if (!/\(/.test(structure.slice(chainStart, dot))) {
54
+ // No locator call in the chain: the legacy `page.click("#save")` form, whose
55
+ // first argument is the selector; or `page.goto(url)`.
56
+ target = firstArgument(structure, src, argsOpen + 1, argsClose);
57
+ if (!target)
58
+ return undefined;
59
+ }
60
+ else if (TARGET_ARG.has(verb))
61
+ target = args;
62
+ if (verb === "goto") {
63
+ chain = "";
64
+ // A template target is keyed on its text, `${…}` included: generated helpers
65
+ // prefix every path with one module constant (`${baseUrl}/login`), and
66
+ // `/orders/${id}` is one page whatever the id. A bare variable is not a
67
+ // target this can read.
68
+ if (/^\s*`/.test(target))
69
+ target = "`" + target.trim().slice(1, -1) + "`";
70
+ else if (!/^\s*["']/.test(target))
71
+ return undefined;
72
+ }
73
+ if (hasDynamicSelector(chain) || hasDynamicSelector(target))
74
+ return undefined;
75
+ if (!/["'`]/.test(chain) && !/["'`]/.test(target))
76
+ return undefined;
77
+ const step = [verb, normalize(chain), normalize(target)]
78
+ .filter(Boolean)
79
+ .join(" ");
80
+ steps.push(step);
81
+ }
82
+ return steps.length > 0 ? steps.join("; ") : undefined;
83
+ }
84
+ /** Index of the `)` that closes the `(` at `open`, on a string-blanked copy. */
85
+ function matchingClose(structure, open) {
86
+ let depth = 0;
87
+ for (let i = open; i < structure.length; i++) {
88
+ const ch = structure[i];
89
+ if (ch === "(" || ch === "[" || ch === "{")
90
+ depth++;
91
+ else if (ch === ")" || ch === "]" || ch === "}") {
92
+ depth--;
93
+ if (depth === 0)
94
+ return i;
95
+ }
96
+ }
97
+ return -1;
98
+ }
99
+ /** Start of the `.`-chain that ends at `dot` (exclusive): walks back over
100
+ * `identifier`, `identifier(...)`, and the `.` between them, so
101
+ * `page.getByRole("b", { name: "x" }).first()` is one chain and the `await` or
102
+ * `[`/`,` before it is not. */
103
+ function chainStartBefore(structure, dot) {
104
+ let i = dot;
105
+ for (;;) {
106
+ // Skip whitespace before the segment.
107
+ while (i > 0 && /\s/.test(structure[i - 1]))
108
+ i--;
109
+ // A call's argument list, balanced.
110
+ if (i > 0 && structure[i - 1] === ")") {
111
+ let depth = 0;
112
+ let j = i - 1;
113
+ for (; j >= 0; j--) {
114
+ const ch = structure[j];
115
+ if (ch === ")" || ch === "]" || ch === "}")
116
+ depth++;
117
+ else if (ch === "(" || ch === "[" || ch === "{") {
118
+ depth--;
119
+ if (depth === 0)
120
+ break;
121
+ }
122
+ }
123
+ if (j < 0)
124
+ return i;
125
+ i = j;
126
+ while (i > 0 && /\s/.test(structure[i - 1]))
127
+ i--;
128
+ }
129
+ // The segment's identifier.
130
+ const idEnd = i;
131
+ while (i > 0 && /[\w$]/.test(structure[i - 1]))
132
+ i--;
133
+ if (i === idEnd)
134
+ return idEnd; // no identifier: the chain ended before this
135
+ // A `.` continues the chain; anything else ends it (root reached).
136
+ let k = i;
137
+ while (k > 0 && /\s/.test(structure[k - 1]))
138
+ k--;
139
+ if (k > 0 && structure[k - 1] === ".") {
140
+ i = k - 1;
141
+ continue;
142
+ }
143
+ return i;
144
+ }
145
+ }
146
+ /** The first top-level argument of a call, as written, or "" when it is empty. */
147
+ function firstArgument(structure, src, from, to) {
148
+ let depth = 0;
149
+ for (let i = from; i < to; i++) {
150
+ const ch = structure[i];
151
+ if ("([{".includes(ch))
152
+ depth++;
153
+ else if (")]}".includes(ch))
154
+ depth--;
155
+ else if (ch === "," && depth === 0)
156
+ return src.slice(from, i).trim();
157
+ }
158
+ return src.slice(from, to).trim();
159
+ }
160
+ /** A locator built from anything but a plain string literal — a variable, a template
161
+ * literal, a regex, an f-string — or a `name:` option given that way. */
162
+ const LOCATOR_BUILDER_RE = /\.(?:getBy\w+|locator|frameLocator)\s*\(\s*([^\s)])/g;
163
+ const NAME_OPTION_RE = /\bname\s*:\s*([^\s,}])/g;
164
+ function hasDynamicSelector(text) {
165
+ if (text.trim() === "")
166
+ return false;
167
+ for (const re of [LOCATOR_BUILDER_RE, NAME_OPTION_RE])
168
+ for (const m of text.matchAll(re))
169
+ if (m[1] !== '"' && m[1] !== "'")
170
+ return true;
171
+ return false;
172
+ }
173
+ /** Whitespace and braces dropped outside string literals, `'` normalised to `"`. */
174
+ function normalize(text) {
175
+ let out = "";
176
+ let quote = null;
177
+ for (let i = 0; i < text.length; i++) {
178
+ const ch = text[i];
179
+ if (quote) {
180
+ if (ch === "\\") {
181
+ out += ch + (text[i + 1] ?? "");
182
+ i++;
183
+ continue;
184
+ }
185
+ if (ch === quote) {
186
+ quote = null;
187
+ out += '"';
188
+ }
189
+ else
190
+ out += ch;
191
+ continue;
192
+ }
193
+ if (ch === '"' || ch === "'" || ch === "`") {
194
+ quote = ch;
195
+ out += '"';
196
+ }
197
+ else if (!/[\s{}]/.test(ch))
198
+ out += ch;
199
+ }
200
+ // A trailing comma is Prettier's, not the locator's.
201
+ return out.replace(/,(?=[)\]])/g, "");
202
+ }
203
+ /** String contents replaced by spaces, delimiters kept, same length. */
204
+ function blankStringContents(text) {
205
+ return text.replace(/(["'`])(?:\\.|(?!\1)[^\\])*\1/g, (q) => q[0] + " ".repeat(Math.max(0, q.length - 2)) + q[0]);
206
+ }
207
+ /** Playwright for Python spells the same API in snake_case, passes locator options
208
+ * as keyword arguments, and exposes positional picks as properties. Rewritten to the
209
+ * JavaScript spelling so one reader and one verb list serve both languages. Only a
210
+ * keyword in argument position is rewritten: `[name=email]` inside a CSS string
211
+ * stays as written. */
212
+ const PY_CALLS = {
213
+ get_by_test_id: "getByTestId",
214
+ get_by_role: "getByRole",
215
+ get_by_text: "getByText",
216
+ get_by_label: "getByLabel",
217
+ get_by_placeholder: "getByPlaceholder",
218
+ get_by_alt_text: "getByAltText",
219
+ get_by_title: "getByTitle",
220
+ frame_locator: "frameLocator",
221
+ select_option: "selectOption",
222
+ set_input_files: "setInputFiles",
223
+ drag_to: "dragTo",
224
+ };
225
+ const PY_OPTIONS = {
226
+ name: "name",
227
+ has_text: "hasText",
228
+ has_not_text: "hasNotText",
229
+ exact: "exact",
230
+ level: "level",
231
+ checked: "checked",
232
+ };
233
+ function toPlaywrightJs(body) {
234
+ return body
235
+ .replace(/\.([a-z]+(?:_[a-z]+)+)\s*\(/g, (m, call) => PY_CALLS[call] ? `.${PY_CALLS[call]}(` : m)
236
+ .replace(/([(,]\s*)(name|has_text|has_not_text|exact|level|checked)\s*=(?!=)\s*/g, (_, before, key) => `${before}${PY_OPTIONS[key]}: `)
237
+ .replace(/\.(first|last)\b(?!\s*\()/g, ".$1()");
238
+ }
239
+ /** `const skip = page.getByRole(…);` followed by `await skip.click()` is the common
240
+ * shape of a guarded step. The alias is replaced by its chain wherever it is used
241
+ * as a receiver, so the click reads as the locator it acts on. Only a chain rooted
242
+ * in `page` (or `this.page`) qualifies, read to its terminating `;` — or, without
243
+ * semicolons, to the end of its `.`-continued lines — so a Prettier-wrapped chain
244
+ * is kept whole. A name bound more than once (`for (const row of rows)`), reassigned
245
+ * (`t = page.…`), or used as a parameter is ambiguous and is left alone; a variable
246
+ * receiver is unreadable. */
247
+ function resolveLocatorAliases(body) {
248
+ const structure = blankStringContents(body);
249
+ const aliases = new Map();
250
+ for (const m of body.matchAll(/\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?=(?:this\.)?page\s*\.)/g)) {
251
+ const name = m[1];
252
+ const from = m.index + m[0].length;
253
+ const to = valueEnd(structure, from);
254
+ aliases.set(name, body.slice(from, to).trim().replace(/;$/, ""));
255
+ }
256
+ for (const name of [...aliases.keys()]) {
257
+ const esc = name.replace(/\$/g, "\\$");
258
+ const bindings = body.match(new RegExp(`\\b(?:const|let|var)\\s+${esc}\\b`, "g"));
259
+ // Every `name =` — the declaration and any plain reassignment; not `==`, `!=`,
260
+ // `<=`, `>=` or `=>`. A reassigned alias would key its first value.
261
+ const assignments = body.match(new RegExp(`(?<![\\w$.])${esc}\\s*(?<![!<>=])=(?![=>])`, "g"));
262
+ const asParam = new RegExp(`\\(([^()]*\\b${esc}\\b[^()]*)\\)\\s*=>`).test(body) ||
263
+ new RegExp(`\\bfunction\\b[^(]*\\([^()]*\\b${esc}\\b[^()]*\\)`).test(body) ||
264
+ new RegExp(`(?<![\\w$.])${esc}\\s*=>`).test(body);
265
+ if ((bindings?.length ?? 0) > 1 ||
266
+ (assignments?.length ?? 0) > 1 ||
267
+ asParam)
268
+ aliases.delete(name);
269
+ }
270
+ if (aliases.size === 0)
271
+ return body;
272
+ return body.replace(/(?<![\w$.])([A-Za-z_$][\w$]*)(?=\s*\.)/g, (id) => aliases.get(id) ?? id);
273
+ }
274
+ /** End of an initialiser that starts at `from`: the `;` at paren depth 0, or a
275
+ * newline at depth 0 whose next line does not continue the chain with `.`. */
276
+ function valueEnd(structure, from) {
277
+ let depth = 0;
278
+ for (let i = from; i < structure.length; i++) {
279
+ const ch = structure[i];
280
+ if ("([{".includes(ch))
281
+ depth++;
282
+ else if (")]}".includes(ch))
283
+ depth = Math.max(0, depth - 1);
284
+ else if (depth === 0) {
285
+ if (ch === ";")
286
+ return i;
287
+ if (ch === "\n" && !/^\s*\./.test(structure.slice(i + 1)))
288
+ return i;
289
+ }
290
+ }
291
+ return structure.length;
292
+ }
@@ -6,6 +6,12 @@ export type UtilsViolationKind = "duplicate-helper" | "body-assertion" | "scenar
6
6
  | "assertion-loss"
7
7
  /** A page-error/console guard present at hand-out is gone. Not marker-eligible. */
8
8
  | "guard-removed"
9
+ /** One name defined twice in ONE utils file, whatever the parameter lists: a
10
+ * TypeScript module with two top-level functions of one name does not load, and
11
+ * in Python the second def silently replaces the first. Not marker-eligible — a
12
+ * module that cannot load has no reason to stay as it is. (The same name in two
13
+ * FILES with different parameter lists is `duplicate-helper`: drift, documentable.) */
14
+ | "helper-redefined"
9
15
  /** Python only: the spec imports the shared module in a form that cannot resolve
10
16
  * in the customer's repo. Not marker-eligible — a broken import has no reason to
11
17
  * stay. */
@@ -13,7 +19,8 @@ export type UtilsViolationKind = "duplicate-helper" | "body-assertion" | "scenar
13
19
  /** Kinds that only ever advise. Kept OUT of UtilsViolationKind so that type keeps
14
20
  * meaning "a kind the allow grammar and the gate know about" — an advisory can
15
21
  * never fail a verify and never needs (or accepts) an allow marker. */
16
- export type UtilsAdvisoryKind = "untyped-parameter";
22
+ export declare const UTILS_ADVISORY_KINDS: readonly ["untyped-parameter", "duplicate-action"];
23
+ export type UtilsAdvisoryKind = (typeof UTILS_ADVISORY_KINDS)[number];
17
24
  export declare const MARKERABLE_UTILS_KINDS: readonly ["duplicate-helper", "body-assertion", "scenario-name", "assertion-loss"];
18
25
  /** A kind the allow grammar accepts. `guard-removed` is a gate kind but never
19
26
  * markerable — a guard is restored, not documented away — so the grammar itself
@@ -1,4 +1,15 @@
1
1
  import { blankStrings } from "./parse.js";
2
+ /** Kinds that only ever advise. Kept OUT of UtilsViolationKind so that type keeps
3
+ * meaning "a kind the allow grammar and the gate know about" — an advisory can
4
+ * never fail a verify and never needs (or accepts) an allow marker. */
5
+ export const UTILS_ADVISORY_KINDS = [
6
+ "untyped-parameter",
7
+ /** Two browser helpers perform one action sequence (see actionKeyOf) — the
8
+ * browser analogue of `duplicate-helper`. Advisory, not blocking: the key is a
9
+ * heuristic over selector literals, and a false positive on a customer's first
10
+ * run is the worse outcome. */
11
+ "duplicate-action",
12
+ ];
2
13
  export const MARKERABLE_UTILS_KINDS = [
3
14
  "duplicate-helper",
4
15
  "body-assertion",
@@ -24,6 +35,8 @@ export const MARKERABLE_UTILS_KINDS = [
24
35
  */
25
36
  const ALLOW_RE = new RegExp(`reuse-verify:\\s*allow\\s+(${MARKERABLE_UTILS_KINDS.join("|")})\\s+([A-Za-z_$][\\w$]*)\\s*(?:[—–]+|\\s+-+\\s+)\\s*(?!<reason)(\\S.*?)\\s*$`);
26
37
  const ALLOW_LOOSE_RE = /reuse-verify:\s*allow\b/;
38
+ /** A marker naming an advisory kind — inert, never malformed (see parseUtilsAllows). */
39
+ const ADVISORY_MARKER_RE = new RegExp(`allow\\s+(?:${UTILS_ADVISORY_KINDS.join("|")})\\b`);
27
40
  /** The exact line to paste for a violation — what the failure text hands the agent. */
28
41
  export function allowMarkerLine(spec, kind, helper) {
29
42
  return `${spec.commentPrefix} reuse-verify: allow ${kind} ${helper} — <reason this helper must stay as it is>`;
@@ -59,7 +72,7 @@ export function parseUtilsAllows(content, spec) {
59
72
  // A marker naming an advisory kind is inert, not malformed: advisories never
60
73
  // block, so "documenting" one must not fail the gate either (the agent sees
61
74
  // the kind in a kind-shaped slot and reasonably tries).
62
- if (/allow\s+untyped-parameter\b/.test(code))
75
+ if (ADVISORY_MARKER_RE.test(code))
63
76
  continue;
64
77
  malformed.push(raw.trim());
65
78
  continue;
@@ -12,9 +12,10 @@ export interface SiblingFile {
12
12
  }
13
13
  /**
14
14
  * The Skyramp-generated tests in the spec's directory, each read once — THE sibling
15
- * walk, shared by the inline-call-site scan (which excludes the spec itself) and the
16
- * importer count (which includes it). Utils files carry no codegen version marker,
17
- * so the marker check alone excludes them. Fails open to [] on any read error.
15
+ * walk, shared by the two sibling call-site scans and the importer count (the inline
16
+ * scan excludes the spec itself; the other two include it). Utils files carry no
17
+ * codegen version marker, so the marker check alone excludes them. Fails open to []
18
+ * on any read error.
18
19
  */
19
20
  export declare function generatedSiblings(testFile: string, spec: UtilsLanguageSpec, opts: {
20
21
  includeSelf: boolean;
@@ -30,22 +31,70 @@ export interface InlineCallSite {
30
31
  path: string;
31
32
  /** The utils helper that wraps the same method+path. */
32
33
  helper: string;
34
+ /** Absolute path of the utils file that defines `helper` — with several modules in
35
+ * scope, the advisory names this one, not the list. */
36
+ utilsFile: string;
37
+ /** The sibling's OWN module-level helper the call sits in, when it does — a local
38
+ * rival to `helper`, to be deleted in favour of the import rather than replaced
39
+ * block-by-block. */
40
+ localHelper?: string;
33
41
  }
34
42
  /**
35
43
  * Inline request calls in OTHER Skyramp-generated tests beside `testFile` that a
36
44
  * utils helper already wraps — the reuse the prompt asks for and the agent does not
37
45
  * perform (measured 0/2 seeded eval runs with the rule as its own mandatory step).
38
46
  *
39
- * A call is INLINE when it sits in a test function, not in a module-level helper of
40
- * the sibling's own: a sibling that defines `create_order` locally is STEP 4b's
41
- * helper-vs-helper case, not a call site. Equivalence is `c3b9157b`'s: same method
42
- * and same normalised path, nothing else literals are what the call's arguments
43
- * are for.
47
+ * A call inside a module-level helper of the sibling's own counts too, named by that
48
+ * helper (`localHelper`): a sibling that defines `create_order` locally beside a
49
+ * shared `create_order` is the shared helper not used, however it is wrapped. The
50
+ * first design left that to STEP 4b of the prompt (helper-vs-helper) and nothing
51
+ * detected it across spec files, so the copies a half-done extraction leaves behind
52
+ * — one spec rewired, the others still defining the helper — went silent the moment
53
+ * the route became covered. Equivalence is `c3b9157b`'s: same method and same
54
+ * normalised path, nothing else — literals are what the call's arguments are for.
44
55
  *
45
56
  * Advisory by design: it names a change to a PRE-EXISTING test, which the maintenance
46
57
  * flow owns, so it informs rather than gates. Never throws.
47
58
  */
48
59
  export declare function findSiblingInlineCallSites(testFile: string, utilsFiles: string[], _helpers: UtilsHelper[], spec: UtilsLanguageSpec, preReadSiblings?: SiblingFile[]): Promise<InlineCallSite[]>;
60
+ /** One operation written in two or more sibling generated specs that no shared
61
+ * helper wraps — the extraction the reuse pass exists to perform and did not. */
62
+ export interface UnextractedDuplicate {
63
+ method: string;
64
+ /** The path as written at the first site. */
65
+ path: string;
66
+ /** Every copy, in directory order, spanning two or more files. */
67
+ sites: UnextractedSite[];
68
+ }
69
+ export interface UnextractedSite {
70
+ /** Absolute path of the generated test. */
71
+ file: string;
72
+ /** 1-based line of the request call. */
73
+ line: number;
74
+ /** The test's OWN module-level helper the call sits in, when it does. A per-spec
75
+ * helper is a copy as much as a bare call is: neither reached the shared module. */
76
+ localHelper?: string;
77
+ }
78
+ /**
79
+ * Request calls in the Skyramp-generated tests beside `testFile` — the delivered spec
80
+ * INCLUDED, since it is one of the copies — grouped by route and call shape, less the
81
+ * routes a utils helper covers. The same walk as findSiblingInlineCallSites keyed the
82
+ * other way: that scan starts from the helpers and finds a helper not USED; this one
83
+ * starts from the sites and finds a helper not CREATED. Every existing check takes a
84
+ * defined helper as one side of its comparison, so an operation never extracted was
85
+ * invisible to all of them (measured: four per-spec copies of the operation a PR was
86
+ * about, differing only in expected status, beside a module that shared two others).
87
+ *
88
+ * Two sites are one operation when they share method, normalised path and keyword
89
+ * shape — the API near-duplicate definition. Body fields and a schema check do not
90
+ * split a group: with no incumbent helper there is nothing to be substitutable FOR,
91
+ * a body difference is one more literal to lift, and a schema check stays in the spec
92
+ * after the helper call because the response variable is kept.
93
+ *
94
+ * A group must span two or more FILES: a spec repeating one request twice is a
95
+ * different question. Advisory by design — consolidation is a judgement. Never throws.
96
+ */
97
+ export declare function findUnextractedDuplicates(testFile: string, utilsFiles: string[], spec: UtilsLanguageSpec, preReadSiblings?: SiblingFile[]): Promise<UnextractedDuplicate[]>;
49
98
  /** Lines after a call scanned for its schema assertion — the generated shape puts it
50
99
  * immediately after the status-code assertion. */
51
100
  export declare const SCHEMA_SCAN_LINES = 12;
@@ -66,6 +115,25 @@ export declare function hasSchemaCheck(content: string, from: number, to: number
66
115
  * request.
67
116
  */
68
117
  export declare function requestFields(content: string, call: string, callAt?: number): Set<string>;
118
+ /**
119
+ * The query-parameter NAMES a request call sends (`query_params={"page": 1}` /
120
+ * `queryParams: { page }`), or the empty set. Kept apart from requestFields, which
121
+ * strips `query_params` on purpose because it is shared with the retrofit gate and a
122
+ * query key is not a body field; here the question is the advisory's own — whether a
123
+ * helper can stand in for a site without ADDING a query parameter — and the rule is
124
+ * the body rule: every helper key must be present at the site.
125
+ */
126
+ export declare function queryKeys(call: string): Set<string>;
127
+ /** The call shape both sibling scans compare: shapeKeys of the call with the interior
128
+ * of every keyed `{…}` value blanked, so the keys INSIDE a body, path-params or
129
+ * query-params object never read as shape. shapeKeys sees every `name:` in the text,
130
+ * and an unquoted TS object key (`body: { discountPercent: 10 }`) is one: a helper
131
+ * inlining `body: { title }` never matched a site whose body added a field, and two
132
+ * copies whose bodies named different fields were two operations — in both scans the
133
+ * fields are the literals to lift, and `substitutable` is what judges them. Python
134
+ * quoted dict keys (`{"order_id": id}`) never matched the identifier pattern, so
135
+ * this is a TS/JS-only change. */
136
+ export declare function callShape(call: string): Set<string>;
69
137
  /** The keyword names of a request call (`path=`, `method=`, `query_params=` … / TS
70
138
  * object keys), less the liftable ones — a cheap normalised call shape. */
71
139
  export declare function shapeKeys(call: string): Set<string>;