ygl-trees 0.79.1 → 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.
@@ -2313,6 +2313,12 @@ var TreeStore = class {
2313
2313
  * + unlocked set + the caller's state bag. The eval API demands a context
2314
2314
  * package — bare state fell into the gray band 6/8 in the material's
2315
2315
  * routing test, so we ship everything the tree knows about this node.
2316
+ *
2317
+ * 0.80: the rel fabric joins the package — evidence/claim/kb summaries
2318
+ * attached to the node (either direction) land in `attachments`, so a
2319
+ * decided-by chain is visible to the eval model without manually
2320
+ * re-carrying facts via --state (user finding: P 0.30 → 0.92 gap).
2321
+ * Bounded: ≤12 rows, text clipped to 120 chars.
2316
2322
  */
2317
2323
  evalContext(treeId, keyOrId, stateBag, context = "default") {
2318
2324
  const n = this.nodeGet(treeId, keyOrId);
@@ -2336,7 +2342,7 @@ var TreeStore = class {
2336
2342
  effect: c.effect
2337
2343
  }));
2338
2344
  const unlocked = [...this.unlockedSet(treeId, context)].map((id) => idToKey.get(id) || id);
2339
- return {
2345
+ const pkg = {
2340
2346
  node: {
2341
2347
  key: n.key,
2342
2348
  name: n.name,
@@ -2350,6 +2356,62 @@ var TreeStore = class {
2350
2356
  unlocked,
2351
2357
  state: stateBag
2352
2358
  };
2359
+ const attachments = this.relAttachments(String(n.id));
2360
+ if (attachments.length) pkg.attachments = attachments;
2361
+ return pkg;
2362
+ }
2363
+ /** 0.80: rel-fabric summaries for the eval context package (both directions). */
2364
+ relAttachments(nodeId) {
2365
+ const rows = this.db.all(`SELECT rel, src_type, src_id, dst_type, dst_id FROM relations
2366
+ WHERE (src_type='node' AND src_id=?) OR (dst_type='node' AND dst_id=?)
2367
+ ORDER BY rowid LIMIT 12`, [nodeId, nodeId]);
2368
+ const clip = (v, n = 120) => String(v ?? "").slice(0, n);
2369
+ return rows.map((r) => {
2370
+ const srcIsMe = r.src_type === "node" && r.src_id === nodeId;
2371
+ const type = String(srcIsMe ? r.dst_type : r.src_type);
2372
+ const id = String(srcIsMe ? r.dst_id : r.src_id);
2373
+ const base = {
2374
+ rel: String(r.rel),
2375
+ type,
2376
+ id
2377
+ };
2378
+ const q = (sql) => this.db.get(sql, [id]);
2379
+ if (type === "claim") {
2380
+ const c = q("SELECT text, status, confidence FROM claims WHERE id=?");
2381
+ if (c) return {
2382
+ ...base,
2383
+ text: clip(c.text),
2384
+ status: c.status,
2385
+ confidence: c.confidence
2386
+ };
2387
+ } else if (type === "evidence") {
2388
+ const e = q("SELECT quote FROM evidence WHERE id=?");
2389
+ if (e) return {
2390
+ ...base,
2391
+ quote: clip(e.quote)
2392
+ };
2393
+ } else if (type === "kb") {
2394
+ const k = q("SELECT title FROM kb_notes WHERE id=?");
2395
+ if (k) return {
2396
+ ...base,
2397
+ title: clip(k.title, 80)
2398
+ };
2399
+ } else if (type === "source") {
2400
+ const s = q("SELECT title FROM sources WHERE id=?");
2401
+ if (s) return {
2402
+ ...base,
2403
+ title: clip(s.title, 80)
2404
+ };
2405
+ } else if (type === "node") {
2406
+ const nd = q("SELECT key, name FROM nodes WHERE id=?");
2407
+ if (nd) return {
2408
+ ...base,
2409
+ key: nd.key,
2410
+ name: clip(nd.name, 80)
2411
+ };
2412
+ }
2413
+ return base;
2414
+ });
2353
2415
  }
2354
2416
  /** Every node whose required gates are currently satisfied. */
2355
2417
  ready(treeId, context = "default", state = {}) {
@@ -4965,6 +5027,19 @@ function run$12(env, action, rest, args) {
4965
5027
  return 0;
4966
5028
  }
4967
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 ")}` : "";
4968
5043
  const days = db.all(`
4969
5044
  SELECT date(ts) as day, COUNT(*) as events,
4970
5045
  SUM(CASE WHEN kind LIKE 'project%' THEN 1 ELSE 0 END) as projects,
@@ -4972,21 +5047,23 @@ function run$12(env, action, rest, args) {
4972
5047
  SUM(CASE WHEN kind LIKE 'claim%' THEN 1 ELSE 0 END) as claims,
4973
5048
  SUM(CASE WHEN kind LIKE 'kb%' THEN 1 ELSE 0 END) as kb,
4974
5049
  SUM(CASE WHEN kind LIKE 'node%' THEN 1 ELSE 0 END) as nodes
4975
- FROM events GROUP BY date(ts) ORDER BY day
4976
- `);
5050
+ FROM events${where} GROUP BY date(ts) ORDER BY day
5051
+ `, params);
4977
5052
  if (!days.length) {
4978
5053
  ok(ctx, "no events yet");
4979
5054
  return 0;
4980
5055
  }
5056
+ const tail = conds.length ? db.all(`SELECT ts, actor, kind, entity_id FROM events${where} ORDER BY ts DESC LIMIT 20`, params) : [];
4981
5057
  if (ctx.json) {
4982
5058
  console.log(JSON.stringify({
4983
5059
  timeline: days,
4984
- total: days.reduce((s2, d) => s2 + d.events, 0)
5060
+ total: days.reduce((s2, d) => s2 + d.events, 0),
5061
+ ...tail.length ? { tail } : {}
4985
5062
  }, null, 2));
4986
5063
  return 0;
4987
5064
  }
4988
5065
  const maxEvents = Math.max(...days.map((d) => d.events));
4989
- head(ctx, "Workspace Timeline");
5066
+ head(ctx, `Workspace Timeline${actorF ? ` · actor=${actorF}` : ""}${sessionF ? ` · session=${sessionF}` : ""}`);
4990
5067
  let totalP = 0, totalS = 0, totalC = 0, totalK = 0, totalN = 0;
4991
5068
  for (const d of days) {
4992
5069
  totalP += d.projects;
@@ -5006,6 +5083,11 @@ function run$12(env, action, rest, args) {
5006
5083
  }
5007
5084
  out(ctx, "");
5008
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
+ }
5009
5091
  return 0;
5010
5092
  }
5011
5093
  case "workspace": {
@@ -6492,6 +6574,10 @@ init_writes();
6492
6574
  init_entities$1();
6493
6575
  /** Run a write through the TypeORM transactional write service and print
6494
6576
  * with JSON discipline. Keeps every write command 6 lines. */
6577
+ /** 0.80: compact timestamp for session:// adjudication urls (yyyymmddhhmm). */
6578
+ function nowCompact() {
6579
+ return (/* @__PURE__ */ new Date()).toISOString().replace(/[-:T]/g, "").slice(0, 12);
6580
+ }
6495
6581
  function runWrite(env, fn, human) {
6496
6582
  const { ctx } = env;
6497
6583
  return getOrm(env.db.path).then(fn).then((r) => {
@@ -6616,14 +6702,24 @@ function run$8(env, action, rest, args) {
6616
6702
  bad(ctx, "flag needs a value: --title \"...\" / --url \"...\" (bare flag was treated as boolean)");
6617
6703
  return 2;
6618
6704
  }
6619
- const title = String(titleRaw || "");
6620
- const url = String(urlRaw || "");
6705
+ let title = String(titleRaw || "");
6706
+ let url = String(urlRaw || "");
6707
+ const adjRaw = args.flags.adjudication;
6708
+ if (adjRaw === true) {
6709
+ bad(ctx, "flag needs a value: --adjudication \"<裁决摘要>\"");
6710
+ return 2;
6711
+ }
6712
+ if (adjRaw !== void 0 && adjRaw !== "") {
6713
+ const adj = String(adjRaw);
6714
+ if (!url) url = `session://adjudication/${nowCompact()}`;
6715
+ if (!title) title = adj.slice(0, 60);
6716
+ }
6621
6717
  if (!title && !url) {
6622
6718
  bad(ctx, "need --title and/or --url");
6623
6719
  return 2;
6624
6720
  }
6625
- if (url && !/^https?:\/\/|^file:\/\//.test(url)) {
6626
- bad(ctx, `--url must be http(s):// or file:// (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)}")`);
6627
6723
  return 2;
6628
6724
  }
6629
6725
  const spec = {
@@ -6895,11 +6991,16 @@ function run$8(env, action, rest, args) {
6895
6991
  if (sub === "claim") {
6896
6992
  if (subAction === "add") {
6897
6993
  if (rejectBare(ctx, args.flags, ["confidence", "text"])) return 2;
6898
- if (validateEnum(ctx, args.flags, "confidence", [
6899
- "A",
6900
- "B",
6901
- "C"
6902
- ])) 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
+ }
6903
7004
  const text = rest.slice(1).join(" ") || String(args.flags.text || "");
6904
7005
  if (!text) {
6905
7006
  bad(ctx, "missing claim text: research claim add \"<text>\"");
@@ -7331,151 +7432,480 @@ function run$7(env, action, rest, args) {
7331
7432
  }
7332
7433
  }
7333
7434
  //#endregion
7334
- //#region packages/research/produce/src/produce-cmds.ts
7335
- /** produce group — evidence-backed outline → sections → report.
7336
- * Writes run on the TypeORM write service (src/writes.ts); reads stay on
7337
- * Store until their own migration batch. */
7338
- 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
+ });
7339
7445
  init_fmt();
7340
7446
  require_text.init_text();
7341
7447
  init_orm();
7342
- init_writes();
7343
- function run$6(env, action, rest, args) {
7344
- const { ctx, store } = env;
7345
- if (env.group === "task") {
7346
- const map = {
7347
- add: "task-add",
7348
- list: "task-list",
7349
- doing: "task-status",
7350
- done: "task-status",
7351
- 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)
7352
7547
  };
7353
- if (!action || !map[action]) {
7354
- bad(ctx, "usage: task add \"<title>\" [--note ...] | list [--status todo|doing|done] | todo|doing|done <id>");
7355
- return 2;
7356
- }
7357
- if (map[action] === "task-status") rest = [rest[0], action];
7358
- action = map[action];
7359
7548
  }
7360
- 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;
7361
7568
  try {
7362
- project = store.projectResolve(args.flags.p ? String(args.flags.p) : void 0);
7363
- } catch (e) {
7364
- bad(ctx, e.message);
7365
- return 1;
7569
+ return fn(env.db, id).display;
7570
+ } catch {
7571
+ return id;
7366
7572
  }
7367
- const pid = project.id;
7368
- const readBody = () => {
7369
- if (args.flags.body) return String(args.flags.body);
7370
- if (args.flags.f) return node_fs.readFileSync(String(args.flags.f), "utf8");
7371
- return "";
7372
- };
7373
- switch (action) {
7374
- case "outline": {
7375
- const claims = store.claimList(pid);
7376
- const lines = [`# ${project.name}`, ""];
7377
- if (project.question) lines.push(`> ${project.question}`, "");
7378
- lines.push("## 结论要点", "");
7379
- if (!claims.length) lines.push("_(尚无 claim,先跑 research claim add)_", "");
7380
- claims.forEach((c, i) => {
7381
- const mark = c.status === "supported" ? "[已证实]" : c.status === "refuted" ? "[已推翻]" : c.status === "contested" ? "[存争议]" : "[待验证]";
7382
- lines.push(`${i + 1}. ${mark} ${c.text}`);
7383
- });
7384
- lines.push("", "## 证据基础", "");
7385
- const ev = store.evidenceList(pid);
7386
- if (!ev.length) lines.push("_(尚无 evidence)_", "");
7387
- ev.forEach((e, i) => lines.push(`- [${i + 1}] ${e.source}:${require_text.clip(e.quote, 80)}`));
7388
- const content = lines.join("\n");
7389
- return getOrm(env.db.path).then((ds) => artifactAdd(ds, pid, "outline", `${project.name} outline`, content)).then((r) => {
7390
- if (ctx.json) console.log(JSON.stringify(r.row, null, 2));
7391
- else {
7392
- ok(ctx, `outline generated: ${r.row.id} (v${r.version})`);
7393
- out(ctx, content);
7394
- }
7395
- return 0;
7396
- }).catch((e) => {
7397
- bad(ctx, e?.message || String(e));
7398
- return 1;
7399
- });
7400
- }
7401
- case "section": {
7402
- const title = rest[0] || String(args.flags.title || "");
7403
- const body = readBody();
7404
- if (!title || !body) {
7405
- bad(ctx, "usage: produce section <title> --body|-f <file>");
7406
- return 2;
7407
- }
7408
- return getOrm(env.db.path).then((ds) => artifactAdd(ds, pid, "section", title, body)).then((r) => {
7409
- if (ctx.json) console.log(JSON.stringify(r.row, null, 2));
7410
- else ok(ctx, `section saved: ${r.row.id} (v${r.version})`);
7411
- return 0;
7412
- }).catch((e) => {
7413
- bad(ctx, e?.message || String(e));
7414
- return 1;
7415
- });
7416
- }
7417
- case "spec": {
7418
- const title = rest[0] || String(args.flags.title || "");
7419
- const body = readBody();
7420
- if (args.flags["design-type"] !== void 0) {
7421
- 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)");
7422
- return 2;
7423
- }
7424
- if (!title || !body) {
7425
- bad(ctx, "usage: produce spec <title> --body|-f <file>");
7426
- return 2;
7427
- }
7428
- return getOrm(env.db.path).then((ds) => artifactAdd(ds, pid, "spec", title, body)).then((r) => {
7429
- if (ctx.json) console.log(JSON.stringify(r.row, null, 2));
7430
- else ok(ctx, `spec saved: ${r.row.id} (v${r.version})`);
7431
- return 0;
7432
- }).catch((e) => {
7433
- bad(ctx, e?.message || String(e));
7434
- return 1;
7435
- });
7436
- }
7437
- case "design": {
7438
- const title = rest[0] || String(args.flags.title || "");
7439
- const body = readBody();
7440
- const designType = String(args.flags["design-type"] || "arch");
7441
- if (!title || !body) {
7442
- bad(ctx, "usage: produce design <title> --body|-f <file> [--design-type ui-ux|arch|decision]");
7443
- return 2;
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;
7444
7678
  }
7445
- if (![
7446
- "ui-ux",
7447
- "arch",
7448
- "decision"
7449
- ].includes(designType)) {
7450
- bad(ctx, "--design-type must be one of ui-ux|arch|decision");
7451
- return 2;
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;
7452
7700
  }
7453
- return getOrm(env.db.path).then((ds) => artifactAdd(ds, pid, "design", title, body, "markdown", JSON.stringify({ design_type: designType }))).then((r) => {
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) => {
7454
7808
  if (ctx.json) console.log(JSON.stringify(r.row, null, 2));
7455
- else ok(ctx, `design saved: ${r.row.id} (v${r.version}) [${designType}]`);
7809
+ else {
7810
+ ok(ctx, `outline generated: ${r.row.id} (v${r.version})`);
7811
+ out(ctx, content);
7812
+ }
7456
7813
  return 0;
7457
7814
  }).catch((e) => {
7458
7815
  bad(ctx, e?.message || String(e));
7459
7816
  return 1;
7460
7817
  });
7461
7818
  }
7462
- case "task-add": {
7463
- if (rejectBare(ctx, args.flags, ["note"])) return 2;
7464
- const title = rest.join(" ");
7465
- if (!title) {
7466
- 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>");
7467
7824
  return 2;
7468
7825
  }
7469
- const note = String(args.flags.note || "");
7470
- return getOrm(env.db.path).then((ds) => taskAdd(ds, pid, title, note)).then((row) => {
7471
- if (ctx.json) console.log(JSON.stringify(row, null, 2));
7472
- 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})`);
7829
+ return 0;
7830
+ }).catch((e) => {
7831
+ bad(ctx, e?.message || String(e));
7832
+ return 1;
7833
+ });
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}]`);
7473
7874
  return 0;
7474
7875
  }).catch((e) => {
7475
7876
  bad(ctx, e?.message || String(e));
7476
7877
  return 1;
7477
7878
  });
7478
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
+ }
7479
7909
  case "task-list": {
7480
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) => {
7481
7911
  let meta = {};
@@ -8010,6 +8440,41 @@ async function condAdd(ds, treeId, keyOrId, o) {
8010
8440
  return (await mgr.query(`SELECT * FROM node_conditions WHERE id=?`, [id]))[0];
8011
8441
  });
8012
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
+ }
8013
8478
  async function stateSet(ds, treeId, keyOrId, status, context = "default", level = 0) {
8014
8479
  return ds.transaction(async (mgr) => {
8015
8480
  const n = await mgr.query(`SELECT id FROM nodes WHERE tree_id=? AND (key=? OR id=?)`, [
@@ -8240,12 +8705,12 @@ async function importTree(ds, spec, opts = {}) {
8240
8705
  * ygl-trees node validate sanity checks
8241
8706
  * ygl-trees node export [--out file] portable JSON
8242
8707
  */
8243
- 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 });
8244
8709
  init_fmt();
8245
8710
  init_orm();
8246
8711
  init_eval();
8247
8712
  require_text.init_text();
8248
- function run$5(env, action, rest, args) {
8713
+ function run$4(env, action, rest, args) {
8249
8714
  const { ctx, db } = env;
8250
8715
  const trees = new TreeStore(db);
8251
8716
  const sel = args.flags.t ? String(args.flags.t) : void 0;
@@ -8429,10 +8894,25 @@ function run$5(env, action, rest, args) {
8429
8894
  });
8430
8895
  }
8431
8896
  if (sub === "cond") {
8432
- if (!key) {
8433
- 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]");
8434
8913
  return 2;
8435
8914
  }
8915
+ const k = cKey;
8436
8916
  if (rejectBare(ctx, args.flags, [
8437
8917
  "name",
8438
8918
  "value",
@@ -8457,7 +8937,7 @@ function run$5(env, action, rest, args) {
8457
8937
  if (validateEnum(ctx, args.flags, "effect", ["gate", "cost_mul"])) return 2;
8458
8938
  const name = flagStr("name");
8459
8939
  if (!name) {
8460
- table(ctx, trees.condList(tid, key), [
8940
+ table(ctx, trees.condList(tid, k), [
8461
8941
  "id",
8462
8942
  "name",
8463
8943
  "kind",
@@ -8513,7 +8993,7 @@ function run$5(env, action, rest, args) {
8513
8993
  value = packEvalScoreSpec(q, levels, min);
8514
8994
  }
8515
8995
  }
8516
- return runTreeWrite((ds) => condAdd(ds, tid, key, {
8996
+ return runTreeWrite((ds) => condAdd(ds, tid, k, {
8517
8997
  name,
8518
8998
  kind: flagStr("kind", "flag"),
8519
8999
  expr: flagStr("expr") || void 0,
@@ -8523,7 +9003,7 @@ function run$5(env, action, rest, args) {
8523
9003
  effect: flagStr("effect", "gate"),
8524
9004
  weight: flagNum("weight", 1)
8525
9005
  }), (c) => {
8526
- 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}]`);
8527
9007
  return 0;
8528
9008
  });
8529
9009
  }
@@ -9005,6 +9485,14 @@ function run$5(env, action, rest, args) {
9005
9485
  return 0;
9006
9486
  }
9007
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
+ }
9008
9496
  table(ctx, trees.nodeList(tid), [
9009
9497
  "key",
9010
9498
  "name",
@@ -9235,12 +9723,12 @@ function loadStateFile(args, shared) {
9235
9723
  //#region packages/knowledge/skills/src/skill-cmds.ts
9236
9724
  /** skill group — the L1/L2/L3 registry agents query before acting.
9237
9725
  * Writes run on the TypeORM write service (src/writes.ts). */
9238
- 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 });
9239
9727
  init_fmt();
9240
9728
  require_text.init_text();
9241
9729
  init_orm();
9242
9730
  init_writes();
9243
- function run$4(env, action, rest, args) {
9731
+ function run$3(env, action, rest, args) {
9244
9732
  const { ctx, store } = env;
9245
9733
  const name = rest[0];
9246
9734
  const need = (v, what) => {
@@ -9342,7 +9830,7 @@ function run$4(env, action, rest, args) {
9342
9830
  case "add": {
9343
9831
  const n = args.flags.name ? String(args.flags.name) : "";
9344
9832
  const file = args.flags.f ? String(args.flags.f) : "";
9345
- if (file) return run$4(env, "import", [file], args);
9833
+ if (file) return run$3(env, "import", [file], args);
9346
9834
  if (!need(n, "--name")) return 2;
9347
9835
  const body = args.flags.body ? String(args.flags.body) : args.flags["body-file"] ? node_fs.readFileSync(String(args.flags["body-file"]), "utf8") : "";
9348
9836
  const spec = {
@@ -9570,12 +10058,13 @@ function wrap(text, indent = 4) {
9570
10058
  //#region packages/knowledge/skills/src/cmds.ts
9571
10059
  /** kb group — long-term knowledge: store, retrieve, connect, promote to skill.
9572
10060
  * Writes (add/ingest) run on the TypeORM write service (src/writes.ts). */
9573
- var cmds_exports = /* @__PURE__ */ require_rolldown_runtime.__exportAll({ run: () => run$3 });
10061
+ var cmds_exports = /* @__PURE__ */ require_rolldown_runtime.__exportAll({ run: () => run$2 });
9574
10062
  init_fmt();
9575
10063
  require_text.init_text();
10064
+ init_eventbus();
9576
10065
  init_orm();
9577
10066
  init_writes();
9578
- function run$3(env, action, rest, args) {
10067
+ function run$2(env, action, rest, args) {
9579
10068
  const { ctx, store } = env;
9580
10069
  const readBody = () => {
9581
10070
  if (args.flags.body) return String(args.flags.body);
@@ -9708,493 +10197,215 @@ function run$3(env, action, rest, args) {
9708
10197
  "conf",
9709
10198
  "reuse",
9710
10199
  "updated"
9711
- ], [
9712
- "id",
9713
- "title",
9714
- "tags",
9715
- "confidence",
9716
- "reuse",
9717
- "updated"
9718
- ], "kb add --title ... --body ... | kb import <dir>");
9719
- return 0;
9720
- case "rm":
9721
- case "remove": {
9722
- const id = rest[0];
9723
- if (!id) {
9724
- bad(ctx, "usage: kb rm <id>");
9725
- return 2;
9726
- }
9727
- const note = store.kbGet(id);
9728
- if (!note) {
9729
- bad(ctx, `note not found: ${id}`);
9730
- return 1;
9731
- }
9732
- store.db.run("DELETE FROM kb_fts WHERE id=?", [id]);
9733
- store.db.run("DELETE FROM kb_notes WHERE id=?", [id]);
9734
- ok(ctx, `note removed: ${id} ${String(note.title || "").slice(0, 40)}`);
9735
- return 0;
9736
- }
9737
- case "show": {
9738
- const n = store.kbGet(rest[0]);
9739
- if (!n) {
9740
- bad(ctx, `note not found: ${rest[0]}`);
9741
- return 1;
9742
- }
9743
- if (ctx.json) {
9744
- console.log(JSON.stringify(n, null, 2));
9745
- return 0;
9746
- }
9747
- head(ctx, n.title);
9748
- show(ctx, {
9749
- id: n.id,
9750
- tags: n.tags || "-",
9751
- confidence: n.confidence,
9752
- reuse: n.reuse,
9753
- origin: n.origin || "-"
9754
- });
9755
- out(ctx, "");
9756
- out(ctx, n.body);
9757
- const links = store.kbLinks(n.id);
9758
- if (links.length) {
9759
- out(ctx, paint(C.bold, "\n links", ctx.color));
9760
- for (const l of links) out(ctx, ` - ${l.rel}: ${l.other}`);
9761
- }
9762
- return 0;
9763
- }
9764
- case "link": {
9765
- const a = store.kbGet(rest[0]);
9766
- const b = store.kbGet(rest[1]);
9767
- if (!a || !b) {
9768
- bad(ctx, "both notes must exist");
9769
- return 1;
9770
- }
9771
- store.kbLink(a.id, b.id, String(args.flags.rel || "related"));
9772
- ok(ctx, `linked ${a.title} --${args.flags.rel || "related"}--> ${b.title}`);
9773
- return 0;
9774
- }
9775
- case "ingest": {
9776
- if (!rest[0]) {
9777
- bad(ctx, "usage: kb ingest <artifactId> [--force]");
9778
- return 2;
9779
- }
9780
- const forceKb = args.flags.force === true || args.flags.force === "true";
9781
- return getOrm(env.db.path).then((ds) => kbIngestArtifact(ds, rest[0], forceKb)).then((r) => {
9782
- writeResult(ctx, r, (row) => `artifact ingested into kb: ${row.id} ${require_text.clip(row.title, 50)}`);
9783
- return 0;
9784
- }).catch((e) => {
9785
- bad(ctx, e?.message || String(e));
9786
- return 1;
9787
- });
9788
- }
9789
- case "promote": {
9790
- if (!rest[0]) {
9791
- bad(ctx, "usage: kb promote <noteId> [--name <skill>]");
9792
- return 2;
9793
- }
9794
- const note = store.kbGet(rest[0]);
9795
- if (!note) {
9796
- bad(ctx, `kb note not found: ${rest[0]}`);
9797
- return 1;
9798
- }
9799
- const name = args.flags.name ? String(args.flags.name) : note.title;
9800
- return getOrm(env.db.path).then((ds) => skillUpsert(ds, {
9801
- name,
9802
- description: deriveL1(note.body || "", note.title),
9803
- body: note.body,
9804
- tags: note.tags,
9805
- origin: `kb:${note.id}`
9806
- })).then((s) => {
9807
- ok(ctx, `promoted to skill: ${s.name}`);
9808
- out(ctx, paint(C.dim, ` L1: ${require_text.clip(String(s.description), 100)}`, ctx.color));
9809
- return 0;
9810
- }).catch((e) => {
9811
- bad(ctx, e?.message || String(e));
9812
- return 1;
9813
- });
9814
- }
9815
- case "import": {
9816
- const target = rest[0];
9817
- if (!target) {
9818
- bad(ctx, "usage: kb import <dir|file.md> [--tags ..]");
9819
- return 2;
9820
- }
9821
- let st;
9822
- try {
9823
- st = node_fs.statSync(target);
9824
- } catch {
9825
- bad(ctx, `not found: ${target}`);
9826
- return 1;
9827
- }
9828
- const files = [];
9829
- if (st.isFile()) files.push(target);
9830
- 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)));
9831
- if (!files.length) {
9832
- bad(ctx, `no .md files under ${target}`);
9833
- return 1;
9834
- }
9835
- files.sort();
9836
- const MAX_FILE_BYTES = 1048576;
9837
- const skipped = [];
9838
- const importable = files.filter((f) => {
9839
- const sz = node_fs.statSync(f).size;
9840
- if (sz > MAX_FILE_BYTES && !args.flags.force) {
9841
- skipped.push({
9842
- file: f,
9843
- size: `${(sz / 1024 / 1024).toFixed(1)}MB`
9844
- });
9845
- return false;
9846
- }
9847
- return true;
9848
- });
9849
- if (skipped.length && !ctx.json) {
9850
- out(ctx, paint(C.yellow, ` ! skipped ${skipped.length} file(s) >1MB (--force to include):`, ctx.color));
9851
- for (const sk of skipped) out(ctx, paint(C.dim, ` ${sk.file} (${sk.size})`, ctx.color));
9852
- }
9853
- if (!importable.length) {
9854
- bad(ctx, `all ${files.length} file(s) exceeded 1MB limit — use --force to import anyway`);
9855
- return 1;
9856
- }
9857
- const items = importable.map((f) => {
9858
- const raw = node_fs.readFileSync(f, "utf8");
9859
- const { meta, body } = require_text.parseFrontmatter(raw);
9860
- const heading = /^#\s+(.+)\s*$/m.exec(body)?.[1]?.trim();
9861
- return {
9862
- title: meta.title || meta.name || heading || node_path.basename(f, ".md"),
9863
- body,
9864
- tags: meta.tags || String(args.flags.tags || ""),
9865
- source_url: meta.url || "",
9866
- confidence: meta.confidence || "B",
9867
- origin: `import:${f}`
9868
- };
9869
- });
9870
- return getOrm(env.db.path).then((ds) => kbImportBulk(ds, items)).then((r) => {
9871
- if (ctx.json) console.log(JSON.stringify({
9872
- added: r.added,
9873
- duplicates: r.dups,
9874
- files: files.length,
9875
- target
9876
- }, null, 2));
9877
- else ok(ctx, `kb import: ${r.added} added, ${r.dups} duplicate(s) kept — ${files.length} file(s) from ${target}`);
9878
- return 0;
9879
- }).catch((e) => {
9880
- bad(ctx, e?.message || String(e));
9881
- return 1;
9882
- });
9883
- }
9884
- case "":
9885
- out(ctx, paint(C.dim, " kb: add | import <dir|file> | search | list | show | link | ingest <artifactId> | promote <noteId>", ctx.color));
9886
- return 0;
9887
- default:
9888
- bad(ctx, `unknown kb action: ${action}`);
9889
- return 2;
9890
- }
9891
- }
9892
- //#endregion
9893
- //#region packages/ops/tools/src/rels.ts
9894
- var rels_exports = /* @__PURE__ */ require_rolldown_runtime.__exportAll({
9895
- REL_TYPES: () => REL_TYPES,
9896
- RelError: () => RelError,
9897
- relAdd: () => relAdd,
9898
- relRm: () => relRm,
9899
- resolveId: () => resolveId,
9900
- resolveRef: () => resolveRef,
9901
- run: () => run$2
9902
- });
9903
- init_fmt();
9904
- require_text.init_text();
9905
- init_orm();
9906
- const REL_TYPES = [
9907
- "node",
9908
- "claim",
9909
- "evidence",
9910
- "source",
9911
- "query",
9912
- "kb",
9913
- "artifact",
9914
- "skill"
9915
- ];
9916
- /** Resolve one `<type>:<ref>` against the database → { type, id, display }.
9917
- * Throws RelError(code) on unknown type / missing row. */
9918
- var RelError = class extends Error {
9919
- code;
9920
- constructor(code, message) {
9921
- super(message);
9922
- this.code = code;
9923
- }
9924
- };
9925
- const DISPLAY = {
9926
- node: (db, ref) => {
9927
- const [treeName, key] = ref.includes("/") ? [ref.slice(0, ref.indexOf("/")), ref.slice(ref.indexOf("/") + 1)] : [null, ref];
9928
- const cur = treeName || db.metaGet("current_tree") || "";
9929
- 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=?)", [
9930
- cur,
9931
- cur,
9932
- key,
9933
- key
9934
- ]);
9935
- if (!rows.length) throw new RelError(1, `node not found: ${ref}`);
9936
- return {
9937
- id: String(rows[0].id),
9938
- display: String(rows[0].name || rows[0].key)
9939
- };
9940
- },
9941
- claim: (db, ref) => {
9942
- const r = db.get("SELECT id, text FROM claims WHERE id=? LIMIT 1", [ref]);
9943
- if (!r) throw new RelError(1, `claim not found: ${ref}`);
9944
- return {
9945
- id: String(r.id),
9946
- display: String(r.text).slice(0, 60)
9947
- };
9948
- },
9949
- evidence: (db, ref) => {
9950
- const r = db.get("SELECT id, quote FROM evidence WHERE id=? LIMIT 1", [ref]);
9951
- if (!r) throw new RelError(1, `evidence not found: ${ref}`);
9952
- return {
9953
- id: String(r.id),
9954
- display: String(r.quote).slice(0, 60)
9955
- };
9956
- },
9957
- source: (db, ref) => {
9958
- const r = db.get("SELECT id, title FROM sources WHERE id=? LIMIT 1", [ref]);
9959
- if (!r) throw new RelError(1, `source not found: ${ref}`);
9960
- return {
9961
- id: String(r.id),
9962
- display: String(r.title)
9963
- };
9964
- },
9965
- query: (db, ref) => {
9966
- const r = db.get("SELECT id, text FROM queries WHERE id=? LIMIT 1", [ref]);
9967
- if (!r) throw new RelError(1, `query not found: ${ref}`);
9968
- return {
9969
- id: String(r.id),
9970
- display: String(r.text).slice(0, 60)
9971
- };
9972
- },
9973
- kb: (db, ref) => {
9974
- const r = db.get("SELECT id, title FROM kb_notes WHERE id=? OR title=? LIMIT 1", [ref, ref]);
9975
- if (!r) throw new RelError(1, `kb note not found: ${ref}`);
9976
- return {
9977
- id: String(r.id),
9978
- display: String(r.title)
9979
- };
9980
- },
9981
- artifact: (db, ref) => {
9982
- const r = db.get("SELECT id, title FROM artifacts WHERE id=? OR title=? LIMIT 1", [ref, ref]);
9983
- if (!r) throw new RelError(1, `artifact not found: ${ref}`);
9984
- return {
9985
- id: String(r.id),
9986
- display: String(r.title)
9987
- };
9988
- },
9989
- skill: (db, ref) => {
9990
- const r = db.get("SELECT name, description FROM skills WHERE name=? LIMIT 1", [ref]);
9991
- if (!r) throw new RelError(1, `skill not found: ${ref}`);
9992
- return {
9993
- id: String(r.name),
9994
- display: String(r.name)
9995
- };
9996
- }
9997
- };
9998
- function resolveRef(env, ref) {
9999
- const i = ref.indexOf(":");
10000
- if (i < 1) throw new RelError(2, `bad reference '${ref}' — want <type>:<ref> where type ∈ ${REL_TYPES.join("|")}`);
10001
- const type = ref.slice(0, i);
10002
- const ident = ref.slice(i + 1);
10003
- const fn = DISPLAY[type];
10004
- if (!fn) throw new RelError(2, `unknown reference type '${type}' — want ${REL_TYPES.join("|")}`);
10005
- const r = fn(env.db, ident);
10006
- return {
10007
- type,
10008
- id: r.id,
10009
- display: r.display
10010
- };
10011
- }
10012
- /** Reverse lookup: type+id → display (for the webui related section). */
10013
- function resolveId(env, type, id) {
10014
- const fn = DISPLAY[type];
10015
- if (!fn) return id;
10016
- try {
10017
- return fn(env.db, id).display;
10018
- } catch {
10019
- return id;
10020
- }
10021
- }
10022
- async function relAdd(env, srcRef, dstRef, rel = "related") {
10023
- const src = resolveRef(env, srcRef);
10024
- const dst = resolveRef(env, dstRef);
10025
- return (await getOrm(env.db.path)).transaction(async (mgr) => {
10026
- const ex = await mgr.query(`SELECT id FROM relations WHERE src_type=? AND src_id=? AND dst_type=? AND dst_id=? AND rel=?`, [
10027
- src.type,
10028
- src.id,
10029
- dst.type,
10030
- dst.id,
10031
- rel
10032
- ]);
10033
- if (ex.length) return {
10034
- row: (await mgr.query(`SELECT * FROM relations WHERE id=?`, [ex[0].id]))[0],
10035
- duplicate: true
10036
- };
10037
- const id = require_text.shortId([
10038
- "rel",
10039
- src.id,
10040
- dst.id,
10041
- rel
10042
- ], 10);
10043
- await mgr.query(`INSERT INTO relations(id,src_type,src_id,dst_type,dst_id,rel,weight,created) VALUES(?,?,?,?,?,?,1,?)`, [
10044
- id,
10045
- src.type,
10046
- src.id,
10047
- dst.type,
10048
- dst.id,
10049
- rel,
10050
- require_text.nowIso()
10051
- ]);
10052
- await mgr.query(`INSERT INTO events(id,ts,actor,kind,entity,entity_id,payload) VALUES(?,?,?,?,?,?,?)`, [
10053
- require_text.shortId([
10054
- "ev",
10055
- "rel.add",
10056
- id
10057
- ], 12),
10058
- require_text.nowIso(),
10059
- "cli",
10060
- "rel.add",
10061
- "relation",
10062
- id,
10063
- JSON.stringify({
10064
- src: `${src.type}:${src.id}`,
10065
- dst: `${dst.type}:${dst.id}`,
10066
- rel
10067
- })
10068
- ]);
10069
- return {
10070
- row: (await mgr.query(`SELECT * FROM relations WHERE id=?`, [id]))[0],
10071
- duplicate: false
10072
- };
10073
- });
10074
- }
10075
- async function relRm(env, srcRef, dstRef, rel = "related") {
10076
- const src = resolveRef(env, srcRef);
10077
- const dst = resolveRef(env, dstRef);
10078
- await (await getOrm(env.db.path)).transaction(async (mgr) => {
10079
- if (!(await mgr.query(`SELECT id FROM relations WHERE src_type=? AND src_id=? AND dst_type=? AND dst_id=? AND rel=?`, [
10080
- src.type,
10081
- src.id,
10082
- dst.type,
10083
- dst.id,
10084
- rel
10085
- ])).length) throw new RelError(1, `relation not found: ${srcRef} → ${dstRef} (${rel})`);
10086
- await mgr.query(`DELETE FROM relations WHERE src_type=? AND src_id=? AND dst_type=? AND dst_id=? AND rel=?`, [
10087
- src.type,
10088
- src.id,
10089
- dst.type,
10090
- dst.id,
10091
- rel
10092
- ]);
10093
- });
10094
- }
10095
- function run$2(env, action, rest, args) {
10096
- const { ctx } = env;
10097
- if (action === "add") {
10098
- const [a, b] = rest;
10099
- if (!a || !b) {
10100
- bad(ctx, "usage: rel add <type>:<ref> <type>:<ref> [--rel <type>]");
10101
- return 2;
10102
- }
10103
- const rel = String(args.flags.rel || "related");
10104
- 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]);
10105
10223
  try {
10106
- const r = await relAdd(env, a, b, rel);
10107
- if (ctx.json) {
10108
- console.log(JSON.stringify({
10109
- id: r.row.id,
10110
- src: `${r.row.src_type}:${r.row.src_id}`,
10111
- dst: `${r.row.dst_type}:${r.row.dst_id}`,
10112
- rel: r.row.rel,
10113
- duplicate: r.duplicate
10114
- }));
10115
- return 0;
10116
- }
10117
- if (r.duplicate) out(ctx, paint(C.yellow, ` ⚠ duplicate: ${a} ${rel} ${b} (already linked)`, ctx.color));
10118
- 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));
10119
10263
  return 0;
10120
- } catch (e) {
10121
- if (e instanceof RelError) {
10122
- bad(ctx, e.message);
10123
- return e.code;
10124
- }
10125
- throw e;
10126
10264
  }
10127
- })();
10128
- }
10129
- if (action === "rm") {
10130
- const [a, b] = rest;
10131
- if (!a || !b) {
10132
- bad(ctx, "usage: rel rm <type>:<ref> <type>:<ref> [--rel <type>]");
10133
- 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;
10134
10281
  }
10135
- const rel = String(args.flags.rel || "related");
10136
- return (async () => {
10137
- try {
10138
- await relRm(env, a, b, rel);
10139
- if (ctx.json) console.log(JSON.stringify({ removed: `${a} -${rel}-> ${b}` }));
10140
- 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)}`);
10141
10301
  return 0;
10142
- } catch (e) {
10143
- if (e instanceof RelError) {
10144
- bad(ctx, e.message);
10145
- return e.code;
10146
- }
10147
- 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;
10148
10311
  }
10149
- })();
10150
- }
10151
- if (action === "list" || action === "") {
10152
- const outRows = (() => {
10153
- const params = [];
10154
- let where = "1=1";
10155
- const srcF = String(args.flags.src || "");
10156
- const dstF = String(args.flags.dst || "");
10157
- const relF = String(args.flags.rel || "");
10158
- if (srcF) {
10159
- const s = resolveRef(env, srcF);
10160
- where += " AND src_type=? AND src_id=?";
10161
- 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;
10162
10316
  }
10163
- if (dstF) {
10164
- const d = resolveRef(env, dstF);
10165
- where += " AND dst_type=? AND dst_id=?";
10166
- 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;
10167
10338
  }
10168
- if (relF) {
10169
- where += " AND rel=?";
10170
- 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;
10171
10345
  }
10172
- return env.db.all(`SELECT * FROM relations WHERE ${where} ORDER BY rowid DESC LIMIT 200`, params);
10173
- })().map((r) => ({
10174
- src: `${r.src_type}:${r.src_id}`,
10175
- dst: `${r.dst_type}:${r.dst_id}`,
10176
- rel: r.rel,
10177
- src_display: resolveId(env, r.src_type, r.src_id),
10178
- dst_display: resolveId(env, r.dst_type, r.dst_id)
10179
- }));
10180
- if (ctx.json) {
10181
- console.log(JSON.stringify(outRows, null, 2));
10182
- 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
+ });
10183
10401
  }
10184
- head(ctx, `Relations (${outRows.length})`);
10185
- table(ctx, outRows, [
10186
- "src",
10187
- "rel",
10188
- "dst"
10189
- ], [
10190
- "src_display",
10191
- "rel",
10192
- "dst_display"
10193
- ]);
10194
- 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;
10195
10408
  }
10196
- bad(ctx, "usage: rel [add|rm|list] ... (see ygl-trees help)");
10197
- return 2;
10198
10409
  }
10199
10410
  //#endregion
10200
10411
  //#region packages/ops/tools/src/webui.ts
@@ -11383,17 +11594,20 @@ function run(env, action, rest, args) {
11383
11594
  {
11384
11595
  kind: "kb.add",
11385
11596
  table: "kb_notes",
11386
- label: "kb"
11597
+ label: "kb",
11598
+ rm: "kb.rm"
11387
11599
  },
11388
11600
  {
11389
11601
  kind: "source.add",
11390
11602
  table: "sources",
11391
- label: "source"
11603
+ label: "source",
11604
+ rm: "source.rm"
11392
11605
  },
11393
11606
  {
11394
11607
  kind: "evidence.add",
11395
11608
  table: "evidence",
11396
- label: "evidence"
11609
+ label: "evidence",
11610
+ rm: "evidence.rm"
11397
11611
  }
11398
11612
  ];
11399
11613
  const phantoms = [];
@@ -11401,13 +11615,14 @@ function run(env, action, rest, args) {
11401
11615
  const rows = db.all(`SELECT e.entity_id id, e.ts FROM events e
11402
11616
  WHERE e.kind='${spec.kind}'
11403
11617
  AND e.ts >= datetime('now', '-2 days')
11404
- 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)`);
11405
11620
  for (const r of rows) phantoms.push(`${spec.label} ${r.id} (${String(r.ts).slice(0, 16)})`);
11406
11621
  }
11407
11622
  checks.push({
11408
11623
  name: "write_integrity",
11409
- status: phantoms.length ? "fail" : "pass",
11410
- 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"
11411
11626
  });
11412
11627
  }
11413
11628
  const failed = checks.filter((c) => c.status === "fail").length;