@lmzhen/dsh-evolution-merge 0.30.16 → 0.30.18

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 (90) hide show
  1. package/README.md +4 -3
  2. package/lib/index.js +2325 -855
  3. package/lib/types/adapters/library.js +4 -1
  4. package/lib/types/adapters/model.d.ts +31 -0
  5. package/lib/types/adapters/model.js +46 -0
  6. package/lib/types/approve-command.d.ts +2 -0
  7. package/lib/types/approve-command.js +41 -46
  8. package/lib/types/authoring-flow.d.ts +3 -1
  9. package/lib/types/authoring-flow.js +15 -5
  10. package/lib/types/command-surface.d.ts +5 -0
  11. package/lib/types/command-surface.js +44 -0
  12. package/lib/types/commit-assembly.d.ts +2 -0
  13. package/lib/types/commit-assembly.js +7 -2
  14. package/lib/types/commit-execute.d.ts +7 -1
  15. package/lib/types/commit-execute.js +40 -10
  16. package/lib/types/commit-recovery.d.ts +7 -2
  17. package/lib/types/commit-recovery.js +73 -96
  18. package/lib/types/contracts.d.ts +1 -1
  19. package/lib/types/contracts.js +1 -1
  20. package/lib/types/coordinator.d.ts +3 -2
  21. package/lib/types/coordinator.js +4 -3
  22. package/lib/types/decision.js +1 -1
  23. package/lib/types/doctor.d.ts +7 -0
  24. package/lib/types/doctor.js +104 -4
  25. package/lib/types/domain/claim.d.ts +4 -1
  26. package/lib/types/domain/claim.js +2 -2
  27. package/lib/types/domain/commit-journal-store.js +5 -1
  28. package/lib/types/domain/commit-journal.js +1 -1
  29. package/lib/types/domain/consistency.d.ts +22 -0
  30. package/lib/types/domain/consistency.js +50 -0
  31. package/lib/types/domain/ledger.js +10 -1
  32. package/lib/types/domain/migrate-v3.d.ts +20 -0
  33. package/lib/types/domain/migrate-v3.js +68 -0
  34. package/lib/types/domain/row-tx.d.ts +27 -0
  35. package/lib/types/domain/row-tx.js +31 -0
  36. package/lib/types/domain/row-v3-bridge.d.ts +6 -0
  37. package/lib/types/domain/row-v3-bridge.js +47 -0
  38. package/lib/types/domain/row-v3.d.ts +113 -0
  39. package/lib/types/domain/row-v3.js +107 -0
  40. package/lib/types/domain/row.d.ts +1 -0
  41. package/lib/types/domain/staging-index.d.ts +14 -0
  42. package/lib/types/domain/staging-index.js +41 -0
  43. package/lib/types/domain/state-v3.d.ts +15 -0
  44. package/lib/types/domain/state-v3.js +80 -0
  45. package/lib/types/facts.d.ts +24 -0
  46. package/lib/types/facts.js +68 -0
  47. package/lib/types/gate-filter.d.ts +31 -0
  48. package/lib/types/gate-filter.js +64 -0
  49. package/lib/types/index.d.ts +5 -2
  50. package/lib/types/index.js +16 -26
  51. package/lib/types/ladder.js +1 -1
  52. package/lib/types/merge-routes.d.ts +2 -2
  53. package/lib/types/merge-routes.js +21 -22
  54. package/lib/types/metrics.d.ts +16 -0
  55. package/lib/types/metrics.js +27 -0
  56. package/lib/types/new-child-authoring.d.ts +3 -1
  57. package/lib/types/new-child-authoring.js +14 -3
  58. package/lib/types/pending-actions.d.ts +2 -2
  59. package/lib/types/pending-actions.js +17 -17
  60. package/lib/types/propose.d.ts +26 -0
  61. package/lib/types/propose.js +69 -0
  62. package/lib/types/read-commands.d.ts +2 -2
  63. package/lib/types/read-commands.js +5 -3
  64. package/lib/types/resume-command.d.ts +2 -0
  65. package/lib/types/resume-command.js +4 -1
  66. package/lib/types/round2-outcome.js +17 -9
  67. package/lib/types/runtime.d.ts +4 -1
  68. package/lib/types/runtime.js +99 -29
  69. package/lib/types/scan-ports.d.ts +3 -1
  70. package/lib/types/scan-ports.js +2 -0
  71. package/lib/types/scan-run.js +116 -51
  72. package/lib/types/screening-port.d.ts +47 -0
  73. package/lib/types/screening-port.js +93 -0
  74. package/lib/types/screening.d.ts +30 -0
  75. package/lib/types/screening.js +75 -0
  76. package/lib/types/state.d.ts +14 -0
  77. package/lib/types/state.js +72 -1
  78. package/lib/types/status.d.ts +3 -0
  79. package/lib/types/status.js +17 -0
  80. package/lib/types/target-claim.d.ts +1 -0
  81. package/lib/types/target-claim.js +63 -18
  82. package/lib/types/unit-claim.d.ts +5 -0
  83. package/lib/types/unit-claim.js +15 -0
  84. package/lib/types/wire-projection.d.ts +36 -0
  85. package/lib/types/wire-projection.js +69 -0
  86. package/lib/types/wire.d.ts +254 -0
  87. package/lib/types/wire.js +3 -0
  88. package/package.json +8 -4
  89. package/lib/types/routes.d.ts +0 -70
  90. package/lib/types/routes.js +0 -78
@@ -0,0 +1,68 @@
1
+ function blocked(message) { return { ok: false, code: 'state-migration-blocked', message }; }
2
+ function text(value) { return typeof value === 'string' && value.length > 0; }
3
+ function decisionOf(value) { return value !== null && typeof value === 'object' ? value : undefined; }
4
+ function failureOf(value) { return value !== null && typeof value === 'object' ? value : undefined; }
5
+ function originOf(value) { return value === 'agent' || value === 'human' || value === 'system' ? value : 'system'; }
6
+ export function migrateV2ToV3(raw) {
7
+ const state = raw;
8
+ if (state === null || typeof state !== 'object' || !Array.isArray(state.rows))
9
+ return blocked('v2 state is not {rows[]}');
10
+ if (state.schemaVersion !== 2 && state.schemaVersion !== undefined)
11
+ return blocked('v2 state schemaVersion is ' + String(state.schemaVersion));
12
+ const rows = [];
13
+ const quarantine = [];
14
+ for (const value of state.rows) {
15
+ if (!text(value.id) || !text(value.pairKey) || !text(value.a) || !text(value.b)) {
16
+ quarantine.push({ reason: 'v2 row identity is incomplete', raw: value });
17
+ continue;
18
+ }
19
+ if (!text(value.state) || !text(value.createdAt) || !text(value.updatedAt)) {
20
+ quarantine.push({ reason: 'v2 row state/timestamps are incomplete', raw: value });
21
+ continue;
22
+ }
23
+ const base = { id: value.id, pairKey: value.pairKey, a: value.a, b: value.b, version: 0, createdAt: value.createdAt, updatedAt: value.updatedAt, origin: originOf(value.origin), ...(failureOf(value.failure) === undefined ? {} : { failure: failureOf(value.failure) }) };
24
+ if (value.state === 'verify') {
25
+ const row = { ...base, state: 'verify', verifySubState: 'unverified', attempts: typeof value.attempts === 'number' ? value.attempts : 0, ...(text(value.sourceRevision) ? { sourceRevision: value.sourceRevision } : {}) };
26
+ rows.push(row);
27
+ continue;
28
+ }
29
+ if (value.state === 'skipped') {
30
+ if (!text(value.reason)) {
31
+ quarantine.push({ reason: 'skipped row lacks reason', raw: value });
32
+ continue;
33
+ }
34
+ const row = { ...base, state: 'skipped', reason: value.reason, attempts: typeof value.attempts === 'number' ? value.attempts : 0, ...(value.reason === 'expired' && text(value.updatedAt) ? { expiredAt: Date.parse(value.updatedAt) } : {}) };
35
+ rows.push(row);
36
+ continue;
37
+ }
38
+ const decision = decisionOf(value.decision);
39
+ if (decision === undefined) {
40
+ quarantine.push({ reason: 'row lacks decision', raw: value });
41
+ continue;
42
+ }
43
+ const targetName = text(value.target?.requested) ? value.target.requested : text(decision.target) ? decision.target : undefined;
44
+ if (targetName === undefined) {
45
+ quarantine.push({ reason: 'row lacks target', raw: value });
46
+ continue;
47
+ }
48
+ if (value.state === 'pending') {
49
+ const row = { ...base, state: 'pending', target: targetName, decision, attempts: typeof value.attempts === 'number' ? value.attempts : 0, ...(text(value.targetRevision) ? { targetRevision: value.targetRevision } : {}), ...(text(value.sourceRevision) ? { sourceRevision: value.sourceRevision } : {}), ...(value.expired === 'target-taken' || value.expired === 'source-changed' ? { expired: value.expired } : {}) };
50
+ rows.push(row);
51
+ continue;
52
+ }
53
+ if (value.state === 'approved') {
54
+ rows.push({ ...base, state: 'approved', target: targetName, decision, attempts: typeof value.attempts === 'number' ? value.attempts : 0, ...(text(value.sourceRevision) ? { sourceRevision: value.sourceRevision } : {}), ...(text(value.runId) ? { runId: value.runId } : {}) });
55
+ continue;
56
+ }
57
+ if (value.state === 'fuse') {
58
+ const staging = text(value.stagingRef)
59
+ ? typeof value.stagingExpiresAt === 'number' ? { kind: 'present', ref: value.stagingRef, expiresAt: value.stagingExpiresAt } : { kind: 'expired', expiresAt: 0, ref: value.stagingRef }
60
+ : { kind: 'absent' };
61
+ rows.push({ ...base, state: 'fuse', target: targetName, decision, attempts: typeof value.attempts === 'number' ? value.attempts : 0, ...(text(value.sourceRevision) ? { sourceRevision: value.sourceRevision } : {}), ...(text(value.runId) ? { runId: value.runId } : {}), staging });
62
+ continue;
63
+ }
64
+ quarantine.push({ reason: 'unknown v2 state: ' + value.state, raw: value });
65
+ }
66
+ return { ok: true, state: { schemaVersion: 3, rows }, quarantine };
67
+ }
68
+ //# sourceMappingURL=migrate-v3.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
@@ -15,6 +15,7 @@ export interface MergeRowV2 {
15
15
  readonly unitId?: string;
16
16
  readonly origin?: 'agent' | 'human' | 'system';
17
17
  readonly attempts?: number;
18
+ readonly version?: number;
18
19
  readonly stagingRef?: string;
19
20
  readonly stagingExpiresAt?: number;
20
21
  readonly target?: {
@@ -0,0 +1,14 @@
1
+ import type { EvolutionIoLike } from '@lmzhen/dsh-evolution-core';
2
+ export interface StagingEntry {
3
+ readonly rowId: string;
4
+ readonly pairKey: string;
5
+ readonly ref: string;
6
+ readonly expiresAt: number;
7
+ readonly runId?: string;
8
+ readonly createdAt: number;
9
+ }
10
+ export declare function stagingIndexPath(home: string, rowId: string): string;
11
+ export declare function readStagingIndex(io: EvolutionIoLike, home: string, rowId: string, now?: number): Promise<StagingEntry | null>;
12
+ export declare function writeStagingIndex(io: EvolutionIoLike, home: string, entry: StagingEntry): Promise<void>;
13
+ export declare function deleteStagingIndex(io: EvolutionIoLike, home: string, rowId: string): Promise<void>;
14
+ //# sourceMappingURL=staging-index.d.ts.map
@@ -0,0 +1,41 @@
1
+ /** Durable staging index for v3 pending/approved rows (rowId-keyed). */
2
+ import { join } from 'node:path';
3
+ export function stagingIndexPath(home, rowId) {
4
+ return join(home, 'merge', 'staging-index', encodeURIComponent(rowId) + '.json');
5
+ }
6
+ export async function readStagingIndex(io, home, rowId, now = Date.now()) {
7
+ let raw;
8
+ try {
9
+ raw = await io.readText(stagingIndexPath(home, rowId));
10
+ }
11
+ catch {
12
+ return null;
13
+ }
14
+ if (raw === null)
15
+ return null;
16
+ try {
17
+ const entry = JSON.parse(raw);
18
+ if (entry.rowId !== rowId || typeof entry.ref !== 'string' || typeof entry.expiresAt !== 'number')
19
+ return null;
20
+ if (entry.expiresAt <= now)
21
+ return null;
22
+ return entry;
23
+ }
24
+ catch {
25
+ return null;
26
+ }
27
+ }
28
+ export async function writeStagingIndex(io, home, entry) {
29
+ const text = JSON.stringify(entry, null, 2) + '\n';
30
+ if (io.transact !== undefined)
31
+ await io.transact(stagingIndexPath(home, entry.rowId), () => text);
32
+ else
33
+ await io.writeText(stagingIndexPath(home, entry.rowId), text);
34
+ }
35
+ export async function deleteStagingIndex(io, home, rowId) {
36
+ try {
37
+ await io.remove(stagingIndexPath(home, rowId));
38
+ }
39
+ catch { /* missing/already removed */ }
40
+ }
41
+ //# sourceMappingURL=staging-index.js.map
@@ -0,0 +1,15 @@
1
+ import type { EvolutionIoLike } from '@lmzhen/dsh-evolution-core';
2
+ import { type QuarantinedRow, type V3State } from './migrate-v3.ts';
3
+ export declare function v2BackupPath(home: string): string;
4
+ export type StateMigration = {
5
+ readonly ok: true;
6
+ readonly migrated: boolean;
7
+ readonly state: V3State;
8
+ readonly quarantine: readonly QuarantinedRow[];
9
+ } | {
10
+ readonly ok: false;
11
+ readonly code: 'state-migration-blocked';
12
+ readonly message: string;
13
+ };
14
+ export declare function loadOrMigrateStateV3(io: EvolutionIoLike, home: string): Promise<StateMigration>;
15
+ //# sourceMappingURL=state-v3.d.ts.map
@@ -0,0 +1,80 @@
1
+ /** G3 schema-3 state IO: backup v2, map, atomic v3 write. */
2
+ import { chmod } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { migrateV2ToV3 } from "./migrate-v3.js";
5
+ import { isV3Row } from "./row-v3.js";
6
+ import { writeStagingIndex } from "./staging-index.js";
7
+ export function v2BackupPath(home) { return join(home, 'merge', 'merge-state.v2.bak.json'); }
8
+ function blocked(message) { return { ok: false, code: 'state-migration-blocked', message }; }
9
+ async function readOnly(path) { try {
10
+ await chmod(path, 0o444);
11
+ }
12
+ catch { /* io seam without chmod; v2 bytes still preserved */ } }
13
+ export async function loadOrMigrateStateV3(io, home) {
14
+ const path = join(home, 'merge', 'merge-state.json');
15
+ const raw = await io.readText(path);
16
+ if (raw === null)
17
+ return { ok: true, migrated: false, state: { schemaVersion: 3, rows: [] }, quarantine: [] };
18
+ let parsed;
19
+ try {
20
+ parsed = JSON.parse(raw);
21
+ }
22
+ catch {
23
+ return blocked('v2 state is not JSON');
24
+ }
25
+ if (parsed !== null && typeof parsed === 'object' && parsed.schemaVersion === 3) {
26
+ const rows = parsed.rows;
27
+ if (!Array.isArray(rows) || !rows.every(isV3Row))
28
+ return blocked('v3 state has an invalid row');
29
+ return { ok: true, migrated: false, state: { schemaVersion: 3, rows }, quarantine: [] };
30
+ }
31
+ const mapped = migrateV2ToV3(parsed);
32
+ if (!mapped.ok)
33
+ return blocked(mapped.message);
34
+ const backup = v2BackupPath(home);
35
+ const existing = await io.readText(backup);
36
+ if (existing !== null && existing !== raw)
37
+ return blocked('v2 backup exists with different bytes');
38
+ if (existing === null) {
39
+ try {
40
+ if (io.transact !== undefined)
41
+ await io.transact(backup, () => raw);
42
+ else
43
+ await io.writeText(backup, raw);
44
+ }
45
+ catch {
46
+ return blocked('v2 backup write failed');
47
+ }
48
+ }
49
+ await readOnly(backup);
50
+ const rawRows = parsed.rows;
51
+ if (Array.isArray(rawRows)) {
52
+ for (const row of rawRows) {
53
+ if (typeof row.id !== 'string' || typeof row.pairKey !== 'string' || typeof row.stagingRef !== 'string')
54
+ continue;
55
+ if (row.state !== 'pending' && row.state !== 'approved')
56
+ continue;
57
+ await writeStagingIndex(io, home, { rowId: row.id, pairKey: row.pairKey, ref: row.stagingRef, expiresAt: typeof row.stagingExpiresAt === 'number' ? row.stagingExpiresAt : Date.now() + 7 * 86_400_000, ...(typeof row.runId === 'string' ? { runId: row.runId } : {}), createdAt: typeof row.updatedAt === 'string' && !Number.isNaN(Date.parse(row.updatedAt)) ? Date.parse(row.updatedAt) : Date.now() });
58
+ }
59
+ }
60
+ if (mapped.quarantine.length > 0) {
61
+ try {
62
+ await io.writeText(join(home, 'merge', 'merge-state.quarantine.json'), JSON.stringify(mapped.quarantine, null, 2) + '\n');
63
+ }
64
+ catch {
65
+ return blocked('quarantine write failed; v3 not written');
66
+ }
67
+ }
68
+ const text = JSON.stringify(mapped.state, null, 2) + '\n';
69
+ try {
70
+ if (io.transact !== undefined)
71
+ await io.transact(path, () => text);
72
+ else
73
+ await io.writeText(path, text);
74
+ }
75
+ catch {
76
+ return blocked('v3 state write failed; v2 backup + quarantine retained');
77
+ }
78
+ return { ok: true, migrated: true, state: mapped.state, quarantine: mapped.quarantine };
79
+ }
80
+ //# sourceMappingURL=state-v3.js.map
@@ -0,0 +1,24 @@
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
+ export declare function countsOf(rows: readonly MergeRow[]): MergeCounts;
6
+ export declare function toRunView(record: RunRecord, readAt: number): RunView;
7
+ export interface FactsInput {
8
+ readonly rows: readonly MergeRow[];
9
+ readonly runs: readonly RunRecord[];
10
+ readonly approval: boolean;
11
+ readonly readAt: number;
12
+ readonly notes?: readonly string[];
13
+ }
14
+ export interface ReadFactsOptions {
15
+ readonly io: EvolutionIoLike;
16
+ readonly home: string;
17
+ readonly runs: readonly RunRecord[];
18
+ readonly approval: boolean;
19
+ readonly readAt: number;
20
+ }
21
+ /** GET projection: reads state + runs; never writes. */
22
+ export declare function readFacts(options: ReadFactsOptions): Promise<Facts>;
23
+ export declare function buildFacts(input: FactsInput): Facts;
24
+ //# sourceMappingURL=facts.d.ts.map