@lmzhen/dsh-evolution-learning-graph 0.1.0-rc.8 → 0.1.0
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 +203 -37
- package/lib/types/index.d.ts +50 -3
- package/package.json +5 -4
package/lib/index.js
CHANGED
|
@@ -1,10 +1,45 @@
|
|
|
1
|
+
import { SKILL_NAME_RE, SkillLibrary, evolutionIoAdapter, relatedSkillNames } from "@lmzhen/dsh-evolution-core";
|
|
1
2
|
//#region lib/types/index.js
|
|
2
3
|
/**
|
|
3
|
-
* Learning graph over skills and memory
|
|
4
|
+
* Learning graph over skills and memory, plus node-level commands
|
|
5
|
+
* (`/evolution graph [detail|edit|delete] <nodeId>`) aligned with the Hermes
|
|
6
|
+
* journey surface: a skill node is its name, a memory node is
|
|
7
|
+
* `memory:<source>:<index>` (source = memory|user, index = position in that
|
|
8
|
+
* file's entries).
|
|
4
9
|
* @module @lmzhen/dsh-evolution-learning-graph
|
|
5
10
|
*/
|
|
6
|
-
|
|
7
|
-
|
|
11
|
+
function graphDensity(graph) {
|
|
12
|
+
const linked = /* @__PURE__ */ new Set();
|
|
13
|
+
let relatedEdges = 0;
|
|
14
|
+
for (const edge of graph.edges) {
|
|
15
|
+
if (edge.type !== "related") continue;
|
|
16
|
+
relatedEdges += 1;
|
|
17
|
+
linked.add(edge.from);
|
|
18
|
+
linked.add(edge.to);
|
|
19
|
+
}
|
|
20
|
+
const skillNodes = graph.nodes.filter((node) => node.kind === "skill").length;
|
|
21
|
+
const isolated = graph.nodes.filter((node) => node.kind === "skill" && !linked.has(node.id)).length;
|
|
22
|
+
return {
|
|
23
|
+
skillNodes,
|
|
24
|
+
relatedEdges,
|
|
25
|
+
edgesPerNode: skillNodes === 0 ? 0 : Math.round(relatedEdges / skillNodes * 100) / 100,
|
|
26
|
+
isolatedPct: skillNodes === 0 ? 0 : Math.round(isolated / skillNodes * 100)
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Build a small deterministic graph from usage records and memory entries.
|
|
31
|
+
* Memory node ids follow `memory:<source>:<index>` (source = memory|user,
|
|
32
|
+
* index = position in that file's entries) — the SAME rule the parser
|
|
33
|
+
* `parseGraphNodeId` accepts, so graph detail/edit/delete round-trips.
|
|
34
|
+
*
|
|
35
|
+
* Skill-skill edges are semantic only (B-line G3): each entry of `related`
|
|
36
|
+
* (skill name -> names its frontmatter references, from
|
|
37
|
+
* `relatedSkillNames`) becomes an edge when BOTH endpoints exist in the
|
|
38
|
+
* usage set, self-edges are dropped and undirected duplicates collapse. An
|
|
39
|
+
* omitted `related` yields no skill-skill edges — the former alphabet-order
|
|
40
|
+
* chain was a placeholder that connected unrelated neighbors.
|
|
41
|
+
*/
|
|
42
|
+
function buildLearningGraph(usage, memoryEntries, userEntries = [], related) {
|
|
8
43
|
const nodes = [...usage.keys()].map((name) => ({
|
|
9
44
|
id: name,
|
|
10
45
|
kind: "skill",
|
|
@@ -12,52 +47,183 @@ function buildLearningGraph(usage, memoryEntries) {
|
|
|
12
47
|
}));
|
|
13
48
|
const edges = [];
|
|
14
49
|
const sorted = [...usage.keys()].sort();
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
from
|
|
20
|
-
to
|
|
21
|
-
|
|
22
|
-
|
|
50
|
+
const seenPairs = /* @__PURE__ */ new Set();
|
|
51
|
+
for (const [from, targets] of related ?? []) {
|
|
52
|
+
if (!usage.has(from)) continue;
|
|
53
|
+
for (const to of targets) {
|
|
54
|
+
if (to === from || !usage.has(to)) continue;
|
|
55
|
+
const pairKey = [from, to].sort().join("->");
|
|
56
|
+
if (seenPairs.has(pairKey)) continue;
|
|
57
|
+
seenPairs.add(pairKey);
|
|
58
|
+
edges.push({
|
|
59
|
+
from,
|
|
60
|
+
to,
|
|
61
|
+
type: "related"
|
|
62
|
+
});
|
|
63
|
+
}
|
|
23
64
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
65
|
+
const appendMemory = (source, entries) => {
|
|
66
|
+
entries.forEach((entry, index) => {
|
|
67
|
+
const id = `memory:${source}:${index}`;
|
|
68
|
+
nodes.push({
|
|
69
|
+
id,
|
|
70
|
+
kind: "memory",
|
|
71
|
+
label: entry.split("\n")[0]?.slice(0, 80) ?? id
|
|
72
|
+
});
|
|
73
|
+
const token = entry.toLowerCase();
|
|
74
|
+
for (const name of sorted) if (name && token.includes(name.toLowerCase())) edges.push({
|
|
75
|
+
from: id,
|
|
76
|
+
to: name,
|
|
77
|
+
type: "memory_skill"
|
|
78
|
+
});
|
|
30
79
|
});
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
to: name,
|
|
35
|
-
type: "memory_skill"
|
|
36
|
-
});
|
|
37
|
-
});
|
|
80
|
+
};
|
|
81
|
+
appendMemory("memory", memoryEntries);
|
|
82
|
+
appendMemory("user", userEntries);
|
|
38
83
|
return {
|
|
39
84
|
nodes,
|
|
40
85
|
edges
|
|
41
86
|
};
|
|
42
87
|
}
|
|
88
|
+
/** Parse a graph node id: skill names pass through; `memory:<source>:<index>` becomes a memory node. */
|
|
89
|
+
function parseGraphNodeId(id) {
|
|
90
|
+
const memory = /^memory:(memory|user):(\d+)$/.exec(id);
|
|
91
|
+
if (memory) return {
|
|
92
|
+
kind: "memory",
|
|
93
|
+
source: memory[1],
|
|
94
|
+
index: Number(memory[2])
|
|
95
|
+
};
|
|
96
|
+
if (SKILL_NAME_RE.test(id)) return {
|
|
97
|
+
kind: "skill",
|
|
98
|
+
name: id
|
|
99
|
+
};
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
/** Resolve a parsed node to its current content (read-only). */
|
|
103
|
+
async function resolveGraphNode(parsed, repository) {
|
|
104
|
+
if (parsed.kind === "skill") {
|
|
105
|
+
const content = await repository.readSkill(parsed.name);
|
|
106
|
+
return content === null ? {
|
|
107
|
+
ok: false,
|
|
108
|
+
message: `Skill "${parsed.name}" not found.`
|
|
109
|
+
} : {
|
|
110
|
+
ok: true,
|
|
111
|
+
message: content
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
const entries = await repository.readMemory(parsed.source);
|
|
115
|
+
const entry = entries[parsed.index];
|
|
116
|
+
return entry === void 0 ? {
|
|
117
|
+
ok: false,
|
|
118
|
+
message: `Memory ${parsed.source}[${parsed.index}] does not exist (${entries.length} entries).`
|
|
119
|
+
} : {
|
|
120
|
+
ok: true,
|
|
121
|
+
message: entry
|
|
122
|
+
};
|
|
123
|
+
}
|
|
43
124
|
const name = "evolution-learning-graph";
|
|
44
125
|
function apply(ctx) {
|
|
45
126
|
ctx.inject(["commands"], (commandCtx) => {
|
|
46
|
-
commandCtx.commands
|
|
47
|
-
|
|
48
|
-
|
|
127
|
+
const commands = commandCtx.commands;
|
|
128
|
+
commandCtx.effect(() => commands.register({
|
|
129
|
+
name: "graph",
|
|
130
|
+
description: "Show the learning graph, or act on a node: graph [detail|edit|delete] <nodeId>",
|
|
49
131
|
recordInput: false,
|
|
50
|
-
handler: async () => {
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
132
|
+
handler: async (invocation) => {
|
|
133
|
+
const ok = (text) => ({
|
|
134
|
+
kind: "success",
|
|
135
|
+
text
|
|
136
|
+
});
|
|
137
|
+
const err = (text) => ({
|
|
138
|
+
kind: "error",
|
|
139
|
+
text
|
|
140
|
+
});
|
|
141
|
+
const usageService = ctx.get("skillUsage");
|
|
142
|
+
const memoryService = ctx.get("memory");
|
|
143
|
+
const ioService = ctx.get("evolutionIo");
|
|
144
|
+
if (!usageService || !memoryService || !ioService) return err("skill-usage, memory or evolution-io service is not mounted.");
|
|
145
|
+
const usage = usageService;
|
|
146
|
+
const memory = memoryService;
|
|
147
|
+
const io = ioService;
|
|
148
|
+
const input = (invocation.rawInput ?? "").trim();
|
|
149
|
+
const detail = /^detail\s+(\S+)$/.exec(input);
|
|
150
|
+
if (detail && detail[1]) return await nodeDetail(detail[1]);
|
|
151
|
+
const edit = /^edit\s+(\S+)\s+([\s\S]+)$/.exec(input);
|
|
152
|
+
if (edit && edit[1] && edit[2] !== void 0) return await nodeEdit(edit[1], edit[2]);
|
|
153
|
+
const remove = /^delete\s+(\S+)$/.exec(input);
|
|
154
|
+
if (remove && remove[1]) return await nodeDelete(remove[1]);
|
|
155
|
+
const directory = await renderGraph();
|
|
156
|
+
if (input !== "") return err(`Unknown graph subcommand "${input.split(" ")[0]}". ${directory}`);
|
|
157
|
+
return ok(directory);
|
|
158
|
+
async function renderGraph() {
|
|
159
|
+
const usageMap = await usage.report();
|
|
160
|
+
const memoryEntries = await memory.read("memory");
|
|
161
|
+
const userEntries = await memory.read("user");
|
|
162
|
+
const skills = withSkills();
|
|
163
|
+
const related = /* @__PURE__ */ new Map();
|
|
164
|
+
for (const name of usageMap.keys()) {
|
|
165
|
+
const content = await skills.read(name);
|
|
166
|
+
if (content === null) continue;
|
|
167
|
+
related.set(name, relatedSkillNames(content, name));
|
|
168
|
+
}
|
|
169
|
+
const graph = buildLearningGraph(usageMap, memoryEntries, userEntries, related);
|
|
170
|
+
const lines = graph.nodes.map((node) => (node.kind === "memory" ? "◆" : "●") + " " + node.label);
|
|
171
|
+
const edges = graph.edges.map((edge) => edge.from + " --" + edge.type + "--> " + edge.to);
|
|
172
|
+
const density = graphDensity(graph);
|
|
173
|
+
const densityLine = `\n\nSkills: ${density.skillNodes} · related edges: ${density.relatedEdges} (${density.edgesPerNode}/node) · isolated: ${density.isolatedPct}%`;
|
|
174
|
+
return lines.join("\n") + "\n\n" + edges.join("\n") + densityLine;
|
|
175
|
+
}
|
|
176
|
+
function withSkills() {
|
|
177
|
+
return new SkillLibrary(void 0, evolutionIoAdapter(() => io.provider()), void 0, (event) => {
|
|
178
|
+
ctx.emit("evolution/skill-mutated", event);
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
async function nodeDetail(id) {
|
|
182
|
+
const parsed = parseGraphNodeId(id);
|
|
183
|
+
if (parsed === null) return err(`Invalid node id "${id}". Skill names or memory:<source>:<index> expected.`);
|
|
184
|
+
const resolved = await resolveGraphNode(parsed, {
|
|
185
|
+
readSkill: (name) => withSkills().read(name),
|
|
186
|
+
readMemory: (target) => memory.read(target)
|
|
187
|
+
});
|
|
188
|
+
return resolved.ok ? ok(resolved.message) : err(resolved.message);
|
|
189
|
+
}
|
|
190
|
+
async function nodeEdit(id, content) {
|
|
191
|
+
const parsed = parseGraphNodeId(id);
|
|
192
|
+
if (parsed === null) return err(`Invalid node id "${id}". Skill names or memory:<source>:<index> expected.`);
|
|
193
|
+
if (parsed.kind === "skill") {
|
|
194
|
+
const result = await withSkills().update(parsed.name, content, "foreground");
|
|
195
|
+
return result.ok ? ok(result.message) : err(result.message);
|
|
196
|
+
}
|
|
197
|
+
const entries = await memory.read(parsed.source);
|
|
198
|
+
const entry = entries[parsed.index];
|
|
199
|
+
if (entry === void 0) return err(`Memory ${parsed.source}[${parsed.index}] does not exist (${entries.length} entries).`);
|
|
200
|
+
const result = await memory.applyBatch(parsed.source, [{
|
|
201
|
+
action: "replace",
|
|
202
|
+
old_text: entry,
|
|
203
|
+
facts: content
|
|
204
|
+
}]);
|
|
205
|
+
return result.ok ? ok(result.message) : err(result.message);
|
|
206
|
+
}
|
|
207
|
+
async function nodeDelete(id) {
|
|
208
|
+
const parsed = parseGraphNodeId(id);
|
|
209
|
+
if (parsed === null) return err(`Invalid node id "${id}". Skill names or memory:<source>:<index> expected.`);
|
|
210
|
+
if (parsed.kind === "skill") {
|
|
211
|
+
const result = await withSkills().archive(parsed.name);
|
|
212
|
+
if (result.ok) await usageService?.markArchived?.(parsed.name);
|
|
213
|
+
return result.ok ? ok(result.message) : err(result.message);
|
|
214
|
+
}
|
|
215
|
+
const entries = await memory.read(parsed.source);
|
|
216
|
+
const entry = entries[parsed.index];
|
|
217
|
+
if (entry === void 0) return err(`Memory ${parsed.source}[${parsed.index}] does not exist (${entries.length} entries).`);
|
|
218
|
+
const result = await memory.applyBatch(parsed.source, [{
|
|
219
|
+
action: "remove",
|
|
220
|
+
old_text: entry
|
|
221
|
+
}]);
|
|
222
|
+
return result.ok ? ok(result.message) : err(result.message);
|
|
223
|
+
}
|
|
58
224
|
}
|
|
59
|
-
});
|
|
225
|
+
}), "evolution-learning-graph.command");
|
|
60
226
|
});
|
|
61
227
|
}
|
|
62
228
|
//#endregion
|
|
63
|
-
export { apply, buildLearningGraph, name };
|
|
229
|
+
export { apply, buildLearningGraph, graphDensity, name, parseGraphNodeId, resolveGraphNode };
|
package/lib/types/index.d.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Learning graph over skills and memory
|
|
2
|
+
* Learning graph over skills and memory, plus node-level commands
|
|
3
|
+
* (`/evolution graph [detail|edit|delete] <nodeId>`) aligned with the Hermes
|
|
4
|
+
* journey surface: a skill node is its name, a memory node is
|
|
5
|
+
* `memory:<source>:<index>` (source = memory|user, index = position in that
|
|
6
|
+
* file's entries).
|
|
3
7
|
* @module @deepseek-ai/dsh-evolution-learning-graph
|
|
4
8
|
*/
|
|
5
9
|
import type { Context } from '@deepseek-ai/cordis';
|
|
@@ -17,11 +21,54 @@ export interface LearningGraph {
|
|
|
17
21
|
nodes: GraphNode[];
|
|
18
22
|
edges: GraphEdge[];
|
|
19
23
|
}
|
|
20
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* Structural summary of the skill subgraph (Hermes `learning_graph` density
|
|
26
|
+
* parity): edges per skill node and the share of skills no edge touches.
|
|
27
|
+
* Memory nodes are excluded — they carry token-matched edges by construction,
|
|
28
|
+
* which would dilute the isolation signal the statistic exists to expose.
|
|
29
|
+
*/
|
|
30
|
+
export interface GraphDensity {
|
|
31
|
+
skillNodes: number;
|
|
32
|
+
relatedEdges: number;
|
|
33
|
+
edgesPerNode: number;
|
|
34
|
+
isolatedPct: number;
|
|
35
|
+
}
|
|
36
|
+
export declare function graphDensity(graph: LearningGraph): GraphDensity;
|
|
37
|
+
/**
|
|
38
|
+
* Build a small deterministic graph from usage records and memory entries.
|
|
39
|
+
* Memory node ids follow `memory:<source>:<index>` (source = memory|user,
|
|
40
|
+
* index = position in that file's entries) — the SAME rule the parser
|
|
41
|
+
* `parseGraphNodeId` accepts, so graph detail/edit/delete round-trips.
|
|
42
|
+
*
|
|
43
|
+
* Skill-skill edges are semantic only (B-line G3): each entry of `related`
|
|
44
|
+
* (skill name -> names its frontmatter references, from
|
|
45
|
+
* `relatedSkillNames`) becomes an edge when BOTH endpoints exist in the
|
|
46
|
+
* usage set, self-edges are dropped and undirected duplicates collapse. An
|
|
47
|
+
* omitted `related` yields no skill-skill edges — the former alphabet-order
|
|
48
|
+
* chain was a placeholder that connected unrelated neighbors.
|
|
49
|
+
*/
|
|
21
50
|
export declare function buildLearningGraph(usage: ReadonlyMap<string, {
|
|
22
51
|
use_count?: number;
|
|
23
52
|
pinned?: boolean;
|
|
24
|
-
}>, memoryEntries: readonly string[]): LearningGraph;
|
|
53
|
+
}>, memoryEntries: readonly string[], userEntries?: readonly string[], related?: ReadonlyMap<string, readonly string[]>): LearningGraph;
|
|
54
|
+
export type GraphNodeId = {
|
|
55
|
+
kind: 'skill';
|
|
56
|
+
name: string;
|
|
57
|
+
} | {
|
|
58
|
+
kind: 'memory';
|
|
59
|
+
source: 'memory' | 'user';
|
|
60
|
+
index: number;
|
|
61
|
+
};
|
|
62
|
+
/** Parse a graph node id: skill names pass through; `memory:<source>:<index>` becomes a memory node. */
|
|
63
|
+
export declare function parseGraphNodeId(id: string): GraphNodeId | null;
|
|
64
|
+
/** Resolve a parsed node to its current content (read-only). */
|
|
65
|
+
export declare function resolveGraphNode(parsed: GraphNodeId, repository: {
|
|
66
|
+
readSkill(name: string): Promise<string | null>;
|
|
67
|
+
readMemory(target: 'memory' | 'user'): Promise<string[]>;
|
|
68
|
+
}): Promise<{
|
|
69
|
+
ok: boolean;
|
|
70
|
+
message: string;
|
|
71
|
+
}>;
|
|
25
72
|
export declare const name = "evolution-learning-graph";
|
|
26
73
|
export declare function apply(ctx: Context): void;
|
|
27
74
|
//# 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.1.0
|
|
4
|
+
"version": "0.1.0",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -32,13 +32,14 @@
|
|
|
32
32
|
],
|
|
33
33
|
"license": "MIT",
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@deepseek-ai/schemastery": "^3.18.1"
|
|
35
|
+
"@deepseek-ai/schemastery": "^3.18.1",
|
|
36
|
+
"@lmzhen/dsh-evolution-core": "^0.1.0"
|
|
36
37
|
},
|
|
37
38
|
"peerDependencies": {
|
|
38
|
-
"@deepseek-ai/dsh-invariants": "^0.1.
|
|
39
|
+
"@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
|
|
39
40
|
"@deepseek-ai/cordis": "^4.0.1"
|
|
40
41
|
},
|
|
41
42
|
"devDependencies": {
|
|
42
|
-
"@deepseek-ai/dsh-invariants": "^0.1.
|
|
43
|
+
"@deepseek-ai/dsh-invariants": "^0.1.1-rc.2"
|
|
43
44
|
}
|
|
44
45
|
}
|