@lmzhen/dsh-evolution-core 0.3.35 → 0.3.37

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/lib/index.js CHANGED
@@ -423,6 +423,7 @@ function parseUsage(raw) {
423
423
  if (raw === null) return map;
424
424
  try {
425
425
  const parsed = JSON.parse(raw);
426
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return map;
426
427
  for (const [name, record] of Object.entries(parsed)) map.set(name, normalizeUsageRecord(record));
427
428
  } catch {}
428
429
  return map;
@@ -499,6 +500,10 @@ function foldCuratorFields(disk, curated, stateOwned) {
499
500
  if (stateOwned === void 0 || stateOwned.has(name)) applyCuratorLifecycleFields(diskRecord, record);
500
501
  }
501
502
  }
503
+ /** Whole-file usage write (V6-37, 0.3.37): this is the ONE path that bypasses
504
+ * the malformed-defense and the transact lock — prefer `mutateUsage` for any
505
+ * read-modify-write so a concurrent writer cannot lose its update and a
506
+ * malformed sidecar stays recoverable. Kept for fixture/test seeding. */
502
507
  async function saveUsage(root, map, io = nodeEvolutionIo()) {
503
508
  const obj = Object.fromEntries(map.entries());
504
509
  await io.writeText(usageFile(root), JSON.stringify(obj, null, 2));
@@ -746,7 +751,8 @@ function buildCuratorRunReport(input) {
746
751
  failed: [...input.failed],
747
752
  ...input.consolidated === void 0 ? {} : { consolidated: [...input.consolidated] },
748
753
  ...input.snapshotPath === void 0 ? {} : { snapshotPath: input.snapshotPath },
749
- ...input.llmReviewEnabled === void 0 ? {} : { llmReviewEnabled: input.llmReviewEnabled }
754
+ ...input.llmReviewEnabled === void 0 ? {} : { llmReviewEnabled: input.llmReviewEnabled },
755
+ ...input.nominationsWarnings === void 0 ? {} : { nominationsWarnings: [...input.nominationsWarnings] }
750
756
  };
751
757
  }
752
758
  /**
@@ -765,7 +771,8 @@ function renderCuratorReportMarkdown(report) {
765
771
  `- **Archived**: ${report.archived.length}`,
766
772
  `- **Failed**: ${report.failed.length}`,
767
773
  ...report.snapshotPath === void 0 ? [] : [`- **Snapshot**: ${report.snapshotPath}`],
768
- ...report.llmReviewEnabled === void 0 ? [] : [`- **llmReview**: ${report.llmReviewEnabled}`]
774
+ ...report.llmReviewEnabled === void 0 ? [] : [`- **llmReview**: ${report.llmReviewEnabled}`],
775
+ ...report.nominationsWarnings === void 0 || report.nominationsWarnings.length === 0 ? [] : [`- **Nomination warnings**: ${report.nominationsWarnings.join("; ")}`]
769
776
  ];
770
777
  const section = (title, items) => items.length === 0 ? [] : [
771
778
  "",
@@ -791,10 +798,16 @@ const NOMINATION_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
791
798
  function parseCuratorNominations(text) {
792
799
  const prunings = [];
793
800
  const consolidations = [];
801
+ const warnings = [];
794
802
  let section = null;
795
803
  let currentFrom = "";
796
804
  let currentMode;
797
805
  for (const line of text.split("\n")) {
806
+ const header = /^\s*(consolidations|prunings)\s*:\s*$/.exec(line);
807
+ if (header) {
808
+ section = header[1] === "consolidations" ? "consolidations" : "prunings";
809
+ continue;
810
+ }
798
811
  const consolidated = /^\s*-\s*from:\s*([a-z0-9][a-z0-9-]*)\s*$/.exec(line);
799
812
  if (consolidated) {
800
813
  section = "consolidations";
@@ -805,6 +818,7 @@ function parseCuratorNominations(text) {
805
818
  const mode = /^\s*mode:\s*(append|reference)\s*$/.exec(line);
806
819
  if (mode) {
807
820
  if (currentFrom !== "") currentMode = mode[1] === "reference" ? "reference" : "append";
821
+ else warnings.push(`mode: ${mode[1]} ignored — no preceding "- from:" entry (the consolidation falls back to append)`);
808
822
  continue;
809
823
  }
810
824
  const into = /^\s*into:\s*([a-z0-9][a-z0-9-]*)\s*$/.exec(line);
@@ -821,6 +835,7 @@ function parseCuratorNominations(text) {
821
835
  }
822
836
  const pruned = /^\s*-\s*name:\s*([a-z0-9][a-z0-9-]*)\s*$/.exec(line);
823
837
  if (pruned) {
838
+ if (section === "consolidations") warnings.push("\"- name:\" inside the consolidations section flips the parse to prunings");
824
839
  section = "prunings";
825
840
  const name = pruned[1];
826
841
  if (name) prunings.push(name);
@@ -829,7 +844,8 @@ function parseCuratorNominations(text) {
829
844
  const valid = (name) => NOMINATION_NAME_RE.test(name);
830
845
  return {
831
846
  prunings: prunings.filter(valid),
832
- consolidations: consolidations.filter((item) => valid(item.from) && valid(item.into))
847
+ consolidations: consolidations.filter((item) => valid(item.from) && valid(item.into)),
848
+ warnings
833
849
  };
834
850
  }
835
851
  /**
@@ -864,7 +880,8 @@ function computeScopeView(usage, config, protectedNames, gates) {
864
880
  }
865
881
  const bundled = config.bundledNames?.has(name) === true;
866
882
  const suppressed = gateSet.suppressed.has(name);
867
- if (record.pinned || bundled || suppressed || protectedNames?.has(name) === true) protectedSet.add(name);
883
+ const isBuiltin = PROTECTED_BUILTIN_SKILLS.has(name);
884
+ if (record.pinned || bundled || suppressed || protectedNames?.has(name) === true || isBuiltin) protectedSet.add(name);
868
885
  if (lifecycleCandidate(name, record, config, bundled, gateSet)) {
869
886
  managed.push(name);
870
887
  if (record.state === "stale" || record.quality_warn === true) watched.push(name);
@@ -1589,6 +1606,24 @@ function buildLearnPrompt(userRequest) {
1589
1606
  ].join("\n");
1590
1607
  }
1591
1608
  //#endregion
1609
+ //#region lib/types/serial.js
1610
+ /**
1611
+ * A process-local serial task queue: each task starts only after the previous
1612
+ * one settles (success or failure), so read-modify-write sequences that share
1613
+ * one file never interleave inside this process. The durable cross-process
1614
+ * serialization layer is the IO backend's transact lock; this chain is the
1615
+ * second layer (0.3.17 S2.8, T-1: the shape was duplicated in state-json and
1616
+ * memory-files — one factory now).
1617
+ */
1618
+ function makeSerialQueue() {
1619
+ let chain = Promise.resolve();
1620
+ return (task) => {
1621
+ const run = chain.then(task, task);
1622
+ chain = run.then(() => void 0, () => void 0);
1623
+ return run;
1624
+ };
1625
+ }
1626
+ //#endregion
1592
1627
  //#region lib/types/numeric.js
1593
1628
  /**
1594
1629
  * Numeric config clamping for the dsh-evolution plugin family.
@@ -1950,6 +1985,12 @@ var MemoryStore = class {
1950
1985
  root;
1951
1986
  maxFailures;
1952
1987
  io;
1988
+ /** V6-16 (0.3.37): same-process RMW serialization (the SkillLibrary queue) —
1989
+ * on a backend WITHOUT a transact lock two concurrent callers compute on the
1990
+ * same old content and the last rename wins, silently dropping one op's
1991
+ * update. The node backend's cross-process lock already serializes; this
1992
+ * chain covers the no-transact custom backends. */
1993
+ serial = makeSerialQueue();
1953
1994
  failureCount = 0;
1954
1995
  lastFailureAt = 0;
1955
1996
  constructor(options = {}) {
@@ -2056,6 +2097,9 @@ var MemoryStore = class {
2056
2097
  };
2057
2098
  }
2058
2099
  async add(target, facts) {
2100
+ return await this.serial(() => this.addChained(target, facts));
2101
+ }
2102
+ async addChained(target, facts) {
2059
2103
  if (!facts.trim()) return {
2060
2104
  ok: false,
2061
2105
  message: "Content cannot be empty.",
@@ -2161,6 +2205,9 @@ var MemoryStore = class {
2161
2205
  return render(entries) !== raw;
2162
2206
  }
2163
2207
  async applyBatch(target, operations) {
2208
+ return await this.serial(() => this.applyBatchChained(target, operations));
2209
+ }
2210
+ async applyBatchChained(target, operations) {
2164
2211
  if (operations.length === 0) return {
2165
2212
  ok: false,
2166
2213
  message: "operations list is empty.",
@@ -2234,6 +2281,17 @@ var MemoryStore = class {
2234
2281
  if (!working.some((entry) => stripDatePrefix(entry) === body)) working.push(this.addDatePrefix ? `## ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}\n${body}` : body);
2235
2282
  continue;
2236
2283
  }
2284
+ const rawAction = op.action;
2285
+ if (rawAction !== "remove" && rawAction !== "replace") return {
2286
+ result: {
2287
+ ok: false,
2288
+ message: `Operation ${position}: unknown action "${String(rawAction)}" (expected add/remove/replace). No operations were applied.${previewEntries(entries)}`,
2289
+ entries,
2290
+ chars: entries.join(ENTRY_DELIMITER).length,
2291
+ limit: this.limitFor(target)
2292
+ },
2293
+ write: null
2294
+ };
2237
2295
  const needle = (op.old_text ?? "").trim();
2238
2296
  if (!needle) return {
2239
2297
  result: {
@@ -2653,24 +2711,6 @@ function redactSecrets(text) {
2653
2711
  return out;
2654
2712
  }
2655
2713
  //#endregion
2656
- //#region lib/types/serial.js
2657
- /**
2658
- * A process-local serial task queue: each task starts only after the previous
2659
- * one settles (success or failure), so read-modify-write sequences that share
2660
- * one file never interleave inside this process. The durable cross-process
2661
- * serialization layer is the IO backend's transact lock; this chain is the
2662
- * second layer (0.3.17 S2.8, T-1: the shape was duplicated in state-json and
2663
- * memory-files — one factory now).
2664
- */
2665
- function makeSerialQueue() {
2666
- let chain = Promise.resolve();
2667
- return (task) => {
2668
- const run = chain.then(task, task);
2669
- chain = run.then(() => void 0, () => void 0);
2670
- return run;
2671
- };
2672
- }
2673
- //#endregion
2674
2714
  //#region lib/types/skill-health.js
2675
2715
  /**
2676
2716
  * Skill structure-health domain (rc.73 A1, 008 design): a SECOND assessment
@@ -2760,6 +2800,7 @@ function observeEvent(signal, event) {
2760
2800
  return;
2761
2801
  }
2762
2802
  if (event.type === "assistant/message") {
2803
+ if (!Array.isArray(event.data.message.content)) return;
2763
2804
  const text = event.data.message.content.map((block) => block.type === "text" ? block.text : "").join(" ");
2764
2805
  signal.assistantChars += text.length;
2765
2806
  return;
@@ -3327,6 +3368,12 @@ function fuzzyIndexOf(content, pattern, from = 0) {
3327
3368
  }
3328
3369
  return null;
3329
3370
  }
3371
+ /** V6-17 (0.3.37): the fuzzy-patch scan is O(n·m) with no input bound; a
3372
+ * non-exact anchor past these budgets would block the event loop (measured
3373
+ * ~6s at 20k×20k). Exact matches go through the fast `includes` path and stay
3374
+ * allowed regardless of size. */
3375
+ const FUZZY_MAX_PATTERN_CHARS = 4096;
3376
+ const FUZZY_MAX_WORK = 8e6;
3330
3377
  /** Trim leading whitespace of the first line and trailing whitespace of the last line. */
3331
3378
  function trimPatternBoundaries(pattern) {
3332
3379
  const from = pattern.search(/\S/);
@@ -3471,6 +3518,10 @@ var SkillLibrary = class {
3471
3518
  if (next !== null && next !== current) await this.io.writeText(path, next);
3472
3519
  }
3473
3520
  const o = outcome;
3521
+ if (o === void 0 || typeof o !== "object" || !Object.prototype.hasOwnProperty.call(o, "write")) return {
3522
+ ok: false,
3523
+ message: "internal error: the write transaction did not invoke the task; no write was performed"
3524
+ };
3474
3525
  if (o.write !== null && o.audit) await this.audit(o.audit.skillName, o.audit.action, o.audit.before, o.audit.after, o.audit.summary);
3475
3526
  if (o.write !== null && o.event) this.notifyMutation(o.event);
3476
3527
  return o.result;
@@ -3863,6 +3914,13 @@ var SkillLibrary = class {
3863
3914
  },
3864
3915
  write: null
3865
3916
  };
3917
+ if (!md.includes(oldString) && (oldString.length > FUZZY_MAX_PATTERN_CHARS || md.length * oldString.length > FUZZY_MAX_WORK)) return {
3918
+ result: {
3919
+ ok: false,
3920
+ message: `old_string too large for fuzzy match (${oldString.length} chars in ${patchLabel}); use update for a full rewrite or a narrower anchor.`
3921
+ },
3922
+ write: null
3923
+ };
3866
3924
  const patched = fuzzyPatch(md, oldString, newString, replaceAll);
3867
3925
  if (patched === null) return {
3868
3926
  result: {
@@ -4502,6 +4560,15 @@ var SkillLibrary = class {
4502
4560
  };
4503
4561
  const target = join(dir, ...filePath.replace(/\\/g, "/").split("/").filter(Boolean));
4504
4562
  return await this.runSingleWrite(target, (current) => {
4563
+ if (current !== null && content.trimEnd() === current.trimEnd()) return {
4564
+ result: {
4565
+ ok: true,
4566
+ message: `Support file "${filePath}" unchanged: the supplied content already matches the current file; nothing written.`,
4567
+ noop: true,
4568
+ path: target
4569
+ },
4570
+ write: null
4571
+ };
4505
4572
  return {
4506
4573
  result: {
4507
4574
  ok: true,
@@ -63,6 +63,8 @@ export interface CuratorRunReport {
63
63
  snapshotPath?: string;
64
64
  /** Whether the LLM nomination pass was enabled for this run (decision visibility). */
65
65
  llmReviewEnabled?: boolean;
66
+ /** V6-35 (0.3.36): lenient-parse shape notes from the LLM nomination block. */
67
+ nominationsWarnings?: string[];
66
68
  }
67
69
  export interface CuratorReportInput {
68
70
  runId: string;
@@ -76,6 +78,7 @@ export interface CuratorReportInput {
76
78
  consolidated?: readonly CuratorConsolidation[];
77
79
  snapshotPath?: string;
78
80
  llmReviewEnabled?: boolean;
81
+ nominationsWarnings?: readonly string[];
79
82
  }
80
83
  export declare function buildCuratorRunReport(input: CuratorReportInput): CuratorRunReport;
81
84
  /**
@@ -97,6 +100,11 @@ export interface CuratorConsolidation {
97
100
  export interface CuratorNominations {
98
101
  prunings: string[];
99
102
  consolidations: CuratorConsolidation[];
103
+ /** V6-35 (0.3.36): lenient-parse shape notes (an entry the lenient logic
104
+ * silently dropped or re-routed). Parsing stays lenient — these are advisory
105
+ * and flow into the run report so the operator sees why a mode or a pruning
106
+ * went somewhere unexpected. */
107
+ warnings: string[];
100
108
  }
101
109
  /**
102
110
  * Parse the curator LLM's YAML nomination block (consolidations + prunings).
@@ -34,6 +34,12 @@ export declare class MemoryStore {
34
34
  readonly root: string;
35
35
  private readonly maxFailures;
36
36
  private readonly io;
37
+ /** V6-16 (0.3.37): same-process RMW serialization (the SkillLibrary queue) —
38
+ * on a backend WITHOUT a transact lock two concurrent callers compute on the
39
+ * same old content and the last rename wins, silently dropping one op's
40
+ * update. The node backend's cross-process lock already serializes; this
41
+ * chain covers the no-transact custom backends. */
42
+ private readonly serial;
37
43
  private failureCount;
38
44
  private lastFailureAt;
39
45
  constructor(options?: MemoryStoreOptions);
@@ -71,6 +77,7 @@ export declare class MemoryStore {
71
77
  */
72
78
  private oversizedRefusal;
73
79
  add(target: MemoryTarget, facts: string): Promise<MemoryApplyResult>;
80
+ private addChained;
74
81
  /**
75
82
  * Single-entry add inside the transaction: shared checks (oversized,
76
83
  * drift, threat) and the content computation. `raw` is the locked view
@@ -80,6 +87,7 @@ export declare class MemoryStore {
80
87
  /** Canonical-form drift check derived from the locked view (same formula as `detectDrift`, no second read). */
81
88
  private driftFromRaw;
82
89
  applyBatch(target: MemoryTarget, operations: MemoryOperation[]): Promise<MemoryApplyResult>;
90
+ private applyBatchChained;
83
91
  /** Batch RMW inside the transaction. `write: null` = failure/no-op, disk untouched. */
84
92
  private applyBatchCore;
85
93
  renderContext(): Promise<string>;
@@ -10,8 +10,10 @@
10
10
  * `computeQualityScores` (different dimension, different consumers).
11
11
  */
12
12
  export interface SkillHealthThresholds {
13
- /** Soft body limit: body chars at/below stay 'healthy' by size; above ->
14
- * 'warn'; >= 2x -> 'needs-restructure'. */
13
+ /** Soft body limit: a body of `softBodyChars` or MORE -> 'warn'; >= 2x ->
14
+ * 'needs-restructure'. V6-34 (0.3.37): the doc comment used to claim
15
+ * "at/below stay healthy" while the engine warns at `>=` — fixed to the
16
+ * implementation edge (the reason copy said "above"). */
15
17
  softBodyChars: number;
16
18
  /** Stamp-density ceiling per KB of body text: rc.NN / commit shas / ISO
17
19
  * dates per KB at/above this -> 'warn' (log-like content living in the
@@ -73,6 +73,10 @@ export declare function applyCuratorMetaFields(disk: UsageRecord, curated: Usage
73
73
  * by a stale snapshot; without it both pairs apply everywhere.
74
74
  */
75
75
  export declare function foldCuratorFields(disk: UsageMap, curated: UsageMap, stateOwned?: ReadonlySet<string>): void;
76
+ /** Whole-file usage write (V6-37, 0.3.37): this is the ONE path that bypasses
77
+ * the malformed-defense and the transact lock — prefer `mutateUsage` for any
78
+ * read-modify-write so a concurrent writer cannot lose its update and a
79
+ * malformed sidecar stays recoverable. Kept for fixture/test seeding. */
76
80
  export declare function saveUsage(root: string, map: UsageMap, io?: EvolutionIoLike): Promise<void>;
77
81
  export declare function getRecord(map: UsageMap, name: string): UsageRecord;
78
82
  export declare function bumpView(map: UsageMap, name: string, when?: Date): void;
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.35",
4
+ "version": "0.3.37",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },