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
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Prediction backtest pipeline (spec: pred-val/backtest-pipeline).
|
|
4
|
+
*
|
|
5
|
+
* Predictions are claims whose text starts with `[预测·<horizon>]` and contains
|
|
6
|
+
* a deadline (`YYYY-MM-DD 前`) plus one or more criteria separated by ` 或 `.
|
|
7
|
+
* The audit parses those locally-measurable criteria, measures the current
|
|
8
|
+
* actual from the workspace DB (or a fast registry probe for npm counts), and
|
|
9
|
+
* reports hit / miss / pending / manual — it NEVER auto-resolves; resolution
|
|
10
|
+
* stays a human arbitration (frozen contract).
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.parsePrediction = parsePrediction;
|
|
14
|
+
exports.loadStateSource = loadStateSource;
|
|
15
|
+
exports.measureCriterion = measureCriterion;
|
|
16
|
+
exports.measureNpmVersions = measureNpmVersions;
|
|
17
|
+
exports.auditPredictions = auditPredictions;
|
|
18
|
+
/** Extract `[预测·X]` horizon + deadline. Explicit `YYYY-MM-DD 前` wins;
|
|
19
|
+
* otherwise derive from claim creation date + horizon days (7天/90天). */
|
|
20
|
+
function parsePrediction(text, created) {
|
|
21
|
+
const h = text.match(/\[预测·([^\]]+)\]/);
|
|
22
|
+
const horizon = h ? h[1] : "";
|
|
23
|
+
const d = text.match(/(\d{4}-\d{2}-\d{2})\s*前/);
|
|
24
|
+
if (d)
|
|
25
|
+
return { horizon, deadline: d[1] };
|
|
26
|
+
const days = horizon.match(/(\d+)\s*天/);
|
|
27
|
+
if (days && created) {
|
|
28
|
+
const t = Date.parse(created.replace(" ", "T"));
|
|
29
|
+
if (!Number.isNaN(t)) {
|
|
30
|
+
return { horizon, deadline: new Date(t + +days[1] * 86400000).toISOString().slice(0, 10) };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return { horizon, deadline: "" };
|
|
34
|
+
}
|
|
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 {
|
|
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) {
|
|
77
|
+
const parts = [];
|
|
78
|
+
const hits = [];
|
|
79
|
+
const tree = crit.match(/树\s*≥\s*(\d+)/);
|
|
80
|
+
if (tree) {
|
|
81
|
+
const n = db.get("SELECT COUNT(*) c FROM trees").c;
|
|
82
|
+
parts.push(`trees=${n}/${tree[1]}`);
|
|
83
|
+
hits.push(n >= +tree[1]);
|
|
84
|
+
}
|
|
85
|
+
const kb = crit.match(/kb\s*≥\s*(\d+)/);
|
|
86
|
+
if (kb) {
|
|
87
|
+
const n = db.get("SELECT COUNT(*) c FROM kb_notes").c;
|
|
88
|
+
parts.push(`kb=${n}/${kb[1]}`);
|
|
89
|
+
hits.push(n >= +kb[1]);
|
|
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;
|
|
118
|
+
const hasOr = crit.includes(" 或 ");
|
|
119
|
+
const hit = hasOr ? hits.some(Boolean) : hits.every(Boolean);
|
|
120
|
+
return { actual: parts.join(" · "), hit };
|
|
121
|
+
}
|
|
122
|
+
/** npm version count since a date — registry probe with a hard 3s timeout,
|
|
123
|
+
* offline-safe fallback to manual. */
|
|
124
|
+
async function measureNpmVersions(pkg, since, fetchImpl) {
|
|
125
|
+
try {
|
|
126
|
+
const ctl = new AbortController();
|
|
127
|
+
const t = setTimeout(() => ctl.abort(), 3000);
|
|
128
|
+
const r = await fetchImpl(`https://registry.npmjs.org/${pkg}`, { signal: ctl.signal });
|
|
129
|
+
clearTimeout(t);
|
|
130
|
+
if (!r.ok)
|
|
131
|
+
return null;
|
|
132
|
+
const meta = await r.json();
|
|
133
|
+
const times = meta.time || {};
|
|
134
|
+
const versions = Object.entries(times)
|
|
135
|
+
.filter(([v, ts]) => v !== "created" && v !== "modified" && String(ts) >= since)
|
|
136
|
+
.map(([v]) => v);
|
|
137
|
+
return { actual: `npm versions since ${since}: ${versions.length} (${versions.slice(-3).join(",")})`, hit: versions.length > 0 };
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
async function auditPredictions(inp) {
|
|
144
|
+
const today = inp.today || new Date().toISOString().slice(0, 10);
|
|
145
|
+
const rows = [];
|
|
146
|
+
for (const c of inp.claims) {
|
|
147
|
+
if (!c.text.startsWith("[预测·"))
|
|
148
|
+
continue;
|
|
149
|
+
const { horizon, deadline } = parsePrediction(c.text, c.created);
|
|
150
|
+
// criterion: prefer the "DATE 前" anchor; fall back to "] " (no-date texts)
|
|
151
|
+
const m = c.text.match(/\d{4}-\d{2}-\d{2}\s*前\s*(.+?)(?:——|$)/) || c.text.match(/\]\s*(.+?)(?:——|$)/);
|
|
152
|
+
const criterion = m ? m[1].trim() : "";
|
|
153
|
+
const daysLeft = deadline ? Math.round((Date.parse(deadline) - Date.parse(today)) / 86400000) : null;
|
|
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);
|
|
159
|
+
// npm version criterion → registry probe (opt-out for offline/tests)
|
|
160
|
+
if (!measure && /npm 再发\s*≥\s*(\d+)\s*个版本/.test(criterion)) {
|
|
161
|
+
const need = +criterion.match(/npm 再发\s*≥\s*(\d+)/)[1];
|
|
162
|
+
const base = c.text.match(/基线\s*\S*?@?(\d{4}-\d{2}-\d{2})/);
|
|
163
|
+
const since = base ? base[1] : (c.created || today).slice(0, 10);
|
|
164
|
+
if (inp.npmProbe !== false) {
|
|
165
|
+
const v = await measureNpmVersions("ygl-trees", since, inp.fetchImpl || fetch);
|
|
166
|
+
if (v) {
|
|
167
|
+
const n = +(v.actual.match(/:\s*(\d+)/) || [0, 0])[1];
|
|
168
|
+
measure = { actual: `${v.actual} · need ≥${need}`, hit: n >= need };
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
let verdict;
|
|
173
|
+
let hint;
|
|
174
|
+
if (resolved) {
|
|
175
|
+
verdict = "resolved";
|
|
176
|
+
hint = `已人工仲裁:${c.status}(回测终态,不再复判)`;
|
|
177
|
+
}
|
|
178
|
+
else if (!measure) {
|
|
179
|
+
verdict = "manual";
|
|
180
|
+
hint = "外部世界判据——人工核验后 claim resolve";
|
|
181
|
+
}
|
|
182
|
+
else if (daysLeft !== null && daysLeft > 0)
|
|
183
|
+
verdict = "pending";
|
|
184
|
+
else
|
|
185
|
+
verdict = measure.hit ? "hit" : "miss";
|
|
186
|
+
rows.push({
|
|
187
|
+
claim_id: c.id, horizon, deadline, criterion,
|
|
188
|
+
actual: measure ? measure.actual + (!resolved && verdict === "pending" && measure.hit ? " · 已达标" : "") : "—",
|
|
189
|
+
verdict, days_left: daysLeft, hint,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
return rows;
|
|
193
|
+
}
|
|
@@ -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
|
}
|