ygl-trees 0.81.0 → 0.82.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 +6 -0
- package/README.md +7 -2
- package/dist/apps/cli/src/bin.js +4 -1
- package/dist/bundle/bin.cjs +214 -27
- package/dist/packages/ops/tools/src/doctor.js +43 -11
- package/dist/packages/techtree/model/src/cmds.js +101 -0
- package/package.json +3 -3
- package/skills/ygl-trees-quickstart/SKILL.md +1 -1
- package/skills/ygl-trees-techtree/SKILL.md +5 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.82.0(2026-10-17)跨树巡检 + 临期 eval 门槛 + 幻影对账一键修复
|
|
4
|
+
- **`node ready --all`**(用户建议②:12 棵树巡检要 24 条命令——每树 tree use + ready):一次进程扫全部树的就绪集,无需 tree use / `-t`;`--json` 返回 `[{tree, ready[]}]`,人类输出按树分节
|
|
5
|
+
- **`node cond --due [--days N] [--eval]`**(建议③:w1/d90 检验依赖人记得看):扫描全部 eval 族条件,提取问题文本中的 `YYYY-MM-DD` 期限,列出 N 天内到期(含逾期)的条件(树/节点/deadline/剩余天数);`--days` 放宽窗口(默认 7);`--eval` 逐条组装上下文包送判定模型(三题型按 op 自动选择,离线安全回落 pending)——**判定只报告不仲裁**,通过 ≠ 自动解锁,resolve/unlock 保持人工(与 predict audit 同纪律);跨树扫描前置于树解析,无当前树也能跑
|
|
6
|
+
- **`doctor --fix` 覆盖 write_integrity**(上报①:幻影写入事件只能围观不能修):`--fix` 把已知幻影事件标记 `reconciled`(payload 增补时间戳,扫描豁免),审计轨迹保留不删事件;warn 文案指路。实测真实工作区:10-16 恢复实验造成的 10 个幻影(kb 47d0d78b18、source f0ad79350f 等)一键对账回 pass
|
|
7
|
+
- 测试:demo 5i 段 7 断言(跨树 ready / 期限提取 / --days 窗口 / --eval 离线 pending / 幻影 warn→fix→pass 全循环)→ 101;`readme-verify.sh` +2(ready --all、cond --due)→ 41
|
|
8
|
+
|
|
3
9
|
## 0.81.0(2026-10-17)命令面五处修正 + task 挂树 + 事件流对账
|
|
4
10
|
- **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
11
|
- **`--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 的差距由挂链补平)
|
|
@@ -289,7 +294,7 @@ ygl-trees agent ask "<question>" natural language interface
|
|
|
289
294
|
## 五b、开发与运维工具
|
|
290
295
|
|
|
291
296
|
```bash
|
|
292
|
-
pnpm test # 四段制回归:unit (vitest 172 例) + build (tsc + tsdown) + demo(
|
|
297
|
+
pnpm test # 四段制回归:unit (vitest 172 例) + build (tsc + tsdown) + demo(101 条断言)+ contract:audit(86 项)
|
|
293
298
|
pnpm bench # 性能基准七席(startup/写路径/检索/索引),每席含 RSS 内存端点 → tmp/bench-results.json
|
|
294
299
|
node benchmarks/bench.js --quick # 缩小规模快速跑
|
|
295
300
|
pnpm pack:check # 发布就绪门禁:打包 → 隔离安装 → 驱动发布产物跑命令面 + --json 纯净性
|
|
@@ -313,7 +318,7 @@ serve。serve 从不取写锁(WAL 并发读写安全),`db unlock` 也以
|
|
|
313
318
|
**多会话安全(0.79)**:`tree use` / `project use` 全部落 `tree.use` / `project.use` 事件(带
|
|
314
319
|
`YGL_SESSION` 会话标识);`agent brief` 的 `selection_changed` 字段暴露最近 10 分钟的选择变更,
|
|
315
320
|
跨会话变更会置顶提示命令带 `-t/-p` 固定上下文。`kb add` 回执自带 `verified` 自检与工作区回显,
|
|
316
|
-
`doctor` 第 10 项回放近 2 天 kb/source/evidence
|
|
321
|
+
`doctor` 第 10 项回放近 2 天 kb/source/evidence 写事件对账基表,幻影写入当场点名。`doctor --fix`(0.82)把已知幻影标记 `reconciled`(payload 留痕、扫描跳过、审计轨迹保留)——db 恢复/导入实验造成的旧幻影一键对账。
|
|
317
322
|
|
|
318
323
|
**提交门禁(无 CI 环境的等效方案)**:`.githooks/pre-push` —— push 分支跑 `pnpm test`,
|
|
319
324
|
**push tag 追加 `pnpm pack:check`**;旁路只能显式 `YGL_SKIP_HOOKS=1`。安装一次:
|
package/dist/apps/cli/src/bin.js
CHANGED
|
@@ -210,6 +210,8 @@ ${(0, fmt_1.paint)(fmt_1.C.bold, "tree / node", true)} tech-tree node model
|
|
|
210
210
|
(--eval: eval 题型经决策模型判定;
|
|
211
211
|
boolean P / choice P(期望答案) / score ±1 灰带)
|
|
212
212
|
node ready [--state k=v] [--eval] currently unlockable set
|
|
213
|
+
(--all: sweep every tree — no tree use needed)
|
|
214
|
+
node cond --due [--days n] [--eval] eval gates with YYYY-MM-DD deadlines due in n days
|
|
213
215
|
node path <key> dependency-order path to target (all locked ancestors; OR branches not optimized)
|
|
214
216
|
node parents|children <key> direct relations
|
|
215
217
|
node ancestors|descendants <key> transitive, via closure index
|
|
@@ -694,7 +696,8 @@ function generateLlmsManifest() {
|
|
|
694
696
|
["node cond <key> --name <c> --kind <k> [--op eval] [--value \"<question>\"]", "add condition (eval = decision model gate)"],
|
|
695
697
|
["node index", "rebuild closure + topo order"],
|
|
696
698
|
["node can <key> [--state k=v] [--eval]", "can unlock? (required vs optional + cost)"],
|
|
697
|
-
["node ready", "currently unlockable set"],
|
|
699
|
+
["node ready [--all]", "currently unlockable set (--all: every tree)"],
|
|
700
|
+
["node cond --due [--days n] [--eval]", "eval gates with deadlines due in n days"],
|
|
698
701
|
["node path <key>", "dependency-order path (OR branches not optimized)"],
|
|
699
702
|
["node unlock <key>", "mark unlocked"],
|
|
700
703
|
["node list", "list with state column (locked/unlocked)"],
|
package/dist/bundle/bin.cjs
CHANGED
|
@@ -8810,6 +8810,167 @@ function run$4(env, action, rest, args) {
|
|
|
8810
8810
|
if (positionalTree) rest = rest.slice(1);
|
|
8811
8811
|
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
8812
|
}
|
|
8813
|
+
if (env.group === "node" && action === "cond" && args?.flags?.due === true) {
|
|
8814
|
+
const days = flagNum("days", 7);
|
|
8815
|
+
const ctxName = flagStr("context", "default");
|
|
8816
|
+
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
|
|
8817
|
+
FROM node_conditions nc JOIN nodes n ON n.id=nc.node_id JOIN trees t ON t.id=n.tree_id
|
|
8818
|
+
WHERE nc.op LIKE 'eval%' ORDER BY t.name, n.key`);
|
|
8819
|
+
const now = Date.now();
|
|
8820
|
+
const due = [];
|
|
8821
|
+
for (const c of conds) {
|
|
8822
|
+
const spec = parseEvalSpec(c.value);
|
|
8823
|
+
const q = spec.q || String(c.name);
|
|
8824
|
+
const m = q.match(/(20\d{2})-(\d{2})-(\d{2})/);
|
|
8825
|
+
if (!m) continue;
|
|
8826
|
+
const dl = Date.parse(`${m[1]}-${m[2]}-${m[3]}T23:59:59Z`);
|
|
8827
|
+
if (!Number.isFinite(dl) || dl > now + days * 864e5) continue;
|
|
8828
|
+
due.push({
|
|
8829
|
+
tree: c.tree,
|
|
8830
|
+
tree_id: c.tree_id,
|
|
8831
|
+
node: c.node_key,
|
|
8832
|
+
node_id: c.node_id,
|
|
8833
|
+
name: String(c.name),
|
|
8834
|
+
op: String(c.op),
|
|
8835
|
+
question: q,
|
|
8836
|
+
envelope: spec,
|
|
8837
|
+
deadline: `${m[1]}-${m[2]}-${m[3]}`,
|
|
8838
|
+
days_left: Math.ceil((dl - now) / 864e5)
|
|
8839
|
+
});
|
|
8840
|
+
}
|
|
8841
|
+
due.sort((a, b) => a.days_left - b.days_left);
|
|
8842
|
+
if (args?.flags?.eval !== true) {
|
|
8843
|
+
if (ctx.json) console.log(JSON.stringify(due, null, 2));
|
|
8844
|
+
else {
|
|
8845
|
+
head(ctx, `临期 eval 门槛 × ${due.length}(${days} 天窗口)`);
|
|
8846
|
+
if (!due.length) out(ctx, paint(C.dim, " (无临期 eval 条件)", ctx.color));
|
|
8847
|
+
table(ctx, due.map((d) => ({
|
|
8848
|
+
tree: d.tree,
|
|
8849
|
+
node: d.node,
|
|
8850
|
+
cond: d.name,
|
|
8851
|
+
deadline: d.deadline,
|
|
8852
|
+
剩余天数: d.days_left,
|
|
8853
|
+
question: require_text.clip(d.question, 40)
|
|
8854
|
+
})), [
|
|
8855
|
+
"tree",
|
|
8856
|
+
"node",
|
|
8857
|
+
"cond",
|
|
8858
|
+
"deadline",
|
|
8859
|
+
"剩余天数",
|
|
8860
|
+
"question"
|
|
8861
|
+
], [
|
|
8862
|
+
"tree",
|
|
8863
|
+
"node",
|
|
8864
|
+
"cond",
|
|
8865
|
+
"deadline",
|
|
8866
|
+
"剩余天数",
|
|
8867
|
+
"question"
|
|
8868
|
+
]);
|
|
8869
|
+
}
|
|
8870
|
+
return 0;
|
|
8871
|
+
}
|
|
8872
|
+
const offline = !!process.env.YGL_EVAL_OFFLINE || configGetBool(env.db, "eval.offline");
|
|
8873
|
+
const cfgToken = configGet(env.db, "decision_model_llm_token").trim();
|
|
8874
|
+
const token = offline ? "" : cfgToken || evalToken();
|
|
8875
|
+
const endpoint = configGet(env.db, "decision_model_llm_api_base").trim() || "https://ai-gateway.vercel.sh/v4/ai/evaluation-model";
|
|
8876
|
+
const passT = configGetNumber(env.db, "eval.pass_threshold");
|
|
8877
|
+
const failT = configGetNumber(env.db, "eval.fail_threshold");
|
|
8878
|
+
return (async () => {
|
|
8879
|
+
const rows = [];
|
|
8880
|
+
for (const d of due) {
|
|
8881
|
+
const res = await evalMixed(trees.evalContext(d.tree_id, d.node, {}, ctxName), [d.op === "eval-choice" ? {
|
|
8882
|
+
type: "choice",
|
|
8883
|
+
question: d.question,
|
|
8884
|
+
options: d.envelope.options || {},
|
|
8885
|
+
answer: d.envelope.answer || ""
|
|
8886
|
+
} : d.op === "eval-score" ? {
|
|
8887
|
+
type: "score",
|
|
8888
|
+
question: d.question,
|
|
8889
|
+
levels: d.envelope.levels || [],
|
|
8890
|
+
min: d.envelope.min ?? 0
|
|
8891
|
+
} : {
|
|
8892
|
+
type: "boolean",
|
|
8893
|
+
question: d.question
|
|
8894
|
+
}], token, endpoint);
|
|
8895
|
+
if (!res.ok) {
|
|
8896
|
+
rows.push({
|
|
8897
|
+
...d,
|
|
8898
|
+
outcome: "pending",
|
|
8899
|
+
reason: res.error
|
|
8900
|
+
});
|
|
8901
|
+
continue;
|
|
8902
|
+
}
|
|
8903
|
+
const r = res.results[0];
|
|
8904
|
+
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);
|
|
8905
|
+
rows.push({
|
|
8906
|
+
...d,
|
|
8907
|
+
...m
|
|
8908
|
+
});
|
|
8909
|
+
}
|
|
8910
|
+
if (ctx.json) console.log(JSON.stringify(rows, null, 2));
|
|
8911
|
+
else {
|
|
8912
|
+
head(ctx, `临期 eval 门槛 — 已判定 × ${rows.length}`);
|
|
8913
|
+
table(ctx, rows.map((d) => ({
|
|
8914
|
+
tree: d.tree,
|
|
8915
|
+
node: d.node,
|
|
8916
|
+
deadline: d.deadline,
|
|
8917
|
+
outcome: d.outcome,
|
|
8918
|
+
p: d.p ?? d.score ?? "",
|
|
8919
|
+
question: require_text.clip(d.question, 36)
|
|
8920
|
+
})), [
|
|
8921
|
+
"tree",
|
|
8922
|
+
"node",
|
|
8923
|
+
"deadline",
|
|
8924
|
+
"判定",
|
|
8925
|
+
"p",
|
|
8926
|
+
"question"
|
|
8927
|
+
], [
|
|
8928
|
+
"tree",
|
|
8929
|
+
"node",
|
|
8930
|
+
"deadline",
|
|
8931
|
+
"outcome",
|
|
8932
|
+
"p",
|
|
8933
|
+
"question"
|
|
8934
|
+
]);
|
|
8935
|
+
out(ctx, paint(C.dim, " 判定只报告不仲裁:通过 ≠ 自动解锁(resolve/unlock 保持人工)", ctx.color));
|
|
8936
|
+
}
|
|
8937
|
+
return 0;
|
|
8938
|
+
})();
|
|
8939
|
+
}
|
|
8940
|
+
if (env.group === "node" && action === "ready" && args?.flags?.all === true) {
|
|
8941
|
+
const state = parseState(args);
|
|
8942
|
+
const ctxName = flagStr("context", "default");
|
|
8943
|
+
const all = db.all("SELECT id, name FROM trees ORDER BY created");
|
|
8944
|
+
const per = [];
|
|
8945
|
+
for (const t of all) {
|
|
8946
|
+
const rows = trees.ready(t.id, ctxName, state);
|
|
8947
|
+
if (rows.length) per.push({
|
|
8948
|
+
tree: t.name,
|
|
8949
|
+
ready: rows
|
|
8950
|
+
});
|
|
8951
|
+
}
|
|
8952
|
+
if (ctx.json) {
|
|
8953
|
+
console.log(JSON.stringify(per, null, 2));
|
|
8954
|
+
return 0;
|
|
8955
|
+
}
|
|
8956
|
+
head(ctx, `Ready across ${all.length} trees`);
|
|
8957
|
+
if (!per.length) out(ctx, paint(C.dim, " (no tree has ready nodes)", ctx.color));
|
|
8958
|
+
for (const p of per) {
|
|
8959
|
+
out(ctx, paint(C.bold, ` ${p.tree} (${p.ready.length})`, ctx.color));
|
|
8960
|
+
table(ctx, p.ready, [
|
|
8961
|
+
"key",
|
|
8962
|
+
"name",
|
|
8963
|
+
"tier",
|
|
8964
|
+
"cost"
|
|
8965
|
+
], [
|
|
8966
|
+
"key",
|
|
8967
|
+
"name",
|
|
8968
|
+
"tier",
|
|
8969
|
+
"cost"
|
|
8970
|
+
]);
|
|
8971
|
+
}
|
|
8972
|
+
return 0;
|
|
8973
|
+
}
|
|
8813
8974
|
let tree;
|
|
8814
8975
|
try {
|
|
8815
8976
|
tree = positionalTree || trees.treeResolve(sel);
|
|
@@ -11589,40 +11750,42 @@ function run(env, action, rest, args) {
|
|
|
11589
11750
|
detail: `profile read failed: ${e?.message || e}`
|
|
11590
11751
|
});
|
|
11591
11752
|
}
|
|
11753
|
+
const PHANTOM_SPECS = [
|
|
11754
|
+
{
|
|
11755
|
+
kind: "kb.add",
|
|
11756
|
+
table: "kb_notes",
|
|
11757
|
+
label: "kb",
|
|
11758
|
+
rm: "kb.rm"
|
|
11759
|
+
},
|
|
11760
|
+
{
|
|
11761
|
+
kind: "source.add",
|
|
11762
|
+
table: "sources",
|
|
11763
|
+
label: "source",
|
|
11764
|
+
rm: "source.rm"
|
|
11765
|
+
},
|
|
11766
|
+
{
|
|
11767
|
+
kind: "evidence.add",
|
|
11768
|
+
table: "evidence",
|
|
11769
|
+
label: "evidence",
|
|
11770
|
+
rm: "evidence.rm"
|
|
11771
|
+
}
|
|
11772
|
+
];
|
|
11773
|
+
const phantomSql = (spec) => `SELECT e.entity_id id, e.ts FROM events e
|
|
11774
|
+
WHERE e.kind='${spec.kind}'
|
|
11775
|
+
AND e.ts >= datetime('now', '-2 days')
|
|
11776
|
+
AND e.payload NOT LIKE '%"reconciled"%'
|
|
11777
|
+
AND NOT EXISTS (SELECT 1 FROM ${spec.table} t WHERE t.id = e.entity_id)
|
|
11778
|
+
AND NOT EXISTS (SELECT 1 FROM events r WHERE r.kind='${spec.rm}' AND r.entity_id = e.entity_id)`;
|
|
11592
11779
|
{
|
|
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
11780
|
const phantoms = [];
|
|
11614
11781
|
for (const spec of PHANTOM_SPECS) {
|
|
11615
|
-
const rows = db.all(
|
|
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)`);
|
|
11782
|
+
const rows = db.all(phantomSql(spec));
|
|
11620
11783
|
for (const r of rows) phantoms.push(`${spec.label} ${r.id} (${String(r.ts).slice(0, 16)})`);
|
|
11621
11784
|
}
|
|
11622
11785
|
checks.push({
|
|
11623
11786
|
name: "write_integrity",
|
|
11624
11787
|
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"
|
|
11788
|
+
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
11789
|
});
|
|
11627
11790
|
}
|
|
11628
11791
|
const failed = checks.filter((c) => c.status === "fail").length;
|
|
@@ -11665,6 +11828,27 @@ function run(env, action, rest, args) {
|
|
|
11665
11828
|
fixes.push("stale lock removed (holder pid dead)");
|
|
11666
11829
|
} catch {}
|
|
11667
11830
|
}
|
|
11831
|
+
for (const spec of PHANTOM_SPECS) {
|
|
11832
|
+
const rows = db.all(phantomSql(spec));
|
|
11833
|
+
if (!rows.length) continue;
|
|
11834
|
+
let marked = 0;
|
|
11835
|
+
for (const r of rows) try {
|
|
11836
|
+
const cur = db.get("SELECT payload FROM events WHERE kind=? AND entity_id=? LIMIT 1", [spec.kind, r.id]);
|
|
11837
|
+
let payload = "{}";
|
|
11838
|
+
try {
|
|
11839
|
+
payload = cur?.payload && String(cur.payload).trim().startsWith("{") ? String(cur.payload) : "{}";
|
|
11840
|
+
} catch {}
|
|
11841
|
+
const obj = JSON.parse(payload);
|
|
11842
|
+
obj.reconciled = (/* @__PURE__ */ new Date()).toISOString();
|
|
11843
|
+
db.run("UPDATE events SET payload=? WHERE kind=? AND entity_id=?", [
|
|
11844
|
+
JSON.stringify(obj),
|
|
11845
|
+
spec.kind,
|
|
11846
|
+
r.id
|
|
11847
|
+
]);
|
|
11848
|
+
marked++;
|
|
11849
|
+
} catch {}
|
|
11850
|
+
if (marked) fixes.push(`write_integrity: ${marked} ${spec.label} phantom event(s) marked reconciled (audit trail kept)`);
|
|
11851
|
+
}
|
|
11668
11852
|
if (fixes.length) for (const f of fixes) ok(ctx, ` fixed: ${f}`);
|
|
11669
11853
|
else out(ctx, paint(C.dim, " (--fix: no auto-fixable issues found)", ctx.color));
|
|
11670
11854
|
}
|
|
@@ -11842,6 +12026,8 @@ ${paint(C.bold, "tree / node", true)} tech-tree node model
|
|
|
11842
12026
|
(--eval: eval 题型经决策模型判定;
|
|
11843
12027
|
boolean P / choice P(期望答案) / score ±1 灰带)
|
|
11844
12028
|
node ready [--state k=v] [--eval] currently unlockable set
|
|
12029
|
+
(--all: sweep every tree — no tree use needed)
|
|
12030
|
+
node cond --due [--days n] [--eval] eval gates with YYYY-MM-DD deadlines due in n days
|
|
11845
12031
|
node path <key> dependency-order path to target (all locked ancestors; OR branches not optimized)
|
|
11846
12032
|
node parents|children <key> direct relations
|
|
11847
12033
|
node ancestors|descendants <key> transitive, via closure index
|
|
@@ -12291,7 +12477,8 @@ function generateLlmsManifest() {
|
|
|
12291
12477
|
["node cond <key> --name <c> --kind <k> [--op eval] [--value \"<question>\"]", "add condition (eval = decision model gate)"],
|
|
12292
12478
|
["node index", "rebuild closure + topo order"],
|
|
12293
12479
|
["node can <key> [--state k=v] [--eval]", "can unlock? (required vs optional + cost)"],
|
|
12294
|
-
["node ready", "currently unlockable set"],
|
|
12480
|
+
["node ready [--all]", "currently unlockable set (--all: every tree)"],
|
|
12481
|
+
["node cond --due [--days n] [--eval]", "eval gates with deadlines due in n days"],
|
|
12295
12482
|
["node path <key>", "dependency-order path (OR branches not optimized)"],
|
|
12296
12483
|
["node unlock <key>", "mark unlocked"],
|
|
12297
12484
|
["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(
|
|
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}`);
|
|
@@ -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.
|
|
3
|
+
"version": "0.82.0",
|
|
4
4
|
"description": "猿光 · 科技树 — database-centric CLI for research → production → knowledge reuse, built for AI agents.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "dist/apps/cli/index.js",
|
|
@@ -26,9 +26,9 @@
|
|
|
26
26
|
"reflect-metadata": "^0.2.2",
|
|
27
27
|
"typeorm": "^1.1.2",
|
|
28
28
|
"@deepseek-ai/cordis": "~4.0.5-alpha.2",
|
|
29
|
+
"@deepseek-ai/cosmokit": "~1.8.6-alpha.2",
|
|
29
30
|
"@deepseek-ai/schemastery": "~3.18.5-alpha.2",
|
|
30
|
-
"@deepseek-ai/cordis-plugin-timer": "~1.1.7-alpha.2"
|
|
31
|
-
"@deepseek-ai/cosmokit": "~1.8.6-alpha.2"
|
|
31
|
+
"@deepseek-ai/cordis-plugin-timer": "~1.1.7-alpha.2"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@types/node": "^20.0.0",
|
|
@@ -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
|
|
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 可访问
|
|
@@ -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
|
|