@hviana/sema 0.5.9 → 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 (79) hide show
  1. package/AGENTS.md +20 -4
  2. package/DATASETS.md +12 -11
  3. package/dist/example/train_base/cache.d.ts +35 -0
  4. package/dist/example/train_base/cache.js +211 -0
  5. package/dist/example/train_base/config.d.ts +21 -0
  6. package/dist/example/train_base/config.js +94 -0
  7. package/dist/example/train_base/corpora/aya.d.ts +19 -0
  8. package/dist/example/train_base/corpora/aya.js +76 -0
  9. package/dist/example/train_base/corpora/converted-parquet.d.ts +14 -0
  10. package/dist/example/train_base/corpora/converted-parquet.js +44 -0
  11. package/dist/example/train_base/corpora/genknow.d.ts +14 -0
  12. package/dist/example/train_base/corpora/genknow.js +83 -0
  13. package/dist/example/train_base/corpora/index.d.ts +29 -0
  14. package/dist/example/train_base/corpora/index.js +81 -0
  15. package/dist/example/train_base/corpora/massive.d.ts +7 -0
  16. package/dist/example/train_base/corpora/massive.js +98 -0
  17. package/dist/example/train_base/corpora/oasst2.d.ts +52 -0
  18. package/dist/example/train_base/corpora/oasst2.js +120 -0
  19. package/dist/example/train_base/corpora/smolsent.d.ts +23 -0
  20. package/dist/example/train_base/corpora/smolsent.js +156 -0
  21. package/dist/example/train_base/corpora/soda.d.ts +12 -0
  22. package/dist/example/train_base/corpora/soda.js +113 -0
  23. package/dist/example/train_base/corpora/taskmaster.d.ts +15 -0
  24. package/dist/example/train_base/corpora/taskmaster.js +144 -0
  25. package/dist/example/train_base/corpora/wiki2.d.ts +23 -0
  26. package/dist/example/train_base/corpora/wiki2.js +132 -0
  27. package/dist/example/train_base/corpus.d.ts +88 -0
  28. package/dist/example/train_base/corpus.js +65 -0
  29. package/dist/example/train_base/discovery.d.ts +48 -0
  30. package/dist/example/train_base/discovery.js +143 -0
  31. package/dist/example/train_base/http.d.ts +82 -0
  32. package/dist/example/train_base/http.js +219 -0
  33. package/dist/example/train_base/items.d.ts +46 -0
  34. package/dist/example/train_base/items.js +98 -0
  35. package/dist/example/train_base/main.d.ts +4 -0
  36. package/dist/example/train_base/main.js +207 -0
  37. package/dist/example/train_base/progress.d.ts +34 -0
  38. package/dist/example/train_base/progress.js +114 -0
  39. package/dist/example/train_base/readers.d.ts +125 -0
  40. package/dist/example/train_base/readers.js +391 -0
  41. package/dist/example/train_base/runtime.d.ts +115 -0
  42. package/dist/example/train_base/runtime.js +637 -0
  43. package/dist/example/train_base/stage.d.ts +3 -0
  44. package/dist/example/train_base/stage.js +246 -0
  45. package/dist/example/train_base/ui.d.ts +88 -0
  46. package/dist/example/train_base/ui.js +272 -0
  47. package/dist/src/mind/mind.d.ts +1 -1
  48. package/dist/src/mind/mind.js +1 -1
  49. package/example/train_base/cache.ts +251 -0
  50. package/example/train_base/config.ts +128 -0
  51. package/example/train_base/corpora/aya.ts +106 -0
  52. package/example/train_base/corpora/converted-parquet.ts +64 -0
  53. package/example/train_base/corpora/genknow.ts +114 -0
  54. package/example/train_base/corpora/index.ts +88 -0
  55. package/example/train_base/corpora/massive.ts +111 -0
  56. package/example/train_base/corpora/oasst2.ts +163 -0
  57. package/example/train_base/corpora/smolsent.ts +203 -0
  58. package/example/train_base/corpora/soda.ts +130 -0
  59. package/example/train_base/corpora/taskmaster.ts +217 -0
  60. package/example/train_base/corpora/wiki2.ts +190 -0
  61. package/example/train_base/corpus.ts +150 -0
  62. package/example/train_base/discovery.ts +203 -0
  63. package/example/train_base/http.ts +284 -0
  64. package/example/train_base/items.ts +118 -0
  65. package/example/train_base/main.ts +240 -0
  66. package/example/train_base/progress.ts +149 -0
  67. package/example/train_base/readers.ts +505 -0
  68. package/example/train_base/runtime.ts +894 -0
  69. package/example/train_base/stage.ts +276 -0
  70. package/example/train_base/ui.ts +333 -0
  71. package/jsr.json +1 -1
  72. package/package.json +2 -4
  73. package/src/mind/mind.ts +1 -1
  74. package/test/13-conversation.test.mjs +1 -1
  75. package/test/84-composed-answer-honesty.test.mjs +2 -1
  76. package/test/88-dependency-footprint.test.mjs +99 -0
  77. package/dist/example/train_base.d.ts +0 -163
  78. package/dist/example/train_base.js +0 -3220
  79. package/example/train_base.ts +0 -3882
@@ -0,0 +1,88 @@
1
+ // train_base/corpora/index.ts — THE CURRICULUM.
2
+ //
3
+ // The order is the curriculum, and it is load-bearing: each stage runs only
4
+ // after the previous one finishes, and every stage records itself in the same
5
+ // completed-set, so a single store resumes the whole sequence.
6
+ //
7
+ // 1. SmolSent sentence-level TRANSLATION pairs across 100+ low-resource
8
+ // languages. Each pair is "two names for one meaning" → a
9
+ // foreign→English translation FACT, so every language's
10
+ // rendering of a meaning converges on ONE English node (cf.
11
+ // test/05-concepts.test.mjs).
12
+ // 2. Aya ~204k human prompt→completion pairs, 70+ languages → one
13
+ // (question → answer) FACT each.
14
+ // 3. oasst2 MULTI-TURN human↔assistant conversation trees → the
15
+ // accumulated-context walk (single-turn trees are skipped).
16
+ // 4. Taskmaster task-oriented DIALOGUE, the best-scoring corpora on the
17
+ // fold-unit recurrence benchmark that predicts halo health.
18
+ // 5. 2Wiki the `evidences` TRIPLES — the one stage aimed at
19
+ // COMPOSITION. Its Wikipedia passages and its composed
20
+ // questions are deliberately NOT read.
21
+ // 6. SODA social/commonsense DIALOGUE, budgeted.
22
+ // 7. MASSIVE short intent utterances → ONE bare experience each.
23
+ // DISABLED BY DEFAULT — edge-less content was measured to
24
+ // manufacture answers where the store should stay silent.
25
+ // 8. GenKnow ~37.6k {Question, Answer} pairs → one FACT each. DISABLED
26
+ // BY DEFAULT on licence grounds; see DATASETS.md §3.2.
27
+
28
+ import type { Corpus } from "../corpus.js";
29
+ import { smolsent } from "./smolsent.js";
30
+ import { aya } from "./aya.js";
31
+ import { oasst2 } from "./oasst2.js";
32
+ import { taskmaster } from "./taskmaster.js";
33
+ import { wiki2 } from "./wiki2.js";
34
+ import { soda } from "./soda.js";
35
+ import { massive } from "./massive.js";
36
+ import { genknow } from "./genknow.js";
37
+
38
+ export const CURRICULUM: Corpus[] = [
39
+ smolsent,
40
+ aya,
41
+ oasst2,
42
+ taskmaster,
43
+ wiki2,
44
+ soda,
45
+ massive,
46
+ genknow,
47
+ ];
48
+
49
+ /** The corpora this RUN will train, for the panel header — derived, so it can
50
+ * never go stale the way the hand-written string it replaces had. */
51
+ export const enabledLabels = (): string =>
52
+ CURRICULUM.filter((c) => c.enabled).map((c) => c.label).join("+");
53
+
54
+ /** The corpora a STORE contains: everything this run will train PLUS everything
55
+ * an earlier run already did, in curriculum order.
56
+ *
57
+ * Not the same question as `enabledLabels`, and conflating them wrote a false
58
+ * statement into every store that was ever resumed with a different set of
59
+ * stages enabled. Observed on a real store: `train.dataset` read
60
+ * "SmolSent+Aya+oasst2" while the tally recorded 37,623 General-Knowledge
61
+ * deposits sitting in it. That is not cosmetic — a Sema store retains its
62
+ * training text VERBATIM, so `train.dataset` is the record of whose licence
63
+ * terms travel with the artifact, and General-Knowledge is precisely the
64
+ * corpus DATASETS.md §3.2 disables on NonCommercial grounds.
65
+ *
66
+ * `trainedIds` are the corpus ids an earlier run deposited under (the keys of
67
+ * the per-corpus tally). An id no longer in the curriculum cannot be named and
68
+ * is dropped — the tally still carries it, which is where that evidence lives. */
69
+ export const storedLabels = (trainedIds: Iterable<string>): string => {
70
+ const trained = new Set(trainedIds);
71
+ return CURRICULUM
72
+ .filter((c) => c.enabled || trained.has(c.id))
73
+ .map((c) => c.label)
74
+ .join("+");
75
+ };
76
+
77
+ // One re-export per corpus, carrying the descriptor AND its adapters. The
78
+ // explicit `export { aya, genknow, … }` list that used to sit here named the
79
+ // eight descriptors a second time; the star exports below already provide them,
80
+ // and a hand-kept list of everything is exactly the thing that goes stale.
81
+ export * from "./smolsent.js";
82
+ export * from "./aya.js";
83
+ export * from "./oasst2.js";
84
+ export * from "./taskmaster.js";
85
+ export * from "./wiki2.js";
86
+ export * from "./soda.js";
87
+ export * from "./massive.js";
88
+ export * from "./genknow.js";
@@ -0,0 +1,111 @@
1
+ // train_base/corpora/massive.ts — AmazonScience/massive short intents
2
+ //
3
+ // Knobs, the row adapter, and the stage descriptor for ONE corpus, together
4
+ // with the evidence that fixed each default. A corpus file owns everything
5
+ // source-specific; the contract it fills is ../corpus.ts and the loop that runs
6
+ // it is ../stage.ts.
7
+
8
+ import { env } from "../config.js";
9
+ import { refineItems, type TrainingItem } from "../items.js";
10
+ import { parquet } from "../readers.js";
11
+ import type { Corpus } from "../corpus.js";
12
+ import { convertedParquetUnits } from "./converted-parquet.js";
13
+
14
+ // MASSIVE deposits BARE UTTERANCES — an experience, not an episode — and that
15
+ // is the only shape its data supports. Two richer shapes were considered and
16
+ // rejected on evidence:
17
+ // • Same-intent pairs as paraphrases. 49.1% of consecutive rows share
18
+ // (locale, intent), but they are NOT meaning-equivalent: intent 48 in mn-MN
19
+ // runs "wake me at nine on the fifth" next to "set an alarm two hours from
20
+ // now". Depositing that pair as an episode teaches a continuation that does
21
+ // not exist.
22
+ // • Same-id rows across locales. Those ARE translations of one another —
23
+ // which is exactly SmolSent's relation, and SmolSent scores worst of every
24
+ // corpus measured on fold-unit recurrence (23.2%) because cross-lingual
25
+ // pairs share no units.
26
+ // So the stage contributes recurring fold units and lexical coverage (65.1%
27
+ // recurring unit mass, median 29 B) and nothing relational. `annot_utt` carries
28
+ // slot markup ("[date : tavdahad] ...") and is never read.
29
+ // DISABLED BY DEFAULT, on evidence gathered after the stage was written. A bare
30
+ // experience deposits content with NO EDGE, and that cuts both ways. Measured on
31
+ // a three-pair dialogue store with and without six MASSIVE-style utterances:
32
+ //
33
+ // "set an alarm" without: "Sure, what size would you like?" (wrong)
34
+ // with: "set an alarm for seven" (better)
35
+ // "play music" without: "" (correct silence)
36
+ // with: "Yes, sweetened or unsweetened?" (wrong)
37
+ //
38
+ // So it displaces some wrong answers and manufactures others, INCLUDING turning
39
+ // a correct silence into a wrong answer — and honest silence is a stated
40
+ // property of this engine (AGENTS §2.13). On the mixed-curriculum store the
41
+ // same shape produced the fragment "nus" for "wake me up at nine am".
42
+ //
43
+ // That evidence is four probes on toy stores and is NOT conclusive; it is,
44
+ // however, the only evidence there is, and it points the wrong way. The stage
45
+ // stays implemented and one env var away. Turn it on (MASSIVE=1) once there is
46
+ // a real measurement showing the recurring fold units it contributes (72.3% of
47
+ // deposited unit mass) buy more than the spurious answers cost.
48
+ const MASSIVE = env("MASSIVE", "0") !== "0";
49
+ const MASSIVE_DATASET = env("MASSIVE_DATASET", "AmazonScience/massive");
50
+ // "all" is the config covering every locale in one set of shards.
51
+ const MASSIVE_CONFIG = env("MASSIVE_CONFIG", "all");
52
+ const MASSIVE_SPLITS = env("MASSIVE_SPLITS", "train")
53
+ .split(",").map((s) => s.trim()).filter(Boolean);
54
+ // 0 = every row (587,214 in `all`/train, ~17 MB of content).
55
+ const MASSIVE_MAX_ROWS = Math.max(
56
+ 0,
57
+ Math.floor(Number(env("MASSIVE_MAX_ROWS", "0"))) || 0,
58
+ );
59
+ const MAX_MASSIVE_UTT_CHARS = Math.max(
60
+ 100,
61
+ Math.floor(Number(env("MAX_MASSIVE_UTT_KB", "2")) * 1000) || 2_000,
62
+ );
63
+
64
+ // ═══════════════════════════════════════════════════════════════════════
65
+ // §6e⁗ MASSIVE parsing — one short utterance → ONE SEMA experience
66
+ //
67
+ // See the constants note for why this deposits a bare experience and not a
68
+ // relation: the two relational shapes this corpus appears to offer are both
69
+ // false (same-intent rows are not paraphrases; same-id rows across locales are
70
+ // translations, SmolSent's worst-scoring relation).
71
+ // ═══════════════════════════════════════════════════════════════════════
72
+
73
+ /** Translate ONE MASSIVE row into SEMA items: its bare utterance, as an
74
+ * experience. `annot_utt` (slot-annotated) is deliberately not used — its
75
+ * "[date : ...]" markup is not prose. Returns [] for an unusable row. */
76
+ export function massiveRowToItems(
77
+ row: unknown,
78
+ maxChars = MAX_MASSIVE_UTT_CHARS,
79
+ ): TrainingItem[] {
80
+ if (!row || typeof row !== "object") return [];
81
+ const utt = (row as Record<string, unknown>).utt;
82
+ const text = typeof utt === "string" ? utt.trim() : "";
83
+ if (!text || text.length > maxChars) return [];
84
+ return refineItems([text]);
85
+ }
86
+
87
+ export const massive: Corpus = {
88
+ id: "massive",
89
+ label: "MASSIVE",
90
+ kind: "short intents",
91
+ enabled: MASSIVE,
92
+ maxRows: MASSIVE_MAX_ROWS,
93
+ // One of ten columns — `annot_utt`, whose slot markup the note above rejects,
94
+ // is not decoded either. Measured on the converted train shard: 92 MB
95
+ // uncompressed across all ten, 36 MB for `utt` (38.7%).
96
+ read: parquet({ columns: ["utt"] }),
97
+ toItems: (row) => {
98
+ const items = massiveRowToItems(row);
99
+ return items.length ? items : null;
100
+ },
101
+ unitNoun: "shard(s)",
102
+ log: { rows: "row(s)" },
103
+ discover: convertedParquetUnits({
104
+ id: "massive",
105
+ label: "MASSIVE",
106
+ dataset: MASSIVE_DATASET,
107
+ config: MASSIVE_CONFIG,
108
+ splits: MASSIVE_SPLITS,
109
+ localSub: "massive",
110
+ }),
111
+ };
@@ -0,0 +1,163 @@
1
+ // train_base/corpora/oasst2.ts — OpenAssistant/oasst2 conversation trees
2
+ //
3
+ // Knobs, the row adapter, and the stage descriptor for ONE corpus, together
4
+ // with the evidence that fixed each default. A corpus file owns everything
5
+ // source-specific; the contract it fills is ../corpus.ts and the loop that runs
6
+ // it is ../stage.ts.
7
+
8
+ import { env } from "../config.js";
9
+ import { accumulate, refineItems, type TrainingItem } from "../items.js";
10
+ import { lines } from "../readers.js";
11
+ import { type Corpus, singleUnit } from "../corpus.js";
12
+
13
+ // ── OpenAssistant/oasst2 (the fourth training stage, after Aya) ──
14
+ // oasst2 is a corpus of human↔assistant conversation TREES. Its richest, most
15
+ // stream-friendly artifact is "<date>_oasst2_ready.trees.jsonl.gz": one JSON
16
+ // conversation tree PER LINE, gzip-compressed (a web standard — Decompression
17
+ // Stream("gzip")). Each tree is {message_tree_id, prompt:{role,text,replies:[…]}}
18
+ // where `replies` nests recursively and a prompt can have several ranked
19
+ // assistant replies (rank 0 = best). We follow the best-ranked, non-deleted
20
+ // reply at each step to get ONE linear, strictly-alternating conversation per
21
+ // tree, then keep only the MULTI-TURN ones (≥ OASST_MIN_TURNS messages, i.e. at
22
+ // least two full user→assistant exchanges) — single Q→A trees are skipped, by
23
+ // design. OASST=0 disables the stage; OASST_URL overrides the source.
24
+ const OASST = env("OASST", "1") !== "0";
25
+ const OASST_URL = env(
26
+ "OASST_URL",
27
+ "https://huggingface.co/datasets/OpenAssistant/oasst2/resolve/main/2023-11-05_oasst2_ready.trees.jsonl.gz",
28
+ );
29
+ // Multi-turn threshold: a conversation must have at least this many turns to be
30
+ // trained (4 = user→assistant→user→assistant, the smallest real multi-turn).
31
+ const OASST_MIN_TURNS = Math.max(
32
+ 2,
33
+ Math.floor(Number(env("OASST_MIN_TURNS", "4"))) || 4,
34
+ );
35
+ // Skip a tree whose decoded JSON line exceeds this (a pathological record); the
36
+ // real maximum is far smaller, so this only guards against corruption.
37
+ const MAX_OASST_LINE_CHARS = Math.max(
38
+ 100_000,
39
+ Math.floor(Number(env("MAX_OASST_LINE_MB", "8")) * 1_000_000) || 8_000_000,
40
+ );
41
+
42
+ // ═══════════════════════════════════════════════════════════════════════
43
+ // §6e OpenAssistant/oasst2 parsing — a conversation TREE → SEMA items
44
+ //
45
+ // Each tree is {prompt:{role,text,replies:[…]}}, replies nested recursively. A
46
+ // prompt can have several ranked assistant replies; we collapse the tree to ONE
47
+ // linear conversation by following the best-ranked (rank 0), non-deleted reply
48
+ // at each step. The result strictly alternates prompter/assistant. Only MULTI-
49
+ // TURN conversations (≥ OASST_MIN_TURNS messages) are kept — the explicit focus
50
+ // of this stage; single Q→A trees are dropped.
51
+ // ═══════════════════════════════════════════════════════════════════════
52
+
53
+ /** A single oasst2 message node (the fields we use; the tree nests via replies). */
54
+ interface OasstNode {
55
+ role?: string;
56
+ text?: string;
57
+ rank?: number | null;
58
+ deleted?: boolean;
59
+ replies?: OasstNode[];
60
+ }
61
+
62
+ /** One conversational turn extracted from a tree. */
63
+ export interface OasstTurn {
64
+ role: string; // "prompter" | "assistant"
65
+ text: string;
66
+ }
67
+
68
+ /** Collapse a conversation tree to ONE linear path: at each node, descend into
69
+ * its best-ranked, non-deleted reply (rank 0 preferred; unranked sorts last).
70
+ * Returns the ordered turns (already strictly alternating in this corpus). */
71
+ export function bestOasstPath(root: OasstNode): OasstTurn[] {
72
+ const turns: OasstTurn[] = [];
73
+ let node: OasstNode | undefined = root;
74
+ while (node) {
75
+ const text = typeof node.text === "string" ? node.text.trim() : "";
76
+ if (text) turns.push({ role: String(node.role ?? "?"), text });
77
+ const live: OasstNode[] = (node.replies ?? []).filter((r: OasstNode) =>
78
+ r && !r.deleted && typeof r.text === "string" && r.text.trim() !== ""
79
+ );
80
+ if (live.length === 0) break;
81
+ live.sort((a: OasstNode, b: OasstNode) =>
82
+ (a.rank ?? Number.MAX_SAFE_INTEGER) - (b.rank ?? Number.MAX_SAFE_INTEGER)
83
+ );
84
+ node = live[0];
85
+ }
86
+ return turns;
87
+ }
88
+
89
+ /** Translate ONE multi-turn oasst2 conversation into SEMA training items.
90
+ *
91
+ * This is the ONE stage where cumulative continuous context is truly necessary:
92
+ * the data is a real multi-turn dialogue, and what must be learned is how each
93
+ * turn follows from the WHOLE conversation so far — not from the previous turn
94
+ * alone. The conversation is emitted ONLY as the accumulated walk; standalone
95
+ * turn experiences and local adjacent-pair facts are NOT emitted (they are
96
+ * subsumed by it and would merely replicate the content).
97
+ *
98
+ * The walk is the pattern proven in test/13-conversation.test.mjs
99
+ * ("teachConversation"): each turn is the continuation of all prior turns,
100
+ * with BARE turn text — NO "User:/Assistant:" labels. The SHAPE is identical
101
+ * (cumulative context → next turn); the join string is not, and does not need
102
+ * to be — that file joins with nothing and this corpus joins with "\n" (see
103
+ * `accumulate`). Saying "byte-for-byte", as this comment used to, invites the
104
+ * reading that the two must agree on a separator. They must not agree,
105
+ * because there is nothing to agree about: turn boundaries are offsets, and
106
+ * the join string is just corpus text. Roles already
107
+ * alternate by position in an oasst2 best-path (the root is a prompter), so a
108
+ * label adds nothing the position does not, while a clean continuation matches
109
+ * the test's recall (predictNext queries bare prior turns) and lets a turn share
110
+ * its gist with the same text elsewhere (e.g. an Aya question stored bare).
111
+ *
112
+ * Returns [] for a conversation below the multi-turn threshold, so callers can
113
+ * simply skip empties. */
114
+ export function oasstConversationToItems(
115
+ turns: OasstTurn[],
116
+ minTurns = OASST_MIN_TURNS,
117
+ ): TrainingItem[] {
118
+ if (turns.length < minTurns) return []; // not multi-turn — skip
119
+ return refineItems(accumulate(turns.map((t) => t.text)));
120
+ }
121
+
122
+ /** The row adapter: ONE line of the tree dump → its deposits. Returns null for
123
+ * a tree with no prompt and for every single-turn tree — the latter is the
124
+ * stage's design, not a defect, which is why the reader counts it `unusable`
125
+ * rather than `skipped`. */
126
+ export function oasstTreeToItems(row: unknown): TrainingItem[] | null {
127
+ if (!row || typeof row !== "object") return null;
128
+ const tree = row as { prompt?: OasstNode };
129
+ if (!tree.prompt) return null;
130
+ const items = oasstConversationToItems(bestOasstPath(tree.prompt));
131
+ return items.length ? items : null;
132
+ }
133
+
134
+ export const oasst2: Corpus = {
135
+ id: "oasst2",
136
+ label: "oasst2",
137
+ kind: "multi-turn chat",
138
+ enabled: OASST,
139
+ read: lines({ gzip: true, maxLineChars: MAX_OASST_LINE_CHARS }),
140
+ toItems: oasstTreeToItems,
141
+ // The one corpus that KEEPS a cached file after a complete read: a copy left
142
+ // behind by a previous interrupted run is not this stage's to reclaim.
143
+ keepCached: true,
144
+ log: {
145
+ deposits: "examples",
146
+ // A tree that yields items is exactly a tree that cleared OASST_MIN_TURNS.
147
+ rows: "conversation(s)",
148
+ // The single-turn trees this stage drops BY DESIGN must not be reported as
149
+ // damage — only genuinely malformed lines are.
150
+ malformedOnly: true,
151
+ },
152
+ discover: singleUnit({
153
+ // Resume id "oasst2::trees" — the string this store already records.
154
+ key: "trees",
155
+ label: "oasst2",
156
+ display: "oasst2 (multi-turn)",
157
+ url: OASST_URL,
158
+ dest: "oasst2_ready.trees.jsonl.gz",
159
+ acquireLabel: "oasst2 trees",
160
+ localMatch: [/oasst.*trees.*\.jsonl\.gz$/i, /oasst.*\.jsonl\.gz$/i],
161
+ localWhat: "oasst2 *trees*.jsonl.gz",
162
+ }),
163
+ };
@@ -0,0 +1,203 @@
1
+ // train_base/corpora/smolsent.ts — google/smol sentence pairs
2
+ //
3
+ // Knobs, the row adapter, and the stage descriptor for ONE corpus, together
4
+ // with the evidence that fixed each default. A corpus file owns everything
5
+ // source-specific; the contract it fills is ../corpus.ts and the loop that runs
6
+ // it is ../stage.ts.
7
+
8
+ import { env, LOCAL_PATH } from "../config.js";
9
+ import { refineItems, type TrainingItem } from "../items.js";
10
+ import { hfTree, type Listed, localFiles } from "../discovery.js";
11
+ import { lines } from "../readers.js";
12
+ import type { Corpus, Unit } from "../corpus.js";
13
+ import type { TrainCtx } from "../runtime.js";
14
+ import { basename, join } from "node:path";
15
+
16
+ // ── google/smol · SmolSent (the first training stage) ──
17
+ // SmolSent is Google's sentence-level translation set: ~863 human sentence pairs
18
+ // per language pair across 100+ low-resource languages, cc-by-4.0 (commercial-
19
+ // friendly). Each row is {sl, tl, src, trg, …} — a source sentence and its
20
+ // translation. A pair is "two names for one meaning", which is exactly the
21
+ // cross-language concept SEMA fuses (see test/05-concepts.test.mjs), so each row
22
+ // becomes FACTS that bind the two phrasings as one concept at recall time.
23
+ //
24
+ // The corpus ships as one plain JSONL file PER language pair under smolsent/ in
25
+ // the HF repo (e.g. smolsent/ha_en.jsonl). We DOWNLOAD each file and stream its
26
+ // lines — far faster and free of the rate-limiting that per-row API paging hit.
27
+ // The file list is discovered from the HF repo tree. SMOLSENT=0 disables the
28
+ // stage; SMOLSENT_PAIRS (comma-separated basenames without .jsonl, e.g.
29
+ // "ha_en,zu_en") restricts to a chosen subset.
30
+ const SMOLSENT = env("SMOLSENT", "1") !== "0";
31
+ const SMOLSENT_DATASET = env("SMOLSENT_DATASET", "google/smol");
32
+ const SMOLSENT_PAIRS = (process.env.SMOLSENT_PAIRS ?? "")
33
+ .split(",").map((s) => s.trim()).filter(Boolean);
34
+ // The resume id PREFIX for the SmolSent stage; one completed-files entry per
35
+ // file (e.g. "smolsent::ha_en.jsonl").
36
+ const SMOLSENT_ID = "smolsent";
37
+ // Which direction(s) of a translation pair to deposit. Was effectively "both",
38
+ // and that is now the default NO longer, for a reason measured rather than
39
+ // assumed.
40
+ //
41
+ // SmolSent's English side is a SHARED POOL translated into every language: row
42
+ // id 0 of smolsent/ha_en.jsonl, zu_en.jsonl and am_en.jsonl all carry the SAME
43
+ // `trg` ("It allows me to work by following my vibes and ..."). The two
44
+ // directions are therefore not symmetric at all:
45
+ //
46
+ // src2trg (foreign -> English) many distinct contexts -> ONE shared
47
+ // continuation. Every language's rendering of
48
+ // a meaning converges on the same English
49
+ // node — the cross-language concept fusion
50
+ // this stage exists for.
51
+ // trg2src (English -> foreign) ONE context -> 100+ DIFFERENT continuations,
52
+ // one per language file. The same English
53
+ // sentence is deposited over and over with a
54
+ // different answer each time.
55
+ //
56
+ // So dropping trg2src is not merely a corpus-size economy (it halves the
57
+ // largest stage, which was 60.9% of all examples in the last trained store); it
58
+ // removes a genuine ambiguity pathology. Set SMOLSENT_DIRECTIONS=both to
59
+ // restore the old behaviour, or trg2src for English->foreign only.
60
+ //
61
+ // WHAT THE CUT DOES NOT DO, measured on a three-pair store: asking the English
62
+ // sentence still ANSWERS with a foreign rendering, because the engine can reach
63
+ // a shared continuation's predecessors on its own. What is removed is the
64
+ // DEPOSITED forward ambiguity — one context carrying ~100 competing
65
+ // continuations — not every reverse association.
66
+ const SMOLSENT_DIRECTIONS = env("SMOLSENT_DIRECTIONS", "src2trg")
67
+ .trim().toLowerCase();
68
+ const SMOLSENT_SRC2TRG = SMOLSENT_DIRECTIONS !== "trg2src";
69
+ const SMOLSENT_TRG2SRC = SMOLSENT_DIRECTIONS === "trg2src" ||
70
+ SMOLSENT_DIRECTIONS === "both";
71
+ // A SmolSent side longer than this is skipped (a sentence pair is short; a huge
72
+ // value is corruption, not a sentence).
73
+ const MAX_SMOLSENT_CHARS = Math.max(
74
+ 2_000,
75
+ Math.floor(Number(env("MAX_SMOLSENT_KB", "16")) * 1000) || 16_000,
76
+ );
77
+
78
+ // ═══════════════════════════════════════════════════════════════════════
79
+ // §6c SmolSent parsing — a translation pair → SEMA facts
80
+ //
81
+ // Each SmolSent row is {sl, tl, src, trg, …}: a source sentence and its
82
+ // translation into another language — "two names for one meaning". This is the
83
+ // cross-language concept SEMA fuses (see test/05-concepts.test.mjs: "ice" and
84
+ // "hielo" become one concept because they share company, so a fact about one
85
+ // transfers to the other). So a pair is rendered as BIDIRECTIONAL translation
86
+ // FACTS — (src → trg) and (trg → src) — binding the two phrasings as one
87
+ // concept at recall time, in both directions. Two facts, no experiences and no
88
+ // cumulative walk: a sentence pair is not multi-turn, and a bare sentence on its
89
+ // own carries no relation to point at.
90
+ // ═══════════════════════════════════════════════════════════════════════
91
+
92
+ /** One normalized SmolSent row. */
93
+ export interface SmolSentRow {
94
+ src: string; // source sentence
95
+ trg: string; // its translation
96
+ sl: string; // source language code
97
+ tl: string; // target language code
98
+ }
99
+
100
+ /** Normalize a raw datasets-server row into a SmolSentRow, or null when it lacks
101
+ * both sides or a side is implausibly large (a dump, not a sentence). */
102
+ export function toSmolSentRow(
103
+ row: unknown,
104
+ maxChars = MAX_SMOLSENT_CHARS,
105
+ ): SmolSentRow | null {
106
+ if (!row || typeof row !== "object") return null;
107
+ const r = row as Record<string, unknown>;
108
+ const src = typeof r.src === "string" ? r.src.trim() : "";
109
+ // `trg` is a single string in smolsent; tolerate a list form defensively.
110
+ const trgRaw = Array.isArray(r.trgs) ? r.trgs[0] : r.trg;
111
+ const trg = typeof trgRaw === "string" ? trgRaw.trim() : "";
112
+ if (!src || !trg) return null;
113
+ if (
114
+ src.length > maxChars || trg.length > maxChars
115
+ ) return null;
116
+ const sl = typeof r.sl === "string" ? r.sl.trim() : "";
117
+ const tl = typeof r.tl === "string" ? r.tl.trim() : "";
118
+ return { src, trg, sl, tl };
119
+ }
120
+
121
+ /** Translate ONE SmolSent pair into SEMA facts. The two sentences are one
122
+ * meaning in two languages, but the two BINDINGS are not equally sound —
123
+ * SmolSent's English side is a shared pool translated into every language, so
124
+ * `trg -> src` gives one English context a different answer in every language
125
+ * file. See SMOLSENT_DIRECTIONS. refineItems drops the degenerate case where
126
+ * src === trg. */
127
+ export function smolSentRowToItems(
128
+ row: SmolSentRow,
129
+ dirs: { src2trg: boolean; trg2src: boolean } = {
130
+ src2trg: SMOLSENT_SRC2TRG,
131
+ trg2src: SMOLSENT_TRG2SRC,
132
+ },
133
+ ): TrainingItem[] {
134
+ const { src, trg } = row;
135
+ const items: TrainingItem[] = [];
136
+ if (dirs.src2trg) items.push({ context: src, continuation: trg });
137
+ if (dirs.trg2src) items.push({ context: trg, continuation: src });
138
+ return refineItems(items);
139
+ }
140
+
141
+ /** Discover the SmolSent per-pair JSONL files from the HF repo tree, restricted
142
+ * to SMOLSENT_PAIRS (basenames without .jsonl) when set. Each entry is the
143
+ * repo-relative path, e.g. "smolsent/ha_en.jsonl". */
144
+ async function listFiles(ctx: TrainCtx): Promise<Listed[]> {
145
+ const paths = await hfTree(
146
+ SMOLSENT_DATASET,
147
+ "smolsent",
148
+ /\.jsonl$/i,
149
+ `GET smol tree`,
150
+ ctx.http,
151
+ );
152
+ if (!SMOLSENT_PAIRS.length) return paths;
153
+ const want = new Set(SMOLSENT_PAIRS.map((p) => p.replace(/\.jsonl$/i, "")));
154
+ return paths.filter((p) =>
155
+ want.has(basename(p.path).replace(/\.jsonl$/i, ""))
156
+ );
157
+ }
158
+
159
+ const unit = (name: string): Unit => ({
160
+ key: name,
161
+ // The log names a pair by its language code alone; the panel keeps the file.
162
+ name: name.replace(/\.jsonl$/i, ""),
163
+ display: `SmolSent ${name}`,
164
+ });
165
+
166
+ export const smolsent: Corpus = {
167
+ id: SMOLSENT_ID,
168
+ label: "SmolSent",
169
+ kind: "translation",
170
+ enabled: SMOLSENT,
171
+ unitNoun: "translation file(s)",
172
+ // The *4 is chars-to-a-generous-JSON-envelope: the guard bounds ONE side of
173
+ // a pair, the line carries both plus its keys.
174
+ read: lines({ maxLineChars: MAX_SMOLSENT_CHARS * 4 }),
175
+ toItems: (row) => {
176
+ const r = toSmolSentRow(row);
177
+ return r ? smolSentRowToItems(r) : null;
178
+ },
179
+ async discover(ctx) {
180
+ // Work-list: local *.jsonl in LOCAL_PATH, else the repo's smolsent/ files.
181
+ if (LOCAL_PATH) {
182
+ let names = localFiles(LOCAL_PATH, /\.jsonl$/i);
183
+ if (SMOLSENT_PAIRS.length) {
184
+ const want = new Set(
185
+ SMOLSENT_PAIRS.map((p) => p.replace(/\.jsonl$/i, "")),
186
+ );
187
+ names = names.filter((n) => want.has(n.path.replace(/\.jsonl$/i, "")));
188
+ }
189
+ return names.map((n) => ({
190
+ ...unit(n.path),
191
+ local: join(LOCAL_PATH, n.path),
192
+ bytes: n.size,
193
+ }));
194
+ }
195
+ return (await listFiles(ctx)).map(({ path, size }) => ({
196
+ ...unit(basename(path)),
197
+ // owner/name and the file path are URL PATH segments — do not encode "/".
198
+ url:
199
+ `https://huggingface.co/datasets/${SMOLSENT_DATASET}/resolve/main/${path}`,
200
+ bytes: size,
201
+ }));
202
+ },
203
+ };