@lmzhen/dsh-evolution-core 0.3.45 → 0.3.46

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
@@ -22,15 +22,24 @@ Independent of request-prefix construction. This package does not alter the asse
22
22
 
23
23
  Skill-library mutations are read-modify-write on one file, so `SkillLibrary`
24
24
  serializes them in-process with a `makeSerialQueue` chain: `update`, `patch`,
25
- `restructure` and `writeSupportFile` run their whole read→validate→write under
26
- one serial task, so two concurrent mutators on one skill never interleave in
27
- this process. Single-file writes (`update`, `patch`, `writeSupportFile`)
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`)
28
29
  additionally run the read and the write inside `transactIo` when a caller
29
30
  injects a `transact` into the constructor — that is the cross-process lock, so
30
31
  two processes sharing `DSH_HOME` cannot interleave their RMW on one file.
31
32
  `create` writes a new file and `archive`/`consolidate` already own a two-phase
32
33
  commit, so they deliberately stay outside the serial chain.
33
34
 
35
+ **0.3.46 residual (documented, per G2.5 precedent):** the low-frequency
36
+ single-file entry points `create`, `archive`, `removeSupportFile` and
37
+ `setPinned` still perform an unlocked read→write (their per-file read is not
38
+ inside the serial/transact task). The race needs a same-process concurrent
39
+ mutator on the SAME skill file, which the serialized entry points above make
40
+ unlikely; the exposure is acknowledged and not locked (收益不抵锁面扩大 —
41
+ adding locks to four low-frequency entry points is not worth the surface).
42
+
34
43
  When the backend provides `transact` (nodeEvolutionIo and the io adapter do),
35
44
  the constructor binds it BY DEFAULT since 0.3.27 — the single-file entry points
36
45
  (`update`, `patch`, `writeSupportFile`, and each per-file piece of
package/lib/index.js CHANGED
@@ -63,7 +63,7 @@ function evolutionIoAdapter(provider) {
63
63
  * sole gate for the self-pid recycle branch. Exported (read-only in practice)
64
64
  * so `io.spec.ts` can drive the self-heal path deterministically.
65
65
  */
66
- const pendingSelfCleanup = /* @__PURE__ */ new Set();
66
+ const pendingSelfCleanup = /* @__PURE__ */ new Map();
67
67
  /**
68
68
  * Retry a rename that a peer is temporarily holding on Windows (EPERM/EBUSY):
69
69
  * a short 50ms backoff, at most 3 retries (~150ms budget), matching the
@@ -170,12 +170,11 @@ function nodeEvolutionIo() {
170
170
  const holder = Number(holderContent.split(":")[0] ?? "");
171
171
  const holderAlive = Number.isInteger(holder) && holder > 0 && isAlive(holder);
172
172
  if (holder === process.pid && pendingSelfCleanup.has(lock)) {
173
- if (await readFile(lock, "utf8").catch(() => "") === holderContent) {
174
- try {
175
- await rm(lock, { force: true });
176
- } catch {}
173
+ const token = pendingSelfCleanup.get(lock);
174
+ if (await readFile(lock, "utf8").catch(() => "") === token) try {
175
+ await rm(lock, { force: true });
177
176
  pendingSelfCleanup.delete(lock);
178
- }
177
+ } catch {}
179
178
  continue;
180
179
  }
181
180
  const staleDead = Number.isInteger(holder) && holder > 0 && Date.now() - st.mtimeMs > 1e3 && !holderAlive;
@@ -208,8 +207,9 @@ function nodeEvolutionIo() {
208
207
  try {
209
208
  return await task();
210
209
  } finally {
211
- if (await readFile(lock, "utf8").catch(() => null) === myClaim) await rm(lock, { force: true }).catch(() => {
212
- pendingSelfCleanup.add(lock);
210
+ if (await readFile(lock, "utf8").catch(() => null) === myClaim) await rm(lock, { force: true }).catch(async () => {
211
+ const body = await readFile(lock, "utf8").catch(() => "");
212
+ pendingSelfCleanup.set(lock, body);
213
213
  });
214
214
  }
215
215
  }
@@ -4148,94 +4148,65 @@ var SkillLibrary = class {
4148
4148
  message: `Invalid skill name "${name}". Use lowercase letters, digits, and hyphens.`
4149
4149
  };
4150
4150
  const targetDir = this.dirOf(targetName);
4151
- const targetMd = await this.io.readText(join(targetDir, "SKILL.md"));
4152
- if (!targetMd) return {
4153
- ok: false,
4154
- message: `Skill "${targetName}" not found.`
4155
- };
4156
4151
  const targetProtection = await this.writeProtection(targetName, origin);
4157
4152
  if (targetProtection) return {
4158
4153
  ok: false,
4159
4154
  message: `Skill "${targetName}" is protected (${targetProtection}).`
4160
4155
  };
4161
- const writes = [];
4162
- if (mode === "append") {
4163
- const parts = [];
4164
- for (const source of normalizedSources) {
4165
- const protection = await this.deleteProtection(source);
4166
- if (protection) return {
4167
- ok: false,
4168
- message: `Skill "${source}" is protected (${protection}).`
4169
- };
4170
- const sourceMd = await this.io.readText(join(this.dirOf(source), "SKILL.md"));
4171
- if (!sourceMd) return {
4172
- ok: false,
4173
- message: `Skill "${source}" not found.`
4174
- };
4175
- const parsed = parseFrontmatter(sourceMd);
4176
- if (!parsed) return {
4177
- ok: false,
4178
- message: `Skill "${source}" has no valid frontmatter; refusing to merge.`
4179
- };
4180
- if (await this.countSupportDirs(source) > 0) return {
4181
- ok: false,
4182
- message: `Consolidation rejected: source "${source}" carries support files — use mode:'reference' or archive the whole package instead.`
4183
- };
4184
- const refs = supportRefs(parsed.body);
4185
- if (refs.length > 0) return {
4186
- ok: false,
4187
- message: `Consolidation rejected: source "${source}" body references support files (${refs.join(", ")}) that would be left behind — use mode:'reference' or archive the whole package instead.`
4188
- };
4189
- parts.push(`\n<!-- consolidated from ${source} at ${(/* @__PURE__ */ new Date()).toISOString()} -->\n${parsed.body.trim()}`);
4190
- }
4191
- const merged = targetMd.trimEnd() + parts.join("\n") + "\n";
4192
- const validation = validateFrontmatter(merged, targetName, this.limits);
4193
- if (validation) return {
4156
+ const referenceWrites = [];
4157
+ const parts = [];
4158
+ if (mode === "append") for (const source of normalizedSources) {
4159
+ const protection = await this.deleteProtection(source);
4160
+ if (protection) return {
4194
4161
  ok: false,
4195
- message: `Consolidation rejected: ${validation}`
4162
+ message: `Skill "${source}" is protected (${protection}).`
4196
4163
  };
4197
- writes.push({
4198
- target: join(targetDir, "SKILL.md"),
4199
- content: merged
4200
- });
4201
- } else {
4202
- for (const source of normalizedSources) {
4203
- const protection = await this.deleteProtection(source);
4204
- if (protection) return {
4205
- ok: false,
4206
- message: `Skill "${source}" is protected (${protection}).`
4207
- };
4208
- const sourceMd = await this.io.readText(join(this.dirOf(source), "SKILL.md"));
4209
- if (!sourceMd) return {
4210
- ok: false,
4211
- message: `Skill "${source}" not found.`
4212
- };
4213
- const parsed = parseFrontmatter(sourceMd);
4214
- if (!parsed) return {
4215
- ok: false,
4216
- message: `Skill "${source}" has no valid frontmatter; refusing to demote.`
4217
- };
4218
- const refs = supportRefs(parsed.body);
4219
- if (refs.length > 0) return {
4220
- ok: false,
4221
- message: `Consolidation rejected: source "${source}" body references support files (${refs.join(", ")}) that would be left behind — archive the whole package instead.`
4222
- };
4223
- const target = join(targetDir, "references", `${source}.md`);
4224
- writes.push({
4225
- target,
4226
- content: `<!-- demoted from ${source} at ${(/* @__PURE__ */ new Date()).toISOString()} -->\n${parsed.body.trim()}\n`
4227
- });
4228
- }
4229
- const pointerLines = normalizedSources.map((source) => `\n${POINTER_LINE_PREFIX}${source}.md`).join("");
4230
- const extended = targetMd.trimEnd() + pointerLines + "\n";
4231
- const validation = validateFrontmatter(extended, targetName, this.limits);
4232
- if (validation) return {
4164
+ const sourceMd = await this.io.readText(join(this.dirOf(source), "SKILL.md"));
4165
+ if (!sourceMd) return {
4233
4166
  ok: false,
4234
- message: `Consolidation rejected: ${validation}`
4167
+ message: `Skill "${source}" not found.`
4235
4168
  };
4236
- writes.push({
4237
- target: join(targetDir, "SKILL.md"),
4238
- content: extended
4169
+ const parsed = parseFrontmatter(sourceMd);
4170
+ if (!parsed) return {
4171
+ ok: false,
4172
+ message: `Skill "${source}" has no valid frontmatter; refusing to merge.`
4173
+ };
4174
+ if (await this.countSupportDirs(source) > 0) return {
4175
+ ok: false,
4176
+ message: `Consolidation rejected: source "${source}" carries support files — use mode:'reference' or archive the whole package instead.`
4177
+ };
4178
+ const refs = supportRefs(parsed.body);
4179
+ if (refs.length > 0) return {
4180
+ ok: false,
4181
+ message: `Consolidation rejected: source "${source}" body references support files (${refs.join(", ")}) that would be left behind — use mode:'reference' or archive the whole package instead.`
4182
+ };
4183
+ parts.push(`\n<!-- consolidated from ${source} at ${(/* @__PURE__ */ new Date()).toISOString()} -->\n${parsed.body.trim()}`);
4184
+ }
4185
+ else for (const source of normalizedSources) {
4186
+ const protection = await this.deleteProtection(source);
4187
+ if (protection) return {
4188
+ ok: false,
4189
+ message: `Skill "${source}" is protected (${protection}).`
4190
+ };
4191
+ const sourceMd = await this.io.readText(join(this.dirOf(source), "SKILL.md"));
4192
+ if (!sourceMd) return {
4193
+ ok: false,
4194
+ message: `Skill "${source}" not found.`
4195
+ };
4196
+ const parsed = parseFrontmatter(sourceMd);
4197
+ if (!parsed) return {
4198
+ ok: false,
4199
+ message: `Skill "${source}" has no valid frontmatter; refusing to demote.`
4200
+ };
4201
+ const refs = supportRefs(parsed.body);
4202
+ if (refs.length > 0) return {
4203
+ ok: false,
4204
+ message: `Consolidation rejected: source "${source}" body references support files (${refs.join(", ")}) that would be left behind — archive the whole package instead.`
4205
+ };
4206
+ const target = join(targetDir, "references", `${source}.md`);
4207
+ referenceWrites.push({
4208
+ target,
4209
+ content: `<!-- demoted from ${source} at ${(/* @__PURE__ */ new Date()).toISOString()} -->\n${parsed.body.trim()}\n`
4239
4210
  });
4240
4211
  }
4241
4212
  const archived = [];
@@ -4245,16 +4216,53 @@ var SkillLibrary = class {
4245
4216
  if (!result.ok) throw new Error(result.message);
4246
4217
  archived.push(source);
4247
4218
  }
4248
- const result = await this.applyTreeChange({
4249
- name: targetName,
4250
- origin,
4251
- protection: "write",
4252
- writes,
4253
- auditAction: "consolidate",
4254
- auditSummary: `consolidated ${normalizedSources.join(", ")} (${mode}) into ${targetName}`,
4255
- eventAction: "consolidate"
4219
+ const result = await this.serial(async () => {
4220
+ const freshTargetMd = await this.io.readText(join(targetDir, "SKILL.md"));
4221
+ if (!freshTargetMd) return {
4222
+ ok: false,
4223
+ message: `Skill "${targetName}" not found.`
4224
+ };
4225
+ const writes = [...referenceWrites];
4226
+ if (mode === "append") {
4227
+ const merged = freshTargetMd.trimEnd() + parts.join("\n") + "\n";
4228
+ const validation = validateFrontmatter(merged, targetName, this.limits);
4229
+ if (validation) return {
4230
+ ok: false,
4231
+ message: `Consolidation rejected: ${validation}`
4232
+ };
4233
+ writes.push({
4234
+ target: join(targetDir, "SKILL.md"),
4235
+ content: merged
4236
+ });
4237
+ } else {
4238
+ const pointerLines = normalizedSources.map((source) => `\n${POINTER_LINE_PREFIX}${source}.md`).join("");
4239
+ const extended = freshTargetMd.trimEnd() + pointerLines + "\n";
4240
+ const validation = validateFrontmatter(extended, targetName, this.limits);
4241
+ if (validation) return {
4242
+ ok: false,
4243
+ message: `Consolidation rejected: ${validation}`
4244
+ };
4245
+ writes.push({
4246
+ target: join(targetDir, "SKILL.md"),
4247
+ content: extended
4248
+ });
4249
+ }
4250
+ return await this.applyTreeChange({
4251
+ name: targetName,
4252
+ origin,
4253
+ protection: "write",
4254
+ writes,
4255
+ auditAction: "consolidate",
4256
+ auditSummary: `consolidated ${normalizedSources.join(", ")} (${mode}) into ${targetName}`,
4257
+ eventAction: "consolidate"
4258
+ });
4256
4259
  });
4257
4260
  if (!result.ok) throw new Error(result.message);
4261
+ return {
4262
+ ok: true,
4263
+ message: `Consolidated ${normalizedSources.join(", ")} into "${targetName}".`,
4264
+ path: targetDir
4265
+ };
4258
4266
  } catch (error) {
4259
4267
  const reason = error instanceof Error ? error.message : String(error);
4260
4268
  const failedRestores = [];
@@ -4272,11 +4280,6 @@ var SkillLibrary = class {
4272
4280
  message: `Consolidation failed and was rolled back: ${reason}`
4273
4281
  };
4274
4282
  }
4275
- return {
4276
- ok: true,
4277
- message: `Consolidated ${normalizedSources.join(", ")} into "${targetName}".`,
4278
- path: targetDir
4279
- };
4280
4283
  }
4281
4284
  /**
4282
4285
  * Content-distribution repair (008 batch B, 009-R kernel): move body
package/lib/types/io.d.ts CHANGED
@@ -64,7 +64,7 @@ export declare function evolutionIoAdapter(provider: () => EvolutionIoLike): Evo
64
64
  * sole gate for the self-pid recycle branch. Exported (read-only in practice)
65
65
  * so `io.spec.ts` can drive the self-heal path deterministically.
66
66
  */
67
- export declare const pendingSelfCleanup: Set<string>;
67
+ export declare const pendingSelfCleanup: Map<string, string>;
68
68
  /**
69
69
  * Retry a rename that a peer is temporarily holding on Windows (EPERM/EBUSY):
70
70
  * a short 50ms backoff, at most 3 retries (~150ms budget), matching the
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.45",
4
+ "version": "0.3.46",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },