@lmzhen/dsh-evolution-core 0.3.82 → 0.3.83

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 CHANGED
@@ -1,4 +1,4 @@
1
- # @deepseek-ai/dsh-evolution-core
1
+ # @lmzhen/dsh-evolution-core
2
2
 
3
3
  Shared pure library for the dsh-evolution plugin family.
4
4
 
@@ -8,7 +8,7 @@ Shared pure library for the dsh-evolution plugin family.
8
8
 
9
9
  #### What the model sees
10
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.
11
+ `@lmzhen/dsh-evolution-core` registers no direct prompt or tool schema itself. Model-visible effects are owned by the packages that consume this service.
12
12
 
13
13
  #### Token effect
14
14
 
package/lib/index.js CHANGED
@@ -1,11 +1,11 @@
1
- import { basename, dirname, isAbsolute, join, resolve } from "node:path";
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 { load } from "js-yaml";
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
- const current = await io.readText(suppressedFile(root));
1035
- if (current !== null) try {
1036
- const parsed = JSON.parse(current);
1037
- if (parsed !== null && typeof parsed.version === "number" && parsed.version > 1) {
1038
- console.warn(`suppression sidecar ${suppressedFile(root)} declares version ${String(parsed.version)} (newer than 1); not overwritten`);
1039
- return;
1040
- }
1041
- } catch {}
1042
- await io.writeText(suppressedFile(root), JSON.stringify({
1043
- version: 1,
1044
- names: [...names].sort()
1045
- }, null, 2));
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 (upstream home-paths:
1989
- * `trim().length > 0` is the adoption test — `DSH_HOME=" "` must not produce
1990
- * a sidecar under a relative "." path).
1991
- * C-11: the adoption test and the RETURNED value now come from the
1992
- * SAME trimmed source the old form tested `trim()` but returned the raw
1993
- * value, so `DSH_HOME=" /x "` was accepted AND persisted with literal spaces.
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. Behavior now matches upstream:
2002
- * `resolve(expandHomePath(selected))`. Only `~`-prefixed and RELATIVE
2003
- * DSH_HOME values change landing spot; absolute homes are byte-identical.
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 home = env.DSH_HOME?.trim();
2007
- const selected = home ? home : join(homedir(), ".dsh");
2008
- const expanded = selected === "~" ? homedir() : selected.startsWith("~/") || selected.startsWith("~\\") ? join(homedir(), selected.slice(2)) : selected;
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 = /^- id:\s*(\S+)/.exec(line)?.[1];
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
- for (let index = 0; index < names.length; index += 1) {
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 [...groups.values()].filter((group) => group.length > 1);
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]+)/g;
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
- out = out.replace(CANDIDATE_ASSIGNMENT_PATTERN, (match, lead, key, separator) => {
3848
- if (typeof key !== "string" || !CREDENTIAL_KEY_RE.test(key)) return match;
3849
- return `${lead ?? ""}${key}${separator ?? ""}<redacted>`;
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
- const next = lines[i + 1] ?? "";
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[i + 1] = `${indent}<redacted>${tail ?? ""}`;
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
- library.push(dedup.length === 0 ? sig("dedup_group", "pass", "none", "size >= 2") : sig("dedup_group", "over", dedup.map((group) => group.join(", ")).join(" | "), "size >= 2", `members=${dedup.map((group) => group.join("|")).join(";")}`));
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 — js-yaml, the
4951
- * parser `normalizeFrontmatter` also verifies rewrites with or `null` when
4952
- * the block is not loadable as a YAML mapping. Also reports the platform string
4953
- * fields whose value is not a string ({@link PlatformStringSplit}).
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 = load(block);
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 (/^\.(?:inf|nan)$/i.test(value)) return true;
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** (js-yaml — the same parser the platform catalog uses): if the
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 = load(rewrittenBlock);
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
- return (config.root ?? "").trim() || skillsRoot();
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, isGlobalRead, 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 };
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** (js-yaml — the same parser the platform catalog uses): if the
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 {
@@ -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
- }): string[][];
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
@@ -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 (upstream home-paths:
18
- * `trim().length > 0` is the adoption test — `DSH_HOME=" "` must not produce
19
- * a sidecar under a relative "." path).
20
- * C-11: the adoption test and the RETURNED value now come from the
21
- * SAME trimmed source the old form tested `trim()` but returned the raw
22
- * value, so `DSH_HOME=" /x "` was accepted AND persisted with literal spaces.
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. Behavior now matches upstream:
31
- * `resolve(expandHomePath(selected))`. Only `~`-prefixed and RELATIVE
32
- * DSH_HOME values change landing spot; absolute homes are byte-identical.
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
@@ -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.3.82",
4
+ "version": "0.3.83",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -30,7 +30,7 @@
30
30
  ],
31
31
  "license": "MIT",
32
32
  "dependencies": {
33
- "js-yaml": "^4.2.0"
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"