story-016: Assert the coordinator's output shapes directly instead of against a historical implementation - #11
Merged
Merged
Conversation
Six tests in tests/test_story_011_validation.py compare the current coordinator against the pre-story-011 one loaded from git history. They were right for story-011, whose constraint was that adding execution-history.json changed nothing else. Merged, they assert the coordinator's output may never differ from one implementation's on one day - which story-012 and story-014 both legitimately break by adding an artifact and an event. story-014 escalated on exactly these nine assertions with no bounded retry available. story-016 replaces equality-with-the-past with direct statements of the shapes wanted now: state.json's field set and types, the escalation summary's five parts, the events.log line format, and the run directory's required contents as a subset rather than an exact set. A shape stated outright can be deliberately changed by a later story that means to change it. The one guarantee genuinely lost - that state.json and the escalation summary have not drifted - is restated rather than dropped, and every new assertion must be shown to fail when violated. Must land before story-014 is re-run. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… against a historical implementation Implemented by the l5 harness story workflow.
The mutation probes assert that violating a shape turns the contract test that names it red. failing_tests() read each FAILED line up to end of line, but pytest appends " - <message>" to that line only when the terminal is wide enough to hold it. Locally the node id alone already exceeds the width, so the message is dropped and the parse yields a bare name; GitHub's runner is wide enough to keep it, so the parse yielded "name - AssertionError: ..." and never matched an expected name. 12 passed locally and all three CI jobs failed. Reproduced locally with COLUMNS=240, which fails identically, and both widths pass with the node id taken up to the first space. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Left to the environment, pytest formats its failure summary to the developer's terminal locally and the runner's in CI, so the same failure prints differently in the two places and a local run cannot reproduce a CI one. COLUMNS is fixed at a wide value for the probe subprocess, so what prints locally is what prints in CI. The parser is already width-independent; this makes the output it reads reproducible rather than merely tolerated. 479 passed under both the local default width and COLUMNS=240. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
Summary
Six tests in
tests/test_story_011_validation.pyloaded the pre-story-011 coordinator out of git history, ran it beside the current one, and asserted the two produced identical output — sameevents.log, samestate.json, same escalation summary, same run-directory contents.That was the right instrument for story-011, whose entire constraint was that adding
execution-history.jsonchanged nothing else. Once merged it said something different: that the coordinator's output may never differ from what one implementation produced on one day, measured against whatever workflow ships today. Nobody holds that requirement. story-012 addsretry-history.json; story-014 addsclean-clone-result.json. Each legitimately adds an artifact and an event, and story-014 escalated on nine failures — every one of them this comparison, in a file inside that story'sdo_not_modify, so no bounded retry could reach it.The instrument also had a shelf life independent of that. The historical coordinator is run against today's workflow, schemas and config, a pairing that grows more artificial each story until the old code cannot run at all — not because anything broke, but because it is old.
This story replaces equality-with-the-past with direct statements of the shapes the harness wants now. A shape stated outright can be read, argued with, and deliberately changed by a later story that means to change it. Equality with a frozen implementation can only be broken.
Changes
tests/test_coordinator_contract.py(new) — the standing home for the coordinator's output contract, deliberately not named for a story and not one story's evidence:state.json's exact field set, read fromdataclasses.fields(RunState)rather than typed out, so the assertion cannot drift from the definition it describes; each field's type; and the statuses a run can end in, exercised by runs that actually reach them.events.logline pattern — timestamp shape, brackets, single separating space.outputs, permitting others. This is the assertion that removes the friction: a later story adding an artifact no longer fails a test about something else.tests/test_story_011_validation.py— the six comparisons removed, along withboth_implementations,legacy_coordinator,clone_target,normalize_timestampsandSHAPES, which served only them. Net 32 insertions / 138 deletions. Everything that never depended on the historical coordinator survives untouched: the log-line-to-history correspondence, the ordering and retry-stream checks, the schema conformance of a run's history, and the prompt-scope assertion with its baseline resolution intact. No module undertests/loads a coordinator implementation out of git history any longer.tests/test_story_016_validation.py(new, tester stage) — independent validation: that each new contract assertion can be made to fail, that the removals took only unreachable code, and that a run gaining an artifact and an event no longer fails anything..harness/docs/ARCHITECTURE.md— records where the output contract now lives, the rule that a differential test against a frozen implementation should be retired once the constraint it was built for has landed, the corollary that the restatement must land before the removal with each new assertion demonstrated red, and the standing rule not to assert a run directory as an exact set (with the schemas inventory named as the deliberate exception, since there the point is that a new shape cannot appear unnoticed).Testing
479 passedon this branch.The verifier did not accept the new assertions on description. It re-ran a mutation probe against the contract file and recorded each result: log format without brackets caught, separator drift caught, timestamp drift caught,
state.jsongaining a fieldRunStatedoes not declare caught,retry_countwritten as a string caught, escalation summary losingStage:caught,completion-report.mdmissing caught by the required-subset check, unmutated control clean.It also confirmed the surviving prompt-scope assertion still bites rather than passing unconditionally: the real range over
prompts/,workflows/,rules/is clean, while a widened range containing prompt edits returns 548 diff lines.Notes for review
tests/test_story_016_validation.pyresolved its "before" copy of the story-011 file asgit show HEAD:.... In the working tree that diff was real; in a clean clone with the story committed, the baseline was the story's own file, so the removal assertion compared the file against itself — one test failed and two neighbouring diff assertions went silently green, the more dangerous outcome. This is the third appearance of theHEAD-baseline trap, and the first time it was caught automatically. Attempt 2 resolves the baseline by walkinggit log -- tests/test_story_011_validation.pynewest-first for the first blob still carrying all six removed tests, raising loudly when none exists, with three tests driving that resolution against synthetic histories (post-story newest, further commits stacked on top, and the refusal case).ARCHITECTURE.mdrecords that the trap applies to any test resolving a baseline out of git, not only to differential tests of orchestration code._complete'sgit add -Aswept all of it into the story's commit. That is exactly the defect thecommit-only-what-the-run-producedrequest describes. This branch was reconstructed by cherry-picking ontomainwith story-014's files reverted, so it contains only story-016's five files. Consequence for reviewers: the verifier's evidence cites530 passedbecause story-014's 826-line test file was present; this branch gives479 passed. story-016's own tests never depended on story-014's code — the only contact is a syntheticclean-clone-result.jsonwritten into a fixture to prove the required-subset assertion tolerates an artifact it does not name..harness/docs/ARCHITECTURE.mdwas still unwritten at verification time (documenter stage, not an acceptance criterion), and the implementation summary names the new guard testtest_the_baseline_is_not_this_storys_own_filewhile the file definestest_the_baseline_walks_past_this_storys_own_commit— the test exists and does what the summary describes; only the prose name is wrong.🤖 Generated with Claude Code