ygl-trees 0.80.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 +17 -0
- package/README.md +25 -15
- package/dist/apps/cli/src/bin.js +4 -1
- package/dist/bundle/bin.cjs +955 -629
- 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 +48 -11
- 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 +136 -5
- package/dist/packages/techtree/model/src/writes.js +23 -0
- package/package.json +3 -3
- package/skills/ygl-trees-quickstart/SKILL.md +4 -2
- package/skills/ygl-trees-research/SKILL.md +4 -2
- package/skills/ygl-trees-techtree/SKILL.md +9 -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": {
|
|
@@ -235,26 +235,36 @@ 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" },
|
|
241
|
-
{ kind: "source.add", table: "sources", label: "source" },
|
|
242
|
-
{ kind: "evidence.add", table: "evidence", label: "evidence" },
|
|
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)`);
|
|
255
|
+
const rows = db.all(phantomSql(spec));
|
|
250
256
|
for (const r of rows)
|
|
251
257
|
phantoms.push(`${spec.label} ${r.id} (${String(r.ts).slice(0, 16)})`);
|
|
252
258
|
}
|
|
259
|
+
// 0.81 ①: downgrade to warn — a missing row can also mean a legit delete
|
|
260
|
+
// made BEFORE 0.81 (rm events didn't exist then) or via raw db sql; only
|
|
261
|
+
// the receipt-vs-table comparison says "lost", and that distinction is
|
|
262
|
+
// not worth failing doctor (which gates scripts) over.
|
|
253
263
|
checks.push({
|
|
254
264
|
name: "write_integrity",
|
|
255
|
-
status: phantoms.length ? "
|
|
265
|
+
status: phantoms.length ? "warn" : "pass",
|
|
256
266
|
detail: phantoms.length
|
|
257
|
-
? `${phantoms.length}
|
|
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)`
|
|
258
268
|
: "recent write events all match stored rows",
|
|
259
269
|
});
|
|
260
270
|
}
|
|
@@ -316,6 +326,33 @@ function run(env, action, rest, args) {
|
|
|
316
326
|
catch { /* */ }
|
|
317
327
|
}
|
|
318
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
|
+
}
|
|
319
356
|
if (fixes.length) {
|
|
320
357
|
for (const f of fixes)
|
|
321
358
|
(0, fmt_1.ok)(ctx, ` fixed: ${f}`);
|
|
@@ -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": {
|
|
@@ -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);
|
|
@@ -242,10 +343,30 @@ function run(env, action, rest, args) {
|
|
|
242
343
|
});
|
|
243
344
|
}
|
|
244
345
|
if (sub === "cond") {
|
|
245
|
-
|
|
246
|
-
|
|
346
|
+
// 0.81 action-word consistency: `node cond add <key>` used to parse
|
|
347
|
+
// "add" as the node key ("node not found: add"); link/unlock style
|
|
348
|
+
// now works, and the long-missing `node cond rm <key> --name <n>`
|
|
349
|
+
// finally exists (was db-sql-only cleanup).
|
|
350
|
+
let cKey = key;
|
|
351
|
+
const cAct = key === "add" ? "add" : key === "rm" || key === "remove" ? "rm" : "";
|
|
352
|
+
if (cAct)
|
|
353
|
+
cKey = rest[1];
|
|
354
|
+
if (cAct === "rm") {
|
|
355
|
+
const name = flagStr("name");
|
|
356
|
+
if (!cKey || !name) {
|
|
357
|
+
(0, fmt_1.bad)(ctx, "usage: node cond rm <key> --name <cond>");
|
|
358
|
+
return 2;
|
|
359
|
+
}
|
|
360
|
+
return runTreeWrite((ds) => treeWrites.condRemove(ds, tid, cKey, name), (r) => {
|
|
361
|
+
(0, fmt_1.ok)(ctx, `condition removed: ${cKey} · ${name} (was ${r.required ? "必检" : "可选"}/${r.effect})`);
|
|
362
|
+
return 0;
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
if (!cKey) {
|
|
366
|
+
(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
367
|
return 2;
|
|
248
368
|
}
|
|
369
|
+
const k = cKey;
|
|
249
370
|
if ((0, flagcheck_1.rejectBare)(ctx, args.flags, ["name", "value", "op", "effect", "weight", "options", "answer", "levels", "min-score"]))
|
|
250
371
|
return 2;
|
|
251
372
|
if ((0, flagcheck_1.validateEnum)(ctx, args.flags, "op", [">=", "==", "<=", "!=", "has", "eval", "eval-choice", "eval-score"]))
|
|
@@ -254,7 +375,7 @@ function run(env, action, rest, args) {
|
|
|
254
375
|
return 2;
|
|
255
376
|
const name = flagStr("name");
|
|
256
377
|
if (!name) {
|
|
257
|
-
(0, fmt_1.table)(ctx, trees.condList(tid,
|
|
378
|
+
(0, fmt_1.table)(ctx, trees.condList(tid, k), ["id", "name", "kind", "op", "value", "必检", "effect"], [
|
|
258
379
|
"id",
|
|
259
380
|
"name",
|
|
260
381
|
"kind",
|
|
@@ -307,7 +428,7 @@ function run(env, action, rest, args) {
|
|
|
307
428
|
value = (0, eval_1.packEvalScoreSpec)(q, levels, min);
|
|
308
429
|
}
|
|
309
430
|
}
|
|
310
|
-
return runTreeWrite((ds) => treeWrites.condAdd(ds, tid,
|
|
431
|
+
return runTreeWrite((ds) => treeWrites.condAdd(ds, tid, k, {
|
|
311
432
|
name,
|
|
312
433
|
kind: flagStr("kind", "flag"),
|
|
313
434
|
expr: flagStr("expr") || undefined,
|
|
@@ -317,7 +438,7 @@ function run(env, action, rest, args) {
|
|
|
317
438
|
effect: flagStr("effect", "gate"),
|
|
318
439
|
weight: flagNum("weight", 1),
|
|
319
440
|
}), (c) => {
|
|
320
|
-
(0, fmt_1.ack)(ctx, c, `condition added: ${
|
|
441
|
+
(0, fmt_1.ack)(ctx, c, `condition added: ${k} · ${c.name} [${c.required ? "必检" : "可选"}/${c.effect}]`);
|
|
321
442
|
return 0;
|
|
322
443
|
});
|
|
323
444
|
}
|
|
@@ -696,6 +817,16 @@ function run(env, action, rest, args) {
|
|
|
696
817
|
return 0;
|
|
697
818
|
}
|
|
698
819
|
if (sub === "list" || sub === undefined) {
|
|
820
|
+
// Contract ⑤ extension (user report: `node list --tree X` silently
|
|
821
|
+
// 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.
|
|
824
|
+
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)`);
|
|
827
|
+
return 2;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
699
830
|
(0, fmt_1.table)(ctx, trees.nodeList(tid), ["key", "name", "kind", "tier", "cost", "状态", "必检父", "子节点"], [
|
|
700
831
|
"key",
|
|
701
832
|
"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.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/cordis-plugin-timer": "~1.1.7-alpha.2",
|
|
30
29
|
"@deepseek-ai/cosmokit": "~1.8.6-alpha.2",
|
|
31
|
-
"@deepseek-ai/schemastery": "~3.18.5-alpha.2"
|
|
30
|
+
"@deepseek-ai/schemastery": "~3.18.5-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",
|
|
@@ -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> # 模板创建技能
|
|
@@ -57,7 +58,7 @@ ygl-trees llms --json # 结构化 JSON
|
|
|
57
58
|
|
|
58
59
|
```bash
|
|
59
60
|
ygl-trees doctor # 10 项健康检查(含锁 pid 存活、幻影写入对账)
|
|
60
|
-
ygl-trees doctor --fix # 一键安全修复(脏索引 + 死 pid
|
|
61
|
+
ygl-trees doctor --fix # 一键安全修复(脏索引 + 死 pid 陈旧锁 + 幻影事件标记 reconciled;活 holder 不动)
|
|
61
62
|
ygl-trees db unlock # 清残留 .ygllock(死 pid 直清;活 holder 回显其命令行并拒绝,--force 强清)
|
|
62
63
|
ygl-trees kb rm <id> # 删除笔记(与 skill rm 对称)
|
|
63
64
|
ygl-trees webui serve --host 0.0.0.0 --port 8642 # LAN/WSL 可访问
|
|
@@ -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
|
```
|