ygl-trees 0.77.3 → 0.79.1
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 +31 -0
- package/README.md +25 -3
- package/dist/apps/cli/src/bin.js +34 -7
- package/dist/bundle/bin.cjs +794 -91
- 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 +128 -4
- package/dist/packages/research/pipeline/src/predict.js +193 -0
- 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 +8 -0
- package/package.json +4 -3
- package/skills/ygl-trees-quickstart/SKILL.md +18 -7
- package/skills/ygl-trees-research/SKILL.md +2 -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. */
|
|
@@ -4684,6 +4747,77 @@ function run$12(env, action, rest, args) {
|
|
|
4684
4747
|
if (ctx.json) console.log(JSON.stringify({ path: file }, null, 2));
|
|
4685
4748
|
else out(ctx, file);
|
|
4686
4749
|
return 0;
|
|
4750
|
+
case "unlock": {
|
|
4751
|
+
const lockDir = `${file}.ygllock`;
|
|
4752
|
+
let holder = "";
|
|
4753
|
+
try {
|
|
4754
|
+
holder = node_fs.readFileSync(`${lockDir}/pid`, "utf8").trim();
|
|
4755
|
+
} catch {}
|
|
4756
|
+
if (!node_fs.existsSync(lockDir)) {
|
|
4757
|
+
if (ctx.json) console.log(JSON.stringify({
|
|
4758
|
+
lock: lockDir,
|
|
4759
|
+
holder: null,
|
|
4760
|
+
cleared: false,
|
|
4761
|
+
reason: "no lock present"
|
|
4762
|
+
}, null, 2));
|
|
4763
|
+
else ok(ctx, `no lock present: ${lockDir}`);
|
|
4764
|
+
return 0;
|
|
4765
|
+
}
|
|
4766
|
+
if (holder && pidAlive(holder) && !args.flags.force) {
|
|
4767
|
+
const self = Number(holder) === process.pid;
|
|
4768
|
+
let holderCmd = "";
|
|
4769
|
+
try {
|
|
4770
|
+
holderCmd = node_fs.readFileSync(`${lockDir}/cmd`, "utf8").trim();
|
|
4771
|
+
} catch {}
|
|
4772
|
+
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.`;
|
|
4773
|
+
if (self) try {
|
|
4774
|
+
node_fs.rmSync(lockDir, {
|
|
4775
|
+
recursive: true,
|
|
4776
|
+
force: true
|
|
4777
|
+
});
|
|
4778
|
+
if (ctx.json) console.log(JSON.stringify({
|
|
4779
|
+
lock: lockDir,
|
|
4780
|
+
holder,
|
|
4781
|
+
cleared: true,
|
|
4782
|
+
reason: "self (acquired during boot)"
|
|
4783
|
+
}, null, 2));
|
|
4784
|
+
else ok(ctx, `lock cleared: ${lockDir} (self — acquired during boot)`);
|
|
4785
|
+
return 0;
|
|
4786
|
+
} catch {}
|
|
4787
|
+
if (ctx.json) console.log(JSON.stringify({
|
|
4788
|
+
lock: lockDir,
|
|
4789
|
+
holder,
|
|
4790
|
+
holder_cmd: holderCmd || null,
|
|
4791
|
+
cleared: false,
|
|
4792
|
+
reason: "holder alive (use --force to override)"
|
|
4793
|
+
}, null, 2));
|
|
4794
|
+
else {
|
|
4795
|
+
bad(ctx, msg);
|
|
4796
|
+
out(ctx, paint(C.yellow, ` that is a parallel CLI session (serve never takes the write lock) — retry after it exits;`, ctx.color));
|
|
4797
|
+
out(ctx, paint(C.yellow, ` the lock self-heals then. --force overrides (corruption risk if writes are in flight).`, ctx.color));
|
|
4798
|
+
}
|
|
4799
|
+
return 1;
|
|
4800
|
+
}
|
|
4801
|
+
try {
|
|
4802
|
+
node_fs.rmSync(lockDir, {
|
|
4803
|
+
recursive: true,
|
|
4804
|
+
force: true
|
|
4805
|
+
});
|
|
4806
|
+
const why = !holder || !pidAlive(holder) ? `stale (pid ${holder || "?"} dead)` : "forced";
|
|
4807
|
+
if (ctx.json) console.log(JSON.stringify({
|
|
4808
|
+
lock: lockDir,
|
|
4809
|
+
holder: holder || null,
|
|
4810
|
+
cleared: true,
|
|
4811
|
+
reason: why
|
|
4812
|
+
}, null, 2));
|
|
4813
|
+
else ok(ctx, `lock cleared: ${lockDir} (${why})`);
|
|
4814
|
+
return 0;
|
|
4815
|
+
} catch (e) {
|
|
4816
|
+
if (ctx.json) console.log(JSON.stringify({ error: `remove failed: ${e?.message || e}` }, null, 2));
|
|
4817
|
+
else bad(ctx, `remove failed: ${e?.message || e}`);
|
|
4818
|
+
return 1;
|
|
4819
|
+
}
|
|
4820
|
+
}
|
|
4687
4821
|
case "stats": {
|
|
4688
4822
|
const s = env.store.stats();
|
|
4689
4823
|
if (ctx.json) {
|
|
@@ -5635,6 +5769,34 @@ function buildBrief(env) {
|
|
|
5635
5769
|
};
|
|
5636
5770
|
let project = null;
|
|
5637
5771
|
if (cur) project = store.projectByName(cur);
|
|
5772
|
+
try {
|
|
5773
|
+
const recent = store.db.all(`SELECT kind, entity_id, ts, payload FROM events
|
|
5774
|
+
WHERE kind IN ('tree.use','project.use')
|
|
5775
|
+
AND ts >= datetime('now','-10 minutes')
|
|
5776
|
+
ORDER BY ts DESC LIMIT 5`);
|
|
5777
|
+
if (recent.length) {
|
|
5778
|
+
payload.selection_changed = recent.map((r) => ({
|
|
5779
|
+
kind: r.kind,
|
|
5780
|
+
name: r.entity_id,
|
|
5781
|
+
session: (() => {
|
|
5782
|
+
try {
|
|
5783
|
+
return JSON.parse(r.payload || "{}").session || null;
|
|
5784
|
+
} catch {
|
|
5785
|
+
return null;
|
|
5786
|
+
}
|
|
5787
|
+
})(),
|
|
5788
|
+
ts: r.ts
|
|
5789
|
+
}));
|
|
5790
|
+
const others = recent.filter((r) => {
|
|
5791
|
+
try {
|
|
5792
|
+
return (JSON.parse(r.payload || "{}").session || "cli") !== (process.env.YGL_SESSION || "cli");
|
|
5793
|
+
} catch {
|
|
5794
|
+
return false;
|
|
5795
|
+
}
|
|
5796
|
+
});
|
|
5797
|
+
if (others.length) payload.next_actions.unshift(`# 并行会话切换过选择: ${others.map((o) => `${o.kind === "tree.use" ? "tree" : "project"} '${o.entity_id}'`).join(", ")} — 命令请带 -t/-p 固定上下文`);
|
|
5798
|
+
}
|
|
5799
|
+
} catch {}
|
|
5638
5800
|
if (project) {
|
|
5639
5801
|
const claims = store.claimList(project.id);
|
|
5640
5802
|
const ev = store.evidenceList(project.id);
|
|
@@ -6143,6 +6305,181 @@ var init_eval_enhance = require_rolldown_runtime.__esmMin((() => {
|
|
|
6143
6305
|
init_eval();
|
|
6144
6306
|
}));
|
|
6145
6307
|
//#endregion
|
|
6308
|
+
//#region packages/research/pipeline/src/predict.ts
|
|
6309
|
+
var predict_exports = /* @__PURE__ */ require_rolldown_runtime.__exportAll({
|
|
6310
|
+
auditPredictions: () => auditPredictions,
|
|
6311
|
+
loadStateSource: () => loadStateSource,
|
|
6312
|
+
measureCriterion: () => measureCriterion,
|
|
6313
|
+
measureNpmVersions: () => measureNpmVersions,
|
|
6314
|
+
parsePrediction: () => parsePrediction
|
|
6315
|
+
});
|
|
6316
|
+
/** Extract `[预测·X]` horizon + deadline. Explicit `YYYY-MM-DD 前` wins;
|
|
6317
|
+
* otherwise derive from claim creation date + horizon days (7天/90天). */
|
|
6318
|
+
function parsePrediction(text, created) {
|
|
6319
|
+
const h = text.match(/\[预测·([^\]]+)\]/);
|
|
6320
|
+
const horizon = h ? h[1] : "";
|
|
6321
|
+
const d = text.match(/(\d{4}-\d{2}-\d{2})\s*前/);
|
|
6322
|
+
if (d) return {
|
|
6323
|
+
horizon,
|
|
6324
|
+
deadline: d[1]
|
|
6325
|
+
};
|
|
6326
|
+
const days = horizon.match(/(\d+)\s*天/);
|
|
6327
|
+
if (days && created) {
|
|
6328
|
+
const t = Date.parse(created.replace(" ", "T"));
|
|
6329
|
+
if (!Number.isNaN(t)) return {
|
|
6330
|
+
horizon,
|
|
6331
|
+
deadline: new Date(t + +days[1] * 864e5).toISOString().slice(0, 10)
|
|
6332
|
+
};
|
|
6333
|
+
}
|
|
6334
|
+
return {
|
|
6335
|
+
horizon,
|
|
6336
|
+
deadline: ""
|
|
6337
|
+
};
|
|
6338
|
+
}
|
|
6339
|
+
/** 0.79.1 --state bridge: load a flat counter map from a JSON file path or an
|
|
6340
|
+
* http(s) URL (e.g. a DSH API endpoint). Nested objects flatten to dot keys
|
|
6341
|
+
* ({counts:{y:12}} → "counts.y"); only finite numbers are kept. Null on any
|
|
6342
|
+
* failure — the audit degrades those criteria back to manual. */
|
|
6343
|
+
async function loadStateSource(src, fetchImpl = fetch) {
|
|
6344
|
+
try {
|
|
6345
|
+
let text;
|
|
6346
|
+
if (/^https?:\/\//.test(src)) {
|
|
6347
|
+
const ctl = new AbortController();
|
|
6348
|
+
const t = setTimeout(() => ctl.abort(), 3e3);
|
|
6349
|
+
const r = await fetchImpl(src, { signal: ctl.signal });
|
|
6350
|
+
clearTimeout(t);
|
|
6351
|
+
if (!r.ok) return null;
|
|
6352
|
+
text = await r.text();
|
|
6353
|
+
} else text = require("node:fs").readFileSync(src, "utf8");
|
|
6354
|
+
const flat = {};
|
|
6355
|
+
const walk = (obj, prefix) => {
|
|
6356
|
+
if (!obj || typeof obj !== "object") return;
|
|
6357
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
6358
|
+
const key = prefix ? `${prefix}.${k}` : k;
|
|
6359
|
+
if (typeof v === "number" && Number.isFinite(v)) flat[key] = v;
|
|
6360
|
+
else if (v && typeof v === "object" && !Array.isArray(v)) walk(v, key);
|
|
6361
|
+
}
|
|
6362
|
+
};
|
|
6363
|
+
walk(JSON.parse(text), "");
|
|
6364
|
+
return flat;
|
|
6365
|
+
} catch {
|
|
6366
|
+
return null;
|
|
6367
|
+
}
|
|
6368
|
+
}
|
|
6369
|
+
/** Locally measurable criterion matchers. Null → manual.
|
|
6370
|
+
* Precedence: 树/kb workspace counts (local truth) → any `key≥N` whose key
|
|
6371
|
+
* exists in the optional --state counter map (external truth, 0.79.1). */
|
|
6372
|
+
function measureCriterion(crit, db, state) {
|
|
6373
|
+
const parts = [];
|
|
6374
|
+
const hits = [];
|
|
6375
|
+
const tree = crit.match(/树\s*≥\s*(\d+)/);
|
|
6376
|
+
if (tree) {
|
|
6377
|
+
const n = db.get("SELECT COUNT(*) c FROM trees").c;
|
|
6378
|
+
parts.push(`trees=${n}/${tree[1]}`);
|
|
6379
|
+
hits.push(n >= +tree[1]);
|
|
6380
|
+
}
|
|
6381
|
+
const kb = crit.match(/kb\s*≥\s*(\d+)/);
|
|
6382
|
+
if (kb) {
|
|
6383
|
+
const n = db.get("SELECT COUNT(*) c FROM kb_notes").c;
|
|
6384
|
+
parts.push(`kb=${n}/${kb[1]}`);
|
|
6385
|
+
hits.push(n >= +kb[1]);
|
|
6386
|
+
}
|
|
6387
|
+
if (state) {
|
|
6388
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6389
|
+
for (const m of crit.matchAll(/([\w\u4e00-\u9fa5][\w\u4e00-\u9fa5.-]*)\s*≥\s*(\d+)/g)) {
|
|
6390
|
+
const key = m[1], need = +m[2];
|
|
6391
|
+
if (key === "树" || key === "kb" || seen.has(key)) continue;
|
|
6392
|
+
let stateKey = key in state ? key : null;
|
|
6393
|
+
if (!stateKey) {
|
|
6394
|
+
const cands = Object.keys(state).filter((sk) => sk.endsWith("." + key) && Number.isFinite(state[sk]));
|
|
6395
|
+
if (cands.length) {
|
|
6396
|
+
cands.sort((a, b) => a.split(".").length - b.split(".").length || a.localeCompare(b));
|
|
6397
|
+
stateKey = cands[0];
|
|
6398
|
+
}
|
|
6399
|
+
}
|
|
6400
|
+
if (!stateKey || !Number.isFinite(state[stateKey])) continue;
|
|
6401
|
+
seen.add(key);
|
|
6402
|
+
parts.push(`${key}=${state[stateKey]}/${need}${stateKey !== key ? ` (${stateKey})` : ""}`);
|
|
6403
|
+
hits.push(state[stateKey] >= need);
|
|
6404
|
+
}
|
|
6405
|
+
}
|
|
6406
|
+
if (!parts.length) return null;
|
|
6407
|
+
const hit = crit.includes(" 或 ") ? hits.some(Boolean) : hits.every(Boolean);
|
|
6408
|
+
return {
|
|
6409
|
+
actual: parts.join(" · "),
|
|
6410
|
+
hit
|
|
6411
|
+
};
|
|
6412
|
+
}
|
|
6413
|
+
/** npm version count since a date — registry probe with a hard 3s timeout,
|
|
6414
|
+
* offline-safe fallback to manual. */
|
|
6415
|
+
async function measureNpmVersions(pkg, since, fetchImpl) {
|
|
6416
|
+
try {
|
|
6417
|
+
const ctl = new AbortController();
|
|
6418
|
+
const t = setTimeout(() => ctl.abort(), 3e3);
|
|
6419
|
+
const r = await fetchImpl(`https://registry.npmjs.org/${pkg}`, { signal: ctl.signal });
|
|
6420
|
+
clearTimeout(t);
|
|
6421
|
+
if (!r.ok) return null;
|
|
6422
|
+
const times = (await r.json()).time || {};
|
|
6423
|
+
const versions = Object.entries(times).filter(([v, ts]) => v !== "created" && v !== "modified" && String(ts) >= since).map(([v]) => v);
|
|
6424
|
+
return {
|
|
6425
|
+
actual: `npm versions since ${since}: ${versions.length} (${versions.slice(-3).join(",")})`,
|
|
6426
|
+
hit: versions.length > 0
|
|
6427
|
+
};
|
|
6428
|
+
} catch {
|
|
6429
|
+
return null;
|
|
6430
|
+
}
|
|
6431
|
+
}
|
|
6432
|
+
async function auditPredictions(inp) {
|
|
6433
|
+
const today = inp.today || (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
6434
|
+
const rows = [];
|
|
6435
|
+
for (const c of inp.claims) {
|
|
6436
|
+
if (!c.text.startsWith("[预测·")) continue;
|
|
6437
|
+
const { horizon, deadline } = parsePrediction(c.text, c.created);
|
|
6438
|
+
const m = c.text.match(/\d{4}-\d{2}-\d{2}\s*前\s*(.+?)(?:——|$)/) || c.text.match(/\]\s*(.+?)(?:——|$)/);
|
|
6439
|
+
const criterion = m ? m[1].trim() : "";
|
|
6440
|
+
const daysLeft = deadline ? Math.round((Date.parse(deadline) - Date.parse(today)) / 864e5) : null;
|
|
6441
|
+
const resolved = c.status === "supported" || c.status === "refuted";
|
|
6442
|
+
let measure = measureCriterion(criterion, inp.db, inp.state);
|
|
6443
|
+
if (!measure && /npm 再发\s*≥\s*(\d+)\s*个版本/.test(criterion)) {
|
|
6444
|
+
const need = +criterion.match(/npm 再发\s*≥\s*(\d+)/)[1];
|
|
6445
|
+
const base = c.text.match(/基线\s*\S*?@?(\d{4}-\d{2}-\d{2})/);
|
|
6446
|
+
const since = base ? base[1] : (c.created || today).slice(0, 10);
|
|
6447
|
+
if (inp.npmProbe !== false) {
|
|
6448
|
+
const v = await measureNpmVersions("ygl-trees", since, inp.fetchImpl || fetch);
|
|
6449
|
+
if (v) {
|
|
6450
|
+
const n = +(v.actual.match(/:\s*(\d+)/) || [0, 0])[1];
|
|
6451
|
+
measure = {
|
|
6452
|
+
actual: `${v.actual} · need ≥${need}`,
|
|
6453
|
+
hit: n >= need
|
|
6454
|
+
};
|
|
6455
|
+
}
|
|
6456
|
+
}
|
|
6457
|
+
}
|
|
6458
|
+
let verdict;
|
|
6459
|
+
let hint;
|
|
6460
|
+
if (resolved) {
|
|
6461
|
+
verdict = "resolved";
|
|
6462
|
+
hint = `已人工仲裁:${c.status}(回测终态,不再复判)`;
|
|
6463
|
+
} else if (!measure) {
|
|
6464
|
+
verdict = "manual";
|
|
6465
|
+
hint = "外部世界判据——人工核验后 claim resolve";
|
|
6466
|
+
} else if (daysLeft !== null && daysLeft > 0) verdict = "pending";
|
|
6467
|
+
else verdict = measure.hit ? "hit" : "miss";
|
|
6468
|
+
rows.push({
|
|
6469
|
+
claim_id: c.id,
|
|
6470
|
+
horizon,
|
|
6471
|
+
deadline,
|
|
6472
|
+
criterion,
|
|
6473
|
+
actual: measure ? measure.actual + (!resolved && verdict === "pending" && measure.hit ? " · 已达标" : "") : "—",
|
|
6474
|
+
verdict,
|
|
6475
|
+
days_left: daysLeft,
|
|
6476
|
+
hint
|
|
6477
|
+
});
|
|
6478
|
+
}
|
|
6479
|
+
return rows;
|
|
6480
|
+
}
|
|
6481
|
+
require_rolldown_runtime.__esmMin((() => {}));
|
|
6482
|
+
//#endregion
|
|
6146
6483
|
//#region packages/research/pipeline/src/cmds.ts
|
|
6147
6484
|
/** research group — collect → screen → extract → claim.
|
|
6148
6485
|
* `source fetch` is async (Node 18+ global fetch) and returns a promise.
|
|
@@ -6453,9 +6790,27 @@ function run$8(env, action, rest, args) {
|
|
|
6453
6790
|
if (sub === "evidence") {
|
|
6454
6791
|
if (subAction === "add") {
|
|
6455
6792
|
const sourceId = rest[1];
|
|
6456
|
-
const
|
|
6793
|
+
const file = String(args.flags.file || "");
|
|
6794
|
+
let quote = String(args.flags.quote || "");
|
|
6795
|
+
if (file) {
|
|
6796
|
+
if (quote) {
|
|
6797
|
+
bad(ctx, "--file and --quote are mutually exclusive");
|
|
6798
|
+
return 2;
|
|
6799
|
+
}
|
|
6800
|
+
try {
|
|
6801
|
+
quote = require("fs").readFileSync(file, "utf8").trim();
|
|
6802
|
+
} catch (e) {
|
|
6803
|
+
bad(ctx, `cannot read --file: ${e?.message || e}`);
|
|
6804
|
+
return 1;
|
|
6805
|
+
}
|
|
6806
|
+
if (!quote) {
|
|
6807
|
+
bad(ctx, `--file is empty: ${file}`);
|
|
6808
|
+
return 2;
|
|
6809
|
+
}
|
|
6810
|
+
}
|
|
6811
|
+
if (!quote) quote = readBody();
|
|
6457
6812
|
if (!sourceId || !quote) {
|
|
6458
|
-
bad(ctx, "usage: research evidence add <sourceId> --quote \"...\"");
|
|
6813
|
+
bad(ctx, "usage: research evidence add <sourceId> --quote \"...\" | --file <path> [--note ..] [--locator ..]");
|
|
6459
6814
|
return 2;
|
|
6460
6815
|
}
|
|
6461
6816
|
const note = String(args.flags.note || "");
|
|
@@ -6555,7 +6910,7 @@ function run$8(env, action, rest, args) {
|
|
|
6555
6910
|
}
|
|
6556
6911
|
if (subAction === "link") {
|
|
6557
6912
|
if (!rest[1] || !rest[2]) {
|
|
6558
|
-
bad(ctx, "usage: research claim link <claimId> <evidenceId> [--stance refutes]");
|
|
6913
|
+
bad(ctx, "usage: research claim link <claimId> <evidenceId> [--stance refutes] [--by-source]");
|
|
6559
6914
|
return 2;
|
|
6560
6915
|
}
|
|
6561
6916
|
const stanceRaw = String(args.flags.stance || "supports");
|
|
@@ -6564,6 +6919,34 @@ function run$8(env, action, rest, args) {
|
|
|
6564
6919
|
return 2;
|
|
6565
6920
|
}
|
|
6566
6921
|
const stance = stanceRaw;
|
|
6922
|
+
if (args.flags["by-source"]) {
|
|
6923
|
+
const evs = store.db.all("SELECT id FROM evidence WHERE source_id=? AND project_id=? ORDER BY created", [rest[2], project.id]);
|
|
6924
|
+
if (!evs.length) {
|
|
6925
|
+
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]}`);
|
|
6926
|
+
return 1;
|
|
6927
|
+
}
|
|
6928
|
+
return getOrm(env.db.path).then(async (ds) => {
|
|
6929
|
+
const existing = new Set(store.db.all("SELECT evidence_id FROM claim_evidence WHERE claim_id=?", [rest[1]]).map((x) => x.evidence_id));
|
|
6930
|
+
let linked = 0, dups = 0, status = "";
|
|
6931
|
+
for (const ev of evs) {
|
|
6932
|
+
const r = await claimLink(ds, rest[1], ev.id, stance);
|
|
6933
|
+
existing.has(ev.id) ? dups++ : linked++;
|
|
6934
|
+
status = r.status;
|
|
6935
|
+
}
|
|
6936
|
+
if (ctx.json) console.log(JSON.stringify({
|
|
6937
|
+
claim: rest[1],
|
|
6938
|
+
source: rest[2],
|
|
6939
|
+
linked,
|
|
6940
|
+
duplicates: dups,
|
|
6941
|
+
status
|
|
6942
|
+
}, null, 2));
|
|
6943
|
+
else ok(ctx, `linked ${linked} evidence from source ${rest[2]} (${dups} already) — claim status: ${status}`);
|
|
6944
|
+
return 0;
|
|
6945
|
+
}).catch((e) => {
|
|
6946
|
+
bad(ctx, e?.message || String(e));
|
|
6947
|
+
return 1;
|
|
6948
|
+
});
|
|
6949
|
+
}
|
|
6567
6950
|
return getOrm(env.db.path).then((ds) => claimLink(ds, rest[1], rest[2], stance)).then((r) => {
|
|
6568
6951
|
ok(ctx, `linked; claim status now: ${r.status}`);
|
|
6569
6952
|
try {
|
|
@@ -6598,6 +6981,18 @@ function run$8(env, action, rest, args) {
|
|
|
6598
6981
|
} catch {}
|
|
6599
6982
|
return 0;
|
|
6600
6983
|
}).catch((e) => {
|
|
6984
|
+
const m = /^evidence not found: (.+)$/.exec(e?.message || "");
|
|
6985
|
+
if (m) {
|
|
6986
|
+
const evs = store.db.all("SELECT id FROM evidence WHERE source_id=? AND project_id=? ORDER BY created LIMIT 8", [m[1], project.id]);
|
|
6987
|
+
if (evs.length) {
|
|
6988
|
+
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)`);
|
|
6989
|
+
return 1;
|
|
6990
|
+
}
|
|
6991
|
+
if (store.db.get("SELECT id FROM sources WHERE id=? AND project_id=?", [m[1], project.id])) {
|
|
6992
|
+
bad(ctx, `${e.message} — that id is a SOURCE with no evidence yet: research evidence add ${m[1]} --quote ".." first`);
|
|
6993
|
+
return 1;
|
|
6994
|
+
}
|
|
6995
|
+
}
|
|
6601
6996
|
bad(ctx, e?.message || String(e));
|
|
6602
6997
|
return 1;
|
|
6603
6998
|
});
|
|
@@ -6719,7 +7114,61 @@ function run$8(env, action, rest, args) {
|
|
|
6719
7114
|
return 1;
|
|
6720
7115
|
});
|
|
6721
7116
|
}
|
|
6722
|
-
|
|
7117
|
+
if (sub === "predict") {
|
|
7118
|
+
if (subAction === "audit") {
|
|
7119
|
+
const claims = store.db.all("SELECT id, text, status, created FROM claims WHERE project_id=? AND text LIKE '[预测·%' ORDER BY created", [project.id]);
|
|
7120
|
+
if (!claims.length) {
|
|
7121
|
+
if (ctx.json) console.log("[]");
|
|
7122
|
+
else out(ctx, paint(C.dim, " no [预测·…] claims in this project", ctx.color));
|
|
7123
|
+
return 0;
|
|
7124
|
+
}
|
|
7125
|
+
const { auditPredictions, loadStateSource } = require_rolldown_runtime.__toCommonJS(predict_exports);
|
|
7126
|
+
return (async () => {
|
|
7127
|
+
let state = null;
|
|
7128
|
+
if (args.flags.state) {
|
|
7129
|
+
state = await loadStateSource(String(args.flags.state));
|
|
7130
|
+
if (!state && !ctx.json) out(ctx, paint(C.yellow, ` ⚠ --state ${args.flags.state} 加载失败(文件/URL 不可读或非 JSON)——相关判据回落 manual`, ctx.color));
|
|
7131
|
+
}
|
|
7132
|
+
const rows = await auditPredictions({
|
|
7133
|
+
claims,
|
|
7134
|
+
db: store.db,
|
|
7135
|
+
npmProbe: args.flags.offline !== true,
|
|
7136
|
+
state
|
|
7137
|
+
});
|
|
7138
|
+
const resolvedN = rows.filter((r) => r.verdict === "resolved").length;
|
|
7139
|
+
const visible = args.flags.all ? rows : rows.filter((r) => r.verdict !== "resolved");
|
|
7140
|
+
if (ctx.json) console.log(JSON.stringify(rows, null, 2));
|
|
7141
|
+
else if (!visible.length) out(ctx, paint(C.dim, ` 全部 ${rows.length} 条预测已结案(resolved)——--all 查看终态表`, ctx.color));
|
|
7142
|
+
else {
|
|
7143
|
+
head(ctx, `prediction audit — ${project.name} (${visible.length}/${rows.length}${resolvedN && !args.flags.all ? ` · ${resolvedN} resolved 隐藏 (--all)` : ""})`);
|
|
7144
|
+
table(ctx, visible, [
|
|
7145
|
+
"claim",
|
|
7146
|
+
"horizon",
|
|
7147
|
+
"deadline",
|
|
7148
|
+
"判据",
|
|
7149
|
+
"实测",
|
|
7150
|
+
"判定",
|
|
7151
|
+
"days"
|
|
7152
|
+
], [
|
|
7153
|
+
"claim_id",
|
|
7154
|
+
"horizon",
|
|
7155
|
+
"deadline",
|
|
7156
|
+
"criterion",
|
|
7157
|
+
"actual",
|
|
7158
|
+
"verdict",
|
|
7159
|
+
"days_left"
|
|
7160
|
+
]);
|
|
7161
|
+
const actionable = visible.filter((r) => r.verdict === "hit" || r.verdict === "miss");
|
|
7162
|
+
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));
|
|
7163
|
+
for (const r of visible.filter((x) => x.verdict === "manual")) out(ctx, paint(C.dim, ` · ${r.claim_id}: ${r.hint}`, ctx.color));
|
|
7164
|
+
}
|
|
7165
|
+
return 0;
|
|
7166
|
+
})();
|
|
7167
|
+
}
|
|
7168
|
+
bad(ctx, "usage: research predict audit [--offline] [--state <file|url>] [--all]");
|
|
7169
|
+
return 2;
|
|
7170
|
+
}
|
|
7171
|
+
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));
|
|
6723
7172
|
return 0;
|
|
6724
7173
|
}
|
|
6725
7174
|
//#endregion
|
|
@@ -6732,6 +7181,25 @@ init_entities$1();
|
|
|
6732
7181
|
function run$7(env, action, rest, args) {
|
|
6733
7182
|
const { ctx, store } = env;
|
|
6734
7183
|
const name = rest[0];
|
|
7184
|
+
/** Report ③-3: selection changes are events (session-tagged) — see the
|
|
7185
|
+
* twin helper in techtree/model/src/cmds.ts for the rationale. */
|
|
7186
|
+
function logSelectionEvent(kind, projName) {
|
|
7187
|
+
try {
|
|
7188
|
+
store.db.run("INSERT INTO events(id,ts,actor,kind,entity,entity_id,payload,level) VALUES(?,?,?,?,?,?,?,?)", [
|
|
7189
|
+
`ev${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`,
|
|
7190
|
+
(/* @__PURE__ */ new Date()).toISOString(),
|
|
7191
|
+
process.env.YGL_SESSION || "cli",
|
|
7192
|
+
kind,
|
|
7193
|
+
"project",
|
|
7194
|
+
projName,
|
|
7195
|
+
JSON.stringify({
|
|
7196
|
+
name: projName,
|
|
7197
|
+
session: process.env.YGL_SESSION || "cli"
|
|
7198
|
+
}),
|
|
7199
|
+
"info"
|
|
7200
|
+
]);
|
|
7201
|
+
} catch {}
|
|
7202
|
+
}
|
|
6735
7203
|
switch (action) {
|
|
6736
7204
|
case "new": {
|
|
6737
7205
|
if (!name) {
|
|
@@ -6739,8 +7207,11 @@ function run$7(env, action, rest, args) {
|
|
|
6739
7207
|
return 2;
|
|
6740
7208
|
}
|
|
6741
7209
|
const question = String(args.flags.q || args.flags.question || "");
|
|
7210
|
+
const prior = store.db.metaGet("current_project");
|
|
7211
|
+
const priorHint = prior && prior !== name ? ` (prior project '${prior}' is no longer selected — project use ${prior} to switch back)` : "";
|
|
6742
7212
|
return getOrm(env.db.path).then((ds) => projectCreate(ds, name, question)).then((p) => {
|
|
6743
|
-
|
|
7213
|
+
logSelectionEvent("project.use", p.name);
|
|
7214
|
+
ack(ctx, p, p?.duplicate === true ? `project exists, selected: ${p.name} (duplicate, not re-created)${priorHint}` : `project created and selected: ${p.name}${priorHint}`);
|
|
6744
7215
|
return 0;
|
|
6745
7216
|
}).catch((e) => {
|
|
6746
7217
|
bad(ctx, e?.message || String(e));
|
|
@@ -6780,6 +7251,7 @@ function run$7(env, action, rest, args) {
|
|
|
6780
7251
|
return 1;
|
|
6781
7252
|
}
|
|
6782
7253
|
store.db.metaSet("current_project", name);
|
|
7254
|
+
logSelectionEvent("project.use", name);
|
|
6783
7255
|
ack(ctx, { project: name }, `now using: ${name}`);
|
|
6784
7256
|
return 0;
|
|
6785
7257
|
case "show": {
|
|
@@ -6945,6 +7417,10 @@ function run$6(env, action, rest, args) {
|
|
|
6945
7417
|
case "spec": {
|
|
6946
7418
|
const title = rest[0] || String(args.flags.title || "");
|
|
6947
7419
|
const body = readBody();
|
|
7420
|
+
if (args.flags["design-type"] !== void 0) {
|
|
7421
|
+
bad(ctx, "--design-type belongs to 'produce design' (spec does not take it; if this is a design decision, use: produce design <title> --design-type decision)");
|
|
7422
|
+
return 2;
|
|
7423
|
+
}
|
|
6948
7424
|
if (!title || !body) {
|
|
6949
7425
|
bad(ctx, "usage: produce spec <title> --body|-f <file>");
|
|
6950
7426
|
return 2;
|
|
@@ -7782,10 +8258,41 @@ function run$5(env, action, rest, args) {
|
|
|
7782
8258
|
/** tree-domain write through the transactional service (async command). */
|
|
7783
8259
|
function runTreeWrite(fn, print) {
|
|
7784
8260
|
return getOrm(env.db.path).then(fn).then(print).catch((e) => {
|
|
7785
|
-
bad(ctx, e?.message || String(e));
|
|
8261
|
+
bad(ctx, enrichNodeNotFound(e?.message || String(e)));
|
|
7786
8262
|
return 1;
|
|
7787
8263
|
});
|
|
7788
8264
|
}
|
|
8265
|
+
/** Report ③-3: selection changes are now first-class events — a parallel
|
|
8266
|
+
* session flipping current_tree/current_project becomes observable
|
|
8267
|
+
* (agent brief surfaces it; db timeline shows the history). Session
|
|
8268
|
+
* identity comes from YGL_SESSION (agents that care can set it). */
|
|
8269
|
+
function logSelectionEvent(database, kind, name) {
|
|
8270
|
+
try {
|
|
8271
|
+
database.run("INSERT INTO events(id,ts,actor,kind,entity,entity_id,payload,level) VALUES(?,?,?,?,?,?,?,?)", [
|
|
8272
|
+
`ev${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`,
|
|
8273
|
+
(/* @__PURE__ */ new Date()).toISOString(),
|
|
8274
|
+
process.env.YGL_SESSION || "cli",
|
|
8275
|
+
kind,
|
|
8276
|
+
kind === "tree.use" ? "tree" : "project",
|
|
8277
|
+
name,
|
|
8278
|
+
JSON.stringify({
|
|
8279
|
+
name,
|
|
8280
|
+
session: process.env.YGL_SESSION || "cli"
|
|
8281
|
+
}),
|
|
8282
|
+
"info"
|
|
8283
|
+
]);
|
|
8284
|
+
} catch {}
|
|
8285
|
+
}
|
|
8286
|
+
/** Report ③ enrichment: "node not found: X" earned a cross-tree lookup —
|
|
8287
|
+
* if a parallel session flipped current_tree, the key usually exists in
|
|
8288
|
+
* another tree; name it instead of making the user blind-list. */
|
|
8289
|
+
function enrichNodeNotFound(msg) {
|
|
8290
|
+
const m = /^node not found: (.+)$/.exec(msg);
|
|
8291
|
+
if (!m) return msg;
|
|
8292
|
+
const others = trees.nodeInOtherTrees(m[1], tid);
|
|
8293
|
+
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}`;
|
|
8294
|
+
return `${msg} (current tree: '${tree?.name ?? "?"}' — node list to inspect, -t <tree> to switch)`;
|
|
8295
|
+
}
|
|
7789
8296
|
if (env.group === "tree") {
|
|
7790
8297
|
const sub = action;
|
|
7791
8298
|
const name = rest[0];
|
|
@@ -7798,6 +8305,7 @@ function run$5(env, action, rest, args) {
|
|
|
7798
8305
|
const proj = pid ? db.get("SELECT id FROM projects WHERE name=?", [pid]) : null;
|
|
7799
8306
|
return runTreeWrite((ds) => treeCreate(ds, name, proj?.id || ""), (t) => {
|
|
7800
8307
|
db.metaSet("current_tree", t.name);
|
|
8308
|
+
logSelectionEvent(db, "tree.use", t.name);
|
|
7801
8309
|
ack(ctx, t, t?.duplicate === true ? `tree exists, selected: ${t.name} (duplicate, not re-created)` : `tree created and selected: ${t.name}`);
|
|
7802
8310
|
return 0;
|
|
7803
8311
|
});
|
|
@@ -7826,6 +8334,7 @@ function run$5(env, action, rest, args) {
|
|
|
7826
8334
|
return 1;
|
|
7827
8335
|
}
|
|
7828
8336
|
db.metaSet("current_tree", name);
|
|
8337
|
+
logSelectionEvent(db, "tree.use", name);
|
|
7829
8338
|
ack(ctx, { tree: name }, `now using tree: ${name}`);
|
|
7830
8339
|
return 0;
|
|
7831
8340
|
}
|
|
@@ -8034,43 +8543,43 @@ function run$5(env, action, rest, args) {
|
|
|
8034
8543
|
});
|
|
8035
8544
|
if (sub === "can") {
|
|
8036
8545
|
if (!key) {
|
|
8037
|
-
bad(ctx, "usage: node can <key
|
|
8546
|
+
bad(ctx, "usage: node can <key...> [--context ctx] [--state k=v] [--state-file file] [--eval]");
|
|
8038
8547
|
return 2;
|
|
8039
8548
|
}
|
|
8040
|
-
const
|
|
8041
|
-
const
|
|
8042
|
-
|
|
8043
|
-
|
|
8044
|
-
|
|
8045
|
-
|
|
8046
|
-
|
|
8047
|
-
|
|
8048
|
-
|
|
8049
|
-
head(ctx, `can unlock: ${r.key}`);
|
|
8050
|
-
out(ctx, ` ${r.ok ? paint(C.green, "✓ 可解锁", ctx.color) : paint(C.red, "✗ 不可解锁", ctx.color)}`);
|
|
8051
|
-
if (r.missing_required.length) {
|
|
8052
|
-
out(ctx, paint(C.bold, " 必检未满足", ctx.color));
|
|
8053
|
-
for (const m of r.missing_required) out(ctx, ` - ${m}`);
|
|
8054
|
-
}
|
|
8055
|
-
if (r.blocked_by.length) {
|
|
8056
|
-
out(ctx, paint(C.bold, " 被排斥", ctx.color));
|
|
8057
|
-
for (const b of r.blocked_by) out(ctx, ` - ${b}`);
|
|
8058
|
-
}
|
|
8059
|
-
if (r.missing_optional.length) {
|
|
8060
|
-
out(ctx, paint(C.dim, " 可选未满足(不阻塞)", ctx.color));
|
|
8061
|
-
for (const m of r.missing_optional) out(ctx, ` - ${m}`);
|
|
8062
|
-
}
|
|
8063
|
-
if (r.cost_multiplier !== 1) out(ctx, ` 成本系数 ${r.cost_multiplier}`);
|
|
8064
|
-
return 0;
|
|
8549
|
+
const keys = [...new Set(rest.filter(Boolean))];
|
|
8550
|
+
const sharedState = parseState(args);
|
|
8551
|
+
let perKeyBags = null;
|
|
8552
|
+
try {
|
|
8553
|
+
perKeyBags = loadStateFile(args, sharedState);
|
|
8554
|
+
} catch (e) {
|
|
8555
|
+
const msg = e?.message || String(e);
|
|
8556
|
+
bad(ctx, msg);
|
|
8557
|
+
return /^usage:/.test(msg) ? 2 : /must contain a JSON object/.test(msg) ? 2 : 1;
|
|
8065
8558
|
}
|
|
8559
|
+
const stateFor = (k) => perKeyBags ? perKeyBags[k] && typeof perKeyBags[k] === "object" && !Array.isArray(perKeyBags[k]) ? perKeyBags[k] : {} : sharedState;
|
|
8560
|
+
const ctxName = flagStr("context", "default");
|
|
8066
8561
|
const offline = !!process.env.YGL_EVAL_OFFLINE || configGetBool(env.db, "eval.offline");
|
|
8067
8562
|
const cfgToken = configGet(env.db, "decision_model_llm_token").trim();
|
|
8068
8563
|
const token = offline ? "" : cfgToken || evalToken();
|
|
8069
8564
|
const endpoint = configGet(env.db, "decision_model_llm_api_base").trim() || "https://ai-gateway.vercel.sh/v4/ai/evaluation-model";
|
|
8070
8565
|
const passT = configGetNumber(env.db, "eval.pass_threshold");
|
|
8071
8566
|
const failT = configGetNumber(env.db, "eval.fail_threshold");
|
|
8072
|
-
|
|
8073
|
-
|
|
8567
|
+
/** Judge one node: canUnlock (+ eval gates when --eval). Never throws —
|
|
8568
|
+
* hard failures come back as {key, error}. */
|
|
8569
|
+
const judgeOne = async (k) => {
|
|
8570
|
+
let r;
|
|
8571
|
+
try {
|
|
8572
|
+
r = trees.canUnlock(tid, k, ctxName, stateFor(k));
|
|
8573
|
+
} catch (e) {
|
|
8574
|
+
return {
|
|
8575
|
+
key: k,
|
|
8576
|
+
error: enrichNodeNotFound(e?.message || String(e))
|
|
8577
|
+
};
|
|
8578
|
+
}
|
|
8579
|
+
const state = stateFor(k);
|
|
8580
|
+
const evalConds = flagBool("eval") ? trees.condList(tid, k).filter((c) => /^eval(-choice|-score)?$/.test(String(c.op))) : [];
|
|
8581
|
+
if (!evalConds.length) return r;
|
|
8582
|
+
const contextPkg = trees.evalContext(tid, k, state, ctxName);
|
|
8074
8583
|
const evaluated = [];
|
|
8075
8584
|
let usageIn = 0, usageOut = 0;
|
|
8076
8585
|
for (const c of evalConds) {
|
|
@@ -8131,13 +8640,13 @@ function run$5(env, action, rest, args) {
|
|
|
8131
8640
|
require_text.shortId([
|
|
8132
8641
|
"eval.gate",
|
|
8133
8642
|
"node",
|
|
8134
|
-
|
|
8643
|
+
k
|
|
8135
8644
|
], 12),
|
|
8136
8645
|
require_text.nowIso(),
|
|
8137
8646
|
"cli",
|
|
8138
8647
|
"eval.gate",
|
|
8139
8648
|
"node",
|
|
8140
|
-
|
|
8649
|
+
k,
|
|
8141
8650
|
JSON.stringify({
|
|
8142
8651
|
conds: evaluated.map((e) => ({
|
|
8143
8652
|
name: e.name,
|
|
@@ -8154,31 +8663,74 @@ function run$5(env, action, rest, args) {
|
|
|
8154
8663
|
})
|
|
8155
8664
|
]);
|
|
8156
8665
|
} catch {}
|
|
8666
|
+
return r;
|
|
8667
|
+
};
|
|
8668
|
+
const batch = keys.length > 1;
|
|
8669
|
+
return (async () => {
|
|
8670
|
+
const results = [];
|
|
8671
|
+
for (const k of keys) results.push(await judgeOne(k));
|
|
8157
8672
|
if (ctx.json) {
|
|
8158
|
-
console.log(JSON.stringify(
|
|
8159
|
-
return 0;
|
|
8160
|
-
}
|
|
8161
|
-
head(ctx, `can unlock: ${r.key}`);
|
|
8162
|
-
out(ctx, ` ${r.ok ? paint(C.green, "✓ 可解锁", ctx.color) : pendingList.length ? paint(C.yellow, "… 待判定(决策模型 pending)", ctx.color) : paint(C.red, "✗ 不可解锁", ctx.color)}`);
|
|
8163
|
-
if (r.missing_required.length) {
|
|
8164
|
-
out(ctx, paint(C.bold, " 必检未满足", ctx.color));
|
|
8165
|
-
for (const m of r.missing_required) out(ctx, ` - ${m}`);
|
|
8166
|
-
}
|
|
8167
|
-
if (pendingList.length) {
|
|
8168
|
-
out(ctx, paint(C.bold, " 决策模型 pending(灰带/未配置,非终态)", ctx.color));
|
|
8169
|
-
for (const p of pendingList) out(ctx, ` - ${p}`);
|
|
8673
|
+
console.log(JSON.stringify(batch ? results : results[0], null, 2));
|
|
8674
|
+
return results.some((r) => r.error) ? 1 : 0;
|
|
8170
8675
|
}
|
|
8171
|
-
if (
|
|
8172
|
-
|
|
8173
|
-
|
|
8676
|
+
if (!batch) {
|
|
8677
|
+
const r = results[0];
|
|
8678
|
+
if (r.error) {
|
|
8679
|
+
bad(ctx, r.error);
|
|
8680
|
+
return 1;
|
|
8681
|
+
}
|
|
8682
|
+
if (!r.evaluated) {
|
|
8683
|
+
head(ctx, `can unlock: ${r.key}`);
|
|
8684
|
+
out(ctx, ` ${r.ok ? paint(C.green, "✓ 可解锁", ctx.color) : paint(C.red, "✗ 不可解锁", ctx.color)}`);
|
|
8685
|
+
if (r.missing_required.length) {
|
|
8686
|
+
out(ctx, paint(C.bold, " 必检未满足", ctx.color));
|
|
8687
|
+
for (const m of r.missing_required) out(ctx, ` - ${m}`);
|
|
8688
|
+
}
|
|
8689
|
+
if (r.blocked_by.length) {
|
|
8690
|
+
out(ctx, paint(C.bold, " 被排斥", ctx.color));
|
|
8691
|
+
for (const b of r.blocked_by) out(ctx, ` - ${b}`);
|
|
8692
|
+
}
|
|
8693
|
+
if (r.missing_optional.length) {
|
|
8694
|
+
out(ctx, paint(C.dim, " 可选未满足(不阻塞)", ctx.color));
|
|
8695
|
+
for (const m of r.missing_optional) out(ctx, ` - ${m}`);
|
|
8696
|
+
}
|
|
8697
|
+
if (r.cost_multiplier !== 1) out(ctx, ` 成本系数 ${r.cost_multiplier}`);
|
|
8698
|
+
return 0;
|
|
8699
|
+
}
|
|
8700
|
+
head(ctx, `can unlock: ${r.key}`);
|
|
8701
|
+
out(ctx, ` ${r.ok ? paint(C.green, "✓ 可解锁", ctx.color) : r.pending.length ? paint(C.yellow, "… 待判定(决策模型 pending)", ctx.color) : paint(C.red, "✗ 不可解锁", ctx.color)}`);
|
|
8702
|
+
if (r.missing_required.length) {
|
|
8703
|
+
out(ctx, paint(C.bold, " 必检未满足", ctx.color));
|
|
8704
|
+
for (const m of r.missing_required) out(ctx, ` - ${m}`);
|
|
8705
|
+
}
|
|
8706
|
+
if (r.pending.length) {
|
|
8707
|
+
out(ctx, paint(C.bold, " 决策模型 pending(灰带/未配置,非终态)", ctx.color));
|
|
8708
|
+
for (const p of r.pending) out(ctx, ` - ${p}`);
|
|
8709
|
+
}
|
|
8710
|
+
if (r.blocked_by.length) {
|
|
8711
|
+
out(ctx, paint(C.bold, " 被排斥", ctx.color));
|
|
8712
|
+
for (const b of r.blocked_by) out(ctx, ` - ${b}`);
|
|
8713
|
+
}
|
|
8714
|
+
if (r.missing_optional.length) {
|
|
8715
|
+
out(ctx, paint(C.dim, " 可选未满足(不阻塞)", ctx.color));
|
|
8716
|
+
for (const m of r.missing_optional) out(ctx, ` - ${m}`);
|
|
8717
|
+
}
|
|
8718
|
+
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));
|
|
8719
|
+
if (r.cost_multiplier !== 1) out(ctx, ` 成本系数 ${r.cost_multiplier}`);
|
|
8720
|
+
return 0;
|
|
8174
8721
|
}
|
|
8175
|
-
|
|
8176
|
-
|
|
8177
|
-
|
|
8722
|
+
const via = perKeyBags ? "per-node --state-file" : Object.keys(sharedState).length ? "shared state" : "";
|
|
8723
|
+
head(ctx, `can unlock × ${results.length}${via ? ` (${via})` : ""}`);
|
|
8724
|
+
for (const r of results) {
|
|
8725
|
+
if (r.error) {
|
|
8726
|
+
out(ctx, ` ${paint(C.red, "✗", ctx.color)} ${r.key} ${r.error}`);
|
|
8727
|
+
continue;
|
|
8728
|
+
}
|
|
8729
|
+
const mark = r.ok ? paint(C.green, "✓", ctx.color) : r.pending?.length ? paint(C.yellow, "…", ctx.color) : paint(C.red, "✗", ctx.color);
|
|
8730
|
+
const blockers = [...(r.missing_required || []).map((m) => m), ...(r.blocked_by || []).map((b) => `excluded:${b}`)].join(", ");
|
|
8731
|
+
out(ctx, ` ${mark} ${r.key}${r.pending?.length ? " (eval pending)" : ""}${blockers ? ` — ${blockers}` : ""}`);
|
|
8178
8732
|
}
|
|
8179
|
-
|
|
8180
|
-
if (r.cost_multiplier !== 1) out(ctx, ` 成本系数 ${r.cost_multiplier}`);
|
|
8181
|
-
return 0;
|
|
8733
|
+
return results.some((r) => r.error) ? 1 : 0;
|
|
8182
8734
|
})().catch((e) => {
|
|
8183
8735
|
bad(ctx, e?.message || String(e));
|
|
8184
8736
|
return 1;
|
|
@@ -8186,6 +8738,16 @@ function run$5(env, action, rest, args) {
|
|
|
8186
8738
|
}
|
|
8187
8739
|
if (sub === "ready") {
|
|
8188
8740
|
const state = parseState(args);
|
|
8741
|
+
try {
|
|
8742
|
+
if (loadStateFile(args, state)) {
|
|
8743
|
+
bad(ctx, "--state-file per-node shape (key→object) only applies to batch `node can k1 k2 …`; ready wants a flat counter object");
|
|
8744
|
+
return 2;
|
|
8745
|
+
}
|
|
8746
|
+
} catch (e) {
|
|
8747
|
+
const msg = e?.message || String(e);
|
|
8748
|
+
bad(ctx, msg);
|
|
8749
|
+
return /^usage:/.test(msg) || /must contain a JSON object/.test(msg) ? 2 : 1;
|
|
8750
|
+
}
|
|
8189
8751
|
const ctxName = flagStr("context", "default");
|
|
8190
8752
|
if (!flagBool("eval")) {
|
|
8191
8753
|
const rows = trees.ready(tid, ctxName, state);
|
|
@@ -8366,7 +8928,13 @@ function run$5(env, action, rest, args) {
|
|
|
8366
8928
|
bad(ctx, "usage: node path <key>");
|
|
8367
8929
|
return 2;
|
|
8368
8930
|
}
|
|
8369
|
-
|
|
8931
|
+
let p;
|
|
8932
|
+
try {
|
|
8933
|
+
p = trees.pathTo(tid, key, flagStr("context", "default"));
|
|
8934
|
+
} catch (e) {
|
|
8935
|
+
bad(ctx, enrichNodeNotFound(e?.message || String(e)));
|
|
8936
|
+
return 1;
|
|
8937
|
+
}
|
|
8370
8938
|
if (ctx.json) {
|
|
8371
8939
|
console.log(JSON.stringify(p, null, 2));
|
|
8372
8940
|
return 0;
|
|
@@ -8476,7 +9044,7 @@ function run$5(env, action, rest, args) {
|
|
|
8476
9044
|
if (sub === "show") {
|
|
8477
9045
|
const n = trees.nodeGet(tid, key);
|
|
8478
9046
|
if (!n) {
|
|
8479
|
-
bad(ctx, `node not found: ${key}`);
|
|
9047
|
+
bad(ctx, enrichNodeNotFound(`node not found: ${key}`));
|
|
8480
9048
|
return 1;
|
|
8481
9049
|
}
|
|
8482
9050
|
const st = trees.nodeStateOf(tid, key);
|
|
@@ -8583,10 +9151,36 @@ function run$5(env, action, rest, args) {
|
|
|
8583
9151
|
conditions: r.conditions
|
|
8584
9152
|
}, `imported tree ${r.tree.name}: ${r.nodes} nodes, ${r.edges} edges, ${r.conditions} conditions`);
|
|
8585
9153
|
db.metaSet("current_tree", r.tree.name);
|
|
9154
|
+
logSelectionEvent(db, "tree.use", r.tree.name);
|
|
8586
9155
|
if (!ctx.json) out(ctx, paint(C.dim, ` index rebuilt; now using tree: ${r.tree.name}`, ctx.color));
|
|
8587
9156
|
return 0;
|
|
8588
9157
|
});
|
|
8589
9158
|
}
|
|
9159
|
+
if (sub === "timeline") {
|
|
9160
|
+
const rows = db.all(`SELECT e.ts, e.kind, e.payload FROM events e
|
|
9161
|
+
WHERE e.entity = 'node'
|
|
9162
|
+
AND e.entity_id IN (SELECT id FROM nodes WHERE tree_id = ?)
|
|
9163
|
+
ORDER BY e.ts DESC LIMIT 30`, [tid]);
|
|
9164
|
+
if (ctx.json) {
|
|
9165
|
+
console.log(JSON.stringify(rows.map((r) => ({
|
|
9166
|
+
ts: r.ts,
|
|
9167
|
+
kind: r.kind,
|
|
9168
|
+
...JSON.parse(r.payload || "{}")
|
|
9169
|
+
})), null, 2));
|
|
9170
|
+
return 0;
|
|
9171
|
+
}
|
|
9172
|
+
head(ctx, `Timeline — tree '${tree.name}' (last ${rows.length})`);
|
|
9173
|
+
for (const r of rows) {
|
|
9174
|
+
let detail = "";
|
|
9175
|
+
try {
|
|
9176
|
+
const p = JSON.parse(r.payload || "{}");
|
|
9177
|
+
detail = [p.key, p.name ? String(p.name).slice(0, 30) : ""].filter(Boolean).join(" ");
|
|
9178
|
+
} catch {}
|
|
9179
|
+
out(ctx, ` ${String(r.ts).slice(0, 16)} ${r.kind.padEnd(14)} ${detail}`);
|
|
9180
|
+
}
|
|
9181
|
+
if (!rows.length) out(ctx, paint(C.dim, " (no node events yet — unlock/add to start the history)", ctx.color));
|
|
9182
|
+
return 0;
|
|
9183
|
+
}
|
|
8590
9184
|
if (sub === "rm") {
|
|
8591
9185
|
if (!key) {
|
|
8592
9186
|
bad(ctx, "usage: node rm <key>");
|
|
@@ -8613,6 +9207,30 @@ function parseState(args) {
|
|
|
8613
9207
|
}
|
|
8614
9208
|
return out;
|
|
8615
9209
|
}
|
|
9210
|
+
/**
|
|
9211
|
+
* 0.79.1 (R297 user report): `--state-file <path.json>` loads the state bag
|
|
9212
|
+
* from JSON instead of inline `--state k=v`. Two accepted shapes:
|
|
9213
|
+
* flat shared {"y_imported": 12} → merged over --state, all keys share it
|
|
9214
|
+
* per-node map {"node-a": {"y": 1}, ...} → each judged node gets its own bag
|
|
9215
|
+
* (disambiguated by "every value is a plain object"). Returns the per-node map
|
|
9216
|
+
* when shape B, null when merged into `shared`. Throws on unreadable/invalid.
|
|
9217
|
+
*/
|
|
9218
|
+
function loadStateFile(args, shared) {
|
|
9219
|
+
const sf = args.flags["state-file"];
|
|
9220
|
+
if (sf === void 0) return null;
|
|
9221
|
+
if (sf === true || sf === "") throw new Error("usage: --state-file <path.json>");
|
|
9222
|
+
let parsed;
|
|
9223
|
+
try {
|
|
9224
|
+
parsed = JSON.parse(require("node:fs").readFileSync(String(sf), "utf8"));
|
|
9225
|
+
} catch (e) {
|
|
9226
|
+
throw new Error(`cannot read --state-file ${sf}: ${e?.message || e}`);
|
|
9227
|
+
}
|
|
9228
|
+
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)");
|
|
9229
|
+
const vals = Object.values(parsed);
|
|
9230
|
+
if (vals.length && vals.every((v) => v && typeof v === "object" && !Array.isArray(v))) return parsed;
|
|
9231
|
+
Object.assign(shared, parsed);
|
|
9232
|
+
return null;
|
|
9233
|
+
}
|
|
8616
9234
|
//#endregion
|
|
8617
9235
|
//#region packages/knowledge/skills/src/skill-cmds.ts
|
|
8618
9236
|
/** skill group — the L1/L2/L3 registry agents query before acting.
|
|
@@ -8987,7 +9605,22 @@ function run$3(env, action, rest, args) {
|
|
|
8987
9605
|
confidence: String(args.flags.confidence || configGet(env.db, "default_confidence")),
|
|
8988
9606
|
force
|
|
8989
9607
|
})).then((r) => {
|
|
8990
|
-
|
|
9608
|
+
const row = r.row;
|
|
9609
|
+
const verify = store.kbGet(row.id);
|
|
9610
|
+
if (ctx.json) console.log(JSON.stringify({
|
|
9611
|
+
...row,
|
|
9612
|
+
...r.dup ? { duplicate: true } : {},
|
|
9613
|
+
verified: !!verify,
|
|
9614
|
+
workspace: env.home
|
|
9615
|
+
}, null, 2));
|
|
9616
|
+
else {
|
|
9617
|
+
writeResult(ctx, r, (rw) => `note stored: ${rw.id} ${require_text.clip(title, 50)}`);
|
|
9618
|
+
out(ctx, paint(C.dim, ` @ ${env.home}`, ctx.color));
|
|
9619
|
+
if (!verify) {
|
|
9620
|
+
out(ctx, paint(C.yellow, ` ⚠ verify miss: id ${row.id} not readable back — the write may not have landed.`, ctx.color));
|
|
9621
|
+
out(ctx, paint(C.yellow, ` run: ygl-trees doctor (write_integrity check names phantom writes)`, ctx.color));
|
|
9622
|
+
}
|
|
9623
|
+
}
|
|
8991
9624
|
return 0;
|
|
8992
9625
|
}).catch((e) => {
|
|
8993
9626
|
bad(ctx, e?.message || String(e));
|
|
@@ -10511,6 +11144,13 @@ function createDashboardServer(env) {
|
|
|
10511
11144
|
* lifecycle-managed: dispose cleans it automatically. */
|
|
10512
11145
|
function serve(env, args) {
|
|
10513
11146
|
const { ctx } = env;
|
|
11147
|
+
if (args.flags.help || args.flags.h) {
|
|
11148
|
+
out(ctx, `usage: ygl-trees webui serve [--port N] [--host H]`);
|
|
11149
|
+
out(ctx, paint(C.dim, ` --port N listen port (default 8642; 0 = pick a free one)`, ctx.color));
|
|
11150
|
+
out(ctx, paint(C.dim, ` --host H bind address (default 127.0.0.1; 0.0.0.0 opens to LAN)`, ctx.color));
|
|
11151
|
+
out(ctx, paint(C.dim, ` endpoints: / React client · /classic embedded dashboard · /api/events SSE`, ctx.color));
|
|
11152
|
+
return Promise.resolve(0);
|
|
11153
|
+
}
|
|
10514
11154
|
const reactAvailable = resolveDistRoot() !== null;
|
|
10515
11155
|
return new Promise((resolve) => {
|
|
10516
11156
|
const port = Number(args.flags.port || 8642);
|
|
@@ -10668,16 +11308,33 @@ function run(env, action, rest, args) {
|
|
|
10668
11308
|
detail: configRows.length === 0 ? "no overrides (all defaults)" : `${configRows.length} override(s), all valid`
|
|
10669
11309
|
});
|
|
10670
11310
|
const lockPath = `${db.path}.ygllock`;
|
|
10671
|
-
|
|
10672
|
-
|
|
10673
|
-
|
|
10674
|
-
|
|
10675
|
-
|
|
10676
|
-
|
|
10677
|
-
|
|
10678
|
-
|
|
10679
|
-
|
|
10680
|
-
|
|
11311
|
+
{
|
|
11312
|
+
let lockHolder = "";
|
|
11313
|
+
try {
|
|
11314
|
+
lockHolder = node_fs.readFileSync(`${lockPath}/pid`, "utf8").trim();
|
|
11315
|
+
} catch {}
|
|
11316
|
+
const mine = lockHolder === String(process.pid);
|
|
11317
|
+
const foreignLock = node_fs.existsSync(lockPath) && !mine;
|
|
11318
|
+
const holderAlive = !!lockHolder && pidAlive(lockHolder);
|
|
11319
|
+
let holderCmd = "";
|
|
11320
|
+
try {
|
|
11321
|
+
holderCmd = node_fs.readFileSync(`${lockPath}/cmd`, "utf8").trim();
|
|
11322
|
+
} catch {}
|
|
11323
|
+
let status = "pass";
|
|
11324
|
+
let detail = "no stale locks";
|
|
11325
|
+
if (foreignLock && holderAlive) {
|
|
11326
|
+
status = "pass";
|
|
11327
|
+
detail = `lock held by live pid ${lockHolder}${holderCmd ? ` (${holderCmd.slice(0, 80)})` : " (parallel session)"} — normal in multi-session use`;
|
|
11328
|
+
} else if (foreignLock) {
|
|
11329
|
+
status = "warn";
|
|
11330
|
+
detail = `stale lock (pid ${lockHolder || "?"} dead) — ygl-trees db unlock or doctor --fix clears it`;
|
|
11331
|
+
}
|
|
11332
|
+
checks.push({
|
|
11333
|
+
name: "stale_lock",
|
|
11334
|
+
status,
|
|
11335
|
+
detail
|
|
11336
|
+
});
|
|
11337
|
+
}
|
|
10681
11338
|
{
|
|
10682
11339
|
const evalConds = db.all(`SELECT c.id, c.name, c.op, c.value, n.key AS node
|
|
10683
11340
|
FROM node_conditions c JOIN nodes n ON n.id=c.node_id
|
|
@@ -10721,6 +11378,38 @@ function run(env, action, rest, args) {
|
|
|
10721
11378
|
detail: `profile read failed: ${e?.message || e}`
|
|
10722
11379
|
});
|
|
10723
11380
|
}
|
|
11381
|
+
{
|
|
11382
|
+
const PHANTOM_SPECS = [
|
|
11383
|
+
{
|
|
11384
|
+
kind: "kb.add",
|
|
11385
|
+
table: "kb_notes",
|
|
11386
|
+
label: "kb"
|
|
11387
|
+
},
|
|
11388
|
+
{
|
|
11389
|
+
kind: "source.add",
|
|
11390
|
+
table: "sources",
|
|
11391
|
+
label: "source"
|
|
11392
|
+
},
|
|
11393
|
+
{
|
|
11394
|
+
kind: "evidence.add",
|
|
11395
|
+
table: "evidence",
|
|
11396
|
+
label: "evidence"
|
|
11397
|
+
}
|
|
11398
|
+
];
|
|
11399
|
+
const phantoms = [];
|
|
11400
|
+
for (const spec of PHANTOM_SPECS) {
|
|
11401
|
+
const rows = db.all(`SELECT e.entity_id id, e.ts FROM events e
|
|
11402
|
+
WHERE e.kind='${spec.kind}'
|
|
11403
|
+
AND e.ts >= datetime('now', '-2 days')
|
|
11404
|
+
AND NOT EXISTS (SELECT 1 FROM ${spec.table} t WHERE t.id = e.entity_id)`);
|
|
11405
|
+
for (const r of rows) phantoms.push(`${spec.label} ${r.id} (${String(r.ts).slice(0, 16)})`);
|
|
11406
|
+
}
|
|
11407
|
+
checks.push({
|
|
11408
|
+
name: "write_integrity",
|
|
11409
|
+
status: phantoms.length ? "fail" : "pass",
|
|
11410
|
+
detail: phantoms.length ? `${phantoms.length} phantom write event(s) in last 48h: ${phantoms.slice(0, 3).join(", ")}${phantoms.length > 3 ? " …" : ""} — event logged but row missing (rollback or different workspace; check receipts' workspace line)` : "recent write events all match stored rows"
|
|
11411
|
+
});
|
|
11412
|
+
}
|
|
10724
11413
|
const failed = checks.filter((c) => c.status === "fail").length;
|
|
10725
11414
|
const warned = checks.filter((c) => c.status === "warn").length;
|
|
10726
11415
|
const passed = checks.filter((c) => c.status === "pass").length;
|
|
@@ -10746,18 +11435,21 @@ function run(env, action, rest, args) {
|
|
|
10746
11435
|
db.run("UPDATE meta SET value='0' WHERE key='index_dirty'");
|
|
10747
11436
|
fixes.push("index dirty flag cleared (run: ygl-trees node index)");
|
|
10748
11437
|
}
|
|
10749
|
-
|
|
10750
|
-
|
|
10751
|
-
|
|
10752
|
-
holder =
|
|
10753
|
-
|
|
10754
|
-
|
|
10755
|
-
|
|
10756
|
-
|
|
10757
|
-
|
|
10758
|
-
|
|
10759
|
-
|
|
10760
|
-
|
|
11438
|
+
{
|
|
11439
|
+
const { pidAlive } = (init_db(), require_rolldown_runtime.__toCommonJS(db_exports));
|
|
11440
|
+
const lockDir = `${env.db.path}.ygllock`;
|
|
11441
|
+
let holder = "";
|
|
11442
|
+
try {
|
|
11443
|
+
holder = node_fs.readFileSync(`${lockDir}/pid`, "utf8").trim();
|
|
11444
|
+
} catch {}
|
|
11445
|
+
if (node_fs.existsSync(lockDir) && holder !== String(process.pid) && !pidAlive(holder)) try {
|
|
11446
|
+
node_fs.rmSync(lockDir, {
|
|
11447
|
+
recursive: true,
|
|
11448
|
+
force: true
|
|
11449
|
+
});
|
|
11450
|
+
fixes.push("stale lock removed (holder pid dead)");
|
|
11451
|
+
} catch {}
|
|
11452
|
+
}
|
|
10761
11453
|
if (fixes.length) for (const f of fixes) ok(ctx, ` fixed: ${f}`);
|
|
10762
11454
|
else out(ctx, paint(C.dim, " (--fix: no auto-fixable issues found)", ctx.color));
|
|
10763
11455
|
}
|
|
@@ -10882,10 +11574,11 @@ ${paint(C.bold, "research", true)} collect → screen → extract → claim
|
|
|
10882
11574
|
source add --title --url [--abstract --content --relevance --confidence --query]
|
|
10883
11575
|
source fetch <url> fetch page, extract title/meta/text (zero side-effect on failure)
|
|
10884
11576
|
source list | search "<q>" | show <id> | rm <id>
|
|
10885
|
-
evidence add <sourceId> --quote "..." [--note --locator]
|
|
11577
|
+
evidence add <sourceId> --quote "..." | --file <path> [--note --locator]
|
|
10886
11578
|
evidence list
|
|
10887
11579
|
claim add "<text>" [--confidence A|B|C]
|
|
10888
11580
|
claim link <claimId> <evidenceId> [--stance supports|refutes]
|
|
11581
|
+
--by-source <claimId> <sourceId>: link ALL evidence under a source
|
|
10889
11582
|
claim resolve <claimId> --as supported|refuted|contested [--note]
|
|
10890
11583
|
claim list
|
|
10891
11584
|
|
|
@@ -10941,6 +11634,7 @@ ${paint(C.bold, "tree / node", true)} tech-tree node model
|
|
|
10941
11634
|
node validate cycles, dangling edges, dirty index
|
|
10942
11635
|
node export [--out file.json] portable JSON
|
|
10943
11636
|
node import --file <file.json> [--tree name] [--merge] — strict, transactional
|
|
11637
|
+
node timeline [tree] per-tree growth history (add/link/unlock events)
|
|
10944
11638
|
|
|
10945
11639
|
Writes are idempotent (claim/evidence/query/kb/job dedupe by content
|
|
10946
11640
|
hash); pass --force to keep a deliberate duplicate.
|
|
@@ -10981,6 +11675,7 @@ async function main(argv) {
|
|
|
10981
11675
|
const group = args._[0];
|
|
10982
11676
|
const action = args._[1];
|
|
10983
11677
|
if (group === "webui" && (action === "serve" || action === "export")) process.env.YGL_SERVE = "1";
|
|
11678
|
+
if (group === "db" && action === "unlock") process.env.YGL_SERVE = "1";
|
|
10984
11679
|
if (args.flags.version === true || group === "version") {
|
|
10985
11680
|
out(ctx, appVersion());
|
|
10986
11681
|
return 0;
|
|
@@ -11100,8 +11795,13 @@ async function main(argv) {
|
|
|
11100
11795
|
alive = err.code === "EPERM";
|
|
11101
11796
|
}
|
|
11102
11797
|
if (alive) {
|
|
11103
|
-
|
|
11104
|
-
|
|
11798
|
+
let holderCmd = "";
|
|
11799
|
+
try {
|
|
11800
|
+
holderCmd = require("node:fs").readFileSync(require("node:path").join(lock, "cmd"), "utf8").trim();
|
|
11801
|
+
} catch {}
|
|
11802
|
+
out(ctx, ` lock held by live process pid ${holder}${holderCmd ? `\n holder command: ${holderCmd}` : " (parallel CLI session — serve never takes the write lock)"}`);
|
|
11803
|
+
out(ctx, ` reads still work; the lock serializes CLI writers and self-heals when pid ${holder} exits`);
|
|
11804
|
+
out(ctx, ` force through (know what you are doing): YGL_TEST=1 · inspect/clear: ygl-trees db unlock`);
|
|
11105
11805
|
} else {
|
|
11106
11806
|
out(ctx, ` orphaned lock (pid ${holder} is dead) — removing: ${lock}`);
|
|
11107
11807
|
try {
|
|
@@ -11125,7 +11825,9 @@ async function main(argv) {
|
|
|
11125
11825
|
db?.close();
|
|
11126
11826
|
} catch {}
|
|
11127
11827
|
try {
|
|
11128
|
-
(init_db(), require_rolldown_runtime.__toCommonJS(db_exports))
|
|
11828
|
+
const { DB } = (init_db(), require_rolldown_runtime.__toCommonJS(db_exports));
|
|
11829
|
+
if (db?.ownsLock) DB.releaseLockFor(file);
|
|
11830
|
+
else if (DB.lockHolder(file) === String(process.pid)) DB.releaseLockFor(file);
|
|
11129
11831
|
} catch {}
|
|
11130
11832
|
});
|
|
11131
11833
|
process.on("SIGINT", () => shutdown.interrupt(0));
|
|
@@ -11420,13 +12122,14 @@ function generateLlmsManifest() {
|
|
|
11420
12122
|
["db export <file>", "full backup"],
|
|
11421
12123
|
["db sync pg <url>", "one-way mirror to PostgreSQL"],
|
|
11422
12124
|
["db migrate pg <url> [--fresh]", "full-fidelity graduation (with type preflight)"],
|
|
12125
|
+
["db unlock [--force]", "clear stale .ygllock (pid-liveness checked; refuses live holders)"],
|
|
11423
12126
|
["db reset", "delete database"]
|
|
11424
12127
|
]
|
|
11425
12128
|
],
|
|
11426
12129
|
[
|
|
11427
12130
|
"doctor",
|
|
11428
|
-
"Health check (
|
|
11429
|
-
[["doctor [--fix]", "diagnose (+ auto-fix: dirty index, stale lock)"]]
|
|
12131
|
+
"Health check (10 items)",
|
|
12132
|
+
[["doctor [--fix]", "diagnose (+ auto-fix: dirty index, dead stale lock, phantom writes)"]]
|
|
11430
12133
|
],
|
|
11431
12134
|
[
|
|
11432
12135
|
"config",
|