@lmzhen/dsh-evolution-learning-graph 0.3.16 → 0.3.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -4
- package/lib/index.js +105 -19
- package/lib/types/index.d.ts +38 -6
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -2,12 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
Learning graph over skills and memory
|
|
4
4
|
|
|
5
|
+
## Graph surface
|
|
6
|
+
|
|
7
|
+
`/graph` renders the learning graph. A skill node is its name; a memory node is `memory:<source>:<index>`, where `<source>` is `memory`|`user` and `<index>` is the position in that file's entries.
|
|
8
|
+
|
|
9
|
+
Memory node ids carry a trailing snapshot token (`memory:<source>:<index>:<snapshot>` — an 8-hex digest of the node label's first line). It exists so `graph edit`/`graph delete` can detect index drift: any memory write between the last render and the command shifts indices, and a stale id is rejected (requiring a re-run of `/graph`) instead of mutating a different entry.
|
|
10
|
+
|
|
11
|
+
Memory→skill edges are word-level, not substring: the entry is tokenized on non-letter/digit/hyphen runs and a skill name links only when it is a whole token. This prevents a skill named `run` from linking the words `running`/`grunt`.
|
|
12
|
+
|
|
13
|
+
`graph edit`/`graph delete` of a skill node route through the evolution approval seam when it is mounted (soft-probed; the write executes directly when it is absent). Each approved/executed edit bumps the skill's patch counter and a delete archives it, matching `skill_manage`.
|
|
14
|
+
|
|
5
15
|
## Model Experience
|
|
6
16
|
|
|
7
17
|
### Indirect model surface
|
|
8
18
|
|
|
9
|
-
#### What the model sees
|
|
10
|
-
|
|
11
19
|
`@deepseek-ai/dsh-evolution-learning-graph` registers no direct prompt or tool schema itself. Model-visible effects are owned by the packages that consume this service.
|
|
12
20
|
|
|
13
21
|
#### Token effect
|
|
@@ -20,5 +28,5 @@ Independent of request-prefix construction. This package does not alter the asse
|
|
|
20
28
|
|
|
21
29
|
## Known Limitations and Deferred Work
|
|
22
30
|
|
|
23
|
-
|
|
24
|
-
-
|
|
31
|
+
- The memory drift snapshot covers only the node label (first line, first 80 chars): a change confined to a later line of the same entry is not detected, because the rendered node label is the comparison anchor.
|
|
32
|
+
- A hand-typed bare `memory:<source>:<index>` id has no snapshot and skips the drift check (legacy path; the rendered ids always carry the snapshot).
|
package/lib/index.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { SKILL_NAME_RE, SkillLibrary, evolutionIoAdapter, relatedSkillNames } from "@lmzhen/dsh-evolution-core";
|
|
1
|
+
import { SKILL_NAME_RE, SkillLibrary, evolutionIoAdapter, relatedSkillNames, resolveOrigins, resolveSkillsRoot } from "@lmzhen/dsh-evolution-core";
|
|
2
2
|
//#region lib/types/index.js
|
|
3
3
|
/**
|
|
4
4
|
* Learning graph over skills and memory, plus node-level commands
|
|
5
5
|
* (`/evolution graph [detail|edit|delete] <nodeId>`) aligned with the Hermes
|
|
6
6
|
* journey surface: a skill node is its name, a memory node is
|
|
7
7
|
* `memory:<source>:<index>` (source = memory|user, index = position in that
|
|
8
|
-
* file's entries).
|
|
8
|
+
* file's entries). Memory node ids carry a trailing snapshot token so
|
|
9
|
+
* edit/delete can reject a stale index (E-21).
|
|
9
10
|
* @module @lmzhen/dsh-evolution-learning-graph
|
|
10
11
|
*/
|
|
11
12
|
function graphDensity(graph) {
|
|
@@ -64,14 +65,15 @@ function buildLearningGraph(usage, memoryEntries, userEntries = [], related) {
|
|
|
64
65
|
}
|
|
65
66
|
const appendMemory = (source, entries) => {
|
|
66
67
|
entries.forEach((entry, index) => {
|
|
67
|
-
const
|
|
68
|
+
const label = (entry.split("\n")[0] ?? "").slice(0, 80);
|
|
69
|
+
const id = `memory:${source}:${index}:${memorySnapshotOf(label)}`;
|
|
68
70
|
nodes.push({
|
|
69
71
|
id,
|
|
70
72
|
kind: "memory",
|
|
71
|
-
label
|
|
73
|
+
label
|
|
72
74
|
});
|
|
73
|
-
const
|
|
74
|
-
for (const name of sorted) if (name &&
|
|
75
|
+
const words = new Set(entry.toLowerCase().split(/[^a-z0-9-]+/).filter(Boolean));
|
|
76
|
+
for (const name of sorted) if (name && words.has(name.toLowerCase())) edges.push({
|
|
75
77
|
from: id,
|
|
76
78
|
to: name,
|
|
77
79
|
type: "memory_skill"
|
|
@@ -85,13 +87,61 @@ function buildLearningGraph(usage, memoryEntries, userEntries = [], related) {
|
|
|
85
87
|
edges
|
|
86
88
|
};
|
|
87
89
|
}
|
|
88
|
-
/**
|
|
90
|
+
/**
|
|
91
|
+
* E-21 snapshot token: a deterministic 8-hex-char digest of a memory entry's
|
|
92
|
+
* rendered label (first line, first 80 chars). The graph builder embeds it in
|
|
93
|
+
* the memory node id so a later edit/delete can detect index drift — any
|
|
94
|
+
* memory write between the last render and the command shifts `memory:<source>:<index>`
|
|
95
|
+
* positions, and the token lets the command reject a stale index instead of
|
|
96
|
+
* mutating the wrong entry. FNV-1a (no imports, cross-platform).
|
|
97
|
+
*/
|
|
98
|
+
function memorySnapshotOf(entry) {
|
|
99
|
+
const label = (entry.split("\n")[0] ?? "").slice(0, 80);
|
|
100
|
+
let hash = 2166136261;
|
|
101
|
+
for (let i = 0; i < label.length; i += 1) {
|
|
102
|
+
hash ^= label.charCodeAt(i);
|
|
103
|
+
hash = Math.imul(hash, 16777619);
|
|
104
|
+
}
|
|
105
|
+
return (hash >>> 0).toString(16).padStart(8, "0").slice(-8);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* E-21 TOCTOU guard for a memory node operation. Re-reads the entry at
|
|
109
|
+
* `parsed.index` and, when the id carries a snapshot token, verifies the
|
|
110
|
+
* current entry's label still matches it. A mismatch means a memory write
|
|
111
|
+
* shifted the index between the last `/graph` render and this command, so the
|
|
112
|
+
* operation is refused (the caller surfaces "re-run /graph"). A missing
|
|
113
|
+
* snapshot (a hand-typed bare `memory:<source>:<index>` id) skips the check
|
|
114
|
+
* and falls back to operating on whatever currently sits at the index.
|
|
115
|
+
*/
|
|
116
|
+
function readMemoryIndex(parsed, entries) {
|
|
117
|
+
const entry = entries[parsed.index];
|
|
118
|
+
if (entry === void 0) return {
|
|
119
|
+
ok: false,
|
|
120
|
+
message: `Memory ${parsed.source}[${parsed.index}] does not exist (${entries.length} entries).`
|
|
121
|
+
};
|
|
122
|
+
if (parsed.snapshot !== void 0 && memorySnapshotOf(entry) !== parsed.snapshot) return {
|
|
123
|
+
ok: false,
|
|
124
|
+
message: `Memory ${parsed.source}[${parsed.index}] changed since the graph was rendered; run /graph again and retry.`
|
|
125
|
+
};
|
|
126
|
+
return {
|
|
127
|
+
ok: true,
|
|
128
|
+
entry
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Parse a graph node id: skill names pass through; `memory:<source>:<index>`
|
|
133
|
+
* becomes a memory node. An optional trailing `:<snapshot>` (the E-21
|
|
134
|
+
* `memorySnapshotOf` token the builder embeds) is carried so edit/delete can
|
|
135
|
+
* detect index drift; a bare index id (hand-typed) still parses with no
|
|
136
|
+
* snapshot and skips the drift check.
|
|
137
|
+
*/
|
|
89
138
|
function parseGraphNodeId(id) {
|
|
90
|
-
const memory = /^memory:(memory|user):(\d+)
|
|
139
|
+
const memory = /^memory:(memory|user):(\d+)(?::([0-9a-f]{8}))?$/.exec(id);
|
|
91
140
|
if (memory) return {
|
|
92
141
|
kind: "memory",
|
|
93
142
|
source: memory[1],
|
|
94
|
-
index: Number(memory[2])
|
|
143
|
+
index: Number(memory[2]),
|
|
144
|
+
...memory[3] !== void 0 ? { snapshot: memory[3] } : {}
|
|
95
145
|
};
|
|
96
146
|
if (SKILL_NAME_RE.test(id)) return {
|
|
97
147
|
kind: "skill",
|
|
@@ -175,7 +225,7 @@ function apply(ctx) {
|
|
|
175
225
|
return lines.join("\n") + "\n\n" + edges.join("\n") + densityLine;
|
|
176
226
|
}
|
|
177
227
|
function withSkills() {
|
|
178
|
-
return new SkillLibrary(
|
|
228
|
+
return new SkillLibrary(resolveSkillsRoot(), evolutionIoAdapter(() => io.provider()), void 0, (event) => {
|
|
179
229
|
ctx.emit("evolution/skill-mutated", event);
|
|
180
230
|
});
|
|
181
231
|
}
|
|
@@ -192,15 +242,34 @@ function apply(ctx) {
|
|
|
192
242
|
const parsed = parseGraphNodeId(id);
|
|
193
243
|
if (parsed === null) return err(`Invalid node id "${id}". Skill names or memory:<source>:<index> expected.`);
|
|
194
244
|
if (parsed.kind === "skill") {
|
|
245
|
+
const approval = ctx.get("evolutionApproval");
|
|
246
|
+
if (approval) {
|
|
247
|
+
const origins = resolveOrigins(void 0);
|
|
248
|
+
const decision = await approval.request({
|
|
249
|
+
kind: "skill",
|
|
250
|
+
summary: `graph edit ${parsed.name}`,
|
|
251
|
+
args: {
|
|
252
|
+
operation: {
|
|
253
|
+
action: "update",
|
|
254
|
+
name: parsed.name,
|
|
255
|
+
content
|
|
256
|
+
},
|
|
257
|
+
origin: origins.approval,
|
|
258
|
+
libraryOrigin: origins.library
|
|
259
|
+
},
|
|
260
|
+
origin: origins.approval
|
|
261
|
+
});
|
|
262
|
+
if (decision.action === "staged") return ok(decision.message);
|
|
263
|
+
}
|
|
195
264
|
const result = await withSkills().update(parsed.name, content, "foreground");
|
|
265
|
+
if (result.ok) await usageService.record?.(parsed.name, "patch");
|
|
196
266
|
return result.ok ? ok(result.message) : err(result.message);
|
|
197
267
|
}
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
if (entry === void 0) return err(`Memory ${parsed.source}[${parsed.index}] does not exist (${entries.length} entries).`);
|
|
268
|
+
const check = readMemoryIndex(parsed, await memory.read(parsed.source));
|
|
269
|
+
if (!check.ok) return err(check.message ?? "Memory index check failed.");
|
|
201
270
|
const result = await memory.applyBatch(parsed.source, [{
|
|
202
271
|
action: "replace",
|
|
203
|
-
old_text: entry,
|
|
272
|
+
old_text: check.entry ?? "",
|
|
204
273
|
facts: content
|
|
205
274
|
}]);
|
|
206
275
|
return result.ok ? ok(result.message) : err(result.message);
|
|
@@ -209,16 +278,33 @@ function apply(ctx) {
|
|
|
209
278
|
const parsed = parseGraphNodeId(id);
|
|
210
279
|
if (parsed === null) return err(`Invalid node id "${id}". Skill names or memory:<source>:<index> expected.`);
|
|
211
280
|
if (parsed.kind === "skill") {
|
|
281
|
+
const approval = ctx.get("evolutionApproval");
|
|
282
|
+
if (approval) {
|
|
283
|
+
const origins = resolveOrigins(void 0);
|
|
284
|
+
const decision = await approval.request({
|
|
285
|
+
kind: "skill",
|
|
286
|
+
summary: `graph delete ${parsed.name}`,
|
|
287
|
+
args: {
|
|
288
|
+
operation: {
|
|
289
|
+
action: "delete",
|
|
290
|
+
name: parsed.name
|
|
291
|
+
},
|
|
292
|
+
origin: origins.approval,
|
|
293
|
+
libraryOrigin: origins.library
|
|
294
|
+
},
|
|
295
|
+
origin: origins.approval
|
|
296
|
+
});
|
|
297
|
+
if (decision.action === "staged") return ok(decision.message);
|
|
298
|
+
}
|
|
212
299
|
const result = await withSkills().archive(parsed.name);
|
|
213
300
|
if (result.ok) await usageService?.markArchived?.(parsed.name);
|
|
214
301
|
return result.ok ? ok(result.message) : err(result.message);
|
|
215
302
|
}
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
if (entry === void 0) return err(`Memory ${parsed.source}[${parsed.index}] does not exist (${entries.length} entries).`);
|
|
303
|
+
const check = readMemoryIndex(parsed, await memory.read(parsed.source));
|
|
304
|
+
if (!check.ok) return err(check.message ?? "Memory index check failed.");
|
|
219
305
|
const result = await memory.applyBatch(parsed.source, [{
|
|
220
306
|
action: "remove",
|
|
221
|
-
old_text: entry
|
|
307
|
+
old_text: check.entry ?? ""
|
|
222
308
|
}]);
|
|
223
309
|
return result.ok ? ok(result.message) : err(result.message);
|
|
224
310
|
}
|
|
@@ -227,4 +313,4 @@ function apply(ctx) {
|
|
|
227
313
|
});
|
|
228
314
|
}
|
|
229
315
|
//#endregion
|
|
230
|
-
export { apply, buildLearningGraph, graphDensity, name, parseGraphNodeId, resolveGraphNode };
|
|
316
|
+
export { apply, buildLearningGraph, graphDensity, memorySnapshotOf, name, parseGraphNodeId, readMemoryIndex, resolveGraphNode };
|
package/lib/types/index.d.ts
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* (`/evolution graph [detail|edit|delete] <nodeId>`) aligned with the Hermes
|
|
4
4
|
* journey surface: a skill node is its name, a memory node is
|
|
5
5
|
* `memory:<source>:<index>` (source = memory|user, index = position in that
|
|
6
|
-
* file's entries).
|
|
6
|
+
* file's entries). Memory node ids carry a trailing snapshot token so
|
|
7
|
+
* edit/delete can reject a stale index (E-21).
|
|
7
8
|
* @module @deepseek-ai/dsh-evolution-learning-graph
|
|
8
9
|
*/
|
|
9
10
|
import type { Context } from '@deepseek-ai/cordis';
|
|
@@ -47,10 +48,7 @@ export declare function graphDensity(graph: LearningGraph): GraphDensity;
|
|
|
47
48
|
* omitted `related` yields no skill-skill edges — the former alphabet-order
|
|
48
49
|
* chain was a placeholder that connected unrelated neighbors.
|
|
49
50
|
*/
|
|
50
|
-
export declare function buildLearningGraph(usage: ReadonlyMap<string,
|
|
51
|
-
use_count?: number;
|
|
52
|
-
pinned?: boolean;
|
|
53
|
-
}>, memoryEntries: readonly string[], userEntries?: readonly string[], related?: ReadonlyMap<string, readonly string[]>): LearningGraph;
|
|
51
|
+
export declare function buildLearningGraph(usage: ReadonlyMap<string, unknown>, memoryEntries: readonly string[], userEntries?: readonly string[], related?: ReadonlyMap<string, readonly string[]>): LearningGraph;
|
|
54
52
|
export type GraphNodeId = {
|
|
55
53
|
kind: 'skill';
|
|
56
54
|
name: string;
|
|
@@ -58,8 +56,42 @@ export type GraphNodeId = {
|
|
|
58
56
|
kind: 'memory';
|
|
59
57
|
source: 'memory' | 'user';
|
|
60
58
|
index: number;
|
|
59
|
+
snapshot?: string;
|
|
61
60
|
};
|
|
62
|
-
/**
|
|
61
|
+
/**
|
|
62
|
+
* E-21 snapshot token: a deterministic 8-hex-char digest of a memory entry's
|
|
63
|
+
* rendered label (first line, first 80 chars). The graph builder embeds it in
|
|
64
|
+
* the memory node id so a later edit/delete can detect index drift — any
|
|
65
|
+
* memory write between the last render and the command shifts `memory:<source>:<index>`
|
|
66
|
+
* positions, and the token lets the command reject a stale index instead of
|
|
67
|
+
* mutating the wrong entry. FNV-1a (no imports, cross-platform).
|
|
68
|
+
*/
|
|
69
|
+
export declare function memorySnapshotOf(entry: string): string;
|
|
70
|
+
export interface MemoryIndexCheck {
|
|
71
|
+
ok: boolean;
|
|
72
|
+
/** The current entry at the index, present when ok. */
|
|
73
|
+
entry?: string;
|
|
74
|
+
message?: string;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* E-21 TOCTOU guard for a memory node operation. Re-reads the entry at
|
|
78
|
+
* `parsed.index` and, when the id carries a snapshot token, verifies the
|
|
79
|
+
* current entry's label still matches it. A mismatch means a memory write
|
|
80
|
+
* shifted the index between the last `/graph` render and this command, so the
|
|
81
|
+
* operation is refused (the caller surfaces "re-run /graph"). A missing
|
|
82
|
+
* snapshot (a hand-typed bare `memory:<source>:<index>` id) skips the check
|
|
83
|
+
* and falls back to operating on whatever currently sits at the index.
|
|
84
|
+
*/
|
|
85
|
+
export declare function readMemoryIndex(parsed: Extract<GraphNodeId, {
|
|
86
|
+
kind: 'memory';
|
|
87
|
+
}>, entries: readonly string[]): MemoryIndexCheck;
|
|
88
|
+
/**
|
|
89
|
+
* Parse a graph node id: skill names pass through; `memory:<source>:<index>`
|
|
90
|
+
* becomes a memory node. An optional trailing `:<snapshot>` (the E-21
|
|
91
|
+
* `memorySnapshotOf` token the builder embeds) is carried so edit/delete can
|
|
92
|
+
* detect index drift; a bare index id (hand-typed) still parses with no
|
|
93
|
+
* snapshot and skips the drift check.
|
|
94
|
+
*/
|
|
63
95
|
export declare function parseGraphNodeId(id: string): GraphNodeId | null;
|
|
64
96
|
/** Resolve a parsed node to its current content (read-only). */
|
|
65
97
|
export declare function resolveGraphNode(parsed: GraphNodeId, repository: {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lmzhen/dsh-evolution-learning-graph",
|
|
3
3
|
"description": "Learning graph over skills and memory (community build)",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.18",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"license": "MIT",
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@deepseek-ai/schemastery": "^3.18.1",
|
|
36
|
-
"@lmzhen/dsh-evolution-core": "^0.3.
|
|
36
|
+
"@lmzhen/dsh-evolution-core": "^0.3.18"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
39
|
"@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
|