- Python 3.11+
- uv — Python package and project manager
git clone <repo-url>
cd lintle
uv syncuv sync installs Python 3.11 if needed, creates a .venv/, and installs the project
plus all dev dependencies (pytest, pytest-cov, sgp4, ruff) from uv.lock.
Dependencies are declared in pyproject.toml and pinned in uv.lock (committed to git).
uv add --group dev <pkg> # Add a dev-only dependency
uv sync # Reinstall from the lock file (after a pull)The runtime's one third-party dependency is rich (>=15,<16, terminal rendering for
clean). Further additions are governed by a relaxed policy (2026-05-31): a popular,
actively-maintained library that genuinely reduces the code we'd otherwise own may be adopted
where it makes sense — gated only by the hard correctness invariants (one validator,
constant-memory streaming, byte-deterministic unstyled structured/stdout output, the
atomic-durable commit + host-aware lock, sgp4-never-at-runtime). The canonical rule and
considered/deferred table live in ARCHITECTURE.md §7.
sgp4 is a dev-only test oracle and must never be imported at runtime.
Pin every dependency >=current_major,<next_major (runtime and dev alike). Minor and patch
releases resolve automatically; major upgrades are manual, one at a time, with the verification
chain green + a changelog note (for 0.x deps the leftmost non-zero component is the major —
ruff>=0.15,<0.16). Commit the updated uv.lock with the bump. See ARCHITECTURE.md §7.
uv run lintle clean # Write cleaned output to data/output/
uv run lintle report # Re-render the last run's summary from report.jsonuv run executes a command inside the project virtual environment — no manual
activation needed.
uv run pytest # Run all tests
uv run pytest -x # Stop on first failure
uv run pytest -k "checksum" # Run tests matching an expression
uv run pytest tests/test_tle.py # Run one file
uv run pytest tests/test_tle.py::TestComputeChecksum # Run one classuv run pytest --cov=lintle --cov-report=term-missing --cov-branchThis reports line and branch coverage, listing uncovered lines in the Missing column.
Tests are grouped into Test* classes, one per unit or behaviour under test.
| File | What it covers |
|---|---|
test_tle.py |
The validator: checksum, column layout, semantic ranges, record pairing |
test_diagnostics.py |
RuleID registry, Diagnostic dataclass, RULES metadata, the diagnostic() constructor |
test_categories.py |
FixClass/FixSpec/FIXES repair-tag registry and its import-time coverage guard |
test_repair.py |
Speculative line/record repair and the rule IDs each repair tier emits |
test_pipeline.py |
Streaming I/O, line pairing, per-file processing, progress, temp-file safety |
test_report.py |
FileStats, the .broken.txt sidecar, summaries, the run report, per-NORAD breakdown |
test_cli.py |
Argument parsing, path discovery, exit codes, elapsed-time formatting |
test_diff.py |
lintle diff — per-rule delta between two runs' report.jsonl |
test_explain.py |
lintle explain — rule/fix docs, examples validated against the live validator, coverage + disjointness guards |
test_integration.py |
End-to-end: golden output, idempotence, re-validation |
test_oracle.py |
Cross-checks a known-good TLE against the trusted sgp4 parser |
test_pipeline_throughput.py |
Opt-in records/sec regression guard, gated by pytest -m slow (excluded by default) |
conftest.py holds the shared line1 / line2 fixtures — a canonical, known-good TLE.
Ruff handles both linting and formatting. Its
configuration lives in pyproject.toml under [tool.ruff] (rule sets E, F, I,
UP, B, SIM; 88-column lines).
This is a Python 3.14 codebase and uses modern idioms. The UP/SIM rule sets
auto-enforce most of them (f-strings, X | None unions, builtin generics,
contextlib.suppress, PEP 758 except A, B:). Three conventions Ruff does not
enforce — please apply them by hand so new code matches the existing style:
matchfor 3-or-more-way type/shape dispatch (notisinstance/elifchains).@dataclasses.dataclass(slots=True)on every dataclass (frozen=Truewhen immutable).collections.Counterfor tally/accumulate loops (notd[k] = d.get(k, 0) + 1); convert back withdict()at byte-deterministic output boundaries to preserve key order.
uv run ruff check . # Lint
uv run ruff check . --fix # Lint with auto-fix
uv run ruff format . # Format
uv run ruff format --check . # Check formatting (no writes)Run both before committing:
uv run ruff check . && uv run ruff format --check .Before reporting any change as done, run — and report the actual output of:
uv run pytest
uv run ruff check .
uv run ruff format --check .Never claim success without the output. If a check fails, report the failure.
Two branches, two roles:
-
developis the long-running trunk. All non-release history lives here. Two paths in, by scope:- Direct commits for chores and bugfixes (
chore:,fix:,docs:,test:,style:) — commit ondevelop, push. No branch, no PR. - Branch + PR for features and multi-file refactors
(
feature/<desc>orrefactor/<desc>) — land via rebase-and-merge sodevelopstays linear (no merge bubbles).
- Direct commits for chores and bugfixes (
-
mainis the release branch. Each release is a single merge commit onmainwhose tree is develop's release-point tree and whose second parent is develop's release-point commit. The second parent gives graph visualizers a "branched-from" edge from each release onmainback to its origin ondevelop. Usegit log --first-parent mainto see only the releases. Releases are annotated tags onmain. There is no separate release branch. Never commit directly tomain— release commits only, hand-built withgit commit-tree(see § Versioning § Release flow). -
Branch names (when branching):
feature/<desc>,refactor/<desc>— lowercase, hyphens. The release-prep branch is the documented exception:chore/release-X.Y.Z(see § Versioning § Release flow) carries the version bump + datedCHANGELOG.mdsection through a review PR. -
Use Conventional Commits:
feat:,fix:,docs:,test:,refactor:,style:,chore:. Direct commits todevelopuse these as the commit prefix; branched work uses them on individual commits inside the branch. -
Run the verification commands above before pushing any commit to
develop(direct or via PR merge). -
Land PRs to
developvia "Rebase and merge" in the GitHub UI (orgh pr merge --rebase --delete-branchlocally). Do not use "Create a merge commit" — merge bubbles fragment the visualizer into apparent multiple develop lanes. Do not use "Squash and merge" either — keep the individual commits readable ingit log develop.
Worktrees let one clone host several branches simultaneously, each in its own
directory with its own .venv/. Use one for any non-trivial feature; iterate in
one worktree while a slow test run finishes in another.
# 1. Create the worktree from develop
git worktree add .worktrees/<branch-dir> -b feature/<desc> develop
# 2. Enter and install
cd .worktrees/<branch-dir>
uv sync
# 3. Symlink the corpus so the CLI sees data/ — keeps a single ~30 GB copy on disk
ln -s ../../data data
# 4. Work, commit incrementally, then verify
uv run pytest && uv run ruff check . && uv run ruff format --check .
# 5. Push and open a PR; land via "Rebase and merge"
git push -u origin feature/<desc>
gh pr create --base develop --title "<title>" --body "<body>"
gh pr merge --rebase --delete-branch
# 6. Clean up (use -D, not -d: rebase rewrites SHAs so the local branch
# won't appear "merged" to git even after origin landed it)
git worktree remove .worktrees/<branch-dir>
git branch -D feature/<desc>Worktree directory names mirror the branch with slashes replaced by hyphens —
feature/repair-tier-2 → .worktrees/feature-repair-tier-2. The whole
.worktrees/ tree is git-ignored.
When running lintle clean from multiple worktrees in parallel, pass
--out-dir <local-dir> to each — the default data/output/ is shared through
the symlink and concurrent runs will collide.
Semantic versioning (MAJOR.MINOR.PATCH). The version lives in one place —
pyproject.toml's [project] version field — and is resolved at runtime from the
installed distribution metadata by src/lintle/__init__.py:
from importlib.metadata import PackageNotFoundError, version as _dist_version
try:
__version__ = _dist_version("lintle")
except PackageNotFoundError: # source checkout that was never installed
__version__ = "0.0.0+local"Because the lookup needs the project to be installed (even editable), keep uv sync
current — every dev workflow in this repo already does.
Release flow:
- On a
chore/release-X.Y.Zbranch offdevelop, bumpversioninpyproject.toml. - Add a new
## [X.Y.Z] - YYYY-MM-DDsection at the top ofCHANGELOG.mdwith### Added/### Changed/### Fixedsubsections (see Keep a Changelog). - Run the verification commands (
uv run pytest,uv run ruff check .,uv run ruff format --check .) and report the actual output. - Open a PR to
develop, land via "Rebase and merge" once it's green. - Build the release commit on
mainwithgit commit-tree. The tree comes fromdevelop's release-point; the parents aremain's current tip anddevelop's release-point. This is what gives the graph a visible "branched-from" edge frommaintodevelopat each release while keeping the release tree byte-identical to what gets published:To see only the release commits ongit fetch origin TREE=$(git rev-parse origin/develop^{tree}) COMMIT=$(git commit-tree "$TREE" \ -p origin/main \ -p origin/develop \ -m "Release vX.Y.Z") git update-ref refs/heads/main "$COMMIT" git checkout main git tag -a vX.Y.Z -m "Release vX.Y.Z" git push origin main vX.Y.Z
main(skipping the develop history reachable via second parents), usegit log --first-parent main. - Create the GitHub release:
gh release create vX.Y.Z --title "vX.Y.Z" --notes-from-tag --latest - Trigger the
Publishworkflow.
Nothing else needs to change — lintle --version, the report.py headers, and
any downstream from lintle import __version__ import all pick the new value up
from pyproject.toml automatically.