@lmzhen/dsh-evolution-core 0.30.18 → 0.30.20
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 +356 -45
- package/lib/types/index.d.ts +1 -0
- package/lib/types/index.js +1 -0
- package/lib/types/params.js +1 -1
- package/lib/types/platform-services.d.ts +1 -1
- package/lib/types/platform-services.js +2 -2
- package/lib/types/run-registry.d.ts +19 -6
- package/lib/types/run-registry.js +39 -16
- package/lib/types/skill-history.d.ts +4 -0
- package/lib/types/skill-revision.d.ts +37 -0
- package/lib/types/skill-revision.js +45 -0
- package/lib/types/skill-store.d.ts +33 -1
- package/lib/types/skill-store.js +165 -6
- package/lib/types/unit-backend.js +7 -1
- package/lib/types/unit-journal.d.ts +1 -1
- package/lib/types/unit-journal.js +1 -0
- package/lib/types/unit-storage.d.ts +12 -0
- package/lib/types/unit-transaction.d.ts +27 -2
- package/lib/types/unit-transaction.js +50 -8
- package/package.json +1 -1
- package/persisted-write-inventory.json +91 -1
package/lib/types/index.d.ts
CHANGED
|
@@ -84,6 +84,7 @@ export * from './drift-signals.ts';
|
|
|
84
84
|
export * from './skill-history.ts';
|
|
85
85
|
export * from './unit-members.ts';
|
|
86
86
|
export * from './skill-store.ts';
|
|
87
|
+
export * from './skill-revision.ts';
|
|
87
88
|
export * from './store-ledger.ts';
|
|
88
89
|
export * from './reachability.ts';
|
|
89
90
|
export * from './state-store.ts';
|
package/lib/types/index.js
CHANGED
|
@@ -85,6 +85,7 @@ export * from "./drift-signals.js";
|
|
|
85
85
|
export * from "./skill-history.js";
|
|
86
86
|
export * from "./unit-members.js";
|
|
87
87
|
export * from "./skill-store.js";
|
|
88
|
+
export * from "./skill-revision.js";
|
|
88
89
|
export * from "./store-ledger.js";
|
|
89
90
|
export * from "./reachability.js";
|
|
90
91
|
export * from "./state-store.js";
|
package/lib/types/params.js
CHANGED
|
@@ -186,7 +186,7 @@ export const PARAM_EXPOSURE = Object.freeze([
|
|
|
186
186
|
{ id: 'threatExemptLabels', group: 'deployment', tier: 'E1', authority: 'cordis', owner: 'evolution-threat', applies: 'none', docAnchor: 'PARAMETERS.md#deployment', summary: 'Threat labels the deployment declares benign (safety surface, never user-writable).', why: 'security' },
|
|
187
187
|
{ id: 'evolution-threat.enabled', group: 'deployment', tier: 'E2', authority: 'cordis', owner: 'evolution-threat', applies: 'none', docAnchor: 'PARAMETERS.md#deployment', summary: 'Mount the write-time threat guard at all.', why: 'security' },
|
|
188
188
|
{ id: 'evolution-threat.maxScanChars', group: 'deployment', tier: 'E2', authority: 'cordis', owner: 'evolution-threat', applies: 'none', docAnchor: 'PARAMETERS.md#deployment', summary: 'Scan window size of the threat guard.', why: 'security' },
|
|
189
|
-
{ id: 'evolution-approval.enabled', group: 'deployment', tier: '
|
|
189
|
+
{ id: 'evolution-approval.enabled', group: 'deployment', tier: 'E3', authority: 'cordis', owner: 'evolution-approval', applies: 'live', docAnchor: 'PARAMETERS.md#deployment', summary: 'Master approval switch: run the approval pipeline for foreground writes.', label: '启用批准流水线', hint: '关闭后前台写入不再经批准。', control: 'switch', unit: '', values: '', card: 'evolution-approval' },
|
|
190
190
|
{ id: 'evolution-approval.stageForeground', group: 'deployment', tier: 'E2', authority: 'cordis', owner: 'evolution-approval', applies: 'none', docAnchor: 'PARAMETERS.md#deployment', summary: 'Stage foreground agent writes for approval too.', why: 'security' },
|
|
191
191
|
{ id: 'evolution-approval.pendingTtlMs', group: 'deployment', tier: 'E3', authority: 'cordis', owner: 'evolution-approval', applies: 'live', docAnchor: 'PARAMETERS.md#deployment', summary: 'How long a staged write may wait before it expires: expired writes are closed as rejected and are NEVER executed; 0 (the default) means the window never expires.', label: '待批过期时间', hint: '一条待批写入最多等多久:超时就按「拒绝」关掉(不会执行,写效果不会落地);0 = 永不过期(默认)。', control: 'number', unit: '毫秒', values: '', card: 'evolution-approval' },
|
|
192
192
|
{ id: 'approvalConfirm', group: 'write-caps', tier: 'E3', authority: 'cordis', owner: 'evolution-approval', applies: 'live', docAnchor: 'PARAMETERS.md#write-caps', summary: 'What the confirmation before an APPROVE does: write straight through, ask first, or treat an unanswered prompt as a cancel. The same three words skillWriteConfirm uses for a skill write.', label: '批准前先确认', hint: '点「批准」时要不要再确认一次:直接执行 / 先确认 / 等待超时即取消。默认直接执行(不打断批量批准)。', control: 'select', unit: '', values: 'auto|ask|timeout', valueLabels: '直接执行|先确认|等待超时即取消', card: 'evolution-approval' },
|
|
@@ -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 26 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 26 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,7 +20,7 @@ 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
|
|
23
|
+
{ service: 'evolutionMerge', feature: 'merge command delegation and the merge doctor fact', side: 'host' },
|
|
24
24
|
{ service: 'hmr', feature: 'the preset install read-merge-write commit (the platform HMR lock serializes it)', side: 'host' },
|
|
25
25
|
{ service: 'evolutionMemoryBudget', feature: 'the memory-budget divergence section of this report', side: 'host' },
|
|
26
26
|
{ service: 'evolutionPolicy', feature: 'the policy snapshot that shadows row knobs', side: 'host' },
|
|
@@ -75,10 +75,13 @@ export interface RunRecord {
|
|
|
75
75
|
supersededAt?: number | undefined;
|
|
76
76
|
/** The run this run was spawned from (approve/resume after an earlier command). */
|
|
77
77
|
parentRunId?: string | undefined;
|
|
78
|
-
/**
|
|
78
|
+
/** @deprecated Legacy merge projection; read-only compatibility, never written. */
|
|
79
79
|
mergeStatus?: 'running' | 'succeeded' | 'failed' | 'cancelled' | 'awaiting-approval' | 'undone' | 'superseded' | 'interrupted' | 'orphan' | undefined;
|
|
80
|
+
/** @deprecated Legacy merge projection; read-only compatibility, never written. */
|
|
80
81
|
stagingRef?: string | undefined;
|
|
82
|
+
/** @deprecated Legacy merge projection; read-only compatibility, never written. */
|
|
81
83
|
pairKey?: string | undefined;
|
|
84
|
+
/** @deprecated Legacy merge projection; read-only compatibility, never written. */
|
|
82
85
|
pairs?: readonly unknown[] | undefined;
|
|
83
86
|
}
|
|
84
87
|
/** What a started run gets: its id and the signal its work must honour. */
|
|
@@ -90,10 +93,13 @@ export interface RunHandle {
|
|
|
90
93
|
readonly startedAt: number;
|
|
91
94
|
readonly command?: RunCommand | undefined;
|
|
92
95
|
readonly parentRunId?: string | undefined;
|
|
93
|
-
/**
|
|
96
|
+
/** @deprecated Legacy merge projection; read-only compatibility, never written. */
|
|
94
97
|
readonly mergeStatus?: 'running' | 'succeeded' | 'failed' | 'cancelled' | 'awaiting-approval' | 'undone' | 'superseded' | 'interrupted' | 'orphan' | undefined;
|
|
98
|
+
/** @deprecated Legacy merge projection; read-only compatibility, never written. */
|
|
95
99
|
readonly stagingRef?: string | undefined;
|
|
100
|
+
/** @deprecated Legacy merge projection; read-only compatibility, never written. */
|
|
96
101
|
readonly pairKey?: string | undefined;
|
|
102
|
+
/** @deprecated Legacy merge projection; read-only compatibility, never written. */
|
|
97
103
|
readonly pairs?: readonly unknown[] | undefined;
|
|
98
104
|
}
|
|
99
105
|
/** How a run ended, as reported by its owner. */
|
|
@@ -101,6 +107,7 @@ export interface RunOutcome {
|
|
|
101
107
|
state: 'succeeded' | 'failed' | 'cancelled';
|
|
102
108
|
resultRef?: string | undefined;
|
|
103
109
|
failure?: string | undefined;
|
|
110
|
+
/** @deprecated Legacy merge projection; read-only compatibility, never written. */
|
|
104
111
|
mergeStatus?: 'running' | 'succeeded' | 'failed' | 'cancelled' | 'awaiting-approval' | 'undone' | 'superseded' | 'interrupted' | 'orphan' | undefined;
|
|
105
112
|
}
|
|
106
113
|
/** The registry surface the commands row drives. */
|
|
@@ -131,13 +138,19 @@ export interface RunRegistry {
|
|
|
131
138
|
command?: RunCommand;
|
|
132
139
|
total?: number;
|
|
133
140
|
parentRunId?: string;
|
|
134
|
-
mergeStatus?: 'running' | 'succeeded' | 'failed' | 'cancelled' | 'awaiting-approval' | 'undone' | 'superseded' | 'interrupted' | 'orphan';
|
|
135
|
-
stagingRef?: string;
|
|
136
|
-
pairKey?: string;
|
|
137
|
-
pairs?: readonly unknown[];
|
|
138
141
|
}): Promise<RunHandle>;
|
|
139
142
|
/** Record a run's terminal state (first writer wins). */
|
|
140
143
|
settle(id: string, outcome: RunOutcome): Promise<void>;
|
|
144
|
+
/**
|
|
145
|
+
* Settle a run this process does not own: the controlled orphan upgrade.
|
|
146
|
+
*
|
|
147
|
+
* Startup recovery runs AFTER `load()` (which materializes the orphan verdict for a
|
|
148
|
+
* dead owner). A committed unit then proves the run really succeeded, so the verdict
|
|
149
|
+
* must be upgradeable — but ONLY from an explicit orphan stamp (or a still-running
|
|
150
|
+
* row whose owner is gone). A run that is genuinely in flight elsewhere is refused.
|
|
151
|
+
* @returns whether the index now carries the requested terminal state.
|
|
152
|
+
*/
|
|
153
|
+
settleRecovered(id: string, outcome: RunOutcome): Promise<boolean>;
|
|
141
154
|
/** Record the current stage/progress of a running run. */
|
|
142
155
|
stage(id: string, stage: RunStage, progress?: RunProgress): Promise<void>;
|
|
143
156
|
/** Mark runs as superseded by a tree restore; idempotent. */
|
|
@@ -110,10 +110,6 @@ function parseIndex(raw) {
|
|
|
110
110
|
...(typeof record.failure === 'string' ? { failure: record.failure } : {}),
|
|
111
111
|
...(typeof record.supersededAt === 'number' ? { supersededAt: record.supersededAt } : {}),
|
|
112
112
|
...(typeof record.parentRunId === 'string' ? { parentRunId: record.parentRunId } : {}),
|
|
113
|
-
...(record.mergeStatus === 'running' || record.mergeStatus === 'succeeded' || record.mergeStatus === 'failed' || record.mergeStatus === 'cancelled' || record.mergeStatus === 'awaiting-approval' || record.mergeStatus === 'undone' || record.mergeStatus === 'superseded' || record.mergeStatus === 'interrupted' || record.mergeStatus === 'orphan' ? { mergeStatus: record.mergeStatus } : {}),
|
|
114
|
-
...(typeof record.stagingRef === 'string' ? { stagingRef: record.stagingRef } : {}),
|
|
115
|
-
...(typeof record.pairKey === 'string' ? { pairKey: record.pairKey } : {}),
|
|
116
|
-
...(Array.isArray(record.pairs) ? { pairs: record.pairs } : {}),
|
|
117
113
|
});
|
|
118
114
|
}
|
|
119
115
|
return { ok: true, records, foreign, ...(notes.length > 0 ? { note: notes.join('; ') } : {}) };
|
|
@@ -139,7 +135,7 @@ export function newRunRegistry(options) {
|
|
|
139
135
|
/** A row whose owner cannot be reached: `pid` absent (an older build) or not alive. */
|
|
140
136
|
const ownerIsGone = (row) => row.pid === undefined || !isProcessAlive(row.pid);
|
|
141
137
|
/** The verdict is materialized ONCE, so its `endedAt` (and the duration a reader sees) is stable. */
|
|
142
|
-
const orphanOf = (row) => ({ ...row, state: 'failed',
|
|
138
|
+
const orphanOf = (row) => ({ ...row, state: 'failed', endedAt: now(), failure: ORPHAN_FAILURE });
|
|
143
139
|
/** Every row this process knows, newest first. ALL reads go through here. */
|
|
144
140
|
const view = () => [...owned.values(), ...foreign.values()].sort((a, b) => b.startedAt - a.startedAt);
|
|
145
141
|
/** Retained history: every running row plus the newest terminal rows per kind. */
|
|
@@ -308,7 +304,7 @@ export function newRunRegistry(options) {
|
|
|
308
304
|
const run = this.find(id);
|
|
309
305
|
if (run?.state !== 'running')
|
|
310
306
|
return false;
|
|
311
|
-
await this.settle(id, { state: 'failed', failure: 'orphan'
|
|
307
|
+
await this.settle(id, { state: 'failed', failure: 'orphan' });
|
|
312
308
|
return true;
|
|
313
309
|
},
|
|
314
310
|
async begin(kind, seed) {
|
|
@@ -325,10 +321,6 @@ export function newRunRegistry(options) {
|
|
|
325
321
|
pid: process.pid,
|
|
326
322
|
...(seed?.command !== undefined ? { command: seed.command } : {}),
|
|
327
323
|
...(seed?.parentRunId !== undefined ? { parentRunId: seed.parentRunId } : {}),
|
|
328
|
-
...(seed?.mergeStatus !== undefined ? { mergeStatus: seed.mergeStatus } : {}),
|
|
329
|
-
...(seed?.stagingRef !== undefined ? { stagingRef: seed.stagingRef } : {}),
|
|
330
|
-
...(seed?.pairKey !== undefined ? { pairKey: seed.pairKey } : {}),
|
|
331
|
-
...(seed?.pairs !== undefined ? { pairs: seed.pairs } : {}),
|
|
332
324
|
...(seed?.total !== undefined ? { progress: { done: 0, total: seed.total } } : {}),
|
|
333
325
|
});
|
|
334
326
|
// Write-through (0.19.1, review P2): answering "started" promises a run another
|
|
@@ -360,8 +352,6 @@ export function newRunRegistry(options) {
|
|
|
360
352
|
if (row === undefined || row.supersededAt !== undefined)
|
|
361
353
|
continue;
|
|
362
354
|
row.supersededAt = now();
|
|
363
|
-
if (row.kind === 'merge')
|
|
364
|
-
row.mergeStatus = 'superseded';
|
|
365
355
|
changed = true;
|
|
366
356
|
}
|
|
367
357
|
if (changed)
|
|
@@ -380,8 +370,6 @@ export function newRunRegistry(options) {
|
|
|
380
370
|
if (row.kind !== kind || row.supersededAt !== undefined)
|
|
381
371
|
continue;
|
|
382
372
|
row.supersededAt = now();
|
|
383
|
-
if (row.kind === 'merge')
|
|
384
|
-
row.mergeStatus = 'superseded';
|
|
385
373
|
marked += 1;
|
|
386
374
|
}
|
|
387
375
|
const kept = capped([...merged.values()]);
|
|
@@ -403,11 +391,46 @@ export function newRunRegistry(options) {
|
|
|
403
391
|
row.resultRef = outcome.resultRef;
|
|
404
392
|
if (outcome.failure !== undefined)
|
|
405
393
|
row.failure = outcome.failure;
|
|
406
|
-
if (outcome.mergeStatus !== undefined)
|
|
407
|
-
row.mergeStatus = outcome.mergeStatus;
|
|
408
394
|
controllers.delete(id);
|
|
409
395
|
await persist();
|
|
410
396
|
},
|
|
397
|
+
async settleRecovered(id, outcome) {
|
|
398
|
+
const local = owned.get(id);
|
|
399
|
+
if (local !== undefined) {
|
|
400
|
+
await this.settle(id, outcome);
|
|
401
|
+
return owned.get(id)?.state === outcome.state;
|
|
402
|
+
}
|
|
403
|
+
let changed = false;
|
|
404
|
+
await transactIo(options.io, path, (current) => {
|
|
405
|
+
const onDisk = parseIndex(current);
|
|
406
|
+
const row = onDisk.records.find(item => item.id === id);
|
|
407
|
+
if (row === undefined)
|
|
408
|
+
return current;
|
|
409
|
+
// The two admissible sources: an unresolved running row with a dead owner, or
|
|
410
|
+
// the orphan verdict load() already materialized. Anything else is a real
|
|
411
|
+
// terminal answer (someone's cancel/success) and stays put.
|
|
412
|
+
const recoverable = (row.state === 'running' && ownerIsGone(row))
|
|
413
|
+
|| (row.state === 'failed' && row.failure === ORPHAN_FAILURE);
|
|
414
|
+
if (!recoverable)
|
|
415
|
+
return current;
|
|
416
|
+
changed = true;
|
|
417
|
+
const next = {
|
|
418
|
+
...row,
|
|
419
|
+
state: outcome.state,
|
|
420
|
+
endedAt: now(),
|
|
421
|
+
...(outcome.resultRef === undefined ? {} : { resultRef: outcome.resultRef }),
|
|
422
|
+
...(outcome.failure === undefined ? {} : { failure: outcome.failure }),
|
|
423
|
+
};
|
|
424
|
+
const kept = capped(onDisk.records.map(item => item.id === id ? next : item));
|
|
425
|
+
const live = kept.filter(item => item.state === 'running');
|
|
426
|
+
const done = kept.filter(item => TERMINAL.has(item.state));
|
|
427
|
+
return JSON.stringify({ schemaVersion: RUNS_SCHEMA_VERSION, runs: [...live, ...done, ...onDisk.foreign] }, null, 2);
|
|
428
|
+
});
|
|
429
|
+
// Rebuild the foreign view so this process answers the upgraded verdict at once.
|
|
430
|
+
if (changed)
|
|
431
|
+
await this.load();
|
|
432
|
+
return changed;
|
|
433
|
+
},
|
|
411
434
|
inFlight(kind) {
|
|
412
435
|
return view().find(row => row.state === 'running' && (kind === undefined || row.kind === kind));
|
|
413
436
|
},
|
|
@@ -36,6 +36,10 @@ export declare const HISTORY_INDEX_VERSION = 1;
|
|
|
36
36
|
export declare const BASELINE_ACTION = "baseline";
|
|
37
37
|
/** G3b: one unit's head. Exactly one of the two forms is present (they never coexist). */
|
|
38
38
|
export interface UnitHead {
|
|
39
|
+
/** The merge run this unit belongs to, when the writer recorded one. */
|
|
40
|
+
runId?: string;
|
|
41
|
+
/** This unit starts a fresh target baseline; older versions remain listable but outside it. */
|
|
42
|
+
baseline?: true;
|
|
39
43
|
/** Representative shard: the unit's WHOLE manifest of moves (one entry per written path). */
|
|
40
44
|
moves?: readonly Readonly<Record<string, unknown>>[];
|
|
41
45
|
/** Representative shard: the skills this unit wrote, so members resolve without a directory scan. */
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** One support file as read through the store (path is normalized `references/x.md`). */
|
|
2
|
+
export interface RevisionFile {
|
|
3
|
+
readonly path: string;
|
|
4
|
+
readonly content: string;
|
|
5
|
+
}
|
|
6
|
+
/** The two-state content anchor: absent (no live skill) or a content hash. */
|
|
7
|
+
export type RevisionAnchor = {
|
|
8
|
+
readonly kind: 'absent';
|
|
9
|
+
} | {
|
|
10
|
+
readonly kind: 'present';
|
|
11
|
+
readonly sha256: string;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Canonical fingerprint of one skill's content.
|
|
15
|
+
*
|
|
16
|
+
* Field order and separators are a wire format: decisions persist anchors derived from
|
|
17
|
+
* this string, and a later commit compares against it. Do not reorder.
|
|
18
|
+
* @param name - the (trimmed) skill name.
|
|
19
|
+
* @param body - the SKILL.md text.
|
|
20
|
+
* @param files - every support file under SUPPORT_DIRS.
|
|
21
|
+
* @returns the fingerprint string to hash.
|
|
22
|
+
*/
|
|
23
|
+
export declare function revisionFingerprint(name: string, body: string, files: readonly RevisionFile[]): string;
|
|
24
|
+
/** Hash one fingerprint into its anchor (CRLF-normalized by contentHash). */
|
|
25
|
+
export declare function revisionAnchor(name: string, body: string, files: readonly RevisionFile[]): RevisionAnchor;
|
|
26
|
+
/** Anchor for a skill whose body may be absent. */
|
|
27
|
+
export declare function revisionAnchorOf(name: string, body: string | null, files: readonly RevisionFile[]): RevisionAnchor;
|
|
28
|
+
/**
|
|
29
|
+
* Support digest over several skills' fingerprints: the union of their full content.
|
|
30
|
+
*
|
|
31
|
+
* Sort is by fingerprint (name-prefixed), so name order and fingerprint order agree and
|
|
32
|
+
* a decision-time digest can be recomputed by a committer that only has hashes.
|
|
33
|
+
* @param fingerprints - one fingerprint per involved skill (missing skills simply absent).
|
|
34
|
+
* @returns the digest recorded on a decision and re-checked at commit.
|
|
35
|
+
*/
|
|
36
|
+
export declare function revisionSupportDigest(fingerprints: readonly string[]): string;
|
|
37
|
+
//# sourceMappingURL=skill-revision.d.ts.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content-revision identity shared by every planner that records a decision and every
|
|
3
|
+
* committer that re-checks it.
|
|
4
|
+
*
|
|
5
|
+
* A skill's revision covers the trimmed NAME, the SKILL.md body and every support file
|
|
6
|
+
* (`references/…`, `templates/…`, `scripts/…`, `assets/…`), each as read through the
|
|
7
|
+
* store. The merge domain records a decision-time anchor and the store compares the live
|
|
8
|
+
* anchor before writing; both sides must hash the same bytes or the compare is meaningless,
|
|
9
|
+
* so the hash lives HERE and nowhere else.
|
|
10
|
+
*/
|
|
11
|
+
import { contentHash } from "./mutations.js";
|
|
12
|
+
/**
|
|
13
|
+
* Canonical fingerprint of one skill's content.
|
|
14
|
+
*
|
|
15
|
+
* Field order and separators are a wire format: decisions persist anchors derived from
|
|
16
|
+
* this string, and a later commit compares against it. Do not reorder.
|
|
17
|
+
* @param name - the (trimmed) skill name.
|
|
18
|
+
* @param body - the SKILL.md text.
|
|
19
|
+
* @param files - every support file under SUPPORT_DIRS.
|
|
20
|
+
* @returns the fingerprint string to hash.
|
|
21
|
+
*/
|
|
22
|
+
export function revisionFingerprint(name, body, files) {
|
|
23
|
+
const ordered = [...files].sort((left, right) => left.path.localeCompare(right.path));
|
|
24
|
+
return name + '\u0000' + body + '\u0000' + ordered.map(file => file.path + '\u0000' + file.content).join('\u0000');
|
|
25
|
+
}
|
|
26
|
+
/** Hash one fingerprint into its anchor (CRLF-normalized by contentHash). */
|
|
27
|
+
export function revisionAnchor(name, body, files) {
|
|
28
|
+
return { kind: 'present', sha256: contentHash(revisionFingerprint(name, body, files)) };
|
|
29
|
+
}
|
|
30
|
+
/** Anchor for a skill whose body may be absent. */
|
|
31
|
+
export function revisionAnchorOf(name, body, files) {
|
|
32
|
+
return body === null ? { kind: 'absent' } : revisionAnchor(name, body, files);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Support digest over several skills' fingerprints: the union of their full content.
|
|
36
|
+
*
|
|
37
|
+
* Sort is by fingerprint (name-prefixed), so name order and fingerprint order agree and
|
|
38
|
+
* a decision-time digest can be recomputed by a committer that only has hashes.
|
|
39
|
+
* @param fingerprints - one fingerprint per involved skill (missing skills simply absent).
|
|
40
|
+
* @returns the digest recorded on a decision and re-checked at commit.
|
|
41
|
+
*/
|
|
42
|
+
export function revisionSupportDigest(fingerprints) {
|
|
43
|
+
return contentHash([...fingerprints].sort().join('\u0000'));
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=skill-revision.js.map
|
|
@@ -37,6 +37,7 @@ import { transactIo, type EvolutionIoLike } from './io.ts';
|
|
|
37
37
|
import { type Probe } from './probe.ts';
|
|
38
38
|
import type { SkillLimits } from './limits.ts';
|
|
39
39
|
import { type MutationRecord } from './mutations.ts';
|
|
40
|
+
import { type RevisionAnchor, type RevisionFile } from './skill-revision.ts';
|
|
40
41
|
import { type UnitHead, type SkillVersion } from './skill-history.ts';
|
|
41
42
|
import { type SkillHealthAssessment, type SkillHealthThresholds } from './skill-health.ts';
|
|
42
43
|
import type { EvolutionSkillMutatedEvent } from './events.ts';
|
|
@@ -181,6 +182,9 @@ export interface ArchiveOptions {
|
|
|
181
182
|
}
|
|
182
183
|
/** One multi-skill merge commit: target body + support files, with the source names it absorbs. */
|
|
183
184
|
export interface CommitMergePlan {
|
|
185
|
+
readonly runId?: string;
|
|
186
|
+
/** The unit transaction id; history must record this exact id for undo. */
|
|
187
|
+
readonly unitId?: string;
|
|
184
188
|
readonly target: string;
|
|
185
189
|
/** Whether the target already exists. The caller planned this; commit re-checks it. */
|
|
186
190
|
readonly targetExists: boolean;
|
|
@@ -196,6 +200,11 @@ export interface CommitMergePlan {
|
|
|
196
200
|
readonly name: string;
|
|
197
201
|
readonly body: string;
|
|
198
202
|
}[];
|
|
203
|
+
/** Stage limits snapshot; absent uses the deployed store limits. */
|
|
204
|
+
readonly limits?: import('./limits.ts').SkillLimits;
|
|
205
|
+
readonly targetRevision?: RevisionAnchor;
|
|
206
|
+
readonly sourceRevisions?: Readonly<Record<string, RevisionAnchor>>;
|
|
207
|
+
readonly supportDigest?: string;
|
|
199
208
|
readonly origin: WriteOrigin;
|
|
200
209
|
readonly summary: string;
|
|
201
210
|
/** Real write-phase callback used by UnitTransaction; not persisted. */
|
|
@@ -210,6 +219,8 @@ export interface CommitMergePlan {
|
|
|
210
219
|
export interface CommitMergeResult {
|
|
211
220
|
readonly ok: boolean;
|
|
212
221
|
readonly message: string;
|
|
222
|
+
/** Typed refusal kind for the revision CAS (target/source/support changed after the plan). */
|
|
223
|
+
readonly code?: 'target-changed' | 'source-changed' | 'support-changed';
|
|
213
224
|
readonly unitId?: string;
|
|
214
225
|
readonly targetPath?: string;
|
|
215
226
|
readonly archived?: readonly string[];
|
|
@@ -422,7 +433,20 @@ export declare class SkillLibrary {
|
|
|
422
433
|
}): Promise<SkillSummary[]>;
|
|
423
434
|
read(rawName: string): Promise<string | null>;
|
|
424
435
|
/**
|
|
425
|
-
|
|
436
|
+
* Content anchor of one live skill: name + body + every support file under SUPPORT_DIRS.
|
|
437
|
+
*
|
|
438
|
+
* This is the commit-side counterpart of the decision-time anchor recorded by the merge
|
|
439
|
+
* domain (see skill-revision.ts): both sides call the same fingerprint, so a changed
|
|
440
|
+
* source, target or support file cannot slip past the commit CAS.
|
|
441
|
+
* @param rawName - caller-supplied skill name (trimmed here).
|
|
442
|
+
* @returns present anchor, or absent when the skill has no SKILL.md.
|
|
443
|
+
*/
|
|
444
|
+
revisionFingerprint(rawName: string): Promise<string | null>;
|
|
445
|
+
/** Content anchor of one live skill; the hash a decision records and a commit re-checks. */
|
|
446
|
+
revisionAnchor(rawName: string): Promise<RevisionAnchor>;
|
|
447
|
+
/** Every support file of one live skill, in listing order; unreadable/missing files fail loud. */
|
|
448
|
+
liveSupportFiles(rawName: string): Promise<readonly RevisionFile[]>;
|
|
449
|
+
/**
|
|
426
450
|
* Single path-building choke point (rc.42 audit P2-5): every directory path
|
|
427
451
|
|
|
428
452
|
* is built from the TRIMMED name, so a name that passes `badName` (which
|
|
@@ -527,6 +551,8 @@ export declare class SkillLibrary {
|
|
|
527
551
|
* @returns the versions, oldest first.
|
|
528
552
|
*/
|
|
529
553
|
listVersions(rawName: string): Promise<SkillVersion[]>;
|
|
554
|
+
/** Versions from the latest merge baseline onward; old versions stay listable via listVersions. */
|
|
555
|
+
listBaselineVersions(rawName: string): Promise<SkillVersion[]>;
|
|
530
556
|
/**
|
|
531
557
|
* One version's content, read back from the content-addressed blob store.
|
|
532
558
|
* @param rawName - the skill's name.
|
|
@@ -535,6 +561,10 @@ export declare class SkillLibrary {
|
|
|
535
561
|
* blob cannot be read — never an empty string.
|
|
536
562
|
*/
|
|
537
563
|
readVersion(rawName: string, v: number): Promise<string | null>;
|
|
564
|
+
/** Read an archived skill's human-readable reason, exact or stamped. */
|
|
565
|
+
readArchiveReason(rawName: string): Promise<string | null>;
|
|
566
|
+
/** Read an archived skill body (SKILL.md) by its original name, exact or stamped. */
|
|
567
|
+
readArchived(rawName: string): Promise<string | null>;
|
|
538
568
|
/**
|
|
539
569
|
* Pin or unpin a skill (`.pinned` marker). Pinned skills are protected from
|
|
540
570
|
* deletion, from background-review writes, and from the lifecycle — a
|
|
@@ -577,6 +607,8 @@ export declare class SkillLibrary {
|
|
|
577
607
|
* P2-4 (v19): a non-string entry (`skills: [123]`) is refused structurally
|
|
578
608
|
* instead of throwing `name.includes is not a function`. */
|
|
579
609
|
private safeSnapshotEntryName;
|
|
610
|
+
/** Mark merge-absorbed sources archived in the usage sidecar (FR-DATA-07). */
|
|
611
|
+
private foldArchivedUsage;
|
|
580
612
|
archive(rawName: string, options?: ArchiveOptions, origin?: WriteOrigin): Promise<SkillActionResult>;
|
|
581
613
|
/**
|
|
582
614
|
* Commit one merge plan as a single multi-skill transaction.
|