/** * @copyright Sister Software * @license AGPL-3.0 * @author Teffen Ellis, et al. * * Train / val / test split with **locality holdout** per the Phase 1 plan. * * The corpus's val + test sets are not randomly sampled rows — they're entire low-density regions * held out so the model cannot memorize them at training time. Rationale (per the plan's "Common * pitfalls" section): random splits leak by neighborhood — a model fed "13 Main St, Springfield, * IL" in train and "15 Main St, Springfield, IL" in test generalizes via region/locality * memorization, not by learning the underlying schema. * * Phase 1 holdouts (chosen for low data density + administrative isolation): * * - **US**: Vermont, Wyoming, North Dakota * - **FR**: Corse, Lozère, Creuse * * Held-out rows are deterministically split 50/50 between val and test by hashing the row's * `source_id`. Non-held-out rows go to train. The 90/5/5 ratio is approximate — what matters is * the locality boundary, not the exact split percentages. * * The output is a `SplitManifest`: three `string[]` arrays of `source_id`. Manifests live in git * (under `corpus/splits//`) so reruns are reproducible bit-for-bit. */ import type { CanonicalRow, LabeledRow } from "@mailwoman/corpus/types"; export type SplitName = "train" | "val" | "test"; export interface SplitOptions { /** * Region-name → holdout policy, keyed by ISO 3166-1 alpha-2 country. The values are the region-component strings the * splitter looks for in `row.components.region`. Override to change the holdout for an experiment; defaults to * `defaultHoldouts()`. */ holdouts?: Record; } /** * Output manifest: source_id lists per split. */ export interface SplitManifest { train: string[]; val: string[]; test: string[]; /** * Echoes the holdouts used, so the manifest is self-describing. */ holdouts: Record; /** * Corpus version stamped onto the manifest. Read from the first row. */ corpus_version: string; /** * Counts for quick sanity checks. */ counts: { train: number; val: number; test: number; total: number; }; } /** * Phase 1 default holdouts (per plan). * * - US: Vermont, Wyoming, North Dakota (low density, easy to identify in WOF/admin sources). * - FR: Corse, Lozère, Creuse (small departments / regions). * - DE (added 2026-06-11, night-11): Saarland + Mecklenburg-Vorpommern — small Länder so the training cost is low while * the slice clears the honest-eval 1000-row trust floor. DE has had NO trustable honest-eval slice since the harness * shipped (flagged 2026-06-08); this takes effect at the NEXT base corpus rebuild — existing versioned corpora keep * their committed SPLIT_MANIFESTs (a holdout added after a corpus is built is leakage-laundering, not a holdout). */ export declare function defaultHoldouts(): Record; type SplitInputRow = Pick; /** * Pure per-row split decision. Used by both the in-memory `splitRows` and by the streaming `buildCorpus` align loop * (`build.ts`) to decide each row's split without retaining the row in heap. Identical hash bucketing semantics to the * array-based path so the decision is stable regardless of caller. */ export declare function splitForRow(row: Pick, holdouts?: Record): SplitName; /** * Compute a `SplitManifest` from an iterable of labeled (or canonical) rows. Both shapes are accepted — only * `source_id`, `country`, `corpus_version`, and `components.region` are consulted. * * Retained for in-memory callers (tests; small-scale fixture runs). Real-data builds via `buildCorpus` use the * streaming path (`splitForRow` + `writeSplitManifestsFromLabeledFiles`) to avoid materializing every aligned row's * split membership in heap. */ export declare function splitRows(rows: Iterable, opts?: SplitOptions): SplitManifest; /** * Lightweight deterministic 0..(n-1) bucket from a string id. */ /** * Deterministic bucket for a stable id. * * Stays on raw `createHash` rather than `sha256Hex` from `@mailwoman/core/utils`: it needs the digest BYTES, and the * shared helper returns hex. Re-parsing hex back into bytes to reach the same four octets would cost more than the one * line it saves. */ export declare function hashBucket(id: string, n: number): number; /** * Write a `SplitManifest` to `/{train,val,test}.json`. The manifests are line-separated source_id lists (one * id per line) so they diff cleanly in git. Also writes `/MANIFEST.json` with the full structured manifest * including holdouts + counts + corpus version. * * Reruns produce byte-identical files (the underlying `splitRows` is deterministic). */ export declare function writeSplitManifests(manifest: SplitManifest, outputDir: string): Promise; /** * Type re-export for callers that want to ingest LabeledRow specifically. */ export type SplitInputLabeledRow = Pick; /** * Streaming variant of `writeSplitManifests`: derives the per-split source-id .txt manifests + `SPLIT_MANIFEST.json` by * streaming three per-split labeled-row JSONL files (one per split). Memory cost is O(1) — `sort(1)` from coreutils * handles the deterministic sort with disk spill for files that exceed in-memory thresholds. * * Used by `buildCorpus` after the align loop has already partitioned labeled rows into `labeled-{train,val,test}.jsonl` * via `splitForRow`. Counts are pre-computed by the align loop and passed in (zero re-scan). */ export declare function writeSplitManifestsFromLabeledFiles(opts: { labeledPaths: Record; outputDir: string; corpusVersion: string; counts: Record; holdouts?: Record; }): Promise; export {}; //# sourceMappingURL=split.d.ts.map