@lmzhen/dsh-evolution-core 0.3.62 → 0.3.63
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 +201 -66
- package/lib/types/skill-store.d.ts +48 -0
- package/lib/types/usage.d.ts +13 -0
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -534,7 +534,9 @@ function normalizeUsageRecord(record) {
|
|
|
534
534
|
pinned: bool(raw.pinned, base.pinned),
|
|
535
535
|
archived_at: nullableTimestamp(raw.archived_at) ? raw.archived_at : base.archived_at,
|
|
536
536
|
quality_score: typeof raw.quality_score === "number" && Number.isFinite(raw.quality_score) ? raw.quality_score : void 0,
|
|
537
|
-
quality_warn: typeof raw.quality_warn === "boolean" ? raw.quality_warn : void 0
|
|
537
|
+
quality_warn: typeof raw.quality_warn === "boolean" ? raw.quality_warn : void 0,
|
|
538
|
+
feedback_score: typeof raw.feedback_score === "number" && Number.isFinite(raw.feedback_score) ? raw.feedback_score : void 0,
|
|
539
|
+
feedback_warn: typeof raw.feedback_warn === "boolean" ? raw.feedback_warn : void 0
|
|
538
540
|
};
|
|
539
541
|
}
|
|
540
542
|
/** Parse a raw usage sidecar; malformed content reads as empty (best-effort telemetry). */
|
|
@@ -560,11 +562,14 @@ async function loadUsage(root, io = nodeEvolutionIo()) {
|
|
|
560
562
|
*/
|
|
561
563
|
async function mutateUsage(root, io, task) {
|
|
562
564
|
await transactIo(io, usageFile(root), async (current) => {
|
|
565
|
+
let shapePreserved = false;
|
|
563
566
|
if (current !== null) try {
|
|
564
|
-
JSON.parse(current);
|
|
567
|
+
const probe = JSON.parse(current);
|
|
568
|
+
if (probe === null || Array.isArray(probe) || typeof probe !== "object") shapePreserved = true;
|
|
565
569
|
} catch {
|
|
566
570
|
return current;
|
|
567
571
|
}
|
|
572
|
+
if (shapePreserved) return current;
|
|
568
573
|
const map = parseUsage(current);
|
|
569
574
|
await task(map);
|
|
570
575
|
return JSON.stringify(Object.fromEntries(map.entries()), null, 2);
|
|
@@ -593,6 +598,9 @@ function applyCuratorLifecycleFields(disk, curated) {
|
|
|
593
598
|
* Copy the recomputed meta pair (quality_score/quality_warn + the
|
|
594
599
|
* marker-mirrored pin flag) — refreshed tree-wide each run by design, so a
|
|
595
600
|
* concurrent curator run's lifecycle changes are never reverted by them.
|
|
601
|
+
* P1-1 (v15): the feedback pair (`feedback_score`/`feedback_warn`) is
|
|
602
|
+
* deliberately NOT copied — it is feedback-owned (see the field-ownership
|
|
603
|
+
* contract on {@link UsageRecord}) and must survive curator runs untouched.
|
|
596
604
|
*/
|
|
597
605
|
function applyCuratorMetaFields(disk, curated) {
|
|
598
606
|
disk.quality_score = curated.quality_score;
|
|
@@ -1018,8 +1026,9 @@ function computeScopeView(usage, config, protectedNames, gates) {
|
|
|
1018
1026
|
if (record.pinned || bundled || suppressed || protectedNames?.has(name) === true || isBuiltin) protectedSet.add(name);
|
|
1019
1027
|
if (lifecycleCandidate(name, record, config, bundled, gateSet, protectedNames)) {
|
|
1020
1028
|
managed.push(name);
|
|
1021
|
-
|
|
1022
|
-
if (record.
|
|
1029
|
+
const warned = record.quality_warn === true || record.feedback_warn === true;
|
|
1030
|
+
if (record.state === "stale" || warned) watched.push(name);
|
|
1031
|
+
if (warned) qualityWarned.push(name);
|
|
1023
1032
|
}
|
|
1024
1033
|
}
|
|
1025
1034
|
return {
|
|
@@ -1046,7 +1055,7 @@ function computeLifecycleTransitions(usage, config, now = /* @__PURE__ */ new Da
|
|
|
1046
1055
|
const age = daysSince(null, record.created_at, now.getTime());
|
|
1047
1056
|
if (record.use_count === 0 && age < config.staleAfterDays) continue;
|
|
1048
1057
|
const idle = daysSince(latestActivityAt(record), record.created_at, now.getTime());
|
|
1049
|
-
const qualityWarn = record.quality_warn === true;
|
|
1058
|
+
const qualityWarn = record.quality_warn === true || record.feedback_warn === true;
|
|
1050
1059
|
const staleAfterDays = qualityWarn && config.qualityWarnStaleAfterDays !== void 0 ? config.qualityWarnStaleAfterDays : config.staleAfterDays;
|
|
1051
1060
|
if (record.state === "active") {
|
|
1052
1061
|
if (idle >= config.archiveAfterDays) {
|
|
@@ -1061,7 +1070,8 @@ function computeLifecycleTransitions(usage, config, now = /* @__PURE__ */ new Da
|
|
|
1061
1070
|
result.archive.push(name);
|
|
1062
1071
|
} else if (idle >= staleAfterDays) {
|
|
1063
1072
|
record.state = "stale";
|
|
1064
|
-
const
|
|
1073
|
+
const warnSource = record.feedback_warn === true ? "feedback-warn stale" : "quality-warn stale";
|
|
1074
|
+
const reason = qualityWarn ? `idle ${Math.round(idle)}d >= ${warnSource} ${staleAfterDays}d` : `idle ${Math.round(idle)}d >= ${staleAfterDays}d`;
|
|
1065
1075
|
result.transitions.push({
|
|
1066
1076
|
name,
|
|
1067
1077
|
from: "active",
|
|
@@ -2092,12 +2102,12 @@ function scanThreats(text, scope = "strict", maxScanChars = 65536, options = NO_
|
|
|
2092
2102
|
if (ZERO_WIDTH_CHARS.test(text) && !excluded.has("unicode_zero_width")) findings.push({
|
|
2093
2103
|
label: "unicode_zero_width",
|
|
2094
2104
|
category: "unicode_obfuscation",
|
|
2095
|
-
scope
|
|
2105
|
+
scope: "all"
|
|
2096
2106
|
});
|
|
2097
2107
|
if (BIDI_CHARS.test(text) && !excluded.has("unicode_bidi_override")) findings.push({
|
|
2098
2108
|
label: "unicode_bidi_override",
|
|
2099
2109
|
category: "unicode_obfuscation",
|
|
2100
|
-
scope
|
|
2110
|
+
scope: "all"
|
|
2101
2111
|
});
|
|
2102
2112
|
const normalized = text.normalize("NFKC");
|
|
2103
2113
|
const windows = [];
|
|
@@ -2397,7 +2407,13 @@ var MemoryStore = class {
|
|
|
2397
2407
|
async addCore(target, facts, raw) {
|
|
2398
2408
|
const content = facts.trim();
|
|
2399
2409
|
if (!content) return {
|
|
2400
|
-
result:
|
|
2410
|
+
result: {
|
|
2411
|
+
ok: false,
|
|
2412
|
+
message: "Content cannot be empty.",
|
|
2413
|
+
entries: [],
|
|
2414
|
+
chars: 0,
|
|
2415
|
+
limit: this.limitFor(target)
|
|
2416
|
+
},
|
|
2401
2417
|
write: null
|
|
2402
2418
|
};
|
|
2403
2419
|
if (this.driftFromRaw(target, raw)) {
|
|
@@ -2737,25 +2753,31 @@ function contentHash(content) {
|
|
|
2737
2753
|
function parseMutationRecords(raw) {
|
|
2738
2754
|
if (raw === null) return [];
|
|
2739
2755
|
try {
|
|
2740
|
-
|
|
2741
|
-
return (Array.isArray(parsed) ? parsed : typeof parsed === "object" && parsed !== null && Array.isArray(parsed.records) ? parsed.records : []).filter((entry) => typeof entry === "object" && entry !== null && typeof entry.skillName === "string" && typeof entry.action === "string" && typeof entry.at === "string");
|
|
2756
|
+
return recordsFromParsed(JSON.parse(raw));
|
|
2742
2757
|
} catch {
|
|
2743
2758
|
return [];
|
|
2744
2759
|
}
|
|
2745
2760
|
}
|
|
2761
|
+
/** Field-level shape guard shared by the parse and the recordMutation write
|
|
2762
|
+
* path (P3/v15: the guard's JSON.parse and this parse used to run twice over
|
|
2763
|
+
* the same bytes inside one transact). */
|
|
2764
|
+
function recordsFromParsed(parsed) {
|
|
2765
|
+
return (Array.isArray(parsed) ? parsed : typeof parsed === "object" && parsed !== null && Array.isArray(parsed.records) ? parsed.records : []).filter((entry) => typeof entry === "object" && entry !== null && typeof entry.skillName === "string" && typeof entry.action === "string" && typeof entry.at === "string");
|
|
2766
|
+
}
|
|
2746
2767
|
async function loadMutations(root, io = nodeEvolutionIo()) {
|
|
2747
2768
|
return parseMutationRecords(await io.readText(mutationsFile(root)));
|
|
2748
2769
|
}
|
|
2749
2770
|
/** Append one record, trim to `cap`, and write atomically (versioned shape). */
|
|
2750
2771
|
async function recordMutation(root, io, record, cap = 500) {
|
|
2751
2772
|
await transactIo(io, mutationsFile(root), (current) => {
|
|
2773
|
+
let parsed = [];
|
|
2752
2774
|
if (current !== null) try {
|
|
2753
|
-
JSON.parse(current);
|
|
2775
|
+
parsed = JSON.parse(current);
|
|
2754
2776
|
} catch {
|
|
2755
2777
|
console.warn(`mutation audit record dropped: ${mutationsFile(root)} is malformed and was not overwritten`);
|
|
2756
2778
|
return current;
|
|
2757
2779
|
}
|
|
2758
|
-
const existing =
|
|
2780
|
+
const existing = recordsFromParsed(parsed);
|
|
2759
2781
|
existing.push(record);
|
|
2760
2782
|
const trimmed = existing.length > cap ? existing.slice(existing.length - cap) : existing;
|
|
2761
2783
|
return JSON.stringify({
|
|
@@ -3693,7 +3715,9 @@ const SUPPORT_FILE_NAME_RE = /^[a-z0-9][a-z0-9._-]*$/;
|
|
|
3693
3715
|
/** C-18: win32 reserves these stems with ANY extension (`nul.md` hits the
|
|
3694
3716
|
* NUL device), and they are fully inside the charset above — so the reserved
|
|
3695
3717
|
* set is checked on the first-dot prefix as well; the charset close alone
|
|
3696
|
-
* cannot refuse them.
|
|
3718
|
+
* cannot refuse them. Exported single source: `badName` (skill directories,
|
|
3719
|
+
* P2-11/v15) and `validateSupportPath` (support-file stems, C-18) both
|
|
3720
|
+
* consume this one set — a third copy would drift. */
|
|
3697
3721
|
const WIN32_RESERVED_DEVICE_NAMES = new Set([
|
|
3698
3722
|
"con",
|
|
3699
3723
|
"prn",
|
|
@@ -3726,6 +3750,7 @@ function validateSupportPath(filePath) {
|
|
|
3726
3750
|
if (parts.length < 2) return "Provide a file name, not just a directory.";
|
|
3727
3751
|
for (const part of parts.slice(1)) {
|
|
3728
3752
|
if (!SUPPORT_FILE_NAME_RE.test(part)) return `Unsupported file name "${part}" — use lowercase letters, digits, dots, hyphens, and underscores (leading letter or digit).`;
|
|
3753
|
+
if (part.toLowerCase().endsWith(".lock")) return `Unsupported file name "${part}" — the .lock suffix is reserved for the writer-lock protocol.`;
|
|
3729
3754
|
const stem = part.split(".")[0]?.toLowerCase() ?? "";
|
|
3730
3755
|
if (WIN32_RESERVED_DEVICE_NAMES.has(stem)) return `Unsupported file name "${part}" — a Windows reserved device name.`;
|
|
3731
3756
|
}
|
|
@@ -4023,9 +4048,10 @@ var SkillLibrary = class {
|
|
|
4023
4048
|
* are the deliberate exceptions — their names come from `listNames()`, i.e.
|
|
4024
4049
|
* from the tree itself, never from caller input.
|
|
4025
4050
|
*/
|
|
4026
|
-
badName(name) {
|
|
4051
|
+
badName(name, opts = {}) {
|
|
4027
4052
|
const normalized = name.trim();
|
|
4028
4053
|
if (!SKILL_NAME_RE.test(normalized) || normalized.length > this.limits.maxNameLength) return `Invalid skill name "${normalized}". Use lowercase letters, digits, and hyphens (<= ${this.limits.maxNameLength}).`;
|
|
4054
|
+
if (!opts.allowReserved && WIN32_RESERVED_DEVICE_NAMES.has(normalized)) return `"${normalized}" is a Windows reserved device name and cannot be used as a skill name.`;
|
|
4029
4055
|
return null;
|
|
4030
4056
|
}
|
|
4031
4057
|
/**
|
|
@@ -4045,7 +4071,7 @@ var SkillLibrary = class {
|
|
|
4045
4071
|
}
|
|
4046
4072
|
async deleteProtection(rawName, options = {}) {
|
|
4047
4073
|
const name = rawName.trim();
|
|
4048
|
-
const badName = this.badName(name);
|
|
4074
|
+
const badName = this.badName(name, { allowReserved: true });
|
|
4049
4075
|
if (badName) return badName;
|
|
4050
4076
|
const dir = this.dirOf(name);
|
|
4051
4077
|
const markers = options.allowBundled ? ["hub-installed", "pinned"] : [
|
|
@@ -4247,7 +4273,21 @@ var SkillLibrary = class {
|
|
|
4247
4273
|
message: `Skill "${normalized}" is protected (${protection}).`
|
|
4248
4274
|
};
|
|
4249
4275
|
const onDisk = finalContent.trimEnd() + "\n";
|
|
4250
|
-
|
|
4276
|
+
const createPath = join(dir, "SKILL.md");
|
|
4277
|
+
let existsAtCommit = false;
|
|
4278
|
+
if (this.transact) await this.transact(this.io, createPath, (current) => {
|
|
4279
|
+
if (current !== null) {
|
|
4280
|
+
existsAtCommit = true;
|
|
4281
|
+
return current;
|
|
4282
|
+
}
|
|
4283
|
+
return onDisk;
|
|
4284
|
+
});
|
|
4285
|
+
else if (await this.io.exists(createPath)) existsAtCommit = true;
|
|
4286
|
+
else await this.io.writeText(createPath, onDisk);
|
|
4287
|
+
if (existsAtCommit) return {
|
|
4288
|
+
ok: false,
|
|
4289
|
+
message: `Skill "${normalized}" already exists.`
|
|
4290
|
+
};
|
|
4251
4291
|
if (origin !== "foreground") await this.io.writeText(markerPath(dir, "hermes-managed"), "");
|
|
4252
4292
|
await this.audit(normalized, "create", null, onDisk, "created");
|
|
4253
4293
|
this.notifyMutation({
|
|
@@ -4267,13 +4307,13 @@ var SkillLibrary = class {
|
|
|
4267
4307
|
return await this.serial(() => this.updateCore(name, content, origin));
|
|
4268
4308
|
}
|
|
4269
4309
|
async updateCore(name, content, origin) {
|
|
4270
|
-
const dir = this.dirOf(name);
|
|
4271
|
-
const path = join(dir, "SKILL.md");
|
|
4272
4310
|
const badName = this.badName(name);
|
|
4273
4311
|
if (badName) return {
|
|
4274
4312
|
ok: false,
|
|
4275
4313
|
message: badName
|
|
4276
4314
|
};
|
|
4315
|
+
const dir = this.dirOf(name);
|
|
4316
|
+
const path = join(dir, "SKILL.md");
|
|
4277
4317
|
const protection = await this.writeProtection(name, origin);
|
|
4278
4318
|
if (protection) return {
|
|
4279
4319
|
ok: false,
|
|
@@ -4493,9 +4533,113 @@ var SkillLibrary = class {
|
|
|
4493
4533
|
};
|
|
4494
4534
|
});
|
|
4495
4535
|
}
|
|
4536
|
+
/**
|
|
4537
|
+
* P2-9 (v15): the destructive directory move shared by archive and
|
|
4538
|
+
* restoreFromArchive — rename first, copy+remove fallback when the backend
|
|
4539
|
+
* cannot rename across media (V5-35), with the E-14 rollback when the
|
|
4540
|
+
* fallback's source removal fails. Returns a failure MESSAGE on a failed
|
|
4541
|
+
* move (caller wraps into a structured result) or undefined on success.
|
|
4542
|
+
*/
|
|
4543
|
+
async moveDir(dir, dest) {
|
|
4544
|
+
try {
|
|
4545
|
+
await this.io.rename(dir, dest);
|
|
4546
|
+
return;
|
|
4547
|
+
} catch {
|
|
4548
|
+
if (await this.io.exists(dest)) return "the destination appeared mid-move (concurrent create or restore); refusing to merge — inspect both trees";
|
|
4549
|
+
try {
|
|
4550
|
+
await this.io.copy(dir, dest);
|
|
4551
|
+
} catch (copyError) {
|
|
4552
|
+
return `the move fell back to copy but failed (${copyError instanceof Error ? copyError.message : String(copyError)}); the tree stays where it is`;
|
|
4553
|
+
}
|
|
4554
|
+
try {
|
|
4555
|
+
await this.io.remove(dir);
|
|
4556
|
+
return;
|
|
4557
|
+
} catch (error) {
|
|
4558
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
4559
|
+
try {
|
|
4560
|
+
await this.io.remove(dest);
|
|
4561
|
+
return `the copy succeeded but the source could not be removed (${reason}); the copied tree was rolled back`;
|
|
4562
|
+
} catch {
|
|
4563
|
+
return `the copy succeeded but the source could not be removed (${reason}) and the copied tree could not be rolled back — the tree now exists in BOTH locations; clean up manually`;
|
|
4564
|
+
}
|
|
4565
|
+
}
|
|
4566
|
+
}
|
|
4567
|
+
}
|
|
4568
|
+
/**
|
|
4569
|
+
* P2 (v16): the write-lock probe for the DESTRUCTIVE MOVERS (archive /
|
|
4570
|
+
* restoreFromArchive). A byte-writer mid-flight is the ghost-generator —
|
|
4571
|
+
* after the move its transact commit re-creates `<dir>/…` (mkdir
|
|
4572
|
+
* recursive) and the tree ends half-archived. The signal is the writer's
|
|
4573
|
+
* own lock file, and its PLACEMENT (inside the moved directory) is why the
|
|
4574
|
+
* mover must PROBE-and-REFUSE instead of acquiring it: an acquired lock
|
|
4575
|
+
* would be renamed into `.archive` with the tree, stranding a phantom live
|
|
4576
|
+
* lock (the v16 audit proved the probe→rename TOCTOU does exactly that,
|
|
4577
|
+
* and restore would later move the residue back into the live root).
|
|
4578
|
+
* Coverage: `SKILL.md.lock` (update/patch of the body) plus one level of
|
|
4579
|
+
* each support dir (write_file's lock sits next to its file). Residual:
|
|
4580
|
+
* NESTED support-subdir locks and the probe→rename TOCTOU itself remain
|
|
4581
|
+
* fail-safe (renameWithRetry rides the write out; the writer's locked
|
|
4582
|
+
* re-read refuses on the moved-away file), and a residue `.lock` from a
|
|
4583
|
+
* CRASHED writer also refuses — correct: inspect, don't archive.
|
|
4584
|
+
*/
|
|
4585
|
+
async hasWriteLock(dir) {
|
|
4586
|
+
const markerLocks = [
|
|
4587
|
+
join(dir, "SKILL.md.lock"),
|
|
4588
|
+
join(dir, ".pinned.lock"),
|
|
4589
|
+
join(dir, ".hermes-managed.lock")
|
|
4590
|
+
];
|
|
4591
|
+
for (const lock of markerLocks) if (await this.isWriterLock(lock)) return true;
|
|
4592
|
+
for (const supportDir of SUPPORT_DIRS) {
|
|
4593
|
+
let entries;
|
|
4594
|
+
try {
|
|
4595
|
+
entries = await this.io.list(join(dir, supportDir));
|
|
4596
|
+
} catch {
|
|
4597
|
+
return true;
|
|
4598
|
+
}
|
|
4599
|
+
for (const entry of entries) {
|
|
4600
|
+
if (!entry.endsWith(".lock")) continue;
|
|
4601
|
+
if (await this.isWriterLock(join(dir, supportDir, entry))) return true;
|
|
4602
|
+
}
|
|
4603
|
+
}
|
|
4604
|
+
return false;
|
|
4605
|
+
}
|
|
4606
|
+
/** P2 (v17): a file only counts as a writer lock when its body has the
|
|
4607
|
+
* `pid:token` shape the io layer writes. User support files legitimately
|
|
4608
|
+
* named `*.lock` (allowed by SUPPORT_FILE_NAME_RE) must not trip the probe
|
|
4609
|
+
* or be swept as residue — the v16 first cut matched on suffix alone,
|
|
4610
|
+
* which permanently refused archiving and deleted user content on restore. */
|
|
4611
|
+
async isWriterLock(lockPath) {
|
|
4612
|
+
const body = await this.io.readText(lockPath).catch(() => null);
|
|
4613
|
+
if (body === null) return false;
|
|
4614
|
+
return /^\d+:[0-9a-f]*$/.test(body.trim());
|
|
4615
|
+
}
|
|
4616
|
+
/** P2 (v16): best-effort removal of lock residue inside a RESTORED tree —
|
|
4617
|
+
* a `.lock` that a pre-restore crash or TOCTOU stranded in `.archive`
|
|
4618
|
+
* cannot have a live writer (restore refuses when the live root is
|
|
4619
|
+
* locked), and if left in place its body (a live pid on a single-host
|
|
4620
|
+
* deployment) structurally closes the writer's self-heal path. */
|
|
4621
|
+
async deleteStrandedLocks(dir) {
|
|
4622
|
+
await this.sweepLockIfStranded(join(dir, "SKILL.md.lock"));
|
|
4623
|
+
for (const supportDir of SUPPORT_DIRS) {
|
|
4624
|
+
let entries = [];
|
|
4625
|
+
try {
|
|
4626
|
+
entries = await this.io.list(join(dir, supportDir));
|
|
4627
|
+
} catch {
|
|
4628
|
+
continue;
|
|
4629
|
+
}
|
|
4630
|
+
for (const entry of entries) if (entry.endsWith(".lock")) await this.sweepLockIfStranded(join(dir, supportDir, entry));
|
|
4631
|
+
}
|
|
4632
|
+
}
|
|
4633
|
+
/** Remove `lockPath` only when its body has the writer-lock `pid:token`
|
|
4634
|
+
* shape; anything else (a user support file) is left untouched. */
|
|
4635
|
+
async sweepLockIfStranded(lockPath) {
|
|
4636
|
+
const body = await this.io.readText(lockPath).catch(() => null);
|
|
4637
|
+
if (body === null || !/^\d+:[0-9a-f]*$/.test(body.trim())) return;
|
|
4638
|
+
await this.io.remove(lockPath).catch(() => {});
|
|
4639
|
+
}
|
|
4496
4640
|
async archive(rawName, options = {}) {
|
|
4497
4641
|
const name = rawName.trim();
|
|
4498
|
-
const badName = this.badName(name);
|
|
4642
|
+
const badName = this.badName(name, { allowReserved: true });
|
|
4499
4643
|
if (badName) return {
|
|
4500
4644
|
ok: false,
|
|
4501
4645
|
message: badName
|
|
@@ -4516,6 +4660,11 @@ var SkillLibrary = class {
|
|
|
4516
4660
|
ok: false,
|
|
4517
4661
|
message: "absorbed_into cannot be the skill being archived (cannot absorb into itself)."
|
|
4518
4662
|
};
|
|
4663
|
+
const intoBad = this.badName(options.absorbedInto.trim());
|
|
4664
|
+
if (intoBad) return {
|
|
4665
|
+
ok: false,
|
|
4666
|
+
message: `absorbed_into: ${intoBad}`
|
|
4667
|
+
};
|
|
4519
4668
|
if (!await this.io.readText(join(this.dirOf(options.absorbedInto), "SKILL.md"))) return {
|
|
4520
4669
|
ok: false,
|
|
4521
4670
|
message: `absorbed_into="${options.absorbedInto}" does not exist.`
|
|
@@ -4534,35 +4683,15 @@ var SkillLibrary = class {
|
|
|
4534
4683
|
message: `Skill "${name}" is a symlink; refusing to archive it.`
|
|
4535
4684
|
};
|
|
4536
4685
|
}
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
};
|
|
4547
|
-
}
|
|
4548
|
-
try {
|
|
4549
|
-
await this.io.remove(dir);
|
|
4550
|
-
} catch (error) {
|
|
4551
|
-
const reason = error instanceof Error ? error.message : String(error);
|
|
4552
|
-
try {
|
|
4553
|
-
await this.io.remove(dest);
|
|
4554
|
-
return {
|
|
4555
|
-
ok: false,
|
|
4556
|
-
message: `Archive copy succeeded but the source could not be removed (${reason}); the copied archive was rolled back.`
|
|
4557
|
-
};
|
|
4558
|
-
} catch {
|
|
4559
|
-
return {
|
|
4560
|
-
ok: false,
|
|
4561
|
-
message: `Archive copy succeeded but the source could not be removed (${reason}) and the archive copy could not be rolled back — the skill now exists in BOTH the active root and .archive; clean up manually.`
|
|
4562
|
-
};
|
|
4563
|
-
}
|
|
4564
|
-
}
|
|
4565
|
-
}
|
|
4686
|
+
if (await this.hasWriteLock(dir)) return {
|
|
4687
|
+
ok: false,
|
|
4688
|
+
message: `Skill "${name}" is being written (write lock present); retry archiving once the write completes.`
|
|
4689
|
+
};
|
|
4690
|
+
const moveFailure = await this.moveDir(dir, dest);
|
|
4691
|
+
if (moveFailure !== void 0) return {
|
|
4692
|
+
ok: false,
|
|
4693
|
+
message: `Skill "${name}" archive failed: ${moveFailure}.`
|
|
4694
|
+
};
|
|
4566
4695
|
const reason = options.reason ?? (options.absorbedInto ? `Consolidated into ${options.absorbedInto}` : "Archived by self-evolution curator");
|
|
4567
4696
|
try {
|
|
4568
4697
|
await this.io.writeText(join(dest, ".archive-reason"), `${(/* @__PURE__ */ new Date()).toISOString()}: ${reason}\n`);
|
|
@@ -4875,7 +5004,7 @@ var SkillLibrary = class {
|
|
|
4875
5004
|
*/
|
|
4876
5005
|
async applyTreeChange(plan) {
|
|
4877
5006
|
const name = plan.name.trim();
|
|
4878
|
-
const badName = this.badName(name);
|
|
5007
|
+
const badName = this.badName(name, { allowReserved: true });
|
|
4879
5008
|
if (badName) return {
|
|
4880
5009
|
ok: false,
|
|
4881
5010
|
message: badName
|
|
@@ -4959,7 +5088,7 @@ var SkillLibrary = class {
|
|
|
4959
5088
|
*/
|
|
4960
5089
|
async restoreFromArchive(rawName) {
|
|
4961
5090
|
const name = rawName.trim();
|
|
4962
|
-
const bad = this.badName(name);
|
|
5091
|
+
const bad = this.badName(name, { allowReserved: true });
|
|
4963
5092
|
if (bad) return {
|
|
4964
5093
|
ok: false,
|
|
4965
5094
|
message: bad
|
|
@@ -4996,20 +5125,18 @@ var SkillLibrary = class {
|
|
|
4996
5125
|
message: `Archived entry "${chosen}" is a symlink; refusing to restore it.`
|
|
4997
5126
|
};
|
|
4998
5127
|
}
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
|
|
5008
|
-
|
|
5009
|
-
};
|
|
5010
|
-
}
|
|
5011
|
-
}
|
|
5128
|
+
if (await this.hasWriteLock(dest)) return {
|
|
5129
|
+
ok: false,
|
|
5130
|
+
message: `Skill "${name}" is being written (write lock present); retry restoring once the write completes.`
|
|
5131
|
+
};
|
|
5132
|
+
const moveFailure = await this.moveDir(source, dest);
|
|
5133
|
+
if (moveFailure !== void 0) return {
|
|
5134
|
+
ok: false,
|
|
5135
|
+
message: `Restore of "${name}" from .archive failed: ${moveFailure}`
|
|
5136
|
+
};
|
|
5137
|
+
await this.deleteStrandedLocks(dest);
|
|
5012
5138
|
if (await this.io.exists(join(dest, ".archive-reason"))) await this.io.remove(join(dest, ".archive-reason"));
|
|
5139
|
+
await this.audit(name, "restore", null, await this.io.readText(join(dest, "SKILL.md")).catch(() => null), `restored from ${source}`);
|
|
5013
5140
|
this.notifyMutation({
|
|
5014
5141
|
action: "restore",
|
|
5015
5142
|
name,
|
|
@@ -5026,12 +5153,12 @@ var SkillLibrary = class {
|
|
|
5026
5153
|
return await this.serial(() => this.writeSupportFileCore(name, filePath, content, origin));
|
|
5027
5154
|
}
|
|
5028
5155
|
async writeSupportFileCore(name, filePath, content, origin) {
|
|
5029
|
-
const dir = this.dirOf(name);
|
|
5030
5156
|
const badName = this.badName(name);
|
|
5031
5157
|
if (badName) return {
|
|
5032
5158
|
ok: false,
|
|
5033
5159
|
message: badName
|
|
5034
5160
|
};
|
|
5161
|
+
const dir = this.dirOf(name);
|
|
5035
5162
|
if (!await this.io.exists(join(dir, "SKILL.md"))) return {
|
|
5036
5163
|
ok: false,
|
|
5037
5164
|
message: `Skill "${name}" not found.`
|
|
@@ -5120,6 +5247,10 @@ var SkillLibrary = class {
|
|
|
5120
5247
|
message: `File "${filePath}" not found in skill "${name}".`
|
|
5121
5248
|
};
|
|
5122
5249
|
const before = await this.io.readText(target).catch(() => null);
|
|
5250
|
+
if (before === null) return {
|
|
5251
|
+
ok: false,
|
|
5252
|
+
message: `"${filePath}" is not a readable regular file — remove the files inside it one by one.`
|
|
5253
|
+
};
|
|
5123
5254
|
await this.io.remove(target);
|
|
5124
5255
|
await this.audit(name, "remove_file", before, null, `removed ${filePath}`);
|
|
5125
5256
|
this.notifyMutation({
|
|
@@ -5320,7 +5451,11 @@ var SkillLibrary = class {
|
|
|
5320
5451
|
await this.io.copy(join(snapshotPath, ".archive"), archiveRoot);
|
|
5321
5452
|
} else if (manifest.hasArchive === false) await this.io.remove(archiveRoot);
|
|
5322
5453
|
}
|
|
5454
|
+
for (const entry of await this.io.list(this.root)) {
|
|
5455
|
+
if (entry.startsWith(".")) continue;
|
|
5456
|
+
await this.deleteStrandedLocks(join(this.root, entry));
|
|
5457
|
+
}
|
|
5323
5458
|
}
|
|
5324
5459
|
};
|
|
5325
5460
|
//#endregion
|
|
5326
|
-
export { AUTHORING_DESCRIPTION_BAR, COMBINED_REVIEW_PLAN_PROMPT, COMBINED_REVIEW_PROMPT, COMPLETION_SKILL_REVIEW_PROMPT, CURATOR_DRY_RUN_BANNER, CURATOR_PROMPT, DEFAULT_ARCHIVE_AFTER_DAYS, DEFAULT_CONSOLIDATION_FAILURES, DEFAULT_CURATOR_BOOT_GRACE_SECONDS, DEFAULT_CURATOR_INTERVAL_HOURS, DEFAULT_CURATOR_REVIEW_MAX_TOKENS, DEFAULT_HEALTH_THRESHOLDS, DEFAULT_MAX_OPS_PER_PLAN, DEFAULT_MEMORY_CHAR_LIMIT, DEFAULT_MIN_IDLE_HOURS, DEFAULT_MUTATION_CAP, DEFAULT_REVIEW_CONTEXT_MESSAGES, DEFAULT_REVIEW_MEMORY_INTERVAL, DEFAULT_REVIEW_MESSAGE_CHARS, DEFAULT_REVIEW_SKILL_INTERVAL, DEFAULT_REVIEW_TIMEOUT_MS, DEFAULT_SKILL_CONTENT_CHARS, DEFAULT_SKILL_LIMITS, DEFAULT_SKILL_REVIEW_COMPLETION_MIN_TOOL_CALLS, DEFAULT_SKILL_REVIEW_TRIGGER, DEFAULT_STALE_AFTER_DAYS, DEFAULT_SUBSTANTIVE_MIN_AGENT_CHARS, DEFAULT_SUBSTANTIVE_MIN_TOOL_CALLS, DEFAULT_SUBSTANTIVE_MIN_USER_CHARS, DEFAULT_USER_CHAR_LIMIT, DRIFT_MAX_LINE_CHARS, DRIFT_SIGNALS_VERSION, DRIFT_SIGNAL_NOUNS, DSH_AUTHORING_STANDARDS, ENTRY_DELIMITER, EVENT_ARCHIVE_PREFIX, EVENT_LOG_RETAIN_ARCHIVES, EVENT_LOG_ROTATE_AT, EVENT_LOG_VERSION, EVOLUTION_WRITE_TOOLS, EvolutionGateSet, FORBIDDEN_CONTROL_KEYS, HEALTH_STAMP_RE, LOW_QUALITY_THRESHOLD, MAINTAIN_OUTPUT_INSTRUCTION, MAINTAIN_PROMPT, MAX_DESCRIPTION_LENGTH, MAX_RESTRUCTURE_MOVES, MAX_SKILL_CONTENT_CHARS, MAX_SKILL_FILE_BYTES, MAX_SKILL_NAME_LENGTH, MEMORY_REVIEW_PROMPT, MIN_STAMP_BODY_CHARS, MUTATIONS_FILE_VERSION, MemoryStore, PATTERN_OVERLAP, PROMPT_BUNDLE, PROMPT_BUNDLE_ID, PROMPT_BUNDLE_VERSION, PROTECTED_BUILTIN_SKILLS, QUALITY_WEIGHTS, RESTRUCTURE_TARGET_RE, SKILLS_GUIDANCE, SKILL_NAME_RE, SKILL_REVIEW_PLAN_PROMPT, SKILL_REVIEW_PROMPT, SNAPSHOT_EXTRA_NAME_RE, SUPPORT_DIRS, SUPPRESSED_FILE_VERSION, SkillLibrary, THREAT_EXEMPT_HINT, advanceReview, allowRowCollisions, appendEvolutionEvent, applyCuratorFields, applyCuratorLifecycleFields, applyCuratorMetaFields, assessStructureHealth, authoringFeedback, buildCuratorRunReport, buildLearnPrompt, bumpPatch, bumpUse, bumpView, clampedNumber, composePresetComposition, computeDedupGroups, computeDriftSignals, computeLifecycleTransitions, computePrefixClusters, computeQualityScores, computeScopeView, contentHash, createGateSet, duplicateHeadings, emptyRecord, evaluateThreat, eventsFile, evolutionHome, evolutionIoAdapter, evolutionRoot, findDriftSignal, foldCuratorFields, foldTurn, frontmatterBlock, frontmatterYamlUnsafeValues, getRecord, latestActivityAt, lifecycleCandidate, listEventArchives, loadMutations, loadSuppressedNames, loadUsage, makeSerialQueue, markAgentCreated, markerEntryName, memoryRoot, missingSupportPointers, mutateUsage, mutationsFile, narrowNameMatches, nodeEvolutionIo, normalizeFrontmatter, normalizeUsageRecord, observeEvent, overlongLines, parseCuratorNominations, parseEvolutionEvents, parseFrontmatter, pendingSelfCleanup, readEvolutionEvents, readEvolutionTimeline, recordMutation, redactSecrets, relatedSkillNames, renameWithRetry, renderCuratorReportMarkdown, resolveOrigins, resolveSkillsRoot, retainEventArchives, reviewPrompt, saveSuppressedNames, saveUsage, scanContentThreats, scanMemoryThreats, scanThreats, skillsRoot, suppressedFile, transactIo, updateSuppressedNames, usageFile, usageObserved, validateFrontmatter, verifyPromptBundle, writeDurableTmp, yamlPlainScalarNeedsQuotes };
|
|
5461
|
+
export { AUTHORING_DESCRIPTION_BAR, COMBINED_REVIEW_PLAN_PROMPT, COMBINED_REVIEW_PROMPT, COMPLETION_SKILL_REVIEW_PROMPT, CURATOR_DRY_RUN_BANNER, CURATOR_PROMPT, DEFAULT_ARCHIVE_AFTER_DAYS, DEFAULT_CONSOLIDATION_FAILURES, DEFAULT_CURATOR_BOOT_GRACE_SECONDS, DEFAULT_CURATOR_INTERVAL_HOURS, DEFAULT_CURATOR_REVIEW_MAX_TOKENS, DEFAULT_HEALTH_THRESHOLDS, DEFAULT_MAX_OPS_PER_PLAN, DEFAULT_MEMORY_CHAR_LIMIT, DEFAULT_MIN_IDLE_HOURS, DEFAULT_MUTATION_CAP, DEFAULT_REVIEW_CONTEXT_MESSAGES, DEFAULT_REVIEW_MEMORY_INTERVAL, DEFAULT_REVIEW_MESSAGE_CHARS, DEFAULT_REVIEW_SKILL_INTERVAL, DEFAULT_REVIEW_TIMEOUT_MS, DEFAULT_SKILL_CONTENT_CHARS, DEFAULT_SKILL_LIMITS, DEFAULT_SKILL_REVIEW_COMPLETION_MIN_TOOL_CALLS, DEFAULT_SKILL_REVIEW_TRIGGER, DEFAULT_STALE_AFTER_DAYS, DEFAULT_SUBSTANTIVE_MIN_AGENT_CHARS, DEFAULT_SUBSTANTIVE_MIN_TOOL_CALLS, DEFAULT_SUBSTANTIVE_MIN_USER_CHARS, DEFAULT_USER_CHAR_LIMIT, DRIFT_MAX_LINE_CHARS, DRIFT_SIGNALS_VERSION, DRIFT_SIGNAL_NOUNS, DSH_AUTHORING_STANDARDS, ENTRY_DELIMITER, EVENT_ARCHIVE_PREFIX, EVENT_LOG_RETAIN_ARCHIVES, EVENT_LOG_ROTATE_AT, EVENT_LOG_VERSION, EVOLUTION_WRITE_TOOLS, EvolutionGateSet, FORBIDDEN_CONTROL_KEYS, HEALTH_STAMP_RE, LOW_QUALITY_THRESHOLD, MAINTAIN_OUTPUT_INSTRUCTION, MAINTAIN_PROMPT, MAX_DESCRIPTION_LENGTH, MAX_RESTRUCTURE_MOVES, MAX_SKILL_CONTENT_CHARS, MAX_SKILL_FILE_BYTES, MAX_SKILL_NAME_LENGTH, MEMORY_REVIEW_PROMPT, MIN_STAMP_BODY_CHARS, MUTATIONS_FILE_VERSION, MemoryStore, PATTERN_OVERLAP, PROMPT_BUNDLE, PROMPT_BUNDLE_ID, PROMPT_BUNDLE_VERSION, PROTECTED_BUILTIN_SKILLS, QUALITY_WEIGHTS, RESTRUCTURE_TARGET_RE, SKILLS_GUIDANCE, SKILL_NAME_RE, SKILL_REVIEW_PLAN_PROMPT, SKILL_REVIEW_PROMPT, SNAPSHOT_EXTRA_NAME_RE, SUPPORT_DIRS, SUPPRESSED_FILE_VERSION, SkillLibrary, THREAT_EXEMPT_HINT, WIN32_RESERVED_DEVICE_NAMES, advanceReview, allowRowCollisions, appendEvolutionEvent, applyCuratorFields, applyCuratorLifecycleFields, applyCuratorMetaFields, assessStructureHealth, authoringFeedback, buildCuratorRunReport, buildLearnPrompt, bumpPatch, bumpUse, bumpView, clampedNumber, composePresetComposition, computeDedupGroups, computeDriftSignals, computeLifecycleTransitions, computePrefixClusters, computeQualityScores, computeScopeView, contentHash, createGateSet, duplicateHeadings, emptyRecord, evaluateThreat, eventsFile, evolutionHome, evolutionIoAdapter, evolutionRoot, findDriftSignal, foldCuratorFields, foldTurn, frontmatterBlock, frontmatterYamlUnsafeValues, getRecord, latestActivityAt, lifecycleCandidate, listEventArchives, loadMutations, loadSuppressedNames, loadUsage, makeSerialQueue, markAgentCreated, markerEntryName, memoryRoot, missingSupportPointers, mutateUsage, mutationsFile, narrowNameMatches, nodeEvolutionIo, normalizeFrontmatter, normalizeUsageRecord, observeEvent, overlongLines, parseCuratorNominations, parseEvolutionEvents, parseFrontmatter, pendingSelfCleanup, readEvolutionEvents, readEvolutionTimeline, recordMutation, redactSecrets, relatedSkillNames, renameWithRetry, renderCuratorReportMarkdown, resolveOrigins, resolveSkillsRoot, retainEventArchives, reviewPrompt, saveSuppressedNames, saveUsage, scanContentThreats, scanMemoryThreats, scanThreats, skillsRoot, suppressedFile, transactIo, updateSuppressedNames, usageFile, usageObserved, validateFrontmatter, verifyPromptBundle, writeDurableTmp, yamlPlainScalarNeedsQuotes };
|
|
@@ -228,6 +228,13 @@ export interface AuthoringFeedback {
|
|
|
228
228
|
* truncated or route-poor instead of silently shipping it.
|
|
229
229
|
*/
|
|
230
230
|
export declare function authoringFeedback(frontmatter: Frontmatter): AuthoringFeedback;
|
|
231
|
+
/** C-18: win32 reserves these stems with ANY extension (`nul.md` hits the
|
|
232
|
+
* NUL device), and they are fully inside the charset above — so the reserved
|
|
233
|
+
* set is checked on the first-dot prefix as well; the charset close alone
|
|
234
|
+
* cannot refuse them. Exported single source: `badName` (skill directories,
|
|
235
|
+
* P2-11/v15) and `validateSupportPath` (support-file stems, C-18) both
|
|
236
|
+
* consume this one set — a third copy would drift. */
|
|
237
|
+
export declare const WIN32_RESERVED_DEVICE_NAMES: ReadonlySet<string>;
|
|
231
238
|
export declare class SkillLibrary {
|
|
232
239
|
readonly root: string;
|
|
233
240
|
readonly limits: SkillLimits;
|
|
@@ -339,6 +346,47 @@ export declare class SkillLibrary {
|
|
|
339
346
|
private updateCore;
|
|
340
347
|
patch(rawName: string, oldString: string, newString: string, filePath?: string, replaceAll?: boolean, origin?: WriteOrigin): Promise<SkillActionResult>;
|
|
341
348
|
private patchCore;
|
|
349
|
+
/**
|
|
350
|
+
* P2-9 (v15): the destructive directory move shared by archive and
|
|
351
|
+
* restoreFromArchive — rename first, copy+remove fallback when the backend
|
|
352
|
+
* cannot rename across media (V5-35), with the E-14 rollback when the
|
|
353
|
+
* fallback's source removal fails. Returns a failure MESSAGE on a failed
|
|
354
|
+
* move (caller wraps into a structured result) or undefined on success.
|
|
355
|
+
*/
|
|
356
|
+
private moveDir;
|
|
357
|
+
/**
|
|
358
|
+
* P2 (v16): the write-lock probe for the DESTRUCTIVE MOVERS (archive /
|
|
359
|
+
* restoreFromArchive). A byte-writer mid-flight is the ghost-generator —
|
|
360
|
+
* after the move its transact commit re-creates `<dir>/…` (mkdir
|
|
361
|
+
* recursive) and the tree ends half-archived. The signal is the writer's
|
|
362
|
+
* own lock file, and its PLACEMENT (inside the moved directory) is why the
|
|
363
|
+
* mover must PROBE-and-REFUSE instead of acquiring it: an acquired lock
|
|
364
|
+
* would be renamed into `.archive` with the tree, stranding a phantom live
|
|
365
|
+
* lock (the v16 audit proved the probe→rename TOCTOU does exactly that,
|
|
366
|
+
* and restore would later move the residue back into the live root).
|
|
367
|
+
* Coverage: `SKILL.md.lock` (update/patch of the body) plus one level of
|
|
368
|
+
* each support dir (write_file's lock sits next to its file). Residual:
|
|
369
|
+
* NESTED support-subdir locks and the probe→rename TOCTOU itself remain
|
|
370
|
+
* fail-safe (renameWithRetry rides the write out; the writer's locked
|
|
371
|
+
* re-read refuses on the moved-away file), and a residue `.lock` from a
|
|
372
|
+
* CRASHED writer also refuses — correct: inspect, don't archive.
|
|
373
|
+
*/
|
|
374
|
+
private hasWriteLock;
|
|
375
|
+
/** P2 (v17): a file only counts as a writer lock when its body has the
|
|
376
|
+
* `pid:token` shape the io layer writes. User support files legitimately
|
|
377
|
+
* named `*.lock` (allowed by SUPPORT_FILE_NAME_RE) must not trip the probe
|
|
378
|
+
* or be swept as residue — the v16 first cut matched on suffix alone,
|
|
379
|
+
* which permanently refused archiving and deleted user content on restore. */
|
|
380
|
+
private isWriterLock;
|
|
381
|
+
/** P2 (v16): best-effort removal of lock residue inside a RESTORED tree —
|
|
382
|
+
* a `.lock` that a pre-restore crash or TOCTOU stranded in `.archive`
|
|
383
|
+
* cannot have a live writer (restore refuses when the live root is
|
|
384
|
+
* locked), and if left in place its body (a live pid on a single-host
|
|
385
|
+
* deployment) structurally closes the writer's self-heal path. */
|
|
386
|
+
private deleteStrandedLocks;
|
|
387
|
+
/** Remove `lockPath` only when its body has the writer-lock `pid:token`
|
|
388
|
+
* shape; anything else (a user support file) is left untouched. */
|
|
389
|
+
private sweepLockIfStranded;
|
|
342
390
|
archive(rawName: string, options?: ArchiveOptions): Promise<SkillActionResult>;
|
|
343
391
|
/**
|
|
344
392
|
* Merge the bodies of `sources` into `target` and archive the sources with
|
package/lib/types/usage.d.ts
CHANGED
|
@@ -18,6 +18,16 @@ export interface UsageRecord {
|
|
|
18
18
|
archived_at: string | null;
|
|
19
19
|
quality_score?: number | undefined;
|
|
20
20
|
quality_warn?: boolean | undefined;
|
|
21
|
+
/** P1-1 (v15): feedback-owned quality signal. Field ownership contract —
|
|
22
|
+
* `quality_score`/`quality_warn` are written ONLY by the curator's
|
|
23
|
+
* six-factor `scoreTree`; `feedback_score`/`feedback_warn` are written ONLY
|
|
24
|
+
* by the feedback channel (`SkillUsageRegistry.setFeedbackQuality`);
|
|
25
|
+
* `foldCuratorFields` refreshes the quality_* pair tree-wide and must never
|
|
26
|
+
* touch feedback_*. The lifecycle engine and the scope view read the UNION
|
|
27
|
+
* of both warn flags, which is what makes negative feedback decision-
|
|
28
|
+
* relevant again. */
|
|
29
|
+
feedback_score?: number | undefined;
|
|
30
|
+
feedback_warn?: boolean | undefined;
|
|
21
31
|
}
|
|
22
32
|
export type UsageMap = Map<string, UsageRecord>;
|
|
23
33
|
export declare function usageFile(root: string): string;
|
|
@@ -60,6 +70,9 @@ export declare function applyCuratorLifecycleFields(disk: UsageRecord, curated:
|
|
|
60
70
|
* Copy the recomputed meta pair (quality_score/quality_warn + the
|
|
61
71
|
* marker-mirrored pin flag) — refreshed tree-wide each run by design, so a
|
|
62
72
|
* concurrent curator run's lifecycle changes are never reverted by them.
|
|
73
|
+
* P1-1 (v15): the feedback pair (`feedback_score`/`feedback_warn`) is
|
|
74
|
+
* deliberately NOT copied — it is feedback-owned (see the field-ownership
|
|
75
|
+
* contract on {@link UsageRecord}) and must survive curator runs untouched.
|
|
63
76
|
*/
|
|
64
77
|
export declare function applyCuratorMetaFields(disk: UsageRecord, curated: UsageRecord): void;
|
|
65
78
|
/**
|
package/package.json
CHANGED