@lmzhen/dsh-evolution-core 0.22.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -0
- package/lib/index.js +271 -521
- package/lib/types/errors.d.ts +2 -1
- package/lib/types/guards.d.ts +23 -0
- package/lib/types/index.d.ts +1 -0
- package/lib/types/io.d.ts +44 -103
- package/lib/types/params.d.ts +10 -0
- package/lib/types/platform-services.d.ts +2 -2
- package/lib/types/preset-composition.d.ts +84 -1
- package/lib/types/skill-row-cells.d.ts +2 -0
- package/lib/types/skill-store.d.ts +9 -49
- package/package.json +1 -1
- package/persisted-write-inventory.json +3 -3
package/lib/types/errors.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* THE error-code table: E-301…E-
|
|
2
|
+
* THE error-code table: E-301…E-322 with the exact text each one emits (E-317…E-319 are the
|
|
3
3
|
* write-admission gates' own refusals).
|
|
4
4
|
*
|
|
5
5
|
* The codes used to be spelled out at 33 call sites across two packages, so one
|
|
@@ -36,6 +36,7 @@ export declare const EVOLUTION_ERRORS: Readonly<{
|
|
|
36
36
|
readonly 'e-317-skill-write-not-confirmed': "E-317: skill \"%a1%\" was not %a2% — the confirmation prompt was declined or dismissed, so nothing was written. The prompt appears on every create and bare delete; repeat the call only if the operator asks for it.";
|
|
37
37
|
readonly 'e-318-skill-write-without-a-read': "E-318: skill \"%a1%\" was not read in this session, so this write is refused. Read it with the `skill` tool first (a read that failed does not count), then repeat the write.";
|
|
38
38
|
readonly 'e-320-the-profile-home-is-not': "E-320: the profile home is not reported by this host, so the legacy settings document cannot be located. /evolution migrate reads `<profile home>/settings.yaml.imported`; run it from a profile-launched session.";
|
|
39
|
+
readonly 'e-322-a-parameter-would-break-a': "E-322: %a1% must be at least %a2%, but this write sets it to %a3%. Nothing was written.";
|
|
39
40
|
readonly 'e-321-no-io-provider-is': "E-321: no evolution-io provider is mounted, so the legacy settings document cannot be read. Mount the evolution-io row (evolution-host/evolution-all) and retry.";
|
|
40
41
|
readonly 'e-319-skill-write-confirm-timed-out': "E-319: skill \"%a1%\" was not %a2% — the confirmation prompt went unanswered for %a3% second(s), so the write was cancelled. Nobody was there to decide: set `skillWriteConfirm` to `auto` to write without a prompt, keep `ask` to wait until someone answers, or raise `skillWriteConfirmTimeoutSeconds`.";
|
|
41
42
|
}>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The family's plain-object predicate, in ONE place (v46 G7.1 / audit A8).
|
|
3
|
+
*
|
|
4
|
+
* The audit found the same guard — "a non-null object that is not an array" — rewritten fifteen
|
|
5
|
+
* times across nine packages, four times inside core itself, and with two byte-identical `asRecord`
|
|
6
|
+
* copies in evolution-policy and evolution-threat. This module is the single definition; callers
|
|
7
|
+
* import it instead of spelling the clause again.
|
|
8
|
+
*
|
|
9
|
+
* @module @lmzhen/dsh-evolution-core
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Is this a non-null, non-array object (what JSON calls an object)?
|
|
13
|
+
* @param value - the value to test.
|
|
14
|
+
* @returns whether it is a plain object.
|
|
15
|
+
*/
|
|
16
|
+
export declare function isRecord(value: unknown): value is Record<string, unknown>;
|
|
17
|
+
/**
|
|
18
|
+
* Read a value as a record, or undefined when it is not one.
|
|
19
|
+
* @param value - the value to read.
|
|
20
|
+
* @returns the value typed as a record, or undefined.
|
|
21
|
+
*/
|
|
22
|
+
export declare function asRecord(value: unknown): Record<string, unknown> | undefined;
|
|
23
|
+
//# sourceMappingURL=guards.d.ts.map
|
package/lib/types/index.d.ts
CHANGED
|
@@ -38,6 +38,7 @@ export * from './curator.ts';
|
|
|
38
38
|
export * from './evolution-events.ts';
|
|
39
39
|
export * from './fuzzy-match.ts';
|
|
40
40
|
export * from './gates.ts';
|
|
41
|
+
export * from './guards.ts';
|
|
41
42
|
export * from './http-routes.ts';
|
|
42
43
|
export * from './events.ts';
|
|
43
44
|
export * from './env.ts';
|
package/lib/types/io.d.ts
CHANGED
|
@@ -98,18 +98,6 @@ export interface TransactTaskGuard {
|
|
|
98
98
|
export declare function transactTaskGuard(what: string): TransactTaskGuard;
|
|
99
99
|
/** Lazy adapter over an IO provider registry, shared by every evolution consumer. */
|
|
100
100
|
export declare function evolutionIoAdapter(provider: () => EvolutionIoLike): EvolutionIoLike;
|
|
101
|
-
/**
|
|
102
|
-
* F-367 (②): lock paths whose release (the finally `rm`) failed. The next write
|
|
103
|
-
* to the same file proactively recycles our own leftover lock — the holder is
|
|
104
|
-
* us, so a leftover is stale by definition. Module-level by design: it must
|
|
105
|
-
* survive across `nodeEvolutionIo()` instances for the self-heal to be
|
|
106
|
-
* effective. (Not a pure function — the cross-call state is the intent.)
|
|
107
|
-
* V4-05: this set — not an mtime heuristic — is the ONLY signal that a
|
|
108
|
-
* same-pid lock is a leftover rather than a live in-process task, so it is the
|
|
109
|
-
* sole gate for the self-pid recycle branch. Exported (read-only in practice)
|
|
110
|
-
* so `io.spec.ts` can drive the self-heal path deterministically.
|
|
111
|
-
*/
|
|
112
|
-
export declare const pendingSelfCleanup: Map<string, string>;
|
|
113
101
|
/**
|
|
114
102
|
* Retry a rename that a peer is temporarily holding on Windows (EPERM/EBUSY):
|
|
115
103
|
* exponential backoff (50ms doubling, capped at 800ms) with a ~2s total
|
|
@@ -159,57 +147,6 @@ export declare function writeDurableTmp(target: string, content: string, openImp
|
|
|
159
147
|
* stranded-lock sweep must use the same liveness rule.
|
|
160
148
|
*/
|
|
161
149
|
export declare function isProcessAlive(pid: number): boolean;
|
|
162
|
-
/** F-17 (v18): the write-lock protocol is a cross-module contract — the lock
|
|
163
|
-
* file is `<target>.lock` and its body is `<pid>:<token>`. This module creates
|
|
164
|
-
* them (`withWriteLock`) and `skill-store`'s probes/sweepers parse them, so both
|
|
165
|
-
* consume these two constants instead of repeating the literals. */
|
|
166
|
-
export declare const LOCK_SUFFIX = ".lock";
|
|
167
|
-
/** Writer-lock body shape: a decimal pid, a colon, then the claim token. A
|
|
168
|
-
* torn body (no parsable pid) still matches the `\\d+:` prefix rule only when
|
|
169
|
-
* the pid part is intact, which is what the takeover probe needs. The capture
|
|
170
|
-
* group feeds `parseLockBody` (v20 A-1: skill-store's sweepers consume this
|
|
171
|
-
* helper instead of re-inlining a third regex copy — F-17 single-source). */
|
|
172
|
-
export declare const LOCK_BODY_RE: RegExp;
|
|
173
|
-
/** Parse a writer-lock body into its holder pid. `null` when the body does
|
|
174
|
-
* not have the `pid:token` shape at all (e.g. a user support file named
|
|
175
|
-
* `*.lock`) — callers leave such files alone. A shape-matching body always
|
|
176
|
-
* yields a number, possibly `0` (state corruption / hand edit): there is no
|
|
177
|
-
* pid 0, so a `0` holder must be treated as DEAD by the caller —
|
|
178
|
-
* `isProcessAlive(0)` signals the caller's own process group on POSIX and
|
|
179
|
-
* answers true, so probe sites must guard `holder > 0` before probing. */
|
|
180
|
-
export declare function parseLockBody(body: string): number | null;
|
|
181
|
-
/**
|
|
182
|
-
* V27 G0.2 (EVO-IO-01): the write lock named by this claim is no longer ours
|
|
183
|
-
* at the commit point — a takeover reclaimed it while we were inside the
|
|
184
|
-
* critical section. The lock layer converts this into a retry of the whole
|
|
185
|
-
* read-modify-write; it must never reach a caller as a successful write.
|
|
186
|
-
*/
|
|
187
|
-
export declare class LostWriteLock extends Error {
|
|
188
|
-
constructor();
|
|
189
|
-
}
|
|
190
|
-
/** V27 G1.1: the three takeover windows, as protocol constants at ONE place
|
|
191
|
-
* (the inline copies that used to live in the acquisition loop, plus the dead
|
|
192
|
-
* branch's bare `1000`, are gone). */
|
|
193
|
-
/** A named holder that is gone, past this age, is reclaimed. Fits inside the
|
|
194
|
-
* `lockAttempts * 50ms` retry budget so a dead holder's lock is reachable
|
|
195
|
-
* within one budget (v19 gate arithmetic: budget >= 2 x threshold). */
|
|
196
|
-
export declare const DEAD_LOCK_TAKEOVER_MS = 1000;
|
|
197
|
-
/** No body at all: nothing attributes the lock to a holder, so this is the one
|
|
198
|
-
* branch that cannot probe liveness — it must outlast any plausible stall
|
|
199
|
-
* between create and body write (V27 G0.2: 1s was below what a loaded machine
|
|
200
|
-
* actually took, which let a peer delete a live holder's lock). */
|
|
201
|
-
export declare const EMPTY_LOCK_TAKEOVER_MS = 30000;
|
|
202
|
-
/** A body with no parseable pid (crash mid-write): 1h, far above any legal hold
|
|
203
|
-
* and far below "forever". */
|
|
204
|
-
export declare const LOCK_TEAR_TAKEOVER_MS = 3600000;
|
|
205
|
-
/** A2 (audit P1-2): even a lock whose holder pid probes ALIVE is reclaimable
|
|
206
|
-
* past this age. Liveness-by-pid cannot distinguish the original holder from
|
|
207
|
-
* an unrelated process the OS later assigned the same pid, so the plain alive
|
|
208
|
-
* probe let one recycled pid brick every writer of one state file forever.
|
|
209
|
-
* No write in this family holds a lock for more than minutes (the longest is
|
|
210
|
-
* the ~120s review window), so a day-old "alive" lock is a recycled pid, not
|
|
211
|
-
* a live writer. */
|
|
212
|
-
export declare const ALIVE_LOCK_TAKEOVER_MS = 86400000;
|
|
213
150
|
/**
|
|
214
151
|
* V27 G1.3: the error `commitTmp` throws when the rename landed but the parent
|
|
215
152
|
* directory fsync failed — the bytes ARE visible, only their durability is
|
|
@@ -219,49 +156,53 @@ export declare const ALIVE_LOCK_TAKEOVER_MS = 86400000;
|
|
|
219
156
|
* shape as SUCCESS-with-warning, never as a rejection.
|
|
220
157
|
*/
|
|
221
158
|
export declare function isCommittedWarning(error: unknown): boolean;
|
|
222
|
-
/** V27 G1.1: the takeover branches, as a value. */
|
|
223
|
-
type TakeoverDecision = 'none' | 'dead' | 'empty' | 'corrupt';
|
|
224
|
-
/** V27 G1.1: one lock observation, plus the liveness probe for its pid. */
|
|
225
|
-
interface TakeoverProbe {
|
|
226
|
-
/** Raw lock body. An empty string means the file exists with no content. */
|
|
227
|
-
body: string;
|
|
228
|
-
/** Lock mtime in epoch ms. */
|
|
229
|
-
mtimeMs: number;
|
|
230
|
-
/** Liveness probe for a pid (injected so the decision is a pure function). */
|
|
231
|
-
alive: (pid: number) => boolean;
|
|
232
|
-
/** Evaluation instant (defaults to now). */
|
|
233
|
-
nowMs?: number;
|
|
234
|
-
/** Threshold overrides — production callers use the protocol defaults. */
|
|
235
|
-
deadAfterMs?: number;
|
|
236
|
-
emptyAfterMs?: number;
|
|
237
|
-
corruptAfterMs?: number;
|
|
238
|
-
aliveAfterMs?: number;
|
|
239
|
-
}
|
|
240
159
|
/**
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
245
|
-
* window → wait, never steal;
|
|
246
|
-
* - `dead` a named holder that is gone past the dead threshold, OR whose
|
|
247
|
-
* pid still probes alive but whose lock is older than the alive
|
|
248
|
-
* window (A2: a recycled pid must not hold the file forever);
|
|
249
|
-
* - `empty` no body at all: nothing attributes it to a holder, so only the
|
|
250
|
-
* wide `emptyAfterMs` window may reclaim it;
|
|
251
|
-
* - `corrupt` a body with no parseable pid (a crash mid-write), past the 1h
|
|
252
|
-
* tear threshold.
|
|
253
|
-
* The age thresholds compare against `nowMs - mtimeMs` with `>` so a lock whose
|
|
254
|
-
* age EQUALS the threshold is not yet reclaimed (the boundary the v19 gate fix
|
|
255
|
-
* pinned).
|
|
160
|
+
* Build the Node IO backend: plain fs promises, durable tmp + rename commits (crash atomicity) and
|
|
161
|
+
* ONE concurrency guarantee of its own — writes to the same path are serialized in-process (see
|
|
162
|
+
* `inProcessWrite`). G2 S4 removed the cross-process write lock and its takeover protocol; the
|
|
163
|
+
* plugin path gets cross-process safety from the platform provider (`ctx.fs`).
|
|
256
164
|
*/
|
|
257
|
-
export declare function
|
|
165
|
+
export declare function nodeEvolutionIo(): EvolutionIoLike;
|
|
166
|
+
/** F-211: the fs surface atomicWriteFiles uses — injectable so the commit-phase
|
|
167
|
+
* rename-failure path is deterministically testable. Defaults to node:fs.
|
|
168
|
+
* `mtime` (optional) probes a path's last-write time so the failed-commit
|
|
169
|
+
* recovery message can name the generation it restored from. */
|
|
170
|
+
interface FsOps {
|
|
171
|
+
writeFileSync(path: string, data: string | Uint8Array): void;
|
|
172
|
+
existsSync(path: string): boolean;
|
|
173
|
+
copyFileSync(from: string, to: string): void;
|
|
174
|
+
renameSync(from: string, to: string): void;
|
|
175
|
+
rmSync(path: string, options?: {
|
|
176
|
+
force?: boolean;
|
|
177
|
+
recursive?: boolean;
|
|
178
|
+
}): void;
|
|
179
|
+
mtime?(path: string): number | null;
|
|
180
|
+
}
|
|
258
181
|
/**
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
*
|
|
182
|
+
* S6.3 (E-40): atomically replace a set of files within one directory.
|
|
183
|
+
* Each file is staged to a sibling `<name>.tmp`, then renamed into place
|
|
184
|
+
* (atomic on the same filesystem); a pre-existing file keeps a single
|
|
185
|
+
* `<name>.bak`. A failure at ANY staging step removes the staged temps and
|
|
186
|
+
* leaves the previous files untouched — a half-written target is impossible
|
|
187
|
+
* up to the staging and backup phases.
|
|
188
|
+
*
|
|
189
|
+
* F-211: the rename COMMIT phase does not carry the same guarantee. When a
|
|
190
|
+
* filesystem refuses to overwrite the destination, the code removes it and
|
|
191
|
+
* retries the rename; if the retry ALSO fails the destination is gone. The
|
|
192
|
+
* single `.bak` created above still holds the previous content, so the target
|
|
193
|
+
* is restored from it (best-effort) before the original rename error is
|
|
194
|
+
* rethrown, so a failed commit never leaves the file missing.
|
|
195
|
+
*
|
|
196
|
+
* V4-17: the `.bak` is refreshed to the committed content after every
|
|
197
|
+
* successful commit, so a later failed commit always recovers to the most
|
|
198
|
+
* recent good generation (a once-at-first-install `.bak` could otherwise
|
|
199
|
+
* restore content several installs old). The recovery error names the `.bak`
|
|
200
|
+
* source (and mtime when available) so the caller knows the generation it
|
|
201
|
+
* restored from.
|
|
264
202
|
*/
|
|
265
|
-
export declare function
|
|
203
|
+
export declare function atomicWriteFiles(targetDir: string, writes: Array<{
|
|
204
|
+
name: string;
|
|
205
|
+
content: string | Uint8Array;
|
|
206
|
+
}>, fs?: FsOps, logger?: (message: string) => void): void;
|
|
266
207
|
export {};
|
|
267
208
|
//# sourceMappingURL=io.d.ts.map
|
package/lib/types/params.d.ts
CHANGED
|
@@ -112,6 +112,16 @@ export interface ParamExposure {
|
|
|
112
112
|
/** The registered reason this row has NO card. Required on every non-E3 row; mutually exclusive
|
|
113
113
|
* with `card`. This is what makes "an E3 row that no card renders" unspellable. */
|
|
114
114
|
why?: ParamWhy;
|
|
115
|
+
/**
|
|
116
|
+
* A cross-field constraint this row must satisfy, declared ONCE here (U-4). The card checks it
|
|
117
|
+
* before a save is attempted and the Host write path derives the same check from the same column,
|
|
118
|
+
* so "保存前就知道输入合不合法" needs no second rule table. Only relations with real behaviour
|
|
119
|
+
* behind them are declared; this is not a constraint language.
|
|
120
|
+
*/
|
|
121
|
+
relation?: {
|
|
122
|
+
/** The sibling id in the SAME group whose value this row may not go below. */
|
|
123
|
+
readonly atLeast?: string;
|
|
124
|
+
};
|
|
115
125
|
/** Unit suffix shown next to a number (empty when it carries none). */
|
|
116
126
|
unit?: string;
|
|
117
127
|
/** Pipe-joined allowed values; non-empty exactly when control is 'select'. */
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Every platform service the family reaches for with `ctx.get`, and what degrades without it.
|
|
3
3
|
*
|
|
4
|
-
* O-2 (v46 audit): the family probes
|
|
4
|
+
* O-2 (v46 audit): the family probes 28 platform services optionally — the platform has no
|
|
5
5
|
* formalized "optional dependency", so an ABSENT service is a silent downgrade and the diagnostic
|
|
6
6
|
* surface is the only place a reader can learn about it. `/evolution doctor` used to judge six of
|
|
7
7
|
* them; this table is the single source it judges the rest from, and the arch guard cross-checks it
|
|
@@ -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 28 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
|
|
@@ -58,11 +58,29 @@ interface RowOverride {
|
|
|
58
58
|
}
|
|
59
59
|
/**
|
|
60
60
|
* Read and validate the shared override table.
|
|
61
|
+
* G6.1/M1: the path is a PARAMETER (default: the table beside this module). The source installer
|
|
62
|
+
* runs the generated port from `scripts/`, so its own root has to be passed in — a module-relative
|
|
63
|
+
* read would look for `scripts/../row-overrides.json` and fail.
|
|
64
|
+
* @param path - the table to read.
|
|
61
65
|
* @returns the entries, in file order. A malformed table fails LOUD: a composer
|
|
62
66
|
* that silently skipped an entry would ship a preset running the platform
|
|
63
67
|
* default while nothing downstream reported it.
|
|
64
68
|
*/
|
|
65
|
-
export declare function loadRowOverrides(): RowOverride[];
|
|
69
|
+
export declare function loadRowOverrides(path?: string): RowOverride[];
|
|
70
|
+
/**
|
|
71
|
+
* Row ids of a composition fragment (line scan, no YAML library).
|
|
72
|
+
*
|
|
73
|
+
* PLAN S5.9 (2026-09-16, audit P2-27): the id extraction accepts INDENTED
|
|
74
|
+
* `- id:` rows too, so a collision hidden in a nested group is still caught —
|
|
75
|
+
* the old `^- id:` anchored at column 0 and was blind to exactly the rows an
|
|
76
|
+
* upstream group nesting would produce. Twin of `rowIds` in
|
|
77
|
+
* `scripts/install-layered.mjs` (installer.spec pins detection parity).
|
|
78
|
+
* Boundary (current, deliberate): DETECTION covers nested rows, while the
|
|
79
|
+
* override INJECTION anchors (`applyOneOverride` below) still match top-level
|
|
80
|
+
* rows only — the injection indent contract (`^ {2}key:`) is defined against a
|
|
81
|
+
* column-0 row.
|
|
82
|
+
*/
|
|
83
|
+
export declare function compositionRowIds(composition: string): Set<string>;
|
|
66
84
|
/** The platform package a DECLARATIVE preset row names (0.2.x replaced preset directories with it). */
|
|
67
85
|
export declare const AGENT_PRESET_PACKAGE = "@deepseek-ai/dsh-agent-preset";
|
|
68
86
|
/** Identity and display metadata of one generated preset row. */
|
|
@@ -78,6 +96,31 @@ export interface PresetRowIdentity {
|
|
|
78
96
|
/** Sort order among the registered presets. */
|
|
79
97
|
readonly order: number;
|
|
80
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* Compose the DECLARATIVE preset row from the platform base composition and the family delta.
|
|
101
|
+
*
|
|
102
|
+
* 0.2.x removed the directory mechanism: `dsh-agent-preset` is an ordinary composition row whose
|
|
103
|
+
* `config.plugins` carries the preset's complete child plugin list
|
|
104
|
+
* (`packages/preset/agent-preset/src/index.ts`, `agent-preset-registry/src/definition.ts` — the
|
|
105
|
+
* definition has NO base/extends field, which is why the platform base is merged in here).
|
|
106
|
+
*
|
|
107
|
+
* The composition itself is {@link composePresetComposition}: same row-collision contract and the
|
|
108
|
+
* same `row-overrides.json` injection, applied while the composed rows are still at column 0 — the
|
|
109
|
+
* override anchors are defined against a column-0 `- id:` row, so the indentation that puts them
|
|
110
|
+
* under `plugins:` must come last.
|
|
111
|
+
* @param baseComposition - the platform base preset patch/composition rows.
|
|
112
|
+
* @param deltaComposition - the family delta rows.
|
|
113
|
+
* @param identity - row id, preset id, display fields and order.
|
|
114
|
+
* @returns the row text, newline-terminated, ready to merge into a profile patch.
|
|
115
|
+
*/
|
|
116
|
+
/**
|
|
117
|
+
* Drop the blank lines that merely separate block-sequence items, keeping the ones INSIDE a block
|
|
118
|
+
* literal (`key: |` / `>`): there a blank line is part of the value, and filtering it flattened a
|
|
119
|
+
* platform base preset's multi-paragraph prompt into one paragraph (G6 review, P1).
|
|
120
|
+
* @param text - the composition rows.
|
|
121
|
+
* @returns the rows with the separating blank lines removed.
|
|
122
|
+
*/
|
|
123
|
+
export declare function denseBlockSequence(text: string): string;
|
|
81
124
|
export declare function composePresetRow(baseComposition: string, deltaComposition: string, identity: PresetRowIdentity): string;
|
|
82
125
|
/**
|
|
83
126
|
* Wrap a composed row as the patch entry a profile patch needs.
|
|
@@ -183,6 +226,36 @@ export interface BasePresetPatchSources {
|
|
|
183
226
|
* @throws {Error} when no candidate exists.
|
|
184
227
|
*/
|
|
185
228
|
export declare function resolveBasePresetPatch(base: string, sources: BasePresetPatchSources): string;
|
|
229
|
+
/**
|
|
230
|
+
* Probe every `@deepseek-ai/dsh-*` package under one unpacked resource root.
|
|
231
|
+
* @param resourceRoot - e.g. `<install>/resources/app.asar.unpacked`.
|
|
232
|
+
* @param file - the patch file name to look for.
|
|
233
|
+
* @param probes - the caller's probe log, appended to for the failure report.
|
|
234
|
+
* @returns the patch path when one exists.
|
|
235
|
+
*/
|
|
236
|
+
export declare function probeDesktopScope(resourceRoot: string, file: string, probes: string[]): string | undefined;
|
|
237
|
+
/**
|
|
238
|
+
* Subdirectories of one directory, or none when it does not exist: a missing level is an ordinary
|
|
239
|
+
* miss on a candidate walk, not a failure.
|
|
240
|
+
* @param dir - the directory to list.
|
|
241
|
+
* @returns the directory names.
|
|
242
|
+
*/
|
|
243
|
+
export declare function listDirectories(dir: string): string[];
|
|
244
|
+
/**
|
|
245
|
+
* The block of one row id: the enclosing column-0 list item and the row's own first line.
|
|
246
|
+
*
|
|
247
|
+
* An installer-written row sits inside `- insert:` (four spaces deeper); a row the Web editor
|
|
248
|
+
* saved is that column-0 item itself. Both must be replaced whole, so the span reaches back to the
|
|
249
|
+
* enclosing item and forward to the next one.
|
|
250
|
+
* @param lines - the patch text's lines.
|
|
251
|
+
* @param rowId - the row id to locate.
|
|
252
|
+
* @returns the span, or null when the patch carries no such row.
|
|
253
|
+
*/
|
|
254
|
+
export declare function rowBlockSpan(lines: string[], rowId: string): {
|
|
255
|
+
start: number;
|
|
256
|
+
end: number;
|
|
257
|
+
rowStart: number;
|
|
258
|
+
} | null;
|
|
186
259
|
/**
|
|
187
260
|
* Remove one row from a patch, taking its `- insert:` item with it when that item holds nothing
|
|
188
261
|
* else (an empty insert entry is left-over structure the platform would keep parsing).
|
|
@@ -191,6 +264,14 @@ export declare function resolveBasePresetPatch(base: string, sources: BasePreset
|
|
|
191
264
|
* @returns the patch text, newline-terminated; byte-identical when the id is absent.
|
|
192
265
|
*/
|
|
193
266
|
export declare function removePresetRow(patchText: string, rowId: string): string;
|
|
267
|
+
/**
|
|
268
|
+
* The end of one nested row's own block inside a patch entry.
|
|
269
|
+
* @param lines - the patch text's lines.
|
|
270
|
+
* @param rowStart - the row's `- id:` line.
|
|
271
|
+
* @param itemEnd - the enclosing item's end.
|
|
272
|
+
* @returns the index of the next sibling row, or the item's end.
|
|
273
|
+
*/
|
|
274
|
+
export declare function rowSiblingEnd(lines: string[], rowStart: number, itemEnd: number): number;
|
|
194
275
|
/**
|
|
195
276
|
* Merge one generated row into a composition (a profile patch) by row id.
|
|
196
277
|
*
|
|
@@ -212,6 +293,8 @@ export declare function removePresetRow(patchText: string, rowId: string): strin
|
|
|
212
293
|
export declare function mergePresetRow(patchText: string, row: string, rowId: string): string;
|
|
213
294
|
/** The empty patch list the platform seeds a profile patch with; see {@link mergePresetRow}. */
|
|
214
295
|
export declare const EMPTY_PATCH_SEED = "[]";
|
|
296
|
+
/** Drop trailing blank lines only: a kept line's own terminator (a CRLF file's `\r`) is never touched. */
|
|
297
|
+
export declare function trimTrailingBlankLines(lines: string[]): string[];
|
|
215
298
|
/**
|
|
216
299
|
* The text to write when a patch is left with no entries.
|
|
217
300
|
*
|
|
@@ -57,6 +57,8 @@ export interface VersionRowFacts {
|
|
|
57
57
|
readonly charsDelta?: number;
|
|
58
58
|
readonly path?: string;
|
|
59
59
|
readonly summary?: string;
|
|
60
|
+
/** U-5: the body hash the host recorded for this version (the row shows a short code, hover = full). */
|
|
61
|
+
readonly hash?: string;
|
|
60
62
|
}
|
|
61
63
|
/** The facts a skill row needs. The client's `SkillRow` satisfies this structurally. */
|
|
62
64
|
export interface SkillRowFacts {
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* itself in `this.serial` (archive/restoreFromArchive deliberately do not).
|
|
20
20
|
* 2. **Per-directory write lock** (io.ts LOCK_*) — cross-process mutual
|
|
21
21
|
* exclusion plus in-process crash ownership (tickets, takeover). Checked
|
|
22
|
-
* with `
|
|
22
|
+
* with `supportDirsUnlistable` before any destructive move (archive/restore/
|
|
23
23
|
* snapshot); held inside transactIo by byte writers.
|
|
24
24
|
* 3. **CAS baseline (`expected:`)** — any read whose bytes feed a later write
|
|
25
25
|
* must either live inside the serial section that commits the write, or
|
|
@@ -306,9 +306,8 @@ export declare class SkillLibrary {
|
|
|
306
306
|
* found no SKILL.md — remove the possibly-resurrected directory ONLY when it
|
|
307
307
|
* holds nothing but this path's own write-lock file. The BR-5 rule from
|
|
308
308
|
* setPinnedCore/createCore applies unchanged: a concurrent mover can land a
|
|
309
|
-
* full directory between the list probe and the remove, so
|
|
310
|
-
*
|
|
311
|
-
* away. Best-effort: a failed cleanup leaves the "not found" result
|
|
309
|
+
* full directory between the list probe and the remove, so ANY content at all (support files, a
|
|
310
|
+
* fresh restore) must never be recursed away. Best-effort: a failed cleanup leaves the "not found" result
|
|
312
311
|
* unchanged (the operator-facing ghost-dir refusal is fail-loud already).
|
|
313
312
|
*/
|
|
314
313
|
private cleanupGhostDir;
|
|
@@ -481,58 +480,19 @@ export declare class SkillLibrary {
|
|
|
481
480
|
* P2 (v16): the write-lock probe for the DESTRUCTIVE MOVERS (archive /
|
|
482
481
|
* restoreFromArchive). A byte-writer mid-flight is the ghost-generator —
|
|
483
482
|
* after the move its transact commit re-creates `<dir>/…` (mkdir
|
|
484
|
-
* recursive) and the tree ends half-archived.
|
|
485
|
-
*
|
|
486
|
-
*
|
|
487
|
-
*
|
|
488
|
-
*
|
|
489
|
-
* and restore would later move the residue back into the live root).
|
|
490
|
-
* Coverage: `SKILL.md.lock` (update/patch of the body) plus one level of
|
|
491
|
-
* each support dir (write_file's lock sits next to its file). Residual:
|
|
492
|
-
* NESTED support-subdir locks and the probe→rename TOCTOU itself remain
|
|
493
|
-
* fail-safe (renameWithRetry rides the write out; the writer's locked
|
|
494
|
-
* re-read refuses on the moved-away file). v37 (P2-8): a residue `.lock` whose
|
|
495
|
-
* holder is GONE is not a writer — `decideTakeover` owns that verdict.
|
|
483
|
+
* recursive) and the tree ends half-archived.
|
|
484
|
+
* G2 S4: the writer-lock probe that used to answer this question is retired — writes go through
|
|
485
|
+
* the provider's versioned writes, so a writer that raced the move fails its own write loudly
|
|
486
|
+
* instead of landing on a moved-away path. What REMAINS load-bearing here is the fail-closed
|
|
487
|
+
* listing rule, which is about the filesystem misbehaving rather than about writers.
|
|
496
488
|
*/
|
|
497
|
-
private
|
|
498
|
-
/** P2 (v17): a `*.lock` file counts as a writer lock only when its body
|
|
499
|
-
* carries the io layer's protocol — `pid:token`, a bare pid, or an empty body
|
|
500
|
-
* (a creator between create and body write); anything else is user content
|
|
501
|
-
* (suffix-only matching refused archiving and deleted user files on restore).
|
|
502
|
-
* v37 (P2-8): the verdict is `decideTakeover`'s, so the mover refuses exactly
|
|
503
|
-
* the lock the io layer refuses to reclaim, the 30s empty window included. */
|
|
504
|
-
private isWriterLock;
|
|
505
|
-
/** P2 (v16): best-effort removal of lock residue inside a RESTORED tree.
|
|
506
|
-
* A1-2/A1-7 (v18): the sweep now covers the marker locks the probe checks
|
|
507
|
-
* (`SKILL.md.lock`/`.pinned.lock`/`.hermes-managed.lock`) and only removes
|
|
508
|
-
* a lock whose holder pid is NOT alive — a live writer's lock is never
|
|
509
|
-
* stolen by the sweep. A dead-pid residue would otherwise permanently
|
|
510
|
-
* refuse archive/restore. */
|
|
511
|
-
private deleteStrandedLocks;
|
|
512
|
-
/** R2 follow-up (audit of A2): the recycled-pid window from io's
|
|
513
|
-
* `ALIVE_LOCK_TAKEOVER_MS`, applied to the stranded-lock sweepers. A lock
|
|
514
|
-
* whose holder pid probes alive but whose mtime is older than the alive
|
|
515
|
-
* window is a recycled pid, not a live writer (no write in this family
|
|
516
|
-
* holds a lock for more than minutes) — without this, one recycled pid
|
|
517
|
-
* blocked whole-tree snapshot recovery forever with a "retry once the
|
|
518
|
-
* write completes" message that could never become true. A backend without
|
|
519
|
-
* `mtime` keeps the conservative refuse-on-alive posture. */
|
|
520
|
-
private lockHolderAgedOut;
|
|
521
|
-
/** Remove `lockPath` only when its body has the writer-lock `pid:token`
|
|
522
|
-
* shape AND the holder pid is not alive (or is an aged-out recycled pid —
|
|
523
|
-
* R2 follow-up); anything else (a user support file or a live writer's
|
|
524
|
-
* lock) is left untouched. */
|
|
525
|
-
private sweepLockIfStranded;
|
|
489
|
+
private supportDirsUnlistable;
|
|
526
490
|
/** A1-4 (v18): a manifest-declared name is copied with `join(root, name)`;
|
|
527
491
|
* only a single, non-traversing path component is safe. Dotfiles
|
|
528
492
|
* (`.usage.json`) stay allowed — sidecars are legitimately dot-prefixed.
|
|
529
493
|
* P2-4 (v19): a non-string entry (`skills: [123]`) is refused structurally
|
|
530
494
|
* instead of throwing `name.includes is not a function`. */
|
|
531
495
|
private safeSnapshotEntryName;
|
|
532
|
-
/** A1-7 (v18): a root-level lock whose holder is alive must refuse the
|
|
533
|
-
* restore; a dead residue is swept so a crashed writer cannot block
|
|
534
|
-
* recovery. A non-lock body shape is left alone (user file). */
|
|
535
|
-
private refuseLiveLockOrSweep;
|
|
536
496
|
archive(rawName: string, options?: ArchiveOptions): Promise<SkillActionResult>;
|
|
537
497
|
/**
|
|
538
498
|
* V2 (design §16.7): what an apply-mode consolidation would do for ONE source,
|
package/package.json
CHANGED
|
@@ -122,10 +122,10 @@
|
|
|
122
122
|
"id": "skill-tree",
|
|
123
123
|
"path": "<skillsRoot>/**",
|
|
124
124
|
"writer": "evolution-core/src/skill-store.ts",
|
|
125
|
-
"serializedBy": "
|
|
126
|
-
"marker": "
|
|
125
|
+
"serializedBy": "transact",
|
|
126
|
+
"marker": "this.transact(this.io, path",
|
|
127
127
|
"state": [],
|
|
128
|
-
"note": "The skill tree itself.
|
|
128
|
+
"note": "The skill tree itself. G2 S4: the family write-lock protocol is retired; every byte writer goes through the provider's versioned read-modify-write (`transact`), so this entry is serialized by the same seam as the rest. Destructive movers refuse only when a support directory cannot be listed (fail-closed)."
|
|
129
129
|
},
|
|
130
130
|
{
|
|
131
131
|
"id": "activity-log",
|