ygl-trees 0.69.0 → 0.71.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/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.70.0(2026-10-17)`agent metrics`——从 R11 手动测量到自动化指标
4
+ - **`ygl-trees agent metrics`**:一条命令输出工作台全量量化指标——冷启动(brief 耗时/next actions/可用技能)、知识(总量/7日增长/增长率)、研究(来源/证据/断言/验证率/证据密度)、技能(总数/命中/均命中)、科技树(棵数/节点/解锁率)、活动(7 日 events)
5
+ - **来源**:R11 冷启动量化研究的手动测量 → 自动化——从 `time agent brief` + 手动 grep 统计变为一键输出
6
+ - 135 单测全绿
7
+
3
8
  ## 0.69.0(2026-10-17)MCP Server——R8 研究设计的工程落地
4
9
  - **`ygl-trees mcp`**:启动 stdio MCP 服务器(JSON-RPC 2.0),Claude Desktop / Cursor / VS Code 等 IDE Agent 可直接连接
5
10
  - **8 工具面**(三支柱理论驱动):agent_brief · agent_match · kb_search · claim_evaluate · tree_state · produce_report · doctor · task_advance
@@ -5351,6 +5351,72 @@ function buildBrief(env) {
5351
5351
  function run$9(env, action, rest, args) {
5352
5352
  const { ctx, store } = env;
5353
5353
  switch (action) {
5354
+ case "metrics": {
5355
+ const t0 = Date.now();
5356
+ const brief = buildBrief(env);
5357
+ const briefMs = Date.now() - t0;
5358
+ const kbTotal = env.db.get("SELECT COUNT(*) AS n FROM kb_notes")?.n || 0;
5359
+ const kbRecent = env.db.get("SELECT COUNT(*) AS n FROM kb_notes WHERE created > datetime('now', '-7 days')")?.n || 0;
5360
+ const claims = env.db.get("SELECT COUNT(*) AS n FROM claims")?.n || 0;
5361
+ const claimsVerified = env.db.get("SELECT COUNT(*) AS n FROM claims WHERE status='supported'")?.n || 0;
5362
+ const evidence = env.db.get("SELECT COUNT(*) AS n FROM evidence")?.n || 0;
5363
+ const sources = env.db.get("SELECT COUNT(*) AS n FROM sources")?.n || 0;
5364
+ const skills = store.skillList();
5365
+ const skillHits = skills.reduce((s, x) => s + (x.hits || 0), 0);
5366
+ const trees = env.db.all("SELECT id, name FROM trees");
5367
+ let nodesTotal = 0, nodesUnlocked = 0;
5368
+ for (const t of trees) {
5369
+ const r = env.db.get("SELECT COUNT(*) AS total, SUM(CASE WHEN ns.status='unlocked' THEN 1 ELSE 0 END) AS unlocked FROM nodes n LEFT JOIN node_state ns ON ns.node_id=n.id AND ns.context='default' WHERE n.tree_id=?", [t.id]);
5370
+ nodesTotal += r?.total || 0;
5371
+ nodesUnlocked += r?.unlocked || 0;
5372
+ }
5373
+ const events7d = env.db.get("SELECT COUNT(*) AS n FROM events WHERE ts > datetime('now', '-7 days')")?.n || 0;
5374
+ const metrics = {
5375
+ cold_start: {
5376
+ brief_ms: briefMs,
5377
+ next_actions: brief.next_actions?.length || 0,
5378
+ skills_available: skills.length
5379
+ },
5380
+ knowledge: {
5381
+ total_notes: kbTotal,
5382
+ recent_7d: kbRecent,
5383
+ growth_rate_7d: kbTotal > 0 ? Math.round(kbRecent / kbTotal * 100) : 0
5384
+ },
5385
+ research: {
5386
+ sources,
5387
+ evidence,
5388
+ claims,
5389
+ verified_claims: claimsVerified,
5390
+ verification_rate: claims > 0 ? Math.round(claimsVerified / claims * 100) : 0,
5391
+ evidence_per_claim: claims > 0 ? (evidence / claims).toFixed(1) : "0"
5392
+ },
5393
+ skills: {
5394
+ total: skills.length,
5395
+ total_hits: skillHits,
5396
+ avg_hits: skills.length > 0 ? (skillHits / skills.length).toFixed(1) : "0"
5397
+ },
5398
+ trees: {
5399
+ total_trees: trees.length,
5400
+ nodes: nodesTotal,
5401
+ unlocked: nodesUnlocked,
5402
+ unlock_rate: nodesTotal > 0 ? Math.round(nodesUnlocked / nodesTotal * 100) : 0
5403
+ },
5404
+ activity: { events_7d: events7d },
5405
+ doctor: "run `doctor` for health checks"
5406
+ };
5407
+ if (ctx.json) {
5408
+ console.log(JSON.stringify(metrics, null, 2));
5409
+ return 0;
5410
+ }
5411
+ head(ctx, "ygl-trees metrics");
5412
+ out(ctx, ` 冷启动 brief ${metrics.cold_start.brief_ms}ms · ${metrics.cold_start.next_actions} next actions · ${metrics.cold_start.skills_available} skills`);
5413
+ out(ctx, ` 知识 ${metrics.knowledge.total_notes} notes (${metrics.knowledge.recent_7d} in 7d, ${metrics.knowledge.growth_rate_7d}% growth)`);
5414
+ out(ctx, ` 研究 ${metrics.research.sources} sources · ${metrics.research.evidence} evidence · ${metrics.research.verified_claims}/${metrics.research.claims} verified (${metrics.research.verification_rate}%)`);
5415
+ out(ctx, ` 技能 ${metrics.skills.total} skills · ${metrics.skills.total_hits} hits (avg ${metrics.skills.avg_hits})`);
5416
+ out(ctx, ` 科技树 ${metrics.trees.total_trees} trees · ${metrics.trees.unlocked}/${metrics.trees.nodes} unlocked (${metrics.trees.unlock_rate}%)`);
5417
+ out(ctx, ` 活动 ${metrics.activity.events_7d} events in 7d`);
5418
+ return 0;
5419
+ }
5354
5420
  case "brief": {
5355
5421
  const payload = buildBrief(env);
5356
5422
  const cur = payload.current_project;
@@ -9090,6 +9156,61 @@ function gatherData(env) {
9090
9156
  }));
9091
9157
  }
9092
9158
  }
9159
+ const kgEdges = [];
9160
+ const kgMap = /* @__PURE__ */ new Map();
9161
+ try {
9162
+ const rels = db.all("SELECT src_type, src_id, dst_type, dst_id, rel FROM relations LIMIT 200");
9163
+ const colors = {
9164
+ node: "#7c9885",
9165
+ claim: "#6a994e",
9166
+ kb: "#8b7355",
9167
+ evidence: "#daa520",
9168
+ artifact: "#5b8db8",
9169
+ skill: "#9b7cb8"
9170
+ };
9171
+ for (const r of rels) {
9172
+ for (const side of ["src", "dst"]) {
9173
+ const type = r[`${side}_type`];
9174
+ const id = r[`${side}_id`];
9175
+ const key = `${type}:${id}`;
9176
+ if (kgMap.has(key)) continue;
9177
+ let label = id;
9178
+ try {
9179
+ if (type === "node") {
9180
+ const n = db.get("SELECT name FROM nodes WHERE id=?", [id]);
9181
+ if (n) label = n.name;
9182
+ } else if (type === "claim") {
9183
+ const c = db.get("SELECT substr(text,1,40) AS t FROM claims WHERE id=?", [id]);
9184
+ if (c) label = c.t;
9185
+ } else if (type === "kb") {
9186
+ const k = db.get("SELECT title FROM kb_notes WHERE id=?", [id]);
9187
+ if (k) label = k.title.slice(0, 40);
9188
+ } else if (type === "evidence") {
9189
+ const e = db.get("SELECT substr(quote,1,40) AS q FROM evidence WHERE id=?", [id]);
9190
+ if (e) label = e.q;
9191
+ } else if (type === "artifact") {
9192
+ const a = db.get("SELECT title FROM artifacts WHERE id=?", [id]);
9193
+ if (a) label = a.title;
9194
+ }
9195
+ } catch {}
9196
+ kgMap.set(key, {
9197
+ id: key,
9198
+ label,
9199
+ type,
9200
+ color: colors[type] || "#999"
9201
+ });
9202
+ }
9203
+ kgEdges.push({
9204
+ source: `${r.src_type}:${r.src_id}`,
9205
+ target: `${r.dst_type}:${r.dst_id}`,
9206
+ label: r.rel
9207
+ });
9208
+ }
9209
+ } catch {}
9210
+ data.knowledge_graph = {
9211
+ nodes: Array.from(kgMap.values()),
9212
+ edges: kgEdges
9213
+ };
9093
9214
  return data;
9094
9215
  }
9095
9216
  /** Single-file dashboard HTML. `live` adds a fetch bootstrap + auto-refresh. */
@@ -74,6 +74,78 @@ function buildBrief(env) {
74
74
  function run(env, action, rest, args) {
75
75
  const { ctx, store } = env;
76
76
  switch (action) {
77
+ case "metrics": {
78
+ // R11 cold-start research → automated measurement. Health/efficiency
79
+ // metrics for the workspace — one command answers "how good is this
80
+ // workbench?" with quantified data.
81
+ const t0 = Date.now();
82
+ const brief = buildBrief(env);
83
+ const briefMs = Date.now() - t0;
84
+ // Knowledge metrics
85
+ const kbTotal = env.db.get("SELECT COUNT(*) AS n FROM kb_notes")?.n || 0;
86
+ const kbRecent = env.db.get("SELECT COUNT(*) AS n FROM kb_notes WHERE created > datetime('now', '-7 days')")?.n || 0;
87
+ // Research metrics
88
+ const claims = env.db.get("SELECT COUNT(*) AS n FROM claims")?.n || 0;
89
+ const claimsVerified = env.db.get("SELECT COUNT(*) AS n FROM claims WHERE status='supported'")?.n || 0;
90
+ const evidence = env.db.get("SELECT COUNT(*) AS n FROM evidence")?.n || 0;
91
+ const sources = env.db.get("SELECT COUNT(*) AS n FROM sources")?.n || 0;
92
+ // Skill metrics
93
+ const skills = store.skillList();
94
+ const skillHits = skills.reduce((s, x) => s + (x.hits || 0), 0);
95
+ // Tree metrics
96
+ const trees = env.db.all("SELECT id, name FROM trees");
97
+ let nodesTotal = 0, nodesUnlocked = 0;
98
+ for (const t of trees) {
99
+ const r = env.db.get("SELECT COUNT(*) AS total, SUM(CASE WHEN ns.status='unlocked' THEN 1 ELSE 0 END) AS unlocked FROM nodes n LEFT JOIN node_state ns ON ns.node_id=n.id AND ns.context='default' WHERE n.tree_id=?", [t.id]);
100
+ nodesTotal += r?.total || 0;
101
+ nodesUnlocked += r?.unlocked || 0;
102
+ }
103
+ // Event metrics
104
+ const events7d = env.db.get("SELECT COUNT(*) AS n FROM events WHERE ts > datetime('now', '-7 days')")?.n || 0;
105
+ const metrics = {
106
+ cold_start: {
107
+ brief_ms: briefMs,
108
+ next_actions: brief.next_actions?.length || 0,
109
+ skills_available: skills.length,
110
+ },
111
+ knowledge: {
112
+ total_notes: kbTotal,
113
+ recent_7d: kbRecent,
114
+ growth_rate_7d: kbTotal > 0 ? Math.round(kbRecent / kbTotal * 100) : 0,
115
+ },
116
+ research: {
117
+ sources, evidence, claims,
118
+ verified_claims: claimsVerified,
119
+ verification_rate: claims > 0 ? Math.round(claimsVerified / claims * 100) : 0,
120
+ evidence_per_claim: claims > 0 ? (evidence / claims).toFixed(1) : "0",
121
+ },
122
+ skills: {
123
+ total: skills.length,
124
+ total_hits: skillHits,
125
+ avg_hits: skills.length > 0 ? (skillHits / skills.length).toFixed(1) : "0",
126
+ },
127
+ trees: {
128
+ total_trees: trees.length,
129
+ nodes: nodesTotal,
130
+ unlocked: nodesUnlocked,
131
+ unlock_rate: nodesTotal > 0 ? Math.round(nodesUnlocked / nodesTotal * 100) : 0,
132
+ },
133
+ activity: { events_7d: events7d },
134
+ doctor: "run `doctor` for health checks",
135
+ };
136
+ if (ctx.json) {
137
+ console.log(JSON.stringify(metrics, null, 2));
138
+ return 0;
139
+ }
140
+ (0, fmt_1.head)(ctx, "ygl-trees metrics");
141
+ (0, fmt_1.out)(ctx, ` 冷启动 brief ${metrics.cold_start.brief_ms}ms · ${metrics.cold_start.next_actions} next actions · ${metrics.cold_start.skills_available} skills`);
142
+ (0, fmt_1.out)(ctx, ` 知识 ${metrics.knowledge.total_notes} notes (${metrics.knowledge.recent_7d} in 7d, ${metrics.knowledge.growth_rate_7d}% growth)`);
143
+ (0, fmt_1.out)(ctx, ` 研究 ${metrics.research.sources} sources · ${metrics.research.evidence} evidence · ${metrics.research.verified_claims}/${metrics.research.claims} verified (${metrics.research.verification_rate}%)`);
144
+ (0, fmt_1.out)(ctx, ` 技能 ${metrics.skills.total} skills · ${metrics.skills.total_hits} hits (avg ${metrics.skills.avg_hits})`);
145
+ (0, fmt_1.out)(ctx, ` 科技树 ${metrics.trees.total_trees} trees · ${metrics.trees.unlocked}/${metrics.trees.nodes} unlocked (${metrics.trees.unlock_rate}%)`);
146
+ (0, fmt_1.out)(ctx, ` 活动 ${metrics.activity.events_7d} events in 7d`);
147
+ return 0;
148
+ }
77
149
  case "brief": {
78
150
  const payload = buildBrief(env);
79
151
  const cur = payload.current_project;
@@ -145,6 +145,59 @@ function gatherData(env) {
145
145
  .map((r) => ({ id: r.id, kind: r.kind, title: r.title, version: r.version, created: r.created }));
146
146
  }
147
147
  }
148
+ // R19: knowledge graph — entities and relations from the rel fabric
149
+ const kgNodes = [];
150
+ const kgEdges = [];
151
+ const kgMap = new Map();
152
+ try {
153
+ const rels = db.all("SELECT src_type, src_id, dst_type, dst_id, rel FROM relations LIMIT 200");
154
+ const colors = {
155
+ node: "#7c9885", claim: "#6a994e", kb: "#8b7355",
156
+ evidence: "#daa520", artifact: "#5b8db8", skill: "#9b7cb8",
157
+ };
158
+ for (const r of rels) {
159
+ for (const side of ["src", "dst"]) {
160
+ const type = r[`${side}_type`];
161
+ const id = r[`${side}_id`];
162
+ const key = `${type}:${id}`;
163
+ if (kgMap.has(key))
164
+ continue;
165
+ let label = id;
166
+ try {
167
+ if (type === "node") {
168
+ const n = db.get("SELECT name FROM nodes WHERE id=?", [id]);
169
+ if (n)
170
+ label = n.name;
171
+ }
172
+ else if (type === "claim") {
173
+ const c = db.get("SELECT substr(text,1,40) AS t FROM claims WHERE id=?", [id]);
174
+ if (c)
175
+ label = c.t;
176
+ }
177
+ else if (type === "kb") {
178
+ const k = db.get("SELECT title FROM kb_notes WHERE id=?", [id]);
179
+ if (k)
180
+ label = k.title.slice(0, 40);
181
+ }
182
+ else if (type === "evidence") {
183
+ const e = db.get("SELECT substr(quote,1,40) AS q FROM evidence WHERE id=?", [id]);
184
+ if (e)
185
+ label = e.q;
186
+ }
187
+ else if (type === "artifact") {
188
+ const a = db.get("SELECT title FROM artifacts WHERE id=?", [id]);
189
+ if (a)
190
+ label = a.title;
191
+ }
192
+ }
193
+ catch { /* best-effort */ }
194
+ kgMap.set(key, { id: key, label, type, color: colors[type] || "#999" });
195
+ }
196
+ kgEdges.push({ source: `${r.src_type}:${r.src_id}`, target: `${r.dst_type}:${r.dst_id}`, label: r.rel });
197
+ }
198
+ }
199
+ catch { /* graph data best-effort */ }
200
+ data.knowledge_graph = { nodes: Array.from(kgMap.values()), edges: kgEdges };
148
201
  return data;
149
202
  }
150
203
  /** Single-file dashboard HTML. `live` adds a fetch bootstrap + auto-refresh. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ygl-trees",
3
- "version": "0.69.0",
3
+ "version": "0.71.0",
4
4
  "description": "猿光 · 科技树 — database-centric CLI for research → production → knowledge reuse, built for AI agents.",
5
5
  "type": "commonjs",
6
6
  "main": "dist/apps/cli/index.js",
@@ -25,8 +25,8 @@
25
25
  "reflect-metadata": "^0.2.2",
26
26
  "typeorm": "^1.1.2",
27
27
  "@deepseek-ai/cordis": "~4.0.5-alpha.2",
28
- "@deepseek-ai/cordis-plugin-timer": "~1.1.7-alpha.2",
29
28
  "@deepseek-ai/cosmokit": "~1.8.6-alpha.2",
29
+ "@deepseek-ai/cordis-plugin-timer": "~1.1.7-alpha.2",
30
30
  "@deepseek-ai/schemastery": "~3.18.5-alpha.2"
31
31
  },
32
32
  "devDependencies": {