Adding a feature to PICurv has a defined documentation shape. This page is that shape. It was not designed in the abstract: the capability-entry contract below was extracted from real entries written for the boundary-handler family and then validated against the momentum-solver family, and the sections it asks for are the ones those entries actually needed.
The framework covers three scales of change and applies the same lifecycle and concern modules at each.
| Scale | Example | What you write |
|---|---|---|
| New value in an existing family | a boundary handler, a momentum solver, a post kernel | one capability entry |
| New family in an existing subsystem | a family of preconditioners, particle-wall models | a family page, then one entry per value |
| New subsystem | immersed boundaries, a grid-generation framework, ML integration | a subsystem charter, then families beneath it |
Pick the smallest scale that fits. Most changes are the first.
A canonical value gets an entry with these eight parts, in this order. Each part is a bold run-in label, and make audit-capability checks that every one is present.
Worked examples: p44_cap_prescribed_flow (complex, nested input), p44_cap_noslip (minimal), p08_cap_newton_krylov (deep, promoted).
A deprecated alias is not a capability and does not get the full contract. It gets three parts, and the audit enforces exactly those:
p08_cap_dual_time_picard_rk4 is the worked example. Declare the alias in tests/tooling/capability_families.json with canonical: false and alias_of naming its target; the audit then requires the stub shape instead of the full one.
Anchor each entry as @anchor <prefix><value_slug> using the family's declared prefix, on its own line and outside any code block. make audit-capability uses that anchor to verify coverage, so an entry without one does not count as written - and an anchor with the family prefix that no current value claims is reported as a stale entry.
When an entry outgrows its family page, promote it to its own page and leave a stub plus a link behind. The entry keeps all eight parts; parts 5 and 8 defer to the promoted page rather than duplicating it.
p44_cap_constant_flux is the worked example: the driven-flux controller has a control law, a cadence, a restart contract, and worked validation cases, so those live on Periodic Boundaries and Driven Flows while the entry stays short and points there. Promotion is the reason a value gets its own page - not the accident of who was working on it recently.
A family page owns what the values have in common:
Boundary Conditions Guide is the reference implementation.
A new subsystem needs a charter, but not all of it at once. Requirements grow with the status the subsystem claims, so experimental work is never blocked by documentation it cannot yet honestly write.
| Status | Minimum documentation obligation |
|---|---|
planned | Purpose, intended scope, design owner, explicit not-implemented status |
internal | Scope boundary, architecture boundary, dependencies, developer entry points |
experimental | Configuration, selection guidance, observability, limitations, safe-use boundaries |
supported | Evidence, lifecycle and restart behavior, operations, troubleshooting, examples, complete reference |
deprecated | Migration path, replacement, compatibility period, removal policy |
known-defective | Defect disclosure at every selector surface, a scope record, safe-use boundaries, limitations |
removed | Changelog and history record, plus the rejection behavior |
The gate applies when a feature claims the next status, not before.
The first four statuses form a ladder and are cumulative: claiming supported owes everything below it as well, so a subsystem cannot reach the top by skipping a rung. known-defective and deprecated additionally owe the ladder up to the peak_status they reached; removed documents an absence and owes only its own row. Visibility is a separate axis: a subsystem may be experimental and public, but internal and public is a contradiction the audit rejects.
Each subsystem has a record in tests/tooling/subsystem_records.json naming its status, its previous status, the capability families it owns, and where each obligation is satisfied. make audit-subsystems fails when an obligation is undeclared, when a cited page or anchor does not resolve, when a publicly visible subsystem cites an unpublished page, when a concern falls outside the vocabulary in section 5, when a planned subsystem already owns a supported capability value, or when the declared transition is not one the lifecycle allows - a removed subsystem cannot be revived in place, and nothing that was ever built reaches removed without first being deprecated or declared defective.
planned -> removed is the single deliberate exception: a cancelled design was never built, so it owes a history record and the rejection behaviour, not a migration path for users who never had it.
An obligation is satisfied by a resolving documentation reference, by a literal value where the obligation is a fact rather than prose, or by a reasoned not-applicable declaration. A bare N/A is rejected. tests/test_subsystem_lifecycle.py holds the negative tests: each one writes a record a careless author could plausibly produce and asserts the audit refuses it.
The gate proves that the documentation a status owes exists. It cannot prove that a subsystem is suitable for production or that its results are scientifically correct, and those are what supported actually claims.
A record may therefore declare a proposed_status above the one it claims, with a promotion_rationale saying what the reviewer is being asked to confirm. Obligations are then checked against the proposal, so the writing is held to the higher bar while the published status stays conservative until a human agrees. make audit-subsystems lists every pending promotion after its summary.
A feature declares which concerns apply to it. Each activated concern adds a checklist. This is what lets the framework cover changes nobody has anticipated: there is no "ML template", but an ML integration activates external service, credentials, determinism, and artifact-topology concerns and inherits their questions.
| Concern | Activates when the feature... |
|---|---|
| Numerical method | changes discretization, a solver, or a model |
| User selector | adds or changes something selectable from YAML or CLI |
| Units and nondimensionalization | introduces a physical quantity crossing the YAML/C boundary |
| Artifact topology and storage lifecycle | reads, writes, moves, archives, restores, or deletes artifacts |
| Persistent/restart state | carries state across a checkpoint |
| Determinism and reproducibility | can change results run to run, or break restart equivalence |
| MPI/distributed execution | has rank-dependent behavior or collective calls |
| External service | talks to anything outside the process |
| Security/credentials | handles secrets or authenticated access |
| Generated artifact | produces something a downstream stage consumes |
| File format | defines or changes a durable on-disk format |
| Concurrency, permissions, destructive scope | can remove or overwrite data, or shares a location |
| Backward compatibility | changes or retires an existing contract |
| Scientific verification and validation | makes a claim about physical correctness |
Unit and scaling errors are the classic silent failure in CFD: wrong by a constant factor, plausible-looking output, no crash. Any feature introducing a physical quantity must state the units expected in YAML and where non-dimensionalization happens. 5. Non-Dimensionalization Before C Input is the worked example.
An activated concern may be declared not applicable with a stated reason. That is a real answer and is accepted as one.
make audit-docs-expansion now rejects that specific failure by signature.Any enumeration that can be extracted is extracted. The capability inventory is generated from the Python validation layer - the code that actually decides what a user may select - and never from C enums, which carry values that are not exposed end to end. The boundary system has 16 handler constants in its enum and 8 public handlers; documenting the enum would advertise eight capabilities that do not exist.
Include a family's table with the HTML include command and the generated **.html** fragment:
The .md snapshot in the same directory is a diff-friendly record, not the include target: Doxygen's plain include command inserts Markdown verbatim as a code block, so including it renders the table as unformatted text.
Use the router to join an identifier across the existing registries instead of copying another file map into prose:
The selectors are mutually exclusive. Page and contract modes preserve documentation review routing. Capability mode joins family to symbols, family page, contracts, freshness, subsystems, and evidence. Subsystem mode joins its families, obligation and concern pages, contracts, freshness, and evidence. Surface mode reverses a freshness record through watched paths and its related pages. Changed-set mode classifies staged, unstaged, and untracked nonignored paths and reports a production path not covered by a declared route.
ROUTE: complete over declared registry data (not over code behavior) is deliberately narrow. A known identifier with an unresolved declaration, or a changed production path with no declaration, returns status 3. Unknown input returns status 2 and lists valid or nearby identifiers. Missing Git metadata is unavailable, also status 3. These states prevent an empty packet from looking like successful coverage.
make docs-xref creates the optional ignored docs_build/xref.json cache from Doxygen XML. It is stamped from dirty source bytes and docs/Doxyfile; packets show direct and one-intermediate reference edges only while the stamp is current. This is navigation evidence, not a semantic call graph. Registry tables, callbacks, function pointers, macros, PETSc dispatch, and runtime-selected paths still require code tracing, and no direct edge does not prove a symbol unused. A fresh clone needs no xref cache for the base registry route.
make review-packet CAPABILITY=<family-id> and trace the nearest live sibling in the routed code; the packet is an index, not proof of behavior.make docs-inventory. The value appears in the generated table.make audit-capability. Parity and coverage must pass.make review-packet CHANGED=working-tree; investigate every unrouted production path, then run make preview-docs and read the entry as a reader would.Skipping step 5 fails step 8 - coverage is enforced for every registered family. That is the intent: the documentation obligation lands at the same moment the capability becomes selectable. Adding a value to a family that does not exist yet fails earlier still, at make audit-family-census, which refuses a public selector surface no family covers.