dexterm-cli 0.1.1 → 0.1.2
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 +20 -5
- package/dist/packages/agents/src/executor.js +25 -0
- package/dist/packages/agents/src/gate.js +3 -0
- package/dist/packages/agents/src/identity.js +41 -2
- package/dist/packages/agents/src/tiers.js +5 -0
- package/dist/packages/agents/src/tools.js +3 -0
- 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 +36 -3
- 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 +41 -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/backtest-cmd.js +62 -3
- package/dist/packages/cli/src/coin-target.js +2 -0
- package/dist/packages/cli/src/doctor.js +20 -6
- package/dist/packages/cli/src/export.js +235 -0
- package/dist/packages/cli/src/live/index.js +2 -0
- package/dist/packages/cli/src/main.js +243 -41
- package/dist/packages/cli/src/mcp-cmd.js +326 -0
- package/dist/packages/cli/src/rules.js +21 -2
- package/dist/packages/cli/src/skill-eval-build.js +109 -0
- 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 +119 -0
- package/dist/packages/cli/src/watch-cmd.js +190 -0
- package/dist/packages/custom/src/rules.js +1 -1
- package/dist/packages/custom/src/schema.js +24 -1
- package/dist/packages/data/src/coin-target.js +82 -0
- package/dist/packages/data/src/index.js +3 -0
- package/dist/packages/data/src/intel/helius-history.js +12 -1
- 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/mcp/src/server.js +89 -2
- package/dist/packages/packs/src/index.js +1 -0
- package/dist/packages/packs/src/skill-backtest.js +300 -0
- package/dist/packages/tui/src/index.js +3 -0
- package/dist/packages/tui/src/keymap.js +2 -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 +15 -3
- package/dist/packages/voice/src/doctor.js +75 -7
- package/dist/packages/voice/src/download.js +184 -0
- package/dist/packages/voice/src/engines.js +48 -8
- package/dist/packages/voice/src/index.js +7 -3
- 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.js +206 -0
- package/dist/packages/voice/src/tts/say.js +71 -35
- package/dist/packages/voice/src/tts/voices.js +206 -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
|
+
}
|
|
@@ -13,3 +13,6 @@ export * from "./sound.js";
|
|
|
13
13
|
export * from "./bubbles.js";
|
|
14
14
|
export * from "./chrome.js";
|
|
15
15
|
export * from "./images/index.js";
|
|
16
|
+
// task-1401: rasterize a canvas to RGBA for the export/share PNG (bitmap font + procedural box/blocks/braille).
|
|
17
|
+
export * from "./raster.js";
|
|
18
|
+
export { glyphBits, fontChars, FONT_W, FONT_H } from "./raster-font.js";
|
|
@@ -215,6 +215,8 @@ export const DEFAULT_KEYMAP = new Keymap([
|
|
|
215
215
|
{ keys: ['p'], action: 'pause.toggle', label: 'pause' },
|
|
216
216
|
// task-1366: `$` (unbound on every screen) toggles price ⇄ market cap on every screen + both charts; persisted.
|
|
217
217
|
{ keys: ['$'], action: 'display.priceMode', label: 'price ⇄ mcap' },
|
|
218
|
+
// task-1401: `E` exports the current view (PNG + text report, local only). Free on every screen.
|
|
219
|
+
{ keys: ['E'], action: 'export.view', label: 'export PNG + report' },
|
|
218
220
|
{ keys: ['/'], action: 'filter.edit', label: 'filter', screen: 'screener' },
|
|
219
221
|
{ keys: ['\\'], action: 'filter.clear', label: 'clear filter', screen: 'screener' },
|
|
220
222
|
{ keys: ['s'], action: 'sort.menu', label: 'sort', screen: 'screener' },
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
// @dexterm/tui — a 5×9 bitmap font for the PNG exporter (task-1401). Drawn for dexterm (no third-party font data):
|
|
2
|
+
// rows 0–6 are the cap height, rows 7–8 the descenders. Box drawing, block elements and braille are not here:
|
|
3
|
+
// raster.ts draws those procedurally so they join up across cells exactly like a terminal does.
|
|
4
|
+
const G = {
|
|
5
|
+
' ': [],
|
|
6
|
+
'!': ['..#..', '..#..', '..#..', '..#..', '..#..', '.....', '..#..'],
|
|
7
|
+
'"': ['.#.#.', '.#.#.', '.#.#.'],
|
|
8
|
+
'#': ['.#.#.', '.#.#.', '#####', '.#.#.', '#####', '.#.#.', '.#.#.'],
|
|
9
|
+
'$': ['..#..', '.####', '#.#..', '.###.', '..#.#', '####.', '..#..'],
|
|
10
|
+
'%': ['##...', '##..#', '...#.', '..#..', '.#...', '#..##', '...##'],
|
|
11
|
+
'&': ['.##..', '#..#.', '#.#..', '.#...', '#.#.#', '#..#.', '.##.#'],
|
|
12
|
+
"'": ['..#..', '..#..', '.#...'],
|
|
13
|
+
'(': ['...#.', '..#..', '.#...', '.#...', '.#...', '..#..', '...#.'],
|
|
14
|
+
')': ['.#...', '..#..', '...#.', '...#.', '...#.', '..#..', '.#...'],
|
|
15
|
+
'*': ['.....', '..#..', '#.#.#', '.###.', '#.#.#', '..#..', '.....'],
|
|
16
|
+
'+': ['.....', '..#..', '..#..', '#####', '..#..', '..#..', '.....'],
|
|
17
|
+
',': ['.....', '.....', '.....', '.....', '.....', '..#..', '..#..', '.#...'],
|
|
18
|
+
'-': ['.....', '.....', '.....', '#####', '.....', '.....', '.....'],
|
|
19
|
+
'.': ['.....', '.....', '.....', '.....', '.....', '.##..', '.##..'],
|
|
20
|
+
'/': ['.....', '....#', '...#.', '..#..', '.#...', '#....', '.....'],
|
|
21
|
+
'0': ['.###.', '#...#', '#..##', '#.#.#', '##..#', '#...#', '.###.'],
|
|
22
|
+
'1': ['..#..', '.##..', '..#..', '..#..', '..#..', '..#..', '.###.'],
|
|
23
|
+
'2': ['.###.', '#...#', '....#', '...#.', '..#..', '.#...', '#####'],
|
|
24
|
+
'3': ['#####', '...#.', '..#..', '...#.', '....#', '#...#', '.###.'],
|
|
25
|
+
'4': ['...#.', '..##.', '.#.#.', '#..#.', '#####', '...#.', '...#.'],
|
|
26
|
+
'5': ['#####', '#....', '####.', '....#', '....#', '#...#', '.###.'],
|
|
27
|
+
'6': ['..##.', '.#...', '#....', '####.', '#...#', '#...#', '.###.'],
|
|
28
|
+
'7': ['#####', '....#', '...#.', '..#..', '.#...', '.#...', '.#...'],
|
|
29
|
+
'8': ['.###.', '#...#', '#...#', '.###.', '#...#', '#...#', '.###.'],
|
|
30
|
+
'9': ['.###.', '#...#', '#...#', '.####', '....#', '...#.', '.##..'],
|
|
31
|
+
':': ['.....', '.##..', '.##..', '.....', '.##..', '.##..', '.....'],
|
|
32
|
+
';': ['.....', '.##..', '.##..', '.....', '.##..', '.##..', '.#...'],
|
|
33
|
+
'<': ['...#.', '..#..', '.#...', '#....', '.#...', '..#..', '...#.'],
|
|
34
|
+
'=': ['.....', '.....', '#####', '.....', '#####', '.....', '.....'],
|
|
35
|
+
'>': ['.#...', '..#..', '...#.', '....#', '...#.', '..#..', '.#...'],
|
|
36
|
+
'?': ['.###.', '#...#', '....#', '...#.', '..#..', '.....', '..#..'],
|
|
37
|
+
'@': ['.###.', '#...#', '....#', '.##.#', '#.#.#', '#.#.#', '.###.'],
|
|
38
|
+
'A': ['.###.', '#...#', '#...#', '#####', '#...#', '#...#', '#...#'],
|
|
39
|
+
'B': ['####.', '#...#', '#...#', '####.', '#...#', '#...#', '####.'],
|
|
40
|
+
'C': ['.###.', '#...#', '#....', '#....', '#....', '#...#', '.###.'],
|
|
41
|
+
'D': ['###..', '#..#.', '#...#', '#...#', '#...#', '#..#.', '###..'],
|
|
42
|
+
'E': ['#####', '#....', '#....', '####.', '#....', '#....', '#####'],
|
|
43
|
+
'F': ['#####', '#....', '#....', '####.', '#....', '#....', '#....'],
|
|
44
|
+
'G': ['.###.', '#...#', '#....', '#.###', '#...#', '#...#', '.####'],
|
|
45
|
+
'H': ['#...#', '#...#', '#...#', '#####', '#...#', '#...#', '#...#'],
|
|
46
|
+
'I': ['.###.', '..#..', '..#..', '..#..', '..#..', '..#..', '.###.'],
|
|
47
|
+
'J': ['..###', '...#.', '...#.', '...#.', '...#.', '#..#.', '.##..'],
|
|
48
|
+
'K': ['#...#', '#..#.', '#.#..', '##...', '#.#..', '#..#.', '#...#'],
|
|
49
|
+
'L': ['#....', '#....', '#....', '#....', '#....', '#....', '#####'],
|
|
50
|
+
'M': ['#...#', '##.##', '#.#.#', '#.#.#', '#...#', '#...#', '#...#'],
|
|
51
|
+
'N': ['#...#', '#...#', '##..#', '#.#.#', '#..##', '#...#', '#...#'],
|
|
52
|
+
'O': ['.###.', '#...#', '#...#', '#...#', '#...#', '#...#', '.###.'],
|
|
53
|
+
'P': ['####.', '#...#', '#...#', '####.', '#....', '#....', '#....'],
|
|
54
|
+
'Q': ['.###.', '#...#', '#...#', '#...#', '#.#.#', '#..#.', '.##.#'],
|
|
55
|
+
'R': ['####.', '#...#', '#...#', '####.', '#.#..', '#..#.', '#...#'],
|
|
56
|
+
'S': ['.####', '#....', '#....', '.###.', '....#', '....#', '####.'],
|
|
57
|
+
'T': ['#####', '..#..', '..#..', '..#..', '..#..', '..#..', '..#..'],
|
|
58
|
+
'U': ['#...#', '#...#', '#...#', '#...#', '#...#', '#...#', '.###.'],
|
|
59
|
+
'V': ['#...#', '#...#', '#...#', '#...#', '#...#', '.#.#.', '..#..'],
|
|
60
|
+
'W': ['#...#', '#...#', '#...#', '#.#.#', '#.#.#', '#.#.#', '.#.#.'],
|
|
61
|
+
'X': ['#...#', '#...#', '.#.#.', '..#..', '.#.#.', '#...#', '#...#'],
|
|
62
|
+
'Y': ['#...#', '#...#', '.#.#.', '..#..', '..#..', '..#..', '..#..'],
|
|
63
|
+
'Z': ['#####', '....#', '...#.', '..#..', '.#...', '#....', '#####'],
|
|
64
|
+
'[': ['.###.', '.#...', '.#...', '.#...', '.#...', '.#...', '.###.'],
|
|
65
|
+
'\\': ['.....', '#....', '.#...', '..#..', '...#.', '....#', '.....'],
|
|
66
|
+
']': ['.###.', '...#.', '...#.', '...#.', '...#.', '...#.', '.###.'],
|
|
67
|
+
'^': ['..#..', '.#.#.', '#...#'],
|
|
68
|
+
'_': ['.....', '.....', '.....', '.....', '.....', '.....', '.....', '#####'],
|
|
69
|
+
'`': ['.#...', '..#..', '...#.'],
|
|
70
|
+
'a': ['.....', '.....', '.###.', '....#', '.####', '#...#', '.####'],
|
|
71
|
+
'b': ['#....', '#....', '#.##.', '##..#', '#...#', '#...#', '####.'],
|
|
72
|
+
'c': ['.....', '.....', '.###.', '#....', '#....', '#...#', '.###.'],
|
|
73
|
+
'd': ['....#', '....#', '.##.#', '#..##', '#...#', '#...#', '.####'],
|
|
74
|
+
'e': ['.....', '.....', '.###.', '#...#', '#####', '#....', '.###.'],
|
|
75
|
+
'f': ['..##.', '.#..#', '.#...', '###..', '.#...', '.#...', '.#...'],
|
|
76
|
+
'g': ['.....', '.....', '.####', '#...#', '#...#', '.####', '....#', '....#', '.###.'],
|
|
77
|
+
'h': ['#....', '#....', '#.##.', '##..#', '#...#', '#...#', '#...#'],
|
|
78
|
+
'i': ['..#..', '.....', '.##..', '..#..', '..#..', '..#..', '.###.'],
|
|
79
|
+
'j': ['...#.', '.....', '..##.', '...#.', '...#.', '...#.', '...#.', '#..#.', '.##..'],
|
|
80
|
+
'k': ['#....', '#....', '#..#.', '#.#..', '##...', '#.#..', '#..#.'],
|
|
81
|
+
'l': ['.##..', '..#..', '..#..', '..#..', '..#..', '..#..', '.###.'],
|
|
82
|
+
'm': ['.....', '.....', '##.#.', '#.#.#', '#.#.#', '#...#', '#...#'],
|
|
83
|
+
'n': ['.....', '.....', '#.##.', '##..#', '#...#', '#...#', '#...#'],
|
|
84
|
+
'o': ['.....', '.....', '.###.', '#...#', '#...#', '#...#', '.###.'],
|
|
85
|
+
'p': ['.....', '.....', '####.', '#...#', '#...#', '####.', '#....', '#....', '#....'],
|
|
86
|
+
'q': ['.....', '.....', '.####', '#...#', '#...#', '.####', '....#', '....#', '....#'],
|
|
87
|
+
'r': ['.....', '.....', '#.##.', '##..#', '#....', '#....', '#....'],
|
|
88
|
+
's': ['.....', '.....', '.####', '#....', '.###.', '....#', '####.'],
|
|
89
|
+
't': ['.#...', '.#...', '###..', '.#...', '.#...', '.#..#', '..##.'],
|
|
90
|
+
'u': ['.....', '.....', '#...#', '#...#', '#...#', '#..##', '.##.#'],
|
|
91
|
+
'v': ['.....', '.....', '#...#', '#...#', '#...#', '.#.#.', '..#..'],
|
|
92
|
+
'w': ['.....', '.....', '#...#', '#...#', '#.#.#', '#.#.#', '.#.#.'],
|
|
93
|
+
'x': ['.....', '.....', '#...#', '.#.#.', '..#..', '.#.#.', '#...#'],
|
|
94
|
+
'y': ['.....', '.....', '#...#', '#...#', '#...#', '.####', '....#', '....#', '.###.'],
|
|
95
|
+
'z': ['.....', '.....', '#####', '...#.', '..#..', '.#...', '#####'],
|
|
96
|
+
'{': ['...#.', '..#..', '..#..', '.#...', '..#..', '..#..', '...#.'],
|
|
97
|
+
'|': ['..#..', '..#..', '..#..', '..#..', '..#..', '..#..', '..#..'],
|
|
98
|
+
'}': ['.#...', '..#..', '..#..', '...#.', '..#..', '..#..', '.#...'],
|
|
99
|
+
'~': ['.....', '.....', '.#...', '#.#.#', '...#.', '.....', '.....'],
|
|
100
|
+
// symbols the screens use
|
|
101
|
+
'·': ['.....', '.....', '.....', '..#..', '.....', '.....', '.....'],
|
|
102
|
+
'•': ['.....', '.....', '.###.', '.###.', '.###.', '.....', '.....'],
|
|
103
|
+
'●': ['.....', '.###.', '#####', '#####', '#####', '.###.', '.....'],
|
|
104
|
+
'◉': ['.....', '.###.', '#...#', '#.#.#', '#...#', '.###.', '.....'],
|
|
105
|
+
'○': ['.....', '.###.', '#...#', '#...#', '#...#', '.###.', '.....'],
|
|
106
|
+
'◌': ['.....', '.#.#.', '#...#', '.....', '#...#', '.#.#.', '.....'],
|
|
107
|
+
'◐': ['.....', '.###.', '##..#', '##..#', '##..#', '.###.', '.....'],
|
|
108
|
+
'◈': ['..#..', '.#.#.', '#.#.#', '.#.#.', '..#..', '.....', '.....'],
|
|
109
|
+
'◆': ['..#..', '.###.', '#####', '.###.', '..#..', '.....', '.....'],
|
|
110
|
+
'◇': ['..#..', '.#.#.', '#...#', '.#.#.', '..#..', '.....', '.....'],
|
|
111
|
+
'■': ['.....', '#####', '#####', '#####', '#####', '#####', '.....'],
|
|
112
|
+
'□': ['.....', '#####', '#...#', '#...#', '#...#', '#####', '.....'],
|
|
113
|
+
'▪': ['.....', '.....', '.###.', '.###.', '.###.', '.....', '.....'],
|
|
114
|
+
'★': ['..#..', '..#..', '#####', '.###.', '.#.#.', '#...#', '.....'],
|
|
115
|
+
'☆': ['..#..', '..#..', '##.##', '.#.#.', '.#.#.', '#...#', '.....'],
|
|
116
|
+
'▲': ['.....', '..#..', '..#..', '.###.', '.###.', '#####', '.....'],
|
|
117
|
+
'▼': ['.....', '#####', '.###.', '.###.', '..#..', '..#..', '.....'],
|
|
118
|
+
'△': ['.....', '..#..', '..#..', '.#.#.', '.#.#.', '#####', '.....'],
|
|
119
|
+
'▽': ['.....', '#####', '.#.#.', '.#.#.', '..#..', '..#..', '.....'],
|
|
120
|
+
'▶': ['.#...', '.##..', '.###.', '.####', '.###.', '.##..', '.#...'],
|
|
121
|
+
'◀': ['...#.', '..##.', '.###.', '####.', '.###.', '..##.', '...#.'],
|
|
122
|
+
'▸': ['.....', '.#...', '.##..', '.###.', '.##..', '.#...', '.....'],
|
|
123
|
+
'◂': ['.....', '...#.', '..##.', '.###.', '..##.', '...#.', '.....'],
|
|
124
|
+
'✦': ['..#..', '..#..', '.###.', '#####', '.###.', '..#..', '..#..'],
|
|
125
|
+
'✧': ['..#..', '..#..', '.#.#.', '##.##', '.#.#.', '..#..', '..#..'],
|
|
126
|
+
'⚠': ['..#..', '.#.#.', '.#.#.', '#.#.#', '#...#', '#.#.#', '#####'],
|
|
127
|
+
'…': ['.....', '.....', '.....', '.....', '.....', '.....', '#.#.#'],
|
|
128
|
+
'→': ['.....', '..#..', '...#.', '#####', '...#.', '..#..', '.....'],
|
|
129
|
+
'←': ['.....', '..#..', '.#...', '#####', '.#...', '..#..', '.....'],
|
|
130
|
+
'↑': ['..#..', '.###.', '#.#.#', '..#..', '..#..', '..#..', '.....'],
|
|
131
|
+
'↓': ['.....', '..#..', '..#..', '..#..', '#.#.#', '.###.', '..#..'],
|
|
132
|
+
'↗': ['.....', '..###', '...##', '..#.#', '.#...', '#....', '.....'],
|
|
133
|
+
'↘': ['.....', '#....', '.#...', '..#.#', '...##', '..###', '.....'],
|
|
134
|
+
'↳': ['#....', '#....', '#..#.', '#...#', '#####', '....#', '...#.'],
|
|
135
|
+
'↪': ['.....', '.###.', '#...#', '#..#.', '#.###', '....#', '.....'],
|
|
136
|
+
'⎿': ['#....', '#....', '#....', '#....', '#####', '.....', '.....'],
|
|
137
|
+
'✔': ['.....', '....#', '...##', '#.##.', '###..', '.#...', '.....'],
|
|
138
|
+
'✖': ['.....', '##.##', '.###.', '..#..', '.###.', '##.##', '.....'],
|
|
139
|
+
'⚡': ['...##', '..##.', '.##..', '#####', '..##.', '.##..', '##...'],
|
|
140
|
+
'⇄': ['...#.', '#####', '...#.', '.#...', '#####', '.#...', '.....'],
|
|
141
|
+
'⏎': ['....#', '....#', '..#.#', '.####', '..#..', '.....', '.....'],
|
|
142
|
+
'✓': ['.....', '....#', '...#.', '#.#..', '.#...', '.....', '.....'],
|
|
143
|
+
'✗': ['.....', '#...#', '.#.#.', '..#..', '.#.#.', '#...#', '.....'],
|
|
144
|
+
'✕': ['.....', '#...#', '.#.#.', '..#..', '.#.#.', '#...#', '.....'],
|
|
145
|
+
'×': ['.....', '.....', '#...#', '.#.#.', '..#..', '.#.#.', '#...#'],
|
|
146
|
+
'—': ['.....', '.....', '.....', '#####', '.....', '.....', '.....'],
|
|
147
|
+
'–': ['.....', '.....', '.....', '.####', '.....', '.....', '.....'],
|
|
148
|
+
'≥': ['.#...', '..#..', '...#.', '..#..', '.#...', '.....', '#####'],
|
|
149
|
+
'≤': ['...#.', '..#..', '.#...', '..#..', '...#.', '.....', '#####'],
|
|
150
|
+
'≈': ['.....', '.#...', '#.#.#', '...#.', '.#...', '#.#.#', '...#.'],
|
|
151
|
+
'±': ['..#..', '..#..', '#####', '..#..', '..#..', '.....', '#####'],
|
|
152
|
+
'°': ['.##..', '#..#.', '.##..'],
|
|
153
|
+
'€': ['..###', '.#...', '####.', '.#...', '####.', '.#...', '..###'],
|
|
154
|
+
'£': ['..##.', '.#..#', '.#...', '###..', '.#...', '.#..#', '#.##.'],
|
|
155
|
+
'ᵖ': ['###..', '#.#..', '###..', '#....'],
|
|
156
|
+
'‹': ['.....', '...#.', '..#..', '.#...', '..#..', '...#.', '.....'],
|
|
157
|
+
'›': ['.....', '.#...', '..#..', '...#.', '..#..', '.#...', '.....'],
|
|
158
|
+
'«': ['.....', '..#.#', '.#.#.', '#.#..', '.#.#.', '..#.#', '.....'],
|
|
159
|
+
'»': ['.....', '#.#..', '.#.#.', '..#.#', '.#.#.', '#.#..', '.....'],
|
|
160
|
+
'‘': ['..#..', '.#...', '.#...'],
|
|
161
|
+
'’': ['..#..', '..#..', '.#...'],
|
|
162
|
+
'“': ['.#.#.', '#.#..', '#.#..'],
|
|
163
|
+
'”': ['.#.#.', '.#.#.', '#.#..'],
|
|
164
|
+
};
|
|
165
|
+
/** Glyph width and height in font pixels. */
|
|
166
|
+
export const FONT_W = 5;
|
|
167
|
+
export const FONT_H = 9;
|
|
168
|
+
// Small 3×5 digits for subscripts (₀-₉: the price formatter's zero-count, e.g. $0.0₅3105) and superscripts (⁰-⁹).
|
|
169
|
+
// A subscript must never render as a full-size digit: that would turn 0.0₅31 into 0.0531.
|
|
170
|
+
const SMALL = [
|
|
171
|
+
['###', '#.#', '#.#', '#.#', '###'], ['.#.', '##.', '.#.', '.#.', '###'], ['###', '..#', '###', '#..', '###'],
|
|
172
|
+
['###', '..#', '.##', '..#', '###'], ['#.#', '#.#', '###', '..#', '..#'], ['###', '#..', '###', '..#', '###'],
|
|
173
|
+
['###', '#..', '###', '#.#', '###'], ['###', '..#', '.#.', '.#.', '.#.'], ['###', '#.#', '###', '#.#', '###'],
|
|
174
|
+
['###', '#.#', '###', '..#', '###'],
|
|
175
|
+
];
|
|
176
|
+
const SUB = '₀₁₂₃₄₅₆₇₈₉', SUP = '⁰¹²³⁴⁵⁶⁷⁸⁹';
|
|
177
|
+
for (let d = 0; d < 10; d++) {
|
|
178
|
+
const rows = SMALL[d].map((r) => `.${r}.`);
|
|
179
|
+
G[SUB[d]] = ['.....', '.....', '.....', '.....', ...rows];
|
|
180
|
+
G[SUP[d]] = rows;
|
|
181
|
+
}
|
|
182
|
+
const BITS = new Map();
|
|
183
|
+
for (const [ch, rows] of Object.entries(G)) {
|
|
184
|
+
const b = new Uint8Array(FONT_W * FONT_H);
|
|
185
|
+
rows.forEach((r, y) => { for (let x = 0; x < FONT_W; x++)
|
|
186
|
+
if (r[x] === '#')
|
|
187
|
+
b[y * FONT_W + x] = 1; });
|
|
188
|
+
BITS.set(ch, b);
|
|
189
|
+
}
|
|
190
|
+
/** The 5×9 bitmap for a character (1 = ink), or null when the font has no glyph for it. */
|
|
191
|
+
export function glyphBits(ch) {
|
|
192
|
+
return BITS.get(ch) ?? null;
|
|
193
|
+
}
|
|
194
|
+
/** Every character with a bitmap glyph (tests check the printable ASCII range is complete). */
|
|
195
|
+
export function fontChars() { return [...BITS.keys()]; }
|