@lmzhen/dsh-evolution-core 0.9.0 → 0.11.1
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 +560 -20
- package/lib/types/constants.d.ts +4 -0
- package/lib/types/events.d.ts +7 -0
- package/lib/types/evidence.d.ts +42 -0
- package/lib/types/index.d.ts +4 -1
- package/lib/types/limits.d.ts +3 -0
- package/lib/types/mutations.d.ts +5 -0
- package/lib/types/quality.d.ts +91 -0
- package/lib/types/skill-history.d.ts +226 -0
- package/lib/types/skill-store.d.ts +29 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
# @lmzhen/dsh-evolution-core
|
|
2
2
|
|
|
3
|
-
Shared pure library for the family: `SkillLibrary`/`MemoryStore`, the prompts and guidance text,
|
|
4
|
-
the threat/quality/drift signals and the IO primitives (`nodeEvolutionIo`, `transactIo`,
|
|
5
|
-
`evolutionIoAdapter`). It is **not a Cordis row** — it registers no service, tool or prompt
|
|
3
|
+
Shared pure library for the family: `SkillLibrary`/`MemoryStore`, the prompts and guidance text, the threat/quality/drift signals, the skill CONTENT history (`skill-history.ts`: content-addressed versions behind every write) and the frame classification the plan path reports over (`evidence.ts`), plus the IO primitives (`nodeEvolutionIo`, `transactIo`, `evolutionIoAdapter`). It is **not a Cordis row** — it registers no service, tool or prompt
|
|
6
4
|
section, and importing the package root is its only entry.
|
|
7
5
|
|
|
8
6
|
## Model surface
|
|
@@ -15,12 +13,24 @@ section, and importing the package root is its only entry.
|
|
|
15
13
|
|
|
16
14
|
- This package is a library, not a Cordis row; do not mount it as a plugin.
|
|
17
15
|
- 数值配置已在消费方 Config 面钳制(`min 1`/各字段域);`MemoryStore` 内部对 `limit <= 0` 仍按 unbounded 防御处理——那是库内部防御,不构成"0 = 禁用"的配置语义。
|
|
16
|
+
- **Content-history storage grows with the number of DISTINCT bodies a skill has ever been written
|
|
17
|
+
with, not with the retained count.** Trimming drops index entries; the blobs they point at stay on
|
|
18
|
+
disk (they are shared by content, so a reference count would have to span every skill's index, and no
|
|
19
|
+
sweeper exists yet). Measured on a 3.6 KB skill written 41 times: a 5.7 KB index over 20 entries and
|
|
20
|
+
41 blob files totalling 150 KB (41x the live body); unchanged writes add nothing. Plan disk accordingly
|
|
21
|
+
or remove `.history/blobs` entries by hand.
|
|
18
22
|
- Skill-library writes are serialized in-process; cross-process writers to the SAME skill file must go through the single-file paths (`update`, `patch`, `writeSupportFile`). The full concurrency model, its two-phase exceptions and their provenance are in Notes and history.
|
|
19
23
|
|
|
20
24
|
**Runtime invariant:** No companion is published. The platform auto-assembles nothing and the family mounts no `<pkg>/invariant` cordis row, so a companion here would never execute (v37 S2.1 / I-3).
|
|
21
25
|
|
|
22
26
|
## Notes and history
|
|
23
27
|
|
|
28
|
+
- Similarity is TWO NAMED QUESTIONS plus a projection the caller declares (`quality.ts`): `identity(text)` ("is it the same text?", a normalized-content hash) and `affinity(a, b)` ("how much vocabulary do these two share?", Jaccard over two `vocabulary()` sets), asked over `projectionText(projection, input)` — `body` for the library-wide scan, `summary` (name + description) for the create-time hint. `computePrefixClusters` stays a name-side STRUCTURAL index, not a score. Adding a projection is one entry in that table; a caller that wants a fourth ruler should name the question it is really asking instead.
|
|
29
|
+
- A whole-body replacement that keeps less than half of the previous body appends one line to its own result
|
|
30
|
+
(`contentRetentionFeedback`, design §4 item 3): the ratio, both character counts, and the fact that the
|
|
31
|
+
replaced version is still in the history. Computed in the write funnel where both bodies are in hand, feedback
|
|
32
|
+
only — no ratio refuses a write, and the function owns no configuration.
|
|
33
|
+
- Content history is HISTORY ONLY: the skill tree is the single truth for current content, and no judgement reads the version index (design invariant I1). Recording is best-effort but happens inside the write lock, so the index never describes content that did not land; a failed record warns once per instance instead of failing the write.
|
|
24
34
|
- Skill-library mutations are read-modify-write on one file, so `SkillLibrary` serializes them in-process with a `makeSerialQueue` chain: `update`, `patch`, `restructure`, `writeSupportFile` and (since 0.3.46) `consolidate`'s target read→merge→commit run their whole read→validate→write under one serial task, so two concurrent mutators on one skill never interleave in this process.
|
|
25
35
|
- `create` is INSIDE the serial chain and, when a transact backend is bound, its exists check runs inside the same per-file transact (v18); `archive`/`consolidate` are rename-based two-phase paths and stay outside the single-file serial chain.
|
|
26
36
|
- **v18 residual (updated):** `create`, `removeSupportFile` and `setPinned` now run on the serial chain (`removeSupportFile`'s delete also goes through the per-file transact), so the remaining single-file residual is the protection TOCTOU (a marker check outside the transact) and the multi-file two-phase paths `archive`/`consolidate`/`restructure`.
|
package/lib/index.js
CHANGED
|
@@ -429,6 +429,10 @@ const PROTECTED_BUILTIN_SKILLS = new Set(["plan"]);
|
|
|
429
429
|
const MAX_SKILL_NAME_LENGTH = 64;
|
|
430
430
|
const MAX_DESCRIPTION_LENGTH = 1024;
|
|
431
431
|
const MAX_SKILL_CONTENT_CHARS = 1e5;
|
|
432
|
+
/** How many content versions to retain per skill (skill-history.ts). 20 mirrors the count
|
|
433
|
+
* KiroCrew settled on for the same job (its `MAX_SKILL_VERSIONS`), and is an E2 deployment
|
|
434
|
+
* parameter — the value is a retention policy, not a protocol constant. */
|
|
435
|
+
const DEFAULT_SKILL_VERSION_KEEP = 20;
|
|
432
436
|
/** The authoring DISCIPLINE band, taken from the upstream standard (archive §5):
|
|
433
437
|
* peer skills sit at 8-14k characters and a body pushing past 20k belongs in
|
|
434
438
|
* `references/*.md`. Deliberately separate from `MAX_SKILL_CONTENT_CHARS`: the
|
|
@@ -600,7 +604,8 @@ const DEFAULT_SKILL_LIMITS = {
|
|
|
600
604
|
maxNameLength: 64,
|
|
601
605
|
maxDescriptionLength: MAX_DESCRIPTION_LENGTH,
|
|
602
606
|
maxSkillContentChars: MAX_SKILL_CONTENT_CHARS,
|
|
603
|
-
maxSkillFileBytes: MAX_SKILL_FILE_BYTES
|
|
607
|
+
maxSkillFileBytes: MAX_SKILL_FILE_BYTES,
|
|
608
|
+
versionKeep: 20
|
|
604
609
|
};
|
|
605
610
|
//#endregion
|
|
606
611
|
//#region lib/types/frontmatter.js
|
|
@@ -4766,6 +4771,12 @@ Learn workflow (when the user asks you to learn a reusable skill, or you decide
|
|
|
4766
4771
|
* mutation maturity is a documented DSH approximation (single per-month patch
|
|
4767
4772
|
* trend ratio replaces the claw timestamp-trend formula, since DSH usage
|
|
4768
4773
|
* records only carry the last patched timestamp).
|
|
4774
|
+
*
|
|
4775
|
+
* Batch C (2026-09-27): the similarity half of this module is now TWO NAMED
|
|
4776
|
+
* QUESTIONS plus a declared projection — {@link identity} ("is it the same
|
|
4777
|
+
* text?") and {@link affinity} ("how much vocabulary do these two share?") asked
|
|
4778
|
+
* over {@link projectionText}. Callers name the question and the projection
|
|
4779
|
+
* instead of growing a fourth unnamed ruler (design §3C / §1 I2).
|
|
4769
4780
|
* @module @lmzhen/dsh-evolution-core
|
|
4770
4781
|
*/
|
|
4771
4782
|
const QUALITY_WEIGHTS = {
|
|
@@ -4820,21 +4831,55 @@ function computeQualityScores(input) {
|
|
|
4820
4831
|
function normalize(content) {
|
|
4821
4832
|
return content.toLowerCase().replace(/\s+/g, " ").trim();
|
|
4822
4833
|
}
|
|
4823
|
-
/**
|
|
4834
|
+
/** Named similarity question 1 — "is it the same text?" — as a normalized
|
|
4835
|
+
* content hash: case- and whitespace-insensitive, so two texts that differ only
|
|
4836
|
+
* in formatting are the SAME text here. Invariant under which projection the
|
|
4837
|
+
* caller read the text from.
|
|
4838
|
+
*
|
|
4839
|
+
* C-26 (v10 audit): NOT named `contentHash` — mutations.ts exports a
|
|
4824
4840
|
* contentHash of RAW bytes, this one hashes the NORMALIZED text for dedup
|
|
4825
|
-
* grouping. Same
|
|
4826
|
-
function
|
|
4827
|
-
return createHash("sha256").update(normalize(
|
|
4841
|
+
* grouping. Same helper as that rename, now a named question. */
|
|
4842
|
+
function identity(text) {
|
|
4843
|
+
return createHash("sha256").update(normalize(text)).digest("hex");
|
|
4828
4844
|
}
|
|
4829
|
-
|
|
4830
|
-
|
|
4831
|
-
|
|
4832
|
-
|
|
4845
|
+
/**
|
|
4846
|
+
* The declared projection: WHICH text a comparison weighs.
|
|
4847
|
+
*
|
|
4848
|
+
* `body` is the library-wide scan's projection (whole SKILL.md, read by the
|
|
4849
|
+
* caller). `summary` is name + description in one string — the projection a
|
|
4850
|
+
* listing already publishes (`SkillSummary` carries both fields, and a body is
|
|
4851
|
+
* attached only when the caller asks for it) — so a summary comparison never
|
|
4852
|
+
* reads a body OF ITS OWN: the create-time hint is fed by the listing pass the
|
|
4853
|
+
* tool already pays for its frontmatter, and calls no read of its own.
|
|
4854
|
+
*/
|
|
4855
|
+
function projectionText(projection, input) {
|
|
4856
|
+
switch (projection) {
|
|
4857
|
+
case "body": return input.content ?? "";
|
|
4858
|
+
case "summary": return `${input.name ?? ""} ${input.description ?? ""}`;
|
|
4859
|
+
}
|
|
4860
|
+
}
|
|
4861
|
+
/** The comparable vocabulary of one text: lowercase alphanumeric runs, plus
|
|
4862
|
+
* whole CJK runs (a Chinese description tokenizes by punctuation, not by word —
|
|
4863
|
+
* deliberately: the ruler is one home, and a real tokenizer is a different
|
|
4864
|
+
* question than "do these two share vocabulary?"). */
|
|
4865
|
+
function vocabulary(text) {
|
|
4866
|
+
return new Set(normalize(text).split(/[^a-z0-9\u4e00-\u9fff]+/).filter(Boolean));
|
|
4867
|
+
}
|
|
4868
|
+
/** Named similarity question 2 — "how much vocabulary do these two share?" —
|
|
4869
|
+
* Jaccard over two {@link vocabulary} sets, 0 (nothing in common, or either side
|
|
4870
|
+
* empty) to 1 (identical vocabulary). Set-based on purpose: the bulk scan
|
|
4871
|
+
* memoizes each name's set once and only the comparisons are budgeted. */
|
|
4872
|
+
function affinity(a, b) {
|
|
4833
4873
|
if (a.size === 0 || b.size === 0) return 0;
|
|
4834
4874
|
let intersection = 0;
|
|
4835
4875
|
for (const token of a) if (b.has(token)) intersection += 1;
|
|
4836
4876
|
return intersection / (a.size + b.size - intersection);
|
|
4837
4877
|
}
|
|
4878
|
+
/** The `affinity` level at which two bodies are treated as the same skill
|
|
4879
|
+
* (design §3C: the default is unchanged at 0.95 — it is now a NAME, because the
|
|
4880
|
+
* docblock below has linked it since PLAN-R2 while the code carried a bare
|
|
4881
|
+
* literal). Callers may still pass their own level. */
|
|
4882
|
+
const DEDUP_SIMILARITY_THRESHOLD = .95;
|
|
4838
4883
|
/** PLAN-R2 P2-8 (2026-09-16): default cap on two-name comparisons in
|
|
4839
4884
|
* {@link computeDedupGroups}. A 2000-skill library is ~2M pairs; the old
|
|
4840
4885
|
* unbounded two-two Jaccard ran seconds to tens of seconds per
|
|
@@ -4861,7 +4906,7 @@ function computeDedupGroups(input) {
|
|
|
4861
4906
|
const names = [...input.contents.keys()];
|
|
4862
4907
|
const hashes = /* @__PURE__ */ new Map();
|
|
4863
4908
|
for (const name of names) {
|
|
4864
|
-
const hash =
|
|
4909
|
+
const hash = identity(projectionText("body", { content: input.contents.get(name) ?? "" }));
|
|
4865
4910
|
const bucket = hashes.get(hash);
|
|
4866
4911
|
if (bucket) bucket.push(name);
|
|
4867
4912
|
else hashes.set(hash, [name]);
|
|
@@ -4888,7 +4933,7 @@ function computeDedupGroups(input) {
|
|
|
4888
4933
|
const tokenSet = (name) => {
|
|
4889
4934
|
let set = tokens.get(name);
|
|
4890
4935
|
if (!set) {
|
|
4891
|
-
set =
|
|
4936
|
+
set = vocabulary(projectionText("body", { content: input.contents.get(name) ?? "" }));
|
|
4892
4937
|
tokens.set(name, set);
|
|
4893
4938
|
}
|
|
4894
4939
|
return set;
|
|
@@ -4908,7 +4953,7 @@ function computeDedupGroups(input) {
|
|
|
4908
4953
|
compared += 1;
|
|
4909
4954
|
const [ta, tb] = [tokenSet(a), tokenSet(b)];
|
|
4910
4955
|
if (Math.max(ta.size, tb.size) / Math.max(1, Math.min(ta.size, tb.size)) > 5) continue;
|
|
4911
|
-
if (
|
|
4956
|
+
if (affinity(ta, tb) >= threshold) union(a, b);
|
|
4912
4957
|
}
|
|
4913
4958
|
}
|
|
4914
4959
|
const groups = /* @__PURE__ */ new Map();
|
|
@@ -4924,6 +4969,40 @@ function computeDedupGroups(input) {
|
|
|
4924
4969
|
};
|
|
4925
4970
|
}
|
|
4926
4971
|
/**
|
|
4972
|
+
* The create-time near-duplicate question (batch C, design §3C-2): WHICH
|
|
4973
|
+
* existing skills is this authoring candidate about to near-copy?
|
|
4974
|
+
*
|
|
4975
|
+
* It is the SAME {@link affinity} question as the library-wide scan, asked over
|
|
4976
|
+
* the `summary` projection — the one a listing already publishes
|
|
4977
|
+
* (`SkillSummary` carries `name` and `description`; nothing here can see a
|
|
4978
|
+
* body, which is the point: a create must not pay a SECOND whole-tree pass — the
|
|
4979
|
+
* listing's own frontmatter read is what feeds this comparison). Deliberately hint-only: the caller writes regardless, and the
|
|
4980
|
+
* matches are news, not a gate (G4 — the create-time foreground behavior is
|
|
4981
|
+
* unchanged).
|
|
4982
|
+
*
|
|
4983
|
+
* The default level is far below the scan's {@link DEDUP_SIMILARITY_THRESHOLD}
|
|
4984
|
+
* because the projection is thinner: a name plus one sentence shares far fewer
|
|
4985
|
+
* tokens than two bodies, and a hint that fires on every new skill is noise.
|
|
4986
|
+
* Exact same-name candidates are skipped — the library's own create refuses
|
|
4987
|
+
* those, and this line must not repeat that refusal.
|
|
4988
|
+
*/
|
|
4989
|
+
const SUMMARY_DUPLICATE_HINT_THRESHOLD = .5;
|
|
4990
|
+
function nearDuplicateSummaries(input) {
|
|
4991
|
+
const threshold = input.threshold ?? .5;
|
|
4992
|
+
const limit = input.limit ?? 3;
|
|
4993
|
+
const candidateTokens = vocabulary(projectionText("summary", input.candidate));
|
|
4994
|
+
const matches = [];
|
|
4995
|
+
for (const existing of input.existing) {
|
|
4996
|
+
if (existing.name === input.candidate.name) continue;
|
|
4997
|
+
const score = affinity(candidateTokens, vocabulary(projectionText("summary", existing)));
|
|
4998
|
+
if (score >= threshold) matches.push({
|
|
4999
|
+
name: existing.name,
|
|
5000
|
+
score
|
|
5001
|
+
});
|
|
5002
|
+
}
|
|
5003
|
+
return matches.sort((a, b) => b.score - a.score || a.name.localeCompare(b.name)).slice(0, limit);
|
|
5004
|
+
}
|
|
5005
|
+
/**
|
|
4927
5006
|
* Prefix-cluster index over a name set (rc.67 merge heuristic, input side):
|
|
4928
5007
|
* the curator prompt asks the model to identify "prefix clusters — skills
|
|
4929
5008
|
* sharing a first word or domain keyword"; the deterministic index supplies
|
|
@@ -5995,6 +6074,58 @@ function filterUnreadSkillOps(ops, readNames) {
|
|
|
5995
6074
|
return dropped;
|
|
5996
6075
|
}
|
|
5997
6076
|
//#endregion
|
|
6077
|
+
//#region lib/types/evidence.js
|
|
6078
|
+
/**
|
|
6079
|
+
* Which session frames are EVIDENCE, and which are bookkeeping.
|
|
6080
|
+
*
|
|
6081
|
+
* A plan op must cite `evidence` seqs, and the validator's range rule
|
|
6082
|
+
* (`evolution-plan-validator`, rule `EVIDENCE_RANGE`) answers only "is this an integer inside
|
|
6083
|
+
* the window the plan was authored against". A boundary frame satisfies that while carrying
|
|
6084
|
+
* nothing a claim can rest on: the `turn/start` seq is the frame a claim about that turn was
|
|
6085
|
+
* most likely read out of, and citing it is indistinguishable from citing a real exchange. This
|
|
6086
|
+
* module owns the ONE classification of a session frame by what it CARRIES, so the plan path can
|
|
6087
|
+
* report an op whose whole citation list is bookkeeping (design
|
|
6088
|
+
* `dsh-evolution-skill-history-design.md` §3D, phase 1: report only — no op is refused here).
|
|
6089
|
+
*
|
|
6090
|
+
* The classification is a DENY list of the pure bookends, not an allow list of content frames.
|
|
6091
|
+
* The platform's own vocabulary (`KNOWN_SESSION_EVENT_TYPES`, 57 members at 0.1.5-rc.2) mixes
|
|
6092
|
+
* content frames with state-change frames, and out-of-repo plugins append types the family cannot
|
|
6093
|
+
* enumerate; an unknown frame is therefore treated as carrying content, because a report that
|
|
6094
|
+
* fires on an unclassifiable frame is a false positive and phase 1 exists to measure how many TRUE
|
|
6095
|
+
* ones there are. Tightening — a narrower definition, or an allow list built from the platform's
|
|
6096
|
+
* vocabulary — changes this table and nothing else (`evidenceKindIndex` is the only reader).
|
|
6097
|
+
* @module @lmzhen/dsh-evolution-core
|
|
6098
|
+
*/
|
|
6099
|
+
/** The frame types that record a boundary and carry no content: the turn and step bookends. */
|
|
6100
|
+
const BOOKKEEPING_FRAME_TYPES = new Set([
|
|
6101
|
+
"turn/start",
|
|
6102
|
+
"turn/end",
|
|
6103
|
+
"step/start",
|
|
6104
|
+
"step/end"
|
|
6105
|
+
]);
|
|
6106
|
+
/**
|
|
6107
|
+
* The seqs of the frames that carry content.
|
|
6108
|
+
*
|
|
6109
|
+
* @param events - the session's frames, oldest first (any iterable; nothing is retained).
|
|
6110
|
+
* @returns the substantive seqs, or `undefined` when NO frame carried a usable seq. The two
|
|
6111
|
+
* answers are different facts: an empty SET says "every sequenced frame is a boundary", while
|
|
6112
|
+
* `undefined` says "this log cannot tell a boundary from a message" (a stub session in a test,
|
|
6113
|
+
* a composition whose events carry no seq). A caller that cannot classify must stay silent
|
|
6114
|
+
* rather than report every op — the same three-state posture as `sessionReadSkillNames`.
|
|
6115
|
+
*/
|
|
6116
|
+
function evidenceKindIndex(events) {
|
|
6117
|
+
const substantive = /* @__PURE__ */ new Set();
|
|
6118
|
+
let sequenced = 0;
|
|
6119
|
+
for (const event of events) {
|
|
6120
|
+
const seq = event.seq;
|
|
6121
|
+
if (typeof seq !== "number" || !Number.isInteger(seq)) continue;
|
|
6122
|
+
sequenced += 1;
|
|
6123
|
+
if (typeof event.type === "string" && BOOKKEEPING_FRAME_TYPES.has(event.type)) continue;
|
|
6124
|
+
substantive.add(seq);
|
|
6125
|
+
}
|
|
6126
|
+
return sequenced === 0 ? void 0 : substantive;
|
|
6127
|
+
}
|
|
6128
|
+
//#endregion
|
|
5998
6129
|
//#region lib/types/drift-signals.js
|
|
5999
6130
|
/**
|
|
6000
6131
|
* Library-level drift signals for the maintenance subagent (design 011).
|
|
@@ -6260,6 +6391,337 @@ function findDriftSignal(signals, id) {
|
|
|
6260
6391
|
return signals.find((signal) => signal.id === id);
|
|
6261
6392
|
}
|
|
6262
6393
|
//#endregion
|
|
6394
|
+
//#region lib/types/skill-history.js
|
|
6395
|
+
/**
|
|
6396
|
+
* Skill content history: content-addressed blobs plus a per-skill version index.
|
|
6397
|
+
*
|
|
6398
|
+
* Why it exists: `.mutations.json` records before/after HASHES and calls every automated edit
|
|
6399
|
+
* "reviewable and replayable" — but a hash cannot be replayed. With no content store, the only way
|
|
6400
|
+
* back was a whole-tree snapshot (`.backups`, keep=5) or `.archive` (removed skills only), so
|
|
6401
|
+
* reverting one skill cost the collateral of every other change in that window. This module keeps
|
|
6402
|
+
* the content, keyed by skill and version.
|
|
6403
|
+
*
|
|
6404
|
+
* Invariants (design `dsh-evolution-skill-history-design.md` §1):
|
|
6405
|
+
* - HISTORY IS HISTORY: the skill tree stays the single truth for "current content"; nothing here
|
|
6406
|
+
* feeds a judgment — callers read back content and version NUMBERS only.
|
|
6407
|
+
* - BEST-EFFORT, like the mutation audit: a failed history write warns and never fails the write
|
|
6408
|
+
* that already landed.
|
|
6409
|
+
* - Blobs are content-addressed, so identical bodies are stored once (across skills too).
|
|
6410
|
+
* - Versions are numbered by `v` (max+1), NOT by the clock: a moved system clock must not reorder
|
|
6411
|
+
* history (KiroCrew's `v<N>` choice, independently arrived at). Numbering and the index read-modify-write run
|
|
6412
|
+
* under the INDEX's own io lock, so two writers of one skill cannot mint the same number.
|
|
6413
|
+
* - The array's ORDER is append order, and the index is appended after the content lock is released, so two
|
|
6414
|
+
* writers of ONE skill can interleave their appends. Each entry therefore carries `beforeHash` — the content
|
|
6415
|
+
* it replaced, read under that writer's lock — and that link, not the position, is what `undo` follows
|
|
6416
|
+
* ({@link orderVersions} rebuilds the content order for display when the chain is complete).
|
|
6417
|
+
*
|
|
6418
|
+
* NOT here: blob garbage collection. Trimming drops INDEX entries; orphan blobs stay until a
|
|
6419
|
+
* sweeper collects them, because a reference count would have to span every skill's index and that
|
|
6420
|
+
* belongs to the curator's whole-library pass, not to the write path.
|
|
6421
|
+
* @module @lmzhen/dsh-evolution-core/skill-history
|
|
6422
|
+
*/
|
|
6423
|
+
/** Root-level history directory. Dot-prefixed so skill discovery skips it. */
|
|
6424
|
+
const HISTORY_DIR = ".history";
|
|
6425
|
+
/** Versioned index shape, so a future field can migrate without guessing. */
|
|
6426
|
+
const HISTORY_INDEX_VERSION = 1;
|
|
6427
|
+
/** The action label for a predecessor the index had never seen (content found on disk before this
|
|
6428
|
+
* feature existed, or a hand-edited tree): it says "this is what was there", not what wrote it. */
|
|
6429
|
+
const BASELINE_ACTION = "baseline";
|
|
6430
|
+
/** The root-level history directory. */
|
|
6431
|
+
function historyRoot(root) {
|
|
6432
|
+
return join(root, HISTORY_DIR);
|
|
6433
|
+
}
|
|
6434
|
+
/** Content-addressed blob path: identical bodies share one file, across skills too. */
|
|
6435
|
+
function blobPath(root, hash) {
|
|
6436
|
+
return join(historyRoot(root), "blobs", hash);
|
|
6437
|
+
}
|
|
6438
|
+
/** One skill's index file. */
|
|
6439
|
+
function historyIndexFile(root, name) {
|
|
6440
|
+
return join(historyRoot(root), "skills", name, "index.json");
|
|
6441
|
+
}
|
|
6442
|
+
/**
|
|
6443
|
+
* The versions in CONTENT order (oldest first), rebuilt from the chain links, for display.
|
|
6444
|
+
*
|
|
6445
|
+
* The stored array is append order, which interleaves when two writers of one skill audit out of
|
|
6446
|
+
* order; the links say what actually replaced what. The rebuild runs only when it can account for
|
|
6447
|
+
* EVERY entry (exactly one start and a forward link for each step) — otherwise the stored order is
|
|
6448
|
+
* returned unchanged, because a partial reconstruction would be a worse answer than the honest one.
|
|
6449
|
+
* @param versions - the stored index, oldest first as recorded.
|
|
6450
|
+
* @returns the same entries, in content order when the chain is complete.
|
|
6451
|
+
*/
|
|
6452
|
+
function orderVersions(versions) {
|
|
6453
|
+
if (versions.length < 2) return [...versions];
|
|
6454
|
+
const replaced = /* @__PURE__ */ new Set();
|
|
6455
|
+
for (const entry of versions) if (entry.beforeHash !== void 0) replaced.add(entry.beforeHash);
|
|
6456
|
+
const ends = versions.filter((entry) => !replaced.has(entry.hash));
|
|
6457
|
+
if (ends.length !== 1) return [...versions];
|
|
6458
|
+
const used = /* @__PURE__ */ new Set();
|
|
6459
|
+
const backward = [];
|
|
6460
|
+
let cursor = ends[0];
|
|
6461
|
+
while (cursor !== void 0 && !used.has(cursor)) {
|
|
6462
|
+
used.add(cursor);
|
|
6463
|
+
backward.push(cursor);
|
|
6464
|
+
const before = cursor.beforeHash;
|
|
6465
|
+
cursor = before === void 0 ? void 0 : [...versions].reverse().find((entry) => !used.has(entry) && entry.hash === before);
|
|
6466
|
+
}
|
|
6467
|
+
return backward.length === versions.length ? backward.reverse() : [...versions];
|
|
6468
|
+
}
|
|
6469
|
+
/** The actions that record a SUPPORT FILE's bytes (they share the skill's version index). */
|
|
6470
|
+
const SUPPORT_ACTIONS = ["write_file", "remove_file"];
|
|
6471
|
+
/** The actions that record the skill's own SKILL.md body. */
|
|
6472
|
+
const CONTENT_ACTIONS = [
|
|
6473
|
+
"create",
|
|
6474
|
+
"update",
|
|
6475
|
+
"patch",
|
|
6476
|
+
"consolidate",
|
|
6477
|
+
"restructure",
|
|
6478
|
+
"restore"
|
|
6479
|
+
];
|
|
6480
|
+
/**
|
|
6481
|
+
* Which artifact one recorded version holds, from the action that produced it.
|
|
6482
|
+
* @param action - the entry's action label.
|
|
6483
|
+
* @returns the artifact class; `other` when the label does not say.
|
|
6484
|
+
*/
|
|
6485
|
+
function versionTarget(action) {
|
|
6486
|
+
if (SUPPORT_ACTIONS.includes(action)) return "support";
|
|
6487
|
+
if (CONTENT_ACTIONS.includes(action)) return "content";
|
|
6488
|
+
return "other";
|
|
6489
|
+
}
|
|
6490
|
+
/**
|
|
6491
|
+
* Split one index into the skill body's chain and the support files' versions.
|
|
6492
|
+
*
|
|
6493
|
+
* Both share ONE index, so a skill that ever wrote a support file has two chains and
|
|
6494
|
+
* {@link orderVersions} cannot account for every entry (it falls back to the stored order). Splitting
|
|
6495
|
+
* first lets each group rebuild its own order, and keeps the body's chain intact by leaving every
|
|
6496
|
+
* entry the index cannot classify in it — a `baseline` predecessor is usually the body's first link.
|
|
6497
|
+
* @param versions - the stored index, oldest first as recorded.
|
|
6498
|
+
* @returns the two groups, each in content order when its chain is complete.
|
|
6499
|
+
*/
|
|
6500
|
+
function partitionVersions(versions) {
|
|
6501
|
+
const content = [];
|
|
6502
|
+
const support = [];
|
|
6503
|
+
for (const entry of versions) if (versionTarget(entry.action) === "support") support.push(entry);
|
|
6504
|
+
else content.push(entry);
|
|
6505
|
+
return {
|
|
6506
|
+
content: orderVersions(content),
|
|
6507
|
+
support: orderVersions(support)
|
|
6508
|
+
};
|
|
6509
|
+
}
|
|
6510
|
+
/** Keep nothing less than one version, whatever the deployment asks for. */
|
|
6511
|
+
function clampKeep(keep) {
|
|
6512
|
+
return Number.isFinite(keep) ? Math.max(1, Math.floor(keep)) : 1;
|
|
6513
|
+
}
|
|
6514
|
+
/** The next version number: max+1, never a timestamp. */
|
|
6515
|
+
function nextVersion(versions) {
|
|
6516
|
+
let max = 0;
|
|
6517
|
+
for (const entry of versions) if (entry.v > max) max = entry.v;
|
|
6518
|
+
return max + 1;
|
|
6519
|
+
}
|
|
6520
|
+
/** The last entry carrying this hash, or undefined (a hash may repeat after an undo). */
|
|
6521
|
+
function lastVersionOf(versions, hash) {
|
|
6522
|
+
for (let index = versions.length - 1; index >= 0; index -= 1) {
|
|
6523
|
+
const entry = versions[index];
|
|
6524
|
+
if (entry !== void 0 && entry.hash === hash) return entry.v;
|
|
6525
|
+
}
|
|
6526
|
+
}
|
|
6527
|
+
/** One entry as this reader accepts it, or null when the entry cannot be understood. */
|
|
6528
|
+
function readVersionEntry(entry) {
|
|
6529
|
+
if (typeof entry !== "object" || entry === null) return null;
|
|
6530
|
+
const candidate = entry;
|
|
6531
|
+
if (typeof candidate.v !== "number" || !Number.isInteger(candidate.v) || candidate.v <= 0) return null;
|
|
6532
|
+
if (typeof candidate.hash !== "string" || candidate.hash === "") return null;
|
|
6533
|
+
if (typeof candidate.at !== "string") return null;
|
|
6534
|
+
if (typeof candidate.action !== "string") return null;
|
|
6535
|
+
if (typeof candidate.chars !== "number") return null;
|
|
6536
|
+
return typeof candidate.beforeHash === "string" && candidate.beforeHash !== "" ? {
|
|
6537
|
+
...candidate,
|
|
6538
|
+
beforeHash: candidate.beforeHash
|
|
6539
|
+
} : {
|
|
6540
|
+
v: candidate.v,
|
|
6541
|
+
at: candidate.at,
|
|
6542
|
+
action: candidate.action,
|
|
6543
|
+
hash: candidate.hash,
|
|
6544
|
+
chars: candidate.chars
|
|
6545
|
+
};
|
|
6546
|
+
}
|
|
6547
|
+
/**
|
|
6548
|
+
* Read one index body for a RECORDER: "understood", "no file", or "cannot be understood".
|
|
6549
|
+
* A file whose shape is right but which carries an entry this reader would have to drop counts as
|
|
6550
|
+
* unreadable — dropping it silently would be the overwrite the audit posture forbids.
|
|
6551
|
+
* @param raw - the index file's bytes, or null when absent.
|
|
6552
|
+
* @returns the three-state answer.
|
|
6553
|
+
*/
|
|
6554
|
+
function readHistoryIndex(raw) {
|
|
6555
|
+
if (raw === null) return { kind: "absent" };
|
|
6556
|
+
if (raw.trim() === "") return { kind: "unreadable" };
|
|
6557
|
+
let parsed;
|
|
6558
|
+
try {
|
|
6559
|
+
parsed = JSON.parse(raw);
|
|
6560
|
+
} catch {
|
|
6561
|
+
return { kind: "unreadable" };
|
|
6562
|
+
}
|
|
6563
|
+
if (typeof parsed !== "object" || parsed === null) return { kind: "unreadable" };
|
|
6564
|
+
const holder = parsed;
|
|
6565
|
+
if (typeof holder.version === "number" && holder.version > 1) return { kind: "unreadable" };
|
|
6566
|
+
if (!Array.isArray(holder.versions)) return { kind: "unreadable" };
|
|
6567
|
+
const versions = [];
|
|
6568
|
+
for (const entry of holder.versions) {
|
|
6569
|
+
const version = readVersionEntry(entry);
|
|
6570
|
+
if (version === null) return { kind: "unreadable" };
|
|
6571
|
+
versions.push(version);
|
|
6572
|
+
}
|
|
6573
|
+
return {
|
|
6574
|
+
kind: "ok",
|
|
6575
|
+
versions
|
|
6576
|
+
};
|
|
6577
|
+
}
|
|
6578
|
+
/**
|
|
6579
|
+
* Parse one index body for a READER. Malformed content, a foreign shape, or a version NEWER than this
|
|
6580
|
+
* reader all read as empty — the audit posture: never guess. A recorder must use
|
|
6581
|
+
* {@link readHistoryIndex} instead, because it has to PRESERVE bytes it cannot understand.
|
|
6582
|
+
* @param raw - the index file's bytes, or null when absent.
|
|
6583
|
+
* @returns the versions, oldest first.
|
|
6584
|
+
*/
|
|
6585
|
+
function parseHistoryIndex(raw) {
|
|
6586
|
+
const state = readHistoryIndex(raw);
|
|
6587
|
+
return state.kind === "ok" ? state.versions : [];
|
|
6588
|
+
}
|
|
6589
|
+
/**
|
|
6590
|
+
* The index after this write, and the version numbers it produced. Pure, so the versioning rules
|
|
6591
|
+
* are testable without io.
|
|
6592
|
+
*
|
|
6593
|
+
* Two rules matter here: a predecessor the index has never seen gets a BASELINE entry (so the state
|
|
6594
|
+
* before this mutation is recoverable even for a skill that predates the feature), and re-writing
|
|
6595
|
+
* identical content mints NO new version (the library's no-op updates must not grow history).
|
|
6596
|
+
* @param versions - the current index, oldest first.
|
|
6597
|
+
* @param input - the write being recorded.
|
|
6598
|
+
* @param keep - how many versions to retain per skill.
|
|
6599
|
+
* @returns the trimmed index and the recorded numbers.
|
|
6600
|
+
*/
|
|
6601
|
+
function nextHistoryIndex(versions, input, keep) {
|
|
6602
|
+
const beforeHash = input.before === null ? null : contentHash(input.before);
|
|
6603
|
+
const afterHash = input.after === null ? null : contentHash(input.after);
|
|
6604
|
+
const grown = [...versions];
|
|
6605
|
+
if (beforeHash !== null && !grown.some((entry) => entry.hash === beforeHash)) grown.push({
|
|
6606
|
+
v: nextVersion(grown),
|
|
6607
|
+
at: input.at,
|
|
6608
|
+
action: BASELINE_ACTION,
|
|
6609
|
+
hash: beforeHash,
|
|
6610
|
+
chars: input.before?.length ?? 0
|
|
6611
|
+
});
|
|
6612
|
+
if (afterHash !== null) {
|
|
6613
|
+
if (grown[grown.length - 1]?.hash !== afterHash) grown.push({
|
|
6614
|
+
v: nextVersion(grown),
|
|
6615
|
+
at: input.at,
|
|
6616
|
+
action: input.action,
|
|
6617
|
+
hash: afterHash,
|
|
6618
|
+
chars: input.after?.length ?? 0,
|
|
6619
|
+
...beforeHash === null ? {} : { beforeHash }
|
|
6620
|
+
});
|
|
6621
|
+
}
|
|
6622
|
+
const trimmed = grown.slice(Math.max(0, grown.length - clampKeep(keep)));
|
|
6623
|
+
const recorded = {};
|
|
6624
|
+
if (beforeHash !== null) {
|
|
6625
|
+
const v = lastVersionOf(trimmed, beforeHash);
|
|
6626
|
+
if (v !== void 0) recorded.beforeVersion = v;
|
|
6627
|
+
}
|
|
6628
|
+
if (afterHash !== null) {
|
|
6629
|
+
const v = lastVersionOf(trimmed, afterHash);
|
|
6630
|
+
if (v !== void 0) recorded.afterVersion = v;
|
|
6631
|
+
}
|
|
6632
|
+
return {
|
|
6633
|
+
versions: trimmed,
|
|
6634
|
+
recorded
|
|
6635
|
+
};
|
|
6636
|
+
}
|
|
6637
|
+
/** Shrink level at which the retention line speaks: a body that keeps less than this share of the
|
|
6638
|
+
* previous one is a rewrite, not an edit. */
|
|
6639
|
+
const RETENTION_FEEDBACK_KEEP_RATIO = .5;
|
|
6640
|
+
/** Floor on the REPLACED body: below this a ratio is noise (a 40-character skill cut in half is not
|
|
6641
|
+
* news), so the line stays off for small skills. */
|
|
6642
|
+
const RETENTION_FEEDBACK_MIN_CHARS = 200;
|
|
6643
|
+
/**
|
|
6644
|
+
* The anti-overwrite feedback line (design §4, item 3): says how much of the previous body a
|
|
6645
|
+
* replacement kept, so a destructive rewrite is visible in the same message that reports the write.
|
|
6646
|
+
*
|
|
6647
|
+
* Computed where the replaced bytes are still in hand — the write path's own in-lock read — instead of
|
|
6648
|
+
* re-reading the file from a caller (that second read would race the writers this library serializes).
|
|
6649
|
+
* It is FEEDBACK, never a gate: no ratio refuses a write, and the function owns no configuration
|
|
6650
|
+
* (a toggle nobody reads would be configuration that does nothing).
|
|
6651
|
+
*
|
|
6652
|
+
* @param input - the write being described.
|
|
6653
|
+
* @returns the line, or null when the write is not a significant whole-body replacement.
|
|
6654
|
+
*/
|
|
6655
|
+
function contentRetentionFeedback(input) {
|
|
6656
|
+
if (input.action !== "update") return null;
|
|
6657
|
+
const before = input.before;
|
|
6658
|
+
const after = input.after;
|
|
6659
|
+
if (before === null || after === null) return null;
|
|
6660
|
+
if (before.length < 200) return null;
|
|
6661
|
+
if (after.length >= before.length * .5) return null;
|
|
6662
|
+
return `Content kept ${Math.round(after.length / before.length * 100)}% of the previous body (${after.length} of ${before.length} characters); the replaced version is preserved in this skill's history.`;
|
|
6663
|
+
}
|
|
6664
|
+
/**
|
|
6665
|
+
* Read one skill's versions, oldest first.
|
|
6666
|
+
* @param root - the skills root.
|
|
6667
|
+
* @param io - the io seam.
|
|
6668
|
+
* @param name - the skill name.
|
|
6669
|
+
* @returns the versions; an absent or unreadable index reads as empty (never a throw).
|
|
6670
|
+
*/
|
|
6671
|
+
async function loadVersions(root, io, name) {
|
|
6672
|
+
return parseHistoryIndex(await io.readText(historyIndexFile(root, name)).catch(() => null));
|
|
6673
|
+
}
|
|
6674
|
+
/**
|
|
6675
|
+
* One version's content.
|
|
6676
|
+
* @param root - the skills root.
|
|
6677
|
+
* @param io - the io seam.
|
|
6678
|
+
* @param name - the skill name.
|
|
6679
|
+
* @param v - the version number.
|
|
6680
|
+
* @returns the content, or null when the version or its blob cannot be read (never '').
|
|
6681
|
+
*/
|
|
6682
|
+
async function loadVersionContent(root, io, name, v) {
|
|
6683
|
+
const entry = (await loadVersions(root, io, name)).find((candidate) => candidate.v === v);
|
|
6684
|
+
if (entry === void 0) return null;
|
|
6685
|
+
return await io.readText(blobPath(root, entry.hash)).catch(() => null);
|
|
6686
|
+
}
|
|
6687
|
+
/**
|
|
6688
|
+
* Record the content one mutation produced, best-effort.
|
|
6689
|
+
*
|
|
6690
|
+
* Blobs are written FIRST: the index must never reference content that is not on disk, and a failed
|
|
6691
|
+
* blob write aborts the whole record (the caller warns; the mutation itself stands).
|
|
6692
|
+
* @param root - the skills root.
|
|
6693
|
+
* @param io - the io seam.
|
|
6694
|
+
* @param input - the write being recorded.
|
|
6695
|
+
* @param keep - how many versions to retain per skill.
|
|
6696
|
+
* @returns the version numbers, or null when there was no content to record.
|
|
6697
|
+
*/
|
|
6698
|
+
async function recordVersions(root, io, input, keep) {
|
|
6699
|
+
if (input.before === null && input.after === null) return null;
|
|
6700
|
+
for (const side of [input.before, input.after]) {
|
|
6701
|
+
if (side === null) continue;
|
|
6702
|
+
const path = blobPath(root, contentHash(side));
|
|
6703
|
+
if (!await io.exists(path).catch(() => false)) await io.writeText(path, side);
|
|
6704
|
+
}
|
|
6705
|
+
const file = historyIndexFile(root, input.skillName);
|
|
6706
|
+
if (readHistoryIndex(await io.readText(file).catch(() => null)).kind === "unreadable") {
|
|
6707
|
+
const quarantine = `${file}.corrupt`;
|
|
6708
|
+
await io.copy(file, quarantine);
|
|
6709
|
+
console.warn(`skill-store: ${file} could not be understood (malformed, foreign, or written by a newer reader); its bytes were copied to ${quarantine} and a fresh index starts from this write`);
|
|
6710
|
+
}
|
|
6711
|
+
const recorded = {};
|
|
6712
|
+
await transactIo(io, file, (current) => {
|
|
6713
|
+
const state = readHistoryIndex(current);
|
|
6714
|
+
const { versions, recorded: numbers } = nextHistoryIndex(state.kind === "ok" ? state.versions : [], input, keep);
|
|
6715
|
+
recorded.beforeVersion = numbers.beforeVersion;
|
|
6716
|
+
recorded.afterVersion = numbers.afterVersion;
|
|
6717
|
+
return JSON.stringify({
|
|
6718
|
+
version: 1,
|
|
6719
|
+
versions
|
|
6720
|
+
}, null, 2);
|
|
6721
|
+
});
|
|
6722
|
+
return recorded;
|
|
6723
|
+
}
|
|
6724
|
+
//#endregion
|
|
6263
6725
|
//#region lib/types/fuzzy-match.js
|
|
6264
6726
|
/**
|
|
6265
6727
|
* Fuzzy string matching and replacement for patch/restructure edits of skill files.
|
|
@@ -6780,6 +7242,9 @@ var SkillLibrary = class {
|
|
|
6780
7242
|
/** V10-03 (P2-18): see the constructor's threatExemptLabels. Empty by
|
|
6781
7243
|
* default — the strict ANY-hit-blocks policy is unchanged. */
|
|
6782
7244
|
threatExemptLabels;
|
|
7245
|
+
/** skill-history.ts: the content history is best-effort, so a failure must not be SILENT —
|
|
7246
|
+
* but it is a deployment condition, not a per-write event, so it warns once per instance. */
|
|
7247
|
+
historyWarned = false;
|
|
6783
7248
|
constructor(root = skillsRoot(), io = nodeEvolutionIo(), limits = DEFAULT_SKILL_LIMITS, onMutation, transact, threatExemptLabels) {
|
|
6784
7249
|
this.root = root;
|
|
6785
7250
|
this.io = io;
|
|
@@ -6844,11 +7309,24 @@ var SkillLibrary = class {
|
|
|
6844
7309
|
message: "internal error: the write transaction did not invoke the task; no write was performed"
|
|
6845
7310
|
};
|
|
6846
7311
|
if (o.ghostDir === true) await this.cleanupGhostDir(path);
|
|
6847
|
-
|
|
7312
|
+
let result = o.result;
|
|
7313
|
+
if (o.write !== null && o.audit) {
|
|
7314
|
+
await this.audit(o.audit.skillName, o.audit.action, o.audit.before, o.audit.after, o.audit.summary);
|
|
7315
|
+
const retention = contentRetentionFeedback({
|
|
7316
|
+
name: o.audit.skillName,
|
|
7317
|
+
action: o.audit.action,
|
|
7318
|
+
before: o.audit.before,
|
|
7319
|
+
after: o.audit.after
|
|
7320
|
+
});
|
|
7321
|
+
if (retention !== null && result.ok) result = {
|
|
7322
|
+
...result,
|
|
7323
|
+
message: `${result.message}\n${retention}`
|
|
7324
|
+
};
|
|
7325
|
+
}
|
|
6848
7326
|
if (o.write !== null && o.event) this.notifyMutation(o.event);
|
|
6849
|
-
return durabilityWarning === "" || !
|
|
6850
|
-
...
|
|
6851
|
-
message: `${
|
|
7327
|
+
return durabilityWarning === "" || !result.ok ? result : {
|
|
7328
|
+
...result,
|
|
7329
|
+
message: `${result.message} (warning: the write landed but the directory fsync failed — durability unconfirmed: ${durabilityWarning})`
|
|
6852
7330
|
};
|
|
6853
7331
|
}
|
|
6854
7332
|
/**
|
|
@@ -7151,16 +7629,41 @@ var SkillLibrary = class {
|
|
|
7151
7629
|
return null;
|
|
7152
7630
|
}
|
|
7153
7631
|
}
|
|
7154
|
-
/** Best-effort audit trail entry; never blocks the mutation.
|
|
7632
|
+
/** Best-effort audit trail entry; never blocks the mutation.
|
|
7633
|
+
*
|
|
7634
|
+
* Two independent best-effort writes run here, because either one failing must not cost the
|
|
7635
|
+
* other: the CONTENT history (skill-history.ts, so a version can be read back at all) and the
|
|
7636
|
+
* mutation ledger that points at it. The history failure is not silent — a deployment whose
|
|
7637
|
+
* history stopped growing is a degradation worth seeing once (the ledger keeps hashes either
|
|
7638
|
+
* way). */
|
|
7155
7639
|
async audit(skillName, action, before, after, summary) {
|
|
7640
|
+
const at = (/* @__PURE__ */ new Date()).toISOString();
|
|
7641
|
+
let versions = null;
|
|
7642
|
+
try {
|
|
7643
|
+
const keep = this.limits.versionKeep ?? 20;
|
|
7644
|
+
versions = await recordVersions(this.root, this.io, {
|
|
7645
|
+
skillName,
|
|
7646
|
+
action,
|
|
7647
|
+
before,
|
|
7648
|
+
after,
|
|
7649
|
+
at
|
|
7650
|
+
}, keep);
|
|
7651
|
+
} catch (error) {
|
|
7652
|
+
if (!this.historyWarned) {
|
|
7653
|
+
this.historyWarned = true;
|
|
7654
|
+
console.warn(`skill-store: content history not recorded for "${skillName}" (${error instanceof Error ? error.message : String(error)}); the mutation itself landed and the audit ledger keeps its hashes`);
|
|
7655
|
+
}
|
|
7656
|
+
}
|
|
7156
7657
|
try {
|
|
7157
7658
|
await recordMutation(this.root, this.io, {
|
|
7158
7659
|
skillName,
|
|
7159
7660
|
action,
|
|
7160
7661
|
...before === null ? {} : { beforeHash: contentHash(before) },
|
|
7161
7662
|
...after === null ? {} : { afterHash: contentHash(after) },
|
|
7663
|
+
...versions?.beforeVersion === void 0 ? {} : { beforeVersion: versions.beforeVersion },
|
|
7664
|
+
...versions?.afterVersion === void 0 ? {} : { afterVersion: versions.afterVersion },
|
|
7162
7665
|
summary,
|
|
7163
|
-
at
|
|
7666
|
+
at
|
|
7164
7667
|
});
|
|
7165
7668
|
} catch {}
|
|
7166
7669
|
}
|
|
@@ -7169,6 +7672,32 @@ var SkillLibrary = class {
|
|
|
7169
7672
|
return await loadMutations(this.root, this.io);
|
|
7170
7673
|
}
|
|
7171
7674
|
/**
|
|
7675
|
+
* Content versions recorded for one skill, oldest first (skill-history.ts, batch A).
|
|
7676
|
+
*
|
|
7677
|
+
* Read-only: the version seam never feeds a judgment — callers use it to LIST and to read content
|
|
7678
|
+
* back. A malformed or unreadable index reads as empty, and a bad name reads as empty too (the
|
|
7679
|
+
* caller is a human command, so "no versions" is the honest answer for a name that cannot exist).
|
|
7680
|
+
* @param rawName - the skill's name.
|
|
7681
|
+
* @returns the versions, oldest first.
|
|
7682
|
+
*/
|
|
7683
|
+
async listVersions(rawName) {
|
|
7684
|
+
const name = rawName.trim();
|
|
7685
|
+
if (this.badName(name) !== null) return [];
|
|
7686
|
+
return await loadVersions(this.root, this.io, name);
|
|
7687
|
+
}
|
|
7688
|
+
/**
|
|
7689
|
+
* One version's content, read back from the content-addressed blob store.
|
|
7690
|
+
* @param rawName - the skill's name.
|
|
7691
|
+
* @param v - the version number (see {@link SkillLibrary.listVersions}).
|
|
7692
|
+
* @returns the content, or null when the version does not exist, the name is unusable, or the
|
|
7693
|
+
* blob cannot be read — never an empty string.
|
|
7694
|
+
*/
|
|
7695
|
+
async readVersion(rawName, v) {
|
|
7696
|
+
const name = rawName.trim();
|
|
7697
|
+
if (this.badName(name) !== null) return null;
|
|
7698
|
+
return await loadVersionContent(this.root, this.io, name, v);
|
|
7699
|
+
}
|
|
7700
|
+
/**
|
|
7172
7701
|
* Pin or unpin a skill (`.pinned` marker). Pinned skills are protected from
|
|
7173
7702
|
* deletion, from background-review writes, and from the lifecycle — a
|
|
7174
7703
|
* protective mutation, so the autonomous pipeline may never call it. The
|
|
@@ -8941,7 +9470,7 @@ var SkillLibrary = class {
|
|
|
8941
9470
|
throw new Error(`snapshot restore refused: the skill root could not be listed (${error instanceof Error ? error.message : String(error)}) — nothing was changed`);
|
|
8942
9471
|
}
|
|
8943
9472
|
for (const entry of rootEntries) {
|
|
8944
|
-
if (entry === ".archive" || entry === ".backups" || entry === ".mutations.json" || entry === ".curator-suppressed.json") continue;
|
|
9473
|
+
if (entry === ".archive" || entry === ".backups" || entry === ".mutations.json" || entry === ".curator-suppressed.json" || entry === ".history") continue;
|
|
8945
9474
|
if (entry.endsWith(".lock") || entry.endsWith(`.lock.next`)) {
|
|
8946
9475
|
await this.refuseLiveLockOrSweep(join(this.root, entry), entry);
|
|
8947
9476
|
continue;
|
|
@@ -8955,6 +9484,7 @@ var SkillLibrary = class {
|
|
|
8955
9484
|
const restoresSuppressed = manifest.sidecars.includes(".curator-suppressed.json");
|
|
8956
9485
|
for (const entry of rootEntries) {
|
|
8957
9486
|
if (entry === ".archive" || entry === ".backups" || entry === ".mutations.json") continue;
|
|
9487
|
+
if (entry === ".history") continue;
|
|
8958
9488
|
if (entry === ".curator-suppressed.json" && restoresSuppressed) continue;
|
|
8959
9489
|
if (entry.endsWith(".lock") || entry.endsWith(`.lock.next`)) continue;
|
|
8960
9490
|
await this.io.remove(join(this.root, entry));
|
|
@@ -9365,6 +9895,16 @@ const PARAM_EXPOSURE = Object.freeze([
|
|
|
9365
9895
|
docAnchor: "PARAMETERS.md#write-caps",
|
|
9366
9896
|
summary: "Report expired archives, or prune them."
|
|
9367
9897
|
},
|
|
9898
|
+
{
|
|
9899
|
+
id: "skillVersionKeep",
|
|
9900
|
+
group: "write-caps",
|
|
9901
|
+
tier: "E2",
|
|
9902
|
+
authority: "cordis",
|
|
9903
|
+
owner: "tool-skill-manage",
|
|
9904
|
+
applies: "none",
|
|
9905
|
+
docAnchor: "PARAMETERS.md#write-caps",
|
|
9906
|
+
summary: "Content versions retained per skill; older ones leave the index. Their stored blobs stay on disk — no automatic sweep exists yet, so a long-lived library grows by the distinct bodies it has written."
|
|
9907
|
+
},
|
|
9368
9908
|
{
|
|
9369
9909
|
id: "supportFileCharPolicy",
|
|
9370
9910
|
group: "write-caps",
|
|
@@ -10414,4 +10954,4 @@ function sessionAudited(ctx, sessionId, sessionScoped) {
|
|
|
10414
10954
|
return false;
|
|
10415
10955
|
}
|
|
10416
10956
|
//#endregion
|
|
10417
|
-
export { ALIVE_LOCK_TAKEOVER_MS, AUTHORING_DESCRIPTION_BAR, AUTHORING_SPLIT_LINE_CHARS, CITATION_SUPPORT_DIRS, COMBINED_REVIEW_PLAN_PROMPT, COMBINED_REVIEW_PROMPT, COMPLETION_SKILL_REVIEW_PROMPT, CONTENT_SPLIT_HINT, COST_ASCII_TOKEN_HIGH, COST_ASCII_TOKEN_LOW, COST_CJK_TOKEN_HIGH, COST_CJK_TOKEN_LOW, CURATOR_DRY_RUN_BANNER, CURATOR_PROMPT, DEAD_LOCK_TAKEOVER_MS, DEDUP_MAX_PAIR_COMPARISONS, DEFAULT_ARCHIVE_AFTER_DAYS, DEFAULT_ARCHIVE_RETENTION_POLICY, DEFAULT_CITATION_POLICY, DEFAULT_CITATION_REF_BUDGET, DEFAULT_CONSOLIDATION_FAILURES, DEFAULT_CURATOR_BOOT_GRACE_SECONDS, DEFAULT_CURATOR_INTERVAL_HOURS, DEFAULT_CURATOR_MODEL, DEFAULT_CURATOR_REVIEW_MAX_TOKENS, DEFAULT_HEALTH_THRESHOLDS, DEFAULT_MAX_OPS_PER_PLAN, DEFAULT_MEMORY_CHAR_LIMIT, DEFAULT_MEMORY_REVIEW_MODEL, DEFAULT_MIN_IDLE_HOURS, DEFAULT_MUTATION_CAP, DEFAULT_REFERENCE_REWRITE_POLICY, DEFAULT_REVIEW_CONTEXT_MESSAGES, DEFAULT_REVIEW_MEMORY_INTERVAL, DEFAULT_REVIEW_MESSAGE_CHARS, DEFAULT_REVIEW_SKILL_INTERVAL, DEFAULT_REVIEW_TIMEOUT_MS, DEFAULT_SKILL_CONTENT_CHARS, DEFAULT_SKILL_LIMITS, DEFAULT_SKILL_REVIEW_COMPLETION_MIN_TOOL_CALLS, DEFAULT_SKILL_REVIEW_MODEL, DEFAULT_SKILL_REVIEW_TRIGGER, DEFAULT_STALE_AFTER_DAYS, DEFAULT_SUBSTANTIVE_MIN_AGENT_CHARS, DEFAULT_SUBSTANTIVE_MIN_TOOL_CALLS, DEFAULT_SUBSTANTIVE_MIN_USER_CHARS, DEFAULT_SUPPORT_FILE_CHAR_POLICY, DEFAULT_SUPPORT_READ_TOOL_NAMES, DEFAULT_THREAT_MAX_SCAN_CHARS, DEFAULT_USER_CHAR_LIMIT, DISPATCH_EVENT_TYPES, DRIFT_MAX_LINE_CHARS, DRIFT_SIGNALS_VERSION, DRIFT_SIGNAL_NOUNS, DSH_AUTHORING_STANDARDS, EMPTY_LOCK_TAKEOVER_MS, ENTRY_DELIMITER, EVENT_ARCHIVE_PREFIX, EVENT_LOG_RETAIN_ARCHIVES, EVENT_LOG_ROTATE_AT, EVENT_LOG_VERSION, EVOLUTION_ERRORS, EVOLUTION_WRITE_TOOLS, EvolutionGateSet, FAMILY_SESSION_TOOL_NAMES, FORBIDDEN_CONTROL_KEYS, HEALTH_STAMP_RE, HOOK_LINE_RE, INSTANCE_KEYS, KEEP_LINE_RE, LOCK_BODY_RE, LOCK_SUFFIX, LOCK_TEAR_TAKEOVER_MS, LOW_QUALITY_THRESHOLD, LostWriteLock, MAINTAIN_OUTPUT_INSTRUCTION, MAINTAIN_PROMPT, MAX_DESCRIPTION_LENGTH, MAX_RESTRUCTURE_MOVES, MAX_SKILL_CONTENT_CHARS, MAX_SKILL_FILE_BYTES, MAX_SKILL_NAME_LENGTH, MAX_SUPPORT_READ_PATHS, MAX_TIMER_DELAY_MS, MEMORY_GUIDANCE_SECTION_ORDER, MEMORY_REVIEW_PROMPT, MIN_STAMP_BODY_CHARS, MUTATIONS_FILE_VERSION, MemoryStore, NATIVE_CALL_EVENT, NATIVE_RESULT_EVENT, PARAM_ALIASES, PARAM_EXPOSURE, PARAM_GROUP_LABELS, PARAM_NAMESPACES, PATTERN_OVERLAP, PLATFORM_CATALOG_DESCRIPTION_DEFAULT, PLATFORM_ESTIMATE_CHARS_PER_TOKEN, POLICY_STAGE_DEFAULTS, PROMPT_BUNDLE, PROMPT_BUNDLE_ID, PROMPT_BUNDLE_VERSION, PROTECTED_BUILTIN_SKILLS, PTC_DISPATCH_EVENT, PTC_DISPATCH_START_EVENT, QUALITY_WEIGHTS, READ_REQUIRED_ACTIONS, RESTRUCTURE_TARGET_RE, SKILLS_GUIDANCE, SKILLS_GUIDANCE_SECTION_ORDER, SKILL_ACTION_REQUIRED_FIELDS, SKILL_NAME_RE, SKILL_REVIEW_PLAN_PROMPT, SKILL_REVIEW_PROMPT, SNAPSHOT_EXTRA_NAME_RE, SUPPORT_DIRS, SUPPRESSED_FILE_VERSION, SkillLibrary, THREAT_EXEMPTION_HINT, TOKEN_ASCII_WEIGHT, TOKEN_CJK_WEIGHT, ToolDispatchNormalizer, ToolDispatchPayloadError, UPSTREAM_LIMIT_CHARS_PER_TOKEN, advanceReview, allowRowCollisions, appendEvolutionEvent, applyCuratorLifecycleFields, applyCuratorMetaFields, applyReferenceRewrite, assertDispatchPayload, assertSkillsRootAliasRetired, assessStructureHealth, authoringFeedback, bodyCost, buildCuratorRunReport, buildLearnPrompt, bumpPatch, bumpSupportRead, bumpUse, bumpView, callingScope, canonicalWriteId, claimInstance, clampedNumber, clearReviewChannel, collectReadSkillNames, composePresetComposition, computeDedupGroups, computeDriftSignals, computeLifecycleTransitions, computePrefixClusters, computeQualityScores, computeScopeView, contentHash, countDispatches, createGateSet, daysSinceIso, decideTakeover, describeReferenceRewrite, duplicateHeadings, emptyRecord, errorText, evaluateThreat, eventsFile, evolutionEventPayloadIssue, evolutionHome, evolutionIoAdapter, evolutionRoot, filterUnreadSkillOps, findDriftSignal, foldCuratorFields, foldToolDispatches, foldTurn, frontmatterBlock, frontmatterCatalogInvalid, getRecord, idleDays, installParamSection, instanceClaimKey, instanceClaimedWriteSites, instanceHolder, isAbsent, isCommittedWarning, isDeprecatedParamId, isFileShapedPath, isMissingPath, isPresent, isProcessAlive, isProgramToolName, isReviewChannelSession, isSkillReadToolName, isSkillToolName, isUnknown, isUnreadWrite, latestActivityAt, lifecycleCandidate, listEventArchives, loadMutations, loadRowOverrides, loadSuppressedNames, loadUsage, makeSerialQueue, mapProbe, markAgentCreated, markReviewChannel, markerEntryName, memoryRoot, missingSupportPointers, mutateUsage, mutationsFile, narrowNameMatches, neutralizePromptVariables, newSkillLibrary, nodeEvolutionIo, normalizeFrontmatter, normalizeUsageRecord, observeEvent, overlongLines, paramNamespace, paramSectionOverrides, parseCuratorNominations, parseEvolutionEvents, parseFrontmatter, parseLockBody, pendingSelfCleanup, persistedWriteSite, persistedWriteSites, planReferenceRewrite, planRehoming, policyStageLimits, probeAbsent, probeList, probeMtime, probePresent, probeReason, probeUnknown, readDispatchSignal, readEvolutionEvents, readEvolutionTimeline, readNumberParam, readParam, recordMutation, redactSecrets, relatedSkillNames, releaseInstance, renameWithRetry, renderCuratorReportMarkdown, resolveCitations, resolveExecOrigins, resolveOrigins, resolveParamId, resolveRootConfig, resolveSkillsRoot, retainEventArchives, retirementReport, reviewPrompt, saveSuppressedNames, saveUsage, scanBodyHooks, scanContentThreats, scanMemoryThreats, scanThreats, scopedProbeReport, sessionAudited, sessionReadSkillNames, sessionSeesFamilyTools, skillReadNameOf, skillsRoot, supportFileReadOf, suppressedFile, sweepReviewChannelSessions, tokenLineFor, transactIo, transactTaskGuard, unhookedSupportPointers, updateSuppressedNames, usageFile, usageObserved, validateFrontmatter, validateRestructureTarget, valueOr, verifyPromptBundle, writeDurableTmp, yamlPlainScalarNeedsQuotes };
|
|
10957
|
+
export { ALIVE_LOCK_TAKEOVER_MS, AUTHORING_DESCRIPTION_BAR, AUTHORING_SPLIT_LINE_CHARS, BASELINE_ACTION, CITATION_SUPPORT_DIRS, COMBINED_REVIEW_PLAN_PROMPT, COMBINED_REVIEW_PROMPT, COMPLETION_SKILL_REVIEW_PROMPT, CONTENT_SPLIT_HINT, COST_ASCII_TOKEN_HIGH, COST_ASCII_TOKEN_LOW, COST_CJK_TOKEN_HIGH, COST_CJK_TOKEN_LOW, CURATOR_DRY_RUN_BANNER, CURATOR_PROMPT, DEAD_LOCK_TAKEOVER_MS, DEDUP_MAX_PAIR_COMPARISONS, DEDUP_SIMILARITY_THRESHOLD, DEFAULT_ARCHIVE_AFTER_DAYS, DEFAULT_ARCHIVE_RETENTION_POLICY, DEFAULT_CITATION_POLICY, DEFAULT_CITATION_REF_BUDGET, DEFAULT_CONSOLIDATION_FAILURES, DEFAULT_CURATOR_BOOT_GRACE_SECONDS, DEFAULT_CURATOR_INTERVAL_HOURS, DEFAULT_CURATOR_MODEL, DEFAULT_CURATOR_REVIEW_MAX_TOKENS, DEFAULT_HEALTH_THRESHOLDS, DEFAULT_MAX_OPS_PER_PLAN, DEFAULT_MEMORY_CHAR_LIMIT, DEFAULT_MEMORY_REVIEW_MODEL, DEFAULT_MIN_IDLE_HOURS, DEFAULT_MUTATION_CAP, DEFAULT_REFERENCE_REWRITE_POLICY, DEFAULT_REVIEW_CONTEXT_MESSAGES, DEFAULT_REVIEW_MEMORY_INTERVAL, DEFAULT_REVIEW_MESSAGE_CHARS, DEFAULT_REVIEW_SKILL_INTERVAL, DEFAULT_REVIEW_TIMEOUT_MS, DEFAULT_SKILL_CONTENT_CHARS, DEFAULT_SKILL_LIMITS, DEFAULT_SKILL_REVIEW_COMPLETION_MIN_TOOL_CALLS, DEFAULT_SKILL_REVIEW_MODEL, DEFAULT_SKILL_REVIEW_TRIGGER, DEFAULT_SKILL_VERSION_KEEP, DEFAULT_STALE_AFTER_DAYS, DEFAULT_SUBSTANTIVE_MIN_AGENT_CHARS, DEFAULT_SUBSTANTIVE_MIN_TOOL_CALLS, DEFAULT_SUBSTANTIVE_MIN_USER_CHARS, DEFAULT_SUPPORT_FILE_CHAR_POLICY, DEFAULT_SUPPORT_READ_TOOL_NAMES, DEFAULT_THREAT_MAX_SCAN_CHARS, DEFAULT_USER_CHAR_LIMIT, DISPATCH_EVENT_TYPES, DRIFT_MAX_LINE_CHARS, DRIFT_SIGNALS_VERSION, DRIFT_SIGNAL_NOUNS, DSH_AUTHORING_STANDARDS, EMPTY_LOCK_TAKEOVER_MS, ENTRY_DELIMITER, EVENT_ARCHIVE_PREFIX, EVENT_LOG_RETAIN_ARCHIVES, EVENT_LOG_ROTATE_AT, EVENT_LOG_VERSION, EVOLUTION_ERRORS, EVOLUTION_WRITE_TOOLS, EvolutionGateSet, FAMILY_SESSION_TOOL_NAMES, FORBIDDEN_CONTROL_KEYS, HEALTH_STAMP_RE, HISTORY_DIR, HISTORY_INDEX_VERSION, HOOK_LINE_RE, INSTANCE_KEYS, KEEP_LINE_RE, LOCK_BODY_RE, LOCK_SUFFIX, LOCK_TEAR_TAKEOVER_MS, LOW_QUALITY_THRESHOLD, LostWriteLock, MAINTAIN_OUTPUT_INSTRUCTION, MAINTAIN_PROMPT, MAX_DESCRIPTION_LENGTH, MAX_RESTRUCTURE_MOVES, MAX_SKILL_CONTENT_CHARS, MAX_SKILL_FILE_BYTES, MAX_SKILL_NAME_LENGTH, MAX_SUPPORT_READ_PATHS, MAX_TIMER_DELAY_MS, MEMORY_GUIDANCE_SECTION_ORDER, MEMORY_REVIEW_PROMPT, MIN_STAMP_BODY_CHARS, MUTATIONS_FILE_VERSION, MemoryStore, NATIVE_CALL_EVENT, NATIVE_RESULT_EVENT, PARAM_ALIASES, PARAM_EXPOSURE, PARAM_GROUP_LABELS, PARAM_NAMESPACES, PATTERN_OVERLAP, PLATFORM_CATALOG_DESCRIPTION_DEFAULT, PLATFORM_ESTIMATE_CHARS_PER_TOKEN, POLICY_STAGE_DEFAULTS, PROMPT_BUNDLE, PROMPT_BUNDLE_ID, PROMPT_BUNDLE_VERSION, PROTECTED_BUILTIN_SKILLS, PTC_DISPATCH_EVENT, PTC_DISPATCH_START_EVENT, QUALITY_WEIGHTS, READ_REQUIRED_ACTIONS, RESTRUCTURE_TARGET_RE, RETENTION_FEEDBACK_KEEP_RATIO, RETENTION_FEEDBACK_MIN_CHARS, SKILLS_GUIDANCE, SKILLS_GUIDANCE_SECTION_ORDER, SKILL_ACTION_REQUIRED_FIELDS, SKILL_NAME_RE, SKILL_REVIEW_PLAN_PROMPT, SKILL_REVIEW_PROMPT, SNAPSHOT_EXTRA_NAME_RE, SUMMARY_DUPLICATE_HINT_THRESHOLD, SUPPORT_DIRS, SUPPRESSED_FILE_VERSION, SkillLibrary, THREAT_EXEMPTION_HINT, TOKEN_ASCII_WEIGHT, TOKEN_CJK_WEIGHT, ToolDispatchNormalizer, ToolDispatchPayloadError, UPSTREAM_LIMIT_CHARS_PER_TOKEN, advanceReview, affinity, allowRowCollisions, appendEvolutionEvent, applyCuratorLifecycleFields, applyCuratorMetaFields, applyReferenceRewrite, assertDispatchPayload, assertSkillsRootAliasRetired, assessStructureHealth, authoringFeedback, blobPath, bodyCost, buildCuratorRunReport, buildLearnPrompt, bumpPatch, bumpSupportRead, bumpUse, bumpView, callingScope, canonicalWriteId, claimInstance, clampedNumber, clearReviewChannel, collectReadSkillNames, composePresetComposition, computeDedupGroups, computeDriftSignals, computeLifecycleTransitions, computePrefixClusters, computeQualityScores, computeScopeView, contentHash, contentRetentionFeedback, countDispatches, createGateSet, daysSinceIso, decideTakeover, describeReferenceRewrite, duplicateHeadings, emptyRecord, errorText, evaluateThreat, eventsFile, evidenceKindIndex, evolutionEventPayloadIssue, evolutionHome, evolutionIoAdapter, evolutionRoot, filterUnreadSkillOps, findDriftSignal, foldCuratorFields, foldToolDispatches, foldTurn, frontmatterBlock, frontmatterCatalogInvalid, getRecord, historyIndexFile, historyRoot, identity, idleDays, installParamSection, instanceClaimKey, instanceClaimedWriteSites, instanceHolder, isAbsent, isCommittedWarning, isDeprecatedParamId, isFileShapedPath, isMissingPath, isPresent, isProcessAlive, isProgramToolName, isReviewChannelSession, isSkillReadToolName, isSkillToolName, isUnknown, isUnreadWrite, latestActivityAt, lifecycleCandidate, listEventArchives, loadMutations, loadRowOverrides, loadSuppressedNames, loadUsage, loadVersionContent, loadVersions, makeSerialQueue, mapProbe, markAgentCreated, markReviewChannel, markerEntryName, memoryRoot, missingSupportPointers, mutateUsage, mutationsFile, narrowNameMatches, nearDuplicateSummaries, neutralizePromptVariables, newSkillLibrary, nextHistoryIndex, nodeEvolutionIo, normalizeFrontmatter, normalizeUsageRecord, observeEvent, orderVersions, overlongLines, paramNamespace, paramSectionOverrides, parseCuratorNominations, parseEvolutionEvents, parseFrontmatter, parseHistoryIndex, parseLockBody, partitionVersions, pendingSelfCleanup, persistedWriteSite, persistedWriteSites, planReferenceRewrite, planRehoming, policyStageLimits, probeAbsent, probeList, probeMtime, probePresent, probeReason, probeUnknown, projectionText, readDispatchSignal, readEvolutionEvents, readEvolutionTimeline, readHistoryIndex, readNumberParam, readParam, recordMutation, recordVersions, redactSecrets, relatedSkillNames, releaseInstance, renameWithRetry, renderCuratorReportMarkdown, resolveCitations, resolveExecOrigins, resolveOrigins, resolveParamId, resolveRootConfig, resolveSkillsRoot, retainEventArchives, retirementReport, reviewPrompt, saveSuppressedNames, saveUsage, scanBodyHooks, scanContentThreats, scanMemoryThreats, scanThreats, scopedProbeReport, sessionAudited, sessionReadSkillNames, sessionSeesFamilyTools, skillReadNameOf, skillsRoot, supportFileReadOf, suppressedFile, sweepReviewChannelSessions, tokenLineFor, transactIo, transactTaskGuard, unhookedSupportPointers, updateSuppressedNames, usageFile, usageObserved, validateFrontmatter, validateRestructureTarget, valueOr, verifyPromptBundle, versionTarget, vocabulary, writeDurableTmp, yamlPlainScalarNeedsQuotes };
|
package/lib/types/constants.d.ts
CHANGED
|
@@ -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
|
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
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
* `quality.ts`, `signals.ts`, `skill-reads.ts` (the read-before-write rule, shared by the
|
|
24
24
|
* tool path and the review's plan path), `drift-signals.ts`, `skill-health.ts`,
|
|
25
25
|
* `preset-composition.ts`, `prompts.ts`, `learn-prompt.ts`,
|
|
26
|
-
* `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).
|
|
27
28
|
* @module @lmzhen/dsh-evolution-core
|
|
28
29
|
*/
|
|
29
30
|
export * from './citations.ts';
|
|
@@ -52,7 +53,9 @@ export * from './scope.ts';
|
|
|
52
53
|
export * from './skill-health.ts';
|
|
53
54
|
export * from './signals.ts';
|
|
54
55
|
export * from './skill-reads.ts';
|
|
56
|
+
export * from './evidence.ts';
|
|
55
57
|
export * from './drift-signals.ts';
|
|
58
|
+
export * from './skill-history.ts';
|
|
56
59
|
export * from './skill-store.ts';
|
|
57
60
|
export * from './state-store.ts';
|
|
58
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/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,226 @@
|
|
|
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
|
+
/** Which artifact one recorded version holds.
|
|
87
|
+
*
|
|
88
|
+
* `content` is the skill's own SKILL.md body. `support` is a support file's bytes: a `write_file`
|
|
89
|
+
* or `remove_file` mutation stores them in the SAME index as the body's versions, so a skill that
|
|
90
|
+
* ever wrote a support file has two chains in one index. `other` is an entry whose action does not say
|
|
91
|
+
* which artifact the bytes belong to — a `baseline` predecessor found on disk, or an action that
|
|
92
|
+
* records no content of its own (`pin`/`unpin`/`archive` record only predecessors).
|
|
93
|
+
*
|
|
94
|
+
* The split is FAIL-CLOSED: an action this module does not know reads as `other` rather than being
|
|
95
|
+
* assumed to be the body, so a caller that offers "restore this version" never advertises bytes the
|
|
96
|
+
* index cannot vouch for. `tests/skill-history.spec.ts` pins every action the library can write, so a
|
|
97
|
+
* new one cannot land unclassified. */
|
|
98
|
+
export type VersionTarget = 'content' | 'support' | 'other';
|
|
99
|
+
/**
|
|
100
|
+
* Which artifact one recorded version holds, from the action that produced it.
|
|
101
|
+
* @param action - the entry's action label.
|
|
102
|
+
* @returns the artifact class; `other` when the label does not say.
|
|
103
|
+
*/
|
|
104
|
+
export declare function versionTarget(action: string): VersionTarget;
|
|
105
|
+
/** The versions of one skill, split by the artifact they hold. */
|
|
106
|
+
export interface VersionGroups {
|
|
107
|
+
/** The body's versions, plus predecessors whose artifact the index cannot name. */
|
|
108
|
+
readonly content: SkillVersion[];
|
|
109
|
+
/** Support-file versions: real history, but not restorable over SKILL.md. */
|
|
110
|
+
readonly support: SkillVersion[];
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Split one index into the skill body's chain and the support files' versions.
|
|
114
|
+
*
|
|
115
|
+
* Both share ONE index, so a skill that ever wrote a support file has two chains and
|
|
116
|
+
* {@link orderVersions} cannot account for every entry (it falls back to the stored order). Splitting
|
|
117
|
+
* first lets each group rebuild its own order, and keeps the body's chain intact by leaving every
|
|
118
|
+
* entry the index cannot classify in it — a `baseline` predecessor is usually the body's first link.
|
|
119
|
+
* @param versions - the stored index, oldest first as recorded.
|
|
120
|
+
* @returns the two groups, each in content order when its chain is complete.
|
|
121
|
+
*/
|
|
122
|
+
export declare function partitionVersions(versions: readonly SkillVersion[]): VersionGroups;
|
|
123
|
+
/** What one index file's bytes said. `absent` (no file yet) and `unreadable` (bytes that cannot be
|
|
124
|
+
* understood: malformed JSON, a foreign shape, an entry this reader would have to drop, or a writer
|
|
125
|
+
* NEWER than this reader) are DIFFERENT facts — the recorder must never replace the second with an
|
|
126
|
+
* index derived from "nothing", so it preserves those bytes instead. */
|
|
127
|
+
export type HistoryIndexState = {
|
|
128
|
+
readonly kind: 'ok';
|
|
129
|
+
readonly versions: SkillVersion[];
|
|
130
|
+
} | {
|
|
131
|
+
readonly kind: 'absent';
|
|
132
|
+
} | {
|
|
133
|
+
readonly kind: 'unreadable';
|
|
134
|
+
};
|
|
135
|
+
/**
|
|
136
|
+
* Read one index body for a RECORDER: "understood", "no file", or "cannot be understood".
|
|
137
|
+
* A file whose shape is right but which carries an entry this reader would have to drop counts as
|
|
138
|
+
* unreadable — dropping it silently would be the overwrite the audit posture forbids.
|
|
139
|
+
* @param raw - the index file's bytes, or null when absent.
|
|
140
|
+
* @returns the three-state answer.
|
|
141
|
+
*/
|
|
142
|
+
export declare function readHistoryIndex(raw: string | null): HistoryIndexState;
|
|
143
|
+
/**
|
|
144
|
+
* Parse one index body for a READER. Malformed content, a foreign shape, or a version NEWER than this
|
|
145
|
+
* reader all read as empty — the audit posture: never guess. A recorder must use
|
|
146
|
+
* {@link readHistoryIndex} instead, because it has to PRESERVE bytes it cannot understand.
|
|
147
|
+
* @param raw - the index file's bytes, or null when absent.
|
|
148
|
+
* @returns the versions, oldest first.
|
|
149
|
+
*/
|
|
150
|
+
export declare function parseHistoryIndex(raw: string | null): SkillVersion[];
|
|
151
|
+
/**
|
|
152
|
+
* The index after this write, and the version numbers it produced. Pure, so the versioning rules
|
|
153
|
+
* are testable without io.
|
|
154
|
+
*
|
|
155
|
+
* Two rules matter here: a predecessor the index has never seen gets a BASELINE entry (so the state
|
|
156
|
+
* before this mutation is recoverable even for a skill that predates the feature), and re-writing
|
|
157
|
+
* identical content mints NO new version (the library's no-op updates must not grow history).
|
|
158
|
+
* @param versions - the current index, oldest first.
|
|
159
|
+
* @param input - the write being recorded.
|
|
160
|
+
* @param keep - how many versions to retain per skill.
|
|
161
|
+
* @returns the trimmed index and the recorded numbers.
|
|
162
|
+
*/
|
|
163
|
+
export declare function nextHistoryIndex(versions: readonly SkillVersion[], input: VersionRecordInput, keep: number): {
|
|
164
|
+
versions: SkillVersion[];
|
|
165
|
+
recorded: RecordedVersions;
|
|
166
|
+
};
|
|
167
|
+
/** Shrink level at which the retention line speaks: a body that keeps less than this share of the
|
|
168
|
+
* previous one is a rewrite, not an edit. */
|
|
169
|
+
export declare const RETENTION_FEEDBACK_KEEP_RATIO = 0.5;
|
|
170
|
+
/** Floor on the REPLACED body: below this a ratio is noise (a 40-character skill cut in half is not
|
|
171
|
+
* news), so the line stays off for small skills. */
|
|
172
|
+
export declare const RETENTION_FEEDBACK_MIN_CHARS = 200;
|
|
173
|
+
/** What {@link contentRetentionFeedback} reads: the one write it describes. */
|
|
174
|
+
export interface RetentionFeedbackInput {
|
|
175
|
+
/** The skill's name, for the sentence. */
|
|
176
|
+
readonly name: string;
|
|
177
|
+
/** The action that produced the write; only a whole-body replacement is described. */
|
|
178
|
+
readonly action: string;
|
|
179
|
+
/** The replaced body, or null when the skill had none (create / restore). */
|
|
180
|
+
readonly before: string | null;
|
|
181
|
+
/** The body now on disk, or null when it was removed (archive / remove_file). */
|
|
182
|
+
readonly after: string | null;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* The anti-overwrite feedback line (design §4, item 3): says how much of the previous body a
|
|
186
|
+
* replacement kept, so a destructive rewrite is visible in the same message that reports the write.
|
|
187
|
+
*
|
|
188
|
+
* Computed where the replaced bytes are still in hand — the write path's own in-lock read — instead of
|
|
189
|
+
* re-reading the file from a caller (that second read would race the writers this library serializes).
|
|
190
|
+
* It is FEEDBACK, never a gate: no ratio refuses a write, and the function owns no configuration
|
|
191
|
+
* (a toggle nobody reads would be configuration that does nothing).
|
|
192
|
+
*
|
|
193
|
+
* @param input - the write being described.
|
|
194
|
+
* @returns the line, or null when the write is not a significant whole-body replacement.
|
|
195
|
+
*/
|
|
196
|
+
export declare function contentRetentionFeedback(input: RetentionFeedbackInput): string | null;
|
|
197
|
+
/**
|
|
198
|
+
* Read one skill's versions, oldest first.
|
|
199
|
+
* @param root - the skills root.
|
|
200
|
+
* @param io - the io seam.
|
|
201
|
+
* @param name - the skill name.
|
|
202
|
+
* @returns the versions; an absent or unreadable index reads as empty (never a throw).
|
|
203
|
+
*/
|
|
204
|
+
export declare function loadVersions(root: string, io: EvolutionIoLike, name: string): Promise<SkillVersion[]>;
|
|
205
|
+
/**
|
|
206
|
+
* One version's content.
|
|
207
|
+
* @param root - the skills root.
|
|
208
|
+
* @param io - the io seam.
|
|
209
|
+
* @param name - the skill name.
|
|
210
|
+
* @param v - the version number.
|
|
211
|
+
* @returns the content, or null when the version or its blob cannot be read (never '').
|
|
212
|
+
*/
|
|
213
|
+
export declare function loadVersionContent(root: string, io: EvolutionIoLike, name: string, v: number): Promise<string | null>;
|
|
214
|
+
/**
|
|
215
|
+
* Record the content one mutation produced, best-effort.
|
|
216
|
+
*
|
|
217
|
+
* Blobs are written FIRST: the index must never reference content that is not on disk, and a failed
|
|
218
|
+
* blob write aborts the whole record (the caller warns; the mutation itself stands).
|
|
219
|
+
* @param root - the skills root.
|
|
220
|
+
* @param io - the io seam.
|
|
221
|
+
* @param input - the write being recorded.
|
|
222
|
+
* @param keep - how many versions to retain per skill.
|
|
223
|
+
* @returns the version numbers, or null when there was no content to record.
|
|
224
|
+
*/
|
|
225
|
+
export declare function recordVersions(root: string, io: EvolutionIoLike, input: VersionRecordInput, keep: number): Promise<RecordedVersions | null>;
|
|
226
|
+
//# sourceMappingURL=skill-history.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