@hviana/sema 0.5.8 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +43 -4
- package/DATASETS.md +160 -0
- package/README.md +12 -0
- package/dist/example/train_base/cache.d.ts +35 -0
- package/dist/example/train_base/cache.js +211 -0
- package/dist/example/train_base/config.d.ts +21 -0
- package/dist/example/train_base/config.js +94 -0
- package/dist/example/train_base/corpora/aya.d.ts +19 -0
- package/dist/example/train_base/corpora/aya.js +76 -0
- package/dist/example/train_base/corpora/converted-parquet.d.ts +14 -0
- package/dist/example/train_base/corpora/converted-parquet.js +44 -0
- package/dist/example/train_base/corpora/genknow.d.ts +14 -0
- package/dist/example/train_base/corpora/genknow.js +83 -0
- package/dist/example/train_base/corpora/index.d.ts +29 -0
- package/dist/example/train_base/corpora/index.js +81 -0
- package/dist/example/train_base/corpora/massive.d.ts +7 -0
- package/dist/example/train_base/corpora/massive.js +98 -0
- package/dist/example/train_base/corpora/oasst2.d.ts +52 -0
- package/dist/example/train_base/corpora/oasst2.js +120 -0
- package/dist/example/train_base/corpora/smolsent.d.ts +23 -0
- package/dist/example/train_base/corpora/smolsent.js +156 -0
- package/dist/example/train_base/corpora/soda.d.ts +12 -0
- package/dist/example/train_base/corpora/soda.js +113 -0
- package/dist/example/train_base/corpora/taskmaster.d.ts +15 -0
- package/dist/example/train_base/corpora/taskmaster.js +144 -0
- package/dist/example/train_base/corpora/wiki2.d.ts +23 -0
- package/dist/example/train_base/corpora/wiki2.js +132 -0
- package/dist/example/train_base/corpus.d.ts +88 -0
- package/dist/example/train_base/corpus.js +65 -0
- package/dist/example/train_base/discovery.d.ts +48 -0
- package/dist/example/train_base/discovery.js +143 -0
- package/dist/example/train_base/http.d.ts +82 -0
- package/dist/example/train_base/http.js +219 -0
- package/dist/example/train_base/items.d.ts +46 -0
- package/dist/example/train_base/items.js +98 -0
- package/dist/example/train_base/main.d.ts +4 -0
- package/dist/example/train_base/main.js +207 -0
- package/dist/example/train_base/progress.d.ts +34 -0
- package/dist/example/train_base/progress.js +114 -0
- package/dist/example/train_base/readers.d.ts +125 -0
- package/dist/example/train_base/readers.js +391 -0
- package/dist/example/train_base/runtime.d.ts +115 -0
- package/dist/example/train_base/runtime.js +637 -0
- package/dist/example/train_base/stage.d.ts +3 -0
- package/dist/example/train_base/stage.js +246 -0
- package/dist/example/train_base/ui.d.ts +88 -0
- package/dist/example/train_base/ui.js +272 -0
- package/dist/src/geometry.d.ts +20 -0
- package/dist/src/geometry.js +22 -0
- package/dist/src/mind/attention.d.ts +6 -0
- package/dist/src/mind/attention.js +44 -4
- package/dist/src/mind/learning.js +134 -50
- package/dist/src/mind/mechanisms/cast.js +45 -1
- package/dist/src/mind/mind.d.ts +7 -2
- package/dist/src/mind/mind.js +15 -3
- package/dist/src/mind/reasoning.js +59 -5
- package/dist/src/mind/recognition.js +29 -3
- package/dist/src/mind/traverse.d.ts +16 -0
- package/dist/src/mind/traverse.js +18 -0
- package/dist/src/store-sqlite.d.ts +4 -0
- package/dist/src/store-sqlite.js +47 -0
- package/dist/src/store.d.ts +7 -0
- package/example/train_base/cache.ts +251 -0
- package/example/train_base/config.ts +128 -0
- package/example/train_base/corpora/aya.ts +106 -0
- package/example/train_base/corpora/converted-parquet.ts +64 -0
- package/example/train_base/corpora/genknow.ts +114 -0
- package/example/train_base/corpora/index.ts +88 -0
- package/example/train_base/corpora/massive.ts +111 -0
- package/example/train_base/corpora/oasst2.ts +163 -0
- package/example/train_base/corpora/smolsent.ts +203 -0
- package/example/train_base/corpora/soda.ts +130 -0
- package/example/train_base/corpora/taskmaster.ts +217 -0
- package/example/train_base/corpora/wiki2.ts +190 -0
- package/example/train_base/corpus.ts +150 -0
- package/example/train_base/discovery.ts +203 -0
- package/example/train_base/http.ts +284 -0
- package/example/train_base/items.ts +118 -0
- package/example/train_base/main.ts +240 -0
- package/example/train_base/progress.ts +149 -0
- package/example/train_base/readers.ts +505 -0
- package/example/train_base/runtime.ts +894 -0
- package/example/train_base/stage.ts +276 -0
- package/example/train_base/ui.ts +333 -0
- package/jsr.json +1 -1
- package/package.json +2 -4
- package/src/geometry.ts +23 -0
- package/src/mind/attention.ts +54 -1
- package/src/mind/learning.ts +137 -43
- package/src/mind/mechanisms/cast.ts +48 -1
- package/src/mind/mind.ts +13 -2
- package/src/mind/reasoning.ts +64 -5
- package/src/mind/recognition.ts +29 -3
- package/src/mind/traverse.ts +19 -0
- package/src/store-sqlite.ts +53 -0
- package/src/store.ts +28 -0
- package/test/13-conversation.test.mjs +1 -1
- package/test/29-counterfactual.test.mjs +43 -6
- package/test/77-company-saturation.test.mjs +302 -0
- package/test/78-atom-hub-recognition-cliff.test.mjs +135 -0
- package/test/84-composed-answer-honesty.test.mjs +137 -0
- package/test/85-answered-directly.test.mjs +126 -0
- package/test/86-cast-voices-committed.test.mjs +164 -0
- package/test/87-codominant-commitment.test.mjs +250 -0
- package/test/88-dependency-footprint.test.mjs +99 -0
- package/dist/example/train_base.d.ts +0 -93
- package/dist/example/train_base.js +0 -2269
- package/example/train_base.ts +0 -2735
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
// train_base/stage.ts — ONE loop, run once per corpus.
|
|
2
|
+
//
|
|
3
|
+
// Every stage in this trainer was the same seven steps — resume-check, build a
|
|
4
|
+
// work-list, acquire each unit, read it, tally it, mark it complete, persist —
|
|
5
|
+
// differing only in where the work-list comes from, what container the bytes
|
|
6
|
+
// are in, and how a row becomes deposits. Those three are now DATA (a
|
|
7
|
+
// `Corpus`, declared in corpus.ts), and this file is the loop they are fed to.
|
|
8
|
+
//
|
|
9
|
+
// Nothing but the loop lives here, so no corpus file needs to import it.
|
|
10
|
+
import { MAX_BYTES } from "./config.js";
|
|
11
|
+
import { unitIdOf } from "./corpus.js";
|
|
12
|
+
import { loadProgress } from "./progress.js";
|
|
13
|
+
import { DIM, dur, GRN, int, R, RED, YEL } from "./ui.js";
|
|
14
|
+
import { statSync, unlinkSync } from "node:fs";
|
|
15
|
+
/** Rows a read could not use, as the log has always reported them: one number.
|
|
16
|
+
* The reader keeps malformed records and adapter-declined rows apart (see
|
|
17
|
+
* FileResult) because they mean different things, but only a corpus that
|
|
18
|
+
* declines records BY DESIGN needs the distinction on screen. */
|
|
19
|
+
const unusedRows = (r, style) => style?.malformedOnly ? r.skipped : r.skipped + r.unusable;
|
|
20
|
+
export async function runStage(ctx, corpus) {
|
|
21
|
+
const { progress, state, store, tick } = ctx;
|
|
22
|
+
const c = ctx.counters;
|
|
23
|
+
if (!corpus.enabled)
|
|
24
|
+
return;
|
|
25
|
+
if (c.trainedContentBytes >= MAX_BYTES || ctx.stopRequested)
|
|
26
|
+
return;
|
|
27
|
+
// Say what is actually happening. Discovery is a network call that can wait
|
|
28
|
+
// out a rate limit for minutes, and until it is announced the panel keeps
|
|
29
|
+
// displaying the PREVIOUS stage's file as though it were still processing.
|
|
30
|
+
state.activity = "list";
|
|
31
|
+
state.filePath = corpus.label;
|
|
32
|
+
state.fileExamples = 0;
|
|
33
|
+
// The unit counter belongs to the stage that is running, so it is cleared
|
|
34
|
+
// here rather than left showing the previous stage's totals through this one.
|
|
35
|
+
state.fileIndex = 0;
|
|
36
|
+
state.fileTotal = 0;
|
|
37
|
+
tick(true);
|
|
38
|
+
let units;
|
|
39
|
+
try {
|
|
40
|
+
units = await corpus.discover(ctx);
|
|
41
|
+
}
|
|
42
|
+
catch (e) {
|
|
43
|
+
if (ctx.stopRequested || e?.name === "AbortError")
|
|
44
|
+
return;
|
|
45
|
+
progress.log(` ${RED}✗${R} ${corpus.label} file listing failed: ${e.message}`);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (units === null)
|
|
49
|
+
return; // discover already said why
|
|
50
|
+
if (units.length === 0) {
|
|
51
|
+
progress.log(` ${DIM}· no ${corpus.label} files found — skipping${R}`);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const idOf = (u) => unitIdOf(corpus, u);
|
|
55
|
+
const maxRows = corpus.maxRows ?? 0;
|
|
56
|
+
const p = await loadProgress(store);
|
|
57
|
+
const done = new Set(p.completedFiles);
|
|
58
|
+
// A budget-limited stage never marks its later shards complete — that is
|
|
59
|
+
// what lets a raised budget resume instead of restarting. But it also means
|
|
60
|
+
// "every shard complete" is NOT how such a stage finishes, so without a
|
|
61
|
+
// marker of its own a satisfied budget would re-read and re-deposit its
|
|
62
|
+
// rows on every subsequent run: harmless to the store (deposition is
|
|
63
|
+
// idempotent) but it repeats the work and double-counts langTally.
|
|
64
|
+
//
|
|
65
|
+
// The marker carries the budget it was satisfied AT, so raising the budget
|
|
66
|
+
// still resumes: a bigger budget does not match the marker and the stage
|
|
67
|
+
// runs again, picking up from the rows it has already taken (below) rather
|
|
68
|
+
// than from zero.
|
|
69
|
+
const budgetMark = maxRows > 0 ? `${corpus.id}::budget=${maxRows}` : "";
|
|
70
|
+
if (budgetMark && done.has(budgetMark)) {
|
|
71
|
+
progress.log(` ${DIM}· ${corpus.label} budget of ${int(maxRows)} row(s) already met — skipping${R}`);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const remaining = units.filter((u) => !done.has(idOf(u)));
|
|
75
|
+
if (remaining.length === 0) {
|
|
76
|
+
progress.log(` ${DIM}· ${corpus.label} already trained — skipping${R}`);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
state.fileTotal = units.length;
|
|
80
|
+
state.unitNoun = corpus.unitNoun ?? "unit(s)";
|
|
81
|
+
// Fix the corpus denominator BEFORE reading anything. It used to grow as each
|
|
82
|
+
// file was opened, so the bar ran to 100% at the end of every file and fell
|
|
83
|
+
// back when the next was added — 100%, 50%, 100%, 66%. Both listings report
|
|
84
|
+
// sizes, so the pending work is knowable up front. Units of unknown size
|
|
85
|
+
// (a single-file corpus, whose size arrives with its HEAD) contribute 0 here
|
|
86
|
+
// and are added when they are opened, as before.
|
|
87
|
+
const pendingBytes = units
|
|
88
|
+
.filter((u) => !done.has(idOf(u)))
|
|
89
|
+
.reduce((n, u) => n + (u.bytes ?? 0), 0);
|
|
90
|
+
c.totalCorpusBytes = c.totalBytesProcessed + pendingBytes;
|
|
91
|
+
if (corpus.unitNoun) {
|
|
92
|
+
const taken = c.rowsTaken[corpus.id] ?? 0;
|
|
93
|
+
progress.log(` ${GRN}✓${R} ${corpus.label}: ${remaining.length}/${units.length} ${corpus.unitNoun} to train` +
|
|
94
|
+
(maxRows > 0
|
|
95
|
+
? ` ${DIM}(budget ${int(maxRows)} rows` +
|
|
96
|
+
(taken > 0 ? `, ${int(taken)} already taken` : "") + `)${R}`
|
|
97
|
+
: ""));
|
|
98
|
+
}
|
|
99
|
+
// The budget spans the whole stage AND the whole STORE, not one shard and not
|
|
100
|
+
// one run. It starts from the rows already taken by units this store has
|
|
101
|
+
// finished, so an interrupted run resumes into the same budget instead of
|
|
102
|
+
// being granted a fresh one — without that, every Ctrl+C between two shards
|
|
103
|
+
// let a budgeted corpus deposit up to a full budget more than asked for, and
|
|
104
|
+
// for SODA the budget IS the curriculum balance (see corpora/soda.ts).
|
|
105
|
+
//
|
|
106
|
+
// The persisted figure counts FINISHED units only. Rows read from a unit that
|
|
107
|
+
// was cut short are deliberately not committed, because a resume re-reads
|
|
108
|
+
// that unit from the top and re-deposits exactly those rows (idempotent); had
|
|
109
|
+
// they been committed, the re-read would count them twice and the stage would
|
|
110
|
+
// stop short of its budget.
|
|
111
|
+
let rowsTaken = c.rowsTaken[corpus.id] ?? 0;
|
|
112
|
+
const spent = () => maxRows > 0 && rowsTaken >= maxRows;
|
|
113
|
+
const adapt = maxRows > 0
|
|
114
|
+
? (row) => {
|
|
115
|
+
const items = corpus.toItems(row);
|
|
116
|
+
if (!items || items.length === 0)
|
|
117
|
+
return null;
|
|
118
|
+
rowsTaken++;
|
|
119
|
+
return items;
|
|
120
|
+
}
|
|
121
|
+
: corpus.toItems;
|
|
122
|
+
let idx = 0;
|
|
123
|
+
for (const u of units) {
|
|
124
|
+
if (c.trainedContentBytes >= MAX_BYTES || ctx.stopRequested)
|
|
125
|
+
break;
|
|
126
|
+
if (spent())
|
|
127
|
+
break;
|
|
128
|
+
idx++;
|
|
129
|
+
if (done.has(idOf(u)))
|
|
130
|
+
continue;
|
|
131
|
+
// Acquire (download or reuse), then read.
|
|
132
|
+
let path = u.local ?? "";
|
|
133
|
+
let downloaded = false;
|
|
134
|
+
if (!path) {
|
|
135
|
+
const got = await ctx.acquire(u.url, u.dest ?? idOf(u).replace(/[^A-Za-z0-9._-]+/g, "_"), u.acquireLabel ?? u.display);
|
|
136
|
+
if (!got) {
|
|
137
|
+
if (ctx.stopRequested)
|
|
138
|
+
break;
|
|
139
|
+
continue; // a single failed unit never aborts the stage
|
|
140
|
+
}
|
|
141
|
+
path = got.path;
|
|
142
|
+
// A file that came from the cache is the corpus's to keep or to reclaim;
|
|
143
|
+
// only oasst2 keeps it. See Corpus.keepCached.
|
|
144
|
+
downloaded = corpus.keepCached ? !got.cached : true;
|
|
145
|
+
}
|
|
146
|
+
// Only for a unit the listing could not size (a single-file corpus). One
|
|
147
|
+
// whose size was declared is already in the denominator set above, and
|
|
148
|
+
// adding it again is exactly the drift that made the bar bounce.
|
|
149
|
+
if (!u.bytes) {
|
|
150
|
+
try {
|
|
151
|
+
c.totalCorpusBytes += statSync(path).size;
|
|
152
|
+
}
|
|
153
|
+
catch { /* best effort */ }
|
|
154
|
+
}
|
|
155
|
+
state.activity = "process";
|
|
156
|
+
state.fileIndex = idx;
|
|
157
|
+
state.filePath = u.display;
|
|
158
|
+
state.fileExamples = 0;
|
|
159
|
+
tick(true);
|
|
160
|
+
const p0 = Date.now();
|
|
161
|
+
let res;
|
|
162
|
+
// Pick up inside this unit if the last run stopped inside THIS one. A
|
|
163
|
+
// cursor is never applied to a different unit: the row count means whatever
|
|
164
|
+
// that unit's reader counts, and only there.
|
|
165
|
+
const cur = ctx.resumeCursor;
|
|
166
|
+
const startRow = cur && cur.unitId === idOf(u) ? cur.rows : 0;
|
|
167
|
+
try {
|
|
168
|
+
res = await corpus.read(path, adapt, ctx.readCtx({
|
|
169
|
+
unitId: idOf(u),
|
|
170
|
+
corpusId: corpus.id,
|
|
171
|
+
startRow,
|
|
172
|
+
shouldStop: maxRows > 0 ? spent : undefined,
|
|
173
|
+
// The LIVE count, for the cursor snapshot only. It must not be
|
|
174
|
+
// written into ctx.counters: those are what persist() records, and
|
|
175
|
+
// recording a budget figure for a unit that has not finished would
|
|
176
|
+
// charge the store for rows a resume is about to re-read.
|
|
177
|
+
rowsTakenNow: maxRows > 0 ? () => rowsTaken : undefined,
|
|
178
|
+
}));
|
|
179
|
+
}
|
|
180
|
+
catch (e) {
|
|
181
|
+
if (ctx.stopRequested || e?.name === "AbortError")
|
|
182
|
+
break;
|
|
183
|
+
progress.log(` ${RED}✗${R} ${u.display} parse failed: ${e.message}`);
|
|
184
|
+
if (downloaded) {
|
|
185
|
+
try {
|
|
186
|
+
unlinkSync(path);
|
|
187
|
+
}
|
|
188
|
+
catch { /* best effort */ }
|
|
189
|
+
}
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
// A shard cut short by the BUDGET is not "done" — leave it resumable so a
|
|
193
|
+
// later run with a bigger budget continues instead of starting over.
|
|
194
|
+
const hitBudget = spent();
|
|
195
|
+
// The tally is accrued per deposit by the runtime now (see onDeposit), so
|
|
196
|
+
// that a cursor taken mid-unit carries a true one. Adding res.examples here
|
|
197
|
+
// as well would count this unit twice.
|
|
198
|
+
const style = corpus.log;
|
|
199
|
+
const bad = unusedRows(res, style);
|
|
200
|
+
const badNoun = style?.malformedOnly
|
|
201
|
+
? "malformed line(s)"
|
|
202
|
+
: style?.bad ?? "unusable row(s)";
|
|
203
|
+
const fromRows = style?.rows
|
|
204
|
+
? `from ${int(res.rowsUsed)} ${style.rows} `
|
|
205
|
+
: "";
|
|
206
|
+
progress.log(` ${GRN}✓${R} ${u.name} ${DIM}[${corpus.kind}]${R} → ${int(res.examples)} ${style?.deposits ?? "facts"} ${DIM}${fromRows}in ${dur((Date.now() - p0) / 1000)}${R}` +
|
|
207
|
+
(bad ? ` ${YEL}· ${int(bad)} ${badNoun} skipped${R}` : "") +
|
|
208
|
+
(hitBudget
|
|
209
|
+
? ` ${YEL}(budget reached)${R}`
|
|
210
|
+
: res.stopped
|
|
211
|
+
? ` ${YEL}(stopped early)${R}`
|
|
212
|
+
: ""));
|
|
213
|
+
if (!res.stopped && !hitBudget) {
|
|
214
|
+
try {
|
|
215
|
+
c.totalBytesProcessed += statSync(path).size;
|
|
216
|
+
}
|
|
217
|
+
catch { /* best effort */ }
|
|
218
|
+
if (downloaded) {
|
|
219
|
+
try {
|
|
220
|
+
unlinkSync(path);
|
|
221
|
+
}
|
|
222
|
+
catch { /* best effort */ }
|
|
223
|
+
}
|
|
224
|
+
done.add(idOf(u));
|
|
225
|
+
p.completedFiles.push(idOf(u));
|
|
226
|
+
// Commit this unit's rows against the budget — see the note above: only
|
|
227
|
+
// a FINISHED unit's rows are committed, so a resume never counts a
|
|
228
|
+
// re-read prefix twice.
|
|
229
|
+
if (maxRows > 0)
|
|
230
|
+
c.rowsTaken[corpus.id] = rowsTaken;
|
|
231
|
+
}
|
|
232
|
+
await ctx.persist(p.completedFiles, !res.stopped && !hitBudget);
|
|
233
|
+
// A budget stop is not a cap/signal stop: the stage is finished, so fall
|
|
234
|
+
// out of the loop rather than treating it as an interruption.
|
|
235
|
+
if (res.stopped && !hitBudget)
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
// Record a satisfied budget so the next run skips this stage instead of
|
|
239
|
+
// re-reading it. Only when the budget was actually reached: a stage that
|
|
240
|
+
// ran out of shards first is complete by the normal per-shard rule, and a
|
|
241
|
+
// stage cut short by MAX_MB or Ctrl+C must stay resumable.
|
|
242
|
+
if (budgetMark && spent() && !ctx.stopRequested && !done.has(budgetMark)) {
|
|
243
|
+
p.completedFiles.push(budgetMark);
|
|
244
|
+
await ctx.persist(p.completedFiles);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { type TrainingItem } from "./items.js";
|
|
2
|
+
export declare const CSI = "\u001B[";
|
|
3
|
+
export declare const B = "\u001B[1m", DIM = "\u001B[2m", R = "\u001B[0m";
|
|
4
|
+
export declare const GREY = "\u001B[90m", CYAN = "\u001B[36m", GRN = "\u001B[32m";
|
|
5
|
+
export declare const YEL = "\u001B[33m", RED = "\u001B[31m";
|
|
6
|
+
export declare const HIDE = "\u001B[?25l", SHOW = "\u001B[?25h";
|
|
7
|
+
/** Human-readable duration from seconds. */
|
|
8
|
+
export declare function dur(seconds: number): string;
|
|
9
|
+
/** Human-readable byte size. */
|
|
10
|
+
export declare function bytes(n: number): string;
|
|
11
|
+
/** Short count: 1234567 → "1.23M". */
|
|
12
|
+
export declare function num(n: number): string;
|
|
13
|
+
export declare const int: (n: number) => string;
|
|
14
|
+
export declare const clamp01: (f: number) => number;
|
|
15
|
+
export declare const pct: (f: number) => string;
|
|
16
|
+
/** A progress bar of width `w` filled to fraction `frac`. */
|
|
17
|
+
export declare function bar(w: number, frac: number): string;
|
|
18
|
+
/** Collapse whitespace and clip to `max` chars with an ellipsis. */
|
|
19
|
+
export declare function clip(text: string, max: number): string;
|
|
20
|
+
export interface ProgState {
|
|
21
|
+
exampleCount: number;
|
|
22
|
+
target: number;
|
|
23
|
+
elapsedS: number;
|
|
24
|
+
trainedBytes: number;
|
|
25
|
+
trainedRate: number;
|
|
26
|
+
bytesDone: number;
|
|
27
|
+
bytesTotal: number;
|
|
28
|
+
bytesRate: number;
|
|
29
|
+
fileIndex: number;
|
|
30
|
+
fileTotal: number;
|
|
31
|
+
/** What this stage's units ARE ("translation file(s)", "shard(s)"). The panel
|
|
32
|
+
* used to say "languages" for every corpus — true only of SmolSent, and
|
|
33
|
+
* plainly wrong while reading Parquet shards or dialogue files. */
|
|
34
|
+
unitNoun: string;
|
|
35
|
+
filePath: string;
|
|
36
|
+
fileSize: number;
|
|
37
|
+
fileExamples: number;
|
|
38
|
+
/** "list" is the work-list call — an HF/GitHub tree fetch, which can wait
|
|
39
|
+
* minutes behind a rate limit and had no way to say so: the panel simply
|
|
40
|
+
* kept showing the PREVIOUS stage's file as though it were still being
|
|
41
|
+
* processed. */
|
|
42
|
+
activity: "download" | "process" | "list" | "idle";
|
|
43
|
+
dlSpeed: number;
|
|
44
|
+
dlDone: number;
|
|
45
|
+
dlTotal: number;
|
|
46
|
+
storeEntries: number;
|
|
47
|
+
cacheBytes: number;
|
|
48
|
+
lastSample: string | null;
|
|
49
|
+
}
|
|
50
|
+
/** A prompt/expected pair to display for an item. */
|
|
51
|
+
export declare function promptOf(it: TrainingItem): {
|
|
52
|
+
prompt: string;
|
|
53
|
+
expected: string | null;
|
|
54
|
+
kind: "episode" | "experience";
|
|
55
|
+
};
|
|
56
|
+
/** A coarse, honest similarity between an expected continuation and SEMA's
|
|
57
|
+
* recall. Both are normalized (lowercased, whitespace-collapsed) and compared
|
|
58
|
+
* by the longest shared leading run plus token overlap, so the verdict is a
|
|
59
|
+
* heuristic signal of recall quality rather than a brittle fixed-prefix test. */
|
|
60
|
+
export declare function recallSimilarity(expected: string, response: string): number;
|
|
61
|
+
/** A framed recall sample. Pinned in the panel on a TTY (so the most recent
|
|
62
|
+
* example is always on screen) and logged once per checkpoint when piped. */
|
|
63
|
+
export declare function renderInferenceBox(prompt: string, expected: string | null, response: string, kind: "episode" | "experience", checkpointN: number): string;
|
|
64
|
+
/** Render the whole panel. `title` names the curriculum being trained; the run
|
|
65
|
+
* supplies it, so the panel never has to know which corpora exist. */
|
|
66
|
+
export declare function renderPanel(s: ProgState, title: string): string;
|
|
67
|
+
/** A live panel pinned to the bottom of stderr. On a TTY it redraws in place,
|
|
68
|
+
* clearing only its own lines; logs are flushed into the scrollback above it.
|
|
69
|
+
* Off a TTY (piped/CI) the panel is suppressed and a plain status line is
|
|
70
|
+
* emitted occasionally, so logs stay clean and parseable. */
|
|
71
|
+
export declare class Progress {
|
|
72
|
+
private readonly title;
|
|
73
|
+
private lines;
|
|
74
|
+
private lastPaint;
|
|
75
|
+
private lastStatus;
|
|
76
|
+
private last;
|
|
77
|
+
private readonly tty;
|
|
78
|
+
constructor(title: string);
|
|
79
|
+
/** True when attached to an interactive terminal (panel is live). */
|
|
80
|
+
get interactive(): boolean;
|
|
81
|
+
/** Cursor sequence that returns to the top of the panel and clears it. */
|
|
82
|
+
private clearPanel;
|
|
83
|
+
render(s: ProgState, force?: boolean): void;
|
|
84
|
+
/** Emit a line (or block) into the scrollback above the panel; the panel is
|
|
85
|
+
* redrawn immediately beneath it so it never disappears between frames. */
|
|
86
|
+
log(msg: string): void;
|
|
87
|
+
dispose(): void;
|
|
88
|
+
}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
// train_base/ui.ts — everything the run puts on the screen: the formatters, the
|
|
2
|
+
// live panel pinned to the bottom of stderr, and the checkpoint recall box.
|
|
3
|
+
//
|
|
4
|
+
// Nothing here decides anything. It renders a ProgState the run owns, and the
|
|
5
|
+
// run never reads a value back out of it — so the whole display can be swapped
|
|
6
|
+
// (or silenced) without touching a line of training logic.
|
|
7
|
+
import { CHECKPOINT_BYTES, D, DB_PATH, PROGRESS_MS, SEED } from "./config.js";
|
|
8
|
+
import { isEpisode } from "./items.js";
|
|
9
|
+
import { basename } from "node:path";
|
|
10
|
+
// ── colours ──
|
|
11
|
+
export const CSI = "\x1b[";
|
|
12
|
+
export const B = `${CSI}1m`, DIM = `${CSI}2m`, R = `${CSI}0m`;
|
|
13
|
+
export const GREY = `${CSI}90m`, CYAN = `${CSI}36m`, GRN = `${CSI}32m`;
|
|
14
|
+
export const YEL = `${CSI}33m`, RED = `${CSI}31m`;
|
|
15
|
+
export const HIDE = `${CSI}?25l`, SHOW = `${CSI}?25h`;
|
|
16
|
+
// ── formatters ──
|
|
17
|
+
/** Human-readable duration from seconds. */
|
|
18
|
+
export function dur(seconds) {
|
|
19
|
+
if (!isFinite(seconds) || seconds < 0)
|
|
20
|
+
return "--";
|
|
21
|
+
const h = Math.floor(seconds / 3600);
|
|
22
|
+
const m = Math.floor((seconds % 3600) / 60);
|
|
23
|
+
const s = Math.floor(seconds % 60);
|
|
24
|
+
if (h > 0)
|
|
25
|
+
return `${h}h ${m}m ${s}s`;
|
|
26
|
+
if (m > 0)
|
|
27
|
+
return `${m}m ${s}s`;
|
|
28
|
+
return `${s}s`;
|
|
29
|
+
}
|
|
30
|
+
/** Human-readable byte size. */
|
|
31
|
+
export function bytes(n) {
|
|
32
|
+
if (!isFinite(n) || n < 0)
|
|
33
|
+
return "--";
|
|
34
|
+
if (n < 1024)
|
|
35
|
+
return `${n} B`;
|
|
36
|
+
if (n < 1e6)
|
|
37
|
+
return `${(n / 1024).toFixed(1)} KB`;
|
|
38
|
+
if (n < 1e9)
|
|
39
|
+
return `${(n / 1e6).toFixed(1)} MB`;
|
|
40
|
+
return `${(n / 1e9).toFixed(2)} GB`;
|
|
41
|
+
}
|
|
42
|
+
/** Short count: 1234567 → "1.23M". */
|
|
43
|
+
export function num(n) {
|
|
44
|
+
if (n >= 1e9)
|
|
45
|
+
return `${(n / 1e9).toFixed(2)}B`;
|
|
46
|
+
if (n >= 1e6)
|
|
47
|
+
return `${(n / 1e6).toFixed(2)}M`;
|
|
48
|
+
if (n >= 1e3)
|
|
49
|
+
return `${(n / 1e3).toFixed(1)}K`;
|
|
50
|
+
return String(n);
|
|
51
|
+
}
|
|
52
|
+
export const int = (n) => Math.round(n).toLocaleString("en-US");
|
|
53
|
+
export const clamp01 = (f) => Math.max(0, Math.min(1, f));
|
|
54
|
+
export const pct = (f) => `${(clamp01(f) * 100).toFixed(1)}%`;
|
|
55
|
+
/** A progress bar of width `w` filled to fraction `frac`. */
|
|
56
|
+
export function bar(w, frac) {
|
|
57
|
+
const filled = Math.round(clamp01(frac) * w);
|
|
58
|
+
return `${GRN}${"█".repeat(filled)}${GREY}${"░".repeat(w - filled)}${R}`;
|
|
59
|
+
}
|
|
60
|
+
/** Collapse whitespace and clip to `max` chars with an ellipsis. */
|
|
61
|
+
export function clip(text, max) {
|
|
62
|
+
const t = text.replace(/\s+/g, " ").trim();
|
|
63
|
+
if (max < 1)
|
|
64
|
+
return "";
|
|
65
|
+
return t.length <= max ? t : t.slice(0, max - 1) + "…";
|
|
66
|
+
}
|
|
67
|
+
/** A prompt/expected pair to display for an item. */
|
|
68
|
+
export function promptOf(it) {
|
|
69
|
+
return isEpisode(it)
|
|
70
|
+
? { prompt: it.context, expected: it.continuation, kind: "episode" }
|
|
71
|
+
: { prompt: it.slice(0, 200), expected: null, kind: "experience" };
|
|
72
|
+
}
|
|
73
|
+
/** A coarse, honest similarity between an expected continuation and SEMA's
|
|
74
|
+
* recall. Both are normalized (lowercased, whitespace-collapsed) and compared
|
|
75
|
+
* by the longest shared leading run plus token overlap, so the verdict is a
|
|
76
|
+
* heuristic signal of recall quality rather than a brittle fixed-prefix test. */
|
|
77
|
+
export function recallSimilarity(expected, response) {
|
|
78
|
+
const norm = (s) => s.toLowerCase().replace(/\s+/g, " ").trim();
|
|
79
|
+
const a = norm(expected), b = norm(response);
|
|
80
|
+
if (!a || !b)
|
|
81
|
+
return 0;
|
|
82
|
+
let lead = 0;
|
|
83
|
+
const lim = Math.min(a.length, b.length);
|
|
84
|
+
while (lead < lim && a[lead] === b[lead])
|
|
85
|
+
lead++;
|
|
86
|
+
const leadFrac = lead / Math.max(1, Math.min(a.length, b.length));
|
|
87
|
+
const ta = new Set(a.split(" ")), tb = new Set(b.split(" "));
|
|
88
|
+
let inter = 0;
|
|
89
|
+
for (const w of ta)
|
|
90
|
+
if (tb.has(w))
|
|
91
|
+
inter++;
|
|
92
|
+
const jac = inter / Math.max(1, ta.size + tb.size - inter);
|
|
93
|
+
return Math.max(leadFrac, jac);
|
|
94
|
+
}
|
|
95
|
+
/** A framed recall sample. Pinned in the panel on a TTY (so the most recent
|
|
96
|
+
* example is always on screen) and logged once per checkpoint when piped. */
|
|
97
|
+
export function renderInferenceBox(prompt, expected, response, kind, checkpointN) {
|
|
98
|
+
const W = 68;
|
|
99
|
+
const hr = `${DIM}${"─".repeat(W)}${R}`;
|
|
100
|
+
const title = kind === "episode"
|
|
101
|
+
? "latest recall"
|
|
102
|
+
: "latest recall (experience)";
|
|
103
|
+
const head = `${title} · checkpoint #${checkpointN} `;
|
|
104
|
+
const shown = response.trim() ? response : "(empty)";
|
|
105
|
+
const lines = [
|
|
106
|
+
`${B}╭─ ${head}${"─".repeat(Math.max(0, W - 2 - head.length))}╮${R}`,
|
|
107
|
+
`${B}│${R} ${hr}`,
|
|
108
|
+
`${B}│${R} ${CYAN}${B}Context:${R} ${clip(prompt, W - 13)}`,
|
|
109
|
+
];
|
|
110
|
+
if (expected) {
|
|
111
|
+
lines.push(`${B}│${R} ${YEL}${B}Expected:${R} ${clip(expected, W - 13)}`);
|
|
112
|
+
}
|
|
113
|
+
lines.push(`${B}│${R} ${GRN}${B}SEMA:${R} ${clip(shown, W - 13)}`);
|
|
114
|
+
lines.push(`${B}│${R} ${hr}`);
|
|
115
|
+
let verdict;
|
|
116
|
+
if (expected) {
|
|
117
|
+
const sim = recallSimilarity(expected, response);
|
|
118
|
+
const pctStr = `${Math.round(sim * 100)}%`;
|
|
119
|
+
verdict = sim >= 0.6
|
|
120
|
+
? `${GRN}✓${R} recall close to expected ${DIM}(~${pctStr} overlap)${R}`
|
|
121
|
+
: sim >= 0.25
|
|
122
|
+
? `${YEL}△${R} partial recall ${DIM}(~${pctStr} overlap)${R}`
|
|
123
|
+
: `${RED}✗${R} recall diverges ${DIM}(~${pctStr} overlap)${R}`;
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
verdict = `${DIM}·${R} plain experience — no expected answer`;
|
|
127
|
+
}
|
|
128
|
+
lines.push(`${B}│${R} ${verdict}`);
|
|
129
|
+
lines.push(`${B}╰${"─".repeat(W)}╯${R}`);
|
|
130
|
+
return lines.join("\n");
|
|
131
|
+
}
|
|
132
|
+
/** Render the whole panel. `title` names the curriculum being trained; the run
|
|
133
|
+
* supplies it, so the panel never has to know which corpora exist. */
|
|
134
|
+
export function renderPanel(s, title) {
|
|
135
|
+
const targetKnown = isFinite(s.target);
|
|
136
|
+
// Primary progress: by learned-content bytes when a MAX_MB target is set,
|
|
137
|
+
// else by how far we are through the corpus on disk (bytes) — so the default
|
|
138
|
+
// unbounded run still shows a real fraction and a real ETA.
|
|
139
|
+
const frac = targetKnown
|
|
140
|
+
? (s.target > 0 ? s.trainedBytes / s.target : 0)
|
|
141
|
+
: (s.bytesTotal > 0 ? s.bytesDone / s.bytesTotal : 0);
|
|
142
|
+
const etaStr = (() => {
|
|
143
|
+
if (targetKnown) {
|
|
144
|
+
return s.trainedRate > 0
|
|
145
|
+
? dur((s.target - s.trainedBytes) / s.trainedRate)
|
|
146
|
+
: "∞";
|
|
147
|
+
}
|
|
148
|
+
if (s.bytesTotal > 0 && s.bytesRate > 0) {
|
|
149
|
+
return dur((s.bytesTotal - s.bytesDone) / s.bytesRate);
|
|
150
|
+
}
|
|
151
|
+
return "∞";
|
|
152
|
+
})();
|
|
153
|
+
const fileFrac = s.fileTotal > 0 ? s.fileIndex / s.fileTotal : 0;
|
|
154
|
+
let actIcon = `${DIM}·${R}`, actText = "waiting…";
|
|
155
|
+
if (s.activity === "download") {
|
|
156
|
+
actIcon = `${CYAN}⬇${R}`;
|
|
157
|
+
const name = s.filePath;
|
|
158
|
+
const total = s.dlTotal > 0 ? s.dlTotal : s.fileSize;
|
|
159
|
+
if (total > 0 && s.dlDone > 0) {
|
|
160
|
+
const dlFrac = clamp01(s.dlDone / total);
|
|
161
|
+
actText =
|
|
162
|
+
`downloading ${name} ${bar(18, dlFrac)} ${B}${pct(dlFrac)}${R}` +
|
|
163
|
+
` ${DIM}${bytes(s.dlDone)}/${bytes(total)}${R}`;
|
|
164
|
+
if (s.dlSpeed > 0)
|
|
165
|
+
actText += ` ${DIM}@ ${bytes(s.dlSpeed)}/s${R}`;
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
actText = total > 0
|
|
169
|
+
? `downloading ${name} · ${bytes(total)}…`
|
|
170
|
+
: `downloading ${name}…`;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
else if (s.activity === "process") {
|
|
174
|
+
actIcon = `${GRN}✓${R}`;
|
|
175
|
+
actText = `processing ${s.filePath} · ${int(s.fileExamples)} examples so far`;
|
|
176
|
+
}
|
|
177
|
+
else if (s.activity === "list") {
|
|
178
|
+
actIcon = `${CYAN}⋯${R}`;
|
|
179
|
+
actText = `listing ${s.filePath} files…`;
|
|
180
|
+
}
|
|
181
|
+
const targetStr = targetKnown ? bytes(s.target) : "∞";
|
|
182
|
+
const headExamples = targetKnown
|
|
183
|
+
? `${CYAN}${bytes(s.trainedBytes)}${R} / ${targetStr} learned ${DIM}·${R} ${int(s.exampleCount)} examples`
|
|
184
|
+
: `${CYAN}${int(s.exampleCount)}${R} examples`;
|
|
185
|
+
const corpusInfo = s.bytesTotal > 0
|
|
186
|
+
? `${B}📦${R} ${bytes(s.bytesDone)}/${bytes(s.bytesTotal)} (${pct(s.bytesDone / s.bytesTotal)})`
|
|
187
|
+
: `${B}📦${R} ${bytes(s.bytesDone)} processed`;
|
|
188
|
+
const fileInfo = s.fileTotal > 0
|
|
189
|
+
? `${B}🌐${R} ${s.fileIndex}/${s.fileTotal} ${s.unitNoun} (${pct(fileFrac)})`
|
|
190
|
+
: `${B}🌐${R} ${s.unitNoun}`;
|
|
191
|
+
const panel = [
|
|
192
|
+
`${B}╭${R}${B} sema train${R} ${DIM}·${R} ${title} ${DIM}·${R} ` +
|
|
193
|
+
`D=${D} ${DIM}·${R} seed=${SEED} ${DIM}·${R} ` +
|
|
194
|
+
`store=${basename(DB_PATH)}.sqlite\n${B}╰${R} target=${CYAN}${targetStr}${R} ` +
|
|
195
|
+
`learned ${DIM}·${R} checkpoint every ${bytes(CHECKPOINT_BYTES)}`,
|
|
196
|
+
`\n${bar(40, frac)} ${B}${pct(frac)}${R} ${headExamples}`,
|
|
197
|
+
`\n${B}⚡${R} ${bytes(s.trainedRate)}/s learned ${B}🧠${R} ${bytes(s.trainedBytes)} content ${B}⏱${R} ${dur(s.elapsedS)} elapsed ${B}🕐${R} ${etaStr} ETA`,
|
|
198
|
+
`${fileInfo} ${corpusInfo} ${B}🗄${R} ${num(s.storeEntries)} entries ` +
|
|
199
|
+
`${B}💾${R} cache ${bytes(s.cacheBytes)}`,
|
|
200
|
+
`\n${actIcon} ${actText}`,
|
|
201
|
+
].join("");
|
|
202
|
+
return s.lastSample ? `${panel}\n${s.lastSample}` : panel;
|
|
203
|
+
}
|
|
204
|
+
/** A live panel pinned to the bottom of stderr. On a TTY it redraws in place,
|
|
205
|
+
* clearing only its own lines; logs are flushed into the scrollback above it.
|
|
206
|
+
* Off a TTY (piped/CI) the panel is suppressed and a plain status line is
|
|
207
|
+
* emitted occasionally, so logs stay clean and parseable. */
|
|
208
|
+
export class Progress {
|
|
209
|
+
title;
|
|
210
|
+
lines = 0; // height of the panel currently on screen
|
|
211
|
+
lastPaint = 0;
|
|
212
|
+
lastStatus = 0;
|
|
213
|
+
last = null;
|
|
214
|
+
tty = process.stderr.isTTY === true;
|
|
215
|
+
constructor(title) {
|
|
216
|
+
this.title = title;
|
|
217
|
+
}
|
|
218
|
+
/** True when attached to an interactive terminal (panel is live). */
|
|
219
|
+
get interactive() {
|
|
220
|
+
return this.tty;
|
|
221
|
+
}
|
|
222
|
+
/** Cursor sequence that returns to the top of the panel and clears it. */
|
|
223
|
+
clearPanel() {
|
|
224
|
+
if (this.lines <= 0)
|
|
225
|
+
return "";
|
|
226
|
+
const up = this.lines - 1; // cursor is on the panel's last line
|
|
227
|
+
return (up > 0 ? `${CSI}${up}F` : "\r") + `${CSI}0J`;
|
|
228
|
+
}
|
|
229
|
+
render(s, force = false) {
|
|
230
|
+
this.last = s;
|
|
231
|
+
const now = Date.now();
|
|
232
|
+
if (!force && now - this.lastPaint < PROGRESS_MS)
|
|
233
|
+
return;
|
|
234
|
+
this.lastPaint = now;
|
|
235
|
+
if (!this.tty) {
|
|
236
|
+
if (force || now - this.lastStatus >= 10_000) {
|
|
237
|
+
this.lastStatus = now;
|
|
238
|
+
const targetKnown = isFinite(s.target);
|
|
239
|
+
const where = s.bytesTotal > 0
|
|
240
|
+
? ` ${pct(s.bytesDone / s.bytesTotal)} of corpus`
|
|
241
|
+
: "";
|
|
242
|
+
process.stderr.write(`[sema] ${bytes(s.trainedBytes)}${targetKnown ? "/" + bytes(s.target) : ""} learned · ${int(s.exampleCount)} examples · ` +
|
|
243
|
+
`${bytes(s.trainedRate)}/s · ${s.fileIndex}/${s.fileTotal} ${s.unitNoun}${where} · ` +
|
|
244
|
+
`${num(s.storeEntries)} entries\n`);
|
|
245
|
+
}
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
const text = renderPanel(s, this.title);
|
|
249
|
+
process.stderr.write(`${this.clearPanel()}${HIDE}${text}`);
|
|
250
|
+
this.lines = text.split("\n").length;
|
|
251
|
+
}
|
|
252
|
+
/** Emit a line (or block) into the scrollback above the panel; the panel is
|
|
253
|
+
* redrawn immediately beneath it so it never disappears between frames. */
|
|
254
|
+
log(msg) {
|
|
255
|
+
if (!this.tty) {
|
|
256
|
+
process.stderr.write(`${msg}\n`);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
let out = `${this.clearPanel()}${msg}\n`;
|
|
260
|
+
this.lines = 0;
|
|
261
|
+
if (this.last) {
|
|
262
|
+
const text = renderPanel(this.last, this.title);
|
|
263
|
+
out += `${HIDE}${text}`;
|
|
264
|
+
this.lines = text.split("\n").length;
|
|
265
|
+
}
|
|
266
|
+
process.stderr.write(out);
|
|
267
|
+
}
|
|
268
|
+
dispose() {
|
|
269
|
+
if (this.tty)
|
|
270
|
+
process.stderr.write(`${SHOW}\n`);
|
|
271
|
+
}
|
|
272
|
+
}
|
package/dist/src/geometry.d.ts
CHANGED
|
@@ -86,6 +86,26 @@ export declare function dominates(partLen: number, wholeLen: number): boolean;
|
|
|
86
86
|
* recallByResonance trusting a climb anchor, and commitVotes admitting a
|
|
87
87
|
* further point of attention. Defined once here so the two can never
|
|
88
88
|
* drift apart. Derived from N, never tuned. */
|
|
89
|
+
/** SUPERPOSITION CAPACITY — how many quasi-orthogonal terms one vector can
|
|
90
|
+
* carry before an individual term stops being readable. `√D`.
|
|
91
|
+
*
|
|
92
|
+
* A superposition of m unit signatures has ‖acc‖² ≈ m, so one term's
|
|
93
|
+
* contribution to any cosine taken against that vector is ≈ 1/m. Setting
|
|
94
|
+
* that against the representation's own floor {@link estimatorNoise} = 1/√D:
|
|
95
|
+
*
|
|
96
|
+
* 1/m < 1/√D ⟺ m > √D
|
|
97
|
+
*
|
|
98
|
+
* Past √D terms a single shared constituent can no longer move a halo cosine
|
|
99
|
+
* above quantisation noise — and because the result is normalized, each extra
|
|
100
|
+
* term also shrinks every ALREADY-accepted term toward that floor. So this is
|
|
101
|
+
* not a budget that trades accuracy for time: beyond capacity, more evidence
|
|
102
|
+
* makes the representation strictly worse. It composes with
|
|
103
|
+
* {@link significanceBar} (3/√D) as it should — three shared units of √D is
|
|
104
|
+
* exactly the significance bar.
|
|
105
|
+
*
|
|
106
|
+
* Consumer: `companyProfile` (mind/learning.ts), which sizes its constituent
|
|
107
|
+
* sketch at this capacity instead of a visit budget. */
|
|
108
|
+
export declare function profileCapacity(D: number): number;
|
|
89
109
|
export declare function consensusFloor(N: number): number;
|
|
90
110
|
/** The coverage bar for the reach (interior) index, when vector-similarity
|
|
91
111
|
* gating is used. Returns the concept threshold — the structural midpoint
|
package/dist/src/geometry.js
CHANGED
|
@@ -121,6 +121,28 @@ export function dominates(partLen, wholeLen) {
|
|
|
121
121
|
* recallByResonance trusting a climb anchor, and commitVotes admitting a
|
|
122
122
|
* further point of attention. Defined once here so the two can never
|
|
123
123
|
* drift apart. Derived from N, never tuned. */
|
|
124
|
+
/** SUPERPOSITION CAPACITY — how many quasi-orthogonal terms one vector can
|
|
125
|
+
* carry before an individual term stops being readable. `√D`.
|
|
126
|
+
*
|
|
127
|
+
* A superposition of m unit signatures has ‖acc‖² ≈ m, so one term's
|
|
128
|
+
* contribution to any cosine taken against that vector is ≈ 1/m. Setting
|
|
129
|
+
* that against the representation's own floor {@link estimatorNoise} = 1/√D:
|
|
130
|
+
*
|
|
131
|
+
* 1/m < 1/√D ⟺ m > √D
|
|
132
|
+
*
|
|
133
|
+
* Past √D terms a single shared constituent can no longer move a halo cosine
|
|
134
|
+
* above quantisation noise — and because the result is normalized, each extra
|
|
135
|
+
* term also shrinks every ALREADY-accepted term toward that floor. So this is
|
|
136
|
+
* not a budget that trades accuracy for time: beyond capacity, more evidence
|
|
137
|
+
* makes the representation strictly worse. It composes with
|
|
138
|
+
* {@link significanceBar} (3/√D) as it should — three shared units of √D is
|
|
139
|
+
* exactly the significance bar.
|
|
140
|
+
*
|
|
141
|
+
* Consumer: `companyProfile` (mind/learning.ts), which sizes its constituent
|
|
142
|
+
* sketch at this capacity instead of a visit budget. */
|
|
143
|
+
export function profileCapacity(D) {
|
|
144
|
+
return Math.max(1, Math.floor(Math.sqrt(D)));
|
|
145
|
+
}
|
|
124
146
|
export function consensusFloor(N) {
|
|
125
147
|
return Math.log(N) + 1 / 2;
|
|
126
148
|
}
|
|
@@ -87,6 +87,12 @@ export interface ConsensusAnchorTrace {
|
|
|
87
87
|
passesNaturalBreak?: boolean;
|
|
88
88
|
passesConsensusFloor?: boolean;
|
|
89
89
|
pastLeadingSaturation?: boolean;
|
|
90
|
+
/** Committed because its margin from the dominant is inside the estimator's
|
|
91
|
+
* own resolution — the co-dominant band in {@link commitVotes}. Recorded
|
|
92
|
+
* because commit decisions are kept in the exact shape the gates applied
|
|
93
|
+
* them: a root admitted this way must never read, in the trace, as one
|
|
94
|
+
* that cleared the two vote gates. */
|
|
95
|
+
tiedWithDominant?: boolean;
|
|
90
96
|
rejectionReasons: AnchorRejectionReason[];
|
|
91
97
|
};
|
|
92
98
|
}
|