@lmzhen/dsh-evolution-core 0.8.0 → 0.10.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.
@@ -21,7 +21,7 @@
21
21
  */
22
22
  /**
23
23
  * Required argument names per `skill_manage` action — the SINGLE SOURCE read
24
- * by the tool's argument gate (tool-skill-manage executeCore) and the plan
24
+ * by the tool's argument gate (tool-skill-manage write-gates.ts) and the plan
25
25
  * validator (evolution-plan-validator), so the two can never drift.
26
26
  * OPT-05 (2026-09): the plan validator used to accept a `write_file`/
27
27
  * `remove_file` op without `file_path` while the executor required it — the
@@ -52,6 +52,10 @@ export declare const PROTECTED_BUILTIN_SKILLS: ReadonlySet<string>;
52
52
  export declare const MAX_SKILL_NAME_LENGTH = 64;
53
53
  export declare const MAX_DESCRIPTION_LENGTH = 1024;
54
54
  export declare const MAX_SKILL_CONTENT_CHARS = 100000;
55
+ /** How many content versions to retain per skill (skill-history.ts). 20 mirrors the count
56
+ * KiroCrew settled on for the same job (its `MAX_SKILL_VERSIONS`), and is an E2 deployment
57
+ * parameter — the value is a retention policy, not a protocol constant. */
58
+ export declare const DEFAULT_SKILL_VERSION_KEEP = 20;
55
59
  /** The authoring DISCIPLINE band, taken from the upstream standard (archive §5):
56
60
  * peer skills sit at 8-14k characters and a body pushing past 20k belongs in
57
61
  * `references/*.md`. Deliberately separate from `MAX_SKILL_CONTENT_CHARS`: the
@@ -128,6 +132,16 @@ export declare const EVOLUTION_WRITE_TOOLS: readonly ["memory", "skill_manage"];
128
132
  * 0.3.16 (T-4): moved here from skill-store.ts so drift-signals (pure, no IO)
129
133
  * can reference it without importing the skill-store module. */
130
134
  export declare const AUTHORING_DESCRIPTION_BAR = 60;
135
+ /** The PLATFORM's default cap on the description a skill catalog shows —
136
+ * `catalogDescriptionMaxLength` on the platform's `tool-skill` row (default 500).
137
+ *
138
+ * Recorded here because it is the third number in the description-length relationship, and the one
139
+ * that is easiest to misread as a family rule: the family's own host/all rows SET that field to
140
+ * `AUTHORING_DESCRIPTION_BAR` (60), so what a family deployment truncates at is the ROW's value —
141
+ * this default only governs compositions that do not override it. It is NOT a refusal threshold:
142
+ * the only refusal is `MAX_DESCRIPTION_LENGTH` / the row's `maxDescriptionLength`, and the catalog
143
+ * cut is a view, not a validation. See tool-skill-manage/README.md for the three-number table. */
144
+ export declare const PLATFORM_CATALOG_DESCRIPTION_DEFAULT = 500;
131
145
  /** The split hint both size refusals share (0.5.0 V1). The same sentence used to
132
146
  * be copied into validateFrontmatter AND the patch path, and neither copy named
133
147
  * where the content should go — the upstream cap message names the destination
@@ -1,5 +1,6 @@
1
1
  /**
2
- * THE error-code table: E-301…E-316 with the exact text each one emits.
2
+ * THE error-code table: E-301…E-318 with the exact text each one emits (E-317 and E-318 are the
3
+ * write-admission gates' own refusals).
3
4
  *
4
5
  * The codes used to be spelled out at 33 call sites across two packages, so one
5
6
  * code's wording could drift between the branches that answer it and nothing could
@@ -33,6 +34,8 @@ export declare const EVOLUTION_ERRORS: Readonly<{
33
34
  readonly 'e-309-revision-conflict-message-Re': "E-309: revision conflict — %a1%. Re-read with /evolution params --json and retry.";
34
35
  readonly 'e-310-the-settings-service-refused': "E-310: the settings service refused the write: %a1% — the owning plugin's rule stands (a cross-field pair, or a cap that may only be tightened). /evolution params shows the current value.";
35
36
  readonly 'e-305-this-invocation-carries-no-2': "E-305: this invocation carries no agent — `%a1%` needs a session-backed call (run it from a session in the GUI or the CLI).";
37
+ 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.";
38
+ 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.";
36
39
  }>;
37
40
  /** One table key. */
38
41
  export type EvolutionErrorId = keyof typeof EVOLUTION_ERRORS;
@@ -57,6 +57,13 @@ export interface EvolutionPlanAppliedEvent {
57
57
  skippedUnread?: number | undefined;
58
58
  /** First execution-layer failure message (abort reason or op failure). */
59
59
  executionError?: string | undefined;
60
+ /** Batch D (2026-09-27): ops whose ENTIRE evidence list cites bookkeeping frames
61
+ * (a turn/step boundary) — the report-only phase of the plan path's
62
+ * `EVIDENCE_CLASS` rule. Not a refusal and not a "not done" cause: the ops in
63
+ * this count executed. It rides the same payload as the other dimensions so the
64
+ * observation window has a durable record; the model's notice says nothing about
65
+ * it. */
66
+ evidenceClassReports?: number | undefined;
60
67
  evidenceQuotes?: number | undefined;
61
68
  estimatedInputChars?: number | undefined;
62
69
  }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Which session frames are EVIDENCE, and which are bookkeeping.
3
+ *
4
+ * A plan op must cite `evidence` seqs, and the validator's range rule
5
+ * (`evolution-plan-validator`, rule `EVIDENCE_RANGE`) answers only "is this an integer inside
6
+ * the window the plan was authored against". A boundary frame satisfies that while carrying
7
+ * nothing a claim can rest on: the `turn/start` seq is the frame a claim about that turn was
8
+ * most likely read out of, and citing it is indistinguishable from citing a real exchange. This
9
+ * module owns the ONE classification of a session frame by what it CARRIES, so the plan path can
10
+ * report an op whose whole citation list is bookkeeping (design
11
+ * `dsh-evolution-skill-history-design.md` §3D, phase 1: report only — no op is refused here).
12
+ *
13
+ * The classification is a DENY list of the pure bookends, not an allow list of content frames.
14
+ * The platform's own vocabulary (`KNOWN_SESSION_EVENT_TYPES`, 57 members at 0.1.5-rc.2) mixes
15
+ * content frames with state-change frames, and out-of-repo plugins append types the family cannot
16
+ * enumerate; an unknown frame is therefore treated as carrying content, because a report that
17
+ * fires on an unclassifiable frame is a false positive and phase 1 exists to measure how many TRUE
18
+ * ones there are. Tightening — a narrower definition, or an allow list built from the platform's
19
+ * vocabulary — changes this table and nothing else (`evidenceKindIndex` is the only reader).
20
+ * @module @lmzhen/dsh-evolution-core
21
+ */
22
+ /** One session frame, as far as the classification reads it. A platform `SessionEvent` is
23
+ * assignable: both fields are optional here so a stub log can be classified without pretending
24
+ * to be a full event. */
25
+ export interface EvidenceFrame {
26
+ /** The platform event type. An unknown value counts as content (see the module docblock). */
27
+ type?: unknown;
28
+ /** The durable sequence number a plan op cites. */
29
+ seq?: unknown;
30
+ }
31
+ /**
32
+ * The seqs of the frames that carry content.
33
+ *
34
+ * @param events - the session's frames, oldest first (any iterable; nothing is retained).
35
+ * @returns the substantive seqs, or `undefined` when NO frame carried a usable seq. The two
36
+ * answers are different facts: an empty SET says "every sequenced frame is a boundary", while
37
+ * `undefined` says "this log cannot tell a boundary from a message" (a stub session in a test,
38
+ * a composition whose events carry no seq). A caller that cannot classify must stay silent
39
+ * rather than report every op — the same three-state posture as `sessionReadSkillNames`.
40
+ */
41
+ export declare function evidenceKindIndex(events: Iterable<EvidenceFrame>): ReadonlySet<number> | undefined;
42
+ //# sourceMappingURL=evidence.d.ts.map
@@ -20,9 +20,11 @@
20
20
  * evolution-policy/threat, both stores, review, maintenance.
21
21
  * - **Core domain stores/logic** — `skill-store.ts` (skill tree engine +
22
22
  * IO-seam consumer), `memory-store.ts`, `usage.ts`, `curator.ts`,
23
- * `quality.ts`, `signals.ts`, `drift-signals.ts`, `skill-health.ts`,
23
+ * `quality.ts`, `signals.ts`, `skill-reads.ts` (the read-before-write rule, shared by the
24
+ * tool path and the review's plan path), `drift-signals.ts`, `skill-health.ts`,
24
25
  * `preset-composition.ts`, `prompts.ts`, `learn-prompt.ts`,
25
- * `evolution-events.ts`, `io.ts` (the ctx.evolutionIo seam itself).
26
+ * `evolution-events.ts`, `evidence.ts` (which session frames count as content, for the plan
27
+ * path's evidence-class report), `io.ts` (the ctx.evolutionIo seam itself).
26
28
  * @module @lmzhen/dsh-evolution-core
27
29
  */
28
30
  export * from './citations.ts';
@@ -50,7 +52,10 @@ export * from './write-inventory.ts';
50
52
  export * from './scope.ts';
51
53
  export * from './skill-health.ts';
52
54
  export * from './signals.ts';
55
+ export * from './skill-reads.ts';
56
+ export * from './evidence.ts';
53
57
  export * from './drift-signals.ts';
58
+ export * from './skill-history.ts';
54
59
  export * from './skill-store.ts';
55
60
  export * from './state-store.ts';
56
61
  export * from './threats.ts';
@@ -78,6 +78,9 @@ export interface SkillLimits {
78
78
  archiveRetention?: ArchiveRetentionPolicy | undefined;
79
79
  /** See SupportFileCharPolicy; absent means the default (`report`). */
80
80
  supportFileCharPolicy?: SupportFileCharPolicy | undefined;
81
+ /** Content versions retained per skill (skill-history.ts); absent means
82
+ * `DEFAULT_SKILL_VERSION_KEEP`. */
83
+ versionKeep?: number | undefined;
81
84
  }
82
85
  export declare const DEFAULT_SKILL_LIMITS: SkillLimits;
83
86
  //# sourceMappingURL=limits.d.ts.map
@@ -10,6 +10,11 @@ export interface MutationRecord {
10
10
  action: string;
11
11
  beforeHash?: string;
12
12
  afterHash?: string;
13
+ /** The skill-history version the content on each side of this mutation belongs to
14
+ * (skill-history.ts). Optional: records written before content history existed carry none, and
15
+ * the hashes above stay the authority for "what changed". */
16
+ beforeVersion?: number;
17
+ afterVersion?: number;
13
18
  summary: string;
14
19
  at: string;
15
20
  }
@@ -43,6 +43,12 @@ export declare function canonicalWriteId(id: string): string;
43
43
  export declare function readParam(carrier: object | undefined, id: string): unknown;
44
44
  /** Exposure group (design §7.2): the unit a developer changes together. */
45
45
  export type ParamGroup = 'library' | 'write-caps' | 'review' | 'memory' | 'curator' | 'deployment' | 'internal';
46
+ /**
47
+ * Chinese display name per group, one entry per line (the scripts read this block
48
+ * by text, the same way they read PARAM_NAMESPACES). The raw group id is what
49
+ * `--group` accepts and what the JSON face reports; this only names it on screen.
50
+ */
51
+ export declare const PARAM_GROUP_LABELS: Readonly<Record<ParamGroup, string>>;
46
52
  /** Exposure tier (design §7.1): the interface combination a parameter gets. */
47
53
  export type ParamTier = 'E0' | 'E1' | 'E2' | 'E3' | 'E4';
48
54
  /** Where the authoritative value lives. */
@@ -61,14 +67,17 @@ export type ParamAuthority = 'code' | 'cordis' | 'install';
61
67
  * writable through the user layer (a cordis.yml change still follows the
62
68
  * deployment's patch-reload policy).
63
69
  *
64
- * UI METADATA (0.7.0): every E3 row carries the optional tail
70
+ * UI METADATA (0.7.0): every E3 row carries the tail
65
71
  * `label, hint, control, unit, values` — in that order, directly after
66
- * `summary`, still one entry per line. E3 is the only tier the settings cards
67
- * render, so the card's Chinese name, help text, control kind, unit suffix and
68
- * value domain come from THIS registry and nowhere else; the browser half is
69
- * generated from this text (`gen-param-client-view.mjs`). Non-E3 rows must NOT
70
- * carry the tail (there is no UI surface for them), and `control: 'select'`
71
- * requires a non-empty pipe-joined `values`.
72
+ * `summary`, still one entry per line — plus an OPTIONAL trailing
73
+ * `valueLabels` (0.9.0). E3 is the only tier the settings cards render, so the
74
+ * card's Chinese name, help text, control kind, unit suffix and value domain
75
+ * come from THIS registry and nowhere else; the browser half is generated from
76
+ * this text (`gen-param-client-view.mjs`). Non-E3 rows must NOT carry the tail
77
+ * (there is no UI surface for them); `control: 'select'` requires a non-empty
78
+ * pipe-joined `values`, and `valueLabels` — allowed on select rows only — names
79
+ * those values for display, one label per value, in the same order. The stored
80
+ * value is never translated: only what the control shows.
72
81
  */
73
82
  export interface ParamExposure {
74
83
  id: string;
@@ -89,6 +98,11 @@ export interface ParamExposure {
89
98
  unit?: string;
90
99
  /** Pipe-joined allowed values; non-empty exactly when control is 'select'. */
91
100
  values?: string;
101
+ /**
102
+ * Pipe-joined display names for `values`, same order and count (optional).
103
+ * The stored value stays the raw one; this only names it on the card.
104
+ */
105
+ valueLabels?: string;
92
106
  }
93
107
  /**
94
108
  * The parameter registry. G1/S1.1 seeds it with the review group (G-C); the
@@ -7,6 +7,12 @@
7
7
  * mutation maturity is a documented DSH approximation (single per-month patch
8
8
  * trend ratio replaces the claw timestamp-trend formula, since DSH usage
9
9
  * records only carry the last patched timestamp).
10
+ *
11
+ * Batch C (2026-09-27): the similarity half of this module is now TWO NAMED
12
+ * QUESTIONS plus a declared projection — {@link identity} ("is it the same
13
+ * text?") and {@link affinity} ("how much vocabulary do these two share?") asked
14
+ * over {@link projectionText}. Callers name the question and the projection
15
+ * instead of growing a fourth unnamed ruler (design §3C / §1 I2).
10
16
  * @module @lmzhen/dsh-evolution-core
11
17
  */
12
18
  import type { UsageMap } from './usage.ts';
@@ -45,6 +51,52 @@ export declare function computeQualityScores(input: {
45
51
  supportDirs?: ReadonlyMap<string, number>;
46
52
  now?: Date;
47
53
  }): Map<string, QualityScore>;
54
+ /** Named similarity question 1 — "is it the same text?" — as a normalized
55
+ * content hash: case- and whitespace-insensitive, so two texts that differ only
56
+ * in formatting are the SAME text here. Invariant under which projection the
57
+ * caller read the text from.
58
+ *
59
+ * C-26 (v10 audit): NOT named `contentHash` — mutations.ts exports a
60
+ * contentHash of RAW bytes, this one hashes the NORMALIZED text for dedup
61
+ * grouping. Same helper as that rename, now a named question. */
62
+ export declare function identity(text: string): string;
63
+ /** Which text a similarity question is asked about. Declared by the caller. */
64
+ export type SimilarityProjection = 'body' | 'summary';
65
+ /** The one input shape both projections read from: a caller passes what it has. */
66
+ export interface SimilarityInput {
67
+ /** The whole SKILL.md — the `body` projection. */
68
+ content?: string;
69
+ /** The skill name — half of the `summary` projection. */
70
+ name?: string;
71
+ /** The frontmatter description — half of the `summary` projection. */
72
+ description?: string;
73
+ }
74
+ /**
75
+ * The declared projection: WHICH text a comparison weighs.
76
+ *
77
+ * `body` is the library-wide scan's projection (whole SKILL.md, read by the
78
+ * caller). `summary` is name + description in one string — the projection a
79
+ * listing already publishes (`SkillSummary` carries both fields, and a body is
80
+ * attached only when the caller asks for it) — so a summary comparison never
81
+ * reads a body OF ITS OWN: the create-time hint is fed by the listing pass the
82
+ * tool already pays for its frontmatter, and calls no read of its own.
83
+ */
84
+ export declare function projectionText(projection: SimilarityProjection, input: SimilarityInput): string;
85
+ /** The comparable vocabulary of one text: lowercase alphanumeric runs, plus
86
+ * whole CJK runs (a Chinese description tokenizes by punctuation, not by word —
87
+ * deliberately: the ruler is one home, and a real tokenizer is a different
88
+ * question than "do these two share vocabulary?"). */
89
+ export declare function vocabulary(text: string): ReadonlySet<string>;
90
+ /** Named similarity question 2 — "how much vocabulary do these two share?" —
91
+ * Jaccard over two {@link vocabulary} sets, 0 (nothing in common, or either side
92
+ * empty) to 1 (identical vocabulary). Set-based on purpose: the bulk scan
93
+ * memoizes each name's set once and only the comparisons are budgeted. */
94
+ export declare function affinity(a: ReadonlySet<string>, b: ReadonlySet<string>): number;
95
+ /** The `affinity` level at which two bodies are treated as the same skill
96
+ * (design §3C: the default is unchanged at 0.95 — it is now a NAME, because the
97
+ * docblock below has linked it since PLAN-R2 while the code carried a bare
98
+ * literal). Callers may still pass their own level. */
99
+ export declare const DEDUP_SIMILARITY_THRESHOLD = 0.95;
48
100
  /** PLAN-R2 P2-8 (2026-09-16): default cap on two-name comparisons in
49
101
  * {@link computeDedupGroups}. A 2000-skill library is ~2M pairs; the old
50
102
  * unbounded two-two Jaccard ran seconds to tens of seconds per
@@ -79,6 +131,45 @@ export declare function computeDedupGroups(input: {
79
131
  * {@link DEDUP_MAX_PAIR_COMPARISONS}; a smaller value truncates earlier. */
80
132
  maxPairComparisons?: number;
81
133
  }): DedupScanResult;
134
+ /**
135
+ * The create-time near-duplicate question (batch C, design §3C-2): WHICH
136
+ * existing skills is this authoring candidate about to near-copy?
137
+ *
138
+ * It is the SAME {@link affinity} question as the library-wide scan, asked over
139
+ * the `summary` projection — the one a listing already publishes
140
+ * (`SkillSummary` carries `name` and `description`; nothing here can see a
141
+ * body, which is the point: a create must not pay a SECOND whole-tree pass — the
142
+ * listing's own frontmatter read is what feeds this comparison). Deliberately hint-only: the caller writes regardless, and the
143
+ * matches are news, not a gate (G4 — the create-time foreground behavior is
144
+ * unchanged).
145
+ *
146
+ * The default level is far below the scan's {@link DEDUP_SIMILARITY_THRESHOLD}
147
+ * because the projection is thinner: a name plus one sentence shares far fewer
148
+ * tokens than two bodies, and a hint that fires on every new skill is noise.
149
+ * Exact same-name candidates are skipped — the library's own create refuses
150
+ * those, and this line must not repeat that refusal.
151
+ */
152
+ export declare const SUMMARY_DUPLICATE_HINT_THRESHOLD = 0.5;
153
+ /** The fields {@link nearDuplicateSummaries} weighs — a structural subset of
154
+ * `SkillSummary`, so a listing feeds it without a conversion. */
155
+ export interface SummaryCandidate {
156
+ name: string;
157
+ description?: string;
158
+ }
159
+ export interface SummaryMatch {
160
+ name: string;
161
+ /** The `summary`-projection {@link affinity} in 0..1. */
162
+ score: number;
163
+ }
164
+ export declare function nearDuplicateSummaries(input: {
165
+ candidate: SummaryCandidate;
166
+ existing: ReadonlyArray<SummaryCandidate>;
167
+ /** Level at which a match is reported. Defaults to
168
+ * {@link SUMMARY_DUPLICATE_HINT_THRESHOLD}. */
169
+ threshold?: number;
170
+ /** Cap on returned matches, closest first. Defaults to 3. */
171
+ limit?: number;
172
+ }): SummaryMatch[];
82
173
  /**
83
174
  * Prefix-cluster index over a name set (rc.67 merge heuristic, input side):
84
175
  * the curator prompt asks the model to identify "prefix clusters — skills
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Skill content history: content-addressed blobs plus a per-skill version index.
3
+ *
4
+ * Why it exists: `.mutations.json` records before/after HASHES and calls every automated edit
5
+ * "reviewable and replayable" — but a hash cannot be replayed. With no content store, the only way
6
+ * back was a whole-tree snapshot (`.backups`, keep=5) or `.archive` (removed skills only), so
7
+ * reverting one skill cost the collateral of every other change in that window. This module keeps
8
+ * the content, keyed by skill and version.
9
+ *
10
+ * Invariants (design `dsh-evolution-skill-history-design.md` §1):
11
+ * - HISTORY IS HISTORY: the skill tree stays the single truth for "current content"; nothing here
12
+ * feeds a judgment — callers read back content and version NUMBERS only.
13
+ * - BEST-EFFORT, like the mutation audit: a failed history write warns and never fails the write
14
+ * that already landed.
15
+ * - Blobs are content-addressed, so identical bodies are stored once (across skills too).
16
+ * - Versions are numbered by `v` (max+1), NOT by the clock: a moved system clock must not reorder
17
+ * history (KiroCrew's `v<N>` choice, independently arrived at). Numbering and the index read-modify-write run
18
+ * under the INDEX's own io lock, so two writers of one skill cannot mint the same number.
19
+ * - The array's ORDER is append order, and the index is appended after the content lock is released, so two
20
+ * writers of ONE skill can interleave their appends. Each entry therefore carries `beforeHash` — the content
21
+ * it replaced, read under that writer's lock — and that link, not the position, is what `undo` follows
22
+ * ({@link orderVersions} rebuilds the content order for display when the chain is complete).
23
+ *
24
+ * NOT here: blob garbage collection. Trimming drops INDEX entries; orphan blobs stay until a
25
+ * sweeper collects them, because a reference count would have to span every skill's index and that
26
+ * belongs to the curator's whole-library pass, not to the write path.
27
+ * @module @lmzhen/dsh-evolution-core/skill-history
28
+ */
29
+ import { type EvolutionIoLike } from './io.ts';
30
+ /** Root-level history directory. Dot-prefixed so skill discovery skips it. */
31
+ export declare const HISTORY_DIR = ".history";
32
+ /** Versioned index shape, so a future field can migrate without guessing. */
33
+ export declare const HISTORY_INDEX_VERSION = 1;
34
+ /** The action label for a predecessor the index had never seen (content found on disk before this
35
+ * feature existed, or a hand-edited tree): it says "this is what was there", not what wrote it. */
36
+ export declare const BASELINE_ACTION = "baseline";
37
+ /** One recorded version of one skill. */
38
+ export interface SkillVersion {
39
+ /** Monotonic per skill; the identity, unlike {@link SkillVersion.at}. */
40
+ v: number;
41
+ at: string;
42
+ /** The mutation that produced it (`create`/`update`/`patch`/…), or {@link BASELINE_ACTION}. */
43
+ action: string;
44
+ /** sha256 of the stored content. */
45
+ hash: string;
46
+ /** Content length in characters, so a listing needs no blob read. */
47
+ chars: number;
48
+ /** The content this write REPLACED, as its hash — absent on a create, on a baseline, and on any
49
+ * entry written before 0.10.0. The array's ORDER is append order, which two writers of one skill
50
+ * can interleave (the index is appended after the content lock is released); this link is computed
51
+ * from the bytes a write lock actually read, so it is the authoritative predecessor. */
52
+ beforeHash?: string;
53
+ }
54
+ /** One history write, carrying the content the caller already holds. */
55
+ export interface VersionRecordInput {
56
+ skillName: string;
57
+ action: string;
58
+ /** Content before the mutation, or null when there was none (create/restore). */
59
+ before: string | null;
60
+ /** Content after the mutation, or null when there is none (archive/remove_file). */
61
+ after: string | null;
62
+ at: string;
63
+ }
64
+ /** The version numbers one write produced; a side with no content contributes nothing. */
65
+ export interface RecordedVersions {
66
+ beforeVersion?: number | undefined;
67
+ afterVersion?: number | undefined;
68
+ }
69
+ /** The root-level history directory. */
70
+ export declare function historyRoot(root: string): string;
71
+ /** Content-addressed blob path: identical bodies share one file, across skills too. */
72
+ export declare function blobPath(root: string, hash: string): string;
73
+ /** One skill's index file. */
74
+ export declare function historyIndexFile(root: string, name: string): string;
75
+ /**
76
+ * The versions in CONTENT order (oldest first), rebuilt from the chain links, for display.
77
+ *
78
+ * The stored array is append order, which interleaves when two writers of one skill audit out of
79
+ * order; the links say what actually replaced what. The rebuild runs only when it can account for
80
+ * EVERY entry (exactly one start and a forward link for each step) — otherwise the stored order is
81
+ * returned unchanged, because a partial reconstruction would be a worse answer than the honest one.
82
+ * @param versions - the stored index, oldest first as recorded.
83
+ * @returns the same entries, in content order when the chain is complete.
84
+ */
85
+ export declare function orderVersions(versions: readonly SkillVersion[]): SkillVersion[];
86
+ /** What one index file's bytes said. `absent` (no file yet) and `unreadable` (bytes that cannot be
87
+ * understood: malformed JSON, a foreign shape, an entry this reader would have to drop, or a writer
88
+ * NEWER than this reader) are DIFFERENT facts — the recorder must never replace the second with an
89
+ * index derived from "nothing", so it preserves those bytes instead. */
90
+ export type HistoryIndexState = {
91
+ readonly kind: 'ok';
92
+ readonly versions: SkillVersion[];
93
+ } | {
94
+ readonly kind: 'absent';
95
+ } | {
96
+ readonly kind: 'unreadable';
97
+ };
98
+ /**
99
+ * Read one index body for a RECORDER: "understood", "no file", or "cannot be understood".
100
+ * A file whose shape is right but which carries an entry this reader would have to drop counts as
101
+ * unreadable — dropping it silently would be the overwrite the audit posture forbids.
102
+ * @param raw - the index file's bytes, or null when absent.
103
+ * @returns the three-state answer.
104
+ */
105
+ export declare function readHistoryIndex(raw: string | null): HistoryIndexState;
106
+ /**
107
+ * Parse one index body for a READER. Malformed content, a foreign shape, or a version NEWER than this
108
+ * reader all read as empty — the audit posture: never guess. A recorder must use
109
+ * {@link readHistoryIndex} instead, because it has to PRESERVE bytes it cannot understand.
110
+ * @param raw - the index file's bytes, or null when absent.
111
+ * @returns the versions, oldest first.
112
+ */
113
+ export declare function parseHistoryIndex(raw: string | null): SkillVersion[];
114
+ /**
115
+ * The index after this write, and the version numbers it produced. Pure, so the versioning rules
116
+ * are testable without io.
117
+ *
118
+ * Two rules matter here: a predecessor the index has never seen gets a BASELINE entry (so the state
119
+ * before this mutation is recoverable even for a skill that predates the feature), and re-writing
120
+ * identical content mints NO new version (the library's no-op updates must not grow history).
121
+ * @param versions - the current index, oldest first.
122
+ * @param input - the write being recorded.
123
+ * @param keep - how many versions to retain per skill.
124
+ * @returns the trimmed index and the recorded numbers.
125
+ */
126
+ export declare function nextHistoryIndex(versions: readonly SkillVersion[], input: VersionRecordInput, keep: number): {
127
+ versions: SkillVersion[];
128
+ recorded: RecordedVersions;
129
+ };
130
+ /** Shrink level at which the retention line speaks: a body that keeps less than this share of the
131
+ * previous one is a rewrite, not an edit. */
132
+ export declare const RETENTION_FEEDBACK_KEEP_RATIO = 0.5;
133
+ /** Floor on the REPLACED body: below this a ratio is noise (a 40-character skill cut in half is not
134
+ * news), so the line stays off for small skills. */
135
+ export declare const RETENTION_FEEDBACK_MIN_CHARS = 200;
136
+ /** What {@link contentRetentionFeedback} reads: the one write it describes. */
137
+ export interface RetentionFeedbackInput {
138
+ /** The skill's name, for the sentence. */
139
+ readonly name: string;
140
+ /** The action that produced the write; only a whole-body replacement is described. */
141
+ readonly action: string;
142
+ /** The replaced body, or null when the skill had none (create / restore). */
143
+ readonly before: string | null;
144
+ /** The body now on disk, or null when it was removed (archive / remove_file). */
145
+ readonly after: string | null;
146
+ }
147
+ /**
148
+ * The anti-overwrite feedback line (design §4, item 3): says how much of the previous body a
149
+ * replacement kept, so a destructive rewrite is visible in the same message that reports the write.
150
+ *
151
+ * Computed where the replaced bytes are still in hand — the write path's own in-lock read — instead of
152
+ * re-reading the file from a caller (that second read would race the writers this library serializes).
153
+ * It is FEEDBACK, never a gate: no ratio refuses a write, and the function owns no configuration
154
+ * (a toggle nobody reads would be configuration that does nothing).
155
+ *
156
+ * @param input - the write being described.
157
+ * @returns the line, or null when the write is not a significant whole-body replacement.
158
+ */
159
+ export declare function contentRetentionFeedback(input: RetentionFeedbackInput): string | null;
160
+ /**
161
+ * Read one skill's versions, oldest first.
162
+ * @param root - the skills root.
163
+ * @param io - the io seam.
164
+ * @param name - the skill name.
165
+ * @returns the versions; an absent or unreadable index reads as empty (never a throw).
166
+ */
167
+ export declare function loadVersions(root: string, io: EvolutionIoLike, name: string): Promise<SkillVersion[]>;
168
+ /**
169
+ * One version's content.
170
+ * @param root - the skills root.
171
+ * @param io - the io seam.
172
+ * @param name - the skill name.
173
+ * @param v - the version number.
174
+ * @returns the content, or null when the version or its blob cannot be read (never '').
175
+ */
176
+ export declare function loadVersionContent(root: string, io: EvolutionIoLike, name: string, v: number): Promise<string | null>;
177
+ /**
178
+ * Record the content one mutation produced, best-effort.
179
+ *
180
+ * Blobs are written FIRST: the index must never reference content that is not on disk, and a failed
181
+ * blob write aborts the whole record (the caller warns; the mutation itself stands).
182
+ * @param root - the skills root.
183
+ * @param io - the io seam.
184
+ * @param input - the write being recorded.
185
+ * @param keep - how many versions to retain per skill.
186
+ * @returns the version numbers, or null when there was no content to record.
187
+ */
188
+ export declare function recordVersions(root: string, io: EvolutionIoLike, input: VersionRecordInput, keep: number): Promise<RecordedVersions | null>;
189
+ //# sourceMappingURL=skill-history.d.ts.map
@@ -0,0 +1,47 @@
1
+ /** The session-log accessor the platform exposes (0.1.5 on; `session.events` before it). */
2
+ export interface EvolutionSessionLogView {
3
+ /** @returns the session's events, oldest first. */
4
+ snapshotEvents?: () => Iterable<{
5
+ type: string;
6
+ data?: unknown;
7
+ }>;
8
+ }
9
+ /**
10
+ * The skill names ONE session read, or `undefined` when its log is not readable.
11
+ *
12
+ * `undefined` is NOT an empty set: a caller that must decide (the tool path's read-before-write
13
+ * gate) can then keep its previous behavior instead of refusing every write in a composition whose
14
+ * session objects expose no log at all.
15
+ * @param session - the session whose reads are wanted; `undefined` when the call carries none.
16
+ * @returns the names read through a non-failed `skill` dispatch, or `undefined` when unreadable.
17
+ */
18
+ export declare function sessionReadSkillNames(session: EvolutionSessionLogView | undefined): ReadonlySet<string> | undefined;
19
+ /** Actions whose target must have been read first (create is authorship, so it is exempt). */
20
+ export declare const READ_REQUIRED_ACTIONS: readonly string[];
21
+ /**
22
+ * Does this one op write a skill the caller never read?
23
+ *
24
+ * V6-26 (0.3.37): the validator normalizes a missing action to `patch`, so a missing action is a
25
+ * read-required write, never silently exempt.
26
+ * @param op - one skill operation (from a tool call or a review plan).
27
+ * @param readNames - names read this session, from {@link collectReadSkillNames}.
28
+ * @returns true when the op is a read-required write to an unread target.
29
+ */
30
+ export declare function isUnreadWrite(op: {
31
+ action?: string;
32
+ name?: string;
33
+ }, readNames: ReadonlySet<string>): boolean;
34
+ /**
35
+ * Drop mutating ops whose target was not read this session, in place.
36
+ *
37
+ * Covers the same mutating surface Hermes guards (edit/patch/write_file/remove_file), so a
38
+ * background review cannot blind-touch support files or edits of skills it never loaded.
39
+ * @param ops - the plan's ops; dropped entries are removed in place.
40
+ * @param readNames - names read this session.
41
+ * @returns the count of dropped ops, so the plan event can report them as rejected.
42
+ */
43
+ export declare function filterUnreadSkillOps(ops: Array<{
44
+ action?: string;
45
+ name?: string;
46
+ }>, readNames: ReadonlySet<string>): number;
47
+ //# sourceMappingURL=skill-reads.d.ts.map
@@ -36,6 +36,7 @@ import { transactIo, type EvolutionIoLike } from './io.ts';
36
36
  import { type Probe } from './probe.ts';
37
37
  import type { SkillLimits } from './limits.ts';
38
38
  import { type MutationRecord } from './mutations.ts';
39
+ import { type SkillVersion } from './skill-history.ts';
39
40
  import { type SkillHealthAssessment, type SkillHealthThresholds } from './skill-health.ts';
40
41
  import type { EvolutionSkillMutatedEvent } from './events.ts';
41
42
  export interface SkillSummary {
@@ -272,6 +273,9 @@ export declare class SkillLibrary {
272
273
  /** V10-03 (P2-18): see the constructor's threatExemptLabels. Empty by
273
274
  * default — the strict ANY-hit-blocks policy is unchanged. */
274
275
  private readonly threatExemptLabels;
276
+ /** skill-history.ts: the content history is best-effort, so a failure must not be SILENT —
277
+ * but it is a deployment condition, not a per-write event, so it warns once per instance. */
278
+ private historyWarned;
275
279
  constructor(root?: string, io?: EvolutionIoLike, limits?: SkillLimits, onMutation?: (event: EvolutionSkillMutatedEvent) => void, transact?: typeof transactIo, threatExemptLabels?: readonly string[]);
276
280
  /**
277
281
  * Run one single-file read-modify-write for a mutator. When `transact` was
@@ -394,10 +398,34 @@ export declare class SkillLibrary {
394
398
  patchCount?: number;
395
399
  readCount?: number;
396
400
  }): Promise<SkillHealthAssessment | null>;
397
- /** Best-effort audit trail entry; never blocks the mutation. */
401
+ /** Best-effort audit trail entry; never blocks the mutation.
402
+ *
403
+ * Two independent best-effort writes run here, because either one failing must not cost the
404
+ * other: the CONTENT history (skill-history.ts, so a version can be read back at all) and the
405
+ * mutation ledger that points at it. The history failure is not silent — a deployment whose
406
+ * history stopped growing is a degradation worth seeing once (the ledger keeps hashes either
407
+ * way). */
398
408
  private audit;
399
409
  /** Recent mutation audit records (read-only inspection surface). */
400
410
  listMutations(): Promise<MutationRecord[]>;
411
+ /**
412
+ * Content versions recorded for one skill, oldest first (skill-history.ts, batch A).
413
+ *
414
+ * Read-only: the version seam never feeds a judgment — callers use it to LIST and to read content
415
+ * back. A malformed or unreadable index reads as empty, and a bad name reads as empty too (the
416
+ * caller is a human command, so "no versions" is the honest answer for a name that cannot exist).
417
+ * @param rawName - the skill's name.
418
+ * @returns the versions, oldest first.
419
+ */
420
+ listVersions(rawName: string): Promise<SkillVersion[]>;
421
+ /**
422
+ * One version's content, read back from the content-addressed blob store.
423
+ * @param rawName - the skill's name.
424
+ * @param v - the version number (see {@link SkillLibrary.listVersions}).
425
+ * @returns the content, or null when the version does not exist, the name is unusable, or the
426
+ * blob cannot be read — never an empty string.
427
+ */
428
+ readVersion(rawName: string, v: number): Promise<string | null>;
401
429
  /**
402
430
  * Pin or unpin a skill (`.pinned` marker). Pinned skills are protected from
403
431
  * deletion, from background-review writes, and from the lifecycle — a
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.8.0",
4
+ "version": "0.10.0",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },