ygl-trees 0.80.0 → 0.81.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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;
@@ -8505,10 +8894,25 @@ function run$5(env, action, rest, args) {
8505
8894
  });
8506
8895
  }
8507
8896
  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]");
8897
+ let cKey = key;
8898
+ const cAct = key === "add" ? "add" : key === "rm" || key === "remove" ? "rm" : "";
8899
+ if (cAct) cKey = rest[1];
8900
+ if (cAct === "rm") {
8901
+ const name = flagStr("name");
8902
+ if (!cKey || !name) {
8903
+ bad(ctx, "usage: node cond rm <key> --name <cond>");
8904
+ return 2;
8905
+ }
8906
+ return runTreeWrite((ds) => condRemove(ds, tid, cKey, name), (r) => {
8907
+ ok(ctx, `condition removed: ${cKey} · ${name} (was ${r.required ? "必检" : "可选"}/${r.effect})`);
8908
+ return 0;
8909
+ });
8910
+ }
8911
+ if (!cKey) {
8912
+ 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
8913
  return 2;
8511
8914
  }
8915
+ const k = cKey;
8512
8916
  if (rejectBare(ctx, args.flags, [
8513
8917
  "name",
8514
8918
  "value",
@@ -8533,7 +8937,7 @@ function run$5(env, action, rest, args) {
8533
8937
  if (validateEnum(ctx, args.flags, "effect", ["gate", "cost_mul"])) return 2;
8534
8938
  const name = flagStr("name");
8535
8939
  if (!name) {
8536
- table(ctx, trees.condList(tid, key), [
8940
+ table(ctx, trees.condList(tid, k), [
8537
8941
  "id",
8538
8942
  "name",
8539
8943
  "kind",
@@ -8589,7 +8993,7 @@ function run$5(env, action, rest, args) {
8589
8993
  value = packEvalScoreSpec(q, levels, min);
8590
8994
  }
8591
8995
  }
8592
- return runTreeWrite((ds) => condAdd(ds, tid, key, {
8996
+ return runTreeWrite((ds) => condAdd(ds, tid, k, {
8593
8997
  name,
8594
8998
  kind: flagStr("kind", "flag"),
8595
8999
  expr: flagStr("expr") || void 0,
@@ -8599,7 +9003,7 @@ function run$5(env, action, rest, args) {
8599
9003
  effect: flagStr("effect", "gate"),
8600
9004
  weight: flagNum("weight", 1)
8601
9005
  }), (c) => {
8602
- ack(ctx, c, `condition added: ${key} · ${c.name} [${c.required ? "必检" : "可选"}/${c.effect}]`);
9006
+ ack(ctx, c, `condition added: ${k} · ${c.name} [${c.required ? "必检" : "可选"}/${c.effect}]`);
8603
9007
  return 0;
8604
9008
  });
8605
9009
  }
@@ -9081,6 +9485,14 @@ function run$5(env, action, rest, args) {
9081
9485
  return 0;
9082
9486
  }
9083
9487
  if (sub === "list" || sub === void 0) {
9488
+ for (const f of Object.keys(args.flags || {})) if (![
9489
+ "t",
9490
+ "json",
9491
+ "no-color"
9492
+ ].includes(f)) {
9493
+ bad(ctx, `unknown flag --${f} for 'node list' — tree selection is '-t <tree>' (positional <tree> also works)`);
9494
+ return 2;
9495
+ }
9084
9496
  table(ctx, trees.nodeList(tid), [
9085
9497
  "key",
9086
9498
  "name",
@@ -9311,12 +9723,12 @@ function loadStateFile(args, shared) {
9311
9723
  //#region packages/knowledge/skills/src/skill-cmds.ts
9312
9724
  /** skill group — the L1/L2/L3 registry agents query before acting.
9313
9725
  * Writes run on the TypeORM write service (src/writes.ts). */
9314
- var skill_cmds_exports = /* @__PURE__ */ require_rolldown_runtime.__exportAll({ run: () => run$4 });
9726
+ var skill_cmds_exports = /* @__PURE__ */ require_rolldown_runtime.__exportAll({ run: () => run$3 });
9315
9727
  init_fmt();
9316
9728
  require_text.init_text();
9317
9729
  init_orm();
9318
9730
  init_writes();
9319
- function run$4(env, action, rest, args) {
9731
+ function run$3(env, action, rest, args) {
9320
9732
  const { ctx, store } = env;
9321
9733
  const name = rest[0];
9322
9734
  const need = (v, what) => {
@@ -9418,7 +9830,7 @@ function run$4(env, action, rest, args) {
9418
9830
  case "add": {
9419
9831
  const n = args.flags.name ? String(args.flags.name) : "";
9420
9832
  const file = args.flags.f ? String(args.flags.f) : "";
9421
- if (file) return run$4(env, "import", [file], args);
9833
+ if (file) return run$3(env, "import", [file], args);
9422
9834
  if (!need(n, "--name")) return 2;
9423
9835
  const body = args.flags.body ? String(args.flags.body) : args.flags["body-file"] ? node_fs.readFileSync(String(args.flags["body-file"]), "utf8") : "";
9424
9836
  const spec = {
@@ -9646,12 +10058,13 @@ function wrap(text, indent = 4) {
9646
10058
  //#region packages/knowledge/skills/src/cmds.ts
9647
10059
  /** kb group — long-term knowledge: store, retrieve, connect, promote to skill.
9648
10060
  * Writes (add/ingest) run on the TypeORM write service (src/writes.ts). */
9649
- var cmds_exports = /* @__PURE__ */ require_rolldown_runtime.__exportAll({ run: () => run$3 });
10061
+ var cmds_exports = /* @__PURE__ */ require_rolldown_runtime.__exportAll({ run: () => run$2 });
9650
10062
  init_fmt();
9651
10063
  require_text.init_text();
10064
+ init_eventbus();
9652
10065
  init_orm();
9653
10066
  init_writes();
9654
- function run$3(env, action, rest, args) {
10067
+ function run$2(env, action, rest, args) {
9655
10068
  const { ctx, store } = env;
9656
10069
  const readBody = () => {
9657
10070
  if (args.flags.body) return String(args.flags.body);
@@ -9784,493 +10197,215 @@ function run$3(env, action, rest, args) {
9784
10197
  "conf",
9785
10198
  "reuse",
9786
10199
  "updated"
9787
- ], [
9788
- "id",
9789
- "title",
9790
- "tags",
9791
- "confidence",
9792
- "reuse",
9793
- "updated"
9794
- ], "kb add --title ... --body ... | kb import <dir>");
9795
- return 0;
9796
- case "rm":
9797
- case "remove": {
9798
- const id = rest[0];
9799
- if (!id) {
9800
- bad(ctx, "usage: kb rm <id>");
9801
- return 2;
9802
- }
9803
- const note = store.kbGet(id);
9804
- if (!note) {
9805
- bad(ctx, `note not found: ${id}`);
9806
- return 1;
9807
- }
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 () => {
10200
+ ], [
10201
+ "id",
10202
+ "title",
10203
+ "tags",
10204
+ "confidence",
10205
+ "reuse",
10206
+ "updated"
10207
+ ], "kb add --title ... --body ... | kb import <dir>");
10208
+ return 0;
10209
+ case "rm":
10210
+ case "remove": {
10211
+ const id = rest[0];
10212
+ if (!id) {
10213
+ bad(ctx, "usage: kb rm <id>");
10214
+ return 2;
10215
+ }
10216
+ const note = store.kbGet(id);
10217
+ if (!note) {
10218
+ bad(ctx, `note not found: ${id}`);
10219
+ return 1;
10220
+ }
10221
+ store.db.run("DELETE FROM kb_fts WHERE id=?", [id]);
10222
+ store.db.run("DELETE FROM kb_notes WHERE id=?", [id]);
10181
10223
  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}`);
10224
+ const ev = {
10225
+ id: `ev${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`,
10226
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
10227
+ actor: process.env.YGL_SESSION || "cli",
10228
+ kind: "kb.rm",
10229
+ entity: "kb_note",
10230
+ entity_id: id,
10231
+ payload: JSON.stringify({ title: String(note.title || "") }),
10232
+ level: "info"
10233
+ };
10234
+ store.db.run("INSERT INTO events(id,ts,actor,kind,entity,entity_id,payload,level) VALUES(?,?,?,?,?,?,?,?)", [
10235
+ ev.id,
10236
+ ev.ts,
10237
+ ev.actor,
10238
+ ev.kind,
10239
+ ev.entity,
10240
+ ev.entity_id,
10241
+ ev.payload,
10242
+ ev.level
10243
+ ]);
10244
+ emitEvent({
10245
+ kind: "kb.rm",
10246
+ entity: "kb_note",
10247
+ entityId: id,
10248
+ payload: JSON.stringify({ title: String(note.title || "") }),
10249
+ ts: ev.ts
10250
+ });
10251
+ } catch {}
10252
+ ok(ctx, `note removed: ${id} ${String(note.title || "").slice(0, 40)}`);
10253
+ return 0;
10254
+ }
10255
+ case "show": {
10256
+ const n = store.kbGet(rest[0]);
10257
+ if (!n) {
10258
+ bad(ctx, `note not found: ${rest[0]}`);
10259
+ return 1;
10260
+ }
10261
+ if (ctx.json) {
10262
+ console.log(JSON.stringify(n, null, 2));
10195
10263
  return 0;
10196
- } catch (e) {
10197
- if (e instanceof RelError) {
10198
- bad(ctx, e.message);
10199
- return e.code;
10200
- }
10201
- throw e;
10202
10264
  }
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;
10265
+ head(ctx, n.title);
10266
+ show(ctx, {
10267
+ id: n.id,
10268
+ tags: n.tags || "-",
10269
+ confidence: n.confidence,
10270
+ reuse: n.reuse,
10271
+ origin: n.origin || "-"
10272
+ });
10273
+ out(ctx, "");
10274
+ out(ctx, n.body);
10275
+ const links = store.kbLinks(n.id);
10276
+ if (links.length) {
10277
+ out(ctx, paint(C.bold, "\n links", ctx.color));
10278
+ for (const l of links) out(ctx, ` - ${l.rel}: ${l.other}`);
10279
+ }
10280
+ return 0;
10210
10281
  }
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}`);
10282
+ case "link": {
10283
+ const a = store.kbGet(rest[0]);
10284
+ const b = store.kbGet(rest[1]);
10285
+ if (!a || !b) {
10286
+ bad(ctx, "both notes must exist");
10287
+ return 1;
10288
+ }
10289
+ store.kbLink(a.id, b.id, String(args.flags.rel || "related"));
10290
+ ok(ctx, `linked ${a.title} --${args.flags.rel || "related"}--> ${b.title}`);
10291
+ return 0;
10292
+ }
10293
+ case "ingest": {
10294
+ if (!rest[0]) {
10295
+ bad(ctx, "usage: kb ingest <artifactId> [--force]");
10296
+ return 2;
10297
+ }
10298
+ const forceKb = args.flags.force === true || args.flags.force === "true";
10299
+ return getOrm(env.db.path).then((ds) => kbIngestArtifact(ds, rest[0], forceKb)).then((r) => {
10300
+ writeResult(ctx, r, (row) => `artifact ingested into kb: ${row.id} ${require_text.clip(row.title, 50)}`);
10217
10301
  return 0;
10218
- } catch (e) {
10219
- if (e instanceof RelError) {
10220
- bad(ctx, e.message);
10221
- return e.code;
10222
- }
10223
- throw e;
10302
+ }).catch((e) => {
10303
+ bad(ctx, e?.message || String(e));
10304
+ return 1;
10305
+ });
10306
+ }
10307
+ case "promote": {
10308
+ if (!rest[0]) {
10309
+ bad(ctx, "usage: kb promote <noteId> [--name <skill>]");
10310
+ return 2;
10224
10311
  }
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);
10312
+ const note = store.kbGet(rest[0]);
10313
+ if (!note) {
10314
+ bad(ctx, `kb note not found: ${rest[0]}`);
10315
+ return 1;
10238
10316
  }
10239
- if (dstF) {
10240
- const d = resolveRef(env, dstF);
10241
- where += " AND dst_type=? AND dst_id=?";
10242
- params.push(d.type, d.id);
10317
+ const name = args.flags.name ? String(args.flags.name) : note.title;
10318
+ return getOrm(env.db.path).then((ds) => skillUpsert(ds, {
10319
+ name,
10320
+ description: deriveL1(note.body || "", note.title),
10321
+ body: note.body,
10322
+ tags: note.tags,
10323
+ origin: `kb:${note.id}`
10324
+ })).then((s) => {
10325
+ ok(ctx, `promoted to skill: ${s.name}`);
10326
+ out(ctx, paint(C.dim, ` L1: ${require_text.clip(String(s.description), 100)}`, ctx.color));
10327
+ return 0;
10328
+ }).catch((e) => {
10329
+ bad(ctx, e?.message || String(e));
10330
+ return 1;
10331
+ });
10332
+ }
10333
+ case "import": {
10334
+ const target = rest[0];
10335
+ if (!target) {
10336
+ bad(ctx, "usage: kb import <dir|file.md> [--tags ..]");
10337
+ return 2;
10243
10338
  }
10244
- if (relF) {
10245
- where += " AND rel=?";
10246
- params.push(relF);
10339
+ let st;
10340
+ try {
10341
+ st = node_fs.statSync(target);
10342
+ } catch {
10343
+ bad(ctx, `not found: ${target}`);
10344
+ return 1;
10247
10345
  }
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;
10346
+ const files = [];
10347
+ if (st.isFile()) files.push(target);
10348
+ 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)));
10349
+ if (!files.length) {
10350
+ bad(ctx, `no .md files under ${target}`);
10351
+ return 1;
10352
+ }
10353
+ files.sort();
10354
+ const MAX_FILE_BYTES = 1048576;
10355
+ const skipped = [];
10356
+ const importable = files.filter((f) => {
10357
+ const sz = node_fs.statSync(f).size;
10358
+ if (sz > MAX_FILE_BYTES && !args.flags.force) {
10359
+ skipped.push({
10360
+ file: f,
10361
+ size: `${(sz / 1024 / 1024).toFixed(1)}MB`
10362
+ });
10363
+ return false;
10364
+ }
10365
+ return true;
10366
+ });
10367
+ if (skipped.length && !ctx.json) {
10368
+ out(ctx, paint(C.yellow, ` ! skipped ${skipped.length} file(s) >1MB (--force to include):`, ctx.color));
10369
+ for (const sk of skipped) out(ctx, paint(C.dim, ` ${sk.file} (${sk.size})`, ctx.color));
10370
+ }
10371
+ if (!importable.length) {
10372
+ bad(ctx, `all ${files.length} file(s) exceeded 1MB limit — use --force to import anyway`);
10373
+ return 1;
10374
+ }
10375
+ const items = importable.map((f) => {
10376
+ const raw = node_fs.readFileSync(f, "utf8");
10377
+ const { meta, body } = require_text.parseFrontmatter(raw);
10378
+ const heading = /^#\s+(.+)\s*$/m.exec(body)?.[1]?.trim();
10379
+ return {
10380
+ title: meta.title || meta.name || heading || node_path.basename(f, ".md"),
10381
+ body,
10382
+ tags: meta.tags || String(args.flags.tags || ""),
10383
+ source_url: meta.url || "",
10384
+ confidence: meta.confidence || "B",
10385
+ origin: `import:${f}`
10386
+ };
10387
+ });
10388
+ return getOrm(env.db.path).then((ds) => kbImportBulk(ds, items)).then((r) => {
10389
+ if (ctx.json) console.log(JSON.stringify({
10390
+ added: r.added,
10391
+ duplicates: r.dups,
10392
+ files: files.length,
10393
+ target
10394
+ }, null, 2));
10395
+ else ok(ctx, `kb import: ${r.added} added, ${r.dups} duplicate(s) kept — ${files.length} file(s) from ${target}`);
10396
+ return 0;
10397
+ }).catch((e) => {
10398
+ bad(ctx, e?.message || String(e));
10399
+ return 1;
10400
+ });
10259
10401
  }
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;
10402
+ case "":
10403
+ out(ctx, paint(C.dim, " kb: add | import <dir|file> | search | list | show | link | ingest <artifactId> | promote <noteId>", ctx.color));
10404
+ return 0;
10405
+ default:
10406
+ bad(ctx, `unknown kb action: ${action}`);
10407
+ return 2;
10271
10408
  }
10272
- bad(ctx, "usage: rel [add|rm|list] ... (see ygl-trees help)");
10273
- return 2;
10274
10409
  }
10275
10410
  //#endregion
10276
10411
  //#region packages/ops/tools/src/webui.ts
@@ -11459,17 +11594,20 @@ function run(env, action, rest, args) {
11459
11594
  {
11460
11595
  kind: "kb.add",
11461
11596
  table: "kb_notes",
11462
- label: "kb"
11597
+ label: "kb",
11598
+ rm: "kb.rm"
11463
11599
  },
11464
11600
  {
11465
11601
  kind: "source.add",
11466
11602
  table: "sources",
11467
- label: "source"
11603
+ label: "source",
11604
+ rm: "source.rm"
11468
11605
  },
11469
11606
  {
11470
11607
  kind: "evidence.add",
11471
11608
  table: "evidence",
11472
- label: "evidence"
11609
+ label: "evidence",
11610
+ rm: "evidence.rm"
11473
11611
  }
11474
11612
  ];
11475
11613
  const phantoms = [];
@@ -11477,13 +11615,14 @@ function run(env, action, rest, args) {
11477
11615
  const rows = db.all(`SELECT e.entity_id id, e.ts FROM events e
11478
11616
  WHERE e.kind='${spec.kind}'
11479
11617
  AND e.ts >= datetime('now', '-2 days')
11480
- AND NOT EXISTS (SELECT 1 FROM ${spec.table} t WHERE t.id = e.entity_id)`);
11618
+ AND NOT EXISTS (SELECT 1 FROM ${spec.table} t WHERE t.id = e.entity_id)
11619
+ AND NOT EXISTS (SELECT 1 FROM events r WHERE r.kind='${spec.rm}' AND r.entity_id = e.entity_id)`);
11481
11620
  for (const r of rows) phantoms.push(`${spec.label} ${r.id} (${String(r.ts).slice(0, 16)})`);
11482
11621
  }
11483
11622
  checks.push({
11484
11623
  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"
11624
+ status: phantoms.length ? "warn" : "pass",
11625
+ detail: phantoms.length ? `${phantoms.length} unexplained write event(s) in last 48h: ${phantoms.slice(0, 3).join(", ")}${phantoms.length > 3 ? " …" : ""} — row missing without an rm event (deleted pre-0.81 or via db sql? otherwise: rollback / different workspace — check receipts' workspace line)` : "recent write events all match stored rows"
11487
11626
  });
11488
11627
  }
11489
11628
  const failed = checks.filter((c) => c.status === "fail").length;