@lmzhen/dsh-evolution-core 0.3.70 → 0.3.71

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
@@ -1647,11 +1647,11 @@ async function readEvolutionEvents(io, path) {
1647
1647
  * malformed (or unreadable) ARCHIVE is skipped — it never bricks the boot and
1648
1648
  * it is still flagged.
1649
1649
  */
1650
- async function readEvolutionTimeline(io, path) {
1650
+ async function readEvolutionTimeline(io, path, archives) {
1651
1651
  const dir = dirname(path);
1652
1652
  let malformed = false;
1653
1653
  const bySeq = /* @__PURE__ */ new Map();
1654
- for (const name of await listEventArchives(io, path)) {
1654
+ for (const name of archives ?? await listEventArchives(io, path)) {
1655
1655
  const read = await readEvolutionEvents(io, join(dir, name));
1656
1656
  if (read.malformed) malformed = true;
1657
1657
  for (const event of read.events) bySeq.set(event.seq, event);
@@ -3888,6 +3888,65 @@ const DEFAULT_SKILL_LIMITS = {
3888
3888
  maxSkillContentChars: MAX_SKILL_CONTENT_CHARS,
3889
3889
  maxSkillFileBytes: MAX_SKILL_FILE_BYTES
3890
3890
  };
3891
+ /**
3892
+ * Evaluate a stage-time anchor against the bytes a locked read observed.
3893
+ * @param anchor - the caller's anchor, or `undefined` for an unanchored write.
3894
+ * @param current - the bytes the write lock read (`null` = the target is absent).
3895
+ * @returns `match` when the write may proceed, otherwise the refusal verdict.
3896
+ */
3897
+ function anchorVerdict(anchor, current) {
3898
+ if (anchor === void 0) return "match";
3899
+ if ("absent" in anchor) return current === null ? "match" : "drift";
3900
+ if (current === null) return "missing";
3901
+ return contentHash(current) === anchor.sha256 ? "match" : "drift";
3902
+ }
3903
+ /**
3904
+ * Build the refusal for a skill write whose anchor did not hold. The wording is
3905
+ * the library's own; a caller with staged-replay wording (the skill tool, the
3906
+ * review plan) re-words it from {@link SkillActionResult.anchor}.
3907
+ * @param name - the skill name the refusal names.
3908
+ * @param verdict - the non-matching verdict.
3909
+ * @returns the refusal result (nothing was written).
3910
+ */
3911
+ function anchorRefusal(name, verdict) {
3912
+ return {
3913
+ ok: false,
3914
+ stale: true,
3915
+ anchor: verdict,
3916
+ message: verdict === "missing" ? `Skill "${name}" not found.` : `Skill "${name}" changed since it was read; the write was refused to avoid overwriting newer content.`
3917
+ };
3918
+ }
3919
+ /**
3920
+ * Build the refusal for a support-file write/remove whose anchor did not hold.
3921
+ * @param name - the owning skill name.
3922
+ * @param filePath - the support-file path inside the skill.
3923
+ * @param verdict - the non-matching verdict.
3924
+ * @returns the refusal result (nothing was written or removed).
3925
+ */
3926
+ /**
3927
+ * Refusal for a target the locked read could not verify at all (EISDIR, an
3928
+ * unreadable file). A staged replay reports "could not be verified" instead of
3929
+ * propagating an exception: nothing was read, so nothing can have been written.
3930
+ * @param name - the owning skill name.
3931
+ * @param filePath - the support-file path, or `null` for the skill body.
3932
+ * @returns the refusal result.
3933
+ */
3934
+ function anchorUnverifiable(name, filePath) {
3935
+ return {
3936
+ ok: false,
3937
+ stale: true,
3938
+ anchor: "drift",
3939
+ message: filePath === null ? `Skill "${name}" could not be read to verify the staged content.` : `Support file "${filePath}" of "${name}" could not be read to verify the staged content.`
3940
+ };
3941
+ }
3942
+ function anchorRefusalFile(name, filePath, verdict) {
3943
+ return {
3944
+ ok: false,
3945
+ stale: true,
3946
+ anchor: verdict,
3947
+ message: verdict === "missing" ? `File "${filePath}" not found in skill "${name}".` : `Support file "${filePath}" of "${name}" changed since it was read; the write was refused to avoid overwriting newer content.`
3948
+ };
3949
+ }
3891
3950
  /** Upper bound of moves per restructure proposal (validator and core agree). */
3892
3951
  const MAX_RESTRUCTURE_MOVES = 5;
3893
3952
  /** Restructure targets are plain markdown files under references/ — no
@@ -4647,9 +4706,11 @@ var SkillLibrary = class {
4647
4706
  * mutation event are issued ONLY when a write actually lands, so a no-op
4648
4707
  * never inflates the mutation-maturity counter.
4649
4708
  */
4650
- async runSingleWrite(path, task) {
4709
+ async runSingleWrite(path, task, readFailure) {
4651
4710
  let outcome;
4711
+ const progress = { entered: false };
4652
4712
  const run = async (current) => {
4713
+ progress.entered = true;
4653
4714
  const o = await task(current ?? null);
4654
4715
  outcome = {
4655
4716
  ...o,
@@ -4662,11 +4723,18 @@ var SkillLibrary = class {
4662
4723
  if (this.transact) try {
4663
4724
  await this.transact(this.io, path, run);
4664
4725
  } catch (error) {
4726
+ if (!progress.entered && readFailure !== void 0) return readFailure;
4665
4727
  if (!committedOnly(error)) throw error;
4666
4728
  durabilityWarning = error instanceof Error ? error.message : String(error);
4667
4729
  }
4668
4730
  else {
4669
- const current = await this.io.readText(path);
4731
+ let current;
4732
+ try {
4733
+ current = await this.io.readText(path);
4734
+ } catch (error) {
4735
+ if (readFailure !== void 0) return readFailure;
4736
+ throw error;
4737
+ }
4670
4738
  const next = await run(current);
4671
4739
  if (next !== null && next !== current) try {
4672
4740
  await this.io.writeText(path, next);
@@ -4745,7 +4813,14 @@ var SkillLibrary = class {
4745
4813
  throw error;
4746
4814
  }
4747
4815
  }
4748
- async list() {
4816
+ /**
4817
+ * Summarize the skill tree.
4818
+ * @param options - `withContent` attaches each skill's whole SKILL.md body to
4819
+ * its summary (v35 C11): the read this listing already performs is the one the
4820
+ * body would cost again, so a content-consuming caller pays no second pass.
4821
+ * @returns one summary per readable skill directory.
4822
+ */
4823
+ async list(options = {}) {
4749
4824
  const summaries = [];
4750
4825
  for (const name of await listNames(this.root, this.io)) {
4751
4826
  const dir = this.dirOf(name);
@@ -4793,7 +4868,8 @@ var SkillLibrary = class {
4793
4868
  hermesManaged
4794
4869
  ].some((value) => value === null),
4795
4870
  managed: hermesManaged === true,
4796
- ...typeof parsedWhenToUse === "string" && parsedWhenToUse.trim() !== "" ? { whenToUse: parsedWhenToUse } : {}
4871
+ ...typeof parsedWhenToUse === "string" && parsedWhenToUse.trim() !== "" ? { whenToUse: parsedWhenToUse } : {},
4872
+ ...options.withContent === true ? { content: md } : {}
4797
4873
  });
4798
4874
  }
4799
4875
  return summaries;
@@ -5145,11 +5221,11 @@ var SkillLibrary = class {
5145
5221
  ...norm.changed ? { normalizedFrontmatterFields: norm.fields } : {}
5146
5222
  };
5147
5223
  }
5148
- async update(rawName, content, origin = "foreground") {
5224
+ async update(rawName, content, origin = "foreground", anchor) {
5149
5225
  const name = rawName.trim();
5150
- return await this.serial(() => this.updateCore(name, content, origin));
5226
+ return await this.serial(() => this.updateCore(name, content, origin, anchor));
5151
5227
  }
5152
- async updateCore(name, content, origin) {
5228
+ async updateCore(name, content, origin, anchor) {
5153
5229
  const badName = this.badName(name);
5154
5230
  if (badName) return {
5155
5231
  ok: false,
@@ -5186,6 +5262,11 @@ var SkillLibrary = class {
5186
5262
  message: threat
5187
5263
  };
5188
5264
  return await this.runSingleWrite(path, (current) => {
5265
+ const verdict = anchorVerdict(anchor, current);
5266
+ if (verdict !== "match") return {
5267
+ result: anchorRefusal(name, verdict),
5268
+ write: null
5269
+ };
5189
5270
  if (current === null) return {
5190
5271
  result: {
5191
5272
  ok: false,
@@ -5224,7 +5305,7 @@ var SkillLibrary = class {
5224
5305
  skillDir: dir
5225
5306
  }
5226
5307
  };
5227
- });
5308
+ }, anchor !== void 0 ? anchorRefusal(name, "missing") : void 0);
5228
5309
  }
5229
5310
  async patch(rawName, oldString, newString, filePath = "", replaceAll = false, origin = "foreground") {
5230
5311
  const name = rawName.trim();
@@ -6081,11 +6162,11 @@ var SkillLibrary = class {
6081
6162
  path: dest
6082
6163
  };
6083
6164
  }
6084
- async writeSupportFile(rawName, filePath, content, origin = "foreground") {
6165
+ async writeSupportFile(rawName, filePath, content, origin = "foreground", anchor) {
6085
6166
  const name = rawName.trim();
6086
- return await this.serial(() => this.writeSupportFileCore(name, filePath, content, origin));
6167
+ return await this.serial(() => this.writeSupportFileCore(name, filePath, content, origin, anchor));
6087
6168
  }
6088
- async writeSupportFileCore(name, filePath, content, origin) {
6169
+ async writeSupportFileCore(name, filePath, content, origin, anchor) {
6089
6170
  const badName = this.badName(name);
6090
6171
  if (badName) return {
6091
6172
  ok: false,
@@ -6117,6 +6198,11 @@ var SkillLibrary = class {
6117
6198
  };
6118
6199
  const target = join(dir, ...filePath.replace(/\\/g, "/").split("/").filter(Boolean));
6119
6200
  return await this.runSingleWrite(target, (current) => {
6201
+ const verdict = anchorVerdict(anchor, current);
6202
+ if (verdict !== "match") return {
6203
+ result: anchorRefusalFile(name, filePath, verdict),
6204
+ write: null
6205
+ };
6120
6206
  if (current !== null && content.trimEnd() === current.trimEnd()) return {
6121
6207
  result: {
6122
6208
  ok: true,
@@ -6147,13 +6233,13 @@ var SkillLibrary = class {
6147
6233
  file: target
6148
6234
  }
6149
6235
  };
6150
- });
6236
+ }, anchor !== void 0 ? anchorUnverifiable(name, filePath) : void 0);
6151
6237
  }
6152
- async removeSupportFile(rawName, filePath, origin = "foreground") {
6238
+ async removeSupportFile(rawName, filePath, origin = "foreground", anchor) {
6153
6239
  const name = rawName.trim();
6154
- return await this.serial(() => this.removeSupportFileCore(name, filePath, origin));
6240
+ return await this.serial(() => this.removeSupportFileCore(name, filePath, origin, anchor));
6155
6241
  }
6156
- async removeSupportFileCore(name, filePath, origin) {
6242
+ async removeSupportFileCore(name, filePath, origin, anchor) {
6157
6243
  const badName = this.badName(name);
6158
6244
  if (badName) return {
6159
6245
  ok: false,
@@ -6175,17 +6261,25 @@ var SkillLibrary = class {
6175
6261
  message: validation
6176
6262
  };
6177
6263
  const target = join(dir, ...filePath.replace(/\\/g, "/").split("/").filter(Boolean));
6178
- if (!await this.io.exists(target)) return {
6179
- ok: false,
6180
- message: `File "${filePath}" not found in skill "${name}".`
6181
- };
6264
+ if (!await this.io.exists(target)) {
6265
+ if (anchor !== void 0) return anchorRefusalFile(name, filePath, "missing");
6266
+ return {
6267
+ ok: false,
6268
+ message: `File "${filePath}" not found in skill "${name}".`
6269
+ };
6270
+ }
6182
6271
  const before = await this.io.readText(target).catch(() => null);
6183
6272
  if (before === null) return {
6184
6273
  ok: false,
6185
6274
  message: `"${filePath}" is not a readable regular file — remove the files inside it one by one.`
6186
6275
  };
6187
- if (this.transact) await this.transact(this.io, target, () => null);
6188
- else await this.io.remove(target);
6276
+ let verdict = anchorVerdict(anchor, before);
6277
+ if (verdict === "match" && this.transact) await this.transact(this.io, target, (current) => {
6278
+ verdict = anchorVerdict(anchor, current);
6279
+ return verdict === "match" ? null : current;
6280
+ });
6281
+ else if (verdict === "match") await this.io.remove(target);
6282
+ if (verdict !== "match") return anchorRefusalFile(name, filePath, verdict);
6189
6283
  await this.audit(name, "remove_file", before, null, `removed ${filePath}`);
6190
6284
  this.notifyMutation({
6191
6285
  action: "remove_file",
@@ -140,5 +140,5 @@ export declare function readEvolutionEvents(io: EvolutionIoLike, path: string):
140
140
  * malformed (or unreadable) ARCHIVE is skipped — it never bricks the boot and
141
141
  * it is still flagged.
142
142
  */
143
- export declare function readEvolutionTimeline(io: EvolutionIoLike, path: string): Promise<EventLogRead>;
143
+ export declare function readEvolutionTimeline(io: EvolutionIoLike, path: string, archives?: readonly string[]): Promise<EventLogRead>;
144
144
  //# sourceMappingURL=evolution-events.d.ts.map
@@ -31,7 +31,32 @@ export interface SkillSummary {
31
31
  * platform catalog keeps it while this provider shadows the upstream
32
32
  * filesystem provider. Absent when the frontmatter has none. */
33
33
  whenToUse?: string;
34
+ /** v35 C11: the whole SKILL.md body, present only for
35
+ * {@link SkillLibrary.list} calls that pass `{ withContent: true }`. A consumer
36
+ * that needs both the summary fields and the body (tree hashing, enrichment,
37
+ * drift scans) saves the second per-skill read; the default stays body-free so
38
+ * the common listing does not hold a whole tree in memory. */
39
+ content?: string;
34
40
  }
41
+ /**
42
+ * Stage-time anchor for a full-content write (v35 C9): the sha256 the caller read
43
+ * when it staged the plan, or `absent` when the target did not exist then. The
44
+ * library compares it against the bytes it reads INSIDE the write's own lock — the
45
+ * same read the write commits — so a concurrent writer landing between staging and
46
+ * commit is refused instead of silently overwritten.
47
+ */
48
+ export type WriteAnchor = {
49
+ readonly sha256: string;
50
+ } | {
51
+ readonly absent: true;
52
+ };
53
+ /**
54
+ * What the write lock observed about an anchor target.
55
+ * - `match`: the anchor holds; the write proceeds.
56
+ * - `drift`: the target exists with different bytes.
57
+ * - `missing`: the target does not exist (an `absent` anchor `match`es this).
58
+ */
59
+ export type AnchorVerdict = 'match' | 'drift' | 'missing';
35
60
  export interface SkillActionResult {
36
61
  ok: boolean;
37
62
  message: string;
@@ -42,6 +67,12 @@ export interface SkillActionResult {
42
67
  /** 0.3.18 (E-68): patch produced byte-identical content (old===new) — no
43
68
  * write, no audit, no mutation event; callers must not count a patch. */
44
69
  noop?: boolean;
70
+ /** Set when the caller passed a {@link WriteAnchor} that the locked read did
71
+ * not satisfy: nothing was written. Carries {@link SkillActionResult.anchor}
72
+ * so a caller with staged-replay wording can translate it (v35 C9). */
73
+ stale?: true;
74
+ /** The locked read's verdict for the caller's anchor. */
75
+ anchor?: AnchorVerdict;
45
76
  }
46
77
  /**
47
78
  * One section move of a restructure proposal (008 batch B): a body section
@@ -369,7 +400,16 @@ export declare class SkillLibrary {
369
400
  * last-writer-wins.
370
401
  */
371
402
  readSupportFile(name: string, filePath: string): Promise<string | null>;
372
- list(): Promise<SkillSummary[]>;
403
+ /**
404
+ * Summarize the skill tree.
405
+ * @param options - `withContent` attaches each skill's whole SKILL.md body to
406
+ * its summary (v35 C11): the read this listing already performs is the one the
407
+ * body would cost again, so a content-consuming caller pays no second pass.
408
+ * @returns one summary per readable skill directory.
409
+ */
410
+ list(options?: {
411
+ withContent?: boolean;
412
+ }): Promise<SkillSummary[]>;
373
413
  read(rawName: string): Promise<string | null>;
374
414
  /**
375
415
 
@@ -439,7 +479,7 @@ export declare class SkillLibrary {
439
479
  private setPinnedCore;
440
480
  create(name: string, content: string, origin?: WriteOrigin): Promise<SkillActionResult>;
441
481
  private createCore;
442
- update(rawName: string, content: string, origin?: WriteOrigin): Promise<SkillActionResult>;
482
+ update(rawName: string, content: string, origin?: WriteOrigin, anchor?: WriteAnchor): Promise<SkillActionResult>;
443
483
  private updateCore;
444
484
  patch(rawName: string, oldString: string, newString: string, filePath?: string, replaceAll?: boolean, origin?: WriteOrigin): Promise<SkillActionResult>;
445
485
  private patchCore;
@@ -546,9 +586,9 @@ export declare class SkillLibrary {
546
586
  * path back. The `.archive-reason` marker is dropped on restore.
547
587
  */
548
588
  restoreFromArchive(rawName: string): Promise<SkillActionResult>;
549
- writeSupportFile(rawName: string, filePath: string, content: string, origin?: WriteOrigin): Promise<SkillActionResult>;
589
+ writeSupportFile(rawName: string, filePath: string, content: string, origin?: WriteOrigin, anchor?: WriteAnchor): Promise<SkillActionResult>;
550
590
  private writeSupportFileCore;
551
- removeSupportFile(rawName: string, filePath: string, origin?: WriteOrigin): Promise<SkillActionResult>;
591
+ removeSupportFile(rawName: string, filePath: string, origin?: WriteOrigin, anchor?: WriteAnchor): Promise<SkillActionResult>;
552
592
  private removeSupportFileCore;
553
593
  /**
554
594
  * v23 (ML-1): `.archive` retention. Archived skills are recoverable history,
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.70",
4
+ "version": "0.3.71",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },