Skip to content

feat(guardrails): semantic preservation constraint + holdout integrity (dedup, seeded splits) - #151

Closed
MaxFreedomPollard wants to merge 1 commit into
NousResearch:mainfrom
MaxFreedomPollard:feat/eval-integrity-guardrails
Closed

MaxFreedomPollard wants to merge 1 commit into
NousResearch:mainfrom
MaxFreedomPollard:feat/eval-integrity-guardrails

Conversation

@MaxFreedomPollard

Copy link
Copy Markdown
Contributor

The problem

PLAN.md's Constraints & Guardrails section says every candidate variant "must pass ALL of these" before it can be considered valid. Two of those guardrails exist only on paper:

1. Semantic preservation (constraint 4). The PLAN requires that evolved text is "compared against the original to ensure it hasn't drifted too far in meaning", and the README's guardrail list already advertises it. Nothing implements it. validate_all checks size, growth, non-emptiness, and structure; a skill for GitHub code review could evolve into a poem and pass every gate.

2. Holdout integrity. The Safety section relies on "Holdout test sets, separate from training data, to catch overfitting". Two things quietly break that separation today:

  • No deduplication anywhere. LLM dataset generation emits near-identical test cases, and mined session history repeats the same prompts constantly. When two copies of one task land on opposite sides of the split, the holdout is contaminated and a measured "improvement" can be pure memorization.
  • All three dataset sources (synthetic, golden, external importers) split with an unseeded global random.shuffle, so re-building a dataset silently reshuffles holdout membership between runs. An example that was holdout yesterday can be training data today.

Both problems corrupt the exact number Phase 1's gate ("at least one skill measurably improved") is judged on.

What this adds

Semantic preservation constraint

semantic_similarity(baseline, evolved) in constraints.py: cosine similarity over content-term frequencies (4+ char words minus a small stopword list). Deterministic, dependency-free, zero API cost. A new semantic_preservation check runs whenever validate_all receives a baseline_text, which is exactly the existing evolved-candidate call in evolve_skill.py, so no caller changes are needed and baseline-only validation is unaffected.

Calibrated against real hermes-agent skills (arxiv, pixel-art, dcf-model, docker-management, cli):

Comparison Similarity
Same skill, rewritten (half-doc, or 33% deleted + lines shuffled) 0.84 to 0.99
Pairs of unrelated skills 0.04 to 0.16

The default threshold of 0.4 sits in the middle of that gap with wide margin on both sides. Failures report the score plus the baseline vocabulary that disappeared ("Baseline terms missing from evolved text: review, diff, ..."), which tells a human reviewer at a glance what the mutation destroyed. min_semantic_similarity = 0 disables the check.

Holdout integrity

Two helpers in dataset_builder.py, applied to all three dataset sources:

  • dedupe_examples(): removes exact duplicates (normalized task text) and near-duplicates (token-set Jaccard, default 0.9), keeping the first occurrence. The importer path reports how many were removed.
  • split_examples(): one seeded shuffled split (default seed 13, configurable) replacing the three copies of the unseeded shuffle-and-slice logic. The same examples now always produce the same train/val/holdout membership, and the input list is not mutated.

New config knobs: min_semantic_similarity, dedup_jaccard, dataset_split_seed.

Deliberate scope boundaries

Test plan

  • 26 new tests: similarity behavior (identity, rewrite, drift, empty, symmetry), constraint gating (runs only with a baseline, pass/fail, lost-terms reporting, threshold 0 disables), dedup (exact, case/punctuation variants, near-dup, threshold respected, first-kept), splits (ratios, seed determinism, disjointness, input not mutated), and golden-loader integration (deterministic across calls, dedupes on load).
  • Full suite: 169 passed (143 existing + 26 new), fully offline.
  • Calibration reproducible with the five skill files named above and semantic_similarity().

Two guardrails that PLAN.md says every candidate must pass existed only
on paper. This implements both.

Semantic preservation (PLAN.md constraint 4): evolved text must not
drift from the baseline's purpose. Measured as content-term cosine
similarity between baseline and evolved text, deterministic and free.
Calibrated on real hermes-agent skills (arxiv, pixel-art, dcf-model,
docker-management, cli): rewrites of the same skill score 0.84 to 0.99
while pairs of unrelated skills score 0.04 to 0.16, so the default 0.4
threshold has wide margin on both sides. Failures report which baseline
vocabulary disappeared. Runs whenever validate_all gets a baseline, so
the evolved-candidate path picks it up with no caller changes. Setting
min_semantic_similarity to 0 disables it.

Holdout integrity: LLM generation and mined session history both
produce repeated tasks, and nothing stopped two copies of one task
from landing in train and holdout, where measured improvement can be
pure memorization. New dedupe_examples() removes exact duplicates
(normalized text) and near-duplicates (token-set Jaccard, default 0.9)
before any split. New split_examples() replaces the three unseeded
random.shuffle splits (synthetic, golden, external importers) with one
seeded implementation, so a dataset always splits the same way and
re-running a build cannot silently move holdout examples into train.

No changes to evolve_skill.py, skill_module.py, or optimizer code.
New tests live in new files. 26 new tests; full suite passes (169).
@MaxFreedomPollard

Copy link
Copy Markdown
Contributor Author

Closing in favour of #162. The two halves of this land differently there. Holdout integrity is already present: evolution/tools/selection_eval.py seeds its train/val/holdout split and rejects duplicate tasks before splitting, so a memorised train example cannot reach the holdout set. The semantic preservation constraint as written here is incompatible with Phase 2. It scores cosine over content-term frequency vectors, so a description that repeats one clause six times has that clause dominate its vector, and removing the repetition scores 0.29 against the 0.40 floor and is rejected as topic drift. Collapsing bloated descriptions is precisely what Phase 2 exists to do, and twelve descriptions in a stock hermes-agent checkout are already over the 500-char budget. The constraint is worth having, so it is being carried into #162 in a form that compares the set of content terms rather than their counts, and that treats a candidate introducing no vocabulary the baseline did not already have as having kept its subject.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant