@6reduk/workspace-pipeline 0.1.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 (157) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +78 -0
  3. package/docs/collaboration.md +59 -0
  4. package/docs/config-fields.md +65 -0
  5. package/docs/contracts.md +149 -0
  6. package/docs/doctor.md +165 -0
  7. package/docs/launch.md +50 -0
  8. package/docs/lifecycle-cli.md +141 -0
  9. package/docs/lifecycle.md +42 -0
  10. package/docs/migrations/unity.md +288 -0
  11. package/docs/native-provider-format.md +149 -0
  12. package/docs/provider-bundles.md +81 -0
  13. package/docs/provider-resources.md +37 -0
  14. package/docs/release.md +25 -0
  15. package/docs/remove.md +70 -0
  16. package/docs/repair.md +80 -0
  17. package/docs/repositories.md +209 -0
  18. package/docs/repository-manual-recovery.md +102 -0
  19. package/docs/repository-observations.md +29 -0
  20. package/docs/repository-recovery.md +204 -0
  21. package/docs/repository-retention.md +46 -0
  22. package/docs/repository-transport-budgets.md +26 -0
  23. package/docs/retention.md +237 -0
  24. package/docs/source.md +50 -0
  25. package/docs/switch.md +412 -0
  26. package/package.json +39 -0
  27. package/schemas/common.schema.json +251 -0
  28. package/schemas/inventory.schema.json +15 -0
  29. package/schemas/operation.schema.json +286 -0
  30. package/schemas/pipeline.schema.json +317 -0
  31. package/schemas/state.schema.json +302 -0
  32. package/schemas/workspace.schema.json +67 -0
  33. package/src/cli.js +7 -0
  34. package/src/commands/adopt.js +2 -0
  35. package/src/commands/bootstrap-recovery.js +90 -0
  36. package/src/commands/dispatch.js +394 -0
  37. package/src/commands/init.js +84 -0
  38. package/src/commands/launch.js +69 -0
  39. package/src/commands/migration-apply.js +53 -0
  40. package/src/commands/migration.js +50 -0
  41. package/src/commands/repositories.js +61 -0
  42. package/src/commands/repository-abandon.js +16 -0
  43. package/src/commands/repository-ancestors.js +21 -0
  44. package/src/commands/repository-locks.js +77 -0
  45. package/src/contracts/parse.js +57 -0
  46. package/src/contracts/semantic.js +240 -0
  47. package/src/contracts/validate.js +21 -0
  48. package/src/launch/grok.js +20 -0
  49. package/src/migrations/legacy-unity-begin.js +35 -0
  50. package/src/migrations/legacy-unity-compensate.js +82 -0
  51. package/src/migrations/legacy-unity-deactivate.js +60 -0
  52. package/src/migrations/legacy-unity-deactivation-resume-apply.js +62 -0
  53. package/src/migrations/legacy-unity-deactivation-resume.js +81 -0
  54. package/src/migrations/legacy-unity-finalize.js +88 -0
  55. package/src/migrations/legacy-unity-install-recovery.js +76 -0
  56. package/src/migrations/legacy-unity-install-resume.js +60 -0
  57. package/src/migrations/legacy-unity-install.js +88 -0
  58. package/src/migrations/legacy-unity-lease.js +136 -0
  59. package/src/migrations/legacy-unity-preflight.js +67 -0
  60. package/src/migrations/legacy-unity-preview.js +91 -0
  61. package/src/migrations/legacy-unity-resume-apply.js +39 -0
  62. package/src/migrations/legacy-unity-resume.js +41 -0
  63. package/src/migrations/legacy-unity-resumed-evidence.js +88 -0
  64. package/src/migrations/legacy-unity.js +99 -0
  65. package/src/operations/apply.js +576 -0
  66. package/src/operations/backup.js +94 -0
  67. package/src/operations/bootstrap-lock.js +87 -0
  68. package/src/operations/bootstrap-owner-retirement.js +121 -0
  69. package/src/operations/bundle-update.js +54 -0
  70. package/src/operations/config-fields.js +24 -0
  71. package/src/operations/continuation-lifecycle.js +77 -0
  72. package/src/operations/doctor.js +214 -0
  73. package/src/operations/history.js +170 -0
  74. package/src/operations/installer-identity.js +49 -0
  75. package/src/operations/journal.js +142 -0
  76. package/src/operations/lifecycle.js +133 -0
  77. package/src/operations/lineage-guard.js +20 -0
  78. package/src/operations/lock.js +109 -0
  79. package/src/operations/maintenance.js +62 -0
  80. package/src/operations/migration-pending.js +22 -0
  81. package/src/operations/ownership.js +122 -0
  82. package/src/operations/plan.js +278 -0
  83. package/src/operations/reconciliation.js +112 -0
  84. package/src/operations/recovery-lease.js +66 -0
  85. package/src/operations/remove.js +140 -0
  86. package/src/operations/repair.js +188 -0
  87. package/src/operations/repository-abandon.js +192 -0
  88. package/src/operations/repository-ancestors.js +158 -0
  89. package/src/operations/repository-apply.js +122 -0
  90. package/src/operations/repository-authorization.js +48 -0
  91. package/src/operations/repository-bootstrap-continuation.js +190 -0
  92. package/src/operations/repository-bootstrap-reconcile.js +106 -0
  93. package/src/operations/repository-bootstrap-recover.js +114 -0
  94. package/src/operations/repository-bootstrap.js +82 -0
  95. package/src/operations/repository-clone.js +63 -0
  96. package/src/operations/repository-history.js +108 -0
  97. package/src/operations/repository-inputs.js +41 -0
  98. package/src/operations/repository-journal.js +127 -0
  99. package/src/operations/repository-lock-reconcile.js +401 -0
  100. package/src/operations/repository-pending.js +29 -0
  101. package/src/operations/repository-reconcile.js +182 -0
  102. package/src/operations/repository-resumption-approvals.js +77 -0
  103. package/src/operations/repository-retention-apply.js +75 -0
  104. package/src/operations/repository-retention.js +137 -0
  105. package/src/operations/repository-workspace.js +78 -0
  106. package/src/operations/retention-apply.js +133 -0
  107. package/src/operations/retention-combined-scan.js +30 -0
  108. package/src/operations/retention-combined.js +41 -0
  109. package/src/operations/retention-policy.js +65 -0
  110. package/src/operations/retention-receipts.js +126 -0
  111. package/src/operations/retention-scan.js +86 -0
  112. package/src/operations/retention.js +56 -0
  113. package/src/operations/state.js +210 -0
  114. package/src/operations/switch-activate.js +64 -0
  115. package/src/operations/switch-backups.js +29 -0
  116. package/src/operations/switch-continuation-journal.js +94 -0
  117. package/src/operations/switch-continuation-pending.js +60 -0
  118. package/src/operations/switch-continuation-records.js +109 -0
  119. package/src/operations/switch-continuation-recovery.js +91 -0
  120. package/src/operations/switch-continuation-runtime.js +135 -0
  121. package/src/operations/switch-continuation-store.js +120 -0
  122. package/src/operations/switch-continuation.js +76 -0
  123. package/src/operations/switch-execute.js +68 -0
  124. package/src/operations/switch-inspect.js +45 -0
  125. package/src/operations/switch-journal-store.js +109 -0
  126. package/src/operations/switch-journal.js +71 -0
  127. package/src/operations/switch-lifecycle.js +72 -0
  128. package/src/operations/switch-pending.js +43 -0
  129. package/src/operations/switch-preflight.js +73 -0
  130. package/src/operations/switch-prepare.js +75 -0
  131. package/src/operations/switch-records.js +60 -0
  132. package/src/operations/switch-recovery-store.js +74 -0
  133. package/src/operations/switch.js +52 -0
  134. package/src/operations/toml-fields.js +133 -0
  135. package/src/providers/bundles.js +42 -0
  136. package/src/providers/common-entry.js +16 -0
  137. package/src/providers/grok.js +26 -0
  138. package/src/providers/interface.js +25 -0
  139. package/src/providers/kimi.js +26 -0
  140. package/src/providers/native.js +155 -0
  141. package/src/providers/registry.js +10 -0
  142. package/src/providers/shared.js +51 -0
  143. package/src/providers/source.js +30 -0
  144. package/src/source/git.js +303 -0
  145. package/src/source/inventory.js +87 -0
  146. package/src/source/repository-budget.js +18 -0
  147. package/src/source/snapshot.js +37 -0
  148. package/src/workspace/paths.js +54 -0
  149. package/src/workspace/profiles.js +8 -0
  150. package/src/workspace/repositories.js +57 -0
  151. package/src/workspace/repository-inventory.js +77 -0
  152. package/src/workspace/repository-observation.js +38 -0
  153. package/src/workspace/repository-preflight.js +129 -0
  154. package/src/workspace/repository-preview.js +196 -0
  155. package/src/workspace/repository-tree.js +57 -0
  156. package/src/workspace/reserved.js +11 -0
  157. package/src/workspace/resolve.js +53 -0
@@ -0,0 +1,88 @@
1
+ import {assertMigrationLockHeld} from '../operations/lock.js';
2
+ import {readRecord,readState,observeTargets} from '../operations/state.js';
3
+ import {inspectRecovery,writeCheckedFile,deleteCheckedFile} from '../operations/apply.js';
4
+ import {resolveChild} from '../workspace/paths.js';
5
+ import {contractDigest} from '../contracts/semantic.js';
6
+ import {sha256} from '../source/inventory.js';
7
+ import {fail} from '../contracts/parse.js';
8
+ import {verifyLegacyUnityDeactivationJournal} from './legacy-unity-install.js';
9
+
10
+ // Read-only installed-phase verification. This never issues a write capability.
11
+ export async function inspectInstalledLegacyUnityMigration(workspace,payload){
12
+ const stored=await readRecord(resolveChild(workspace,payload.recoveryPath));
13
+ if(stored.digest!==payload.recoveryHash)fail('migration.recovery-drift');
14
+ const preview=stored.value.preview;
15
+ const phase=await verifyLegacyUnityDeactivationJournal(workspace,payload.recoveryPath,payload.recoveryHash,preview);
16
+ const locator=await readRecord(resolveChild(workspace,payload.recoveryPath.replace(/recovery\.json$/,'installation.json')));
17
+ const native=await readRecord(resolveChild(workspace,locator.value.recoveryPath));
18
+ const body={kind:'prepared-plan',preview:preview.setup,stateFileHash:null,rebind:null,
19
+ preparation:{objects:preview.preflight.supply.objectsPath,snapshot:preview.preflight.supply.snapshotPath},runtime:'not-run'};
20
+ const digest=contractDigest(body);
21
+ if(contractDigest(native.value.prepared)!==contractDigest({...body,digest}) || native.value.previous!==null ||
22
+ contractDigest(native.value.approval)!==contractDigest({decision:'approve',preparedDigest:digest}))fail('migration.install-binding');
23
+ let selectedPath=locator.value.recoveryPath,selected=native,continuationHash=null;
24
+ const continuationPath=payload.recoveryPath.replace(/recovery\.json$/,'installation-continuation.json');
25
+ if((await observeTargets(workspace,[continuationPath]))[0].bytes!==null){
26
+ const pointer=await readRecord(resolveChild(workspace,continuationPath));
27
+ const authorizationPath=payload.recoveryPath.replace(/recovery\.json$/,'installation-continuation-authorization.json');
28
+ const authorization=await readRecord(resolveChild(workspace,authorizationPath)),a=authorization.value,p=a.preview;
29
+ if(!p||a.kind!=='legacy-unity-install-recovery-authorization'||a.schemaVersion!==1)fail('migration.continuation-binding');
30
+ const {digest:approvedDigest,...approvedBody}=p;
31
+ if(contractDigest(approvedBody)!==approvedDigest||contractDigest(a.approval)!==contractDigest({decision:'approve',previewDigest:approvedDigest})||
32
+ p.workspace!==workspace||p.recoveryPath!==payload.recoveryPath||p.recoveryHash!==payload.recoveryHash||p.locatorHash!==locator.digest||
33
+ contractDigest(p.phase)!==contractDigest(phase)||p.continuation?.evidence?.recoveryPath!==locator.value.recoveryPath||
34
+ p.continuation.evidence.recoveryHash!==native.digest)fail('migration.continuation-binding');
35
+ selectedPath=pointer.value.recoveryPath;
36
+ if(typeof selectedPath!=='string'||!/^\.pipeline\/transactions\/[a-f0-9-]{36}\/recovery\.json$/.test(selectedPath))fail('migration.continuation-binding');
37
+ const expectedPointer={schemaVersion:1,kind:'legacy-unity-install-continuation',migrationRecoveryHash:payload.recoveryHash,
38
+ authorizationPath,authorizationHash:authorization.digest,preparedDigest:p.continuation.digest,recoveryPath:selectedPath,
39
+ journal:selectedPath.replace('/transactions/','/journals/').replace('/recovery.json','')};
40
+ if(contractDigest(pointer.value)!==contractDigest(expectedPointer))fail('migration.continuation-binding');
41
+ selected=await readRecord(resolveChild(workspace,selectedPath));
42
+ if(contractDigest(selected.value.prepared)!==contractDigest(p.continuation)||
43
+ contractDigest(selected.value.approval)!==contractDigest({decision:'approve',preparedDigest:p.continuation.digest}))fail('migration.continuation-binding');
44
+ continuationHash=pointer.digest;
45
+ }
46
+ const checked=await inspectRecovery(workspace,selectedPath);
47
+ const expected={schemaVersion:1,kind:'legacy-unity-installation',migrationRecoveryHash:payload.recoveryHash,
48
+ preparedDigest:digest,deactivation:preview.preflight.deactivation.digest,
49
+ journal:locator.value.recoveryPath.replace('/transactions/','/journals/').replace('/recovery.json',''),recoveryPath:locator.value.recoveryPath};
50
+ if(contractDigest(locator.value)!==contractDigest(expected) || checked.recoveryHash!==selected.digest)fail('migration.install-binding');
51
+ if(continuationHash&&(checked.lineage.length!==1||checked.lineage[0].recovery!==locator.value.recoveryPath||checked.lineage[0].recoveryHash!==native.digest))fail('migration.continuation-binding');
52
+ if(checked.status!=='applied'||checked.statePhase!=='active'||checked.diagnostics.length)fail('migration.install-incomplete');
53
+ const state=await readState(resolveChild(workspace,'.pipeline/state.json'));
54
+ if(contractDigest(state.value.activation)!==contractDigest({recovery:selectedPath,
55
+ recoveryHash:selected.digest,journalHead:checked.journalHead}))fail('migration.activation-binding');
56
+ // Native exact target verification covers final configs (including opt-outs).
57
+ // Legacy binding markers are not native targets and must also remain unchanged.
58
+ const markers=preview.preflight.observations.filter(o=>o.path.startsWith('.unity-sdd/'));
59
+ for(const row of await observeTargets(workspace,markers.map(o=>o.path))){
60
+ const expected=markers.find(o=>o.path===row.path);
61
+ if(row.bytes===null||sha256(row.bytes)!==expected.sha256)fail('migration.legacy-marker-drift');
62
+ }
63
+ return {migrationRecoveryHash:stored.digest,installationLocatorHash:locator.digest,
64
+ deactivationHead:phase.head,stateHash:state.digest,nativeRecoveryHash:selected.digest,native:checked,
65
+ ...(continuationHash?{continuationHash,originalNativeRecoveryHash:native.digest}:{})};
66
+ }
67
+
68
+ // Internal same-process closeout. Restart authorization/recovery is separate.
69
+ export async function finalizeLegacyUnityMigration(lock,payload,{boundary=async()=>{}}={}){
70
+ const check=async()=>{
71
+ await assertMigrationLockHeld(lock,payload.recoveryPath,payload.recoveryHash);
72
+ return inspectInstalledLegacyUnityMigration(lock.workspace,payload);
73
+ };
74
+ const verified=await check();
75
+ const completionPath=payload.recoveryPath.replace(/recovery\.json$/,'completion.json');
76
+ const bytes=Buffer.from(JSON.stringify({schemaVersion:1,kind:'legacy-unity-completion',
77
+ workspace:lock.workspace,...verified,runtime:'not-run'})+'\n');
78
+ await writeCheckedFile(lock,completionPath,null,bytes);
79
+ await boundary('completion-recorded');
80
+ if(contractDigest(await check())!==contractDigest(verified))fail('migration.completion-drift');
81
+ if((await readRecord(resolveChild(lock.workspace,completionPath))).digest!==sha256(bytes))fail('migration.completion-drift');
82
+ await deleteCheckedFile(lock,payload.markerPath,payload.markerHash,async phase=>{
83
+ if(phase==='before-delete')await boundary('before-marker-removal');
84
+ });
85
+ // Marker removal deliberately invalidates the sealed migration capability.
86
+ // Caller can release it, but cannot perform further migration writes.
87
+ return {status:'completed',completionPath,completionHash:sha256(bytes),runtime:'not-run'};
88
+ }
@@ -0,0 +1,76 @@
1
+ import {absoluteRoot,resolveChild} from '../workspace/paths.js';
2
+ import {readRecord,observeTargets} from '../operations/state.js';
3
+ import {prepareContinuation} from '../operations/reconciliation.js';
4
+ import {contractDigest} from '../contracts/semantic.js';
5
+ import {sha256} from '../source/inventory.js';
6
+ import {fail} from '../contracts/parse.js';
7
+ import {verifyLegacyUnityDeactivationJournal} from './legacy-unity-install.js';
8
+ import {createLegacyUnityInstallRecoveryLease} from './legacy-unity-lease.js';
9
+ import {acquireWorkspaceLock} from '../operations/lock.js';
10
+ import {applyContinuation,writeCheckedFile} from '../operations/apply.js';
11
+
12
+ // Read-only wrapper around the existing native continuation planner. No retry,
13
+ // outcome rewriting, migration lease, native write or source acquisition.
14
+ export async function prepareLegacyUnityInstallRecovery(workspace,recoveryPath){
15
+ workspace=absoluteRoot(workspace);
16
+ if(typeof recoveryPath!=='string'||!/^\.pipeline\/migrations\/[a-f0-9-]{36}\/recovery\.json$/.test(recoveryPath))fail('migration.recovery-path');
17
+ if((await observeTargets(workspace,[recoveryPath.replace(/recovery\.json$/,'installation-continuation.json')]))[0].bytes!==null)fail('migration.continuation-started');
18
+ const original=await readRecord(resolveChild(workspace,recoveryPath)),r=original.value,p=r.preview;
19
+ if(r.schemaVersion!==1||r.kind!=='legacy-unity-recovery'||r.workspace!==workspace||p?.workspace!==workspace||
20
+ recoveryPath!==`.pipeline/migrations/${r.id}/recovery.json`)fail('migration.recovery-binding');
21
+ const {digest,...body}=p;
22
+ if(contractDigest(body)!==digest||contractDigest(r.approval)!==contractDigest({decision:'approve',previewDigest:digest}))fail('migration.preview-binding');
23
+ const markerPath='.pipeline/migration-operation.json',marker=await readRecord(resolveChild(workspace,markerPath));
24
+ if(contractDigest(marker.value)!==contractDigest({schemaVersion:1,kind:'legacy-unity-pending',workspace,recoveryPath,
25
+ recoveryHash:original.digest,previewDigest:digest}))fail('migration.marker-binding');
26
+ const phase=await verifyLegacyUnityDeactivationJournal(workspace,recoveryPath,original.digest,p);
27
+ const locatorPath=recoveryPath.replace(/recovery\.json$/,'installation.json'),locator=await readRecord(resolveChild(workspace,locatorPath));
28
+ const nativePath=locator.value.recoveryPath;
29
+ if(typeof nativePath!=='string'||!/^\.pipeline\/transactions\/[a-f0-9-]{36}\/recovery\.json$/.test(nativePath))fail('migration.install-binding');
30
+ const native=await readRecord(resolveChild(workspace,nativePath));
31
+ const preparedBody={kind:'prepared-plan',preview:p.setup,stateFileHash:null,rebind:null,
32
+ preparation:{objects:p.preflight.supply.objectsPath,snapshot:p.preflight.supply.snapshotPath},runtime:'not-run'};
33
+ const preparedDigest=contractDigest(preparedBody);
34
+ if(contractDigest(native.value.prepared)!==contractDigest({...preparedBody,digest:preparedDigest})||native.value.previous!==null||
35
+ contractDigest(native.value.approval)!==contractDigest({decision:'approve',preparedDigest}))fail('migration.install-binding');
36
+ const continuation=await prepareContinuation(workspace,nativePath);
37
+ if(continuation.evidence.recoveryHash!==native.digest||contractDigest(locator.value)!==contractDigest({schemaVersion:1,
38
+ kind:'legacy-unity-installation',migrationRecoveryHash:original.digest,preparedDigest,deactivation:p.preflight.deactivation.digest,
39
+ journal:continuation.evidence.journal,recoveryPath:nativePath}))fail('migration.install-binding');
40
+ // These legacy markers are outside native ownership and continuation checks.
41
+ const bindings=p.preflight.observations.filter(o=>o.path.startsWith('.unity-sdd/'));
42
+ for(const row of await observeTargets(workspace,bindings.map(o=>o.path)))
43
+ if(row.bytes===null||sha256(row.bytes)!==bindings.find(o=>o.path===row.path).sha256)fail('migration.legacy-marker-drift');
44
+ for(const record of [{path:recoveryPath,hash:original.digest},{path:markerPath,hash:marker.digest},
45
+ {path:locatorPath,hash:locator.digest},{path:nativePath,hash:native.digest}])
46
+ if((await readRecord(resolveChild(workspace,record.path))).digest!==record.hash)fail('migration.resume-drift');
47
+ if(contractDigest(await verifyLegacyUnityDeactivationJournal(workspace,recoveryPath,original.digest,p))!==contractDigest(phase)||
48
+ (await prepareContinuation(workspace,nativePath)).digest!==continuation.digest)fail('migration.resume-drift');
49
+ const result={schemaVersion:1,kind:'legacy-unity-install-recovery-preview',workspace,recoveryPath,recoveryHash:original.digest,
50
+ markerHash:marker.digest,locatorHash:locator.digest,phase,continuation,status:'needs-approval',executable:false,runtime:'not-run'};
51
+ return {...result,digest:contractDigest(result)};
52
+ }
53
+
54
+ export async function applyLegacyUnityInstallRecovery(proposal,approval,workspace,options={}){
55
+ const {lease,preview}=await createLegacyUnityInstallRecoveryLease(proposal,approval,workspace);
56
+ const lock=await acquireWorkspaceLock(workspace,{migrationOperation:lease});
57
+ try{
58
+ if(contractDigest(await prepareLegacyUnityInstallRecovery(lock.workspace,preview.recoveryPath))!==contractDigest(preview))fail('migration.resume-drift');
59
+ const authorizationPath=preview.recoveryPath.replace(/recovery\.json$/,'installation-continuation-authorization.json');
60
+ const bytes=Buffer.from(JSON.stringify({schemaVersion:1,kind:'legacy-unity-install-recovery-authorization',preview,
61
+ approval:{decision:'approve',previewDigest:preview.digest}})+'\n');
62
+ await writeCheckedFile(lock,authorizationPath,null,bytes);
63
+ const locatorPath=preview.recoveryPath.replace(/recovery\.json$/,'installation-continuation.json');
64
+ const result=await applyContinuation(lock,preview.continuation,{decision:'approve',preparedDigest:preview.continuation.digest},{
65
+ ...options,onJournal:async location=>{
66
+ if(location.status!=='planned')return;
67
+ const recoveryPath=`.pipeline/transactions/${location.relative.split('/').at(-1)}/recovery.json`;
68
+ await writeCheckedFile(lock,locatorPath,null,Buffer.from(JSON.stringify({schemaVersion:1,kind:'legacy-unity-install-continuation',
69
+ migrationRecoveryHash:preview.recoveryHash,authorizationPath,authorizationHash:sha256(bytes),
70
+ preparedDigest:preview.continuation.digest,recoveryPath,journal:location.relative})+'\n'));
71
+ }
72
+ });
73
+ if((await readRecord(resolveChild(lock.workspace,authorizationPath))).digest!==sha256(bytes))fail('migration.authorization-drift');
74
+ return {status:'needs-reconciliation',installationStatus:result.status,recoveryPath:result.recoveryPath,authorizationPath,runtime:'not-run'};
75
+ }finally{await lock.release();}
76
+ }
@@ -0,0 +1,60 @@
1
+ import {randomUUID} from 'node:crypto';
2
+ import {absoluteRoot,resolveChild} from '../workspace/paths.js';
3
+ import {readRecord,observeTargets,verifyPreparedSnapshot} from '../operations/state.js';
4
+ import {contractDigest} from '../contracts/semantic.js';
5
+ import {sha256} from '../source/inventory.js';
6
+ import {fail} from '../contracts/parse.js';
7
+ import {verifyLegacyUnityDeactivationJournal,executeLegacyUnityInstall} from './legacy-unity-install.js';
8
+ import {createLegacyUnityInstallResumeLease,legacyUnityLeasePayload} from './legacy-unity-lease.js';
9
+ import {acquireWorkspaceLock} from '../operations/lock.js';
10
+ import {writeCheckedFile} from '../operations/apply.js';
11
+
12
+ export async function prepareLegacyUnityInstallResume(workspace,recoveryPath){
13
+ workspace=absoluteRoot(workspace);
14
+ if(typeof recoveryPath!=='string'||!/^\.pipeline\/migrations\/[a-f0-9-]{36}\/recovery\.json$/.test(recoveryPath))fail('migration.recovery-path');
15
+ const record=await readRecord(resolveChild(workspace,recoveryPath)),r=record.value,p=r.preview;
16
+ if(r.schemaVersion!==1||r.kind!=='legacy-unity-recovery'||r.workspace!==workspace||p?.workspace!==workspace||
17
+ recoveryPath!==`.pipeline/migrations/${r.id}/recovery.json`)fail('migration.recovery-binding');
18
+ const {digest,...body}=p;
19
+ if(contractDigest(body)!==digest||contractDigest(r.approval)!==contractDigest({decision:'approve',previewDigest:digest}))fail('migration.preview-binding');
20
+ const markerPath='.pipeline/migration-operation.json',marker=await readRecord(resolveChild(workspace,markerPath));
21
+ if(contractDigest(marker.value)!==contractDigest({schemaVersion:1,kind:'legacy-unity-pending',workspace,recoveryPath,recoveryHash:record.digest,previewDigest:digest}))fail('migration.marker-binding');
22
+ const absent=[recoveryPath.replace(/recovery\.json$/,'installation.json'),'.pipeline/state.json'];
23
+ for(const o of await observeTargets(workspace,absent))if(o.bytes!==null)fail('migration.install-started');
24
+ const phase=await verifyLegacyUnityDeactivationJournal(workspace,recoveryPath,record.digest,p);
25
+ const expected=new Map(p.initialObservations.map(o=>[o.path,o.hash]));
26
+ for(const o of p.preflight.observations){
27
+ if(expected.has(o.path)&&expected.get(o.path)!==o.sha256)fail('migration.observation-binding');
28
+ expected.set(o.path,o.sha256);
29
+ }
30
+ for(const t of p.preflight.deactivation.targets)expected.set(t.path,t.afterHash);
31
+ const observe=async()=>{
32
+ const rows=await observeTargets(workspace,[...expected.keys()]);
33
+ const hashes=rows.map(o=>({path:o.path,hash:o.bytes===null?null:sha256(o.bytes)}));
34
+ if(hashes.some(o=>o.hash!==expected.get(o.path)))fail('migration.observation-drift');
35
+ return hashes;
36
+ };
37
+ const observations=await observe(),s=p.preflight.supply;
38
+ await verifyPreparedSnapshot({snapshotPath:s.snapshotPath,manifest:{id:'unity-sdd',version:s.version},digest:s.digest,inventoryDigest:s.inventoryDigest});
39
+ if(contractDigest(await observe())!==contractDigest(observations)||
40
+ contractDigest(await verifyLegacyUnityDeactivationJournal(workspace,recoveryPath,record.digest,p))!==contractDigest(phase)||
41
+ (await readRecord(resolveChild(workspace,recoveryPath))).digest!==record.digest||
42
+ (await readRecord(resolveChild(workspace,markerPath))).digest!==marker.digest)fail('migration.resume-drift');
43
+ for(const o of await observeTargets(workspace,absent))if(o.bytes!==null)fail('migration.install-started');
44
+ const result={schemaVersion:1,kind:'legacy-unity-install-resume-preview',workspace,recoveryPath,
45
+ recoveryHash:record.digest,markerHash:marker.digest,phase,observations,setup:p.setup,status:'needs-approval',executable:false,runtime:'not-run'};
46
+ return {...result,digest:contractDigest(result)};
47
+ }
48
+
49
+ export async function applyLegacyUnityInstallResume(proposal,approval,workspace,options={}){
50
+ const {lease,preview}=await createLegacyUnityInstallResumeLease(proposal,approval,workspace);
51
+ const lock=await acquireWorkspaceLock(workspace,{migrationOperation:lease});
52
+ try{
53
+ if(contractDigest(await prepareLegacyUnityInstallResume(lock.workspace,preview.recoveryPath))!==contractDigest(preview))fail('migration.resume-drift');
54
+ const authorizationPath=preview.recoveryPath.replace(/recovery\.json$/,`install-authorizations/${randomUUID()}.json`);
55
+ await writeCheckedFile(lock,authorizationPath,null,Buffer.from(JSON.stringify({schemaVersion:1,kind:'legacy-unity-install-authorization',
56
+ preview,approval:{decision:'approve',previewDigest:preview.digest}})+'\n'));
57
+ const result=await executeLegacyUnityInstall(lock,legacyUnityLeasePayload(lease,lock.workspace),options);
58
+ return {...result,authorizationPath};
59
+ }finally{await lock.release();}
60
+ }
@@ -0,0 +1,88 @@
1
+ import {readdir} from 'node:fs/promises';
2
+ import {readRecord,observeTargets} from '../operations/state.js';
3
+ import {assertMigrationLockHeld} from '../operations/lock.js';
4
+ import {resolveChild,inspectDirectory} from '../workspace/paths.js';
5
+ import {applyPrepared,writeCheckedFile} from '../operations/apply.js';
6
+ import {assertRecoveryBudget} from '../operations/plan.js';
7
+ import {providerRegistry} from '../providers/registry.js';
8
+ import {validateLegacyUnityDeactivation} from './legacy-unity.js';
9
+ import {contractDigest} from '../contracts/semantic.js';
10
+ import {sha256} from '../source/inventory.js';
11
+ import {parse,fail} from '../contracts/parse.js';
12
+ import {verifyLegacyUnityResumedEvidence} from './legacy-unity-resumed-evidence.js';
13
+
14
+ // Require complete first-phase evidence, not merely disabled settings.
15
+ export async function verifyLegacyUnityDeactivationJournal(workspace,recoveryPath,recoveryHash,preview){
16
+ const resumed=await verifyLegacyUnityResumedEvidence(workspace,recoveryPath,recoveryHash,preview);
17
+ if(resumed)return resumed;
18
+ const phase=validateLegacyUnityDeactivation(preview.preflight.deactivation);
19
+ const directory=recoveryPath.replace(/recovery\.json$/,'deactivation');
20
+ await inspectDirectory(resolveChild(workspace,directory));
21
+ const names=(await readdir(resolveChild(workspace,directory))).sort();
22
+ if(names.join(',')!=='000000.json,000001.json,000002.json,000003.json,000004.json,000005.json')fail('migration.phase-incomplete');
23
+ const expected=[{kind:'start',detail:null}];
24
+ for(const t of phase.targets)expected.push(
25
+ {kind:'intent',detail:{path:t.path,beforeHash:t.beforeHash,afterHash:t.afterHash}},
26
+ {kind:'outcome',detail:{path:t.path,status:'completed',observedHash:t.afterHash}});
27
+ expected.push({kind:'phase-checked',detail:{phase:'deactivate-legacy'}});
28
+ let previous=null;
29
+ for(let seq=0;seq<expected.length;seq++){
30
+ const actual=await readRecord(resolveChild(workspace,directory+'/'+names[seq]));
31
+ const event={schemaVersion:1,seq,previous,recoveryHash,...expected[seq]};
32
+ if(contractDigest(actual.value)!==contractDigest(event))fail('migration.phase-evidence');
33
+ previous=actual.digest;
34
+ }
35
+ return {directory,head:previous};
36
+ }
37
+
38
+ // Trusted compiled adapters remain authoritative. The only extra requests are
39
+ // exact takeover bindings for the two entry files in the approved migration.
40
+ function migrationRegistry(preview){
41
+ const decorate=adapter=>({...adapter,async plan(context){
42
+ const requests=await adapter.plan(context);
43
+ return requests.map(request=>{
44
+ const entry=preview.entries.find(e=>e.path===request.path);
45
+ if(!entry)return request;
46
+ if(request.kind!=='file'||sha256(request.bytes)!==entry.desiredHash)fail('migration.entry-drift');
47
+ return {...request,takeover:{beforeHash:entry.beforeHash,desiredHash:entry.desiredHash}};
48
+ });
49
+ }});
50
+ return {sharedAdapter:decorate(providerRegistry.sharedAdapter),adapters:Object.fromEntries(
51
+ Object.entries(providerRegistry.adapters).map(([id,adapter])=>[id,decorate(adapter)]))};
52
+ }
53
+
54
+ // Second phase delegates target writes/backups/state to native apply. Marker
55
+ // remains afterwards; a separate validated finalization must remove it.
56
+ export async function executeLegacyUnityInstall(lock,payload,options={}){
57
+ await assertMigrationLockHeld(lock,payload.recoveryPath,payload.recoveryHash);
58
+ const stored=await readRecord(resolveChild(lock.workspace,payload.recoveryPath));
59
+ if(stored.digest!==payload.recoveryHash)fail('migration.recovery-drift');
60
+ const preview=stored.value.preview;
61
+ await verifyLegacyUnityDeactivationJournal(lock.workspace,payload.recoveryPath,payload.recoveryHash,preview);
62
+ for(const row of await observeTargets(lock.workspace,preview.preflight.deactivation.targets.map(t=>t.path))){
63
+ const t=preview.preflight.deactivation.targets.find(t=>t.path===row.path);
64
+ if(row.bytes===null||sha256(row.bytes)!==t.afterHash)fail('migration.phase-drift');
65
+ }
66
+ const body={kind:'prepared-plan',preview:preview.setup,stateFileHash:null,rebind:null,
67
+ preparation:{objects:preview.preflight.supply.objectsPath,snapshot:preview.preflight.supply.snapshotPath},runtime:'not-run'};
68
+ const prepared={...body,digest:contractDigest(body)};
69
+ assertRecoveryBudget(prepared,null);
70
+ // This subordinate approval is derived only from the already authenticated
71
+ // full migration approval. It is not a new claim of a human decision.
72
+ const approval={decision:'approve',preparedDigest:prepared.digest};
73
+ const locatorPath=payload.recoveryPath.replace(/recovery\.json$/,'installation.json');
74
+ const [existing]=await observeTargets(lock.workspace,[locatorPath]);
75
+ if(existing.bytes!==null)fail('migration.install-already-started');
76
+ const applied=await applyPrepared(lock,prepared,approval,migrationRegistry(preview),null,{
77
+ ...options,onJournal:async location=>{
78
+ if(location.status!=='planned')return;
79
+ const runId=location.relative.split('/').at(-1);
80
+ const locator={schemaVersion:1,kind:'legacy-unity-installation',migrationRecoveryHash:payload.recoveryHash,
81
+ preparedDigest:prepared.digest,deactivation:preview.preflight.deactivation.digest,
82
+ journal:location.relative,recoveryPath:`.pipeline/transactions/${runId}/recovery.json`};
83
+ await writeCheckedFile(lock,locatorPath,null,Buffer.from(JSON.stringify(parse(JSON.stringify(locator),'json'))+'\n'));
84
+ }
85
+ });
86
+ return {status:'needs-reconciliation',phase:'install-local-delivery',installationStatus:applied.status,
87
+ recoveryPath:applied.recoveryPath,journal:applied.journal,runtime:'not-run'};
88
+ }
@@ -0,0 +1,136 @@
1
+ import {randomUUID} from 'node:crypto';
2
+ import {verifyLegacyUnityPreview} from './legacy-unity-preview.js';
3
+ import {absoluteRoot,resolveChild} from '../workspace/paths.js';
4
+ import {readRecord,observeTargets} from '../operations/state.js';
5
+ import {parse,fail} from '../contracts/parse.js';
6
+ import {sha256} from '../source/inventory.js';
7
+ import {contractDigest} from '../contracts/semantic.js';
8
+
9
+ const leases=new WeakMap();
10
+ const encode=value=>Buffer.from(JSON.stringify(parse(JSON.stringify(value),'json'))+'\n');
11
+ const markerPath='.pipeline/migration-operation.json';
12
+
13
+ // Fresh closeout approval can recreate only a verified persisted migration lease.
14
+ export async function createLegacyUnityCloseoutLease(proposal,approval,workspace){
15
+ const {prepareLegacyUnityCloseoutResume}=await import('./legacy-unity-resume.js');
16
+ workspace=absoluteRoot(workspace);
17
+ const detached=parse(JSON.stringify(proposal),'json');
18
+ if(contractDigestApproval(approval)!==detached.digest)fail('migration.resume-approval');
19
+ const actual=await prepareLegacyUnityCloseoutResume(workspace,detached.recoveryPath);
20
+ if(contractDigest(actual)!==contractDigest(detached))fail('migration.resume-drift');
21
+ const recovery=await readRecord(resolveChild(workspace,actual.recoveryPath));
22
+ const [marker]=await observeTargets(workspace,[markerPath]);
23
+ if(recovery.digest!==actual.observations.recovery||marker.bytes===null||sha256(marker.bytes)!==actual.observations.marker)fail('migration.resume-drift');
24
+ const lease=Object.freeze({workspace});
25
+ leases.set(lease,{workspace,recoveryPath:actual.recoveryPath,recoveryBytes:encode(recovery.value),
26
+ recoveryHash:recovery.digest,markerBytes:Buffer.from(marker.bytes),markerHash:sha256(marker.bytes),sealed:true});
27
+ return {lease,preview:actual};
28
+ }
29
+ function contractDigestApproval(value){
30
+ if(!value||value.decision!=='approve'||typeof value.previewDigest!=='string'||Object.keys(value).sort().join(',')!=='decision,previewDigest')fail('migration.resume-approval');
31
+ return value.previewDigest;
32
+ }
33
+
34
+ export async function createLegacyUnityCompensationLease(proposal,approval,workspace){
35
+ const {prepareLegacyUnityCompensation}=await import('./legacy-unity-compensate.js');
36
+ workspace=absoluteRoot(workspace);const detached=parse(JSON.stringify(proposal),'json');
37
+ if(contractDigestApproval(approval)!==detached.digest)fail('migration.resume-approval');
38
+ const actual=await prepareLegacyUnityCompensation(workspace,detached.recoveryPath);
39
+ if(contractDigest(actual)!==contractDigest(detached))fail('migration.resume-drift');
40
+ const recovery=await readRecord(resolveChild(workspace,actual.recoveryPath));
41
+ const [marker]=await observeTargets(workspace,[markerPath]);
42
+ if(recovery.digest!==actual.recoveryHash||marker.bytes===null||sha256(marker.bytes)!==actual.markerHash)fail('migration.resume-drift');
43
+ const lease=Object.freeze({workspace});
44
+ leases.set(lease,{workspace,recoveryPath:actual.recoveryPath,recoveryBytes:encode(recovery.value),recoveryHash:recovery.digest,
45
+ markerBytes:Buffer.from(marker.bytes),markerHash:sha256(marker.bytes),sealed:true});
46
+ return {lease,preview:actual};
47
+ }
48
+
49
+ export async function createLegacyUnityInstallRecoveryLease(proposal,approval,workspace){
50
+ const {prepareLegacyUnityInstallRecovery}=await import('./legacy-unity-install-recovery.js');
51
+ workspace=absoluteRoot(workspace);const detached=parse(JSON.stringify(proposal),'json');
52
+ if(contractDigestApproval(approval)!==detached.digest)fail('migration.resume-approval');
53
+ const actual=await prepareLegacyUnityInstallRecovery(workspace,detached.recoveryPath);
54
+ if(contractDigest(actual)!==contractDigest(detached))fail('migration.resume-drift');
55
+ const recovery=await readRecord(resolveChild(workspace,actual.recoveryPath));
56
+ const [marker]=await observeTargets(workspace,[markerPath]);
57
+ if(recovery.digest!==actual.recoveryHash||marker.bytes===null||sha256(marker.bytes)!==actual.markerHash)fail('migration.resume-drift');
58
+ const lease=Object.freeze({workspace});
59
+ leases.set(lease,{workspace,recoveryPath:actual.recoveryPath,recoveryBytes:encode(recovery.value),recoveryHash:recovery.digest,
60
+ markerBytes:Buffer.from(marker.bytes),markerHash:sha256(marker.bytes),sealed:true});
61
+ return {lease,preview:actual};
62
+ }
63
+
64
+ export async function createLegacyUnityInstallResumeLease(proposal,approval,workspace){
65
+ const {prepareLegacyUnityInstallResume}=await import('./legacy-unity-install-resume.js');
66
+ workspace=absoluteRoot(workspace);
67
+ const detached=parse(JSON.stringify(proposal),'json');
68
+ if(contractDigestApproval(approval)!==detached.digest)fail('migration.resume-approval');
69
+ const actual=await prepareLegacyUnityInstallResume(workspace,detached.recoveryPath);
70
+ if(contractDigest(actual)!==contractDigest(detached))fail('migration.resume-drift');
71
+ const recovery=await readRecord(resolveChild(workspace,actual.recoveryPath));
72
+ const [marker]=await observeTargets(workspace,[markerPath]);
73
+ if(recovery.digest!==actual.recoveryHash||marker.bytes===null||sha256(marker.bytes)!==actual.markerHash)fail('migration.resume-drift');
74
+ const lease=Object.freeze({workspace});
75
+ leases.set(lease,{workspace,recoveryPath:actual.recoveryPath,recoveryBytes:encode(recovery.value),recoveryHash:recovery.digest,
76
+ markerBytes:Buffer.from(marker.bytes),markerHash:sha256(marker.bytes),sealed:true});
77
+ return {lease,preview:actual};
78
+ }
79
+
80
+ export async function createLegacyUnityDeactivationResumeLease(proposal,approval,workspace){
81
+ const {prepareLegacyUnityDeactivationResume}=await import('./legacy-unity-deactivation-resume.js');
82
+ workspace=absoluteRoot(workspace);
83
+ const detached=parse(JSON.stringify(proposal),'json');
84
+ if(contractDigestApproval(approval)!==detached.digest)fail('migration.resume-approval');
85
+ const actual=await prepareLegacyUnityDeactivationResume(workspace,detached.recoveryPath);
86
+ if(actual.status!=='needs-approval'||contractDigest(actual)!==contractDigest(detached))fail('migration.resume-drift');
87
+ const recovery=await readRecord(resolveChild(workspace,actual.recoveryPath));
88
+ const [marker]=await observeTargets(workspace,[markerPath]);
89
+ if(recovery.digest!==actual.recoveryHash||marker.bytes===null||sha256(marker.bytes)!==actual.markerHash)fail('migration.resume-drift');
90
+ const lease=Object.freeze({workspace});
91
+ leases.set(lease,{workspace,recoveryPath:actual.recoveryPath,recoveryBytes:encode(recovery.value),recoveryHash:recovery.digest,
92
+ markerBytes:Buffer.from(marker.bytes),markerHash:sha256(marker.bytes),sealed:true});
93
+ return {lease,preview:actual};
94
+ }
95
+
96
+ // Process-local capability, issued only after trusted reconstruction and explicit
97
+ // complete-preview approval. Serialized objects cannot recreate this capability.
98
+ export async function createLegacyUnityLease(proposal,approval,workspace){
99
+ workspace=absoluteRoot(workspace);
100
+ const preview=await verifyLegacyUnityPreview(proposal,approval,workspace);
101
+ const id=randomUUID(),recoveryPath=`.pipeline/migrations/${id}/recovery.json`;
102
+ const record={schemaVersion:1,kind:'legacy-unity-recovery',id,workspace,preview,
103
+ approval:{decision:'approve',previewDigest:preview.digest}};
104
+ const recoveryBytes=encode(record),recoveryHash=sha256(recoveryBytes);
105
+ const marker={schemaVersion:1,kind:'legacy-unity-pending',workspace,recoveryPath,recoveryHash,previewDigest:preview.digest};
106
+ const markerBytes=encode(marker),markerHash=sha256(markerBytes);
107
+ const lease=Object.freeze({workspace});
108
+ leases.set(lease,{workspace,recoveryPath,recoveryBytes,recoveryHash,markerBytes,markerHash,sealed:false});
109
+ return lease;
110
+ }
111
+ function selected(lease,workspace){
112
+ const value=lease&&leases.get(lease);
113
+ if(!value||value.workspace!==absoluteRoot(workspace))fail('migration.lease');
114
+ return value;
115
+ }
116
+ export async function assertLegacyUnityLease(lease,workspace){
117
+ const value=selected(lease,workspace);
118
+ const [marker]=await observeTargets(workspace,[markerPath]);
119
+ if(marker.bytes===null){if(value.sealed)fail('migration.marker-missing');return;}
120
+ if(sha256(marker.bytes)!==value.markerHash)fail('migration.marker-drift');
121
+ if(value.sealed&&(await readRecord(resolveChild(workspace,value.recoveryPath))).digest!==value.recoveryHash)fail('migration.recovery-drift');
122
+ }
123
+ // Internal persistence payload, not another source of approval. Copies prevent
124
+ // callers mutating the private capability's expected hashes or bytes.
125
+ export function legacyUnityLeasePayload(lease,workspace){
126
+ const value=selected(lease,workspace);
127
+ return {markerPath,markerBytes:Buffer.from(value.markerBytes),markerHash:value.markerHash,
128
+ recoveryPath:value.recoveryPath,recoveryBytes:Buffer.from(value.recoveryBytes),recoveryHash:value.recoveryHash};
129
+ }
130
+ export async function sealLegacyUnityLease(lease,workspace){
131
+ const value=selected(lease,workspace);
132
+ const [marker]=await observeTargets(workspace,[markerPath]);
133
+ if(marker.bytes===null||sha256(marker.bytes)!==value.markerHash)fail('migration.marker-drift');
134
+ if((await readRecord(resolveChild(workspace,value.recoveryPath))).digest!==value.recoveryHash)fail('migration.recovery-drift');
135
+ value.sealed=true;
136
+ }
@@ -0,0 +1,67 @@
1
+ // Read-only wrapper observation. Acquisition writes only to external temp storage.
2
+ // No approval, target writes, lock acquisition or native harness invocation.
3
+ import path from 'node:path';
4
+ import {tmpdir} from 'node:os';
5
+ import {readdir} from 'node:fs/promises';
6
+ import {assertLockHeld} from '../operations/lock.js';
7
+ import {fail} from '../contracts/parse.js';
8
+ import {contractDigest} from '../contracts/semantic.js';
9
+ import {absoluteRoot,inspectDirectory} from '../workspace/paths.js';
10
+ import {observeTargets,verifyPreparedSnapshot} from '../operations/state.js';
11
+ import {acquire} from '../source/git.js';
12
+ import {sha256} from '../source/inventory.js';
13
+ import {prepareLegacyUnityDeactivation} from './legacy-unity.js';
14
+
15
+ const targets=['.codex/config.toml','.claude/settings.local.json','AGENTS.md','CLAUDE.md',
16
+ '.unity-sdd/workspace.json','.unity-sdd/claude.json'];
17
+ const nested=(parent,child)=>{const r=path.relative(parent,child);return !r||(!path.isAbsolute(r)&&r!=='..'&&!r.startsWith('..'+path.sep));};
18
+ async function observe(wrapper,lock){
19
+ if(!(await inspectDirectory(wrapper)).exists)fail('migration.wrapper-missing');
20
+ // Existing managed metadata requires reconciliation, not a second bootstrap.
21
+ if((await inspectDirectory(path.join(wrapper,'.pipeline'))).exists){
22
+ if(!lock||lock.workspace!==wrapper)fail('migration.already-managed');
23
+ await assertLockHeld(lock);
24
+ const names=await readdir(path.join(wrapper,'.pipeline'));
25
+ if(names.length!==1||names[0]!=='lock')fail('migration.already-managed');
26
+ }
27
+ const rows=await observeTargets(wrapper,targets);
28
+ if(rows.some(r=>r.bytes===null))fail('migration.legacy-missing');
29
+ // Native observation order is sorted; callers must not depend on input order.
30
+ return targets.map(name=>rows.find(row=>row.path===name));
31
+ }
32
+ const fingerprints=rows=>rows.map(r=>({path:r.path,sha256:sha256(r.bytes)}));
33
+ export async function prepareLegacyUnityPreflight({wrapper,source,manifestBase,tempRoot=tmpdir(),network=false}){
34
+ wrapper=absoluteRoot(wrapper);tempRoot=absoluteRoot(tempRoot);
35
+ if(nested(wrapper,tempRoot)||nested(tempRoot,wrapper))fail('migration.preparation-location');
36
+ const before=await observe(wrapper);
37
+ const deactivation=prepareLegacyUnityDeactivation([
38
+ {provider:'codex',bytes:before[0].bytes},{provider:'claude',bytes:before[1].bytes}]);
39
+ const acquired=await acquire(source,{manifestBase,tempRoot,network});
40
+ const verified=await verifyPreparedSnapshot(acquired);
41
+ if(verified.manifest.id!=='unity-sdd'||!verified.manifest.providers.codex||!verified.manifest.providers.claude)fail('migration.supply');
42
+ const after=await observe(wrapper);
43
+ if(contractDigest(fingerprints(before))!==contractDigest(fingerprints(after)))fail('migration.observation-drift');
44
+ const body={schemaVersion:1,kind:'legacy-unity-preflight',workspace:wrapper,
45
+ observations:fingerprints(before),deactivation,
46
+ supply:{source:acquired.source,commit:acquired.commit,version:verified.manifest.version,digest:acquired.digest,inventoryDigest:acquired.inventoryDigest,
47
+ snapshotPath:acquired.snapshotPath,objectsPath:acquired.preparation},runtime:'not-run'};
48
+ return {...body,digest:contractDigest(body)};
49
+ }
50
+
51
+ // Recheck observed wrapper and staged package without contacting Git/network.
52
+ // Digest equality is integrity, not authorization or proof of an earlier check.
53
+ export async function recheckLegacyUnityPreflight(record,wrapper,lock){
54
+ const {digest,...body}=record;
55
+ if(record.kind!=='legacy-unity-preflight'||contractDigest(body)!==digest||record.workspace!==absoluteRoot(wrapper))fail('migration.preflight-binding');
56
+ const rows=await observe(wrapper,lock);
57
+ if(contractDigest(fingerprints(rows))!==contractDigest(record.observations))fail('migration.observation-drift');
58
+ for(const candidate of [record.supply.snapshotPath,record.supply.objectsPath]){
59
+ const root=absoluteRoot(candidate);
60
+ if(nested(wrapper,root)||nested(root,wrapper))fail('migration.preparation-location');
61
+ if(!(await inspectDirectory(root)).exists)fail('migration.preparation-missing');
62
+ }
63
+ const expected=prepareLegacyUnityDeactivation([{provider:'codex',bytes:rows[0].bytes},{provider:'claude',bytes:rows[1].bytes}]);
64
+ if(contractDigest(expected)!==contractDigest(record.deactivation))fail('migration.preflight-binding');
65
+ const verified=await verifyPreparedSnapshot({...record.supply,manifest:{id:'unity-sdd',version:record.supply.version}});
66
+ return {observed:true,pipeline:verified.manifest.id,runtime:'not-run',authorized:false};
67
+ }
@@ -0,0 +1,91 @@
1
+ // Full rendered proposal only: never pass setup directly to generic apply.
2
+ // It observes the virtual post-deactivation configuration, not current bytes.
3
+ import {fail,parse} from '../contracts/parse.js';
4
+ import {requestShape} from '../operations/ownership.js';
5
+ import {contractDigest} from '../contracts/semantic.js';
6
+ import {sha256} from '../source/inventory.js';
7
+ import {absoluteRoot} from '../workspace/paths.js';
8
+ import {planLayout} from '../workspace/resolve.js';
9
+ import {resolveOrigin,observeTargets,verifyPreparedSnapshot} from '../operations/state.js';
10
+ import {composePlan,assertRequestScope} from '../operations/plan.js';
11
+ import {providerRegistry} from '../providers/registry.js';
12
+ import {prepareLegacyUnityPreflight,recheckLegacyUnityPreflight} from './legacy-unity-preflight.js';
13
+
14
+ export async function prepareLegacyUnityPreview({wrapper,manifestPath,tempRoot,network=false}){
15
+ wrapper=absoluteRoot(wrapper);
16
+ const origin=await resolveOrigin({wrapper,manifestPath,command:'setup'});
17
+ if(origin.manifest.bundles || [...(origin.manifest.providers??[])].sort().join(',')!=='claude,codex')fail('migration.providers');
18
+ const preflight=await prepareLegacyUnityPreflight({wrapper,source:origin.manifest.pipeline,manifestBase:origin.origin.base,tempRoot,network});
19
+ return renderLegacyUnityPreview(wrapper,origin,preflight);
20
+ }
21
+
22
+ async function renderLegacyUnityPreview(wrapper,origin,preflight,lock){
23
+ const manifestPath=origin.origin.path;
24
+ const verified=await verifyPreparedSnapshot({...preflight.supply,manifest:{id:'unity-sdd',version:preflight.supply.version}});
25
+ const {adapters,sharedAdapter}=providerRegistry;
26
+ const layout=planLayout(verified.manifest,origin.manifest,wrapper,{adapters});
27
+ const snapshot={source:structuredClone(preflight.supply.source),commit:preflight.supply.commit,
28
+ path:'.pipeline/snapshots/'+preflight.supply.digest.slice(7),digest:preflight.supply.digest,
29
+ inventoryDigest:preflight.supply.inventoryDigest,origin:structuredClone(origin.origin)};
30
+ const context=()=>({pipeline:structuredClone(verified.manifest),workspace:structuredClone(origin.manifest),
31
+ layout:structuredClone(layout),snapshot:structuredClone(snapshot),files:new Map([...verified.files].map(([p,b])=>[p,Buffer.from(b)]))});
32
+ const requests=[];
33
+ for(const [owner,adapter] of [['shared',sharedAdapter],['codex',adapters.codex],['claude',adapters.claude]]){
34
+ if(owner!=='shared'&&(await adapter.validate(context()))?.valid===false)fail('provider.validation');
35
+ for(const request of await adapter.plan(context())){
36
+ if(request.owner!==owner)fail('plan.adapter-owner');
37
+ assertRequestScope(request,origin.manifest.providers);requests.push(request);
38
+ }
39
+ }
40
+ const observed=await observeTargets(wrapper,requests.map(r=>r.path));
41
+ const entries=[];
42
+ for(const request of requests){
43
+ if(!['AGENTS.md','CLAUDE.md'].includes(request.path))continue;
44
+ const before=observed.find(r=>r.path===request.path)?.bytes;
45
+ if(!Buffer.isBuffer(before)||request.kind!=='file')fail('migration.entry');
46
+ request.takeover={beforeHash:sha256(before),desiredHash:sha256(request.bytes)};
47
+ // Exact original entry content retained in the private approval proposal.
48
+ entries.push({path:request.path,before:before.toString('base64'),after:request.bytes.toString('base64'),...request.takeover});
49
+ }
50
+ if(entries.length!==2)fail('migration.entry');
51
+ const projected=observed.map(item=>{
52
+ const change=preflight.deactivation.targets.find(t=>t.path===item.path);
53
+ if(!change)return item;
54
+ if(item.bytes===null||sha256(item.bytes)!==change.beforeHash)fail('migration.observation-drift');
55
+ return {path:item.path,bytes:Buffer.from(change.after,'base64')};
56
+ });
57
+ const setup=composePlan({pipeline:verified.manifest,workspace:origin.manifest,wrapper,snapshot,
58
+ adapters,requests,observations:projected});
59
+ // All output dependencies, not just the legacy six, must still be unchanged.
60
+ const fresh=await observeTargets(wrapper,requests.map(r=>r.path));
61
+ const hashes=items=>items.map(x=>({path:x.path,hash:x.bytes===null?null:sha256(x.bytes)}));
62
+ if(contractDigest(hashes(fresh))!==contractDigest(hashes(observed)))fail('migration.observation-drift');
63
+ await recheckLegacyUnityPreflight(preflight,wrapper,lock);
64
+ if(contractDigest(await resolveOrigin({wrapper,manifestPath,command:'setup'}))!==contractDigest(origin))fail('migration.origin-drift');
65
+ const body={schemaVersion:1,kind:'legacy-unity-preview',workspace:wrapper,preflight,
66
+ origin:origin.origin,entries,initialObservations:hashes(observed),setup,
67
+ phases:['deactivate-legacy','install-local-delivery'],runtime:'not-run'};
68
+ return {...body,digest:contractDigest(body)};
69
+ }
70
+
71
+ // Genuine approval is supplied by the caller. It binds the complete proposal,
72
+ // not its embedded setup phase. Re-render trusted adapters without acquiring Git.
73
+ // Read-only, no lock or write capability returned; execution must recheck again.
74
+ export async function verifyLegacyUnityPreview(proposal,approval,wrapper,lock){
75
+ requestShape(approval,['decision','previewDigest'],[],'migration.approval');
76
+ requestShape(proposal,['schemaVersion','kind','workspace','preflight','origin','entries','initialObservations','setup','phases','runtime','digest'],[],'migration.preview');
77
+ if(approval.decision!=='approve'||approval.previewDigest!==proposal.digest)fail('migration.approval');
78
+ // Limit serializable proposal complexity and detach before asynchronous reads.
79
+ const copy=parse(JSON.stringify(proposal),'json');
80
+ const {digest,...body}=copy;
81
+ wrapper=absoluteRoot(wrapper);
82
+ if(copy.workspace!==wrapper||copy.schemaVersion!==1||copy.kind!=='legacy-unity-preview'||contractDigest(body)!==digest)fail('migration.preview-binding');
83
+ const origin=await resolveOrigin({wrapper,manifestPath:copy.origin.path,command:'setup'});
84
+ if(origin.manifest.bundles || [...(origin.manifest.providers??[])].sort().join(',')!=='claude,codex'||
85
+ contractDigest(origin.origin)!==contractDigest(copy.origin)||
86
+ contractDigest(origin.manifest.pipeline)!==contractDigest(copy.preflight.supply.source))fail('migration.origin-drift');
87
+ await recheckLegacyUnityPreflight(copy.preflight,wrapper,lock);
88
+ const rebuilt=await renderLegacyUnityPreview(wrapper,origin,copy.preflight,lock);
89
+ if(contractDigest(rebuilt)!==contractDigest(copy))fail('migration.render-drift');
90
+ return rebuilt;
91
+ }