ygl-trees 0.80.0 → 0.81.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 +11 -0
- package/README.md +19 -14
- package/dist/bundle/bin.cjs +771 -632
- package/dist/packages/knowledge/skills/src/cmds.js +27 -0
- package/dist/packages/ops/tools/src/db-cmds.js +29 -4
- package/dist/packages/ops/tools/src/doctor.js +11 -6
- package/dist/packages/ops/tools/src/rels.js +16 -2
- package/dist/packages/research/pipeline/src/cmds.js +18 -4
- package/dist/packages/research/produce/src/produce-cmds.js +23 -8
- package/dist/packages/techtree/model/src/cmds.js +35 -5
- package/dist/packages/techtree/model/src/writes.js +23 -0
- package/package.json +3 -3
- package/skills/ygl-trees-quickstart/SKILL.md +3 -1
- package/skills/ygl-trees-research/SKILL.md +4 -2
- package/skills/ygl-trees-techtree/SKILL.md +4 -3
|
@@ -40,6 +40,7 @@ const fs = __importStar(require("node:fs"));
|
|
|
40
40
|
const path = __importStar(require("node:path"));
|
|
41
41
|
const fmt_1 = require("../../../core/text/src/fmt");
|
|
42
42
|
const text_1 = require("../../../core/text/src/text");
|
|
43
|
+
const eventbus_1 = require("../../../core/text/src/eventbus");
|
|
43
44
|
const orm_1 = require("../../../core/database/src/orm");
|
|
44
45
|
const writes = __importStar(require("../../../research/pipeline/src/writes"));
|
|
45
46
|
const reads_1 = require("../../../research/pipeline/src/reads");
|
|
@@ -178,6 +179,32 @@ function run(env, action, rest, args) {
|
|
|
178
179
|
}
|
|
179
180
|
store.db.run("DELETE FROM kb_fts WHERE id=?", [id]);
|
|
180
181
|
store.db.run("DELETE FROM kb_notes WHERE id=?", [id]);
|
|
182
|
+
// 0.81: kb.rm event — doctor check 10 replays *.add events against base
|
|
183
|
+
// tables; a legit removal used to look like a phantom write. Logging the
|
|
184
|
+
// rm (mirroring source/evidence rm paths) lets doctor reconcile.
|
|
185
|
+
try {
|
|
186
|
+
const ev = {
|
|
187
|
+
id: `ev${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`,
|
|
188
|
+
ts: new Date().toISOString(),
|
|
189
|
+
actor: process.env.YGL_SESSION || "cli",
|
|
190
|
+
kind: "kb.rm",
|
|
191
|
+
entity: "kb_note",
|
|
192
|
+
entity_id: id,
|
|
193
|
+
payload: JSON.stringify({ title: String(note.title || "") }),
|
|
194
|
+
level: "info",
|
|
195
|
+
};
|
|
196
|
+
store.db.run("INSERT INTO events(id,ts,actor,kind,entity,entity_id,payload,level) VALUES(?,?,?,?,?,?,?,?)", [ev.id, ev.ts, ev.actor, ev.kind, ev.entity, ev.entity_id, ev.payload, ev.level]);
|
|
197
|
+
(0, eventbus_1.emitEvent)({
|
|
198
|
+
kind: "kb.rm",
|
|
199
|
+
entity: "kb_note",
|
|
200
|
+
entityId: id,
|
|
201
|
+
payload: JSON.stringify({ title: String(note.title || "") }),
|
|
202
|
+
ts: ev.ts,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
// event logging is best-effort — the delete itself already succeeded
|
|
207
|
+
}
|
|
181
208
|
(0, fmt_1.ok)(ctx, `note removed: ${id} ${String(note.title || "").slice(0, 40)}`);
|
|
182
209
|
return 0;
|
|
183
210
|
}
|
|
@@ -328,6 +328,22 @@ function run(env, action, rest, args) {
|
|
|
328
328
|
}
|
|
329
329
|
case "timeline": {
|
|
330
330
|
// v2: workspace growth timeline — events per day with entity milestones
|
|
331
|
+
// 0.81 ⑧: --actor <a> / --session <s> narrow the stream for multi-agent
|
|
332
|
+
// reconciliation (actor column / payload.session); a filtered run also
|
|
333
|
+
// shows a raw tail so 对账 doesn't need db sql.
|
|
334
|
+
const actorF = String(args.flags.actor || "");
|
|
335
|
+
const sessionF = String(args.flags.session || "");
|
|
336
|
+
const conds = [];
|
|
337
|
+
const params = [];
|
|
338
|
+
if (actorF) {
|
|
339
|
+
conds.push("actor=?");
|
|
340
|
+
params.push(actorF);
|
|
341
|
+
}
|
|
342
|
+
if (sessionF) {
|
|
343
|
+
conds.push(`payload LIKE ?`);
|
|
344
|
+
params.push(`%"session":"${sessionF}"%`);
|
|
345
|
+
}
|
|
346
|
+
const where = conds.length ? ` WHERE ${conds.join(" AND ")}` : "";
|
|
331
347
|
const days = db.all(`
|
|
332
348
|
SELECT date(ts) as day, COUNT(*) as events,
|
|
333
349
|
SUM(CASE WHEN kind LIKE 'project%' THEN 1 ELSE 0 END) as projects,
|
|
@@ -335,19 +351,22 @@ function run(env, action, rest, args) {
|
|
|
335
351
|
SUM(CASE WHEN kind LIKE 'claim%' THEN 1 ELSE 0 END) as claims,
|
|
336
352
|
SUM(CASE WHEN kind LIKE 'kb%' THEN 1 ELSE 0 END) as kb,
|
|
337
353
|
SUM(CASE WHEN kind LIKE 'node%' THEN 1 ELSE 0 END) as nodes
|
|
338
|
-
FROM events GROUP BY date(ts) ORDER BY day
|
|
339
|
-
|
|
354
|
+
FROM events${where} GROUP BY date(ts) ORDER BY day
|
|
355
|
+
`, params);
|
|
340
356
|
if (!days.length) {
|
|
341
357
|
(0, fmt_1.ok)(ctx, "no events yet");
|
|
342
358
|
return 0;
|
|
343
359
|
}
|
|
360
|
+
const tail = conds.length
|
|
361
|
+
? db.all(`SELECT ts, actor, kind, entity_id FROM events${where} ORDER BY ts DESC LIMIT 20`, params)
|
|
362
|
+
: [];
|
|
344
363
|
// F2 fix: --json outputs pure JSON (frozen contract ④)
|
|
345
364
|
if (ctx.json) {
|
|
346
|
-
console.log(JSON.stringify({ timeline: days, total: days.reduce((s2, d) => s2 + d.events, 0) }, null, 2));
|
|
365
|
+
console.log(JSON.stringify({ timeline: days, total: days.reduce((s2, d) => s2 + d.events, 0), ...(tail.length ? { tail } : {}) }, null, 2));
|
|
347
366
|
return 0;
|
|
348
367
|
}
|
|
349
368
|
const maxEvents = Math.max(...days.map(d => d.events));
|
|
350
|
-
(0, fmt_1.head)(ctx,
|
|
369
|
+
(0, fmt_1.head)(ctx, `Workspace Timeline${actorF ? ` · actor=${actorF}` : ""}${sessionF ? ` · session=${sessionF}` : ""}`);
|
|
351
370
|
let totalP = 0, totalS = 0, totalC = 0, totalK = 0, totalN = 0;
|
|
352
371
|
for (const d of days) {
|
|
353
372
|
totalP += d.projects;
|
|
@@ -372,6 +391,12 @@ function run(env, action, rest, args) {
|
|
|
372
391
|
}
|
|
373
392
|
(0, fmt_1.out)(ctx, "");
|
|
374
393
|
(0, fmt_1.out)(ctx, (0, fmt_1.paint)(fmt_1.C.dim, ` 累计: ${totalP}项目 · ${totalS}来源 · ${totalC}断言 · ${totalK}知识 · ${totalN}节点变更`, ctx.color));
|
|
394
|
+
if (tail.length) {
|
|
395
|
+
(0, fmt_1.out)(ctx, "");
|
|
396
|
+
(0, fmt_1.out)(ctx, (0, fmt_1.paint)(fmt_1.C.dim, ` 最近 ${tail.length} 条(按时间倒序):`, ctx.color));
|
|
397
|
+
for (const t of tail)
|
|
398
|
+
(0, fmt_1.out)(ctx, ` ${String(t.ts).slice(0, 19)} ${String(t.actor).padEnd(12)} ${String(t.kind).padEnd(14)} ${String(t.entity_id || "")}`);
|
|
399
|
+
}
|
|
375
400
|
return 0;
|
|
376
401
|
}
|
|
377
402
|
case "workspace": {
|
|
@@ -237,24 +237,29 @@ function run(env, action, rest, args) {
|
|
|
237
237
|
// write events against the base tables and names the phantom ids.
|
|
238
238
|
{
|
|
239
239
|
const PHANTOM_SPECS = [
|
|
240
|
-
{ kind: "kb.add", table: "kb_notes", label: "kb" },
|
|
241
|
-
{ kind: "source.add", table: "sources", label: "source" },
|
|
242
|
-
{ kind: "evidence.add", table: "evidence", label: "evidence" },
|
|
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
243
|
];
|
|
244
244
|
const phantoms = [];
|
|
245
245
|
for (const spec of PHANTOM_SPECS) {
|
|
246
246
|
const rows = db.all(`SELECT e.entity_id id, e.ts FROM events e
|
|
247
247
|
WHERE e.kind='${spec.kind}'
|
|
248
248
|
AND e.ts >= datetime('now', '-2 days')
|
|
249
|
-
AND NOT EXISTS (SELECT 1 FROM ${spec.table} t WHERE t.id = e.entity_id)
|
|
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)`);
|
|
250
251
|
for (const r of rows)
|
|
251
252
|
phantoms.push(`${spec.label} ${r.id} (${String(r.ts).slice(0, 16)})`);
|
|
252
253
|
}
|
|
254
|
+
// 0.81 ①: downgrade to warn — a missing row can also mean a legit delete
|
|
255
|
+
// made BEFORE 0.81 (rm events didn't exist then) or via raw db sql; only
|
|
256
|
+
// the receipt-vs-table comparison says "lost", and that distinction is
|
|
257
|
+
// not worth failing doctor (which gates scripts) over.
|
|
253
258
|
checks.push({
|
|
254
259
|
name: "write_integrity",
|
|
255
|
-
status: phantoms.length ? "
|
|
260
|
+
status: phantoms.length ? "warn" : "pass",
|
|
256
261
|
detail: phantoms.length
|
|
257
|
-
? `${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)`
|
|
258
263
|
: "recent write events all match stored rows",
|
|
259
264
|
});
|
|
260
265
|
}
|
|
@@ -9,7 +9,7 @@ exports.run = run;
|
|
|
9
9
|
const fmt_1 = require("../../../core/text/src/fmt");
|
|
10
10
|
const text_1 = require("../../../core/text/src/text");
|
|
11
11
|
const orm_1 = require("../../../core/database/src/orm");
|
|
12
|
-
exports.REL_TYPES = ["node", "claim", "evidence", "source", "query", "kb", "artifact", "skill"];
|
|
12
|
+
exports.REL_TYPES = ["node", "claim", "evidence", "source", "query", "kb", "artifact", "task", "skill"];
|
|
13
13
|
/** Resolve one `<type>:<ref>` against the database → { type, id, display }.
|
|
14
14
|
* Throws RelError(code) on unknown type / missing row. */
|
|
15
15
|
class RelError extends Error {
|
|
@@ -25,9 +25,15 @@ const DISPLAY = {
|
|
|
25
25
|
// node:<tree>/<key> explicit, or node:<key> resolved in the current tree
|
|
26
26
|
const [treeName, key] = ref.includes("/") ? [ref.slice(0, ref.indexOf("/")), ref.slice(ref.indexOf("/") + 1)] : [null, ref];
|
|
27
27
|
const cur = treeName || db.metaGet("current_tree") || "";
|
|
28
|
+
if (!cur) {
|
|
29
|
+
// 0.81: bare node:<key> with NO current tree used to die as a generic
|
|
30
|
+
// "node not found" — name the real cause (parallel session cleared the
|
|
31
|
+
// selection) and both ways out.
|
|
32
|
+
throw new RelError(1, `no current tree — qualify as node:<tree>/${key} or run 'ygl-trees tree use <name>' first`);
|
|
33
|
+
}
|
|
28
34
|
const rows = db.all("SELECT n.id, n.key, n.name FROM nodes n JOIN trees t ON t.id=n.tree_id WHERE (t.name=? OR t.id=?) AND (n.key=? OR n.id=?)", [cur, cur, key, key]);
|
|
29
35
|
if (!rows.length)
|
|
30
|
-
throw new RelError(1, `node not found: ${ref}`);
|
|
36
|
+
throw new RelError(1, `node not found: ${ref}${treeName ? "" : ` (searched current tree '${cur}' — qualify as node:<tree>/${key} for another tree)`}`);
|
|
31
37
|
return { id: String(rows[0].id), display: String(rows[0].name || rows[0].key) };
|
|
32
38
|
},
|
|
33
39
|
claim: (db, ref) => {
|
|
@@ -66,6 +72,14 @@ const DISPLAY = {
|
|
|
66
72
|
throw new RelError(1, `artifact not found: ${ref}`);
|
|
67
73
|
return { id: String(r.id), display: String(r.title) };
|
|
68
74
|
},
|
|
75
|
+
task: (db, ref) => {
|
|
76
|
+
// tasks are artifacts kind='task' (0.81: task:<id|title> so `task add
|
|
77
|
+
// --node` can link without spelling artifact:)
|
|
78
|
+
const r = db.get("SELECT id, title FROM artifacts WHERE kind='task' AND (id=? OR title=?) LIMIT 1", [ref, ref]);
|
|
79
|
+
if (!r)
|
|
80
|
+
throw new RelError(1, `task not found: ${ref}`);
|
|
81
|
+
return { id: String(r.id), display: String(r.title) };
|
|
82
|
+
},
|
|
69
83
|
skill: (db, ref) => {
|
|
70
84
|
const r = db.get("SELECT name, description FROM skills WHERE name=? LIMIT 1", [ref]);
|
|
71
85
|
if (!r)
|
|
@@ -204,8 +204,11 @@ function run(env, action, rest, args) {
|
|
|
204
204
|
(0, fmt_1.bad)(ctx, "need --title and/or --url");
|
|
205
205
|
return 2;
|
|
206
206
|
}
|
|
207
|
-
if (url && !/^
|
|
208
|
-
|
|
207
|
+
if (url && !/^[a-z][a-z0-9+.-]*:\/\//i.test(url)) {
|
|
208
|
+
// 0.81: any RFC-3986 scheme is a valid carrier — cross-agent refs
|
|
209
|
+
// (a2a://peer/task), session://, file://, http(s):// … url_hash
|
|
210
|
+
// dedupe protects the rest (user report: a2a:// rejected).
|
|
211
|
+
(0, fmt_1.bad)(ctx, `--url must be <scheme>:// — https://, file://, session://, a2a:// … (got: "${(0, text_1.clip)(url, 30)}")`);
|
|
209
212
|
return 2;
|
|
210
213
|
}
|
|
211
214
|
const spec = {
|
|
@@ -482,8 +485,19 @@ function run(env, action, rest, args) {
|
|
|
482
485
|
if (subAction === "add") {
|
|
483
486
|
if ((0, flagcheck_1.rejectBare)(ctx, args.flags, ["confidence", "text"]))
|
|
484
487
|
return 2;
|
|
485
|
-
|
|
486
|
-
|
|
488
|
+
// 0.81: confidence accepts the letter grades (A|B|C) OR a numeric
|
|
489
|
+
// level — 0-1 decimal (0.92) or 0-100 percentage (92, 85%). Column
|
|
490
|
+
// stays TEXT; raw input is stored as given.
|
|
491
|
+
{
|
|
492
|
+
const raw = String(args.flags.confidence ?? "");
|
|
493
|
+
if (raw !== "" && !/^[ABC]$/.test(raw)) {
|
|
494
|
+
const n = Number(raw.replace(/%$/, ""));
|
|
495
|
+
if (!Number.isFinite(n) || n < 0 || n > 100) {
|
|
496
|
+
(0, fmt_1.bad)(ctx, `--confidence must be A|B|C or a number 0-1 / 0-100(%) (got: "${(0, text_1.clip)(raw, 20)}")`);
|
|
497
|
+
return 2;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
487
501
|
const text = rest.slice(1).join(" ") || String(args.flags.text || "");
|
|
488
502
|
if (!text) {
|
|
489
503
|
(0, fmt_1.bad)(ctx, 'missing claim text: research claim add "<text>"');
|
|
@@ -43,6 +43,7 @@ const flagcheck_1 = require("../../../core/text/src/flagcheck");
|
|
|
43
43
|
const text_1 = require("../../../core/text/src/text");
|
|
44
44
|
const orm_1 = require("../../../core/database/src/orm");
|
|
45
45
|
const writes = __importStar(require("../../pipeline/src/writes"));
|
|
46
|
+
const rels_1 = require("../../../ops/tools/src/rels");
|
|
46
47
|
function run(env, action, rest, args) {
|
|
47
48
|
const { ctx, store } = env;
|
|
48
49
|
// R12: the `task` command group rides this router — translate its verbs
|
|
@@ -187,26 +188,40 @@ function run(env, action, rest, args) {
|
|
|
187
188
|
}
|
|
188
189
|
// ── task group rides the produce router (`ygl-trees task ...`, R12) ──
|
|
189
190
|
case "task-add": {
|
|
190
|
-
if ((0, flagcheck_1.rejectBare)(ctx, args.flags, ["note"]))
|
|
191
|
+
if ((0, flagcheck_1.rejectBare)(ctx, args.flags, ["note", "node"]))
|
|
191
192
|
return 2;
|
|
192
193
|
const title = rest.join(" ");
|
|
193
194
|
if (!title) {
|
|
194
|
-
(0, fmt_1.bad)(ctx, 'usage: task add "<title>" [--note ...]');
|
|
195
|
+
(0, fmt_1.bad)(ctx, 'usage: task add "<title>" [--note ...] [--node <tree>/<key>]');
|
|
195
196
|
return 2;
|
|
196
197
|
}
|
|
197
198
|
const note = String(args.flags.note || "");
|
|
199
|
+
const nodeRef = args.flags.node !== undefined ? String(args.flags.node) : "";
|
|
198
200
|
return (0, orm_1.getOrm)(env.db.path)
|
|
199
201
|
.then((ds) => writes.taskAdd(ds, pid, title, note))
|
|
200
202
|
.then((row) => {
|
|
201
|
-
if (
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
203
|
+
if (!nodeRef) {
|
|
204
|
+
if (ctx.json)
|
|
205
|
+
console.log(JSON.stringify(row, null, 2));
|
|
206
|
+
else
|
|
207
|
+
(0, fmt_1.ok)(ctx, `task added: ${row.id} ${title}`);
|
|
208
|
+
return 0;
|
|
209
|
+
}
|
|
210
|
+
// 0.81 ⑦: tasks join the relation fabric — `task add --node` links
|
|
211
|
+
// the fresh task to a tree node (rel=tracked-by) in the same breath,
|
|
212
|
+
// so task lists and node timelines stop being parallel pipelines.
|
|
213
|
+
return (0, rels_1.relAdd)(env, `node:${nodeRef}`, `task:${row.id}`, "tracked-by")
|
|
214
|
+
.then((r) => {
|
|
215
|
+
if (ctx.json)
|
|
216
|
+
console.log(JSON.stringify({ ...row, node: nodeRef, relation: "tracked-by" }, null, 2));
|
|
217
|
+
else
|
|
218
|
+
(0, fmt_1.ok)(ctx, `task added: ${row.id} ${title} (tracked-by node:${nodeRef})`);
|
|
219
|
+
return 0;
|
|
220
|
+
});
|
|
206
221
|
})
|
|
207
222
|
.catch((e) => {
|
|
208
223
|
(0, fmt_1.bad)(ctx, e?.message || String(e));
|
|
209
|
-
return 1;
|
|
224
|
+
return e instanceof rels_1.RelError && e.code === 2 ? 2 : 1;
|
|
210
225
|
});
|
|
211
226
|
}
|
|
212
227
|
case "task-list": {
|
|
@@ -242,10 +242,30 @@ function run(env, action, rest, args) {
|
|
|
242
242
|
});
|
|
243
243
|
}
|
|
244
244
|
if (sub === "cond") {
|
|
245
|
-
|
|
246
|
-
|
|
245
|
+
// 0.81 action-word consistency: `node cond add <key>` used to parse
|
|
246
|
+
// "add" as the node key ("node not found: add"); link/unlock style
|
|
247
|
+
// now works, and the long-missing `node cond rm <key> --name <n>`
|
|
248
|
+
// finally exists (was db-sql-only cleanup).
|
|
249
|
+
let cKey = key;
|
|
250
|
+
const cAct = key === "add" ? "add" : key === "rm" || key === "remove" ? "rm" : "";
|
|
251
|
+
if (cAct)
|
|
252
|
+
cKey = rest[1];
|
|
253
|
+
if (cAct === "rm") {
|
|
254
|
+
const name = flagStr("name");
|
|
255
|
+
if (!cKey || !name) {
|
|
256
|
+
(0, fmt_1.bad)(ctx, "usage: node cond rm <key> --name <cond>");
|
|
257
|
+
return 2;
|
|
258
|
+
}
|
|
259
|
+
return runTreeWrite((ds) => treeWrites.condRemove(ds, tid, cKey, name), (r) => {
|
|
260
|
+
(0, fmt_1.ok)(ctx, `condition removed: ${cKey} · ${name} (was ${r.required ? "必检" : "可选"}/${r.effect})`);
|
|
261
|
+
return 0;
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
if (!cKey) {
|
|
265
|
+
(0, fmt_1.bad)(ctx, "usage: node cond [add|rm] <key> --name <cond> [--kind ..] [--op >=|==|<=|!=|has] [--value n] [--op eval [--value q] | eval-choice --options k=d,.. --answer k | eval-score --levels a,b,.. --min-score n] [--optional] [--effect gate|cost_mul] [--weight n]");
|
|
247
266
|
return 2;
|
|
248
267
|
}
|
|
268
|
+
const k = cKey;
|
|
249
269
|
if ((0, flagcheck_1.rejectBare)(ctx, args.flags, ["name", "value", "op", "effect", "weight", "options", "answer", "levels", "min-score"]))
|
|
250
270
|
return 2;
|
|
251
271
|
if ((0, flagcheck_1.validateEnum)(ctx, args.flags, "op", [">=", "==", "<=", "!=", "has", "eval", "eval-choice", "eval-score"]))
|
|
@@ -254,7 +274,7 @@ function run(env, action, rest, args) {
|
|
|
254
274
|
return 2;
|
|
255
275
|
const name = flagStr("name");
|
|
256
276
|
if (!name) {
|
|
257
|
-
(0, fmt_1.table)(ctx, trees.condList(tid,
|
|
277
|
+
(0, fmt_1.table)(ctx, trees.condList(tid, k), ["id", "name", "kind", "op", "value", "必检", "effect"], [
|
|
258
278
|
"id",
|
|
259
279
|
"name",
|
|
260
280
|
"kind",
|
|
@@ -307,7 +327,7 @@ function run(env, action, rest, args) {
|
|
|
307
327
|
value = (0, eval_1.packEvalScoreSpec)(q, levels, min);
|
|
308
328
|
}
|
|
309
329
|
}
|
|
310
|
-
return runTreeWrite((ds) => treeWrites.condAdd(ds, tid,
|
|
330
|
+
return runTreeWrite((ds) => treeWrites.condAdd(ds, tid, k, {
|
|
311
331
|
name,
|
|
312
332
|
kind: flagStr("kind", "flag"),
|
|
313
333
|
expr: flagStr("expr") || undefined,
|
|
@@ -317,7 +337,7 @@ function run(env, action, rest, args) {
|
|
|
317
337
|
effect: flagStr("effect", "gate"),
|
|
318
338
|
weight: flagNum("weight", 1),
|
|
319
339
|
}), (c) => {
|
|
320
|
-
(0, fmt_1.ack)(ctx, c, `condition added: ${
|
|
340
|
+
(0, fmt_1.ack)(ctx, c, `condition added: ${k} · ${c.name} [${c.required ? "必检" : "可选"}/${c.effect}]`);
|
|
321
341
|
return 0;
|
|
322
342
|
});
|
|
323
343
|
}
|
|
@@ -696,6 +716,16 @@ function run(env, action, rest, args) {
|
|
|
696
716
|
return 0;
|
|
697
717
|
}
|
|
698
718
|
if (sub === "list" || sub === undefined) {
|
|
719
|
+
// Contract ⑤ extension (user report: `node list --tree X` silently
|
|
720
|
+
// swallowed the unknown flag and listed the CURRENT tree). node list
|
|
721
|
+
// takes exactly -t <tree> / --json — any other flag is a typo worth
|
|
722
|
+
// an explicit rc=2 instead of silent misdirection.
|
|
723
|
+
for (const f of Object.keys(args.flags || {})) {
|
|
724
|
+
if (!["t", "json", "no-color"].includes(f)) {
|
|
725
|
+
(0, fmt_1.bad)(ctx, `unknown flag --${f} for 'node list' — tree selection is '-t <tree>' (positional <tree> also works)`);
|
|
726
|
+
return 2;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
699
729
|
(0, fmt_1.table)(ctx, trees.nodeList(tid), ["key", "name", "kind", "tier", "cost", "状态", "必检父", "子节点"], [
|
|
700
730
|
"key",
|
|
701
731
|
"name",
|
|
@@ -8,6 +8,7 @@ exports.nodeDel = nodeDel;
|
|
|
8
8
|
exports.link = link;
|
|
9
9
|
exports.unlink = unlink;
|
|
10
10
|
exports.condAdd = condAdd;
|
|
11
|
+
exports.condRemove = condRemove;
|
|
11
12
|
exports.stateSet = stateSet;
|
|
12
13
|
exports.rebuildIndex = rebuildIndex;
|
|
13
14
|
exports.importTree = importTree;
|
|
@@ -270,6 +271,28 @@ async function condAdd(ds, treeId, keyOrId, o) {
|
|
|
270
271
|
return rows[0];
|
|
271
272
|
});
|
|
272
273
|
}
|
|
274
|
+
/** 0.81: `node cond rm <key> --name <n>` — remove one condition by natural
|
|
275
|
+
* key. Throws "node not found" / "condition not found: <name> (node X has:
|
|
276
|
+
* ...)" (named, per frozen contract ④). Mirrors condAdd: transactional +
|
|
277
|
+
* event (node.cond-rm) + markDirty. */
|
|
278
|
+
async function condRemove(ds, treeId, keyOrId, name) {
|
|
279
|
+
return ds.transaction(async (mgr) => {
|
|
280
|
+
const n = (await mgr.query(`SELECT id FROM nodes WHERE tree_id=? AND (key=? OR id=?)`, [treeId, keyOrId, keyOrId]));
|
|
281
|
+
if (!n.length)
|
|
282
|
+
throw new Error(`node not found: ${keyOrId}`);
|
|
283
|
+
const existing = (await mgr.query(`SELECT * FROM node_conditions WHERE node_id=? AND name=?`, [n[0].id, name]));
|
|
284
|
+
if (!existing.length) {
|
|
285
|
+
const all = (await mgr.query(`SELECT name FROM node_conditions WHERE node_id=?`, [n[0].id]));
|
|
286
|
+
const have = all.map((r) => r.name).join(", ") || "(none)";
|
|
287
|
+
throw new Error(`condition not found: ${name} (node ${keyOrId} has: ${have})`);
|
|
288
|
+
}
|
|
289
|
+
const row = existing[0];
|
|
290
|
+
await mgr.query(`DELETE FROM node_conditions WHERE id=?`, [row.id]);
|
|
291
|
+
await markDirty(mgr, treeId);
|
|
292
|
+
await logEvent(mgr, "node.cond-rm", row.id, { tree: treeId, node: keyOrId, name, kind: row.kind, op: row.op, required: !!row.required, effect: row.effect });
|
|
293
|
+
return { ...row, removed: true };
|
|
294
|
+
});
|
|
295
|
+
}
|
|
273
296
|
async function stateSet(ds, treeId, keyOrId, status, context = "default", level = 0) {
|
|
274
297
|
return ds.transaction(async (mgr) => {
|
|
275
298
|
const n = (await mgr.query(`SELECT id FROM nodes WHERE tree_id=? AND (key=? OR id=?)`, [treeId, keyOrId, keyOrId]));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ygl-trees",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.81.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/schemastery": "~3.18.5-alpha.2",
|
|
29
30
|
"@deepseek-ai/cordis-plugin-timer": "~1.1.7-alpha.2",
|
|
30
|
-
"@deepseek-ai/cosmokit": "~1.8.6-alpha.2"
|
|
31
|
-
"@deepseek-ai/schemastery": "~3.18.5-alpha.2"
|
|
31
|
+
"@deepseek-ai/cosmokit": "~1.8.6-alpha.2"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@types/node": "^20.0.0",
|
|
@@ -45,6 +45,7 @@ ygl-trees skill link --global # 连接到 ~/.agents/skills/(全局)
|
|
|
45
45
|
ygl-trees agent ask "<问题>" # 自然语言路由(projects/search/tree/health/...)
|
|
46
46
|
ygl-trees db time-travel "日期" # 查询过去某时刻的工作区状态
|
|
47
47
|
ygl-trees db timeline # 增长时间线(events/天+里程碑)
|
|
48
|
+
ygl-trees db timeline --actor <a> [--session <s>] # 按 actor/会话过滤 + 最近 20 条原始事件(多 Agent 对账)
|
|
48
49
|
ygl-trees db workspace list # 多工作区管理
|
|
49
50
|
ygl-trees db obsidian [dir] # 导出 Obsidian markdown vault
|
|
50
51
|
ygl-trees skill init <name> # 模板创建技能
|
|
@@ -84,8 +85,9 @@ PG 迁移预检:`db migrate pg` 前自动逐表扫描 INTEGER 列类型——
|
|
|
84
85
|
## 3. 健康检查与任务
|
|
85
86
|
|
|
86
87
|
```bash
|
|
87
|
-
ygl-trees doctor # 10 项健康检查(schema/索引/FTS/重复/孤儿/config/锁存活/eval
|
|
88
|
+
ygl-trees doctor # 10 项健康检查(schema/索引/FTS/重复/孤儿/config/锁存活/eval信封/插件/幻影写入→warn)
|
|
88
89
|
ygl-trees task add "<标题>" # 建任务(todo 起步)
|
|
90
|
+
ygl-trees task add "验证 X" --node <tree>/<key> # 建任务并挂树节点(tracked-by,0.81)
|
|
89
91
|
ygl-trees task list # 按状态分组
|
|
90
92
|
ygl-trees task done <id> # 勾销(全程留痕)
|
|
91
93
|
```
|
|
@@ -33,13 +33,15 @@ ygl-trees research source add --adjudication "会话内裁决:三条件已逐
|
|
|
33
33
|
```
|
|
34
34
|
|
|
35
35
|
来源按 `(project, url_hash)` 去重;`--query` 顺带建立召回关联(query list 显示召回数)。
|
|
36
|
-
`--url`
|
|
36
|
+
`--url` 收任意 `<scheme>://`(https/file/session/a2a/…,0.81)——跨 Agent 引用免落盘。
|
|
37
37
|
|
|
38
38
|
## 2. 摘录与断言
|
|
39
39
|
|
|
40
40
|
```bash
|
|
41
41
|
ygl-trees research evidence add <sourceId> --quote "原文片段" --locator "§2"
|
|
42
|
-
ygl-trees research
|
|
42
|
+
ygl-trees research evidence add <sourceId> --file quotes.md # 长摘录免转义
|
|
43
|
+
ygl-trees research claim add "断言文本" --confidence A # 档位
|
|
44
|
+
ygl-trees research claim add "断言文本" --confidence 0.92 # 数值档(0.81:0-1 或 0-100%)
|
|
43
45
|
ygl-trees research claim link <claimId> <evidenceId> [--stance refutes] # 状态自动推导
|
|
44
46
|
ygl-trees research claim resolve <claimId> --as contested --note "矛盾待查" # 人工仲裁出口
|
|
45
47
|
```
|
|
@@ -27,9 +27,10 @@ ygl-trees node link mining smelting # prereq/AND/必检(
|
|
|
27
27
|
ygl-trees node link bronze steel --optional # 可选:不阻塞,进 missing_optional
|
|
28
28
|
ygl-trees node link bronze iron --mode OR # 任一父解锁即可
|
|
29
29
|
ygl-trees node link a b --rel excludes # 互斥分支:父解锁则子被封死
|
|
30
|
-
ygl-trees node cond steel --name lab_level --kind level --value 3
|
|
31
|
-
ygl-trees node cond steel --name has_eng --kind flag --op has --optional \
|
|
32
|
-
|
|
30
|
+
ygl-trees node cond add steel --name lab_level --kind level --value 3 # 必检门槛(add 动作词 0.81)
|
|
31
|
+
ygl-trees node cond add steel --name has_eng --kind flag --op has --optional \
|
|
32
|
+
--effect cost_mul --weight 0.8 # 可选打折
|
|
33
|
+
ygl-trees node cond rm steel --name lab_level # 按自然键删除(0.81)
|
|
33
34
|
# 语义型门槛(决策模型三题型 boolean/choice/score)→ 见 ygl-trees-eval-gates 技能
|
|
34
35
|
ygl-trees node index && ygl-trees node validate
|
|
35
36
|
```
|