@lmzhen/dsh-evolution-merge 0.30.17 → 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 +25 -7
- package/data/minhash_params.json +1063 -0
- package/lib/index.js +7308 -1800
- 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/library.js +4 -1
- package/lib/types/adapters/model-runner.d.ts +0 -5
- package/lib/types/adapters/model-runner.js +8 -2
- package/lib/types/adapters/model.d.ts +31 -0
- package/lib/types/adapters/model.js +47 -0
- package/lib/types/adapters/select.js +12 -8
- package/lib/types/approve-command.d.ts +14 -1
- package/lib/types/approve-command.js +72 -44
- package/lib/types/authoring-flow.d.ts +19 -1
- package/lib/types/authoring-flow.js +49 -15
- 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 +5 -0
- package/lib/types/command-surface.js +11 -0
- package/lib/types/commands.d.ts +3 -4
- package/lib/types/commands.js +33 -72
- package/lib/types/commit-assembly.d.ts +16 -1
- package/lib/types/commit-assembly.js +72 -11
- package/lib/types/commit-execute.d.ts +14 -2
- package/lib/types/commit-execute.js +76 -14
- 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/commit-recovery.d.ts +7 -2
- package/lib/types/commit-recovery.js +73 -96
- package/lib/types/contracts.d.ts +44 -1
- package/lib/types/contracts.js +1 -1
- package/lib/types/coordinator.d.ts +3 -2
- package/lib/types/coordinator.js +4 -3
- package/lib/types/decision.js +1 -1
- package/lib/types/doctor.d.ts +18 -1
- package/lib/types/doctor.js +164 -10
- package/lib/types/domain/authoring-manifest.d.ts +26 -0
- package/lib/types/domain/authoring-manifest.js +43 -0
- package/lib/types/domain/claim.d.ts +4 -1
- package/lib/types/domain/claim.js +2 -2
- package/lib/types/domain/commit-journal-store.js +5 -1
- package/lib/types/domain/commit-journal.js +1 -1
- package/lib/types/domain/consistency.d.ts +22 -0
- package/lib/types/domain/consistency.js +50 -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/ledger.js +10 -1
- 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/migrate-v3.d.ts +20 -0
- package/lib/types/domain/migrate-v3.js +68 -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-tx.d.ts +27 -0
- package/lib/types/domain/row-tx.js +31 -0
- package/lib/types/domain/row-v3-bridge.d.ts +6 -0
- package/lib/types/domain/row-v3-bridge.js +47 -0
- package/lib/types/domain/row-v3.d.ts +113 -0
- package/lib/types/domain/row-v3.js +107 -0
- package/lib/types/domain/row-v4.d.ts +148 -0
- package/lib/types/domain/row-v4.js +159 -0
- package/lib/types/domain/row.d.ts +1 -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 +15 -0
- package/lib/types/domain/staging-index.js +41 -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 +4 -0
- package/lib/types/domain/state-v3.js +4 -0
- 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 +26 -0
- package/lib/types/facts.js +65 -0
- package/lib/types/gate-filter.d.ts +31 -0
- package/lib/types/gate-filter.js +64 -0
- package/lib/types/index.d.ts +7 -2
- package/lib/types/index.js +185 -82
- package/lib/types/ladder.js +1 -1
- package/lib/types/merge-command.d.ts +3 -1
- package/lib/types/merge-command.js +10 -6
- package/lib/types/merge-routes.d.ts +12 -7
- package/lib/types/merge-routes.js +87 -23
- package/lib/types/merge-tool.d.ts +1 -1
- package/lib/types/merge-tool.js +2 -2
- package/lib/types/metrics.d.ts +16 -0
- package/lib/types/metrics.js +27 -0
- package/lib/types/new-child-authoring.d.ts +5 -1
- package/lib/types/new-child-authoring.js +71 -13
- 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 +17 -17
- package/lib/types/prompts.d.ts +2 -4
- package/lib/types/prompts.js +21 -8
- package/lib/types/propose.d.ts +26 -0
- package/lib/types/propose.js +69 -0
- package/lib/types/read-commands.d.ts +4 -4
- package/lib/types/read-commands.js +19 -26
- 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 +7 -1
- package/lib/types/resume-command.js +50 -14
- 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 +68 -12
- package/lib/types/run-shell.d.ts +16 -2
- package/lib/types/run-shell.js +49 -5
- package/lib/types/runtime.d.ts +4 -6
- package/lib/types/runtime.js +341 -86
- package/lib/types/scan-command.d.ts +1 -0
- package/lib/types/scan-command.js +2 -0
- package/lib/types/scan-ports.d.ts +21 -1
- package/lib/types/scan-ports.js +51 -23
- package/lib/types/scan-run.d.ts +17 -0
- package/lib/types/scan-run.js +221 -63
- package/lib/types/screening-port.d.ts +47 -0
- package/lib/types/screening-port.js +93 -0
- package/lib/types/screening.d.ts +30 -0
- package/lib/types/screening.js +75 -0
- 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 +15 -1
- package/lib/types/state.js +39 -17
- package/lib/types/status.d.ts +8 -0
- package/lib/types/status.js +64 -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/target-claim.d.ts +1 -0
- package/lib/types/target-claim.js +63 -18
- package/lib/types/undo-operation.d.ts +24 -0
- package/lib/types/undo-operation.js +135 -0
- package/lib/types/unit-claim.d.ts +5 -0
- package/lib/types/unit-claim.js +15 -0
- package/lib/types/wire-projection.d.ts +36 -0
- package/lib/types/wire-projection.js +69 -0
- package/lib/types/wire.d.ts +273 -0
- package/lib/types/wire.js +3 -0
- package/package.json +10 -4
- package/lib/types/routes.d.ts +0 -70
- package/lib/types/routes.js +0 -78
|
@@ -9,9 +9,20 @@ export class StageToolRegistry {
|
|
|
9
9
|
clear() {
|
|
10
10
|
this.bindings.clear();
|
|
11
11
|
}
|
|
12
|
+
/** Diagnostic view for a refused tool call; names only, no content. */
|
|
13
|
+
snapshot(exec) {
|
|
14
|
+
const childId = childIdOf(exec);
|
|
15
|
+
return { ...(childId === undefined ? {} : { childId }), bound: this.bindings.size, authors: [...this.bindings.values()].filter(context => context.phase === 'author').length, keys: [...this.bindings.keys()] };
|
|
16
|
+
}
|
|
12
17
|
resolve(exec) {
|
|
13
18
|
const childId = childIdOf(exec);
|
|
14
|
-
|
|
19
|
+
const direct = childId === undefined ? undefined : this.bindings.get(childId);
|
|
20
|
+
if (direct !== undefined)
|
|
21
|
+
return direct;
|
|
22
|
+
// New-child sessions may expose a session id that differs from the channel's childId.
|
|
23
|
+
// Only a single author-phase binding is unambiguous; never guess with concurrent bindings.
|
|
24
|
+
const authors = [...this.bindings.values()].filter(context => context.phase === 'author');
|
|
25
|
+
return authors.length === 1 ? authors[0] : undefined;
|
|
15
26
|
}
|
|
16
27
|
}
|
|
17
28
|
function childIdOf(exec) {
|
package/lib/types/state.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { EvolutionIoLike } from '@lmzhen/dsh-evolution-core';
|
|
2
2
|
import { type MergePairState, type MergeQueue, type MergeRow, type MergeState } from './contracts.ts';
|
|
3
|
+
import type { RowWriteResult } from './domain/row-tx.ts';
|
|
4
|
+
import type { SkippedRowV3, V3Event, V3Row, VerifyRowV3 } from './domain/row-v3.ts';
|
|
3
5
|
export declare function mergeStatePath(home: string): string;
|
|
4
6
|
export interface LoadMergeStateResult {
|
|
5
7
|
readonly ok: boolean;
|
|
@@ -8,8 +10,18 @@ export interface LoadMergeStateResult {
|
|
|
8
10
|
}
|
|
9
11
|
/** Pure read: never writes, never deletes a row. */
|
|
10
12
|
export declare function loadMergeState(io: EvolutionIoLike, home: string): Promise<LoadMergeStateResult>;
|
|
13
|
+
/** Schema probe for doctor; the only other reader of mergeStatePath. */
|
|
14
|
+
export declare function loadStateSchema(io: EvolutionIoLike, home: string): Promise<number | null>;
|
|
15
|
+
export interface RowTx {
|
|
16
|
+
rows(): readonly V3Row[];
|
|
17
|
+
createVerify(row: VerifyRowV3): RowWriteResult;
|
|
18
|
+
createSkipped(row: SkippedRowV3): RowWriteResult;
|
|
19
|
+
transition(id: string, event: V3Event, expectedVersion: number): RowWriteResult;
|
|
20
|
+
}
|
|
21
|
+
/** G3-5 runtime write API: only createVerify/createSkipped/transition. v2 writes are rejected. */
|
|
22
|
+
export declare function transactRows<T>(_io: EvolutionIoLike, _home: string, _fn: (tx: RowTx) => T): Promise<T>;
|
|
11
23
|
/** The ONE write point. The callback returns the whole row set, not a delta. */
|
|
12
|
-
export declare function mutateMergeState<T>(
|
|
24
|
+
export declare function mutateMergeState<T>(_io: EvolutionIoLike, _home: string, _now: () => number, _fn: (rows: readonly MergeRow[]) => {
|
|
13
25
|
rows: readonly MergeRow[];
|
|
14
26
|
result: T;
|
|
15
27
|
}): Promise<T>;
|
|
@@ -31,6 +43,8 @@ export type UpsertResult = {
|
|
|
31
43
|
};
|
|
32
44
|
export declare function upsertRow(io: EvolutionIoLike, home: string, now: () => number, row: MergeRow, expect?: UpsertExpect, cas?: string | null): Promise<UpsertResult>;
|
|
33
45
|
export declare function moveRow(io: EvolutionIoLike, home: string, now: () => number, id: string, patch: MoveRowPatch): Promise<MergeRow | undefined>;
|
|
46
|
+
/** G3-5 v3-aware drop: only terminal events. */
|
|
47
|
+
export declare function dropRowV3(_io: EvolutionIoLike, _home: string, _id: string, _event: 'committed' | 'superseded' | 'restored', _unitId?: string): Promise<boolean>;
|
|
34
48
|
export declare function dropRow(io: EvolutionIoLike, home: string, now: () => number, id: string): Promise<boolean>;
|
|
35
49
|
export declare function rowsOf(io: EvolutionIoLike, home: string, queue?: MergeQueue): Promise<readonly MergeRow[]>;
|
|
36
50
|
//# sourceMappingURL=state.d.ts.map
|
package/lib/types/state.js
CHANGED
|
@@ -7,9 +7,10 @@
|
|
|
7
7
|
* mismatch is repaired by the next write, never by a GET.
|
|
8
8
|
*/
|
|
9
9
|
import { join } from 'node:path';
|
|
10
|
-
import { transactIo } from '@lmzhen/dsh-evolution-core';
|
|
11
10
|
import { MERGE_STATE_VERSION, queueOf, } from "./contracts.js";
|
|
12
11
|
import { parseLedger } from "./domain/ledger.js";
|
|
12
|
+
import { parseV4State } from "./domain/schema4.js";
|
|
13
|
+
import { v4ToLegacy } from "./domain/row-v4.js";
|
|
13
14
|
export function mergeStatePath(home) {
|
|
14
15
|
return join(home, 'merge', 'merge-state.json');
|
|
15
16
|
}
|
|
@@ -32,6 +33,20 @@ export async function loadMergeState(io, home) {
|
|
|
32
33
|
catch {
|
|
33
34
|
return { ok: false, state: { schemaVersion: MERGE_STATE_VERSION, rows: [] } };
|
|
34
35
|
}
|
|
36
|
+
if (raw !== null) {
|
|
37
|
+
try {
|
|
38
|
+
const header = JSON.parse(raw);
|
|
39
|
+
if (header.schemaVersion === 4) {
|
|
40
|
+
const v4 = parseV4State(raw);
|
|
41
|
+
if (v4 === null)
|
|
42
|
+
return { ok: false, state: { schemaVersion: MERGE_STATE_VERSION, rows: [] }, note: 'merge-state-needs-repair' };
|
|
43
|
+
return { ok: true, state: { schemaVersion: MERGE_STATE_VERSION, rows: v4.rows.map(v4ToLegacy) } };
|
|
44
|
+
}
|
|
45
|
+
if (typeof header.schemaVersion === 'number' && header.schemaVersion > 4)
|
|
46
|
+
return { ok: false, state: { schemaVersion: MERGE_STATE_VERSION, rows: [] }, note: 'merge-state-forward-only' };
|
|
47
|
+
}
|
|
48
|
+
catch { /* parseState below reports the detailed note */ }
|
|
49
|
+
}
|
|
35
50
|
const parsed = parseState(raw);
|
|
36
51
|
return {
|
|
37
52
|
ok: parsed.ok,
|
|
@@ -39,23 +54,26 @@ export async function loadMergeState(io, home) {
|
|
|
39
54
|
...(parsed.note === undefined ? {} : { note: parsed.note }),
|
|
40
55
|
};
|
|
41
56
|
}
|
|
57
|
+
/** Schema probe for doctor; the only other reader of mergeStatePath. */
|
|
58
|
+
export async function loadStateSchema(io, home) {
|
|
59
|
+
try {
|
|
60
|
+
const raw = await io.readText(mergeStatePath(home));
|
|
61
|
+
if (raw === null)
|
|
62
|
+
return null;
|
|
63
|
+
const parsed = JSON.parse(raw);
|
|
64
|
+
return typeof parsed.schemaVersion === 'number' ? parsed.schemaVersion : null;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** G3-5 runtime write API: only createVerify/createSkipped/transition. v2 writes are rejected. */
|
|
71
|
+
export async function transactRows(_io, _home, _fn) {
|
|
72
|
+
throw new Error('legacy-writer-removed');
|
|
73
|
+
}
|
|
42
74
|
/** The ONE write point. The callback returns the whole row set, not a delta. */
|
|
43
|
-
export async function mutateMergeState(
|
|
44
|
-
|
|
45
|
-
let ran = false;
|
|
46
|
-
await transactIo(io, mergeStatePath(home), (current) => {
|
|
47
|
-
ran = true;
|
|
48
|
-
const parsed = parseState(current);
|
|
49
|
-
if (!parsed.ok || parsed.note === 'merge-state-forward-only')
|
|
50
|
-
throw new Error(parsed.note ?? 'merge-state-invalid');
|
|
51
|
-
const out = fn(parsed.rows);
|
|
52
|
-
result = out.result;
|
|
53
|
-
return JSON.stringify({ schemaVersion: MERGE_STATE_VERSION, rows: out.rows }, null, 2);
|
|
54
|
-
});
|
|
55
|
-
// transactIo only returns after fn ran; a skipped write is not a state outcome.
|
|
56
|
-
void ran;
|
|
57
|
-
void now;
|
|
58
|
-
return result;
|
|
75
|
+
export async function mutateMergeState(_io, _home, _now, _fn) {
|
|
76
|
+
throw new Error('legacy-writer-removed');
|
|
59
77
|
}
|
|
60
78
|
export async function upsertRow(io, home, now, row, expect = 'any', cas) {
|
|
61
79
|
return await mutateMergeState(io, home, now, (rows) => {
|
|
@@ -102,6 +120,10 @@ export async function moveRow(io, home, now, id, patch) {
|
|
|
102
120
|
return { rows: copy, result: next };
|
|
103
121
|
});
|
|
104
122
|
}
|
|
123
|
+
/** G3-5 v3-aware drop: only terminal events. */
|
|
124
|
+
export async function dropRowV3(_io, _home, _id, _event, _unitId = 'unknown') {
|
|
125
|
+
throw new Error('legacy-writer-removed');
|
|
126
|
+
}
|
|
105
127
|
export async function dropRow(io, home, now, id) {
|
|
106
128
|
return await mutateMergeState(io, home, now, (rows) => {
|
|
107
129
|
const next = rows.filter(item => item.id !== id);
|
package/lib/types/status.d.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* writes, no report reads. The UI and command parser share this vocabulary.
|
|
4
4
|
*/
|
|
5
5
|
import type { MergeQueue, MergeRow } from './contracts.ts';
|
|
6
|
+
import type { Facts, RunDetail, RunsView } from './wire.ts';
|
|
6
7
|
export interface MergeCounts {
|
|
7
8
|
readonly verify: number;
|
|
8
9
|
readonly pending: number;
|
|
@@ -11,5 +12,12 @@ export interface MergeCounts {
|
|
|
11
12
|
}
|
|
12
13
|
export declare function countRows(rows: readonly MergeRow[]): MergeCounts;
|
|
13
14
|
export declare function formatStatus(rows: readonly MergeRow[]): string;
|
|
15
|
+
export declare function formatFacts(facts: Facts): string;
|
|
16
|
+
export declare function formatRunsView(view: RunsView): string;
|
|
17
|
+
/** CLI face of the RunDetail wire projection: one run, its current pair and every pair outcome. */
|
|
18
|
+
export declare function formatRunDetail(detail: RunDetail): string;
|
|
14
19
|
export declare function formatQueue(rows: readonly MergeRow[], queue: MergeQueue): string;
|
|
20
|
+
/** Snapshot-compatible formatting, no legacy MergeRow. */
|
|
21
|
+
export declare function formatSnapshotStatus(rows: readonly import('./wire.ts').RowFact[]): string;
|
|
22
|
+
export declare function formatSnapshotQueue(rows: readonly import('./wire.ts').RowFact[], queue: import('./wire.ts').QueueId): string;
|
|
15
23
|
//# sourceMappingURL=status.d.ts.map
|
package/lib/types/status.js
CHANGED
|
@@ -20,10 +20,74 @@ export function formatStatus(rows) {
|
|
|
20
20
|
lines.push('- ' + describe(row));
|
|
21
21
|
return fitToolText(lines.join('\n'));
|
|
22
22
|
}
|
|
23
|
+
export function formatFacts(facts) {
|
|
24
|
+
const lines = [
|
|
25
|
+
'merge: verify ' + String(facts.counts.active.verify) + ', pending ' + String(facts.counts.active.pending) + ', fuse ' + String(facts.counts.active.fuse) + ', skipped ' + String(facts.counts.active.skipped) + ', expired ' + String(facts.counts.expired),
|
|
26
|
+
];
|
|
27
|
+
if (facts.active !== null)
|
|
28
|
+
lines.push('active: ' + facts.active.id + ' [' + facts.active.state + ']');
|
|
29
|
+
if (facts.last !== null)
|
|
30
|
+
lines.push('last: ' + facts.last.id + ' [' + facts.last.state + ']');
|
|
31
|
+
if (facts.degraded)
|
|
32
|
+
lines.push('degraded: ' + facts.notes.join(','));
|
|
33
|
+
return fitToolText(lines.join('\n'));
|
|
34
|
+
}
|
|
35
|
+
export function formatRunsView(view) {
|
|
36
|
+
if (view.runs.length === 0)
|
|
37
|
+
return 'merge runs: empty';
|
|
38
|
+
return fitToolText(['merge runs: ' + String(view.runs.length)].concat(view.runs.map(run => '- ' + run.id + ' [' + run.state + ']' + (run.endedAt === undefined ? '' : ' ' + new Date(run.endedAt).toISOString()))).join('\n'));
|
|
39
|
+
}
|
|
40
|
+
/** CLI face of the RunDetail wire projection: one run, its current pair and every pair outcome. */
|
|
41
|
+
export function formatRunDetail(detail) {
|
|
42
|
+
const lines = ['merge run ' + detail.id + ' [' + detail.state + '] command=' + detail.command
|
|
43
|
+
+ (detail.stage === undefined ? '' : ' stage=' + detail.stage)
|
|
44
|
+
+ (detail.progress === null || detail.progress === undefined ? '' : ' progress=' + String(detail.progress.done) + '/' + String(detail.progress.total))
|
|
45
|
+
+ (detail.currentPairKey === undefined ? '' : ' currentPair=' + detail.currentPairKey.replace('\u0000', ' + '))];
|
|
46
|
+
lines.push('started=' + new Date(detail.startedAt).toISOString()
|
|
47
|
+
+ (detail.endedAt === undefined ? '' : ' ended=' + new Date(detail.endedAt).toISOString())
|
|
48
|
+
+ (detail.parentRunId === undefined ? '' : ' parent=' + detail.parentRunId)
|
|
49
|
+
+ (detail.resultRef === undefined ? '' : ' resultRef=' + detail.resultRef)
|
|
50
|
+
+ (detail.reportRef === undefined ? '' : ' report=' + detail.reportRef)
|
|
51
|
+
+ (detail.meta?.phase === undefined ? '' : ' phase=' + detail.meta.phase)
|
|
52
|
+
+ (detail.meta?.approval === undefined ? '' : ' approval=' + detail.meta.approval)
|
|
53
|
+
+ (detail.stopReason === undefined ? '' : ' stopReason=' + detail.stopReason)
|
|
54
|
+
+ (detail.failure === undefined ? '' : ' failure=' + detail.failure.code));
|
|
55
|
+
for (const pair of detail.pairs) {
|
|
56
|
+
lines.push('- ' + pair.a + ' + ' + pair.b + ': ' + pair.outcome
|
|
57
|
+
+ (pair.stage === undefined ? '' : ' stage=' + pair.stage)
|
|
58
|
+
+ (pair.code === undefined ? '' : ' code=' + pair.code)
|
|
59
|
+
+ (pair.target === undefined ? '' : ' target=' + pair.target.requested));
|
|
60
|
+
}
|
|
61
|
+
if (detail.pairsTruncated)
|
|
62
|
+
lines.push('(pairs truncated)');
|
|
63
|
+
return fitToolText(lines.join('\n'));
|
|
64
|
+
}
|
|
23
65
|
export function formatQueue(rows, queue) {
|
|
24
66
|
const selected = rows.filter(row => queueOf(row.state) === queue);
|
|
25
67
|
if (selected.length === 0)
|
|
26
68
|
return 'merge ' + queue + ': empty';
|
|
27
69
|
return fitToolText(['merge ' + queue + ': ' + String(selected.length)].concat(selected.map(row => '- ' + describe(row))).join('\n'));
|
|
28
70
|
}
|
|
71
|
+
const REASON_TEXT = {
|
|
72
|
+
kept: '门禁判定保留,不合并。',
|
|
73
|
+
'gate-keep': '门禁判定保留,不合并。',
|
|
74
|
+
rejected: '人工拒绝合并。',
|
|
75
|
+
'merge-over-limit': '超出合并限制,未合并。',
|
|
76
|
+
'validation-failed': '合并校验未通过,未合并。',
|
|
77
|
+
};
|
|
78
|
+
function humanReason(reason) { return REASON_TEXT[reason] ?? reason; }
|
|
79
|
+
/** Snapshot-compatible formatting, no legacy MergeRow. */
|
|
80
|
+
export function formatSnapshotStatus(rows) {
|
|
81
|
+
const counts = { verify: 0, pending: 0, fuse: 0, skipped: 0 };
|
|
82
|
+
for (const row of rows)
|
|
83
|
+
counts[row.queue] += 1;
|
|
84
|
+
const lines = ['merge: verify ' + String(counts.verify) + ', pending ' + String(counts.pending) + ', fuse ' + String(counts.fuse) + ', skipped ' + String(counts.skipped)];
|
|
85
|
+
for (const row of rows)
|
|
86
|
+
lines.push('- ' + row.a + ' + ' + row.b + ' [' + row.state + ']' + (row.reason === undefined ? '' : ' — ' + humanReason(row.reason)));
|
|
87
|
+
return fitToolText(lines.join('\n'));
|
|
88
|
+
}
|
|
89
|
+
export function formatSnapshotQueue(rows, queue) {
|
|
90
|
+
const selected = rows.filter(row => row.queue === queue);
|
|
91
|
+
return selected.length === 0 ? 'merge ' + queue + ': empty' : fitToolText(['merge ' + queue + ': ' + String(selected.length)].concat(selected.map(row => '- ' + row.a + ' + ' + row.b + ' [' + row.state + ']' + (row.reason === undefined ? '' : ' — ' + humanReason(row.reason)))).join('\n'));
|
|
92
|
+
}
|
|
29
93
|
//# sourceMappingURL=status.js.map
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { EvolutionIoLike } from '@lmzhen/dsh-evolution-core';
|
|
2
|
+
import type { BodyRef } from '../domain/merge-snapshot.ts';
|
|
3
|
+
export declare function resolveBody(io: EvolutionIoLike, home: string, ref: BodyRef, rowBody: () => Promise<string | null>): Promise<string | null>;
|
|
4
|
+
//# sourceMappingURL=body.d.ts.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { readTextOrNull } from "../domain/read-probe.js";
|
|
2
|
+
/** Body resolution over a snapshot ref: stage body wins; lost/expired stage returns null. */
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { readStageEntry } from "../domain/stage-aggregate.js";
|
|
5
|
+
export async function resolveBody(io, home, ref, rowBody) {
|
|
6
|
+
const stage = await readStageEntry(io, home, ref.rowId).catch(() => null);
|
|
7
|
+
if (stage !== null && (stage.state === 'lost' || stage.state === 'expired'))
|
|
8
|
+
return null;
|
|
9
|
+
if (stage !== null && stage.state === 'present') {
|
|
10
|
+
const staged = await readTextOrNull(io, join(home, 'merge', 'runs', stage.ref, 'merged', ref.name));
|
|
11
|
+
if (staged !== null)
|
|
12
|
+
return staged;
|
|
13
|
+
}
|
|
14
|
+
return await rowBody();
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=body.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Pure wireVersion=1 builders over one MergeSnapshot. No IO, no writes. */
|
|
2
|
+
import type { MergeSnapshot, SnapshotRow, BodyRef } from '../domain/merge-snapshot.ts';
|
|
3
|
+
import type { Facts, QueuePage, RowFact, BodyView, HistoryView, RunView, RunDetail, RunsView, DoctorView, QueueId } from '../wire.ts';
|
|
4
|
+
export declare function toRowFact(row: SnapshotRow): RowFact;
|
|
5
|
+
export declare function buildFacts(snapshot: MergeSnapshot): Facts;
|
|
6
|
+
export declare function parseCursor(cursor: string, snapshot: MergeSnapshot): number;
|
|
7
|
+
export declare function buildQueuePage(snapshot: MergeSnapshot, queue: QueueId, expiredOnly: boolean, cursor?: string, limit?: number): QueuePage;
|
|
8
|
+
export declare function buildBodyView(snapshot: MergeSnapshot, ref: BodyRef, text: string): BodyView;
|
|
9
|
+
export declare function buildHistoryView(snapshot: MergeSnapshot): HistoryView;
|
|
10
|
+
export declare function buildRunView(snapshot: MergeSnapshot, runId: string): RunView | null;
|
|
11
|
+
export declare function buildRunDetail(snapshot: MergeSnapshot, runId: string): RunDetail | null;
|
|
12
|
+
export declare function buildRunsView(snapshot: MergeSnapshot): RunsView;
|
|
13
|
+
export declare function buildDoctorView(snapshot: MergeSnapshot): DoctorView;
|
|
14
|
+
//# sourceMappingURL=merge-view.d.ts.map
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { MERGE_COMMAND_SURFACE } from "../command-surface.js";
|
|
2
|
+
const base = '/evolution merge ';
|
|
3
|
+
function counts(rows) {
|
|
4
|
+
const active = { verify: 0, pending: 0, fuse: 0, skipped: 0 };
|
|
5
|
+
let expired = 0;
|
|
6
|
+
for (const item of rows) {
|
|
7
|
+
const skippedExpired = item.row.state === 'skipped' && item.row.expiredAt !== undefined;
|
|
8
|
+
if (item.expired || skippedExpired)
|
|
9
|
+
expired += 1;
|
|
10
|
+
else
|
|
11
|
+
active[item.queue] += 1;
|
|
12
|
+
}
|
|
13
|
+
return { active, expired };
|
|
14
|
+
}
|
|
15
|
+
const rowActionTemplates = new Map(MERGE_COMMAND_SURFACE.commands.flatMap(command => command.rowActions.map(action => [action.id, action])));
|
|
16
|
+
function renderAction(id, context, enabled = true, confirmOverride, reason) {
|
|
17
|
+
const template = rowActionTemplates.get(id);
|
|
18
|
+
if (template === undefined)
|
|
19
|
+
throw new Error('missing CommandDef rowAction: ' + id);
|
|
20
|
+
const command = template.command.replaceAll('{id}', context.id).replaceAll('{a}', context.a).replaceAll('{b}', context.b).replaceAll('{runId}', context.runId ?? context.id);
|
|
21
|
+
return { id, command, confirm: confirmOverride ?? template.confirm === true, enabled, ...(enabled ? {} : { disabledReason: reason ?? 'invalid' }) };
|
|
22
|
+
}
|
|
23
|
+
function actions(row) {
|
|
24
|
+
const context = { id: row.row.id, a: row.row.a, b: row.row.b, ...(row.row.runId === undefined ? {} : { runId: row.row.runId }) };
|
|
25
|
+
if (row.row.state === 'pending' && row.row.expired !== undefined)
|
|
26
|
+
return [renderAction('discard', context, true, true), renderAction('pair', context, true, true)];
|
|
27
|
+
if (row.row.state === 'pending')
|
|
28
|
+
return [renderAction('approve', context), renderAction('reject', context, true, true), renderAction('discard', context, true, true)];
|
|
29
|
+
if (row.row.state === 'approved' || row.row.state === 'fuse')
|
|
30
|
+
return [renderAction('retry-resume', context, true, true), renderAction('discard', context, true, true), renderAction('status', context)];
|
|
31
|
+
if (row.row.state === 'skipped')
|
|
32
|
+
return [renderAction('restore', context, true, true)];
|
|
33
|
+
return [renderAction('pair', context)];
|
|
34
|
+
}
|
|
35
|
+
function verifyState(row) {
|
|
36
|
+
if (row.row.state !== 'verify')
|
|
37
|
+
return undefined;
|
|
38
|
+
if (row.row.failure?.code === 'cancelled')
|
|
39
|
+
return 'cancelled';
|
|
40
|
+
if (row.row.failure !== undefined)
|
|
41
|
+
return 'failed';
|
|
42
|
+
if (row.row.verifySubState === 'screened')
|
|
43
|
+
return 'running';
|
|
44
|
+
return 'queued';
|
|
45
|
+
}
|
|
46
|
+
export function toRowFact(row) {
|
|
47
|
+
const state = row.row;
|
|
48
|
+
const target = state.state === 'pending' || state.state === 'approved' || state.state === 'fuse' ? { requested: state.target, ...(state.state === 'approved' || state.state === 'fuse' ? { actual: state.target } : {}) } : undefined;
|
|
49
|
+
const summary = state.state === 'pending' ? { a: state.decision.aSummary ?? null, b: state.decision.bSummary ?? null, rationale: state.decision.rationale } : undefined;
|
|
50
|
+
const stage = row.stage;
|
|
51
|
+
const staging = stage === null ? undefined : stage.state === 'reserved' || stage.state === 'present' ? { state: 'kept', ...(stage.expiresAt === undefined ? {} : { expiresAt: stage.expiresAt }) } : undefined;
|
|
52
|
+
const stageField = stage === null ? undefined : stage.state === 'present' || stage.state === 'reserved' ? 'present' : stage.state === 'lost' ? 'lost' : 'expired';
|
|
53
|
+
const expiredKind = row.expiredKind ?? (state.state === 'pending' ? state.expired : undefined);
|
|
54
|
+
const expired = expiredKind !== undefined ? { kind: expiredKind, at: Date.parse(state.updatedAt) } : state.state === 'skipped' && state.expiredAt !== undefined ? { kind: 'skipped-ttl', at: state.expiredAt } : undefined;
|
|
55
|
+
return {
|
|
56
|
+
id: state.id, state: state.state, queue: row.queue,
|
|
57
|
+
...(verifyState(row) === undefined ? {} : { verifySubState: verifyState(row) }),
|
|
58
|
+
...(stageField === undefined ? {} : { stage: stageField }),
|
|
59
|
+
a: state.a, b: state.b,
|
|
60
|
+
...(target === undefined ? {} : { target }),
|
|
61
|
+
...(summary === undefined ? {} : { summary }),
|
|
62
|
+
...(state.state === 'skipped' ? { reason: state.reason } : {}),
|
|
63
|
+
...(state.origin === undefined ? {} : { origin: state.origin }),
|
|
64
|
+
...(state.failure === undefined ? {} : { failure: { stage: state.failure.stage, code: state.failure.code, retryable: state.failure.retryable, retryFrom: state.failure.retryFrom, detail: state.failure.message } }),
|
|
65
|
+
...(staging === undefined ? {} : { staging }),
|
|
66
|
+
...(expired === undefined ? {} : { expired }),
|
|
67
|
+
...(state.attempts === undefined ? {} : { attempts: state.attempts }),
|
|
68
|
+
...(state.state === 'skipped' && state.expiredAt !== undefined ? { expiresAt: state.expiredAt } : {}),
|
|
69
|
+
bodies: row.bodies,
|
|
70
|
+
createdAt: Date.parse(state.createdAt),
|
|
71
|
+
updatedAt: Date.parse(state.updatedAt),
|
|
72
|
+
actions: actions(row),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function runView(run, readAt, includeResultRef = false) {
|
|
76
|
+
void includeResultRef;
|
|
77
|
+
const record = run.record;
|
|
78
|
+
const metaPhase = run.meta?.phase;
|
|
79
|
+
const state = metaPhase === 'undone' ? 'undone' : run.meta?.approval === 'pending' ? 'awaiting-approval' : record.state;
|
|
80
|
+
const stopReason = run.meta?.stopReason ?? (record.state === 'cancelled' ? 'cancelled' : record.state === 'failed' ? 'failed' : record.state === 'succeeded' ? 'completed' : undefined);
|
|
81
|
+
return { wireVersion: 1, readAt, id: record.id, state, command: record.command ?? 'scan', ...(record.stage === undefined ? {} : { stage: record.stage }), progress: record.progress === undefined ? null : { done: record.progress.done, total: record.progress.total }, startedAt: record.startedAt, ...(record.endedAt === undefined ? {} : { endedAt: record.endedAt }), ...(stopReason === undefined ? {} : { stopReason }), ...(record.failure === undefined ? {} : { failure: { stage: 'settle', code: 'run-failed', retryable: false, retryFrom: 'none', detail: record.failure } }), ...(record.parentRunId === undefined ? {} : { parentRunId: record.parentRunId }), ...(run.pairs[0]?.pairKey === undefined ? {} : { currentPairKey: run.pairs[0].pairKey }), ...(record.resultRef === undefined ? {} : { resultRef: record.resultRef }), actions: [{ id: 'status', command: base + 'status ' + record.id, confirm: false, enabled: true }, { id: 'report', command: base + 'report ' + record.id, confirm: false, enabled: record.resultRef !== undefined }, { id: 'cancel', command: base + 'cancel ' + record.id, confirm: false, enabled: record.state === 'running' }] };
|
|
82
|
+
}
|
|
83
|
+
export function buildFacts(snapshot) {
|
|
84
|
+
const ordered = [...snapshot.runs].sort((a, b) => b.record.startedAt - a.record.startedAt);
|
|
85
|
+
const active = ordered.find(run => run.record.state === 'running');
|
|
86
|
+
const last = ordered.find(run => run.record.state !== 'running');
|
|
87
|
+
return { wireVersion: 1, readAt: snapshot.readAt, approval: { mergeApproval: snapshot.approval.mergeApproval, ...(snapshot.approval.platformWrite === undefined ? {} : { platformWrite: snapshot.approval.platformWrite }) }, counts: counts(snapshot.rows), active: active === undefined ? null : runView(active, snapshot.readAt), last: last === undefined ? null : runView(last, snapshot.readAt), startCommands: { scan: base + 'scan', pair: base + 'pair <a> <b>' }, fallback: MERGE_COMMAND_SURFACE.fallback, notes: snapshot.notes, degraded: snapshot.degraded };
|
|
88
|
+
}
|
|
89
|
+
function encodeCursor(snapshot, offset) { return Buffer.from(JSON.stringify({ revision: snapshot.queueRevision, offset }), 'utf8').toString('base64url'); }
|
|
90
|
+
export function parseCursor(cursor, snapshot) {
|
|
91
|
+
try {
|
|
92
|
+
const value = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8'));
|
|
93
|
+
if (value.revision !== snapshot.queueRevision || typeof value.offset !== 'number' || value.offset < 0)
|
|
94
|
+
throw new Error('stale');
|
|
95
|
+
return value.offset;
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
throw new Error('queue cursor is stale; reload');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
export function buildQueuePage(snapshot, queue, expiredOnly, cursor, limit = 50) {
|
|
102
|
+
const selected = snapshot.rows.filter(row => row.queue === queue && (queue === 'skipped' ? true : expiredOnly ? row.expired : !row.expired));
|
|
103
|
+
const offset = cursor === undefined ? 0 : parseCursor(cursor, snapshot);
|
|
104
|
+
const page = selected.slice(offset, offset + Math.max(0, Math.min(200, limit)));
|
|
105
|
+
const next = offset + page.length < selected.length ? encodeCursor(snapshot, offset + page.length) : undefined;
|
|
106
|
+
return { wireVersion: 1, readAt: snapshot.readAt, queue, expiredOnly, counts: counts(snapshot.rows), rows: page.map(toRowFact), ...(next === undefined ? {} : { nextCursor: next }), notes: snapshot.notes };
|
|
107
|
+
}
|
|
108
|
+
export function buildBodyView(snapshot, ref, text) {
|
|
109
|
+
const max = 64 * 1024;
|
|
110
|
+
const truncated = Buffer.byteLength(text, 'utf8') > max;
|
|
111
|
+
const body = truncated ? text.slice(0, max) : text;
|
|
112
|
+
return { wireVersion: 1, readAt: snapshot.readAt, name: ref.name, side: ref.side, text: body, markdown: true, truncated, totalBytes: Buffer.byteLength(text, 'utf8') };
|
|
113
|
+
}
|
|
114
|
+
export function buildHistoryView(snapshot) { return { wireVersion: 1, readAt: snapshot.readAt, items: snapshot.history }; }
|
|
115
|
+
export function buildRunView(snapshot, runId) { const run = snapshot.runs.find(item => item.record.id === runId); return run === undefined ? null : runView(run, snapshot.readAt); }
|
|
116
|
+
export function buildRunDetail(snapshot, runId) {
|
|
117
|
+
const run = snapshot.runs.find(item => item.record.id === runId);
|
|
118
|
+
if (run === undefined)
|
|
119
|
+
return null;
|
|
120
|
+
return { ...runView(run, snapshot.readAt, true), ...(run.meta === null ? {} : { meta: { phase: run.meta.phase, ...(run.meta.approval === undefined ? {} : { approval: run.meta.approval }) } }), ...(run.reportRef === null ? {} : { reportRef: run.reportRef }), pairs: run.pairs.map(pair => ({ pairKey: pair.pairKey, a: pair.pairKey.split('\u0000')[0] ?? '', b: pair.pairKey.split('\u0000')[1] ?? '', outcome: pair.result === 'kept' ? 'kept' : pair.result === 'failed' ? 'failed' : pair.result === 'cancelled' ? 'cancelled' : 'merged', ...(pair.target === undefined ? {} : { target: { requested: pair.target } }) })), pairsTruncated: false };
|
|
121
|
+
}
|
|
122
|
+
export function buildRunsView(snapshot) { return { wireVersion: 1, readAt: snapshot.readAt, runs: [...snapshot.runs].sort((a, b) => b.record.startedAt - a.record.startedAt).map(run => runView(run, snapshot.readAt, true)) }; }
|
|
123
|
+
export function buildDoctorView(snapshot) {
|
|
124
|
+
return { wireVersion: 1, readAt: snapshot.readAt, counts: snapshot.doctor.counts, ...(snapshot.runs[0] === undefined ? {} : { latestRun: runView(snapshot.runs[0], snapshot.readAt) }), staging: snapshot.doctor.staging, approval: snapshot.approval, ...(snapshot.blocked === undefined ? {} : { blocked: { code: snapshot.blocked.code, message: snapshot.blocked.message } }), rows: snapshot.doctor.rows };
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=merge-view.js.map
|
|
@@ -3,5 +3,6 @@ export interface TargetClaim {
|
|
|
3
3
|
readonly token: string;
|
|
4
4
|
release(): Promise<void>;
|
|
5
5
|
}
|
|
6
|
+
export declare function releaseTargetClaimQuietly(claim: TargetClaim | null | undefined): Promise<void>;
|
|
6
7
|
export declare function acquireTargetClaim(io: EvolutionIoLike, home: string, target: string, now: number): Promise<TargetClaim | null>;
|
|
7
8
|
//# sourceMappingURL=target-claim.d.ts.map
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/** Durable target claim: one approve writer per merge target at a time. */
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { mkdir, open, readFile, unlink } from 'node:fs/promises';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
import { contentHash, isProcessAlive } from '@lmzhen/dsh-evolution-core';
|
|
5
6
|
import { acquireClaim, releaseClaim } from "./domain/claim.js";
|
|
6
|
-
const
|
|
7
|
+
const PROCESS_STARTED_AT = Date.now() - process.uptime() * 1000;
|
|
7
8
|
function claimPath(home, target) {
|
|
8
9
|
return join(home, 'merge', 'claims', contentHash(target) + '.json');
|
|
9
10
|
}
|
|
@@ -13,32 +14,76 @@ function parseClaim(raw) {
|
|
|
13
14
|
const parsed = JSON.parse(raw);
|
|
14
15
|
if (typeof parsed.token !== 'string' || typeof parsed.expiresAt !== 'number')
|
|
15
16
|
throw new Error('corrupt target claim');
|
|
16
|
-
return {
|
|
17
|
+
return {
|
|
18
|
+
id: 'claim', token: parsed.token, expiresAt: parsed.expiresAt,
|
|
19
|
+
...(typeof parsed.pid === 'number' ? { pid: parsed.pid } : {}),
|
|
20
|
+
...(typeof parsed.startedAt === 'number' ? { startedAt: parsed.startedAt } : {}),
|
|
21
|
+
};
|
|
17
22
|
}
|
|
18
|
-
async function
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
if (current !== null)
|
|
23
|
+
async function withGuard(path, fn) {
|
|
24
|
+
const guard = path + '.guard';
|
|
25
|
+
await mkdir(dirname(path), { recursive: true });
|
|
26
|
+
for (let attempt = 0; attempt < 500; attempt += 1) {
|
|
27
|
+
try {
|
|
28
|
+
const handle = await open(guard, 'wx');
|
|
29
|
+
await handle.writeFile(String(process.pid));
|
|
30
|
+
await handle.close();
|
|
27
31
|
try {
|
|
28
|
-
await
|
|
32
|
+
return await fn();
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
await unlink(guard).catch(() => { });
|
|
29
36
|
}
|
|
30
|
-
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
if (error.code !== 'EEXIST')
|
|
40
|
+
throw error;
|
|
41
|
+
const pid = Number(await readFile(guard, 'utf8').catch(() => ''));
|
|
42
|
+
if (Number.isFinite(pid) && pid > 0 && !isProcessAlive(pid))
|
|
43
|
+
await unlink(guard).catch(() => { });
|
|
44
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
45
|
+
}
|
|
31
46
|
}
|
|
32
|
-
|
|
33
|
-
|
|
47
|
+
throw new Error('target-claim-guard-timeout');
|
|
48
|
+
}
|
|
49
|
+
async function compareAndSwap(io, path, task) {
|
|
50
|
+
await withGuard(path, async () => {
|
|
51
|
+
if (io.transact !== undefined) {
|
|
52
|
+
await io.transact(path, current => task(current));
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const current = (await io.exists(path)) ? await io.readText(path) : null;
|
|
56
|
+
const next = task(current);
|
|
57
|
+
if (next === null) {
|
|
58
|
+
if (current !== null)
|
|
59
|
+
try {
|
|
60
|
+
await io.remove(path);
|
|
61
|
+
}
|
|
62
|
+
catch { /* already gone */ }
|
|
63
|
+
}
|
|
64
|
+
else if (next !== current) {
|
|
65
|
+
await io.writeText(path, next);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
export async function releaseTargetClaimQuietly(claim) {
|
|
70
|
+
if (claim === null || claim === undefined)
|
|
71
|
+
return;
|
|
72
|
+
try {
|
|
73
|
+
await claim.release();
|
|
34
74
|
}
|
|
75
|
+
catch { /* best effort; dead-holder reclaim or TTL handles it */ }
|
|
35
76
|
}
|
|
36
77
|
export async function acquireTargetClaim(io, home, target, now) {
|
|
37
78
|
const path = claimPath(home, target);
|
|
38
79
|
const token = randomUUID();
|
|
39
80
|
let granted;
|
|
40
81
|
await compareAndSwap(io, path, (raw) => {
|
|
41
|
-
const
|
|
82
|
+
const current = parseClaim(raw);
|
|
83
|
+
const reused = current !== undefined && current.pid === process.pid && current.startedAt !== undefined && Math.abs(current.startedAt - PROCESS_STARTED_AT) > 1_000;
|
|
84
|
+
const abandoned = reused || (current !== undefined && current.expiresAt > now
|
|
85
|
+
&& current.pid !== undefined && current.pid !== process.pid && !isProcessAlive(current.pid));
|
|
86
|
+
const claim = acquireClaim(abandoned ? undefined : current, now, Number.MAX_SAFE_INTEGER, token, process.pid, PROCESS_STARTED_AT);
|
|
42
87
|
if (claim === undefined)
|
|
43
88
|
return raw;
|
|
44
89
|
granted = claim;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Batch 2b undo operation: per-pair preflight, journal, restore, receipt and settlement. */
|
|
2
|
+
import type { EvolutionIoLike } from '@lmzhen/dsh-evolution-core';
|
|
3
|
+
import { type MergeUndoPort } from './undo-merge.ts';
|
|
4
|
+
export type UndoErrorCode = 'undo-unavailable-legacy-receipt' | 'undo-evidence-hash-mismatch' | 'undo-target-drift' | 'undo-source-drift' | 'undo-partial-state' | 'undo-unit-not-committed';
|
|
5
|
+
export interface UndoResult {
|
|
6
|
+
readonly ok: boolean;
|
|
7
|
+
readonly code?: UndoErrorCode;
|
|
8
|
+
readonly message: string;
|
|
9
|
+
readonly undone: readonly string[];
|
|
10
|
+
}
|
|
11
|
+
export declare function runUndoOperation(options: {
|
|
12
|
+
readonly io: EvolutionIoLike;
|
|
13
|
+
readonly home: string;
|
|
14
|
+
readonly runId: string;
|
|
15
|
+
readonly port: MergeUndoPort;
|
|
16
|
+
readonly now: () => number;
|
|
17
|
+
readonly pairKey?: string;
|
|
18
|
+
readonly finalizeUnit?: (unitId: string) => Promise<{
|
|
19
|
+
readonly ok: boolean;
|
|
20
|
+
readonly message?: string;
|
|
21
|
+
}>;
|
|
22
|
+
readonly onPhase?: (phase: string, pairKey: string) => void | Promise<void>;
|
|
23
|
+
}): Promise<UndoResult>;
|
|
24
|
+
//# sourceMappingURL=undo-operation.d.ts.map
|