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.
@@ -5027,6 +5027,19 @@ function run$12(env, action, rest, args) {
5027
5027
  return 0;
5028
5028
  }
5029
5029
  case "timeline": {
5030
+ const actorF = String(args.flags.actor || "");
5031
+ const sessionF = String(args.flags.session || "");
5032
+ const conds = [];
5033
+ const params = [];
5034
+ if (actorF) {
5035
+ conds.push("actor=?");
5036
+ params.push(actorF);
5037
+ }
5038
+ if (sessionF) {
5039
+ conds.push(`payload LIKE ?`);
5040
+ params.push(`%"session":"${sessionF}"%`);
5041
+ }
5042
+ const where = conds.length ? ` WHERE ${conds.join(" AND ")}` : "";
5030
5043
  const days = db.all(`
5031
5044
  SELECT date(ts) as day, COUNT(*) as events,
5032
5045
  SUM(CASE WHEN kind LIKE 'project%' THEN 1 ELSE 0 END) as projects,
@@ -5034,21 +5047,23 @@ function run$12(env, action, rest, args) {
5034
5047
  SUM(CASE WHEN kind LIKE 'claim%' THEN 1 ELSE 0 END) as claims,
5035
5048
  SUM(CASE WHEN kind LIKE 'kb%' THEN 1 ELSE 0 END) as kb,
5036
5049
  SUM(CASE WHEN kind LIKE 'node%' THEN 1 ELSE 0 END) as nodes
5037
- FROM events GROUP BY date(ts) ORDER BY day
5038
- `);
5050
+ FROM events${where} GROUP BY date(ts) ORDER BY day
5051
+ `, params);
5039
5052
  if (!days.length) {
5040
5053
  ok(ctx, "no events yet");
5041
5054
  return 0;
5042
5055
  }
5056
+ const tail = conds.length ? db.all(`SELECT ts, actor, kind, entity_id FROM events${where} ORDER BY ts DESC LIMIT 20`, params) : [];
5043
5057
  if (ctx.json) {
5044
5058
  console.log(JSON.stringify({
5045
5059
  timeline: days,
5046
- total: days.reduce((s2, d) => s2 + d.events, 0)
5060
+ total: days.reduce((s2, d) => s2 + d.events, 0),
5061
+ ...tail.length ? { tail } : {}
5047
5062
  }, null, 2));
5048
5063
  return 0;
5049
5064
  }
5050
5065
  const maxEvents = Math.max(...days.map((d) => d.events));
5051
- head(ctx, "Workspace Timeline");
5066
+ head(ctx, `Workspace Timeline${actorF ? ` · actor=${actorF}` : ""}${sessionF ? ` · session=${sessionF}` : ""}`);
5052
5067
  let totalP = 0, totalS = 0, totalC = 0, totalK = 0, totalN = 0;
5053
5068
  for (const d of days) {
5054
5069
  totalP += d.projects;
@@ -5068,6 +5083,11 @@ function run$12(env, action, rest, args) {
5068
5083
  }
5069
5084
  out(ctx, "");
5070
5085
  out(ctx, paint(C.dim, ` 累计: ${totalP}项目 · ${totalS}来源 · ${totalC}断言 · ${totalK}知识 · ${totalN}节点变更`, ctx.color));
5086
+ if (tail.length) {
5087
+ out(ctx, "");
5088
+ out(ctx, paint(C.dim, ` 最近 ${tail.length} 条(按时间倒序):`, ctx.color));
5089
+ for (const t of tail) out(ctx, ` ${String(t.ts).slice(0, 19)} ${String(t.actor).padEnd(12)} ${String(t.kind).padEnd(14)} ${String(t.entity_id || "")}`);
5090
+ }
5071
5091
  return 0;
5072
5092
  }
5073
5093
  case "workspace": {
@@ -6698,8 +6718,8 @@ function run$8(env, action, rest, args) {
6698
6718
  bad(ctx, "need --title and/or --url");
6699
6719
  return 2;
6700
6720
  }
6701
- if (url && !/^https?:\/\/|^file:\/\/|^session:\/\//.test(url)) {
6702
- bad(ctx, `--url must be http(s)://, file:// or session:// (got: "${require_text.clip(url, 30)}")`);
6721
+ if (url && !/^[a-z][a-z0-9+.-]*:\/\//i.test(url)) {
6722
+ bad(ctx, `--url must be <scheme>:// — https://, file://, session://, a2a:// … (got: "${require_text.clip(url, 30)}")`);
6703
6723
  return 2;
6704
6724
  }
6705
6725
  const spec = {
@@ -6971,11 +6991,16 @@ function run$8(env, action, rest, args) {
6971
6991
  if (sub === "claim") {
6972
6992
  if (subAction === "add") {
6973
6993
  if (rejectBare(ctx, args.flags, ["confidence", "text"])) return 2;
6974
- if (validateEnum(ctx, args.flags, "confidence", [
6975
- "A",
6976
- "B",
6977
- "C"
6978
- ])) return 2;
6994
+ {
6995
+ const raw = String(args.flags.confidence ?? "");
6996
+ if (raw !== "" && !/^[ABC]$/.test(raw)) {
6997
+ const n = Number(raw.replace(/%$/, ""));
6998
+ if (!Number.isFinite(n) || n < 0 || n > 100) {
6999
+ bad(ctx, `--confidence must be A|B|C or a number 0-1 / 0-100(%) (got: "${require_text.clip(raw, 20)}")`);
7000
+ return 2;
7001
+ }
7002
+ }
7003
+ }
6979
7004
  const text = rest.slice(1).join(" ") || String(args.flags.text || "");
6980
7005
  if (!text) {
6981
7006
  bad(ctx, "missing claim text: research claim add \"<text>\"");
@@ -7407,151 +7432,480 @@ function run$7(env, action, rest, args) {
7407
7432
  }
7408
7433
  }
7409
7434
  //#endregion
7410
- //#region packages/research/produce/src/produce-cmds.ts
7411
- /** produce group — evidence-backed outline → sections → report.
7412
- * Writes run on the TypeORM write service (src/writes.ts); reads stay on
7413
- * Store until their own migration batch. */
7414
- var produce_cmds_exports = /* @__PURE__ */ require_rolldown_runtime.__exportAll({ run: () => run$6 });
7435
+ //#region packages/ops/tools/src/rels.ts
7436
+ var rels_exports = /* @__PURE__ */ require_rolldown_runtime.__exportAll({
7437
+ REL_TYPES: () => REL_TYPES,
7438
+ RelError: () => RelError,
7439
+ relAdd: () => relAdd,
7440
+ relRm: () => relRm,
7441
+ resolveId: () => resolveId,
7442
+ resolveRef: () => resolveRef,
7443
+ run: () => run$6
7444
+ });
7415
7445
  init_fmt();
7416
7446
  require_text.init_text();
7417
7447
  init_orm();
7418
- init_writes();
7419
- function run$6(env, action, rest, args) {
7420
- const { ctx, store } = env;
7421
- if (env.group === "task") {
7422
- const map = {
7423
- add: "task-add",
7424
- list: "task-list",
7425
- doing: "task-status",
7426
- done: "task-status",
7427
- todo: "task-status"
7448
+ const REL_TYPES = [
7449
+ "node",
7450
+ "claim",
7451
+ "evidence",
7452
+ "source",
7453
+ "query",
7454
+ "kb",
7455
+ "artifact",
7456
+ "task",
7457
+ "skill"
7458
+ ];
7459
+ /** Resolve one `<type>:<ref>` against the database → { type, id, display }.
7460
+ * Throws RelError(code) on unknown type / missing row. */
7461
+ var RelError = class extends Error {
7462
+ code;
7463
+ constructor(code, message) {
7464
+ super(message);
7465
+ this.code = code;
7466
+ }
7467
+ };
7468
+ const DISPLAY = {
7469
+ node: (db, ref) => {
7470
+ const [treeName, key] = ref.includes("/") ? [ref.slice(0, ref.indexOf("/")), ref.slice(ref.indexOf("/") + 1)] : [null, ref];
7471
+ const cur = treeName || db.metaGet("current_tree") || "";
7472
+ if (!cur) throw new RelError(1, `no current tree — qualify as node:<tree>/${key} or run 'ygl-trees tree use <name>' first`);
7473
+ 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=?)", [
7474
+ cur,
7475
+ cur,
7476
+ key,
7477
+ key
7478
+ ]);
7479
+ if (!rows.length) throw new RelError(1, `node not found: ${ref}${treeName ? "" : ` (searched current tree '${cur}' — qualify as node:<tree>/${key} for another tree)`}`);
7480
+ return {
7481
+ id: String(rows[0].id),
7482
+ display: String(rows[0].name || rows[0].key)
7483
+ };
7484
+ },
7485
+ claim: (db, ref) => {
7486
+ const r = db.get("SELECT id, text FROM claims WHERE id=? LIMIT 1", [ref]);
7487
+ if (!r) throw new RelError(1, `claim not found: ${ref}`);
7488
+ return {
7489
+ id: String(r.id),
7490
+ display: String(r.text).slice(0, 60)
7491
+ };
7492
+ },
7493
+ evidence: (db, ref) => {
7494
+ const r = db.get("SELECT id, quote FROM evidence WHERE id=? LIMIT 1", [ref]);
7495
+ if (!r) throw new RelError(1, `evidence not found: ${ref}`);
7496
+ return {
7497
+ id: String(r.id),
7498
+ display: String(r.quote).slice(0, 60)
7499
+ };
7500
+ },
7501
+ source: (db, ref) => {
7502
+ const r = db.get("SELECT id, title FROM sources WHERE id=? LIMIT 1", [ref]);
7503
+ if (!r) throw new RelError(1, `source not found: ${ref}`);
7504
+ return {
7505
+ id: String(r.id),
7506
+ display: String(r.title)
7507
+ };
7508
+ },
7509
+ query: (db, ref) => {
7510
+ const r = db.get("SELECT id, text FROM queries WHERE id=? LIMIT 1", [ref]);
7511
+ if (!r) throw new RelError(1, `query not found: ${ref}`);
7512
+ return {
7513
+ id: String(r.id),
7514
+ display: String(r.text).slice(0, 60)
7515
+ };
7516
+ },
7517
+ kb: (db, ref) => {
7518
+ const r = db.get("SELECT id, title FROM kb_notes WHERE id=? OR title=? LIMIT 1", [ref, ref]);
7519
+ if (!r) throw new RelError(1, `kb note not found: ${ref}`);
7520
+ return {
7521
+ id: String(r.id),
7522
+ display: String(r.title)
7523
+ };
7524
+ },
7525
+ artifact: (db, ref) => {
7526
+ const r = db.get("SELECT id, title FROM artifacts WHERE id=? OR title=? LIMIT 1", [ref, ref]);
7527
+ if (!r) throw new RelError(1, `artifact not found: ${ref}`);
7528
+ return {
7529
+ id: String(r.id),
7530
+ display: String(r.title)
7531
+ };
7532
+ },
7533
+ task: (db, ref) => {
7534
+ const r = db.get("SELECT id, title FROM artifacts WHERE kind='task' AND (id=? OR title=?) LIMIT 1", [ref, ref]);
7535
+ if (!r) throw new RelError(1, `task not found: ${ref}`);
7536
+ return {
7537
+ id: String(r.id),
7538
+ display: String(r.title)
7539
+ };
7540
+ },
7541
+ skill: (db, ref) => {
7542
+ const r = db.get("SELECT name, description FROM skills WHERE name=? LIMIT 1", [ref]);
7543
+ if (!r) throw new RelError(1, `skill not found: ${ref}`);
7544
+ return {
7545
+ id: String(r.name),
7546
+ display: String(r.name)
7428
7547
  };
7429
- if (!action || !map[action]) {
7430
- bad(ctx, "usage: task add \"<title>\" [--note ...] | list [--status todo|doing|done] | todo|doing|done <id>");
7431
- return 2;
7432
- }
7433
- if (map[action] === "task-status") rest = [rest[0], action];
7434
- action = map[action];
7435
7548
  }
7436
- let project;
7549
+ };
7550
+ function resolveRef(env, ref) {
7551
+ const i = ref.indexOf(":");
7552
+ if (i < 1) throw new RelError(2, `bad reference '${ref}' — want <type>:<ref> where type ∈ ${REL_TYPES.join("|")}`);
7553
+ const type = ref.slice(0, i);
7554
+ const ident = ref.slice(i + 1);
7555
+ const fn = DISPLAY[type];
7556
+ if (!fn) throw new RelError(2, `unknown reference type '${type}' — want ${REL_TYPES.join("|")}`);
7557
+ const r = fn(env.db, ident);
7558
+ return {
7559
+ type,
7560
+ id: r.id,
7561
+ display: r.display
7562
+ };
7563
+ }
7564
+ /** Reverse lookup: type+id → display (for the webui related section). */
7565
+ function resolveId(env, type, id) {
7566
+ const fn = DISPLAY[type];
7567
+ if (!fn) return id;
7437
7568
  try {
7438
- project = store.projectResolve(args.flags.p ? String(args.flags.p) : void 0);
7439
- } catch (e) {
7440
- bad(ctx, e.message);
7441
- return 1;
7569
+ return fn(env.db, id).display;
7570
+ } catch {
7571
+ return id;
7442
7572
  }
7443
- const pid = project.id;
7444
- const readBody = () => {
7445
- if (args.flags.body) return String(args.flags.body);
7446
- if (args.flags.f) return node_fs.readFileSync(String(args.flags.f), "utf8");
7447
- return "";
7448
- };
7449
- switch (action) {
7450
- case "outline": {
7451
- const claims = store.claimList(pid);
7452
- const lines = [`# ${project.name}`, ""];
7453
- if (project.question) lines.push(`> ${project.question}`, "");
7454
- lines.push("## 结论要点", "");
7455
- if (!claims.length) lines.push("_(尚无 claim,先跑 research claim add)_", "");
7456
- claims.forEach((c, i) => {
7457
- const mark = c.status === "supported" ? "[已证实]" : c.status === "refuted" ? "[已推翻]" : c.status === "contested" ? "[存争议]" : "[待验证]";
7458
- lines.push(`${i + 1}. ${mark} ${c.text}`);
7459
- });
7460
- lines.push("", "## 证据基础", "");
7461
- const ev = store.evidenceList(pid);
7462
- if (!ev.length) lines.push("_(尚无 evidence)_", "");
7463
- ev.forEach((e, i) => lines.push(`- [${i + 1}] ${e.source}:${require_text.clip(e.quote, 80)}`));
7464
- const content = lines.join("\n");
7465
- return getOrm(env.db.path).then((ds) => artifactAdd(ds, pid, "outline", `${project.name} outline`, content)).then((r) => {
7466
- if (ctx.json) console.log(JSON.stringify(r.row, null, 2));
7467
- else {
7468
- ok(ctx, `outline generated: ${r.row.id} (v${r.version})`);
7469
- out(ctx, content);
7470
- }
7471
- return 0;
7472
- }).catch((e) => {
7473
- bad(ctx, e?.message || String(e));
7474
- return 1;
7475
- });
7476
- }
7477
- case "section": {
7478
- const title = rest[0] || String(args.flags.title || "");
7479
- const body = readBody();
7480
- if (!title || !body) {
7481
- bad(ctx, "usage: produce section <title> --body|-f <file>");
7482
- return 2;
7483
- }
7484
- return getOrm(env.db.path).then((ds) => artifactAdd(ds, pid, "section", title, body)).then((r) => {
7485
- if (ctx.json) console.log(JSON.stringify(r.row, null, 2));
7486
- else ok(ctx, `section saved: ${r.row.id} (v${r.version})`);
7487
- return 0;
7488
- }).catch((e) => {
7489
- bad(ctx, e?.message || String(e));
7490
- return 1;
7491
- });
7492
- }
7493
- case "spec": {
7494
- const title = rest[0] || String(args.flags.title || "");
7495
- const body = readBody();
7496
- if (args.flags["design-type"] !== void 0) {
7497
- bad(ctx, "--design-type belongs to 'produce design' (spec does not take it; if this is a design decision, use: produce design <title> --design-type decision)");
7498
- return 2;
7499
- }
7500
- if (!title || !body) {
7501
- bad(ctx, "usage: produce spec <title> --body|-f <file>");
7502
- return 2;
7503
- }
7504
- return getOrm(env.db.path).then((ds) => artifactAdd(ds, pid, "spec", title, body)).then((r) => {
7505
- if (ctx.json) console.log(JSON.stringify(r.row, null, 2));
7506
- else ok(ctx, `spec saved: ${r.row.id} (v${r.version})`);
7507
- return 0;
7508
- }).catch((e) => {
7509
- bad(ctx, e?.message || String(e));
7510
- return 1;
7511
- });
7512
- }
7513
- case "design": {
7514
- const title = rest[0] || String(args.flags.title || "");
7515
- const body = readBody();
7516
- const designType = String(args.flags["design-type"] || "arch");
7517
- if (!title || !body) {
7518
- bad(ctx, "usage: produce design <title> --body|-f <file> [--design-type ui-ux|arch|decision]");
7519
- return 2;
7520
- }
7521
- if (![
7522
- "ui-ux",
7523
- "arch",
7524
- "decision"
7525
- ].includes(designType)) {
7526
- bad(ctx, "--design-type must be one of ui-ux|arch|decision");
7527
- return 2;
7528
- }
7529
- return getOrm(env.db.path).then((ds) => artifactAdd(ds, pid, "design", title, body, "markdown", JSON.stringify({ design_type: designType }))).then((r) => {
7530
- if (ctx.json) console.log(JSON.stringify(r.row, null, 2));
7531
- else ok(ctx, `design saved: ${r.row.id} (v${r.version}) [${designType}]`);
7532
- return 0;
7533
- }).catch((e) => {
7534
- bad(ctx, e?.message || String(e));
7573
+ }
7574
+ async function relAdd(env, srcRef, dstRef, rel = "related") {
7575
+ const src = resolveRef(env, srcRef);
7576
+ const dst = resolveRef(env, dstRef);
7577
+ return (await getOrm(env.db.path)).transaction(async (mgr) => {
7578
+ const ex = await mgr.query(`SELECT id FROM relations WHERE src_type=? AND src_id=? AND dst_type=? AND dst_id=? AND rel=?`, [
7579
+ src.type,
7580
+ src.id,
7581
+ dst.type,
7582
+ dst.id,
7583
+ rel
7584
+ ]);
7585
+ if (ex.length) return {
7586
+ row: (await mgr.query(`SELECT * FROM relations WHERE id=?`, [ex[0].id]))[0],
7587
+ duplicate: true
7588
+ };
7589
+ const id = require_text.shortId([
7590
+ "rel",
7591
+ src.id,
7592
+ dst.id,
7593
+ rel
7594
+ ], 10);
7595
+ await mgr.query(`INSERT INTO relations(id,src_type,src_id,dst_type,dst_id,rel,weight,created) VALUES(?,?,?,?,?,?,1,?)`, [
7596
+ id,
7597
+ src.type,
7598
+ src.id,
7599
+ dst.type,
7600
+ dst.id,
7601
+ rel,
7602
+ require_text.nowIso()
7603
+ ]);
7604
+ await mgr.query(`INSERT INTO events(id,ts,actor,kind,entity,entity_id,payload) VALUES(?,?,?,?,?,?,?)`, [
7605
+ require_text.shortId([
7606
+ "ev",
7607
+ "rel.add",
7608
+ id
7609
+ ], 12),
7610
+ require_text.nowIso(),
7611
+ "cli",
7612
+ "rel.add",
7613
+ "relation",
7614
+ id,
7615
+ JSON.stringify({
7616
+ src: `${src.type}:${src.id}`,
7617
+ dst: `${dst.type}:${dst.id}`,
7618
+ rel
7619
+ })
7620
+ ]);
7621
+ return {
7622
+ row: (await mgr.query(`SELECT * FROM relations WHERE id=?`, [id]))[0],
7623
+ duplicate: false
7624
+ };
7625
+ });
7626
+ }
7627
+ async function relRm(env, srcRef, dstRef, rel = "related") {
7628
+ const src = resolveRef(env, srcRef);
7629
+ const dst = resolveRef(env, dstRef);
7630
+ await (await getOrm(env.db.path)).transaction(async (mgr) => {
7631
+ if (!(await mgr.query(`SELECT id FROM relations WHERE src_type=? AND src_id=? AND dst_type=? AND dst_id=? AND rel=?`, [
7632
+ src.type,
7633
+ src.id,
7634
+ dst.type,
7635
+ dst.id,
7636
+ rel
7637
+ ])).length) throw new RelError(1, `relation not found: ${srcRef} → ${dstRef} (${rel})`);
7638
+ await mgr.query(`DELETE FROM relations WHERE src_type=? AND src_id=? AND dst_type=? AND dst_id=? AND rel=?`, [
7639
+ src.type,
7640
+ src.id,
7641
+ dst.type,
7642
+ dst.id,
7643
+ rel
7644
+ ]);
7645
+ });
7646
+ }
7647
+ function run$6(env, action, rest, args) {
7648
+ const { ctx } = env;
7649
+ if (action === "add") {
7650
+ const [a, b] = rest;
7651
+ if (!a || !b) {
7652
+ bad(ctx, "usage: rel add <type>:<ref> <type>:<ref> [--rel <type>]");
7653
+ return 2;
7654
+ }
7655
+ const rel = String(args.flags.rel || "related");
7656
+ return (async () => {
7657
+ try {
7658
+ const r = await relAdd(env, a, b, rel);
7659
+ if (ctx.json) {
7660
+ console.log(JSON.stringify({
7661
+ id: r.row.id,
7662
+ src: `${r.row.src_type}:${r.row.src_id}`,
7663
+ dst: `${r.row.dst_type}:${r.row.dst_id}`,
7664
+ rel: r.row.rel,
7665
+ duplicate: r.duplicate
7666
+ }));
7667
+ return 0;
7668
+ }
7669
+ if (r.duplicate) out(ctx, paint(C.yellow, ` ⚠ duplicate: ${a} ${rel} ${b} (already linked)`, ctx.color));
7670
+ else ok(ctx, `linked: ${a} ${paint(C.dim, `—${rel}→`, ctx.color)} ${b}`);
7671
+ return 0;
7672
+ } catch (e) {
7673
+ if (e instanceof RelError) {
7674
+ bad(ctx, e.message);
7675
+ return e.code;
7676
+ }
7677
+ throw e;
7678
+ }
7679
+ })();
7680
+ }
7681
+ if (action === "rm") {
7682
+ const [a, b] = rest;
7683
+ if (!a || !b) {
7684
+ bad(ctx, "usage: rel rm <type>:<ref> <type>:<ref> [--rel <type>]");
7685
+ return 2;
7686
+ }
7687
+ const rel = String(args.flags.rel || "related");
7688
+ return (async () => {
7689
+ try {
7690
+ await relRm(env, a, b, rel);
7691
+ if (ctx.json) console.log(JSON.stringify({ removed: `${a} -${rel}-> ${b}` }));
7692
+ else ok(ctx, `unlinked: ${a} (${rel}) ${b}`);
7693
+ return 0;
7694
+ } catch (e) {
7695
+ if (e instanceof RelError) {
7696
+ bad(ctx, e.message);
7697
+ return e.code;
7698
+ }
7699
+ throw e;
7700
+ }
7701
+ })();
7702
+ }
7703
+ if (action === "list" || action === "") {
7704
+ const outRows = (() => {
7705
+ const params = [];
7706
+ let where = "1=1";
7707
+ const srcF = String(args.flags.src || "");
7708
+ const dstF = String(args.flags.dst || "");
7709
+ const relF = String(args.flags.rel || "");
7710
+ if (srcF) {
7711
+ const s = resolveRef(env, srcF);
7712
+ where += " AND src_type=? AND src_id=?";
7713
+ params.push(s.type, s.id);
7714
+ }
7715
+ if (dstF) {
7716
+ const d = resolveRef(env, dstF);
7717
+ where += " AND dst_type=? AND dst_id=?";
7718
+ params.push(d.type, d.id);
7719
+ }
7720
+ if (relF) {
7721
+ where += " AND rel=?";
7722
+ params.push(relF);
7723
+ }
7724
+ return env.db.all(`SELECT * FROM relations WHERE ${where} ORDER BY rowid DESC LIMIT 200`, params);
7725
+ })().map((r) => ({
7726
+ src: `${r.src_type}:${r.src_id}`,
7727
+ dst: `${r.dst_type}:${r.dst_id}`,
7728
+ rel: r.rel,
7729
+ src_display: resolveId(env, r.src_type, r.src_id),
7730
+ dst_display: resolveId(env, r.dst_type, r.dst_id)
7731
+ }));
7732
+ if (ctx.json) {
7733
+ console.log(JSON.stringify(outRows, null, 2));
7734
+ return 0;
7735
+ }
7736
+ head(ctx, `Relations (${outRows.length})`);
7737
+ table(ctx, outRows, [
7738
+ "src",
7739
+ "rel",
7740
+ "dst"
7741
+ ], [
7742
+ "src_display",
7743
+ "rel",
7744
+ "dst_display"
7745
+ ]);
7746
+ return 0;
7747
+ }
7748
+ bad(ctx, "usage: rel [add|rm|list] ... (see ygl-trees help)");
7749
+ return 2;
7750
+ }
7751
+ //#endregion
7752
+ //#region packages/research/produce/src/produce-cmds.ts
7753
+ /** produce group — evidence-backed outline → sections → report.
7754
+ * Writes run on the TypeORM write service (src/writes.ts); reads stay on
7755
+ * Store until their own migration batch. */
7756
+ var produce_cmds_exports = /* @__PURE__ */ require_rolldown_runtime.__exportAll({ run: () => run$5 });
7757
+ init_fmt();
7758
+ require_text.init_text();
7759
+ init_orm();
7760
+ init_writes();
7761
+ function run$5(env, action, rest, args) {
7762
+ const { ctx, store } = env;
7763
+ if (env.group === "task") {
7764
+ const map = {
7765
+ add: "task-add",
7766
+ list: "task-list",
7767
+ doing: "task-status",
7768
+ done: "task-status",
7769
+ todo: "task-status"
7770
+ };
7771
+ if (!action || !map[action]) {
7772
+ bad(ctx, "usage: task add \"<title>\" [--note ...] | list [--status todo|doing|done] | todo|doing|done <id>");
7773
+ return 2;
7774
+ }
7775
+ if (map[action] === "task-status") rest = [rest[0], action];
7776
+ action = map[action];
7777
+ }
7778
+ let project;
7779
+ try {
7780
+ project = store.projectResolve(args.flags.p ? String(args.flags.p) : void 0);
7781
+ } catch (e) {
7782
+ bad(ctx, e.message);
7783
+ return 1;
7784
+ }
7785
+ const pid = project.id;
7786
+ const readBody = () => {
7787
+ if (args.flags.body) return String(args.flags.body);
7788
+ if (args.flags.f) return node_fs.readFileSync(String(args.flags.f), "utf8");
7789
+ return "";
7790
+ };
7791
+ switch (action) {
7792
+ case "outline": {
7793
+ const claims = store.claimList(pid);
7794
+ const lines = [`# ${project.name}`, ""];
7795
+ if (project.question) lines.push(`> ${project.question}`, "");
7796
+ lines.push("## 结论要点", "");
7797
+ if (!claims.length) lines.push("_(尚无 claim,先跑 research claim add)_", "");
7798
+ claims.forEach((c, i) => {
7799
+ const mark = c.status === "supported" ? "[已证实]" : c.status === "refuted" ? "[已推翻]" : c.status === "contested" ? "[存争议]" : "[待验证]";
7800
+ lines.push(`${i + 1}. ${mark} ${c.text}`);
7801
+ });
7802
+ lines.push("", "## 证据基础", "");
7803
+ const ev = store.evidenceList(pid);
7804
+ if (!ev.length) lines.push("_(尚无 evidence)_", "");
7805
+ ev.forEach((e, i) => lines.push(`- [${i + 1}] ${e.source}:${require_text.clip(e.quote, 80)}`));
7806
+ const content = lines.join("\n");
7807
+ return getOrm(env.db.path).then((ds) => artifactAdd(ds, pid, "outline", `${project.name} outline`, content)).then((r) => {
7808
+ if (ctx.json) console.log(JSON.stringify(r.row, null, 2));
7809
+ else {
7810
+ ok(ctx, `outline generated: ${r.row.id} (v${r.version})`);
7811
+ out(ctx, content);
7812
+ }
7813
+ return 0;
7814
+ }).catch((e) => {
7815
+ bad(ctx, e?.message || String(e));
7535
7816
  return 1;
7536
7817
  });
7537
7818
  }
7538
- case "task-add": {
7539
- if (rejectBare(ctx, args.flags, ["note"])) return 2;
7540
- const title = rest.join(" ");
7541
- if (!title) {
7542
- bad(ctx, "usage: task add \"<title>\" [--note ...]");
7819
+ case "section": {
7820
+ const title = rest[0] || String(args.flags.title || "");
7821
+ const body = readBody();
7822
+ if (!title || !body) {
7823
+ bad(ctx, "usage: produce section <title> --body|-f <file>");
7543
7824
  return 2;
7544
7825
  }
7545
- const note = String(args.flags.note || "");
7546
- return getOrm(env.db.path).then((ds) => taskAdd(ds, pid, title, note)).then((row) => {
7547
- if (ctx.json) console.log(JSON.stringify(row, null, 2));
7548
- else ok(ctx, `task added: ${row.id} ${title}`);
7826
+ return getOrm(env.db.path).then((ds) => artifactAdd(ds, pid, "section", title, body)).then((r) => {
7827
+ if (ctx.json) console.log(JSON.stringify(r.row, null, 2));
7828
+ else ok(ctx, `section saved: ${r.row.id} (v${r.version})`);
7549
7829
  return 0;
7550
7830
  }).catch((e) => {
7551
7831
  bad(ctx, e?.message || String(e));
7552
7832
  return 1;
7553
7833
  });
7554
7834
  }
7835
+ case "spec": {
7836
+ const title = rest[0] || String(args.flags.title || "");
7837
+ const body = readBody();
7838
+ if (args.flags["design-type"] !== void 0) {
7839
+ bad(ctx, "--design-type belongs to 'produce design' (spec does not take it; if this is a design decision, use: produce design <title> --design-type decision)");
7840
+ return 2;
7841
+ }
7842
+ if (!title || !body) {
7843
+ bad(ctx, "usage: produce spec <title> --body|-f <file>");
7844
+ return 2;
7845
+ }
7846
+ return getOrm(env.db.path).then((ds) => artifactAdd(ds, pid, "spec", title, body)).then((r) => {
7847
+ if (ctx.json) console.log(JSON.stringify(r.row, null, 2));
7848
+ else ok(ctx, `spec saved: ${r.row.id} (v${r.version})`);
7849
+ return 0;
7850
+ }).catch((e) => {
7851
+ bad(ctx, e?.message || String(e));
7852
+ return 1;
7853
+ });
7854
+ }
7855
+ case "design": {
7856
+ const title = rest[0] || String(args.flags.title || "");
7857
+ const body = readBody();
7858
+ const designType = String(args.flags["design-type"] || "arch");
7859
+ if (!title || !body) {
7860
+ bad(ctx, "usage: produce design <title> --body|-f <file> [--design-type ui-ux|arch|decision]");
7861
+ return 2;
7862
+ }
7863
+ if (![
7864
+ "ui-ux",
7865
+ "arch",
7866
+ "decision"
7867
+ ].includes(designType)) {
7868
+ bad(ctx, "--design-type must be one of ui-ux|arch|decision");
7869
+ return 2;
7870
+ }
7871
+ return getOrm(env.db.path).then((ds) => artifactAdd(ds, pid, "design", title, body, "markdown", JSON.stringify({ design_type: designType }))).then((r) => {
7872
+ if (ctx.json) console.log(JSON.stringify(r.row, null, 2));
7873
+ else ok(ctx, `design saved: ${r.row.id} (v${r.version}) [${designType}]`);
7874
+ return 0;
7875
+ }).catch((e) => {
7876
+ bad(ctx, e?.message || String(e));
7877
+ return 1;
7878
+ });
7879
+ }
7880
+ case "task-add": {
7881
+ if (rejectBare(ctx, args.flags, ["note", "node"])) return 2;
7882
+ const title = rest.join(" ");
7883
+ if (!title) {
7884
+ bad(ctx, "usage: task add \"<title>\" [--note ...] [--node <tree>/<key>]");
7885
+ return 2;
7886
+ }
7887
+ const note = String(args.flags.note || "");
7888
+ const nodeRef = args.flags.node !== void 0 ? String(args.flags.node) : "";
7889
+ return getOrm(env.db.path).then((ds) => taskAdd(ds, pid, title, note)).then((row) => {
7890
+ if (!nodeRef) {
7891
+ if (ctx.json) console.log(JSON.stringify(row, null, 2));
7892
+ else ok(ctx, `task added: ${row.id} ${title}`);
7893
+ return 0;
7894
+ }
7895
+ return relAdd(env, `node:${nodeRef}`, `task:${row.id}`, "tracked-by").then((r) => {
7896
+ if (ctx.json) console.log(JSON.stringify({
7897
+ ...row,
7898
+ node: nodeRef,
7899
+ relation: "tracked-by"
7900
+ }, null, 2));
7901
+ else ok(ctx, `task added: ${row.id} ${title} (tracked-by node:${nodeRef})`);
7902
+ return 0;
7903
+ });
7904
+ }).catch((e) => {
7905
+ bad(ctx, e?.message || String(e));
7906
+ return e instanceof RelError && e.code === 2 ? 2 : 1;
7907
+ });
7908
+ }
7555
7909
  case "task-list": {
7556
7910
  const rows = store.db.all("SELECT id, title, meta, created FROM artifacts WHERE project_id=? AND kind='task' ORDER BY created DESC", [pid]).map((r) => {
7557
7911
  let meta = {};
@@ -8086,6 +8440,41 @@ async function condAdd(ds, treeId, keyOrId, o) {
8086
8440
  return (await mgr.query(`SELECT * FROM node_conditions WHERE id=?`, [id]))[0];
8087
8441
  });
8088
8442
  }
8443
+ /** 0.81: `node cond rm <key> --name <n>` — remove one condition by natural
8444
+ * key. Throws "node not found" / "condition not found: <name> (node X has:
8445
+ * ...)" (named, per frozen contract ④). Mirrors condAdd: transactional +
8446
+ * event (node.cond-rm) + markDirty. */
8447
+ async function condRemove(ds, treeId, keyOrId, name) {
8448
+ return ds.transaction(async (mgr) => {
8449
+ const n = await mgr.query(`SELECT id FROM nodes WHERE tree_id=? AND (key=? OR id=?)`, [
8450
+ treeId,
8451
+ keyOrId,
8452
+ keyOrId
8453
+ ]);
8454
+ if (!n.length) throw new Error(`node not found: ${keyOrId}`);
8455
+ const existing = await mgr.query(`SELECT * FROM node_conditions WHERE node_id=? AND name=?`, [n[0].id, name]);
8456
+ if (!existing.length) {
8457
+ const have = (await mgr.query(`SELECT name FROM node_conditions WHERE node_id=?`, [n[0].id])).map((r) => r.name).join(", ") || "(none)";
8458
+ throw new Error(`condition not found: ${name} (node ${keyOrId} has: ${have})`);
8459
+ }
8460
+ const row = existing[0];
8461
+ await mgr.query(`DELETE FROM node_conditions WHERE id=?`, [row.id]);
8462
+ await markDirty(mgr, treeId);
8463
+ await logEvent(mgr, "node.cond-rm", row.id, {
8464
+ tree: treeId,
8465
+ node: keyOrId,
8466
+ name,
8467
+ kind: row.kind,
8468
+ op: row.op,
8469
+ required: !!row.required,
8470
+ effect: row.effect
8471
+ });
8472
+ return {
8473
+ ...row,
8474
+ removed: true
8475
+ };
8476
+ });
8477
+ }
8089
8478
  async function stateSet(ds, treeId, keyOrId, status, context = "default", level = 0) {
8090
8479
  return ds.transaction(async (mgr) => {
8091
8480
  const n = await mgr.query(`SELECT id FROM nodes WHERE tree_id=? AND (key=? OR id=?)`, [
@@ -8316,12 +8705,12 @@ async function importTree(ds, spec, opts = {}) {
8316
8705
  * ygl-trees node validate sanity checks
8317
8706
  * ygl-trees node export [--out file] portable JSON
8318
8707
  */
8319
- var cmds_exports$1 = /* @__PURE__ */ require_rolldown_runtime.__exportAll({ run: () => run$5 });
8708
+ var cmds_exports$1 = /* @__PURE__ */ require_rolldown_runtime.__exportAll({ run: () => run$4 });
8320
8709
  init_fmt();
8321
8710
  init_orm();
8322
8711
  init_eval();
8323
8712
  require_text.init_text();
8324
- function run$5(env, action, rest, args) {
8713
+ function run$4(env, action, rest, args) {
8325
8714
  const { ctx, db } = env;
8326
8715
  const trees = new TreeStore(db);
8327
8716
  const sel = args.flags.t ? String(args.flags.t) : void 0;
@@ -8421,6 +8810,167 @@ function run$5(env, action, rest, args) {
8421
8810
  if (positionalTree) rest = rest.slice(1);
8422
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));
8423
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
+ }
8424
8974
  let tree;
8425
8975
  try {
8426
8976
  tree = positionalTree || trees.treeResolve(sel);
@@ -8505,10 +9055,25 @@ function run$5(env, action, rest, args) {
8505
9055
  });
8506
9056
  }
8507
9057
  if (sub === "cond") {
8508
- if (!key) {
8509
- 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]");
9058
+ let cKey = key;
9059
+ const cAct = key === "add" ? "add" : key === "rm" || key === "remove" ? "rm" : "";
9060
+ if (cAct) cKey = rest[1];
9061
+ if (cAct === "rm") {
9062
+ const name = flagStr("name");
9063
+ if (!cKey || !name) {
9064
+ bad(ctx, "usage: node cond rm <key> --name <cond>");
9065
+ return 2;
9066
+ }
9067
+ return runTreeWrite((ds) => condRemove(ds, tid, cKey, name), (r) => {
9068
+ ok(ctx, `condition removed: ${cKey} · ${name} (was ${r.required ? "必检" : "可选"}/${r.effect})`);
9069
+ return 0;
9070
+ });
9071
+ }
9072
+ if (!cKey) {
9073
+ 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]");
8510
9074
  return 2;
8511
9075
  }
9076
+ const k = cKey;
8512
9077
  if (rejectBare(ctx, args.flags, [
8513
9078
  "name",
8514
9079
  "value",
@@ -8533,7 +9098,7 @@ function run$5(env, action, rest, args) {
8533
9098
  if (validateEnum(ctx, args.flags, "effect", ["gate", "cost_mul"])) return 2;
8534
9099
  const name = flagStr("name");
8535
9100
  if (!name) {
8536
- table(ctx, trees.condList(tid, key), [
9101
+ table(ctx, trees.condList(tid, k), [
8537
9102
  "id",
8538
9103
  "name",
8539
9104
  "kind",
@@ -8589,7 +9154,7 @@ function run$5(env, action, rest, args) {
8589
9154
  value = packEvalScoreSpec(q, levels, min);
8590
9155
  }
8591
9156
  }
8592
- return runTreeWrite((ds) => condAdd(ds, tid, key, {
9157
+ return runTreeWrite((ds) => condAdd(ds, tid, k, {
8593
9158
  name,
8594
9159
  kind: flagStr("kind", "flag"),
8595
9160
  expr: flagStr("expr") || void 0,
@@ -8599,7 +9164,7 @@ function run$5(env, action, rest, args) {
8599
9164
  effect: flagStr("effect", "gate"),
8600
9165
  weight: flagNum("weight", 1)
8601
9166
  }), (c) => {
8602
- ack(ctx, c, `condition added: ${key} · ${c.name} [${c.required ? "必检" : "可选"}/${c.effect}]`);
9167
+ ack(ctx, c, `condition added: ${k} · ${c.name} [${c.required ? "必检" : "可选"}/${c.effect}]`);
8603
9168
  return 0;
8604
9169
  });
8605
9170
  }
@@ -9081,6 +9646,14 @@ function run$5(env, action, rest, args) {
9081
9646
  return 0;
9082
9647
  }
9083
9648
  if (sub === "list" || sub === void 0) {
9649
+ for (const f of Object.keys(args.flags || {})) if (![
9650
+ "t",
9651
+ "json",
9652
+ "no-color"
9653
+ ].includes(f)) {
9654
+ bad(ctx, `unknown flag --${f} for 'node list' — tree selection is '-t <tree>' (positional <tree> also works)`);
9655
+ return 2;
9656
+ }
9084
9657
  table(ctx, trees.nodeList(tid), [
9085
9658
  "key",
9086
9659
  "name",
@@ -9311,12 +9884,12 @@ function loadStateFile(args, shared) {
9311
9884
  //#region packages/knowledge/skills/src/skill-cmds.ts
9312
9885
  /** skill group — the L1/L2/L3 registry agents query before acting.
9313
9886
  * Writes run on the TypeORM write service (src/writes.ts). */
9314
- var skill_cmds_exports = /* @__PURE__ */ require_rolldown_runtime.__exportAll({ run: () => run$4 });
9887
+ var skill_cmds_exports = /* @__PURE__ */ require_rolldown_runtime.__exportAll({ run: () => run$3 });
9315
9888
  init_fmt();
9316
9889
  require_text.init_text();
9317
9890
  init_orm();
9318
9891
  init_writes();
9319
- function run$4(env, action, rest, args) {
9892
+ function run$3(env, action, rest, args) {
9320
9893
  const { ctx, store } = env;
9321
9894
  const name = rest[0];
9322
9895
  const need = (v, what) => {
@@ -9418,7 +9991,7 @@ function run$4(env, action, rest, args) {
9418
9991
  case "add": {
9419
9992
  const n = args.flags.name ? String(args.flags.name) : "";
9420
9993
  const file = args.flags.f ? String(args.flags.f) : "";
9421
- if (file) return run$4(env, "import", [file], args);
9994
+ if (file) return run$3(env, "import", [file], args);
9422
9995
  if (!need(n, "--name")) return 2;
9423
9996
  const body = args.flags.body ? String(args.flags.body) : args.flags["body-file"] ? node_fs.readFileSync(String(args.flags["body-file"]), "utf8") : "";
9424
9997
  const spec = {
@@ -9646,12 +10219,13 @@ function wrap(text, indent = 4) {
9646
10219
  //#region packages/knowledge/skills/src/cmds.ts
9647
10220
  /** kb group — long-term knowledge: store, retrieve, connect, promote to skill.
9648
10221
  * Writes (add/ingest) run on the TypeORM write service (src/writes.ts). */
9649
- var cmds_exports = /* @__PURE__ */ require_rolldown_runtime.__exportAll({ run: () => run$3 });
10222
+ var cmds_exports = /* @__PURE__ */ require_rolldown_runtime.__exportAll({ run: () => run$2 });
9650
10223
  init_fmt();
9651
10224
  require_text.init_text();
10225
+ init_eventbus();
9652
10226
  init_orm();
9653
10227
  init_writes();
9654
- function run$3(env, action, rest, args) {
10228
+ function run$2(env, action, rest, args) {
9655
10229
  const { ctx, store } = env;
9656
10230
  const readBody = () => {
9657
10231
  if (args.flags.body) return String(args.flags.body);
@@ -9805,472 +10379,194 @@ function run$3(env, action, rest, args) {
9805
10379
  bad(ctx, `note not found: ${id}`);
9806
10380
  return 1;
9807
10381
  }
9808
- store.db.run("DELETE FROM kb_fts WHERE id=?", [id]);
9809
- store.db.run("DELETE FROM kb_notes WHERE id=?", [id]);
9810
- ok(ctx, `note removed: ${id} ${String(note.title || "").slice(0, 40)}`);
9811
- return 0;
9812
- }
9813
- case "show": {
9814
- const n = store.kbGet(rest[0]);
9815
- if (!n) {
9816
- bad(ctx, `note not found: ${rest[0]}`);
9817
- return 1;
9818
- }
9819
- if (ctx.json) {
9820
- console.log(JSON.stringify(n, null, 2));
9821
- return 0;
9822
- }
9823
- head(ctx, n.title);
9824
- show(ctx, {
9825
- id: n.id,
9826
- tags: n.tags || "-",
9827
- confidence: n.confidence,
9828
- reuse: n.reuse,
9829
- origin: n.origin || "-"
9830
- });
9831
- out(ctx, "");
9832
- out(ctx, n.body);
9833
- const links = store.kbLinks(n.id);
9834
- if (links.length) {
9835
- out(ctx, paint(C.bold, "\n links", ctx.color));
9836
- for (const l of links) out(ctx, ` - ${l.rel}: ${l.other}`);
9837
- }
9838
- return 0;
9839
- }
9840
- case "link": {
9841
- const a = store.kbGet(rest[0]);
9842
- const b = store.kbGet(rest[1]);
9843
- if (!a || !b) {
9844
- bad(ctx, "both notes must exist");
9845
- return 1;
9846
- }
9847
- store.kbLink(a.id, b.id, String(args.flags.rel || "related"));
9848
- ok(ctx, `linked ${a.title} --${args.flags.rel || "related"}--> ${b.title}`);
9849
- return 0;
9850
- }
9851
- case "ingest": {
9852
- if (!rest[0]) {
9853
- bad(ctx, "usage: kb ingest <artifactId> [--force]");
9854
- return 2;
9855
- }
9856
- const forceKb = args.flags.force === true || args.flags.force === "true";
9857
- return getOrm(env.db.path).then((ds) => kbIngestArtifact(ds, rest[0], forceKb)).then((r) => {
9858
- writeResult(ctx, r, (row) => `artifact ingested into kb: ${row.id} ${require_text.clip(row.title, 50)}`);
9859
- return 0;
9860
- }).catch((e) => {
9861
- bad(ctx, e?.message || String(e));
9862
- return 1;
9863
- });
9864
- }
9865
- case "promote": {
9866
- if (!rest[0]) {
9867
- bad(ctx, "usage: kb promote <noteId> [--name <skill>]");
9868
- return 2;
9869
- }
9870
- const note = store.kbGet(rest[0]);
9871
- if (!note) {
9872
- bad(ctx, `kb note not found: ${rest[0]}`);
9873
- return 1;
9874
- }
9875
- const name = args.flags.name ? String(args.flags.name) : note.title;
9876
- return getOrm(env.db.path).then((ds) => skillUpsert(ds, {
9877
- name,
9878
- description: deriveL1(note.body || "", note.title),
9879
- body: note.body,
9880
- tags: note.tags,
9881
- origin: `kb:${note.id}`
9882
- })).then((s) => {
9883
- ok(ctx, `promoted to skill: ${s.name}`);
9884
- out(ctx, paint(C.dim, ` L1: ${require_text.clip(String(s.description), 100)}`, ctx.color));
9885
- return 0;
9886
- }).catch((e) => {
9887
- bad(ctx, e?.message || String(e));
9888
- return 1;
9889
- });
9890
- }
9891
- case "import": {
9892
- const target = rest[0];
9893
- if (!target) {
9894
- bad(ctx, "usage: kb import <dir|file.md> [--tags ..]");
9895
- return 2;
9896
- }
9897
- let st;
9898
- try {
9899
- st = node_fs.statSync(target);
9900
- } catch {
9901
- bad(ctx, `not found: ${target}`);
9902
- return 1;
9903
- }
9904
- const files = [];
9905
- if (st.isFile()) files.push(target);
9906
- else for (const f of node_fs.readdirSync(target, { recursive: true })) if (String(f).toLowerCase().endsWith(".md")) files.push(node_path.join(target, String(f)));
9907
- if (!files.length) {
9908
- bad(ctx, `no .md files under ${target}`);
9909
- return 1;
9910
- }
9911
- files.sort();
9912
- const MAX_FILE_BYTES = 1048576;
9913
- const skipped = [];
9914
- const importable = files.filter((f) => {
9915
- const sz = node_fs.statSync(f).size;
9916
- if (sz > MAX_FILE_BYTES && !args.flags.force) {
9917
- skipped.push({
9918
- file: f,
9919
- size: `${(sz / 1024 / 1024).toFixed(1)}MB`
9920
- });
9921
- return false;
9922
- }
9923
- return true;
9924
- });
9925
- if (skipped.length && !ctx.json) {
9926
- out(ctx, paint(C.yellow, ` ! skipped ${skipped.length} file(s) >1MB (--force to include):`, ctx.color));
9927
- for (const sk of skipped) out(ctx, paint(C.dim, ` ${sk.file} (${sk.size})`, ctx.color));
9928
- }
9929
- if (!importable.length) {
9930
- bad(ctx, `all ${files.length} file(s) exceeded 1MB limit — use --force to import anyway`);
9931
- return 1;
9932
- }
9933
- const items = importable.map((f) => {
9934
- const raw = node_fs.readFileSync(f, "utf8");
9935
- const { meta, body } = require_text.parseFrontmatter(raw);
9936
- const heading = /^#\s+(.+)\s*$/m.exec(body)?.[1]?.trim();
9937
- return {
9938
- title: meta.title || meta.name || heading || node_path.basename(f, ".md"),
9939
- body,
9940
- tags: meta.tags || String(args.flags.tags || ""),
9941
- source_url: meta.url || "",
9942
- confidence: meta.confidence || "B",
9943
- origin: `import:${f}`
9944
- };
9945
- });
9946
- return getOrm(env.db.path).then((ds) => kbImportBulk(ds, items)).then((r) => {
9947
- if (ctx.json) console.log(JSON.stringify({
9948
- added: r.added,
9949
- duplicates: r.dups,
9950
- files: files.length,
9951
- target
9952
- }, null, 2));
9953
- else ok(ctx, `kb import: ${r.added} added, ${r.dups} duplicate(s) kept — ${files.length} file(s) from ${target}`);
9954
- return 0;
9955
- }).catch((e) => {
9956
- bad(ctx, e?.message || String(e));
9957
- return 1;
9958
- });
9959
- }
9960
- case "":
9961
- out(ctx, paint(C.dim, " kb: add | import <dir|file> | search | list | show | link | ingest <artifactId> | promote <noteId>", ctx.color));
9962
- return 0;
9963
- default:
9964
- bad(ctx, `unknown kb action: ${action}`);
9965
- return 2;
9966
- }
9967
- }
9968
- //#endregion
9969
- //#region packages/ops/tools/src/rels.ts
9970
- var rels_exports = /* @__PURE__ */ require_rolldown_runtime.__exportAll({
9971
- REL_TYPES: () => REL_TYPES,
9972
- RelError: () => RelError,
9973
- relAdd: () => relAdd,
9974
- relRm: () => relRm,
9975
- resolveId: () => resolveId,
9976
- resolveRef: () => resolveRef,
9977
- run: () => run$2
9978
- });
9979
- init_fmt();
9980
- require_text.init_text();
9981
- init_orm();
9982
- const REL_TYPES = [
9983
- "node",
9984
- "claim",
9985
- "evidence",
9986
- "source",
9987
- "query",
9988
- "kb",
9989
- "artifact",
9990
- "skill"
9991
- ];
9992
- /** Resolve one `<type>:<ref>` against the database → { type, id, display }.
9993
- * Throws RelError(code) on unknown type / missing row. */
9994
- var RelError = class extends Error {
9995
- code;
9996
- constructor(code, message) {
9997
- super(message);
9998
- this.code = code;
9999
- }
10000
- };
10001
- const DISPLAY = {
10002
- node: (db, ref) => {
10003
- const [treeName, key] = ref.includes("/") ? [ref.slice(0, ref.indexOf("/")), ref.slice(ref.indexOf("/") + 1)] : [null, ref];
10004
- const cur = treeName || db.metaGet("current_tree") || "";
10005
- 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=?)", [
10006
- cur,
10007
- cur,
10008
- key,
10009
- key
10010
- ]);
10011
- if (!rows.length) throw new RelError(1, `node not found: ${ref}`);
10012
- return {
10013
- id: String(rows[0].id),
10014
- display: String(rows[0].name || rows[0].key)
10015
- };
10016
- },
10017
- claim: (db, ref) => {
10018
- const r = db.get("SELECT id, text FROM claims WHERE id=? LIMIT 1", [ref]);
10019
- if (!r) throw new RelError(1, `claim not found: ${ref}`);
10020
- return {
10021
- id: String(r.id),
10022
- display: String(r.text).slice(0, 60)
10023
- };
10024
- },
10025
- evidence: (db, ref) => {
10026
- const r = db.get("SELECT id, quote FROM evidence WHERE id=? LIMIT 1", [ref]);
10027
- if (!r) throw new RelError(1, `evidence not found: ${ref}`);
10028
- return {
10029
- id: String(r.id),
10030
- display: String(r.quote).slice(0, 60)
10031
- };
10032
- },
10033
- source: (db, ref) => {
10034
- const r = db.get("SELECT id, title FROM sources WHERE id=? LIMIT 1", [ref]);
10035
- if (!r) throw new RelError(1, `source not found: ${ref}`);
10036
- return {
10037
- id: String(r.id),
10038
- display: String(r.title)
10039
- };
10040
- },
10041
- query: (db, ref) => {
10042
- const r = db.get("SELECT id, text FROM queries WHERE id=? LIMIT 1", [ref]);
10043
- if (!r) throw new RelError(1, `query not found: ${ref}`);
10044
- return {
10045
- id: String(r.id),
10046
- display: String(r.text).slice(0, 60)
10047
- };
10048
- },
10049
- kb: (db, ref) => {
10050
- const r = db.get("SELECT id, title FROM kb_notes WHERE id=? OR title=? LIMIT 1", [ref, ref]);
10051
- if (!r) throw new RelError(1, `kb note not found: ${ref}`);
10052
- return {
10053
- id: String(r.id),
10054
- display: String(r.title)
10055
- };
10056
- },
10057
- artifact: (db, ref) => {
10058
- const r = db.get("SELECT id, title FROM artifacts WHERE id=? OR title=? LIMIT 1", [ref, ref]);
10059
- if (!r) throw new RelError(1, `artifact not found: ${ref}`);
10060
- return {
10061
- id: String(r.id),
10062
- display: String(r.title)
10063
- };
10064
- },
10065
- skill: (db, ref) => {
10066
- const r = db.get("SELECT name, description FROM skills WHERE name=? LIMIT 1", [ref]);
10067
- if (!r) throw new RelError(1, `skill not found: ${ref}`);
10068
- return {
10069
- id: String(r.name),
10070
- display: String(r.name)
10071
- };
10072
- }
10073
- };
10074
- function resolveRef(env, ref) {
10075
- const i = ref.indexOf(":");
10076
- if (i < 1) throw new RelError(2, `bad reference '${ref}' — want <type>:<ref> where type ∈ ${REL_TYPES.join("|")}`);
10077
- const type = ref.slice(0, i);
10078
- const ident = ref.slice(i + 1);
10079
- const fn = DISPLAY[type];
10080
- if (!fn) throw new RelError(2, `unknown reference type '${type}' — want ${REL_TYPES.join("|")}`);
10081
- const r = fn(env.db, ident);
10082
- return {
10083
- type,
10084
- id: r.id,
10085
- display: r.display
10086
- };
10087
- }
10088
- /** Reverse lookup: type+id → display (for the webui related section). */
10089
- function resolveId(env, type, id) {
10090
- const fn = DISPLAY[type];
10091
- if (!fn) return id;
10092
- try {
10093
- return fn(env.db, id).display;
10094
- } catch {
10095
- return id;
10096
- }
10097
- }
10098
- async function relAdd(env, srcRef, dstRef, rel = "related") {
10099
- const src = resolveRef(env, srcRef);
10100
- const dst = resolveRef(env, dstRef);
10101
- return (await getOrm(env.db.path)).transaction(async (mgr) => {
10102
- const ex = await mgr.query(`SELECT id FROM relations WHERE src_type=? AND src_id=? AND dst_type=? AND dst_id=? AND rel=?`, [
10103
- src.type,
10104
- src.id,
10105
- dst.type,
10106
- dst.id,
10107
- rel
10108
- ]);
10109
- if (ex.length) return {
10110
- row: (await mgr.query(`SELECT * FROM relations WHERE id=?`, [ex[0].id]))[0],
10111
- duplicate: true
10112
- };
10113
- const id = require_text.shortId([
10114
- "rel",
10115
- src.id,
10116
- dst.id,
10117
- rel
10118
- ], 10);
10119
- await mgr.query(`INSERT INTO relations(id,src_type,src_id,dst_type,dst_id,rel,weight,created) VALUES(?,?,?,?,?,?,1,?)`, [
10120
- id,
10121
- src.type,
10122
- src.id,
10123
- dst.type,
10124
- dst.id,
10125
- rel,
10126
- require_text.nowIso()
10127
- ]);
10128
- await mgr.query(`INSERT INTO events(id,ts,actor,kind,entity,entity_id,payload) VALUES(?,?,?,?,?,?,?)`, [
10129
- require_text.shortId([
10130
- "ev",
10131
- "rel.add",
10132
- id
10133
- ], 12),
10134
- require_text.nowIso(),
10135
- "cli",
10136
- "rel.add",
10137
- "relation",
10138
- id,
10139
- JSON.stringify({
10140
- src: `${src.type}:${src.id}`,
10141
- dst: `${dst.type}:${dst.id}`,
10142
- rel
10143
- })
10144
- ]);
10145
- return {
10146
- row: (await mgr.query(`SELECT * FROM relations WHERE id=?`, [id]))[0],
10147
- duplicate: false
10148
- };
10149
- });
10150
- }
10151
- async function relRm(env, srcRef, dstRef, rel = "related") {
10152
- const src = resolveRef(env, srcRef);
10153
- const dst = resolveRef(env, dstRef);
10154
- await (await getOrm(env.db.path)).transaction(async (mgr) => {
10155
- if (!(await mgr.query(`SELECT id FROM relations WHERE src_type=? AND src_id=? AND dst_type=? AND dst_id=? AND rel=?`, [
10156
- src.type,
10157
- src.id,
10158
- dst.type,
10159
- dst.id,
10160
- rel
10161
- ])).length) throw new RelError(1, `relation not found: ${srcRef} → ${dstRef} (${rel})`);
10162
- await mgr.query(`DELETE FROM relations WHERE src_type=? AND src_id=? AND dst_type=? AND dst_id=? AND rel=?`, [
10163
- src.type,
10164
- src.id,
10165
- dst.type,
10166
- dst.id,
10167
- rel
10168
- ]);
10169
- });
10170
- }
10171
- function run$2(env, action, rest, args) {
10172
- const { ctx } = env;
10173
- if (action === "add") {
10174
- const [a, b] = rest;
10175
- if (!a || !b) {
10176
- bad(ctx, "usage: rel add <type>:<ref> <type>:<ref> [--rel <type>]");
10177
- return 2;
10178
- }
10179
- const rel = String(args.flags.rel || "related");
10180
- return (async () => {
10382
+ store.db.run("DELETE FROM kb_fts WHERE id=?", [id]);
10383
+ store.db.run("DELETE FROM kb_notes WHERE id=?", [id]);
10181
10384
  try {
10182
- const r = await relAdd(env, a, b, rel);
10183
- if (ctx.json) {
10184
- console.log(JSON.stringify({
10185
- id: r.row.id,
10186
- src: `${r.row.src_type}:${r.row.src_id}`,
10187
- dst: `${r.row.dst_type}:${r.row.dst_id}`,
10188
- rel: r.row.rel,
10189
- duplicate: r.duplicate
10190
- }));
10191
- return 0;
10192
- }
10193
- if (r.duplicate) out(ctx, paint(C.yellow, ` ⚠ duplicate: ${a} ${rel} ${b} (already linked)`, ctx.color));
10194
- else ok(ctx, `linked: ${a} ${paint(C.dim, `—${rel}→`, ctx.color)} ${b}`);
10385
+ const ev = {
10386
+ id: `ev${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`,
10387
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
10388
+ actor: process.env.YGL_SESSION || "cli",
10389
+ kind: "kb.rm",
10390
+ entity: "kb_note",
10391
+ entity_id: id,
10392
+ payload: JSON.stringify({ title: String(note.title || "") }),
10393
+ level: "info"
10394
+ };
10395
+ store.db.run("INSERT INTO events(id,ts,actor,kind,entity,entity_id,payload,level) VALUES(?,?,?,?,?,?,?,?)", [
10396
+ ev.id,
10397
+ ev.ts,
10398
+ ev.actor,
10399
+ ev.kind,
10400
+ ev.entity,
10401
+ ev.entity_id,
10402
+ ev.payload,
10403
+ ev.level
10404
+ ]);
10405
+ emitEvent({
10406
+ kind: "kb.rm",
10407
+ entity: "kb_note",
10408
+ entityId: id,
10409
+ payload: JSON.stringify({ title: String(note.title || "") }),
10410
+ ts: ev.ts
10411
+ });
10412
+ } catch {}
10413
+ ok(ctx, `note removed: ${id} ${String(note.title || "").slice(0, 40)}`);
10414
+ return 0;
10415
+ }
10416
+ case "show": {
10417
+ const n = store.kbGet(rest[0]);
10418
+ if (!n) {
10419
+ bad(ctx, `note not found: ${rest[0]}`);
10420
+ return 1;
10421
+ }
10422
+ if (ctx.json) {
10423
+ console.log(JSON.stringify(n, null, 2));
10195
10424
  return 0;
10196
- } catch (e) {
10197
- if (e instanceof RelError) {
10198
- bad(ctx, e.message);
10199
- return e.code;
10200
- }
10201
- throw e;
10202
10425
  }
10203
- })();
10204
- }
10205
- if (action === "rm") {
10206
- const [a, b] = rest;
10207
- if (!a || !b) {
10208
- bad(ctx, "usage: rel rm <type>:<ref> <type>:<ref> [--rel <type>]");
10209
- return 2;
10426
+ head(ctx, n.title);
10427
+ show(ctx, {
10428
+ id: n.id,
10429
+ tags: n.tags || "-",
10430
+ confidence: n.confidence,
10431
+ reuse: n.reuse,
10432
+ origin: n.origin || "-"
10433
+ });
10434
+ out(ctx, "");
10435
+ out(ctx, n.body);
10436
+ const links = store.kbLinks(n.id);
10437
+ if (links.length) {
10438
+ out(ctx, paint(C.bold, "\n links", ctx.color));
10439
+ for (const l of links) out(ctx, ` - ${l.rel}: ${l.other}`);
10440
+ }
10441
+ return 0;
10210
10442
  }
10211
- const rel = String(args.flags.rel || "related");
10212
- return (async () => {
10213
- try {
10214
- await relRm(env, a, b, rel);
10215
- if (ctx.json) console.log(JSON.stringify({ removed: `${a} -${rel}-> ${b}` }));
10216
- else ok(ctx, `unlinked: ${a} (${rel}) ${b}`);
10443
+ case "link": {
10444
+ const a = store.kbGet(rest[0]);
10445
+ const b = store.kbGet(rest[1]);
10446
+ if (!a || !b) {
10447
+ bad(ctx, "both notes must exist");
10448
+ return 1;
10449
+ }
10450
+ store.kbLink(a.id, b.id, String(args.flags.rel || "related"));
10451
+ ok(ctx, `linked ${a.title} --${args.flags.rel || "related"}--> ${b.title}`);
10452
+ return 0;
10453
+ }
10454
+ case "ingest": {
10455
+ if (!rest[0]) {
10456
+ bad(ctx, "usage: kb ingest <artifactId> [--force]");
10457
+ return 2;
10458
+ }
10459
+ const forceKb = args.flags.force === true || args.flags.force === "true";
10460
+ return getOrm(env.db.path).then((ds) => kbIngestArtifact(ds, rest[0], forceKb)).then((r) => {
10461
+ writeResult(ctx, r, (row) => `artifact ingested into kb: ${row.id} ${require_text.clip(row.title, 50)}`);
10217
10462
  return 0;
10218
- } catch (e) {
10219
- if (e instanceof RelError) {
10220
- bad(ctx, e.message);
10221
- return e.code;
10222
- }
10223
- throw e;
10463
+ }).catch((e) => {
10464
+ bad(ctx, e?.message || String(e));
10465
+ return 1;
10466
+ });
10467
+ }
10468
+ case "promote": {
10469
+ if (!rest[0]) {
10470
+ bad(ctx, "usage: kb promote <noteId> [--name <skill>]");
10471
+ return 2;
10224
10472
  }
10225
- })();
10226
- }
10227
- if (action === "list" || action === "") {
10228
- const outRows = (() => {
10229
- const params = [];
10230
- let where = "1=1";
10231
- const srcF = String(args.flags.src || "");
10232
- const dstF = String(args.flags.dst || "");
10233
- const relF = String(args.flags.rel || "");
10234
- if (srcF) {
10235
- const s = resolveRef(env, srcF);
10236
- where += " AND src_type=? AND src_id=?";
10237
- params.push(s.type, s.id);
10473
+ const note = store.kbGet(rest[0]);
10474
+ if (!note) {
10475
+ bad(ctx, `kb note not found: ${rest[0]}`);
10476
+ return 1;
10238
10477
  }
10239
- if (dstF) {
10240
- const d = resolveRef(env, dstF);
10241
- where += " AND dst_type=? AND dst_id=?";
10242
- params.push(d.type, d.id);
10478
+ const name = args.flags.name ? String(args.flags.name) : note.title;
10479
+ return getOrm(env.db.path).then((ds) => skillUpsert(ds, {
10480
+ name,
10481
+ description: deriveL1(note.body || "", note.title),
10482
+ body: note.body,
10483
+ tags: note.tags,
10484
+ origin: `kb:${note.id}`
10485
+ })).then((s) => {
10486
+ ok(ctx, `promoted to skill: ${s.name}`);
10487
+ out(ctx, paint(C.dim, ` L1: ${require_text.clip(String(s.description), 100)}`, ctx.color));
10488
+ return 0;
10489
+ }).catch((e) => {
10490
+ bad(ctx, e?.message || String(e));
10491
+ return 1;
10492
+ });
10493
+ }
10494
+ case "import": {
10495
+ const target = rest[0];
10496
+ if (!target) {
10497
+ bad(ctx, "usage: kb import <dir|file.md> [--tags ..]");
10498
+ return 2;
10243
10499
  }
10244
- if (relF) {
10245
- where += " AND rel=?";
10246
- params.push(relF);
10500
+ let st;
10501
+ try {
10502
+ st = node_fs.statSync(target);
10503
+ } catch {
10504
+ bad(ctx, `not found: ${target}`);
10505
+ return 1;
10247
10506
  }
10248
- return env.db.all(`SELECT * FROM relations WHERE ${where} ORDER BY rowid DESC LIMIT 200`, params);
10249
- })().map((r) => ({
10250
- src: `${r.src_type}:${r.src_id}`,
10251
- dst: `${r.dst_type}:${r.dst_id}`,
10252
- rel: r.rel,
10253
- src_display: resolveId(env, r.src_type, r.src_id),
10254
- dst_display: resolveId(env, r.dst_type, r.dst_id)
10255
- }));
10256
- if (ctx.json) {
10257
- console.log(JSON.stringify(outRows, null, 2));
10258
- return 0;
10507
+ const files = [];
10508
+ if (st.isFile()) files.push(target);
10509
+ else for (const f of node_fs.readdirSync(target, { recursive: true })) if (String(f).toLowerCase().endsWith(".md")) files.push(node_path.join(target, String(f)));
10510
+ if (!files.length) {
10511
+ bad(ctx, `no .md files under ${target}`);
10512
+ return 1;
10513
+ }
10514
+ files.sort();
10515
+ const MAX_FILE_BYTES = 1048576;
10516
+ const skipped = [];
10517
+ const importable = files.filter((f) => {
10518
+ const sz = node_fs.statSync(f).size;
10519
+ if (sz > MAX_FILE_BYTES && !args.flags.force) {
10520
+ skipped.push({
10521
+ file: f,
10522
+ size: `${(sz / 1024 / 1024).toFixed(1)}MB`
10523
+ });
10524
+ return false;
10525
+ }
10526
+ return true;
10527
+ });
10528
+ if (skipped.length && !ctx.json) {
10529
+ out(ctx, paint(C.yellow, ` ! skipped ${skipped.length} file(s) >1MB (--force to include):`, ctx.color));
10530
+ for (const sk of skipped) out(ctx, paint(C.dim, ` ${sk.file} (${sk.size})`, ctx.color));
10531
+ }
10532
+ if (!importable.length) {
10533
+ bad(ctx, `all ${files.length} file(s) exceeded 1MB limit — use --force to import anyway`);
10534
+ return 1;
10535
+ }
10536
+ const items = importable.map((f) => {
10537
+ const raw = node_fs.readFileSync(f, "utf8");
10538
+ const { meta, body } = require_text.parseFrontmatter(raw);
10539
+ const heading = /^#\s+(.+)\s*$/m.exec(body)?.[1]?.trim();
10540
+ return {
10541
+ title: meta.title || meta.name || heading || node_path.basename(f, ".md"),
10542
+ body,
10543
+ tags: meta.tags || String(args.flags.tags || ""),
10544
+ source_url: meta.url || "",
10545
+ confidence: meta.confidence || "B",
10546
+ origin: `import:${f}`
10547
+ };
10548
+ });
10549
+ return getOrm(env.db.path).then((ds) => kbImportBulk(ds, items)).then((r) => {
10550
+ if (ctx.json) console.log(JSON.stringify({
10551
+ added: r.added,
10552
+ duplicates: r.dups,
10553
+ files: files.length,
10554
+ target
10555
+ }, null, 2));
10556
+ else ok(ctx, `kb import: ${r.added} added, ${r.dups} duplicate(s) kept — ${files.length} file(s) from ${target}`);
10557
+ return 0;
10558
+ }).catch((e) => {
10559
+ bad(ctx, e?.message || String(e));
10560
+ return 1;
10561
+ });
10259
10562
  }
10260
- head(ctx, `Relations (${outRows.length})`);
10261
- table(ctx, outRows, [
10262
- "src",
10263
- "rel",
10264
- "dst"
10265
- ], [
10266
- "src_display",
10267
- "rel",
10268
- "dst_display"
10269
- ]);
10270
- return 0;
10563
+ case "":
10564
+ out(ctx, paint(C.dim, " kb: add | import <dir|file> | search | list | show | link | ingest <artifactId> | promote <noteId>", ctx.color));
10565
+ return 0;
10566
+ default:
10567
+ bad(ctx, `unknown kb action: ${action}`);
10568
+ return 2;
10271
10569
  }
10272
- bad(ctx, "usage: rel [add|rm|list] ... (see ygl-trees help)");
10273
- return 2;
10274
10570
  }
10275
10571
  //#endregion
10276
10572
  //#region packages/ops/tools/src/webui.ts
@@ -11454,36 +11750,42 @@ function run(env, action, rest, args) {
11454
11750
  detail: `profile read failed: ${e?.message || e}`
11455
11751
  });
11456
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)`;
11457
11779
  {
11458
- const PHANTOM_SPECS = [
11459
- {
11460
- kind: "kb.add",
11461
- table: "kb_notes",
11462
- label: "kb"
11463
- },
11464
- {
11465
- kind: "source.add",
11466
- table: "sources",
11467
- label: "source"
11468
- },
11469
- {
11470
- kind: "evidence.add",
11471
- table: "evidence",
11472
- label: "evidence"
11473
- }
11474
- ];
11475
11780
  const phantoms = [];
11476
11781
  for (const spec of PHANTOM_SPECS) {
11477
- const rows = db.all(`SELECT e.entity_id id, e.ts FROM events e
11478
- WHERE e.kind='${spec.kind}'
11479
- AND e.ts >= datetime('now', '-2 days')
11480
- AND NOT EXISTS (SELECT 1 FROM ${spec.table} t WHERE t.id = e.entity_id)`);
11782
+ const rows = db.all(phantomSql(spec));
11481
11783
  for (const r of rows) phantoms.push(`${spec.label} ${r.id} (${String(r.ts).slice(0, 16)})`);
11482
11784
  }
11483
11785
  checks.push({
11484
11786
  name: "write_integrity",
11485
- status: phantoms.length ? "fail" : "pass",
11486
- detail: phantoms.length ? `${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)` : "recent write events all match stored rows"
11787
+ status: phantoms.length ? "warn" : "pass",
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"
11487
11789
  });
11488
11790
  }
11489
11791
  const failed = checks.filter((c) => c.status === "fail").length;
@@ -11526,6 +11828,27 @@ function run(env, action, rest, args) {
11526
11828
  fixes.push("stale lock removed (holder pid dead)");
11527
11829
  } catch {}
11528
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
+ }
11529
11852
  if (fixes.length) for (const f of fixes) ok(ctx, ` fixed: ${f}`);
11530
11853
  else out(ctx, paint(C.dim, " (--fix: no auto-fixable issues found)", ctx.color));
11531
11854
  }
@@ -11703,6 +12026,8 @@ ${paint(C.bold, "tree / node", true)} tech-tree node model
11703
12026
  (--eval: eval 题型经决策模型判定;
11704
12027
  boolean P / choice P(期望答案) / score ±1 灰带)
11705
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
11706
12031
  node path <key> dependency-order path to target (all locked ancestors; OR branches not optimized)
11707
12032
  node parents|children <key> direct relations
11708
12033
  node ancestors|descendants <key> transitive, via closure index
@@ -12152,7 +12477,8 @@ function generateLlmsManifest() {
12152
12477
  ["node cond <key> --name <c> --kind <k> [--op eval] [--value \"<question>\"]", "add condition (eval = decision model gate)"],
12153
12478
  ["node index", "rebuild closure + topo order"],
12154
12479
  ["node can <key> [--state k=v] [--eval]", "can unlock? (required vs optional + cost)"],
12155
- ["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"],
12156
12482
  ["node path <key>", "dependency-order path (OR branches not optimized)"],
12157
12483
  ["node unlock <key>", "mark unlocked"],
12158
12484
  ["node list", "list with state column (locked/unlocked)"],