dsh-git-worktree 0.1.2 → 0.2.0

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 (133) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/LICENSE +21 -21
  3. package/README.md +115 -43
  4. package/README.zh.md +115 -41
  5. package/cordis.patch.yml +7 -7
  6. package/lib/adapters/registry.js +40 -0
  7. package/lib/adapters/registry.js.map +1 -1
  8. package/lib/adapters/safe-file.js +34 -4
  9. package/lib/adapters/safe-file.js.map +1 -1
  10. package/lib/client/WorktreeCreateRow.d.ts +7 -0
  11. package/lib/client/WorktreeCreateRow.js +26 -0
  12. package/lib/client/WorktreeCreateRow.js.map +1 -0
  13. package/lib/client/WorktreeReviewRow.d.ts +11 -0
  14. package/lib/client/WorktreeReviewRow.js +67 -0
  15. package/lib/client/WorktreeReviewRow.js.map +1 -0
  16. package/lib/client/actions.d.ts +86 -0
  17. package/lib/client/actions.js +35 -0
  18. package/lib/client/actions.js.map +1 -0
  19. package/lib/client/console-remote/adapter.d.ts +3 -0
  20. package/lib/client/console-remote/adapter.js +59 -0
  21. package/lib/client/console-remote/adapter.js.map +1 -0
  22. package/lib/client/console-remote/index.d.ts +18 -0
  23. package/lib/client/console-remote/index.js +17 -0
  24. package/lib/client/console-remote/index.js.map +1 -0
  25. package/lib/client/index.d.ts +17 -0
  26. package/lib/client/index.js +44 -0
  27. package/lib/client/index.js.map +1 -0
  28. package/lib/client/model.d.ts +68 -0
  29. package/lib/client/model.js +127 -0
  30. package/lib/client/model.js.map +1 -0
  31. package/lib/client/pre-session/PreSessionWorktreeToggle.d.ts +16 -0
  32. package/lib/client/pre-session/PreSessionWorktreeToggle.js +128 -0
  33. package/lib/client/pre-session/PreSessionWorktreeToggle.js.map +1 -0
  34. package/lib/client/pre-session/controller.d.ts +42 -0
  35. package/lib/client/pre-session/controller.js +149 -0
  36. package/lib/client/pre-session/controller.js.map +1 -0
  37. package/lib/client/pre-session/index.d.ts +15 -0
  38. package/lib/client/pre-session/index.js +16 -0
  39. package/lib/client/pre-session/index.js.map +1 -0
  40. package/lib/client/review-console/ReviewActions.d.ts +15 -0
  41. package/lib/client/review-console/ReviewActions.js +219 -0
  42. package/lib/client/review-console/ReviewActions.js.map +1 -0
  43. package/lib/client/review-console/WorktreeReviewPanel.d.ts +20 -0
  44. package/lib/client/review-console/WorktreeReviewPanel.js +75 -0
  45. package/lib/client/review-console/WorktreeReviewPanel.js.map +1 -0
  46. package/lib/client/review-console/index.d.ts +8 -0
  47. package/lib/client/review-console/index.js +22 -0
  48. package/lib/client/review-console/index.js.map +1 -0
  49. package/lib/client/review-console/review-console.styles.d.ts +1 -0
  50. package/lib/client/review-console/review-console.styles.js +81 -0
  51. package/lib/client/review-console/review-console.styles.js.map +1 -0
  52. package/lib/client/review-console/status-events.d.ts +3 -0
  53. package/lib/client/review-console/status-events.js +6 -0
  54. package/lib/client/review-console/status-events.js.map +1 -0
  55. package/lib/client/styles.d.ts +1 -0
  56. package/lib/client/styles.js +78 -0
  57. package/lib/client/styles.js.map +1 -0
  58. package/lib/client/target-console/TargetStatusAction.d.ts +7 -0
  59. package/lib/client/target-console/TargetStatusAction.js +46 -0
  60. package/lib/client/target-console/TargetStatusAction.js.map +1 -0
  61. package/lib/client/target-console/WorktreeConsoleView.d.ts +9 -0
  62. package/lib/client/target-console/WorktreeConsoleView.js +362 -0
  63. package/lib/client/target-console/WorktreeConsoleView.js.map +1 -0
  64. package/lib/client/target-console/WorktreeReviewStatus.d.ts +9 -0
  65. package/lib/client/target-console/WorktreeReviewStatus.js +91 -0
  66. package/lib/client/target-console/WorktreeReviewStatus.js.map +1 -0
  67. package/lib/client/target-console/index.d.ts +14 -0
  68. package/lib/client/target-console/index.js +23 -0
  69. package/lib/client/target-console/index.js.map +1 -0
  70. package/lib/client/target-console/target-console.styles.d.ts +1 -0
  71. package/lib/client/target-console/target-console.styles.js +130 -0
  72. package/lib/client/target-console/target-console.styles.js.map +1 -0
  73. package/lib/client.js +6813 -0
  74. package/lib/commands.d.ts +2 -4
  75. package/lib/commands.js +54 -28
  76. package/lib/commands.js.map +1 -1
  77. package/lib/console-contract.d.ts +236 -0
  78. package/lib/console-contract.js +86 -0
  79. package/lib/console-contract.js.map +1 -0
  80. package/lib/console-host/control-plane.d.ts +29 -0
  81. package/lib/console-host/control-plane.js +443 -0
  82. package/lib/console-host/control-plane.js.map +1 -0
  83. package/lib/console-host/errors.d.ts +6 -0
  84. package/lib/console-host/errors.js +31 -0
  85. package/lib/console-host/errors.js.map +1 -0
  86. package/lib/console-host/projection.d.ts +12 -0
  87. package/lib/console-host/projection.js +131 -0
  88. package/lib/console-host/projection.js.map +1 -0
  89. package/lib/console-host/review-diff.d.ts +18 -0
  90. package/lib/console-host/review-diff.js +176 -0
  91. package/lib/console-host/review-diff.js.map +1 -0
  92. package/lib/console-host/service.d.ts +24 -0
  93. package/lib/console-host/service.js +131 -0
  94. package/lib/console-host/service.js.map +1 -0
  95. package/lib/console-remote/descriptors.d.ts +3 -0
  96. package/lib/console-remote/descriptors.js +96 -0
  97. package/lib/console-remote/descriptors.js.map +1 -0
  98. package/lib/console-remote/remote.d.ts +27 -0
  99. package/lib/console-remote/remote.js +4 -0
  100. package/lib/console-remote/remote.js.map +1 -0
  101. package/lib/console-remote/schemas.d.ts +29 -0
  102. package/lib/console-remote/schemas.js +143 -0
  103. package/lib/console-remote/schemas.js.map +1 -0
  104. package/lib/console-remote/typert.d.ts +3 -0
  105. package/lib/console-remote/typert.js +14 -0
  106. package/lib/console-remote/typert.js.map +1 -0
  107. package/lib/index.d.ts +22 -6
  108. package/lib/index.js +28 -10
  109. package/lib/index.js.map +1 -1
  110. package/lib/managed-worktree-path.d.ts +14 -13
  111. package/lib/managed-worktree-path.js +32 -22
  112. package/lib/managed-worktree-path.js.map +1 -1
  113. package/lib/ports.d.ts +37 -1
  114. package/lib/session-checkout-apply.d.ts +54 -1
  115. package/lib/session-checkout-apply.js +318 -28
  116. package/lib/session-checkout-apply.js.map +1 -1
  117. package/lib/session-checkout-module.js +853 -33
  118. package/lib/session-checkout-module.js.map +1 -1
  119. package/lib/session-target-context.d.ts +6 -0
  120. package/lib/session-target-context.js +16 -0
  121. package/lib/session-target-context.js.map +1 -0
  122. package/lib/tools.d.ts +5 -6
  123. package/lib/tools.js +43 -233
  124. package/lib/tools.js.map +1 -1
  125. package/lib/types.d.ts +57 -11
  126. package/lib/types.js +6 -6
  127. package/lib/types.js.map +1 -1
  128. package/package.json +116 -69
  129. package/scripts/check-publish.mjs +193 -0
  130. package/scripts/clean-lib.mjs +7 -0
  131. package/scripts/dev-dsh-lib.mjs +453 -0
  132. package/scripts/dev-dsh.mjs +89 -0
  133. package/scripts/dsh-source-runner.mjs +14 -0
package/lib/ports.d.ts CHANGED
@@ -87,10 +87,11 @@ export interface ManagedCheckoutCreateJournal extends ManagedCheckoutJournalBase
87
87
  step: 'creating_worktree';
88
88
  }
89
89
  export interface ManagedCheckoutMutationJournal extends ManagedCheckoutJournalBase {
90
- operation: 'apply' | 'finish' | 'cleanup';
90
+ operation: 'apply' | 'preview' | 'rollback_preview' | 'finish' | 'finalize_preview' | 'cleanup';
91
91
  step: 'planning' | 'artifacts_retained' | 'writing_local' | 'updating_ref' | 'replacing_index' | 'removing_worktree';
92
92
  baseOid?: string;
93
93
  planRevision?: string;
94
+ previewId?: string;
94
95
  reviewId?: string;
95
96
  localFingerprint?: string;
96
97
  isolatedFingerprint?: string;
@@ -100,6 +101,8 @@ export interface ManagedCheckoutMutationJournal extends ManagedCheckoutJournalBa
100
101
  isolatedHeadOid?: string;
101
102
  commitOid?: string;
102
103
  retention?: WorktreeRetentionMode;
104
+ /** rollback_preview crash recovery must preserve whether the review returns to working or ready_for_review. */
105
+ resumeRevision?: boolean;
103
106
  changedFiles?: string[];
104
107
  managedDirectoryIdentity?: DirectoryIdentity;
105
108
  cleanupQuarantinePath?: string;
@@ -119,6 +122,26 @@ export interface ManagedWorktreeReviewRecord {
119
122
  isolatedFingerprint: string;
120
123
  isolatedHeadOid: string;
121
124
  }
125
+ export interface ManagedPreviewReceipt {
126
+ previewId: string;
127
+ reviewId: string;
128
+ iteration: number;
129
+ previewedAt: number;
130
+ configuredBaseOid: string;
131
+ effectiveBaseOid: string;
132
+ baseStrategy: ApplyBaseStrategy;
133
+ localHeadOid: string;
134
+ localHeadRef: string | null;
135
+ localFingerprintBefore: string;
136
+ localFingerprintPreview: string;
137
+ localWorkingTreeOid: string;
138
+ localIndexTreeOid: string;
139
+ previewWorkingTreeOid: string;
140
+ isolatedHeadOid: string;
141
+ isolatedFingerprint: string;
142
+ isolatedSnapshotOid: string;
143
+ changedFiles: string[];
144
+ }
122
145
  export interface ManagedDeliveryProof {
123
146
  localBranch: string | null;
124
147
  localHeadBefore: string;
@@ -131,6 +154,17 @@ export type ManagedCheckoutDelivery = {
131
154
  } | {
132
155
  state: 'ready_for_review';
133
156
  review: ManagedWorktreeReviewRecord;
157
+ } | {
158
+ state: 'preview_active';
159
+ review: ManagedWorktreeReviewRecord;
160
+ preview: ManagedPreviewReceipt;
161
+ } | {
162
+ state: 'preview_detached';
163
+ review: ManagedWorktreeReviewRecord;
164
+ preview: ManagedPreviewReceipt;
165
+ detachedAt: number;
166
+ reason: 'stale_local' | 'preview_modified';
167
+ attemptedAction: 'rollback_preview' | 'finalize_preview' | 'discard';
134
168
  } | {
135
169
  state: 'finalized';
136
170
  review: ManagedWorktreeReviewRecord;
@@ -165,6 +199,8 @@ export interface ManagedCheckoutRecord {
165
199
  projectId: string;
166
200
  projectName: string;
167
201
  ownerSessionId: string;
202
+ /** Local session that created this target; historical records may omit it. */
203
+ sourceSessionId?: string;
168
204
  /** Canonical root of the project in the user's Local Checkout. */
169
205
  localRoot: string;
170
206
  /** Canonical root of the project in the managed worktree; also the lease cwd. */
@@ -64,6 +64,37 @@ export interface FinishSuccessResult {
64
64
  nextBaseOid: string;
65
65
  }
66
66
  export type FinishResult = FinishSuccessResult | ApplyErrorResult;
67
+ export interface PreviewReceipt {
68
+ previewId: string;
69
+ reviewId: string;
70
+ iteration: number;
71
+ previewedAt: number;
72
+ configuredBaseOid: string;
73
+ effectiveBaseOid: string;
74
+ baseStrategy: ApplyBaseStrategy;
75
+ localHeadOid: string;
76
+ localHeadRef: string | null;
77
+ localFingerprintBefore: string;
78
+ localFingerprintPreview: string;
79
+ localWorkingTreeOid: string;
80
+ localIndexTreeOid: string;
81
+ previewWorkingTreeOid: string;
82
+ isolatedHeadOid: string;
83
+ isolatedFingerprint: string;
84
+ isolatedSnapshotOid: string;
85
+ changedFiles: string[];
86
+ }
87
+ export interface PreviewSuccessResult {
88
+ status: 'previewed';
89
+ receipt: PreviewReceipt;
90
+ changedFiles: string[];
91
+ }
92
+ export type PreviewResult = PreviewSuccessResult | ApplyErrorResult;
93
+ export interface RollbackSuccessResult {
94
+ status: 'preview_rolled_back';
95
+ changedFiles: string[];
96
+ }
97
+ export type RollbackResult = RollbackSuccessResult | ApplyErrorResult;
67
98
  export interface InvalidInputApplyError {
68
99
  code: 'invalid_input';
69
100
  message: string;
@@ -92,7 +123,11 @@ export interface OperationNotAllowedApplyError {
92
123
  code: 'operation_not_allowed';
93
124
  message: string;
94
125
  }
95
- export type ApplyError = InvalidInputApplyError | InvalidPlanApplyError | StaleLocalApplyError | StaleIsolatedApplyError | GitApplyError | CommitIsolationApplyError | OperationNotAllowedApplyError;
126
+ export interface PreviewModifiedApplyError {
127
+ code: 'preview_modified';
128
+ message: string;
129
+ }
130
+ export type ApplyError = InvalidInputApplyError | InvalidPlanApplyError | StaleLocalApplyError | StaleIsolatedApplyError | GitApplyError | CommitIsolationApplyError | OperationNotAllowedApplyError | PreviewModifiedApplyError;
96
131
  export interface SessionCheckoutApplyEngine {
97
132
  inspectReview(input: ApplyPlanInput): Promise<InspectReviewResult>;
98
133
  /** 复用真实 Apply merge 计算,但不持久化可执行 plan,也不修改任一 checkout。 */
@@ -102,6 +137,24 @@ export interface SessionCheckoutApplyEngine {
102
137
  finish(plan: ApplyPlan, options: {
103
138
  commitMessage: string;
104
139
  }): Promise<FinishResult>;
140
+ preview(plan: ApplyPlan, options: {
141
+ previewId: string;
142
+ reviewId: string;
143
+ iteration: number;
144
+ /** Local 写入前持久化 receipt artifacts/journal;失败时不得触碰 Local。 */
145
+ beforeWrite?(receipt: PreviewReceipt): Promise<void>;
146
+ }): Promise<PreviewResult>;
147
+ rollback(input: {
148
+ localPath: string;
149
+ receipt: PreviewReceipt;
150
+ }): Promise<RollbackResult>;
151
+ finalize(input: {
152
+ localPath: string;
153
+ receipt: PreviewReceipt;
154
+ commitMessage: string;
155
+ /** Commit object 已创建但 branch ref 尚未更新时持久化恢复信息。 */
156
+ beforeCommit?(commitOid: string): Promise<void>;
157
+ }): Promise<FinishResult>;
105
158
  }
106
159
  export interface SessionCheckoutApplyEngineOptions {
107
160
  /** 测试/宿主 seam:在最后一次 Local fingerprint 校验前执行。 */
@@ -1,18 +1,3 @@
1
- /**
2
- * dsh-git-worktree apply engine, ported from Domi's session-checkout-apply
3
- * with the Local preview surface removed (preview/rollback/finalize). The
4
- * remaining surface is the deterministic merge pipeline: plan (conflict
5
- * detection with ancestry-aware base selection), apply (patch Local with a
6
- * verified plan), and finish (commit the task delta onto Local HEAD while
7
- * preserving the user's own staged and working state). All merge computation
8
- * runs in temporary checkouts over shared object stores; only the final
9
- * verified patch touches the real Local worktree, and ref/index updates use
10
- * CAS so a crash leaves a recoverable journal.
11
- *
12
- * Ports note: `runGit` still spawns git directly (plugin code is trusted host
13
- * code, not sandboxed agent code); tests inject a real git in temp repos.
14
- * @module dsh-git-worktree/session-checkout-apply
15
- */
16
1
  import { spawn } from 'node:child_process';
17
2
  import { createHash, randomUUID } from 'node:crypto';
18
3
  import { realpathSync } from 'node:fs';
@@ -196,10 +181,10 @@ async function createSnapshotCommit(checkoutPath, treeOid, parentOid, objectDire
196
181
  const result = await runGit(checkoutPath, args, {
197
182
  env: {
198
183
  ...(objectDirectory && sourceObjects ? gitObjectEnvironment(objectDirectory, sourceObjects) : {}),
199
- GIT_AUTHOR_NAME: 'dsh-git-worktree Apply',
200
- GIT_AUTHOR_EMAIL: 'dsh-worktree-apply@localhost',
201
- GIT_COMMITTER_NAME: 'dsh-git-worktree Apply',
202
- GIT_COMMITTER_EMAIL: 'dsh-worktree-apply@localhost',
184
+ GIT_AUTHOR_NAME: 'Domi Apply',
185
+ GIT_AUTHOR_EMAIL: 'domi-apply@localhost',
186
+ GIT_COMMITTER_NAME: 'Domi Apply',
187
+ GIT_COMMITTER_EMAIL: 'domi-apply@localhost',
203
188
  },
204
189
  input: `${label}\n`,
205
190
  });
@@ -215,8 +200,8 @@ async function computeMerge(tempRoot, sourceObjects, objectDirectory, baseOid, l
215
200
  await runGit(mergePath, ['init', '--template=']);
216
201
  await runGit(mergePath, ['config', 'core.autocrlf', 'false']);
217
202
  await runGit(mergePath, ['config', 'core.hooksPath', hooksPath]);
218
- await runGit(mergePath, ['config', 'user.name', 'dsh-git-worktree Apply']);
219
- await runGit(mergePath, ['config', 'user.email', 'dsh-worktree-apply@localhost']);
203
+ await runGit(mergePath, ['config', 'user.name', 'Domi Apply']);
204
+ await runGit(mergePath, ['config', 'user.email', 'domi-apply@localhost']);
220
205
  const mergeEnv = {
221
206
  GIT_ALTERNATE_OBJECT_DIRECTORIES: [objectDirectory, sourceObjects].join(delimiter),
222
207
  };
@@ -278,8 +263,8 @@ async function computeTreeMerge(tempRoot, sourceObjects, objectDirectory, checko
278
263
  await runGit(mergePath, ['init', '--template=']);
279
264
  await runGit(mergePath, ['config', 'core.autocrlf', 'false']);
280
265
  await runGit(mergePath, ['config', 'core.hooksPath', hooksPath]);
281
- await runGit(mergePath, ['config', 'user.name', 'dsh-git-worktree Apply']);
282
- await runGit(mergePath, ['config', 'user.email', 'dsh-worktree-apply@localhost']);
266
+ await runGit(mergePath, ['config', 'user.name', 'Domi Apply']);
267
+ await runGit(mergePath, ['config', 'user.email', 'domi-apply@localhost']);
283
268
  const env = {
284
269
  GIT_ALTERNATE_OBJECT_DIRECTORIES: [objectDirectory, sourceObjects].join(delimiter),
285
270
  };
@@ -389,7 +374,7 @@ class DefaultSessionCheckoutApplyEngine {
389
374
  }
390
375
  let tempRoot = null;
391
376
  try {
392
- tempRoot = await mkdtemp(join(tmpdir(), 'dsh-wt-review-'));
377
+ tempRoot = await mkdtemp(join(tmpdir(), 'domi-review-snapshot-'));
393
378
  const objectDirectory = join(tempRoot, 'objects');
394
379
  await mkdir(objectDirectory, { recursive: true });
395
380
  const localGitRoot = await resolveGitRoot(input.localPath);
@@ -437,7 +422,7 @@ class DefaultSessionCheckoutApplyEngine {
437
422
  }
438
423
  let tempRoot = null;
439
424
  try {
440
- tempRoot = await mkdtemp(join(tmpdir(), 'dsh-wt-plan-'));
425
+ tempRoot = await mkdtemp(join(tmpdir(), 'domi-apply-plan-'));
441
426
  const objectDirectory = join(tempRoot, 'objects');
442
427
  await mkdir(objectDirectory, { recursive: true });
443
428
  const localGitRoot = await resolveGitRoot(input.localPath);
@@ -519,7 +504,7 @@ class DefaultSessionCheckoutApplyEngine {
519
504
  }
520
505
  let tempRoot = null;
521
506
  try {
522
- tempRoot = await mkdtemp(join(tmpdir(), 'dsh-wt-apply-'));
507
+ tempRoot = await mkdtemp(join(tmpdir(), 'domi-apply-check-'));
523
508
  const objectDirectory = join(tempRoot, 'objects');
524
509
  await mkdir(objectDirectory, { recursive: true });
525
510
  const sourceObjects = await sourceObjectDirectory(stored.scope.localGitRoot);
@@ -563,6 +548,311 @@ class DefaultSessionCheckoutApplyEngine {
563
548
  await this.cleanup(tempRoot);
564
549
  }
565
550
  }
551
+ async preview(plan, options) {
552
+ const stored = this.plans.get(plan.revision);
553
+ if (!stored || !planMatches(stored.plan, plan)) {
554
+ return { status: 'error', error: { code: 'invalid_plan', message: 'Preview plan 不存在、已使用或已被修改' } };
555
+ }
556
+ if (!options.previewId.trim() || !options.reviewId.trim() || !Number.isSafeInteger(options.iteration) || options.iteration < 1) {
557
+ return { status: 'error', error: { code: 'invalid_input', message: 'Preview identity 无效' } };
558
+ }
559
+ let tempRoot = null;
560
+ try {
561
+ tempRoot = await mkdtemp(join(tmpdir(), 'domi-preview-'));
562
+ const objectDirectory = join(tempRoot, 'objects');
563
+ await mkdir(objectDirectory, { recursive: true });
564
+ const sourceObjects = await sourceObjectDirectory(stored.scope.localGitRoot);
565
+ if (!pathsMatch(sourceObjects, stored.scope.sourceObjects)) {
566
+ return { status: 'error', error: { code: 'invalid_plan', message: 'Preview plan 的 Git 仓库身份已变化' } };
567
+ }
568
+ const local = await captureSnapshot(stored.scope.localGitRoot, join(tempRoot, 'local.index'), objectDirectory, sourceObjects);
569
+ if (local.headOid !== stored.plan.localHeadOid
570
+ || local.headRef !== stored.plan.localHeadRef
571
+ || local.fingerprint !== stored.plan.localFingerprint) {
572
+ return { status: 'error', error: { code: 'stale_local', message: 'Local 在 plan 后发生变化,请重新计算' } };
573
+ }
574
+ const isolated = await captureSnapshot(stored.scope.isolatedGitRoot, join(tempRoot, 'isolated.index'), objectDirectory, sourceObjects);
575
+ if (isolated.headOid !== stored.plan.isolatedHeadOid || isolated.fingerprint !== stored.plan.isolatedFingerprint) {
576
+ return { status: 'error', error: { code: 'stale_isolated', message: 'Isolated 在 plan 后发生变化,请重新计算' } };
577
+ }
578
+ const persistentLocal = await captureSnapshot(stored.scope.localGitRoot, join(tempRoot, 'persistent-local.index'), null, null);
579
+ const persistentIsolated = await captureSnapshot(stored.scope.isolatedGitRoot, join(tempRoot, 'persistent-isolated.index'), null, null);
580
+ if (persistentLocal.fingerprint !== stored.plan.localFingerprint
581
+ || persistentIsolated.fingerprint !== stored.plan.isolatedFingerprint) {
582
+ return { status: 'error', error: { code: 'invalid_plan', message: 'Preview 持久快照与审核 plan 不一致' } };
583
+ }
584
+ const previewWorkingTreeOid = await prepareIndexFromPatch(stored.scope.localGitRoot, join(tempRoot, 'preview-working.index'), persistentLocal.treeOid, stored.patch);
585
+ const isolatedSnapshotOid = await createSnapshotCommit(stored.scope.isolatedGitRoot, persistentIsolated.treeOid, stored.plan.effectiveBaseOid, null, null, 'Previewed Isolated snapshot');
586
+ const preparedReceipt = {
587
+ previewId: options.previewId,
588
+ reviewId: options.reviewId,
589
+ iteration: options.iteration,
590
+ previewedAt: Date.now(),
591
+ configuredBaseOid: stored.input.baseOid,
592
+ effectiveBaseOid: stored.plan.effectiveBaseOid,
593
+ baseStrategy: stored.plan.baseStrategy,
594
+ localHeadOid: persistentLocal.headOid,
595
+ localHeadRef: persistentLocal.headRef,
596
+ localFingerprintBefore: persistentLocal.fingerprint,
597
+ localFingerprintPreview: '',
598
+ localWorkingTreeOid: persistentLocal.treeOid,
599
+ localIndexTreeOid: persistentLocal.indexTreeOid,
600
+ previewWorkingTreeOid,
601
+ isolatedHeadOid: persistentIsolated.headOid,
602
+ isolatedFingerprint: persistentIsolated.fingerprint,
603
+ isolatedSnapshotOid,
604
+ changedFiles: [...stored.plan.changedFiles],
605
+ };
606
+ await this.options.beforeFinalLocalValidation?.();
607
+ const finalLocal = await captureSnapshot(stored.scope.localGitRoot, join(tempRoot, 'final-local.index'), objectDirectory, sourceObjects);
608
+ if (finalLocal.headOid !== stored.plan.localHeadOid || finalLocal.fingerprint !== stored.plan.localFingerprint) {
609
+ return { status: 'error', error: { code: 'stale_local', message: 'Local 在 Preview 写入前发生变化,请重新计算' } };
610
+ }
611
+ await options.beforeWrite?.(preparedReceipt);
612
+ if (stored.patch.length > 0) {
613
+ await runGit(stored.scope.localGitRoot, ['apply', '--binary', '--whitespace=nowarn'], { input: stored.patch });
614
+ }
615
+ const previewedLocal = await captureSnapshot(stored.scope.localGitRoot, join(tempRoot, 'previewed-local.index'), null, null);
616
+ if (previewedLocal.treeOid !== previewWorkingTreeOid) {
617
+ return {
618
+ status: 'error',
619
+ error: { code: 'git_error', message: 'Preview 写入后的 Local snapshot 与准备结果不一致,需要恢复确认' },
620
+ };
621
+ }
622
+ this.plans.delete(plan.revision);
623
+ const receipt = {
624
+ ...preparedReceipt,
625
+ localFingerprintPreview: previewedLocal.fingerprint,
626
+ };
627
+ return { status: 'previewed', receipt, changedFiles: [...receipt.changedFiles] };
628
+ }
629
+ catch (error) {
630
+ return { status: 'error', error: { code: 'git_error', message: this.errorMessage(error) } };
631
+ }
632
+ finally {
633
+ if (tempRoot)
634
+ await this.cleanup(tempRoot);
635
+ }
636
+ }
637
+ async rollback(input) {
638
+ let tempRoot = null;
639
+ try {
640
+ tempRoot = await mkdtemp(join(tmpdir(), 'domi-preview-rollback-'));
641
+ const localGitRoot = await resolveGitRoot(input.localPath);
642
+ const objectDirectory = join(tempRoot, 'objects');
643
+ await mkdir(objectDirectory, { recursive: true });
644
+ const sourceObjects = await sourceObjectDirectory(localGitRoot);
645
+ const current = await captureSnapshot(localGitRoot, join(tempRoot, 'current.index'), objectDirectory, sourceObjects);
646
+ if (current.headRef !== input.receipt.localHeadRef) {
647
+ return { status: 'error', error: { code: 'stale_local', message: 'Local branch 已变化,不能自动撤回 Preview' } };
648
+ }
649
+ if (current.headOid !== input.receipt.localHeadOid
650
+ && !await isAncestor(localGitRoot, input.receipt.localHeadOid, current.headOid)) {
651
+ return { status: 'error', error: { code: 'stale_local', message: 'Local HEAD 不是 Preview 基线的安全快进,不能自动撤回' } };
652
+ }
653
+ let rollbackBaselineTreeOid = input.receipt.localWorkingTreeOid;
654
+ let expectedPreviewTreeOid = input.receipt.previewWorkingTreeOid;
655
+ if (current.headOid !== input.receipt.localHeadOid) {
656
+ const receiptHeadTreeOid = stdoutText(await runGit(localGitRoot, ['rev-parse', `${input.receipt.localHeadOid}^{tree}`]));
657
+ const advancedBaseline = await computeTreeMerge(tempRoot, sourceObjects, objectDirectory, localGitRoot, 'preview-rollback-advanced-baseline', receiptHeadTreeOid, current.headTreeOid, input.receipt.localWorkingTreeOid);
658
+ if (advancedBaseline.status === 'conflict') {
659
+ return {
660
+ status: 'error',
661
+ error: {
662
+ code: 'preview_modified',
663
+ message: `Local 新提交与 Preview 前的本地修改冲突,无法安全撤回:${advancedBaseline.conflictingFiles.join('、')}`,
664
+ },
665
+ };
666
+ }
667
+ const previewAbsence = await computeTreeMerge(tempRoot, sourceObjects, objectDirectory, localGitRoot, 'preview-rollback-preview-absence', input.receipt.previewWorkingTreeOid, advancedBaseline.treeOid, input.receipt.localWorkingTreeOid);
668
+ if (previewAbsence.status === 'conflict') {
669
+ return {
670
+ status: 'error',
671
+ error: {
672
+ code: 'preview_modified',
673
+ message: `Local 新提交与 Preview 任务增量冲突,无法安全撤回:${previewAbsence.conflictingFiles.join('、')}`,
674
+ },
675
+ };
676
+ }
677
+ if (previewAbsence.treeOid !== advancedBaseline.treeOid) {
678
+ return {
679
+ status: 'error',
680
+ error: {
681
+ code: 'preview_modified',
682
+ message: 'Local 新提交已经包含部分或全部 Preview 增量,不能通过撤回工作区改动来改写已提交历史',
683
+ },
684
+ };
685
+ }
686
+ const advancedPreview = await computeTreeMerge(tempRoot, sourceObjects, objectDirectory, localGitRoot, 'preview-rollback-advanced-preview', input.receipt.localWorkingTreeOid, advancedBaseline.treeOid, input.receipt.previewWorkingTreeOid);
687
+ if (advancedPreview.status === 'conflict') {
688
+ return {
689
+ status: 'error',
690
+ error: {
691
+ code: 'preview_modified',
692
+ message: `Local 新提交与 Preview 任务增量冲突,无法安全撤回:${advancedPreview.conflictingFiles.join('、')}`,
693
+ },
694
+ };
695
+ }
696
+ rollbackBaselineTreeOid = advancedBaseline.treeOid;
697
+ expectedPreviewTreeOid = advancedPreview.treeOid;
698
+ }
699
+ const rollbackTree = await computeTreeMerge(tempRoot, sourceObjects, objectDirectory, localGitRoot, 'preview-rollback', expectedPreviewTreeOid, current.treeOid, rollbackBaselineTreeOid);
700
+ if (rollbackTree.status === 'conflict') {
701
+ return {
702
+ status: 'error',
703
+ error: {
704
+ code: 'preview_modified',
705
+ message: `Local 在 Preview 区域出现额外修改,无法安全撤回:${rollbackTree.conflictingFiles.join('、')}`,
706
+ },
707
+ };
708
+ }
709
+ const rollbackPatch = await treePatch(localGitRoot, current.treeOid, rollbackTree.treeOid, objectDirectory, sourceObjects);
710
+ await this.options.beforeFinalLocalValidation?.();
711
+ const finalLocal = await captureSnapshot(localGitRoot, join(tempRoot, 'final-local.index'), objectDirectory, sourceObjects);
712
+ if (finalLocal.fingerprint !== current.fingerprint || finalLocal.headOid !== current.headOid) {
713
+ return { status: 'error', error: { code: 'stale_local', message: 'Local 在撤回 Preview 前发生变化,请重试' } };
714
+ }
715
+ if (rollbackPatch.length > 0) {
716
+ await runGit(localGitRoot, ['apply', '--binary', '--whitespace=nowarn'], { input: rollbackPatch });
717
+ }
718
+ const rolledBack = await captureSnapshot(localGitRoot, join(tempRoot, 'rolled-back.index'), null, null);
719
+ if (rolledBack.headOid !== current.headOid
720
+ || rolledBack.headRef !== current.headRef
721
+ || rolledBack.indexTreeOid !== current.indexTreeOid
722
+ || rolledBack.treeOid !== rollbackTree.treeOid) {
723
+ return {
724
+ status: 'error',
725
+ error: { code: 'git_error', message: 'Preview 撤回后的 Local snapshot 与安全恢复结果不一致,需要保留现场确认' },
726
+ };
727
+ }
728
+ return { status: 'preview_rolled_back', changedFiles: [...input.receipt.changedFiles] };
729
+ }
730
+ catch (error) {
731
+ return { status: 'error', error: { code: 'git_error', message: this.errorMessage(error) } };
732
+ }
733
+ finally {
734
+ if (tempRoot)
735
+ await this.cleanup(tempRoot);
736
+ }
737
+ }
738
+ async finalize(input) {
739
+ const commitMessage = input.commitMessage.trim();
740
+ if (!commitMessage) {
741
+ return { status: 'error', error: { code: 'invalid_input', message: '提交信息不能为空' } };
742
+ }
743
+ if (!input.receipt.localHeadRef?.startsWith('refs/heads/')) {
744
+ return { status: 'error', error: { code: 'operation_not_allowed', message: 'Local 当前不是普通分支,不能自动创建任务提交' } };
745
+ }
746
+ let tempRoot = null;
747
+ let adjacentIndex = null;
748
+ try {
749
+ tempRoot = await mkdtemp(join(tmpdir(), 'domi-preview-finalize-'));
750
+ const localGitRoot = await resolveGitRoot(input.localPath);
751
+ const objectDirectory = join(tempRoot, 'objects');
752
+ await mkdir(objectDirectory, { recursive: true });
753
+ const sourceObjects = await sourceObjectDirectory(localGitRoot);
754
+ const current = await captureSnapshot(localGitRoot, join(tempRoot, 'current.index'), objectDirectory, sourceObjects);
755
+ if (current.headOid !== input.receipt.localHeadOid || current.headRef !== input.receipt.localHeadRef) {
756
+ return { status: 'error', error: { code: 'stale_local', message: 'Local branch/HEAD 已变化,不能完成 Preview 提交' } };
757
+ }
758
+ const previewRemoval = await computeTreeMerge(tempRoot, sourceObjects, objectDirectory, localGitRoot, 'preview-finalize-separation', input.receipt.previewWorkingTreeOid, current.treeOid, input.receipt.localWorkingTreeOid);
759
+ if (previewRemoval.status === 'conflict') {
760
+ return {
761
+ status: 'error',
762
+ error: {
763
+ code: 'preview_modified',
764
+ message: `Local 在 Preview 区域出现额外修改,无法可靠提交:${previewRemoval.conflictingFiles.join('、')}`,
765
+ },
766
+ };
767
+ }
768
+ if (input.receipt.changedFiles.length === 0) {
769
+ await this.options.beforeFinalLocalValidation?.();
770
+ const finalLocal = await captureSnapshot(localGitRoot, join(tempRoot, 'final-empty.index'), objectDirectory, sourceObjects);
771
+ if (finalLocal.fingerprint !== current.fingerprint) {
772
+ return { status: 'error', error: { code: 'stale_local', message: 'Local 在完成 Preview 前发生变化,请重试' } };
773
+ }
774
+ return {
775
+ status: 'finished',
776
+ changedFiles: [],
777
+ commitOid: null,
778
+ nextBaseOid: input.receipt.isolatedSnapshotOid,
779
+ };
780
+ }
781
+ const taskTree = await computeTreeMerge(tempRoot, sourceObjects, objectDirectory, localGitRoot, 'preview-task-isolation', input.receipt.localWorkingTreeOid, current.headTreeOid, input.receipt.previewWorkingTreeOid);
782
+ if (taskTree.status === 'conflict') {
783
+ return {
784
+ status: 'error',
785
+ error: {
786
+ code: 'commit_isolation_conflict',
787
+ message: `Preview 任务增量无法与 Local HEAD 可靠拆分:${taskTree.conflictingFiles.join('、')}`,
788
+ },
789
+ };
790
+ }
791
+ const finalIndexTree = await computeTreeMerge(tempRoot, sourceObjects, objectDirectory, localGitRoot, 'preview-index-preservation', current.headTreeOid, taskTree.treeOid, current.indexTreeOid);
792
+ if (finalIndexTree.status === 'conflict') {
793
+ return {
794
+ status: 'error',
795
+ error: {
796
+ code: 'commit_isolation_conflict',
797
+ message: `Preview 提交与 Local staged 修改无法可靠分离:${finalIndexTree.conflictingFiles.join('、')}`,
798
+ },
799
+ };
800
+ }
801
+ const taskPatch = await treePatch(localGitRoot, current.headTreeOid, taskTree.treeOid, objectDirectory, sourceObjects);
802
+ const finalIndexPatch = await treePatch(localGitRoot, taskTree.treeOid, finalIndexTree.treeOid, objectDirectory, sourceObjects);
803
+ const actualTaskTreeOid = await prepareIndexFromPatch(localGitRoot, join(tempRoot, 'task.index'), current.headOid, taskPatch);
804
+ const commitOid = await createUserCommit(localGitRoot, actualTaskTreeOid, current.headOid, commitMessage);
805
+ const finalIndexPath = join(tempRoot, 'final.index');
806
+ await prepareIndexFromPatch(localGitRoot, finalIndexPath, commitOid, finalIndexPatch);
807
+ await input.beforeCommit?.(commitOid);
808
+ await this.options.beforeFinalLocalValidation?.();
809
+ const finalLocal = await captureSnapshot(localGitRoot, join(tempRoot, 'final-local.index'), objectDirectory, sourceObjects);
810
+ if (finalLocal.fingerprint !== current.fingerprint || finalLocal.headOid !== current.headOid) {
811
+ return { status: 'error', error: { code: 'stale_local', message: 'Local 在 Preview 提交写入前发生变化,请重试' } };
812
+ }
813
+ const realIndexPath = await resolveIndexPath(localGitRoot);
814
+ adjacentIndex = `${realIndexPath}.domi-${randomUUID()}`;
815
+ await copyFile(finalIndexPath, adjacentIndex);
816
+ let refUpdated = false;
817
+ try {
818
+ await runGit(localGitRoot, ['update-ref', input.receipt.localHeadRef, commitOid, current.headOid]);
819
+ refUpdated = true;
820
+ await rename(adjacentIndex, realIndexPath);
821
+ adjacentIndex = null;
822
+ }
823
+ catch (error) {
824
+ if (refUpdated) {
825
+ try {
826
+ await runGit(localGitRoot, ['update-ref', input.receipt.localHeadRef, current.headOid, commitOid]);
827
+ }
828
+ catch (rollbackError) {
829
+ return {
830
+ status: 'error',
831
+ error: {
832
+ code: 'git_error',
833
+ message: `Preview 提交写入失败且 ref 无法回滚:${this.errorMessage(error)};${this.errorMessage(rollbackError)}`,
834
+ },
835
+ };
836
+ }
837
+ }
838
+ return { status: 'error', error: { code: 'git_error', message: `Preview 提交写入失败,已回滚:${this.errorMessage(error)}` } };
839
+ }
840
+ return {
841
+ status: 'finished',
842
+ changedFiles: [...input.receipt.changedFiles],
843
+ commitOid,
844
+ nextBaseOid: input.receipt.isolatedSnapshotOid,
845
+ };
846
+ }
847
+ catch (error) {
848
+ return { status: 'error', error: { code: 'git_error', message: this.errorMessage(error) } };
849
+ }
850
+ finally {
851
+ await removeBestEffort(adjacentIndex);
852
+ if (tempRoot)
853
+ await this.cleanup(tempRoot);
854
+ }
855
+ }
566
856
  async finish(plan, options) {
567
857
  const stored = this.plans.get(plan.revision);
568
858
  if (!stored || !planMatches(stored.plan, plan)) {
@@ -581,7 +871,7 @@ class DefaultSessionCheckoutApplyEngine {
581
871
  let tempRoot = null;
582
872
  let adjacentIndex = null;
583
873
  try {
584
- tempRoot = await mkdtemp(join(tmpdir(), 'dsh-wt-finish-'));
874
+ tempRoot = await mkdtemp(join(tmpdir(), 'domi-finish-'));
585
875
  const objectDirectory = join(tempRoot, 'objects');
586
876
  await mkdir(objectDirectory, { recursive: true });
587
877
  const sourceObjects = await sourceObjectDirectory(stored.scope.localGitRoot);
@@ -660,7 +950,7 @@ class DefaultSessionCheckoutApplyEngine {
660
950
  return { status: 'error', error: { code: 'stale_local', message: 'Local 在 Finish 写入前发生变化,请重新计算' } };
661
951
  }
662
952
  const realIndexPath = await resolveIndexPath(stored.scope.localGitRoot);
663
- adjacentIndex = `${realIndexPath}.dsh-wt-${randomUUID()}`;
953
+ adjacentIndex = `${realIndexPath}.domi-${randomUUID()}`;
664
954
  await copyFile(finalIndexPath, adjacentIndex);
665
955
  let worktreePatched = false;
666
956
  let refUpdated = false;