@skyramp/mcp 0.4.1 → 0.4.2-rc.1

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