@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
@@ -1,4 +1,5 @@
1
1
  import type { ReuseRecord } from "../tools/code-refactor/reuse-outcome.js";
2
+ import type { AssertionRecord } from "../types/AssertionOutcome.js";
2
3
  import { TestAnalysisResult, MaintenanceActionCore } from "../types/TestAnalysis.js";
3
4
  import { RepositoryAnalysis, AnalysisScope } from "../types/RepositoryAnalysis.js";
4
5
  import { PRTestContext } from "./pr-comment-parser.js";
@@ -181,6 +182,16 @@ export interface PlanBudgetContext {
181
182
  /** Whether the PR diff changes any test file; see BudgetContext. */
182
183
  diffChangesTestFiles?: boolean;
183
184
  }
185
+ /** A pre-existing Skyramp-generated test the reuse pass edited to import from a
186
+ * shared utils file (STEP 5c / the sibling-inline advisory). Keyed by ABSOLUTE
187
+ * PATH. Recorded in-process where staging finds the edit; skyramp_submit_report
188
+ * refuses a report that leaves one undisclosed or unexecuted. */
189
+ export interface ReuseRetrofit {
190
+ /** BASENAME(s) of the utils file(s) it now imports from. */
191
+ utilsFile: string;
192
+ /** Absolute path of the generated test whose reuse pass caused the edit. */
193
+ testFile: string;
194
+ }
184
195
  export interface ReuseHandOff {
185
196
  testType: string;
186
197
  language: string;
@@ -231,19 +242,14 @@ export interface UnifiedAnalysisState {
231
242
  * which computes every value in-process; skyramp_submit_report merges it into
232
243
  * the report's `reuse` field. Never supplied by the LLM. */
233
244
  reuseOutcomes?: Record<string, ReuseRecord>;
234
- /** SKYR-4262. Assertion-enhancement obligations per spec, keyed by test-file
235
- * BASENAME (same key as `reuseOutcomes`). Written in-process by
236
- * skyramp_enhance_assertions when it hands out instructions; checked by
237
- * skyramp_execute_test, which blocks when the spec is byte-identical to its
238
- * state at handout time (the agent acknowledged the instructions but never
239
- * acted on them). Never supplied by the LLM. */
240
- enhanceOutcomes?: Record<string, import("../tools/code-refactor/enhance-state.js").EnhanceRecord>;
241
245
  /** Generation hand-offs that owe a modularize→reuse pass, keyed by the ABSOLUTE
242
246
  * PATH of each file the generation call wrote (found by snapshotting `outputDir`
243
247
  * before and after codegen — the agent, not the tool, chooses the file name).
244
248
  * Written by the generation service at the exact site that emits the hand-off, so
245
249
  * the debt has the same server-derived trust as `reuseOutcomes`. */
246
250
  reuseHandOffs?: Record<string, ReuseHandOff>;
251
+ /** See {@link ReuseRetrofit}. Never supplied by the LLM. */
252
+ reuseRetrofits?: Record<string, ReuseRetrofit>;
247
253
  /**
248
254
  * SKYR-4156. Recorded video per executed browser test, keyed by test-file
249
255
  * BASENAME (the same key `reuseOutcomes` uses, so matching needs no path
@@ -258,6 +264,13 @@ export interface UnifiedAnalysisState {
258
264
  * the earlier attempt's now-stale directory.
259
265
  */
260
266
  executionVideos?: Record<string, VideoRecord>;
267
+ /** Assertion-enhancement baseline + verify verdict per test file, keyed by
268
+ * CANONICAL ABSOLUTE PATH (path.resolve — unlike `reuseOutcomes`, nothing
269
+ * here matches report rows, and path keying removes basename collisions and
270
+ * alias bypasses). Written in-process by skyramp_enhance_assertions;
271
+ * skyramp_execute_test verifies against it server-side before executing.
272
+ * Never supplied by the LLM. */
273
+ assertionOutcomes?: Record<string, AssertionRecord>;
261
274
  }
262
275
  /**
263
276
  * State file metadata
@@ -0,0 +1,5 @@
1
+ import type { AssertionLanguage } from "./metrics.js";
2
+ import { type LintFinding, type LintOptions } from "./lint-types.js";
3
+ /** Checks shared by integration and provider-contract tests. Java is never
4
+ * linted (count+hash gates only). */
5
+ export declare function lintApiShared(raw: string, language: AssertionLanguage, opts?: LintOptions): LintFinding[];
@@ -0,0 +1,315 @@
1
+ import { escapeRegExp } from "../regex.js";
2
+ import { strippedSources } from "./strip-dispatch.js";
3
+ import { balancedCloseIndex, identifierRe, inScope, interpolationRe, lineOfOffset, } from "./lint-types.js";
4
+ /**
5
+ * Deterministic checks shared by integration and provider-contract tests — the
6
+ * machine-checkable slice of the D5 rubric's echo_computed and array_depth
7
+ * dims. Exact VALUE correctness (does the assertion match the recorded/sent
8
+ * value?) needs trace ground truth and stays with the LLM judge.
9
+ */
10
+ const RESPONSE_BINDING_RES = {
11
+ jsts: /(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*await\s+[\w$.]*sendRequest\s*\(/g,
12
+ python: /^[ \t]*([A-Za-z_]\w*)\s*=\s*[\w.]*send_request\s*\(/gm,
13
+ };
14
+ const STATUS_USE_RE = /^\s*\.\s*(?:statusCode|status_code|status)\b/;
15
+ /** All send_request/sendRequest bindings in the stripped source. */
16
+ function responseBindings(stripped, language) {
17
+ const bindings = [];
18
+ const re = language === "python"
19
+ ? RESPONSE_BINDING_RES.python
20
+ : RESPONSE_BINDING_RES.jsts;
21
+ re.lastIndex = 0;
22
+ let m;
23
+ while ((m = re.exec(stripped)) !== null) {
24
+ const openIdx = stripped.indexOf("(", m.index + m[0].length - 1);
25
+ const declClose = balancedCloseIndex(stripped, openIdx);
26
+ if (declClose === -1)
27
+ continue;
28
+ bindings.push({ varName: m[1], declIndex: m.index, declClose });
29
+ }
30
+ return bindings;
31
+ }
32
+ /** Statuses asserted on a var's status lines; used for the 204/202 exemption. */
33
+ function assertedStatuses(commentless, statusUseOffsets) {
34
+ const statuses = [];
35
+ for (const offset of statusUseOffsets) {
36
+ const lineEnd = commentless.indexOf("\n", offset);
37
+ const line = commentless.slice(offset, lineEnd === -1 ? undefined : lineEnd);
38
+ const code = /\b([1-5]\d{2})\b/.exec(line);
39
+ if (code)
40
+ statuses.push(Number(code[1]));
41
+ }
42
+ return statuses;
43
+ }
44
+ /** True when the binding at `idx` is declared inside a loop body — codegen's
45
+ * polling loops (`for (…) { const r = await sendRequest(…); if (r.status …) break; }`)
46
+ * bind a response that legitimately exists only to poll, and the remediation
47
+ * ("assert after the status assertion") points at a place that is out of
48
+ * scope after the loop. Poll bindings are exempt. */
49
+ function isInsideLoop(stripped, idx, language) {
50
+ if (language === "python") {
51
+ const upto = stripped.slice(0, idx);
52
+ const lineStart = upto.lastIndexOf("\n") + 1;
53
+ let threshold = (/^[ \t]*/.exec(stripped.slice(lineStart)) ?? [""])[0]
54
+ .length;
55
+ const lines = upto.slice(0, lineStart).split("\n");
56
+ for (let i = lines.length - 1; i >= 0; i--) {
57
+ const line = lines[i];
58
+ if (line.trim() === "")
59
+ continue;
60
+ const indent = (/^[ \t]*/.exec(line) ?? [""])[0].length;
61
+ if (indent >= threshold)
62
+ continue;
63
+ if (/^[ \t]*(?:for|while)\b/.test(line))
64
+ return true;
65
+ threshold = indent;
66
+ if (threshold === 0)
67
+ return false;
68
+ }
69
+ return false;
70
+ }
71
+ const blockIsLoop = [];
72
+ for (let i = 0; i < idx; i++) {
73
+ const c = stripped[i];
74
+ if (c === "{") {
75
+ const head = stripped.slice(Math.max(0, i - 160), i);
76
+ blockIsLoop.push(/\b(?:for|while)\s*\([^{]*\)\s*$/.test(head) || /\bdo\s*$/.test(head));
77
+ }
78
+ else if (c === "}") {
79
+ blockIsLoop.pop();
80
+ }
81
+ }
82
+ return blockIsLoop.some(Boolean);
83
+ }
84
+ // The SDK's own status helper: `checkStatusCode(r, "200")` passes the response
85
+ // as a call argument, but it is a STATUS use, not a body read.
86
+ const STATUS_HELPER_BEFORE_RE = /check_?status_?code\s*\(\s*$/i;
87
+ /** Response bindings whose every use is a status read (or that are never used).
88
+ * Any other read — getValue/get_response_value/checkSchema/.json()/passed as a
89
+ * call argument, including inside a template-literal or f-string interpolation
90
+ * — clears it. Exempt: DELETE calls, sole 204/202 statuses, poll bindings
91
+ * inside loops. Severity follows the rubric: unconditional (`hard`) for
92
+ * 4xx/5xx — an error body must always be asserted — and advisory (`warn`)
93
+ * when only 2xx statuses are asserted, because a 200 can legitimately carry
94
+ * no body (/logout, /cancel) and that is not deterministically knowable. */
95
+ function lintStatusOnlyResponses(stripped, commentless, language, bindings) {
96
+ const findings = [];
97
+ for (const { varName, declIndex, declClose } of bindings) {
98
+ // DELETE exemption: the request itself is a DELETE (args read from the
99
+ // comment-stripped raw so the method string literal is visible; the
100
+ // enum spelling `method: Method.DELETE` counts too).
101
+ const openIdx = stripped.indexOf("(", declIndex);
102
+ const declArgs = commentless.slice(openIdx, declClose + 1);
103
+ if (/['"]delete['"]|method\s*[:=]\s*[\w.]*\bDELETE\b/i.test(declArgs)) {
104
+ continue;
105
+ }
106
+ if (isInsideLoop(stripped, declIndex, language))
107
+ continue;
108
+ const after = stripped.slice(declClose + 1);
109
+ const refRe = identifierRe(varName, "g");
110
+ let ref;
111
+ let statusUses = 0;
112
+ let otherUses = 0;
113
+ const statusUseOffsets = [];
114
+ while ((ref = refRe.exec(after)) !== null) {
115
+ // Bounded window: STATUS_USE_RE is anchored and needs ~20 chars, so
116
+ // slicing to EOF per reference was O(n × references).
117
+ const tail = after.slice(ref.index + varName.length, ref.index + varName.length + 40);
118
+ const before = after.slice(Math.max(0, ref.index - 24), ref.index);
119
+ if (STATUS_USE_RE.test(tail) || STATUS_HELPER_BEFORE_RE.test(before)) {
120
+ statusUses++;
121
+ statusUseOffsets.push(declClose + 1 + ref.index);
122
+ }
123
+ else {
124
+ otherUses++;
125
+ }
126
+ }
127
+ // Template-literal `${...}` and python f-string `{...}` interpolations are
128
+ // blanked wholesale by the strippers, so a use like
129
+ // `sendRequest(\`/orders/\${getValue(resp, "id")}\`)` is invisible above.
130
+ // Count interpolation reads from the comment-stripped source.
131
+ if (otherUses === 0 &&
132
+ interpolationRe(varName, language).test(commentless.slice(declClose + 1))) {
133
+ otherUses++;
134
+ }
135
+ if (otherUses > 0)
136
+ continue;
137
+ const statuses = assertedStatuses(commentless, statusUseOffsets);
138
+ if (statuses.length > 0 && statuses.every((s) => s === 204 || s === 202)) {
139
+ continue;
140
+ }
141
+ // Never-used responses and asserted error statuses are unconditional
142
+ // violations; a 2xx-only status check is advisory (empty-body 200s exist).
143
+ const severity = statusUses === 0 || statuses.some((s) => s >= 400) ? "hard" : "warn";
144
+ findings.push({
145
+ rule: "status-only-response",
146
+ severity,
147
+ line: lineOfOffset(stripped, declIndex),
148
+ message: statusUses > 0
149
+ ? severity === "hard"
150
+ ? `Error response \`${varName}\` is only checked for its status code — the error body (code/message/detail) must be asserted.`
151
+ : `Response \`${varName}\` is only checked for its status code — if this endpoint returns a body, assert its fields (skip only for genuinely empty-body responses).`
152
+ : `Response \`${varName}\` is captured but never asserted.`,
153
+ remediation: language === "python"
154
+ ? `Assert body fields via \`skyramp.get_response_value(${varName}, "json.path")\` after the status assertion.`
155
+ : `Assert body fields via \`getValue(${varName}, "json.path")\` after the status assertion.`,
156
+ });
157
+ }
158
+ return findings;
159
+ }
160
+ /** Dict-subscript access on a response binding (`r["field"]`) — the rules
161
+ * mandate the SDK helper for every body read; subscript access bypasses the
162
+ * helper's null-safe path handling. Near-zero false positives: response
163
+ * objects are not lists/dicts, so a legitimate subscript on one is not a real
164
+ * pattern. */
165
+ function lintSdkHelperBypass(stripped, bindings, language) {
166
+ const findings = [];
167
+ for (const { varName, declClose } of bindings) {
168
+ const after = stripped.slice(declClose + 1);
169
+ const subscriptRe = new RegExp(`(?<![\\w$])${escapeRegExp(varName)}\\s*\\[`, "g");
170
+ const m = subscriptRe.exec(after);
171
+ if (m) {
172
+ findings.push({
173
+ rule: "sdk-helper-bypass",
174
+ severity: "hard",
175
+ line: lineOfOffset(stripped, declClose + 1 + m.index),
176
+ message: `Response \`${varName}\` is read via dict/subscript access instead of the SDK helper.`,
177
+ remediation: language === "python"
178
+ ? `Use \`skyramp.get_response_value(${varName}, "json.path")\` — never \`${varName}[...]\`.`
179
+ : `Use \`getValue(${varName}, "json.path")\` — never \`${varName}[...]\`.`,
180
+ });
181
+ }
182
+ }
183
+ return findings;
184
+ }
185
+ // Exactly the permissive forms the rules name — anything cleverer is not chased.
186
+ const PERMISSIVE_STATUS_RES = [
187
+ /toBeGreaterThanOrEqual\s*\(\s*200\s*\)/,
188
+ /toMatch\s*\(\s*\/\^?2/,
189
+ /\b200\s*<=\s*[\w.]*status(?:_code|Code)?\b/,
190
+ /str\s*\(\s*[\w.]*status_code\s*\)\s*\.\s*startswith\s*\(\s*['"]2/,
191
+ // Only WILDCARD forms ('20x', '2xx') are permissive — the SDK documents
192
+ // '"200" matches exactly 200', so a fully numeric literal is an exact match.
193
+ /check_?status_?code\s*\([^)]*['"][0-9x]*x[0-9x]*['"]/i,
194
+ ];
195
+ const STATUS_MENTION_RE = /statusCode|status_code|\.status\b|check_?status/i;
196
+ function lintPermissiveStatusMatchers(commentless, opts) {
197
+ const findings = [];
198
+ const lines = commentless.split("\n");
199
+ for (let i = 0; i < lines.length; i++) {
200
+ const line = lines[i];
201
+ if (!STATUS_MENTION_RE.test(line))
202
+ continue;
203
+ if (PERMISSIVE_STATUS_RES.some((re) => re.test(line)) &&
204
+ inScope(i + 1, opts)) {
205
+ findings.push({
206
+ rule: "permissive-status-matcher",
207
+ severity: "hard",
208
+ line: i + 1,
209
+ message: "Permissive status matcher — the exact recorded status is knowable.",
210
+ remediation: "Assert the exact status code from the recorded trace or expected_response_body (e.g. `.toBe(201)` / `== 201`).",
211
+ });
212
+ }
213
+ }
214
+ return findings;
215
+ }
216
+ const SHAPE_ONLY_RE = /Array\.isArray\s*\(|typeof\s+[\w.[\]]+\s*===|instanceof\s+Array|Object\.keys\s*\([^)]*\)\s*\.length|isinstance\s*\(/;
217
+ function lintShapeOnlyChecks(commentless) {
218
+ const findings = [];
219
+ const lines = commentless.split("\n");
220
+ for (let i = 0; i < lines.length; i++) {
221
+ if (SHAPE_ONLY_RE.test(lines[i])) {
222
+ findings.push({
223
+ rule: "shape-only-check",
224
+ severity: "warn",
225
+ line: i + 1,
226
+ message: "Shape/type-only check — fine as a companion, not as the sole validation.",
227
+ remediation: "Ensure the response's actual values are also asserted exactly (fields, lengths, per-item values).",
228
+ });
229
+ }
230
+ }
231
+ return findings;
232
+ }
233
+ // JSON paths read through the SDK helpers, e.g. getValue(r, "items.0.id").
234
+ const SDK_PATH_RE = /(?:getValue|getResponseValue|get_response_value)\s*\(\s*[\w.]+\s*,\s*['"]([^'"]+)['"]/g;
235
+ // Length reads on an SDK-helper result: `getValue(r, "items").length` or
236
+ // `len(skyramp.get_response_value(r, "items"))`.
237
+ const JS_LENGTH_RE = /(?:getValue|getResponseValue)\s*\(\s*[\w.]+\s*,\s*['"]([^'"]+)['"]\s*\)\s*\.\s*length/g;
238
+ const PY_LENGTH_RE = /len\s*\(\s*[\w.]*get_response_value\s*\(\s*[\w.]+\s*,\s*['"]([^'"]+)['"]/g;
239
+ /** Array-depth advisories (warn-only — array emptiness/size is trace-dependent):
240
+ * - length asserted for a path but no per-item field asserted under it
241
+ * - per-item assertions exist but the next index is never asserted absent */
242
+ function lintArrayDepth(commentless) {
243
+ const findings = [];
244
+ const paths = [];
245
+ SDK_PATH_RE.lastIndex = 0;
246
+ let m;
247
+ while ((m = SDK_PATH_RE.exec(commentless)) !== null)
248
+ paths.push(m[1]);
249
+ const lengthPaths = new Set();
250
+ for (const re of [JS_LENGTH_RE, PY_LENGTH_RE]) {
251
+ re.lastIndex = 0;
252
+ while ((m = re.exec(commentless)) !== null)
253
+ lengthPaths.add(m[1]);
254
+ }
255
+ // Length-only: a counted array with no per-item content assertion under it.
256
+ for (const arrayPath of lengthPaths) {
257
+ const hasItemRead = paths.some((p) => p.startsWith(`${arrayPath}.`));
258
+ if (!hasItemRead) {
259
+ findings.push({
260
+ rule: "array-length-only",
261
+ severity: "warn",
262
+ message: `Array \`${arrayPath}\` is only checked for its length — no per-item field is asserted.`,
263
+ remediation: `Assert key fields on each recorded item (e.g. "${arrayPath}.0.<field>") in addition to the length.`,
264
+ });
265
+ }
266
+ }
267
+ // Indexed item-content reads (`items.N.field`) without a next-index-absent
268
+ // assertion (a bare `items.N+1` read). Scalar arrays (bare-index-only reads)
269
+ // are skipped — a bare index there is a content assertion, not an absence
270
+ // check, and the two are indistinguishable without parsing matchers.
271
+ const contentMaxByPath = new Map();
272
+ const barePaths = new Set();
273
+ for (const p of paths) {
274
+ const withField = /^(.+)\.(\d+)\./.exec(p);
275
+ if (withField) {
276
+ const base = withField[1];
277
+ const n = Number(withField[2]);
278
+ contentMaxByPath.set(base, Math.max(contentMaxByPath.get(base) ?? -1, n));
279
+ continue;
280
+ }
281
+ if (/^.+\.\d+$/.test(p))
282
+ barePaths.add(p);
283
+ }
284
+ for (const [base, maxIdx] of contentMaxByPath) {
285
+ const nextPath = `${base}.${maxIdx + 1}`;
286
+ if (!barePaths.has(nextPath)) {
287
+ findings.push({
288
+ rule: "array-missing-next-index-check",
289
+ severity: "warn",
290
+ message: `Items of \`${base}\` are asserted up to index ${maxIdx}, but index ${maxIdx + 1} is never asserted absent.`,
291
+ remediation: `Add an absence assertion for the index after the last expected item (e.g. \`getValue(response, "${nextPath}")\` is undefined / None).`,
292
+ });
293
+ }
294
+ }
295
+ return findings;
296
+ }
297
+ /** Checks shared by integration and provider-contract tests. Java is never
298
+ * linted (count+hash gates only). */
299
+ export function lintApiShared(raw, language, opts) {
300
+ if (language === "java")
301
+ return [];
302
+ const maintenance = opts?.scopeLines !== undefined;
303
+ const { commentless, stripped } = strippedSources(raw, language);
304
+ const findings = [];
305
+ if (!maintenance) {
306
+ // Bindings discovered once, shared by both binding-scoped lints.
307
+ const bindings = responseBindings(stripped, language);
308
+ findings.push(...lintStatusOnlyResponses(stripped, commentless, language, bindings));
309
+ findings.push(...lintSdkHelperBypass(stripped, bindings, language));
310
+ findings.push(...lintShapeOnlyChecks(commentless));
311
+ findings.push(...lintArrayDepth(commentless));
312
+ }
313
+ findings.push(...lintPermissiveStatusMatchers(commentless, opts));
314
+ return findings;
315
+ }
@@ -0,0 +1,3 @@
1
+ import type { AssertionLanguage } from "./metrics.js";
2
+ import { type LintFinding, type LintOptions } from "./lint-types.js";
3
+ export declare function lintContractTest(raw: string, language: AssertionLanguage, opts?: LintOptions): LintFinding[];
@@ -0,0 +1,87 @@
1
+ import { strippedSources } from "./strip-dispatch.js";
2
+ import { balancedCloseIndex, identifierRe, lineOfOffset, } from "./lint-types.js";
3
+ import { lintApiShared } from "./api-shared-lints.js";
4
+ /**
5
+ * Provider-contract lints: the shared API checks plus the machine-checkable
6
+ * slice of the D5 `value_sources` dim — contract assertions must use inline
7
+ * request values, never data provisioned in beforeAll/beforeEach.
8
+ */
9
+ const SETUP_BLOCK_RE = /\b(?:beforeAll|beforeEach)\s*\(/g;
10
+ const DECL_RE = /(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=/g;
11
+ /** Identifiers assigned inside beforeAll/beforeEach blocks and the block
12
+ * ranges themselves (JS/TS only — python contract tests use pytest fixtures,
13
+ * out of scope here). The ranges matter: codegen's own provisioning status
14
+ * checks live INSIDE the setup blocks and reference these names legitimately
15
+ * — only TEST-BODY assertions using setup data violate the rubric (measured:
16
+ * 7 of 8 pristine provider-contract goldens false-warned without this). */
17
+ function setupDeclarations(stripped) {
18
+ const names = new Set();
19
+ const ranges = [];
20
+ SETUP_BLOCK_RE.lastIndex = 0;
21
+ let m;
22
+ while ((m = SETUP_BLOCK_RE.exec(stripped)) !== null) {
23
+ const close = balancedCloseIndex(stripped, m.index + m[0].length - 1);
24
+ if (close === -1)
25
+ continue;
26
+ ranges.push([m.index, close]);
27
+ const block = stripped.slice(m.index, close + 1);
28
+ DECL_RE.lastIndex = 0;
29
+ let decl;
30
+ while ((decl = DECL_RE.exec(block)) !== null)
31
+ names.add(decl[1]);
32
+ }
33
+ return { names, ranges };
34
+ }
35
+ /** An assertion references a value provisioned in beforeAll/beforeEach.
36
+ * Warn-only: block-scoped `const` inside beforeAll is not reachable from a
37
+ * test body in real code, so hits are usually closure vars — a strong smell,
38
+ * but shared infra handles (clients, tokens) are legitimate closure vars too. */
39
+ function lintSetupDataInAssertions(stripped) {
40
+ const { names, ranges } = setupDeclarations(stripped);
41
+ if (names.size === 0)
42
+ return [];
43
+ // Compile once per name, not per (site × name).
44
+ const nameRes = new Map([...names].map((name) => [name, identifierRe(name)]));
45
+ const findings = [];
46
+ const expectRe = /\bexpect(?:\s*\.\s*soft)?\s*\(/g;
47
+ let m;
48
+ while ((m = expectRe.exec(stripped)) !== null) {
49
+ // Setup-internal assertions (provisioning status checks) are codegen's
50
+ // own sanctioned shape — only test-body references are the violation.
51
+ const at = m.index;
52
+ if (ranges.some(([start, end]) => at >= start && at <= end))
53
+ continue;
54
+ const close = balancedCloseIndex(stripped, m.index + m[0].length - 1);
55
+ if (close === -1)
56
+ continue;
57
+ // The whole statement line, so matcher arguments count too.
58
+ const lineEnd = stripped.indexOf("\n", close);
59
+ const stmt = stripped.slice(m.index, lineEnd === -1 ? undefined : lineEnd);
60
+ for (const [name, re] of nameRes) {
61
+ if (re.test(stmt)) {
62
+ findings.push({
63
+ rule: "setup-data-in-assertion",
64
+ severity: "warn",
65
+ line: lineOfOffset(stripped, m.index),
66
+ message: `Assertion references \`${name}\`, which is provisioned in beforeAll/beforeEach.`,
67
+ remediation: "Contract assertions must use values from the inline request body, path, query, or expected_response_body — not setup-provisioned data.",
68
+ });
69
+ break;
70
+ }
71
+ }
72
+ }
73
+ return findings;
74
+ }
75
+ export function lintContractTest(raw, language, opts) {
76
+ // Java is count+hash gated only (a documented limit of the verifier): the
77
+ // lint regexes and strength weights are written for the python/ts/js SDK
78
+ // helper idioms, and applying them to JUnit source would produce false
79
+ // findings, not protection. Same guard as helper-imports.ts.
80
+ if (language === "java")
81
+ return [];
82
+ const findings = lintApiShared(raw, language, opts);
83
+ if (opts?.scopeLines === undefined && language !== "python") {
84
+ findings.push(...lintSetupDataInAssertions(strippedSources(raw, language).stripped));
85
+ }
86
+ return findings;
87
+ }
@@ -0,0 +1,5 @@
1
+ import type { AssertionVerifyResult } from "./verify.js";
2
+ /** Renders the verify result as the text returned to the agent — PASSED/FAILED
3
+ * with one actionable remediation per violation, in the style of the reuse
4
+ * verify report (verify-gates.ts). */
5
+ export declare function formatAssertionVerifyReport(r: AssertionVerifyResult, testFile: string): string;
@@ -0,0 +1,65 @@
1
+ import * as path from "path";
2
+ import { detectAssertionLanguage } from "./metrics.js";
3
+ import { markerCommentToken } from "./marker.js";
4
+ /** Renders the verify result as the text returned to the agent — PASSED/FAILED
5
+ * with one actionable remediation per violation, in the style of the reuse
6
+ * verify report (verify-gates.ts). */
7
+ export function formatAssertionVerifyReport(r, testFile) {
8
+ const spec = path.basename(testFile);
9
+ // Paste-ready marker lines must use THIS file's comment token — a `//`
10
+ // marker in a .py file is a syntax error.
11
+ const token = markerCommentToken(detectAssertionLanguage(testFile) ?? "typescript");
12
+ const finding = (f) => `- ${f.line !== undefined ? `line ${f.line}: ` : ""}${f.message} Fix: ${f.remediation}`;
13
+ if (r.ok) {
14
+ // Maintenance drops the strength-family gates (value-only fixes are
15
+ // legitimate), so its PASS is a weaker claim — say so, rather than letting
16
+ // the agent and the eval judge read it as a full verification.
17
+ const heading = r.enhanceType === "maintenance" && r.baselinePresent
18
+ ? r.maintenanceNoAssertionChanges
19
+ ? `ASSERTION MAINTENANCE CHECK PASSED — ${spec}: no assertion changes since hand-out; nothing to verify.`
20
+ : `ASSERTION MAINTENANCE CHECK PASSED — ${spec}: ${r.currentMetrics?.count ?? "?"} assertion sites (differential strength gates are not applicable in maintenance; added/changed assertions were linted and removals guarded).`
21
+ : `ASSERTION VERIFICATION PASSED — ${spec}: ${r.currentMetrics?.count ?? "?"} assertion sites` +
22
+ (r.strengthDelta !== undefined && r.strengthDelta > 0
23
+ ? `, strength +${r.strengthDelta} over the generated baseline.`
24
+ : ".");
25
+ const lines = [heading];
26
+ if (r.replacedCount !== undefined && r.replacedCount > 0) {
27
+ lines.push(`${r.replacedCount} generated assertion${r.replacedCount === 1 ? "" : "s"} replaced on the same subject — accepted.`);
28
+ }
29
+ if (r.movedToHelperCount !== undefined && r.movedToHelperCount > 0) {
30
+ lines.push(`${r.movedToHelperCount} assertion${r.movedToHelperCount === 1 ? "" : "s"} now covered by an imported helper file — accepted.`);
31
+ }
32
+ if (r.markerReason !== undefined) {
33
+ lines.push(`Documented decline accepted: ${r.markerReason}`);
34
+ }
35
+ if (!r.baselinePresent) {
36
+ lines.push("No enhancement baseline was recorded for this file, so only the stateless checks (rubric lints, markers) ran.");
37
+ }
38
+ return [...lines, ...warningsBlock(r.warnings, finding)].join("\n");
39
+ }
40
+ const lines = [`ASSERTION VERIFICATION FAILED — ${spec}:`];
41
+ if (r.hashUnchanged) {
42
+ lines.push("- The file is byte-identical to when the enhancement instructions were handed out — nothing was applied. Read the file, apply the instructions, and re-verify.");
43
+ }
44
+ if (r.removedFingerprints !== undefined) {
45
+ lines.push(`- Assertions were REMOVED — their subjects are asserted neither in this file nor in any helper file it imports. Replacing an assertion with a stronger one on the same subject is fine — losing coverage is not, and a documented decline does NOT clear this. Restore the assertions in THIS spec file${r.baselineFilePath !== undefined ? ` (the generated baseline is preserved at ${r.baselineFilePath})` : ""}. Do NOT edit shared helper/utils files — they are imported by other tests.`, r.removedCount !== undefined &&
46
+ r.removedCount > r.removedFingerprints.length
47
+ ? ` Missing (showing ${r.removedFingerprints.length} of ${r.removedCount} — restore ALL of them in this pass):`
48
+ : " Missing:", ...r.removedFingerprints.map((fp) => ` ${fp}`));
49
+ }
50
+ lines.push(...r.hardFindings.map(finding));
51
+ if (r.strengthGateFailed) {
52
+ lines.push("- Assertion strength did not increase over the generated baseline: no assertion was added and no weak matcher was strengthened (e.g. `toBeVisible` → `toHaveText`). Apply the enhancement rules. A documented decline (`assertions complete` comment) is accepted only AFTER at least one assertion was added or replaced — a marker-only edit does not pass this gate.");
53
+ }
54
+ if (r.weakAdditionsOnly) {
55
+ lines.push("- Every assertion added since the baseline is existence/visibility-tier (`toBeVisible`, `not.toBeNull`, `is not None`, ranges). When the exact text/value/count is knowable from the trace, source, or request, assert it exactly (`toHaveText`/`toHaveCount`/`toBe`/`==`). If only existence is genuinely knowable, document the decline:", ` ${token} assertions complete: ${spec} — <reason exact values are not knowable>`);
56
+ }
57
+ lines.push(...warningsBlock(r.warnings, finding));
58
+ lines.push("Re-run this verification (verify: true) after every fix pass — skyramp_execute_test runs the same checks automatically and will not execute this test until they pass.");
59
+ return lines.join("\n");
60
+ }
61
+ function warningsBlock(warnings, finding) {
62
+ if (warnings.length === 0)
63
+ return [];
64
+ return ["WARNINGS (informational — do not block):", ...warnings.map(finding)];
65
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Assertion SUBJECTS found in the spec's locally imported, actually-called
3
+ * helper files. Strict-by-default failure direction: an unreadable/oversized/
4
+ * uncalled helper is skipped, so its subjects don't excuse anything.
5
+ */
6
+ export declare function importedHelperSubjects(testFile: string, content: string): Promise<Set<string>>;