ygl-trees 0.79.1 → 0.80.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.80.0(2026-10-17)rel 织物并入 eval 上下文 + 会话内裁决载体
4
+ - **eval 上下文包并入 rel 挂接摘要**(用户实测发现:裁决已 `decided-by` 挂链,P 仍 0.30,必须 `--state k=v` 手动携带事实才 0.92):`evalContext` 新增 `attachments`——节点挂接的 relations 双向查询(≤12 条),claim 摘录 text+status+confidence、evidence 摘录 quote(截 120 字)、kb/source 标题、node key+name、其余 id-only;无挂接时字段缺省(与旧形状逐字节兼容)
5
+ - **`research source add --adjudication "<裁决摘要>"`**:会话内裁决原生载体——url 自动 `session://adjudication/<yyyymmddhhmm>`,摘要兼作缺省标题;`--url` 校验放宽收 `session://`;裸 `--adjudication` 旗标缺值 rc=2
6
+ - 测试:`packages/techtree/model/tests/evalctx.spec.ts` 5 例(无挂接兼容 / decided-by 摘要 / 反向 evidenced-by / 截断与悬空退化 / 12 条上限);`scripts/readme-verify.sh` +1(--adjudication rc=0)
7
+
3
8
  ## 0.79.1(2026-10-17)锁身份 + serve --help 短路 + 批量判定 + 回测状态桥
4
9
  - **异常①锁归属**:`.ygllock` 记录持有者命令行;撞锁报错、`db unlock`(含 `holder_cmd` JSON 字段)、`doctor` 三处回显真实持有者身份——CLI 撞 CLI 不再被误标为 serve(serve 从不取写锁,报错明示这一点)
5
10
  - **异常②**:`webui serve --help` 短路打印用法即退出,不再直接启动服务(随机端口误触发实证)
package/README.md CHANGED
@@ -103,7 +103,9 @@ ygl-trees node import --file tree.json --tree copy # 严格校验、事务导
103
103
 
104
104
  **批量判定 + `--state-file`(0.79.1)**:`node can k1 k2 k3` 一次进程判定多节点(多键自动进批模式,`--json` 返回数组);`--state-file <json>` 喂状态袋,两种形状——**扁平计数** `{"y_imported": 40}` 全节点共享,或**按节点** `{"a": {"y_imported": 40}, "b": {...}}` 各取各袋。探针脚本从此每树起一次进程而非每节点。
105
105
 
106
- **决策模型评估门(spec R8;0.14 布尔 → 0.26 三题型 → 0.27 混批)**:语义型必检条件用 eval 题型声明,`node can <key> --eval` 会把**节点元数据 + 直接父(关联)+ 祖先链(闭包索引)+ 条件组 + 已解锁集 + --state 袋**组装为上下文包,交给评估模型(TypeSafe jev,经 Vercel AI Gateway)判定:
106
+ **决策模型评估门(spec R8;0.14 布尔 → 0.26 三题型 → 0.27 混批 → 0.80 织物并入)**:语义型必检条件用 eval 题型声明,`node can <key> --eval` 会把**节点元数据 + 直接父(关联)+ 祖先链(闭包索引)+ 条件组 + 已解锁集 + --state 袋 + rel 挂接摘要**组装为上下文包,交给评估模型(TypeSafe jev,经 Vercel AI Gateway)判定:
107
+
108
+ - **rel 织物并入(0.80)**:节点挂接的 evidence(quote 摘要)/ claim(text+status+confidence)/ kb/source(标题)等 rel 关系(双向,≤12 条,文本截 120 字)自动进上下文包的 `attachments`——「证据挂链」纪律从此对语义门槛可见,不必再 `--state k=v` 手动携带裁决事实(实测 P 0.30 → 0.92 的差距由挂链补平)
107
109
 
108
110
  - **三题型**:`--op eval`(布尔 P(true))/ `--op eval-choice`(多选一,判 **P(期望答案)**,`--options "k=desc,..."` + `--answer k`)/ `--op eval-score`(等级评分 `--levels "级0,..."` 2-10 级 + `--min-score n`,判概率加权均值 ±1 灰带)
109
111
  - P≥0.7 通过 · P≤0.4 未通过 · **灰带 pending**(过程态,禁作终态,单列不阻塞也不放行);score 的灰带是 [min−1, min)
@@ -124,6 +126,7 @@ ygl-trees node import --file tree.json --tree copy # 严格校验、事务导
124
126
  query add 登记检索式
125
127
  query attach 检索式 ↔ 来源关联(query list 显示召回数)
126
128
  source add 收来源,按 (project, url_hash) 去重;--query 顺带关联
129
+ --adjudication "<裁决摘要>":会话内裁决原生载体(url 自动 session://adjudication/<时刻>,摘要兼作标题);--url 也收 session://
127
130
  source fetch <url> 直接抓取网页入库:title/meta description/去标签正文;15s 超时、非 html 拒收、失败零副作用,正文过薄自动打 needs_review
128
131
  evidence add 从来源摘原文片段,带 locator(页码/章节);长摘录用 --file <path> 免转义
129
132
  claim add 写下断言
@@ -2313,6 +2313,12 @@ var TreeStore = class {
2313
2313
  * + unlocked set + the caller's state bag. The eval API demands a context
2314
2314
  * package — bare state fell into the gray band 6/8 in the material's
2315
2315
  * routing test, so we ship everything the tree knows about this node.
2316
+ *
2317
+ * 0.80: the rel fabric joins the package — evidence/claim/kb summaries
2318
+ * attached to the node (either direction) land in `attachments`, so a
2319
+ * decided-by chain is visible to the eval model without manually
2320
+ * re-carrying facts via --state (user finding: P 0.30 → 0.92 gap).
2321
+ * Bounded: ≤12 rows, text clipped to 120 chars.
2316
2322
  */
2317
2323
  evalContext(treeId, keyOrId, stateBag, context = "default") {
2318
2324
  const n = this.nodeGet(treeId, keyOrId);
@@ -2336,7 +2342,7 @@ var TreeStore = class {
2336
2342
  effect: c.effect
2337
2343
  }));
2338
2344
  const unlocked = [...this.unlockedSet(treeId, context)].map((id) => idToKey.get(id) || id);
2339
- return {
2345
+ const pkg = {
2340
2346
  node: {
2341
2347
  key: n.key,
2342
2348
  name: n.name,
@@ -2350,6 +2356,62 @@ var TreeStore = class {
2350
2356
  unlocked,
2351
2357
  state: stateBag
2352
2358
  };
2359
+ const attachments = this.relAttachments(String(n.id));
2360
+ if (attachments.length) pkg.attachments = attachments;
2361
+ return pkg;
2362
+ }
2363
+ /** 0.80: rel-fabric summaries for the eval context package (both directions). */
2364
+ relAttachments(nodeId) {
2365
+ const rows = this.db.all(`SELECT rel, src_type, src_id, dst_type, dst_id FROM relations
2366
+ WHERE (src_type='node' AND src_id=?) OR (dst_type='node' AND dst_id=?)
2367
+ ORDER BY rowid LIMIT 12`, [nodeId, nodeId]);
2368
+ const clip = (v, n = 120) => String(v ?? "").slice(0, n);
2369
+ return rows.map((r) => {
2370
+ const srcIsMe = r.src_type === "node" && r.src_id === nodeId;
2371
+ const type = String(srcIsMe ? r.dst_type : r.src_type);
2372
+ const id = String(srcIsMe ? r.dst_id : r.src_id);
2373
+ const base = {
2374
+ rel: String(r.rel),
2375
+ type,
2376
+ id
2377
+ };
2378
+ const q = (sql) => this.db.get(sql, [id]);
2379
+ if (type === "claim") {
2380
+ const c = q("SELECT text, status, confidence FROM claims WHERE id=?");
2381
+ if (c) return {
2382
+ ...base,
2383
+ text: clip(c.text),
2384
+ status: c.status,
2385
+ confidence: c.confidence
2386
+ };
2387
+ } else if (type === "evidence") {
2388
+ const e = q("SELECT quote FROM evidence WHERE id=?");
2389
+ if (e) return {
2390
+ ...base,
2391
+ quote: clip(e.quote)
2392
+ };
2393
+ } else if (type === "kb") {
2394
+ const k = q("SELECT title FROM kb_notes WHERE id=?");
2395
+ if (k) return {
2396
+ ...base,
2397
+ title: clip(k.title, 80)
2398
+ };
2399
+ } else if (type === "source") {
2400
+ const s = q("SELECT title FROM sources WHERE id=?");
2401
+ if (s) return {
2402
+ ...base,
2403
+ title: clip(s.title, 80)
2404
+ };
2405
+ } else if (type === "node") {
2406
+ const nd = q("SELECT key, name FROM nodes WHERE id=?");
2407
+ if (nd) return {
2408
+ ...base,
2409
+ key: nd.key,
2410
+ name: clip(nd.name, 80)
2411
+ };
2412
+ }
2413
+ return base;
2414
+ });
2353
2415
  }
2354
2416
  /** Every node whose required gates are currently satisfied. */
2355
2417
  ready(treeId, context = "default", state = {}) {
@@ -6492,6 +6554,10 @@ init_writes();
6492
6554
  init_entities$1();
6493
6555
  /** Run a write through the TypeORM transactional write service and print
6494
6556
  * with JSON discipline. Keeps every write command 6 lines. */
6557
+ /** 0.80: compact timestamp for session:// adjudication urls (yyyymmddhhmm). */
6558
+ function nowCompact() {
6559
+ return (/* @__PURE__ */ new Date()).toISOString().replace(/[-:T]/g, "").slice(0, 12);
6560
+ }
6495
6561
  function runWrite(env, fn, human) {
6496
6562
  const { ctx } = env;
6497
6563
  return getOrm(env.db.path).then(fn).then((r) => {
@@ -6616,14 +6682,24 @@ function run$8(env, action, rest, args) {
6616
6682
  bad(ctx, "flag needs a value: --title \"...\" / --url \"...\" (bare flag was treated as boolean)");
6617
6683
  return 2;
6618
6684
  }
6619
- const title = String(titleRaw || "");
6620
- const url = String(urlRaw || "");
6685
+ let title = String(titleRaw || "");
6686
+ let url = String(urlRaw || "");
6687
+ const adjRaw = args.flags.adjudication;
6688
+ if (adjRaw === true) {
6689
+ bad(ctx, "flag needs a value: --adjudication \"<裁决摘要>\"");
6690
+ return 2;
6691
+ }
6692
+ if (adjRaw !== void 0 && adjRaw !== "") {
6693
+ const adj = String(adjRaw);
6694
+ if (!url) url = `session://adjudication/${nowCompact()}`;
6695
+ if (!title) title = adj.slice(0, 60);
6696
+ }
6621
6697
  if (!title && !url) {
6622
6698
  bad(ctx, "need --title and/or --url");
6623
6699
  return 2;
6624
6700
  }
6625
- if (url && !/^https?:\/\/|^file:\/\//.test(url)) {
6626
- bad(ctx, `--url must be http(s):// or file:// (got: "${require_text.clip(url, 30)}")`);
6701
+ if (url && !/^https?:\/\/|^file:\/\/|^session:\/\//.test(url)) {
6702
+ bad(ctx, `--url must be http(s)://, file:// or session:// (got: "${require_text.clip(url, 30)}")`);
6627
6703
  return 2;
6628
6704
  }
6629
6705
  const spec = {
@@ -48,6 +48,10 @@ const config_1 = require("../../../ops/tools/src/config");
48
48
  const entities_1 = require("./entities");
49
49
  /** Run a write through the TypeORM transactional write service and print
50
50
  * with JSON discipline. Keeps every write command 6 lines. */
51
+ /** 0.80: compact timestamp for session:// adjudication urls (yyyymmddhhmm). */
52
+ function nowCompact() {
53
+ return new Date().toISOString().replace(/[-:T]/g, "").slice(0, 12);
54
+ }
51
55
  function runWrite(env, fn, human) {
52
56
  const { ctx } = env;
53
57
  return (0, orm_1.getOrm)(env.db.path)
@@ -177,14 +181,31 @@ function run(env, action, rest, args) {
177
181
  (0, fmt_1.bad)(ctx, 'flag needs a value: --title "..." / --url "..." (bare flag was treated as boolean)');
178
182
  return 2;
179
183
  }
180
- const title = String(titleRaw || "");
181
- const url = String(urlRaw || "");
184
+ let title = String(titleRaw || "");
185
+ let url = String(urlRaw || "");
186
+ // 0.80: in-session adjudication carrier — the most common human
187
+ // arbitration form had no native --url shape (http(s)/file only),
188
+ // forcing temp-file workarounds. `--adjudication "<裁决摘要>"`
189
+ // fills url with a session:// pseudo-scheme and doubles as the
190
+ // title when --title is absent.
191
+ const adjRaw = args.flags.adjudication;
192
+ if (adjRaw === true) {
193
+ (0, fmt_1.bad)(ctx, 'flag needs a value: --adjudication "<裁决摘要>"');
194
+ return 2;
195
+ }
196
+ if (adjRaw !== undefined && adjRaw !== "") {
197
+ const adj = String(adjRaw);
198
+ if (!url)
199
+ url = `session://adjudication/${nowCompact()}`;
200
+ if (!title)
201
+ title = adj.slice(0, 60);
202
+ }
182
203
  if (!title && !url) {
183
204
  (0, fmt_1.bad)(ctx, "need --title and/or --url");
184
205
  return 2;
185
206
  }
186
- if (url && !/^https?:\/\/|^file:\/\//.test(url)) {
187
- (0, fmt_1.bad)(ctx, `--url must be http(s):// or file:// (got: "${(0, text_1.clip)(url, 30)}")`);
207
+ if (url && !/^https?:\/\/|^file:\/\/|^session:\/\//.test(url)) {
208
+ (0, fmt_1.bad)(ctx, `--url must be http(s)://, file:// or session:// (got: "${(0, text_1.clip)(url, 30)}")`);
188
209
  return 2;
189
210
  }
190
211
  const spec = {
@@ -386,6 +386,12 @@ class TreeStore {
386
386
  * + unlocked set + the caller's state bag. The eval API demands a context
387
387
  * package — bare state fell into the gray band 6/8 in the material's
388
388
  * routing test, so we ship everything the tree knows about this node.
389
+ *
390
+ * 0.80: the rel fabric joins the package — evidence/claim/kb summaries
391
+ * attached to the node (either direction) land in `attachments`, so a
392
+ * decided-by chain is visible to the eval model without manually
393
+ * re-carrying facts via --state (user finding: P 0.30 → 0.92 gap).
394
+ * Bounded: ≤12 rows, text clipped to 120 chars.
389
395
  */
390
396
  evalContext(treeId, keyOrId, stateBag, context = "default") {
391
397
  const n = this.nodeGet(treeId, keyOrId);
@@ -398,7 +404,7 @@ class TreeStore {
398
404
  name: c.name, kind: c.kind, op: c.op, value: c.value, required: !!c.required, effect: c.effect,
399
405
  }));
400
406
  const unlocked = [...this.unlockedSet(treeId, context)].map((id) => idToKey.get(id) || id);
401
- return {
407
+ const pkg = {
402
408
  node: { key: n.key, name: n.name, kind: n.kind, tier: n.tier, cost: n.cost },
403
409
  parents,
404
410
  ancestors,
@@ -406,6 +412,50 @@ class TreeStore {
406
412
  unlocked,
407
413
  state: stateBag,
408
414
  };
415
+ const attachments = this.relAttachments(String(n.id));
416
+ if (attachments.length)
417
+ pkg.attachments = attachments;
418
+ return pkg;
419
+ }
420
+ /** 0.80: rel-fabric summaries for the eval context package (both directions). */
421
+ relAttachments(nodeId) {
422
+ const rows = this.db.all(`SELECT rel, src_type, src_id, dst_type, dst_id FROM relations
423
+ WHERE (src_type='node' AND src_id=?) OR (dst_type='node' AND dst_id=?)
424
+ ORDER BY rowid LIMIT 12`, [nodeId, nodeId]);
425
+ const clip = (v, n = 120) => String(v ?? "").slice(0, n);
426
+ return rows.map((r) => {
427
+ const srcIsMe = r.src_type === "node" && r.src_id === nodeId;
428
+ const type = String(srcIsMe ? r.dst_type : r.src_type);
429
+ const id = String(srcIsMe ? r.dst_id : r.src_id);
430
+ const base = { rel: String(r.rel), type, id };
431
+ const q = (sql) => this.db.get(sql, [id]);
432
+ if (type === "claim") {
433
+ const c = q("SELECT text, status, confidence FROM claims WHERE id=?");
434
+ if (c)
435
+ return { ...base, text: clip(c.text), status: c.status, confidence: c.confidence };
436
+ }
437
+ else if (type === "evidence") {
438
+ const e = q("SELECT quote FROM evidence WHERE id=?");
439
+ if (e)
440
+ return { ...base, quote: clip(e.quote) };
441
+ }
442
+ else if (type === "kb") {
443
+ const k = q("SELECT title FROM kb_notes WHERE id=?");
444
+ if (k)
445
+ return { ...base, title: clip(k.title, 80) };
446
+ }
447
+ else if (type === "source") {
448
+ const s = q("SELECT title FROM sources WHERE id=?");
449
+ if (s)
450
+ return { ...base, title: clip(s.title, 80) };
451
+ }
452
+ else if (type === "node") {
453
+ const nd = q("SELECT key, name FROM nodes WHERE id=?");
454
+ if (nd)
455
+ return { ...base, key: nd.key, name: clip(nd.name, 80) };
456
+ }
457
+ return base; // query/artifact/skill or dangling ref: id only
458
+ });
409
459
  }
410
460
  /** Every node whose required gates are currently satisfied. */
411
461
  ready(treeId, context = "default", state = {}) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ygl-trees",
3
- "version": "0.79.1",
3
+ "version": "0.80.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",
@@ -60,7 +60,12 @@ ygl-trees node can gate-node --eval --json # evaluated[] 带 type 字段 + p
60
60
  ## 4. 上下文包(模型看到什么)
61
61
 
62
62
  节点元数据(key/name/kind/tier/cost)+ 直接父(node_edges)+ 祖先链(node_closure)+
63
- 该节点全部条件 + 已解锁集 + `--state` 袋。**state 必带上下文包**——裸 state 大概率落灰带。
63
+ 该节点全部条件 + 已解锁集 + `--state` 袋 + **rel 挂接摘要(0.80)**。**state 必带上下文包**——裸 state 大概率落灰带。
64
+
65
+ **attachments(0.80 织物并入)**:节点挂接的 rel 关系(双向 ≤12 条)自动摘录入包——
66
+ claim(text+status+confidence)、evidence(quote 截 120 字)、kb/source(标题)、node(key+name)、
67
+ 其余类型 id-only。裁决链 `decided-by` 挂好后 eval 模型直接可见,不必 `--state k=v` 手动携带事实
68
+ (用户实测:不挂链 P=0.30,挂链后 0.92)。
64
69
 
65
70
  ## 5. 凭证与端点(优先级:config > 环境变量/文件 > 默认网关)
66
71
 
@@ -17,6 +17,11 @@ tags: ygl-trees,methodology,relations,fabric
17
17
  **两条平行流水线**:研究管线(source→evidence→claim→report)和科技树各自发育、零缝合——
18
18
  检阅者看不出"这个节点依据什么证据、由什么断言决定、跟踪着什么任务"。`relations` 表就是缝合线。
19
19
 
20
+ **缝合直接喂决策模型(0.80)**:节点挂接的 rel 关系(双向 ≤12 条)自动进 eval 上下文包
21
+ `attachments`——claim 带 text+status+confidence、evidence 带 quote 摘要。裁决 `decided-by`
22
+ 挂链后 `node can --eval` 不必 `--state k=v` 手动携带事实(实测 P 0.30 → 0.92)。
23
+ 挂链纪律由此获得语义门槛层面的回报。
24
+
20
25
  ## 2. rel 词汇表(方向:src --rel→ dst)
21
26
 
22
27
  | rel | 语义 | 典型挂法 |
@@ -26,9 +26,14 @@ ygl-trees research source add --title t --url https://... \
26
26
  --abstract "..." --relevance 4 --query "邻接表 vs 闭包表" # 手工入库+关联检索式
27
27
  ygl-trees research source fetch https://example.com/page # 自动抓取(title/meta/正文;
28
28
  # 非 html 拒收零副作用;薄内容打 needs_review)
29
+ ygl-trees research source add --adjudication "会话内裁决:三条件已逐条核对" \
30
+ --tags adjudication # 会话内裁决原生载体(0.80):
31
+ # url 自动 session://adjudication/<时刻>,
32
+ # 摘要兼作标题——不再需要临时文件绕行
29
33
  ```
30
34
 
31
35
  来源按 `(project, url_hash)` 去重;`--query` 顺带建立召回关联(query list 显示召回数)。
36
+ `--url` 收 http(s):// · file:// · session://(0.80)。
32
37
 
33
38
  ## 2. 摘录与断言
34
39