feat(calibration): score what a run made happen, not what it lost - #27
Merged
Conversation
Two hand-written controls on ALE Breakout differ only in what they read: the ASCII frame at four screen pixels per character, or the RAM channels at one pixel each. The RAM control scores 24 to 7 at 600 decisions and never dies. It scored one milestone LOWER, because the packaged life-lost milestone is lives == 4: a point for dying. ProgressionKind splits a milestone into achievement and attrition. The split is measured, not declared, and it reads no field name. measureProgressions watches every numeric channel the evidence publishes across the reference and every baseline, and calls a milestone attrition when its check reads a numeric channel, that channel never rose and fell at least once, and the check does not hold at the initial value. Attrition propagates through requires. scoreAchievements drops attrition from the numerator and the denominator; separating and separates now count legible achievement milestones only. report.collapse states how much of a contract hangs off one milestone, whether a trivial baseline reaches it, and how many milestones first pass at the same reference input. Six of six Breakout milestones require score-opened, and three of the six open at input 32. PackagedContract.calibrate is the only way to build a PackagedContract, and it runs the whole gate. Calibration was optional before: the packaged Airstriker contract reports separates false with an empty separating set, and it shipped. nonSeparating is the escape hatch for a demonstration target, and it is refused when the contract does separate. pnpm test:ale, pnpm test:retro, and pnpm test:gym now calibrate the packaged contracts. No adapter file is edited, and every contract keeps its bytes and its hash.
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
deriveContractsamples milestones from one reference playthrough. Three failures followed, and all three are measured here.1. A milestone anti-correlated with skill
ALE Breakout's
life-lostislives == 4: earned by dying. Two deterministic controls, same control law, same deadzone in screen pixels, differing only in what they read. Seed 0, no model spend, both now gates inpnpm test:ale.steer-from-asciisteer-from-ramram_ball_x,ram_paddle_xThe RAM control wins every column the game itself reports and never dies. Under the whole contract it scored LOWER. Under achievements alone the ordering is no longer backwards.
2. A contract that does not separate, shipped
calibrateContracton the packaged stable-retro Airstriker contract:separates: false, separating set empty. A seeded pseudo-random walk over the 25 advertised button words earns three of the four legible milestones, the same three the reference earns. It shipped because running calibration was optional.3. Milestones chained behind one event
Breakout gates 6 of 6 behind
score-opened; three of the six open at the same input.What changed
A. Achievement vs attrition, derived from measured motion.
measureProgressionswatches every numeric channel the evidence publishes, across the reference and every baseline, and calls a milestone attrition when three measured statements hold: its check reads a numeric channel; that channel never rose and fell at least once, over every snapshot of every trajectory; and the check does not hold at the initial value of that channel. Attrition propagates throughrequires.No field-name list. A sibling matched the literal string
terminaland reportedobserved: falsefor every adapter spelling it otherwise. A fixture channel deliberately namedlivesthat counts rescued divers only rises, and the measurement classifies it as the achievement it is. The mutation test flips the classification of the same milestone, on the same contract, by adding one input that repairs the resource.scoreAchievementsdrops attrition from the numerator AND the denominator.separatingandseparatescount legible achievement milestones only;attritionSeparatingrecords the rest.B.
PackagedContract.calibrateis the only way to build aPackagedContract— private constructor, private field, so an object literal is not assignable andnewis not available. It runscalibrateContractand refuses every finding.{ nonSeparating: '<why>' }is the escape hatch for a tier demonstration or a smoke fixture; it is a sentence, not a switch, and it is refused when the contract DOES separate.C.
report.collapsenames the milestone the largest share requires, whether a trivial baseline reaches it, and how many milestones first pass at the same reference input. Declared with{ gatedBehind: 'score-opened' }, refused when stale.Calibration across every packaged contract
Run on every target this machine can boot.
pnpm test:ale,pnpm test:retro, andpnpm test:gymnow carry these as gates; only the offline fixtures calibrated before.score-opened,score-tier-2,score-tier-4life-lostlife-lostsurvived-50-steps,reward-at-50-stepsreached-goal,goal-cellconstant:r)constant:right)Collapse structure:
score-openedscore-openedsurvived-25-stepsreached-goalfirst-legal-movePyBoy Tetris and RetroArch are not measured here: PyBoy is not installed on this machine and RetroArch needs a host binary.
Guards against a gate that passes everything
engine-crawler'shpExactonly ever falls, and its milestone ishpExact == 1, which HOLDS at the initial value. It is classified an achievement — the third condition of the rule doing its work.engine-crawlerstates a milestone that requires nothing, socollapsesis false at 3 of 4 gated. The collapse flag is not universal.attritionChecksnaming a non-attrition milestone,gatedBehindnaming the wrong prerequisite,nonSeparatingon a contract that separates.Existing adapters
Untouched in behaviour. No file under
adapters/is edited, no reference JSON is edited, every contract keeps its bytes, and the pinnedengine-crawler,save-levels, andscreen-puzzlecontract hashes still hold.Proof
pnpm run ciwas run from a copy of the tree outside/Users/drew, because a stale/Users/drew/node_modules/@tangle-networkon this machine shadows@tangle-network/*into a sibling repository and breakstsconmaintoo. That is a local environment fault, not a repository one.Where the code disagreed with the brief
The brief attributes failure 3 to "milestones chained behind a trivial first one" and names Breakout. Measured: no trivial baseline scores anything at all on Breakout, so
score-openedis not free there. The collapse flag therefore fires on the STRUCTURE (every milestone requires one), and the report states separately whether a baseline reaches the prerequisite — free on Airstriker, CartPole and 2048; out of reach on Breakout and FrozenLake. Both are worth refusing, and the message says which one it found.The falsifiable test asks the RAM control to rank at or above the ASCII control under an achievement-only score. It ties at 3 of 5, and the reason is a second finding rather than a partial fix: the contract's top achievement is
score >= 4, so nothing in it can tell 7 from 24. Raising that ceiling would change the packaged reference and every artifact that pinned its hash, so it is not done here.Not merged
Not merged, per the brief.