ygl-trees 0.77.3 → 0.78.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,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.78.0(2026-10-17)预测回测管线
4
+ - **新命令** `research predict audit`:扫描 [预测·N天] 断言——deadline 解析(显式日期优先,created+horizon 推导兜底)、本地可测判据自动实测(树/kb 计数,或语义 any-hit)、npm 版本数 registry 探测(3s 超时,--offline 关闭)
5
+ - 判定四态:hit / miss / pending(含已达标标记)/ manual(外部判据)——只报告不仲裁,给出待 resolve 命令建议
6
+ - 修复 lifecycle 测试并行负载超时抖动(5s→20s 余量)
7
+ - 解锁 pred-val/backtest-pipeline(能力落地即解锁)
8
+
3
9
  ## 0.77.3(2026-10-17)node list 位置参数 UX + 树漂移修正
4
10
  - **UX**:node list <tree> 位置参数现在真的选中该树(此前静默忽略并回退 current_tree——巡检中实际踩中)
5
11
  - 非树名位置参数给显式提示而非静默误导;-t 标志仍优先
package/README.md CHANGED
@@ -125,6 +125,7 @@ evidence add 从来源摘原文片段,带 locator(页码/章节)
125
125
  claim add 写下断言
126
126
  claim link 断言 ↔ 证据,自动算 supported / refuted / contested
127
127
  claim resolve 人工仲裁出口:--as supported|refuted|contested,事件留痕;后续新证据链接会重新推导
128
+ predict audit 预测回测:扫描 [预测·N天] 断言,本地可测判据自动测(树/kb 计数、npm 版本数),判 hit/miss/pending/manual——只报告不仲裁,给出待 resolve 建议
128
129
  ```
129
130
 
130
131
  **JSON 纪律**:写命令在 `--json` 下 stdout 只有一个 JSON 对象(重复时带 `duplicate: true`),Agent 可直接 `JSON.parse` 全量输出。
@@ -6143,6 +6143,125 @@ var init_eval_enhance = require_rolldown_runtime.__esmMin((() => {
6143
6143
  init_eval();
6144
6144
  }));
6145
6145
  //#endregion
6146
+ //#region packages/research/pipeline/src/predict.ts
6147
+ var predict_exports = /* @__PURE__ */ require_rolldown_runtime.__exportAll({
6148
+ auditPredictions: () => auditPredictions,
6149
+ measureCriterion: () => measureCriterion,
6150
+ measureNpmVersions: () => measureNpmVersions,
6151
+ parsePrediction: () => parsePrediction
6152
+ });
6153
+ /** Extract `[预测·X]` horizon + deadline. Explicit `YYYY-MM-DD 前` wins;
6154
+ * otherwise derive from claim creation date + horizon days (7天/90天). */
6155
+ function parsePrediction(text, created) {
6156
+ const h = text.match(/\[预测·([^\]]+)\]/);
6157
+ const horizon = h ? h[1] : "";
6158
+ const d = text.match(/(\d{4}-\d{2}-\d{2})\s*前/);
6159
+ if (d) return {
6160
+ horizon,
6161
+ deadline: d[1]
6162
+ };
6163
+ const days = horizon.match(/(\d+)\s*天/);
6164
+ if (days && created) {
6165
+ const t = Date.parse(created.replace(" ", "T"));
6166
+ if (!Number.isNaN(t)) return {
6167
+ horizon,
6168
+ deadline: new Date(t + +days[1] * 864e5).toISOString().slice(0, 10)
6169
+ };
6170
+ }
6171
+ return {
6172
+ horizon,
6173
+ deadline: ""
6174
+ };
6175
+ }
6176
+ /** Locally measurable criterion matchers. Null → manual. */
6177
+ function measureCriterion(crit, db) {
6178
+ const tree = crit.match(/树\s*≥\s*(\d+)/);
6179
+ const kb = crit.match(/kb\s*≥\s*(\d+)/);
6180
+ if (!tree && !kb) return null;
6181
+ const parts = [];
6182
+ const hits = [];
6183
+ if (tree) {
6184
+ const n = db.get("SELECT COUNT(*) c FROM trees").c;
6185
+ parts.push(`trees=${n}/${tree[1]}`);
6186
+ hits.push(n >= +tree[1]);
6187
+ }
6188
+ if (kb) {
6189
+ const n = db.get("SELECT COUNT(*) c FROM kb_notes").c;
6190
+ parts.push(`kb=${n}/${kb[1]}`);
6191
+ hits.push(n >= +kb[1]);
6192
+ }
6193
+ const hit = crit.includes(" 或 ") ? hits.some(Boolean) : hits.every(Boolean);
6194
+ return {
6195
+ actual: parts.join(" · "),
6196
+ hit
6197
+ };
6198
+ }
6199
+ /** npm version count since a date — registry probe with a hard 3s timeout,
6200
+ * offline-safe fallback to manual. */
6201
+ async function measureNpmVersions(pkg, since, fetchImpl) {
6202
+ try {
6203
+ const ctl = new AbortController();
6204
+ const t = setTimeout(() => ctl.abort(), 3e3);
6205
+ const r = await fetchImpl(`https://registry.npmjs.org/${pkg}`, { signal: ctl.signal });
6206
+ clearTimeout(t);
6207
+ if (!r.ok) return null;
6208
+ const times = (await r.json()).time || {};
6209
+ const versions = Object.entries(times).filter(([v, ts]) => v !== "created" && v !== "modified" && String(ts) >= since).map(([v]) => v);
6210
+ return {
6211
+ actual: `npm versions since ${since}: ${versions.length} (${versions.slice(-3).join(",")})`,
6212
+ hit: versions.length > 0
6213
+ };
6214
+ } catch {
6215
+ return null;
6216
+ }
6217
+ }
6218
+ async function auditPredictions(inp) {
6219
+ const today = inp.today || (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
6220
+ const rows = [];
6221
+ for (const c of inp.claims) {
6222
+ if (!c.text.startsWith("[预测·")) continue;
6223
+ const { horizon, deadline } = parsePrediction(c.text, c.created);
6224
+ const m = c.text.match(/\d{4}-\d{2}-\d{2}\s*前\s*(.+?)(?:——|$)/) || c.text.match(/\]\s*(.+?)(?:——|$)/);
6225
+ const criterion = m ? m[1].trim() : "";
6226
+ const daysLeft = deadline ? Math.round((Date.parse(deadline) - Date.parse(today)) / 864e5) : null;
6227
+ let measure = measureCriterion(criterion, inp.db);
6228
+ if (!measure && /npm 再发\s*≥\s*(\d+)\s*个版本/.test(criterion)) {
6229
+ const need = +criterion.match(/npm 再发\s*≥\s*(\d+)/)[1];
6230
+ const base = c.text.match(/基线\s*\S*?@?(\d{4}-\d{2}-\d{2})/);
6231
+ const since = base ? base[1] : (c.created || today).slice(0, 10);
6232
+ if (inp.npmProbe !== false) {
6233
+ const v = await measureNpmVersions("ygl-trees", since, inp.fetchImpl || fetch);
6234
+ if (v) {
6235
+ const n = +(v.actual.match(/:\s*(\d+)/) || [0, 0])[1];
6236
+ measure = {
6237
+ actual: `${v.actual} · need ≥${need}`,
6238
+ hit: n >= need
6239
+ };
6240
+ }
6241
+ }
6242
+ }
6243
+ let verdict;
6244
+ let hint;
6245
+ if (!measure) {
6246
+ verdict = "manual";
6247
+ hint = "外部世界判据——人工核验后 claim resolve";
6248
+ } else if (daysLeft !== null && daysLeft > 0) verdict = "pending";
6249
+ else verdict = measure.hit ? "hit" : "miss";
6250
+ rows.push({
6251
+ claim_id: c.id,
6252
+ horizon,
6253
+ deadline,
6254
+ criterion,
6255
+ actual: measure ? measure.actual + (verdict === "pending" && measure.hit ? " · 已达标" : "") : "—",
6256
+ verdict,
6257
+ days_left: daysLeft,
6258
+ hint
6259
+ });
6260
+ }
6261
+ return rows;
6262
+ }
6263
+ require_rolldown_runtime.__esmMin((() => {}));
6264
+ //#endregion
6146
6265
  //#region packages/research/pipeline/src/cmds.ts
6147
6266
  /** research group — collect → screen → extract → claim.
6148
6267
  * `source fetch` is async (Node 18+ global fetch) and returns a promise.
@@ -6719,7 +6838,52 @@ function run$8(env, action, rest, args) {
6719
6838
  return 1;
6720
6839
  });
6721
6840
  }
6722
- out(ctx, paint(C.dim, " research: query add|list · source add|list|search|show|rm · evidence add|list · claim add|link|list", ctx.color));
6841
+ if (sub === "predict") {
6842
+ if (subAction === "audit") {
6843
+ const claims = store.db.all("SELECT id, text, status, created FROM claims WHERE project_id=? AND text LIKE '[预测·%' ORDER BY created", [project.id]);
6844
+ if (!claims.length) {
6845
+ if (ctx.json) console.log("[]");
6846
+ else out(ctx, paint(C.dim, " no [预测·…] claims in this project", ctx.color));
6847
+ return 0;
6848
+ }
6849
+ const { auditPredictions } = require_rolldown_runtime.__toCommonJS(predict_exports);
6850
+ return (async () => {
6851
+ const rows = await auditPredictions({
6852
+ claims,
6853
+ db: store.db,
6854
+ npmProbe: args.flags.offline !== true
6855
+ });
6856
+ if (ctx.json) console.log(JSON.stringify(rows, null, 2));
6857
+ else {
6858
+ head(ctx, `prediction audit — ${project.name} (${rows.length} predictions)`);
6859
+ table(ctx, rows, [
6860
+ "claim",
6861
+ "horizon",
6862
+ "deadline",
6863
+ "判据",
6864
+ "实测",
6865
+ "判定",
6866
+ "days"
6867
+ ], [
6868
+ "claim_id",
6869
+ "horizon",
6870
+ "deadline",
6871
+ "criterion",
6872
+ "actual",
6873
+ "verdict",
6874
+ "days_left"
6875
+ ]);
6876
+ const actionable = rows.filter((r) => r.verdict === "hit" || r.verdict === "miss");
6877
+ for (const r of actionable) out(ctx, paint(C.dim, ` → research claim resolve ${r.claim_id} --as ${r.verdict === "hit" ? "supported" : "refuted"} # ${r.deadline} 已到`, ctx.color));
6878
+ for (const r of rows.filter((x) => x.verdict === "manual")) out(ctx, paint(C.dim, ` · ${r.claim_id}: ${r.hint}`, ctx.color));
6879
+ }
6880
+ return 0;
6881
+ })();
6882
+ }
6883
+ bad(ctx, "usage: research predict audit [--offline]");
6884
+ return 2;
6885
+ }
6886
+ out(ctx, paint(C.dim, " research: query add|list · source add|list|search|show|rm · evidence add|list · claim add|link|list · predict audit", ctx.color));
6723
6887
  return 0;
6724
6888
  }
6725
6889
  //#endregion
@@ -599,6 +599,45 @@ function run(env, action, rest, args) {
599
599
  });
600
600
  }
601
601
  }
602
- (0, fmt_1.out)(ctx, (0, fmt_1.paint)(fmt_1.C.dim, " research: query add|list · source add|list|search|show|rm · evidence add|list · claim add|link|list", ctx.color));
602
+ // ---------------------------------------------------------------- predict group
603
+ // pred-val/backtest-pipeline: deadline audit of `[预测·N天]` claims.
604
+ // Measures locally-measurable criteria, reports hit/miss/pending/manual —
605
+ // never auto-resolves (arbitration stays human, frozen contract).
606
+ if (sub === "predict") {
607
+ if (subAction === "audit") {
608
+ const claims = store.db.all("SELECT id, text, status, created FROM claims WHERE project_id=? AND text LIKE '[预测·%' ORDER BY created", [project.id]);
609
+ if (!claims.length) {
610
+ if (ctx.json)
611
+ console.log("[]");
612
+ else
613
+ (0, fmt_1.out)(ctx, (0, fmt_1.paint)(fmt_1.C.dim, " no [预测·…] claims in this project", ctx.color));
614
+ return 0;
615
+ }
616
+ const { auditPredictions } = require("./predict");
617
+ return (async () => {
618
+ const rows = await auditPredictions({ claims, db: store.db, npmProbe: args.flags.offline !== true });
619
+ if (ctx.json) {
620
+ console.log(JSON.stringify(rows, null, 2));
621
+ }
622
+ else {
623
+ (0, fmt_1.head)(ctx, `prediction audit — ${project.name} (${rows.length} predictions)`);
624
+ (0, fmt_1.table)(ctx, rows, ["claim", "horizon", "deadline", "判据", "实测", "判定", "days"], [
625
+ "claim_id", "horizon", "deadline", "criterion", "actual", "verdict", "days_left",
626
+ ]);
627
+ const actionable = rows.filter((r) => r.verdict === "hit" || r.verdict === "miss");
628
+ for (const r of actionable) {
629
+ (0, fmt_1.out)(ctx, (0, fmt_1.paint)(fmt_1.C.dim, ` → research claim resolve ${r.claim_id} --as ${r.verdict === "hit" ? "supported" : "refuted"} # ${r.deadline} 已到`, ctx.color));
630
+ }
631
+ for (const r of rows.filter((x) => x.verdict === "manual")) {
632
+ (0, fmt_1.out)(ctx, (0, fmt_1.paint)(fmt_1.C.dim, ` · ${r.claim_id}: ${r.hint}`, ctx.color));
633
+ }
634
+ }
635
+ return 0;
636
+ })();
637
+ }
638
+ (0, fmt_1.bad)(ctx, "usage: research predict audit [--offline]");
639
+ return 2;
640
+ }
641
+ (0, fmt_1.out)(ctx, (0, fmt_1.paint)(fmt_1.C.dim, " research: query add|list · source add|list|search|show|rm · evidence add|list · claim add|link|list · predict audit", ctx.color));
603
642
  return 0;
604
643
  }
@@ -0,0 +1,120 @@
1
+ "use strict";
2
+ /**
3
+ * Prediction backtest pipeline (spec: pred-val/backtest-pipeline).
4
+ *
5
+ * Predictions are claims whose text starts with `[预测·<horizon>]` and contains
6
+ * a deadline (`YYYY-MM-DD 前`) plus one or more criteria separated by ` 或 `.
7
+ * The audit parses those locally-measurable criteria, measures the current
8
+ * actual from the workspace DB (or a fast registry probe for npm counts), and
9
+ * reports hit / miss / pending / manual — it NEVER auto-resolves; resolution
10
+ * stays a human arbitration (frozen contract).
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.parsePrediction = parsePrediction;
14
+ exports.measureCriterion = measureCriterion;
15
+ exports.measureNpmVersions = measureNpmVersions;
16
+ exports.auditPredictions = auditPredictions;
17
+ /** Extract `[预测·X]` horizon + deadline. Explicit `YYYY-MM-DD 前` wins;
18
+ * otherwise derive from claim creation date + horizon days (7天/90天). */
19
+ function parsePrediction(text, created) {
20
+ const h = text.match(/\[预测·([^\]]+)\]/);
21
+ const horizon = h ? h[1] : "";
22
+ const d = text.match(/(\d{4}-\d{2}-\d{2})\s*前/);
23
+ if (d)
24
+ return { horizon, deadline: d[1] };
25
+ const days = horizon.match(/(\d+)\s*天/);
26
+ if (days && created) {
27
+ const t = Date.parse(created.replace(" ", "T"));
28
+ if (!Number.isNaN(t)) {
29
+ return { horizon, deadline: new Date(t + +days[1] * 86400000).toISOString().slice(0, 10) };
30
+ }
31
+ }
32
+ return { horizon, deadline: "" };
33
+ }
34
+ /** Locally measurable criterion matchers. Null → manual. */
35
+ function measureCriterion(crit, db) {
36
+ // 树≥N / kb≥N — workspace counts (pure local truth)
37
+ const tree = crit.match(/树\s*≥\s*(\d+)/);
38
+ const kb = crit.match(/kb\s*≥\s*(\d+)/);
39
+ if (!tree && !kb)
40
+ return null;
41
+ const parts = [];
42
+ const hits = [];
43
+ if (tree) {
44
+ const n = db.get("SELECT COUNT(*) c FROM trees").c;
45
+ parts.push(`trees=${n}/${tree[1]}`);
46
+ hits.push(n >= +tree[1]);
47
+ }
48
+ if (kb) {
49
+ const n = db.get("SELECT COUNT(*) c FROM kb_notes").c;
50
+ parts.push(`kb=${n}/${kb[1]}`);
51
+ hits.push(n >= +kb[1]);
52
+ }
53
+ const hasOr = crit.includes(" 或 ");
54
+ const hit = hasOr ? hits.some(Boolean) : hits.every(Boolean);
55
+ return { actual: parts.join(" · "), hit };
56
+ }
57
+ /** npm version count since a date — registry probe with a hard 3s timeout,
58
+ * offline-safe fallback to manual. */
59
+ async function measureNpmVersions(pkg, since, fetchImpl) {
60
+ try {
61
+ const ctl = new AbortController();
62
+ const t = setTimeout(() => ctl.abort(), 3000);
63
+ const r = await fetchImpl(`https://registry.npmjs.org/${pkg}`, { signal: ctl.signal });
64
+ clearTimeout(t);
65
+ if (!r.ok)
66
+ return null;
67
+ const meta = await r.json();
68
+ const times = meta.time || {};
69
+ const versions = Object.entries(times)
70
+ .filter(([v, ts]) => v !== "created" && v !== "modified" && String(ts) >= since)
71
+ .map(([v]) => v);
72
+ return { actual: `npm versions since ${since}: ${versions.length} (${versions.slice(-3).join(",")})`, hit: versions.length > 0 };
73
+ }
74
+ catch {
75
+ return null;
76
+ }
77
+ }
78
+ async function auditPredictions(inp) {
79
+ const today = inp.today || new Date().toISOString().slice(0, 10);
80
+ const rows = [];
81
+ for (const c of inp.claims) {
82
+ if (!c.text.startsWith("[预测·"))
83
+ continue;
84
+ const { horizon, deadline } = parsePrediction(c.text, c.created);
85
+ // criterion: prefer the "DATE 前" anchor; fall back to "] " (no-date texts)
86
+ const m = c.text.match(/\d{4}-\d{2}-\d{2}\s*前\s*(.+?)(?:——|$)/) || c.text.match(/\]\s*(.+?)(?:——|$)/);
87
+ const criterion = m ? m[1].trim() : "";
88
+ const daysLeft = deadline ? Math.round((Date.parse(deadline) - Date.parse(today)) / 86400000) : null;
89
+ let measure = measureCriterion(criterion, inp.db);
90
+ // npm version criterion → registry probe (opt-out for offline/tests)
91
+ if (!measure && /npm 再发\s*≥\s*(\d+)\s*个版本/.test(criterion)) {
92
+ const need = +criterion.match(/npm 再发\s*≥\s*(\d+)/)[1];
93
+ const base = c.text.match(/基线\s*\S*?@?(\d{4}-\d{2}-\d{2})/);
94
+ const since = base ? base[1] : (c.created || today).slice(0, 10);
95
+ if (inp.npmProbe !== false) {
96
+ const v = await measureNpmVersions("ygl-trees", since, inp.fetchImpl || fetch);
97
+ if (v) {
98
+ const n = +(v.actual.match(/:\s*(\d+)/) || [0, 0])[1];
99
+ measure = { actual: `${v.actual} · need ≥${need}`, hit: n >= need };
100
+ }
101
+ }
102
+ }
103
+ let verdict;
104
+ let hint;
105
+ if (!measure) {
106
+ verdict = "manual";
107
+ hint = "外部世界判据——人工核验后 claim resolve";
108
+ }
109
+ else if (daysLeft !== null && daysLeft > 0)
110
+ verdict = "pending";
111
+ else
112
+ verdict = measure.hit ? "hit" : "miss";
113
+ rows.push({
114
+ claim_id: c.id, horizon, deadline, criterion,
115
+ actual: measure ? measure.actual + (verdict === "pending" && measure.hit ? " · 已达标" : "") : "—",
116
+ verdict, days_left: daysLeft, hint,
117
+ });
118
+ }
119
+ return rows;
120
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ygl-trees",
3
- "version": "0.77.3",
3
+ "version": "0.78.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",
@@ -26,9 +26,9 @@
26
26
  "reflect-metadata": "^0.2.2",
27
27
  "typeorm": "^1.1.2",
28
28
  "@deepseek-ai/cordis": "~4.0.5-alpha.2",
29
- "@deepseek-ai/cosmokit": "~1.8.6-alpha.2",
30
29
  "@deepseek-ai/cordis-plugin-timer": "~1.1.7-alpha.2",
31
- "@deepseek-ai/schemastery": "~3.18.5-alpha.2"
30
+ "@deepseek-ai/schemastery": "~3.18.5-alpha.2",
31
+ "@deepseek-ai/cosmokit": "~1.8.6-alpha.2"
32
32
  },
33
33
  "devDependencies": {
34
34
  "@types/node": "^20.0.0",