dexterm-cli 0.1.1 → 0.1.3
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/dist/packages/agents/src/bus.js +57 -6
- package/dist/packages/agents/src/executor.js +25 -0
- package/dist/packages/agents/src/follow.js +105 -0
- package/dist/packages/agents/src/gate.js +3 -0
- package/dist/packages/agents/src/identity.js +41 -2
- package/dist/packages/agents/src/index.js +5 -0
- package/dist/packages/agents/src/spectator-server.js +153 -0
- package/dist/packages/agents/src/spectator.js +212 -0
- package/dist/packages/agents/src/tiers.js +5 -0
- package/dist/packages/agents/src/tools.js +3 -0
- package/dist/packages/cli/assets/claude/analyst-system.md +6 -1
- package/dist/packages/cli/assets/claude/plugin/skills/larp-check/SKILL.md +11 -3
- package/dist/packages/cli/assets/claude/plugin/skills/rug-check/SKILL.md +13 -3
- package/dist/packages/cli/assets/claude/plugin/skills/token-dossier/SKILL.md +3 -3
- package/dist/packages/cli/src/agent-cmd.js +107 -10
- package/dist/packages/cli/src/agent-spectator.js +37 -0
- package/dist/packages/cli/src/alerts/adapters/desktop.js +40 -0
- package/dist/packages/cli/src/alerts/adapters/discord.js +36 -0
- package/dist/packages/cli/src/alerts/adapters/telegram.js +43 -0
- package/dist/packages/cli/src/alerts/adapters/toast.js +16 -0
- package/dist/packages/cli/src/alerts/creds.js +83 -0
- package/dist/packages/cli/src/alerts/format.js +66 -0
- package/dist/packages/cli/src/alerts/index.js +10 -0
- package/dist/packages/cli/src/alerts/notifier.js +53 -0
- package/dist/packages/cli/src/alerts/route.js +19 -0
- package/dist/packages/cli/src/alerts/types.js +1 -0
- package/dist/packages/cli/src/alerts-cmd.js +212 -0
- package/dist/packages/cli/src/app-help-gen.js +365 -0
- package/dist/packages/cli/src/backtest-cmd.js +62 -3
- package/dist/packages/cli/src/coin-target.js +2 -0
- package/dist/packages/cli/src/doctor.js +21 -6
- package/dist/packages/cli/src/export.js +235 -0
- package/dist/packages/cli/src/follow-cmd.js +107 -1
- package/dist/packages/cli/src/help-cmd.js +48 -0
- package/dist/packages/cli/src/live/index.js +66 -3
- package/dist/packages/cli/src/live/screens/token-live.js +24 -0
- package/dist/packages/cli/src/live/signals.js +109 -0
- package/dist/packages/cli/src/live/trade.js +24 -1
- package/dist/packages/cli/src/main.js +596 -49
- package/dist/packages/cli/src/mcp-cmd.js +321 -0
- package/dist/packages/cli/src/pnl-app.js +221 -0
- package/dist/packages/cli/src/pnl-card.js +615 -0
- package/dist/packages/cli/src/record-cmd.js +455 -0
- package/dist/packages/cli/src/rules.js +69 -2
- package/dist/packages/cli/src/screens/follows.js +91 -2
- package/dist/packages/cli/src/screens/overlay.js +3 -0
- package/dist/packages/cli/src/screens/positions.js +1 -1
- package/dist/packages/cli/src/screens/rules.js +1 -1
- package/dist/packages/cli/src/skill-eval-build.js +109 -0
- package/dist/packages/cli/src/spectate-cmd.js +104 -0
- package/dist/packages/cli/src/state.js +1 -1
- package/dist/packages/cli/src/update.js +294 -0
- package/dist/packages/cli/src/version.js +3 -0
- package/dist/packages/cli/src/voice-cmd.js +194 -0
- package/dist/packages/cli/src/voice-hud.js +1 -1
- package/dist/packages/cli/src/watch-cmd.js +190 -0
- package/dist/packages/custom/src/rules.js +3 -2
- package/dist/packages/custom/src/schema.js +24 -1
- package/dist/packages/data/src/analyst-actions.js +130 -3
- package/dist/packages/data/src/coin-target.js +82 -0
- package/dist/packages/data/src/fast-lane.js +16 -2
- package/dist/packages/data/src/index.js +4 -0
- package/dist/packages/data/src/intel/helius-history.js +12 -1
- package/dist/packages/data/src/intel/live-signals.js +185 -0
- package/dist/packages/data/src/larp/link-baseline.js +196 -0
- package/dist/packages/data/src/sources/dexscreener.js +14 -0
- package/dist/packages/data/src/sources/pumpportal.js +6 -1
- package/dist/packages/data/src/trade/copy/index.js +1 -1
- package/dist/packages/data/src/trade/copy/stats.js +36 -2
- package/dist/packages/data/src/trade/copy/tracker.js +3 -2
- package/dist/packages/mcp/assets/kb/app-help/app-help-command.md +35 -0
- package/dist/packages/mcp/assets/kb/app-help/ask-the-analyst.md +43 -0
- package/dist/packages/mcp/assets/kb/app-help/backtest.md +33 -0
- package/dist/packages/mcp/assets/kb/app-help/bubble-map.md +35 -0
- package/dist/packages/mcp/assets/kb/app-help/calm-mode.md +31 -0
- package/dist/packages/mcp/assets/kb/app-help/change-voice-speed.md +31 -0
- package/dist/packages/mcp/assets/kb/app-help/change-voice.md +30 -0
- package/dist/packages/mcp/assets/kb/app-help/chart-popup-selftest.md +32 -0
- package/dist/packages/mcp/assets/kb/app-help/coin-images.md +29 -0
- package/dist/packages/mcp/assets/kb/app-help/configure-rpc-key.md +28 -0
- package/dist/packages/mcp/assets/kb/app-help/connect-an-agent-registry.md +35 -0
- package/dist/packages/mcp/assets/kb/app-help/connect-an-agent.md +39 -0
- package/dist/packages/mcp/assets/kb/app-help/custom-keymap.md +32 -0
- package/dist/packages/mcp/assets/kb/app-help/edit-kb-or-skill.md +36 -0
- package/dist/packages/mcp/assets/kb/app-help/edit-rules.md +34 -0
- package/dist/packages/mcp/assets/kb/app-help/export-a-view.md +36 -0
- package/dist/packages/mcp/assets/kb/app-help/follow-a-wallet.md +33 -0
- package/dist/packages/mcp/assets/kb/app-help/getting-started.md +37 -0
- package/dist/packages/mcp/assets/kb/app-help/kol-lists.md +34 -0
- package/dist/packages/mcp/assets/kb/app-help/larp-check.md +36 -0
- package/dist/packages/mcp/assets/kb/app-help/link-a-wallet.md +41 -0
- package/dist/packages/mcp/assets/kb/app-help/login-with-x.md +33 -0
- package/dist/packages/mcp/assets/kb/app-help/narrative-board.md +32 -0
- package/dist/packages/mcp/assets/kb/app-help/open-a-coin.md +43 -0
- package/dist/packages/mcp/assets/kb/app-help/open-chart-popup.md +33 -0
- package/dist/packages/mcp/assets/kb/app-help/order-plans.md +33 -0
- package/dist/packages/mcp/assets/kb/app-help/paper-trade.md +36 -0
- package/dist/packages/mcp/assets/kb/app-help/price-or-marketcap.md +38 -0
- package/dist/packages/mcp/assets/kb/app-help/record-headless.md +34 -0
- package/dist/packages/mcp/assets/kb/app-help/recorder-history.md +39 -0
- package/dist/packages/mcp/assets/kb/app-help/rug-check.md +39 -0
- package/dist/packages/mcp/assets/kb/app-help/run-doctor.md +32 -0
- package/dist/packages/mcp/assets/kb/app-help/scan-mode.md +37 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-analyst.md +181 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-kol.md +190 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-newpairs.md +181 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-positions.md +180 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-screener.md +199 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-token.md +240 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-trending.md +192 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-watchlist.md +181 -0
- package/dist/packages/mcp/assets/kb/app-help/set-a-filter.md +36 -0
- package/dist/packages/mcp/assets/kb/app-help/setup-neural-voice.md +36 -0
- package/dist/packages/mcp/assets/kb/app-help/share-packs.md +34 -0
- package/dist/packages/mcp/assets/kb/app-help/sort-the-screener.md +34 -0
- package/dist/packages/mcp/assets/kb/app-help/spectate-an-agent.md +36 -0
- package/dist/packages/mcp/assets/kb/app-help/standing-mandates.md +33 -0
- package/dist/packages/mcp/assets/kb/app-help/switch-theme.md +42 -0
- package/dist/packages/mcp/assets/kb/app-help/turn-on-alerts.md +37 -0
- package/dist/packages/mcp/assets/kb/app-help/update-dexterm.md +30 -0
- package/dist/packages/mcp/assets/kb/app-help/use-presets.md +42 -0
- package/dist/packages/mcp/assets/kb/app-help/use-voice-mode.md +36 -0
- package/dist/packages/mcp/assets/kb/app-help/watch-a-coin.md +34 -0
- package/dist/packages/mcp/assets/kb/app-help/watch-headless.md +35 -0
- package/dist/packages/mcp/assets/kb/app-help/what-dexterm-shows-agents.md +31 -0
- package/dist/packages/mcp/assets/kb/app-help/workspaces.md +38 -0
- package/dist/packages/mcp/src/app-help.js +135 -0
- package/dist/packages/mcp/src/index.js +3 -0
- package/dist/packages/mcp/src/server.js +179 -5
- package/dist/packages/mcp/src/spectator-sink.js +72 -0
- package/dist/packages/packs/src/index.js +3 -0
- package/dist/packages/packs/src/skill-backtest.js +300 -0
- package/dist/packages/packs/src/skill-care.js +206 -0
- package/dist/packages/popup/src/pages/spectator.css +45 -0
- package/dist/packages/popup/src/pages/spectator.html +25 -0
- package/dist/packages/popup/src/pages/spectator.js +74 -0
- package/dist/packages/popup/src/selftest.js +19 -0
- package/dist/packages/popup/src/server.js +7 -1
- package/dist/packages/tui/src/index.js +3 -0
- package/dist/packages/tui/src/keymap.js +12 -0
- package/dist/packages/tui/src/raster-font.js +195 -0
- package/dist/packages/tui/src/raster.js +213 -0
- package/dist/packages/voice/src/controller.js +57 -4
- package/dist/packages/voice/src/doctor.js +96 -11
- package/dist/packages/voice/src/download.js +184 -0
- package/dist/packages/voice/src/engines.js +55 -8
- package/dist/packages/voice/src/index.js +9 -3
- package/dist/packages/voice/src/speakable.js +111 -0
- package/dist/packages/voice/src/stt/whisper.js +14 -4
- package/dist/packages/voice/src/tts/espeak.js +131 -0
- package/dist/packages/voice/src/tts/piper-setup.js +229 -0
- package/dist/packages/voice/src/tts/piper.js +449 -0
- package/dist/packages/voice/src/tts/say.js +72 -35
- package/dist/packages/voice/src/tts/voices.js +209 -0
- package/package.json +5 -9
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
// Skill backtest harness (task-1401). Runs the replayable part of the default analyst skills over a recorded,
|
|
2
|
+
// outcome-labelled eval set and reports per-grade precision/recall with 95 % Wilson intervals, on a TRAIN and a
|
|
3
|
+
// held-out TEST split. Pure: input is the eval set JSON (built from a recorder DB by `dexterm backtest
|
|
4
|
+
// --build-eval`), so every number is re-runnable offline.
|
|
5
|
+
//
|
|
6
|
+
// Which skills, and what is actually replayed:
|
|
7
|
+
// - larp-check: the links-only baseline grade (@dexterm/data link-baseline.ts) the skill starts from. The model
|
|
8
|
+
// stage (fetched pages, RDAP, GitHub) cannot be replayed on history and is NOT measured here.
|
|
9
|
+
// - rug-check (and token-dossier, which shares its verdict rules): the skill's stated verdict rules over the t0
|
|
10
|
+
// snapshot columns the recorder keeps (authorities, dev %, organic score, top-10 %, liquidity). The model's
|
|
11
|
+
// own rug_probability is not replayed either; the recorded-verdict path (`--subject pack_verdicts|larp`)
|
|
12
|
+
// measures that going forward.
|
|
13
|
+
// The other default skills (trench-scan) have no per-token labelled outcome.
|
|
14
|
+
//
|
|
15
|
+
// Labels: the recorder's keyless-proxy outcome at the eval horizon; bad = rug ∪ dead, ok = flat ∪ fade ∪ runner.
|
|
16
|
+
// That is a PRICE outcome, not ground truth about a product: a LARP grade "agrees" when the coin went bad.
|
|
17
|
+
//
|
|
18
|
+
// Honesty rules enforced in code: tuning functions refuse any TEST row (throw), the split is by time and fixed
|
|
19
|
+
// in the eval file before tuning, and rates under MIN_N rows print as counts only.
|
|
20
|
+
import { gradeLinks, LINK_SIGNALS, linkSignals, } from '../../data/src/index.js';
|
|
21
|
+
import { wilson } from "./larp-eval.js";
|
|
22
|
+
export const SKILL_EVAL_SCHEMA = 'dexterm/packs/skill-eval/1';
|
|
23
|
+
export const SKILL_BACKTEST_SCHEMA = 'dexterm/packs/skill-backtest/1';
|
|
24
|
+
/** Below this many rows in a grade, its rates are printed as counts only (same spirit as the n ≥ 50 gate). */
|
|
25
|
+
export const SKILL_MIN_N = 20;
|
|
26
|
+
/** v0: the rug-check skill's own rules as written before the backtest (dev > 10 %, organic < 40 = soft flags). */
|
|
27
|
+
export const RUG_RULES_V0 = {
|
|
28
|
+
id: 'rug-rules-v0', dev_pct_over: 10, organic_below: 40, top10_over: null, liq_below: null,
|
|
29
|
+
provenance: 'the rug-check skill 0.2.0 / token-dossier computeFlags thresholds before the backtest',
|
|
30
|
+
};
|
|
31
|
+
/** v1: tuned on the TRAIN split (grid search, objective MCC of AVOID ∪ SPECULATIVE → bad). */
|
|
32
|
+
export const RUG_RULES_V1 = {
|
|
33
|
+
id: 'rug-rules-v1', dev_pct_over: null, organic_below: null, top10_over: null, liq_below: 5_000,
|
|
34
|
+
provenance: 'tuned on the train split of the task-1401 eval set (see packages/packs/eval/README.md)',
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* The rug-check verdict rules on recorder t0 columns. AVOID rules are the skill's hard disqualifiers and are never
|
|
38
|
+
* tuned (authorities; LP unlocked off pump.fun with top-10 above 50 %). INSUFFICIENT_DATA only when there is no
|
|
39
|
+
* price and no liquidity (skill rule). Everything tunable only moves a coin between SPECULATIVE and WATCH.
|
|
40
|
+
*/
|
|
41
|
+
export function gradeRugRules(f, rules = RUG_RULES_V1) {
|
|
42
|
+
if (!((f.price_usd ?? 0) > 0) && !((f.liquidity_usd ?? 0) > 0))
|
|
43
|
+
return { grade: 'INSUFFICIENT_DATA', reasons: ['no price and no liquidity'] };
|
|
44
|
+
const avoid = [];
|
|
45
|
+
if (f.freeze_auth === true)
|
|
46
|
+
avoid.push('freeze_authority_enabled');
|
|
47
|
+
if (f.mint_auth === true)
|
|
48
|
+
avoid.push('mint_authority_enabled');
|
|
49
|
+
if (f.lp_locked_pct !== null && f.lp_locked_pct < 100 && f.launchpad !== 'pump.fun' && (f.top10_pct ?? 0) > 50)
|
|
50
|
+
avoid.push('lp_unlocked_and_concentrated');
|
|
51
|
+
if (avoid.length)
|
|
52
|
+
return { grade: 'AVOID', reasons: avoid };
|
|
53
|
+
const soft = [];
|
|
54
|
+
if (rules.dev_pct_over !== null && (f.dev_pct ?? 0) > rules.dev_pct_over)
|
|
55
|
+
soft.push(`dev_over_${rules.dev_pct_over}pct`);
|
|
56
|
+
if (rules.organic_below !== null && f.organic_score !== null && f.organic_score < rules.organic_below)
|
|
57
|
+
soft.push(`organic_below_${rules.organic_below}`);
|
|
58
|
+
if (rules.top10_over !== null && f.top10_pct !== null && f.top10_pct > rules.top10_over)
|
|
59
|
+
soft.push(`top10_over_${rules.top10_over}pct`);
|
|
60
|
+
if (rules.liq_below !== null && (f.liquidity_usd ?? 0) < rules.liq_below)
|
|
61
|
+
soft.push(`liquidity_below_${rules.liq_below}`);
|
|
62
|
+
return soft.length ? { grade: 'SPECULATIVE', reasons: soft } : { grade: 'WATCH', reasons: [] };
|
|
63
|
+
}
|
|
64
|
+
export const LARP_GRADE_CLAIMS = { LARP: 'bad', EARLY_BUT_REAL: 'ok', UNVERIFIABLE: null };
|
|
65
|
+
export const RUG_GRADE_CLAIMS = { AVOID: 'bad', SPECULATIVE: 'bad', WATCH: 'ok', INSUFFICIENT_DATA: null };
|
|
66
|
+
export function mcc(tp, fp, fn, tn) {
|
|
67
|
+
const d = Math.sqrt((tp + fp) * (tp + fn) * (tn + fp) * (tn + fn));
|
|
68
|
+
return d === 0 ? null : (tp * tn - fp * fn) / d;
|
|
69
|
+
}
|
|
70
|
+
export function splitMetrics(rows, claims, order) {
|
|
71
|
+
const n = rows.length;
|
|
72
|
+
const bad = rows.filter((r) => r.bad).length;
|
|
73
|
+
const ok = n - bad;
|
|
74
|
+
const by_grade = order.map((g) => {
|
|
75
|
+
const gr = rows.filter((r) => r.grade === g);
|
|
76
|
+
const gb = gr.filter((r) => r.bad).length;
|
|
77
|
+
const claim = claims[g] ?? null;
|
|
78
|
+
const enough = gr.length >= SKILL_MIN_N;
|
|
79
|
+
const right = claim === 'bad' ? gb : claim === 'ok' ? gr.length - gb : 0;
|
|
80
|
+
const classN = claim === 'bad' ? bad : claim === 'ok' ? ok : 0;
|
|
81
|
+
return {
|
|
82
|
+
grade: g, claims: claim, n: gr.length, bad: gb,
|
|
83
|
+
bad_rate: enough ? wilson(gb, gr.length) : null,
|
|
84
|
+
precision: claim && enough ? wilson(right, gr.length) : null,
|
|
85
|
+
recall: claim && enough && classN > 0 ? wilson(right, classN) : null,
|
|
86
|
+
};
|
|
87
|
+
});
|
|
88
|
+
let tp = 0, fp = 0, fn = 0, tn = 0;
|
|
89
|
+
for (const r of rows) {
|
|
90
|
+
const warn = claims[r.grade] === 'bad';
|
|
91
|
+
if (warn && r.bad)
|
|
92
|
+
tp++;
|
|
93
|
+
else if (warn)
|
|
94
|
+
fp++;
|
|
95
|
+
else if (r.bad)
|
|
96
|
+
fn++;
|
|
97
|
+
else
|
|
98
|
+
tn++;
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
n, bad, base_rate: wilson(bad, n), by_grade,
|
|
102
|
+
binary: { tp, fp, fn, tn, precision: tp + fp >= SKILL_MIN_N ? wilson(tp, tp + fp) : null, recall: bad >= SKILL_MIN_N ? wilson(tp, tp + fn) : null, mcc: mcc(tp, fp, fn, tn) },
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
export const LARP_GRADE_ORDER = ['LARP', 'UNVERIFIABLE', 'EARLY_BUT_REAL'];
|
|
106
|
+
export const RUG_GRADE_ORDER = ['AVOID', 'SPECULATIVE', 'WATCH', 'INSUFFICIENT_DATA'];
|
|
107
|
+
function evalSplit(rows, grade, p, claims, order) {
|
|
108
|
+
return splitMetrics(rows.map((r) => ({ grade: grade(r, p), bad: r.bad })), claims, order);
|
|
109
|
+
}
|
|
110
|
+
const larpGradeOf = (r, rubric) => gradeLinks(r.link, rubric).grade;
|
|
111
|
+
const rugGradeOf = (r, rules) => gradeRugRules(r.rug, rules).grade;
|
|
112
|
+
export function runLarpSubject(set, rubric) {
|
|
113
|
+
const train = set.rows.filter((r) => r.split === 'train');
|
|
114
|
+
const test = set.rows.filter((r) => r.split === 'test');
|
|
115
|
+
return {
|
|
116
|
+
params: rubric.id,
|
|
117
|
+
train: evalSplit(train, larpGradeOf, rubric, LARP_GRADE_CLAIMS, LARP_GRADE_ORDER),
|
|
118
|
+
test: evalSplit(test, larpGradeOf, rubric, LARP_GRADE_CLAIMS, LARP_GRADE_ORDER),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
export function runRugSubject(set, rules) {
|
|
122
|
+
const train = set.rows.filter((r) => r.split === 'train');
|
|
123
|
+
const test = set.rows.filter((r) => r.split === 'test');
|
|
124
|
+
return {
|
|
125
|
+
params: rules.id,
|
|
126
|
+
train: evalSplit(train, rugGradeOf, rules, RUG_GRADE_CLAIMS, RUG_GRADE_ORDER),
|
|
127
|
+
test: evalSplit(test, rugGradeOf, rules, RUG_GRADE_CLAIMS, RUG_GRADE_ORDER),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
// ------------------------------------------------------------------------------------------ tuning (train only)
|
|
131
|
+
function assertTrainOnly(rows) {
|
|
132
|
+
const bad = rows.find((r) => r.split !== 'train');
|
|
133
|
+
if (bad)
|
|
134
|
+
throw new Error(`tuning refused: row ${bad.mint.slice(0, 6)}… is in the ${bad.split} split (tune on the train split only)`);
|
|
135
|
+
}
|
|
136
|
+
/** Better = higher MCC; ties go to the simpler parameter set (smaller `cost`). Null MCC never wins. */
|
|
137
|
+
function better(m, cost, best) {
|
|
138
|
+
if (m === null)
|
|
139
|
+
return false;
|
|
140
|
+
if (!best || best.m === null)
|
|
141
|
+
return true;
|
|
142
|
+
if (m > best.m + 1e-12)
|
|
143
|
+
return true;
|
|
144
|
+
return Math.abs(m - best.m) <= 1e-12 && cost < best.cost;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Grid-search the link rubric on TRAIN rows: weights 0..2 per signal, threshold 1..5, objective MCC of
|
|
148
|
+
* LARP → bad. Rows are bucketed by signal mask first, so the 98 k candidates evaluate in well under a second.
|
|
149
|
+
*/
|
|
150
|
+
export function tuneLinkRubric(rows, id = 'larp-links-v1') {
|
|
151
|
+
assertTrainOnly(rows);
|
|
152
|
+
const buckets = new Map();
|
|
153
|
+
for (const r of rows) {
|
|
154
|
+
const sig = linkSignals(r.link);
|
|
155
|
+
const key = `${r.link.web === 'none' ? 0 : 1}|${sig.join(',')}`;
|
|
156
|
+
const b = buckets.get(key) ?? { sig, hasWeb: r.link.web !== 'none', bad: 0, ok: 0 };
|
|
157
|
+
if (r.bad)
|
|
158
|
+
b.bad++;
|
|
159
|
+
else
|
|
160
|
+
b.ok++;
|
|
161
|
+
buckets.set(key, b);
|
|
162
|
+
}
|
|
163
|
+
const list = [...buckets.values()].map((b) => ({ ...b, idx: b.sig.map((s) => LINK_SIGNALS.indexOf(s)) }));
|
|
164
|
+
const totalBad = rows.filter((r) => r.bad).length;
|
|
165
|
+
const totalOk = rows.length - totalBad;
|
|
166
|
+
const K = LINK_SIGNALS.length;
|
|
167
|
+
const w = new Array(K).fill(0);
|
|
168
|
+
let best = null;
|
|
169
|
+
let candidates = 0;
|
|
170
|
+
const combos = 3 ** K;
|
|
171
|
+
for (let c = 0; c < combos; c++) {
|
|
172
|
+
let x = c;
|
|
173
|
+
let sum = 0;
|
|
174
|
+
for (let k = 0; k < K; k++) {
|
|
175
|
+
w[k] = x % 3;
|
|
176
|
+
x = Math.floor(x / 3);
|
|
177
|
+
sum += w[k];
|
|
178
|
+
}
|
|
179
|
+
if (sum === 0)
|
|
180
|
+
continue;
|
|
181
|
+
for (let t = 1; t <= 5; t++) {
|
|
182
|
+
candidates++;
|
|
183
|
+
let tp = 0, fp = 0;
|
|
184
|
+
for (const b of list) {
|
|
185
|
+
if (!b.hasWeb)
|
|
186
|
+
continue;
|
|
187
|
+
let s = 0;
|
|
188
|
+
for (const i of b.idx)
|
|
189
|
+
s += w[i];
|
|
190
|
+
if (s >= t) {
|
|
191
|
+
tp += b.bad;
|
|
192
|
+
fp += b.ok;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
const m = mcc(tp, fp, totalBad - tp, totalOk - fp);
|
|
196
|
+
if (better(m, sum * 10 + t, best))
|
|
197
|
+
best = { m, cost: sum * 10 + t, w: w.slice(), t };
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const weights = Object.fromEntries(LINK_SIGNALS.map((s, i) => [s, best ? best.w[i] : 0]));
|
|
201
|
+
return {
|
|
202
|
+
rubric: { id, weights, threshold: best?.t ?? 99, provenance: `tuned on ${rows.length} train rows (MCC ${best?.m?.toFixed(3) ?? '—'})` },
|
|
203
|
+
train_mcc: best?.m ?? null, candidates,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
export const RUG_GRID = {
|
|
207
|
+
dev_pct_over: [null, 5, 10, 15, 20, 30],
|
|
208
|
+
organic_below: [null, 10, 20, 30, 40, 50, 60, 70],
|
|
209
|
+
top10_over: [null, 20, 30, 40, 50, 60],
|
|
210
|
+
liq_below: [null, 2_000, 5_000, 10_000, 20_000, 50_000],
|
|
211
|
+
};
|
|
212
|
+
/** Grid-search the tunable rug-check thresholds on TRAIN rows; objective MCC of AVOID ∪ SPECULATIVE → bad. */
|
|
213
|
+
export function tuneRugRules(rows, id = 'rug-rules-v1') {
|
|
214
|
+
assertTrainOnly(rows);
|
|
215
|
+
let best = null;
|
|
216
|
+
let candidates = 0;
|
|
217
|
+
for (const dev of RUG_GRID.dev_pct_over)
|
|
218
|
+
for (const org of RUG_GRID.organic_below)
|
|
219
|
+
for (const top of RUG_GRID.top10_over)
|
|
220
|
+
for (const liq of RUG_GRID.liq_below) {
|
|
221
|
+
candidates++;
|
|
222
|
+
const r = { id, dev_pct_over: dev, organic_below: org, top10_over: top, liq_below: liq, provenance: '' };
|
|
223
|
+
let tp = 0, fp = 0, fn = 0, tn = 0;
|
|
224
|
+
for (const row of rows) {
|
|
225
|
+
const warn = RUG_GRADE_CLAIMS[gradeRugRules(row.rug, r).grade] === 'bad';
|
|
226
|
+
if (warn && row.bad)
|
|
227
|
+
tp++;
|
|
228
|
+
else if (warn)
|
|
229
|
+
fp++;
|
|
230
|
+
else if (row.bad)
|
|
231
|
+
fn++;
|
|
232
|
+
else
|
|
233
|
+
tn++;
|
|
234
|
+
}
|
|
235
|
+
const cost = [dev, org, top, liq].filter((v) => v !== null).length;
|
|
236
|
+
const m = mcc(tp, fp, fn, tn);
|
|
237
|
+
if (better(m, cost, best))
|
|
238
|
+
best = { m, cost, r };
|
|
239
|
+
}
|
|
240
|
+
const rules = best?.r ?? { ...RUG_RULES_V0, id };
|
|
241
|
+
return { rules: { ...rules, provenance: `tuned on ${rows.length} train rows (MCC ${best?.m?.toFixed(3) ?? '—'})` }, train_mcc: best?.m ?? null, candidates };
|
|
242
|
+
}
|
|
243
|
+
export function skillBacktest(set, opts) {
|
|
244
|
+
return {
|
|
245
|
+
schema: SKILL_BACKTEST_SCHEMA,
|
|
246
|
+
eval: { built_at: set.built_at, horizon_sec: set.horizon_sec, cutoff_t0: set.cutoff_t0, counts: set.counts, label_rule: set.label_rule, evidence_rule: set.evidence_rule, split_rule: set.split_rule },
|
|
247
|
+
larp: { before: runLarpSubject(set, opts.larpBefore), after: runLarpSubject(set, opts.larpAfter), rubric_before: opts.larpBefore, rubric_after: opts.larpAfter, tuned: opts.tuned?.larp ?? null },
|
|
248
|
+
rug: { before: runRugSubject(set, opts.rugBefore), after: runRugSubject(set, opts.rugAfter), rules_before: opts.rugBefore, rules_after: opts.rugAfter, tuned: opts.tuned?.rug ?? null },
|
|
249
|
+
not_measured: [
|
|
250
|
+
'larp-check model stage (fetch_page / check_domain / check_github and the model\'s judgement): pages from the past cannot be re-fetched as they were; measured forward only via `dexterm backtest --subject larp` once ≥ 50 recorded verdicts match an outcome',
|
|
251
|
+
'rug-check model output (rug_probability, bias): measured forward via `--subject pack_verdicts` from recorded verdicts',
|
|
252
|
+
'trench-scan: no per-token labelled outcome',
|
|
253
|
+
],
|
|
254
|
+
method: [
|
|
255
|
+
`eval set: ${set.counts.rows} coins from this machine's recorder (${set.counts.train} train / ${set.counts.test} test), one row per coin`,
|
|
256
|
+
`labels: ${set.label_rule}`,
|
|
257
|
+
`evidence: ${set.evidence_rule}`,
|
|
258
|
+
`split: ${set.split_rule}`,
|
|
259
|
+
'tuning: grid search on the train split only (the code refuses test rows); objective MCC of warning grade → bad; ties go to the simpler setting',
|
|
260
|
+
`rates: 95% Wilson intervals; a grade with fewer than ${SKILL_MIN_N} rows prints counts only`,
|
|
261
|
+
'meaning: agreement between a grade and a bad price outcome; a risk flag, not a certificate, and not a measure of whether a product is real',
|
|
262
|
+
],
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
const pct = (r) => (r === null ? '—' : `${(r.value * 100).toFixed(1)}% [${(r.lo * 100).toFixed(0)}–${(r.hi * 100).toFixed(0)}]`);
|
|
266
|
+
function splitLines(name, m) {
|
|
267
|
+
const L = [` ${name}: n=${m.n} bad=${m.bad} base rate ${pct(m.base_rate)} MCC ${m.binary.mcc === null ? '—' : m.binary.mcc.toFixed(3)} (tp ${m.binary.tp} fp ${m.binary.fp} fn ${m.binary.fn} tn ${m.binary.tn})`];
|
|
268
|
+
for (const g of m.by_grade) {
|
|
269
|
+
L.push(` ${g.grade.padEnd(17)} n=${String(g.n).padEnd(4)} bad=${String(g.bad).padEnd(4)} P(bad)=${pct(g.bad_rate).padEnd(18)} ${g.claims ? `claims ${g.claims.padEnd(3)} precision=${pct(g.precision).padEnd(18)} recall=${pct(g.recall)}` : 'claims nothing'}`);
|
|
270
|
+
}
|
|
271
|
+
return L;
|
|
272
|
+
}
|
|
273
|
+
export function renderSkillBacktestText(r) {
|
|
274
|
+
const L = [];
|
|
275
|
+
L.push(`SKILL BACKTEST horizon=${r.eval.horizon_sec}s eval built ${r.eval.built_at} rows ${r.eval.counts.rows} (train ${r.eval.counts.train} · test ${r.eval.counts.test})`);
|
|
276
|
+
L.push('');
|
|
277
|
+
L.push(`larp-check · links-only baseline before=${r.larp.rubric_before.id} after=${r.larp.rubric_after.id}${r.larp.tuned ? ` (tuned: train MCC ${r.larp.tuned.train_mcc?.toFixed(3) ?? '—'}, ${r.larp.tuned.candidates} candidates)` : ''}`);
|
|
278
|
+
L.push(' before');
|
|
279
|
+
L.push(...splitLines('train', r.larp.before.train));
|
|
280
|
+
L.push(...splitLines('TEST ', r.larp.before.test));
|
|
281
|
+
L.push(' after');
|
|
282
|
+
L.push(...splitLines('train', r.larp.after.train));
|
|
283
|
+
L.push(...splitLines('TEST ', r.larp.after.test));
|
|
284
|
+
L.push('');
|
|
285
|
+
L.push(`rug-check · verdict rules before=${r.rug.rules_before.id} after=${r.rug.rules_after.id}${r.rug.tuned ? ` (tuned: train MCC ${r.rug.tuned.train_mcc?.toFixed(3) ?? '—'}, ${r.rug.tuned.candidates} candidates)` : ''}`);
|
|
286
|
+
L.push(' before');
|
|
287
|
+
L.push(...splitLines('train', r.rug.before.train));
|
|
288
|
+
L.push(...splitLines('TEST ', r.rug.before.test));
|
|
289
|
+
L.push(' after');
|
|
290
|
+
L.push(...splitLines('train', r.rug.after.train));
|
|
291
|
+
L.push(...splitLines('TEST ', r.rug.after.test));
|
|
292
|
+
L.push('');
|
|
293
|
+
L.push('not measured:');
|
|
294
|
+
for (const m of r.not_measured)
|
|
295
|
+
L.push(` - ${m}`);
|
|
296
|
+
L.push('method:');
|
|
297
|
+
for (const m of r.method)
|
|
298
|
+
L.push(` - ${m}`);
|
|
299
|
+
return L.join('\n') + '\n';
|
|
300
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
// Skill care (task-1414): the pure decision logic behind the recurring skill backtest that research/skill-care/
|
|
2
|
+
// run.mjs schedules. It never ships anything — it decides whether the newly labelled recorder history is enough,
|
|
3
|
+
// and improves the shipped skills enough, to be worth PROPOSING a tuning (a Kanban card + a parameter diff), or
|
|
4
|
+
// whether to record "no change" and stay silent. Pure over the eval sets + metrics it is given, so the gate is
|
|
5
|
+
// unit-testable with fixtures.
|
|
6
|
+
//
|
|
7
|
+
// Two eval slices matter:
|
|
8
|
+
// - ROLLING: the full current eval set, re-split so the NEWEST `windowDays` of coins are a held-out TEST
|
|
9
|
+
// window (never used to tune; the tuning functions already refuse test rows). Rates drift with the market,
|
|
10
|
+
// so a fresh tune has to beat the shipped rules HERE, beyond Wilson-interval noise.
|
|
11
|
+
// - FROZEN CANARY: the committed task-1401 eval set and its published numbers. A proposed tuning must also
|
|
12
|
+
// hold on the frozen canary (an overfitting guard); and if the SHIPPED rules stop reproducing the frozen
|
|
13
|
+
// numbers (code/data drift), that itself is worth a proposal.
|
|
14
|
+
import { gradeLinks } from '../../data/src/index.js';
|
|
15
|
+
import {} from "./larp-eval.js";
|
|
16
|
+
import { gradeRugRules, splitMetrics, LARP_GRADE_CLAIMS, LARP_GRADE_ORDER, RUG_GRADE_CLAIMS, RUG_GRADE_ORDER, } from "./skill-backtest.js";
|
|
17
|
+
export const SKILL_CARE_SCHEMA = 'dexterm/packs/skill-care/1';
|
|
18
|
+
/** Minimum newly labelled coins since the last accepted tuning before an improvement may be proposed. */
|
|
19
|
+
export const DEFAULT_MIN_NEW_COINS = 150;
|
|
20
|
+
/** Tolerance (absolute MCC) for calling the frozen canary "regressed" or "held". */
|
|
21
|
+
export const DEFAULT_CANARY_NOISE = 0.05;
|
|
22
|
+
/**
|
|
23
|
+
* Re-split an eval set so the NEWEST coins are a held-out TEST window the tuning never sees. By default (and
|
|
24
|
+
* whenever `windowDays` is not given) the newest `testFrac` of coins by t0 are TEST (count-based, so TRAIN stays
|
|
25
|
+
* the majority even when t0 is clustered); with `windowDays` it is a time window instead. Returns a new set
|
|
26
|
+
* (rows cloned with updated `split`); the original is untouched.
|
|
27
|
+
*/
|
|
28
|
+
export function rollingSplit(set, opts = {}) {
|
|
29
|
+
let cutoff;
|
|
30
|
+
let rule;
|
|
31
|
+
if (opts.windowDays !== undefined) {
|
|
32
|
+
const anchor = opts.anchor ?? set.rows.reduce((m, r) => Math.max(m, r.t0), 0);
|
|
33
|
+
cutoff = anchor - opts.windowDays * 86_400_000;
|
|
34
|
+
rule = `rolling window: coins with t0 in the newest ${opts.windowDays} day(s) (t0 ≥ ${new Date(cutoff).toISOString()}) are the held-out TEST window; never tuned on`;
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
const frac = opts.testFrac ?? 0.3;
|
|
38
|
+
const sorted = [...set.rows].sort((a, b) => a.t0 - b.t0);
|
|
39
|
+
const idx = Math.min(sorted.length, Math.floor(sorted.length * (1 - frac)));
|
|
40
|
+
cutoff = sorted[idx]?.t0 ?? Number.MAX_SAFE_INTEGER;
|
|
41
|
+
rule = `rolling window: the newest ${Math.round(frac * 100)}% of coins by t0 (t0 ≥ ${new Date(cutoff).toISOString()}) are the held-out TEST window; never tuned on`;
|
|
42
|
+
}
|
|
43
|
+
const rows = set.rows.map((r) => ({ ...r, split: r.t0 >= cutoff ? 'test' : 'train' }));
|
|
44
|
+
const train = rows.filter((r) => r.split === 'train');
|
|
45
|
+
const test = rows.filter((r) => r.split === 'test');
|
|
46
|
+
return {
|
|
47
|
+
...set,
|
|
48
|
+
split_rule: rule,
|
|
49
|
+
cutoff_t0: cutoff,
|
|
50
|
+
counts: { ...set.counts, rows: rows.length, train: train.length, test: test.length, bad_train: train.filter((r) => r.bad).length, bad_test: test.filter((r) => r.bad).length },
|
|
51
|
+
rows,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
// ---------------------------------------------------------------- new coins since the last tuning
|
|
55
|
+
/** Coins in `current` whose mint is not in `baselineMints` — the newly labelled coins since that baseline. */
|
|
56
|
+
export function newCoinsSince(current, baselineMints) {
|
|
57
|
+
const newMints = current.rows.filter((r) => !baselineMints.has(r.mint)).map((r) => r.mint);
|
|
58
|
+
return { newMints, count: newMints.length };
|
|
59
|
+
}
|
|
60
|
+
// ---------------------------------------------------------------- metrics helpers (one split, one subject)
|
|
61
|
+
/** Binary warning→bad metrics for the link rubric on one set of rows. */
|
|
62
|
+
export function larpSplit(rows, rubric) {
|
|
63
|
+
return splitMetrics(rows.map((r) => ({ grade: gradeLinks(r.link, rubric).grade, bad: r.bad })), LARP_GRADE_CLAIMS, LARP_GRADE_ORDER);
|
|
64
|
+
}
|
|
65
|
+
/** Binary warning→bad metrics for the rug rules on one set of rows. */
|
|
66
|
+
export function rugSplit(rows, rules) {
|
|
67
|
+
return splitMetrics(rows.map((r) => ({ grade: gradeRugRules(r.rug, rules).grade, bad: r.bad })), RUG_GRADE_CLAIMS, RUG_GRADE_ORDER);
|
|
68
|
+
}
|
|
69
|
+
/** The MCC of "warning grade → bad" on the TEST rows of `set` for each subject (null below the metric's floor). */
|
|
70
|
+
export function canaryMcc(set, params) {
|
|
71
|
+
const test = set.rows.filter((r) => r.split === 'test');
|
|
72
|
+
return { larpMcc: larpSplit(test, params.larp).binary.mcc, rugMcc: rugSplit(test, params.rug).binary.mcc };
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Does `after` beat `before` on a held-out split BEYOND interval noise? The test is on the binary warning→bad
|
|
76
|
+
* classifier: the after precision's Wilson lower bound must clear the before precision's upper bound (a gain
|
|
77
|
+
* that the intervals do not overlap on), with no interval-exceeding recall loss and a positive MCC delta. When
|
|
78
|
+
* a precision interval is unavailable (too few flagged rows) the answer is a conservative "no".
|
|
79
|
+
*/
|
|
80
|
+
export function intervalImproves(before, after) {
|
|
81
|
+
const bp = before.binary.precision, ap = after.binary.precision;
|
|
82
|
+
const br = before.binary.recall, ar = after.binary.recall;
|
|
83
|
+
const mccDelta = after.binary.mcc !== null && before.binary.mcc !== null ? after.binary.mcc - before.binary.mcc : null;
|
|
84
|
+
if (!ap || !bp)
|
|
85
|
+
return { improves: false, mccDelta, reason: 'precision interval unavailable (too few flagged rows on the held-out window)' };
|
|
86
|
+
const precisionBeatsNoise = ap.lo > bp.hi;
|
|
87
|
+
const recallHolds = !ar || !br ? true : ar.hi >= br.lo; // no noise-exceeding recall drop
|
|
88
|
+
const mccUp = mccDelta !== null && mccDelta > 0;
|
|
89
|
+
const improves = precisionBeatsNoise && recallHolds && mccUp;
|
|
90
|
+
const reason = improves
|
|
91
|
+
? `precision ${(ap.value * 100).toFixed(0)}% [${(ap.lo * 100).toFixed(0)}-${(ap.hi * 100).toFixed(0)}] clears before ${(bp.value * 100).toFixed(0)}% [${(bp.lo * 100).toFixed(0)}-${(bp.hi * 100).toFixed(0)}], ΔMCC ${mccDelta.toFixed(3)}`
|
|
92
|
+
: !precisionBeatsNoise ? `precision gain within noise (after [${(ap.lo * 100).toFixed(0)}-${(ap.hi * 100).toFixed(0)}] overlaps before [${(bp.lo * 100).toFixed(0)}-${(bp.hi * 100).toFixed(0)}])`
|
|
93
|
+
: !recallHolds ? 'recall dropped beyond interval noise' : `ΔMCC ${mccDelta === null ? '—' : mccDelta.toFixed(3)} not positive`;
|
|
94
|
+
return { improves, mccDelta, reason };
|
|
95
|
+
}
|
|
96
|
+
function rubricChanged(a, b) {
|
|
97
|
+
return a.threshold !== b.threshold || JSON.stringify(a.weights) !== JSON.stringify(b.weights);
|
|
98
|
+
}
|
|
99
|
+
function rugChanged(a, b) {
|
|
100
|
+
return a.dev_pct_over !== b.dev_pct_over || a.organic_below !== b.organic_below || a.top10_over !== b.top10_over || a.liq_below !== b.liq_below;
|
|
101
|
+
}
|
|
102
|
+
/** MCC `a` has not regressed below `b` by more than `noise` (nulls treated as not-worse). */
|
|
103
|
+
function holds(a, b, noise) {
|
|
104
|
+
if (a === null || b === null)
|
|
105
|
+
return true;
|
|
106
|
+
return a >= b - noise;
|
|
107
|
+
}
|
|
108
|
+
function regressed(a, b, noise) {
|
|
109
|
+
return a !== null && b !== null && a < b - noise;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Decide. Priority: a frozen-canary REGRESSION of the shipped rules (the published numbers no longer reproduce)
|
|
113
|
+
* is always worth a proposal, even with few new coins. Otherwise an IMPROVEMENT needs all of: at least N new
|
|
114
|
+
* coins, at least one subject beating the held-out window beyond noise (`intervalImproves`), and the proposed
|
|
115
|
+
* params holding on the frozen canary. Anything else is 'none' (record it, stay silent).
|
|
116
|
+
*/
|
|
117
|
+
export function proposalGate(input) {
|
|
118
|
+
const N = input.N ?? DEFAULT_MIN_NEW_COINS;
|
|
119
|
+
const noise = input.canaryNoise ?? DEFAULT_CANARY_NOISE;
|
|
120
|
+
const reasons = [];
|
|
121
|
+
const parameterDrift = { larp: rubricChanged(input.proposed.larp, input.shipped.larp), rug: rugChanged(input.proposed.rug, input.shipped.rug) };
|
|
122
|
+
// (0) Canary regression of the SHIPPED rules — a canary, not an improvement.
|
|
123
|
+
const larpReg = regressed(input.canaryShipped.larpMcc, input.canaryBaseline.larpMcc, noise);
|
|
124
|
+
const rugReg = regressed(input.canaryShipped.rugMcc, input.canaryBaseline.rugMcc, noise);
|
|
125
|
+
if (larpReg || rugReg) {
|
|
126
|
+
if (larpReg)
|
|
127
|
+
reasons.push(`frozen canary regressed: shipped larp MCC ${fmt(input.canaryShipped.larpMcc)} < baseline ${fmt(input.canaryBaseline.larpMcc)} − ${noise}`);
|
|
128
|
+
if (rugReg)
|
|
129
|
+
reasons.push(`frozen canary regressed: shipped rug MCC ${fmt(input.canaryShipped.rugMcc)} < baseline ${fmt(input.canaryBaseline.rugMcc)} − ${noise}`);
|
|
130
|
+
return { propose: true, kind: 'canary-regression', reasons, improved: [], canaryHolds: false, parameterDrift };
|
|
131
|
+
}
|
|
132
|
+
// (1) Improvement path.
|
|
133
|
+
const larpI = intervalImproves(input.larp.before, input.larp.after);
|
|
134
|
+
const rugI = intervalImproves(input.rug.before, input.rug.after);
|
|
135
|
+
const improved = [];
|
|
136
|
+
if (larpI.improves)
|
|
137
|
+
improved.push('larp');
|
|
138
|
+
if (rugI.improves)
|
|
139
|
+
improved.push('rug');
|
|
140
|
+
const canaryHolds = holds(input.canaryProposed.larpMcc, input.canaryBaseline.larpMcc, noise) && holds(input.canaryProposed.rugMcc, input.canaryBaseline.rugMcc, noise);
|
|
141
|
+
if (input.newCoins < N)
|
|
142
|
+
reasons.push(`only ${input.newCoins} new labelled coins since the last accepted tuning (need ${N})`);
|
|
143
|
+
if (!improved.length)
|
|
144
|
+
reasons.push(`no subject beats the held-out window beyond noise (larp: ${larpI.reason}; rug: ${rugI.reason})`);
|
|
145
|
+
else
|
|
146
|
+
reasons.push(`held-out improvement — larp: ${larpI.improves ? larpI.reason : 'no'}; rug: ${rugI.improves ? rugI.reason : 'no'}`);
|
|
147
|
+
if (improved.length && !canaryHolds)
|
|
148
|
+
reasons.push('proposed params would regress the frozen canary (overfit guard) — not proposing');
|
|
149
|
+
const propose = input.newCoins >= N && improved.length > 0 && canaryHolds;
|
|
150
|
+
return { propose, kind: propose ? 'improvement' : 'none', reasons, improved, canaryHolds, parameterDrift };
|
|
151
|
+
}
|
|
152
|
+
const fmt = (x) => (x === null ? '—' : x.toFixed(3));
|
|
153
|
+
// ---------------------------------------------------------------- the one-line trend
|
|
154
|
+
const pr = (r) => (r === null ? '—' : `${(r.value * 100).toFixed(0)}%[${(r.lo * 100).toFixed(0)}-${(r.hi * 100).toFixed(0)}]`);
|
|
155
|
+
/** One subject's compact per-grade precision/recall with Wilson intervals, for the trend line. */
|
|
156
|
+
function subjectTrend(label, m) {
|
|
157
|
+
const grades = m.by_grade.filter((g) => g.claims).map((g) => `${g.grade}=${pr(g.precision)}`).join(' ');
|
|
158
|
+
return `${label} MCC ${m.binary.mcc === null ? '—' : m.binary.mcc.toFixed(3)} ${grades}`;
|
|
159
|
+
}
|
|
160
|
+
/** The dated one-line trend record (MCC + per-grade precision Wilson intervals + n + the decision). */
|
|
161
|
+
export function trendLine(t) {
|
|
162
|
+
return `${t.date} test n=${t.testN} bad=${t.badTest} · train n=${t.trainN} · new=${t.newCoins} · ${subjectTrend('rug', t.rugTest)} · ${subjectTrend('larp', t.larpTest)} · decision=${t.decision}`;
|
|
163
|
+
}
|
|
164
|
+
const cv = (v) => (v === null || v === undefined ? 'off' : String(v));
|
|
165
|
+
const cm = (m) => (m === null || m === undefined ? '—' : m.toFixed(3));
|
|
166
|
+
/**
|
|
167
|
+
* The Kanban card a proposal creates (priority normal, owner `i.owner`, default 'maintainers': the maintainers'
|
|
168
|
+
* tooling names the lane). It carries the numbers and the exact tunable-grid parameter change; it NEVER edits app
|
|
169
|
+
* main and NEVER ships to users — a maintainer opens a scratch branch, applies the diff, it is verified, and the
|
|
170
|
+
* skills reach users only in a normal release.
|
|
171
|
+
*/
|
|
172
|
+
export function proposalCard(i) {
|
|
173
|
+
const d = [
|
|
174
|
+
`Skill care proposes a ${i.kind} tuning for the dexterm analyst skills (task-1414). It never edits app main; a maintainer creates a scratch branch and applies the parameter diff below, it is verified, and it reaches users only in a normal release.`,
|
|
175
|
+
'',
|
|
176
|
+
`New labelled coins since the last accepted tuning: ${i.newCoins} (threshold ${i.minNew}).`,
|
|
177
|
+
`Rolling held-out window: ${i.splitRule}`,
|
|
178
|
+
`Gate: ${i.reasons.join('; ')}`,
|
|
179
|
+
'',
|
|
180
|
+
'Proposed parameters (the existing tunable grid only; keep the AVOID hard rules):',
|
|
181
|
+
` rug-check (packages/packs/src/skill-backtest.ts RUG_RULES_V1): dev_pct_over=${cv(i.proposed.rug.dev_pct_over)} organic_below=${cv(i.proposed.rug.organic_below)} top10_over=${cv(i.proposed.rug.top10_over)} liq_below=${cv(i.proposed.rug.liq_below)}`,
|
|
182
|
+
` shipped: dev_pct_over=${cv(i.shipped.rug.dev_pct_over)} organic_below=${cv(i.shipped.rug.organic_below)} top10_over=${cv(i.shipped.rug.top10_over)} liq_below=${cv(i.shipped.rug.liq_below)}`,
|
|
183
|
+
` larp-check (packages/data/src/larp/link-baseline.ts LINK_RUBRIC_V1): threshold=${i.proposed.larp.threshold} weights=${JSON.stringify(i.proposed.larp.weights)}`,
|
|
184
|
+
` shipped: threshold=${i.shipped.larp.threshold} weights=${JSON.stringify(i.shipped.larp.weights)}`,
|
|
185
|
+
'',
|
|
186
|
+
`Held-out MCC — rug ${cm(i.heldOut.rug.before)} → ${cm(i.heldOut.rug.after)}; larp ${cm(i.heldOut.larp.before)} → ${cm(i.heldOut.larp.after)}.`,
|
|
187
|
+
`Frozen canary MCC (must not regress) — baseline rug ${cm(i.canary.baseline.rugMcc)} / larp ${cm(i.canary.baseline.larpMcc)}; proposed rug ${cm(i.canary.proposed.rugMcc)} / larp ${cm(i.canary.proposed.larpMcc)}.`,
|
|
188
|
+
'',
|
|
189
|
+
'Also update the skill prose that names these numbers (rug-check/larp-check SKILL.md, token-dossier.md, packages/packs/eval/README.md) and the site docs/larp.md in the release lane — published numbers change only via a release and must keep the weak results. Do NOT auto-ship.',
|
|
190
|
+
].join('\n');
|
|
191
|
+
return {
|
|
192
|
+
title: `dexterm skill-care: proposed ${i.kind} tuning (${i.date.slice(0, 10)})`,
|
|
193
|
+
description: d,
|
|
194
|
+
project: 'dexterm', priority: 'normal', owner: i.owner ?? 'maintainers', delegationTarget: i.owner ?? 'maintainers', actor: 'skill-care',
|
|
195
|
+
constraints: [
|
|
196
|
+
'Scratch branch off app main; never commit to main; ship to users only via a release after verification.',
|
|
197
|
+
'Change the tunable grid only (RUG_RULES_V1 / LINK_RUBRIC_V1); keep the AVOID hard rules.',
|
|
198
|
+
'Keep the weak results in the published numbers (site docs/larp.md).',
|
|
199
|
+
],
|
|
200
|
+
acceptanceCriteria: [
|
|
201
|
+
'Parameter diff applied on a scratch branch with the before/after numbers in the handoff',
|
|
202
|
+
'Frozen canary does not regress; one npm test green',
|
|
203
|
+
'Site docs/larp.md + skill prose updated in the release lane',
|
|
204
|
+
],
|
|
205
|
+
};
|
|
206
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/* dexterm popup — spectator page styles (task-1406, "watch the agent cook"). The palette comes from the
|
|
2
|
+
theme CSS vars common.js sets (--text, --muted, --accent, --up, --down, --warn, --pump, --ai …). */
|
|
3
|
+
.body { overflow-y: auto; padding: 6px 8px; }
|
|
4
|
+
|
|
5
|
+
.spec-screen { font: 12px ui-monospace, Menlo, monospace; color: var(--text); margin-bottom: 8px; padding: 4px 6px; border: 1px solid var(--border); border-radius: 6px; background: color-mix(in srgb, var(--panel) 85%, transparent); }
|
|
6
|
+
.spec-screen-k { color: var(--accent); font-weight: 600; }
|
|
7
|
+
.spec-screen-dim { color: var(--dim); }
|
|
8
|
+
.spec-takeover { color: var(--warn); font-weight: 600; }
|
|
9
|
+
|
|
10
|
+
.spec-agents { display: flex; flex-direction: column; gap: 6px; }
|
|
11
|
+
.spec-agent { display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px; border: 1px solid var(--border); border-radius: 6px; padding: 6px 8px; font: 12px ui-monospace, Menlo, monospace; background: color-mix(in srgb, var(--panel) 85%, transparent); }
|
|
12
|
+
.spec-agent.off { opacity: 0.6; border-color: var(--warn); }
|
|
13
|
+
.spec-badge { font-size: 9px; letter-spacing: 0.06em; text-transform: uppercase; padding: 1px 5px; border-radius: 4px; background: var(--ai, var(--accent)); color: #000; font-weight: 700; }
|
|
14
|
+
.spec-badge.sm { font-size: 8px; padding: 0 4px; }
|
|
15
|
+
.spec-name { color: var(--text); font-weight: 600; }
|
|
16
|
+
.spec-tier { font-size: 10px; padding: 1px 5px; border-radius: 999px; border: 1px solid var(--border); color: var(--muted); }
|
|
17
|
+
.tier-observe { color: var(--muted); }
|
|
18
|
+
.tier-annotate { color: var(--accent); border-color: var(--accent); }
|
|
19
|
+
.tier-propose { color: var(--pump); border-color: var(--pump); }
|
|
20
|
+
.spec-status { font-size: 10px; padding: 1px 5px; border-radius: 4px; border: 1px solid var(--border); color: var(--muted); }
|
|
21
|
+
.status-active { color: var(--up); border-color: color-mix(in srgb, var(--up) 50%, var(--border)); }
|
|
22
|
+
.status-paused { color: var(--warn); border-color: color-mix(in srgb, var(--warn) 50%, var(--border)); }
|
|
23
|
+
.status-killed { color: var(--down); border-color: color-mix(in srgb, var(--down) 50%, var(--border)); }
|
|
24
|
+
.spec-dim { color: var(--dim); }
|
|
25
|
+
|
|
26
|
+
.spec-ctl { display: flex; gap: 6px; margin-left: auto; }
|
|
27
|
+
.spec-btn { font: 11px ui-monospace, Menlo, monospace; padding: 2px 10px; border-radius: 4px; border: 1px solid var(--border); background: transparent; color: var(--text); cursor: pointer; }
|
|
28
|
+
.spec-btn:hover { border-color: var(--borderFocus); }
|
|
29
|
+
.spec-btn.danger { color: var(--down); border-color: color-mix(in srgb, var(--down) 50%, var(--border)); }
|
|
30
|
+
.spec-btn.danger:hover { border-color: var(--down); }
|
|
31
|
+
|
|
32
|
+
.spec-activity-head { margin: 10px 0 4px; font: 11px ui-monospace, Menlo, monospace; color: var(--dim); text-transform: uppercase; letter-spacing: 0.08em; }
|
|
33
|
+
.spec-activity { display: flex; flex-direction: column; gap: 1px; font: 11px ui-monospace, Menlo, monospace; }
|
|
34
|
+
.spec-act { display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px; color: var(--muted); }
|
|
35
|
+
.spec-act.bad { color: var(--down); }
|
|
36
|
+
.spec-act.nav .spec-act-phase { color: var(--ai, var(--accent)); font-weight: 600; }
|
|
37
|
+
.spec-act-t { color: var(--dim); min-width: 28px; }
|
|
38
|
+
.spec-act-agent { color: var(--text); }
|
|
39
|
+
.spec-act-phase { color: var(--accent); }
|
|
40
|
+
.spec-act-tool { color: var(--muted); }
|
|
41
|
+
.spec-act-sum { color: var(--text); overflow: hidden; text-overflow: ellipsis; }
|
|
42
|
+
.spec-act-ms { color: var(--dim); }
|
|
43
|
+
.spec-act-age { color: var(--warn); }
|
|
44
|
+
|
|
45
|
+
.spec-foot-redact { color: var(--up); font-weight: 600; }
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="dexterm-ws" content="__WS_TOKEN__">
|
|
6
|
+
<meta name="dexterm-popup" content="__POPUP_ID__">
|
|
7
|
+
<meta name="dexterm-port" content="__PORT__">
|
|
8
|
+
<title>dexterm · watch the agent cook</title>
|
|
9
|
+
<style nonce="__NONCE__">/*__CSS__*/</style>
|
|
10
|
+
</head>
|
|
11
|
+
<body>
|
|
12
|
+
<div id="hud" class="hud">
|
|
13
|
+
<div id="drag" class="drag"><span class="mark">✦</span><span id="title" class="title">dexterm · WATCH THE AGENT COOK</span><span id="status" class="status">connecting…</span><button id="close" class="close" type="button" aria-label="close">✕</button></div>
|
|
14
|
+
<div class="stage"><canvas id="rain"></canvas><div class="body">
|
|
15
|
+
<div id="screen" class="spec-screen"></div>
|
|
16
|
+
<div id="agents" class="spec-agents"></div>
|
|
17
|
+
<div class="spec-activity-head">agent activity</div>
|
|
18
|
+
<div id="activity" class="spec-activity"></div>
|
|
19
|
+
</div></div>
|
|
20
|
+
<div id="foot" class="foot"><span id="redact" class="spec-foot-redact">redacted</span><span>read-only · pause / take over / kill</span><span id="fps" class="fps">— fps</span></div>
|
|
21
|
+
</div>
|
|
22
|
+
<script nonce="__NONCE__">/*__COMMON_JS__*/</script>
|
|
23
|
+
<script nonce="__NONCE__">/*__JS__*/</script>
|
|
24
|
+
</body>
|
|
25
|
+
</html>
|