@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.
- package/build/prompts/enhance-assertions/contractProviderAssertionsPrompt.js +2 -1
- package/build/prompts/enhance-assertions/integrationAssertionsPrompt.js +2 -1
- package/build/prompts/enhance-assertions/sharedAssertionRules.d.ts +1 -1
- package/build/prompts/enhance-assertions/sharedAssertionRules.js +57 -22
- package/build/prompts/enhance-assertions/uiAssertionsPrompt.js +17 -9
- package/build/prompts/test-recommendation/diffExecutionPlan.js +0 -2
- package/build/prompts/test-recommendation/test-recommendation-prompt.js +11 -6
- package/build/prompts/testbot/testbot-prompts.js +19 -12
- package/build/recommendation/answers.d.ts +11 -7
- package/build/recommendation/answers.js +14 -10
- package/build/recommendation/pullRequestText.d.ts +18 -0
- package/build/recommendation/pullRequestText.js +31 -0
- package/build/recommendation/registerPlan.d.ts +9 -1
- package/build/recommendation/registerPlan.js +8 -1
- package/build/recommendation/runVerifiers.js +6 -0
- package/build/recommendation/types.d.ts +58 -0
- package/build/recommendation/verifierContracts.d.ts +108 -13
- package/build/recommendation/verifierContracts.js +147 -29
- package/build/recommendation/verifiers/coverage.d.ts +10 -0
- package/build/recommendation/verifiers/coverage.js +144 -22
- package/build/recommendation/verifiers/defects.d.ts +9 -0
- package/build/recommendation/verifiers/defects.js +117 -0
- package/build/recommendation/verifiers/deliveredMatchesPlan.d.ts +22 -0
- package/build/recommendation/verifiers/deliveredMatchesPlan.js +43 -0
- package/build/recommendation/verifiers/existingCoverage.js +53 -0
- package/build/recommendation/verifiers/expectedValueSourced.d.ts +14 -0
- package/build/recommendation/verifiers/expectedValueSourced.js +246 -0
- package/build/recommendation/verifiers/issueTraceability.d.ts +52 -0
- package/build/recommendation/verifiers/issueTraceability.js +197 -0
- package/build/recommendation/verifiers/requirementSourced.d.ts +2 -0
- package/build/recommendation/verifiers/requirementSourced.js +168 -0
- package/build/services/TestGenerationService.js +3 -1
- package/build/tools/code-refactor/codeReuseTool.js +1 -1
- package/build/tools/code-refactor/reuse-outcome.d.ts +1 -1
- package/build/tools/code-refactor/reuse-state.d.ts +85 -7
- package/build/tools/code-refactor/reuse-state.js +239 -34
- package/build/tools/code-refactor/utils-verify-gates.d.ts +5 -0
- package/build/tools/code-refactor/utils-verify-gates.js +103 -11
- package/build/tools/generate-tests/generateBatchScenarioRestTool.js +2 -1
- package/build/tools/submitReportTool.js +276 -39
- package/build/tools/test-management/actionsTool.js +5 -0
- package/build/tools/test-management/analyzeChangesTool.d.ts +53 -0
- package/build/tools/test-management/analyzeChangesTool.js +55 -2
- package/build/tools/test-management/registerTestPlanTool.d.ts +32 -17
- package/build/tools/test-management/registerTestPlanTool.js +105 -16
- package/build/types/ReuseOutcome.d.ts +73 -7
- package/build/types/TestAnalysis.d.ts +6 -0
- package/build/types/TestbotReport.d.ts +22 -4
- package/build/utils/AnalysisStateManager.d.ts +7 -1
- package/build/utils/AnalysisStateManager.js +5 -1
- package/build/utils/assertion-verify/api-shared-lints.js +118 -11
- package/build/utils/assertion-verify/format.js +2 -2
- package/build/utils/assertion-verify/helper-imports.d.ts +7 -0
- package/build/utils/assertion-verify/helper-imports.js +119 -27
- package/build/utils/assertion-verify/lint-types.d.ts +31 -2
- package/build/utils/assertion-verify/lint-types.js +66 -0
- package/build/utils/assertion-verify/metrics.d.ts +13 -0
- package/build/utils/assertion-verify/metrics.js +16 -0
- package/build/utils/assertion-verify/verify.d.ts +11 -6
- package/build/utils/assertion-verify/verify.js +56 -15
- package/build/utils/canonicalJson.d.ts +11 -0
- package/build/utils/canonicalJson.js +17 -0
- package/build/utils/utils-verify/action-key.d.ts +27 -0
- package/build/utils/utils-verify/action-key.js +292 -0
- package/build/utils/utils-verify/allow.d.ts +8 -1
- package/build/utils/utils-verify/allow.js +14 -1
- package/build/utils/utils-verify/call-sites.d.ts +76 -8
- package/build/utils/utils-verify/call-sites.js +256 -70
- package/build/utils/utils-verify/language-spec.d.ts +3 -2
- package/build/utils/utils-verify/parse.d.ts +22 -3
- package/build/utils/utils-verify/parse.js +123 -52
- package/build/utils/utils-verify/verify.d.ts +33 -3
- package/build/utils/utils-verify/verify.js +126 -12
- package/build/utils/workspaceAuth.d.ts +59 -19
- package/build/utils/workspaceAuth.js +228 -31
- package/package.json +1 -1
- package/plugin/prompts/generate-tests/execution-plan.md +3 -3
- package/plugin/prompts/generate-tests/generation.md +1 -0
- package/plugin/prompts/plan-tests.md +41 -15
- package/plugin/prompts/testbot-task1.md +3 -9
- package/build/prompts/testbot/planDeclarations.d.ts +0 -6
- package/build/prompts/testbot/planDeclarations.js +0 -9
- 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
|
|
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 (
|
|
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
|
|
16
|
-
*
|
|
17
|
-
* so the marker check alone excludes them. Fails open to []
|
|
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
|
|
40
|
-
*
|
|
41
|
-
* helper
|
|
42
|
-
*
|
|
43
|
-
*
|
|
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>;
|