@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.
- package/LICENSE +21 -0
- package/README.md +78 -0
- package/docs/collaboration.md +59 -0
- package/docs/config-fields.md +65 -0
- package/docs/contracts.md +149 -0
- package/docs/doctor.md +165 -0
- package/docs/launch.md +50 -0
- package/docs/lifecycle-cli.md +141 -0
- package/docs/lifecycle.md +42 -0
- package/docs/migrations/unity.md +288 -0
- package/docs/native-provider-format.md +149 -0
- package/docs/provider-bundles.md +81 -0
- package/docs/provider-resources.md +37 -0
- package/docs/release.md +25 -0
- package/docs/remove.md +70 -0
- package/docs/repair.md +80 -0
- package/docs/repositories.md +209 -0
- package/docs/repository-manual-recovery.md +102 -0
- package/docs/repository-observations.md +29 -0
- package/docs/repository-recovery.md +204 -0
- package/docs/repository-retention.md +46 -0
- package/docs/repository-transport-budgets.md +26 -0
- package/docs/retention.md +237 -0
- package/docs/source.md +50 -0
- package/docs/switch.md +412 -0
- package/package.json +39 -0
- package/schemas/common.schema.json +251 -0
- package/schemas/inventory.schema.json +15 -0
- package/schemas/operation.schema.json +286 -0
- package/schemas/pipeline.schema.json +317 -0
- package/schemas/state.schema.json +302 -0
- package/schemas/workspace.schema.json +67 -0
- package/src/cli.js +7 -0
- package/src/commands/adopt.js +2 -0
- package/src/commands/bootstrap-recovery.js +90 -0
- package/src/commands/dispatch.js +394 -0
- package/src/commands/init.js +84 -0
- package/src/commands/launch.js +69 -0
- package/src/commands/migration-apply.js +53 -0
- package/src/commands/migration.js +50 -0
- package/src/commands/repositories.js +61 -0
- package/src/commands/repository-abandon.js +16 -0
- package/src/commands/repository-ancestors.js +21 -0
- package/src/commands/repository-locks.js +77 -0
- package/src/contracts/parse.js +57 -0
- package/src/contracts/semantic.js +240 -0
- package/src/contracts/validate.js +21 -0
- package/src/launch/grok.js +20 -0
- package/src/migrations/legacy-unity-begin.js +35 -0
- package/src/migrations/legacy-unity-compensate.js +82 -0
- package/src/migrations/legacy-unity-deactivate.js +60 -0
- package/src/migrations/legacy-unity-deactivation-resume-apply.js +62 -0
- package/src/migrations/legacy-unity-deactivation-resume.js +81 -0
- package/src/migrations/legacy-unity-finalize.js +88 -0
- package/src/migrations/legacy-unity-install-recovery.js +76 -0
- package/src/migrations/legacy-unity-install-resume.js +60 -0
- package/src/migrations/legacy-unity-install.js +88 -0
- package/src/migrations/legacy-unity-lease.js +136 -0
- package/src/migrations/legacy-unity-preflight.js +67 -0
- package/src/migrations/legacy-unity-preview.js +91 -0
- package/src/migrations/legacy-unity-resume-apply.js +39 -0
- package/src/migrations/legacy-unity-resume.js +41 -0
- package/src/migrations/legacy-unity-resumed-evidence.js +88 -0
- package/src/migrations/legacy-unity.js +99 -0
- package/src/operations/apply.js +576 -0
- package/src/operations/backup.js +94 -0
- package/src/operations/bootstrap-lock.js +87 -0
- package/src/operations/bootstrap-owner-retirement.js +121 -0
- package/src/operations/bundle-update.js +54 -0
- package/src/operations/config-fields.js +24 -0
- package/src/operations/continuation-lifecycle.js +77 -0
- package/src/operations/doctor.js +214 -0
- package/src/operations/history.js +170 -0
- package/src/operations/installer-identity.js +49 -0
- package/src/operations/journal.js +142 -0
- package/src/operations/lifecycle.js +133 -0
- package/src/operations/lineage-guard.js +20 -0
- package/src/operations/lock.js +109 -0
- package/src/operations/maintenance.js +62 -0
- package/src/operations/migration-pending.js +22 -0
- package/src/operations/ownership.js +122 -0
- package/src/operations/plan.js +278 -0
- package/src/operations/reconciliation.js +112 -0
- package/src/operations/recovery-lease.js +66 -0
- package/src/operations/remove.js +140 -0
- package/src/operations/repair.js +188 -0
- package/src/operations/repository-abandon.js +192 -0
- package/src/operations/repository-ancestors.js +158 -0
- package/src/operations/repository-apply.js +122 -0
- package/src/operations/repository-authorization.js +48 -0
- package/src/operations/repository-bootstrap-continuation.js +190 -0
- package/src/operations/repository-bootstrap-reconcile.js +106 -0
- package/src/operations/repository-bootstrap-recover.js +114 -0
- package/src/operations/repository-bootstrap.js +82 -0
- package/src/operations/repository-clone.js +63 -0
- package/src/operations/repository-history.js +108 -0
- package/src/operations/repository-inputs.js +41 -0
- package/src/operations/repository-journal.js +127 -0
- package/src/operations/repository-lock-reconcile.js +401 -0
- package/src/operations/repository-pending.js +29 -0
- package/src/operations/repository-reconcile.js +182 -0
- package/src/operations/repository-resumption-approvals.js +77 -0
- package/src/operations/repository-retention-apply.js +75 -0
- package/src/operations/repository-retention.js +137 -0
- package/src/operations/repository-workspace.js +78 -0
- package/src/operations/retention-apply.js +133 -0
- package/src/operations/retention-combined-scan.js +30 -0
- package/src/operations/retention-combined.js +41 -0
- package/src/operations/retention-policy.js +65 -0
- package/src/operations/retention-receipts.js +126 -0
- package/src/operations/retention-scan.js +86 -0
- package/src/operations/retention.js +56 -0
- package/src/operations/state.js +210 -0
- package/src/operations/switch-activate.js +64 -0
- package/src/operations/switch-backups.js +29 -0
- package/src/operations/switch-continuation-journal.js +94 -0
- package/src/operations/switch-continuation-pending.js +60 -0
- package/src/operations/switch-continuation-records.js +109 -0
- package/src/operations/switch-continuation-recovery.js +91 -0
- package/src/operations/switch-continuation-runtime.js +135 -0
- package/src/operations/switch-continuation-store.js +120 -0
- package/src/operations/switch-continuation.js +76 -0
- package/src/operations/switch-execute.js +68 -0
- package/src/operations/switch-inspect.js +45 -0
- package/src/operations/switch-journal-store.js +109 -0
- package/src/operations/switch-journal.js +71 -0
- package/src/operations/switch-lifecycle.js +72 -0
- package/src/operations/switch-pending.js +43 -0
- package/src/operations/switch-preflight.js +73 -0
- package/src/operations/switch-prepare.js +75 -0
- package/src/operations/switch-records.js +60 -0
- package/src/operations/switch-recovery-store.js +74 -0
- package/src/operations/switch.js +52 -0
- package/src/operations/toml-fields.js +133 -0
- package/src/providers/bundles.js +42 -0
- package/src/providers/common-entry.js +16 -0
- package/src/providers/grok.js +26 -0
- package/src/providers/interface.js +25 -0
- package/src/providers/kimi.js +26 -0
- package/src/providers/native.js +155 -0
- package/src/providers/registry.js +10 -0
- package/src/providers/shared.js +51 -0
- package/src/providers/source.js +30 -0
- package/src/source/git.js +303 -0
- package/src/source/inventory.js +87 -0
- package/src/source/repository-budget.js +18 -0
- package/src/source/snapshot.js +37 -0
- package/src/workspace/paths.js +54 -0
- package/src/workspace/profiles.js +8 -0
- package/src/workspace/repositories.js +57 -0
- package/src/workspace/repository-inventory.js +77 -0
- package/src/workspace/repository-observation.js +38 -0
- package/src/workspace/repository-preflight.js +129 -0
- package/src/workspace/repository-preview.js +196 -0
- package/src/workspace/repository-tree.js +57 -0
- package/src/workspace/reserved.js +11 -0
- package/src/workspace/resolve.js +53 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import {randomUUID} from 'node:crypto';
|
|
2
|
+
import {createLegacyUnityCloseoutLease,legacyUnityLeasePayload} from './legacy-unity-lease.js';
|
|
3
|
+
import {prepareLegacyUnityCloseoutResume} from './legacy-unity-resume.js';
|
|
4
|
+
import {acquireWorkspaceLock} from '../operations/lock.js';
|
|
5
|
+
import {writeCheckedFile,deleteCheckedFile} from '../operations/apply.js';
|
|
6
|
+
import {readRecord} from '../operations/state.js';
|
|
7
|
+
import {resolveChild} from '../workspace/paths.js';
|
|
8
|
+
import {contractDigest} from '../contracts/semantic.js';
|
|
9
|
+
import {sha256} from '../source/inventory.js';
|
|
10
|
+
import {fail} from '../contracts/parse.js';
|
|
11
|
+
|
|
12
|
+
// Executes only reconstructed closeout operations, never caller-supplied paths.
|
|
13
|
+
export async function applyLegacyUnityCloseoutResume(proposal,approval,workspace,{boundary=async()=>{}}={}){
|
|
14
|
+
const {lease,preview}=await createLegacyUnityCloseoutLease(proposal,approval,workspace);
|
|
15
|
+
const lock=await acquireWorkspaceLock(workspace,{migrationOperation:lease});
|
|
16
|
+
try{
|
|
17
|
+
const payload=legacyUnityLeasePayload(lease,lock.workspace);
|
|
18
|
+
const check=()=>prepareLegacyUnityCloseoutResume(lock.workspace,preview.recoveryPath);
|
|
19
|
+
if(contractDigest(await check())!==contractDigest(preview))fail('migration.resume-drift');
|
|
20
|
+
const authorizationPath=preview.recoveryPath.replace(/recovery\.json$/,`closeout-authorizations/${randomUUID()}.json`);
|
|
21
|
+
const authorizationBytes=Buffer.from(JSON.stringify({schemaVersion:1,
|
|
22
|
+
kind:'legacy-unity-closeout-authorization',preview,approval:{decision:'approve',previewDigest:preview.digest}})+'\n');
|
|
23
|
+
await writeCheckedFile(lock,authorizationPath,null,authorizationBytes);
|
|
24
|
+
await boundary('authorization-recorded');
|
|
25
|
+
if(contractDigest(await check())!==contractDigest(preview))fail('migration.resume-drift');
|
|
26
|
+
const creation=preview.operations.find(o=>o.action==='create');
|
|
27
|
+
if(creation)await writeCheckedFile(lock,creation.path,null,Buffer.from(JSON.stringify(creation.value)+'\n'));
|
|
28
|
+
await boundary('completion-recorded');
|
|
29
|
+
const final=await check();
|
|
30
|
+
if(contractDigest(final.verified)!==contractDigest(preview.verified)||final.observations.marker!==preview.observations.marker||
|
|
31
|
+
final.observations.recovery!==preview.observations.recovery||final.operations.length!==1||final.operations[0].action!=='delete')fail('migration.resume-drift');
|
|
32
|
+
const completionPath=preview.recoveryPath.replace(/recovery\.json$/,'completion.json');
|
|
33
|
+
const completion=await readRecord(resolveChild(lock.workspace,completionPath));
|
|
34
|
+
if(completion.digest!==final.observations.completion)fail('migration.completion-drift');
|
|
35
|
+
if((await readRecord(resolveChild(lock.workspace,authorizationPath))).digest!==sha256(authorizationBytes))fail('migration.authorization-drift');
|
|
36
|
+
await deleteCheckedFile(lock,payload.markerPath,payload.markerHash,async()=>{});
|
|
37
|
+
return {status:'completed',authorizationPath,completionPath,completionHash:completion.digest,runtime:'not-run'};
|
|
38
|
+
}finally{await lock.release();}
|
|
39
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import {absoluteRoot,resolveChild} from '../workspace/paths.js';
|
|
2
|
+
import {readRecord,observeTargets} from '../operations/state.js';
|
|
3
|
+
import {contractDigest} from '../contracts/semantic.js';
|
|
4
|
+
import {sha256} from '../source/inventory.js';
|
|
5
|
+
import {fail} from '../contracts/parse.js';
|
|
6
|
+
import {inspectInstalledLegacyUnityMigration} from './legacy-unity-finalize.js';
|
|
7
|
+
|
|
8
|
+
// Narrow restart preview: installed phases only, not partial-target recovery.
|
|
9
|
+
// Digests bind observations, not authorization. No lock or write is performed.
|
|
10
|
+
export async function prepareLegacyUnityCloseoutResume(workspace,recoveryPath){
|
|
11
|
+
workspace=absoluteRoot(workspace);
|
|
12
|
+
if(typeof recoveryPath!=='string'||!/^\.pipeline\/migrations\/[a-f0-9-]{36}\/recovery\.json$/.test(recoveryPath))fail('migration.recovery-path');
|
|
13
|
+
const markerPath='.pipeline/migration-operation.json';
|
|
14
|
+
const marker=await readRecord(resolveChild(workspace,markerPath));
|
|
15
|
+
const recovery=await readRecord(resolveChild(workspace,recoveryPath));
|
|
16
|
+
const r=recovery.value,p=r.preview;
|
|
17
|
+
if(r.kind!=='legacy-unity-recovery'||r.schemaVersion!==1||r.workspace!==workspace||
|
|
18
|
+
recoveryPath!==`.pipeline/migrations/${r.id}/recovery.json`||p?.workspace!==workspace)fail('migration.recovery-binding');
|
|
19
|
+
const {digest,...body}=p;
|
|
20
|
+
if(contractDigest(body)!==digest||contractDigest(r.approval)!==contractDigest({decision:'approve',previewDigest:digest}))fail('migration.preview-binding');
|
|
21
|
+
const expected={schemaVersion:1,kind:'legacy-unity-pending',workspace,recoveryPath,recoveryHash:recovery.digest,previewDigest:digest};
|
|
22
|
+
if(contractDigest(marker.value)!==contractDigest(expected))fail('migration.marker-binding');
|
|
23
|
+
const verified=await inspectInstalledLegacyUnityMigration(workspace,{recoveryPath,recoveryHash:recovery.digest});
|
|
24
|
+
const completionPath=recoveryPath.replace(/recovery\.json$/,'completion.json');
|
|
25
|
+
const [completion]=await observeTargets(workspace,[completionPath]);
|
|
26
|
+
const completionValue={schemaVersion:1,kind:'legacy-unity-completion',workspace,...verified,runtime:'not-run'};
|
|
27
|
+
if(completion.bytes!==null){
|
|
28
|
+
const record=await readRecord(resolveChild(workspace,completionPath));
|
|
29
|
+
if(record.digest!==sha256(completion.bytes)||contractDigest(record.value)!==contractDigest(completionValue))fail('migration.completion-drift');
|
|
30
|
+
}
|
|
31
|
+
const observations={marker:marker.digest,recovery:recovery.digest,completion:completion.bytes===null?null:sha256(completion.bytes)};
|
|
32
|
+
if((await readRecord(resolveChild(workspace,markerPath))).digest!==marker.digest||
|
|
33
|
+
contractDigest(await inspectInstalledLegacyUnityMigration(workspace,{recoveryPath,recoveryHash:recovery.digest}))!==contractDigest(verified))fail('migration.resume-drift');
|
|
34
|
+
const [last]=await observeTargets(workspace,[completionPath]);
|
|
35
|
+
if((last.bytes===null?null:sha256(last.bytes))!==observations.completion)fail('migration.resume-drift');
|
|
36
|
+
const result={schemaVersion:1,kind:'legacy-unity-closeout-preview',workspace,recoveryPath,observations,verified,
|
|
37
|
+
operations:[...(completion.bytes===null?[{action:'create',path:completionPath,value:completionValue}]:[]),
|
|
38
|
+
{action:'delete',path:markerPath,beforeHash:marker.digest}],
|
|
39
|
+
status:'needs-approval',executable:false,runtime:'not-run'};
|
|
40
|
+
return {...result,digest:contractDigest(result)};
|
|
41
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import {readdir} from 'node:fs/promises';
|
|
2
|
+
import {resolveChild,inspectDirectory} from '../workspace/paths.js';
|
|
3
|
+
import {readRecord} from '../operations/state.js';
|
|
4
|
+
import {contractDigest} from '../contracts/semantic.js';
|
|
5
|
+
import {fail} from '../contracts/parse.js';
|
|
6
|
+
import {validateLegacyUnityDeactivation} from './legacy-unity.js';
|
|
7
|
+
|
|
8
|
+
// One explicit completed attempt; never choose newest among competing histories.
|
|
9
|
+
export async function verifyLegacyUnityResumedEvidence(workspace,recoveryPath,recoveryHash,original){
|
|
10
|
+
const root=recoveryPath.replace(/recovery\.json$/,'deactivation-resumes');
|
|
11
|
+
if(!(await inspectDirectory(resolveChild(workspace,root))).exists)return null;
|
|
12
|
+
const attempts=await readdir(resolveChild(workspace,root));
|
|
13
|
+
if(attempts.length!==1||! /^[a-f0-9]{64}$/.test(attempts[0]))fail('migration.resume-selection');
|
|
14
|
+
const directory=root+'/'+attempts[0];await inspectDirectory(resolveChild(workspace,directory));
|
|
15
|
+
const names=(await readdir(resolveChild(workspace,directory))).sort();
|
|
16
|
+
if(names.join(',')!=='000000.json,000001.json,000002.json,000003.json,000004.json,000005.json')fail('migration.resume-incomplete');
|
|
17
|
+
const records=[];for(const name of names)records.push(await readRecord(resolveChild(workspace,directory+'/'+name)));
|
|
18
|
+
const authorization=records[0].value.detail,p=authorization?.preview;
|
|
19
|
+
if(!p||p.kind!=='legacy-unity-deactivation-resume-preview'||p.workspace!==workspace||p.recoveryPath!==recoveryPath||
|
|
20
|
+
p.recoveryHash!==recoveryHash||p.status!=='needs-approval'||p.conflicts?.length!==0||p.digest!=='sha256:'+attempts[0])fail('migration.resume-binding');
|
|
21
|
+
const {digest,...body}=p;
|
|
22
|
+
if(contractDigest(body)!==digest||contractDigest(authorization.approval)!==contractDigest({decision:'approve',previewDigest:digest}))fail('migration.resume-binding');
|
|
23
|
+
const marker=await readRecord(resolveChild(workspace,'.pipeline/migration-operation.json'));
|
|
24
|
+
if(marker.digest!==p.markerHash||contractDigest(marker.value)!==contractDigest({schemaVersion:1,kind:'legacy-unity-pending',
|
|
25
|
+
workspace,recoveryPath,recoveryHash,previewDigest:original.digest}))fail('migration.marker-binding');
|
|
26
|
+
const phase=validateLegacyUnityDeactivation(original.preflight.deactivation);
|
|
27
|
+
const expectedObservations=new Map(original.initialObservations.map(o=>[o.path,o.hash]));
|
|
28
|
+
for(const o of original.preflight.observations){
|
|
29
|
+
if(expectedObservations.has(o.path)&&expectedObservations.get(o.path)!==o.sha256)fail('migration.observation-binding');
|
|
30
|
+
expectedObservations.set(o.path,o.sha256);
|
|
31
|
+
}
|
|
32
|
+
if(!Array.isArray(p.observations)||p.observations.length!==expectedObservations.size||new Set(p.observations.map(o=>o.path)).size!==p.observations.length)fail('migration.resume-binding');
|
|
33
|
+
for(const o of p.observations){
|
|
34
|
+
const target=phase.targets.find(t=>t.path===o.path);
|
|
35
|
+
if(!expectedObservations.has(o.path)||(!target&&o.hash!==expectedObservations.get(o.path))||
|
|
36
|
+
(target&&o.hash!==target.beforeHash&&o.hash!==target.afterHash))fail('migration.resume-binding');
|
|
37
|
+
}
|
|
38
|
+
const oldDirectory=recoveryPath.replace(/recovery\.json$/,'deactivation');
|
|
39
|
+
const oldNames=(await inspectDirectory(resolveChild(workspace,oldDirectory))).exists?(await readdir(resolveChild(workspace,oldDirectory))).sort():[];
|
|
40
|
+
if(!Array.isArray(p.events)||oldNames.length!==p.events.length||oldNames.length>6)fail('migration.phase-evidence');
|
|
41
|
+
let previous=null,stopped=false;const intents=new Set(),completed=new Set();
|
|
42
|
+
for(let seq=0;seq<oldNames.length;seq++){
|
|
43
|
+
const name=String(seq).padStart(6,'0')+'.json',relative=oldDirectory+'/'+name;
|
|
44
|
+
if(stopped||oldNames[seq]!==name||p.events[seq].path!==relative)fail('migration.phase-evidence');
|
|
45
|
+
const record=await readRecord(resolveChild(workspace,relative));let expected;
|
|
46
|
+
if(record.digest!==p.events[seq].hash)fail('migration.phase-evidence');
|
|
47
|
+
if(seq===0)expected={kind:'start',detail:null};
|
|
48
|
+
else if(seq===5)expected={kind:'phase-checked',detail:{phase:'deactivate-legacy'}};
|
|
49
|
+
else{
|
|
50
|
+
const t=phase.targets[Math.floor((seq-1)/2)];
|
|
51
|
+
if(seq%2){expected={kind:'intent',detail:{path:t.path,beforeHash:t.beforeHash,afterHash:t.afterHash}};intents.add(t.path);}
|
|
52
|
+
else{
|
|
53
|
+
const d=record.value.detail;
|
|
54
|
+
if(!d||!['completed','failed','uncertain'].includes(d.status)||!(d.observedHash===null||/^sha256:[a-f0-9]{64}$/.test(d.observedHash))||
|
|
55
|
+
d.status==='completed'&&d.observedHash!==t.afterHash||d.status==='failed'&&d.observedHash!==t.beforeHash)fail('migration.phase-evidence');
|
|
56
|
+
stopped=d.status!=='completed';if(!stopped)completed.add(t.path);
|
|
57
|
+
expected={kind:'outcome',detail:{path:t.path,status:d.status,observedHash:d.observedHash}};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if(contractDigest(record.value)!==contractDigest({schemaVersion:1,seq,previous,recoveryHash,...expected}))fail('migration.phase-evidence');
|
|
61
|
+
previous=record.digest;
|
|
62
|
+
}
|
|
63
|
+
const operations=phase.targets.map(t=>{
|
|
64
|
+
const hash=p.observations.find(o=>o.path===t.path).hash;
|
|
65
|
+
const state=hash===t.beforeHash?(t.beforeHash===t.afterHash?'unchanged-disabled':'before'):'after';
|
|
66
|
+
if(state==='after'&&!intents.has(t.path)||state==='before'&&completed.has(t.path))fail('migration.resume-binding');
|
|
67
|
+
return {path:t.path,action:state==='before'?'write-disabled':'confirm-observed',state,beforeHash:hash,afterHash:t.afterHash};
|
|
68
|
+
});
|
|
69
|
+
if(contractDigest(p.operations)!==contractDigest(operations))fail('migration.resume-binding');
|
|
70
|
+
const classifications=operations.map(o=>({provider:phase.targets.find(t=>t.path===o.path).provider,path:o.path,state:o.state}));
|
|
71
|
+
if(contractDigest(p.classifications)!==contractDigest(classifications))fail('migration.resume-binding');
|
|
72
|
+
const expected=[{kind:'authorization',detail:authorization}];
|
|
73
|
+
for(const o of operations)expected.push({kind:'intent',detail:o},{kind:'outcome',detail:{path:o.path,
|
|
74
|
+
status:o.action==='write-disabled'?'written':'confirmed-observed',observedHash:o.afterHash}});
|
|
75
|
+
expected.push({kind:'phase-observed',detail:{phase:'deactivate-legacy',recoveryHash}});
|
|
76
|
+
previous=null;
|
|
77
|
+
for(let seq=0;seq<records.length;seq++){
|
|
78
|
+
if(contractDigest(records[seq].value)!==contractDigest({schemaVersion:1,seq,previous,...expected[seq]}))fail('migration.resume-evidence');
|
|
79
|
+
previous=records[seq].digest;
|
|
80
|
+
}
|
|
81
|
+
for(let i=0;i<names.length;i++)if((await readRecord(resolveChild(workspace,directory+'/'+names[i]))).digest!==records[i].digest)fail('migration.journal-drift');
|
|
82
|
+
if((await readdir(resolveChild(workspace,root))).sort().join(',')!==attempts.sort().join(',')||
|
|
83
|
+
(await readdir(resolveChild(workspace,directory))).sort().join(',')!==names.join(','))fail('migration.journal-drift');
|
|
84
|
+
for(const e of p.events)if((await readRecord(resolveChild(workspace,e.path))).digest!==e.hash)fail('migration.journal-drift');
|
|
85
|
+
const oldFinal=(await inspectDirectory(resolveChild(workspace,oldDirectory))).exists?(await readdir(resolveChild(workspace,oldDirectory))).sort():[];
|
|
86
|
+
if(oldFinal.join(',')!==oldNames.join(','))fail('migration.journal-drift');
|
|
87
|
+
return {directory,head:previous};
|
|
88
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// Pure preparation; no filesystem, authorization, installation or rollback.
|
|
2
|
+
import {parseTOML, getStaticTOMLValue} from 'toml-eslint-parser';
|
|
3
|
+
import {parseDocument} from 'yaml';
|
|
4
|
+
import {isDeepStrictEqual} from 'node:util';
|
|
5
|
+
import {createHash} from 'node:crypto';
|
|
6
|
+
import {parse, fail, MAX_INPUT_BYTES} from '../contracts/parse.js';
|
|
7
|
+
import {utf8} from '../source/inventory.js';
|
|
8
|
+
import {contractDigest} from '../contracts/semantic.js';
|
|
9
|
+
import {requestShape} from '../operations/ownership.js';
|
|
10
|
+
const id='unity-sdd-pipeline@unity-sdd';
|
|
11
|
+
const hash=b=>'sha256:'+createHash('sha256').update(b).digest('hex');
|
|
12
|
+
const object=v=>v!==null&&typeof v==='object'&&!Array.isArray(v);
|
|
13
|
+
function toml(text){try{const ast=parseTOML(text,{tomlVersion:'1.0'});return {ast,value:getStaticTOMLValue(ast)};}catch{fail('migration.toml');}}
|
|
14
|
+
const keys=k=>k.keys.map(x=>x.type==='TOMLBare'?x.name:x.value);
|
|
15
|
+
export function prepareLegacyUnityOptOut(provider,bytes){
|
|
16
|
+
if(!['codex','claude'].includes(provider))fail('migration.provider');
|
|
17
|
+
if(!Buffer.isBuffer(bytes)||bytes.length>MAX_INPUT_BYTES)fail('migration.input');
|
|
18
|
+
const text=utf8(bytes);let value,range,read;
|
|
19
|
+
if(provider==='codex'){
|
|
20
|
+
const parsed=toml(text);value=parsed.value;read=s=>toml(s).value;
|
|
21
|
+
const matches=[];
|
|
22
|
+
const pair=(node,base)=>{if(isDeepStrictEqual([...base,...keys(node.key)],['plugins',id,'enabled']))matches.push(node.value.range);};
|
|
23
|
+
for(const node of parsed.ast.body[0].body){
|
|
24
|
+
if(node.type==='TOMLKeyValue')pair(node,[]);
|
|
25
|
+
else if(node.type==='TOMLTable'&&node.kind==='standard')for(const item of node.body)pair(item,node.resolvedKey);
|
|
26
|
+
}
|
|
27
|
+
if(!object(value.plugins)||!object(value.plugins[id])||typeof value.plugins[id].enabled!=='boolean')fail('migration.activation-missing');
|
|
28
|
+
if(value.plugins[id].enabled){if(matches.length!==1)fail('migration.toml-layout');range=matches[0];}
|
|
29
|
+
value.plugins[id].enabled=false;
|
|
30
|
+
}else{
|
|
31
|
+
value=parse(text,'json');read=s=>parse(s,'json');
|
|
32
|
+
if(!object(value)||!object(value.enabledPlugins)||typeof value.enabledPlugins[id]!=='boolean')fail('migration.activation-missing');
|
|
33
|
+
if(value.enabledPlugins[id])range=parseDocument(text).getIn(['enabledPlugins',id],true)?.range?.slice(0,2);
|
|
34
|
+
value.enabledPlugins[id]=false;
|
|
35
|
+
}
|
|
36
|
+
let output=text;
|
|
37
|
+
if(range){if(text.slice(...range)!=='true')fail('migration.layout');output=text.slice(0,range[0])+'false'+text.slice(range[1]);}
|
|
38
|
+
if(!isDeepStrictEqual(read(output),value))fail('migration.postcondition');
|
|
39
|
+
const after=Buffer.from(output);
|
|
40
|
+
return {provider,path:provider==='codex'?'.codex/config.toml':'.claude/settings.local.json',beforeHash:hash(bytes),afterHash:hash(after),changed:!bytes.equals(after),bytes:after};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Configuration-only phase: entry replacement and installation are deliberately
|
|
44
|
+
// NOT encoded here. The outer migration coordinator must prefetch first and bind
|
|
45
|
+
// this phase to its complete approved migration, using the existing workspace lock.
|
|
46
|
+
export function prepareLegacyUnityDeactivation(observations){
|
|
47
|
+
if(!Array.isArray(observations)||observations.length!==2)fail('migration.observations');
|
|
48
|
+
const found=new Map();
|
|
49
|
+
for(const item of observations){
|
|
50
|
+
requestShape(item,['provider','bytes'],[],'migration.observations');
|
|
51
|
+
if(found.has(item.provider))fail('migration.observations');
|
|
52
|
+
found.set(item.provider,prepareLegacyUnityOptOut(item.provider,item.bytes));
|
|
53
|
+
}
|
|
54
|
+
if(!found.has('codex')||!found.has('claude'))fail('migration.observations');
|
|
55
|
+
const targets=['codex','claude'].map(provider=>{
|
|
56
|
+
const result=found.get(provider),before=observations.find(x=>x.provider===provider).bytes;
|
|
57
|
+
return {provider,path:result.path,beforeHash:result.beforeHash,afterHash:result.afterHash,
|
|
58
|
+
before:before.toString('base64'),after:result.bytes.toString('base64')};
|
|
59
|
+
});
|
|
60
|
+
const body={schemaVersion:1,kind:'legacy-unity-deactivation',targets};
|
|
61
|
+
return {...body,digest:contractDigest(body)};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function validateLegacyUnityDeactivation(record){
|
|
65
|
+
requestShape(record,['schemaVersion','kind','targets','digest'],[],'migration.record');
|
|
66
|
+
if(record.schemaVersion!==1||record.kind!=='legacy-unity-deactivation'||!Array.isArray(record.targets)||record.targets.length!==2)fail('migration.record');
|
|
67
|
+
const observations=record.targets.map(t=>{
|
|
68
|
+
requestShape(t,['provider','path','beforeHash','afterHash','before','after'],[],'migration.record');
|
|
69
|
+
if(Object.values(t).some(v=>typeof v!=='string'))fail('migration.record');
|
|
70
|
+
if(typeof t.before!=='string'||t.before.length>Math.ceil(MAX_INPUT_BYTES/3)*4)fail('migration.record');
|
|
71
|
+
const bytes=Buffer.from(t.before,'base64');
|
|
72
|
+
if(bytes.toString('base64')!==t.before)fail('migration.record');
|
|
73
|
+
return {provider:t.provider,bytes};
|
|
74
|
+
});
|
|
75
|
+
const rebuilt=prepareLegacyUnityDeactivation(observations);
|
|
76
|
+
// Rebuild transformations; a caller-supplied digest alone grants no scope.
|
|
77
|
+
// The strict record reader returns null-prototype JSON objects. Prototype is
|
|
78
|
+
// not persisted identity; validated keys/values and ordering are.
|
|
79
|
+
if(contractDigest(record)!==contractDigest(rebuilt))fail('migration.binding');
|
|
80
|
+
return rebuilt;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Observation classification only, never an instruction to write or restore.
|
|
84
|
+
// An after-state could have been written by somebody else: hash equality is not
|
|
85
|
+
// ownership or proof that this migration executed it.
|
|
86
|
+
export function inspectLegacyUnityDeactivation(record,observations){
|
|
87
|
+
const checked=validateLegacyUnityDeactivation(record);
|
|
88
|
+
if(!Array.isArray(observations)||observations.length!==2)fail('migration.observations');
|
|
89
|
+
const found=new Map();
|
|
90
|
+
for(const item of observations){
|
|
91
|
+
requestShape(item,['provider','bytes'],[],'migration.observations');
|
|
92
|
+
if(!['codex','claude'].includes(item.provider)||found.has(item.provider)||
|
|
93
|
+
(item.bytes!==null&&(!Buffer.isBuffer(item.bytes)||item.bytes.length>MAX_INPUT_BYTES)))fail('migration.observations');
|
|
94
|
+
found.set(item.provider,item.bytes===null?null:hash(item.bytes));
|
|
95
|
+
}
|
|
96
|
+
return checked.targets.map(t=>({provider:t.provider,path:t.path,
|
|
97
|
+
state:found.get(t.provider)===t.beforeHash?(t.beforeHash===t.afterHash?'unchanged-disabled':'before'):
|
|
98
|
+
found.get(t.provider)===t.afterHash?'after':'conflict'}));
|
|
99
|
+
}
|