@lmzhen/dsh-evolution-merge 0.28.0 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -18
- package/lib/index.js +1583 -1209
- package/lib/types/adapters/channel.js +29 -0
- package/lib/types/adapters/engine-screen.d.ts +55 -0
- package/lib/types/adapters/engine-screen.js +45 -0
- package/lib/types/adapters/library.js +14 -0
- package/lib/types/adapters/model-runner.d.ts +28 -0
- package/lib/types/adapters/model-runner.js +34 -0
- package/lib/types/adapters/select.d.ts +3 -0
- package/lib/types/adapters/select.js +35 -0
- package/lib/types/adapters/undo.js +35 -0
- package/lib/types/approve-command.js +42 -0
- package/lib/types/authoring-flow.d.ts +7 -2
- package/lib/types/authoring-flow.js +48 -0
- package/lib/types/channel-port.d.ts +2 -1
- package/lib/types/channel-port.js +74 -0
- package/lib/types/commands.d.ts +9 -0
- package/lib/types/commands.js +79 -0
- package/lib/types/commit-assembly.d.ts +2 -0
- package/lib/types/commit-assembly.js +48 -0
- package/lib/types/commit-execute.js +52 -0
- package/lib/types/commit-plan.js +56 -0
- package/lib/types/commit-receipt.d.ts +6 -1
- package/lib/types/commit-receipt.js +85 -0
- package/lib/types/contracts.d.ts +21 -7
- package/lib/types/contracts.js +57 -0
- package/lib/types/coordinator.d.ts +11 -0
- package/lib/types/coordinator.js +131 -0
- package/lib/types/decision.js +61 -0
- package/lib/types/doctor.d.ts +1 -1
- package/lib/types/doctor.js +50 -0
- package/lib/types/domain/claim.d.ts +10 -0
- package/lib/types/domain/claim.js +12 -0
- package/lib/types/domain/commit-journal-store.d.ts +6 -0
- package/lib/types/domain/commit-journal-store.js +22 -0
- package/lib/types/domain/commit-journal.d.ts +16 -0
- package/lib/types/domain/commit-journal.js +25 -0
- package/lib/types/domain/errors.d.ts +10 -0
- package/lib/types/domain/errors.js +7 -0
- package/lib/types/domain/failure-route.d.ts +25 -0
- package/lib/types/domain/failure-route.js +22 -0
- package/lib/types/domain/indexes.d.ts +21 -0
- package/lib/types/domain/indexes.js +15 -0
- package/lib/types/domain/ledger.d.ts +17 -0
- package/lib/types/domain/ledger.js +65 -0
- package/lib/types/domain/markdown.d.ts +2 -0
- package/lib/types/domain/markdown.js +57 -0
- package/lib/types/domain/migrate-v1.d.ts +41 -0
- package/lib/types/domain/migrate-v1.js +23 -0
- package/lib/types/domain/pair-index-store.d.ts +6 -0
- package/lib/types/domain/pair-index-store.js +25 -0
- package/lib/types/domain/pair-index.d.ts +9 -0
- package/lib/types/domain/pair-index.js +28 -0
- package/lib/types/domain/resolve-pair.d.ts +11 -0
- package/lib/types/domain/resolve-pair.js +17 -0
- package/lib/types/domain/row.d.ts +113 -0
- package/lib/types/domain/row.js +111 -0
- package/lib/types/domain/run-record.d.ts +45 -0
- package/lib/types/domain/run-record.js +24 -0
- package/lib/types/domain/state.d.ts +57 -0
- package/lib/types/domain/state.js +65 -0
- package/lib/types/gate-cache.js +36 -0
- package/lib/types/index.d.ts +6 -7
- package/lib/types/index.js +241 -0
- package/lib/types/ladder.js +162 -0
- package/lib/types/merge-command.d.ts +3 -0
- package/lib/types/merge-command.js +43 -0
- package/lib/types/merge-routes.d.ts +15 -4
- package/lib/types/merge-routes.js +99 -0
- package/lib/types/merge-tool.js +69 -0
- package/lib/types/new-child-authoring.d.ts +1 -1
- package/lib/types/new-child-authoring.js +83 -0
- package/lib/types/pair-command.js +18 -0
- package/lib/types/params.d.ts +1 -0
- package/lib/types/params.js +49 -0
- package/lib/types/pending-actions.js +28 -0
- package/lib/types/prompts.d.ts +7 -5
- package/lib/types/prompts.js +60 -0
- package/lib/types/read-commands.js +32 -0
- package/lib/types/report.js +70 -0
- package/lib/types/resume-command.js +57 -0
- package/lib/types/round2-outcome.js +50 -0
- package/lib/types/routes.d.ts +20 -2
- package/lib/types/routes.js +75 -0
- package/lib/types/rules.js +55 -0
- package/lib/types/run-shell.d.ts +7 -0
- package/lib/types/run-shell.js +69 -0
- package/lib/types/runtime.d.ts +5 -4
- package/lib/types/runtime.js +269 -0
- package/lib/types/scan-command.d.ts +1 -0
- package/lib/types/scan-command.js +30 -0
- package/lib/types/scan-ports.d.ts +1 -1
- package/lib/types/scan-ports.js +93 -0
- package/lib/types/scan-run.d.ts +4 -0
- package/lib/types/scan-run.js +186 -0
- package/lib/types/skill-card.d.ts +12 -0
- package/lib/types/skill-card.js +2 -0
- package/lib/types/skipped.js +24 -0
- package/lib/types/stage-tool-registry.js +32 -0
- package/lib/types/stage-tool.js +31 -0
- package/lib/types/staged-tree.js +75 -0
- package/lib/types/staging-files.js +86 -0
- package/lib/types/staging.d.ts +13 -0
- package/lib/types/staging.js +149 -0
- package/lib/types/state.js +103 -0
- package/lib/types/status.d.ts +2 -1
- package/lib/types/status.js +28 -0
- package/lib/types/supersede.js +14 -0
- package/lib/types/undo-merge.js +23 -0
- package/package.json +4 -7
- package/lib/types/adapters/model.d.ts +0 -31
- package/lib/types/gate-filter.d.ts +0 -31
- package/lib/types/propose.d.ts +0 -26
- package/lib/types/screening-port.d.ts +0 -47
- package/lib/types/screening.d.ts +0 -30
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { isPresent } from '@lmzhen/dsh-evolution-core';
|
|
2
|
+
import { readStaging } from "./staging.js";
|
|
3
|
+
import { readStagedTree } from "./staged-tree.js";
|
|
4
|
+
import { buildCommitPlan } from "./commit-plan.js";
|
|
5
|
+
export async function assembleStagedCommit(io, home, runId, port, options) {
|
|
6
|
+
const staging = await readStaging(io, home, runId);
|
|
7
|
+
if (!isPresent(staging))
|
|
8
|
+
return { ok: false, message: 'staging is not present for run ' + runId };
|
|
9
|
+
const tree = await readStagedTree(io, home, runId);
|
|
10
|
+
if (!isPresent(tree))
|
|
11
|
+
return { ok: false, message: 'merged tree is not readable for run ' + runId };
|
|
12
|
+
const requested = staging.value.manifest.target;
|
|
13
|
+
let target = requested;
|
|
14
|
+
let targetCurrent = await port.readSkill(requested);
|
|
15
|
+
const requestedArchived = targetCurrent === null && await port.isArchived?.(requested) === true;
|
|
16
|
+
if (staging.value.manifest.mode === 'create-umbrella' && (targetCurrent !== null || requestedArchived)) {
|
|
17
|
+
target = requested;
|
|
18
|
+
for (let suffix = 2; suffix <= 99; suffix += 1) {
|
|
19
|
+
const candidate = requested + '-v' + String(suffix);
|
|
20
|
+
if (await port.readSkill(candidate) === null
|
|
21
|
+
&& await port.isArchived?.(candidate) !== true) {
|
|
22
|
+
target = candidate;
|
|
23
|
+
targetCurrent = null;
|
|
24
|
+
break;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (target === requested)
|
|
28
|
+
return { ok: false, message: 'no free umbrella target for ' + requested };
|
|
29
|
+
}
|
|
30
|
+
const sources = [];
|
|
31
|
+
for (const name of staging.value.manifest.sources) {
|
|
32
|
+
const body = await port.readSkill(name);
|
|
33
|
+
if (body === null)
|
|
34
|
+
return { ok: false, message: 'source skill is not live: ' + name };
|
|
35
|
+
sources.push({ name, body });
|
|
36
|
+
}
|
|
37
|
+
return buildCommitPlan({
|
|
38
|
+
target,
|
|
39
|
+
create: targetCurrent === null,
|
|
40
|
+
mergedBody: tree.value.body,
|
|
41
|
+
mergedFiles: tree.value.files,
|
|
42
|
+
targetCurrent,
|
|
43
|
+
sourceBodies: sources,
|
|
44
|
+
origin: options.origin,
|
|
45
|
+
summary: options.summary,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=commit-assembly.js.map
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { assembleStagedCommit } from "./commit-assembly.js";
|
|
2
|
+
import { writeCommitReceipt } from "./commit-receipt.js";
|
|
3
|
+
import { pairKey } from "./contracts.js";
|
|
4
|
+
import { writeJournal } from "./domain/commit-journal-store.js";
|
|
5
|
+
export async function commitStagedRun(io, home, runId, port, options) {
|
|
6
|
+
const assembled = await assembleStagedCommit(io, home, runId, port, { origin: options.origin, summary: options.summary });
|
|
7
|
+
if (!assembled.ok)
|
|
8
|
+
return { ok: false, message: assembled.message };
|
|
9
|
+
const journal = { runId, target: assembled.plan.target, create: !assembled.plan.targetExists, sources: assembled.plan.sources, phase: 'prepared', updatedAt: new Date(options.now()).toISOString() };
|
|
10
|
+
try {
|
|
11
|
+
await writeJournal(io, home, journal);
|
|
12
|
+
}
|
|
13
|
+
catch { /* journal is best-effort; receipt remains the commit record */ }
|
|
14
|
+
const result = await port.commitMerge(assembled.plan);
|
|
15
|
+
if (!result.ok) {
|
|
16
|
+
try {
|
|
17
|
+
await writeJournal(io, home, { ...journal, phase: 'failed', updatedAt: new Date(options.now()).toISOString() });
|
|
18
|
+
}
|
|
19
|
+
catch { }
|
|
20
|
+
return { ok: false, message: result.message, plan: assembled.plan };
|
|
21
|
+
}
|
|
22
|
+
if (result.unitId === undefined)
|
|
23
|
+
return { ok: true, plan: assembled.plan, result, warning: 'commit landed without a unitId; receipt not written' };
|
|
24
|
+
const receipt = {
|
|
25
|
+
runId,
|
|
26
|
+
parentRunId: runId.split('/pairs/')[0] ?? runId,
|
|
27
|
+
unitId: result.unitId,
|
|
28
|
+
target: assembled.plan.target,
|
|
29
|
+
create: !assembled.plan.targetExists,
|
|
30
|
+
sources: assembled.plan.sources,
|
|
31
|
+
...(assembled.plan.sources[0] === undefined ? {} : { pairKey: pairKey(assembled.plan.target, assembled.plan.sources[0]) }),
|
|
32
|
+
archived: result.archived ?? [],
|
|
33
|
+
at: options.now(),
|
|
34
|
+
};
|
|
35
|
+
try {
|
|
36
|
+
await writeCommitReceipt(io, home, receipt);
|
|
37
|
+
try {
|
|
38
|
+
await writeJournal(io, home, { ...journal, unitId: result.unitId, phase: 'committed', updatedAt: new Date(options.now()).toISOString() });
|
|
39
|
+
}
|
|
40
|
+
catch { }
|
|
41
|
+
return { ok: true, plan: assembled.plan, result, receipt };
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
return {
|
|
45
|
+
ok: true,
|
|
46
|
+
plan: assembled.plan,
|
|
47
|
+
result,
|
|
48
|
+
warning: 'commit landed but receipt write failed: ' + (error instanceof Error ? error.message : String(error)),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=commit-execute.js.map
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const MAX_SKILL_NAME_LENGTH = 64;
|
|
2
|
+
/** Pick the first free target: requested, then -v2, -v3, ... (never truncate a name). */
|
|
3
|
+
export function resolveTargetName(requested, exists) {
|
|
4
|
+
if (requested.length === 0 || requested.length > MAX_SKILL_NAME_LENGTH) {
|
|
5
|
+
return { ok: false, message: 'target name must be 1..' + String(MAX_SKILL_NAME_LENGTH) + ' characters' };
|
|
6
|
+
}
|
|
7
|
+
if (!exists(requested))
|
|
8
|
+
return { ok: true, name: requested, suffix: 1 };
|
|
9
|
+
for (let suffix = 2; suffix <= 99; suffix += 1) {
|
|
10
|
+
const candidate = requested + '-v' + String(suffix);
|
|
11
|
+
if (candidate.length > MAX_SKILL_NAME_LENGTH)
|
|
12
|
+
break;
|
|
13
|
+
if (!exists(candidate))
|
|
14
|
+
return { ok: true, name: candidate, suffix };
|
|
15
|
+
}
|
|
16
|
+
return { ok: false, message: 'no free target name for ' + requested + ' (tried up to -v99)' };
|
|
17
|
+
}
|
|
18
|
+
/** Rewrite only the frontmatter name; body bytes stay otherwise intact. */
|
|
19
|
+
export function rewriteFrontmatterName(body, name) {
|
|
20
|
+
const lines = body.split('\n');
|
|
21
|
+
if (lines[0]?.trim() !== '---')
|
|
22
|
+
return body;
|
|
23
|
+
const end = lines.findIndex((line, index) => index > 0 && line.trim() === '---');
|
|
24
|
+
if (end < 0)
|
|
25
|
+
return body;
|
|
26
|
+
for (let index = 1; index < end; index += 1) {
|
|
27
|
+
if (/^name\s*:/.test(lines[index] ?? '')) {
|
|
28
|
+
lines[index] = 'name: ' + name;
|
|
29
|
+
return lines.join('\n');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return body;
|
|
33
|
+
}
|
|
34
|
+
export function buildCommitPlan(input) {
|
|
35
|
+
if (input.create !== (input.targetCurrent === null))
|
|
36
|
+
return { ok: false, message: 'create flag disagrees with the live target' };
|
|
37
|
+
const sources = input.sourceBodies.filter(source => source.name !== input.target);
|
|
38
|
+
if (sources.length === 0)
|
|
39
|
+
return { ok: false, message: 'a merge needs at least one source different from the target' };
|
|
40
|
+
const body = rewriteFrontmatterName(input.mergedBody, input.target);
|
|
41
|
+
return {
|
|
42
|
+
ok: true,
|
|
43
|
+
plan: {
|
|
44
|
+
target: input.target,
|
|
45
|
+
targetExists: !input.create,
|
|
46
|
+
sources: sources.map(source => source.name),
|
|
47
|
+
body,
|
|
48
|
+
supportFiles: input.mergedFiles,
|
|
49
|
+
expectedTargetBody: input.targetCurrent,
|
|
50
|
+
expectedSourceBodies: sources,
|
|
51
|
+
origin: input.origin,
|
|
52
|
+
summary: input.summary,
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=commit-plan.js.map
|
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import type { EvolutionIoLike } from '@lmzhen/dsh-evolution-core';
|
|
2
2
|
export interface CommitReceipt {
|
|
3
3
|
readonly runId: string;
|
|
4
|
+
readonly parentRunId?: string;
|
|
4
5
|
readonly unitId: string;
|
|
5
6
|
readonly target: string;
|
|
6
7
|
readonly create: boolean;
|
|
7
8
|
readonly sources: readonly string[];
|
|
9
|
+
readonly pairKey?: string;
|
|
8
10
|
readonly archived: readonly string[];
|
|
9
11
|
readonly at: number;
|
|
10
12
|
}
|
|
11
13
|
export declare function commitReceiptPath(home: string, runId: string): string;
|
|
14
|
+
export declare function durableReceiptDir(home: string, parentRunId: string): string;
|
|
15
|
+
export declare function durableReceiptPath(home: string, parentRunId: string, stageKey: string): string;
|
|
12
16
|
export declare function writeCommitReceipt(io: EvolutionIoLike, home: string, receipt: CommitReceipt): Promise<string>;
|
|
13
|
-
export declare function
|
|
17
|
+
export declare function readCommitReceipts(io: EvolutionIoLike, home: string): Promise<readonly CommitReceipt[]>;
|
|
18
|
+
export declare function readCommitReceipt(io: EvolutionIoLike, home: string, runId: string, target?: string): Promise<CommitReceipt | null>;
|
|
14
19
|
//# sourceMappingURL=commit-receipt.d.ts.map
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Commit receipt: <run>/commit.json, written immediately after a successful commit.
|
|
3
|
+
* It is the idempotency/recovery marker — merge resume and run start read it before
|
|
4
|
+
* replaying a commit. Receipt write failure must not turn a landed commit into a failure;
|
|
5
|
+
* the caller reports the warning instead.
|
|
6
|
+
*/
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { stagingRoot } from "./staging.js";
|
|
9
|
+
export function commitReceiptPath(home, runId) {
|
|
10
|
+
return join(stagingRoot(home, runId), 'commit.json');
|
|
11
|
+
}
|
|
12
|
+
export function durableReceiptDir(home, parentRunId) {
|
|
13
|
+
return join(home, 'merge', 'receipts', parentRunId);
|
|
14
|
+
}
|
|
15
|
+
export function durableReceiptPath(home, parentRunId, stageKey) {
|
|
16
|
+
const leaf = stageKey.split('/').pop() ?? stageKey;
|
|
17
|
+
return join(durableReceiptDir(home, parentRunId), leaf + '.json');
|
|
18
|
+
}
|
|
19
|
+
function parseReceipt(raw) {
|
|
20
|
+
if (raw === null)
|
|
21
|
+
return null;
|
|
22
|
+
try {
|
|
23
|
+
const parsed = JSON.parse(raw);
|
|
24
|
+
if (typeof parsed.runId !== 'string' || typeof parsed.unitId !== 'string' || typeof parsed.target !== 'string'
|
|
25
|
+
|| typeof parsed.create !== 'boolean' || !Array.isArray(parsed.sources) || !Array.isArray(parsed.archived) || typeof parsed.at !== 'number') {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
return parsed;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export async function writeCommitReceipt(io, home, receipt) {
|
|
35
|
+
const path = commitReceiptPath(home, receipt.runId);
|
|
36
|
+
await io.writeText(path, JSON.stringify(receipt, null, 2) + '\n');
|
|
37
|
+
const parent = receipt.parentRunId ?? receipt.runId.split('/pairs/')[0] ?? receipt.runId;
|
|
38
|
+
await io.writeText(durableReceiptPath(home, parent, receipt.runId), JSON.stringify({ ...receipt, parentRunId: parent }, null, 2) + '\n');
|
|
39
|
+
return path;
|
|
40
|
+
}
|
|
41
|
+
export async function readCommitReceipts(io, home) {
|
|
42
|
+
const root = join(home, 'merge', 'receipts');
|
|
43
|
+
const out = [];
|
|
44
|
+
let runs;
|
|
45
|
+
try {
|
|
46
|
+
runs = await io.list(root);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
for (const run of runs.sort()) {
|
|
52
|
+
const dir = join(root, run);
|
|
53
|
+
let files;
|
|
54
|
+
try {
|
|
55
|
+
files = await io.list(dir);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
for (const file of files.sort()) {
|
|
61
|
+
if (!file.endsWith('.json'))
|
|
62
|
+
continue;
|
|
63
|
+
const receipt = parseReceipt(await io.readText(join(dir, file)));
|
|
64
|
+
if (receipt !== null)
|
|
65
|
+
out.push(receipt);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
export async function readCommitReceipt(io, home, runId, target) {
|
|
71
|
+
const direct = parseReceipt(await io.readText(commitReceiptPath(home, runId)));
|
|
72
|
+
if (direct !== null && (target === undefined || direct.target === target))
|
|
73
|
+
return direct;
|
|
74
|
+
try {
|
|
75
|
+
const names = await io.list(durableReceiptDir(home, runId));
|
|
76
|
+
for (const name of names.sort()) {
|
|
77
|
+
const receipt = parseReceipt(await io.readText(join(durableReceiptDir(home, runId), name)));
|
|
78
|
+
if (receipt !== null && (target === undefined || receipt.target === target))
|
|
79
|
+
return receipt;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch { /* absent */ }
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=commit-receipt.js.map
|
package/lib/types/contracts.d.ts
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
* The data layer keeps four states; the user face collapses verify/fuse into one retry
|
|
5
5
|
* queue and keeps the stage as a chip. The API queue must never expose the raw state.
|
|
6
6
|
*/
|
|
7
|
-
export declare const MERGE_STATE_VERSION =
|
|
8
|
-
export type MergePairState = 'verify' | 'pending' | 'fuse' | 'skipped';
|
|
9
|
-
export type MergeQueue = 'pending' | '
|
|
7
|
+
export declare const MERGE_STATE_VERSION = 2;
|
|
8
|
+
export type MergePairState = 'verify' | 'pending' | 'approved' | 'fuse' | 'skipped';
|
|
9
|
+
export type MergeQueue = 'verify' | 'pending' | 'fuse' | 'skipped';
|
|
10
10
|
export type MergeStage = 'verify' | 'decide' | 'author' | 'commit';
|
|
11
11
|
export type MergeCommand = 'scan' | 'pair' | 'approve' | 'resume';
|
|
12
12
|
export declare const MERGE_PAIR_STATES: readonly MergePairState[];
|
|
@@ -41,7 +41,7 @@ export interface MergeCandidate {
|
|
|
41
41
|
readonly arms?: readonly string[];
|
|
42
42
|
readonly reason?: string;
|
|
43
43
|
readonly gate?: MergeGateFact;
|
|
44
|
-
readonly source?: 'lexical' | 'proposed';
|
|
44
|
+
readonly source?: 'lexical' | 'proposed' | 'engine';
|
|
45
45
|
}
|
|
46
46
|
export interface MergeDecision {
|
|
47
47
|
readonly shouldMerge: boolean;
|
|
@@ -50,6 +50,8 @@ export interface MergeDecision {
|
|
|
50
50
|
readonly sources: readonly string[];
|
|
51
51
|
readonly rationale: string;
|
|
52
52
|
readonly confidence?: number;
|
|
53
|
+
readonly aSummary?: string;
|
|
54
|
+
readonly bSummary?: string;
|
|
53
55
|
}
|
|
54
56
|
export interface MergeUsage {
|
|
55
57
|
readonly inputTokens?: number;
|
|
@@ -86,7 +88,7 @@ export interface MergeReportPair {
|
|
|
86
88
|
readonly timing?: MergeTiming;
|
|
87
89
|
}
|
|
88
90
|
export interface MergeReport {
|
|
89
|
-
readonly schemaVersion:
|
|
91
|
+
readonly schemaVersion: 2;
|
|
90
92
|
readonly runId: string;
|
|
91
93
|
readonly kind: 'merge';
|
|
92
94
|
readonly command: MergeRunCommand;
|
|
@@ -116,13 +118,24 @@ export interface MergeRow {
|
|
|
116
118
|
readonly b: string;
|
|
117
119
|
readonly state: MergePairState;
|
|
118
120
|
readonly queue: MergeQueue;
|
|
121
|
+
readonly expired?: 'target-taken' | 'source-changed';
|
|
122
|
+
readonly reason?: string;
|
|
123
|
+
readonly target?: {
|
|
124
|
+
readonly requested: string;
|
|
125
|
+
readonly actual?: string;
|
|
126
|
+
readonly created?: boolean;
|
|
127
|
+
};
|
|
119
128
|
readonly failure?: MergeFailure;
|
|
120
129
|
/** Round-1 decision, persisted for the later approve round. */
|
|
121
130
|
readonly decision?: MergeDecision;
|
|
131
|
+
readonly sourceRevision?: string;
|
|
132
|
+
readonly targetRevision?: string;
|
|
122
133
|
readonly runId?: string;
|
|
123
134
|
readonly unitId?: string;
|
|
124
|
-
readonly origin?: 'agent' | 'human';
|
|
135
|
+
readonly origin?: 'agent' | 'human' | 'system';
|
|
125
136
|
readonly attempts?: number;
|
|
137
|
+
readonly stagingRef?: string;
|
|
138
|
+
readonly stagingExpiresAt?: number;
|
|
126
139
|
readonly createdAt: string;
|
|
127
140
|
readonly updatedAt: string;
|
|
128
141
|
}
|
|
@@ -132,5 +145,6 @@ export interface MergeState {
|
|
|
132
145
|
}
|
|
133
146
|
export declare function emptyMergeState(): MergeState;
|
|
134
147
|
/** One record can be retried only when the failure says so and names a stage. */
|
|
135
|
-
export declare
|
|
148
|
+
export declare const MERGE_MAX_ATTEMPTS = 3;
|
|
149
|
+
export declare function canRetry(record: Pick<MergeRow, 'state' | 'failure' | 'attempts'>): boolean;
|
|
136
150
|
//# sourceMappingURL=contracts.d.ts.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Frozen merge-domain vocabulary. No io, no host, no imports from the plugin shell.
|
|
3
|
+
*
|
|
4
|
+
* The data layer keeps four states; the user face collapses verify/fuse into one retry
|
|
5
|
+
* queue and keeps the stage as a chip. The API queue must never expose the raw state.
|
|
6
|
+
*/
|
|
7
|
+
export const MERGE_STATE_VERSION = 2;
|
|
8
|
+
export const MERGE_PAIR_STATES = ['verify', 'pending', 'approved', 'fuse', 'skipped'];
|
|
9
|
+
export const MERGE_QUEUES = ['verify', 'pending', 'fuse', 'skipped'];
|
|
10
|
+
/** User-facing queue of one data state. verify/fuse share retry, not two tabs. */
|
|
11
|
+
export function queueOf(state) {
|
|
12
|
+
switch (state) {
|
|
13
|
+
case 'verify': return 'verify';
|
|
14
|
+
case 'pending': return 'pending';
|
|
15
|
+
case 'approved':
|
|
16
|
+
case 'fuse': return 'fuse';
|
|
17
|
+
case 'skipped': return 'skipped';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/** Coarse stage chip; skipped has no active stage. */
|
|
21
|
+
export function stageOf(state) {
|
|
22
|
+
switch (state) {
|
|
23
|
+
case 'verify': return 'verify';
|
|
24
|
+
case 'pending': return 'decide';
|
|
25
|
+
case 'approved':
|
|
26
|
+
case 'fuse': return 'author';
|
|
27
|
+
case 'skipped': return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/** Stable pair identity; NUL cannot appear in a skill name. */
|
|
31
|
+
export function pairKey(a, b) {
|
|
32
|
+
return a < b ? a + '\u0000' + b : b + '\u0000' + a;
|
|
33
|
+
}
|
|
34
|
+
/** Deterministic, URL-safe id derived from the pair key (FNV-1a 32). */
|
|
35
|
+
export function rowIdOf(key) {
|
|
36
|
+
let hash = 0x811c9dc5;
|
|
37
|
+
for (let index = 0; index < key.length; index += 1) {
|
|
38
|
+
hash ^= key.charCodeAt(index);
|
|
39
|
+
hash = Math.imul(hash, 0x01000193) >>> 0;
|
|
40
|
+
}
|
|
41
|
+
return 'mr_' + hash.toString(36).padStart(7, '0');
|
|
42
|
+
}
|
|
43
|
+
export function emptyMergeState() {
|
|
44
|
+
return { schemaVersion: MERGE_STATE_VERSION, rows: [] };
|
|
45
|
+
}
|
|
46
|
+
/** One record can be retried only when the failure says so and names a stage. */
|
|
47
|
+
export const MERGE_MAX_ATTEMPTS = 3;
|
|
48
|
+
export function canRetry(record) {
|
|
49
|
+
if (record.state === 'skipped')
|
|
50
|
+
return false;
|
|
51
|
+
if ((record.attempts ?? 0) >= MERGE_MAX_ATTEMPTS)
|
|
52
|
+
return false;
|
|
53
|
+
if (record.failure === undefined)
|
|
54
|
+
return record.state !== 'pending';
|
|
55
|
+
return record.failure.retryable && record.failure.retryFrom !== 'none';
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=contracts.js.map
|
|
@@ -18,6 +18,8 @@ export type MergeEvent = {
|
|
|
18
18
|
readonly type: 'round1-merge';
|
|
19
19
|
readonly approval: 'on' | 'off';
|
|
20
20
|
readonly decision?: MergeDecision;
|
|
21
|
+
readonly stagingRef?: string;
|
|
22
|
+
readonly stagingExpiresAt?: number;
|
|
21
23
|
readonly at: string;
|
|
22
24
|
} | {
|
|
23
25
|
readonly type: 'round1-fail';
|
|
@@ -34,6 +36,9 @@ export type MergeEvent = {
|
|
|
34
36
|
readonly type: 'round2-fail';
|
|
35
37
|
readonly failure: MergeFailure;
|
|
36
38
|
readonly at: string;
|
|
39
|
+
} | {
|
|
40
|
+
readonly type: 'round2-cancel';
|
|
41
|
+
readonly at: string;
|
|
37
42
|
} | {
|
|
38
43
|
readonly type: 'round2-success';
|
|
39
44
|
readonly unitId?: string;
|
|
@@ -44,6 +49,12 @@ export type MergeEvent = {
|
|
|
44
49
|
} | {
|
|
45
50
|
readonly type: 'source-gone';
|
|
46
51
|
readonly at: string;
|
|
52
|
+
} | {
|
|
53
|
+
readonly type: 'gate-keep';
|
|
54
|
+
readonly a: string;
|
|
55
|
+
readonly b: string;
|
|
56
|
+
readonly reason?: string;
|
|
57
|
+
readonly at: string;
|
|
47
58
|
};
|
|
48
59
|
export type TransitionResult = {
|
|
49
60
|
readonly ok: true;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure merge transition table. No io, no run shell, no subagents.
|
|
3
|
+
*
|
|
4
|
+
* Every transition returns the whole next row (or a drop), so the caller can persist it
|
|
5
|
+
* through mutateMergeState in one transaction. Invalid transitions are refused by name;
|
|
6
|
+
* a caller never "repairs" a row by guessing which state the operator meant.
|
|
7
|
+
*/
|
|
8
|
+
import { pairKey, queueOf, rowIdOf, } from "./contracts.js";
|
|
9
|
+
function invalid(event, state) {
|
|
10
|
+
return {
|
|
11
|
+
ok: false,
|
|
12
|
+
code: 'invalid-transition',
|
|
13
|
+
message: 'cannot apply ' + event.type + ' to ' + (state ?? 'no row'),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/** Drop fields that must not survive a transition; optional fields stay absent. */
|
|
17
|
+
function base(row, at) {
|
|
18
|
+
return {
|
|
19
|
+
id: row.id,
|
|
20
|
+
pairKey: row.pairKey,
|
|
21
|
+
a: row.a,
|
|
22
|
+
b: row.b,
|
|
23
|
+
state: row.state,
|
|
24
|
+
queue: row.queue,
|
|
25
|
+
createdAt: row.createdAt,
|
|
26
|
+
updatedAt: at,
|
|
27
|
+
...(row.runId === undefined ? {} : { runId: row.runId }),
|
|
28
|
+
...(row.unitId === undefined ? {} : { unitId: row.unitId }),
|
|
29
|
+
...(row.origin === undefined ? {} : { origin: row.origin }),
|
|
30
|
+
...(row.decision === undefined ? {} : { decision: row.decision }),
|
|
31
|
+
...(row.sourceRevision === undefined ? {} : { sourceRevision: row.sourceRevision }),
|
|
32
|
+
...(row.targetRevision === undefined ? {} : { targetRevision: row.targetRevision }),
|
|
33
|
+
...(row.attempts === undefined ? {} : { attempts: row.attempts }),
|
|
34
|
+
...(row.stagingRef === undefined ? {} : { stagingRef: row.stagingRef }),
|
|
35
|
+
...(row.stagingExpiresAt === undefined ? {} : { stagingExpiresAt: row.stagingExpiresAt }),
|
|
36
|
+
...(row.expired === undefined ? {} : { expired: row.expired }),
|
|
37
|
+
...(row.reason === undefined ? {} : { reason: row.reason }),
|
|
38
|
+
...(row.target === undefined ? {} : { target: row.target }),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function next(row, at, state, patch = {}) {
|
|
42
|
+
return {
|
|
43
|
+
ok: true,
|
|
44
|
+
row: {
|
|
45
|
+
...base(row, at),
|
|
46
|
+
state,
|
|
47
|
+
queue: queueOf(state),
|
|
48
|
+
...patch,
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export function transition(current, event) {
|
|
53
|
+
if (event.type === 'candidate') {
|
|
54
|
+
if (current !== undefined)
|
|
55
|
+
return { ok: true, row: current };
|
|
56
|
+
const key = pairKey(event.a, event.b);
|
|
57
|
+
return {
|
|
58
|
+
ok: true,
|
|
59
|
+
row: {
|
|
60
|
+
id: rowIdOf(key),
|
|
61
|
+
pairKey: key,
|
|
62
|
+
a: event.a,
|
|
63
|
+
b: event.b,
|
|
64
|
+
state: 'verify',
|
|
65
|
+
queue: 'verify',
|
|
66
|
+
createdAt: event.at,
|
|
67
|
+
updatedAt: event.at,
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
if (event.type === 'source-gone')
|
|
72
|
+
return { ok: true, row: null };
|
|
73
|
+
if (event.type === 'gate-keep') {
|
|
74
|
+
if (current !== undefined)
|
|
75
|
+
return { ok: true, row: current };
|
|
76
|
+
const key = pairKey(event.a, event.b);
|
|
77
|
+
return {
|
|
78
|
+
ok: true,
|
|
79
|
+
row: {
|
|
80
|
+
id: rowIdOf(key),
|
|
81
|
+
pairKey: key,
|
|
82
|
+
a: event.a,
|
|
83
|
+
b: event.b,
|
|
84
|
+
state: 'skipped',
|
|
85
|
+
queue: 'skipped',
|
|
86
|
+
origin: 'agent',
|
|
87
|
+
createdAt: event.at,
|
|
88
|
+
updatedAt: event.at,
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
if (current === undefined)
|
|
93
|
+
return invalid(event, undefined);
|
|
94
|
+
switch (event.type) {
|
|
95
|
+
case 'round1-keep':
|
|
96
|
+
return current.state === 'verify' ? next(current, event.at, 'skipped', { origin: 'agent' }) : invalid(event, current.state);
|
|
97
|
+
case 'round1-merge':
|
|
98
|
+
if (current.state !== 'verify')
|
|
99
|
+
return invalid(event, current.state);
|
|
100
|
+
return event.approval === 'on'
|
|
101
|
+
? next(current, event.at, 'pending', { ...(event.decision === undefined ? {} : { decision: event.decision }), ...(event.stagingRef === undefined ? {} : { stagingRef: event.stagingRef }), ...(event.stagingExpiresAt === undefined ? {} : { stagingExpiresAt: event.stagingExpiresAt }) })
|
|
102
|
+
: next(current, event.at, 'approved', { ...(event.decision === undefined ? {} : { decision: event.decision }), ...(event.stagingRef === undefined ? {} : { stagingRef: event.stagingRef }), ...(event.stagingExpiresAt === undefined ? {} : { stagingExpiresAt: event.stagingExpiresAt }) });
|
|
103
|
+
case 'round1-fail':
|
|
104
|
+
return current.state === 'verify'
|
|
105
|
+
? next(current, event.at, 'verify', { failure: event.failure, attempts: (current.attempts ?? 0) + 1 })
|
|
106
|
+
: invalid(event, current.state);
|
|
107
|
+
case 'approve':
|
|
108
|
+
return current.state === 'pending'
|
|
109
|
+
? next(current, event.at, 'approved', { ...(event.runId === undefined ? {} : { runId: event.runId }) })
|
|
110
|
+
: invalid(event, current.state);
|
|
111
|
+
case 'reject':
|
|
112
|
+
return current.state === 'pending' ? next(current, event.at, 'skipped', { origin: 'human' }) : invalid(event, current.state);
|
|
113
|
+
case 'round2-cancel':
|
|
114
|
+
return current.state === 'fuse' || current.state === 'approved'
|
|
115
|
+
? next(current, event.at, 'approved')
|
|
116
|
+
: invalid(event, current.state);
|
|
117
|
+
case 'round2-fail':
|
|
118
|
+
return current.state === 'fuse' || current.state === 'approved'
|
|
119
|
+
? next(current, event.at, 'approved', { failure: event.failure, attempts: (current.attempts ?? 0) + 1 })
|
|
120
|
+
: invalid(event, current.state);
|
|
121
|
+
case 'round2-success':
|
|
122
|
+
return current.state === 'fuse' || current.state === 'approved'
|
|
123
|
+
? { ok: true, row: null }
|
|
124
|
+
: invalid(event, current.state);
|
|
125
|
+
case 'restore':
|
|
126
|
+
return current.state === 'skipped'
|
|
127
|
+
? next(current, event.at, 'verify')
|
|
128
|
+
: invalid(event, current.state);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
//# sourceMappingURL=coordinator.js.map
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
function extractJson(text) {
|
|
2
|
+
const fenced = /\`\`\`(?:json)?\s*([\s\S]*?)\`\`\`/i.exec(text);
|
|
3
|
+
const body = fenced?.[1] ?? text;
|
|
4
|
+
const start = body.indexOf('{');
|
|
5
|
+
if (start < 0)
|
|
6
|
+
return undefined;
|
|
7
|
+
let depth = 0;
|
|
8
|
+
for (let index = start; index < body.length; index += 1) {
|
|
9
|
+
if (body[index] === '{')
|
|
10
|
+
depth += 1;
|
|
11
|
+
else if (body[index] === '}') {
|
|
12
|
+
depth -= 1;
|
|
13
|
+
if (depth === 0) {
|
|
14
|
+
try {
|
|
15
|
+
return JSON.parse(body.slice(start, index + 1));
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
export function parseDecision(text, pair) {
|
|
26
|
+
if (text === undefined || text.trim() === '')
|
|
27
|
+
return { ok: false, code: 'decision-invalid', message: 'empty decision text' };
|
|
28
|
+
const value = extractJson(text);
|
|
29
|
+
if (typeof value !== 'object' || value === null)
|
|
30
|
+
return { ok: false, code: 'decision-invalid', message: 'decision is not a JSON object' };
|
|
31
|
+
const record = value;
|
|
32
|
+
const shouldMerge = record.shouldMerge === true || record.should_merge === true;
|
|
33
|
+
const mode = record.mode;
|
|
34
|
+
if (mode !== 'merge-into-existing' && mode !== 'create-umbrella' && mode !== 'keep') {
|
|
35
|
+
return { ok: false, code: 'decision-invalid', message: 'mode must be merge-into-existing, create-umbrella or keep' };
|
|
36
|
+
}
|
|
37
|
+
const target = typeof record.target === 'string' ? record.target.trim() : '';
|
|
38
|
+
const sources = Array.isArray(record.sources) ? record.sources.filter((item) => typeof item === 'string').map(item => item.trim()).filter(Boolean) : [];
|
|
39
|
+
const rationale = typeof record.rationale === 'string' ? record.rationale.trim() : '';
|
|
40
|
+
if (!shouldMerge || mode === 'keep') {
|
|
41
|
+
return { ok: true, decision: { shouldMerge: false, mode: 'keep', target, sources, rationale } };
|
|
42
|
+
}
|
|
43
|
+
if (target === '')
|
|
44
|
+
return { ok: false, code: 'decision-invalid', message: 'merge decision needs target' };
|
|
45
|
+
if (sources.length === 0)
|
|
46
|
+
return { ok: false, code: 'decision-invalid', message: 'merge decision needs sources' };
|
|
47
|
+
if (!sources.includes(pair.a) || !sources.includes(pair.b))
|
|
48
|
+
return { ok: false, code: 'decision-invalid', message: 'sources must include both pair skills' };
|
|
49
|
+
return {
|
|
50
|
+
ok: true,
|
|
51
|
+
decision: {
|
|
52
|
+
shouldMerge: true,
|
|
53
|
+
mode,
|
|
54
|
+
target,
|
|
55
|
+
sources,
|
|
56
|
+
rationale,
|
|
57
|
+
...(typeof record.confidence === 'number' ? { confidence: record.confidence } : {}),
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=decision.js.map
|
package/lib/types/doctor.d.ts
CHANGED
|
@@ -6,5 +6,5 @@ export interface MergeDoctorRow {
|
|
|
6
6
|
readonly status: 'ok' | 'warn' | 'unknown';
|
|
7
7
|
readonly detail: string;
|
|
8
8
|
}
|
|
9
|
-
export declare function mergeDoctor(io: EvolutionIoLike, home: string): Promise<readonly MergeDoctorRow[]>;
|
|
9
|
+
export declare function mergeDoctor(io: EvolutionIoLike, home: string, configuredRoot?: string): Promise<readonly MergeDoctorRow[]>;
|
|
10
10
|
//# sourceMappingURL=doctor.d.ts.map
|