@lmzhen/dsh-evolution-core 0.3.67 → 0.3.68

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.
@@ -83,4 +83,18 @@ export declare const EVOLUTION_WRITE_TOOLS: readonly ["memory", "skill_manage"];
83
83
  * 0.3.16 (T-4): moved here from skill-store.ts so drift-signals (pure, no IO)
84
84
  * can reference it without importing the skill-store module. */
85
85
  export declare const AUTHORING_DESCRIPTION_BAR = 60;
86
+ /** V27 G2.4: the largest millisecond delay a timer accepts. `AbortSignal.timeout`
87
+ * (and `setTimeout`) coerce anything larger to 1ms after a Node warning, so a
88
+ * timeout configured above this ceiling silently collapses to "immediately
89
+ * aborted". The curator's review timeout and the review timeout each carried
90
+ * their own copy of the literal; the bound is one protocol constant.
91
+ * (v19 P2-10 corrected the value from 2^32-1 to Node's real 2^31-1 ceiling.) */
92
+ export declare const MAX_TIMER_DELAY_MS = 2147483647;
93
+ /** V27 G2.4: the model each review/curation leg defaults to. The policy schema,
94
+ * the policy resolver and the curator's LLM nomination pass each carried their
95
+ * own copy of these strings — a deployment that changed the policy default used
96
+ * to leave the curator passing a different model than the reviews. */
97
+ export declare const DEFAULT_MEMORY_REVIEW_MODEL = "deepseek-v4-flash";
98
+ export declare const DEFAULT_SKILL_REVIEW_MODEL = "deepseek-v4-pro";
99
+ export declare const DEFAULT_CURATOR_MODEL = "deepseek-v4-pro";
86
100
  //# sourceMappingURL=constants.d.ts.map
@@ -57,7 +57,17 @@ export interface CuratorRunReport {
57
57
  llmNominations: string[];
58
58
  archiveCandidates: string[];
59
59
  archived: CuratorArchivedSkill[];
60
+ /** Failures attributable to a SKILL NAME (`<name>: …`). V27 CUR-2: this field
61
+ * is the skill-attributable subset only — run-level facts (an abort, a phase
62
+ * that could not start) live in `aborted`/`unattributed` so a report can no
63
+ * longer show `failed: 0` for a run that was cut short. */
60
64
  failed: CuratorFailedSkill[];
65
+ /** V27 CUR-2: the run stopped early (e.g. disposed mid-run, timeout). The
66
+ * string names the phase that did not complete; absent on a full run. */
67
+ aborted?: string;
68
+ /** V27 CUR-2: error strings that could not be attributed to a skill name.
69
+ * They were previously collected in memory and then dropped on write. */
70
+ unattributed?: string[];
61
71
  /** Consolidations actually executed this run (source absorbed into target). */
62
72
  consolidated?: CuratorConsolidation[];
63
73
  snapshotPath?: string;
@@ -75,6 +85,10 @@ export interface CuratorReportInput {
75
85
  archiveCandidates: readonly string[];
76
86
  archived: readonly CuratorArchivedSkill[];
77
87
  failed: readonly CuratorFailedSkill[];
88
+ /** V27 CUR-2: run-level abort reason (see CuratorRunReport.aborted). */
89
+ aborted?: string;
90
+ /** V27 CUR-2: errors with no skill name to attribute them to. */
91
+ unattributed?: readonly string[];
78
92
  consolidated?: readonly CuratorConsolidation[];
79
93
  snapshotPath?: string;
80
94
  llmReviewEnabled?: boolean;
@@ -39,12 +39,22 @@ export interface EvolutionPlanAppliedEvent {
39
39
  policyFingerprint?: string | undefined;
40
40
  memoryApplied: number;
41
41
  skillApplied: number;
42
+ /** Validation rejects ONLY (see the contract note below). V27 R-03: a plan op
43
+ * that was skipped because the session had not read the skill is NOT a
44
+ * validation reject and now travels in `skippedUnread`. */
42
45
  rejectedOps: number;
43
46
  /** 0.3.31 (V5-19): execution-layer failures — ops that reached execution but
44
47
  * did not land (non-throw `ok:false` results). `rejectedOps` counts only
45
48
  * VALIDATION rejects; a consumer that treats rejectedOps as "work not done"
46
49
  * would otherwise miss a partial application. */
47
50
  executionFailures?: number | undefined;
51
+ /** V27 R-03: "not done" has a third, independent cause — an op naming a skill
52
+ * this session never read (the review pipeline refuses to touch unread
53
+ * skills). It is neither a validation reject nor an execution failure, so it
54
+ * has its own field; folding it into `rejectedOps` broke that field's stated
55
+ * contract and made the replay leaderboard penalize one refusal twice
56
+ * (rejectedOps weight AND the executionFailures dimension). */
57
+ skippedUnread?: number | undefined;
48
58
  /** First execution-layer failure message (abort reason or op failure). */
49
59
  executionError?: string | undefined;
50
60
  evidenceQuotes?: number | undefined;
@@ -8,12 +8,15 @@
8
8
  *
9
9
  * Usage events (C semantics, rc.73+): `type:'usage'` records are the
10
10
  * OBSERVATION WINDOW ANCHOR — written once, when the library's first observed
11
- * read (`view_count` 0 -> 1) happens. Before that anchor the usage sidecar
12
- * has no read evidence (reads were invisible pre-A2), so churn-based health
13
- * judgments are NOT trustworthy; the curator suppresses them (its
14
- * `usageObserved()` gate) until the anchor exists. `counts` on the event is a
15
- * cumulative library-wide snapshot (skills/views/use/patches) at that moment,
16
- * and `window.opened` pins the window start for the timeline.
11
+ * read (`view_count` 0 -> 1) happens. The anchor is the durable timeline record
12
+ * of that moment: the churn-suppression gate itself (`usageObserved()`) reads
13
+ * the usage SIDECAR's own first-view evidence, since reads were invisible to it
14
+ * pre-A2 and no sidecar record can reach `view_count > 0` without the same
15
+ * 0 -> 1 transition. `counts` on the event is a cumulative library-wide
16
+ * snapshot (skills/views/use/patches) at that moment, and `window.opened` pins
17
+ * the window start for the timeline. (V27 G3.3: `verify-event-pairing` requires
18
+ * every persisted type to have a production reader or a declared external
19
+ * contract — this one is the latter.)
17
20
  *
18
21
  * Rotation (rc.71, 007 design): when the active log reaches
19
22
  * `EVENT_LOG_ROTATE_AT` the older half is split into an archive
package/lib/types/io.d.ts CHANGED
@@ -146,6 +146,71 @@ export declare const LOCK_BODY_RE: RegExp;
146
146
  * `*.lock`) — callers leave such files alone. A shape-matching body always
147
147
  * yields a number (possibly `0`, which `isProcessAlive` treats as dead). */
148
148
  export declare function parseLockBody(body: string): number | null;
149
+ /**
150
+ * V27 G0.2 (EVO-IO-01): the write lock named by this claim is no longer ours
151
+ * at the commit point — a takeover reclaimed it while we were inside the
152
+ * critical section. The lock layer converts this into a retry of the whole
153
+ * read-modify-write; it must never reach a caller as a successful write.
154
+ */
155
+ export declare class LostWriteLock extends Error {
156
+ constructor();
157
+ }
158
+ /** V27 G1.1: the three takeover windows, as protocol constants at ONE place
159
+ * (the inline copies that used to live in the acquisition loop, plus the dead
160
+ * branch's bare `1000`, are gone). */
161
+ /** A named holder that is gone, past this age, is reclaimed. Fits inside the
162
+ * `lockAttempts * 50ms` retry budget so a dead holder's lock is reachable
163
+ * within one budget (v19 gate arithmetic: budget >= 2 x threshold). */
164
+ export declare const DEAD_LOCK_TAKEOVER_MS = 1000;
165
+ /** No body at all: nothing attributes the lock to a holder, so this is the one
166
+ * branch that cannot probe liveness — it must outlast any plausible stall
167
+ * between create and body write (V27 G0.2: 1s was below what a loaded machine
168
+ * actually took, which let a peer delete a live holder's lock). */
169
+ export declare const EMPTY_LOCK_TAKEOVER_MS = 30000;
170
+ /** A body with no parseable pid (crash mid-write): 1h, far above any legal hold
171
+ * and far below "forever". */
172
+ export declare const LOCK_TEAR_TAKEOVER_MS = 3600000;
173
+ /**
174
+ * V27 G1.3: the error `commitTmp` throws when the rename landed but the parent
175
+ * directory fsync failed — the bytes ARE visible, only their durability is
176
+ * unconfirmed. A consumer that treats it as a plain failure reports "not
177
+ * written" for a write that happened (and a two-phase caller may try to roll
178
+ * back a visible write). Every transaction consumer must therefore treat this
179
+ * shape as SUCCESS-with-warning, never as a rejection.
180
+ */
181
+ export declare function isCommittedWarning(error: unknown): boolean;
182
+ /** V27 G1.1: the takeover branches, as a value. */
183
+ export type TakeoverDecision = 'none' | 'dead' | 'empty' | 'corrupt';
184
+ /** V27 G1.1: one lock observation, plus the liveness probe for its pid. */
185
+ export interface TakeoverProbe {
186
+ /** Raw lock body. An empty string means the file exists with no content. */
187
+ body: string;
188
+ /** Lock mtime in epoch ms. */
189
+ mtimeMs: number;
190
+ /** Liveness probe for a pid (injected so the decision is a pure function). */
191
+ alive: (pid: number) => boolean;
192
+ /** Evaluation instant (defaults to now). */
193
+ nowMs?: number;
194
+ /** Threshold overrides — production callers use the protocol defaults. */
195
+ deadAfterMs?: number;
196
+ emptyAfterMs?: number;
197
+ corruptAfterMs?: number;
198
+ }
199
+ /**
200
+ * V27 G1.1: the lock-takeover decision as ONE pure function, so the protocol is
201
+ * testable and exhaustive instead of being an inline expression inside the
202
+ * acquisition loop:
203
+ * - `none` the lock is fresh, or its holder is alive → wait, never steal;
204
+ * - `dead` a named holder that is gone, past the dead threshold;
205
+ * - `empty` no body at all: nothing attributes it to a holder, so only the
206
+ * wide `emptyAfterMs` window may reclaim it;
207
+ * - `corrupt` a body with no parseable pid (a crash mid-write), past the 1h
208
+ * tear threshold.
209
+ * The age thresholds compare against `nowMs - mtimeMs` with `>` so a lock whose
210
+ * age EQUALS the threshold is not yet reclaimed (the boundary the v19 gate fix
211
+ * pinned).
212
+ */
213
+ export declare function decideTakeover(probe: TakeoverProbe): TakeoverDecision;
149
214
  /**
150
215
  * Build the Node IO backend. `lockAttempts` scales the write-lock retry budget
151
216
  * (attempts × 50ms); the default 40 (~2s, rc.69) covers production contention,
@@ -100,6 +100,18 @@ export declare class MemoryStore {
100
100
  * silently overwritten.
101
101
  */
102
102
  private oversizedRefusal;
103
+ /**
104
+ * The ONE memory write skeleton (V27 G2.5): oversized read-guard pre-transact,
105
+ * one transaction over the target path, and the C-01 structured refusal when a
106
+ * backend never invokes the task. `addChained` and `applyBatchChained` supply
107
+ * only their own in-transaction core, so the two write paths cannot drift in
108
+ * their guard order, their missing-file handling or their error text.
109
+ *
110
+ * @param target - memory target being written
111
+ * @param core - the in-transaction read-modify-write for the locked body
112
+ * @returns the core's result, or the oversized / contract-violation refusal
113
+ */
114
+ private chainedWrite;
103
115
  add(target: MemoryTarget, facts: string): Promise<MemoryApplyResult>;
104
116
  private addChained;
105
117
  /**
@@ -108,26 +120,49 @@ export declare class MemoryStore {
108
120
  * (`current`) — never a second IO read. `write: null` means "no change".
109
121
  */
110
122
  private addCore;
111
- /** Canonical-form drift check derived from the locked view (same formula as `detectDrift`, no second read). */
112
- private driftFromRaw;
123
+ /**
124
+ * The single drift predicate. `raw` is in canonical form when it byte-matches
125
+ * `render(normalizeEntries(raw))`; anything else means it was edited outside
126
+ * MemoryStore (empty/`§`-only entries, stray blank lines, leading or trailing
127
+ * delimiters — structural anomalies the writer would quietly normalize away).
128
+ * Both write paths derive this from their locked view and `detectDrift` from
129
+ * a fresh read, so a write and a later read can never disagree about the same
130
+ * bytes.
131
+ *
132
+ * An absent, empty, or whitespace-only body is the "never written" state
133
+ * (rc.42 audit P1-6): it parses to zero entries, and the canonical form
134
+ * `'\n'` can never byte-match it, so flagging it would permanently refuse
135
+ * every write path — including the repairs the model would need to make.
136
+ * Such files are adopted instead of flagged.
137
+ *
138
+ * @param target - memory target whose char limit bounds one parsed entry
139
+ * @param raw - on-disk body, or `null` when the file does not exist
140
+ * @returns whether these bytes count as externally drifted
141
+ */
142
+ private drifted;
143
+ /**
144
+ * Drift refusal for a body already read under the write lock, or `null` when
145
+ * the body is canonical. Both write paths return this unchanged, so their
146
+ * refusals stay byte-identical and each carries the same backup.
147
+ *
148
+ * @param target - memory target that owns the drifted file
149
+ * @param raw - locked file body
150
+ * @returns the refusal to hand back, or `null` to continue writing
151
+ */
152
+ private driftRefusal;
113
153
  applyBatch(target: MemoryTarget, operations: MemoryOperation[]): Promise<MemoryApplyResult>;
114
154
  private applyBatchChained;
115
155
  /** Batch RMW inside the transaction. `write: null` = failure/no-op, disk untouched. */
116
156
  private applyBatchCore;
117
157
  renderContext(): Promise<string>;
118
158
  /**
119
- * Detect on-disk drift: true when the file is not in the canonical
120
- * `render(normalizeEntries(raw))` form. This catches structural anomalies
121
- * the writer would quietly normalize away (empty/`§`-only entries, stray
122
- * blank lines, leading/trailing delimiters) that indicate the file was
123
- * edited outside MemoryStore. Purely single-canonical content reaches the
124
- * same serialization and returns false, so a normal write is never flagged.
159
+ * Detect on-disk drift for a caller that holds no locked view: `true` when the
160
+ * file is not in canonical form, or when its size trips the read guard. The
161
+ * write paths apply the same predicate (`drifted`) to the body they read under
162
+ * the lock, so a write and a follow-up read agree about the same bytes.
125
163
  *
126
- * An absent, empty, or whitespace-only file is the "never written" state
127
- * (rc.42 audit P1-6): it parses to zero entries, so the canonical form
128
- * `'\n'` can never byte-match it and every write path was permanently
129
- * refused with "External drift detected" — including the repairs the model
130
- * would need to make. Such files are adopted instead of flagged.
164
+ * @param target - memory target to inspect
165
+ * @returns whether the file on disk counts as externally drifted
131
166
  */
132
167
  detectDrift(target: MemoryTarget): Promise<boolean>;
133
168
  }
@@ -113,21 +113,31 @@ export declare function skillsRoot(env?: NodeJS.ProcessEnv): string;
113
113
  export declare function resolveSkillsRoot(config?: {
114
114
  root?: string | undefined;
115
115
  }): string;
116
- /** E-7 (v18): every family row reads ONE root key. `root` is canonical;
117
- * `skillsRoot` is a deprecated alias honoured only while `root` is empty (so a
118
- * deployment that sets both keeps the canonical one) and removed after 0.3.65.
119
- * Callers log their own deprecation warning.
120
- * @param config - the raw plugin config, carrying `root` and/or `skillsRoot`.
121
- * @returns the effective root (empty when neither key is set) and whether the
122
- * deprecated alias supplied it.
116
+ /** E-7 (v18) → V27 G2.4 (M-08): every family row reads ONE root key. `root` is
117
+ * canonical; the `skillsRoot` alias was honoured for one minor version and its
118
+ * window closed at 0.3.65 it is now two releases past expiry, so this
119
+ * resolver no longer reads it at all. A deployment that still sets the alias
120
+ * must fail LOUDLY at load (see {@link assertSkillsRootAliasRetired}): silently
121
+ * ignoring a config key leaves the deployment pointing at a root nobody reads,
122
+ * which is the worst form of compatibility.
123
+ * @param config - the raw plugin config.
124
+ * @returns the effective root (empty when the key is unset or blank).
123
125
  */
124
126
  export declare function resolveRootConfig(config?: {
125
127
  root?: string | undefined;
126
- skillsRoot?: string | undefined;
127
128
  }): {
128
129
  root: string;
129
- usedDeprecatedAlias: boolean;
130
130
  };
131
+ /** V27 G2.4 (M-08): the retirement gate for the expired `skillsRoot` alias.
132
+ * Called at each plugin's load boundary (before the root is resolved), it turns
133
+ * a stale key into an explicit load error naming the replacement — the
134
+ * fail-loud form the plan requires instead of a silent no-op.
135
+ * @param config - the raw plugin config (the alias field stays DECLARED in each
136
+ * schema so the loader can hand it here instead of dropping it).
137
+ */
138
+ export declare function assertSkillsRootAliasRetired(config?: {
139
+ skillsRoot?: string | undefined;
140
+ }): void;
131
141
  /**
132
142
  * Map a requesting session onto the two origin surfaces (rc.44 plan M2-2.3):
133
143
  * the APPROVAL surface treats every delegated subagent as the autonomous
@@ -156,10 +166,17 @@ export interface Frontmatter {
156
166
  }
157
167
  /**
158
168
  * Shared frontmatter block detection (P3-3 single owner): opening line `---`
159
- * and closing line exactly `---` (both trimmed). Used by `parseFrontmatter`,
169
+ * and closing line exactly `---`. Used by `parseFrontmatter`,
160
170
  * `frontmatterYamlUnsafeValues` and `normalizeFrontmatter` so the three can
161
171
  * never disagree about where the block ends (the loose `indexOf('\n---')`
162
172
  * form matched `\n----` and was replaced by this strict line rule).
173
+ *
174
+ * V27 G2.1: both fence lines are matched EXACTLY, tolerating only a trailing
175
+ * `\r` — the same rule the upstream filesystem catalog uses
176
+ * (`skill-filesystem.parseFrontmatter`). The former `.trim()` comparison
177
+ * accepted ` --- `, so an indented fence loaded in the family while the
178
+ * platform ignored the file: family visibility split from platform visibility,
179
+ * which is exactly what a strict-YAML frontmatter is supposed to prevent.
163
180
  */
164
181
  export declare function frontmatterBlock(content: string): {
165
182
  block: string;
@@ -167,10 +184,51 @@ export declare function frontmatterBlock(content: string): {
167
184
  end: number;
168
185
  nl: string;
169
186
  } | null;
170
- export declare function parseFrontmatter(content: string): {
187
+ /**
188
+ * One frontmatter read (V27 G2.1): the values, the body, and every signal the
189
+ * strict-YAML platform catalog derives from the same block. Returned by
190
+ * {@link parseFrontmatter} so a caller never has to parse the block twice to
191
+ * reach a description and the catalog verdict.
192
+ */
193
+ export interface FrontmatterRead {
171
194
  frontmatter: Frontmatter;
172
195
  body: string;
173
- } | null;
196
+ /** Raw entries whose UNQUOTED value the strict catalog cannot load as
197
+ * written. Quotes are included, so a value already normalized by the write
198
+ * path (`normalizeFrontmatter`) is never re-flagged. */
199
+ unsafeValues: Array<{
200
+ key: string;
201
+ value: string;
202
+ }>;
203
+ /** Whether the frontmatter is not valid AS WRITTEN for the strict platform
204
+ * catalog: the strict parser rejects the block, or an unquoted value would
205
+ * read as something other than its text (a dropped ` # ` comment, a
206
+ * number/bool shorthand the catalog refuses as a string field). The write
207
+ * path quotes such a value on its next edit. */
208
+ catalogInvalid: boolean;
209
+ }
210
+ /**
211
+ * Parse a SKILL.md: its frontmatter values and body, or `null` when the file
212
+ * has no frontmatter block or no body. Every consumer of frontmatter values
213
+ * goes through here — the write path's validation, `list()`'s published
214
+ * description, `relatedSkillNames` and the audit — so all of them read the same
215
+ * bytes the same way.
216
+ *
217
+ * @param content - the SKILL.md text.
218
+ * @returns the read, or `null` when there is no block or no body.
219
+ */
220
+ export declare function parseFrontmatter(content: string): FrontmatterRead | null;
221
+ /**
222
+ * Whether this file's frontmatter is valid as written for the strict platform
223
+ * catalog (see `FrontmatterRead.catalogInvalid`). Body-independent (a body-less
224
+ * file is still judged), and derived from the same read as `parseFrontmatter` —
225
+ * so the audit's verdict and the values the family publishes for one file can
226
+ * never disagree (V27 G2.1).
227
+ *
228
+ * @param content - the SKILL.md text.
229
+ * @returns `true` when the strict parser rejects the block or an unquoted value would read as something else.
230
+ */
231
+ export declare function frontmatterCatalogInvalid(content: string): boolean;
174
232
  /** YAML plain-scalar hazards that make an UNQUOTED frontmatter value
175
233
  * unloadable to the platform catalog (strict YAML parser): `: ` (mapping
176
234
  * separator), ` #` (comment start), a trailing `:` (a mapping marker),
@@ -190,7 +248,10 @@ export declare function yamlPlainScalarNeedsQuotes(value: string): boolean;
190
248
  * YAML-unsafe for the strict platform catalog. Operates on the ORIGINAL line
191
249
  * value (quotes included), so a value already wrapped by
192
250
  * `normalizeFrontmatter` is never re-flagged — one source with the write
193
- * path. Single-line entries only; lines with embedded line breaks skip. */
251
+ * path. V27 G2.1: delegates to the shared scan, which
252
+ * `parseFrontmatter(...).catalogInvalid` also uses, so the audit view and the
253
+ * read view of one file can never disagree. Independent of the body: a
254
+ * body-less file is still reported here. */
194
255
  export declare function frontmatterYamlUnsafeValues(content: string): Array<{
195
256
  key: string;
196
257
  value: string;
@@ -508,6 +569,16 @@ export declare class SkillLibrary {
508
569
  * 64 chars each (the name-rule maximum — real skill names always fit).
509
570
  */
510
571
  private sanitizeSkippedNames;
572
+ /**
573
+ * V27 G0.4 (core-a-01): the per-entry gate `skipped` already has. A corrupted
574
+ * or hand-edited manifest could carry `extras: [123]`: the array check passed,
575
+ * `SNAPSHOT_EXTRA_NAME_RE.test(123)` coerced the number to the string "123"
576
+ * and matched, and the value then threw `TypeError` inside `path.join` — which
577
+ * `readSnapshotExtras` reached only AFTER a whole-tree restore had committed.
578
+ * Extras are path components under `extras/`, so they take the entry gate too;
579
+ * bounded like `skipped` so a hostile manifest cannot grow the read set.
580
+ */
581
+ private sanitizeExtraNames;
511
582
  readSnapshotManifest(path: string): Promise<SnapshotManifest | null>;
512
583
  /** Keep only the newest N snapshots (Hermes keep=5 parity); older ones are removed outright. */
513
584
  private retainSnapshots;
@@ -28,6 +28,9 @@ export interface ThreatFinding {
28
28
  * benign phrasing (e.g. a skill that legitimately opens with "You are now a ...")
29
29
  * can exclude that label by name here. This is opt-in and never widens strict
30
30
  * scope; it only permits callers to drop a known-innocent match.
31
+ * V27 G-1: an exclusion drops the FINDING for that label — it never removes the
32
+ * de-obfuscation the other patterns are matched against, so exempting the
33
+ * unicode rules cannot re-open a splitting bypass.
31
34
  */
32
35
  export interface ScanOptions {
33
36
  /** Pattern labels to skip during this scan. */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lmzhen/dsh-evolution-core",
3
3
  "description": "Shared stores, prompts, signals and lifecycle logic for the dsh-evolution plugin family (community build)",
4
- "version": "0.3.67",
4
+ "version": "0.3.68",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },