@lmzhen/dsh-evolution-merge 0.30.17 → 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 +2321 -854
  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 +114 -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 +70 -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
@@ -8,7 +8,10 @@ export function skillLibraryPort(skills) {
8
8
  const names = await archivedNames.call(skills);
9
9
  return names === null ? true : names.includes(name);
10
10
  },
11
- commitMerge: plan => skills.commitMerge(plan),
11
+ isNameOccupied: async (name) => {
12
+ const probe = skills.skillDirExists;
13
+ return typeof probe === 'function' ? await probe.call(skills, name) : false;
14
+ },
12
15
  async readSupportFiles(name) {
13
16
  if (typeof skills.listSupportFiles !== 'function')
14
17
  return [];
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Structural ModelPort adapter over the platform llm service. The platform exposes either
3
+ * `stream()` (the current shape) or the older prepareCall()/stream pair; both return raw text
4
+ * because the gate and proposer own JSON extraction.
5
+ */
6
+ import type { ModelPort } from '../propose.ts';
7
+ export interface ModelRoute {
8
+ readonly provider: string;
9
+ readonly model: string;
10
+ }
11
+ export interface LlmChunkLike {
12
+ readonly type: string;
13
+ readonly text?: string;
14
+ readonly usage?: unknown;
15
+ readonly reason?: {
16
+ readonly kind?: string;
17
+ readonly failure?: {
18
+ readonly message?: string;
19
+ };
20
+ };
21
+ }
22
+ export interface LlmCallLike {
23
+ readonly config: Record<string, unknown>;
24
+ stream(input: Record<string, unknown>): AsyncIterable<LlmChunkLike>;
25
+ }
26
+ export interface LlmServiceLike {
27
+ prepareCall?(config: Record<string, unknown>, signal?: AbortSignal): Promise<LlmCallLike>;
28
+ stream?(options: Record<string, unknown>): AsyncIterable<LlmChunkLike>;
29
+ }
30
+ export declare function createModelPort(llm: LlmServiceLike, route: () => ModelRoute): ModelPort;
31
+ //# sourceMappingURL=model.d.ts.map
@@ -0,0 +1,46 @@
1
+ function append(chunk, text) {
2
+ if (chunk.type === 'text-delta' || chunk.type === 'text')
3
+ return text + (chunk.text ?? '');
4
+ if (chunk.type === 'finish' && chunk.reason?.kind === 'error') {
5
+ throw new Error('model call failed: ' + (chunk.reason.failure?.message ?? 'unknown'));
6
+ }
7
+ return text;
8
+ }
9
+ export function createModelPort(llm, route) {
10
+ return {
11
+ async callJson(request) {
12
+ const selection = route();
13
+ let text = '';
14
+ if (llm.stream !== undefined) {
15
+ for await (const chunk of llm.stream({
16
+ provider: selection.provider,
17
+ model: selection.model,
18
+ system: request.system,
19
+ messages: [{ role: 'user', content: [{ type: 'text', text: request.user }] }],
20
+ maxTokens: request.maxTokens,
21
+ temperature: 0,
22
+ signal: request.signal,
23
+ }))
24
+ text = append(chunk, text);
25
+ return text;
26
+ }
27
+ if (llm.prepareCall === undefined)
28
+ throw new Error('llm service has neither stream nor prepareCall');
29
+ const call = await llm.prepareCall({
30
+ provider: selection.provider,
31
+ model: selection.model,
32
+ maxTokens: request.maxTokens,
33
+ temperature: 0,
34
+ }, request.signal);
35
+ for await (const chunk of call.stream({
36
+ ...call.config,
37
+ system: request.system,
38
+ messages: [{ role: 'user', content: [{ type: 'text', text: request.user }] }],
39
+ signal: request.signal,
40
+ }))
41
+ text = append(chunk, text);
42
+ return text;
43
+ },
44
+ };
45
+ }
46
+ //# sourceMappingURL=model.js.map
@@ -16,6 +16,8 @@ export interface RunApproveCommandOptions {
16
16
  readonly ports: ApprovePorts;
17
17
  readonly rowId: string;
18
18
  readonly platformWrite: boolean;
19
+ readonly detached?: boolean;
20
+ readonly pairTimeoutMinutes?: number;
19
21
  readonly now: () => number;
20
22
  }
21
23
  export declare function runApproveCommand(options: RunApproveCommandOptions): Promise<WithMergeRunResult>;
@@ -1,55 +1,50 @@
1
1
  import { approvePending } from "./pending-actions.js";
2
2
  import { loadMergeState } from "./state.js";
3
- import { acquireTargetClaim } from "./target-claim.js";
4
3
  import { withMergeRun } from "./run-shell.js";
5
4
  import { recordRound2Outcome } from "./round2-outcome.js";
6
5
  export async function runApproveCommand(options) {
7
6
  const loaded = await loadMergeState(options.io, options.home);
8
7
  const before = loaded.state.rows.find(item => item.id === options.rowId);
9
- const target = before?.decision?.target;
10
- const claim = target === undefined ? null : await acquireTargetClaim(options.io, options.home, target, options.now());
11
- if (target !== undefined && claim === null)
12
- return { ok: false, message: 'target already claimed by another approve: ' + target };
13
- try {
14
- const taken = await approvePending(options.io, options.home, options.now, options.rowId);
15
- if (!taken.ok)
16
- return { ok: false, message: taken.message };
17
- const row = taken.row;
18
- const decision = row.decision;
19
- if (decision === undefined)
20
- return { ok: false, message: 'pending row has no decision' };
21
- return await withMergeRun({
22
- io: options.io,
23
- home: options.home,
24
- registry: options.registry,
25
- command: 'approve',
26
- approval: { platformWrite: options.platformWrite, mergeApproval: true },
27
- now: options.now,
28
- async body(ctx) {
29
- const stage = (name) => ctx.stage(name, { done: 0, total: 1 });
30
- await stage('author');
31
- let outcome;
32
- try {
33
- outcome = await options.ports.round2(row, decision, ctx.signal, ctx.runId);
34
- }
35
- catch (error) {
36
- outcome = { kind: 'failed', failure: { stage: 'author', code: 'merge-failed', message: error instanceof Error ? error.message : String(error), retryable: true, retryFrom: 'agent2' } };
37
- }
38
- const base = { scope: { candidatePairs: 1, queueBefore: { verify: 0, pending: 0, fuse: 1, skipped: 0 } }, notes: [] };
39
- const record = await recordRound2Outcome(options.io, options.home, options.now, row, outcome);
40
- return {
41
- ...base,
42
- stopReason: record.merged ? 'completed' : 'failed',
43
- pairs: [record.pair],
44
- totals: record.merged
45
- ? { merged: 1, kept: 0, pending: 0, failed: 0 }
46
- : { merged: 0, kept: 0, pending: 0, failed: 1 },
47
- };
48
- },
49
- });
50
- }
51
- finally {
52
- await claim?.release();
53
- }
8
+ if (before === undefined)
9
+ return { ok: false, message: 'pending row not found: ' + options.rowId };
10
+ if (before.state !== 'pending')
11
+ return { ok: false, message: 'row is not pending: ' + before.state };
12
+ const taken = await approvePending(options.io, options.home, options.now, options.rowId);
13
+ if (!taken.ok)
14
+ return { ok: false, message: taken.message };
15
+ const row = taken.row;
16
+ const decision = row.decision;
17
+ if (decision === undefined)
18
+ return { ok: false, message: 'pending row has no decision' };
19
+ return await withMergeRun({
20
+ io: options.io,
21
+ home: options.home,
22
+ registry: options.registry,
23
+ command: 'approve',
24
+ approval: { platformWrite: options.platformWrite, mergeApproval: true },
25
+ now: options.now,
26
+ ...(options.detached === true ? { detached: true } : {}),
27
+ async body(ctx) {
28
+ const stage = (name) => ctx.stage(name, { done: 0, total: 1 });
29
+ await stage('author');
30
+ let outcome;
31
+ try {
32
+ outcome = await options.ports.round2(row, decision, ctx.signal, ctx.runId);
33
+ }
34
+ catch (error) {
35
+ outcome = { kind: 'failed', failure: { stage: 'author', code: 'merge-failed', message: error instanceof Error ? error.message : String(error), retryable: true, retryFrom: 'agent2' } };
36
+ }
37
+ const base = { scope: { candidatePairs: 1, queueBefore: { verify: 0, pending: 0, fuse: 1, skipped: 0 } }, notes: [] };
38
+ const record = await recordRound2Outcome(options.io, options.home, options.now, row, outcome);
39
+ return {
40
+ ...base,
41
+ stopReason: record.merged ? 'completed' : 'failed',
42
+ pairs: [record.pair],
43
+ totals: record.merged
44
+ ? { merged: 1, kept: 0, pending: 0, failed: 0 }
45
+ : { merged: 0, kept: 0, pending: 0, failed: 1 },
46
+ };
47
+ },
48
+ });
54
49
  }
55
50
  //# sourceMappingURL=approve-command.js.map
@@ -3,7 +3,7 @@
3
3
  * rule gate, then commit through the injected CommitPort. The channel adapter and the
4
4
  * author tool live outside; this is the seam they share.
5
5
  */
6
- import type { EvolutionIoLike } from '@lmzhen/dsh-evolution-core';
6
+ import type { EvolutionIoLike, UnitTransaction } from '@lmzhen/dsh-evolution-core';
7
7
  import { type RuleFinding } from './rules.ts';
8
8
  import { type CommitPort, type CommitStagedResult } from './commit-execute.ts';
9
9
  import type { StagedTree } from './rules.ts';
@@ -24,6 +24,8 @@ export interface RunAuthoringOptions {
24
24
  readonly commitPort: CommitPort;
25
25
  readonly now: () => number;
26
26
  readonly summary: string;
27
+ readonly isBlocked?: (name: string) => boolean | Promise<boolean>;
28
+ readonly unit?: UnitTransaction | undefined;
27
29
  }
28
30
  export type AuthoringResult = {
29
31
  readonly ok: true;
@@ -4,18 +4,26 @@ import { validateStagedTree, hasBlockingFinding } from "./rules.js";
4
4
  import { commitStagedRun } from "./commit-execute.js";
5
5
  export async function runAuthoring(options) {
6
6
  const targetCard = options.cards.find(card => card.name === options.target);
7
- if (targetCard?.protectionUnknown === true || targetCard?.protectedBy === 'pinned')
8
- return { ok: false, message: 'target is pinned: ' + options.target };
7
+ const mode = options.mode ?? (targetCard === undefined ? 'create-umbrella' : 'merge-into-existing');
8
+ const createMode = mode === 'create-umbrella';
9
+ if (!createMode && targetCard !== undefined) {
10
+ if (targetCard.managed !== true)
11
+ return { ok: false, message: 'target is not family-managed: ' + options.target };
12
+ if (targetCard.protectionUnknown === true || targetCard.protectedBy != null)
13
+ return { ok: false, message: 'target is protected (' + (targetCard.protectedBy ?? 'unknown') + '): ' + options.target };
14
+ }
15
+ if (!createMode && await options.isBlocked?.(options.target) === true)
16
+ return { ok: false, message: 'target is blocked by curator: ' + options.target };
9
17
  const stageKey = options.stageKey ?? options.runId;
10
18
  if (options.reuseStaging === true) {
11
- if (!await setStagingTarget(options.io, options.home, stageKey, options.target, options.mode))
19
+ if (!await setStagingTarget(options.io, options.home, stageKey, options.target, mode))
12
20
  return { ok: false, message: 'staging missing or unreadable: ' + stageKey };
13
21
  }
14
22
  else {
15
23
  const created = await createStaging(options.io, options.home, {
16
24
  runId: stageKey,
17
25
  target: options.target,
18
- ...(options.mode === undefined ? {} : { mode: options.mode }),
26
+ mode,
19
27
  sources: options.cards.map(card => ({ name: card.name, body: card.body, files: card.files ?? [] })),
20
28
  promptVersion: 'merge-b/v1',
21
29
  ttlDays: 7,
@@ -37,9 +45,11 @@ export async function runAuthoring(options) {
37
45
  if (hasBlockingFinding(findings))
38
46
  return { ok: false, message: 'staged tree failed the rule gate', findings };
39
47
  const commit = await commitStagedRun(options.io, options.home, stageKey, options.commitPort, {
40
- origin: 'subagent',
48
+ origin: 'foreground',
41
49
  summary: options.summary,
42
50
  now: options.now,
51
+ ...(options.isBlocked === undefined ? {} : { isBlocked: options.isBlocked }),
52
+ ...(options.unit === undefined ? {} : { unit: options.unit }),
43
53
  });
44
54
  if (!commit.ok)
45
55
  return { ok: false, message: commit.message };
@@ -0,0 +1,5 @@
1
+ /** Frozen merge CommandSurface v1: 19 commands, JSON-safe, no handlers. */
2
+ import type { CommandSpec, CommandSurface } from './wire.ts';
3
+ export declare const MERGE_COMMANDS: readonly CommandSpec[];
4
+ export declare const MERGE_COMMAND_SURFACE: CommandSurface;
5
+ //# sourceMappingURL=command-surface.d.ts.map
@@ -0,0 +1,44 @@
1
+ const flag = (name, kind = 'boolean', valueName) => ({ name, kind, ...(valueName === undefined ? {} : { valueName }) });
2
+ const pos = (name, kind, required = true) => ({ name, kind, required });
3
+ const action = (id, command, confirm = false) => ({ id, command, confirm });
4
+ function spec(id, words, flags, positional, readOnly, needsRun, rowActions, fallback) {
5
+ return { id, words, flags, positional, summary: id, readOnly, needsRun, defaults: {}, rowActions, fallback };
6
+ }
7
+ export const MERGE_COMMANDS = [
8
+ spec('merge.scan', ['scan'], [], [], false, true, [], '/evolution merge scan'),
9
+ spec('merge.pair', ['pair'], [], [pos('a', 'skill'), pos('b', 'skill')], false, true, [action('pair', '/evolution merge pair', true)], '/evolution merge pair <a> <b>'),
10
+ spec('merge.pair.force', ['pair'], [flag('force')], [pos('a', 'skill'), pos('b', 'skill')], false, true, [], '/evolution merge pair --force <a> <b>'),
11
+ spec('merge.status', ['status'], [], [pos('id', 'id', false)], true, false, [action('status', '/evolution merge status')], '/evolution merge status'),
12
+ spec('merge.report', ['report'], [], [pos('id', 'id', false)], true, false, [action('report', '/evolution merge report')], '/evolution merge report'),
13
+ spec('merge.cancel', ['cancel'], [], [pos('id', 'id', false)], false, false, [action('cancel', '/evolution merge cancel')], '/evolution merge cancel <id>'),
14
+ spec('merge.runs', ['runs'], [], [], true, false, [], '/evolution merge runs'),
15
+ spec('merge.pending', ['pending'], [], [], true, false, [], '/evolution merge pending'),
16
+ spec('merge.pending.expired', ['pending'], [flag('expired')], [], true, false, [], '/evolution merge pending --expired'),
17
+ spec('merge.approve', ['approve'], [], [pos('id', 'id')], false, true, [action('approve', '/evolution merge approve <id>')], '/evolution merge approve <id>'),
18
+ spec('merge.reject', ['reject'], [], [pos('id', 'id')], false, false, [action('reject', '/evolution merge reject <id>', true)], '/evolution merge reject <id>'),
19
+ spec('merge.skipped', ['skipped'], [], [], true, false, [], '/evolution merge skipped'),
20
+ spec('merge.skipped.restore', ['skipped'], [flag('restore', 'value', 'id')], [], false, false, [action('restore', '/evolution merge skipped --restore <id>', true)], '/evolution merge skipped --restore <id>'),
21
+ spec('merge.candidates', ['candidates'], [], [], true, false, [], '/evolution merge candidates'),
22
+ spec('merge.candidates.retry', ['candidates'], [flag('retry', 'value', 'id')], [], false, true, [action('retry-candidates', '/evolution merge candidates --retry <id>')], '/evolution merge candidates --retry <id>'),
23
+ spec('merge.resume', ['resume'], [], [pos('id', 'id', false)], true, false, [], '/evolution merge resume'),
24
+ spec('merge.resume.retry', ['resume'], [flag('retry', 'value', 'id')], [], false, true, [action('retry-resume', '/evolution merge resume --retry <id>')], '/evolution merge resume --retry <id>'),
25
+ spec('merge.discard', ['discard'], [], [pos('id', 'id')], false, false, [action('discard', '/evolution merge discard <id>', true)], '/evolution merge discard <id>'),
26
+ spec('merge.undo', ['undo'], [flag('run', 'value', 'id')], [pos('target', 'target')], false, false, [], '/evolution merge undo <target> --run <id>'),
27
+ ];
28
+ export const MERGE_COMMAND_SURFACE = {
29
+ wireVersion: 1,
30
+ prefix: '/evolution merge',
31
+ commands: MERGE_COMMANDS,
32
+ fallback: [
33
+ { id: 'scan', command: '/evolution merge scan' },
34
+ { id: 'pair', command: '/evolution merge pair <a> <b>' },
35
+ { id: 'candidates', command: '/evolution merge candidates' },
36
+ { id: 'pending', command: '/evolution merge pending' },
37
+ { id: 'skipped', command: '/evolution merge skipped' },
38
+ { id: 'resume', command: '/evolution merge resume' },
39
+ { id: 'status', command: '/evolution merge status' },
40
+ { id: 'report', command: '/evolution merge report' },
41
+ { id: 'doctor', command: '/evolution merge doctor' },
42
+ ],
43
+ };
44
+ //# sourceMappingURL=command-surface.js.map
@@ -11,6 +11,8 @@ export interface CommitAssemblyPort {
11
11
  readSkill(name: string): Promise<string | null>;
12
12
  /** Whether the name is already occupied by an archived (tombstoned) skill. */
13
13
  isArchived?(name: string): Promise<boolean>;
14
+ /** Whether a directory exists for the name (including a SKILL.md-less ghost). */
15
+ isNameOccupied?(name: string): Promise<boolean>;
14
16
  }
15
17
  export interface CommitAssemblyOptions {
16
18
  readonly origin: WriteOrigin;
@@ -12,13 +12,18 @@ export async function assembleStagedCommit(io, home, runId, port, options) {
12
12
  const requested = staging.value.manifest.target;
13
13
  let target = requested;
14
14
  let targetCurrent = await port.readSkill(requested);
15
+ if (staging.value.manifest.mode !== 'create-umbrella' && targetCurrent === null) {
16
+ return { ok: false, message: 'target is gone; refusing to re-create it: ' + requested };
17
+ }
15
18
  const requestedArchived = targetCurrent === null && await port.isArchived?.(requested) === true;
16
- if (staging.value.manifest.mode === 'create-umbrella' && (targetCurrent !== null || requestedArchived)) {
19
+ const requestedOccupied = targetCurrent === null && await port.isNameOccupied?.(requested) === true;
20
+ if (staging.value.manifest.mode === 'create-umbrella' && (targetCurrent !== null || requestedArchived || requestedOccupied)) {
17
21
  target = requested;
18
22
  for (let suffix = 2; suffix <= 99; suffix += 1) {
19
23
  const candidate = requested + '-v' + String(suffix);
20
24
  if (await port.readSkill(candidate) === null
21
- && await port.isArchived?.(candidate) !== true) {
25
+ && await port.isArchived?.(candidate) !== true
26
+ && await port.isNameOccupied?.(candidate) !== true) {
22
27
  target = candidate;
23
28
  targetCurrent = null;
24
29
  break;
@@ -6,8 +6,11 @@
6
6
  import type { CommitMergePlan, CommitMergeResult, EvolutionIoLike } from '@lmzhen/dsh-evolution-core';
7
7
  import { type CommitAssemblyPort } from './commit-assembly.ts';
8
8
  import { type CommitReceipt } from './commit-receipt.ts';
9
+ import type { UnitTransaction } from '@lmzhen/dsh-evolution-core';
9
10
  export interface CommitPort extends CommitAssemblyPort {
10
- commitMerge(plan: CommitMergePlan): Promise<CommitMergeResult>;
11
+ /** Transitional G2: when set, commit-execute prefers this over commitMerge. */
12
+ unit?: UnitTransaction;
13
+ commitMerge?(plan: CommitMergePlan): Promise<CommitMergeResult>;
11
14
  readSupportFiles?(name: string): Promise<readonly {
12
15
  readonly path: string;
13
16
  readonly content: string;
@@ -17,6 +20,9 @@ export interface CommitStagedOptions {
17
20
  readonly origin: CommitMergePlan['origin'];
18
21
  readonly summary: string;
19
22
  readonly now: () => number;
23
+ readonly isBlocked?: (name: string) => boolean | Promise<boolean>;
24
+ /** G2 transitional: when present, commit through core UnitTransaction. */
25
+ readonly unit?: UnitTransaction;
20
26
  }
21
27
  export type CommitStagedResult = {
22
28
  readonly ok: true;
@@ -7,18 +7,48 @@ export async function commitStagedRun(io, home, runId, port, options) {
7
7
  const assembled = await assembleStagedCommit(io, home, runId, port, { origin: options.origin, summary: options.summary });
8
8
  if (!assembled.ok)
9
9
  return { ok: false, message: assembled.message };
10
- try {
11
- await writeCommitSnapshot(io, home, runId, assembled.plan, options.now(), port);
10
+ if (await options.isBlocked?.(assembled.plan.target) === true) {
11
+ return { ok: false, message: 'target is blocked by curator: ' + assembled.plan.target, plan: assembled.plan };
12
12
  }
13
- catch (error) {
14
- return { ok: false, message: 'commit snapshot failed; refusing to write: ' + (error instanceof Error ? error.message : String(error)), plan: assembled.plan };
13
+ const journal = { runId, target: assembled.plan.target, create: !assembled.plan.targetExists, sources: assembled.plan.sources, phase: 'prepared', updatedAt: new Date(options.now()).toISOString() };
14
+ let result;
15
+ const transaction = options.unit ?? port.unit;
16
+ if (transaction !== undefined) {
17
+ const first = assembled.plan.sources[0] ?? assembled.plan.target;
18
+ const unitPlan = { runId, pairKey: pairKey(assembled.plan.target, first), target: assembled.plan.target, targetExists: assembled.plan.targetExists, sources: assembled.plan.sources, body: assembled.plan.body, supportFiles: assembled.plan.supportFiles, expectedTargetBody: assembled.plan.expectedTargetBody, expectedSourceBodies: assembled.plan.expectedSourceBodies, origin: options.origin, summary: options.summary };
19
+ try {
20
+ await writeJournal(io, home, journal);
21
+ }
22
+ catch { /* compat mirror; unit journal is authority */ }
23
+ const prepared = await transaction.prepare(unitPlan);
24
+ if (!prepared.ok)
25
+ return { ok: false, message: prepared.message, plan: assembled.plan };
26
+ const committed = await transaction.commit(prepared.prepared);
27
+ if (!committed.ok) {
28
+ try {
29
+ await writeJournal(io, home, { ...journal, phase: 'failed', updatedAt: new Date(options.now()).toISOString() });
30
+ }
31
+ catch { }
32
+ ;
33
+ return { ok: false, message: committed.message, plan: assembled.plan };
34
+ }
35
+ result = { ok: true, message: 'merged', unitId: committed.evidence.unitId, archived: committed.evidence.sources.filter(source => source.archived).map(source => source.name), mutations: committed.evidence.mutations };
15
36
  }
16
- const journal = { runId, target: assembled.plan.target, create: !assembled.plan.targetExists, sources: assembled.plan.sources, phase: 'sources-archived', updatedAt: new Date(options.now()).toISOString() };
17
- try {
18
- await writeJournal(io, home, journal);
37
+ else {
38
+ try {
39
+ await writeCommitSnapshot(io, home, runId, assembled.plan, options.now(), port);
40
+ }
41
+ catch (error) {
42
+ return { ok: false, message: 'commit snapshot failed; refusing to write: ' + (error instanceof Error ? error.message : String(error)), plan: assembled.plan };
43
+ }
44
+ try {
45
+ await writeJournal(io, home, journal);
46
+ }
47
+ catch { /* journal is best-effort */ }
48
+ if (port.commitMerge === undefined)
49
+ return { ok: false, message: 'commit port missing: UnitTransaction or commitMerge required', plan: assembled.plan };
50
+ result = await port.commitMerge(assembled.plan);
19
51
  }
20
- catch { /* journal is best-effort; receipt remains the commit record */ }
21
- const result = await port.commitMerge(assembled.plan);
22
52
  if (!result.ok) {
23
53
  try {
24
54
  await writeJournal(io, home, { ...journal, phase: 'failed', updatedAt: new Date(options.now()).toISOString() });
@@ -26,7 +56,7 @@ export async function commitStagedRun(io, home, runId, port, options) {
26
56
  catch { }
27
57
  return { ok: false, message: result.message, plan: assembled.plan };
28
58
  }
29
- const committed = { ...journal, phase: 'committed', updatedAt: new Date(options.now()).toISOString() };
59
+ const committed = { ...journal, phase: 'committed', updatedAt: new Date(options.now()).toISOString(), ...(result.unitId === undefined ? {} : { unitId: result.unitId }) };
30
60
  try {
31
61
  await writeJournal(io, home, committed);
32
62
  }
@@ -2,9 +2,14 @@ import type { EvolutionIoLike } from '@lmzhen/dsh-evolution-core';
2
2
  import type { CommitJournal } from './domain/commit-journal.ts';
3
3
  export interface CommitRecoveryReport {
4
4
  readonly runId: string;
5
- readonly phase: CommitJournal['phase'];
5
+ readonly phase: CommitJournal['phase'] | 'unknown';
6
6
  readonly action: string;
7
7
  readonly ok: boolean;
8
+ readonly target?: string;
9
+ readonly sources?: readonly string[];
10
+ readonly journalPath?: string;
11
+ readonly snapshotPath?: string;
12
+ readonly unitId?: string;
8
13
  }
9
- export declare function recoverCommitJournals(io: EvolutionIoLike, home: string, now: number, skillRoot: string, restoreSource?: (name: string) => Promise<boolean>, isActive?: (runId: string) => boolean): Promise<readonly CommitRecoveryReport[]>;
14
+ export declare function recoverCommitJournals(io: EvolutionIoLike, home: string, now: number, isActive?: (runId: string) => boolean): Promise<readonly CommitRecoveryReport[]>;
10
15
  //# sourceMappingURL=commit-recovery.d.ts.map