@lmzhen/dsh-evolution-merge 0.30.18 → 0.30.20
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,53 @@
|
|
|
1
|
+
import { type EvolutionIoLike } from '@lmzhen/dsh-evolution-core';
|
|
2
|
+
import { type CommitUnitRef, type V4Event, type V4Row, type V4WriteResult } from './row-v4.ts';
|
|
3
|
+
import type { V3Row } from './row-v3.ts';
|
|
4
|
+
export interface MergeStateV4 {
|
|
5
|
+
readonly schemaVersion: 4;
|
|
6
|
+
readonly rows: readonly V4Row[];
|
|
7
|
+
readonly migration?: unknown;
|
|
8
|
+
readonly pre1b?: {
|
|
9
|
+
readonly backupRef: string;
|
|
10
|
+
readonly at: string;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export type V4BlockCode = 'merge-state-normalize-required' | 'merge-state-migration-required' | 'merge-state-v4-required' | 'merge-state-forward-only' | 'merge-state-needs-repair' | 'merge-state-backup-missing';
|
|
14
|
+
export type V4Readiness = {
|
|
15
|
+
readonly ok: true;
|
|
16
|
+
readonly schema: 4;
|
|
17
|
+
readonly state: MergeStateV4;
|
|
18
|
+
} | {
|
|
19
|
+
readonly ok: false;
|
|
20
|
+
readonly code: V4BlockCode;
|
|
21
|
+
readonly message: string;
|
|
22
|
+
};
|
|
23
|
+
export declare class V4BlockedError extends Error {
|
|
24
|
+
readonly readiness: V4Readiness;
|
|
25
|
+
constructor(readiness: V4Readiness);
|
|
26
|
+
}
|
|
27
|
+
export declare function schema4StatePath(home: string): string;
|
|
28
|
+
/** Pure read-only v4 readiness. Does not normalize, repair, or write. */
|
|
29
|
+
export declare function readV4Readiness(io: EvolutionIoLike, home: string): Promise<V4Readiness>;
|
|
30
|
+
export interface V4RowTx {
|
|
31
|
+
rows(): readonly V4Row[];
|
|
32
|
+
createVerify(row: Extract<V4Row, {
|
|
33
|
+
state: 'verify';
|
|
34
|
+
}>): V4WriteResult;
|
|
35
|
+
createSkipped(row: Extract<V4Row, {
|
|
36
|
+
state: 'skipped';
|
|
37
|
+
}>): V4WriteResult;
|
|
38
|
+
transition(id: string, event: V4Event, expectedVersion: number): V4WriteResult;
|
|
39
|
+
remove(id: string, terminal: 'committed' | 'superseded' | 'restored', unit: CommitUnitRef | undefined, expectedVersion: number): V4WriteResult;
|
|
40
|
+
}
|
|
41
|
+
/** Atomic readiness + V4 row write under one state-file lock. No production caller in 1b-2a. */
|
|
42
|
+
export declare function transactMergeState<T>(io: EvolutionIoLike, home: string, fn: (tx: V4RowTx) => T | Promise<T>): Promise<T>;
|
|
43
|
+
/** Pure v3-to-v4 projection for fixtures; 1b-2b wires it to the production normalize. */
|
|
44
|
+
export declare function normalizeV3State(state: {
|
|
45
|
+
readonly schemaVersion: 3;
|
|
46
|
+
readonly rows: readonly V3Row[];
|
|
47
|
+
readonly migration?: unknown;
|
|
48
|
+
}, pre1b: {
|
|
49
|
+
readonly backupRef: string;
|
|
50
|
+
readonly at: string;
|
|
51
|
+
}): MergeStateV4;
|
|
52
|
+
export declare function parseV4State(raw: string): MergeStateV4 | null;
|
|
53
|
+
//# sourceMappingURL=schema4.d.ts.map
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/** Batch 1b-2a schema4 state codec, readiness, and atomic row transaction. */
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { transactIo } from '@lmzhen/dsh-evolution-core';
|
|
4
|
+
import { isV4Row, applyV4Write, v3ToV4 } from "./row-v4.js";
|
|
5
|
+
import { readPre1bManifest } from "./pre-1b-backup.js";
|
|
6
|
+
export class V4BlockedError extends Error {
|
|
7
|
+
readiness;
|
|
8
|
+
constructor(readiness) {
|
|
9
|
+
super(readiness.ok ? 'merge state v4 is ready' : readiness.code + ': ' + readiness.message);
|
|
10
|
+
this.name = 'V4BlockedError';
|
|
11
|
+
this.readiness = readiness;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function schema4StatePath(home) { return join(home, 'merge', 'merge-state.json'); }
|
|
15
|
+
function blocked(code, message) { return { ok: false, code, message }; }
|
|
16
|
+
function object(value) { return value !== null && typeof value === 'object'; }
|
|
17
|
+
function string(value) { return typeof value === 'string' && value.length > 0; }
|
|
18
|
+
function parse(raw) {
|
|
19
|
+
try {
|
|
20
|
+
const value = JSON.parse(raw);
|
|
21
|
+
if (!object(value) || value.schemaVersion !== 4 || !Array.isArray(value.rows) || !value.rows.every(isV4Row))
|
|
22
|
+
return null;
|
|
23
|
+
return { body: value, rows: value.rows };
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async function inspect(io, home, body, rows) {
|
|
30
|
+
const pre1b = body.pre1b;
|
|
31
|
+
if (!object(pre1b) || !string(pre1b.backupRef))
|
|
32
|
+
return blocked('merge-state-needs-repair', 'schema4 state is missing pre1b backup reference');
|
|
33
|
+
const manifest = await readPre1bManifest(io, home, pre1b.backupRef).catch(() => null);
|
|
34
|
+
if (manifest === null)
|
|
35
|
+
return blocked('merge-state-backup-missing', 'pre-1b backup manifest is unreadable: ' + pre1b.backupRef);
|
|
36
|
+
return { ok: true, schema: 4, state: { schemaVersion: 4, rows, ...(body.migration === undefined ? {} : { migration: body.migration }), pre1b: { backupRef: pre1b.backupRef, at: typeof pre1b.at === 'string' ? pre1b.at : '' } } };
|
|
37
|
+
}
|
|
38
|
+
/** Pure read-only v4 readiness. Does not normalize, repair, or write. */
|
|
39
|
+
export async function readV4Readiness(io, home) {
|
|
40
|
+
let raw;
|
|
41
|
+
try {
|
|
42
|
+
raw = await io.readText(schema4StatePath(home));
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return blocked('merge-state-needs-repair', 'merge state is unreadable');
|
|
46
|
+
}
|
|
47
|
+
if (raw === null)
|
|
48
|
+
return blocked('merge-state-v4-required', 'schema4 state is missing');
|
|
49
|
+
let value;
|
|
50
|
+
try {
|
|
51
|
+
value = JSON.parse(raw);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return blocked('merge-state-needs-repair', 'merge state is not JSON');
|
|
55
|
+
}
|
|
56
|
+
if (!object(value))
|
|
57
|
+
return blocked('merge-state-needs-repair', 'merge state is not an object');
|
|
58
|
+
if (typeof value.schemaVersion === 'number' && value.schemaVersion > 4)
|
|
59
|
+
return blocked('merge-state-forward-only', 'state schema ' + String(value.schemaVersion) + ' is newer than this build');
|
|
60
|
+
if (value.schemaVersion === 3)
|
|
61
|
+
return blocked('merge-state-normalize-required', 'schema3 state requires v4 normalize');
|
|
62
|
+
if (value.schemaVersion === 2 || value.schemaVersion === 1)
|
|
63
|
+
return blocked('merge-state-migration-required', 'legacy state requires schema migration');
|
|
64
|
+
if (value.schemaVersion !== 4)
|
|
65
|
+
return blocked('merge-state-needs-repair', 'merge state has no supported schemaVersion');
|
|
66
|
+
const parsed = parse(raw);
|
|
67
|
+
if (parsed === null)
|
|
68
|
+
return blocked('merge-state-needs-repair', 'schema4 state has an invalid row');
|
|
69
|
+
return await inspect(io, home, parsed.body, parsed.rows);
|
|
70
|
+
}
|
|
71
|
+
/** Atomic readiness + V4 row write under one state-file lock. No production caller in 1b-2a. */
|
|
72
|
+
export async function transactMergeState(io, home, fn) {
|
|
73
|
+
let result;
|
|
74
|
+
await transactIo(io, schema4StatePath(home), async (current) => {
|
|
75
|
+
if (current === null)
|
|
76
|
+
throw new V4BlockedError(blocked('merge-state-v4-required', 'schema4 state is missing'));
|
|
77
|
+
let value;
|
|
78
|
+
try {
|
|
79
|
+
value = JSON.parse(current);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
throw new V4BlockedError(blocked('merge-state-needs-repair', 'merge state is not JSON'));
|
|
83
|
+
}
|
|
84
|
+
if (!object(value))
|
|
85
|
+
throw new V4BlockedError(blocked('merge-state-needs-repair', 'merge state is not an object'));
|
|
86
|
+
if (typeof value.schemaVersion === 'number' && value.schemaVersion > 4)
|
|
87
|
+
throw new V4BlockedError(blocked('merge-state-forward-only', 'state schema ' + String(value.schemaVersion) + ' is newer than this build'));
|
|
88
|
+
if (value.schemaVersion === 3)
|
|
89
|
+
throw new V4BlockedError(blocked('merge-state-normalize-required', 'schema3 state requires v4 normalize'));
|
|
90
|
+
if (value.schemaVersion !== 4)
|
|
91
|
+
throw new V4BlockedError(blocked('merge-state-needs-repair', 'merge state is not schema4'));
|
|
92
|
+
const parsed = parse(current);
|
|
93
|
+
if (parsed === null)
|
|
94
|
+
throw new V4BlockedError(blocked('merge-state-needs-repair', 'schema4 state has an invalid row'));
|
|
95
|
+
const ready = await inspect(io, home, parsed.body, parsed.rows);
|
|
96
|
+
if (!ready.ok)
|
|
97
|
+
throw new V4BlockedError(ready);
|
|
98
|
+
let rows = parsed.rows;
|
|
99
|
+
const apply = (write) => {
|
|
100
|
+
const next = applyV4Write(rows, write, new Date().toISOString());
|
|
101
|
+
if (next.ok)
|
|
102
|
+
rows = next.rows;
|
|
103
|
+
return next;
|
|
104
|
+
};
|
|
105
|
+
const tx = {
|
|
106
|
+
rows: () => rows,
|
|
107
|
+
createVerify: row => apply({ kind: 'createVerify', row }),
|
|
108
|
+
createSkipped: row => apply({ kind: 'createSkipped', row }),
|
|
109
|
+
transition: (id, event, expectedVersion) => apply({ kind: 'transition', id, event, expectedVersion }),
|
|
110
|
+
remove: (id, terminal, unit, expectedVersion) => apply({ kind: 'remove', id, terminal, ...(unit === undefined ? {} : { unit }), expectedVersion }),
|
|
111
|
+
};
|
|
112
|
+
result = await fn(tx);
|
|
113
|
+
return JSON.stringify({ ...parsed.body, schemaVersion: 4, rows }, null, 2) + '\n';
|
|
114
|
+
});
|
|
115
|
+
if (result === undefined)
|
|
116
|
+
throw new V4BlockedError(blocked('merge-state-needs-repair', 'v4 transaction did not produce a result'));
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
119
|
+
/** Pure v3-to-v4 projection for fixtures; 1b-2b wires it to the production normalize. */
|
|
120
|
+
export function normalizeV3State(state, pre1b) {
|
|
121
|
+
return { schemaVersion: 4, rows: state.rows.map(v3ToV4), ...(state.migration === undefined ? {} : { migration: state.migration }), pre1b };
|
|
122
|
+
}
|
|
123
|
+
export function parseV4State(raw) {
|
|
124
|
+
const parsed = parse(raw);
|
|
125
|
+
if (parsed === null)
|
|
126
|
+
return null;
|
|
127
|
+
return { schemaVersion: 4, rows: parsed.rows, ...(parsed.body.migration === undefined ? {} : { migration: parsed.body.migration }), ...(object(parsed.body.pre1b) && string(parsed.body.pre1b.backupRef) ? { pre1b: { backupRef: parsed.body.pre1b.backupRef, at: typeof parsed.body.pre1b.at === 'string' ? parsed.body.pre1b.at : '' } } : {}) };
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=schema4.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Batch 3 selection policy: filter managed/protected before writing skipped. */
|
|
2
|
+
import type { MergeCandidate } from '../contracts.ts';
|
|
3
|
+
export interface SelectionLike {
|
|
4
|
+
readonly candidates: readonly MergeCandidate[];
|
|
5
|
+
readonly skipped: readonly {
|
|
6
|
+
readonly pairKey: string;
|
|
7
|
+
readonly a: string;
|
|
8
|
+
readonly b: string;
|
|
9
|
+
readonly reason?: string;
|
|
10
|
+
}[];
|
|
11
|
+
readonly reportOnly: readonly unknown[];
|
|
12
|
+
}
|
|
13
|
+
export declare function filterSelection<T extends SelectionLike>(selection: T, isManaged: (name: string) => boolean, isProtected: (name: string) => boolean): T;
|
|
14
|
+
//# sourceMappingURL=selection-policy.d.ts.map
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export function filterSelection(selection, isManaged, isProtected) {
|
|
2
|
+
const skipped = selection.skipped.filter(item => isManaged(item.a) && isManaged(item.b) && !isProtected(item.a) && !isProtected(item.b));
|
|
3
|
+
const candidates = selection.candidates.filter(item => isManaged(item.a) && isManaged(item.b) && !isProtected(item.a) && !isProtected(item.b));
|
|
4
|
+
return { ...selection, candidates, skipped };
|
|
5
|
+
}
|
|
6
|
+
//# sourceMappingURL=selection-policy.js.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Pure skipped-TTL planning. Reconcile owns the durable write. */
|
|
2
|
+
import type { V4Row } from './row-v4.ts';
|
|
3
|
+
/** Row ids whose skipped TTL has elapsed at `now`; the caller applies one transition each. */
|
|
4
|
+
export declare function planSkippedExpiry(rows: readonly V4Row[], ttlDays: number, now: number): readonly string[];
|
|
5
|
+
//# sourceMappingURL=skipped-expiry.d.ts.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Row ids whose skipped TTL has elapsed at `now`; the caller applies one transition each. */
|
|
2
|
+
export function planSkippedExpiry(rows, ttlDays, now) {
|
|
3
|
+
if (ttlDays <= 0)
|
|
4
|
+
return [];
|
|
5
|
+
const ids = [];
|
|
6
|
+
for (const row of rows) {
|
|
7
|
+
if (row.state !== 'skipped' || row.expiredAt !== undefined)
|
|
8
|
+
continue;
|
|
9
|
+
const created = Date.parse(row.createdAt);
|
|
10
|
+
if (!Number.isFinite(created) || now - created < ttlDays * 86_400_000)
|
|
11
|
+
continue;
|
|
12
|
+
ids.push(row.id);
|
|
13
|
+
}
|
|
14
|
+
return ids;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=skipped-expiry.js.map
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { EvolutionIoLike } from '@lmzhen/dsh-evolution-core';
|
|
2
|
+
import { type StagingEntry } from './staging-index.ts';
|
|
3
|
+
import { readStaging, type CreateStagingInput, type CreateStagingResult } from '../staging.ts';
|
|
4
|
+
/**
|
|
5
|
+
* Old production staging writers (grep baseline at Batch 1b-1). Their production
|
|
6
|
+
* path is merge/staging-index + runs/<runId>/pairs/<hash>. adoptLegacy must never
|
|
7
|
+
* call any of these; it only writes merge/.shadow/staging-index.
|
|
8
|
+
*/
|
|
9
|
+
export declare const LEGACY_STAGING_WRITER_FILES: readonly ["src/scan-ports.ts", "src/scan-run.ts", "src/staging.ts", "src/new-child-authoring.ts", "src/domain/staging-index.ts", "src/state.ts", "src/domain/state-bootstrap.ts"];
|
|
10
|
+
export type StageState = 'reserved' | 'present' | 'expired' | 'lost';
|
|
11
|
+
export interface StageEntry {
|
|
12
|
+
readonly version: 1;
|
|
13
|
+
readonly rowId: string;
|
|
14
|
+
readonly pairKey: string;
|
|
15
|
+
readonly runId?: string;
|
|
16
|
+
readonly ref: string;
|
|
17
|
+
readonly root?: string;
|
|
18
|
+
readonly manifestPath?: string;
|
|
19
|
+
readonly state: StageState;
|
|
20
|
+
readonly createdAt: number;
|
|
21
|
+
readonly expiresAt: number;
|
|
22
|
+
readonly updatedAt: number;
|
|
23
|
+
readonly sourceDigest?: string;
|
|
24
|
+
readonly targetDigest?: string;
|
|
25
|
+
readonly manifestDigest?: string;
|
|
26
|
+
readonly sourceRevision?: string;
|
|
27
|
+
readonly targetRevision?: string;
|
|
28
|
+
readonly files?: readonly string[];
|
|
29
|
+
}
|
|
30
|
+
export interface ShadowAdoptResult {
|
|
31
|
+
readonly entries: readonly StageEntry[];
|
|
32
|
+
readonly anomalies: readonly string[];
|
|
33
|
+
readonly shadowPath: string;
|
|
34
|
+
readonly wrote: boolean;
|
|
35
|
+
}
|
|
36
|
+
export interface StageAggregateRead {
|
|
37
|
+
readonly entries: readonly StageEntry[];
|
|
38
|
+
readonly shadowPath: string;
|
|
39
|
+
}
|
|
40
|
+
export declare function shadowStagingIndexPath(home: string, rowId: string): string;
|
|
41
|
+
export declare function shadowStagingRoot(home: string): string;
|
|
42
|
+
/** Read-only adoption into the shadow index; never writes production staging-index. */
|
|
43
|
+
export declare function adoptLegacy(io: EvolutionIoLike, home: string): Promise<ShadowAdoptResult>;
|
|
44
|
+
export declare function readShadowStageAggregate(io: EvolutionIoLike, home: string, rowId?: string): Promise<StageAggregateRead>;
|
|
45
|
+
/** 1b-2b execution order; kept here so 1b-2a tests can pin the contract. */
|
|
46
|
+
export declare const STAGE_WRITE_ORDER: readonly ["index:reserved", "tree", "index:present"];
|
|
47
|
+
export type StageAdmission = {
|
|
48
|
+
readonly ok: true;
|
|
49
|
+
} | {
|
|
50
|
+
readonly ok: false;
|
|
51
|
+
readonly message: string;
|
|
52
|
+
};
|
|
53
|
+
export declare function admitStage(rowState: string): StageAdmission;
|
|
54
|
+
export type ApprovedReservedPlan = {
|
|
55
|
+
readonly kind: 'transition-fuse';
|
|
56
|
+
readonly rowId: string;
|
|
57
|
+
readonly ref: string;
|
|
58
|
+
readonly expiresAt: number;
|
|
59
|
+
} | {
|
|
60
|
+
readonly kind: 'write-present';
|
|
61
|
+
readonly rowId: string;
|
|
62
|
+
readonly ref: string;
|
|
63
|
+
} | {
|
|
64
|
+
readonly kind: 'drop-orphan';
|
|
65
|
+
readonly rowId: string;
|
|
66
|
+
} | {
|
|
67
|
+
readonly kind: 'none';
|
|
68
|
+
};
|
|
69
|
+
export declare function planApprovedReserved(rowState: string, entry: StageEntry | null): ApprovedReservedPlan;
|
|
70
|
+
/** Production adoption for 1b-2b normalize; writes the existing staging-index, not shadow. */
|
|
71
|
+
export declare function adoptLegacyProduction(io: EvolutionIoLike, home: string): Promise<ShadowAdoptResult>;
|
|
72
|
+
/** Production stage-tree facade. These wrappers keep the low-level tree module private to StageAggregate. */
|
|
73
|
+
export declare function ensureStageTree(io: EvolutionIoLike, home: string, input: CreateStagingInput): Promise<CreateStagingResult>;
|
|
74
|
+
export declare function createStageTree(io: EvolutionIoLike, home: string, input: CreateStagingInput): Promise<CreateStagingResult>;
|
|
75
|
+
export declare function readStageTree(io: EvolutionIoLike, home: string, runId: string): Promise<Awaited<ReturnType<typeof readStaging>>>;
|
|
76
|
+
export declare function setStageTreeTarget(io: EvolutionIoLike, home: string, stageKey: string, target: string, mode?: 'merge-into-existing' | 'create-umbrella' | 'keep'): Promise<boolean>;
|
|
77
|
+
export declare function dropStageTree(io: EvolutionIoLike, home: string, stageKey: string): Promise<void>;
|
|
78
|
+
export declare function writeStageIndex(io: EvolutionIoLike, home: string, entry: StagingEntry): Promise<void>;
|
|
79
|
+
export declare function readStageIndex(io: EvolutionIoLike, home: string, rowId: string, now?: number): Promise<StagingEntry | null>;
|
|
80
|
+
export declare function readStageEntry(io: EvolutionIoLike, home: string, rowId: string): Promise<StageEntry | null>;
|
|
81
|
+
export declare function reserveStage(io: EvolutionIoLike, home: string, entry: StageEntry): Promise<void>;
|
|
82
|
+
export declare function presentStage(io: EvolutionIoLike, home: string, entry: StageEntry): Promise<void>;
|
|
83
|
+
export declare function dropStage(io: EvolutionIoLike, home: string, rowId: string, ref?: string): Promise<void>;
|
|
84
|
+
export interface StageCleanupResult {
|
|
85
|
+
readonly removed: readonly string[];
|
|
86
|
+
readonly expired: readonly string[];
|
|
87
|
+
readonly kept: readonly string[];
|
|
88
|
+
readonly orphans: readonly string[];
|
|
89
|
+
}
|
|
90
|
+
/** Bounded cleanup: reserved/present TTL, pending/verify stale index removal, terminal/absent rows, orphan trees. */
|
|
91
|
+
export declare function cleanupStage(io: EvolutionIoLike, home: string, now: number, rows: ReadonlyMap<string, {
|
|
92
|
+
readonly state: string;
|
|
93
|
+
}>, limit?: number): Promise<StageCleanupResult>;
|
|
94
|
+
/** Read all production v2 stage entries. */
|
|
95
|
+
export declare function readAllStages(io: EvolutionIoLike, home: string): Promise<readonly StageEntry[]>;
|
|
96
|
+
//# sourceMappingURL=stage-aggregate.d.ts.map
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import { readTextOrNull, listOrEmpty } from "./read-probe.js";
|
|
2
|
+
/** 1b-1 shadow staging adoption. Production staging is still owned by the old staged writers. */
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { readPairLedger } from "./pair-ledger.js";
|
|
5
|
+
import { readStagingIndex, writeStagingIndex } from "./staging-index.js";
|
|
6
|
+
import { createStaging, dropStaging, ensureStaging, readStaging, setStagingTarget } from "../staging.js";
|
|
7
|
+
/**
|
|
8
|
+
* Old production staging writers (grep baseline at Batch 1b-1). Their production
|
|
9
|
+
* path is merge/staging-index + runs/<runId>/pairs/<hash>. adoptLegacy must never
|
|
10
|
+
* call any of these; it only writes merge/.shadow/staging-index.
|
|
11
|
+
*/
|
|
12
|
+
export const LEGACY_STAGING_WRITER_FILES = [
|
|
13
|
+
'src/scan-ports.ts',
|
|
14
|
+
'src/scan-run.ts',
|
|
15
|
+
'src/staging.ts',
|
|
16
|
+
'src/new-child-authoring.ts',
|
|
17
|
+
'src/domain/staging-index.ts',
|
|
18
|
+
'src/state.ts',
|
|
19
|
+
'src/domain/state-bootstrap.ts',
|
|
20
|
+
];
|
|
21
|
+
export function shadowStagingIndexPath(home, rowId) {
|
|
22
|
+
return join(home, 'merge', '.shadow', 'staging-index', encodeURIComponent(rowId) + '.json');
|
|
23
|
+
}
|
|
24
|
+
export function shadowStagingRoot(home) {
|
|
25
|
+
return join(home, 'merge', '.shadow', 'staging-index');
|
|
26
|
+
}
|
|
27
|
+
function stagingTreePath(home, ref) {
|
|
28
|
+
return join(home, 'merge', 'runs', ref);
|
|
29
|
+
}
|
|
30
|
+
function markerStaging(migration) {
|
|
31
|
+
if (migration === null || typeof migration !== 'object')
|
|
32
|
+
return [];
|
|
33
|
+
const value = migration;
|
|
34
|
+
if (!Array.isArray(value.staging))
|
|
35
|
+
return [];
|
|
36
|
+
const facts = [];
|
|
37
|
+
for (const item of value.staging) {
|
|
38
|
+
if (item === null || typeof item !== 'object')
|
|
39
|
+
continue;
|
|
40
|
+
const entry = item;
|
|
41
|
+
if (typeof entry.rowId !== 'string' || typeof entry.pairKey !== 'string' || typeof entry.ref !== 'string' || typeof entry.expiresAt !== 'number' || typeof entry.createdAt !== 'number')
|
|
42
|
+
continue;
|
|
43
|
+
facts.push({ rowId: entry.rowId, pairKey: entry.pairKey, ref: entry.ref, expiresAt: entry.expiresAt, ...(typeof entry.runId === 'string' ? { runId: entry.runId } : {}), createdAt: entry.createdAt });
|
|
44
|
+
}
|
|
45
|
+
return facts;
|
|
46
|
+
}
|
|
47
|
+
/** Read-only adoption into the shadow index; never writes production staging-index. */
|
|
48
|
+
export async function adoptLegacy(io, home) {
|
|
49
|
+
const ledger = await readPairLedger(io, home);
|
|
50
|
+
const entries = new Map();
|
|
51
|
+
const anomalies = [];
|
|
52
|
+
const now = Date.now();
|
|
53
|
+
for (const fact of markerStaging(ledger.migration)) {
|
|
54
|
+
const present = fact.ref !== '' && await io.exists(stagingTreePath(home, fact.ref));
|
|
55
|
+
entries.set(fact.rowId, { version: 1, rowId: fact.rowId, pairKey: fact.pairKey, ...(fact.runId === undefined ? {} : { runId: fact.runId }), ref: fact.ref, state: present ? 'present' : 'lost', createdAt: fact.createdAt, expiresAt: fact.expiresAt, updatedAt: now });
|
|
56
|
+
}
|
|
57
|
+
for (const row of ledger.rows) {
|
|
58
|
+
if (row.state === 'fuse' && !entries.has(row.id)) {
|
|
59
|
+
if (row.staging.kind === 'present')
|
|
60
|
+
entries.set(row.id, { version: 1, rowId: row.id, pairKey: row.pairKey, ...(row.runId === undefined ? {} : { runId: row.runId }), ref: row.staging.ref, state: 'present', createdAt: now, expiresAt: row.staging.expiresAt, updatedAt: now });
|
|
61
|
+
else if (row.staging.kind === 'expired')
|
|
62
|
+
entries.set(row.id, { version: 1, rowId: row.id, pairKey: row.pairKey, ...(row.runId === undefined ? {} : { runId: row.runId }), ref: row.staging.ref ?? '', state: 'expired', createdAt: now, expiresAt: row.staging.expiresAt, updatedAt: now });
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (row.state === 'pending') {
|
|
66
|
+
if (await readStagingIndex(io, home, row.id, now) !== null)
|
|
67
|
+
anomalies.push('pending-staging-index:' + row.id);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (row.state === 'approved' && !entries.has(row.id)) {
|
|
71
|
+
const old = await readStagingIndex(io, home, row.id, now);
|
|
72
|
+
if (old !== null)
|
|
73
|
+
entries.set(row.id, { version: 1, rowId: row.id, pairKey: row.pairKey, ...(row.runId === undefined ? {} : { runId: row.runId }), ref: old.ref, state: 'reserved', createdAt: old.createdAt, expiresAt: old.expiresAt, updatedAt: now });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const adopted = [...entries.values()].sort((a, b) => a.rowId.localeCompare(b.rowId));
|
|
77
|
+
let wrote = false;
|
|
78
|
+
for (const entry of adopted) {
|
|
79
|
+
const path = shadowStagingIndexPath(home, entry.rowId);
|
|
80
|
+
const text = JSON.stringify(entry, null, 2) + '\n';
|
|
81
|
+
const current = await readTextOrNull(io, path);
|
|
82
|
+
if (current !== null) {
|
|
83
|
+
try {
|
|
84
|
+
const previous = JSON.parse(current);
|
|
85
|
+
if (previous.rowId === entry.rowId && previous.state === entry.state && previous.ref === entry.ref && previous.expiresAt === entry.expiresAt)
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
catch { /* malformed shadow entry is replaced below */ }
|
|
89
|
+
}
|
|
90
|
+
if (io.transact !== undefined)
|
|
91
|
+
await io.transact(path, () => text);
|
|
92
|
+
else
|
|
93
|
+
await io.writeText(path, text);
|
|
94
|
+
wrote = true;
|
|
95
|
+
}
|
|
96
|
+
return { entries: adopted, anomalies, shadowPath: shadowStagingRoot(home), wrote };
|
|
97
|
+
}
|
|
98
|
+
export async function readShadowStageAggregate(io, home, rowId) {
|
|
99
|
+
if (rowId !== undefined) {
|
|
100
|
+
const raw = await readTextOrNull(io, shadowStagingIndexPath(home, rowId));
|
|
101
|
+
if (raw === null)
|
|
102
|
+
return { entries: [], shadowPath: shadowStagingRoot(home) };
|
|
103
|
+
try {
|
|
104
|
+
return { entries: [JSON.parse(raw)], shadowPath: shadowStagingRoot(home) };
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return { entries: [], shadowPath: shadowStagingRoot(home) };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const listed = await io.exists(shadowStagingRoot(home)) ? await io.list(shadowStagingRoot(home)) : [];
|
|
111
|
+
const entries = [];
|
|
112
|
+
for (const name of listed) {
|
|
113
|
+
const raw = await readTextOrNull(io, join(shadowStagingRoot(home), name));
|
|
114
|
+
if (raw === null)
|
|
115
|
+
continue;
|
|
116
|
+
try {
|
|
117
|
+
entries.push(JSON.parse(raw));
|
|
118
|
+
}
|
|
119
|
+
catch { /* ignore malformed shadow */ }
|
|
120
|
+
}
|
|
121
|
+
entries.sort((a, b) => a.rowId.localeCompare(b.rowId));
|
|
122
|
+
return { entries, shadowPath: shadowStagingRoot(home) };
|
|
123
|
+
}
|
|
124
|
+
/** 1b-2b execution order; kept here so 1b-2a tests can pin the contract. */
|
|
125
|
+
export const STAGE_WRITE_ORDER = ['index:reserved', 'tree', 'index:present'];
|
|
126
|
+
export function admitStage(rowState) {
|
|
127
|
+
return rowState === 'approved' || rowState === 'fuse' ? { ok: true } : { ok: false, message: 'stage requires approved or fuse: ' + rowState };
|
|
128
|
+
}
|
|
129
|
+
export function planApprovedReserved(rowState, entry) {
|
|
130
|
+
if (entry === null)
|
|
131
|
+
return rowState === 'approved' || rowState === 'fuse' ? { kind: 'drop-orphan', rowId: '' } : { kind: 'none' };
|
|
132
|
+
if (rowState === 'approved' && entry.state === 'reserved')
|
|
133
|
+
return { kind: 'transition-fuse', rowId: entry.rowId, ref: entry.ref, expiresAt: entry.expiresAt };
|
|
134
|
+
if (rowState === 'approved' && entry.state === 'present')
|
|
135
|
+
return { kind: 'transition-fuse', rowId: entry.rowId, ref: entry.ref, expiresAt: entry.expiresAt };
|
|
136
|
+
if (rowState === 'fuse' && entry.state === 'reserved')
|
|
137
|
+
return { kind: 'write-present', rowId: entry.rowId, ref: entry.ref };
|
|
138
|
+
return { kind: 'none' };
|
|
139
|
+
}
|
|
140
|
+
/** Production adoption for 1b-2b normalize; writes the existing staging-index, not shadow. */
|
|
141
|
+
export async function adoptLegacyProduction(io, home) {
|
|
142
|
+
const ledger = await readPairLedger(io, home);
|
|
143
|
+
const entries = new Map();
|
|
144
|
+
const anomalies = [];
|
|
145
|
+
const now = Date.now();
|
|
146
|
+
for (const fact of markerStaging(ledger.migration)) {
|
|
147
|
+
const present = fact.ref !== '' && await io.exists(stagingTreePath(home, fact.ref));
|
|
148
|
+
entries.set(fact.rowId, { version: 1, rowId: fact.rowId, pairKey: fact.pairKey, ...(fact.runId === undefined ? {} : { runId: fact.runId }), ref: fact.ref, state: present ? 'present' : 'lost', createdAt: fact.createdAt, expiresAt: fact.expiresAt, updatedAt: now });
|
|
149
|
+
}
|
|
150
|
+
for (const row of ledger.rows) {
|
|
151
|
+
if (row.state === 'fuse' && !entries.has(row.id)) {
|
|
152
|
+
if (row.staging.kind === 'present')
|
|
153
|
+
entries.set(row.id, { version: 1, rowId: row.id, pairKey: row.pairKey, ...(row.runId === undefined ? {} : { runId: row.runId }), ref: row.staging.ref, state: 'present', createdAt: now, expiresAt: row.staging.expiresAt, updatedAt: now });
|
|
154
|
+
else if (row.staging.kind === 'expired')
|
|
155
|
+
entries.set(row.id, { version: 1, rowId: row.id, pairKey: row.pairKey, ...(row.runId === undefined ? {} : { runId: row.runId }), ref: row.staging.ref ?? '', state: 'expired', createdAt: now, expiresAt: row.staging.expiresAt, updatedAt: now });
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (row.state === 'pending') {
|
|
159
|
+
if (await readStagingIndex(io, home, row.id, now) !== null)
|
|
160
|
+
anomalies.push('pending-staging-index:' + row.id);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (row.state === 'approved' && !entries.has(row.id)) {
|
|
164
|
+
const old = await readStagingIndex(io, home, row.id, now);
|
|
165
|
+
if (old !== null)
|
|
166
|
+
entries.set(row.id, { version: 1, rowId: row.id, pairKey: row.pairKey, ...(row.runId === undefined ? {} : { runId: row.runId }), ref: old.ref, state: 'reserved', createdAt: old.createdAt, expiresAt: old.expiresAt, updatedAt: now });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const adopted = [...entries.values()].sort((a, b) => a.rowId.localeCompare(b.rowId));
|
|
170
|
+
let wrote = false;
|
|
171
|
+
for (const entry of adopted) {
|
|
172
|
+
const current = await readStagingIndex(io, home, entry.rowId, 0);
|
|
173
|
+
if (current !== null && current.ref === entry.ref && current.expiresAt === entry.expiresAt)
|
|
174
|
+
continue;
|
|
175
|
+
await writeStagingIndex(io, home, { rowId: entry.rowId, pairKey: entry.pairKey, ref: entry.ref, expiresAt: entry.expiresAt, ...(entry.runId === undefined ? {} : { runId: entry.runId }), createdAt: entry.createdAt, state: entry.state });
|
|
176
|
+
wrote = true;
|
|
177
|
+
}
|
|
178
|
+
return { entries: adopted, anomalies, shadowPath: join(home, 'merge', 'staging-index'), wrote };
|
|
179
|
+
}
|
|
180
|
+
/** Production stage-tree facade. These wrappers keep the low-level tree module private to StageAggregate. */
|
|
181
|
+
export async function ensureStageTree(io, home, input) { return await ensureStaging(io, home, input); }
|
|
182
|
+
export async function createStageTree(io, home, input) { return await createStaging(io, home, input); }
|
|
183
|
+
export async function readStageTree(io, home, runId) { return await readStaging(io, home, runId); }
|
|
184
|
+
export async function setStageTreeTarget(io, home, stageKey, target, mode) { return await setStagingTarget(io, home, stageKey, target, mode); }
|
|
185
|
+
export async function dropStageTree(io, home, stageKey) { await dropStaging(io, home, stageKey); }
|
|
186
|
+
export async function writeStageIndex(io, home, entry) { await writeStagingIndex(io, home, entry); }
|
|
187
|
+
export async function readStageIndex(io, home, rowId, now = Date.now()) { return await readStagingIndex(io, home, rowId, now); }
|
|
188
|
+
export async function readStageEntry(io, home, rowId) {
|
|
189
|
+
const raw = await readTextOrNull(io, join(home, 'merge', 'staging-index', encodeURIComponent(rowId) + '.json'));
|
|
190
|
+
if (raw === null)
|
|
191
|
+
return null;
|
|
192
|
+
try {
|
|
193
|
+
const entry = JSON.parse(raw);
|
|
194
|
+
if (entry.rowId !== rowId || typeof entry.ref !== 'string' || typeof entry.expiresAt !== 'number')
|
|
195
|
+
return null;
|
|
196
|
+
return entry;
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
export async function reserveStage(io, home, entry) {
|
|
203
|
+
await writeStageIndex(io, home, { rowId: entry.rowId, pairKey: entry.pairKey, ref: entry.ref, expiresAt: entry.expiresAt, ...(entry.runId === undefined ? {} : { runId: entry.runId }), createdAt: entry.createdAt, state: 'reserved' });
|
|
204
|
+
}
|
|
205
|
+
export async function presentStage(io, home, entry) {
|
|
206
|
+
await writeStageIndex(io, home, { rowId: entry.rowId, pairKey: entry.pairKey, ref: entry.ref, expiresAt: entry.expiresAt, ...(entry.runId === undefined ? {} : { runId: entry.runId }), createdAt: entry.createdAt, state: 'present' });
|
|
207
|
+
}
|
|
208
|
+
export async function dropStage(io, home, rowId, ref) {
|
|
209
|
+
await io.remove(join(home, 'merge', 'staging-index', encodeURIComponent(rowId) + '.json')).catch(() => { });
|
|
210
|
+
if (ref !== undefined && ref !== '')
|
|
211
|
+
await io.remove(join(home, 'merge', 'runs', ref)).catch(() => { });
|
|
212
|
+
}
|
|
213
|
+
/** Bounded cleanup: reserved/present TTL, pending/verify stale index removal, terminal/absent rows, orphan trees. */
|
|
214
|
+
export async function cleanupStage(io, home, now, rows, limit = 100) {
|
|
215
|
+
const removed = [];
|
|
216
|
+
const expired = [];
|
|
217
|
+
const kept = [];
|
|
218
|
+
const orphans = [];
|
|
219
|
+
const refs = new Set();
|
|
220
|
+
const indexRoot = join(home, 'merge', 'staging-index');
|
|
221
|
+
const names = await io.exists(indexRoot) ? await listOrEmpty(io, indexRoot) : [];
|
|
222
|
+
for (const name of names.slice(0, limit)) {
|
|
223
|
+
if (!name.endsWith('.json'))
|
|
224
|
+
continue;
|
|
225
|
+
const raw = await readTextOrNull(io, join(indexRoot, name));
|
|
226
|
+
if (raw === null)
|
|
227
|
+
continue;
|
|
228
|
+
let entry;
|
|
229
|
+
try {
|
|
230
|
+
entry = JSON.parse(raw);
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
if (typeof entry.rowId !== 'string' || typeof entry.ref !== 'string' || typeof entry.expiresAt !== 'number')
|
|
236
|
+
continue;
|
|
237
|
+
if (entry.ref !== '')
|
|
238
|
+
refs.add(entry.ref);
|
|
239
|
+
const row = rows.get(entry.rowId);
|
|
240
|
+
const terminal = row === undefined || row.state === 'skipped';
|
|
241
|
+
if (terminal || row?.state === 'pending' || row?.state === 'verify') {
|
|
242
|
+
await dropStage(io, home, entry.rowId, entry.ref);
|
|
243
|
+
removed.push(entry.rowId);
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
if (entry.expiresAt <= now) {
|
|
247
|
+
if (entry.state === 'present') {
|
|
248
|
+
// The index is the durable record both the read face and the resume path consult:
|
|
249
|
+
// stamp it expired instead of leaving a present entry that reads as live elsewhere.
|
|
250
|
+
try {
|
|
251
|
+
await writeStagingIndex(io, home, { rowId: entry.rowId, pairKey: typeof entry.pairKey === 'string' ? entry.pairKey : '', ref: entry.ref, expiresAt: entry.expiresAt, createdAt: typeof entry.createdAt === 'number' ? entry.createdAt : now, state: 'expired' });
|
|
252
|
+
}
|
|
253
|
+
catch { /* the read face still derives expired */ }
|
|
254
|
+
expired.push(entry.rowId);
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
await dropStage(io, home, entry.rowId, entry.ref);
|
|
258
|
+
removed.push(entry.rowId);
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
kept.push(entry.rowId);
|
|
262
|
+
}
|
|
263
|
+
const runRoot = join(home, 'merge', 'runs');
|
|
264
|
+
const runNames = await io.exists(runRoot) ? await listOrEmpty(io, runRoot) : [];
|
|
265
|
+
for (const run of runNames.slice(0, limit)) {
|
|
266
|
+
const prefix = run + '/';
|
|
267
|
+
if ([...refs].some(ref => ref === run || ref.startsWith(prefix)))
|
|
268
|
+
continue;
|
|
269
|
+
await io.remove(join(runRoot, run)).catch(() => { });
|
|
270
|
+
orphans.push(run);
|
|
271
|
+
}
|
|
272
|
+
return { removed, expired, kept, orphans };
|
|
273
|
+
}
|
|
274
|
+
/** Read all production v2 stage entries. */
|
|
275
|
+
export async function readAllStages(io, home) {
|
|
276
|
+
const root = join(home, 'merge', 'staging-index');
|
|
277
|
+
const names = await io.exists(root) ? await listOrEmpty(io, root) : [];
|
|
278
|
+
const entries = [];
|
|
279
|
+
for (const name of names) {
|
|
280
|
+
const raw = await readTextOrNull(io, join(root, name));
|
|
281
|
+
if (raw === null)
|
|
282
|
+
continue;
|
|
283
|
+
try {
|
|
284
|
+
const entry = JSON.parse(raw);
|
|
285
|
+
if (typeof entry.rowId !== 'string' || typeof entry.ref !== 'string' || typeof entry.expiresAt !== 'number')
|
|
286
|
+
continue;
|
|
287
|
+
entries.push(entry);
|
|
288
|
+
}
|
|
289
|
+
catch { /* skip malformed index */ }
|
|
290
|
+
}
|
|
291
|
+
return entries.sort((a, b) => a.rowId.localeCompare(b.rowId));
|
|
292
|
+
}
|
|
293
|
+
//# sourceMappingURL=stage-aggregate.js.map
|
|
@@ -6,6 +6,7 @@ export interface StagingEntry {
|
|
|
6
6
|
readonly expiresAt: number;
|
|
7
7
|
readonly runId?: string;
|
|
8
8
|
readonly createdAt: number;
|
|
9
|
+
readonly state?: 'reserved' | 'present' | 'expired' | 'lost';
|
|
9
10
|
}
|
|
10
11
|
export declare function stagingIndexPath(home: string, rowId: string): string;
|
|
11
12
|
export declare function readStagingIndex(io: EvolutionIoLike, home: string, rowId: string, now?: number): Promise<StagingEntry | null>;
|