ygl-trees 0.82.0 → 0.83.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,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.83.0(2026-10-17)并发防护:指针钉住 + 半写拒绝 + 帮助面
4
+ - **写命令长旗标钉住**(用户上报①,最重:并发会话两次劫持共享 current-project 指针,资产碎片化跨 3 项目):`research *` / `produce *`(含 task)/ `project *` 全部接受 `--project <p>` 长旗标,`node *` 接受 `--tree <t>`(短旗标 `-p`/`-t` 不变;`node import --tree` 仍是目标树名,别名不遮蔽);source/evidence/claim/task 写回执带 `· project <名>` 回显落点——写错项目当场可见,不必事后对账
5
+ - **`source add --query` 半写防护**(上报①:attach 失败仍落源):引用不存在时**先拒绝后写入**(rc=1 点名、零落地),不再"源已落、链接失败";attach 落库后失败的路径改报已落 id + `research query attach` 重试指路
6
+ - **`--help` 任意深度**(上报②:`kb add --help` 不识别直接 rc=2):任意子命令带 `--help` 打印所在组的用法切片 rc=0(`--json` 返回 `{help}`),在 flag 校验与 db boot 之前短路;`webui serve --help` 等原有短路兼容
7
+ - **no-db 报错点名工作区根因**(上报③:`db init` 前报错不提示 YGL_HOME):无库时错误带 `workspace: <path>` 与来源(YGL_HOME 显式 / 默认 ~/.ygl-trees),冷启动错环境一眼可辨
8
+ - **`node can --eval` attachments 快照**(建议④:P 值过了看不出吃了哪些 rel 证据):输出回显本次判定实际吃到的 rel 挂接——`--json` 增 `eval_attachments[]`,人类视图 `attachments ×N:` 逐条(rel ← type:id 摘要),复现不必翻库;无挂接时字段省略(字节兼容)
9
+ - **job 子命令无锁启动**(建议⑤配套):`job run/tick` 的子进程带 `YGL_SERVE=1` 启动——此前 runner 自持应用级写锁,任何 `ygl-trees …` 型任务命令必撞自身死锁(rc=1 "locked");WAL 并发读 + SQLite 层写串行,与 webui serve 同推理。到期巡检范式:`job add "到期巡检" --every 1440 -- ygl-trees node cond --due --days 30`(真实工作区已挂,30 天窗口即刻命中 w1 三门槛)
10
+ - 测试:demo 5k 段 7 断言(--project 钉住 / 落点校验 / 半写拒绝+零落地 / --help rc=0 / attachments 快照)+ 5h `--tree` 别名断言 → 113;readme-verify +3(kb add --help、source add --project、node list --tree 点名)→ 46
11
+
12
+ ## 0.82.1(2026-10-17)预测生命周期护栏:挂证据 ≠ 仲裁
13
+ - **`claim link` 不再抢跑预测仲裁**(用户上报③:`--stance supports` 自动把 [预测·…] 断言置 supported,predict audit 判 supported||refuted → resolved——预测刚挂证据就退出跟踪表,实际无人仲裁):预测前缀断言挂证据时跳过自动晋升,状态保持 unverified 直到显式 `claim resolve` / 到期回测;证据链接与 stance 照常落库,回执明示 `auto_promote: false`(`--json` 返回 `{claim, evidence, stance, status, auto_promote}`);普通断言的自动推导不变(回归护栏)
14
+ - **`claim unresolve <claimId>`**(建议②:误晋升后无归位命令):复位通道把断言重置回 unverified(撤销链接副作用 / 抢跑仲裁),幂等(已 unverified → `duplicate:true`),落 `claim.unresolve` 事件留痕——0.82.1 前被误晋升的预测可用它归队
15
+ - **predict audit `--json` 覆盖确认**(建议③):`--json` 始终输出全量 rows(含 resolved 终态行),人类视图默认隐藏终态——机器读全量、人看增量,同构无缺口
16
+ - 测试:predlink.spec.ts 5 例(预测不晋升 / 普通照常推导 / unresolve 复位+事件 / 幂等 / 坏 id 点名)→ 177;demo 5j 段 5 断言(挂证据不晋升 / 普通推导回归 / unresolve 复位 / 幂等 / 复位后仍在跟踪表)→ 106
17
+
3
18
  ## 0.82.0(2026-10-17)跨树巡检 + 临期 eval 门槛 + 幻影对账一键修复
4
19
  - **`node ready --all`**(用户建议②:12 棵树巡检要 24 条命令——每树 tree use + ready):一次进程扫全部树的就绪集,无需 tree use / `-t`;`--json` 返回 `[{tree, ready[]}]`,人类输出按树分节
5
20
  - **`node cond --due [--days N] [--eval]`**(建议③:w1/d90 检验依赖人记得看):扫描全部 eval 族条件,提取问题文本中的 `YYYY-MM-DD` 期限,列出 N 天内到期(含逾期)的条件(树/节点/deadline/剩余天数);`--days` 放宽窗口(默认 7);`--eval` 逐条组装上下文包送判定模型(三题型按 op 自动选择,离线安全回落 pending)——**判定只报告不仲裁**,通过 ≠ 自动解锁,resolve/unlock 保持人工(与 predict audit 同纪律);跨树扫描前置于树解析,无当前树也能跑
package/README.md CHANGED
@@ -109,9 +109,12 @@ ygl-trees node import --file tree.json --tree copy # 严格校验、事务导
109
109
 
110
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
111
 
112
+ **并发钉住 + 半写防护(0.83,真实用户报告)**:并发会话共享 `current_project` 指针互踩导致资产碎片化——研究/产出/树三域写命令全部接受长旗标钉住:`research * --project <p>`、`produce/task * --project <p>`、`node * --tree <t>`(短旗标 `-p`/`-t` 不变);写回执带 `· project <名>` 回显落点,写错项目当场可见。`source add --query` 引用不存在时**先拒绝后写入**(rc=1、零落地),不再出现"源已落、链接失败"的半写;`job run/tick` 的子命令以无锁环境启动(WAL 并发读),`ygl-trees …` 型任务命令不再与运行器自身死锁。`--help` 在任意深度(如 `kb add --help`)打组帮助 rc=0;`db init` 前的报错提示 `YGL_HOME` 工作区根因;到期巡检可直接挂 job(`job add "到期巡检" --every 1440 -- ygl-trees node cond --due --days 30`)。
113
+
112
114
  **决策模型评估门(spec R8;0.14 布尔 → 0.26 三题型 → 0.27 混批 → 0.80 织物并入)**:语义型必检条件用 eval 题型声明,`node can <key> --eval` 会把**节点元数据 + 直接父(关联)+ 祖先链(闭包索引)+ 条件组 + 已解锁集 + --state 袋 + rel 挂接摘要**组装为上下文包,交给评估模型(TypeSafe jev,经 Vercel AI Gateway)判定:
113
115
 
114
116
  - **rel 织物并入(0.80)**:节点挂接的 evidence(quote 摘要)/ claim(text+status+confidence)/ kb/source(标题)等 rel 关系(双向,≤12 条,文本截 120 字)自动进上下文包的 `attachments`——「证据挂链」纪律从此对语义门槛可见,不必再 `--state k=v` 手动携带裁决事实(实测 P 0.30 → 0.92 的差距由挂链补平)
117
+ - **attachments 快照(0.83)**:`node can <key> --eval` 的输出(`--json` 的 `eval_attachments[]` + 人类视图 `attachments ×N:`)回显本次判定实际吃到的 rel 挂接——P 值过了也能一眼看出依据是哪几条 evidence/claim,复现不必翻库
115
118
 
116
119
  - **三题型**:`--op eval`(布尔 P(true))/ `--op eval-choice`(多选一,判 **P(期望答案)**,`--options "k=desc,..."` + `--answer k`)/ `--op eval-score`(等级评分 `--levels "级0,..."` 2-10 级 + `--min-score n`,判概率加权均值 ±1 灰带)
117
120
  - P≥0.7 通过 · P≤0.4 未通过 · **灰带 pending**(过程态,禁作终态,单列不阻塞也不放行);score 的灰带是 [min−1, min)
@@ -131,14 +134,17 @@ ygl-trees node import --file tree.json --tree copy # 严格校验、事务导
131
134
  ```
132
135
  query add 登记检索式
133
136
  query attach 检索式 ↔ 来源关联(query list 显示召回数)
134
- source add 收来源,按 (project, url_hash) 去重;--query 顺带关联
137
+ source add 收来源,按 (project, url_hash) 去重;--query 顺带关联(引用不存在 → 整体拒绝 rc=1,零落地)
138
+ 全组接受 --project <p> 长旗标钉住目标项目(并发会话不依赖共享 current 指针;回执带 · project 回显)
135
139
  --adjudication "<裁决摘要>":会话内裁决原生载体(url 自动 session://adjudication/<时刻>,摘要兼作标题);--url 收任意 <scheme>://(https/file/session/a2a/… 跨 Agent 引用免落盘)
136
140
  source fetch <url> 直接抓取网页入库:title/meta description/去标签正文;15s 超时、非 html 拒收、失败零副作用,正文过薄自动打 needs_review
137
141
  evidence add 从来源摘原文片段,带 locator(页码/章节);长摘录用 --file <path> 免转义
138
142
  claim add 写下断言;--confidence 收 A|B|C 或数值档(0.92 / 85% 原样入库)
139
143
  claim link 断言 ↔ 证据,自动算 supported / refuted / contested
140
144
  --by-source:传来源 id 一次挂齐其全部证据;误传来源 id 会列出该来源的证据 id
145
+ [预测·…] 断言不自动晋升(0.82.1):挂证据≠仲裁,状态保持 unverified 直到显式 resolve / 到期回测
141
146
  claim resolve 人工仲裁出口:--as supported|refuted|contested,事件留痕;后续新证据链接会重新推导
147
+ claim unresolve 复位通道:把断言重置回 unverified(撤销链接副作用 / 抢跑仲裁),幂等 duplicate:true
142
148
  predict audit 预测回测:扫描 [预测·N天] 断言,本地可测判据自动测(树/kb 计数、npm 版本数),判 hit/miss/pending/manual——只报告不仲裁,给出待 resolve 建议
143
149
  --state <file|url>:外部计数桥接(如 DSH API 快照),判据里的裸键名后缀匹配嵌套键(y_imported≥12 ← counts.y_imported)
144
150
  已 claim resolve 的预测默认隐藏(回测终态),--all 查看全表
@@ -294,7 +300,7 @@ ygl-trees agent ask "<question>" natural language interface
294
300
  ## 五b、开发与运维工具
295
301
 
296
302
  ```bash
297
- pnpm test # 四段制回归:unit (vitest 172 例) + build (tsc + tsdown) + demo(101 条断言)+ contract:audit(86 项)
303
+ pnpm test # 四段制回归:unit (vitest 177 例) + build (tsc + tsdown) + demo(113 条断言)+ contract:audit(86 项)
298
304
  pnpm bench # 性能基准七席(startup/写路径/检索/索引),每席含 RSS 内存端点 → tmp/bench-results.json
299
305
  node benchmarks/bench.js --quick # 缩小规模快速跑
300
306
  pnpm pack:check # 发布就绪门禁:打包 → 隔离安装 → 驱动发布产物跑命令面 + --json 纯净性
@@ -42,6 +42,7 @@ exports.loadBundle = loadBundle;
42
42
  exports.repoRoot = repoRoot;
43
43
  exports.appVersion = appVersion;
44
44
  exports.usage = usage;
45
+ exports.groupHelp = groupHelp;
45
46
  exports.main = main;
46
47
  exports.generateLlmsManifest = generateLlmsManifest;
47
48
  /**
@@ -163,6 +164,7 @@ ${(0, fmt_1.paint)(fmt_1.C.bold, "research", true)} collect → screen → ex
163
164
  claim link <claimId> <evidenceId> [--stance supports|refutes]
164
165
  --by-source <claimId> <sourceId>: link ALL evidence under a source
165
166
  claim resolve <claimId> --as supported|refuted|contested [--note]
167
+ claim unresolve <claimId> reset to unverified (undo auto-promote / early arbitration)
166
168
  claim list
167
169
 
168
170
  ${(0, fmt_1.paint)(fmt_1.C.bold, "produce", true)} outline → section → report · spec/design(工程产出)
@@ -250,6 +252,25 @@ Global: --json --no-color --db <file> --home <dir>
250
252
  function usage() {
251
253
  return USAGE;
252
254
  }
255
+ /** 0.83 (user report ②): `--help` at any depth prints the relevant usage
256
+ * slice — `kb add --help` used to fall into flag validation rc=2. Renders the
257
+ * group's own block (found by its bold header line); unknown groups get the
258
+ * full usage. */
259
+ function groupHelp(group) {
260
+ const pfx = (0, fmt_1.paint)(fmt_1.C.bold, group, true);
261
+ const lines = USAGE.split("\n");
262
+ const start = lines.findIndex((l) => l.startsWith(pfx));
263
+ if (start < 0)
264
+ return USAGE;
265
+ let end = lines.length;
266
+ for (let i = start + 1; i < lines.length; i++) {
267
+ if (lines[i].startsWith("\u001b[1m")) {
268
+ end = i;
269
+ break;
270
+ }
271
+ }
272
+ return lines.slice(0, 4).join("\n") + "\n\n" + lines.slice(start, end).join("\n").trimEnd();
273
+ }
253
274
  // ── Main ──
254
275
  async function main(argv) {
255
276
  const args = (0, types_1.parse)(argv);
@@ -290,6 +311,17 @@ async function main(argv) {
290
311
  (0, fmt_1.out)(ctx, appVersion());
291
312
  return 0;
292
313
  }
314
+ // 0.83 (user report ②): `--help` at ANY depth prints the group's usage
315
+ // slice rc=0 — `kb add --help` used to fall into flag validation rc=2.
316
+ // Fires before dispatch and before db boot: help never touches the db.
317
+ if (args.flags.help === true && group) {
318
+ const text = groupHelp(group);
319
+ if (ctx.json)
320
+ console.log(JSON.stringify({ help: text.replace(/\u001b\[[0-9;]*m/g, "") }, null, 2));
321
+ else
322
+ (0, fmt_1.out)(ctx, text);
323
+ return 0;
324
+ }
293
325
  // llms: structured capability manifest for LLM consumption (Ant Design
294
326
  // "for-agents" pattern — one output gives any Agent the full command surface).
295
327
  // Onboarding: detect first run (no DB) and guide to db init
@@ -363,6 +395,10 @@ async function main(argv) {
363
395
  if (needInit && !require("node:fs").existsSync(file)) {
364
396
  (0, fmt_1.bad)(ctx, `no database at ${file}`);
365
397
  (0, fmt_1.out)(ctx, ` run: ygl-trees db init`);
398
+ // 0.83 (user report ③): name the workspace root cause — a foreign
399
+ // YGL_HOME (or its absence) is invisible otherwise, and the user chases
400
+ // the db instead of the environment.
401
+ (0, fmt_1.out)(ctx, ` workspace: ${home}${process.env.YGL_HOME ? " (from YGL_HOME)" : " (default ~/.ygl-trees — set YGL_HOME to switch)"}`);
366
402
  return 2;
367
403
  }
368
404
  // ── Cordis composition via bootApp (DSH profile-boot exemplar) ──
@@ -664,6 +700,7 @@ function generateLlmsManifest() {
664
700
  ["research claim add \"<text>\"", "refutable assertion"],
665
701
  ["research claim link <cid> <eid> --stance supports|refutes", "link claim to evidence"],
666
702
  ["research claim resolve <cid> --as supported|refuted|contested", "manual arbitration"],
703
+ ["research claim unresolve <cid>", "reset to unverified (undo link auto-promote)"],
667
704
  ["research claim evaluate <cid>", "decision model confidence (0-5 → A/B/C/D)"],
668
705
  ["research evidence assess <eid>", "decision model quality assessment"],
669
706
  ]],
@@ -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 = "") {
@@ -5751,7 +5788,11 @@ function execute(env, job) {
5751
5788
  "pipe",
5752
5789
  "pipe"
5753
5790
  ],
5754
- timeout: 6e5
5791
+ timeout: 6e5,
5792
+ env: {
5793
+ ...process.env,
5794
+ YGL_SERVE: "1"
5795
+ }
5755
5796
  });
5756
5797
  rc = 0;
5757
5798
  } catch (e) {
@@ -6594,7 +6635,7 @@ function run$8(env, action, rest, args) {
6594
6635
  const subAction = rest[0];
6595
6636
  let project;
6596
6637
  try {
6597
- project = store.projectResolve(args.flags.p ? String(args.flags.p) : void 0);
6638
+ project = store.projectResolve(args.flags.p ?? args.flags.project ? String(args.flags.p ?? args.flags.project) : void 0);
6598
6639
  } catch (e) {
6599
6640
  bad(ctx, e.message);
6600
6641
  return 1;
@@ -6733,12 +6774,25 @@ function run$8(env, action, rest, args) {
6733
6774
  tags: String(args.flags.tags || "")
6734
6775
  };
6735
6776
  const qRef = String(args.flags.query || "");
6777
+ if (qRef) {
6778
+ if (!store.db.get("SELECT id FROM queries WHERE (id=? OR text=?) AND project_id=?", [
6779
+ qRef,
6780
+ qRef,
6781
+ project.id
6782
+ ])) {
6783
+ bad(ctx, `query not found: ${require_text.clip(qRef, 40)} — refusing to write (source add would land without its query link; fix the ref or drop --query)`);
6784
+ return 1;
6785
+ }
6786
+ }
6736
6787
  return getOrm(env.db.path).then(async (ds) => {
6737
6788
  const r = await sourceAdd(ds, project.id, spec);
6738
- writeResult(ctx, r, (row) => `source added: ${row.id} ${require_text.clip(row.title, 60)}`);
6739
- if (qRef) {
6789
+ writeResult(ctx, r, (row) => `source added: ${row.id} ${require_text.clip(row.title, 60)} · project ${project.name}`);
6790
+ if (qRef) try {
6740
6791
  const at = await queryAttach(ds, project.id, qRef, r.row.id);
6741
6792
  if (!ctx.json && !at.dup) out(ctx, paint(C.dim, ` attached to query: "${require_text.clip(at.queryText, 40)}"`, ctx.color));
6793
+ } catch (e) {
6794
+ bad(ctx, `source landed as ${r.row.id} but attach failed: ${e?.message} — research query attach ${require_text.clip(qRef, 20)} ${r.row.id} to retry`);
6795
+ return 1;
6742
6796
  }
6743
6797
  return 0;
6744
6798
  }).catch((e) => {
@@ -6911,7 +6965,7 @@ function run$8(env, action, rest, args) {
6911
6965
  }
6912
6966
  const note = String(args.flags.note || "");
6913
6967
  const locator = String(args.flags.locator || "");
6914
- return runWrite(env, (ds) => evidenceAdd(ds, project.id, sourceId, quote, note, locator, force), (r) => `evidence added: ${r.id} ${require_text.clip(quote, 50)}`);
6968
+ return runWrite(env, (ds) => evidenceAdd(ds, project.id, sourceId, quote, note, locator, force), (r) => `evidence added: ${r.id} ${require_text.clip(quote, 50)} · project ${project.name}`);
6915
6969
  }
6916
6970
  if (action === "evidence" && subAction === "assess") {
6917
6971
  const id = rest[1];
@@ -7007,7 +7061,7 @@ function run$8(env, action, rest, args) {
7007
7061
  return 2;
7008
7062
  }
7009
7063
  const confidence = String(args.flags.confidence || configGet(env.db, "default_confidence"));
7010
- return runWrite(env, (ds) => claimAdd(ds, project.id, text, confidence, force), (r) => `claim added: ${r.id} ${require_text.clip(text, 60)}`);
7064
+ return runWrite(env, (ds) => claimAdd(ds, project.id, text, confidence, force), (r) => `claim added: ${r.id} ${require_text.clip(text, 60)} · project ${project.name}`);
7011
7065
  }
7012
7066
  if (subAction === "link") {
7013
7067
  if (!rest[1] || !rest[2]) {
@@ -7049,7 +7103,15 @@ function run$8(env, action, rest, args) {
7049
7103
  });
7050
7104
  }
7051
7105
  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}`);
7106
+ if (ctx.json) console.log(JSON.stringify({
7107
+ claim: rest[1],
7108
+ evidence: rest[2],
7109
+ stance,
7110
+ status: r.status,
7111
+ auto_promote: r.auto_promote
7112
+ }, null, 2));
7113
+ else if (r.auto_promote === false) ok(ctx, `linked; prediction claim stays ${r.status} — auto-promote skipped (arbitration: claim resolve / deadline audit)`);
7114
+ else ok(ctx, `linked; claim status now: ${r.status}`);
7053
7115
  try {
7054
7116
  const { evaluateClaimConfidence } = (init_eval_enhance(), require_rolldown_runtime.__toCommonJS(eval_enhance_exports));
7055
7117
  const evRows = store.db.all(`SELECT e.quote, ce.stance, s.title AS source_title
@@ -7129,6 +7191,30 @@ function run$8(env, action, rest, args) {
7129
7191
  return 1;
7130
7192
  });
7131
7193
  }
7194
+ if (subAction === "unresolve") {
7195
+ const id = rest[1];
7196
+ if (!id) {
7197
+ bad(ctx, "usage: research claim unresolve <claimId>");
7198
+ return 2;
7199
+ }
7200
+ const c0 = store.db.get("SELECT id, status FROM claims WHERE id=? AND project_id=?", [id, project.id]);
7201
+ if (!c0) {
7202
+ bad(ctx, `claim not found: ${id}`);
7203
+ return 1;
7204
+ }
7205
+ return getOrm(env.db.path).then((ds) => claimUnresolve(ds, id)).then((r) => {
7206
+ if (ctx.json) console.log(JSON.stringify({
7207
+ id,
7208
+ status: r.status,
7209
+ duplicate: r.duplicate
7210
+ }, null, 2));
7211
+ else ok(ctx, r.duplicate ? `claim ${id} already unverified (no change)` : `claim ${id}: ${c0.status} → unverified (back in tracking; arbitration: claim resolve)`);
7212
+ return 0;
7213
+ }).catch((e) => {
7214
+ bad(ctx, e?.message || String(e));
7215
+ return 1;
7216
+ });
7217
+ }
7132
7218
  if (subAction === "evaluate") {
7133
7219
  const id = rest[1];
7134
7220
  if (!id) {
@@ -7356,7 +7442,7 @@ function run$7(env, action, rest, args) {
7356
7442
  ack(ctx, { project: name }, `now using: ${name}`);
7357
7443
  return 0;
7358
7444
  case "show": {
7359
- const resolved = store.projectResolve(args.flags.p ? String(args.flags.p) : void 0);
7445
+ const resolved = store.projectResolve(args.flags.p ?? args.flags.project ? String(args.flags.p ?? args.flags.project) : void 0);
7360
7446
  return getOrm(env.db.path).then(async (ds) => {
7361
7447
  const proj = await ds.getRepository(Project).findOne({
7362
7448
  where: { id: resolved.id },
@@ -7777,7 +7863,7 @@ function run$5(env, action, rest, args) {
7777
7863
  }
7778
7864
  let project;
7779
7865
  try {
7780
- project = store.projectResolve(args.flags.p ? String(args.flags.p) : void 0);
7866
+ project = store.projectResolve(args.flags.p ?? args.flags.project ? String(args.flags.p ?? args.flags.project) : void 0);
7781
7867
  } catch (e) {
7782
7868
  bad(ctx, e.message);
7783
7869
  return 1;
@@ -7889,7 +7975,7 @@ function run$5(env, action, rest, args) {
7889
7975
  return getOrm(env.db.path).then((ds) => taskAdd(ds, pid, title, note)).then((row) => {
7890
7976
  if (!nodeRef) {
7891
7977
  if (ctx.json) console.log(JSON.stringify(row, null, 2));
7892
- else ok(ctx, `task added: ${row.id} ${title}`);
7978
+ else ok(ctx, `task added: ${row.id} ${title} · project ${project.name}`);
7893
7979
  return 0;
7894
7980
  }
7895
7981
  return relAdd(env, `node:${nodeRef}`, `task:${row.id}`, "tracked-by").then((r) => {
@@ -8713,7 +8799,7 @@ require_text.init_text();
8713
8799
  function run$4(env, action, rest, args) {
8714
8800
  const { ctx, db } = env;
8715
8801
  const trees = new TreeStore(db);
8716
- const sel = args.flags.t ? String(args.flags.t) : void 0;
8802
+ const sel = !(action === "import") ? args.flags.t ?? args.flags.tree ? String(args.flags.t ?? args.flags.tree) : void 0 : args.flags.t ? String(args.flags.t) : void 0;
8717
8803
  const flagNum = (k, d) => {
8718
8804
  const v = Number(args.flags[k]);
8719
8805
  return Number.isFinite(v) ? v : d;
@@ -9274,6 +9360,8 @@ function run$4(env, action, rest, args) {
9274
9360
  }
9275
9361
  r.pending = pendingList;
9276
9362
  r.evaluated = evaluated;
9363
+ const atts = contextPkg.attachments;
9364
+ if (Array.isArray(atts) && atts.length) r.eval_attachments = atts;
9277
9365
  r.ok = r.missing_required.length === 0 && r.blocked_by.length === 0 && pendingList.length === 0;
9278
9366
  try {
9279
9367
  const run = process.env.YGL_RUN_ID || "";
@@ -9357,6 +9445,13 @@ function run$4(env, action, rest, args) {
9357
9445
  for (const m of r.missing_optional) out(ctx, ` - ${m}`);
9358
9446
  }
9359
9447
  for (const e of r.evaluated) out(ctx, paint(C.dim, ` eval ${e.name}: P=${e.p ?? "-"} → ${e.outcome}${e.reason ? ` (${e.reason})` : ""}`, ctx.color));
9448
+ if (Array.isArray(r.eval_attachments) && r.eval_attachments.length) {
9449
+ out(ctx, paint(C.dim, ` attachments ×${r.eval_attachments.length}:`, ctx.color));
9450
+ for (const a of r.eval_attachments) {
9451
+ const label = a.type === "claim" ? `${a.id}(${a.status})` : a.type === "evidence" ? `${a.id} "${require_text.clip(a.quote, 36)}"` : a.type === "kb" || a.type === "source" ? `${a.id} ${require_text.clip(a.title, 30)}` : a.type === "node" ? `${a.key ?? a.id}` : String(a.id);
9452
+ out(ctx, paint(C.dim, ` - ${a.rel} ← ${a.type}:${label}`, ctx.color));
9453
+ }
9454
+ }
9360
9455
  if (r.cost_multiplier !== 1) out(ctx, ` 成本系数 ${r.cost_multiplier}`);
9361
9456
  return 0;
9362
9457
  }
@@ -9648,10 +9743,11 @@ function run$4(env, action, rest, args) {
9648
9743
  if (sub === "list" || sub === void 0) {
9649
9744
  for (const f of Object.keys(args.flags || {})) if (![
9650
9745
  "t",
9746
+ "tree",
9651
9747
  "json",
9652
9748
  "no-color"
9653
9749
  ].includes(f)) {
9654
- bad(ctx, `unknown flag --${f} for 'node list' — tree selection is '-t <tree>' (positional <tree> also works)`);
9750
+ bad(ctx, `unknown flag --${f} for 'node list' — tree selection is '-t|--tree <tree>' (positional <tree> also works)`);
9655
9751
  return 2;
9656
9752
  }
9657
9753
  table(ctx, trees.nodeList(tid), [
@@ -11979,6 +12075,7 @@ ${paint(C.bold, "research", true)} collect → screen → extract → claim
11979
12075
  claim link <claimId> <evidenceId> [--stance supports|refutes]
11980
12076
  --by-source <claimId> <sourceId>: link ALL evidence under a source
11981
12077
  claim resolve <claimId> --as supported|refuted|contested [--note]
12078
+ claim unresolve <claimId> reset to unverified (undo auto-promote / early arbitration)
11982
12079
  claim list
11983
12080
 
11984
12081
  ${paint(C.bold, "produce", true)} outline → section → report · spec/design(工程产出)
@@ -12066,6 +12163,22 @@ Global: --json --no-color --db <file> --home <dir>
12066
12163
  function usage() {
12067
12164
  return USAGE;
12068
12165
  }
12166
+ /** 0.83 (user report ②): `--help` at any depth prints the relevant usage
12167
+ * slice — `kb add --help` used to fall into flag validation rc=2. Renders the
12168
+ * group's own block (found by its bold header line); unknown groups get the
12169
+ * full usage. */
12170
+ function groupHelp(group) {
12171
+ const pfx = paint(C.bold, group, true);
12172
+ const lines = USAGE.split("\n");
12173
+ const start = lines.findIndex((l) => l.startsWith(pfx));
12174
+ if (start < 0) return USAGE;
12175
+ let end = lines.length;
12176
+ for (let i = start + 1; i < lines.length; i++) if (lines[i].startsWith("\x1B[1m")) {
12177
+ end = i;
12178
+ break;
12179
+ }
12180
+ return lines.slice(0, 4).join("\n") + "\n\n" + lines.slice(start, end).join("\n").trimEnd();
12181
+ }
12069
12182
  async function main(argv) {
12070
12183
  const args = parse(argv);
12071
12184
  const ctx = {
@@ -12081,6 +12194,12 @@ async function main(argv) {
12081
12194
  out(ctx, appVersion());
12082
12195
  return 0;
12083
12196
  }
12197
+ if (args.flags.help === true && group) {
12198
+ const text = groupHelp(group);
12199
+ if (ctx.json) console.log(JSON.stringify({ help: text.replace(/\u001b\[[0-9;]*m/g, "") }, null, 2));
12200
+ else out(ctx, text);
12201
+ return 0;
12202
+ }
12084
12203
  if (!group || group === "help") {
12085
12204
  const dbFile = require("node:path").join(process.env.YGL_HOME || require("node:path").join(require("node:os").homedir(), ".ygl-trees"), "ygl-trees.db");
12086
12205
  if (!require("node:fs").existsSync(dbFile)) {
@@ -12141,6 +12260,7 @@ async function main(argv) {
12141
12260
  if (!(group === "db" && action === "init") && group !== "plugin" && !require("node:fs").existsSync(file)) {
12142
12261
  bad(ctx, `no database at ${file}`);
12143
12262
  out(ctx, ` run: ygl-trees db init`);
12263
+ out(ctx, ` workspace: ${home}${process.env.YGL_HOME ? " (from YGL_HOME)" : " (default ~/.ygl-trees — set YGL_HOME to switch)"}`);
12144
12264
  return 2;
12145
12265
  }
12146
12266
  const VERSION = appVersion();
@@ -12429,6 +12549,7 @@ function generateLlmsManifest() {
12429
12549
  ["research claim add \"<text>\"", "refutable assertion"],
12430
12550
  ["research claim link <cid> <eid> --stance supports|refutes", "link claim to evidence"],
12431
12551
  ["research claim resolve <cid> --as supported|refuted|contested", "manual arbitration"],
12552
+ ["research claim unresolve <cid>", "reset to unverified (undo link auto-promote)"],
12432
12553
  ["research claim evaluate <cid>", "decision model confidence (0-5 → A/B/C/D)"],
12433
12554
  ["research evidence assess <eid>", "decision model quality assessment"]
12434
12555
  ]
@@ -12593,6 +12714,7 @@ if (require.main === module) main(process.argv.slice(2)).then((rc) => {
12593
12714
  exports.HOME = HOME;
12594
12715
  exports.appVersion = appVersion;
12595
12716
  exports.generateLlmsManifest = generateLlmsManifest;
12717
+ exports.groupHelp = groupHelp;
12596
12718
  exports.loadBundle = loadBundle;
12597
12719
  exports.main = main;
12598
12720
  exports.repoRoot = repoRoot;
@@ -162,7 +162,17 @@ function execute(env, job) {
162
162
  let output = "";
163
163
  const started = Date.now();
164
164
  try {
165
- output = (0, node_child_process_1.execSync)(job.command, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 600_000 });
165
+ // 0.83: the runner process itself holds the app-level write lock; a job
166
+ // command that invokes ygl-trees again would collide with its own parent
167
+ // (self-deadlock — every `ygl-trees …` job failed rc=1 "locked"). Children
168
+ // boot lock-free like webui serve: WAL already permits concurrent readers
169
+ // and serializes writers at the SQLite layer.
170
+ output = (0, node_child_process_1.execSync)(job.command, {
171
+ encoding: "utf8",
172
+ stdio: ["ignore", "pipe", "pipe"],
173
+ timeout: 600_000,
174
+ env: { ...process.env, YGL_SERVE: "1" },
175
+ });
166
176
  rc = 0;
167
177
  }
168
178
  catch (e) {
@@ -72,7 +72,7 @@ function run(env, action, rest, args) {
72
72
  // -- project resolution (allow --p override) -----------------------------
73
73
  let project;
74
74
  try {
75
- project = store.projectResolve(args.flags.p ? String(args.flags.p) : undefined);
75
+ project = store.projectResolve((args.flags.p ?? args.flags.project) ? String(args.flags.p ?? args.flags.project) : undefined);
76
76
  }
77
77
  catch (e) {
78
78
  (0, fmt_1.bad)(ctx, e.message);
@@ -222,14 +222,32 @@ function run(env, action, rest, args) {
222
222
  tags: String(args.flags.tags || ""),
223
223
  };
224
224
  const qRef = String(args.flags.query || "");
225
+ // 0.83 (user report ①): half-write guard — `source add --query X` used
226
+ // to commit the source FIRST and attach separately; a bad query ref
227
+ // left the source landed with a pure error receipt (assets fragment
228
+ // silently). Pre-validate the query ref before ANY write; if the
229
+ // attach still fails post-write, name the landed source + the retry.
230
+ if (qRef) {
231
+ const q = store.db.get("SELECT id FROM queries WHERE (id=? OR text=?) AND project_id=?", [qRef, qRef, project.id]);
232
+ if (!q) {
233
+ (0, fmt_1.bad)(ctx, `query not found: ${(0, text_1.clip)(qRef, 40)} — refusing to write (source add would land without its query link; fix the ref or drop --query)`);
234
+ return 1;
235
+ }
236
+ }
225
237
  return (0, orm_1.getOrm)(env.db.path)
226
238
  .then(async (ds) => {
227
239
  const r = await writes.sourceAdd(ds, project.id, spec);
228
- (0, fmt_1.writeResult)(ctx, r, (row) => `source added: ${row.id} ${(0, text_1.clip)(row.title, 60)}`);
240
+ (0, fmt_1.writeResult)(ctx, r, (row) => `source added: ${row.id} ${(0, text_1.clip)(row.title, 60)} · project ${project.name}`);
229
241
  if (qRef) {
230
- const at = await writes.queryAttach(ds, project.id, qRef, r.row.id);
231
- if (!ctx.json && !at.dup)
232
- (0, fmt_1.out)(ctx, (0, fmt_1.paint)(fmt_1.C.dim, ` attached to query: "${(0, text_1.clip)(at.queryText, 40)}"`, ctx.color));
242
+ try {
243
+ const at = await writes.queryAttach(ds, project.id, qRef, r.row.id);
244
+ if (!ctx.json && !at.dup)
245
+ (0, fmt_1.out)(ctx, (0, fmt_1.paint)(fmt_1.C.dim, ` attached to query: "${(0, text_1.clip)(at.queryText, 40)}"`, ctx.color));
246
+ }
247
+ catch (e) {
248
+ (0, fmt_1.bad)(ctx, `source landed as ${r.row.id} but attach failed: ${e?.message} — research query attach ${(0, text_1.clip)(qRef, 20)} ${r.row.id} to retry`);
249
+ return 1;
250
+ }
233
251
  }
234
252
  return 0;
235
253
  })
@@ -411,7 +429,7 @@ function run(env, action, rest, args) {
411
429
  }
412
430
  const note = String(args.flags.note || "");
413
431
  const locator = String(args.flags.locator || "");
414
- return runWrite(env, (ds) => writes.evidenceAdd(ds, project.id, sourceId, quote, note, locator, force), (r) => `evidence added: ${r.id} ${(0, text_1.clip)(quote, 50)}`);
432
+ return runWrite(env, (ds) => writes.evidenceAdd(ds, project.id, sourceId, quote, note, locator, force), (r) => `evidence added: ${r.id} ${(0, text_1.clip)(quote, 50)} · project ${project.name}`);
415
433
  }
416
434
  // R15.3: evidence assess — decision model evaluates evidence quality
417
435
  if (action === "evidence" && subAction === "assess") {
@@ -504,7 +522,7 @@ function run(env, action, rest, args) {
504
522
  return 2;
505
523
  }
506
524
  const confidence = String(args.flags.confidence || (0, config_1.configGet)(env.db, "default_confidence"));
507
- return runWrite(env, (ds) => writes.claimAdd(ds, project.id, text, confidence, force), (r) => `claim added: ${r.id} ${(0, text_1.clip)(text, 60)}`);
525
+ return runWrite(env, (ds) => writes.claimAdd(ds, project.id, text, confidence, force), (r) => `claim added: ${r.id} ${(0, text_1.clip)(text, 60)} · project ${project.name}`);
508
526
  }
509
527
  if (subAction === "link") {
510
528
  if (!rest[1] || !rest[2]) {
@@ -555,7 +573,17 @@ function run(env, action, rest, args) {
555
573
  return (0, orm_1.getOrm)(env.db.path)
556
574
  .then((ds) => writes.claimLink(ds, rest[1], rest[2], stance))
557
575
  .then((r) => {
558
- (0, fmt_1.ok)(ctx, `linked; claim status now: ${r.status}`);
576
+ // 0.82.1: prediction claims never auto-promote on link — the receipt
577
+ // says so explicitly instead of showing a silently-unchanged status.
578
+ if (ctx.json) {
579
+ console.log(JSON.stringify({ claim: rest[1], evidence: rest[2], stance, status: r.status, auto_promote: r.auto_promote }, null, 2));
580
+ }
581
+ else if (r.auto_promote === false) {
582
+ (0, fmt_1.ok)(ctx, `linked; prediction claim stays ${r.status} — auto-promote skipped (arbitration: claim resolve / deadline audit)`);
583
+ }
584
+ else {
585
+ (0, fmt_1.ok)(ctx, `linked; claim status now: ${r.status}`);
586
+ }
559
587
  // R15 深度集成:链接后自动触发置信度评估(有模型时静默执行)
560
588
  try {
561
589
  const { evaluateClaimConfidence } = require("../../../ops/tools/src/eval-enhance");
@@ -629,6 +657,36 @@ function run(env, action, rest, args) {
629
657
  return 1;
630
658
  });
631
659
  }
660
+ // 0.82.1 (user report ③): the repair channel for link side effects —
661
+ // resets a claim to unverified. Covers predictions auto-promoted before
662
+ // 0.82.1 AND any claim arbitrated too early (explicit un-arbitration).
663
+ if (subAction === "unresolve") {
664
+ const id = rest[1];
665
+ if (!id) {
666
+ (0, fmt_1.bad)(ctx, "usage: research claim unresolve <claimId>");
667
+ return 2;
668
+ }
669
+ const c0 = store.db.get("SELECT id, status FROM claims WHERE id=? AND project_id=?", [id, project.id]);
670
+ if (!c0) {
671
+ (0, fmt_1.bad)(ctx, `claim not found: ${id}`);
672
+ return 1;
673
+ }
674
+ return (0, orm_1.getOrm)(env.db.path)
675
+ .then((ds) => writes.claimUnresolve(ds, id))
676
+ .then((r) => {
677
+ if (ctx.json)
678
+ console.log(JSON.stringify({ id, status: r.status, duplicate: r.duplicate }, null, 2));
679
+ else
680
+ (0, fmt_1.ok)(ctx, r.duplicate
681
+ ? `claim ${id} already unverified (no change)`
682
+ : `claim ${id}: ${c0.status} → unverified (back in tracking; arbitration: claim resolve)`);
683
+ return 0;
684
+ })
685
+ .catch((e) => {
686
+ (0, fmt_1.bad)(ctx, e?.message || String(e));
687
+ return 1;
688
+ });
689
+ }
632
690
  // R15.2: claim evaluate — decision model evaluates confidence from linked evidence
633
691
  if (subAction === "evaluate") {
634
692
  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 = "") {
@@ -60,7 +60,7 @@ function run(env, action, rest, args) {
60
60
  }
61
61
  let project;
62
62
  try {
63
- project = store.projectResolve(args.flags.p ? String(args.flags.p) : undefined);
63
+ project = store.projectResolve((args.flags.p ?? args.flags.project) ? String(args.flags.p ?? args.flags.project) : undefined);
64
64
  }
65
65
  catch (e) {
66
66
  (0, fmt_1.bad)(ctx, e.message);
@@ -204,7 +204,7 @@ function run(env, action, rest, args) {
204
204
  if (ctx.json)
205
205
  console.log(JSON.stringify(row, null, 2));
206
206
  else
207
- (0, fmt_1.ok)(ctx, `task added: ${row.id} ${title}`);
207
+ (0, fmt_1.ok)(ctx, `task added: ${row.id} ${title} · project ${project.name}`);
208
208
  return 0;
209
209
  }
210
210
  // 0.81 ⑦: tasks join the relation fabric — `task add --node` links
@@ -115,7 +115,7 @@ function run(env, action, rest, args) {
115
115
  return 0;
116
116
  }
117
117
  case "show": {
118
- const resolved = store.projectResolve(args.flags.p ? String(args.flags.p) : undefined);
118
+ const resolved = store.projectResolve((args.flags.p ?? args.flags.project) ? String(args.flags.p ?? args.flags.project) : undefined);
119
119
  return (0, orm_1.getOrm)(env.db.path)
120
120
  .then(async (ds) => {
121
121
  const proj = await ds.getRepository(entities_1.Project).findOne({
@@ -61,7 +61,14 @@ const text_1 = require("../../../core/text/src/text");
61
61
  function run(env, action, rest, args) {
62
62
  const { ctx, db } = env;
63
63
  const trees = new reads_1.TreeStore(db);
64
- const sel = args.flags.t ? String(args.flags.t) : undefined;
64
+ // 0.83 (user report ①): --tree long alias alongside -t — pointer-hijack
65
+ // pinning needs an ergonomic, self-documenting form. EXCEPTION: `node
66
+ // import --tree <name>` is that command's own target-tree flag (create or
67
+ // merge destination), never a selector — alias must not shadow it.
68
+ const treeFlagAliasOk = !(action === "import");
69
+ const sel = treeFlagAliasOk
70
+ ? ((args.flags.t ?? args.flags.tree) ? String(args.flags.t ?? args.flags.tree) : undefined)
71
+ : (args.flags.t ? String(args.flags.t) : undefined);
65
72
  const flagNum = (k, d) => {
66
73
  const v = Number(args.flags[k]);
67
74
  return Number.isFinite(v) ? v : d;
@@ -548,6 +555,12 @@ function run(env, action, rest, args) {
548
555
  }
549
556
  r.pending = pendingList;
550
557
  r.evaluated = evaluated;
558
+ // 0.83 (user report ④): snapshot the rel attachments the eval actually
559
+ // consumed — otherwise a passing P value can't be reproduced without
560
+ // digging the db. Byte-compat: field only when attachments exist.
561
+ const atts = contextPkg.attachments;
562
+ if (Array.isArray(atts) && atts.length)
563
+ r.eval_attachments = atts;
551
564
  r.ok = r.missing_required.length === 0 && r.blocked_by.length === 0 && pendingList.length === 0;
552
565
  try {
553
566
  const run = process.env.YGL_RUN_ID || "";
@@ -623,6 +636,18 @@ function run(env, action, rest, args) {
623
636
  }
624
637
  for (const e of r.evaluated)
625
638
  (0, fmt_1.out)(ctx, (0, fmt_1.paint)(fmt_1.C.dim, ` eval ${e.name}: P=${e.p ?? "-"} → ${e.outcome}${e.reason ? ` (${e.reason})` : ""}`, ctx.color));
639
+ // 0.83 ④: what the model actually ate this run (rel fabric snapshot)
640
+ if (Array.isArray(r.eval_attachments) && r.eval_attachments.length) {
641
+ (0, fmt_1.out)(ctx, (0, fmt_1.paint)(fmt_1.C.dim, ` attachments ×${r.eval_attachments.length}:`, ctx.color));
642
+ for (const a of r.eval_attachments) {
643
+ const label = a.type === "claim" ? `${a.id}(${a.status})`
644
+ : a.type === "evidence" ? `${a.id} "${(0, text_1.clip)(a.quote, 36)}"`
645
+ : a.type === "kb" || a.type === "source" ? `${a.id} ${(0, text_1.clip)(a.title, 30)}`
646
+ : a.type === "node" ? `${a.key ?? a.id}`
647
+ : String(a.id);
648
+ (0, fmt_1.out)(ctx, (0, fmt_1.paint)(fmt_1.C.dim, ` - ${a.rel} ← ${a.type}:${label}`, ctx.color));
649
+ }
650
+ }
626
651
  if (r.cost_multiplier !== 1)
627
652
  (0, fmt_1.out)(ctx, ` 成本系数 ${r.cost_multiplier}`);
628
653
  return 0;
@@ -819,11 +844,11 @@ function run(env, action, rest, args) {
819
844
  if (sub === "list" || sub === undefined) {
820
845
  // Contract ⑤ extension (user report: `node list --tree X` silently
821
846
  // swallowed the unknown flag and listed the CURRENT tree). node list
822
- // takes exactly -t <tree> / --json — any other flag is a typo worth
823
- // an explicit rc=2 instead of silent misdirection.
847
+ // takes exactly -t/--tree <tree> / --json — any other flag is a typo
848
+ // worth an explicit rc=2 instead of silent misdirection.
824
849
  for (const f of Object.keys(args.flags || {})) {
825
- if (!["t", "json", "no-color"].includes(f)) {
826
- (0, fmt_1.bad)(ctx, `unknown flag --${f} for 'node list' — tree selection is '-t <tree>' (positional <tree> also works)`);
850
+ if (!["t", "tree", "json", "no-color"].includes(f)) {
851
+ (0, fmt_1.bad)(ctx, `unknown flag --${f} for 'node list' — tree selection is '-t|--tree <tree>' (positional <tree> also works)`);
827
852
  return 2;
828
853
  }
829
854
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ygl-trees",
3
- "version": "0.82.0",
3
+ "version": "0.83.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,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/cosmokit": "~1.8.6-alpha.2",
30
29
  "@deepseek-ai/schemastery": "~3.18.5-alpha.2",
30
+ "@deepseek-ai/cosmokit": "~1.8.6-alpha.2",
31
31
  "@deepseek-ai/cordis-plugin-timer": "~1.1.7-alpha.2"
32
32
  },
33
33
  "devDependencies": {
@@ -66,6 +66,8 @@ ygl-trees node can gate-node --eval --json # evaluated[] 带 type 字段 + p
66
66
  claim(text+status+confidence)、evidence(quote 截 120 字)、kb/source(标题)、node(key+name)、
67
67
  其余类型 id-only。裁决链 `decided-by` 挂好后 eval 模型直接可见,不必 `--state k=v` 手动携带事实
68
68
  (用户实测:不挂链 P=0.30,挂链后 0.92)。
69
+ **快照回显(0.83)**:`node can <key> --eval` 的输出把本次吃到的 attachments 打回来——`--json` 的
70
+ `eval_attachments[]` + 人类视图 `attachments ×N:` 逐条;P 值依据可复现,不必翻库。
69
71
 
70
72
  ## 5. 凭证与端点(优先级:config > 环境变量/文件 > 默认网关)
71
73
 
@@ -67,12 +67,16 @@ ygl-trees webui serve --help # 只打印用法不启服务(0.79.1 短路)
67
67
 
68
68
  PG 迁移预检:`db migrate pg` 前自动逐表扫描 INTEGER 列类型——非整数值会**点名 table.column rowid=N** 而非迁移时才爆。
69
69
 
70
- ## 2b2. 多会话安全(0.79)
70
+ ## 2b2. 多会话安全(0.79 → 0.83)
71
71
 
72
+ - **0.83 并发钉住(最重要)**:并发会话共享 current 指针互踩——写命令直接钉住目标:`research * --project <p>`、`produce/task * --project <p>`、`node * --tree <t>`(`node import --tree` 是目标树名,非选择器);写回执带 `· project <名>` 回显落点
72
73
  - `tree use` / `project use` 落 `tree.use` / `project.use` 事件(`YGL_SESSION` 会话标识)
73
74
  - `agent brief` 的 `selection_changed`:最近 10 分钟选择变更;跨会话变更置顶提示带 `-t`/`-p`
74
75
  - `kb add` 回执自检(`verified:true`)+ 工作区回显(防写错 home 的"丢笔记"事故)
75
76
  - `research claim link <cid> <sid> --by-source`:来源下全部证据一次挂齐;误传来源 id 会列出其证据 id
77
+ - `research source add --query` 引用不存在 → 整体拒绝 rc=1 零落地(0.83 半写防护)
78
+ - `--help` 任意深度打组帮助 rc=0(如 `kb add --help`);job 子命令无锁启动(`ygl-trees …` 型任务命令不再与 runner 死锁)
79
+ - 到期巡检范式:`job add "到期巡检" --every 1440 -- ygl-trees node cond --due --days 30`
76
80
 
77
81
  ## 2c. DB 锁行为(0.68 → 0.79.1)
78
82
 
@@ -34,6 +34,7 @@ ygl-trees research source add --adjudication "会话内裁决:三条件已逐
34
34
 
35
35
  来源按 `(project, url_hash)` 去重;`--query` 顺带建立召回关联(query list 显示召回数)。
36
36
  `--url` 收任意 `<scheme>://`(https/file/session/a2a/…,0.81)——跨 Agent 引用免落盘。
37
+ **并发钉住(0.83)**:全组接受 `--project <p>` 长旗标(与 `-p` 等价)——并发会话共享 current 指针互踩时直接钉住目标项目,回执带 `· project <名>` 回显落点;`--query` 引用不存在 → 整体拒绝 rc=1 零落地(半写防护)。
37
38
 
38
39
  ## 2. 摘录与断言
39
40
 
@@ -42,11 +43,13 @@ ygl-trees research evidence add <sourceId> --quote "原文片段" --locator "§2
42
43
  ygl-trees research evidence add <sourceId> --file quotes.md # 长摘录免转义
43
44
  ygl-trees research claim add "断言文本" --confidence A # 档位
44
45
  ygl-trees research claim add "断言文本" --confidence 0.92 # 数值档(0.81:0-1 或 0-100%)
45
- ygl-trees research claim link <claimId> <evidenceId> [--stance refutes] # 状态自动推导
46
+ ygl-trees research claim link <claimId> <evidenceId> [--stance refutes] # 状态自动推导(普通断言)
46
47
  ygl-trees research claim resolve <claimId> --as contested --note "矛盾待查" # 人工仲裁出口
48
+ ygl-trees research claim unresolve <claimId> # 复位回 unverified(0.82.1:撤销链接副作用/抢跑仲裁)
47
49
  ```
48
50
 
49
51
  - 有支持有反对 → `contested`;仲裁后新证据链接会重新推导
52
+ - **预测断言(`[预测·…]` 前缀)挂证据不自动晋升**(0.82.1):挂证据 ≠ 仲裁,状态保持 unverified 直到显式 resolve / 到期回测——predict audit 才不会把刚挂证据的预测误判已结案;误晋升用 `claim unresolve` 归位(幂等 duplicate:true)
50
53
  - 全部命令幂等:同内容重跑返回已存在行(`duplicate: true`,rc=0)
51
54
 
52
55
  ## 3. 产出
@@ -65,6 +68,7 @@ ygl-trees produce export <reportId> report.md
65
68
  - 先 `agent brief --json` 看 next_actions(它会告诉你缺来源/缺证据/缺断言)
66
69
  - 每条断言至少一条证据再进报告;contested 的要么补证据要么 resolve
67
70
  - 预测断言(`[预测·N天]`)定期 `research predict audit` 回测:本地判据自动测;`--state <file|url>`
68
- 桥接外部计数(裸键名后缀匹配 `counts.y_imported`);已 resolve 的默认隐藏(`--all` 查看终态表)
71
+ 桥接外部计数(裸键名后缀匹配 `counts.y_imported`);已 resolve 的默认隐藏(`--all` 查看终态表,
72
+ `--json` 始终含全量行含终态——机器读全量、人看增量)
69
73
  - `--json` 输出可直接 parse 拿 id;列表命令字段:source(id,title,url,relevance,…)、
70
74
  claim(id,text,status,confidence,sup,ref)、evidence(id,source,locator,quote,used_by)
@@ -54,6 +54,8 @@ ygl-trees node unlock mining && ygl-trees node lock mining
54
54
  - `--state-file` 两种形状:扁平 `{"y_imported":40}` 全节点共享;按节点 `{"a":{"y_imported":40}}` 各取各袋;多键 `--json` 返回数组(0.79.1)
55
55
  - `ready --all` / `cond --due` 前置于树解析:无当前树也能跑,跨树巡检一条命令(0.82)
56
56
  - `cond --due` 提取 eval 问题文本中的 `YYYY-MM-DD`;默认 7 天窗口(含逾期),`--eval` 判定后仍需人工 resolve/unlock——判定 ≠ 仲裁(0.82)
57
+ - **`--tree <t>` 长旗标(0.83)**:node 组树选择别名(与 `-t` 等价)——并发会话互踩 current_tree 时钉住目标;例外:`node import --tree <name>` 是导入目标树名,不是选择器
58
+ - **`can --eval` attachments 快照(0.83)**:`--json` 增 `eval_attachments[]`、人类视图 `attachments ×N:`——本次判定吃了哪些 rel 证据一目了然,P 值复现不必翻库
57
59
 
58
60
  ## 3. 迁移与保真
59
61