@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
@@ -1,120 +1,97 @@
1
- /** Startup replay for non-terminal commit journals. */
1
+ /** Startup replay for non-terminal commit journals: report only, never write skills. */
2
2
  import { join } from 'node:path';
3
- import { recoveryFor } from "./domain/commit-journal.js";
4
3
  import { writeJournal } from "./domain/commit-journal-store.js";
5
- import { readCommitSnapshot, skillBodyPath, skillFilePath } from "./commit-snapshot.js";
4
+ import { snapshotPath } from "./commit-snapshot.js";
6
5
  import { readCommitReceipt } from "./commit-receipt.js";
7
- export async function recoverCommitJournals(io, home, now, skillRoot, restoreSource, isActive) {
6
+ import { readUnitEvidence } from '@lmzhen/dsh-evolution-core';
7
+ export async function recoverCommitJournals(io, home, now, isActive) {
8
8
  const dir = join(home, 'merge', 'journals');
9
- if (!(await io.exists(dir)))
10
- return [];
11
- const files = (await io.list(dir)).filter(name => name.endsWith('.json'));
12
9
  const reports = [];
13
- for (const file of files) {
14
- let journal;
15
- try {
16
- journal = JSON.parse(await io.readText(join(dir, file)) ?? '');
17
- }
18
- catch {
19
- continue;
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
- }
25
- const recovery = recoveryFor(journal);
26
- if (recovery === 'none')
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;
10
+ let files = [];
11
+ if (await io.exists(dir)) {
33
12
  try {
34
- receipt = await readCommitReceipt(io, home, journal.runId, journal.target);
13
+ files = (await io.list(dir)).filter(name => name.endsWith('.json'));
35
14
  }
36
15
  catch {
37
- reports.push({ runId: journal.runId, phase: journal.phase, action: 'receipt-read-failed', ok: false });
38
- continue;
16
+ reports.push({ runId: '(list)', phase: 'unknown', action: 'journal-list-failed', ok: false, journalPath: dir });
39
17
  }
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
- }
45
- let snapshot;
18
+ }
19
+ for (const file of files) {
20
+ const path = join(dir, file);
46
21
  try {
47
- snapshot = await readCommitSnapshot(io, home, journal.runId);
22
+ let journal;
23
+ try {
24
+ journal = JSON.parse(await io.readText(path) ?? '');
25
+ }
26
+ catch {
27
+ reports.push({ runId: file.replace(/\.json$/, ''), phase: 'unknown', action: 'journal-unreadable', ok: false, journalPath: path });
28
+ continue;
29
+ }
30
+ if (isActive?.(journal.runId) === true) {
31
+ reports.push({ runId: journal.runId, phase: journal.phase, action: 'skipped-active-run', ok: true, target: journal.target, sources: journal.sources, journalPath: path });
32
+ continue;
33
+ }
34
+ let receipt = null;
35
+ try {
36
+ receipt = await readCommitReceipt(io, home, journal.runId, journal.target);
37
+ }
38
+ catch {
39
+ reports.push({ runId: journal.runId, phase: journal.phase, action: 'receipt-read-failed', ok: false, target: journal.target, sources: journal.sources, journalPath: path });
40
+ continue;
41
+ }
42
+ if (receipt !== null) {
43
+ await writeJournal(io, home, { ...journal, phase: 'committed', unitId: receipt.unitId, updatedAt: new Date(now).toISOString() });
44
+ reports.push({ runId: journal.runId, phase: journal.phase, action: 'receipt-committed', ok: true, target: journal.target, sources: journal.sources, journalPath: path, unitId: receipt.unitId });
45
+ continue;
46
+ }
47
+ if (journal.phase === 'committed') {
48
+ reports.push({ runId: journal.runId, phase: journal.phase, action: 'receipt-missing', ok: false, target: journal.target, sources: journal.sources, journalPath: path });
49
+ continue;
50
+ }
51
+ if (journal.phase === 'failed' || journal.phase === 'rolled-back')
52
+ continue;
53
+ reports.push({
54
+ runId: journal.runId, phase: journal.phase, action: 'manual-required', ok: false,
55
+ target: journal.target, sources: journal.sources, journalPath: path, snapshotPath: snapshotPath(home, journal.runId),
56
+ });
48
57
  }
49
58
  catch (error) {
50
- reports.push({ runId: journal.runId, phase: journal.phase, action: 'snapshot-read-failed: ' + (error instanceof Error ? error.message : String(error)), ok: false });
51
- continue;
52
- }
53
- let action = 'report-only';
54
- let ok = true;
55
- if (recovery === 'retry-commit') {
56
- action = 'no-host-write';
59
+ reports.push({ runId: file.replace(/\.json$/, ''), phase: 'unknown', action: 'recovery-error: ' + (error instanceof Error ? error.message : String(error)), ok: false, journalPath: path });
57
60
  }
58
- else if (snapshot === null) {
59
- action = 'snapshot-missing';
60
- ok = false;
61
+ }
62
+ const unitDir = join(home, 'merge', 'unit-journals');
63
+ if (await io.exists(unitDir)) {
64
+ let unitFiles = [];
65
+ try {
66
+ unitFiles = (await io.list(unitDir)).filter(name => name.endsWith('.json'));
61
67
  }
62
- else {
68
+ catch { /* no unit journals */ }
69
+ for (const file of unitFiles) {
70
+ const path = join(unitDir, file);
63
71
  try {
64
- if (recovery === 'rollback-sources') {
65
- let restoredArchived = restoreSource !== undefined;
66
- if (restoreSource !== undefined) {
67
- for (const source of snapshot.sourceBodies) {
68
- if (!(await restoreSource(source.name)))
69
- restoredArchived = false;
70
- }
71
- }
72
- if (!restoredArchived) {
73
- for (const source of snapshot.sourceBodies)
74
- await io.writeText(skillBodyPath(skillRoot, source.name), source.body);
75
- }
76
- }
77
- if (snapshot.create && snapshot.targetBody === null) {
78
- try {
79
- await io.remove(skillBodyPath(skillRoot, snapshot.target));
80
- }
81
- catch { /* already gone */ }
82
- try {
83
- const dir = join(skillRoot, snapshot.target);
84
- if ((await io.list(dir)).length === 0)
85
- await io.remove(dir);
86
- }
87
- catch { /* absent or non-empty */ }
72
+ const journal = JSON.parse(await io.readText(path) ?? '');
73
+ const evidence = await readUnitEvidence(io, home, journal.unitId);
74
+ if (evidence !== null) {
75
+ reports.push({ runId: journal.runId, phase: 'committed', action: 'unit-committed', ok: true, target: journal.target, sources: journal.sources, journalPath: path, unitId: journal.unitId });
76
+ continue;
88
77
  }
89
- else if (snapshot.targetBody !== null) {
90
- await io.writeText(skillBodyPath(skillRoot, snapshot.target), snapshot.targetBody);
78
+ if (journal.phase === 'committed') {
79
+ reports.push({ runId: journal.runId, phase: 'committed', action: 'unit-evidence-missing', ok: false, target: journal.target, sources: journal.sources, journalPath: path, unitId: journal.unitId });
80
+ continue;
91
81
  }
92
- const oldPaths = new Set((snapshot.targetSupport ?? []).map(file => file.path));
93
- for (const file of snapshot.targetSupport ?? []) {
94
- await io.writeText(skillFilePath(skillRoot, snapshot.target, file.path), file.content);
95
- }
96
- for (const file of snapshot.targetSupportAfter ?? []) {
97
- if (!oldPaths.has(file.path)) {
98
- try {
99
- await io.remove(skillFilePath(skillRoot, snapshot.target, file.path));
100
- }
101
- catch { /* already gone */ }
102
- }
103
- }
104
- for (const file of snapshot.sourceSupport ?? [])
105
- await io.writeText(skillFilePath(skillRoot, file.name, file.path), file.content);
106
- action = recovery === 'rollback-sources' ? 'restored-sources-and-target' : 'restored-target';
82
+ if (journal.phase === 'failed' || journal.phase === 'rolled-back')
83
+ continue;
84
+ reports.push({ runId: journal.runId, phase: journal.phase, action: 'unit-manual-required', ok: false, target: journal.target, sources: journal.sources, journalPath: path, unitId: journal.unitId });
107
85
  }
108
- catch (error) {
109
- action = 'restore-failed: ' + (error instanceof Error ? error.message : String(error));
110
- ok = false;
86
+ catch {
87
+ reports.push({ runId: '(unit)', phase: 'unknown', action: 'unit-journal-unreadable', ok: false, journalPath: path });
111
88
  }
112
89
  }
113
- if (ok)
114
- await writeJournal(io, home, { ...journal, phase: 'rolled-back', updatedAt: new Date(now).toISOString() });
115
- reports.push({ runId: journal.runId, phase: journal.phase, action, ok });
116
90
  }
117
- await io.writeText(join(home, 'merge', 'recovery.json'), JSON.stringify(reports, null, 2));
91
+ try {
92
+ await io.writeText(join(home, 'merge', 'recovery.json'), JSON.stringify(reports, null, 2));
93
+ }
94
+ catch { /* report file is best-effort; reports are still returned */ }
118
95
  return reports;
119
96
  }
120
97
  //# sourceMappingURL=commit-recovery.js.map
@@ -4,7 +4,7 @@
4
4
  * The data layer keeps four states; the user face collapses verify/fuse into one retry
5
5
  * queue and keeps the stage as a chip. The API queue must never expose the raw state.
6
6
  */
7
- export declare const MERGE_STATE_VERSION = 2;
7
+ export declare const MERGE_STATE_VERSION = 3;
8
8
  export type MergePairState = 'verify' | 'pending' | 'approved' | 'fuse' | 'skipped';
9
9
  export type MergeQueue = 'verify' | 'pending' | 'fuse' | 'skipped';
10
10
  export type MergeStage = 'verify' | 'decide' | 'author' | 'commit';
@@ -4,7 +4,7 @@
4
4
  * The data layer keeps four states; the user face collapses verify/fuse into one retry
5
5
  * queue and keeps the stage as a chip. The API queue must never expose the raw state.
6
6
  */
7
- export const MERGE_STATE_VERSION = 2;
7
+ export const MERGE_STATE_VERSION = 3;
8
8
  export const MERGE_PAIR_STATES = ['verify', 'pending', 'approved', 'fuse', 'skipped'];
9
9
  export const MERGE_QUEUES = ['verify', 'pending', 'fuse', 'skipped'];
10
10
  /** User-facing queue of one data state. verify/fuse share retry, not two tabs. */
@@ -2,7 +2,7 @@
2
2
  * Pure merge transition table. No io, no run shell, no subagents.
3
3
  *
4
4
  * Every transition returns the whole next row (or a drop), so the caller can persist it
5
- * through mutateMergeState in one transaction. Invalid transitions are refused by name;
5
+ * through one state transaction. Invalid transitions are refused by name;
6
6
  * a caller never "repairs" a row by guessing which state the operator meant.
7
7
  */
8
8
  import { type MergeDecision, type MergeFailure, type MergeRow } from './contracts.ts';
@@ -72,5 +72,6 @@ export type TransitionResult = {
72
72
  readonly code: 'invalid-transition';
73
73
  readonly message: string;
74
74
  };
75
- export declare function transition(current: MergeRow | undefined, event: MergeEvent): TransitionResult;
75
+ /** Legacy v2 projection only; runtime writes must use transitionV3/RowTx. */
76
+ export declare function transitionLegacyForProjection(current: MergeRow | undefined, event: MergeEvent): TransitionResult;
76
77
  //# sourceMappingURL=coordinator.d.ts.map
@@ -2,7 +2,7 @@
2
2
  * Pure merge transition table. No io, no run shell, no subagents.
3
3
  *
4
4
  * Every transition returns the whole next row (or a drop), so the caller can persist it
5
- * through mutateMergeState in one transaction. Invalid transitions are refused by name;
5
+ * through one state transaction. Invalid transitions are refused by name;
6
6
  * a caller never "repairs" a row by guessing which state the operator meant.
7
7
  */
8
8
  import { pairKey, queueOf, rowIdOf, } from "./contracts.js";
@@ -49,7 +49,8 @@ function next(row, at, state, patch = {}) {
49
49
  },
50
50
  };
51
51
  }
52
- export function transition(current, event) {
52
+ /** Legacy v2 projection only; runtime writes must use transitionV3/RowTx. */
53
+ export function transitionLegacyForProjection(current, event) {
53
54
  if (event.type === 'candidate') {
54
55
  if (current !== undefined)
55
56
  return { ok: true, row: current };
@@ -106,7 +107,7 @@ export function transition(current, event) {
106
107
  : invalid(event, current.state);
107
108
  case 'approve':
108
109
  return current.state === 'pending'
109
- ? next(current, event.at, 'approved', { ...(event.runId === undefined ? {} : { runId: event.runId }) })
110
+ ? next(current, event.at, 'approved', (event.runId === undefined ? {} : { runId: event.runId }))
110
111
  : invalid(event, current.state);
111
112
  case 'reject':
112
113
  return current.state === 'pending' ? next(current, event.at, 'skipped', { origin: 'human' }) : invalid(event, current.state);
@@ -1,5 +1,5 @@
1
1
  function extractJson(text) {
2
- const fenced = /\`\`\`(?:json)?\s*([\s\S]*?)\`\`\`/i.exec(text);
2
+ const fenced = /`\``(?:json)?\s*([\s\S]*?)`\``/i.exec(text);
3
3
  const body = fenced?.[1] ?? text;
4
4
  const start = body.indexOf('{');
5
5
  if (start < 0)
@@ -6,5 +6,12 @@ export interface MergeDoctorRow {
6
6
  readonly status: 'ok' | 'warn' | 'unknown';
7
7
  readonly detail: string;
8
8
  }
9
+ /** Global doctor host section: a missing merge mount must fail with E-325, not render empty. */
10
+ export declare function mergeDoctorAvailability(mounted: boolean): {
11
+ readonly ok: true;
12
+ } | {
13
+ readonly ok: false;
14
+ readonly code: 'E-325';
15
+ };
9
16
  export declare function mergeDoctor(io: EvolutionIoLike, home: string, configuredRoot?: string): Promise<readonly MergeDoctorRow[]>;
10
17
  //# sourceMappingURL=doctor.d.ts.map
@@ -4,11 +4,19 @@
4
4
  * operator can act on. This module never repairs or writes.
5
5
  */
6
6
  import { join } from 'node:path';
7
- import { probeList } from '@lmzhen/dsh-evolution-core';
7
+ import { probeList, readTreeLock, runsFile } from '@lmzhen/dsh-evolution-core';
8
+ import { v2BackupPath } from "./domain/state-v3.js";
8
9
  import { loadMergeState } from "./state.js";
9
10
  import { countRows } from "./status.js";
10
11
  import { stagingRoot } from "./staging.js";
12
+ import { validateArtifacts } from "./domain/consistency.js";
13
+ import { readJournal } from "./domain/commit-journal-store.js";
14
+ import { loadStateSchema } from "./state.js";
11
15
  export const MERGE_DOCTOR_SCHEMA_VERSION = 3;
16
+ /** Global doctor host section: a missing merge mount must fail with E-325, not render empty. */
17
+ export function mergeDoctorAvailability(mounted) {
18
+ return mounted ? { ok: true } : { ok: false, code: 'E-325' };
19
+ }
12
20
  export async function mergeDoctor(io, home, configuredRoot) {
13
21
  const rows = [];
14
22
  const loaded = await loadMergeState(io, home);
@@ -19,9 +27,83 @@ export async function mergeDoctor(io, home, configuredRoot) {
19
27
  status: loaded.note === undefined ? 'ok' : 'warn',
20
28
  detail: loaded.note ?? 'pairs ' + String(loaded.state.rows.length) + ';verify ' + String(counts.verify) + ' / pending ' + String(counts.pending) + ' / fuse ' + String(counts.fuse) + ' / skipped ' + String(counts.skipped),
21
29
  });
22
- const modelRoot = configuredRoot !== undefined && configuredRoot !== '' ? configuredRoot : process.env.SKILL_MERGE_MODELS_ROOT;
23
- if (modelRoot === undefined || modelRoot === '') {
24
- rows.push({ key: 'models', label: '合并模型', status: 'warn', detail: '未设置 SKILL_MERGE_MODELS_ROOT;合并扫描会 fail loud' });
30
+ const recoveryRuns = new Set();
31
+ try {
32
+ const raw = await io.readText(join(home, 'merge', 'recovery.json'));
33
+ if (raw !== null)
34
+ for (const report of JSON.parse(raw))
35
+ if (report.ok === true && report.runId !== undefined)
36
+ recoveryRuns.add(report.runId);
37
+ }
38
+ catch { }
39
+ const receiptRuns = new Set();
40
+ const receiptDir = await probeList(io, join(home, 'merge', 'receipts'));
41
+ if (receiptDir.kind === 'present')
42
+ for (const name of receiptDir.value)
43
+ receiptRuns.add(name);
44
+ const invalid = [];
45
+ for (const row of loaded.state.rows) {
46
+ const run = row.state === 'pending' && row.runId !== undefined ? 'awaiting-approval' : row.runId === undefined ? 'none' : 'running';
47
+ const staging = row.stagingRef !== undefined ? 'kept' : row.state === 'fuse' ? 'missing' : 'none';
48
+ const phase = row.runId === undefined ? undefined : (await readJournal(io, home, row.runId))?.phase;
49
+ const journal = phase === undefined || phase === null ? 'none'
50
+ : phase === 'committed' ? 'committed'
51
+ : phase === 'prepared' ? 'prepared'
52
+ : phase === 'rolled-back' ? 'rolled-back'
53
+ : phase === 'failed' ? 'failed'
54
+ : 'committing';
55
+ const evidence = row.runId !== undefined && receiptRuns.has(row.runId) ? 'receipt' : 'none';
56
+ const recovery = row.runId !== undefined && recoveryRuns.has(row.runId);
57
+ if (!validateArtifacts({ row: row.state, run, staging, journal, evidence, activeRuns: run === 'running' || run === 'awaiting-approval' ? 1 : 0, recovery }).ok)
58
+ invalid.push(row);
59
+ }
60
+ const schema = await loadStateSchema(io, home);
61
+ rows.push({ key: 'merge-state-v3', label: 'state schema', status: schema === 3 ? 'ok' : schema === 2 ? 'warn' : 'unknown', detail: schema === 3 ? 'schemaVersion 3' : schema === 2 ? 'schemaVersion 2;等待 v3 迁移' : 'schemaVersion 未知/不可读' });
62
+ let quarantineCount = 0;
63
+ try {
64
+ const qraw = await io.readText(join(home, 'merge', 'merge-state.quarantine.json'));
65
+ if (qraw !== null) {
66
+ const parsed = JSON.parse(qraw);
67
+ quarantineCount = Array.isArray(parsed) ? parsed.length : 0;
68
+ }
69
+ }
70
+ catch {
71
+ quarantineCount = -1;
72
+ }
73
+ rows.push({ key: 'quarantine', label: 'quarantine', status: quarantineCount < 0 ? 'unknown' : quarantineCount === 0 ? 'ok' : 'warn', detail: quarantineCount < 0 ? 'quarantine 不可读' : quarantineCount + ' 行待人工检查' });
74
+ const holder = await readTreeLock(io, home).catch(() => null);
75
+ rows.push({ key: 'tree-lock', label: 'tree lock', status: holder == null ? 'ok' : 'warn', detail: holder == null ? 'free' : 'holder pid=' + String(holder.pid) + ' epoch=' + String(holder.epoch) + ' token=' + holder.token.slice(0, 8) });
76
+ rows.push({ key: 'backup', label: 'v2 backup', status: await io.exists(v2BackupPath(home)) ? 'ok' : 'unknown', detail: await io.exists(v2BackupPath(home)) ? 'merge-state.v2.bak.json' : 'no v2 backup (not migrated or already cleaned)' });
77
+ const claims = await probeList(io, join(home, 'merge', 'claims'));
78
+ rows.push({ key: 'target-claim', label: 'target claim', status: claims.kind === 'unknown' ? 'unknown' : 'ok', detail: claims.kind === 'unknown' ? 'claims unreadable' : claims.kind === 'absent' ? 'no claims' : String(claims.value.length) + ' claim files' });
79
+ let orphanRuns = 0;
80
+ let runCount = 0;
81
+ try {
82
+ const raw = await io.readText(runsFile(home));
83
+ if (raw !== null) {
84
+ const parsed = JSON.parse(raw);
85
+ runCount = parsed.runs?.length ?? 0;
86
+ for (const run of parsed.runs ?? []) {
87
+ if (run.state !== 'running' || typeof run.pid !== 'number')
88
+ continue;
89
+ try {
90
+ process.kill(run.pid, 0);
91
+ }
92
+ catch {
93
+ orphanRuns += 1;
94
+ }
95
+ }
96
+ }
97
+ }
98
+ catch {
99
+ orphanRuns = -1;
100
+ }
101
+ rows.push({ key: 'runs', label: 'run owner', status: orphanRuns < 0 ? 'unknown' : orphanRuns === 0 ? 'ok' : 'warn', detail: orphanRuns < 0 ? 'runs.json unreadable' : String(runCount) + ' runs; ' + String(orphanRuns) + ' dead-owner running' });
102
+ rows.push({ key: 'install', label: 'install form', status: io.exists === undefined ? 'unknown' : 'ok', detail: 'merge plugin loaded' });
103
+ rows.push({ key: 'consistency', label: '一致性', status: invalid.length === 0 ? 'ok' : 'warn', detail: invalid.length === 0 ? '无非法组合' : String(invalid.length) + ' 行需人工检查: ' + invalid.map(row => row.id).join(', ') });
104
+ const modelRoot = configuredRoot?.trim() ?? '';
105
+ if (modelRoot === '') {
106
+ rows.push({ key: 'models', label: '合并模型', status: 'warn', detail: '未配置 mergeModelsRoot;合并扫描会 fail loud' });
25
107
  }
26
108
  else {
27
109
  const int8 = join(modelRoot, 'potion-multilingual-128M-int8');
@@ -45,6 +127,24 @@ export async function mergeDoctor(io, home, configuredRoot) {
45
127
  status: 'ok',
46
128
  detail: stagingRoot(home, '<runId>'),
47
129
  });
130
+ try {
131
+ const raw = await io.readText(join(home, 'merge', 'recovery.json'));
132
+ if (raw !== null) {
133
+ const reports = JSON.parse(raw);
134
+ const failed = reports.filter(report => report.ok !== true);
135
+ rows.push({
136
+ key: 'recovery',
137
+ label: '提交恢复',
138
+ status: failed.length === 0 ? 'ok' : 'warn',
139
+ detail: failed.length === 0
140
+ ? String(reports.length) + ' 条恢复报告,无待处理'
141
+ : String(failed.length) + ' 条待处理:' + failed.map(report => (report.runId ?? '?') + '=' + (report.action ?? '?')).join(', '),
142
+ });
143
+ }
144
+ }
145
+ catch {
146
+ rows.push({ key: 'recovery', label: '提交恢复', status: 'unknown', detail: '无法读取 merge/recovery.json' });
147
+ }
48
148
  return rows;
49
149
  }
50
150
  //# sourceMappingURL=doctor.js.map
@@ -3,8 +3,11 @@ export interface Claim {
3
3
  readonly id: string;
4
4
  readonly token: string;
5
5
  readonly expiresAt: number;
6
+ readonly pid?: number;
7
+ /** Process start time; detects pid reuse when the same pid hosts a newer process. */
8
+ readonly startedAt?: number;
6
9
  }
7
- export declare function acquireClaim(current: Claim | undefined, now: number, leaseMs: number, token: string): Claim | undefined;
10
+ export declare function acquireClaim(current: Claim | undefined, now: number, leaseMs: number, token: string, pid?: number, startedAt?: number): Claim | undefined;
8
11
  export declare function releaseClaim(current: Claim | undefined, token: string): Claim | undefined;
9
12
  export declare function isClaimExpired(current: Claim | undefined, now: number): boolean;
10
13
  //# sourceMappingURL=claim.d.ts.map
@@ -1,7 +1,7 @@
1
- export function acquireClaim(current, now, leaseMs, token) {
1
+ export function acquireClaim(current, now, leaseMs, token, pid, startedAt) {
2
2
  if (current !== undefined && current.expiresAt > now)
3
3
  return undefined;
4
- return { id: 'claim', token, expiresAt: now + Math.max(1, leaseMs) };
4
+ return { id: 'claim', token, expiresAt: now + Math.max(1, leaseMs), ...(pid === undefined ? {} : { pid }), ...(startedAt === undefined ? {} : { startedAt }) };
5
5
  }
6
6
  export function releaseClaim(current, token) {
7
7
  return current?.token === token ? undefined : current;
@@ -5,7 +5,11 @@ export function journalPath(home, runId) {
5
5
  }
6
6
  export async function writeJournal(io, home, journal) {
7
7
  const path = journalPath(home, journal.runId);
8
- await io.writeText(path, JSON.stringify(journal, null, 2));
8
+ const content = JSON.stringify(journal, null, 2);
9
+ if (io.transact !== undefined)
10
+ await io.transact(path, () => content);
11
+ else
12
+ await io.writeText(path, content);
9
13
  return path;
10
14
  }
11
15
  export async function readJournal(io, home, runId) {
@@ -1,5 +1,5 @@
1
1
  const NEXT = {
2
- prepared: ['target-written', 'failed'],
2
+ prepared: ['committed', 'target-written', 'failed'],
3
3
  'target-written': ['sources-archived', 'rolled-back', 'failed'],
4
4
  'sources-archived': ['committed', 'rolled-back', 'failed'],
5
5
  committed: [],
@@ -0,0 +1,22 @@
1
+ /** RG0 cross-artifact consistency. Pure; no IO. */
2
+ export type RowState = 'verify' | 'pending' | 'approved' | 'fuse' | 'skipped' | 'merged';
3
+ export type RunState = 'none' | 'awaiting-approval' | 'running' | 'succeeded' | 'failed' | 'cancelled' | 'orphan';
4
+ export type StagingState = 'none' | 'cleaned' | 'kept' | 'missing';
5
+ export type JournalState = 'none' | 'committing' | 'prepared' | 'committed' | 'rolled-back' | 'failed';
6
+ export type EvidenceState = 'none' | 'receipt';
7
+ export interface ArtifactState {
8
+ readonly row: RowState;
9
+ readonly run: RunState;
10
+ readonly staging: StagingState;
11
+ readonly journal: JournalState;
12
+ readonly evidence: EvidenceState;
13
+ readonly activeRuns: number;
14
+ readonly hasOwner?: boolean;
15
+ readonly recovery?: boolean;
16
+ }
17
+ export interface ConsistencyResult {
18
+ readonly ok: boolean;
19
+ readonly reason?: string;
20
+ }
21
+ export declare function validateArtifacts(state: ArtifactState): ConsistencyResult;
22
+ //# sourceMappingURL=consistency.d.ts.map
@@ -0,0 +1,50 @@
1
+ const ALLOWED = [
2
+ { row: 'verify', run: 'none', staging: 'none', journal: 'none', evidence: 'none', activeRuns: 0 },
3
+ { row: 'verify', run: 'running', staging: 'none', journal: 'none', evidence: 'none', activeRuns: 1 },
4
+ { row: 'verify', run: 'failed', staging: 'none', journal: 'none', evidence: 'none', activeRuns: 0 },
5
+ { row: 'pending', run: 'none', staging: 'none', journal: 'none', evidence: 'none', activeRuns: 0 },
6
+ { row: 'pending', run: 'awaiting-approval', staging: 'none', journal: 'none', evidence: 'none', activeRuns: 1 },
7
+ { row: 'approved', run: 'none', staging: 'none', journal: 'none', evidence: 'none', activeRuns: 0 },
8
+ { row: 'approved', run: 'running', staging: 'none', journal: 'none', evidence: 'none', activeRuns: 1 },
9
+ { row: 'fuse', run: 'none', staging: 'kept', journal: 'none', evidence: 'none', activeRuns: 0 },
10
+ { row: 'fuse', run: 'running', staging: 'kept', journal: 'none', evidence: 'none', activeRuns: 1 },
11
+ { row: 'fuse', run: 'failed', staging: 'kept', journal: 'none', evidence: 'none', activeRuns: 0 },
12
+ { row: 'fuse', run: 'orphan', staging: 'kept', journal: 'none', evidence: 'none', activeRuns: 0 },
13
+ { row: 'fuse', run: 'none', staging: 'none', journal: 'none', evidence: 'none', activeRuns: 0, recovery: true },
14
+ { row: 'fuse', run: 'failed', staging: 'none', journal: 'none', evidence: 'none', activeRuns: 0, recovery: true },
15
+ { row: 'fuse', run: 'orphan', staging: 'none', journal: 'none', evidence: 'none', activeRuns: 0, recovery: true },
16
+ { row: 'fuse', run: 'running', staging: 'none', journal: 'none', evidence: 'none', activeRuns: 1, recovery: true },
17
+ { row: 'fuse', run: 'none', staging: 'missing', journal: 'none', evidence: 'none', activeRuns: 0, recovery: true },
18
+ { row: 'fuse', run: 'failed', staging: 'missing', journal: 'none', evidence: 'none', activeRuns: 0, recovery: true },
19
+ { row: 'fuse', run: 'orphan', staging: 'missing', journal: 'none', evidence: 'none', activeRuns: 0, recovery: true },
20
+ { row: 'fuse', run: 'running', staging: 'missing', journal: 'none', evidence: 'none', activeRuns: 1, recovery: true },
21
+ { row: 'fuse', run: 'running', staging: 'kept', journal: 'committing', evidence: 'none', activeRuns: 1 },
22
+ { row: 'skipped', run: 'none', staging: 'none', journal: 'none', evidence: 'none', activeRuns: 0 },
23
+ { row: 'merged', run: 'succeeded', staging: 'cleaned', journal: 'committed', evidence: 'receipt', activeRuns: 0 },
24
+ { row: 'merged', run: 'succeeded', staging: 'kept', journal: 'committed', evidence: 'receipt', activeRuns: 0 },
25
+ ];
26
+ function same(a, b) {
27
+ return a.row === b.row && a.run === b.run && a.staging === b.staging && a.journal === b.journal
28
+ && a.evidence === b.evidence && a.activeRuns === b.activeRuns
29
+ && (b.recovery ?? false) === (a.recovery ?? false);
30
+ }
31
+ export function validateArtifacts(state) {
32
+ if (state.activeRuns > 1)
33
+ return { ok: false, reason: 'multiple-active-runs' };
34
+ if (state.activeRuns > 0 && state.hasOwner === false)
35
+ return { ok: false, reason: 'active-run-no-owner' };
36
+ if (ALLOWED.some(item => same(state, item)))
37
+ return { ok: true };
38
+ if (state.row === 'pending' && state.staging !== 'none')
39
+ return { ok: false, reason: 'pending-staging' };
40
+ if (state.row === 'skipped' && state.run === 'running')
41
+ return { ok: false, reason: 'skipped-running' };
42
+ if (state.row === 'fuse' && state.staging === 'missing' && state.recovery !== true)
43
+ return { ok: false, reason: 'fuse-lost-staging' };
44
+ if (state.journal === 'committed' && state.evidence !== 'receipt')
45
+ return { ok: false, reason: 'committed-no-evidence' };
46
+ if (state.row === 'merged' && state.journal === 'rolled-back')
47
+ return { ok: false, reason: 'committed-rollback' };
48
+ return { ok: false, reason: 'not-allowed' };
49
+ }
50
+ //# sourceMappingURL=consistency.js.map
@@ -1,5 +1,7 @@
1
1
  /** Pure v2 ledger normalization/projection. No IO. */
2
2
  import { migrateV1Row } from "./migrate-v1.js";
3
+ import { isV3Row } from "./row-v3.js";
4
+ import { v3ToLegacy } from "./row-v3-bridge.js";
3
5
  import { queueOf } from "./state.js";
4
6
  function isRow(value) {
5
7
  const row = value;
@@ -17,11 +19,18 @@ export function parseLedger(raw) {
17
19
  return { ok: false, ledger: { schemaVersion: 2, rows: [] }, note: 'invalid json' };
18
20
  }
19
21
  const body = parsed;
20
- if (typeof body.schemaVersion === 'number' && body.schemaVersion > 2) {
22
+ if (typeof body.schemaVersion === 'number' && body.schemaVersion > 3) {
21
23
  return { ok: false, ledger: { schemaVersion: 2, rows: [] }, note: 'merge-state-forward-only' };
22
24
  }
23
25
  if (!Array.isArray(body.rows))
24
26
  return { ok: false, ledger: { schemaVersion: 2, rows: [] }, note: 'rows missing' };
27
+ if (body.schemaVersion === 3) {
28
+ if (!body.rows.every(isV3Row)) {
29
+ const v3Like = body.rows.some(row => row !== null && typeof row === 'object' && 'pairKey' in row && 'version' in row);
30
+ return { ok: false, ledger: { schemaVersion: 2, rows: [] }, note: v3Like ? 'merge-state-needs-repair' : 'merge-state-forward-only' };
31
+ }
32
+ return { ok: true, ledger: { schemaVersion: 2, rows: body.rows.map(v3ToLegacy) } };
33
+ }
25
34
  const rows = [];
26
35
  let skipped = 0;
27
36
  for (const value of body.rows) {
@@ -0,0 +1,20 @@
1
+ import type { V3Row } from './row-v3.ts';
2
+ export interface V3State {
3
+ readonly schemaVersion: 3;
4
+ readonly rows: readonly V3Row[];
5
+ }
6
+ export interface QuarantinedRow {
7
+ readonly reason: string;
8
+ readonly raw: unknown;
9
+ }
10
+ export type V3Migration = {
11
+ readonly ok: true;
12
+ readonly state: V3State;
13
+ readonly quarantine: readonly QuarantinedRow[];
14
+ } | {
15
+ readonly ok: false;
16
+ readonly code: 'state-migration-blocked';
17
+ readonly message: string;
18
+ };
19
+ export declare function migrateV2ToV3(raw: unknown): V3Migration;
20
+ //# sourceMappingURL=migrate-v3.d.ts.map