@lmzhen/dsh-evolution-merge 0.30.16 → 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 +2325 -855
  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 +116 -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 +72 -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
@@ -0,0 +1,68 @@
1
+ import { loadMergeState } from "./state.js";
2
+ import { MERGE_COMMAND_SURFACE } from "./command-surface.js";
3
+ const queueOf = (row) => row.state === 'approved' ? 'fuse' : row.queue;
4
+ const isExpired = (row) => row.expired !== undefined;
5
+ export function countsOf(rows) {
6
+ const active = { verify: 0, pending: 0, fuse: 0, skipped: 0 };
7
+ let expired = 0;
8
+ for (const row of rows) {
9
+ if (isExpired(row)) {
10
+ expired += 1;
11
+ continue;
12
+ }
13
+ active[queueOf(row)] += 1;
14
+ }
15
+ return { active, expired };
16
+ }
17
+ export function toRunView(record, readAt) {
18
+ const state = (record.mergeStatus ?? record.state);
19
+ return {
20
+ wireVersion: 1,
21
+ readAt,
22
+ id: record.id,
23
+ state,
24
+ command: record.command ?? 'scan',
25
+ ...(record.stage === undefined ? {} : { stage: record.stage }),
26
+ progress: record.progress === undefined ? null : { done: record.progress.done, total: record.progress.total },
27
+ startedAt: record.startedAt,
28
+ ...(record.endedAt === undefined ? {} : { endedAt: record.endedAt }),
29
+ ...(record.failure === undefined ? {} : { failure: { stage: 'settle', code: 'run-failed', retryable: false, retryFrom: 'none', detail: record.failure } }),
30
+ ...(record.parentRunId === undefined ? {} : { parentRunId: record.parentRunId }),
31
+ ...(record.pairKey === undefined ? {} : { currentPairKey: record.pairKey }),
32
+ actions: [
33
+ { id: 'status', command: '/evolution merge status ' + record.id, confirm: false, enabled: true },
34
+ { id: 'report', command: '/evolution merge report ' + record.id, confirm: false, enabled: record.resultRef !== undefined },
35
+ { id: 'cancel', command: '/evolution merge cancel ' + record.id, confirm: false, enabled: state === 'running' },
36
+ ],
37
+ };
38
+ }
39
+ /** GET projection: reads state + runs; never writes. */
40
+ export async function readFacts(options) {
41
+ const loaded = await loadMergeState(options.io, options.home);
42
+ return buildFacts({
43
+ rows: loaded.state.rows,
44
+ runs: options.runs,
45
+ approval: options.approval,
46
+ readAt: options.readAt,
47
+ notes: loaded.ok ? [] : [loaded.note ?? 'state-unreadable'],
48
+ });
49
+ }
50
+ export function buildFacts(input) {
51
+ const ordered = [...input.runs].sort((a, b) => b.startedAt - a.startedAt);
52
+ const active = ordered.find(run => (run.mergeStatus ?? run.state) === 'running');
53
+ const last = ordered.find(run => (run.mergeStatus ?? run.state) !== 'running');
54
+ const notes = input.notes ?? [];
55
+ return {
56
+ wireVersion: 1,
57
+ readAt: input.readAt,
58
+ approval: { mergeApproval: input.approval },
59
+ counts: countsOf(input.rows),
60
+ active: active === undefined ? null : toRunView(active, input.readAt),
61
+ last: last === undefined ? null : toRunView(last, input.readAt),
62
+ startCommands: { scan: '/evolution merge scan', pair: '/evolution merge pair <a> <b>' },
63
+ fallback: MERGE_COMMAND_SURFACE.fallback,
64
+ notes,
65
+ degraded: notes.length > 0,
66
+ };
67
+ }
68
+ //# sourceMappingURL=facts.js.map
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Gate filter: turn lexical/proposed candidates into the candidates scan may process.
3
+ * L0 keeps are dropped unless the deterministic 10% audit re-runs L2 and overrides.
4
+ */
5
+ import type { MergeCandidate } from './contracts.ts';
6
+ import { type GateCache, type GateFacts, type GateModelPort, type GateVerdict } from './ladder.ts';
7
+ export interface GateFilterOptions {
8
+ readonly facts: readonly GateFacts[];
9
+ readonly model: GateModelPort;
10
+ readonly modelName: string;
11
+ readonly signal: AbortSignal;
12
+ readonly runSeed: string;
13
+ readonly cache?: GateCache;
14
+ readonly auditRate?: number;
15
+ }
16
+ export interface GateDecision {
17
+ readonly candidate: MergeCandidate;
18
+ readonly verdicts: readonly GateVerdict[];
19
+ readonly auditOverride: boolean;
20
+ }
21
+ export interface GateFilterResult {
22
+ readonly kept: readonly GateDecision[];
23
+ readonly dropped: readonly GateDecision[];
24
+ readonly errors: readonly {
25
+ readonly candidate: MergeCandidate;
26
+ readonly message: string;
27
+ }[];
28
+ readonly calls: number;
29
+ }
30
+ export declare function filterCandidates(candidates: readonly MergeCandidate[], options: GateFilterOptions): Promise<GateFilterResult>;
31
+ //# sourceMappingURL=gate-filter.d.ts.map
@@ -0,0 +1,64 @@
1
+ import { pairKey } from "./contracts.js";
2
+ import { runLadder, selectedForAudit } from "./ladder.js";
3
+ function withGate(candidate, verdicts, auditOverride) {
4
+ const last = verdicts[verdicts.length - 1];
5
+ if (last === undefined)
6
+ return candidate;
7
+ return {
8
+ ...candidate,
9
+ gate: {
10
+ level: last.level,
11
+ verdict: last.verdict,
12
+ reason: last.reason,
13
+ ...(last.cached ? { cached: true } : {}),
14
+ ...(auditOverride ? { auditOverride: true } : {}),
15
+ },
16
+ };
17
+ }
18
+ export async function filterCandidates(candidates, options) {
19
+ const kept = [];
20
+ const dropped = [];
21
+ const errors = [];
22
+ let calls = 0;
23
+ for (const candidate of candidates) {
24
+ const first = await runLadder({
25
+ level: 'L0', candidate, facts: options.facts, model: options.model,
26
+ modelName: options.modelName, signal: options.signal,
27
+ ...(options.cache === undefined ? {} : { cache: options.cache }),
28
+ });
29
+ calls += first.calls;
30
+ if (first.route === 'error') {
31
+ errors.push({ candidate, message: first.error ?? 'gate error' });
32
+ continue;
33
+ }
34
+ if (first.route !== 'keep' || first.droppedAt !== 'L0') {
35
+ if (first.route === 'merge' || first.route === 'undecided')
36
+ kept.push({ candidate: withGate(candidate, first.verdicts, false), verdicts: first.verdicts, auditOverride: false });
37
+ else
38
+ dropped.push({ candidate, verdicts: first.verdicts, auditOverride: false });
39
+ continue;
40
+ }
41
+ if (!selectedForAudit(pairKey(candidate.a, candidate.b), options.runSeed, options.auditRate ?? 0.1)) {
42
+ dropped.push({ candidate, verdicts: first.verdicts, auditOverride: false });
43
+ continue;
44
+ }
45
+ const audit = await runLadder({
46
+ level: 'L0', startLevel: 'L2', candidate, facts: options.facts, model: options.model,
47
+ modelName: options.modelName, signal: options.signal,
48
+ ...(options.cache === undefined ? {} : { cache: options.cache }),
49
+ });
50
+ calls += audit.calls;
51
+ if (audit.route === 'merge' || audit.route === 'undecided') {
52
+ const all = [...first.verdicts, ...audit.verdicts];
53
+ kept.push({ candidate: withGate(candidate, all, true), verdicts: all, auditOverride: true });
54
+ }
55
+ else if (audit.route === 'error') {
56
+ errors.push({ candidate, message: audit.error ?? 'audit gate error' });
57
+ }
58
+ else {
59
+ dropped.push({ candidate, verdicts: [...first.verdicts, ...audit.verdicts], auditOverride: true });
60
+ }
61
+ }
62
+ return { kept, dropped, errors, calls };
63
+ }
64
+ //# sourceMappingURL=gate-filter.js.map
@@ -22,7 +22,11 @@ export interface MergePresentationScope {
22
22
  }, owner?: unknown) => unknown;
23
23
  };
24
24
  }
25
- export declare function engineConfigFromEnv(configuredRoot?: string): Record<string, unknown> | undefined;
25
+ export interface EngineModelConfig {
26
+ readonly embedder_model: string;
27
+ readonly embedder_model_last: string;
28
+ }
29
+ export declare function engineConfigFromRoot(configuredRoot?: string): EngineModelConfig | undefined;
26
30
  export interface MergePresentationContext {
27
31
  inject(names: string[], run: (scope: MergePresentationScope) => void): void;
28
32
  readonly fiber?: unknown;
@@ -69,7 +73,6 @@ export * from './new-child-authoring.ts';
69
73
  export * from './status.ts';
70
74
  export * from './skipped.ts';
71
75
  export * from './read-commands.ts';
72
- export * from './routes.ts';
73
76
  export * from './merge-routes.ts';
74
77
  export * from './merge-command.ts';
75
78
  export * from './params.ts';
@@ -4,13 +4,13 @@ import { screen as engineScreen } from '@lmzhen/dsh-evolution-merge-engine';
4
4
  import { selectWithErrors } from "./adapters/select.js";
5
5
  import { createEngineScreening } from "./adapters/engine-screen.js";
6
6
  import { createEngineRunner } from "./adapters/model-runner.js";
7
- import { authorityFrom, createSubagentChannel, DEFAULT_SKILL_LIMITS, evolutionHome, evolutionIoAdapter, newRunRegistry, newSkillLibrary, policyStageLimits, skillsRoot } from '@lmzhen/dsh-evolution-core';
7
+ import { authorityFrom, createSubagentChannel, DEFAULT_SKILL_LIMITS, evolutionHome, evolutionIoAdapter, newRunRegistry, newSkillLibrary, policyStageLimits } from '@lmzhen/dsh-evolution-core';
8
8
  import { registerHostRoutes } from '@lmzhen/dsh-evolution-core/http-routes';
9
- import { loadMergeState, upsertRow } from "./state.js";
9
+ import { loadMergeState, transactRows } from "./state.js";
10
10
  import { cleanupExpiredStaging } from "./staging.js";
11
11
  import { recoverCommitJournals } from "./commit-recovery.js";
12
12
  import { readCommitReceipts } from "./commit-receipt.js";
13
- import { transition } from "./coordinator.js";
13
+ import { pairKey, rowIdOf } from "./contracts.js";
14
14
  import { makeMergeRoutes } from "./merge-routes.js";
15
15
  import { createMergeCommand, runMergeCommand } from "./merge-command.js";
16
16
  import { createMergeRuntime } from "./runtime.js";
@@ -27,15 +27,11 @@ export const Config = z.object({
27
27
  mergePairTimeoutMinutes: z.number().min(1).default(15),
28
28
  mergeModelsRoot: z.string().default(''),
29
29
  });
30
- export function engineConfigFromEnv(configuredRoot) {
31
- const root = configuredRoot !== undefined && configuredRoot !== '' ? configuredRoot : process.env.SKILL_MERGE_MODELS_ROOT;
32
- if (root === undefined || root === '')
30
+ export function engineConfigFromRoot(configuredRoot) {
31
+ const root = configuredRoot?.trim() ?? '';
32
+ if (root === '')
33
33
  return undefined;
34
34
  return {
35
- models_dir: '',
36
- offline: true,
37
- auto_provision: false,
38
- embed_backend: 'local',
39
35
  embedder_model: join(root, 'potion-multilingual-128M-int8'),
40
36
  embedder_model_last: join(root, 'potion-multilingual-128M-pca64-int8'),
41
37
  };
@@ -91,7 +87,6 @@ export * from "./new-child-authoring.js";
91
87
  export * from "./status.js";
92
88
  export * from "./skipped.js";
93
89
  export * from "./read-commands.js";
94
- export * from "./routes.js";
95
90
  export * from "./merge-routes.js";
96
91
  export * from "./merge-command.js";
97
92
  export * from "./params.js";
@@ -123,7 +118,6 @@ export function apply(ctx, rawConfig = {}) {
123
118
  const home = evolutionHome();
124
119
  void loadMergeState(io, home).then(loaded => cleanupExpiredStaging(io, home, Date.now(), new Set(loaded.state.rows.map(row => row.runId).filter((id) => id !== undefined)))).catch((error) => { ctx.logger.warn('merge staging cleanup failed: ' + (error instanceof Error ? error.message : String(error))); });
125
120
  const runs = newRunRegistry({ io, home, warn: (message) => { ctx.logger.warn(message); } });
126
- void runs.load();
127
121
  ctx.effect(() => {
128
122
  const host = ctx;
129
123
  if (typeof host.on !== 'function')
@@ -138,18 +132,14 @@ export function apply(ctx, rawConfig = {}) {
138
132
  io, ctx, authority: authorityFrom({ origin: 'subagent' }),
139
133
  limits: { ...DEFAULT_SKILL_LIMITS, ...policyStageLimits(policySnapshot?.get?.()) },
140
134
  });
141
- const mergeSkillsRoot = skills.root ?? skillsRoot();
142
- void recoverCommitJournals(io, home, Date.now(), mergeSkillsRoot, async (name) => {
143
- const result = await skills.restoreFromArchive(name);
144
- return result.ok;
145
- }, (runId) => {
135
+ void runs.load().then(() => recoverCommitJournals(io, home, Date.now(), (runId) => {
146
136
  const run = runs.find(runId);
147
137
  return run !== undefined && run.state === 'running';
148
- }).then((reports) => { if (reports.length > 0)
138
+ })).then((reports) => { if (reports.length > 0)
149
139
  ctx.logger.info('merge commit recovery: ' + reports.map(report => report.runId + '=' + report.action).join(', ')); }).catch((error) => { ctx.logger.warn('merge commit recovery failed: ' + (error instanceof Error ? error.message : String(error))); });
150
140
  const channel = subagentChannelAdapter(createSubagentChannel(ctx));
151
141
  const stage = new StageToolRegistry();
152
- const engineConfig = engineConfigFromEnv(params.mergeModelsRoot);
142
+ const engineConfig = engineConfigFromRoot(params.mergeModelsRoot);
153
143
  const llm = ctx.get('llm');
154
144
  const defaultModel = ctx.get('agentDefaultModel');
155
145
  const runner = llm === undefined || defaultModel === undefined
@@ -160,7 +150,7 @@ export function apply(ctx, rawConfig = {}) {
160
150
  home,
161
151
  engine: { screen: engineScreen },
162
152
  select: selectWithErrors,
163
- cfg: { ...(engineConfig ?? {}), judge: 'deepseek' },
153
+ cfg: { ...engineConfig, judge: 'deepseek' },
164
154
  ...(runner === undefined ? {} : { runner }),
165
155
  async onSelection(selection) {
166
156
  const loaded = await loadMergeState(io, home);
@@ -168,12 +158,12 @@ export function apply(ctx, rawConfig = {}) {
168
158
  for (const skipped of selection.skipped) {
169
159
  if (loaded.state.rows.some(row => row.pairKey === skipped.pairKey))
170
160
  continue;
171
- const next = transition(undefined, {
172
- type: 'gate-keep', a: skipped.a, b: skipped.b,
173
- ...(skipped.reason === undefined ? {} : { reason: skipped.reason }), at,
174
- });
175
- if (next.ok && next.row !== null)
176
- await upsertRow(io, home, () => Date.now(), next.row);
161
+ const key = pairKey(skipped.a, skipped.b);
162
+ await transactRows(io, home, tx => tx.createSkipped({
163
+ id: rowIdOf(key), pairKey: key, a: skipped.a, b: skipped.b, version: 0,
164
+ createdAt: at, updatedAt: at, origin: 'agent',
165
+ state: 'skipped', reason: skipped.reason ?? 'gate-keep', attempts: 0,
166
+ }).ok);
177
167
  }
178
168
  },
179
169
  });
@@ -57,7 +57,7 @@ function payloadFor(level, candidate, facts) {
57
57
  }
58
58
  const SYSTEM = 'Judge whether two skill documents describe the same reusable capability. Shared vocabulary or a shared preamble is not duplication. Return only strict JSON: {"verdict":1|0|-1,"reason":"..."}';
59
59
  function extractJson(text) {
60
- const fenced = /\`\`\`(?:json)?\s*([\s\S]*?)\`\`\`/i.exec(text);
60
+ const fenced = /`\``(?:json)?\s*([\s\S]*?)`\``/i.exec(text);
61
61
  const body = fenced?.[1] ?? text;
62
62
  const start = body.indexOf('{');
63
63
  if (start < 0)
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Four read-only merge routes: view / rows / run / runs. The host loads rows and runs; these
3
- * handlers only project. A missing merge service answers 503 instead of pretending empty.
2
+ * Four read-only merge routes: view / rows / body / history.
3
+ * The host loads rows and runs; these handlers only project wire envelopes.
4
4
  */
5
5
  import type { RunRecord } from '@lmzhen/dsh-evolution-core';
6
6
  import { type ConnectionFetchRouteLike, type DecisionAudit } from '@lmzhen/dsh-evolution-core/http-routes';
@@ -1,6 +1,6 @@
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 } from "./facts.js";
3
+ import { toBodyView, toHistoryView, toQueuePage } from "./wire-projection.js";
4
4
  const MERGE_PATHS = HOST_ROUTES.merge;
5
5
  const MERGE_BASE = MERGE_PATHS.view.slice(0, -'view'.length);
6
6
  export const MERGE_ROUTES = {
@@ -10,7 +10,8 @@ export const MERGE_ROUTES = {
10
10
  history: (MERGE_PATHS.history ?? MERGE_BASE + 'history'),
11
11
  };
12
12
  export function makeMergeRoutes(deps, _audit) {
13
- const absent = () => json(503, { ok: false, error: { code: 'merge-service-absent', message: 'merge service is not mounted' } });
13
+ const absent = () => json(404, { wireVersion: 1, code: 'E-325', message: 'merge service is not mounted' });
14
+ const bad = (message) => json(400, { wireVersion: 1, code: 'merge-bad-query', message });
14
15
  const get = (request) => (request.method === 'GET' ? null : methodNotAllowed(['GET']));
15
16
  const url = (request) => new URL(request.url, 'http://local');
16
17
  return [
@@ -25,12 +26,10 @@ export function makeMergeRoutes(deps, _audit) {
25
26
  const source = deps.getSource();
26
27
  if (source === undefined)
27
28
  return absent();
29
+ const readAt = Date.now();
30
+ const rows = await source.rows();
28
31
  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
- });
32
+ return json(200, buildFacts({ rows, runs, approval: source.approval?.() === true, readAt }));
34
33
  },
35
34
  },
36
35
  {
@@ -46,15 +45,16 @@ export function makeMergeRoutes(deps, _audit) {
46
45
  return absent();
47
46
  const query = url(request).searchParams;
48
47
  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);
48
+ if (queue !== 'verify' && queue !== 'pending' && queue !== 'fuse' && queue !== 'skipped')
49
+ return bad('queue must be verify/pending/fuse/skipped');
50
+ const expiredOnly = query.get('expired') === '1';
51
+ if (expiredOnly && queue !== 'pending')
52
+ return bad('expired is only allowed for pending');
53
53
  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
- });
54
+ const offset = Number.parseInt(query.get('cursor') ?? '0', 10);
55
+ const rows = await source.rows();
56
+ const page = toQueuePage(rows, queue, expiredOnly, Date.now(), Number.isFinite(limit) ? limit : 50);
57
+ return json(200, Number.isFinite(offset) && offset > 0 ? { ...page, rows: page.rows.slice(offset) } : page);
58
58
  },
59
59
  },
60
60
  {
@@ -71,13 +71,12 @@ export function makeMergeRoutes(deps, _audit) {
71
71
  const query = url(request).searchParams;
72
72
  const id = query.get('id');
73
73
  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' } });
76
- }
74
+ if (id === null || id === '' || (side !== 'a' && side !== 'b'))
75
+ return bad('id and side=a|b are required');
77
76
  const content = await source.body(id, side);
78
77
  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) } });
78
+ ? json(200, { wireVersion: 1, code: 'merge-body-missing', message: 'merge body not found: ' + id })
79
+ : json(200, toBodyView(id, side === 'a' ? 'live' : 'archive', content, Date.now()));
81
80
  },
82
81
  },
83
82
  {
@@ -91,7 +90,7 @@ export function makeMergeRoutes(deps, _audit) {
91
90
  const source = deps.getSource();
92
91
  if (source === undefined)
93
92
  return absent();
94
- return json(200, { ok: true, data: { items: await source.history() } });
93
+ return json(200, toHistoryView(await source.history(), Date.now()));
95
94
  },
96
95
  },
97
96
  ];
@@ -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.mergeStatus ?? 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,8 +21,10 @@ 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;
26
28
  }
27
29
  export declare function runNewChildAuthoring(options: NewChildAuthoringOptions, row: MergeRow, decision: MergeDecision, signal: AbortSignal): Promise<ScanRound2Outcome>;
28
30
  export declare function createNewChildRound2(options: Omit<NewChildAuthoringOptions, 'runId'>): (row: MergeRow, decision: MergeDecision, signal: AbortSignal, runId: string) => Promise<ScanRound2Outcome>;
@@ -6,8 +6,15 @@ import { validateStagedTree, hasBlockingFinding } from "./rules.js";
6
6
  import { commitStagedRun } from "./commit-execute.js";
7
7
  export async function runNewChildAuthoring(options, row, decision, signal) {
8
8
  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' } };
9
+ const createMode = decision.mode === 'create-umbrella';
10
+ if (!createMode && targetCard !== undefined) {
11
+ if (targetCard.managed !== true)
12
+ return { kind: 'failed', failure: { stage: 'author', code: 'target-not-managed', message: 'target is not family-managed: ' + decision.target, retryable: false, retryFrom: 'none' } };
13
+ if (targetCard.protectionUnknown === true || targetCard.protectedBy != null)
14
+ return { kind: 'failed', failure: { stage: 'author', code: 'target-protected', message: 'target is protected: ' + decision.target, retryable: false, retryFrom: 'none' } };
15
+ }
16
+ if (!createMode && await options.isBlocked?.(decision.target) === true)
17
+ return { kind: 'failed', failure: { stage: 'author', code: 'target-blocked', message: 'target is blocked by curator: ' + decision.target, retryable: false, retryFrom: 'none' } };
11
18
  const stageKey = row.stagingRef ?? stageKeyOf(options.runId, pairKey(row.a, row.b));
12
19
  const existing = await readStaging(options.io, options.home, stageKey);
13
20
  if (existing.kind === 'present') {
@@ -58,7 +65,11 @@ export async function runNewChildAuthoring(options, row, decision, signal) {
58
65
  const findings = validateStagedTree(tree.value);
59
66
  if (hasBlockingFinding(findings))
60
67
  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 });
68
+ const commit = await commitStagedRun(options.io, options.home, stageKey, options.commitPort, {
69
+ origin: 'foreground', summary: 'merge ' + row.a + ' + ' + row.b, now: options.now,
70
+ ...(options.isBlocked === undefined ? {} : { isBlocked: options.isBlocked }),
71
+ ...(options.unit === undefined ? {} : { unit: options.unit }),
72
+ });
62
73
  if (!commit.ok)
63
74
  return { kind: 'failed', failure: { stage: 'commit', code: 'commit-failed', message: commit.message, retryable: true, retryFrom: 'agent2' } };
64
75
  const receipt = commit.receipt;
@@ -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 v3
3
+ * transactRows transaction, 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 { v3ToLegacy } from "./domain/row-v3-bridge.js";
2
+ import { transactRows } from "./state.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 transactRows(io, home, 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: v3ToLegacy(moved.row) };
20
20
  });
21
21
  }
22
22
  export async function approvePending(io, home, now, id) {
@@ -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