@lmzhen/dsh-evolution-learning-graph 0.3.17 → 0.3.19
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 +111 -19
- package/lib/types/index.d.ts +44 -6
- package/package.json +3 -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",
|
|
@@ -121,6 +171,12 @@ async function resolveGraphNode(parsed, repository) {
|
|
|
121
171
|
message: entry
|
|
122
172
|
};
|
|
123
173
|
}
|
|
174
|
+
/**
|
|
175
|
+
* E-26 approval seam (soft-probed, mirrors tool-skill-manage): graph skill
|
|
176
|
+
* mutations route through the same `evolutionApproval.request` gate. The
|
|
177
|
+
* staged `args` use the skill_manage runner's shape so the runner tool-skill-
|
|
178
|
+
* manage registers for `kind: 'skill'` replays them on approve.
|
|
179
|
+
*/
|
|
124
180
|
const name = "evolution-learning-graph";
|
|
125
181
|
function apply(ctx) {
|
|
126
182
|
ctx.inject(["commands"], (commandCtx) => {
|
|
@@ -175,7 +231,7 @@ function apply(ctx) {
|
|
|
175
231
|
return lines.join("\n") + "\n\n" + edges.join("\n") + densityLine;
|
|
176
232
|
}
|
|
177
233
|
function withSkills() {
|
|
178
|
-
return new SkillLibrary(
|
|
234
|
+
return new SkillLibrary(resolveSkillsRoot(), evolutionIoAdapter(() => io.provider()), void 0, (event) => {
|
|
179
235
|
ctx.emit("evolution/skill-mutated", event);
|
|
180
236
|
});
|
|
181
237
|
}
|
|
@@ -192,15 +248,34 @@ function apply(ctx) {
|
|
|
192
248
|
const parsed = parseGraphNodeId(id);
|
|
193
249
|
if (parsed === null) return err(`Invalid node id "${id}". Skill names or memory:<source>:<index> expected.`);
|
|
194
250
|
if (parsed.kind === "skill") {
|
|
251
|
+
const approval = ctx.get("evolutionApproval");
|
|
252
|
+
if (approval) {
|
|
253
|
+
const origins = resolveOrigins(void 0);
|
|
254
|
+
const decision = await approval.request({
|
|
255
|
+
kind: "skill",
|
|
256
|
+
summary: `graph edit ${parsed.name}`,
|
|
257
|
+
args: {
|
|
258
|
+
operation: {
|
|
259
|
+
action: "update",
|
|
260
|
+
name: parsed.name,
|
|
261
|
+
content
|
|
262
|
+
},
|
|
263
|
+
origin: origins.approval,
|
|
264
|
+
libraryOrigin: origins.library
|
|
265
|
+
},
|
|
266
|
+
origin: origins.approval
|
|
267
|
+
});
|
|
268
|
+
if (decision.action === "staged") return ok(decision.message);
|
|
269
|
+
}
|
|
195
270
|
const result = await withSkills().update(parsed.name, content, "foreground");
|
|
271
|
+
if (result.ok) await usageService.record?.(parsed.name, "patch");
|
|
196
272
|
return result.ok ? ok(result.message) : err(result.message);
|
|
197
273
|
}
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
if (entry === void 0) return err(`Memory ${parsed.source}[${parsed.index}] does not exist (${entries.length} entries).`);
|
|
274
|
+
const check = readMemoryIndex(parsed, await memory.read(parsed.source));
|
|
275
|
+
if (!check.ok) return err(check.message ?? "Memory index check failed.");
|
|
201
276
|
const result = await memory.applyBatch(parsed.source, [{
|
|
202
277
|
action: "replace",
|
|
203
|
-
old_text: entry,
|
|
278
|
+
old_text: check.entry ?? "",
|
|
204
279
|
facts: content
|
|
205
280
|
}]);
|
|
206
281
|
return result.ok ? ok(result.message) : err(result.message);
|
|
@@ -209,16 +284,33 @@ function apply(ctx) {
|
|
|
209
284
|
const parsed = parseGraphNodeId(id);
|
|
210
285
|
if (parsed === null) return err(`Invalid node id "${id}". Skill names or memory:<source>:<index> expected.`);
|
|
211
286
|
if (parsed.kind === "skill") {
|
|
287
|
+
const approval = ctx.get("evolutionApproval");
|
|
288
|
+
if (approval) {
|
|
289
|
+
const origins = resolveOrigins(void 0);
|
|
290
|
+
const decision = await approval.request({
|
|
291
|
+
kind: "skill",
|
|
292
|
+
summary: `graph delete ${parsed.name}`,
|
|
293
|
+
args: {
|
|
294
|
+
operation: {
|
|
295
|
+
action: "delete",
|
|
296
|
+
name: parsed.name
|
|
297
|
+
},
|
|
298
|
+
origin: origins.approval,
|
|
299
|
+
libraryOrigin: origins.library
|
|
300
|
+
},
|
|
301
|
+
origin: origins.approval
|
|
302
|
+
});
|
|
303
|
+
if (decision.action === "staged") return ok(decision.message);
|
|
304
|
+
}
|
|
212
305
|
const result = await withSkills().archive(parsed.name);
|
|
213
306
|
if (result.ok) await usageService?.markArchived?.(parsed.name);
|
|
214
307
|
return result.ok ? ok(result.message) : err(result.message);
|
|
215
308
|
}
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
if (entry === void 0) return err(`Memory ${parsed.source}[${parsed.index}] does not exist (${entries.length} entries).`);
|
|
309
|
+
const check = readMemoryIndex(parsed, await memory.read(parsed.source));
|
|
310
|
+
if (!check.ok) return err(check.message ?? "Memory index check failed.");
|
|
219
311
|
const result = await memory.applyBatch(parsed.source, [{
|
|
220
312
|
action: "remove",
|
|
221
|
-
old_text: entry
|
|
313
|
+
old_text: check.entry ?? ""
|
|
222
314
|
}]);
|
|
223
315
|
return result.ok ? ok(result.message) : err(result.message);
|
|
224
316
|
}
|
|
@@ -227,4 +319,4 @@ function apply(ctx) {
|
|
|
227
319
|
});
|
|
228
320
|
}
|
|
229
321
|
//#endregion
|
|
230
|
-
export { apply, buildLearningGraph, graphDensity, name, parseGraphNodeId, resolveGraphNode };
|
|
322
|
+
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: {
|
|
@@ -69,6 +101,12 @@ export declare function resolveGraphNode(parsed: GraphNodeId, repository: {
|
|
|
69
101
|
ok: boolean;
|
|
70
102
|
message: string;
|
|
71
103
|
}>;
|
|
104
|
+
/**
|
|
105
|
+
* E-26 approval seam (soft-probed, mirrors tool-skill-manage): graph skill
|
|
106
|
+
* mutations route through the same `evolutionApproval.request` gate. The
|
|
107
|
+
* staged `args` use the skill_manage runner's shape so the runner tool-skill-
|
|
108
|
+
* manage registers for `kind: 'skill'` replays them on approve.
|
|
109
|
+
*/
|
|
72
110
|
export declare const name = "evolution-learning-graph";
|
|
73
111
|
export declare function apply(ctx: Context): void;
|
|
74
112
|
//# sourceMappingURL=index.d.ts.map
|
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.19",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -33,7 +33,8 @@
|
|
|
33
33
|
"license": "MIT",
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@deepseek-ai/schemastery": "^3.18.1",
|
|
36
|
-
"@lmzhen/dsh-evolution-
|
|
36
|
+
"@lmzhen/dsh-evolution-approval": "^0.3.19",
|
|
37
|
+
"@lmzhen/dsh-evolution-core": "^0.3.19"
|
|
37
38
|
},
|
|
38
39
|
"peerDependencies": {
|
|
39
40
|
"@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
|