@lmzhen/dsh-evolution-core 0.30.37 → 0.30.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/lib/index.js +3776 -2300
  2. package/lib/types/evolution-events.d.ts +6 -2
  3. package/lib/types/evolution-events.js +6 -0
  4. package/lib/types/extract-plan.js +17 -11
  5. package/lib/types/extract-prompt.d.ts +1 -5
  6. package/lib/types/extract-prompt.js +2 -0
  7. package/lib/types/extract-request.d.ts +32 -0
  8. package/lib/types/extract-request.js +155 -0
  9. package/lib/types/index.d.ts +4 -0
  10. package/lib/types/index.js +4 -0
  11. package/lib/types/mutation-storage-adapter.d.ts +14 -0
  12. package/lib/types/mutation-storage-adapter.js +107 -0
  13. package/lib/types/mutation-unit.d.ts +164 -0
  14. package/lib/types/mutation-unit.js +509 -0
  15. package/lib/types/prompts.d.ts +3 -3
  16. package/lib/types/prompts.js +8 -8
  17. package/lib/types/proposal-view.d.ts +4 -0
  18. package/lib/types/proposal-view.js +3 -1
  19. package/lib/types/skill-health.d.ts +5 -0
  20. package/lib/types/skill-health.js +3 -0
  21. package/lib/types/skill-history.d.ts +2 -0
  22. package/lib/types/skill-history.js +3 -0
  23. package/lib/types/skill-lint.d.ts +2 -0
  24. package/lib/types/skill-lint.js +13 -2
  25. package/lib/types/skill-model.d.ts +45 -0
  26. package/lib/types/skill-model.js +74 -0
  27. package/lib/types/skill-store.d.ts +22 -1
  28. package/lib/types/skill-store.js +415 -274
  29. package/lib/types/unit-members.js +3 -3
  30. package/lib/types/unit-storage.d.ts +10 -0
  31. package/lib/types/unit-transaction.d.ts +1 -1
  32. package/lib/types/unit-undo-plan.js +5 -3
  33. package/lib/types/unit-undo.d.ts +1 -1
  34. package/lib/types/unit-undo.js +18 -3
  35. package/package.json +1 -1
  36. package/persisted-write-inventory.json +3 -3
@@ -48,7 +48,7 @@ export interface EvolutionEvent {
48
48
  * observation anchors (C semantics: `usage` events carry the library-wide
49
49
  * count snapshot at the moment the observation window opened), and
50
50
  * maintain scans (011: verdict + recommendation count + runId). */
51
- type: 'feedback' | 'learn' | 'usage' | 'maintain';
51
+ type: 'feedback' | 'learn' | 'usage' | 'maintain' | 'hard-cap';
52
52
  /** Fold key for a `feedback` event: required and non-empty there (P2-10),
53
53
  * optional on the other tags and on a record this version only reads. */
54
54
  target?: string | undefined;
@@ -71,6 +71,10 @@ export interface EvolutionEvent {
71
71
  window?: {
72
72
  opened?: string;
73
73
  } | undefined;
74
+ /** hard-cap: the rejected body size and the write source. */
75
+ bodyChars?: number | undefined;
76
+ supportFiles?: number | null | undefined;
77
+ restructureRounds?: number | undefined;
74
78
  }
75
79
  /** The durable-write shape of one event: `feedback` REQUIRES a `target` —
76
80
  * the fold key the aggregate is keyed by (P2-10). The other tags keep every
@@ -79,7 +83,7 @@ type EvolutionEventInput = (Omit<EvolutionEvent, 'seq' | 'at' | 'target'> & {
79
83
  type: 'feedback';
80
84
  target: string;
81
85
  }) | (Omit<EvolutionEvent, 'seq' | 'at'> & {
82
- type: 'learn' | 'usage' | 'maintain';
86
+ type: 'learn' | 'usage' | 'maintain' | 'hard-cap';
83
87
  });
84
88
  export declare function eventsFile(home: string): string;
85
89
  /** I-5 (v18): one lightweight description of the durable event payload
@@ -77,6 +77,12 @@ export function evolutionEventPayloadIssue(event) {
77
77
  case 'learn':
78
78
  case 'usage':
79
79
  return null;
80
+ case 'hard-cap':
81
+ if (typeof event.target !== 'string' || event.target.trim() === '')
82
+ return 'hard-cap event requires a non-empty target';
83
+ if (typeof event.bodyChars !== 'number' || !Number.isFinite(event.bodyChars))
84
+ return 'hard-cap event requires a finite bodyChars';
85
+ return null;
80
86
  default:
81
87
  // Unreachable for the declared union; reachable when a JS host writes
82
88
  // the file. The cast restores the runtime value the union erased.
@@ -118,18 +118,24 @@ export function planExtract(input) {
118
118
  return { ok: false, code: selection.code, message: selection.message };
119
119
  if (selection.sections.length === 0)
120
120
  return { ok: false, code: 'part-empty', message: 'at least one part is required' };
121
- const parts = selection.sections.map(section => ({
122
- heading: section.heading,
123
- anchor: {
121
+ const externalRewrites = new Map((input.external ?? []).map(file => [file.ref, file.targetPath]));
122
+ const parts = selection.sections.map(section => {
123
+ let movedText = section.text;
124
+ for (const [from, to] of externalRewrites)
125
+ movedText = movedText.split(from).join(to);
126
+ return {
124
127
  heading: section.heading,
125
- ...(section.anchorStart === undefined ? {} : { start: section.anchorStart }),
126
- ...(section.anchorEnd === undefined ? {} : { end: section.anchorEnd }),
127
- },
128
- sourceText: section.text,
129
- movedText: section.text,
130
- bytes: Buffer.byteLength(section.text, 'utf8'),
131
- sha256: sha256(section.text),
132
- }));
128
+ anchor: {
129
+ heading: section.heading,
130
+ ...(section.anchorStart === undefined ? {} : { start: section.anchorStart }),
131
+ ...(section.anchorEnd === undefined ? {} : { end: section.anchorEnd }),
132
+ },
133
+ sourceText: section.text,
134
+ movedText,
135
+ bytes: Buffer.byteLength(movedText, 'utf8'),
136
+ sha256: sha256(movedText),
137
+ };
138
+ });
133
139
  const ownerName = input.owner.mode === 'covered' ? (input.owner.name ?? input.targetName) : (input.owner.neededOwner ?? input.targetName);
134
140
  const pointer = 'This capability now lives in ' + ownerName + '; project-specific facts stay here.';
135
141
  const ordered = [...selection.sections].sort((a, b) => a.start - b.start);
@@ -1,8 +1,4 @@
1
- /**
2
- * The frozen semantic prompt for skill extraction. One constant, consumed by
3
- * explicit extract agents and tests; it is deliberately not part of the review
4
- * prompt bundle, and no review path injects it.
5
- */
6
1
  export declare const EXTRACT_PROMPT_VERSION = "v1";
7
2
  export declare const EXTRACT_PROMPT_V1 = "Inspect the source skill, its linked support files, and the candidate owners\nin the library. A part of the skill (one or more sections) is a separate\ncapability when it has its own trigger and outcome, can be used without the\nparent context, and leaves the parent coherent. If it is separate and a\nclass-level owner covers it, extract that part. If no owner covers it, report\nthe owner gap and name the class-level owner that would be needed. If it mixes\na reusable capability with parent-specific facts, return mixed and name only\nthe reusable part. Otherwise keep it. Keep, extract, mixed, and owner-gap are\nall valid outcomes; no split is required.";
3
+ export declare const EXTRACT_PROMPT_HASH: string;
8
4
  //# sourceMappingURL=extract-prompt.d.ts.map
@@ -3,6 +3,7 @@
3
3
  * explicit extract agents and tests; it is deliberately not part of the review
4
4
  * prompt bundle, and no review path injects it.
5
5
  */
6
+ import { createHash } from 'node:crypto';
6
7
  export const EXTRACT_PROMPT_VERSION = 'v1';
7
8
  export const EXTRACT_PROMPT_V1 = `Inspect the source skill, its linked support files, and the candidate owners
8
9
  in the library. A part of the skill (one or more sections) is a separate
@@ -13,4 +14,5 @@ the owner gap and name the class-level owner that would be needed. If it mixes
13
14
  a reusable capability with parent-specific facts, return mixed and name only
14
15
  the reusable part. Otherwise keep it. Keep, extract, mixed, and owner-gap are
15
16
  all valid outcomes; no split is required.`;
17
+ export const EXTRACT_PROMPT_HASH = createHash('sha256').update(EXTRACT_PROMPT_V1, 'utf8').digest('hex');
16
18
  //# sourceMappingURL=extract-prompt.js.map
@@ -0,0 +1,32 @@
1
+ import { type OwnerCandidate } from './owner-resolver.ts';
2
+ import { type ExtractPlanDraft } from './extract-plan.ts';
3
+ import type { SkillLibrary } from './skill-store.ts';
4
+ import type { SkillModel } from './skill-model.ts';
5
+ export type ExtractPlanLibrary = Pick<SkillLibrary, 'root' | 'read' | 'listSupportFiles'> & {
6
+ readonly model: Pick<SkillModel, 'list'>;
7
+ };
8
+ export interface ExtractRequestItem {
9
+ heading?: string;
10
+ start?: string;
11
+ end?: string;
12
+ to_skill?: string;
13
+ to_heading?: string;
14
+ }
15
+ export interface ExtractRequestInput {
16
+ source: string;
17
+ items: readonly ExtractRequestItem[];
18
+ outcome?: 'extract' | 'mixed';
19
+ ownerCovers?: boolean;
20
+ }
21
+ export interface ExtractRequestPlan {
22
+ ok: true;
23
+ plan: ExtractPlanDraft;
24
+ candidates: OwnerCandidate[];
25
+ warnings?: string[];
26
+ }
27
+ /** Build the one extract plan both the tool and CLI consume. */
28
+ export declare function buildExtractPlanFromLibrary(library: ExtractPlanLibrary, input: ExtractRequestInput): Promise<ExtractRequestPlan | {
29
+ ok: false;
30
+ message: string;
31
+ }>;
32
+ //# sourceMappingURL=extract-request.d.ts.map
@@ -0,0 +1,155 @@
1
+ import { dirname, join } from 'node:path';
2
+ import { DEFAULT_SKILL_LIMITS } from "./limits.js";
3
+ import { contentHash } from "./mutations.js";
4
+ import { parseFrontmatter } from "./frontmatter.js";
5
+ import { isPresent } from "./probe.js";
6
+ import { findOwnerCandidate, ownerCandidatesFromSummaries } from "./owner-resolver.js";
7
+ import { nearDuplicateSummaries } from "./quality.js";
8
+ import { planExtract } from "./extract-plan.js";
9
+ import { readExternalSupport } from "./external-support.js";
10
+ /** Build the one extract plan both the tool and CLI consume. */
11
+ export async function buildExtractPlanFromLibrary(library, input) {
12
+ const source = input.source.trim();
13
+ const raw = input.items;
14
+ if (source === '' || raw.length === 0)
15
+ return { ok: false, message: 'extract 需要 name 与至少一个 extract 项。' };
16
+ const requests = [];
17
+ let target = '';
18
+ for (const item of raw) {
19
+ if (item === null || typeof item !== 'object')
20
+ return { ok: false, message: 'extract 的每一项都必须是对象。' };
21
+ const heading = typeof item.heading === 'string' ? item.heading.trim().replace(/^#+\s*/, '') : '';
22
+ const toSkill = typeof item.to_skill === 'string' ? item.to_skill.trim() : '';
23
+ if (heading === '' || toSkill === '')
24
+ return { ok: false, message: 'extract 的每一项都需要 heading 与 to_skill。' };
25
+ if (target === '')
26
+ target = toSkill;
27
+ if (target !== toSkill)
28
+ return { ok: false, message: '一次 extract 只能有一个 to_skill;多目标请分多次调用。' };
29
+ const toHeading = typeof item.to_heading === 'string' ? item.to_heading.trim().replace(/^#+\s*/, '') : undefined;
30
+ if (toHeading !== undefined && toHeading !== heading) {
31
+ return { ok: false, message: 'to_heading 暂不支持改名;G3 要求 heading 与 to_heading 相同。' };
32
+ }
33
+ requests.push({
34
+ heading,
35
+ ...(typeof item.start === 'string' ? { start: item.start } : {}),
36
+ ...(typeof item.end === 'string' ? { end: item.end } : {}),
37
+ });
38
+ }
39
+ const sourceMd = await library.read(source);
40
+ if (sourceMd === null)
41
+ return { ok: false, message: '技能「' + source + '」不存在。' };
42
+ if (source === target)
43
+ return { ok: false, message: 'source 与 target 不能相同。' };
44
+ const summaries = await library.model.list({ withContent: true, withFrontmatter: true });
45
+ const lookup = ownerCandidatesFromSummaries(source, summaries);
46
+ const candidateNames = lookup.candidates.map(candidate => candidate.name);
47
+ const candidateHint = candidateNames.length === 0
48
+ ? '当前没有可用 owner candidate(描述为空或被排除)。'
49
+ : '候选 owner:' + candidateNames.slice(0, 8).join('、') + (candidateNames.length > 8 ? ' 等' : '') + '。';
50
+ const targetMd = await library.read(target);
51
+ if (targetMd === null) {
52
+ const parsed = parseFrontmatter(sourceMd);
53
+ if (parsed === null)
54
+ return { ok: false, message: '技能「' + source + '」没有可读取的正文。' };
55
+ const sourceProbe = await library.listSupportFiles(source);
56
+ if (!isPresent(sourceProbe))
57
+ return { ok: false, message: '支撑文件列表不可读;拒绝在不确定的闭包上提取。' };
58
+ const external = await readExternalSupport(parsed.body, join(library.root, source), dirname(library.root), []);
59
+ if (!external.ok)
60
+ return { ok: false, message: external.message };
61
+ const plan = planExtract({
62
+ body: parsed.body,
63
+ sourceName: source,
64
+ targetName: target,
65
+ targetExists: false,
66
+ outcome: 'owner-gap',
67
+ owner: { mode: 'owner-gap', neededOwner: target, triggerCovers: true, outcomeCovers: false, reason: 'target missing; no auto create' },
68
+ requests,
69
+ sourceFiles: sourceProbe.value,
70
+ targetFiles: [],
71
+ external: external.files,
72
+ });
73
+ if (!plan.ok)
74
+ return { ok: false, message: plan.message + candidateHint };
75
+ return { ok: true, plan: plan.plan, candidates: lookup.candidates };
76
+ }
77
+ if (targetMd.length > DEFAULT_SKILL_LIMITS.maxSkillContentChars)
78
+ return { ok: false, message: '目标技能「' + target + '」已超过 ' + DEFAULT_SKILL_LIMITS.maxSkillContentChars + ' 字符,拒绝继续提取。' };
79
+ const candidate = findOwnerCandidate(lookup, target);
80
+ if (candidate === undefined)
81
+ return { ok: false, message: '目标「' + target + '」不是可用的 owner candidate(描述为空或已排除);owner-gap,不自动 create。' + candidateHint };
82
+ if (!candidate.managed)
83
+ return { ok: false, message: '目标「' + target + '」不是家族 managed 技能;拒绝把可复用部分写进不受管目录。' };
84
+ if (input.ownerCovers === false) {
85
+ const parsed = parseFrontmatter(sourceMd);
86
+ if (parsed === null)
87
+ return { ok: false, message: '技能「' + source + '」没有可读取的正文。' };
88
+ const sourceProbe = await library.listSupportFiles(source);
89
+ if (!isPresent(sourceProbe))
90
+ return { ok: false, message: '支撑文件列表不可读;拒绝在不确定的闭包上提取。' };
91
+ const external = await readExternalSupport(parsed.body, join(library.root, source), dirname(library.root), []);
92
+ if (!external.ok)
93
+ return { ok: false, message: external.message };
94
+ const plan = planExtract({
95
+ body: parsed.body,
96
+ sourceName: source,
97
+ targetName: target,
98
+ targetExists: true,
99
+ outcome: 'owner-gap',
100
+ owner: { mode: 'owner-gap', neededOwner: target, triggerCovers: false, outcomeCovers: false, reason: 'agent judged the target does not cover this part' },
101
+ requests,
102
+ sourceFiles: sourceProbe.value,
103
+ targetFiles: [],
104
+ external: external.files,
105
+ });
106
+ if (!plan.ok)
107
+ return { ok: false, message: plan.message + candidateHint };
108
+ return { ok: true, plan: plan.plan, candidates: lookup.candidates };
109
+ }
110
+ const overlaps = nearDuplicateSummaries({
111
+ candidate: { name: target, description: candidate.description },
112
+ existing: summaries.filter(summary => summary.name !== source && summary.name !== target),
113
+ });
114
+ const overlapWarnings = overlaps.length === 0
115
+ ? []
116
+ : ['目标「' + target + '」描述与邻技能重叠:' + overlaps.map(match => match.name + ' (' + match.score.toFixed(2) + ')').join('、') + ';请确认 owner 覆盖,提取仍可继续。'];
117
+ if (candidate.protectionUnknown || candidate.protectedBy !== null) {
118
+ return { ok: false, message: '目标「' + target + '」受保护(' + (candidate.protectedBy ?? '未知') + '),拒绝提取。' };
119
+ }
120
+ const parsed = parseFrontmatter(sourceMd);
121
+ if (parsed === null)
122
+ return { ok: false, message: '技能「' + source + '」没有可读取的正文。' };
123
+ const sourceProbe = await library.listSupportFiles(source);
124
+ const targetProbe = await library.listSupportFiles(target);
125
+ if (!isPresent(sourceProbe) || !isPresent(targetProbe))
126
+ return { ok: false, message: '支撑文件列表不可读;拒绝在不确定的闭包上提取。' };
127
+ const external = await readExternalSupport(parsed.body, join(library.root, source), dirname(library.root), targetProbe.value);
128
+ if (!external.ok)
129
+ return { ok: false, message: external.message };
130
+ if (input.outcome !== undefined && input.outcome !== 'extract' && input.outcome !== 'mixed') {
131
+ return { ok: false, message: 'outcome 只能是 extract 或 mixed;owner-gap 由缺失 target 的路径自动决定。' };
132
+ }
133
+ const outcome = input.outcome ?? (requests.some(request => request.start !== undefined || request.end !== undefined) ? 'mixed' : 'extract');
134
+ const plan = planExtract({
135
+ body: parsed.body,
136
+ sourceName: source,
137
+ targetName: target,
138
+ targetExists: true,
139
+ outcome,
140
+ owner: { mode: 'covered', name: target, triggerCovers: true, outcomeCovers: true, reason: 'explicit target' },
141
+ requests,
142
+ sourceFiles: sourceProbe.value,
143
+ targetFiles: targetProbe.value,
144
+ external: external.files,
145
+ anchors: { sourceMdSha256: contentHash(sourceMd), targetMdSha256: contentHash(targetMd) },
146
+ });
147
+ if (!plan.ok)
148
+ return { ok: false, message: plan.message };
149
+ const projectedChars = targetMd.length + plan.plan.parts.reduce((sum, part) => sum + part.movedText.length + 2, 0);
150
+ if (projectedChars > DEFAULT_SKILL_LIMITS.maxSkillContentChars) {
151
+ return { ok: false, message: '目标「' + target + '」投影后约 ' + projectedChars + ' 字符,超过 ' + DEFAULT_SKILL_LIMITS.maxSkillContentChars + ' 上限;拒绝提取。' };
152
+ }
153
+ return { ok: true, plan: plan.plan, candidates: lookup.candidates, ...(overlapWarnings.length === 0 ? {} : { warnings: overlapWarnings }) };
154
+ }
155
+ //# sourceMappingURL=extract-request.js.map
@@ -44,6 +44,7 @@ export * from './reference-rewrite.ts';
44
44
  export * from './support-refs.ts';
45
45
  export * from './section-selection.ts';
46
46
  export * from './extract-plan.ts';
47
+ export * from './extract-request.ts';
47
48
  export * from './owner-resolver.ts';
48
49
  export * from './proposal-view.ts';
49
50
  export * from './proposal-report.ts';
@@ -92,6 +93,9 @@ export * from './drift-signals.ts';
92
93
  export * from './skill-history.ts';
93
94
  export * from './unit-members.ts';
94
95
  export * from './skill-store.ts';
96
+ export * from './skill-model.ts';
97
+ export * from './mutation-unit.ts';
98
+ export * from './mutation-storage-adapter.ts';
95
99
  export * from './skill-revision.ts';
96
100
  export * from './store-ledger.ts';
97
101
  export * from './reachability.ts';
@@ -45,6 +45,7 @@ export * from "./reference-rewrite.js";
45
45
  export * from "./support-refs.js";
46
46
  export * from "./section-selection.js";
47
47
  export * from "./extract-plan.js";
48
+ export * from "./extract-request.js";
48
49
  export * from "./owner-resolver.js";
49
50
  export * from "./proposal-view.js";
50
51
  export * from "./proposal-report.js";
@@ -93,6 +94,9 @@ export * from "./drift-signals.js";
93
94
  export * from "./skill-history.js";
94
95
  export * from "./unit-members.js";
95
96
  export * from "./skill-store.js";
97
+ export * from "./skill-model.js";
98
+ export * from "./mutation-unit.js";
99
+ export * from "./mutation-storage-adapter.js";
96
100
  export * from "./skill-revision.js";
97
101
  export * from "./store-ledger.js";
98
102
  export * from "./reachability.js";
@@ -0,0 +1,14 @@
1
+ import type { EvolutionIoLike } from './io.ts';
2
+ import type { MutationJournalPort, MutationSnapshotPort } from './mutation-unit.ts';
3
+ /**
4
+ * Production adapter onto the existing unit snapshot store. A tree-change
5
+ * snapshot is encoded as a UnitSnapshot with kind='tree-change'; merge
6
+ * snapshots do not carry that kind and are ignored by the reader.
7
+ */
8
+ export declare function unitSnapshotPort(io: EvolutionIoLike, home: string): MutationSnapshotPort;
9
+ /**
10
+ * Production adapter onto the existing unit journal. It never writes a second
11
+ * journal file; merge and tree-change units share the one journal directory.
12
+ */
13
+ export declare function unitJournalPort(io: EvolutionIoLike, home: string): MutationJournalPort;
14
+ //# sourceMappingURL=mutation-storage-adapter.d.ts.map
@@ -0,0 +1,107 @@
1
+ import { join } from 'node:path';
2
+ import { contentHash } from "./mutations.js";
3
+ import { readUnitSnapshot, unitSnapshotsDir, unitStorageKey, writeUnitSnapshot } from "./unit-storage.js";
4
+ import { writeUnitJournal } from "./unit-journal.js";
5
+ /**
6
+ * Production adapter onto the existing unit snapshot store. A tree-change
7
+ * snapshot is encoded as a UnitSnapshot with kind='tree-change'; merge
8
+ * snapshots do not carry that kind and are ignored by the reader.
9
+ */
10
+ export function unitSnapshotPort(io, home) {
11
+ return {
12
+ async write(snapshot) {
13
+ const first = snapshot.ops[0];
14
+ const body = {
15
+ schemaVersion: 1,
16
+ unitId: snapshot.unitId,
17
+ planDigest: contentHash(JSON.stringify(snapshot.ops)),
18
+ runId: snapshot.unitId,
19
+ target: first?.target ?? '',
20
+ targetExists: true,
21
+ targetBefore: first?.before ?? null,
22
+ ...(first?.after === null || first?.after === undefined ? {} : { targetAfter: first.after }),
23
+ supportBefore: [],
24
+ supportAfter: [],
25
+ sourceBefore: [],
26
+ origin: 'foreground',
27
+ summary: 'tree-change ' + snapshot.unitId,
28
+ createdAt: snapshot.createdAt,
29
+ kind: 'tree-change',
30
+ mutationState: snapshot.state,
31
+ ...(snapshot.cleanup === undefined ? {} : { cleanup: snapshot.cleanup }),
32
+ ops: snapshot.ops.map(op => ({
33
+ target: op.target,
34
+ before: op.before,
35
+ after: op.after,
36
+ ...(op.skill === undefined ? {} : { skill: op.skill }),
37
+ })),
38
+ };
39
+ await writeUnitSnapshot(io, home, body);
40
+ },
41
+ async read(unitId) {
42
+ const snapshot = await readUnitSnapshot(io, home, unitId);
43
+ if (snapshot === null || snapshot.kind !== 'tree-change' || snapshot.ops === undefined)
44
+ return null;
45
+ return {
46
+ unitId,
47
+ state: snapshot.mutationState ?? 'committed',
48
+ ops: snapshot.ops,
49
+ ...(snapshot.cleanup === undefined ? {} : { cleanup: snapshot.cleanup }),
50
+ createdAt: snapshot.createdAt,
51
+ };
52
+ },
53
+ async listPrepared() {
54
+ const names = await io.list(unitSnapshotsDir(home));
55
+ const out = [];
56
+ for (const name of names) {
57
+ if (!name.endsWith('.json'))
58
+ continue;
59
+ const raw = await io.readText(join(unitSnapshotsDir(home), name));
60
+ if (raw === null)
61
+ continue;
62
+ let snapshot;
63
+ try {
64
+ snapshot = JSON.parse(raw);
65
+ }
66
+ catch {
67
+ continue;
68
+ }
69
+ if (snapshot.kind !== 'tree-change' || snapshot.mutationState !== 'prepared' || snapshot.ops === undefined)
70
+ continue;
71
+ out.push({ unitId: snapshot.unitId, state: 'prepared', ops: snapshot.ops, ...(snapshot.cleanup === undefined ? {} : { cleanup: snapshot.cleanup }), createdAt: snapshot.createdAt });
72
+ }
73
+ return out;
74
+ },
75
+ };
76
+ }
77
+ const PHASE = {
78
+ prepared: 'prepared',
79
+ 'unit-written': 'unit-written',
80
+ committed: 'committed',
81
+ failed: 'failed',
82
+ undone: 'rolled-back',
83
+ };
84
+ /**
85
+ * Production adapter onto the existing unit journal. It never writes a second
86
+ * journal file; merge and tree-change units share the one journal directory.
87
+ */
88
+ export function unitJournalPort(io, home) {
89
+ return {
90
+ async write(record) {
91
+ const journal = {
92
+ schemaVersion: 1,
93
+ unitId: record.unitId,
94
+ runId: record.unitId,
95
+ planDigest: contentHash(JSON.stringify(record)),
96
+ target: record.target ?? record.changed[0] ?? '',
97
+ sources: record.roots ?? [],
98
+ phase: PHASE[record.phase],
99
+ storageKey: unitStorageKey(record.unitId),
100
+ snapshotSha256: contentHash(JSON.stringify(record)),
101
+ updatedAt: Date.now(),
102
+ };
103
+ await writeUnitJournal(io, home, journal);
104
+ },
105
+ };
106
+ }
107
+ //# sourceMappingURL=mutation-storage-adapter.js.map
@@ -0,0 +1,164 @@
1
+ import { type transactIo } from './io.ts';
2
+ export interface MutationRoot {
3
+ name: string;
4
+ protection: 'write' | 'delete' | 'none';
5
+ }
6
+ export type TreeChangeOp = {
7
+ kind: 'write';
8
+ target: string;
9
+ content: string;
10
+ expected?: string | null;
11
+ skill?: string;
12
+ } | {
13
+ kind: 'delete';
14
+ target: string;
15
+ expected?: string | null;
16
+ skill?: string;
17
+ };
18
+ export interface MutationRequest {
19
+ name: string;
20
+ roots: MutationRoot[];
21
+ ops: TreeChangeOp[];
22
+ origin: string;
23
+ auditAction: string;
24
+ auditSummary: string;
25
+ eventAction: string;
26
+ unitId?: string;
27
+ dryRun?: boolean;
28
+ /** Optional root directory map; when present, every target must resolve under a declared root. */
29
+ rootDirs?: Readonly<Record<string, string>>;
30
+ /** Sidecars that undo/recover must remove after restoring the ops (e.g. a create marker). */
31
+ cleanupPaths?: readonly string[];
32
+ }
33
+ export interface MutationLimits {
34
+ maxSkillFileBytes: number;
35
+ maxSkillContentChars: number;
36
+ }
37
+ export interface MutationValidationError {
38
+ code: string;
39
+ message: string;
40
+ target?: string;
41
+ }
42
+ export interface MutationValidation {
43
+ ok: boolean;
44
+ roots: MutationRoot[];
45
+ ops: TreeChangeOp[];
46
+ errors: MutationValidationError[];
47
+ }
48
+ /**
49
+ * Pure preflight for one mutation request. It deliberately does no IO: the
50
+ * caller must resolve roots and read bodies before this point; the unit kernel
51
+ * performs CAS/protection/journal after validation.
52
+ */
53
+ export declare function validateMutationRequest(request: MutationRequest, limits: MutationLimits): MutationValidation;
54
+ export interface MutationJournalRecord {
55
+ unitId: string;
56
+ phase: 'prepared' | 'unit-written' | 'committed' | 'failed' | 'undone';
57
+ changed: readonly string[];
58
+ target?: string;
59
+ roots?: readonly string[];
60
+ error?: string;
61
+ }
62
+ export interface MutationJournalPort {
63
+ write(record: MutationJournalRecord): Promise<void>;
64
+ }
65
+ export interface MutationLockLease {
66
+ release(): Promise<void>;
67
+ }
68
+ export interface MutationLockPort {
69
+ acquire(): Promise<MutationLockLease>;
70
+ }
71
+ export interface MutationSnapshotOp {
72
+ target: string;
73
+ before: string | null;
74
+ after: string | null;
75
+ skill?: string;
76
+ }
77
+ export interface MutationSnapshot {
78
+ unitId: string;
79
+ state: 'prepared' | 'committed' | 'undone';
80
+ ops: readonly MutationSnapshotOp[];
81
+ cleanup?: readonly string[];
82
+ createdAt: number;
83
+ }
84
+ export interface MutationSnapshotPort {
85
+ write(snapshot: MutationSnapshot): Promise<void>;
86
+ read(unitId: string): Promise<MutationSnapshot | null>;
87
+ listPrepared?(): Promise<readonly MutationSnapshot[]>;
88
+ }
89
+ export interface MutationUnitEvent {
90
+ unitId: string;
91
+ changed: readonly string[];
92
+ ops: readonly MutationSnapshotOp[];
93
+ roots?: readonly string[];
94
+ }
95
+ export interface MutationUnitHooks {
96
+ committed?(event: MutationUnitEvent): Promise<void> | void;
97
+ rolledBack?(event: MutationUnitEvent): Promise<void> | void;
98
+ undone?(event: MutationUnitEvent): Promise<void> | void;
99
+ }
100
+ export interface MutationUnitOptions {
101
+ io: import('./io.ts').EvolutionIoLike;
102
+ limits: MutationLimits;
103
+ serial?: <T>(task: () => Promise<T>) => Promise<T>;
104
+ lock?: MutationLockPort;
105
+ journal?: MutationJournalPort;
106
+ snapshots?: MutationSnapshotPort;
107
+ hooks?: MutationUnitHooks;
108
+ validateWrite?: (op: TreeChangeOp) => string | null;
109
+ transact?: typeof transactIo;
110
+ /** Whether the io.transact callback re-checks the baseline. Default true; create sets false under its outer root lock. */
111
+ transactGuard?: boolean;
112
+ now?: () => number;
113
+ }
114
+ export interface MutationResult {
115
+ ok: boolean;
116
+ unitId: string;
117
+ dryRun: boolean;
118
+ changed: string[];
119
+ errors: MutationValidationError[];
120
+ warnings?: readonly string[];
121
+ }
122
+ export interface MutationPreviewResult {
123
+ ok: boolean;
124
+ unitId: string;
125
+ roots: MutationRoot[];
126
+ ops: TreeChangeOp[];
127
+ errors: MutationValidationError[];
128
+ }
129
+ export declare class MutationUnit {
130
+ private readonly io;
131
+ private readonly limits;
132
+ private readonly serial;
133
+ private readonly lock;
134
+ private readonly journal;
135
+ private readonly snapshots;
136
+ private readonly hooks;
137
+ private readonly validateWrite;
138
+ private readonly transact;
139
+ private readonly transactGuard;
140
+ private readonly now;
141
+ constructor(options: MutationUnitOptions);
142
+ preview(request: MutationRequest): MutationPreviewResult;
143
+ apply(request: MutationRequest): Promise<MutationResult>;
144
+ /** Recover every prepared snapshot known to the port; safe to call at boot. */
145
+ recoverPrepared(): Promise<MutationResult[]>;
146
+ /** Converge a prepared unit left by a crash. Every path must be at its after-bytes; a drift refuses the whole recovery. */
147
+ recover(unitId: string): Promise<MutationResult>;
148
+ undo(unitId: string): Promise<MutationResult>;
149
+ private applyOp;
150
+ private rollback;
151
+ }
152
+ export interface FileMutationLockOptions {
153
+ waitMs?: number;
154
+ pollMs?: number;
155
+ now?: () => number;
156
+ sleep?: (ms: number) => Promise<void>;
157
+ }
158
+ /**
159
+ * Cross-process root mutation lock. A live holder is never stolen; a dead pid
160
+ * or corrupt record can be taken over. This is separate from merge's run-level
161
+ * tree lock so an ordinary skill write cannot deadlock a merge run.
162
+ */
163
+ export declare function fileMutationLock(io: import('./io.ts').EvolutionIoLike, root: string, options?: FileMutationLockOptions): MutationLockPort;
164
+ //# sourceMappingURL=mutation-unit.d.ts.map