ygl-trees 0.81.0 → 0.82.1

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,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.82.1(2026-10-17)预测生命周期护栏:挂证据 ≠ 仲裁
4
+ - **`claim link` 不再抢跑预测仲裁**(用户上报③:`--stance supports` 自动把 [预测·…] 断言置 supported,predict audit 判 supported||refuted → resolved——预测刚挂证据就退出跟踪表,实际无人仲裁):预测前缀断言挂证据时跳过自动晋升,状态保持 unverified 直到显式 `claim resolve` / 到期回测;证据链接与 stance 照常落库,回执明示 `auto_promote: false`(`--json` 返回 `{claim, evidence, stance, status, auto_promote}`);普通断言的自动推导不变(回归护栏)
5
+ - **`claim unresolve <claimId>`**(建议②:误晋升后无归位命令):复位通道把断言重置回 unverified(撤销链接副作用 / 抢跑仲裁),幂等(已 unverified → `duplicate:true`),落 `claim.unresolve` 事件留痕——0.82.1 前被误晋升的预测可用它归队
6
+ - **predict audit `--json` 覆盖确认**(建议③):`--json` 始终输出全量 rows(含 resolved 终态行),人类视图默认隐藏终态——机器读全量、人看增量,同构无缺口
7
+ - 测试:predlink.spec.ts 5 例(预测不晋升 / 普通照常推导 / unresolve 复位+事件 / 幂等 / 坏 id 点名)→ 177;demo 5j 段 5 断言(挂证据不晋升 / 普通推导回归 / unresolve 复位 / 幂等 / 复位后仍在跟踪表)→ 106
8
+
9
+ ## 0.82.0(2026-10-17)跨树巡检 + 临期 eval 门槛 + 幻影对账一键修复
10
+ - **`node ready --all`**(用户建议②:12 棵树巡检要 24 条命令——每树 tree use + ready):一次进程扫全部树的就绪集,无需 tree use / `-t`;`--json` 返回 `[{tree, ready[]}]`,人类输出按树分节
11
+ - **`node cond --due [--days N] [--eval]`**(建议③:w1/d90 检验依赖人记得看):扫描全部 eval 族条件,提取问题文本中的 `YYYY-MM-DD` 期限,列出 N 天内到期(含逾期)的条件(树/节点/deadline/剩余天数);`--days` 放宽窗口(默认 7);`--eval` 逐条组装上下文包送判定模型(三题型按 op 自动选择,离线安全回落 pending)——**判定只报告不仲裁**,通过 ≠ 自动解锁,resolve/unlock 保持人工(与 predict audit 同纪律);跨树扫描前置于树解析,无当前树也能跑
12
+ - **`doctor --fix` 覆盖 write_integrity**(上报①:幻影写入事件只能围观不能修):`--fix` 把已知幻影事件标记 `reconciled`(payload 增补时间戳,扫描豁免),审计轨迹保留不删事件;warn 文案指路。实测真实工作区:10-16 恢复实验造成的 10 个幻影(kb 47d0d78b18、source f0ad79350f 等)一键对账回 pass
13
+ - 测试:demo 5i 段 7 断言(跨树 ready / 期限提取 / --days 窗口 / --eval 离线 pending / 幻影 warn→fix→pass 全循环)→ 101;`readme-verify.sh` +2(ready --all、cond --due)→ 41
14
+
3
15
  ## 0.81.0(2026-10-17)命令面五处修正 + task 挂树 + 事件流对账
4
16
  - **doctor write_integrity 误报修复**(用户上报①:合法 `kb remove` 后 doctor 报 phantom write fail):`kb rm` 现在落 `kb.rm` 事件(与 source/evidence 对齐);check 10 的幽灵扫描加 `NOT EXISTS rm 事件` 豁免,且整体降级 fail→warn——无 rm 事件的缺行可能是 0.81 前的合法删除或 db sql 手工清理,不值得炸掉脚本门禁
5
17
  - **`--url` 收任意 scheme**(上报②:a2a:// 被拒,跨 Agent 引用被迫落盘):正则放宽为 RFC-3986 `<scheme>://`,https/file/session/a2a/… 均可;url_hash 去重照常兜底
package/README.md CHANGED
@@ -91,6 +91,9 @@ ygl-trees node link a b --rel excludes # 互斥分支
91
91
  ygl-trees node can steel --state "research_lab_level=3,has_engineer=1"
92
92
  ygl-trees node can a b c --state-file probe.json # 批量判定:一进程测 N 节点(探针冷启动 1s×N → 1s)
93
93
  ygl-trees node ready # 当前可解锁集合
94
+ ygl-trees node ready --all # 跨树扫描就绪集(无需逐树 tree use)
95
+ ygl-trees node cond --due # 临期 eval 门槛(问题文本带 YYYY-MM-DD 期限)
96
+ ygl-trees node cond --due --days 30 # 放宽窗口;--eval 到期自动送判定模型
94
97
  ygl-trees node path advanced-alloy # 到目标的最省顺序 + 累计成本
95
98
  ygl-trees node ancestors bronze # 闭包索引,直接父 depth=1
96
99
  ygl-trees node timeline demo # 单树成长时间线(add/link/unlock 倒序)
@@ -104,6 +107,8 @@ ygl-trees node import --file tree.json --tree copy # 严格校验、事务导
104
107
 
105
108
  **批量判定 + `--state-file`(0.79.1)**:`node can k1 k2 k3` 一次进程判定多节点(多键自动进批模式,`--json` 返回数组);`--state-file <json>` 喂状态袋,两种形状——**扁平计数** `{"y_imported": 40}` 全节点共享,或**按节点** `{"a": {"y_imported": 40}, "b": {...}}` 各取各袋。探针脚本从此每树起一次进程而非每节点。
106
109
 
110
+ **跨树巡检 + 临期门槛(0.82)**:`node ready --all` 一次扫全部树(12 棵树 24 条命令 → 1 条);`node cond --due [--days N]` 列出问题文本带 `YYYY-MM-DD` 期限且 N 天内到期(含逾期)的 eval 条件——w1/d90 预测门槛不再依赖人记得看;`--eval` 逐条送判定模型(离线安全回落 pending),**判定只报告不仲裁**(通过 ≠ 自动解锁,resolve/unlock 保持人工,与 predict audit 同纪律)。
111
+
107
112
  **决策模型评估门(spec R8;0.14 布尔 → 0.26 三题型 → 0.27 混批 → 0.80 织物并入)**:语义型必检条件用 eval 题型声明,`node can <key> --eval` 会把**节点元数据 + 直接父(关联)+ 祖先链(闭包索引)+ 条件组 + 已解锁集 + --state 袋 + rel 挂接摘要**组装为上下文包,交给评估模型(TypeSafe jev,经 Vercel AI Gateway)判定:
108
113
 
109
114
  - **rel 织物并入(0.80)**:节点挂接的 evidence(quote 摘要)/ claim(text+status+confidence)/ kb/source(标题)等 rel 关系(双向,≤12 条,文本截 120 字)自动进上下文包的 `attachments`——「证据挂链」纪律从此对语义门槛可见,不必再 `--state k=v` 手动携带裁决事实(实测 P 0.30 → 0.92 的差距由挂链补平)
@@ -133,7 +138,9 @@ evidence add 从来源摘原文片段,带 locator(页码/章节);长
133
138
  claim add 写下断言;--confidence 收 A|B|C 或数值档(0.92 / 85% 原样入库)
134
139
  claim link 断言 ↔ 证据,自动算 supported / refuted / contested
135
140
  --by-source:传来源 id 一次挂齐其全部证据;误传来源 id 会列出该来源的证据 id
141
+ [预测·…] 断言不自动晋升(0.82.1):挂证据≠仲裁,状态保持 unverified 直到显式 resolve / 到期回测
136
142
  claim resolve 人工仲裁出口:--as supported|refuted|contested,事件留痕;后续新证据链接会重新推导
143
+ claim unresolve 复位通道:把断言重置回 unverified(撤销链接副作用 / 抢跑仲裁),幂等 duplicate:true
137
144
  predict audit 预测回测:扫描 [预测·N天] 断言,本地可测判据自动测(树/kb 计数、npm 版本数),判 hit/miss/pending/manual——只报告不仲裁,给出待 resolve 建议
138
145
  --state <file|url>:外部计数桥接(如 DSH API 快照),判据里的裸键名后缀匹配嵌套键(y_imported≥12 ← counts.y_imported)
139
146
  已 claim resolve 的预测默认隐藏(回测终态),--all 查看全表
@@ -289,7 +296,7 @@ ygl-trees agent ask "<question>" natural language interface
289
296
  ## 五b、开发与运维工具
290
297
 
291
298
  ```bash
292
- pnpm test # 四段制回归:unit (vitest 172 例) + build (tsc + tsdown) + demo(94 条断言)+ contract:audit(86 项)
299
+ pnpm test # 四段制回归:unit (vitest 177 例) + build (tsc + tsdown) + demo(106 条断言)+ contract:audit(86 项)
293
300
  pnpm bench # 性能基准七席(startup/写路径/检索/索引),每席含 RSS 内存端点 → tmp/bench-results.json
294
301
  node benchmarks/bench.js --quick # 缩小规模快速跑
295
302
  pnpm pack:check # 发布就绪门禁:打包 → 隔离安装 → 驱动发布产物跑命令面 + --json 纯净性
@@ -313,7 +320,7 @@ serve。serve 从不取写锁(WAL 并发读写安全),`db unlock` 也以
313
320
  **多会话安全(0.79)**:`tree use` / `project use` 全部落 `tree.use` / `project.use` 事件(带
314
321
  `YGL_SESSION` 会话标识);`agent brief` 的 `selection_changed` 字段暴露最近 10 分钟的选择变更,
315
322
  跨会话变更会置顶提示命令带 `-t/-p` 固定上下文。`kb add` 回执自带 `verified` 自检与工作区回显,
316
- `doctor` 第 10 项回放近 2 天 kb/source/evidence 写事件对账基表,幻影写入当场点名。
323
+ `doctor` 第 10 项回放近 2 天 kb/source/evidence 写事件对账基表,幻影写入当场点名。`doctor --fix`(0.82)把已知幻影标记 `reconciled`(payload 留痕、扫描跳过、审计轨迹保留)——db 恢复/导入实验造成的旧幻影一键对账。
317
324
 
318
325
  **提交门禁(无 CI 环境的等效方案)**:`.githooks/pre-push` —— push 分支跑 `pnpm test`,
319
326
  **push tag 追加 `pnpm pack:check`**;旁路只能显式 `YGL_SKIP_HOOKS=1`。安装一次:
@@ -163,6 +163,7 @@ ${(0, fmt_1.paint)(fmt_1.C.bold, "research", true)} collect → screen → ex
163
163
  claim link <claimId> <evidenceId> [--stance supports|refutes]
164
164
  --by-source <claimId> <sourceId>: link ALL evidence under a source
165
165
  claim resolve <claimId> --as supported|refuted|contested [--note]
166
+ claim unresolve <claimId> reset to unverified (undo auto-promote / early arbitration)
166
167
  claim list
167
168
 
168
169
  ${(0, fmt_1.paint)(fmt_1.C.bold, "produce", true)} outline → section → report · spec/design(工程产出)
@@ -210,6 +211,8 @@ ${(0, fmt_1.paint)(fmt_1.C.bold, "tree / node", true)} tech-tree node model
210
211
  (--eval: eval 题型经决策模型判定;
211
212
  boolean P / choice P(期望答案) / score ±1 灰带)
212
213
  node ready [--state k=v] [--eval] currently unlockable set
214
+ (--all: sweep every tree — no tree use needed)
215
+ node cond --due [--days n] [--eval] eval gates with YYYY-MM-DD deadlines due in n days
213
216
  node path <key> dependency-order path to target (all locked ancestors; OR branches not optimized)
214
217
  node parents|children <key> direct relations
215
218
  node ancestors|descendants <key> transitive, via closure index
@@ -662,6 +665,7 @@ function generateLlmsManifest() {
662
665
  ["research claim add \"<text>\"", "refutable assertion"],
663
666
  ["research claim link <cid> <eid> --stance supports|refutes", "link claim to evidence"],
664
667
  ["research claim resolve <cid> --as supported|refuted|contested", "manual arbitration"],
668
+ ["research claim unresolve <cid>", "reset to unverified (undo link auto-promote)"],
665
669
  ["research claim evaluate <cid>", "decision model confidence (0-5 → A/B/C/D)"],
666
670
  ["research evidence assess <eid>", "decision model quality assessment"],
667
671
  ]],
@@ -694,7 +698,8 @@ function generateLlmsManifest() {
694
698
  ["node cond <key> --name <c> --kind <k> [--op eval] [--value \"<question>\"]", "add condition (eval = decision model gate)"],
695
699
  ["node index", "rebuild closure + topo order"],
696
700
  ["node can <key> [--state k=v] [--eval]", "can unlock? (required vs optional + cost)"],
697
- ["node ready", "currently unlockable set"],
701
+ ["node ready [--all]", "currently unlockable set (--all: every tree)"],
702
+ ["node cond --due [--days n] [--eval]", "eval gates with deadlines due in n days"],
698
703
  ["node path <key>", "dependency-order path (OR branches not optimized)"],
699
704
  ["node unlock <key>", "mark unlocked"],
700
705
  ["node list", "list with state column (locked/unlocked)"],
@@ -4088,6 +4088,7 @@ var writes_exports = /* @__PURE__ */ require_rolldown_runtime.__exportAll({
4088
4088
  claimAdd: () => claimAdd,
4089
4089
  claimLink: () => claimLink,
4090
4090
  claimResolve: () => claimResolve,
4091
+ claimUnresolve: () => claimUnresolve,
4091
4092
  evidenceAdd: () => evidenceAdd,
4092
4093
  jobAdd: () => jobAdd,
4093
4094
  kbAdd: () => kbAdd,
@@ -4343,7 +4344,8 @@ function mapClaim(c) {
4343
4344
  }
4344
4345
  async function claimLink(ds, claimId, evidenceId, stance = "supports") {
4345
4346
  return ds.transaction(async (mgr) => {
4346
- if (!(await mgr.query(`SELECT id FROM claims WHERE id=?`, [claimId])).length) throw new Error(`claim not found: ${claimId}`);
4347
+ const c = await mgr.query(`SELECT id, text FROM claims WHERE id=?`, [claimId]);
4348
+ if (!c.length) throw new Error(`claim not found: ${claimId}`);
4347
4349
  if (!(await mgr.query(`SELECT id FROM evidence WHERE id=?`, [evidenceId])).length) throw new Error(`evidence not found: ${evidenceId}`);
4348
4350
  await mgr.query(`INSERT INTO claim_evidence(claim_id,evidence_id,stance) VALUES(?,?,?)
4349
4351
  ON CONFLICT(claim_id,evidence_id) DO UPDATE SET stance=excluded.stance`, [
@@ -4351,6 +4353,15 @@ async function claimLink(ds, claimId, evidenceId, stance = "supports") {
4351
4353
  evidenceId,
4352
4354
  stance
4353
4355
  ]);
4356
+ if (String(c[0]?.text ?? "").startsWith("[预测·")) {
4357
+ const cur = await mgr.query(`SELECT status FROM claims WHERE id=?`, [claimId]);
4358
+ const status = String(cur?.[0]?.status ?? "unverified");
4359
+ await logEvent$1(mgr, "claim.link", "claim", claimId, `${stance} ${evidenceId} (prediction: auto-promote skipped)`);
4360
+ return {
4361
+ status,
4362
+ auto_promote: false
4363
+ };
4364
+ }
4354
4365
  const sup = await mgr.query(`SELECT COUNT(*) AS c FROM claim_evidence WHERE claim_id=? AND stance='supports'`, [claimId]);
4355
4366
  const ref = await mgr.query(`SELECT COUNT(*) AS c FROM claim_evidence WHERE claim_id=? AND stance='refutes'`, [claimId]);
4356
4367
  const s = Number(sup?.[0]?.c ?? 0);
@@ -4358,7 +4369,33 @@ async function claimLink(ds, claimId, evidenceId, stance = "supports") {
4358
4369
  const status = s && r ? "contested" : s ? "supported" : r ? "refuted" : "unverified";
4359
4370
  await mgr.update(Claim, { id: claimId }, { status });
4360
4371
  await logEvent$1(mgr, "claim.link", "claim", claimId, `${stance} ${evidenceId}`);
4361
- return { status };
4372
+ return {
4373
+ status,
4374
+ auto_promote: true
4375
+ };
4376
+ });
4377
+ }
4378
+ /** 0.82.1 (user report ③): reset an auto-promoted / wrongly arbitrated claim
4379
+ * back to unverified — the repair channel for link side effects on
4380
+ * predictions (and for any claim arbitrated too early). Idempotent: already
4381
+ * unverified → duplicate:true, no event spam. */
4382
+ async function claimUnresolve(ds, claimId) {
4383
+ return ds.transaction(async (mgr) => {
4384
+ const c = await mgr.query(`SELECT id, status FROM claims WHERE id=?`, [claimId]);
4385
+ if (!c.length) throw new Error(`claim not found: ${claimId}`);
4386
+ if (String(c[0]?.status) === "unverified") return {
4387
+ status: "unverified",
4388
+ duplicate: true
4389
+ };
4390
+ await mgr.update(Claim, { id: claimId }, {
4391
+ status: "unverified",
4392
+ updated: require_text.nowIso()
4393
+ });
4394
+ await logEvent$1(mgr, "claim.unresolve", "claim", claimId, `${c[0]?.status} → unverified`);
4395
+ return {
4396
+ status: "unverified",
4397
+ duplicate: false
4398
+ };
4362
4399
  });
4363
4400
  }
4364
4401
  async function claimResolve(ds, claimId, status, note = "") {
@@ -7049,7 +7086,15 @@ function run$8(env, action, rest, args) {
7049
7086
  });
7050
7087
  }
7051
7088
  return getOrm(env.db.path).then((ds) => claimLink(ds, rest[1], rest[2], stance)).then((r) => {
7052
- ok(ctx, `linked; claim status now: ${r.status}`);
7089
+ if (ctx.json) console.log(JSON.stringify({
7090
+ claim: rest[1],
7091
+ evidence: rest[2],
7092
+ stance,
7093
+ status: r.status,
7094
+ auto_promote: r.auto_promote
7095
+ }, null, 2));
7096
+ else if (r.auto_promote === false) ok(ctx, `linked; prediction claim stays ${r.status} — auto-promote skipped (arbitration: claim resolve / deadline audit)`);
7097
+ else ok(ctx, `linked; claim status now: ${r.status}`);
7053
7098
  try {
7054
7099
  const { evaluateClaimConfidence } = (init_eval_enhance(), require_rolldown_runtime.__toCommonJS(eval_enhance_exports));
7055
7100
  const evRows = store.db.all(`SELECT e.quote, ce.stance, s.title AS source_title
@@ -7129,6 +7174,30 @@ function run$8(env, action, rest, args) {
7129
7174
  return 1;
7130
7175
  });
7131
7176
  }
7177
+ if (subAction === "unresolve") {
7178
+ const id = rest[1];
7179
+ if (!id) {
7180
+ bad(ctx, "usage: research claim unresolve <claimId>");
7181
+ return 2;
7182
+ }
7183
+ const c0 = store.db.get("SELECT id, status FROM claims WHERE id=? AND project_id=?", [id, project.id]);
7184
+ if (!c0) {
7185
+ bad(ctx, `claim not found: ${id}`);
7186
+ return 1;
7187
+ }
7188
+ return getOrm(env.db.path).then((ds) => claimUnresolve(ds, id)).then((r) => {
7189
+ if (ctx.json) console.log(JSON.stringify({
7190
+ id,
7191
+ status: r.status,
7192
+ duplicate: r.duplicate
7193
+ }, null, 2));
7194
+ else ok(ctx, r.duplicate ? `claim ${id} already unverified (no change)` : `claim ${id}: ${c0.status} → unverified (back in tracking; arbitration: claim resolve)`);
7195
+ return 0;
7196
+ }).catch((e) => {
7197
+ bad(ctx, e?.message || String(e));
7198
+ return 1;
7199
+ });
7200
+ }
7132
7201
  if (subAction === "evaluate") {
7133
7202
  const id = rest[1];
7134
7203
  if (!id) {
@@ -8810,6 +8879,167 @@ function run$4(env, action, rest, args) {
8810
8879
  if (positionalTree) rest = rest.slice(1);
8811
8880
  else if (!ctx.json) out(ctx, paint(C.yellow, ` ! "${rest[0]}" is not a tree — showing current tree (use -t <tree> to switch)`, ctx.color));
8812
8881
  }
8882
+ if (env.group === "node" && action === "cond" && args?.flags?.due === true) {
8883
+ const days = flagNum("days", 7);
8884
+ const ctxName = flagStr("context", "default");
8885
+ const conds = db.all(`SELECT nc.id, nc.name, nc.op, nc.value, n.key node_key, n.id node_id, t.id tree_id, t.name tree
8886
+ FROM node_conditions nc JOIN nodes n ON n.id=nc.node_id JOIN trees t ON t.id=n.tree_id
8887
+ WHERE nc.op LIKE 'eval%' ORDER BY t.name, n.key`);
8888
+ const now = Date.now();
8889
+ const due = [];
8890
+ for (const c of conds) {
8891
+ const spec = parseEvalSpec(c.value);
8892
+ const q = spec.q || String(c.name);
8893
+ const m = q.match(/(20\d{2})-(\d{2})-(\d{2})/);
8894
+ if (!m) continue;
8895
+ const dl = Date.parse(`${m[1]}-${m[2]}-${m[3]}T23:59:59Z`);
8896
+ if (!Number.isFinite(dl) || dl > now + days * 864e5) continue;
8897
+ due.push({
8898
+ tree: c.tree,
8899
+ tree_id: c.tree_id,
8900
+ node: c.node_key,
8901
+ node_id: c.node_id,
8902
+ name: String(c.name),
8903
+ op: String(c.op),
8904
+ question: q,
8905
+ envelope: spec,
8906
+ deadline: `${m[1]}-${m[2]}-${m[3]}`,
8907
+ days_left: Math.ceil((dl - now) / 864e5)
8908
+ });
8909
+ }
8910
+ due.sort((a, b) => a.days_left - b.days_left);
8911
+ if (args?.flags?.eval !== true) {
8912
+ if (ctx.json) console.log(JSON.stringify(due, null, 2));
8913
+ else {
8914
+ head(ctx, `临期 eval 门槛 × ${due.length}(${days} 天窗口)`);
8915
+ if (!due.length) out(ctx, paint(C.dim, " (无临期 eval 条件)", ctx.color));
8916
+ table(ctx, due.map((d) => ({
8917
+ tree: d.tree,
8918
+ node: d.node,
8919
+ cond: d.name,
8920
+ deadline: d.deadline,
8921
+ 剩余天数: d.days_left,
8922
+ question: require_text.clip(d.question, 40)
8923
+ })), [
8924
+ "tree",
8925
+ "node",
8926
+ "cond",
8927
+ "deadline",
8928
+ "剩余天数",
8929
+ "question"
8930
+ ], [
8931
+ "tree",
8932
+ "node",
8933
+ "cond",
8934
+ "deadline",
8935
+ "剩余天数",
8936
+ "question"
8937
+ ]);
8938
+ }
8939
+ return 0;
8940
+ }
8941
+ const offline = !!process.env.YGL_EVAL_OFFLINE || configGetBool(env.db, "eval.offline");
8942
+ const cfgToken = configGet(env.db, "decision_model_llm_token").trim();
8943
+ const token = offline ? "" : cfgToken || evalToken();
8944
+ const endpoint = configGet(env.db, "decision_model_llm_api_base").trim() || "https://ai-gateway.vercel.sh/v4/ai/evaluation-model";
8945
+ const passT = configGetNumber(env.db, "eval.pass_threshold");
8946
+ const failT = configGetNumber(env.db, "eval.fail_threshold");
8947
+ return (async () => {
8948
+ const rows = [];
8949
+ for (const d of due) {
8950
+ const res = await evalMixed(trees.evalContext(d.tree_id, d.node, {}, ctxName), [d.op === "eval-choice" ? {
8951
+ type: "choice",
8952
+ question: d.question,
8953
+ options: d.envelope.options || {},
8954
+ answer: d.envelope.answer || ""
8955
+ } : d.op === "eval-score" ? {
8956
+ type: "score",
8957
+ question: d.question,
8958
+ levels: d.envelope.levels || [],
8959
+ min: d.envelope.min ?? 0
8960
+ } : {
8961
+ type: "boolean",
8962
+ question: d.question
8963
+ }], token, endpoint);
8964
+ if (!res.ok) {
8965
+ rows.push({
8966
+ ...d,
8967
+ outcome: "pending",
8968
+ reason: res.error
8969
+ });
8970
+ continue;
8971
+ }
8972
+ const r = res.results[0];
8973
+ const m = d.op === "eval-choice" ? mapChoiceOutcome(r, d.envelope.answer || "", passT, failT) : d.op === "eval-score" ? mapScoreOutcome(r, d.envelope.min ?? 0) : mapOutcome(r, passT, failT);
8974
+ rows.push({
8975
+ ...d,
8976
+ ...m
8977
+ });
8978
+ }
8979
+ if (ctx.json) console.log(JSON.stringify(rows, null, 2));
8980
+ else {
8981
+ head(ctx, `临期 eval 门槛 — 已判定 × ${rows.length}`);
8982
+ table(ctx, rows.map((d) => ({
8983
+ tree: d.tree,
8984
+ node: d.node,
8985
+ deadline: d.deadline,
8986
+ outcome: d.outcome,
8987
+ p: d.p ?? d.score ?? "",
8988
+ question: require_text.clip(d.question, 36)
8989
+ })), [
8990
+ "tree",
8991
+ "node",
8992
+ "deadline",
8993
+ "判定",
8994
+ "p",
8995
+ "question"
8996
+ ], [
8997
+ "tree",
8998
+ "node",
8999
+ "deadline",
9000
+ "outcome",
9001
+ "p",
9002
+ "question"
9003
+ ]);
9004
+ out(ctx, paint(C.dim, " 判定只报告不仲裁:通过 ≠ 自动解锁(resolve/unlock 保持人工)", ctx.color));
9005
+ }
9006
+ return 0;
9007
+ })();
9008
+ }
9009
+ if (env.group === "node" && action === "ready" && args?.flags?.all === true) {
9010
+ const state = parseState(args);
9011
+ const ctxName = flagStr("context", "default");
9012
+ const all = db.all("SELECT id, name FROM trees ORDER BY created");
9013
+ const per = [];
9014
+ for (const t of all) {
9015
+ const rows = trees.ready(t.id, ctxName, state);
9016
+ if (rows.length) per.push({
9017
+ tree: t.name,
9018
+ ready: rows
9019
+ });
9020
+ }
9021
+ if (ctx.json) {
9022
+ console.log(JSON.stringify(per, null, 2));
9023
+ return 0;
9024
+ }
9025
+ head(ctx, `Ready across ${all.length} trees`);
9026
+ if (!per.length) out(ctx, paint(C.dim, " (no tree has ready nodes)", ctx.color));
9027
+ for (const p of per) {
9028
+ out(ctx, paint(C.bold, ` ${p.tree} (${p.ready.length})`, ctx.color));
9029
+ table(ctx, p.ready, [
9030
+ "key",
9031
+ "name",
9032
+ "tier",
9033
+ "cost"
9034
+ ], [
9035
+ "key",
9036
+ "name",
9037
+ "tier",
9038
+ "cost"
9039
+ ]);
9040
+ }
9041
+ return 0;
9042
+ }
8813
9043
  let tree;
8814
9044
  try {
8815
9045
  tree = positionalTree || trees.treeResolve(sel);
@@ -11589,40 +11819,42 @@ function run(env, action, rest, args) {
11589
11819
  detail: `profile read failed: ${e?.message || e}`
11590
11820
  });
11591
11821
  }
11822
+ const PHANTOM_SPECS = [
11823
+ {
11824
+ kind: "kb.add",
11825
+ table: "kb_notes",
11826
+ label: "kb",
11827
+ rm: "kb.rm"
11828
+ },
11829
+ {
11830
+ kind: "source.add",
11831
+ table: "sources",
11832
+ label: "source",
11833
+ rm: "source.rm"
11834
+ },
11835
+ {
11836
+ kind: "evidence.add",
11837
+ table: "evidence",
11838
+ label: "evidence",
11839
+ rm: "evidence.rm"
11840
+ }
11841
+ ];
11842
+ const phantomSql = (spec) => `SELECT e.entity_id id, e.ts FROM events e
11843
+ WHERE e.kind='${spec.kind}'
11844
+ AND e.ts >= datetime('now', '-2 days')
11845
+ AND e.payload NOT LIKE '%"reconciled"%'
11846
+ AND NOT EXISTS (SELECT 1 FROM ${spec.table} t WHERE t.id = e.entity_id)
11847
+ AND NOT EXISTS (SELECT 1 FROM events r WHERE r.kind='${spec.rm}' AND r.entity_id = e.entity_id)`;
11592
11848
  {
11593
- const PHANTOM_SPECS = [
11594
- {
11595
- kind: "kb.add",
11596
- table: "kb_notes",
11597
- label: "kb",
11598
- rm: "kb.rm"
11599
- },
11600
- {
11601
- kind: "source.add",
11602
- table: "sources",
11603
- label: "source",
11604
- rm: "source.rm"
11605
- },
11606
- {
11607
- kind: "evidence.add",
11608
- table: "evidence",
11609
- label: "evidence",
11610
- rm: "evidence.rm"
11611
- }
11612
- ];
11613
11849
  const phantoms = [];
11614
11850
  for (const spec of PHANTOM_SPECS) {
11615
- const rows = db.all(`SELECT e.entity_id id, e.ts FROM events e
11616
- WHERE e.kind='${spec.kind}'
11617
- AND e.ts >= datetime('now', '-2 days')
11618
- AND NOT EXISTS (SELECT 1 FROM ${spec.table} t WHERE t.id = e.entity_id)
11619
- AND NOT EXISTS (SELECT 1 FROM events r WHERE r.kind='${spec.rm}' AND r.entity_id = e.entity_id)`);
11851
+ const rows = db.all(phantomSql(spec));
11620
11852
  for (const r of rows) phantoms.push(`${spec.label} ${r.id} (${String(r.ts).slice(0, 16)})`);
11621
11853
  }
11622
11854
  checks.push({
11623
11855
  name: "write_integrity",
11624
11856
  status: phantoms.length ? "warn" : "pass",
11625
- detail: phantoms.length ? `${phantoms.length} unexplained write event(s) in last 48h: ${phantoms.slice(0, 3).join(", ")}${phantoms.length > 3 ? " …" : ""} — row missing without an rm event (deleted pre-0.81 or via db sql? otherwise: rollback / different workspace — check receipts' workspace line)` : "recent write events all match stored rows"
11857
+ detail: phantoms.length ? `${phantoms.length} unexplained write event(s) in last 48h: ${phantoms.slice(0, 3).join(", ")}${phantoms.length > 3 ? " …" : ""} — row missing without an rm event (deleted pre-0.81 or via db sql? otherwise: rollback / different workspace — check receipts' workspace line; doctor --fix marks known ones reconciled)` : "recent write events all match stored rows"
11626
11858
  });
11627
11859
  }
11628
11860
  const failed = checks.filter((c) => c.status === "fail").length;
@@ -11665,6 +11897,27 @@ function run(env, action, rest, args) {
11665
11897
  fixes.push("stale lock removed (holder pid dead)");
11666
11898
  } catch {}
11667
11899
  }
11900
+ for (const spec of PHANTOM_SPECS) {
11901
+ const rows = db.all(phantomSql(spec));
11902
+ if (!rows.length) continue;
11903
+ let marked = 0;
11904
+ for (const r of rows) try {
11905
+ const cur = db.get("SELECT payload FROM events WHERE kind=? AND entity_id=? LIMIT 1", [spec.kind, r.id]);
11906
+ let payload = "{}";
11907
+ try {
11908
+ payload = cur?.payload && String(cur.payload).trim().startsWith("{") ? String(cur.payload) : "{}";
11909
+ } catch {}
11910
+ const obj = JSON.parse(payload);
11911
+ obj.reconciled = (/* @__PURE__ */ new Date()).toISOString();
11912
+ db.run("UPDATE events SET payload=? WHERE kind=? AND entity_id=?", [
11913
+ JSON.stringify(obj),
11914
+ spec.kind,
11915
+ r.id
11916
+ ]);
11917
+ marked++;
11918
+ } catch {}
11919
+ if (marked) fixes.push(`write_integrity: ${marked} ${spec.label} phantom event(s) marked reconciled (audit trail kept)`);
11920
+ }
11668
11921
  if (fixes.length) for (const f of fixes) ok(ctx, ` fixed: ${f}`);
11669
11922
  else out(ctx, paint(C.dim, " (--fix: no auto-fixable issues found)", ctx.color));
11670
11923
  }
@@ -11795,6 +12048,7 @@ ${paint(C.bold, "research", true)} collect → screen → extract → claim
11795
12048
  claim link <claimId> <evidenceId> [--stance supports|refutes]
11796
12049
  --by-source <claimId> <sourceId>: link ALL evidence under a source
11797
12050
  claim resolve <claimId> --as supported|refuted|contested [--note]
12051
+ claim unresolve <claimId> reset to unverified (undo auto-promote / early arbitration)
11798
12052
  claim list
11799
12053
 
11800
12054
  ${paint(C.bold, "produce", true)} outline → section → report · spec/design(工程产出)
@@ -11842,6 +12096,8 @@ ${paint(C.bold, "tree / node", true)} tech-tree node model
11842
12096
  (--eval: eval 题型经决策模型判定;
11843
12097
  boolean P / choice P(期望答案) / score ±1 灰带)
11844
12098
  node ready [--state k=v] [--eval] currently unlockable set
12099
+ (--all: sweep every tree — no tree use needed)
12100
+ node cond --due [--days n] [--eval] eval gates with YYYY-MM-DD deadlines due in n days
11845
12101
  node path <key> dependency-order path to target (all locked ancestors; OR branches not optimized)
11846
12102
  node parents|children <key> direct relations
11847
12103
  node ancestors|descendants <key> transitive, via closure index
@@ -12243,6 +12499,7 @@ function generateLlmsManifest() {
12243
12499
  ["research claim add \"<text>\"", "refutable assertion"],
12244
12500
  ["research claim link <cid> <eid> --stance supports|refutes", "link claim to evidence"],
12245
12501
  ["research claim resolve <cid> --as supported|refuted|contested", "manual arbitration"],
12502
+ ["research claim unresolve <cid>", "reset to unverified (undo link auto-promote)"],
12246
12503
  ["research claim evaluate <cid>", "decision model confidence (0-5 → A/B/C/D)"],
12247
12504
  ["research evidence assess <eid>", "decision model quality assessment"]
12248
12505
  ]
@@ -12291,7 +12548,8 @@ function generateLlmsManifest() {
12291
12548
  ["node cond <key> --name <c> --kind <k> [--op eval] [--value \"<question>\"]", "add condition (eval = decision model gate)"],
12292
12549
  ["node index", "rebuild closure + topo order"],
12293
12550
  ["node can <key> [--state k=v] [--eval]", "can unlock? (required vs optional + cost)"],
12294
- ["node ready", "currently unlockable set"],
12551
+ ["node ready [--all]", "currently unlockable set (--all: every tree)"],
12552
+ ["node cond --due [--days n] [--eval]", "eval gates with deadlines due in n days"],
12295
12553
  ["node path <key>", "dependency-order path (OR branches not optimized)"],
12296
12554
  ["node unlock <key>", "mark unlocked"],
12297
12555
  ["node list", "list with state column (locked/unlocked)"],
@@ -235,19 +235,24 @@ function run(env, action, rest, args) {
235
235
  // the row did not, the write path lost data (rollback / crash between
236
236
  // commit and receipt / a different workspace). This check replays recent
237
237
  // write events against the base tables and names the phantom ids.
238
+ // 0.82 ①: `--fix write_integrity` — doctor --fix marks phantom events
239
+ // reconciled (payload gains {"reconciled":<iso>}); the scan skips them.
240
+ // Marking beats deleting: the audit trail stays, the operator said "known".
241
+ const PHANTOM_SPECS = [
242
+ { kind: "kb.add", table: "kb_notes", label: "kb", rm: "kb.rm" },
243
+ { kind: "source.add", table: "sources", label: "source", rm: "source.rm" },
244
+ { kind: "evidence.add", table: "evidence", label: "evidence", rm: "evidence.rm" },
245
+ ];
246
+ const phantomSql = (spec) => `SELECT e.entity_id id, e.ts FROM events e
247
+ WHERE e.kind='${spec.kind}'
248
+ AND e.ts >= datetime('now', '-2 days')
249
+ AND e.payload NOT LIKE '%"reconciled"%'
250
+ AND NOT EXISTS (SELECT 1 FROM ${spec.table} t WHERE t.id = e.entity_id)
251
+ AND NOT EXISTS (SELECT 1 FROM events r WHERE r.kind='${spec.rm}' AND r.entity_id = e.entity_id)`;
238
252
  {
239
- const PHANTOM_SPECS = [
240
- { kind: "kb.add", table: "kb_notes", label: "kb", rm: "kb.rm" },
241
- { kind: "source.add", table: "sources", label: "source", rm: "source.rm" },
242
- { kind: "evidence.add", table: "evidence", label: "evidence", rm: "evidence.rm" },
243
- ];
244
253
  const phantoms = [];
245
254
  for (const spec of PHANTOM_SPECS) {
246
- const rows = db.all(`SELECT e.entity_id id, e.ts FROM events e
247
- WHERE e.kind='${spec.kind}'
248
- AND e.ts >= datetime('now', '-2 days')
249
- AND NOT EXISTS (SELECT 1 FROM ${spec.table} t WHERE t.id = e.entity_id)
250
- AND NOT EXISTS (SELECT 1 FROM events r WHERE r.kind='${spec.rm}' AND r.entity_id = e.entity_id)`);
255
+ const rows = db.all(phantomSql(spec));
251
256
  for (const r of rows)
252
257
  phantoms.push(`${spec.label} ${r.id} (${String(r.ts).slice(0, 16)})`);
253
258
  }
@@ -259,7 +264,7 @@ function run(env, action, rest, args) {
259
264
  name: "write_integrity",
260
265
  status: phantoms.length ? "warn" : "pass",
261
266
  detail: phantoms.length
262
- ? `${phantoms.length} unexplained write event(s) in last 48h: ${phantoms.slice(0, 3).join(", ")}${phantoms.length > 3 ? " …" : ""} — row missing without an rm event (deleted pre-0.81 or via db sql? otherwise: rollback / different workspace — check receipts' workspace line)`
267
+ ? `${phantoms.length} unexplained write event(s) in last 48h: ${phantoms.slice(0, 3).join(", ")}${phantoms.length > 3 ? " …" : ""} — row missing without an rm event (deleted pre-0.81 or via db sql? otherwise: rollback / different workspace — check receipts' workspace line; doctor --fix marks known ones reconciled)`
263
268
  : "recent write events all match stored rows",
264
269
  });
265
270
  }
@@ -321,6 +326,33 @@ function run(env, action, rest, args) {
321
326
  catch { /* */ }
322
327
  }
323
328
  }
329
+ // 0.82 ①: phantom write events → mark reconciled. The operator looked at
330
+ // the warn, decided the rows are gone for a known reason (db restore /
331
+ // import experiment / pre-0.81 delete), and wants the warn gone. We
332
+ // stamp payload.reconciled — the event stays for audit, the scan skips it.
333
+ for (const spec of PHANTOM_SPECS) {
334
+ const rows = db.all(phantomSql(spec));
335
+ if (!rows.length)
336
+ continue;
337
+ let marked = 0;
338
+ for (const r of rows) {
339
+ try {
340
+ const cur = db.get("SELECT payload FROM events WHERE kind=? AND entity_id=? LIMIT 1", [spec.kind, r.id]);
341
+ let payload = "{}";
342
+ try {
343
+ payload = cur?.payload && String(cur.payload).trim().startsWith("{") ? String(cur.payload) : "{}";
344
+ }
345
+ catch { /* */ }
346
+ const obj = JSON.parse(payload);
347
+ obj.reconciled = new Date().toISOString();
348
+ db.run("UPDATE events SET payload=? WHERE kind=? AND entity_id=?", [JSON.stringify(obj), spec.kind, r.id]);
349
+ marked++;
350
+ }
351
+ catch { /* best-effort per event */ }
352
+ }
353
+ if (marked)
354
+ fixes.push(`write_integrity: ${marked} ${spec.label} phantom event(s) marked reconciled (audit trail kept)`);
355
+ }
324
356
  if (fixes.length) {
325
357
  for (const f of fixes)
326
358
  (0, fmt_1.ok)(ctx, ` fixed: ${f}`);
@@ -555,7 +555,17 @@ function run(env, action, rest, args) {
555
555
  return (0, orm_1.getOrm)(env.db.path)
556
556
  .then((ds) => writes.claimLink(ds, rest[1], rest[2], stance))
557
557
  .then((r) => {
558
- (0, fmt_1.ok)(ctx, `linked; claim status now: ${r.status}`);
558
+ // 0.82.1: prediction claims never auto-promote on link — the receipt
559
+ // says so explicitly instead of showing a silently-unchanged status.
560
+ if (ctx.json) {
561
+ console.log(JSON.stringify({ claim: rest[1], evidence: rest[2], stance, status: r.status, auto_promote: r.auto_promote }, null, 2));
562
+ }
563
+ else if (r.auto_promote === false) {
564
+ (0, fmt_1.ok)(ctx, `linked; prediction claim stays ${r.status} — auto-promote skipped (arbitration: claim resolve / deadline audit)`);
565
+ }
566
+ else {
567
+ (0, fmt_1.ok)(ctx, `linked; claim status now: ${r.status}`);
568
+ }
559
569
  // R15 深度集成:链接后自动触发置信度评估(有模型时静默执行)
560
570
  try {
561
571
  const { evaluateClaimConfidence } = require("../../../ops/tools/src/eval-enhance");
@@ -629,6 +639,36 @@ function run(env, action, rest, args) {
629
639
  return 1;
630
640
  });
631
641
  }
642
+ // 0.82.1 (user report ③): the repair channel for link side effects —
643
+ // resets a claim to unverified. Covers predictions auto-promoted before
644
+ // 0.82.1 AND any claim arbitrated too early (explicit un-arbitration).
645
+ if (subAction === "unresolve") {
646
+ const id = rest[1];
647
+ if (!id) {
648
+ (0, fmt_1.bad)(ctx, "usage: research claim unresolve <claimId>");
649
+ return 2;
650
+ }
651
+ const c0 = store.db.get("SELECT id, status FROM claims WHERE id=? AND project_id=?", [id, project.id]);
652
+ if (!c0) {
653
+ (0, fmt_1.bad)(ctx, `claim not found: ${id}`);
654
+ return 1;
655
+ }
656
+ return (0, orm_1.getOrm)(env.db.path)
657
+ .then((ds) => writes.claimUnresolve(ds, id))
658
+ .then((r) => {
659
+ if (ctx.json)
660
+ console.log(JSON.stringify({ id, status: r.status, duplicate: r.duplicate }, null, 2));
661
+ else
662
+ (0, fmt_1.ok)(ctx, r.duplicate
663
+ ? `claim ${id} already unverified (no change)`
664
+ : `claim ${id}: ${c0.status} → unverified (back in tracking; arbitration: claim resolve)`);
665
+ return 0;
666
+ })
667
+ .catch((e) => {
668
+ (0, fmt_1.bad)(ctx, e?.message || String(e));
669
+ return 1;
670
+ });
671
+ }
632
672
  // R15.2: claim evaluate — decision model evaluates confidence from linked evidence
633
673
  if (subAction === "evaluate") {
634
674
  const id = rest[1];
@@ -5,6 +5,7 @@ exports.sourceAdd = sourceAdd;
5
5
  exports.evidenceAdd = evidenceAdd;
6
6
  exports.claimAdd = claimAdd;
7
7
  exports.claimLink = claimLink;
8
+ exports.claimUnresolve = claimUnresolve;
8
9
  exports.claimResolve = claimResolve;
9
10
  exports.queryAttach = queryAttach;
10
11
  exports.kbAdd = kbAdd;
@@ -178,7 +179,7 @@ async function claimLink(ds, claimId, evidenceId, stance = "supports") {
178
179
  return ds.transaction(async (mgr) => {
179
180
  // Resolve both ends before writing: a bad/foreign id must surface as a
180
181
  // named not-found, not as a raw FOREIGN KEY constraint failure.
181
- const c = (await mgr.query(`SELECT id FROM claims WHERE id=?`, [claimId]));
182
+ const c = (await mgr.query(`SELECT id, text FROM claims WHERE id=?`, [claimId]));
182
183
  if (!c.length)
183
184
  throw new Error(`claim not found: ${claimId}`);
184
185
  const e = (await mgr.query(`SELECT id FROM evidence WHERE id=?`, [evidenceId]));
@@ -186,6 +187,19 @@ async function claimLink(ds, claimId, evidenceId, stance = "supports") {
186
187
  throw new Error(`evidence not found: ${evidenceId}`);
187
188
  await mgr.query(`INSERT INTO claim_evidence(claim_id,evidence_id,stance) VALUES(?,?,?)
188
189
  ON CONFLICT(claim_id,evidence_id) DO UPDATE SET stance=excluded.stance`, [claimId, evidenceId, stance]);
190
+ // 0.82.1 (user report ③): prediction claims (`[预测·…]` prefix) must NOT
191
+ // auto-promote on link — predict audit treats supported/refuted as
192
+ // human-arbitrated (terminal), so a mere evidence link would kick the
193
+ // prediction out of the tracking table before its deadline. Evidence
194
+ // linking is evidence gathering; arbitration stays an explicit
195
+ // `claim resolve` (frozen contract, same discipline as predict audit).
196
+ const text = String(c[0]?.text ?? "");
197
+ if (text.startsWith("[预测·")) {
198
+ const cur = (await mgr.query(`SELECT status FROM claims WHERE id=?`, [claimId]));
199
+ const status = String(cur?.[0]?.status ?? "unverified");
200
+ await logEvent(mgr, "claim.link", "claim", claimId, `${stance} ${evidenceId} (prediction: auto-promote skipped)`);
201
+ return { status, auto_promote: false };
202
+ }
189
203
  const sup = (await mgr.query(`SELECT COUNT(*) AS c FROM claim_evidence WHERE claim_id=? AND stance='supports'`, [claimId]));
190
204
  const ref = (await mgr.query(`SELECT COUNT(*) AS c FROM claim_evidence WHERE claim_id=? AND stance='refutes'`, [claimId]));
191
205
  const s = Number(sup?.[0]?.c ?? 0);
@@ -193,7 +207,23 @@ async function claimLink(ds, claimId, evidenceId, stance = "supports") {
193
207
  const status = s && r ? "contested" : s ? "supported" : r ? "refuted" : "unverified";
194
208
  await mgr.update(entities_1.Claim, { id: claimId }, { status });
195
209
  await logEvent(mgr, "claim.link", "claim", claimId, `${stance} ${evidenceId}`);
196
- return { status };
210
+ return { status, auto_promote: true };
211
+ });
212
+ }
213
+ /** 0.82.1 (user report ③): reset an auto-promoted / wrongly arbitrated claim
214
+ * back to unverified — the repair channel for link side effects on
215
+ * predictions (and for any claim arbitrated too early). Idempotent: already
216
+ * unverified → duplicate:true, no event spam. */
217
+ async function claimUnresolve(ds, claimId) {
218
+ return ds.transaction(async (mgr) => {
219
+ const c = (await mgr.query(`SELECT id, status FROM claims WHERE id=?`, [claimId]));
220
+ if (!c.length)
221
+ throw new Error(`claim not found: ${claimId}`);
222
+ if (String(c[0]?.status) === "unverified")
223
+ return { status: "unverified", duplicate: true };
224
+ await mgr.update(entities_1.Claim, { id: claimId }, { status: "unverified", updated: (0, text_1.nowIso)() });
225
+ await logEvent(mgr, "claim.unresolve", "claim", claimId, `${c[0]?.status} → unverified`);
226
+ return { status: "unverified", duplicate: false };
197
227
  });
198
228
  }
199
229
  async function claimResolve(ds, claimId, status, note = "") {
@@ -168,6 +168,107 @@ function run(env, action, rest, args) {
168
168
  (0, fmt_1.out)(ctx, (0, fmt_1.paint)(fmt_1.C.yellow, ` ! "${rest[0]}" is not a tree — showing current tree (use -t <tree> to switch)`, ctx.color));
169
169
  }
170
170
  }
171
+ // 0.82 report ③: `node cond --due [--days N] [--eval]` — eval conds whose
172
+ // question carries a YYYY-MM-DD deadline, due (or overdue) within N days
173
+ // (default 7). --eval runs the decision model on each; judgment only —
174
+ // resolve/unlock stay human (same discipline as predict audit). Pre-tree-
175
+ // resolution like ready --all: --due is inherently cross-tree.
176
+ if (env.group === "node" && action === "cond" && args?.flags?.due === true) {
177
+ const days = flagNum("days", 7);
178
+ const ctxName = flagStr("context", "default");
179
+ const conds = db.all(`SELECT nc.id, nc.name, nc.op, nc.value, n.key node_key, n.id node_id, t.id tree_id, t.name tree
180
+ FROM node_conditions nc JOIN nodes n ON n.id=nc.node_id JOIN trees t ON t.id=n.tree_id
181
+ WHERE nc.op LIKE 'eval%' ORDER BY t.name, n.key`);
182
+ const now = Date.now();
183
+ const due = [];
184
+ for (const c of conds) {
185
+ const spec = (0, eval_1.parseEvalSpec)(c.value);
186
+ const q = spec.q || String(c.name);
187
+ const m = q.match(/(20\d{2})-(\d{2})-(\d{2})/);
188
+ if (!m)
189
+ continue;
190
+ const dl = Date.parse(`${m[1]}-${m[2]}-${m[3]}T23:59:59Z`);
191
+ if (!Number.isFinite(dl) || dl > now + days * 86400000)
192
+ continue;
193
+ due.push({
194
+ tree: c.tree, tree_id: c.tree_id, node: c.node_key, node_id: c.node_id,
195
+ name: String(c.name), op: String(c.op), question: q, envelope: spec,
196
+ deadline: `${m[1]}-${m[2]}-${m[3]}`, days_left: Math.ceil((dl - now) / 86400000),
197
+ });
198
+ }
199
+ due.sort((a, b) => a.days_left - b.days_left);
200
+ if (args?.flags?.eval !== true) {
201
+ if (ctx.json)
202
+ console.log(JSON.stringify(due, null, 2));
203
+ else {
204
+ (0, fmt_1.head)(ctx, `临期 eval 门槛 × ${due.length}(${days} 天窗口)`);
205
+ if (!due.length)
206
+ (0, fmt_1.out)(ctx, (0, fmt_1.paint)(fmt_1.C.dim, " (无临期 eval 条件)", ctx.color));
207
+ (0, fmt_1.table)(ctx, due.map((d) => ({ tree: d.tree, node: d.node, cond: d.name, deadline: d.deadline, 剩余天数: d.days_left, question: (0, text_1.clip)(d.question, 40) })), ["tree", "node", "cond", "deadline", "剩余天数", "question"], ["tree", "node", "cond", "deadline", "剩余天数", "question"]);
208
+ }
209
+ return 0;
210
+ }
211
+ // --eval: one decision-model request per due cond (ready --eval wiring)
212
+ const offline = !!process.env.YGL_EVAL_OFFLINE || (0, config_1.configGetBool)(env.db, "eval.offline");
213
+ const cfgToken = (0, config_1.configGet)(env.db, "decision_model_llm_token").trim();
214
+ const token = offline ? "" : cfgToken || (0, eval_1.evalToken)();
215
+ const endpoint = (0, config_1.configGet)(env.db, "decision_model_llm_api_base").trim() || eval_1.DEFAULT_ENDPOINT;
216
+ const passT = (0, config_1.configGetNumber)(env.db, "eval.pass_threshold");
217
+ const failT = (0, config_1.configGetNumber)(env.db, "eval.fail_threshold");
218
+ return (async () => {
219
+ const rows = [];
220
+ for (const d of due) {
221
+ const ctxPkg = trees.evalContext(d.tree_id, d.node, {}, ctxName);
222
+ const q = d.op === "eval-choice" ? { type: "choice", question: d.question, options: d.envelope.options || {}, answer: d.envelope.answer || "" }
223
+ : d.op === "eval-score" ? { type: "score", question: d.question, levels: d.envelope.levels || [], min: d.envelope.min ?? 0 }
224
+ : { type: "boolean", question: d.question };
225
+ const res = await (0, eval_1.evalMixed)(ctxPkg, [q], token, endpoint);
226
+ if (!res.ok) {
227
+ rows.push({ ...d, outcome: "pending", reason: res.error });
228
+ continue;
229
+ }
230
+ const r = res.results[0];
231
+ const m = d.op === "eval-choice" ? (0, eval_1.mapChoiceOutcome)(r, d.envelope.answer || "", passT, failT)
232
+ : d.op === "eval-score" ? (0, eval_1.mapScoreOutcome)(r, d.envelope.min ?? 0)
233
+ : (0, eval_1.mapOutcome)(r, passT, failT);
234
+ rows.push({ ...d, ...m });
235
+ }
236
+ if (ctx.json)
237
+ console.log(JSON.stringify(rows, null, 2));
238
+ else {
239
+ (0, fmt_1.head)(ctx, `临期 eval 门槛 — 已判定 × ${rows.length}`);
240
+ (0, fmt_1.table)(ctx, rows.map((d) => ({ tree: d.tree, node: d.node, deadline: d.deadline, outcome: d.outcome, p: d.p ?? d.score ?? "", question: (0, text_1.clip)(d.question, 36) })), ["tree", "node", "deadline", "判定", "p", "question"], ["tree", "node", "deadline", "outcome", "p", "question"]);
241
+ (0, fmt_1.out)(ctx, (0, fmt_1.paint)(fmt_1.C.dim, " 判定只报告不仲裁:通过 ≠ 自动解锁(resolve/unlock 保持人工)", ctx.color));
242
+ }
243
+ return 0;
244
+ })();
245
+ }
246
+ // 0.82 report ②: `node ready --all` sweeps EVERY tree — no tree use / -t
247
+ // needed (12 trees used to cost 24 commands). Runs before tree resolution
248
+ // because --all deliberately ignores the current selection.
249
+ if (env.group === "node" && action === "ready" && args?.flags?.all === true) {
250
+ const state = parseState(args);
251
+ const ctxName = flagStr("context", "default");
252
+ const all = db.all("SELECT id, name FROM trees ORDER BY created");
253
+ const per = [];
254
+ for (const t of all) {
255
+ const rows = trees.ready(t.id, ctxName, state);
256
+ if (rows.length)
257
+ per.push({ tree: t.name, ready: rows });
258
+ }
259
+ if (ctx.json) {
260
+ console.log(JSON.stringify(per, null, 2));
261
+ return 0;
262
+ }
263
+ (0, fmt_1.head)(ctx, `Ready across ${all.length} trees`);
264
+ if (!per.length)
265
+ (0, fmt_1.out)(ctx, (0, fmt_1.paint)(fmt_1.C.dim, " (no tree has ready nodes)", ctx.color));
266
+ for (const p of per) {
267
+ (0, fmt_1.out)(ctx, (0, fmt_1.paint)(fmt_1.C.bold, ` ${p.tree} (${p.ready.length})`, ctx.color));
268
+ (0, fmt_1.table)(ctx, p.ready, ["key", "name", "tier", "cost"], ["key", "name", "tier", "cost"]);
269
+ }
270
+ return 0;
271
+ }
171
272
  let tree;
172
273
  try {
173
274
  tree = positionalTree || trees.treeResolve(sel);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ygl-trees",
3
- "version": "0.81.0",
3
+ "version": "0.82.1",
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,8 +26,8 @@
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/schemastery": "~3.18.5-alpha.2",
30
29
  "@deepseek-ai/cordis-plugin-timer": "~1.1.7-alpha.2",
30
+ "@deepseek-ai/schemastery": "~3.18.5-alpha.2",
31
31
  "@deepseek-ai/cosmokit": "~1.8.6-alpha.2"
32
32
  },
33
33
  "devDependencies": {
@@ -58,7 +58,7 @@ ygl-trees llms --json # 结构化 JSON
58
58
 
59
59
  ```bash
60
60
  ygl-trees doctor # 10 项健康检查(含锁 pid 存活、幻影写入对账)
61
- ygl-trees doctor --fix # 一键安全修复(脏索引 + 死 pid 陈旧锁;活 holder 不动)
61
+ ygl-trees doctor --fix # 一键安全修复(脏索引 + 死 pid 陈旧锁 + 幻影事件标记 reconciled;活 holder 不动)
62
62
  ygl-trees db unlock # 清残留 .ygllock(死 pid 直清;活 holder 回显其命令行并拒绝,--force 强清)
63
63
  ygl-trees kb rm <id> # 删除笔记(与 skill rm 对称)
64
64
  ygl-trees webui serve --host 0.0.0.0 --port 8642 # LAN/WSL 可访问
@@ -42,11 +42,13 @@ ygl-trees research evidence add <sourceId> --quote "原文片段" --locator "§2
42
42
  ygl-trees research evidence add <sourceId> --file quotes.md # 长摘录免转义
43
43
  ygl-trees research claim add "断言文本" --confidence A # 档位
44
44
  ygl-trees research claim add "断言文本" --confidence 0.92 # 数值档(0.81:0-1 或 0-100%)
45
- ygl-trees research claim link <claimId> <evidenceId> [--stance refutes] # 状态自动推导
45
+ ygl-trees research claim link <claimId> <evidenceId> [--stance refutes] # 状态自动推导(普通断言)
46
46
  ygl-trees research claim resolve <claimId> --as contested --note "矛盾待查" # 人工仲裁出口
47
+ ygl-trees research claim unresolve <claimId> # 复位回 unverified(0.82.1:撤销链接副作用/抢跑仲裁)
47
48
  ```
48
49
 
49
50
  - 有支持有反对 → `contested`;仲裁后新证据链接会重新推导
51
+ - **预测断言(`[预测·…]` 前缀)挂证据不自动晋升**(0.82.1):挂证据 ≠ 仲裁,状态保持 unverified 直到显式 resolve / 到期回测——predict audit 才不会把刚挂证据的预测误判已结案;误晋升用 `claim unresolve` 归位(幂等 duplicate:true)
50
52
  - 全部命令幂等:同内容重跑返回已存在行(`duplicate: true`,rc=0)
51
53
 
52
54
  ## 3. 产出
@@ -65,6 +67,7 @@ ygl-trees produce export <reportId> report.md
65
67
  - 先 `agent brief --json` 看 next_actions(它会告诉你缺来源/缺证据/缺断言)
66
68
  - 每条断言至少一条证据再进报告;contested 的要么补证据要么 resolve
67
69
  - 预测断言(`[预测·N天]`)定期 `research predict audit` 回测:本地判据自动测;`--state <file|url>`
68
- 桥接外部计数(裸键名后缀匹配 `counts.y_imported`);已 resolve 的默认隐藏(`--all` 查看终态表)
70
+ 桥接外部计数(裸键名后缀匹配 `counts.y_imported`);已 resolve 的默认隐藏(`--all` 查看终态表,
71
+ `--json` 始终含全量行含终态——机器读全量、人看增量)
69
72
  - `--json` 输出可直接 parse 拿 id;列表命令字段:source(id,title,url,relevance,…)、
70
73
  claim(id,text,status,confidence,sup,ref)、evidence(id,source,locator,quote,used_by)
@@ -41,6 +41,9 @@ ygl-trees node index && ygl-trees node validate
41
41
  ygl-trees node can steel --state "lab_level=3,has_eng=1" # 三类失败分开列+成本系数
42
42
  ygl-trees node can a b c --state-file bags.json # 批量:一进程测 N 节点(探针 1s×N → 1s)
43
43
  ygl-trees node ready # 当前可解锁集合
44
+ ygl-trees node ready --all # 跨树扫描(无需逐树 tree use)
45
+ ygl-trees node cond --due [--days 30] # 临期 eval 门槛(问题文本带 YYYY-MM-DD 期限)
46
+ ygl-trees node cond --due --eval # 到期逐条送判定模型(只报告不仲裁)
44
47
  ygl-trees node path steel # 解锁顺序+累计成本(目标恒最后)
45
48
  ygl-trees node ancestors bronze # 闭包索引直查,depth=1 是直接父
46
49
  ygl-trees node unlock mining && ygl-trees node lock mining
@@ -49,6 +52,8 @@ ygl-trees node unlock mining && ygl-trees node lock mining
49
52
  - 写入时环守卫:门控边(prereq/unlock)成环当场拒绝
50
53
  - `node can` 输出:missing_required(阻塞)/ blocked_by(被排斥)/ missing_optional(不阻塞)
51
54
  - `--state-file` 两种形状:扁平 `{"y_imported":40}` 全节点共享;按节点 `{"a":{"y_imported":40}}` 各取各袋;多键 `--json` 返回数组(0.79.1)
55
+ - `ready --all` / `cond --due` 前置于树解析:无当前树也能跑,跨树巡检一条命令(0.82)
56
+ - `cond --due` 提取 eval 问题文本中的 `YYYY-MM-DD`;默认 7 天窗口(含逾期),`--eval` 判定后仍需人工 resolve/unlock——判定 ≠ 仲裁(0.82)
52
57
 
53
58
  ## 3. 迁移与保真
54
59