@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,341 @@
1
+ import * as path from "path";
2
+ import { strippedSources } from "./strip-dispatch.js";
3
+ import { balancedCloseIndex, lineOfOffset } from "./lint-types.js";
4
+ const FINGERPRINT_CHAR_CAP = 120;
5
+ const FINGERPRINT_SITE_CAP = 500;
6
+ /** Exact-value matchers (camelCase and playwright-python snake_case twins). */
7
+ const EXACT_MATCHERS = new Set([
8
+ "toHaveText",
9
+ "toHaveValue",
10
+ "toHaveAttribute",
11
+ "toHaveCount",
12
+ "toBeChecked",
13
+ "toHaveLength",
14
+ "toBe",
15
+ "toEqual",
16
+ "toStrictEqual",
17
+ "toBeUndefined",
18
+ "to_have_text",
19
+ "to_have_value",
20
+ "to_have_attribute",
21
+ "to_have_count",
22
+ "to_be_checked",
23
+ "to_have_length",
24
+ "to_be",
25
+ "to_equal",
26
+ "toHaveJSProperty",
27
+ "to_have_js_property",
28
+ ]);
29
+ /** Pattern/partial/state matchers — weaker than exact values, but real
30
+ * behavioral assertions: `toBeDisabled` after an action asserts a semantic
31
+ * state transition, not mere presence, so it must clear the weak-additions
32
+ * gate (weight ≥ 2). */
33
+ const PARTIAL_MATCHERS = new Set([
34
+ "toMatch",
35
+ "toContainText",
36
+ "toHaveClass",
37
+ "toHaveURL",
38
+ "toContainEqual",
39
+ "toBeDisabled",
40
+ "toBeEnabled",
41
+ "toBeHidden",
42
+ "to_match",
43
+ "to_contain_text",
44
+ "to_have_class",
45
+ "to_have_url",
46
+ "to_be_disabled",
47
+ "to_be_enabled",
48
+ "to_be_hidden",
49
+ ]);
50
+ /** Chain links to skip between `expect(...)` and the matcher. */
51
+ const CHAIN_LINKS = new Set([
52
+ "not",
53
+ "resolves",
54
+ "rejects",
55
+ "soft",
56
+ "not_to",
57
+ "to",
58
+ ]);
59
+ export function detectAssertionLanguage(testFile) {
60
+ switch (path.extname(testFile).toLowerCase()) {
61
+ case ".py":
62
+ return "python";
63
+ case ".ts":
64
+ case ".tsx":
65
+ case ".mts":
66
+ return "typescript";
67
+ case ".js":
68
+ case ".jsx":
69
+ case ".mjs":
70
+ case ".cjs":
71
+ return "javascript";
72
+ case ".java":
73
+ return "java";
74
+ default:
75
+ return undefined;
76
+ }
77
+ }
78
+ function matcherWeight(matcher) {
79
+ if (matcher === undefined)
80
+ return 1;
81
+ if (EXACT_MATCHERS.has(matcher))
82
+ return 3;
83
+ if (PARTIAL_MATCHERS.has(matcher))
84
+ return 2;
85
+ // Unknown matchers weigh 1, never 0 — any added assertion raises the score.
86
+ return 1;
87
+ }
88
+ function fingerprintOf(raw, start, end) {
89
+ return raw
90
+ .slice(start, end)
91
+ .replace(/\s+/g, " ")
92
+ .trim()
93
+ .slice(0, FINGERPRINT_CHAR_CAP);
94
+ }
95
+ function startOfLine(src, offset) {
96
+ const nl = src.lastIndexOf("\n", offset - 1);
97
+ return nl === -1 ? 0 : nl + 1;
98
+ }
99
+ /** expect-chain sites (`expect(...)[.not/.resolves/...].matcher(...)`), shared by
100
+ * TS/JS and playwright-python. Scans stripped content, fingerprints from raw. */
101
+ function expectSites(raw, stripped) {
102
+ const sites = [];
103
+ const re = /\bexpect(?:\s*\.\s*soft)?\s*\(/g;
104
+ let m;
105
+ while ((m = re.exec(stripped)) !== null) {
106
+ const openIdx = m.index + m[0].length - 1;
107
+ const argClose = balancedCloseIndex(stripped, openIdx);
108
+ if (argClose === -1)
109
+ continue;
110
+ // Walk the chain past link members (.not / .resolves / …) to the matcher.
111
+ let cursor = argClose + 1;
112
+ let matcher;
113
+ for (;;) {
114
+ const link = /^\s*\.\s*([A-Za-z_]\w*)/.exec(stripped.slice(cursor));
115
+ if (!link)
116
+ break;
117
+ cursor += link[0].length;
118
+ if (CHAIN_LINKS.has(link[1]))
119
+ continue;
120
+ matcher = link[1];
121
+ break;
122
+ }
123
+ // Statement extent: start of the site's line through the matcher call's
124
+ // closing paren (or the expect close when there is no chained call).
125
+ let stmtEnd = argClose + 1;
126
+ if (matcher !== undefined) {
127
+ const matcherOpen = /^\s*\(/.exec(stripped.slice(cursor));
128
+ if (matcherOpen) {
129
+ const close = balancedCloseIndex(stripped, cursor + matcherOpen[0].length - 1);
130
+ if (close !== -1)
131
+ stmtEnd = close + 1;
132
+ }
133
+ else {
134
+ stmtEnd = cursor;
135
+ }
136
+ }
137
+ const stmtStart = startOfLine(raw, m.index);
138
+ sites.push({
139
+ line: lineOfOffset(stripped, m.index),
140
+ matcher,
141
+ weight: matcherWeight(matcher),
142
+ fingerprint: fingerprintOf(raw, stmtStart, stmtEnd),
143
+ });
144
+ re.lastIndex = Math.max(re.lastIndex, argClose + 1);
145
+ }
146
+ return sites;
147
+ }
148
+ /** Weight of a bare python `assert` statement, classified by its operators. */
149
+ function pythonAssertWeight(stmt) {
150
+ if (stmt.includes("==") ||
151
+ stmt.includes("!=") ||
152
+ // Same tier rule (and same spelling tolerance) as the node-assert scanner
153
+ // — the two had drifted, scoring an identical schema check 3 in JS and 1
154
+ // in python when spelled checkSchema.
155
+ /\bcheck_?schema\s*\(/i.test(stmt))
156
+ return 3;
157
+ if (/\bre\s*\.\s*(?:match|fullmatch)\s*\(|\.startswith\s*\(|\bin\b/.test(stmt))
158
+ return 2;
159
+ return 1;
160
+ }
161
+ function pythonAssertSites(raw, stripped) {
162
+ const sites = [];
163
+ const lines = stripped.split("\n");
164
+ let offset = 0;
165
+ for (let i = 0; i < lines.length; i++) {
166
+ const line = lines[i];
167
+ if (/^\s*assert\b/.test(line)) {
168
+ // Extend across paren/backslash continuations (bounded) so one multi-line
169
+ // assert counts once and fingerprints whole.
170
+ let stmt = line;
171
+ let last = i;
172
+ const depthOf = (s) => (s.match(/[([{]/g)?.length ?? 0) - (s.match(/[)\]}]/g)?.length ?? 0);
173
+ let depth = depthOf(stmt);
174
+ while ((depth > 0 || stmt.trimEnd().endsWith("\\")) &&
175
+ last + 1 < lines.length &&
176
+ last - i < 10) {
177
+ last++;
178
+ stmt += "\n" + lines[last];
179
+ depth = depthOf(stmt);
180
+ }
181
+ const stmtEnd = offset + stmt.length;
182
+ sites.push({
183
+ line: i + 1,
184
+ weight: pythonAssertWeight(stmt),
185
+ fingerprint: fingerprintOf(raw, offset + (line.length - line.trimStart().length), stmtEnd),
186
+ });
187
+ i = last;
188
+ offset = stmtEnd + 1;
189
+ continue;
190
+ }
191
+ offset += line.length + 1;
192
+ }
193
+ return sites;
194
+ }
195
+ // Node's assert module — the idiom JS codegen actually emits
196
+ // (`assert.strictEqual(...)`, `assert.ok(checkSchema(...))`); Playwright
197
+ // `expect()` never appears in those files, so without this scanner every JS
198
+ // spec scored 0 sites and the strength gate false-failed on 0 > 0
199
+ // (found by running the verifier over the javascript/playwright goldens).
200
+ const NODE_ASSERT_RE = /(?<![\w$.])assert\s*\.\s*([\w$]+)\s*\(/g;
201
+ // Exact-value methods score like their expect() equivalents (toBe/toEqual=3);
202
+ // assert.match is a pattern check (2); everything else — ok, notStrictEqual
203
+ // (the "not null" idiom), throws — is existence-tier (1).
204
+ const NODE_ASSERT_EXACT = new Set([
205
+ "strictEqual",
206
+ "deepStrictEqual",
207
+ "equal",
208
+ "deepEqual",
209
+ ]);
210
+ /** Shared regex-scan-and-fingerprint loop for call-shaped assertion dialects
211
+ * (node assert, java) — they differ only in the regex and the weighting. */
212
+ function callSites(raw, stripped, re, matcherOf, weightOf) {
213
+ const sites = [];
214
+ re.lastIndex = 0;
215
+ let m;
216
+ while ((m = re.exec(stripped)) !== null) {
217
+ const close = balancedCloseIndex(stripped, m.index + m[0].length - 1);
218
+ const args = close === -1 ? "" : stripped.slice(m.index + m[0].length, close);
219
+ sites.push({
220
+ line: lineOfOffset(stripped, m.index),
221
+ matcher: matcherOf(m),
222
+ weight: weightOf(m, args),
223
+ fingerprint: fingerprintOf(raw, startOfLine(raw, m.index), close === -1 ? m.index + m[0].length : close + 1),
224
+ });
225
+ }
226
+ return sites;
227
+ }
228
+ function nodeAssertSites(raw, stripped) {
229
+ return callSites(raw, stripped, NODE_ASSERT_RE, (m) => `assert.${m[1]}`,
230
+ // assert.ok(checkSchema(...)) is a full-shape check — exact tier, same
231
+ // treatment as check_schema in the python scanner.
232
+ (m, args) => NODE_ASSERT_EXACT.has(m[1])
233
+ ? 3
234
+ : /\bcheck_?schema\s*\(/i.test(args)
235
+ ? 3
236
+ : m[1] === "match"
237
+ ? 2
238
+ : 1);
239
+ }
240
+ const JAVA_ASSERT_RE = /\b(assertEquals|assertNotEquals|assertTrue|assertFalse|assertNull|assertNotNull|assertThat|assertArrayEquals)\s*\(/g;
241
+ function javaAssertSites(raw, stripped) {
242
+ return callSites(raw, stripped, JAVA_ASSERT_RE, (m) => m[1], (m) => (m[1] === "assertEquals" || m[1] === "assertArrayEquals" ? 3 : 1));
243
+ }
244
+ /**
245
+ * The assertion's SUBJECT — what it asserts about — recovered from a fingerprint
246
+ * (`expect(SUBJECT).matcher(...)` arg, or a python assert's left-hand side).
247
+ * Correlating subjects across baseline and current fingerprints is what tells a
248
+ * REPLACED assertion (subject still asserted, usually more strongly) from a
249
+ * REMOVED one (subject lost all coverage). Undefined when unrecoverable (e.g.
250
+ * truncated fingerprint) — callers treat that conservatively.
251
+ */
252
+ export function subjectOf(fingerprint) {
253
+ const expectMatch = /\bexpect(?:\s*\.\s*soft)?\s*\(/.exec(fingerprint);
254
+ if (expectMatch) {
255
+ const openIdx = expectMatch.index + expectMatch[0].length - 1;
256
+ const close = balancedCloseIndex(fingerprint, openIdx);
257
+ if (close === -1)
258
+ return undefined;
259
+ return fingerprint.slice(openIdx + 1, close).replace(/\s+/g, "");
260
+ }
261
+ // Node assert: the subject is the first top-level argument
262
+ // (`assert.strictEqual(SUBJECT, expected, msg?)`, `assert.ok(SUBJECT, msg?)`).
263
+ const nodeAssert = /(?<![\w$.])assert\s*\.\s*[\w$]+\s*\(/.exec(fingerprint);
264
+ if (nodeAssert) {
265
+ const openIdx = nodeAssert.index + nodeAssert[0].length - 1;
266
+ const close = balancedCloseIndex(fingerprint, openIdx);
267
+ const args = fingerprint.slice(openIdx + 1, close === -1 ? undefined : close);
268
+ let depth = 0;
269
+ let end = args.length;
270
+ for (let i = 0; i < args.length; i++) {
271
+ const c = args[i];
272
+ if (c === "(" || c === "[" || c === "{")
273
+ depth++;
274
+ else if (c === ")" || c === "]" || c === "}")
275
+ depth--;
276
+ else if (depth === 0 && c === ",") {
277
+ end = i;
278
+ break;
279
+ }
280
+ }
281
+ const subject = args.slice(0, end).replace(/\s+/g, "");
282
+ return subject.length > 0 ? subject : undefined;
283
+ }
284
+ const assertPrefix = /^assert\s+/.exec(fingerprint);
285
+ if (assertPrefix) {
286
+ // Depth-aware scan to the first TOP-LEVEL comparison operator, `is`/`in`,
287
+ // or comma (an assert message) — commas inside call args must not split.
288
+ const rest = fingerprint.slice(assertPrefix[0].length);
289
+ let depth = 0;
290
+ let end = rest.length;
291
+ for (let i = 0; i < rest.length; i++) {
292
+ const c = rest[i];
293
+ if (c === "(" || c === "[" || c === "{")
294
+ depth++;
295
+ else if (c === ")" || c === "]" || c === "}")
296
+ depth--;
297
+ else if (depth === 0) {
298
+ if (c === "," ||
299
+ c === "<" ||
300
+ c === ">" ||
301
+ (c === "=" && rest[i + 1] === "=") ||
302
+ (c === "!" && rest[i + 1] === "=") ||
303
+ rest.startsWith(" is ", i) ||
304
+ rest.startsWith(" in ", i)) {
305
+ end = i;
306
+ break;
307
+ }
308
+ }
309
+ }
310
+ const lhs = rest.slice(0, end).replace(/\s+/g, "");
311
+ return lhs.length > 0 ? lhs : undefined;
312
+ }
313
+ return undefined;
314
+ }
315
+ export function computeAssertionMetrics(content, language) {
316
+ let sites;
317
+ const { stripped } = strippedSources(content, language);
318
+ if (language === "python") {
319
+ sites = [
320
+ ...pythonAssertSites(content, stripped),
321
+ ...expectSites(content, stripped),
322
+ ];
323
+ }
324
+ else if (language === "java") {
325
+ sites = javaAssertSites(content, stripped);
326
+ }
327
+ else {
328
+ sites = [
329
+ ...expectSites(content, stripped),
330
+ ...nodeAssertSites(content, stripped),
331
+ ];
332
+ }
333
+ return {
334
+ count: sites.length,
335
+ strengthScore: sites.reduce((sum, s) => sum + s.weight, 0),
336
+ fingerprints: sites
337
+ .slice(0, FINGERPRINT_SITE_CAP)
338
+ .map((s) => s.fingerprint),
339
+ sites,
340
+ };
341
+ }
@@ -0,0 +1,6 @@
1
+ /** Blanks `#` comments only, preserving string contents. */
2
+ export declare function stripPythonComments(src: string): string;
3
+ /** Blanks string literal contents only, preserving comments. */
4
+ export declare function stripPythonStrings(src: string): string;
5
+ /** Blanks both comments and strings. */
6
+ export declare function stripPythonCommentsAndStrings(src: string): string;
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Python twin of `src/utils/pom-scope/strip.ts`: one lexer-lite pass classifying
3
+ * `#` comments and string literals ('...', "...", '''...''', """...""", with
4
+ * prefixes like f/r/b treated as part of plain strings). Blanked characters
5
+ * become spaces (newlines preserved) so surviving code keeps its offsets.
6
+ */
7
+ function classifySpans(src) {
8
+ const spans = [];
9
+ let i = 0;
10
+ const n = src.length;
11
+ while (i < n) {
12
+ const c = src[i];
13
+ if (c === "#") {
14
+ const start = i;
15
+ while (i < n && src[i] !== "\n")
16
+ i++;
17
+ spans.push({ kind: "comment", start, end: i });
18
+ continue;
19
+ }
20
+ if (c === "'" || c === '"') {
21
+ const quote = c;
22
+ const triple = src.startsWith(quote.repeat(3), i);
23
+ const closer = triple ? quote.repeat(3) : quote;
24
+ const start = i;
25
+ i += closer.length;
26
+ let closed = false;
27
+ while (i < n) {
28
+ if (src[i] === "\\" && i + 1 < n) {
29
+ i += 2;
30
+ continue;
31
+ }
32
+ if (src.startsWith(closer, i)) {
33
+ i += closer.length;
34
+ closed = true;
35
+ break;
36
+ }
37
+ // Single-quoted strings can't span a raw newline; treat an unclosed
38
+ // opener as a stray quote rather than blanking to end of file.
39
+ if (src[i] === "\n" && !triple)
40
+ break;
41
+ i++;
42
+ }
43
+ if (!closed && !triple) {
44
+ i = start + 1;
45
+ continue;
46
+ }
47
+ spans.push({ kind: "string", start, end: i });
48
+ continue;
49
+ }
50
+ i++;
51
+ }
52
+ return spans;
53
+ }
54
+ function blankSpans(src, spans) {
55
+ if (spans.length === 0)
56
+ return src;
57
+ const out = src.split("");
58
+ for (const { start, end } of spans) {
59
+ for (let i = start; i < end; i++)
60
+ out[i] = src[i] === "\n" ? "\n" : " ";
61
+ }
62
+ return out.join("");
63
+ }
64
+ /** Blanks `#` comments only, preserving string contents. */
65
+ export function stripPythonComments(src) {
66
+ return blankSpans(src, classifySpans(src).filter((s) => s.kind === "comment"));
67
+ }
68
+ /** Blanks string literal contents only, preserving comments. */
69
+ export function stripPythonStrings(src) {
70
+ return blankSpans(src, classifySpans(src).filter((s) => s.kind === "string"));
71
+ }
72
+ /** Blanks both comments and strings. */
73
+ export function stripPythonCommentsAndStrings(src) {
74
+ return blankSpans(src, classifySpans(src));
75
+ }
@@ -0,0 +1,19 @@
1
+ import type { AssertionLanguage } from "./metrics.js";
2
+ /**
3
+ * The one place that knows how to strip a source per language — including the
4
+ * rule that JS/TS regex literals must be blanked BEFORE string stripping (an
5
+ * apostrophe inside a regex otherwise opens a phantom string span). Before
6
+ * this module, that rule was re-asserted by hand at seven call sites, and one
7
+ * verify pass re-stripped the same content 5–8 times; the one-entry memo makes
8
+ * repeated calls on the same content free (metrics + every lint module + the
9
+ * marker scan all run on one file per pass).
10
+ */
11
+ export interface StrippedSources {
12
+ /** Comments blanked; strings intact. */
13
+ commentless: string;
14
+ /** Comments AND strings blanked (JS/TS: regex literals blanked first). */
15
+ stripped: string;
16
+ /** Strings blanked; comments intact (the marker scan reads comments). */
17
+ stringless: string;
18
+ }
19
+ export declare function strippedSources(content: string, language: AssertionLanguage): StrippedSources;
@@ -0,0 +1,42 @@
1
+ import { stripComments, stripCommentsAndStrings, stripStrings, } from "../pom-scope/strip.js";
2
+ import { stripPythonComments, stripPythonCommentsAndStrings, stripPythonStrings, } from "./python-strip.js";
3
+ import { blankJsRegexLiterals } from "./js-regex-blank.js";
4
+ let memoKeyContent;
5
+ let memoKeyLanguage;
6
+ let memoValue;
7
+ export function strippedSources(content, language) {
8
+ if (memoValue !== undefined &&
9
+ memoKeyContent === content &&
10
+ memoKeyLanguage === language) {
11
+ return memoValue;
12
+ }
13
+ let value;
14
+ if (language === "python") {
15
+ value = {
16
+ commentless: stripPythonComments(content),
17
+ stripped: stripPythonCommentsAndStrings(content),
18
+ stringless: stripPythonStrings(content),
19
+ };
20
+ }
21
+ else if (language === "java") {
22
+ // Java gets the JS/TS comment/string grammar (same tokens) minus the
23
+ // regex-literal pre-pass, which does not exist in Java.
24
+ value = {
25
+ commentless: stripComments(content),
26
+ stripped: stripCommentsAndStrings(content),
27
+ stringless: stripStrings(content),
28
+ };
29
+ }
30
+ else {
31
+ const blanked = blankJsRegexLiterals(content);
32
+ value = {
33
+ commentless: stripComments(content),
34
+ stripped: stripCommentsAndStrings(blanked),
35
+ stringless: stripStrings(blanked),
36
+ };
37
+ }
38
+ memoKeyContent = content;
39
+ memoKeyLanguage = language;
40
+ memoValue = value;
41
+ return value;
42
+ }
@@ -0,0 +1,8 @@
1
+ import type { AssertionLanguage } from "./metrics.js";
2
+ import { type LintFinding, type LintOptions } from "./lint-types.js";
3
+ /** UI (Playwright) lints. Python (playwright-python) UI specs deliberately get
4
+ * the site-scoped lints only: the structural rules' patterns and accept-lists
5
+ * are JS/TS-specific (imports, waitForResponse shapes, expect matcher accept
6
+ * forms), and porting them without a python golden corpus to validate against
7
+ * would recreate the false-block class review round 2 removed. Pinned by test. */
8
+ export declare function lintUiSpec(raw: string, language: AssertionLanguage, opts?: LintOptions): LintFinding[];