@lmzhen/dsh-evolution-core 0.3.36 → 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));
@@ -1601,6 +1606,24 @@ function buildLearnPrompt(userRequest) {
1601
1606
  ].join("\n");
1602
1607
  }
1603
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
1604
1627
  //#region lib/types/numeric.js
1605
1628
  /**
1606
1629
  * Numeric config clamping for the dsh-evolution plugin family.
@@ -1962,6 +1985,12 @@ var MemoryStore = class {
1962
1985
  root;
1963
1986
  maxFailures;
1964
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();
1965
1994
  failureCount = 0;
1966
1995
  lastFailureAt = 0;
1967
1996
  constructor(options = {}) {
@@ -2068,6 +2097,9 @@ var MemoryStore = class {
2068
2097
  };
2069
2098
  }
2070
2099
  async add(target, facts) {
2100
+ return await this.serial(() => this.addChained(target, facts));
2101
+ }
2102
+ async addChained(target, facts) {
2071
2103
  if (!facts.trim()) return {
2072
2104
  ok: false,
2073
2105
  message: "Content cannot be empty.",
@@ -2173,6 +2205,9 @@ var MemoryStore = class {
2173
2205
  return render(entries) !== raw;
2174
2206
  }
2175
2207
  async applyBatch(target, operations) {
2208
+ return await this.serial(() => this.applyBatchChained(target, operations));
2209
+ }
2210
+ async applyBatchChained(target, operations) {
2176
2211
  if (operations.length === 0) return {
2177
2212
  ok: false,
2178
2213
  message: "operations list is empty.",
@@ -2246,6 +2281,17 @@ var MemoryStore = class {
2246
2281
  if (!working.some((entry) => stripDatePrefix(entry) === body)) working.push(this.addDatePrefix ? `## ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}\n${body}` : body);
2247
2282
  continue;
2248
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
+ };
2249
2295
  const needle = (op.old_text ?? "").trim();
2250
2296
  if (!needle) return {
2251
2297
  result: {
@@ -2665,24 +2711,6 @@ function redactSecrets(text) {
2665
2711
  return out;
2666
2712
  }
2667
2713
  //#endregion
2668
- //#region lib/types/serial.js
2669
- /**
2670
- * A process-local serial task queue: each task starts only after the previous
2671
- * one settles (success or failure), so read-modify-write sequences that share
2672
- * one file never interleave inside this process. The durable cross-process
2673
- * serialization layer is the IO backend's transact lock; this chain is the
2674
- * second layer (0.3.17 S2.8, T-1: the shape was duplicated in state-json and
2675
- * memory-files — one factory now).
2676
- */
2677
- function makeSerialQueue() {
2678
- let chain = Promise.resolve();
2679
- return (task) => {
2680
- const run = chain.then(task, task);
2681
- chain = run.then(() => void 0, () => void 0);
2682
- return run;
2683
- };
2684
- }
2685
- //#endregion
2686
2714
  //#region lib/types/skill-health.js
2687
2715
  /**
2688
2716
  * Skill structure-health domain (rc.73 A1, 008 design): a SECOND assessment
@@ -2772,6 +2800,7 @@ function observeEvent(signal, event) {
2772
2800
  return;
2773
2801
  }
2774
2802
  if (event.type === "assistant/message") {
2803
+ if (!Array.isArray(event.data.message.content)) return;
2775
2804
  const text = event.data.message.content.map((block) => block.type === "text" ? block.text : "").join(" ");
2776
2805
  signal.assistantChars += text.length;
2777
2806
  return;
@@ -3339,6 +3368,12 @@ function fuzzyIndexOf(content, pattern, from = 0) {
3339
3368
  }
3340
3369
  return null;
3341
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;
3342
3377
  /** Trim leading whitespace of the first line and trailing whitespace of the last line. */
3343
3378
  function trimPatternBoundaries(pattern) {
3344
3379
  const from = pattern.search(/\S/);
@@ -3483,6 +3518,10 @@ var SkillLibrary = class {
3483
3518
  if (next !== null && next !== current) await this.io.writeText(path, next);
3484
3519
  }
3485
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
+ };
3486
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);
3487
3526
  if (o.write !== null && o.event) this.notifyMutation(o.event);
3488
3527
  return o.result;
@@ -3875,6 +3914,13 @@ var SkillLibrary = class {
3875
3914
  },
3876
3915
  write: null
3877
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
+ };
3878
3924
  const patched = fuzzyPatch(md, oldString, newString, replaceAll);
3879
3925
  if (patched === null) return {
3880
3926
  result: {
@@ -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.36",
4
+ "version": "0.3.37",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },