@lmzhen/dsh-evolution-merge 0.30.18 → 0.30.19
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/README.md +21 -4
- package/data/minhash_params.json +1063 -0
- package/lib/index.js +7313 -3272
- package/lib/types/adapters/channel.d.ts +1 -0
- package/lib/types/adapters/channel.js +15 -1
- package/lib/types/adapters/engine-screen.d.ts +1 -1
- package/lib/types/adapters/engine-screen.js +5 -2
- package/lib/types/adapters/model-runner.d.ts +0 -5
- package/lib/types/adapters/model-runner.js +8 -2
- package/lib/types/adapters/model.js +2 -1
- package/lib/types/adapters/select.js +12 -8
- package/lib/types/approve-command.d.ts +12 -1
- package/lib/types/approve-command.js +39 -6
- package/lib/types/authoring-flow.d.ts +16 -0
- package/lib/types/authoring-flow.js +37 -13
- package/lib/types/channel-port.d.ts +3 -1
- package/lib/types/channel-port.js +8 -3
- package/lib/types/command-defs.d.ts +5 -0
- package/lib/types/command-defs.js +33 -0
- package/lib/types/command-surface.d.ts +1 -1
- package/lib/types/command-surface.js +10 -43
- package/lib/types/commands.d.ts +3 -4
- package/lib/types/commands.js +33 -72
- package/lib/types/commit-assembly.d.ts +14 -1
- package/lib/types/commit-assembly.js +65 -9
- package/lib/types/commit-execute.d.ts +7 -1
- package/lib/types/commit-execute.js +54 -22
- package/lib/types/commit-plan.d.ts +7 -1
- package/lib/types/commit-plan.js +4 -0
- package/lib/types/commit-receipt.d.ts +3 -0
- package/lib/types/contracts.d.ts +43 -0
- package/lib/types/doctor.d.ts +11 -1
- package/lib/types/doctor.js +81 -27
- package/lib/types/domain/authoring-manifest.d.ts +26 -0
- package/lib/types/domain/authoring-manifest.js +43 -0
- package/lib/types/domain/engine-criteria.d.ts +10 -0
- package/lib/types/domain/engine-criteria.js +11 -0
- package/lib/types/domain/engine-input.d.ts +10 -0
- package/lib/types/domain/engine-input.js +26 -0
- package/lib/types/domain/errors.d.ts +15 -1
- package/lib/types/domain/errors.js +23 -4
- package/lib/types/domain/evidence-hash.d.ts +3 -0
- package/lib/types/domain/evidence-hash.js +14 -0
- package/lib/types/domain/failure-route.d.ts +14 -0
- package/lib/types/domain/failure-route.js +12 -0
- package/lib/types/domain/live-facts.d.ts +31 -0
- package/lib/types/domain/live-facts.js +54 -0
- package/lib/types/domain/merge-limits.d.ts +18 -0
- package/lib/types/domain/merge-limits.js +22 -0
- package/lib/types/domain/merge-report.d.ts +41 -0
- package/lib/types/domain/merge-report.js +2 -0
- package/lib/types/domain/merge-snapshot.d.ts +99 -0
- package/lib/types/domain/merge-snapshot.js +82 -0
- package/lib/types/domain/pair-ledger.d.ts +34 -0
- package/lib/types/domain/pair-ledger.js +96 -0
- package/lib/types/domain/pre-1b-backup.d.ts +43 -0
- package/lib/types/domain/pre-1b-backup.js +140 -0
- package/lib/types/domain/provider-key.d.ts +2 -0
- package/lib/types/domain/provider-key.js +11 -0
- package/lib/types/domain/read-probe.d.ts +5 -0
- package/lib/types/domain/read-probe.js +11 -0
- package/lib/types/domain/reconcile.d.ts +47 -0
- package/lib/types/domain/reconcile.js +178 -0
- package/lib/types/domain/resolve-pair.d.ts +2 -9
- package/lib/types/domain/resolve-pair.js +2 -15
- package/lib/types/domain/row-v4.d.ts +148 -0
- package/lib/types/domain/row-v4.js +159 -0
- package/lib/types/domain/run-coordinator.d.ts +51 -0
- package/lib/types/domain/run-coordinator.js +45 -0
- package/lib/types/domain/schema4.d.ts +53 -0
- package/lib/types/domain/schema4.js +129 -0
- package/lib/types/domain/selection-policy.d.ts +14 -0
- package/lib/types/domain/selection-policy.js +6 -0
- package/lib/types/domain/skipped-expiry.d.ts +5 -0
- package/lib/types/domain/skipped-expiry.js +16 -0
- package/lib/types/domain/stage-aggregate.d.ts +96 -0
- package/lib/types/domain/stage-aggregate.js +293 -0
- package/lib/types/domain/staging-index.d.ts +1 -0
- package/lib/types/domain/state-bootstrap.d.ts +43 -0
- package/lib/types/domain/state-bootstrap.js +267 -0
- package/lib/types/domain/state-v3.d.ts +3 -14
- package/lib/types/domain/state-v3.js +3 -79
- package/lib/types/domain/undo-journal.d.ts +19 -0
- package/lib/types/domain/undo-journal.js +20 -0
- package/lib/types/domain/undo-receipt.d.ts +18 -0
- package/lib/types/domain/undo-receipt.js +22 -0
- package/lib/types/domain/validation-plan.d.ts +65 -0
- package/lib/types/domain/validation-plan.js +49 -0
- package/lib/types/facts.d.ts +3 -1
- package/lib/types/facts.js +13 -16
- package/lib/types/index.d.ts +2 -0
- package/lib/types/index.js +181 -68
- package/lib/types/merge-command.d.ts +3 -1
- package/lib/types/merge-command.js +10 -6
- package/lib/types/merge-routes.d.ts +10 -5
- package/lib/types/merge-routes.js +72 -7
- package/lib/types/merge-tool.d.ts +1 -1
- package/lib/types/merge-tool.js +2 -2
- package/lib/types/metrics.js +1 -1
- package/lib/types/new-child-authoring.d.ts +2 -0
- package/lib/types/new-child-authoring.js +57 -10
- package/lib/types/params.d.ts +2 -4
- package/lib/types/params.js +13 -0
- package/lib/types/pending-actions.d.ts +2 -2
- package/lib/types/pending-actions.js +4 -4
- package/lib/types/prompts.d.ts +2 -4
- package/lib/types/prompts.js +21 -8
- package/lib/types/read-commands.d.ts +3 -3
- package/lib/types/read-commands.js +19 -28
- package/lib/types/recovery-coordinator.d.ts +34 -0
- package/lib/types/recovery-coordinator.js +84 -0
- package/lib/types/recovery-projection.d.ts +31 -0
- package/lib/types/recovery-projection.js +96 -0
- package/lib/types/resume-command.d.ts +5 -1
- package/lib/types/resume-command.js +46 -13
- package/lib/types/revision.d.ts +17 -0
- package/lib/types/revision.js +39 -3
- package/lib/types/round2-outcome.d.ts +2 -1
- package/lib/types/round2-outcome.js +59 -11
- package/lib/types/run-shell.d.ts +16 -2
- package/lib/types/run-shell.js +49 -5
- package/lib/types/runtime.d.ts +0 -5
- package/lib/types/runtime.js +274 -89
- package/lib/types/scan-command.d.ts +1 -0
- package/lib/types/scan-command.js +2 -0
- package/lib/types/scan-ports.d.ts +18 -0
- package/lib/types/scan-ports.js +49 -23
- package/lib/types/scan-run.d.ts +17 -0
- package/lib/types/scan-run.js +143 -48
- package/lib/types/stage-tool-registry.d.ts +7 -0
- package/lib/types/stage-tool-registry.js +12 -1
- package/lib/types/state.d.ts +5 -5
- package/lib/types/state.js +22 -69
- package/lib/types/status.d.ts +6 -1
- package/lib/types/status.js +47 -0
- package/lib/types/surface/body.d.ts +4 -0
- package/lib/types/surface/body.js +16 -0
- package/lib/types/surface/merge-view.d.ts +14 -0
- package/lib/types/surface/merge-view.js +126 -0
- package/lib/types/undo-operation.d.ts +24 -0
- package/lib/types/undo-operation.js +135 -0
- package/lib/types/wire.d.ts +22 -3
- package/package.json +6 -4
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { readTextOrNull } from "./read-probe.js";
|
|
2
|
+
/**
|
|
3
|
+
* Explicit merge reconciliation: the ONE writer of row-state convergence.
|
|
4
|
+
*
|
|
5
|
+
* Reconcile owns the durable corrections that used to be scattered across startup
|
|
6
|
+
* (skipped TTL expiry, approved+reserved continuation) and reports them in
|
|
7
|
+
* `<home>/merge/reconcile.json`. Snapshots and all four GET routes stay read-only:
|
|
8
|
+
* they may DERIVE a fact (an expired stage, a source that disappeared) but never
|
|
9
|
+
* persist it; only this module and the command lifecycle write rows.
|
|
10
|
+
*/
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
import { transactIo } from '@lmzhen/dsh-evolution-core';
|
|
13
|
+
import { readV4Readiness } from "./schema4.js";
|
|
14
|
+
import { pairLedger } from "./pair-ledger.js";
|
|
15
|
+
import { planSkippedExpiry } from "./skipped-expiry.js";
|
|
16
|
+
import { presentStage, readAllStages, readStageTree, writeStageIndex } from "./stage-aggregate.js";
|
|
17
|
+
import { pendingExpiry } from "./live-facts.js";
|
|
18
|
+
export const RECONCILE_REPORT_VERSION = 1;
|
|
19
|
+
export function reconcileReportPath(home) {
|
|
20
|
+
return join(home, 'merge', 'reconcile.json');
|
|
21
|
+
}
|
|
22
|
+
/** Read the last reconcile report; a malformed report is absent, never a throw. */
|
|
23
|
+
export async function readReconcileReport(io, home) {
|
|
24
|
+
const raw = await readTextOrNull(io, reconcileReportPath(home));
|
|
25
|
+
if (raw === null)
|
|
26
|
+
return null;
|
|
27
|
+
try {
|
|
28
|
+
const parsed = JSON.parse(raw);
|
|
29
|
+
if (parsed.version !== RECONCILE_REPORT_VERSION || (parsed.state !== 'ok' && parsed.state !== 'blocked'))
|
|
30
|
+
return null;
|
|
31
|
+
if (typeof parsed.at !== 'number' || !Array.isArray(parsed.actions))
|
|
32
|
+
return null;
|
|
33
|
+
return parsed;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function blockedReport(now, code, message) {
|
|
40
|
+
return { version: RECONCILE_REPORT_VERSION, at: now, state: 'blocked', code, message, actions: [], notes: ['reconcile wrote nothing: the merge state is not readable'] };
|
|
41
|
+
}
|
|
42
|
+
/** One stage's tree can still serve the round-2 author. */
|
|
43
|
+
async function stageUsable(io, home, entry, now) {
|
|
44
|
+
if (entry.state !== 'reserved' && entry.state !== 'present')
|
|
45
|
+
return false;
|
|
46
|
+
if (entry.expiresAt <= now)
|
|
47
|
+
return false;
|
|
48
|
+
return (await readStageTree(io, home, entry.ref).catch(() => ({ kind: 'absent' }))).kind === 'present';
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Converge the row layer once, atomically, then persist the report.
|
|
52
|
+
* A blocked state writes no rows and no report; every other run is idempotent.
|
|
53
|
+
*/
|
|
54
|
+
export async function reconcileMergeState(options) {
|
|
55
|
+
const now = options.now();
|
|
56
|
+
const ttlDays = options.ttlDays ?? 0;
|
|
57
|
+
const ready = await readV4Readiness(options.io, options.home);
|
|
58
|
+
if (!ready.ok)
|
|
59
|
+
return blockedReport(now, ready.code, ready.message);
|
|
60
|
+
const stages = await readAllStages(options.io, options.home);
|
|
61
|
+
const byRow = new Map(stages.map(entry => [entry.rowId, entry]));
|
|
62
|
+
const usable = new Map();
|
|
63
|
+
const expiredStages = [];
|
|
64
|
+
for (const entry of stages) {
|
|
65
|
+
if (entry.state !== 'reserved' && entry.state !== 'present')
|
|
66
|
+
continue;
|
|
67
|
+
if (entry.expiresAt <= now) {
|
|
68
|
+
expiredStages.push(entry);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
usable.set(entry.rowId, await stageUsable(options.io, options.home, entry, now));
|
|
72
|
+
}
|
|
73
|
+
const actions = [];
|
|
74
|
+
const promote = [];
|
|
75
|
+
await pairLedger(options.io, options.home).transact(tx => {
|
|
76
|
+
for (const row of [...tx.rows()]) {
|
|
77
|
+
if (row.state === 'skipped' && ttlDays > 0 && planSkippedExpiry([row], ttlDays, now).length === 1) {
|
|
78
|
+
const current = tx.rows().find(item => item.id === row.id);
|
|
79
|
+
if (current !== undefined && tx.transition(row.id, { type: 'mark-skipped-expired', at: now }, current.version).ok) {
|
|
80
|
+
actions.push({ kind: 'skipped-expired', rowId: row.id, from: 'skipped', to: 'skipped', code: 'skipped-ttl' });
|
|
81
|
+
}
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (row.state === 'pending' && row.expired === undefined && options.live !== undefined) {
|
|
85
|
+
const current = tx.rows().find(item => item.id === row.id);
|
|
86
|
+
if (current === undefined || current.state !== 'pending' || current.expired !== undefined)
|
|
87
|
+
continue;
|
|
88
|
+
const kind = pendingExpiry(current, options.live);
|
|
89
|
+
if (kind === null)
|
|
90
|
+
continue;
|
|
91
|
+
if (kind === 'source-gone') {
|
|
92
|
+
const removed = tx.remove(row.id, 'superseded', undefined, current.version);
|
|
93
|
+
if (removed.ok)
|
|
94
|
+
actions.push({ kind: 'source-gone', rowId: row.id, from: 'pending', to: 'removed', code: 'source-gone', detail: [row.a, row.b].filter(name => !options.live?.managed(name)).join(', ') });
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const moved = tx.transition(row.id, { type: 'expired', kind }, current.version);
|
|
98
|
+
if (moved.ok)
|
|
99
|
+
actions.push({ kind: 'pending-expired', rowId: row.id, from: 'pending', to: 'pending', code: kind });
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (row.state !== 'approved')
|
|
103
|
+
continue;
|
|
104
|
+
const current = tx.rows().find(item => item.id === row.id);
|
|
105
|
+
if (current === undefined || current.state !== 'approved')
|
|
106
|
+
continue;
|
|
107
|
+
const entry = byRow.get(row.id);
|
|
108
|
+
if (usable.get(row.id) === true) {
|
|
109
|
+
const moved = tx.transition(row.id, { type: 'staged' }, current.version);
|
|
110
|
+
if (moved.ok) {
|
|
111
|
+
actions.push({ kind: 'approved-fused', rowId: row.id, from: 'approved', to: 'fuse', code: 'stage-present' });
|
|
112
|
+
if (entry !== undefined && entry.state === 'reserved')
|
|
113
|
+
promote.push(entry);
|
|
114
|
+
}
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const failure = { stage: 'author', code: 'stage-lost', message: 'stage tree is missing, expired, or lost before round2', retryable: true, retryFrom: 'agent2' };
|
|
118
|
+
const moved = tx.transition(row.id, { type: 'failed', failure }, current.version);
|
|
119
|
+
if (moved.ok)
|
|
120
|
+
actions.push({ kind: 'approved-fused', rowId: row.id, from: 'approved', to: 'fuse', code: 'stage-lost' });
|
|
121
|
+
}
|
|
122
|
+
if (options.managed !== undefined) {
|
|
123
|
+
for (const row of [...tx.rows()]) {
|
|
124
|
+
if (row.state === 'skipped')
|
|
125
|
+
continue;
|
|
126
|
+
if (options.managed.has(row.a) && options.managed.has(row.b))
|
|
127
|
+
continue;
|
|
128
|
+
const current = tx.rows().find(item => item.id === row.id);
|
|
129
|
+
if (current === undefined)
|
|
130
|
+
continue;
|
|
131
|
+
const removed = tx.remove(row.id, 'superseded', undefined, current.version);
|
|
132
|
+
if (removed.ok)
|
|
133
|
+
actions.push({ kind: 'source-gone', rowId: row.id, from: current.state, to: 'removed', code: 'source-gone', detail: [row.a, row.b].filter(name => !options.managed?.has(name)).join(', ') });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return actions.length;
|
|
137
|
+
});
|
|
138
|
+
for (const entry of promote) {
|
|
139
|
+
try {
|
|
140
|
+
await presentStage(options.io, options.home, entry);
|
|
141
|
+
}
|
|
142
|
+
catch { /* the stage index stays reserved; the report still names the row */ }
|
|
143
|
+
}
|
|
144
|
+
// NFR-06: the index (the record the read face and the resume path consult) must agree
|
|
145
|
+
// with the read-side derivation that an expired stage is no longer present.
|
|
146
|
+
for (const entry of expiredStages) {
|
|
147
|
+
try {
|
|
148
|
+
await writeStageIndex(options.io, options.home, { ...entry, state: 'expired' });
|
|
149
|
+
}
|
|
150
|
+
catch { /* report keeps the read-side verdict */ }
|
|
151
|
+
}
|
|
152
|
+
const notes = [];
|
|
153
|
+
if (expiredStages.length > 0)
|
|
154
|
+
notes.push(String(expiredStages.length) + ' expired stage(s) marked in the index');
|
|
155
|
+
if (options.managed === undefined)
|
|
156
|
+
notes.push('source-gone convergence skipped: no live library was supplied');
|
|
157
|
+
if (ttlDays <= 0)
|
|
158
|
+
notes.push('skipped TTL disabled (mergeSkippedTtlDays=0)');
|
|
159
|
+
const report = { version: RECONCILE_REPORT_VERSION, at: now, state: 'ok', actions, notes };
|
|
160
|
+
await transactIo(options.io, reconcileReportPath(options.home), () => JSON.stringify(report, null, 2) + '\n');
|
|
161
|
+
return report;
|
|
162
|
+
}
|
|
163
|
+
/** One-line text face for the CLI/doctor consumers. */
|
|
164
|
+
export function formatReconcileReport(report) {
|
|
165
|
+
if (report.state === 'blocked')
|
|
166
|
+
return 'merge reconcile: blocked ' + (report.code ?? 'merge-state-blocked') + ' — ' + (report.message ?? 'state is not readable');
|
|
167
|
+
const counts = new Map();
|
|
168
|
+
for (const action of report.actions)
|
|
169
|
+
counts.set(action.kind, (counts.get(action.kind) ?? 0) + 1);
|
|
170
|
+
const summary = counts.size === 0 ? 'no actions' : [...counts].map(([kind, count]) => kind + ' ' + String(count)).join(', ');
|
|
171
|
+
const lines = ['merge reconcile: ' + summary + ' (at ' + new Date(report.at).toISOString() + ')'];
|
|
172
|
+
for (const action of report.actions)
|
|
173
|
+
lines.push('- ' + action.rowId + ': ' + action.from + ' → ' + action.to + ' [' + action.code + ']' + (action.detail === undefined ? '' : ' ' + action.detail));
|
|
174
|
+
for (const note of report.notes)
|
|
175
|
+
lines.push('note: ' + note);
|
|
176
|
+
return lines.join('\n');
|
|
177
|
+
}
|
|
178
|
+
//# sourceMappingURL=reconcile.js.map
|
|
@@ -1,11 +1,4 @@
|
|
|
1
|
-
/** Pure
|
|
1
|
+
/** Pure pair-index lookup. No run-record merge fields. */
|
|
2
2
|
import type { PairIndexEntryV2 } from './indexes.ts';
|
|
3
|
-
|
|
4
|
-
export interface PairRunLike {
|
|
5
|
-
readonly id: string;
|
|
6
|
-
readonly pairKey?: string;
|
|
7
|
-
readonly pairs: readonly PairRecord[];
|
|
8
|
-
readonly resultRef?: string;
|
|
9
|
-
}
|
|
10
|
-
export declare function findPairRun(runs: readonly PairRunLike[], pairKey: string): PairIndexEntryV2 | undefined;
|
|
3
|
+
export declare function findPairRun(pairs: readonly PairIndexEntryV2[], pairKey: string): PairIndexEntryV2 | undefined;
|
|
11
4
|
//# sourceMappingURL=resolve-pair.d.ts.map
|
|
@@ -1,17 +1,4 @@
|
|
|
1
|
-
export function findPairRun(
|
|
2
|
-
|
|
3
|
-
if (run.pairKey === pairKey) {
|
|
4
|
-
return { pairKey, runId: run.id, result: 'merged', ...(run.resultRef === undefined ? {} : { reportRef: run.resultRef }) };
|
|
5
|
-
}
|
|
6
|
-
const pair = run.pairs.find(item => item.pairKey === pairKey);
|
|
7
|
-
if (pair !== undefined) {
|
|
8
|
-
return {
|
|
9
|
-
pairKey, runId: run.id, result: pair.result === 'undone' ? 'undone' : pair.result === 'kept' ? 'kept' : pair.result === 'failed' ? 'failed' : pair.result === 'cancelled' ? 'cancelled' : 'merged',
|
|
10
|
-
...(pair.target?.actual === undefined ? {} : { target: pair.target.actual }),
|
|
11
|
-
...(run.resultRef === undefined ? {} : { reportRef: run.resultRef }),
|
|
12
|
-
};
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
return undefined;
|
|
1
|
+
export function findPairRun(pairs, pairKey) {
|
|
2
|
+
return pairs.find(entry => entry.pairKey === pairKey);
|
|
16
3
|
}
|
|
17
4
|
//# sourceMappingURL=resolve-pair.js.map
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/** Batch 1b-2a additive schema4 row types and pure write helpers. */
|
|
2
|
+
import type { MergeDecision, MergeFailure, MergeRow } from '../contracts.ts';
|
|
3
|
+
import type { V3Row } from './row-v3.ts';
|
|
4
|
+
export type RowOriginV4 = 'agent' | 'human' | 'system';
|
|
5
|
+
export type VerifySubStateV4 = 'candidate' | 'screened' | 'unverified';
|
|
6
|
+
export interface RowBaseV4 {
|
|
7
|
+
readonly id: string;
|
|
8
|
+
readonly pairKey: string;
|
|
9
|
+
readonly a: string;
|
|
10
|
+
readonly b: string;
|
|
11
|
+
readonly version: number;
|
|
12
|
+
readonly createdAt: string;
|
|
13
|
+
readonly updatedAt: string;
|
|
14
|
+
readonly origin: RowOriginV4;
|
|
15
|
+
readonly failure?: MergeFailure;
|
|
16
|
+
readonly runId?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface VerifyRowV4 extends RowBaseV4 {
|
|
19
|
+
readonly state: 'verify';
|
|
20
|
+
readonly verifySubState: VerifySubStateV4;
|
|
21
|
+
readonly sourceRevision?: string;
|
|
22
|
+
readonly attempts: number;
|
|
23
|
+
}
|
|
24
|
+
export interface PendingRowV4 extends RowBaseV4 {
|
|
25
|
+
readonly state: 'pending';
|
|
26
|
+
readonly target: string;
|
|
27
|
+
readonly decision: MergeDecision;
|
|
28
|
+
readonly targetRevision?: string;
|
|
29
|
+
readonly sourceRevision?: string;
|
|
30
|
+
readonly promptVersion?: string;
|
|
31
|
+
readonly promptHash?: string;
|
|
32
|
+
readonly expired?: 'target-taken' | 'source-changed';
|
|
33
|
+
readonly expiredAt?: number;
|
|
34
|
+
readonly attempts: number;
|
|
35
|
+
}
|
|
36
|
+
export interface ApprovedRowV4 extends RowBaseV4 {
|
|
37
|
+
readonly state: 'approved';
|
|
38
|
+
readonly target: string;
|
|
39
|
+
readonly decision: MergeDecision;
|
|
40
|
+
readonly sourceRevision?: string;
|
|
41
|
+
readonly targetRevision?: string;
|
|
42
|
+
readonly runId?: string;
|
|
43
|
+
readonly attempts: number;
|
|
44
|
+
}
|
|
45
|
+
export interface FuseRowV4 extends RowBaseV4 {
|
|
46
|
+
readonly state: 'fuse';
|
|
47
|
+
readonly target: string;
|
|
48
|
+
readonly decision: MergeDecision;
|
|
49
|
+
readonly sourceRevision?: string;
|
|
50
|
+
readonly targetRevision?: string;
|
|
51
|
+
readonly runId?: string;
|
|
52
|
+
readonly attempts: number;
|
|
53
|
+
}
|
|
54
|
+
export interface SkippedRowV4 extends RowBaseV4 {
|
|
55
|
+
readonly state: 'skipped';
|
|
56
|
+
readonly reason: string;
|
|
57
|
+
readonly skipSource?: string;
|
|
58
|
+
readonly expiredAt?: number;
|
|
59
|
+
readonly attempts: number;
|
|
60
|
+
}
|
|
61
|
+
export type V4Row = VerifyRowV4 | PendingRowV4 | ApprovedRowV4 | FuseRowV4 | SkippedRowV4;
|
|
62
|
+
export type V4Event = {
|
|
63
|
+
readonly type: 'verify-passed';
|
|
64
|
+
readonly decision: MergeDecision;
|
|
65
|
+
readonly target: string;
|
|
66
|
+
readonly sourceRevision?: string;
|
|
67
|
+
readonly promptVersion?: string;
|
|
68
|
+
readonly promptHash?: string;
|
|
69
|
+
} | {
|
|
70
|
+
readonly type: 'approved';
|
|
71
|
+
readonly runId: string;
|
|
72
|
+
} | {
|
|
73
|
+
readonly type: 'auto-approved';
|
|
74
|
+
readonly decision: MergeDecision;
|
|
75
|
+
readonly target: string;
|
|
76
|
+
readonly runId?: string;
|
|
77
|
+
} | {
|
|
78
|
+
readonly type: 'staged';
|
|
79
|
+
} | {
|
|
80
|
+
readonly type: 'committed';
|
|
81
|
+
readonly unitId: CommitUnitRef;
|
|
82
|
+
} | {
|
|
83
|
+
readonly type: 'skipped';
|
|
84
|
+
readonly reason: string;
|
|
85
|
+
readonly origin?: RowOriginV4;
|
|
86
|
+
readonly skipSource?: string;
|
|
87
|
+
} | {
|
|
88
|
+
readonly type: 'mark-skipped-expired';
|
|
89
|
+
readonly at: number;
|
|
90
|
+
} | {
|
|
91
|
+
readonly type: 'expired';
|
|
92
|
+
readonly kind: 'target-taken' | 'source-changed';
|
|
93
|
+
} | {
|
|
94
|
+
readonly type: 'failed';
|
|
95
|
+
readonly failure: MergeFailure;
|
|
96
|
+
} | {
|
|
97
|
+
readonly type: 'restored';
|
|
98
|
+
} | {
|
|
99
|
+
readonly type: 'superseded';
|
|
100
|
+
};
|
|
101
|
+
export interface CommitUnitRef {
|
|
102
|
+
readonly unitId: string;
|
|
103
|
+
readonly evidenceSha256: string;
|
|
104
|
+
}
|
|
105
|
+
export type V4Transition = {
|
|
106
|
+
readonly ok: true;
|
|
107
|
+
readonly row: V4Row | null;
|
|
108
|
+
readonly version: number;
|
|
109
|
+
} | {
|
|
110
|
+
readonly ok: false;
|
|
111
|
+
readonly code: 'version-conflict' | 'illegal-transition';
|
|
112
|
+
readonly message: string;
|
|
113
|
+
};
|
|
114
|
+
export type V4Terminal = 'committed' | 'superseded' | 'restored';
|
|
115
|
+
export type V4Write = {
|
|
116
|
+
readonly kind: 'createVerify';
|
|
117
|
+
readonly row: VerifyRowV4;
|
|
118
|
+
} | {
|
|
119
|
+
readonly kind: 'createSkipped';
|
|
120
|
+
readonly row: SkippedRowV4;
|
|
121
|
+
} | {
|
|
122
|
+
readonly kind: 'transition';
|
|
123
|
+
readonly id: string;
|
|
124
|
+
readonly event: V4Event;
|
|
125
|
+
readonly expectedVersion: number;
|
|
126
|
+
} | {
|
|
127
|
+
readonly kind: 'remove';
|
|
128
|
+
readonly id: string;
|
|
129
|
+
readonly terminal: V4Terminal;
|
|
130
|
+
readonly unit?: CommitUnitRef;
|
|
131
|
+
readonly expectedVersion: number;
|
|
132
|
+
};
|
|
133
|
+
export type V4WriteError = 'row-invalid' | 'row-duplicate' | 'row-not-found' | 'version-conflict' | 'illegal-transition';
|
|
134
|
+
export type V4WriteResult = {
|
|
135
|
+
readonly ok: true;
|
|
136
|
+
readonly rows: readonly V4Row[];
|
|
137
|
+
readonly row: V4Row | null;
|
|
138
|
+
} | {
|
|
139
|
+
readonly ok: false;
|
|
140
|
+
readonly code: V4WriteError;
|
|
141
|
+
readonly message: string;
|
|
142
|
+
};
|
|
143
|
+
export declare function transitionV4(row: V4Row, event: V4Event, expectedVersion: number, at?: string): V4Transition;
|
|
144
|
+
export declare function isV4Row(value: unknown): value is V4Row;
|
|
145
|
+
export declare function v3ToV4(row: V3Row): V4Row;
|
|
146
|
+
export declare function applyV4Write(rows: readonly V4Row[], write: V4Write, at: string): V4WriteResult;
|
|
147
|
+
export declare function v4ToLegacy(row: V4Row): MergeRow;
|
|
148
|
+
//# sourceMappingURL=row-v4.d.ts.map
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
function bad(code, message) { return { ok: false, code, message }; }
|
|
2
|
+
function conflict(message) { return { ok: false, code: 'version-conflict', message }; }
|
|
3
|
+
function illegal(message) { return { ok: false, code: 'illegal-transition', message }; }
|
|
4
|
+
function advance(row, at, patch) { return { ...row, ...patch, version: row.version + 1, updatedAt: at }; }
|
|
5
|
+
export function transitionV4(row, event, expectedVersion, at = new Date().toISOString()) {
|
|
6
|
+
if (row.version !== expectedVersion)
|
|
7
|
+
return conflict('expected ' + String(expectedVersion) + ', got ' + String(row.version));
|
|
8
|
+
switch (row.state) {
|
|
9
|
+
case 'verify':
|
|
10
|
+
if (event.type === 'verify-passed')
|
|
11
|
+
return { ok: true, row: advance(row, at, { state: 'pending', target: event.target, decision: event.decision, ...(event.sourceRevision === undefined ? {} : { sourceRevision: event.sourceRevision }), ...(event.promptVersion === undefined ? {} : { promptVersion: event.promptVersion }), ...(event.promptHash === undefined ? {} : { promptHash: event.promptHash }), attempts: 0 }), version: expectedVersion + 1 };
|
|
12
|
+
if (event.type === 'auto-approved')
|
|
13
|
+
return { ok: true, row: advance(row, at, { state: 'approved', target: event.target, decision: event.decision, attempts: 0, ...(event.runId === undefined ? {} : { runId: event.runId }) }), version: expectedVersion + 1 };
|
|
14
|
+
if (event.type === 'failed')
|
|
15
|
+
return { ok: true, row: advance(row, at, { failure: event.failure, attempts: row.attempts + 1 }), version: expectedVersion + 1 };
|
|
16
|
+
if (event.type === 'skipped')
|
|
17
|
+
return { ok: true, row: advance(row, at, { state: 'skipped', reason: event.reason, attempts: row.attempts, ...(event.skipSource === undefined ? {} : { skipSource: event.skipSource }) }), version: expectedVersion + 1 };
|
|
18
|
+
if (event.type === 'superseded')
|
|
19
|
+
return { ok: true, row: null, version: expectedVersion + 1 };
|
|
20
|
+
return illegal('verify/' + event.type);
|
|
21
|
+
case 'pending':
|
|
22
|
+
if (event.type === 'approved')
|
|
23
|
+
return { ok: true, row: advance(row, at, { state: 'approved', runId: event.runId }), version: expectedVersion + 1 };
|
|
24
|
+
if (event.type === 'auto-approved')
|
|
25
|
+
return { ok: true, row: advance(row, at, { state: 'approved', ...(event.runId === undefined ? {} : { runId: event.runId }) }), version: expectedVersion + 1 };
|
|
26
|
+
if (event.type === 'staged')
|
|
27
|
+
return { ok: true, row: advance(row, at, { state: 'fuse' }), version: expectedVersion + 1 };
|
|
28
|
+
if (event.type === 'skipped')
|
|
29
|
+
return { ok: true, row: advance(row, at, { state: 'skipped', reason: event.reason, ...(event.origin === undefined ? {} : { origin: event.origin }), ...(event.skipSource === undefined ? {} : { skipSource: event.skipSource }) }), version: expectedVersion + 1 };
|
|
30
|
+
if (event.type === 'expired')
|
|
31
|
+
return { ok: true, row: advance(row, at, { expired: event.kind }), version: expectedVersion + 1 };
|
|
32
|
+
if (event.type === 'failed')
|
|
33
|
+
return { ok: true, row: advance(row, at, { failure: event.failure, attempts: row.attempts + 1 }), version: expectedVersion + 1 };
|
|
34
|
+
if (event.type === 'superseded')
|
|
35
|
+
return { ok: true, row: null, version: expectedVersion + 1 };
|
|
36
|
+
return illegal('pending/' + event.type);
|
|
37
|
+
case 'approved':
|
|
38
|
+
if (event.type === 'staged')
|
|
39
|
+
return { ok: true, row: advance(row, at, { state: 'fuse' }), version: expectedVersion + 1 };
|
|
40
|
+
if (event.type === 'committed')
|
|
41
|
+
return { ok: true, row: null, version: expectedVersion + 1 };
|
|
42
|
+
if (event.type === 'skipped')
|
|
43
|
+
return { ok: true, row: advance(row, at, { state: 'skipped', reason: event.reason, ...(event.skipSource === undefined ? {} : { skipSource: event.skipSource }) }), version: expectedVersion + 1 };
|
|
44
|
+
if (event.type === 'failed')
|
|
45
|
+
return event.failure.retryable
|
|
46
|
+
? { ok: true, row: advance(row, at, { state: 'fuse', failure: event.failure, attempts: row.attempts + 1 }), version: expectedVersion + 1 }
|
|
47
|
+
: { ok: true, row: advance(row, at, { state: 'skipped', reason: event.failure.code, failure: event.failure, attempts: row.attempts + 1 }), version: expectedVersion + 1 };
|
|
48
|
+
if (event.type === 'superseded')
|
|
49
|
+
return { ok: true, row: null, version: expectedVersion + 1 };
|
|
50
|
+
return illegal('approved/' + event.type);
|
|
51
|
+
case 'fuse':
|
|
52
|
+
if (event.type === 'committed')
|
|
53
|
+
return { ok: true, row: null, version: expectedVersion + 1 };
|
|
54
|
+
if (event.type === 'skipped')
|
|
55
|
+
return { ok: true, row: advance(row, at, { state: 'skipped', reason: event.reason, ...(event.skipSource === undefined ? {} : { skipSource: event.skipSource }) }), version: expectedVersion + 1 };
|
|
56
|
+
if (event.type === 'failed')
|
|
57
|
+
return event.failure.retryable
|
|
58
|
+
? { ok: true, row: advance(row, at, { state: 'fuse', failure: event.failure, attempts: row.attempts + 1 }), version: expectedVersion + 1 }
|
|
59
|
+
: { ok: true, row: advance(row, at, { state: 'skipped', reason: event.failure.code, failure: event.failure, attempts: row.attempts + 1 }), version: expectedVersion + 1 };
|
|
60
|
+
if (event.type === 'superseded')
|
|
61
|
+
return { ok: true, row: null, version: expectedVersion + 1 };
|
|
62
|
+
return illegal('fuse/' + event.type);
|
|
63
|
+
case 'skipped':
|
|
64
|
+
if (event.type === 'restored' || event.type === 'superseded')
|
|
65
|
+
return { ok: true, row: null, version: expectedVersion + 1 };
|
|
66
|
+
if (event.type === 'mark-skipped-expired')
|
|
67
|
+
return row.expiredAt !== undefined ? { ok: true, row, version: expectedVersion } : { ok: true, row: { ...row, expiredAt: event.at }, version: expectedVersion + 1 };
|
|
68
|
+
return illegal('skipped is terminal');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function object(value) { return value !== null && typeof value === 'object'; }
|
|
72
|
+
function string(value) { return typeof value === 'string' && value.length > 0; }
|
|
73
|
+
function number(value) { return typeof value === 'number' && Number.isFinite(value); }
|
|
74
|
+
export function isV4Row(value) {
|
|
75
|
+
if (!object(value))
|
|
76
|
+
return false;
|
|
77
|
+
if (!string(value.id) || !string(value.pairKey) || !string(value.a) || !string(value.b) || !number(value.version))
|
|
78
|
+
return false;
|
|
79
|
+
if (!string(value.createdAt) || !string(value.updatedAt) || (value.origin !== 'agent' && value.origin !== 'human' && value.origin !== 'system'))
|
|
80
|
+
return false;
|
|
81
|
+
if (typeof value.attempts !== 'number')
|
|
82
|
+
return false;
|
|
83
|
+
if (value.state === 'verify')
|
|
84
|
+
return value.verifySubState === 'candidate' || value.verifySubState === 'screened' || value.verifySubState === 'unverified';
|
|
85
|
+
if (value.state === 'pending' || value.state === 'approved' || value.state === 'fuse')
|
|
86
|
+
return string(value.target) && object(value.decision);
|
|
87
|
+
if (value.state === 'skipped')
|
|
88
|
+
return string(value.reason);
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
export function v3ToV4(row) {
|
|
92
|
+
const base = { id: row.id, pairKey: row.pairKey, a: row.a, b: row.b, version: row.version, createdAt: row.createdAt, updatedAt: row.updatedAt, origin: row.origin, attempts: row.attempts, ...(row.failure === undefined ? {} : { failure: row.failure }), ...(row.runId === undefined ? {} : { runId: row.runId }) };
|
|
93
|
+
if (row.state === 'verify')
|
|
94
|
+
return { ...base, state: 'verify', verifySubState: row.verifySubState, ...(row.sourceRevision === undefined ? {} : { sourceRevision: row.sourceRevision }) };
|
|
95
|
+
if (row.state === 'pending')
|
|
96
|
+
return { ...base, state: 'pending', target: row.target, decision: row.decision, ...(row.targetRevision === undefined ? {} : { targetRevision: row.targetRevision }), ...(row.sourceRevision === undefined ? {} : { sourceRevision: row.sourceRevision }), ...(row.expired === undefined ? {} : { expired: row.expired }) };
|
|
97
|
+
if (row.state === 'approved')
|
|
98
|
+
return { ...base, state: 'approved', target: row.target, decision: row.decision, ...(row.sourceRevision === undefined ? {} : { sourceRevision: row.sourceRevision }), ...(row.runId === undefined ? {} : { runId: row.runId }) };
|
|
99
|
+
if (row.state === 'fuse')
|
|
100
|
+
return { ...base, state: 'fuse', target: row.target, decision: row.decision, ...(row.sourceRevision === undefined ? {} : { sourceRevision: row.sourceRevision }), ...(row.runId === undefined ? {} : { runId: row.runId }) };
|
|
101
|
+
return { ...base, state: 'skipped', reason: row.reason, ...(row.expiredAt === undefined ? {} : { expiredAt: row.expiredAt }) };
|
|
102
|
+
}
|
|
103
|
+
export function applyV4Write(rows, write, at) {
|
|
104
|
+
if (write.kind === 'transition') {
|
|
105
|
+
const index = rows.findIndex(row => row.id === write.id);
|
|
106
|
+
if (index < 0)
|
|
107
|
+
return bad('row-not-found', 'row not found: ' + write.id);
|
|
108
|
+
const moved = transitionV4(rows[index], write.event, write.expectedVersion, at);
|
|
109
|
+
if (!moved.ok)
|
|
110
|
+
return bad(moved.code, moved.message);
|
|
111
|
+
if (moved.row === null)
|
|
112
|
+
return { ok: true, rows: rows.filter((_, atIndex) => atIndex !== index), row: null };
|
|
113
|
+
const next = [...rows];
|
|
114
|
+
next[index] = moved.row;
|
|
115
|
+
return { ok: true, rows: next, row: moved.row };
|
|
116
|
+
}
|
|
117
|
+
if (write.kind === 'remove') {
|
|
118
|
+
const index = rows.findIndex(row => row.id === write.id);
|
|
119
|
+
if (index < 0)
|
|
120
|
+
return bad('row-not-found', 'row not found: ' + write.id);
|
|
121
|
+
const current = rows[index];
|
|
122
|
+
if (write.terminal === 'committed' && (write.unit === undefined || write.unit.unitId.length === 0 || write.unit.evidenceSha256.length === 0))
|
|
123
|
+
return bad('row-invalid', 'committed remove requires commit evidence');
|
|
124
|
+
const event = write.terminal === 'committed' ? { type: 'committed', unitId: write.unit } : { type: write.terminal };
|
|
125
|
+
const moved = transitionV4(current, event, write.expectedVersion, at);
|
|
126
|
+
if (!moved.ok)
|
|
127
|
+
return bad(moved.code, moved.message);
|
|
128
|
+
return moved.row === null ? { ok: true, rows: rows.filter((_, atIndex) => atIndex !== index), row: null } : { ok: true, rows: rows.map((row, atIndex) => atIndex === index ? moved.row : row), row: moved.row };
|
|
129
|
+
}
|
|
130
|
+
if (write.row.version !== 0)
|
|
131
|
+
return bad('row-invalid', 'create requires version 0');
|
|
132
|
+
if (write.kind === 'createVerify' && (write.row.state !== 'verify' || write.row.verifySubState !== 'unverified'))
|
|
133
|
+
return bad('row-invalid', 'createVerify requires verify/unverified');
|
|
134
|
+
if (write.kind === 'createSkipped' && write.row.state !== 'skipped')
|
|
135
|
+
return bad('row-invalid', 'createSkipped requires skipped');
|
|
136
|
+
if (!isV4Row(write.row))
|
|
137
|
+
return bad('row-invalid', 'created row is invalid');
|
|
138
|
+
if (rows.some(row => row.id === write.row.id))
|
|
139
|
+
return bad('row-duplicate', 'duplicate row id: ' + write.row.id);
|
|
140
|
+
return { ok: true, rows: [...rows, write.row], row: write.row };
|
|
141
|
+
}
|
|
142
|
+
export function v4ToLegacy(row) {
|
|
143
|
+
const queue = row.state === 'verify' ? 'verify' : row.state === 'pending' ? 'pending' : row.state === 'skipped' ? 'skipped' : 'fuse';
|
|
144
|
+
const target = row.state === 'pending' || row.state === 'approved' || row.state === 'fuse' ? { requested: row.target } : undefined;
|
|
145
|
+
return {
|
|
146
|
+
id: row.id, pairKey: row.pairKey, a: row.a, b: row.b, state: row.state, queue,
|
|
147
|
+
...(row.state === 'skipped' ? { reason: row.reason } : {}),
|
|
148
|
+
...(target === undefined ? {} : { target }),
|
|
149
|
+
...(row.failure === undefined ? {} : { failure: row.failure }),
|
|
150
|
+
...(row.state === 'pending' || row.state === 'approved' || row.state === 'fuse' ? { decision: row.decision } : {}),
|
|
151
|
+
...((row.state === 'pending' || row.state === 'approved' || row.state === 'fuse') && row.sourceRevision !== undefined ? { sourceRevision: row.sourceRevision } : {}),
|
|
152
|
+
...((row.state === 'pending' || row.state === 'approved' || row.state === 'fuse') && row.targetRevision !== undefined ? { targetRevision: row.targetRevision } : {}),
|
|
153
|
+
...(row.runId === undefined ? {} : { runId: row.runId }),
|
|
154
|
+
...(row.origin === undefined ? {} : { origin: row.origin }),
|
|
155
|
+
...(row.attempts === undefined ? {} : { attempts: row.attempts }),
|
|
156
|
+
createdAt: row.createdAt, updatedAt: row.updatedAt,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
//# sourceMappingURL=row-v4.js.map
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { type EvolutionIoLike } from '@lmzhen/dsh-evolution-core';
|
|
2
|
+
export type MergeRunPhase = 'begin' | 'running' | 'awaiting-approval' | 'settled' | 'cancelled' | 'undone';
|
|
3
|
+
export interface MergeRunMeta {
|
|
4
|
+
readonly version: 1;
|
|
5
|
+
readonly runId: string;
|
|
6
|
+
readonly pairCount?: number;
|
|
7
|
+
/** The pair a single-pair command (pair/approve/resume) is working on; absent for scan. */
|
|
8
|
+
readonly pairKey?: string;
|
|
9
|
+
readonly parentRunId?: string;
|
|
10
|
+
readonly phase: MergeRunPhase;
|
|
11
|
+
readonly stopReason?: string;
|
|
12
|
+
readonly approval?: 'pending' | 'approved' | 'rejected';
|
|
13
|
+
readonly reportRef?: string;
|
|
14
|
+
readonly undonePairs?: readonly string[];
|
|
15
|
+
readonly undoneAt?: string;
|
|
16
|
+
readonly degraded?: boolean;
|
|
17
|
+
readonly createdAt: string;
|
|
18
|
+
readonly updatedAt: string;
|
|
19
|
+
}
|
|
20
|
+
export interface PairIndexEntry {
|
|
21
|
+
readonly version: 1;
|
|
22
|
+
readonly runId: string;
|
|
23
|
+
readonly pairKey: string;
|
|
24
|
+
readonly a: string;
|
|
25
|
+
readonly b: string;
|
|
26
|
+
readonly target: string;
|
|
27
|
+
readonly targetRevision?: string;
|
|
28
|
+
readonly sourceRevisions?: Readonly<Record<string, string>>;
|
|
29
|
+
readonly state: 'pending' | 'approved' | 'fused' | 'skipped';
|
|
30
|
+
readonly updatedAt: string;
|
|
31
|
+
}
|
|
32
|
+
export interface RunCoordinatorRead {
|
|
33
|
+
readonly runId: string;
|
|
34
|
+
readonly meta: MergeRunMeta | null;
|
|
35
|
+
readonly pairs: readonly PairIndexEntry[];
|
|
36
|
+
readonly consistency: 'eventual';
|
|
37
|
+
}
|
|
38
|
+
export interface RunCoordinator {
|
|
39
|
+
begin(meta: MergeRunMeta): Promise<void>;
|
|
40
|
+
recordPair(entry: PairIndexEntry): Promise<void>;
|
|
41
|
+
read(runId: string): Promise<RunCoordinatorRead>;
|
|
42
|
+
}
|
|
43
|
+
export declare function runMetaPath(home: string, runId: string): string;
|
|
44
|
+
export declare function writeRunMeta(io: EvolutionIoLike, home: string, meta: MergeRunMeta): Promise<void>;
|
|
45
|
+
export declare function readRunMeta(io: EvolutionIoLike, home: string, runId: string): Promise<MergeRunMeta | null>;
|
|
46
|
+
/** Every run-meta record on disk (the in-flight index for pair/target serialisation). */
|
|
47
|
+
export declare function readRunMetas(io: EvolutionIoLike, home: string): Promise<readonly MergeRunMeta[]>;
|
|
48
|
+
/** The run currently working on this pair, if any (pairKey is written at run begin). */
|
|
49
|
+
export declare function inFlightPairRun(metas: readonly MergeRunMeta[], pairKeyValue: string): MergeRunMeta | undefined;
|
|
50
|
+
export { recordPair } from './pair-index-store.ts';
|
|
51
|
+
//# sourceMappingURL=run-coordinator.d.ts.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { readTextOrNull, listOrEmpty } from "./read-probe.js";
|
|
2
|
+
/** 1b-1 additive types for the merge run lifecycle; no writer is active in this phase. */
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { transactIo } from '@lmzhen/dsh-evolution-core';
|
|
5
|
+
export function runMetaPath(home, runId) { return join(home, 'merge', 'run-meta', encodeURIComponent(runId) + '.json'); }
|
|
6
|
+
export async function writeRunMeta(io, home, meta) {
|
|
7
|
+
await transactIo(io, runMetaPath(home, meta.runId), () => JSON.stringify(meta, null, 2) + '\n');
|
|
8
|
+
}
|
|
9
|
+
export async function readRunMeta(io, home, runId) {
|
|
10
|
+
const raw = await readTextOrNull(io, runMetaPath(home, runId));
|
|
11
|
+
if (raw === null)
|
|
12
|
+
return null;
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(raw);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/** Every run-meta record on disk (the in-flight index for pair/target serialisation). */
|
|
21
|
+
export async function readRunMetas(io, home) {
|
|
22
|
+
const root = join(home, 'merge', 'run-meta');
|
|
23
|
+
const names = await io.exists(root) ? await listOrEmpty(io, root) : [];
|
|
24
|
+
const metas = [];
|
|
25
|
+
for (const name of names) {
|
|
26
|
+
if (!name.endsWith('.json'))
|
|
27
|
+
continue;
|
|
28
|
+
const raw = await readTextOrNull(io, join(root, name));
|
|
29
|
+
if (raw === null)
|
|
30
|
+
continue;
|
|
31
|
+
try {
|
|
32
|
+
const meta = JSON.parse(raw);
|
|
33
|
+
if (typeof meta.runId === 'string' && typeof meta.phase === 'string')
|
|
34
|
+
metas.push(meta);
|
|
35
|
+
}
|
|
36
|
+
catch { /* malformed meta is not an in-flight claim */ }
|
|
37
|
+
}
|
|
38
|
+
return metas;
|
|
39
|
+
}
|
|
40
|
+
/** The run currently working on this pair, if any (pairKey is written at run begin). */
|
|
41
|
+
export function inFlightPairRun(metas, pairKeyValue) {
|
|
42
|
+
return metas.find(meta => meta.phase === 'running' && meta.pairKey === pairKeyValue);
|
|
43
|
+
}
|
|
44
|
+
export { recordPair } from "./pair-index-store.js";
|
|
45
|
+
//# sourceMappingURL=run-coordinator.js.map
|