Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 31 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@

**Current Version:** 0.0.1 — no release has been made.

PyFlow has completed **Stage 3 (Numerical Engine)** and is about to
begin Stage 4 (First Numerical Methods). Stage 0 built the engineering
PyFlow has completed **Stage 4 (First Numerical Methods)** and has not
yet begun Stage 5 (First Fluid Solver). Stage 0 built the engineering
foundations; Stage 1 added the first real engine code -- a
`CoordinateSystem`, a `Mesh` with a structured Cartesian implementation,
and a mesh visualiser you can zoom and pan; Stage 2 added `Field` and
Expand All @@ -18,11 +18,18 @@ them visible; Stage 3 added the six `adr/ADR-003-modular-numerical-
strategies.md` interfaces (advection, diffusion, time integration,
pressure-velocity coupling, linear solver, boundary condition) and the
configuration/assembly mechanism that resolves a configured name to a
real instance. There is still no *physics*: the interfaces exist and
assemble, but nothing under `src/` computes anything real yet -- that is
Stage 4. See `docs/planning/roadmap.md` for the per-task status and each
stage's exit audit, and `docs/implementation/golden-demos.md` for what
each stage's demonstration proves.
real instance, with every interface still resolving only to a trivial,
non-physical reference implementation; Stage 4 gave each of those six
interfaces its first real, physically meaningful implementation
(`FirstOrderUpwindAdvection`, `CentralDifferenceDiffusion`, `RK4Integrator`,
`ConjugateGradientSolver`, `PISO`, `Dirichlet`/`Neumann`/periodic boundary
conditions) and, with them, PyFlow's first live-stepping simulation --
see the Passive Scalar Transport demo below. Stage 5 is what solves
incompressible flow for real (a coupled velocity/pressure system);
today's numerical schemes are individually real but not yet assembled
into that solve. See `docs/planning/roadmap.md` for the per-task status
and each stage's exit audit, and `docs/implementation/golden-demos.md`
for what each stage's demonstration proves.

The project's primary objective is to build a reusable fluid simulation engine while documenting every significant engineering decision along the way.

Expand Down Expand Up @@ -74,7 +81,7 @@ make install # creates .venv, installs dependencies, installs the git pre-comm
Then:

```bash
make demo # opens the render window -- press Escape/Enter or close the window to exit (no simulation yet)
make demo # opens the render window with the built-in default config -- press Escape/Enter or close the window to exit (no simulation configured by default; see "Current Phase" below for one that steps live)
make test # runs the test suite, with a coverage report
make lint # formats and lints code and docs (see the Makefile's own comment for exactly what runs)
make ci # the full sequence CI runs on every push and pull request -- lint, typecheck, test, and the documentation/graph/inventory/manifest checks; see CLAUDE.md for what each one covers
Expand Down Expand Up @@ -117,9 +124,10 @@ need to find it.

## Current Phase

Stage 4 — First Numerical Methods.
Stage 5 — First Fluid Solver -- not yet started (Stage 4 closed
2026-08-28, PR #38).

Stages 0 through 3 are complete, each closed against its own written
Stages 0 through 4 are complete, each closed against its own written
completion criteria (`docs/planning/roadmap.md`):

- Stage 0 — planning system, capability map, repository structure,
Expand All @@ -140,14 +148,23 @@ completion criteria (`docs/planning/roadmap.md`):
interface resolves only to a trivial, non-physical reference
implementation, an explicit exception recorded against that stage's
own completion criteria.
- Stage 4 — each of Stage 3's six interfaces gets its first real,
physically meaningful implementation (`FirstOrderUpwindAdvection`,
`CentralDifferenceDiffusion`, `RK4Integrator`, `ConjugateGradientSolver`,
`PISO`, and Dirichlet/Neumann/periodic boundary conditions), plus the
simulation-stepping mechanism that drives a live `pyflow run`
(`engine/simulation.py`), demonstrated in the Passive Scalar Transport
golden demo. Individually real numerics, not yet the coupled
velocity/pressure solve -- that's Stage 5.

Stage 4 gives each of Stage 3's interfaces its first real, physically
meaningful implementation.
Stage 5 will solve incompressible flow: a coupled velocity/pressure
system built from Stage 4's now-real numerical schemes.

Try the most recent demonstration:
Try the most recent demonstration -- a scalar blob advected and
diffused across a periodic domain, stepped live:

```bash
uv run python -m pyflow run --config examples/golden-demos/field_display.yaml
uv run python -m pyflow run --config examples/golden-demos/passive_scalar_transport.yaml
```

---
Expand Down
18 changes: 15 additions & 3 deletions docs/planning/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ This paragraph previously said `make install` and `make test` were still
expected to fail, pending `uv.lock` and a test suite (B2/C1) -- stale
since 2026-08-16 and corrected 2026-08-19. Both now succeed: `uv.lock`
is committed (B2) and `make test` runs the suite with coverage
(C1a/C1b): **603 tests at 99% as of 2026-08-28**, having been 64 when
(C1a/C1b): **605 tests at 99% as of 2026-08-28**, having been 64 when
this paragraph was rewritten on 2026-08-19, 202 earlier the same day,
212 after TASK-014, 226 after TASK-015, 250 after TASK-016, 287 after
TASK-017, 297 after TASK-039, 315 after the Stage 2 exit audit and 337
Expand Down Expand Up @@ -371,8 +371,20 @@ The rest of the climb to 508 that same day is
suite -- growing from 23 to 35 tests as that tool itself grew, a live
demonstration that this count moves for reasons having nothing to do
with the fluid solver and everything to do with why it needs checking
rather than re-reading. **53 of those 603 are Gherkin scenarios
rather than pytest functions**
rather than re-reading. The same is true again, 2026-08-28: 603 to 605
from a CLI help-message accuracy fix, no Stage 4/5 task work involved --
`pyflow --help`'s top-level text still described the CLI as a "Stage 0
skeleton -- no simulation functionality yet" and never mentioned
`--config` or how to run a golden demo, caught by a user auditing the
CLI directly. Fixed in `src/pyflow/__main__.py`'s `description`/`epilog`
(phrased by capability now, not roadmap stage number, so it survives a
stage exit unedited), with two new tests holding it there --
`test_top_level_help_describes_current_capabilities`
(`tests/unit/test_main.py`) and its subprocess-boundary mirror in
`tests/integration/test_cli.py` -- and a dated rule in
`src/pyflow/CLAUDE.md` requiring this text be revisited whenever a
subcommand, flag, or golden demo changes. **53 of those 605 are Gherkin
scenarios rather than pytest functions**
(`adr/ADR-007-executable-acceptance-criteria.md`; up from fourteen with
`field_display.feature` gaining scenarios and `numerics_assembly.feature`
joining, TASK-021; to 24 with TASK-040's own
Expand Down
2 changes: 1 addition & 1 deletion docs/planning/status.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ pie showData
## Live repository facts

- **45** `CLAUDE.md` files
- **603** tests collected
- **605** tests collected
- **53** Gherkin scenarios (`tests/features/*.feature`)

## Stages
Expand Down
26 changes: 26 additions & 0 deletions docs/practices.md
Original file line number Diff line number Diff line change
Expand Up @@ -1115,6 +1115,32 @@ where a command already explains itself when run (e.g. `make clean`
stating what it can't remove and why), point at running it rather than
restating its output in the README.

**The same obligation extends to `README.md`'s Project Status and
Current Phase sections** (added 2026-08-28, maintainer's instruction,
after Stage 4's own closing PR (#38) merged and neither section was
updated in the same change -- both still described the project as
mid-Stage-3/about to begin Stage 4, and pointed a new reader at Stage
2's own demo as "the most recent" one to try, discovered when a user
asked for an unrelated CLI fix and read the README while auditing what
else might be stale). These sections exist for the audience the Quick
Start rule above already names -- specifically, "relevant information a
potential user would want to see": whoever lands on this README to find
out what PyFlow can currently do, not only how to install it. Whenever a
stage opens or closes -- Completion Criteria met, exit audit written,
`roadmap.md`'s own status line updated -- update `README.md`'s Project
Status paragraph, Current Phase section, and its "try the most recent
demonstration" pointer in the same change; this is a Blast Radius
consequence of closing a stage, not a separate documentation pass to
remember later. Stale status here is worse than stale Quick Start
instructions: it doesn't just inconvenience a contributor who already
knows what the project does, it actively misinforms a first-time reader
about what they are looking at. **Found stale, fix it -- don't only
report it**: if a session doing unrelated work notices README.md (or any
other document) stating something the repository no longer does, correct
it in the same change rather than leaving a note for later, the same
standard `docs/CLAUDE.md`'s Validation section already sets for a
principle violation.

The Definition of Done for documentation lives in
`docs/documentation-guidelines.md` and is not restated elsewhere.

Expand Down
4 changes: 3 additions & 1 deletion docs/repository-manifest.md
Original file line number Diff line number Diff line change
Expand Up @@ -585,7 +585,9 @@ stage boundary, not only when something here is being edited.

`tests/` with `unit/`, `integration/`, `golden/`, `performance/`.

🟨 — 56 test modules, **603 tests, 99% coverage** (2026-08-28; 35 of
🟨 — 56 test modules, **605 tests, 99% coverage** (2026-08-28; two of
those from a same-day CLI help-message accuracy fix in existing modules
`test_main.py`/`test_cli.py`, no new module; 35 of
these being `test_generate_status_report.py` itself; the 47th module,
`test_simulation.py` (TASK-040), was the first Gherkin feature file
bound outside `tests/golden/` -- not a golden demo, so it lives here per
Expand Down
31 changes: 31 additions & 0 deletions src/pyflow/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,37 @@ machinery, not a phenomenon. Keep that line; the moment a discretisation
lands in `physics/` or a phenomenon in `numerics/`, the distinction stops
paying for itself.

**The CLI's own help text (`__main__.py`'s top-level `description`/
`epilog`, plus `run_parser`'s own `epilog`) must be kept current with
what PyFlow can actually do** (rule added 2026-08-28, prompted by a user
noticing it wasn't: the top-level `description` still read "Stage 0
skeleton -- no simulation functionality yet" through the entirety of
Stage 4 (TASK-023 through TASK-030, 2026-08-27..28) landing real
numerics, PISO, and a live-stepping golden demo -- stale from the first
of those, not from some distant point; PyFlow moves fast enough that a
day of silence is enough for this text to go wrong, so "recently" is not
a reason to skip re-reading it. Neither `--config` nor how to run a
golden demo was mentioned anywhere the bare-invocation or top-level
`--help` output would show them --
argparse only surfaces a subcommand's own flags under that subcommand's
own `--help`, so `run_parser`'s `--config` help text was never enough on
its own). Concretely: whenever a subcommand or flag is added, removed,
or renamed, or a new golden demo lands under `examples/golden-demos/`,
re-read `__main__.py`'s `description`/`epilog` text in the same change
and update it if it no longer matches -- this is the Blast Radius rule
applied to the CLI's own self-description, not a separate obligation.
Phrase the top-level `description` in terms of what the CLI can *do*,
never by roadmap stage number, so a stage exit that changes nothing
about the CLI itself never forces an edit here. **This is enforced by a
test, not only remembered**: `tests/unit/test_main.py`'s
`test_top_level_help_describes_current_capabilities` (mirrored in
`tests/integration/test_cli.py` across the real subprocess boundary)
asserts concrete current content (`--config`, `examples/golden-demos`)
is present and the stale `"Stage 0"` claim is not, rather than only the
structural markers (`usage:`, `-h, --help`) the original C1a test
already checked -- so a forgotten or reverted update fails `make test`
instead of depending on a reviewer noticing.

**`__main__.py`'s second subcommand, `pyflow generate-config [--output
PATH]` (TASK-039, added 2026-08-21)**, does not orchestrate multiple
subpackages the way `bootstrap()` does -- it is a thin argparse wrapper
Expand Down
46 changes: 43 additions & 3 deletions src/pyflow/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,17 @@
scaffold to stdout, or writes it to `--output PATH` if given -- so a
config author starts from something `load_config` already accepts
rather than hand-typing section and field names from memory.

The top-level parser's own `description`/`epilog` (below) is the CLI's
self-description, printed both by bare invocation and by `--help`.
**It must be kept current with what the CLI can actually do** -- see
`src/pyflow/CLAUDE.md`'s dated rule, added 2026-08-28 after this text
spent well past Stage 0 still claiming "no simulation functionality
yet" and never mentioning `--config` or how to run a golden demo at
all (argparse does not surface a subcommand's own flags at the
top level, so `run_parser`'s `--config` help text alone was never
enough). `description` is phrased by capability, not by roadmap stage
number, so it does not need editing every stage exit.
"""

import argparse
Expand All @@ -37,16 +48,45 @@ def main(argv: list[str] | None = None) -> None:
"""
parser = argparse.ArgumentParser(
prog="pyflow",
description="PyFlow: a modular, field-centric computational fluid "
"dynamics engine. Stage 0 skeleton -- no simulation "
"functionality yet.",
description=(
"PyFlow: a modular, field-centric computational fluid dynamics\n"
"engine. Configure a mesh, boundary conditions, and numerical\n"
"scheme in YAML, and PyFlow will assemble, run, and visualise\n"
"the simulation."
),
epilog=(
"examples:\n"
" pyflow run\n"
" Run with the built-in default configuration.\n"
" pyflow run --config path/to/config.yaml\n"
" Run with your own configuration file.\n"
" pyflow run --config examples/golden-demos/<name>.yaml\n"
" Run one of the golden demos shipped under "
"examples/golden-demos/\n"
" (see docs/implementation/golden-demos.md for what each "
"one shows).\n"
" pyflow generate-config --output config.yaml\n"
" Write a valid starting configuration file, ready to "
"edit.\n"
"\n"
"Run 'pyflow <command> --help' for a command's own options -- "
"e.g. 'pyflow run --help'\n"
"for --config, --max-frames, and --backend."
),
formatter_class=argparse.RawDescriptionHelpFormatter,
)
subparsers = parser.add_subparsers(dest="command")

run_parser = subparsers.add_parser(
"run",
help="Bootstrap the engine: load configuration, initialise logging, "
"open the rendering window, and run until it's closed.",
epilog=(
"example:\n"
" pyflow run --config examples/golden-demos/<name>.yaml "
"--backend offscreen --max-frames 100\n"
),
formatter_class=argparse.RawDescriptionHelpFormatter,
)
run_parser.add_argument(
"--config",
Expand Down
17 changes: 17 additions & 0 deletions tests/integration/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,23 @@ def test_entry_point_prints_version_and_help() -> None:
assert "-h, --help" in result.stdout


def test_entry_point_help_mentions_config_flag_and_golden_demos() -> None:
"""Real packaged-entry-point companion to
`tests/unit/test_main.py::test_top_level_help_describes_current_capabilities`
-- the same content, verified crossing the real subprocess boundary.
"""
result = subprocess.run(
[sys.executable, "-m", "pyflow", "--help"],
capture_output=True,
text=True,
check=False,
)

assert result.returncode == 0
assert "--config" in result.stdout
assert "examples/golden-demos" in result.stdout


def test_generate_config_prints_valid_yaml_to_stdout() -> None:
"""`pyflow generate-config` with no arguments (TASK-039): a real
subprocess, per this project's CLI-testing convention, checking
Expand Down
18 changes: 18 additions & 0 deletions tests/unit/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,24 @@ def test_no_args_prints_version_and_help(capsys: pytest.CaptureFixture[str]) ->
assert "-h, --help" in captured.out


def test_top_level_help_describes_current_capabilities(
capsys: pytest.CaptureFixture[str],
) -> None:
"""The top-level help text must reflect what PyFlow can actually do
(`src/pyflow/CLAUDE.md`'s "help text must stay current" rule) -- it
previously described the CLI as a "Stage 0 skeleton -- no simulation
functionality yet" long after real numerics and golden demos landed,
and never mentioned `--config` or how to run one at all, since
`--config` lives on the `run` subcommand's own help and argparse
does not surface a subcommand's flags in the top-level listing.
"""
main([])
captured = capsys.readouterr()
assert "Stage 0" not in captured.out
assert "--config" in captured.out
assert "examples/golden-demos" in captured.out


def test_run_dispatches_to_bootstrap_with_parsed_args() -> None:
with patch("pyflow.__main__.bootstrap") as mock_bootstrap:
main(
Expand Down
Loading