@lmzhen/dsh-evolution-merge 0.30.17 → 0.30.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. package/README.md +25 -7
  2. package/data/minhash_params.json +1063 -0
  3. package/lib/index.js +7308 -1800
  4. package/lib/types/adapters/channel.d.ts +1 -0
  5. package/lib/types/adapters/channel.js +15 -1
  6. package/lib/types/adapters/engine-screen.d.ts +1 -1
  7. package/lib/types/adapters/engine-screen.js +5 -2
  8. package/lib/types/adapters/library.js +4 -1
  9. package/lib/types/adapters/model-runner.d.ts +0 -5
  10. package/lib/types/adapters/model-runner.js +8 -2
  11. package/lib/types/adapters/model.d.ts +31 -0
  12. package/lib/types/adapters/model.js +47 -0
  13. package/lib/types/adapters/select.js +12 -8
  14. package/lib/types/approve-command.d.ts +14 -1
  15. package/lib/types/approve-command.js +72 -44
  16. package/lib/types/authoring-flow.d.ts +19 -1
  17. package/lib/types/authoring-flow.js +49 -15
  18. package/lib/types/channel-port.d.ts +3 -1
  19. package/lib/types/channel-port.js +8 -3
  20. package/lib/types/command-defs.d.ts +5 -0
  21. package/lib/types/command-defs.js +33 -0
  22. package/lib/types/command-surface.d.ts +5 -0
  23. package/lib/types/command-surface.js +11 -0
  24. package/lib/types/commands.d.ts +3 -4
  25. package/lib/types/commands.js +33 -72
  26. package/lib/types/commit-assembly.d.ts +16 -1
  27. package/lib/types/commit-assembly.js +72 -11
  28. package/lib/types/commit-execute.d.ts +14 -2
  29. package/lib/types/commit-execute.js +76 -14
  30. package/lib/types/commit-plan.d.ts +7 -1
  31. package/lib/types/commit-plan.js +4 -0
  32. package/lib/types/commit-receipt.d.ts +3 -0
  33. package/lib/types/commit-recovery.d.ts +7 -2
  34. package/lib/types/commit-recovery.js +73 -96
  35. package/lib/types/contracts.d.ts +44 -1
  36. package/lib/types/contracts.js +1 -1
  37. package/lib/types/coordinator.d.ts +3 -2
  38. package/lib/types/coordinator.js +4 -3
  39. package/lib/types/decision.js +1 -1
  40. package/lib/types/doctor.d.ts +18 -1
  41. package/lib/types/doctor.js +164 -10
  42. package/lib/types/domain/authoring-manifest.d.ts +26 -0
  43. package/lib/types/domain/authoring-manifest.js +43 -0
  44. package/lib/types/domain/claim.d.ts +4 -1
  45. package/lib/types/domain/claim.js +2 -2
  46. package/lib/types/domain/commit-journal-store.js +5 -1
  47. package/lib/types/domain/commit-journal.js +1 -1
  48. package/lib/types/domain/consistency.d.ts +22 -0
  49. package/lib/types/domain/consistency.js +50 -0
  50. package/lib/types/domain/engine-criteria.d.ts +10 -0
  51. package/lib/types/domain/engine-criteria.js +11 -0
  52. package/lib/types/domain/engine-input.d.ts +10 -0
  53. package/lib/types/domain/engine-input.js +26 -0
  54. package/lib/types/domain/errors.d.ts +15 -1
  55. package/lib/types/domain/errors.js +23 -4
  56. package/lib/types/domain/evidence-hash.d.ts +3 -0
  57. package/lib/types/domain/evidence-hash.js +14 -0
  58. package/lib/types/domain/failure-route.d.ts +14 -0
  59. package/lib/types/domain/failure-route.js +12 -0
  60. package/lib/types/domain/ledger.js +10 -1
  61. package/lib/types/domain/live-facts.d.ts +31 -0
  62. package/lib/types/domain/live-facts.js +54 -0
  63. package/lib/types/domain/merge-limits.d.ts +18 -0
  64. package/lib/types/domain/merge-limits.js +22 -0
  65. package/lib/types/domain/merge-report.d.ts +41 -0
  66. package/lib/types/domain/merge-report.js +2 -0
  67. package/lib/types/domain/merge-snapshot.d.ts +99 -0
  68. package/lib/types/domain/merge-snapshot.js +82 -0
  69. package/lib/types/domain/migrate-v3.d.ts +20 -0
  70. package/lib/types/domain/migrate-v3.js +68 -0
  71. package/lib/types/domain/pair-ledger.d.ts +34 -0
  72. package/lib/types/domain/pair-ledger.js +96 -0
  73. package/lib/types/domain/pre-1b-backup.d.ts +43 -0
  74. package/lib/types/domain/pre-1b-backup.js +140 -0
  75. package/lib/types/domain/provider-key.d.ts +2 -0
  76. package/lib/types/domain/provider-key.js +11 -0
  77. package/lib/types/domain/read-probe.d.ts +5 -0
  78. package/lib/types/domain/read-probe.js +11 -0
  79. package/lib/types/domain/reconcile.d.ts +47 -0
  80. package/lib/types/domain/reconcile.js +178 -0
  81. package/lib/types/domain/resolve-pair.d.ts +2 -9
  82. package/lib/types/domain/resolve-pair.js +2 -15
  83. package/lib/types/domain/row-tx.d.ts +27 -0
  84. package/lib/types/domain/row-tx.js +31 -0
  85. package/lib/types/domain/row-v3-bridge.d.ts +6 -0
  86. package/lib/types/domain/row-v3-bridge.js +47 -0
  87. package/lib/types/domain/row-v3.d.ts +113 -0
  88. package/lib/types/domain/row-v3.js +107 -0
  89. package/lib/types/domain/row-v4.d.ts +148 -0
  90. package/lib/types/domain/row-v4.js +159 -0
  91. package/lib/types/domain/row.d.ts +1 -0
  92. package/lib/types/domain/run-coordinator.d.ts +51 -0
  93. package/lib/types/domain/run-coordinator.js +45 -0
  94. package/lib/types/domain/schema4.d.ts +53 -0
  95. package/lib/types/domain/schema4.js +129 -0
  96. package/lib/types/domain/selection-policy.d.ts +14 -0
  97. package/lib/types/domain/selection-policy.js +6 -0
  98. package/lib/types/domain/skipped-expiry.d.ts +5 -0
  99. package/lib/types/domain/skipped-expiry.js +16 -0
  100. package/lib/types/domain/stage-aggregate.d.ts +96 -0
  101. package/lib/types/domain/stage-aggregate.js +293 -0
  102. package/lib/types/domain/staging-index.d.ts +15 -0
  103. package/lib/types/domain/staging-index.js +41 -0
  104. package/lib/types/domain/state-bootstrap.d.ts +43 -0
  105. package/lib/types/domain/state-bootstrap.js +267 -0
  106. package/lib/types/domain/state-v3.d.ts +4 -0
  107. package/lib/types/domain/state-v3.js +4 -0
  108. package/lib/types/domain/undo-journal.d.ts +19 -0
  109. package/lib/types/domain/undo-journal.js +20 -0
  110. package/lib/types/domain/undo-receipt.d.ts +18 -0
  111. package/lib/types/domain/undo-receipt.js +22 -0
  112. package/lib/types/domain/validation-plan.d.ts +65 -0
  113. package/lib/types/domain/validation-plan.js +49 -0
  114. package/lib/types/facts.d.ts +26 -0
  115. package/lib/types/facts.js +65 -0
  116. package/lib/types/gate-filter.d.ts +31 -0
  117. package/lib/types/gate-filter.js +64 -0
  118. package/lib/types/index.d.ts +7 -2
  119. package/lib/types/index.js +185 -82
  120. package/lib/types/ladder.js +1 -1
  121. package/lib/types/merge-command.d.ts +3 -1
  122. package/lib/types/merge-command.js +10 -6
  123. package/lib/types/merge-routes.d.ts +12 -7
  124. package/lib/types/merge-routes.js +87 -23
  125. package/lib/types/merge-tool.d.ts +1 -1
  126. package/lib/types/merge-tool.js +2 -2
  127. package/lib/types/metrics.d.ts +16 -0
  128. package/lib/types/metrics.js +27 -0
  129. package/lib/types/new-child-authoring.d.ts +5 -1
  130. package/lib/types/new-child-authoring.js +71 -13
  131. package/lib/types/params.d.ts +2 -4
  132. package/lib/types/params.js +13 -0
  133. package/lib/types/pending-actions.d.ts +2 -2
  134. package/lib/types/pending-actions.js +17 -17
  135. package/lib/types/prompts.d.ts +2 -4
  136. package/lib/types/prompts.js +21 -8
  137. package/lib/types/propose.d.ts +26 -0
  138. package/lib/types/propose.js +69 -0
  139. package/lib/types/read-commands.d.ts +4 -4
  140. package/lib/types/read-commands.js +19 -26
  141. package/lib/types/recovery-coordinator.d.ts +34 -0
  142. package/lib/types/recovery-coordinator.js +84 -0
  143. package/lib/types/recovery-projection.d.ts +31 -0
  144. package/lib/types/recovery-projection.js +96 -0
  145. package/lib/types/resume-command.d.ts +7 -1
  146. package/lib/types/resume-command.js +50 -14
  147. package/lib/types/revision.d.ts +17 -0
  148. package/lib/types/revision.js +39 -3
  149. package/lib/types/round2-outcome.d.ts +2 -1
  150. package/lib/types/round2-outcome.js +68 -12
  151. package/lib/types/run-shell.d.ts +16 -2
  152. package/lib/types/run-shell.js +49 -5
  153. package/lib/types/runtime.d.ts +4 -6
  154. package/lib/types/runtime.js +341 -86
  155. package/lib/types/scan-command.d.ts +1 -0
  156. package/lib/types/scan-command.js +2 -0
  157. package/lib/types/scan-ports.d.ts +21 -1
  158. package/lib/types/scan-ports.js +51 -23
  159. package/lib/types/scan-run.d.ts +17 -0
  160. package/lib/types/scan-run.js +221 -63
  161. package/lib/types/screening-port.d.ts +47 -0
  162. package/lib/types/screening-port.js +93 -0
  163. package/lib/types/screening.d.ts +30 -0
  164. package/lib/types/screening.js +75 -0
  165. package/lib/types/stage-tool-registry.d.ts +7 -0
  166. package/lib/types/stage-tool-registry.js +12 -1
  167. package/lib/types/state.d.ts +15 -1
  168. package/lib/types/state.js +39 -17
  169. package/lib/types/status.d.ts +8 -0
  170. package/lib/types/status.js +64 -0
  171. package/lib/types/surface/body.d.ts +4 -0
  172. package/lib/types/surface/body.js +16 -0
  173. package/lib/types/surface/merge-view.d.ts +14 -0
  174. package/lib/types/surface/merge-view.js +126 -0
  175. package/lib/types/target-claim.d.ts +1 -0
  176. package/lib/types/target-claim.js +63 -18
  177. package/lib/types/undo-operation.d.ts +24 -0
  178. package/lib/types/undo-operation.js +135 -0
  179. package/lib/types/unit-claim.d.ts +5 -0
  180. package/lib/types/unit-claim.js +15 -0
  181. package/lib/types/wire-projection.d.ts +36 -0
  182. package/lib/types/wire-projection.js +69 -0
  183. package/lib/types/wire.d.ts +273 -0
  184. package/lib/types/wire.js +3 -0
  185. package/package.json +10 -4
  186. package/lib/types/routes.d.ts +0 -70
  187. package/lib/types/routes.js +0 -78
@@ -0,0 +1,47 @@
1
+ import { type EvolutionIoLike } from '@lmzhen/dsh-evolution-core';
2
+ import { type LiveRowFacts } from './live-facts.ts';
3
+ import type { V4Row } from './row-v4.ts';
4
+ export declare const RECONCILE_REPORT_VERSION = 1;
5
+ export type ReconcileActionKind = 'skipped-expired' | 'pending-expired' | 'approved-fused' | 'source-gone';
6
+ export type ReconcileActionCode = 'skipped-ttl' | 'target-taken' | 'source-changed' | 'stage-present' | 'stage-lost' | 'stage-expired' | 'source-gone';
7
+ /** One durable row transition reconcile performed. */
8
+ export interface ReconcileAction {
9
+ readonly kind: ReconcileActionKind;
10
+ readonly rowId: string;
11
+ readonly from: V4Row['state'];
12
+ readonly to: V4Row['state'] | 'removed';
13
+ readonly code: ReconcileActionCode;
14
+ readonly detail?: string;
15
+ }
16
+ /** The persisted report a CLI/doctor consumer reads; `state:'blocked'` means nothing was written. */
17
+ export interface ReconcileReport {
18
+ readonly version: typeof RECONCILE_REPORT_VERSION;
19
+ readonly at: number;
20
+ readonly state: 'ok' | 'blocked';
21
+ readonly code?: string;
22
+ readonly message?: string;
23
+ readonly actions: readonly ReconcileAction[];
24
+ readonly notes: readonly string[];
25
+ }
26
+ export interface ReconcileOptions {
27
+ readonly io: EvolutionIoLike;
28
+ readonly home: string;
29
+ readonly now: () => number;
30
+ /** Skipped rows past this TTL get their durable expiredAt stamp. */
31
+ readonly ttlDays?: number;
32
+ /** Live managed skill names. When supplied, active rows whose sources vanished are cleared. */
33
+ readonly managed?: ReadonlySet<string>;
34
+ /** Live revision facts; when supplied, pending rows that went stale are stamped expired. */
35
+ readonly live?: LiveRowFacts;
36
+ }
37
+ export declare function reconcileReportPath(home: string): string;
38
+ /** Read the last reconcile report; a malformed report is absent, never a throw. */
39
+ export declare function readReconcileReport(io: EvolutionIoLike, home: string): Promise<ReconcileReport | null>;
40
+ /**
41
+ * Converge the row layer once, atomically, then persist the report.
42
+ * A blocked state writes no rows and no report; every other run is idempotent.
43
+ */
44
+ export declare function reconcileMergeState(options: ReconcileOptions): Promise<ReconcileReport>;
45
+ /** One-line text face for the CLI/doctor consumers. */
46
+ export declare function formatReconcileReport(report: ReconcileReport): string;
47
+ //# sourceMappingURL=reconcile.d.ts.map
@@ -0,0 +1,178 @@
1
+ import { readTextOrNull } from "./read-probe.js";
2
+ /**
3
+ * Explicit merge reconciliation: the ONE writer of row-state convergence.
4
+ *
5
+ * Reconcile owns the durable corrections that used to be scattered across startup
6
+ * (skipped TTL expiry, approved+reserved continuation) and reports them in
7
+ * `<home>/merge/reconcile.json`. Snapshots and all four GET routes stay read-only:
8
+ * they may DERIVE a fact (an expired stage, a source that disappeared) but never
9
+ * persist it; only this module and the command lifecycle write rows.
10
+ */
11
+ import { join } from 'node:path';
12
+ import { transactIo } from '@lmzhen/dsh-evolution-core';
13
+ import { readV4Readiness } from "./schema4.js";
14
+ import { pairLedger } from "./pair-ledger.js";
15
+ import { planSkippedExpiry } from "./skipped-expiry.js";
16
+ import { presentStage, readAllStages, readStageTree, writeStageIndex } from "./stage-aggregate.js";
17
+ import { pendingExpiry } from "./live-facts.js";
18
+ export const RECONCILE_REPORT_VERSION = 1;
19
+ export function reconcileReportPath(home) {
20
+ return join(home, 'merge', 'reconcile.json');
21
+ }
22
+ /** Read the last reconcile report; a malformed report is absent, never a throw. */
23
+ export async function readReconcileReport(io, home) {
24
+ const raw = await readTextOrNull(io, reconcileReportPath(home));
25
+ if (raw === null)
26
+ return null;
27
+ try {
28
+ const parsed = JSON.parse(raw);
29
+ if (parsed.version !== RECONCILE_REPORT_VERSION || (parsed.state !== 'ok' && parsed.state !== 'blocked'))
30
+ return null;
31
+ if (typeof parsed.at !== 'number' || !Array.isArray(parsed.actions))
32
+ return null;
33
+ return parsed;
34
+ }
35
+ catch {
36
+ return null;
37
+ }
38
+ }
39
+ function blockedReport(now, code, message) {
40
+ return { version: RECONCILE_REPORT_VERSION, at: now, state: 'blocked', code, message, actions: [], notes: ['reconcile wrote nothing: the merge state is not readable'] };
41
+ }
42
+ /** One stage's tree can still serve the round-2 author. */
43
+ async function stageUsable(io, home, entry, now) {
44
+ if (entry.state !== 'reserved' && entry.state !== 'present')
45
+ return false;
46
+ if (entry.expiresAt <= now)
47
+ return false;
48
+ return (await readStageTree(io, home, entry.ref).catch(() => ({ kind: 'absent' }))).kind === 'present';
49
+ }
50
+ /**
51
+ * Converge the row layer once, atomically, then persist the report.
52
+ * A blocked state writes no rows and no report; every other run is idempotent.
53
+ */
54
+ export async function reconcileMergeState(options) {
55
+ const now = options.now();
56
+ const ttlDays = options.ttlDays ?? 0;
57
+ const ready = await readV4Readiness(options.io, options.home);
58
+ if (!ready.ok)
59
+ return blockedReport(now, ready.code, ready.message);
60
+ const stages = await readAllStages(options.io, options.home);
61
+ const byRow = new Map(stages.map(entry => [entry.rowId, entry]));
62
+ const usable = new Map();
63
+ const expiredStages = [];
64
+ for (const entry of stages) {
65
+ if (entry.state !== 'reserved' && entry.state !== 'present')
66
+ continue;
67
+ if (entry.expiresAt <= now) {
68
+ expiredStages.push(entry);
69
+ continue;
70
+ }
71
+ usable.set(entry.rowId, await stageUsable(options.io, options.home, entry, now));
72
+ }
73
+ const actions = [];
74
+ const promote = [];
75
+ await pairLedger(options.io, options.home).transact(tx => {
76
+ for (const row of [...tx.rows()]) {
77
+ if (row.state === 'skipped' && ttlDays > 0 && planSkippedExpiry([row], ttlDays, now).length === 1) {
78
+ const current = tx.rows().find(item => item.id === row.id);
79
+ if (current !== undefined && tx.transition(row.id, { type: 'mark-skipped-expired', at: now }, current.version).ok) {
80
+ actions.push({ kind: 'skipped-expired', rowId: row.id, from: 'skipped', to: 'skipped', code: 'skipped-ttl' });
81
+ }
82
+ continue;
83
+ }
84
+ if (row.state === 'pending' && row.expired === undefined && options.live !== undefined) {
85
+ const current = tx.rows().find(item => item.id === row.id);
86
+ if (current === undefined || current.state !== 'pending' || current.expired !== undefined)
87
+ continue;
88
+ const kind = pendingExpiry(current, options.live);
89
+ if (kind === null)
90
+ continue;
91
+ if (kind === 'source-gone') {
92
+ const removed = tx.remove(row.id, 'superseded', undefined, current.version);
93
+ if (removed.ok)
94
+ actions.push({ kind: 'source-gone', rowId: row.id, from: 'pending', to: 'removed', code: 'source-gone', detail: [row.a, row.b].filter(name => !options.live?.managed(name)).join(', ') });
95
+ continue;
96
+ }
97
+ const moved = tx.transition(row.id, { type: 'expired', kind }, current.version);
98
+ if (moved.ok)
99
+ actions.push({ kind: 'pending-expired', rowId: row.id, from: 'pending', to: 'pending', code: kind });
100
+ continue;
101
+ }
102
+ if (row.state !== 'approved')
103
+ continue;
104
+ const current = tx.rows().find(item => item.id === row.id);
105
+ if (current === undefined || current.state !== 'approved')
106
+ continue;
107
+ const entry = byRow.get(row.id);
108
+ if (usable.get(row.id) === true) {
109
+ const moved = tx.transition(row.id, { type: 'staged' }, current.version);
110
+ if (moved.ok) {
111
+ actions.push({ kind: 'approved-fused', rowId: row.id, from: 'approved', to: 'fuse', code: 'stage-present' });
112
+ if (entry !== undefined && entry.state === 'reserved')
113
+ promote.push(entry);
114
+ }
115
+ continue;
116
+ }
117
+ const failure = { stage: 'author', code: 'stage-lost', message: 'stage tree is missing, expired, or lost before round2', retryable: true, retryFrom: 'agent2' };
118
+ const moved = tx.transition(row.id, { type: 'failed', failure }, current.version);
119
+ if (moved.ok)
120
+ actions.push({ kind: 'approved-fused', rowId: row.id, from: 'approved', to: 'fuse', code: 'stage-lost' });
121
+ }
122
+ if (options.managed !== undefined) {
123
+ for (const row of [...tx.rows()]) {
124
+ if (row.state === 'skipped')
125
+ continue;
126
+ if (options.managed.has(row.a) && options.managed.has(row.b))
127
+ continue;
128
+ const current = tx.rows().find(item => item.id === row.id);
129
+ if (current === undefined)
130
+ continue;
131
+ const removed = tx.remove(row.id, 'superseded', undefined, current.version);
132
+ if (removed.ok)
133
+ actions.push({ kind: 'source-gone', rowId: row.id, from: current.state, to: 'removed', code: 'source-gone', detail: [row.a, row.b].filter(name => !options.managed?.has(name)).join(', ') });
134
+ }
135
+ }
136
+ return actions.length;
137
+ });
138
+ for (const entry of promote) {
139
+ try {
140
+ await presentStage(options.io, options.home, entry);
141
+ }
142
+ catch { /* the stage index stays reserved; the report still names the row */ }
143
+ }
144
+ // NFR-06: the index (the record the read face and the resume path consult) must agree
145
+ // with the read-side derivation that an expired stage is no longer present.
146
+ for (const entry of expiredStages) {
147
+ try {
148
+ await writeStageIndex(options.io, options.home, { ...entry, state: 'expired' });
149
+ }
150
+ catch { /* report keeps the read-side verdict */ }
151
+ }
152
+ const notes = [];
153
+ if (expiredStages.length > 0)
154
+ notes.push(String(expiredStages.length) + ' expired stage(s) marked in the index');
155
+ if (options.managed === undefined)
156
+ notes.push('source-gone convergence skipped: no live library was supplied');
157
+ if (ttlDays <= 0)
158
+ notes.push('skipped TTL disabled (mergeSkippedTtlDays=0)');
159
+ const report = { version: RECONCILE_REPORT_VERSION, at: now, state: 'ok', actions, notes };
160
+ await transactIo(options.io, reconcileReportPath(options.home), () => JSON.stringify(report, null, 2) + '\n');
161
+ return report;
162
+ }
163
+ /** One-line text face for the CLI/doctor consumers. */
164
+ export function formatReconcileReport(report) {
165
+ if (report.state === 'blocked')
166
+ return 'merge reconcile: blocked ' + (report.code ?? 'merge-state-blocked') + ' — ' + (report.message ?? 'state is not readable');
167
+ const counts = new Map();
168
+ for (const action of report.actions)
169
+ counts.set(action.kind, (counts.get(action.kind) ?? 0) + 1);
170
+ const summary = counts.size === 0 ? 'no actions' : [...counts].map(([kind, count]) => kind + ' ' + String(count)).join(', ');
171
+ const lines = ['merge reconcile: ' + summary + ' (at ' + new Date(report.at).toISOString() + ')'];
172
+ for (const action of report.actions)
173
+ lines.push('- ' + action.rowId + ': ' + action.from + ' → ' + action.to + ' [' + action.code + ']' + (action.detail === undefined ? '' : ' ' + action.detail));
174
+ for (const note of report.notes)
175
+ lines.push('note: ' + note);
176
+ return lines.join('\n');
177
+ }
178
+ //# sourceMappingURL=reconcile.js.map
@@ -1,11 +1,4 @@
1
- /** Pure pairKey lookup over run records. No IO. */
1
+ /** Pure pair-index lookup. No run-record merge fields. */
2
2
  import type { PairIndexEntryV2 } from './indexes.ts';
3
- import type { PairRecord } from './run-record.ts';
4
- export interface PairRunLike {
5
- readonly id: string;
6
- readonly pairKey?: string;
7
- readonly pairs: readonly PairRecord[];
8
- readonly resultRef?: string;
9
- }
10
- export declare function findPairRun(runs: readonly PairRunLike[], pairKey: string): PairIndexEntryV2 | undefined;
3
+ export declare function findPairRun(pairs: readonly PairIndexEntryV2[], pairKey: string): PairIndexEntryV2 | undefined;
11
4
  //# sourceMappingURL=resolve-pair.d.ts.map
@@ -1,17 +1,4 @@
1
- export function findPairRun(runs, pairKey) {
2
- for (const run of runs) {
3
- if (run.pairKey === pairKey) {
4
- return { pairKey, runId: run.id, result: 'merged', ...(run.resultRef === undefined ? {} : { reportRef: run.resultRef }) };
5
- }
6
- const pair = run.pairs.find(item => item.pairKey === pairKey);
7
- if (pair !== undefined) {
8
- return {
9
- pairKey, runId: run.id, result: pair.result === 'undone' ? 'undone' : pair.result === 'kept' ? 'kept' : pair.result === 'failed' ? 'failed' : pair.result === 'cancelled' ? 'cancelled' : 'merged',
10
- ...(pair.target?.actual === undefined ? {} : { target: pair.target.actual }),
11
- ...(run.resultRef === undefined ? {} : { reportRef: run.resultRef }),
12
- };
13
- }
14
- }
15
- return undefined;
1
+ export function findPairRun(pairs, pairKey) {
2
+ return pairs.find(entry => entry.pairKey === pairKey);
16
3
  }
17
4
  //# sourceMappingURL=resolve-pair.js.map
@@ -0,0 +1,27 @@
1
+ /** G3-5 RowTx pure write API: only createVerify/createSkipped/transition. */
2
+ import { type SkippedRowV3, type V3Event, type V3Row, type VerifyRowV3 } from './row-v3.ts';
3
+ export type RowWrite = {
4
+ readonly kind: 'createVerify';
5
+ readonly row: VerifyRowV3;
6
+ } | {
7
+ readonly kind: 'createSkipped';
8
+ readonly row: SkippedRowV3;
9
+ } | {
10
+ readonly kind: 'transition';
11
+ readonly id: string;
12
+ readonly event: V3Event;
13
+ readonly expectedVersion: number;
14
+ };
15
+ type RowWriteErrorCode = 'row-invalid' | 'row-duplicate' | 'row-not-found' | 'version-conflict' | 'illegal-transition';
16
+ export type RowWriteResult = {
17
+ readonly ok: true;
18
+ readonly rows: readonly V3Row[];
19
+ readonly row: V3Row | null;
20
+ } | {
21
+ readonly ok: false;
22
+ readonly code: RowWriteErrorCode;
23
+ readonly message: string;
24
+ };
25
+ export declare function applyRowWrite(rows: readonly V3Row[], write: RowWrite, at: string): RowWriteResult;
26
+ export {};
27
+ //# sourceMappingURL=row-tx.d.ts.map
@@ -0,0 +1,31 @@
1
+ /** G3-5 RowTx pure write API: only createVerify/createSkipped/transition. */
2
+ import { isV3Row, transitionV3 } from "./row-v3.js";
3
+ function bad(code, message) { return { ok: false, code, message }; }
4
+ export function applyRowWrite(rows, write, at) {
5
+ if (write.kind === 'transition') {
6
+ const index = rows.findIndex(row => row.id === write.id);
7
+ if (index < 0)
8
+ return bad('row-not-found', 'row not found: ' + write.id);
9
+ const current = rows[index];
10
+ const moved = transitionV3(current, write.event, write.expectedVersion, at);
11
+ if (!moved.ok)
12
+ return bad(moved.code, moved.message);
13
+ if (moved.row === null)
14
+ return { ok: true, rows: rows.filter((_, atIndex) => atIndex !== index), row: null };
15
+ const next = [...rows];
16
+ next[index] = moved.row;
17
+ return { ok: true, rows: next, row: moved.row };
18
+ }
19
+ if (write.row.version !== 0)
20
+ return bad('row-invalid', 'create requires version 0');
21
+ if (write.kind === 'createVerify' && (write.row.state !== 'verify' || write.row.verifySubState !== 'unverified'))
22
+ return bad('row-invalid', 'createVerify requires verify/unverified');
23
+ if (write.kind === 'createSkipped' && write.row.state !== 'skipped')
24
+ return bad('row-invalid', 'createSkipped requires skipped');
25
+ if (!isV3Row(write.row))
26
+ return bad('row-invalid', 'created row is invalid');
27
+ if (rows.some(row => row.id === write.row.id))
28
+ return bad('row-duplicate', 'duplicate row id: ' + write.row.id);
29
+ return { ok: true, rows: [...rows, write.row], row: write.row };
30
+ }
31
+ //# sourceMappingURL=row-tx.js.map
@@ -0,0 +1,6 @@
1
+ /** Temporary v3 <-> legacy projection while commands still consume the flat row. */
2
+ import type { MergeRowV2 } from './row.ts';
3
+ import type { V3Row } from './row-v3.ts';
4
+ export declare function v3ToLegacy(row: V3Row): MergeRowV2;
5
+ export declare function legacyToV3(row: MergeRowV2): V3Row | null;
6
+ //# sourceMappingURL=row-v3-bridge.d.ts.map
@@ -0,0 +1,47 @@
1
+ export function v3ToLegacy(row) {
2
+ const common = {
3
+ id: row.id, pairKey: row.pairKey, a: row.a, b: row.b, state: row.state,
4
+ queue: row.state === 'verify' ? 'verify' : row.state === 'pending' ? 'pending' : row.state === 'skipped' ? 'skipped' : 'fuse',
5
+ version: row.version, origin: row.origin, ...(row.runId === undefined ? {} : { runId: row.runId }),
6
+ ...(row.failure === undefined ? {} : { failure: row.failure }),
7
+ createdAt: row.createdAt, updatedAt: row.updatedAt,
8
+ };
9
+ if (row.state === 'verify')
10
+ return { ...common, ...(row.sourceRevision === undefined ? {} : { sourceRevision: row.sourceRevision }), attempts: row.attempts };
11
+ if (row.state === 'skipped')
12
+ return { ...common, reason: row.reason, queue: 'skipped', ...(row.expiredAt === undefined ? {} : { expired: 'target-taken' }), attempts: row.attempts };
13
+ const target = { requested: row.target, ...(row.state === 'fuse' ? { actual: row.target } : {}) };
14
+ if (row.state === 'fuse') {
15
+ return {
16
+ ...common, target, decision: row.decision, attempts: row.attempts,
17
+ ...(row.sourceRevision === undefined ? {} : { sourceRevision: row.sourceRevision }),
18
+ ...(row.runId === undefined ? {} : { runId: row.runId }),
19
+ ...(row.staging.kind === 'present' ? { stagingRef: row.staging.ref, stagingExpiresAt: row.staging.expiresAt } : row.staging.kind === 'expired' ? { stagingExpiresAt: row.staging.expiresAt, ...(row.staging.ref === undefined ? {} : { stagingRef: row.staging.ref }) } : {}),
20
+ };
21
+ }
22
+ return {
23
+ ...common, target, decision: row.decision, attempts: row.attempts,
24
+ ...(row.sourceRevision === undefined ? {} : { sourceRevision: row.sourceRevision }),
25
+ ...(row.state === 'pending' && row.targetRevision !== undefined ? { targetRevision: row.targetRevision } : {}),
26
+ ...(row.state === 'pending' && row.expired !== undefined ? { expired: row.expired } : {}),
27
+ ...(row.state === 'approved' && row.runId !== undefined ? { runId: row.runId } : {}),
28
+ };
29
+ }
30
+ export function legacyToV3(row) {
31
+ const base = { id: row.id, pairKey: row.pairKey, a: row.a, b: row.b, version: row.version ?? 0, createdAt: row.createdAt, updatedAt: row.updatedAt, origin: row.origin ?? 'system', ...(row.runId === undefined ? {} : { runId: row.runId }), ...(row.failure === undefined ? {} : { failure: row.failure }) };
32
+ if (row.state === 'verify')
33
+ return { ...base, state: 'verify', verifySubState: 'unverified', attempts: row.attempts ?? 0, ...(row.sourceRevision === undefined ? {} : { sourceRevision: row.sourceRevision }) };
34
+ if (row.state === 'skipped')
35
+ return { ...base, state: 'skipped', reason: row.reason ?? 'skipped', attempts: row.attempts ?? 0 };
36
+ const targetName = row.target?.requested ?? row.a;
37
+ const decision = row.decision ?? { shouldMerge: false, mode: 'keep', target: targetName, sources: [row.b], rationale: 'legacy row without decision' };
38
+ if (row.state === 'pending')
39
+ return { ...base, state: 'pending', target: targetName, decision, attempts: row.attempts ?? 0, ...(row.sourceRevision === undefined ? {} : { sourceRevision: row.sourceRevision }), ...(row.targetRevision === undefined ? {} : { targetRevision: row.targetRevision }), ...(row.expired === undefined ? {} : { expired: row.expired }) };
40
+ if (row.state === 'approved')
41
+ return { ...base, state: 'approved', target: targetName, decision, attempts: row.attempts ?? 0, ...(row.sourceRevision === undefined ? {} : { sourceRevision: row.sourceRevision }), ...(row.runId === undefined ? {} : { runId: row.runId }) };
42
+ const staging = row.stagingRef !== undefined
43
+ ? row.stagingExpiresAt !== undefined ? { kind: 'present', ref: row.stagingRef, expiresAt: row.stagingExpiresAt } : { kind: 'expired', expiresAt: 0 }
44
+ : { kind: 'absent' };
45
+ return { ...base, state: 'fuse', target: targetName, decision, attempts: row.attempts ?? 0, ...(row.sourceRevision === undefined ? {} : { sourceRevision: row.sourceRevision }), ...(row.runId === undefined ? {} : { runId: row.runId }), staging };
46
+ }
47
+ //# sourceMappingURL=row-v3-bridge.js.map
@@ -0,0 +1,113 @@
1
+ /** G3 v3 row machine (parallel to the v2 row until the schema-3 cutover). */
2
+ import type { MergeDecision, MergeFailure } from '../contracts.ts';
3
+ export type RowOriginV3 = 'agent' | 'human' | 'system';
4
+ export type VerifySubStateV3 = 'candidate' | 'screened' | 'unverified';
5
+ export interface RowBaseV3 {
6
+ readonly id: string;
7
+ readonly pairKey: string;
8
+ readonly a: string;
9
+ readonly b: string;
10
+ readonly version: number;
11
+ readonly createdAt: string;
12
+ readonly updatedAt: string;
13
+ readonly origin: RowOriginV3;
14
+ readonly failure?: MergeFailure;
15
+ readonly runId?: string;
16
+ }
17
+ export interface VerifyRowV3 extends RowBaseV3 {
18
+ readonly state: 'verify';
19
+ readonly verifySubState: VerifySubStateV3;
20
+ readonly sourceRevision?: string;
21
+ readonly attempts: number;
22
+ }
23
+ export interface PendingRowV3 extends RowBaseV3 {
24
+ readonly state: 'pending';
25
+ readonly target: string;
26
+ readonly decision: MergeDecision;
27
+ readonly targetRevision?: string;
28
+ readonly sourceRevision?: string;
29
+ readonly expired?: 'target-taken' | 'source-changed';
30
+ readonly attempts: number;
31
+ }
32
+ export interface ApprovedRowV3 extends RowBaseV3 {
33
+ readonly state: 'approved';
34
+ readonly target: string;
35
+ readonly decision: MergeDecision;
36
+ readonly sourceRevision?: string;
37
+ readonly runId?: string;
38
+ readonly attempts: number;
39
+ }
40
+ export interface FuseRowV3 extends RowBaseV3 {
41
+ readonly state: 'fuse';
42
+ readonly target: string;
43
+ readonly decision: MergeDecision;
44
+ readonly sourceRevision?: string;
45
+ readonly staging: {
46
+ readonly kind: 'present';
47
+ readonly ref: string;
48
+ readonly expiresAt: number;
49
+ } | {
50
+ readonly kind: 'absent';
51
+ } | {
52
+ readonly kind: 'expired';
53
+ readonly expiresAt: number;
54
+ readonly ref?: string;
55
+ };
56
+ readonly runId?: string;
57
+ readonly attempts: number;
58
+ }
59
+ export interface SkippedRowV3 extends RowBaseV3 {
60
+ readonly state: 'skipped';
61
+ readonly reason: string;
62
+ readonly expiredAt?: number;
63
+ readonly attempts: number;
64
+ }
65
+ export type V3Row = VerifyRowV3 | PendingRowV3 | ApprovedRowV3 | FuseRowV3 | SkippedRowV3;
66
+ export type V3Event = {
67
+ readonly type: 'verify-passed';
68
+ readonly decision: MergeDecision;
69
+ readonly target: string;
70
+ readonly sourceRevision?: string;
71
+ } | {
72
+ readonly type: 'approved';
73
+ readonly runId: string;
74
+ } | {
75
+ readonly type: 'auto-approved';
76
+ readonly decision: MergeDecision;
77
+ readonly target: string;
78
+ readonly runId?: string;
79
+ } | {
80
+ readonly type: 'staged';
81
+ readonly ref?: string;
82
+ readonly expiresAt?: number;
83
+ } | {
84
+ readonly type: 'committed';
85
+ readonly unitId: string;
86
+ } | {
87
+ readonly type: 'skipped';
88
+ readonly reason: string;
89
+ readonly origin?: 'agent' | 'human' | 'system';
90
+ } | {
91
+ readonly type: 'expired';
92
+ readonly kind: 'target-taken' | 'source-changed';
93
+ } | {
94
+ readonly type: 'failed';
95
+ readonly failure: MergeFailure;
96
+ } | {
97
+ readonly type: 'restored';
98
+ } | {
99
+ readonly type: 'superseded';
100
+ };
101
+ export type V3Transition = {
102
+ readonly ok: true;
103
+ readonly row: V3Row | null;
104
+ readonly version: number;
105
+ } | {
106
+ readonly ok: false;
107
+ readonly code: 'version-conflict' | 'illegal-transition';
108
+ readonly message: string;
109
+ };
110
+ export declare function transitionV3(row: V3Row, event: V3Event, expectedVersion: number, at?: string): V3Transition;
111
+ /** Runtime validation for a persisted v3 row. */
112
+ export declare function isV3Row(value: unknown): value is V3Row;
113
+ //# sourceMappingURL=row-v3.d.ts.map
@@ -0,0 +1,107 @@
1
+ function bad(code, message) { return { ok: false, code, message }; }
2
+ function advance(row, at, patch) { return { ...row, ...patch, version: row.version + 1, updatedAt: at }; }
3
+ export function transitionV3(row, event, expectedVersion, at = new Date().toISOString()) {
4
+ if (row.version !== expectedVersion)
5
+ return bad('version-conflict', 'expected ' + String(expectedVersion) + ', got ' + String(row.version));
6
+ switch (row.state) {
7
+ case 'verify':
8
+ if (event.type === 'verify-passed')
9
+ return { ok: true, row: advance(row, at, { state: 'pending', target: event.target, decision: event.decision, sourceRevision: event.sourceRevision, attempts: 0 }), version: expectedVersion + 1 };
10
+ if (event.type === 'auto-approved')
11
+ return { ok: true, row: advance(row, at, { state: 'approved', target: event.target, decision: event.decision, attempts: 0, ...(event.runId === undefined ? {} : { runId: event.runId }) }), version: expectedVersion + 1 };
12
+ if (event.type === 'failed')
13
+ return { ok: true, row: advance(row, at, { failure: event.failure, attempts: row.attempts + 1 }), version: expectedVersion + 1 };
14
+ if (event.type === 'skipped')
15
+ return { ok: true, row: advance(row, at, { state: 'skipped', reason: event.reason, attempts: row.attempts }), version: expectedVersion + 1 };
16
+ if (event.type === 'superseded')
17
+ return { ok: true, row: null, version: expectedVersion + 1 };
18
+ return bad('illegal-transition', 'verify/' + event.type);
19
+ case 'pending':
20
+ if (event.type === 'approved')
21
+ return { ok: true, row: advance(row, at, { state: 'approved', runId: event.runId }), version: expectedVersion + 1 };
22
+ if (event.type === 'auto-approved')
23
+ return { ok: true, row: advance(row, at, { state: 'approved', ...(event.runId === undefined ? {} : { runId: event.runId }) }), version: expectedVersion + 1 };
24
+ if (event.type === 'staged')
25
+ return { ok: true, row: advance(row, at, { state: 'fuse', staging: event.ref === undefined ? { kind: 'absent' } : { kind: 'present', ref: event.ref, expiresAt: event.expiresAt ?? Date.now() + 7 * 86_400_000 } }), version: expectedVersion + 1 };
26
+ if (event.type === 'skipped')
27
+ return { ok: true, row: advance(row, at, { state: 'skipped', reason: event.reason, ...(event.origin === undefined ? {} : { origin: event.origin }) }), version: expectedVersion + 1 };
28
+ if (event.type === 'expired')
29
+ return { ok: true, row: advance(row, at, { expired: event.kind }), version: expectedVersion + 1 };
30
+ if (event.type === 'failed')
31
+ return { ok: true, row: advance(row, at, { failure: event.failure, attempts: row.attempts + 1 }), version: expectedVersion + 1 };
32
+ if (event.type === 'superseded')
33
+ return { ok: true, row: null, version: expectedVersion + 1 };
34
+ return bad('illegal-transition', 'pending/' + event.type);
35
+ case 'approved':
36
+ if (event.type === 'staged')
37
+ return { ok: true, row: advance(row, at, { state: 'fuse', staging: event.ref === undefined ? { kind: 'absent' } : { kind: 'present', ref: event.ref, expiresAt: event.expiresAt ?? Date.now() + 7 * 86_400_000 } }), version: expectedVersion + 1 };
38
+ if (event.type === 'committed')
39
+ return { ok: true, row: null, version: expectedVersion + 1 };
40
+ if (event.type === 'skipped')
41
+ return { ok: true, row: advance(row, at, { state: 'skipped', reason: event.reason }), version: expectedVersion + 1 };
42
+ if (event.type === 'failed')
43
+ return event.failure.retryable
44
+ ? { ok: true, row: advance(row, at, { state: 'pending', failure: event.failure, attempts: row.attempts + 1 }), version: expectedVersion + 1 }
45
+ : { ok: true, row: advance(row, at, { state: 'skipped', reason: event.failure.code, failure: event.failure, attempts: row.attempts + 1 }), version: expectedVersion + 1 };
46
+ if (event.type === 'superseded')
47
+ return { ok: true, row: null, version: expectedVersion + 1 };
48
+ return bad('illegal-transition', 'approved/' + event.type);
49
+ case 'fuse':
50
+ if (event.type === 'staged')
51
+ return { ok: true, row: advance(row, at, { staging: event.ref === undefined ? { kind: 'absent' } : { kind: 'present', ref: event.ref, expiresAt: event.expiresAt ?? Date.now() + 7 * 86_400_000 } }), version: expectedVersion + 1 };
52
+ if (event.type === 'committed')
53
+ return { ok: true, row: null, version: expectedVersion + 1 };
54
+ if (event.type === 'skipped')
55
+ return { ok: true, row: advance(row, at, { state: 'skipped', reason: event.reason }), version: expectedVersion + 1 };
56
+ if (event.type === 'failed')
57
+ return event.failure.retryable
58
+ ? { ok: true, row: advance(row, at, { state: 'pending', failure: event.failure, attempts: row.attempts + 1 }), version: expectedVersion + 1 }
59
+ : { ok: true, row: advance(row, at, { state: 'skipped', reason: event.failure.code, failure: event.failure, attempts: row.attempts + 1 }), version: expectedVersion + 1 };
60
+ if (event.type === 'superseded')
61
+ return { ok: true, row: null, version: expectedVersion + 1 };
62
+ return bad('illegal-transition', 'fuse/' + event.type);
63
+ case 'skipped':
64
+ if (event.type === 'restored' || event.type === 'superseded')
65
+ return { ok: true, row: null, version: expectedVersion + 1 };
66
+ return bad('illegal-transition', 'skipped is terminal');
67
+ }
68
+ }
69
+ function object(value) { return value !== null && typeof value === 'object'; }
70
+ function string(value) { return typeof value === 'string' && value.length > 0; }
71
+ function number(value) { return typeof value === 'number' && Number.isFinite(value); }
72
+ /** Runtime validation for a persisted v3 row. */
73
+ export function isV3Row(value) {
74
+ if (!object(value))
75
+ return false;
76
+ if (!string(value.id) || !string(value.pairKey) || !string(value.a) || !string(value.b) || !number(value.version))
77
+ return false;
78
+ if (!string(value.createdAt) || !string(value.updatedAt))
79
+ return false;
80
+ if (value.origin !== 'agent' && value.origin !== 'human' && value.origin !== 'system')
81
+ return false;
82
+ if (value.failure !== undefined && !object(value.failure))
83
+ return false;
84
+ if (!number(value.attempts))
85
+ return false;
86
+ switch (value.state) {
87
+ case 'verify':
88
+ return value.verifySubState === 'candidate' || value.verifySubState === 'screened' || value.verifySubState === 'unverified';
89
+ case 'pending':
90
+ return string(value.target) && object(value.decision);
91
+ case 'approved':
92
+ return string(value.target) && object(value.decision);
93
+ case 'fuse':
94
+ if (!string(value.target) || !object(value.decision) || !object(value.staging))
95
+ return false;
96
+ if (value.staging.kind === 'present')
97
+ return string(value.staging.ref) && number(value.staging.expiresAt);
98
+ if (value.staging.kind === 'expired')
99
+ return number(value.staging.expiresAt);
100
+ return value.staging.kind === 'absent';
101
+ case 'skipped':
102
+ return string(value.reason);
103
+ default:
104
+ return false;
105
+ }
106
+ }
107
+ //# sourceMappingURL=row-v3.js.map