@lmzhen/dsh-evolution-core 0.25.8 → 0.25.9

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
@@ -95,3 +95,10 @@ consumer — so the `@mode` target stays at six (decision 10, provisional: keep)
95
95
  | `E-322` | a cross-field parameter constraint would be broken (the field, its floor and the attempted value are included) | raise the value to the named floor, or lower the sibling first |
96
96
  | `E-319` | the confirmation before a create or bare delete went unanswered for `skillWriteConfirmTimeoutSeconds` (`skillWriteConfirm: 'timeout'`), so the write was cancelled | set `skillWriteConfirm` to `auto` (write without asking), keep `ask` (wait until answered), or raise the timeout |
97
97
  Symptom → next step; the exact text of every code (`E-301`–`E-322`) is stored once in
98
+
99
+ ## 单元与撤销(本包负责的契约)
100
+
101
+ - 本包持有单元契约的**类型与折叠**:SkillVersion.unitHead 两形态互斥(代表片 { moves, sources }/路标 { skill }),判别式固定为 moves !== undefined;读端折叠必须带上新字段,否则下一次写就丢。
102
+ - 成员解析与孤路标判定在 src/unit-members.ts:candidateSkills(代表片首位、去重保序)、resolveUnitMembersFromIndexes(只按已知 key、三向校验)、landmarkVerdict 与 staleRouteStubs(可跳 ⟺ 目标存在 ∧ 其索引仍持该 unitId;输出按 skill/version/owner 确定性排序)。
103
+ - 写作端的硬规则:kind 判 previous === null(不是 undefined);path 抄循环里既有的 rel;同一目标写两次时清单保留最后一次写入的 afterRef。
104
+ - 面向读者的完整说明见仓库 docs/units-and-undo.md。
package/lib/index.js CHANGED
@@ -577,6 +577,34 @@ const MEMORY_GUIDANCE_SECTION_ORDER = 11e3;
577
577
  /** Order of the `evolution-skills-guidance` section (last of the two). */
578
578
  const SKILLS_GUIDANCE_SECTION_ORDER = 11100;
579
579
  //#endregion
580
+ //#region lib/types/guards.js
581
+ /**
582
+ * The family's plain-object predicate, in ONE place (v46 G7.1 / audit A8).
583
+ *
584
+ * The audit found the same guard — "a non-null object that is not an array" — rewritten fifteen
585
+ * times across nine packages, four times inside core itself, and with two byte-identical `asRecord`
586
+ * copies in evolution-policy and evolution-threat. This module is the single definition; callers
587
+ * import it instead of spelling the clause again.
588
+ *
589
+ * @module @lmzhen/dsh-evolution-core
590
+ */
591
+ /**
592
+ * Is this a non-null, non-array object (what JSON calls an object)?
593
+ * @param value - the value to test.
594
+ * @returns whether it is a plain object.
595
+ */
596
+ function isRecord(value) {
597
+ return typeof value === "object" && value !== null && !Array.isArray(value);
598
+ }
599
+ /**
600
+ * Read a value as a record, or undefined when it is not one.
601
+ * @param value - the value to read.
602
+ * @returns the value typed as a record, or undefined.
603
+ */
604
+ function asRecord(value) {
605
+ return isRecord(value) ? value : void 0;
606
+ }
607
+ //#endregion
580
608
  //#region lib/types/limits.js
581
609
  /**
582
610
  * Skill content limits: the byte/char budgets every write path validates against.
@@ -766,6 +794,7 @@ function strictFrontmatterValues(block) {
766
794
  if (typeof loaded !== "object" || Array.isArray(loaded)) return null;
767
795
  const values = /* @__PURE__ */ new Map();
768
796
  const split = [];
797
+ let metadata;
769
798
  for (const [key, value] of Object.entries(loaded)) {
770
799
  if (typeof value !== "string" && PLATFORM_STRING_FIELDS.includes(key)) split.push({
771
800
  key,
@@ -783,10 +812,15 @@ function strictFrontmatterValues(block) {
783
812
  values.set(key, `[${value.map((item) => String(item)).join(", ")}]`);
784
813
  continue;
785
814
  }
815
+ if (key === "metadata" && isRecord(value)) {
816
+ metadata = value;
817
+ continue;
818
+ }
786
819
  }
787
820
  return {
788
821
  values,
789
- split
822
+ split,
823
+ metadata
790
824
  };
791
825
  }
792
826
  /**
@@ -858,7 +892,8 @@ function readFrontmatterBlock(content) {
858
892
  body,
859
893
  unsafeValues: unsafeFrontmatterEntries(found.block),
860
894
  strictFailed: strict === null,
861
- platformStringSplit: strict?.split ?? []
895
+ platformStringSplit: strict?.split ?? [],
896
+ metadata: strict?.metadata
862
897
  };
863
898
  }
864
899
  /**
@@ -878,6 +913,7 @@ function parseFrontmatter(content) {
878
913
  frontmatter: read.frontmatter,
879
914
  body: read.body,
880
915
  unsafeValues: read.unsafeValues,
916
+ metadata: read.metadata,
881
917
  catalogInvalid: read.strictFailed || read.unsafeValues.length > 0 || read.platformStringSplit.length > 0,
882
918
  platformStringSplit: read.platformStringSplit
883
919
  };
@@ -1028,7 +1064,10 @@ function normalizeFrontmatter(content) {
1028
1064
  function relatedSkillNames(content, exclude) {
1029
1065
  const parsed = parseFrontmatter(content);
1030
1066
  if (!parsed) return [];
1031
- const raw = parsed.frontmatter["related_skills"];
1067
+ const nestedHost = parsed.metadata?.["hermes"];
1068
+ const nestedRaw = isRecord(nestedHost) ? nestedHost["related_skills"] : void 0;
1069
+ const flat = parsed.frontmatter["related_skills"];
1070
+ const raw = typeof flat === "string" ? flat : Array.isArray(nestedRaw) ? `[${nestedRaw.map((item) => String(item)).join(", ")}]` : void 0;
1032
1071
  if (typeof raw !== "string") return [];
1033
1072
  const names = /* @__PURE__ */ new Set();
1034
1073
  for (const match of Array.from(raw.matchAll(/(?<![A-Za-z0-9_-])[a-z0-9][a-z0-9-]*(?![A-Za-z0-9_-])/g))) {
@@ -1371,34 +1410,6 @@ function admissionVerdict(baseline, ruleId, thresholds = DEFAULT_ADMISSION) {
1371
1410
  };
1372
1411
  }
1373
1412
  //#endregion
1374
- //#region lib/types/guards.js
1375
- /**
1376
- * The family's plain-object predicate, in ONE place (v46 G7.1 / audit A8).
1377
- *
1378
- * The audit found the same guard — "a non-null object that is not an array" — rewritten fifteen
1379
- * times across nine packages, four times inside core itself, and with two byte-identical `asRecord`
1380
- * copies in evolution-policy and evolution-threat. This module is the single definition; callers
1381
- * import it instead of spelling the clause again.
1382
- *
1383
- * @module @lmzhen/dsh-evolution-core
1384
- */
1385
- /**
1386
- * Is this a non-null, non-array object (what JSON calls an object)?
1387
- * @param value - the value to test.
1388
- * @returns whether it is a plain object.
1389
- */
1390
- function isRecord(value) {
1391
- return typeof value === "object" && value !== null && !Array.isArray(value);
1392
- }
1393
- /**
1394
- * Read a value as a record, or undefined when it is not one.
1395
- * @param value - the value to read.
1396
- * @returns the value typed as a record, or undefined.
1397
- */
1398
- function asRecord(value) {
1399
- return isRecord(value) ? value : void 0;
1400
- }
1401
- //#endregion
1402
1413
  //#region lib/types/probe.js
1403
1414
  function probePresent(value) {
1404
1415
  return {
@@ -2491,6 +2502,7 @@ function renderCuratorReportMarkdown(report) {
2491
2502
  ...report.aborted === void 0 ? [] : [`- **Aborted**: ${report.aborted}`],
2492
2503
  ...report.unattributed === void 0 || report.unattributed.length === 0 ? [] : [`- **Unattributed errors**: ${report.unattributed.length}`],
2493
2504
  ...report.snapshotPath === void 0 ? [] : [`- **Snapshot**: ${report.snapshotPath}`],
2505
+ ...report.routeStubs === void 0 ? [] : [`- **Stale route stubs**: ${report.routeStubs.stubs}`],
2494
2506
  ...report.llmReviewEnabled === void 0 ? [] : [`- **llmReview**: ${report.llmReviewEnabled}`],
2495
2507
  ...report.wouldPrune === void 0 ? [] : [report.wouldPrune === null ? "- **Would prune (archiveRetention=report)**: unknown (archive listing unreadable)" : `- **Would prune (archiveRetention=report)**: ${report.wouldPrune.length}`],
2496
2508
  ...report.nominationsWarnings === void 0 || report.nominationsWarnings.length === 0 ? [] : [`- **Nomination warnings**: ${report.nominationsWarnings.join("; ")}`]
@@ -2506,6 +2518,7 @@ function renderCuratorReportMarkdown(report) {
2506
2518
  ...section("Archived", report.archived.map((item) => `${item.name} (${item.reason})`)),
2507
2519
  ...section("Failed", report.failed.map((item) => `${item.name}: ${item.reason}`)),
2508
2520
  ...section("Unattributed", report.unattributed ?? []),
2521
+ ...section("Stale route stubs", (report.routeStubs?.detail ?? []).map((stub) => stub.skill + " v" + String(stub.v) + " -> " + stub.owner + " (" + stub.reason + ")")),
2509
2522
  ...section("Stale candidates", report.staleCandidates),
2510
2523
  ...section("LLM nominations", report.llmNominations),
2511
2524
  ...section("Retention (kept, not deleted)", report.wouldPrune ?? []),
@@ -8717,6 +8730,8 @@ function readVersionEntry(entry) {
8717
8730
  if (typeof candidate.chars !== "number") return null;
8718
8731
  const summary = typeof candidate.summary === "string" && candidate.summary !== "" ? candidate.summary : void 0;
8719
8732
  const path = typeof candidate.path === "string" && candidate.path !== "" ? candidate.path : void 0;
8733
+ const unitId = typeof candidate.unitId === "string" && candidate.unitId !== "" ? candidate.unitId : void 0;
8734
+ const unitHead = candidate.unitHead;
8720
8735
  const base = {
8721
8736
  v: candidate.v,
8722
8737
  at: candidate.at,
@@ -8724,7 +8739,9 @@ function readVersionEntry(entry) {
8724
8739
  hash: candidate.hash,
8725
8740
  chars: candidate.chars,
8726
8741
  ...summary === void 0 ? {} : { summary },
8727
- ...path === void 0 ? {} : { path }
8742
+ ...path === void 0 ? {} : { path },
8743
+ ...unitId === void 0 ? {} : { unitId },
8744
+ ...unitHead === void 0 ? {} : { unitHead }
8728
8745
  };
8729
8746
  return typeof candidate.beforeHash === "string" && candidate.beforeHash !== "" ? {
8730
8747
  ...base,
@@ -8796,11 +8813,14 @@ function nextHistoryIndex(versions, input, keep) {
8796
8813
  action: BASELINE_ACTION,
8797
8814
  hash: beforeHash,
8798
8815
  chars: input.before?.length ?? 0,
8799
- ...pathField
8816
+ ...pathField,
8817
+ ...input.unitId === void 0 ? {} : { unitId: input.unitId }
8800
8818
  });
8801
8819
  const removalHash = afterHash === null ? beforeHash : null;
8802
8820
  if (afterHash !== null) {
8803
8821
  if (grown[grown.length - 1]?.hash !== afterHash) grown.push({
8822
+ ...input.unitId === void 0 ? {} : { unitId: input.unitId },
8823
+ ...input.unitHead === void 0 ? {} : { unitHead: input.unitHead },
8804
8824
  v: nextVersion(grown),
8805
8825
  at: input.at,
8806
8826
  action: input.action,
@@ -8820,10 +8840,39 @@ function nextHistoryIndex(versions, input, keep) {
8820
8840
  hash: removalHash,
8821
8841
  chars: input.before?.length ?? 0,
8822
8842
  ...pathField,
8843
+ ...input.unitId === void 0 ? {} : { unitId: input.unitId },
8844
+ ...input.unitHead === void 0 ? {} : { unitHead: input.unitHead },
8823
8845
  ...input.summary === void 0 ? {} : { summary: input.summary }
8824
8846
  });
8825
8847
  }
8826
- const trimmed = grown.slice(Math.max(0, grown.length - clampKeep(keep)));
8848
+ const budget = clampKeep(keep);
8849
+ const groups = [];
8850
+ const byUnit = /* @__PURE__ */ new Map();
8851
+ for (const entry of grown) {
8852
+ const unit = entry.unitId;
8853
+ if (unit === void 0) {
8854
+ groups.push([entry]);
8855
+ continue;
8856
+ }
8857
+ const found = byUnit.get(unit);
8858
+ if (found !== void 0) {
8859
+ found.push(entry);
8860
+ continue;
8861
+ }
8862
+ const made = [entry];
8863
+ byUnit.set(unit, made);
8864
+ groups.push(made);
8865
+ }
8866
+ const keptIndexes = /* @__PURE__ */ new Set();
8867
+ let keptCount = 0;
8868
+ for (let i = groups.length - 1; i >= 0; i -= 1) {
8869
+ const group = groups[i];
8870
+ if (group === void 0) continue;
8871
+ if (keptCount > 0 && keptCount + group.length > budget) break;
8872
+ for (const entry of group) keptIndexes.add(grown.indexOf(entry));
8873
+ keptCount += group.length;
8874
+ }
8875
+ const trimmed = grown.filter((_, index) => keptIndexes.has(index));
8827
8876
  const recorded = {};
8828
8877
  if (beforeHash !== null) {
8829
8878
  const v = lastVersionOf(trimmed, beforeHash);
@@ -9133,6 +9182,141 @@ function elapsedSince(at, nowMs) {
9133
9182
  };
9134
9183
  }
9135
9184
  //#endregion
9185
+ //#region lib/types/unit-members.js
9186
+ /** Candidate skills: head first, then `sources[]` order, DEDUPLICATED (sources may name the head). */
9187
+ function candidateSkills(head) {
9188
+ const out = [head.skill];
9189
+ for (const source of head.sources) if (!out.includes(source.skill)) out.push(source.skill);
9190
+ return out;
9191
+ }
9192
+ /** @param indexes - skill name -> that skill's index text, or null when the index file is absent. */
9193
+ /** The `path` of one manifest move, or the empty string when the record carries no string one.
9194
+ * Kept as a function so the read never stringifies an object (the linter refuses
9195
+ * `String(unknown)` for exactly that reason) and every call site reads the same way. */
9196
+ function movePath(move) {
9197
+ const raw = move.path;
9198
+ return typeof raw === "string" ? raw : "";
9199
+ }
9200
+ function resolveUnitMembersFromIndexes(head, indexes, readEntries) {
9201
+ const members = [];
9202
+ const missing = [];
9203
+ for (const skill of candidateSkills(head)) {
9204
+ const text = indexes.get(skill);
9205
+ if (text === void 0 || text === null) {
9206
+ missing.push({
9207
+ skill,
9208
+ reason: "index-absent"
9209
+ });
9210
+ continue;
9211
+ }
9212
+ const entries = readEntries(text).filter((entry) => entry.unitId === head.unitId);
9213
+ if (entries.length === 0) {
9214
+ missing.push({
9215
+ skill,
9216
+ reason: "unit-absent"
9217
+ });
9218
+ continue;
9219
+ }
9220
+ members.push({
9221
+ skill,
9222
+ entries
9223
+ });
9224
+ }
9225
+ const conflicts = [];
9226
+ const manifest = head.unitHead?.moves ?? void 0;
9227
+ if (manifest !== void 0) {
9228
+ const shardPaths = [];
9229
+ for (const slice of members) for (const entry of slice.entries) for (const move of entry.unitHead?.moves ?? []) {
9230
+ const path = movePath(move);
9231
+ if (path !== "") shardPaths.push(path);
9232
+ }
9233
+ const wanted = manifest.map((move) => movePath(move)).filter((p) => p !== "");
9234
+ for (const path of wanted) if (!shardPaths.includes(path)) conflicts.push({
9235
+ kind: "missing",
9236
+ path
9237
+ });
9238
+ for (const path of shardPaths) if (!wanted.includes(path)) conflicts.push({
9239
+ kind: "foreign",
9240
+ path
9241
+ });
9242
+ for (const path of wanted) if (shardPaths.filter((p) => p === path).length > 1) conflicts.push({
9243
+ kind: "duplicated",
9244
+ path
9245
+ });
9246
+ }
9247
+ return {
9248
+ members,
9249
+ missing,
9250
+ conflicts
9251
+ };
9252
+ }
9253
+ /** A unit's members are trustworthy only when nothing is missing and nothing conflicts. */
9254
+ function unitMembersAreComplete(members) {
9255
+ return members.missing.length === 0 && members.conflicts.length === 0;
9256
+ }
9257
+ /**
9258
+ * The io shell (A19.2): open ONLY the known keys -- never enumerate a directory. A skill whose index
9259
+ * file is unreadable becomes `null` (index-absent), which the core then reports by name.
9260
+ */
9261
+ async function resolveUnitMembers(root, io, head, indexPath, readEntries) {
9262
+ const indexes = /* @__PURE__ */ new Map();
9263
+ for (const skill of candidateSkills(head)) {
9264
+ let text = null;
9265
+ try {
9266
+ text = await io.readText(indexPath(root, skill));
9267
+ } catch {
9268
+ text = null;
9269
+ }
9270
+ indexes.set(skill, text);
9271
+ }
9272
+ return resolveUnitMembersFromIndexes(head, indexes, (raw) => readEntries(raw));
9273
+ }
9274
+ /** G3b: a LANDMARK carries the owning skill and no manifest (the two forms never coexist). */
9275
+ function isLandmark(entry) {
9276
+ const head = entry.unitHead;
9277
+ return head !== void 0 && head.moves === void 0 && head.skill !== void 0;
9278
+ }
9279
+ /** @param entry - the entry being judged. @param ownerEntries - the owner skill's index, or null when
9280
+ * the owner has no readable index. */
9281
+ function landmarkVerdict(entry, ownerEntries) {
9282
+ const head = entry.unitHead;
9283
+ if ((isLandmark(entry) ? head?.skill : void 0) === void 0 || entry.unitId === void 0) return {
9284
+ jumpable: false,
9285
+ reason: "not-a-landmark"
9286
+ };
9287
+ if (ownerEntries === null) return {
9288
+ jumpable: false,
9289
+ reason: "owner-absent"
9290
+ };
9291
+ return ownerEntries.some((candidate) => candidate.unitId === entry.unitId) ? { jumpable: true } : {
9292
+ jumpable: false,
9293
+ reason: "unit-absent"
9294
+ };
9295
+ }
9296
+ /**
9297
+ * Scan a whole corpus (skill name -> index entries, or null when unreadable) for stale route stubs.
9298
+ * Sorted by (skill, version, owner): a function of the corpus, not of the caller's map order.
9299
+ */
9300
+ function staleRouteStubs(indexes) {
9301
+ const out = [];
9302
+ for (const [skill, entries] of indexes) {
9303
+ if (entries === null) continue;
9304
+ for (const entry of entries) {
9305
+ const head = entry.unitHead;
9306
+ const owner = isLandmark(entry) ? head?.skill : void 0;
9307
+ if (owner === void 0) continue;
9308
+ const verdict = landmarkVerdict(entry, indexes.get(owner) ?? null);
9309
+ if (!verdict.jumpable && verdict.reason !== "not-a-landmark") out.push({
9310
+ skill,
9311
+ v: entry.v,
9312
+ owner,
9313
+ reason: verdict.reason
9314
+ });
9315
+ }
9316
+ }
9317
+ return out.sort((a, b) => a.skill.localeCompare(b.skill) || a.v - b.v || a.owner.localeCompare(b.owner));
9318
+ }
9319
+ //#endregion
9136
9320
  //#region lib/types/skill-store.js
9137
9321
  /**
9138
9322
  * Skill library management for the self-evolution plugin.
@@ -9206,6 +9390,7 @@ function pointerBlock(rel, sectionText) {
9206
9390
  function collisionsNote(collisions) {
9207
9391
  return collisions.length === 0 ? "" : ` collisions: ${collisions.slice(0, 5).join(", ")}`;
9208
9392
  }
9393
+ let nextUnitSeq = 0;
9209
9394
  /**
9210
9395
  * Evaluate a stage-time anchor against the bytes a locked read observed.
9211
9396
  * @param anchor - the caller's anchor, or `undefined` for an unanchored write.
@@ -10005,7 +10190,7 @@ var SkillLibrary = class {
10005
10190
  * mutation ledger that points at it. The history failure is not silent — a deployment whose
10006
10191
  * history stopped growing is a degradation worth seeing once (the ledger keeps hashes either
10007
10192
  * way). */
10008
- async audit(skillName, action, before, after, summary, path, origin) {
10193
+ async audit(skillName, action, before, after, summary, path, unitId, unitHead, origin) {
10009
10194
  const at = (/* @__PURE__ */ new Date()).toISOString();
10010
10195
  let versions = null;
10011
10196
  try {
@@ -10016,7 +10201,9 @@ var SkillLibrary = class {
10016
10201
  before,
10017
10202
  after,
10018
10203
  at,
10019
- ...path === void 0 ? {} : { path }
10204
+ ...path === void 0 ? {} : { path },
10205
+ ...unitId === void 0 ? {} : { unitId },
10206
+ ...unitHead === void 0 ? {} : { unitHead }
10020
10207
  };
10021
10208
  versions = await recordVersions(this.root, this.io, input, keep);
10022
10209
  } catch (error) {
@@ -11077,8 +11264,7 @@ var SkillLibrary = class {
11077
11264
  message: badName
11078
11265
  };
11079
11266
  const dir = this.dirOf(name);
11080
- const md = await this.io.readText(join(dir, "SKILL.md"));
11081
- if (!md) return {
11267
+ if (!await this.io.readText(join(dir, "SKILL.md"))) return {
11082
11268
  ok: false,
11083
11269
  message: `技能「${name}」不存在。`
11084
11270
  };
@@ -11162,10 +11348,26 @@ var SkillLibrary = class {
11162
11348
  message: `目录树变更失败:${reason}。回滚无法恢复 ${stuck.join(", ")}:请从 .backups 快照恢复这些目标(或重新应用这次变更)。`
11163
11349
  };
11164
11350
  }
11165
- const skillMdWrite = landing.find((entry) => entry.target.split(/[\\/]/).pop() === "SKILL.md");
11166
- const auditedBefore = skillMdWrite === void 0 ? md : skillMdWrite.previous;
11167
- const auditedAfter = skillMdWrite === void 0 ? md : skillMdWrite.content;
11168
- await this.audit(name, plan.auditAction, auditedBefore, auditedAfter, plan.auditSummary, void 0, plan.origin);
11351
+ const unitId = plan.unitId ?? "u-" + Date.now().toString(36) + "-" + (nextUnitSeq += 1).toString(36);
11352
+ for (const land of landing) {
11353
+ const rel = land.target.split(/[\\/]/).pop() === "SKILL.md" ? void 0 : land.target.slice(dir.length + 1).split("\\").join("/");
11354
+ const unitHead = land === landing[0] ? {
11355
+ moves: landing.filter((l) => l.target.split(/[\\/]/).pop() !== "SKILL.md").map((l) => {
11356
+ const p = l.target.slice(dir.length + 1).split(String.fromCharCode(92)).join(String.fromCharCode(47));
11357
+ return {
11358
+ path: p,
11359
+ kind: l.previous === null ? "created" : "moved",
11360
+ to: p,
11361
+ ...l.previous === null ? {} : { beforeRef: contentHash(l.previous) },
11362
+ afterRef: contentHash(l.content)
11363
+ };
11364
+ }).filter((m, idx, all) => all.map((x) => x.path).lastIndexOf(m.path) === idx),
11365
+ sources: [{ skill: name }]
11366
+ } : void 0;
11367
+ const auditedBefore = land.previous;
11368
+ const auditedAfter = land.content;
11369
+ await this.audit(name, plan.auditAction, auditedBefore, auditedAfter, plan.auditSummary, rel, unitId, unitHead, plan.origin);
11370
+ }
11169
11371
  this.notifyMutation({
11170
11372
  action: plan.eventAction,
11171
11373
  name,
@@ -11366,11 +11568,11 @@ var SkillLibrary = class {
11366
11568
  };
11367
11569
  }, anchor !== void 0 ? () => anchorUnverifiable(name, filePath) : (error) => unreadableTarget(name + "/" + filePath, error));
11368
11570
  }
11369
- async removeSupportFile(rawName, filePath, origin = "foreground", anchor) {
11571
+ async removeSupportFile(rawName, filePath, origin = "foreground", anchor, unitId) {
11370
11572
  const name = rawName.trim();
11371
- return await this.serial(() => this.removeSupportFileCore(name, filePath, origin, anchor));
11573
+ return await this.serial(() => this.removeSupportFileCore(name, filePath, origin, anchor, unitId));
11372
11574
  }
11373
- async removeSupportFileCore(name, filePath, origin, anchor) {
11575
+ async removeSupportFileCore(name, filePath, origin, anchor, unitId) {
11374
11576
  const badName = this.badName(name);
11375
11577
  if (badName) return {
11376
11578
  ok: false,
@@ -11424,7 +11626,7 @@ var SkillLibrary = class {
11424
11626
  };
11425
11627
  } else if (verdict === "match") await this.io.remove(target);
11426
11628
  if (verdict !== "match") return anchorRefusalFile(name, filePath, verdict);
11427
- await this.audit(name, "remove_file", lockedBefore, null, `removed ${filePath}`, filePath);
11629
+ await this.audit(name, "remove_file", lockedBefore, null, `removed ${filePath}`, filePath, unitId);
11428
11630
  this.notifyMutation({
11429
11631
  action: "remove_file",
11430
11632
  name,
@@ -13759,4 +13961,4 @@ function foldUntrustedLines(text, limit) {
13759
13961
  return limit > 0 ? marked.slice(0, limit) : marked;
13760
13962
  }
13761
13963
  //#endregion
13762
- export { AGENT_PRESET_PACKAGE, AGENT_PRESET_ROOT_HINT, AUTHORING_DESCRIPTION_BAR, AUTHORING_SPLIT_LINE_CHARS, BASELINE_ACTION, CITATION_SUPPORT_DIRS, COMBINED_REVIEW_PLAN_PROMPT, COMBINED_REVIEW_PROMPT, COMPLETION_SKILL_REVIEW_PROMPT, CONTENT_SPLIT_HINT, COST_ASCII_TOKEN_HIGH, COST_ASCII_TOKEN_LOW, COST_CJK_TOKEN_HIGH, COST_CJK_TOKEN_LOW, CURATOR_DRY_RUN_BANNER, CURATOR_PROMPT, DEDUP_MAX_PAIR_COMPARISONS, DEDUP_SIMILARITY_THRESHOLD, DEFAULT_ADMISSION, DEFAULT_ARCHIVE_AFTER_DAYS, DEFAULT_ARCHIVE_RETENTION_POLICY, DEFAULT_CITATION_POLICY, DEFAULT_CITATION_REF_BUDGET, DEFAULT_CONSOLIDATION_FAILURES, DEFAULT_CURATOR_BOOT_GRACE_SECONDS, DEFAULT_CURATOR_INTERVAL_HOURS, DEFAULT_CURATOR_MODEL, DEFAULT_CURATOR_REVIEW_MAX_TOKENS, DEFAULT_HEALTH_THRESHOLDS, DEFAULT_MAX_OPS_PER_PLAN, DEFAULT_MEMORY_CHAR_LIMIT, DEFAULT_MEMORY_REVIEW_MODEL, DEFAULT_MIN_IDLE_HOURS, DEFAULT_MUTATION_CAP, DEFAULT_PROBE_TIMEOUT_MS, DEFAULT_REFERENCE_REWRITE_POLICY, DEFAULT_REVIEW_CONTEXT_MESSAGES, DEFAULT_REVIEW_MEMORY_INTERVAL, DEFAULT_REVIEW_MESSAGE_CHARS, DEFAULT_REVIEW_SKILL_INTERVAL, DEFAULT_REVIEW_TIMEOUT_MS, DEFAULT_RUN_RECORDS, DEFAULT_SKILL_CONTENT_CHARS, DEFAULT_SKILL_LIMITS, DEFAULT_SKILL_REVIEW_COMPLETION_MIN_TOOL_CALLS, DEFAULT_SKILL_REVIEW_MODEL, DEFAULT_SKILL_REVIEW_TRIGGER, DEFAULT_SKILL_VERSION_KEEP, DEFAULT_STALE_AFTER_DAYS, DEFAULT_SUBSTANTIVE_MIN_AGENT_CHARS, DEFAULT_SUBSTANTIVE_MIN_TOOL_CALLS, DEFAULT_SUBSTANTIVE_MIN_USER_CHARS, DEFAULT_SUPPORT_FILE_CHAR_POLICY, DEFAULT_SUPPORT_READ_TOOL_NAMES, DEFAULT_THREAT_MAX_SCAN_CHARS, DEFAULT_USER_CHAR_LIMIT, DISPATCH_EVENT_TYPES, DRIFT_MAX_LINE_CHARS, DRIFT_SIGNALS_VERSION, DRIFT_SIGNAL_NOUNS, DSH_AUTHORING_STANDARDS, EMPTY_PATCH_SEED, ENTRY_DELIMITER, EVENT_ARCHIVE_PREFIX, EVENT_LOG_RETAIN_ARCHIVES, EVENT_LOG_ROTATE_AT, EVENT_LOG_VERSION, EVOLUTION_ERRORS, EVOLUTION_WRITE_TOOLS, EvolutionGateSet, FAMILY_SESSION_TOOL_NAMES, FORBIDDEN_CONTROL_KEYS, FUZZY_MAX_PATTERN_CHARS, FUZZY_MAX_WORK, HEALTH_STAMP_RE, HISTORY_DIR, HISTORY_INDEX_VERSION, HOOK_LINE_RE, INSTANCE_KEYS, KEEP_LINE_RE, LONG_RUNS, LOW_QUALITY_THRESHOLD, MAINTAIN_OUTPUT_INSTRUCTION, MAINTAIN_PROMPT, MAX_DESCRIPTION_LENGTH, MAX_REQUEST_BODY_BYTES, MAX_RESTRUCTURE_MOVES, MAX_SKILL_CONTENT_CHARS, MAX_SKILL_FILE_BYTES, MAX_SKILL_NAME_LENGTH, MAX_SUPPORT_READ_PATHS, MAX_TIMER_DELAY_MS, MEMORY_GUIDANCE_SECTION_ORDER, MEMORY_REVIEW_PROMPT, MIN_STAMP_BODY_CHARS, MUTATIONS_FILE_VERSION, MemoryStore, NAMESPACE_MIGRATIONS, NATIVE_CALL_EVENT, NATIVE_RESULT_EVENT, PARAM_ALIASES, PARAM_EXPOSURE, PARAM_GROUP_LABELS, PARAM_NAMESPACES, PATTERN_OVERLAP, PLATFORM_CATALOG_DESCRIPTION_DEFAULT, PLATFORM_ESTIMATE_CHARS_PER_TOKEN, PLATFORM_SERVICE_PROBES, POLICY_STAGE_DEFAULTS, PROMPT_BUNDLE, PROMPT_BUNDLE_ID, PROMPT_BUNDLE_VERSION, PROTECTED_BUILTIN_SKILLS, PTC_DISPATCH_EVENT, PTC_DISPATCH_START_EVENT, QUALITY_WEIGHTS, READ_REQUIRED_ACTIONS, RESTRUCTURE_TARGET_RE, RETENTION_FEEDBACK_KEEP_RATIO, RETENTION_FEEDBACK_MIN_CHARS, RULES, RUNS_SCHEMA_VERSION, SKILLS_GUIDANCE, SKILLS_GUIDANCE_SECTION_ORDER, SKILL_ACTION_REQUIRED_FIELDS, SKILL_NAME_RE, SKILL_REVIEW_PLAN_PROMPT, SKILL_REVIEW_PROMPT, SNAPSHOT_EXTRA_NAME_RE, SUMMARY_DUPLICATE_HINT_THRESHOLD, SUPPORT_DIRS, SUPPORT_SPRAWL_FILES, SUPPRESSED_FILE_VERSION, SkillLibrary, TEXT_DIFF_MAX_CHARS, TEXT_DIFF_MAX_LINES, THREAT_EXEMPTION_HINT, TOKEN_ASCII_WEIGHT, TOKEN_CJK_WEIGHT, ToolDispatchNormalizer, ToolDispatchPayloadError, UNTRUSTED_LINE_BREAK, UNTRUSTED_LINE_MARK, UPSTREAM_LIMIT_CHARS_PER_TOKEN, VERSION_SUMMARY_MAX_CHARS, absentHostServices, admissionVerdict, advanceReview, affinity, ageCopyKey, allowRowCollisions, anchorVerdict, appendEvolutionEvent, applyCuratorLifecycleFields, applyCuratorMetaFields, applyNamespaceMigration, applyReferenceRewrite, asRecord, assertDispatchPayload, assertSkillsRootAliasRetired, assessStructureHealth, atomicWriteFiles, authoringFeedback, basePresetPlugins, blobPath, bodyCost, buildCuratorRunReport, buildHealthBaseline, buildLearnPrompt, bumpPatch, bumpSupportRead, bumpUse, bumpView, callingScope, canonicalWriteId, claimInstance, clampOnce, clampedNumber, classifyNamespaceMigration, clearReviewChannel, collectReadSkillNames, composePresetComposition, composePresetEntry, composePresetInsert, composePresetRow, compositionRowIds, computeDedupGroups, computeDriftSignals, computeLifecycleTransitions, computePrefixClusters, computeQualityScores, computeScopeView, contentHash, contentRetentionFeedback, countDispatches, countReferenceFiles, createGateSet, daysSinceIso, declaredReportStamp, denseBlockSequence, describeReferenceRewrite, diffFindings, dispatchKeyOf, dispatchStepOf, displayBodyOf, duplicateHeadings, elapsedSince, emptyRecord, entryTarget, errorText, evaluateThreat, eventsFile, evidenceKindIndex, evolutionActivityProjection, evolutionEventPayloadIssue, evolutionEvidenceProjection, evolutionHome, evolutionIoAdapter, evolutionReadsProjection, evolutionRoot, evolutionSignalsProjection, filterUnreadSkillOps, findDriftSignal, foldCuratorFields, foldToolDispatches, foldTurnEvent, foldTurnEvents, foldUntrustedLines, frontmatterBlock, frontmatterCatalogInvalid, fuzzyPatch, getRecord, h2Sections, historyIndexFile, historyRoot, identity, idleDays, initialTurnFold, installEvolutionProjections, instanceClaimKey, instanceClaimedWriteSites, instanceHolder, isAbsent, isBookkeepingFrame, isCommittedWarning, isDeprecatedParamId, isFileShapedPath, isLoopbackRequest, isMissingPath, isPresent, isProcessAlive, isProgramToolName, isRecord, isReviewChannelSession, isSkillReadToolName, isSkillToolName, isUnknown, isUnreadWrite, latestActivityAt, latestVersionAt, lifecycleCandidate, lintBody, lintSkill, listDirectories, listEventArchives, liveBool, liveEnum, liveNumber, liveString, loadMutations, loadRowOverrides, loadSuppressedNames, loadUsage, loadVersionContent, loadVersions, makeSerialQueue, mapProbe, markAgentCreated, markReviewChannel, markerEntryName, memoryRoot, mergePresetRow, missingSupportPointers, mutateUsage, mutationsFile, narrowNameMatches, nearDuplicateSummaries, neutralizePromptVariables, newRunRegistry, newSkillLibrary, nextHistoryIndex, nodeEvolutionIo, normalizeFrontmatter, normalizeUsageRecord, normalizeVersionSummary, observeEvent, orderVersions, overlongLines, paramNamespace, paramRowId, paramSettingsId, parseCuratorNominations, parseEvolutionEvents, parseFrontmatter, parseHistoryIndex, partitionVersions, payloadText, persistedWriteSite, persistedWriteSites, pickWithPolicy, planNamespaceMigration, planReferenceRewrite, planRehoming, policyStageLimits, presetPatchText, presetRowBlock, presetRowBody, presetRowId, probeAbsent, probeDesktopScope, probeList, probeMtime, probePresent, probeReason, probeText, probeUnknown, projectionText, readDispatchSignal, readEvolutionEvents, readEvolutionTimeline, readHistoryIndex, readJsonBody, readJsonObject, readNumberParam, readParam, readingStats, recordMutation, recordVersions, redactSecrets, relatedSkillNames, releaseInstance, removePresetRow, renameWithRetry, renderCuratorReportMarkdown, renderLongRunTable, reportTime, reportsSweepLockTarget, resolveBasePresetPatch, resolveCitations, resolveExecOrigins, resolveOrigins, resolveParamId, resolveRootConfig, resolveSkillsRoot, restructureCandidates, retainEventArchives, retirementReport, reviewPrompt, rowBlockSpan, rowSiblingEnd, runsFile, saveSuppressedNames, saveUsage, scanBodyHooks, scanContentThreats, scanMemoryThreats, scanThreats, scopedProbeReport, sessionAudited, sessionEvidenceIndex, sessionLastEventTime, sessionReadNames, sessionReadSkillNames, sessionSeesFamilyTools, sessionTurnSignals, skillReadNameOf, skillRowCells, skillStateKey, skillsRoot, supportFileReadOf, suppressedFile, sweepReports, sweepReviewChannelSessions, textDiffFacts, tokenLineFor, transactIo, transactTaskGuard, trimPatternBoundaries, trimTrailingBlankLines, unhookedSupportPointers, unwrapLive, updateSuppressedNames, usageFile, usageObserved, userSetKeys, validateFrontmatter, validateRestructureTarget, valueOr, verifyPromptBundle, versionActionKind, versionRowCells, versionSummaryPrompt, versionTarget, vocabulary, writeDurableTmp, writeJson, writePathSymbols, yamlPlainScalarNeedsQuotes };
13964
+ export { AGENT_PRESET_PACKAGE, AGENT_PRESET_ROOT_HINT, AUTHORING_DESCRIPTION_BAR, AUTHORING_SPLIT_LINE_CHARS, BASELINE_ACTION, CITATION_SUPPORT_DIRS, COMBINED_REVIEW_PLAN_PROMPT, COMBINED_REVIEW_PROMPT, COMPLETION_SKILL_REVIEW_PROMPT, CONTENT_SPLIT_HINT, COST_ASCII_TOKEN_HIGH, COST_ASCII_TOKEN_LOW, COST_CJK_TOKEN_HIGH, COST_CJK_TOKEN_LOW, CURATOR_DRY_RUN_BANNER, CURATOR_PROMPT, DEDUP_MAX_PAIR_COMPARISONS, DEDUP_SIMILARITY_THRESHOLD, DEFAULT_ADMISSION, DEFAULT_ARCHIVE_AFTER_DAYS, DEFAULT_ARCHIVE_RETENTION_POLICY, DEFAULT_CITATION_POLICY, DEFAULT_CITATION_REF_BUDGET, DEFAULT_CONSOLIDATION_FAILURES, DEFAULT_CURATOR_BOOT_GRACE_SECONDS, DEFAULT_CURATOR_INTERVAL_HOURS, DEFAULT_CURATOR_MODEL, DEFAULT_CURATOR_REVIEW_MAX_TOKENS, DEFAULT_HEALTH_THRESHOLDS, DEFAULT_MAX_OPS_PER_PLAN, DEFAULT_MEMORY_CHAR_LIMIT, DEFAULT_MEMORY_REVIEW_MODEL, DEFAULT_MIN_IDLE_HOURS, DEFAULT_MUTATION_CAP, DEFAULT_PROBE_TIMEOUT_MS, DEFAULT_REFERENCE_REWRITE_POLICY, DEFAULT_REVIEW_CONTEXT_MESSAGES, DEFAULT_REVIEW_MEMORY_INTERVAL, DEFAULT_REVIEW_MESSAGE_CHARS, DEFAULT_REVIEW_SKILL_INTERVAL, DEFAULT_REVIEW_TIMEOUT_MS, DEFAULT_RUN_RECORDS, DEFAULT_SKILL_CONTENT_CHARS, DEFAULT_SKILL_LIMITS, DEFAULT_SKILL_REVIEW_COMPLETION_MIN_TOOL_CALLS, DEFAULT_SKILL_REVIEW_MODEL, DEFAULT_SKILL_REVIEW_TRIGGER, DEFAULT_SKILL_VERSION_KEEP, DEFAULT_STALE_AFTER_DAYS, DEFAULT_SUBSTANTIVE_MIN_AGENT_CHARS, DEFAULT_SUBSTANTIVE_MIN_TOOL_CALLS, DEFAULT_SUBSTANTIVE_MIN_USER_CHARS, DEFAULT_SUPPORT_FILE_CHAR_POLICY, DEFAULT_SUPPORT_READ_TOOL_NAMES, DEFAULT_THREAT_MAX_SCAN_CHARS, DEFAULT_USER_CHAR_LIMIT, DISPATCH_EVENT_TYPES, DRIFT_MAX_LINE_CHARS, DRIFT_SIGNALS_VERSION, DRIFT_SIGNAL_NOUNS, DSH_AUTHORING_STANDARDS, EMPTY_PATCH_SEED, ENTRY_DELIMITER, EVENT_ARCHIVE_PREFIX, EVENT_LOG_RETAIN_ARCHIVES, EVENT_LOG_ROTATE_AT, EVENT_LOG_VERSION, EVOLUTION_ERRORS, EVOLUTION_WRITE_TOOLS, EvolutionGateSet, FAMILY_SESSION_TOOL_NAMES, FORBIDDEN_CONTROL_KEYS, FUZZY_MAX_PATTERN_CHARS, FUZZY_MAX_WORK, HEALTH_STAMP_RE, HISTORY_DIR, HISTORY_INDEX_VERSION, HOOK_LINE_RE, INSTANCE_KEYS, KEEP_LINE_RE, LONG_RUNS, LOW_QUALITY_THRESHOLD, MAINTAIN_OUTPUT_INSTRUCTION, MAINTAIN_PROMPT, MAX_DESCRIPTION_LENGTH, MAX_REQUEST_BODY_BYTES, MAX_RESTRUCTURE_MOVES, MAX_SKILL_CONTENT_CHARS, MAX_SKILL_FILE_BYTES, MAX_SKILL_NAME_LENGTH, MAX_SUPPORT_READ_PATHS, MAX_TIMER_DELAY_MS, MEMORY_GUIDANCE_SECTION_ORDER, MEMORY_REVIEW_PROMPT, MIN_STAMP_BODY_CHARS, MUTATIONS_FILE_VERSION, MemoryStore, NAMESPACE_MIGRATIONS, NATIVE_CALL_EVENT, NATIVE_RESULT_EVENT, PARAM_ALIASES, PARAM_EXPOSURE, PARAM_GROUP_LABELS, PARAM_NAMESPACES, PATTERN_OVERLAP, PLATFORM_CATALOG_DESCRIPTION_DEFAULT, PLATFORM_ESTIMATE_CHARS_PER_TOKEN, PLATFORM_SERVICE_PROBES, POLICY_STAGE_DEFAULTS, PROMPT_BUNDLE, PROMPT_BUNDLE_ID, PROMPT_BUNDLE_VERSION, PROTECTED_BUILTIN_SKILLS, PTC_DISPATCH_EVENT, PTC_DISPATCH_START_EVENT, QUALITY_WEIGHTS, READ_REQUIRED_ACTIONS, RESTRUCTURE_TARGET_RE, RETENTION_FEEDBACK_KEEP_RATIO, RETENTION_FEEDBACK_MIN_CHARS, RULES, RUNS_SCHEMA_VERSION, SKILLS_GUIDANCE, SKILLS_GUIDANCE_SECTION_ORDER, SKILL_ACTION_REQUIRED_FIELDS, SKILL_NAME_RE, SKILL_REVIEW_PLAN_PROMPT, SKILL_REVIEW_PROMPT, SNAPSHOT_EXTRA_NAME_RE, SUMMARY_DUPLICATE_HINT_THRESHOLD, SUPPORT_DIRS, SUPPORT_SPRAWL_FILES, SUPPRESSED_FILE_VERSION, SkillLibrary, TEXT_DIFF_MAX_CHARS, TEXT_DIFF_MAX_LINES, THREAT_EXEMPTION_HINT, TOKEN_ASCII_WEIGHT, TOKEN_CJK_WEIGHT, ToolDispatchNormalizer, ToolDispatchPayloadError, UNTRUSTED_LINE_BREAK, UNTRUSTED_LINE_MARK, UPSTREAM_LIMIT_CHARS_PER_TOKEN, VERSION_SUMMARY_MAX_CHARS, absentHostServices, admissionVerdict, advanceReview, affinity, ageCopyKey, allowRowCollisions, anchorVerdict, appendEvolutionEvent, applyCuratorLifecycleFields, applyCuratorMetaFields, applyNamespaceMigration, applyReferenceRewrite, asRecord, assertDispatchPayload, assertSkillsRootAliasRetired, assessStructureHealth, atomicWriteFiles, authoringFeedback, basePresetPlugins, blobPath, bodyCost, buildCuratorRunReport, buildHealthBaseline, buildLearnPrompt, bumpPatch, bumpSupportRead, bumpUse, bumpView, callingScope, candidateSkills, canonicalWriteId, claimInstance, clampOnce, clampedNumber, classifyNamespaceMigration, clearReviewChannel, collectReadSkillNames, composePresetComposition, composePresetEntry, composePresetInsert, composePresetRow, compositionRowIds, computeDedupGroups, computeDriftSignals, computeLifecycleTransitions, computePrefixClusters, computeQualityScores, computeScopeView, contentHash, contentRetentionFeedback, countDispatches, countReferenceFiles, createGateSet, daysSinceIso, declaredReportStamp, denseBlockSequence, describeReferenceRewrite, diffFindings, dispatchKeyOf, dispatchStepOf, displayBodyOf, duplicateHeadings, elapsedSince, emptyRecord, entryTarget, errorText, evaluateThreat, eventsFile, evidenceKindIndex, evolutionActivityProjection, evolutionEventPayloadIssue, evolutionEvidenceProjection, evolutionHome, evolutionIoAdapter, evolutionReadsProjection, evolutionRoot, evolutionSignalsProjection, filterUnreadSkillOps, findDriftSignal, foldCuratorFields, foldToolDispatches, foldTurnEvent, foldTurnEvents, foldUntrustedLines, frontmatterBlock, frontmatterCatalogInvalid, fuzzyPatch, getRecord, h2Sections, historyIndexFile, historyRoot, identity, idleDays, initialTurnFold, installEvolutionProjections, instanceClaimKey, instanceClaimedWriteSites, instanceHolder, isAbsent, isBookkeepingFrame, isCommittedWarning, isDeprecatedParamId, isFileShapedPath, isLandmark, isLoopbackRequest, isMissingPath, isPresent, isProcessAlive, isProgramToolName, isRecord, isReviewChannelSession, isSkillReadToolName, isSkillToolName, isUnknown, isUnreadWrite, landmarkVerdict, latestActivityAt, latestVersionAt, lifecycleCandidate, lintBody, lintSkill, listDirectories, listEventArchives, liveBool, liveEnum, liveNumber, liveString, loadMutations, loadRowOverrides, loadSuppressedNames, loadUsage, loadVersionContent, loadVersions, makeSerialQueue, mapProbe, markAgentCreated, markReviewChannel, markerEntryName, memoryRoot, mergePresetRow, missingSupportPointers, mutateUsage, mutationsFile, narrowNameMatches, nearDuplicateSummaries, neutralizePromptVariables, newRunRegistry, newSkillLibrary, nextHistoryIndex, nodeEvolutionIo, normalizeFrontmatter, normalizeUsageRecord, normalizeVersionSummary, observeEvent, orderVersions, overlongLines, paramNamespace, paramRowId, paramSettingsId, parseCuratorNominations, parseEvolutionEvents, parseFrontmatter, parseHistoryIndex, partitionVersions, payloadText, persistedWriteSite, persistedWriteSites, pickWithPolicy, planNamespaceMigration, planReferenceRewrite, planRehoming, policyStageLimits, presetPatchText, presetRowBlock, presetRowBody, presetRowId, probeAbsent, probeDesktopScope, probeList, probeMtime, probePresent, probeReason, probeText, probeUnknown, projectionText, readDispatchSignal, readEvolutionEvents, readEvolutionTimeline, readHistoryIndex, readJsonBody, readJsonObject, readNumberParam, readParam, readingStats, recordMutation, recordVersions, redactSecrets, relatedSkillNames, releaseInstance, removePresetRow, renameWithRetry, renderCuratorReportMarkdown, renderLongRunTable, reportTime, reportsSweepLockTarget, resolveBasePresetPatch, resolveCitations, resolveExecOrigins, resolveOrigins, resolveParamId, resolveRootConfig, resolveSkillsRoot, resolveUnitMembers, resolveUnitMembersFromIndexes, restructureCandidates, retainEventArchives, retirementReport, reviewPrompt, rowBlockSpan, rowSiblingEnd, runsFile, saveSuppressedNames, saveUsage, scanBodyHooks, scanContentThreats, scanMemoryThreats, scanThreats, scopedProbeReport, sessionAudited, sessionEvidenceIndex, sessionLastEventTime, sessionReadNames, sessionReadSkillNames, sessionSeesFamilyTools, sessionTurnSignals, skillReadNameOf, skillRowCells, skillStateKey, skillsRoot, staleRouteStubs, supportFileReadOf, suppressedFile, sweepReports, sweepReviewChannelSessions, textDiffFacts, tokenLineFor, transactIo, transactTaskGuard, trimPatternBoundaries, trimTrailingBlankLines, unhookedSupportPointers, unitMembersAreComplete, unwrapLive, updateSuppressedNames, usageFile, usageObserved, userSetKeys, validateFrontmatter, validateRestructureTarget, valueOr, verifyPromptBundle, versionActionKind, versionRowCells, versionSummaryPrompt, versionTarget, vocabulary, writeDurableTmp, writeJson, writePathSymbols, yamlPlainScalarNeedsQuotes };
@@ -54,6 +54,18 @@ export interface CuratorFailedSkill {
54
54
  reason: string;
55
55
  }
56
56
  export interface CuratorRunReport {
57
+ /** G3b: cross-index orphan landmarks -- report-only, never a verdict. */
58
+ routeStubs?: {
59
+ readonly skills: number;
60
+ readonly stubs: number;
61
+ readonly reason: string;
62
+ readonly detail: readonly {
63
+ readonly skill: string;
64
+ readonly v: number;
65
+ readonly owner: string;
66
+ readonly reason: string;
67
+ }[];
68
+ };
57
69
  /** Report shape version; readers may ignore unknown fields on later versions. */
58
70
  schemaVersion: 1;
59
71
  runId: string;
@@ -25,6 +25,12 @@ export declare function frontmatterBlock(content: string): {
25
25
  */
26
26
  export interface FrontmatterRead {
27
27
  frontmatter: Frontmatter;
28
+ /** P0′: the structured `metadata:` mapping, when the strict parser loaded one.
29
+ * Published as an OBJECT (never stringified): nested mappings used to be dropped
30
+ * because a lossy string could be picked up by a routing field — keeping it
31
+ * structured removes that risk while giving the one consumer the nested
32
+ * `metadata.hermes.related_skills` form the real corpus actually writes. */
33
+ metadata?: Record<string, unknown> | undefined;
28
34
  body: string;
29
35
  /** Raw entries whose UNQUOTED value the strict catalog cannot load as
30
36
  * written. Quotes are included, so a value already normalized by the write
@@ -72,6 +72,7 @@ export * from './evidence.ts';
72
72
  export * from './session-projection.ts';
73
73
  export * from './drift-signals.ts';
74
74
  export * from './skill-history.ts';
75
+ export * from './unit-members.ts';
75
76
  export * from './skill-store.ts';
76
77
  export * from './state-store.ts';
77
78
  export * from './threats.ts';
@@ -0,0 +1,19 @@
1
+ /** 技能名文法(与平台 `isSkillName` 等价:kebab-case,点前缀与下划线一律不是技能名)。 */
2
+ export declare const SKILL_NAME_RE: RegExp;
3
+ /** 名字是否是技能名 —— `._x`、`Bad_Name`、`.archive` 都返回 false。 */
4
+ export declare function isSkillName(name: string): boolean;
5
+ /** 相对技能根的路径是否落在平台会发现的位置(depth<=2 且最后一段是 SKILL.md 或 `<name>.md`)。 */
6
+ export declare function isPotentialSkillPath(relPath: string): boolean;
7
+ export interface DiscoveredSkill {
8
+ name: string;
9
+ path: string;
10
+ }
11
+ /**
12
+ * 扫描一个技能根,返回平台**真正会发现**的技能(非递归、depth<=2、含 `SKILL.md` 的目录、
13
+ * 名字过 kebab-case、跳过点前缀目录)。忽略目录里的边不得计入,也不得算悬空。
14
+ *
15
+ * @param root - 技能根目录。
16
+ * @returns 按名字排序的技能列表(根缺失时返回空数组)。
17
+ */
18
+ export declare function discoverSkillFiles(root: string): DiscoveredSkill[];
19
+ //# sourceMappingURL=skill-discovery.d.ts.map
@@ -34,11 +34,30 @@ export declare const HISTORY_INDEX_VERSION = 1;
34
34
  /** The action label for a predecessor the index had never seen (content found on disk before this
35
35
  * feature existed, or a hand-edited tree): it says "this is what was there", not what wrote it. */
36
36
  export declare const BASELINE_ACTION = "baseline";
37
+ /** G3b: one unit's head. Exactly one of the two forms is present (they never coexist). */
38
+ export interface UnitHead {
39
+ /** Representative shard: the unit's WHOLE manifest of moves (one entry per written path). */
40
+ moves?: readonly Readonly<Record<string, unknown>>[];
41
+ /** Representative shard: the skills this unit wrote, so members resolve without a directory scan. */
42
+ sources?: readonly {
43
+ readonly skill: string;
44
+ readonly dest?: string;
45
+ }[];
46
+ /** Cross-skill landmark: the skill that owns the unit (route-only; never carries `moves`). */
47
+ skill?: string;
48
+ }
37
49
  /** One recorded version of one skill. */
38
50
  export interface SkillVersion {
39
51
  /** Monotonic per skill; the identity, unlike {@link SkillVersion.at}. */
40
52
  v: number;
41
53
  at: string;
54
+ /** G3a: the unit this entry belongs to (one tree change = one unit); absent on entries recorded before it. */
55
+ unitId?: string;
56
+ /** G3b: the unit HEAD. A representative shard carries `{ moves, sources }` (`moves` is the whole
57
+ * manifest of the unit, `sources` names the skills it wrote); a cross-skill landmark carries
58
+ * `{ skill }` only, and the two forms are mutually exclusive. Discriminate on `moves !== undefined`
59
+ * (a landmark also carries `unitId`, so that field can never discriminate). */
60
+ unitHead?: UnitHead;
42
61
  /** The mutation that produced it (`create`/`update`/`patch`/…), or {@link BASELINE_ACTION}. */
43
62
  action: string;
44
63
  /** sha256 of the stored content. */
@@ -75,6 +94,10 @@ export interface VersionRecordInput {
75
94
  summary?: string;
76
95
  /** The support file this write touched, relative to the skill directory; undefined for content. */
77
96
  path?: string;
97
+ /** G3a: the unit this write belongs to — one tree change writes N paths under ONE id. */
98
+ unitId?: string;
99
+ /** G3b: present only on the unit's representative shard (see {@link SkillVersion.unitHead}). */
100
+ unitHead?: UnitHead;
78
101
  }
79
102
  /** The version numbers one write produced; a side with no content contributes nothing. */
80
103
  export interface RecordedVersions {
@@ -570,7 +570,7 @@ export declare class SkillLibrary {
570
570
  restoreFromArchive(rawName: string): Promise<SkillActionResult>;
571
571
  writeSupportFile(rawName: string, filePath: string, content: string, origin?: WriteOrigin, anchor?: WriteAnchor): Promise<SkillActionResult>;
572
572
  private writeSupportFileCore;
573
- removeSupportFile(rawName: string, filePath: string, origin?: WriteOrigin, anchor?: WriteAnchor): Promise<SkillActionResult>;
573
+ removeSupportFile(rawName: string, filePath: string, origin?: WriteOrigin, anchor?: WriteAnchor, unitId?: string): Promise<SkillActionResult>;
574
574
  private removeSupportFileCore;
575
575
  /**
576
576
  * v23 (ML-1): `.archive` retention. Archived skills are recoverable history,
@@ -0,0 +1,73 @@
1
+ /**
2
+ * G3b: resolve a unit's members from the skills named in its head's `sources[]`.
3
+ *
4
+ * Pure core (no io): the caller hands in each candidate skill's index TEXT, so this function can be
5
+ * tested with synthetic indexes. Membership is decided ONLY by the known keys -- never by scanning a
6
+ * directory -- and the result order is the `sources[]` order with the head skill first (deterministic:
7
+ * no Map/Set iteration order leaks into the output).
8
+ */
9
+ import type { SkillVersion, UnitHead } from './skill-history.ts';
10
+ export interface UnitMemberSlice {
11
+ readonly skill: string;
12
+ readonly entries: readonly SkillVersion[];
13
+ }
14
+ export interface UnitMemberIssue {
15
+ readonly skill: string;
16
+ readonly reason: 'index-absent' | 'unit-absent';
17
+ }
18
+ export interface UnitConflict {
19
+ readonly kind: 'missing' | 'foreign' | 'duplicated';
20
+ readonly path: string;
21
+ }
22
+ export interface UnitMembers {
23
+ readonly members: readonly UnitMemberSlice[];
24
+ readonly missing: readonly UnitMemberIssue[];
25
+ readonly conflicts: readonly UnitConflict[];
26
+ }
27
+ export interface UnitHeadInput {
28
+ readonly skill: string;
29
+ readonly unitId: string;
30
+ readonly sources: readonly {
31
+ readonly skill: string;
32
+ }[];
33
+ readonly unitHead?: UnitHead;
34
+ }
35
+ /** Candidate skills: head first, then `sources[]` order, DEDUPLICATED (sources may name the head). */
36
+ export declare function candidateSkills(head: UnitHeadInput): string[];
37
+ export declare function resolveUnitMembersFromIndexes(head: UnitHeadInput, indexes: ReadonlyMap<string, string | null>, readEntries: (text: string) => readonly SkillVersion[]): UnitMembers;
38
+ /** A unit's members are trustworthy only when nothing is missing and nothing conflicts. */
39
+ export declare function unitMembersAreComplete(members: UnitMembers): boolean;
40
+ /** Minimal io view the shell needs (structural, so this module imports no io type). */
41
+ export interface UnitMemberIo {
42
+ readText(path: string): Promise<string | null>;
43
+ }
44
+ /**
45
+ * The io shell (A19.2): open ONLY the known keys -- never enumerate a directory. A skill whose index
46
+ * file is unreadable becomes `null` (index-absent), which the core then reports by name.
47
+ */
48
+ export declare function resolveUnitMembers(root: string, io: UnitMemberIo, head: UnitHeadInput, indexPath: (root: string, name: string) => string, readEntries: (raw: string | null) => readonly SkillVersion[]): Promise<UnitMembers>;
49
+ /** G3b: a LANDMARK carries the owning skill and no manifest (the two forms never coexist). */
50
+ export declare function isLandmark(entry: SkillVersion): boolean;
51
+ /** A landmark is jumpable only when its owner exists AND that index still holds the unit. */
52
+ export type LandmarkVerdict = {
53
+ readonly jumpable: true;
54
+ } | {
55
+ readonly jumpable: false;
56
+ readonly reason: 'not-a-landmark' | 'owner-absent' | 'unit-absent';
57
+ };
58
+ /** @param entry - the entry being judged. @param ownerEntries - the owner skill's index, or null when
59
+ * the owner has no readable index. */
60
+ export declare function landmarkVerdict(entry: SkillVersion, ownerEntries: readonly SkillVersion[] | null): LandmarkVerdict;
61
+ /** One cross-index orphan: a landmark whose owner or unit can no longer be reached. */
62
+ export interface StaleRouteStub {
63
+ readonly skill: string;
64
+ readonly v: number;
65
+ readonly owner: string;
66
+ readonly reason: 'owner-absent' | 'unit-absent';
67
+ }
68
+ /**
69
+ * Scan a whole corpus (skill name -> index entries, or null when unreadable) for stale route stubs.
70
+ * Sorted by (skill, version, owner): a function of the corpus, not of the caller's map order.
71
+ */
72
+ export declare function staleRouteStubs(indexes: ReadonlyMap<string, readonly SkillVersion[] | null>): StaleRouteStub[];
73
+ //# sourceMappingURL=unit-members.d.ts.map
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.25.8",
4
+ "version": "0.25.9",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },