@lmzhen/dsh-evolution-merge 0.30.23 → 0.30.25
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/lib/index.js +518 -182
- package/lib/types/approve-command.js +2 -1
- package/lib/types/authoring-flow.d.ts +5 -0
- package/lib/types/authoring-flow.js +8 -5
- package/lib/types/commands.d.ts +7 -0
- package/lib/types/commands.js +8 -1
- package/lib/types/commit-execute.d.ts +4 -0
- package/lib/types/commit-execute.js +8 -5
- package/lib/types/commit-receipt.js +3 -1
- package/lib/types/contracts.d.ts +20 -1
- package/lib/types/doctor.js +1 -1
- package/lib/types/domain/errors.js +5 -1
- package/lib/types/domain/merge-snapshot.d.ts +12 -3
- package/lib/types/domain/merge-snapshot.js +9 -4
- package/lib/types/domain/reconcile.js +1 -1
- package/lib/types/domain/row-v4.d.ts +4 -0
- package/lib/types/domain/row-v4.js +9 -9
- package/lib/types/domain/run-coordinator.d.ts +9 -0
- package/lib/types/domain/stage-aggregate.d.ts +6 -1
- package/lib/types/domain/stage-aggregate.js +16 -9
- package/lib/types/domain/validation-plan.js +5 -1
- package/lib/types/index.js +14 -2
- package/lib/types/kernel/adapter-v4.d.ts +12 -0
- package/lib/types/kernel/adapter-v4.js +54 -0
- package/lib/types/kernel/failure.d.ts +8 -0
- package/lib/types/kernel/failure.js +33 -0
- package/lib/types/kernel/index.d.ts +9 -0
- package/lib/types/kernel/index.js +9 -0
- package/lib/types/kernel/invariants.d.ts +4 -0
- package/lib/types/kernel/invariants.js +47 -0
- package/lib/types/kernel/ledger.d.ts +46 -0
- package/lib/types/kernel/ledger.js +62 -0
- package/lib/types/kernel/machine.d.ts +13 -0
- package/lib/types/kernel/machine.js +108 -0
- package/lib/types/kernel/model.d.ts +154 -0
- package/lib/types/kernel/model.js +5 -0
- package/lib/types/kernel/projection.d.ts +28 -0
- package/lib/types/kernel/projection.js +44 -0
- package/lib/types/kernel/recovery.d.ts +31 -0
- package/lib/types/kernel/recovery.js +19 -0
- package/lib/types/kernel/unit.d.ts +43 -0
- package/lib/types/kernel/unit.js +63 -0
- package/lib/types/merge-routes.js +9 -0
- package/lib/types/new-child-authoring.d.ts +1 -0
- package/lib/types/new-child-authoring.js +15 -7
- package/lib/types/pending-actions.d.ts +1 -1
- package/lib/types/pending-actions.js +4 -0
- package/lib/types/recovery-coordinator.d.ts +2 -0
- package/lib/types/recovery-coordinator.js +14 -4
- package/lib/types/recovery-projection.d.ts +4 -2
- package/lib/types/recovery-projection.js +3 -3
- package/lib/types/resume-command.js +2 -1
- package/lib/types/round2-outcome.js +26 -9
- package/lib/types/run-shell.d.ts +1 -0
- package/lib/types/run-shell.js +57 -15
- package/lib/types/runtime.d.ts +4 -1
- package/lib/types/runtime.js +34 -22
- package/lib/types/scan-command.d.ts +1 -0
- package/lib/types/scan-command.js +1 -0
- package/lib/types/scan-ports.js +3 -0
- package/lib/types/scan-run.d.ts +6 -1
- package/lib/types/scan-run.js +34 -18
- package/lib/types/surface/merge-view.js +12 -4
- package/lib/types/undo-operation.d.ts +2 -1
- package/lib/types/undo-operation.js +10 -4
- package/lib/types/wire-projection.js +7 -2
- package/lib/types/wire.d.ts +1 -0
- package/package.json +4 -4
- package/lib/types/adapters/model.d.ts +0 -31
- package/lib/types/adapters/model.js +0 -47
- package/lib/types/gate-cache.d.ts +0 -12
- package/lib/types/gate-cache.js +0 -36
- package/lib/types/gate-filter.d.ts +0 -31
- package/lib/types/gate-filter.js +0 -64
- package/lib/types/ladder.d.ts +0 -65
- package/lib/types/ladder.js +0 -162
- package/lib/types/propose.d.ts +0 -26
- package/lib/types/propose.js +0 -69
- package/lib/types/screening-port.d.ts +0 -47
- package/lib/types/screening-port.js +0 -93
- package/lib/types/screening.d.ts +0 -30
- package/lib/types/screening.js +0 -75
|
@@ -38,7 +38,7 @@ export async function runApproveCommand(options) {
|
|
|
38
38
|
const current = tx.rows().find(item => item.id === row.id);
|
|
39
39
|
if (current === undefined)
|
|
40
40
|
return null;
|
|
41
|
-
const moved = tx.transition(row.id, { type: 'staged' }, current.version);
|
|
41
|
+
const moved = tx.transition(row.id, { type: 'staged', stagingRef: staged.entry.ref, stagingExpiresAt: staged.entry.expiresAt }, current.version);
|
|
42
42
|
return moved.ok && moved.row !== null ? v4ToLegacy(moved.row) : null;
|
|
43
43
|
});
|
|
44
44
|
if (fused === null)
|
|
@@ -53,6 +53,7 @@ export async function runApproveCommand(options) {
|
|
|
53
53
|
command: 'approve',
|
|
54
54
|
approval: { platformWrite: options.platformWrite, mergeApproval: true },
|
|
55
55
|
pairKey: row.pairKey,
|
|
56
|
+
...(row.stagingRef === undefined ? {} : { stagingRef: row.stagingRef }),
|
|
56
57
|
parentRunId,
|
|
57
58
|
now: options.now,
|
|
58
59
|
...(options.detached === true ? { detached: true } : {}),
|
|
@@ -23,6 +23,8 @@ export interface RunAuthoringOptions {
|
|
|
23
23
|
readonly stageKey?: string;
|
|
24
24
|
readonly reuseStaging?: boolean;
|
|
25
25
|
readonly target: string;
|
|
26
|
+
/** The two participating source skills; conflict detection is limited to these. */
|
|
27
|
+
readonly sources?: readonly string[];
|
|
26
28
|
readonly mode?: 'merge-into-existing' | 'create-umbrella' | 'keep';
|
|
27
29
|
readonly cards: readonly SkillCard[];
|
|
28
30
|
readonly author: (staging: {
|
|
@@ -37,6 +39,8 @@ export interface RunAuthoringOptions {
|
|
|
37
39
|
readonly limits?: SkillLimits;
|
|
38
40
|
readonly promptVersion?: string;
|
|
39
41
|
readonly promptHash?: string;
|
|
42
|
+
/** Propagated to the commit; aborting before the durable point prevents a orphaned commit. */
|
|
43
|
+
readonly signal?: AbortSignal;
|
|
40
44
|
/** Round-1 decision anchors; when absent the commit keeps the legacy body-only CAS. */
|
|
41
45
|
readonly revisions?: CommitRevisionRefs;
|
|
42
46
|
}
|
|
@@ -47,6 +51,7 @@ export type AuthoringResult = {
|
|
|
47
51
|
readonly commit: Extract<CommitStagedResult, {
|
|
48
52
|
readonly ok: true;
|
|
49
53
|
}>;
|
|
54
|
+
readonly lengths: Readonly<Record<string, number>>;
|
|
50
55
|
} | {
|
|
51
56
|
readonly ok: false;
|
|
52
57
|
readonly code?: string;
|
|
@@ -58,25 +58,28 @@ export async function runAuthoring(options) {
|
|
|
58
58
|
if (missing !== null)
|
|
59
59
|
return { ok: false, code: 'merge-manifest-missing', message: 'manifest file missing from staged tree: ' + missing };
|
|
60
60
|
const limitsSnapshot = limitsSnapshotOf(options.limits ?? DEFAULT_SKILL_LIMITS);
|
|
61
|
-
const
|
|
61
|
+
const participants = new Set(options.sources ?? []);
|
|
62
|
+
const supportSources = options.cards.filter(card => participants.has(card.name)).flatMap(card => (card.files ?? []).map(file => ({ path: file.path, content: file.content, source: card.name })));
|
|
62
63
|
const validationPlan = buildValidationPlan({ tree: tree.value, target: options.target, limits: limitsSnapshot, cards: options.cards, supportSources, ...(options.promptVersion === undefined ? {} : { promptVersion: options.promptVersion }), ...(options.promptHash === undefined ? {} : { promptHash: options.promptHash }), supportFiles: tree.value.files.map(file => ({ path: file.path, content: file.content, source: 'staged' })), manifestFiles, existingSupport: (options.cards.find(card => card.name === options.target)?.files ?? []).map(file => file.path) });
|
|
63
64
|
const findings = [...validateStagedTree(tree.value, limitsSnapshot.skillLimits), ...validationPlan.findings];
|
|
65
|
+
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) };
|
|
64
66
|
if (hasBlockingFinding(findings))
|
|
65
|
-
return { ok: false, code: findings.some(finding => finding.rule === 'limit') ? 'merge-over-limit' : 'merge-validation', message: 'staged tree failed the rule gate', findings, lengths
|
|
67
|
+
return { ok: false, code: findings.some(finding => finding.rule === 'limit') ? 'merge-over-limit' : 'merge-validation', message: 'staged tree failed the rule gate', findings, lengths };
|
|
66
68
|
const commit = await commitStagedRun(options.io, options.home, stageKey, options.commitPort, {
|
|
67
69
|
origin: 'foreground',
|
|
68
70
|
summary: options.summary,
|
|
69
71
|
now: options.now,
|
|
72
|
+
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
70
73
|
...(options.isBlocked === undefined ? {} : { isBlocked: options.isBlocked }),
|
|
71
74
|
...(options.unit === undefined ? {} : { unit: options.unit }),
|
|
72
|
-
|
|
73
|
-
validationPlan: { planVersion: validationPlan.planVersion, stageDigest: validationPlan.stageDigest, revision: validationPlan.revision },
|
|
75
|
+
limits: limitsSnapshot.skillLimits,
|
|
76
|
+
validationPlan: { planVersion: validationPlan.planVersion, stageDigest: validationPlan.stageDigest, revision: validationPlan.revision, manifestFiles: validationPlan.manifestFiles },
|
|
74
77
|
...(options.revisions?.targetRevision === undefined ? {} : { targetRevision: options.revisions.targetRevision }),
|
|
75
78
|
...(options.revisions?.sourceRevisions === undefined ? {} : { sourceRevisions: options.revisions.sourceRevisions }),
|
|
76
79
|
...(options.revisions?.supportDigest === undefined ? {} : { supportDigest: options.revisions.supportDigest }),
|
|
77
80
|
});
|
|
78
81
|
if (commit.ok)
|
|
79
|
-
return { ok: true, tree: tree.value, validationPlan, commit };
|
|
82
|
+
return { ok: true, tree: tree.value, validationPlan, commit, lengths };
|
|
80
83
|
return { ok: false, code: commit.code ?? 'commit-failed', message: commit.message };
|
|
81
84
|
}
|
|
82
85
|
//# sourceMappingURL=authoring-flow.js.map
|
package/lib/types/commands.d.ts
CHANGED
|
@@ -59,5 +59,12 @@ export type ParseMergeCommandResult = {
|
|
|
59
59
|
readonly message: string;
|
|
60
60
|
};
|
|
61
61
|
export declare function parseMergeCommand(argv: readonly string[]): ParseMergeCommandResult;
|
|
62
|
+
export interface MergeLifecycleVerb {
|
|
63
|
+
readonly verb: string;
|
|
64
|
+
readonly appliesTo: string;
|
|
65
|
+
readonly excludes: readonly string[];
|
|
66
|
+
}
|
|
67
|
+
/** Machine-readable three-verb table: merge undo / skill undo / skill restore. */
|
|
68
|
+
export declare const MERGE_LIFECYCLE_VERBS: readonly MergeLifecycleVerb[];
|
|
62
69
|
export declare function mergeUsage(): string;
|
|
63
70
|
//# sourceMappingURL=commands.d.ts.map
|
package/lib/types/commands.js
CHANGED
|
@@ -34,7 +34,14 @@ export function parseMergeCommand(argv) {
|
|
|
34
34
|
default: return { ok: false, code: 'usage', message: 'unknown merge command: ' + parsed.id };
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
|
+
/** Machine-readable three-verb table: merge undo / skill undo / skill restore. */
|
|
38
|
+
export const MERGE_LIFECYCLE_VERBS = [
|
|
39
|
+
{ verb: 'merge undo', appliesTo: '一次 merge run(按 run 反序撤销 unit)', excludes: ['skill undo', 'skill restore'] },
|
|
40
|
+
{ verb: 'skill undo', appliesTo: '单个技能的一次 patch', excludes: ['merge undo'] },
|
|
41
|
+
{ verb: 'skill restore', appliesTo: '从 .archive 恢复整棵技能树', excludes: ['merge undo'] },
|
|
42
|
+
];
|
|
37
43
|
export function mergeUsage() {
|
|
38
|
-
return 'usage: ' + MERGE_COMMAND_DEFS.filter(def => def.id !== 'merge.doctor' && def.usage !== false).map(def => def.fallback).join(' | ') + '\ndefaults: mergeApproval=false, mergeSkippedTtlDays=0, mergeRunTimeoutMinutes=0, mergePairTimeoutMinutes=15, mergeModelsRoot=""'
|
|
44
|
+
return 'usage: ' + MERGE_COMMAND_DEFS.filter(def => def.id !== 'merge.doctor' && def.usage !== false).map(def => def.fallback).join(' | ') + '\ndefaults: mergeApproval=false, mergeSkippedTtlDays=0, mergeRunTimeoutMinutes=0, mergePairTimeoutMinutes=15, mergeModelsRoot=""'
|
|
45
|
+
+ '\nlifecycle: ' + MERGE_LIFECYCLE_VERBS.map(item => item.verb + '=' + item.appliesTo + ';互斥 ' + item.excludes.join('/')).join(' | ');
|
|
39
46
|
}
|
|
40
47
|
//# sourceMappingURL=commands.js.map
|
|
@@ -26,6 +26,10 @@ export interface CommitStagedOptions {
|
|
|
26
26
|
readonly targetRevision?: import('./contracts.ts').RevisionRef;
|
|
27
27
|
readonly sourceRevisions?: Readonly<Record<string, import('./contracts.ts').RevisionRef>>;
|
|
28
28
|
readonly supportDigest?: string;
|
|
29
|
+
/** The run that started this child; absent means the receipt itself is the root run. */
|
|
30
|
+
readonly parentRunId?: string;
|
|
31
|
+
/** Aborts a commit before its durable point; a landed commit still gets a receipt. */
|
|
32
|
+
readonly signal?: AbortSignal;
|
|
29
33
|
/** G2 transitional: when present, commit through core UnitTransaction. */
|
|
30
34
|
readonly unit?: UnitTransaction;
|
|
31
35
|
}
|
|
@@ -21,6 +21,7 @@ export async function commitStagedRun(io, home, runId, port, options) {
|
|
|
21
21
|
const journal = { runId, target: assembled.plan.target, create: !assembled.plan.targetExists, sources: assembled.plan.sources, phase: 'prepared', updatedAt: new Date(options.now()).toISOString() };
|
|
22
22
|
let result;
|
|
23
23
|
let evidenceSha256;
|
|
24
|
+
let warning;
|
|
24
25
|
const transaction = options.unit ?? port.unit ?? legacyUnitAdapter(port, assembled.plan, options, runId);
|
|
25
26
|
if (transaction === undefined)
|
|
26
27
|
return { ok: false, message: 'commit port missing: UnitTransaction or commitMerge required', plan: assembled.plan };
|
|
@@ -34,7 +35,7 @@ export async function commitStagedRun(io, home, runId, port, options) {
|
|
|
34
35
|
const prepared = await transaction.prepare(unitPlan);
|
|
35
36
|
if (!prepared.ok)
|
|
36
37
|
return { ok: false, message: prepared.message, plan: assembled.plan };
|
|
37
|
-
const committed = await transaction.commit(prepared.prepared);
|
|
38
|
+
const committed = await transaction.commit(prepared.prepared, options.signal === undefined ? undefined : { signal: options.signal });
|
|
38
39
|
if (!committed.ok) {
|
|
39
40
|
try {
|
|
40
41
|
await writeJournal(io, home, { ...journal, phase: 'failed', updatedAt: new Date(options.now()).toISOString() });
|
|
@@ -49,8 +50,10 @@ export async function commitStagedRun(io, home, runId, port, options) {
|
|
|
49
50
|
const finalize = transaction.finalize;
|
|
50
51
|
if (finalize !== undefined) {
|
|
51
52
|
const finalized = await finalize.call(transaction, unitId);
|
|
53
|
+
// The tree is already committed. Finalize is post-commit bookkeeping; failing it must
|
|
54
|
+
// not turn a landed merge into a failed run. Keep the receipt path alive and warn.
|
|
52
55
|
if (!finalized.ok)
|
|
53
|
-
|
|
56
|
+
warning = 'commit landed but finalize failed: ' + finalized.message;
|
|
54
57
|
}
|
|
55
58
|
evidenceSha256 = digestEvidence(committed.evidence);
|
|
56
59
|
result = { ok: true, message: 'merged', unitId, archived: committed.evidence.sources.filter(source => source.archived).map(source => source.name), mutations: committed.evidence.mutations };
|
|
@@ -73,7 +76,7 @@ export async function commitStagedRun(io, home, runId, port, options) {
|
|
|
73
76
|
runId,
|
|
74
77
|
...(evidenceSha256 === undefined ? {} : { evidenceSha256 }),
|
|
75
78
|
...(options.validationPlan === undefined ? {} : { validationPlan: options.validationPlan }),
|
|
76
|
-
parentRunId:
|
|
79
|
+
...(options.parentRunId === undefined ? {} : { parentRunId: options.parentRunId }),
|
|
77
80
|
unitId: result.unitId,
|
|
78
81
|
target: assembled.plan.target,
|
|
79
82
|
create: !assembled.plan.targetExists,
|
|
@@ -88,14 +91,14 @@ export async function commitStagedRun(io, home, runId, port, options) {
|
|
|
88
91
|
await io.remove(snapshotPath(home, runId));
|
|
89
92
|
}
|
|
90
93
|
catch { /* committed journal is the record */ }
|
|
91
|
-
return { ok: true, plan: assembled.plan, result, receipt };
|
|
94
|
+
return { ok: true, plan: assembled.plan, result, receipt, ...(warning === undefined ? {} : { warning }) };
|
|
92
95
|
}
|
|
93
96
|
catch (error) {
|
|
94
97
|
return {
|
|
95
98
|
ok: true,
|
|
96
99
|
plan: assembled.plan,
|
|
97
100
|
result,
|
|
98
|
-
warning: 'commit landed but receipt write failed: ' + (error instanceof Error ? error.message : String(error)),
|
|
101
|
+
warning: (warning === undefined ? '' : warning + '; ') + 'commit landed but receipt write failed: ' + (error instanceof Error ? error.message : String(error)),
|
|
99
102
|
};
|
|
100
103
|
}
|
|
101
104
|
}
|
|
@@ -35,7 +35,9 @@ export async function writeCommitReceipt(io, home, receipt) {
|
|
|
35
35
|
const path = commitReceiptPath(home, receipt.runId);
|
|
36
36
|
await io.writeText(path, JSON.stringify(receipt, null, 2) + '\n');
|
|
37
37
|
const parent = receipt.parentRunId ?? receipt.runId.split('/pairs/')[0] ?? receipt.runId;
|
|
38
|
-
|
|
38
|
+
// One durable file per unit: two pairs of the same run/target must not overwrite each other.
|
|
39
|
+
const leaf = (receipt.runId + '-' + receipt.unitId).replace(/[^A-Za-z0-9._-]+/g, '-');
|
|
40
|
+
await io.writeText(durableReceiptPath(home, parent, leaf), JSON.stringify({ ...receipt, parentRunId: parent }, null, 2) + '\n');
|
|
39
41
|
return path;
|
|
40
42
|
}
|
|
41
43
|
export async function readCommitReceipts(io, home) {
|
package/lib/types/contracts.d.ts
CHANGED
|
@@ -57,6 +57,7 @@ export interface MergeDecision {
|
|
|
57
57
|
readonly sources: readonly string[];
|
|
58
58
|
readonly rationale: string;
|
|
59
59
|
readonly confidence?: number;
|
|
60
|
+
readonly model?: string;
|
|
60
61
|
readonly promptVersion?: string;
|
|
61
62
|
readonly promptHash?: string;
|
|
62
63
|
readonly aSummary?: string;
|
|
@@ -72,6 +73,7 @@ export interface MergeValidationPlanRef {
|
|
|
72
73
|
readonly planVersion: 1;
|
|
73
74
|
readonly stageDigest: string;
|
|
74
75
|
readonly revision: string;
|
|
76
|
+
readonly manifestFiles?: readonly string[];
|
|
75
77
|
}
|
|
76
78
|
export interface MergeUsage {
|
|
77
79
|
readonly inputTokens?: number;
|
|
@@ -108,7 +110,11 @@ export interface MergeReportPair {
|
|
|
108
110
|
readonly lengths?: Readonly<Record<string, number>>;
|
|
109
111
|
readonly timing?: MergeTiming;
|
|
110
112
|
readonly validationPlan?: MergeValidationPlanRef;
|
|
113
|
+
readonly manifestFiles?: readonly string[];
|
|
111
114
|
readonly criteria?: MergeCriteria;
|
|
115
|
+
readonly validation?: {
|
|
116
|
+
readonly issues: readonly MergeValidationIssue[];
|
|
117
|
+
};
|
|
112
118
|
readonly reportOnly?: boolean;
|
|
113
119
|
}
|
|
114
120
|
export interface MergeCriteria {
|
|
@@ -118,6 +124,12 @@ export interface MergeCriteria {
|
|
|
118
124
|
readonly archive: 'none' | 'sources';
|
|
119
125
|
readonly evidence: 'none' | 'receipt';
|
|
120
126
|
}
|
|
127
|
+
export interface MergeValidationIssue {
|
|
128
|
+
readonly rule: string;
|
|
129
|
+
readonly path: string;
|
|
130
|
+
readonly severity: 'block' | 'warn';
|
|
131
|
+
readonly message: string;
|
|
132
|
+
}
|
|
121
133
|
export interface MergeReport {
|
|
122
134
|
readonly schemaVersion: 2;
|
|
123
135
|
readonly runId: string;
|
|
@@ -125,7 +137,7 @@ export interface MergeReport {
|
|
|
125
137
|
readonly command: MergeRunCommand;
|
|
126
138
|
readonly startedAt: number;
|
|
127
139
|
readonly endedAt: number;
|
|
128
|
-
readonly stopReason: 'completed' | 'failed' | 'cancelled' | 'timeout';
|
|
140
|
+
readonly stopReason: 'completed' | 'failed' | 'cancelled' | 'timeout' | 'interrupted';
|
|
129
141
|
readonly approval: {
|
|
130
142
|
readonly platformWrite: boolean;
|
|
131
143
|
readonly mergeApproval: boolean;
|
|
@@ -135,12 +147,19 @@ export interface MergeReport {
|
|
|
135
147
|
readonly queueBefore: Record<MergeQueue, number>;
|
|
136
148
|
};
|
|
137
149
|
readonly pairs: readonly MergeReportPair[];
|
|
150
|
+
/** Backward-compatible alias; same objects as pairs[]. */
|
|
151
|
+
readonly processedPairs?: readonly MergeReportPair[];
|
|
138
152
|
readonly totals: Record<'merged' | 'kept' | 'pending' | 'failed', number>;
|
|
139
153
|
readonly notes: readonly string[];
|
|
140
154
|
readonly remaining?: readonly {
|
|
141
155
|
readonly pairKey: string;
|
|
142
156
|
readonly queue: MergeQueue;
|
|
143
157
|
}[];
|
|
158
|
+
/** Backward-compatible alias; same objects as remaining[]. */
|
|
159
|
+
readonly remainingPairs?: readonly {
|
|
160
|
+
readonly pairKey: string;
|
|
161
|
+
readonly queue: MergeQueue;
|
|
162
|
+
}[];
|
|
144
163
|
readonly reportOnly?: readonly MergeReportPair[];
|
|
145
164
|
readonly validationFailures?: readonly MergeReportPair[];
|
|
146
165
|
readonly archived?: readonly string[];
|
package/lib/types/doctor.js
CHANGED
|
@@ -61,7 +61,7 @@ export async function mergeDoctor(io, home, configuredRoot) {
|
|
|
61
61
|
key: 'state',
|
|
62
62
|
label: '合并状态',
|
|
63
63
|
status: snapshot.blocked === undefined || !hasState ? 'ok' : 'warn',
|
|
64
|
-
detail: snapshot.blocked === undefined ? 'pairs ' + String(snapshot.rows.length) + ';verify ' + String(counts.verify) + ' / pending ' + String(counts.pending) + ' / fuse ' + String(counts.fuse) + ' / skipped ' + String(counts.skipped) : snapshot.blocked.code + ': ' + snapshot.blocked.message,
|
|
64
|
+
detail: snapshot.blocked === undefined ? 'pairs ' + String(snapshot.rows.length) + ';verify ' + String(counts.verify) + ' / pending ' + String(counts.pending) + ' / fuse ' + String(counts.fuse) + ' / skipped ' + String(counts.skipped) + ' / expired ' + String(snapshot.doctor.counts.expired) : snapshot.blocked.code + ': ' + snapshot.blocked.message,
|
|
65
65
|
});
|
|
66
66
|
const recoveryRuns = new Set();
|
|
67
67
|
try {
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
function redact(text) {
|
|
2
2
|
return text
|
|
3
3
|
.replace(/sk-[A-Za-z0-9_-]{16,}/g, '[redacted]')
|
|
4
|
+
.replace(/ghp_[A-Za-z0-9]{20,}/g, '[redacted]')
|
|
5
|
+
.replace(/github_pat_[A-Za-z0-9_]{20,}/g, '[redacted]')
|
|
6
|
+
.replace(/xox[baprs]-[A-Za-z0-9-]{10,}/g, '[redacted]')
|
|
7
|
+
.replace(/AKIA[0-9A-Z]{16}/g, '[redacted]')
|
|
4
8
|
.replace(/Bearer\s+\S+/gi, 'Bearer [redacted]')
|
|
5
|
-
.replace(/(api[_-]?key\s*[:=]\s*)\S+/gi, '$1[redacted]');
|
|
9
|
+
.replace(/((?:api[_-]?key|token|password|secret)\s*[:=]\s*)\S+/gi, '$1[redacted]');
|
|
6
10
|
}
|
|
7
11
|
/** Build the typed failure every command/CLI/runtime branch returns. */
|
|
8
12
|
export function commandError(stage, code, hint, next, runId) {
|
|
@@ -4,15 +4,15 @@ import { type StageEntry } from './stage-aggregate.ts';
|
|
|
4
4
|
import { type MergeRunMeta } from './run-coordinator.ts';
|
|
5
5
|
import type { MergeReport } from '../contracts.ts';
|
|
6
6
|
import type { V4Row } from './row-v4.ts';
|
|
7
|
-
import { type LiveRowFacts
|
|
8
|
-
import type { QueueId, MergeCounts, DoctorFact } from '../wire.ts';
|
|
7
|
+
import { type LiveRowFacts } from './live-facts.ts';
|
|
8
|
+
import type { QueueId, MergeCounts, DoctorFact, ExpiredKind } from '../wire.ts';
|
|
9
9
|
export interface SnapshotRow {
|
|
10
10
|
readonly row: V4Row;
|
|
11
11
|
readonly stage: StageEntry | null;
|
|
12
12
|
readonly bodies: readonly BodyRef[];
|
|
13
13
|
readonly queue: QueueId;
|
|
14
14
|
readonly expired: boolean;
|
|
15
|
-
readonly expiredKind?:
|
|
15
|
+
readonly expiredKind?: ExpiredKind;
|
|
16
16
|
}
|
|
17
17
|
export interface BodyRef {
|
|
18
18
|
readonly rowId: string;
|
|
@@ -76,6 +76,10 @@ export interface MergeSnapshot {
|
|
|
76
76
|
readonly history: readonly HistoryItem[];
|
|
77
77
|
readonly bodyRefs: readonly BodyRef[];
|
|
78
78
|
readonly doctor: SnapshotDoctor;
|
|
79
|
+
readonly hostOnly?: {
|
|
80
|
+
readonly code: 'E-323';
|
|
81
|
+
readonly detail?: string;
|
|
82
|
+
};
|
|
79
83
|
readonly approval: {
|
|
80
84
|
readonly mergeApproval: boolean;
|
|
81
85
|
readonly platformWrite?: boolean;
|
|
@@ -89,6 +93,11 @@ export interface ReadSnapshotOptions {
|
|
|
89
93
|
readonly runs: readonly RunRecord[];
|
|
90
94
|
readonly approval: boolean;
|
|
91
95
|
readonly platformWrite?: boolean;
|
|
96
|
+
/** Host data-plane capability probe; absent means available/unknown. */
|
|
97
|
+
readonly hostOnly?: () => Promise<{
|
|
98
|
+
readonly code: 'E-323';
|
|
99
|
+
readonly detail?: string;
|
|
100
|
+
} | undefined>;
|
|
92
101
|
readonly now?: () => number;
|
|
93
102
|
readonly history?: () => Promise<readonly HistoryItem[]>;
|
|
94
103
|
readonly notes?: readonly string[];
|
|
@@ -13,9 +13,10 @@ const queueOf = (state) => state === 'approved' || state === 'fuse' ? 'fuse' : s
|
|
|
13
13
|
export async function readMergeSnapshot(options) {
|
|
14
14
|
const now = options.now ?? (() => Date.now());
|
|
15
15
|
const readAt = now();
|
|
16
|
+
const hostOnly = options.hostOnly === undefined ? undefined : await options.hostOnly().catch((error) => ({ code: 'E-323', detail: error instanceof Error ? error.message : String(error) }));
|
|
16
17
|
const readiness = await readV4Readiness(options.io, options.home);
|
|
17
18
|
if (!readiness.ok) {
|
|
18
|
-
return { wireVersion: 1, schemaVersion: 4, readAt, queueRevision: 'blocked', consistency: 'eventual', blocked: { code: readiness.code, message: readiness.message, readiness }, rows: [], stages: [], runs: [], reports: [], report: null, history: [], bodyRefs: [], doctor: { counts: emptyCounts, staging: { state: 'absent', dirs: 0 }, rows: [] }, approval: { mergeApproval: options.approval, ...(options.platformWrite === undefined ? {} : { platformWrite: options.platformWrite }) }, notes: options.notes ?? [], degraded: true };
|
|
19
|
+
return { wireVersion: 1, schemaVersion: 4, readAt, queueRevision: 'blocked', consistency: 'eventual', ...(hostOnly === undefined ? {} : { hostOnly }), blocked: { code: readiness.code, message: readiness.message, readiness }, rows: [], stages: [], runs: [], reports: [], report: null, history: [], bodyRefs: [], doctor: { counts: emptyCounts, staging: { state: 'absent', dirs: 0 }, rows: [] }, approval: { mergeApproval: options.approval, ...(options.platformWrite === undefined ? {} : { platformWrite: options.platformWrite }) }, notes: options.notes ?? [], degraded: true };
|
|
19
20
|
}
|
|
20
21
|
const ledger = await readPairLedgerV4(options.io, options.home);
|
|
21
22
|
// Read-time derivation only: a stage past its TTL reads as expired even when the
|
|
@@ -31,8 +32,12 @@ export async function readMergeSnapshot(options) {
|
|
|
31
32
|
for (const row of ledger?.rows ?? []) {
|
|
32
33
|
const stage = stages.find(item => item.rowId === row.id) ?? null;
|
|
33
34
|
const queue = queueOf(row.state);
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
// A row's b is a live source until it is actually archived; only then is the archive
|
|
36
|
+
// body the addressable one. Pending/fuse/verify rows therefore expose two live bodies.
|
|
37
|
+
const bSide = live !== undefined && live.managed(row.b) ? 'live' : 'archive';
|
|
38
|
+
const bodies = [{ rowId: row.id, name: row.a, side: 'live' }, { rowId: row.id, name: row.b, side: bSide }];
|
|
39
|
+
const skippedExpired = row.state === 'skipped' && row.expiredAt !== undefined;
|
|
40
|
+
const expiredKind = skippedExpired ? 'skipped-ttl' : persistedExpiry(row) ?? pendingExpiry(row, live);
|
|
36
41
|
rows.push({ row, stage, bodies, queue, expired: expiredKind !== null, ...(expiredKind === null ? {} : { expiredKind }) });
|
|
37
42
|
bodyRefs.push(...bodies);
|
|
38
43
|
}
|
|
@@ -69,7 +74,7 @@ export async function readMergeSnapshot(options) {
|
|
|
69
74
|
&& rows.some(item => item.row.pairKey === entry.pairKey && (item.row.state === 'verify' || item.row.state === 'pending' || item.row.state === 'approved' || item.row.state === 'fuse')));
|
|
70
75
|
if (ledgerConflicts.length > 0)
|
|
71
76
|
notes.push('ledger-conflict: pair-index says completed but the ledger still has it active (' + String(ledgerConflicts.length) + ')');
|
|
72
|
-
return { wireVersion: 1, schemaVersion: 4, readAt, queueRevision, consistency: 'eventual', rows, stages, runs, reports, report: reports.at(-1)?.report ?? null, history, bodyRefs, doctor, approval: { mergeApproval: options.approval, ...(options.platformWrite === undefined ? {} : { platformWrite: options.platformWrite }) }, notes, degraded: notes.length > 0 };
|
|
77
|
+
return { wireVersion: 1, schemaVersion: 4, readAt, queueRevision, consistency: 'eventual', ...(hostOnly === undefined ? {} : { hostOnly }), rows, stages, runs, reports, report: reports.at(-1)?.report ?? null, history, bodyRefs, doctor, approval: { mergeApproval: options.approval, ...(options.platformWrite === undefined ? {} : { platformWrite: options.platformWrite }) }, notes, degraded: notes.length > 0 };
|
|
73
78
|
}
|
|
74
79
|
async function readHistory(io, home) {
|
|
75
80
|
const receipts = await readCommitReceipts(io, home);
|
|
@@ -106,7 +106,7 @@ export async function reconcileMergeState(options) {
|
|
|
106
106
|
continue;
|
|
107
107
|
const entry = byRow.get(row.id);
|
|
108
108
|
if (usable.get(row.id) === true) {
|
|
109
|
-
const moved = tx.transition(row.id, { type: 'staged' }, current.version);
|
|
109
|
+
const moved = tx.transition(row.id, { type: 'staged', ...(entry === undefined ? {} : { stagingRef: entry.ref, stagingExpiresAt: entry.expiresAt }) }, current.version);
|
|
110
110
|
if (moved.ok) {
|
|
111
111
|
actions.push({ kind: 'approved-fused', rowId: row.id, from: 'approved', to: 'fuse', code: 'stage-present' });
|
|
112
112
|
if (entry !== undefined && entry.state === 'reserved')
|
|
@@ -49,6 +49,8 @@ export interface FuseRowV4 extends RowBaseV4 {
|
|
|
49
49
|
readonly sourceRevision?: string;
|
|
50
50
|
readonly targetRevision?: string;
|
|
51
51
|
readonly runId?: string;
|
|
52
|
+
readonly stagingRef?: string;
|
|
53
|
+
readonly stagingExpiresAt?: number;
|
|
52
54
|
readonly attempts: number;
|
|
53
55
|
}
|
|
54
56
|
export interface SkippedRowV4 extends RowBaseV4 {
|
|
@@ -76,6 +78,8 @@ export type V4Event = {
|
|
|
76
78
|
readonly runId?: string;
|
|
77
79
|
} | {
|
|
78
80
|
readonly type: 'staged';
|
|
81
|
+
readonly stagingRef?: string;
|
|
82
|
+
readonly stagingExpiresAt?: number;
|
|
79
83
|
} | {
|
|
80
84
|
readonly type: 'committed';
|
|
81
85
|
readonly unitId: CommitUnitRef;
|
|
@@ -24,7 +24,7 @@ export function transitionV4(row, event, expectedVersion, at = new Date().toISOS
|
|
|
24
24
|
if (event.type === 'auto-approved')
|
|
25
25
|
return { ok: true, row: advance(row, at, { state: 'approved', ...(event.runId === undefined ? {} : { runId: event.runId }) }), version: expectedVersion + 1 };
|
|
26
26
|
if (event.type === 'staged')
|
|
27
|
-
return { ok: true, row: advance(row, at, { state: 'fuse' }), version: expectedVersion + 1 };
|
|
27
|
+
return { ok: true, row: advance(row, at, { state: 'fuse', ...(event.stagingRef === undefined ? {} : { stagingRef: event.stagingRef }), ...(event.stagingExpiresAt === undefined ? {} : { stagingExpiresAt: event.stagingExpiresAt }) }), version: expectedVersion + 1 };
|
|
28
28
|
if (event.type === 'skipped')
|
|
29
29
|
return { ok: true, row: advance(row, at, { state: 'skipped', reason: event.reason, ...(event.origin === undefined ? {} : { origin: event.origin }), ...(event.skipSource === undefined ? {} : { skipSource: event.skipSource }) }), version: expectedVersion + 1 };
|
|
30
30
|
if (event.type === 'expired')
|
|
@@ -36,15 +36,15 @@ export function transitionV4(row, event, expectedVersion, at = new Date().toISOS
|
|
|
36
36
|
return illegal('pending/' + event.type);
|
|
37
37
|
case 'approved':
|
|
38
38
|
if (event.type === 'staged')
|
|
39
|
-
return { ok: true, row: advance(row, at, { state: 'fuse' }), version: expectedVersion + 1 };
|
|
39
|
+
return { ok: true, row: advance(row, at, { state: 'fuse', ...(event.stagingRef === undefined ? {} : { stagingRef: event.stagingRef }), ...(event.stagingExpiresAt === undefined ? {} : { stagingExpiresAt: event.stagingExpiresAt }) }), version: expectedVersion + 1 };
|
|
40
40
|
if (event.type === 'committed')
|
|
41
41
|
return { ok: true, row: null, version: expectedVersion + 1 };
|
|
42
42
|
if (event.type === 'skipped')
|
|
43
43
|
return { ok: true, row: advance(row, at, { state: 'skipped', reason: event.reason, ...(event.skipSource === undefined ? {} : { skipSource: event.skipSource }) }), version: expectedVersion + 1 };
|
|
44
|
+
// A failure is never a "no merge" decision. Keep the row in fuse with the failure
|
|
45
|
+
// recorded; retryable is a property of the failure, not a different queue.
|
|
44
46
|
if (event.type === 'failed')
|
|
45
|
-
return event.failure.
|
|
46
|
-
? { ok: true, row: advance(row, at, { state: 'fuse', failure: event.failure, attempts: row.attempts + 1 }), version: expectedVersion + 1 }
|
|
47
|
-
: { ok: true, row: advance(row, at, { state: 'skipped', reason: event.failure.code, failure: event.failure, attempts: row.attempts + 1 }), version: expectedVersion + 1 };
|
|
47
|
+
return { ok: true, row: advance(row, at, { state: 'fuse', failure: event.failure, attempts: row.attempts + 1 }), version: expectedVersion + 1 };
|
|
48
48
|
if (event.type === 'superseded')
|
|
49
49
|
return { ok: true, row: null, version: expectedVersion + 1 };
|
|
50
50
|
return illegal('approved/' + event.type);
|
|
@@ -54,9 +54,7 @@ export function transitionV4(row, event, expectedVersion, at = new Date().toISOS
|
|
|
54
54
|
if (event.type === 'skipped')
|
|
55
55
|
return { ok: true, row: advance(row, at, { state: 'skipped', reason: event.reason, ...(event.skipSource === undefined ? {} : { skipSource: event.skipSource }) }), version: expectedVersion + 1 };
|
|
56
56
|
if (event.type === 'failed')
|
|
57
|
-
return event.failure.
|
|
58
|
-
? { ok: true, row: advance(row, at, { state: 'fuse', failure: event.failure, attempts: row.attempts + 1 }), version: expectedVersion + 1 }
|
|
59
|
-
: { ok: true, row: advance(row, at, { state: 'skipped', reason: event.failure.code, failure: event.failure, attempts: row.attempts + 1 }), version: expectedVersion + 1 };
|
|
57
|
+
return { ok: true, row: advance(row, at, { state: 'fuse', failure: event.failure, attempts: row.attempts + 1 }), version: expectedVersion + 1 };
|
|
60
58
|
if (event.type === 'superseded')
|
|
61
59
|
return { ok: true, row: null, version: expectedVersion + 1 };
|
|
62
60
|
return illegal('fuse/' + event.type);
|
|
@@ -97,7 +95,7 @@ export function v3ToV4(row) {
|
|
|
97
95
|
if (row.state === 'approved')
|
|
98
96
|
return { ...base, state: 'approved', target: row.target, decision: row.decision, ...(row.sourceRevision === undefined ? {} : { sourceRevision: row.sourceRevision }), ...(row.runId === undefined ? {} : { runId: row.runId }) };
|
|
99
97
|
if (row.state === 'fuse')
|
|
100
|
-
return { ...base, state: 'fuse', target: row.target, decision: row.decision, ...(row.sourceRevision === undefined ? {} : { sourceRevision: row.sourceRevision }), ...(row.runId === undefined ? {} : { runId: row.runId }) };
|
|
98
|
+
return { ...base, state: 'fuse', target: row.target, decision: row.decision, ...(row.sourceRevision === undefined ? {} : { sourceRevision: row.sourceRevision }), ...(row.runId === undefined ? {} : { runId: row.runId }), ...(row.staging?.kind === 'present' ? { stagingRef: row.staging.ref, stagingExpiresAt: row.staging.expiresAt } : row.staging?.kind === 'expired' && row.staging.ref !== undefined ? { stagingRef: row.staging.ref, stagingExpiresAt: row.staging.expiresAt } : {}) };
|
|
101
99
|
return { ...base, state: 'skipped', reason: row.reason, ...(row.expiredAt === undefined ? {} : { expiredAt: row.expiredAt }) };
|
|
102
100
|
}
|
|
103
101
|
export function applyV4Write(rows, write, at) {
|
|
@@ -151,6 +149,8 @@ export function v4ToLegacy(row) {
|
|
|
151
149
|
...((row.state === 'pending' || row.state === 'approved' || row.state === 'fuse') && row.sourceRevision !== undefined ? { sourceRevision: row.sourceRevision } : {}),
|
|
152
150
|
...((row.state === 'pending' || row.state === 'approved' || row.state === 'fuse') && row.targetRevision !== undefined ? { targetRevision: row.targetRevision } : {}),
|
|
153
151
|
...(row.runId === undefined ? {} : { runId: row.runId }),
|
|
152
|
+
...(row.state === 'fuse' && row.stagingRef !== undefined ? { stagingRef: row.stagingRef } : {}),
|
|
153
|
+
...(row.state === 'fuse' && row.stagingExpiresAt !== undefined ? { stagingExpiresAt: row.stagingExpiresAt } : {}),
|
|
154
154
|
...(row.origin === undefined ? {} : { origin: row.origin }),
|
|
155
155
|
...(row.attempts === undefined ? {} : { attempts: row.attempts }),
|
|
156
156
|
createdAt: row.createdAt, updatedAt: row.updatedAt,
|
|
@@ -6,7 +6,16 @@ export interface MergeRunMeta {
|
|
|
6
6
|
readonly pairCount?: number;
|
|
7
7
|
/** The pair a single-pair command (pair/approve/resume) is working on; absent for scan. */
|
|
8
8
|
readonly pairKey?: string;
|
|
9
|
+
/** Staging tree this run authors/commits; absent for scan before staging. */
|
|
10
|
+
readonly stagingRef?: string;
|
|
11
|
+
/** Pair rows this run claims, when it is a single-pair command. */
|
|
12
|
+
readonly pairs?: readonly {
|
|
13
|
+
readonly pairKey: string;
|
|
14
|
+
readonly state: 'pending' | 'approved' | 'fused' | 'skipped';
|
|
15
|
+
}[];
|
|
9
16
|
readonly parentRunId?: string;
|
|
17
|
+
/** Owner process; lets a shared run-meta distinguish a live foreign plane from a dead writer. */
|
|
18
|
+
readonly pid?: number;
|
|
10
19
|
readonly phase: MergeRunPhase;
|
|
11
20
|
readonly stopReason?: string;
|
|
12
21
|
readonly approval?: 'pending' | 'approved' | 'rejected';
|
|
@@ -80,12 +80,17 @@ export declare function readStageIndex(io: EvolutionIoLike, home: string, rowId:
|
|
|
80
80
|
export declare function readStageEntry(io: EvolutionIoLike, home: string, rowId: string): Promise<StageEntry | null>;
|
|
81
81
|
export declare function reserveStage(io: EvolutionIoLike, home: string, entry: StageEntry): Promise<void>;
|
|
82
82
|
export declare function presentStage(io: EvolutionIoLike, home: string, entry: StageEntry): Promise<void>;
|
|
83
|
-
export declare function dropStage(io: EvolutionIoLike, home: string, rowId: string, ref?: string): Promise<void>;
|
|
83
|
+
export declare function dropStage(io: EvolutionIoLike, home: string, rowId: string, ref?: string, onError?: (path: string, error: unknown) => void): Promise<void>;
|
|
84
|
+
export interface StageCleanupFailure {
|
|
85
|
+
readonly path: string;
|
|
86
|
+
readonly message: string;
|
|
87
|
+
}
|
|
84
88
|
export interface StageCleanupResult {
|
|
85
89
|
readonly removed: readonly string[];
|
|
86
90
|
readonly expired: readonly string[];
|
|
87
91
|
readonly kept: readonly string[];
|
|
88
92
|
readonly orphans: readonly string[];
|
|
93
|
+
readonly failures: readonly StageCleanupFailure[];
|
|
89
94
|
}
|
|
90
95
|
/** Bounded cleanup: reserved/present TTL, pending/verify stale index removal, terminal/absent rows, orphan trees. */
|
|
91
96
|
export declare function cleanupStage(io: EvolutionIoLike, home: string, now: number, rows: ReadonlyMap<string, {
|
|
@@ -205,10 +205,13 @@ export async function reserveStage(io, home, entry) {
|
|
|
205
205
|
export async function presentStage(io, home, entry) {
|
|
206
206
|
await writeStageIndex(io, home, { rowId: entry.rowId, pairKey: entry.pairKey, ref: entry.ref, expiresAt: entry.expiresAt, ...(entry.runId === undefined ? {} : { runId: entry.runId }), createdAt: entry.createdAt, state: 'present' });
|
|
207
207
|
}
|
|
208
|
-
export async function dropStage(io, home, rowId, ref) {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
208
|
+
export async function dropStage(io, home, rowId, ref, onError) {
|
|
209
|
+
const index = join(home, 'merge', 'staging-index', encodeURIComponent(rowId) + '.json');
|
|
210
|
+
await io.remove(index).catch(error => onError?.(index, error));
|
|
211
|
+
if (ref !== undefined && ref !== '') {
|
|
212
|
+
const tree = join(home, 'merge', 'runs', ref);
|
|
213
|
+
await io.remove(tree).catch(error => onError?.(tree, error));
|
|
214
|
+
}
|
|
212
215
|
}
|
|
213
216
|
/** Bounded cleanup: reserved/present TTL, pending/verify stale index removal, terminal/absent rows, orphan trees. */
|
|
214
217
|
export async function cleanupStage(io, home, now, rows, limit = 100) {
|
|
@@ -216,6 +219,8 @@ export async function cleanupStage(io, home, now, rows, limit = 100) {
|
|
|
216
219
|
const expired = [];
|
|
217
220
|
const kept = [];
|
|
218
221
|
const orphans = [];
|
|
222
|
+
const failures = [];
|
|
223
|
+
const fail = (path, error) => { failures.push({ path, message: error instanceof Error ? error.message : String(error) }); };
|
|
219
224
|
const refs = new Set();
|
|
220
225
|
const indexRoot = join(home, 'merge', 'staging-index');
|
|
221
226
|
const names = await io.exists(indexRoot) ? await listOrEmpty(io, indexRoot) : [];
|
|
@@ -239,7 +244,7 @@ export async function cleanupStage(io, home, now, rows, limit = 100) {
|
|
|
239
244
|
const row = rows.get(entry.rowId);
|
|
240
245
|
const terminal = row === undefined || row.state === 'skipped';
|
|
241
246
|
if (terminal || row?.state === 'pending' || row?.state === 'verify') {
|
|
242
|
-
await dropStage(io, home, entry.rowId, entry.ref);
|
|
247
|
+
await dropStage(io, home, entry.rowId, entry.ref, fail);
|
|
243
248
|
removed.push(entry.rowId);
|
|
244
249
|
continue;
|
|
245
250
|
}
|
|
@@ -250,11 +255,13 @@ export async function cleanupStage(io, home, now, rows, limit = 100) {
|
|
|
250
255
|
try {
|
|
251
256
|
await writeStagingIndex(io, home, { rowId: entry.rowId, pairKey: typeof entry.pairKey === 'string' ? entry.pairKey : '', ref: entry.ref, expiresAt: entry.expiresAt, createdAt: typeof entry.createdAt === 'number' ? entry.createdAt : now, state: 'expired' });
|
|
252
257
|
}
|
|
253
|
-
catch { /* the read face still derives expired */
|
|
258
|
+
catch (error) { /* the read face still derives expired; visibility is still required */
|
|
259
|
+
fail(join(indexRoot, name), error);
|
|
260
|
+
}
|
|
254
261
|
expired.push(entry.rowId);
|
|
255
262
|
continue;
|
|
256
263
|
}
|
|
257
|
-
await dropStage(io, home, entry.rowId, entry.ref);
|
|
264
|
+
await dropStage(io, home, entry.rowId, entry.ref, fail);
|
|
258
265
|
removed.push(entry.rowId);
|
|
259
266
|
continue;
|
|
260
267
|
}
|
|
@@ -266,10 +273,10 @@ export async function cleanupStage(io, home, now, rows, limit = 100) {
|
|
|
266
273
|
const prefix = run + '/';
|
|
267
274
|
if ([...refs].some(ref => ref === run || ref.startsWith(prefix)))
|
|
268
275
|
continue;
|
|
269
|
-
await io.remove(join(runRoot, run)).catch(
|
|
276
|
+
await io.remove(join(runRoot, run)).catch(error => fail(join(runRoot, run), error));
|
|
270
277
|
orphans.push(run);
|
|
271
278
|
}
|
|
272
|
-
return { removed, expired, kept, orphans };
|
|
279
|
+
return { removed, expired, kept, orphans, failures };
|
|
273
280
|
}
|
|
274
281
|
/** Read all production v2 stage entries. */
|
|
275
282
|
export async function readAllStages(io, home) {
|
|
@@ -14,7 +14,11 @@ export function buildValidationPlan(input) {
|
|
|
14
14
|
const existing = new Set(input.existingSupport ?? []);
|
|
15
15
|
const orphanFiles = [...existing].filter(path => !staged.has(path)).sort();
|
|
16
16
|
const byPath = new Map();
|
|
17
|
-
|
|
17
|
+
// Conflict means TWO SKILLS contributing different bytes at one path. Staged edits and
|
|
18
|
+
// the target's previous file are not conflicts: the target file is a CAS baseline that
|
|
19
|
+
// round 2 is expected to update, so including either here would block every support edit.
|
|
20
|
+
const supportSources = input.supportSources ?? [];
|
|
21
|
+
for (const file of supportSources) {
|
|
18
22
|
const entry = byPath.get(file.path) ?? { contents: new Set(), sources: [] };
|
|
19
23
|
entry.contents.add(file.content);
|
|
20
24
|
entry.sources.push(file.source);
|
package/lib/types/index.js
CHANGED
|
@@ -214,6 +214,8 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
214
214
|
const cleanup = await cleanupStage(io, home, Date.now(), new Map(v4.state.rows.map(row => [row.id, { state: row.state }])));
|
|
215
215
|
if (cleanup.expired.length > 0 || cleanup.removed.length > 0)
|
|
216
216
|
ctx.logger.info('merge stage cleanup: ' + String(cleanup.expired.length) + ' expired, ' + String(cleanup.removed.length) + ' removed, ' + String(cleanup.orphans.length) + ' orphan(s)');
|
|
217
|
+
if (cleanup.failures.length > 0)
|
|
218
|
+
ctx.logger.warn('merge stage cleanup failed for ' + String(cleanup.failures.length) + ' path(s): ' + cleanup.failures.map(item => item.path + '=' + item.message).join('; '));
|
|
217
219
|
// Reconcile is the one row-state convergence writer; startup simply invokes it.
|
|
218
220
|
// A live library is only supplied when it is non-empty (an empty/unreadable list
|
|
219
221
|
// must never authorize source-gone removal).
|
|
@@ -230,7 +232,8 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
230
232
|
}
|
|
231
233
|
})().catch((error) => { ctx.logger.warn('merge recovery failed: ' + (error instanceof Error ? error.message : String(error))); });
|
|
232
234
|
}).catch((error) => { ctx.logger.warn('merge state readiness failed: ' + (error instanceof Error ? error.message : String(error))); });
|
|
233
|
-
const
|
|
235
|
+
const platformChannel = createSubagentChannel(ctx);
|
|
236
|
+
const channel = subagentChannelAdapter(platformChannel);
|
|
234
237
|
const stage = new StageToolRegistry();
|
|
235
238
|
const engineInput = resolveEngineInput({ modelsRoot: params.mergeModelsRoot });
|
|
236
239
|
const engineConfig = engineConfigFromRoot(engineInput.modelsRoot);
|
|
@@ -273,7 +276,15 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
273
276
|
});
|
|
274
277
|
source = {
|
|
275
278
|
async snapshot() {
|
|
276
|
-
return await readMergeSnapshot({ io, home, runs: runs.runs('merge'), approval: params.mergeApproval, platformWrite,
|
|
279
|
+
return await readMergeSnapshot({ io, home, runs: runs.runs('merge'), approval: params.mergeApproval, platformWrite, hostOnly: async () => {
|
|
280
|
+
try {
|
|
281
|
+
const state = await platformChannel.prepare({ kind: 'reviewer', allow: ['skill'] });
|
|
282
|
+
return state.kind === 'unavailable' ? { code: 'E-323', detail: state.reason } : undefined;
|
|
283
|
+
}
|
|
284
|
+
catch (error) {
|
|
285
|
+
return { code: 'E-323', detail: error instanceof Error ? error.message : String(error) };
|
|
286
|
+
}
|
|
287
|
+
}, live: async () => liveRowFactsOf(await readSkillCards(skills)), history: async () => {
|
|
277
288
|
const items = new Map();
|
|
278
289
|
const receipts = [...await readCommitReceipts(io, home)].sort((a, b) => a.at - b.at);
|
|
279
290
|
const generationOf = new Map(receipts.map((receipt, index) => [receipt.runId, index + 1]));
|
|
@@ -318,6 +329,7 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
318
329
|
return;
|
|
319
330
|
const runtime = createMergeRuntime({
|
|
320
331
|
io, home, registry: runs, skills, now: () => Date.now(), params, platformWrite,
|
|
332
|
+
...(defaultModel === undefined ? {} : { model: defaultModel.currentSelection().model }),
|
|
321
333
|
channel, parent: undefined, allow: ['merge_stage'], timeoutMinutes: params.mergeRunTimeoutMinutes,
|
|
322
334
|
isBlocked: async (name) => {
|
|
323
335
|
const curator = ctx.get('evolutionCurator');
|