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
24 changes: 23 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -239,9 +239,31 @@ prevent (P-011, single authoritative source).
- `make check-status` -- fail if `docs/planning/status.md` is stale, or
if that drift check finds a disagreement. Added 2026-08-26, part of
`make ci`.
- `make config-template` -- regenerate
`docs/implementation/config-template.yaml`: every `PyFlowConfig` field
from `src/pyflow/configuration/schema.py`, with a comment above each
one stating what counts as a valid value and what does not.
`pyflow generate-config` (TASK-039) already produces a loadable
scaffold from the same schema, but `PyYAML`'s `safe_dump` cannot emit
comments, so it carries no explanation -- this generator is that
explanation, kept next to its own source of truth (its
`FIELD_COMMENTS`/`SECTION_COMMENTS`) instead of hand-typed once into a
committed file and left to drift, the same restated-fact failure mode
this section's other generators exist to close. Added 2026-08-28 at a
user's direct request, with the explicit condition that it stay
current as the schema evolves -- see
`src/pyflow/configuration/CLAUDE.md` for the rule that keeps it so.
- `make check-config-template` -- fail if the committed template is
stale relative to the live schema or this generator's own comments.
Part of `make ci`. `tests/unit/test_generate_config_template.py`'s own
`test_every_live_config_field_has_a_comment` is the narrower,
always-on companion: it fails a plain `make test` the moment a field
is added to `schema.py` with no matching comment, not only at
`make ci` time.
- `make ci` -- `lint typecheck test check-docs check-docs-index
check-graph check-dependency-tree check-inventory check-manifest
check-references check-scenarios check-status` together (this list
check-references check-scenarios check-status check-config-template`
together (this list
itself went stale by two targets, `check-references` and
`check-scenarios`, before this correction -- restated facts drift even
in the document that warns about restated facts); this is what CI
Expand Down
29 changes: 27 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
.PHONY: install lint format typecheck test check-docs check-docs-index check-graph \
dependency-tree check-dependency-tree inventory check-inventory \
check-manifest check-references check-scenarios check-claims status-report \
check-status docs demo ci clean
check-status config-template check-config-template docs demo ci clean

install:
uv sync
Expand Down Expand Up @@ -118,7 +118,7 @@ check-inventory:
check-manifest:
uv run python tools/validators/check_manifest.py

ci: lint typecheck test check-docs check-docs-index check-graph check-dependency-tree check-inventory check-manifest check-references check-scenarios check-status
ci: lint typecheck test check-docs check-docs-index check-graph check-dependency-tree check-inventory check-manifest check-references check-scenarios check-status check-config-template

# Fails if prose names a repository path that does not exist. Gating:
# every rule is a definite structural fact (does this path resolve),
Expand Down Expand Up @@ -172,6 +172,31 @@ status-report:
check-status:
uv run python tools/generators/generate_status_report.py --check

# Regenerates docs/implementation/config-template.yaml: every
# PyFlowConfig field, with a comment above each one stating what counts
# as a valid value and what does not. `pyflow generate-config` (TASK-039)
# already produces a loadable scaffold from the same schema, but
# PyYAML's safe_dump cannot emit comments, so it carries no explanation
# -- this generator is that explanation, kept next to its own source of
# truth instead of hand-typed once and left to drift (root CLAUDE.md,
# docs/CLAUDE.md: generate a document that restates a fact the
# repository already knows). See
# tools/generators/generate_config_template.py's own module docstring.
config-template:
uv run python tools/generators/generate_config_template.py

# Fails if the committed template is stale relative to the live schema
# (a field's default or type changed) or this generator's own
# FIELD_COMMENTS/SECTION_COMMENTS (an explanation changed without
# regenerating). Part of `make ci`. The narrower, always-on companion
# check -- does every field have *a* comment at all, regardless of
# whether the committed file matches -- is
# tests/unit/test_generate_config_template.py::test_every_live_config_field_has_a_comment,
# which fails a plain `make test` the moment a field is added to
# schema.py with no matching entry, not only at `make ci` time.
check-config-template:
uv run python tools/generators/generate_config_template.py --check

# Regenerates docs/index.md, the navigable map of every documentation
# page (tools/generators/CLAUDE.md). Not a hand-maintained file -- see
# root CLAUDE.md's "Generated documentation must never be edited
Expand Down
12 changes: 12 additions & 0 deletions docs/implementation/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,18 @@ This directory holds implementation-level artifacts per
This line said "runnable code" until 2026-08-18, describing the shape
the rule had already replaced -- see `examples/golden-demos/CLAUDE.md`,
which is authoritative for what lives there.
- `config-template.yaml`, added 2026-08-28 at a user's direct request.
**Not itemised in `docs/planning/knowledge-architecture.md`** -- same
as `.claude/` and `tools/`, the KA spec does not enumerate every
directory or file the project ends up wanting; recorded in
`docs/repository-manifest.md` instead. **Generated, never hand-edited**
(`tools/generators/generate_config_template.py`, from
`src/pyflow/configuration/schema.py`): every `PyFlowConfig` field, with
a comment above each one stating what counts as a valid value and what
does not. Regenerate with `make config-template`; `make
check-config-template` (part of `make ci`) fails if it's stale. See
`src/pyflow/configuration/CLAUDE.md` for the rule keeping it current as
the schema evolves.

Keep these in sync with `adr/ADR-002-fvm-first.md` and
`adr/ADR-003-modular-numerical-strategies.md`, which `mvp.md` and
Expand Down
201 changes: 201 additions & 0 deletions docs/implementation/config-template.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
# Configuration template -- every PyFlowConfig field, with a comment
# above each one stating what counts as a valid value and what does
# not.
#
# GENERATED by tools/generators/generate_config_template.py from
# src/pyflow/configuration/schema.py. Do not edit by hand -- per root
# CLAUDE.md, generated documentation is never edited manually, and
# `make check-config-template` fails if this file is stale relative to
# the live schema. To change what appears here, change schema.py (the
# value shown) or this script's FIELD_COMMENTS/SECTION_COMMENTS (the
# explanation), then run `make config-template`.
#
# Every value below is PyFlowConfig()'s own default -- this file is a
# real, loadable configuration (see
# tests/unit/test_generate_config_template.py), not a set of
# placeholder tokens that would fail `pyflow run --config` if used
# as-is. Copy it and edit the fields you actually want to change; a
# field left out of your own config file simply keeps its default.
#
# For a plain scaffold with no comments, `uv run python -m pyflow
# generate-config` produces the same defaults directly from the schema.

# Logging framework settings.
logging:
# Valid: one of DEBUG, INFO, WARNING, ERROR, CRITICAL (case-insensitive).
# Invalid: any other string, e.g. "VERBOSE".
level: INFO

# Rendering window/canvas settings.
rendering:
# Valid: "glfw" (a real interactive window) or "offscreen" (renders to an
# in-memory array, no window -- what CI and golden-demo regression tests
# use). Invalid: any other string.
backend: glfw
# Valid: a positive integer, in pixels. Invalid: zero, negative, or a
# float.
width: 1280
# Valid: a positive integer, in pixels. Invalid: zero, negative, or a
# float.
height: 720
# Valid: any string. The window's title bar text.
title: PyFlow
# Valid: null (leaves the background unset, pygfx's own default) or a
# "#RRGGBB" hex string, e.g. "#1a1a2e". Invalid: a colour name ("red"), a
# short hex ("#fff"), or a string missing the "#".
background_color: null
# Valid: true or false. Invalid: anything else -- not a colour or a
# string.
show_mesh: false
# Valid: a "#RRGGBB" hex string -- unlike background_color, this one has
# no null/off state; show_mesh above is the switch. Invalid: null, or a
# malformed hex string.
grid_color: '#4477aa'
# Valid: a positive number, and within [zoom_min, zoom_max] below.
# Invalid: zero, negative, or outside that range.
zoom: 1.0
# Valid: a pair of finite numbers [x, y] -- a world-space offset from the
# camera's default centring. Invalid: anything other than exactly two
# numbers.
pan: [0.0, 0.0]
# Valid: a positive number, less than zoom_max. Invalid: zero, negative,
# or >= zoom_max.
zoom_min: 0.1
# Valid: a number greater than zoom_min. Invalid: <= zoom_min.
zoom_max: 10.0

# Structured Cartesian mesh: where the domain starts, how big one cell is,
# and how many cells there are. There is no single "domain size" field --
# the physical domain is extent * spacing along each axis.
mesh:
# Valid: a pair of finite numbers [x, y] -- the mesh's own lower-left
# corner in world space. Invalid: anything other than exactly two numbers.
origin: [0.0, 0.0]
# Valid: a pair of positive numbers [dx, dy] -- the physical size of one
# cell along each axis. Invalid: zero or negative.
spacing: [1.0, 1.0]
# Valid: a pair of positive whole numbers [nx, ny] -- the *number of
# cells* along each axis, not a physical size (a whole number written as a
# float, e.g. [10.0, 4.0], is fine). Invalid: zero, negative, or
# fractional, e.g. [10.9, 3.99] -- rejected outright rather than rounded,
# since silently building a different mesh than the one asked for is worse
# than refusing.
extent: [10, 10]

# Static field visualisation for one rendered frame (built-in demo patterns
# only, not a general initial-condition API).
field_display:
# Valid: null (draw no scalar field) or "radial_gradient", the only built-
# in pattern this field currently accepts. Invalid: any other string.
scalar_pattern: null
# Valid: null (draw no vector field) or "rotational", the only built-in
# pattern this field currently accepts. Invalid: any other string.
vector_pattern: null
# Valid: a "#RRGGBB" hex string. Invalid: null, or a malformed hex string.
low_color: '#0000ff'
# Valid: a "#RRGGBB" hex string. Invalid: null, or a malformed hex string.
high_color: '#ff0000'
# Valid: a pair [min, max] with max strictly greater than min. Invalid:
# max <= min.
value_range: [0.0, 1.0]
# Valid: a "#RRGGBB" hex string. Invalid: null, or a malformed hex string.
arrow_color: '#ffffff'
# Valid: a positive number. Invalid: zero or negative.
arrow_scale: 0.3
# Valid: true or false.
show_legend: true

# Live, repeatedly-stepped simulation seeding -- distinct from field_display
# above, which renders one static frame.
simulation:
# Valid: null (no live simulation runs) or "gaussian_blob", the only
# built-in pattern this field currently accepts. Invalid: any other
# string.
scalar_pattern: null
# Valid: null or "uniform", the only built-in pattern this field currently
# accepts. Invalid: any other string.
velocity_pattern: null
# Valid: a pair of finite numbers [vx, vy] -- a *prescribed*, not solved,
# constant velocity (Stage 5 is what eventually solves for velocity).
# Invalid: anything other than exactly two numbers.
velocity: [1.0, 0.0]

# Numerical scheme selection (adr/ADR-003-modular-numerical-strategies.md)
# plus the physical/solver parameters those schemes take.
numerics:
# Valid: "first_order_upwind" -- the only scheme PyFlow currently
# implements for this component. Invalid: any other string.
advection: first_order_upwind
# Valid: "central_difference" -- the only scheme PyFlow currently
# implements for this component. Invalid: any other string.
diffusion: central_difference
# Valid: a positive number (the physical diffusivity, Gamma). Invalid:
# zero or negative.
diffusion_coefficient: 1.0
# Valid: "rk4" -- the only scheme PyFlow currently implements for this
# component. Invalid: any other string.
time_integration: rk4
# Valid: a positive number. Fixed for the whole run -- PyFlow applies no
# automatic stability/CFL limit, so an unstable choice is not rejected at
# load time, only at runtime (as diverging values). Invalid: zero or
# negative.
timestep: 0.01
# Valid: "conjugate_gradient" -- the only solver PyFlow currently
# implements for this component. Invalid: any other string.
linear_solver: conjugate_gradient
# Valid: a positive number -- the convergence tolerance. Invalid: zero or
# negative.
linear_solver_tolerance: 1.0e-06
# Valid: a positive integer. Invalid: zero, negative, or a float.
linear_solver_max_iterations: 1000
# Valid: "piso" -- the only scheme PyFlow currently implements for this
# component. Invalid: any other string.
pressure_coupling: piso
# One entry per domain edge. All four faces share the same shape
# (BoundaryFaceConfig) and the same per-field rules, explained once below
# under 'north' and not repeated for the other three.
boundary_conditions:
north:
# Valid: "dirichlet", "neumann", or "periodic". If "periodic", the
# OPPOSITE face (north<->south, east<->west) must also be "periodic"
# -- checked jointly across all four faces, not per-face. Invalid: any
# other string, or a lone periodic face whose pair is not also
# periodic.
type: dirichlet
# Valid: null (not prescribed here) or a number -- the
# boundary-*normal* component only, positive = outward. A face may
# prescribe velocity or pressure, never both (see .pressure below); if
# every one of the four faces prescribes a velocity, they must sum to
# zero net flux, weighted by each face's physical length. Invalid:
# prescribing both velocity and pressure on one face, or a nonzero net
# flux across all four.
velocity: 0.0
# Valid: null (not prescribed here) or a number. Mutually exclusive
# with .velocity above on the same face.
pressure: null
# Valid: any finite number -- the Dirichlet value a transported scalar
# field is given at this face. Only read when type is "dirichlet";
# harmless but unused otherwise.
scalar_value: 0.0
# Valid: any finite number -- the Neumann gradient a transported
# scalar field is given at this face. Only read when type is
# "neumann"; harmless but unused otherwise.
scalar_gradient: 0.0
south:
type: dirichlet
velocity: 0.0
pressure: null
scalar_value: 0.0
scalar_gradient: 0.0
east:
type: dirichlet
velocity: 0.0
pressure: null
scalar_value: 0.0
scalar_gradient: 0.0
west:
type: dirichlet
velocity: 0.0
pressure: null
scalar_value: 0.0
scalar_gradient: 0.0
32 changes: 26 additions & 6 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): **606 tests at 99% as of 2026-08-28**, having been 64 when
(C1a/C1b): **614 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 @@ -389,8 +389,14 @@ Stage 4 exit audit: one new Gherkin scenario in
periodic domain"), added because the existing closed-domain conservation
scenario turned out to pass for any flux array whatsoever -- see this
Stage's own Completion Criterion 4 row, below, for the mutation evidence
and why the weak scenario was annotated rather than deleted. **54 of
those 606 are Gherkin scenarios rather than pytest functions**
and why the weak scenario was annotated rather than deleted. 614 the
same day: `tools/generators/generate_config_template.py`'s own test
suite (`tests/unit/test_generate_config_template.py`), eight tests, built
at a user's direct request for an annotated, always-current example
config -- again no Stage 4/5 task work involved, the same "count moves
for reasons having nothing to do with the fluid solver" pattern the
paragraph above already records. **54 of those 614 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 Expand Up @@ -4987,6 +4993,12 @@ restated here as prose. Written to cover, at minimum:

PISO Pressure Coupling

**Status: Done, 2026-08-27, Stage 4's sixth task.**
`src/pyflow/engine/numerics/pressure_coupling.py` implements `PISO`;
`src/pyflow/engine/numerics/gradient.py`/`divergence.py` implement
`GreenGaussGradient`/`GreenGaussDivergence`, both built by this task --
see Design decision One below.

**Intent:** the claim is that a single correction pass **measurably and
boundedly reduces the divergence** of a manufactured provisional velocity
field, checked cell by cell against a stated tolerance -- not "the
Expand Down Expand Up @@ -5229,7 +5241,9 @@ restated here as prose. Written to cover, at minimum:

Dirichlet Boundary

**Status:** Done, 2026-08-28, Stage 4's seventh task.
**Status: Done, 2026-08-28, Stage 4's seventh task.**
`src/pyflow/engine/numerics/boundary_condition.py` implements
`DirichletBoundaryCondition`.

**Intent:** the criterion is what the *interior scheme* computes at a
boundary face, not what the condition object returns when asked. A
Expand Down Expand Up @@ -5353,7 +5367,9 @@ exception class of its own).

Neumann Boundary

**Status:** Done, 2026-08-28, Stage 4's eighth task.
**Status: Done, 2026-08-28, Stage 4's eighth task.**
`src/pyflow/engine/numerics/boundary_condition.py` implements
`NeumannBoundaryCondition`.

**Intent:** as TASK-028, for a prescribed **gradient** -- and the
zero-gradient case must not be the only one tested, since a
Expand Down Expand Up @@ -5461,7 +5477,11 @@ contract suite once `NeumannBoundaryCondition` joined it).

Periodic Boundary

**Status:** Done, 2026-08-28, Stage 4's ninth and last task.
**Status: Done, 2026-08-28, Stage 4's ninth and last task.**
Periodic bypasses `BoundaryCondition` entirely (see the Design decision
below); the real mechanism is `src/pyflow/engine/mesh.py`'s
`wrapped_neighbour_cell`, read directly by
`src/pyflow/engine/numerics/advection.py`/`diffusion.py`.

**Intent:** the claim is that a field advected once around a periodic
domain returns to its starting distribution -- a round-trip invariant,
Expand Down
Loading
Loading