@lmzhen/dsh-evolution-merge 0.30.18 → 0.30.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/README.md +21 -4
  2. package/data/minhash_params.json +1063 -0
  3. package/lib/index.js +7313 -3272
  4. package/lib/types/adapters/channel.d.ts +1 -0
  5. package/lib/types/adapters/channel.js +15 -1
  6. package/lib/types/adapters/engine-screen.d.ts +1 -1
  7. package/lib/types/adapters/engine-screen.js +5 -2
  8. package/lib/types/adapters/model-runner.d.ts +0 -5
  9. package/lib/types/adapters/model-runner.js +8 -2
  10. package/lib/types/adapters/model.js +2 -1
  11. package/lib/types/adapters/select.js +12 -8
  12. package/lib/types/approve-command.d.ts +12 -1
  13. package/lib/types/approve-command.js +39 -6
  14. package/lib/types/authoring-flow.d.ts +16 -0
  15. package/lib/types/authoring-flow.js +37 -13
  16. package/lib/types/channel-port.d.ts +3 -1
  17. package/lib/types/channel-port.js +8 -3
  18. package/lib/types/command-defs.d.ts +5 -0
  19. package/lib/types/command-defs.js +33 -0
  20. package/lib/types/command-surface.d.ts +1 -1
  21. package/lib/types/command-surface.js +10 -43
  22. package/lib/types/commands.d.ts +3 -4
  23. package/lib/types/commands.js +33 -72
  24. package/lib/types/commit-assembly.d.ts +14 -1
  25. package/lib/types/commit-assembly.js +65 -9
  26. package/lib/types/commit-execute.d.ts +7 -1
  27. package/lib/types/commit-execute.js +54 -22
  28. package/lib/types/commit-plan.d.ts +7 -1
  29. package/lib/types/commit-plan.js +4 -0
  30. package/lib/types/commit-receipt.d.ts +3 -0
  31. package/lib/types/contracts.d.ts +43 -0
  32. package/lib/types/doctor.d.ts +11 -1
  33. package/lib/types/doctor.js +81 -27
  34. package/lib/types/domain/authoring-manifest.d.ts +26 -0
  35. package/lib/types/domain/authoring-manifest.js +43 -0
  36. package/lib/types/domain/engine-criteria.d.ts +10 -0
  37. package/lib/types/domain/engine-criteria.js +11 -0
  38. package/lib/types/domain/engine-input.d.ts +10 -0
  39. package/lib/types/domain/engine-input.js +26 -0
  40. package/lib/types/domain/errors.d.ts +15 -1
  41. package/lib/types/domain/errors.js +23 -4
  42. package/lib/types/domain/evidence-hash.d.ts +3 -0
  43. package/lib/types/domain/evidence-hash.js +14 -0
  44. package/lib/types/domain/failure-route.d.ts +14 -0
  45. package/lib/types/domain/failure-route.js +12 -0
  46. package/lib/types/domain/live-facts.d.ts +31 -0
  47. package/lib/types/domain/live-facts.js +54 -0
  48. package/lib/types/domain/merge-limits.d.ts +18 -0
  49. package/lib/types/domain/merge-limits.js +22 -0
  50. package/lib/types/domain/merge-report.d.ts +41 -0
  51. package/lib/types/domain/merge-report.js +2 -0
  52. package/lib/types/domain/merge-snapshot.d.ts +99 -0
  53. package/lib/types/domain/merge-snapshot.js +82 -0
  54. package/lib/types/domain/pair-ledger.d.ts +34 -0
  55. package/lib/types/domain/pair-ledger.js +96 -0
  56. package/lib/types/domain/pre-1b-backup.d.ts +43 -0
  57. package/lib/types/domain/pre-1b-backup.js +140 -0
  58. package/lib/types/domain/provider-key.d.ts +2 -0
  59. package/lib/types/domain/provider-key.js +11 -0
  60. package/lib/types/domain/read-probe.d.ts +5 -0
  61. package/lib/types/domain/read-probe.js +11 -0
  62. package/lib/types/domain/reconcile.d.ts +47 -0
  63. package/lib/types/domain/reconcile.js +178 -0
  64. package/lib/types/domain/resolve-pair.d.ts +2 -9
  65. package/lib/types/domain/resolve-pair.js +2 -15
  66. package/lib/types/domain/row-v4.d.ts +148 -0
  67. package/lib/types/domain/row-v4.js +159 -0
  68. package/lib/types/domain/run-coordinator.d.ts +51 -0
  69. package/lib/types/domain/run-coordinator.js +45 -0
  70. package/lib/types/domain/schema4.d.ts +53 -0
  71. package/lib/types/domain/schema4.js +129 -0
  72. package/lib/types/domain/selection-policy.d.ts +14 -0
  73. package/lib/types/domain/selection-policy.js +6 -0
  74. package/lib/types/domain/skipped-expiry.d.ts +5 -0
  75. package/lib/types/domain/skipped-expiry.js +16 -0
  76. package/lib/types/domain/stage-aggregate.d.ts +96 -0
  77. package/lib/types/domain/stage-aggregate.js +293 -0
  78. package/lib/types/domain/staging-index.d.ts +1 -0
  79. package/lib/types/domain/state-bootstrap.d.ts +43 -0
  80. package/lib/types/domain/state-bootstrap.js +267 -0
  81. package/lib/types/domain/state-v3.d.ts +3 -14
  82. package/lib/types/domain/state-v3.js +3 -79
  83. package/lib/types/domain/undo-journal.d.ts +19 -0
  84. package/lib/types/domain/undo-journal.js +20 -0
  85. package/lib/types/domain/undo-receipt.d.ts +18 -0
  86. package/lib/types/domain/undo-receipt.js +22 -0
  87. package/lib/types/domain/validation-plan.d.ts +65 -0
  88. package/lib/types/domain/validation-plan.js +49 -0
  89. package/lib/types/facts.d.ts +3 -1
  90. package/lib/types/facts.js +13 -16
  91. package/lib/types/index.d.ts +2 -0
  92. package/lib/types/index.js +181 -68
  93. package/lib/types/merge-command.d.ts +3 -1
  94. package/lib/types/merge-command.js +10 -6
  95. package/lib/types/merge-routes.d.ts +10 -5
  96. package/lib/types/merge-routes.js +72 -7
  97. package/lib/types/merge-tool.d.ts +1 -1
  98. package/lib/types/merge-tool.js +2 -2
  99. package/lib/types/metrics.js +1 -1
  100. package/lib/types/new-child-authoring.d.ts +2 -0
  101. package/lib/types/new-child-authoring.js +57 -10
  102. package/lib/types/params.d.ts +2 -4
  103. package/lib/types/params.js +13 -0
  104. package/lib/types/pending-actions.d.ts +2 -2
  105. package/lib/types/pending-actions.js +4 -4
  106. package/lib/types/prompts.d.ts +2 -4
  107. package/lib/types/prompts.js +21 -8
  108. package/lib/types/read-commands.d.ts +3 -3
  109. package/lib/types/read-commands.js +19 -28
  110. package/lib/types/recovery-coordinator.d.ts +34 -0
  111. package/lib/types/recovery-coordinator.js +84 -0
  112. package/lib/types/recovery-projection.d.ts +31 -0
  113. package/lib/types/recovery-projection.js +96 -0
  114. package/lib/types/resume-command.d.ts +5 -1
  115. package/lib/types/resume-command.js +46 -13
  116. package/lib/types/revision.d.ts +17 -0
  117. package/lib/types/revision.js +39 -3
  118. package/lib/types/round2-outcome.d.ts +2 -1
  119. package/lib/types/round2-outcome.js +59 -11
  120. package/lib/types/run-shell.d.ts +16 -2
  121. package/lib/types/run-shell.js +49 -5
  122. package/lib/types/runtime.d.ts +0 -5
  123. package/lib/types/runtime.js +274 -89
  124. package/lib/types/scan-command.d.ts +1 -0
  125. package/lib/types/scan-command.js +2 -0
  126. package/lib/types/scan-ports.d.ts +18 -0
  127. package/lib/types/scan-ports.js +49 -23
  128. package/lib/types/scan-run.d.ts +17 -0
  129. package/lib/types/scan-run.js +143 -48
  130. package/lib/types/stage-tool-registry.d.ts +7 -0
  131. package/lib/types/stage-tool-registry.js +12 -1
  132. package/lib/types/state.d.ts +5 -5
  133. package/lib/types/state.js +22 -69
  134. package/lib/types/status.d.ts +6 -1
  135. package/lib/types/status.js +47 -0
  136. package/lib/types/surface/body.d.ts +4 -0
  137. package/lib/types/surface/body.js +16 -0
  138. package/lib/types/surface/merge-view.d.ts +14 -0
  139. package/lib/types/surface/merge-view.js +126 -0
  140. package/lib/types/undo-operation.d.ts +24 -0
  141. package/lib/types/undo-operation.js +135 -0
  142. package/lib/types/wire.d.ts +22 -3
  143. package/package.json +6 -4
@@ -1,79 +1,40 @@
1
- const USAGE = 'usage: /evolution merge scan | pair [--force] <a> <b> | status [id] | report [id] | cancel [id] | runs | doctor | pending [--expired] | candidates [--retry] | approve <id> | reject <id> | resume [id] [--retry] [--clear <id>] | skipped [--restore <id>] | discard <id> | undo <target> --run <id>\ndefaults: mergeApproval=false, mergeSkippedTtlDays=0, mergeRunTimeoutMinutes=0, mergePairTimeoutMinutes=15, mergeModelsRoot=""';
2
- function flags(args, allowed) {
3
- const values = new Map();
4
- for (let index = 0; index < args.length; index += 1) {
5
- const token = args[index];
6
- if (!token.startsWith('--') || token === '--force' || token === '--expired' || token === '--retry')
7
- continue;
8
- const name = token.slice(2);
9
- if (!allowed.includes(name))
10
- return { values, error: 'unknown flag: --' + name };
11
- const value = args[index + 1];
12
- if (value === undefined || value.startsWith('--'))
13
- return { values, error: 'missing value for --' + name };
14
- values.set(name, value);
15
- index += 1;
16
- }
17
- return { values };
18
- }
19
- function switches(args) {
20
- return new Set(args.filter(token => token === '--force' || token === '--expired' || token === '--retry').map(token => token.slice(2)));
21
- }
22
- function positional(args) {
23
- const filtered = args.filter(token => token !== '--force' && token !== '--expired' && token !== '--retry');
24
- return filtered.filter((token, index) => !token.startsWith('--') && !(index > 0 && filtered[index - 1].startsWith('--')));
25
- }
1
+ /** Merge command adapter over the family command catalog. No parser/usage table here. */
2
+ import { parseCommand } from '@lmzhen/dsh-evolution-commands';
3
+ import { MERGE_COMMAND_DEFS } from "./command-defs.js";
4
+ function text(value) { return typeof value === 'string' ? value : undefined; }
26
5
  export function parseMergeCommand(argv) {
27
- const [verb, ...rest] = argv;
28
- if (verb === undefined || verb === 'help' || verb === '--help' || verb === '-h')
6
+ const parsed = parseCommand(MERGE_COMMAND_DEFS, argv);
7
+ if (!parsed.ok)
8
+ return parsed;
9
+ if (parsed.help === true)
29
10
  return { ok: true, command: { verb: 'scan' }, help: true };
30
- const pos = positional(rest);
31
- const marked = switches(rest);
32
- switch (verb) {
33
- case 'scan': return pos.length === 0 ? { ok: true, command: { verb: 'scan' } } : { ok: false, code: 'usage', message: USAGE };
34
- case 'pair': return pos.length === 2 ? { ok: true, command: { verb: 'pair', a: pos[0], b: pos[1], ...(marked.has('force') ? { force: true } : {}) } } : { ok: false, code: 'usage', message: USAGE };
35
- case 'status': return pos.length <= 1 ? { ok: true, command: { verb: 'status', ...(pos[0] === undefined ? {} : { id: pos[0] }) } } : { ok: false, code: 'usage', message: USAGE };
36
- case 'report': return pos.length <= 1 ? { ok: true, command: { verb: 'report', ...(pos[0] === undefined ? {} : { id: pos[0] }) } } : { ok: false, code: 'usage', message: USAGE };
37
- case 'cancel': return pos.length <= 1 ? { ok: true, command: { verb: 'cancel', ...(pos[0] === undefined ? {} : { id: pos[0] }) } } : { ok: false, code: 'usage', message: USAGE };
38
- case 'runs': return pos.length === 0 ? { ok: true, command: { verb: 'runs' } } : { ok: false, code: 'usage', message: USAGE };
39
- case 'doctor': return pos.length === 0 ? { ok: true, command: { verb: 'doctor' } } : { ok: false, code: 'usage', message: USAGE };
40
- case 'pending': return pos.length === 0 ? { ok: true, command: { verb: 'pending', ...(marked.has('expired') ? { expired: true } : {}) } } : { ok: false, code: 'usage', message: USAGE };
41
- case 'candidates': return pos.length === 0 ? { ok: true, command: { verb: 'candidates', ...(marked.has('retry') ? { retry: true } : {}) } } : { ok: false, code: 'usage', message: USAGE };
42
- case 'approve': return pos.length === 1 ? { ok: true, command: { verb: 'approve', id: pos[0] } } : { ok: false, code: 'usage', message: USAGE };
43
- case 'reject': return pos.length === 1 ? { ok: true, command: { verb: 'reject', id: pos[0] } } : { ok: false, code: 'usage', message: USAGE };
44
- case 'resume': {
45
- const parsed = flags(rest, ['clear']);
46
- if (parsed.error !== undefined)
47
- return { ok: false, code: 'usage', message: parsed.error };
48
- const clear = parsed.values.get('clear');
49
- if (pos.length > 1)
50
- return { ok: false, code: 'usage', message: USAGE };
51
- return { ok: true, command: { verb: 'resume', ...(pos[0] === undefined ? {} : { id: pos[0] }), ...(clear === undefined ? {} : { clear }), ...(marked.has('retry') ? { retry: true } : {}) } };
52
- }
53
- case 'skipped': {
54
- const parsed = flags(rest, ['restore']);
55
- if (parsed.error !== undefined)
56
- return { ok: false, code: 'usage', message: parsed.error };
57
- if (pos.length > 0)
58
- return { ok: false, code: 'usage', message: USAGE };
59
- const restore = parsed.values.get('restore');
60
- return { ok: true, command: { verb: 'skipped', ...(restore === undefined ? {} : { restore }) } };
61
- }
62
- case 'discard': return pos.length === 1 ? { ok: true, command: { verb: 'discard', id: pos[0] } } : { ok: false, code: 'usage', message: USAGE };
63
- case 'undo': {
64
- const parsed = flags(rest, ['run']);
65
- if (parsed.error !== undefined)
66
- return { ok: false, code: 'usage', message: parsed.error };
67
- const target = pos[0];
68
- const run = parsed.values.get('run');
69
- if (target === undefined || run === undefined || pos.length !== 1)
70
- return { ok: false, code: 'usage', message: USAGE };
71
- return { ok: true, command: { verb: 'undo', target, run } };
72
- }
73
- default: return { ok: false, code: 'usage', message: 'unknown merge command: ' + verb + '\n' + USAGE };
11
+ const v = parsed.values;
12
+ switch (parsed.id) {
13
+ case 'merge.scan': return { ok: true, command: { verb: 'scan' } };
14
+ case 'merge.doctor': return { ok: true, command: { verb: 'doctor' } };
15
+ case 'merge.reconcile': return { ok: true, command: { verb: 'reconcile' } };
16
+ case 'merge.pair': return { ok: true, command: { verb: 'pair', a: text(v.a) ?? '', b: text(v.b) ?? '' } };
17
+ case 'merge.pair.force': return { ok: true, command: { verb: 'pair', a: text(v.a) ?? '', b: text(v.b) ?? '', force: true } };
18
+ case 'merge.status': return { ok: true, command: { verb: 'status', ...(text(v.id) === undefined ? {} : { id: text(v.id) }) } };
19
+ case 'merge.report': return { ok: true, command: { verb: 'report', ...(text(v.id) === undefined ? {} : { id: text(v.id) }) } };
20
+ case 'merge.cancel': return { ok: true, command: { verb: 'cancel', ...(text(v.id) === undefined ? {} : { id: text(v.id) }) } };
21
+ case 'merge.runs': return { ok: true, command: { verb: 'runs' } };
22
+ case 'merge.pending': return { ok: true, command: { verb: 'pending' } };
23
+ case 'merge.pending.expired': return { ok: true, command: { verb: 'pending', expired: true } };
24
+ case 'merge.approve': return { ok: true, command: { verb: 'approve', id: text(v.id) ?? '' } };
25
+ case 'merge.reject': return { ok: true, command: { verb: 'reject', id: text(v.id) ?? '' } };
26
+ case 'merge.skipped': return { ok: true, command: { verb: 'skipped' } };
27
+ case 'merge.skipped.restore': return { ok: true, command: { verb: 'skipped', restore: text(v.restore) ?? '' } };
28
+ case 'merge.candidates': return { ok: true, command: { verb: 'candidates' } };
29
+ case 'merge.candidates.retry': return { ok: true, command: { verb: 'candidates', retry: true, ...(text(v.id) === undefined ? {} : { id: text(v.id) }) } };
30
+ case 'merge.resume': return { ok: true, command: { verb: 'resume', ...(text(v.id) === undefined ? {} : { id: text(v.id) }), ...(text(v.clear) === undefined ? {} : { clear: text(v.clear) }) } };
31
+ case 'merge.resume.retry': return { ok: true, command: { verb: 'resume', retry: true, ...(text(v.id) === undefined ? {} : { id: text(v.id) }), ...(text(v.clear) === undefined ? {} : { clear: text(v.clear) }) } };
32
+ case 'merge.discard': return { ok: true, command: { verb: 'discard', id: text(v.id) ?? '' } };
33
+ case 'merge.undo': return { ok: true, command: { verb: 'undo', target: text(v.target) ?? '', run: text(v.run) ?? '' } };
34
+ default: return { ok: false, code: 'usage', message: 'unknown merge command: ' + parsed.id };
74
35
  }
75
36
  }
76
37
  export function mergeUsage() {
77
- return USAGE;
38
+ return 'usage: ' + MERGE_COMMAND_DEFS.filter(def => def.id !== 'merge.doctor' && def.usage !== false).map(def => def.fallback).join(' | ') + '\ndefaults: mergeApproval=false, mergeSkippedTtlDays=0, mergeRunTimeoutMinutes=0, mergePairTimeoutMinutes=15, mergeModelsRoot=""';
78
39
  }
79
40
  //# sourceMappingURL=commands.js.map
@@ -3,12 +3,19 @@
3
3
  *
4
4
  * Source bodies come from the LIVE tree (the plan's CAS baseline), the target body from
5
5
  * the merged tree. This function is pure except for the injected reads; it never commits.
6
+ * It also re-checks the round-1 decision anchors against the live tree BEFORE the commit
7
+ * port is called, so a source/target change is refused with a typed cause rather than a
8
+ * generic commit message.
6
9
  */
7
- import type { CommitMergePlan, WriteOrigin } from '@lmzhen/dsh-evolution-core';
10
+ import type { CommitMergePlan, WriteOrigin, SkillLimits } from '@lmzhen/dsh-evolution-core';
11
+ import { type RevisionFile } from '@lmzhen/dsh-evolution-core';
8
12
  import type { EvolutionIoLike } from '@lmzhen/dsh-evolution-core';
13
+ import type { RevisionRef } from './contracts.ts';
9
14
  export interface CommitAssemblyPort {
10
15
  /** Current SKILL.md body, or null when the skill is not live. */
11
16
  readSkill(name: string): Promise<string | null>;
17
+ /** Every support file of a live skill; absent means the port cannot enumerate them. */
18
+ readSupportFiles?(name: string): Promise<readonly RevisionFile[]>;
12
19
  /** Whether the name is already occupied by an archived (tombstoned) skill. */
13
20
  isArchived?(name: string): Promise<boolean>;
14
21
  /** Whether a directory exists for the name (including a SKILL.md-less ghost). */
@@ -17,12 +24,18 @@ export interface CommitAssemblyPort {
17
24
  export interface CommitAssemblyOptions {
18
25
  readonly origin: WriteOrigin;
19
26
  readonly summary: string;
27
+ readonly limits?: SkillLimits;
28
+ /** Round-1 decision anchors re-checked here (the store repeats the check under its lock). */
29
+ readonly targetRevision?: RevisionRef;
30
+ readonly sourceRevisions?: Readonly<Record<string, RevisionRef>>;
31
+ readonly supportDigest?: string;
20
32
  }
21
33
  export type CommitAssemblyResult = {
22
34
  readonly ok: true;
23
35
  readonly plan: CommitMergePlan;
24
36
  } | {
25
37
  readonly ok: false;
38
+ readonly code?: 'target-changed' | 'source-changed' | 'support-changed';
26
39
  readonly message: string;
27
40
  };
28
41
  export declare function assembleStagedCommit(io: EvolutionIoLike, home: string, runId: string, port: CommitAssemblyPort, options: CommitAssemblyOptions): Promise<CommitAssemblyResult>;
@@ -1,7 +1,55 @@
1
- import { isPresent } from '@lmzhen/dsh-evolution-core';
1
+ import { isPresent, revisionAnchorOf, revisionFingerprint, revisionSupportDigest } from '@lmzhen/dsh-evolution-core';
2
2
  import { readStaging } from "./staging.js";
3
3
  import { readStagedTree } from "./staged-tree.js";
4
4
  import { buildCommitPlan } from "./commit-plan.js";
5
+ function sameAnchor(expected, live) {
6
+ if (expected.kind === 'absent')
7
+ return live.kind === 'absent';
8
+ return live.kind === 'present' && expected.sha256 === live.sha256;
9
+ }
10
+ async function liveRevision(port, name, body) {
11
+ if (port.readSupportFiles === undefined)
12
+ return { verified: false };
13
+ let files;
14
+ try {
15
+ files = body === null ? [] : await port.readSupportFiles(name);
16
+ }
17
+ catch {
18
+ return { verified: false };
19
+ }
20
+ return { verified: true, anchor: revisionAnchorOf(name, body, files), fingerprint: body === null ? null : revisionFingerprint(name, body, files) };
21
+ }
22
+ /** Why the staging tree no longer matches the decision, or null when it still does. */
23
+ async function decisionRevisionFailure(port, requested, targetCurrent, sources, options) {
24
+ if (options.targetRevision === undefined && options.sourceRevisions === undefined && options.supportDigest === undefined)
25
+ return null;
26
+ const target = await liveRevision(port, requested, targetCurrent);
27
+ if (!target.verified)
28
+ return null;
29
+ if (options.targetRevision !== undefined && !sameAnchor(options.targetRevision, target.anchor)) {
30
+ return { code: 'target-changed', message: '目标技能「' + requested + '」的内容在决策之后被改过;拒绝提交(未写入任何内容)。' };
31
+ }
32
+ const fingerprints = target.fingerprint === null ? [] : [target.fingerprint];
33
+ for (const source of sources) {
34
+ // The staging manifest lists the target among its sources; the digest is over the
35
+ // participating skills (target + absorbed sources), so the target appears once.
36
+ if (source.name === requested)
37
+ continue;
38
+ const live = await liveRevision(port, source.name, source.body);
39
+ if (!live.verified)
40
+ continue;
41
+ const expected = options.sourceRevisions?.[source.name];
42
+ if (expected !== undefined && !sameAnchor(expected, live.anchor)) {
43
+ return { code: 'source-changed', message: '来源技能「' + source.name + '」的内容在决策之后被改过;拒绝提交(未写入任何内容)。' };
44
+ }
45
+ if (live.fingerprint !== null)
46
+ fingerprints.push(live.fingerprint);
47
+ }
48
+ if (options.supportDigest !== undefined && revisionSupportDigest(fingerprints) !== options.supportDigest) {
49
+ return { code: 'support-changed', message: '参与合并的技能支撑文件在决策之后被改过;拒绝提交(未写入任何内容)。' };
50
+ }
51
+ return null;
52
+ }
5
53
  export async function assembleStagedCommit(io, home, runId, port, options) {
6
54
  const staging = await readStaging(io, home, runId);
7
55
  if (!isPresent(staging))
@@ -10,11 +58,22 @@ export async function assembleStagedCommit(io, home, runId, port, options) {
10
58
  if (!isPresent(tree))
11
59
  return { ok: false, message: 'merged tree is not readable for run ' + runId };
12
60
  const requested = staging.value.manifest.target;
61
+ const requestedCurrent = await port.readSkill(requested);
13
62
  let target = requested;
14
- let targetCurrent = await port.readSkill(requested);
63
+ let targetCurrent = requestedCurrent;
15
64
  if (staging.value.manifest.mode !== 'create-umbrella' && targetCurrent === null) {
16
65
  return { ok: false, message: 'target is gone; refusing to re-create it: ' + requested };
17
66
  }
67
+ const sources = [];
68
+ for (const name of staging.value.manifest.sources) {
69
+ const body = await port.readSkill(name);
70
+ if (body === null)
71
+ return { ok: false, message: 'source skill is not live: ' + name };
72
+ sources.push({ name, body });
73
+ }
74
+ const revisionFailure = await decisionRevisionFailure(port, requested, requestedCurrent, sources, options);
75
+ if (revisionFailure !== null)
76
+ return { ok: false, code: revisionFailure.code, message: revisionFailure.message };
18
77
  const requestedArchived = targetCurrent === null && await port.isArchived?.(requested) === true;
19
78
  const requestedOccupied = targetCurrent === null && await port.isNameOccupied?.(requested) === true;
20
79
  if (staging.value.manifest.mode === 'create-umbrella' && (targetCurrent !== null || requestedArchived || requestedOccupied)) {
@@ -32,13 +91,6 @@ export async function assembleStagedCommit(io, home, runId, port, options) {
32
91
  if (target === requested)
33
92
  return { ok: false, message: 'no free umbrella target for ' + requested };
34
93
  }
35
- const sources = [];
36
- for (const name of staging.value.manifest.sources) {
37
- const body = await port.readSkill(name);
38
- if (body === null)
39
- return { ok: false, message: 'source skill is not live: ' + name };
40
- sources.push({ name, body });
41
- }
42
94
  return buildCommitPlan({
43
95
  target,
44
96
  create: targetCurrent === null,
@@ -48,6 +100,10 @@ export async function assembleStagedCommit(io, home, runId, port, options) {
48
100
  sourceBodies: sources,
49
101
  origin: options.origin,
50
102
  summary: options.summary,
103
+ ...(options.limits === undefined ? {} : { limits: options.limits }),
104
+ ...(options.targetRevision === undefined ? {} : { targetRevision: options.targetRevision }),
105
+ ...(options.sourceRevisions === undefined ? {} : { sourceRevisions: options.sourceRevisions }),
106
+ ...(options.supportDigest === undefined ? {} : { supportDigest: options.supportDigest }),
51
107
  });
52
108
  }
53
109
  //# sourceMappingURL=commit-assembly.js.map
@@ -3,7 +3,7 @@
3
3
  * receipt. A failed commit returns without a receipt; a landed commit with a failed receipt
4
4
  * stays successful and carries a warning.
5
5
  */
6
- import type { CommitMergePlan, CommitMergeResult, EvolutionIoLike } from '@lmzhen/dsh-evolution-core';
6
+ import type { CommitMergePlan, CommitMergeResult, EvolutionIoLike, SkillLimits } from '@lmzhen/dsh-evolution-core';
7
7
  import { type CommitAssemblyPort } from './commit-assembly.ts';
8
8
  import { type CommitReceipt } from './commit-receipt.ts';
9
9
  import type { UnitTransaction } from '@lmzhen/dsh-evolution-core';
@@ -21,6 +21,11 @@ export interface CommitStagedOptions {
21
21
  readonly summary: string;
22
22
  readonly now: () => number;
23
23
  readonly isBlocked?: (name: string) => boolean | Promise<boolean>;
24
+ readonly limits?: SkillLimits;
25
+ readonly validationPlan?: import('./contracts.ts').MergeValidationPlanRef;
26
+ readonly targetRevision?: import('./contracts.ts').RevisionRef;
27
+ readonly sourceRevisions?: Readonly<Record<string, import('./contracts.ts').RevisionRef>>;
28
+ readonly supportDigest?: string;
24
29
  /** G2 transitional: when present, commit through core UnitTransaction. */
25
30
  readonly unit?: UnitTransaction;
26
31
  }
@@ -34,6 +39,7 @@ export type CommitStagedResult = {
34
39
  readonly ok: false;
35
40
  readonly message: string;
36
41
  readonly plan?: CommitMergePlan;
42
+ readonly code?: CommitMergeResult['code'];
37
43
  };
38
44
  export declare function commitStagedRun(io: EvolutionIoLike, home: string, runId: string, port: CommitPort, options: CommitStagedOptions): Promise<CommitStagedResult>;
39
45
  //# sourceMappingURL=commit-execute.d.ts.map
@@ -2,20 +2,31 @@ import { assembleStagedCommit } from "./commit-assembly.js";
2
2
  import { writeCommitReceipt } from "./commit-receipt.js";
3
3
  import { pairKey } from "./contracts.js";
4
4
  import { writeJournal } from "./domain/commit-journal-store.js";
5
- import { snapshotPath, writeCommitSnapshot } from "./commit-snapshot.js";
5
+ import { evidenceSha256 as digestEvidence } from "./domain/evidence-hash.js";
6
+ import { snapshotPath } from "./commit-snapshot.js";
6
7
  export async function commitStagedRun(io, home, runId, port, options) {
7
- const assembled = await assembleStagedCommit(io, home, runId, port, { origin: options.origin, summary: options.summary });
8
+ const assembled = await assembleStagedCommit(io, home, runId, port, {
9
+ origin: options.origin,
10
+ summary: options.summary,
11
+ ...(options.limits === undefined ? {} : { limits: options.limits }),
12
+ ...(options.targetRevision === undefined ? {} : { targetRevision: options.targetRevision }),
13
+ ...(options.sourceRevisions === undefined ? {} : { sourceRevisions: options.sourceRevisions }),
14
+ ...(options.supportDigest === undefined ? {} : { supportDigest: options.supportDigest }),
15
+ });
8
16
  if (!assembled.ok)
9
- return { ok: false, message: assembled.message };
17
+ return { ok: false, message: assembled.message, ...(assembled.code === undefined ? {} : { code: assembled.code }) };
10
18
  if (await options.isBlocked?.(assembled.plan.target) === true) {
11
19
  return { ok: false, message: 'target is blocked by curator: ' + assembled.plan.target, plan: assembled.plan };
12
20
  }
13
21
  const journal = { runId, target: assembled.plan.target, create: !assembled.plan.targetExists, sources: assembled.plan.sources, phase: 'prepared', updatedAt: new Date(options.now()).toISOString() };
14
22
  let result;
15
- const transaction = options.unit ?? port.unit;
16
- if (transaction !== undefined) {
23
+ let evidenceSha256;
24
+ const transaction = options.unit ?? port.unit ?? legacyUnitAdapter(port, assembled.plan, options, runId);
25
+ if (transaction === undefined)
26
+ return { ok: false, message: 'commit port missing: UnitTransaction or commitMerge required', plan: assembled.plan };
27
+ {
17
28
  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 };
29
+ 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, ...(options.limits === undefined ? {} : { skillLimits: options.limits }), ...(options.validationPlan === undefined ? {} : { validationPlan: options.validationPlan }), ...(options.targetRevision === undefined ? {} : { targetRevision: options.targetRevision }), ...(options.sourceRevisions === undefined ? {} : { sourceRevisions: options.sourceRevisions }), ...(options.supportDigest === undefined ? {} : { supportDigest: options.supportDigest }) };
19
30
  try {
20
31
  await writeJournal(io, home, journal);
21
32
  }
@@ -30,24 +41,19 @@ export async function commitStagedRun(io, home, runId, port, options) {
30
41
  }
31
42
  catch { }
32
43
  ;
33
- return { ok: false, message: committed.message, plan: assembled.plan };
44
+ return { ok: false, message: committed.message, plan: assembled.plan, ...(committed.code === 'target-changed' || committed.code === 'source-changed' || committed.code === 'support-changed' ? { code: committed.code } : {}) };
34
45
  }
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 };
36
- }
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
+ const unitId = committed.evidence.unitId;
47
+ if (committed.evidence.unitId !== prepared.prepared.unitId && !prepared.prepared.unitId.startsWith('legacy:'))
48
+ return { ok: false, message: 'unit evidence does not match prepared unitId', plan: assembled.plan };
49
+ const finalize = transaction.finalize;
50
+ if (finalize !== undefined) {
51
+ const finalized = await finalize.call(transaction, unitId);
52
+ if (!finalized.ok)
53
+ return { ok: false, message: 'unit committed but finalize failed: ' + finalized.message, plan: assembled.plan };
46
54
  }
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);
55
+ evidenceSha256 = digestEvidence(committed.evidence);
56
+ result = { ok: true, message: 'merged', unitId, archived: committed.evidence.sources.filter(source => source.archived).map(source => source.name), mutations: committed.evidence.mutations };
51
57
  }
52
58
  if (!result.ok) {
53
59
  try {
@@ -65,6 +71,8 @@ export async function commitStagedRun(io, home, runId, port, options) {
65
71
  return { ok: true, plan: assembled.plan, result, warning: 'commit landed without a unitId; receipt not written' };
66
72
  const receipt = {
67
73
  runId,
74
+ ...(evidenceSha256 === undefined ? {} : { evidenceSha256 }),
75
+ ...(options.validationPlan === undefined ? {} : { validationPlan: options.validationPlan }),
68
76
  parentRunId: runId.split('/pairs/')[0] ?? runId,
69
77
  unitId: result.unitId,
70
78
  target: assembled.plan.target,
@@ -91,4 +99,28 @@ export async function commitStagedRun(io, home, runId, port, options) {
91
99
  };
92
100
  }
93
101
  }
102
+ function legacyUnitAdapter(port, plan, options, runId) {
103
+ if (port.commitMerge === undefined)
104
+ return undefined;
105
+ let evidence;
106
+ const unitId = 'legacy:' + plan.target + ':' + String(options.now());
107
+ return {
108
+ async prepare() { return { ok: true, prepared: { unitId, planDigest: unitId } }; },
109
+ async commit() {
110
+ if (port.commitMerge === undefined)
111
+ return { ok: false, code: 'unit-commit-failed', message: 'commitMerge missing' };
112
+ const result = await port.commitMerge(plan);
113
+ if (!result.ok)
114
+ return { ok: false, code: result.code ?? 'unit-commit-failed', message: result.message };
115
+ const actualUnitId = result.unitId ?? unitId;
116
+ evidence = { schemaVersion: 1, unitId: actualUnitId, runId, planDigest: actualUnitId, state: 'complete', target: { before: plan.expectedTargetBody, after: plan.body, supportDigest: '', created: !plan.targetExists }, sources: plan.expectedSourceBodies.map(source => ({ name: source.name, before: source.body, after: null, archived: (result.archived ?? []).includes(source.name), absorbedInto: plan.target })), mutations: result.mutations ?? 0, at: options.now() };
117
+ return { ok: true, evidence };
118
+ },
119
+ async verify() { return evidence ?? null; },
120
+ async rollback() { return { ok: false, code: 'unit-committed', message: 'legacy commit cannot roll back' }; },
121
+ async finalize() { return evidence === undefined ? { ok: false, code: 'unit-plan-missing', message: 'legacy unit has no evidence' } : { ok: true, evidence }; },
122
+ async rollForward() { return evidence === undefined ? { ok: false, code: 'unit-plan-missing', message: 'legacy unit has no evidence' } : { ok: true, evidence }; },
123
+ async cancel() { return { ok: false, phase: 'none' }; },
124
+ };
125
+ }
94
126
  //# sourceMappingURL=commit-execute.js.map
@@ -5,7 +5,8 @@
5
5
  * This module only turns those bytes into a CommitMergePlan; it never reads the live tree and
6
6
  * never decides whether a merge is allowed (validateStagedTree does that).
7
7
  */
8
- import type { CommitMergePlan, WriteOrigin } from '@lmzhen/dsh-evolution-core';
8
+ import type { CommitMergePlan, WriteOrigin, SkillLimits } from '@lmzhen/dsh-evolution-core';
9
+ import type { RevisionRef } from './contracts.ts';
9
10
  export type ResolveTargetResult = {
10
11
  readonly ok: true;
11
12
  readonly name: string;
@@ -33,6 +34,11 @@ export interface BuildCommitPlanInput {
33
34
  }[];
34
35
  readonly origin: WriteOrigin;
35
36
  readonly summary: string;
37
+ readonly limits?: SkillLimits;
38
+ /** Round-1 anchors; carried on the plan so every committer (unit or legacy port) re-checks them. */
39
+ readonly targetRevision?: RevisionRef;
40
+ readonly sourceRevisions?: Readonly<Record<string, RevisionRef>>;
41
+ readonly supportDigest?: string;
36
42
  }
37
43
  export type BuildCommitPlanResult = {
38
44
  readonly ok: true;
@@ -48,8 +48,12 @@ export function buildCommitPlan(input) {
48
48
  supportFiles: input.mergedFiles,
49
49
  expectedTargetBody: input.targetCurrent,
50
50
  expectedSourceBodies: sources,
51
+ ...(input.targetRevision === undefined ? {} : { targetRevision: input.targetRevision }),
52
+ ...(input.sourceRevisions === undefined ? {} : { sourceRevisions: input.sourceRevisions }),
53
+ ...(input.supportDigest === undefined ? {} : { supportDigest: input.supportDigest }),
51
54
  origin: input.origin,
52
55
  summary: input.summary,
56
+ ...(input.limits === undefined ? {} : { limits: input.limits }),
53
57
  },
54
58
  };
55
59
  }
@@ -1,8 +1,11 @@
1
1
  import type { EvolutionIoLike } from '@lmzhen/dsh-evolution-core';
2
+ import type { MergeValidationPlanRef } from './contracts.ts';
2
3
  export interface CommitReceipt {
3
4
  readonly runId: string;
4
5
  readonly parentRunId?: string;
5
6
  readonly unitId: string;
7
+ readonly evidenceSha256?: string;
8
+ readonly validationPlan?: MergeValidationPlanRef;
6
9
  readonly target: string;
7
10
  readonly create: boolean;
8
11
  readonly sources: readonly string[];
@@ -25,6 +25,7 @@ export interface MergeFailure {
25
25
  readonly message: string;
26
26
  readonly retryable: boolean;
27
27
  readonly retryFrom: 'agent1' | 'agent2' | 'none';
28
+ readonly lengths?: Readonly<Record<string, number>>;
28
29
  }
29
30
  /** One persisted pair. The id is stable for the UI; the pairKey is the natural key. */
30
31
  export interface MergeGateFact {
@@ -43,6 +44,12 @@ export interface MergeCandidate {
43
44
  readonly gate?: MergeGateFact;
44
45
  readonly source?: 'lexical' | 'proposed' | 'engine';
45
46
  }
47
+ export type RevisionRef = {
48
+ readonly kind: 'absent';
49
+ } | {
50
+ readonly kind: 'present';
51
+ readonly sha256: string;
52
+ };
46
53
  export interface MergeDecision {
47
54
  readonly shouldMerge: boolean;
48
55
  readonly mode: 'merge-into-existing' | 'create-umbrella' | 'keep';
@@ -50,9 +57,21 @@ export interface MergeDecision {
50
57
  readonly sources: readonly string[];
51
58
  readonly rationale: string;
52
59
  readonly confidence?: number;
60
+ readonly promptVersion?: string;
61
+ readonly promptHash?: string;
53
62
  readonly aSummary?: string;
54
63
  readonly bSummary?: string;
55
64
  readonly cacheRead?: number;
65
+ readonly targetRevisionRef?: RevisionRef;
66
+ readonly sourceRevisionRef?: RevisionRef;
67
+ /** Per-source anchors recorded with the decision; a commit re-checks each one. */
68
+ readonly sourceRevisions?: Readonly<Record<string, RevisionRef>>;
69
+ readonly supportDigest?: string;
70
+ }
71
+ export interface MergeValidationPlanRef {
72
+ readonly planVersion: 1;
73
+ readonly stageDigest: string;
74
+ readonly revision: string;
56
75
  }
57
76
  export interface MergeUsage {
58
77
  readonly inputTokens?: number;
@@ -86,7 +105,18 @@ export interface MergeReportPair {
86
105
  readonly unitId?: string;
87
106
  readonly failure?: MergeFailure;
88
107
  readonly usage?: MergeUsage;
108
+ readonly lengths?: Readonly<Record<string, number>>;
89
109
  readonly timing?: MergeTiming;
110
+ readonly validationPlan?: MergeValidationPlanRef;
111
+ readonly criteria?: MergeCriteria;
112
+ readonly reportOnly?: boolean;
113
+ }
114
+ export interface MergeCriteria {
115
+ readonly decision: 'keep' | 'merge' | 'candidate';
116
+ readonly validation: 'not-run' | 'report-only' | 'required';
117
+ readonly commit: 'never' | 'if-approved' | 'required';
118
+ readonly archive: 'none' | 'sources';
119
+ readonly evidence: 'none' | 'receipt';
90
120
  }
91
121
  export interface MergeReport {
92
122
  readonly schemaVersion: 2;
@@ -111,6 +141,17 @@ export interface MergeReport {
111
141
  readonly pairKey: string;
112
142
  readonly queue: MergeQueue;
113
143
  }[];
144
+ readonly reportOnly?: readonly MergeReportPair[];
145
+ readonly validationFailures?: readonly MergeReportPair[];
146
+ readonly archived?: readonly string[];
147
+ readonly commit?: {
148
+ readonly unitId?: string;
149
+ readonly target?: string;
150
+ readonly created?: boolean;
151
+ };
152
+ readonly usage?: MergeUsage;
153
+ readonly timing?: MergeTiming;
154
+ readonly criteria?: readonly MergeCriteria[];
114
155
  }
115
156
  export interface MergeRow {
116
157
  readonly id: string;
@@ -131,6 +172,8 @@ export interface MergeRow {
131
172
  readonly decision?: MergeDecision;
132
173
  readonly sourceRevision?: string;
133
174
  readonly targetRevision?: string;
175
+ readonly sourceRevisionRef?: RevisionRef;
176
+ readonly targetRevisionRef?: RevisionRef;
134
177
  readonly runId?: string;
135
178
  readonly unitId?: string;
136
179
  readonly origin?: 'agent' | 'human' | 'system';
@@ -1,9 +1,19 @@
1
1
  import { type EvolutionIoLike } from '@lmzhen/dsh-evolution-core';
2
+ import { type ReconcileReport } from './domain/reconcile.ts';
3
+ import type { DoctorView } from './wire.ts';
2
4
  export declare const MERGE_DOCTOR_SCHEMA_VERSION = 3;
5
+ /** The global doctor's merge line; built from the DoctorView envelope, never from raw files. */
6
+ export interface MergeDoctorFact {
7
+ readonly status: 'ok' | 'warn' | 'fail' | 'unknown';
8
+ readonly detail: string;
9
+ readonly remedy?: string;
10
+ }
11
+ /** Compose the one-line global-doctor fact from buildDoctorView + the last reconcile report. */
12
+ export declare function mergeDoctorFact(view: DoctorView, report: ReconcileReport | null): MergeDoctorFact;
3
13
  export interface MergeDoctorRow {
4
14
  readonly key: string;
5
15
  readonly label: string;
6
- readonly status: 'ok' | 'warn' | 'unknown';
16
+ readonly status: 'ok' | 'warn' | 'blocked' | 'unknown';
7
17
  readonly detail: string;
8
18
  }
9
19
  /** Global doctor host section: a missing merge mount must fail with E-325, not render empty. */