@skyramp/mcp 0.4.1 → 0.4.2-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (583) hide show
  1. package/build/commands/localDevTestChangesCommand.js +2 -1
  2. package/build/commands/recommendTestsAndExecuteCommand.js +15 -7
  3. package/build/commands/testThisEndpointCommand.js +35 -6
  4. package/build/execution/wrapperConfig.d.ts +56 -0
  5. package/build/execution/wrapperConfig.js +155 -0
  6. package/build/index.js +40 -10
  7. package/build/playwright/blueprintDigest.js +28 -7
  8. package/build/playwright/registerPlaywrightTools.js +47 -28
  9. package/build/playwright/traceExportStore.d.ts +22 -0
  10. package/build/playwright/traceExportStore.js +81 -0
  11. package/build/playwright/traceRecordingPrompt.js +11 -3
  12. package/build/prompts/code-reuse.js +118 -49
  13. package/build/prompts/fix-error-prompt.d.ts +9 -1
  14. package/build/prompts/fix-error-prompt.js +31 -28
  15. package/build/prompts/local-dev/local-dev-plan.d.ts +3 -0
  16. package/build/prompts/local-dev/local-dev-plan.js +8 -23
  17. package/build/prompts/local-dev/local-dev-prompts.d.ts +1 -1
  18. package/build/prompts/local-dev/local-dev-prompts.js +31 -4
  19. package/build/prompts/modularization/integration-test-modularization.js +13 -6
  20. package/build/prompts/modularization/ui-test-modularization.js +1 -1
  21. package/build/prompts/personas.js +1 -1
  22. package/build/prompts/pom-aware-code-reuse.js +7 -9
  23. package/build/prompts/reuse-hand-off.d.ts +64 -0
  24. package/build/prompts/reuse-hand-off.js +130 -0
  25. package/build/prompts/shared-helper-policy.d.ts +124 -2
  26. package/build/prompts/shared-helper-policy.js +178 -12
  27. package/build/prompts/startTraceCollectionPrompts.js +1 -1
  28. package/build/prompts/sut-setup/modes/adaptWorkflowPrompt.js +7 -8
  29. package/build/prompts/sut-setup/modes/dockerComposePrompt.js +1 -1
  30. package/build/prompts/sut-setup/shared.d.ts +4 -1
  31. package/build/prompts/sut-setup/shared.js +6 -4
  32. package/build/prompts/test-maintenance/actionsInstructions.d.ts +19 -11
  33. package/build/prompts/test-maintenance/actionsInstructions.js +47 -26
  34. package/build/prompts/test-maintenance/drift-analysis-prompt.d.ts +16 -8
  35. package/build/prompts/test-maintenance/drift-analysis-prompt.js +93 -38
  36. package/build/prompts/test-maintenance/driftAnalysisSections.js +5 -3
  37. package/build/prompts/test-maintenance/driftAnalysisShared.js +4 -2
  38. package/build/prompts/test-maintenance/uiDriftAnalysisSections.js +8 -5
  39. package/build/prompts/test-recommendation/recommendationSections.js +19 -8
  40. package/build/prompts/test-recommendation/recommendationShared.d.ts +1 -1
  41. package/build/prompts/test-recommendation/recommendationShared.js +0 -1
  42. package/build/prompts/test-recommendation/registerRecommendTestsPrompt.js +5 -2
  43. package/build/prompts/test-recommendation/test-recommendation-prompt.js +10 -7
  44. package/build/prompts/testbot/testbot-prompts.js +133 -63
  45. package/build/recommendation/answers.js +18 -9
  46. package/build/recommendation/pullRequestText.js +1 -1
  47. package/build/recommendation/registerPlan.js +31 -17
  48. package/build/recommendation/subjectStep.d.ts +8 -10
  49. package/build/recommendation/subjectStep.js +17 -16
  50. package/build/recommendation/types.d.ts +71 -17
  51. package/build/recommendation/types.js +9 -15
  52. package/build/recommendation/verifierContracts.d.ts +7 -3
  53. package/build/recommendation/verifierContracts.js +8 -4
  54. package/build/recommendation/verifiers/changedFile.js +11 -13
  55. package/build/recommendation/verifiers/citedPath.d.ts +8 -0
  56. package/build/recommendation/verifiers/citedPath.js +16 -2
  57. package/build/recommendation/verifiers/coverage.d.ts +2 -2
  58. package/build/recommendation/verifiers/coverage.js +190 -134
  59. package/build/recommendation/verifiers/defects.js +56 -16
  60. package/build/recommendation/verifiers/deliveredMatchesPlan.d.ts +7 -1
  61. package/build/recommendation/verifiers/deliveredMatchesPlan.js +28 -14
  62. package/build/recommendation/verifiers/endpointGrounded.js +14 -8
  63. package/build/recommendation/verifiers/existingCoverage.d.ts +6 -0
  64. package/build/recommendation/verifiers/existingCoverage.js +30 -21
  65. package/build/recommendation/verifiers/expectedOutcome.js +17 -8
  66. package/build/recommendation/verifiers/expectedValueSourced.js +54 -43
  67. package/build/recommendation/verifiers/issueTraceability.d.ts +5 -0
  68. package/build/recommendation/verifiers/issueTraceability.js +64 -7
  69. package/build/recommendation/verifiers/removedElementGuarded.js +19 -11
  70. package/build/recommendation/verifiers/reportedCategory.js +9 -4
  71. package/build/recommendation/verifiers/requirementSourced.js +34 -21
  72. package/build/recommendation/verifiers/screenRoute.js +15 -10
  73. package/build/recommendation/verifiers/statedDifference.js +15 -8
  74. package/build/recommendation/verifiers/uiElementGrounded.js +50 -16
  75. package/build/resources/analysisResources.js +7 -3
  76. package/build/resources/progressResource.js +4 -2
  77. package/build/resources/sutSetupResource.js +20 -2
  78. package/build/resources/testbotResource.js +19 -1
  79. package/build/services/AnalyticsService.js +3 -1
  80. package/build/services/ScenarioGenerationService.js +5 -5
  81. package/build/services/TestDiscoveryService.js +53 -9
  82. package/build/services/TestExecutionService.js +63 -21
  83. package/build/services/TestGenerationService.d.ts +1 -1
  84. package/build/services/TestGenerationService.js +43 -24
  85. package/build/skills/enhanceAssertionsSkill.d.ts +45 -0
  86. package/build/skills/enhanceAssertionsSkill.js +103 -0
  87. package/build/skills/fixTestImportErrorsSkill.d.ts +2 -2
  88. package/build/skills/fixTestImportErrorsSkill.js +2 -2
  89. package/build/skills/runTestSkill.d.ts +6 -0
  90. package/build/skills/runTestSkill.js +17 -0
  91. package/build/skills/skillFiles.d.ts +38 -0
  92. package/build/skills/skillFiles.js +94 -0
  93. package/build/skills/validateAssertionAlignmentSkill.d.ts +34 -0
  94. package/build/skills/validateAssertionAlignmentSkill.js +59 -0
  95. package/build/tool-phases.js +4 -1
  96. package/build/tools/auth/loginTool.js +3 -1
  97. package/build/tools/auth/logoutTool.js +3 -1
  98. package/build/tools/budgetExcuse.d.ts +15 -0
  99. package/build/tools/budgetExcuse.js +113 -0
  100. package/build/tools/code-refactor/alignAssertionsTool.d.ts +2 -0
  101. package/build/tools/code-refactor/alignAssertionsTool.js +51 -0
  102. package/build/tools/code-refactor/assertion-state.d.ts +1 -1
  103. package/build/tools/code-refactor/assertion-state.js +1 -1
  104. package/build/tools/code-refactor/assertionOperations.d.ts +44 -0
  105. package/build/tools/code-refactor/assertionOperations.js +82 -0
  106. package/build/tools/code-refactor/assertionSkillTools.d.ts +3 -0
  107. package/build/tools/code-refactor/assertionSkillTools.js +86 -0
  108. package/build/tools/code-refactor/codeReuseTool.js +27 -3
  109. package/build/tools/code-refactor/enhanceAssertionsTool.js +19 -83
  110. package/build/tools/code-refactor/gate-markers.d.ts +51 -0
  111. package/build/tools/code-refactor/gate-markers.js +95 -0
  112. package/build/tools/code-refactor/retrofit-state.d.ts +3 -1
  113. package/build/tools/code-refactor/retrofit-state.js +44 -2
  114. package/build/tools/code-refactor/reuse-outcome.d.ts +23 -1
  115. package/build/tools/code-refactor/reuse-outcome.js +36 -13
  116. package/build/tools/code-refactor/reuse-state.d.ts +90 -16
  117. package/build/tools/code-refactor/reuse-state.js +191 -61
  118. package/build/tools/code-refactor/utils-verify-gates.d.ts +4 -0
  119. package/build/tools/code-refactor/utils-verify-gates.js +269 -30
  120. package/build/tools/code-refactor/verify-gates.d.ts +15 -1
  121. package/build/tools/code-refactor/verify-gates.js +36 -4
  122. package/build/tools/enrichTestWithMocksTool.d.ts +1 -1
  123. package/build/tools/enrichTestWithMocksTool.js +9 -5
  124. package/build/tools/executeSkyrampTestTool.d.ts +118 -48
  125. package/build/tools/executeSkyrampTestTool.js +998 -372
  126. package/build/tools/execution-video-state.js +1 -1
  127. package/build/tools/fixErrorTool.js +5 -6
  128. package/build/tools/generate-tests/batchMockGenerationTool.js +1 -1
  129. package/build/tools/generate-tests/generateBatchScenarioRestTool.js +114 -73
  130. package/build/tools/generate-tests/generateContractRestTool.js +34 -16
  131. package/build/tools/generate-tests/generateE2ERestTool.d.ts +1 -0
  132. package/build/tools/generate-tests/generateE2ERestTool.js +9 -1
  133. package/build/tools/generate-tests/generateIntegrationRestTool.js +22 -7
  134. package/build/tools/generate-tests/generateMockRestTool.js +3 -1
  135. package/build/tools/generate-tests/generateUIRestTool.d.ts +2 -0
  136. package/build/tools/generate-tests/generateUIRestTool.js +11 -2
  137. package/build/tools/generate-tests/loadTestSchema.js +1 -3
  138. package/build/tools/generate-tests/planGuard.js +6 -3
  139. package/build/tools/generate-tests/scenarioFileIdentity.js +4 -1
  140. package/build/tools/generate-tests/scenarioLint.js +17 -5
  141. package/build/tools/generate-tests/trace-reuse-guard.js +5 -2
  142. package/build/tools/generateEnrichedIntegrationTestTool.js +9 -3
  143. package/build/tools/one-click/oneClickTool.js +3 -1
  144. package/build/tools/preflightMockCheckTool.js +23 -7
  145. package/build/tools/submitReportTool.d.ts +51 -12
  146. package/build/tools/submitReportTool.js +988 -161
  147. package/build/tools/test-management/actionsTool.js +241 -51
  148. package/build/tools/test-management/analyzeChangesTool.d.ts +8 -9
  149. package/build/tools/test-management/analyzeChangesTool.js +127 -68
  150. package/build/tools/test-management/analyzeTestHealthTool.d.ts +0 -11
  151. package/build/tools/test-management/analyzeTestHealthTool.js +42 -76
  152. package/build/tools/test-management/registerTestPlanTool.d.ts +44 -34
  153. package/build/tools/test-management/registerTestPlanTool.js +255 -111
  154. package/build/tools/test-management/resolveScreenTool.js +33 -9
  155. package/build/tools/test-management/testsOwedBeforeRun.d.ts +28 -0
  156. package/build/tools/test-management/testsOwedBeforeRun.js +53 -0
  157. package/build/tools/trace/startTraceCollectionTool.js +3 -1
  158. package/build/tools/trace/stopTraceCollectionTool.js +42 -6
  159. package/build/tools/verifyTestDependenciesTool.d.ts +3 -0
  160. package/build/tools/verifyTestDependenciesTool.js +54 -0
  161. package/build/tools/workspace/initScanWorkspaceTool.js +9 -3
  162. package/build/tools/workspace/initializeWorkspaceTool.js +3 -1
  163. package/build/types/AssertionOutcome.d.ts +1 -1
  164. package/build/types/EnhanceType.d.ts +6 -0
  165. package/build/types/EnhanceType.js +1 -0
  166. package/build/types/RepositoryAnalysis.d.ts +32 -72
  167. package/build/types/ReuseOutcome.d.ts +100 -7
  168. package/build/types/ReuseOutcome.js +16 -0
  169. package/build/types/StepMethod.js +20 -6
  170. package/build/types/TestAnalysis.d.ts +10 -2
  171. package/build/types/TestExecution.d.ts +45 -0
  172. package/build/types/TestRecommendation.d.ts +1 -1
  173. package/build/types/TestRecommendation.js +4 -1
  174. package/build/types/TestTypes.d.ts +16 -0
  175. package/build/types/TestTypes.js +40 -3
  176. package/build/types/TestbotPromptOptions.d.ts +9 -1
  177. package/build/types/TestbotReport.d.ts +45 -9
  178. package/build/utils/AnalysisStateManager.d.ts +137 -34
  179. package/build/utils/AnalysisStateManager.js +228 -44
  180. package/build/utils/assertion-verify/api-shared-lints.js +34 -16
  181. package/build/utils/assertion-verify/metrics.js +39 -6
  182. package/build/utils/assertion-verify/ui-lints.js +4 -2
  183. package/build/utils/branchDiff.js +47 -12
  184. package/build/utils/canonicalJson.js +3 -1
  185. package/build/utils/connectionErrors.d.ts +10 -0
  186. package/build/utils/connectionErrors.js +10 -0
  187. package/build/utils/dartRouteExtractor.js +36 -7
  188. package/build/utils/fixAttempts.d.ts +26 -0
  189. package/build/utils/fixAttempts.js +109 -0
  190. package/build/utils/frontendSelectors.js +23 -4
  191. package/build/utils/generatedTestRecord.d.ts +19 -0
  192. package/build/utils/generatedTestRecord.js +61 -0
  193. package/build/utils/gitStaging.js +7 -2
  194. package/build/utils/initAgent.js +26 -6
  195. package/build/utils/language-helper.js +60 -45
  196. package/build/utils/pathMatching.js +2 -1
  197. package/build/utils/pathSignatures.js +6 -2
  198. package/build/utils/planMatchKeys.d.ts +2 -2
  199. package/build/utils/planMatchKeys.js +20 -14
  200. package/build/utils/pom-catalog-parse.js +7 -2
  201. package/build/utils/pom-scope/import-expansion.js +6 -1
  202. package/build/utils/pom-scope/index.js +50 -12
  203. package/build/utils/pom-scope/scoring.js +13 -3
  204. package/build/utils/pom-scope/selector-extractor.js +16 -3
  205. package/build/utils/pom-scope/strip.d.ts +8 -0
  206. package/build/utils/pom-scope/strip.js +238 -0
  207. package/build/utils/pom-scope/testIdDiscovery.js +14 -2
  208. package/build/utils/pom-verify/__fixtures__/af-style/asset-list.page.d.ts +1 -1
  209. package/build/utils/pom-verify/__fixtures__/af-style/asset-list.page.js +4 -2
  210. package/build/utils/pom-verify/__fixtures__/af-style/report.iframe.page.js +3 -1
  211. package/build/utils/pom-verify/__fixtures__/af-style/workflow-footer.page.js +3 -1
  212. package/build/utils/pom-verify/bindings.js +5 -1
  213. package/build/utils/pom-verify/calls.js +9 -2
  214. package/build/utils/pom-verify/verify.js +27 -5
  215. package/build/utils/pr-comment-parser.js +20 -7
  216. package/build/utils/progress.d.ts +1 -1
  217. package/build/utils/progress.js +1 -1
  218. package/build/utils/proxy-terminal.d.ts +19 -1
  219. package/build/utils/proxy-terminal.js +346 -21
  220. package/build/utils/rebaselineSnapshots.d.ts +1 -1
  221. package/build/utils/rebaselineSnapshots.js +6 -16
  222. package/build/utils/removedUiElements.js +1 -1
  223. package/build/utils/reportLanguage.js +35 -7
  224. package/build/utils/reportVerification.d.ts +14 -8
  225. package/build/utils/reportVerification.js +19 -19
  226. package/build/utils/repositorySlug.d.ts +32 -0
  227. package/build/utils/repositorySlug.js +77 -0
  228. package/build/utils/reuseRouting.d.ts +10 -0
  229. package/build/utils/reuseRouting.js +21 -2
  230. package/build/utils/runContextGauge.d.ts +27 -0
  231. package/build/utils/runContextGauge.js +181 -0
  232. package/build/utils/runSerialized.d.ts +3 -0
  233. package/build/utils/runSerialized.js +39 -0
  234. package/build/utils/screenRoutes.js +74 -18
  235. package/build/utils/skyrampMdContent.d.ts +1 -1
  236. package/build/utils/skyrampMdContent.js +1 -1
  237. package/build/utils/skyrampSdkVersion.d.ts +9 -0
  238. package/build/utils/skyrampSdkVersion.js +16 -0
  239. package/build/utils/sourceRouteExtractor.js +13 -6
  240. package/build/utils/telemetry.d.ts +1 -0
  241. package/build/utils/telemetry.js +8 -5
  242. package/build/utils/testDependencyPolicy.d.ts +9 -0
  243. package/build/utils/testDependencyPolicy.js +809 -0
  244. package/build/utils/testExecutionRecord.d.ts +94 -0
  245. package/build/utils/testExecutionRecord.js +269 -0
  246. package/build/utils/testFileClassification.d.ts +8 -0
  247. package/build/utils/testFileClassification.js +39 -4
  248. package/build/utils/trace-parser.js +62 -14
  249. package/build/utils/urlPath.js +3 -1
  250. package/build/utils/utils-verify/action-key.d.ts +46 -0
  251. package/build/utils/utils-verify/action-key.js +120 -38
  252. package/build/utils/utils-verify/action-sites.d.ts +32 -0
  253. package/build/utils/utils-verify/action-sites.js +202 -0
  254. package/build/utils/utils-verify/allow.d.ts +122 -3
  255. package/build/utils/utils-verify/allow.js +146 -21
  256. package/build/utils/utils-verify/body-reach.d.ts +120 -0
  257. package/build/utils/utils-verify/body-reach.js +333 -0
  258. package/build/utils/utils-verify/call-sites.d.ts +31 -7
  259. package/build/utils/utils-verify/call-sites.js +80 -13
  260. package/build/utils/utils-verify/delivered-imports.d.ts +43 -0
  261. package/build/utils/utils-verify/delivered-imports.js +84 -0
  262. package/build/utils/utils-verify/fixed-sleep.d.ts +96 -0
  263. package/build/utils/utils-verify/fixed-sleep.js +461 -0
  264. package/build/utils/utils-verify/in-house.d.ts +93 -0
  265. package/build/utils/utils-verify/in-house.js +719 -0
  266. package/build/utils/utils-verify/incumbent.d.ts +3 -0
  267. package/build/utils/utils-verify/incumbent.js +75 -0
  268. package/build/utils/utils-verify/index.d.ts +2 -0
  269. package/build/utils/utils-verify/index.js +2 -0
  270. package/build/utils/utils-verify/language-spec.d.ts +51 -10
  271. package/build/utils/utils-verify/language-spec.js +232 -21
  272. package/build/utils/utils-verify/locate.d.ts +26 -0
  273. package/build/utils/utils-verify/locate.js +125 -23
  274. package/build/utils/utils-verify/module-name.d.ts +40 -0
  275. package/build/utils/utils-verify/module-name.js +98 -0
  276. package/build/utils/utils-verify/parse.d.ts +56 -7
  277. package/build/utils/utils-verify/parse.js +195 -40
  278. package/build/utils/utils-verify/retrofit-equivalence.d.ts +5 -0
  279. package/build/utils/utils-verify/retrofit-equivalence.js +4 -2
  280. package/build/utils/utils-verify/stage.d.ts +5 -0
  281. package/build/utils/utils-verify/stage.js +47 -2
  282. package/build/utils/utils-verify/status-once.d.ts +62 -0
  283. package/build/utils/utils-verify/status-once.js +207 -0
  284. package/build/utils/utils-verify/typecheck.d.ts +61 -0
  285. package/build/utils/utils-verify/typecheck.js +338 -0
  286. package/build/utils/utils-verify/verify.d.ts +79 -3
  287. package/build/utils/utils-verify/verify.js +531 -61
  288. package/build/utils/versions.d.ts +3 -3
  289. package/build/utils/versions.js +1 -1
  290. package/build/utils/workspaceAuth.js +107 -37
  291. package/build/workspace/queryParamResolution.js +11 -4
  292. package/build/workspace/workspace.d.ts +72 -52
  293. package/build/workspace/workspace.js +19 -15
  294. package/node_modules/playwright/ThirdPartyNotices.txt +19 -19
  295. package/node_modules/playwright/lib/mcp/skyramp/assertTool.js +8 -2
  296. package/node_modules/playwright/lib/mcp/skyramp/common/cssValue.js +87 -0
  297. package/node_modules/playwright/lib/mcp/skyramp/loadTraceTool.js +31 -0
  298. package/node_modules/playwright/lib/mcp/skyramp/skyRampImport.js +3 -0
  299. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +181 -15
  300. package/node_modules/playwright/lib/mcp/test/skyRampExport.js +24 -0
  301. package/node_modules/playwright/lib/transform/babelBundleImpl.js +2353 -190
  302. package/node_modules/playwright/node_modules/playwright-core/ThirdPartyNotices.txt +62 -34
  303. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/.package-lock.json +72 -41
  304. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/@hono/node-server/dist/serve-static.js +1 -1
  305. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/@hono/node-server/dist/serve-static.mjs +1 -1
  306. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/@hono/node-server/package.json +2 -2
  307. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/body-parser/README.md +16 -1
  308. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/body-parser/index.js +4 -20
  309. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/body-parser/lib/read.js +17 -17
  310. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/body-parser/lib/types/json.js +60 -32
  311. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/body-parser/lib/types/raw.js +3 -3
  312. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/body-parser/lib/types/text.js +3 -3
  313. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/body-parser/lib/types/urlencoded.js +16 -20
  314. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/body-parser/lib/utils.js +18 -16
  315. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/body-parser/node_modules/content-type/LICENSE +22 -0
  316. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/body-parser/node_modules/content-type/README.md +71 -0
  317. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/body-parser/node_modules/content-type/dist/index.d.ts +46 -0
  318. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/body-parser/node_modules/content-type/dist/index.js +176 -0
  319. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/body-parser/node_modules/content-type/dist/index.js.map +1 -0
  320. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/body-parser/node_modules/content-type/package.json +52 -0
  321. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/body-parser/package.json +23 -10
  322. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/es-object-atoms/CHANGELOG.md +21 -14
  323. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/es-object-atoms/isObject.d.ts +1 -1
  324. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/es-object-atoms/package.json +6 -7
  325. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/es-object-atoms/tsconfig.json +1 -0
  326. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/fast-uri/index.js +266 -45
  327. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/fast-uri/lib/schemes.js +9 -4
  328. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/fast-uri/lib/utils.js +396 -92
  329. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/fast-uri/package.json +1 -1
  330. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/fast-uri/test/component-safe-serialization.test.js +163 -0
  331. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/fast-uri/test/equal.test.js +31 -3
  332. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/fast-uri/test/fixtures/uri-js-parse.json +2 -0
  333. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/fast-uri/test/ipv6-canonical.test.js +34 -0
  334. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/fast-uri/test/ipv6-validation.test.js +124 -0
  335. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/fast-uri/test/malformed-percent.test.js +77 -0
  336. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/fast-uri/test/malformed-urn.test.js +61 -0
  337. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/fast-uri/test/parse.test.js +7 -3
  338. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/fast-uri/test/query-fragment-normalization.test.js +33 -0
  339. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/fast-uri/test/reserved-path-normalization.test.js +109 -0
  340. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/fast-uri/test/scheme-validation.test.js +124 -0
  341. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/fast-uri/test/security-normalization.test.js +101 -0
  342. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/fast-uri/test/security.test.js +301 -3
  343. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/fast-uri/test/urn-full-input.test.js +29 -0
  344. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/fast-uri/test/websocket-query-preservation.test.js +24 -0
  345. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hasown/CHANGELOG.md +7 -0
  346. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hasown/index.d.ts +0 -1
  347. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hasown/package.json +4 -5
  348. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/client/client.js +30 -16
  349. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/client/utils.js +4 -1
  350. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/context.js +32 -13
  351. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/helper/accepts/accepts.js +36 -2
  352. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/helper/proxy/index.js +4 -0
  353. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/helper/ssg/ssg.js +1 -1
  354. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/helper/ssg/utils.js +30 -10
  355. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/helper/streaming/sse.js +5 -4
  356. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/hono-base.js +10 -8
  357. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/jsx/base.js +44 -23
  358. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/jsx/components.js +25 -26
  359. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/jsx/context.js +5 -5
  360. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/jsx/dom/render.js +2 -0
  361. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/jsx/dom/server.js +5 -1
  362. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/jsx/hooks/index.js +16 -13
  363. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/jsx/intrinsic-element/components.js +3 -3
  364. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/jsx/streaming.js +4 -5
  365. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/middleware/cache/index.js +103 -8
  366. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/middleware/compress/index.js +5 -0
  367. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/middleware/cors/index.js +17 -14
  368. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/middleware/csrf/index.js +1 -1
  369. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/middleware/etag/digest.js +47 -1
  370. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/middleware/etag/index.js +7 -4
  371. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/middleware/jwk/jwk.js +9 -4
  372. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/middleware/jwt/jwt.js +9 -4
  373. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/middleware/language/language.js +10 -6
  374. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/middleware/method-not-allowed/index.js +90 -0
  375. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/middleware/pretty-json/index.js +3 -1
  376. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/middleware/secure-headers/secure-headers.js +16 -7
  377. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/request.js +20 -13
  378. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/router/linear-router/router.js +7 -2
  379. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/router/pattern-router/router.js +3 -9
  380. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/router/reg-exp-router/node.js +65 -59
  381. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/router/reg-exp-router/router.js +71 -128
  382. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/router/reg-exp-router/trie.js +14 -5
  383. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/router/trie-router/node.js +47 -70
  384. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/router/trie-router/router.js +3 -11
  385. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/router/utils.js +27 -0
  386. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/router.js +1 -1
  387. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/utils/accept.js +1 -1
  388. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/utils/body.js +21 -3
  389. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/utils/cookie.js +4 -3
  390. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/utils/ipaddr.js +5 -3
  391. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/utils/stream.js +12 -3
  392. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/utils/url.js +19 -11
  393. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/client/client.js +30 -16
  394. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/client/utils.js +4 -1
  395. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/context.js +32 -13
  396. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/helper/accepts/accepts.js +36 -2
  397. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/helper/proxy/index.js +4 -0
  398. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/helper/ssg/ssg.js +1 -1
  399. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/helper/ssg/utils.js +30 -10
  400. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/helper/streaming/sse.js +5 -4
  401. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/hono-base.js +10 -8
  402. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/jsx/base.js +41 -23
  403. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/jsx/components.js +26 -27
  404. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/jsx/context.js +6 -6
  405. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/jsx/dom/render.js +2 -0
  406. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/jsx/dom/server.js +5 -1
  407. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/jsx/hooks/index.js +16 -13
  408. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/jsx/intrinsic-element/components.js +4 -4
  409. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/jsx/streaming.js +5 -6
  410. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/middleware/cache/index.js +103 -8
  411. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/middleware/compress/index.js +5 -0
  412. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/middleware/cors/index.js +17 -14
  413. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/middleware/csrf/index.js +1 -1
  414. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/middleware/etag/digest.js +47 -1
  415. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/middleware/etag/index.js +7 -4
  416. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/middleware/jwk/jwk.js +9 -4
  417. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/middleware/jwt/jwt.js +9 -4
  418. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/middleware/language/language.js +10 -6
  419. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/middleware/method-not-allowed/index.js +68 -0
  420. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/middleware/pretty-json/index.js +3 -1
  421. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/middleware/secure-headers/secure-headers.js +16 -7
  422. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/request.js +21 -14
  423. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/router/linear-router/router.js +7 -2
  424. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/router/pattern-router/router.js +3 -9
  425. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/router/reg-exp-router/node.js +61 -58
  426. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/router/reg-exp-router/router.js +77 -129
  427. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/router/reg-exp-router/trie.js +14 -5
  428. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/router/trie-router/node.js +47 -70
  429. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/router/trie-router/router.js +3 -11
  430. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/router/utils.js +5 -0
  431. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/router.js +1 -1
  432. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/adapter/aws-lambda/types.d.ts +9 -0
  433. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/client/types.d.ts +1 -1
  434. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/context.d.ts +6 -2
  435. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/hono-base.d.ts +4 -3
  436. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/jsx/base.d.ts +7 -2
  437. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/jsx/dom/index.d.ts +5 -5
  438. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/jsx/dom/intrinsic-element/components.d.ts +2 -2
  439. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/jsx/dom/server.d.ts +5 -5
  440. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/jsx/hooks/index.d.ts +8 -6
  441. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/jsx/index.d.ts +5 -5
  442. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/middleware/cache/index.d.ts +6 -4
  443. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/middleware/combine/index.d.ts +1 -1
  444. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/middleware/cors/index.d.ts +1 -1
  445. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/middleware/jsx-renderer/index.d.ts +2 -2
  446. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/middleware/jwk/jwk.d.ts +2 -0
  447. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/middleware/jwt/jwt.d.ts +2 -0
  448. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/middleware/method-not-allowed/index.d.ts +49 -0
  449. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/middleware/secure-headers/permissions-policy.d.ts +3 -3
  450. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/router/reg-exp-router/node.d.ts +4 -1
  451. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/router/reg-exp-router/trie.d.ts +2 -1
  452. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/router/trie-router/node.d.ts +1 -2
  453. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/router/trie-router/router.d.ts +0 -1
  454. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/router/utils.d.ts +1 -0
  455. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/router.d.ts +1 -1
  456. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/utils/headers.d.ts +2 -2
  457. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/utils/url.d.ts +5 -0
  458. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/utils/accept.js +1 -1
  459. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/utils/body.js +21 -3
  460. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/utils/cookie.js +5 -4
  461. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/utils/ipaddr.js +5 -3
  462. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/utils/stream.js +12 -3
  463. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/utils/url.js +17 -10
  464. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/package.json +11 -4
  465. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/ip-address/README.md +173 -143
  466. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/ip-address/dist/address-error.d.ts +11 -0
  467. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/ip-address/dist/address-error.js.map +1 -1
  468. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/ip-address/dist/common.d.ts +49 -0
  469. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/ip-address/dist/common.js +79 -4
  470. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/ip-address/dist/common.js.map +1 -1
  471. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/ip-address/dist/ipv4.d.ts +78 -5
  472. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/ip-address/dist/ipv4.js +119 -24
  473. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/ip-address/dist/ipv4.js.map +1 -1
  474. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/ip-address/dist/ipv6.d.ts +151 -10
  475. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/ip-address/dist/ipv6.js +316 -90
  476. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/ip-address/dist/ipv6.js.map +1 -1
  477. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/ip-address/dist/v4/constants.d.ts +12 -0
  478. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/ip-address/dist/v4/constants.js +45 -2
  479. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/ip-address/dist/v4/constants.js.map +1 -1
  480. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/ip-address/dist/v6/constants.d.ts +14 -0
  481. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/ip-address/dist/v6/constants.js +50 -3
  482. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/ip-address/dist/v6/constants.js.map +1 -1
  483. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/ip-address/package.json +6 -4
  484. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/qs/.github/THREAT_MODEL.md +3 -3
  485. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/qs/CHANGELOG.md +26 -0
  486. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/qs/README.md +19 -1
  487. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/qs/dist/qs.js +25 -25
  488. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/qs/eslint.config.mjs +1 -1
  489. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/qs/lib/parse.js +16 -6
  490. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/qs/lib/stringify.js +23 -8
  491. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/qs/lib/utils.js +57 -11
  492. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/qs/package.json +8 -7
  493. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/qs/test/parse.js +419 -0
  494. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/qs/test/stringify.js +317 -0
  495. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/qs/test/utils.js +206 -0
  496. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/side-channel/CHANGELOG.md +10 -0
  497. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/side-channel/README.md +1 -1
  498. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/side-channel/index.js +5 -2
  499. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/side-channel/package.json +10 -10
  500. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/side-channel/test/index.js +16 -0
  501. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/type-is/index.js +8 -18
  502. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/type-is/node_modules/content-type/LICENSE +22 -0
  503. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/type-is/node_modules/content-type/README.md +71 -0
  504. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/type-is/node_modules/content-type/dist/index.d.ts +46 -0
  505. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/type-is/node_modules/content-type/dist/index.js +176 -0
  506. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/type-is/node_modules/content-type/dist/index.js.map +1 -0
  507. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/type-is/node_modules/content-type/package.json +52 -0
  508. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/type-is/package.json +9 -5
  509. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/package-lock.json +72 -41
  510. package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/.package-lock.json +6 -6
  511. package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/brace-expansion/README.md +23 -1
  512. package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/brace-expansion/index.js +265 -86
  513. package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/brace-expansion/package.json +1 -1
  514. package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/ip-address/README.md +173 -143
  515. package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/ip-address/dist/address-error.d.ts +11 -0
  516. package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/ip-address/dist/address-error.js.map +1 -1
  517. package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/ip-address/dist/common.d.ts +49 -0
  518. package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/ip-address/dist/common.js +79 -4
  519. package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/ip-address/dist/common.js.map +1 -1
  520. package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/ip-address/dist/ipv4.d.ts +78 -5
  521. package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/ip-address/dist/ipv4.js +119 -24
  522. package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/ip-address/dist/ipv4.js.map +1 -1
  523. package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/ip-address/dist/ipv6.d.ts +151 -10
  524. package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/ip-address/dist/ipv6.js +316 -90
  525. package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/ip-address/dist/ipv6.js.map +1 -1
  526. package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/ip-address/dist/v4/constants.d.ts +12 -0
  527. package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/ip-address/dist/v4/constants.js +45 -2
  528. package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/ip-address/dist/v4/constants.js.map +1 -1
  529. package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/ip-address/dist/v6/constants.d.ts +14 -0
  530. package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/ip-address/dist/v6/constants.js +50 -3
  531. package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/ip-address/dist/v6/constants.js.map +1 -1
  532. package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/ip-address/package.json +6 -4
  533. package/node_modules/playwright/node_modules/playwright-core/bundles/utils/package-lock.json +6 -6
  534. package/node_modules/playwright/node_modules/playwright-core/lib/cli/program.js +18 -9
  535. package/node_modules/playwright/node_modules/playwright-core/lib/mcpBundleImpl/index.js +47 -47
  536. package/node_modules/playwright/node_modules/playwright-core/lib/server/codegen/skyramp/jsonlReader.js +3 -0
  537. package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/recorderRunner.js +42 -0
  538. package/node_modules/playwright/node_modules/playwright-core/lib/utilsBundleImpl/index.js +121 -121
  539. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.B7KbSQcC.js → index.BAkLd5DX.js} +1 -1
  540. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +1 -1
  541. package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
  542. package/node_modules/playwright/node_modules/playwright-core/src/cli/program.ts +31 -9
  543. package/node_modules/playwright/node_modules/playwright-core/src/server/codegen/skyramp/jsonlReader.ts +1 -0
  544. package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/recorderRunner.ts +57 -1
  545. package/node_modules/playwright/package.json +2 -2
  546. package/package.json +7 -6
  547. package/plugin/prompts/code-reuse/hand-off.md +30 -0
  548. package/plugin/prompts/generate-tests/generation.md +1 -1
  549. package/plugin/prompts/plan-tests.md +14 -14
  550. package/plugin/prompts/testbot-task1.md +2 -2
  551. package/plugin/skills/enhance-assertions/SKILL.md +25 -0
  552. package/plugin/skills/enhance-assertions/reference/contract.md +51 -0
  553. package/plugin/skills/enhance-assertions/reference/integration.md +58 -0
  554. package/plugin/skills/enhance-assertions/reference/shared-rules.md +220 -0
  555. package/plugin/skills/enhance-assertions/reference/ui.md +373 -0
  556. package/plugin/skills/fix-test-import-errors/SKILL.md +45 -50
  557. package/plugin/skills/run-test/SKILL.md +16 -0
  558. package/plugin/skills/validate-assertion-alignment-post-execution/SKILL.md +32 -0
  559. package/plugin/skills/validate-assertion-alignment-post-execution/reference/checks.md +44 -0
  560. package/plugin/skills/validate-assertion-alignment-post-execution/reference/evidence.md +47 -0
  561. package/build/adapters/jestAdapter.d.ts +0 -14
  562. package/build/adapters/jestAdapter.js +0 -113
  563. package/build/adapters/mochaAdapter.d.ts +0 -13
  564. package/build/adapters/mochaAdapter.js +0 -87
  565. package/build/adapters/playwrightAdapter.d.ts +0 -17
  566. package/build/adapters/playwrightAdapter.js +0 -182
  567. package/build/adapters/pytestAdapter.d.ts +0 -15
  568. package/build/adapters/pytestAdapter.js +0 -108
  569. package/build/prompts/enhance-assertions/contractProviderAssertionsPrompt.d.ts +0 -2
  570. package/build/prompts/enhance-assertions/contractProviderAssertionsPrompt.js +0 -29
  571. package/build/prompts/enhance-assertions/integrationAssertionsPrompt.d.ts +0 -2
  572. package/build/prompts/enhance-assertions/integrationAssertionsPrompt.js +0 -36
  573. package/build/prompts/enhance-assertions/sharedAssertionRules.d.ts +0 -16
  574. package/build/prompts/enhance-assertions/sharedAssertionRules.js +0 -284
  575. package/build/prompts/enhance-assertions/uiAssertionsPrompt.d.ts +0 -2
  576. package/build/prompts/enhance-assertions/uiAssertionsPrompt.js +0 -388
  577. package/build/tools/runExistingTestsTool.d.ts +0 -138
  578. package/build/tools/runExistingTestsTool.js +0 -644
  579. package/build/types/ExternalTestExecution.d.ts +0 -67
  580. package/build/types/ExternalTestExecution.js +0 -8
  581. package/build/workspace/testSuites.d.ts +0 -20
  582. package/build/workspace/testSuites.js +0 -17
  583. package/node_modules/playwright/node_modules/playwright-core/.DS_Store +0 -0
@@ -1,28 +1,36 @@
1
1
  import { z } from "zod";
2
2
  import { logger } from "../utils/logger.js";
3
3
  import * as fs from "fs/promises";
4
+ import { constants as fsConstants } from "fs";
4
5
  import * as path from "path";
5
6
  import { AnalyticsService } from "../services/AnalyticsService.js";
6
7
  import { SCENARIO_CATEGORIES, categoryExpectsRed, } from "../types/TestRecommendation.js";
7
- import { TestType } from "../types/TestTypes.js";
8
- import { STEP_METHOD_DESCRIPTION, stepMethodSchema } from "../types/StepMethod.js";
8
+ import { TESTBOT_SUPPORTED_TEST_TYPES, TestType } from "../types/TestTypes.js";
9
+ import { STEP_METHOD_DESCRIPTION, stepMethodSchema, } from "../types/StepMethod.js";
9
10
  import { DriftAction } from "../types/TestAnalysis.js";
10
11
  import { TestExecutionStatus } from "../types/TestExecution.js";
11
12
  import { IssueFoundCategory } from "../types/TestbotReport.js";
12
- import { StateManager, runArtifactDir, getTestsRepoDir, resolveRunStatePath, } from "../utils/AnalysisStateManager.js";
13
+ import { StateManager, runArtifactDir, getTestsRepoDir, resolveOwnRunStatePath, } from "../utils/AnalysisStateManager.js";
13
14
  import { toolError, testFileMatches } from "../utils/utils.js";
14
- import { isTestbotEnabled, } from "../utils/featureFlags.js";
15
- import { answerFor, unknownAnswerObjections } from "../recommendation/answers.js";
16
- import { checkDeliveredMatchesPlan, checkMaintenanceDelivered } from "../recommendation/verifiers/deliveredMatchesPlan.js";
15
+ import { isTestbotEnabled, isUtilsReuseEnabled, } from "../utils/featureFlags.js";
16
+ import { answerFor, unknownAnswerObjections, } from "../recommendation/answers.js";
17
+ import { checkDeliveredMatchesPlan, checkMaintenanceDelivered, } from "../recommendation/verifiers/deliveredMatchesPlan.js";
17
18
  import { checkReportedCategoryMatchesPlan, checkRequirementConflictReported, } from "../recommendation/verifiers/reportedCategory.js";
18
- import { checkDefectsReported, checkIssueTraceability } from "../recommendation/verifiers/issueTraceability.js";
19
+ import { checkDefectsReported, checkIssueTraceability, } from "../recommendation/verifiers/issueTraceability.js";
19
20
  import { checkExpectedOutcomeAfterExecution, } from "../recommendation/verifiers/expectedOutcome.js";
20
21
  import { findInvalidSourceCitations, findUnchangedFileClaims, listChangedFiles, listChangedFilesAcross, listChangedFilesAbs, } from "../utils/reportVerification.js";
21
22
  import { isPlanOnlyMode } from "../utils/planOnlyMode.js";
23
+ import { contextGaugeSentence } from "../utils/runContextGauge.js";
24
+ import { budgetExcuseRefusals, citesBudget, countBudgetExcuseRefusal, MAX_BUDGET_EXCUSE_REFUSALS, } from "./budgetExcuse.js";
25
+ import { countUiDeliveryRefusal, MAX_UI_DELIVERY_REFUSALS, traceExportAttempts, uiDeliveryRefusals, } from "../playwright/traceExportStore.js";
22
26
  import { getReportLanguage, isEnforcedReportLanguage, findLanguageViolations, findLanguageNearMisses, reportLanguageDisplayName, } from "../utils/reportLanguage.js";
23
27
  import { canonicalTestPath, findAssertionRecordByFileName, rederiveAssertionOutcome, } from "./code-refactor/assertion-state.js";
24
- import { rederiveReuse, REUSE_SUBMIT_MAX_REFUSALS, reuseChainSkipped, samePath, } from "./code-refactor/reuse-state.js";
28
+ import { rederiveReuse, deriveReuseFromDelivered, REUSE_SUBMIT_MAX_REFUSALS, reuseChainSkipped, samePath, } from "./code-refactor/reuse-state.js";
25
29
  import { retrofitGate, } from "./code-refactor/retrofit-state.js";
30
+ import { onlyCommentLinesRemoved, stripGateMarkers, } from "./code-refactor/gate-markers.js";
31
+ import { parseKeptInline } from "./code-refactor/reuse-outcome.js";
32
+ import { hasUtilsHeader, locateUtilsFiles, parseUtilsAllows, realpath, utilsSpecForFile, } from "../utils/utils-verify/index.js";
33
+ import { verifyTestDependencyChanges } from "../utils/testDependencyPolicy.js";
26
34
  // Mirrors the tools wired to planGuard: UI and E2E are not gated at generation
27
35
  // time, so gating them here would be a report-time-only surprise.
28
36
  const PLAN_GATED_TEST_TYPES = new Set([
@@ -65,7 +73,9 @@ const MAINTENANCE_COVERAGE_ACTIONS = new Set([
65
73
  * an external test's REGENERATE keeps its real action and touches no file, so counting it
66
74
  * would credit a claim nothing backs. */
67
75
  function rowsLeavingCoverage(rows) {
68
- return (rows ?? []).filter((row) => row?.action !== undefined && MAINTENANCE_COVERAGE_ACTIONS.has(row.action) && row?.reportOnly !== true);
76
+ return (rows ?? []).filter((row) => row?.action !== undefined &&
77
+ MAINTENANCE_COVERAGE_ACTIONS.has(row.action) &&
78
+ row?.reportOnly !== true);
69
79
  }
70
80
  /** Objection ids for an untested declared change, `coverage:change:<id>`. The
71
81
  * change table reads the agent's answer back off them. */
@@ -74,22 +84,13 @@ const TOOL_NAME = "skyramp_submit_report";
74
84
  const DEFAULT_COMMIT_MESSAGE = "Added recommendations by Skyramp Testbot.";
75
85
  // Per-repo attribution. In a multi-repo run, every report item carries the
76
86
  // owner/repo it pertains to (the primary's owner/repo for primary objections, or a
77
- // related repo's owner/repo for objections from that repo's diff). Whitespace-only
78
- // values are normalized to undefined so a blank string is never treated as a
79
- // distinct repo. Single-repo runs may omit it.
87
+ // related repo's owner/repo for objections from that repo's diff). Trimmed, and
88
+ // a blank string is rejected: every item names its repo, single-repo runs too.
80
89
  const repositoryField = z
81
90
  .string()
82
- .optional()
83
- .describe("The 'owner/repo' this item pertains to (e.g. 'letsramp/api-insight'). In a multi-repo run, ALWAYS set it — the primary's owner/repo for primary-repo objections, or a related repo's owner/repo for objections derived from that repository's diff. May be omitted only in single-repo runs.");
84
- /** Normalize a `repository` attribution: trim, and map a blank/whitespace-only
85
- * value to undefined so it is never serialized as a distinct empty repo
86
- * (downstream consumers treat absence as "the primary repo"). */
87
- function normalizeRepository(item) {
88
- const trimmed = item.repository?.trim();
89
- return trimmed
90
- ? { ...item, repository: trimmed }
91
- : { ...item, repository: undefined };
92
- }
91
+ .trim()
92
+ .min(1)
93
+ .describe("The 'owner/repo' this item pertains to (e.g. 'letsramp/api-insight'). Set it on every item — the primary's owner/repo for primary-repo objections, or a related repo's owner/repo for objections derived from that repository's diff.");
93
94
  // videoPath is deliberately absent from this input contract: it is attached server-side
94
95
  // from the run's execution records (see attachVideoPath), and zod strips any the model
95
96
  // supplies anyway. SKYR-4156 is what happens when the agent owns that field instead.
@@ -347,7 +348,7 @@ const issueFoundSchema = z
347
348
  .optional()
348
349
  .describe("The `id` of the plan defect this issue reports, from the `defects` list you registered with skyramp_register_test_plan. Spell it as that list spells it: an id the plan does not declare traces nothing. " +
349
350
  "Set it on the `category: bug` entry that reports the defect. A `defectId` on a `lint`, `type` or `config` entry does not report the defect — the report renders those in its own Configuration Errors section. " +
350
- "CHECKED: every plan defect has a `bug` issue that names it, and a `bug` entry with no `plannedTestId` traces through this defect when a delivered test that expects to fail cites it, or when you answered `defects:untested:<id>` at plan time. Leave it out for an issue the plan did not declare."),
351
+ "CHECKED: every plan defect has a `bug` issue that names it, and a `bug` entry with no `plannedTestId` traces through this defect when a delivered test that expects to fail cites it, when a `maintains` entry you registered cites it, or when you answered `defects:untested:<id>` at plan time. Leave it out for an issue the plan did not declare."),
351
352
  sourceFile: citationString.describe("Path of the application file whose code is missing or wrong, relative to the repository root (e.g. 'src/crud/products.py'). " +
352
353
  "REQUIRED when category is 'bug'. " +
353
354
  "For code that is MISSING — an unmounted router, an unregistered route, an import never added — name the file where the line should be, not the file that defines what is unmounted. " +
@@ -538,6 +539,12 @@ const testMaintenanceSchema = z.object({
538
539
  "For failing runs: failure name and one-line root cause, e.g. " +
539
540
  "'FAILED test_foo — check_schema fails, order_id=1 has discount from prior PATCH test'. " +
540
541
  "Empty string for VERIFY/IGNORE/DELETE entries where no after-execution was run."),
542
+ /** The ids of the plan defects this maintained test's own run proves. Written
543
+ * at report time, not at plan time: by here the test has been executed and the
544
+ * agent is recording what it saw, so naming a defect here never displaces the
545
+ * planned test that also proves it. A defect may be named by a planned test, by
546
+ * a maintenance row, or by both. */
547
+ defects: z.array(z.string()).default([]),
541
548
  // Server-populated from the verdict's own state section, falling back to the
542
549
  // run's primary repo. Always set: a consumer that has to treat absence as
543
550
  // "probably the primary" cannot tell a single-repo row from a mis-stamped one.
@@ -546,18 +553,28 @@ const testMaintenanceSchema = z.object({
546
553
  beforeStatus: z.nativeEnum(TestExecutionStatus),
547
554
  afterStatus: z.nativeEnum(TestExecutionStatus),
548
555
  });
549
- // LLM input contract: status fields are server-populated from stateFile.
556
+ // LLM input contract: status fields are server-populated from stateFile, and
557
+ // `defects` is attached from the per-file detail the agent drafts, not from the
558
+ // verdict rows this schema describes.
550
559
  const testMaintenanceLLMSchema = testMaintenanceSchema.omit({
551
560
  beforeStatus: true,
552
561
  afterStatus: true,
562
+ defects: true,
553
563
  });
554
564
  // LLM only drafts beforeDetails/afterDetails per file — action/testType/endpoint/description
555
565
  // come from stateData.maintenanceVerdicts. Picked from testMaintenanceLLMSchema to avoid
556
566
  // redeclaring those fields.
557
- const testMaintenanceDetailSchema = testMaintenanceLLMSchema.pick({
567
+ const testMaintenanceDetailSchema = testMaintenanceLLMSchema
568
+ .pick({
558
569
  testFilePath: true,
559
570
  beforeDetails: true,
560
571
  afterDetails: true,
572
+ })
573
+ .extend({
574
+ defects: z
575
+ .array(z.string())
576
+ .optional()
577
+ .describe("The ids of the plan defects this test's own run proves, from the `defects` list you registered with skyramp_register_test_plan. Name a defect here when this test failed on it — the failure you summarised in `afterDetails` is the evidence. Spell each id as that list spells it; the report is rejected if an id is not one the plan declares. Naming a defect here does not replace a planned test that proves it: name it here as well."),
561
578
  });
562
579
  /** The key an answer is matched by: the objection's id AND its message. The id
563
580
  * alone is not unique — one verifier raises several different objections under
@@ -647,45 +664,156 @@ async function attachAssertionOutcome(test, outcomes, checkouts) {
647
664
  return test;
648
665
  return { ...test, assertions: await rederiveAssertionOutcome(record) };
649
666
  }
667
+ /**
668
+ * The absolute path of a `newTestsCreated` row's delivered file, from the paths the
669
+ * generators recorded — or the reason it cannot be answered, which the row states.
670
+ *
671
+ * A created row carries a `fileName` the agent wrote, which may be a basename or a
672
+ * repository-relative path, so it cannot be resolved on its own. `generatedTests` is
673
+ * keyed by canonical absolute path and written by the generators, so the row selects
674
+ * from it through `testFileMatches` — the segment-aware predicate the delivered-
675
+ * matches-plan verifier already uses for this question. A row that names a directory
676
+ * matches only inside it; a bare basename matches on the name alone. Matching by
677
+ * basename and treating the directory as a tie-breaker let a single same-named file
678
+ * win against a row whose own path contradicted it, and the row then reported another
679
+ * file's helpers.
680
+ *
681
+ * Both sides are put in posix form first. That predicate reads a path-qualified
682
+ * candidate by its `/` separators while these keys carry the host's, so on Windows a
683
+ * uniquely recorded file matched nothing and its row said it could not be found.
684
+ * Untested: `path.sep` is `/` on the platforms this runs on, so no test on them can
685
+ * tell the normalization from its absence.
686
+ *
687
+ * An unresolved row returns a REASON rather than nothing. Nothing is read by the
688
+ * caller as "no file to derive from", which ships the ambiguous `reuse: null` this
689
+ * whole derivation exists to remove — so an ambiguity would have recreated the fault
690
+ * under a different cause.
691
+ */
692
+ function generatedPathFor(fileName, generated) {
693
+ const posix = (p) => p.split(path.sep).join("/");
694
+ const wanted = posix(fileName);
695
+ const matches = Object.keys(generated).filter((p) => testFileMatches(posix(p), wanted));
696
+ if (matches.length === 1)
697
+ return matches[0];
698
+ if (matches.length === 0)
699
+ return {
700
+ reason: `this run recorded no generated file matching ${fileName}, so its imports could not be read`,
701
+ };
702
+ // Several specs of one name in different directories, and a row that names only
703
+ // the name. Reading the wrong spec's imports is worse than reading none.
704
+ return {
705
+ reason: `${matches.length} generated files match ${fileName} and this row does not say which, so its imports could not be read`,
706
+ };
707
+ }
650
708
  async function attachReuseOutcome(test, outcomes, handOffs, retrofits,
651
709
  // A blocking verdict measured on the delivered files is collected here rather
652
710
  // than written into the row: the caller refuses the report on it. Required, not
653
711
  // defaulted — a caller that forgot it would ship the report with no refusal and
654
712
  // no compiler error. See the refusal below the row map.
655
- blocking) {
713
+ blocking,
714
+ /** Declines this run removed from the delivered files. See stripDeliveredGateMarkers. */
715
+ carriedDeclines,
716
+ /** Filled with the carried declines a finding consumed. What consumed nothing is
717
+ * retired: permission that covers no fault authorises the NEXT one instead. */
718
+ consumed,
719
+ /** Filled with the files whose declines this pass actually MEASURED. Only those may
720
+ * be retired: a pass that read nothing consumes nothing, and deleting on that
721
+ * silence drops permission while the fault it covers is still in the file. */
722
+ evaluated,
723
+ /** The row's absolute path when the caller has it (a maintenance row does; a
724
+ * newTestsCreated row carries a basename only). With it, a record or hand-off
725
+ * that names a same-named spec in another directory is not this row's. */
726
+ specPath,
727
+ /** The delivered file this row is about, absolute — a maintenance row's own path,
728
+ * or the path the generators recorded for a newTestsCreated row. Used ONLY for the
729
+ * import-derived fallback below, never for record matching, so passing it cannot
730
+ * change which record a row attaches.
731
+ *
732
+ * A `{ reason }` says the row HAS a delivered file that could not be identified,
733
+ * and the row states that reason. Omitting it altogether says the row has no
734
+ * delivered file to read — a VERIFY row, or one this run never wrote. */
735
+ deliveredPath) {
656
736
  // POM records describe browser specs; a basename collision with an API test's
657
737
  // fileName must not attach them there. A utils-path record is attachable anywhere.
658
- const record = outcomes?.[path.basename(test.fileName)];
738
+ const byBasename = outcomes?.[path.basename(test.fileName)];
739
+ const record = specPath &&
740
+ byBasename?.testFilePath &&
741
+ !samePath(byBasename.testFilePath, specPath)
742
+ ? undefined
743
+ : byBasename;
744
+ if (specPath && handOffs) {
745
+ handOffs = Object.fromEntries(Object.entries(handOffs).filter(([file]) => samePath(file, specPath)));
746
+ }
659
747
  // A utils record carries the test type its verify ran under: a mismatch is a
660
748
  // basename collision with another test, not this row's outcome.
661
749
  const typeMatches = !record?.utils?.testType || record.utils.testType === test.testType;
662
750
  const found = record && typeMatches && (test.testType === TestType.UI || record.utils)
663
751
  ? record
664
752
  : undefined;
665
- const rederived = found ? await rederiveReuse(found) : undefined;
753
+ const rederived = found
754
+ ? await rederiveReuse(found, carriedDeclines)
755
+ : undefined;
756
+ consumed.push(...(rederived?.carriedConsumed ?? []));
757
+ // A measured pass answers for every decline it was handed, including the ones it
758
+ // found nothing for — that is the answer retirement needs. An unmeasured one
759
+ // (no record attached here, a missing spec identity, or a verifier that threw)
760
+ // answers for none of them, and its files stay out of this set.
761
+ if (rederived?.carriedEvaluated)
762
+ for (const d of carriedDeclines)
763
+ if (d.file)
764
+ evaluated.add(d.file);
666
765
  const derived = rederived?.outcome;
667
766
  if (rederived?.blocking)
668
767
  blocking.push(rederived.blocking);
669
- // Pre-existing generated tests this spec's reuse pass rewired (SKYR-4276 A4): the
670
- // report names each with its recorded execution, so a reviewer sees that the
671
- // module became a dependency of code they already owned — and that it still runs.
672
- if (derived?.helpers && found?.testFilePath) {
673
- const specPath = found.testFilePath;
674
- const mine = retrofits.filter((r) => samePath(r.testFile, specPath));
675
- if (mine.length > 0)
676
- derived.helpers.retrofits = mine.map((r) => ({
677
- file: path.basename(r.file),
678
- ...(r.execution ? { execution: r.execution } : {}),
679
- }));
680
- }
681
768
  // The RAW record: a colliding record of any kind means reuse ran for this basename,
682
- // and the chain claim must not be made on the filtered view.
683
- const chainSkipped = await reuseChainSkipped(test.fileName, test.testType, record, handOffs);
769
+ // and the chain claim must not be made on the filtered view — pendingReuseChain
770
+ // fails open on a record it cannot attribute to the spec in front of it, so
771
+ // claiming the chain was skipped here would put the report and the execute gate
772
+ // in direct disagreement. The hand-offs ARE the path-filtered view, which is the
773
+ // same exact-path lookup that gate makes.
774
+ const chainSkipped = await reuseChainSkipped(test.fileName, test.testType, byBasename, handOffs);
775
+ // No record and no chain claim leaves the row with `reuse: null`, which reads
776
+ // identically to "this test reused nothing" — and is read that way. The record is
777
+ // written by the verify pass, so a row only ever got one when the agent chose to
778
+ // call `skyramp_reuse_code` with `verify: true` on that file: a field describing
779
+ // the tool's own activity, presented as a fact about the delivered test. Contract
780
+ // rows are where it shows, because nothing hands the contract chain a reuse step,
781
+ // but the delivered file still imports and calls the helpers.
782
+ //
783
+ // So answer the row from the file instead. Only where there is nothing else to
784
+ // report: a row that already carries a record or a chain claim is untouched, and
785
+ // this can neither overwrite a measured value nor refuse a report.
786
+ const fallback = !derived && !chainSkipped && deliveredPath && isUtilsReuseEnabled()
787
+ ? typeof deliveredPath === "string"
788
+ ? await deriveReuseFromDelivered(deliveredPath)
789
+ : { notDerived: deliveredPath.reason }
790
+ : undefined;
684
791
  const reuse = derived || chainSkipped
685
792
  ? { ...(derived ?? {}), ...(chainSkipped ? { chainSkipped } : {}) }
686
- : undefined;
793
+ : fallback;
687
794
  if (!reuse)
688
795
  return test;
796
+ // Pre-existing generated tests this spec's reuse pass rewired (SKYR-4276 A4): the
797
+ // report names each with its recorded execution, so a reviewer sees that the module
798
+ // became a dependency of code they already owned — and that it still runs.
799
+ //
800
+ // Attached to WHICHEVER helpers outcome reached the row, from the row's own spec
801
+ // path. Gating it on a stored record instead dropped every retrofit from a row
802
+ // answered by the fallback — and the retrofit gate still refuses the report until
803
+ // each one has run, so the run was blocked on a disclosure the report never made.
804
+ const rowSpecPath = found?.testFilePath ??
805
+ (typeof deliveredPath === "string" ? deliveredPath : undefined);
806
+ if (reuse.helpers && rowSpecPath) {
807
+ const mine = retrofits.filter((r) => samePath(r.testFile, rowSpecPath));
808
+ if (mine.length > 0)
809
+ reuse.helpers = {
810
+ ...reuse.helpers,
811
+ retrofits: mine.map((r) => ({
812
+ file: path.basename(r.file),
813
+ ...(r.execution ? { execution: r.execution } : {}),
814
+ })),
815
+ };
816
+ }
689
817
  // A record that re-derives to nothing (e.g. a utils path that wrote no file) must
690
818
  // not attach an empty object — consumers treat presence as "reuse ran".
691
819
  const hasContent = Object.values(reuse).some((v) => v !== undefined);
@@ -708,6 +836,164 @@ function attachVideoPath(row, videos) {
708
836
  videoPath: videos?.[path.basename(row.testFilePath)]?.videoPath,
709
837
  };
710
838
  }
839
+ /**
840
+ * Remove every comment addressed to a Skyramp gate from the files this run delivers.
841
+ *
842
+ * Two gates ask the agent to write a decline INTO the delivered file — the utils
843
+ * verifier's `reuse-verify: allow` and the POM gate's `// kept inline:` — because a
844
+ * gate on a later run can only read what the repository holds. The customer ends up
845
+ * owning both, and a marker records the state at the moment it was written: nothing
846
+ * re-checks it when the file changes afterwards, so a delivered file can carry a
847
+ * claim its own code contradicts.
848
+ *
849
+ * Called only once the report is WRITTEN, which is the one point that is certainly
850
+ * the accepted path. A refusal must leave every marker in place: the agent repairs
851
+ * and resubmits, and a stripped marker would fire the gate again on a fault it has
852
+ * already declined. Nothing downstream reads a marker — the re-derivation that
853
+ * builds the report rows has already run, and the action stages current working-tree
854
+ * content before it commits (`git update-index --again`, plus the test-directory
855
+ * adds), so an edit made here reaches the commit without any change on that side.
856
+ *
857
+ * The accepted cost: a later run re-detects the same fault and the agent declines it
858
+ * again. That costs turns inside the run and never reaches the customer — and each
859
+ * run then judges the file in front of it rather than honouring a verdict taken
860
+ * against an older one.
861
+ *
862
+ * Best-effort throughout. The report is already on disk, so nothing here may throw.
863
+ */
864
+ /**
865
+ * The delivered files one reuse record owns: the spec itself, and the shared modules
866
+ * located beside it.
867
+ *
868
+ * Only files this RUN changed. `locateUtilsFiles` also returns a module the spec
869
+ * merely imports, and a marker an older run left in one of those is not this run's to
870
+ * remove — editing it would put a file the run never touched into the customer's pull
871
+ * request. When the change list cannot be read the located set is used unchanged,
872
+ * which is the behaviour every other reuse check falls back to.
873
+ */
874
+ export async function deliveredFilesOf(record, changed) {
875
+ const testFile = record.testFilePath;
876
+ if (!testFile)
877
+ return [];
878
+ // Both sides of the comparison are canonicalised HERE, because this function owns
879
+ // the comparison. `listChangedFilesAbs` resolves its paths and the located files are
880
+ // real paths, so on a repository whose path traverses a symlink the two spellings
881
+ // never matched and nothing was stripped — silently, and only on those setups.
882
+ const changedReal = changed
883
+ ? new Set(await Promise.all([...changed].map(realpath)))
884
+ : undefined;
885
+ // The spec takes the same membership check as the modules beside it. Exempting it
886
+ // let the strip edit a spec this run never touched — the same fault as an
887
+ // imported-only module, and the same new diff in the customer's pull request.
888
+ const spec = await realpath(testFile);
889
+ const files = !changedReal || changedReal.has(spec) ? [spec] : [];
890
+ try {
891
+ for (const utilsFile of await locateUtilsFiles({
892
+ testFile,
893
+ language: record.language,
894
+ })) {
895
+ const real = await realpath(utilsFile);
896
+ if (!changedReal || changedReal.has(real))
897
+ files.push(real);
898
+ }
899
+ }
900
+ catch (err) {
901
+ logger.warning("Could not locate the shared utils files for a delivered spec", { testFile, error: String(err) });
902
+ }
903
+ return files;
904
+ }
905
+ async function stripDeliveredGateMarkers(outcomes, retrofits,
906
+ /** Absolute paths this run changed, or undefined when they cannot be read. */
907
+ changed) {
908
+ const stripped = {};
909
+ const files = new Set();
910
+ // Each grammar has ONE file role its gate reads it from: `verifyUtils` reads
911
+ // `reuse-verify: allow` out of a located utils module, and the re-derivation reads
912
+ // `// kept inline:` out of the record's spec. Parsing both out of every file made a
913
+ // marker in the wrong place MORE powerful than the gate treats it: an allow the
914
+ // agent wrote into the spec is ignored where it sits, but was recorded here and
915
+ // then authorised the next matching helper fault. The strip still removes it —
916
+ // nothing addressed to the gate ships — it is only not carried.
917
+ const specPaths = new Set(await Promise.all(Object.values(outcomes ?? {})
918
+ .map((r) => r?.testFilePath)
919
+ .filter((p) => !!p)
920
+ .map(realpath)));
921
+ // Every path is canonicalised here and in the record, for the reason `reuseHandOffs`
922
+ // keys on `realpath`: a tool-supplied path may reach the same file through a symlink
923
+ // (macOS resolves /tmp to /private/tmp), so a raw key written by one source and read
924
+ // from another names one file twice.
925
+ // Every file this run CHANGED, when the change list can be read. Recording a reuse
926
+ // outcome is best-effort, so a changed file with no record would never be scanned —
927
+ // and its marker would ship, against what AC5 and this tool's description promise.
928
+ // The records are the fallback for when the change list is not available.
929
+ // Canonicalised on the way in, like every other source. `listChangedFilesAbs` only
930
+ // RESOLVES its names, against the root git was given, so a checkout reached through
931
+ // a symlink keeps that spelling — and the record would name a file the next
932
+ // submission looks up by real path, losing the decline it just carried.
933
+ if (changed)
934
+ for (const file of changed)
935
+ files.add(await realpath(file));
936
+ else
937
+ for (const record of Object.values(outcomes ?? {}))
938
+ for (const file of await deliveredFilesOf(record ?? {}, undefined))
939
+ files.add(file);
940
+ // Pre-existing generated tests this run rewired: the delivery commits them too.
941
+ for (const retrofit of retrofits)
942
+ files.add(await realpath(retrofit.file));
943
+ for (const file of files) {
944
+ const spec = utilsSpecForFile(file);
945
+ if (!spec)
946
+ continue;
947
+ try {
948
+ const before = await fs.readFile(file, "utf-8");
949
+ const { content, removed, unremovable } = stripGateMarkers(before, spec);
950
+ if (unremovable.length > 0) {
951
+ logger.warning("A gate-addressed comment could not be removed from a delivered file without risking its syntax — it ships as it stands", { file, markers: unremovable });
952
+ }
953
+ if (removed.length === 0)
954
+ continue;
955
+ // The last check before the customer's file changes, and the only one that does
956
+ // not depend on the strip being right. Every defect this had was a case nobody
957
+ // foresaw; this states the one edit that is permitted and refuses the rest, so
958
+ // an unforeseen case delivers the marker instead of a file that does not build.
959
+ if (!onlyCommentLinesRemoved(before, content, spec.commentPrefix)) {
960
+ logger.warning("The gate-comment strip would have changed more than whole comment lines — the file is left exactly as delivered", { file, removed });
961
+ continue;
962
+ }
963
+ await fs.writeFile(file, content, "utf-8");
964
+ // Parsed, not kept as text: what a later call needs is WHICH decline was made,
965
+ // so it can read it the way it reads a marker still in the file. Keeping the
966
+ // line and appending it back edited the customer's file a second time, and an
967
+ // agent that reformatted or reworded the decline in between defeated the
968
+ // substring match in both directions.
969
+ const text = removed.join("\n");
970
+ const declines = [
971
+ ...((await hasUtilsHeader(file))
972
+ ? parseUtilsAllows(text, spec).allows.map(({ kind, helper }) => ({
973
+ kind,
974
+ helper,
975
+ }))
976
+ : []),
977
+ ...(specPaths.has(file)
978
+ ? parseKeptInline(text).map(({ pageObject, reason }) => ({
979
+ keptInline: pageObject,
980
+ reason,
981
+ }))
982
+ : []),
983
+ ];
984
+ if (declines.length > 0)
985
+ stripped[file] = declines;
986
+ logger.info("Removed gate-addressed comment(s) from a delivered file", {
987
+ file,
988
+ removed,
989
+ });
990
+ }
991
+ catch (err) {
992
+ logger.warning("Could not strip gate-addressed comments from a delivered file", { file, error: String(err) });
993
+ }
994
+ }
995
+ return stripped;
996
+ }
711
997
  /** Two-space indent on every line, so a multi-line verdict reads as one bullet's body. */
712
998
  function indent(text) {
713
999
  return text
@@ -740,20 +1026,13 @@ function deduplicateById(items) {
740
1026
  * basename. A file can own several rows: failed if ANY failed, first failure
741
1027
  * carries the text; `Skipped` rows are evidence in neither direction. */
742
1028
  function collectExecutionOutcomes(delivered, results) {
743
- // Absent names nothing to disagree with, so it matches any — the same rule
744
- // `checkEveryDeliveredTestRan` uses.
745
- const sameRepository = (a, b) => {
746
- const left = (a ?? "").trim();
747
- const right = (b ?? "").trim();
748
- return left === "" || right === "" || left === right;
749
- };
750
1029
  const outcomes = [];
751
1030
  for (const test of delivered) {
752
1031
  const plannedTestId = test.plannedTestId?.trim();
753
1032
  if (!plannedTestId)
754
1033
  continue;
755
1034
  const rows = results.filter((row) => row.status !== "Skipped" &&
756
- sameRepository(row.repository, test.repository) &&
1035
+ row.repository === test.repository &&
757
1036
  testFileMatches(row.testFilePath, test.fileName));
758
1037
  if (rows.length === 0)
759
1038
  continue;
@@ -819,23 +1098,15 @@ function checkOneEntryPerCandidate(delivered) {
819
1098
  * a planned test with no outcome, so a filename the results do not carry was the
820
1099
  * way past it. This is the check that says so. */
821
1100
  function checkEveryDeliveredTestRan(delivered, results) {
822
- // A side that names no repository names none to disagree with, so it matches
823
- // any. A single-repo run fills the field on one list and not the other often
824
- // enough that requiring both to agree reported tests that had plainly run.
825
- const sameRepository = (a, b) => {
826
- const left = (a ?? "").trim();
827
- const right = (b ?? "").trim();
828
- return left === "" || right === "" || left === right;
829
- };
830
1101
  return delivered
831
- .filter((test) => !results.some((row) => sameRepository(row.repository, test.repository) &&
1102
+ .filter((test) => !results.some((row) => row.repository === test.repository &&
832
1103
  testFileMatches(row.testFilePath, test.fileName)))
833
1104
  .map((test) => {
834
1105
  // Say which half failed. "No row names that file" reads as a missing run
835
1106
  // when the file is there and only the repository differs.
836
1107
  const byName = results.filter((row) => testFileMatches(row.testFilePath, test.fileName));
837
1108
  const evidence = byName.length
838
- ? `reported as ${test.fileName} for ${test.repository?.trim() || "no repository"}, and the ${byName.length === 1 ? "row" : "rows"} naming that file ${byName.length === 1 ? "is" : "are"} for ${byName.map((row) => row.repository?.trim() || "no repository").join(", ")}`
1109
+ ? `reported as ${test.fileName} for ${test.repository}, and the ${byName.length === 1 ? "row" : "rows"} naming that file ${byName.length === 1 ? "is" : "are"} for ${byName.map((row) => row.repository).join(", ")}`
839
1110
  : `reported as ${test.fileName}, no testResults row names that file`;
840
1111
  return {
841
1112
  objectionId: `deliveredTestRan:${test.testId}`,
@@ -846,15 +1117,118 @@ function checkEveryDeliveredTestRan(delivered, results) {
846
1117
  };
847
1118
  });
848
1119
  }
849
- /** The four types a plan can express. `registerTestPlanTool`'s `PLAN_TEST_TYPES`
850
- * is the same list; the report accepts every `TestType`, so an entry outside
851
- * these four can never name a plan planned test and must not be asked to. */
852
- const PLANNABLE_TEST_TYPES = new Set([
853
- TestType.CONTRACT,
854
- TestType.INTEGRATION,
855
- TestType.E2E,
856
- TestType.UI,
857
- ]);
1120
+ /** A report cannot treat an explicit skip as a completed execution. Unlike an absent
1121
+ * result, this is a direct claim that the agent chose not to run a delivered test. */
1122
+ function skippedDeliveredTests(delivered, results) {
1123
+ return delivered.filter((test) => {
1124
+ const matching = results.filter((row) => row.repository === test.repository &&
1125
+ testFileMatches(row.testFilePath, test.fileName));
1126
+ return (matching.some((row) => row.status === "Skipped") &&
1127
+ !matching.some((row) => row.status !== "Skipped"));
1128
+ });
1129
+ }
1130
+ /** A planned test the plan typed `ui`. The plan's `testType` is free-form text —
1131
+ * the unapproved-entry check above says so — so it is trimmed and lower-cased
1132
+ * rather than compared against the enum. */
1133
+ function plannedUiTests(plannedTests) {
1134
+ return plannedTests.filter((test) => !!test?.plannedTestId?.trim() &&
1135
+ String(test?.scenario?.testType ?? "")
1136
+ .trim()
1137
+ .toLowerCase() === TestType.UI);
1138
+ }
1139
+ /** Planned UI tests with no entry in the report: the recordings that never
1140
+ * happened, named by the ids the agent would otherwise only have to answer for. */
1141
+ /** Every planned test with no entry in the report, whatever its type. The UI
1142
+ * filter below narrows this; the budget-excuse gate does not, because a dropped
1143
+ * contract test is dropped the same way a dropped recording is. */
1144
+ function undeliveredPlannedTests(plannedTests, delivered) {
1145
+ const shipped = new Set(delivered
1146
+ .map((test) => test.plannedTestId?.trim())
1147
+ .filter((id) => !!id));
1148
+ return plannedTests.filter((test) => !!test?.plannedTestId?.trim() && !shipped.has(test.plannedTestId.trim()));
1149
+ }
1150
+ function undeliveredPlannedUiTests(plannedTests, delivered) {
1151
+ return undeliveredPlannedTests(plannedUiTests(plannedTests), delivered);
1152
+ }
1153
+ /** Whether the file can be read. The written record outlives the file, and a
1154
+ * path that exists but cannot be opened owes no run either — the objection
1155
+ * would name a file nobody can look at. */
1156
+ async function pathExists(file) {
1157
+ try {
1158
+ await fs.access(file, fsConstants.R_OK);
1159
+ return true;
1160
+ }
1161
+ catch {
1162
+ return false;
1163
+ }
1164
+ }
1165
+ /** Whether the recorded run is a run of what this run WROTE.
1166
+ *
1167
+ * An edited file is run twice — once before the edit for a baseline, once
1168
+ * after — and only the second says anything about the edit, so a file left with
1169
+ * just its baseline has not been run in the sense this check is about.
1170
+ *
1171
+ * A generated file has no baseline of its own, but the path may still carry an
1172
+ * older run: REGENERATE rewrites a file that already existed and was already
1173
+ * run, and generating twice to one path does the same. The run has to be later
1174
+ * than the write, or it is a run of bytes that are gone. An unreadable or
1175
+ * missing timestamp on either side falls back to "a run is a run", which is the
1176
+ * behaviour before this and never invents an objection. */
1177
+ function runOfWhatWasWritten(file) {
1178
+ if (file.how === "updated")
1179
+ return file.recordedPhase === "after";
1180
+ if (file.recordedPhase === undefined)
1181
+ return false;
1182
+ const wrote = file.generatedAt;
1183
+ // The execution record keeps an ISO string, written by the execution path
1184
+ // rather than here, so it is parsed rather than changed in this PR.
1185
+ const ran = Date.parse(file.executedAt ?? "");
1186
+ if (wrote === undefined || Number.isNaN(ran))
1187
+ return true;
1188
+ return ran >= wrote;
1189
+ }
1190
+ /** Every test file this run wrote has an execution this server performed.
1191
+ *
1192
+ * Informational, like every check here: it surfaces the gap for a person to read
1193
+ * and the agent can answer it. The agent is not blocked — a hard ban on running a
1194
+ * test outside the tool can dead-end a run, which is why the prompt states a
1195
+ * preference rather than a rule.
1196
+ *
1197
+ * The two lists it joins are both the server's own, keyed by canonical absolute
1198
+ * path: `generatedTests`, written by the generators, and `testExecutions`,
1199
+ * written by skyramp_execute_test. The agent supplies neither, so unlike the
1200
+ * report-side checks this cannot be satisfied by what the report claims.
1201
+ *
1202
+ * What it proves is that the file was handed to the runner. It is NOT proof that
1203
+ * a test ran: a runner can exit 0 having executed nothing. */
1204
+ function checkEveryWrittenTestRan(written) {
1205
+ return written
1206
+ .filter((file) => !runOfWhatWasWritten(file))
1207
+ .map((file) => ({
1208
+ // The path is the id, because the agent quotes the id back in `answers` and a
1209
+ // path is what it can recognise. Everything after the first colon is that
1210
+ // path, colons included — a reader must not split the id on every colon.
1211
+ objectionId: `testRanOnThisServer:${file.file}`,
1212
+ verifier: "testRanOnThisServer",
1213
+ // Says what is recorded, never that the file went unrun: the agent is
1214
+ // allowed to run a test with the repository's own runner, and a run it
1215
+ // drove itself is not visible here.
1216
+ message: file.how === "generated"
1217
+ ? "This run generated this test file, and this server recorded no run of it after it was written."
1218
+ : file.recordedPhase === "before"
1219
+ ? "This run edited this test file, and the only run of it this server recorded is from before the edit."
1220
+ : "This run edited this test file, and this server recorded no run of it.",
1221
+ evidence: file.recordedPhase === "before"
1222
+ ? `${file.file} — the only recorded skyramp_execute_test run of this file is the pre-edit baseline`
1223
+ : file.how === "generated" && file.recordedPhase !== undefined
1224
+ ? `${file.file} — the last recorded skyramp_execute_test run of this path is older than the file this run wrote`
1225
+ : `${file.file} — no skyramp_execute_test run of this file is recorded`,
1226
+ suggestion: "Either run the file with `skyramp_execute_test`, or answer this objection in `answers` — say how the test was run, or why it was not run. A `testResults` row does not answer this: the run is checked against the executions this server performed, not against what the report claims.",
1227
+ }));
1228
+ }
1229
+ /** The report accepts every `TestType`, so an entry outside the four a plan can
1230
+ * express can never name a planned test and must not be asked to. */
1231
+ const PLANNABLE_TEST_TYPES = new Set(TESTBOT_SUPPORTED_TEST_TYPES);
858
1232
  function runPostExecutionChecks(plan, delivered, results, issues,
859
1233
  /** The plan-only lane delivers nothing, so there a planned test is proof
860
1234
  * enough for a bug. Passed in, not read here, so the checks stay pure. */
@@ -864,9 +1238,12 @@ planOnly, maintained = []) {
864
1238
  // unplanned: a plan cannot hold one, so the objection would name a mistake the
865
1239
  // agent has no way to fix.
866
1240
  .filter((test) => PLANNABLE_TEST_TYPES.has(test.testType))
867
- .map((test) => test.plannedTestId?.trim())
868
- .filter((id) => !!id)
869
- .map((plannedTestId) => ({ plannedTestId }));
1241
+ .flatMap((test) => {
1242
+ const plannedTestId = test.plannedTestId?.trim();
1243
+ return plannedTestId
1244
+ ? [{ plannedTestId, repository: test.repository }]
1245
+ : [];
1246
+ });
870
1247
  return [
871
1248
  ...runPostExecutionCheck("deliveredMatchesPlan", "deliveredMatchesPlan", () => checkDeliveredMatchesPlan(plan, shipped)),
872
1249
  // The maintenance mirror: a `maintains` entry credited coverage at plan time,
@@ -881,17 +1258,37 @@ planOnly, maintained = []) {
881
1258
  ...runPostExecutionCheck("defectsReported", "defects:unreported", () => checkDefectsReported(plan, issues)),
882
1259
  ];
883
1260
  }
1261
+ /**
1262
+ * Dependency edits can land in the primary checkout, a separate tests checkout,
1263
+ * or a related repository. Verify each configured path once, skipping absent paths.
1264
+ */
1265
+ function collectDependencyVerificationRoots(primaryRepoRoot, testsRepoRoot, relatedRepos) {
1266
+ const roots = new Set([primaryRepoRoot]);
1267
+ if (testsRepoRoot) {
1268
+ roots.add(testsRepoRoot);
1269
+ }
1270
+ for (const repo of Object.values(relatedRepos ?? {})) {
1271
+ if (repo.repositoryPath) {
1272
+ roots.add(repo.repositoryPath);
1273
+ }
1274
+ }
1275
+ return Array.from(roots);
1276
+ }
884
1277
  export function registerSubmitReportTool(server) {
885
1278
  server.registerTool(TOOL_NAME, {
886
1279
  annotations: {
887
1280
  readOnlyHint: false,
888
- destructiveHint: true, // overwrites the report file on disk
1281
+ // Overwrites the report file on disk, AND edits delivered source files on an
1282
+ // accepted call: a comment addressed to a Skyramp gate is removed from each
1283
+ // file this run changed. See stripDeliveredGateMarkers.
1284
+ destructiveHint: true,
889
1285
  idempotentHint: false,
890
1286
  openWorldHint: false,
891
1287
  },
892
1288
  description: "Submit the final testbot report. Call this tool once, after all test analysis, generation and execution. " +
893
1289
  "This is the ONLY way to submit the report — do NOT write the report to a file manually. " +
894
- "If a check objects to what you reported, the objection names an id: fix what you can, and answer the rest through `answers` on a second call.",
1290
+ "If a check objects to what you reported, the objection names an id: fix what you can, and answer the rest through `answers` on a second call. " +
1291
+ "On an ACCEPTED call this tool also edits the files this run changed: a comment addressed to a Skyramp gate — `reuse-verify: allow …` or `// kept inline: …` — is removed from each of them, because it speaks to a gate and not to the customer who owns the file. Nothing else in those files is touched. A REFUSED call leaves every source file exactly as it found it; it still records its own run state, such as the refusal count and the objections it raised.",
895
1292
  inputSchema: {
896
1293
  businessCaseAnalysis: z
897
1294
  .string()
@@ -951,8 +1348,9 @@ export function registerSubmitReportTool(server) {
951
1348
  stateFile: z
952
1349
  .string()
953
1350
  .optional()
954
- .describe("Optional. A Testbot run finds its own state file, so leave this out. " +
955
- "Give a path only to name a state file the run would not find by itself. " +
1351
+ .describe("Optional, and a Testbot run leaves it out: the run finds its own state file. " +
1352
+ "A path is accepted only when it names that same file one from another run holds " +
1353
+ "another run's execution results, and the call is refused. " +
956
1354
  "The report is written beside it, and it holds the execution results, the " +
957
1355
  "page captures and the videos this report needs."),
958
1356
  },
@@ -967,7 +1365,9 @@ export function registerSubmitReportTool(server) {
967
1365
  // event in the finally. Empty (no properties) for non-enforced runs.
968
1366
  const languageTelemetry = {};
969
1367
  // The run's own state file, unless the agent named one.
970
- const stateFile = resolveRunStatePath(params.stateFile);
1368
+ const { path: stateFile, refusal } = resolveOwnRunStatePath(params.stateFile);
1369
+ if (refusal)
1370
+ return toolError(refusal);
971
1371
  if (!stateFile) {
972
1372
  return toolError("This run has no state file. Call skyramp_analyze_changes first — it writes the state file this report goes beside.");
973
1373
  }
@@ -1145,8 +1545,7 @@ export function registerSubmitReportTool(server) {
1145
1545
  const plannedTest = plannedById.get((t.plannedTestId ?? "").trim());
1146
1546
  if (!plannedTest)
1147
1547
  return true;
1148
- const plannedType = String(plannedTest.scenario?.testType ?? "").trim().toLowerCase();
1149
- return plannedType !== "" && plannedType !== t.testType;
1548
+ return plannedTest.scenario.testType !== t.testType;
1150
1549
  });
1151
1550
  if (unapproved.length > 0) {
1152
1551
  const plannedList = plannedById.size > 0
@@ -1168,13 +1567,10 @@ export function registerSubmitReportTool(server) {
1168
1567
  // Read once — the file can carry a large diffText and readRepoData would
1169
1568
  // re-parse it twice per repo.
1170
1569
  const fullMaintenanceState = await stateManager.readFullState();
1171
- // The state file's root names the primary only when a caller declared one
1172
- // (or wrote first). GITHUB_REPOSITORY is the trigger event's own repo, so
1173
- // it names the primary even for a run that passed neither repo argument.
1174
- const primaryRepository = fullMaintenanceState?.metadata?.repository?.trim() ||
1175
- process.env.GITHUB_REPOSITORY?.trim() ||
1176
- undefined;
1177
- const maintenanceSections = [{ data: stateData }];
1570
+ // The handler already refused above when the file was missing or empty,
1571
+ // and the run that created it names the primary on the root.
1572
+ const primaryRepository = fullMaintenanceState.metadata.repository;
1573
+ const maintenanceSections = [{ repository: primaryRepository, data: stateData }];
1178
1574
  for (const [repo, section] of Object.entries(fullMaintenanceState?.relatedRepos ?? {})) {
1179
1575
  // Only these two keys resolve back to the primary, already collected
1180
1576
  // above. Any other key — a case variant, or another name over the same
@@ -1189,27 +1585,16 @@ export function registerSubmitReportTool(server) {
1189
1585
  maintenanceSections.push({ repository: repo, data });
1190
1586
  }
1191
1587
  // Stamp every row, primary included and single-repo runs included — as the
1192
- // prompt already requires of the other report sections. A section without
1193
- // its own name is the primary's, so it takes the run's primary repo.
1194
- const repositoryOf = (section) => section.repository ?? primaryRepository ?? "";
1195
- // Required field, so refuse rather than stamp a blank one: nothing named
1196
- // the primary and the run is not on a GitHub runner either. Only sections
1197
- // that actually produce rows need a name — a run with no verdicts at all
1198
- // has nothing to attribute and must still be able to submit.
1199
- if (!primaryRepository &&
1200
- maintenanceSections.some((section) => !section.repository &&
1201
- (section.data.maintenanceVerdicts?.length ?? 0) > 0)) {
1202
- errorResult = toolError("Cannot name the repository for the maintenance rows: the stateFile records no primary repository " +
1203
- "and GITHUB_REPOSITORY is unset. Pass `repository` (and `primaryRepository` in a multi-repo run) " +
1204
- "to skyramp_analyze_changes, then re-run it.");
1205
- return errorResult;
1206
- }
1588
+ // prompt already requires of the other report sections. The primary's
1589
+ // section carries the name the state file's root records; a related repo's
1590
+ // carries its own section key.
1591
+ const repositoryOf = (section) => section.repository;
1207
1592
  // Reject rather than throw: a non-array here, or a missing testFilePath
1208
1593
  // below, escapes the handler and loses the analytics event in its `finally`.
1209
1594
  const malformedSections = maintenanceSections
1210
1595
  .filter(({ data }) => data.maintenanceVerdicts !== undefined &&
1211
1596
  !Array.isArray(data.maintenanceVerdicts))
1212
- .map(({ repository }) => repository ?? "the primary repo");
1597
+ .map(({ repository }) => repository);
1213
1598
  if (malformedSections.length > 0) {
1214
1599
  errorResult = toolError(`stateFile has a non-array maintenanceVerdicts for: ${malformedSections.join(", ")}. ` +
1215
1600
  `Re-run skyramp_actions for that repository to rewrite the section.`);
@@ -1225,9 +1610,11 @@ export function registerSubmitReportTool(server) {
1225
1610
  // verdict PER page object, so a spec-keyed map keeps only the last.
1226
1611
  const rawMaintenance = maintenanceSections.flatMap((section) => (section.data.maintenanceVerdicts ?? [])
1227
1612
  .filter((v) => {
1228
- if (v && typeof v.testFilePath === "string" && v.testFilePath.trim())
1613
+ if (v &&
1614
+ typeof v.testFilePath === "string" &&
1615
+ v.testFilePath.trim())
1229
1616
  return true;
1230
- badVerdicts.push(section.repository ?? "the primary repo");
1617
+ badVerdicts.push(section.repository);
1231
1618
  return false;
1232
1619
  })
1233
1620
  .map((v) => ({
@@ -1237,6 +1624,7 @@ export function registerSubmitReportTool(server) {
1237
1624
  testType: v.testType,
1238
1625
  endpoint: v.endpoint,
1239
1626
  action: v.action,
1627
+ reportOnly: v.reportOnly,
1240
1628
  // A baseline refresh (SKYR-4298) leaves the spec untouched, so name the
1241
1629
  // replaced PNGs here — the reviewer's cue to open the image diff in the PR.
1242
1630
  description: v.rebaselineSnapshots?.length
@@ -1273,6 +1661,12 @@ export function registerSubmitReportTool(server) {
1273
1661
  // draft a summary for it) — collected here and rejected below, rather than
1274
1662
  // silently shipping a blank field.
1275
1663
  const missingDetails = [];
1664
+ // An id no plan defect declares traces nothing, exactly as `defectId` on an
1665
+ // issue does not: without this the row could name a defect the run invented.
1666
+ const declaredDefectIds = new Set((stateData.plan?.defects ?? [])
1667
+ .map((d) => String(d?.id ?? "").trim())
1668
+ .filter((id) => id.length > 0));
1669
+ const undeclaredDefects = [];
1276
1670
  if (dedupedMaintenance.length > 0) {
1277
1671
  // beforeStatus/afterStatus are always stateFile-authoritative. beforeDetails/
1278
1672
  // afterDetails come from the LLM's drafted summary (testMaintenanceDetails) —
@@ -1299,20 +1693,39 @@ export function registerSubmitReportTool(server) {
1299
1693
  // must not satisfy the "drafted a summary" requirement.
1300
1694
  const beforeDetails = detail?.beforeDetails?.trim() ?? "";
1301
1695
  const afterDetails = detail?.afterDetails?.trim() ?? "";
1302
- if (recorded?.executionBefore && !beforeDetails)
1696
+ // A recorded run owes a summary. The Unknown placeholder a reservation
1697
+ // leaves when the run never landed a result (SKYR-4460) is not one:
1698
+ // there is no output to summarise, and the row publishes Unknown.
1699
+ const recordedBefore = recorded?.executionBefore &&
1700
+ recorded.executionBefore.status !== TestExecutionStatus.Unknown;
1701
+ const recordedAfter = recorded?.executionAfter &&
1702
+ recorded.executionAfter.status !== TestExecutionStatus.Unknown;
1703
+ if (recordedBefore && !beforeDetails)
1303
1704
  missingDetails.push(`${displayName} (beforeDetails)`);
1304
- if (recorded?.executionAfter && !afterDetails)
1705
+ if (recordedAfter && !afterDetails)
1305
1706
  missingDetails.push(`${displayName} (afterDetails)`);
1306
1707
  logger.info(`${displayName}: before=${beforeStatus} after=${afterStatus}`);
1708
+ const defects = (detail?.defects ?? [])
1709
+ .map((id) => String(id ?? "").trim())
1710
+ .filter((id) => id.length > 0);
1711
+ for (const id of defects)
1712
+ if (!declaredDefectIds.has(id))
1713
+ undeclaredDefects.push(`${displayName}: ${id}`);
1307
1714
  return {
1308
1715
  ...m,
1309
1716
  beforeDetails,
1310
1717
  afterDetails,
1718
+ defects,
1311
1719
  beforeStatus,
1312
1720
  afterStatus,
1313
1721
  };
1314
1722
  });
1315
1723
  }
1724
+ if (undeclaredDefects.length > 0) {
1725
+ errorResult = toolError(`${undeclaredDefects.length} maintenance row(s) name a defect the plan does not declare: ${undeclaredDefects.join(", ")}. ` +
1726
+ "Spell each id as your registered `defects` list spells it, or leave it out.");
1727
+ return errorResult;
1728
+ }
1316
1729
  if (missingDetails.length > 0) {
1317
1730
  const uniqueMissing = [...new Set(missingDetails)];
1318
1731
  errorResult = toolError(`${uniqueMissing.length} maintenance row(s) have a recorded execution but no drafted summary: ${uniqueMissing.join(", ")}. ` +
@@ -1324,6 +1737,9 @@ export function registerSubmitReportTool(server) {
1324
1737
  // rows below; an unexecuted one refuses the report (inside the SKYR-3883 block,
1325
1738
  // which already enumerates the working tree).
1326
1739
  let retrofitViews = [];
1740
+ /** Absolute paths this run changed. Undefined when they cannot be read — every
1741
+ * reuse check falls back to acting on what it located. */
1742
+ let changedFilesAbs;
1327
1743
  // SKYR-3883: in a testbot run, refuse to ship a report that claims file work
1328
1744
  // the working tree doesn't reflect. The delivery step can only commit what the
1329
1745
  // agent actually created/edited, so a report claiming otherwise erodes trust
@@ -1335,6 +1751,25 @@ export function registerSubmitReportTool(server) {
1335
1751
  const fullState = await stateManager.readFullState();
1336
1752
  const repoRoot = fullState?.metadata?.repositoryPath;
1337
1753
  if (repoRoot && repoRoot !== "unknown") {
1754
+ const dependencyRoots = collectDependencyVerificationRoots(repoRoot, getTestsRepoDir(), fullState?.relatedRepos);
1755
+ for (const dependencyRoot of dependencyRoots) {
1756
+ try {
1757
+ const dependencyVerification = await verifyTestDependencyChanges(dependencyRoot);
1758
+ if (!dependencyVerification.ok) {
1759
+ errorResult = toolError("The working tree violates the safe test-only dependency policy:\n" +
1760
+ dependencyVerification.violations
1761
+ .map((violation) => ` - ${violation}`)
1762
+ .join("\n") +
1763
+ "\nMove approved packages to the required development/test scope, remove unapproved or unrelated manifest edits, run skyramp_verify_test_dependencies, and submit the report again.");
1764
+ return errorResult;
1765
+ }
1766
+ }
1767
+ catch (err) {
1768
+ errorResult = toolError(`Could not verify test dependency changes in ${dependencyRoot}: ${err.message}. ` +
1769
+ "Testbot will not submit the report until every configured repository can be checked.");
1770
+ return errorResult;
1771
+ }
1772
+ }
1338
1773
  try {
1339
1774
  // A run writes tests into up to three kinds of trees: the primary
1340
1775
  // checkout, the tests-repo checkout (testRepoPath), and related
@@ -1363,7 +1798,11 @@ export function registerSubmitReportTool(server) {
1363
1798
  changedFiles,
1364
1799
  newTests: dedupedNewTests,
1365
1800
  verdicts: stateData.maintenanceVerdicts ?? [],
1366
- primaryRepository: fullState?.metadata?.repository,
1801
+ primaryRepository,
1802
+ knownRepositories: [
1803
+ primaryRepository,
1804
+ ...Object.keys(fullState?.relatedRepos ?? {}),
1805
+ ],
1367
1806
  // SKYR-4250: captured at prompt-render time, since planOnly is a
1368
1807
  // prompt-only argument (mirrors the SKYR-4185 language capture).
1369
1808
  planOnly: isPlanOnlyMode(),
@@ -1392,7 +1831,7 @@ export function registerSubmitReportTool(server) {
1392
1831
  `Files with actual working-tree changes:\n${changedList || " (none)"}`);
1393
1832
  return errorResult;
1394
1833
  }
1395
- const changedFilesAbs = await listChangedFilesAbs([
1834
+ changedFilesAbs = await listChangedFilesAbs([
1396
1835
  repoRoot,
1397
1836
  getTestsRepoDir(),
1398
1837
  ...Object.values(fullState?.relatedRepos ?? {}).map((section) => section.repositoryPath),
@@ -1417,7 +1856,7 @@ export function registerSubmitReportTool(server) {
1417
1856
  // `repository` is normalized BEFORE the check so the attribution the check
1418
1857
  // reconciles is the one the report ships, and so the stamp below survives
1419
1858
  // into the written file — this array is what the report is built from.
1420
- const issuesFound = params.issuesFound.map(normalizeRepository);
1859
+ const issuesFound = params.issuesFound;
1421
1860
  if (issuesFound.some((issue) => issue.sourceFile?.trim())) {
1422
1861
  const citations = await findInvalidSourceCitations({
1423
1862
  checkouts: await stateManager.listRepoCheckouts(),
@@ -1446,11 +1885,134 @@ export function registerSubmitReportTool(server) {
1446
1885
  const assertionCheckouts = await stateManager
1447
1886
  .listRepoCheckouts()
1448
1887
  .catch(() => []);
1888
+ // Declines an earlier accepted submission removed from the delivered files.
1889
+ // They are handed to the re-derivation rather than written back into the files:
1890
+ // the run still knows what it declined, and the customer's copy is edited once.
1891
+ //
1892
+ // Keyed by the SPEC that owns the file the decline came from. Handing every
1893
+ // row every decline let one about one spec's helper clear a live finding in
1894
+ // another spec that defines a helper of the same name.
1895
+ const changedSet = changedFilesAbs ? new Set(changedFilesAbs) : undefined;
1896
+ const declinesBySpec = new Map();
1897
+ /** Files whose declines some record can still reach. One no record owns can
1898
+ * never be consumed by anything, so it is retired without a measurement; one
1899
+ * that IS owned is retired only on a measurement that found no fault. */
1900
+ const reachableDeclineFiles = new Set();
1901
+ for (const [name, record] of Object.entries(stateData.reuseOutcomes ?? {})) {
1902
+ const mine = [];
1903
+ // Every file the record owns, NOT only the ones this run changed. The filter
1904
+ // decides what the strip may EDIT; a decline already recorded against a file
1905
+ // is this run's whatever the working tree says, and dropping it makes the
1906
+ // gate refuse a fault the run already declined.
1907
+ for (const file of await deliveredFilesOf(record ?? {}, undefined)) {
1908
+ if ((stateData.strippedDeclines?.[file] ?? []).length > 0)
1909
+ reachableDeclineFiles.add(file);
1910
+ mine.push(...(stateData.strippedDeclines?.[file] ?? []).map(
1911
+ // Tagged with where it was read from, so a consumption retires THAT
1912
+ // file's record and not an identical one stored for another spec.
1913
+ (d) => ({ ...d, file })));
1914
+ }
1915
+ if (mine.length > 0)
1916
+ declinesBySpec.set(name, mine);
1917
+ }
1449
1918
  const reuseBlocking = [];
1919
+ /** Carried declines a finding consumed on this call. */
1920
+ const consumedDeclines = [];
1921
+ /** Files whose stored declines a re-derivation actually measured on this call. */
1922
+ const evaluatedDeclineFiles = new Set();
1450
1923
  const sanitizedNewTests = await Promise.all(dedupedNewTests.map(async ({ scenarioFile: _sf, traceFile: _tf, frontendTrace: _ft, ...rest }) => {
1451
- const row = await attachReuseOutcome(normalizeRepository(rest), stateData.reuseOutcomes, stateData.reuseHandOffs, retrofitViews, reuseBlocking);
1924
+ const row = await attachReuseOutcome(rest, stateData.reuseOutcomes, stateData.reuseHandOffs, retrofitViews, reuseBlocking, declinesBySpec.get(path.basename(rest.fileName)) ?? [], consumedDeclines, evaluatedDeclineFiles, undefined, generatedPathFor(rest.fileName, stateData.generatedTests ?? {}));
1452
1925
  return attachAssertionOutcome(row, stateData.assertionOutcomes ?? {}, assertionCheckouts);
1453
1926
  }));
1927
+ const maintenanceRows = testMaintenance
1928
+ ? await Promise.all(testMaintenance.map(async ({ testFilePath, pomFile, reportOnly, ...row }) => {
1929
+ // `pomFile` is the file skyramp_actions told the agent to edit;
1930
+ // `testFilePath` stays the spec, which is what ran. Name both when
1931
+ // they differ, or a page-object edit is reported against the spec.
1932
+ // Maintenance rows still carry the absolute path here, so the
1933
+ // assertion summary uses an EXACT canonical-path lookup — no
1934
+ // basename ambiguity. This is what carries the maintenance
1935
+ // honesty labels (nothing-to-verify vs verified) into the
1936
+ // report instead of leaving them as tool text the agent can
1937
+ // paraphrase.
1938
+ const record = stateData.assertionOutcomes?.[canonicalTestPath(testFilePath)];
1939
+ const assertions = record
1940
+ ? await rederiveAssertionOutcome(record)
1941
+ : undefined;
1942
+ // A maintained test goes through modularize → reuse like a
1943
+ // generated one, so its row carries the same server-derived
1944
+ // reuse outcome, matched by its exact path. Wraps the
1945
+ // repository-attributed row, the same composition
1946
+ // newTestsCreated uses.
1947
+ return attachReuseOutcome({
1948
+ ...row,
1949
+ fileName: path.basename(testFilePath),
1950
+ ...(pomFile &&
1951
+ path.basename(pomFile) !== path.basename(testFilePath)
1952
+ ? { editedFileName: path.basename(pomFile) }
1953
+ : {}),
1954
+ ...(assertions ? { assertions } : {}),
1955
+ }, stateData.reuseOutcomes, stateData.reuseHandOffs, retrofitViews, reuseBlocking, declinesBySpec.get(path.basename(testFilePath)) ?? [], consumedDeclines, evaluatedDeclineFiles, testFilePath,
1956
+ // Only where the run edited the file and left a test standing.
1957
+ // VERIFY and IGNORE read a repository-owned test and changed nothing,
1958
+ // and DELETE removed it — none of the three has reuse to state, so a
1959
+ // sentence about what they import is noise on a complete row. A
1960
+ // report-only row keeps a real REGENERATE or DELETE while this run
1961
+ // touched nothing, so the action alone does not answer it; the same
1962
+ // pair of conditions `rowsLeavingCoverage` applies.
1963
+ MAINTENANCE_COVERAGE_ACTIONS.has(row.action) &&
1964
+ reportOnly !== true
1965
+ ? testFilePath
1966
+ : undefined);
1967
+ }))
1968
+ : undefined;
1969
+ // Retirement happens HERE: after BOTH row kinds have re-derived, and still
1970
+ // before any refusal can return. Running it between them left a decline owned
1971
+ // only by a maintained spec unmeasured at this point — retired on that silence
1972
+ // before the fix below, and then kept forever once silence stopped retiring. A decline whose fault
1973
+ // the agent repaired has nothing left to authorise, and a submission that is
1974
+ // then refused for an UNRELATED fault used to leave it in place — so while
1975
+ // repairing that one the agent could reintroduce the first and the next
1976
+ // submission would authorise it in silence. Only the physical strip has to wait
1977
+ // for the accepted path; the record does not.
1978
+ //
1979
+ // Reconciled whenever any decline is stored, not when a count says it changed:
1980
+ // `consumedDeclines` accumulates per report ROW, so two rows consuming one
1981
+ // decline made the totals match while a second, stale decline survived.
1982
+ if (Object.keys(stateData.strippedDeclines ?? {}).length > 0) {
1983
+ const budget = [...consumedDeclines];
1984
+ const kept = Object.fromEntries(Object.entries(stateData.strippedDeclines ?? {}).map(([file, stored]) => [
1985
+ file,
1986
+ // Retire on an ANSWER, not on a silence. A file a record still owns,
1987
+ // whose re-derivation did not measure it — no row for that spec on this
1988
+ // call, a record with no spec identity, a verifier that threw —
1989
+ // consumed nothing for want of a reading, not for want of a fault, and
1990
+ // deleting it there drops permission while the fault is still in the
1991
+ // file. A file no record owns is retired without a measurement: nothing
1992
+ // can consume it, so no reading would ever come.
1993
+ reachableDeclineFiles.has(file) &&
1994
+ !evaluatedDeclineFiles.has(file)
1995
+ ? stored
1996
+ : stored.filter((d) => {
1997
+ const i = budget.findIndex((c) => c.file === file &&
1998
+ c.kind === d.kind &&
1999
+ c.helper === d.helper &&
2000
+ c.keptInline === d.keptInline);
2001
+ if (i === -1)
2002
+ return false;
2003
+ budget.splice(i, 1);
2004
+ return true;
2005
+ }),
2006
+ ]));
2007
+ try {
2008
+ const latest = (await stateManager.readData()) ?? stateData;
2009
+ await stateManager.appendData({ ...latest, strippedDeclines: kept });
2010
+ stateData.strippedDeclines = kept;
2011
+ }
2012
+ catch (err) {
2013
+ logger.warning("Could not retire the declines this call did not consume — one may authorise a later fault", { error: String(err) });
2014
+ }
2015
+ }
1454
2016
  // A blocking reuse verdict on the delivered files refuses the report. The
1455
2017
  // live verify pass is a one-time checkpoint on a file that keeps changing —
1456
2018
  // the execution fix loop and a post-verification rewrite both edit after it —
@@ -1469,6 +2031,10 @@ export function registerSubmitReportTool(server) {
1469
2031
  // off than one with a disclosed fault (see REUSE_SUBMIT_MAX_REFUSALS). The
1470
2032
  // count is persisted per spec path; a count that cannot be persisted cannot
1471
2033
  // bound anything, so a failed write accepts rather than refuses forever.
2034
+ // Built BEFORE the blocking-verdict refusal below, not inside the report
2035
+ // object: a maintained test goes through the same reuse chain as a generated
2036
+ // one, so its delivered files can fail the same check, and a verdict collected
2037
+ // after the refusal would never refuse anything.
1472
2038
  // `reuseOutcomes` is keyed by basename, so two rows sharing a file name find
1473
2039
  // one record and push one verdict twice — deduplicated by path here, as
1474
2040
  // pendingReuseVerification skips that case on its side. Sorted by file so the
@@ -1484,7 +2050,10 @@ export function registerSubmitReportTool(server) {
1484
2050
  for (const v of owed)
1485
2051
  refusals[v.file] = (refusals[v.file] ?? 0) + 1;
1486
2052
  try {
1487
- await stateManager.writeData({ ...stateData, reuseRefusals: refusals });
2053
+ await stateManager.appendData({
2054
+ ...stateData,
2055
+ reuseRefusals: refusals,
2056
+ });
1488
2057
  }
1489
2058
  catch (err) {
1490
2059
  persisted = false;
@@ -1532,6 +2101,202 @@ export function registerSubmitReportTool(server) {
1532
2101
  test.expectedToFail = true;
1533
2102
  }
1534
2103
  }
2104
+ // Every file this run wrote, from the server's own records — one canonical
2105
+ // absolute path each, which is also the key an execution is recorded under.
2106
+ // Generated files come from the generators; edited files from the maintenance
2107
+ // triage, narrowed by `rowsLeavingCoverage` to UPDATE and REGENERATE. The rest
2108
+ // cannot be run: DELETE removed the file, VERIFY and IGNORE never edited one,
2109
+ // and a `reportOnly` row only advised a change this run did not make.
2110
+ //
2111
+ // A verdict names the SPEC, never `pomFile`: a POM-backed row edits the page
2112
+ // object, but the execution is recorded against the spec.
2113
+ // A file a DELETE verdict names is gone by report time, so no run is owed for
2114
+ // it. The verdicts keep every recommendation, not the winning one, so one file
2115
+ // can carry both a DELETE and an UPDATE — and the DELETE is what was applied.
2116
+ const deletedPaths = new Set(maintenanceSections.flatMap((section) => (section.data.maintenanceVerdicts ?? [])
2117
+ .filter((verdict) => verdict?.action === DriftAction.Delete &&
2118
+ verdict?.reportOnly !== true)
2119
+ .map((verdict) => canonicalTestPath(verdict.testFilePath))));
2120
+ const filesWritten = new Map();
2121
+ // When each generated path was last written, so a run recorded before that
2122
+ // write is not credited for the bytes that replaced it.
2123
+ const generatedAt = new Map();
2124
+ for (const section of maintenanceSections) {
2125
+ for (const [generated, record] of Object.entries(section.data.generatedTests ?? {}))
2126
+ if (!deletedPaths.has(generated)) {
2127
+ filesWritten.set(generated, "generated");
2128
+ if (typeof record?.generatedAt === "number")
2129
+ generatedAt.set(generated, record.generatedAt);
2130
+ }
2131
+ for (const verdict of rowsLeavingCoverage(section.data.maintenanceVerdicts)) {
2132
+ // A rename UPDATE moves the spec before it is executed, so the run is
2133
+ // recorded under the new name while `testFilePath` keeps the old one.
2134
+ const raw = verdict._suggestedNewFile?.trim() || verdict.testFilePath.trim();
2135
+ if (!raw)
2136
+ continue;
2137
+ // Canonical because that is how an execution is keyed: a checkout under a
2138
+ // symlinked temp or home directory spells one file two ways.
2139
+ const key = canonicalTestPath(raw);
2140
+ if (!filesWritten.has(key) && !deletedPaths.has(key))
2141
+ filesWritten.set(key, "updated");
2142
+ }
2143
+ }
2144
+ // The LATEST run of each file, because a re-run replaces the record. An edited
2145
+ // file needs a run of the edit, so its pre-edit baseline does not answer for
2146
+ // it — `phase` is what tells the two apart. A generated file has no baseline,
2147
+ // so any run of it is a run of what was written.
2148
+ const executions = new Map(maintenanceSections.flatMap((section) => Object.entries(section.data.testExecutions ?? {}).map(([file, record]) => [
2149
+ file,
2150
+ {
2151
+ phase: String(record?.phase ?? ""),
2152
+ executedAt: String(record?.executedAt ?? ""),
2153
+ },
2154
+ ])));
2155
+ // A record is never removed once written, so a file the agent generated and
2156
+ // then deleted would object forever — and name a path that is not there. What
2157
+ // is gone owes no run. Unreadable counts as gone: a false objection about a
2158
+ // file nobody can open is worse than the silence.
2159
+ const written = [];
2160
+ for (const [file, how] of filesWritten) {
2161
+ if (!(await pathExists(file)))
2162
+ continue;
2163
+ const run = executions.get(file);
2164
+ written.push({
2165
+ file,
2166
+ how,
2167
+ recordedPhase: run?.phase,
2168
+ executedAt: run?.executedAt,
2169
+ generatedAt: generatedAt.get(file),
2170
+ });
2171
+ }
2172
+ // The counts a reader compares at a glance, from the same two sets the check
2173
+ // below joins — so a shortfall here is exactly the files it names.
2174
+ const executionCoverage = {
2175
+ generated: written.filter((f) => f.how === "generated").length,
2176
+ updated: written.filter((f) => f.how === "updated").length,
2177
+ executed: written.filter(runOfWhatWasWritten).length,
2178
+ };
2179
+ // A skipped test is not an execution. Refuse before writing a report so the
2180
+ // agent must execute every delivered plan test instead of declaring a time
2181
+ // constraint in a Skipped row and ending the run.
2182
+ const registeredPlanTestIds = new Set(Array.isArray(stateData.plan?.plannedTests)
2183
+ ? stateData.plan.plannedTests
2184
+ .map((test) => test?.plannedTestId?.trim())
2185
+ .filter((id) => !!id)
2186
+ : []);
2187
+ const skippedTests = isPlanOnlyMode()
2188
+ ? []
2189
+ : skippedDeliveredTests(dedupedNewTests.filter((test) => registeredPlanTestIds.has((test.plannedTestId ?? "").trim())), params.testResults);
2190
+ if (skippedTests.length > 0) {
2191
+ errorResult = toolError(`Cannot submit the report: ${skippedTests.length} registered plan test${skippedTests.length === 1 ? "" : "s"} ${skippedTests
2192
+ .map((test) => `${test.plannedTestId?.trim() || test.testId}`)
2193
+ .join(", ")} ${skippedTests.length === 1 ? "has" : "have"} no non-skipped execution result. ` +
2194
+ "Do not skip because you believe you are low on context or time, or because you speculate that another attempt will not help. " +
2195
+ contextGaugeSentence() +
2196
+ "Run each remaining test with skyramp_execute_test, include its Pass or Fail result in testResults, then call skyramp_submit_report again.");
2197
+ return errorResult;
2198
+ }
2199
+ // A planned UI test with no report entry is a recording that never happened.
2200
+ // `deliveredMatchesPlan` already raises it and already accepts an answer, and
2201
+ // that is how one run shipped a 14-test plan with ZERO UI tests: a single
2202
+ // skyramp_export_zip, a trace that came out polluted, and an answer blaming
2203
+ // the recorder. Refuse while the run has recorded fewer times than it planned
2204
+ // UI tests; past that it has genuinely tried each one and the answer stands.
2205
+ //
2206
+ // WHY THE RETRY IS NOT FUTILE, and why the message says so: a successful
2207
+ // skyramp_export_zip DRAINS the recorder and closes the browser, so the next
2208
+ // recording starts from an empty buffer. Nothing the agent can read says that,
2209
+ // and that run reasoned the opposite and stopped one call short of a clean
2210
+ // trace.
2211
+ const undeliveredUi = isPlanOnlyMode() || planUnreadable
2212
+ ? []
2213
+ : undeliveredPlannedUiTests(stateData.plan?.plannedTests ?? [], dedupedNewTests);
2214
+ const plannedUiCount = planUnreadable
2215
+ ? 0
2216
+ : plannedUiTests(stateData.plan?.plannedTests ?? []).length;
2217
+ // ATTEMPTS, so one more recording always answers a refusal and the gate opens
2218
+ // after at most `plannedUiCount` of them. The refusal cap is the second bound,
2219
+ // for the agent that resubmits having recorded nothing: this check is meant to
2220
+ // cost a recording, never the whole run.
2221
+ const recordings = traceExportAttempts();
2222
+ if (undeliveredUi.length > 0 &&
2223
+ recordings < plannedUiCount &&
2224
+ uiDeliveryRefusals() < MAX_UI_DELIVERY_REFUSALS) {
2225
+ const refusal = countUiDeliveryRefusal();
2226
+ const ids = undeliveredUi
2227
+ .map((test) => test.plannedTestId.trim())
2228
+ .join(", ");
2229
+ errorResult = toolError(`Cannot submit the report: ${undeliveredUi.length} registered UI plan test${undeliveredUi.length === 1 ? "" : "s"} ` +
2230
+ `${ids} ${undeliveredUi.length === 1 ? "has" : "have"} no entry in newTestsCreated, and this run recorded ` +
2231
+ `${recordings} trace${recordings === 1 ? "" : "s"} for ${plannedUiCount} planned UI test${plannedUiCount === 1 ? "" : "s"}. ` +
2232
+ "Do not skip because you believe you are low on context or time, or because you speculate that another attempt will not help. " +
2233
+ contextGaugeSentence() +
2234
+ "A successful skyramp_export_zip clears the recording buffer and closes the browser, so the next recording starts clean — " +
2235
+ "a trace that came out polluted is a reason to record again, not a reason to stop. " +
2236
+ "Record each remaining test in its own pass (browser_navigate to the start URL, the flow, skyramp_export_zip, skyramp_ui_test_generation), " +
2237
+ "add it to newTestsCreated with its plannedTestId, then call skyramp_submit_report again. " +
2238
+ `The plan and every answer you have given are in the state file and survive this refusal (refusal ${refusal} of ${MAX_UI_DELIVERY_REFUSALS}).`);
2239
+ return errorResult;
2240
+ }
2241
+ // A planned test dropped because the run says it was running out.
2242
+ //
2243
+ // `deliveredMatchesPlan` raises "planned but not delivered" and suggests
2244
+ // "add the test to the report, or record in the report why it was dropped".
2245
+ // Run 9dfc8405 recorded the reason seven times — "this run exhausted its
2246
+ // working budget" — and shipped 2 of 9 planned tests with the report
2247
+ // ACCEPTED. It wrote that at 46% of a 1M context window with no warning from
2248
+ // anywhere, and the word appears nowhere in its reasoning: the budget was not
2249
+ // a finding, it was a sentence that closed an objection.
2250
+ //
2251
+ // This refuses that ONE sentence, not a budget. A test blocked by something
2252
+ // outside the run answers and ships exactly as before, which is why the
2253
+ // message says what a reason that stands looks like.
2254
+ //
2255
+ // Answers from earlier calls count too: one that closed the objection on
2256
+ // submit N still closes it on N+1 without being re-sent, and a gate reading
2257
+ // only this call would be walked past by not repeating it.
2258
+ const answerText = new Map();
2259
+ const carriedAnswers = Array.isArray(stateData.reportObjections?.answeredObjections)
2260
+ ? stateData.reportObjections.answeredObjections
2261
+ : [];
2262
+ for (const entry of carriedAnswers) {
2263
+ const id = entry
2264
+ ?.objection?.objectionId;
2265
+ const answer = entry?.answer;
2266
+ if (typeof id === "string" && typeof answer === "string")
2267
+ answerText.set(id, answer);
2268
+ }
2269
+ for (const entry of params.answers ?? []) {
2270
+ if (typeof entry?.objectionId === "string" &&
2271
+ typeof entry?.answer === "string")
2272
+ answerText.set(entry.objectionId, entry.answer);
2273
+ }
2274
+ const droppedForBudget = isPlanOnlyMode() || planUnreadable
2275
+ ? []
2276
+ : undeliveredPlannedTests(stateData.plan?.plannedTests ?? [], dedupedNewTests).filter((test) => citesBudget(answerText.get(`deliveredMatchesPlan:${test.plannedTestId.trim()}`)));
2277
+ if (droppedForBudget.length > 0 &&
2278
+ budgetExcuseRefusals() < MAX_BUDGET_EXCUSE_REFUSALS) {
2279
+ const refusal = countBudgetExcuseRefusal();
2280
+ const ids = droppedForBudget
2281
+ .map((test) => test.plannedTestId.trim())
2282
+ .join(", ");
2283
+ const plural = droppedForBudget.length === 1;
2284
+ errorResult = toolError(`Cannot submit the report: ${droppedForBudget.length} planned test${plural ? "" : "s"} ` +
2285
+ `${ids} ${plural ? "is" : "are"} missing from newTestsCreated, and the answer given for ` +
2286
+ `${plural ? "it" : "each"} is that the run was short of budget, context or time. ` +
2287
+ "That is not a reason this report accepts. " +
2288
+ contextGaugeSentence() +
2289
+ "A reason that stands names something outside the run that stopped the test: a service that is not " +
2290
+ "running, a branch that no longer exists, a credential this run does not hold, a behaviour the " +
2291
+ "application will not reach. Running low is not one of those — it is a reason to write the test now, " +
2292
+ "with the rest of the report already built. " +
2293
+ `Write the missing test${plural ? "" : "s"}, add ${plural ? "it" : "them"} to newTestsCreated with the ` +
2294
+ "plannedTestId, and call skyramp_submit_report again — or replace the answer with what actually " +
2295
+ "stopped it. " +
2296
+ `The plan, the report and every answer you have given are in the state file and survive this refusal ` +
2297
+ `(refusal ${refusal} of ${MAX_BUDGET_EXCUSE_REFUSALS}).`);
2298
+ return errorResult;
2299
+ }
1535
2300
  // Report-time checks on what the customer will read. They need no plan, so a
1536
2301
  // run without one still gets them.
1537
2302
  const reportChecks = [
@@ -1541,6 +2306,15 @@ export function registerSubmitReportTool(server) {
1541
2306
  ...(isPlanOnlyMode()
1542
2307
  ? []
1543
2308
  : runPostExecutionCheck("deliveredTestRan", "deliveredTestRan", () => checkEveryDeliveredTestRan(dedupedNewTests, params.testResults))),
2309
+ // Every lane, not just testbot: both records are written whenever the
2310
+ // tools run, and a file written and never run is the same defect
2311
+ // wherever it happens. Silent when the run recorded nothing — an older
2312
+ // state file has no `generatedTests`, and the check must not read that
2313
+ // as "nothing was run". Skipped in the plan-only lane, which has no
2314
+ // application to run anything against.
2315
+ ...(isPlanOnlyMode()
2316
+ ? []
2317
+ : runPostExecutionCheck("testRanOnThisServer", "testRanOnThisServer", () => checkEveryWrittenTestRan(written))),
1544
2318
  ];
1545
2319
  // Run against an EMPTY plan when none was submitted, never skipped: the gate
1546
2320
  // above covers contract and integration only. An empty plan raises exactly
@@ -1548,7 +2322,14 @@ export function registerSubmitReportTool(server) {
1548
2322
  let objections;
1549
2323
  let changeTable;
1550
2324
  {
1551
- const postExecution = runPostExecutionChecks(stateData.plan ?? { plannedTests: [], registrationNumber: 0, answers: [], openObjections: [], answeredObjections: [] }, dedupedNewTests, params.testResults, params.issuesFound ?? [], isPlanOnlyMode(),
2325
+ const postExecution = runPostExecutionChecks(stateData.plan ??
2326
+ {
2327
+ plannedTests: [],
2328
+ registrationNumber: 0,
2329
+ answers: [],
2330
+ openObjections: [],
2331
+ answeredObjections: [],
2332
+ }, dedupedNewTests, params.testResults, params.issuesFound ?? [], isPlanOnlyMode(),
1552
2333
  // The triage's own verdicts, not this tool's input, so the join is against
1553
2334
  // what the run is recorded as having edited — and only the rows that left a
1554
2335
  // test standing and were actually applied, so a no-op assessment, a deletion
@@ -1610,7 +2391,10 @@ export function registerSubmitReportTool(server) {
1610
2391
  // objection and answers it in the same call: fixing it stops the check
1611
2392
  // raising it, so matching against this call alone called the answer unknown
1612
2393
  // and objected to the agent for doing both (run 33946409570, sb19).
1613
- const answerable = [...raised, ...previouslyRaised.map((entry) => entry.objection)];
2394
+ const answerable = [
2395
+ ...raised,
2396
+ ...previouslyRaised.map((entry) => entry.objection),
2397
+ ];
1614
2398
  const reportStageObjections = [
1615
2399
  ...raised,
1616
2400
  ...unknownAnswerObjections(answerable, acknowledged, {
@@ -1623,24 +2407,31 @@ export function registerSubmitReportTool(server) {
1623
2407
  // their silence is not evidence: only a call carrying results can resolve an
1624
2408
  // earlier objection.
1625
2409
  const judgedThisCall = (params.testResults ?? []).length > 0;
2410
+ // These verifiers read the server's own records, not `testResults`, so
2411
+ // they reach a verdict on every call. An empty resubmission leaves them
2412
+ // judged, and an objection they no longer raise is resolved — without
2413
+ // this it would vanish from the report and sit in `raisedObjections`
2414
+ // for the rest of the run with nothing able to clear it.
2415
+ const stateBackedVerifiers = new Set(["testRanOnThisServer"]);
2416
+ const judged = (objection) => judgedThisCall || stateBackedVerifiers.has(objection.verifier);
1626
2417
  // Resolved = raised earlier, absent from a call that could have raised it.
1627
2418
  // One that comes back drops out: publishing it as both says two things.
1628
2419
  const resolvedObjections = [
1629
2420
  ...previouslyResolved,
1630
- ...(judgedThisCall
1631
- ? previouslyRaised
1632
- .filter((entry) => !raisedNow.has(answerKey(entry.objection)))
1633
- .map((entry) => ({
1634
- objection: entry.objection,
1635
- resolvedBy: `submission ${submissionNumber}`,
1636
- }))
1637
- : []),
2421
+ ...previouslyRaised
2422
+ .filter((entry) => judged(entry.objection) &&
2423
+ !raisedNow.has(answerKey(entry.objection)))
2424
+ .map((entry) => ({
2425
+ objection: entry.objection,
2426
+ resolvedBy: `submission ${submissionNumber}`,
2427
+ })),
1638
2428
  ].filter((entry, index, all) => !raisedNow.has(answerKey(entry.objection)) &&
1639
2429
  all.findIndex((other) => answerKey(other.objection) === answerKey(entry.objection)) === index);
1640
2430
  const answeredReportObjections = [];
1641
2431
  const openReportObjections = [];
1642
2432
  for (const objection of reportStageObjections) {
1643
- const answer = answerFor(objection, acknowledged) ?? carriedByKey.get(answerKey(objection))?.answer;
2433
+ const answer = answerFor(objection, acknowledged) ??
2434
+ carriedByKey.get(answerKey(objection))?.answer;
1644
2435
  if (answer)
1645
2436
  answeredReportObjections.push({ objection, answer });
1646
2437
  else
@@ -1715,14 +2506,19 @@ export function registerSubmitReportTool(server) {
1715
2506
  // corroborate a claim that an existing test covers the change.
1716
2507
  const editedPaths = [];
1717
2508
  for (const row of rowsLeavingCoverage(stateData.maintenanceVerdicts)) {
1718
- for (const full of [row?.testFilePath, row?.pomFile]) {
2509
+ for (const full of [
2510
+ row?.testFilePath,
2511
+ row?.pomFile,
2512
+ ]) {
1719
2513
  const path = typeof full === "string" ? full.trim() : "";
1720
2514
  if (path)
1721
2515
  editedPaths.push(path);
1722
2516
  }
1723
2517
  }
1724
2518
  const maintainedByChange = new Map();
1725
- for (const entry of Array.isArray(stateData.plan?.maintains) ? stateData.plan.maintains : []) {
2519
+ for (const entry of Array.isArray(stateData.plan?.maintains)
2520
+ ? stateData.plan.maintains
2521
+ : []) {
1726
2522
  const file = String(entry?.file ?? "").trim();
1727
2523
  if (!file || !Array.isArray(entry?.changes))
1728
2524
  continue;
@@ -1742,7 +2538,9 @@ export function registerSubmitReportTool(server) {
1742
2538
  for (const objection of objections) {
1743
2539
  if (!objection.objectionId.startsWith(CHANGE_OBJECTION_PREFIX))
1744
2540
  continue;
1745
- const answer = typeof objection.answer === "string" ? objection.answer.trim() : "";
2541
+ const answer = typeof objection.answer === "string"
2542
+ ? objection.answer.trim()
2543
+ : "";
1746
2544
  if (answer)
1747
2545
  answerByChange.set(objection.objectionId.slice(CHANGE_OBJECTION_PREFIX.length), answer);
1748
2546
  }
@@ -1750,7 +2548,8 @@ export function registerSubmitReportTool(server) {
1750
2548
  const id = String(change?.id ?? "").trim();
1751
2549
  const testedBy = dedupedNewTests
1752
2550
  .map((test) => (test.plannedTestId ?? "").trim())
1753
- .filter((plannedTestId) => plannedTestId && (changesByCandidate.get(plannedTestId) ?? []).includes(id));
2551
+ .filter((plannedTestId) => plannedTestId &&
2552
+ (changesByCandidate.get(plannedTestId) ?? []).includes(id));
1754
2553
  const answer = answerByChange.get(id);
1755
2554
  const maintainedBy = maintainedByChange.get(id) ?? [];
1756
2555
  return {
@@ -1771,7 +2570,7 @@ export function registerSubmitReportTool(server) {
1771
2570
  // The report is the deliverable, so a failed state write must not cost
1772
2571
  // it; the answer is simply repeated on the next call.
1773
2572
  try {
1774
- await stateManager.writeData({
2573
+ await stateManager.appendData({
1775
2574
  ...stateData,
1776
2575
  reportObjections: {
1777
2576
  answeredObjections: [
@@ -1785,9 +2584,8 @@ export function registerSubmitReportTool(server) {
1785
2584
  objection,
1786
2585
  submission: firstRaisedAt.get(answerKey(objection)) ?? submissionNumber,
1787
2586
  })),
1788
- ...(judgedThisCall
1789
- ? []
1790
- : previouslyRaised.filter((entry) => !raisedNow.has(answerKey(entry.objection)))),
2587
+ ...previouslyRaised.filter((entry) => !judged(entry.objection) &&
2588
+ !raisedNow.has(answerKey(entry.objection))),
1791
2589
  ],
1792
2590
  resolvedObjections,
1793
2591
  submissionCount: submissionNumber,
@@ -1801,44 +2599,20 @@ export function registerSubmitReportTool(server) {
1801
2599
  const report = {
1802
2600
  businessCaseAnalysis: params.businessCaseAnalysis,
1803
2601
  newTestsCreated: sanitizedNewTests,
1804
- additionalRecommendations: dedupedRecommendations.map(normalizeRepository),
2602
+ additionalRecommendations: dedupedRecommendations,
1805
2603
  // Report wire format keeps main's original `fileName` (basename) — testFilePath is
1806
2604
  // an internal-only field, needed for matching but never meant to reach the report.
1807
- testMaintenance: testMaintenance
1808
- ? await Promise.all(testMaintenance.map(async ({ testFilePath, pomFile, ...row }) => {
1809
- // `pomFile` is the file skyramp_actions told the agent to edit;
1810
- // `testFilePath` stays the spec, which is what ran. Name both when
1811
- // they differ, or a page-object edit is reported against the spec.
1812
- // Maintenance rows still carry the absolute path here, so the
1813
- // assertion summary uses an EXACT canonical-path lookup — no
1814
- // basename ambiguity. This is what carries the maintenance
1815
- // honesty labels (nothing-to-verify vs verified) into the
1816
- // report instead of leaving them as tool text the agent can
1817
- // paraphrase.
1818
- const record = stateData.assertionOutcomes?.[canonicalTestPath(testFilePath)];
1819
- const assertions = record
1820
- ? await rederiveAssertionOutcome(record)
1821
- : undefined;
1822
- return normalizeRepository({
1823
- ...row,
1824
- fileName: path.basename(testFilePath),
1825
- ...(pomFile &&
1826
- path.basename(pomFile) !== path.basename(testFilePath)
1827
- ? { editedFileName: path.basename(pomFile) }
1828
- : {}),
1829
- ...(assertions ? { assertions } : {}),
1830
- });
1831
- }))
1832
- : undefined,
2605
+ testMaintenance: maintenanceRows,
1833
2606
  // videoPath is filled from the run's execution records; testFilePath is the
1834
2607
  // match-only key and is stripped from the wire format, the same line drawn for
1835
2608
  // testMaintenance's own testFilePath above (downstream scoring scripts traverse
1836
2609
  // these objects and don't expect it).
1837
2610
  testResults: params.testResults.map((row) => {
1838
- const { testFilePath: _tfp, ...wire } = attachVideoPath(normalizeRepository(row), stateData.executionVideos);
2611
+ const { testFilePath: _tfp, ...wire } = attachVideoPath(row, stateData.executionVideos);
1839
2612
  return wire;
1840
2613
  }),
1841
2614
  issuesFound,
2615
+ executionCoverage,
1842
2616
  nextSteps: params.nextSteps ?? [],
1843
2617
  ...(changeTable ? { changes: changeTable } : {}),
1844
2618
  ...(objections ? { objections } : {}),
@@ -1861,11 +2635,64 @@ export function registerSubmitReportTool(server) {
1861
2635
  outputFile: reportPath,
1862
2636
  elapsedMs: elapsed,
1863
2637
  });
2638
+ // The report is on disk, so this call is past every refusal in the handler
2639
+ // — including the one a failed write above would have raised. See
2640
+ // stripDeliveredGateMarkers for why the accepted path is the only one that
2641
+ // may remove a decline.
2642
+ const stripped = await stripDeliveredGateMarkers(stateData.reuseOutcomes, retrofitViews, changedSet);
2643
+ // Only ADDS here. Retirement already happened above, before any refusal
2644
+ // could return, so this call has nothing left to draw down.
2645
+ if (Object.keys(stripped).length > 0) {
2646
+ // Recorded so a later call in this run can read them back — the removal is
2647
+ // for the customer, not for our own gates. A record that cannot be
2648
+ // persisted would make that call refuse a declined fault, so the failure
2649
+ // is logged where the next refusal can be explained by it.
2650
+ try {
2651
+ // Re-read rather than spread the `stateData` this call opened with. The
2652
+ // objections block above has already persisted its own record, and
2653
+ // carryForwardRootFields carries `reportObjections` only when the
2654
+ // incoming value is undefined — so writing the call-start copy back would
2655
+ // revert the submission count and re-raise answers this call resolved.
2656
+ const latest = (await stateManager.readData()) ?? stateData;
2657
+ await stateManager.appendData({
2658
+ ...latest,
2659
+ // Merged per file, never replaced: a later submission that strips a
2660
+ // second marker from one file must not forget the first, which is
2661
+ // still a decline this run made.
2662
+ //
2663
+ // A decline this call did NOT consume is retired. The fault it covered
2664
+ // is gone, and permission that covers no fault authorises the next one
2665
+ // instead — which nobody declined. A marker left in a file retires the
2666
+ // same way, through `stale-allow`; this is that rule for the records.
2667
+ strippedDeclines: Object.fromEntries([
2668
+ ...new Set([
2669
+ ...Object.keys(latest.strippedDeclines ?? {}),
2670
+ ...Object.keys(stripped),
2671
+ ]),
2672
+ ].map((file) => [
2673
+ file,
2674
+ [
2675
+ ...(latest.strippedDeclines?.[file] ?? []),
2676
+ ...(stripped[file] ?? []),
2677
+ ].map((d) => {
2678
+ // The `file` tag is a runtime aid for retirement, not part of
2679
+ // the record: the record is already keyed by file.
2680
+ const { file: _tag, ...rest } = d;
2681
+ return rest;
2682
+ }),
2683
+ ])),
2684
+ });
2685
+ }
2686
+ catch (err) {
2687
+ logger.warning("Could not record the declines removed from the delivered files — a further submission may refuse one of them", { error: String(err) });
2688
+ }
2689
+ }
1864
2690
  // Addressed to the AGENT in the second person, each naming a next move: the
1865
2691
  // report is already written when this returns and the tool can be called
1866
2692
  // again, so the instruction is actionable. An answered objection is
1867
2693
  // published but not listed back, which would read as a second demand.
1868
- const open = objections?.filter((objection) => objection.stage === "post-execution" && objection.answer === undefined) ?? [];
2694
+ const open = objections?.filter((objection) => objection.stage === "post-execution" &&
2695
+ objection.answer === undefined) ?? [];
1869
2696
  return {
1870
2697
  content: [
1871
2698
  {