@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,31 +1,67 @@
1
+ import { readTextOrNull } from "./domain/read-probe.js";
1
2
  /**
2
3
  * Merge doctor rows: state, pending, gate cache and staging are each probed read-only.
3
4
  * A missing file is an empty feature, not a failure; an unreadable file is a warning the
4
5
  * operator can act on. This module never repairs or writes.
5
6
  */
7
+ import { createHash } from 'node:crypto';
8
+ import { stat } from 'node:fs/promises';
6
9
  import { join } from 'node:path';
7
10
  import { probeList, readTreeLock, runsFile } from '@lmzhen/dsh-evolution-core';
8
- import { v2BackupPath } from "./domain/state-v3.js";
9
- import { loadMergeState } from "./state.js";
10
- import { countRows } from "./status.js";
11
- import { stagingRoot } from "./staging.js";
11
+ import { migrationJournalPath, migrationReportPath, v2BackupPath } from "./domain/state-bootstrap.js";
12
+ import { readMergeSnapshot } from "./domain/merge-snapshot.js";
12
13
  import { validateArtifacts } from "./domain/consistency.js";
13
14
  import { readJournal } from "./domain/commit-journal-store.js";
14
- import { loadStateSchema } from "./state.js";
15
+ import { stagingRoot } from "./staging.js";
16
+ import { readReconcileReport } from "./domain/reconcile.js";
15
17
  export const MERGE_DOCTOR_SCHEMA_VERSION = 3;
18
+ /** Compose the one-line global-doctor fact from buildDoctorView + the last reconcile report. */
19
+ export function mergeDoctorFact(view, report) {
20
+ const counts = view.counts;
21
+ const reconcileText = report === null
22
+ ? 'reconcile 尚未运行'
23
+ : report.state === 'blocked'
24
+ ? 'reconcile blocked ' + (report.code ?? 'merge-state-blocked')
25
+ : 'reconcile ' + String(report.actions.length) + ' 项动作';
26
+ const problems = [];
27
+ if (view.blocked !== undefined)
28
+ problems.push('state blocked ' + view.blocked.code);
29
+ if (view.hostOnly !== undefined)
30
+ problems.push('宿主数据面不可用 ' + view.hostOnly.code);
31
+ if (view.staging.state === 'unreadable')
32
+ problems.push('staging 索引不可读' + (view.staging.detail === undefined ? '' : ':' + view.staging.detail));
33
+ if (counts.expired > 0)
34
+ problems.push('expired ' + String(counts.expired));
35
+ const unstable = view.rows.filter(row => row.status === 'warn' || row.status === 'fail');
36
+ if (unstable.length > 0)
37
+ problems.push(unstable.map(row => row.key + '=' + row.status).join(','));
38
+ if (report?.state === 'blocked')
39
+ problems.push('reconcile blocked');
40
+ const latest = view.latestRun;
41
+ const latestText = latest === undefined ? '' : ';最近 run ' + latest.id + ' [' + latest.state + ']';
42
+ const detail = 'verify ' + String(counts.active.verify) + ' / pending ' + String(counts.active.pending) + ' / fuse ' + String(counts.active.fuse) + ' / skipped ' + String(counts.active.skipped) + ' / expired ' + String(counts.expired)
43
+ + ';staging ' + view.staging.state + ' dirs=' + String(view.staging.dirs)
44
+ + ';审批 ' + (view.approval.mergeApproval ? '开' : '关') + ';' + reconcileText + latestText
45
+ + (problems.length === 0 ? '' : ';问题:' + problems.join(';'));
46
+ const status = problems.length === 0
47
+ ? 'ok'
48
+ : view.blocked !== undefined || view.hostOnly !== undefined || report?.state === 'blocked' ? 'fail' : 'warn';
49
+ return problems.length === 0 ? { status, detail } : { status, detail, remedy: '运行 /evolution merge doctor 查看逐项;必要时 /evolution merge reconcile 收敛' };
50
+ }
16
51
  /** Global doctor host section: a missing merge mount must fail with E-325, not render empty. */
17
52
  export function mergeDoctorAvailability(mounted) {
18
53
  return mounted ? { ok: true } : { ok: false, code: 'E-325' };
19
54
  }
20
55
  export async function mergeDoctor(io, home, configuredRoot) {
21
56
  const rows = [];
22
- const loaded = await loadMergeState(io, home);
23
- const counts = countRows(loaded.state.rows);
57
+ const snapshot = await readMergeSnapshot({ io, home, runs: [], approval: false });
58
+ const hasState = await io.exists(join(home, 'merge', 'merge-state.json'));
59
+ const counts = snapshot.doctor.counts.active;
24
60
  rows.push({
25
61
  key: 'state',
26
62
  label: '合并状态',
27
- status: loaded.note === undefined ? 'ok' : 'warn',
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),
63
+ status: snapshot.blocked === undefined || !hasState ? 'ok' : 'warn',
64
+ detail: snapshot.blocked === undefined ? 'pairs ' + String(snapshot.rows.length) + ';verify ' + String(counts.verify) + ' / pending ' + String(counts.pending) + ' / fuse ' + String(counts.fuse) + ' / skipped ' + String(counts.skipped) : snapshot.blocked.code + ': ' + snapshot.blocked.message,
29
65
  });
30
66
  const recoveryRuns = new Set();
31
67
  try {
@@ -42,23 +78,22 @@ export async function mergeDoctor(io, home, configuredRoot) {
42
78
  for (const name of receiptDir.value)
43
79
  receiptRuns.add(name);
44
80
  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);
81
+ if (snapshot.blocked !== undefined)
82
+ invalid.push({ id: 'state', reason: snapshot.blocked.code });
83
+ for (const item of snapshot.rows) {
84
+ const row = item.row;
85
+ const runId = row.runId ?? (item.stage?.ref.split('/')[0]);
86
+ const artifactRow = row.state === 'approved' ? 'approved' : row.state === 'fuse' ? 'fuse' : row.state;
87
+ const run = runId === undefined ? 'none' : row.state === 'pending' ? 'awaiting-approval' : 'running';
88
+ const staging = item.stage === null ? (row.state === 'fuse' ? 'missing' : 'none') : item.stage.state === 'present' || item.stage.state === 'reserved' ? 'kept' : row.state === 'fuse' ? 'missing' : 'none';
89
+ const phase = runId === undefined ? null : await readJournal(io, home, runId);
90
+ const journal = phase === null ? 'none' : phase.phase === 'prepared' ? 'prepared' : phase.phase === 'target-written' || phase.phase === 'sources-archived' ? 'committing' : phase.phase === 'committed' ? 'committed' : phase.phase === 'rolled-back' ? 'rolled-back' : 'failed';
91
+ const evidence = runId !== undefined && await readTextOrNull(io, join(home, 'merge', 'receipts', runId + '.json')) !== null ? 'receipt' : 'none';
92
+ const result = validateArtifacts({ row: artifactRow, run, staging, journal, evidence, activeRuns: run === 'running' || run === 'awaiting-approval' ? 1 : 0, ...(runId !== undefined && recoveryRuns.has(runId) ? { recovery: true } : {}) });
93
+ if (!result.ok)
94
+ invalid.push({ id: row.id, reason: result.reason ?? 'not-allowed' });
59
95
  }
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 未知/不可读' });
96
+ rows.push({ key: 'merge-state-v3', label: 'state schema', status: snapshot.blocked === undefined ? 'ok' : 'warn', detail: snapshot.blocked === undefined ? 'schemaVersion 4' : snapshot.blocked.code });
62
97
  let quarantineCount = 0;
63
98
  try {
64
99
  const qraw = await io.readText(join(home, 'merge', 'merge-state.quarantine.json'));
@@ -73,7 +108,15 @@ export async function mergeDoctor(io, home, configuredRoot) {
73
108
  rows.push({ key: 'quarantine', label: 'quarantine', status: quarantineCount < 0 ? 'unknown' : quarantineCount === 0 ? 'ok' : 'warn', detail: quarantineCount < 0 ? 'quarantine 不可读' : quarantineCount + ' 行待人工检查' });
74
109
  const holder = await readTreeLock(io, home).catch(() => null);
75
110
  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)' });
111
+ const backupPath = v2BackupPath(home);
112
+ const backupRaw = await readTextOrNull(io, backupPath);
113
+ const backupMode = await stat(backupPath).then(value => value.mode).catch(() => null);
114
+ const backupReadOnly = backupMode === null ? null : (backupMode & 0o200) === 0;
115
+ rows.push({ key: 'backup', label: 'v2 backup', status: backupRaw === null ? 'unknown' : backupReadOnly === false ? 'warn' : 'ok', detail: backupRaw === null ? 'no v2 backup (not migrated or already cleaned)' : 'merge-state.v2.bak.json sha256=' + createHash('sha256').update(backupRaw).digest('hex').slice(0, 16) + ' readonly=' + String(backupReadOnly) });
116
+ const journal = await readTextOrNull(io, migrationJournalPath(home));
117
+ const report = await readTextOrNull(io, migrationReportPath(home));
118
+ rows.push({ key: 'migration', label: 'state migration', status: snapshot.blocked === undefined ? (journal === null ? 'ok' : 'warn') : 'warn', detail: snapshot.blocked === undefined ? (journal === null ? (report === null ? 'ready (no migration report)' : 'ready; report present') : 'ready; migration journal pending') : snapshot.blocked.code + ': ' + snapshot.blocked.message });
119
+ rows.push({ key: 'staging-index', label: 'staging index', status: 'ok', detail: String(snapshot.stages.length) + ' index entries' });
77
120
  const claims = await probeList(io, join(home, 'merge', 'claims'));
78
121
  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
122
  let orphanRuns = 0;
@@ -100,7 +143,18 @@ export async function mergeDoctor(io, home, configuredRoot) {
100
143
  }
101
144
  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
145
  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(', ') });
146
+ const reconcile = await readReconcileReport(io, home);
147
+ rows.push({
148
+ key: 'reconcile',
149
+ label: '状态收敛',
150
+ status: reconcile === null ? 'unknown' : reconcile.state === 'blocked' ? 'warn' : reconcile.actions.length > 0 ? 'warn' : 'ok',
151
+ detail: reconcile === null
152
+ ? '尚无 reconcile 报告(启动或 /evolution merge reconcile 会生成)'
153
+ : reconcile.state === 'blocked'
154
+ ? 'blocked:' + (reconcile.code ?? 'merge-state-blocked') + (reconcile.message === undefined ? '' : ' — ' + reconcile.message)
155
+ : String(reconcile.actions.length) + ' 项动作(' + new Date(reconcile.at).toISOString() + ')' + (reconcile.actions.length === 0 ? '' : ':' + reconcile.actions.map(action => action.rowId + '=' + action.code).join(', ')),
156
+ });
157
+ rows.push({ key: 'consistency', label: '一致性', status: snapshot.blocked !== undefined ? 'blocked' : invalid.length === 0 ? 'ok' : 'warn', detail: snapshot.blocked !== undefined ? 'blocked: ' + snapshot.blocked.code : invalid.length === 0 ? '无非法组合' : String(invalid.length) + ' 行需人工检查: ' + invalid.map(row => row.id + '=' + row.reason).join(', ') });
104
158
  const modelRoot = configuredRoot?.trim() ?? '';
105
159
  if (modelRoot === '') {
106
160
  rows.push({ key: 'models', label: '合并模型', status: 'warn', detail: '未配置 mergeModelsRoot;合并扫描会 fail loud' });
@@ -0,0 +1,26 @@
1
+ /**
2
+ * The round-2 JSON envelope a child writes after authoring: which files the merge left
3
+ * behind. Both authoring paths (same-child scan round2 and the new-child approve/resume
4
+ * path) parse it identically, so a declared-but-missing file fails the same way on both.
5
+ */
6
+ export interface AuthoringManifest {
7
+ readonly target?: string;
8
+ readonly files: readonly string[];
9
+ }
10
+ /**
11
+ * Parse `{"target":..., "files":[...]}` from a child's final turn text.
12
+ * @param text - the child's answer (JSON is expected; prose is tolerated).
13
+ * @returns the manifest, or null when the turn carried no well-formed envelope.
14
+ */
15
+ export declare function parseAuthoringManifest(text: string): AuthoringManifest | null;
16
+ /** Files a manifest promises for a staged tree (declared list, or the tree when undeclared). */
17
+ export declare function manifestFilesOf(manifest: AuthoringManifest | null, stagedPaths: readonly string[]): readonly string[];
18
+ /**
19
+ * The first file the manifest promises but the staged tree does not contain.
20
+ * SKILL.md itself is always required, whether or not the manifest lists it.
21
+ * @param files - manifest files (see {@link manifestFilesOf}).
22
+ * @param stagedPaths - support-file paths actually present in the staged tree.
23
+ * @returns the missing path, or null when the declaration is satisfied.
24
+ */
25
+ export declare function missingManifestFile(files: readonly string[], stagedPaths: readonly string[]): string | null;
26
+ //# sourceMappingURL=authoring-manifest.d.ts.map
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Parse `{"target":..., "files":[...]}` from a child's final turn text.
3
+ * @param text - the child's answer (JSON is expected; prose is tolerated).
4
+ * @returns the manifest, or null when the turn carried no well-formed envelope.
5
+ */
6
+ export function parseAuthoringManifest(text) {
7
+ try {
8
+ const value = JSON.parse(text);
9
+ if (!Array.isArray(value.files) || value.files.some(file => typeof file !== 'string'))
10
+ return null;
11
+ if (value.target !== undefined && typeof value.target !== 'string')
12
+ return null;
13
+ return { ...(typeof value.target === 'string' ? { target: value.target } : {}), files: value.files };
14
+ }
15
+ catch {
16
+ return null;
17
+ }
18
+ }
19
+ /** Files a manifest promises for a staged tree (declared list, or the tree when undeclared). */
20
+ export function manifestFilesOf(manifest, stagedPaths) {
21
+ if (manifest !== null)
22
+ return manifest.files;
23
+ return ['SKILL.md', ...stagedPaths];
24
+ }
25
+ /**
26
+ * The first file the manifest promises but the staged tree does not contain.
27
+ * SKILL.md itself is always required, whether or not the manifest lists it.
28
+ * @param files - manifest files (see {@link manifestFilesOf}).
29
+ * @param stagedPaths - support-file paths actually present in the staged tree.
30
+ * @returns the missing path, or null when the declaration is satisfied.
31
+ */
32
+ export function missingManifestFile(files, stagedPaths) {
33
+ if (!files.includes('SKILL.md'))
34
+ return 'SKILL.md';
35
+ for (const path of files) {
36
+ if (path === 'SKILL.md')
37
+ continue;
38
+ if (!stagedPaths.includes(path))
39
+ return path;
40
+ }
41
+ return null;
42
+ }
43
+ //# sourceMappingURL=authoring-manifest.js.map
@@ -0,0 +1,10 @@
1
+ /** Batch 3 engine criteria projection: merge maps level/verdict/action, engine does not. */
2
+ export interface EngineCriteria {
3
+ readonly decision: 'keep' | 'merge' | 'candidate';
4
+ readonly validation: 'not-run' | 'report-only' | 'required';
5
+ readonly commit: 'never' | 'if-approved' | 'required';
6
+ readonly archive: 'none' | 'sources';
7
+ readonly evidence: 'none' | 'receipt';
8
+ }
9
+ export declare function criteriaFrom(level: 'L0' | 'L1' | 'L2', verdict: 1 | 0 | -1, action: 'keep' | 'merge'): EngineCriteria;
10
+ //# sourceMappingURL=engine-criteria.d.ts.map
@@ -0,0 +1,11 @@
1
+ export function criteriaFrom(level, verdict, action) {
2
+ const candidate = level === 'L2' && verdict !== -1;
3
+ return {
4
+ decision: action === 'keep' ? 'keep' : candidate ? 'merge' : 'candidate',
5
+ validation: level === 'L2' ? 'required' : 'report-only',
6
+ commit: candidate && action === 'merge' ? 'required' : 'never',
7
+ archive: candidate && action === 'merge' ? 'sources' : 'none',
8
+ evidence: candidate && action === 'merge' ? 'receipt' : 'none',
9
+ };
10
+ }
11
+ //# sourceMappingURL=engine-criteria.js.map
@@ -0,0 +1,10 @@
1
+ /** OI-11 engine input allowlist: exactly modelsRoot/reportOnly/maxTokens/timeoutMs. */
2
+ export interface EngineInput {
3
+ readonly modelsRoot: string;
4
+ readonly reportOnly: boolean;
5
+ readonly maxTokens: number;
6
+ readonly timeoutMs: number;
7
+ }
8
+ export declare const ENGINE_FORBIDDEN_KEYS: readonly ["offline", "auto_provision", "embed_backend", "rank_mode", "models_dir", "embedder_model", "embedder_model_last", "judge"];
9
+ export declare function resolveEngineInput(input?: unknown): EngineInput;
10
+ //# sourceMappingURL=engine-input.d.ts.map
@@ -0,0 +1,26 @@
1
+ const ALLOWED = new Set(['modelsRoot', 'reportOnly', 'maxTokens', 'timeoutMs']);
2
+ export const ENGINE_FORBIDDEN_KEYS = ['offline', 'auto_provision', 'embed_backend', 'rank_mode', 'models_dir', 'embedder_model', 'embedder_model_last', 'judge'];
3
+ function integer(name, value, fallback) {
4
+ if (value === undefined)
5
+ return fallback;
6
+ if (typeof value !== 'number' || !Number.isInteger(value) || value < 0)
7
+ throw new Error(name + ' must be a non-negative integer');
8
+ return value;
9
+ }
10
+ export function resolveEngineInput(input = {}) {
11
+ if (typeof input !== 'object' || input === null || Array.isArray(input))
12
+ throw new Error('engine input must be an object');
13
+ for (const key of Object.keys(input)) {
14
+ if (ENGINE_FORBIDDEN_KEYS.includes(key))
15
+ throw new Error('engine input key is forbidden: ' + key);
16
+ if (!ALLOWED.has(key))
17
+ throw new Error('unknown engine input: ' + key);
18
+ }
19
+ const record = input;
20
+ if (record.modelsRoot !== undefined && typeof record.modelsRoot !== 'string')
21
+ throw new Error('modelsRoot must be a string');
22
+ if (record.reportOnly !== undefined && typeof record.reportOnly !== 'boolean')
23
+ throw new Error('reportOnly must be a boolean');
24
+ return { modelsRoot: typeof record.modelsRoot === 'string' ? record.modelsRoot : '', reportOnly: record.reportOnly === true, maxTokens: integer('maxTokens', record.maxTokens, 0), timeoutMs: integer('timeoutMs', record.timeoutMs, 0) };
25
+ }
26
+ //# sourceMappingURL=engine-input.js.map
@@ -1,12 +1,26 @@
1
- /** Pure merge error rendering: parameter errors are two lines, runtime/target failures three. */
1
+ /** Pure merge error rendering: parameter errors stay one line; command failures are code/hint/next. */
2
2
  export type MergeErrorKind = 'parameter' | 'runtime' | 'target' | 'approval';
3
3
  export interface MergeError {
4
4
  readonly kind: MergeErrorKind;
5
5
  readonly code: string;
6
6
  readonly message: string;
7
+ /** Reader-facing explanation; defaults to the message (plus detail). */
8
+ readonly hint?: string;
9
+ /** The one next step; every runtime failure names one. */
10
+ readonly next?: string;
7
11
  readonly detail?: string;
8
12
  readonly stage?: string;
9
13
  readonly runId?: string;
10
14
  }
15
+ /** Build the typed failure every command/CLI/runtime branch returns. */
16
+ export declare function commandError(stage: string, code: string, hint: string, next: string, runId?: string): MergeError;
17
+ /** Render a merge command failure: one line for syntax, three lines (code/hint/next) for runtime. */
11
18
  export declare function formatMergeError(error: MergeError): string;
19
+ /** A typed failure result: `message` is the rendered three-line text, `error` keeps the fields. */
20
+ export interface FailureResult {
21
+ readonly ok: false;
22
+ readonly message: string;
23
+ readonly error: MergeError;
24
+ }
25
+ export declare function failure(stage: string, code: string, hint: string, next: string, runId?: string): FailureResult;
12
26
  //# sourceMappingURL=errors.d.ts.map
@@ -4,12 +4,31 @@ function redact(text) {
4
4
  .replace(/Bearer\s+\S+/gi, 'Bearer [redacted]')
5
5
  .replace(/(api[_-]?key\s*[:=]\s*)\S+/gi, '$1[redacted]');
6
6
  }
7
+ /** Build the typed failure every command/CLI/runtime branch returns. */
8
+ export function commandError(stage, code, hint, next, runId) {
9
+ return { kind: 'runtime', code, message: hint, hint, next, stage, ...(runId === undefined ? {} : { runId }) };
10
+ }
11
+ function oneLine(value) {
12
+ return value.replace(/\r?\n+/g, ' ').trim();
13
+ }
14
+ function render(error) {
15
+ const where = [
16
+ error.stage === undefined ? undefined : 'stage=' + error.stage,
17
+ error.runId === undefined ? undefined : 'run=' + error.runId,
18
+ ].filter((value) => value !== undefined).join(', ');
19
+ const heading = 'code: ' + error.code + (where === '' ? '' : ' (' + where + ')');
20
+ const hint = 'hint: ' + oneLine(error.hint ?? [error.message, error.detail].filter((value) => value !== undefined && value !== '').join(' — '));
21
+ const next = 'next: ' + oneLine(error.next ?? error.detail ?? 'fix the target or rerun after the cause is gone');
22
+ return redact([heading, hint, next].join('\n'));
23
+ }
24
+ /** Render a merge command failure: one line for syntax, three lines (code/hint/next) for runtime. */
7
25
  export function formatMergeError(error) {
8
26
  if (error.kind === 'parameter')
9
27
  return 'merge: ' + error.message + ' (' + error.code + ')';
10
- const where = error.stage === undefined && error.runId === undefined
11
- ? (error.detail ?? 'no additional detail')
12
- : [error.stage === undefined ? undefined : 'stage=' + error.stage, error.runId === undefined ? undefined : 'run=' + error.runId].filter(Boolean).join(' ');
13
- return redact('merge failed: ' + error.message + ' (' + error.code + ')\n' + where + '\nnext: fix the target or rerun after the cause is gone');
28
+ return render(error);
29
+ }
30
+ export function failure(stage, code, hint, next, runId) {
31
+ const error = commandError(stage, code, hint, next, runId);
32
+ return { ok: false, message: formatMergeError(error), error };
14
33
  }
15
34
  //# sourceMappingURL=errors.js.map
@@ -0,0 +1,3 @@
1
+ import type { UnitEvidenceRecord } from '@lmzhen/dsh-evolution-core';
2
+ export declare function evidenceSha256(evidence: UnitEvidenceRecord): string;
3
+ //# sourceMappingURL=evidence-hash.d.ts.map
@@ -0,0 +1,14 @@
1
+ /** Canonical evidence JSON hash shared with core unit receipts. */
2
+ import { createHash } from 'node:crypto';
3
+ function canonical(value) {
4
+ if (value === null || typeof value !== 'object')
5
+ return JSON.stringify(value);
6
+ if (Array.isArray(value))
7
+ return '[' + value.map(canonical).join(',') + ']';
8
+ const object = value;
9
+ return '{' + Object.keys(object).sort().map(key => JSON.stringify(key) + ':' + canonical(object[key])).join(',') + '}';
10
+ }
11
+ export function evidenceSha256(evidence) {
12
+ return createHash('sha256').update(canonical({ ...evidence, at: 0, mutations: 0 })).digest('hex');
13
+ }
14
+ //# sourceMappingURL=evidence-hash.js.map
@@ -20,6 +20,20 @@ export type MergeFailureRoute = {
20
20
  } | {
21
21
  readonly kind: 'runtime';
22
22
  readonly reportOnly: true;
23
+ } | {
24
+ readonly kind: 'over-limit';
25
+ readonly reportOnly: true;
26
+ readonly reason: 'merge-over-limit';
27
+ readonly lengths: Readonly<Record<string, number>>;
23
28
  };
29
+ export declare function routeLimitFailure(lengths: Readonly<Record<string, number>>): MergeFailureRoute;
24
30
  export declare function routeFailure(code: string): MergeFailureRoute;
31
+ /** Batch 2 typed CAS hook; Batch 3 owns full targetRevision expiry. */
32
+ export type CasFailureCode = 'target-changed' | 'target-taken' | 'source-changed' | 'support-changed';
33
+ export interface CasHookResult {
34
+ readonly kind: 'expired';
35
+ readonly reason: Exclude<CasFailureCode, 'target-changed' | 'support-changed'>;
36
+ readonly write: false;
37
+ }
38
+ export declare function casExpiredHook(code: string): CasHookResult | null;
25
39
  //# sourceMappingURL=failure-route.d.ts.map
@@ -4,7 +4,12 @@ const TARGET_TAKEN = new Set(['target-taken', 'target-exists']);
4
4
  const VALIDATION = new Set(['validation-failed', 'invalid-decision']);
5
5
  const COMMIT = new Set(['commit-failed', 'commit-refused']);
6
6
  const SCHEMA = new Set(['schema-invalid', 'schema-version']);
7
+ export function routeLimitFailure(lengths) {
8
+ return { kind: 'over-limit', reportOnly: true, reason: 'merge-over-limit', lengths };
9
+ }
7
10
  export function routeFailure(code) {
11
+ if (code === 'merge-over-limit')
12
+ return routeLimitFailure({});
8
13
  if (PARAMETER.has(code))
9
14
  return { kind: 'parameter', reportOnly: false };
10
15
  if (SOURCE_GONE.has(code))
@@ -19,4 +24,11 @@ export function routeFailure(code) {
19
24
  return { kind: 'schema', reportOnly: true };
20
25
  return { kind: 'runtime', reportOnly: true };
21
26
  }
27
+ export function casExpiredHook(code) {
28
+ if (code === 'target-changed' || code === 'target-taken')
29
+ return { kind: 'expired', reason: 'target-taken', write: false };
30
+ if (code === 'source-changed' || code === 'support-changed')
31
+ return { kind: 'expired', reason: 'source-changed', write: false };
32
+ return null;
33
+ }
22
34
  //# sourceMappingURL=failure-route.js.map
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Read-time convergence facts for pending rows (FR-LIFE-14).
3
+ *
4
+ * Snapshot and reconcile must agree on WHY a pending row is no longer actionable, but no
5
+ * reader may write. This module is the single derivation both sides call: given the live
6
+ * library view it answers absent/source-gone/target-taken/source-changed, and the
7
+ * persisted row state is only consulted for an already-recorded expiry.
8
+ */
9
+ import type { RevisionRef } from '../contracts.ts';
10
+ import type { SkillCard } from '../skill-card.ts';
11
+ import type { V4Row } from './row-v4.ts';
12
+ /** Why a pending row reads as expired. `source-gone` rows are removed by reconcile. */
13
+ export type PendingExpiryKind = 'target-taken' | 'source-changed' | 'source-gone';
14
+ /** The live library facts the derivation needs; no io, no writes. */
15
+ export interface LiveRowFacts {
16
+ readonly managed: (name: string) => boolean;
17
+ readonly targetRevisionRef: (name: string) => RevisionRef;
18
+ readonly sourceRevisionRef: (a: string, b: string) => RevisionRef;
19
+ readonly supportDigest: (names: readonly string[]) => string;
20
+ }
21
+ export declare function liveRowFactsOf(cards: readonly SkillCard[]): LiveRowFacts;
22
+ /**
23
+ * The read-only expiry verdict for one row.
24
+ * @param row - the persisted row (only pending rows converge here).
25
+ * @param live - current library facts; absent means "cannot derive".
26
+ * @returns the expiry kind, or null when the row is still actionable.
27
+ */
28
+ export declare function pendingExpiry(row: V4Row, live: LiveRowFacts | undefined): PendingExpiryKind | null;
29
+ /** The persisted expiry kind, when the row already carries one. */
30
+ export declare function persistedExpiry(row: V4Row): PendingExpiryKind | null;
31
+ //# sourceMappingURL=live-facts.d.ts.map
@@ -0,0 +1,54 @@
1
+ import { revisionMismatch, sourceRevisionRefOf, supportDigestOf, targetRevisionRefOf } from "../revision.js";
2
+ export function liveRowFactsOf(cards) {
3
+ const byName = new Map(cards.map(card => [card.name, card]));
4
+ return {
5
+ managed: name => byName.get(name)?.managed === true,
6
+ targetRevisionRef: name => targetRevisionRefOf(cards, name),
7
+ sourceRevisionRef: (a, b) => sourceRevisionRefOf(cards, a, b),
8
+ supportDigest: names => supportDigestOf(cards, names),
9
+ };
10
+ }
11
+ /**
12
+ * The read-only expiry verdict for one row.
13
+ * @param row - the persisted row (only pending rows converge here).
14
+ * @param live - current library facts; absent means "cannot derive".
15
+ * @returns the expiry kind, or null when the row is still actionable.
16
+ */
17
+ export function pendingExpiry(row, live) {
18
+ if (live === undefined)
19
+ return null;
20
+ if (row.state === 'verify') {
21
+ return !live.managed(row.a) || !live.managed(row.b) ? 'source-gone' : null;
22
+ }
23
+ if (row.state !== 'pending' && row.state !== 'fuse' && row.state !== 'approved')
24
+ return null;
25
+ if (!live.managed(row.a) || !live.managed(row.b))
26
+ return 'source-gone';
27
+ const decision = row.decision;
28
+ if (row.state !== 'pending') {
29
+ if (revisionMismatch(decision.sourceRevisionRef, live.sourceRevisionRef(row.a, row.b)) !== null)
30
+ return 'source-changed';
31
+ if (decision.supportDigest !== undefined && decision.supportDigest !== live.supportDigest([decision.target, row.a, row.b]))
32
+ return 'source-changed';
33
+ return null;
34
+ }
35
+ if (decision.mode !== 'create-umbrella') {
36
+ if (!live.managed(decision.target))
37
+ return 'target-taken';
38
+ }
39
+ // A create-umbrella decision recorded "this name was free"; the name being taken now is
40
+ // the same conflict the approve precheck reports.
41
+ if (revisionMismatch(decision.targetRevisionRef, live.targetRevisionRef(decision.target)) !== null)
42
+ return 'target-taken';
43
+ const expectedSource = decision.sourceRevisionRef;
44
+ if (revisionMismatch(expectedSource, live.sourceRevisionRef(row.a, row.b)) !== null)
45
+ return 'source-changed';
46
+ if (decision.supportDigest !== undefined && decision.supportDigest !== live.supportDigest([decision.target, row.a, row.b]))
47
+ return 'source-changed';
48
+ return null;
49
+ }
50
+ /** The persisted expiry kind, when the row already carries one. */
51
+ export function persistedExpiry(row) {
52
+ return row.state === 'pending' && row.expired !== undefined ? row.expired : null;
53
+ }
54
+ //# sourceMappingURL=live-facts.js.map
@@ -0,0 +1,18 @@
1
+ import { policyStageLimits, type SkillLimits } from '@lmzhen/dsh-evolution-core';
2
+ export interface MergeLimitsSnapshot {
3
+ readonly skillLimits: SkillLimits;
4
+ readonly limitsDigest: string;
5
+ }
6
+ export declare function resolveLimitsSnapshot(policy?: Parameters<typeof policyStageLimits>[0]): MergeLimitsSnapshot;
7
+ export declare function limitsSnapshotOf(skillLimits: SkillLimits): MergeLimitsSnapshot;
8
+ export declare function mergeLimitFinding(path: string, limit: string, observed: number, limitValue: number): {
9
+ readonly rule: string;
10
+ readonly path: string;
11
+ readonly severity: 'block';
12
+ readonly code: 'limit';
13
+ readonly limit: string;
14
+ readonly observed: number;
15
+ readonly limitValue: number;
16
+ readonly message: string;
17
+ };
18
+ //# sourceMappingURL=merge-limits.d.ts.map
@@ -0,0 +1,22 @@
1
+ /** One limits snapshot shared by authoring, validation and commit. */
2
+ import { createHash } from 'node:crypto';
3
+ import { DEFAULT_SKILL_LIMITS, policyStageLimits } from '@lmzhen/dsh-evolution-core';
4
+ function canonical(value) {
5
+ if (value === null || typeof value !== 'object')
6
+ return JSON.stringify(value);
7
+ if (Array.isArray(value))
8
+ return '[' + value.map(canonical).join(',') + ']';
9
+ const object = value;
10
+ return '{' + Object.keys(object).sort().map(key => JSON.stringify(key) + ':' + canonical(object[key])).join(',') + '}';
11
+ }
12
+ export function resolveLimitsSnapshot(policy) {
13
+ const skillLimits = { ...DEFAULT_SKILL_LIMITS, ...policyStageLimits(policy) };
14
+ return { skillLimits, limitsDigest: createHash('sha256').update(canonical(skillLimits)).digest('hex') };
15
+ }
16
+ export function limitsSnapshotOf(skillLimits) {
17
+ return { skillLimits, limitsDigest: createHash('sha256').update(canonical(skillLimits)).digest('hex') };
18
+ }
19
+ export function mergeLimitFinding(path, limit, observed, limitValue) {
20
+ return { rule: 'limit', path, severity: 'block', code: 'limit', limit, observed, limitValue, message: path + ' exceeds ' + limit + ' (' + String(observed) + ' > ' + String(limitValue) + ')' };
21
+ }
22
+ //# sourceMappingURL=merge-limits.js.map
@@ -0,0 +1,41 @@
1
+ /** 1b-1 additive report schema; values are filled by later batches. */
2
+ export interface MergeReportLimits {
3
+ readonly maxSkillFileBytes?: number;
4
+ readonly maxTokenBytes?: number;
5
+ readonly pairTimeoutMinutes?: number;
6
+ }
7
+ export interface MergeReportValidation {
8
+ readonly ok: boolean;
9
+ readonly errors: readonly string[];
10
+ }
11
+ export interface MergeReport {
12
+ readonly schemaVersion: 2;
13
+ readonly reportId: string;
14
+ readonly runId: string;
15
+ readonly pairKey: string;
16
+ readonly target: string;
17
+ readonly sourceRevisions?: Readonly<Record<string, string>>;
18
+ readonly targetRevision?: string;
19
+ readonly validation: MergeReportValidation;
20
+ readonly limits: MergeReportLimits;
21
+ readonly q09?: {
22
+ readonly forwardRefs: readonly string[];
23
+ readonly reverseRefs: readonly string[];
24
+ readonly orphanFiles: readonly string[];
25
+ };
26
+ readonly threat?: {
27
+ readonly body: string;
28
+ readonly source: string;
29
+ };
30
+ readonly createdAt: string;
31
+ }
32
+ export interface MergeReportRef {
33
+ readonly path: string;
34
+ readonly digest: string;
35
+ readonly writtenAt: string;
36
+ }
37
+ export interface MergeReportWriter {
38
+ write(report: MergeReport): Promise<MergeReportRef>;
39
+ read(runId: string): Promise<MergeReport | null>;
40
+ }
41
+ //# sourceMappingURL=merge-report.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=merge-report.js.map