@lmzhen/dsh-evolution-merge 0.45.0 → 0.47.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,11 +1,11 @@
1
1
  /**
2
2
  * Four read-only merge routes: view / rows / body / history.
3
- * The host loads rows and runs; these handlers only project wire envelopes.
3
+ * The host loads one snapshot; these handlers only project wire envelopes.
4
+ * There is no second (legacy rows/runs/history) source: the production constructor has
5
+ * always provided the snapshot, and the legacy branches were the only consumers of
6
+ * facts.ts / wire-projection.ts (W11 D2b).
4
7
  */
5
- import type { RunRecord } from '@lmzhen/dsh-evolution-core';
6
8
  import { type ConnectionFetchRouteLike, type DecisionAudit } from '@lmzhen/dsh-evolution-core/http-routes';
7
- import type { MergeRow } from './contracts.ts';
8
- import type { StateReadiness } from './domain/state-bootstrap.ts';
9
9
  import type { MergeSnapshot, BodyRef } from './domain/merge-snapshot.ts';
10
10
  export declare const MERGE_ROUTES: {
11
11
  view: `/api/dsh-evolution/${string}`;
@@ -25,25 +25,9 @@ export interface MergeDecisionResult {
25
25
  readonly code?: string;
26
26
  readonly runId?: string;
27
27
  }
28
- export interface MergeHistoryItem {
29
- readonly name: string;
30
- readonly kind: 'merged' | 'deleted' | 'split' | 'other';
31
- readonly reason: string;
32
- readonly lastVersion: number;
33
- readonly archivedAt: string | null;
34
- readonly sourceRunId?: string;
35
- }
36
28
  export interface MergeRouteSource {
37
- snapshot?(): Promise<MergeSnapshot>;
29
+ snapshot(): Promise<MergeSnapshot>;
38
30
  bodyRef?(ref: BodyRef): Promise<string | null>;
39
- rows?(): Promise<readonly MergeRow[]>;
40
- run?(id: string): Promise<RunRecord | undefined>;
41
- runs?(cursor?: string, limit?: number): Promise<readonly RunRecord[]>;
42
- body?(id: string, side: 'a' | 'b'): Promise<string | null>;
43
- history?(): Promise<readonly MergeHistoryItem[]>;
44
- commands(): readonly string[];
45
- approval?(): boolean;
46
- readiness?(): Promise<StateReadiness>;
47
31
  }
48
32
  export declare function makeMergeRoutes(deps: {
49
33
  getSource: () => MergeRouteSource | undefined;
@@ -1,6 +1,11 @@
1
+ /**
2
+ * Four read-only merge routes: view / rows / body / history.
3
+ * The host loads one snapshot; these handlers only project wire envelopes.
4
+ * There is no second (legacy rows/runs/history) source: the production constructor has
5
+ * always provided the snapshot, and the legacy branches were the only consumers of
6
+ * facts.ts / wire-projection.ts (W11 D2b).
7
+ */
1
8
  import { HOST_ROUTES, json, methodNotAllowed } from '@lmzhen/dsh-evolution-core/http-routes';
2
- import { buildFacts as buildLegacyFacts } from "./facts.js";
3
- import { toBodyView, toHistoryView, toQueuePage } from "./wire-projection.js";
4
9
  import { buildBodyView, buildFacts as buildSnapshotFacts, buildHistoryView, buildQueuePage } from "./surface/merge-view.js";
5
10
  const MERGE_PATHS = HOST_ROUTES.merge;
6
11
  const MERGE_BASE = MERGE_PATHS.view.slice(0, -'view'.length);
@@ -13,17 +18,16 @@ export const MERGE_ROUTES = {
13
18
  };
14
19
  export function makeMergeRoutes(deps, _audit) {
15
20
  const absent = () => json(404, { wireVersion: 1, code: 'E-325', message: 'merge service is not mounted' });
16
- const blocked = (readiness) => json(503, { wireVersion: 1, code: 'merge-state-blocked', readiness });
21
+ const blocked = (snapshot) => snapshot.blocked === undefined ? null : json(503, { wireVersion: 1, code: 'merge-state-blocked', readiness: snapshot.blocked.readiness });
17
22
  const hostOnly = (snapshot) => snapshot.hostOnly === undefined ? null : json(503, { wireVersion: 1, code: 'E-323', message: snapshot.hostOnly.detail ?? 'host data plane unavailable' });
18
- const notReady = async (source) => {
19
- if (source.readiness === undefined)
20
- return null;
21
- const readiness = await source.readiness();
22
- return readiness.ok ? null : blocked(readiness);
23
- };
24
23
  const bad = (message) => json(400, { wireVersion: 1, code: 'merge-bad-query', message });
25
24
  const get = (request) => (request.method === 'GET' ? null : methodNotAllowed(['GET']));
26
25
  const url = (request) => new URL(request.url, 'http://local');
26
+ const snapshotOf = async () => {
27
+ const source = deps.getSource();
28
+ return source === undefined ? null : await source.snapshot();
29
+ };
30
+ const unavailable = (snapshot) => snapshot === null ? absent() : blocked(snapshot) ?? hostOnly(snapshot);
27
31
  return [
28
32
  {
29
33
  path: MERGE_ROUTES.view,
@@ -33,25 +37,11 @@ export function makeMergeRoutes(deps, _audit) {
33
37
  const denied = get(request);
34
38
  if (denied !== null)
35
39
  return denied;
36
- const source = deps.getSource();
37
- if (source === undefined)
38
- return absent();
39
- if (source.snapshot !== undefined) {
40
- const snapshot = await source.snapshot();
41
- if (snapshot.blocked !== undefined)
42
- return json(503, { wireVersion: 1, code: 'merge-state-blocked', readiness: snapshot.blocked.readiness });
43
- const capability = hostOnly(snapshot);
44
- if (capability !== null)
45
- return capability;
46
- return json(200, buildSnapshotFacts(snapshot));
47
- }
48
- const readiness = await notReady(source);
49
- if (readiness !== null)
50
- return readiness;
51
- const readAt = Date.now();
52
- const rows = await source.rows?.() ?? [];
53
- const runs = await source.runs?.(undefined, 20) ?? [];
54
- return json(200, buildLegacyFacts({ rows, runs, approval: source.approval?.() === true, readAt }));
40
+ const snapshot = await snapshotOf();
41
+ const notReady = unavailable(snapshot);
42
+ if (notReady !== null)
43
+ return notReady;
44
+ return json(200, buildSnapshotFacts(snapshot));
55
45
  },
56
46
  },
57
47
  {
@@ -62,9 +52,6 @@ export function makeMergeRoutes(deps, _audit) {
62
52
  const denied = get(request);
63
53
  if (denied !== null)
64
54
  return denied;
65
- const source = deps.getSource();
66
- if (source === undefined)
67
- return absent();
68
55
  const query = url(request).searchParams;
69
56
  const queue = query.get('queue') ?? 'verify';
70
57
  if (queue !== 'verify' && queue !== 'pending' && queue !== 'fuse' && queue !== 'skipped')
@@ -73,27 +60,16 @@ export function makeMergeRoutes(deps, _audit) {
73
60
  if (expiredOnly && queue !== 'pending')
74
61
  return bad('expired is only allowed for pending');
75
62
  const limit = Number.parseInt(query.get('limit') ?? '50', 10);
76
- if (source.snapshot !== undefined) {
77
- const snapshot = await source.snapshot();
78
- if (snapshot.blocked !== undefined)
79
- return json(503, { wireVersion: 1, code: 'merge-state-blocked', readiness: snapshot.blocked.readiness });
80
- const capability = hostOnly(snapshot);
81
- if (capability !== null)
82
- return capability;
83
- try {
84
- return json(200, buildQueuePage(snapshot, queue, expiredOnly, query.get('cursor') ?? undefined, Number.isFinite(limit) ? limit : 50));
85
- }
86
- catch (error) {
87
- return bad(error instanceof Error ? error.message : 'queue cursor is stale');
88
- }
63
+ const snapshot = await snapshotOf();
64
+ const notReady = unavailable(snapshot);
65
+ if (notReady !== null)
66
+ return notReady;
67
+ try {
68
+ return json(200, buildQueuePage(snapshot, queue, expiredOnly, query.get('cursor') ?? undefined, Number.isFinite(limit) ? limit : 50));
69
+ }
70
+ catch (error) {
71
+ return bad(error instanceof Error ? error.message : 'queue cursor is stale');
89
72
  }
90
- const readiness = await notReady(source);
91
- if (readiness !== null)
92
- return readiness;
93
- const offset = Number.parseInt(query.get('cursor') ?? '0', 10);
94
- const rows = await source.rows?.() ?? [];
95
- const page = toQueuePage(rows, queue, expiredOnly, Date.now(), Number.isFinite(limit) ? limit : 50);
96
- return json(200, Number.isFinite(offset) && offset > 0 ? { ...page, rows: page.rows.slice(offset) } : page);
97
73
  },
98
74
  },
99
75
  {
@@ -113,36 +89,26 @@ export function makeMergeRoutes(deps, _audit) {
113
89
  const side = query.get('side');
114
90
  if ((id === null || id === '') && (name === null || name === ''))
115
91
  return json(404, { wireVersion: 1, code: 'merge-body-not-found', message: 'merge body requires id or name' });
116
- if (source.snapshot !== undefined) {
117
- const snapshot = await source.snapshot();
118
- if (snapshot.blocked !== undefined)
119
- return json(503, { wireVersion: 1, code: 'merge-state-blocked', readiness: snapshot.blocked.readiness });
120
- // Body/history are read-only projections of already-written files; a host without
121
- // the live agent data plane can still read past reports and archived bodies.
122
- const sideFilter = side === 'a' ? 'live' : side === 'b' ? 'archive' : side === 'live' || side === 'archive' ? side : undefined;
123
- const refs = snapshot.bodyRefs.filter(ref => (id === null || id === '' || ref.rowId === id)
124
- && (name === null || name === '' || ref.name === name)
125
- && (sideFilter === undefined || ref.side === sideFilter));
126
- if (refs.length === 0)
127
- return json(404, { wireVersion: 1, code: 'merge-body-not-found', message: 'merge body not found' });
128
- if (refs.length > 1)
129
- return bad('merge body is ambiguous; pass id, name and side');
130
- const row = snapshot.rows.find(item => item.row.id === refs[0].rowId);
131
- if (row?.stage?.state === 'lost' || row?.stage?.state === 'expired')
132
- return json(404, { wireVersion: 1, code: 'merge-body-not-found', message: 'merge stage is lost' });
133
- const ref = refs[0];
134
- const content = source.bodyRef === undefined ? null : await source.bodyRef(ref);
135
- return json(200, buildBodyView(snapshot, ref, content ?? ''));
136
- }
137
- const readiness = await notReady(source);
138
- if (readiness !== null)
139
- return readiness;
140
- if (id === null || id === '' || (side !== 'a' && side !== 'b'))
141
- return bad('id and side=a|b are required');
142
- const content = await source.body?.(id, side) ?? null;
143
- return content === null
144
- ? json(200, { wireVersion: 1, code: 'merge-body-missing', message: 'merge body not found: ' + id })
145
- : json(200, toBodyView(id, side === 'a' ? 'live' : 'archive', content, Date.now()));
92
+ const snapshot = await source.snapshot();
93
+ const notReady = blocked(snapshot);
94
+ if (notReady !== null)
95
+ return notReady;
96
+ // Body/history are read-only projections of already-written files; a host without
97
+ // the live agent data plane can still read past reports and archived bodies.
98
+ const sideFilter = side === 'a' ? 'live' : side === 'b' ? 'archive' : side === 'live' || side === 'archive' ? side : undefined;
99
+ const refs = snapshot.bodyRefs.filter(ref => (id === null || id === '' || ref.rowId === id)
100
+ && (name === null || name === '' || ref.name === name)
101
+ && (sideFilter === undefined || ref.side === sideFilter));
102
+ if (refs.length === 0)
103
+ return json(404, { wireVersion: 1, code: 'merge-body-not-found', message: 'merge body not found' });
104
+ if (refs.length > 1)
105
+ return bad('merge body is ambiguous; pass id, name and side');
106
+ const row = snapshot.rows.find(item => item.row.id === refs[0].rowId);
107
+ if (row?.stage?.state === 'lost' || row?.stage?.state === 'expired')
108
+ return json(404, { wireVersion: 1, code: 'merge-body-not-found', message: 'merge stage is lost' });
109
+ const ref = refs[0];
110
+ const content = source.bodyRef === undefined ? null : await source.bodyRef(ref);
111
+ return json(200, buildBodyView(snapshot, ref, content ?? ''));
146
112
  },
147
113
  },
148
114
  {
@@ -153,19 +119,13 @@ export function makeMergeRoutes(deps, _audit) {
153
119
  const denied = get(request);
154
120
  if (denied !== null)
155
121
  return denied;
156
- const source = deps.getSource();
157
- if (source === undefined)
122
+ const snapshot = await snapshotOf();
123
+ if (snapshot === null)
158
124
  return absent();
159
- if (source.snapshot !== undefined) {
160
- const snapshot = await source.snapshot();
161
- if (snapshot.blocked !== undefined)
162
- return json(503, { wireVersion: 1, code: 'merge-state-blocked', readiness: snapshot.blocked.readiness });
163
- return json(200, buildHistoryView(snapshot));
164
- }
165
- const readiness = await notReady(source);
166
- if (readiness !== null)
167
- return readiness;
168
- return json(200, toHistoryView(await source.history?.() ?? [], Date.now()));
125
+ const notReady = blocked(snapshot);
126
+ if (notReady !== null)
127
+ return notReady;
128
+ return json(200, buildHistoryView(snapshot));
169
129
  },
170
130
  },
171
131
  {
@@ -1,7 +1,5 @@
1
1
  import type { EvolutionIoLike } from '@lmzhen/dsh-evolution-core';
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';
2
+ import { type MergeQueue, type MergeRow, type MergeState } from './contracts.ts';
5
3
  export declare function mergeStatePath(home: string): string;
6
4
  export interface LoadMergeStateResult {
7
5
  readonly ok: boolean;
@@ -12,39 +10,5 @@ export interface LoadMergeStateResult {
12
10
  export declare function loadMergeState(io: EvolutionIoLike, home: string): Promise<LoadMergeStateResult>;
13
11
  /** Schema probe for doctor; the only other reader of mergeStatePath. */
14
12
  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>;
23
- /** The ONE write point. The callback returns the whole row set, not a delta. */
24
- export declare function mutateMergeState<T>(_io: EvolutionIoLike, _home: string, _now: () => number, _fn: (rows: readonly MergeRow[]) => {
25
- rows: readonly MergeRow[];
26
- result: T;
27
- }): Promise<T>;
28
- export interface MoveRowPatch {
29
- readonly queue: MergeQueue;
30
- readonly state?: MergePairState;
31
- readonly failure?: MergeRow['failure'];
32
- readonly runId?: string;
33
- readonly unitId?: string;
34
- readonly origin?: MergeRow['origin'];
35
- readonly attempts?: number;
36
- }
37
- export type UpsertExpect = 'any' | 'absent' | 'verify' | 'pending' | 'fuse' | 'skipped';
38
- export type UpsertResult = {
39
- readonly ok: true;
40
- } | {
41
- readonly ok: false;
42
- readonly current: MergeRow;
43
- };
44
- export declare function upsertRow(io: EvolutionIoLike, home: string, now: () => number, row: MergeRow, expect?: UpsertExpect, cas?: string | null): Promise<UpsertResult>;
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>;
48
- export declare function dropRow(io: EvolutionIoLike, home: string, now: () => number, id: string): Promise<boolean>;
49
13
  export declare function rowsOf(io: EvolutionIoLike, home: string, queue?: MergeQueue): Promise<readonly MergeRow[]>;
50
14
  //# sourceMappingURL=state.d.ts.map
@@ -2,12 +2,12 @@
2
2
  * The one merge-state ledger: <home>/merge/merge-state.json.
3
3
  *
4
4
  * Reads are pure projections (no write, no reconciliation side effect). Every write goes
5
- * through mutateMergeState and a single transactIo, so there is no second file and no
5
+ * through pairLedger and a single transactIo, so there is no second file and no
6
6
  * incremental writer. A row's queue is always derived from its state on read; a hand-edited
7
7
  * mismatch is repaired by the next write, never by a GET.
8
8
  */
9
9
  import { join } from 'node:path';
10
- import { MERGE_STATE_VERSION, queueOf, } from "./contracts.js";
10
+ import { MERGE_STATE_VERSION, } from "./contracts.js";
11
11
  import { parseLedger } from "./domain/ledger.js";
12
12
  import { parseV4State } from "./domain/schema4.js";
13
13
  import { v4ToLegacy } from "./domain/row-v4.js";
@@ -67,69 +67,6 @@ export async function loadStateSchema(io, home) {
67
67
  return null;
68
68
  }
69
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
- }
74
- /** The ONE write point. The callback returns the whole row set, not a delta. */
75
- export async function mutateMergeState(_io, _home, _now, _fn) {
76
- throw new Error('legacy-writer-removed');
77
- }
78
- export async function upsertRow(io, home, now, row, expect = 'any', cas) {
79
- return await mutateMergeState(io, home, now, (rows) => {
80
- const index = rows.findIndex(item => item.pairKey === row.pairKey);
81
- const current = index < 0 ? undefined : rows[index];
82
- if (cas !== undefined && ((cas === null && current !== undefined) || (cas !== null && current?.updatedAt !== cas))) {
83
- return { rows, result: { ok: false, current: current ?? row } };
84
- }
85
- if (current !== undefined && expect !== 'any' && (expect === 'absent' || queueOf(current.state) !== expect)) {
86
- return { rows, result: { ok: false, current } };
87
- }
88
- if (current === undefined && expect !== 'any' && expect !== 'absent' && expect !== 'verify') {
89
- return { rows, result: { ok: false, current: row } };
90
- }
91
- const normalized = { ...row, queue: queueOf(row.state) };
92
- const next = [...rows];
93
- if (current === undefined)
94
- next.push(normalized);
95
- else
96
- next[index] = { ...normalized, createdAt: current.createdAt };
97
- return { rows: next, result: { ok: true } };
98
- });
99
- }
100
- export async function moveRow(io, home, now, id, patch) {
101
- return await mutateMergeState(io, home, now, (rows) => {
102
- const index = rows.findIndex(item => item.id === id);
103
- if (index < 0)
104
- return { rows, result: undefined };
105
- const current = rows[index];
106
- const state = patch.state ?? current.state;
107
- const next = {
108
- ...current,
109
- state,
110
- queue: queueOf(state),
111
- updatedAt: new Date(now()).toISOString(),
112
- ...(patch.failure === undefined ? {} : { failure: patch.failure }),
113
- ...(patch.runId === undefined ? {} : { runId: patch.runId }),
114
- ...(patch.unitId === undefined ? {} : { unitId: patch.unitId }),
115
- ...(patch.origin === undefined ? {} : { origin: patch.origin }),
116
- ...(patch.attempts === undefined ? {} : { attempts: patch.attempts }),
117
- };
118
- const copy = [...rows];
119
- copy[index] = next;
120
- return { rows: copy, result: next };
121
- });
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
- }
127
- export async function dropRow(io, home, now, id) {
128
- return await mutateMergeState(io, home, now, (rows) => {
129
- const next = rows.filter(item => item.id !== id);
130
- return { rows: next, result: next.length !== rows.length };
131
- });
132
- }
133
70
  export async function rowsOf(io, home, queue) {
134
71
  const loaded = await loadMergeState(io, home);
135
72
  return queue === undefined ? loaded.state.rows : loaded.state.rows.filter(row => row.queue === queue);
@@ -135,7 +135,7 @@ export function buildRunDetail(snapshot, runId) {
135
135
  const run = snapshot.runs.find(item => item.record.id === runId);
136
136
  if (run === undefined)
137
137
  return null;
138
- 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 };
138
+ 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' : pair.result === 'undone' ? 'undone' : 'merged', ...(pair.target === undefined ? {} : { target: { requested: pair.target } }) })), pairsTruncated: false };
139
139
  }
140
140
  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)) }; }
141
141
  export function buildDoctorView(snapshot) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lmzhen/dsh-evolution-merge",
3
3
  "description": "Skill auto-merge domain: screening, two-round authoring, queues and commit orchestration (community build)",
4
- "version": "0.45.0",
4
+ "version": "0.47.0",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -34,9 +34,9 @@
34
34
  },
35
35
  "dependencies": {
36
36
  "@deepseek-ai/schemastery": "^3.18.5",
37
- "@lmzhen/dsh-evolution-core": "^0.45.0",
38
- "@lmzhen/dsh-evolution-commands": "^0.45.0",
39
- "@lmzhen/dsh-evolution-merge-engine": "0.45.0"
37
+ "@lmzhen/dsh-evolution-core": "^0.47.0",
38
+ "@lmzhen/dsh-evolution-commands": "^0.47.0",
39
+ "@lmzhen/dsh-evolution-merge-engine": "0.47.0"
40
40
  },
41
41
  "peerDependencies": {
42
42
  "@deepseek-ai/cordis": "^4.0.5"
@@ -1,26 +0,0 @@
1
- /** G5-2 Facts pure projection. Reads a snapshot; never writes. */
2
- import type { EvolutionIoLike, RunRecord } from '@lmzhen/dsh-evolution-core';
3
- import type { MergeRow } from './contracts.ts';
4
- import type { Facts, MergeCounts, RunView } from './wire.ts';
5
- import type { MergeRunMeta } from './domain/run-coordinator.ts';
6
- export declare function countsOf(rows: readonly MergeRow[]): MergeCounts;
7
- export declare function toRunView(record: RunRecord, readAt: number, meta?: MergeRunMeta | null): RunView;
8
- export interface FactsInput {
9
- readonly rows: readonly MergeRow[];
10
- readonly runs: readonly RunRecord[];
11
- readonly runMeta?: ReadonlyMap<string, MergeRunMeta>;
12
- readonly approval: boolean;
13
- readonly readAt: number;
14
- readonly notes?: readonly string[];
15
- }
16
- export interface ReadFactsOptions {
17
- readonly io: EvolutionIoLike;
18
- readonly home: string;
19
- readonly runs: readonly RunRecord[];
20
- readonly approval: boolean;
21
- readonly readAt: number;
22
- }
23
- /** GET projection: reads state + runs; never writes. */
24
- export declare function readFacts(options: ReadFactsOptions): Promise<Facts>;
25
- export declare function buildFacts(input: FactsInput): Facts;
26
- //# sourceMappingURL=facts.d.ts.map
@@ -1,65 +0,0 @@
1
- import { MERGE_COMMAND_SURFACE } from "./command-surface.js";
2
- import { readMergeSnapshot } from "./domain/merge-snapshot.js";
3
- import { buildFacts as buildSnapshotFacts } from "./surface/merge-view.js";
4
- const queueOf = (row) => row.state === 'approved' ? 'fuse' : row.queue;
5
- const isExpired = (row) => row.expired !== undefined;
6
- export function countsOf(rows) {
7
- const active = { verify: 0, pending: 0, fuse: 0, skipped: 0 };
8
- let expired = 0;
9
- for (const row of rows) {
10
- if (isExpired(row)) {
11
- expired += 1;
12
- continue;
13
- }
14
- active[queueOf(row)] += 1;
15
- }
16
- return { active, expired };
17
- }
18
- export function toRunView(record, readAt, meta = null) {
19
- const state = record.state;
20
- void meta;
21
- return {
22
- wireVersion: 1,
23
- readAt,
24
- id: record.id,
25
- state,
26
- command: record.command ?? 'scan',
27
- ...(record.stage === undefined ? {} : { stage: record.stage }),
28
- progress: record.progress === undefined ? null : { done: record.progress.done, total: record.progress.total },
29
- startedAt: record.startedAt,
30
- ...(record.endedAt === undefined ? {} : { endedAt: record.endedAt }),
31
- ...(record.failure === undefined ? {} : { failure: { stage: 'settle', code: 'run-failed', retryable: false, retryFrom: 'none', detail: record.failure } }),
32
- ...(record.parentRunId === undefined ? {} : { parentRunId: record.parentRunId }),
33
- actions: [
34
- { id: 'status', command: '/evolution merge status ' + record.id, confirm: false, enabled: true },
35
- { id: 'report', command: '/evolution merge report ' + record.id, confirm: false, enabled: record.resultRef !== undefined },
36
- { id: 'cancel', command: '/evolution merge cancel ' + record.id, confirm: false, enabled: state === 'running' },
37
- ],
38
- };
39
- }
40
- /** GET projection: reads state + runs; never writes. */
41
- export async function readFacts(options) {
42
- const snapshot = await readMergeSnapshot({ io: options.io, home: options.home, runs: options.runs, approval: options.approval, now: () => options.readAt });
43
- if (snapshot.blocked !== undefined)
44
- throw new Error(snapshot.blocked.code + ': ' + snapshot.blocked.message);
45
- return buildSnapshotFacts(snapshot);
46
- }
47
- export function buildFacts(input) {
48
- const ordered = [...input.runs].sort((a, b) => b.startedAt - a.startedAt);
49
- const active = ordered.find(run => run.state === 'running');
50
- const last = ordered.find(run => run.state !== 'running');
51
- const notes = input.notes ?? [];
52
- return {
53
- wireVersion: 1,
54
- readAt: input.readAt,
55
- approval: { mergeApproval: input.approval },
56
- counts: countsOf(input.rows),
57
- active: active === undefined ? null : toRunView(active, input.readAt, input.runMeta?.get(active.id) ?? null),
58
- last: last === undefined ? null : toRunView(last, input.readAt, input.runMeta?.get(last.id) ?? null),
59
- startCommands: { scan: '/evolution merge scan', pair: '/evolution merge pair <a> <b>' },
60
- fallback: MERGE_COMMAND_SURFACE.fallback,
61
- notes,
62
- degraded: notes.length > 0,
63
- };
64
- }
65
- //# sourceMappingURL=facts.js.map
@@ -1,16 +0,0 @@
1
- /** RG6 merge metrics projection: stable names/labels from state + runs. */
2
- import type { RunRecord } from '@lmzhen/dsh-evolution-core';
3
- import type { MergeRow } from './contracts.ts';
4
- export interface MetricSample {
5
- readonly name: string;
6
- readonly labels: Readonly<Record<string, string>>;
7
- readonly value: number;
8
- }
9
- export interface MergeMetricCounters {
10
- readonly lockWaitMs?: number;
11
- readonly claimConflict?: number;
12
- readonly recoveryFail?: number;
13
- }
14
- export declare const MERGE_METRIC_NAMES: readonly ["merge_queue_depth", "merge_run_total", "merge_run_duration_ms", "merge_lock_wait_ms", "merge_claim_conflict_total", "merge_recovery_fail_total"];
15
- export declare function buildMergeMetrics(rows: readonly MergeRow[], runs: readonly RunRecord[], counters?: MergeMetricCounters): readonly MetricSample[];
16
- //# sourceMappingURL=metrics.d.ts.map
@@ -1,27 +0,0 @@
1
- import { countsOf } from "./facts.js";
2
- export const MERGE_METRIC_NAMES = [
3
- 'merge_queue_depth',
4
- 'merge_run_total',
5
- 'merge_run_duration_ms',
6
- 'merge_lock_wait_ms',
7
- 'merge_claim_conflict_total',
8
- 'merge_recovery_fail_total',
9
- ];
10
- export function buildMergeMetrics(rows, runs, counters = {}) {
11
- const counts = countsOf(rows);
12
- const samples = [];
13
- for (const queue of ['verify', 'pending', 'fuse', 'skipped'])
14
- samples.push({ name: 'merge_queue_depth', labels: { queue, kind: 'active' }, value: counts.active[queue] });
15
- samples.push({ name: 'merge_queue_depth', labels: { queue: 'pending', kind: 'expired' }, value: counts.expired });
16
- for (const run of runs) {
17
- const state = run.state;
18
- samples.push({ name: 'merge_run_total', labels: { state }, value: 1 });
19
- if (run.endedAt !== undefined)
20
- samples.push({ name: 'merge_run_duration_ms', labels: { state }, value: Math.max(0, run.endedAt - run.startedAt) });
21
- }
22
- samples.push({ name: 'merge_lock_wait_ms', labels: {}, value: counters.lockWaitMs ?? 0 });
23
- samples.push({ name: 'merge_claim_conflict_total', labels: {}, value: counters.claimConflict ?? 0 });
24
- samples.push({ name: 'merge_recovery_fail_total', labels: {}, value: counters.recoveryFail ?? 0 });
25
- return samples;
26
- }
27
- //# sourceMappingURL=metrics.js.map
@@ -1,36 +0,0 @@
1
- /** G5-4 wire projections from merge rows to the frozen envelopes. */
2
- import type { RunRecord } from '@lmzhen/dsh-evolution-core';
3
- import type { MergeRow } from './contracts.ts';
4
- import type { BodyView, DoctorView, HistoryView, MergeCounts, QueueId, QueuePage, RowFact, RunView, RunsView } from './wire.ts';
5
- /** Host-owned row actions; client only renders/copies them. */
6
- export declare function actionRefs(row: MergeRow): RowFact['actions'];
7
- export declare function toRowFact(row: MergeRow, actions?: RowFact['actions']): RowFact;
8
- export declare function toQueuePage(rows: readonly MergeRow[], queue: QueueId, expiredOnly: boolean, readAt: number, limit?: number): QueuePage;
9
- export declare function toRunsView(runs: readonly RunRecord[], readAt: number): RunsView;
10
- export interface DoctorSourceFact {
11
- readonly key: string;
12
- readonly status: 'ok' | 'warn' | 'fail' | 'unknown';
13
- readonly count?: number;
14
- readonly detail?: string;
15
- }
16
- export declare function toDoctorView(input: {
17
- readonly readAt: number;
18
- readonly counts: MergeCounts;
19
- readonly latestRun?: RunView;
20
- readonly approval: {
21
- mergeApproval: boolean;
22
- writeApproval?: boolean;
23
- };
24
- readonly staging: DoctorView['staging'];
25
- readonly rows: readonly DoctorSourceFact[];
26
- }): DoctorView;
27
- export declare function toBodyView(name: string, side: 'live' | 'archive', text: string, readAt: number): BodyView;
28
- export declare function toHistoryView(items: readonly {
29
- name: string;
30
- kind: 'merged' | 'deleted' | 'split' | 'other';
31
- reason: string;
32
- lastVersion: number;
33
- archivedAt: string | number | null;
34
- sourceRunId?: string;
35
- }[], readAt: number): HistoryView;
36
- //# sourceMappingURL=wire-projection.d.ts.map