@lmzhen/dsh-evolution-merge 0.30.17 → 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 (187) hide show
  1. package/README.md +25 -7
  2. package/data/minhash_params.json +1063 -0
  3. package/lib/index.js +7308 -1800
  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/library.js +4 -1
  9. package/lib/types/adapters/model-runner.d.ts +0 -5
  10. package/lib/types/adapters/model-runner.js +8 -2
  11. package/lib/types/adapters/model.d.ts +31 -0
  12. package/lib/types/adapters/model.js +47 -0
  13. package/lib/types/adapters/select.js +12 -8
  14. package/lib/types/approve-command.d.ts +14 -1
  15. package/lib/types/approve-command.js +72 -44
  16. package/lib/types/authoring-flow.d.ts +19 -1
  17. package/lib/types/authoring-flow.js +49 -15
  18. package/lib/types/channel-port.d.ts +3 -1
  19. package/lib/types/channel-port.js +8 -3
  20. package/lib/types/command-defs.d.ts +5 -0
  21. package/lib/types/command-defs.js +33 -0
  22. package/lib/types/command-surface.d.ts +5 -0
  23. package/lib/types/command-surface.js +11 -0
  24. package/lib/types/commands.d.ts +3 -4
  25. package/lib/types/commands.js +33 -72
  26. package/lib/types/commit-assembly.d.ts +16 -1
  27. package/lib/types/commit-assembly.js +72 -11
  28. package/lib/types/commit-execute.d.ts +14 -2
  29. package/lib/types/commit-execute.js +76 -14
  30. package/lib/types/commit-plan.d.ts +7 -1
  31. package/lib/types/commit-plan.js +4 -0
  32. package/lib/types/commit-receipt.d.ts +3 -0
  33. package/lib/types/commit-recovery.d.ts +7 -2
  34. package/lib/types/commit-recovery.js +73 -96
  35. package/lib/types/contracts.d.ts +44 -1
  36. package/lib/types/contracts.js +1 -1
  37. package/lib/types/coordinator.d.ts +3 -2
  38. package/lib/types/coordinator.js +4 -3
  39. package/lib/types/decision.js +1 -1
  40. package/lib/types/doctor.d.ts +18 -1
  41. package/lib/types/doctor.js +164 -10
  42. package/lib/types/domain/authoring-manifest.d.ts +26 -0
  43. package/lib/types/domain/authoring-manifest.js +43 -0
  44. package/lib/types/domain/claim.d.ts +4 -1
  45. package/lib/types/domain/claim.js +2 -2
  46. package/lib/types/domain/commit-journal-store.js +5 -1
  47. package/lib/types/domain/commit-journal.js +1 -1
  48. package/lib/types/domain/consistency.d.ts +22 -0
  49. package/lib/types/domain/consistency.js +50 -0
  50. package/lib/types/domain/engine-criteria.d.ts +10 -0
  51. package/lib/types/domain/engine-criteria.js +11 -0
  52. package/lib/types/domain/engine-input.d.ts +10 -0
  53. package/lib/types/domain/engine-input.js +26 -0
  54. package/lib/types/domain/errors.d.ts +15 -1
  55. package/lib/types/domain/errors.js +23 -4
  56. package/lib/types/domain/evidence-hash.d.ts +3 -0
  57. package/lib/types/domain/evidence-hash.js +14 -0
  58. package/lib/types/domain/failure-route.d.ts +14 -0
  59. package/lib/types/domain/failure-route.js +12 -0
  60. package/lib/types/domain/ledger.js +10 -1
  61. package/lib/types/domain/live-facts.d.ts +31 -0
  62. package/lib/types/domain/live-facts.js +54 -0
  63. package/lib/types/domain/merge-limits.d.ts +18 -0
  64. package/lib/types/domain/merge-limits.js +22 -0
  65. package/lib/types/domain/merge-report.d.ts +41 -0
  66. package/lib/types/domain/merge-report.js +2 -0
  67. package/lib/types/domain/merge-snapshot.d.ts +99 -0
  68. package/lib/types/domain/merge-snapshot.js +82 -0
  69. package/lib/types/domain/migrate-v3.d.ts +20 -0
  70. package/lib/types/domain/migrate-v3.js +68 -0
  71. package/lib/types/domain/pair-ledger.d.ts +34 -0
  72. package/lib/types/domain/pair-ledger.js +96 -0
  73. package/lib/types/domain/pre-1b-backup.d.ts +43 -0
  74. package/lib/types/domain/pre-1b-backup.js +140 -0
  75. package/lib/types/domain/provider-key.d.ts +2 -0
  76. package/lib/types/domain/provider-key.js +11 -0
  77. package/lib/types/domain/read-probe.d.ts +5 -0
  78. package/lib/types/domain/read-probe.js +11 -0
  79. package/lib/types/domain/reconcile.d.ts +47 -0
  80. package/lib/types/domain/reconcile.js +178 -0
  81. package/lib/types/domain/resolve-pair.d.ts +2 -9
  82. package/lib/types/domain/resolve-pair.js +2 -15
  83. package/lib/types/domain/row-tx.d.ts +27 -0
  84. package/lib/types/domain/row-tx.js +31 -0
  85. package/lib/types/domain/row-v3-bridge.d.ts +6 -0
  86. package/lib/types/domain/row-v3-bridge.js +47 -0
  87. package/lib/types/domain/row-v3.d.ts +113 -0
  88. package/lib/types/domain/row-v3.js +107 -0
  89. package/lib/types/domain/row-v4.d.ts +148 -0
  90. package/lib/types/domain/row-v4.js +159 -0
  91. package/lib/types/domain/row.d.ts +1 -0
  92. package/lib/types/domain/run-coordinator.d.ts +51 -0
  93. package/lib/types/domain/run-coordinator.js +45 -0
  94. package/lib/types/domain/schema4.d.ts +53 -0
  95. package/lib/types/domain/schema4.js +129 -0
  96. package/lib/types/domain/selection-policy.d.ts +14 -0
  97. package/lib/types/domain/selection-policy.js +6 -0
  98. package/lib/types/domain/skipped-expiry.d.ts +5 -0
  99. package/lib/types/domain/skipped-expiry.js +16 -0
  100. package/lib/types/domain/stage-aggregate.d.ts +96 -0
  101. package/lib/types/domain/stage-aggregate.js +293 -0
  102. package/lib/types/domain/staging-index.d.ts +15 -0
  103. package/lib/types/domain/staging-index.js +41 -0
  104. package/lib/types/domain/state-bootstrap.d.ts +43 -0
  105. package/lib/types/domain/state-bootstrap.js +267 -0
  106. package/lib/types/domain/state-v3.d.ts +4 -0
  107. package/lib/types/domain/state-v3.js +4 -0
  108. package/lib/types/domain/undo-journal.d.ts +19 -0
  109. package/lib/types/domain/undo-journal.js +20 -0
  110. package/lib/types/domain/undo-receipt.d.ts +18 -0
  111. package/lib/types/domain/undo-receipt.js +22 -0
  112. package/lib/types/domain/validation-plan.d.ts +65 -0
  113. package/lib/types/domain/validation-plan.js +49 -0
  114. package/lib/types/facts.d.ts +26 -0
  115. package/lib/types/facts.js +65 -0
  116. package/lib/types/gate-filter.d.ts +31 -0
  117. package/lib/types/gate-filter.js +64 -0
  118. package/lib/types/index.d.ts +7 -2
  119. package/lib/types/index.js +185 -82
  120. package/lib/types/ladder.js +1 -1
  121. package/lib/types/merge-command.d.ts +3 -1
  122. package/lib/types/merge-command.js +10 -6
  123. package/lib/types/merge-routes.d.ts +12 -7
  124. package/lib/types/merge-routes.js +87 -23
  125. package/lib/types/merge-tool.d.ts +1 -1
  126. package/lib/types/merge-tool.js +2 -2
  127. package/lib/types/metrics.d.ts +16 -0
  128. package/lib/types/metrics.js +27 -0
  129. package/lib/types/new-child-authoring.d.ts +5 -1
  130. package/lib/types/new-child-authoring.js +71 -13
  131. package/lib/types/params.d.ts +2 -4
  132. package/lib/types/params.js +13 -0
  133. package/lib/types/pending-actions.d.ts +2 -2
  134. package/lib/types/pending-actions.js +17 -17
  135. package/lib/types/prompts.d.ts +2 -4
  136. package/lib/types/prompts.js +21 -8
  137. package/lib/types/propose.d.ts +26 -0
  138. package/lib/types/propose.js +69 -0
  139. package/lib/types/read-commands.d.ts +4 -4
  140. package/lib/types/read-commands.js +19 -26
  141. package/lib/types/recovery-coordinator.d.ts +34 -0
  142. package/lib/types/recovery-coordinator.js +84 -0
  143. package/lib/types/recovery-projection.d.ts +31 -0
  144. package/lib/types/recovery-projection.js +96 -0
  145. package/lib/types/resume-command.d.ts +7 -1
  146. package/lib/types/resume-command.js +50 -14
  147. package/lib/types/revision.d.ts +17 -0
  148. package/lib/types/revision.js +39 -3
  149. package/lib/types/round2-outcome.d.ts +2 -1
  150. package/lib/types/round2-outcome.js +68 -12
  151. package/lib/types/run-shell.d.ts +16 -2
  152. package/lib/types/run-shell.js +49 -5
  153. package/lib/types/runtime.d.ts +4 -6
  154. package/lib/types/runtime.js +341 -86
  155. package/lib/types/scan-command.d.ts +1 -0
  156. package/lib/types/scan-command.js +2 -0
  157. package/lib/types/scan-ports.d.ts +21 -1
  158. package/lib/types/scan-ports.js +51 -23
  159. package/lib/types/scan-run.d.ts +17 -0
  160. package/lib/types/scan-run.js +221 -63
  161. package/lib/types/screening-port.d.ts +47 -0
  162. package/lib/types/screening-port.js +93 -0
  163. package/lib/types/screening.d.ts +30 -0
  164. package/lib/types/screening.js +75 -0
  165. package/lib/types/stage-tool-registry.d.ts +7 -0
  166. package/lib/types/stage-tool-registry.js +12 -1
  167. package/lib/types/state.d.ts +15 -1
  168. package/lib/types/state.js +39 -17
  169. package/lib/types/status.d.ts +8 -0
  170. package/lib/types/status.js +64 -0
  171. package/lib/types/surface/body.d.ts +4 -0
  172. package/lib/types/surface/body.js +16 -0
  173. package/lib/types/surface/merge-view.d.ts +14 -0
  174. package/lib/types/surface/merge-view.js +126 -0
  175. package/lib/types/target-claim.d.ts +1 -0
  176. package/lib/types/target-claim.js +63 -18
  177. package/lib/types/undo-operation.d.ts +24 -0
  178. package/lib/types/undo-operation.js +135 -0
  179. package/lib/types/unit-claim.d.ts +5 -0
  180. package/lib/types/unit-claim.js +15 -0
  181. package/lib/types/wire-projection.d.ts +36 -0
  182. package/lib/types/wire-projection.js +69 -0
  183. package/lib/types/wire.d.ts +273 -0
  184. package/lib/types/wire.js +3 -0
  185. package/package.json +10 -4
  186. package/lib/types/routes.d.ts +0 -70
  187. package/lib/types/routes.js +0 -78
@@ -1,6 +1,7 @@
1
1
  import { HOST_ROUTES, json, methodNotAllowed } from '@lmzhen/dsh-evolution-core/http-routes';
2
- import { buildMergeView, pageQueue, toRunFact } from "./routes.js";
3
- import { renderMarkdown } from "./domain/markdown.js";
2
+ import { buildFacts as buildLegacyFacts } from "./facts.js";
3
+ import { toBodyView, toHistoryView, toQueuePage } from "./wire-projection.js";
4
+ import { buildBodyView, buildFacts as buildSnapshotFacts, buildHistoryView, buildQueuePage } from "./surface/merge-view.js";
4
5
  const MERGE_PATHS = HOST_ROUTES.merge;
5
6
  const MERGE_BASE = MERGE_PATHS.view.slice(0, -'view'.length);
6
7
  export const MERGE_ROUTES = {
@@ -10,7 +11,15 @@ export const MERGE_ROUTES = {
10
11
  history: (MERGE_PATHS.history ?? MERGE_BASE + 'history'),
11
12
  };
12
13
  export function makeMergeRoutes(deps, _audit) {
13
- const absent = () => json(503, { ok: false, error: { code: 'merge-service-absent', message: 'merge service is not mounted' } });
14
+ const absent = () => json(404, { wireVersion: 1, code: 'E-325', message: 'merge service is not mounted' });
15
+ const blocked = (readiness) => json(503, { wireVersion: 1, code: 'merge-state-blocked', readiness });
16
+ const notReady = async (source) => {
17
+ if (source.readiness === undefined)
18
+ return null;
19
+ const readiness = await source.readiness();
20
+ return readiness.ok ? null : blocked(readiness);
21
+ };
22
+ const bad = (message) => json(400, { wireVersion: 1, code: 'merge-bad-query', message });
14
23
  const get = (request) => (request.method === 'GET' ? null : methodNotAllowed(['GET']));
15
24
  const url = (request) => new URL(request.url, 'http://local');
16
25
  return [
@@ -25,12 +34,19 @@ export function makeMergeRoutes(deps, _audit) {
25
34
  const source = deps.getSource();
26
35
  if (source === undefined)
27
36
  return absent();
28
- const runs = await source.runs(undefined, 20);
29
- const latestMerge = runs.find(run => run.kind === 'merge');
30
- return json(200, {
31
- ok: true,
32
- data: buildMergeView(await source.rows(), source.commands(), latestMerge === undefined ? undefined : toRunFact(latestMerge), source.approval?.()),
33
- });
37
+ if (source.snapshot !== undefined) {
38
+ const snapshot = await source.snapshot();
39
+ if (snapshot.blocked !== undefined)
40
+ return json(503, { wireVersion: 1, code: 'merge-state-blocked', readiness: snapshot.blocked.readiness });
41
+ return json(200, buildSnapshotFacts(snapshot));
42
+ }
43
+ const readiness = await notReady(source);
44
+ if (readiness !== null)
45
+ return readiness;
46
+ const readAt = Date.now();
47
+ const rows = await source.rows?.() ?? [];
48
+ const runs = await source.runs?.(undefined, 20) ?? [];
49
+ return json(200, buildLegacyFacts({ rows, runs, approval: source.approval?.() === true, readAt }));
34
50
  },
35
51
  },
36
52
  {
@@ -46,15 +62,30 @@ export function makeMergeRoutes(deps, _audit) {
46
62
  return absent();
47
63
  const query = url(request).searchParams;
48
64
  const queue = query.get('queue') ?? 'verify';
49
- if (queue !== 'verify' && queue !== 'pending' && queue !== 'fuse' && queue !== 'skipped') {
50
- return json(400, { ok: false, error: { code: 'bad-queue', message: 'queue must be verify/pending/fuse/skipped' } });
51
- }
52
- const cursor = Number.parseInt(query.get('cursor') ?? '0', 10);
65
+ if (queue !== 'verify' && queue !== 'pending' && queue !== 'fuse' && queue !== 'skipped')
66
+ return bad('queue must be verify/pending/fuse/skipped');
67
+ const expiredOnly = query.get('expired') === '1';
68
+ if (expiredOnly && queue !== 'pending')
69
+ return bad('expired is only allowed for pending');
53
70
  const limit = Number.parseInt(query.get('limit') ?? '50', 10);
54
- return json(200, {
55
- ok: true,
56
- data: pageQueue(await source.rows(), queue, Number.isFinite(cursor) ? cursor : 0, Number.isFinite(limit) ? limit : 50),
57
- });
71
+ if (source.snapshot !== undefined) {
72
+ const snapshot = await source.snapshot();
73
+ if (snapshot.blocked !== undefined)
74
+ return json(503, { wireVersion: 1, code: 'merge-state-blocked', readiness: snapshot.blocked.readiness });
75
+ try {
76
+ return json(200, buildQueuePage(snapshot, queue, expiredOnly, query.get('cursor') ?? undefined, Number.isFinite(limit) ? limit : 50));
77
+ }
78
+ catch (error) {
79
+ return bad(error instanceof Error ? error.message : 'queue cursor is stale');
80
+ }
81
+ }
82
+ const readiness = await notReady(source);
83
+ if (readiness !== null)
84
+ return readiness;
85
+ const offset = Number.parseInt(query.get('cursor') ?? '0', 10);
86
+ const rows = await source.rows?.() ?? [];
87
+ const page = toQueuePage(rows, queue, expiredOnly, Date.now(), Number.isFinite(limit) ? limit : 50);
88
+ return json(200, Number.isFinite(offset) && offset > 0 ? { ...page, rows: page.rows.slice(offset) } : page);
58
89
  },
59
90
  },
60
91
  {
@@ -70,14 +101,38 @@ export function makeMergeRoutes(deps, _audit) {
70
101
  return absent();
71
102
  const query = url(request).searchParams;
72
103
  const id = query.get('id');
104
+ const name = query.get('name');
73
105
  const side = query.get('side');
74
- if (id === null || id === '' || (side !== 'a' && side !== 'b')) {
75
- return json(400, { ok: false, error: { code: 'bad-body-query', message: 'id and side=a|b are required' } });
106
+ if (id !== null && name !== null)
107
+ return bad('id and name are mutually exclusive');
108
+ if ((id === null || id === '') && (name === null || name === ''))
109
+ return json(404, { wireVersion: 1, code: 'merge-body-not-found', message: 'merge body requires id or name' });
110
+ if (source.snapshot !== undefined) {
111
+ const snapshot = await source.snapshot();
112
+ if (snapshot.blocked !== undefined)
113
+ return json(503, { wireVersion: 1, code: 'merge-state-blocked', readiness: snapshot.blocked.readiness });
114
+ const refs = id !== null && id !== '' ? snapshot.bodyRefs.filter(ref => ref.rowId === id) : snapshot.bodyRefs.filter(ref => ref.name === name);
115
+ if (refs.length === 0)
116
+ return json(404, { wireVersion: 1, code: 'merge-body-not-found', message: 'merge body not found' });
117
+ if (name !== null && refs.length > 1)
118
+ return bad('name matches multiple merge rows');
119
+ const row = snapshot.rows.find(item => item.row.id === refs[0].rowId);
120
+ if (row?.stage?.state === 'lost' || row?.stage?.state === 'expired')
121
+ return json(404, { wireVersion: 1, code: 'merge-body-not-found', message: 'merge stage is lost' });
122
+ const sideFilter = side === 'a' ? 'live' : side === 'b' ? 'archive' : side === 'live' || side === 'archive' ? side : undefined;
123
+ const ref = (sideFilter === undefined ? refs[0] : refs.find(item => item.side === sideFilter)) ?? refs[0];
124
+ const content = source.bodyRef === undefined ? null : await source.bodyRef(ref);
125
+ return json(200, buildBodyView(snapshot, ref, content ?? ''));
76
126
  }
77
- const content = await source.body(id, side);
127
+ const readiness = await notReady(source);
128
+ if (readiness !== null)
129
+ return readiness;
130
+ if (id === null || id === '' || (side !== 'a' && side !== 'b'))
131
+ return bad('id and side=a|b are required');
132
+ const content = await source.body?.(id, side) ?? null;
78
133
  return content === null
79
- ? json(404, { ok: false, error: { code: 'body-not-found', message: 'merge body not found: ' + id } })
80
- : json(200, { ok: true, data: { content, markdown: true, html: renderMarkdown(content) } });
134
+ ? json(200, { wireVersion: 1, code: 'merge-body-missing', message: 'merge body not found: ' + id })
135
+ : json(200, toBodyView(id, side === 'a' ? 'live' : 'archive', content, Date.now()));
81
136
  },
82
137
  },
83
138
  {
@@ -91,7 +146,16 @@ export function makeMergeRoutes(deps, _audit) {
91
146
  const source = deps.getSource();
92
147
  if (source === undefined)
93
148
  return absent();
94
- return json(200, { ok: true, data: { items: await source.history() } });
149
+ if (source.snapshot !== undefined) {
150
+ const snapshot = await source.snapshot();
151
+ if (snapshot.blocked !== undefined)
152
+ return json(503, { wireVersion: 1, code: 'merge-state-blocked', readiness: snapshot.blocked.readiness });
153
+ return json(200, buildHistoryView(snapshot));
154
+ }
155
+ const readiness = await notReady(source);
156
+ if (readiness !== null)
157
+ return readiness;
158
+ return json(200, toHistoryView(await source.history?.() ?? [], Date.now()));
95
159
  },
96
160
  },
97
161
  ];
@@ -18,5 +18,5 @@ export interface ToolDefinitionLike {
18
18
  };
19
19
  readonly execute: (args: Record<string, unknown>, exec: unknown) => Promise<string>;
20
20
  }
21
- export declare function createMergeStageTool(io: EvolutionIoLike, home: string, context: (exec: unknown) => StageToolContext | undefined): ToolDefinitionLike;
21
+ export declare function createMergeStageTool(io: EvolutionIoLike, home: string, context: (exec: unknown) => StageToolContext | undefined, diagnose?: (exec: unknown) => unknown): ToolDefinitionLike;
22
22
  //# sourceMappingURL=merge-tool.d.ts.map
@@ -33,7 +33,7 @@ function operation(args) {
33
33
  }
34
34
  return shapeError('op must be list/read/write/copy');
35
35
  }
36
- export function createMergeStageTool(io, home, context) {
36
+ export function createMergeStageTool(io, home, context, diagnose) {
37
37
  return {
38
38
  name: MERGE_STAGE_TOOL_NAME,
39
39
  description: 'Read source copies and write the merged skill into the staging tree. list/read use area=sources|merged plus source (for sources); write/copy paths are relative to merged/<target> (for example path=SKILL.md, from=SKILL.md, to=references/x.md). Write/copy are only allowed in the author phase.',
@@ -61,7 +61,7 @@ export function createMergeStageTool(io, home, context) {
61
61
  return JSON.stringify(op);
62
62
  const ctx = context(exec);
63
63
  if (ctx === undefined)
64
- return JSON.stringify({ ok: false, code: 'not-merge-child', message: 'merge_stage is only available to an active merge child' });
64
+ return JSON.stringify({ ok: false, code: 'not-merge-child', message: 'merge_stage is only available to an active merge child', ...(diagnose === undefined ? {} : { diagnostic: diagnose(exec) }) });
65
65
  return JSON.stringify(await applyStageTool(io, home, ctx, op));
66
66
  },
67
67
  };
@@ -0,0 +1,16 @@
1
+ /** RG6 merge metrics projection: stable names/labels from state + runs. */
2
+ import type { RunRecord } from '@lmzhen/dsh-evolution-core';
3
+ import type { MergeRow } from './contracts.ts';
4
+ export interface MetricSample {
5
+ readonly name: string;
6
+ readonly labels: Readonly<Record<string, string>>;
7
+ readonly value: number;
8
+ }
9
+ export interface MergeMetricCounters {
10
+ readonly lockWaitMs?: number;
11
+ readonly claimConflict?: number;
12
+ readonly recoveryFail?: number;
13
+ }
14
+ export declare const MERGE_METRIC_NAMES: readonly ["merge_queue_depth", "merge_run_total", "merge_run_duration_ms", "merge_lock_wait_ms", "merge_claim_conflict_total", "merge_recovery_fail_total"];
15
+ export declare function buildMergeMetrics(rows: readonly MergeRow[], runs: readonly RunRecord[], counters?: MergeMetricCounters): readonly MetricSample[];
16
+ //# sourceMappingURL=metrics.d.ts.map
@@ -0,0 +1,27 @@
1
+ import { countsOf } from "./facts.js";
2
+ export const MERGE_METRIC_NAMES = [
3
+ 'merge_queue_depth',
4
+ 'merge_run_total',
5
+ 'merge_run_duration_ms',
6
+ 'merge_lock_wait_ms',
7
+ 'merge_claim_conflict_total',
8
+ 'merge_recovery_fail_total',
9
+ ];
10
+ export function buildMergeMetrics(rows, runs, counters = {}) {
11
+ const counts = countsOf(rows);
12
+ const samples = [];
13
+ for (const queue of ['verify', 'pending', 'fuse', 'skipped'])
14
+ samples.push({ name: 'merge_queue_depth', labels: { queue, kind: 'active' }, value: counts.active[queue] });
15
+ samples.push({ name: 'merge_queue_depth', labels: { queue: 'pending', kind: 'expired' }, value: counts.expired });
16
+ for (const run of runs) {
17
+ const state = run.state;
18
+ samples.push({ name: 'merge_run_total', labels: { state }, value: 1 });
19
+ if (run.endedAt !== undefined)
20
+ samples.push({ name: 'merge_run_duration_ms', labels: { state }, value: Math.max(0, run.endedAt - run.startedAt) });
21
+ }
22
+ samples.push({ name: 'merge_lock_wait_ms', labels: {}, value: counters.lockWaitMs ?? 0 });
23
+ samples.push({ name: 'merge_claim_conflict_total', labels: {}, value: counters.claimConflict ?? 0 });
24
+ samples.push({ name: 'merge_recovery_fail_total', labels: {}, value: counters.recoveryFail ?? 0 });
25
+ return samples;
26
+ }
27
+ //# sourceMappingURL=metrics.js.map
@@ -2,7 +2,7 @@
2
2
  * Round2 with a NEW child (approval after the pending window). The child gets the round2
3
3
  * prompt and writes the staging tree; the plugin then runs the same gate/commit path.
4
4
  */
5
- import type { EvolutionIoLike } from '@lmzhen/dsh-evolution-core';
5
+ import type { EvolutionIoLike, UnitTransaction } from '@lmzhen/dsh-evolution-core';
6
6
  import type { MergeDecision, MergeRow } from './contracts.ts';
7
7
  import type { SkillCard } from './skill-card.ts';
8
8
  import type { ConversationChannelLike } from './channel-port.ts';
@@ -21,9 +21,13 @@ export interface NewChildAuthoringOptions {
21
21
  readonly timeoutMs: number;
22
22
  readonly commitPort: CommitPort;
23
23
  readonly now: () => number;
24
+ readonly isBlocked?: (name: string) => boolean | Promise<boolean>;
24
25
  readonly onChild?: (context: StageToolContext) => void;
25
26
  readonly onChildClosed?: (childId: string) => void;
27
+ readonly unit?: UnitTransaction | undefined;
28
+ readonly limits?: import('@lmzhen/dsh-evolution-core').SkillLimits;
26
29
  }
27
30
  export declare function runNewChildAuthoring(options: NewChildAuthoringOptions, row: MergeRow, decision: MergeDecision, signal: AbortSignal): Promise<ScanRound2Outcome>;
31
+ /** Bind the round-2 port to one run: approve/resume open a NEW child per run. */
28
32
  export declare function createNewChildRound2(options: Omit<NewChildAuthoringOptions, 'runId'>): (row: MergeRow, decision: MergeDecision, signal: AbortSignal, runId: string) => Promise<ScanRound2Outcome>;
29
33
  //# sourceMappingURL=new-child-authoring.d.ts.map
@@ -1,20 +1,48 @@
1
1
  import { buildRound2Prompt, STANDBY_PROMPT } from "./prompts.js";
2
- import { createStaging, readStaging, setStagingTarget, stageKeyOf, stagingRoot } from "./staging.js";
2
+ import { stageKeyOf, stagingRoot } from "./staging.js";
3
+ import { createStageTree, readStageEntry, readStageTree, setStageTreeTarget, dropStage } from "./domain/stage-aggregate.js";
3
4
  import { pairKey } from "./contracts.js";
4
5
  import { readStagedTree } from "./staged-tree.js";
5
6
  import { validateStagedTree, hasBlockingFinding } from "./rules.js";
7
+ import { DEFAULT_SKILL_LIMITS } from '@lmzhen/dsh-evolution-core';
8
+ import { limitsSnapshotOf } from "./domain/merge-limits.js";
9
+ import { buildValidationPlan } from "./domain/validation-plan.js";
6
10
  import { commitStagedRun } from "./commit-execute.js";
11
+ import { manifestFilesOf, missingManifestFile, parseAuthoringManifest } from "./domain/authoring-manifest.js";
12
+ import { MERGE_PROMPT_VERSION, promptHashOf } from "./prompts.js";
13
+ /** Commit-side causes that mean "the world moved"; they must not be retried blindly. */
14
+ const STALE_CAUSES = new Set(['target-changed', 'source-changed', 'support-changed', 'target-taken', 'target-gone']);
7
15
  export async function runNewChildAuthoring(options, row, decision, signal) {
16
+ if (decision.target.includes('/') || decision.target.includes(':'))
17
+ return { kind: 'failed', failure: { stage: 'author', code: 'target-invalid', message: 'target must be a bare skill name: ' + decision.target, retryable: false, retryFrom: 'none' } };
8
18
  const targetCard = options.cards.find(card => card.name === decision.target);
9
- if (targetCard?.protectionUnknown === true || targetCard?.protectedBy === 'pinned')
10
- return { kind: 'failed', failure: { stage: 'author', code: 'target-pinned', message: 'target is pinned: ' + decision.target, retryable: false, retryFrom: 'none' } };
19
+ const createMode = decision.mode === 'create-umbrella';
20
+ if (!createMode && targetCard !== undefined) {
21
+ if (targetCard.managed !== true)
22
+ return { kind: 'failed', failure: { stage: 'author', code: 'target-not-managed', message: 'target is not family-managed: ' + decision.target, retryable: false, retryFrom: 'none' } };
23
+ if (targetCard.protectionUnknown === true || targetCard.protectedBy != null)
24
+ return { kind: 'failed', failure: { stage: 'author', code: 'target-protected', message: 'target is protected: ' + decision.target, retryable: false, retryFrom: 'none' } };
25
+ }
26
+ if (!createMode && await options.isBlocked?.(decision.target) === true)
27
+ return { kind: 'failed', failure: { stage: 'author', code: 'target-blocked', message: 'target is blocked by curator: ' + decision.target, retryable: false, retryFrom: 'none' } };
28
+ const at = options.now();
29
+ // NFR-06: an expired stage is never reused and never re-presented. Drop its tree and
30
+ // index and author into a fresh one; the recorded decision anchors still guard the
31
+ // sources at commit, so re-staging cannot smuggle a stale merge through.
32
+ const stageEntry = await readStageEntry(options.io, options.home, row.id);
33
+ const expiredStage = stageEntry !== null && stageEntry.expiresAt <= at;
34
+ // The row's own staging ref wins; otherwise this run authors into its own stage key.
35
+ // The index entry is consulted for TTL only — switching the authoring key to it would
36
+ // break the approve flow, where the reserved entry and the run's tree are separate.
11
37
  const stageKey = row.stagingRef ?? stageKeyOf(options.runId, pairKey(row.a, row.b));
12
- const existing = await readStaging(options.io, options.home, stageKey);
38
+ if (expiredStage && stageEntry !== null)
39
+ await dropStage(options.io, options.home, row.id, stageEntry.ref);
40
+ const existing = expiredStage ? { kind: 'absent' } : await readStageTree(options.io, options.home, stageKey);
13
41
  if (existing.kind === 'present') {
14
- await setStagingTarget(options.io, options.home, stageKey, decision.target, decision.mode);
42
+ await setStageTreeTarget(options.io, options.home, stageKey, decision.target, decision.mode);
15
43
  }
16
44
  else {
17
- const staging = await createStaging(options.io, options.home, {
45
+ const staging = await createStageTree(options.io, options.home, {
18
46
  runId: stageKey,
19
47
  target: decision.target,
20
48
  mode: decision.mode,
@@ -32,7 +60,7 @@ export async function runNewChildAuthoring(options, row, decision, signal) {
32
60
  parent: options.parent,
33
61
  label: 'merge-round2',
34
62
  promptText: STANDBY_PROMPT,
35
- kind: 'maintainer',
63
+ kind: 'reviewer',
36
64
  allow: options.allow,
37
65
  cwd: stagingRoot(options.home, stageKey),
38
66
  timeoutMs: options.timeoutMs,
@@ -55,17 +83,46 @@ export async function runNewChildAuthoring(options, row, decision, signal) {
55
83
  const tree = await readStagedTree(options.io, options.home, stageKey);
56
84
  if (tree.kind !== 'present')
57
85
  return { kind: 'failed', failure: { stage: 'author', code: 'staging-missing', message: tree.kind === 'unknown' ? tree.reason : 'staged tree absent', retryable: true, retryFrom: 'agent2' } };
58
- const findings = validateStagedTree(tree.value);
59
- if (hasBlockingFinding(findings))
60
- return { kind: 'failed', failure: { stage: 'validate', code: 'rule-gate', message: 'staged tree failed the rule gate', retryable: true, retryFrom: 'agent2' } };
61
- const commit = await commitStagedRun(options.io, options.home, stageKey, options.commitPort, { origin: 'subagent', summary: 'merge ' + row.a + ' + ' + row.b, now: options.now });
62
- if (!commit.ok)
63
- return { kind: 'failed', failure: { stage: 'commit', code: 'commit-failed', message: commit.message, retryable: true, retryFrom: 'agent2' } };
86
+ const manifest = parseAuthoringManifest(turn.turn.text);
87
+ const stagedPaths = tree.value.files.map(file => file.path);
88
+ const manifestFiles = manifestFilesOf(manifest, stagedPaths);
89
+ const missing = missingManifestFile(manifestFiles, stagedPaths);
90
+ if (missing !== null)
91
+ return { kind: 'failed', failure: { stage: 'author', code: 'merge-manifest-missing', message: 'manifest file missing from staged tree: ' + missing, retryable: true, retryFrom: 'agent2' } };
92
+ const limitsSnapshot = limitsSnapshotOf(options.limits ?? DEFAULT_SKILL_LIMITS);
93
+ const validationPlan = buildValidationPlan({ tree: tree.value, target: decision.target, limits: limitsSnapshot, cards: options.cards, promptVersion: MERGE_PROMPT_VERSION, promptHash: promptHashOf(buildRound2Prompt(row, decision)), supportFiles: tree.value.files.map(file => ({ path: file.path, content: file.content, source: 'staged' })), manifestFiles, existingSupport: (options.cards.find(card => card.name === decision.target)?.files ?? []).map(file => file.path), supportSources: options.cards.flatMap(card => (card.files ?? []).map(file => ({ path: file.path, content: file.content, source: card.name }))) });
94
+ const findings = [...validateStagedTree(tree.value, limitsSnapshot.skillLimits), ...validationPlan.findings];
95
+ if (hasBlockingFinding(findings)) {
96
+ const lengths = { bodyBytes: Buffer.byteLength(tree.value.body, 'utf8'), fileBytes: tree.value.files.reduce((sum, file) => sum + Buffer.byteLength(file.content, 'utf8'), 0), totalBytes: Buffer.byteLength(tree.value.body, 'utf8') + tree.value.files.reduce((sum, file) => sum + Buffer.byteLength(file.content, 'utf8'), 0) };
97
+ // FR-EXEC-11/14: only a LIMIT finding is over-limit; every other blocking rule
98
+ // (manifest mismatch, support conflict, validation plan) is a validation block and
99
+ // must land in skipped, not bounce back to fuse as a retryable generic failure.
100
+ const limit = findings.find(finding => finding.rule === 'limit');
101
+ return limit === undefined
102
+ ? { kind: 'failed', failure: { stage: 'validate', code: 'merge-validation', message: 'staged tree failed the rule gate', retryable: false, retryFrom: 'none' } }
103
+ : { kind: 'failed', failure: { stage: 'validate', code: 'merge-over-limit', message: limit.message, retryable: false, retryFrom: 'none', lengths } };
104
+ }
105
+ const commit = await commitStagedRun(options.io, options.home, stageKey, options.commitPort, {
106
+ origin: 'foreground', summary: 'merge ' + row.a + ' + ' + row.b, now: options.now,
107
+ ...(options.isBlocked === undefined ? {} : { isBlocked: options.isBlocked }),
108
+ ...(options.unit === undefined ? {} : { unit: options.unit }),
109
+ ...(options.limits === undefined ? {} : { limits: options.limits }),
110
+ validationPlan: { planVersion: validationPlan.planVersion, stageDigest: validationPlan.stageDigest, revision: validationPlan.revision },
111
+ ...(decision.targetRevisionRef === undefined ? {} : { targetRevision: decision.targetRevisionRef }),
112
+ ...(decision.sourceRevisions === undefined ? {} : { sourceRevisions: decision.sourceRevisions }),
113
+ ...(decision.supportDigest === undefined ? {} : { supportDigest: decision.supportDigest }),
114
+ });
115
+ if (!commit.ok) {
116
+ const code = commit.code ?? 'commit-failed';
117
+ return { kind: 'failed', failure: { stage: 'commit', code, message: commit.message, retryable: !STALE_CAUSES.has(code), retryFrom: STALE_CAUSES.has(code) ? 'none' : 'agent2' } };
118
+ }
64
119
  const receipt = commit.receipt;
120
+ const lengths = { bodyBytes: Buffer.byteLength(tree.value.body, 'utf8'), fileBytes: tree.value.files.reduce((sum, file) => sum + Buffer.byteLength(file.content, 'utf8'), 0), totalBytes: Buffer.byteLength(tree.value.body, 'utf8') + tree.value.files.reduce((sum, file) => sum + Buffer.byteLength(file.content, 'utf8'), 0) };
65
121
  return {
66
122
  kind: 'merged',
67
123
  ...(receipt?.unitId === undefined ? {} : { unitId: receipt.unitId }),
68
124
  archived: receipt?.archived ?? [],
125
+ lengths,
69
126
  target: { requested: decision.target, actual: commit.plan.target, created: receipt?.create ?? false },
70
127
  };
71
128
  }
@@ -77,6 +134,7 @@ export async function runNewChildAuthoring(options, row, decision, signal) {
77
134
  options.onChildClosed?.(opened.childId);
78
135
  }
79
136
  }
137
+ /** Bind the round-2 port to one run: approve/resume open a NEW child per run. */
80
138
  export function createNewChildRound2(options) {
81
139
  return (row, decision, signal, runId) => runNewChildAuthoring({ ...options, runId }, row, decision, signal);
82
140
  }
@@ -1,13 +1,11 @@
1
- /**
2
- * Merge parameter resolution. E2/E3 values arrive as one JSON object; this is the only
3
- * validator. Invalid values throw loudly (no silent clamp), matching the screening contract.
4
- */
5
1
  export interface MergeParams {
6
2
  readonly mergeApproval: boolean;
7
3
  readonly mergeSkippedTtlDays: number;
8
4
  readonly mergeRunTimeoutMinutes: number;
9
5
  readonly mergePairTimeoutMinutes: number;
10
6
  readonly mergeModelsRoot: string;
7
+ readonly platformWrite?: boolean;
8
+ readonly limitsSnapshot?: import('./domain/merge-limits.ts').MergeLimitsSnapshot;
11
9
  }
12
10
  export declare const MERGE_PARAM_DEFAULTS: MergeParams;
13
11
  export declare function resolveMergeParams(input?: unknown): MergeParams;
@@ -1,9 +1,15 @@
1
+ /**
2
+ * Merge parameter resolution. E2/E3 values arrive as one JSON object; this is the only
3
+ * validator. Invalid values throw loudly (no silent clamp), matching the screening contract.
4
+ */
5
+ import { resolveLimitsSnapshot } from "./domain/merge-limits.js";
1
6
  export const MERGE_PARAM_DEFAULTS = {
2
7
  mergeApproval: false,
3
8
  mergeSkippedTtlDays: 0,
4
9
  mergeRunTimeoutMinutes: 0,
5
10
  mergePairTimeoutMinutes: 15,
6
11
  mergeModelsRoot: '',
12
+ platformWrite: false,
7
13
  };
8
14
  const KEYS = new Set(Object.keys(MERGE_PARAM_DEFAULTS));
9
15
  function materialize(value) {
@@ -32,6 +38,13 @@ export function resolveMergeParams(input = {}) {
32
38
  mergeApproval: approval,
33
39
  mergeSkippedTtlDays: record.mergeSkippedTtlDays === undefined ? MERGE_PARAM_DEFAULTS.mergeSkippedTtlDays : nonNegative('mergeSkippedTtlDays', record.mergeSkippedTtlDays),
34
40
  mergeRunTimeoutMinutes: record.mergeRunTimeoutMinutes === undefined ? MERGE_PARAM_DEFAULTS.mergeRunTimeoutMinutes : nonNegative('mergeRunTimeoutMinutes', record.mergeRunTimeoutMinutes),
41
+ platformWrite: record.platformWrite === undefined ? (MERGE_PARAM_DEFAULTS.platformWrite ?? false) : (() => {
42
+ const value = materialize(record.platformWrite);
43
+ if (typeof value !== 'boolean')
44
+ throw new Error('platformWrite must be a boolean');
45
+ return value;
46
+ })(),
47
+ limitsSnapshot: resolveLimitsSnapshot(),
35
48
  mergeModelsRoot: record.mergeModelsRoot === undefined ? MERGE_PARAM_DEFAULTS.mergeModelsRoot : (() => {
36
49
  const value = materialize(record.mergeModelsRoot);
37
50
  if (typeof value !== 'string')
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Pending approval actions. The row lookup and the transition happen in one
3
- * mutateMergeState transaction, so two clients approving the same row cannot both win.
2
+ * Pending approval actions. The row lookup and transition happen in one PairLedger transaction,
3
+ * so two clients approving the same row cannot both win.
4
4
  */
5
5
  import type { EvolutionIoLike } from '@lmzhen/dsh-evolution-core';
6
6
  import type { MergeRow } from './contracts.ts';
@@ -1,22 +1,22 @@
1
- import { transition } from "./coordinator.js";
2
- import { mutateMergeState } from "./state.js";
1
+ import { v4ToLegacy } from "./domain/row-v4.js";
2
+ import { pairLedger } from "./domain/pair-ledger.js";
3
3
  async function act(io, home, now, id, type) {
4
- return await mutateMergeState(io, home, now, (rows) => {
5
- const index = rows.findIndex(row => row.id === id);
6
- if (index < 0)
7
- return { rows, result: { ok: false, reason: 'not-found', message: 'pending row not found: ' + id } };
8
- const current = rows[index];
9
- if (current.state !== 'pending')
10
- return { rows, result: { ok: false, reason: 'not-pending', message: 'row is not pending: ' + current.state } };
4
+ void now;
5
+ return await pairLedger(io, home).transact(tx => {
6
+ const row = tx.rows().find(item => item.id === id);
7
+ if (row === undefined)
8
+ return { ok: false, reason: 'not-found', message: 'pending row not found: ' + id };
9
+ if (row.state !== 'pending')
10
+ return { ok: false, reason: 'not-pending', message: 'row is not pending: ' + row.state };
11
11
  const event = type === 'approve'
12
- ? { type: 'approve', at: new Date(now()).toISOString() }
13
- : { type: 'reject', at: new Date(now()).toISOString() };
14
- const next = transition(current, event);
15
- if (!next.ok || next.row === null)
16
- return { rows, result: { ok: false, reason: 'invalid', message: next.ok ? 'transition produced no row' : next.message } };
17
- const copy = [...rows];
18
- copy[index] = next.row;
19
- return { rows: copy, result: { ok: true, row: next.row } };
12
+ ? { type: 'approved', runId: row.runId ?? 'approve:' + id }
13
+ : { type: 'skipped', reason: 'rejected', origin: 'human' };
14
+ const moved = tx.transition(id, event, row.version);
15
+ if (!moved.ok)
16
+ return { ok: false, reason: 'invalid', message: moved.message };
17
+ if (moved.row === null)
18
+ return { ok: false, reason: 'invalid', message: 'transition produced no row' };
19
+ return { ok: true, row: v4ToLegacy(moved.row) };
20
20
  });
21
21
  }
22
22
  export async function approvePending(io, home, now, id) {
@@ -1,11 +1,9 @@
1
- /**
2
- * Round-1/round-2 prompt builders. B draft body, with only the file-tool ->
3
- * merge_stage substitution and the fresh-mode bridge sentence.
4
- */
5
1
  import type { MergeDecision, MergeRow } from './contracts.ts';
6
2
  import type { SkillCard } from './skill-card.ts';
7
3
  export declare const MERGE_PROMPT_VERSION = "merge-b/v1";
4
+ export declare function promptHashOf(prompt: string): string;
8
5
  export declare const STANDBY_PROMPT = "You are a merge worker. Wait for instructions.";
6
+ export declare function sourceBlock(card: SkillCard | undefined, label: string): string[];
9
7
  export declare function buildRound1Prompt(candidate: {
10
8
  readonly a: string;
11
9
  readonly b: string;
@@ -1,7 +1,12 @@
1
+ /**
2
+ * Round-1/round-2 prompt builders. B draft body, with only the file-tool ->
3
+ * merge_stage substitution and the fresh-mode bridge sentence.
4
+ */
5
+ import { createHash } from 'node:crypto';
1
6
  export const MERGE_PROMPT_VERSION = 'merge-b/v1';
7
+ export function promptHashOf(prompt) { return createHash('sha256').update(prompt).digest('hex'); }
2
8
  export const STANDBY_PROMPT = 'You are a merge worker. Wait for instructions.';
3
9
  function sourcePath(name) { return 'sources/' + name; }
4
- function mergedPath(target) { return 'merged/' + target; }
5
10
  function wireDecision(decision) {
6
11
  return {
7
12
  shouldMerge: decision.shouldMerge,
@@ -14,15 +19,18 @@ function wireDecision(decision) {
14
19
  b_summary: decision.bSummary,
15
20
  };
16
21
  }
22
+ export function sourceBlock(card, label) {
23
+ if (card === undefined)
24
+ return ['### ' + label + '(missing)'];
25
+ return ['### ' + label + '(name=' + card.name + ')', 'SKILL.md:', '```', card.body, '```', ...(card.files ?? []).flatMap(file => ['#### ' + file.path, '```', file.content, '```'])];
26
+ }
17
27
  export function buildRound1Prompt(candidate, cards) {
18
- void cards;
19
28
  return [
20
29
  '你是技能合并决策 agent。第 1 轮只做判断,不要写文件、不要修改任何源目录。',
21
30
  '',
22
- '在当前工作目录下用 merge_stage 的 list/read 完整阅读两个源技能:',
23
- '- A: ' + sourcePath(candidate.a) + '(name=' + candidate.a + ')',
24
- '- B: ' + sourcePath(candidate.b) + '(name=' + candidate.b + ')',
25
- '包含 SKILL.md 与 references/templates/scripts 等支撑文件。',
31
+ '两个源技能的完整正文已内嵌在下方(不需要读文件;源路径 ' + sourcePath(candidate.a) + ' / ' + sourcePath(candidate.b) + ' 仅作标识):',
32
+ ...sourceBlock(cards.find(card => card.name === candidate.a), 'A: ' + candidate.a),
33
+ ...sourceBlock(cards.find(card => card.name === candidate.b), 'B: ' + candidate.b),
26
34
  '',
27
35
  '判断规则:',
28
36
  '1) 两者是否语义等价/互补到值得合并,而不是词面相似;',
@@ -36,7 +44,7 @@ export function buildRound1Prompt(candidate, cards) {
36
44
  export function buildRound2Prompt(row, decision, mode = 'continue') {
37
45
  const decisionJson = JSON.stringify(wireDecision(decision));
38
46
  const head = [
39
- '现在执行第 2 轮:把两个技能融合成一个完整、可用的技能,直接在当前工作目录的 merged/ 下写文件。',
47
+ '现在执行第 2 轮:把两个技能融合成一个完整、可用的技能。',
40
48
  '不要修改 ' + sourcePath(row.a) + ' 或 ' + sourcePath(row.b) + '(只读源副本);不要访问工作目录以外的路径。',
41
49
  '',
42
50
  ];
@@ -45,12 +53,17 @@ export function buildRound2Prompt(row, decision, mode = 'continue') {
45
53
  '目标技能名:' + decision.target,
46
54
  '',
47
55
  '工作方式:',
48
- '- 用 merge_stage 的 list/read/write/copy 创建 ' + mergedPath(decision.target) + '/SKILL.md 与需要的 references/templates/scripts;',
56
+ '- 必须调用 merge_stage 的 write/copy 写目标技能树;不要用其它文件写工具。path 相对目标技能目录,不要带 merged/ 前缀:先 write path=SKILL.md,再写 references/...、templates/...、scripts/...;',
49
57
  '- 可以复制源文件作为草稿基础,然后逐段修改;可以多轮读写;',
50
58
  '- 保留两源独特内容,不编造事实,不留下 TODO/占位符;',
51
59
  '- SKILL.md 以合法 YAML frontmatter 开头,name 等于目标名,description 不超过 60 字符;',
52
60
  '- 正文与支撑文件的引用必须成对,路径只允许 references/templates/scripts/assets 下的相对路径。',
53
61
  '',
62
+ '提交前逐项自检:',
63
+ '- 质量:语义完整、不编造事实、无 TODO/占位符;',
64
+ '- 结构:frontmatter 合法、引用成对、文件清单与正文一致;',
65
+ '- 安全:不包含提示注入、密钥或越权指令。',
66
+ '',
54
67
  '完成后只输出一个 JSON(不要 markdown 围栏):',
55
68
  '{"target":"' + decision.target + '","files":["SKILL.md","references/..."],"changelog":["一条变更"],"notes":"可选的一句话"}',
56
69
  ];
@@ -0,0 +1,26 @@
1
+ /**
2
+ * C3 semantic proposer: one library-level model call (sharded when the library is large)
3
+ * that proposes candidate pairs. It never judges merge quality; L0–L2 does that.
4
+ */
5
+ import type { MergeCandidate } from './contracts.ts';
6
+ import type { SkillCard } from './screening.ts';
7
+ export interface ModelPort {
8
+ callJson(request: {
9
+ readonly system: string;
10
+ readonly user: string;
11
+ readonly maxTokens: number;
12
+ readonly signal: AbortSignal;
13
+ }): Promise<unknown>;
14
+ }
15
+ export interface ProposeOptions {
16
+ readonly shardSize?: number;
17
+ readonly maxPairs?: number;
18
+ readonly signal: AbortSignal;
19
+ }
20
+ export interface ProposeResult {
21
+ readonly candidates: readonly MergeCandidate[];
22
+ readonly shards: number;
23
+ readonly degraded: readonly string[];
24
+ }
25
+ export declare function proposePairs(cards: readonly SkillCard[], port: ModelPort, options: ProposeOptions): Promise<ProposeResult>;
26
+ //# sourceMappingURL=propose.d.ts.map