ygl-trees 0.78.0 → 0.80.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 +30 -0
- package/README.md +28 -4
- package/dist/apps/cli/src/bin.js +34 -7
- package/dist/bundle/bin.cjs +725 -110
- package/dist/packages/core/database/src/db.js +73 -5
- package/dist/packages/knowledge/skills/src/cmds.js +26 -1
- package/dist/packages/ops/tools/src/agent-cmds.js +35 -0
- package/dist/packages/ops/tools/src/db-cmds.js +70 -0
- package/dist/packages/ops/tools/src/doctor.js +79 -24
- package/dist/packages/ops/tools/src/webui.js +9 -0
- package/dist/packages/research/pipeline/src/cmds.js +120 -14
- package/dist/packages/research/pipeline/src/predict.js +82 -9
- package/dist/packages/research/produce/src/produce-cmds.js +6 -0
- package/dist/packages/research/produce/src/project-cmds.js +27 -2
- package/dist/packages/techtree/model/src/cmds.js +236 -72
- package/dist/packages/techtree/model/src/reads.js +59 -1
- package/package.json +5 -4
- package/skills/ygl-trees-eval-gates/SKILL.md +6 -1
- package/skills/ygl-trees-quickstart/SKILL.md +18 -7
- package/skills/ygl-trees-relating/SKILL.md +5 -0
- package/skills/ygl-trees-research/SKILL.md +7 -0
- package/skills/ygl-trees-techtree/SKILL.md +2 -0
package/dist/bundle/bin.cjs
CHANGED
|
@@ -34,8 +34,23 @@ var db_exports = /* @__PURE__ */ require_rolldown_runtime.__exportAll({
|
|
|
34
34
|
EDGE_RELS: () => EDGE_RELS,
|
|
35
35
|
SCHEMA_SQL: () => SCHEMA_SQL,
|
|
36
36
|
SCHEMA_VERSION: () => 9,
|
|
37
|
-
dbPath: () => dbPath
|
|
37
|
+
dbPath: () => dbPath,
|
|
38
|
+
pidAlive: () => pidAlive
|
|
38
39
|
});
|
|
40
|
+
/** PID liveness probe (shared by lock acquisition, `db unlock`, and doctor).
|
|
41
|
+
* signal 0 = pure existence check (does NOT kill the process)
|
|
42
|
+
* EPERM = exists but permission-denied (Windows ACL) — still alive
|
|
43
|
+
* ESRCH = no such process — dead. NaN (garbage pid file) counts as dead. */
|
|
44
|
+
function pidAlive(pidStr) {
|
|
45
|
+
const pid = Number(pidStr);
|
|
46
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
47
|
+
try {
|
|
48
|
+
process.kill(pid, 0);
|
|
49
|
+
return true;
|
|
50
|
+
} catch (e) {
|
|
51
|
+
return e.code === "EPERM";
|
|
52
|
+
}
|
|
53
|
+
}
|
|
39
54
|
/** Resolve the active database file. */
|
|
40
55
|
function dbPath(home) {
|
|
41
56
|
return node_path.join(home, "ygl-trees.db");
|
|
@@ -392,6 +407,9 @@ CREATE UNIQUE INDEX IF NOT EXISTS uq_skill_refs ON skill_refs(skill_id, path);
|
|
|
392
407
|
DB = class {
|
|
393
408
|
db;
|
|
394
409
|
path;
|
|
410
|
+
/** True when THIS instance acquired the .ygllock (0.79.1) — lock-less
|
|
411
|
+
* instances (serve, db unlock) never release a foreign lock. */
|
|
412
|
+
ownsLock = false;
|
|
395
413
|
constructor(file, opts) {
|
|
396
414
|
this.path = file;
|
|
397
415
|
node_fs.mkdirSync(node_path.dirname(file), { recursive: true });
|
|
@@ -424,11 +442,23 @@ CREATE UNIQUE INDEX IF NOT EXISTS uq_skill_refs ON skill_refs(skill_id, path);
|
|
|
424
442
|
force: true
|
|
425
443
|
});
|
|
426
444
|
} catch {}
|
|
427
|
-
else
|
|
445
|
+
else {
|
|
446
|
+
let holderCmd = "";
|
|
447
|
+
try {
|
|
448
|
+
holderCmd = node_fs.readFileSync(node_path.join(lockDir, "cmd"), "utf8").trim();
|
|
449
|
+
} catch {}
|
|
450
|
+
throw new Error(`database is locked by another process (pid: ${holder}).\n` + (holderCmd ? ` holder command: ${holderCmd}\n` : ` the holder is almost certainly a parallel CLI session (serve never takes the write lock);\n`) + ` read commands still work. The lock self-heals once pid ${holder} exits —\n simply retry after it finishes.\n to clear manually: ygl-trees db unlock (checks pid liveness, refuses live holders)\n last resort: rmdir "${lockDir}"`);
|
|
451
|
+
}
|
|
428
452
|
}
|
|
453
|
+
let acquired = false;
|
|
429
454
|
if (useLock) try {
|
|
430
455
|
node_fs.mkdirSync(lockDir, { recursive: true });
|
|
431
456
|
node_fs.writeFileSync(node_path.join(lockDir, "pid"), String(process.pid));
|
|
457
|
+
acquired = true;
|
|
458
|
+
try {
|
|
459
|
+
const cmd = [process.argv0, ...process.argv.slice(1)].join(" ").slice(0, 240);
|
|
460
|
+
node_fs.writeFileSync(node_path.join(lockDir, "cmd"), cmd);
|
|
461
|
+
} catch {}
|
|
432
462
|
const cleanup = () => {
|
|
433
463
|
for (let i = 0; i < 3; i++) try {
|
|
434
464
|
node_fs.rmSync(lockDir, {
|
|
@@ -441,14 +471,19 @@ CREATE UNIQUE INDEX IF NOT EXISTS uq_skill_refs ON skill_refs(skill_id, path);
|
|
|
441
471
|
process.on("beforeExit", cleanup);
|
|
442
472
|
process.on("exit", cleanup);
|
|
443
473
|
} catch {}
|
|
474
|
+
this.ownsLock = acquired;
|
|
444
475
|
this.db = new node_sqlite3_wasm.Database(file);
|
|
445
476
|
this.db.exec("PRAGMA journal_mode=WAL");
|
|
446
477
|
this.db.exec("PRAGMA synchronous=NORMAL");
|
|
447
478
|
this.db.exec("PRAGMA foreign_keys=ON");
|
|
448
479
|
this.migrate();
|
|
449
480
|
}
|
|
450
|
-
/** Release the write lock (called by close() and process exit).
|
|
481
|
+
/** Release the write lock (called by close() and process exit).
|
|
482
|
+
* Ownership-guarded since 0.79.1: a lock-less instance (serve, db unlock)
|
|
483
|
+
* shares the workspace with a foreign holder — its close() must not nuke
|
|
484
|
+
* that holder's lock. */
|
|
451
485
|
releaseLock() {
|
|
486
|
+
if (!this.ownsLock) return;
|
|
452
487
|
try {
|
|
453
488
|
const lockDir = `${this.path}.ygllock`;
|
|
454
489
|
if (node_fs.existsSync(lockDir)) node_fs.rmSync(lockDir, {
|
|
@@ -467,6 +502,22 @@ CREATE UNIQUE INDEX IF NOT EXISTS uq_skill_refs ON skill_refs(skill_id, path);
|
|
|
467
502
|
});
|
|
468
503
|
} catch {}
|
|
469
504
|
}
|
|
505
|
+
/** Static: read the holder pid of a lock file ("" when absent/unreadable). */
|
|
506
|
+
static lockHolder(file) {
|
|
507
|
+
try {
|
|
508
|
+
return node_fs.readFileSync(node_path.join(`${file}.ygllock`, "pid"), "utf8").trim();
|
|
509
|
+
} catch {
|
|
510
|
+
return "";
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
/** Static: read the holder's self-reported command line ("" when absent). */
|
|
514
|
+
static lockHolderCmd(file) {
|
|
515
|
+
try {
|
|
516
|
+
return node_fs.readFileSync(node_path.join(`${file}.ygllock`, "cmd"), "utf8").trim();
|
|
517
|
+
} catch {
|
|
518
|
+
return "";
|
|
519
|
+
}
|
|
520
|
+
}
|
|
470
521
|
migrate() {
|
|
471
522
|
let row;
|
|
472
523
|
try {
|
|
@@ -1927,6 +1978,18 @@ var TreeStore = class {
|
|
|
1927
1978
|
keyOrId
|
|
1928
1979
|
]);
|
|
1929
1980
|
}
|
|
1981
|
+
/** Which OTHER trees contain this key? (Report ③: a parallel session
|
|
1982
|
+
* flipped current_tree; `node can p_dates` then said just "node not
|
|
1983
|
+
* found" — the key existed the whole time, in another tree. This lookup
|
|
1984
|
+
* lets the error name the tree instead of forcing a blind node list.) */
|
|
1985
|
+
nodeInOtherTrees(keyOrId, excludeTreeId) {
|
|
1986
|
+
return this.db.all(`SELECT t.name AS tree, n.key FROM nodes n JOIN trees t ON t.id=n.tree_id
|
|
1987
|
+
WHERE n.tree_id<>? AND (n.key=? OR n.id=?) LIMIT 5`, [
|
|
1988
|
+
excludeTreeId,
|
|
1989
|
+
keyOrId,
|
|
1990
|
+
keyOrId
|
|
1991
|
+
]);
|
|
1992
|
+
}
|
|
1930
1993
|
/** Runtime state for one node (default context). Separate from nodeGet on
|
|
1931
1994
|
* purpose: nodeGet feeds WRITE paths, and a context join there could return
|
|
1932
1995
|
* one row per context — silently wrong for link/unlink/rm. */
|
|
@@ -2250,6 +2313,12 @@ var TreeStore = class {
|
|
|
2250
2313
|
* + unlocked set + the caller's state bag. The eval API demands a context
|
|
2251
2314
|
* package — bare state fell into the gray band 6/8 in the material's
|
|
2252
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.
|
|
2253
2322
|
*/
|
|
2254
2323
|
evalContext(treeId, keyOrId, stateBag, context = "default") {
|
|
2255
2324
|
const n = this.nodeGet(treeId, keyOrId);
|
|
@@ -2273,7 +2342,7 @@ var TreeStore = class {
|
|
|
2273
2342
|
effect: c.effect
|
|
2274
2343
|
}));
|
|
2275
2344
|
const unlocked = [...this.unlockedSet(treeId, context)].map((id) => idToKey.get(id) || id);
|
|
2276
|
-
|
|
2345
|
+
const pkg = {
|
|
2277
2346
|
node: {
|
|
2278
2347
|
key: n.key,
|
|
2279
2348
|
name: n.name,
|
|
@@ -2287,6 +2356,62 @@ var TreeStore = class {
|
|
|
2287
2356
|
unlocked,
|
|
2288
2357
|
state: stateBag
|
|
2289
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
|
+
});
|
|
2290
2415
|
}
|
|
2291
2416
|
/** Every node whose required gates are currently satisfied. */
|
|
2292
2417
|
ready(treeId, context = "default", state = {}) {
|
|
@@ -4684,6 +4809,77 @@ function run$12(env, action, rest, args) {
|
|
|
4684
4809
|
if (ctx.json) console.log(JSON.stringify({ path: file }, null, 2));
|
|
4685
4810
|
else out(ctx, file);
|
|
4686
4811
|
return 0;
|
|
4812
|
+
case "unlock": {
|
|
4813
|
+
const lockDir = `${file}.ygllock`;
|
|
4814
|
+
let holder = "";
|
|
4815
|
+
try {
|
|
4816
|
+
holder = node_fs.readFileSync(`${lockDir}/pid`, "utf8").trim();
|
|
4817
|
+
} catch {}
|
|
4818
|
+
if (!node_fs.existsSync(lockDir)) {
|
|
4819
|
+
if (ctx.json) console.log(JSON.stringify({
|
|
4820
|
+
lock: lockDir,
|
|
4821
|
+
holder: null,
|
|
4822
|
+
cleared: false,
|
|
4823
|
+
reason: "no lock present"
|
|
4824
|
+
}, null, 2));
|
|
4825
|
+
else ok(ctx, `no lock present: ${lockDir}`);
|
|
4826
|
+
return 0;
|
|
4827
|
+
}
|
|
4828
|
+
if (holder && pidAlive(holder) && !args.flags.force) {
|
|
4829
|
+
const self = Number(holder) === process.pid;
|
|
4830
|
+
let holderCmd = "";
|
|
4831
|
+
try {
|
|
4832
|
+
holderCmd = node_fs.readFileSync(`${lockDir}/cmd`, "utf8").trim();
|
|
4833
|
+
} catch {}
|
|
4834
|
+
const msg = self ? `lock is held by THIS process (pid ${holder}) — acquired during boot; clearing.` : `lock held by LIVE process (pid: ${holder}${holderCmd ? ` — ${holderCmd}` : ""}) — refusing to clear.`;
|
|
4835
|
+
if (self) try {
|
|
4836
|
+
node_fs.rmSync(lockDir, {
|
|
4837
|
+
recursive: true,
|
|
4838
|
+
force: true
|
|
4839
|
+
});
|
|
4840
|
+
if (ctx.json) console.log(JSON.stringify({
|
|
4841
|
+
lock: lockDir,
|
|
4842
|
+
holder,
|
|
4843
|
+
cleared: true,
|
|
4844
|
+
reason: "self (acquired during boot)"
|
|
4845
|
+
}, null, 2));
|
|
4846
|
+
else ok(ctx, `lock cleared: ${lockDir} (self — acquired during boot)`);
|
|
4847
|
+
return 0;
|
|
4848
|
+
} catch {}
|
|
4849
|
+
if (ctx.json) console.log(JSON.stringify({
|
|
4850
|
+
lock: lockDir,
|
|
4851
|
+
holder,
|
|
4852
|
+
holder_cmd: holderCmd || null,
|
|
4853
|
+
cleared: false,
|
|
4854
|
+
reason: "holder alive (use --force to override)"
|
|
4855
|
+
}, null, 2));
|
|
4856
|
+
else {
|
|
4857
|
+
bad(ctx, msg);
|
|
4858
|
+
out(ctx, paint(C.yellow, ` that is a parallel CLI session (serve never takes the write lock) — retry after it exits;`, ctx.color));
|
|
4859
|
+
out(ctx, paint(C.yellow, ` the lock self-heals then. --force overrides (corruption risk if writes are in flight).`, ctx.color));
|
|
4860
|
+
}
|
|
4861
|
+
return 1;
|
|
4862
|
+
}
|
|
4863
|
+
try {
|
|
4864
|
+
node_fs.rmSync(lockDir, {
|
|
4865
|
+
recursive: true,
|
|
4866
|
+
force: true
|
|
4867
|
+
});
|
|
4868
|
+
const why = !holder || !pidAlive(holder) ? `stale (pid ${holder || "?"} dead)` : "forced";
|
|
4869
|
+
if (ctx.json) console.log(JSON.stringify({
|
|
4870
|
+
lock: lockDir,
|
|
4871
|
+
holder: holder || null,
|
|
4872
|
+
cleared: true,
|
|
4873
|
+
reason: why
|
|
4874
|
+
}, null, 2));
|
|
4875
|
+
else ok(ctx, `lock cleared: ${lockDir} (${why})`);
|
|
4876
|
+
return 0;
|
|
4877
|
+
} catch (e) {
|
|
4878
|
+
if (ctx.json) console.log(JSON.stringify({ error: `remove failed: ${e?.message || e}` }, null, 2));
|
|
4879
|
+
else bad(ctx, `remove failed: ${e?.message || e}`);
|
|
4880
|
+
return 1;
|
|
4881
|
+
}
|
|
4882
|
+
}
|
|
4687
4883
|
case "stats": {
|
|
4688
4884
|
const s = env.store.stats();
|
|
4689
4885
|
if (ctx.json) {
|
|
@@ -5635,6 +5831,34 @@ function buildBrief(env) {
|
|
|
5635
5831
|
};
|
|
5636
5832
|
let project = null;
|
|
5637
5833
|
if (cur) project = store.projectByName(cur);
|
|
5834
|
+
try {
|
|
5835
|
+
const recent = store.db.all(`SELECT kind, entity_id, ts, payload FROM events
|
|
5836
|
+
WHERE kind IN ('tree.use','project.use')
|
|
5837
|
+
AND ts >= datetime('now','-10 minutes')
|
|
5838
|
+
ORDER BY ts DESC LIMIT 5`);
|
|
5839
|
+
if (recent.length) {
|
|
5840
|
+
payload.selection_changed = recent.map((r) => ({
|
|
5841
|
+
kind: r.kind,
|
|
5842
|
+
name: r.entity_id,
|
|
5843
|
+
session: (() => {
|
|
5844
|
+
try {
|
|
5845
|
+
return JSON.parse(r.payload || "{}").session || null;
|
|
5846
|
+
} catch {
|
|
5847
|
+
return null;
|
|
5848
|
+
}
|
|
5849
|
+
})(),
|
|
5850
|
+
ts: r.ts
|
|
5851
|
+
}));
|
|
5852
|
+
const others = recent.filter((r) => {
|
|
5853
|
+
try {
|
|
5854
|
+
return (JSON.parse(r.payload || "{}").session || "cli") !== (process.env.YGL_SESSION || "cli");
|
|
5855
|
+
} catch {
|
|
5856
|
+
return false;
|
|
5857
|
+
}
|
|
5858
|
+
});
|
|
5859
|
+
if (others.length) payload.next_actions.unshift(`# 并行会话切换过选择: ${others.map((o) => `${o.kind === "tree.use" ? "tree" : "project"} '${o.entity_id}'`).join(", ")} — 命令请带 -t/-p 固定上下文`);
|
|
5860
|
+
}
|
|
5861
|
+
} catch {}
|
|
5638
5862
|
if (project) {
|
|
5639
5863
|
const claims = store.claimList(project.id);
|
|
5640
5864
|
const ev = store.evidenceList(project.id);
|
|
@@ -6146,6 +6370,7 @@ var init_eval_enhance = require_rolldown_runtime.__esmMin((() => {
|
|
|
6146
6370
|
//#region packages/research/pipeline/src/predict.ts
|
|
6147
6371
|
var predict_exports = /* @__PURE__ */ require_rolldown_runtime.__exportAll({
|
|
6148
6372
|
auditPredictions: () => auditPredictions,
|
|
6373
|
+
loadStateSource: () => loadStateSource,
|
|
6149
6374
|
measureCriterion: () => measureCriterion,
|
|
6150
6375
|
measureNpmVersions: () => measureNpmVersions,
|
|
6151
6376
|
parsePrediction: () => parsePrediction
|
|
@@ -6173,23 +6398,74 @@ function parsePrediction(text, created) {
|
|
|
6173
6398
|
deadline: ""
|
|
6174
6399
|
};
|
|
6175
6400
|
}
|
|
6176
|
-
/**
|
|
6177
|
-
|
|
6178
|
-
|
|
6179
|
-
|
|
6180
|
-
|
|
6401
|
+
/** 0.79.1 --state bridge: load a flat counter map from a JSON file path or an
|
|
6402
|
+
* http(s) URL (e.g. a DSH API endpoint). Nested objects flatten to dot keys
|
|
6403
|
+
* ({counts:{y:12}} → "counts.y"); only finite numbers are kept. Null on any
|
|
6404
|
+
* failure — the audit degrades those criteria back to manual. */
|
|
6405
|
+
async function loadStateSource(src, fetchImpl = fetch) {
|
|
6406
|
+
try {
|
|
6407
|
+
let text;
|
|
6408
|
+
if (/^https?:\/\//.test(src)) {
|
|
6409
|
+
const ctl = new AbortController();
|
|
6410
|
+
const t = setTimeout(() => ctl.abort(), 3e3);
|
|
6411
|
+
const r = await fetchImpl(src, { signal: ctl.signal });
|
|
6412
|
+
clearTimeout(t);
|
|
6413
|
+
if (!r.ok) return null;
|
|
6414
|
+
text = await r.text();
|
|
6415
|
+
} else text = require("node:fs").readFileSync(src, "utf8");
|
|
6416
|
+
const flat = {};
|
|
6417
|
+
const walk = (obj, prefix) => {
|
|
6418
|
+
if (!obj || typeof obj !== "object") return;
|
|
6419
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
6420
|
+
const key = prefix ? `${prefix}.${k}` : k;
|
|
6421
|
+
if (typeof v === "number" && Number.isFinite(v)) flat[key] = v;
|
|
6422
|
+
else if (v && typeof v === "object" && !Array.isArray(v)) walk(v, key);
|
|
6423
|
+
}
|
|
6424
|
+
};
|
|
6425
|
+
walk(JSON.parse(text), "");
|
|
6426
|
+
return flat;
|
|
6427
|
+
} catch {
|
|
6428
|
+
return null;
|
|
6429
|
+
}
|
|
6430
|
+
}
|
|
6431
|
+
/** Locally measurable criterion matchers. Null → manual.
|
|
6432
|
+
* Precedence: 树/kb workspace counts (local truth) → any `key≥N` whose key
|
|
6433
|
+
* exists in the optional --state counter map (external truth, 0.79.1). */
|
|
6434
|
+
function measureCriterion(crit, db, state) {
|
|
6181
6435
|
const parts = [];
|
|
6182
6436
|
const hits = [];
|
|
6437
|
+
const tree = crit.match(/树\s*≥\s*(\d+)/);
|
|
6183
6438
|
if (tree) {
|
|
6184
6439
|
const n = db.get("SELECT COUNT(*) c FROM trees").c;
|
|
6185
6440
|
parts.push(`trees=${n}/${tree[1]}`);
|
|
6186
6441
|
hits.push(n >= +tree[1]);
|
|
6187
6442
|
}
|
|
6443
|
+
const kb = crit.match(/kb\s*≥\s*(\d+)/);
|
|
6188
6444
|
if (kb) {
|
|
6189
6445
|
const n = db.get("SELECT COUNT(*) c FROM kb_notes").c;
|
|
6190
6446
|
parts.push(`kb=${n}/${kb[1]}`);
|
|
6191
6447
|
hits.push(n >= +kb[1]);
|
|
6192
6448
|
}
|
|
6449
|
+
if (state) {
|
|
6450
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6451
|
+
for (const m of crit.matchAll(/([\w\u4e00-\u9fa5][\w\u4e00-\u9fa5.-]*)\s*≥\s*(\d+)/g)) {
|
|
6452
|
+
const key = m[1], need = +m[2];
|
|
6453
|
+
if (key === "树" || key === "kb" || seen.has(key)) continue;
|
|
6454
|
+
let stateKey = key in state ? key : null;
|
|
6455
|
+
if (!stateKey) {
|
|
6456
|
+
const cands = Object.keys(state).filter((sk) => sk.endsWith("." + key) && Number.isFinite(state[sk]));
|
|
6457
|
+
if (cands.length) {
|
|
6458
|
+
cands.sort((a, b) => a.split(".").length - b.split(".").length || a.localeCompare(b));
|
|
6459
|
+
stateKey = cands[0];
|
|
6460
|
+
}
|
|
6461
|
+
}
|
|
6462
|
+
if (!stateKey || !Number.isFinite(state[stateKey])) continue;
|
|
6463
|
+
seen.add(key);
|
|
6464
|
+
parts.push(`${key}=${state[stateKey]}/${need}${stateKey !== key ? ` (${stateKey})` : ""}`);
|
|
6465
|
+
hits.push(state[stateKey] >= need);
|
|
6466
|
+
}
|
|
6467
|
+
}
|
|
6468
|
+
if (!parts.length) return null;
|
|
6193
6469
|
const hit = crit.includes(" 或 ") ? hits.some(Boolean) : hits.every(Boolean);
|
|
6194
6470
|
return {
|
|
6195
6471
|
actual: parts.join(" · "),
|
|
@@ -6224,7 +6500,8 @@ async function auditPredictions(inp) {
|
|
|
6224
6500
|
const m = c.text.match(/\d{4}-\d{2}-\d{2}\s*前\s*(.+?)(?:——|$)/) || c.text.match(/\]\s*(.+?)(?:——|$)/);
|
|
6225
6501
|
const criterion = m ? m[1].trim() : "";
|
|
6226
6502
|
const daysLeft = deadline ? Math.round((Date.parse(deadline) - Date.parse(today)) / 864e5) : null;
|
|
6227
|
-
|
|
6503
|
+
const resolved = c.status === "supported" || c.status === "refuted";
|
|
6504
|
+
let measure = measureCriterion(criterion, inp.db, inp.state);
|
|
6228
6505
|
if (!measure && /npm 再发\s*≥\s*(\d+)\s*个版本/.test(criterion)) {
|
|
6229
6506
|
const need = +criterion.match(/npm 再发\s*≥\s*(\d+)/)[1];
|
|
6230
6507
|
const base = c.text.match(/基线\s*\S*?@?(\d{4}-\d{2}-\d{2})/);
|
|
@@ -6242,7 +6519,10 @@ async function auditPredictions(inp) {
|
|
|
6242
6519
|
}
|
|
6243
6520
|
let verdict;
|
|
6244
6521
|
let hint;
|
|
6245
|
-
if (
|
|
6522
|
+
if (resolved) {
|
|
6523
|
+
verdict = "resolved";
|
|
6524
|
+
hint = `已人工仲裁:${c.status}(回测终态,不再复判)`;
|
|
6525
|
+
} else if (!measure) {
|
|
6246
6526
|
verdict = "manual";
|
|
6247
6527
|
hint = "外部世界判据——人工核验后 claim resolve";
|
|
6248
6528
|
} else if (daysLeft !== null && daysLeft > 0) verdict = "pending";
|
|
@@ -6252,7 +6532,7 @@ async function auditPredictions(inp) {
|
|
|
6252
6532
|
horizon,
|
|
6253
6533
|
deadline,
|
|
6254
6534
|
criterion,
|
|
6255
|
-
actual: measure ? measure.actual + (verdict === "pending" && measure.hit ? " · 已达标" : "") : "—",
|
|
6535
|
+
actual: measure ? measure.actual + (!resolved && verdict === "pending" && measure.hit ? " · 已达标" : "") : "—",
|
|
6256
6536
|
verdict,
|
|
6257
6537
|
days_left: daysLeft,
|
|
6258
6538
|
hint
|
|
@@ -6274,6 +6554,10 @@ init_writes();
|
|
|
6274
6554
|
init_entities$1();
|
|
6275
6555
|
/** Run a write through the TypeORM transactional write service and print
|
|
6276
6556
|
* with JSON discipline. Keeps every write command 6 lines. */
|
|
6557
|
+
/** 0.80: compact timestamp for session:// adjudication urls (yyyymmddhhmm). */
|
|
6558
|
+
function nowCompact() {
|
|
6559
|
+
return (/* @__PURE__ */ new Date()).toISOString().replace(/[-:T]/g, "").slice(0, 12);
|
|
6560
|
+
}
|
|
6277
6561
|
function runWrite(env, fn, human) {
|
|
6278
6562
|
const { ctx } = env;
|
|
6279
6563
|
return getOrm(env.db.path).then(fn).then((r) => {
|
|
@@ -6398,14 +6682,24 @@ function run$8(env, action, rest, args) {
|
|
|
6398
6682
|
bad(ctx, "flag needs a value: --title \"...\" / --url \"...\" (bare flag was treated as boolean)");
|
|
6399
6683
|
return 2;
|
|
6400
6684
|
}
|
|
6401
|
-
|
|
6402
|
-
|
|
6685
|
+
let title = String(titleRaw || "");
|
|
6686
|
+
let url = String(urlRaw || "");
|
|
6687
|
+
const adjRaw = args.flags.adjudication;
|
|
6688
|
+
if (adjRaw === true) {
|
|
6689
|
+
bad(ctx, "flag needs a value: --adjudication \"<裁决摘要>\"");
|
|
6690
|
+
return 2;
|
|
6691
|
+
}
|
|
6692
|
+
if (adjRaw !== void 0 && adjRaw !== "") {
|
|
6693
|
+
const adj = String(adjRaw);
|
|
6694
|
+
if (!url) url = `session://adjudication/${nowCompact()}`;
|
|
6695
|
+
if (!title) title = adj.slice(0, 60);
|
|
6696
|
+
}
|
|
6403
6697
|
if (!title && !url) {
|
|
6404
6698
|
bad(ctx, "need --title and/or --url");
|
|
6405
6699
|
return 2;
|
|
6406
6700
|
}
|
|
6407
|
-
if (url && !/^https?:\/\/|^file:\/\//.test(url)) {
|
|
6408
|
-
bad(ctx, `--url must be http(s):// or
|
|
6701
|
+
if (url && !/^https?:\/\/|^file:\/\/|^session:\/\//.test(url)) {
|
|
6702
|
+
bad(ctx, `--url must be http(s)://, file:// or session:// (got: "${require_text.clip(url, 30)}")`);
|
|
6409
6703
|
return 2;
|
|
6410
6704
|
}
|
|
6411
6705
|
const spec = {
|
|
@@ -6572,9 +6866,27 @@ function run$8(env, action, rest, args) {
|
|
|
6572
6866
|
if (sub === "evidence") {
|
|
6573
6867
|
if (subAction === "add") {
|
|
6574
6868
|
const sourceId = rest[1];
|
|
6575
|
-
const
|
|
6869
|
+
const file = String(args.flags.file || "");
|
|
6870
|
+
let quote = String(args.flags.quote || "");
|
|
6871
|
+
if (file) {
|
|
6872
|
+
if (quote) {
|
|
6873
|
+
bad(ctx, "--file and --quote are mutually exclusive");
|
|
6874
|
+
return 2;
|
|
6875
|
+
}
|
|
6876
|
+
try {
|
|
6877
|
+
quote = require("fs").readFileSync(file, "utf8").trim();
|
|
6878
|
+
} catch (e) {
|
|
6879
|
+
bad(ctx, `cannot read --file: ${e?.message || e}`);
|
|
6880
|
+
return 1;
|
|
6881
|
+
}
|
|
6882
|
+
if (!quote) {
|
|
6883
|
+
bad(ctx, `--file is empty: ${file}`);
|
|
6884
|
+
return 2;
|
|
6885
|
+
}
|
|
6886
|
+
}
|
|
6887
|
+
if (!quote) quote = readBody();
|
|
6576
6888
|
if (!sourceId || !quote) {
|
|
6577
|
-
bad(ctx, "usage: research evidence add <sourceId> --quote \"...\"");
|
|
6889
|
+
bad(ctx, "usage: research evidence add <sourceId> --quote \"...\" | --file <path> [--note ..] [--locator ..]");
|
|
6578
6890
|
return 2;
|
|
6579
6891
|
}
|
|
6580
6892
|
const note = String(args.flags.note || "");
|
|
@@ -6674,7 +6986,7 @@ function run$8(env, action, rest, args) {
|
|
|
6674
6986
|
}
|
|
6675
6987
|
if (subAction === "link") {
|
|
6676
6988
|
if (!rest[1] || !rest[2]) {
|
|
6677
|
-
bad(ctx, "usage: research claim link <claimId> <evidenceId> [--stance refutes]");
|
|
6989
|
+
bad(ctx, "usage: research claim link <claimId> <evidenceId> [--stance refutes] [--by-source]");
|
|
6678
6990
|
return 2;
|
|
6679
6991
|
}
|
|
6680
6992
|
const stanceRaw = String(args.flags.stance || "supports");
|
|
@@ -6683,6 +6995,34 @@ function run$8(env, action, rest, args) {
|
|
|
6683
6995
|
return 2;
|
|
6684
6996
|
}
|
|
6685
6997
|
const stance = stanceRaw;
|
|
6998
|
+
if (args.flags["by-source"]) {
|
|
6999
|
+
const evs = store.db.all("SELECT id FROM evidence WHERE source_id=? AND project_id=? ORDER BY created", [rest[2], project.id]);
|
|
7000
|
+
if (!evs.length) {
|
|
7001
|
+
bad(ctx, store.db.get("SELECT id FROM sources WHERE id=? AND project_id=?", [rest[2], project.id]) ? `source ${rest[2]} has no evidence yet — research evidence add ${rest[2]} --quote ".." first` : `source not found: ${rest[2]}`);
|
|
7002
|
+
return 1;
|
|
7003
|
+
}
|
|
7004
|
+
return getOrm(env.db.path).then(async (ds) => {
|
|
7005
|
+
const existing = new Set(store.db.all("SELECT evidence_id FROM claim_evidence WHERE claim_id=?", [rest[1]]).map((x) => x.evidence_id));
|
|
7006
|
+
let linked = 0, dups = 0, status = "";
|
|
7007
|
+
for (const ev of evs) {
|
|
7008
|
+
const r = await claimLink(ds, rest[1], ev.id, stance);
|
|
7009
|
+
existing.has(ev.id) ? dups++ : linked++;
|
|
7010
|
+
status = r.status;
|
|
7011
|
+
}
|
|
7012
|
+
if (ctx.json) console.log(JSON.stringify({
|
|
7013
|
+
claim: rest[1],
|
|
7014
|
+
source: rest[2],
|
|
7015
|
+
linked,
|
|
7016
|
+
duplicates: dups,
|
|
7017
|
+
status
|
|
7018
|
+
}, null, 2));
|
|
7019
|
+
else ok(ctx, `linked ${linked} evidence from source ${rest[2]} (${dups} already) — claim status: ${status}`);
|
|
7020
|
+
return 0;
|
|
7021
|
+
}).catch((e) => {
|
|
7022
|
+
bad(ctx, e?.message || String(e));
|
|
7023
|
+
return 1;
|
|
7024
|
+
});
|
|
7025
|
+
}
|
|
6686
7026
|
return getOrm(env.db.path).then((ds) => claimLink(ds, rest[1], rest[2], stance)).then((r) => {
|
|
6687
7027
|
ok(ctx, `linked; claim status now: ${r.status}`);
|
|
6688
7028
|
try {
|
|
@@ -6717,6 +7057,18 @@ function run$8(env, action, rest, args) {
|
|
|
6717
7057
|
} catch {}
|
|
6718
7058
|
return 0;
|
|
6719
7059
|
}).catch((e) => {
|
|
7060
|
+
const m = /^evidence not found: (.+)$/.exec(e?.message || "");
|
|
7061
|
+
if (m) {
|
|
7062
|
+
const evs = store.db.all("SELECT id FROM evidence WHERE source_id=? AND project_id=? ORDER BY created LIMIT 8", [m[1], project.id]);
|
|
7063
|
+
if (evs.length) {
|
|
7064
|
+
bad(ctx, `${e.message} — that id is a SOURCE; its evidence: ${evs.map((x) => x.id).join(" ")} (or rerun with --by-source to link them all)`);
|
|
7065
|
+
return 1;
|
|
7066
|
+
}
|
|
7067
|
+
if (store.db.get("SELECT id FROM sources WHERE id=? AND project_id=?", [m[1], project.id])) {
|
|
7068
|
+
bad(ctx, `${e.message} — that id is a SOURCE with no evidence yet: research evidence add ${m[1]} --quote ".." first`);
|
|
7069
|
+
return 1;
|
|
7070
|
+
}
|
|
7071
|
+
}
|
|
6720
7072
|
bad(ctx, e?.message || String(e));
|
|
6721
7073
|
return 1;
|
|
6722
7074
|
});
|
|
@@ -6846,17 +7198,26 @@ function run$8(env, action, rest, args) {
|
|
|
6846
7198
|
else out(ctx, paint(C.dim, " no [预测·…] claims in this project", ctx.color));
|
|
6847
7199
|
return 0;
|
|
6848
7200
|
}
|
|
6849
|
-
const { auditPredictions } = require_rolldown_runtime.__toCommonJS(predict_exports);
|
|
7201
|
+
const { auditPredictions, loadStateSource } = require_rolldown_runtime.__toCommonJS(predict_exports);
|
|
6850
7202
|
return (async () => {
|
|
7203
|
+
let state = null;
|
|
7204
|
+
if (args.flags.state) {
|
|
7205
|
+
state = await loadStateSource(String(args.flags.state));
|
|
7206
|
+
if (!state && !ctx.json) out(ctx, paint(C.yellow, ` ⚠ --state ${args.flags.state} 加载失败(文件/URL 不可读或非 JSON)——相关判据回落 manual`, ctx.color));
|
|
7207
|
+
}
|
|
6851
7208
|
const rows = await auditPredictions({
|
|
6852
7209
|
claims,
|
|
6853
7210
|
db: store.db,
|
|
6854
|
-
npmProbe: args.flags.offline !== true
|
|
7211
|
+
npmProbe: args.flags.offline !== true,
|
|
7212
|
+
state
|
|
6855
7213
|
});
|
|
7214
|
+
const resolvedN = rows.filter((r) => r.verdict === "resolved").length;
|
|
7215
|
+
const visible = args.flags.all ? rows : rows.filter((r) => r.verdict !== "resolved");
|
|
6856
7216
|
if (ctx.json) console.log(JSON.stringify(rows, null, 2));
|
|
7217
|
+
else if (!visible.length) out(ctx, paint(C.dim, ` 全部 ${rows.length} 条预测已结案(resolved)——--all 查看终态表`, ctx.color));
|
|
6857
7218
|
else {
|
|
6858
|
-
head(ctx, `prediction audit — ${project.name} (${rows.length}
|
|
6859
|
-
table(ctx,
|
|
7219
|
+
head(ctx, `prediction audit — ${project.name} (${visible.length}/${rows.length}${resolvedN && !args.flags.all ? ` · ${resolvedN} resolved 隐藏 (--all)` : ""})`);
|
|
7220
|
+
table(ctx, visible, [
|
|
6860
7221
|
"claim",
|
|
6861
7222
|
"horizon",
|
|
6862
7223
|
"deadline",
|
|
@@ -6873,14 +7234,14 @@ function run$8(env, action, rest, args) {
|
|
|
6873
7234
|
"verdict",
|
|
6874
7235
|
"days_left"
|
|
6875
7236
|
]);
|
|
6876
|
-
const actionable =
|
|
7237
|
+
const actionable = visible.filter((r) => r.verdict === "hit" || r.verdict === "miss");
|
|
6877
7238
|
for (const r of actionable) out(ctx, paint(C.dim, ` → research claim resolve ${r.claim_id} --as ${r.verdict === "hit" ? "supported" : "refuted"} # ${r.deadline} 已到`, ctx.color));
|
|
6878
|
-
for (const r of
|
|
7239
|
+
for (const r of visible.filter((x) => x.verdict === "manual")) out(ctx, paint(C.dim, ` · ${r.claim_id}: ${r.hint}`, ctx.color));
|
|
6879
7240
|
}
|
|
6880
7241
|
return 0;
|
|
6881
7242
|
})();
|
|
6882
7243
|
}
|
|
6883
|
-
bad(ctx, "usage: research predict audit [--offline]");
|
|
7244
|
+
bad(ctx, "usage: research predict audit [--offline] [--state <file|url>] [--all]");
|
|
6884
7245
|
return 2;
|
|
6885
7246
|
}
|
|
6886
7247
|
out(ctx, paint(C.dim, " research: query add|list · source add|list|search|show|rm · evidence add|list · claim add|link|list · predict audit", ctx.color));
|
|
@@ -6896,6 +7257,25 @@ init_entities$1();
|
|
|
6896
7257
|
function run$7(env, action, rest, args) {
|
|
6897
7258
|
const { ctx, store } = env;
|
|
6898
7259
|
const name = rest[0];
|
|
7260
|
+
/** Report ③-3: selection changes are events (session-tagged) — see the
|
|
7261
|
+
* twin helper in techtree/model/src/cmds.ts for the rationale. */
|
|
7262
|
+
function logSelectionEvent(kind, projName) {
|
|
7263
|
+
try {
|
|
7264
|
+
store.db.run("INSERT INTO events(id,ts,actor,kind,entity,entity_id,payload,level) VALUES(?,?,?,?,?,?,?,?)", [
|
|
7265
|
+
`ev${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`,
|
|
7266
|
+
(/* @__PURE__ */ new Date()).toISOString(),
|
|
7267
|
+
process.env.YGL_SESSION || "cli",
|
|
7268
|
+
kind,
|
|
7269
|
+
"project",
|
|
7270
|
+
projName,
|
|
7271
|
+
JSON.stringify({
|
|
7272
|
+
name: projName,
|
|
7273
|
+
session: process.env.YGL_SESSION || "cli"
|
|
7274
|
+
}),
|
|
7275
|
+
"info"
|
|
7276
|
+
]);
|
|
7277
|
+
} catch {}
|
|
7278
|
+
}
|
|
6899
7279
|
switch (action) {
|
|
6900
7280
|
case "new": {
|
|
6901
7281
|
if (!name) {
|
|
@@ -6903,8 +7283,11 @@ function run$7(env, action, rest, args) {
|
|
|
6903
7283
|
return 2;
|
|
6904
7284
|
}
|
|
6905
7285
|
const question = String(args.flags.q || args.flags.question || "");
|
|
7286
|
+
const prior = store.db.metaGet("current_project");
|
|
7287
|
+
const priorHint = prior && prior !== name ? ` (prior project '${prior}' is no longer selected — project use ${prior} to switch back)` : "";
|
|
6906
7288
|
return getOrm(env.db.path).then((ds) => projectCreate(ds, name, question)).then((p) => {
|
|
6907
|
-
|
|
7289
|
+
logSelectionEvent("project.use", p.name);
|
|
7290
|
+
ack(ctx, p, p?.duplicate === true ? `project exists, selected: ${p.name} (duplicate, not re-created)${priorHint}` : `project created and selected: ${p.name}${priorHint}`);
|
|
6908
7291
|
return 0;
|
|
6909
7292
|
}).catch((e) => {
|
|
6910
7293
|
bad(ctx, e?.message || String(e));
|
|
@@ -6944,6 +7327,7 @@ function run$7(env, action, rest, args) {
|
|
|
6944
7327
|
return 1;
|
|
6945
7328
|
}
|
|
6946
7329
|
store.db.metaSet("current_project", name);
|
|
7330
|
+
logSelectionEvent("project.use", name);
|
|
6947
7331
|
ack(ctx, { project: name }, `now using: ${name}`);
|
|
6948
7332
|
return 0;
|
|
6949
7333
|
case "show": {
|
|
@@ -7109,6 +7493,10 @@ function run$6(env, action, rest, args) {
|
|
|
7109
7493
|
case "spec": {
|
|
7110
7494
|
const title = rest[0] || String(args.flags.title || "");
|
|
7111
7495
|
const body = readBody();
|
|
7496
|
+
if (args.flags["design-type"] !== void 0) {
|
|
7497
|
+
bad(ctx, "--design-type belongs to 'produce design' (spec does not take it; if this is a design decision, use: produce design <title> --design-type decision)");
|
|
7498
|
+
return 2;
|
|
7499
|
+
}
|
|
7112
7500
|
if (!title || !body) {
|
|
7113
7501
|
bad(ctx, "usage: produce spec <title> --body|-f <file>");
|
|
7114
7502
|
return 2;
|
|
@@ -7946,10 +8334,41 @@ function run$5(env, action, rest, args) {
|
|
|
7946
8334
|
/** tree-domain write through the transactional service (async command). */
|
|
7947
8335
|
function runTreeWrite(fn, print) {
|
|
7948
8336
|
return getOrm(env.db.path).then(fn).then(print).catch((e) => {
|
|
7949
|
-
bad(ctx, e?.message || String(e));
|
|
8337
|
+
bad(ctx, enrichNodeNotFound(e?.message || String(e)));
|
|
7950
8338
|
return 1;
|
|
7951
8339
|
});
|
|
7952
8340
|
}
|
|
8341
|
+
/** Report ③-3: selection changes are now first-class events — a parallel
|
|
8342
|
+
* session flipping current_tree/current_project becomes observable
|
|
8343
|
+
* (agent brief surfaces it; db timeline shows the history). Session
|
|
8344
|
+
* identity comes from YGL_SESSION (agents that care can set it). */
|
|
8345
|
+
function logSelectionEvent(database, kind, name) {
|
|
8346
|
+
try {
|
|
8347
|
+
database.run("INSERT INTO events(id,ts,actor,kind,entity,entity_id,payload,level) VALUES(?,?,?,?,?,?,?,?)", [
|
|
8348
|
+
`ev${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`,
|
|
8349
|
+
(/* @__PURE__ */ new Date()).toISOString(),
|
|
8350
|
+
process.env.YGL_SESSION || "cli",
|
|
8351
|
+
kind,
|
|
8352
|
+
kind === "tree.use" ? "tree" : "project",
|
|
8353
|
+
name,
|
|
8354
|
+
JSON.stringify({
|
|
8355
|
+
name,
|
|
8356
|
+
session: process.env.YGL_SESSION || "cli"
|
|
8357
|
+
}),
|
|
8358
|
+
"info"
|
|
8359
|
+
]);
|
|
8360
|
+
} catch {}
|
|
8361
|
+
}
|
|
8362
|
+
/** Report ③ enrichment: "node not found: X" earned a cross-tree lookup —
|
|
8363
|
+
* if a parallel session flipped current_tree, the key usually exists in
|
|
8364
|
+
* another tree; name it instead of making the user blind-list. */
|
|
8365
|
+
function enrichNodeNotFound(msg) {
|
|
8366
|
+
const m = /^node not found: (.+)$/.exec(msg);
|
|
8367
|
+
if (!m) return msg;
|
|
8368
|
+
const others = trees.nodeInOtherTrees(m[1], tid);
|
|
8369
|
+
if (others.length) return `${msg}\n key exists in other tree(s): ${others.map((o) => `'${o.tree}'`).join(", ")} — pin it: ygl-trees ${env.group} ... -t ${others[0].tree}`;
|
|
8370
|
+
return `${msg} (current tree: '${tree?.name ?? "?"}' — node list to inspect, -t <tree> to switch)`;
|
|
8371
|
+
}
|
|
7953
8372
|
if (env.group === "tree") {
|
|
7954
8373
|
const sub = action;
|
|
7955
8374
|
const name = rest[0];
|
|
@@ -7962,6 +8381,7 @@ function run$5(env, action, rest, args) {
|
|
|
7962
8381
|
const proj = pid ? db.get("SELECT id FROM projects WHERE name=?", [pid]) : null;
|
|
7963
8382
|
return runTreeWrite((ds) => treeCreate(ds, name, proj?.id || ""), (t) => {
|
|
7964
8383
|
db.metaSet("current_tree", t.name);
|
|
8384
|
+
logSelectionEvent(db, "tree.use", t.name);
|
|
7965
8385
|
ack(ctx, t, t?.duplicate === true ? `tree exists, selected: ${t.name} (duplicate, not re-created)` : `tree created and selected: ${t.name}`);
|
|
7966
8386
|
return 0;
|
|
7967
8387
|
});
|
|
@@ -7990,6 +8410,7 @@ function run$5(env, action, rest, args) {
|
|
|
7990
8410
|
return 1;
|
|
7991
8411
|
}
|
|
7992
8412
|
db.metaSet("current_tree", name);
|
|
8413
|
+
logSelectionEvent(db, "tree.use", name);
|
|
7993
8414
|
ack(ctx, { tree: name }, `now using tree: ${name}`);
|
|
7994
8415
|
return 0;
|
|
7995
8416
|
}
|
|
@@ -8198,43 +8619,43 @@ function run$5(env, action, rest, args) {
|
|
|
8198
8619
|
});
|
|
8199
8620
|
if (sub === "can") {
|
|
8200
8621
|
if (!key) {
|
|
8201
|
-
bad(ctx, "usage: node can <key
|
|
8622
|
+
bad(ctx, "usage: node can <key...> [--context ctx] [--state k=v] [--state-file file] [--eval]");
|
|
8202
8623
|
return 2;
|
|
8203
8624
|
}
|
|
8204
|
-
const
|
|
8205
|
-
const
|
|
8206
|
-
|
|
8207
|
-
|
|
8208
|
-
|
|
8209
|
-
|
|
8210
|
-
|
|
8211
|
-
|
|
8212
|
-
|
|
8213
|
-
head(ctx, `can unlock: ${r.key}`);
|
|
8214
|
-
out(ctx, ` ${r.ok ? paint(C.green, "✓ 可解锁", ctx.color) : paint(C.red, "✗ 不可解锁", ctx.color)}`);
|
|
8215
|
-
if (r.missing_required.length) {
|
|
8216
|
-
out(ctx, paint(C.bold, " 必检未满足", ctx.color));
|
|
8217
|
-
for (const m of r.missing_required) out(ctx, ` - ${m}`);
|
|
8218
|
-
}
|
|
8219
|
-
if (r.blocked_by.length) {
|
|
8220
|
-
out(ctx, paint(C.bold, " 被排斥", ctx.color));
|
|
8221
|
-
for (const b of r.blocked_by) out(ctx, ` - ${b}`);
|
|
8222
|
-
}
|
|
8223
|
-
if (r.missing_optional.length) {
|
|
8224
|
-
out(ctx, paint(C.dim, " 可选未满足(不阻塞)", ctx.color));
|
|
8225
|
-
for (const m of r.missing_optional) out(ctx, ` - ${m}`);
|
|
8226
|
-
}
|
|
8227
|
-
if (r.cost_multiplier !== 1) out(ctx, ` 成本系数 ${r.cost_multiplier}`);
|
|
8228
|
-
return 0;
|
|
8625
|
+
const keys = [...new Set(rest.filter(Boolean))];
|
|
8626
|
+
const sharedState = parseState(args);
|
|
8627
|
+
let perKeyBags = null;
|
|
8628
|
+
try {
|
|
8629
|
+
perKeyBags = loadStateFile(args, sharedState);
|
|
8630
|
+
} catch (e) {
|
|
8631
|
+
const msg = e?.message || String(e);
|
|
8632
|
+
bad(ctx, msg);
|
|
8633
|
+
return /^usage:/.test(msg) ? 2 : /must contain a JSON object/.test(msg) ? 2 : 1;
|
|
8229
8634
|
}
|
|
8635
|
+
const stateFor = (k) => perKeyBags ? perKeyBags[k] && typeof perKeyBags[k] === "object" && !Array.isArray(perKeyBags[k]) ? perKeyBags[k] : {} : sharedState;
|
|
8636
|
+
const ctxName = flagStr("context", "default");
|
|
8230
8637
|
const offline = !!process.env.YGL_EVAL_OFFLINE || configGetBool(env.db, "eval.offline");
|
|
8231
8638
|
const cfgToken = configGet(env.db, "decision_model_llm_token").trim();
|
|
8232
8639
|
const token = offline ? "" : cfgToken || evalToken();
|
|
8233
8640
|
const endpoint = configGet(env.db, "decision_model_llm_api_base").trim() || "https://ai-gateway.vercel.sh/v4/ai/evaluation-model";
|
|
8234
8641
|
const passT = configGetNumber(env.db, "eval.pass_threshold");
|
|
8235
8642
|
const failT = configGetNumber(env.db, "eval.fail_threshold");
|
|
8236
|
-
|
|
8237
|
-
|
|
8643
|
+
/** Judge one node: canUnlock (+ eval gates when --eval). Never throws —
|
|
8644
|
+
* hard failures come back as {key, error}. */
|
|
8645
|
+
const judgeOne = async (k) => {
|
|
8646
|
+
let r;
|
|
8647
|
+
try {
|
|
8648
|
+
r = trees.canUnlock(tid, k, ctxName, stateFor(k));
|
|
8649
|
+
} catch (e) {
|
|
8650
|
+
return {
|
|
8651
|
+
key: k,
|
|
8652
|
+
error: enrichNodeNotFound(e?.message || String(e))
|
|
8653
|
+
};
|
|
8654
|
+
}
|
|
8655
|
+
const state = stateFor(k);
|
|
8656
|
+
const evalConds = flagBool("eval") ? trees.condList(tid, k).filter((c) => /^eval(-choice|-score)?$/.test(String(c.op))) : [];
|
|
8657
|
+
if (!evalConds.length) return r;
|
|
8658
|
+
const contextPkg = trees.evalContext(tid, k, state, ctxName);
|
|
8238
8659
|
const evaluated = [];
|
|
8239
8660
|
let usageIn = 0, usageOut = 0;
|
|
8240
8661
|
for (const c of evalConds) {
|
|
@@ -8295,13 +8716,13 @@ function run$5(env, action, rest, args) {
|
|
|
8295
8716
|
require_text.shortId([
|
|
8296
8717
|
"eval.gate",
|
|
8297
8718
|
"node",
|
|
8298
|
-
|
|
8719
|
+
k
|
|
8299
8720
|
], 12),
|
|
8300
8721
|
require_text.nowIso(),
|
|
8301
8722
|
"cli",
|
|
8302
8723
|
"eval.gate",
|
|
8303
8724
|
"node",
|
|
8304
|
-
|
|
8725
|
+
k,
|
|
8305
8726
|
JSON.stringify({
|
|
8306
8727
|
conds: evaluated.map((e) => ({
|
|
8307
8728
|
name: e.name,
|
|
@@ -8318,31 +8739,74 @@ function run$5(env, action, rest, args) {
|
|
|
8318
8739
|
})
|
|
8319
8740
|
]);
|
|
8320
8741
|
} catch {}
|
|
8742
|
+
return r;
|
|
8743
|
+
};
|
|
8744
|
+
const batch = keys.length > 1;
|
|
8745
|
+
return (async () => {
|
|
8746
|
+
const results = [];
|
|
8747
|
+
for (const k of keys) results.push(await judgeOne(k));
|
|
8321
8748
|
if (ctx.json) {
|
|
8322
|
-
console.log(JSON.stringify(
|
|
8323
|
-
return 0;
|
|
8749
|
+
console.log(JSON.stringify(batch ? results : results[0], null, 2));
|
|
8750
|
+
return results.some((r) => r.error) ? 1 : 0;
|
|
8324
8751
|
}
|
|
8325
|
-
|
|
8326
|
-
|
|
8327
|
-
|
|
8328
|
-
|
|
8329
|
-
|
|
8330
|
-
|
|
8331
|
-
|
|
8332
|
-
|
|
8333
|
-
|
|
8334
|
-
|
|
8335
|
-
|
|
8336
|
-
|
|
8337
|
-
|
|
8752
|
+
if (!batch) {
|
|
8753
|
+
const r = results[0];
|
|
8754
|
+
if (r.error) {
|
|
8755
|
+
bad(ctx, r.error);
|
|
8756
|
+
return 1;
|
|
8757
|
+
}
|
|
8758
|
+
if (!r.evaluated) {
|
|
8759
|
+
head(ctx, `can unlock: ${r.key}`);
|
|
8760
|
+
out(ctx, ` ${r.ok ? paint(C.green, "✓ 可解锁", ctx.color) : paint(C.red, "✗ 不可解锁", ctx.color)}`);
|
|
8761
|
+
if (r.missing_required.length) {
|
|
8762
|
+
out(ctx, paint(C.bold, " 必检未满足", ctx.color));
|
|
8763
|
+
for (const m of r.missing_required) out(ctx, ` - ${m}`);
|
|
8764
|
+
}
|
|
8765
|
+
if (r.blocked_by.length) {
|
|
8766
|
+
out(ctx, paint(C.bold, " 被排斥", ctx.color));
|
|
8767
|
+
for (const b of r.blocked_by) out(ctx, ` - ${b}`);
|
|
8768
|
+
}
|
|
8769
|
+
if (r.missing_optional.length) {
|
|
8770
|
+
out(ctx, paint(C.dim, " 可选未满足(不阻塞)", ctx.color));
|
|
8771
|
+
for (const m of r.missing_optional) out(ctx, ` - ${m}`);
|
|
8772
|
+
}
|
|
8773
|
+
if (r.cost_multiplier !== 1) out(ctx, ` 成本系数 ${r.cost_multiplier}`);
|
|
8774
|
+
return 0;
|
|
8775
|
+
}
|
|
8776
|
+
head(ctx, `can unlock: ${r.key}`);
|
|
8777
|
+
out(ctx, ` ${r.ok ? paint(C.green, "✓ 可解锁", ctx.color) : r.pending.length ? paint(C.yellow, "… 待判定(决策模型 pending)", ctx.color) : paint(C.red, "✗ 不可解锁", ctx.color)}`);
|
|
8778
|
+
if (r.missing_required.length) {
|
|
8779
|
+
out(ctx, paint(C.bold, " 必检未满足", ctx.color));
|
|
8780
|
+
for (const m of r.missing_required) out(ctx, ` - ${m}`);
|
|
8781
|
+
}
|
|
8782
|
+
if (r.pending.length) {
|
|
8783
|
+
out(ctx, paint(C.bold, " 决策模型 pending(灰带/未配置,非终态)", ctx.color));
|
|
8784
|
+
for (const p of r.pending) out(ctx, ` - ${p}`);
|
|
8785
|
+
}
|
|
8786
|
+
if (r.blocked_by.length) {
|
|
8787
|
+
out(ctx, paint(C.bold, " 被排斥", ctx.color));
|
|
8788
|
+
for (const b of r.blocked_by) out(ctx, ` - ${b}`);
|
|
8789
|
+
}
|
|
8790
|
+
if (r.missing_optional.length) {
|
|
8791
|
+
out(ctx, paint(C.dim, " 可选未满足(不阻塞)", ctx.color));
|
|
8792
|
+
for (const m of r.missing_optional) out(ctx, ` - ${m}`);
|
|
8793
|
+
}
|
|
8794
|
+
for (const e of r.evaluated) out(ctx, paint(C.dim, ` eval ${e.name}: P=${e.p ?? "-"} → ${e.outcome}${e.reason ? ` (${e.reason})` : ""}`, ctx.color));
|
|
8795
|
+
if (r.cost_multiplier !== 1) out(ctx, ` 成本系数 ${r.cost_multiplier}`);
|
|
8796
|
+
return 0;
|
|
8338
8797
|
}
|
|
8339
|
-
|
|
8340
|
-
|
|
8341
|
-
|
|
8798
|
+
const via = perKeyBags ? "per-node --state-file" : Object.keys(sharedState).length ? "shared state" : "";
|
|
8799
|
+
head(ctx, `can unlock × ${results.length}${via ? ` (${via})` : ""}`);
|
|
8800
|
+
for (const r of results) {
|
|
8801
|
+
if (r.error) {
|
|
8802
|
+
out(ctx, ` ${paint(C.red, "✗", ctx.color)} ${r.key} ${r.error}`);
|
|
8803
|
+
continue;
|
|
8804
|
+
}
|
|
8805
|
+
const mark = r.ok ? paint(C.green, "✓", ctx.color) : r.pending?.length ? paint(C.yellow, "…", ctx.color) : paint(C.red, "✗", ctx.color);
|
|
8806
|
+
const blockers = [...(r.missing_required || []).map((m) => m), ...(r.blocked_by || []).map((b) => `excluded:${b}`)].join(", ");
|
|
8807
|
+
out(ctx, ` ${mark} ${r.key}${r.pending?.length ? " (eval pending)" : ""}${blockers ? ` — ${blockers}` : ""}`);
|
|
8342
8808
|
}
|
|
8343
|
-
|
|
8344
|
-
if (r.cost_multiplier !== 1) out(ctx, ` 成本系数 ${r.cost_multiplier}`);
|
|
8345
|
-
return 0;
|
|
8809
|
+
return results.some((r) => r.error) ? 1 : 0;
|
|
8346
8810
|
})().catch((e) => {
|
|
8347
8811
|
bad(ctx, e?.message || String(e));
|
|
8348
8812
|
return 1;
|
|
@@ -8350,6 +8814,16 @@ function run$5(env, action, rest, args) {
|
|
|
8350
8814
|
}
|
|
8351
8815
|
if (sub === "ready") {
|
|
8352
8816
|
const state = parseState(args);
|
|
8817
|
+
try {
|
|
8818
|
+
if (loadStateFile(args, state)) {
|
|
8819
|
+
bad(ctx, "--state-file per-node shape (key→object) only applies to batch `node can k1 k2 …`; ready wants a flat counter object");
|
|
8820
|
+
return 2;
|
|
8821
|
+
}
|
|
8822
|
+
} catch (e) {
|
|
8823
|
+
const msg = e?.message || String(e);
|
|
8824
|
+
bad(ctx, msg);
|
|
8825
|
+
return /^usage:/.test(msg) || /must contain a JSON object/.test(msg) ? 2 : 1;
|
|
8826
|
+
}
|
|
8353
8827
|
const ctxName = flagStr("context", "default");
|
|
8354
8828
|
if (!flagBool("eval")) {
|
|
8355
8829
|
const rows = trees.ready(tid, ctxName, state);
|
|
@@ -8530,7 +9004,13 @@ function run$5(env, action, rest, args) {
|
|
|
8530
9004
|
bad(ctx, "usage: node path <key>");
|
|
8531
9005
|
return 2;
|
|
8532
9006
|
}
|
|
8533
|
-
|
|
9007
|
+
let p;
|
|
9008
|
+
try {
|
|
9009
|
+
p = trees.pathTo(tid, key, flagStr("context", "default"));
|
|
9010
|
+
} catch (e) {
|
|
9011
|
+
bad(ctx, enrichNodeNotFound(e?.message || String(e)));
|
|
9012
|
+
return 1;
|
|
9013
|
+
}
|
|
8534
9014
|
if (ctx.json) {
|
|
8535
9015
|
console.log(JSON.stringify(p, null, 2));
|
|
8536
9016
|
return 0;
|
|
@@ -8640,7 +9120,7 @@ function run$5(env, action, rest, args) {
|
|
|
8640
9120
|
if (sub === "show") {
|
|
8641
9121
|
const n = trees.nodeGet(tid, key);
|
|
8642
9122
|
if (!n) {
|
|
8643
|
-
bad(ctx, `node not found: ${key}`);
|
|
9123
|
+
bad(ctx, enrichNodeNotFound(`node not found: ${key}`));
|
|
8644
9124
|
return 1;
|
|
8645
9125
|
}
|
|
8646
9126
|
const st = trees.nodeStateOf(tid, key);
|
|
@@ -8747,10 +9227,36 @@ function run$5(env, action, rest, args) {
|
|
|
8747
9227
|
conditions: r.conditions
|
|
8748
9228
|
}, `imported tree ${r.tree.name}: ${r.nodes} nodes, ${r.edges} edges, ${r.conditions} conditions`);
|
|
8749
9229
|
db.metaSet("current_tree", r.tree.name);
|
|
9230
|
+
logSelectionEvent(db, "tree.use", r.tree.name);
|
|
8750
9231
|
if (!ctx.json) out(ctx, paint(C.dim, ` index rebuilt; now using tree: ${r.tree.name}`, ctx.color));
|
|
8751
9232
|
return 0;
|
|
8752
9233
|
});
|
|
8753
9234
|
}
|
|
9235
|
+
if (sub === "timeline") {
|
|
9236
|
+
const rows = db.all(`SELECT e.ts, e.kind, e.payload FROM events e
|
|
9237
|
+
WHERE e.entity = 'node'
|
|
9238
|
+
AND e.entity_id IN (SELECT id FROM nodes WHERE tree_id = ?)
|
|
9239
|
+
ORDER BY e.ts DESC LIMIT 30`, [tid]);
|
|
9240
|
+
if (ctx.json) {
|
|
9241
|
+
console.log(JSON.stringify(rows.map((r) => ({
|
|
9242
|
+
ts: r.ts,
|
|
9243
|
+
kind: r.kind,
|
|
9244
|
+
...JSON.parse(r.payload || "{}")
|
|
9245
|
+
})), null, 2));
|
|
9246
|
+
return 0;
|
|
9247
|
+
}
|
|
9248
|
+
head(ctx, `Timeline — tree '${tree.name}' (last ${rows.length})`);
|
|
9249
|
+
for (const r of rows) {
|
|
9250
|
+
let detail = "";
|
|
9251
|
+
try {
|
|
9252
|
+
const p = JSON.parse(r.payload || "{}");
|
|
9253
|
+
detail = [p.key, p.name ? String(p.name).slice(0, 30) : ""].filter(Boolean).join(" ");
|
|
9254
|
+
} catch {}
|
|
9255
|
+
out(ctx, ` ${String(r.ts).slice(0, 16)} ${r.kind.padEnd(14)} ${detail}`);
|
|
9256
|
+
}
|
|
9257
|
+
if (!rows.length) out(ctx, paint(C.dim, " (no node events yet — unlock/add to start the history)", ctx.color));
|
|
9258
|
+
return 0;
|
|
9259
|
+
}
|
|
8754
9260
|
if (sub === "rm") {
|
|
8755
9261
|
if (!key) {
|
|
8756
9262
|
bad(ctx, "usage: node rm <key>");
|
|
@@ -8777,6 +9283,30 @@ function parseState(args) {
|
|
|
8777
9283
|
}
|
|
8778
9284
|
return out;
|
|
8779
9285
|
}
|
|
9286
|
+
/**
|
|
9287
|
+
* 0.79.1 (R297 user report): `--state-file <path.json>` loads the state bag
|
|
9288
|
+
* from JSON instead of inline `--state k=v`. Two accepted shapes:
|
|
9289
|
+
* flat shared {"y_imported": 12} → merged over --state, all keys share it
|
|
9290
|
+
* per-node map {"node-a": {"y": 1}, ...} → each judged node gets its own bag
|
|
9291
|
+
* (disambiguated by "every value is a plain object"). Returns the per-node map
|
|
9292
|
+
* when shape B, null when merged into `shared`. Throws on unreadable/invalid.
|
|
9293
|
+
*/
|
|
9294
|
+
function loadStateFile(args, shared) {
|
|
9295
|
+
const sf = args.flags["state-file"];
|
|
9296
|
+
if (sf === void 0) return null;
|
|
9297
|
+
if (sf === true || sf === "") throw new Error("usage: --state-file <path.json>");
|
|
9298
|
+
let parsed;
|
|
9299
|
+
try {
|
|
9300
|
+
parsed = JSON.parse(require("node:fs").readFileSync(String(sf), "utf8"));
|
|
9301
|
+
} catch (e) {
|
|
9302
|
+
throw new Error(`cannot read --state-file ${sf}: ${e?.message || e}`);
|
|
9303
|
+
}
|
|
9304
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("--state-file must contain a JSON object (flat counters, or per-node key→object map)");
|
|
9305
|
+
const vals = Object.values(parsed);
|
|
9306
|
+
if (vals.length && vals.every((v) => v && typeof v === "object" && !Array.isArray(v))) return parsed;
|
|
9307
|
+
Object.assign(shared, parsed);
|
|
9308
|
+
return null;
|
|
9309
|
+
}
|
|
8780
9310
|
//#endregion
|
|
8781
9311
|
//#region packages/knowledge/skills/src/skill-cmds.ts
|
|
8782
9312
|
/** skill group — the L1/L2/L3 registry agents query before acting.
|
|
@@ -9151,7 +9681,22 @@ function run$3(env, action, rest, args) {
|
|
|
9151
9681
|
confidence: String(args.flags.confidence || configGet(env.db, "default_confidence")),
|
|
9152
9682
|
force
|
|
9153
9683
|
})).then((r) => {
|
|
9154
|
-
|
|
9684
|
+
const row = r.row;
|
|
9685
|
+
const verify = store.kbGet(row.id);
|
|
9686
|
+
if (ctx.json) console.log(JSON.stringify({
|
|
9687
|
+
...row,
|
|
9688
|
+
...r.dup ? { duplicate: true } : {},
|
|
9689
|
+
verified: !!verify,
|
|
9690
|
+
workspace: env.home
|
|
9691
|
+
}, null, 2));
|
|
9692
|
+
else {
|
|
9693
|
+
writeResult(ctx, r, (rw) => `note stored: ${rw.id} ${require_text.clip(title, 50)}`);
|
|
9694
|
+
out(ctx, paint(C.dim, ` @ ${env.home}`, ctx.color));
|
|
9695
|
+
if (!verify) {
|
|
9696
|
+
out(ctx, paint(C.yellow, ` ⚠ verify miss: id ${row.id} not readable back — the write may not have landed.`, ctx.color));
|
|
9697
|
+
out(ctx, paint(C.yellow, ` run: ygl-trees doctor (write_integrity check names phantom writes)`, ctx.color));
|
|
9698
|
+
}
|
|
9699
|
+
}
|
|
9155
9700
|
return 0;
|
|
9156
9701
|
}).catch((e) => {
|
|
9157
9702
|
bad(ctx, e?.message || String(e));
|
|
@@ -10675,6 +11220,13 @@ function createDashboardServer(env) {
|
|
|
10675
11220
|
* lifecycle-managed: dispose cleans it automatically. */
|
|
10676
11221
|
function serve(env, args) {
|
|
10677
11222
|
const { ctx } = env;
|
|
11223
|
+
if (args.flags.help || args.flags.h) {
|
|
11224
|
+
out(ctx, `usage: ygl-trees webui serve [--port N] [--host H]`);
|
|
11225
|
+
out(ctx, paint(C.dim, ` --port N listen port (default 8642; 0 = pick a free one)`, ctx.color));
|
|
11226
|
+
out(ctx, paint(C.dim, ` --host H bind address (default 127.0.0.1; 0.0.0.0 opens to LAN)`, ctx.color));
|
|
11227
|
+
out(ctx, paint(C.dim, ` endpoints: / React client · /classic embedded dashboard · /api/events SSE`, ctx.color));
|
|
11228
|
+
return Promise.resolve(0);
|
|
11229
|
+
}
|
|
10678
11230
|
const reactAvailable = resolveDistRoot() !== null;
|
|
10679
11231
|
return new Promise((resolve) => {
|
|
10680
11232
|
const port = Number(args.flags.port || 8642);
|
|
@@ -10832,16 +11384,33 @@ function run(env, action, rest, args) {
|
|
|
10832
11384
|
detail: configRows.length === 0 ? "no overrides (all defaults)" : `${configRows.length} override(s), all valid`
|
|
10833
11385
|
});
|
|
10834
11386
|
const lockPath = `${db.path}.ygllock`;
|
|
10835
|
-
|
|
10836
|
-
|
|
10837
|
-
|
|
10838
|
-
|
|
10839
|
-
|
|
10840
|
-
|
|
10841
|
-
|
|
10842
|
-
|
|
10843
|
-
|
|
10844
|
-
|
|
11387
|
+
{
|
|
11388
|
+
let lockHolder = "";
|
|
11389
|
+
try {
|
|
11390
|
+
lockHolder = node_fs.readFileSync(`${lockPath}/pid`, "utf8").trim();
|
|
11391
|
+
} catch {}
|
|
11392
|
+
const mine = lockHolder === String(process.pid);
|
|
11393
|
+
const foreignLock = node_fs.existsSync(lockPath) && !mine;
|
|
11394
|
+
const holderAlive = !!lockHolder && pidAlive(lockHolder);
|
|
11395
|
+
let holderCmd = "";
|
|
11396
|
+
try {
|
|
11397
|
+
holderCmd = node_fs.readFileSync(`${lockPath}/cmd`, "utf8").trim();
|
|
11398
|
+
} catch {}
|
|
11399
|
+
let status = "pass";
|
|
11400
|
+
let detail = "no stale locks";
|
|
11401
|
+
if (foreignLock && holderAlive) {
|
|
11402
|
+
status = "pass";
|
|
11403
|
+
detail = `lock held by live pid ${lockHolder}${holderCmd ? ` (${holderCmd.slice(0, 80)})` : " (parallel session)"} — normal in multi-session use`;
|
|
11404
|
+
} else if (foreignLock) {
|
|
11405
|
+
status = "warn";
|
|
11406
|
+
detail = `stale lock (pid ${lockHolder || "?"} dead) — ygl-trees db unlock or doctor --fix clears it`;
|
|
11407
|
+
}
|
|
11408
|
+
checks.push({
|
|
11409
|
+
name: "stale_lock",
|
|
11410
|
+
status,
|
|
11411
|
+
detail
|
|
11412
|
+
});
|
|
11413
|
+
}
|
|
10845
11414
|
{
|
|
10846
11415
|
const evalConds = db.all(`SELECT c.id, c.name, c.op, c.value, n.key AS node
|
|
10847
11416
|
FROM node_conditions c JOIN nodes n ON n.id=c.node_id
|
|
@@ -10885,6 +11454,38 @@ function run(env, action, rest, args) {
|
|
|
10885
11454
|
detail: `profile read failed: ${e?.message || e}`
|
|
10886
11455
|
});
|
|
10887
11456
|
}
|
|
11457
|
+
{
|
|
11458
|
+
const PHANTOM_SPECS = [
|
|
11459
|
+
{
|
|
11460
|
+
kind: "kb.add",
|
|
11461
|
+
table: "kb_notes",
|
|
11462
|
+
label: "kb"
|
|
11463
|
+
},
|
|
11464
|
+
{
|
|
11465
|
+
kind: "source.add",
|
|
11466
|
+
table: "sources",
|
|
11467
|
+
label: "source"
|
|
11468
|
+
},
|
|
11469
|
+
{
|
|
11470
|
+
kind: "evidence.add",
|
|
11471
|
+
table: "evidence",
|
|
11472
|
+
label: "evidence"
|
|
11473
|
+
}
|
|
11474
|
+
];
|
|
11475
|
+
const phantoms = [];
|
|
11476
|
+
for (const spec of PHANTOM_SPECS) {
|
|
11477
|
+
const rows = db.all(`SELECT e.entity_id id, e.ts FROM events e
|
|
11478
|
+
WHERE e.kind='${spec.kind}'
|
|
11479
|
+
AND e.ts >= datetime('now', '-2 days')
|
|
11480
|
+
AND NOT EXISTS (SELECT 1 FROM ${spec.table} t WHERE t.id = e.entity_id)`);
|
|
11481
|
+
for (const r of rows) phantoms.push(`${spec.label} ${r.id} (${String(r.ts).slice(0, 16)})`);
|
|
11482
|
+
}
|
|
11483
|
+
checks.push({
|
|
11484
|
+
name: "write_integrity",
|
|
11485
|
+
status: phantoms.length ? "fail" : "pass",
|
|
11486
|
+
detail: phantoms.length ? `${phantoms.length} phantom write event(s) in last 48h: ${phantoms.slice(0, 3).join(", ")}${phantoms.length > 3 ? " …" : ""} — event logged but row missing (rollback or different workspace; check receipts' workspace line)` : "recent write events all match stored rows"
|
|
11487
|
+
});
|
|
11488
|
+
}
|
|
10888
11489
|
const failed = checks.filter((c) => c.status === "fail").length;
|
|
10889
11490
|
const warned = checks.filter((c) => c.status === "warn").length;
|
|
10890
11491
|
const passed = checks.filter((c) => c.status === "pass").length;
|
|
@@ -10910,18 +11511,21 @@ function run(env, action, rest, args) {
|
|
|
10910
11511
|
db.run("UPDATE meta SET value='0' WHERE key='index_dirty'");
|
|
10911
11512
|
fixes.push("index dirty flag cleared (run: ygl-trees node index)");
|
|
10912
11513
|
}
|
|
10913
|
-
|
|
10914
|
-
|
|
10915
|
-
|
|
10916
|
-
holder =
|
|
10917
|
-
|
|
10918
|
-
|
|
10919
|
-
|
|
10920
|
-
|
|
10921
|
-
|
|
10922
|
-
|
|
10923
|
-
|
|
10924
|
-
|
|
11514
|
+
{
|
|
11515
|
+
const { pidAlive } = (init_db(), require_rolldown_runtime.__toCommonJS(db_exports));
|
|
11516
|
+
const lockDir = `${env.db.path}.ygllock`;
|
|
11517
|
+
let holder = "";
|
|
11518
|
+
try {
|
|
11519
|
+
holder = node_fs.readFileSync(`${lockDir}/pid`, "utf8").trim();
|
|
11520
|
+
} catch {}
|
|
11521
|
+
if (node_fs.existsSync(lockDir) && holder !== String(process.pid) && !pidAlive(holder)) try {
|
|
11522
|
+
node_fs.rmSync(lockDir, {
|
|
11523
|
+
recursive: true,
|
|
11524
|
+
force: true
|
|
11525
|
+
});
|
|
11526
|
+
fixes.push("stale lock removed (holder pid dead)");
|
|
11527
|
+
} catch {}
|
|
11528
|
+
}
|
|
10925
11529
|
if (fixes.length) for (const f of fixes) ok(ctx, ` fixed: ${f}`);
|
|
10926
11530
|
else out(ctx, paint(C.dim, " (--fix: no auto-fixable issues found)", ctx.color));
|
|
10927
11531
|
}
|
|
@@ -11046,10 +11650,11 @@ ${paint(C.bold, "research", true)} collect → screen → extract → claim
|
|
|
11046
11650
|
source add --title --url [--abstract --content --relevance --confidence --query]
|
|
11047
11651
|
source fetch <url> fetch page, extract title/meta/text (zero side-effect on failure)
|
|
11048
11652
|
source list | search "<q>" | show <id> | rm <id>
|
|
11049
|
-
evidence add <sourceId> --quote "..." [--note --locator]
|
|
11653
|
+
evidence add <sourceId> --quote "..." | --file <path> [--note --locator]
|
|
11050
11654
|
evidence list
|
|
11051
11655
|
claim add "<text>" [--confidence A|B|C]
|
|
11052
11656
|
claim link <claimId> <evidenceId> [--stance supports|refutes]
|
|
11657
|
+
--by-source <claimId> <sourceId>: link ALL evidence under a source
|
|
11053
11658
|
claim resolve <claimId> --as supported|refuted|contested [--note]
|
|
11054
11659
|
claim list
|
|
11055
11660
|
|
|
@@ -11105,6 +11710,7 @@ ${paint(C.bold, "tree / node", true)} tech-tree node model
|
|
|
11105
11710
|
node validate cycles, dangling edges, dirty index
|
|
11106
11711
|
node export [--out file.json] portable JSON
|
|
11107
11712
|
node import --file <file.json> [--tree name] [--merge] — strict, transactional
|
|
11713
|
+
node timeline [tree] per-tree growth history (add/link/unlock events)
|
|
11108
11714
|
|
|
11109
11715
|
Writes are idempotent (claim/evidence/query/kb/job dedupe by content
|
|
11110
11716
|
hash); pass --force to keep a deliberate duplicate.
|
|
@@ -11145,6 +11751,7 @@ async function main(argv) {
|
|
|
11145
11751
|
const group = args._[0];
|
|
11146
11752
|
const action = args._[1];
|
|
11147
11753
|
if (group === "webui" && (action === "serve" || action === "export")) process.env.YGL_SERVE = "1";
|
|
11754
|
+
if (group === "db" && action === "unlock") process.env.YGL_SERVE = "1";
|
|
11148
11755
|
if (args.flags.version === true || group === "version") {
|
|
11149
11756
|
out(ctx, appVersion());
|
|
11150
11757
|
return 0;
|
|
@@ -11264,8 +11871,13 @@ async function main(argv) {
|
|
|
11264
11871
|
alive = err.code === "EPERM";
|
|
11265
11872
|
}
|
|
11266
11873
|
if (alive) {
|
|
11267
|
-
|
|
11268
|
-
|
|
11874
|
+
let holderCmd = "";
|
|
11875
|
+
try {
|
|
11876
|
+
holderCmd = require("node:fs").readFileSync(require("node:path").join(lock, "cmd"), "utf8").trim();
|
|
11877
|
+
} catch {}
|
|
11878
|
+
out(ctx, ` lock held by live process pid ${holder}${holderCmd ? `\n holder command: ${holderCmd}` : " (parallel CLI session — serve never takes the write lock)"}`);
|
|
11879
|
+
out(ctx, ` reads still work; the lock serializes CLI writers and self-heals when pid ${holder} exits`);
|
|
11880
|
+
out(ctx, ` force through (know what you are doing): YGL_TEST=1 · inspect/clear: ygl-trees db unlock`);
|
|
11269
11881
|
} else {
|
|
11270
11882
|
out(ctx, ` orphaned lock (pid ${holder} is dead) — removing: ${lock}`);
|
|
11271
11883
|
try {
|
|
@@ -11289,7 +11901,9 @@ async function main(argv) {
|
|
|
11289
11901
|
db?.close();
|
|
11290
11902
|
} catch {}
|
|
11291
11903
|
try {
|
|
11292
|
-
(init_db(), require_rolldown_runtime.__toCommonJS(db_exports))
|
|
11904
|
+
const { DB } = (init_db(), require_rolldown_runtime.__toCommonJS(db_exports));
|
|
11905
|
+
if (db?.ownsLock) DB.releaseLockFor(file);
|
|
11906
|
+
else if (DB.lockHolder(file) === String(process.pid)) DB.releaseLockFor(file);
|
|
11293
11907
|
} catch {}
|
|
11294
11908
|
});
|
|
11295
11909
|
process.on("SIGINT", () => shutdown.interrupt(0));
|
|
@@ -11584,13 +12198,14 @@ function generateLlmsManifest() {
|
|
|
11584
12198
|
["db export <file>", "full backup"],
|
|
11585
12199
|
["db sync pg <url>", "one-way mirror to PostgreSQL"],
|
|
11586
12200
|
["db migrate pg <url> [--fresh]", "full-fidelity graduation (with type preflight)"],
|
|
12201
|
+
["db unlock [--force]", "clear stale .ygllock (pid-liveness checked; refuses live holders)"],
|
|
11587
12202
|
["db reset", "delete database"]
|
|
11588
12203
|
]
|
|
11589
12204
|
],
|
|
11590
12205
|
[
|
|
11591
12206
|
"doctor",
|
|
11592
|
-
"Health check (
|
|
11593
|
-
[["doctor [--fix]", "diagnose (+ auto-fix: dirty index, stale lock)"]]
|
|
12207
|
+
"Health check (10 items)",
|
|
12208
|
+
[["doctor [--fix]", "diagnose (+ auto-fix: dirty index, dead stale lock, phantom writes)"]]
|
|
11594
12209
|
],
|
|
11595
12210
|
[
|
|
11596
12211
|
"config",
|