@lmzhen/dsh-evolution-merge 0.30.3 → 0.30.4

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.
package/lib/index.js CHANGED
@@ -588,6 +588,7 @@ function recoveryFor(journal) {
588
588
  case "committed":
589
589
  case "rolled-back":
590
590
  case "failed": return "none";
591
+ default: return "refuse";
591
592
  }
592
593
  }
593
594
  //#endregion
@@ -648,7 +649,7 @@ function isSafeSkillName(name) {
648
649
  return /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name) && name !== "." && name !== "..";
649
650
  }
650
651
  function skillFilePath(root, name, path) {
651
- if (!isSafeSkillName(name) || path.startsWith("/") || path.includes("..")) throw new Error("unsafe skill file: " + name + "/" + path);
652
+ if (!isSafeSkillName(name) || path.startsWith("/") || path.split("/").includes("..")) throw new Error("unsafe skill file: " + name + "/" + path);
652
653
  const base = resolve(root, name);
653
654
  const file = resolve(base, path);
654
655
  if (!file.startsWith(base + sep)) throw new Error("skill file escapes skill: " + name + "/" + path);
@@ -662,68 +663,6 @@ function skillBodyPath(root, name) {
662
663
  return path;
663
664
  }
664
665
  //#endregion
665
- //#region lib/types/commit-recovery.js
666
- /** Startup replay for non-terminal commit journals. */
667
- async function recoverCommitJournals(io, home, now, skillRoot, restoreSource) {
668
- const dir = join(home, "merge", "journals");
669
- if (!await io.exists(dir)) return [];
670
- const files = (await io.list(dir)).filter((name) => name.endsWith(".json"));
671
- const reports = [];
672
- for (const file of files) {
673
- let journal;
674
- try {
675
- journal = JSON.parse(await io.readText(join(dir, file)) ?? "");
676
- } catch {
677
- continue;
678
- }
679
- const recovery = recoveryFor(journal);
680
- if (recovery === "none") continue;
681
- const snapshot = await readCommitSnapshot(io, home, journal.runId);
682
- let action = "report-only";
683
- let ok = true;
684
- if (recovery === "retry-commit") action = "no-host-write";
685
- else if (snapshot === null) {
686
- action = "snapshot-missing";
687
- ok = false;
688
- } else try {
689
- if (recovery === "rollback-sources") {
690
- let restoredArchived = restoreSource !== void 0;
691
- if (restoreSource !== void 0) {
692
- for (const source of snapshot.sourceBodies) if (!await restoreSource(source.name)) restoredArchived = false;
693
- }
694
- if (!restoredArchived) for (const source of snapshot.sourceBodies) await io.writeText(skillBodyPath(skillRoot, source.name), source.body);
695
- }
696
- if (snapshot.create && snapshot.targetBody === null) try {
697
- await io.remove(skillBodyPath(skillRoot, snapshot.target));
698
- } catch {}
699
- else if (snapshot.targetBody !== null) await io.writeText(skillBodyPath(skillRoot, snapshot.target), snapshot.targetBody);
700
- const oldPaths = new Set((snapshot.targetSupport ?? []).map((file) => file.path));
701
- for (const file of snapshot.targetSupport ?? []) await io.writeText(skillFilePath(skillRoot, snapshot.target, file.path), file.content);
702
- for (const file of snapshot.targetSupportAfter ?? []) if (!oldPaths.has(file.path)) try {
703
- await io.remove(skillFilePath(skillRoot, snapshot.target, file.path));
704
- } catch {}
705
- for (const file of snapshot.sourceSupport ?? []) await io.writeText(skillFilePath(skillRoot, file.name, file.path), file.content);
706
- action = recovery === "rollback-sources" ? "restored-sources-and-target" : "restored-target";
707
- } catch (error) {
708
- action = "restore-failed: " + (error instanceof Error ? error.message : String(error));
709
- ok = false;
710
- }
711
- await writeJournal(io, home, {
712
- ...journal,
713
- phase: ok ? "rolled-back" : "failed",
714
- updatedAt: new Date(now).toISOString()
715
- });
716
- reports.push({
717
- runId: journal.runId,
718
- phase: journal.phase,
719
- action,
720
- ok
721
- });
722
- }
723
- await io.writeText(join(home, "merge", "recovery.json"), JSON.stringify(reports, null, 2));
724
- return reports;
725
- }
726
- //#endregion
727
666
  //#region lib/types/commit-receipt.js
728
667
  /**
729
668
  * Commit receipt: <run>/commit.json, written immediately after a successful commit.
@@ -799,6 +738,113 @@ async function readCommitReceipt(io, home, runId, target) {
799
738
  return null;
800
739
  }
801
740
  //#endregion
741
+ //#region lib/types/commit-recovery.js
742
+ /** Startup replay for non-terminal commit journals. */
743
+ async function recoverCommitJournals(io, home, now, skillRoot, restoreSource, isActive) {
744
+ const dir = join(home, "merge", "journals");
745
+ if (!await io.exists(dir)) return [];
746
+ const files = (await io.list(dir)).filter((name) => name.endsWith(".json"));
747
+ const reports = [];
748
+ for (const file of files) {
749
+ let journal;
750
+ try {
751
+ journal = JSON.parse(await io.readText(join(dir, file)) ?? "");
752
+ } catch {
753
+ continue;
754
+ }
755
+ if (isActive?.(journal.runId) === true) {
756
+ reports.push({
757
+ runId: journal.runId,
758
+ phase: journal.phase,
759
+ action: "skipped-active-run",
760
+ ok: true
761
+ });
762
+ continue;
763
+ }
764
+ const recovery = recoveryFor(journal);
765
+ if (recovery === "none") continue;
766
+ if (recovery === "refuse") {
767
+ reports.push({
768
+ runId: journal.runId,
769
+ phase: journal.phase,
770
+ action: "unknown-phase-refused",
771
+ ok: false
772
+ });
773
+ continue;
774
+ }
775
+ let receipt = null;
776
+ try {
777
+ receipt = await readCommitReceipt(io, home, journal.runId, journal.target);
778
+ } catch {
779
+ reports.push({
780
+ runId: journal.runId,
781
+ phase: journal.phase,
782
+ action: "receipt-read-failed",
783
+ ok: false
784
+ });
785
+ continue;
786
+ }
787
+ if (receipt !== null) {
788
+ await writeJournal(io, home, {
789
+ ...journal,
790
+ phase: "committed",
791
+ unitId: receipt.unitId,
792
+ updatedAt: new Date(now).toISOString()
793
+ });
794
+ reports.push({
795
+ runId: journal.runId,
796
+ phase: journal.phase,
797
+ action: "receipt-committed",
798
+ ok: true
799
+ });
800
+ continue;
801
+ }
802
+ const snapshot = await readCommitSnapshot(io, home, journal.runId);
803
+ let action = "report-only";
804
+ let ok = true;
805
+ if (recovery === "retry-commit") action = "no-host-write";
806
+ else if (snapshot === null) {
807
+ action = "snapshot-missing";
808
+ ok = false;
809
+ } else try {
810
+ if (recovery === "rollback-sources") {
811
+ let restoredArchived = restoreSource !== void 0;
812
+ if (restoreSource !== void 0) {
813
+ for (const source of snapshot.sourceBodies) if (!await restoreSource(source.name)) restoredArchived = false;
814
+ }
815
+ if (!restoredArchived) for (const source of snapshot.sourceBodies) await io.writeText(skillBodyPath(skillRoot, source.name), source.body);
816
+ }
817
+ if (snapshot.create && snapshot.targetBody === null) try {
818
+ await io.remove(skillBodyPath(skillRoot, snapshot.target));
819
+ } catch {}
820
+ else if (snapshot.targetBody !== null) await io.writeText(skillBodyPath(skillRoot, snapshot.target), snapshot.targetBody);
821
+ const oldPaths = new Set((snapshot.targetSupport ?? []).map((file) => file.path));
822
+ for (const file of snapshot.targetSupport ?? []) await io.writeText(skillFilePath(skillRoot, snapshot.target, file.path), file.content);
823
+ for (const file of snapshot.targetSupportAfter ?? []) if (!oldPaths.has(file.path)) try {
824
+ await io.remove(skillFilePath(skillRoot, snapshot.target, file.path));
825
+ } catch {}
826
+ for (const file of snapshot.sourceSupport ?? []) await io.writeText(skillFilePath(skillRoot, file.name, file.path), file.content);
827
+ action = recovery === "rollback-sources" ? "restored-sources-and-target" : "restored-target";
828
+ } catch (error) {
829
+ action = "restore-failed: " + (error instanceof Error ? error.message : String(error));
830
+ ok = false;
831
+ }
832
+ if (ok) await writeJournal(io, home, {
833
+ ...journal,
834
+ phase: "rolled-back",
835
+ updatedAt: new Date(now).toISOString()
836
+ });
837
+ reports.push({
838
+ runId: journal.runId,
839
+ phase: journal.phase,
840
+ action,
841
+ ok
842
+ });
843
+ }
844
+ await io.writeText(join(home, "merge", "recovery.json"), JSON.stringify(reports, null, 2));
845
+ return reports;
846
+ }
847
+ //#endregion
802
848
  //#region lib/types/coordinator.js
803
849
  /**
804
850
  * Pure merge transition table. No io, no run shell, no subagents.
@@ -3552,11 +3598,16 @@ function skillLibraryPort(skills) {
3552
3598
  async readSupportFiles(name) {
3553
3599
  if (typeof skills.listSupportFiles !== "function") return [];
3554
3600
  const probe = await skills.listSupportFiles(name);
3555
- if (probe.kind !== "present") return [];
3556
- return await Promise.all(probe.value.map(async (path) => ({
3557
- path,
3558
- content: await skills.readSupportFile(name, path) ?? ""
3559
- })));
3601
+ if (probe.kind === "unknown") throw new Error("support file list unknown: " + name);
3602
+ if (probe.kind === "absent") return [];
3603
+ return await Promise.all(probe.value.map(async (path) => {
3604
+ const content = await skills.readSupportFile(name, path);
3605
+ if (content === null) throw new Error("support file disappeared: " + name + "/" + path);
3606
+ return {
3607
+ path,
3608
+ content
3609
+ };
3610
+ }));
3560
3611
  }
3561
3612
  };
3562
3613
  }
@@ -3614,6 +3665,7 @@ function createMergeRuntime(deps) {
3614
3665
  b: command.b
3615
3666
  }];
3616
3667
  const existing = (await loadMergeState(deps.io, deps.home)).state.rows.find((row) => row.pairKey === pairKey(command.a, command.b));
3668
+ if (existing?.state === "skipped") await dropRow(deps.io, deps.home, deps.now, existing.id);
3617
3669
  if (existing?.state === "pending") return {
3618
3670
  ok: false,
3619
3671
  text: "该 pair 已在待批准队列,先 approve/reject/discard: " + existing.id
@@ -4482,8 +4534,12 @@ function apply(ctx, rawConfig = {}) {
4482
4534
  ...policyStageLimits(policySnapshot?.get?.())
4483
4535
  }
4484
4536
  });
4485
- recoverCommitJournals(io, home, Date.now(), skillsRoot(), async (name) => {
4537
+ const mergeSkillsRoot = skills.root ?? skillsRoot();
4538
+ recoverCommitJournals(io, home, Date.now(), mergeSkillsRoot, async (name) => {
4486
4539
  return (await skills.restoreFromArchive(name)).ok;
4540
+ }, (runId) => {
4541
+ const run = runs.find(runId);
4542
+ return run !== void 0 && run.state === "running";
4487
4543
  }).then((reports) => {
4488
4544
  if (reports.length > 0) ctx.logger.info("merge commit recovery: " + reports.map((report) => report.runId + "=" + report.action).join(", "));
4489
4545
  }).catch((error) => {
@@ -13,9 +13,16 @@ export function skillLibraryPort(skills) {
13
13
  if (typeof skills.listSupportFiles !== 'function')
14
14
  return [];
15
15
  const probe = await skills.listSupportFiles(name);
16
- if (probe.kind !== 'present')
16
+ if (probe.kind === 'unknown')
17
+ throw new Error('support file list unknown: ' + name);
18
+ if (probe.kind === 'absent')
17
19
  return [];
18
- return await Promise.all(probe.value.map(async (path) => ({ path, content: await skills.readSupportFile(name, path) ?? '' })));
20
+ return await Promise.all(probe.value.map(async (path) => {
21
+ const content = await skills.readSupportFile(name, path);
22
+ if (content === null)
23
+ throw new Error('support file disappeared: ' + name + '/' + path);
24
+ return { path, content };
25
+ }));
19
26
  },
20
27
  };
21
28
  }
@@ -6,5 +6,5 @@ export interface CommitRecoveryReport {
6
6
  readonly action: string;
7
7
  readonly ok: boolean;
8
8
  }
9
- export declare function recoverCommitJournals(io: EvolutionIoLike, home: string, now: number, skillRoot: string, restoreSource?: (name: string) => Promise<boolean>): Promise<readonly CommitRecoveryReport[]>;
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[]>;
10
10
  //# sourceMappingURL=commit-recovery.d.ts.map
@@ -3,7 +3,8 @@ import { join } from 'node:path';
3
3
  import { recoveryFor } from "./domain/commit-journal.js";
4
4
  import { writeJournal } from "./domain/commit-journal-store.js";
5
5
  import { readCommitSnapshot, skillBodyPath, skillFilePath } from "./commit-snapshot.js";
6
- export async function recoverCommitJournals(io, home, now, skillRoot, restoreSource) {
6
+ import { readCommitReceipt } from "./commit-receipt.js";
7
+ export async function recoverCommitJournals(io, home, now, skillRoot, restoreSource, isActive) {
7
8
  const dir = join(home, 'merge', 'journals');
8
9
  if (!(await io.exists(dir)))
9
10
  return [];
@@ -17,9 +18,30 @@ export async function recoverCommitJournals(io, home, now, skillRoot, restoreSou
17
18
  catch {
18
19
  continue;
19
20
  }
21
+ if (isActive?.(journal.runId) === true) {
22
+ reports.push({ runId: journal.runId, phase: journal.phase, action: 'skipped-active-run', ok: true });
23
+ continue;
24
+ }
20
25
  const recovery = recoveryFor(journal);
21
26
  if (recovery === 'none')
22
27
  continue;
28
+ if (recovery === 'refuse') {
29
+ reports.push({ runId: journal.runId, phase: journal.phase, action: 'unknown-phase-refused', ok: false });
30
+ continue;
31
+ }
32
+ let receipt = null;
33
+ try {
34
+ receipt = await readCommitReceipt(io, home, journal.runId, journal.target);
35
+ }
36
+ catch {
37
+ reports.push({ runId: journal.runId, phase: journal.phase, action: 'receipt-read-failed', ok: false });
38
+ continue;
39
+ }
40
+ if (receipt !== null) {
41
+ await writeJournal(io, home, { ...journal, phase: 'committed', unitId: receipt.unitId, updatedAt: new Date(now).toISOString() });
42
+ reports.push({ runId: journal.runId, phase: journal.phase, action: 'receipt-committed', ok: true });
43
+ continue;
44
+ }
23
45
  const snapshot = await readCommitSnapshot(io, home, journal.runId);
24
46
  let action = 'report-only';
25
47
  let ok = true;
@@ -75,7 +97,8 @@ export async function recoverCommitJournals(io, home, now, skillRoot, restoreSou
75
97
  ok = false;
76
98
  }
77
99
  }
78
- await writeJournal(io, home, { ...journal, phase: ok ? 'rolled-back' : 'failed', updatedAt: new Date(now).toISOString() });
100
+ if (ok)
101
+ await writeJournal(io, home, { ...journal, phase: 'rolled-back', updatedAt: new Date(now).toISOString() });
79
102
  reports.push({ runId: journal.runId, phase: journal.phase, action, ok });
80
103
  }
81
104
  await io.writeText(join(home, 'merge', 'recovery.json'), JSON.stringify(reports, null, 2));
@@ -45,7 +45,7 @@ export function isSafeSkillName(name) {
45
45
  return /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name) && name !== '.' && name !== '..';
46
46
  }
47
47
  export function skillFilePath(root, name, path) {
48
- if (!isSafeSkillName(name) || path.startsWith('/') || path.includes('..'))
48
+ if (!isSafeSkillName(name) || path.startsWith('/') || path.split('/').includes('..'))
49
49
  throw new Error('unsafe skill file: ' + name + '/' + path);
50
50
  const base = resolve(root, name);
51
51
  const file = resolve(base, path);
@@ -10,7 +10,7 @@ export interface CommitJournal {
10
10
  readonly updatedAt: string;
11
11
  }
12
12
  export declare function advanceJournal(journal: CommitJournal, phase: JournalPhase, at: string): CommitJournal;
13
- export type JournalRecovery = 'none' | 'retry-commit' | 'rollback-target' | 'rollback-sources';
13
+ export type JournalRecovery = 'none' | 'retry-commit' | 'rollback-target' | 'rollback-sources' | 'refuse';
14
14
  /** What a restart may do with the last durable phase. */
15
15
  export declare function recoveryFor(journal: CommitJournal): JournalRecovery;
16
16
  //# sourceMappingURL=commit-journal.d.ts.map
@@ -20,6 +20,7 @@ export function recoveryFor(journal) {
20
20
  case 'committed':
21
21
  case 'rolled-back':
22
22
  case 'failed': return 'none';
23
+ default: return 'refuse';
23
24
  }
24
25
  }
25
26
  //# sourceMappingURL=commit-journal.js.map
@@ -138,9 +138,13 @@ export function apply(ctx, rawConfig = {}) {
138
138
  io, ctx, authority: authorityFrom({ origin: 'subagent' }),
139
139
  limits: { ...DEFAULT_SKILL_LIMITS, ...policyStageLimits(policySnapshot?.get?.()) },
140
140
  });
141
- void recoverCommitJournals(io, home, Date.now(), skillsRoot(), async (name) => {
141
+ const mergeSkillsRoot = skills.root ?? skillsRoot();
142
+ void recoverCommitJournals(io, home, Date.now(), mergeSkillsRoot, async (name) => {
142
143
  const result = await skills.restoreFromArchive(name);
143
144
  return result.ok;
145
+ }, (runId) => {
146
+ const run = runs.find(runId);
147
+ return run !== undefined && run.state === 'running';
144
148
  }).then((reports) => { if (reports.length > 0)
145
149
  ctx.logger.info('merge commit recovery: ' + reports.map(report => report.runId + '=' + report.action).join(', ')); }).catch((error) => { ctx.logger.warn('merge commit recovery failed: ' + (error instanceof Error ? error.message : String(error))); });
146
150
  const channel = subagentChannelAdapter(createSubagentChannel(ctx));
@@ -55,6 +55,9 @@ export function createMergeRuntime(deps) {
55
55
  }
56
56
  const pair = [{ a: command.a, b: command.b }];
57
57
  const existing = (await loadMergeState(deps.io, deps.home)).state.rows.find(row => row.pairKey === pairKey(command.a, command.b));
58
+ if (existing?.state === 'skipped') {
59
+ await dropRow(deps.io, deps.home, deps.now, existing.id);
60
+ }
58
61
  if (existing?.state === 'pending') {
59
62
  return { ok: false, text: '该 pair 已在待批准队列,先 approve/reject/discard: ' + existing.id };
60
63
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lmzhen/dsh-evolution-merge",
3
3
  "description": "Skill auto-merge domain: screening, two-round authoring, queues and commit orchestration (community build)",
4
- "version": "0.30.3",
4
+ "version": "0.30.4",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -27,8 +27,8 @@
27
27
  "license": "MIT",
28
28
  "dependencies": {
29
29
  "@deepseek-ai/schemastery": "^3.18.5",
30
- "@lmzhen/dsh-evolution-core": "^0.30.3",
31
- "@lmzhen/dsh-evolution-merge-engine": "^0.30.3"
30
+ "@lmzhen/dsh-evolution-core": "^0.30.4",
31
+ "@lmzhen/dsh-evolution-merge-engine": "^0.30.4"
32
32
  },
33
33
  "peerDependencies": {
34
34
  "@deepseek-ai/cordis": "^4.0.5"