@lmzhen/dsh-evolution-learning-graph 0.1.0-rc.43 → 0.1.0-rc.44

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 CHANGED
@@ -1,4 +1,4 @@
1
- import { SKILL_NAME_RE, SkillLibrary, evolutionIoAdapter } from "@lmzhen/dsh-evolution-core";
1
+ import { SKILL_NAME_RE, SkillLibrary, evolutionIoAdapter, relatedSkillNames } 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
@@ -8,13 +8,38 @@ import { SKILL_NAME_RE, SkillLibrary, evolutionIoAdapter } from "@lmzhen/dsh-evo
8
8
  * file's entries).
9
9
  * @module @lmzhen/dsh-evolution-learning-graph
10
10
  */
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
+ }
11
29
  /**
12
30
  * Build a small deterministic graph from usage records and memory entries.
13
31
  * Memory node ids follow `memory:<source>:<index>` (source = memory|user,
14
32
  * index = position in that file's entries) — the SAME rule the parser
15
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.
16
41
  */
17
- function buildLearningGraph(usage, memoryEntries, userEntries = []) {
42
+ function buildLearningGraph(usage, memoryEntries, userEntries = [], related) {
18
43
  const nodes = [...usage.keys()].map((name) => ({
19
44
  id: name,
20
45
  kind: "skill",
@@ -22,14 +47,20 @@ function buildLearningGraph(usage, memoryEntries, userEntries = []) {
22
47
  }));
23
48
  const edges = [];
24
49
  const sorted = [...usage.keys()].sort();
25
- for (let i = 1; i < sorted.length; i += 1) {
26
- const from = sorted[i - 1];
27
- const to = sorted[i];
28
- if (from && to) edges.push({
29
- from,
30
- to,
31
- type: "related"
32
- });
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
+ }
33
64
  }
34
65
  const appendMemory = (source, entries) => {
35
66
  entries.forEach((entry, index) => {
@@ -124,10 +155,22 @@ function apply(ctx) {
124
155
  if (input !== "") return err(`Unknown graph subcommand "${input.split(" ")[0]}". ${directory}`);
125
156
  return ok(directory);
126
157
  async function renderGraph() {
127
- const graph = buildLearningGraph(await usage.report(), await memory.read("memory"), await memory.read("user"));
158
+ const usageMap = await usage.report();
159
+ const memoryEntries = await memory.read("memory");
160
+ const userEntries = await memory.read("user");
161
+ const skills = withSkills();
162
+ const related = /* @__PURE__ */ new Map();
163
+ for (const name of usageMap.keys()) {
164
+ const content = await skills.read(name);
165
+ if (content === null) continue;
166
+ related.set(name, relatedSkillNames(content, name));
167
+ }
168
+ const graph = buildLearningGraph(usageMap, memoryEntries, userEntries, related);
128
169
  const lines = graph.nodes.map((node) => (node.kind === "memory" ? "◆" : "●") + " " + node.label);
129
170
  const edges = graph.edges.map((edge) => edge.from + " --" + edge.type + "--> " + edge.to);
130
- return lines.join("\n") + "\n\n" + edges.join("\n");
171
+ const density = graphDensity(graph);
172
+ const densityLine = `\n\nSkills: ${density.skillNodes} · related edges: ${density.relatedEdges} (${density.edgesPerNode}/node) · isolated: ${density.isolatedPct}%`;
173
+ return lines.join("\n") + "\n\n" + edges.join("\n") + densityLine;
131
174
  }
132
175
  function withSkills() {
133
176
  return new SkillLibrary(void 0, evolutionIoAdapter(() => io.provider()));
@@ -180,4 +223,4 @@ function apply(ctx) {
180
223
  });
181
224
  }
182
225
  //#endregion
183
- export { apply, buildLearningGraph, name, parseGraphNodeId, resolveGraphNode };
226
+ export { apply, buildLearningGraph, graphDensity, name, parseGraphNodeId, resolveGraphNode };
@@ -21,16 +21,36 @@ export interface LearningGraph {
21
21
  nodes: GraphNode[];
22
22
  edges: GraphEdge[];
23
23
  }
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;
24
37
  /**
25
38
  * Build a small deterministic graph from usage records and memory entries.
26
39
  * Memory node ids follow `memory:<source>:<index>` (source = memory|user,
27
40
  * index = position in that file's entries) — the SAME rule the parser
28
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.
29
49
  */
30
50
  export declare function buildLearningGraph(usage: ReadonlyMap<string, {
31
51
  use_count?: number;
32
52
  pinned?: boolean;
33
- }>, memoryEntries: readonly string[], userEntries?: readonly string[]): LearningGraph;
53
+ }>, memoryEntries: readonly string[], userEntries?: readonly string[], related?: ReadonlyMap<string, readonly string[]>): LearningGraph;
34
54
  export type GraphNodeId = {
35
55
  kind: 'skill';
36
56
  name: string;
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-rc.43",
4
+ "version": "0.1.0-rc.44",
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.1.0-rc.43"
36
+ "@lmzhen/dsh-evolution-core": "^0.1.0-rc.44"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "@deepseek-ai/dsh-invariants": "^0.1.0-rc.6",