Stage 4 exit audit: three overstated verdicts, seven doc defects, one unbuildable criterion - #40
Merged
Merged
Conversation
Run under prompts/common/AUDITOR.md's stance against a green make ci (605 tests, 99%, 53 scenarios) and a green real CI run on main. Seven verdicts held; three did not, and the stage now closes at nine of ten rather than ten of ten. Criterion 4: the Advection conservation scenario passed for any flux array whatsoever -- its fixture zeroes every boundary face's normal velocity, and interior faces cancel structurally. Confirmed by mutation, not inferred. Adds the criterion's own first-named case (a fully periodic domain), mutation-verified in both directions; keeps and annotates the weak one rather than deleting it. Criterion 6: the shared-step-vocabulary half was never attempted, and the criterion had pre-declared its own failure signature. Left open deliberately -- it conflicts with a documented tests/unit/ convention, and choosing between them is a design decision, not an audit's call. Criterion 10: the sweep covered the files Stage 4 touched, not the files it invalidated. Seven documentation defects, the last three found by the new rule the first four produced. Why, in full: docs/planning/roadmap.md's Stage 4 status table and docs/CHANGELOG-DESIGN.md, 28-08-2026. Carried forward: docs/planning/backlog.md §13. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Stage 4 Completion Criterion 6 asked for a shared step vocabulary in tests/golden/conftest.py, reused rather than re-derived. A conftest.py applies only to its own subtree, and all nine of Stage 4's binding modules live in tests/unit/ -- so the criterion was unbuildable as written. Verified by running it, not by reading pytest's docs: a tests/unit/ scenario using a step defined in tests/golden/conftest.py fails with StepDefinitionNotFoundError. So this was never criterion-versus-convention. The criterion named a venue that could not serve its consumers; tests/unit/CLAUDE.md's "each binding test supplies its own local steps" grew into the vacuum and hardened into a principle while the duplication accumulated. Fix the venue, share the building blocks, not the step definitions. tests/unit/_numerics.py is the tests/golden/_demo.py counterpart; all nine modules import from it. Condition doubles 9 to 0, local _face_normal_velocity 4 to 0, _west_face 4 to 0, modules carrying the mesh constants 8 to 0. Each module keeps its own _Context and step bodies, so the coupling the convention was right about is not introduced -- a shared tests/unit/conftest.py was considered and rejected for exactly that reason. Both documents amended. Stage 4 now closes at ten of ten, on a different ten than it started with. Why, in full: docs/CHANGELOG-DESIGN.md, 28-08-2026, and the Stage 4 Criterion 6 row in docs/planning/roadmap.md. 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.
Closes Stage 4 with a per-criterion exit audit, run under
prompts/common/AUDITOR.md's stance in a session that did not write the work under review.The audit ran against a green
make ci(605 tests, 99% coverage, 53 scenarios across 14 feature files) and a green real CI run onmain(33163793986). Seven of the ten verdicts held. Three did not. That makes four stage audits in a row — Stages 1, 2, 3 and 4 — to find real defects behind a green build.Stage 4 still closes at ten of ten, but not the ten it started with.
Criterion 4 — the Advection conservation scenario had no teeth
Its fixture gives every boundary cell zero velocity, so every boundary flux is
velocity_normal * phi = 0whatever face value the scheme picks, while interior faces cancel structurally insideaccumulate_flux_to_cells. It passes for any flux array. Verified, not inferred: forcing every advective face flux to0.0left it green.That is exactly the qualifier the criterion had reserved — "a bounded scheme can still fail to conserve if its flux accounting is wrong" — and the criterion's own first-named fixture, "a periodic or fully-closed domain", is the one that carries it.
Fixed by building the case nobody had: Conservation on a fully periodic domain (all four edges periodic, velocity
(1.7, -0.9), no boundary condition configured at all). Every boundary face now carries a real nonzero flux, and global cancellation is a genuine property of the wrap accounting. Mutation-verified both ways: the clamped-wrap mutation drifts the total 52.0 → 54.87 and fails it, while leaving the old scenario passing.The weak scenario is kept, with its limitation written into the feature file rather than deleted — it still checks that a zero-velocity boundary face contributes nothing additively.
Worth noting for reviewers: Diffusion's conservation scenario, written the same day in the same shape, does have teeth (mutating its boundary-gradient branch drifts the total by 37.4). The difference is not care — diffusion's boundary flux has no velocity factor in front of it to zero the whole term out.
Criterion 6 — the criterion was unbuildable as written
It asked for a shared step vocabulary in
tests/golden/conftest.py, reused rather than re-derived. Aconftest.pyapplies only to its own directory subtree, and all nine of Stage 4's binding modules live intests/unit/. Proven rather than argued: atests/unit/scenario using a step defined intests/golden/conftest.pyfails withStepDefinitionNotFoundError.So this was never criterion-versus-convention. The criterion named a venue that could not serve its consumers;
tests/unit/CLAUDE.md's "each binding test supplies its own local steps" grew into that vacuum and hardened into a principle nobody re-examined.The convention's own argument was also weaker than it read. It warned that sharing couples nine tasks' fixtures — but they were already coupled in substance and merely uncoupled in maintenance:
origin=(0.5, -1.0), spacing=(0.2, 0.3)byte-identical across eight modules,_face_normal_velocityacross four,_west_faceacross four.Resolution: fix the venue, share the building blocks, not the step definitions.
tests/unit/_numerics.pyis thetests/golden/_demo.pycounterpart — an existing in-repo pattern, not a new one — and all nine modules import from it._face_normal_velocity_west_faceStep definitions went 109 → 110 (the audit's own new scenario), and that is the point: the target was re-derivation, not step count.
A shared
tests/unit/conftest.pyof step definitions was considered and rejected in the same decision — a shared step must populate a shared context, forcing one_Contexttype across nine modules, which is the coupling the convention was right about. Each module keeps its own_Contextand its own step bodies.Criterion 10 — the sweep covered the wrong files
It checked the files Stage 4 touched, not the files Stage 4 invalidated. Seven defects, none in a file any Stage 4 task opened:
engine.md's Flux entry named no module foraccumulate_flux_to_cells— which this criterion's own text explicitly required.engine.mdentries carried a decaying count of surviving_Null*classes ("the five that still do" … "the two"), of which there have been zero since TASK-029 — contradicting Criterion 7 two rows above in the same table.rendering.mdcalled simulation/frame scheduling "Stage 4+ work" three commits after Stage 4 shipped it.bootstrap.py's module docstring said "No simulation functionality" twenty lines above its ownimport ... simulation_step.overview.md, one inadr/ADR-002) found by the new rule the first four produced, run before the verdict was written.The
bootstrap.pyone is the sharpest: the identical stale self-description was found in__main__.py's CLI help text and fixed the same morning (73ff113). That fix's blast-radius sweep stopped at the file it had been pointed at.The rule this produced
docs/practices.mdgains "A stage's documentation sweep is a grep, not a diff review". The rule above it — "Let a checked artifact carry status, not a tense", written by Stage 3's own audit — convertedengine.md'sImplementation:lines to checked paths and stopped there; Stage 4 then reproduced the class in three files it did not cover. A count is not a tense, and a negation in asrc/docstring is not a documentation field.So the new rule is three greps run before a stage closes: the stage's own number, every count of anything the stage changed, and the negations (
does not exist,no … yet,nothing that,still). It found three more defects the moment it was written, which is a better argument for it than the four that prompted it.Also recorded, not fixed
docs/CHANGELOG-DESIGN.mdhad no entry after 2026-08-22 — Stages 3 and 4 entirely, against Session Workflow step 4. This PR appends the audit's own entry and records the gap. It deliberately does not backfill six days of decision history from its outcomes; that would mean narrating sessions nobody present witnessed. Carried as an open item indocs/planning/backlog.md§13 with the decision it needs stated.Verification
make cigreen: 606 tests, 99% coverage, 54 scenarios across 14 feature files, all twelve gates.make check-claims: only its two known false positives.Reviewer notes
Two things kept deliberately rather than collapsed, in case they look like oversights:
face_normal_velocity_toward's explicitneighbourparameter. A periodic face has no mesh-reported neighbour, and hiding that behind a default would hide the one difference the scenario exists to check.🤖 Generated with Claude Code