Imagine preparing an exam from a collection of documents. Several questions were written from each document. If questions from the same document appear both in practice and in the final exam, a good score may partly reflect familiarity with that source. This is an illustrative reason to examine a split, not a measured result from a deployed model.
Machine-learning experiments often use three collections: training data for fitting, validation data for choosing settings, and test data for final evaluation. Splitting individual rows at random can separate records that the experimenter intended to keep together. I built this planner to make that boundary visible before training starts. You supply each record's group, such as its source document or template family; the tool keeps that group whole.
There is another complication: different groups can contain exactly the same source text. The planner connects those groups and assigns the entire connected component together. It keeps every record and leaves conflicting labels visible. Similar-looking text after normalization is only a review warning; the tool does not decide that it means the same thing.
The frozen synthetic example shows the trade-off. A requested 80/10/10 split of 36 records suggests 28.8, 3.6 and 3.6 rows. Whole groups cannot be divided into fractional rows: the actual plan is 30/3/3. The report shows that difference instead of dropping records to make the percentages look better. It also keeps one normalized-text warning and one conflicting-label set, so the result needs review.
The planning rule makes one greedy pass. I retained a small counterexample where an exhaustive search finds a better balance. This tool therefore offers an inspectable proposal, with downloadable assignments and an audit trail, rather than an optimality certificate. It cannot discover trustworthy groups for you or prove that a test set is independent. The exact method, counterexample and original auditor are below.
Create train, validation and test assignments while keeping declared groups together. Exact duplicate source texts also connect groups into indivisible allocation units. Inspect achieved row and per-label ratios, then export all assigned records for the original Python split auditor.
This is version 0.2 of the same Split Boundary Audit work. The unchanged, independently accepted 0.1 CLI package is included in cli/, with its original manifest. The browser adds planning and a local command-line adapter; it does not claim that a particular split is statistically independent or free of all contamination.
python3 -m http.server 8000 --bind 127.0.0.1
Open the localhost address. Enter grouped JSONL, choose three positive integer percentages totaling 100, and plan. A modern browser with ES modules, Unicode normalization and Web Crypto is required; use HTTPS or localhost for the input digest. This is a static app with no build step, package installation or external scripts. The local preparation checks described here were collected on 2026-11-04 and do not include HF cloud execution.
{"id":"q-1","group":"document-a","label":"question","text":"How does this work?"}
{"id":"q-2","group":"document-a","label":"question","text":"A sibling from document a."}
{"id":"b-1","group":"document-b","label":"bug","text":"A different source."}
{"id":"e-1","group":"document-c","label":"example","text":"An example source."}
Every record needs exactly id, group, label and text. Ids must be unique. A group is the caller's indivisible evaluation unit, such as a document or template family. Labels and population cohorts are not automatically suitable groups. The planner cannot discover trustworthy groups from ungrouped data.
Input limits are 1–5,000 valid records, 2 MiB UTF-8 input, 128 UTF-16 units for each id/group/label, and 16,384 units for each text. Fields must be nonblank Unicode scalar strings. Blank lines are counted and ignored; literal U+2028 within JSON strings is preserved because records split on LF only. Native JSON.parse keeps the last duplicate object key; this input parser does not detect duplicate keys within a line. Percentages and seed must be integers; the seed range is 0–4,294,967,295.
Changing data, percentages or seed clears previous results and download links. The input digest is SHA-256 of the exact UTF-8 editor text, including whitespace and blank lines; a revision check prevents an old async digest from installing a plan after an edit.
The planner starts with declared groups. When two records have exactly equal raw source text, it connects their groups. All groups in a connected component are allocated together. For example, group A shares one exact text with B, and B shares another with C: A, B and C form one allocation unit.
This is an explicit co-location constraint on identical strings. No rows are deleted, labels are not changed, and normalization is not used to create those edges. An exact text with different labels remains in the data and produces a review warning.
NFC followed by Unicode-whitespace collapse is used only for review candidates across completed splits. Different raw texts with the same normalized form are flagged for inspection. BOM U+FEFF is retained; the whitespace set matches Python's original auditor, including U+0085 and U+001C–U+001F. Such candidates are not automatically merged or declared semantically identical.
At least three allocation units are needed for three nonempty splits. If exact-text links leave fewer than three units, planning fails without producing a partial split. Every label need not appear in every split; zero cells remain visible.
The method uses one deterministic greedy pass, without optimization restarts. Units are ordered by descending row count, then unsigned FNV-1a of UTF-8 (decimal seed + NUL + unit key), then the unit key's Unicode code points. The unit key is its smallest declared group id in code-point order. Label sums use code-point order too.
For each unit, the planner chooses the eligible split with the smallest increase in the following fixed balance objective:
E = sum_s (n_s - N*r_s)^2 / (N * max(1, N*r_s))
+ (1/L) * sum_label sum_s
(n_label,s - N_label*r_s)^2
/ (N_label * max(1, N_label*r_s))
N is the total record count, L the distinct-label count, and r_s the requested percentage divided by 100. The first term balances total rows; the second balances labels with equal per-label weight. Targets stay fractional. Delta scoring changes only affected cells; the final objective is recalculated from the completed counts.
If the remaining units equal the number of empty splits, choices are restricted to those empty splits. This guarantees three nonempty splits when at least three units exist, and can worsen balance. Exact computed-score ties use train, validation, test order. Floating-point algebra or normalization-version changes can affect reproducibility; no bitwise result guarantee is made across implementations. Within this implementation, input row permutation preserves assignments, while exports keep the original input order.
The result is not guaranteed to minimize E. reference-results.json retains a concrete 13-record, four-group counterexample with targets 34/33/33 and seed 71: greedy E=0.1930676083, exhaustive optimum E=0.1775424801. Both preserve group integrity and nonempty splits. No record or difficult example was removed to improve balance.
The frozen example has 36 authored synthetic records, 12 declared template-family groups and three labels. Two disjoint exact-text links reduce them to ten allocation units. With targets 80/10/10 and seed 17:
| Split | Fractional target rows | Actual rows | Percentage-point deviation |
|---|---|---|---|
| train | 28.8 | 30 | +3.33 |
| validation | 3.6 | 3 | -1.67 |
| test | 3.6 | 3 | -1.67 |
All 36 records are retained. There are zero declared-group or exact-text crossings, one normalized cross-split candidate set, and one conflicting-label exact-text set. The plan is REVIEW. The unchanged Python auditor also returns REVIEW for its normalized candidate; it does not inspect labels, so the planner's additional label conflict is a separate check.
SAMPLE-SNAPSHOT.json records the sample hash before implementation. This small constructed example and the arithmetic checks are development evidence, not a general split-quality benchmark. The original 0.1 CLI fixture is a separate synthetic audit example, not extra independent data for this planning result.
The plan report includes configuration, objective, actual and target counts, allocation units, every id/group/label/split assignment, review warnings and the greedy trace. It does not contain source texts. Assigned JSONL includes every source text, with exactly the original auditor's fields: id, split, group, text. Labels remain in the report, joined by unique id. Review either file before sharing; ids and labels can also be sensitive.
Use the same planner headlessly with Node.js standard libraries:
node run_plan.mjs sample.jsonl --output fresh-plan --percentages 80,10,10 --seed 17
python3 cli/split_audit.py fresh-plan/assigned-records.jsonl --output fresh-audit
The adapter reads only the specified UTF-8 file and writes a new output directory. It rejects invalid UTF-8 and refuses existing output directories. Exit 0 means a complete plan, including REVIEW; exit 2 means input/output failure. If a filesystem write fails, any partial output is not a completed plan. No filenames or URLs inside records are followed.
In the browser, inputs and settings stay local. Initial loading requests fixed assets and sample.jsonl. Planning, edits and Blob downloads make no network calls. There is no storage, telemetry, model invocation or training. Hosting still receives ordinary asset requests. Downloading assigned JSONL is the explicit action that saves its full source texts.
CLEAR only means the shown bounded checks found no flag. It cannot certify correct group design, semantic disjointness, representative class coverage, independence or absence of other data contamination. Retaining whole groups also does not guarantee the requested ratios.
Node.js and Python standard libraries only:
node test_planner.mjs
python3 verify_reference.py
python3 verify_unicode.py
cd cli
python3 -m unittest test_splits -v
cd ..
The unchanged CLI tests expect the cli/ working directory. Run checks in a working copy: the Node tests and arithmetic verifier regenerate sample-plan.json, sample-assigned.jsonl and reference-results.json. Keep a frozen release candidate unchanged for review.
Eight Node groups check linked components, conservation, nonempty outputs, schema/Unicode/byte/row limits, prototype-shaped ids, fixed-score arithmetic and input-order behavior. An additional 100 permutations of the sample preserved all assignments and the objective. The independent Python oracle checked 100 small 3–6-group cases, 1,244 considered deltas, ordering, nonempty constraints and exhaustive best objectives. It found 30 nonoptimal greedy cases in that constructed set; this is not a population failure-rate estimate. The full first counterexample is retained. A separate normalization check compares all 29 Python whitespace code points and seven other boundary vectors.
Actual local browser tests planned and downloaded the sample and custom Unicode data, then passed the real JSONL bytes through the unchanged Python CLI. The per-record source strings, normalized-candidate member sets and sample core result matched. Config and data edits cleared exports; invalid ratios and schema left no partial downloads. Script-shaped text was displayed literally. A 5,000-record/5,000-label UI case retained all rows, rendered 5,000 class rows and allocation units, and produced both download links. Desktop and 390-pixel layouts were inspected with no document overflow. These are local checks, not a hosted HF build, universal security assurance or evidence of another user's adoption.
设想你用一批资料出考题,每份资料各出几道。如果同一份资料的题同时进入练习和考试,考试成绩可能部分反映对来源的熟悉。这个例子用于解释为什么要检查数据划分,不是某个模型已经发生泄漏的实测结论。
训练资料用于拟合模型,验证资料用于选择设置,测试资料用于最后评估。这个工具要求你先说明哪些记录属于不可拆的一组,例如同一来源文档或模板族,然后整组分配。它不会从没有分组的信息中猜出可靠的组,也不能把类别相同当作来源相同。
如果不同组出现完全相同的原文,工具会把这些组连起来,一起分配。全部记录仍然保留,标签冲突也不会自动修掉。规范化后看起来一样的文字只提醒你复核,不据此认定语义相同。
固定示例有36条合成记录。目标80/10/10对应28.8、3.6、3.6条,但整组不能切成小数,最终得到30、3、3条。报告把比例偏差、一个规范化候选和一个标签冲突都展示出来,状态是需要复核。它没有通过丢记录把比例修饰得更漂亮。
我还保留了一个小反例:穷举能找到比这套单遍贪心规则更好的平衡方案。因此它交付的是可检查、可导出、能交给原审计程序复核的划分建议,不保证最优、统计独立或排除所有污染。下面保留完整方法与限制。
给已经声明好group的资料生成训练、验证、测试划分。整组不可拆,完全相同文本会把组连成更大的分配单位;规范化后相同的不同文本只提醒,不自动合并。原样保留全部记录、混合标签和比例偏差,导出的JSONL能交给原Python审计工具继续检查,标签保存在报告里。
默认36条合成样本的目标是80/10/10,实际30/3/3,没有为凑比例丢记录。一个规范化候选和一个相同文字标签冲突都保留,显示REVIEW。该单遍贪心方法有已保存的非最优反例,不能宣传成最优划分、语义无泄漏或统计独立证明。group应由资料来源或模板族等不可拆单位决定,缺失时不能随便用类别或每条记录的id冒充。
这是Split Boundary Audit同作品的浏览器规划扩展,不另计新项目。本包记录2026-11-04本地制作与验收准备的证据;公开云端运行另以实际发布回执为准。