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.
@@ -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, "Workspace Timeline");
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 ? "fail" : "pass",
260
+ status: phantoms.length ? "warn" : "pass",
256
261
  detail: phantoms.length
257
- ? `${phantoms.length} phantom write event(s) in last 48h: ${phantoms.slice(0, 3).join(", ")}${phantoms.length > 3 ? " …" : ""} — event logged but row missing (rollback or different workspace; check receipts' workspace line)`
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 && !/^https?:\/\/|^file:\/\/|^session:\/\//.test(url)) {
208
- (0, fmt_1.bad)(ctx, `--url must be http(s)://, file:// or session:// (got: "${(0, text_1.clip)(url, 30)}")`);
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
- if ((0, flagcheck_1.validateEnum)(ctx, args.flags, "confidence", ["A", "B", "C"]))
486
- return 2;
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 (ctx.json)
202
- console.log(JSON.stringify(row, null, 2));
203
- else
204
- (0, fmt_1.ok)(ctx, `task added: ${row.id} ${title}`);
205
- return 0;
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
- if (!key) {
246
- (0, fmt_1.bad)(ctx, "usage: node cond <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]");
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, key), ["id", "name", "kind", "op", "value", "必检", "effect"], [
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, key, {
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: ${key} · ${c.name} [${c.required ? "必检" : "可选"}/${c.effect}]`);
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.80.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` 收 http(s):// · file:// · session://(0.80)。
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 claim add "断言文本" --confidence A
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
- --effect cost_mul --weight 0.8 # 可选打折
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
  ```