dsh-git-worktree 0.3.2 → 0.6.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.
- package/CHANGELOG.md +83 -0
- package/README.en.md +48 -18
- package/README.md +49 -18
- package/lib/adapters/git.js +8 -0
- package/lib/adapters/git.js.map +1 -1
- package/lib/adapters/registry.js +107 -2
- package/lib/adapters/registry.js.map +1 -1
- package/lib/checkpoint.d.ts +3 -0
- package/lib/checkpoint.js +29 -0
- package/lib/checkpoint.js.map +1 -0
- package/lib/client/WorktreeReviewRow.d.ts +1 -1
- package/lib/client/WorktreeReviewRow.js +26 -6
- package/lib/client/WorktreeReviewRow.js.map +1 -1
- package/lib/client/actions.d.ts +49 -4
- package/lib/client/actions.js +104 -0
- package/lib/client/actions.js.map +1 -1
- package/lib/client/console-remote/adapter.js +10 -3
- package/lib/client/console-remote/adapter.js.map +1 -1
- package/lib/client/review-console/DeliveryProof.d.ts +5 -0
- package/lib/client/review-console/DeliveryProof.js +32 -0
- package/lib/client/review-console/DeliveryProof.js.map +1 -0
- package/lib/client/review-console/PreflightStatus.d.ts +12 -0
- package/lib/client/review-console/PreflightStatus.js +31 -0
- package/lib/client/review-console/PreflightStatus.js.map +1 -0
- package/lib/client/review-console/ReviewActions.d.ts +4 -1
- package/lib/client/review-console/ReviewActions.js +657 -31
- package/lib/client/review-console/ReviewActions.js.map +1 -1
- package/lib/client/review-console/WorktreeReviewPanel.d.ts +3 -1
- package/lib/client/review-console/WorktreeReviewPanel.js +35 -5
- package/lib/client/review-console/WorktreeReviewPanel.js.map +1 -1
- package/lib/client/review-console/preflight-cache.d.ts +21 -0
- package/lib/client/review-console/preflight-cache.js +98 -0
- package/lib/client/review-console/preflight-cache.js.map +1 -0
- package/lib/client/review-console/preview-recovery-cache.d.ts +20 -0
- package/lib/client/review-console/preview-recovery-cache.js +103 -0
- package/lib/client/review-console/preview-recovery-cache.js.map +1 -0
- package/lib/client/review-console/recovery-continuation.d.ts +53 -0
- package/lib/client/review-console/recovery-continuation.js +483 -0
- package/lib/client/review-console/recovery-continuation.js.map +1 -0
- package/lib/client/review-console/review-console.styles.js +36 -4
- package/lib/client/review-console/review-console.styles.js.map +1 -1
- package/lib/client/target-console/TargetStatusAction.d.ts +4 -2
- package/lib/client/target-console/TargetStatusAction.js +196 -23
- package/lib/client/target-console/TargetStatusAction.js.map +1 -1
- package/lib/client/target-console/WorktreeConsoleView.d.ts +4 -2
- package/lib/client/target-console/WorktreeConsoleView.js +33 -73
- package/lib/client/target-console/WorktreeConsoleView.js.map +1 -1
- package/lib/client/target-console/WorktreeManagerModal.d.ts +13 -0
- package/lib/client/target-console/WorktreeManagerModal.js +8 -0
- package/lib/client/target-console/WorktreeManagerModal.js.map +1 -0
- package/lib/client/target-console/WorktreeReviewStatus.d.ts +3 -1
- package/lib/client/target-console/WorktreeReviewStatus.js +35 -15
- package/lib/client/target-console/WorktreeReviewStatus.js.map +1 -1
- package/lib/client/target-console/index.d.ts +2 -1
- package/lib/client/target-console/index.js +4 -3
- package/lib/client/target-console/index.js.map +1 -1
- package/lib/client/target-console/target-console.styles.js +22 -3
- package/lib/client/target-console/target-console.styles.js.map +1 -1
- package/lib/client.js +2976 -211
- package/lib/console-contract.d.ts +110 -1
- package/lib/console-contract.js.map +1 -1
- package/lib/console-host/control-plane.d.ts +6 -1
- package/lib/console-host/control-plane.js +363 -44
- package/lib/console-host/control-plane.js.map +1 -1
- package/lib/console-host/errors.d.ts +2 -2
- package/lib/console-host/errors.js +21 -5
- package/lib/console-host/errors.js.map +1 -1
- package/lib/console-host/projection.d.ts +5 -3
- package/lib/console-host/projection.js +103 -11
- package/lib/console-host/projection.js.map +1 -1
- package/lib/console-host/service.d.ts +10 -5
- package/lib/console-host/service.js +48 -6
- package/lib/console-host/service.js.map +1 -1
- package/lib/console-remote/descriptors.js +37 -1
- package/lib/console-remote/descriptors.js.map +1 -1
- package/lib/console-remote/remote.d.ts +10 -5
- package/lib/console-remote/remote.js.map +1 -1
- package/lib/console-remote/schemas.d.ts +103 -1
- package/lib/console-remote/schemas.js +166 -6
- package/lib/console-remote/schemas.js.map +1 -1
- package/lib/index.d.ts +13 -2
- package/lib/index.js.map +1 -1
- package/lib/ports.d.ts +79 -1
- package/lib/session-checkout-apply.d.ts +76 -0
- package/lib/session-checkout-apply.js +608 -143
- package/lib/session-checkout-apply.js.map +1 -1
- package/lib/session-checkout-module.js +775 -117
- package/lib/session-checkout-module.js.map +1 -1
- package/lib/types.d.ts +104 -3
- package/lib/types.js.map +1 -1
- package/package.json +14 -14
- package/scripts/check-publish.mjs +2 -2
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
1
2
|
import { realpathSync } from 'node:fs';
|
|
2
3
|
import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
3
4
|
import { SessionCheckoutError } from './index.js';
|
|
5
|
+
import { checkpointGenerationForRecord } from './checkpoint.js';
|
|
4
6
|
import { createManagedWorktreePathCandidates, createManagedWorktreeRepositoryKey, } from './managed-worktree-path.js';
|
|
5
7
|
const UNVERSIONED_OID = 'unversioned';
|
|
6
8
|
const UNVERSIONED_REF = 'WORKING_TREE';
|
|
@@ -140,6 +142,26 @@ export function createSessionCheckoutModule(dependencies) {
|
|
|
140
142
|
revision: binding.revision,
|
|
141
143
|
};
|
|
142
144
|
}
|
|
145
|
+
function projectPreviousReview(review) {
|
|
146
|
+
return {
|
|
147
|
+
reviewId: review.reviewId,
|
|
148
|
+
iteration: review.iteration,
|
|
149
|
+
summary: review.summary,
|
|
150
|
+
suggestedCommitMessage: review.suggestedCommitMessage,
|
|
151
|
+
changedFiles: review.changedFiles.slice(0, 50),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function projectCheckpoint(checkpoint) {
|
|
155
|
+
return {
|
|
156
|
+
checkpointId: checkpoint.checkpointId,
|
|
157
|
+
sequence: checkpoint.sequence,
|
|
158
|
+
reviewId: checkpoint.reviewId,
|
|
159
|
+
createdAt: checkpoint.createdAt,
|
|
160
|
+
summary: checkpoint.summary,
|
|
161
|
+
validationStatus: checkpoint.validationStatus,
|
|
162
|
+
changedFiles: [...checkpoint.changedFiles],
|
|
163
|
+
};
|
|
164
|
+
}
|
|
143
165
|
function projectDelivery(record, commitInLocalHistory = null) {
|
|
144
166
|
const delivery = record.delivery;
|
|
145
167
|
const projectProof = (proof) => proof
|
|
@@ -148,6 +170,8 @@ export function createSessionCheckoutModule(dependencies) {
|
|
|
148
170
|
localHeadBefore: proof.localHeadBefore,
|
|
149
171
|
localHeadAfter: proof.localHeadAfter,
|
|
150
172
|
changedFiles: [...proof.changedFiles],
|
|
173
|
+
...(proof.validationStatus === undefined ? {} : { validationStatus: proof.validationStatus }),
|
|
174
|
+
...(proof.validationSummary === undefined ? {} : { validationSummary: proof.validationSummary }),
|
|
151
175
|
commitInLocalHistory,
|
|
152
176
|
}
|
|
153
177
|
: undefined;
|
|
@@ -234,6 +258,8 @@ export function createSessionCheckoutModule(dependencies) {
|
|
|
234
258
|
dirty,
|
|
235
259
|
revision: record?.revision ?? binding.revision,
|
|
236
260
|
...(record ? { delivery: projectDelivery(record) } : {}),
|
|
261
|
+
...((record?.checkpoints?.length ?? 0) > 0 ? { checkpoints: record.checkpoints.map(projectCheckpoint) } : {}),
|
|
262
|
+
...(record && checkpointGenerationForRecord(record) ? { checkpointGeneration: checkpointGenerationForRecord(record) } : {}),
|
|
237
263
|
};
|
|
238
264
|
}
|
|
239
265
|
async function validateCommittedLocalCheckout(binding, record) {
|
|
@@ -320,6 +346,29 @@ export function createSessionCheckoutModule(dependencies) {
|
|
|
320
346
|
dependencies.registry.write(registry);
|
|
321
347
|
return recovered;
|
|
322
348
|
}
|
|
349
|
+
async function checkpointHeadInvariantHolds(record, managedRoot, headOid) {
|
|
350
|
+
const checkpoints = record.checkpoints ?? [];
|
|
351
|
+
if (checkpoints.length === 0)
|
|
352
|
+
return true;
|
|
353
|
+
const checkpointIds = new Set();
|
|
354
|
+
const reviewIds = new Set();
|
|
355
|
+
for (let index = 0; index < checkpoints.length; index += 1) {
|
|
356
|
+
const checkpoint = checkpoints[index];
|
|
357
|
+
if (checkpoint.sequence !== index + 1
|
|
358
|
+
|| checkpointIds.has(checkpoint.checkpointId)
|
|
359
|
+
|| reviewIds.has(checkpoint.reviewId))
|
|
360
|
+
return false;
|
|
361
|
+
checkpointIds.add(checkpoint.checkpointId);
|
|
362
|
+
reviewIds.add(checkpoint.reviewId);
|
|
363
|
+
if (await dependencies.git.readInternalArtifact(record.localRoot, record.checkoutId, `checkpoints/${checkpoint.checkpointId}`) !== checkpoint.commitOid)
|
|
364
|
+
return false;
|
|
365
|
+
if (!(await dependencies.git.isAncestor(managedRoot, checkpoint.parentOid, checkpoint.commitOid)))
|
|
366
|
+
return false;
|
|
367
|
+
if (index > 0 && !(await dependencies.git.isAncestor(managedRoot, checkpoints[index - 1].commitOid, checkpoint.parentOid)))
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
return dependencies.git.isAncestor(managedRoot, checkpoints[checkpoints.length - 1].commitOid, headOid);
|
|
371
|
+
}
|
|
323
372
|
async function validateManagedCheckoutDetailed(binding, record, requireCreateBase) {
|
|
324
373
|
if (binding.target.kind !== 'isolated'
|
|
325
374
|
|| binding.target.checkoutId !== record.checkoutId
|
|
@@ -354,7 +403,8 @@ export function createSessionCheckoutModule(dependencies) {
|
|
|
354
403
|
|| !pathsEqual(localSnapshot.commonDir, record.gitCommonDir)
|
|
355
404
|
|| !pathsEqual(resolve(snapshot.root, projectRelativePath), canonicalManagedRoot)
|
|
356
405
|
|| (!requireCreateBase && !pathsEqual(snapshot.gitDir, record.gitDir))
|
|
357
|
-
|| (requireCreateBase && snapshot.headOid !== record.baseOid)
|
|
406
|
+
|| (requireCreateBase && snapshot.headOid !== record.baseOid)
|
|
407
|
+
|| (!requireCreateBase && !(await checkpointHeadInvariantHolds(record, canonicalManagedRoot, snapshot.headOid))))
|
|
358
408
|
return { status: 'invalid' };
|
|
359
409
|
const status = await dependencies.git.status(canonicalManagedRoot);
|
|
360
410
|
return {
|
|
@@ -580,6 +630,8 @@ export function createSessionCheckoutModule(dependencies) {
|
|
|
580
630
|
dirty: status.dirty,
|
|
581
631
|
revision: record.revision,
|
|
582
632
|
delivery,
|
|
633
|
+
...((record.checkpoints?.length ?? 0) > 0 ? { checkpoints: record.checkpoints.map(projectCheckpoint) } : {}),
|
|
634
|
+
...(checkpointGenerationForRecord(record) ? { checkpointGeneration: checkpointGenerationForRecord(record) } : {}),
|
|
583
635
|
...(delivery?.state === 'ready_for_review'
|
|
584
636
|
? {
|
|
585
637
|
reviewSlot: activePreview ? 'waiting' : 'available',
|
|
@@ -649,6 +701,9 @@ export function createSessionCheckoutModule(dependencies) {
|
|
|
649
701
|
`Original Local boundary: ${record.localRoot}`,
|
|
650
702
|
'Make task changes only in the authoritative cwd; never write directly to the Original Local boundary.',
|
|
651
703
|
];
|
|
704
|
+
if ((record.checkpoints?.length ?? 0) > 0) {
|
|
705
|
+
lines.push(`Saved Worktree checkpoints: ${record.checkpoints.length}. They remain unpublished to Local.`, 'Final review and commit wording must summarize the cumulative diff from the original delivery base to the final Worktree snapshot. Do not manually reset, rewrite, or publish checkpoint commits.');
|
|
706
|
+
}
|
|
652
707
|
if (record.delivery.state === 'ready_for_review') {
|
|
653
708
|
lines.push('Delivery state: Ready for Review, not yet synchronized to Local. Continue ordinary discussion and answer questions without changing this Review. If the user requests new code or file changes, call worktree_resume_revision first, then continue the requested work in this same Session; do not ask the user to click a recovery control and do not synchronize Local. Until that tool succeeds, treat the Worktree as read-only. The user may still preview, directly finish, or discard through the Worktree acceptance UI.');
|
|
654
709
|
}
|
|
@@ -844,70 +899,96 @@ export function createSessionCheckoutModule(dependencies) {
|
|
|
844
899
|
}
|
|
845
900
|
if (current.phase === 'mutating'
|
|
846
901
|
&& (current.journal?.operation === 'finalize_preview' || current.journal?.operation === 'finish')
|
|
847
|
-
&& current.journal.step === 'updating_ref'
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
902
|
+
&& (current.journal.step === 'updating_ref' || current.journal.step === 'replacing_index')) {
|
|
903
|
+
// HEAD/ref alone cannot prove that the adjacent index replacement and working-tree preservation completed.
|
|
904
|
+
// Preserve delivery, receipt, retained artifacts and journal until an explicit recovery path can verify all facts.
|
|
905
|
+
record = {
|
|
906
|
+
...current,
|
|
907
|
+
phase: 'recovery_required',
|
|
908
|
+
revision: current.revision + 1,
|
|
909
|
+
};
|
|
910
|
+
registry.managedCheckouts[checkoutId] = record;
|
|
911
|
+
registry.revision += 1;
|
|
912
|
+
recoveryRequiredCheckoutIds.push(checkoutId);
|
|
913
|
+
changed = true;
|
|
914
|
+
}
|
|
915
|
+
else if ((current.phase === 'mutating' || current.phase === 'recovery_required')
|
|
916
|
+
&& current.journal?.operation === 'checkpoint'
|
|
917
|
+
&& current.delivery.state === 'ready_for_review') {
|
|
918
|
+
const journal = current.journal;
|
|
919
|
+
if (journal.step === 'planning') {
|
|
920
|
+
record = { ...current, phase: 'ready', journal: null, revision: current.revision + 1 };
|
|
921
|
+
}
|
|
922
|
+
else if (journal.step === 'updating_ref'
|
|
923
|
+
&& typeof journal.commitOid === 'string'
|
|
924
|
+
&& typeof journal.parentOid === 'string'
|
|
925
|
+
&& typeof journal.checkpointId === 'string'
|
|
926
|
+
&& typeof journal.checkpointSequence === 'number'
|
|
927
|
+
&& typeof journal.checkpointRequestId === 'string'
|
|
928
|
+
&& typeof journal.checkpointRequestedRevision === 'number'
|
|
929
|
+
&& typeof journal.recoveryGeneration === 'string'
|
|
930
|
+
&& typeof journal.checkpointMessage === 'string'
|
|
931
|
+
&& typeof journal.checkpointIndexTreeOid === 'string'
|
|
932
|
+
&& Array.isArray(journal.changedFiles)) {
|
|
933
|
+
const recovered = await dependencies.applyEngine.recoverCheckpoint({
|
|
934
|
+
isolatedPath: current.managedRoot,
|
|
935
|
+
commitOid: journal.commitOid,
|
|
936
|
+
parentOid: journal.parentOid,
|
|
937
|
+
expectedIndexTreeOid: journal.checkpointIndexTreeOid,
|
|
938
|
+
});
|
|
939
|
+
if (recovered.status === 'checkpoint_aborted') {
|
|
940
|
+
try {
|
|
941
|
+
await dependencies.git.releaseInternalArtifacts(current.localRoot, current.checkoutId, `checkpoints/${journal.checkpointId}`);
|
|
942
|
+
}
|
|
943
|
+
catch {
|
|
944
|
+
console.warn('[session-checkout] 清理未生效 Checkpoint ref 失败,已保守保留不可见引用');
|
|
877
945
|
}
|
|
878
|
-
:
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
946
|
+
record = { ...current, phase: 'ready', journal: null, revision: current.revision + 1 };
|
|
947
|
+
}
|
|
948
|
+
else if (recovered.status === 'checkpoint_recovered') {
|
|
949
|
+
const retained = await dependencies.git.readInternalArtifact(current.localRoot, current.checkoutId, `checkpoints/${journal.checkpointId}`);
|
|
950
|
+
if (retained !== journal.commitOid) {
|
|
951
|
+
record = { ...current, phase: 'recovery_required', revision: current.revision + 1 };
|
|
952
|
+
recoveryRequiredCheckoutIds.push(checkoutId);
|
|
953
|
+
}
|
|
954
|
+
else {
|
|
955
|
+
const review = current.delivery.review;
|
|
956
|
+
const checkpoint = {
|
|
957
|
+
checkpointId: journal.checkpointId,
|
|
958
|
+
sequence: journal.checkpointSequence,
|
|
959
|
+
reviewId: review.reviewId,
|
|
960
|
+
requestId: journal.checkpointRequestId,
|
|
961
|
+
requestedRevision: journal.checkpointRequestedRevision,
|
|
962
|
+
generation: journal.recoveryGeneration,
|
|
963
|
+
iteration: review.iteration,
|
|
964
|
+
createdAt: journal.startedAt,
|
|
965
|
+
commitOid: journal.commitOid,
|
|
966
|
+
parentOid: journal.parentOid,
|
|
967
|
+
summary: review.summary,
|
|
968
|
+
commitMessage: journal.checkpointMessage,
|
|
969
|
+
validationStatus: review.validationStatus,
|
|
970
|
+
changedFiles: [...journal.changedFiles],
|
|
971
|
+
};
|
|
972
|
+
record = {
|
|
973
|
+
...current,
|
|
974
|
+
phase: 'ready',
|
|
975
|
+
previousReview: projectPreviousReview(review),
|
|
976
|
+
checkpoints: [...(current.checkpoints ?? []), checkpoint],
|
|
977
|
+
delivery: { state: 'working', iteration: review.iteration },
|
|
978
|
+
recoveryContinuation: undefined,
|
|
979
|
+
journal: null,
|
|
980
|
+
revision: current.revision + 1,
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
else {
|
|
985
|
+
record = { ...current, phase: 'recovery_required', revision: current.revision + 1 };
|
|
986
|
+
recoveryRequiredCheckoutIds.push(checkoutId);
|
|
987
|
+
}
|
|
903
988
|
}
|
|
904
989
|
else {
|
|
905
|
-
record = {
|
|
906
|
-
|
|
907
|
-
phase: 'ready',
|
|
908
|
-
journal: null,
|
|
909
|
-
revision: current.revision + 1,
|
|
910
|
-
};
|
|
990
|
+
record = { ...current, phase: 'recovery_required', revision: current.revision + 1 };
|
|
991
|
+
recoveryRequiredCheckoutIds.push(checkoutId);
|
|
911
992
|
}
|
|
912
993
|
registry.managedCheckouts[checkoutId] = record;
|
|
913
994
|
registry.revision += 1;
|
|
@@ -1048,38 +1129,271 @@ export function createSessionCheckoutModule(dependencies) {
|
|
|
1048
1129
|
throw new SessionCheckoutError(snapshot.error.code, snapshot.error.message);
|
|
1049
1130
|
}
|
|
1050
1131
|
const reviewId = dependencies.createCheckoutId();
|
|
1051
|
-
updateManagedCheckout(record.checkoutId, (current) =>
|
|
1052
|
-
...current
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1132
|
+
updateManagedCheckout(record.checkoutId, (current) => {
|
|
1133
|
+
const { recoveryContinuation: _recovery, ...withoutRecovery } = current;
|
|
1134
|
+
return {
|
|
1135
|
+
...withoutRecovery,
|
|
1136
|
+
...(current.delivery.state !== 'working' && current.delivery.state !== 'delivered'
|
|
1137
|
+
? { previousReview: projectPreviousReview(current.delivery.review) }
|
|
1138
|
+
: {}),
|
|
1139
|
+
delivery: {
|
|
1140
|
+
state: 'ready_for_review',
|
|
1141
|
+
review: {
|
|
1142
|
+
reviewId,
|
|
1143
|
+
iteration: current.delivery.state === 'working'
|
|
1144
|
+
? current.delivery.iteration
|
|
1145
|
+
: current.delivery.state === 'delivered'
|
|
1146
|
+
? current.delivery.iteration + 1
|
|
1147
|
+
: current.delivery.review.iteration,
|
|
1148
|
+
preparedAt: Date.now(),
|
|
1149
|
+
...(input.detailsMarkdown?.trim() ? { detailsMarkdown: input.detailsMarkdown.trim() } : {}),
|
|
1150
|
+
summary,
|
|
1151
|
+
validationStatus: input.validationStatus,
|
|
1152
|
+
...(input.validationSummary?.trim() ? { validationSummary: input.validationSummary.trim() } : {}),
|
|
1153
|
+
tests: input.tests.map((test) => ({
|
|
1154
|
+
command: test.command.trim(),
|
|
1155
|
+
status: test.status,
|
|
1156
|
+
...(test.summary?.trim() ? { summary: test.summary.trim() } : {}),
|
|
1157
|
+
})),
|
|
1158
|
+
changedFiles: [...snapshot.changedFiles],
|
|
1159
|
+
suggestedCommitMessage,
|
|
1160
|
+
isolatedFingerprint: snapshot.isolatedFingerprint,
|
|
1161
|
+
isolatedHeadOid: snapshot.isolatedHeadOid,
|
|
1162
|
+
},
|
|
1076
1163
|
},
|
|
1164
|
+
revision: current.revision + 1,
|
|
1165
|
+
};
|
|
1166
|
+
});
|
|
1167
|
+
return inspectIsolated(binding);
|
|
1168
|
+
}
|
|
1169
|
+
async function operateCheckpoint(input, binding) {
|
|
1170
|
+
if (binding.ownerSessionId !== input.sessionId || binding.target.kind !== 'isolated') {
|
|
1171
|
+
return operationError('not_owner', '只有 owner Isolated 会话可以保存阶段');
|
|
1172
|
+
}
|
|
1173
|
+
const commitMessage = input.commitMessage.trim();
|
|
1174
|
+
if (!commitMessage
|
|
1175
|
+
|| commitMessage.length > 500
|
|
1176
|
+
|| !input.expectedReviewId.trim()
|
|
1177
|
+
|| !/^[0-9a-f]{64}$/u.test(input.expectedGeneration)
|
|
1178
|
+
|| !input.requestId.trim()
|
|
1179
|
+
|| input.requestId.length > 200
|
|
1180
|
+
|| /[\0\r\n]/u.test(input.requestId))
|
|
1181
|
+
return operationError('invalid_input', 'Checkpoint 请求或 Commit Message 无效');
|
|
1182
|
+
let record = dependencies.registry.read().managedCheckouts[binding.target.checkoutId];
|
|
1183
|
+
if (!record)
|
|
1184
|
+
return operationError('checkout_missing', 'Isolated Checkout 记录不存在');
|
|
1185
|
+
const previousRequest = record.checkpoints?.find(checkpoint => checkpoint.requestId === input.requestId);
|
|
1186
|
+
if (previousRequest) {
|
|
1187
|
+
const exactReplay = record.delivery.state === 'working'
|
|
1188
|
+
&& record.checkpoints?.at(-1)?.checkpointId === previousRequest.checkpointId
|
|
1189
|
+
&& previousRequest.reviewId === input.expectedReviewId
|
|
1190
|
+
&& previousRequest.requestedRevision === input.expectedRevision
|
|
1191
|
+
&& previousRequest.generation === input.expectedGeneration
|
|
1192
|
+
&& previousRequest.commitMessage === commitMessage;
|
|
1193
|
+
if (!exactReplay)
|
|
1194
|
+
return operationError('stale_target', 'Checkpoint requestId 已被其他状态使用,请刷新');
|
|
1195
|
+
return {
|
|
1196
|
+
status: 'checkpointed',
|
|
1197
|
+
target: await inspectIsolated(binding),
|
|
1198
|
+
checkpoint: projectCheckpoint(previousRequest),
|
|
1199
|
+
changedFiles: [...previousRequest.changedFiles],
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
1202
|
+
if (record.revision !== input.expectedRevision) {
|
|
1203
|
+
return operationError('stale_target', 'Session Target 已变化,请刷新后重试', await inspectIsolated(binding));
|
|
1204
|
+
}
|
|
1205
|
+
if (record.phase !== 'ready' || (record.delivery.state !== 'ready_for_review' && record.delivery.state !== 'preview_active')) {
|
|
1206
|
+
return operationError('operation_not_allowed', `当前 ${record.phase}/${record.delivery.state} 状态不能保存阶段`, await inspectIsolated(binding));
|
|
1207
|
+
}
|
|
1208
|
+
if (record.delivery.review.reviewId !== input.expectedReviewId || checkpointGenerationForRecord(record) !== input.expectedGeneration) {
|
|
1209
|
+
return operationError('stale_target', 'Checkpoint Review 或 generation 已变化,请刷新后重试', await inspectIsolated(binding));
|
|
1210
|
+
}
|
|
1211
|
+
const holder = findProjectAcceptanceHolder(record);
|
|
1212
|
+
if (holder) {
|
|
1213
|
+
return operationError('project_acceptance_busy', '另一个任务正在占用该项目的 Local 验收槽位', await inspectIsolated(binding));
|
|
1214
|
+
}
|
|
1215
|
+
const inspected = await inspectIsolated(binding);
|
|
1216
|
+
if (inspected.checkout.phase !== 'ready') {
|
|
1217
|
+
return operationError('recovery_required', 'Isolated Checkout 身份无法确认,需要恢复', inspected);
|
|
1218
|
+
}
|
|
1219
|
+
record = dependencies.registry.read().managedCheckouts[record.checkoutId];
|
|
1220
|
+
if (!record
|
|
1221
|
+
|| record.revision !== input.expectedRevision
|
|
1222
|
+
|| record.phase !== 'ready'
|
|
1223
|
+
|| (record.delivery.state !== 'ready_for_review' && record.delivery.state !== 'preview_active')
|
|
1224
|
+
|| record.delivery.review.reviewId !== input.expectedReviewId
|
|
1225
|
+
|| checkpointGenerationForRecord(record) !== input.expectedGeneration)
|
|
1226
|
+
return operationError('stale_target', 'Checkpoint 状态在 Host CAS 前发生变化,请刷新后重试');
|
|
1227
|
+
if (record.delivery.state === 'preview_active') {
|
|
1228
|
+
const { preview, review } = record.delivery;
|
|
1229
|
+
const rollbackOperationId = dependencies.createCheckoutId();
|
|
1230
|
+
updateManagedCheckout(record.checkoutId, (current) => ({
|
|
1231
|
+
...current,
|
|
1232
|
+
phase: 'mutating',
|
|
1233
|
+
journal: {
|
|
1234
|
+
operation: 'rollback_preview',
|
|
1235
|
+
operationId: rollbackOperationId,
|
|
1236
|
+
step: 'planning',
|
|
1237
|
+
startedAt: Date.now(),
|
|
1238
|
+
previewId: preview.previewId,
|
|
1239
|
+
reviewId: review.reviewId,
|
|
1240
|
+
resumeRevision: false,
|
|
1241
|
+
},
|
|
1242
|
+
revision: current.revision + 1,
|
|
1243
|
+
}));
|
|
1244
|
+
const rollback = await dependencies.applyEngine.rollback({
|
|
1245
|
+
localPath: record.localRoot,
|
|
1246
|
+
receipt: preview,
|
|
1247
|
+
beforeWrite: async () => {
|
|
1248
|
+
updateManagedCheckout(record.checkoutId, (current) => ({
|
|
1249
|
+
...current,
|
|
1250
|
+
journal: current.journal?.operation === 'rollback_preview'
|
|
1251
|
+
? { ...current.journal, step: 'writing_local' }
|
|
1252
|
+
: current.journal,
|
|
1253
|
+
revision: current.revision + 1,
|
|
1254
|
+
}));
|
|
1255
|
+
},
|
|
1256
|
+
});
|
|
1257
|
+
if (rollback.status === 'error') {
|
|
1258
|
+
const current = dependencies.registry.read().managedCheckouts[record.checkoutId];
|
|
1259
|
+
const writeStarted = current?.journal?.operation === 'rollback_preview' && current.journal.step === 'writing_local';
|
|
1260
|
+
if (rollback.error.code === 'stale_local' || rollback.error.code === 'preview_modified') {
|
|
1261
|
+
return detachPreviewAfterLocalDrift(record, binding, rollback.error.code, 'rollback_preview');
|
|
1262
|
+
}
|
|
1263
|
+
if (!writeStarted || (rollback.error.code === 'git_error' && rollback.error.recoveryState === 'unchanged')) {
|
|
1264
|
+
updateManagedCheckout(record.checkoutId, (currentRecord) => ({ ...currentRecord, phase: 'ready', journal: null, revision: currentRecord.revision + 1 }));
|
|
1265
|
+
}
|
|
1266
|
+
else {
|
|
1267
|
+
updateManagedCheckout(record.checkoutId, (currentRecord) => ({ ...currentRecord, phase: 'recovery_required', revision: currentRecord.revision + 1 }));
|
|
1268
|
+
}
|
|
1269
|
+
return operationError(rollback.error.code, rollback.error.message, await inspectIsolated(binding));
|
|
1270
|
+
}
|
|
1271
|
+
const restored = updateManagedCheckout(record.checkoutId, (current) => ({
|
|
1272
|
+
...current,
|
|
1273
|
+
phase: 'ready',
|
|
1274
|
+
delivery: { state: 'ready_for_review', review },
|
|
1275
|
+
journal: null,
|
|
1276
|
+
revision: current.revision + 1,
|
|
1277
|
+
}));
|
|
1278
|
+
if (!restored)
|
|
1279
|
+
return operationError('checkout_missing', 'Preview 已撤回,但 Checkout 记录丢失');
|
|
1280
|
+
await releasePreviewArtifactsBestEffort(record, preview.previewId);
|
|
1281
|
+
record = restored;
|
|
1282
|
+
}
|
|
1283
|
+
if (record.delivery.state !== 'ready_for_review') {
|
|
1284
|
+
return operationError('operation_not_allowed', '当前没有可保存的验收阶段', await inspectIsolated(binding));
|
|
1285
|
+
}
|
|
1286
|
+
const review = record.delivery.review;
|
|
1287
|
+
const checkpointId = dependencies.createCheckoutId();
|
|
1288
|
+
const checkpointSequence = (record.checkpoints?.length ?? 0) + 1;
|
|
1289
|
+
const operationId = dependencies.createCheckoutId();
|
|
1290
|
+
const startedAt = Date.now();
|
|
1291
|
+
const mutating = updateManagedCheckout(record.checkoutId, (current) => ({
|
|
1292
|
+
...current,
|
|
1293
|
+
phase: 'mutating',
|
|
1294
|
+
journal: {
|
|
1295
|
+
operation: 'checkpoint',
|
|
1296
|
+
operationId,
|
|
1297
|
+
step: 'planning',
|
|
1298
|
+
startedAt,
|
|
1299
|
+
reviewId: review.reviewId,
|
|
1300
|
+
checkpointId,
|
|
1301
|
+
checkpointSequence,
|
|
1302
|
+
checkpointRequestId: input.requestId,
|
|
1303
|
+
checkpointRequestedRevision: input.expectedRevision,
|
|
1304
|
+
checkpointMessage: commitMessage,
|
|
1305
|
+
recoveryGeneration: input.expectedGeneration,
|
|
1306
|
+
isolatedFingerprint: review.isolatedFingerprint,
|
|
1307
|
+
isolatedHeadOid: review.isolatedHeadOid,
|
|
1077
1308
|
},
|
|
1078
1309
|
revision: current.revision + 1,
|
|
1079
1310
|
}));
|
|
1080
|
-
|
|
1311
|
+
if (!mutating)
|
|
1312
|
+
return operationError('checkout_missing', 'Isolated Checkout 记录不存在');
|
|
1313
|
+
const result = await dependencies.applyEngine.checkpoint({
|
|
1314
|
+
isolatedPath: mutating.managedRoot,
|
|
1315
|
+
expectedFingerprint: review.isolatedFingerprint,
|
|
1316
|
+
expectedHeadOid: review.isolatedHeadOid,
|
|
1317
|
+
commitMessage,
|
|
1318
|
+
beforeCommit: async (prepared) => {
|
|
1319
|
+
const journaled = updateManagedCheckout(mutating.checkoutId, (current) => ({
|
|
1320
|
+
...current,
|
|
1321
|
+
journal: current.journal?.operation === 'checkpoint'
|
|
1322
|
+
? {
|
|
1323
|
+
...current.journal,
|
|
1324
|
+
step: 'updating_ref',
|
|
1325
|
+
commitOid: prepared.commitOid,
|
|
1326
|
+
parentOid: prepared.parentOid,
|
|
1327
|
+
checkpointIndexTreeOid: prepared.indexTreeOid,
|
|
1328
|
+
changedFiles: [...prepared.changedFiles],
|
|
1329
|
+
}
|
|
1330
|
+
: current.journal,
|
|
1331
|
+
revision: current.revision + 1,
|
|
1332
|
+
}));
|
|
1333
|
+
if (!journaled?.journal || journaled.journal.operation !== 'checkpoint' || journaled.journal.commitOid !== prepared.commitOid) {
|
|
1334
|
+
throw new SessionCheckoutError('stale_target', 'Checkpoint journal 在保留内部 ref 前发生变化');
|
|
1335
|
+
}
|
|
1336
|
+
await dependencies.git.retainInternalArtifact(mutating.localRoot, mutating.checkoutId, `checkpoints/${checkpointId}`, prepared.commitOid);
|
|
1337
|
+
},
|
|
1338
|
+
});
|
|
1339
|
+
if (result.status === 'error') {
|
|
1340
|
+
const current = dependencies.registry.read().managedCheckouts[mutating.checkoutId];
|
|
1341
|
+
const commitPrepared = current?.journal?.operation === 'checkpoint' && typeof current.journal.commitOid === 'string';
|
|
1342
|
+
if (!commitPrepared) {
|
|
1343
|
+
updateManagedCheckout(mutating.checkoutId, (checkout) => ({
|
|
1344
|
+
...checkout,
|
|
1345
|
+
phase: 'ready',
|
|
1346
|
+
...(result.error.code === 'stale_isolated' ? { previousReview: projectPreviousReview(review) } : {}),
|
|
1347
|
+
delivery: result.error.code === 'stale_isolated'
|
|
1348
|
+
? { state: 'working', iteration: review.iteration }
|
|
1349
|
+
: { state: 'ready_for_review', review },
|
|
1350
|
+
journal: null,
|
|
1351
|
+
revision: checkout.revision + 1,
|
|
1352
|
+
}));
|
|
1353
|
+
}
|
|
1354
|
+
else {
|
|
1355
|
+
updateManagedCheckout(mutating.checkoutId, (checkout) => ({ ...checkout, phase: 'recovery_required', revision: checkout.revision + 1 }));
|
|
1356
|
+
}
|
|
1357
|
+
return operationError(result.error.code, result.error.message, await inspectIsolated(binding, false));
|
|
1358
|
+
}
|
|
1359
|
+
const checkpoint = {
|
|
1360
|
+
checkpointId,
|
|
1361
|
+
sequence: checkpointSequence,
|
|
1362
|
+
reviewId: review.reviewId,
|
|
1363
|
+
requestId: input.requestId,
|
|
1364
|
+
requestedRevision: input.expectedRevision,
|
|
1365
|
+
generation: input.expectedGeneration,
|
|
1366
|
+
iteration: review.iteration,
|
|
1367
|
+
createdAt: startedAt,
|
|
1368
|
+
commitOid: result.commitOid,
|
|
1369
|
+
parentOid: result.parentOid,
|
|
1370
|
+
summary: review.summary,
|
|
1371
|
+
commitMessage,
|
|
1372
|
+
validationStatus: review.validationStatus,
|
|
1373
|
+
changedFiles: [...result.changedFiles],
|
|
1374
|
+
};
|
|
1375
|
+
const completed = updateManagedCheckout(mutating.checkoutId, (current) => {
|
|
1376
|
+
const { recoveryContinuation: _continuation, ...withoutContinuation } = current;
|
|
1377
|
+
return {
|
|
1378
|
+
...withoutContinuation,
|
|
1379
|
+
phase: 'ready',
|
|
1380
|
+
previousReview: projectPreviousReview(review),
|
|
1381
|
+
checkpoints: [...(current.checkpoints ?? []), checkpoint],
|
|
1382
|
+
delivery: { state: 'working', iteration: review.iteration },
|
|
1383
|
+
journal: null,
|
|
1384
|
+
revision: current.revision + 1,
|
|
1385
|
+
};
|
|
1386
|
+
});
|
|
1387
|
+
if (!completed)
|
|
1388
|
+
return operationError('checkout_missing', 'Checkpoint 已创建,但 Checkout 记录丢失');
|
|
1389
|
+
return {
|
|
1390
|
+
status: 'checkpointed',
|
|
1391
|
+
target: await inspectIsolated(binding),
|
|
1392
|
+
checkpoint: projectCheckpoint(checkpoint),
|
|
1393
|
+
changedFiles: [...checkpoint.changedFiles],
|
|
1394
|
+
};
|
|
1081
1395
|
}
|
|
1082
|
-
async function resumeRevisionTarget(sessionId, expectedRevision, expectedReviewId) {
|
|
1396
|
+
async function resumeRevisionTarget(sessionId, expectedRevision, expectedReviewId, recovery) {
|
|
1083
1397
|
const binding = await resolveBinding(sessionId);
|
|
1084
1398
|
if (binding.ownerSessionId !== sessionId || binding.target.kind !== 'isolated') {
|
|
1085
1399
|
throw new SessionCheckoutError('not_owner', '只有 owner Isolated 会话可以恢复编辑');
|
|
@@ -1108,14 +1422,131 @@ export function createSessionCheckoutModule(dependencies) {
|
|
|
1108
1422
|
|| record.delivery.review.reviewId !== expectedReviewId) {
|
|
1109
1423
|
throw new SessionCheckoutError('stale_target', '验收状态在恢复编辑前发生变化,请刷新后重试');
|
|
1110
1424
|
}
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1425
|
+
if (recovery) {
|
|
1426
|
+
const preflight = await preflightTarget(sessionId, expectedRevision);
|
|
1427
|
+
const sameFiles = preflight.status === 'conflict'
|
|
1428
|
+
&& preflight.conflictingFiles.length === recovery.conflictingFiles.length
|
|
1429
|
+
&& preflight.conflictingFiles.every((file, index) => file === recovery.conflictingFiles[index]);
|
|
1430
|
+
if (recovery.kind !== 'worktree_apply_conflict'
|
|
1431
|
+
|| recovery.reviewId !== expectedReviewId
|
|
1432
|
+
|| recovery.readyRevision !== expectedRevision
|
|
1433
|
+
|| preflight.status !== 'conflict'
|
|
1434
|
+
|| preflight.localHeadOid !== recovery.localHeadOid
|
|
1435
|
+
|| !sameFiles)
|
|
1436
|
+
throw new SessionCheckoutError('stale_target', '冲突恢复身份在 Host CAS 前已变化,请重新预检');
|
|
1437
|
+
record = dependencies.registry.read().managedCheckouts[record.checkoutId];
|
|
1438
|
+
if (!record
|
|
1439
|
+
|| record.revision !== expectedRevision
|
|
1440
|
+
|| record.delivery.state !== 'ready_for_review'
|
|
1441
|
+
|| record.delivery.review.reviewId !== expectedReviewId)
|
|
1442
|
+
throw new SessionCheckoutError('stale_target', '验收状态在冲突恢复前发生变化,请刷新后重试');
|
|
1443
|
+
}
|
|
1444
|
+
const reviewBeforeResume = record.delivery.review;
|
|
1445
|
+
const iteration = reviewBeforeResume.iteration;
|
|
1446
|
+
updateManagedCheckout(record.checkoutId, (current) => {
|
|
1447
|
+
const { recoveryContinuation: _previousRecovery, ...withoutRecovery } = current;
|
|
1448
|
+
return {
|
|
1449
|
+
...withoutRecovery,
|
|
1450
|
+
previousReview: projectPreviousReview(reviewBeforeResume),
|
|
1451
|
+
...(recovery ? {
|
|
1452
|
+
recoveryContinuation: { ...recovery, workingRevision: current.revision + 1 },
|
|
1453
|
+
} : {}),
|
|
1454
|
+
delivery: { state: 'working', iteration },
|
|
1455
|
+
revision: current.revision + 1,
|
|
1456
|
+
};
|
|
1457
|
+
});
|
|
1117
1458
|
return inspectIsolated(binding);
|
|
1118
1459
|
}
|
|
1460
|
+
async function prepareReviewRegenerationTarget(sessionId, expectedRevision, expectedReviewId, requestId) {
|
|
1461
|
+
const binding = await resolveBinding(sessionId);
|
|
1462
|
+
if (binding.ownerSessionId !== sessionId || binding.target.kind !== 'isolated') {
|
|
1463
|
+
throw new SessionCheckoutError('not_owner', '只有 owner Isolated 会话可以请求重新生成验收结果');
|
|
1464
|
+
}
|
|
1465
|
+
const preflight = await preflightTarget(sessionId, expectedRevision);
|
|
1466
|
+
if (preflight.status !== 'blocked'
|
|
1467
|
+
|| preflight.reason !== 'stale_isolated'
|
|
1468
|
+
|| preflight.checkoutId !== binding.target.checkoutId
|
|
1469
|
+
|| preflight.reviewId !== expectedReviewId
|
|
1470
|
+
|| preflight.revision !== expectedRevision)
|
|
1471
|
+
throw new SessionCheckoutError('stale_target', '只读验收再生成身份在 Host 授权前已变化');
|
|
1472
|
+
const continuation = {
|
|
1473
|
+
kind: 'worktree_review_regeneration', requestId, reviewId: expectedReviewId, revision: expectedRevision,
|
|
1474
|
+
};
|
|
1475
|
+
const updated = updateManagedCheckout(binding.target.checkoutId, (current) => {
|
|
1476
|
+
if (current.revision !== expectedRevision
|
|
1477
|
+
|| current.delivery.state !== 'ready_for_review'
|
|
1478
|
+
|| current.delivery.review.reviewId !== expectedReviewId)
|
|
1479
|
+
throw new SessionCheckoutError('stale_target', 'Ready Review 在只读授权前发生变化');
|
|
1480
|
+
return { ...current, recoveryContinuation: continuation };
|
|
1481
|
+
});
|
|
1482
|
+
if (!updated)
|
|
1483
|
+
throw new SessionCheckoutError('checkout_missing', 'Isolated Checkout 记录不存在');
|
|
1484
|
+
return continuation;
|
|
1485
|
+
}
|
|
1486
|
+
async function preparePreviewRecoveryAnalysisTarget(sessionId, proof, requestId) {
|
|
1487
|
+
const recovery = await preflightPreviewRecoveryTarget(sessionId, proof.revision, proof.reviewId, proof.previewId);
|
|
1488
|
+
if (recovery.status !== 'assessed' || !recoveryProofMatches(proof, recovery.proof)) {
|
|
1489
|
+
throw new SessionCheckoutError('stale_target', 'Detached Preview Recovery proof 已变化,请重新检查');
|
|
1490
|
+
}
|
|
1491
|
+
const continuation = {
|
|
1492
|
+
kind: 'worktree_preview_recovery_analysis',
|
|
1493
|
+
requestId,
|
|
1494
|
+
reviewId: proof.reviewId,
|
|
1495
|
+
previewId: proof.previewId,
|
|
1496
|
+
revision: proof.revision,
|
|
1497
|
+
generation: proof.generation,
|
|
1498
|
+
};
|
|
1499
|
+
const updated = updateManagedCheckout(proof.checkoutId, (current) => {
|
|
1500
|
+
if (current.ownerSessionId !== sessionId
|
|
1501
|
+
|| current.revision !== proof.revision
|
|
1502
|
+
|| current.delivery.state !== 'preview_detached'
|
|
1503
|
+
|| current.delivery.review.reviewId !== proof.reviewId
|
|
1504
|
+
|| current.delivery.preview.previewId !== proof.previewId)
|
|
1505
|
+
throw new SessionCheckoutError('stale_target', 'Detached Preview 在分析授权前发生变化');
|
|
1506
|
+
return { ...current, recoveryContinuation: continuation };
|
|
1507
|
+
});
|
|
1508
|
+
if (!updated)
|
|
1509
|
+
throw new SessionCheckoutError('checkout_missing', 'Isolated Checkout 记录不存在');
|
|
1510
|
+
return continuation;
|
|
1511
|
+
}
|
|
1512
|
+
async function createPreviewRecoveryHandoffTarget(sessionId, proof, targetSessionId, requestId) {
|
|
1513
|
+
if (!targetSessionId || targetSessionId === sessionId) {
|
|
1514
|
+
throw new SessionCheckoutError('invalid_input', 'Recovery handoff 必须使用新的预分配 Session ID');
|
|
1515
|
+
}
|
|
1516
|
+
if (dependencies.lookup.getSession(targetSessionId) || getPersistedBinding(targetSessionId)) {
|
|
1517
|
+
throw new SessionCheckoutError('target_already_bound', 'Recovery handoff Session ID 已被占用');
|
|
1518
|
+
}
|
|
1519
|
+
const recovery = await preflightPreviewRecoveryTarget(sessionId, proof.revision, proof.reviewId, proof.previewId);
|
|
1520
|
+
if (recovery.status !== 'assessed' || !recoveryProofMatches(proof, recovery.proof)) {
|
|
1521
|
+
throw new SessionCheckoutError('stale_target', 'Detached Preview Recovery proof 已变化,请重新检查');
|
|
1522
|
+
}
|
|
1523
|
+
const source = dependencies.registry.read().managedCheckouts[proof.checkoutId];
|
|
1524
|
+
if (!source || source.ownerSessionId !== sessionId || source.delivery.state !== 'preview_detached') {
|
|
1525
|
+
throw new SessionCheckoutError('stale_target', '旧 Detached Preview 身份已变化');
|
|
1526
|
+
}
|
|
1527
|
+
const originSessionId = source.sourceSessionId ?? source.ownerSessionId;
|
|
1528
|
+
if (!dependencies.lookup.getSession(originSessionId)) {
|
|
1529
|
+
throw new SessionCheckoutError('session_not_found', '原始 source Session 不可用,未创建 handoff Worktree');
|
|
1530
|
+
}
|
|
1531
|
+
const target = await bindTarget(targetSessionId, { kind: 'isolated' }, 0, Date.now(), originSessionId);
|
|
1532
|
+
const binding = getPersistedBinding(targetSessionId);
|
|
1533
|
+
const created = binding?.target.kind === 'isolated'
|
|
1534
|
+
? dependencies.registry.read().managedCheckouts[binding.target.checkoutId]
|
|
1535
|
+
: undefined;
|
|
1536
|
+
if (!created)
|
|
1537
|
+
throw new SessionCheckoutError('checkout_missing', 'Recovery handoff Worktree 创建后记录缺失');
|
|
1538
|
+
const continuation = {
|
|
1539
|
+
kind: 'worktree_preview_recovery_handoff',
|
|
1540
|
+
requestId,
|
|
1541
|
+
sourceCheckoutId: source.checkoutId,
|
|
1542
|
+
reviewId: proof.reviewId,
|
|
1543
|
+
previewId: proof.previewId,
|
|
1544
|
+
sourceRevision: proof.revision,
|
|
1545
|
+
generation: proof.generation,
|
|
1546
|
+
};
|
|
1547
|
+
updateManagedCheckout(created.checkoutId, (current) => ({ ...current, recoveryContinuation: continuation }));
|
|
1548
|
+
return { targetSessionId, managedRoot: created.managedRoot, target, continuation };
|
|
1549
|
+
}
|
|
1119
1550
|
async function operateApply(input, binding) {
|
|
1120
1551
|
if (binding.ownerSessionId !== input.sessionId) {
|
|
1121
1552
|
return operationError('not_owner', '继承 Session Target 的会话不能执行 Apply');
|
|
@@ -1280,7 +1711,138 @@ export function createSessionCheckoutModule(dependencies) {
|
|
|
1280
1711
|
console.warn('[session-checkout] 清理 Preview refs 失败,已保守保留不可见引用');
|
|
1281
1712
|
}
|
|
1282
1713
|
}
|
|
1283
|
-
function
|
|
1714
|
+
async function previewArtifactsMatch(record, receipt) {
|
|
1715
|
+
const prefix = `previews/${receipt.previewId}`;
|
|
1716
|
+
const expected = [
|
|
1717
|
+
[`${prefix}/local-working`, receipt.localWorkingTreeOid],
|
|
1718
|
+
[`${prefix}/local-index`, receipt.localIndexTreeOid],
|
|
1719
|
+
[`${prefix}/preview-working`, receipt.previewWorkingTreeOid],
|
|
1720
|
+
[`${prefix}/isolated-snapshot`, receipt.isolatedSnapshotOid],
|
|
1721
|
+
];
|
|
1722
|
+
for (const [name, oid] of expected) {
|
|
1723
|
+
if (await dependencies.git.readInternalArtifact(record.localRoot, record.checkoutId, name) !== oid)
|
|
1724
|
+
return false;
|
|
1725
|
+
}
|
|
1726
|
+
return true;
|
|
1727
|
+
}
|
|
1728
|
+
function blockedPreviewRecovery(record, reason, message) {
|
|
1729
|
+
const delivery = record?.delivery;
|
|
1730
|
+
const detached = delivery?.state === 'preview_detached' ? delivery : undefined;
|
|
1731
|
+
return {
|
|
1732
|
+
status: 'blocked',
|
|
1733
|
+
localModified: false,
|
|
1734
|
+
checkoutId: record?.checkoutId ?? '',
|
|
1735
|
+
reviewId: detached?.review.reviewId ?? null,
|
|
1736
|
+
previewId: detached?.preview.previewId ?? null,
|
|
1737
|
+
revision: record?.revision ?? 0,
|
|
1738
|
+
reason,
|
|
1739
|
+
message,
|
|
1740
|
+
};
|
|
1741
|
+
}
|
|
1742
|
+
function canonicalJson(value) {
|
|
1743
|
+
if (Array.isArray(value))
|
|
1744
|
+
return `[${value.map(canonicalJson).join(',')}]`;
|
|
1745
|
+
if (value !== null && typeof value === 'object') {
|
|
1746
|
+
const entries = Object.entries(value)
|
|
1747
|
+
.filter(([, entry]) => entry !== undefined)
|
|
1748
|
+
.sort(([left], [right]) => left.localeCompare(right));
|
|
1749
|
+
return `{${entries.map(([key, entry]) => `${JSON.stringify(key)}:${canonicalJson(entry)}`).join(',')}}`;
|
|
1750
|
+
}
|
|
1751
|
+
return JSON.stringify(value);
|
|
1752
|
+
}
|
|
1753
|
+
function sha256Facts(value) {
|
|
1754
|
+
return createHash('sha256').update(canonicalJson(value)).digest('hex');
|
|
1755
|
+
}
|
|
1756
|
+
function recoveryGeneration(facts) {
|
|
1757
|
+
return sha256Facts(facts);
|
|
1758
|
+
}
|
|
1759
|
+
async function preflightPreviewRecoveryTarget(sessionId, expectedRevision, expectedReviewId, expectedPreviewId) {
|
|
1760
|
+
const binding = await resolveBinding(sessionId);
|
|
1761
|
+
if (binding.ownerSessionId !== sessionId || binding.target.kind !== 'isolated') {
|
|
1762
|
+
return blockedPreviewRecovery(undefined, 'not_owner', '只有 owner Isolated 会话可以检查 Preview 恢复');
|
|
1763
|
+
}
|
|
1764
|
+
const record = dependencies.registry.read().managedCheckouts[binding.target.checkoutId];
|
|
1765
|
+
if (!record)
|
|
1766
|
+
return blockedPreviewRecovery(undefined, 'checkout_unavailable', 'Isolated Checkout 记录不存在');
|
|
1767
|
+
if (record.revision !== expectedRevision) {
|
|
1768
|
+
return blockedPreviewRecovery(record, 'stale_target', 'Session Target 已变化,请刷新后重新检查');
|
|
1769
|
+
}
|
|
1770
|
+
if (record.phase !== 'ready'
|
|
1771
|
+
|| record.delivery.state !== 'preview_detached'
|
|
1772
|
+
|| record.delivery.review.reviewId !== expectedReviewId
|
|
1773
|
+
|| record.delivery.preview.previewId !== expectedPreviewId) {
|
|
1774
|
+
return blockedPreviewRecovery(record, 'not_preview_detached', '当前并非指定的 detached Preview 恢复状态');
|
|
1775
|
+
}
|
|
1776
|
+
const validated = await validateManagedCheckoutDetailed(binding, record, false);
|
|
1777
|
+
if (validated.status !== 'valid') {
|
|
1778
|
+
return blockedPreviewRecovery(record, 'checkout_unavailable', 'Worktree 身份、路径或 Git 状态暂时无法确认');
|
|
1779
|
+
}
|
|
1780
|
+
const { review, preview } = record.delivery;
|
|
1781
|
+
try {
|
|
1782
|
+
if (!await previewArtifactsMatch(record, preview)) {
|
|
1783
|
+
return blockedPreviewRecovery(record, 'artifacts_missing', 'Preview retained artifacts 缺失或与 receipt 不一致');
|
|
1784
|
+
}
|
|
1785
|
+
const assessment = await dependencies.applyEngine.assessPreviewRecovery({
|
|
1786
|
+
localPath: record.localRoot,
|
|
1787
|
+
receipt: preview,
|
|
1788
|
+
});
|
|
1789
|
+
if ('error' in assessment) {
|
|
1790
|
+
return blockedPreviewRecovery(record, 'git_error', assessment.error.message);
|
|
1791
|
+
}
|
|
1792
|
+
const holder = findProjectAcceptanceHolder(record);
|
|
1793
|
+
const blocker = holder
|
|
1794
|
+
? { checkoutId: holder.checkoutId, ownerSessionId: holder.ownerSessionId, revision: holder.revision, state: holder.delivery.state }
|
|
1795
|
+
: undefined;
|
|
1796
|
+
const rollback = holder
|
|
1797
|
+
? { status: 'blocked', code: 'project_acceptance_busy', message: '另一个任务正在占用该项目的 Local 验收槽位' }
|
|
1798
|
+
: assessment.rollback;
|
|
1799
|
+
const finalize = holder
|
|
1800
|
+
? { status: 'blocked', code: 'project_acceptance_busy', message: '另一个任务正在占用该项目的 Local 验收槽位' }
|
|
1801
|
+
: assessment.finalize;
|
|
1802
|
+
const facts = {
|
|
1803
|
+
sessionId,
|
|
1804
|
+
checkoutId: record.checkoutId,
|
|
1805
|
+
reviewId: review.reviewId,
|
|
1806
|
+
previewId: preview.previewId,
|
|
1807
|
+
revision: record.revision,
|
|
1808
|
+
receiptFingerprint: sha256Facts(preview),
|
|
1809
|
+
localFingerprint: assessment.localFingerprint,
|
|
1810
|
+
localHeadOid: assessment.localHeadOid,
|
|
1811
|
+
localHeadRef: assessment.localHeadRef,
|
|
1812
|
+
localHeadTreeOid: assessment.localHeadTreeOid,
|
|
1813
|
+
localIndexTreeOid: assessment.localIndexTreeOid,
|
|
1814
|
+
localWorkingTreeOid: assessment.localWorkingTreeOid,
|
|
1815
|
+
rollback,
|
|
1816
|
+
finalize,
|
|
1817
|
+
...(blocker ? { blocker } : {}),
|
|
1818
|
+
};
|
|
1819
|
+
const current = dependencies.registry.read().managedCheckouts[record.checkoutId];
|
|
1820
|
+
if (!current
|
|
1821
|
+
|| current.revision !== record.revision
|
|
1822
|
+
|| current.phase !== 'ready'
|
|
1823
|
+
|| current.delivery.state !== 'preview_detached'
|
|
1824
|
+
|| current.delivery.review.reviewId !== review.reviewId
|
|
1825
|
+
|| current.delivery.preview.previewId !== preview.previewId) {
|
|
1826
|
+
return blockedPreviewRecovery(current, 'stale_target', 'Preview 恢复状态在检查期间发生变化');
|
|
1827
|
+
}
|
|
1828
|
+
if (!await previewArtifactsMatch(current, current.delivery.preview)) {
|
|
1829
|
+
return blockedPreviewRecovery(current, 'artifacts_missing', 'Preview retained artifacts 在检查期间发生变化');
|
|
1830
|
+
}
|
|
1831
|
+
const proof = { ...facts, generation: recoveryGeneration(facts) };
|
|
1832
|
+
return { status: 'assessed', localModified: false, proof };
|
|
1833
|
+
}
|
|
1834
|
+
catch (error) {
|
|
1835
|
+
const message = error instanceof Error ? error.message : 'Preview 恢复检查失败';
|
|
1836
|
+
return blockedPreviewRecovery(record, 'git_error', message);
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
function recoveryProofMatches(left, right) {
|
|
1840
|
+
if (left === undefined)
|
|
1841
|
+
return false;
|
|
1842
|
+
const { generation: leftGeneration, ...leftFacts } = left;
|
|
1843
|
+
return leftGeneration === recoveryGeneration(leftFacts) && leftGeneration === right.generation;
|
|
1844
|
+
}
|
|
1845
|
+
function blockedPreflight(record, reason, message, blocker) {
|
|
1284
1846
|
return {
|
|
1285
1847
|
status: 'blocked',
|
|
1286
1848
|
localModified: false,
|
|
@@ -1289,6 +1851,14 @@ export function createSessionCheckoutModule(dependencies) {
|
|
|
1289
1851
|
revision: record?.revision ?? 0,
|
|
1290
1852
|
reason,
|
|
1291
1853
|
message,
|
|
1854
|
+
...(blocker === undefined ? {} : {
|
|
1855
|
+
blocker: {
|
|
1856
|
+
checkoutId: blocker.checkoutId,
|
|
1857
|
+
ownerSessionId: blocker.ownerSessionId,
|
|
1858
|
+
revision: blocker.revision,
|
|
1859
|
+
state: blocker.delivery.state,
|
|
1860
|
+
},
|
|
1861
|
+
}),
|
|
1292
1862
|
};
|
|
1293
1863
|
}
|
|
1294
1864
|
async function preflightTarget(sessionId, expectedRevision) {
|
|
@@ -1305,8 +1875,9 @@ export function createSessionCheckoutModule(dependencies) {
|
|
|
1305
1875
|
if (record.phase !== 'ready' || record.delivery.state !== 'ready_for_review') {
|
|
1306
1876
|
return blockedPreflight(record, 'not_ready_for_review', '当前 Worktree 尚未处于可验收状态');
|
|
1307
1877
|
}
|
|
1308
|
-
|
|
1309
|
-
|
|
1878
|
+
const holder = findProjectAcceptanceHolder(record);
|
|
1879
|
+
if (holder) {
|
|
1880
|
+
return blockedPreflight(record, 'project_acceptance_busy', '另一个任务正在占用该项目的 Local 验收槽位', holder);
|
|
1310
1881
|
}
|
|
1311
1882
|
const validated = await validateManagedCheckoutDetailed(binding, record, false);
|
|
1312
1883
|
if (validated.status !== 'valid') {
|
|
@@ -1325,7 +1896,12 @@ export function createSessionCheckoutModule(dependencies) {
|
|
|
1325
1896
|
|| current.delivery.review.reviewId !== review.reviewId)
|
|
1326
1897
|
return blockedPreflight(current, 'stale_target', 'Session Target 在预检期间发生变化,请刷新后重试');
|
|
1327
1898
|
if (result.status === 'error') {
|
|
1328
|
-
|
|
1899
|
+
const reason = result.error.code === 'stale_isolated'
|
|
1900
|
+
? 'stale_isolated'
|
|
1901
|
+
: result.error.code === 'stale_local'
|
|
1902
|
+
? 'stale_local'
|
|
1903
|
+
: 'git_error';
|
|
1904
|
+
return blockedPreflight(current, reason, result.error.message);
|
|
1329
1905
|
}
|
|
1330
1906
|
const isolatedFingerprint = result.status === 'ready'
|
|
1331
1907
|
? result.plan.isolatedFingerprint
|
|
@@ -1552,6 +2128,18 @@ export function createSessionCheckoutModule(dependencies) {
|
|
|
1552
2128
|
return operationError('preview_not_active', '当前没有可撤回的 Local Preview', await inspectIsolated(binding));
|
|
1553
2129
|
}
|
|
1554
2130
|
const retryingDetached = record.delivery.state === 'preview_detached';
|
|
2131
|
+
if (retryingDetached) {
|
|
2132
|
+
const recovery = await preflightPreviewRecoveryTarget(input.sessionId, input.expectedRevision, record.delivery.review.reviewId, record.delivery.preview.previewId);
|
|
2133
|
+
if (recovery.status !== 'assessed') {
|
|
2134
|
+
return operationError('stale_target', recovery.message, await inspectIsolated(binding));
|
|
2135
|
+
}
|
|
2136
|
+
if (!recoveryProofMatches(input.recoveryProof, recovery.proof)) {
|
|
2137
|
+
return operationError('stale_target', 'Preview Recovery proof 已过期或不匹配,请重新检查', await inspectIsolated(binding));
|
|
2138
|
+
}
|
|
2139
|
+
if (recovery.proof.rollback.status !== 'safe') {
|
|
2140
|
+
return operationError(recovery.proof.rollback.code, recovery.proof.rollback.message, await inspectIsolated(binding));
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
1555
2143
|
const resumeRevision = input.resumeRevision ?? (retryingRecovery && record.journal?.operation === 'rollback_preview'
|
|
1556
2144
|
? record.journal.resumeRevision ?? false
|
|
1557
2145
|
: false);
|
|
@@ -1569,19 +2157,39 @@ export function createSessionCheckoutModule(dependencies) {
|
|
|
1569
2157
|
previewId: preview.previewId,
|
|
1570
2158
|
reviewId: review.reviewId,
|
|
1571
2159
|
resumeRevision,
|
|
2160
|
+
...(input.recoveryProof ? { recoveryGeneration: input.recoveryProof.generation } : {}),
|
|
1572
2161
|
},
|
|
1573
2162
|
revision: current.revision + 1,
|
|
1574
2163
|
}));
|
|
1575
|
-
const result = await dependencies.applyEngine.rollback({
|
|
2164
|
+
const result = await dependencies.applyEngine.rollback({
|
|
2165
|
+
localPath: record.localRoot,
|
|
2166
|
+
receipt: preview,
|
|
2167
|
+
beforeWrite: async () => {
|
|
2168
|
+
updateManagedCheckout(record.checkoutId, (current) => ({
|
|
2169
|
+
...current,
|
|
2170
|
+
journal: current.journal?.operation === 'rollback_preview'
|
|
2171
|
+
? { ...current.journal, step: 'writing_local' }
|
|
2172
|
+
: current.journal,
|
|
2173
|
+
revision: current.revision + 1,
|
|
2174
|
+
}));
|
|
2175
|
+
},
|
|
2176
|
+
});
|
|
1576
2177
|
if (result.status === 'error') {
|
|
2178
|
+
const current = dependencies.registry.read().managedCheckouts[record.checkoutId];
|
|
2179
|
+
const writeStarted = current?.journal?.operation === 'rollback_preview' && current.journal.step === 'writing_local';
|
|
1577
2180
|
if (result.error.code === 'stale_local' || result.error.code === 'preview_modified') {
|
|
1578
2181
|
if (!retryingDetached) {
|
|
1579
2182
|
return detachPreviewAfterLocalDrift(record, binding, result.error.code, 'rollback_preview');
|
|
1580
2183
|
}
|
|
1581
|
-
updateManagedCheckout(record.checkoutId, (
|
|
2184
|
+
updateManagedCheckout(record.checkoutId, (checkout) => ({ ...checkout, phase: 'ready', journal: null, revision: checkout.revision + 1 }));
|
|
2185
|
+
}
|
|
2186
|
+
else if (result.error.code === 'invalid_input'
|
|
2187
|
+
|| !writeStarted
|
|
2188
|
+
|| (result.error.code === 'git_error' && result.error.recoveryState === 'unchanged')) {
|
|
2189
|
+
updateManagedCheckout(record.checkoutId, (checkout) => ({ ...checkout, phase: 'ready', journal: null, revision: checkout.revision + 1 }));
|
|
1582
2190
|
}
|
|
1583
|
-
else
|
|
1584
|
-
updateManagedCheckout(record.checkoutId, (
|
|
2191
|
+
else {
|
|
2192
|
+
updateManagedCheckout(record.checkoutId, (checkout) => ({ ...checkout, phase: 'recovery_required', revision: checkout.revision + 1 }));
|
|
1585
2193
|
}
|
|
1586
2194
|
return operationError(result.error.code, result.error.message, await inspectIsolated(binding));
|
|
1587
2195
|
}
|
|
@@ -1810,9 +2418,23 @@ export function createSessionCheckoutModule(dependencies) {
|
|
|
1810
2418
|
if (record.revision !== input.expectedRevision) {
|
|
1811
2419
|
return operationError('stale_target', 'Session Target 已变化,请刷新后重试', await inspectIsolated(binding));
|
|
1812
2420
|
}
|
|
1813
|
-
if (record.phase !== 'ready'
|
|
2421
|
+
if (record.phase !== 'ready'
|
|
2422
|
+
|| (record.delivery.state !== 'preview_active' && record.delivery.state !== 'preview_detached')) {
|
|
1814
2423
|
return operationError('preview_not_active', '当前没有等待验收的 Local Preview', await inspectIsolated(binding));
|
|
1815
2424
|
}
|
|
2425
|
+
const retryingDetached = record.delivery.state === 'preview_detached';
|
|
2426
|
+
if (retryingDetached) {
|
|
2427
|
+
const recovery = await preflightPreviewRecoveryTarget(input.sessionId, input.expectedRevision, record.delivery.review.reviewId, record.delivery.preview.previewId);
|
|
2428
|
+
if (recovery.status !== 'assessed') {
|
|
2429
|
+
return operationError('stale_target', recovery.message, await inspectIsolated(binding));
|
|
2430
|
+
}
|
|
2431
|
+
if (!recoveryProofMatches(input.recoveryProof, recovery.proof)) {
|
|
2432
|
+
return operationError('stale_target', 'Preview Recovery proof 已过期或不匹配,请重新检查', await inspectIsolated(binding));
|
|
2433
|
+
}
|
|
2434
|
+
if (recovery.proof.finalize.status !== 'safe') {
|
|
2435
|
+
return operationError(recovery.proof.finalize.code, recovery.proof.finalize.message, await inspectIsolated(binding));
|
|
2436
|
+
}
|
|
2437
|
+
}
|
|
1816
2438
|
// Harness plugin does not expose Domi collaborator checkout inheritance.
|
|
1817
2439
|
const { preview, review } = record.delivery;
|
|
1818
2440
|
const operationId = dependencies.createCheckoutId();
|
|
@@ -1820,7 +2442,16 @@ export function createSessionCheckoutModule(dependencies) {
|
|
|
1820
2442
|
updateManagedCheckout(record.checkoutId, (current) => ({
|
|
1821
2443
|
...current,
|
|
1822
2444
|
phase: 'mutating',
|
|
1823
|
-
journal: {
|
|
2445
|
+
journal: {
|
|
2446
|
+
operation: 'finalize_preview',
|
|
2447
|
+
operationId,
|
|
2448
|
+
step: 'planning',
|
|
2449
|
+
startedAt,
|
|
2450
|
+
previewId: preview.previewId,
|
|
2451
|
+
reviewId: review.reviewId,
|
|
2452
|
+
retention: input.retention ?? 'cleanup',
|
|
2453
|
+
...(input.recoveryProof ? { recoveryGeneration: input.recoveryProof.generation } : {}),
|
|
2454
|
+
},
|
|
1824
2455
|
revision: current.revision + 1,
|
|
1825
2456
|
}));
|
|
1826
2457
|
const result = await dependencies.applyEngine.finalize({
|
|
@@ -1838,11 +2469,22 @@ export function createSessionCheckoutModule(dependencies) {
|
|
|
1838
2469
|
},
|
|
1839
2470
|
});
|
|
1840
2471
|
if (result.status === 'error') {
|
|
1841
|
-
if (result.error.code === 'stale_local' || result.error.code === 'preview_modified') {
|
|
2472
|
+
if ((result.error.code === 'stale_local' || result.error.code === 'preview_modified') && !retryingDetached) {
|
|
1842
2473
|
return detachPreviewAfterLocalDrift(record, binding, result.error.code, 'finalize_preview');
|
|
1843
2474
|
}
|
|
1844
|
-
|
|
1845
|
-
|
|
2475
|
+
const current = dependencies.registry.read().managedCheckouts[record.checkoutId];
|
|
2476
|
+
const writeStarted = current?.journal?.operation === 'finalize_preview' && current.journal.step === 'updating_ref';
|
|
2477
|
+
if (result.error.code === 'commit_isolation_conflict'
|
|
2478
|
+
|| result.error.code === 'operation_not_allowed'
|
|
2479
|
+
|| result.error.code === 'invalid_input'
|
|
2480
|
+
|| result.error.code === 'stale_local'
|
|
2481
|
+
|| result.error.code === 'preview_modified'
|
|
2482
|
+
|| !writeStarted
|
|
2483
|
+
|| (result.error.code === 'git_error' && result.error.recoveryState === 'unchanged')) {
|
|
2484
|
+
updateManagedCheckout(record.checkoutId, (checkout) => ({ ...checkout, phase: 'ready', journal: null, revision: checkout.revision + 1 }));
|
|
2485
|
+
}
|
|
2486
|
+
else {
|
|
2487
|
+
updateManagedCheckout(record.checkoutId, (checkout) => ({ ...checkout, phase: 'recovery_required', revision: checkout.revision + 1 }));
|
|
1846
2488
|
}
|
|
1847
2489
|
return operationError(result.error.code, result.error.message, await inspectIsolated(binding));
|
|
1848
2490
|
}
|
|
@@ -1854,12 +2496,16 @@ export function createSessionCheckoutModule(dependencies) {
|
|
|
1854
2496
|
review,
|
|
1855
2497
|
commitOid: result.commitOid,
|
|
1856
2498
|
proof: {
|
|
1857
|
-
localBranch:
|
|
1858
|
-
?
|
|
1859
|
-
:
|
|
1860
|
-
|
|
1861
|
-
|
|
2499
|
+
localBranch: input.recoveryProof?.localHeadRef?.startsWith('refs/heads/')
|
|
2500
|
+
? input.recoveryProof.localHeadRef.slice('refs/heads/'.length)
|
|
2501
|
+
: preview.localHeadRef?.startsWith('refs/heads/')
|
|
2502
|
+
? preview.localHeadRef.slice('refs/heads/'.length)
|
|
2503
|
+
: null,
|
|
2504
|
+
localHeadBefore: input.recoveryProof?.localHeadOid ?? preview.localHeadOid,
|
|
2505
|
+
localHeadAfter: result.commitOid ?? input.recoveryProof?.localHeadOid ?? preview.localHeadOid,
|
|
1862
2506
|
changedFiles: [...result.changedFiles],
|
|
2507
|
+
validationStatus: review.validationStatus,
|
|
2508
|
+
...(review.validationSummary === undefined ? {} : { validationSummary: review.validationSummary }),
|
|
1863
2509
|
},
|
|
1864
2510
|
isolatedFingerprint: preview.isolatedFingerprint,
|
|
1865
2511
|
finalizedAt: Date.now(),
|
|
@@ -2140,6 +2786,8 @@ export function createSessionCheckoutModule(dependencies) {
|
|
|
2140
2786
|
localHeadBefore: receipt.localHeadOid,
|
|
2141
2787
|
localHeadAfter: finishResult.commitOid ?? receipt.localHeadOid,
|
|
2142
2788
|
changedFiles: [...finishResult.changedFiles],
|
|
2789
|
+
validationStatus: review.validationStatus,
|
|
2790
|
+
...(review.validationSummary === undefined ? {} : { validationSummary: review.validationSummary }),
|
|
2143
2791
|
},
|
|
2144
2792
|
isolatedFingerprint: receipt.isolatedFingerprint,
|
|
2145
2793
|
finalizedAt: Date.now(),
|
|
@@ -2256,11 +2904,11 @@ export function createSessionCheckoutModule(dependencies) {
|
|
|
2256
2904
|
if (!record || record.phase !== 'ready') {
|
|
2257
2905
|
return operationError('recovery_required', 'Isolated Checkout 状态已变化,需要恢复');
|
|
2258
2906
|
}
|
|
2259
|
-
dirty = (await dependencies.git.status(record.managedRoot)).dirty;
|
|
2907
|
+
dirty = (await dependencies.git.status(record.managedRoot)).dirty || (record.checkpoints?.length ?? 0) > 0;
|
|
2260
2908
|
}
|
|
2261
2909
|
else {
|
|
2262
2910
|
try {
|
|
2263
|
-
dirty = (await dependencies.git.status(record.managedRoot)).dirty;
|
|
2911
|
+
dirty = (await dependencies.git.status(record.managedRoot)).dirty || (record.checkpoints?.length ?? 0) > 0;
|
|
2264
2912
|
}
|
|
2265
2913
|
catch {
|
|
2266
2914
|
dirty = true;
|
|
@@ -2268,7 +2916,10 @@ export function createSessionCheckoutModule(dependencies) {
|
|
|
2268
2916
|
inspected = recoveryView(binding, record, dirty);
|
|
2269
2917
|
}
|
|
2270
2918
|
if (dirty && !input.confirmDirty) {
|
|
2271
|
-
|
|
2919
|
+
const checkpointDetail = (record.checkpoints?.length ?? 0) > 0
|
|
2920
|
+
? `;确认后会永久删除 ${record.checkpoints.length} 个尚未交付到 Local 的阶段`
|
|
2921
|
+
: '';
|
|
2922
|
+
return operationError('dirty_confirmation_required', `Isolated Checkout 含未提交修改、未交付阶段或状态无法确认,需要明确确认${checkpointDetail}`, inspected);
|
|
2272
2923
|
}
|
|
2273
2924
|
try {
|
|
2274
2925
|
await dependencies.git.removeWorktree(record.localRoot, record.managedGitRoot);
|
|
@@ -2329,6 +2980,8 @@ export function createSessionCheckoutModule(dependencies) {
|
|
|
2329
2980
|
return await operateFinish(input, binding);
|
|
2330
2981
|
if (input.action === 'preview')
|
|
2331
2982
|
return await operatePreview(input, binding);
|
|
2983
|
+
if (input.action === 'checkpoint')
|
|
2984
|
+
return await operateCheckpoint(input, binding);
|
|
2332
2985
|
if (input.action === 'rollback_preview')
|
|
2333
2986
|
return await operateRollbackPreview(input, binding);
|
|
2334
2987
|
if (input.action === 'finalize_preview')
|
|
@@ -2451,6 +3104,7 @@ export function createSessionCheckoutModule(dependencies) {
|
|
|
2451
3104
|
phase: record.phase,
|
|
2452
3105
|
dirty,
|
|
2453
3106
|
commitOid,
|
|
3107
|
+
...((record.checkpoints?.length ?? 0) > 0 ? { checkpointCount: record.checkpoints.length } : {}),
|
|
2454
3108
|
...(delivery.state === 'retained' ? {
|
|
2455
3109
|
retention: delivery.retention,
|
|
2456
3110
|
retainedAt: delivery.retainedAt,
|
|
@@ -3137,6 +3791,7 @@ export function createSessionCheckoutModule(dependencies) {
|
|
|
3137
3791
|
inspect: inspectAvailable,
|
|
3138
3792
|
runtimeContext,
|
|
3139
3793
|
preflight: (sessionId, expectedRevision) => withBindingLock(() => preflightTarget(sessionId, expectedRevision)),
|
|
3794
|
+
preflightPreviewRecovery: (sessionId, expectedRevision, expectedReviewId, expectedPreviewId) => withBindingLock(() => preflightPreviewRecoveryTarget(sessionId, expectedRevision, expectedReviewId, expectedPreviewId), { allowConcurrentInspect: true }),
|
|
3140
3795
|
runExclusiveSessionMutation: (sessionId, operation) => withBindingLock(async () => (operation(await inspectTarget(sessionId, true)))),
|
|
3141
3796
|
bind: (sessionId, choice) => {
|
|
3142
3797
|
const requestStartedAt = Date.now();
|
|
@@ -3166,7 +3821,10 @@ export function createSessionCheckoutModule(dependencies) {
|
|
|
3166
3821
|
return { targetSessionId, managedRoot: record.managedRoot, target };
|
|
3167
3822
|
}),
|
|
3168
3823
|
beginNextIteration: (sessionId, expectedRevision) => withBindingLock(() => beginNextIterationTarget(sessionId, expectedRevision)),
|
|
3169
|
-
resumeRevision: (sessionId, expectedRevision, expectedReviewId) => withBindingLock(() => resumeRevisionTarget(sessionId, expectedRevision, expectedReviewId)),
|
|
3824
|
+
resumeRevision: (sessionId, expectedRevision, expectedReviewId, recovery) => withBindingLock(() => resumeRevisionTarget(sessionId, expectedRevision, expectedReviewId, recovery)),
|
|
3825
|
+
prepareReviewRegeneration: (sessionId, expectedRevision, expectedReviewId, requestId) => withBindingLock(() => prepareReviewRegenerationTarget(sessionId, expectedRevision, expectedReviewId, requestId)),
|
|
3826
|
+
preparePreviewRecoveryAnalysis: (sessionId, proof, requestId) => withBindingLock(() => preparePreviewRecoveryAnalysisTarget(sessionId, proof, requestId)),
|
|
3827
|
+
createPreviewRecoveryHandoff: (sessionId, proof, targetSessionId, requestId) => withBindingLock(() => createPreviewRecoveryHandoffTarget(sessionId, proof, targetSessionId, requestId)),
|
|
3170
3828
|
markReadyForReview: (sessionId, input) => withBindingLock(() => markReadyForReviewTarget(sessionId, input)),
|
|
3171
3829
|
operate: (input) => withBindingLock(() => operateTarget(input)),
|
|
3172
3830
|
// 只读管理列表不占用全局 mutation lock;慢速目录诊断与用户操作互不阻塞。
|