Files
wehub-resource-sync 426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

777 lines
32 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Pack normalized + synthesized records into final {train,val,test}.jsonl.
Operates on the DEPRECATED flat `ElizaRecord` intermediate (see
`scripts/lib/eliza_record.py` and `scripts/normalize.py`), NOT the canonical
Eliza-1 corpus record. The canonical corpus record is `eliza_native_v1`; see
`packages/training/docs/dataset/CANONICAL_RECORD.md`. This path is kept only so
the existing bulk corpus keeps building — new corpus data should be authored as
`eliza_native_v1` rows.
Streaming + reservoir-sampled. We never load all records into RAM —
instead we walk each `data/normalized/<slug>.jsonl` once with two passes:
Pass 1: count records per source and (lazily) collect line-offsets.
Pass 2: reservoir-sample up to `--per-source-cap` records per source,
weighted by the registry's `weight`. Hash on the fly to dedupe.
Stream straight to per-split temp files honoring metadata.split.
The total in-memory footprint is bounded by:
- the dedupe hash set (16 bytes per unique record)
- one reservoir per source (≤ per-source-cap × 1 ref)
- one pass through the file at a time
That keeps us well under a few GB even on the 1.5M agent-trove file.
Usage:
uv run python scripts/pack_dataset.py
uv run python scripts/pack_dataset.py --per-source-cap 75000
uv run python scripts/pack_dataset.py --max-train 1000000
uv run python scripts/pack_dataset.py --no-weights
"""
from __future__ import annotations
import argparse
import hashlib
import json
import logging
import os
import random
import sys
from collections import Counter
from pathlib import Path
import yaml
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT / "scripts"))
from lib.runtime_phases import classify_phase, PHASE_OOB # noqa: E402
NORMALIZED = ROOT / "data" / "normalized"
SYNTHESIZED = ROOT / "data" / "synthesized"
FINAL = ROOT / "data" / "final"
ABLITERATION = ROOT / "data" / "abliteration"
REGISTRY_FILE = ROOT / "datasets.yaml"
# ─────────────────────────── tier table ────────────────────────────
# Source: docs/DATASET_REVIEW.md §"Per-source caps + sampling".
# These are module-level so unit tests can import them. Pass-2 reads
# them via the local `tier_for(slug)` helper, which strips any
# `synth:` prefix before lookup.
TIER_S = { # gold standard — full × 5 replicate
"nubilio-trajectories",
}
TIER_A = { # eliza-aligned bench, take full corpus
"scambench", "scam-defense-corpus",
}
TIER_B = { # tool-call agent traces, structurally salvageable
"tool-reasoning-toucan", "agent-trove", "nemotron-terminal-corpus",
"swebench-verified-opus-47", "mcp-agent-training-data",
"tool-reasoning-coding-nemotron",
}
TIER_C = { # synthetic ChatML wrapping (single-turn tool calls)
"glaive-fc-v2", "bitagent-tool-calling", "dolci-instruct-tool-use",
"glaive-fc-v2-reasoning", "nemotron-rl-tool-use",
"sharegpt-tool-calls", "toolhop",
"functions-53k", "deepfabric-github-mcp",
"playwright-mcp-toolcalling", "mcp-flow-comprehensive",
"ha-mcp-dataset", "limbic-eval-tool-use-mcp",
"mcp-memory-auto-trigger", "phi3-mcp",
"hf-coding-tools-traces",
"nemotron-coding-reasoning-rlmt-tool-use",
"nemotron-post-training-tool-use",
}
TIER_D = { # pure reasoning/coding, over-represented (also OOB by phase)
"kimi-k25-reasoning-1m", "glm-51-reasoning-1m",
"glm-47-multiturn-cot",
"opus-47-thinking-25k-ansulev",
"opus-4647-reasoning-8k7",
"opus-46-10kx-bas95",
"opus-47-max-sft-labs",
"opus-47-reasoning-cot-ansulev",
"deepseek-v4-distill-8000x", "qwen35-reasoning-700x",
}
TIER_E_HERMES_COMBINED = { # 100k total across all hermes-family
"hermes-3", "aureth-corpus-hermes", "hermes-omniforge-qwen36",
"hermes-agent-reasoning-traces", "hermes-agent-traces-filtered",
"hermes-reasoning-tool-use", "hermes-fc-thinking-v1",
"hermes-fc-v1", "nemotron-nano-hermes-traces",
"talos-kimi-hermes", "carnice-glm5-hermes",
"qwen36-trajectory",
}
TIER_F_N8N = { # n8n_workflow_generation — combined cap
"n8n-mega-workflows", "n8n-master-corpus",
"n8n-grpo-2k-aks729", "n8n-grpo-4k-aks729",
"n8n-toolkit-davidrpatton",
"n8n-workflow-template-rubenz",
"n8n-workflows-batuhanilgarr",
"n8n-workflows-sft-eclaude",
"n8n-workflows-templates-0xarchit",
"n8n-workflows-thinking-stmasson",
"n8n-workflows-v2-4k-arkelai",
"n8n-workflows-yagnik",
"n8n-workflow-dataset-ruh-ai",
"n8n-workflow-di12", "n8n-workflow-fmd053131",
"n8n-workflow-mzw2004", "n8n-workflow-npv2k1",
"n8n-workflow-ruh-ai", "n8n-workflow-tahakk",
"n8n-workflow-yonibabi", "n8n-testset-ruh-ai",
"n8nbuilder-perspicacious", "n8nbuilder-velixar",
"n8nbuilder-webman",
}
TIER_CAPS: dict[str, tuple[int, int]] = {
# tier → (cap, replicate_factor). cap is records-per-source for
# per-source tiers, or the combined budget for E/F.
"S": (5_000, 5), # full × 5 replicate
"A": (50_000, 1), # full
"B": (50_000, 1),
"C": (30_000, 1),
"D": (15_000, 1),
"E": (100_000, 1), # combined budget across hermes family
"F": (50_000, 1), # combined budget across n8n family
}
def tier_for(slug: str) -> str:
"""Return the tier letter ('S'..'F') for a normalized or `synth:` slug.
Un-tiered sources default to 'B' (50k cap), which is the conservative
behavior for synthetic corpora not yet promoted into the explicit
tables above (mostly synth:lifeops-* and synth:ea-*).
"""
base = slug.split(":", 1)[1] if slug.startswith("synth:") else slug
if base in TIER_S:
return "S"
if base in TIER_A:
return "A"
if base in TIER_B:
return "B"
if base in TIER_C:
return "C"
if base in TIER_D:
return "D"
if base in TIER_E_HERMES_COMBINED:
return "E"
if base in TIER_F_N8N:
return "F"
return "B"
def compute_targets(
counts: dict[str, int],
*,
per_source_cap: int = 100_000,
no_weights: bool = False,
) -> dict[str, int]:
"""Compute the per-source sampling target honoring tier caps.
Behavior matches the inline pass-2 logic in `main()`:
- Tier S: target = min(cap, n) × replicate_factor.
- Tier E / F: combined cap split proportionally to record counts.
- Tier A/B/C/D: target = min(cap, n).
- When `no_weights` is True, the global `per_source_cap` overrides.
- When `per_source_cap` is set below the tier-derived target, it
caps the result.
"""
e_total = sum(n for s, n in counts.items() if tier_for(s) == "E")
f_total = sum(n for s, n in counts.items() if tier_for(s) == "F")
e_budget = TIER_CAPS["E"][0]
f_budget = TIER_CAPS["F"][0]
targets: dict[str, int] = {}
for slug, n in counts.items():
tier = tier_for(slug)
if tier == "E":
t = int(e_budget * n / max(1, e_total))
elif tier == "F":
t = int(f_budget * n / max(1, f_total))
elif tier == "S":
cap, rep = TIER_CAPS[tier]
t = min(cap, n) * rep
else:
cap, _ = TIER_CAPS[tier]
t = min(cap, n)
if no_weights:
t = min(per_source_cap, n)
elif per_source_cap and per_source_cap < t:
t = per_source_cap
targets[slug] = max(0, t)
return targets
# Phase-distribution acceptance bands (post-pack).
# Source: docs/dataset/COVERAGE_AUDIT.md §"Per-phase coverage assessment".
# When --phase-distribution-target=balanced and any phase falls more
# than 5% outside its band, pack_dataset.py emits a WARNING (not fatal:
# Phase 3/4 may be empty until synthesizers run).
PHASE_BANDS_BALANCED: dict[str, tuple[float, float]] = {
"1": (0.20, 0.30),
"2": (0.45, 0.55),
"3": (0.10, 0.20),
"4": (0.07, 0.13),
}
PHASE_BANDS_FLAT: dict[str, tuple[float, float]] = {
# No-op gate: every phase passes any non-negative fraction.
"1": (0.0, 1.0),
"2": (0.0, 1.0),
"3": (0.0, 1.0),
"4": (0.0, 1.0),
}
PHASE_BAND_TOLERANCE = 0.05
# Records with these task_types are calibration corpora for the
# orthogonal-projection abliteration in scripts/quantization/abliteration_apply.py.
# They MUST NOT enter train/val/test; pack_dataset.py routes them to
# data/abliteration/{harmful,harmless}.jsonl instead. Their source entries
# in datasets.yaml carry weight=0.0 as a redundant guard.
ABLITERATION_TASK_TYPES = {
"abliteration_harmful": "harmful.jsonl",
"abliteration_harmless": "harmless.jsonl",
}
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
)
log = logging.getLogger("pack")
def record_hash(rec: dict) -> bytes:
"""Return a 16-byte hash so the dedupe set stays compact."""
h = hashlib.blake2b(digest_size=16)
md = rec.get("metadata") or {}
h.update((md.get("system_prompt") or "").encode("utf-8", "replace"))
cm = rec.get("currentMessage") or {}
h.update((cm.get("content") or "").encode("utf-8", "replace"))
h.update((rec.get("expectedResponse") or "").encode("utf-8", "replace"))
return h.digest()
def group_key(rec: dict) -> bytes:
"""Return a 16-byte hash of (system_prompt, currentMessage.content).
Records sharing the same group_key represent different supervised
targets for the same input prefix (e.g. LIGHT/multilight emits
1 RESPOND + 2 IGNORE + 1 reply per turn). They MUST land in the
same split to avoid train/val/test contamination.
"""
h = hashlib.blake2b(digest_size=16)
md = rec.get("metadata") or {}
h.update((md.get("system_prompt") or "").encode("utf-8", "replace"))
h.update(b"\x00")
cm = rec.get("currentMessage") or {}
h.update((cm.get("content") or "").encode("utf-8", "replace"))
return h.digest()
def stream_jsonl(path: Path):
"""Yield (line, parsed_dict). Skips bad lines silently."""
with path.open("r", encoding="utf-8", errors="replace") as f:
for line in f:
line = line.rstrip("\n")
if not line:
continue
try:
yield line, json.loads(line)
except json.JSONDecodeError:
continue
def count_records(path: Path) -> int:
n = 0
with path.open("rb") as f:
for _ in f:
n += 1
return n
def detect_explicit_splits(path: Path, *, sample: int = 4000) -> bool:
"""Return True if any record in the first `sample` lines has a non-train
metadata.split value. We use this to decide whether to respect the source's
`metadata.split == "train"` (when val/test markers exist) or to dice-roll
every record from that source so val/test get a representative slice."""
seen = 0
with path.open("r", encoding="utf-8", errors="replace") as f:
for line in f:
seen += 1
if seen > sample:
return False
try:
md = (json.loads(line).get("metadata") or {})
except json.JSONDecodeError:
continue
sp = (md.get("split") or "").lower()
if sp in ("test", "validation", "val", "dev"):
return True
return False
def reservoir_sample_indices(n_total: int, k: int, rng: random.Random) -> set[int]:
"""Return a set of k indices uniformly sampled from [0, n_total)."""
if k >= n_total:
return set(range(n_total))
# Algorithm L
indices = list(range(k))
i = k
w = pow(rng.random(), 1.0 / k) if k > 0 else 0.0
while i < n_total:
i += int(__import__("math").log(rng.random()) / __import__("math").log(1 - w)) + 1
if i < n_total:
indices[rng.randrange(k)] = i
w *= pow(rng.random(), 1.0 / k)
return set(indices)
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--seed", type=int, default=0xE71A05)
ap.add_argument("--no-weights", action="store_true",
help="ignore per-source weights from datasets.yaml")
ap.add_argument("--per-source-cap", type=int, default=100_000,
help="hard upper bound on records sampled per source")
ap.add_argument("--sample-per-source", type=int, default=0,
help="when >0, override per-source-cap and tier caps so "
"each source contributes at most ~N records. Used by "
"run_pipeline.py --from-scratch for a tiny sampled mix.")
ap.add_argument("--smoke", action="store_true",
help="relax acceptance gates for a tiny sampled mix: "
"out-of-band records pass through (oob-policy=allow) "
"and the phase-distribution gate is disabled "
"(phase-distribution-target=flat). A clear warning is "
"logged. Do NOT use for production packs.")
ap.add_argument("--max-train", type=int, default=0,
help="cap final train size after split (0 = no cap)")
ap.add_argument("--val-frac", type=float, default=0.04)
ap.add_argument("--test-frac", type=float, default=0.01)
ap.add_argument(
"--oob-policy",
choices=("drop", "route", "fail", "allow"),
default="route",
help=(
"How to handle records whose task_type does not map to a runtime "
"phase (see docs/dataset/COVERAGE_AUDIT.md). drop=silently exclude, "
"route=write to data/final/out_of_band.jsonl and exclude, fail=hard "
"error if any encountered, allow=pass through (legacy)."
),
)
ap.add_argument(
"--phase-distribution-target",
choices=("balanced", "flat", "legacy"),
default="balanced",
help=(
"Post-pack phase distribution gate. balanced=warn if any phase "
"drifts more than 5%% from the target bands in "
"docs/dataset/COVERAGE_AUDIT.md (P1=20-30%%, P2=45-55%%, "
"P3=10-20%%, P4=7-13%%). flat=no gate. legacy=no gate; "
"manifest still records distribution."
),
)
args = ap.parse_args()
if args.smoke:
log.warning(
"SMOKE MODE: skipping out-of-band rejection (oob-policy→allow) and "
"the phase-distribution acceptance gate (phase-distribution-target→"
"flat). The resulting pack is for pipeline validation only — NOT a "
"production training corpus."
)
args.oob_policy = "allow"
args.phase_distribution_target = "flat"
if args.sample_per_source and args.sample_per_source < args.per_source_cap:
log.info("sample-per-source=%d overrides per-source-cap=%d",
args.sample_per_source, args.per_source_cap)
args.per_source_cap = args.sample_per_source
rng = random.Random(args.seed)
FINAL.mkdir(parents=True, exist_ok=True)
with REGISTRY_FILE.open() as f:
registry = yaml.safe_load(f)
weights: dict[str, float] = {}
for e in (registry.get("datasets") or []):
weights[e["slug"]] = float(e.get("weight", 1.0))
for s in (registry.get("synthesized") or []):
weights[s["task_id"]] = float(s.get("weight", 1.0))
# Slugs whose normalized output should NOT enter the train mix and
# instead be copied verbatim into data/abliteration/{harmful,harmless}.jsonl.
# Determined by adapter name: any source using harmful_behaviors /
# harmless_alpaca is calibration data.
abliteration_slugs: dict[str, str] = {}
for e in (registry.get("datasets") or []):
adapter = e.get("normalizer")
if adapter == "harmful_behaviors":
abliteration_slugs[e["slug"]] = "harmful.jsonl"
elif adapter == "harmless_alpaca":
abliteration_slugs[e["slug"]] = "harmless.jsonl"
# ─────────────── route abliteration sources directly ─────────────
if abliteration_slugs:
ABLITERATION.mkdir(parents=True, exist_ok=True)
for slug, fname in abliteration_slugs.items():
src = NORMALIZED / f"{slug}.jsonl"
if not src.exists():
log.info(" abliteration: %s not yet normalized; skipping", slug)
continue
dst = ABLITERATION / fname
n = 0
with src.open("r", encoding="utf-8", errors="replace") as fin, \
dst.open("w", encoding="utf-8") as fout:
for line in fin:
line = line.rstrip("\n")
if not line:
continue
try:
rec = json.loads(line)
except json.JSONDecodeError:
continue
tt = (rec.get("metadata") or {}).get("task_type") or ""
if tt not in ABLITERATION_TASK_TYPES:
continue
fout.write(line + "\n")
n += 1
log.info(" abliteration: %s%s (%d records)", slug, dst, n)
# ─────────────── enumerate sources ────────────────────────────────
sources: list[tuple[str, Path]] = []
for path in sorted(NORMALIZED.glob("*.jsonl")):
if path.name.endswith(".errors.jsonl"):
continue
# Abliteration calibration data is routed separately (above) and
# must NEVER appear in train/val/test.
if path.stem in abliteration_slugs:
continue
sources.append((path.stem, path))
for path in sorted(SYNTHESIZED.rglob("*.jsonl")):
# Skip dotfile / progress markers like .sample_n200_seed42.jsonl
if any(part.startswith(".") for part in path.parts):
continue
# Use parent dir as namespace when nested (e.g. action_pairs/, translated/).
rel = path.relative_to(SYNTHESIZED)
if len(rel.parts) > 1:
slug = f"synth:{rel.parts[0]}-{path.stem}"
else:
slug = f"synth:{path.stem}"
sources.append((slug, path))
if not sources:
log.error("no normalized or synthesized records found")
return 1
# ─────────────── pass 1: count + compute per-source budgets ──────
counts: dict[str, int] = {}
has_explicit_splits: dict[str, bool] = {}
total = 0
log.info("pass 1: counting records per source")
surviving_sources: list[tuple[str, Path]] = []
for slug, path in sources:
if not path.exists():
# Concurrent producer can rename/remove a file between glob
# enumeration and counting. Drop it from this run.
log.warning(" %-40s vanished before count; skipping", slug)
continue
n = count_records(path)
counts[slug] = n
has_explicit_splits[slug] = detect_explicit_splits(path)
total += n
log.info(" %-40s %10d records (%.1f MB)%s", slug, n,
path.stat().st_size / 1e6,
" [explicit val/test]" if has_explicit_splits[slug] else "")
surviving_sources.append((slug, path))
sources = surviving_sources
log.info("pass 1 done: %d sources, %d records total", len(sources), total)
# Tier-based per-source caps. Defs live at module scope so unit
# tests can import them; see TIER_S/A/B/C/D/E_HERMES_COMBINED/F_N8N
# and TIER_CAPS at the top of this file.
targets = compute_targets(
counts,
per_source_cap=args.per_source_cap,
no_weights=args.no_weights,
)
log.info("tier breakdown: S=%d A=%d B=%d C=%d D=%d E=%d F=%d",
*(sum(1 for s in counts if tier_for(s) == t) for t in "SABCDEF"))
grand_target = sum(targets.values())
log.info("pass 2 will sample up to %d records (per_source_cap=%d, weights=%s)",
grand_target, args.per_source_cap, not args.no_weights)
# ─────────────── pass 2: reservoir-sample + stream-write ─────────
train_path = FINAL / "train.jsonl"
val_path = FINAL / "val.jsonl"
test_path = FINAL / "test.jsonl"
seen: set[bytes] = set()
# group_key → "train"|"val"|"test"; ensures all records sharing the
# same (system_prompt, currentMessage.content) prefix end up in the
# same split. Without this, sources that emit multiple supervised
# targets per turn (e.g. LIGHT/multilight: RESPOND + IGNORE + reply)
# leak across splits and inflate eval metrics.
group_split: dict[bytes, str] = {}
by_source = Counter()
by_task_type = Counter()
by_phase: Counter = Counter()
n_train = n_val = n_test = 0
n_group_forced = 0
n_oob = 0
n_replicated = 0
by_oob_task_type: Counter = Counter()
oob_path = FINAL / "out_of_band.jsonl"
foob = oob_path.open("w", encoding="utf-8") if args.oob_policy == "route" else None
with train_path.open("w", encoding="utf-8") as ftr, \
val_path.open("w", encoding="utf-8") as fva, \
test_path.open("w", encoding="utf-8") as fte:
for slug, path in sources:
n = counts[slug]
k = targets[slug]
if n == 0 or k == 0:
continue
if not path.exists():
log.warning(" %s vanished before sampling; skipping", slug)
continue
# Tier S sources are replicated: target == min(cap, n) × rep,
# but the underlying file only has `n` distinct records. Sample
# the unique reservoir size (target / rep) and emit each kept
# record `rep` times below.
tier = tier_for(slug)
replicate_factor = TIER_CAPS[tier][1] if tier == "S" else 1
unique_target = k // replicate_factor if replicate_factor > 1 else k
log.info(" sampling %s: %d/%d (rep=%d)", slug,
unique_target, n, replicate_factor)
keep = reservoir_sample_indices(n, unique_target, rng)
n_kept = 0
n_dup = 0
with path.open("r", encoding="utf-8", errors="replace") as f:
for idx, line in enumerate(f):
if idx not in keep:
continue
line = line.rstrip("\n")
if not line:
continue
try:
rec = json.loads(line)
except json.JSONDecodeError:
continue
# Defensive: never let abliteration calibration leak
# into the supervised splits. The slug-level filter
# above is the primary gate; this catches any record
# whose metadata.task_type was set after the slug was
# already enumerated as a regular source.
rec_tt = (rec.get("metadata") or {}).get("task_type") or ""
if rec_tt in ABLITERATION_TASK_TYPES:
continue
if classify_phase(rec_tt) == PHASE_OOB:
n_oob += 1
by_oob_task_type[rec_tt or "<missing>"] += 1
if args.oob_policy == "fail":
log.error(
"OOB record (task_type=%r) in %s; pack rejected. "
"See docs/dataset/COVERAGE_AUDIT.md.",
rec_tt, slug,
)
return 2
if args.oob_policy in ("drop", "route"):
if foob is not None:
foob.write(line + "\n")
continue
# allow: legacy behavior — fall through to inclusion
h = record_hash(rec)
if h in seen:
n_dup += 1
continue
seen.add(h)
# Decide split — GROUP-AWARE.
# Records sharing (system_prompt, currentMessage.content)
# must land in the same split. We cache the decision per
# group_key and force subsequent records into that split.
# If the source has explicit val/test markers, respect
# whatever metadata.split says. Otherwise (most sources,
# which only ship train.parquet), dice-roll every NEW
# group so val/test get a representative slice instead
# of being dominated by the rare sources with explicit
# splits.
md = rec.get("metadata") or {}
gk = group_key(rec)
forced = group_split.get(gk)
if forced is not None:
was = forced
n_group_forced += 1
else:
split = (md.get("split") or "").lower()
explicit = has_explicit_splits.get(slug, False)
if explicit and split == "test":
was = "test"
elif explicit and split in ("validation", "val", "dev"):
was = "val"
elif explicit and split == "train":
was = "train"
else:
roll = rng.random()
if roll < args.test_frac:
was = "test"
elif roll < args.test_frac + args.val_frac:
was = "val"
else:
was = "train"
group_split[gk] = was
if was == "test":
out = fte
elif was == "val":
out = fva
else:
out = ftr
out.write(line + "\n")
n_kept += 1
if was == "train":
n_train += 1
elif was == "val":
n_val += 1
else:
n_test += 1
by_source[md.get("source_dataset") or slug] += 1
by_task_type[md.get("task_type") or "?"] += 1
by_phase[classify_phase(rec_tt)] += 1
# Tier S replication: emit `replicate_factor - 1` extra
# copies of this record with a `metadata.replicate_index`
# breadcrumb. We use a per-replica RNG seeded from the
# base hash so future passes can reproduce, and so that
# an optional augmentation pass downstream has a stable
# per-copy seed to key off.
for r in range(1, replicate_factor):
rep_md = dict(md)
rep_md["replicate_index"] = r
# Stable per-replica seed: 32-bit hash of
# (record_hash, replicate_index). Varies the
# randomness slightly so downstream augmentation
# (paraphrase, dropout) won't produce identical
# outputs across replicas.
rep_md["replicate_seed"] = (
int.from_bytes(h[:4], "big") ^ (r * 0x9E3779B1)
) & 0xFFFFFFFF
rep_rec = dict(rec)
rep_rec["metadata"] = rep_md
out.write(json.dumps(rep_rec, ensure_ascii=False,
separators=(",", ":")) + "\n")
n_kept += 1
n_replicated += 1
if was == "train":
n_train += 1
elif was == "val":
n_val += 1
else:
n_test += 1
by_source[md.get("source_dataset") or slug] += 1
by_task_type[md.get("task_type") or "?"] += 1
by_phase[classify_phase(rec_tt)] += 1
log.info(" kept %d, dropped %d duplicates", n_kept, n_dup)
# ─────────────── enforce --max-train if needed ───────────────────
if args.max_train and n_train > args.max_train:
log.info("truncating train.jsonl to %d records (was %d)",
args.max_train, n_train)
tmp = train_path.with_suffix(".tmp")
n_emit = 0
with train_path.open("r", encoding="utf-8") as f, \
tmp.open("w", encoding="utf-8") as g:
# Reservoir-sample by line
keep = reservoir_sample_indices(n_train, args.max_train, rng)
for idx, line in enumerate(f):
if idx in keep:
g.write(line)
n_emit += 1
os.replace(tmp, train_path)
n_train = n_emit
if foob is not None:
foob.close()
# ─────────────── phase-distribution gate (post-pack) ───────────
# by_phase counts every record written across train/val/test.
# We compute the in-band fraction (excluding OOB, which the route/
# drop policies already excluded from the splits) and check each
# phase against its target band ± PHASE_BAND_TOLERANCE. Empty
# phases are not fatal — Phase 3/4 may be sparse until the
# synthesizers run — they only emit a WARNING.
in_band_total = sum(by_phase[p] for p in ("1", "2", "3", "4"))
phase_distribution: dict[str, float] = {}
if in_band_total > 0:
for p in ("1", "2", "3", "4"):
phase_distribution[p] = by_phase[p] / in_band_total
if args.phase_distribution_target == "balanced":
bands = PHASE_BANDS_BALANCED
elif args.phase_distribution_target == "flat":
bands = PHASE_BANDS_FLAT
else:
bands = None # legacy: no gate
drift: dict[str, dict[str, float]] = {}
if bands is not None and in_band_total > 0:
for p, (lo, hi) in bands.items():
actual = phase_distribution.get(p, 0.0)
lo_with_tol = max(0.0, lo - PHASE_BAND_TOLERANCE)
hi_with_tol = min(1.0, hi + PHASE_BAND_TOLERANCE)
if actual < lo_with_tol or actual > hi_with_tol:
drift[p] = {
"actual": round(actual, 4),
"lo": lo,
"hi": hi,
"tolerance": PHASE_BAND_TOLERANCE,
}
if drift:
log.warning(
"phase distribution outside ±%.0f%% of target=%s: %s",
PHASE_BAND_TOLERANCE * 100,
args.phase_distribution_target,
drift,
)
manifest = {
"totals": {"train": n_train, "val": n_val, "test": n_test},
"by_source": dict(by_source.most_common()),
"by_task_type": dict(by_task_type.most_common()),
"seed": args.seed,
"per_source_cap": args.per_source_cap,
"weights_applied": not args.no_weights,
"unique_records": len(seen),
"unique_groups": len(group_split),
"group_forced_routings": n_group_forced,
"replicated_records": n_replicated,
"out_of_band": {
"policy": args.oob_policy,
"count": n_oob,
"by_task_type": dict(by_oob_task_type.most_common()),
},
"phase_target": args.phase_distribution_target,
"phase_distribution": {
p: round(v, 4) for p, v in phase_distribution.items()
},
"phase_drift": drift,
}
(FINAL / "manifest.json").write_text(json.dumps(manifest, indent=2),
encoding="utf-8")
log.info("totals: train=%d val=%d test=%d (unique=%d, groups=%d, forced=%d)",
n_train, n_val, n_test, len(seen), len(group_split), n_group_forced)
log.info("by_task_type: %s", dict(by_task_type.most_common()))
if n_oob:
log.warning(
"out-of-band records (policy=%s): %d total; by task_type=%s",
args.oob_policy, n_oob, dict(by_oob_task_type.most_common()),
)
if args.oob_policy == "route":
log.warning(" routed to %s for review/transform", oob_path)
log.info("manifest at %s", FINAL / "manifest.json")
return 0
if __name__ == "__main__":
sys.exit(main())