@lmzhen/dsh-evolution-core 0.3.35 → 0.3.37
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 +89 -22
- package/lib/types/curator.d.ts +8 -0
- package/lib/types/memory-store.d.ts +8 -0
- package/lib/types/skill-health.d.ts +4 -2
- package/lib/types/usage.d.ts +4 -0
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -423,6 +423,7 @@ function parseUsage(raw) {
|
|
|
423
423
|
if (raw === null) return map;
|
|
424
424
|
try {
|
|
425
425
|
const parsed = JSON.parse(raw);
|
|
426
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return map;
|
|
426
427
|
for (const [name, record] of Object.entries(parsed)) map.set(name, normalizeUsageRecord(record));
|
|
427
428
|
} catch {}
|
|
428
429
|
return map;
|
|
@@ -499,6 +500,10 @@ function foldCuratorFields(disk, curated, stateOwned) {
|
|
|
499
500
|
if (stateOwned === void 0 || stateOwned.has(name)) applyCuratorLifecycleFields(diskRecord, record);
|
|
500
501
|
}
|
|
501
502
|
}
|
|
503
|
+
/** Whole-file usage write (V6-37, 0.3.37): this is the ONE path that bypasses
|
|
504
|
+
* the malformed-defense and the transact lock — prefer `mutateUsage` for any
|
|
505
|
+
* read-modify-write so a concurrent writer cannot lose its update and a
|
|
506
|
+
* malformed sidecar stays recoverable. Kept for fixture/test seeding. */
|
|
502
507
|
async function saveUsage(root, map, io = nodeEvolutionIo()) {
|
|
503
508
|
const obj = Object.fromEntries(map.entries());
|
|
504
509
|
await io.writeText(usageFile(root), JSON.stringify(obj, null, 2));
|
|
@@ -746,7 +751,8 @@ function buildCuratorRunReport(input) {
|
|
|
746
751
|
failed: [...input.failed],
|
|
747
752
|
...input.consolidated === void 0 ? {} : { consolidated: [...input.consolidated] },
|
|
748
753
|
...input.snapshotPath === void 0 ? {} : { snapshotPath: input.snapshotPath },
|
|
749
|
-
...input.llmReviewEnabled === void 0 ? {} : { llmReviewEnabled: input.llmReviewEnabled }
|
|
754
|
+
...input.llmReviewEnabled === void 0 ? {} : { llmReviewEnabled: input.llmReviewEnabled },
|
|
755
|
+
...input.nominationsWarnings === void 0 ? {} : { nominationsWarnings: [...input.nominationsWarnings] }
|
|
750
756
|
};
|
|
751
757
|
}
|
|
752
758
|
/**
|
|
@@ -765,7 +771,8 @@ function renderCuratorReportMarkdown(report) {
|
|
|
765
771
|
`- **Archived**: ${report.archived.length}`,
|
|
766
772
|
`- **Failed**: ${report.failed.length}`,
|
|
767
773
|
...report.snapshotPath === void 0 ? [] : [`- **Snapshot**: ${report.snapshotPath}`],
|
|
768
|
-
...report.llmReviewEnabled === void 0 ? [] : [`- **llmReview**: ${report.llmReviewEnabled}`]
|
|
774
|
+
...report.llmReviewEnabled === void 0 ? [] : [`- **llmReview**: ${report.llmReviewEnabled}`],
|
|
775
|
+
...report.nominationsWarnings === void 0 || report.nominationsWarnings.length === 0 ? [] : [`- **Nomination warnings**: ${report.nominationsWarnings.join("; ")}`]
|
|
769
776
|
];
|
|
770
777
|
const section = (title, items) => items.length === 0 ? [] : [
|
|
771
778
|
"",
|
|
@@ -791,10 +798,16 @@ const NOMINATION_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
|
791
798
|
function parseCuratorNominations(text) {
|
|
792
799
|
const prunings = [];
|
|
793
800
|
const consolidations = [];
|
|
801
|
+
const warnings = [];
|
|
794
802
|
let section = null;
|
|
795
803
|
let currentFrom = "";
|
|
796
804
|
let currentMode;
|
|
797
805
|
for (const line of text.split("\n")) {
|
|
806
|
+
const header = /^\s*(consolidations|prunings)\s*:\s*$/.exec(line);
|
|
807
|
+
if (header) {
|
|
808
|
+
section = header[1] === "consolidations" ? "consolidations" : "prunings";
|
|
809
|
+
continue;
|
|
810
|
+
}
|
|
798
811
|
const consolidated = /^\s*-\s*from:\s*([a-z0-9][a-z0-9-]*)\s*$/.exec(line);
|
|
799
812
|
if (consolidated) {
|
|
800
813
|
section = "consolidations";
|
|
@@ -805,6 +818,7 @@ function parseCuratorNominations(text) {
|
|
|
805
818
|
const mode = /^\s*mode:\s*(append|reference)\s*$/.exec(line);
|
|
806
819
|
if (mode) {
|
|
807
820
|
if (currentFrom !== "") currentMode = mode[1] === "reference" ? "reference" : "append";
|
|
821
|
+
else warnings.push(`mode: ${mode[1]} ignored — no preceding "- from:" entry (the consolidation falls back to append)`);
|
|
808
822
|
continue;
|
|
809
823
|
}
|
|
810
824
|
const into = /^\s*into:\s*([a-z0-9][a-z0-9-]*)\s*$/.exec(line);
|
|
@@ -821,6 +835,7 @@ function parseCuratorNominations(text) {
|
|
|
821
835
|
}
|
|
822
836
|
const pruned = /^\s*-\s*name:\s*([a-z0-9][a-z0-9-]*)\s*$/.exec(line);
|
|
823
837
|
if (pruned) {
|
|
838
|
+
if (section === "consolidations") warnings.push("\"- name:\" inside the consolidations section flips the parse to prunings");
|
|
824
839
|
section = "prunings";
|
|
825
840
|
const name = pruned[1];
|
|
826
841
|
if (name) prunings.push(name);
|
|
@@ -829,7 +844,8 @@ function parseCuratorNominations(text) {
|
|
|
829
844
|
const valid = (name) => NOMINATION_NAME_RE.test(name);
|
|
830
845
|
return {
|
|
831
846
|
prunings: prunings.filter(valid),
|
|
832
|
-
consolidations: consolidations.filter((item) => valid(item.from) && valid(item.into))
|
|
847
|
+
consolidations: consolidations.filter((item) => valid(item.from) && valid(item.into)),
|
|
848
|
+
warnings
|
|
833
849
|
};
|
|
834
850
|
}
|
|
835
851
|
/**
|
|
@@ -864,7 +880,8 @@ function computeScopeView(usage, config, protectedNames, gates) {
|
|
|
864
880
|
}
|
|
865
881
|
const bundled = config.bundledNames?.has(name) === true;
|
|
866
882
|
const suppressed = gateSet.suppressed.has(name);
|
|
867
|
-
|
|
883
|
+
const isBuiltin = PROTECTED_BUILTIN_SKILLS.has(name);
|
|
884
|
+
if (record.pinned || bundled || suppressed || protectedNames?.has(name) === true || isBuiltin) protectedSet.add(name);
|
|
868
885
|
if (lifecycleCandidate(name, record, config, bundled, gateSet)) {
|
|
869
886
|
managed.push(name);
|
|
870
887
|
if (record.state === "stale" || record.quality_warn === true) watched.push(name);
|
|
@@ -1589,6 +1606,24 @@ function buildLearnPrompt(userRequest) {
|
|
|
1589
1606
|
].join("\n");
|
|
1590
1607
|
}
|
|
1591
1608
|
//#endregion
|
|
1609
|
+
//#region lib/types/serial.js
|
|
1610
|
+
/**
|
|
1611
|
+
* A process-local serial task queue: each task starts only after the previous
|
|
1612
|
+
* one settles (success or failure), so read-modify-write sequences that share
|
|
1613
|
+
* one file never interleave inside this process. The durable cross-process
|
|
1614
|
+
* serialization layer is the IO backend's transact lock; this chain is the
|
|
1615
|
+
* second layer (0.3.17 S2.8, T-1: the shape was duplicated in state-json and
|
|
1616
|
+
* memory-files — one factory now).
|
|
1617
|
+
*/
|
|
1618
|
+
function makeSerialQueue() {
|
|
1619
|
+
let chain = Promise.resolve();
|
|
1620
|
+
return (task) => {
|
|
1621
|
+
const run = chain.then(task, task);
|
|
1622
|
+
chain = run.then(() => void 0, () => void 0);
|
|
1623
|
+
return run;
|
|
1624
|
+
};
|
|
1625
|
+
}
|
|
1626
|
+
//#endregion
|
|
1592
1627
|
//#region lib/types/numeric.js
|
|
1593
1628
|
/**
|
|
1594
1629
|
* Numeric config clamping for the dsh-evolution plugin family.
|
|
@@ -1950,6 +1985,12 @@ var MemoryStore = class {
|
|
|
1950
1985
|
root;
|
|
1951
1986
|
maxFailures;
|
|
1952
1987
|
io;
|
|
1988
|
+
/** V6-16 (0.3.37): same-process RMW serialization (the SkillLibrary queue) —
|
|
1989
|
+
* on a backend WITHOUT a transact lock two concurrent callers compute on the
|
|
1990
|
+
* same old content and the last rename wins, silently dropping one op's
|
|
1991
|
+
* update. The node backend's cross-process lock already serializes; this
|
|
1992
|
+
* chain covers the no-transact custom backends. */
|
|
1993
|
+
serial = makeSerialQueue();
|
|
1953
1994
|
failureCount = 0;
|
|
1954
1995
|
lastFailureAt = 0;
|
|
1955
1996
|
constructor(options = {}) {
|
|
@@ -2056,6 +2097,9 @@ var MemoryStore = class {
|
|
|
2056
2097
|
};
|
|
2057
2098
|
}
|
|
2058
2099
|
async add(target, facts) {
|
|
2100
|
+
return await this.serial(() => this.addChained(target, facts));
|
|
2101
|
+
}
|
|
2102
|
+
async addChained(target, facts) {
|
|
2059
2103
|
if (!facts.trim()) return {
|
|
2060
2104
|
ok: false,
|
|
2061
2105
|
message: "Content cannot be empty.",
|
|
@@ -2161,6 +2205,9 @@ var MemoryStore = class {
|
|
|
2161
2205
|
return render(entries) !== raw;
|
|
2162
2206
|
}
|
|
2163
2207
|
async applyBatch(target, operations) {
|
|
2208
|
+
return await this.serial(() => this.applyBatchChained(target, operations));
|
|
2209
|
+
}
|
|
2210
|
+
async applyBatchChained(target, operations) {
|
|
2164
2211
|
if (operations.length === 0) return {
|
|
2165
2212
|
ok: false,
|
|
2166
2213
|
message: "operations list is empty.",
|
|
@@ -2234,6 +2281,17 @@ var MemoryStore = class {
|
|
|
2234
2281
|
if (!working.some((entry) => stripDatePrefix(entry) === body)) working.push(this.addDatePrefix ? `## ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}\n${body}` : body);
|
|
2235
2282
|
continue;
|
|
2236
2283
|
}
|
|
2284
|
+
const rawAction = op.action;
|
|
2285
|
+
if (rawAction !== "remove" && rawAction !== "replace") return {
|
|
2286
|
+
result: {
|
|
2287
|
+
ok: false,
|
|
2288
|
+
message: `Operation ${position}: unknown action "${String(rawAction)}" (expected add/remove/replace). No operations were applied.${previewEntries(entries)}`,
|
|
2289
|
+
entries,
|
|
2290
|
+
chars: entries.join(ENTRY_DELIMITER).length,
|
|
2291
|
+
limit: this.limitFor(target)
|
|
2292
|
+
},
|
|
2293
|
+
write: null
|
|
2294
|
+
};
|
|
2237
2295
|
const needle = (op.old_text ?? "").trim();
|
|
2238
2296
|
if (!needle) return {
|
|
2239
2297
|
result: {
|
|
@@ -2653,24 +2711,6 @@ function redactSecrets(text) {
|
|
|
2653
2711
|
return out;
|
|
2654
2712
|
}
|
|
2655
2713
|
//#endregion
|
|
2656
|
-
//#region lib/types/serial.js
|
|
2657
|
-
/**
|
|
2658
|
-
* A process-local serial task queue: each task starts only after the previous
|
|
2659
|
-
* one settles (success or failure), so read-modify-write sequences that share
|
|
2660
|
-
* one file never interleave inside this process. The durable cross-process
|
|
2661
|
-
* serialization layer is the IO backend's transact lock; this chain is the
|
|
2662
|
-
* second layer (0.3.17 S2.8, T-1: the shape was duplicated in state-json and
|
|
2663
|
-
* memory-files — one factory now).
|
|
2664
|
-
*/
|
|
2665
|
-
function makeSerialQueue() {
|
|
2666
|
-
let chain = Promise.resolve();
|
|
2667
|
-
return (task) => {
|
|
2668
|
-
const run = chain.then(task, task);
|
|
2669
|
-
chain = run.then(() => void 0, () => void 0);
|
|
2670
|
-
return run;
|
|
2671
|
-
};
|
|
2672
|
-
}
|
|
2673
|
-
//#endregion
|
|
2674
2714
|
//#region lib/types/skill-health.js
|
|
2675
2715
|
/**
|
|
2676
2716
|
* Skill structure-health domain (rc.73 A1, 008 design): a SECOND assessment
|
|
@@ -2760,6 +2800,7 @@ function observeEvent(signal, event) {
|
|
|
2760
2800
|
return;
|
|
2761
2801
|
}
|
|
2762
2802
|
if (event.type === "assistant/message") {
|
|
2803
|
+
if (!Array.isArray(event.data.message.content)) return;
|
|
2763
2804
|
const text = event.data.message.content.map((block) => block.type === "text" ? block.text : "").join(" ");
|
|
2764
2805
|
signal.assistantChars += text.length;
|
|
2765
2806
|
return;
|
|
@@ -3327,6 +3368,12 @@ function fuzzyIndexOf(content, pattern, from = 0) {
|
|
|
3327
3368
|
}
|
|
3328
3369
|
return null;
|
|
3329
3370
|
}
|
|
3371
|
+
/** V6-17 (0.3.37): the fuzzy-patch scan is O(n·m) with no input bound; a
|
|
3372
|
+
* non-exact anchor past these budgets would block the event loop (measured
|
|
3373
|
+
* ~6s at 20k×20k). Exact matches go through the fast `includes` path and stay
|
|
3374
|
+
* allowed regardless of size. */
|
|
3375
|
+
const FUZZY_MAX_PATTERN_CHARS = 4096;
|
|
3376
|
+
const FUZZY_MAX_WORK = 8e6;
|
|
3330
3377
|
/** Trim leading whitespace of the first line and trailing whitespace of the last line. */
|
|
3331
3378
|
function trimPatternBoundaries(pattern) {
|
|
3332
3379
|
const from = pattern.search(/\S/);
|
|
@@ -3471,6 +3518,10 @@ var SkillLibrary = class {
|
|
|
3471
3518
|
if (next !== null && next !== current) await this.io.writeText(path, next);
|
|
3472
3519
|
}
|
|
3473
3520
|
const o = outcome;
|
|
3521
|
+
if (o === void 0 || typeof o !== "object" || !Object.prototype.hasOwnProperty.call(o, "write")) return {
|
|
3522
|
+
ok: false,
|
|
3523
|
+
message: "internal error: the write transaction did not invoke the task; no write was performed"
|
|
3524
|
+
};
|
|
3474
3525
|
if (o.write !== null && o.audit) await this.audit(o.audit.skillName, o.audit.action, o.audit.before, o.audit.after, o.audit.summary);
|
|
3475
3526
|
if (o.write !== null && o.event) this.notifyMutation(o.event);
|
|
3476
3527
|
return o.result;
|
|
@@ -3863,6 +3914,13 @@ var SkillLibrary = class {
|
|
|
3863
3914
|
},
|
|
3864
3915
|
write: null
|
|
3865
3916
|
};
|
|
3917
|
+
if (!md.includes(oldString) && (oldString.length > FUZZY_MAX_PATTERN_CHARS || md.length * oldString.length > FUZZY_MAX_WORK)) return {
|
|
3918
|
+
result: {
|
|
3919
|
+
ok: false,
|
|
3920
|
+
message: `old_string too large for fuzzy match (${oldString.length} chars in ${patchLabel}); use update for a full rewrite or a narrower anchor.`
|
|
3921
|
+
},
|
|
3922
|
+
write: null
|
|
3923
|
+
};
|
|
3866
3924
|
const patched = fuzzyPatch(md, oldString, newString, replaceAll);
|
|
3867
3925
|
if (patched === null) return {
|
|
3868
3926
|
result: {
|
|
@@ -4502,6 +4560,15 @@ var SkillLibrary = class {
|
|
|
4502
4560
|
};
|
|
4503
4561
|
const target = join(dir, ...filePath.replace(/\\/g, "/").split("/").filter(Boolean));
|
|
4504
4562
|
return await this.runSingleWrite(target, (current) => {
|
|
4563
|
+
if (current !== null && content.trimEnd() === current.trimEnd()) return {
|
|
4564
|
+
result: {
|
|
4565
|
+
ok: true,
|
|
4566
|
+
message: `Support file "${filePath}" unchanged: the supplied content already matches the current file; nothing written.`,
|
|
4567
|
+
noop: true,
|
|
4568
|
+
path: target
|
|
4569
|
+
},
|
|
4570
|
+
write: null
|
|
4571
|
+
};
|
|
4505
4572
|
return {
|
|
4506
4573
|
result: {
|
|
4507
4574
|
ok: true,
|
package/lib/types/curator.d.ts
CHANGED
|
@@ -63,6 +63,8 @@ export interface CuratorRunReport {
|
|
|
63
63
|
snapshotPath?: string;
|
|
64
64
|
/** Whether the LLM nomination pass was enabled for this run (decision visibility). */
|
|
65
65
|
llmReviewEnabled?: boolean;
|
|
66
|
+
/** V6-35 (0.3.36): lenient-parse shape notes from the LLM nomination block. */
|
|
67
|
+
nominationsWarnings?: string[];
|
|
66
68
|
}
|
|
67
69
|
export interface CuratorReportInput {
|
|
68
70
|
runId: string;
|
|
@@ -76,6 +78,7 @@ export interface CuratorReportInput {
|
|
|
76
78
|
consolidated?: readonly CuratorConsolidation[];
|
|
77
79
|
snapshotPath?: string;
|
|
78
80
|
llmReviewEnabled?: boolean;
|
|
81
|
+
nominationsWarnings?: readonly string[];
|
|
79
82
|
}
|
|
80
83
|
export declare function buildCuratorRunReport(input: CuratorReportInput): CuratorRunReport;
|
|
81
84
|
/**
|
|
@@ -97,6 +100,11 @@ export interface CuratorConsolidation {
|
|
|
97
100
|
export interface CuratorNominations {
|
|
98
101
|
prunings: string[];
|
|
99
102
|
consolidations: CuratorConsolidation[];
|
|
103
|
+
/** V6-35 (0.3.36): lenient-parse shape notes (an entry the lenient logic
|
|
104
|
+
* silently dropped or re-routed). Parsing stays lenient — these are advisory
|
|
105
|
+
* and flow into the run report so the operator sees why a mode or a pruning
|
|
106
|
+
* went somewhere unexpected. */
|
|
107
|
+
warnings: string[];
|
|
100
108
|
}
|
|
101
109
|
/**
|
|
102
110
|
* Parse the curator LLM's YAML nomination block (consolidations + prunings).
|
|
@@ -34,6 +34,12 @@ export declare class MemoryStore {
|
|
|
34
34
|
readonly root: string;
|
|
35
35
|
private readonly maxFailures;
|
|
36
36
|
private readonly io;
|
|
37
|
+
/** V6-16 (0.3.37): same-process RMW serialization (the SkillLibrary queue) —
|
|
38
|
+
* on a backend WITHOUT a transact lock two concurrent callers compute on the
|
|
39
|
+
* same old content and the last rename wins, silently dropping one op's
|
|
40
|
+
* update. The node backend's cross-process lock already serializes; this
|
|
41
|
+
* chain covers the no-transact custom backends. */
|
|
42
|
+
private readonly serial;
|
|
37
43
|
private failureCount;
|
|
38
44
|
private lastFailureAt;
|
|
39
45
|
constructor(options?: MemoryStoreOptions);
|
|
@@ -71,6 +77,7 @@ export declare class MemoryStore {
|
|
|
71
77
|
*/
|
|
72
78
|
private oversizedRefusal;
|
|
73
79
|
add(target: MemoryTarget, facts: string): Promise<MemoryApplyResult>;
|
|
80
|
+
private addChained;
|
|
74
81
|
/**
|
|
75
82
|
* Single-entry add inside the transaction: shared checks (oversized,
|
|
76
83
|
* drift, threat) and the content computation. `raw` is the locked view
|
|
@@ -80,6 +87,7 @@ export declare class MemoryStore {
|
|
|
80
87
|
/** Canonical-form drift check derived from the locked view (same formula as `detectDrift`, no second read). */
|
|
81
88
|
private driftFromRaw;
|
|
82
89
|
applyBatch(target: MemoryTarget, operations: MemoryOperation[]): Promise<MemoryApplyResult>;
|
|
90
|
+
private applyBatchChained;
|
|
83
91
|
/** Batch RMW inside the transaction. `write: null` = failure/no-op, disk untouched. */
|
|
84
92
|
private applyBatchCore;
|
|
85
93
|
renderContext(): Promise<string>;
|
|
@@ -10,8 +10,10 @@
|
|
|
10
10
|
* `computeQualityScores` (different dimension, different consumers).
|
|
11
11
|
*/
|
|
12
12
|
export interface SkillHealthThresholds {
|
|
13
|
-
/** Soft body limit: body
|
|
14
|
-
* '
|
|
13
|
+
/** Soft body limit: a body of `softBodyChars` or MORE -> 'warn'; >= 2x ->
|
|
14
|
+
* 'needs-restructure'. V6-34 (0.3.37): the doc comment used to claim
|
|
15
|
+
* "at/below stay healthy" while the engine warns at `>=` — fixed to the
|
|
16
|
+
* implementation edge (the reason copy said "above"). */
|
|
15
17
|
softBodyChars: number;
|
|
16
18
|
/** Stamp-density ceiling per KB of body text: rc.NN / commit shas / ISO
|
|
17
19
|
* dates per KB at/above this -> 'warn' (log-like content living in the
|
package/lib/types/usage.d.ts
CHANGED
|
@@ -73,6 +73,10 @@ export declare function applyCuratorMetaFields(disk: UsageRecord, curated: Usage
|
|
|
73
73
|
* by a stale snapshot; without it both pairs apply everywhere.
|
|
74
74
|
*/
|
|
75
75
|
export declare function foldCuratorFields(disk: UsageMap, curated: UsageMap, stateOwned?: ReadonlySet<string>): void;
|
|
76
|
+
/** Whole-file usage write (V6-37, 0.3.37): this is the ONE path that bypasses
|
|
77
|
+
* the malformed-defense and the transact lock — prefer `mutateUsage` for any
|
|
78
|
+
* read-modify-write so a concurrent writer cannot lose its update and a
|
|
79
|
+
* malformed sidecar stays recoverable. Kept for fixture/test seeding. */
|
|
76
80
|
export declare function saveUsage(root: string, map: UsageMap, io?: EvolutionIoLike): Promise<void>;
|
|
77
81
|
export declare function getRecord(map: UsageMap, name: string): UsageRecord;
|
|
78
82
|
export declare function bumpView(map: UsageMap, name: string, when?: Date): void;
|
package/package.json
CHANGED