@lmzhen/dsh-evolution-merge 0.47.0 → 0.49.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/lib/index.js +224 -775
- package/lib/types/adapters/engine-screen.d.ts +0 -3
- package/lib/types/authoring-flow.d.ts +5 -0
- package/lib/types/authoring-flow.js +17 -10
- package/lib/types/commit-execute.js +1 -22
- package/lib/types/contracts.d.ts +0 -1
- package/lib/types/contracts.js +0 -1
- package/lib/types/doctor.d.ts +0 -1
- package/lib/types/doctor.js +2 -31
- package/lib/types/domain/consistency.d.ts +0 -1
- package/lib/types/domain/consistency.js +2 -11
- package/lib/types/domain/merge-limits.d.ts +0 -10
- package/lib/types/domain/merge-limits.js +0 -3
- package/lib/types/domain/merge-snapshot.js +4 -6
- package/lib/types/domain/pair-ledger.d.ts +0 -1
- package/lib/types/domain/row-v3.d.ts +0 -45
- package/lib/types/domain/row-v3.js +0 -68
- package/lib/types/domain/row.d.ts +2 -84
- package/lib/types/domain/row.js +1 -110
- package/lib/types/domain/run-coordinator.d.ts +0 -24
- package/lib/types/domain/run-coordinator.js +0 -1
- package/lib/types/domain/stage-aggregate.d.ts +0 -42
- package/lib/types/domain/stage-aggregate.js +1 -116
- package/lib/types/domain/state-migrate.d.ts +27 -0
- package/lib/types/domain/state-migrate.js +29 -0
- package/lib/types/domain/state.d.ts +1 -53
- package/lib/types/domain/state.js +0 -55
- package/lib/types/domain/undo-journal.d.ts +4 -4
- package/lib/types/domain/undo-journal.js +5 -7
- package/lib/types/index.d.ts +0 -1
- package/lib/types/index.js +8 -14
- package/lib/types/new-child-authoring.js +4 -10
- package/lib/types/prompts.d.ts +0 -1
- package/lib/types/prompts.js +0 -1
- package/lib/types/read-commands.d.ts +0 -1
- package/lib/types/read-commands.js +0 -4
- package/lib/types/recovery-projection.js +0 -3
- package/lib/types/round2-outcome.js +15 -37
- package/lib/types/scan-run.js +0 -3
- package/lib/types/skipped.d.ts +1 -2
- package/lib/types/skipped.js +0 -8
- package/lib/types/staging.d.ts +0 -2
- package/lib/types/staging.js +0 -9
- package/lib/types/state.d.ts +0 -2
- package/lib/types/state.js +0 -13
- package/lib/types/surface/merge-view.d.ts +1 -2
- package/lib/types/surface/merge-view.js +0 -1
- package/lib/types/undo-operation.js +0 -9
- package/lib/types/unit-protocol.d.ts +6 -0
- package/lib/types/unit-protocol.js +72 -102
- package/package.json +10 -4
- package/scripts/merge-backup.mjs +31 -0
- package/scripts/merge-migrate.mjs +163 -0
- package/scripts/merge-restore.mjs +36 -0
- package/lib/types/commit-recovery.d.ts +0 -14
- package/lib/types/commit-recovery.js +0 -96
- package/lib/types/coordinator.d.ts +0 -77
- package/lib/types/coordinator.js +0 -137
- package/lib/types/domain/commit-journal-store.d.ts +0 -6
- package/lib/types/domain/commit-journal-store.js +0 -26
- package/lib/types/domain/commit-journal.d.ts +0 -16
- package/lib/types/domain/commit-journal.js +0 -26
- package/lib/types/domain/indexes.d.ts +0 -21
- package/lib/types/domain/indexes.js +0 -15
- package/lib/types/domain/merge-report.d.ts +0 -41
- package/lib/types/domain/merge-report.js +0 -2
- package/lib/types/domain/pair-index-store.d.ts +0 -6
- package/lib/types/domain/pair-index-store.js +0 -25
- package/lib/types/domain/pair-index.d.ts +0 -9
- package/lib/types/domain/pair-index.js +0 -28
- package/lib/types/domain/resolve-pair.d.ts +0 -4
- package/lib/types/domain/resolve-pair.js +0 -4
- package/lib/types/domain/row-tx.d.ts +0 -27
- package/lib/types/domain/row-tx.js +0 -31
- package/lib/types/domain/run-record.d.ts +0 -45
- package/lib/types/domain/run-record.js +0 -24
- package/lib/types/domain/state-v3.d.ts +0 -4
- package/lib/types/domain/state-v3.js +0 -4
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lmzhen/dsh-evolution-merge",
|
|
3
3
|
"description": "Skill auto-merge domain: screening, two-round authoring, queues and commit orchestration (community build)",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.49.0",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
"type": "module",
|
|
14
14
|
"main": "lib/index.js",
|
|
15
15
|
"types": "lib/types/index.d.ts",
|
|
16
|
+
"bin": {
|
|
17
|
+
"dsh-evolution-merge-migrate": "scripts/merge-migrate.mjs"
|
|
18
|
+
},
|
|
16
19
|
"exports": {
|
|
17
20
|
".": {
|
|
18
21
|
"types": "./lib/types/index.d.ts",
|
|
@@ -26,6 +29,9 @@
|
|
|
26
29
|
"lib/**/*.js",
|
|
27
30
|
"lib/types/**/*.d.ts",
|
|
28
31
|
"data/**/*",
|
|
32
|
+
"scripts/merge-migrate.mjs",
|
|
33
|
+
"scripts/merge-restore.mjs",
|
|
34
|
+
"scripts/merge-backup.mjs",
|
|
29
35
|
"package.json"
|
|
30
36
|
],
|
|
31
37
|
"license": "MIT",
|
|
@@ -34,9 +40,9 @@
|
|
|
34
40
|
},
|
|
35
41
|
"dependencies": {
|
|
36
42
|
"@deepseek-ai/schemastery": "^3.18.5",
|
|
37
|
-
"@lmzhen/dsh-evolution-core": "^0.
|
|
38
|
-
"@lmzhen/dsh-evolution-commands": "^0.
|
|
39
|
-
"@lmzhen/dsh-evolution-merge-engine": "0.
|
|
43
|
+
"@lmzhen/dsh-evolution-core": "^0.49.0",
|
|
44
|
+
"@lmzhen/dsh-evolution-commands": "^0.49.0",
|
|
45
|
+
"@lmzhen/dsh-evolution-merge-engine": "0.49.0"
|
|
40
46
|
},
|
|
41
47
|
"peerDependencies": {
|
|
42
48
|
"@deepseek-ai/cordis": "^4.0.5"
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs'
|
|
3
|
+
import { dirname, join, relative } from 'node:path'
|
|
4
|
+
const args = new Map()
|
|
5
|
+
for (let i = 2; i < process.argv.length; i += 2) args.set(process.argv[i], process.argv[i + 1])
|
|
6
|
+
const home = args.get('--home')
|
|
7
|
+
const out = args.get('--out')
|
|
8
|
+
if (!home || !out) { console.error('usage: node scripts/merge-backup.mjs --home <home> --out <dir> [--quiesce <json>]'); process.exit(2) }
|
|
9
|
+
const quiesce = args.get('--quiesce')
|
|
10
|
+
if (!quiesce) { console.error('backup refused: --quiesce <json> required (stop plugin or fence writer)'); process.exit(2) }
|
|
11
|
+
try { if (JSON.parse(readFileSync(quiesce, 'utf8')).stopped !== true) { console.error('backup refused: writer not stopped'); process.exit(3) } } catch { console.error('backup refused: unreadable quiesce marker'); process.exit(3) }
|
|
12
|
+
const sha = file => createHash('sha256').update(readFileSync(file)).digest('hex')
|
|
13
|
+
const roots = ['merge/merge-state.json', 'runs.json', 'merge/receipts', 'merge/journals', 'merge/runs', 'merge/snapshots']
|
|
14
|
+
const files = []
|
|
15
|
+
function walk(path) {
|
|
16
|
+
if (!existsSync(path)) return
|
|
17
|
+
const stat = statSync(path)
|
|
18
|
+
if (stat.isDirectory()) { for (const name of readdirSync(path)) walk(join(path, name)); return }
|
|
19
|
+
const rel = relative(home, path).replace(/\\/g, '/')
|
|
20
|
+
files.push({ path: rel, bytes: stat.size, sha256: sha(path) })
|
|
21
|
+
}
|
|
22
|
+
for (const root of roots) walk(join(home, root))
|
|
23
|
+
mkdirSync(out, { recursive: true })
|
|
24
|
+
for (const file of files) {
|
|
25
|
+
const target = join(out, file.path)
|
|
26
|
+
mkdirSync(dirname(target), { recursive: true })
|
|
27
|
+
copyFileSync(join(home, file.path), target)
|
|
28
|
+
}
|
|
29
|
+
const manifest = { schemaVersion: 1, createdAt: new Date().toISOString(), home, quiesce: { path: quiesce, sha256: sha(quiesce) }, files }
|
|
30
|
+
writeFileSync(join(out, 'backup-manifest.json'), JSON.stringify(manifest, null, 2) + '\n')
|
|
31
|
+
console.log('MERGE BACKUP OK files=' + files.length + ' out=' + out)
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* One-time merge state migration: v1/v2 -> v3 -> v4, with a home-level lock.
|
|
4
|
+
*
|
|
5
|
+
* The runtime used to do v2->v3 on mount and v3->v4 on the first write. That left the old
|
|
6
|
+
* readers in the hot path and gave an operator no backup/dry-run/restore story. This CLI is
|
|
7
|
+
* the explicit entry: it reuses the SAME domain migrations (ensureMergeStateReady and
|
|
8
|
+
* normalizePairLedger), so it cannot drift from the runtime.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* merge-migrate --home <dir> [--dry-run] [--apply] [--restore <backup-dir>] [--json]
|
|
12
|
+
*
|
|
13
|
+
* Stop every host that has this home open before --apply/--restore: the lock only serializes
|
|
14
|
+
* this CLI, not a mounted plugin.
|
|
15
|
+
*/
|
|
16
|
+
import { spawnSync } from 'node:child_process'
|
|
17
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs'
|
|
18
|
+
import { dirname, join, resolve } from 'node:path'
|
|
19
|
+
import { fileURLToPath } from 'node:url'
|
|
20
|
+
|
|
21
|
+
const scriptDir = dirname(fileURLToPath(import.meta.url))
|
|
22
|
+
const argv = process.argv.slice(2)
|
|
23
|
+
const value = (flag) => {
|
|
24
|
+
const index = argv.indexOf(flag)
|
|
25
|
+
return index >= 0 ? argv[index + 1] : undefined
|
|
26
|
+
}
|
|
27
|
+
const homeArg = value('--home')
|
|
28
|
+
if (homeArg === undefined || homeArg === '') {
|
|
29
|
+
console.error('usage: merge-migrate --home <dir> [--dry-run] [--apply] [--restore <backup-dir>] [--json]')
|
|
30
|
+
process.exit(2)
|
|
31
|
+
}
|
|
32
|
+
const home = resolve(homeArg)
|
|
33
|
+
const apply = argv.includes('--apply')
|
|
34
|
+
const restoreAt = argv.indexOf('--restore')
|
|
35
|
+
const restore = restoreAt >= 0 ? argv[restoreAt + 1] : undefined
|
|
36
|
+
if (restoreAt >= 0 && (restore === undefined || restore === '' || restore.startsWith('--'))) {
|
|
37
|
+
console.error('usage: merge-migrate --home <dir> [--dry-run] [--apply] [--restore <backup-dir>] [--json]')
|
|
38
|
+
process.exit(2)
|
|
39
|
+
}
|
|
40
|
+
const dryRun = argv.includes('--dry-run') || (!apply && restoreAt < 0)
|
|
41
|
+
const asJson = argv.includes('--json')
|
|
42
|
+
const lockDir = join(home, 'merge', '.migrate-lock')
|
|
43
|
+
const LOCK_TTL_MS = 60 * 60 * 1000
|
|
44
|
+
|
|
45
|
+
function pidAlive(pid) {
|
|
46
|
+
try {
|
|
47
|
+
process.kill(pid, 0)
|
|
48
|
+
return true
|
|
49
|
+
} catch (error) {
|
|
50
|
+
return error?.code === 'EPERM'
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function acquireLock() {
|
|
55
|
+
mkdirSync(join(home, 'merge'), { recursive: true })
|
|
56
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
57
|
+
try {
|
|
58
|
+
mkdirSync(lockDir)
|
|
59
|
+
writeFileSync(join(lockDir, 'owner.json'), JSON.stringify({ pid: process.pid, at: new Date().toISOString() }) + '\n')
|
|
60
|
+
return
|
|
61
|
+
} catch (error) {
|
|
62
|
+
if (error?.code !== 'EEXIST') throw error
|
|
63
|
+
let owner = null
|
|
64
|
+
try { owner = JSON.parse(readFileSync(join(lockDir, 'owner.json'), 'utf8')) } catch { /* stale/incomplete */ }
|
|
65
|
+
const age = Date.now() - statSync(lockDir).mtimeMs
|
|
66
|
+
if (owner !== null && pidAlive(owner.pid) && age < LOCK_TTL_MS) {
|
|
67
|
+
const failure = { ok: false, code: 'migration-locked', message: 'another process owns ' + lockDir + ' (pid ' + String(owner.pid) + ')' }
|
|
68
|
+
if (asJson) console.log(JSON.stringify(failure, null, 2))
|
|
69
|
+
else console.error('merge-migrate: ' + failure.message)
|
|
70
|
+
process.exit(3)
|
|
71
|
+
}
|
|
72
|
+
rmSync(lockDir, { recursive: true, force: true })
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
throw new Error('could not acquire ' + lockDir)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (restore !== undefined && restore !== '') {
|
|
79
|
+
if (dryRun && !apply) {
|
|
80
|
+
const report = { ok: true, dryRun: true, restore }
|
|
81
|
+
if (asJson) console.log(JSON.stringify(report, null, 2))
|
|
82
|
+
else console.log('DRY RUN: would restore ' + restore + ' into ' + home + ' (stop the host first)')
|
|
83
|
+
process.exit(0)
|
|
84
|
+
}
|
|
85
|
+
acquireLock()
|
|
86
|
+
let restoreStatus = 1
|
|
87
|
+
try {
|
|
88
|
+
const result = spawnSync(process.execPath, [join(scriptDir, 'merge-restore.mjs'), '--home', home, '--backup', restore], { stdio: 'inherit' })
|
|
89
|
+
restoreStatus = result.status ?? 1
|
|
90
|
+
} finally {
|
|
91
|
+
rmSync(lockDir, { recursive: true, force: true })
|
|
92
|
+
}
|
|
93
|
+
process.exit(restoreStatus)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function inspect() {
|
|
97
|
+
let raw = null
|
|
98
|
+
try { raw = readFileSync(join(home, 'merge', 'merge-state.json'), 'utf8') } catch { /* unreadable is reported below */ }
|
|
99
|
+
let schema = null
|
|
100
|
+
let rows = 0
|
|
101
|
+
try {
|
|
102
|
+
const parsed = raw === null ? null : JSON.parse(raw)
|
|
103
|
+
schema = parsed?.schemaVersion ?? null
|
|
104
|
+
rows = Array.isArray(parsed?.rows) ? parsed.rows.length : 0
|
|
105
|
+
} catch { /* malformed is a blocker */ }
|
|
106
|
+
const v4 = schema === 4
|
|
107
|
+
? { ok: true, rows }
|
|
108
|
+
: { ok: false, code: 'merge-state-migration-required', message: 'state is not v4' }
|
|
109
|
+
const v3 = schema === 3
|
|
110
|
+
? { ok: true, rows }
|
|
111
|
+
: { ok: false, code: 'merge-state-migration-required', message: 'state is not v3 readable' }
|
|
112
|
+
return { home, state: { present: raw !== null, schema, rows }, v4, v3 }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (dryRun && !apply) {
|
|
116
|
+
const report = { ok: true, dryRun: true, ...await inspect() }
|
|
117
|
+
if (asJson) console.log(JSON.stringify(report, null, 2))
|
|
118
|
+
else console.log('DRY RUN ' + JSON.stringify(report.state) + ' v4=' + JSON.stringify(report.v4) + ' v3=' + JSON.stringify(report.v3))
|
|
119
|
+
process.exit(0)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const core = await import('@lmzhen/dsh-evolution-core')
|
|
123
|
+
const io = core.nodeEvolutionIo()
|
|
124
|
+
const { migrateMergeStateToV4 } = await import('../lib/types/domain/state-migrate.js')
|
|
125
|
+
const { readV4Readiness } = await import('../lib/types/domain/schema4.js')
|
|
126
|
+
|
|
127
|
+
acquireLock()
|
|
128
|
+
let exitCode = 0
|
|
129
|
+
try {
|
|
130
|
+
const before = await inspect()
|
|
131
|
+
const beforeV4 = await readV4Readiness(io, home)
|
|
132
|
+
if (before.state.schema === 4 && beforeV4.ok) {
|
|
133
|
+
const report = { ok: true, applied: false, reason: 'already-v4', before }
|
|
134
|
+
if (asJson) console.log(JSON.stringify(report, null, 2))
|
|
135
|
+
else console.log('MIGRATE OK already v4 rows=' + String(beforeV4.state.rows.length))
|
|
136
|
+
} else {
|
|
137
|
+
const backupDir = value('--backup') ?? join(home, 'merge', '.backup', 'pre-migrate-' + new Date().toISOString().replace(/[:.]/g, '-'))
|
|
138
|
+
const quiesce = join(home, 'merge', '.migrate-quiesce.json')
|
|
139
|
+
writeFileSync(quiesce, JSON.stringify({ stopped: true, at: new Date().toISOString() }) + '\n')
|
|
140
|
+
try {
|
|
141
|
+
const backedUp = spawnSync(process.execPath, [join(scriptDir, 'merge-backup.mjs'), '--home', home, '--out', backupDir, '--quiesce', quiesce], { stdio: 'inherit' })
|
|
142
|
+
if (backedUp.status !== 0) throw new Error('backup failed with exit ' + String(backedUp.status))
|
|
143
|
+
} finally {
|
|
144
|
+
rmSync(quiesce, { force: true })
|
|
145
|
+
}
|
|
146
|
+
const migration = await migrateMergeStateToV4(io, home)
|
|
147
|
+
if (!migration.ok) throw new Error(migration.code + ': ' + migration.message)
|
|
148
|
+
const after = await inspect()
|
|
149
|
+
const afterV4 = await readV4Readiness(io, home)
|
|
150
|
+
if (!afterV4.ok) throw new Error(afterV4.code + ': ' + afterV4.message)
|
|
151
|
+
const report = { ok: true, applied: true, migrated: migration.migrated, backupRef: migration.backupRef, backupDir, before, after }
|
|
152
|
+
if (asJson) console.log(JSON.stringify(report, null, 2))
|
|
153
|
+
else console.log('MIGRATE OK v2/v3 -> v4 rows=' + String(afterV4.state.rows.length) + ' backup=' + backupDir + (migration.backupRef === undefined ? '' : ' pre1b=' + migration.backupRef))
|
|
154
|
+
}
|
|
155
|
+
} catch (error) {
|
|
156
|
+
const failure = { ok: false, code: 'migration-failed', message: error instanceof Error ? error.message : String(error) }
|
|
157
|
+
if (asJson) console.log(JSON.stringify(failure, null, 2))
|
|
158
|
+
else console.error('merge-migrate: ' + failure.message)
|
|
159
|
+
exitCode = 1
|
|
160
|
+
} finally {
|
|
161
|
+
rmSync(lockDir, { recursive: true, force: true })
|
|
162
|
+
}
|
|
163
|
+
process.exit(exitCode)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, statSync } from 'node:fs'
|
|
3
|
+
import { dirname, join, resolve, sep } from 'node:path'
|
|
4
|
+
import { tmpdir } from 'node:os'
|
|
5
|
+
const args = new Map()
|
|
6
|
+
for (let i = 2; i < process.argv.length; i += 2) args.set(process.argv[i], process.argv[i + 1])
|
|
7
|
+
const home = args.get('--home')
|
|
8
|
+
const backup = args.get('--backup')
|
|
9
|
+
if (!home || !backup) { console.error('usage: node scripts/merge-restore.mjs --home <home> --backup <dir>'); process.exit(2) }
|
|
10
|
+
const manifest = JSON.parse(readFileSync(join(backup, 'backup-manifest.json'), 'utf8'))
|
|
11
|
+
const sha = file => createHash('sha256').update(readFileSync(file)).digest('hex')
|
|
12
|
+
const safe = rel => { const target = resolve(home, rel); const base = resolve(home) + sep; if (!target.startsWith(base)) throw new Error('unsafe backup path: ' + rel); return target }
|
|
13
|
+
for (const file of manifest.files) {
|
|
14
|
+
const source = join(backup, file.path)
|
|
15
|
+
if (!existsSync(source)) throw new Error('missing backup file: ' + file.path)
|
|
16
|
+
if (sha(source) !== file.sha256 || statSync(source).size !== file.bytes) throw new Error('backup hash mismatch: ' + file.path)
|
|
17
|
+
safe(file.path)
|
|
18
|
+
}
|
|
19
|
+
const temp = mkdtempSync(join(tmpdir(), 'dsh-merge-restore-'))
|
|
20
|
+
try {
|
|
21
|
+
for (const file of manifest.files) {
|
|
22
|
+
const source = join(backup, file.path)
|
|
23
|
+
const staged = join(temp, file.path)
|
|
24
|
+
mkdirSync(dirname(staged), { recursive: true })
|
|
25
|
+
copyFileSync(source, staged)
|
|
26
|
+
}
|
|
27
|
+
for (const file of manifest.files) {
|
|
28
|
+
const target = safe(file.path)
|
|
29
|
+
mkdirSync(dirname(target), { recursive: true })
|
|
30
|
+
if (existsSync(target)) renameSync(target, target + '.pre-restore')
|
|
31
|
+
copyFileSync(join(temp, file.path), target)
|
|
32
|
+
}
|
|
33
|
+
console.log('MERGE RESTORE OK files=' + manifest.files.length)
|
|
34
|
+
} finally {
|
|
35
|
+
rmSync(temp, { recursive: true, force: true })
|
|
36
|
+
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import type { EvolutionIoLike } from '@lmzhen/dsh-evolution-core';
|
|
2
|
-
import type { CommitJournal } from './domain/commit-journal.ts';
|
|
3
|
-
export interface CommitRecoveryReport {
|
|
4
|
-
readonly runId: string;
|
|
5
|
-
readonly phase: CommitJournal['phase'] | 'unknown';
|
|
6
|
-
readonly action: string;
|
|
7
|
-
readonly ok: boolean;
|
|
8
|
-
readonly target?: string;
|
|
9
|
-
readonly sources?: readonly string[];
|
|
10
|
-
readonly journalPath?: string;
|
|
11
|
-
readonly unitId?: string;
|
|
12
|
-
}
|
|
13
|
-
export declare function recoverCommitJournals(io: EvolutionIoLike, home: string, now: number, isActive?: (runId: string) => boolean): Promise<readonly CommitRecoveryReport[]>;
|
|
14
|
-
//# sourceMappingURL=commit-recovery.d.ts.map
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
/** Startup replay for non-terminal commit journals: report only, never write skills. */
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
import { writeJournal } from "./domain/commit-journal-store.js";
|
|
4
|
-
import { readCommitReceipt } from "./commit-receipt.js";
|
|
5
|
-
import { readUnitEvidence } from '@lmzhen/dsh-evolution-core/write';
|
|
6
|
-
export async function recoverCommitJournals(io, home, now, isActive) {
|
|
7
|
-
const dir = join(home, 'merge', 'journals');
|
|
8
|
-
const reports = [];
|
|
9
|
-
let files = [];
|
|
10
|
-
if (await io.exists(dir)) {
|
|
11
|
-
try {
|
|
12
|
-
files = (await io.list(dir)).filter(name => name.endsWith('.json'));
|
|
13
|
-
}
|
|
14
|
-
catch {
|
|
15
|
-
reports.push({ runId: '(list)', phase: 'unknown', action: 'journal-list-failed', ok: false, journalPath: dir });
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
for (const file of files) {
|
|
19
|
-
const path = join(dir, file);
|
|
20
|
-
try {
|
|
21
|
-
let journal;
|
|
22
|
-
try {
|
|
23
|
-
journal = JSON.parse(await io.readText(path) ?? '');
|
|
24
|
-
}
|
|
25
|
-
catch {
|
|
26
|
-
reports.push({ runId: file.replace(/\.json$/, ''), phase: 'unknown', action: 'journal-unreadable', ok: false, journalPath: path });
|
|
27
|
-
continue;
|
|
28
|
-
}
|
|
29
|
-
if (isActive?.(journal.runId) === true) {
|
|
30
|
-
reports.push({ runId: journal.runId, phase: journal.phase, action: 'skipped-active-run', ok: true, target: journal.target, sources: journal.sources, journalPath: path });
|
|
31
|
-
continue;
|
|
32
|
-
}
|
|
33
|
-
let receipt = null;
|
|
34
|
-
try {
|
|
35
|
-
receipt = await readCommitReceipt(io, home, journal.runId, journal.target);
|
|
36
|
-
}
|
|
37
|
-
catch {
|
|
38
|
-
reports.push({ runId: journal.runId, phase: journal.phase, action: 'receipt-read-failed', ok: false, target: journal.target, sources: journal.sources, journalPath: path });
|
|
39
|
-
continue;
|
|
40
|
-
}
|
|
41
|
-
if (receipt !== null) {
|
|
42
|
-
await writeJournal(io, home, { ...journal, phase: 'committed', unitId: receipt.unitId, updatedAt: new Date(now).toISOString() });
|
|
43
|
-
reports.push({ runId: journal.runId, phase: journal.phase, action: 'receipt-committed', ok: true, target: journal.target, sources: journal.sources, journalPath: path, unitId: receipt.unitId });
|
|
44
|
-
continue;
|
|
45
|
-
}
|
|
46
|
-
if (journal.phase === 'committed') {
|
|
47
|
-
reports.push({ runId: journal.runId, phase: journal.phase, action: 'receipt-missing', ok: false, target: journal.target, sources: journal.sources, journalPath: path });
|
|
48
|
-
continue;
|
|
49
|
-
}
|
|
50
|
-
if (journal.phase === 'failed' || journal.phase === 'rolled-back')
|
|
51
|
-
continue;
|
|
52
|
-
reports.push({
|
|
53
|
-
runId: journal.runId, phase: journal.phase, action: 'manual-required', ok: false,
|
|
54
|
-
target: journal.target, sources: journal.sources, journalPath: path,
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
catch (error) {
|
|
58
|
-
reports.push({ runId: file.replace(/\.json$/, ''), phase: 'unknown', action: 'recovery-error: ' + (error instanceof Error ? error.message : String(error)), ok: false, journalPath: path });
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
const unitDir = join(home, 'merge', 'unit-journals');
|
|
62
|
-
if (await io.exists(unitDir)) {
|
|
63
|
-
let unitFiles = [];
|
|
64
|
-
try {
|
|
65
|
-
unitFiles = (await io.list(unitDir)).filter(name => name.endsWith('.json'));
|
|
66
|
-
}
|
|
67
|
-
catch { /* no unit journals */ }
|
|
68
|
-
for (const file of unitFiles) {
|
|
69
|
-
const path = join(unitDir, file);
|
|
70
|
-
try {
|
|
71
|
-
const journal = JSON.parse(await io.readText(path) ?? '');
|
|
72
|
-
const evidence = await readUnitEvidence(io, home, journal.unitId);
|
|
73
|
-
if (evidence !== null) {
|
|
74
|
-
reports.push({ runId: journal.runId, phase: 'committed', action: 'unit-committed', ok: true, target: journal.target, sources: journal.sources, journalPath: path, unitId: journal.unitId });
|
|
75
|
-
continue;
|
|
76
|
-
}
|
|
77
|
-
if (journal.phase === 'committed') {
|
|
78
|
-
reports.push({ runId: journal.runId, phase: 'committed', action: 'unit-evidence-missing', ok: false, target: journal.target, sources: journal.sources, journalPath: path, unitId: journal.unitId });
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
81
|
-
if (journal.phase === 'failed' || journal.phase === 'rolled-back')
|
|
82
|
-
continue;
|
|
83
|
-
reports.push({ runId: journal.runId, phase: journal.phase, action: 'unit-manual-required', ok: false, target: journal.target, sources: journal.sources, journalPath: path, unitId: journal.unitId });
|
|
84
|
-
}
|
|
85
|
-
catch {
|
|
86
|
-
reports.push({ runId: '(unit)', phase: 'unknown', action: 'unit-journal-unreadable', ok: false, journalPath: path });
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
try {
|
|
91
|
-
await io.writeText(join(home, 'merge', 'recovery.json'), JSON.stringify(reports, null, 2));
|
|
92
|
-
}
|
|
93
|
-
catch { /* report file is best-effort; reports are still returned */ }
|
|
94
|
-
return reports;
|
|
95
|
-
}
|
|
96
|
-
//# sourceMappingURL=commit-recovery.js.map
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pure merge transition table. No io, no run shell, no subagents.
|
|
3
|
-
*
|
|
4
|
-
* Every transition returns the whole next row (or a drop), so the caller can persist it
|
|
5
|
-
* through one state transaction. Invalid transitions are refused by name;
|
|
6
|
-
* a caller never "repairs" a row by guessing which state the operator meant.
|
|
7
|
-
*/
|
|
8
|
-
import { type MergeDecision, type MergeFailure, type MergeRow } from './contracts.ts';
|
|
9
|
-
export type MergeEvent = {
|
|
10
|
-
readonly type: 'candidate';
|
|
11
|
-
readonly a: string;
|
|
12
|
-
readonly b: string;
|
|
13
|
-
readonly at: string;
|
|
14
|
-
} | {
|
|
15
|
-
readonly type: 'round1-keep';
|
|
16
|
-
readonly at: string;
|
|
17
|
-
} | {
|
|
18
|
-
readonly type: 'round1-merge';
|
|
19
|
-
readonly approval: 'on' | 'off';
|
|
20
|
-
readonly decision?: MergeDecision;
|
|
21
|
-
readonly stagingRef?: string;
|
|
22
|
-
readonly stagingExpiresAt?: number;
|
|
23
|
-
readonly sourceRevision?: string;
|
|
24
|
-
readonly targetRevision?: string;
|
|
25
|
-
readonly at: string;
|
|
26
|
-
} | {
|
|
27
|
-
readonly type: 'round1-fail';
|
|
28
|
-
readonly failure: MergeFailure;
|
|
29
|
-
readonly at: string;
|
|
30
|
-
} | {
|
|
31
|
-
readonly type: 'approve';
|
|
32
|
-
readonly runId?: string;
|
|
33
|
-
readonly at: string;
|
|
34
|
-
} | {
|
|
35
|
-
readonly type: 'reject';
|
|
36
|
-
readonly at: string;
|
|
37
|
-
} | {
|
|
38
|
-
readonly type: 'round2-fail';
|
|
39
|
-
readonly failure: MergeFailure;
|
|
40
|
-
readonly at: string;
|
|
41
|
-
} | {
|
|
42
|
-
readonly type: 'round2-cancel';
|
|
43
|
-
readonly at: string;
|
|
44
|
-
} | {
|
|
45
|
-
readonly type: 'round2-success';
|
|
46
|
-
readonly unitId?: string;
|
|
47
|
-
readonly at: string;
|
|
48
|
-
} | {
|
|
49
|
-
readonly type: 'restore';
|
|
50
|
-
readonly at: string;
|
|
51
|
-
} | {
|
|
52
|
-
readonly type: 'source-gone';
|
|
53
|
-
readonly at: string;
|
|
54
|
-
} | {
|
|
55
|
-
readonly type: 'target-taken';
|
|
56
|
-
readonly at: string;
|
|
57
|
-
} | {
|
|
58
|
-
readonly type: 'source-changed';
|
|
59
|
-
readonly at: string;
|
|
60
|
-
} | {
|
|
61
|
-
readonly type: 'gate-keep';
|
|
62
|
-
readonly a: string;
|
|
63
|
-
readonly b: string;
|
|
64
|
-
readonly reason?: string;
|
|
65
|
-
readonly at: string;
|
|
66
|
-
};
|
|
67
|
-
export type TransitionResult = {
|
|
68
|
-
readonly ok: true;
|
|
69
|
-
readonly row: MergeRow | null;
|
|
70
|
-
} | {
|
|
71
|
-
readonly ok: false;
|
|
72
|
-
readonly code: 'invalid-transition';
|
|
73
|
-
readonly message: string;
|
|
74
|
-
};
|
|
75
|
-
/** Legacy v2 projection only; runtime writes must use transitionV3/RowTx. */
|
|
76
|
-
export declare function transitionLegacyForProjection(current: MergeRow | undefined, event: MergeEvent): TransitionResult;
|
|
77
|
-
//# sourceMappingURL=coordinator.d.ts.map
|
package/lib/types/coordinator.js
DELETED
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pure merge transition table. No io, no run shell, no subagents.
|
|
3
|
-
*
|
|
4
|
-
* Every transition returns the whole next row (or a drop), so the caller can persist it
|
|
5
|
-
* through one state transaction. Invalid transitions are refused by name;
|
|
6
|
-
* a caller never "repairs" a row by guessing which state the operator meant.
|
|
7
|
-
*/
|
|
8
|
-
import { pairKey, queueOf, rowIdOf, } from "./contracts.js";
|
|
9
|
-
function invalid(event, state) {
|
|
10
|
-
return {
|
|
11
|
-
ok: false,
|
|
12
|
-
code: 'invalid-transition',
|
|
13
|
-
message: 'cannot apply ' + event.type + ' to ' + (state ?? 'no row'),
|
|
14
|
-
};
|
|
15
|
-
}
|
|
16
|
-
/** Drop fields that must not survive a transition; optional fields stay absent. */
|
|
17
|
-
function base(row, at) {
|
|
18
|
-
return {
|
|
19
|
-
id: row.id,
|
|
20
|
-
pairKey: row.pairKey,
|
|
21
|
-
a: row.a,
|
|
22
|
-
b: row.b,
|
|
23
|
-
state: row.state,
|
|
24
|
-
queue: row.queue,
|
|
25
|
-
createdAt: row.createdAt,
|
|
26
|
-
updatedAt: at,
|
|
27
|
-
...(row.runId === undefined ? {} : { runId: row.runId }),
|
|
28
|
-
...(row.unitId === undefined ? {} : { unitId: row.unitId }),
|
|
29
|
-
...(row.origin === undefined ? {} : { origin: row.origin }),
|
|
30
|
-
...(row.decision === undefined ? {} : { decision: row.decision }),
|
|
31
|
-
...(row.sourceRevision === undefined ? {} : { sourceRevision: row.sourceRevision }),
|
|
32
|
-
...(row.targetRevision === undefined ? {} : { targetRevision: row.targetRevision }),
|
|
33
|
-
...(row.attempts === undefined ? {} : { attempts: row.attempts }),
|
|
34
|
-
...(row.stagingRef === undefined ? {} : { stagingRef: row.stagingRef }),
|
|
35
|
-
...(row.stagingExpiresAt === undefined ? {} : { stagingExpiresAt: row.stagingExpiresAt }),
|
|
36
|
-
...(row.expired === undefined ? {} : { expired: row.expired }),
|
|
37
|
-
...(row.reason === undefined ? {} : { reason: row.reason }),
|
|
38
|
-
...(row.target === undefined ? {} : { target: row.target }),
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
function next(row, at, state, patch = {}) {
|
|
42
|
-
return {
|
|
43
|
-
ok: true,
|
|
44
|
-
row: {
|
|
45
|
-
...base(row, at),
|
|
46
|
-
state,
|
|
47
|
-
queue: queueOf(state),
|
|
48
|
-
...patch,
|
|
49
|
-
},
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
/** Legacy v2 projection only; runtime writes must use transitionV3/RowTx. */
|
|
53
|
-
export function transitionLegacyForProjection(current, event) {
|
|
54
|
-
if (event.type === 'candidate') {
|
|
55
|
-
if (current !== undefined)
|
|
56
|
-
return { ok: true, row: current };
|
|
57
|
-
const key = pairKey(event.a, event.b);
|
|
58
|
-
return {
|
|
59
|
-
ok: true,
|
|
60
|
-
row: {
|
|
61
|
-
id: rowIdOf(key),
|
|
62
|
-
pairKey: key,
|
|
63
|
-
a: event.a,
|
|
64
|
-
b: event.b,
|
|
65
|
-
state: 'verify',
|
|
66
|
-
queue: 'verify',
|
|
67
|
-
createdAt: event.at,
|
|
68
|
-
updatedAt: event.at,
|
|
69
|
-
},
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
if (event.type === 'source-gone')
|
|
73
|
-
return { ok: true, row: null };
|
|
74
|
-
if (event.type === 'gate-keep') {
|
|
75
|
-
if (current !== undefined)
|
|
76
|
-
return { ok: true, row: current };
|
|
77
|
-
const key = pairKey(event.a, event.b);
|
|
78
|
-
return {
|
|
79
|
-
ok: true,
|
|
80
|
-
row: {
|
|
81
|
-
id: rowIdOf(key),
|
|
82
|
-
pairKey: key,
|
|
83
|
-
a: event.a,
|
|
84
|
-
b: event.b,
|
|
85
|
-
state: 'skipped',
|
|
86
|
-
queue: 'skipped',
|
|
87
|
-
origin: 'agent',
|
|
88
|
-
createdAt: event.at,
|
|
89
|
-
updatedAt: event.at,
|
|
90
|
-
},
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
if (current === undefined)
|
|
94
|
-
return invalid(event, undefined);
|
|
95
|
-
switch (event.type) {
|
|
96
|
-
case 'round1-keep':
|
|
97
|
-
return current.state === 'verify' ? next(current, event.at, 'skipped', { origin: 'agent' }) : invalid(event, current.state);
|
|
98
|
-
case 'round1-merge':
|
|
99
|
-
if (current.state !== 'verify')
|
|
100
|
-
return invalid(event, current.state);
|
|
101
|
-
return event.approval === 'on'
|
|
102
|
-
? next(current, event.at, 'pending', { ...(event.decision === undefined ? {} : { decision: event.decision }), ...(event.stagingRef === undefined ? {} : { stagingRef: event.stagingRef }), ...(event.stagingExpiresAt === undefined ? {} : { stagingExpiresAt: event.stagingExpiresAt }), ...(event.sourceRevision === undefined ? {} : { sourceRevision: event.sourceRevision }), ...(event.targetRevision === undefined ? {} : { targetRevision: event.targetRevision }) })
|
|
103
|
-
: next(current, event.at, 'approved', { ...(event.decision === undefined ? {} : { decision: event.decision }), ...(event.stagingRef === undefined ? {} : { stagingRef: event.stagingRef }), ...(event.stagingExpiresAt === undefined ? {} : { stagingExpiresAt: event.stagingExpiresAt }), ...(event.sourceRevision === undefined ? {} : { sourceRevision: event.sourceRevision }), ...(event.targetRevision === undefined ? {} : { targetRevision: event.targetRevision }) });
|
|
104
|
-
case 'round1-fail':
|
|
105
|
-
return current.state === 'verify'
|
|
106
|
-
? next(current, event.at, 'verify', { failure: event.failure, attempts: (current.attempts ?? 0) + 1 })
|
|
107
|
-
: invalid(event, current.state);
|
|
108
|
-
case 'approve':
|
|
109
|
-
return current.state === 'pending'
|
|
110
|
-
? next(current, event.at, 'approved', (event.runId === undefined ? {} : { runId: event.runId }))
|
|
111
|
-
: invalid(event, current.state);
|
|
112
|
-
case 'reject':
|
|
113
|
-
return current.state === 'pending' ? next(current, event.at, 'skipped', { origin: 'human' }) : invalid(event, current.state);
|
|
114
|
-
case 'round2-cancel':
|
|
115
|
-
return current.state === 'fuse' || current.state === 'approved'
|
|
116
|
-
? next(current, event.at, 'approved')
|
|
117
|
-
: invalid(event, current.state);
|
|
118
|
-
case 'round2-fail':
|
|
119
|
-
return current.state === 'fuse' || current.state === 'approved'
|
|
120
|
-
? next(current, event.at, 'approved', { failure: event.failure, attempts: (current.attempts ?? 0) + 1 })
|
|
121
|
-
: invalid(event, current.state);
|
|
122
|
-
case 'round2-success':
|
|
123
|
-
return current.state === 'fuse' || current.state === 'approved'
|
|
124
|
-
? { ok: true, row: null }
|
|
125
|
-
: invalid(event, current.state);
|
|
126
|
-
case 'target-taken':
|
|
127
|
-
case 'source-changed':
|
|
128
|
-
return current.state === 'pending' || current.state === 'approved' || current.state === 'fuse'
|
|
129
|
-
? next(current, event.at, current.state, { expired: event.type })
|
|
130
|
-
: invalid(event, current.state);
|
|
131
|
-
case 'restore':
|
|
132
|
-
return current.state === 'skipped'
|
|
133
|
-
? next(current, event.at, 'verify')
|
|
134
|
-
: invalid(event, current.state);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
//# sourceMappingURL=coordinator.js.map
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import type { EvolutionIoLike } from '@lmzhen/dsh-evolution-core';
|
|
2
|
-
import type { CommitJournal } from './commit-journal.ts';
|
|
3
|
-
export declare function journalPath(home: string, runId: string): string;
|
|
4
|
-
export declare function writeJournal(io: EvolutionIoLike, home: string, journal: CommitJournal): Promise<string>;
|
|
5
|
-
export declare function readJournal(io: EvolutionIoLike, home: string, runId: string): Promise<CommitJournal | null>;
|
|
6
|
-
//# sourceMappingURL=commit-journal-store.d.ts.map
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
/** Durable commit journal: <home>/merge/journals/<run>.json. */
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
export function journalPath(home, runId) {
|
|
4
|
-
return join(home, 'merge', 'journals', runId.replace(/[^A-Za-z0-9_-]+/g, '_') + '.json');
|
|
5
|
-
}
|
|
6
|
-
export async function writeJournal(io, home, journal) {
|
|
7
|
-
const path = journalPath(home, journal.runId);
|
|
8
|
-
const content = JSON.stringify(journal, null, 2);
|
|
9
|
-
if (io.transact !== undefined)
|
|
10
|
-
await io.transact(path, () => content);
|
|
11
|
-
else
|
|
12
|
-
await io.writeText(path, content);
|
|
13
|
-
return path;
|
|
14
|
-
}
|
|
15
|
-
export async function readJournal(io, home, runId) {
|
|
16
|
-
const raw = await io.readText(journalPath(home, runId));
|
|
17
|
-
if (raw === null)
|
|
18
|
-
return null;
|
|
19
|
-
try {
|
|
20
|
-
return JSON.parse(raw);
|
|
21
|
-
}
|
|
22
|
-
catch {
|
|
23
|
-
return null;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
//# sourceMappingURL=commit-journal-store.js.map
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
/** Pure commit-journal phases for target-first merge. */
|
|
2
|
-
export type JournalPhase = 'prepared' | 'target-written' | 'sources-archived' | 'committed' | 'rolled-back' | 'failed';
|
|
3
|
-
export interface CommitJournal {
|
|
4
|
-
readonly runId: string;
|
|
5
|
-
readonly unitId?: string;
|
|
6
|
-
readonly target: string;
|
|
7
|
-
readonly create: boolean;
|
|
8
|
-
readonly sources: readonly string[];
|
|
9
|
-
readonly phase: JournalPhase;
|
|
10
|
-
readonly updatedAt: string;
|
|
11
|
-
}
|
|
12
|
-
export declare function advanceJournal(journal: CommitJournal, phase: JournalPhase, at: string): CommitJournal;
|
|
13
|
-
export type JournalRecovery = 'none' | 'retry-commit' | 'rollback-target' | 'rollback-sources' | 'refuse';
|
|
14
|
-
/** What a restart may do with the last durable phase. */
|
|
15
|
-
export declare function recoveryFor(journal: CommitJournal): JournalRecovery;
|
|
16
|
-
//# sourceMappingURL=commit-journal.d.ts.map
|