@lmzhen/dsh-evolution-core 0.1.0-rc.8 → 0.1.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.
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Shared constants for the dsh-evolution plugin family.
3
+ *
4
+ * Two classes of value live here, deliberately separated by section so future
5
+ * edits do not blur the semantic boundary:
6
+ *
7
+ * 1. **Fixed protocol/format/security invariants** — changing these breaks an
8
+ * on-disk format, a naming/format contract, a path-security boundary, or a
9
+ * cross-component invariant. They are NOT exposed as deployment config.
10
+ *
11
+ * 2. **Cross-package shared tunable defaults** — the same semantic default is
12
+ * read (with a config override path) by more than one package (e.g.
13
+ * `evolution-policy` and `evolution-curator` both default `staleAfterDays`
14
+ * to 30). Centralizing them here means one authoritative default: a config
15
+ * override still applies per package, but the fallback is single-sourced.
16
+ *
17
+ * Package-private tunables (used by exactly one package) stay in that package,
18
+ * not here — see evolution-replay's `DEFAULT_WEIGHTS` and evolution-feedback's
19
+ * threshold, which are intentionally left where they are used.
20
+ * @module @deepseek-ai/dsh-evolution-core
21
+ */
22
+ /** Skill frontmatter `name` validated for the file name (lowercase + hyphen). */
23
+ export declare const SKILL_NAME_RE: RegExp;
24
+ /** Allowed skill support-file subdirectories (path-traversal boundary). */
25
+ export declare const SUPPORT_DIRS: readonly ["references", "templates", "scripts", "assets"];
26
+ /** Delimiter between durable memory entries (on-disk storage format). */
27
+ export declare const ENTRY_DELIMITER = "\n\u00A7\n";
28
+ /** Built-in skill names the curator must never lifecycle-manage. */
29
+ export declare const PROTECTED_BUILTIN_SKILLS: ReadonlySet<string>;
30
+ export declare const MAX_SKILL_NAME_LENGTH = 64;
31
+ export declare const MAX_DESCRIPTION_LENGTH = 1024;
32
+ export declare const MAX_SKILL_CONTENT_CHARS = 100000;
33
+ export declare const MAX_SKILL_FILE_BYTES = 1048576;
34
+ export declare const DEFAULT_REVIEW_MEMORY_INTERVAL = 10;
35
+ export declare const DEFAULT_REVIEW_SKILL_INTERVAL = 10;
36
+ /** Skill-review completion channel trigger mode: 'cadence' | 'completion' | 'both'. */
37
+ export declare const DEFAULT_SKILL_REVIEW_TRIGGER: "both";
38
+ /** Cumulative session tool calls before a session counts as "proven long" for the completion channel. */
39
+ export declare const DEFAULT_SKILL_REVIEW_COMPLETION_MIN_TOOL_CALLS = 20;
40
+ export declare const DEFAULT_SUBSTANTIVE_MIN_TOOL_CALLS = 3;
41
+ export declare const DEFAULT_SUBSTANTIVE_MIN_USER_CHARS = 200;
42
+ export declare const DEFAULT_SUBSTANTIVE_MIN_AGENT_CHARS = 500;
43
+ export declare const DEFAULT_MAX_OPS_PER_PLAN = 32;
44
+ export declare const DEFAULT_CURATOR_INTERVAL_HOURS = 168;
45
+ export declare const DEFAULT_MIN_IDLE_HOURS = 2;
46
+ export declare const DEFAULT_STALE_AFTER_DAYS = 30;
47
+ export declare const DEFAULT_ARCHIVE_AFTER_DAYS = 90;
48
+ export declare const DEFAULT_MEMORY_CHAR_LIMIT = 2200;
49
+ export declare const DEFAULT_USER_CHAR_LIMIT = 1375;
50
+ /** Consolidation-failure backoff cap, shared by MemoryStore and memory-files' Config default. */
51
+ export declare const DEFAULT_CONSOLIDATION_FAILURES = 3;
52
+ export declare const DEFAULT_SKILL_CONTENT_CHARS = 100000;
53
+ //# sourceMappingURL=constants.d.ts.map
@@ -1,8 +1,13 @@
1
1
  /**
2
2
  * Deterministic skill curator: active → stale → archived transitions.
3
- * Pure function; file moves are performed by SkillLibrary.
3
+ * Pure function with one deliberate side effect: records in the passed
4
+ * `usage` map are MUTATED (state/archived_at) to carry the transition — the
5
+ * caller owns the map and decides whether to clone first (dry-run) or persist
6
+ * after. File moves are performed by SkillLibrary.
4
7
  */
5
- import type { UsageMap } from './usage.ts';
8
+ import type { UsageMap, UsageRecord } from './usage.ts';
9
+ import { EvolutionGateSet } from './gates.ts';
10
+ export { PROTECTED_BUILTIN_SKILLS } from './constants.ts';
6
11
  export interface CuratorConfig {
7
12
  staleAfterDays: number;
8
13
  archiveAfterDays: number;
@@ -12,6 +17,14 @@ export interface CuratorConfig {
12
17
  excludeSkillNames?: ReadonlySet<string>;
13
18
  /** When true, usage records without created_by='agent' also enter the lifecycle. */
14
19
  manageUnmanaged?: boolean;
20
+ /** When true, bundled skills (in `bundledNames`) are curation candidates like agent-created ones. */
21
+ pruneBuiltins?: boolean;
22
+ /** Skill names carrying the bundled marker; only read when `pruneBuiltins` is true. */
23
+ bundledNames?: ReadonlySet<string>;
24
+ /** Skill names the curator archived once and must not fight across re-seeds. */
25
+ suppressedNames?: ReadonlySet<string>;
26
+ /** Skills referenced by scheduled/automated jobs: never auto-transitioned (idle clocks mislead for rarely-firing tasks). */
27
+ referencedSkillNames?: ReadonlySet<string>;
15
28
  }
16
29
  export interface CuratorTransition {
17
30
  name: string;
@@ -25,7 +38,6 @@ export interface CuratorResult {
25
38
  reactivate: string[];
26
39
  markStale: string[];
27
40
  }
28
- export declare const PROTECTED_BUILTIN_SKILLS: ReadonlySet<string>;
29
41
  export interface CuratorArchivedSkill {
30
42
  name: string;
31
43
  path: string;
@@ -36,6 +48,8 @@ export interface CuratorFailedSkill {
36
48
  reason: string;
37
49
  }
38
50
  export interface CuratorRunReport {
51
+ /** Report shape version; readers may ignore unknown fields on later versions. */
52
+ schemaVersion: 1;
39
53
  runId: string;
40
54
  startedAt: string;
41
55
  finishedAt: string;
@@ -44,7 +58,11 @@ export interface CuratorRunReport {
44
58
  archiveCandidates: string[];
45
59
  archived: CuratorArchivedSkill[];
46
60
  failed: CuratorFailedSkill[];
61
+ /** Consolidations actually executed this run (source absorbed into target). */
62
+ consolidated?: CuratorConsolidation[];
47
63
  snapshotPath?: string;
64
+ /** Whether the LLM nomination pass was enabled for this run (decision visibility). */
65
+ llmReviewEnabled?: boolean;
48
66
  }
49
67
  export interface CuratorReportInput {
50
68
  runId: string;
@@ -55,8 +73,57 @@ export interface CuratorReportInput {
55
73
  archiveCandidates: readonly string[];
56
74
  archived: readonly CuratorArchivedSkill[];
57
75
  failed: readonly CuratorFailedSkill[];
76
+ consolidated?: readonly CuratorConsolidation[];
58
77
  snapshotPath?: string;
78
+ llmReviewEnabled?: boolean;
59
79
  }
60
80
  export declare function buildCuratorRunReport(input: CuratorReportInput): CuratorRunReport;
61
- export declare function computeLifecycleTransitions(usage: UsageMap, config: CuratorConfig, now?: Date): CuratorResult;
81
+ /**
82
+ * Render a curator run report as a compact human-readable markdown digest
83
+ * (G6): run metadata first, then the notable sections (archived / failed /
84
+ * stale candidates / LLM nominations).
85
+ */
86
+ export declare function renderCuratorReportMarkdown(report: CuratorRunReport): string;
87
+ /** One LLM-nominated consolidation: `from` merges into the umbrella `into`. */
88
+ export interface CuratorConsolidation {
89
+ from: string;
90
+ into: string;
91
+ }
92
+ /** Structured result of the optional curator LLM nomination pass. */
93
+ export interface CuratorNominations {
94
+ prunings: string[];
95
+ consolidations: CuratorConsolidation[];
96
+ }
97
+ /**
98
+ * Parse the curator LLM's YAML nomination block (consolidations + prunings).
99
+ * Line-oriented and lenient by design: the LLM output is advisory, every name
100
+ * is re-validated against the tree before any file move happens downstream.
101
+ */
102
+ export declare function parseCuratorNominations(text: string): CuratorNominations;
103
+ /**
104
+ * The lifecycle-candidate gate, shared by the transition engine and the scope
105
+ * view so the two can never disagree: records failing ANY of these gates are
106
+ * outside the managed scope.
107
+ */
108
+ export declare function lifecycleCandidate(name: string, record: UsageRecord, config: CuratorConfig, bundled: boolean, gates?: EvolutionGateSet): boolean;
109
+ export interface ScopeView {
110
+ /** Skills inside the lifecycle scope right now (candidate gate + active state). */
111
+ managed: string[];
112
+ /** Managed skills already flagged stale or quality-warned — the ones to watch. */
113
+ watched: string[];
114
+ /** Managed skills flagged low quality (subset of `watched`) — consolidation candidates. */
115
+ qualityWarned: string[];
116
+ /** Explicitly exempted by excludeSkillNames / referencedSkillNames. */
117
+ exempted: string[];
118
+ /** Carrying a protection marker (pinned / bundled / hub-installed). */
119
+ protected: string[];
120
+ }
121
+ /**
122
+ * Read-only scope classification, derived from the SAME gate the transition
123
+ * engine uses (`lifecycleCandidate`), so the view always predicts what a
124
+ * curator pass may touch. `protectedNames` carries the marker info the usage
125
+ * records lack (bundled / hub-installed / pinned from `SkillLibrary.list()`).
126
+ */
127
+ export declare function computeScopeView(usage: UsageMap, config: CuratorConfig, protectedNames?: ReadonlyMap<string, string>, gates?: EvolutionGateSet): ScopeView;
128
+ export declare function computeLifecycleTransitions(usage: UsageMap, config: CuratorConfig, now?: Date, gates?: EvolutionGateSet): CuratorResult;
62
129
  //# sourceMappingURL=curator.d.ts.map
@@ -1,15 +1,28 @@
1
1
  /**
2
- * Durable session events emitted by the evolution family.
3
- * These are non-surface events: they never enter model history, but make
4
- * self-evolution activity replayable and observable by UI/projections.
2
+ * Process-local events emitted by the evolution family on the cordis event
3
+ * bus. Consumers subscribe with `ctx.on(...)`; producers dispatch with
4
+ * `ctx.emit(...)`.
5
+ *
6
+ * These are deliberately NOT session events: a persisted session log may only
7
+ * contain types from the host's generated `KNOWN_SESSION_EVENT_TYPES` set —
8
+ * the persistence read path refuses to interpret a log carrying any other
9
+ * type unless the envelope marks it `ignorable`, and `Session.append` offers
10
+ * no channel to write that marker. Appending any `evolution/*` type therefore
11
+ * made the whole session unresumable (A-line P0-1, fixed in rc.42 by moving
12
+ * these events off `session.append`). Plan-outcome durability lives in the
13
+ * evolution-activity store, not the session log.
5
14
  */
6
15
  export interface EvolutionReviewScheduledEvent {
16
+ /** Owning session (payload v2): process events carry no session envelope. */
17
+ sessionId: string;
7
18
  kind: 'memory' | 'skill' | 'combined';
8
19
  toolCalls: number;
9
20
  userChars: number;
10
21
  assistantChars: number;
11
22
  }
12
23
  export interface EvolutionPlanAppliedEvent {
24
+ /** Owning session (payload v2): process events carry no session envelope. */
25
+ sessionId: string;
13
26
  planId: string;
14
27
  /** Stable fingerprint of the policy snapshot that produced this plan. */
15
28
  policyFingerprint?: string | undefined;
@@ -25,15 +38,10 @@ export interface EvolutionSkillMutatedEvent {
25
38
  filePath?: string;
26
39
  archivedPath?: string;
27
40
  }
28
- declare module '@deepseek-ai/dsh-session/types' {
29
- interface SessionEventMap {
30
- 'evolution/review-scheduled': EvolutionReviewScheduledEvent;
31
- 'evolution/plan-applied': EvolutionPlanAppliedEvent;
32
- 'evolution/skill-mutated': EvolutionSkillMutatedEvent;
33
- }
34
- }
35
41
  declare module '@deepseek-ai/cordis' {
36
42
  interface Events {
43
+ 'evolution/review-scheduled'(event: EvolutionReviewScheduledEvent): void;
44
+ 'evolution/plan-applied'(event: EvolutionPlanAppliedEvent): void;
37
45
  'evolution/skill-mutated'(event: EvolutionSkillMutatedEvent): void;
38
46
  }
39
47
  }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Self-evolution event log (rc.68): an append-only sidecar under
3
+ * `$DSH_HOME/evolution/events.json` that is the single source of truth for
4
+ * the self-improvement loop. Feedback increments and learn actions share one
5
+ * ordered timeline (`seq` is the ordering key), so "feedback before/after a
6
+ * learn on target X" is answerable. The aggregate `feedback.json` is a
7
+ * rebuildable boot cache, never the truth.
8
+ *
9
+ * Rotation (rc.71, 007 design): when the active log reaches
10
+ * `EVENT_LOG_ROTATE_AT` the older half is split into an archive
11
+ * (`events-<lastArchivedSeq>.json`); the boot timeline merges active +
12
+ * archives and dedupes by seq (active copy wins), so the rotation crash window
13
+ * yields the identical timeline. Archival naming is STRICTLY numeric
14
+ * (`/^events-\d+\.json$/`) — user files under the same directory are never
15
+ * read as archives and never pruned (rc.72 G-2).
16
+ */
17
+ import { type EvolutionIoLike } from './io.ts';
18
+ export declare const EVENT_LOG_VERSION = 1;
19
+ /** Active-log split point (rc.71): when the active log reaches this many events
20
+ * the older half is rotated into an archive; the active stays bounded so a
21
+ * single append stays O(active) instead of O(total-history). Tunable default —
22
+ * callers may override per append (the tests use small values). */
23
+ export declare const EVENT_LOG_ROTATE_AT = 4000;
24
+ /** Number of archives retained (rc.71): older archives are pruned at rotation,
25
+ * mirroring retainReports. The horizon covers the loop-analysis window. */
26
+ export declare const EVENT_LOG_RETAIN_ARCHIVES = 10;
27
+ /** Archive file prefix: `events-<lastArchivedSeq>.json`. The active file is
28
+ * `events.json` and never matches this glob. */
29
+ export declare const EVENT_ARCHIVE_PREFIX = "events-";
30
+ export interface EvolutionEvent {
31
+ /** Global monotonic order key, assigned inside the append transact. */
32
+ seq: number;
33
+ /** ISO timestamp at append time. */
34
+ at: string;
35
+ /** Tagged-union discriminator: feedback increments vs learn actions. */
36
+ type: 'feedback' | 'learn';
37
+ target?: string | undefined;
38
+ kind?: 'skill' | 'session' | undefined;
39
+ rating?: 'positive' | 'negative' | undefined;
40
+ note?: string | undefined;
41
+ source?: string | undefined;
42
+ request?: string | undefined;
43
+ }
44
+ export declare function eventsFile(home: string): string;
45
+ /**
46
+ * Parse an event log body. A missing file, a whitespace-only file (rc.69:
47
+ * rebuildable, NOT malformed) or a corrupt one reads as empty; corrupt content
48
+ * is still refused on append, never overwritten.
49
+ *
50
+ * Per-entry normalization (rc.70 F-1): entries without a numeric `seq` are
51
+ * skipped here and dropped at the next append — valid entries survive, the
52
+ * damaged record is the only loss (self-heal semantics, matching the usage
53
+ * sidecar's per-field normalization on read).
54
+ */
55
+ export declare function parseEvolutionEvents(raw: string | null): EvolutionEvent[];
56
+ /**
57
+ * List the numeric archives under the log's directory, sorted ascending by
58
+ * their last-archived seq. Single glob predicate for the timeline, the
59
+ * retention pass and the feedback migration check (rc.72 H-3).
60
+ */
61
+ export declare function listEventArchives(io: EvolutionIoLike, path: string): Promise<string[]>;
62
+ /**
63
+ * Append one event under the write lock (rc.68): `seq` = current max + 1
64
+ * computed inside the transact, so two processes appending concurrently never
65
+ * collide. A malformed log is refused (bytes preserved) and the append fails.
66
+ * Returns the assigned seq.
67
+ *
68
+ * Rotation (rc.71, 007 design): when the active log reaches `rotateAt`, the
69
+ * older half is copied into an archive inside the SAME transact (the archive
70
+ * path has its own lock, so no recursion) and the active is replaced with the
71
+ * newer half + the new event. seqs stay globally monotonic; a crash between
72
+ * archive write and active write leaves both copies, which the timeline merge
73
+ * dedupes by seq. An archive-write failure aborts the append (active keeps the
74
+ * full old content — no loss) and the caller's best-effort handling applies.
75
+ *
76
+ * rc.72 G-1: when the ACTIVE is missing/whitespace but archives exist (a
77
+ * deleted active, or B-2 self-heal), seq derivation consults the archive names
78
+ * — the active restarts AFTER the highest archived seq, never at 1, so a new
79
+ * event can never shadow an archived one in the seq-deduped timeline.
80
+ */
81
+ export declare function appendEvolutionEvent(io: EvolutionIoLike, path: string, event: Omit<EvolutionEvent, 'seq' | 'at'>, rotateAt?: number): Promise<number>;
82
+ /**
83
+ * Prune old event archives (rc.71): keep the newest `EVENT_LOG_RETAIN_ARCHIVES`.
84
+ * The name's numeric part is the last archived seq, so ordering is NUMERIC —
85
+ * lexicographic would rank `events-10` before `events-2`. Only strictly
86
+ * numeric names participate (rc.72 G-2: user files are never deleted).
87
+ * Best-effort per removal; exported for the retention test.
88
+ */
89
+ export declare function retainEventArchives(io: EvolutionIoLike, path: string): Promise<void>;
90
+ export interface EventLogRead {
91
+ events: EvolutionEvent[];
92
+ /** True when the body is not valid JSON (syntax-level damage): refused on
93
+ * append, bytes untouched. Well-formed JSON with a damaged `events` field
94
+ * is REPLACEABLE garbage — reads as empty and is rewritten at the next
95
+ * append (rc.70 F-1: read and append agree on the same boundary). A READ
96
+ * error (EISDIR etc.) also flags malformed — the file is unusable either
97
+ * way and is never overwritten (the append read would fail identically). */
98
+ malformed: boolean;
99
+ }
100
+ /** Read the event log; a missing/whitespace-only file reads as empty,
101
+ * corrupt content is flagged (and refused on append). */
102
+ export declare function readEvolutionEvents(io: EvolutionIoLike, path: string): Promise<EventLogRead>;
103
+ /**
104
+ * Read the full timeline (rc.71): active log + all archives, merged by seq
105
+ * (active copy wins, duplicates only arise from the rotation crash window),
106
+ * sorted ascending. Per-file malformed flag as in `readEvolutionEvents`; a
107
+ * malformed (or unreadable) ARCHIVE is skipped — it never bricks the boot and
108
+ * it is still flagged.
109
+ */
110
+ export declare function readEvolutionTimeline(io: EvolutionIoLike, path: string): Promise<EventLogRead>;
111
+ //# sourceMappingURL=evolution-events.d.ts.map
@@ -0,0 +1,38 @@
1
+ /**
2
+ * The control-plane protection sets, held once and queried everywhere
3
+ * (decision B, rc.44 plan M2): the lifecycle engine, the scope view, the LLM
4
+ * nomination gate and the control-plane consolidate all answer "is this name
5
+ * off limits — and why" from the same instance, so the gate sets can never
6
+ * drift apart the way the three pre-rc.46 implementations did.
7
+ *
8
+ * Scope boundary: a GateSet covers NAME-SET protections only. Marker-based
9
+ * protections (pinned / bundled / hub-installed) are file markers resolved by
10
+ * `SkillLibrary.writeProtection` / `deleteProtection` — they depend on the
11
+ * filesystem and the write origin, not on a name list.
12
+ * @module @deepseek-ai/dsh-evolution-core
13
+ */
14
+ export type GateReason = 'excluded' | 'referenced' | 'suppressed' | 'protected-builtin';
15
+ export interface GateSetInputs {
16
+ exclude?: ReadonlySet<string> | undefined;
17
+ referenced?: ReadonlySet<string> | undefined;
18
+ suppressed?: ReadonlySet<string> | undefined;
19
+ }
20
+ export declare class EvolutionGateSet {
21
+ readonly exclude: ReadonlySet<string>;
22
+ readonly referenced: ReadonlySet<string>;
23
+ readonly suppressed: ReadonlySet<string>;
24
+ constructor(inputs?: GateSetInputs);
25
+ /**
26
+ * The first protection blocking this name, or null. Any hit blocks — the
27
+ * order is diagnostic only, so a name in two sets reports the first.
28
+ */
29
+ blockReason(name: string): GateReason | null;
30
+ isBlocked(name: string): boolean;
31
+ }
32
+ /** Build a GateSet from the curator-style config field names. */
33
+ export declare function createGateSet(config: {
34
+ excludeSkillNames?: ReadonlySet<string>;
35
+ referencedSkillNames?: ReadonlySet<string>;
36
+ suppressedNames?: ReadonlySet<string>;
37
+ }): EvolutionGateSet;
38
+ //# sourceMappingURL=gates.d.ts.map
@@ -8,13 +8,19 @@
8
8
  * @module @deepseek-ai/dsh-evolution-core
9
9
  */
10
10
  export * from './curator.ts';
11
+ export * from './evolution-events.ts';
12
+ export * from './gates.ts';
11
13
  export * from './events.ts';
12
14
  export * from './io.ts';
15
+ export * from './learn-prompt.ts';
13
16
  export * from './memory-store.ts';
17
+ export * from './mutations.ts';
14
18
  export * from './prompts.ts';
19
+ export * from './quality.ts';
15
20
  export * from './signals.ts';
16
21
  export * from './skill-store.ts';
17
22
  export * from './state-store.ts';
18
23
  export * from './threats.ts';
19
24
  export * from './usage.ts';
25
+ export * from './constants.ts';
20
26
  //# sourceMappingURL=index.d.ts.map
package/lib/types/io.d.ts CHANGED
@@ -1,9 +1,8 @@
1
1
  /**
2
- * Structural IO seam for the legacy facade stores.
2
+ * Structural IO seam for the evolution plugin family.
3
3
  *
4
- * The facade accepts any object exposing this small async file-tree surface.
5
- * Native DSH packages pass `ctx.evolutionIo.provider()`; standalone consumers
6
- * (and the facade's own tests) can use `nodeEvolutionIo`.
4
+ * Every evolution package passes `ctx.evolutionIo.provider()`; standalone
5
+ * consumers (and the core's own tests) can use `nodeEvolutionIo`.
7
6
  */
8
7
  export interface EvolutionIoLike {
9
8
  readText(path: string): Promise<string | null>;
@@ -13,7 +12,35 @@ export interface EvolutionIoLike {
13
12
  exists(path: string): Promise<boolean>;
14
13
  rename(path: string, destination: string): Promise<void>;
15
14
  copy(path: string, destination: string): Promise<void>;
15
+ /**
16
+ * Optional byte-size probe for the read guard. Return the file's size in
17
+ * bytes, or `null`/`undefined` when unknown (unsupported backend, missing
18
+ * file, stat failure). An implementation without this probe gets no guard:
19
+ * consumers treat an unknown size as "guard not applicable".
20
+ */
21
+ size?(path: string): Promise<number | null>;
22
+ /**
23
+ * Optional atomic read-modify-write on one file: the read and the write run
24
+ * inside a single cross-process lock, so two processes that share DSH_HOME
25
+ * cannot interleave their RMW sequences. `task` receives the current content
26
+ * (`null` when missing) and returns the next content; returning `null`
27
+ * deletes the file. A backend without it falls back to plain read+write and
28
+ * the caller keeps its single-process chain as the second layer.
29
+ */
30
+ transact?(this: void, path: string, task: (current: string | null) => Promise<string | null>): Promise<void>;
31
+ /**
32
+ * Optional symlink probe (G7). `true` = the path is a symlink, `false` = a
33
+ * real entry, `null` = guard not applicable (backend without the probe or
34
+ * the path does not exist). Consumers treat `null` as "let it through".
35
+ */
36
+ isSymlink?(this: void, path: string): Promise<boolean | null>;
16
37
  }
38
+ /**
39
+ * Run `task` inside `io.transact` when the backend provides it; otherwise fall
40
+ * back to a plain read → task → write/remove sequence (no cross-process lock —
41
+ * callers keep their single-process serialize chain as the second layer).
42
+ */
43
+ export declare function transactIo(io: EvolutionIoLike, path: string, task: (current: string | null) => Promise<string | null>): Promise<void>;
17
44
  /** Lazy adapter over an IO provider registry, shared by every evolution consumer. */
18
45
  export declare function evolutionIoAdapter(provider: () => EvolutionIoLike): EvolutionIoLike;
19
46
  export declare function nodeEvolutionIo(): EvolutionIoLike;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Open-ended `/evolution learn` prompt builder.
3
+ *
4
+ * `learn` is open-ended: the user can name anything they can describe — a
5
+ * directory of code, an API doc URL, a workflow they just walked the agent
6
+ * through, or pasted notes. The prompt instructs the live agent to gather the
7
+ * named sources with its existing tools, then author a single SKILL.md via
8
+ * `skill_manage` following `DSH_AUTHORING_STANDARDS`. There is no separate
9
+ * distillation engine and no model-tool footprint.
10
+ */
11
+ /**
12
+ * Build the agent prompt for an open-ended `/evolution learn` request.
13
+ *
14
+ * @param userRequest free-text the user gave after `/evolution learn`; an
15
+ * empty string falls back to "the workflow we just went through".
16
+ * @returns a complete instruction the agent runs as a normal turn.
17
+ */
18
+ export declare function buildLearnPrompt(userRequest: string): string;
19
+ //# sourceMappingURL=learn-prompt.d.ts.map
@@ -3,7 +3,7 @@
3
3
  * Stores are MEMORY.md and USER.md under $DSH_HOME/memories (~/.dsh/memories).
4
4
  */
5
5
  import { type EvolutionIoLike } from './io.ts';
6
- export declare const ENTRY_DELIMITER = "\n\u00A7\n";
6
+ export { ENTRY_DELIMITER } from './constants.ts';
7
7
  export type MemoryTarget = 'memory' | 'user';
8
8
  export interface MemoryOperation {
9
9
  action: 'add' | 'replace' | 'remove';
@@ -35,26 +35,69 @@ export declare class MemoryStore {
35
35
  private readonly maxFailures;
36
36
  private readonly io;
37
37
  private failureCount;
38
+ private lastFailureAt;
38
39
  constructor(options?: MemoryStoreOptions);
39
40
  limitFor(target: MemoryTarget): number;
41
+ /**
42
+ * Read-guard probe: `{ size, limit }` when the on-disk file exceeds
43
+ * `limit * READ_GUARD_FACTOR` bytes, `null` when it is absent, unknown
44
+ * (backend without a size probe), under the bound, or the target has no
45
+ * limit configured.
46
+ */
47
+ private oversizedFile;
40
48
  read(target: MemoryTarget): Promise<string[]>;
41
49
  write(target: MemoryTarget, entries: string[]): Promise<void>;
42
50
  resetFailures(): void;
43
51
  private failure;
52
+ /**
53
+ * StorageHint percentage must clamp at 100 like the render header: a drifted
54
+ * entry can push chars past the limit, and "Storage at 125%" contradicts the
55
+ * clamped usage indicator.
56
+ */
57
+ private storageHint;
58
+ /**
59
+ * Best-effort raw-copy backup of the on-disk file to `<file>.bak.<stamp>`
60
+ * before a refusal, so an externally modified (or oversized) file stays
61
+ * recoverable. Copies bytes instead of reading them so a pathologically
62
+ * large file is never loaded just to back it up. Failure to back up does
63
+ * not change the refusal semantics.
64
+ */
65
+ private backupFile;
66
+ /**
67
+ * Read-guard refusal for write paths. Returns the refusal result when the
68
+ * target file is oversized, `null` otherwise. The file is skipped for
69
+ * reading (never loaded), backed up by raw copy, and the model is told to
70
+ * fix it manually — mirroring the drift refusal so corrupted state is never
71
+ * silently overwritten.
72
+ */
73
+ private oversizedRefusal;
44
74
  add(target: MemoryTarget, facts: string): Promise<MemoryApplyResult>;
45
- replace(target: MemoryTarget, oldText: string, facts: string): Promise<MemoryApplyResult>;
46
- remove(target: MemoryTarget, oldText: string): Promise<MemoryApplyResult>;
47
- private mutate;
75
+ /**
76
+ * Single-entry add inside the transaction: shared checks (oversized,
77
+ * drift, threat) and the content computation. `raw` is the locked view
78
+ * (`current`) — never a second IO read. `write: null` means "no change".
79
+ */
80
+ private addCore;
81
+ /** Canonical-form drift check derived from the locked view (same formula as `detectDrift`, no second read). */
82
+ private driftFromRaw;
48
83
  applyBatch(target: MemoryTarget, operations: MemoryOperation[]): Promise<MemoryApplyResult>;
84
+ /** Batch RMW inside the transaction. `write: null` = failure/no-op, disk untouched. */
85
+ private applyBatchCore;
49
86
  renderContext(): Promise<string>;
50
- snapshot(): Promise<{
51
- memory: string[];
52
- user: string[];
53
- }>;
54
- restoreSnapshot(snapshot: {
55
- memory: string[];
56
- user: string[];
57
- }): Promise<void>;
87
+ /**
88
+ * Detect on-disk drift: true when the file is not in the canonical
89
+ * `render(normalizeEntries(raw))` form. This catches structural anomalies
90
+ * the writer would quietly normalize away (empty/`§`-only entries, stray
91
+ * blank lines, leading/trailing delimiters) that indicate the file was
92
+ * edited outside MemoryStore. Purely single-canonical content reaches the
93
+ * same serialization and returns false, so a normal write is never flagged.
94
+ *
95
+ * An absent, empty, or whitespace-only file is the "never written" state
96
+ * (rc.42 audit P1-6): it parses to zero entries, so the canonical form
97
+ * `'\n'` can never byte-match it and every write path was permanently
98
+ * refused with "External drift detected" — including the repairs the model
99
+ * would need to make. Such files are adopted instead of flagged.
100
+ */
58
101
  detectDrift(target: MemoryTarget): Promise<boolean>;
59
102
  }
60
103
  //# sourceMappingURL=memory-store.d.ts.map
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Curator/author audit trail: `.mutations.json` records every skill mutation
3
+ * with before/after content hashes so any automated edit is reviewable and
4
+ * replayable. Best-effort persistence, mirroring the usage sidecar posture.
5
+ * @module @deepseek-ai/dsh-evolution-core
6
+ */
7
+ import { type EvolutionIoLike } from './io.ts';
8
+ export interface MutationRecord {
9
+ skillName: string;
10
+ action: string;
11
+ beforeHash?: string;
12
+ afterHash?: string;
13
+ summary: string;
14
+ at: string;
15
+ }
16
+ export declare const DEFAULT_MUTATION_CAP = 500;
17
+ /** Version of the `.mutations.json` file shape; writers always emit the current one. */
18
+ export declare const MUTATIONS_FILE_VERSION = 1;
19
+ export declare function mutationsFile(root: string): string;
20
+ export declare function contentHash(content: string): string;
21
+ export declare function loadMutations(root: string, io?: EvolutionIoLike): Promise<MutationRecord[]>;
22
+ /** Append one record, trim to `cap`, and write atomically (versioned shape). */
23
+ export declare function recordMutation(root: string, io: EvolutionIoLike, record: MutationRecord, cap?: number): Promise<void>;
24
+ //# sourceMappingURL=mutations.d.ts.map