From d99ce24d324e232c3a28f007d7b3c1531665b813 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 25 Jul 2026 08:33:35 +0000 Subject: [PATCH 1/4] SLM-300: wire self-context exposure-bias curriculum (AP-015) Add the preregistered self-context/scheduled-corruption curriculum manifest, an exact policy_origin_mixture(rate, lagged_share) function (gold vs. current-policy vs. lagged-policy mass split), and a torch-free fixture runner/CLI following the SLM-120 corruption curriculum pattern. Proves the "mixture zero reproduces legacy behavior" acceptance criterion as an exact, unit-tested invariant. Actual multi-seed self-context training arms remain gated on a GPU host and the certified baseline checkpoint (SLM-103); this is wiring evidence only, no recovery or ship-gate claim is made. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01KShriKrGosZr67yVPFgi78 --- ...0-self-context-exposure-bias-20260725.json | 333 +++++++++++++++ ...300-self-context-exposure-bias-20260725.md | 92 +++++ scripts/run_self_context_curriculum.py | 130 ++++++ .../experiments/self_context_curriculum.py | 381 ++++++++++++++++++ src/slm_training/resources/versions.json | 39 +- .../test_self_context_curriculum.py | 160 ++++++++ .../test_self_context_curriculum.py | 78 ++++ 7 files changed, 1203 insertions(+), 10 deletions(-) create mode 100644 docs/design/iter-slm300-self-context-exposure-bias-20260725.json create mode 100644 docs/design/iter-slm300-self-context-exposure-bias-20260725.md create mode 100644 scripts/run_self_context_curriculum.py create mode 100644 src/slm_training/harnesses/experiments/self_context_curriculum.py create mode 100644 tests/test_harnesses/experiments/test_self_context_curriculum.py create mode 100644 tests/test_scripts/test_self_context_curriculum.py diff --git a/docs/design/iter-slm300-self-context-exposure-bias-20260725.json b/docs/design/iter-slm300-self-context-exposure-bias-20260725.json new file mode 100644 index 000000000..c85615a3d --- /dev/null +++ b/docs/design/iter-slm300-self-context-exposure-bias-20260725.json @@ -0,0 +1,333 @@ +{ + "arms": [ + { + "arm_label": "A_control", + "binder_reference_recovery": null, + "binding_aware_meaningful_v2_rate_strict": null, + "checkpoint_uri": null, + "denoising_round_recovery": null, + "notes": [ + "planned self-context rate 0% seed 0", + "fixture-only: no model trained", + "policy origin mixture: gold=1.00 model_current=0.00 model_lagged=0.00", + "mixture zero reproduces legacy gold-only corruption exactly" + ], + "policy_origin_mixture": { + "gold": 1.0, + "model_current": 0.0, + "model_lagged": 0.0 + }, + "run_id": "slm300_fixture_A_control_s0", + "seed": 0, + "self_context_rate": 0.0, + "status": "fixture_planned", + "train_loss": null, + "validity_regression": null + }, + { + "arm_label": "A_control", + "binder_reference_recovery": null, + "binding_aware_meaningful_v2_rate_strict": null, + "checkpoint_uri": null, + "denoising_round_recovery": null, + "notes": [ + "planned self-context rate 0% seed 1", + "fixture-only: no model trained", + "policy origin mixture: gold=1.00 model_current=0.00 model_lagged=0.00", + "mixture zero reproduces legacy gold-only corruption exactly" + ], + "policy_origin_mixture": { + "gold": 1.0, + "model_current": 0.0, + "model_lagged": 0.0 + }, + "run_id": "slm300_fixture_A_control_s1", + "seed": 1, + "self_context_rate": 0.0, + "status": "fixture_planned", + "train_loss": null, + "validity_regression": null + }, + { + "arm_label": "A_control", + "binder_reference_recovery": null, + "binding_aware_meaningful_v2_rate_strict": null, + "checkpoint_uri": null, + "denoising_round_recovery": null, + "notes": [ + "planned self-context rate 0% seed 2", + "fixture-only: no model trained", + "policy origin mixture: gold=1.00 model_current=0.00 model_lagged=0.00", + "mixture zero reproduces legacy gold-only corruption exactly" + ], + "policy_origin_mixture": { + "gold": 1.0, + "model_current": 0.0, + "model_lagged": 0.0 + }, + "run_id": "slm300_fixture_A_control_s2", + "seed": 2, + "self_context_rate": 0.0, + "status": "fixture_planned", + "train_loss": null, + "validity_regression": null + }, + { + "arm_label": "SC10", + "binder_reference_recovery": null, + "binding_aware_meaningful_v2_rate_strict": null, + "checkpoint_uri": null, + "denoising_round_recovery": null, + "notes": [ + "planned self-context rate 10% seed 0", + "fixture-only: no model trained", + "policy origin mixture: gold=0.90 model_current=0.05 model_lagged=0.05" + ], + "policy_origin_mixture": { + "gold": 0.9, + "model_current": 0.05, + "model_lagged": 0.05 + }, + "run_id": "slm300_fixture_SC10_s0", + "seed": 0, + "self_context_rate": 0.1, + "status": "fixture_planned", + "train_loss": null, + "validity_regression": null + }, + { + "arm_label": "SC10", + "binder_reference_recovery": null, + "binding_aware_meaningful_v2_rate_strict": null, + "checkpoint_uri": null, + "denoising_round_recovery": null, + "notes": [ + "planned self-context rate 10% seed 1", + "fixture-only: no model trained", + "policy origin mixture: gold=0.90 model_current=0.05 model_lagged=0.05" + ], + "policy_origin_mixture": { + "gold": 0.9, + "model_current": 0.05, + "model_lagged": 0.05 + }, + "run_id": "slm300_fixture_SC10_s1", + "seed": 1, + "self_context_rate": 0.1, + "status": "fixture_planned", + "train_loss": null, + "validity_regression": null + }, + { + "arm_label": "SC10", + "binder_reference_recovery": null, + "binding_aware_meaningful_v2_rate_strict": null, + "checkpoint_uri": null, + "denoising_round_recovery": null, + "notes": [ + "planned self-context rate 10% seed 2", + "fixture-only: no model trained", + "policy origin mixture: gold=0.90 model_current=0.05 model_lagged=0.05" + ], + "policy_origin_mixture": { + "gold": 0.9, + "model_current": 0.05, + "model_lagged": 0.05 + }, + "run_id": "slm300_fixture_SC10_s2", + "seed": 2, + "self_context_rate": 0.1, + "status": "fixture_planned", + "train_loss": null, + "validity_regression": null + }, + { + "arm_label": "SC25", + "binder_reference_recovery": null, + "binding_aware_meaningful_v2_rate_strict": null, + "checkpoint_uri": null, + "denoising_round_recovery": null, + "notes": [ + "planned self-context rate 25% seed 0", + "fixture-only: no model trained", + "policy origin mixture: gold=0.75 model_current=0.12 model_lagged=0.12" + ], + "policy_origin_mixture": { + "gold": 0.75, + "model_current": 0.125, + "model_lagged": 0.125 + }, + "run_id": "slm300_fixture_SC25_s0", + "seed": 0, + "self_context_rate": 0.25, + "status": "fixture_planned", + "train_loss": null, + "validity_regression": null + }, + { + "arm_label": "SC25", + "binder_reference_recovery": null, + "binding_aware_meaningful_v2_rate_strict": null, + "checkpoint_uri": null, + "denoising_round_recovery": null, + "notes": [ + "planned self-context rate 25% seed 1", + "fixture-only: no model trained", + "policy origin mixture: gold=0.75 model_current=0.12 model_lagged=0.12" + ], + "policy_origin_mixture": { + "gold": 0.75, + "model_current": 0.125, + "model_lagged": 0.125 + }, + "run_id": "slm300_fixture_SC25_s1", + "seed": 1, + "self_context_rate": 0.25, + "status": "fixture_planned", + "train_loss": null, + "validity_regression": null + }, + { + "arm_label": "SC25", + "binder_reference_recovery": null, + "binding_aware_meaningful_v2_rate_strict": null, + "checkpoint_uri": null, + "denoising_round_recovery": null, + "notes": [ + "planned self-context rate 25% seed 2", + "fixture-only: no model trained", + "policy origin mixture: gold=0.75 model_current=0.12 model_lagged=0.12" + ], + "policy_origin_mixture": { + "gold": 0.75, + "model_current": 0.125, + "model_lagged": 0.125 + }, + "run_id": "slm300_fixture_SC25_s2", + "seed": 2, + "self_context_rate": 0.25, + "status": "fixture_planned", + "train_loss": null, + "validity_regression": null + }, + { + "arm_label": "SC50", + "binder_reference_recovery": null, + "binding_aware_meaningful_v2_rate_strict": null, + "checkpoint_uri": null, + "denoising_round_recovery": null, + "notes": [ + "planned self-context rate 50% seed 0", + "fixture-only: no model trained", + "policy origin mixture: gold=0.50 model_current=0.25 model_lagged=0.25" + ], + "policy_origin_mixture": { + "gold": 0.5, + "model_current": 0.25, + "model_lagged": 0.25 + }, + "run_id": "slm300_fixture_SC50_s0", + "seed": 0, + "self_context_rate": 0.5, + "status": "fixture_planned", + "train_loss": null, + "validity_regression": null + }, + { + "arm_label": "SC50", + "binder_reference_recovery": null, + "binding_aware_meaningful_v2_rate_strict": null, + "checkpoint_uri": null, + "denoising_round_recovery": null, + "notes": [ + "planned self-context rate 50% seed 1", + "fixture-only: no model trained", + "policy origin mixture: gold=0.50 model_current=0.25 model_lagged=0.25" + ], + "policy_origin_mixture": { + "gold": 0.5, + "model_current": 0.25, + "model_lagged": 0.25 + }, + "run_id": "slm300_fixture_SC50_s1", + "seed": 1, + "self_context_rate": 0.5, + "status": "fixture_planned", + "train_loss": null, + "validity_regression": null + }, + { + "arm_label": "SC50", + "binder_reference_recovery": null, + "binding_aware_meaningful_v2_rate_strict": null, + "checkpoint_uri": null, + "denoising_round_recovery": null, + "notes": [ + "planned self-context rate 50% seed 2", + "fixture-only: no model trained", + "policy origin mixture: gold=0.50 model_current=0.25 model_lagged=0.25" + ], + "policy_origin_mixture": { + "gold": 0.5, + "model_current": 0.25, + "model_lagged": 0.25 + }, + "run_id": "slm300_fixture_SC50_s2", + "seed": 2, + "self_context_rate": 0.5, + "status": "fixture_planned", + "train_loss": null, + "validity_regression": null + } + ], + "curriculum_id": "self-context-scheduled-corruption", + "manifest": { + "base_recipe": { + "batch_size": 4, + "checkpoint_sync": false, + "context_backend": "hf_local_files_only", + "device": "cpu", + "eval_version": "remediated", + "learning_rate": 0.0003, + "max_wall_minutes": 3.0, + "seed": 0, + "steps": 32, + "train_version": "e218_schema_normalized_judge_v5" + }, + "base_recipe_hash": "11a333490bd3913a8ed1c573d36d1e6c12a38b000253611f5de34ecd021b8e0d", + "checkpoint_bucket": "hf://buckets/TKendrick/OpenUI", + "claim_class": "wiring", + "curriculum_id": "self-context-scheduled-corruption", + "falsifier": "Self-context mixture produces no paired strict-semantic recovery gain over the gold-only (rate=0) control, or any gain requires validity regression greater than 0.01 or more total tokens/updates than the matched control.", + "hypothesis": "Train-decode state mismatch (exposure bias) contributes to binding/reference recovery failures, and mixing verifier-aware, model-generated partial states into the corruption schedule at a moderate self-context rate improves recovery without validity regression or a hidden training-budget increase.", + "lagged_policy_share": 0.5, + "matrix_set": "self-context-exposure-bias", + "matrix_version": "ap015-v1", + "parent_checkpoint_uri": null, + "seeds": [ + 0, + 1, + 2 + ], + "self_context_rates": [ + 0.0, + 0.1, + 0.25, + 0.5 + ], + "status": "not_run" + }, + "matrix_set": "self-context-exposure-bias", + "matrix_version": "ap015-v1", + "run_id": "slm300_fixture", + "status": "fixture", + "version_stamp": { + "code_commit": "86212e9ab2338bb9cf5ab135e85e3d3d4e7d21b5", + "code_dirty": true, + "components": { + "harness.experiments": "v105" + }, + "stamp_schema": "version_stamp/v1", + "stamped_at": "2026-07-25T08:30:53.792312+00:00" + } +} diff --git a/docs/design/iter-slm300-self-context-exposure-bias-20260725.md b/docs/design/iter-slm300-self-context-exposure-bias-20260725.md new file mode 100644 index 000000000..abbc00196 --- /dev/null +++ b/docs/design/iter-slm300-self-context-exposure-bias-20260725.md @@ -0,0 +1,92 @@ +# SLM-300 AP-015 — Self-context exposure-bias curriculum (wiring) + +## What +Preregistered self-context curriculum manifest and exact `policy_origin_mixture` +math for AP-015. The experiment tests whether train-decode state mismatch +(exposure bias) explains binder/reference recovery failures, and whether +mixing model-generated ("self-context") partial states into the corruption +schedule -- instead of exclusively gold-label-derived corruption -- improves +recovery without validity regression or a hidden training-budget increase. + +## Matrix registration +- `matrix_set`: `self-context-exposure-bias` +- `matrix_version`: `ap015-v1` +- `curriculum_id`: `self-context-scheduled-corruption` + +## Arms + +| Arm | Self-context rate | Purpose | +| --- | --- | --- | +| A_control | 0% | clean/legacy control (mixture-zero invariant) | +| SC10 | 10% | low self-context mixture | +| SC25 | 25% | medium self-context mixture | +| SC50 | 50% | high self-context mixture | + +Within each non-zero arm, the self-context mass is split between the current +policy and a lagged/EMA policy checkpoint by `lagged_policy_share` (default +50/50) — this is the "current-versus-lagged policy origin" provenance +required by the issue, and it damps the instability of purely on-policy +self-distillation. + +## Policy origin mixture + +`policy_origin_mixture(self_context_rate, lagged_policy_share)` is a pure, +deterministic function (no training required): + +```text +gold = 1 - self_context_rate +model_lagged = self_context_rate * lagged_policy_share +model_current = self_context_rate - model_lagged +``` + +At `self_context_rate == 0.0` the mixture is always +`{gold: 1.0, model_current: 0.0, model_lagged: 0.0}` for every +`lagged_policy_share` — this is the exact, tested proof that the mixture-zero +arm reproduces legacy (gold-only) corruption behavior, satisfying the issue's +"Mixture zero reproduces legacy behavior" acceptance criterion at the wiring +level. + +## Frozen base recipe +The base recipe mirrors the SLM-120 corruption-curriculum recipe shape +(CPU device, matched steps/batch/seed fields) so both curricula remain +directly comparable at matched exposure. Its SHA-256 is stored in the +manifest. + +## Files added +- `src/slm_training/harnesses/experiments/self_context_curriculum.py` +- `scripts/run_self_context_curriculum.py` +- `tests/test_harnesses/experiments/test_self_context_curriculum.py` +- `tests/test_scripts/test_self_context_curriculum.py` +- `docs/design/iter-slm300-self-context-exposure-bias-20260725.md` +- `docs/design/iter-slm300-self-context-exposure-bias-20260725.json` + +## Commands + +```bash +# Plan only (CPU, no model load) +python -m scripts.run_self_context_curriculum --mode plan-only \ + --output-dir outputs/runs/slm300_self_context_curriculum + +# Fixture wiring check +python -m scripts.run_self_context_curriculum --mode fixture \ + --parent-checkpoint-uri hf://buckets/TKendrick/OpenUI/checkpoints/certified-baseline/ref.json \ + --output-dir outputs/runs/slm300_self_context_fixture +``` + +## Verification +- `pytest tests/test_harnesses/experiments/test_self_context_curriculum.py -q` → 19 passed +- `pytest tests/test_scripts/test_self_context_curriculum.py -q` → 4 passed +- `python -m scripts.verify_version_stamps --check` → ok + +## Honest caveats +This is **wiring evidence only**. The policy-origin-mixture math is exact and +unit-tested (including the mixture-zero legacy-equivalence invariant), but +the actual multi-seed self-context/scheduled-corruption training arms — +collecting model-generated partial states from the certified baseline, +training matched variants at self-context rates 0/0.10/0.25/0.50, and +evaluating recovery by binder/reference mutation class and denoising round — +require a GPU host, the certified baseline checkpoint, and durable HF bucket +sync per SLM-103. The `frontier` mode emits a fixture plan and raises a clear +stderr message. No recovery or ship-gate claim is made from this artifact; +`binder_reference_recovery` and `denoising_round_recovery` fields remain +`null` until a real training run populates them. diff --git a/scripts/run_self_context_curriculum.py b/scripts/run_self_context_curriculum.py new file mode 100644 index 000000000..ec6d07c88 --- /dev/null +++ b/scripts/run_self_context_curriculum.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python3 +"""Run the SLM-300 AP-015 self-context exposure-bias curriculum. + +Example (plan only, no model load): + python -m scripts.run_self_context_curriculum --mode plan-only \ + --output-dir outputs/runs/slm300_self_context_curriculum + +Example (fixture wiring check): + python -m scripts.run_self_context_curriculum --mode fixture \ + --parent-checkpoint-uri hf://buckets/TKendrick/OpenUI/checkpoints/certified-baseline/ref.json \ + --output-dir outputs/runs/slm300_self_context_fixture + +Frontier execution (GPU + durable checkpoint required): + python -m scripts.run_self_context_curriculum --mode frontier \ + --parent-checkpoint-uri hf://buckets/TKendrick/OpenUI/checkpoints/certified-baseline/ref.json \ + --checkpoint-bucket hf://buckets/TKendrick/OpenUI \ + --output-dir outputs/runs/slm300_self_context_frontier +""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +from slm_training.harnesses.experiments.self_context_curriculum import ( + build_self_context_manifest, + render_markdown, + run_fixture_self_context_curriculum, + validate_manifest, +) + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser( + description="SLM-300 AP-015 self-context exposure-bias curriculum" + ) + parser.add_argument( + "--mode", + choices=("plan-only", "fixture", "frontier"), + default="plan-only", + help=( + "plan-only emits the manifest without loading models; fixture runs a " + "torch-free wiring check; frontier dispatches the real GPU curriculum (not " + "implemented in this wiring slice)." + ), + ) + parser.add_argument( + "--output-dir", + type=Path, + default=Path("outputs/runs/slm300_self_context_curriculum"), + ) + parser.add_argument( + "--parent-checkpoint-uri", + default=None, + help="Durable URI of the certified-baseline parent checkpoint", + ) + parser.add_argument( + "--checkpoint-bucket", + default="hf://buckets/TKendrick/OpenUI", + help="HF bucket for durable curriculum checkpoints", + ) + parser.add_argument( + "--seeds", + default="0,1,2", + help="Comma-separated seeds for the curriculum arms", + ) + parser.add_argument( + "--self-context-rates", + default="0.0,0.10,0.25,0.50", + help="Comma-separated self-context mixture rates", + ) + parser.add_argument( + "--lagged-policy-share", + type=float, + default=0.5, + help="Share of self-context mass drawn from the lagged/EMA policy vs current policy", + ) + args = parser.parse_args(argv) + + seeds = tuple(int(s.strip()) for s in args.seeds.split(",") if s.strip()) + rates = tuple( + float(x.strip()) for x in args.self_context_rates.split(",") if x.strip() + ) + + manifest = build_self_context_manifest( + parent_checkpoint_uri=args.parent_checkpoint_uri, + checkpoint_bucket=args.checkpoint_bucket, + seeds=seeds, + self_context_rates=rates, + lagged_policy_share=args.lagged_policy_share, + ) + errors = validate_manifest(manifest) + if errors: + for error in errors: + print(f"manifest error: {error}", file=sys.stderr) + return 1 + + args.output_dir.mkdir(parents=True, exist_ok=True) + manifest.to_json(args.output_dir / "self_context_curriculum_manifest.json") + + if args.mode == "plan-only": + report = run_fixture_self_context_curriculum( + manifest, run_id="slm300_plan", output_dir=args.output_dir + ) + elif args.mode == "fixture": + report = run_fixture_self_context_curriculum( + manifest, run_id="slm300_fixture", output_dir=args.output_dir + ) + else: + print( + "frontier mode requires GPU host and durable checkpoints; " + "emitting fixture plan only", + file=sys.stderr, + ) + report = run_fixture_self_context_curriculum( + manifest, run_id="slm300_frontier_partial", output_dir=args.output_dir + ) + + markdown = render_markdown(report) + (args.output_dir / "self_context_curriculum_report.md").write_text( + markdown, encoding="utf-8" + ) + report.to_json(args.output_dir / "self_context_curriculum_report.json") + print(markdown) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/slm_training/harnesses/experiments/self_context_curriculum.py b/src/slm_training/harnesses/experiments/self_context_curriculum.py new file mode 100644 index 000000000..ed58db68d --- /dev/null +++ b/src/slm_training/harnesses/experiments/self_context_curriculum.py @@ -0,0 +1,381 @@ +"""SLM-300 AP-015 self-context / scheduled-corruption exposure-bias manifest. + +This module provides the preregistered self-context curriculum manifest, a +pure deterministic policy-origin mixture calculation, and a torch-free +fixture runner. It follows the SLM-120 ``corruption_curriculum`` module +pattern: the manifest/mixture math is real, versioned, and testable; the +actual multi-seed training arms require a GPU host and durable checkpoint +provenance (SLM-103 / EFS1 readout) and are out of scope for this wiring +slice. + +Objective (AP-015): determine whether train-decode state mismatch (exposure +bias) explains binding/reference recovery failures, and whether mixing +model-generated ("self-context") partial states into the corruption +schedule -- instead of exclusively gold-label-derived corruption -- improves +recovery without a hidden training-budget increase or validity regression. +""" + +from __future__ import annotations + +import hashlib +import json +import time +from dataclasses import asdict, dataclass, field +from enum import Enum +from pathlib import Path +from typing import Any + +from slm_training.harness_core.versioning import build_version_stamp +from slm_training.levers import MAX_RUN_MINUTES + +__all__ = [ + "DEFAULT_LAGGED_POLICY_SHARE", + "MATRIX_SET", + "MATRIX_VERSION", + "SELF_CONTEXT_CURRICULUM_ID", + "SELF_CONTEXT_RATES", + "PolicyOrigin", + "SelfContextArmResult", + "SelfContextCurriculumManifest", + "SelfContextCurriculumReport", + "build_self_context_manifest", + "policy_origin_mixture", + "render_markdown", + "run_fixture_self_context_curriculum", + "validate_manifest", +] + +MATRIX_VERSION = "ap015-v1" +MATRIX_SET = "self-context-exposure-bias" +SELF_CONTEXT_CURRICULUM_ID = "self-context-scheduled-corruption" + +# Preregistered self-context rates: 0.0 is the mandatory clean/legacy control; +# 0.10/0.25/0.50 are the scheduled-corruption mixture arms from the issue. +SELF_CONTEXT_RATES = (0.0, 0.10, 0.25, 0.50) + +# Of the self-context mass, the share drawn from a lagged/EMA policy +# checkpoint rather than the live/current policy. A lagged origin damps the +# instability of purely on-policy self-distillation (moving target), matching +# the "current-versus-lagged policy origin" provenance field required by the +# issue's acceptance criteria. +DEFAULT_LAGGED_POLICY_SHARE = 0.5 + + +class PolicyOrigin(str, Enum): + """Provenance of one corrupted/partial training state.""" + + GOLD = "gold" + MODEL_CURRENT = "model_current" + MODEL_LAGGED = "model_lagged" + + +def policy_origin_mixture( + self_context_rate: float, + lagged_policy_share: float = DEFAULT_LAGGED_POLICY_SHARE, +) -> dict[str, float]: + """Return the exact {gold, model_current, model_lagged} mass split. + + Pure function, no model/training required: ``self_context_rate`` is the + total probability mass drawn from model-generated partial states (as + opposed to gold/locked-label corruption); ``lagged_policy_share`` splits + that self-context mass between the live policy and a lagged/EMA + checkpoint. At ``self_context_rate == 0.0`` the mixture is always + ``{"gold": 1.0, "model_current": 0.0, "model_lagged": 0.0}`` regardless of + ``lagged_policy_share`` -- this is the exact wiring proof that the + mixture-zero arm reproduces legacy (gold-only) corruption behavior. + """ + if not 0.0 <= self_context_rate <= 1.0: + raise ValueError("self_context_rate must be in [0, 1]") + if not 0.0 <= lagged_policy_share <= 1.0: + raise ValueError("lagged_policy_share must be in [0, 1]") + gold = 1.0 - self_context_rate + model_lagged = self_context_rate * lagged_policy_share + model_current = self_context_rate - model_lagged + return { + PolicyOrigin.GOLD.value: gold, + PolicyOrigin.MODEL_CURRENT.value: model_current, + PolicyOrigin.MODEL_LAGGED.value: model_lagged, + } + + +@dataclass(frozen=True) +class SelfContextCurriculumManifest: + matrix_set: str = MATRIX_SET + matrix_version: str = MATRIX_VERSION + curriculum_id: str = SELF_CONTEXT_CURRICULUM_ID + hypothesis: str = ( + "Train-decode state mismatch (exposure bias) contributes to " + "binding/reference recovery failures, and mixing verifier-aware, " + "model-generated partial states into the corruption schedule at a " + "moderate self-context rate improves recovery without validity " + "regression or a hidden training-budget increase." + ) + falsifier: str = ( + "Self-context mixture produces no paired strict-semantic recovery " + "gain over the gold-only (rate=0) control, or any gain requires " + "validity regression greater than 0.01 or more total tokens/updates " + "than the matched control." + ) + # Base recipe fields mirror the SLM-120 corruption-curriculum base recipe + # shape so both curricula stay directly comparable at matched exposure. + base_recipe: dict[str, Any] = field( + default_factory=lambda: { + "device": "cpu", + "steps": 32, + "batch_size": 4, + "learning_rate": 0.0003, + "seed": 0, + "context_backend": "hf_local_files_only", + "train_version": "e218_schema_normalized_judge_v5", + "eval_version": "remediated", + "checkpoint_sync": False, + "max_wall_minutes": float(MAX_RUN_MINUTES), + } + ) + self_context_rates: tuple[float, ...] = SELF_CONTEXT_RATES + lagged_policy_share: float = DEFAULT_LAGGED_POLICY_SHARE + seeds: tuple[int, ...] = (0, 1, 2) + claim_class: str = "frontier" + status: str = "not_run" + parent_checkpoint_uri: str | None = None + checkpoint_bucket: str | None = None + + def to_dict(self) -> dict[str, Any]: + data = dict(asdict(self)) + data["base_recipe_hash"] = self.recipe_hash() + return data + + def to_json(self, path: Path) -> None: + path.write_text( + json.dumps(self.to_dict(), indent=2, sort_keys=True, default=str) + "\n", + encoding="utf-8", + ) + + def recipe_hash(self) -> str: + return hashlib.sha256( + json.dumps(self.base_recipe, sort_keys=True, default=str).encode("utf-8") + ).hexdigest() + + +@dataclass(frozen=True) +class SelfContextArmResult: + arm_label: str + self_context_rate: float + seed: int + status: str + policy_origin_mixture: dict[str, float] + run_id: str | None = None + checkpoint_uri: str | None = None + train_loss: float | None = None + binding_aware_meaningful_v2_rate_strict: float | None = None + validity_regression: float | None = None + binder_reference_recovery: dict[str, float] | None = None + denoising_round_recovery: dict[str, float] | None = None + notes: list[str] = field(default_factory=list) + + def to_dict(self) -> dict[str, Any]: + return dict(asdict(self)) + + +@dataclass(frozen=True) +class SelfContextCurriculumReport: + matrix_set: str + matrix_version: str + curriculum_id: str + run_id: str + status: str + manifest: SelfContextCurriculumManifest + arms: list[SelfContextArmResult] + version_stamp: dict[str, Any] = field(default_factory=dict) + + def to_dict(self) -> dict[str, Any]: + return { + "matrix_set": self.matrix_set, + "matrix_version": self.matrix_version, + "curriculum_id": self.curriculum_id, + "run_id": self.run_id, + "status": self.status, + "manifest": self.manifest.to_dict(), + "arms": [a.to_dict() for a in self.arms], + "version_stamp": self.version_stamp, + } + + def to_json(self, path: Path) -> None: + path.write_text( + json.dumps(self.to_dict(), indent=2, sort_keys=True, default=str) + "\n", + encoding="utf-8", + ) + + +def build_self_context_manifest( + *, + parent_checkpoint_uri: str | None = None, + checkpoint_bucket: str | None = None, + seeds: tuple[int, ...] = (0, 1, 2), + self_context_rates: tuple[float, ...] = SELF_CONTEXT_RATES, + lagged_policy_share: float = DEFAULT_LAGGED_POLICY_SHARE, +) -> SelfContextCurriculumManifest: + """Return the preregistered SLM-300 self-context curriculum manifest.""" + if parent_checkpoint_uri is None: + status = "not_run" + claim_class = "wiring" + else: + status = "frontier_pending_gpu" + claim_class = "frontier" + return SelfContextCurriculumManifest( + parent_checkpoint_uri=parent_checkpoint_uri, + checkpoint_bucket=checkpoint_bucket, + seeds=seeds, + self_context_rates=self_context_rates, + lagged_policy_share=lagged_policy_share, + status=status, + claim_class=claim_class, + ) + + +def validate_manifest(manifest: SelfContextCurriculumManifest) -> list[str]: + errors: list[str] = [] + if not manifest.self_context_rates: + errors.append("self_context_rates must not be empty") + if 0.0 not in manifest.self_context_rates: + errors.append("0.0 control rate is required (mixture zero must reproduce legacy behavior)") + if any(not 0.0 <= rate <= 1.0 for rate in manifest.self_context_rates): + errors.append("self_context_rates must be in [0, 1]") + if not 0.0 <= manifest.lagged_policy_share <= 1.0: + errors.append("lagged_policy_share must be in [0, 1]") + if not manifest.seeds: + errors.append("seeds must not be empty") + if manifest.claim_class in {"frontier", "ship_candidate"}: + if not manifest.parent_checkpoint_uri: + errors.append("frontier/ship_candidate manifest requires parent_checkpoint_uri") + if not manifest.checkpoint_bucket: + errors.append("frontier/ship_candidate manifest requires checkpoint_bucket") + return errors + + +def _arm_label(rate: float) -> str: + if rate == 0.0: + return "A_control" + return f"SC{int(round(rate * 100)):02d}" + + +def run_fixture_self_context_curriculum( + manifest: SelfContextCurriculumManifest, + *, + run_id: str = "slm300_fixture", + output_dir: Path | None = None, +) -> SelfContextCurriculumReport: + """Torch-free fixture that validates the manifest and emits a plan. + + The policy-origin mixture is computed exactly (it is a pure function of + the manifest, not a training result); every other per-arm metric remains + ``None`` because no model is trained in this wiring slice. + """ + arms: list[SelfContextArmResult] = [] + time.perf_counter() # mark start timing for future telemetry + for rate in manifest.self_context_rates: + label = _arm_label(rate) + mixture = policy_origin_mixture(rate, manifest.lagged_policy_share) + for seed in manifest.seeds: + notes = [ + f"planned self-context rate {rate:.0%} seed {seed}", + "fixture-only: no model trained", + f"policy origin mixture: gold={mixture[PolicyOrigin.GOLD.value]:.2f} " + f"model_current={mixture[PolicyOrigin.MODEL_CURRENT.value]:.2f} " + f"model_lagged={mixture[PolicyOrigin.MODEL_LAGGED.value]:.2f}", + ] + if rate == 0.0: + notes.append("mixture zero reproduces legacy gold-only corruption exactly") + arms.append( + SelfContextArmResult( + arm_label=label, + self_context_rate=rate, + seed=seed, + status="fixture_planned", + policy_origin_mixture=mixture, + run_id=f"{run_id}_{label}_s{seed}", + notes=notes, + ) + ) + report = SelfContextCurriculumReport( + matrix_set=manifest.matrix_set, + matrix_version=manifest.matrix_version, + curriculum_id=manifest.curriculum_id, + run_id=run_id, + status="fixture", + manifest=manifest, + arms=arms, + version_stamp=build_version_stamp("harness.experiments"), + ) + if output_dir is not None: + output_dir.mkdir(parents=True, exist_ok=True) + report.to_json(output_dir / "self_context_curriculum_report.json") + return report + + +def render_markdown(report: SelfContextCurriculumReport) -> str: + lines = [ + f"# SLM-300 AP-015 — Self-context exposure-bias curriculum ({report.run_id})", + "", + f"Matrix set: `{report.matrix_set}` ", + f"Version: `{report.matrix_version}` ", + f"Status: **{report.status}** ", + "", + "## Hypothesis", + "", + report.manifest.hypothesis, + "", + "## Falsifier", + "", + report.manifest.falsifier, + "", + "## Frozen base recipe (SHA-256)", + "", + f"```\n{report.manifest.recipe_hash()}\n```", + "", + "## Curriculum arms", + "", + "| Arm | Self-context rate | Lagged share | Seeds | Status |", + "| --- | --- | --- | --- | --- |", + ] + for rate in report.manifest.self_context_rates: + label = _arm_label(rate) + statuses = {a.status for a in report.arms if a.self_context_rate == rate} + lines.append( + f"| {label} | {rate:.0%} | {report.manifest.lagged_policy_share:.0%} | " + f"{len(report.manifest.seeds)} | {','.join(sorted(statuses))} |" + ) + lines.extend(["", "## Policy origin mixture", "", "| Arm | gold | model_current | model_lagged |", "| --- | --- | --- | --- |"]) + seen_labels: set[str] = set() + for arm in report.arms: + if arm.arm_label in seen_labels: + continue + seen_labels.add(arm.arm_label) + mix = arm.policy_origin_mixture + lines.append( + f"| {arm.arm_label} | {mix[PolicyOrigin.GOLD.value]:.2f} | " + f"{mix[PolicyOrigin.MODEL_CURRENT.value]:.2f} | " + f"{mix[PolicyOrigin.MODEL_LAGGED.value]:.2f} |" + ) + lines.extend(["", "## Results", ""]) + for arm in report.arms: + lines.append( + f"- **{arm.arm_label} seed {arm.seed}**: {arm.status} — " + f"run_id `{arm.run_id}`, self-context rate {arm.self_context_rate:.0%}" + ) + for note in arm.notes: + lines.append(f" - {note}") + lines.extend( + [ + "", + "## Verdict", + "", + "Fixture/plan only. The policy-origin mixture math is exact and tested " + "(including the mixture-zero legacy-equivalence invariant); the actual " + "multi-seed self-context training arms require a GPU host, the " + "certified baseline checkpoint, and durable HF bucket sync per SLM-103. " + "No recovery or ship-gate claim is made from this artifact.", + "", + ] + ) + return "\n".join(lines) diff --git a/src/slm_training/resources/versions.json b/src/slm_training/resources/versions.json index 4e7a55620..5b1671ca3 100644 --- a/src/slm_training/resources/versions.json +++ b/src/slm_training/resources/versions.json @@ -683,7 +683,7 @@ { "version": "v1", "date": "2026-07-25", - "note": "DSH5-02: initial exact atomic bulk openui.map_set_property operator over SelectorRefV1 — one schema-valid property update applied to every exact selected node atomically, with per-target effect deltas, replay, fresh post-commit reference-table continuation, and a diagnostic-only primitive-lowering equivalence oracle" + "note": "DSH5-02: initial exact atomic bulk openui.map_set_property operator over SelectorRefV1 \u2014 one schema-valid property update applied to every exact selected node atomically, with per-target effect deltas, replay, fresh post-commit reference-table continuation, and a diagnostic-only primitive-lowering equivalence oracle" } ] }, @@ -774,7 +774,7 @@ { "version": "v1", "date": "2026-07-25", - "note": "DSH5-04: initial OperatorTransactionV1/PreparedOperatorActionV1/OperatorReadWriteSetV1 base-state, dependency, and conflict contracts — one-base multi-action preparation, semantic read/write footprints, write/write and write/read conflict resolution via mutual commutativity (reusing merge.py's _effect_targets and mutually-commuting gate), dependency-cycle detection, and canonical permutation-invariant transaction identity; schema/safety layer only, no execution path wired" + "note": "DSH5-04: initial OperatorTransactionV1/PreparedOperatorActionV1/OperatorReadWriteSetV1 base-state, dependency, and conflict contracts \u2014 one-base multi-action preparation, semantic read/write footprints, write/write and write/read conflict resolution via mutual commutativity (reusing merge.py's _effect_targets and mutually-commuting gate), dependency-cycle detection, and canonical permutation-invariant transaction identity; schema/safety layer only, no execution path wired" } ] }, @@ -1209,7 +1209,7 @@ { "version": "v9", "date": "2026-07-19", - "note": "add EFS2-01 X22 tree-edit beam-width × edit-depth scaling harness (TreeEditScalingConfig, run_scaling_grid, valid-state beam search) on top of quantized-energy/solver-state/verifier-cascade stack" + "note": "add EFS2-01 X22 tree-edit beam-width \u00d7 edit-depth scaling harness (TreeEditScalingConfig, run_scaling_grid, valid-state beam search) on top of quantized-energy/solver-state/verifier-cascade stack" }, { "version": "v8", @@ -1716,7 +1716,7 @@ ] }, "harness.experiments": { - "version": "v108", + "version": "v109", "kind": "harness", "paths": [ "src/slm_training/harnesses/experiments/", @@ -1725,6 +1725,11 @@ "tests/test_scripts/test_run_slm298_capacity_context_curriculum.py" ], "history": [ + { + "version": "v109", + "date": "2026-07-25", + "note": "add SLM-300 AP-015 self-context exposure-bias curriculum manifest, policy-origin-mixture wiring/fixture harness, and mixture-zero legacy-equivalence invariant" + }, { "version": "v108", "date": "2026-07-25", @@ -2148,7 +2153,7 @@ { "version": "v31", "date": "2026-07-20", - "note": "add SLM-148 SPV1-05 plan-conditioned X22 × conflict-slice staged campaign wiring/fixture harness" + "note": "add SLM-148 SPV1-05 plan-conditioned X22 \u00d7 conflict-slice staged campaign wiring/fixture harness" }, { "version": "v30", @@ -2188,7 +2193,7 @@ { "version": "v24", "date": "2026-07-19", - "note": "add SLM-133 EFS3-06 AST-sketch dedup × choice-native retrieval factorial wiring/fixture harness" + "note": "add SLM-133 EFS3-06 AST-sketch dedup \u00d7 choice-native retrieval factorial wiring/fixture harness" }, { "version": "v23", @@ -2248,7 +2253,7 @@ { "version": "v13", "date": "2026-07-19", - "note": "add CAP3-05 (SLM-94) equal-byte width × precision ladder planner and dry-run manifest support" + "note": "add CAP3-05 (SLM-94) equal-byte width \u00d7 precision ladder planner and dry-run manifest support" }, { "version": "v12", @@ -2367,7 +2372,7 @@ { "version": "v1", "date": "2026-07-19", - "note": "initial registration; SLM-133 EFS3-06 AST-sketch dedup × choice-native retrieval factorial wiring/fixture harness" + "note": "initial registration; SLM-133 EFS3-06 AST-sketch dedup \u00d7 choice-native retrieval factorial wiring/fixture harness" } ] }, @@ -2595,7 +2600,7 @@ { "version": "sde5-03-v1", "date": "2026-07-21", - "note": "initial registration; SLM-210 SDE5-03 prompt-plan × grammar-mass × high-debt exposure floor-escape matrix wiring/fixture harness" + "note": "initial registration; SLM-210 SDE5-03 prompt-plan \u00d7 grammar-mass \u00d7 high-debt exposure floor-escape matrix wiring/fixture harness" } ] }, @@ -3066,7 +3071,7 @@ { "version": "v1", "date": "2026-07-20", - "note": "initial registration of SLM-148 SPV1-05 plan-conditioned X22 × conflict-slice staged campaign wiring/fixture harness" + "note": "initial registration of SLM-148 SPV1-05 plan-conditioned X22 \u00d7 conflict-slice staged campaign wiring/fixture harness" } ] }, @@ -8647,6 +8652,20 @@ "note": "initial registration; SLM-249 (LOT0-02) CompilerReasoningTraceV1 typed step schema, deterministic extractor reusing SemanticPlanV1/production_codec, lossless visible serialization, permutation-invariant/cycle-free/truncation-explicit helpers, a bounded (n=16 fixture) K/c coverage probe, and the CompilerReasoningTraceGateV1 contract with an inconclusive verdict (oracle-ceiling experiment plan only, no training run, per the LOT0-01 authorization's allowed_lot1_work scope)" } ] + }, + "matrix.self_context_curriculum": { + "version": "v1", + "kind": "matrix", + "paths": [ + "scripts/run_self_context_curriculum.py" + ], + "history": [ + { + "version": "v1", + "date": "2026-07-25", + "note": "initial registration; SLM-300 AP-015 self-context exposure-bias curriculum plan/fixture CLI" + } + ] } } } diff --git a/tests/test_harnesses/experiments/test_self_context_curriculum.py b/tests/test_harnesses/experiments/test_self_context_curriculum.py new file mode 100644 index 000000000..e8deaf5cd --- /dev/null +++ b/tests/test_harnesses/experiments/test_self_context_curriculum.py @@ -0,0 +1,160 @@ +"""Tests for slm_training.harnesses.experiments.self_context_curriculum (SLM-300).""" + +from __future__ import annotations + +from pathlib import Path + +from slm_training.harnesses.experiments.self_context_curriculum import ( + MATRIX_SET, + MATRIX_VERSION, + SELF_CONTEXT_CURRICULUM_ID, + PolicyOrigin, + SelfContextCurriculumManifest, + build_self_context_manifest, + policy_origin_mixture, + render_markdown, + run_fixture_self_context_curriculum, + validate_manifest, +) + + +def test_default_manifest() -> None: + manifest = build_self_context_manifest() + assert manifest.curriculum_id == SELF_CONTEXT_CURRICULUM_ID + assert manifest.matrix_set == MATRIX_SET + assert manifest.matrix_version == MATRIX_VERSION + assert 0.0 in manifest.self_context_rates + assert manifest.status == "not_run" + assert manifest.parent_checkpoint_uri is None + + +def test_manifest_with_parent_is_pending() -> None: + manifest = build_self_context_manifest( + parent_checkpoint_uri="hf://bucket/checkpoint/ref.json", + checkpoint_bucket="hf://bucket", + ) + assert manifest.status == "frontier_pending_gpu" + + +def test_validate_manifest_ok() -> None: + manifest = build_self_context_manifest() + assert validate_manifest(manifest) == [] + + +def test_validate_missing_control() -> None: + manifest = build_self_context_manifest(self_context_rates=(0.10, 0.25)) + errors = validate_manifest(manifest) + assert any("0.0 control rate is required" in e for e in errors) + + +def test_validate_invalid_rate() -> None: + manifest = build_self_context_manifest(self_context_rates=(0.0, 0.10, 1.5)) + errors = validate_manifest(manifest) + assert any("self_context_rates must be in [0, 1]" in e for e in errors) + + +def test_validate_invalid_lagged_share() -> None: + manifest = SelfContextCurriculumManifest(lagged_policy_share=1.5) + errors = validate_manifest(manifest) + assert any("lagged_policy_share must be in [0, 1]" in e for e in errors) + + +def test_validate_frontier_requires_parent() -> None: + manifest = SelfContextCurriculumManifest( + self_context_rates=(0.0, 0.10), + claim_class="frontier", + parent_checkpoint_uri=None, + checkpoint_bucket=None, + ) + errors = validate_manifest(manifest) + assert any("parent_checkpoint_uri" in e for e in errors) + assert any("checkpoint_bucket" in e for e in errors) + + +def test_policy_origin_mixture_sums_to_one() -> None: + for rate in (0.0, 0.10, 0.25, 0.50, 1.0): + for lagged_share in (0.0, 0.3, 0.5, 1.0): + mixture = policy_origin_mixture(rate, lagged_share) + assert abs(sum(mixture.values()) - 1.0) < 1e-12 + assert set(mixture) == { + PolicyOrigin.GOLD.value, + PolicyOrigin.MODEL_CURRENT.value, + PolicyOrigin.MODEL_LAGGED.value, + } + + +def test_policy_origin_mixture_zero_reproduces_legacy_behavior() -> None: + """Mixture zero must reproduce legacy (gold-only) corruption exactly. + + This is the wiring-level proof of the issue's acceptance criterion: + "Mixture zero reproduces legacy behavior." Regardless of the lagged-policy + split, a self-context rate of 0.0 must yield 100% gold-derived corruption + and zero model-sampled mass. + """ + for lagged_share in (0.0, 0.25, 0.5, 0.75, 1.0): + mixture = policy_origin_mixture(0.0, lagged_share) + assert mixture == { + PolicyOrigin.GOLD.value: 1.0, + PolicyOrigin.MODEL_CURRENT.value: 0.0, + PolicyOrigin.MODEL_LAGGED.value: 0.0, + } + + +def test_policy_origin_mixture_rejects_out_of_range() -> None: + import pytest + + with pytest.raises(ValueError): + policy_origin_mixture(-0.1) + with pytest.raises(ValueError): + policy_origin_mixture(0.5, lagged_policy_share=1.1) + + +def test_policy_origin_mixture_splits_current_and_lagged() -> None: + mixture = policy_origin_mixture(0.5, lagged_policy_share=0.5) + assert mixture[PolicyOrigin.MODEL_CURRENT.value] == 0.25 + assert mixture[PolicyOrigin.MODEL_LAGGED.value] == 0.25 + assert mixture[PolicyOrigin.GOLD.value] == 0.5 + + +def test_run_fixture_curriculum(tmp_path: Path) -> None: + manifest = build_self_context_manifest( + self_context_rates=(0.0, 0.10), + seeds=(0, 1), + ) + report = run_fixture_self_context_curriculum( + manifest, run_id="test", output_dir=tmp_path + ) + assert report.status == "fixture" + assert report.matrix_set == MATRIX_SET + assert len(report.arms) == 4 # 2 rates * 2 seeds + assert all(a.status == "fixture_planned" for a in report.arms) + assert (tmp_path / "self_context_curriculum_report.json").exists() + + +def test_control_arm_has_pure_gold_mixture(tmp_path: Path) -> None: + manifest = build_self_context_manifest(self_context_rates=(0.0, 0.25), seeds=(0,)) + report = run_fixture_self_context_curriculum(manifest, run_id="control_test") + control_arms = [a for a in report.arms if a.self_context_rate == 0.0] + assert len(control_arms) == 1 + assert control_arms[0].policy_origin_mixture[PolicyOrigin.GOLD.value] == 1.0 + assert any("legacy" in note for note in control_arms[0].notes) + + +def test_render_markdown_includes_hypothesis() -> None: + manifest = build_self_context_manifest(self_context_rates=(0.0,)) + report = run_fixture_self_context_curriculum(manifest, run_id="md_test") + md = render_markdown(report) + assert "SLM-300" in md + assert manifest.hypothesis[:20] in md + assert "fixture-only" in md + assert "Policy origin mixture" in md + + +def test_render_markdown_arm_table() -> None: + manifest = build_self_context_manifest( + self_context_rates=(0.0, 0.10), seeds=(0,) + ) + report = run_fixture_self_context_curriculum(manifest, run_id="arm_test") + md = render_markdown(report) + assert "A_control" in md + assert "SC10" in md diff --git a/tests/test_scripts/test_self_context_curriculum.py b/tests/test_scripts/test_self_context_curriculum.py new file mode 100644 index 000000000..2bc944e7e --- /dev/null +++ b/tests/test_scripts/test_self_context_curriculum.py @@ -0,0 +1,78 @@ +"""Tests for scripts/run_self_context_curriculum.py (SLM-300).""" + +from __future__ import annotations + +from pathlib import Path + +from scripts import run_self_context_curriculum + + +def test_plan_only_mode(tmp_path: Path) -> None: + out = tmp_path / "plan" + rc = run_self_context_curriculum.main( + ["--mode", "plan-only", "--output-dir", str(out), "--seeds", "0"] + ) + assert rc == 0 + assert (out / "self_context_curriculum_manifest.json").exists() + assert (out / "self_context_curriculum_report.json").exists() + assert (out / "self_context_curriculum_report.md").exists() + + +def test_fixture_mode_with_parent(tmp_path: Path) -> None: + out = tmp_path / "fixture" + rc = run_self_context_curriculum.main( + [ + "--mode", + "fixture", + "--output-dir", + str(out), + "--seeds", + "0,1", + "--self-context-rates", + "0.0,0.10", + "--parent-checkpoint-uri", + "hf://bucket/checkpoint/ref.json", + ] + ) + assert rc == 0 + report_text = (out / "self_context_curriculum_report.json").read_text() + assert "fixture" in report_text + assert "slm300_fixture" in report_text + + +def test_invalid_rates_returns_error(tmp_path: Path) -> None: + out = tmp_path / "bad" + rc = run_self_context_curriculum.main( + [ + "--mode", + "plan-only", + "--output-dir", + str(out), + "--self-context-rates", + "0.10,0.25", + "--seeds", + "0", + ] + ) + assert rc == 1 + + +def test_frontier_mode_emits_fixture_plan(tmp_path: Path) -> None: + out = tmp_path / "frontier" + rc = run_self_context_curriculum.main( + [ + "--mode", + "frontier", + "--output-dir", + str(out), + "--seeds", + "0", + "--parent-checkpoint-uri", + "hf://bucket/checkpoint/ref.json", + "--checkpoint-bucket", + "hf://bucket", + ] + ) + assert rc == 0 + report_text = (out / "self_context_curriculum_report.json").read_text() + assert "slm300_frontier_partial" in report_text From 72f5de89631cf1a7ebd9994c9cd90f9219e6c5ce Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 25 Jul 2026 08:43:22 +0000 Subject: [PATCH 2/4] SLM-300: address CodeRabbit review on PR #895 - run_fixture_self_context_curriculum now calls validate_manifest and raises ValueError on an invalid manifest (missing 0.0 control, empty seeds), matching its own docstring and hardening the function as a trust boundary. - _arm_label is now lossless for arbitrary caller-supplied rates (e.g. 0.101 vs 0.104 no longer collide onto the same "SC10" label and run_id); default preregistered rates (0.10/0.25/0.50) render identically to before. - CLI: malformed --seeds/--self-context-rates CSV values now return a controlled exit code 2 instead of an uncaught ValueError traceback. - CLI: frontier mode's fixture fallback now returns exit code 2 instead of 0, so a scheduler checking only the exit code can't mistake it for a completed frontier run. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01KShriKrGosZr67yVPFgi78 --- ...0-self-context-exposure-bias-20260725.json | 6 ++-- scripts/run_self_context_curriculum.py | 20 +++++++---- .../experiments/self_context_curriculum.py | 10 +++++- src/slm_training/resources/versions.json | 14 ++++++-- .../test_self_context_curriculum.py | 27 ++++++++++++++ .../test_self_context_curriculum.py | 35 +++++++++++++++++-- 6 files changed, 98 insertions(+), 14 deletions(-) diff --git a/docs/design/iter-slm300-self-context-exposure-bias-20260725.json b/docs/design/iter-slm300-self-context-exposure-bias-20260725.json index c85615a3d..bc84da08c 100644 --- a/docs/design/iter-slm300-self-context-exposure-bias-20260725.json +++ b/docs/design/iter-slm300-self-context-exposure-bias-20260725.json @@ -322,12 +322,12 @@ "run_id": "slm300_fixture", "status": "fixture", "version_stamp": { - "code_commit": "86212e9ab2338bb9cf5ab135e85e3d3d4e7d21b5", + "code_commit": "caa144b0524a758dbdefa3b8e70b563dc126e11a", "code_dirty": true, "components": { - "harness.experiments": "v105" + "harness.experiments": "v106" }, "stamp_schema": "version_stamp/v1", - "stamped_at": "2026-07-25T08:30:53.792312+00:00" + "stamped_at": "2026-07-25T08:43:03.262457+00:00" } } diff --git a/scripts/run_self_context_curriculum.py b/scripts/run_self_context_curriculum.py index ec6d07c88..60a57a91b 100644 --- a/scripts/run_self_context_curriculum.py +++ b/scripts/run_self_context_curriculum.py @@ -78,10 +78,14 @@ def main(argv: list[str] | None = None) -> int: ) args = parser.parse_args(argv) - seeds = tuple(int(s.strip()) for s in args.seeds.split(",") if s.strip()) - rates = tuple( - float(x.strip()) for x in args.self_context_rates.split(",") if x.strip() - ) + try: + seeds = tuple(int(s.strip()) for s in args.seeds.split(",") if s.strip()) + rates = tuple( + float(x.strip()) for x in args.self_context_rates.split(",") if x.strip() + ) + except ValueError as error: + print(f"argument error: {error}", file=sys.stderr) + return 2 manifest = build_self_context_manifest( parent_checkpoint_uri=args.parent_checkpoint_uri, @@ -99,6 +103,7 @@ def main(argv: list[str] | None = None) -> int: args.output_dir.mkdir(parents=True, exist_ok=True) manifest.to_json(args.output_dir / "self_context_curriculum_manifest.json") + exit_code = 0 if args.mode == "plan-only": report = run_fixture_self_context_curriculum( manifest, run_id="slm300_plan", output_dir=args.output_dir @@ -110,12 +115,15 @@ def main(argv: list[str] | None = None) -> int: else: print( "frontier mode requires GPU host and durable checkpoints; " - "emitting fixture plan only", + "emitting fixture plan only (not a completed frontier run)", file=sys.stderr, ) report = run_fixture_self_context_curriculum( manifest, run_id="slm300_frontier_partial", output_dir=args.output_dir ) + # A fixture fallback must never be mistaken for completed frontier + # evidence by a scheduler that only checks the exit code. + exit_code = 2 markdown = render_markdown(report) (args.output_dir / "self_context_curriculum_report.md").write_text( @@ -123,7 +131,7 @@ def main(argv: list[str] | None = None) -> int: ) report.to_json(args.output_dir / "self_context_curriculum_report.json") print(markdown) - return 0 + return exit_code if __name__ == "__main__": diff --git a/src/slm_training/harnesses/experiments/self_context_curriculum.py b/src/slm_training/harnesses/experiments/self_context_curriculum.py index ed58db68d..2e69fd9f4 100644 --- a/src/slm_training/harnesses/experiments/self_context_curriculum.py +++ b/src/slm_training/harnesses/experiments/self_context_curriculum.py @@ -256,7 +256,7 @@ def validate_manifest(manifest: SelfContextCurriculumManifest) -> list[str]: def _arm_label(rate: float) -> str: if rate == 0.0: return "A_control" - return f"SC{int(round(rate * 100)):02d}" + return f"SC{rate * 100:g}".replace(".", "p") def run_fixture_self_context_curriculum( @@ -270,7 +270,15 @@ def run_fixture_self_context_curriculum( The policy-origin mixture is computed exactly (it is a pure function of the manifest, not a training result); every other per-arm metric remains ``None`` because no model is trained in this wiring slice. + + Raises: + ValueError: if ``manifest`` fails :func:`validate_manifest` (e.g. a + missing ``0.0`` control rate or empty seeds). """ + errors = validate_manifest(manifest) + if errors: + raise ValueError("; ".join(errors)) + arms: list[SelfContextArmResult] = [] time.perf_counter() # mark start timing for future telemetry for rate in manifest.self_context_rates: diff --git a/src/slm_training/resources/versions.json b/src/slm_training/resources/versions.json index 5b1671ca3..a16996eb7 100644 --- a/src/slm_training/resources/versions.json +++ b/src/slm_training/resources/versions.json @@ -1716,7 +1716,7 @@ ] }, "harness.experiments": { - "version": "v109", + "version": "v110", "kind": "harness", "paths": [ "src/slm_training/harnesses/experiments/", @@ -1725,6 +1725,11 @@ "tests/test_scripts/test_run_slm298_capacity_context_curriculum.py" ], "history": [ + { + "version": "v110", + "date": "2026-07-25", + "note": "SLM-300 self-context curriculum: validate manifest inside run_fixture_self_context_curriculum (trust-boundary hardening) and make _arm_label lossless for arbitrary caller-supplied rates (address CodeRabbit review on PR #895)" + }, { "version": "v109", "date": "2026-07-25", @@ -8654,12 +8659,17 @@ ] }, "matrix.self_context_curriculum": { - "version": "v1", + "version": "v2", "kind": "matrix", "paths": [ "scripts/run_self_context_curriculum.py" ], "history": [ + { + "version": "v2", + "date": "2026-07-25", + "note": "frontier fallback now returns nonzero exit code instead of masquerading as a completed frontier run; malformed --seeds/--self-context-rates CSV values now return a controlled exit code 2 instead of an uncaught traceback (address CodeRabbit review on PR #895)" + }, { "version": "v1", "date": "2026-07-25", diff --git a/tests/test_harnesses/experiments/test_self_context_curriculum.py b/tests/test_harnesses/experiments/test_self_context_curriculum.py index e8deaf5cd..b91347eed 100644 --- a/tests/test_harnesses/experiments/test_self_context_curriculum.py +++ b/tests/test_harnesses/experiments/test_self_context_curriculum.py @@ -158,3 +158,30 @@ def test_render_markdown_arm_table() -> None: md = render_markdown(report) assert "A_control" in md assert "SC10" in md + + +def test_run_fixture_rejects_invalid_manifest_directly() -> None: + """The runner is itself a trust boundary: a hand-built invalid manifest + + (bypassing the CLI's own validate_manifest call) must not silently + produce a successful fixture report. + """ + manifest = SelfContextCurriculumManifest( + self_context_rates=(0.10, 0.25), seeds=(0,) + ) + import pytest + + with pytest.raises(ValueError, match="0.0 control rate is required"): + run_fixture_self_context_curriculum(manifest, run_id="invalid_test") + + +def test_arm_labels_are_lossless_for_close_rates() -> None: + """Distinct nearby rates must not collide onto the same arm label/run_id.""" + manifest = build_self_context_manifest( + self_context_rates=(0.0, 0.101, 0.104), seeds=(0,) + ) + report = run_fixture_self_context_curriculum(manifest, run_id="close_rates") + labels = {arm.arm_label for arm in report.arms} + run_ids = {arm.run_id for arm in report.arms} + assert len(labels) == 3 + assert len(run_ids) == 3 diff --git a/tests/test_scripts/test_self_context_curriculum.py b/tests/test_scripts/test_self_context_curriculum.py index 2bc944e7e..2111fe562 100644 --- a/tests/test_scripts/test_self_context_curriculum.py +++ b/tests/test_scripts/test_self_context_curriculum.py @@ -57,7 +57,13 @@ def test_invalid_rates_returns_error(tmp_path: Path) -> None: assert rc == 1 -def test_frontier_mode_emits_fixture_plan(tmp_path: Path) -> None: +def test_frontier_mode_emits_fixture_plan_but_reports_nonzero(tmp_path: Path) -> None: + """Frontier fallback must never look like a completed frontier run. + + A scheduler that only checks the exit code must be able to tell that no + real GPU work happened, even though the fixture artifacts are still + written for inspection. + """ out = tmp_path / "frontier" rc = run_self_context_curriculum.main( [ @@ -73,6 +79,31 @@ def test_frontier_mode_emits_fixture_plan(tmp_path: Path) -> None: "hf://bucket", ] ) - assert rc == 0 + assert rc != 0 report_text = (out / "self_context_curriculum_report.json").read_text() assert "slm300_frontier_partial" in report_text + + +def test_malformed_seeds_returns_controlled_error(tmp_path: Path) -> None: + out = tmp_path / "bad_seeds" + rc = run_self_context_curriculum.main( + ["--mode", "plan-only", "--output-dir", str(out), "--seeds", "0,x"] + ) + assert rc == 2 + assert not (out / "self_context_curriculum_manifest.json").exists() + + +def test_malformed_rates_returns_controlled_error(tmp_path: Path) -> None: + out = tmp_path / "bad_rates" + rc = run_self_context_curriculum.main( + [ + "--mode", + "plan-only", + "--output-dir", + str(out), + "--self-context-rates", + "0.0,not-a-float", + ] + ) + assert rc == 2 + assert not (out / "self_context_curriculum_manifest.json").exists() From 2da5a2eed1cd7bdfc10c0741d2a3e74ab55cb743 Mon Sep 17 00:00:00 2001 From: Tyler Kendrick <145080887+Tyler-R-Kendrick@users.noreply.github.com> Date: Sat, 25 Jul 2026 10:47:19 -0500 Subject: [PATCH 3/4] fix: restore main CI after #920 (levers, tokenizers, vocab remap) Main #920 dropped DEFAULT_* / PROHIBITED lever constants, left selection importing DIFFICULTY_EASY_TAIL_FRACTION without defining it, expanded STRUCTURAL_ID_ATOMS without tokenizer version/layout updates, and removed _remap_vocab_weight while still calling it. Restore those and refresh the layout registry so data-build + verify_tokenizer_grammar_invariants pass. --- src/slm_training/dsl/language_contract.py | 8 +- src/slm_training/levers.py | 82 +++++++++++++++++++ src/slm_training/models/choice_tokenizer.py | 11 ++- src/slm_training/models/dsl_tokenizer.py | 2 +- src/slm_training/models/twotower.py | 27 ++++++ .../resources/tokenizer_layout_registry.json | 30 +++++-- src/slm_training/resources/versions.json | 26 +++++- 7 files changed, 170 insertions(+), 16 deletions(-) diff --git a/src/slm_training/dsl/language_contract.py b/src/slm_training/dsl/language_contract.py index ddda3b169..0495e6041 100644 --- a/src/slm_training/dsl/language_contract.py +++ b/src/slm_training/dsl/language_contract.py @@ -515,14 +515,18 @@ def output_contract_violations( if not token.startswith(f'{LIT_PREFIX}"'): continue value = json.loads(token[len(LIT_PREFIX) :]) - if not is_placeholder(value) and value not in grammar_string_literals(): + if ( + not is_placeholder(value) + and value not in grammar_string_literals() + and value not in STRUCTURAL_ID_ATOMS + ): violations.append(value) return tuple(dict.fromkeys(violations)) # Official document validation rejects content literals before encoding; # inspect that repairable AST to report the contract violation itself. bindings = parse_statement_bindings(source, validate=False) - allowed = grammar_string_literals() + allowed = grammar_string_literals() | STRUCTURAL_ID_ATOMS violations: list[str] = [] def walk(value: Any) -> None: diff --git a/src/slm_training/levers.py b/src/slm_training/levers.py index f62070412..a76652fe5 100644 --- a/src/slm_training/levers.py +++ b/src/slm_training/levers.py @@ -30,6 +30,9 @@ MAX_HARNESS_WALL_MINUTES: Final = MAX_HARNESS_WALL_SECONDS / 60 HF_JOB_TIMEOUT: Final = f"{MAX_RUN_MINUTES}m" CHANGED_TEST_WORKERS: Final = 4 +# Superfiltering easy-tail fraction for build-time difficulty curation +# (records below this NLL percentile are down-weighted / droppable). +DIFFICULTY_EASY_TAIL_FRACTION: Final = 0.2 VERCEL_FUNCTION_INCLUDE_FILES: Final = ( "docs/design/*.json", "docs/MODEL_CARD.md", @@ -45,6 +48,58 @@ # Capability profiles are deliberately expressed as deviations from the # model-build defaults. A default-off lever is harmless; activating it requires # the listed certificate stage. + +# Active, source-controlled corpora. Historical snapshots remain immutable +# evidence but fail the canonical marker guard and are never CLI defaults. +DEFAULT_TRAIN_DATA_DIR: Final = Path( + "src/slm_training/resources/data/train/e937_role_safe_all_targets_v2" +) +DEFAULT_EVAL_DATA_DIR: Final = Path( + "src/slm_training/resources/data/eval/e938_role_safe_all_targets_v2" +) +DEFAULT_CONTEXT_BACKEND: Final = "scratch" + +# Template markers are codec identities, never semantic supervision. Keep this +# policy beside every other user-facing lever so there is one discoverable +# source of truth for training and decode configuration. +TEMPLATE_MARKERS_ARE_OPAQUE: Final = True +DEFAULT_OUTPUT_TOKENIZER: Final = "lexer" +DEFAULT_DECODE_TIMEOUT_SECONDS: Final = 12.0 +CHECKPOINT_DECLARED_POLICY: Final = "checkpoint_declared" +STRICT_COMPILER_TREE_POLICY_ID: Final = "strict_compiler_tree" +# Every evaluation enforces the symbol-only completion boundary independently +# of checkpoint provenance. The compiler-tree bundle is the canonical default. +DEFAULT_EVALUATION_POLICY: Final = STRICT_COMPILER_TREE_POLICY_ID +STRICT_EVALUATION_POLICY: Final = { + "grammar_constrained": True, + "grammar_ltr_primary": True, + "grammar_finalize_validate": True, + "slot_contract_constrained_decode": True, + "honest_slot_contract": True, + "allow_unconstrained_fallback": False, +} +STRICT_COMPILER_TREE_POLICY: Final = { + **STRICT_EVALUATION_POLICY, + "output_tokenizer": DEFAULT_OUTPUT_TOKENIZER, + "compiler_decode_mode": "tree", + # Structural AST-plan scoring is part of the policy, not a caller-owned + # pair of optional flags. This consumes no marker names or free-form text. + "semantic_plan_decode_weight": 4.0, + "semantic_plan_margin_decode_weight": 2.0, +} +PROHIBITED_TEMPLATE_SEMANTIC_LEVERS: Final = { + "namespace_augment": "renames opaque markers into user-defined namespaces", + "prompt_semantic_role_contract": "adds user-defined marker labels to training prompts", + "semantic_role_contract_in_context": "exposes user-defined marker labels", + "semantic_role_decode_weight": "scores user-defined marker labels", + "semantic_role_schema_candidates": "maps user-defined marker labels to schema", + "schema_role_slot_decode_weight": "maps user-defined marker labels to schema", + "slot_coverage_close_decode_weight": "uses marker-label-derived schema reachability", + "semantic_plan_repeated_slot_margin_decode_weight": ( + "groups markers by user-defined namespace labels" + ), +} + CAPABILITY_LEVER_MINIMUMS: Final = { # CAP1 introduces authored schema / natural-language conditioning. "design_md_in_context": (False, Capability.CAP1_SEMANTICS), @@ -475,6 +530,33 @@ def lever_catalog() -> dict[str, dict[str, Any]]: "type": "tuple[str, ...]", "source": "slm_training.levers.VERCEL_FUNCTION_INCLUDE_FILES", } + + catalog["template_markers_are_opaque"] = { + "category": "data", + "default": TEMPLATE_MARKERS_ARE_OPAQUE, + "type": "bool", + "source": "slm_training.levers.TEMPLATE_MARKERS_ARE_OPAQUE", + } + catalog["default_train_data_dir"] = { + "category": "data", + "default": str(DEFAULT_TRAIN_DATA_DIR), + "type": "Path", + "source": "slm_training.levers.DEFAULT_TRAIN_DATA_DIR", + } + catalog["default_eval_data_dir"] = { + "category": "data", + "default": str(DEFAULT_EVAL_DATA_DIR), + "type": "Path", + "source": "slm_training.levers.DEFAULT_EVAL_DATA_DIR", + } + catalog["default_context_backend"] = { + "category": "model", + "default": DEFAULT_CONTEXT_BACKEND, + "type": "str", + "choices": ["scratch", "hf"], + "source": "slm_training.levers.DEFAULT_CONTEXT_BACKEND", + } + from slm_training.harnesses.model_build.eval_policy import EVALUATION_POLICIES catalog["evaluation_policy"].update( diff --git a/src/slm_training/models/choice_tokenizer.py b/src/slm_training/models/choice_tokenizer.py index 6fc25dbbe..888f7498e 100644 --- a/src/slm_training/models/choice_tokenizer.py +++ b/src/slm_training/models/choice_tokenizer.py @@ -27,7 +27,10 @@ from typing import TYPE_CHECKING, Any, Iterable from slm_training.dsl.lang_core import ParseError -from slm_training.dsl.language_contract import grammar_string_literals +from slm_training.dsl.language_contract import ( + STRUCTURAL_ID_ATOMS, + grammar_string_literals, +) from slm_training.dsl.production_codec import ( BUILTIN_PREFIX, CHOICE_STMT_MARKERS, @@ -59,7 +62,7 @@ from slm_training.dsl.grammar.fastpath.compiler_draft import ConstraintEvidence # v2 removes the free-form string opener from new vocabularies. -CHOICE_TOKENIZER_VERSION = 2 +CHOICE_TOKENIZER_VERSION = 3 CHOICE_TOKENIZER_KIND = "choice_codec" PAD = "" @@ -123,7 +126,9 @@ def _byte_token(ch: str) -> str: ) # One schema-derived closed set shared by validation and both output tokenizers. -_FIXED_STRING_BODIES: tuple[str, ...] = tuple(sorted(grammar_string_literals())) +_FIXED_STRING_BODIES: tuple[str, ...] = tuple( + sorted(grammar_string_literals() | STRUCTURAL_ID_ATOMS) +) def _grammar_names() -> tuple[str, ...]: diff --git a/src/slm_training/models/dsl_tokenizer.py b/src/slm_training/models/dsl_tokenizer.py index 0ee5c645b..0dbc41f6a 100644 --- a/src/slm_training/models/dsl_tokenizer.py +++ b/src/slm_training/models/dsl_tokenizer.py @@ -58,7 +58,7 @@ def _active_structural_tokens() -> frozenset[str]: # Bump when serialization / vocab layout changes. # v4: removed the free-form string literal opener. -DSL_TOKENIZER_VERSION = 4 +DSL_TOKENIZER_VERSION = 5 SYMBOL_TABLE_VERSION = 3 PAD = "" diff --git a/src/slm_training/models/twotower.py b/src/slm_training/models/twotower.py index f83183ba0..0c22cd900 100644 --- a/src/slm_training/models/twotower.py +++ b/src/slm_training/models/twotower.py @@ -814,6 +814,33 @@ def _load_checkpoint_state( ) +def _remap_vocab_weight( + source_weight: torch.Tensor, + source_token_to_id: dict[str, int], + target_weight: torch.Tensor, + target_token_to_id: dict[str, int], +) -> torch.Tensor: + """Copy shared token rows while retaining initialized rows for new tokens.""" + remapped = target_weight.detach().clone() + for token, target_id in target_token_to_id.items(): + source_id = source_token_to_id.get(token) + if source_id is not None: + remapped[target_id] = source_weight[source_id] + return remapped + + +def _resize_position_weight( + source_weight: torch.Tensor, target_weight: torch.Tensor +) -> torch.Tensor: + """Copy the shared learned position prefix across context-length changes.""" + if source_weight.ndim != 2 or source_weight.shape[1:] != target_weight.shape[1:]: + return source_weight + resized = target_weight.detach().clone() + shared = min(source_weight.shape[0], target_weight.shape[0]) + resized[:shared] = source_weight[:shared] + return resized + + def _check_output_head_tie_migration( model: "TwoTowerModel", source_config: dict[str, Any], diff --git a/src/slm_training/resources/tokenizer_layout_registry.json b/src/slm_training/resources/tokenizer_layout_registry.json index 2ede07c4b..795388d6b 100644 --- a/src/slm_training/resources/tokenizer_layout_registry.json +++ b/src/slm_training/resources/tokenizer_layout_registry.json @@ -3,9 +3,9 @@ "namespace_registry_version": 2, "codecs": { "choice_codec": { - "version": 2, - "vocab_size": 717, - "layout_sha256": "df0a1a317757fc0692dfce70b08007e63f5de77e9a5b6862d1e5dbeb48804574", + "version": 3, + "vocab_size": 781, + "layout_sha256": "50d5ee9484c972a76f8e2d2d58129943380dcb5dbd28099a21795f1b25ce19c8", "special_ids": { "pad": 0, "bos": 1, @@ -15,9 +15,9 @@ } }, "dsl_native": { - "version": 4, - "vocab_size": 505, - "layout_sha256": "5ce4a6f69b12024a9ea1c5ef4547659b0f9f7c058fc60c86e58fc1f19eeb559a", + "version": 5, + "vocab_size": 569, + "layout_sha256": "381a05d05d6fbe85366997286e9e36b7d3d89c9771efe7b24117a45534793a0a", "special_ids": { "pad": 0, "bos": 1, @@ -26,5 +26,21 @@ "unk": 4 } } - } + }, + "migration_notes": [ + { + "date": "2026-07-25", + "codec": "choice_codec", + "from_version": 2, + "to_version": 3, + "note": "STRUCTURAL_ID_ATOMS ($0..$63) added to fixed string bodies so choice encode matches native/structural-id contract from main #920" + }, + { + "date": "2026-07-25", + "codec": "dsl_native", + "from_version": 4, + "to_version": 5, + "note": "STRUCTURAL_ID_ATOMS folded into fixed string bodies (vocab 505->569); layout fingerprint refreshed after main #920" + } + ] } diff --git a/src/slm_training/resources/versions.json b/src/slm_training/resources/versions.json index a16996eb7..9a03fc627 100644 --- a/src/slm_training/resources/versions.json +++ b/src/slm_training/resources/versions.json @@ -27,13 +27,18 @@ ] }, "config.levers": { - "version": "v22", + "version": "v23", "kind": "harness", "paths": [ "src/slm_training/levers.py", "tests/test_levers.py" ], "history": [ + { + "version": "v23", + "date": "2026-07-25", + "note": "Restore DEFAULT_* data/eval/context/tokenizer/evaluation-policy constants and PROHIBITED_TEMPLATE_SEMANTIC_LEVERS dropped by main #920; add DIFFICULTY_EASY_TAIL_FRACTION (0.2)" + }, { "version": "v22", "date": "2026-07-25", @@ -811,7 +816,7 @@ ] }, "dsl.symbolic_surface": { - "version": "v3", + "version": "v4", "kind": "harness", "paths": [ "src/slm_training/dsl/language_contract.py", @@ -819,6 +824,11 @@ "tests/test_dsl/test_language_contract.py" ], "history": [ + { + "version": "v4", + "date": "2026-07-25", + "note": "Allow STRUCTURAL_ID_ATOMS ($0..$63) in symbol-only output contract checks so structural id props are not treated as free-form strings" + }, { "version": "v3", "date": "2026-07-25", @@ -6901,7 +6911,7 @@ ] }, "model.twotower": { - "version": "v244", + "version": "v245", "kind": "harness", "paths": [ "src/slm_training/models/twotower.py", @@ -6936,6 +6946,11 @@ "src/slm_training/resources/decode/speculative_ngram_v1.json" ], "history": [ + { + "version": "v245", + "date": "2026-07-25", + "note": "Bump DSL_TOKENIZER_VERSION 4->5 and CHOICE_TOKENIZER_VERSION 2->3; fold STRUCTURAL_ID_ATOMS into fixed string bodies; restore _remap_vocab_weight/_resize_position_weight dropped by main #920; refresh tokenizer layout registry" + }, { "version": "v244", "date": "2026-07-25", @@ -8627,6 +8642,11 @@ "date": "2026-07-25", "note": "SLM-413: rebuilt registry for the new conversation.transaction_commit op (DSH5-05's TRANSACTION_COMMIT turn) -- 19 -> 20 ops; every history-family/set/topology token_id after the insertion point shifts by one, per `python -c 'from slm_training.dsl.ops_vocab import write_registry; write_registry()'`" }, + { + "version": "v1", + "date": "2026-07-25", + "note": "no-bump: refresh checked-in dsl_native + choice_codec tokenizer layout registry fingerprints after STRUCTURAL_ID_ATOMS vocab expansion (main #920 CI unblock); ops token-id namespace unchanged" + }, { "version": "v1", "date": "2026-07-25", From d3b1c9c548d7f492b280870fc47c7e10b1798e54 Mon Sep 17 00:00:00 2001 From: Tyler Kendrick <145080887+Tyler-R-Kendrick@users.noreply.github.com> Date: Sat, 25 Jul 2026 10:50:21 -0500 Subject: [PATCH 4/4] fix(SLM-300): prepend ops.vocab no-bump after rebase onto main Keep ops.vocab history append-only vs main v2 (SLM-413) after the tokenizer layout registry refresh for #920 CI recovery. --- src/slm_training/resources/versions.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/slm_training/resources/versions.json b/src/slm_training/resources/versions.json index 9a03fc627..13a10c076 100644 --- a/src/slm_training/resources/versions.json +++ b/src/slm_training/resources/versions.json @@ -555,7 +555,7 @@ { "version": "v1", "date": "2026-07-25", - "note": "no-bump: DSH5-05 extracts the private _merge_value/_StructuralConflict/MergeConflictKind primitive into the new src/slm_training/dsl/operators/ast_merge.py module (merge_ast_value/StructuralMergeConflict/MergeConflictKind) so the transaction executor's N-way fold can reuse it without importing merge.py (which imports conversation.py) and closing an import cycle; merge.py now imports from ast_merge.py instead of defining these locally — public behavior and every test_operator_merge.py test are unchanged" + "note": "no-bump: DSH5-05 extracts the private _merge_value/_StructuralConflict/MergeConflictKind primitive into the new src/slm_training/dsl/operators/ast_merge.py module (merge_ast_value/StructuralMergeConflict/MergeConflictKind) so the transaction executor's N-way fold can reuse it without importing merge.py (which imports conversation.py) and closing an import cycle; merge.py now imports from ast_merge.py instead of defining these locally \u2014 public behavior and every test_operator_merge.py test are unchanged" }, { "version": "v1", @@ -769,7 +769,7 @@ { "version": "v3", "date": "2026-07-25", - "note": "DSH5-05: extend _target_lineage to also map ReferenceTableV1.selectors (not just .entries), so a selector-argument operator (openui.map_set_property, DSH5-02's only bulk operator) can be prepared into a transaction at all — previously unresolvable in both prepare_operator_action's semantic-id derivation and derive_read_write_set's precondition footprint, invisible until DSH5-05's bulk-plus-primitive matrix requirement exercised it; purely additive, no existing non-selector lineage entry changes" + "note": "DSH5-05: extend _target_lineage to also map ReferenceTableV1.selectors (not just .entries), so a selector-argument operator (openui.map_set_property, DSH5-02's only bulk operator) can be prepared into a transaction at all \u2014 previously unresolvable in both prepare_operator_action's semantic-id derivation and derive_read_write_set's precondition footprint, invisible until DSH5-05's bulk-plus-primitive matrix requirement exercised it; purely additive, no existing non-selector lineage entry changes" }, { "version": "v2", @@ -796,7 +796,7 @@ { "version": "v1", "date": "2026-07-25", - "note": "DSH5-05: initial atomic multi-action operator transaction executor — independent per-action constituent replay against one fixed base, base-relative N-way AST composition (a fold over the new shared ast_merge.merge_ast_value primitive, extracted from merge.py to avoid a conversation<->transaction_executor import cycle), a real structural-conflict safety net over declared-but-unverified commutativity, full pack-authority validation, fresh branch-local reference-table continuation via a caller-supplied builder, exact commit replay, and one CRDT conversation turn (TRANSACTION_COMMIT) carrying the whole transaction with every constituent application ID retained" + "note": "DSH5-05: initial atomic multi-action operator transaction executor \u2014 independent per-action constituent replay against one fixed base, base-relative N-way AST composition (a fold over the new shared ast_merge.merge_ast_value primitive, extracted from merge.py to avoid a conversation<->transaction_executor import cycle), a real structural-conflict safety net over declared-but-unverified commutativity, full pack-authority validation, fresh branch-local reference-table continuation via a caller-supplied builder, exact commit replay, and one CRDT conversation turn (TRANSACTION_COMMIT) carrying the whole transaction with every constituent application ID retained" } ] }, @@ -8640,12 +8640,12 @@ { "version": "v2", "date": "2026-07-25", - "note": "SLM-413: rebuilt registry for the new conversation.transaction_commit op (DSH5-05's TRANSACTION_COMMIT turn) -- 19 -> 20 ops; every history-family/set/topology token_id after the insertion point shifts by one, per `python -c 'from slm_training.dsl.ops_vocab import write_registry; write_registry()'`" + "note": "no-bump: refresh checked-in dsl_native + choice_codec tokenizer layout registry fingerprints after STRUCTURAL_ID_ATOMS vocab expansion (main #920 CI unblock); ops token-id namespace unchanged" }, { - "version": "v1", + "version": "v2", "date": "2026-07-25", - "note": "no-bump: refresh checked-in dsl_native + choice_codec tokenizer layout registry fingerprints after STRUCTURAL_ID_ATOMS vocab expansion (main #920 CI unblock); ops token-id namespace unchanged" + "note": "SLM-413: rebuilt registry for the new conversation.transaction_commit op (DSH5-05's TRANSACTION_COMMIT turn) -- 19 -> 20 ops; every history-family/set/topology token_id after the insertion point shifts by one, per `python -c 'from slm_training.dsl.ops_vocab import write_registry; write_registry()'`" }, { "version": "v1",