@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.
- package/README.md +13 -3
- package/lib/index.js +706 -93
- package/lib/types/constants.d.ts +15 -1
- package/lib/types/errors.d.ts +4 -1
- package/lib/types/events.d.ts +7 -0
- package/lib/types/evidence.d.ts +42 -0
- package/lib/types/index.d.ts +7 -2
- package/lib/types/limits.d.ts +3 -0
- package/lib/types/mutations.d.ts +5 -0
- package/lib/types/params.d.ts +21 -7
- package/lib/types/quality.d.ts +91 -0
- package/lib/types/skill-history.d.ts +189 -0
- package/lib/types/skill-reads.d.ts +47 -0
- package/lib/types/skill-store.d.ts +29 -1
- package/package.json +1 -1
package/lib/types/constants.d.ts
CHANGED
|
@@ -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
|
|
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
|
package/lib/types/errors.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* THE error-code table: E-301…E-
|
|
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;
|
package/lib/types/events.d.ts
CHANGED
|
@@ -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
|
package/lib/types/index.d.ts
CHANGED
|
@@ -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`, `
|
|
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`, `
|
|
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';
|
package/lib/types/limits.d.ts
CHANGED
|
@@ -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
|
package/lib/types/mutations.d.ts
CHANGED
|
@@ -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
|
}
|
package/lib/types/params.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
* requires a non-empty
|
|
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
|
package/lib/types/quality.d.ts
CHANGED
|
@@ -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