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.
@@ -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
  }