@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 +12 -3
- package/lib/index.js +103 -100
- package/lib/types/io.d.ts +1 -1
- package/package.json +1 -1
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
|
|
26
|
-
|
|
27
|
-
|
|
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
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
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
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
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: `
|
|
4162
|
+
message: `Skill "${source}" is protected (${protection}).`
|
|
4196
4163
|
};
|
|
4197
|
-
|
|
4198
|
-
|
|
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: `
|
|
4167
|
+
message: `Skill "${source}" not found.`
|
|
4235
4168
|
};
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
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.
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
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:
|
|
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