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.
- package/CHANGELOG.md +16 -0
- package/README.md +22 -14
- package/dist/bundle/bin.cjs +844 -629
- package/dist/packages/knowledge/skills/src/cmds.js +27 -0
- package/dist/packages/ops/tools/src/db-cmds.js +29 -4
- package/dist/packages/ops/tools/src/doctor.js +11 -6
- package/dist/packages/ops/tools/src/rels.js +16 -2
- package/dist/packages/research/pipeline/src/cmds.js +41 -6
- package/dist/packages/research/produce/src/produce-cmds.js +23 -8
- package/dist/packages/techtree/model/src/cmds.js +35 -5
- package/dist/packages/techtree/model/src/reads.js +51 -1
- package/dist/packages/techtree/model/src/writes.js +23 -0
- package/package.json +3 -3
- package/skills/ygl-trees-eval-gates/SKILL.md +6 -1
- package/skills/ygl-trees-quickstart/SKILL.md +3 -1
- package/skills/ygl-trees-relating/SKILL.md +5 -0
- package/skills/ygl-trees-research/SKILL.md +8 -1
- package/skills/ygl-trees-techtree/SKILL.md +4 -3
package/dist/bundle/bin.cjs
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
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
|
-
|
|
6620
|
-
|
|
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 && !/^
|
|
6626
|
-
bad(ctx, `--url must be
|
|
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
|
-
|
|
6899
|
-
"
|
|
6900
|
-
"
|
|
6901
|
-
|
|
6902
|
-
|
|
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/
|
|
7335
|
-
|
|
7336
|
-
|
|
7337
|
-
|
|
7338
|
-
|
|
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
|
-
|
|
7343
|
-
|
|
7344
|
-
|
|
7345
|
-
|
|
7346
|
-
|
|
7347
|
-
|
|
7348
|
-
|
|
7349
|
-
|
|
7350
|
-
|
|
7351
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7363
|
-
} catch
|
|
7364
|
-
|
|
7365
|
-
return 1;
|
|
7569
|
+
return fn(env.db, id).display;
|
|
7570
|
+
} catch {
|
|
7571
|
+
return id;
|
|
7366
7572
|
}
|
|
7367
|
-
|
|
7368
|
-
|
|
7369
|
-
|
|
7370
|
-
|
|
7371
|
-
|
|
7372
|
-
|
|
7373
|
-
|
|
7374
|
-
|
|
7375
|
-
|
|
7376
|
-
|
|
7377
|
-
|
|
7378
|
-
|
|
7379
|
-
|
|
7380
|
-
|
|
7381
|
-
|
|
7382
|
-
|
|
7383
|
-
|
|
7384
|
-
|
|
7385
|
-
|
|
7386
|
-
|
|
7387
|
-
|
|
7388
|
-
|
|
7389
|
-
|
|
7390
|
-
|
|
7391
|
-
|
|
7392
|
-
|
|
7393
|
-
|
|
7394
|
-
|
|
7395
|
-
|
|
7396
|
-
|
|
7397
|
-
|
|
7398
|
-
|
|
7399
|
-
|
|
7400
|
-
|
|
7401
|
-
|
|
7402
|
-
|
|
7403
|
-
|
|
7404
|
-
|
|
7405
|
-
|
|
7406
|
-
|
|
7407
|
-
|
|
7408
|
-
|
|
7409
|
-
|
|
7410
|
-
|
|
7411
|
-
|
|
7412
|
-
|
|
7413
|
-
|
|
7414
|
-
|
|
7415
|
-
|
|
7416
|
-
|
|
7417
|
-
|
|
7418
|
-
|
|
7419
|
-
|
|
7420
|
-
|
|
7421
|
-
|
|
7422
|
-
|
|
7423
|
-
|
|
7424
|
-
|
|
7425
|
-
|
|
7426
|
-
|
|
7427
|
-
|
|
7428
|
-
|
|
7429
|
-
|
|
7430
|
-
|
|
7431
|
-
|
|
7432
|
-
|
|
7433
|
-
|
|
7434
|
-
|
|
7435
|
-
|
|
7436
|
-
|
|
7437
|
-
|
|
7438
|
-
|
|
7439
|
-
|
|
7440
|
-
|
|
7441
|
-
|
|
7442
|
-
|
|
7443
|
-
|
|
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
|
-
|
|
7446
|
-
|
|
7447
|
-
|
|
7448
|
-
|
|
7449
|
-
|
|
7450
|
-
|
|
7451
|
-
|
|
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
|
-
|
|
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
|
|
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 "
|
|
7463
|
-
|
|
7464
|
-
const
|
|
7465
|
-
if (!title) {
|
|
7466
|
-
bad(ctx, "usage:
|
|
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
|
-
|
|
7470
|
-
|
|
7471
|
-
|
|
7472
|
-
|
|
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$
|
|
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$
|
|
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
|
-
|
|
8433
|
-
|
|
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,
|
|
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,
|
|
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: ${
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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
|
|
10107
|
-
|
|
10108
|
-
|
|
10109
|
-
|
|
10110
|
-
|
|
10111
|
-
|
|
10112
|
-
|
|
10113
|
-
|
|
10114
|
-
|
|
10115
|
-
|
|
10116
|
-
|
|
10117
|
-
|
|
10118
|
-
|
|
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
|
-
|
|
10130
|
-
|
|
10131
|
-
|
|
10132
|
-
|
|
10133
|
-
|
|
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
|
-
|
|
10136
|
-
|
|
10137
|
-
|
|
10138
|
-
|
|
10139
|
-
|
|
10140
|
-
|
|
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
|
-
}
|
|
10143
|
-
|
|
10144
|
-
|
|
10145
|
-
|
|
10146
|
-
|
|
10147
|
-
|
|
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
|
-
|
|
10152
|
-
|
|
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
|
-
|
|
10164
|
-
|
|
10165
|
-
|
|
10166
|
-
|
|
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
|
-
|
|
10169
|
-
|
|
10170
|
-
|
|
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
|
-
|
|
10173
|
-
|
|
10174
|
-
|
|
10175
|
-
|
|
10176
|
-
|
|
10177
|
-
|
|
10178
|
-
|
|
10179
|
-
|
|
10180
|
-
|
|
10181
|
-
|
|
10182
|
-
|
|
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
|
-
|
|
10185
|
-
|
|
10186
|
-
|
|
10187
|
-
|
|
10188
|
-
|
|
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 ? "
|
|
11410
|
-
detail: phantoms.length ? `${phantoms.length}
|
|
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;
|