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
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
13
|
exports.parsePrediction = parsePrediction;
|
|
14
|
+
exports.loadStateSource = loadStateSource;
|
|
14
15
|
exports.measureCriterion = measureCriterion;
|
|
15
16
|
exports.measureNpmVersions = measureNpmVersions;
|
|
16
17
|
exports.auditPredictions = auditPredictions;
|
|
@@ -31,25 +32,89 @@ function parsePrediction(text, created) {
|
|
|
31
32
|
}
|
|
32
33
|
return { horizon, deadline: "" };
|
|
33
34
|
}
|
|
34
|
-
/**
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
35
|
+
/** 0.79.1 --state bridge: load a flat counter map from a JSON file path or an
|
|
36
|
+
* http(s) URL (e.g. a DSH API endpoint). Nested objects flatten to dot keys
|
|
37
|
+
* ({counts:{y:12}} → "counts.y"); only finite numbers are kept. Null on any
|
|
38
|
+
* failure — the audit degrades those criteria back to manual. */
|
|
39
|
+
async function loadStateSource(src, fetchImpl = fetch) {
|
|
40
|
+
try {
|
|
41
|
+
let text;
|
|
42
|
+
if (/^https?:\/\//.test(src)) {
|
|
43
|
+
const ctl = new AbortController();
|
|
44
|
+
const t = setTimeout(() => ctl.abort(), 3000);
|
|
45
|
+
const r = await fetchImpl(src, { signal: ctl.signal });
|
|
46
|
+
clearTimeout(t);
|
|
47
|
+
if (!r.ok)
|
|
48
|
+
return null;
|
|
49
|
+
text = await r.text();
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
text = require("node:fs").readFileSync(src, "utf8");
|
|
53
|
+
}
|
|
54
|
+
const flat = {};
|
|
55
|
+
const walk = (obj, prefix) => {
|
|
56
|
+
if (!obj || typeof obj !== "object")
|
|
57
|
+
return;
|
|
58
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
59
|
+
const key = prefix ? `${prefix}.${k}` : k;
|
|
60
|
+
if (typeof v === "number" && Number.isFinite(v))
|
|
61
|
+
flat[key] = v;
|
|
62
|
+
else if (v && typeof v === "object" && !Array.isArray(v))
|
|
63
|
+
walk(v, key);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
walk(JSON.parse(text), "");
|
|
67
|
+
return flat;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
40
70
|
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/** Locally measurable criterion matchers. Null → manual.
|
|
74
|
+
* Precedence: 树/kb workspace counts (local truth) → any `key≥N` whose key
|
|
75
|
+
* exists in the optional --state counter map (external truth, 0.79.1). */
|
|
76
|
+
function measureCriterion(crit, db, state) {
|
|
41
77
|
const parts = [];
|
|
42
78
|
const hits = [];
|
|
79
|
+
const tree = crit.match(/树\s*≥\s*(\d+)/);
|
|
43
80
|
if (tree) {
|
|
44
81
|
const n = db.get("SELECT COUNT(*) c FROM trees").c;
|
|
45
82
|
parts.push(`trees=${n}/${tree[1]}`);
|
|
46
83
|
hits.push(n >= +tree[1]);
|
|
47
84
|
}
|
|
85
|
+
const kb = crit.match(/kb\s*≥\s*(\d+)/);
|
|
48
86
|
if (kb) {
|
|
49
87
|
const n = db.get("SELECT COUNT(*) c FROM kb_notes").c;
|
|
50
88
|
parts.push(`kb=${n}/${kb[1]}`);
|
|
51
89
|
hits.push(n >= +kb[1]);
|
|
52
90
|
}
|
|
91
|
+
if (state) {
|
|
92
|
+
const seen = new Set();
|
|
93
|
+
for (const m of crit.matchAll(/([\w\u4e00-\u9fa5][\w\u4e00-\u9fa5.-]*)\s*≥\s*(\d+)/g)) {
|
|
94
|
+
const key = m[1], need = +m[2];
|
|
95
|
+
if (key === "树" || key === "kb" || seen.has(key))
|
|
96
|
+
continue;
|
|
97
|
+
// 0.79.1: suffix match — a bare criterion key (`y_imported`) binds to a
|
|
98
|
+
// nested flattened state key (`counts.y_imported`) so users don't have
|
|
99
|
+
// to know the state file's shape. Exact match wins; else the SHALLOWEST
|
|
100
|
+
// `.*.<key>` (fewest dots) keeps resolution deterministic.
|
|
101
|
+
let stateKey = key in state ? key : null;
|
|
102
|
+
if (!stateKey) {
|
|
103
|
+
const cands = Object.keys(state).filter((sk) => sk.endsWith("." + key) && Number.isFinite(state[sk]));
|
|
104
|
+
if (cands.length) {
|
|
105
|
+
cands.sort((a, b) => (a.split(".").length - b.split(".").length) || a.localeCompare(b));
|
|
106
|
+
stateKey = cands[0];
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (!stateKey || !Number.isFinite(state[stateKey]))
|
|
110
|
+
continue;
|
|
111
|
+
seen.add(key);
|
|
112
|
+
parts.push(`${key}=${state[stateKey]}/${need}${stateKey !== key ? ` (${stateKey})` : ""}`);
|
|
113
|
+
hits.push(state[stateKey] >= need);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (!parts.length)
|
|
117
|
+
return null;
|
|
53
118
|
const hasOr = crit.includes(" 或 ");
|
|
54
119
|
const hit = hasOr ? hits.some(Boolean) : hits.every(Boolean);
|
|
55
120
|
return { actual: parts.join(" · "), hit };
|
|
@@ -86,7 +151,11 @@ async function auditPredictions(inp) {
|
|
|
86
151
|
const m = c.text.match(/\d{4}-\d{2}-\d{2}\s*前\s*(.+?)(?:——|$)/) || c.text.match(/\]\s*(.+?)(?:——|$)/);
|
|
87
152
|
const criterion = m ? m[1].trim() : "";
|
|
88
153
|
const daysLeft = deadline ? Math.round((Date.parse(deadline) - Date.parse(today)) / 86400000) : null;
|
|
89
|
-
|
|
154
|
+
// 0.79.1 (user report): a claim the human already arbitrated (supported /
|
|
155
|
+
// refuted) is CLOSED — the audit reports the terminal state instead of
|
|
156
|
+
// re-judging it, so the backtest table stops accumulating settled rows.
|
|
157
|
+
const resolved = c.status === "supported" || c.status === "refuted";
|
|
158
|
+
let measure = measureCriterion(criterion, inp.db, inp.state);
|
|
90
159
|
// npm version criterion → registry probe (opt-out for offline/tests)
|
|
91
160
|
if (!measure && /npm 再发\s*≥\s*(\d+)\s*个版本/.test(criterion)) {
|
|
92
161
|
const need = +criterion.match(/npm 再发\s*≥\s*(\d+)/)[1];
|
|
@@ -102,7 +171,11 @@ async function auditPredictions(inp) {
|
|
|
102
171
|
}
|
|
103
172
|
let verdict;
|
|
104
173
|
let hint;
|
|
105
|
-
if (
|
|
174
|
+
if (resolved) {
|
|
175
|
+
verdict = "resolved";
|
|
176
|
+
hint = `已人工仲裁:${c.status}(回测终态,不再复判)`;
|
|
177
|
+
}
|
|
178
|
+
else if (!measure) {
|
|
106
179
|
verdict = "manual";
|
|
107
180
|
hint = "外部世界判据——人工核验后 claim resolve";
|
|
108
181
|
}
|
|
@@ -112,7 +185,7 @@ async function auditPredictions(inp) {
|
|
|
112
185
|
verdict = measure.hit ? "hit" : "miss";
|
|
113
186
|
rows.push({
|
|
114
187
|
claim_id: c.id, horizon, deadline, criterion,
|
|
115
|
-
actual: measure ? measure.actual + (verdict === "pending" && measure.hit ? " · 已达标" : "") : "—",
|
|
188
|
+
actual: measure ? measure.actual + (!resolved && verdict === "pending" && measure.hit ? " · 已达标" : "") : "—",
|
|
116
189
|
verdict, days_left: daysLeft, hint,
|
|
117
190
|
});
|
|
118
191
|
}
|
|
@@ -133,6 +133,12 @@ function run(env, action, rest, args) {
|
|
|
133
133
|
case "spec": {
|
|
134
134
|
const title = rest[0] || String(args.flags.title || "");
|
|
135
135
|
const body = readBody();
|
|
136
|
+
// Feedback confirmation ①: --design-type is a produce design flag; spec
|
|
137
|
+
// silently swallowing it let a decision record land as a plain spec.
|
|
138
|
+
if (args.flags["design-type"] !== undefined) {
|
|
139
|
+
(0, fmt_1.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)");
|
|
140
|
+
return 2;
|
|
141
|
+
}
|
|
136
142
|
if (!title || !body) {
|
|
137
143
|
(0, fmt_1.bad)(ctx, "usage: produce spec <title> --body|-f <file>");
|
|
138
144
|
return 2;
|
|
@@ -41,6 +41,23 @@ const entities_1 = require("../../pipeline/src/entities");
|
|
|
41
41
|
function run(env, action, rest, args) {
|
|
42
42
|
const { ctx, store } = env;
|
|
43
43
|
const name = rest[0];
|
|
44
|
+
/** Report ③-3: selection changes are events (session-tagged) — see the
|
|
45
|
+
* twin helper in techtree/model/src/cmds.ts for the rationale. */
|
|
46
|
+
function logSelectionEvent(kind, projName) {
|
|
47
|
+
try {
|
|
48
|
+
store.db.run("INSERT INTO events(id,ts,actor,kind,entity,entity_id,payload,level) VALUES(?,?,?,?,?,?,?,?)", [
|
|
49
|
+
`ev${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`,
|
|
50
|
+
new Date().toISOString(),
|
|
51
|
+
process.env.YGL_SESSION || "cli",
|
|
52
|
+
kind,
|
|
53
|
+
"project",
|
|
54
|
+
projName,
|
|
55
|
+
JSON.stringify({ name: projName, session: process.env.YGL_SESSION || "cli" }),
|
|
56
|
+
"info",
|
|
57
|
+
]);
|
|
58
|
+
}
|
|
59
|
+
catch { /* telemetry only */ }
|
|
60
|
+
}
|
|
44
61
|
switch (action) {
|
|
45
62
|
case "new": {
|
|
46
63
|
if (!name) {
|
|
@@ -48,12 +65,19 @@ function run(env, action, rest, args) {
|
|
|
48
65
|
return 2;
|
|
49
66
|
}
|
|
50
67
|
const question = String(args.flags.q || args.flags.question || "");
|
|
68
|
+
// Feedback confirmation ②: auto-selecting the new project silently
|
|
69
|
+
// re-points every flag-less command away from prior work — say so.
|
|
70
|
+
const prior = store.db.metaGet("current_project");
|
|
71
|
+
const priorHint = prior && prior !== name
|
|
72
|
+
? ` (prior project '${prior}' is no longer selected — project use ${prior} to switch back)`
|
|
73
|
+
: "";
|
|
51
74
|
return (0, orm_1.getOrm)(env.db.path)
|
|
52
75
|
.then((ds) => writes.projectCreate(ds, name, question))
|
|
53
76
|
.then((p) => {
|
|
77
|
+
logSelectionEvent("project.use", p.name);
|
|
54
78
|
(0, fmt_1.ack)(ctx, p, p?.duplicate === true
|
|
55
|
-
? `project exists, selected: ${p.name} (duplicate, not re-created)`
|
|
56
|
-
: `project created and selected: ${p.name}`);
|
|
79
|
+
? `project exists, selected: ${p.name} (duplicate, not re-created)${priorHint}`
|
|
80
|
+
: `project created and selected: ${p.name}${priorHint}`);
|
|
57
81
|
return 0;
|
|
58
82
|
})
|
|
59
83
|
.catch((e) => {
|
|
@@ -86,6 +110,7 @@ function run(env, action, rest, args) {
|
|
|
86
110
|
return 1;
|
|
87
111
|
}
|
|
88
112
|
store.db.metaSet("current_project", name);
|
|
113
|
+
logSelectionEvent("project.use", name);
|
|
89
114
|
(0, fmt_1.ack)(ctx, { project: name }, `now using: ${name}`);
|
|
90
115
|
return 0;
|
|
91
116
|
}
|
|
@@ -74,10 +74,44 @@ function run(env, action, rest, args) {
|
|
|
74
74
|
.then(fn)
|
|
75
75
|
.then(print)
|
|
76
76
|
.catch((e) => {
|
|
77
|
-
(0, fmt_1.bad)(ctx, e?.message || String(e));
|
|
77
|
+
(0, fmt_1.bad)(ctx, enrichNodeNotFound(e?.message || String(e)));
|
|
78
78
|
return 1;
|
|
79
79
|
});
|
|
80
80
|
}
|
|
81
|
+
/** Report ③-3: selection changes are now first-class events — a parallel
|
|
82
|
+
* session flipping current_tree/current_project becomes observable
|
|
83
|
+
* (agent brief surfaces it; db timeline shows the history). Session
|
|
84
|
+
* identity comes from YGL_SESSION (agents that care can set it). */
|
|
85
|
+
function logSelectionEvent(database, kind, name) {
|
|
86
|
+
try {
|
|
87
|
+
database.run("INSERT INTO events(id,ts,actor,kind,entity,entity_id,payload,level) VALUES(?,?,?,?,?,?,?,?)", [
|
|
88
|
+
`ev${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`,
|
|
89
|
+
new Date().toISOString(),
|
|
90
|
+
process.env.YGL_SESSION || "cli",
|
|
91
|
+
kind,
|
|
92
|
+
kind === "tree.use" ? "tree" : "project",
|
|
93
|
+
name,
|
|
94
|
+
JSON.stringify({ name, session: process.env.YGL_SESSION || "cli" }),
|
|
95
|
+
"info",
|
|
96
|
+
]);
|
|
97
|
+
}
|
|
98
|
+
catch { /* telemetry only — never block the selection */
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/** Report ③ enrichment: "node not found: X" earned a cross-tree lookup —
|
|
102
|
+
* if a parallel session flipped current_tree, the key usually exists in
|
|
103
|
+
* another tree; name it instead of making the user blind-list. */
|
|
104
|
+
function enrichNodeNotFound(msg) {
|
|
105
|
+
const m = /^node not found: (.+)$/.exec(msg);
|
|
106
|
+
if (!m)
|
|
107
|
+
return msg;
|
|
108
|
+
const others = trees.nodeInOtherTrees(m[1], tid);
|
|
109
|
+
if (others.length) {
|
|
110
|
+
const names = others.map((o) => `'${o.tree}'`).join(", ");
|
|
111
|
+
return `${msg}\n key exists in other tree(s): ${names} — pin it: ygl-trees ${env.group} ... -t ${others[0].tree}`;
|
|
112
|
+
}
|
|
113
|
+
return `${msg} (current tree: '${tree?.name ?? "?"}' — node list to inspect, -t <tree> to switch)`;
|
|
114
|
+
}
|
|
81
115
|
// ---------------------------------------------------------------- tree group
|
|
82
116
|
if (env.group === "tree") {
|
|
83
117
|
const sub = action; // new | list | use
|
|
@@ -91,6 +125,7 @@ function run(env, action, rest, args) {
|
|
|
91
125
|
const proj = pid ? db.get("SELECT id FROM projects WHERE name=?", [pid]) : null;
|
|
92
126
|
return runTreeWrite((ds) => treeWrites.treeCreate(ds, name, proj?.id || ""), (t) => {
|
|
93
127
|
db.metaSet("current_tree", t.name);
|
|
128
|
+
logSelectionEvent(db, "tree.use", t.name);
|
|
94
129
|
(0, fmt_1.ack)(ctx, t, t?.duplicate === true
|
|
95
130
|
? `tree exists, selected: ${t.name} (duplicate, not re-created)`
|
|
96
131
|
: `tree created and selected: ${t.name}`);
|
|
@@ -114,6 +149,7 @@ function run(env, action, rest, args) {
|
|
|
114
149
|
return 1;
|
|
115
150
|
}
|
|
116
151
|
db.metaSet("current_tree", name);
|
|
152
|
+
logSelectionEvent(db, "tree.use", name);
|
|
117
153
|
(0, fmt_1.ack)(ctx, { tree: name }, `now using tree: ${name}`);
|
|
118
154
|
return 0;
|
|
119
155
|
}
|
|
@@ -300,56 +336,52 @@ function run(env, action, rest, args) {
|
|
|
300
336
|
}
|
|
301
337
|
if (sub === "can") {
|
|
302
338
|
if (!key) {
|
|
303
|
-
(0, fmt_1.bad)(ctx, "usage: node can <key
|
|
339
|
+
(0, fmt_1.bad)(ctx, "usage: node can <key...> [--context ctx] [--state k=v] [--state-file file] [--eval]");
|
|
304
340
|
return 2;
|
|
305
341
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
console.log(JSON.stringify(r, null, 2));
|
|
315
|
-
return 0;
|
|
316
|
-
}
|
|
317
|
-
(0, fmt_1.head)(ctx, `can unlock: ${r.key}`);
|
|
318
|
-
(0, fmt_1.out)(ctx, ` ${r.ok ? (0, fmt_1.paint)(fmt_1.C.green, "✓ 可解锁", ctx.color) : (0, fmt_1.paint)(fmt_1.C.red, "✗ 不可解锁", ctx.color)}`);
|
|
319
|
-
if (r.missing_required.length) {
|
|
320
|
-
(0, fmt_1.out)(ctx, (0, fmt_1.paint)(fmt_1.C.bold, " 必检未满足", ctx.color));
|
|
321
|
-
for (const m of r.missing_required)
|
|
322
|
-
(0, fmt_1.out)(ctx, ` - ${m}`);
|
|
323
|
-
}
|
|
324
|
-
if (r.blocked_by.length) {
|
|
325
|
-
(0, fmt_1.out)(ctx, (0, fmt_1.paint)(fmt_1.C.bold, " 被排斥", ctx.color));
|
|
326
|
-
for (const b of r.blocked_by)
|
|
327
|
-
(0, fmt_1.out)(ctx, ` - ${b}`);
|
|
328
|
-
}
|
|
329
|
-
if (r.missing_optional.length) {
|
|
330
|
-
(0, fmt_1.out)(ctx, (0, fmt_1.paint)(fmt_1.C.dim, " 可选未满足(不阻塞)", ctx.color));
|
|
331
|
-
for (const m of r.missing_optional)
|
|
332
|
-
(0, fmt_1.out)(ctx, ` - ${m}`);
|
|
333
|
-
}
|
|
334
|
-
if (r.cost_multiplier !== 1)
|
|
335
|
-
(0, fmt_1.out)(ctx, ` 成本系数 ${r.cost_multiplier}`);
|
|
336
|
-
return 0;
|
|
342
|
+
// 0.79.1 (R297 user report): batch mode — multiple keys judged in ONE
|
|
343
|
+
// process (probes used to pay ~1s cold start per node), plus
|
|
344
|
+
// --state-file for JSON state bags (shared or per-node).
|
|
345
|
+
const keys = [...new Set(rest.filter(Boolean))];
|
|
346
|
+
const sharedState = parseState(args);
|
|
347
|
+
let perKeyBags = null;
|
|
348
|
+
try {
|
|
349
|
+
perKeyBags = loadStateFile(args, sharedState);
|
|
337
350
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
351
|
+
catch (e) {
|
|
352
|
+
const msg = e?.message || String(e);
|
|
353
|
+
(0, fmt_1.bad)(ctx, msg);
|
|
354
|
+
return /^usage:/.test(msg) ? 2 : /must contain a JSON object/.test(msg) ? 2 : 1;
|
|
355
|
+
}
|
|
356
|
+
const stateFor = (k) => perKeyBags ? (perKeyBags[k] && typeof perKeyBags[k] === "object" && !Array.isArray(perKeyBags[k]) ? perKeyBags[k] : {}) : sharedState;
|
|
357
|
+
const ctxName = flagStr("context", "default");
|
|
358
|
+
// eval-family config read once per run (R8 credential priority:
|
|
359
|
+
// config decision_model_llm_* first, then env AI_GATEWAY_API_KEY)
|
|
345
360
|
const offline = !!process.env.YGL_EVAL_OFFLINE || (0, config_1.configGetBool)(env.db, "eval.offline");
|
|
346
361
|
const cfgToken = (0, config_1.configGet)(env.db, "decision_model_llm_token").trim();
|
|
347
362
|
const token = offline ? "" : cfgToken || (0, eval_1.evalToken)();
|
|
348
363
|
const endpoint = (0, config_1.configGet)(env.db, "decision_model_llm_api_base").trim() || eval_1.DEFAULT_ENDPOINT;
|
|
349
364
|
const passT = (0, config_1.configGetNumber)(env.db, "eval.pass_threshold");
|
|
350
365
|
const failT = (0, config_1.configGetNumber)(env.db, "eval.fail_threshold");
|
|
351
|
-
|
|
352
|
-
|
|
366
|
+
/** Judge one node: canUnlock (+ eval gates when --eval). Never throws —
|
|
367
|
+
* hard failures come back as {key, error}. */
|
|
368
|
+
const judgeOne = async (k) => {
|
|
369
|
+
let r;
|
|
370
|
+
try {
|
|
371
|
+
r = trees.canUnlock(tid, k, ctxName, stateFor(k));
|
|
372
|
+
}
|
|
373
|
+
catch (e) {
|
|
374
|
+
return { key: k, error: enrichNodeNotFound(e?.message || String(e)) };
|
|
375
|
+
}
|
|
376
|
+
const state = stateFor(k);
|
|
377
|
+
const evalConds = flagBool("eval")
|
|
378
|
+
? trees.condList(tid, k).filter((c) => /^eval(-choice|-score)?$/.test(String(c.op)))
|
|
379
|
+
: [];
|
|
380
|
+
if (!evalConds.length)
|
|
381
|
+
return r;
|
|
382
|
+
// R8: decision-model evaluation for op=eval conditions.
|
|
383
|
+
// Offline-safe: no token / network failure → pending (rc=0).
|
|
384
|
+
const contextPkg = trees.evalContext(tid, k, state, ctxName);
|
|
353
385
|
const evaluated = [];
|
|
354
386
|
let usageIn = 0, usageOut = 0; // R10.4: token spend per run
|
|
355
387
|
for (const c of evalConds) {
|
|
@@ -398,7 +430,7 @@ function run(env, action, rest, args) {
|
|
|
398
430
|
r.ok = r.missing_required.length === 0 && r.blocked_by.length === 0 && pendingList.length === 0;
|
|
399
431
|
try {
|
|
400
432
|
const run = process.env.YGL_RUN_ID || "";
|
|
401
|
-
db.run("INSERT INTO events(id,ts,actor,kind,entity,entity_id,payload,level) VALUES(?,?,?,?,?,?,?,'info')", [(0, text_1.shortId)(["eval.gate", "node",
|
|
433
|
+
db.run("INSERT INTO events(id,ts,actor,kind,entity,entity_id,payload,level) VALUES(?,?,?,?,?,?,?,'info')", [(0, text_1.shortId)(["eval.gate", "node", k], 12), (0, text_1.nowIso)(), "cli", "eval.gate", "node", k,
|
|
402
434
|
JSON.stringify({
|
|
403
435
|
conds: evaluated.map((e) => ({ name: e.name, type: e.type, outcome: e.outcome, p: e.p ?? null, score: e.score ?? null })),
|
|
404
436
|
usage: { inputTokens: usageIn, outputTokens: usageOut },
|
|
@@ -406,37 +438,90 @@ function run(env, action, rest, args) {
|
|
|
406
438
|
})]);
|
|
407
439
|
}
|
|
408
440
|
catch { /* audit best-effort */ }
|
|
441
|
+
return r;
|
|
442
|
+
};
|
|
443
|
+
const batch = keys.length > 1;
|
|
444
|
+
return (async () => {
|
|
445
|
+
const results = [];
|
|
446
|
+
for (const k of keys)
|
|
447
|
+
results.push(await judgeOne(k));
|
|
409
448
|
if (ctx.json) {
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
(0, fmt_1.head)(ctx, `can unlock: ${r.key}`);
|
|
414
|
-
(0, fmt_1.out)(ctx, ` ${r.ok ? (0, fmt_1.paint)(fmt_1.C.green, "✓ 可解锁", ctx.color) : pendingList.length ? (0, fmt_1.paint)(fmt_1.C.yellow, "… 待判定(决策模型 pending)", ctx.color) : (0, fmt_1.paint)(fmt_1.C.red, "✗ 不可解锁", ctx.color)}`);
|
|
415
|
-
if (r.missing_required.length) {
|
|
416
|
-
(0, fmt_1.out)(ctx, (0, fmt_1.paint)(fmt_1.C.bold, " 必检未满足", ctx.color));
|
|
417
|
-
for (const m of r.missing_required)
|
|
418
|
-
(0, fmt_1.out)(ctx, ` - ${m}`);
|
|
449
|
+
// single key → object (frozen shape); batch → array
|
|
450
|
+
console.log(JSON.stringify(batch ? results : results[0], null, 2));
|
|
451
|
+
return results.some((r) => r.error) ? 1 : 0;
|
|
419
452
|
}
|
|
420
|
-
if (
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
(0, fmt_1.
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
(0, fmt_1.out)(ctx, `
|
|
453
|
+
if (!batch) {
|
|
454
|
+
const r = results[0];
|
|
455
|
+
if (r.error) {
|
|
456
|
+
(0, fmt_1.bad)(ctx, r.error);
|
|
457
|
+
return 1;
|
|
458
|
+
}
|
|
459
|
+
if (!r.evaluated) {
|
|
460
|
+
(0, fmt_1.head)(ctx, `can unlock: ${r.key}`);
|
|
461
|
+
(0, fmt_1.out)(ctx, ` ${r.ok ? (0, fmt_1.paint)(fmt_1.C.green, "✓ 可解锁", ctx.color) : (0, fmt_1.paint)(fmt_1.C.red, "✗ 不可解锁", ctx.color)}`);
|
|
462
|
+
if (r.missing_required.length) {
|
|
463
|
+
(0, fmt_1.out)(ctx, (0, fmt_1.paint)(fmt_1.C.bold, " 必检未满足", ctx.color));
|
|
464
|
+
for (const m of r.missing_required)
|
|
465
|
+
(0, fmt_1.out)(ctx, ` - ${m}`);
|
|
466
|
+
}
|
|
467
|
+
if (r.blocked_by.length) {
|
|
468
|
+
(0, fmt_1.out)(ctx, (0, fmt_1.paint)(fmt_1.C.bold, " 被排斥", ctx.color));
|
|
469
|
+
for (const b of r.blocked_by)
|
|
470
|
+
(0, fmt_1.out)(ctx, ` - ${b}`);
|
|
471
|
+
}
|
|
472
|
+
if (r.missing_optional.length) {
|
|
473
|
+
(0, fmt_1.out)(ctx, (0, fmt_1.paint)(fmt_1.C.dim, " 可选未满足(不阻塞)", ctx.color));
|
|
474
|
+
for (const m of r.missing_optional)
|
|
475
|
+
(0, fmt_1.out)(ctx, ` - ${m}`);
|
|
476
|
+
}
|
|
477
|
+
if (r.cost_multiplier !== 1)
|
|
478
|
+
(0, fmt_1.out)(ctx, ` 成本系数 ${r.cost_multiplier}`);
|
|
479
|
+
return 0;
|
|
480
|
+
}
|
|
481
|
+
(0, fmt_1.head)(ctx, `can unlock: ${r.key}`);
|
|
482
|
+
(0, fmt_1.out)(ctx, ` ${r.ok ? (0, fmt_1.paint)(fmt_1.C.green, "✓ 可解锁", ctx.color) : r.pending.length ? (0, fmt_1.paint)(fmt_1.C.yellow, "… 待判定(决策模型 pending)", ctx.color) : (0, fmt_1.paint)(fmt_1.C.red, "✗ 不可解锁", ctx.color)}`);
|
|
483
|
+
if (r.missing_required.length) {
|
|
484
|
+
(0, fmt_1.out)(ctx, (0, fmt_1.paint)(fmt_1.C.bold, " 必检未满足", ctx.color));
|
|
485
|
+
for (const m of r.missing_required)
|
|
486
|
+
(0, fmt_1.out)(ctx, ` - ${m}`);
|
|
487
|
+
}
|
|
488
|
+
if (r.pending.length) {
|
|
489
|
+
(0, fmt_1.out)(ctx, (0, fmt_1.paint)(fmt_1.C.bold, " 决策模型 pending(灰带/未配置,非终态)", ctx.color));
|
|
490
|
+
for (const p of r.pending)
|
|
491
|
+
(0, fmt_1.out)(ctx, ` - ${p}`);
|
|
492
|
+
}
|
|
493
|
+
if (r.blocked_by.length) {
|
|
494
|
+
(0, fmt_1.out)(ctx, (0, fmt_1.paint)(fmt_1.C.bold, " 被排斥", ctx.color));
|
|
495
|
+
for (const b of r.blocked_by)
|
|
496
|
+
(0, fmt_1.out)(ctx, ` - ${b}`);
|
|
497
|
+
}
|
|
498
|
+
if (r.missing_optional.length) {
|
|
499
|
+
(0, fmt_1.out)(ctx, (0, fmt_1.paint)(fmt_1.C.dim, " 可选未满足(不阻塞)", ctx.color));
|
|
500
|
+
for (const m of r.missing_optional)
|
|
501
|
+
(0, fmt_1.out)(ctx, ` - ${m}`);
|
|
502
|
+
}
|
|
503
|
+
for (const e of r.evaluated)
|
|
504
|
+
(0, fmt_1.out)(ctx, (0, fmt_1.paint)(fmt_1.C.dim, ` eval ${e.name}: P=${e.p ?? "-"} → ${e.outcome}${e.reason ? ` (${e.reason})` : ""}`, ctx.color));
|
|
505
|
+
if (r.cost_multiplier !== 1)
|
|
506
|
+
(0, fmt_1.out)(ctx, ` 成本系数 ${r.cost_multiplier}`);
|
|
507
|
+
return 0;
|
|
429
508
|
}
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
509
|
+
// batch human view: one compact line per node (probe-friendly)
|
|
510
|
+
const via = perKeyBags ? "per-node --state-file" : Object.keys(sharedState).length ? "shared state" : "";
|
|
511
|
+
(0, fmt_1.head)(ctx, `can unlock × ${results.length}${via ? ` (${via})` : ""}`);
|
|
512
|
+
for (const r of results) {
|
|
513
|
+
if (r.error) {
|
|
514
|
+
(0, fmt_1.out)(ctx, ` ${(0, fmt_1.paint)(fmt_1.C.red, "✗", ctx.color)} ${r.key} ${r.error}`);
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
const mark = r.ok ? (0, fmt_1.paint)(fmt_1.C.green, "✓", ctx.color) : r.pending?.length ? (0, fmt_1.paint)(fmt_1.C.yellow, "…", ctx.color) : (0, fmt_1.paint)(fmt_1.C.red, "✗", ctx.color);
|
|
518
|
+
const blockers = [
|
|
519
|
+
...(r.missing_required || []).map((m) => m),
|
|
520
|
+
...(r.blocked_by || []).map((b) => `excluded:${b}`),
|
|
521
|
+
].join(", ");
|
|
522
|
+
(0, fmt_1.out)(ctx, ` ${mark} ${r.key}${r.pending?.length ? " (eval pending)" : ""}${blockers ? ` — ${blockers}` : ""}`);
|
|
434
523
|
}
|
|
435
|
-
|
|
436
|
-
(0, fmt_1.out)(ctx, (0, fmt_1.paint)(fmt_1.C.dim, ` eval ${e.name}: P=${e.p ?? "-"} → ${e.outcome}${e.reason ? ` (${e.reason})` : ""}`, ctx.color));
|
|
437
|
-
if (r.cost_multiplier !== 1)
|
|
438
|
-
(0, fmt_1.out)(ctx, ` 成本系数 ${r.cost_multiplier}`);
|
|
439
|
-
return 0;
|
|
524
|
+
return results.some((r) => r.error) ? 1 : 0;
|
|
440
525
|
})().catch((e) => {
|
|
441
526
|
(0, fmt_1.bad)(ctx, e?.message || String(e));
|
|
442
527
|
return 1;
|
|
@@ -444,6 +529,20 @@ function run(env, action, rest, args) {
|
|
|
444
529
|
}
|
|
445
530
|
if (sub === "ready") {
|
|
446
531
|
const state = parseState(args);
|
|
532
|
+
try {
|
|
533
|
+
// 0.79.1: --state-file flat bag works here too (per-node shape makes
|
|
534
|
+
// no sense for ready — it judges the whole tree, so flat-only).
|
|
535
|
+
const bags = loadStateFile(args, state);
|
|
536
|
+
if (bags) {
|
|
537
|
+
(0, fmt_1.bad)(ctx, "--state-file per-node shape (key→object) only applies to batch `node can k1 k2 …`; ready wants a flat counter object");
|
|
538
|
+
return 2;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
catch (e) {
|
|
542
|
+
const msg = e?.message || String(e);
|
|
543
|
+
(0, fmt_1.bad)(ctx, msg);
|
|
544
|
+
return /^usage:/.test(msg) || /must contain a JSON object/.test(msg) ? 2 : 1;
|
|
545
|
+
}
|
|
447
546
|
const ctxName = flagStr("context", "default");
|
|
448
547
|
if (!flagBool("eval")) {
|
|
449
548
|
const rows = trees.ready(tid, ctxName, state);
|
|
@@ -549,7 +648,14 @@ function run(env, action, rest, args) {
|
|
|
549
648
|
(0, fmt_1.bad)(ctx, "usage: node path <key>");
|
|
550
649
|
return 2;
|
|
551
650
|
}
|
|
552
|
-
|
|
651
|
+
let p;
|
|
652
|
+
try {
|
|
653
|
+
p = trees.pathTo(tid, key, flagStr("context", "default"));
|
|
654
|
+
}
|
|
655
|
+
catch (e) {
|
|
656
|
+
(0, fmt_1.bad)(ctx, enrichNodeNotFound(e?.message || String(e)));
|
|
657
|
+
return 1;
|
|
658
|
+
}
|
|
553
659
|
if (ctx.json) {
|
|
554
660
|
console.log(JSON.stringify(p, null, 2));
|
|
555
661
|
return 0;
|
|
@@ -610,7 +716,7 @@ function run(env, action, rest, args) {
|
|
|
610
716
|
if (sub === "show") {
|
|
611
717
|
const n = trees.nodeGet(tid, key);
|
|
612
718
|
if (!n) {
|
|
613
|
-
(0, fmt_1.bad)(ctx, `node not found: ${key}`);
|
|
719
|
+
(0, fmt_1.bad)(ctx, enrichNodeNotFound(`node not found: ${key}`));
|
|
614
720
|
return 1;
|
|
615
721
|
}
|
|
616
722
|
const st = trees.nodeStateOf(tid, key);
|
|
@@ -704,11 +810,39 @@ function run(env, action, rest, args) {
|
|
|
704
810
|
return runTreeWrite((ds) => treeWrites.importTree(ds, spec, { name: flagStr("tree") || undefined, merge: flagBool("merge") }), (r) => {
|
|
705
811
|
(0, fmt_1.ack)(ctx, { tree: r.tree.name, nodes: r.nodes, edges: r.edges, conditions: r.conditions }, `imported tree ${r.tree.name}: ${r.nodes} nodes, ${r.edges} edges, ${r.conditions} conditions`);
|
|
706
812
|
db.metaSet("current_tree", r.tree.name);
|
|
813
|
+
logSelectionEvent(db, "tree.use", r.tree.name);
|
|
707
814
|
if (!ctx.json)
|
|
708
815
|
(0, fmt_1.out)(ctx, (0, fmt_1.paint)(fmt_1.C.dim, ` index rebuilt; now using tree: ${r.tree.name}`, ctx.color));
|
|
709
816
|
return 0;
|
|
710
817
|
});
|
|
711
818
|
}
|
|
819
|
+
// Report ⑤: tree-scoped growth timeline — workspace-level `db timeline`
|
|
820
|
+
// already existed; this projects a single tree's unlock/add milestones.
|
|
821
|
+
if (sub === "timeline") {
|
|
822
|
+
const rows = db.all(`SELECT e.ts, e.kind, e.payload FROM events e
|
|
823
|
+
WHERE e.entity = 'node'
|
|
824
|
+
AND e.entity_id IN (SELECT id FROM nodes WHERE tree_id = ?)
|
|
825
|
+
ORDER BY e.ts DESC LIMIT 30`, [tid]);
|
|
826
|
+
if (ctx.json) {
|
|
827
|
+
console.log(JSON.stringify(rows.map((r) => ({
|
|
828
|
+
ts: r.ts, kind: r.kind, ...JSON.parse(r.payload || "{}"),
|
|
829
|
+
})), null, 2));
|
|
830
|
+
return 0;
|
|
831
|
+
}
|
|
832
|
+
(0, fmt_1.head)(ctx, `Timeline — tree '${tree.name}' (last ${rows.length})`);
|
|
833
|
+
for (const r of rows) {
|
|
834
|
+
let detail = "";
|
|
835
|
+
try {
|
|
836
|
+
const p = JSON.parse(r.payload || "{}");
|
|
837
|
+
detail = [p.key, p.name ? String(p.name).slice(0, 30) : ""].filter(Boolean).join(" ");
|
|
838
|
+
}
|
|
839
|
+
catch { /* */ }
|
|
840
|
+
(0, fmt_1.out)(ctx, ` ${String(r.ts).slice(0, 16)} ${r.kind.padEnd(14)} ${detail}`);
|
|
841
|
+
}
|
|
842
|
+
if (!rows.length)
|
|
843
|
+
(0, fmt_1.out)(ctx, (0, fmt_1.paint)(fmt_1.C.dim, " (no node events yet — unlock/add to start the history)", ctx.color));
|
|
844
|
+
return 0;
|
|
845
|
+
}
|
|
712
846
|
if (sub === "rm") {
|
|
713
847
|
if (!key) {
|
|
714
848
|
(0, fmt_1.bad)(ctx, "usage: node rm <key>");
|
|
@@ -737,3 +871,33 @@ function parseState(args) {
|
|
|
737
871
|
}
|
|
738
872
|
return out;
|
|
739
873
|
}
|
|
874
|
+
/**
|
|
875
|
+
* 0.79.1 (R297 user report): `--state-file <path.json>` loads the state bag
|
|
876
|
+
* from JSON instead of inline `--state k=v`. Two accepted shapes:
|
|
877
|
+
* flat shared {"y_imported": 12} → merged over --state, all keys share it
|
|
878
|
+
* per-node map {"node-a": {"y": 1}, ...} → each judged node gets its own bag
|
|
879
|
+
* (disambiguated by "every value is a plain object"). Returns the per-node map
|
|
880
|
+
* when shape B, null when merged into `shared`. Throws on unreadable/invalid.
|
|
881
|
+
*/
|
|
882
|
+
function loadStateFile(args, shared) {
|
|
883
|
+
const sf = args.flags["state-file"];
|
|
884
|
+
if (sf === undefined)
|
|
885
|
+
return null; // flag absent → no bags (plain can/ready)
|
|
886
|
+
if (sf === true || sf === "")
|
|
887
|
+
throw new Error("usage: --state-file <path.json>");
|
|
888
|
+
let parsed;
|
|
889
|
+
try {
|
|
890
|
+
parsed = JSON.parse(require("node:fs").readFileSync(String(sf), "utf8"));
|
|
891
|
+
}
|
|
892
|
+
catch (e) {
|
|
893
|
+
throw new Error(`cannot read --state-file ${sf}: ${e?.message || e}`);
|
|
894
|
+
}
|
|
895
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
896
|
+
throw new Error("--state-file must contain a JSON object (flat counters, or per-node key→object map)");
|
|
897
|
+
}
|
|
898
|
+
const vals = Object.values(parsed);
|
|
899
|
+
if (vals.length && vals.every((v) => v && typeof v === "object" && !Array.isArray(v)))
|
|
900
|
+
return parsed;
|
|
901
|
+
Object.assign(shared, parsed);
|
|
902
|
+
return null;
|
|
903
|
+
}
|