@lmzhen/dsh-evolution-merge 0.46.0 → 0.48.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.
Files changed (81) hide show
  1. package/lib/index.js +248 -1224
  2. package/lib/types/adapters/engine-screen.d.ts +0 -3
  3. package/lib/types/authoring-flow.d.ts +5 -0
  4. package/lib/types/authoring-flow.js +17 -10
  5. package/lib/types/commit-execute.js +1 -22
  6. package/lib/types/contracts.d.ts +0 -1
  7. package/lib/types/contracts.js +0 -1
  8. package/lib/types/doctor.d.ts +0 -1
  9. package/lib/types/doctor.js +2 -31
  10. package/lib/types/domain/consistency.d.ts +0 -1
  11. package/lib/types/domain/consistency.js +2 -11
  12. package/lib/types/domain/merge-limits.d.ts +0 -10
  13. package/lib/types/domain/merge-limits.js +0 -3
  14. package/lib/types/domain/merge-snapshot.js +4 -6
  15. package/lib/types/domain/pair-ledger.d.ts +0 -1
  16. package/lib/types/domain/row-v3.d.ts +0 -45
  17. package/lib/types/domain/row-v3.js +0 -68
  18. package/lib/types/domain/row.d.ts +2 -84
  19. package/lib/types/domain/row.js +1 -110
  20. package/lib/types/domain/run-coordinator.d.ts +0 -24
  21. package/lib/types/domain/run-coordinator.js +0 -1
  22. package/lib/types/domain/stage-aggregate.d.ts +0 -42
  23. package/lib/types/domain/stage-aggregate.js +1 -116
  24. package/lib/types/domain/state.d.ts +1 -53
  25. package/lib/types/domain/state.js +0 -55
  26. package/lib/types/domain/undo-journal.d.ts +4 -4
  27. package/lib/types/domain/undo-journal.js +5 -7
  28. package/lib/types/index.d.ts +0 -1
  29. package/lib/types/index.js +0 -6
  30. package/lib/types/merge-routes.d.ts +5 -21
  31. package/lib/types/merge-routes.js +53 -93
  32. package/lib/types/new-child-authoring.js +4 -10
  33. package/lib/types/prompts.d.ts +0 -1
  34. package/lib/types/prompts.js +0 -1
  35. package/lib/types/read-commands.d.ts +0 -1
  36. package/lib/types/read-commands.js +0 -4
  37. package/lib/types/recovery-projection.js +0 -3
  38. package/lib/types/round2-outcome.js +15 -37
  39. package/lib/types/scan-run.js +0 -3
  40. package/lib/types/skipped.d.ts +1 -2
  41. package/lib/types/skipped.js +0 -8
  42. package/lib/types/staging.d.ts +0 -2
  43. package/lib/types/staging.js +0 -9
  44. package/lib/types/state.d.ts +1 -39
  45. package/lib/types/state.js +2 -78
  46. package/lib/types/surface/merge-view.d.ts +1 -2
  47. package/lib/types/surface/merge-view.js +1 -2
  48. package/lib/types/undo-operation.js +0 -9
  49. package/lib/types/unit-protocol.d.ts +6 -0
  50. package/lib/types/unit-protocol.js +72 -102
  51. package/package.json +4 -4
  52. package/lib/types/commit-recovery.d.ts +0 -14
  53. package/lib/types/commit-recovery.js +0 -96
  54. package/lib/types/coordinator.d.ts +0 -77
  55. package/lib/types/coordinator.js +0 -137
  56. package/lib/types/domain/commit-journal-store.d.ts +0 -6
  57. package/lib/types/domain/commit-journal-store.js +0 -26
  58. package/lib/types/domain/commit-journal.d.ts +0 -16
  59. package/lib/types/domain/commit-journal.js +0 -26
  60. package/lib/types/domain/indexes.d.ts +0 -21
  61. package/lib/types/domain/indexes.js +0 -15
  62. package/lib/types/domain/merge-report.d.ts +0 -41
  63. package/lib/types/domain/merge-report.js +0 -2
  64. package/lib/types/domain/pair-index-store.d.ts +0 -6
  65. package/lib/types/domain/pair-index-store.js +0 -25
  66. package/lib/types/domain/pair-index.d.ts +0 -9
  67. package/lib/types/domain/pair-index.js +0 -28
  68. package/lib/types/domain/resolve-pair.d.ts +0 -4
  69. package/lib/types/domain/resolve-pair.js +0 -4
  70. package/lib/types/domain/row-tx.d.ts +0 -27
  71. package/lib/types/domain/row-tx.js +0 -31
  72. package/lib/types/domain/run-record.d.ts +0 -45
  73. package/lib/types/domain/run-record.js +0 -24
  74. package/lib/types/domain/state-v3.d.ts +0 -4
  75. package/lib/types/domain/state-v3.js +0 -4
  76. package/lib/types/facts.d.ts +0 -26
  77. package/lib/types/facts.js +0 -65
  78. package/lib/types/metrics.d.ts +0 -16
  79. package/lib/types/metrics.js +0 -27
  80. package/lib/types/wire-projection.d.ts +0 -36
  81. package/lib/types/wire-projection.js +0 -77
@@ -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
  {
@@ -10,21 +10,15 @@ import { buildValidationPlan } from "./domain/validation-plan.js";
10
10
  import { commitStagedRun } from "./commit-execute.js";
11
11
  import { manifestFilesOf, missingManifestFile, parseAuthoringManifest } from "./domain/authoring-manifest.js";
12
12
  import { MERGE_PROMPT_VERSION, promptHashOf } from "./prompts.js";
13
+ import { targetAdmission } from "./authoring-flow.js";
13
14
  /** Commit-side causes that mean "the world moved"; they must not be retried blindly. */
14
15
  const STALE_CAUSES = new Set(['target-changed', 'source-changed', 'support-changed', 'target-taken', 'target-gone']);
15
16
  export async function runNewChildAuthoring(options, row, decision, signal) {
16
- if (decision.target.includes('/') || decision.target.includes(':'))
17
- return { kind: 'failed', failure: { stage: 'author', code: 'target-invalid', message: 'target must be a bare skill name: ' + decision.target, retryable: false, retryFrom: 'none' } };
18
17
  const targetCard = options.cards.find(card => card.name === decision.target);
19
18
  const createMode = decision.mode === 'create-umbrella';
20
- if (!createMode && targetCard !== undefined) {
21
- if (targetCard.managed !== true)
22
- return { kind: 'failed', failure: { stage: 'author', code: 'target-not-managed', message: 'target is not family-managed: ' + decision.target, retryable: false, retryFrom: 'none' } };
23
- if (targetCard.protectionUnknown === true || targetCard.protectedBy != null)
24
- return { kind: 'failed', failure: { stage: 'author', code: 'target-protected', message: 'target is protected: ' + decision.target, retryable: false, retryFrom: 'none' } };
25
- }
26
- if (!createMode && await options.isBlocked?.(decision.target) === true)
27
- return { kind: 'failed', failure: { stage: 'author', code: 'target-blocked', message: 'target is blocked by curator: ' + decision.target, retryable: false, retryFrom: 'none' } };
19
+ const targetRefusal = await targetAdmission(decision.target, targetCard, createMode, options.isBlocked);
20
+ if (targetRefusal !== null)
21
+ return { kind: 'failed', failure: { stage: 'author', code: targetRefusal.code, message: targetRefusal.message, retryable: false, retryFrom: 'none' } };
28
22
  const at = options.now();
29
23
  // NFR-06: an expired stage is never reused and never re-presented. Drop its tree and
30
24
  // index and author into a fresh one; the recorded decision anchors still guard the
@@ -9,5 +9,4 @@ export declare function buildRound1Prompt(candidate: {
9
9
  readonly b: string;
10
10
  }, cards: readonly SkillCard[]): string;
11
11
  export declare function buildRound2Prompt(row: MergeRow, decision: MergeDecision, mode?: 'continue' | 'fresh'): string;
12
- export declare function buildStandbyPrompt(): string;
13
12
  //# sourceMappingURL=prompts.d.ts.map
@@ -69,5 +69,4 @@ export function buildRound2Prompt(row, decision, mode = 'continue') {
69
69
  ];
70
70
  return (mode === 'fresh' ? ['基于已持久化的 decision。', ...head, ...body] : [...head, ...body]).join('\n');
71
71
  }
72
- export function buildStandbyPrompt() { return STANDBY_PROMPT; }
73
72
  //# sourceMappingURL=prompts.js.map
@@ -7,5 +7,4 @@ import type { LiveRowFacts } from './domain/live-facts.ts';
7
7
  export declare function handleReadOnlyMergeCommand(command: MergeCliCommand, io: EvolutionIoLike, home: string, cards?: readonly {
8
8
  readonly name: string;
9
9
  }[], runs?: readonly RunRecord[], approval?: boolean, live?: () => Promise<LiveRowFacts | undefined>): Promise<string | null>;
10
- export declare function mergeCommandHelp(): string;
11
10
  //# sourceMappingURL=read-commands.d.ts.map
@@ -1,7 +1,6 @@
1
1
  import { readMergeSnapshot } from "./domain/merge-snapshot.js";
2
2
  import { buildFacts, toRowFact } from "./surface/merge-view.js";
3
3
  import { formatFacts, formatSnapshotQueue, formatSnapshotStatus } from "./status.js";
4
- import { mergeUsage } from "./commands.js";
5
4
  export async function handleReadOnlyMergeCommand(command, io, home, cards, runs, approval = false, live) {
6
5
  if (!['status', 'pending', 'candidates', 'skipped'].includes(command.verb))
7
6
  return null;
@@ -27,7 +26,4 @@ export async function handleReadOnlyMergeCommand(command, io, home, cards, runs,
27
26
  }
28
27
  return formatSnapshotQueue(rows.filter(row => row.queue === 'skipped'), 'skipped');
29
28
  }
30
- export function mergeCommandHelp() {
31
- return mergeUsage();
32
- }
33
29
  //# sourceMappingURL=read-commands.js.map
@@ -3,7 +3,6 @@ import { mergeReportPath, readMergeReport, writeMergeReport } from "./report.js"
3
3
  import { pairLedger, readPairLedgerV4 } from "./domain/pair-ledger.js";
4
4
  import { readAllStages, readStageTree, writeStageIndex } from "./domain/stage-aggregate.js";
5
5
  import { dropStage } from "./domain/stage-aggregate.js";
6
- import { recordPair } from "./domain/pair-index-store.js";
7
6
  import { readRunMeta, writeRunMeta } from "./domain/run-coordinator.js";
8
7
  import { evidenceSha256 } from "./domain/evidence-hash.js";
9
8
  export async function projectRecoveredUnit(options) {
@@ -79,8 +78,6 @@ export async function projectRecoveredUnit(options) {
79
78
  await options.onPhase?.('stage-drop');
80
79
  await dropStage(options.io, options.home, stageRowId);
81
80
  }
82
- await options.onPhase?.('pair-index');
83
- await recordPair(options.io, options.home, { pairKey, runId: options.runId, result: 'merged', target });
84
81
  await options.onPhase?.('run-meta');
85
82
  const meta = await readRunMeta(options.io, options.home, options.runId);
86
83
  await writeRunMeta(options.io, options.home, { version: 1, runId: options.runId, ...(meta?.pid === undefined ? {} : { pid: meta.pid }), phase: 'settled', ...(options.interrupted === true ? { stopReason: 'interrupted' } : {}), reportRef: reportPath, pairCount: 1, ...(meta?.parentRunId === undefined ? {} : { parentRunId: meta.parentRunId }), ...(meta?.stagingRef === undefined ? {} : { stagingRef: meta.stagingRef }), ...(meta?.pairs === undefined ? {} : { pairs: meta.pairs }), createdAt: meta?.createdAt ?? new Date(at).toISOString(), updatedAt: new Date(at).toISOString() });
@@ -2,7 +2,6 @@ import { v4ToLegacy } from "./domain/row-v4.js";
2
2
  import { pairLedger } from "./domain/pair-ledger.js";
3
3
  import { dropStage } from "./domain/stage-aggregate.js";
4
4
  import { casExpiredHook } from "./domain/failure-route.js";
5
- import { recordPair } from "./domain/run-coordinator.js";
6
5
  export async function recordRound2Outcome(io, home, now, row, outcome, deferProjection = false, fence) {
7
6
  void now;
8
7
  const reportExtra = {
@@ -10,9 +9,13 @@ export async function recordRound2Outcome(io, home, now, row, outcome, deferProj
10
9
  ...(outcome.criteria === undefined ? {} : { criteria: outcome.criteria }),
11
10
  ...(outcome.validationIssues === undefined ? {} : { validation: { issues: outcome.validationIssues } }),
12
11
  };
12
+ const pair = (outcomeName, extra = {}, withDecision = true) => ({
13
+ id: row.id, pairKey: row.pairKey, a: row.a, b: row.b, queue: row.queue,
14
+ ...(withDecision && row.decision !== undefined ? { decision: row.decision } : {}),
15
+ outcome: outcomeName, archived: [], ...extra,
16
+ });
13
17
  if (outcome.kind === 'merged') {
14
18
  const unitRef = { unitId: outcome.unitId ?? 'unknown', evidenceSha256: outcome.evidenceSha256 ?? 'unknown' };
15
- const target = outcome.target?.actual ?? outcome.target?.requested ?? row.decision?.target;
16
19
  const finalize = async () => {
17
20
  const removed = await pairLedger(io, home, fence).transact(tx => {
18
21
  const current = tx.rows().find(item => item.id === row.id);
@@ -21,31 +24,20 @@ export async function recordRound2Outcome(io, home, now, row, outcome, deferProj
21
24
  if (!removed.ok)
22
25
  throw new Error('row changed before commit record: ' + row.id);
23
26
  await dropStage(io, home, row.id);
24
- await recordPair(io, home, {
25
- pairKey: row.pairKey,
26
- runId: row.runId ?? 'merge',
27
- result: 'merged',
28
- ...(target === undefined ? {} : { target }),
29
- ...(outcome.unitId === undefined ? {} : { reportRef: outcome.unitId }),
30
- });
31
27
  };
32
28
  if (deferProjection)
33
- return { merged: true, finalize, pair: { id: row.id, pairKey: row.pairKey, a: row.a, b: row.b, queue: row.queue, ...(row.decision === undefined ? {} : { decision: row.decision }), outcome: 'merged', archived: outcome.archived ?? [], ...(outcome.lengths === undefined ? {} : { lengths: outcome.lengths }), ...(outcome.unitId === undefined ? {} : { unitId: outcome.unitId }), ...(outcome.target === undefined ? {} : { target: outcome.target }), ...reportExtra } };
34
- {
35
- await finalize();
36
- }
29
+ return { merged: true, finalize, pair: pair('merged', { ...(outcome.lengths === undefined ? {} : { lengths: outcome.lengths }), ...(outcome.unitId === undefined ? {} : { unitId: outcome.unitId }), ...(outcome.target === undefined ? {} : { target: outcome.target }), ...reportExtra }) };
30
+ await finalize();
37
31
  return {
38
32
  merged: true,
39
- pair: {
40
- id: row.id, pairKey: row.pairKey, a: row.a, b: row.b, queue: row.queue,
41
- ...(row.decision === undefined ? {} : { decision: row.decision }),
42
- outcome: 'merged', archived: outcome.archived ?? [],
33
+ pair: pair('merged', {
34
+ ...(outcome.archived === undefined ? {} : { archived: outcome.archived }),
43
35
  ...(outcome.lengths === undefined ? {} : { lengths: outcome.lengths }),
44
36
  ...reportExtra,
45
37
  ...(outcome.unitId === undefined ? {} : { unitId: outcome.unitId }),
46
38
  ...(outcome.target === undefined ? {} : { target: outcome.target }),
47
39
  ...(outcome.validationPlan === undefined ? {} : { validationPlan: outcome.validationPlan }),
48
- },
40
+ }),
49
41
  };
50
42
  }
51
43
  const failure = outcome.failure ?? { stage: 'author', code: 'merge-failed', message: 'round2 failed', retryable: true, retryFrom: 'agent2' };
@@ -57,7 +49,7 @@ export async function recordRound2Outcome(io, home, now, row, outcome, deferProj
57
49
  });
58
50
  if (skipped)
59
51
  await dropStage(io, home, row.id);
60
- return { merged: false, pair: { id: row.id, pairKey: row.pairKey, a: row.a, b: row.b, queue: row.queue, ...(row.decision === undefined ? {} : { decision: row.decision }), outcome: 'failed', archived: [], failure, ...reportExtra } };
52
+ return { merged: false, pair: pair('failed', { failure, ...reportExtra }) };
61
53
  }
62
54
  const cas = casExpiredHook(failure.code);
63
55
  if (cas !== null) {
@@ -70,7 +62,7 @@ export async function recordRound2Outcome(io, home, now, row, outcome, deferProj
70
62
  return moved.ok;
71
63
  });
72
64
  await dropStage(io, home, row.id);
73
- return { merged: false, pair: { id: row.id, pairKey: row.pairKey, a: row.a, b: row.b, queue: row.queue, ...(row.decision === undefined ? {} : { decision: row.decision }), outcome: 'failed', archived: [], failure: terminalFailure, ...reportExtra } };
65
+ return { merged: false, pair: pair('failed', { failure: terminalFailure, ...reportExtra }) };
74
66
  }
75
67
  if (failure.code === 'commit-failed' || failure.code === 'validation-failed') {
76
68
  // FR-EXEC-11/14: report-only failure. The pair leaves the active queue (so a later scan
@@ -81,7 +73,6 @@ export async function recordRound2Outcome(io, home, now, row, outcome, deferProj
81
73
  });
82
74
  if (dropped) {
83
75
  await dropStage(io, home, row.id, row.stagingRef);
84
- await recordPair(io, home, { pairKey: row.pairKey, runId: row.runId ?? 'merge', result: 'failed', ...(row.decision?.target === undefined ? {} : { target: row.decision.target }) });
85
76
  }
86
77
  }
87
78
  if (failure.code === 'source-gone') {
@@ -91,7 +82,7 @@ export async function recordRound2Outcome(io, home, now, row, outcome, deferProj
91
82
  });
92
83
  if (dropped)
93
84
  await dropStage(io, home, row.id, row.stagingRef);
94
- return { merged: false, pair: { id: row.id, pairKey: row.pairKey, a: row.a, b: row.b, queue: row.queue, outcome: 'failed', archived: [], failure } };
85
+ return { merged: false, pair: pair('failed', { failure }, false) };
95
86
  }
96
87
  const projected = await pairLedger(io, home, fence).transact(tx => {
97
88
  const current = tx.rows().find(item => item.id === row.id);
@@ -101,23 +92,10 @@ export async function recordRound2Outcome(io, home, now, row, outcome, deferProj
101
92
  return moved.ok && moved.row !== null ? v4ToLegacy(moved.row) : null;
102
93
  });
103
94
  if (projected === null)
104
- return { merged: false, pair: { id: row.id, pairKey: row.pairKey, a: row.a, b: row.b, queue: row.queue, outcome: 'failed', archived: [], failure: { ...failure, code: 'row-changed', message: 'row changed before failure record' }, ...reportExtra } };
105
- {
106
- await recordPair(io, home, {
107
- pairKey: row.pairKey,
108
- runId: row.runId ?? 'merge',
109
- result: 'failed',
110
- ...(row.decision?.target === undefined ? {} : { target: row.decision.target }),
111
- });
112
- }
95
+ return { merged: false, pair: pair('failed', { failure: { ...failure, code: 'row-changed', message: 'row changed before failure record' }, ...reportExtra }, false) };
113
96
  return {
114
97
  merged: false,
115
- pair: {
116
- id: row.id, pairKey: row.pairKey, a: row.a, b: row.b, queue: row.queue,
117
- ...(row.decision === undefined ? {} : { decision: row.decision }),
118
- outcome: 'failed', archived: [], failure,
119
- ...reportExtra,
120
- },
98
+ pair: pair('failed', { failure, ...reportExtra }),
121
99
  };
122
100
  }
123
101
  //# sourceMappingURL=round2-outcome.js.map
@@ -6,7 +6,6 @@ import { pairKey, rowIdOf, } from "./contracts.js";
6
6
  import { loadMergeState } from "./state.js";
7
7
  import { pairLedger } from "./domain/pair-ledger.js";
8
8
  import { presentStage, dropStage } from "./domain/stage-aggregate.js";
9
- import { recordPair } from "./domain/run-coordinator.js";
10
9
  import { v4ToLegacy } from "./domain/row-v4.js";
11
10
  import { criteriaFrom } from "./domain/engine-criteria.js";
12
11
  import { withDecisionRevisions } from "./revision.js";
@@ -176,7 +175,6 @@ export async function runScan(options) {
176
175
  continue;
177
176
  }
178
177
  row = projected;
179
- await recordPair(options.io, options.home, { pairKey: row.pairKey, runId: row.runId ?? 'merge', result: 'kept' });
180
178
  pairs.push(pairOf(row, candidate, 'kept', { ...(round1.decision === undefined ? {} : { decision: round1.decision }), ...(round1.usage === undefined ? {} : { usage: round1.usage }) }));
181
179
  totals.kept += 1;
182
180
  continue;
@@ -324,7 +322,6 @@ export async function runScan(options) {
324
322
  });
325
323
  if (dropped) {
326
324
  await dropStage(options.io, options.home, row.id, row.stagingRef);
327
- await recordPair(options.io, options.home, { pairKey: row.pairKey, runId: row.runId ?? 'merge', result: 'failed', ...(row.decision?.target === undefined ? {} : { target: row.decision.target }) });
328
325
  }
329
326
  pairs.push(pairOf(row, candidate, 'failed', { decision: mergeDecision, failure: failed, ...(round2.manifestFiles === undefined ? {} : { manifestFiles: round2.manifestFiles }), ...(round2.criteria === undefined ? {} : { criteria: round2.criteria }), ...(round2.validationIssues === undefined ? {} : { validation: { issues: round2.validationIssues } }) }));
330
327
  totals.failed += 1;
@@ -2,7 +2,6 @@
2
2
  * Skipped filtering: a pair the operator marked as not-worth-merging stays out of automatic
3
3
  * screening until mergeSkippedTtlDays expires. Explicit /merge pair ignores this set.
4
4
  */
5
- import { type MergeRow } from './contracts.ts';
5
+ import type { MergeRow } from './contracts.ts';
6
6
  export declare function skippedPairKeys(rows: readonly MergeRow[], ttlDays: number, now: number): ReadonlySet<string>;
7
- export declare function isSkippedPair(keys: ReadonlySet<string>, a: string, b: string): boolean;
8
7
  //# sourceMappingURL=skipped.d.ts.map
@@ -1,8 +1,3 @@
1
- /**
2
- * Skipped filtering: a pair the operator marked as not-worth-merging stays out of automatic
3
- * screening until mergeSkippedTtlDays expires. Explicit /merge pair ignores this set.
4
- */
5
- import { pairKey } from "./contracts.js";
6
1
  export function skippedPairKeys(rows, ttlDays, now) {
7
2
  const keys = new Set();
8
3
  for (const row of rows) {
@@ -18,7 +13,4 @@ export function skippedPairKeys(rows, ttlDays, now) {
18
13
  }
19
14
  return keys;
20
15
  }
21
- export function isSkippedPair(keys, a, b) {
22
- return keys.has(pairKey(a, b));
23
- }
24
16
  //# sourceMappingURL=skipped.js.map
@@ -43,9 +43,7 @@ export interface StagedView {
43
43
  readonly manifest: StagingManifest;
44
44
  }
45
45
  export declare function readStaging(io: EvolutionIoLike, home: string, runId: string): Promise<Probe<StagedView>>;
46
- export declare function ensureStaging(io: EvolutionIoLike, home: string, input: CreateStagingInput): Promise<CreateStagingResult>;
47
46
  export declare function setStagingTarget(io: EvolutionIoLike, home: string, stageKey: string, target: string, mode?: 'merge-into-existing' | 'create-umbrella' | 'keep'): Promise<boolean>;
48
- export declare function dropStaging(io: EvolutionIoLike, home: string, stageKey: string): Promise<void>;
49
47
  export interface StagingCleanupResult {
50
48
  readonly removed: readonly string[];
51
49
  readonly kept: readonly string[];
@@ -93,12 +93,6 @@ export async function readStaging(io, home, runId) {
93
93
  return probeUnknown('staging manifest is not JSON: ' + (error instanceof Error ? error.message : String(error)));
94
94
  }
95
95
  }
96
- export async function ensureStaging(io, home, input) {
97
- const existing = await readStaging(io, home, input.runId);
98
- if (existing.kind === 'present')
99
- return { ok: true, path: stagingRoot(home, input.runId), manifest: existing.value.manifest };
100
- return createStaging(io, home, input);
101
- }
102
96
  export async function setStagingTarget(io, home, stageKey, target, mode) {
103
97
  const view = await readStaging(io, home, stageKey);
104
98
  if (view.kind !== 'present')
@@ -107,9 +101,6 @@ export async function setStagingTarget(io, home, stageKey, target, mode) {
107
101
  await io.writeText(join(stagingRoot(home, stageKey), 'manifest.json'), JSON.stringify(manifest, null, 2) + '\n');
108
102
  return true;
109
103
  }
110
- export async function dropStaging(io, home, stageKey) {
111
- await io.remove(stagingRoot(home, stageKey));
112
- }
113
104
  /** Remove expired run-level staging trees; `keep` names runs a live row still points at. */
114
105
  export async function cleanupExpiredStaging(io, home, now, keep = new Set()) {
115
106
  let runs;
@@ -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;
@@ -10,41 +8,5 @@ export interface LoadMergeStateResult {
10
8
  }
11
9
  /** Pure read: never writes, never deletes a row. */
12
10
  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>;
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
11
  export declare function rowsOf(io: EvolutionIoLike, home: string, queue?: MergeQueue): Promise<readonly MergeRow[]>;
50
12
  //# sourceMappingURL=state.d.ts.map