@skyramp/mcp 0.4.4 → 0.5.0-rc.10

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 (144) hide show
  1. package/build/entitlements/entitlementGate.d.ts +8 -0
  2. package/build/entitlements/entitlementGate.js +57 -0
  3. package/build/entitlements/runCodes.d.ts +18 -0
  4. package/build/entitlements/runCodes.js +16 -0
  5. package/build/index.js +32 -6
  6. package/build/playwright/PlaywrightTraceService.d.ts +17 -0
  7. package/build/playwright/PlaywrightTraceService.js +29 -1
  8. package/build/playwright/dataSafetyConfig.d.ts +74 -0
  9. package/build/playwright/dataSafetyConfig.js +109 -0
  10. package/build/playwright/essentialTools.d.ts +1 -0
  11. package/build/playwright/essentialTools.js +44 -0
  12. package/build/playwright/registerPlaywrightTools.d.ts +36 -1
  13. package/build/playwright/registerPlaywrightTools.js +87 -43
  14. package/build/playwright/safeResolveDataSafety.d.ts +24 -0
  15. package/build/playwright/safeResolveDataSafety.js +33 -0
  16. package/build/prompts/initialize-workspace/initializeWorkspacePrompt.d.ts +17 -0
  17. package/build/prompts/initialize-workspace/initializeWorkspacePrompt.js +46 -177
  18. package/build/prompts/modularization/ui-test-modularization.d.ts +1 -0
  19. package/build/prompts/modularization/ui-test-modularization.js +9 -2
  20. package/build/prompts/test-maintenance/driftAnalysisShared.js +1 -1
  21. package/build/services/FleetExecutionService.d.ts +87 -16
  22. package/build/services/FleetExecutionService.js +487 -48
  23. package/build/services/FleetRunRegistry.d.ts +49 -0
  24. package/build/services/FleetRunRegistry.js +178 -0
  25. package/build/services/containerEnv.js +8 -2
  26. package/build/services/fleetLimitConfirmation.d.ts +19 -0
  27. package/build/services/fleetLimitConfirmation.js +126 -0
  28. package/build/tool-phases.d.ts +14 -8
  29. package/build/tool-phases.js +88 -43
  30. package/build/tools/code-refactor/assertion-state.js +3 -1
  31. package/build/tools/code-refactor/codeReuseTool.js +2 -1
  32. package/build/tools/code-refactor/utils-verify-gates.js +1 -1
  33. package/build/tools/executeFleetTool.d.ts +6 -4
  34. package/build/tools/executeFleetTool.js +67 -23
  35. package/build/tools/fixErrorTool.js +2 -1
  36. package/build/tools/fleetLifecycleTools.d.ts +20 -0
  37. package/build/tools/fleetLifecycleTools.js +124 -0
  38. package/build/tools/generate-tests/generateIntegrationRestTool.d.ts +38 -0
  39. package/build/tools/generate-tests/generateIntegrationRestTool.js +7 -1
  40. package/build/tools/generate-tests/generateUILoadRestTool.d.ts +19 -1
  41. package/build/tools/generate-tests/generateUILoadRestTool.js +316 -50
  42. package/build/tools/submitReportTool.js +4 -122
  43. package/build/tools/test-management/registerTestPlanTool.js +22 -6
  44. package/build/tools/workspace/initScanWorkspaceTool.d.ts +11 -1
  45. package/build/tools/workspace/initScanWorkspaceTool.js +111 -9
  46. package/build/tools/workspace/initializeWorkspaceTool.js +40 -4
  47. package/build/types/AssertionOutcome.d.ts +3 -0
  48. package/build/types/TestbotReport.d.ts +0 -37
  49. package/build/types/TestbotReport.js +0 -22
  50. package/build/types/index.d.ts +1 -1
  51. package/build/types/index.js +1 -1
  52. package/build/utils/assertion-verify/created-add-guard.d.ts +12 -0
  53. package/build/utils/assertion-verify/created-add-guard.js +173 -0
  54. package/build/utils/assertion-verify/index.d.ts +1 -0
  55. package/build/utils/assertion-verify/index.js +1 -0
  56. package/build/utils/assertion-verify/verify.d.ts +7 -3
  57. package/build/utils/assertion-verify/verify.js +9 -3
  58. package/build/utils/featureFlags.js +6 -3
  59. package/build/utils/repositorySlug.d.ts +14 -0
  60. package/build/utils/repositorySlug.js +46 -1
  61. package/build/utils/telemetry.d.ts +2 -0
  62. package/build/utils/telemetry.js +12 -0
  63. package/build/utils/utils-verify/action-key.d.ts +59 -2
  64. package/build/utils/utils-verify/action-key.js +162 -6
  65. package/build/utils/utils-verify/action-sites.d.ts +7 -0
  66. package/build/utils/utils-verify/action-sites.js +277 -35
  67. package/build/utils/utils-verify/call-sites.d.ts +8 -2
  68. package/build/utils/utils-verify/language-spec.js +1 -1
  69. package/build/utils/utils-verify/parse.d.ts +4 -0
  70. package/build/utils/utils-verify/parse.js +2 -1
  71. package/build/utils/versions.d.ts +3 -3
  72. package/build/utils/versions.js +1 -1
  73. package/build/workspace/workspace.d.ts +63 -121
  74. package/build/workspace/workspace.js +45 -48
  75. package/build/workspace/workspaceVerifier.d.ts +22 -0
  76. package/build/workspace/workspaceVerifier.js +65 -0
  77. package/node_modules/playwright/lib/dom-analyzer/blueprint.js +7 -6
  78. package/node_modules/playwright/lib/dom-analyzer/dynamicId.js +5 -4
  79. package/node_modules/playwright/lib/dom-analyzer/dynamicId.test.js +12 -0
  80. package/node_modules/playwright/lib/mcp/skyramp/dataSafetyGuard.js +822 -0
  81. package/node_modules/playwright/lib/mcp/skyramp/dataSafetySession.js +372 -0
  82. package/node_modules/playwright/lib/mcp/skyramp/exportTool.js +101 -8
  83. package/node_modules/playwright/lib/mcp/skyramp/finishTestSetupTool.js +45 -0
  84. package/node_modules/playwright/lib/mcp/skyramp/index.js +16 -0
  85. package/node_modules/playwright/lib/mcp/skyramp/recordingPhase.js +65 -0
  86. package/node_modules/playwright/lib/mcp/skyramp/skyRampImport.js +1 -0
  87. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +598 -127
  88. package/node_modules/playwright/lib/mcp/test/skyRampExport.js +85 -31
  89. package/node_modules/playwright/lib/mcp/test/skyRampExport.test.js +132 -5
  90. package/node_modules/playwright/node_modules/playwright-core/lib/cli/program.js +2 -0
  91. package/node_modules/playwright/node_modules/playwright-core/lib/client/browserContext.js +4 -2
  92. package/node_modules/playwright/node_modules/playwright-core/lib/generated/injectedScriptSource.js +1 -1
  93. package/node_modules/playwright/node_modules/playwright-core/lib/generated/pollingRecorderSource.js +1 -1
  94. package/node_modules/playwright/node_modules/playwright-core/lib/protocol/validator.js +2 -1
  95. package/node_modules/playwright/node_modules/playwright-core/lib/server/browserContext.js +17 -1
  96. package/node_modules/playwright/node_modules/playwright-core/lib/server/codegen/skyramp/jsonlReader.js +1 -0
  97. package/node_modules/playwright/node_modules/playwright-core/lib/server/har/harRecorder.js +1 -0
  98. package/node_modules/playwright/node_modules/playwright-core/lib/server/har/harTracer.js +1 -1
  99. package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/recorderRunner.js +2 -1
  100. package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/recorderSignalProcessor.js +2 -1
  101. package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/skyramp/clickExpandedState.js +75 -0
  102. package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder.js +4 -2
  103. package/node_modules/playwright/node_modules/playwright-core/lib/utils/isomorphic/volatileDate.js +85 -0
  104. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-Bou8kKzE.js → codeMirrorModule-BuhAdckO.js} +1 -1
  105. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-COAHN430.js +809 -0
  106. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.0MJW0uG7.js → index.7AU8h0PZ.js} +1 -1
  107. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +2 -2
  108. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{uiMode.GLqsx5cI.js → uiMode.Dl7smBJn.js} +1 -1
  109. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +2 -2
  110. package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
  111. package/node_modules/playwright/node_modules/playwright-core/src/cli/program.ts +5 -0
  112. package/node_modules/playwright/node_modules/playwright-core/src/client/browserContext.ts +3 -1
  113. package/node_modules/playwright/node_modules/playwright-core/src/client/types.ts +2 -0
  114. package/node_modules/playwright/node_modules/playwright-core/src/generated/injectedScriptSource.ts +1 -1
  115. package/node_modules/playwright/node_modules/playwright-core/src/generated/pollingRecorderSource.ts +1 -1
  116. package/node_modules/playwright/node_modules/playwright-core/src/protocol/validator.ts +1 -0
  117. package/node_modules/playwright/node_modules/playwright-core/src/server/browserContext.ts +25 -1
  118. package/node_modules/playwright/node_modules/playwright-core/src/server/codegen/skyramp/jsonlReader.ts +2 -0
  119. package/node_modules/playwright/node_modules/playwright-core/src/server/har/harRecorder.ts +1 -0
  120. package/node_modules/playwright/node_modules/playwright-core/src/server/har/harTracer.ts +3 -1
  121. package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/recorderRunner.ts +3 -1
  122. package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/recorderSignalProcessor.ts +1 -0
  123. package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/skyramp/clickExpandedState.ts +82 -0
  124. package/node_modules/playwright/node_modules/playwright-core/src/server/recorder.ts +2 -0
  125. package/node_modules/playwright/node_modules/playwright-core/src/utils/isomorphic/volatileDate.ts +137 -1
  126. package/node_modules/playwright/package.json +1 -1
  127. package/package.json +4 -4
  128. package/plugin/prompts/plan-tests.md +2 -2
  129. package/plugin/prompts/testbot-task1.md +1 -1
  130. package/plugin/skills/enhance-assertions/reference/integration.md +1 -0
  131. package/plugin/skills/setup-testbot-target-sut/SKILL.md +60 -0
  132. package/plugin/skills/setup-testbot-target-sut/reference/ci-workflow.md +26 -0
  133. package/plugin/skills/setup-testbot-target-sut/reference/deployed-environment.md +105 -0
  134. package/plugin/skills/setup-testbot-target-sut/reference/existing-tests.md +14 -0
  135. package/plugin/skills/setup-testbot-target-sut/reference/troubleshooting.md +20 -0
  136. package/build/prompts/sut-setup/modes/adaptWorkflowPrompt.d.ts +0 -2
  137. package/build/prompts/sut-setup/modes/adaptWorkflowPrompt.js +0 -178
  138. package/build/prompts/sut-setup/modes/dockerComposePrompt.d.ts +0 -2
  139. package/build/prompts/sut-setup/modes/dockerComposePrompt.js +0 -311
  140. package/build/prompts/sut-setup/shared.d.ts +0 -62
  141. package/build/prompts/sut-setup/shared.js +0 -196
  142. package/build/resources/sutSetupResource.d.ts +0 -2
  143. package/build/resources/sutSetupResource.js +0 -63
  144. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-DTp2-8Si.js +0 -809
@@ -55,6 +55,11 @@ var import_resultCode = require("./resultCode");
55
55
  var import_mouseActionTool = require("./mouseActionTool");
56
56
  var import_visualSnapshotTool = require("./visualSnapshotTool");
57
57
  var import_gojsTool = require("./gojsTool");
58
+ var import_dataSafetyGuard = require("./dataSafetyGuard");
59
+ var import_dataSafetySession = require("./dataSafetySession");
60
+ var import_finishTestSetupTool = require("./finishTestSetupTool");
61
+ var import_recordingPhase = require("./recordingPhase");
62
+ var import_mouseActions = require("./common/mouseActions");
58
63
  var import_cssValue = require("./common/cssValue");
59
64
  var import_visualSnapshot = require("./common/visualSnapshot");
60
65
  var import_snapshotLocators = require("./snapshotLocators");
@@ -63,6 +68,7 @@ var import_tableSelectorBuilder = require("./tableSelectorBuilder");
63
68
  var import_types = require("./types");
64
69
  var import_dynamicId = require("../../dom-analyzer/dynamicId");
65
70
  const traceDebug = (0, import_utilsBundle.debug)("pw:mcp:trace");
71
+ const PASSIVE_TOOLS = /* @__PURE__ */ new Set(["browser_snapshot", "browser_wait_for", "browser_take_screenshot", "browser_console_messages", "browser_network_requests"]);
66
72
  function selectorIdentity(s) {
67
73
  if (!s)
68
74
  return null;
@@ -89,11 +95,11 @@ const REF_RESOLVE_TIMEOUT_MS = 1e3;
89
95
  const MAX_ANCESTOR_HOVERS = 8;
90
96
  const ANCESTOR_WALK_BUDGET_MS = 1e4;
91
97
  class TraceRecordingBackend {
92
- // per-type baseline filename counter
93
98
  constructor(options) {
94
99
  // The attribute getByTestId() resolves against (configured or the default).
95
100
  this._testIdAttribute = "data-testid";
96
101
  this._trackedActions = [];
102
+ this._recordingPhase = new import_recordingPhase.RecordingPhaseState();
97
103
  this._droppedActions = [];
98
104
  this._initialized = false;
99
105
  this._pageCount = 0;
@@ -105,10 +111,48 @@ class TraceRecordingBackend {
105
111
  this._reloading = false;
106
112
  // true while page.reload() is in progress — suppresses spurious popup tracking
107
113
  this._visualSnapshotCounters = { page: 0, element: 0, region: 0, gojsDiagram: 0 };
114
+ this._dataSafetyReady = false;
115
+ // Action ids of passive tool calls (see PASSIVE_TOOLS) in this recording:
116
+ // a create that started during one came from the app on its own.
117
+ this._passiveActionIds = /* @__PURE__ */ new Set();
108
118
  this._options = options || {};
109
119
  this._outputDir = options?.outputDir || process.cwd();
110
120
  this._tempDir = import_fs.default.mkdtempSync(import_path.default.join(import_os.default.tmpdir(), "skyramp-trace-"));
111
121
  this._harPath = import_path.default.join(this._tempDir, "skyramp_network.har");
122
+ const dataSafety = (0, import_dataSafetyGuard.parseDataSafetyConfig)(this._options.dataSafety, process.env.SKYRAMP_DATA_SAFETY);
123
+ const ledgerDir = dataSafety.ledgerDir;
124
+ let ledgerSeq = 0;
125
+ const nextLedgerPath = ledgerDir ? () => import_path.default.join(ledgerDir, `dataSafetyLedger-${process.pid}-${Date.now()}-${ledgerSeq++}.json`) : void 0;
126
+ this._guard = new import_dataSafetyGuard.DataSafetyGuard(
127
+ dataSafety,
128
+ nextLedgerPath ? nextLedgerPath() : import_path.default.join(this._tempDir, "dataSafetyLedger.json"),
129
+ () => this._recordingPhase.setupCompleted,
130
+ nextLedgerPath
131
+ );
132
+ this._session = new import_dataSafetySession.DataSafetySession(this._guard, () => this._recordingPhase.requestProvenance());
133
+ }
134
+ /**
135
+ * An unusable data-safety config part, or a ledger that cannot be
136
+ * written, fails every tool call: recording without it would be looser
137
+ * than the caller asked for, and a ledgerDir caller has no other record.
138
+ * Checked on the first call (no I/O in the constructor) and again until
139
+ * it passes, and again after any ledger write fails mid-session: the
140
+ * check rewrites the full in-memory ledger, so recovery loses nothing.
141
+ */
142
+ _checkDataSafetyStartup() {
143
+ if (this._dataSafetyReady && !this._guard.persistFailed)
144
+ return void 0;
145
+ if (!this._session.active && !this._guard.config.startupError) {
146
+ this._dataSafetyReady = true;
147
+ return void 0;
148
+ }
149
+ try {
150
+ this._guard.ensureLedgerWritable();
151
+ this._dataSafetyReady = true;
152
+ return void 0;
153
+ } catch (e) {
154
+ return e instanceof Error ? e.message : String(e);
155
+ }
112
156
  }
113
157
  get trackedActions() {
114
158
  return this._trackedActions;
@@ -175,6 +219,8 @@ class TraceRecordingBackend {
175
219
  channel: config.browser.launchOptions?.channel
176
220
  });
177
221
  this._browserBackend.context.onBrowserContextCreated = async (browserContext) => {
222
+ (0, import_dataSafetySession.assertNoNetworkPolicyCollision)(config.network, this._session.active);
223
+ await this._session.install(browserContext);
178
224
  this._installPopupListener(browserContext);
179
225
  const ctx = this._browserBackend.context;
180
226
  if (ctx && !ctx.sessionLog) {
@@ -198,40 +244,156 @@ class TraceRecordingBackend {
198
244
  }
199
245
  async listTools() {
200
246
  const browserTools = await this._browserBackend.listTools();
201
- return [...browserTools, (0, import_exportTool.exportZipMcpTool)(), (0, import_assertTool.assertMcpTool)(), (0, import_assertHiddenTool.assertHiddenMcpTool)(), (0, import_assertApiRequestTool.assertApiRequestMcpTool)(), (0, import_loadTraceTool.loadTraceMcpTool)(), (0, import_mouseActionTool.mouseActionMcpTool)(), (0, import_tableAssertTool.tableAssertMcpTool)(), (0, import_visualSnapshotTool.visualSnapshotMcpTool)(), (0, import_gojsTool.gojsAddNodeMcpTool)(), (0, import_gojsTool.gojsLinkMcpTool)()];
247
+ return [...browserTools, (0, import_exportTool.exportZipMcpTool)(), (0, import_finishTestSetupTool.finishTestSetupMcpTool)(), (0, import_assertTool.assertMcpTool)(), (0, import_assertHiddenTool.assertHiddenMcpTool)(), (0, import_assertApiRequestTool.assertApiRequestMcpTool)(), (0, import_loadTraceTool.loadTraceMcpTool)(), (0, import_mouseActionTool.mouseActionMcpTool)(), (0, import_tableAssertTool.tableAssertMcpTool)(), (0, import_visualSnapshotTool.visualSnapshotMcpTool)(), (0, import_gojsTool.gojsAddNodeMcpTool)(), (0, import_gojsTool.gojsLinkMcpTool)()];
202
248
  }
249
+ /**
250
+ * Thin wrapper around _callToolInner so every tool call — not just
251
+ * the browser-tool common path — gets any pending data-safety block notice
252
+ * appended to ITS OWN result. Several browser-interaction tools return early
253
+ * from deep inside _callToolInner (browser_select_option and its combobox
254
+ * fallback, the browser_fill_form→select retry, the same-URL navigate→reload
255
+ * branch) and can each trigger network requests the guard blocks; draining
256
+ * notices here — once, after every call resolves — ensures a block is always
257
+ * attributed to the call that caused it, never delayed onto a later,
258
+ * unrelated tool result or dropped if no later call ever runs. Applies even
259
+ * when isError is true: a blocked navigation routinely surfaces as an error
260
+ * result, and that's exactly the case the notice needs to explain.
261
+ *
262
+ * _callToolInner can also THROW (e.g. exportZipSchema.inputSchema.parse
263
+ * on bad args) instead of returning a CallToolResult — without a catch here
264
+ * that path skipped consumeBlockNotices() entirely, leaving the pending
265
+ * notice to be silently re-attributed to whatever call happened to succeed
266
+ * next (or dropped if none ever did). The catch drains notices for the
267
+ * throwing call too and folds them into a synthesized error result;
268
+ * re-throws untouched when there's nothing to attribute.
269
+ */
203
270
  async callTool(name, args, progress) {
271
+ try {
272
+ const result = await this._callToolInner(name, args, progress);
273
+ const notice = this._guard.consumeBlockNotices();
274
+ if (notice)
275
+ result.content = [...result.content ?? [], { type: "text", text: notice }];
276
+ return result;
277
+ } catch (e) {
278
+ const notice = this._guard.consumeBlockNotices();
279
+ if (notice)
280
+ return { content: [{ type: "text", text: `### Error
281
+ ${String(e)}${notice}` }], isError: true };
282
+ throw e;
283
+ }
284
+ }
285
+ async _callToolInner(name, args, progress) {
204
286
  if (!this._initialized)
205
287
  return { content: [{ type: "text", text: "### Error\nBackend not initialized. Call initialize() first." }], isError: true };
288
+ const dataSafetyError = this._checkDataSafetyStartup();
289
+ if (dataSafetyError)
290
+ return { content: [{ type: "text", text: `### Error
291
+ ${dataSafetyError}
292
+ No browser action was run.` }], isError: true };
293
+ if (name === import_finishTestSetupTool.finishTestSetupSchema.name) {
294
+ import_finishTestSetupTool.finishTestSetupSchema.inputSchema.parse(args || {});
295
+ if (!this._recordingPhase.setupCompleted && this._guard.teardownStarted) {
296
+ return {
297
+ content: [{ type: "text", text: "### Error\nskyramp_finish_test_setup must be called before the scenario steps. Data safety teardown already ran for this recording and deleted the records it created, so the boundary cannot be set now. Finish skyramp_export_zip (it refuses the recording and resets it), then record again from the start: setup steps, skyramp_finish_test_setup, then the scenario." }],
298
+ isError: true
299
+ };
300
+ }
301
+ const changed = this._recordingPhase.finishSetup();
302
+ if (changed && this._session.active)
303
+ this._guard.flush();
304
+ return {
305
+ content: [{
306
+ type: "text",
307
+ text: changed ? "### Result\nScenario recording started." : "### Result\nScenario recording had already started."
308
+ }]
309
+ };
310
+ }
206
311
  if (name === import_exportTool.exportZipSchema.name) {
207
312
  const parsed = import_exportTool.exportZipSchema.inputSchema.parse(args || {});
208
- const closePromise = this._browserBackend.callTool("browser_close", {}).catch(() => {
209
- });
210
- let closeTimer;
211
- const closeTimeout = new Promise((resolve) => {
212
- closeTimer = setTimeout(() => {
213
- traceDebug("export: browser_close did not finish within 20s; proceeding with HAR as-is");
214
- resolve();
215
- }, 2e4);
216
- });
217
- await Promise.race([closePromise, closeTimeout]);
218
- clearTimeout(closeTimer);
313
+ const prep = await this._session.prepareExport();
314
+ if (prep.kind === "capture-pending") {
315
+ return {
316
+ content: [{ type: "text", text: `### Error
317
+ Data safety could not settle ${prep.urls.length} in-flight create response(s) before export \u2014 the following request(s) were still being captured when the settle timeout elapsed:
318
+ ${prep.manifest}
319
+ The recording was NOT exported. Retry skyramp_export_zip once the response(s) complete, or check dataSafetyLedger.json for the audit trail.` }],
320
+ isError: true
321
+ };
322
+ }
323
+ if (prep.kind === "context-closed") {
324
+ return {
325
+ content: [{ type: "text", text: `### Error
326
+ Data safety teardown could not run: the browser context is already closed, so ${prep.pending.length} created entities could not be verified as deleted:
327
+ ${prep.manifest}
328
+ Re-open the target with browser_navigate (to restore an authenticated session) and retry skyramp_export_zip, or delete these records manually.` }],
329
+ isError: true
330
+ };
331
+ }
332
+ if (prep.kind === "leftovers") {
333
+ return {
334
+ content: [{ type: "text", text: `### Error
335
+ Data safety teardown could not delete every entity created during recording. The following records were left behind in the target environment:
336
+ ${prep.manifest}
337
+ The recording was NOT exported. Delete these records manually or re-record against an ephemeral target.` }],
338
+ isError: true
339
+ };
340
+ }
341
+ const unverified = this._guard.unparsedCreates.filter((u) => u.rule === "capture-timeout" || u.rule === "capture-failed");
342
+ if (unverified.length && !parsed.acknowledgeUnverifiedCreates) {
343
+ const manifest = (0, import_dataSafetyGuard.renderCappedList)(unverified.map((u) => `- POST ${u.url} [${u.rule}]`), "unverified create(s) \u2014 see dataSafetyLedger.json.");
344
+ return {
345
+ content: [{ type: "text", text: `### Error
346
+ Data safety: ${unverified.length} create request(s) may have reached the server, but their responses could not be recorded, so teardown could not delete what they created:
347
+ ${manifest}
348
+ The recording was NOT exported. Tell the user these requests may have left records behind, then retry skyramp_export_zip with acknowledgeUnverifiedCreates: true.` }],
349
+ isError: true
350
+ };
351
+ }
352
+ this._resolveSourceActions();
353
+ const provenance2 = (0, import_exportTool.validateResourceProvenance)(this._trackedActions, this._guard.ledger, this._recordingPhase.setupCompleted);
354
+ if (!provenance2.ok)
355
+ return this._refuseAndResetRecording(provenance2.message);
356
+ await this._closeBrowserForExport();
357
+ const lateSettle = await this._session.settleBodyReads();
358
+ if (lateSettle.timedOut) {
359
+ return {
360
+ content: [{ type: "text", text: `### Error
361
+ Data safety: ${lateSettle.unsettled} create response(s) were still being captured after the browser closed.
362
+ The recording was NOT exported. Retry skyramp_export_zip once the response(s) complete, or check dataSafetyLedger.json for the audit trail.` }],
363
+ isError: true
364
+ };
365
+ }
366
+ const lateCreates = this._guard.ledger.filter((e) => !e.tornDown);
367
+ if (lateCreates.length) {
368
+ const manifest = (0, import_dataSafetyGuard.renderCappedList)(lateCreates.map((e) => `- ${e.entityUrl} (id ${e.id})`), "left-behind entities \u2014 see dataSafetyLedger.json.");
369
+ return {
370
+ content: [{ type: "text", text: `### Error
371
+ Data safety: ${lateCreates.length} record(s) were created while the browser was closing, so teardown could not delete them:
372
+ ${manifest}
373
+ The recording was NOT exported. Delete these records manually.` }],
374
+ isError: true
375
+ };
376
+ }
219
377
  this._consumeRecorderSelectors();
378
+ this._resolveSourceActions();
379
+ const serializedProvenance = (0, import_exportTool.validateResourceProvenance)(this._trackedActions, this._guard.ledger, this._recordingPhase.setupCompleted);
380
+ if (!serializedProvenance.ok)
381
+ return this._refuseAndResetRecording(serializedProvenance.message);
220
382
  const handler = (0, import_exportTool.createExportZipHandler)({
221
383
  trackedActions: this._trackedActions,
222
384
  droppedActions: this._droppedActions,
223
385
  harPath: this._harPath,
224
- outputDir: this._outputDir
386
+ outputDir: this._outputDir,
387
+ // The ledger (and unparsedCreates advisory) rides the trace zip
388
+ // whenever the guard could have done something (strict, or
389
+ // offLimits blocks even in mode 'off'). By this point prepareExport()
390
+ // already returned 'clean', so this is never the leftovers/dead-context
391
+ // manifest — just the audit trail for a successful export.
392
+ dataSafety: this._session.active ? { ledgerJson: this._guard.ledgerJson(this._recordingPhase.setupCompleted), unparsedCreates: this._guard.unparsedCreates } : void 0
225
393
  });
226
394
  const exportResult = await handler(parsed);
227
- if (!exportResult.isError) {
228
- this._trackedActions = [];
229
- this._droppedActions = [];
230
- const ra = this._browserBackend.context?.recorderActions;
231
- if (ra)
232
- ra.length = 0;
233
- this._cleanupTempDir(true);
234
- }
395
+ if (!exportResult.isError)
396
+ this._resetRecording();
235
397
  return exportResult;
236
398
  }
237
399
  if (name === import_loadTraceTool.loadTraceSchema.name) {
@@ -244,47 +406,56 @@ class TraceRecordingBackend {
244
406
  }
245
407
  if (name === import_visualSnapshotTool.visualSnapshotSchema.name) {
246
408
  const parsed = import_visualSnapshotTool.visualSnapshotSchema.inputSchema.parse(args || {});
247
- return this._handleVisualSnapshot(parsed);
409
+ return this._withActionProvenance((provenance2) => this._handleVisualSnapshot(parsed, provenance2));
248
410
  }
249
411
  if (name === import_assertTool.assertToolSchema.name) {
250
412
  const parsed = import_assertTool.assertToolSchema.inputSchema.parse(args || {});
251
- if (parsed.type === "order")
252
- return this._handleAssertOrder(parsed);
253
- return this._handleAssert({ ...parsed, type: parsed.type });
413
+ if (parsed.type === "order") {
414
+ const orderParams = parsed;
415
+ return this._withActionProvenance((provenance2) => this._handleAssertOrder(orderParams, provenance2));
416
+ }
417
+ const assertParams = { ...parsed, type: parsed.type };
418
+ return this._withActionProvenance((provenance2) => this._handleAssert(assertParams, provenance2));
254
419
  }
255
420
  if (name === import_assertHiddenTool.assertHiddenToolSchema.name) {
256
421
  const parsed = import_assertHiddenTool.assertHiddenToolSchema.inputSchema.parse(args || {});
257
- return this._handleAssertHidden(parsed, Date.now());
422
+ return this._withActionProvenance((provenance2) => this._handleAssertHidden(parsed, Date.now(), provenance2));
258
423
  }
259
424
  if (name === import_assertApiRequestTool.assertApiRequestSchema.name) {
260
- this._trackedActions.push({
261
- toolName: "browser_assert_api_request",
262
- args: {},
263
- code: "",
264
- timestamp: Date.now(),
265
- pageAlias: this._currentPageAlias
425
+ return this._withActionProvenance(async (provenance2) => {
426
+ this._recordAction({
427
+ toolName: "browser_assert_api_request",
428
+ args: {},
429
+ code: "",
430
+ timestamp: Date.now(),
431
+ pageAlias: this._currentPageAlias
432
+ }, provenance2);
433
+ traceDebug("Assert API request marker recorded");
434
+ return {
435
+ content: [{ type: "text", text: "### Result\nAPI request assertion marker recorded. The next action that triggers a network request will have its payload captured for validation in the generated test." }]
436
+ };
266
437
  });
267
- traceDebug("Assert API request marker recorded");
268
- return {
269
- content: [{ type: "text", text: "### Result\nAPI request assertion marker recorded. The next action that triggers a network request will have its payload captured for validation in the generated test." }]
270
- };
271
438
  }
272
439
  if (name === import_tableAssertTool.tableAssertToolSchema.name) {
273
440
  const parsed = import_tableAssertTool.tableAssertToolSchema.inputSchema.parse(args || {});
274
- return this._handleAssertTableCell(parsed);
441
+ return this._withActionProvenance((provenance2) => this._handleAssertTableCell(parsed, provenance2));
275
442
  }
276
443
  if (name === import_gojsTool.gojsAddNodeSchema.name) {
277
444
  const parsed = import_gojsTool.gojsAddNodeSchema.inputSchema.parse(args || {});
278
- return this._handleGoJSAddNode(parsed);
445
+ return this._withActionProvenance((provenance2) => this._handleGoJSAddNode(parsed, provenance2));
279
446
  }
280
447
  if (name === import_gojsTool.gojsLinkSchema.name) {
281
448
  const parsed = import_gojsTool.gojsLinkSchema.inputSchema.parse(args || {});
282
- return this._handleGoJSLink(parsed);
449
+ return this._withActionProvenance((provenance2) => this._handleGoJSLink(parsed, provenance2));
283
450
  }
284
451
  if (name === "browser_select_option") {
285
452
  const result2 = await this._handleSelectOption(args || {});
286
453
  return result2;
287
454
  }
455
+ if (name === "browser_fill_form")
456
+ return this._handleFillForm(args || {});
457
+ if ((name === "browser_type" || name === "browser_press_sequentially") && args?.submit)
458
+ return this._handleTypeWithSubmit(name, args || {});
288
459
  if (name === "browser_tabs" && ["select", "switch"].includes(args?.action)) {
289
460
  const index = args?.index ?? 0;
290
461
  const targetUrl = this._browserBackend.context?.tabs()[index]?.page.url();
@@ -302,21 +473,26 @@ class TraceRecordingBackend {
302
473
  const ts = Date.now();
303
474
  const reloadAlias = this._currentPageAlias;
304
475
  const tab = this._browserBackend.context.currentTab();
305
- this._reloading = true;
476
+ const provenance2 = this._recordingPhase.beginAction();
306
477
  try {
307
- await tab.page.reload({ waitUntil: "domcontentloaded" });
478
+ this._reloading = true;
479
+ try {
480
+ await tab.page.reload({ waitUntil: "domcontentloaded" });
481
+ } finally {
482
+ this._reloading = false;
483
+ this._currentPageAlias = reloadAlias;
484
+ this._pendingPopupAlias = null;
485
+ }
486
+ this._recordAction({
487
+ toolName: "browser_navigate",
488
+ args: { url: currentUrl },
489
+ code: "",
490
+ timestamp: ts,
491
+ pageAlias: this._currentPageAlias
492
+ }, provenance2);
308
493
  } finally {
309
- this._reloading = false;
310
- this._currentPageAlias = reloadAlias;
311
- this._pendingPopupAlias = null;
494
+ this._recordingPhase.endAction(provenance2.actionId);
312
495
  }
313
- this._trackedActions.push({
314
- toolName: "browser_navigate",
315
- args: { url: currentUrl },
316
- code: "",
317
- timestamp: ts,
318
- pageAlias: this._currentPageAlias
319
- });
320
496
  await this._enableFlutterAccessibility();
321
497
  const snapResult = await this._browserBackend.callTool("browser_snapshot", {});
322
498
  return {
@@ -332,6 +508,7 @@ Reloaded current page: ${currentUrl}
332
508
  const timestampBeforeAction = Date.now();
333
509
  const pageAliasBeforeAction = this._currentPageAlias;
334
510
  const stripRestore = await this._maybeStripWrapperRoles(name, args);
511
+ const provenance = this._recordingPhase.beginAction();
335
512
  let preClickHoverCode = null;
336
513
  let preClickSnapshot = null;
337
514
  let preClickLocatorCount = 0;
@@ -407,37 +584,38 @@ Reloaded current page: ${currentUrl}
407
584
  }
408
585
  result = await this._browserBackend.callTool(name, args);
409
586
  } finally {
410
- if (stripRestore)
411
- await stripRestore();
587
+ try {
588
+ if (stripRestore)
589
+ await stripRestore();
590
+ } finally {
591
+ this._recordingPhase.endAction(provenance.actionId);
592
+ }
412
593
  }
413
594
  if (name === "browser_navigate" && !result.isError) {
414
595
  await this._enableFlutterAccessibility();
415
596
  }
416
597
  result = await this._improveActionSelector(name, args || {}, result, preClickHoverCode, preClickSnapshot, preClickLocatorCount, preClickParentChain);
417
- if (name === "browser_fill_form" && result.isError) {
418
- const resultText = result.content?.[0]?.type === "text" ? result.content[0].text : "";
419
- if (resultText.includes("not a <select> element") || resultText.includes("selectOption") && resultText.includes("Timeout")) {
420
- const fields = args?.fields;
421
- if (fields) {
422
- const comboField = fields.find((f) => f.type === "combobox");
423
- if (comboField) {
424
- traceDebug("fill_form failed on combobox field, retrying with select fallback");
425
- this._maybeTrackAction(name, args || {}, { content: result.content, isError: false }, timestampBeforeAction, pageAliasBeforeAction);
426
- const selectResult = await this._handleSelectOption({
427
- element: comboField.name || "dropdown",
428
- ref: comboField.ref,
429
- values: [comboField.value]
430
- });
431
- return selectResult;
432
- }
433
- }
434
- }
435
- }
436
- this._maybeTrackAction(name, args || {}, result, timestampBeforeAction, pageAliasBeforeAction);
598
+ if (PASSIVE_TOOLS.has(name) && !result.isError)
599
+ this._passiveActionIds.add(provenance.actionId);
600
+ this._maybeTrackAction(name, args || {}, result, provenance, timestampBeforeAction, pageAliasBeforeAction);
437
601
  return result;
438
602
  }
603
+ /**
604
+ * The MCP SDK's server 'close' listener calls this synchronously
605
+ * (`ServerBackend.serverClosed?(server): void` — sdk/server.ts's
606
+ * `addServerListener(server, 'close', () => backend.serverClosed?.(server))`
607
+ * never awaits it), so the underlying work is unavoidably fire-and-forget
608
+ * from THAT caller's perspective. What was missing is any way for
609
+ * anything else — tests, or a future dispose/close path this class grows
610
+ * — to observe when the work actually finishes: the promise is now
611
+ * captured on the instance instead of discarded.
612
+ */
439
613
  serverClosed() {
440
- void this._autoExportAndClose().catch(import_log.logUnhandledError);
614
+ this._autoExportPromise = this._autoExportAndClose().catch(import_log.logUnhandledError);
615
+ }
616
+ /** The in-flight (or settled) promise from the most recent serverClosed() call, if any — see the field comment. */
617
+ get autoExportPromise() {
618
+ return this._autoExportPromise;
441
619
  }
442
620
  /**
443
621
  * Execute a coordinate-based mouse action and track each decomposed sub-action
@@ -448,23 +626,85 @@ Reloaded current page: ${currentUrl}
448
626
  */
449
627
  async _handleMouseAction(params) {
450
628
  const page = this._browserBackend.context?.currentTab()?.page;
629
+ const built = (0, import_mouseActions.mouseActionToJsonl)(params);
630
+ if ("error" in built)
631
+ return { content: [{ type: "text", text: `### Error
632
+ ${built.error}` }], isError: true };
633
+ if (!page)
634
+ return { content: [{ type: "text", text: "### Error\nNo open page. Call browser_navigate first." }], isError: true };
451
635
  const timestamp = Date.now();
452
636
  const pageAlias = this._currentPageAlias;
453
- const { result, actions } = await (0, import_mouseActionTool.executeMouseAction)(page, params);
454
- if (result.isError)
455
- return result;
456
- actions.forEach((a, i) => {
457
- this._trackedActions.push({
637
+ const recorded = [];
638
+ const code = [];
639
+ try {
640
+ for (const action of built.actions) {
641
+ const provenance = this._recordingPhase.beginAction();
642
+ try {
643
+ code.push(...(0, import_mouseActions.mouseJsonlToCode)(action));
644
+ await this._runMouseSubAction(page, action);
645
+ recorded.push({ action, provenance });
646
+ } finally {
647
+ this._recordingPhase.endAction(provenance.actionId);
648
+ }
649
+ }
650
+ } catch (e) {
651
+ return { content: [{ type: "text", text: `### Error
652
+ Mouse action failed: ${e.message}` }], isError: true };
653
+ }
654
+ recorded.forEach(({ action, provenance }, i) => {
655
+ this._recordAction({
458
656
  toolName: "browser_mouse_action",
459
- args: a,
657
+ args: action,
460
658
  code: "",
461
659
  // Stagger timestamps so a decomposed drag keeps its move<down<move<up order.
462
660
  timestamp: timestamp + i,
463
661
  pageAlias
464
- });
662
+ }, provenance);
465
663
  });
466
- traceDebug(`Tracked ${actions.length} mouse sub-action(s) for "${params.action}" on ${pageAlias}`);
467
- return result;
664
+ traceDebug(`Tracked ${recorded.length} mouse sub-action(s) for "${params.action}" on ${pageAlias}`);
665
+ return { content: [{ type: "text", text: `### Ran Playwright code
666
+ ${code.join("\n")}` }] };
667
+ }
668
+ async _runMouseSubAction(page, action) {
669
+ const withModifiers = async (mask, operation) => {
670
+ const keys = (0, import_mouseActions.decodeModifierKeys)(mask);
671
+ for (const key of keys)
672
+ await page.keyboard.down(key);
673
+ try {
674
+ await operation();
675
+ } finally {
676
+ for (const key of [...keys].reverse())
677
+ await page.keyboard.up(key);
678
+ }
679
+ };
680
+ switch (action.name) {
681
+ case "mouse.move":
682
+ if (action.steps === void 0)
683
+ await page.mouse.move(action.position.x, action.position.y);
684
+ else
685
+ await page.mouse.move(action.position.x, action.position.y, { steps: action.steps });
686
+ return;
687
+ case "mouse.down":
688
+ if (action.button && action.button !== "left")
689
+ await page.mouse.down({ button: action.button });
690
+ else
691
+ await page.mouse.down();
692
+ return;
693
+ case "mouse.up":
694
+ if (action.button && action.button !== "left")
695
+ await page.mouse.up({ button: action.button });
696
+ else
697
+ await page.mouse.up();
698
+ return;
699
+ case "mouse.wheel":
700
+ await withModifiers(action.modifiers, () => page.mouse.wheel(action.deltaX, action.deltaY));
701
+ return;
702
+ case "click": {
703
+ const options = action.button !== "left" || action.clickCount !== 1 ? { button: action.button, clickCount: action.clickCount } : void 0;
704
+ await withModifiers(action.modifiers, () => options ? page.mouse.click(action.position.x, action.position.y, options) : page.mouse.click(action.position.x, action.position.y));
705
+ return;
706
+ }
707
+ }
468
708
  }
469
709
  /**
470
710
  * Handle browser_visual_snapshot: record a `visualSnapshot` marker that
@@ -486,7 +726,7 @@ Reloaded current page: ${currentUrl}
486
726
  * (same machinery as the GoJS tools). Iframe-element snapshots from the recorder
487
727
  * remain out of scope here (see common/visualSnapshot.ts).
488
728
  */
489
- async _handleVisualSnapshot(params) {
729
+ async _handleVisualSnapshot(params, provenance) {
490
730
  const timestamp = Date.now();
491
731
  const pageAlias = this._currentPageAlias;
492
732
  const input = {
@@ -522,13 +762,13 @@ GoJS diagram "${diagramSelector}" is not inside an iframe; the gojsDiagram snaps
522
762
  ${built.error}` }], isError: true };
523
763
  this._visualSnapshotCounters[params.snapshotType]++;
524
764
  this._advanceSnapshotCounterFor(params.snapshotType, input.filename);
525
- this._trackedActions.push({
765
+ this._recordAction({
526
766
  toolName: "browser_visual_snapshot",
527
767
  args: built.action,
528
768
  code: "",
529
769
  timestamp,
530
770
  pageAlias
531
- });
771
+ }, provenance);
532
772
  traceDebug(`Tracked visualSnapshot (${params.snapshotType}) "${input.filename}" on ${pageAlias}`);
533
773
  const targetDesc = params.snapshotType === "element" ? ` (${input.selector})` : params.snapshotType === "region" && params.clip ? ` (clip ${params.clip.width}x${params.clip.height} at ${params.clip.x},${params.clip.y})` : params.snapshotType === "gojsDiagram" ? ` (${input.diagramSelector} via ${input.iframeSelectors?.length ?? 0} iframe(s))` : params.fullPage ? " (full page)" : "";
534
774
  return { content: [{ type: "text", text: `### Visual snapshot recorded
@@ -686,11 +926,17 @@ The selected range acts on tab "${a.frame.pageAlias}" but the step that opens th
686
926
  if (seedPage)
687
927
  claimedTabs.add(seedPage);
688
928
  let tabsBefore = [];
929
+ const replayProvenance = /* @__PURE__ */ new Map();
930
+ let activeReplayProvenance;
689
931
  let result;
690
932
  try {
691
933
  result = await (0, import_loadTraceTool.replayActions)(loadedActions, {
692
934
  pageForAlias: (alias) => remap.get(alias)?.page ?? this._pageForAlias(alias),
693
- beforeAction: () => {
935
+ beforeAction: (action) => {
936
+ if (activeReplayProvenance)
937
+ this._recordingPhase.endAction(activeReplayProvenance.actionId);
938
+ activeReplayProvenance = this._recordingPhase.beginAction();
939
+ replayProvenance.set(action, activeReplayProvenance);
694
940
  tabsBefore = liveTabs();
695
941
  },
696
942
  afterAction: async (action) => {
@@ -709,11 +955,18 @@ The selected range acts on tab "${a.frame.pageAlias}" but the step that opens th
709
955
  claimedTabs.add(fresh);
710
956
  remap.set(signal.popupAlias, { page: fresh, liveAlias: this._currentPageAlias });
711
957
  }
958
+ if (activeReplayProvenance) {
959
+ this._recordingPhase.endAction(activeReplayProvenance.actionId);
960
+ activeReplayProvenance = void 0;
961
+ }
712
962
  }
713
963
  }, params);
714
964
  } catch (e) {
715
965
  return { content: [{ type: "text", text: `### Error
716
966
  Replay failed: ${e.message}` }], isError: true };
967
+ } finally {
968
+ if (activeReplayProvenance)
969
+ this._recordingPhase.endAction(activeReplayProvenance.actionId);
717
970
  }
718
971
  for (let i = result.startIndex; i < result.stopIndex; i++) {
719
972
  const a = loadedActions[i];
@@ -721,7 +974,10 @@ Replay failed: ${e.message}` }], isError: true };
721
974
  if (liveAlias && liveAlias !== a.frame.pageAlias)
722
975
  a.frame = { ...a.frame, pageAlias: liveAlias };
723
976
  const before = this._trackedActions.length;
724
- this._seedTrackedAction(a);
977
+ const provenance = replayProvenance.get(a);
978
+ if (!provenance)
979
+ continue;
980
+ this._seedTrackedAction(a, provenance);
725
981
  const popupSignal = a.action.signals.find((s) => s.name === "popup");
726
982
  if (popupSignal && this._trackedActions.length > before)
727
983
  this._trackedActions[this._trackedActions.length - 1].popupAlias = remap.get(popupSignal.popupAlias)?.liveAlias ?? popupSignal.popupAlias;
@@ -774,17 +1030,17 @@ Continue recording with browser_* tools, then call skyramp_export_zip to write t
774
1030
  * re-derives the JSONL selector from the `code` field, so we regenerate clean
775
1031
  * Playwright code from the stored internal: selector via asLocator.
776
1032
  */
777
- _seedTrackedAction(action) {
1033
+ _seedTrackedAction(action, provenance) {
778
1034
  const a = action.action;
779
1035
  const pageAlias = action.frame.pageAlias || "page";
780
1036
  if (a.name === "navigate") {
781
- this._trackedActions.push({
1037
+ this._recordAction({
782
1038
  toolName: "browser_navigate",
783
1039
  args: { url: a.url },
784
1040
  code: "",
785
1041
  timestamp: action.startTime,
786
1042
  pageAlias
787
- });
1043
+ }, provenance);
788
1044
  return;
789
1045
  }
790
1046
  const locatorExpr = a.selector ? (0, import_utils.asLocator)("javascript", a.selector) : "";
@@ -793,12 +1049,12 @@ Continue recording with browser_* tools, then call skyramp_export_zip to write t
793
1049
  return;
794
1050
  if (seeded.toolName === "browser_visual_snapshot")
795
1051
  this._advanceSnapshotCounterFor(seeded.args.snapshotType, seeded.args.filename);
796
- this._trackedActions.push({
1052
+ this._recordAction({
797
1053
  ...seeded,
798
1054
  timestamp: action.startTime,
799
1055
  pageAlias,
800
1056
  framePath: action.frame.framePath?.length ? action.frame.framePath : void 0
801
- });
1057
+ }, provenance);
802
1058
  }
803
1059
  /**
804
1060
  * Bump the per-type snapshot counter to at least the number embedded in a
@@ -1028,7 +1284,9 @@ Continue recording with browser_* tools, then call skyramp_export_zip to write t
1028
1284
  * take a snapshot to find the options, then click the matching option.
1029
1285
  */
1030
1286
  async _handleSelectOption(args) {
1031
- const result = await this._browserBackend.callTool("browser_select_option", args);
1287
+ const { result, provenance } = await this._runActionOperation(
1288
+ () => this._browserBackend.callTool("browser_select_option", args)
1289
+ );
1032
1290
  const resultText = result.content?.[0]?.type === "text" ? result.content[0].text : "";
1033
1291
  if (!result.isError) {
1034
1292
  const code = (0, import_response.parseResponse)(result)?.code ?? "";
@@ -1050,18 +1308,18 @@ Continue recording with browser_* tools, then call skyramp_export_zip to write t
1050
1308
  const selectCode = `await page.${locatorExpr}.selectOption(${JSON.stringify(values2.length === 1 ? values2[0] : values2)});`;
1051
1309
  traceDebug(`Improved select selector: ${selectCode}`);
1052
1310
  const timestamp = Date.now();
1053
- this._trackedActions.push({
1311
+ this._recordAction({
1054
1312
  toolName: "browser_select_option",
1055
1313
  args,
1056
1314
  code: selectCode,
1057
1315
  timestamp
1058
- });
1316
+ }, provenance);
1059
1317
  return result;
1060
1318
  }
1061
1319
  }
1062
1320
  }
1063
1321
  }
1064
- this._maybeTrackAction("browser_select_option", args, result);
1322
+ this._maybeTrackAction("browser_select_option", args, result, provenance);
1065
1323
  return result;
1066
1324
  }
1067
1325
  const shouldFallback = resultText.includes("not a <select> element") || resultText.includes("selectOption") && resultText.includes("Timeout") || resultText.includes("not found in the current page snapshot");
@@ -1080,11 +1338,13 @@ Continue recording with browser_* tools, then call skyramp_export_zip to write t
1080
1338
  const comboboxes = [...snapText.matchAll(/combobox[^\n]*\[ref=(\w+)\]/g)];
1081
1339
  const clickableCombo = comboboxes.find((m) => m[0].includes("cursor=pointer"));
1082
1340
  const comboRef = clickableCombo?.[1] || args.ref;
1083
- const clickResult = await this._browserBackend.callTool("browser_click", { element: args.element || "dropdown", ref: comboRef });
1341
+ const { result: clickResult, provenance: clickProvenance } = await this._runActionOperation(
1342
+ () => this._browserBackend.callTool("browser_click", { element: args.element || "dropdown", ref: comboRef })
1343
+ );
1084
1344
  if (clickResult.isError)
1085
1345
  return { content: [{ type: "text", text: `### Error
1086
1346
  Failed to open custom dropdown. The dropdown modal may not be visible yet. Try calling browser_snapshot first to check the page state, then ensure the form/modal is open before selecting an option.` }], isError: true };
1087
- this._maybeTrackAction("browser_click", { element: args.element || "dropdown", ref: comboRef }, clickResult);
1347
+ this._maybeTrackAction("browser_click", { element: args.element || "dropdown", ref: comboRef }, clickResult, clickProvenance);
1088
1348
  const snapResult = await this._browserBackend.callTool("browser_snapshot", {});
1089
1349
  if (snapResult.isError)
1090
1350
  return snapResult;
@@ -1102,15 +1362,19 @@ Failed to open custom dropdown. The dropdown modal may not be visible yet. Try c
1102
1362
  const pattern1 = new RegExp(`option\\s+"${escaped}"[^\\n]*\\[ref=(\\w+)\\]`, "i");
1103
1363
  const match1 = snapText.match(pattern1);
1104
1364
  if (match1) {
1105
- const optionClickResult = await this._browserBackend.callTool("browser_click", { element: `${candidate} option`, ref: match1[1] });
1106
- this._maybeTrackAction("browser_click", { element: `${candidate} option`, ref: match1[1] }, optionClickResult);
1365
+ const { result: optionClickResult, provenance: optionProvenance } = await this._runActionOperation(
1366
+ () => this._browserBackend.callTool("browser_click", { element: `${candidate} option`, ref: match1[1] })
1367
+ );
1368
+ this._maybeTrackAction("browser_click", { element: `${candidate} option`, ref: match1[1] }, optionClickResult, optionProvenance);
1107
1369
  return optionClickResult;
1108
1370
  }
1109
1371
  const pattern2 = new RegExp(`option[^\\n]*\\[ref=(\\w+)\\][^\\n]*${escaped}`, "i");
1110
1372
  const match2 = snapText.match(pattern2);
1111
1373
  if (match2) {
1112
- const optionClickResult = await this._browserBackend.callTool("browser_click", { element: `${candidate} option`, ref: match2[1] });
1113
- this._maybeTrackAction("browser_click", { element: `${candidate} option`, ref: match2[1] }, optionClickResult);
1374
+ const { result: optionClickResult, provenance: optionProvenance } = await this._runActionOperation(
1375
+ () => this._browserBackend.callTool("browser_click", { element: `${candidate} option`, ref: match2[1] })
1376
+ );
1377
+ this._maybeTrackAction("browser_click", { element: `${candidate} option`, ref: match2[1] }, optionClickResult, optionProvenance);
1114
1378
  return optionClickResult;
1115
1379
  }
1116
1380
  }
@@ -1119,6 +1383,82 @@ Failed to open custom dropdown. The dropdown modal may not be visible yet. Try c
1119
1383
  Opened the custom dropdown but could not find a matching option for "${targetValue}". Available options:
1120
1384
  ${optionLines}` }], isError: true };
1121
1385
  }
1386
+ async _handleFillForm(args) {
1387
+ const fields = args.fields;
1388
+ if (!fields?.length)
1389
+ return this._browserBackend.callTool("browser_fill_form", args);
1390
+ const content = [];
1391
+ for (const field of fields) {
1392
+ const fieldArgs = { fields: [field] };
1393
+ const timestamp = Date.now();
1394
+ const pageAlias = this._currentPageAlias;
1395
+ const { result, provenance } = await this._runActionOperation(
1396
+ () => this._browserBackend.callTool("browser_fill_form", fieldArgs)
1397
+ );
1398
+ if (result.isError) {
1399
+ const resultText = result.content?.[0]?.type === "text" ? result.content[0].text : "";
1400
+ if (field.type === "combobox" && (resultText.includes("not a <select> element") || resultText.includes("selectOption") && resultText.includes("Timeout"))) {
1401
+ const selectResult = await this._handleSelectOption({
1402
+ element: field.name || "dropdown",
1403
+ ref: field.ref,
1404
+ values: [field.value]
1405
+ });
1406
+ if (selectResult.isError)
1407
+ return { ...selectResult, content: [...content, ...selectResult.content ?? []] };
1408
+ content.push(...selectResult.content ?? []);
1409
+ continue;
1410
+ }
1411
+ return { ...result, content: [...content, ...result.content ?? []] };
1412
+ }
1413
+ this._maybeTrackAction("browser_fill_form", fieldArgs, result, provenance, timestamp, pageAlias);
1414
+ content.push(...result.content ?? []);
1415
+ }
1416
+ return { content };
1417
+ }
1418
+ async _handleTypeWithSubmit(name, args) {
1419
+ const tab = this._browserBackend.context?.currentTab();
1420
+ let submitTarget;
1421
+ const typeArgs = { ...args, submit: false };
1422
+ const typeTimestamp = Date.now();
1423
+ const typePageAlias = this._currentPageAlias;
1424
+ const { result: rawTypeResult, provenance: typeProvenance } = await this._runActionOperation(
1425
+ async () => {
1426
+ if (name === "browser_type" && tab) {
1427
+ try {
1428
+ submitTarget = await tab.refLocator({ element: args.element, ref: args.ref });
1429
+ } catch (error) {
1430
+ return { content: [{ type: "text", text: `### Error
1431
+ ${String(error)}` }], isError: true };
1432
+ }
1433
+ }
1434
+ return this._browserBackend.callTool(name, typeArgs);
1435
+ }
1436
+ );
1437
+ const typeResult = await this._improveActionSelector(name, typeArgs, rawTypeResult, null, null, 0, null);
1438
+ this._maybeTrackAction(name, typeArgs, typeResult, typeProvenance, typeTimestamp, typePageAlias);
1439
+ if (typeResult.isError)
1440
+ return typeResult;
1441
+ const pressArgs = { key: "Enter" };
1442
+ const pressTimestamp = Date.now();
1443
+ const pressPageAlias = this._currentPageAlias;
1444
+ const { result: pressResult, provenance: pressProvenance } = await this._runActionOperation(
1445
+ async () => {
1446
+ if (!submitTarget || !tab)
1447
+ return this._browserBackend.callTool("browser_press_key", pressArgs);
1448
+ const response = import_response.Response.create(tab.context, "browser_press_key", pressArgs);
1449
+ response.setIncludeSnapshot();
1450
+ response.addCode(`await page.${submitTarget.resolved}.press('Enter');`);
1451
+ try {
1452
+ await tab.waitForCompletion(() => submitTarget.locator.press("Enter"));
1453
+ } catch (error) {
1454
+ response.addError(String(error));
1455
+ }
1456
+ return response.build();
1457
+ }
1458
+ );
1459
+ this._maybeTrackAction("browser_press_key", pressArgs, pressResult, pressProvenance, pressTimestamp, pressPageAlias);
1460
+ return { ...pressResult, content: [...typeResult.content ?? [], ...pressResult.content ?? []] };
1461
+ }
1122
1462
  /**
1123
1463
  * Find the Playwright Frame whose document hosts a GoJS diagram in the given
1124
1464
  * container div (walks all frames; GoJS lives in nested same-origin iframes).
@@ -1197,7 +1537,7 @@ ${optionLines}` }], isError: true };
1197
1537
  * record a diagramNodeAdd action carrying the semantic fields skyramp codegen
1198
1538
  * consumes.
1199
1539
  */
1200
- async _handleGoJSAddNode(params) {
1540
+ async _handleGoJSAddNode(params, provenance) {
1201
1541
  const timestamp = Date.now();
1202
1542
  const { frame, error: frameError } = await this._findGoJSFrame(params.diagramSelector);
1203
1543
  if (!frame)
@@ -1275,13 +1615,13 @@ GoJS add node failed: ${result ? result.err : "unknown"}` }], isError: true };
1275
1615
  anchorDocX: result.anchorDocX,
1276
1616
  anchorDocY: result.anchorDocY
1277
1617
  };
1278
- this._trackedActions.push({
1618
+ this._recordAction({
1279
1619
  toolName: import_gojsTool.gojsAddNodeSchema.name,
1280
1620
  args,
1281
1621
  code: "",
1282
1622
  timestamp,
1283
1623
  framePath
1284
- });
1624
+ }, provenance);
1285
1625
  traceDebug(`GoJS add node: ${params.category} key=${result.key} anchor=${params.anchorKey ?? "-"}`);
1286
1626
  return { content: [{ type: "text", text: `### Result
1287
1627
  Added GoJS node "${params.category}" (key ${result.key})${params.anchorKey ? ` near ${params.anchorKey}` : ""}.` }] };
@@ -1290,7 +1630,7 @@ Added GoJS node "${params.category}" (key ${result.key})${params.anchorKey ? ` n
1290
1630
  * Handle browser_gojs_link: connect two GoJS nodes via the model API
1291
1631
  * (mirroring the GoJSDiagram helper) and record a diagramLinkAdd action.
1292
1632
  */
1293
- async _handleGoJSLink(params) {
1633
+ async _handleGoJSLink(params, provenance) {
1294
1634
  const timestamp = Date.now();
1295
1635
  const { frame, error: frameError } = await this._findGoJSFrame(params.diagramSelector);
1296
1636
  if (!frame)
@@ -1336,13 +1676,13 @@ GoJS link failed: ${result ? result.err : "unknown"}` }], isError: true };
1336
1676
  fromPort: params.fromPort,
1337
1677
  toPort: params.toPort
1338
1678
  };
1339
- this._trackedActions.push({
1679
+ this._recordAction({
1340
1680
  toolName: import_gojsTool.gojsLinkSchema.name,
1341
1681
  args,
1342
1682
  code: "",
1343
1683
  timestamp,
1344
1684
  framePath
1345
- });
1685
+ }, provenance);
1346
1686
  traceDebug(`GoJS link: ${params.fromKey} -> ${params.toKey} (${params.fromPort}->${params.toPort})`);
1347
1687
  return { content: [{ type: "text", text: `### Result
1348
1688
  Linked GoJS nodes ${params.fromKey} \u2192 ${params.toKey} (${params.fromPort}\u2192${params.toPort}).` }] };
@@ -1356,7 +1696,7 @@ Linked GoJS nodes ${params.fromKey} \u2192 ${params.toKey} (${params.fromPort}\u
1356
1696
  * dispatched to _handleAssertOrder instead: two refs, a live boundingBox()
1357
1697
  * comparison, no pointer-off read. Only passing assertions are recorded.
1358
1698
  */
1359
- async _handleAssert(params) {
1699
+ async _handleAssert(params, provenance) {
1360
1700
  const timestamp = Date.now();
1361
1701
  if (!params.expected && (params.type === "text" || params.type === "value" || params.type === "count"))
1362
1702
  return { content: [{ type: "text", text: '### Error\n"expected" parameter is required.' }], isError: true };
@@ -1620,8 +1960,8 @@ Could not count "${refRole}" elements: ${e.message}` }], isError: true };
1620
1960
  break;
1621
1961
  }
1622
1962
  if (unhoverMove)
1623
- this._trackedActions.push({ toolName: "browser_mouse_action", args: { name: "mouse.move", position: unhoverMove }, code: "", timestamp: timestamp - 1, pageAlias: this._currentPageAlias });
1624
- this._trackedActions.push({ toolName: "browser_assert", args, code, timestamp, pageAlias: this._currentPageAlias });
1963
+ this._recordAction({ toolName: "browser_mouse_action", args: { name: "mouse.move", position: unhoverMove }, code: "", timestamp: timestamp - 1, pageAlias: this._currentPageAlias }, provenance);
1964
+ this._recordAction({ toolName: "browser_assert", args, code, timestamp, pageAlias: this._currentPageAlias }, provenance);
1625
1965
  traceDebug(`Assert: ${assertName} with selector ${args.selector}`);
1626
1966
  }
1627
1967
  return {
@@ -1640,7 +1980,7 @@ ${details}` }]
1640
1980
  * above/below — compared strictly. It says where each element STARTS, not
1641
1981
  * that one lies wholly beyond the other.
1642
1982
  */
1643
- async _handleAssertOrder(params) {
1983
+ async _handleAssertOrder(params, provenance) {
1644
1984
  const timestamp = Date.now();
1645
1985
  const error = (text) => ({ content: [{ type: "text", text: `### Error
1646
1986
  ${text}` }], isError: true });
@@ -1700,7 +2040,7 @@ ${details}` }] };
1700
2040
  otherLocator: b.parsed.locator,
1701
2041
  relation
1702
2042
  };
1703
- this._trackedActions.push({ toolName: "browser_assert", args, code: `assertOrder:${a.selector}:${relation}:${b.selector}`, timestamp, pageAlias: this._currentPageAlias });
2043
+ this._recordAction({ toolName: "browser_assert", args, code: `assertOrder:${a.selector}:${relation}:${b.selector}`, timestamp, pageAlias: this._currentPageAlias }, provenance);
1704
2044
  traceDebug(`Assert: assertOrder ${a.selector} ${relation} ${b.selector}`);
1705
2045
  return { content: [{ type: "text", text: `### Assertion Passed
1706
2046
  ${details}${a.note}${b.note}` }] };
@@ -1711,7 +2051,7 @@ ${details}${a.note}${b.note}` }] };
1711
2051
  * action. Column is 1-based over DATA columns (A=1); the leading row-number
1712
2052
  * <th> is skipped via td:nth-child(column + 1). Row is the 1-based visible row.
1713
2053
  */
1714
- async _handleAssertTableCell(params) {
2054
+ async _handleAssertTableCell(params, provenance) {
1715
2055
  const timestamp = Date.now();
1716
2056
  const tablePrefix = params.table ? `${params.table} ` : "";
1717
2057
  const selector = `${tablePrefix}tbody tr:nth-child(${params.row}) td:nth-child(${params.column + 1})`;
@@ -1770,7 +2110,7 @@ No cell at row ${params.row}, column ${params.column} (selector: ${selector}).`
1770
2110
  isInput: !!cell.isInput
1771
2111
  });
1772
2112
  if (passed) {
1773
- this._trackedActions.push({
2113
+ this._recordAction({
1774
2114
  toolName: import_tableAssertTool.tableAssertToolSchema.name,
1775
2115
  // Record the active page so assertions on popup/secondary tabs are
1776
2116
  // attributed correctly (mirrors _handleAssertApiRequest).
@@ -1785,7 +2125,7 @@ No cell at row ${params.row}, column ${params.column} (selector: ${selector}).`
1785
2125
  },
1786
2126
  code: "",
1787
2127
  timestamp
1788
- });
2128
+ }, provenance);
1789
2129
  traceDebug(`AssertTableCell: ${selector} \u2192 "${params.expected}" (isInput=${cell.isInput})`);
1790
2130
  }
1791
2131
  return {
@@ -1870,7 +2210,7 @@ The current page (${url}) is not a loaded application page; navigate to the app
1870
2210
  * assertActionToJsonl and the re-seed path share one shape with the other
1871
2211
  * assertions.
1872
2212
  */
1873
- async _handleAssertHidden(params, timestamp) {
2213
+ async _handleAssertHidden(params, timestamp, provenance) {
1874
2214
  const err = (text) => ({ content: [{ type: "text", text: `### Error
1875
2215
  ${text}` }], isError: true });
1876
2216
  const given = [params.testId, params.role, params.text, params.attribute].filter((v) => typeof v === "string" && v.trim() !== "");
@@ -1927,7 +2267,7 @@ ${text}` }], isError: true });
1927
2267
  let details;
1928
2268
  if (passed) {
1929
2269
  details = `Hidden assertion passed: "${params.element}" is absent or not visible.`;
1930
- this._trackedActions.push({ toolName: "browser_assert", args: { type: "hidden", selector, locator }, code: `assertHidden:${selector}`, timestamp, pageAlias: this._currentPageAlias });
2270
+ this._recordAction({ toolName: "browser_assert", args: { type: "hidden", selector, locator }, code: `assertHidden:${selector}`, timestamp, pageAlias: this._currentPageAlias }, provenance);
1931
2271
  traceDebug(`Assert: assertHidden with selector ${selector}`);
1932
2272
  } else {
1933
2273
  const count = await tab.page.locator(selector).count().catch(() => -1);
@@ -2683,7 +3023,87 @@ ${parsed.selector} does not pick out "${opts.element}" on the live page: ${pinFa
2683
3023
  }
2684
3024
  };
2685
3025
  }
2686
- _maybeTrackAction(toolName, args, result, timestamp, pageAliasBeforeAction) {
3026
+ async _withActionProvenance(operation) {
3027
+ const provenance = this._recordingPhase.beginAction();
3028
+ try {
3029
+ return await operation(provenance);
3030
+ } finally {
3031
+ this._recordingPhase.endAction(provenance.actionId);
3032
+ }
3033
+ }
3034
+ async _runActionOperation(operation) {
3035
+ const provenance = this._recordingPhase.beginAction();
3036
+ try {
3037
+ return { result: await operation(), provenance };
3038
+ } finally {
3039
+ this._recordingPhase.endAction(provenance.actionId);
3040
+ }
3041
+ }
3042
+ _recordAction(action, provenance) {
3043
+ this._trackedActions.push({ ...action, ...provenance });
3044
+ }
3045
+ /**
3046
+ * Close the browser context to flush the HAR file to disk (Playwright
3047
+ * writes HAR on context close). Bounded: a heavy page (e.g. a
3048
+ * nested-iframe GoJS canvas with live timers) can stall context close
3049
+ * indefinitely. The HAR flush is best-effort; the JSONL trace (what
3050
+ * codegen consumes) is complete regardless.
3051
+ */
3052
+ async _closeBrowserForExport() {
3053
+ const closePromise = this._browserBackend.callTool("browser_close", {}).catch(() => {
3054
+ });
3055
+ let closeTimer;
3056
+ const closeTimeout = new Promise((resolve) => {
3057
+ closeTimer = setTimeout(() => {
3058
+ traceDebug("export: browser_close did not finish within 20s; proceeding with HAR as-is");
3059
+ resolve();
3060
+ }, 2e4);
3061
+ });
3062
+ await Promise.race([closePromise, closeTimeout]);
3063
+ clearTimeout(closeTimer);
3064
+ }
3065
+ /**
3066
+ * End the recording: the next one starts fresh. A successful export ends
3067
+ * the data-safety session too — the next recording's ledger must contain
3068
+ * only its own entries and blocks, and its teardown must not re-DELETE
3069
+ * this session's entities. The temp dir and HAR path stay stable so
3070
+ * recordHar stays in sync (SKYR-3830).
3071
+ */
3072
+ _resetRecording() {
3073
+ this._trackedActions = [];
3074
+ this._droppedActions = [];
3075
+ const ra = this._browserBackend.context?.recorderActions;
3076
+ if (ra)
3077
+ ra.length = 0;
3078
+ this._recordingPhase.reset();
3079
+ this._passiveActionIds.clear();
3080
+ this._guard.resetSession();
3081
+ this._cleanupTempDir(true);
3082
+ }
3083
+ /**
3084
+ * A recording whose resource provenance is invalid can never export: its
3085
+ * records are already deleted and the phases of its creates are fixed.
3086
+ * Close the browser (so the next HAR holds only the new attempt) and reset
3087
+ * the recording, so recording again from the start can export.
3088
+ */
3089
+ async _refuseAndResetRecording(reason) {
3090
+ const created = this._guard.ledger.length;
3091
+ await this._closeBrowserForExport();
3092
+ this._resetRecording();
3093
+ const deleted = created ? ` Data safety teardown already deleted the ${created} record(s) this attempt created, setup data included.` : "";
3094
+ return {
3095
+ content: [{ type: "text", text: `### Error
3096
+ ${reason}
3097
+ The recording was NOT exported, and it was reset.${deleted} The browser was closed. Record again from the start: browser_navigate, the setup steps, skyramp_finish_test_setup, then the scenario steps.` }],
3098
+ isError: true
3099
+ };
3100
+ }
3101
+ /** See resolveSourceActions: a create's source action must have a JSONL line. */
3102
+ _resolveSourceActions() {
3103
+ for (const [entry, actionId] of (0, import_exportTool.resolveSourceActions)(this._trackedActions, this._guard.ledger, this._passiveActionIds))
3104
+ this._guard.setSourceAction(entry, actionId);
3105
+ }
3106
+ _maybeTrackAction(toolName, args, result, provenance, timestamp, pageAliasBeforeAction) {
2687
3107
  if (result.isError) {
2688
3108
  const errParsed = (0, import_response.parseResponse)(result);
2689
3109
  const errText = (errParsed?.error ?? errParsed?.text ?? "unknown error").replace(/\s+/g, " ").trim().slice(0, 300);
@@ -2699,14 +3119,14 @@ ${parsed.selector} does not pick out "${opts.element}" on the live page: ${pinFa
2699
3119
  const popupAlias = this._pendingPopupAlias ?? void 0;
2700
3120
  this._pendingPopupAlias = null;
2701
3121
  const pageAlias = popupAlias ? pageAliasBeforeAction ?? this._currentPageAlias : this._currentPageAlias;
2702
- this._trackedActions.push({
3122
+ this._recordAction({
2703
3123
  toolName,
2704
3124
  args,
2705
3125
  code,
2706
3126
  timestamp: timestamp ?? Date.now(),
2707
3127
  pageAlias,
2708
3128
  popupAlias
2709
- });
3129
+ }, provenance);
2710
3130
  traceDebug(`Tracked action: ${toolName} on ${pageAlias}${popupAlias ? ` \u2192 popup:${popupAlias}` : ""} (total: ${this._trackedActions.length})`);
2711
3131
  }
2712
3132
  }
@@ -2799,18 +3219,69 @@ ${parsed.selector} does not pick out "${opts.element}" on the live page: ${pinFa
2799
3219
  }
2800
3220
  }
2801
3221
  async _autoExportAndClose() {
3222
+ let { teardownClean } = await this._session.settleAndTeardownBestEffort();
3223
+ const guardActive = this._session.active;
3224
+ const hasAudit = guardActive && (this._guard.ledger.length > 0 || this._guard.blocks.length > 0 || this._guard.unparsedCreates.length > 0);
3225
+ let zipWritten = false;
2802
3226
  if (this._trackedActions.length) {
2803
3227
  try {
2804
3228
  this._consumeRecorderSelectors();
3229
+ this._resolveSourceActions();
3230
+ const provenance = (0, import_exportTool.validateResourceProvenance)(this._trackedActions, this._guard.ledger, this._recordingPhase.setupCompleted);
3231
+ if (!provenance.ok)
3232
+ process.stderr.write(`[skyramp] Auto-export: the recording's resource provenance is invalid, so test generation will refuse it: ${provenance.message}
3233
+ `);
2805
3234
  const outputZip = import_path.default.resolve(this._outputDir, "skyramp_export.zip");
2806
- const { jsonl: jsonlContent, actionCount } = (0, import_skyRampExport.buildJsonlContent)(this._trackedActions, "chromium", this._harPath);
2807
- await (0, import_skyRampExport.writeSkyrampZip)(outputZip, jsonlContent, this._harPath);
2808
- process.stderr.write(`[skyramp] Auto-exported ${actionCount} actions to ${outputZip}
3235
+ const { jsonl: jsonlContent, actionCount } = (0, import_skyRampExport.buildJsonlContent)(this._trackedActions, "chromium", this._harPath, void 0, void 0, this._guard.ledger);
3236
+ await (0, import_skyRampExport.writeSkyrampZip)(
3237
+ outputZip,
3238
+ jsonlContent,
3239
+ this._harPath,
3240
+ guardActive ? [{ name: "dataSafetyLedger.json", content: this._guard.ledgerJson(this._recordingPhase.setupCompleted) }] : void 0
3241
+ );
3242
+ zipWritten = true;
3243
+ const incompleteReasons = [];
3244
+ if (!teardownClean)
3245
+ incompleteReasons.push("teardown left entities behind \u2014 see dataSafetyLedger.json");
3246
+ const unparsedCount = this._guard.unparsedCreates.length;
3247
+ if (unparsedCount > 0)
3248
+ incompleteReasons.push(`${unparsedCount} unidentified create(s) \u2014 see dataSafetyLedger.json`);
3249
+ const incompleteNote = incompleteReasons.length ? ` (data-safety-incomplete: ${incompleteReasons.join("; ")})` : "";
3250
+ process.stderr.write(`[skyramp] Auto-exported ${actionCount} actions to ${outputZip}${incompleteNote}
3251
+ `);
3252
+ } catch (e) {
3253
+ process.stderr.write(`[skyramp] Auto-export failed: ${String(e)}
2809
3254
  `);
2810
- } catch {
2811
3255
  }
2812
3256
  }
2813
3257
  this._browserBackend?.serverClosed();
3258
+ if (guardActive) {
3259
+ const late = await this._session.settleBodyReads();
3260
+ if (late.timedOut || this._guard.pendingTeardownCount > 0)
3261
+ teardownClean = false;
3262
+ }
3263
+ const mustPreserve = !teardownClean || hasAudit && !zipWritten;
3264
+ const callerLedger = guardActive && this._guard.config.ledgerDir && (hasAudit || import_fs.default.existsSync(this._guard.ledgerPath)) ? this._guard.ledgerPath : void 0;
3265
+ let preserve = "not-needed";
3266
+ if (mustPreserve || callerLedger) {
3267
+ try {
3268
+ const preservedPath = callerLedger ?? import_path.default.join(this._outputDir, `dataSafetyLedger-${Date.now()}.json`);
3269
+ (0, import_dataSafetyGuard.writeLedgerFile)(preservedPath, this._guard.ledgerJson(this._recordingPhase.setupCompleted));
3270
+ if (mustPreserve)
3271
+ process.stderr.write(`[skyramp] Data safety: ${teardownClean ? "audit trail" : "teardown incomplete"} \u2014 ledger preserved at ${preservedPath}
3272
+ `);
3273
+ preserve = "done";
3274
+ } catch (e) {
3275
+ process.stderr.write(`[skyramp] Data safety: failed to preserve ledger (${String(e)}) \u2014 dumping inline so the record survives:
3276
+ ${this._guard.ledgerJson(this._recordingPhase.setupCompleted)}
3277
+ `);
3278
+ preserve = "failed";
3279
+ }
3280
+ }
3281
+ traceDebug(`data safety abnormal-end preserve outcome: ${preserve}`);
3282
+ this._recordingPhase.reset();
3283
+ this._passiveActionIds.clear();
3284
+ this._guard.resetSession();
2814
3285
  this._cleanupTempDir(false);
2815
3286
  }
2816
3287
  }