@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.
Files changed (108) hide show
  1. package/AGENTS.md +43 -4
  2. package/DATASETS.md +160 -0
  3. package/README.md +12 -0
  4. package/dist/example/train_base/cache.d.ts +35 -0
  5. package/dist/example/train_base/cache.js +211 -0
  6. package/dist/example/train_base/config.d.ts +21 -0
  7. package/dist/example/train_base/config.js +94 -0
  8. package/dist/example/train_base/corpora/aya.d.ts +19 -0
  9. package/dist/example/train_base/corpora/aya.js +76 -0
  10. package/dist/example/train_base/corpora/converted-parquet.d.ts +14 -0
  11. package/dist/example/train_base/corpora/converted-parquet.js +44 -0
  12. package/dist/example/train_base/corpora/genknow.d.ts +14 -0
  13. package/dist/example/train_base/corpora/genknow.js +83 -0
  14. package/dist/example/train_base/corpora/index.d.ts +29 -0
  15. package/dist/example/train_base/corpora/index.js +81 -0
  16. package/dist/example/train_base/corpora/massive.d.ts +7 -0
  17. package/dist/example/train_base/corpora/massive.js +98 -0
  18. package/dist/example/train_base/corpora/oasst2.d.ts +52 -0
  19. package/dist/example/train_base/corpora/oasst2.js +120 -0
  20. package/dist/example/train_base/corpora/smolsent.d.ts +23 -0
  21. package/dist/example/train_base/corpora/smolsent.js +156 -0
  22. package/dist/example/train_base/corpora/soda.d.ts +12 -0
  23. package/dist/example/train_base/corpora/soda.js +113 -0
  24. package/dist/example/train_base/corpora/taskmaster.d.ts +15 -0
  25. package/dist/example/train_base/corpora/taskmaster.js +144 -0
  26. package/dist/example/train_base/corpora/wiki2.d.ts +23 -0
  27. package/dist/example/train_base/corpora/wiki2.js +132 -0
  28. package/dist/example/train_base/corpus.d.ts +88 -0
  29. package/dist/example/train_base/corpus.js +65 -0
  30. package/dist/example/train_base/discovery.d.ts +48 -0
  31. package/dist/example/train_base/discovery.js +143 -0
  32. package/dist/example/train_base/http.d.ts +82 -0
  33. package/dist/example/train_base/http.js +219 -0
  34. package/dist/example/train_base/items.d.ts +46 -0
  35. package/dist/example/train_base/items.js +98 -0
  36. package/dist/example/train_base/main.d.ts +4 -0
  37. package/dist/example/train_base/main.js +207 -0
  38. package/dist/example/train_base/progress.d.ts +34 -0
  39. package/dist/example/train_base/progress.js +114 -0
  40. package/dist/example/train_base/readers.d.ts +125 -0
  41. package/dist/example/train_base/readers.js +391 -0
  42. package/dist/example/train_base/runtime.d.ts +115 -0
  43. package/dist/example/train_base/runtime.js +637 -0
  44. package/dist/example/train_base/stage.d.ts +3 -0
  45. package/dist/example/train_base/stage.js +246 -0
  46. package/dist/example/train_base/ui.d.ts +88 -0
  47. package/dist/example/train_base/ui.js +272 -0
  48. package/dist/src/geometry.d.ts +20 -0
  49. package/dist/src/geometry.js +22 -0
  50. package/dist/src/mind/attention.d.ts +6 -0
  51. package/dist/src/mind/attention.js +44 -4
  52. package/dist/src/mind/learning.js +134 -50
  53. package/dist/src/mind/mechanisms/cast.js +45 -1
  54. package/dist/src/mind/mind.d.ts +7 -2
  55. package/dist/src/mind/mind.js +15 -3
  56. package/dist/src/mind/reasoning.js +59 -5
  57. package/dist/src/mind/recognition.js +29 -3
  58. package/dist/src/mind/traverse.d.ts +16 -0
  59. package/dist/src/mind/traverse.js +18 -0
  60. package/dist/src/store-sqlite.d.ts +4 -0
  61. package/dist/src/store-sqlite.js +47 -0
  62. package/dist/src/store.d.ts +7 -0
  63. package/example/train_base/cache.ts +251 -0
  64. package/example/train_base/config.ts +128 -0
  65. package/example/train_base/corpora/aya.ts +106 -0
  66. package/example/train_base/corpora/converted-parquet.ts +64 -0
  67. package/example/train_base/corpora/genknow.ts +114 -0
  68. package/example/train_base/corpora/index.ts +88 -0
  69. package/example/train_base/corpora/massive.ts +111 -0
  70. package/example/train_base/corpora/oasst2.ts +163 -0
  71. package/example/train_base/corpora/smolsent.ts +203 -0
  72. package/example/train_base/corpora/soda.ts +130 -0
  73. package/example/train_base/corpora/taskmaster.ts +217 -0
  74. package/example/train_base/corpora/wiki2.ts +190 -0
  75. package/example/train_base/corpus.ts +150 -0
  76. package/example/train_base/discovery.ts +203 -0
  77. package/example/train_base/http.ts +284 -0
  78. package/example/train_base/items.ts +118 -0
  79. package/example/train_base/main.ts +240 -0
  80. package/example/train_base/progress.ts +149 -0
  81. package/example/train_base/readers.ts +505 -0
  82. package/example/train_base/runtime.ts +894 -0
  83. package/example/train_base/stage.ts +276 -0
  84. package/example/train_base/ui.ts +333 -0
  85. package/jsr.json +1 -1
  86. package/package.json +2 -4
  87. package/src/geometry.ts +23 -0
  88. package/src/mind/attention.ts +54 -1
  89. package/src/mind/learning.ts +137 -43
  90. package/src/mind/mechanisms/cast.ts +48 -1
  91. package/src/mind/mind.ts +13 -2
  92. package/src/mind/reasoning.ts +64 -5
  93. package/src/mind/recognition.ts +29 -3
  94. package/src/mind/traverse.ts +19 -0
  95. package/src/store-sqlite.ts +53 -0
  96. package/src/store.ts +28 -0
  97. package/test/13-conversation.test.mjs +1 -1
  98. package/test/29-counterfactual.test.mjs +43 -6
  99. package/test/77-company-saturation.test.mjs +302 -0
  100. package/test/78-atom-hub-recognition-cliff.test.mjs +135 -0
  101. package/test/84-composed-answer-honesty.test.mjs +137 -0
  102. package/test/85-answered-directly.test.mjs +126 -0
  103. package/test/86-cast-voices-committed.test.mjs +164 -0
  104. package/test/87-codominant-commitment.test.mjs +250 -0
  105. package/test/88-dependency-footprint.test.mjs +99 -0
  106. package/dist/example/train_base.d.ts +0 -93
  107. package/dist/example/train_base.js +0 -2269
  108. package/example/train_base.ts +0 -2735
@@ -0,0 +1,391 @@
1
+ // train_base/readers.ts — bytes on disk → rows → deposits.
2
+ //
3
+ // A READER knows a container format (newline-delimited JSON, a JSON array, a
4
+ // Parquet file) and nothing about any corpus. It is handed a row ADAPTER and
5
+ // deposits whatever that adapter returns. The pairing is free: any corpus can
6
+ // use any reader, which is the whole point of separating them.
7
+ //
8
+ // Every reader stops at an ITEM boundary — never mid-deposit — on any of three
9
+ // signals: the run's abort signal, `shouldStop` (a stage budget), or `onExample`
10
+ // returning false (the MAX_MB cap). A stopped read leaves its file un-completed
11
+ // so a resume re-reads it from the top, which is safe because deposition is
12
+ // idempotent.
13
+ //
14
+ // THE ONLY THIRD-PARTY CODE IN THIS REPOSITORY IS BELOW, and it is LAZILY
15
+ // LOADED. Sema itself imports nothing outside `node:` — that is a product
16
+ // property, not an accident (AGENTS.md §6) — and this trainer is an EXAMPLE,
17
+ // not part of the library. hyparquet (+ its Snappy codec) is therefore a dev
18
+ // dependency, and it is loaded by a dynamic import the first time a Parquet
19
+ // corpus is actually read: a curriculum with no Parquet stage (SmolSent,
20
+ // oasst2, Taskmaster, General-Knowledge) runs with the package absent
21
+ // entirely, and one that needs it fails with a sentence instead of a module
22
+ // resolution error.
23
+ import { isEpisode, itemBytes } from "./items.js";
24
+ import { PARQUET_BATCH_BYTES } from "./config.js";
25
+ import { openAsBlob } from "node:fs";
26
+ /** Deposit a row's items: an experience via ingest(text), an episode via
27
+ * ingest(context, continuation). After each, the per-example callback receives
28
+ * the item's UTF-8 content size — the quantity the scaling suite
29
+ * (14-scaling.test.mjs) reports as a constant KB/s — then gates the global
30
+ * example count and checkpointing (returns false to stop). */
31
+ export async function ingestItems(ci, items, onItem, sample) {
32
+ for (const it of items) {
33
+ if (isEpisode(it))
34
+ await ci.ingest(it.context, it.continuation);
35
+ else
36
+ await ci.ingest(it);
37
+ sample?.(it);
38
+ if (!(await onItem(itemBytes(it))))
39
+ return false; // stop requested
40
+ }
41
+ return true;
42
+ }
43
+ /** Shared tail of every reader: deposit one row's items and keep the counts. */
44
+ async function depositRow(row, toItems, rc, res) {
45
+ const items = toItems(row);
46
+ if (!items || items.length === 0) {
47
+ res.unusable++;
48
+ return true;
49
+ }
50
+ res.rowsUsed++;
51
+ return ingestItems(rc.ci, items, async (contentBytes) => {
52
+ res.examples++;
53
+ return rc.onExample(contentBytes);
54
+ }, rc.sample);
55
+ }
56
+ const blank = () => ({
57
+ examples: 0,
58
+ rowsUsed: 0,
59
+ skipped: 0,
60
+ unusable: 0,
61
+ stopped: false,
62
+ });
63
+ /** Newline-delimited JSON, optionally gzipped.
64
+ *
65
+ * ONE reader serves both the plain JSONL sources and the gzipped oasst2 tree
66
+ * dump: the only difference between them is a `DecompressionStream("gzip")` in
67
+ * the pipeline, and duplicating an 80-line splitter to express that was how
68
+ * the two copies drifted apart in the first place.
69
+ *
70
+ * Lines are split without buffering the whole file OR an unbounded line: a
71
+ * record longer than `maxLineChars` is dropped (counted `skipped`) and the
72
+ * stream continues at the next newline, so a corrupt record can never exhaust
73
+ * memory or abort a good file. */
74
+ export const lines = (opts) => async (filePath, toItems, rc) => {
75
+ const res = blank();
76
+ const blob = await openAsBlob(filePath);
77
+ // gzip is a web standard here too (DecompressionStream), so the compressed
78
+ // and plain forms differ by exactly one pipe stage and nothing else.
79
+ const reader = (opts.gzip
80
+ ? blob.stream()
81
+ .pipeThrough(new DecompressionStream("gzip"))
82
+ .pipeThrough(new TextDecoderStream())
83
+ : blob.stream()
84
+ .pipeThrough(new TextDecoderStream())).getReader();
85
+ let leftover = "", dropping = false;
86
+ // A "row" here is a non-blank line, counted whether or not it parses — so the
87
+ // position is a property of the FILE, reproducible on a later run.
88
+ const skip = rc.startRow ?? 0;
89
+ let rowIndex = 0;
90
+ const processLine = async (line) => {
91
+ if (!line.trim())
92
+ return true;
93
+ rowIndex++;
94
+ // Already deposited by an earlier run: advance the position, touch nothing
95
+ // else. Not parsed and not counted, so this read's numbers describe only
96
+ // the rows it actually trained.
97
+ if (rowIndex <= skip) {
98
+ rc.onRowDone?.(rowIndex);
99
+ return true;
100
+ }
101
+ if (rc.shouldStop?.())
102
+ return false;
103
+ let row;
104
+ try {
105
+ row = JSON.parse(line);
106
+ }
107
+ catch {
108
+ res.skipped++;
109
+ rc.onRowDone?.(rowIndex); // nothing to deposit — the row is dealt with
110
+ return true;
111
+ }
112
+ const ok = await depositRow(row, toItems, rc, res);
113
+ if (ok)
114
+ rc.onRowDone?.(rowIndex); // every item landed
115
+ return ok;
116
+ };
117
+ try {
118
+ while (true) {
119
+ const { done, value } = await reader.read();
120
+ if (done)
121
+ break;
122
+ let chunk = value;
123
+ for (;;) {
124
+ const nl = chunk.indexOf("\n");
125
+ if (nl < 0) {
126
+ if (!dropping) {
127
+ if (leftover.length + chunk.length > opts.maxLineChars) {
128
+ leftover = "";
129
+ dropping = true;
130
+ res.skipped++;
131
+ }
132
+ else
133
+ leftover += chunk;
134
+ }
135
+ break;
136
+ }
137
+ const part = chunk.slice(0, nl);
138
+ chunk = chunk.slice(nl + 1);
139
+ if (dropping) {
140
+ dropping = false;
141
+ leftover = "";
142
+ continue;
143
+ }
144
+ if (leftover.length + part.length > opts.maxLineChars) {
145
+ leftover = "";
146
+ res.skipped++;
147
+ continue;
148
+ }
149
+ const line = leftover + part;
150
+ leftover = "";
151
+ if (!(await processLine(line))) {
152
+ res.stopped = true;
153
+ return res;
154
+ }
155
+ }
156
+ }
157
+ if (!dropping && leftover.trim()) {
158
+ if (!(await processLine(leftover)))
159
+ res.stopped = true;
160
+ }
161
+ return res;
162
+ }
163
+ finally {
164
+ // CANCEL, not releaseLock: a read that returns early (a budget, the MAX_MB
165
+ // cap, a signal) leaves the file source open otherwise, to be closed
166
+ // whenever the collector gets to it. Measured, that is tidiness rather than
167
+ // a leak — 300 abandoned reads peaked at 42 open descriptors against 38
168
+ // with cancel — but "closed when we are done with it" is the cheaper thing
169
+ // to reason about, and cancel releases the lock too.
170
+ try {
171
+ await reader.cancel();
172
+ }
173
+ catch { /* already closed */ }
174
+ }
175
+ };
176
+ /** A whole-file JSON ARRAY of rows. The arrays this reads are small enough
177
+ * (~16 MB) to parse whole; a huge file would be rejected by the cache ceiling
178
+ * long before this. */
179
+ export const jsonArray = () => async (filePath, toItems, rc) => {
180
+ const res = blank();
181
+ const blob = await openAsBlob(filePath);
182
+ let arr;
183
+ try {
184
+ arr = JSON.parse(await blob.text());
185
+ }
186
+ catch (e) {
187
+ throw new Error(`invalid JSON: ${e.message}`);
188
+ }
189
+ // A file that parses but is not an array is a DIFFERENT CONTAINER, not an
190
+ // empty one, so it is an error rather than zero rows. Silently reading it as
191
+ // empty marked the unit complete, logged "0 facts", and never looked again —
192
+ // and the live path to that is a LOCAL_PATH run, where the Taskmaster stage
193
+ // takes every *.json in the directory while the remote listing filters out
194
+ // exactly the two files (ontology.json, sample.json) that are not arrays of
195
+ // conversations. Throwing leaves the unit un-completed and says which file.
196
+ if (!Array.isArray(arr)) {
197
+ throw new Error(`expected a JSON array of rows, got ${arr === null ? "null" : Array.isArray(arr) ? "array" : typeof arr}`);
198
+ }
199
+ const rows = arr;
200
+ const skip = rc.startRow ?? 0;
201
+ for (let i = 0; i < rows.length; i++) {
202
+ if (i < skip) { // deposited by an earlier run — see ReadContext
203
+ rc.onRowDone?.(i + 1);
204
+ continue;
205
+ }
206
+ if (rc.signal.aborted || rc.shouldStop?.()) {
207
+ res.stopped = true;
208
+ return res;
209
+ }
210
+ if (!(await depositRow(rows[i], toItems, rc, res))) {
211
+ res.stopped = true;
212
+ return res;
213
+ }
214
+ rc.onRowDone?.(i + 1);
215
+ }
216
+ return res;
217
+ };
218
+ /** How many rows to materialise in one read from a row-group of `rgRows` rows
219
+ * occupying `groupBytes` uncompressed bytes, under a `budgetBytes` target.
220
+ *
221
+ * The group's own footer statistics give the mean row width, so the batch
222
+ * follows the CORPUS's row size rather than the writer's layout: wide rows
223
+ * (SODA carries a whole dialogue per row) batch smaller than narrow ones at
224
+ * the same memory cost. Never exceeds the group — a batch is a subdivision of
225
+ * a group, never a span across two, because `parquetReadObjects` is given an
226
+ * absolute row range and column chunks are per-group. Never returns 0, or the
227
+ * read loop could not advance.
228
+ *
229
+ * A writer that omits `total_byte_size` yields `groupBytes <= 0`; the batch is
230
+ * then the whole group, which is exactly the behaviour this replaced. That
231
+ * fallback is safe for every file we read today (all three report it) and
232
+ * degrades to the old memory profile rather than to a wrong result. */
233
+ export function parquetBatchRows(rgRows, groupBytes, budgetBytes) {
234
+ if (!(rgRows > 0))
235
+ return 0; // empty group — the caller skips it
236
+ if (!(groupBytes > 0) || !Number.isFinite(groupBytes))
237
+ return rgRows;
238
+ const perRow = groupBytes / rgRows;
239
+ const fit = Math.floor(budgetBytes / perRow);
240
+ return Math.min(rgRows, Math.max(1, fit));
241
+ }
242
+ /** hyparquet + its Snappy codec, resolved on FIRST USE and remembered.
243
+ *
244
+ * A pure-JS, dependency-free Parquet reader driven over a web-standard Blob
245
+ * byte source. It is the one thing the web platform cannot do alone, and the
246
+ * one package this repository pulls in — so it is loaded here, where it is
247
+ * used, rather than at module scope, where it would become a load-time
248
+ * requirement of the whole trainer. */
249
+ let parquetLib = null;
250
+ const loadParquet = () => (parquetLib ??= (async () => {
251
+ try {
252
+ const [hp, hc] = await Promise.all([
253
+ import("hyparquet"),
254
+ import("hyparquet-compressors"),
255
+ ]);
256
+ return {
257
+ metadata: hp.parquetMetadataAsync,
258
+ readObjects: hp.parquetReadObjects,
259
+ compressors: hc.compressors,
260
+ };
261
+ }
262
+ catch (e) {
263
+ // A missing package here is a SETUP problem with a specific remedy, so say
264
+ // the remedy. Anything else (a genuine load error inside the package) is
265
+ // rethrown untouched.
266
+ const code = e?.code;
267
+ if (code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND") {
268
+ throw new Error("this corpus ships as Parquet and needs the hyparquet reader, which " +
269
+ "is a DEV dependency of this example (the library itself has none). " +
270
+ "Run `npm install` in the repository, or disable the Parquet " +
271
+ "stages: AYA=0 WIKI2=0 SODA=0 MASSIVE=0.");
272
+ }
273
+ throw e;
274
+ }
275
+ })());
276
+ /** The top-level column names a Parquet file actually carries, taken from the
277
+ * chunk metadata (`path_in_schema[0]` is the top-level name, which is exactly
278
+ * what hyparquet matches a `columns` request against). */
279
+ const columnsOf = (meta) => {
280
+ const out = new Set();
281
+ for (const rg of meta.row_groups) {
282
+ for (const c of rg.columns) {
283
+ const top = c.meta_data?.path_in_schema?.[0];
284
+ if (top)
285
+ out.add(top);
286
+ }
287
+ }
288
+ return out;
289
+ };
290
+ /** Reject a column projection that names a column the file does not have.
291
+ *
292
+ * hyparquet ignores an unknown name in `columns` rather than complaining, so a
293
+ * typo would read NO columns, hand every adapter an empty row, and finish with
294
+ * "0 facts" and no error at all. A projection is a claim about the file, so a
295
+ * wrong claim is worth stopping for — and the message names both what is
296
+ * missing and what is there, which is what you need to fix it. */
297
+ function checkColumns(available, want) {
298
+ const missing = want.filter((c) => !available.has(c));
299
+ if (missing.length > 0) {
300
+ throw new Error(`column(s) not in this file: ${missing.join(", ")} — ` +
301
+ `it carries ${[...available].join(", ")}`);
302
+ }
303
+ return want;
304
+ }
305
+ /** Parquet, read in bounded row batches with hyparquet (+Snappy from
306
+ * hyparquet-compressors) over a web-standard Blob byte source. At most
307
+ * `batchBytes` of source rows are materialised at a time, so neither a
308
+ * multi-hundred-MB file nor a file written as ONE giant row-group loads whole
309
+ * into memory.
310
+ *
311
+ * Batching also makes a single-group file INTERRUPTIBLE: the abort check runs
312
+ * per batch, where before a 1.19M-row group could not be cancelled at all.
313
+ *
314
+ * `columns` PROJECTS the read down to the columns the adapter actually uses.
315
+ * That is not only a memory economy: 2Wiki's `context` column holds the
316
+ * Wikipedia prose the adapter exists to avoid depositing, and naming the
317
+ * columns makes that exclusion structural — the bytes are never decoded at
318
+ * all — in the same way reading only `utterances[].text` structurally excludes
319
+ * Taskmaster's `instructions` scaffolding. Absent ⇒ every column, as before. */
320
+ export const parquet = (opts = {}) => async (filePath, toItems, rc) => {
321
+ const { metadata, readObjects, compressors } = await loadParquet();
322
+ const budget = opts.batchBytes ?? PARQUET_BATCH_BYTES;
323
+ const res = blank();
324
+ const blob = await openAsBlob(filePath);
325
+ const file = {
326
+ byteLength: blob.size,
327
+ slice: async (start, end) => await blob.slice(start, end ?? blob.size).arrayBuffer(),
328
+ };
329
+ const meta = await metadata(file);
330
+ const columns = opts.columns
331
+ ? checkColumns(columnsOf(meta), opts.columns)
332
+ : undefined;
333
+ // A "row" here is the file's ABSOLUTE row index, which is what hyparquet's
334
+ // rowStart/rowEnd already speak in — so resuming is not merely cheaper than
335
+ // re-reading, it decodes nothing at all before the cursor.
336
+ const skip = rc.startRow ?? 0;
337
+ let rowStart = 0;
338
+ for (const rg of meta.row_groups) {
339
+ const rgRows = Number(rg.num_rows);
340
+ const rgEnd = rowStart + rgRows;
341
+ if (rgEnd <= skip) {
342
+ // Entirely behind the cursor: never fetched, never decompressed.
343
+ rowStart = rgEnd;
344
+ rc.onRowDone?.(rowStart);
345
+ continue;
346
+ }
347
+ // `total_byte_size` covers EVERY column, including ones a projection skips,
348
+ // so a projected read materialises less than the budget rather than more.
349
+ // Erring small is the safe direction for a memory budget, and correcting it
350
+ // per-column would tie the batch size to a layout detail for no gain.
351
+ const batchRows = parquetBatchRows(rgRows, Number(rg.total_byte_size ?? 0), budget);
352
+ if (batchRows <= 0)
353
+ continue; // empty group
354
+ // Resume inside a group: begin at the cursor, not at the group's first row.
355
+ if (rowStart < skip)
356
+ rowStart = skip;
357
+ // Materialise one bounded batch at a time, then deposit its rows.
358
+ while (rowStart < rgEnd) {
359
+ if (rc.signal.aborted || rc.shouldStop?.()) {
360
+ res.stopped = true;
361
+ return res;
362
+ }
363
+ const batchStart = rowStart;
364
+ const rowEnd = Math.min(rowStart + batchRows, rgEnd);
365
+ const rows = await readObjects({
366
+ file,
367
+ // Hand back the footer we already parsed: without it every batch
368
+ // re-reads and re-parses the file's metadata, which on a large shard
369
+ // means dozens of redundant footer parses per file.
370
+ metadata: meta,
371
+ compressors,
372
+ columns,
373
+ rowStart,
374
+ rowEnd,
375
+ });
376
+ rowStart = rowEnd;
377
+ for (let i = 0; i < rows.length; i++) {
378
+ if (rc.shouldStop?.()) {
379
+ res.stopped = true;
380
+ return res;
381
+ }
382
+ if (!(await depositRow(rows[i], toItems, rc, res))) {
383
+ res.stopped = true;
384
+ return res;
385
+ }
386
+ rc.onRowDone?.(batchStart + i + 1);
387
+ }
388
+ }
389
+ }
390
+ return res;
391
+ };
@@ -0,0 +1,115 @@
1
+ import { type CachedIngest, type Mind, type Store } from "../../src/index.js";
2
+ import type { TrainingItem } from "./items.js";
3
+ import { type HttpOptions } from "./http.js";
4
+ import type { ReadContext } from "./readers.js";
5
+ import { type SavedProgress } from "./progress.js";
6
+ import { Progress, type ProgState } from "./ui.js";
7
+ /** A single process-wide abort signal. SIGINT/SIGTERM aborts it, which cancels
8
+ * every in-flight fetch immediately (instead of waiting out a slow socket), so
9
+ * Ctrl+C is responsive even mid-download. The deposit loop also polls it to
10
+ * stop cleanly at the next item boundary, leaving the store consistent. */
11
+ export declare const shutdown: AbortController;
12
+ /** The totals a run accumulates. Mutable BY DESIGN — see the file header. */
13
+ export interface Counters {
14
+ depositCount: number;
15
+ trainedContentBytes: number;
16
+ totalBytesProcessed: number;
17
+ totalCorpusBytes: number;
18
+ langTally: Record<string, number>;
19
+ /** Rows a BUDGETED corpus has already taken, by corpus id, counting only
20
+ * units it finished. A budget is a property of the STORE, not of a run, so
21
+ * it has to survive a resume — see the budget notes in stage.ts. Absent from
22
+ * an older store, which reads as 0 and reproduces the previous behaviour. */
23
+ rowsTaken: Record<string, number>;
24
+ }
25
+ export interface TrainCtx {
26
+ readonly store: Store;
27
+ readonly mind: Mind;
28
+ readonly ci: CachedIngest;
29
+ readonly progress: Progress;
30
+ readonly state: ProgState;
31
+ readonly counters: Counters;
32
+ /** Network options for a LISTING call: cancellable, and its throttle waits
33
+ * are surfaced into the run log. Downloads and HEAD probes deliberately do
34
+ * NOT carry the notifier — they wait silently, as they always have. */
35
+ readonly http: HttpOptions;
36
+ /** Set once a stop has been requested (a signal, or the MAX_MB cap). Every
37
+ * stage checks it at its file and item boundaries. */
38
+ stopRequested: boolean;
39
+ stopReason: string;
40
+ /** Repaint the panel. `force` bypasses the frame-rate limiter. */
41
+ tick(force?: boolean): void;
42
+ /** Offer an item to the reservoir behind the checkpoint recall box. */
43
+ sample(it: TrainingItem): void;
44
+ /** The per-deposit gate: counts, checkpoints, and returns false to stop. */
45
+ onDeposit(contentBytes: number): Promise<boolean>;
46
+ /** Bind a reader to this run, for the unit recorded as `unitId`, resuming at
47
+ * `startRow`. `shouldStop` is a stage BUDGET; the MAX_MB cap and the
48
+ * shutdown signal reach the reader by other routes.
49
+ *
50
+ * Naming the unit is what makes the cursor meaningful: a cursor is only ever
51
+ * applied to the unit it was taken from. */
52
+ readCtx(opts: {
53
+ unitId: string;
54
+ /** Which corpus these deposits are tallied to. */
55
+ corpusId: string;
56
+ startRow?: number;
57
+ shouldStop?: () => boolean;
58
+ /** Live budget count for this corpus, for the cursor snapshot only. */
59
+ rowsTakenNow?: () => number;
60
+ }): ReadContext;
61
+ /** The durable position a previous run reached inside a unit it did not
62
+ * finish, or null. A stage applies it only to the matching unit. */
63
+ readonly resumeCursor: UnitCursor | null;
64
+ /** Reuse a cached copy, else download into the cache. Null on failure.
65
+ * `cached` says which happened — the stages disagree about whether a file
66
+ * they did not download is theirs to delete afterwards, so the answer has
67
+ * to reach the caller rather than being decided here. */
68
+ acquire(url: string, destName: string, label: string): Promise<AcquiredFile | null>;
69
+ /** Persist the resume record: completed units, counters, per-corpus tally,
70
+ * and the in-flight unit's cursor. `unitDone` CLEARS the cursor — the unit
71
+ * is recorded in `completedFiles` now, so a position inside it is meaningless
72
+ * and must not be left behind for the next run to apply. */
73
+ persist(completedFiles: string[], unitDone?: boolean): Promise<void>;
74
+ /** Restore counters and the tally from the store, and announce the resume. */
75
+ restore(): Promise<SavedProgress>;
76
+ /** Final checkpoint, summary line, and exit. */
77
+ finish(why: string): Promise<void>;
78
+ }
79
+ /** A durable position inside a unit that is not finished, together with the
80
+ * counters that describe exactly the data behind it.
81
+ *
82
+ * The counters travel WITH the position for one reason: they have to agree. A
83
+ * resume that restored the store's running totals but re-read the unit from the
84
+ * top counted the same deposits twice, and every interruption inflated the
85
+ * figures again — measured at +77% (16,000 examples reported as 28,283) after
86
+ * six interruptions of one corpus. Restoring the pair together makes the
87
+ * reported numbers describe the store, however many times it was interrupted.
88
+ *
89
+ * `rows` is what the reader counts, so its unit is reader-specific — non-blank
90
+ * lines for `lines`, array index for `jsonArray`, absolute file row for
91
+ * `parquet`. That is safe because a corpus never changes reader, and it is why
92
+ * a cursor names the unit it came from. */
93
+ export interface UnitCursor {
94
+ unitId: string;
95
+ rows: number;
96
+ depositCount: number;
97
+ trainedContentBytes: number;
98
+ totalBytesProcessed: number;
99
+ langTally: Record<string, number>;
100
+ rowsTaken: Record<string, number>;
101
+ }
102
+ /** The outcome of {@link TrainCtx.acquire}. */
103
+ export interface AcquiredFile {
104
+ path: string;
105
+ /** True when the file was already in the cache and nothing was fetched. */
106
+ cached: boolean;
107
+ }
108
+ export interface RuntimeOptions {
109
+ store: Store;
110
+ mind: Mind;
111
+ ci: CachedIngest;
112
+ /** Names the curriculum in the panel header. */
113
+ title: string;
114
+ }
115
+ export declare function createRuntime(opts: RuntimeOptions): TrainCtx;