@lmzhen/dsh-evolution-merge 0.30.24 → 0.30.27

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 (84) hide show
  1. package/lib/index.js +549 -183
  2. package/lib/types/approve-command.js +2 -1
  3. package/lib/types/authoring-flow.d.ts +5 -0
  4. package/lib/types/authoring-flow.js +14 -6
  5. package/lib/types/commands.d.ts +7 -0
  6. package/lib/types/commands.js +8 -1
  7. package/lib/types/commit-execute.d.ts +4 -0
  8. package/lib/types/commit-execute.js +8 -5
  9. package/lib/types/commit-receipt.js +3 -1
  10. package/lib/types/contracts.d.ts +20 -1
  11. package/lib/types/doctor.js +1 -1
  12. package/lib/types/domain/errors.js +5 -1
  13. package/lib/types/domain/merge-snapshot.d.ts +12 -3
  14. package/lib/types/domain/merge-snapshot.js +9 -4
  15. package/lib/types/domain/reconcile.js +1 -1
  16. package/lib/types/domain/row-v4.d.ts +4 -0
  17. package/lib/types/domain/row-v4.js +9 -9
  18. package/lib/types/domain/run-coordinator.d.ts +9 -0
  19. package/lib/types/domain/stage-aggregate.d.ts +6 -1
  20. package/lib/types/domain/stage-aggregate.js +16 -9
  21. package/lib/types/domain/validation-plan.js +5 -1
  22. package/lib/types/host-capabilities.d.ts +22 -0
  23. package/lib/types/host-capabilities.js +24 -0
  24. package/lib/types/index.js +12 -2
  25. package/lib/types/kernel/adapter-v4.d.ts +12 -0
  26. package/lib/types/kernel/adapter-v4.js +54 -0
  27. package/lib/types/kernel/failure.d.ts +8 -0
  28. package/lib/types/kernel/failure.js +33 -0
  29. package/lib/types/kernel/index.d.ts +9 -0
  30. package/lib/types/kernel/index.js +9 -0
  31. package/lib/types/kernel/invariants.d.ts +4 -0
  32. package/lib/types/kernel/invariants.js +47 -0
  33. package/lib/types/kernel/ledger.d.ts +46 -0
  34. package/lib/types/kernel/ledger.js +62 -0
  35. package/lib/types/kernel/machine.d.ts +13 -0
  36. package/lib/types/kernel/machine.js +108 -0
  37. package/lib/types/kernel/model.d.ts +154 -0
  38. package/lib/types/kernel/model.js +5 -0
  39. package/lib/types/kernel/projection.d.ts +28 -0
  40. package/lib/types/kernel/projection.js +44 -0
  41. package/lib/types/kernel/recovery.d.ts +31 -0
  42. package/lib/types/kernel/recovery.js +19 -0
  43. package/lib/types/kernel/unit.d.ts +43 -0
  44. package/lib/types/kernel/unit.js +63 -0
  45. package/lib/types/merge-routes.js +9 -0
  46. package/lib/types/new-child-authoring.d.ts +1 -0
  47. package/lib/types/new-child-authoring.js +15 -7
  48. package/lib/types/pending-actions.d.ts +1 -1
  49. package/lib/types/pending-actions.js +4 -0
  50. package/lib/types/recovery-coordinator.d.ts +2 -0
  51. package/lib/types/recovery-coordinator.js +14 -4
  52. package/lib/types/recovery-projection.d.ts +4 -2
  53. package/lib/types/recovery-projection.js +3 -3
  54. package/lib/types/resume-command.js +2 -1
  55. package/lib/types/round2-outcome.js +26 -9
  56. package/lib/types/run-shell.d.ts +1 -0
  57. package/lib/types/run-shell.js +57 -15
  58. package/lib/types/runtime.d.ts +6 -1
  59. package/lib/types/runtime.js +46 -22
  60. package/lib/types/scan-command.d.ts +1 -0
  61. package/lib/types/scan-command.js +1 -0
  62. package/lib/types/scan-ports.js +3 -0
  63. package/lib/types/scan-run.d.ts +6 -1
  64. package/lib/types/scan-run.js +34 -18
  65. package/lib/types/surface/merge-view.js +12 -4
  66. package/lib/types/undo-operation.d.ts +2 -1
  67. package/lib/types/undo-operation.js +10 -4
  68. package/lib/types/wire-projection.js +7 -2
  69. package/lib/types/wire.d.ts +1 -0
  70. package/package.json +5 -5
  71. package/lib/types/adapters/model.d.ts +0 -31
  72. package/lib/types/adapters/model.js +0 -47
  73. package/lib/types/gate-cache.d.ts +0 -12
  74. package/lib/types/gate-cache.js +0 -36
  75. package/lib/types/gate-filter.d.ts +0 -31
  76. package/lib/types/gate-filter.js +0 -64
  77. package/lib/types/ladder.d.ts +0 -65
  78. package/lib/types/ladder.js +0 -162
  79. package/lib/types/propose.d.ts +0 -26
  80. package/lib/types/propose.js +0 -69
  81. package/lib/types/screening-port.d.ts +0 -47
  82. package/lib/types/screening-port.js +0 -93
  83. package/lib/types/screening.d.ts +0 -30
  84. package/lib/types/screening.js +0 -75
@@ -0,0 +1,24 @@
1
+ /**
2
+ * The host's data-plane capability as ONE value shared by snapshot, doctor, HTTP and run preflight.
3
+ * The probe is injected so the Cordis wiring stays in index.ts; a rejected probe is the same E-323
4
+ * answer as an unavailable one, never an unhandled promise.
5
+ * @module @lmzhen/dsh-evolution-merge
6
+ */
7
+ /**
8
+ * Wrap one probe with the shared error normalization.
9
+ * @param probe - the platform-specific availability check.
10
+ * @returns the capability value consumed by snapshot and runtime.
11
+ */
12
+ export function createHostCapabilities(probe) {
13
+ return {
14
+ hostOnly: async () => {
15
+ try {
16
+ return await probe();
17
+ }
18
+ catch (error) {
19
+ return { code: 'E-323', detail: error instanceof Error ? error.message : String(error) };
20
+ }
21
+ },
22
+ };
23
+ }
24
+ //# sourceMappingURL=host-capabilities.js.map
@@ -31,6 +31,7 @@ import { pairKey, rowIdOf } from "./contracts.js";
31
31
  import { makeMergeRoutes } from "./merge-routes.js";
32
32
  import { createMergeCommand, runMergeCommand } from "./merge-command.js";
33
33
  import { createMergeRuntime } from "./runtime.js";
34
+ import { createHostCapabilities } from "./host-capabilities.js";
34
35
  import { resolveMergeParams } from "./params.js";
35
36
  import { subagentChannelAdapter } from "./adapters/channel.js";
36
37
  import { StageToolRegistry } from "./stage-tool-registry.js";
@@ -214,6 +215,8 @@ export function apply(ctx, rawConfig = {}) {
214
215
  const cleanup = await cleanupStage(io, home, Date.now(), new Map(v4.state.rows.map(row => [row.id, { state: row.state }])));
215
216
  if (cleanup.expired.length > 0 || cleanup.removed.length > 0)
216
217
  ctx.logger.info('merge stage cleanup: ' + String(cleanup.expired.length) + ' expired, ' + String(cleanup.removed.length) + ' removed, ' + String(cleanup.orphans.length) + ' orphan(s)');
218
+ if (cleanup.failures.length > 0)
219
+ ctx.logger.warn('merge stage cleanup failed for ' + String(cleanup.failures.length) + ' path(s): ' + cleanup.failures.map(item => item.path + '=' + item.message).join('; '));
217
220
  // Reconcile is the one row-state convergence writer; startup simply invokes it.
218
221
  // A live library is only supplied when it is non-empty (an empty/unreadable list
219
222
  // must never authorize source-gone removal).
@@ -230,7 +233,12 @@ export function apply(ctx, rawConfig = {}) {
230
233
  }
231
234
  })().catch((error) => { ctx.logger.warn('merge recovery failed: ' + (error instanceof Error ? error.message : String(error))); });
232
235
  }).catch((error) => { ctx.logger.warn('merge state readiness failed: ' + (error instanceof Error ? error.message : String(error))); });
233
- const channel = subagentChannelAdapter(createSubagentChannel(ctx));
236
+ const platformChannel = createSubagentChannel(ctx);
237
+ const channel = subagentChannelAdapter(platformChannel);
238
+ const hostCapabilities = createHostCapabilities(async () => {
239
+ const state = await platformChannel.prepare({ kind: 'reviewer', allow: ['skill'] });
240
+ return state.kind === 'unavailable' ? { code: 'E-323', detail: state.reason } : undefined;
241
+ });
234
242
  const stage = new StageToolRegistry();
235
243
  const engineInput = resolveEngineInput({ modelsRoot: params.mergeModelsRoot });
236
244
  const engineConfig = engineConfigFromRoot(engineInput.modelsRoot);
@@ -273,7 +281,7 @@ export function apply(ctx, rawConfig = {}) {
273
281
  });
274
282
  source = {
275
283
  async snapshot() {
276
- return await readMergeSnapshot({ io, home, runs: runs.runs('merge'), approval: params.mergeApproval, platformWrite, live: async () => liveRowFactsOf(await readSkillCards(skills)), history: async () => {
284
+ return await readMergeSnapshot({ io, home, runs: runs.runs('merge'), approval: params.mergeApproval, platformWrite, hostOnly: hostCapabilities.hostOnly, live: async () => liveRowFactsOf(await readSkillCards(skills)), history: async () => {
277
285
  const items = new Map();
278
286
  const receipts = [...await readCommitReceipts(io, home)].sort((a, b) => a.at - b.at);
279
287
  const generationOf = new Map(receipts.map((receipt, index) => [receipt.runId, index + 1]));
@@ -318,7 +326,9 @@ export function apply(ctx, rawConfig = {}) {
318
326
  return;
319
327
  const runtime = createMergeRuntime({
320
328
  io, home, registry: runs, skills, now: () => Date.now(), params, platformWrite,
329
+ ...(defaultModel === undefined ? {} : { model: defaultModel.currentSelection().model }),
321
330
  channel, parent: undefined, allow: ['merge_stage'], timeoutMinutes: params.mergeRunTimeoutMinutes,
331
+ hostCapabilities,
322
332
  isBlocked: async (name) => {
323
333
  const curator = ctx.get('evolutionCurator');
324
334
  return (await curator?.isBlocked?.(name)) === true;
@@ -0,0 +1,12 @@
1
+ import type { V4Row } from '../domain/row-v4.ts';
2
+ import type { PairSnapshot } from './model.ts';
3
+ export interface V4AdapterResult {
4
+ readonly snapshot: PairSnapshot;
5
+ readonly warnings: readonly string[];
6
+ }
7
+ export declare function fromV4Row(row: V4Row): V4AdapterResult;
8
+ export declare function fromV4Rows(rows: readonly V4Row[]): {
9
+ readonly snapshots: readonly PairSnapshot[];
10
+ readonly warnings: readonly string[];
11
+ };
12
+ //# sourceMappingURL=adapter-v4.d.ts.map
@@ -0,0 +1,54 @@
1
+ function decisionOf(row) {
2
+ return { target: row.target, sources: row.decision.sources, ...(row.decision.rationale === undefined ? {} : { rationale: row.decision.rationale }) };
3
+ }
4
+ function failureOfRow(failure) {
5
+ const known = new Set(['source-gone', 'source-changed', 'target-taken', 'validation-failed', 'commit-failed', 'schema-invalid', 'timeout', 'capability-missing', 'runtime']);
6
+ const code = known.has(failure.code) ? failure.code : 'runtime';
7
+ return { code, stage: failure.stage === 'screening' ? 'screen' : failure.stage === 'decide' ? 'decide' : failure.stage === 'author' ? 'author' : failure.stage === 'validate' ? 'validate' : 'commit', retryable: failure.retryable, reportOnly: true, userMessageKey: 'failure.' + code, detail: failure.message };
8
+ }
9
+ function closedReasonOf(row) {
10
+ if (row.reason.includes('merge-over-limit') || row.reason.includes('over-limit'))
11
+ return 'over-limit';
12
+ return row.origin === 'human' ? 'reject' : 'keep';
13
+ }
14
+ export function fromV4Row(row) {
15
+ const warnings = [];
16
+ const base = { id: row.id, pairKey: row.pairKey, a: row.a, b: row.b, version: row.version, createdAt: row.createdAt, updatedAt: row.updatedAt, origin: row.origin, attempts: row.attempts, ...(row.runId === undefined ? {} : { runId: row.runId }) };
17
+ if (row.failure !== undefined) {
18
+ warnings.push('row carries failure; kernel state=failed (current row state=' + row.state + ')');
19
+ return { snapshot: { ...base, state: 'failed', failure: failureOfRow(row.failure) }, warnings };
20
+ }
21
+ if (row.state === 'verify') {
22
+ return { snapshot: { ...base, state: row.verifySubState === 'screened' ? 'deciding' : 'candidate' }, warnings };
23
+ }
24
+ if (row.state === 'pending') {
25
+ if (row.expired !== undefined) {
26
+ warnings.push('pending expired without a timestamp; kernel state=expired');
27
+ return { snapshot: { ...base, state: 'expired', target: row.target, decision: decisionOf(row), approval: 'on' }, warnings };
28
+ }
29
+ return { snapshot: { ...base, state: 'pending', target: row.target, decision: decisionOf(row), approval: 'on' }, warnings };
30
+ }
31
+ if (row.state === 'approved')
32
+ return { snapshot: { ...base, state: 'approved', target: row.target, decision: decisionOf(row), approval: 'off' }, warnings };
33
+ if (row.state === 'fuse') {
34
+ warnings.push('current fuse row has no stagingRef; kernel state=authoring');
35
+ return { snapshot: { ...base, state: 'authoring', target: row.target, decision: decisionOf(row), approval: 'off' }, warnings };
36
+ }
37
+ if (row.expiredAt !== undefined) {
38
+ warnings.push('skipped-expired row maps to closed+expiresAt, not a failure');
39
+ return { snapshot: { ...base, state: 'closed', closedReason: closedReasonOf(row), expiresAt: row.expiredAt }, warnings };
40
+ }
41
+ return { snapshot: { ...base, state: 'closed', closedReason: closedReasonOf(row) }, warnings };
42
+ }
43
+ export function fromV4Rows(rows) {
44
+ const snapshots = [];
45
+ const warnings = [];
46
+ for (const row of rows) {
47
+ const result = fromV4Row(row);
48
+ snapshots.push(result.snapshot);
49
+ for (const warning of result.warnings)
50
+ warnings.push(row.id + ': ' + warning);
51
+ }
52
+ return { snapshots, warnings };
53
+ }
54
+ //# sourceMappingURL=adapter-v4.js.map
@@ -0,0 +1,8 @@
1
+ /** Pure failure taxonomy: the ONE place a code becomes a route. */
2
+ import type { ClosedReason, Failure, FailureCode } from './model.ts';
3
+ export declare function failureOf(code: FailureCode, detail?: string): Failure;
4
+ /** Only policy closures are closed; failures never map here. */
5
+ export declare function closedReasonFor(code: string): ClosedReason | undefined;
6
+ export declare function isFailureRoute(reportOnly: boolean): boolean;
7
+ export declare const FAILURE_CODES: readonly FailureCode[];
8
+ //# sourceMappingURL=failure.d.ts.map
@@ -0,0 +1,33 @@
1
+ const TABLE = {
2
+ 'bad-pair': { stage: 'command', retryable: true, reportOnly: false, userMessageKey: 'failure.bad-pair' },
3
+ 'bad-target': { stage: 'command', retryable: true, reportOnly: false, userMessageKey: 'failure.bad-target' },
4
+ 'bad-flag': { stage: 'command', retryable: true, reportOnly: false, userMessageKey: 'failure.bad-flag' },
5
+ 'source-gone': { stage: 'decide', retryable: true, reportOnly: true, userMessageKey: 'failure.source-gone' },
6
+ 'source-changed': { stage: 'commit', retryable: true, reportOnly: true, userMessageKey: 'failure.source-changed' },
7
+ 'target-taken': { stage: 'commit', retryable: false, reportOnly: true, userMessageKey: 'failure.target-taken' },
8
+ 'validation-failed': { stage: 'validate', retryable: true, reportOnly: true, userMessageKey: 'failure.validation' },
9
+ 'commit-failed': { stage: 'commit', retryable: false, reportOnly: true, userMessageKey: 'failure.commit' },
10
+ 'schema-invalid': { stage: 'schema', retryable: false, reportOnly: true, userMessageKey: 'failure.schema' },
11
+ timeout: { stage: 'run', retryable: true, reportOnly: true, userMessageKey: 'failure.timeout' },
12
+ 'capability-missing': { stage: 'surface', retryable: false, reportOnly: false, userMessageKey: 'failure.capability' },
13
+ runtime: { stage: 'run', retryable: true, reportOnly: true, userMessageKey: 'failure.runtime' },
14
+ };
15
+ export function failureOf(code, detail) {
16
+ const spec = TABLE[code];
17
+ return { code, stage: spec.stage, retryable: spec.retryable, reportOnly: spec.reportOnly, userMessageKey: spec.userMessageKey, ...(detail === undefined ? {} : { detail }) };
18
+ }
19
+ /** Only policy closures are closed; failures never map here. */
20
+ export function closedReasonFor(code) {
21
+ if (code === 'merge-over-limit')
22
+ return 'over-limit';
23
+ if (code === 'keep')
24
+ return 'keep';
25
+ if (code === 'reject')
26
+ return 'reject';
27
+ return undefined;
28
+ }
29
+ export function isFailureRoute(reportOnly) {
30
+ return reportOnly;
31
+ }
32
+ export const FAILURE_CODES = Object.keys(TABLE);
33
+ //# sourceMappingURL=failure.js.map
@@ -0,0 +1,9 @@
1
+ export * from './model.ts';
2
+ export * from './failure.ts';
3
+ export * from './machine.ts';
4
+ export * from './unit.ts';
5
+ export * from './ledger.ts';
6
+ export * from './projection.ts';
7
+ export * from './recovery.ts';
8
+ export * from './invariants.ts';
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,9 @@
1
+ export * from "./model.js";
2
+ export * from "./failure.js";
3
+ export * from "./machine.js";
4
+ export * from "./unit.js";
5
+ export * from "./ledger.js";
6
+ export * from "./projection.js";
7
+ export * from "./recovery.js";
8
+ export * from "./invariants.js";
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,4 @@
1
+ /** Pure invariant checks over one pair snapshot. */
2
+ import { type PairSnapshot } from './model.ts';
3
+ export declare function checkPair(snapshot: PairSnapshot): string[];
4
+ //# sourceMappingURL=invariants.d.ts.map
@@ -0,0 +1,47 @@
1
+ /** Pure invariant checks over one pair snapshot. */
2
+ import { CLOSED_REASONS } from "./model.js";
3
+ export function checkPair(snapshot) {
4
+ const problems = [];
5
+ if (snapshot.id.length === 0)
6
+ problems.push('id is empty');
7
+ if (snapshot.pairKey.length === 0)
8
+ problems.push('pairKey is empty');
9
+ if (snapshot.a.length === 0 || snapshot.b.length === 0)
10
+ problems.push('a/b is empty');
11
+ if (!Number.isInteger(snapshot.version) || snapshot.version < 0)
12
+ problems.push('version must be a non-negative integer');
13
+ if (snapshot.attempts < 0)
14
+ problems.push('attempts must be non-negative');
15
+ if (snapshot.state === 'closed') {
16
+ if (snapshot.closedReason === undefined || !CLOSED_REASONS.includes(snapshot.closedReason))
17
+ problems.push('closed requires closedReason in ' + CLOSED_REASONS.join('/'));
18
+ if (snapshot.failure !== undefined)
19
+ problems.push('closed must not carry a failure');
20
+ }
21
+ if (snapshot.state === 'failed') {
22
+ if (snapshot.failure === undefined)
23
+ problems.push('failed requires a failure');
24
+ if (snapshot.closedReason !== undefined)
25
+ problems.push('failed must not carry closedReason');
26
+ }
27
+ if (snapshot.state === 'committed') {
28
+ if (snapshot.receipt === undefined)
29
+ problems.push('committed requires a receipt');
30
+ if (snapshot.failure !== undefined)
31
+ problems.push('committed must not carry a failure');
32
+ if (snapshot.unitId === undefined)
33
+ problems.push('committed requires unitId');
34
+ }
35
+ if (snapshot.state === 'committing' && snapshot.unitId === undefined)
36
+ problems.push('committing requires unitId');
37
+ if (snapshot.state === 'staged' && (snapshot.stageRef === undefined || snapshot.stageRef.length === 0))
38
+ problems.push('staged requires stageRef');
39
+ if ((snapshot.state === 'pending' || snapshot.state === 'approved') && snapshot.decision === undefined)
40
+ problems.push(snapshot.state + ' requires a decision');
41
+ if ((snapshot.state === 'pending' || snapshot.state === 'approved') && snapshot.target === undefined)
42
+ problems.push(snapshot.state + ' requires a target');
43
+ if (snapshot.state === 'committed' && snapshot.receipt !== undefined && snapshot.receipt.unitId !== snapshot.unitId)
44
+ problems.push('receipt.unitId must equal unitId');
45
+ return problems;
46
+ }
47
+ //# sourceMappingURL=invariants.js.map
@@ -0,0 +1,46 @@
1
+ /** Pure append-only ledger and deterministic fold. No IO. */
2
+ import type { PairEvent, PairSnapshot } from './model.ts';
3
+ import { type UnitEvent, type UnitSnapshot } from './unit.ts';
4
+ export type LedgerEntry = {
5
+ readonly seq: number;
6
+ readonly at: string;
7
+ readonly type: 'pair-created';
8
+ readonly snapshot: PairSnapshot;
9
+ } | {
10
+ readonly seq: number;
11
+ readonly at: string;
12
+ readonly type: 'pair-transition';
13
+ readonly pairId: string;
14
+ readonly expectedVersion: number;
15
+ readonly event: PairEvent;
16
+ } | {
17
+ readonly seq: number;
18
+ readonly at: string;
19
+ readonly type: 'unit-created';
20
+ readonly snapshot: UnitSnapshot;
21
+ } | {
22
+ readonly seq: number;
23
+ readonly at: string;
24
+ readonly type: 'unit-transition';
25
+ readonly unitId: string;
26
+ readonly expectedVersion: number;
27
+ readonly event: UnitEvent;
28
+ };
29
+ export interface LedgerState {
30
+ readonly seq: number;
31
+ readonly pairs: ReadonlyMap<string, PairSnapshot>;
32
+ readonly units: ReadonlyMap<string, UnitSnapshot>;
33
+ }
34
+ export type FoldResult = {
35
+ readonly ok: true;
36
+ readonly state: LedgerState;
37
+ } | {
38
+ readonly ok: false;
39
+ readonly seq: number;
40
+ readonly message: string;
41
+ };
42
+ export declare function emptyLedger(): LedgerState;
43
+ export declare function fold(entries: readonly LedgerEntry[]): FoldResult;
44
+ /** Deterministic projection bytes for replay-equality tests. */
45
+ export declare function ledgerProjectionJson(state: LedgerState): string;
46
+ //# sourceMappingURL=ledger.d.ts.map
@@ -0,0 +1,62 @@
1
+ import { transition as transitionPair } from "./machine.js";
2
+ import { checkPair } from "./invariants.js";
3
+ import { checkUnit, transitionUnit } from "./unit.js";
4
+ export function emptyLedger() {
5
+ return { seq: 0, pairs: new Map(), units: new Map() };
6
+ }
7
+ export function fold(entries) {
8
+ let state = emptyLedger();
9
+ for (const entry of entries) {
10
+ if (!Number.isInteger(entry.seq) || entry.seq !== state.seq + 1)
11
+ return { ok: false, seq: entry.seq, message: 'expected seq ' + String(state.seq + 1) + ', got ' + String(entry.seq) };
12
+ if (entry.type === 'pair-created') {
13
+ if (state.pairs.has(entry.snapshot.id))
14
+ return { ok: false, seq: entry.seq, message: 'duplicate pair ' + entry.snapshot.id };
15
+ const problems = checkPair(entry.snapshot);
16
+ if (problems.length > 0)
17
+ return { ok: false, seq: entry.seq, message: 'pair invalid: ' + problems.join('; ') };
18
+ state = { ...state, seq: entry.seq, pairs: new Map(state.pairs).set(entry.snapshot.id, entry.snapshot) };
19
+ continue;
20
+ }
21
+ if (entry.type === 'pair-transition') {
22
+ const current = state.pairs.get(entry.pairId);
23
+ if (current === undefined)
24
+ return { ok: false, seq: entry.seq, message: 'pair not found: ' + entry.pairId };
25
+ const moved = transitionPair(current, entry.event, entry.expectedVersion);
26
+ if (!moved.ok)
27
+ return { ok: false, seq: entry.seq, message: moved.message };
28
+ const problems = checkPair(moved.snapshot);
29
+ if (problems.length > 0)
30
+ return { ok: false, seq: entry.seq, message: 'pair invalid after transition: ' + problems.join('; ') };
31
+ state = { ...state, seq: entry.seq, pairs: new Map(state.pairs).set(entry.pairId, moved.snapshot) };
32
+ continue;
33
+ }
34
+ if (entry.type === 'unit-created') {
35
+ if (state.units.has(entry.snapshot.unitId))
36
+ return { ok: false, seq: entry.seq, message: 'duplicate unit ' + entry.snapshot.unitId };
37
+ const problems = checkUnit(entry.snapshot);
38
+ if (problems.length > 0)
39
+ return { ok: false, seq: entry.seq, message: 'unit invalid: ' + problems.join('; ') };
40
+ state = { ...state, seq: entry.seq, units: new Map(state.units).set(entry.snapshot.unitId, entry.snapshot) };
41
+ continue;
42
+ }
43
+ const current = state.units.get(entry.unitId);
44
+ if (current === undefined)
45
+ return { ok: false, seq: entry.seq, message: 'unit not found: ' + entry.unitId };
46
+ const moved = transitionUnit(current, entry.event, entry.expectedVersion);
47
+ if (!moved.ok)
48
+ return { ok: false, seq: entry.seq, message: moved.message };
49
+ const problems = checkUnit(moved.snapshot);
50
+ if (problems.length > 0)
51
+ return { ok: false, seq: entry.seq, message: 'unit invalid after transition: ' + problems.join('; ') };
52
+ state = { ...state, seq: entry.seq, units: new Map(state.units).set(entry.unitId, moved.snapshot) };
53
+ }
54
+ return { ok: true, state };
55
+ }
56
+ /** Deterministic projection bytes for replay-equality tests. */
57
+ export function ledgerProjectionJson(state) {
58
+ const pairs = [...state.pairs.values()].sort((left, right) => left.id.localeCompare(right.id));
59
+ const units = [...state.units.values()].sort((left, right) => left.unitId.localeCompare(right.unitId));
60
+ return JSON.stringify({ seq: state.seq, pairs, units });
61
+ }
62
+ //# sourceMappingURL=ledger.js.map
@@ -0,0 +1,13 @@
1
+ /** Pure pair state machine. Every legal transition is explicit; no IO. */
2
+ import type { PairEvent, PairSnapshot } from './model.ts';
3
+ export type TransitionErrorCode = 'illegal-transition' | 'version-conflict' | 'invalid-event';
4
+ export type TransitionResult = {
5
+ readonly ok: true;
6
+ readonly snapshot: PairSnapshot;
7
+ } | {
8
+ readonly ok: false;
9
+ readonly code: TransitionErrorCode;
10
+ readonly message: string;
11
+ };
12
+ export declare function transition(snapshot: PairSnapshot, event: PairEvent, expectedVersion: number): TransitionResult;
13
+ //# sourceMappingURL=machine.d.ts.map
@@ -0,0 +1,108 @@
1
+ function fail(code, message) {
2
+ return { ok: false, code, message };
3
+ }
4
+ function clean(snapshot) {
5
+ const { failure, closedReason, ...rest } = snapshot;
6
+ void failure;
7
+ void closedReason;
8
+ return rest;
9
+ }
10
+ function next(snapshot, state, at, patch = {}) {
11
+ return { ...clean(snapshot), state, version: snapshot.version + 1, updatedAt: at, ...patch };
12
+ }
13
+ function requireText(value, field) {
14
+ return value === undefined || value.length === 0 ? fail('invalid-event', field + ' is required') : null;
15
+ }
16
+ export function transition(snapshot, event, expectedVersion) {
17
+ if (snapshot.version !== expectedVersion)
18
+ return fail('version-conflict', 'expected ' + String(expectedVersion) + ', got ' + String(snapshot.version));
19
+ const at = event.at;
20
+ switch (event.type) {
21
+ case 'screen':
22
+ return snapshot.state === 'candidate' ? { ok: true, snapshot: next(snapshot, 'deciding', at) } : fail('illegal-transition', 'screen from ' + snapshot.state);
23
+ case 'decide': {
24
+ if (snapshot.state !== 'deciding')
25
+ return fail('illegal-transition', 'decide from ' + snapshot.state);
26
+ if (event.decision === 'keep')
27
+ return { ok: true, snapshot: next(snapshot, 'closed', at, { closedReason: 'keep', origin: 'agent' }) };
28
+ const missing = requireText(event.target, 'target');
29
+ if (missing !== null)
30
+ return missing;
31
+ const decision = { target: event.target, sources: [snapshot.a, snapshot.b] };
32
+ const patch = { decision, target: event.target, approval: event.approval, ...(event.runId === undefined ? {} : { runId: event.runId }) };
33
+ return { ok: true, snapshot: next(snapshot, event.approval === 'on' ? 'pending' : 'approved', at, patch) };
34
+ }
35
+ case 'approve':
36
+ return snapshot.state === 'pending' ? { ok: true, snapshot: next(snapshot, 'approved', at) } : fail('illegal-transition', 'approve from ' + snapshot.state);
37
+ case 'reject':
38
+ return snapshot.state === 'pending' ? { ok: true, snapshot: next(snapshot, 'closed', at, { closedReason: 'reject', origin: 'human' }) } : fail('illegal-transition', 'reject from ' + snapshot.state);
39
+ case 'author-start':
40
+ return snapshot.state === 'approved' ? { ok: true, snapshot: next(snapshot, 'authoring', at) } : fail('illegal-transition', 'author-start from ' + snapshot.state);
41
+ case 'staged': {
42
+ if (snapshot.state !== 'authoring')
43
+ return fail('illegal-transition', 'staged from ' + snapshot.state);
44
+ const missing = requireText(event.stageRef, 'stageRef');
45
+ return missing ?? { ok: true, snapshot: next(snapshot, 'staged', at, { stageRef: event.stageRef }) };
46
+ }
47
+ case 'commit-start': {
48
+ if (snapshot.state !== 'staged')
49
+ return fail('illegal-transition', 'commit-start from ' + snapshot.state);
50
+ const missing = requireText(event.unitId, 'unitId');
51
+ return missing ?? { ok: true, snapshot: next(snapshot, 'committing', at, { unitId: event.unitId }) };
52
+ }
53
+ case 'commit-succeeded':
54
+ if (snapshot.state !== 'committing')
55
+ return fail('illegal-transition', 'commit-succeeded from ' + snapshot.state);
56
+ if (event.receipt.unitId.length === 0)
57
+ return fail('invalid-event', 'receipt.unitId is required');
58
+ return { ok: true, snapshot: next(snapshot, 'committed', at, { unitId: event.receipt.unitId, receipt: event.receipt }) };
59
+ case 'commit-failed':
60
+ return snapshot.state === 'committing' ? { ok: true, snapshot: next(snapshot, 'failed', at, { failure: event.failure }) } : fail('illegal-transition', 'commit-failed from ' + snapshot.state);
61
+ case 'commit-unknown':
62
+ return snapshot.state === 'committing' ? { ok: true, snapshot: next(snapshot, 'unknown', at) } : fail('illegal-transition', 'commit-unknown from ' + snapshot.state);
63
+ case 'recover': {
64
+ if (snapshot.state !== 'unknown')
65
+ return fail('illegal-transition', 'recover from ' + snapshot.state);
66
+ if (event.outcome === 'committed') {
67
+ if (event.receipt === undefined || event.receipt.unitId.length === 0)
68
+ return fail('invalid-event', 'receipt is required for recovered commit');
69
+ return { ok: true, snapshot: next(snapshot, 'committed', at, { unitId: event.receipt.unitId, receipt: event.receipt }) };
70
+ }
71
+ if (event.outcome === 'failed') {
72
+ if (event.failure === undefined)
73
+ return fail('invalid-event', 'failure is required for recovered failure');
74
+ return { ok: true, snapshot: next(snapshot, 'failed', at, { failure: event.failure }) };
75
+ }
76
+ return { ok: true, snapshot: next(snapshot, 'cancelled', at) };
77
+ }
78
+ case 'close':
79
+ if (event.reason !== 'over-limit')
80
+ return fail('invalid-event', 'close only accepts over-limit; keep/reject belong to decide/reject');
81
+ return snapshot.state === 'authoring' || snapshot.state === 'staged'
82
+ ? { ok: true, snapshot: next(snapshot, 'closed', at, { closedReason: event.reason }) }
83
+ : fail('illegal-transition', 'close from ' + snapshot.state);
84
+ case 'expire':
85
+ return snapshot.state === 'pending' || snapshot.state === 'staged'
86
+ ? { ok: true, snapshot: next(snapshot, 'expired', at) }
87
+ : fail('illegal-transition', 'expire from ' + snapshot.state);
88
+ case 'cancel':
89
+ return ['candidate', 'deciding', 'pending', 'approved', 'authoring', 'staged', 'unknown'].includes(snapshot.state)
90
+ ? { ok: true, snapshot: next(snapshot, 'cancelled', at) }
91
+ : fail('illegal-transition', 'cancel from ' + snapshot.state);
92
+ case 'fail':
93
+ return ['candidate', 'deciding', 'pending', 'approved', 'authoring', 'staged'].includes(snapshot.state)
94
+ ? { ok: true, snapshot: next(snapshot, 'failed', at, { failure: event.failure }) }
95
+ : fail('illegal-transition', 'fail from ' + snapshot.state);
96
+ case 'retry':
97
+ return snapshot.state === 'failed'
98
+ ? { ok: true, snapshot: next(snapshot, 'candidate', at, { attempts: snapshot.attempts + 1 }) }
99
+ : fail('illegal-transition', 'retry from ' + snapshot.state);
100
+ case 'undo':
101
+ return snapshot.state === 'committed' ? { ok: true, snapshot: next(snapshot, 'undone', at) } : fail('illegal-transition', 'undo from ' + snapshot.state);
102
+ case 'supersede':
103
+ return snapshot.state === 'undone' ? fail('illegal-transition', 'supersede from undone') : { ok: true, snapshot: next(snapshot, 'superseded', at) };
104
+ default:
105
+ return fail('invalid-event', 'unknown event');
106
+ }
107
+ }
108
+ //# sourceMappingURL=machine.js.map
@@ -0,0 +1,154 @@
1
+ /** Pure merge-kernel model. No IO, no host, no clock. */
2
+ export type PairState = 'candidate' | 'deciding' | 'pending' | 'approved' | 'authoring' | 'staged' | 'committing' | 'committed' | 'closed' | 'expired' | 'failed' | 'cancelled' | 'undone' | 'superseded' | 'unknown';
3
+ export type ClosedReason = 'keep' | 'reject' | 'over-limit';
4
+ export type ApprovalState = 'on' | 'off';
5
+ export type FailureStage = 'command' | 'screen' | 'decide' | 'author' | 'validate' | 'commit' | 'run' | 'surface' | 'schema';
6
+ export type FailureCode = 'bad-pair' | 'bad-target' | 'bad-flag' | 'source-gone' | 'source-changed' | 'target-taken' | 'validation-failed' | 'commit-failed' | 'schema-invalid' | 'timeout' | 'capability-missing' | 'runtime';
7
+ export interface Failure {
8
+ readonly code: FailureCode;
9
+ readonly stage: FailureStage;
10
+ readonly retryable: boolean;
11
+ readonly reportOnly: boolean;
12
+ readonly userMessageKey: string;
13
+ readonly detail?: string;
14
+ }
15
+ export interface MergeDecision {
16
+ readonly target: string;
17
+ readonly sources: readonly string[];
18
+ readonly rationale?: string;
19
+ }
20
+ export interface CommitReceipt {
21
+ readonly unitId: string;
22
+ readonly evidenceSha256: string;
23
+ readonly targetRevision?: string;
24
+ readonly sourceRevisions?: Readonly<Record<string, string>>;
25
+ }
26
+ export type RunState = 'queued' | 'screening' | 'deciding' | 'awaiting-approval' | 'authoring' | 'committing' | 'settled' | 'failed' | 'cancelled' | 'recovering';
27
+ export interface SkillSnapshot {
28
+ readonly name: string;
29
+ readonly revision: string;
30
+ readonly protection?: 'managed' | 'bundled' | 'hub' | 'pinned' | 'protected' | 'unknown';
31
+ }
32
+ export interface Candidate {
33
+ readonly pairKey: string;
34
+ readonly a: string;
35
+ readonly b: string;
36
+ readonly evidenceSeq: readonly number[];
37
+ readonly score?: number;
38
+ }
39
+ export interface RunSnapshot {
40
+ readonly id: string;
41
+ readonly state: RunState;
42
+ readonly version: number;
43
+ readonly pairs: readonly string[];
44
+ readonly startedAt: string;
45
+ readonly updatedAt: string;
46
+ }
47
+ export interface UnitEffect {
48
+ readonly kind: 'skill' | 'usage' | 'staging' | 'report';
49
+ readonly ref: string;
50
+ }
51
+ export interface UnitPlan {
52
+ readonly unitId: string;
53
+ readonly pairKey: string;
54
+ readonly target: string;
55
+ readonly sources: readonly string[];
56
+ readonly effects: readonly UnitEffect[];
57
+ }
58
+ export interface PairSnapshot {
59
+ readonly id: string;
60
+ readonly pairKey: string;
61
+ readonly a: string;
62
+ readonly b: string;
63
+ readonly state: PairState;
64
+ readonly version: number;
65
+ readonly createdAt: string;
66
+ readonly updatedAt: string;
67
+ readonly origin?: 'agent' | 'human' | 'system';
68
+ readonly runId?: string;
69
+ readonly target?: string;
70
+ readonly decision?: MergeDecision;
71
+ readonly approval?: ApprovalState;
72
+ readonly failure?: Failure;
73
+ readonly closedReason?: ClosedReason;
74
+ readonly stageRef?: string;
75
+ readonly unitId?: string;
76
+ readonly expiresAt?: number;
77
+ readonly receipt?: CommitReceipt;
78
+ readonly attempts: number;
79
+ }
80
+ export type PairEvent = {
81
+ readonly type: 'screen';
82
+ readonly at: string;
83
+ } | {
84
+ readonly type: 'decide';
85
+ readonly at: string;
86
+ readonly decision: 'merge' | 'keep';
87
+ readonly approval: ApprovalState;
88
+ readonly target?: string;
89
+ readonly runId?: string;
90
+ } | {
91
+ readonly type: 'approve';
92
+ readonly at: string;
93
+ } | {
94
+ readonly type: 'reject';
95
+ readonly at: string;
96
+ } | {
97
+ readonly type: 'author-start';
98
+ readonly at: string;
99
+ } | {
100
+ readonly type: 'staged';
101
+ readonly at: string;
102
+ readonly stageRef: string;
103
+ } | {
104
+ readonly type: 'commit-start';
105
+ readonly at: string;
106
+ readonly unitId: string;
107
+ } | {
108
+ readonly type: 'commit-succeeded';
109
+ readonly at: string;
110
+ readonly receipt: CommitReceipt;
111
+ } | {
112
+ readonly type: 'commit-failed';
113
+ readonly at: string;
114
+ readonly failure: Failure;
115
+ } | {
116
+ readonly type: 'commit-unknown';
117
+ readonly at: string;
118
+ readonly reason: string;
119
+ } | {
120
+ readonly type: 'recover';
121
+ readonly at: string;
122
+ readonly outcome: 'committed' | 'failed' | 'cancelled';
123
+ readonly receipt?: CommitReceipt;
124
+ readonly failure?: Failure;
125
+ } | {
126
+ readonly type: 'close';
127
+ readonly at: string;
128
+ readonly reason: ClosedReason;
129
+ } | {
130
+ readonly type: 'expire';
131
+ readonly at: string;
132
+ readonly reason: 'pending-ttl' | 'staging-ttl';
133
+ } | {
134
+ readonly type: 'cancel';
135
+ readonly at: string;
136
+ } | {
137
+ readonly type: 'fail';
138
+ readonly at: string;
139
+ readonly failure: Failure;
140
+ } | {
141
+ readonly type: 'retry';
142
+ readonly at: string;
143
+ } | {
144
+ readonly type: 'undo';
145
+ readonly at: string;
146
+ } | {
147
+ readonly type: 'supersede';
148
+ readonly at: string;
149
+ };
150
+ export declare const TERMINAL_STATES: readonly PairState[];
151
+ export declare const ACTIVE_STATES: readonly PairState[];
152
+ export declare const ALL_STATES: readonly PairState[];
153
+ export declare const CLOSED_REASONS: readonly ClosedReason[];
154
+ //# sourceMappingURL=model.d.ts.map