diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 1820687..0fbfbed 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -15,7 +15,7 @@ merge), so keep it conventional: type(scope): summary --> - [ ] `./venv/bin/ruff check .` and `./venv/bin/ruff format .` pass - [ ] `./venv/bin/pytest -q` passes - [ ] Docs updated in this PR, if this changes a config option, an importer, - or an emissions provider + or an emissions provider - [ ] Tests added or updated, if this changes behaviour - [ ] Skimmed `gh issue list` for open issues this change touches @@ -25,6 +25,7 @@ merge), so keep it conventional: type(scope): summary --> spend the time. On a PR ready for review: what you tested and what you saw. --> + diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dca62c3..41568fc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -57,23 +57,28 @@ jobs: docs: name: markdown - # `npx` fetches Prettier from the registry on every run, and a stalled - # fetch hangs rather than failing. The same line in atdr/contrail-gh has - # sat silent for exactly 301 seconds, npm's fetch-timeout expiring and the - # retry succeeding, in a job that takes twelve seconds when the fetch is - # clean. Ten minutes bounds a stall that never recovers. + # Ten minutes against a job that takes seconds. The point is a `pip` fetch + # that stalls rather than fails, which would otherwise sit until GitHub's + # six-hour default. timeout-minutes: 10 runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - # An action rather than `npx`, so the github-actions dependabot ecosystem - # already configured for this repo keeps it current. Prettier has no - # first-party action, so its pin below is a hand edit — the same deal as - # the `rev:` pins in .pre-commit-config.yaml. + # A JavaScript action, so GitHub runs it on the runner's own Node: no + # toolchain to install, nothing fetched from a registry, and the + # github-actions dependabot ecosystem already configured for this repo + # keeps the pin current. That is why there is no setup-node step here. - uses: DavidAnson/markdownlint-cli2-action@v24 - - uses: actions/setup-node@v7 + # 3.13 rather than the 3.12 the other jobs use, and not an oversight: + # .mdformat.toml sets `exclude`, which errors below 3.13. It says why. + - uses: actions/setup-python@v7 with: - node-version: "24" - # Prettier owns table alignment and whitespace; markdownlint owns the - # rules it can't express. Neither is redundant. - - run: npx prettier@3.9.6 --check "**/*.md" + python-version: "3.13" + # mdformat's pin lives in pyproject.toml's dev extra, where the pip + # dependabot ecosystem bumps it. Prettier's had to be a hand edit in this + # file, because it has no first-party action and `npx` fetched it fresh + # on every run — a fetch that sat silent for exactly 301 seconds often + # enough to dominate a job that otherwise takes twelve. + - run: pip install -e ".[dev]" + # No filter: .mdformat.toml's `exclude` covers CHANGELOG.md and the rest. + - run: mdformat --check . diff --git a/.markdownlint-cli2.yaml b/.markdownlint-cli2.yaml index 0bd0322..1e5acd6 100644 --- a/.markdownlint-cli2.yaml +++ b/.markdownlint-cli2.yaml @@ -1,12 +1,13 @@ -# Prettier owns whitespace and table alignment. These are the rules it cannot +# mdformat owns whitespace and table alignment. These are the rules it cannot # express. Kept in step by hand across contrail, contrail-gh and any instance -# created from it — the three files are copies, not a shared package. +# created from it — the files are copies, not a shared package. # -# .prettierrc.json carries two settings and no room to explain them: -# `proseWrap: preserve`, because the prose here is wrapped by hand at -# deliberate points and `always` would rewrap every paragraph in the repo; and -# `embeddedLanguageFormatting: off`, because the code inside a fence is a -# worked example or a command someone will paste, not source to be tidied. +# .mdformat.toml carries the formatter's settings and the shared ignore list, +# and explains both. Two things it does not have to say, because mdformat's +# defaults already do what this repo wants: prose is never rewrapped (`wrap` +# defaults to keep, and the prose here is wrapped by hand at deliberate +# points), and the code inside a fence is left alone unless a codeformatter +# plugin is installed, which is why pyproject.toml says never to add one. gitignore: true # Without this, a bare `markdownlint-cli2` finds nothing and reports success. diff --git a/.mdformat.toml b/.mdformat.toml new file mode 100644 index 0000000..55560d9 --- /dev/null +++ b/.mdformat.toml @@ -0,0 +1,39 @@ +# Markdown formatting. mdformat owns table padding and whitespace; +# markdownlint-cli2 owns line length and the rest, in .markdownlint-cli2.yaml. +# Neither is redundant: markdownlint has no rule that pads a table cell, and +# mdformat has no opinion on line length. +# +# Kept in step by hand across contrail, contrail-gh and any instance created +# from it — the files are copies, not a shared package. + +# Without this, every ordered list is renumbered to `1.` on the next run. +number = true + +# The successor to .prettierignore. `exclude` applies to paths passed +# explicitly, not just to a directory walk, so this one list also covers the +# files pre-commit hands over — which is why the hook carries no `exclude:` of +# its own and ci.yml needs no filter. +# +# It is also why both call sites pin Python 3.13: this key errors outright +# below it, and mdformat says so rather than ignoring it. That is a deliberate +# trade. Writing these three entries twice instead, in a shell filter and a +# pre-commit regex, would buy nothing but the ability to format Markdown on an +# interpreter nobody here runs. `requires-python` is unaffected — the matrix +# still tests 3.11. +# Unlike Prettier and markdownlint-cli2, mdformat does not read .gitignore, so +# anything ignored that holds Markdown has to be named here or a full-tree run +# walks into it. site-packages alone carries hundreds of other projects' +# READMEs. CI never has a venv; a dev machine always does. +exclude = [ + "venv/**", + ".venv/**", + # Regenerated by release-please from commit subjects. A reformat here is + # undone on the next release and can desync the manifest. + "CHANGELOG.md", + # A symlink to AGENTS.md. Formatting it writes the same file twice. + "CLAUDE.md", + # Local agent scratch, kept out of this public repo via .git/info/exclude, + # which mdformat does not read. CI never sees it; a full-tree run on a dev + # machine does. + ".claude/**", +] diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b7af228..66ac0d2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -31,9 +31,22 @@ repos: # them twice. pass_filenames: false - # The maintained fork; pre-commit/mirrors-prettier is archived. - - repo: https://github.com/rbubley/mirrors-prettier - rev: v3.9.6 + - repo: https://github.com/hukkin/mdformat + rev: 1.0.0 hooks: - - id: prettier - types_or: [markdown] + - id: mdformat + # Both are extensions, not core CommonMark. Without mdformat-gfm every + # padded table in the repo is reflowed into unpadded pipes; without + # mdformat-frontmatter a YAML block at the top of a file is rewritten + # into a horizontal rule and a heading. Keep in step with the floors in + # pyproject.toml, which explain them. + additional_dependencies: + - mdformat-gfm==1.0.0 + - mdformat-frontmatter==2.1.2 + # .mdformat.toml sets `exclude`, which errors below 3.13. Pinning the + # hook's own env means that holds whatever the developer's default + # interpreter is. + language_version: python3.13 + # No `exclude:` here on purpose. .mdformat.toml owns that list, and it + # applies to the paths pre-commit passes as well as to a directory + # walk, so a second copy here could only drift from it. diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index 9f4364a..0000000 --- a/.prettierignore +++ /dev/null @@ -1,7 +0,0 @@ -# Regenerated by release-please; see .markdownlint-cli2.yaml for why. -CHANGELOG.md -# Symlink to AGENTS.md — formatting it writes through to the same file twice. -CLAUDE.md -# Local agent scratch, excluded via .git/info/exclude, which Prettier does not -# read. See .markdownlint-cli2.yaml. -.claude/ diff --git a/.prettierrc.json b/.prettierrc.json deleted file mode 100644 index bd07196..0000000 --- a/.prettierrc.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "proseWrap": "preserve", - "embeddedLanguageFormatting": "off" -} diff --git a/AGENTS.md b/AGENTS.md index 19a4eb1..787e7ac 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,7 +7,7 @@ Model, writes a CSV. ## Commands ```bash -python3.12 -m venv venv && ./venv/bin/pip install -e ".[dev]" +python3.13 -m venv venv && ./venv/bin/pip install -e ".[dev]" ./venv/bin/pytest -q ./venv/bin/ruff check . && ./venv/bin/ruff format . TRIPIT_ICAL_URL=tests/fixtures/sample_feed.ics \ @@ -15,8 +15,11 @@ TRIPIT_ICAL_URL=tests/fixtures/sample_feed.ics \ ./venv/bin/python scripts/refresh_airline_codes.py # needs network; run by hand ``` -Python 3.11+. The default `python3` on this machine is -3.7 — use `/usr/local/bin/python3.12` explicitly. +Python 3.11+ to run, 3.13 to develop: `.mdformat.toml` sets `exclude`, which +errors below 3.13, so a 3.12 venv would put an `mdformat` on `PATH` that cannot +read its own config. The published floor is unchanged and the matrix still +tests 3.11. The default `python3` on this machine is 3.7 — use +`/usr/local/bin/python3.13` explicitly. ## Architecture @@ -49,11 +52,14 @@ Two keys, and the difference matters: - Conventional commits. release-please owns versions, tags and `CHANGELOG.md` — never hand-edit the changelog or the version in `pyproject.toml`. + - `__version__` is read from installed package metadata, so `pyproject.toml` is the single source of truth. Code that needs the version imports `__version__` rather than spelling it out — see the gotcha on version pins for the two literals that remain. + - **No test may make a real network call.** Mock `requests` in both directions. + - **Every workflow is named after its own file**, and the description goes on the job. GitHub labels a check ` / ` and never shows the filename, so `pr-title / conventional title` names both the file to open @@ -65,19 +71,24 @@ Two keys, and the difference matters: `Analyze (actions)` checks are the exception and always will be: CodeQL runs from default setup, which is a repo setting rather than a file, so its name and its timeout are GitHub's to choose. + - **Every job that can carry `timeout-minutes` sets one.** GitHub's default is six hours, and the failure that matters is a stall rather than an error: a `pip` or `npx` fetch that hangs never fails on its own. Ten minutes everywhere except `pr-title.yml`, which gets five. A job whose only key is `uses:` cannot carry it, which is why the setting lives inside a reusable workflow rather than on its callers. -- **Markdown is formatted, not hand-aligned.** Prettier owns table padding and + +- **Markdown is formatted, not hand-aligned.** mdformat owns table padding and whitespace; markdownlint-cli2 owns line length and the rest. Run - `npx prettier@3.9.6 --write "**/*.md"` rather than lining a table up by - hand. Prose wraps at 80, except `README.md`, which wraps at 100 and says so - in a `markdownlint-configure-file` comment at its foot. + `./venv/bin/mdformat .` rather than lining a table up by hand. Prose wraps at + 80, except `README.md`, which wraps at 100 and says so in a + `markdownlint-configure-file` comment at its foot — a per-file override only + markdownlint honours, and one reason it is still here. + - This repo is public. Never commit a real CSV, a raw log, or `config.json` — all are gitignored. + - **When updating docs at the end of a change, skim the open issues** (`gh issue list`) for any the change touched. Cheap, and it catches both directions: an issue quietly fixed, and one made easier to hit. @@ -150,17 +161,17 @@ Two keys, and the difference matters: drifted before. A _new_ literal version anywhere else is a bug: import `__version__` in code, or wire the file into `extra-files` if it's not code. - **release-please needs the repo setting "Allow GitHub Actions to create and - approve pull requests"** (Settings → Actions → General). `permissions: -pull-requests: write` in the workflow is _not_ sufficient on its own, and the - API can report the flag as enabled while it is still blocked. Without it the - release job fails with "GitHub Actions is not permitted to create or approve - pull requests". + approve pull requests"** (Settings → Actions → General). Granting + `pull-requests: write` in the workflow is _not_ sufficient on its own, and + the API can report the flag as enabled while it is still blocked. Without it + the release job fails with "GitHub Actions is not permitted to create or + approve pull requests". - **`CHANGELOG.md` and `CLAUDE.md` are excluded from both Markdown tools.** release-please regenerates the changelog from commit subjects, so a reformat there is undone on the next release and can desync the manifest; `CLAUDE.md` is a symlink to `AGENTS.md`, so linting it reports every line twice and formatting it writes the same file twice. Both are named in - `.markdownlint-cli2.yaml` and `.prettierignore`. + `.markdownlint-cli2.yaml` and `.mdformat.toml`. - **`cli._now()` exists to be monkeypatched.** Tests that use the real clock rot once the fixture's dates fall into the past. - **`src/contrail/data/airline_codes.csv` is generated, never hand-edited.** Fix diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index f6468da..ee5e23f 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -121,14 +121,14 @@ version 2.1, available at [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. Community Impact Guidelines were inspired by -[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. +[Mozilla's code of conduct enforcement ladder][mozilla coc]. For answers to common questions about this code of conduct, see the FAQ at -[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at +[https://www.contributor-covenant.org/faq][faq]. Translations are available at [https://www.contributor-covenant.org/translations][translations]. +[faq]: https://www.contributor-covenant.org/faq [homepage]: https://www.contributor-covenant.org -[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html -[Mozilla CoC]: https://github.com/mozilla/diversity -[FAQ]: https://www.contributor-covenant.org/faq +[mozilla coc]: https://github.com/mozilla/diversity [translations]: https://www.contributor-covenant.org/translations +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html diff --git a/README.md b/README.md index eebbc65..482ac89 100644 --- a/README.md +++ b/README.md @@ -531,6 +531,7 @@ changelog are handled by release-please. MIT — see [LICENSE](LICENSE). + diff --git a/docs/contrail-gh.md b/docs/contrail-gh.md index 24a5a97..fb62f89 100644 --- a/docs/contrail-gh.md +++ b/docs/contrail-gh.md @@ -163,11 +163,11 @@ hand, so nothing else would catch it. ## The Markdown config is duplicated, not shared -`.markdownlint-cli2.yaml`, `.prettierrc.json` and `.prettierignore` exist in this -repo, in the template, and in every instance created from it. They are copies: -three separate repositories can't share a config file, and neither tool reads one -from a package. Change a rule here and the same edit has to be made in the -template, or the two repos start disagreeing about what correct Markdown is. +`.markdownlint-cli2.yaml` and `.mdformat.toml` exist in this repo, in the +template, and in every instance created from it. They are copies: three separate +repositories can't share a config file, and neither tool reads one from a +package. Change a rule here and the same edit has to be made in the template, or +the two repos start disagreeing about what correct Markdown is. The template's copies ship to instances through "Use this template", so its `README.md` names them in the recipe for pulling template updates — the workflow diff --git a/docs/resync.md b/docs/resync.md index 0e3f757..61efb0c 100644 --- a/docs/resync.md +++ b/docs/resync.md @@ -110,6 +110,7 @@ exactly. The cost of the clash is double counting, not a wrong figure. every stored row changed on the first sync after an upgrade — and `changed` is what bypasses the no-downgrade guard, which would replace a whole file of exact figures with route averages and then freeze them that way. + - **Only a source that returned something may cancel its own rows.** With several sources configured, one silently empty source would otherwise cancel every flight it owns while the others kept a global guard happy. Granularity is the @@ -126,6 +127,7 @@ exactly. The cost of the clash is double counting, not a wrong figure. - **A feed yielding no flights at all refuses to cancel anything** and exits non-zero. `--dry-run` is exempt: it can neither write nor cancel. + - **The file is written only when content actually changed.** Re-pricing runs unconditionally, so `_merge_row` keeps the original `sync_timestamp` on a no-op and `cmd_sync` compares against the rows as loaded. Otherwise contrail-gh would diff --git a/pyproject.toml b/pyproject.toml index 9027833..0e7343a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,6 +46,25 @@ dev = [ # tools. Optional to use — CI is still the gate — but declared here so # `pip install -e ".[dev]"` leaves nothing to install by hand. "pre-commit>=4.6.2", + # Formats Markdown; markdownlint-cli2 checks what it cannot express. Both + # `docs` in ci.yml and the pre-commit hook run 3.13, because .mdformat.toml + # sets `exclude` and that key errors below it — see the comment there. + # + # Never add an mdformat *codeformatter* plugin (mdformat-black and the + # like). mdformat enables every installed one by default, which would + # reformat the code inside fences — worked examples and commands people + # paste, not source to tidy. Prettier needed embeddedLanguageFormatting + # off for the same reason. + "mdformat>=1.0.0", + # Tables. Core CommonMark has none, so without this every padded table is + # reflowed into unpadded pipes. + "mdformat-gfm>=1.0.0", + # YAML frontmatter. mdformat does not recognise it otherwise and rewrites + # the block into a horizontal rule and a heading — it did exactly that to a + # skill file in contrail-gh. Nothing tracked here carries frontmatter + # today, which is why this is a guard rather than a fix: the failure is + # silent, and the first .md that needs it would not announce itself. + "mdformat-frontmatter>=2.1.2", # Self-reference rather than repeating the pyyaml floor: declared once, so # the two can't drift, and Dependabot sees pyyaml for what it is — a runtime # dependency of the `yaml` extra, not a dev tool.