@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,375 @@
1
+ import * as path from "path";
2
+ import { realpathSync } from "fs";
3
+ import { mkdir, readFile, rm, writeFile } from "fs/promises";
4
+ import { logger } from "../../utils/logger.js";
5
+ import { resolveRunStatePath, StateManager, } from "../../utils/AnalysisStateManager.js";
6
+ import { computeAssertionMetrics, detectAssertionLanguage, formatAssertionVerifyReport, sha256Of, verifyAssertionEnhancement, } from "../../utils/assertion-verify/index.js";
7
+ /**
8
+ * Persistence for the assertion-enhancement verification loop, keyed by the
9
+ * spec's CANONICAL absolute path in `UnifiedAnalysisState.assertionOutcomes`.
10
+ * Path keying (not basename, unlike reuseOutcomes — nothing here needs to
11
+ * match report rows) removes the multi-repo basename-collision class outright,
12
+ * and canonicalization closes the alias bypass: `dir//x.spec.ts` and
13
+ * `dir/./x.spec.ts` must hit the same record as `dir/x.spec.ts`.
14
+ *
15
+ * Every write is best-effort: verification must never be able to fail an
16
+ * enhancement that otherwise succeeded, so a missing/unreadable state file
17
+ * degrades to stateless checks and open gates — always with a warning.
18
+ */
19
+ /** Canonical identity of a test file — all keying and ownership comparisons
20
+ * go through this so a non-normalized agent-supplied path cannot fork or
21
+ * bypass a record. realpath also collapses symlinked directories; a path
22
+ * that does not (yet) exist falls back to plain resolution. */
23
+ export function canonicalTestPath(testFile) {
24
+ const resolved = path.resolve(testFile);
25
+ try {
26
+ return realpathSync(resolved);
27
+ }
28
+ catch {
29
+ return resolved;
30
+ }
31
+ }
32
+ /** Read-modify-write one spec's record. The patch sees the existing record
33
+ * (or undefined) and returns the COMPLETE next record — or undefined to skip
34
+ * the write (e.g. a verdict for a record that does not exist). Returns true
35
+ * only when the record was actually written. */
36
+ async function updateAssertionOutcome(testFile, patch, explicitStateFile,
37
+ // State the caller already read for THIS file — skips the re-read (each
38
+ // gate call was re-parsing the same run-state JSON up to 8 times).
39
+ preloaded) {
40
+ const stateFile = resolveRunStatePath(explicitStateFile);
41
+ if (!stateFile)
42
+ return false;
43
+ try {
44
+ const manager = StateManager.fromStatePath(stateFile);
45
+ const data = preloaded ?? (await manager.readData());
46
+ if (!data) {
47
+ // A resolvable path is not proof of a run (this repo's own CI sets
48
+ // RUNNER_TEMP): the state file simply doesn't exist. Say so — silently
49
+ // returning here disables the whole gate for the run with no trace.
50
+ logger.warning("Run state file not found — assertion-enhancement state not recorded, verification gate inactive", { stateFile, testFile });
51
+ return false;
52
+ }
53
+ const key = canonicalTestPath(testFile);
54
+ const next = patch(data.assertionOutcomes?.[key]);
55
+ if (next === undefined)
56
+ return false;
57
+ await manager.writeData({
58
+ ...data,
59
+ assertionOutcomes: {
60
+ ...data.assertionOutcomes,
61
+ [key]: next,
62
+ },
63
+ });
64
+ return true;
65
+ }
66
+ catch (err) {
67
+ logger.warning("Could not record assertion-enhancement state", {
68
+ testFile,
69
+ error: String(err),
70
+ });
71
+ return false;
72
+ }
73
+ }
74
+ /** The state files a record may live in: the explicitly supplied one first,
75
+ * then the run-default path. Callers thread `stateFile` inconsistently across
76
+ * tools (enhance may record to the default while execute checks an explicit
77
+ * path, or vice versa) — reading both closes the asymmetry that otherwise
78
+ * fails the gate open on a mismatched `stateFile`. */
79
+ function candidateStateFiles(explicitStateFile) {
80
+ const explicit = resolveRunStatePath(explicitStateFile);
81
+ const fallback = resolveRunStatePath(undefined);
82
+ return [...new Set([explicit, fallback])].filter((p) => p !== undefined);
83
+ }
84
+ /** The record and the state file it actually lives in, or undefined. */
85
+ async function locateAssertionRecord(testFile, explicitStateFile) {
86
+ for (const stateFile of candidateStateFiles(explicitStateFile)) {
87
+ try {
88
+ const data = await StateManager.fromStatePath(stateFile).readData();
89
+ const record = data?.assertionOutcomes?.[canonicalTestPath(testFile)];
90
+ if (data && record !== undefined)
91
+ return { record, stateFile, data };
92
+ }
93
+ catch (err) {
94
+ logger.warning("Could not read a candidate run state file for the assertion record — trying the next", { stateFile, testFile, error: String(err) });
95
+ }
96
+ }
97
+ return undefined;
98
+ }
99
+ /** A record only counts for `testFile` when its stored path canonicalizes to
100
+ * the same spec — the single home for the ownership invariant (previously
101
+ * copied in baselineFrom, recordAssertionVerdict, and the enhance tool). */
102
+ export function trustedRecord(record, testFile) {
103
+ return record !== undefined &&
104
+ canonicalTestPath(record.testFilePath) === canonicalTestPath(testFile)
105
+ ? record
106
+ : undefined;
107
+ }
108
+ /** One spec's record, or undefined when there is no run state / no record. */
109
+ export async function readAssertionRecord(testFile, explicitStateFile) {
110
+ return (await locateAssertionRecord(testFile, explicitStateFile))?.record;
111
+ }
112
+ /**
113
+ * Snapshot the file at instruction hand-out time — the baseline verify measures
114
+ * against. Keep-first rule: while no Passed verdict exists for the same file and
115
+ * enhanceType, a repeat hand-out does NOT re-baseline (re-calling the tool after
116
+ * half-editing must not shrink the required delta). A hand-out after a Passed
117
+ * verdict, or with a different enhanceType, starts a new enhancement round.
118
+ */
119
+ export async function recordAssertionBaseline(testFile, testType, enhanceType, explicitStateFile) {
120
+ const stateFile = resolveRunStatePath(explicitStateFile);
121
+ if (!stateFile)
122
+ return;
123
+ try {
124
+ // Probe the state file BEFORE writing the baseline copy: a resolvable path
125
+ // with no state file (RUNNER_TEMP set, no analyze-changes run) must not
126
+ // leave an orphan copy behind while the record write silently no-ops.
127
+ const data = await StateManager.fromStatePath(stateFile).readData();
128
+ if (!data) {
129
+ logger.warning("Run state file not found — assertion baseline not recorded, verification gate inactive for this file", { stateFile, testFile });
130
+ return;
131
+ }
132
+ const canonical = canonicalTestPath(testFile);
133
+ const existing = data.assertionOutcomes?.[canonical];
134
+ if (existing?.fileSha256 !== undefined &&
135
+ existing.verification !== "passed" &&
136
+ existing.enhanceType === enhanceType) {
137
+ return;
138
+ }
139
+ const content = await readFile(testFile, "utf8");
140
+ const sha = sha256Of(content);
141
+ const language = detectAssertionLanguage(testFile);
142
+ const metrics = language
143
+ ? computeAssertionMetrics(content, language)
144
+ : undefined;
145
+ const baselineFilePath = await saveBaselineCopy(stateFile, canonical, sha, content);
146
+ const written = await updateAssertionOutcome(testFile,
147
+ // Full replacement — a new enhancement round resets any earlier verdict.
148
+ // Only executionCount survives: executions already happened regardless
149
+ // of which round's instructions were in force.
150
+ (previous) => ({
151
+ testFilePath: canonical,
152
+ testType,
153
+ executionCount: previous?.executionCount ?? 0,
154
+ enhanceType,
155
+ fileSha256: sha,
156
+ baselineFilePath,
157
+ baselineCount: metrics?.count,
158
+ baselineStrength: metrics?.strengthScore,
159
+ baselineFingerprints: metrics?.fingerprints,
160
+ issuedAt: new Date().toISOString(),
161
+ }), explicitStateFile, data);
162
+ // A failed record write must not strand an orphan copy nobody references.
163
+ if (!written && baselineFilePath !== undefined) {
164
+ await rm(baselineFilePath, { force: true }).catch(() => { });
165
+ }
166
+ }
167
+ catch (err) {
168
+ logger.warning("Could not record assertion baseline — verification will run stateless checks only", { testFile, error: String(err) });
169
+ }
170
+ }
171
+ /**
172
+ * Preserve the generated file as delivered at hand-out time, beside the run
173
+ * state (never in the repo — a repo sidecar could leak into the customer
174
+ * commit and would need its own cleanup step, like the reuse flow's .raw.bak).
175
+ * The sha prefix disambiguates basename collisions across repos and rounds.
176
+ * Best-effort: an unsaved copy costs the diff/restore aid, not the baseline.
177
+ */
178
+ async function saveBaselineCopy(stateFile, testFile, sha, content) {
179
+ try {
180
+ const dir = path.join(path.dirname(stateFile), "assertion-baselines");
181
+ await mkdir(dir, { recursive: true });
182
+ const copyPath = path.join(dir, `${sha.slice(0, 8)}-${path.basename(testFile)}`);
183
+ await writeFile(copyPath, content, "utf8");
184
+ return copyPath;
185
+ }
186
+ catch (err) {
187
+ logger.warning("Could not save the assertion baseline copy", {
188
+ testFile,
189
+ error: String(err),
190
+ });
191
+ return undefined;
192
+ }
193
+ }
194
+ /** The baseline verify measures against, from a record; undefined when the
195
+ * record carries no snapshot or belongs to a different canonical path. */
196
+ export function baselineFrom(record, testFile) {
197
+ if (record?.fileSha256 === undefined)
198
+ return undefined;
199
+ if (trustedRecord(record, testFile) === undefined)
200
+ return undefined;
201
+ return {
202
+ fileSha256: record.fileSha256,
203
+ count: record.baselineCount,
204
+ strengthScore: record.baselineStrength,
205
+ fingerprints: record.baselineFingerprints,
206
+ baselineFilePath: record.baselineFilePath,
207
+ };
208
+ }
209
+ /** Record what one verify pass established. The verdict is written to the
210
+ * state file the record actually LIVES in (not blindly to the explicit path —
211
+ * a mismatched `stateFile` would otherwise split verdict from baseline).
212
+ * Never writes onto a record owned by a different canonical path, and never
213
+ * fails silently. */
214
+ export async function recordAssertionVerdict(testFile, result, explicitStateFile) {
215
+ let home;
216
+ let homeData;
217
+ try {
218
+ const located = await locateAssertionRecord(testFile, explicitStateFile);
219
+ home = located?.stateFile;
220
+ homeData = located?.data;
221
+ if (located !== undefined &&
222
+ trustedRecord(located.record, testFile) === undefined) {
223
+ logger.warning("Assertion verdict not recorded — the record is owned by a different spec path", { testFile, recordedPath: located.record.testFilePath });
224
+ return;
225
+ }
226
+ }
227
+ catch (err) {
228
+ logger.warning("Assertion verdict not recorded — could not read the run state", { testFile, error: String(err) });
229
+ return;
230
+ }
231
+ await updateAssertionOutcome(testFile,
232
+ // A verdict amends an existing record; without one there is nothing the
233
+ // verdict is ABOUT (verify-with-no-baseline records a baseline instead,
234
+ // never a verdict), so skip the write rather than invent a partial record.
235
+ (existing) => existing === undefined
236
+ ? undefined
237
+ : {
238
+ ...existing,
239
+ verification: result.ok ? "passed" : "failed",
240
+ declinedReason: result.markerReason,
241
+ }, home ?? explicitStateFile, homeData);
242
+ }
243
+ /**
244
+ * Deterministic execute-time verification: skyramp_execute_test calls this
245
+ * itself, so the check runs whether or not the agent ever called `verify: true`
246
+ * — a prose instruction can be ignored, a server-side call cannot. Returns the
247
+ * feedback text when the assertions are insufficient (the caller returns it
248
+ * instead of executing), or undefined to proceed.
249
+ *
250
+ * Self-clearing: the checks run on the file's CURRENT content, so fixing the
251
+ * assertions and re-executing passes — no separate verify call is required.
252
+ * Skipped once a verdict has PASSED: sanctioned later rewrites (a `.raw.bak`
253
+ * restore during execution fix-up) would otherwise false-fail the differential
254
+ * gates. Modularization needs no special handling: the shared-helper policy
255
+ * keeps every value assertion in the test body, so the baseline comparison
256
+ * stays valid across it. Fails open on no baseline and unreadable state/file.
257
+ */
258
+ export async function assertionFeedbackForExecution(testFile, explicitStateFile) {
259
+ try {
260
+ const record = await readAssertionRecord(testFile, explicitStateFile);
261
+ if (record === undefined)
262
+ return undefined;
263
+ const baseline = baselineFrom(record, testFile);
264
+ if (baseline === undefined)
265
+ return undefined;
266
+ if (record.verification === "passed")
267
+ return undefined;
268
+ const result = await verifyAssertionEnhancement({
269
+ testFile,
270
+ testType: record.testType,
271
+ // The baseline writer sets enhanceType with the snapshot, so a record
272
+ // with a baseline always carries it; a hand-truncated state file
273
+ // defaults to the STRICTER mode rather than deferring execution.
274
+ enhanceType: record.enhanceType ?? "generation",
275
+ baseline,
276
+ });
277
+ await recordAssertionVerdict(testFile, result, explicitStateFile);
278
+ if (result.ok)
279
+ return undefined;
280
+ return (`Execution deferred — the assertion enhancement of ${path.basename(testFile)} is insufficient. ` +
281
+ `Fix the violations below, then call skyramp_execute_test again (it re-checks the file automatically; ` +
282
+ `you can also get early feedback anytime via skyramp_enhance_assertions with verify: true).\n\n` +
283
+ formatAssertionVerifyReport(result, testFile));
284
+ }
285
+ catch (err) {
286
+ logger.warning("Could not verify assertion enhancement at execution time — proceeding", { testFile, error: String(err) });
287
+ return undefined;
288
+ }
289
+ }
290
+ /**
291
+ * Count one execution of this spec (called by skyramp_execute_test after its
292
+ * gates pass, best-effort). Creates a minimal record when none exists so an
293
+ * executed-but-never-enhanced spec is still visible to the report cross-check.
294
+ */
295
+ export async function recordAssertionExecution(testFile, testType, explicitStateFile) {
296
+ const located = await locateAssertionRecord(testFile, explicitStateFile).catch(() => undefined);
297
+ await updateAssertionOutcome(testFile, (existing) => existing === undefined
298
+ ? {
299
+ testFilePath: canonicalTestPath(testFile),
300
+ testType,
301
+ executionCount: 1,
302
+ }
303
+ : { ...existing, executionCount: existing.executionCount + 1 }, located?.stateFile ?? explicitStateFile, located?.data);
304
+ }
305
+ /** The record for a report row, matched by the canonical-path keys' basenames
306
+ * and then GUARDED: the record's testType must match the row's, and when the
307
+ * run's checkouts are known, the record's path must live in the row's repo
308
+ * (primary when the row names none). Without the guards, a basename shared
309
+ * across repos published one spec's proof-of-work under another's name — and
310
+ * the victim was exactly the never-enhanced spec the field exists to expose.
311
+ * Ambiguity after filtering still fails open: wrong attribution is worse
312
+ * than omission. */
313
+ export function findAssertionRecordByFileName(outcomes, row, checkouts) {
314
+ const base = path.basename(row.fileName);
315
+ const matches = Object.entries(outcomes).filter(([key, record]) => {
316
+ if (path.basename(key) !== base)
317
+ return false;
318
+ if (row.testType !== undefined && record.testType !== row.testType) {
319
+ return false;
320
+ }
321
+ if (checkouts.length > 0) {
322
+ const home = checkouts.find((c) => record.testFilePath.startsWith(c.root + path.sep));
323
+ // A record living in a KNOWN checkout must belong to the row's repo
324
+ // (rows without a repository belong to the primary). A record outside
325
+ // every known checkout stays eligible — fail open on the guard itself.
326
+ if (home) {
327
+ const rowIsPrimary = row.repository === undefined;
328
+ if (rowIsPrimary ? !home.primary : home.repository !== row.repository) {
329
+ return false;
330
+ }
331
+ }
332
+ }
333
+ return true;
334
+ });
335
+ return matches.length === 1 ? matches[0][1] : undefined;
336
+ }
337
+ /**
338
+ * Report-facing proof-of-work summary re-derived from the spec as delivered
339
+ * NOW — final metrics come from the file, not from verify-time state, so
340
+ * post-verify edits cannot publish stale counts. Always returns the status
341
+ * fields (verification, declinedReason, executionCount — they come from the
342
+ * record); the metric fields are omitted when the file cannot supply them.
343
+ */
344
+ export async function rederiveAssertionOutcome(record) {
345
+ const { testFilePath, verification, declinedReason } = record;
346
+ const base = {
347
+ verification: verification ?? "never-verified",
348
+ declinedReason,
349
+ executionCount: record.executionCount,
350
+ };
351
+ try {
352
+ // Metrics exist only for the languages the analyzer supports (python, ts,
353
+ // js, java). Maintained rows can point at other languages the discovery
354
+ // patterns accept (go, ruby, php) — those report status without metrics.
355
+ const language = detectAssertionLanguage(testFilePath);
356
+ if (language === undefined)
357
+ return base;
358
+ const metrics = computeAssertionMetrics(await readFile(testFilePath, "utf8"), language);
359
+ return {
360
+ ...base,
361
+ assertionsBaseline: record.baselineCount,
362
+ assertionsFinal: metrics.count,
363
+ strengthBaseline: record.baselineStrength,
364
+ strengthFinal: metrics.strengthScore,
365
+ };
366
+ }
367
+ catch (err) {
368
+ // readFile is the only throw here: the delivered spec is gone or
369
+ // unreadable at report time (deleted after execution, permissions).
370
+ // Deterministic — a retry would re-read the same missing file — so
371
+ // report the status fields and omit the metrics.
372
+ logger.warning("Could not re-derive the assertion outcome from the delivered spec — omitting metrics from the report", { testFile: testFilePath, error: String(err) });
373
+ return base;
374
+ }
375
+ }
@@ -1,3 +1,4 @@
1
+ import { stageAndRecordRetrofits } from "./retrofit-state.js";
1
2
  import { z } from "zod";
2
3
  import * as path from "path";
3
4
  import { logger } from "../../utils/logger.js";
@@ -7,9 +8,9 @@ import { isPomAwareTarget } from "../../utils/reuseRouting.js";
7
8
  import { selectScopedPoms, } from "../../utils/pom-scope/index.js";
8
9
  import { verifyReuse } from "../../utils/pom-verify/index.js";
9
10
  import { infraGateFailure, zeroReuseGateFailure, composeVerifyText, } from "./verify-gates.js";
10
- import { recordCandidates, recordNoPomLayer, recordVerifyOutcome, recordUtilsReuseIssued, recordUtilsVerifyOutcome, recordUtilsVerifyError, pendingModularization, } from "./reuse-state.js";
11
+ import { recordCandidates, recordNoPomLayer, recordVerifyOutcome, recordUtilsReuseIssued, recordUtilsVerifyOutcome, recordUtilsVerifyError, pendingModularization, utilsBaselineFor, } from "./reuse-state.js";
11
12
  import { formatUtilsVerifyText } from "./utils-verify-gates.js";
12
- import { stageUtilsArtifacts, utilsSpecFor, verifyUtils, } from "../../utils/utils-verify/index.js";
13
+ import { utilsSpecFor, verifyUtils } from "../../utils/utils-verify/index.js";
13
14
  import { codeRefactoringSchema, languageSchema, TestType, } from "../../types/TestTypes.js";
14
15
  import { SKYRAMP_UTILS_HEADER } from "../../utils/utils.js";
15
16
  import { AnalyticsService } from "../../services/AnalyticsService.js";
@@ -30,7 +31,7 @@ const codeReuseSchema = z.object({
30
31
  verify: z
31
32
  .boolean()
32
33
  .default(false)
33
- .describe("Verify a previously refactored test instead of returning the reuse prompt. POM path (browser tests): checks the test's POM calls against source; requires the discovery pass — this tool called for the same testFile without `verify` — to have run first. SkyrampUtils path (every other test type): stages the shared utils file for the output commit and checks its invariants (API helpers: one helper per method+path, status-code-only assertions, method+resource names; browser helpers: actions and structural waits only, intent names). Call it after the reuse edits are written; skyramp_enhance_assertions and skyramp_execute_test refuse until it has passed."),
34
+ .describe("Verify a previously refactored test instead of returning the reuse prompt. POM path (browser tests): checks the test's POM calls against source; requires the discovery pass — this tool called for the same testFile without `verify` — to have run first. SkyrampUtils path (every other test type): stages the shared utils file for the output commit, checks its invariants, and — when skyramp_modularization recorded a baseline for this spec — compares the delivered assertion count and page.on('pageerror') guard against that hand-out, rejecting a net loss (API helpers: one helper per method+path, status-code-only assertions, method+resource names; browser helpers: actions and structural waits only, intent names). Call it after the reuse edits are written; skyramp_enhance_assertions and skyramp_execute_test refuse until it has passed."),
34
35
  testType: z
35
36
  .nativeEnum(TestType)
36
37
  .optional()
@@ -131,11 +132,12 @@ async function runUtilsVerify(params) {
131
132
  };
132
133
  }
133
134
  try {
134
- const staging = await stageUtilsArtifacts(params.testFile, params.language);
135
+ const staging = await stageAndRecordRetrofits(params.testFile, params.language);
135
136
  const r = await verifyUtils({
136
137
  testFile: params.testFile,
137
138
  language: params.language,
138
139
  testType: params.testType,
140
+ baseline: await utilsBaselineFor(params.testFile),
139
141
  });
140
142
  // A utils file git refused to stage ships a test importing a module the output
141
143
  // commit lacks — the exact failure this gate exists for — so it fails the pass
@@ -1,9 +1,11 @@
1
1
  import { z } from "zod";
2
2
  import { pendingReuseDebt } from "./reuse-state.js";
3
- import { recordEnhanceObligation } from "./enhance-state.js";
4
- import { stageUtilsArtifacts } from "../../utils/utils-verify/index.js";
3
+ import { baselineFrom, readAssertionRecord, recordAssertionBaseline, recordAssertionVerdict, trustedRecord, } from "./assertion-state.js";
4
+ import { formatAssertionVerifyReport, verifyAssertionEnhancement, } from "../../utils/assertion-verify/index.js";
5
+ import { stageAndRecordRetrofits } from "./retrofit-state.js";
6
+ import * as path from "path";
5
7
  import { toolError } from "../../utils/utils.js";
6
- import { TestType } from "../../types/TestTypes.js";
8
+ import { ASSERTION_ENHANCEABLE_TEST_TYPES, TestType, } from "../../types/TestTypes.js";
7
9
  import { AnalyticsService } from "../../services/AnalyticsService.js";
8
10
  import { getContractProviderAssertionsPrompt } from "../../prompts/enhance-assertions/contractProviderAssertionsPrompt.js";
9
11
  import { getIntegrationAssertionsPrompt } from "../../prompts/enhance-assertions/integrationAssertionsPrompt.js";
@@ -53,7 +55,7 @@ const enhanceAssertionsSchema = {
53
55
  .string()
54
56
  .describe("Absolute path to the generated or updated test file whose assertions must be enhanced. Pass the exact file you just created or modified."),
55
57
  testType: z
56
- .enum([TestType.INTEGRATION, TestType.CONTRACT, TestType.UI])
58
+ .enum(ASSERTION_ENHANCEABLE_TEST_TYPES)
57
59
  .describe("The type of test whose assertions should be enhanced. Contract applies to provider contract tests only, not consumer contract tests."),
58
60
  enhanceType: z
59
61
  .enum(["generation", "maintenance"])
@@ -66,6 +68,18 @@ const enhanceAssertionsSchema = {
66
68
  .describe("When true, returns a compact instruction set (10 lines) that tells the agent to " +
67
69
  "read the test file, enhance assertions per standard rules, and write it back directly. " +
68
70
  "When false (default), returns the full verbose instruction set."),
71
+ verify: z
72
+ .boolean()
73
+ .default(false)
74
+ .describe("Set true AFTER applying the enhancement instructions to deterministically verify " +
75
+ "they were applied (assertions added or strengthened, rubric rules followed). " +
76
+ "Returns VERIFICATION PASSED or FAILED with per-violation remediations. " +
77
+ "Use the same testFile, testType, and enhanceType as the instruction call."),
78
+ stateFile: z
79
+ .string()
80
+ .optional()
81
+ .describe("Absolute path to the run state file, when one was provided by the workflow. " +
82
+ "Optional — in CI the run state is resolved automatically."),
69
83
  };
70
84
  export function registerEnhanceAssertionsTool(server) {
71
85
  server.registerTool(TOOL_NAME, {
@@ -73,36 +87,77 @@ export function registerEnhanceAssertionsTool(server) {
73
87
 
74
88
  ### When to call
75
89
  - After generating a new supported test file
76
- - After updating an existing supported test file during maintenance`,
90
+ - After updating an existing supported test file during maintenance
91
+ - Optionally with \`verify: true\` for early feedback — the same deterministic checks run automatically inside skyramp_execute_test, which returns per-violation feedback instead of executing until they pass.`,
77
92
  inputSchema: enhanceAssertionsSchema,
78
93
  }, async (params) => {
79
- const { testFile, testType, enhanceType, autoApply } = params;
94
+ const { testFile, testType, enhanceType, autoApply, verify, stateFile } = params;
95
+ // Everything downstream keys state by this path; a relative spelling
96
+ // would fork records and fail gates open (review finding on SKYR-4262).
97
+ if (!path.isAbsolute(testFile)) {
98
+ return toolError(`testFile must be an absolute path, got: ${testFile}`);
99
+ }
80
100
  // SKYR-4115: the reuse tool hands out POM mapping instructions and then relies on
81
101
  // the agent to come back with `verify: true` — the call that runs the zero-reuse
82
102
  // gate. This is the first tool called after it on the same spec, so it is where a
83
103
  // skipped verify is caught cheapest. Enforcing the call (not the outcome) keeps
84
104
  // the policy in the reuse phase where it belongs.
85
- //
86
- // No stateFile is passed because this tool's schema has no such field, so outside
87
- // CI — where nothing anchors the run state path — this checkpoint fails open. The
88
- // execute_test backstop does take params.stateFile and covers that caller.
89
105
  // SKYR-4220: stage the shared utils file this spec wrote or imports BEFORE the
90
106
  // debt check, unconditionally — so the file reaches the output commit even on a
91
107
  // path where the agent never calls verify. Located by header, not by name.
92
- await stageUtilsArtifacts(testFile);
93
- const owed = await pendingReuseDebt(testFile, undefined, testType, {
108
+ await stageAndRecordRetrofits(testFile);
109
+ const owed = await pendingReuseDebt(testFile, stateFile, testType, {
94
110
  chain: false,
95
111
  });
96
112
  if (owed)
97
113
  return toolError(owed);
98
114
  // Stage so testbot includes the generated files in its output commit.
99
115
  await stageGeneratedPaths(testFile);
100
- // SKYR-4262: snapshot the spec at instruction-handout time. This tool only
101
- // returns instructions nothing here verifies the agent follows them — so
102
- // skyramp_execute_test blocks later if the file is still byte-identical
103
- // (instructions acknowledged, never acted on). Any edit clears the debt,
104
- // including a documented `// assertions reviewed:` no-change decision.
105
- await recordEnhanceObligation(testFile);
116
+ // Verify pass: deterministically check the hand-out instructions were applied
117
+ // (hash delta, no removed coverage, rubric lints, strength increase) and give
118
+ // per-violation feedback. isError on FAILED forces the fix-and-retry loop.
119
+ if (verify) {
120
+ try {
121
+ const record = await readAssertionRecord(testFile, stateFile);
122
+ // Discriminators come from the persisted record when it matches this
123
+ // canonical path — params are agent-supplied, and flipping enhanceType
124
+ // to "maintenance" would disarm the strength gate and structural lints
125
+ // for a permanent PASS. Canonical comparison closes the alias bypass
126
+ // (dir//x.spec.ts). Falls back to params only when stateless.
127
+ const trusted = trustedRecord(record, testFile);
128
+ const result = await verifyAssertionEnhancement({
129
+ testFile,
130
+ testType: trusted?.testType ?? testType,
131
+ enhanceType: (trusted?.enhanceType ??
132
+ enhanceType),
133
+ baseline: baselineFrom(record, testFile),
134
+ });
135
+ await recordAssertionVerdict(testFile, result, stateFile);
136
+ AnalyticsService.pushMCPToolEvent(TOOL_NAME, undefined, {
137
+ testFile,
138
+ testType,
139
+ enhanceType,
140
+ verify: true,
141
+ verifyPassed: result.ok,
142
+ }).catch(() => { });
143
+ return {
144
+ content: [
145
+ {
146
+ type: "text",
147
+ text: formatAssertionVerifyReport(result, testFile),
148
+ },
149
+ ],
150
+ isError: !result.ok,
151
+ };
152
+ }
153
+ catch (err) {
154
+ // Not necessarily the test file — state reads can throw here too.
155
+ return toolError(`${TOOL_NAME} verification failed for ${testFile}: ${err.message}`);
156
+ }
157
+ }
158
+ // Hand-out: snapshot the file as generated so the verify pass has a baseline
159
+ // to measure against. Best-effort — no run state means stateless verify later.
160
+ await recordAssertionBaseline(testFile, testType, enhanceType, stateFile);
106
161
  if (autoApply) {
107
162
  const compactInstructions = [
108
163
  `Enhance response body assertions in: \`${testFile}\``,
@@ -0,0 +1,53 @@
1
+ import { type UnifiedAnalysisState } from "../../utils/AnalysisStateManager.js";
2
+ import { type StagingResult } from "../../utils/utils-verify/index.js";
3
+ /**
4
+ * Retrofit tracking (SKYR-4276 A4): a RETROFIT is a pre-existing Skyramp-generated
5
+ * test the reuse pass edited to import from the shared utils file (STEP 5c / the
6
+ * sibling-inline advisory). Staging carries the edit into the output commit, so the
7
+ * report must disclose it — with an execution that ran the edited bytes.
8
+ *
9
+ * Whether an execution counts is answered by TIME, not by a stored hash: the
10
+ * execution record already carries `executedAt`, and the file's mtime says when it
11
+ * was last edited — an execution older than the last edit ran bytes that no longer
12
+ * exist. That also covers a bare editor write with no tool call in between, which a
13
+ * record-time hash cannot see. (Same identity pattern as VideoRecord's
14
+ * status/executedAt.)
15
+ */
16
+ /** Stage the utils artifacts for `testFile` and record any retrofits staging found.
17
+ * The one call every checkpoint makes (reuse verify, enhance, execute). */
18
+ export declare function stageAndRecordRetrofits(testFile: string, language?: string, explicitStateFile?: string): Promise<StagingResult>;
19
+ /**
20
+ * Record the pre-existing generated tests the reuse pass edited (found by staging).
21
+ * Idempotent; a retrofit already recorded keeps its first attribution. Fails open
22
+ * like every other reuse-state write.
23
+ */
24
+ export declare function recordReuseRetrofits(retrofits: string[], utilsFiles: string[], testFile: string, explicitStateFile?: string): Promise<void>;
25
+ /** One retrofit as the report sees it. */
26
+ export interface RetrofitView {
27
+ /** Absolute path. */
28
+ file: string;
29
+ utilsFile: string;
30
+ /** The spec whose reuse pass caused the edit (absolute). */
31
+ testFile: string;
32
+ /** Status of an execution that ran the CURRENT bytes (executedAt ≥ the file's
33
+ * mtime), if any. */
34
+ execution?: import("../../types/TestExecution.js").TestExecutionStatus;
35
+ }
36
+ /**
37
+ * The retrofits that still stand in the working tree, with the execution that ran
38
+ * their current bytes. `changedFilesAbs` is ABSOLUTE paths across every checkout (see
39
+ * listChangedFilesAbs) — a retrofit the agent reverted is not among them and drops
40
+ * out. An execution older than the file's mtime ran bytes that no longer exist and
41
+ * does not count; an unreadable mtime fails open (the execution counts), matching
42
+ * the surrounding SKYR-3883 infrastructure-failure semantics.
43
+ */
44
+ export declare function standingRetrofits(state: Pick<UnifiedAnalysisState, "reuseRetrofits" | "existingTests">, changedFilesAbs: string[]): Promise<RetrofitView[]>;
45
+ /**
46
+ * The report gate over the standing retrofits: every one needs an execution of its
47
+ * current bytes, or the report is refused. Returns the views (for the report rows)
48
+ * and, when any are unexecuted, the refusal text.
49
+ */
50
+ export declare function retrofitGate(state: Pick<UnifiedAnalysisState, "reuseRetrofits" | "existingTests">, changedFilesAbs: string[], repoRoot: string): Promise<{
51
+ views: RetrofitView[];
52
+ refusal?: string;
53
+ }>;