Contributing¶
Other groups load this package in their experiments, so the rules keep their numbers stable. Every rule is a command you can run.
Setup¶
git clone https://github.com/myersben9/fdia-graph
cd fdia-graph
pip install -e ".[generate,se,torch,dev]"
pytest -n auto tests # in parallel (pytest-xdist, in the dev and test extras);
# each worker builds its own tiny timeline and pool download
docs/ROADMAP.md is the map and the reading order.
Every diagram is a rendered image, never a Mermaid block: the README is also the PyPI page, and the
docs are read in the GitHub mobile app, and neither renders Mermaid. Each diagram's source is a
docs/figures/diagrams/<name>.mmd next to its <name>.png and <name>.svg; edit the source and
re-render with python tools/render_mermaid.py docs/figures/diagrams/<name>.mmd (Playwright with
Chromium), which writes both images, then commit all three. The class and module diagrams of
docs/reference/CLASS_MAP.md are generated: python tools/class_diagrams.py rewrites their sources from
the code (CI fails when they are stale), then render them the same way. README images use the
raw.githubusercontent.com URL so PyPI shows them; docs pages use relative paths.
The Models section of docs/reference/DATA_DICTIONARY.md is generated too: python tools/models_doc.py
rewrites it from the dataclasses in src/fdia_graph/models/ and the comment beside each field. Edit
the comment in the source, never the section, and rerun the tool; the tests fail when it is stale.
The docs site is built from docs/ plus the three root pages by mkdocs.yml: the Vercel build serves
the main branch at the custom domain, and .github/workflows/docs.yml deploys one copy per package
version to GitHub Pages with mike (a tag push deploys that version as latest, a push to main
deploys dev), so every release's docs stay readable after the next release; the version picker on
each page switches between them. python scripts/site_index.py && python -m mkdocs build --strict
checks a docs change locally.
The rules¶
| rule | means | command |
|---|---|---|
| nothing a user sees changes without a changelog entry | public API, file formats, the numbers a timeline, estimator or localizer produces; moved private names get a warning alias for one minor version | CHANGELOG.md, ## Unreleased |
| every change is proven behaviour-free, or its effect is measured | the tests pin the fewest-tamper search's answers (tests/test_search_speed.py), [WU26]'s scenarios (tests/test_wu_scenarios.py) and the formulas and models; a change that moves a generated number states the delta |
pytest -W "error::DeprecationWarning:fdia_graph" tests |
| code reads as its subject | complexity 10, nesting 3, no closure captures, at most 7 parameters, no positional record indexing; every equation a named function in formulas/ with a source key; every returned bundle a model in models/ |
python tools/readability.py --report, --check --base origin/main |
| an input is checked in one place | a new argument is a field on its consumer's model in models/config.py, with its rules in the annotation (Annotated[float, Positive()], OneOf(<Choice>)) and cross-field rules in invariants(); a dataset precondition is a ds.require(...) capability; a condition only the data reveals raises a named error from errors.py; never a raise ValueError in a function body |
python tools/readability.py --check (refuses a new one) |
| everything is typed | every parameter, return and field carries its real type; an optional dependency's type (pandapower, torch, h5py, scipy, pandas) is imported under if TYPE_CHECKING: so the import stays lazy; Any only where the value can be anything, listed in ANY_ALLOWED in tools/readability.py with the reason |
python tools/readability.py --check (refuses an unlisted Any and a stale entry), pyright in CI |
| typed, formatted, linted | pyright at zero, ruff format and check clean | pyright src/fdia_graph, ruff format --check src, ruff check src tests tools |
| generated docs match the code | the class and module diagram sources and the data dictionary's Models section | python tools/class_diagrams.py --check, python tools/models_doc.py --check |
| no measured number is typed into markdown | a harness writes what it measures through fdia_graph.results.Run into the results store (results/, see its README); a doc shows a number or a table through a results block (<!-- results: <query> ... -->, queries in results/queries.py), a CHANGELOG entry names the experiment; settings and published values from papers stay as text |
python tools/results_docs.py --write, --check; --lint lists typed decimals |
| the paper maps to the code | an equation of [WU26] is cited as [WU26 eq. 28] ([WU26 eqs. 21-23], [WU26 Alg. 1]) and one of our decisions as [D9] or [E13], in the docstring or a comment of the function that implements it and of the test that checks it (or the test is named test_wu26_eq28_...); every equation in schema.WU26_EQUATIONS has a function and a test, every decision is listed in schema.DECISIONS as the paper's reading or ours |
python tools/equation_map.py writes docs/reference/DECISIONS.md and the generated sections of FORMULAS.md and CONCEPTS_TO_CODE.md; --check in the pre-review and CI |
| speed is tracked | per-record timings against the last row from this machine, 3x tolerance | python tools/bench.py --check before a release, python tools/bench.py after |
The pull request¶

python tools/prereview.py # every gate, locally, on this checkout's source
python tools/prereview.py --also-python <path-to-python3.12> # and the suite on a second Python, as CI runs 3.9 and 3.12
python tools/pr.py create my-branch "One-line title" body.md # a draft; body: what, why, what you checked
python tools/pr.py ready 80 # once clean: the one review of every bot
python tools/pr.py wait 80 # CI plus the one review of every required bot
python tools/pr.py comments 80
python tools/pr.py reply 80 <comment-id> "what changed"
python tools/pr.py merge 80 # refuses unless green, reviewed once, every finding answered
python tools/review_ledger.py 80 # record the review's findings, print the tally by kind
Green means: every job of the smoke workflow (tests, tests (3.9), tests (windows), typecheck,
format, readability, the two installs) is a completed success on the head (a job that has not
started yet counts as not green) and every other listed check has finished without failure.
Reviewed means every required review bot has reviewed the pull request once and every one of its
findings has a reply: an inline finding a reply in its thread, and the findings a review lists in
its body ("Previously missed", outside the diff) a conversation comment on the pull request posted
after that review.
The automated review is billed per review, so each pull request gets one: the repository
ruleset's Copilot rule reviews a pull request when it is opened or marked ready, and no longer on
every push or on drafts (review_on_push and review_draft_pull_requests off). A pull request is
therefore opened as a draft while it is worked on and marked ready once it is clean; the fixes to
the review's findings go in one push, answered one by one, and the pull request merges when that
push is green, without a second review. A fix that changes behaviour beyond what its finding asked
is the exception: request one more review for it with tools/pr.py request-review. Related work goes
in one pull request per theme rather than one per step, and an open pull request is rebased only for
a real conflict. The order:
python tools/prereview.pyuntil it passes: the CI gates, the test suite, and the checks CI does not run (changelog, cited paths, vacuous tests, integer fields, rendered diagrams).- The review checklist,
docs/reference/REVIEW_CHECKLIST.md, against the diff and every touched file read whole, searching outward from every change (a/code-reviewin Claude Code with the checklist does this). Fold what it finds into the branch. - Mark the draft ready: Copilot reviews it once. Answer every finding, fix them all in one push,
repeat steps 1 and 2 on that push, and merge it on green. Record the review with
tools/review_ledger.py; a kind of finding that keeps coming up becomes a checklist item or a check intools/prereview.py. - CodeRabbit (
.coderabbit.yamlcarries the repository's review instructions) and Gemini Code Assist, both GitHub apps installed on the repository;tools/pr.pyrequires one review from a bot as soon as that bot has reviewed the pull request, so a bot that is not installed never blocks a merge and an installed one is never skipped.
Bot comments are suggestions: apply the ones that are right and answer every one with what changed
or why not. tools/pr.py uses the token the Git Credential Manager holds for git push, so it
needs no gh CLI. Commit as yourself, with a message that says what the change does. If CI does not
start on a push, check the account's Actions and Copilot credits first.
Releasing¶

A released version is never re-cut; fix forward. Data releases (timelines, pools) have their own
tags, data-v<x.y.z> from v0.8.0 (the bare v0.x.y tags belong to package versions; the two
earlier data releases keep v0.7.1 and v0.7.2), and do not move the package version. The
registry maps the short name (fg.load(..., release="v0.8.0")) to the tag, and
tools/upload_assets.py v0.8.0 <files> refuses to touch a package tag.
Where things live¶
| you want to change | look in |
|---|---|
| an attack family, the plausibility band, the meter model | engine/, formulas/attacks.py, formulas/noise.py |
| what a timeline file contains | generation/ by stage: plan.py (splits and placement), design.py (the overload Am, in parallel), emit.py (the frames), write.py (the file); generation/__init__.py (the pool and generate); timeline.py orchestrates them; then docs/reference/DATA_DICTIONARY.md |
| a generation setting | its model in models/config.py (TimelineSettings and the nested RampSettings, SearchSettings, OverloadSettings, MeterSettings, SplitSettings); generate_timeline takes every field as a flat keyword too |
| how a timeline is loaded or exported | dataset/, one concern per file |
| an estimator | se/methods.py, its algebra in formulas/estimation.py |
| a localizer | localization/methods.py or learned.py |
| the trusted-meter selectors | trust/, its algebra in formulas/trust.py |
| the federated localizers and prior | federated/, its algebra in formulas/federated.py |
| a returned record's fields | models/ |
| a formula | formulas/, and its row in docs/reference/FORMULAS.md |
The design documents behind the current layout are archived in docs/plans/.