@lmzhen/dsh-evolution-core 0.30.17 → 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.
- package/lib/composition/index.js +2 -2
- package/lib/{composition-D8Y3-ROd.js → composition-DSesmTvf.js} +3 -3
- package/lib/{http-routes-ifJKGnu6.js → http-routes-D1Ty6nnh.js} +2 -0
- package/lib/http-routes.js +1 -1
- package/lib/index.js +2863 -2049
- package/lib/subagent/index.js +1 -1
- package/lib/types/composition/composer.js +1 -1
- package/lib/types/frontmatter.js +1 -1
- package/lib/types/http-routes.d.ts +2 -0
- package/lib/types/http-routes.js +2 -0
- package/lib/types/index.d.ts +4 -0
- package/lib/types/index.js +4 -0
- package/lib/types/params.js +0 -1
- package/lib/types/platform-services.d.ts +1 -1
- package/lib/types/platform-services.js +2 -2
- package/lib/types/redact.js +2 -2
- package/lib/types/run-registry.d.ts +8 -0
- package/lib/types/run-registry.js +20 -0
- package/lib/types/skill-store.d.ts +10 -0
- package/lib/types/skill-store.js +70 -3
- package/lib/types/tree-lock.d.ts +3 -0
- package/lib/types/tree-lock.js +13 -5
- package/lib/types/unit-backend.d.ts +5 -0
- package/lib/types/unit-backend.js +49 -0
- package/lib/types/unit-journal.d.ts +29 -0
- package/lib/types/unit-journal.js +50 -0
- package/lib/types/unit-recovery.d.ts +5 -0
- package/lib/types/unit-recovery.js +12 -0
- package/lib/types/unit-storage.d.ts +83 -0
- package/lib/types/unit-storage.js +124 -0
- package/lib/types/unit-transaction.d.ts +195 -0
- package/lib/types/unit-transaction.js +445 -0
- package/package.json +1 -1
- package/lib/{ensure-MEYMj9jn.js → ensure-BuWsjPGD.js} +1 -1
package/lib/subagent/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { h as restrictedPresetId, n as ensureRestrictedPreset, r as restrictedPresetState } from "../ensure-
|
|
1
|
+
import { h as restrictedPresetId, n as ensureRestrictedPreset, r as restrictedPresetState } from "../ensure-BuWsjPGD.js";
|
|
2
2
|
//#region lib/types/subagent/readiness.js
|
|
3
3
|
function serviceOf$1(ctx, name) {
|
|
4
4
|
return ctx.get(name);
|
|
@@ -372,7 +372,7 @@ export function presetRowBlock(patchText, rowId) {
|
|
|
372
372
|
const span = rowBlockSpan(lines, rowId);
|
|
373
373
|
if (span === null)
|
|
374
374
|
return null;
|
|
375
|
-
const block =
|
|
375
|
+
const block = lines.slice(span.start, span.end);
|
|
376
376
|
while (block.length > 0 && (block[block.length - 1] ?? '').trim() === '')
|
|
377
377
|
block.pop();
|
|
378
378
|
return block.join('\n') + '\n';
|
package/lib/types/frontmatter.js
CHANGED
|
@@ -385,7 +385,7 @@ export function yamlPlainScalarNeedsQuotes(value) {
|
|
|
385
385
|
return true;
|
|
386
386
|
if (/^(?:[-+]?\.inf|\.nan)$/i.test(value))
|
|
387
387
|
return true;
|
|
388
|
-
if (/^[-?:,[\]{}#&*!|>'
|
|
388
|
+
if (/^[-?:,[\]{}#&*!|>'"%@`\s]/.test(value))
|
|
389
389
|
return true;
|
|
390
390
|
return false;
|
|
391
391
|
}
|
|
@@ -56,6 +56,8 @@ export declare const HOST_ROUTES: {
|
|
|
56
56
|
readonly merge: {
|
|
57
57
|
readonly view: "/api/dsh-evolution/merge/view";
|
|
58
58
|
readonly rows: "/api/dsh-evolution/merge/rows";
|
|
59
|
+
readonly run: "/api/dsh-evolution/merge/run";
|
|
60
|
+
readonly runs: "/api/dsh-evolution/merge/runs";
|
|
59
61
|
readonly body: "/api/dsh-evolution/merge/body";
|
|
60
62
|
readonly history: "/api/dsh-evolution/merge/history";
|
|
61
63
|
};
|
package/lib/types/http-routes.js
CHANGED
|
@@ -53,6 +53,8 @@ export const HOST_ROUTES = {
|
|
|
53
53
|
merge: {
|
|
54
54
|
view: '/api/dsh-evolution/merge/view',
|
|
55
55
|
rows: '/api/dsh-evolution/merge/rows',
|
|
56
|
+
run: '/api/dsh-evolution/merge/run',
|
|
57
|
+
runs: '/api/dsh-evolution/merge/runs',
|
|
56
58
|
body: '/api/dsh-evolution/merge/body',
|
|
57
59
|
history: '/api/dsh-evolution/merge/history',
|
|
58
60
|
},
|
package/lib/types/index.d.ts
CHANGED
|
@@ -33,6 +33,10 @@
|
|
|
33
33
|
*/
|
|
34
34
|
export * from './citations.ts';
|
|
35
35
|
export * from './tree-write.ts';
|
|
36
|
+
export * from './unit-storage.ts';
|
|
37
|
+
export * from './unit-journal.ts';
|
|
38
|
+
export * from './unit-transaction.ts';
|
|
39
|
+
export * from './unit-backend.ts';
|
|
36
40
|
export * from './package-identity.ts';
|
|
37
41
|
export * from './write-authority.ts';
|
|
38
42
|
export * from './write-gates.ts';
|
package/lib/types/index.js
CHANGED
|
@@ -34,6 +34,10 @@
|
|
|
34
34
|
export * from "./citations.js";
|
|
35
35
|
// G1/1.1: the ONE authority behind a write (its type and the authorityFrom builder).
|
|
36
36
|
export * from "./tree-write.js";
|
|
37
|
+
export * from "./unit-storage.js";
|
|
38
|
+
export * from "./unit-journal.js";
|
|
39
|
+
export * from "./unit-transaction.js";
|
|
40
|
+
export * from "./unit-backend.js";
|
|
37
41
|
export * from "./package-identity.js";
|
|
38
42
|
export * from "./write-authority.js";
|
|
39
43
|
export * from "./write-gates.js";
|
package/lib/types/params.js
CHANGED
|
@@ -208,7 +208,6 @@ export const PARAM_EXPOSURE = Object.freeze([
|
|
|
208
208
|
{ id: 'mergeSkippedTtlDays', group: 'merge', tier: 'E3', authority: 'cordis', owner: 'evolution-merge', applies: 'live', docAnchor: 'PARAMETERS.md#merge', summary: 'Days a skipped pair stays out of screening; 0 means it never expires.', label: '无需合并保留天数', hint: '判定无需合并的对,多少天内不再参与筛选;0 表示一直保留。', control: 'number', unit: '天', values: '', card: 'evolution-merge' },
|
|
209
209
|
{ id: 'mergeRunTimeoutMinutes', group: 'merge', tier: 'E3', authority: 'cordis', owner: 'evolution-merge', applies: 'live', docAnchor: 'PARAMETERS.md#merge', summary: 'Wall-clock budget of one merge run; 0 means no limit.', label: '单次运行上限', hint: '一次合并运行最多持续多少分钟;0 表示不设上限。', control: 'number', unit: '分钟', values: '', card: 'evolution-merge' },
|
|
210
210
|
{ id: 'mergePairTimeoutMinutes', group: 'merge', tier: 'E2', authority: 'cordis', owner: 'evolution-merge', applies: 'none', docAnchor: 'PARAMETERS.md#merge', summary: 'Wall-clock budget of one pair round (a stuck child guard).', why: 'internal' },
|
|
211
|
-
{ id: 'mergeModelsRoot', group: 'merge', tier: 'E2', authority: 'cordis', owner: 'evolution-merge', applies: 'none', docAnchor: 'PARAMETERS.md#merge', summary: 'Local model root containing the int8 and pca64 model directories; empty falls back to SKILL_MERGE_MODELS_ROOT. No auto provisioning.', why: 'path' },
|
|
212
211
|
]);
|
|
213
212
|
/** The settings namespace each OWNER PACKAGE registers (design §7.2).
|
|
214
213
|
*
|
|
@@ -20,7 +20,7 @@ export interface PlatformServiceProbe {
|
|
|
20
20
|
/** Where the probe runs. */
|
|
21
21
|
readonly side: 'host' | 'client';
|
|
22
22
|
}
|
|
23
|
-
/** The
|
|
23
|
+
/** The 25 probes, in service-name order (the guard checks this list against the tree). */
|
|
24
24
|
export declare const PLATFORM_SERVICE_PROBES: readonly PlatformServiceProbe[];
|
|
25
25
|
/**
|
|
26
26
|
* The HOST-side probes that do not resolve in this runtime — what `/evolution doctor` prints as
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* `client` probes run in the browser half, where the host has no view of the seats.
|
|
12
12
|
* @module @lmzhen/dsh-evolution-core
|
|
13
13
|
*/
|
|
14
|
-
/** The
|
|
14
|
+
/** The 25 probes, in service-name order (the guard checks this list against the tree). */
|
|
15
15
|
export const PLATFORM_SERVICE_PROBES = Object.freeze([
|
|
16
16
|
{ service: 'agents', feature: 'review and curator subagent wake-ups', side: 'host' },
|
|
17
17
|
{ service: 'approval', feature: 'the platform approval policy the approval row shadows', side: 'host' },
|
|
@@ -20,9 +20,9 @@ export const PLATFORM_SERVICE_PROBES = Object.freeze([
|
|
|
20
20
|
{ service: 'evolutionApproval', feature: 'pending writes: list, approve and reject from the command surface', side: 'host' },
|
|
21
21
|
{ service: 'evolutionCurator', feature: '/evolution curator subcommands, skill undo and history', side: 'host' },
|
|
22
22
|
{ service: 'evolutionIo', feature: 'every durable read and write (the IO seam)', side: 'host' },
|
|
23
|
+
{ service: 'evolutionMerge', feature: 'merge rows and run state', side: 'host' },
|
|
23
24
|
{ service: 'hmr', feature: 'the preset install read-merge-write commit (the platform HMR lock serializes it)', side: 'host' },
|
|
24
25
|
{ service: 'evolutionMemoryBudget', feature: 'the memory-budget divergence section of this report', side: 'host' },
|
|
25
|
-
{ service: 'evolutionMerge', feature: 'the merge command and its read-only panel routes', side: 'host' },
|
|
26
26
|
{ service: 'evolutionPolicy', feature: 'the policy snapshot that shadows row knobs', side: 'host' },
|
|
27
27
|
{ service: 'evolutionReplay', feature: '/evolution replay', side: 'host' },
|
|
28
28
|
{ service: 'evolutionState', feature: 'the state facade behind activity and replay', side: 'host' },
|
package/lib/types/redact.js
CHANGED
|
@@ -30,7 +30,7 @@ const SECRET_PATTERNS = [
|
|
|
30
30
|
['google api key', /AIza[0-9A-Za-z_-]{30,}/g],
|
|
31
31
|
// F-335 (0.3.23): case-insensitive (`bearer`), and `\s+` so a tab or run of
|
|
32
32
|
// spaces between `Bearer` and the token is still masked (`Bearer\t...`).
|
|
33
|
-
['bearer credential', /Bearer[\s]+[a-z0-9._
|
|
33
|
+
['bearer credential', /Bearer[\s]+[a-z0-9._~+/=-]{16,}/gi],
|
|
34
34
|
// R3 (round-2 audit): HTTP Basic — `Authorization: Basic <base64>` crossed
|
|
35
35
|
// every model boundary: `Basic` was in no value-shape row and
|
|
36
36
|
// `authorization` in no keyword set, so the base64 user:password blob
|
|
@@ -117,7 +117,7 @@ const PEM_PRIVATE_KEY_PATTERN = new RegExp(`${PEM_HEAD}[\\s\\S]*?${PEM_TAIL}`, '
|
|
|
117
117
|
// secret crossed verbatim. Both anchors now tolerate an optional `\r` so the
|
|
118
118
|
// line-paired pass behaves identically on LF and CRLF input (Windows-authored
|
|
119
119
|
// .env / compose / kubectl YAML is the realistic carrier).
|
|
120
|
-
const BLOCK_KEY_ONLY_LINE = /(?:^|\s)([\w-]*[_
|
|
120
|
+
const BLOCK_KEY_ONLY_LINE = /(?:^|\s)([\w-]*[_-])?((?:token|api[_-]?key|secret|password|passwd)(?:[_-][\w-]*)?)\s*:(?:\r)?$/i;
|
|
121
121
|
// S0.3 (v37 P0-2): the two patterns above cannot express a camelCase hump —
|
|
122
122
|
// they are case-INSENSITIVE, so [A-Z] would match lowercase too. This pair
|
|
123
123
|
// decides by a case-insensitive KEYWORD test that rejects a lowercase
|
|
@@ -114,6 +114,12 @@ export interface RunRegistry {
|
|
|
114
114
|
ok: boolean;
|
|
115
115
|
note?: string;
|
|
116
116
|
}>;
|
|
117
|
+
/** Load and throw on unreadable/invalid index. */
|
|
118
|
+
ensureLoaded(): Promise<void>;
|
|
119
|
+
/** The active run after ensureLoaded; explicit read entry. */
|
|
120
|
+
findActive(kind?: RunKind): Promise<RunRecord | undefined>;
|
|
121
|
+
/** Whether this run id is currently owned by a live running process (RG0b). */
|
|
122
|
+
hasOwner(id: string): Promise<boolean>;
|
|
117
123
|
/**
|
|
118
124
|
* Register a new run, arm its cancellation, and return its handle.
|
|
119
125
|
*
|
|
@@ -146,6 +152,8 @@ export interface RunRegistry {
|
|
|
146
152
|
runs(kind?: RunKind): readonly RunRecord[];
|
|
147
153
|
/** One run by id, converged view included. */
|
|
148
154
|
find(id: string): RunRecord | undefined;
|
|
155
|
+
/** Settle a running run whose owner is gone as failed(orphan). @returns whether a run was running. */
|
|
156
|
+
settleOrphan(id: string): Promise<boolean>;
|
|
149
157
|
/** Ask a running run to stop; it settles as `cancelled`. @returns whether it was running. */
|
|
150
158
|
cancel(id: string): boolean;
|
|
151
159
|
/** Ask every running run to stop (fiber dispose). @returns how many were running. */
|
|
@@ -291,6 +291,26 @@ export function newRunRegistry(options) {
|
|
|
291
291
|
].filter((note) => note !== undefined);
|
|
292
292
|
return { ok: true, ...(notes.length > 0 ? { note: notes.join('; ') } : {}) };
|
|
293
293
|
},
|
|
294
|
+
async ensureLoaded() {
|
|
295
|
+
const loaded = await this.load();
|
|
296
|
+
if (!loaded.ok)
|
|
297
|
+
throw new Error(loaded.note ?? 'runs-unreadable');
|
|
298
|
+
},
|
|
299
|
+
async findActive(kind) {
|
|
300
|
+
await this.ensureLoaded();
|
|
301
|
+
return this.inFlight(kind);
|
|
302
|
+
},
|
|
303
|
+
async hasOwner(id) {
|
|
304
|
+
await this.ensureLoaded();
|
|
305
|
+
return this.find(id)?.state === 'running';
|
|
306
|
+
},
|
|
307
|
+
async settleOrphan(id) {
|
|
308
|
+
const run = this.find(id);
|
|
309
|
+
if (run?.state !== 'running')
|
|
310
|
+
return false;
|
|
311
|
+
await this.settle(id, { state: 'failed', failure: 'orphan', mergeStatus: 'orphan' });
|
|
312
|
+
return true;
|
|
313
|
+
},
|
|
294
314
|
async begin(kind, seed) {
|
|
295
315
|
const id = randomUUID();
|
|
296
316
|
const startedAt = now();
|
|
@@ -198,6 +198,14 @@ export interface CommitMergePlan {
|
|
|
198
198
|
}[];
|
|
199
199
|
readonly origin: WriteOrigin;
|
|
200
200
|
readonly summary: string;
|
|
201
|
+
/** Real write-phase callback used by UnitTransaction; not persisted. */
|
|
202
|
+
readonly onPhase?: (phase: 'target-pending' | 'target-applied' | 'target-written' | 'source-archiving' | 'sources-archived' | 'support-pending' | 'unit-written', detail?: {
|
|
203
|
+
source?: string;
|
|
204
|
+
}) => void | Promise<void>;
|
|
205
|
+
/** Phase-boundary cancellation; checked before target/source writes. */
|
|
206
|
+
readonly signal?: AbortSignal;
|
|
207
|
+
/** Tree-fence guard; checked before every tree/source write. */
|
|
208
|
+
readonly assertFence?: () => Promise<boolean>;
|
|
201
209
|
}
|
|
202
210
|
export interface CommitMergeResult {
|
|
203
211
|
readonly ok: boolean;
|
|
@@ -444,6 +452,8 @@ export declare class SkillLibrary {
|
|
|
444
452
|
deleteProtection(rawName: string, options?: {
|
|
445
453
|
allowBundled?: boolean | undefined;
|
|
446
454
|
}): Promise<string | null>;
|
|
455
|
+
/** Whether the target directory exists at all (a SKILL.md-less ghost still occupies the name). */
|
|
456
|
+
skillDirExists(rawName: string): Promise<boolean>;
|
|
447
457
|
isManaged(rawName: string): Promise<boolean>;
|
|
448
458
|
/** Whether the skill carries the bundled marker (curator prune-builtins eligibility). */
|
|
449
459
|
isBundled(rawName: string): Promise<boolean>;
|
package/lib/types/skill-store.js
CHANGED
|
@@ -51,7 +51,7 @@ import { contentHash, loadMutations, recordMutation } from "./mutations.js";
|
|
|
51
51
|
import { contentRetentionFeedback, HISTORY_DIR, loadVersionContent, loadVersions, recordVersions } from "./skill-history.js";
|
|
52
52
|
import { suppressedFile, usageFile } from "./usage.js";
|
|
53
53
|
import { assessStructureHealth, DEFAULT_HEALTH_THRESHOLDS, lintBody } from "./skill-health.js";
|
|
54
|
-
import { CONTENT_SPLIT_HINT, DEFAULT_SKILL_VERSION_KEEP, SKILL_NAME_RE, SUPPORT_DIRS } from "./constants.js";
|
|
54
|
+
import { CONTENT_SPLIT_HINT, DEFAULT_SKILL_VERSION_KEEP, PROTECTED_BUILTIN_SKILLS, SKILL_NAME_RE, SUPPORT_DIRS } from "./constants.js";
|
|
55
55
|
/**
|
|
56
56
|
* The CLASS boundary default authority (G1/1.1, Stage 2).
|
|
57
57
|
*
|
|
@@ -949,6 +949,13 @@ export class SkillLibrary {
|
|
|
949
949
|
}
|
|
950
950
|
return null;
|
|
951
951
|
}
|
|
952
|
+
/** Whether the target directory exists at all (a SKILL.md-less ghost still occupies the name). */
|
|
953
|
+
async skillDirExists(rawName) {
|
|
954
|
+
const name = rawName.trim();
|
|
955
|
+
if (this.badName(name) !== null)
|
|
956
|
+
return false;
|
|
957
|
+
return await this.io.exists(this.dirOf(name));
|
|
958
|
+
}
|
|
952
959
|
async isManaged(rawName) {
|
|
953
960
|
// One trim per entry: paths (dirOf), validation and messages all see the same name.
|
|
954
961
|
const name = rawName.trim();
|
|
@@ -1477,7 +1484,7 @@ export class SkillLibrary {
|
|
|
1477
1484
|
// marker never landed" inside the fsync/durability wrapper, producing a
|
|
1478
1485
|
// self-contradicting operator-facing message.
|
|
1479
1486
|
let markerWarning = '';
|
|
1480
|
-
if (origin !== 'foreground') {
|
|
1487
|
+
if (origin !== 'foreground' || options.managed === true) {
|
|
1481
1488
|
// v29 SK-05: the marker write gets the same committed-only tolerance as
|
|
1482
1489
|
// every other write in this file (A1-15) — a post-rename dir-fsync
|
|
1483
1490
|
// failure means the MARKER landed; rejecting the create as a plain
|
|
@@ -1942,9 +1949,17 @@ export class SkillLibrary {
|
|
|
1942
1949
|
if (bad)
|
|
1943
1950
|
return { ok: false, message: bad };
|
|
1944
1951
|
}
|
|
1952
|
+
if (PROTECTED_BUILTIN_SKILLS.has(target))
|
|
1953
|
+
return { ok: false, message: `目标技能「${target}」是受保护的内置技能。` };
|
|
1954
|
+
const protectedSource = sources.find(name => PROTECTED_BUILTIN_SKILLS.has(name));
|
|
1955
|
+
if (protectedSource !== undefined)
|
|
1956
|
+
return { ok: false, message: `来源技能「${protectedSource}」是受保护的内置技能。` };
|
|
1945
1957
|
return await this.serial(async () => {
|
|
1946
1958
|
const dir = this.dirOf(target);
|
|
1947
1959
|
const targetPath = join(dir, 'SKILL.md');
|
|
1960
|
+
if (!plan.targetExists && await this.io.exists(dir)) {
|
|
1961
|
+
return { ok: false, message: `目标目录「${target}」已存在但不是技能;拒绝把 SKILL.md 写进同名目录。` };
|
|
1962
|
+
}
|
|
1948
1963
|
const currentTarget = await this.io.readText(targetPath);
|
|
1949
1964
|
if (plan.targetExists !== (currentTarget !== null)) {
|
|
1950
1965
|
return { ok: false, message: plan.targetExists ? `目标技能「${target}」不存在。` : `目标技能「${target}」已存在。` };
|
|
@@ -1952,6 +1967,12 @@ export class SkillLibrary {
|
|
|
1952
1967
|
if (currentTarget !== plan.expectedTargetBody) {
|
|
1953
1968
|
return { ok: false, message: `目标技能「${target}」在计划之后被改过;拒绝提交(未写入任何内容)。` };
|
|
1954
1969
|
}
|
|
1970
|
+
if (plan.targetExists && !(await this.isManaged(target))) {
|
|
1971
|
+
return { ok: false, message: `目标技能「${target}」不是 family-managed;拒绝提交。` };
|
|
1972
|
+
}
|
|
1973
|
+
if (await this.io.exists(markerPath(dir, 'pinned'))) {
|
|
1974
|
+
return { ok: false, message: `目标技能「${target}」已被固定;拒绝提交。` };
|
|
1975
|
+
}
|
|
1955
1976
|
const targetProtection = await this.writeProtection(target, plan.origin);
|
|
1956
1977
|
if (targetProtection)
|
|
1957
1978
|
return { ok: false, message: `技能「${target}」受保护(${targetProtection})。` };
|
|
@@ -2081,6 +2102,9 @@ export class SkillLibrary {
|
|
|
2081
2102
|
expected: supportPrevious.get(file.path) ?? null,
|
|
2082
2103
|
})),
|
|
2083
2104
|
];
|
|
2105
|
+
await plan.onPhase?.('target-pending');
|
|
2106
|
+
if (plan.assertFence !== undefined && !(await plan.assertFence()))
|
|
2107
|
+
return { ok: false, message: 'tree fence lost before target write' };
|
|
2084
2108
|
const result = await this.applyTreeChange({
|
|
2085
2109
|
name: target,
|
|
2086
2110
|
origin: plan.origin,
|
|
@@ -2094,13 +2118,28 @@ export class SkillLibrary {
|
|
|
2094
2118
|
});
|
|
2095
2119
|
if (!result.ok)
|
|
2096
2120
|
return rollbackMessage(result.message, []);
|
|
2121
|
+
await plan.onPhase?.('target-applied');
|
|
2122
|
+
await plan.onPhase?.('target-written');
|
|
2123
|
+
if (plan.signal?.aborted) {
|
|
2124
|
+
const stuck = await rollbackTarget();
|
|
2125
|
+
return rollbackMessage('cancelled before sources archived', stuck);
|
|
2126
|
+
}
|
|
2097
2127
|
for (const source of sources) {
|
|
2128
|
+
if (plan.signal?.aborted) {
|
|
2129
|
+
const stuck = await rollbackSources();
|
|
2130
|
+
stuck.push(...await rollbackTarget());
|
|
2131
|
+
return rollbackMessage('cancelled before next source archive', stuck);
|
|
2132
|
+
}
|
|
2098
2133
|
const fresh = await this.io.readText(join(this.dirOf(source), 'SKILL.md'));
|
|
2099
2134
|
if (fresh !== sourceBodies.get(source)) {
|
|
2100
2135
|
const stuck = await rollbackSources();
|
|
2101
2136
|
stuck.push(...await rollbackTarget());
|
|
2102
2137
|
return rollbackMessage(`来源技能「${source}」在归档前被改过`, stuck);
|
|
2103
2138
|
}
|
|
2139
|
+
if (plan.assertFence !== undefined && !(await plan.assertFence())) {
|
|
2140
|
+
return { ok: false, message: 'tree fence lost before source archive; recovery required' };
|
|
2141
|
+
}
|
|
2142
|
+
await plan.onPhase?.('source-archiving', { source });
|
|
2104
2143
|
const archivedResult = await this.archive(source, {
|
|
2105
2144
|
absorbedInto: target,
|
|
2106
2145
|
unitId,
|
|
@@ -2114,12 +2153,30 @@ export class SkillLibrary {
|
|
|
2114
2153
|
}
|
|
2115
2154
|
archived.push(source);
|
|
2116
2155
|
}
|
|
2156
|
+
await plan.onPhase?.('sources-archived');
|
|
2157
|
+
await plan.onPhase?.('unit-written');
|
|
2117
2158
|
return { ok: true, message: `已把 ${sources.join(', ')} 合并进「${target}」。`, unitId, targetPath: dir, archived, mutations: 1 + sources.length };
|
|
2118
2159
|
}
|
|
2119
|
-
|
|
2160
|
+
await plan.onPhase?.('target-pending');
|
|
2161
|
+
if (plan.assertFence !== undefined && !(await plan.assertFence()))
|
|
2162
|
+
return { ok: false, message: 'tree fence lost before target write' };
|
|
2163
|
+
const created = await this.createCore(target, plan.body, plan.origin, { unitId, unitHead, managed: true });
|
|
2120
2164
|
if (!created.ok)
|
|
2121
2165
|
return { ok: false, message: created.message };
|
|
2166
|
+
await plan.onPhase?.('target-applied');
|
|
2167
|
+
await plan.onPhase?.('target-written');
|
|
2168
|
+
if (plan.signal?.aborted) {
|
|
2169
|
+
const targetRollback = await this.archive(target, { reason: 'merge cancelled' }, plan.origin);
|
|
2170
|
+
return rollbackMessage('cancelled before sources archived', targetRollback.ok ? [] : [target]);
|
|
2171
|
+
}
|
|
2122
2172
|
for (const source of sources) {
|
|
2173
|
+
if (plan.signal?.aborted) {
|
|
2174
|
+
const stuck = await rollbackSources();
|
|
2175
|
+
const targetRollback = await this.archive(target, { reason: 'merge cancelled' }, plan.origin);
|
|
2176
|
+
if (!targetRollback.ok)
|
|
2177
|
+
stuck.push(target);
|
|
2178
|
+
return rollbackMessage('cancelled before next source archive', stuck);
|
|
2179
|
+
}
|
|
2123
2180
|
const fresh = await this.io.readText(join(this.dirOf(source), 'SKILL.md'));
|
|
2124
2181
|
if (fresh !== sourceBodies.get(source)) {
|
|
2125
2182
|
const stuck = await rollbackSources();
|
|
@@ -2128,6 +2185,10 @@ export class SkillLibrary {
|
|
|
2128
2185
|
stuck.push(target);
|
|
2129
2186
|
return rollbackMessage(`来源技能「${source}」在归档前被改过`, stuck);
|
|
2130
2187
|
}
|
|
2188
|
+
if (plan.assertFence !== undefined && !(await plan.assertFence())) {
|
|
2189
|
+
return { ok: false, message: 'tree fence lost before source archive; recovery required' };
|
|
2190
|
+
}
|
|
2191
|
+
await plan.onPhase?.('source-archiving', { source });
|
|
2131
2192
|
const result = await this.archive(source, {
|
|
2132
2193
|
absorbedInto: target,
|
|
2133
2194
|
unitId,
|
|
@@ -2144,6 +2205,10 @@ export class SkillLibrary {
|
|
|
2144
2205
|
archived.push(source);
|
|
2145
2206
|
}
|
|
2146
2207
|
if (plan.supportFiles.length > 0) {
|
|
2208
|
+
await plan.onPhase?.('support-pending');
|
|
2209
|
+
if (plan.assertFence !== undefined && !(await plan.assertFence())) {
|
|
2210
|
+
return { ok: false, message: 'tree fence lost before support write; recovery required' };
|
|
2211
|
+
}
|
|
2147
2212
|
const writes = plan.supportFiles.map(file => ({
|
|
2148
2213
|
target: join(dir, ...file.path.replace(/\\/g, '/').split('/').filter(Boolean)),
|
|
2149
2214
|
content: file.content,
|
|
@@ -2168,6 +2233,8 @@ export class SkillLibrary {
|
|
|
2168
2233
|
return rollbackMessage(result.message, stuck);
|
|
2169
2234
|
}
|
|
2170
2235
|
}
|
|
2236
|
+
await plan.onPhase?.('sources-archived');
|
|
2237
|
+
await plan.onPhase?.('unit-written');
|
|
2171
2238
|
return { ok: true, message: `已把 ${sources.join(', ')} 合并进「${target}」。`, unitId, targetPath: dir, archived, mutations: 1 + sources.length };
|
|
2172
2239
|
});
|
|
2173
2240
|
}
|
package/lib/types/tree-lock.d.ts
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import type { EvolutionIoLike } from './io.ts';
|
|
2
2
|
export interface TreeLockRecord {
|
|
3
3
|
readonly token: string;
|
|
4
|
+
readonly epoch: number;
|
|
4
5
|
readonly pid: number;
|
|
5
6
|
readonly startedAt: number;
|
|
6
7
|
}
|
|
7
8
|
export interface TreeLock {
|
|
8
9
|
readonly token: string;
|
|
10
|
+
readonly epoch: number;
|
|
9
11
|
readonly path: string;
|
|
10
12
|
readonly acquiredAt: number;
|
|
11
13
|
release(): Promise<void>;
|
|
14
|
+
assertHeld(): Promise<boolean>;
|
|
12
15
|
}
|
|
13
16
|
export type TreeLockAcquireResult = {
|
|
14
17
|
readonly ok: true;
|
package/lib/types/tree-lock.js
CHANGED
|
@@ -25,7 +25,7 @@ function parseLock(raw) {
|
|
|
25
25
|
const value = JSON.parse(raw);
|
|
26
26
|
if (typeof value.token !== 'string' || typeof value.pid !== 'number' || typeof value.startedAt !== 'number')
|
|
27
27
|
return undefined;
|
|
28
|
-
return { token: value.token, pid: value.pid, startedAt: value.startedAt };
|
|
28
|
+
return { token: value.token, epoch: typeof value.epoch === 'number' ? value.epoch : 0, pid: value.pid, startedAt: value.startedAt };
|
|
29
29
|
}
|
|
30
30
|
catch {
|
|
31
31
|
return undefined;
|
|
@@ -40,6 +40,7 @@ export async function readTreeLock(io, home) {
|
|
|
40
40
|
async function tryAcquire(io, home, now) {
|
|
41
41
|
const path = treeLockFile(home);
|
|
42
42
|
let token;
|
|
43
|
+
let epoch;
|
|
43
44
|
let holder;
|
|
44
45
|
await transactIo(io, path, (current) => {
|
|
45
46
|
const existing = parseLock(current);
|
|
@@ -47,11 +48,12 @@ async function tryAcquire(io, home, now) {
|
|
|
47
48
|
holder = existing;
|
|
48
49
|
return current;
|
|
49
50
|
}
|
|
50
|
-
// Absent, corrupt, our own stale row, or a dead owner: take it.
|
|
51
|
+
// Absent, corrupt, our own stale row, or a dead owner: take it and bump the epoch.
|
|
51
52
|
token = randomUUID();
|
|
52
|
-
|
|
53
|
+
epoch = (existing?.epoch ?? 0) + 1;
|
|
54
|
+
return JSON.stringify({ token, epoch, pid: process.pid, startedAt: now() });
|
|
53
55
|
});
|
|
54
|
-
return token === undefined ? (holder === undefined ? {} : { holder }) : { token };
|
|
56
|
+
return token === undefined ? (holder === undefined ? {} : { holder }) : { token, epoch: epoch ?? 0 };
|
|
55
57
|
}
|
|
56
58
|
/**
|
|
57
59
|
* Acquire the tree write lock, waiting up to `waitMs`.
|
|
@@ -71,19 +73,25 @@ export async function acquireTreeWriteLock(options) {
|
|
|
71
73
|
const attempt = await tryAcquire(options.io, options.home, now);
|
|
72
74
|
if (attempt.token !== undefined) {
|
|
73
75
|
const token = attempt.token;
|
|
76
|
+
const epoch = attempt.epoch ?? 0;
|
|
74
77
|
const path = treeLockFile(options.home);
|
|
75
78
|
let released = false;
|
|
76
79
|
const lock = {
|
|
77
80
|
token,
|
|
81
|
+
epoch,
|
|
78
82
|
path,
|
|
79
83
|
acquiredAt: now(),
|
|
84
|
+
async assertHeld() {
|
|
85
|
+
const current = parseLock(await options.io.readText(path));
|
|
86
|
+
return current?.token === token && current.epoch === epoch;
|
|
87
|
+
},
|
|
80
88
|
async release() {
|
|
81
89
|
if (released)
|
|
82
90
|
return;
|
|
83
91
|
released = true;
|
|
84
92
|
await transactIo(options.io, path, (current) => {
|
|
85
93
|
const existing = parseLock(current);
|
|
86
|
-
return existing?.token === token ? null : current;
|
|
94
|
+
return existing?.token === token && existing.epoch === epoch ? null : current;
|
|
87
95
|
});
|
|
88
96
|
},
|
|
89
97
|
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** G2 adapter: core UnitBackend over the existing SkillLibrary. */
|
|
2
|
+
import type { SkillLibrary } from './skill-store.ts';
|
|
3
|
+
import type { UnitBackend } from './unit-transaction.ts';
|
|
4
|
+
export declare function skillLibraryUnitBackend(skills: SkillLibrary): UnitBackend;
|
|
5
|
+
//# sourceMappingURL=unit-backend.d.ts.map
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export function skillLibraryUnitBackend(skills) {
|
|
2
|
+
return {
|
|
3
|
+
async readSkill(name) { return await skills.read(name); },
|
|
4
|
+
async readSupportFile(name, path) {
|
|
5
|
+
const probe = skills.readSupportFile;
|
|
6
|
+
return typeof probe === 'function' ? await probe.call(skills, name, path) : null;
|
|
7
|
+
},
|
|
8
|
+
async commit(plan, onPhase, signal, assertFence) {
|
|
9
|
+
const result = await skills.commitMerge({
|
|
10
|
+
target: plan.target,
|
|
11
|
+
targetExists: plan.targetExists,
|
|
12
|
+
sources: plan.sources,
|
|
13
|
+
body: plan.body,
|
|
14
|
+
supportFiles: plan.supportFiles,
|
|
15
|
+
expectedTargetBody: plan.expectedTargetBody,
|
|
16
|
+
expectedSourceBodies: plan.expectedSourceBodies,
|
|
17
|
+
origin: plan.origin,
|
|
18
|
+
summary: plan.summary,
|
|
19
|
+
...(onPhase === undefined ? {} : { onPhase }),
|
|
20
|
+
...(signal === undefined ? {} : { signal }),
|
|
21
|
+
...(assertFence === undefined ? {} : { assertFence }),
|
|
22
|
+
});
|
|
23
|
+
return { ok: result.ok, message: result.message, ...(result.archived === undefined ? {} : { archived: result.archived }), ...(result.mutations === undefined ? {} : { mutations: result.mutations }) };
|
|
24
|
+
},
|
|
25
|
+
async restoreTarget(name, body) {
|
|
26
|
+
if (body === null) {
|
|
27
|
+
await skills.archive(name, { reason: 'unit-rollback' });
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const result = await skills.update(name, body);
|
|
31
|
+
if (!result.ok)
|
|
32
|
+
throw new Error(result.message);
|
|
33
|
+
},
|
|
34
|
+
async restoreSupportFile(name, path, content) {
|
|
35
|
+
const result = content === null ? await skills.removeSupportFile(name, path) : await skills.writeSupportFile(name, path, content);
|
|
36
|
+
if (!result.ok)
|
|
37
|
+
throw new Error(result.message);
|
|
38
|
+
},
|
|
39
|
+
async restoreSource(name, body) {
|
|
40
|
+
const restored = await skills.restoreFromArchive(name);
|
|
41
|
+
if (restored.ok)
|
|
42
|
+
return;
|
|
43
|
+
const result = await skills.update(name, body);
|
|
44
|
+
if (!result.ok)
|
|
45
|
+
throw new Error(result.message);
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=unit-backend.js.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { EvolutionIoLike } from './io.ts';
|
|
2
|
+
export declare const UNIT_JOURNAL_SCHEMA_VERSION = 1;
|
|
3
|
+
export declare const UNIT_JOURNAL_PHASES: readonly ["prepared", "target-pending", "target-written", "source-archiving", "sources-archived", "support-pending", "committed", "failed", "rolled-back"];
|
|
4
|
+
export type UnitJournalPhase = typeof UNIT_JOURNAL_PHASES[number];
|
|
5
|
+
export interface UnitJournalCheckpoint {
|
|
6
|
+
readonly source: string;
|
|
7
|
+
readonly phase: 'archived';
|
|
8
|
+
readonly hash: string;
|
|
9
|
+
readonly at: number;
|
|
10
|
+
}
|
|
11
|
+
export interface UnitJournal {
|
|
12
|
+
readonly schemaVersion: typeof UNIT_JOURNAL_SCHEMA_VERSION;
|
|
13
|
+
readonly unitId: string;
|
|
14
|
+
readonly runId: string;
|
|
15
|
+
readonly planDigest: string;
|
|
16
|
+
readonly target: string;
|
|
17
|
+
readonly sources: readonly string[];
|
|
18
|
+
readonly phase: UnitJournalPhase;
|
|
19
|
+
readonly storageKey: string;
|
|
20
|
+
readonly snapshotSha256: string;
|
|
21
|
+
readonly checkpoints?: readonly UnitJournalCheckpoint[];
|
|
22
|
+
readonly updatedAt: number;
|
|
23
|
+
}
|
|
24
|
+
export declare function unitJournalPath(home: string, unitId: string): string;
|
|
25
|
+
export declare function unitJournalPhaseRank(phase: UnitJournalPhase): number;
|
|
26
|
+
export declare function assertUnitJournalTransition(from: UnitJournalPhase, to: UnitJournalPhase): void;
|
|
27
|
+
export declare function writeUnitJournal(io: EvolutionIoLike, home: string, journal: UnitJournal): Promise<string>;
|
|
28
|
+
export declare function readUnitJournal(io: EvolutionIoLike, home: string, unitId: string): Promise<UnitJournal | null>;
|
|
29
|
+
//# sourceMappingURL=unit-journal.d.ts.map
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** G2 unit journal: intent/phase/checkpoint only, never evidence bodies. */
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
export const UNIT_JOURNAL_SCHEMA_VERSION = 1;
|
|
5
|
+
export const UNIT_JOURNAL_PHASES = [
|
|
6
|
+
'prepared',
|
|
7
|
+
'target-pending',
|
|
8
|
+
'target-written',
|
|
9
|
+
'source-archiving',
|
|
10
|
+
'sources-archived',
|
|
11
|
+
'support-pending',
|
|
12
|
+
'committed',
|
|
13
|
+
'failed',
|
|
14
|
+
'rolled-back',
|
|
15
|
+
];
|
|
16
|
+
export function unitJournalPath(home, unitId) {
|
|
17
|
+
return join(home, 'merge', 'unit-journals', createHash('sha256').update(unitId).digest('hex') + '.json');
|
|
18
|
+
}
|
|
19
|
+
export function unitJournalPhaseRank(phase) {
|
|
20
|
+
return UNIT_JOURNAL_PHASES.indexOf(phase);
|
|
21
|
+
}
|
|
22
|
+
export function assertUnitJournalTransition(from, to) {
|
|
23
|
+
if (to === 'failed' || to === 'rolled-back')
|
|
24
|
+
return;
|
|
25
|
+
if (from === 'failed' || from === 'rolled-back' || from === 'committed')
|
|
26
|
+
throw new Error('unit-journal-terminal:' + from);
|
|
27
|
+
if (unitJournalPhaseRank(to) <= unitJournalPhaseRank(from))
|
|
28
|
+
throw new Error('unit-journal-nonmonotonic:' + from + '->' + to);
|
|
29
|
+
}
|
|
30
|
+
export async function writeUnitJournal(io, home, journal) {
|
|
31
|
+
const path = unitJournalPath(home, journal.unitId);
|
|
32
|
+
const content = JSON.stringify(journal, null, 2) + '\n';
|
|
33
|
+
if (io.transact !== undefined)
|
|
34
|
+
await io.transact(path, () => content);
|
|
35
|
+
else
|
|
36
|
+
await io.writeText(path, content);
|
|
37
|
+
return path;
|
|
38
|
+
}
|
|
39
|
+
export async function readUnitJournal(io, home, unitId) {
|
|
40
|
+
const raw = await io.readText(unitJournalPath(home, unitId));
|
|
41
|
+
if (raw === null)
|
|
42
|
+
return null;
|
|
43
|
+
try {
|
|
44
|
+
return JSON.parse(raw);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=unit-journal.js.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** G2 recovery arbitration: unit journal/evidence wins over a legacy commit journal. */
|
|
2
|
+
import type { EvolutionIoLike } from './io.ts';
|
|
3
|
+
export type UnitAuthority = 'unit' | 'legacy' | 'none';
|
|
4
|
+
export declare function resolveUnitAuthority(io: EvolutionIoLike, home: string, unitId: string, legacyExists: boolean): Promise<UnitAuthority>;
|
|
5
|
+
//# sourceMappingURL=unit-recovery.d.ts.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { readUnitEvidence, readUnitSnapshot } from "./unit-storage.js";
|
|
2
|
+
import { readUnitJournal } from "./unit-journal.js";
|
|
3
|
+
export async function resolveUnitAuthority(io, home, unitId, legacyExists) {
|
|
4
|
+
if (await readUnitEvidence(io, home, unitId) !== null)
|
|
5
|
+
return 'unit';
|
|
6
|
+
if (await readUnitJournal(io, home, unitId) !== null)
|
|
7
|
+
return 'unit';
|
|
8
|
+
if (await readUnitSnapshot(io, home, unitId) !== null)
|
|
9
|
+
return 'unit';
|
|
10
|
+
return legacyExists ? 'legacy' : 'none';
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=unit-recovery.js.map
|