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
14 changes: 14 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,20 @@ easiest to implement -- also `docs/practices.md`.

---

# Branch Discipline

**All coding work happens on a branch, never directly on `main`.**
Standing rule, 2026-09-08, maintainer's instruction.

Create a branch before the first edit, not after -- `docs/practices.md`
already names the branch-naming convention (`<kind>/<short-hyphenated-
subject>`) and the one-branch-per-task granularity; this rule is what
makes both apply every time rather than only when someone remembers to
branch first. `main` only receives commits by merge, once the Merge Gate
above is satisfied.

---

# Acceptance Criteria for Simulation Work

**From Stage 4 (`docs/planning/roadmap.md` TASK-023) onward, a task's
Expand Down
2 changes: 1 addition & 1 deletion adr/ADR-003-modular-numerical-strategies.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ anticipated situation actually arose.
under the exact same registered name (`"conjugate_gradient"`), again
with no interface change and no edit to `test_linear_solver_contract.py`'s
existing test bodies. Its own real content was the positive-semi-definite
pressure system `icds.md`'s Linear Solver ICD already named as "a real
pressure system `icds.md`'s Linear Solvers ICD already named as "a real
compatibility requirement, not an incidental detail" -- a genuine
correctness trap (naive "always project the null space out" silently
solves a different problem for a well-conditioned system) caught by a
Expand Down
27 changes: 19 additions & 8 deletions docs/architecture/icds.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ progresses ahead of this one.

---

## Time Integrator
## Time Integration

**Represents:** advancing the full simulation state forward by one
timestep, given the state and a function that computes its time
Expand Down Expand Up @@ -267,8 +267,9 @@ TASK-033 -- the outer corrector loop's own tunables, distinct from
`numerics.linear_solver_tolerance`/`numerics.linear_solver_max_iterations`,
which govern each pass's inner solve).

**Compatibility requirements:** requires a configured Linear Solver to
solve the pressure-correction equation it produces each timestep -- the
**Compatibility requirements:** requires a configured Linear Solvers
instance to solve the pressure-correction equation it produces each
timestep -- the
one real cross-layer dependency among the six (every other layer here is
independent of the others' choice).

Expand Down Expand Up @@ -356,7 +357,7 @@ measuring the Lid-Driven Cavity validation's own real runtime.

---

## Linear Solver
## Linear Solvers

**Represents:** solving the linear system Pressure–Velocity Coupling (and
any other implicit step) produces.
Expand Down Expand Up @@ -404,7 +405,7 @@ multigrid/preconditioned end of its upgrade path.

---

## Boundary Condition
## Boundary Conditions

**Represents:** how a field behaves at domain edges where no neighbouring
control volume supplies a flux.
Expand Down Expand Up @@ -558,10 +559,20 @@ Reviewed 2026-08-18 against the numerical-methods handbook, which was
written after this document and in places contradicts what it recorded.
Three ICDs changed: Advection's "unconditionally stable" became
"unconditionally bounded" (`docs/handbook/numerical-methods/fluxes.md`
explains why the distinction matters); Time Integrator's fourth-order
explains why the distinction matters); Time Integration's fourth-order
claim was scoped, since the finished solver's temporal order is capped by
upwind advection and by pressure-coupling splitting; and Linear Solver and
Boundary Condition gained the singular-pressure-system and
upwind advection and by pressure-coupling splitting; and Linear Solvers and
Boundary Conditions gained the singular-pressure-system and
global-mass-conservation compatibility requirements, both of which apply
to the MVP's own validation cases rather than to a hypothetical future
configuration.

**Checked against `planning/data/components.yaml` as of 2026-09-08:**
each of the six `##` headings above (and each component's own page under
`docs/handbook/numerical-methods/`) is now a `must_appear_in` target for
its matching entity there, so a rename here that isn't mirrored in both
places fails `make check-graph`. Wording must stay byte-identical to the
entity's `name` field -- "Time Integration", "Linear Solvers", "Boundary
Conditions" (plural), matching `engine.md`/`upgrade-paths.md` -- not the
singular forms this document used until that date, found and fixed in
the same change that added the check.
4 changes: 2 additions & 2 deletions docs/handbook/numerical-methods/boundary-conditions.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ this is closer to an interior face than either Dirichlet or Neumann: the
periodic face pair supplies each other's "neighbour" value directly, the
same way two interior cells do, just across a computational rather than
geometric gap. A periodic condition requires its paired boundary to also
be periodic (`docs/architecture/icds.md`'s Boundary Condition ICD notes
be periodic (`docs/architecture/icds.md`'s Boundary Conditions ICD notes
this explicitly) -- a periodic condition on only one side of a domain has
no physical meaning, since there would be nothing for it to be paired
with.
Expand Down Expand Up @@ -133,7 +133,7 @@ independently, rather than a single shared condition).
A real domain typically needs different condition types on different
edges simultaneously (a fixed-velocity inlet, a zero-gradient outlet, a
no-slip wall, and a periodic spanwise pair, for example, on the same
mesh) -- this is why `docs/architecture/icds.md`'s Boundary Condition ICD
mesh) -- this is why `docs/architecture/icds.md`'s Boundary Conditions ICD
is deliberately per-boundary-face rather than a single simulation-wide
choice, unlike the other five ADR-003-named components. Nothing in FVM's
treatment requires uniformity across boundaries; each boundary face's
Expand Down
2 changes: 1 addition & 1 deletion docs/handbook/numerical-methods/linear-solvers.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ here, and PyFlow's MVP choice, is iterative.
**Conjugate Gradient (CG)** is an iterative method for systems whose
matrix is **symmetric positive-definite** (SPD) -- a property the
discrete Poisson-type pressure-correction system has on PyFlow's MVP
mesh (`docs/architecture/icds.md`'s Linear Solver ICD notes this
mesh (`docs/architecture/icds.md`'s Linear Solvers ICD notes this
explicitly as a real compatibility requirement, not an incidental
detail). In exact arithmetic CG reaches the exact solution in at most as
many iterations as the system has unknowns; in practice it reaches an
Expand Down
2 changes: 1 addition & 1 deletion docs/handbook/numerical-methods/meshes.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ special treatment -- a thin wall, a material interface, or a
mesh-refinement interface between regions of different resolution.
PyFlow's MVP has only external boundaries (`docs/implementation/mvp.md`);
internal boundaries are explicitly future work
(`docs/architecture/icds.md`'s Boundary Condition ICD limitations note,
(`docs/architecture/icds.md`'s Boundary Conditions ICD limitations note,
and `upgrade-paths.md`'s Boundary Conditions entry).

## Future: Arbitrary Geometry and Adaptive Refinement
Expand Down
2 changes: 1 addition & 1 deletion docs/handbook/numerical-methods/time-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,5 +193,5 @@ RK4 and central difference above upwind, so the two paths must not be
traversed independently. And RK4's fourth-order claim is now scoped to the
ODE system it is handed, with the spatial and operator-splitting caps on
the finished solver's observed order stated;
`docs/architecture/icds.md`'s Time Integrator ICD carries the same
`docs/architecture/icds.md`'s Time Integration ICD carries the same
caveat.
9 changes: 5 additions & 4 deletions docs/planning/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -306,9 +306,10 @@ 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): **1160 tests as of 2026-09-07**, up from 1154 slightly
earlier the same day (below), then 1153, 1143, 1137, 1131, and 1052 the
day before that.
(C1a/C1b): **1163 tests as of 2026-09-08**, up from 1160 the day before
(three new `check_manifest.py` tests, `ka-name-matches-manifest`), then
1154 slightly earlier that day (below), then 1153, 1143, 1137, 1131, and
1052 the day before that.

**The 6 most recent are `pyflow resume`'s own new `--config`/`config_path`
alternative** -- a further same-day user request ("do pyflow resume
Expand Down Expand Up @@ -12354,7 +12355,7 @@ they are not the same amount of work.
This is the Unlock whose contribution to *accuracy* is least obvious,
and the question should be asked before a task is written rather than
discovered by an exit audit looking for the improvement.
`docs/architecture/icds.md`'s Linear Solver ICD records that the
`docs/architecture/icds.md`'s Linear Solvers ICD records that the
pressure-correction system PISO produces on the MVP's mesh is symmetric
positive-definite, which is precisely the case Conjugate Gradient is
already the right solver for;
Expand Down
2 changes: 1 addition & 1 deletion docs/planning/status.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ pie showData
## Live repository facts

- **47** `CLAUDE.md` files
- **1160** tests collected
- **1163** tests collected
- **144** Gherkin scenarios (`tests/features/*.feature`)

## Stages
Expand Down
26 changes: 26 additions & 0 deletions planning/data/components.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,20 @@
# entry, uses lowercase prose bullets with different wording
# ("variable arrangement" for Variables), so it is named here but not
# machine-checked -- stated rather than silently dropped.
#
# The six entities `adr/ADR-003-modular-numerical-strategies.md` names as
# independently configuration-selected (advection, diffusion,
# boundary-conditions, time-integration, linear-solvers,
# pressure-velocity-coupling) carry two *additional* `must_appear_in`
# entries beyond the nine-layer pair above: `docs/architecture/icds.md`
# (each restates the name as its own `##` ICD heading) and that
# component's own page under `docs/handbook/numerical-methods/`. This is
# a narrower, ADR-003-specific pattern -- mesh, variables and flux are not
# ADR-003 components and do not get it. Added 2026-09-08, after `icds.md`
# was found spelling three of the six differently ("Time Integrator",
# "Linear Solver", "Boundary Condition") from every other document and
# from these entities' own `name` fields -- fixed in the same change that
# added the check.

category: components

Expand Down Expand Up @@ -61,6 +75,8 @@ entities:
must_appear_in:
- docs/architecture/engine.md
- docs/implementation/upgrade-paths.md
- docs/architecture/icds.md
- docs/handbook/numerical-methods/advection.md
edges:
- type: depends_on
to: variables
Expand All @@ -78,6 +94,8 @@ entities:
must_appear_in:
- docs/architecture/engine.md
- docs/implementation/upgrade-paths.md
- docs/architecture/icds.md
- docs/handbook/numerical-methods/diffusion.md
edges:
- type: depends_on
to: variables
Expand All @@ -95,6 +113,8 @@ entities:
must_appear_in:
- docs/architecture/engine.md
- docs/implementation/upgrade-paths.md
- docs/architecture/icds.md
- docs/handbook/numerical-methods/boundary-conditions.md
edges:
- type: depends_on
to: mesh
Expand Down Expand Up @@ -141,6 +161,8 @@ entities:
must_appear_in:
- docs/architecture/engine.md
- docs/implementation/upgrade-paths.md
- docs/architecture/icds.md
- docs/handbook/numerical-methods/time-integration.md
edges:
- type: depends_on
to: variables
Expand All @@ -161,6 +183,8 @@ entities:
must_appear_in:
- docs/architecture/engine.md
- docs/implementation/upgrade-paths.md
- docs/architecture/icds.md
- docs/handbook/numerical-methods/linear-solvers.md
# No edges, and that is the contract rather than an omission: "given
# a linear system, produces its solution, independent of the
# system's origin". A dependency here would contradict engine.md.
Expand All @@ -175,6 +199,8 @@ entities:
must_appear_in:
- docs/architecture/engine.md
- docs/implementation/upgrade-paths.md
- docs/architecture/icds.md
- docs/handbook/numerical-methods/pressure-velocity-coupling.md
edges:
- type: depends_on
to: variables
Expand Down
2 changes: 1 addition & 1 deletion src/pyflow/engine/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -929,7 +929,7 @@ module with the interface the same way `FirstOrderUpwindAdvection`/
from `x0 = 0`, with one addition: null-space handling is *gated*, not
unconditional. `matrix @ ones` close to zero relative to `matrix`'s own
norm signals the constant vector is in the null space (the lid-driven
cavity's own pressure system, `icds.md`'s Linear Solver ICD); when true,
cavity's own pressure system, `icds.md`'s Linear Solvers ICD); when true,
the constant mode is projected out of the residual after every update.
**Verified before being written, not assumed:** a throwaway numerical
prototype confirmed unconditional projection reports `converged=True`
Expand Down
62 changes: 62 additions & 0 deletions tests/unit/test_check_manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,68 @@ def test_a_not_started_row_naming_a_genuinely_empty_file_is_accepted(tmp_path: P
assert check_manifest(root) == []


def test_ka_name_mismatch_between_manifest_and_ka_doc_is_reported(tmp_path: Path) -> None:
"""The one correspondence the manifest and the knowledge-architecture
document can agree or disagree about without a reader's judgement:
where both cite the same KA-NNN, do they name the same file?
"""
root = _repo(
tmp_path,
manifest="# Repository Manifest\n\n| File | Status |\n|---|---|\n"
"| bar.md | 🟨 | Something (KA-001) |\n",
files={
"bar.md": "x\n",
"foo.md": "x\n",
"docs/planning/knowledge-architecture.md": (
"# Knowledge Architecture\n\n## KA-001 — Foo\n\n**Name:** `foo.md`\n"
),
},
)

findings = _findings(root)
assert "ka-name-matches-manifest" in findings
assert "KA-001" in findings


def test_ka_name_agreement_is_not_reported(tmp_path: Path) -> None:
root = _repo(
tmp_path,
manifest="# Repository Manifest\n\n| File | Status |\n|---|---|\n"
"| foo.md | 🟨 | Something (KA-001) |\n"
"| knowledge-architecture.md | 🟨 | KA source |\n",
files={
"foo.md": "x\n",
"docs/planning/knowledge-architecture.md": (
"# Knowledge Architecture\n\n## KA-001 — Foo\n\n**Name:** `foo.md`\n"
),
},
)

assert check_manifest(root) == []


def test_a_manifest_row_citing_an_unknown_ka_is_not_reported_by_this_rule(tmp_path: Path) -> None:
"""Deliberately narrow scope, pinned so it isn't widened without
someone first seeing why it was kept this way: this rule checks
agreement, not completeness. A citation with no matching KA heading
(the document might retire an entry in prose, the same way this
manifest retires paths in prose) is a different rule's job, if it is
anyone's job at all -- not this one's.
"""
root = _repo(
tmp_path,
manifest="# Repository Manifest\n\n| File | Status |\n|---|---|\n"
"| bar.md | 🟨 | Something (KA-999) |\n"
"| knowledge-architecture.md | 🟨 | KA source |\n",
files={
"bar.md": "x\n",
"docs/planning/knowledge-architecture.md": "# Knowledge Architecture\n",
},
)

assert check_manifest(root) == []


def test_the_repositorys_own_manifest_passes() -> None:
"""A different assertion from every test above: those prove the rules
fire, this proves the real manifest satisfies them. Named so a
Expand Down
24 changes: 24 additions & 0 deletions tools/validators/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -324,3 +324,27 @@ rule. Dropping it was right, and
pins the decision so it isn't re-added without someone seeing why it
went. **Prefer three rules that always mean something to four where one
needs interpreting.**

**`ka-name-matches-manifest`, added 2026-09-08.** Checks the one
correspondence `docs/planning/knowledge-architecture.md` and
`docs/repository-manifest.md` can agree or disagree about without a
reader's judgement: where a manifest row cites `(KA-NNN)` and that id
exists as a heading in the KA document with a `**Name:**` field, do the
two name the same file (by final path segment)? It says nothing about
completeness in either direction, deliberately -- a naive "every KA-NNN
must be cited somewhere in the manifest" rule, or its reverse, would
reproduce this file's own dropped fourth rule above almost exactly:
`docs/planning/knowledge-architecture.md` retires entries in prose on
purpose (KA-034, "never created and will not be"; KA-038, a category of
files rather than one), the same way this manifest names retired paths
on purpose. Scoping to "both sides already agree the id exists" keeps
every finding a structural fact rather than a completeness judgement,
the same trade `check_graph.py`'s `must_appear_in` already makes for a
different pair of documents.

Verified against the real repository before landing (not merely
assumed clean): all 43 KA entries have exactly one `**Name:**` field, no
id repeats, every manifest citation resolves to a real KA heading, and
every citing row already agreed with its KA entry's `**Name:**` field --
the gate landed with zero findings against `docs/repository-manifest.md`
and `docs/planning/knowledge-architecture.md` as they stood that day.
Loading
Loading