@lmzhen/dsh-evolution-core 0.3.82 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -56
- package/lib/index.js +156 -59
- package/lib/types/frontmatter.d.ts +4 -1
- package/lib/types/memory-store.d.ts +4 -0
- package/lib/types/quality.d.ts +26 -1
- package/lib/types/scope.d.ts +0 -3
- package/lib/types/skill-store.d.ts +10 -1
- package/lib/types/state-store.d.ts +15 -9
- package/lib/types/usage.d.ts +17 -0
- package/lib/types/write-inventory.d.ts +8 -2
- package/package.json +2 -3
package/README.md
CHANGED
|
@@ -1,65 +1,30 @@
|
|
|
1
|
-
# @
|
|
1
|
+
# @lmzhen/dsh-evolution-core
|
|
2
2
|
|
|
3
|
-
Shared pure library for the
|
|
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
|
|
6
|
+
section, and importing the package root is its only entry.
|
|
4
7
|
|
|
5
|
-
## Model
|
|
8
|
+
## Model surface
|
|
6
9
|
|
|
7
|
-
|
|
10
|
+
- **Model-visible:** nothing of its own: the guidance text and memory snapshot it renders are injected by the rows that consume it (`tool-memory`, `tool-skill-manage`).
|
|
11
|
+
- **Prompt prefix / KV cache:** unchanged by this package: family-level rules single-sourced in `packages/README.md` §"Model-visible prompt prefix and the KV cache".
|
|
12
|
+
- **Mount it?** no: a library, not a row; consumed by the family's rows (e.g. `evolution-threat`, `evolution-policy`, `memory-files`, `skill-usage`, `evolution-state-json`).
|
|
8
13
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
`@deepseek-ai/dsh-evolution-core` registers no direct prompt or tool schema itself. Model-visible effects are owned by the packages that consume this service.
|
|
12
|
-
|
|
13
|
-
#### Token effect
|
|
14
|
-
|
|
15
|
-
Zero direct token effect from this package; consumers add any model-visible tokens.
|
|
16
|
-
|
|
17
|
-
#### KV Cache effect
|
|
18
|
-
|
|
19
|
-
Independent of request-prefix construction. This package does not alter the assembled prompt or tool list.
|
|
20
|
-
|
|
21
|
-
## SkillLibrary concurrency model
|
|
22
|
-
|
|
23
|
-
Skill-library mutations are read-modify-write on one file, so `SkillLibrary`
|
|
24
|
-
serializes them in-process with a `makeSerialQueue` chain: `update`, `patch`,
|
|
25
|
-
`restructure`, `writeSupportFile` and — since 0.3.46 — `consolidate`'s target
|
|
26
|
-
read→merge→commit run their whole read→validate→write under one serial task,
|
|
27
|
-
so two concurrent mutators on one skill never interleave in this process.
|
|
28
|
-
Single-file writes (`update`, `patch`, `writeSupportFile`)
|
|
29
|
-
additionally run the read and the write inside `transactIo` when a caller
|
|
30
|
-
injects a `transact` into the constructor — that is the cross-process lock, so
|
|
31
|
-
two processes sharing `DSH_HOME` cannot interleave their RMW on one file.
|
|
32
|
-
`create` is INSIDE the serial chain and, when a transact backend is bound, its
|
|
33
|
-
exists check runs inside the same per-file transact (v18); `archive`/`consolidate`
|
|
34
|
-
are rename-based two-phase paths and stay outside the single-file serial chain.
|
|
35
|
-
|
|
36
|
-
**v18 residual (updated):** `create`, `removeSupportFile` and `setPinned` now
|
|
37
|
-
run on the serial chain (`removeSupportFile`'s delete also goes through the
|
|
38
|
-
per-file transact), so the remaining single-file residual is the protection
|
|
39
|
-
TOCTOU (a marker check outside the transact) and the multi-file two-phase
|
|
40
|
-
paths `archive`/`consolidate`/`restructure`. The race needs a concurrent
|
|
41
|
-
mutator on the SAME skill file/package; the exposure is acknowledged and the
|
|
42
|
-
protection check is the next candidate (see the v18 optimization plan, E-5).
|
|
43
|
-
|
|
44
|
-
When the backend provides `transact` (nodeEvolutionIo and the io adapter do),
|
|
45
|
-
the constructor binds it BY DEFAULT since 0.3.27 — the single-file entry points
|
|
46
|
-
(`update`, `patch`, `writeSupportFile`, and each per-file piece of
|
|
47
|
-
`restructure`) run their read→write inside the cross-process lock for every
|
|
48
|
-
instantiation, so same-file concurrent writes from different processes no
|
|
49
|
-
longer resolve to last-writer-wins there. An explicit `transact` argument
|
|
50
|
-
overrides the default binding. The two-phase paths deliberately stay outside
|
|
51
|
-
that lock: `create`'s exists probe runs inside the transact when a transact
|
|
52
|
-
backend is bound (v18), so only a transact-less custom backend can still
|
|
53
|
-
double-pass the probe across processes; `archive`/`consolidate` are rename-based with best-effort
|
|
54
|
-
rollback (an archive loser's rollback surfaces the raw failure when the source
|
|
55
|
-
vanished), and `restructure`'s multi-file swap can expose an interleaved tree
|
|
56
|
-
to a concurrent reader. These residual windows are documented rather than
|
|
57
|
-
locked — cross-process writers to the SAME skill file should serialize through
|
|
58
|
-
the single-file paths above.
|
|
59
|
-
|
|
60
|
-
## Known Limitations and Deferred Work
|
|
14
|
+
## Known limitations
|
|
61
15
|
|
|
62
16
|
- This package is a library, not a Cordis row; do not mount it as a plugin.
|
|
63
17
|
- 数值配置已在消费方 Config 面钳制(`min 1`/各字段域);`MemoryStore` 内部对 `limit <= 0` 仍按 unbounded 防御处理——那是库内部防御,不构成"0 = 禁用"的配置语义。
|
|
18
|
+
- 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.
|
|
64
19
|
|
|
65
20
|
**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
|
+
|
|
22
|
+
## Notes and history
|
|
23
|
+
|
|
24
|
+
- 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
|
+
- `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
|
+
- **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`.
|
|
27
|
+
- The race needs a concurrent mutator on the SAME skill file/package; the exposure is acknowledged and the protection check is the next candidate (see the v18 optimization plan, E-5).
|
|
28
|
+
- When the backend provides `transact` (nodeEvolutionIo and the io adapter do), the constructor binds it BY DEFAULT since 0.3.27: the single-file entry points (`update`, `patch`, `writeSupportFile`, and each per-file piece of `restructure`) run their read→write inside the cross-process lock for every instantiation, so same-file concurrent writes from different processes no longer resolve to last-writer-wins there.
|
|
29
|
+
- The two-phase paths deliberately stay outside that lock: `create`'s exists probe runs inside the transact when a transact backend is bound (v18), so only a transact-less custom backend can still double-pass the probe across processes; `archive`/`consolidate` are rename-based with best-effort rollback (an archive loser's rollback surfaces the raw failure when the source vanished), and `restructure`'s multi-file swap can expose an interleaved tree to a concurrent reader.
|
|
30
|
+
- These residual windows are documented rather than locked: cross-process writers to the SAME skill file should serialize through the single-file paths above.
|
package/lib/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { basename, dirname,
|
|
1
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
2
2
|
import { cp, lstat, mkdir, open, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
3
3
|
import { createHash, randomBytes } from "node:crypto";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { readFileSync } from "node:fs";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { scopeOf } from "@deepseek-ai/dsh-scope";
|
|
8
|
-
import {
|
|
8
|
+
import { parse } from "yaml";
|
|
9
9
|
//#region lib/types/probe.js
|
|
10
10
|
function probePresent(value) {
|
|
11
11
|
return {
|
|
@@ -1030,19 +1030,37 @@ function parseSuppressed(raw) {
|
|
|
1030
1030
|
return /* @__PURE__ */ new Set();
|
|
1031
1031
|
}
|
|
1032
1032
|
}
|
|
1033
|
+
/**
|
|
1034
|
+
* Plain wholesale write of the suppression sidecar (tests and fixture seeding).
|
|
1035
|
+
* @internal S2.4 (PLAN 2026-09-16, audit P2-31): NO production consumer
|
|
1036
|
+
* (verified by grep; production suppressions go through
|
|
1037
|
+
* {@link updateSuppressedNames}) — exported for the family's tests only. Do
|
|
1038
|
+
* not use it to write the sidecar in new code.
|
|
1039
|
+
*
|
|
1040
|
+
* The write rides the {@link transactIo} channel (the same lock the RMW
|
|
1041
|
+
* writer uses), not the former naked read → `io.writeText`: that form was an
|
|
1042
|
+
* unserialized read-modify-write — the last `saveSuppressedNames` in this file
|
|
1043
|
+
* with no lock, while its sibling `saveUsage` already carried the `@internal`
|
|
1044
|
+
* test-only note. Version discipline is unchanged (V24-09 / S2-14): a read
|
|
1045
|
+
* failure surfaces (an unreadable sidecar is never written over), a newer
|
|
1046
|
+
* on-disk schema is warned about and preserved byte-for-byte (the
|
|
1047
|
+
* byte-identical result short-circuits the write), and a malformed sidecar is
|
|
1048
|
+
* still overwritten with the v1 shape (the historical plain-writer posture).
|
|
1049
|
+
*/
|
|
1033
1050
|
async function saveSuppressedNames(root, names, io = nodeEvolutionIo()) {
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1051
|
+
await transactIo(io, suppressedFile(root), (current) => {
|
|
1052
|
+
if (current !== null) try {
|
|
1053
|
+
const parsed = JSON.parse(current);
|
|
1054
|
+
if (parsed !== null && typeof parsed.version === "number" && parsed.version > 1) {
|
|
1055
|
+
console.warn(`suppression sidecar ${suppressedFile(root)} declares version ${String(parsed.version)} (newer than 1); not overwritten`);
|
|
1056
|
+
return current;
|
|
1057
|
+
}
|
|
1058
|
+
} catch {}
|
|
1059
|
+
return JSON.stringify({
|
|
1060
|
+
version: 1,
|
|
1061
|
+
names: [...names].sort()
|
|
1062
|
+
}, null, 2);
|
|
1063
|
+
});
|
|
1046
1064
|
}
|
|
1047
1065
|
/**
|
|
1048
1066
|
* Atomic read-modify-write on the suppression sidecar (rc.50 P2-2): `task`
|
|
@@ -1985,12 +2003,14 @@ function buildLearnPrompt(userRequest) {
|
|
|
1985
2003
|
* DSH home root: `$DSH_HOME` or `~/.dsh`. Single source of the empty-string
|
|
1986
2004
|
* fallback — an EMPTY or WHITESPACE-ONLY DSH_HOME resolves to the default
|
|
1987
2005
|
* home, never to a CWD-relative path (0.3.19 W1.3, 0.3.22 F-207);
|
|
1988
|
-
* V8-06 (0.3.47) extends the guard to whitespace
|
|
1989
|
-
*
|
|
1990
|
-
*
|
|
1991
|
-
*
|
|
1992
|
-
*
|
|
1993
|
-
*
|
|
2006
|
+
* V8-06 (0.3.47) extends the guard to whitespace.
|
|
2007
|
+
* C-11 correction (S2.2, PLAN 2026-09-16): upstream `resolveDshHome`
|
|
2008
|
+
* (util/home-paths) uses `trim()` ONLY as the ADOPTION test and then uses the
|
|
2009
|
+
* RAW env value — the earlier "same trimmed source" form returned the trimmed
|
|
2010
|
+
* text, so `DSH_HOME=" /x "` produced `/x` where upstream produced the literal
|
|
2011
|
+
* padded path. The value semantics now match upstream line for line:
|
|
2012
|
+
* `const selected = configured ?? (fromEnv !== undefined &&
|
|
2013
|
+
* fromEnv.trim().length > 0 ? fromEnv : defaultDshHome())`.
|
|
1994
2014
|
* OPT-27 (2026-09, plan D5 — accepted): the v10-era "no `~` expansion, no
|
|
1995
2015
|
* resolve" divergence from upstream `resolveDshHome` is RETIRED. It became
|
|
1996
2016
|
* load-bearing when the skill-catalog shadow made "same tree as the upstream
|
|
@@ -1998,15 +2018,18 @@ function buildLearnPrompt(userRequest) {
|
|
|
1998
2018
|
* absolute `<home>/skills` while this value fed a literal `~/x` (a directory
|
|
1999
2019
|
* named `~` under the host CWD) or a CWD-relative path — split-brain skill
|
|
2000
2020
|
* trees, preset installs the platform never reads, doctor probes of a
|
|
2001
|
-
* directory nothing serves.
|
|
2002
|
-
*
|
|
2003
|
-
*
|
|
2021
|
+
* directory nothing serves.
|
|
2022
|
+
* S2.2 (PLAN 2026-09-16), final correction: the result is ALWAYS
|
|
2023
|
+
* `resolve(expandHomePath(selected))` — the earlier form returned an
|
|
2024
|
+
* already-absolute value VERBATIM, so a trailing slash or `..` segment
|
|
2025
|
+
* landed unnormalized while upstream normalizes every value. Relative
|
|
2026
|
+
* `DSH_HOME` values change landing spot; a CLEAN absolute home stays
|
|
2027
|
+
* byte-identical (resolve is a no-op on it).
|
|
2004
2028
|
*/
|
|
2005
2029
|
function evolutionRoot(env = process.env) {
|
|
2006
|
-
const
|
|
2007
|
-
const selected =
|
|
2008
|
-
|
|
2009
|
-
return isAbsolute(expanded) ? expanded : resolve(expanded);
|
|
2030
|
+
const fromEnv = env.DSH_HOME;
|
|
2031
|
+
const selected = fromEnv !== void 0 && fromEnv.trim().length > 0 ? fromEnv : join(homedir(), ".dsh");
|
|
2032
|
+
return resolve(selected === "~" ? homedir() : selected.startsWith("~/") || selected.startsWith("~\\") ? join(homedir(), selected.slice(2)) : selected);
|
|
2010
2033
|
}
|
|
2011
2034
|
/** Evolution home path helper: `$DSH_HOME/evolution` for plugin-owned sidecar
|
|
2012
2035
|
* state (reports, activity store, feedback file, state-domain data). */
|
|
@@ -2192,7 +2215,7 @@ const PATTERNS = [
|
|
|
2192
2215
|
label: "read_secrets",
|
|
2193
2216
|
category: "exfiltration",
|
|
2194
2217
|
scope: "all",
|
|
2195
|
-
regex: /\bcat\s+[^\n]{0,512}(?:\.env(?!\w)|(?:\bcredentials\b)|\.netrc|\.pgpass|\.npmrc|\.pypirc)/i
|
|
2218
|
+
regex: /\bcat\s+[^\n]{0,512}(?:\.env(?!\w)(?!\.(?:[\w-]+\.)*(?:example|sample)(?=$|[\s"']))|(?:\bcredentials\b)|\.netrc|\.pgpass|\.npmrc|\.pypirc)/i
|
|
2196
2219
|
},
|
|
2197
2220
|
{
|
|
2198
2221
|
label: "ssh_backdoor",
|
|
@@ -2894,7 +2917,7 @@ var MemoryStore = class {
|
|
|
2894
2917
|
for (const [index, op] of operations.entries()) {
|
|
2895
2918
|
const position = index + 1;
|
|
2896
2919
|
if (op.action === "add") {
|
|
2897
|
-
const body = (op.facts ?? "").trim();
|
|
2920
|
+
const body = (op.facts ?? op.content ?? "").trim();
|
|
2898
2921
|
if (!body) return {
|
|
2899
2922
|
result: {
|
|
2900
2923
|
ok: false,
|
|
@@ -2973,7 +2996,7 @@ var MemoryStore = class {
|
|
|
2973
2996
|
const matchIndex = matches[0]?.matchIndex ?? -1;
|
|
2974
2997
|
if (op.action === "remove") working.splice(matchIndex, 1);
|
|
2975
2998
|
else {
|
|
2976
|
-
const body = (op.facts ?? "").trim();
|
|
2999
|
+
const body = (op.facts ?? op.content ?? "").trim();
|
|
2977
3000
|
if (!body) return {
|
|
2978
3001
|
result: {
|
|
2979
3002
|
ok: false,
|
|
@@ -3245,10 +3268,23 @@ function applyOneOverride(lines, override) {
|
|
|
3245
3268
|
if (!found) console.warn("evolution preset composition: warning — " + override.missingReason);
|
|
3246
3269
|
return lines;
|
|
3247
3270
|
}
|
|
3271
|
+
/**
|
|
3272
|
+
* Row ids of a composition fragment (line scan, no YAML library).
|
|
3273
|
+
*
|
|
3274
|
+
* PLAN S5.9 (2026-09-16, audit P2-27): the id extraction accepts INDENTED
|
|
3275
|
+
* `- id:` rows too, so a collision hidden in a nested group is still caught —
|
|
3276
|
+
* the old `^- id:` anchored at column 0 and was blind to exactly the rows an
|
|
3277
|
+
* upstream group nesting would produce. Twin of `rowIds` in
|
|
3278
|
+
* `scripts/install-layered.mjs` (installer.spec pins detection parity).
|
|
3279
|
+
* Boundary (current, deliberate): DETECTION covers nested rows, while the
|
|
3280
|
+
* override INJECTION anchors (`applyOneOverride` below) still match top-level
|
|
3281
|
+
* rows only — the injection indent contract (`^ {2}key:`) is defined against a
|
|
3282
|
+
* column-0 row.
|
|
3283
|
+
*/
|
|
3248
3284
|
function compositionRowIds(composition) {
|
|
3249
3285
|
const ids = /* @__PURE__ */ new Set();
|
|
3250
3286
|
for (const line of composition.split("\n")) {
|
|
3251
|
-
const id =
|
|
3287
|
+
const id = /^\s*- id:\s*(\S+)/.exec(line)?.[1];
|
|
3252
3288
|
if (id) ids.add(id);
|
|
3253
3289
|
}
|
|
3254
3290
|
return ids;
|
|
@@ -3718,13 +3754,29 @@ function jaccard(a, b) {
|
|
|
3718
3754
|
for (const token of a) if (b.has(token)) intersection += 1;
|
|
3719
3755
|
return intersection / (a.size + b.size - intersection);
|
|
3720
3756
|
}
|
|
3757
|
+
/** PLAN-R2 P2-8 (2026-09-16): default cap on two-name comparisons in
|
|
3758
|
+
* {@link computeDedupGroups}. A 2000-skill library is ~2M pairs; the old
|
|
3759
|
+
* unbounded two-two Jaccard ran seconds to tens of seconds per
|
|
3760
|
+
* review/`dedup_group` probe. 250k comparisons bounds that to well under a
|
|
3761
|
+
* second while staying far above every real library's pair count. Only
|
|
3762
|
+
* pairwise comparisons are budgeted — materializing a name's token set
|
|
3763
|
+
* (memoized, once per name) and the exact-hash pre-union phase are not. */
|
|
3764
|
+
const DEDUP_MAX_PAIR_COMPARISONS = 25e4;
|
|
3721
3765
|
/**
|
|
3722
3766
|
* Two-phase near-duplicate clustering: exact normalized-hash groups first,
|
|
3723
3767
|
* then token-Jaccard edges at {@link DEDUP_SIMILARITY_THRESHOLD} with a token
|
|
3724
3768
|
* ratio guard, union-find across the whole set.
|
|
3769
|
+
*
|
|
3770
|
+
* PLAN-R2 P2-8 (2026-09-16): each name's token set is materialized once
|
|
3771
|
+
* (memoized map), each pair takes an O(1) size-ratio short-circuit before the
|
|
3772
|
+
* intersection, and the pairwise loop is bounded by
|
|
3773
|
+
* `maxPairComparisons` (default {@link DEDUP_MAX_PAIR_COMPARISONS}); hitting
|
|
3774
|
+
* the budget stops the scan and `truncated: true` says so. Small libraries
|
|
3775
|
+
* (below the budget) behave exactly as the unbounded scan did.
|
|
3725
3776
|
*/
|
|
3726
3777
|
function computeDedupGroups(input) {
|
|
3727
3778
|
const threshold = input.threshold ?? .95;
|
|
3779
|
+
const maxPairComparisons = input.maxPairComparisons ?? 25e4;
|
|
3728
3780
|
const names = [...input.contents.keys()];
|
|
3729
3781
|
const hashes = /* @__PURE__ */ new Map();
|
|
3730
3782
|
for (const name of names) {
|
|
@@ -3760,12 +3812,19 @@ function computeDedupGroups(input) {
|
|
|
3760
3812
|
}
|
|
3761
3813
|
return set;
|
|
3762
3814
|
};
|
|
3763
|
-
|
|
3815
|
+
let compared = 0;
|
|
3816
|
+
let truncated = false;
|
|
3817
|
+
for (let index = 0; index < names.length && !truncated; index += 1) {
|
|
3764
3818
|
const a = names[index];
|
|
3765
3819
|
if (a === void 0) continue;
|
|
3766
3820
|
for (let other = index + 1; other < names.length; other += 1) {
|
|
3767
3821
|
const b = names[other];
|
|
3768
3822
|
if (b === void 0) continue;
|
|
3823
|
+
if (compared >= maxPairComparisons) {
|
|
3824
|
+
truncated = true;
|
|
3825
|
+
break;
|
|
3826
|
+
}
|
|
3827
|
+
compared += 1;
|
|
3769
3828
|
const [ta, tb] = [tokenSet(a), tokenSet(b)];
|
|
3770
3829
|
if (Math.max(ta.size, tb.size) / Math.max(1, Math.min(ta.size, tb.size)) > 5) continue;
|
|
3771
3830
|
if (jaccard(ta, tb) >= threshold) union(a, b);
|
|
@@ -3778,7 +3837,10 @@ function computeDedupGroups(input) {
|
|
|
3778
3837
|
if (group) group.push(name);
|
|
3779
3838
|
else groups.set(root, [name]);
|
|
3780
3839
|
}
|
|
3781
|
-
return
|
|
3840
|
+
return {
|
|
3841
|
+
groups: [...groups.values()].filter((group) => group.length > 1),
|
|
3842
|
+
truncated
|
|
3843
|
+
};
|
|
3782
3844
|
}
|
|
3783
3845
|
/**
|
|
3784
3846
|
* Prefix-cluster index over a name set (rc.67 merge heuristic, input side):
|
|
@@ -3832,7 +3894,7 @@ const URL_CREDENTIALS_PATTERN = /([a-z][a-z0-9+.-]{0,63}:\/\/[^\s:/@]+:)([^\s/@]
|
|
|
3832
3894
|
const PEM_PRIVATE_KEY_PATTERN = new RegExp(`-----BEGIN\\s+(?:(?:RSA|EC|DSA|OPENSSH|ENCRYPTED|PGP)\\s+)?PRIVATE\\s+KEY(?:\\s+BLOCK)?-----[\\s\\S]*?-----END\\s+(?:(?:RSA|EC|DSA|OPENSSH|ENCRYPTED|PGP)\\s+)?PRIVATE\\s+KEY(?:\\s+BLOCK)?-----`, "g");
|
|
3833
3895
|
const BLOCK_KEY_ONLY_LINE = /(?:^|\s)([\w-]*[_\-])?((?:token|api[_-]?key|secret|password|passwd)(?:[_\-][\w-]*)?)\s*:(?:\r)?$/i;
|
|
3834
3896
|
const CREDENTIAL_KEY_RE = /(?:[Tt]oken|[Ss]ecret|[Pp]assword|[Pp]asswd|[Aa]pi[_-]?[Kk]ey)(?![a-z])/;
|
|
3835
|
-
const CANDIDATE_ASSIGNMENT_PATTERN = /(^|[^\w-])([\w-]+)(["']?[\t ]*[:=][\t ]*)([^\r\n]
|
|
3897
|
+
const CANDIDATE_ASSIGNMENT_PATTERN = /(^|[^\w-])([\w-]+)(["']?[\t ]*[:=][\t ]*)(?=[^\r\n])/g;
|
|
3836
3898
|
/**
|
|
3837
3899
|
* Mask credential-shaped text before it crosses a session boundary.
|
|
3838
3900
|
* @param text - the text about to be sent to a model outside this session.
|
|
@@ -3844,21 +3906,39 @@ function redactSecrets(text) {
|
|
|
3844
3906
|
for (const [, pattern] of SECRET_PATTERNS) out = out.replace(pattern, "<redacted>");
|
|
3845
3907
|
out = out.replace(URL_CREDENTIALS_PATTERN, (_match, lead) => `${lead ?? ""}<redacted>@`);
|
|
3846
3908
|
out = out.replace(INLINE_ASSIGNMENT_PATTERN, (_match, lead, prefix, key, separator) => `${lead ?? ""}${prefix ?? ""}${key ?? ""}${separator ?? ""}<redacted>`);
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3909
|
+
const candidatePattern = new RegExp(CANDIDATE_ASSIGNMENT_PATTERN.source, "g");
|
|
3910
|
+
let candidateOut = "";
|
|
3911
|
+
let consumed = 0;
|
|
3912
|
+
for (let m = candidatePattern.exec(out); m !== null; m = candidatePattern.exec(out)) {
|
|
3913
|
+
const lead = m[1] ?? "";
|
|
3914
|
+
const key = m[2] ?? "";
|
|
3915
|
+
const separator = m[3] ?? "";
|
|
3916
|
+
if (!CREDENTIAL_KEY_RE.test(key)) continue;
|
|
3917
|
+
let valueEnd = m.index + m[0].length;
|
|
3918
|
+
while (valueEnd < out.length && out[valueEnd] !== "\n" && out[valueEnd] !== "\r") valueEnd += 1;
|
|
3919
|
+
candidateOut += out.slice(consumed, m.index) + lead + key + separator + "<redacted>";
|
|
3920
|
+
consumed = valueEnd;
|
|
3921
|
+
candidatePattern.lastIndex = valueEnd;
|
|
3922
|
+
}
|
|
3923
|
+
out = candidateOut + out.slice(consumed);
|
|
3851
3924
|
const lines = out.split("\n");
|
|
3852
3925
|
for (let i = 0; i < lines.length - 1; i++) {
|
|
3853
3926
|
const line = lines[i];
|
|
3854
3927
|
if (line === void 0) continue;
|
|
3855
3928
|
const camelKey = /^([\w-]+)\s*:(?:\r)?$/.exec(line)?.[1];
|
|
3856
3929
|
if (!(BLOCK_KEY_ONLY_LINE.test(line) || camelKey !== void 0 && CREDENTIAL_KEY_RE.test(camelKey))) continue;
|
|
3857
|
-
|
|
3930
|
+
let valueLine = -1;
|
|
3931
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
3932
|
+
if ((lines[j] ?? "").trim() === "") continue;
|
|
3933
|
+
valueLine = j;
|
|
3934
|
+
break;
|
|
3935
|
+
}
|
|
3936
|
+
if (valueLine < 0) continue;
|
|
3937
|
+
const next = lines[valueLine] ?? "";
|
|
3858
3938
|
const [, indent, value, tail] = /^([ \t]+)(\S.*?)([ \t]*)(?:\r)?$/.exec(next) ?? [];
|
|
3859
3939
|
if (indent === void 0 || value === void 0) continue;
|
|
3860
3940
|
if (value.includes("<redacted>")) continue;
|
|
3861
|
-
lines[
|
|
3941
|
+
lines[valueLine] = `${indent}<redacted>${tail ?? ""}`;
|
|
3862
3942
|
}
|
|
3863
3943
|
out = lines.join("\n");
|
|
3864
3944
|
out = out.split("\n").map((line) => {
|
|
@@ -4075,6 +4155,8 @@ let cachedSites;
|
|
|
4075
4155
|
* fails on this asset, while the first caller still gets a loud, descriptive
|
|
4076
4156
|
* failure instead of an empty table ("no declared write sites" would silently
|
|
4077
4157
|
* disable rule N20).
|
|
4158
|
+
* @internal S2.4 (PLAN 2026-09-16): no production caller — the arch guard
|
|
4159
|
+
* reads the JSON asset directly; the tests are the only in-tree consumers.
|
|
4078
4160
|
* @returns the sites, in file order.
|
|
4079
4161
|
*/
|
|
4080
4162
|
function persistedWriteSites() {
|
|
@@ -4089,12 +4171,16 @@ function persistedWriteSites() {
|
|
|
4089
4171
|
cachedSites = parseSites(JSON.parse(raw));
|
|
4090
4172
|
return cachedSites;
|
|
4091
4173
|
}
|
|
4092
|
-
/** Sites serialized by the per-home instance claim, with their instance keys.
|
|
4174
|
+
/** Sites serialized by the per-home instance claim, with their instance keys.
|
|
4175
|
+
* @internal S2.4 (PLAN 2026-09-16): no production caller — the arch guard
|
|
4176
|
+
* reads the JSON asset directly; the tests are the only in-tree consumers. */
|
|
4093
4177
|
function instanceClaimedWriteSites() {
|
|
4094
4178
|
return persistedWriteSites().filter((site) => site.serializedBy === "instance-claim");
|
|
4095
4179
|
}
|
|
4096
4180
|
/** One declared site by id. An undeclared id throws — a stale caller must fail
|
|
4097
|
-
* loud rather than read "nothing is declared".
|
|
4181
|
+
* loud rather than read "nothing is declared".
|
|
4182
|
+
* @internal S2.4 (PLAN 2026-09-16): no production caller — the arch guard
|
|
4183
|
+
* reads the JSON asset directly; the tests are the only in-tree consumers. */
|
|
4098
4184
|
function persistedWriteSite(id) {
|
|
4099
4185
|
const site = persistedWriteSites().find((candidate) => candidate.id === id);
|
|
4100
4186
|
if (site === void 0) throw new Error(`evolution-core: no persisted write site "${id}" in persisted-write-inventory.json`);
|
|
@@ -4106,11 +4192,6 @@ function callingScope(ctx, held) {
|
|
|
4106
4192
|
if (held !== void 0) return held;
|
|
4107
4193
|
return scopeOf(ctx);
|
|
4108
4194
|
}
|
|
4109
|
-
/** True when a read is deliberately scope-less (global layer only). Callers
|
|
4110
|
-
* pass this to the register so the choice is reviewed, not accidental. */
|
|
4111
|
-
function isGlobalRead(scope) {
|
|
4112
|
-
return scope === void 0;
|
|
4113
|
-
}
|
|
4114
4195
|
//#endregion
|
|
4115
4196
|
//#region lib/types/skill-health.js
|
|
4116
4197
|
/**
|
|
@@ -4792,7 +4873,8 @@ function computeDriftSignals(snapshots) {
|
|
|
4792
4873
|
const library = [];
|
|
4793
4874
|
const names = snapshots.map((s) => s.name);
|
|
4794
4875
|
const dedup = computeDedupGroups({ contents: new Map(snapshots.map((s) => [s.name, s.body])) });
|
|
4795
|
-
|
|
4876
|
+
const dedupTruncation = dedup.truncated ? " (dedup scan truncated at the pair-comparison budget)" : "";
|
|
4877
|
+
library.push(dedup.groups.length === 0 ? sig("dedup_group", "pass", `none${dedupTruncation}`, "size >= 2") : sig("dedup_group", "over", `${dedup.groups.map((group) => group.join(", ")).join(" | ")}${dedupTruncation}`, "size >= 2", `members=${dedup.groups.map((group) => group.join("|")).join(";")}`));
|
|
4796
4878
|
const clusters = computePrefixClusters(names);
|
|
4797
4879
|
library.push(clusters.length === 0 ? sig("prefix_cluster", "pass", "none", "size >= 2") : sig("prefix_cluster", "over", clusters.map((cluster) => cluster.members.join(", ")).join(" | "), "size >= 2", `key=${clusters.map((cluster) => cluster.key).join("|")}`));
|
|
4798
4880
|
const allProvided = snapshots.length > 0 && snapshots.every((s) => s.usageObserved !== null && s.usageObserved !== void 0);
|
|
@@ -4947,10 +5029,12 @@ const PLATFORM_STRING_FIELDS = [
|
|
|
4947
5029
|
"whenToUse"
|
|
4948
5030
|
];
|
|
4949
5031
|
/**
|
|
4950
|
-
* Frontmatter values as the STRICT platform catalog reads them —
|
|
4951
|
-
*
|
|
4952
|
-
*
|
|
4953
|
-
*
|
|
5032
|
+
* Frontmatter values as the STRICT platform catalog reads them — the `yaml`
|
|
5033
|
+
* package (YAML 1.2 core schema, the same dependency the platform's
|
|
5034
|
+
* skill-filesystem parses with — see the import note above),
|
|
5035
|
+
* the parser `normalizeFrontmatter` also verifies rewrites with — or `null`
|
|
5036
|
+
* when the block is not loadable as a YAML mapping. Also reports the platform
|
|
5037
|
+
* string fields whose value is not a string ({@link PlatformStringSplit}).
|
|
4954
5038
|
*
|
|
4955
5039
|
* Scalars publish their text (`name`, `description`, `whenToUse` are strings by
|
|
4956
5040
|
* contract; a number/boolean-shaped value keeps the text the family always
|
|
@@ -4968,7 +5052,7 @@ function strictFrontmatterValues(block) {
|
|
|
4968
5052
|
};
|
|
4969
5053
|
let loaded;
|
|
4970
5054
|
try {
|
|
4971
|
-
loaded =
|
|
5055
|
+
loaded = parse(block);
|
|
4972
5056
|
} catch {
|
|
4973
5057
|
return null;
|
|
4974
5058
|
}
|
|
@@ -5136,7 +5220,7 @@ function yamlPlainScalarNeedsQuotes(value) {
|
|
|
5136
5220
|
if (/^[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?$/.test(value)) return true;
|
|
5137
5221
|
if (/^0x[0-9a-f]+$/i.test(value)) return true;
|
|
5138
5222
|
if (/^0o[0-7]+$/.test(value)) return true;
|
|
5139
|
-
if (
|
|
5223
|
+
if (/^(?:[-+]?\.inf|\.nan)$/i.test(value)) return true;
|
|
5140
5224
|
if (/^[-?:,[\]{}#&*!|>'\"%@`\s]/.test(value)) return true;
|
|
5141
5225
|
return false;
|
|
5142
5226
|
}
|
|
@@ -5147,7 +5231,10 @@ function yamlPlainScalarNeedsQuotes(value) {
|
|
|
5147
5231
|
* unescaped inside single-quoted YAML). Idempotent; only single-line
|
|
5148
5232
|
* `key: value` entries are touched; body text is never modified; line-ending
|
|
5149
5233
|
* style is preserved. **Every rewrite is re-verified with the real YAML
|
|
5150
|
-
* parser** (
|
|
5234
|
+
* parser** (`yaml` — the package the platform's skill-filesystem catalog parses
|
|
5235
|
+
* with, YAML 1.2 core schema; PLAN S2.1, 2026-09-16 — the former "js-yaml, the
|
|
5236
|
+
* same parser" claim was false: js-yaml speaks YAML 1.1 full and diverged on
|
|
5237
|
+
* dates, timestamps and 1.1 int forms): if the
|
|
5151
5238
|
* rewritten block no longer parses, or a rewritten value's parsed content
|
|
5152
5239
|
* differs from the original, the rewrite is rolled back and reported in
|
|
5153
5240
|
* `issues` (fail-loud, never a silent value corruption — P3-4).
|
|
@@ -5210,7 +5297,7 @@ function normalizeFrontmatter(content) {
|
|
|
5210
5297
|
};
|
|
5211
5298
|
const rewrittenBlock = lines.slice(1, end).join("\n");
|
|
5212
5299
|
try {
|
|
5213
|
-
const parsed =
|
|
5300
|
+
const parsed = parse(rewrittenBlock);
|
|
5214
5301
|
for (const key of fields) if (String(parsed[key]) !== originalValues.get(key)) throw new Error(`rewritten value for ${key} differs from the original`);
|
|
5215
5302
|
return {
|
|
5216
5303
|
content: lines.join("\n"),
|
|
@@ -5533,9 +5620,19 @@ function skillsRoot(env = process.env) {
|
|
|
5533
5620
|
* (and the graph ignored config entirely). Empty/whitespace config falls
|
|
5534
5621
|
* through to the default; callers pass their raw Config. The optional field is
|
|
5535
5622
|
* declared `| undefined` so a config object whose root field is explicitly
|
|
5536
|
-
* `string | undefined` still assignable under exactOptionalPropertyTypes.
|
|
5623
|
+
* `string | undefined` still assignable under exactOptionalPropertyTypes.
|
|
5624
|
+
* P2-31 core half (S2.2 batch, PLAN 2026-09-16): an explicit non-empty root
|
|
5625
|
+
* is `resolve()`d — the same normalization `evolutionRoot` applies and the
|
|
5626
|
+
* same one upstream skill-filesystem applies to `customSkillDirs`
|
|
5627
|
+
* (`(config.customSkillDirs ?? []).map(root => resolve(root))`). The former
|
|
5628
|
+
* verbatim return left a RELATIVE config root CWD-relative, so the family's
|
|
5629
|
+
* skill tree moved with the host process's launch directory while every
|
|
5630
|
+
* absolute consumer (watchers, the platform catalog) resolved it — a
|
|
5631
|
+
* split-brain tree; a trailing slash or `..` segment likewise landed
|
|
5632
|
+
* unnormalized. A clean absolute root is byte-identical (resolve is a no-op). */
|
|
5537
5633
|
function resolveSkillsRoot(config = {}) {
|
|
5538
|
-
|
|
5634
|
+
const explicit = (config.root ?? "").trim();
|
|
5635
|
+
return explicit ? resolve(explicit) : skillsRoot();
|
|
5539
5636
|
}
|
|
5540
5637
|
/** E-7 (v18) → V27 G2.4 (M-08): every family row reads ONE root key. `root` is
|
|
5541
5638
|
* canonical; the `skillsRoot` alias was honoured for one minor version and its
|
|
@@ -7878,4 +7975,4 @@ function sessionAudited(ctx, sessionId, sessionScoped) {
|
|
|
7878
7975
|
return false;
|
|
7879
7976
|
}
|
|
7880
7977
|
//#endregion
|
|
7881
|
-
export { AUTHORING_DESCRIPTION_BAR, COMBINED_REVIEW_PLAN_PROMPT, COMBINED_REVIEW_PROMPT, COMPLETION_SKILL_REVIEW_PROMPT, CURATOR_DRY_RUN_BANNER, CURATOR_PROMPT, DEAD_LOCK_TAKEOVER_MS, DEFAULT_ARCHIVE_AFTER_DAYS, 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_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_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_WRITE_TOOLS, EvolutionGateSet, FAMILY_SESSION_TOOL_NAMES, FORBIDDEN_CONTROL_KEYS, HEALTH_STAMP_RE, INSTANCE_KEYS, 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_TIMER_DELAY_MS, MEMORY_GUIDANCE_SECTION_ORDER, MEMORY_REVIEW_PROMPT, MIN_STAMP_BODY_CHARS, MUTATIONS_FILE_VERSION, MemoryStore, NATIVE_CALL_EVENT, NATIVE_RESULT_EVENT, PATTERN_OVERLAP, PROMPT_BUNDLE, PROMPT_BUNDLE_ID, PROMPT_BUNDLE_VERSION, PROTECTED_BUILTIN_SKILLS, PTC_DISPATCH_EVENT, PTC_DISPATCH_START_EVENT, QUALITY_WEIGHTS, 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, ToolDispatchNormalizer, ToolDispatchPayloadError, advanceReview, allowRowCollisions, appendEvolutionEvent, applyCuratorLifecycleFields, applyCuratorMetaFields, assertDispatchPayload, assertSkillsRootAliasRetired, assessStructureHealth, authoringFeedback, buildCuratorRunReport, buildLearnPrompt, bumpPatch, bumpUse, bumpView, callingScope, claimInstance, clampedNumber, clearReviewChannel, collectReadSkillNames, composePresetComposition, computeDedupGroups, computeDriftSignals, computeLifecycleTransitions, computePrefixClusters, computeQualityScores, computeScopeView, contentHash, countDispatches, createGateSet, decideTakeover, duplicateHeadings, emptyRecord, evaluateThreat, eventsFile, evolutionEventPayloadIssue, evolutionHome, evolutionIoAdapter, evolutionRoot, findDriftSignal, foldCuratorFields, foldToolDispatches, foldTurn, frontmatterBlock, frontmatterCatalogInvalid, getRecord, instanceClaimKey, instanceClaimedWriteSites, instanceHolder, isAbsent, isCommittedWarning,
|
|
7978
|
+
export { AUTHORING_DESCRIPTION_BAR, COMBINED_REVIEW_PLAN_PROMPT, COMBINED_REVIEW_PROMPT, COMPLETION_SKILL_REVIEW_PROMPT, CURATOR_DRY_RUN_BANNER, CURATOR_PROMPT, DEAD_LOCK_TAKEOVER_MS, DEDUP_MAX_PAIR_COMPARISONS, DEFAULT_ARCHIVE_AFTER_DAYS, 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_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_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_WRITE_TOOLS, EvolutionGateSet, FAMILY_SESSION_TOOL_NAMES, FORBIDDEN_CONTROL_KEYS, HEALTH_STAMP_RE, INSTANCE_KEYS, 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_TIMER_DELAY_MS, MEMORY_GUIDANCE_SECTION_ORDER, MEMORY_REVIEW_PROMPT, MIN_STAMP_BODY_CHARS, MUTATIONS_FILE_VERSION, MemoryStore, NATIVE_CALL_EVENT, NATIVE_RESULT_EVENT, PATTERN_OVERLAP, PROMPT_BUNDLE, PROMPT_BUNDLE_ID, PROMPT_BUNDLE_VERSION, PROTECTED_BUILTIN_SKILLS, PTC_DISPATCH_EVENT, PTC_DISPATCH_START_EVENT, QUALITY_WEIGHTS, 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, ToolDispatchNormalizer, ToolDispatchPayloadError, advanceReview, allowRowCollisions, appendEvolutionEvent, applyCuratorLifecycleFields, applyCuratorMetaFields, assertDispatchPayload, assertSkillsRootAliasRetired, assessStructureHealth, authoringFeedback, buildCuratorRunReport, buildLearnPrompt, bumpPatch, bumpUse, bumpView, callingScope, claimInstance, clampedNumber, clearReviewChannel, collectReadSkillNames, composePresetComposition, computeDedupGroups, computeDriftSignals, computeLifecycleTransitions, computePrefixClusters, computeQualityScores, computeScopeView, contentHash, countDispatches, createGateSet, decideTakeover, duplicateHeadings, emptyRecord, evaluateThreat, eventsFile, evolutionEventPayloadIssue, evolutionHome, evolutionIoAdapter, evolutionRoot, findDriftSignal, foldCuratorFields, foldToolDispatches, foldTurn, frontmatterBlock, frontmatterCatalogInvalid, getRecord, instanceClaimKey, instanceClaimedWriteSites, instanceHolder, isAbsent, isCommittedWarning, isMissingPath, isPresent, isProcessAlive, isProgramToolName, isReviewChannelSession, isSkillReadToolName, isSkillToolName, isUnknown, latestActivityAt, lifecycleCandidate, listEventArchives, loadMutations, loadRowOverrides, loadSuppressedNames, loadUsage, makeSerialQueue, mapProbe, markAgentCreated, markReviewChannel, markerEntryName, memoryRoot, missingSupportPointers, mutateUsage, mutationsFile, narrowNameMatches, neutralizePromptVariables, newSkillLibrary, nodeEvolutionIo, normalizeFrontmatter, normalizeUsageRecord, observeEvent, overlongLines, parseCuratorNominations, parseEvolutionEvents, parseFrontmatter, parseLockBody, pendingSelfCleanup, persistedWriteSite, persistedWriteSites, probeAbsent, probeList, probeMtime, probePresent, probeReason, probeUnknown, readDispatchSignal, readEvolutionEvents, readEvolutionTimeline, recordMutation, redactSecrets, relatedSkillNames, releaseInstance, renameWithRetry, renderCuratorReportMarkdown, resolveExecOrigins, resolveOrigins, resolveRootConfig, resolveSkillsRoot, retainEventArchives, reviewPrompt, saveSuppressedNames, saveUsage, scanContentThreats, scanMemoryThreats, scanThreats, scopedProbeReport, sessionAudited, sessionSeesFamilyTools, skillReadNameOf, skillsRoot, suppressedFile, sweepReviewChannelSessions, transactIo, transactTaskGuard, updateSuppressedNames, usageFile, usageObserved, validateFrontmatter, validateRestructureTarget, valueOr, verifyPromptBundle, writeDurableTmp, yamlPlainScalarNeedsQuotes };
|
|
@@ -109,7 +109,10 @@ export interface FrontmatterNormalizeResult {
|
|
|
109
109
|
* unescaped inside single-quoted YAML). Idempotent; only single-line
|
|
110
110
|
* `key: value` entries are touched; body text is never modified; line-ending
|
|
111
111
|
* style is preserved. **Every rewrite is re-verified with the real YAML
|
|
112
|
-
* parser** (
|
|
112
|
+
* parser** (`yaml` — the package the platform's skill-filesystem catalog parses
|
|
113
|
+
* with, YAML 1.2 core schema; PLAN S2.1, 2026-09-16 — the former "js-yaml, the
|
|
114
|
+
* same parser" claim was false: js-yaml speaks YAML 1.1 full and diverged on
|
|
115
|
+
* dates, timestamps and 1.1 int forms): if the
|
|
113
116
|
* rewritten block no longer parses, or a rewritten value's parsed content
|
|
114
117
|
* differs from the original, the rewrite is rolled back and reported in
|
|
115
118
|
* `issues` (fail-loud, never a silent value corruption — P3-4).
|
|
@@ -8,6 +8,10 @@ export type MemoryTarget = 'memory' | 'user';
|
|
|
8
8
|
export interface MemoryOperation {
|
|
9
9
|
action: 'add' | 'replace' | 'remove';
|
|
10
10
|
facts?: string | undefined;
|
|
11
|
+
/** PLAN-R2 P2-6 (2026-09-16): the dsh-memory contract's alias. applyBatchCore
|
|
12
|
+
* reads `facts ?? content`, so a provider reusing this store directly can
|
|
13
|
+
* write the tool-memory schema shape without a normalization shim. */
|
|
14
|
+
content?: string | undefined;
|
|
11
15
|
old_text?: string | undefined;
|
|
12
16
|
}
|
|
13
17
|
export interface MemoryApplyResult {
|
package/lib/types/quality.d.ts
CHANGED
|
@@ -45,15 +45,40 @@ export declare function computeQualityScores(input: {
|
|
|
45
45
|
supportDirs?: ReadonlyMap<string, number>;
|
|
46
46
|
now?: Date;
|
|
47
47
|
}): Map<string, QualityScore>;
|
|
48
|
+
/** PLAN-R2 P2-8 (2026-09-16): default cap on two-name comparisons in
|
|
49
|
+
* {@link computeDedupGroups}. A 2000-skill library is ~2M pairs; the old
|
|
50
|
+
* unbounded two-two Jaccard ran seconds to tens of seconds per
|
|
51
|
+
* review/`dedup_group` probe. 250k comparisons bounds that to well under a
|
|
52
|
+
* second while staying far above every real library's pair count. Only
|
|
53
|
+
* pairwise comparisons are budgeted — materializing a name's token set
|
|
54
|
+
* (memoized, once per name) and the exact-hash pre-union phase are not. */
|
|
55
|
+
export declare const DEDUP_MAX_PAIR_COMPARISONS = 250000;
|
|
56
|
+
/** PLAN-R2 P2-8 (2026-09-16): result of the near-duplicate scan. `truncated`
|
|
57
|
+
* reports that the pairwise scan stopped at the comparison budget: groups
|
|
58
|
+
* already found are valid, but pairs beyond the budget were never examined. */
|
|
59
|
+
export interface DedupScanResult {
|
|
60
|
+
groups: string[][];
|
|
61
|
+
truncated: boolean;
|
|
62
|
+
}
|
|
48
63
|
/**
|
|
49
64
|
* Two-phase near-duplicate clustering: exact normalized-hash groups first,
|
|
50
65
|
* then token-Jaccard edges at {@link DEDUP_SIMILARITY_THRESHOLD} with a token
|
|
51
66
|
* ratio guard, union-find across the whole set.
|
|
67
|
+
*
|
|
68
|
+
* PLAN-R2 P2-8 (2026-09-16): each name's token set is materialized once
|
|
69
|
+
* (memoized map), each pair takes an O(1) size-ratio short-circuit before the
|
|
70
|
+
* intersection, and the pairwise loop is bounded by
|
|
71
|
+
* `maxPairComparisons` (default {@link DEDUP_MAX_PAIR_COMPARISONS}); hitting
|
|
72
|
+
* the budget stops the scan and `truncated: true` says so. Small libraries
|
|
73
|
+
* (below the budget) behave exactly as the unbounded scan did.
|
|
52
74
|
*/
|
|
53
75
|
export declare function computeDedupGroups(input: {
|
|
54
76
|
contents: ReadonlyMap<string, string>;
|
|
55
77
|
threshold?: number;
|
|
56
|
-
|
|
78
|
+
/** Pair-comparison budget (PLAN-R2 P2-8). Defaults to
|
|
79
|
+
* {@link DEDUP_MAX_PAIR_COMPARISONS}; a smaller value truncates earlier. */
|
|
80
|
+
maxPairComparisons?: number;
|
|
81
|
+
}): DedupScanResult;
|
|
57
82
|
/**
|
|
58
83
|
* Prefix-cluster index over a name set (rc.67 merge heuristic, input side):
|
|
59
84
|
* the curator prompt asks the model to identify "prefix clusters — skills
|
package/lib/types/scope.d.ts
CHANGED
|
@@ -21,7 +21,4 @@ import type { Context } from '@deepseek-ai/cordis';
|
|
|
21
21
|
* platform type (`ScopeKey` is `object`). */
|
|
22
22
|
export type OpaqueScopeKey = object;
|
|
23
23
|
export declare function callingScope(ctx: Context, held?: OpaqueScopeKey): OpaqueScopeKey | undefined;
|
|
24
|
-
/** True when a read is deliberately scope-less (global layer only). Callers
|
|
25
|
-
* pass this to the register so the choice is reviewed, not accidental. */
|
|
26
|
-
export declare function isGlobalRead(scope: OpaqueScopeKey | undefined): boolean;
|
|
27
24
|
//# sourceMappingURL=scope.d.ts.map
|
|
@@ -166,7 +166,16 @@ export declare function skillsRoot(env?: NodeJS.ProcessEnv): string;
|
|
|
166
166
|
* (and the graph ignored config entirely). Empty/whitespace config falls
|
|
167
167
|
* through to the default; callers pass their raw Config. The optional field is
|
|
168
168
|
* declared `| undefined` so a config object whose root field is explicitly
|
|
169
|
-
* `string | undefined` still assignable under exactOptionalPropertyTypes.
|
|
169
|
+
* `string | undefined` still assignable under exactOptionalPropertyTypes.
|
|
170
|
+
* P2-31 core half (S2.2 batch, PLAN 2026-09-16): an explicit non-empty root
|
|
171
|
+
* is `resolve()`d — the same normalization `evolutionRoot` applies and the
|
|
172
|
+
* same one upstream skill-filesystem applies to `customSkillDirs`
|
|
173
|
+
* (`(config.customSkillDirs ?? []).map(root => resolve(root))`). The former
|
|
174
|
+
* verbatim return left a RELATIVE config root CWD-relative, so the family's
|
|
175
|
+
* skill tree moved with the host process's launch directory while every
|
|
176
|
+
* absolute consumer (watchers, the platform catalog) resolved it — a
|
|
177
|
+
* split-brain tree; a trailing slash or `..` segment likewise landed
|
|
178
|
+
* unnormalized. A clean absolute root is byte-identical (resolve is a no-op). */
|
|
170
179
|
export declare function resolveSkillsRoot(config?: {
|
|
171
180
|
root?: string | undefined;
|
|
172
181
|
}): string;
|
|
@@ -14,12 +14,14 @@
|
|
|
14
14
|
* DSH home root: `$DSH_HOME` or `~/.dsh`. Single source of the empty-string
|
|
15
15
|
* fallback — an EMPTY or WHITESPACE-ONLY DSH_HOME resolves to the default
|
|
16
16
|
* home, never to a CWD-relative path (0.3.19 W1.3, 0.3.22 F-207);
|
|
17
|
-
* V8-06 (0.3.47) extends the guard to whitespace
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
17
|
+
* V8-06 (0.3.47) extends the guard to whitespace.
|
|
18
|
+
* C-11 correction (S2.2, PLAN 2026-09-16): upstream `resolveDshHome`
|
|
19
|
+
* (util/home-paths) uses `trim()` ONLY as the ADOPTION test and then uses the
|
|
20
|
+
* RAW env value — the earlier "same trimmed source" form returned the trimmed
|
|
21
|
+
* text, so `DSH_HOME=" /x "` produced `/x` where upstream produced the literal
|
|
22
|
+
* padded path. The value semantics now match upstream line for line:
|
|
23
|
+
* `const selected = configured ?? (fromEnv !== undefined &&
|
|
24
|
+
* fromEnv.trim().length > 0 ? fromEnv : defaultDshHome())`.
|
|
23
25
|
* OPT-27 (2026-09, plan D5 — accepted): the v10-era "no `~` expansion, no
|
|
24
26
|
* resolve" divergence from upstream `resolveDshHome` is RETIRED. It became
|
|
25
27
|
* load-bearing when the skill-catalog shadow made "same tree as the upstream
|
|
@@ -27,9 +29,13 @@
|
|
|
27
29
|
* absolute `<home>/skills` while this value fed a literal `~/x` (a directory
|
|
28
30
|
* named `~` under the host CWD) or a CWD-relative path — split-brain skill
|
|
29
31
|
* trees, preset installs the platform never reads, doctor probes of a
|
|
30
|
-
* directory nothing serves.
|
|
31
|
-
*
|
|
32
|
-
*
|
|
32
|
+
* directory nothing serves.
|
|
33
|
+
* S2.2 (PLAN 2026-09-16), final correction: the result is ALWAYS
|
|
34
|
+
* `resolve(expandHomePath(selected))` — the earlier form returned an
|
|
35
|
+
* already-absolute value VERBATIM, so a trailing slash or `..` segment
|
|
36
|
+
* landed unnormalized while upstream normalizes every value. Relative
|
|
37
|
+
* `DSH_HOME` values change landing spot; a CLEAN absolute home stays
|
|
38
|
+
* byte-identical (resolve is a no-op on it).
|
|
33
39
|
*/
|
|
34
40
|
export declare function evolutionRoot(env?: NodeJS.ProcessEnv): string;
|
|
35
41
|
/** Evolution home path helper: `$DSH_HOME/evolution` for plugin-owned sidecar
|
package/lib/types/usage.d.ts
CHANGED
|
@@ -119,6 +119,23 @@ export declare function usageObserved(usage: ReadonlyMap<string, UsageRecord>):
|
|
|
119
119
|
export declare const SUPPRESSED_FILE_VERSION = 1;
|
|
120
120
|
export declare function suppressedFile(root: string): string;
|
|
121
121
|
export declare function loadSuppressedNames(root: string, io?: EvolutionIoLike): Promise<ReadonlySet<string>>;
|
|
122
|
+
/**
|
|
123
|
+
* Plain wholesale write of the suppression sidecar (tests and fixture seeding).
|
|
124
|
+
* @internal S2.4 (PLAN 2026-09-16, audit P2-31): NO production consumer
|
|
125
|
+
* (verified by grep; production suppressions go through
|
|
126
|
+
* {@link updateSuppressedNames}) — exported for the family's tests only. Do
|
|
127
|
+
* not use it to write the sidecar in new code.
|
|
128
|
+
*
|
|
129
|
+
* The write rides the {@link transactIo} channel (the same lock the RMW
|
|
130
|
+
* writer uses), not the former naked read → `io.writeText`: that form was an
|
|
131
|
+
* unserialized read-modify-write — the last `saveSuppressedNames` in this file
|
|
132
|
+
* with no lock, while its sibling `saveUsage` already carried the `@internal`
|
|
133
|
+
* test-only note. Version discipline is unchanged (V24-09 / S2-14): a read
|
|
134
|
+
* failure surfaces (an unreadable sidecar is never written over), a newer
|
|
135
|
+
* on-disk schema is warned about and preserved byte-for-byte (the
|
|
136
|
+
* byte-identical result short-circuits the write), and a malformed sidecar is
|
|
137
|
+
* still overwritten with the v1 shape (the historical plain-writer posture).
|
|
138
|
+
*/
|
|
122
139
|
export declare function saveSuppressedNames(root: string, names: ReadonlySet<string>, io?: EvolutionIoLike): Promise<void>;
|
|
123
140
|
/**
|
|
124
141
|
* Atomic read-modify-write on the suppression sidecar (rc.50 P2-2): `task`
|
|
@@ -53,15 +53,21 @@ export declare const INSTANCE_KEYS: {
|
|
|
53
53
|
* fails on this asset, while the first caller still gets a loud, descriptive
|
|
54
54
|
* failure instead of an empty table ("no declared write sites" would silently
|
|
55
55
|
* disable rule N20).
|
|
56
|
+
* @internal S2.4 (PLAN 2026-09-16): no production caller — the arch guard
|
|
57
|
+
* reads the JSON asset directly; the tests are the only in-tree consumers.
|
|
56
58
|
* @returns the sites, in file order.
|
|
57
59
|
*/
|
|
58
60
|
export declare function persistedWriteSites(): readonly PersistedWriteSite[];
|
|
59
|
-
/** Sites serialized by the per-home instance claim, with their instance keys.
|
|
61
|
+
/** Sites serialized by the per-home instance claim, with their instance keys.
|
|
62
|
+
* @internal S2.4 (PLAN 2026-09-16): no production caller — the arch guard
|
|
63
|
+
* reads the JSON asset directly; the tests are the only in-tree consumers. */
|
|
60
64
|
export declare function instanceClaimedWriteSites(): readonly (PersistedWriteSite & {
|
|
61
65
|
readonly instance: string;
|
|
62
66
|
})[];
|
|
63
67
|
/** One declared site by id. An undeclared id throws — a stale caller must fail
|
|
64
|
-
* loud rather than read "nothing is declared".
|
|
68
|
+
* loud rather than read "nothing is declared".
|
|
69
|
+
* @internal S2.4 (PLAN 2026-09-16): no production caller — the arch guard
|
|
70
|
+
* reads the JSON asset directly; the tests are the only in-tree consumers. */
|
|
65
71
|
export declare function persistedWriteSite(id: string): PersistedWriteSite;
|
|
66
72
|
export {};
|
|
67
73
|
//# sourceMappingURL=write-inventory.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lmzhen/dsh-evolution-core",
|
|
3
3
|
"description": "Shared stores, prompts, signals and lifecycle logic for the dsh-evolution plugin family (community build)",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.4.0",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
],
|
|
31
31
|
"license": "MIT",
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"
|
|
33
|
+
"yaml": "^2.4.2"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
36
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
@@ -38,7 +38,6 @@
|
|
|
38
38
|
"@deepseek-ai/dsh-session": "^0.1.5-rc.2"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
|
-
"@types/js-yaml": "^4.0.9",
|
|
42
41
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
43
42
|
"@deepseek-ai/dsh-scope": "^0.1.5-rc.2",
|
|
44
43
|
"@deepseek-ai/dsh-session": "^0.1.5-rc.2"
|