From 8224b3655e8c857b483f331bb77f265150274841 Mon Sep 17 00:00:00 2001 From: Adam Clemens Date: Sat, 12 Sep 2026 18:01:49 +0100 Subject: [PATCH] Insert Stage 9 (Solver & Run Integrity), renumber Stages 9-15 to 10-16 An end-to-end audit on 2026-09-12 found four defects behind a green `make ci` (1209 tests, 99% coverage, all fifteen structural checks passing). Two of them falsify use cases Stage 4 wrote down for itself: - Solid walls are permeable to advection. `FirstOrderUpwindAdvection` takes a boundary face's transporting velocity from the owner cell rather than the prescribed value, so a sealed box loses 14.27% of a purely advected tracer in 400 steps with diffusion zeroed. - A diverged run reports success. `DivergenceDidNotConvergeError` raised inside the frame callback is swallowed by rendercanvas's own `log_exception`, so `pyflow run` prints "pyflow exited cleanly" and exits 0 after 22 of 40 frames failed. On glfw the same raise kills the reschedule, so a --max-frames run hangs instead. And two more: nothing checks a configured timestep against the stability limit (refining the shipped cavity to 64x64 diverges at step 17, silently), and `BoundaryFaceConfig.velocity` is a validated, documented configuration field no engine code reads -- two docstrings claim it drives the divergence operator, and it reaches no scheme at all. This commit is the stage, not the fixes. Placed before Better Numerics by dependency rather than preference: Stage 10's own Criterion 7 locates Rayleigh-Benard onset between heated walls, which is not meaningful while those walls leak. Fifth divergence of the same shape as the first, third and fourth; seven completion criteria written before the first task, per the standing rule. The renumbering sweep covers 42 occurrences in the roadmap and 15 files outside it, each checked individually rather than substituted blindly. `docs/CHANGELOG-DESIGN.md` is append-only and is deliberately untouched, as are the Was/Is now tables and the prose around each describing what that divergence found. What is renumbered is every live forward pointer. Two things found stale on the way past, both fixed here rather than flagged: the first divergence's "Stage 10 below therefore carries an explicit architectural caution" had been stale through three renumberings, because it omitted the Stage's name and so had nothing for a grep to check against; and `docs/practices.md`'s own "Stage 13 (Three Dimensions)" illustration, which that rule requires to track the live number. One new standing rule, `docs/practices.md`: when a test is found weak, ask why its fixture had to be that shape. The Stage 4 exit audit found the closed-domain advection-conservation scenario vacuous by mutation and correctly replaced it -- but nobody asked why the fixture needed every boundary cell's velocity to be exactly zero. It needed it because of the defect above. The weak test was a symptom sitting next to its own cause. Co-Authored-By: Claude Opus 5 --- README.md | 33 +- adr/ADR-004-compute-rendering-class.md | 5 +- adr/ADR-005-compute-rendering-instances.md | 3 +- .../compute-and-rendering-stack.md | 7 +- docs/architecture/icds.md | 3 +- docs/planning/CLAUDE.md | 2 +- docs/planning/backlog.md | 126 +++++++- docs/planning/implementation-plan.md | 26 +- docs/planning/roadmap.md | 288 ++++++++++++++++-- docs/planning/stage-shape.yaml | 6 +- docs/planning/status.md | 22 +- docs/practices.md | 49 ++- planning/data/capabilities.yaml | 2 +- planning/data/stages.yaml | 36 ++- src/pyflow/engine/CLAUDE.md | 4 +- tools/generators/generate_status_report.py | 2 +- 16 files changed, 514 insertions(+), 100 deletions(-) diff --git a/README.md b/README.md index 681ecab..d6b1be4 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,9 @@ **Current Version:** 0.3.0 — cut 2026-09-03 when Stage 7 (Rendering Annotations) closed (`docs/planning/releases.md`). -PyFlow has completed **Stage 7 (Rendering Annotations)** and has not yet -begun Stage 9 (Better Numerics) -- Stage 8 (Recording & Playback), added -2026-09-07, comes first. Stage 0 built +PyFlow has completed **Stage 8 (Recording & Playback)** and has not yet +begun Stage 10 (Better Numerics) -- Stage 9 (Solver & Run Integrity), +added 2026-09-12, comes first. Stage 0 built the engineering foundations; Stage 1 added the first real engine code -- a `CoordinateSystem`, a `Mesh` with a structured Cartesian implementation, @@ -131,7 +131,24 @@ need to find it. ## Current Phase -Stage 9 — Better Numerics -- not yet started. Stage 8 (Recording & +Stage 9 — Solver & Run Integrity -- opened 2026-09-12, not yet +complete. It exists because an end-to-end audit that day found four +defects behind a green `make ci`, two of which falsify use cases **Stage +4 wrote down for itself**: solid walls turned out to be permeable to +advection (a sealed box loses 14.27% of a purely advected tracer in 400 +steps), and a diverged run reported `pyflow exited cleanly` with exit 0 +because the render loop swallowed the engine's own +`DivergenceDidNotConvergeError`. Two more: nothing checks a configured +timestep against the stability limit, so refining a shipped demo's mesh +blows up silently at step 17; and `BoundaryFaceConfig.velocity` is a +validated, documented configuration field that no engine code reads. +See `docs/planning/roadmap.md`'s own Stage 9 section for the criteria +and the measurements. It is placed before Better Numerics by dependency, +not preference -- Stage 10's own Rayleigh-Bénard criterion measures +convection between heated walls, which is not meaningful while those +walls leak. + +Stage 8 (Recording & Playback) was **reopened and reclosed on the same day, 2026-09-09**: an audit, prompted by the maintainer's own suspicion that it "never actually went through a design/planning session," found the suspicion @@ -153,8 +170,8 @@ its exit on 2026-09-11 -- each edit to this paragraph has so far landed in the same change as the roadmap event it describes, unlike the multi-day staleness windows the two paragraphs below describe for Stages 7 and 8's own *earlier* drafts. Don't read this as the pattern -solved; read Stage 9's own eventual entry here as the next real test of -it. +solved; read Stage 10 (Better Numerics)'s own eventual entry here as +the next real test of it. **And read the exit audit before trusting that record too far.** This paragraph tracks whether *this section* stays current, which it has. @@ -345,8 +362,8 @@ together). A config with no solved velocity at all (Heat Diffusion's own shape) still has nothing for this to render, and is rejected the same way it always was. -Stage 9 (Better Numerics) follows Stage 8 (Recording & Playback, added -2026-09-07) -- better advection and diffusion +Stage 10 (Better Numerics) follows Stage 9 (Solver & Run Integrity, +added 2026-09-12) -- better advection and diffusion schemes, and with them the quantitative Rayleigh-Bénard comparison Stage 6 deliberately deferred rather than met on a first-order-upwind solver. **Its eight completion criteria were written on 2026-09-04, before the diff --git a/adr/ADR-004-compute-rendering-class.md b/adr/ADR-004-compute-rendering-class.md index 401bf76..7f5e457 100644 --- a/adr/ADR-004-compute-rendering-class.md +++ b/adr/ADR-004-compute-rendering-class.md @@ -102,7 +102,8 @@ Alternatives Considered below. wgpu, CUDA-GL interop) remains unproven for every candidate in this class and is accepted as a known, deferred risk, not resolved by this decision. If profiling ever shows the round-trip has become a real - bottleneck (most likely at Stage 13's 3D scale, where cost scales as + bottleneck (most likely at Stage 14 (Three Dimensions)'s 3D scale, + where cost scales as N³ rather than N²), that interop engineering will need to be done then. - **No production-scale validation as strong as Warp's Newton/Isaac Lab @@ -173,7 +174,7 @@ interface so a future swap (e.g. if the Array API standard matures further, or if a specific instance's GPU story regresses) is an implementation change, not an architectural one. -Re-profile the host round-trip's cost before Stage 13 (Three +Re-profile the host round-trip's cost before Stage 14 (Three Dimensions) rather than assuming §4.1's 2D-scale estimate still holds -- that is the point at which this decision's accepted risk was identified as most likely to diff --git a/adr/ADR-005-compute-rendering-instances.md b/adr/ADR-005-compute-rendering-instances.md index 49dc501..1b53467 100644 --- a/adr/ADR-005-compute-rendering-instances.md +++ b/adr/ADR-005-compute-rendering-instances.md @@ -153,5 +153,6 @@ budget for this as real implementation work, not assume it is free. Re-evaluate the DLPack-based zero-copy path between PyTorch and wgpu/pygfx if profiling ever shows the host round-trip has become a real -bottleneck -- per `ADR-004`, most likely at Stage 13's 3D scale, not +bottleneck -- per `ADR-004`, most likely at Stage 14 (Three +Dimensions)'s 3D scale, not before. diff --git a/docs/architecture/compute-and-rendering-stack.md b/docs/architecture/compute-and-rendering-stack.md index e47100f..f0e09c1 100644 --- a/docs/architecture/compute-and-rendering-stack.md +++ b/docs/architecture/compute-and-rendering-stack.md @@ -148,7 +148,7 @@ uniform grid) at a generous 2048×2048 cells with four `float32` fields At a more MVP-realistic 512×512 with the same four fields (~4.2 MB), the cost is **sub-millisecond either way** -- not a real constraint at all. -**Where this changes:** 3D (Stage 13, Three Dimensions) scales as N³, +**Where this changes:** 3D (Stage 14, Three Dimensions) scales as N³, not N². A 512³ grid with the same four fields is ~4.3 GB/frame -- the same fallback would be disqualifying at that scale, not merely slow. That is explicitly future @@ -173,7 +173,7 @@ project's near/medium-term scope -- revised there. | **Python 3.14 support** | Confidence varies per library at this snapshot and needs a direct check before A2c, not assumed from this document -- new-release lag is exactly the pattern the version-review policy in `docs/practices.md` exists to catch. | | **Licence vs. BSD-3-Clause** | All candidates above are permissively licensed at this snapshot to the best of this survey's knowledge (BSD/MIT/Apache-2.0-family) -- **confidence: medium**, worth a direct check per final candidate rather than trusted wholesale. | | **Headless / CI capable** | Hard requirement, from `docs/implementation/golden-demos.md` via D5. NumPy/CuPy/PyTorch/JAX's *compute* side is unaffected (no display needed). On the *rendering* side, now the live question since Class 2 was decided: **wgpu/pygfx has the strongest confirmed story of any general-purpose renderer surveyed** (verified 2026-08-15 -- own CI runs headless via LavaPipe as standard practice, not just documented as possible). **VisPy's is real but rougher** (verified 2026-08-15 -- EGL backend exists, but a headless-rendering-without-sudo issue has sat open and unaddressed since 2023, and other EGL issues recur through its history). **VTK's and ModernGL/glfw-family's headless claims were not re-verified live this session** -- they rest on the original May-2026 snapshot ("VTK has established offscreen support," "ModernGL/glfw can run headless via EGL/OSMesa with known extra setup") and should be checked with the same rigour before A2c finalises if either becomes a live candidate. (Taichi GGUI's headless support was confirmed live 2026-08-15 during the Class 3 evaluation, now moot since Class 2 was chosen -- kept here for the record.) | -| **2D now, 3D at Stage 13 (Three Dimensions) without a rewrite** | VTK/PyVista strongest here (3D-native). VisPy, wgpu/pygfx and Taichi GGUI all support 3D. Thin layers (ModernGL) support it but every capability (camera, projection) is ours to build. | +| **2D now, 3D at Stage 14 (Three Dimensions) without a rewrite** | VTK/PyVista strongest here (3D-native). VisPy, wgpu/pygfx and Taichi GGUI all support 3D. Thin layers (ModernGL) support it but every capability (camera, projection) is ours to build. | | **Capability Level 9 (GPU execution)** | Only the GPU-capable axis-1 candidates are relevant at all; among those, Taichi's story is the most coherent *because* compute and render already share a device and runtime. The others would need the interop work in §4 regardless of whether Level 9 is pursued, if a GPU array library is chosen now for compute alone. | | **Multiple renderers (maintainer's stated ambition)** | The NumPy row is renderer-agnostic almost by definition -- any renderer can consume it, which is what keeps a second renderer cheap. Every 🟡/❔ cell above represents *renderer-specific* plumbing that would need re-doing per additional renderer if a GPU array library couples tightly to one. Taichi GGUI is the extreme case: choosing it as the primary renderer effectively forecloses easily adding a second, different renderer for the *same* field data, because the coupling **is** the point of that class. | | **Proven for this domain specifically** | Checked live, 2026-08-15, for the Class 2 array libraries. **JAX-Fluids** (differentiable compressible/two-phase CFD, runs CPU/GPU/TPU) and **PhiFlow** (multi-backend differentiable PDE/fluid framework -- "the exact same code runs a 2D NumPy sim or a 3D GPU PyTorch/JAX sim") both exist and validate the *compute-side* pattern -- swappable NumPy-shaped backends genuinely work for fluid simulation, not just in theory. **Caveats, checked rather than assumed:** both are themselves stale as dependencies would be judged in this survey -- JAX-Fluids' latest release is 2025-03-21 (~17 months old), PhiFlow's is 2025-08-02 (~1 year old) -- though this matters less here than Taichi's staleness does, since PyFlow would not depend on either package, only take them as evidence the architecture works. **More importantly: neither resolves the rendering-coupling question above.** PhiFlow's own answer to visualization is a web-based interactive UI (`view()`), not a native desktop render loop -- architecturally different from what `roadmap.md` TASK-007 specifies (window, render loop, clean shutdown). These projects prove the compute pattern; they sidestep the native-rendering question rather than answering it. **Also checked and explicitly ruled out as evidence: JAX-CFD**, Google's own earlier CFD-in-JAX project, carries an explicit "no longer maintained" notice from Google in its own commit history (2026-02-24) -- it very nearly went into this survey as a positive data point before that was found; JAX-Fluids and PhiFlow are the sources actually being cited here. | @@ -205,7 +205,8 @@ round-trip (or spiking the DLPack/CUDA-GL interop path from §4). point of a GPU array library," is very likely a non-issue at this project's near/medium-term scale (sub-millisecond at MVP grid sizes, single-digit milliseconds even at a generous 2048×2048) -- a real cost - only reappears at Stage 13's 3D scale, later work. This meaningfully + only reappears at Stage 14 (Three Dimensions)'s 3D scale, later work. + This meaningfully de-risks Class 2 relative to the first survey pass. - *Proven for the domain:* CuPy, PyTorch and JAX are all independently and currently well-maintained (verified live, §2), and the diff --git a/docs/architecture/icds.md b/docs/architecture/icds.md index 8cc1436..a8c41d8 100644 --- a/docs/architecture/icds.md +++ b/docs/architecture/icds.md @@ -166,7 +166,8 @@ transported, not a discretisation decision. **That reasoning is why it does not live under `numerics` at all: it moved to `FluidConfig` in TASK-041 (2026-08-28, Stage 5's design question four), and this entry still said `numerics.diffusion_coefficient` until 2026-09-04** -- found -while drafting Stage 9's completion criteria, which is a stage that +while drafting Stage 10 (Better Numerics)'s completion criteria, which +is a stage that reads this ICD for what its own "improved diffusion" work would have to change. A config setting the old name is rejected at load with a named error pointing at the new one, so the stale entry named a field no run diff --git a/docs/planning/CLAUDE.md b/docs/planning/CLAUDE.md index afe2b24..6dc69b6 100644 --- a/docs/planning/CLAUDE.md +++ b/docs/planning/CLAUDE.md @@ -29,7 +29,7 @@ The two files are gated against each other, so adding a section to the shape without explaining it in the specification fails the build. **A `sketched` stage may already carry some of them, so check before -writing rather than assuming a blank.** Stage 9 (Better Numerics) was +writing rather than assuming a blank.** Stage 10 (Better Numerics) was given its eight Completion Criteria and its **Serves** line on 2026-09-04, while still sketched -- earlier than the rule asks, at the maintainer's request. What it deliberately does *not* have is **Use diff --git a/docs/planning/backlog.md b/docs/planning/backlog.md index bd27213..f9ce3a6 100644 --- a/docs/planning/backlog.md +++ b/docs/planning/backlog.md @@ -1631,8 +1631,8 @@ Not blocking, not forgotten. Each has a stated reason and, where it exists, an unblock condition. - [x] **Decide Capability Level 7's fate.** **Resolved 2026-08-21: a - Stage was added.** `roadmap.md` Stage 12 (Additional Numerical - Frameworks) now serves the Level (renumbered twice since -- + Stage was added.** `roadmap.md` Stage 13 (Additional Numerical + Frameworks) now serves the Level (renumbered three times since -- `roadmap.md`'s own "Stages and Capability Levels" section has the full history), and the former Stages 10-12 were renumbered 11-13. No `TASK-NNN` moved -- Stages 7-13 were all still @@ -1968,6 +1968,27 @@ here.): distinct, still-open claim, found while closing this note rather than left implied-done by periodic boundaries existing at all -- not added to this item's own scope without a decision to do so. + + **Reopened 2026-09-12: the parenthetical above ("every boundary + cell's velocity exactly zero") was not a fixture choice, it was + the only fixture that could have passed.** The end-to-end audit + that opened Stage 9 (Solver & Run Integrity) found that + `FirstOrderUpwindAdvection` takes a boundary face's transporting + velocity from the owner cell rather than from the prescribed + boundary value, so a closed domain with any interior motion near + a wall advects straight through it -- 14.27% of a purely + advected tracer lost in 400 steps on + `examples/golden-demos/smoke_transport.yaml` with diffusion + zeroed. The Stage 4 exit audit correctly found this scenario + vacuous by mutation and correctly added the periodic scenario to + carry the criterion; what it did not ask was why the closed + domain needed a motionless boundary. **So this bullet's "done" + stands for what it says and not for what it appears to say**: + conservation on a closed domain was never actually demonstrated + by it. Stage 9's Completion Criterion 1 is where that claim + lands, with the same mutation discipline. The general lesson is + `docs/practices.md`'s "When a test is found weak, ask why its + fixture had to be that shape". - **Diffusion scheme** -- **done, TASK-024 (Stage 4, 2026-08-27)**: same conservation check under zero-flux (Neumann) boundaries as Advection's above -- an insulated domain's field total is @@ -2143,7 +2164,7 @@ here.): that Stage 6 checks convection onset *qualitatively*: rolls form heated from below and do not heated from above, which no sign error survives. The quantitative critical-Rayleigh-number - comparison is deferred to Stage 9 (Better Numerics) at the + comparison is deferred to Stage 10 (Better Numerics) at the earliest -- hitting ≈1708 on a first-order-upwind solver at MVP mesh resolutions is the same shape of bar Stage 5 rejected when it declined `adr/ADR-007`'s illustrative "within 2%" for Ghia. @@ -2153,7 +2174,8 @@ here.): name it say so now". Only `docs/planning/implementation-plan.md` was amended; this one was the other document, and it was not. - **Claimed 2026-09-04, when Stage 9's completion criteria were + **Claimed 2026-09-04, when Stage 10 (Better Numerics)'s + completion criteria were drafted (maintainer's call): it is that stage's Completion Criterion 7, and no longer "at the earliest".** The criterion is a measured threshold -- a sweep across Rayleigh numbers, @@ -2198,7 +2220,7 @@ here.): - **Flow Around Cylinder's von Kármán/Strouhal correlation was already flagged as unclaimed** in `implementation-plan.md` before this pass (2026-08-20) -- confirmed still accurate, - cross-referenced here rather than duplicated, since Stage 10 + cross-referenced here rather than duplicated, since Stage 11 (Geometry) has no `TASK-NNN` breakdown yet to attach a criterion to. @@ -2232,7 +2254,7 @@ here.): Ghia et al.'s tolerance activated on 2026-08-28 and is now Stage 5 Completion Criterion 5, closed above -- as a convergence requirement rather than the illustrative 2%. The cylinder - correlation activates when Stage 10 (Geometry) gets real task numbers; + correlation activates when Stage 11 (Geometry) gets real task numbers; MMS itself stays open as a general-purpose technique for whichever future task turns out to need it, TASK-024 no longer being that @@ -2320,7 +2342,7 @@ here.): maintainer named "both 2D and 3D examples" explicitly -- some emergent phenomena (vortex stretching; the 3D energy cascade differing qualitatively from 2D's inverse cascade) only exist once - 3D does. Apply the identical reasoning at Stage 13 (Three + 3D does. Apply the identical reasoning at Stage 14 (Three Dimensions) when it's reached -- observing the right 3D-specific phenomenon under the right configuration becomes that stage's own acceptance criteria, the same way it just became Level 2's. Not @@ -2408,7 +2430,7 @@ here.): **Scoped to a solved-velocity-only config for this first cut** (`UnsupportedPlaybackConfigError` otherwise) -- declared-field/ scalar-colormap playback is real, deferred future work, not built - here; open a new backlog item if a demo needs it before Stage 9 + here; open a new backlog item if a demo needs it before Stage 10 does anything else that would motivate it. **Reopened 2026-09-09, not via a new backlog item.** An audit of @@ -3344,3 +3366,91 @@ section for the full per-criterion record. *Unblock condition:* none -- investigable now. Decide first whether the honest answer is a computed gap or a screen-space HUD (§14 above), since the second makes this one moot. + +--- + +## 15. Carried forward from the end-to-end audit (2026-09-12) + +The audit that opened Stage 9 (Solver & Run Integrity) found four +defects behind a green `make ci` -- 1209 tests, 99% coverage, all +fifteen structural checks passing. **Three of the four are being fixed +in that stage, not carried here** (`docs/planning/roadmap.md`'s Stage 9 +Completion Criteria own them); the fourth, +`BoundaryFaceConfig.velocity` being a validated field no engine code +reads, is fixed as part of the first. What is below is what the audit +deliberately did **not** take on, each with a stated reason, per the +same convention §13 and §14 already use. + +**One observation that is not an item, because it is already fixed by +the stage above**: the closed-domain advection-conservation scenario +this repository has carried since 2026-08-27 was already known to be +weak -- the Stage 4 exit audit recorded that it "passes for *any* flux +array" and added a periodic scenario to carry the criterion instead. +What nobody asked was *why* the closed-domain fixture needed every +boundary cell's velocity to be exactly zero. It needed it because +advection reads the owner cell's velocity at a wall, so any other +fixture would have leaked. The weak scenario was a symptom of the defect +it was sitting next to, and the audit that found the weakness stopped +one question short. Recorded in `docs/practices.md` rather than here. + +- [ ] **A prescribed-inflow/outflow channel demo.** TASK-052 makes + `BoundaryFaceConfig.velocity` live, which means PyFlow can express + a prescribed inlet for the first time -- the rung + `docs/planning/implementation-plan.md`'s Level 2 catalogue already + names below Poiseuille flow. It was the more ambitious candidate + for Stage 9's own Golden Demo and lost to Sealed Box. + + **Why it was not taken on.** An inlet alone is not a channel: the + outlet needs a Neumann velocity face *and* an answer to where + pressure is anchored once one boundary stops prescribing velocity, + and `PISO` currently applies a zero-gradient pressure condition to + every wall unconditionally + (`src/pyflow/engine/numerics/pressure_coupling.py`'s own + `_ZeroGradientPressureCondition`). Neither has been explored here. + A demo whose own boundary treatment is undesigned is a design + session, not a demonstration -- and Stage 9's criteria are about + correcting what already ships, not about adding a boundary type. + + *Unblock condition:* none -- investigable the moment TASK-052 + lands and `velocity` reaches the schemes. Decide the outlet's + pressure treatment first; it is the part with no current answer. + +- [ ] **`numerics.timestep: auto`.** TASK-054 warns when a configured + timestep exceeds `stable_timestep()`'s own limit; it does not let + a user ask for the derived value instead. Deriving it is the + obvious next step and is deliberately not taken. + + **Why it was not taken on.** Stage 10 (Better Numerics)'s own + Design Question Two already owns whether "adaptive timestep" means + CFL-driven selection -- which is exactly this, and which that + question notes needs no interface change -- or an embedded + error-estimating integrator, which needs `TimeIntegrator.advance` + to return a proposed next step and is the one interface change + that stage's Criterion 4 permits. Building the cheap half now + would pre-empt a design question with a decision nobody has taken, + and would do it in a stage whose Goal is correcting shipped + behaviour rather than choosing numerics. + + *Unblock condition:* Stage 10's Design Question Two being + answered. + +- [ ] **`docs/CHANGELOG-DESIGN.md` has no entry after 2026-09-09.** The + Stage 8 exit audit (2026-09-11) and its six fixes are recorded in + `docs/planning/roadmap.md`'s own Stage 8 status section and in + three `CLAUDE.md` files, but not there. This is §13's own + still-open item recurring: that entry was raised on 2026-08-28 + when the log had been skipped for six days, asked whether the log + is still a live obligation, and has not been answered since. + + **Why it was not taken on.** The same reason §13 gave, and it has + not changed: reconstructing a session's decisions from their + outcomes produces a plausible narrative nobody witnessed, which + this repository's Integrity section rules out. What has changed is + that the gap has now recurred twice, which is evidence for §13's + own second option rather than its first -- either the workflow + needs something that fails when the step is skipped, or step 4 + should be retired and the roadmap declared the decision record. + + *Unblock condition:* none -- this is a decision, not an + investigation. It is §13's decision, and this entry only records + that waiting has now cost a second stage's worth of history. diff --git a/docs/planning/implementation-plan.md b/docs/planning/implementation-plan.md index 1a6f1ae..e84e5e3 100644 --- a/docs/planning/implementation-plan.md +++ b/docs/planning/implementation-plan.md @@ -98,12 +98,12 @@ Release are in `docs/glossary.md`. Level 7 had no corresponding Stage at all until 2026-08-21, which also made the "Dam Break / Free Surface" entry in the Golden Demos table below -unreachable from the roadmap. **Resolved: `roadmap.md` Stage 12 +unreachable from the roadmap. **Resolved: `roadmap.md` Stage 13 (Additional Numerical Frameworks) was added to serve it**, renumbering -the former Stages 10-12 to 11-13 (itself renumbered twice more since -- -2026-08-31 to make room for a new Stage 7, and 2026-09-07 for a new -Stage 8 -- see that document's own "Third" and "Fourth -divergence" entries). See that document's "Stages and +the former Stages 10-12 to 11-13 (itself renumbered three times more +since -- 2026-08-31 to make room for a new Stage 7, 2026-09-07 for a new +Stage 8, and 2026-09-12 for a new Stage 9 -- see that document's own +"Third", "Fourth" and "Fifth divergence" entries). See that document's "Stages and Capability Levels" section for the mapping, the architectural caution attached to the new Stage, and the evidence the decision was taken against. @@ -361,7 +361,7 @@ quoted out of context. Recorded 2026-08-27.) quantitative threshold is not Stage 6's bar.** Stage 6 checks the qualitative onset -- rolls form when the layer is heated from below and do not when it is heated from above, which no sign error survives -- and -the critical-Rayleigh-number comparison is deferred to Stage 9 (Better +the critical-Rayleigh-number comparison is deferred to Stage 10 (Better Numerics) at the earliest. The reasoning is the one Stage 5 already applied to Ghia et al.'s illustrative 2%: hitting a critical threshold on a first-order-upwind solver at MVP mesh resolutions is a criterion @@ -369,9 +369,10 @@ meetable only by loosening its own number later, which is not a criterion. The number is not discarded -- it is waiting for a scheme that could clear it. -**Claimed 2026-09-04, when Stage 9's completion criteria were drafted +**Claimed 2026-09-04, when Stage 10 (Better Numerics)'s completion +criteria were drafted (maintainer's call): it is that stage's Completion Criterion 7, and no -longer "at the earliest".** `docs/planning/roadmap.md`'s Stage 9 now +longer "at the earliest".** `docs/planning/roadmap.md`'s Stage 10 now carries it as a numbered criterion -- onset located by a sweep across Rayleigh numbers and reported as a bracketing interval rather than asserted at a single run, bounded against whichever of 1708 / 1101 / @@ -470,7 +471,8 @@ Golden Demo Cross-framework comparison (changed 2026-08-21 -- see the note below). -**Scheduled 2026-08-21: `roadmap.md` Stage 12 serves this Level.** The +**Scheduled 2026-08-21: `roadmap.md` Stage 13 (Additional Numerical +Frameworks) serves this Level.** The alternative considered was dropping the Level; the maintainer chose to keep it. Read that Stage before designing anything here -- it carries an architectural caution drawn from this project's own survey, which found @@ -657,9 +659,9 @@ as capabilities are implemented", a real deliberate absence. (noted 2026-08-20): past a Reynolds-number threshold, flow around a cylinder sheds a von Kármán vortex street with a known Reynolds-number-to-Strouhal-number correlation. `docs/planning/roadmap.md` -Stage 10 (Geometry, this demo's Stage) has no `TASK-NNN` numbers assigned -yet -- Stages 8-15 are all still at the looser "Tasks include" stage of -planning, unlike Stages 0-7 (complete). When that task exists and gets its own +Stage 11 (Geometry, this demo's Stage) has no `TASK-NNN` numbers assigned +yet -- Stages 10-16 are all still at the looser "Tasks include" stage of +planning, unlike Stages 0-8 (complete) and Stage 9 (open). When that task exists and gets its own acceptance criteria (`docs/practices.md`, "Acceptance criteria must be testable"), include checking the shed frequency against that correlation -- the demo already produces the phenomenon; nothing diff --git a/docs/planning/roadmap.md b/docs/planning/roadmap.md index 465f431..f5f48cc 100644 --- a/docs/planning/roadmap.md +++ b/docs/planning/roadmap.md @@ -66,13 +66,14 @@ can serve one Level. | 6 — Additional Physical Fields | 3 — Multiple Transported Fields | | 7 — Rendering Annotations | — (no dedicated Level; see "Rendering" below) | | 8 — Recording & Playback | — (no dedicated Level; see "Rendering" below) | -| 9 — Better Numerics | 4 — Numerical Improvements | -| 10 — Geometry | 5 — Geometry | -| 11 — Adaptive Resolution | 6 — Adaptive Resolution | -| 12 — Additional Numerical Frameworks | 7 — Additional Numerical Frameworks | -| 13 — Three Dimensions | 8 — Three-Dimensional Simulation | -| 14 — Performance | 9 — High Performance Computing | -| 15 — Advanced Physics | 10 — Advanced Physics | +| 9 — Solver & Run Integrity | — (no dedicated Level; see "Fifth divergence" below) | +| 10 — Better Numerics | 4 — Numerical Improvements | +| 11 — Geometry | 5 — Geometry | +| 12 — Adaptive Resolution | 6 — Adaptive Resolution | +| 13 — Additional Numerical Frameworks | 7 — Additional Numerical Frameworks | +| 14 — Three Dimensions | 8 — Three-Dimensional Simulation | +| 15 — Performance | 9 — High Performance Computing | +| 16 — Advanced Physics | 10 — Advanced Physics | **First divergence, resolved 2026-08-21 (maintainer's call): a Stage was added.** Capability Level 7 had no corresponding Stage, leaving the @@ -105,9 +106,21 @@ impractical"; `adr/ADR-002-fvm-first.md` had already placed SPH as scope; and SPH, FLIP, PIC and free-surface flow appear nowhere in `docs/planning/dreams.md`, `docs/implementation/mvp.md` or `docs/planning/capability-map.md`. The maintainer chose to keep the -Level and add the Stage. **Stage 10 below therefore carries an explicit -architectural caution**, so whoever reaches it meets the handbook's -finding before designing rather than after. +Level and add the Stage. **Stage 13 (Additional Numerical Frameworks) +below therefore carries an explicit architectural caution**, so whoever +reaches it meets the handbook's finding before designing rather than +after. + +**That sentence read "Stage 10 below" until 2026-09-12, stale through +three renumberings** -- the third divergence moved that Stage to 11, the +fourth to 12, and the fifth (below) to 13, and all three sweeps treated +this line as part of the first divergence's historical narrative rather +than as the live forward pointer its own "below" makes it. It is the +narrow case `docs/practices.md`'s "Name a Stage when you cite its +number" rule does not catch on its own: the name was omitted here, so +there was nothing for a reader or a grep to check the number against. +Fixed by adding the name, which is what makes the next renumbering's +sweep able to see it. **Second known divergence, found and decided 2026-08-20:** `docs/planning/capability-map.md`'s "Analysis" top-level capability @@ -219,6 +232,68 @@ own renumbering event (this section's first and third, plus `docs/CHANGELOG-DESIGN.md`/`docs/practices.md`) was left as the historical record it is, not rewritten to describe this one. +**Fifth divergence, resolved 2026-09-12 (maintainer's call): a Stage was +added, the same shape as the first, third and fourth.** An end-to-end +audit run that day found four defects behind a green `make ci` (1209 +tests, 99% coverage, every structural check passing), two of which +falsify use cases **Stage 4 wrote down for itself** -- "have the interior +schemes honour it" and "be told when it did not converge instead of +receiving a plausible wrong answer". Three placements were considered +and are recorded rather than smoothed over: reopening Stage 4 (the Stage +8 precedent, rejected because it would drag a rendering-loop fix into a +stage that predates the render loop by three stages); folding the work +into Better Numerics (rejected because that stage's Goal is *improve +accuracy without changing architecture*, so criteria drafted against it +would not test what this work claims -- precisely the failure +`docs/planning/stage-specification.md` exists to prevent, and the same +reasoning the fourth divergence used against opening Performance for +recording); and inserting a Stage, which is what was chosen. + +**Placed before Better Numerics by dependency, not by convenience.** +Stage 10 (Better Numerics)'s own Completion Criterion 7 locates +Rayleigh-Bénard convection's onset between a heated lower wall and a +cooled upper one, bounded against a published critical Rayleigh number. +That measurement is not meaningful while those walls leak, which is +exactly what this stage's first defect is -- so the ordering is forced +by what Stage 10 has already committed to measuring. **Stages 9-15 were +renumbered to 10-16** to make room: + +| Was | Is now | +|-----|--------| +| Stage 9 — Better Numerics | Stage 10 — Better Numerics | +| Stage 10 — Geometry | Stage 11 — Geometry | +| Stage 11 — Adaptive Resolution | Stage 12 — Adaptive Resolution | +| Stage 12 — Additional Numerical Frameworks | Stage 13 — Additional Numerical Frameworks | +| Stage 13 — Three Dimensions | Stage 14 — Three Dimensions | +| Stage 14 — Performance | Stage 15 — Performance | +| Stage 15 — Advanced Physics | Stage 16 — Advanced Physics | + +No `TASK-NNN` identifiers moved: every renumbered stage is still at the +looser "Tasks include" level of planning -- checked directly with `grep +-c '^## TASK-' ` scoped to each renumbered section rather than assumed +from the first, third and fourth divergences all having matched that +pattern, and checked for old Stage 9 specifically, which alone among +them already carried pre-drafted Serves/Completion Criteria content +(written 2026-09-04, ahead of its own opening). Zero `## TASK-NNN` +entries exist under any of the seven. Checked before renumbering, not +assumed cheap: `grep -rlE "Stage (9|10|11|12|13|14|15)\b"` outside this +file found 17 files, the same "most already written as `Stage N (Name)`" +shape that made the three earlier sweeps tractable; each occurrence was +checked individually rather than replaced by a blind substitution. + +**Two categories were deliberately left alone, and the distinction is +the whole difficulty of a renumbering sweep.** `docs/CHANGELOG-DESIGN.md` +is append-only and is not renumbered at all -- it still says "3D at +Stage 10" from 2026-08-18, which is the historical record it is meant to +be, and `docs/practices.md`'s own "Name a Stage when you cite its +number" rule quotes the *first* divergence's sweep as its worked +example, not this one. The `Was`/`Is now` tables above, and the prose +around each describing what that divergence found, are likewise the +record of what was true on their own dates. **What is renumbered is +every live forward pointer** -- a sentence telling a reader which stage +to go and read. The one this sweep found already stale for three +renumberings is recorded under the first divergence above. + For the definitions of Stage, Capability Level and Release, see `docs/glossary.md`. @@ -2547,11 +2622,11 @@ Python floats/tuples `CoordinateSystem`/`Mesh` use for geometry -- PyTorch is the array library `ADR-005` already committed the project to. `float64`, not PyTorch's own `float32` default, to match the double precision those two layers already carry throughout; revisited only if -Stage 14 (Performance) profiling gives a real reason to trade it for GPU +Stage 15 (Performance) profiling gives a real reason to trade it for GPU throughput, not before, per this project's "don't build ahead of a real consumer" (TASK-011) applied to a trade-off rather than a capability. Device placement (CPU vs. GPU) is out of scope for the same reason -- -storage is always a CPU tensor until Stage 14. +storage is always a CPU tensor until Stage 15 (Performance). **A collocated field's storage shape is tied to its mesh by construction, not merely validated against it** -- Stage 2 Completion @@ -4827,7 +4902,7 @@ gradient). An orchestrator that "corrects" a scheme's boundary output would have to know each scheme's own interpolation logic to do it right, which leaks scheme-specific knowledge into the one place `adr/ADR-003` exists to keep generic -- and breaks the moment a second advection -scheme (Stage 9: TVD, QUICK, WENO) has a different boundary formula from +scheme (Stage 10: TVD, QUICK, WENO) has a different boundary formula from upwind's. **Decided: each concrete scheme receives its own boundary conditions at @@ -7012,8 +7087,9 @@ actual bar (Criterion 5). the viscosity. Measure before committing a scenario to either. **If neither survives at a resolution this stage can afford, that is a real finding about the MVP's numerics -- report it and - rescope with the maintainer** (Stage 9 is where a less diffusive - scheme lands, `docs/implementation/upgrade-paths.md`). It is not + rescope with the maintainer** (Stage 10 (Better Numerics) is + where a less diffusive scheme lands, + `docs/implementation/upgrade-paths.md`). It is not licence to quietly drop the negative control, which is the half of this bullet that does the work. - **Conservation, a claim none of the three above makes.** On a @@ -7533,7 +7609,8 @@ the error budget:** first-order upwind's numerical diffusion is the dominant error term at every mesh this stage will run (Criterion 5's own cavity bullet turns on exactly that), so paying four pressure solves per timestep instead of one buys a reduction in splitting error that nothing -in this stage could measure. Revisit when Stage 9's less diffusive +in this stage could measure. Revisit when Stage 10 (Better Numerics)'s +less diffusive schemes make the splitting error visible, not before. Recorded in the scenario as well as here, per this question's own original instruction. @@ -8885,7 +8962,7 @@ for TASK-035 to trip over: both now have Golden Demos table rows and Level 3's own Golden Demo list names all three. Rayleigh-Bénard stays where it is, as this stage's validation case rather than a fourth golden demo, and Criterion 6 records that its critical-Rayleigh-number -comparison is deferred to Stage 9 (Better Numerics) at the earliest. +comparison is deferred to Stage 10 (Better Numerics) at the earliest. ### Completion Criteria @@ -9129,7 +9206,7 @@ surprise, but not a free one. Design question two. The bar is the qualitative one -- rolls form when the layer is heated from below and do not when it is heated from above, which no sign error survives. The quantitative comparison is deferred rather - than dropped, to Stage 9 (Better Numerics) at the earliest, and + than dropped, to Stage 10 (Better Numerics) at the earliest, and `docs/planning/backlog.md`'s own Rayleigh-Bénard item is amended to say so rather than left reading as though this stage owed it. - **Density: what conservation means here.** The recorded intent asks @@ -9387,7 +9464,7 @@ number later is not a criterion. **The number is not discarded, it is reassigned**, and `docs/planning/backlog.md`'s own Rayleigh-Bénard item is amended in the same change rather than left reading as though this stage owed it: the critical-Rayleigh-number comparison becomes due when -a scheme exists that could clear it, which is Stage 9 (Better Numerics) +a scheme exists that could clear it, which is Stage 10 (Better Numerics) at the earliest. Criterion 6's buoyancy bullet is written to the qualitative bar and says which half was deferred and why. @@ -9600,7 +9677,8 @@ Documents: the ten listed under Criterion 11, plus `docs/planning/backlog.md`'s two amendments (Criterion 6's promised Rayleigh-Bénard deferral, and a note that the buoyancy half of the "physical sanity checks" item landed while the rest stays open), and a -note on Stage 9's own section recording that it inherits the +note on Stage 10 (Better Numerics)'s own section recording that it +inherits the critical-Rayleigh-number comparison -- previously carried only in the two documents that deferred it, neither of which a reader opening Stage 8 would reach. @@ -10667,7 +10745,7 @@ stage names one. - **Left at their defaults, the numbers are unchanged from before this stage**, so an unconfigured run is not silently rescaled. 5. **The annotation layer is one mechanism, not one per annotation.** - The architectural claim, and the one that decides whether Stage 9+ + The architectural claim, and the one that decides whether Stage 10+ can add an annotation cheaply. - **One module**, `rendering/hud.py`, holding plain-values-in, `pygfx.Text`-out and nothing else -- no camera, no render loop, no @@ -11253,7 +11331,7 @@ Raised by the maintainer 2026-09-04 (`docs/planning/backlog.md`), not scheduled until the maintainer's decision on 2026-09-07 to open it -- see this file's own "Stages and Capability Levels" section, Fourth divergence, for why it is a Stage of its own rather than folded into -Stage 14 (Performance) as the backlog's own first guess had it. +Stage 15 (Performance) as the backlog's own first guess had it. ### Completion Criteria @@ -12641,7 +12719,150 @@ Completion Criterion 7 in full. --- -# Stage 9 — Better Numerics +# Stage 9 — Solver & Run Integrity + +Goal + +Make the engine's own answers trustworthy: what a configuration +prescribes at a boundary is what every scheme uses, and a run that fails +says so. + +Serves + +No dedicated Capability Level -- the same "tasks added to whichever +Stage needs them" pattern the Stage/Capability Level table at the top of +this document already records for Rendering (Stage 7), for Recording & +Playback (Stage 8), and for Measurements/Diagnostics/Export. This stage +corrects and reports on capability already claimed rather than +unlocking a new physical or numerical one, which is exactly why it has +no Level: `docs/planning/stage-specification.md` drafts a stage's +criteria against its Level's Unlocks, and there is no Unlock here to +draft against. The criteria below are drafted against the Goal instead. + +Use cases + +- Seal a domain and have what is inside it stay inside it. +- Prescribe a non-zero velocity at a boundary and have the solver + transport through it, rather than validate it and discard it. +- Have a run that blows up exit non-zero with the real diagnostic, + from every subcommand that opens a window. +- Be told before a run starts that its configured timestep is above the + stability limit for the mesh it is about to run on. + +Golden Demo + +Sealed Box (new, TASK-052) -- a tracer in a closed lid-driven cavity, +visibly staying in the box, with its domain integral asserted constant +under pure advection. Lid-Driven Cavity is re-measured alongside it +rather than replaced: it is the demo whose numbers move, so it is the +regression evidence that the change is physics rather than a tolerance +edit. + +**Chosen over a prescribed-inflow channel demo, which was the more +ambitious option and is recorded as deferred rather than dropped** +(`docs/planning/backlog.md`). Making `BoundaryFaceConfig.velocity` live +means PyFlow can express an inlet for the first time, but an outlet +needs a Neumann velocity face plus pressure anchoring, neither of which +this stage has explored -- and a demo whose own treatment is unexplored +is a design session, not a demonstration. + +Tasks include + +- TASK-052 — Prescribed Boundary Velocity Reaches The Schemes +- TASK-053 — A Failed Frame Fails The Run +- TASK-054 — Timestep Stability Warning + +### Completion Criteria + +**Written 2026-09-12, before this stage's first task**, per +`docs/practices.md`'s "A stage gets completion criteria before its first +task" -- and drafted from the Goal above rather than from the three +tasks' own acceptance criteria, per that rule's own second half: a +checklist assembled from its tasks cannot fail an audit its tasks +passed. + +**Each criterion's qualifier is its own bullet with its own named +check**, per the qualifier rule (`docs/practices.md`, "The intent lives +in the qualifier"). Where a criterion names a count, the count is the +claim. + +1. **Every operator that consumes a boundary face's normal velocity + reads the same prescribed value, from one source.** + - Structural: exactly one function resolves a boundary face's normal + velocity, and both `FirstOrderUpwindAdvection` and + `GreenGaussDivergence` call it. Checked by + `inspect.getsource`, not by reading the diff -- two operators that + agree today and drift tomorrow is the defect this criterion exists + for, and it is the shape the repository already had. + - Physical: a closed domain conserves a purely advected scalar to + floating-point tolerance, with diffusion and source both zeroed so + only transport can move it. + - Mutation-verified: reverting the resolution to the owner cell's own + velocity must fail the conservation scenario. A scenario that + passes either way is what let this through for sixteen days. +2. **A prescribed non-zero boundary normal velocity reaches the solver.** + - A configuration that sets one produces a measurably different + solution from the same configuration without it -- not merely one + that loads without error, which is what it already did. +3. **No `BoundaryFaceConfig` field is validated and then ignored.** + - Checked by a sweep over `dataclasses.fields(BoundaryFaceConfig)` + rather than a hand-kept list, so a field added later is covered + without anybody remembering to add it. + - With the guard that the sweep reaches something at all + (`tests/unit/test_golden_demo_annotations.py`'s own precedent): a + sweep over an empty set passes silently. +4. **A run that raises inside a frame exits non-zero and prints the real + diagnostic.** + - Both backends (`glfw` and `offscreen`), and both window-opening + subcommands (`run` and `play`). + - Asserted as an exit code **and** a stderr substring from the + engine's own exception, per `tests/integration/`'s own convention: + an exit code alone does not distinguish a real failure from + argparse. + - A `--max-frames` run that raises must *terminate*. It currently + hangs on `glfw`, because the raise skips the reschedule. +5. **A configured timestep above this scheme combination's own stability + limit is reported before the run starts.** + - On `run`, `record` and `resume` alike -- `record` is the path a + long unattended run uses, and has the same defect. + - Naming the configured timestep, the stable one, and their ratio. + A warning that says only "unstable" tells a user nothing they can + act on. + - Non-fatal, and its *absence* below the limit is checked too: a + warning that always fires is a warning nobody reads. +6. **Documentation describes what now exists, checked by grep rather + than diff review** (`docs/practices.md`, "A stage's documentation + sweep is a grep, not a diff review"). + - Including the two docstrings that currently describe a data path + that does not exist (`divergence.py`'s `BoundaryFaceConfig.velocity` + claim, and `schema.py`'s claim that `GreenGaussDivergence`/`PISO` + read it). + - And Stage 4's own use case this stage exists to make true, which + stays as written -- it was the right claim; it was not true. +7. **The re-baselined validation numbers are recorded with their + predecessors.** + - In the feature file and in the task entry, both old and new, so + the change in physics is visible evidence rather than a silent + tolerance edit. + - And the checks that should *not* move (Taylor-Green, Couette, and + Stage 6's periodic domain integrals and decay rates) are confirmed + unmoved rather than assumed so. + +### Discharge map + +| Criterion | Task | +|-----------|------| +| 1. One source for a boundary's normal velocity | TASK-052 | +| 2. A prescribed velocity reaches the solver | TASK-052 | +| 3. No validated-then-ignored config field | TASK-052 | +| 4. A failed frame fails the run | TASK-053 | +| 5. Timestep stability reported up front | TASK-054 | +| 6. Documentation matches the tree | Whichever task lands last | +| 7. Re-baselined numbers recorded with their predecessors | TASK-052 | + +--- + +# Stage 10 — Better Numerics Goal @@ -12697,7 +12918,7 @@ future capability level ('Additional Numerical Frameworks')". restates the point and cites that ADR for it. Keeping WENO here would have meant a completion criterion this stage could only meet by contradicting both -- a bar that gets loosened later rather than met, -which is the shape `docs/practices.md` rules out. Stage 12 (Additional +which is the shape `docs/practices.md` rules out. Stage 13 (Additional Numerical Frameworks) carries the pointer now. `docs/implementation/upgrade-paths.md`'s Advection path is unchanged, because a path is an ordering, not a stage assignment, and WENO is still @@ -13148,7 +13369,7 @@ sits closer to architecture than to a scheme. Separately, `docs/CHANGELOG-DESIGN.md`'s TASK-034 entry records that the pressure correction sits outside the time integrator because four pressure solves per RK4 step "would buy a reduction in splitting error that nothing in -Stage 5 could measure", and says in as many words: "Revisit when Stage 9 +Stage 5 could measure", and says in as many words: "Revisit when Stage 10 (Better Numerics)'s less diffusive schemes make it visible." That revisit is now due, it is squarely this stage's subject, and it is a different piece of work from implementing SIMPLE. Which of the two this @@ -13168,7 +13389,7 @@ second. --- -# Stage 10 — Geometry +# Stage 11 — Geometry Goal @@ -13186,7 +13407,7 @@ Flow around a cylinder. --- -# Stage 11 — Adaptive Resolution +# Stage 12 — Adaptive Resolution Goal @@ -13204,7 +13425,7 @@ Adaptive vortex refinement. --- -# Stage 12 — Additional Numerical Frameworks +# Stage 13 — Additional Numerical Frameworks Goal @@ -13242,9 +13463,10 @@ Tasks include - Coupling or co-simulation boundary between it and the FVM core - Rendering for whatever representation the alternative framework uses -**WENO arrives here rather than in Stage 9 (Better Numerics), moved +**WENO arrives here rather than in Stage 10 (Better Numerics), moved 2026-09-04 when that stage's completion criteria were drafted -(maintainer's call).** It sat in Stage 9's work list, and two documents +(maintainer's call).** It sat in Stage 10 (Better Numerics)'s work +list, and two documents that predate that list already put it somewhere else: `adr/ADR-002-fvm-first.md`'s Negative consequences say "some very high-order schemes (e.g. high-order WENO) are more naturally expressed @@ -13276,7 +13498,7 @@ does not. --- -# Stage 13 — Three Dimensions +# Stage 14 — Three Dimensions Goal @@ -13295,7 +13517,7 @@ Golden Demo --- -# Stage 14 — Performance +# Stage 15 — Performance Goal @@ -13314,7 +13536,7 @@ Performance benchmark suite. --- -# Stage 15 — Advanced Physics +# Stage 16 — Advanced Physics Goal diff --git a/docs/planning/stage-shape.yaml b/docs/planning/stage-shape.yaml index 34fea8c..935975e 100644 --- a/docs/planning/stage-shape.yaml +++ b/docs/planning/stage-shape.yaml @@ -20,7 +20,7 @@ # no declared shape for it to be missing from. A survey run during that # audit found no two stages shared a shape: Stage 0 uses headings where # every other stage uses bare labels, Stage 6 has no stage-level Golden -# Demo at all, Stage 15 calls its work list something different, and the +# Demo at all, Stage 16 calls its work list something different, and the # "design questions" section has six different names across five stages. # See `docs/planning/stage-specification.md` for that survey and what it # implies. @@ -102,7 +102,7 @@ sections: failure mode this project keeps finding. Stages 0-7 correctly have none. - "Possible capabilities" is Stage 15's (Advanced Physics) form, and + "Possible capabilities" is Stage 16's (Advanced Physics) form, and it is the more honest label for the furthest-out stage on the roadmap: nothing there is a task yet. Accepted rather than normalised, for the same reason. @@ -128,7 +128,7 @@ sections: demos sat under a `### Golden Demos` heading *inside* TASK-038's own entry; Stages 1 and 2 put theirs at the very end of the stage, below every task, where a reader scanning the stage's own preamble - would not meet them; and Stage 15 (Advanced Physics) named none at + would not meet them; and Stage 16 (Advanced Physics) named none at all. - id: serves diff --git a/docs/planning/status.md b/docs/planning/status.md index cd5c08b..9b3fcf1 100644 --- a/docs/planning/status.md +++ b/docs/planning/status.md @@ -17,7 +17,7 @@ demand, not part of this file. ## Progress -**52/52 tasks complete (100%)** across 16 planned stages. For the full plan, including +**52/52 tasks complete (100%)** across 17 planned stages. For the full plan, including stages below not yet broken into tasks: [roadmap.md](roadmap.md). ```mermaid @@ -41,7 +41,7 @@ pie showData ### Up next -**Stage 9 -- Better Numerics** is next, and has not been broken into tasks yet. +**Stage 9 -- Solver & Run Integrity** is next, and has not been broken into tasks yet. ## Live repository facts @@ -166,31 +166,35 @@ pie showData | TASK-048 -- Live Scrub | Done | 2026-09-09 | `playback.py` | | TASK-051 -- Combined Solved-Velocity + Declared-Field Playback | Done | 2026-09-09 | `playback.py` | -### Stage 9 -- Better Numerics +### Stage 9 -- Solver & Run Integrity + +**no status recorded** -- not yet broken into tasks; 7 criteria defined, no status line yet + +### Stage 10 -- Better Numerics **no status recorded** -- not yet broken into tasks; 8 criteria defined, no status line yet -### Stage 10 -- Geometry +### Stage 11 -- Geometry **no status recorded** -- not yet broken into tasks; 0 criteria defined, no status line yet -### Stage 11 -- Adaptive Resolution +### Stage 12 -- Adaptive Resolution **no status recorded** -- not yet broken into tasks; 0 criteria defined, no status line yet -### Stage 12 -- Additional Numerical Frameworks +### Stage 13 -- Additional Numerical Frameworks **no status recorded** -- not yet broken into tasks; 0 criteria defined, no status line yet -### Stage 13 -- Three Dimensions +### Stage 14 -- Three Dimensions **no status recorded** -- not yet broken into tasks; 0 criteria defined, no status line yet -### Stage 14 -- Performance +### Stage 15 -- Performance **no status recorded** -- not yet broken into tasks; 0 criteria defined, no status line yet -### Stage 15 -- Advanced Physics +### Stage 16 -- Advanced Physics **no status recorded** -- not yet broken into tasks; 0 criteria defined, no status line yet diff --git a/docs/practices.md b/docs/practices.md index e552e6e..e9149d6 100644 --- a/docs/practices.md +++ b/docs/practices.md @@ -253,8 +253,9 @@ number that has just changed, which is the worst combination: the search you would run finds the references that are still *right*. So when referring to a Stage from outside `roadmap.md`, write the name -alongside the number: "Stage 13 (Three Dimensions)", not "Stage 13" -(updated 2026-09-07 to the number's current value -- the whole point of +alongside the number: "Stage 14 (Three Dimensions)", not "Stage 14" +(updated 2026-09-12 to the number's current value, and 2026-09-07 before +that -- the whole point of this rule is that the name, not the number, is what a reader should trust, so the illustration has to keep matching the live number or it undermines its own point; the 2026-08-21 history in the paragraph above @@ -1542,6 +1543,50 @@ implementation before trusting it green*. This rule says what to do with the answer when the test passes anyway: the problem is usually not the assertion, it is where the expected value came from. +## When a test is found weak, ask why its fixture had to be that shape + +**Standing rule, 2026-09-12, from the end-to-end audit that opened Stage +9 (Solver & Run Integrity).** The rule above says what to do when a test +passes and should not have. This one is about the step after: a weak +test is frequently a *symptom* of the defect sitting next to it, and +stopping at "the test was weak" is stopping one question short. + +The worked case. `first_order_upwind_advection.feature`'s "Conservation +on a closed domain" scenario was found weak by the Stage 4 exit audit on +2026-08-28, correctly and by mutation: forcing every advective face flux +to `0.0` left it passing, because the fixture makes every boundary +cell's velocity exactly zero, so every boundary flux is zero whatever +face value the scheme picks. That audit did the right things -- it +recorded the weakness in the feature file, added a periodic scenario +with real teeth to carry the criterion, and deliberately kept the weak +one for the narrower claim it does still check. + +**What nobody asked was why the fixture needed that shape.** The answer, +found fifteen days later, is that it could not have had any other: +`FirstOrderUpwindAdvection` reads the *owner cell's* velocity at a +boundary face rather than the prescribed one, so a closed-domain fixture +with any interior motion near a wall leaks -- 14.27% of a purely +advected tracer in 400 steps on a shipped demo. The fixture was not +lazily chosen. It was the only closed-domain fixture that could have +passed, and that was the finding. + +So when an audit finds a fixture that makes a check vacuous, ask both +questions rather than one: + +- **Could this fixture be strengthened?** -- the question that produces + a better test, and the one the 2026-08-28 audit answered. +- **Why was this fixture necessary?** -- the question that produces a + bug report. A fixture narrowed until the test passes is evidence about + the implementation, not only about the test. Say what a realistic + fixture would have done, and if the answer is "failed", the defect is + in the code. + +**This is not a criticism of writing the weak test down.** Recording it +honestly is what made it findable at all; a deleted scenario leaves no +trace to re-read. The addition is only that a recorded weakness is an +open question about the implementation until somebody answers it, not a +closed one about the test. + ## A gap recorded in a `CLAUDE.md` is not recorded against a criterion **Standing rule, 2026-08-29, from the Stage 5 exit audit.** That stage diff --git a/planning/data/capabilities.yaml b/planning/data/capabilities.yaml index 98e8a34..cd01bcc 100644 --- a/planning/data/capabilities.yaml +++ b/planning/data/capabilities.yaml @@ -166,7 +166,7 @@ entities: description: Alternative numerical frameworks (SPH, FLIP, PIC) where justified. documented_in: docs/planning/implementation-plan.md unresolved: >- - Scheduled 2026-08-21 -- `roadmap.md` Stage 12 (Additional Numerical + Scheduled 2026-08-21 -- `roadmap.md` Stage 13 (Additional Numerical Frameworks) now serves this Level, closing the "no corresponding Stage" gap open since 2026-08-15. It still has no `realised_by` edge, and that is now a different statement: no component in diff --git a/planning/data/stages.yaml b/planning/data/stages.yaml index 38dd442..b71ed09 100644 --- a/planning/data/stages.yaml +++ b/planning/data/stages.yaml @@ -94,55 +94,65 @@ entities: fact, rather than unlocking a new physical or numerical capability. - id: stage-9 - name: "Stage 9 — Better Numerics" + name: "Stage 9 — Solver & Run Integrity" + documented_in: docs/planning/roadmap.md + must_appear_in: docs/planning/roadmap.md + unresolved: >- + No dedicated Capability Level -- the same "tasks added to whichever + Stage needs them" pattern this file already records for stage-7 and + stage-8. It corrects and reports on capability already claimed + rather than unlocking a new physical or numerical one. + + - id: stage-10 + name: "Stage 10 — Better Numerics" documented_in: docs/planning/roadmap.md must_appear_in: docs/planning/roadmap.md edges: - type: serves to: capability-level-4 - - id: stage-10 - name: "Stage 10 — Geometry" + - id: stage-11 + name: "Stage 11 — Geometry" documented_in: docs/planning/roadmap.md must_appear_in: docs/planning/roadmap.md edges: - type: serves to: capability-level-5 - - id: stage-11 - name: "Stage 11 — Adaptive Resolution" + - id: stage-12 + name: "Stage 12 — Adaptive Resolution" documented_in: docs/planning/roadmap.md must_appear_in: docs/planning/roadmap.md edges: - type: serves to: capability-level-6 - - id: stage-12 - name: "Stage 12 — Additional Numerical Frameworks" + - id: stage-13 + name: "Stage 13 — Additional Numerical Frameworks" documented_in: docs/planning/roadmap.md must_appear_in: docs/planning/roadmap.md edges: - type: serves to: capability-level-7 - - id: stage-13 - name: "Stage 13 — Three Dimensions" + - id: stage-14 + name: "Stage 14 — Three Dimensions" documented_in: docs/planning/roadmap.md must_appear_in: docs/planning/roadmap.md edges: - type: serves to: capability-level-8 - - id: stage-14 - name: "Stage 14 — Performance" + - id: stage-15 + name: "Stage 15 — Performance" documented_in: docs/planning/roadmap.md must_appear_in: docs/planning/roadmap.md edges: - type: serves to: capability-level-9 - - id: stage-15 - name: "Stage 15 — Advanced Physics" + - id: stage-16 + name: "Stage 16 — Advanced Physics" documented_in: docs/planning/roadmap.md must_appear_in: docs/planning/roadmap.md edges: diff --git a/src/pyflow/engine/CLAUDE.md b/src/pyflow/engine/CLAUDE.md index e83c5c1..c764284 100644 --- a/src/pyflow/engine/CLAUDE.md +++ b/src/pyflow/engine/CLAUDE.md @@ -127,7 +127,7 @@ contract suite gained one more implementation-independent invariant for it: a face's two vertices are exactly `face_area(face)` apart, since that's just what "area" means for a line segment, true for any `Mesh`. 2D-specific, like every other `Mesh`/`CoordinateSystem` method so far -- -expected to need revisiting once Stage 13 (Three Dimensions) arrives, +expected to need revisiting once Stage 14 (Three Dimensions) arrives, not a gap being worked around now. @@ -422,7 +422,7 @@ in `advection.py`, `gradient.py`, and `divergence.py` -- the last two since TASK-027's own concrete schemes -- rather than read off `Mesh`, which exposes no dimensionality accessor) records that this project is 2D-only for now, matching every other `Mesh`/`CoordinateSystem` method --- revisit both when Stage 13 (Three Dimensions) arrives. +-- revisit both when Stage 14 (Three Dimensions) arrives. **`AdvectionScheme._check_velocity`** is a concrete helper every implementation must call itself, the same `_check_cell`/`_check_face` diff --git a/tools/generators/generate_status_report.py b/tools/generators/generate_status_report.py index 3fd3e33..f6d2602 100644 --- a/tools/generators/generate_status_report.py +++ b/tools/generators/generate_status_report.py @@ -497,7 +497,7 @@ def _frontier_stage(stages: list[StageStatus]) -> StageStatus | None: """The first stage not marked complete, in roadmap order. `None` (all complete) is representable but not expected any time - soon -- Stage 15 is the last one currently in the roadmap. + soon -- Stage 16 is the last one currently in the roadmap. """ for stage in stages: if not stage.complete_claimed: