{"version":3,"file":"transaction.d.ts","sourceRoot":"","sources":["../../../src/core/safety/transaction.ts"],"names":[],"mappings":"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","sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport fs, { chmod, mkdir, readFile, rm, symlink, unlink, writeFile } from \"node:fs/promises\";\nimport nodePath from \"node:path\";\nimport type { WorkspaceBoundary } from \"./boundary.js\";\nimport { type CheckpointEntry, type CheckpointStore, sha256 } from \"./checkpoint.js\";\nimport type { ExecutionMode, PolicyDecision, RecoveryClass, RollbackCapability, TransactionId } from \"./types.js\";\n\nexport type WorkspaceEdit =\n\t| { kind: \"create_file\"; path: string; content: string }\n\t| { kind: \"replace_file\"; path: string; content: string; expectedSha256?: string }\n\t| { kind: \"delete_file\"; path: string }\n\t| { kind: \"create_directory\"; path: string }\n\t| { kind: \"delete_directory\"; path: string };\n\nexport interface ValidationGate {\n\tid: string;\n\t/** command identity / human label */\n\tlabel: string;\n\trun: (() => Promise<{ exitCode: number; outputArtifact: string }>) | null;\n}\n\nexport type TransactionStage =\n\t| \"prepared\"\n\t| \"checkpointed\"\n\t| \"applied\"\n\t| \"validating\"\n\t| \"validated\"\n\t| \"confirmed\"\n\t| \"rolled_back\"\n\t| \"recovery_required\";\n\nexport interface TransactionRecord {\n\ttransactionId: TransactionId;\n\tworkspaceId: string;\n\trunId?: string;\n\tmode: ExecutionMode;\n\tcreatedAt: number;\n\tpolicy: PolicyDecision | null;\n\tcheckpointId?: string;\n\tstage: TransactionStage;\n\tappliedPaths: string[];\n\t/** sha of the transaction-intended post-state per path. */\n\tappliedSha: Record<string, string | null>;\n\toutput?: {\n\t\tcreated: string[];\n\t\tmodified: string[];\n\t\tdeleted: string[];\n\t\tbytesChanged: number;\n\t};\n\tvalidation?: {\n\t\tcommand: string;\n\t\texitCode: number;\n\t\toutputArtifact: string;\n\t\tdurationMs: number;\n\t\ttimedOut: boolean;\n\t\taborted: boolean;\n\t\tresult: \"passed\" | \"failed\" | \"skipped\" | \"timed_out\" | \"aborted\";\n\t};\n\trollbackCapability: RollbackCapability;\n}\n\nexport interface ApplyResult {\n\tchanged: { path: string; sha256: string | null }[];\n\tbytesChanged: number;\n}\n\nexport interface RollbackResult {\n\tstatus: \"rolled_back\" | \"conflict\" | \"nothing_to_do\";\n\tconflicts: RollbackConflict[];\n\trestored: string[];\n}\n\nexport interface RollbackConflict {\n\tpath: string;\n\texpectedTransactionSha: string | null;\n\tcurrentSha: string;\n\tcheckpointSha: string | null;\n\tmessage: string;\n}\n\nexport class TransactionError extends Error {\n\treadonly code: string;\n\tconstructor(code: string, message: string) {\n\t\tsuper(message);\n\t\tthis.code = code;\n\t\tthis.name = \"TransactionError\";\n\t}\n}\n\n/**\n * Transactional edit batches over the workspace.\n *\n * Lifecycle: begin → checkpoint → apply → validate → confirm | rollback →\n * release. All target paths are resolved/validated up-front. Partial apply\n * triggers rollback. Validation gates run before confirmation and failed\n * validation rolls back (rollback is idempotent). Drift caused by unrelated\n * post-transaction user edits is reported as a structured conflict rather than\n * silently overwritten.\n */\nexport class WorkspaceTransactionManager {\n\treadonly storageDir: string;\n\tprivate readonly boundary: WorkspaceBoundary;\n\tprivate readonly checkpoints: CheckpointStore;\n\n\tconstructor(storageDir: string, boundary: WorkspaceBoundary, checkpoints: CheckpointStore) {\n\t\tthis.storageDir = storageDir;\n\t\tthis.boundary = boundary;\n\t\tthis.checkpoints = checkpoints;\n\t}\n\n\tprivate txPath(id: string): string {\n\t\treturn nodePath.join(this.storageDir, \"transactions\", `${id}.json`);\n\t}\n\n\tasync begin(\n\t\tworkspaceId: string,\n\t\topts: {\n\t\t\trunId?: string;\n\t\t\tmode: ExecutionMode;\n\t\t\tpolicy: PolicyDecision | null;\n\t\t\trollbackCapability?: RollbackCapability;\n\t\t},\n\t): Promise<TransactionRecord> {\n\t\tconst record: TransactionRecord = {\n\t\t\ttransactionId: randomUUID(),\n\t\t\tworkspaceId,\n\t\t\trunId: opts.runId,\n\t\t\tmode: opts.mode,\n\t\t\tcreatedAt: Date.now(),\n\t\t\tpolicy: opts.policy,\n\t\t\tstage: \"prepared\",\n\t\t\tappliedPaths: [],\n\t\t\tappliedSha: {},\n\t\t\trollbackCapability: opts.rollbackCapability ?? \"full\",\n\t\t};\n\t\tawait fs.mkdir(nodePath.dirname(this.txPath(record.transactionId)), { recursive: true });\n\t\tawait this.persist(record);\n\t\treturn record;\n\t}\n\n\t/** Plan-mode preview: reports intended effect with zero physical mutation. */\n\tasync preview(edits: WorkspaceEdit[]): Promise<{\n\t\tcreated: string[];\n\t\tmodified: string[];\n\t\tdeleted: string[];\n\t\tbytesChanged: number;\n\t}> {\n\t\tconst created: string[] = [];\n\t\tconst modified: string[] = [];\n\t\tconst deleted: string[] = [];\n\t\tlet bytesChanged = 0;\n\t\tfor (const e of edits) {\n\t\t\tawait this.boundary.resolveWithin(e.path);\n\t\t\tif (e.kind === \"create_file\") {\n\t\t\t\tcreated.push(e.path);\n\t\t\t\tbytesChanged += Buffer.byteLength(e.content);\n\t\t\t} else if (e.kind === \"replace_file\") {\n\t\t\t\tmodified.push(e.path);\n\t\t\t\tbytesChanged += Buffer.byteLength(e.content);\n\t\t\t} else if (e.kind === \"delete_file\" || e.kind === \"delete_directory\") {\n\t\t\t\tdeleted.push(e.path);\n\t\t\t} else if (e.kind === \"create_directory\") {\n\t\t\t\tcreated.push(e.path);\n\t\t\t}\n\t\t}\n\t\treturn { created, modified, deleted, bytesChanged };\n\t}\n\n\tasync resolvePaths(edits: WorkspaceEdit[]): Promise<string[]> {\n\t\tconst out: string[] = [];\n\t\tfor (const e of edits) {\n\t\t\tout.push(await this.boundary.resolveWithin(e.path));\n\t\t\tout.push(await this.boundary.resolveWithin(nodePath.dirname(e.path)));\n\t\t}\n\t\treturn [...new Set(out)];\n\t}\n\n\tasync checkpoint(record: TransactionRecord, editPaths: string[]): Promise<string> {\n\t\tconst cp = await this.checkpoints.create(record.workspaceId, record.transactionId, editPaths, record.runId);\n\t\trecord.checkpointId = cp.checkpointId;\n\t\trecord.stage = \"checkpointed\";\n\t\tawait this.persist(record);\n\t\treturn cp.checkpointId;\n\t}\n\n\t/**\n\t * Apply a batch of edits deterministically. Order: create_directory first,\n\t * then create_file/replace_file, then delete_file, then delete_directory.\n\t * Returns hash results. Drift/expectedSha preconditions abort safely.\n\t */\n\tasync apply(record: TransactionRecord, edits: WorkspaceEdit[]): Promise<ApplyResult> {\n\t\tconst absEdits: { e: WorkspaceEdit; abs: string }[] = [];\n\t\tfor (const e of edits) {\n\t\t\tabsEdits.push({ e, abs: await this.boundary.resolveWithin(e.path) });\n\t\t}\n\t\tconst byKind = {\n\t\t\tcreate_directory: [] as typeof absEdits,\n\t\t\tfiles: [] as typeof absEdits,\n\t\t\tdelete: [] as typeof absEdits,\n\t\t};\n\t\tfor (const x of absEdits) {\n\t\t\tif (x.e.kind === \"create_directory\") byKind.create_directory.push(x);\n\t\t\telse if (x.e.kind === \"delete_file\" || x.e.kind === \"delete_directory\") byKind.delete.push(x);\n\t\t\telse byKind.files.push(x);\n\t\t}\n\t\tconst changed: ApplyResult[\"changed\"] = [];\n\t\tlet bytesChanged = 0;\n\t\ttry {\n\t\t\tconst create = async (x: { e: WorkspaceEdit; abs: string }) => {\n\t\t\t\tawait this.boundary.assertParentWithin(x.abs);\n\t\t\t\tawait writeFile(x.abs, (x.e as { content: string }).content, { encoding: \"utf-8\", flag: \"wx\" });\n\t\t\t\tconst content = (x.e as { content: string }).content;\n\t\t\t\tbytesChanged += Buffer.byteLength(content);\n\t\t\t\tchanged.push({ path: x.abs, sha256: sha256(content) });\n\t\t\t\trecord.appliedSha[x.abs] = sha256(content);\n\t\t\t};\n\t\t\tconst replace = async (x: { e: WorkspaceEdit; abs: string }) => {\n\t\t\t\tconst e = x.e as { kind: \"replace_file\"; path: string; content: string; expectedSha256?: string };\n\t\t\t\tawait this.boundary.assertParentWithin(x.abs);\n\t\t\t\tif (e.expectedSha256) {\n\t\t\t\t\tconst cur = await readFile(x.abs).catch(() => null);\n\t\t\t\t\tif (cur && sha256(cur) !== e.expectedSha256) {\n\t\t\t\t\t\tthrow new TransactionError(\"drift\", `precondition hash mismatch for ${x.abs}`);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tawait writeFile(x.abs, e.content, { encoding: \"utf-8\", flag: \"w\" });\n\t\t\t\tbytesChanged += Buffer.byteLength(e.content);\n\t\t\t\tchanged.push({ path: x.abs, sha256: sha256(e.content) });\n\t\t\t\trecord.appliedSha[x.abs] = sha256(e.content);\n\t\t\t};\n\t\t\tconst del = async (x: { e: WorkspaceEdit; abs: string }) => {\n\t\t\t\tawait this.boundary.assertParentWithin(x.abs);\n\t\t\t\tawait rm(x.abs, { recursive: true, force: true });\n\t\t\t\tchanged.push({ path: x.abs, sha256: null });\n\t\t\t\trecord.appliedSha[x.abs] = null;\n\t\t\t};\n\t\t\tfor (const x of byKind.create_directory) {\n\t\t\t\tawait this.boundary.assertParentWithin(x.abs);\n\t\t\t\tawait mkdir(x.abs, { recursive: true });\n\t\t\t\tchanged.push({ path: x.abs, sha256: null });\n\t\t\t\trecord.appliedSha[x.abs] = null;\n\t\t\t}\n\t\t\tfor (const x of byKind.files) {\n\t\t\t\tif (x.e.kind === \"create_file\") await create(x);\n\t\t\t\telse await replace(x);\n\t\t\t}\n\t\t\tfor (const x of byKind.delete) await del(x);\n\t\t} catch (err) {\n\t\t\t// Partial application: roll back.\n\t\t\trecord.appliedPaths = changed.map((c) => c.path);\n\t\t\tawait this.rollback(record).catch(() => {});\n\t\t\tthrow err;\n\t\t}\n\t\trecord.stage = \"applied\";\n\t\trecord.appliedPaths = changed.map((c) => c.path);\n\t\trecord.output = {\n\t\t\tcreated: edits.filter((e) => e.kind === \"create_file\").map((e) => e.path),\n\t\t\tmodified: edits.filter((e) => e.kind === \"replace_file\").map((e) => e.path),\n\t\t\tdeleted: edits.filter((e) => e.kind === \"delete_file\" || e.kind === \"delete_directory\").map((e) => e.path),\n\t\t\tbytesChanged,\n\t\t};\n\t\tawait this.persist(record);\n\t\treturn { changed, bytesChanged };\n\t}\n\n\tasync validate(record: TransactionRecord, gate: ValidationGate): Promise<TransactionRecord> {\n\t\tif (!gate.run) {\n\t\t\trecord.validation = {\n\t\t\t\tcommand: gate.label,\n\t\t\t\texitCode: 0,\n\t\t\t\toutputArtifact: \"\",\n\t\t\t\tdurationMs: 0,\n\t\t\t\ttimedOut: false,\n\t\t\t\taborted: false,\n\t\t\t\tresult: \"skipped\",\n\t\t\t};\n\t\t\treturn record;\n\t\t}\n\t\trecord.stage = \"validating\";\n\t\tconst started = Date.now();\n\t\tlet exitCode = 1;\n\t\tlet outputArtifact = \"\";\n\t\tlet aborted = false;\n\t\tconst timedOut = false;\n\t\ttry {\n\t\t\tconst r = await gate.run();\n\t\t\texitCode = r.exitCode;\n\t\t\toutputArtifact = r.outputArtifact;\n\t\t} catch {\n\t\t\taborted = true;\n\t\t}\n\t\trecord.validation = {\n\t\t\tcommand: gate.label,\n\t\t\texitCode,\n\t\t\toutputArtifact,\n\t\t\tdurationMs: Date.now() - started,\n\t\t\ttimedOut,\n\t\t\taborted,\n\t\t\tresult: aborted ? \"aborted\" : exitCode === 0 ? \"passed\" : \"failed\",\n\t\t};\n\t\treturn record;\n\t}\n\n\tasync confirm(record: TransactionRecord): Promise<void> {\n\t\tif (record.validation && record.validation.result !== \"passed\" && record.validation.result !== \"skipped\") {\n\t\t\trecord.stage = \"recovery_required\";\n\t\t\tawait this.persist(record);\n\t\t\tthrow new TransactionError(\"validation_failed\", \"cannot confirm a transaction whose validation did not pass\");\n\t\t}\n\t\trecord.stage = \"confirmed\";\n\t\tawait this.persist(record);\n\t\tif (record.checkpointId) await this.checkpoints.updateStatus(record.checkpointId, \"confirmed\");\n\t}\n\n\tasync persist(record: TransactionRecord): Promise<void> {\n\t\tawait writeFile(this.txPath(record.transactionId), JSON.stringify(record, null, 2), { mode: 0o600 });\n\t}\n\n\tasync read(id: TransactionId): Promise<TransactionRecord | null> {\n\t\ttry {\n\t\t\treturn JSON.parse(await readFile(this.txPath(id), \"utf-8\")) as TransactionRecord;\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\tasync list(): Promise<TransactionRecord[]> {\n\t\tconst base = nodePath.join(this.storageDir, \"transactions\");\n\t\tlet names: string[] = [];\n\t\ttry {\n\t\t\tnames = await fs.readdir(base);\n\t\t} catch {\n\t\t\treturn [];\n\t\t}\n\t\tconst out: TransactionRecord[] = [];\n\t\tfor (const n of names) {\n\t\t\tif (!n.endsWith(\".json\")) continue;\n\t\t\tconst rec = await this.read(n.slice(0, -5));\n\t\t\tif (rec) out.push(rec);\n\t\t}\n\t\tout.sort((a, b) => b.createdAt - a.createdAt);\n\t\treturn out;\n\t}\n\n\t/**\n\t * Drift-aware rollback. Restores prior state from the checkpoint without\n\t * overwriting unrelated post-transaction user changes.\n\t */\n\tasync rollback(record: TransactionRecord): Promise<RollbackResult> {\n\t\tif (!record.checkpointId) return { status: \"nothing_to_do\", conflicts: [], restored: [] };\n\t\tconst cp = await this.checkpoints.verify(record.checkpointId);\n\t\tconst conflicts: RollbackConflict[] = [];\n\t\tconst restored: string[] = [];\n\t\t// Restore only the top-level target paths; ignore parent-metadata dirs.\n\t\tconst targets = cp.entries.filter((e) => record.appliedPaths.includes(e.path));\n\t\tconst targetSet = new Set(targets.map((t) => t.path));\n\t\tconst restoreEntry = async (entry: CheckpointEntry) => {\n\t\t\tconst abs = entry.path;\n\t\t\tawait this.boundary.assertParentWithin(abs);\n\t\t\tconst current = await readFile(abs).catch(() => null);\n\t\t\tconst currentSha = current ? sha256(current) : null;\n\t\t\tconst applied = record.appliedSha[abs] ?? null;\n\t\t\t// If the transaction intended a different state than what's there now,\n\t\t\t// and what's there now isn't the transaction's own state, it's user drift.\n\t\t\tif (entry.type === \"file\" && entry.existed) {\n\t\t\t\tif (current !== null && applied !== null && currentSha !== applied) {\n\t\t\t\t\t// Somebody changed it after the transaction wrote `applied`.\n\t\t\t\t\t// Only restore if current still equals the transaction's write.\n\t\t\t\t\tconflicts.push({\n\t\t\t\t\t\tpath: abs,\n\t\t\t\t\t\texpectedTransactionSha: applied as string,\n\t\t\t\t\t\tcurrentSha: currentSha as string,\n\t\t\t\t\t\tcheckpointSha: entry.contentSha256 ?? null,\n\t\t\t\t\t\tmessage: \"post-transaction user edit detected; refusing to overwrite\",\n\t\t\t\t\t});\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst content = await this.checkpoints.materialize(cp.checkpointId, entry);\n\t\t\t\tif (content !== null) {\n\t\t\t\t\tawait writeFile(abs, content, { flag: \"w\" });\n\t\t\t\t} else {\n\t\t\t\t\tawait unlink(abs).catch(() => {});\n\t\t\t\t}\n\t\t\t\tif (entry.mode) await chmod(abs, entry.mode).catch(() => {});\n\t\t\t\trestored.push(abs);\n\t\t\t} else if (entry.type === \"missing\" && !entry.existed) {\n\t\t\t\t// Transaction created it; remove it unless user modified afterwards.\n\t\t\t\tif (current !== null && applied !== null && currentSha !== applied && currentSha !== null) {\n\t\t\t\t\tconflicts.push({\n\t\t\t\t\t\tpath: abs,\n\t\t\t\t\t\texpectedTransactionSha: applied,\n\t\t\t\t\t\tcurrentSha,\n\t\t\t\t\t\tcheckpointSha: null,\n\t\t\t\t\t\tmessage: \"transaction-created file modified by user; refusing to delete\",\n\t\t\t\t\t});\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tawait rm(abs, { recursive: true, force: true }).catch(() => {});\n\t\t\t\trestored.push(abs);\n\t\t\t} else if (entry.type === \"symlink\") {\n\t\t\t\tawait unlink(abs).catch(() => {});\n\t\t\t\tif (entry.symlinkTarget) await symlink(entry.symlinkTarget, abs).catch(() => {});\n\t\t\t\trestored.push(abs);\n\t\t\t} else if (entry.type === \"directory\" && entry.existed) {\n\t\t\t\t// restore nothing; dirs removed by transaction are recreated\n\t\t\t\tawait mkdir(abs, { recursive: true }).catch(() => {});\n\t\t\t}\n\t\t};\n\t\tfor (const entry of cp.entries) {\n\t\t\tif (targetSet.has(entry.path)) await restoreEntry(entry);\n\t\t}\n\t\tif (conflicts.length > 0) {\n\t\t\trecord.stage = \"recovery_required\";\n\t\t\tawait this.persist(record);\n\t\t\treturn { status: \"conflict\", conflicts, restored };\n\t\t}\n\t\trecord.stage = \"rolled_back\";\n\t\tawait this.persist(record);\n\t\tif (record.checkpointId) await this.checkpoints.updateStatus(record.checkpointId, \"rolled_back\");\n\t\treturn { status: \"rolled_back\", conflicts, restored };\n\t}\n\n\t/** Classify an interrupted transaction at startup/resume. */\n\tasync classify(id: TransactionId): Promise<RecoveryClass> {\n\t\tconst rec = await this.read(id);\n\t\tif (!rec) return \"not_found\";\n\t\tif (rec.stage === \"confirmed\") return \"already_confirmed\";\n\t\tif (rec.stage === \"rolled_back\") return \"already_rolled_back\";\n\t\tif (!rec.checkpointId) return \"safe_to_resume_apply\";\n\t\tconst cp = await this.checkpoints.read(rec.checkpointId);\n\t\tif (!cp) return \"manual_conflict\";\n\t\tswitch (rec.stage) {\n\t\t\tcase \"checkpointed\":\n\t\t\tcase \"validated\":\n\t\t\t\treturn \"safe_to_resume_apply\";\n\t\t\tcase \"applied\":\n\t\t\tcase \"validating\":\n\t\t\t\treturn \"validation_required\";\n\t\t\tdefault:\n\t\t\t\treturn \"rollback_required\";\n\t\t}\n\t}\n}\n"]}