From 06c34e3bcd5353fd45f0d03ebeb45022fa0e40ae Mon Sep 17 00:00:00 2001 From: Ola Skavhaug Date: Sun, 27 Sep 2026 09:45:36 +0200 Subject: [PATCH 01/13] =?UTF-8?q?CLAUDE.md=20=C2=A72:=20lines=20count=20as?= =?UTF-8?q?=20written;=20hand-packing=20allowed=20with=20a=20reason=20(Ola?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index afba060..f68162a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,8 +16,11 @@ This project is governed by specialized sub-agents. Always defer tasks to the co * **Strict Size Limit:** Under **700 lines of production code per pull request**, where a line counts unless it is a comment, a docstring, or the body of a raw literal; tests excluded. The exclusions exist so the ceiling does not penalise - the comment density this project asks for. This is the only statement of the - rule; everywhere else points here. + the comment density this project asks for. Lines count as written: packing + code by hand under `# fmt: skip` / `# fmt: off` is allowed, provided the + packed lines stay readable and the review says why each new region is + packed (Ola, 2026-09-27, on `tools/bench.py`). This is the only statement of + the rule; everywhere else points here. * **Prohibited Dependencies:** Never introduce `CGAL`, `GDAL`, `OGR`, `Fiona`, `Rasterio` (it wraps GDAL), or external `date` libraries. Enforced by `tools/check_prohibited_deps.py` over imports, includes, declared From 48cb90fcb509483bda3d29f5ff327e72601a8726 Mon Sep 17 00:00:00 2001 From: Ola Skavhaug Date: Sun, 27 Sep 2026 09:49:03 +0200 Subject: [PATCH 02/13] red: bench.py threshold boundary and clean exits Two follow-ups from @reviewer's round on tools/bench.py, approved by Ola (2026-09-27), pinned in bench-py.md's "Pinned by the red suite": - Threshold boundary: exactly 5 % (20.0 -> 21.0) is ACCEPTED, 5.5 % is a REGRESSION; same at 7.5. Red today: 21.0 / 20.0 - 1 rounds above 0.05, so the ratio test calls exactly 5 % a regression. - Clean exits: a missing --dem or --domain, and a malformed run.json the caller named (compare NEW_DIR, compare/run --baseline), exit 3 with the file named and no evidence; run refuses before building. A malformed run.json met in the baseline search is skipped with a UserWarning naming it (settled here, reasons in bench-py.md). Co-Authored-By: Claude Opus 5.5 --- docs/benchmarks/bench-py.md | 28 +++++ tests/python/test_bench.py | 207 ++++++++++++++++++++++++++++++++++++ 2 files changed, 235 insertions(+) diff --git a/docs/benchmarks/bench-py.md b/docs/benchmarks/bench-py.md index a59d0ea..8221327 100644 --- a/docs/benchmarks/bench-py.md +++ b/docs/benchmarks/bench-py.md @@ -294,6 +294,34 @@ whose generated part ends at the line `MARKER`; whatever follows that line in an existing README is kept on a rerun. The README names `2.2x` beside the ceiling, every verdict line, and `--accept-quality` when it was used. +**The threshold boundary** (follow-up red, 2026-09-27). "Strictly more than +the threshold" is decided without a rounded ratio: a median exactly +`threshold_pct` above the baseline's is `ACCEPTED`. With medians 20.0 and 21.0 +the ratio `21.0 / 20.0 - 1` rounds to 0.050000000000000044, so a +`pct > threshold` test called exactly 5 % a regression; comparing exact +products (`new * 100 > base * (100 + threshold)`) does not. 5.5 % at the +default and 8 % at 7.5 are regressions with their size. + +**Bad input exits 3, not a traceback** (follow-up red, 2026-09-27). Exit 3 +through `typer.Exit`, one line naming the offending file, no evidence written: + +- `run` with a `--dem` or `--domain` path that does not exist is refused + **before the build and before any child**, so a typo is not found out after + the measurement it would spoil. +- A `run.json` that does not load as a `RunRecord` (truncated, empty, or valid + JSON from another schema) and that the caller **named** is refused: + `compare NEW_DIR` (also when `NEW_DIR/run.json` is missing), `compare + --baseline DIR`, and `run --baseline DIR`, the last before the build and any + child. +- *Settled here:* a malformed `run.json` met during the **baseline search** + (`find_baseline`, from `run` or `compare` without `--baseline`) is + **skipped with a `UserWarning` naming its path**, and the search goes on. + Not an error, because `run` searches after measuring: an error there would + throw away the measurement over a file unrelated to it, and a `RunRecord` + that gains a required field would make every older record invalid and every + run fail. Not silent, because a skipped record can change which baseline is + chosen, and the warning is how the reader of the verdict learns that. + ## Settled at green (@perf, 2026-09-27) **Where the Release `.so` lands and how `pkg/` is assembled** (the red suite diff --git a/tests/python/test_bench.py b/tests/python/test_bench.py index f882186..a707d67 100644 --- a/tests/python/test_bench.py +++ b/tests/python/test_bench.py @@ -556,6 +556,37 @@ def test_threshold_lowered_rejects_four_percent(bench: ModuleType) -> None: assert v.lines[0].endswith("(+4.0 %)") +@pytest.mark.parametrize( + ("threshold", "base", "new", "status", "size"), + [ + (5.0, 20.0, 21.0, "ACCEPTED", None), + (5.0, 20.0, 21.1, "REGRESSION", "(+5.5 %)"), + (7.5, 40.0, 43.0, "ACCEPTED", None), + (7.5, 40.0, 43.2, "REGRESSION", "(+8.0 %)"), + ], + ids=["exactly-5-accepted", "5.5-regression", "exactly-7.5-accepted", "8-regression"], +) +def test_time_regression_is_strictly_more_than_the_threshold( + bench: ModuleType, threshold: float, base: float, new: float, status: str, size: str | None +) -> None: + """A median exactly `threshold` percent slower is accepted, anything above is not. + + The medians are exact in binary and so are the products `new * 100` and + `base * (100 + threshold)`, so "exactly 5 %" is exact in the reals. The + ratio `21.0 / 20.0 - 1` is not: it rounds to 0.050000000000000044, which a + `pct > threshold` test reads as more than 5 %. The boundary has to be + decided without that rounding. + """ + base_record = make(bench, with_median(record_dict(), "tile", 0, base)) + new_record = make(bench, with_median(record_dict(), "tile", 0, new), threshold_pct=threshold) + v = bench.verdict(new_record, base_record) + assert v.status == status, v.lines + if size is None: + assert not any(line.startswith("REGRESSION") for line in v.lines) + else: + assert v.lines == [f"REGRESSION: tile refine_s[t=0] {base:.4f} -> {new:.4f} {size}"] + + def test_faster_is_accepted(bench: ModuleType) -> None: assert bench.verdict(make(bench, slower(0.5)), make(bench)).status == "ACCEPTED" @@ -675,6 +706,50 @@ def test_find_baseline_never_returns_the_record_itself(bench: ModuleType, tmp_pa assert bench.find_baseline(tmp_path, record, lambda a, b: True) is None +# A stored run.json that is not a RunRecord: truncated, empty, and valid JSON +# from a schema that is not this one (the realistic case: RunRecord gains a +# required field and every older record stops validating). +MALFORMED_RUN_JSON = [ + '{"label": "i21", "started": ', + "", + json.dumps({"label": "i21", "started": "2026-09-20T02:00:00+00:00"}), +] +MALFORMED_IDS = ["truncated", "empty", "schema-drift"] + + +def store_malformed(root: Path, text: str, date: str, label: str) -> Path: + directory = root / date / label + directory.mkdir(parents=True) + (directory / "run.json").write_text(text) + return directory + + +@pytest.mark.parametrize("text", MALFORMED_RUN_JSON, ids=MALFORMED_IDS) +def test_find_baseline_skips_a_malformed_run_json_with_a_warning_naming_it( + bench: ModuleType, tmp_path: Path, text: str +) -> None: + """Pinned in bench-py.md: skip with a warning, never abort the search. + + The file sorts after the valid baseline, so an implementation that stops at + the first bad file does not find the valid one either. + """ + valid = store(tmp_path, make(bench, started="2026-09-20T01:00:00+00:00"), "2026-09-20", "good") + store_malformed(tmp_path, text, "2026-09-20", "zz-broken") + new = make(bench, started="2026-09-27T10:00:00+00:00") + with pytest.warns(UserWarning, match="zz-broken"): + found = bench.find_baseline(tmp_path, new, lambda a, b: True) + assert found is not None + assert found[0] == valid + + +def test_find_baseline_with_only_a_malformed_run_json_is_none( + bench: ModuleType, tmp_path: Path +) -> None: + store_malformed(tmp_path, "{", "2026-09-20", "broken") + with pytest.warns(UserWarning, match="broken"): + assert bench.find_baseline(tmp_path, make(bench), lambda a, b: True) is None + + # --------------------------------------------------------------- evidence @@ -1013,6 +1088,138 @@ def test_compare_rejudges_stored_evidence( assert status in result.output +# ------------------------------------------------- clean exits, not tracebacks +# +# bench-py.md: a refused or failed run exits 3 and writes no evidence. These pin +# that bad input is refused the same way: exit 3 through `typer.Exit` (so +# `result.exception` is the `SystemExit`, not an escaped `FileNotFoundError` or +# `ValidationError`), a message naming the offending file, and, for `run`, +# refused before any build or child, so a missing input is not found out only +# after the measurement it spoils. + + +def assert_refused(result: Any, names: str) -> None: + assert result.exit_code == 3, result.output + assert isinstance(result.exception, SystemExit), repr(result.exception) + assert "Traceback" not in result.output + # The name only: the printed path may be resolved (/private/var vs /var) + # and a long line may be wrapped. + assert names in "".join(result.output.split()), result.output + + +def assert_nothing_started(fake: FakeRunner, ws: dict[str, Path]) -> None: + assert fake.children() == [] + assert not any(Path(c[0]).name == "cmake" for c in fake.calls) + assert list(ws["out_root"].rglob("run.json")) == [] + + +def test_run_refuses_a_missing_dem_before_building( + bench: ModuleType, workspace: dict[str, Path], use_runner: Callable[[Any], None] +) -> None: + fake = FakeRunner(bench) + use_runner(fake) + workspace["dem"] = workspace["dem"].with_name("missing-dem.tif") + result = cli.invoke(bench.app, run_args(workspace, "t1")) # a build is asked for + assert_refused(result, "missing-dem.tif") + assert_nothing_started(fake, workspace) + + +def test_run_refuses_a_missing_domain_before_building( + bench: ModuleType, workspace: dict[str, Path], use_runner: Callable[[Any], None] +) -> None: + fake = FakeRunner(bench) + use_runner(fake) + nowhere = workspace["tree"] / "nowhere.geojson" + args = [*run_args(workspace, "t1"), "--domain", str(nowhere)] # after --domain tile + result = cli.invoke(bench.app, args) + assert_refused(result, "nowhere.geojson") + assert_nothing_started(fake, workspace) + + +@pytest.mark.parametrize("text", MALFORMED_RUN_JSON, ids=MALFORMED_IDS) +def test_run_refuses_a_malformed_named_baseline_before_measuring( + bench: ModuleType, workspace: dict[str, Path], use_runner: Callable[[Any], None], text: str +) -> None: + """A `--baseline` the caller named is not skipped: it is refused, and + before the measurement, which would otherwise be lost with it.""" + fake = FakeRunner(bench) + use_runner(fake) + named = store_malformed(workspace["tree"], text, "stored", "bad-baseline") + result = cli.invoke(bench.app, [*run_args(workspace, "t1"), "--baseline", str(named)]) + assert_refused(result, "bad-baseline") + assert_nothing_started(fake, workspace) + + +def test_run_skips_a_malformed_stored_run_and_keeps_its_evidence( + bench: ModuleType, workspace: dict[str, Path], use_runner: Callable[[Any], None] +) -> None: + use_runner(FakeRunner(bench, refine_s=1.0)) + first = cli.invoke(bench.app, [*run_args(workspace, "base"), "--no-build"]) + assert first.exit_code == 2, first.output + (date_dir,) = workspace["out_root"].iterdir() + store_malformed(workspace["out_root"], "{", date_dir.name, "zz-broken") + + use_runner(FakeRunner(bench, refine_s=1.0)) + with pytest.warns(UserWarning, match="zz-broken"): + second = cli.invoke(bench.app, [*run_args(workspace, "new"), "--no-build"]) + assert second.exit_code == 0, second.output + assert "ACCEPTED" in second.output + assert run_json(workspace, "new")["verdict"][0] == "ACCEPTED" + + +@pytest.fixture +def stored_pair(bench: ModuleType, tmp_path: Path) -> tuple[Path, Path]: + base_dir, new_dir = tmp_path / "base", tmp_path / "new" + base_dir.mkdir() + new_dir.mkdir() + bench.write_evidence(make(bench), base_dir) + bench.write_evidence(make(bench, started="2026-09-27T12:00:00+00:00"), new_dir) + return base_dir, new_dir + + +@pytest.mark.parametrize("text", MALFORMED_RUN_JSON, ids=MALFORMED_IDS) +def test_compare_refuses_a_malformed_new_run( + bench: ModuleType, stored_pair: tuple[Path, Path], text: str +) -> None: + base_dir, new_dir = stored_pair + (new_dir / "run.json").write_text(text) + result = cli.invoke(bench.app, ["compare", str(new_dir), "--baseline", str(base_dir)]) + assert_refused(result, "new/run.json") + + +def test_compare_refuses_a_directory_without_run_json( + bench: ModuleType, stored_pair: tuple[Path, Path] +) -> None: + base_dir, new_dir = stored_pair + (new_dir / "run.json").unlink() + result = cli.invoke(bench.app, ["compare", str(new_dir), "--baseline", str(base_dir)]) + assert_refused(result, "new/run.json") + + +@pytest.mark.parametrize("text", MALFORMED_RUN_JSON, ids=MALFORMED_IDS) +def test_compare_refuses_a_malformed_named_baseline( + bench: ModuleType, stored_pair: tuple[Path, Path], text: str +) -> None: + base_dir, new_dir = stored_pair + (base_dir / "run.json").write_text(text) + result = cli.invoke(bench.app, ["compare", str(new_dir), "--baseline", str(base_dir)]) + assert_refused(result, "base/run.json") + + +def test_compare_search_skips_a_malformed_stored_run( + bench: ModuleType, tmp_path: Path, use_runner: Callable[[Any], None] +) -> None: + root = tmp_path / "evidence" + store(root, make(bench), "2026-09-27", "base") + store_malformed(root, "{", "2026-09-27", "zz-broken") + new_dir = store(root, make(bench, started="2026-09-27T12:00:00+00:00"), "2026-09-28", "new") + use_runner(FakeRunner(bench)) # git merge-base --is-ancestor: yes + with pytest.warns(UserWarning, match="zz-broken"): + result = cli.invoke(bench.app, ["compare", str(new_dir), "--out-root", str(root)]) + assert result.exit_code == 0, result.output + assert "ACCEPTED" in result.output + + # ------------------------------------------------------- one real child run From c854edd321f5d6bdbb453c9569fa8264324735d4 Mon Sep 17 00:00:00 2001 From: Ola Skavhaug Date: Sun, 27 Sep 2026 09:51:50 +0200 Subject: [PATCH 03/13] green: bench.py threshold boundary and clean exits - verdict: a time regression is `new * 100 > base * (100 + threshold)`, exact products instead of the rounded ratio, so exactly 5 % is ACCEPTED. - _load raises BenchError naming the run.json it could not read or validate; run refuses a missing --dem/--domain and a bad --baseline before the build and any child, compare refuses a bad NEW_DIR or --baseline; both exit 3 through typer.Exit. - find_baseline skips a malformed run.json with a UserWarning naming it. - bench-py.md "Size": the 700-line ceiling is per PR (lines the PR adds or changes), replacing the "about 10 lines of room" misreading. Co-Authored-By: Claude Opus 5.5 --- docs/benchmarks/bench-py.md | 5 ++++- tools/bench.py | 37 ++++++++++++++++++++++++++++++------- 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/docs/benchmarks/bench-py.md b/docs/benchmarks/bench-py.md index 8221327..891500e 100644 --- a/docs/benchmarks/bench-py.md +++ b/docs/benchmarks/bench-py.md @@ -177,7 +177,10 @@ lines in, comments and docstrings out; 688 to 691 depending on whether the two `# fmt: off/on` lines count), 582 without blank lines, so the estimate was low by about 80 lines. The margin rests on 16 regions packed by hand under `# fmt: skip` / `# fmt: off`; formatted normally it is about 780 counted lines -(827 raw). The next change to `tools/bench.py` has about 10 lines of room. +(827 raw). That total was the first PR's count, because that PR added the +whole file. The ceiling is per pull request: a later PR counts the lines of +`tools/bench.py` it adds or changes (`git diff` against its base), not the +file's total, so the file's size does not cap the next change. ## Ruled by Ola (2026-09-27) diff --git a/tools/bench.py b/tools/bench.py index dc819f7..bbc21c2 100644 --- a/tools/bench.py +++ b/tools/bench.py @@ -26,6 +26,7 @@ import sys import tempfile import time +import warnings from collections.abc import Callable, Mapping, Sequence from datetime import datetime from fractions import Fraction @@ -378,7 +379,11 @@ def find_baseline( it, and whose commit is an ancestor of ``record``'s.""" found: tuple[Path, RunRecord] | None = None for path in sorted(root.glob("*/*/run.json")): - candidate = RunRecord.model_validate_json(path.read_text()) + try: + candidate = _load(path.parent) + except BenchError as exc: # skipped, not fatal: bench-py.md "Bad input exits 3" + warnings.warn(f"skipped {exc}", UserWarning, stacklevel=2) + continue if candidate.started >= record.started or comparable(record, candidate) is not None: continue if found is not None and candidate.started <= found[1].started: @@ -399,7 +404,8 @@ def verdict(new: RunRecord, base: RunRecord | None) -> Verdict: if old is None or old <= 0: continue pct = (s.median / old - 1.0) * 100.0 - if pct > new.threshold_pct: + # Exact products, not the rounded ratio: exactly the threshold is accepted. + if s.median * 100.0 > old * (100.0 + new.threshold_pct): where = f"{s.domain} refine_s[t={s.threads}]" lines.append(f"REGRESSION: {where} {old:.4f} -> {s.median:.4f} (+{pct:.1f} %)") for name, q in new.quality.items(): @@ -551,14 +557,18 @@ def _machine(runner: Runner) -> Machine: def _load(directory: Path) -> RunRecord: - return RunRecord.model_validate_json((directory / "run.json").read_text()) + path = directory / "run.json" + try: + return RunRecord.model_validate_json(path.read_text()) + except (OSError, ValueError) as exc: # pydantic's ValidationError is a ValueError + raise BenchError(f"{path}: not a stored run: {str(exc).splitlines()[0]}") from exc def _judge( - runner: Runner, record: RunRecord, baseline: Path | None, root: Path, tree: Path + runner: Runner, record: RunRecord, baseline: RunRecord | None, root: Path, tree: Path ) -> Verdict: if baseline is not None: - return verdict(record, _load(baseline)) + return verdict(record, baseline) git = ["git", "-C", str(tree), "merge-base", "--is-ancestor"] found = find_baseline(root, record, lambda a, d: runner.run([*git, a, d]).returncode == 0) return verdict(record, found[1] if found else None) @@ -633,6 +643,14 @@ def run( tree, dem = tree.resolve(), dem.resolve() counts = [int(t) for t in threads.split(",") if t.strip()] names = domain or ["tile", str(DEFAULT_QUARTER)] + try: # refuse bad input before the build and any child + for path in [dem, *(Path(n) for n in names if n != "tile")]: + if not path.exists(): + raise BenchError(f"{path}: no such file") + base = _load(baseline) if baseline is not None else None + except BenchError as exc: + typer.echo(f"bench: {exc}") + raise typer.Exit(3) from exc domains = [ Domain(name="tile", path=None, sha256=None) if n == "tile" else Domain(name=Path(n).stem, path=str(Path(n).resolve()), sha256=_sha256(Path(n))) @@ -669,7 +687,7 @@ def run( samples=samples, stats=median_stats(samples), quality=qualities, accept_quality=accept_quality, threshold_pct=threshold, ) # fmt: skip - v = _judge(runner, record, baseline, out_root, tree) + v = _judge(runner, record, base, out_root, tree) record.verdict = v.lines directory = out_root / started.date().isoformat() / label write_evidence(record, directory) @@ -685,7 +703,12 @@ def compare( out_root: OutRoot = REPO / "docs/benchmarks", ) -> None: """Re-judge stored evidence with the threshold and waiver it carries.""" - v = _judge(make_runner(), _load(new_dir), baseline, out_root, REPO) + try: + new, base = _load(new_dir), (_load(baseline) if baseline is not None else None) + except BenchError as exc: + typer.echo(f"bench: {exc}") + raise typer.Exit(3) from exc + v = _judge(make_runner(), new, base, out_root, REPO) typer.echo("\n".join(v.lines)) raise typer.Exit(v.exit_code) From 99993f64d50284c7a7dbd31fb38ced43e942cc20 Mon Sep 17 00:00:00 2001 From: Ola Skavhaug Date: Sun, 27 Sep 2026 09:53:30 +0200 Subject: [PATCH 04/13] ruff: pin to 0.16.x; format excludes docs/, whose code blocks are cited quotations Co-Authored-By: Claude Opus 5.5 --- pyproject.toml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c7dd56d..b0b378d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,7 +65,7 @@ dev = [ "pytest-cov>=4.1", "hypothesis>=6.100", "mypy>=1.10", - "ruff>=0.5", + "ruff~=0.16.8", # pinned: `ruff format --check` gates CI; style shifts per minor ] [project.scripts] @@ -112,6 +112,11 @@ src = ["src_python", "tests/python"] # as evidence. The maintained benchmark is tools/bench.py (@perf), checked by mypy below. exclude = ["legacy", "docs/benchmarks"] +[tool.ruff.format] +# docs/ holds design records whose code blocks are quotations, cited by line; +# the formatter would rewrite Python inside Markdown and break both. +exclude = ["docs/**"] + [tool.ruff.lint] select = ["E", "F", "I", "N", "UP", "B", "SIM", "RUF", "ASYNC"] ignore = [] From 41db2ab24ab76c8e2f7abb5f98ddae174bf4765b Mon Sep 17 00:00:00 2001 From: Ola Skavhaug Date: Sun, 27 Sep 2026 09:53:30 +0200 Subject: [PATCH 05/13] ruff format: mechanical reformat of the tree, no behaviour change Layout only, produced by `ruff format .` (0.16.8). Listed in .git-blame-ignore-revs. Co-Authored-By: Claude Opus 5.5 --- src_python/tin_engine/cli.py | 32 ++++-- src_python/tin_engine/io/ply.py | 8 +- src_python/tin_engine/viz/fixtures.py | 86 ++++++++++++---- src_python/tin_engine/viz/scene.py | 3 +- src_python/tin_engine/viz/svg.py | 9 +- tests/python/plyread.py | 6 +- tests/python/test_cli_constraint_feet.py | 38 +++++-- tests/python/test_cli_draw.py | 62 +++++------ tests/python/test_cli_mesh.py | 44 ++++++-- tests/python/test_cli_mesh_dem.py | 4 +- tests/python/test_cli_mesh_stats.py | 126 ++++++++++++++++++----- tests/python/test_cli_start_quality.py | 77 +++++++++++--- tests/python/test_core_cdt.py | 16 +-- tests/python/test_core_noding.py | 50 +++------ tests/python/test_elevation.py | 4 +- tests/python/test_features.py | 3 +- tests/python/test_grid_domain.py | 8 +- tests/python/test_io_ply.py | 8 +- tests/python/test_io_vtk_legacy.py | 1 - tests/python/test_refine_golden.py | 7 +- tests/python/test_session_state.py | 22 ++-- tests/python/test_stats.py | 1 - tests/python/test_viz_scene.py | 12 +-- tests/python/test_viz_svg.py | 100 ++++++++---------- tools/check_citations.py | 12 +-- tools/check_prohibited_deps.py | 58 +++++++---- tools/session_state.py | 4 +- 27 files changed, 482 insertions(+), 319 deletions(-) diff --git a/src_python/tin_engine/cli.py b/src_python/tin_engine/cli.py index b295351..5e2afbb 100644 --- a/src_python/tin_engine/cli.py +++ b/src_python/tin_engine/cli.py @@ -700,7 +700,14 @@ def mesh( ) label = dem.stem dem_run = _dem_mesh( - dem, stride, delaunay, snap_spacing, tolerance, clock, domain, domain_crs, + dem, + stride, + delaunay, + snap_spacing, + tolerance, + clock, + domain, + domain_crs, DEFAULT_START_MIN_ANGLE if start_min_angle is None else start_min_angle, not no_constraint_feet, ) @@ -973,7 +980,12 @@ def _dem_mesh( else: t0 = time.perf_counter_ns() out = refine( - to_core(tile), run.mesh, edges, masks, tolerance=tolerance, min_angle_deg=min_angle, + to_core(tile), + run.mesh, + edges, + masks, + tolerance=tolerance, + min_angle_deg=min_angle, constraint_feet=feet, ) _refine_phases(clock, (time.perf_counter_ns() - t0) / 1e9, out) @@ -989,11 +1001,19 @@ def _dem_mesh( valid=out.valid, ) refinement = Refinement( - tolerance, out.max_error, out.rounds, out.inserted, out.flips, out.uncovered, - out.carved, out.quality_inserted, out.quality_skipped, out.feet, + tolerance, + out.max_error, + out.rounds, + out.inserted, + out.flips, + out.uncovered, + out.carved, + out.quality_inserted, + out.quality_skipped, + out.feet, ) - quality_start = f"start min angle {_exact(min_angle)} deg" if min_angle > 0 else ( - "start quality off" + quality_start = ( + f"start min angle {_exact(min_angle)} deg" if min_angle > 0 else ("start quality off") ) sentence = ( f"refined from DEM nodes, constrained Delaunay, tolerance {_exact(tolerance)} m, " diff --git a/src_python/tin_engine/io/ply.py b/src_python/tin_engine/io/ply.py index c2dc53e..fa49b63 100644 --- a/src_python/tin_engine/io/ply.py +++ b/src_python/tin_engine/io/ply.py @@ -112,9 +112,7 @@ def write_ply( # wrote that second line into the header and exited 0. bad = next((ch for ch in comment if ch < " " or ch == "\x7f"), None) if bad is not None: - raise ValueError( - f"a comment may not contain control characters; got {bad!r}" - ) + raise ValueError(f"a comment may not contain control characters; got {bad!r}") # ASCII is the header's encoding, so a non-ASCII comment cannot be # written. --crs is unvalidated free text by ruling 5 and a degree sign # in a projection string is ordinary, so this is a refusal a caller @@ -123,9 +121,7 @@ def write_ply( # below. if not comment.isascii(): bad = next(ch for ch in comment if not ch.isascii()) - raise ValueError( - f"a comment must be ASCII; got {bad!r} in {comment!r}" - ) + raise ValueError(f"a comment must be ASCII; got {bad!r} in {comment!r}") blocks = [(_vertex_declaration(len(points)), _vertex_body(points, ascii))] if faces is not None: diff --git a/src_python/tin_engine/viz/fixtures.py b/src_python/tin_engine/viz/fixtures.py index 6020ea0..eb1dc0e 100644 --- a/src_python/tin_engine/viz/fixtures.py +++ b/src_python/tin_engine/viz/fixtures.py @@ -125,14 +125,30 @@ def _fixture( " -- the shape the project is for", [ # The outline, counter-clockwise from the south-west. - [0.0, 0.0], [300.0, 40.0], [620.0, 0.0], [900.0, 180.0], [960.0, 520.0], - [740.0, 760.0], [420.0, 900.0], [120.0, 760.0], [20.0, 480.0], [60.0, 220.0], + [0.0, 0.0], + [300.0, 40.0], + [620.0, 0.0], + [900.0, 180.0], + [960.0, 520.0], + [740.0, 760.0], + [420.0, 900.0], + [120.0, 760.0], + [20.0, 480.0], + [60.0, 220.0], # The first interior hole, clockwise. - [200.0, 380.0], [300.0, 340.0], [260.0, 250.0], [160.0, 280.0], + [200.0, 380.0], + [300.0, 340.0], + [260.0, 250.0], + [160.0, 280.0], # The second interior hole, clockwise. - [640.0, 640.0], [760.0, 600.0], [720.0, 500.0], [600.0, 520.0], + [640.0, 640.0], + [760.0, 600.0], + [720.0, 500.0], + [600.0, 520.0], # A breakline through the northern half, clear of both holes. - [120.0, 560.0], [340.0, 620.0], [520.0, 760.0], + [120.0, 560.0], + [340.0, 620.0], + [520.0, 760.0], ], [ (range(10), "outer", 0), @@ -146,11 +162,18 @@ def _fixture( "sliver-fan", "a fan of near-collinear constraints; what the triangulator does with extreme aspect ratios", [ - [0.0, 0.0], [1000.0, 0.0], [1000.0, 200.0], [0.0, 200.0], + [0.0, 0.0], + [1000.0, 0.0], + [1000.0, 200.0], + [0.0, 200.0], # The apex, shared by all three rays, and their far ends. The first ray # is a three-point polyline that is near-collinear without being # collinear -- 1000 m long and 1 m off straight. - [10.0, 100.0], [500.0, 101.0], [990.0, 100.5], [990.0, 150.0], [990.0, 55.0], + [10.0, 100.0], + [500.0, 101.0], + [990.0, 100.5], + [990.0, 150.0], + [990.0, 55.0], ], [ (range(4), "outer", 0), @@ -164,8 +187,12 @@ def _fixture( "corner-hole", "a hole touching the outer ring at exactly one vertex -- InvalidTopology's neighbour", [ - [0.0, 0.0], [600.0, 0.0], [600.0, 600.0], [0.0, 600.0], - [560.0, 400.0], [400.0, 560.0], + [0.0, 0.0], + [600.0, 0.0], + [600.0, 600.0], + [0.0, 600.0], + [560.0, 400.0], + [400.0, 560.0], ], [ (range(4), "outer", 0), @@ -179,9 +206,18 @@ def _fixture( "hole-in-hole", "a hole nested inside a second outline; what in-domain means, drawn", [ - [0.0, 0.0], [800.0, 0.0], [800.0, 800.0], [0.0, 800.0], - [150.0, 650.0], [650.0, 650.0], [650.0, 150.0], [150.0, 150.0], - [300.0, 500.0], [500.0, 500.0], [500.0, 300.0], [300.0, 300.0], + [0.0, 0.0], + [800.0, 0.0], + [800.0, 800.0], + [0.0, 800.0], + [150.0, 650.0], + [650.0, 650.0], + [650.0, 150.0], + [150.0, 150.0], + [300.0, 500.0], + [500.0, 500.0], + [500.0, 300.0], + [300.0, 300.0], ], [ (range(4), "outer", 0), @@ -210,8 +246,14 @@ def _fixture( "road-crosses-river", "a road crossing a river: two constraints meeting at a point neither names, noded", [ - [0.0, 0.0], [700.0, 0.0], [700.0, 700.0], [0.0, 700.0], - [100.0, 100.0], [600.0, 600.0], [100.0, 600.0], [600.0, 100.0], + [0.0, 0.0], + [700.0, 0.0], + [700.0, 700.0], + [0.0, 700.0], + [100.0, 100.0], + [600.0, 600.0], + [100.0, 600.0], + [600.0, 100.0], ], [ (range(4), "outer", 0), @@ -241,11 +283,15 @@ def _fixture( [ *_BOX_700_500, # The forest ring, closed at index 4. - [200.0, 150.0], [500.0, 150.0], [500.0, 350.0], [200.0, 350.0], + [200.0, 150.0], + [500.0, 150.0], + [500.0, 350.0], + [200.0, 350.0], # The road: it crosses the ring's western edge at (200, 250) and ends # at (350, 250), inside it. The interior endpoint is what makes this # ENTERING rather than passing through, which is `bridge-over-lake`. - [60.0, 250.0], [350.0, 250.0], + [60.0, 250.0], + [350.0, 250.0], ], [ (range(4), "outer", 0), @@ -280,9 +326,13 @@ def _fixture( [ *_BOX_700_500, # The shoreline, closed at index 4. - [250.0, 150.0], [450.0, 150.0], [450.0, 350.0], [250.0, 350.0], + [250.0, 150.0], + [450.0, 150.0], + [450.0, 350.0], + [250.0, 350.0], # The deck, spanning the ring: crossings at (250, 250) and (450, 250). - [120.0, 250.0], [580.0, 250.0], + [120.0, 250.0], + [580.0, 250.0], ], [ (range(4), "outer", 0), diff --git a/src_python/tin_engine/viz/scene.py b/src_python/tin_engine/viz/scene.py index a90ebf1..02e3c93 100644 --- a/src_python/tin_engine/viz/scene.py +++ b/src_python/tin_engine/viz/scene.py @@ -253,8 +253,7 @@ def build_scene( triangles = np.asarray(drawable.triangles, dtype=np.uint32) edges = tuple( - SceneEdge(a, b, (a, b) in masked, *joined.get((a, b), (None, 0))) - for a, b in sorted(drawn) + SceneEdge(a, b, (a, b) in masked, *joined.get((a, b), (None, 0))) for a, b in sorted(drawn) ) findings = _findings(edges) if drawable is not None else () return Scene(kind, vertices, triangles, edges, findings, _bbox(vertices)) diff --git a/src_python/tin_engine/viz/svg.py b/src_python/tin_engine/viz/svg.py index b013c80..ee0916f 100644 --- a/src_python/tin_engine/viz/svg.py +++ b/src_python/tin_engine/viz/svg.py @@ -165,9 +165,7 @@ def _property_token(properties: int, strokes: tuple[PropertyStroke, ...]) -> str return next((s.token for s in strokes if properties >> s.bit & 1), None) -def _edge_classes( - edge: SceneEdge, findings: frozenset[tuple[int, int]], style: SvgStyle -) -> str: +def _edge_classes(edge: SceneEdge, findings: frozenset[tuple[int, int]], style: SvgStyle) -> str: """Every stroke class this edge belongs to, space separated. ``constrained`` is the mesh mask's verdict and ``role-*`` the input chains'; @@ -312,10 +310,7 @@ def _legend(style: SvgStyle) -> str: ("role-outer constrained", "outer ring"), ("role-hole constrained", "hole ring"), ("role-breakline constrained", "breakline"), - *( - (f"constrained {stroke.token}", stroke.token) - for stroke in style.property_strokes - ), + *((f"constrained {stroke.token}", stroke.token) for stroke in style.property_strokes), ("unconstrained", "unconstrained edge"), ] rows: list[str] = [] diff --git a/tests/python/plyread.py b/tests/python/plyread.py index 53d81dc..909f1b5 100644 --- a/tests/python/plyread.py +++ b/tests/python/plyread.py @@ -174,8 +174,10 @@ def read_ply(blob: bytes) -> tuple[Header, dict[str, dict[str, np.ndarray]]]: else: raise ValueError(f"unsupported format {header.fmt!r}") if consumed != len(blob): - raise ValueError(f"body is {len(blob) - header.body_offset} bytes, declarations want " - f"{consumed - header.body_offset}") + raise ValueError( + f"body is {len(blob) - header.body_offset} bytes, declarations want " + f"{consumed - header.body_offset}" + ) return header, data diff --git a/tests/python/test_cli_constraint_feet.py b/tests/python/test_cli_constraint_feet.py index 9c7aba5..d675172 100644 --- a/tests/python/test_cli_constraint_feet.py +++ b/tests/python/test_cli_constraint_feet.py @@ -162,7 +162,13 @@ def test_no_constraint_feet_is_off( self, tmp_path: Path, bumpy: Path, box: Path, calls: list[dict[str, Any]] ) -> None: vtk, stderr = run( - tmp_path, "--dem", str(bumpy), "--domain", str(box), "--tolerance", "1", + tmp_path, + "--dem", + str(bumpy), + "--domain", + str(box), + "--tolerance", + "1", "--no-constraint-feet", ) assert [c["constraint_feet"] for c in calls] == [False] @@ -196,9 +202,7 @@ def test_without_tolerance(self, tmp_path: Path, bumpy: Path) -> None: def test_without_dem(self, tmp_path: Path) -> None: target = tmp_path / "x.vtk" - code, output = invoke( - "catchment", "--flat", "--no-constraint-feet", "--out", str(target) - ) + code, output = invoke("catchment", "--flat", "--no-constraint-feet", "--out", str(target)) assert code == USAGE, output assert "No such option" not in output # refused for its use, not unknown assert "applies only with --dem" in output @@ -209,13 +213,19 @@ def test_without_dem(self, tmp_path: Path) -> None: class TestReport: """R9: stderr and ``--stats`` carry the count.""" - def test_stats_feet_column_matches_stderr( - self, tmp_path: Path, bumpy: Path, box: Path - ) -> None: + def test_stats_feet_column_matches_stderr(self, tmp_path: Path, bumpy: Path, box: Path) -> None: md = tmp_path / "x.md" result = mesh( - "--dem", str(bumpy), "--domain", str(box), "--tolerance", "1", - "--out", str(tmp_path / "x.vtk"), "--stats", str(md), + "--dem", + str(bumpy), + "--domain", + str(box), + "--tolerance", + "1", + "--out", + str(tmp_path / "x.vtk"), + "--stats", + str(md), ) refinement = table(section(md.read_text(encoding="utf-8"), "Refinement")) header = refinement.pop("tolerance") @@ -290,8 +300,14 @@ def test_the_quarter_circle(self, tmp_path: Path, capsys: pytest.CaptureFixture[ out = tmp_path / f"q_{tolerance}.vtk" began = time.perf_counter() result = mesh( - "--dem", str(KARTVERKET), "--domain", str(domain), "--tolerance", tolerance, - "--out", str(out), + "--dem", + str(KARTVERKET), + "--domain", + str(domain), + "--tolerance", + tolerance, + "--out", + str(out), ) elapsed = time.perf_counter() - began vtk = read_vtk(out.read_bytes()) diff --git a/tests/python/test_cli_draw.py b/tests/python/test_cli_draw.py index 8ffa944..99ab69f 100644 --- a/tests/python/test_cli_draw.py +++ b/tests/python/test_cli_draw.py @@ -294,9 +294,7 @@ def test_the_river_fixture_draws_its_property_stroke(self, tmp_path: Path) -> No } assert "river" in tokens - def test_a_fixture_with_no_properties_draws_no_property_stroke( - self, tmp_path: Path - ) -> None: + def test_a_fixture_with_no_properties_draws_no_property_stroke(self, tmp_path: Path) -> None: # Able to fail on its own: a renderer that put the token on every # constrained edge would pass the test above. `breakline-chain` is the # same picture as `river` with the mask cleared, which is what makes @@ -391,9 +389,7 @@ def test_above_the_limit_it_refuses_and_names_the_count(self, tmp_path: Path) -> count = len(np.asarray(gallery()["catchment"].vertices)) assert count > 3, "the fixture cannot exceed a limit of 3" target = tmp_path / "unreadable.svg" - result = invoke( - "catchment", "--out", str(target), "--labels", "--label-limit", "3" - ) + result = invoke("catchment", "--out", str(target), "--labels", "--label-limit", "3") assert result.exit_code != 0 message = plain(result.output) assert str(count) in message, "the refusal does not name the vertex count" @@ -431,9 +427,7 @@ class TestPathHandling: def test_a_path_inside_the_permitted_parent_is_written(self, tmp_path: Path) -> None: target = tmp_path / "inside.svg" - result = invoke( - "catchment", "--out", str(target), "--out-parent", str(tmp_path) - ) + result = invoke("catchment", "--out", str(target), "--out-parent", str(tmp_path)) assert result.exit_code == 0, plain(result.output) assert target.is_file() @@ -505,9 +499,7 @@ def test_the_degenerate_fixture_is_refused_before_triangulation(self) -> None: assert drawable is False assert words != [] - def test_the_degenerate_picture_carries_the_validators_own_words( - self, tmp_path: Path - ) -> None: + def test_the_degenerate_picture_carries_the_validators_own_words(self, tmp_path: Path) -> None: _, words = core_verdict("degenerate") target = tmp_path / "degenerate.svg" assert invoke("degenerate", "--out", str(target)).exit_code == 0 @@ -524,9 +516,7 @@ def test_the_hole_in_hole_fixture_really_fails_in_the_backend(self) -> None: assert words != ["Ok"] @pytest.mark.parametrize("name", ["degenerate", "hole-in-hole"]) - def test_a_failed_fixture_is_never_a_blank_page( - self, name: str, tmp_path: Path - ) -> None: + def test_a_failed_fixture_is_never_a_blank_page(self, name: str, tmp_path: Path) -> None: # The input drawn alone, in its role colours. This is the guard that # increment 4's risk 5 asks the viewer to be, and it is worth nothing if # the page comes out empty. @@ -535,8 +525,7 @@ def test_a_failed_fixture_is_never_a_blank_page( document = written(target) assert elements(document, "edges", "line") != [] assert any( - "role-" in (line.get("class") or "") - for line in elements(document, "edges", "line") + "role-" in (line.get("class") or "") for line in elements(document, "edges", "line") ), "the input is drawn without its role colours" @@ -698,8 +687,7 @@ class TestCrossingGallery: @pytest.mark.parametrize("name", sorted(PREDICTED_NODES)) def test_each_crossing_produces_a_node_where_the_design_says(self, name: str) -> None: assert PREDICTED_NODES[name] <= constructed_nodes(name), ( - f"{name} built {sorted(constructed_nodes(name))}, " - f"not {sorted(PREDICTED_NODES[name])}" + f"{name} built {sorted(constructed_nodes(name))}, not {sorted(PREDICTED_NODES[name])}" ) def test_an_input_vertex_is_not_a_constructed_node(self) -> None: @@ -758,9 +746,7 @@ def test_the_other_new_fixtures_draw_no_finding(self) -> None: _, scene = scene_of(name) assert scene.findings == (), f"{name}: {[f.kind.name for f in scene.findings]}" - def test_the_wall_finding_reaches_the_picture_in_the_alarm_colour( - self, tmp_path: Path - ) -> None: + def test_the_wall_finding_reaches_the_picture_in_the_alarm_colour(self, tmp_path: Path) -> None: # The overlay shipped in 6b-ii and no gallery picture has ever shown # one. A finding the renderer drops is a finding nobody sees. target = tmp_path / "wall-leaves-domain.svg" @@ -901,11 +887,7 @@ def test_the_default_is_a_named_constant_with_its_reasoning(self) -> None: # statuses point at this number and two point in opposite directions, # so it is a policy choice the composition root makes on the record. assert cli.DEFAULT_SNAP_SPACING == DEFAULT_SNAP_SPACING - source = ( - Path(cli.__file__).read_text(encoding="utf-8") - if cli.__file__ - else "" - ) + source = Path(cli.__file__).read_text(encoding="utf-8") if cli.__file__ else "" assert "DEFAULT_SNAP_SPACING" in source def test_a_coarser_spacing_still_draws(self, tmp_path: Path) -> None: @@ -943,9 +925,7 @@ class TestNoderRefusalPresentation: caller who wants the verdict reads the band. """ - def test_a_refused_noding_draws_the_input_in_role_colours( - self, tmp_path: Path - ) -> None: + def test_a_refused_noding_draws_the_input_in_role_colours(self, tmp_path: Path) -> None: target = tmp_path / "refused.svg" result = invoke( "catchment", f"--snap-spacing={OVERFLOWING_SNAP_SPACING}", "--out", str(target) @@ -956,13 +936,14 @@ def test_a_refused_noding_draws_the_input_in_role_colours( assert lines != [], "the refusal drew a blank page" assert any("role-" in (line.get("class") or "") for line in lines) - def test_a_refused_noding_draws_no_triangle_and_no_finding( - self, tmp_path: Path - ) -> None: + def test_a_refused_noding_draws_no_triangle_and_no_finding(self, tmp_path: Path) -> None: target = tmp_path / "refused.svg" - assert invoke( - "catchment", f"--snap-spacing={OVERFLOWING_SNAP_SPACING}", "--out", str(target) - ).exit_code == 0 + assert ( + invoke( + "catchment", f"--snap-spacing={OVERFLOWING_SNAP_SPACING}", "--out", str(target) + ).exit_code + == 0 + ) document = written(target) assert elements(document, "triangles", "polygon") == [] alarms = [ @@ -976,9 +957,12 @@ def test_the_band_carries_the_engines_own_words(self, tmp_path: Path) -> None: import tin_engine._core as core target = tmp_path / "refused.svg" - assert invoke( - "catchment", f"--snap-spacing={OVERFLOWING_SNAP_SPACING}", "--out", str(target) - ).exit_code == 0 + assert ( + invoke( + "catchment", f"--snap-spacing={OVERFLOWING_SNAP_SPACING}", "--out", str(target) + ).exit_code + == 0 + ) header = group_text(written(target), "header") sentence = core.describe(core.NodeStatus.CoordinateOutOfRange) # The prose is the engine's and is not pinned here; that it CROSSED is. diff --git a/tests/python/test_cli_mesh.py b/tests/python/test_cli_mesh.py index 15cbe8c..54d483f 100644 --- a/tests/python/test_cli_mesh.py +++ b/tests/python/test_cli_mesh.py @@ -62,6 +62,7 @@ def plain(text: str) -> str: """ return " ".join(BOX.sub(" ", ANSI.sub("", text)).split()) + #: The design's acceptance line uses this one. It triangulates cleanly. FIXTURE = "catchment" @@ -129,8 +130,16 @@ def test_the_vertex_blocks_are_byte_identical(self, tmp_path: Path) -> None: surface, edges = tmp_path / "s.ply", tmp_path / "e.ply" result = runner.invoke( app, - ["mesh", FIXTURE, "--flat", "--binary", "--out", str(surface), - "--out-edges", str(edges)], + [ + "mesh", + FIXTURE, + "--flat", + "--binary", + "--out", + str(surface), + "--out-edges", + str(edges), + ], ) assert result.exit_code == 0, plain(result.output) blobs = surface.read_bytes(), edges.read_bytes() @@ -196,8 +205,7 @@ def test_the_same_path_twice_is_refused_before_anything_is_written( target = tmp_path / "both.ply" result = CliRunner().invoke( app, - ["mesh", "catchment", "--flat", "--out", str(target), - "--out-edges", str(target)], + ["mesh", "catchment", "--flat", "--out", str(target), "--out-edges", str(target)], ) assert result.exit_code != 0, plain(result.output) assert "overwrite" in plain(result.output) @@ -222,9 +230,7 @@ class TestTheChainMaskJoin: """ @pytest.mark.parametrize("attempt", [FEATURED], indirect=True) - def test_every_pair_carries_the_mask_its_chains_gave_it( - self, attempt: cli.Attempt - ) -> None: + def test_every_pair_carries_the_mask_its_chains_gave_it(self, attempt: cli.Attempt) -> None: pslg = attempt.source assert pslg is not None properties = np.asarray(pslg.edge_properties) @@ -256,8 +262,16 @@ def test_the_masks_come_from_the_noded_graph( ) -> None: edges = tmp_path / "edges.ply" result = runner.invoke( - app, ["mesh", FEATURED, "--flat", "--out", str(tmp_path / "s.ply"), - "--out-edges", str(edges)] + app, + [ + "mesh", + FEATURED, + "--flat", + "--out", + str(tmp_path / "s.ply"), + "--out-edges", + str(edges), + ], ) assert result.exit_code == 0, plain(result.output) header, data = read_ply(edges.read_bytes()) @@ -329,8 +343,16 @@ def test_both_files_switch_together(self, tmp_path: Path) -> None: edges = tmp_path / "edges.ply" result = runner.invoke( app, - ["mesh", FIXTURE, "--flat", "--ascii", "--out", str(surface), - "--out-edges", str(edges)], + [ + "mesh", + FIXTURE, + "--flat", + "--ascii", + "--out", + str(surface), + "--out-edges", + str(edges), + ], ) assert result.exit_code == 0, plain(result.output) assert parse_header(surface.read_bytes()).fmt == "ascii" diff --git a/tests/python/test_cli_mesh_dem.py b/tests/python/test_cli_mesh_dem.py index 4266359..00536a9 100644 --- a/tests/python/test_cli_mesh_dem.py +++ b/tests/python/test_cli_mesh_dem.py @@ -125,9 +125,7 @@ def test_an_assumed_vertical_unit_is_said(self, tmp_path: Path, baseline: Path) assert "vertical unit assumed metres" in elevation def test_a_declared_vertical_unit_is_not_called_assumed(self, tmp_path: Path) -> None: - tif = write_tiff( - tmp_path / "m.tif", micro_tiff(geokeys=with_keys({VERTICAL_UNITS: METRE})) - ) + tif = write_tiff(tmp_path / "m.tif", micro_tiff(geokeys=with_keys({VERTICAL_UNITS: METRE}))) (elevation,) = run_vtk(tmp_path, tif).field_data["elevation_source"].values assert "assumed" not in elevation diff --git a/tests/python/test_cli_mesh_stats.py b/tests/python/test_cli_mesh_stats.py index 0fcd996..0e73d1a 100644 --- a/tests/python/test_cli_mesh_stats.py +++ b/tests/python/test_cli_mesh_stats.py @@ -165,27 +165,49 @@ def test_the_report_is_written_and_its_path_follows_the_mesh( def test_ply_with_edges_then_the_report(self, tmp_path: Path) -> None: surface, edges, md = tmp_path / "s.ply", tmp_path / "e.ply", tmp_path / "s.md" result = mesh( - "catchment", "--flat", "--out", str(surface), "--out-edges", str(edges), - "--stats", str(md), + "catchment", + "--flat", + "--out", + str(surface), + "--out-edges", + str(edges), + "--stats", + str(md), ) lines = result.stdout.splitlines() assert [same_file(a, b) for a, b in zip(lines, (surface, edges, md), strict=True)] == [ - True, True, True, + True, + True, + True, ] rows = table(section(md.read_text(encoding="utf-8"), "Sizes")) assert "s.ply" in rows and "e.ply" in rows def test_out_parent_applies_to_the_report(self, tmp_path: Path) -> None: out, md = tmp_path / "x.vtk", tmp_path / "x.md" - mesh("catchment", "--flat", "--out", str(out), "--stats", str(md), - "--out-parent", str(tmp_path)) + mesh( + "catchment", + "--flat", + "--out", + str(out), + "--stats", + str(md), + "--out-parent", + str(tmp_path), + ) assert md.is_file() def test_the_stderr_summary_is_unchanged(self, tmp_path: Path, bumpy: Path) -> None: plain_run = mesh("--dem", str(bumpy), "--tolerance", "1", "--out", str(tmp_path / "a.vtk")) stats_run = mesh( - "--dem", str(bumpy), "--tolerance", "1", "--out", str(tmp_path / "b.vtk"), - "--stats", str(tmp_path / "b.md"), + "--dem", + str(bumpy), + "--tolerance", + "1", + "--out", + str(tmp_path / "b.vtk"), + "--stats", + str(tmp_path / "b.md"), ) assert stats_run.stderr == plain_run.stderr @@ -195,7 +217,11 @@ class TestStatsToStdout: @pytest.mark.parametrize("kind", KINDS) def test_the_report_follows_the_path_line( - self, tmp_path: Path, bumpy: Path, square: Path, kind: str, + self, + tmp_path: Path, + bumpy: Path, + square: Path, + kind: str, monkeypatch: pytest.MonkeyPatch, ) -> None: monkeypatch.chdir(tmp_path) @@ -212,8 +238,14 @@ def test_the_report_follows_the_path_line( def test_after_both_ply_paths(self, tmp_path: Path) -> None: surface, edges = tmp_path / "s.ply", tmp_path / "e.ply" result = mesh( - "catchment", "--flat", "--out", str(surface), "--out-edges", str(edges), - "--stats", "-", + "catchment", + "--flat", + "--out", + str(surface), + "--out-edges", + str(edges), + "--stats", + "-", ) lines = result.stdout.splitlines() assert same_file(lines[0], surface) and same_file(lines[1], edges) @@ -235,8 +267,14 @@ def test_a_fixture(self, report_of: ReportOf) -> None: assert out.name in sizes phases = seconds(report) for row in ( - "start mesh: build", "start mesh: node", "start mesh: triangulate", - "start mesh: constraint edges", "write: encode", "write: disk", "other", "total", + "start mesh: build", + "start mesh: node", + "start mesh: triangulate", + "start mesh: constraint edges", + "write: encode", + "write: disk", + "other", + "total", ): assert row in phases, row for absent in ("decode", "sample", "refine", "trim"): @@ -256,7 +294,10 @@ def test_a_dem_without_tolerance(self, report_of: ReportOf) -> None: def test_a_tolerance_run(self, report_of: ReportOf) -> None: report, result, _ = report_of("tolerance") assert sections(report) == [ - "Sizes", "Quality (plan view, x/y)", "Refinement", "Timings", + "Sizes", + "Quality (plan view, x/y)", + "Refinement", + "Timings", ] refinement = table(section(report, "Refinement")) header = refinement.pop("tolerance") @@ -270,9 +311,13 @@ def test_a_tolerance_run(self, report_of: ReportOf) -> None: assert "Threads:" in section(report, "Timings") phases = seconds(report) for name in ( - "refine", "refine: legalise start", "refine: start quality", - "refine: scan (parallel)", "refine: split + flip (serial)", - "refine: setup + output", "trim", + "refine", + "refine: legalise start", + "refine: start quality", + "refine: scan (parallel)", + "refine: split + flip (serial)", + "refine: setup + output", + "trim", ): assert name in phases, name assert "sample" not in phases @@ -294,16 +339,23 @@ def nothing_written(tmp_path: Path) -> list[Path]: def test_stats_resolving_to_out(self, tmp_path: Path) -> None: out = tmp_path / "x.vtk" - code, output = invoke("catchment", "--flat", "--out", str(out), - "--stats", str(tmp_path / "." / "x.vtk")) + code, output = invoke( + "catchment", "--flat", "--out", str(out), "--stats", str(tmp_path / "." / "x.vtk") + ) assert code == USAGE, output assert "--stats" in output and "overwrite" in output assert self.nothing_written(tmp_path) == [] def test_stats_resolving_to_out_edges(self, tmp_path: Path) -> None: code, output = invoke( - "catchment", "--flat", "--out", str(tmp_path / "s.ply"), - "--out-edges", str(tmp_path / "e.ply"), "--stats", str(tmp_path / "e.ply"), + "catchment", + "--flat", + "--out", + str(tmp_path / "s.ply"), + "--out-edges", + str(tmp_path / "e.ply"), + "--stats", + str(tmp_path / "e.ply"), ) assert code == USAGE, output assert "--stats" in output and "overwrite" in output @@ -313,8 +365,14 @@ def test_stats_outside_out_parent(self, tmp_path: Path) -> None: inside = tmp_path / "inside" inside.mkdir() code, output = invoke( - "catchment", "--flat", "--out", str(inside / "x.vtk"), - "--out-parent", str(inside), "--stats", str(tmp_path / "x.md"), + "catchment", + "--flat", + "--out", + str(inside / "x.vtk"), + "--out-parent", + str(inside), + "--stats", + str(tmp_path / "x.md"), ) assert code == USAGE, output assert "outside the permitted parent" in output @@ -325,8 +383,9 @@ def test_stats_through_a_symlink(self, tmp_path: Path) -> None: target.write_text("keep\n") link = tmp_path / "link.md" link.symlink_to(target) - code, output = invoke("catchment", "--flat", "--out", str(tmp_path / "x.vtk"), - "--stats", str(link)) + code, output = invoke( + "catchment", "--flat", "--out", str(tmp_path / "x.vtk"), "--stats", str(link) + ) assert code == USAGE, output assert "symlink" in output assert target.read_text() == "keep\n" @@ -334,8 +393,16 @@ def test_stats_through_a_symlink(self, tmp_path: Path) -> None: def test_a_refused_mesh_run_writes_no_report(self, tmp_path: Path, bumpy: Path) -> None: md = tmp_path / "x.md" - code, output = invoke("--dem", str(bumpy), "--tolerance", "-1", - "--out", str(tmp_path / "x.vtk"), "--stats", str(md)) + code, output = invoke( + "--dem", + str(bumpy), + "--tolerance", + "-1", + "--out", + str(tmp_path / "x.vtk"), + "--stats", + str(md), + ) assert code == USAGE, output assert "must be finite and >= 0" in output # the tolerance, not an unknown --stats assert not md.exists() and not (tmp_path / "x.vtk").exists() @@ -377,6 +444,9 @@ def test_the_refine_sub_rows_sum_to_at_most_refine( subs = [s for name, s in phases.items() if name.startswith("refine: ")] assert len(subs) == 5 # increment 20 adds "refine: start quality" assert sum(subs) <= phases["refine"] + self.HALF_MS * 6 - four = sum(s for name, s in phases.items() - if name.startswith("refine: ") and name != "refine: setup + output") + four = sum( + s + for name, s in phases.items() + if name.startswith("refine: ") and name != "refine: setup + output" + ) assert four <= phases["refine"] + self.HALF_MS * 5 diff --git a/tests/python/test_cli_start_quality.py b/tests/python/test_cli_start_quality.py index 494c230..9cdec45 100644 --- a/tests/python/test_cli_start_quality.py +++ b/tests/python/test_cli_start_quality.py @@ -100,8 +100,15 @@ def test_zero_is_off( self, tmp_path: Path, bumpy: Path, box: Path, calls: list[dict[str, Any]] ) -> None: vtk, stderr = run( - tmp_path, "--dem", str(bumpy), "--domain", str(box), "--tolerance", "1", - "--start-min-angle", "0", + tmp_path, + "--dem", + str(bumpy), + "--domain", + str(box), + "--tolerance", + "1", + "--start-min-angle", + "0", ) assert [c["min_angle_deg"] for c in calls] == [0.0] text = sentence(vtk) @@ -116,8 +123,15 @@ def test_a_value_up_to_35_is_accepted_and_named( self, tmp_path: Path, bumpy: Path, box: Path, value: str, calls: list[dict[str, Any]] ) -> None: vtk, _ = run( - tmp_path, "--dem", str(bumpy), "--domain", str(box), "--tolerance", "1", - "--start-min-angle", value, + tmp_path, + "--dem", + str(bumpy), + "--domain", + str(box), + "--tolerance", + "1", + "--start-min-angle", + value, ) assert [c["min_angle_deg"] for c in calls] == [float(value)] assert f"start min angle {value} deg" in sentence(vtk) @@ -130,8 +144,16 @@ class TestRefusals: def test_a_bad_value(self, tmp_path: Path, bumpy: Path, box: Path, value: str) -> None: target = tmp_path / "x.vtk" code, output = invoke( - "--dem", str(bumpy), "--domain", str(box), "--tolerance", "1", - "--start-min-angle", value, "--out", str(target), + "--dem", + str(bumpy), + "--domain", + str(box), + "--tolerance", + "1", + "--start-min-angle", + value, + "--out", + str(target), ) assert code == USAGE, output assert "No such option" not in output # refused for its value, not unknown @@ -140,9 +162,7 @@ def test_a_bad_value(self, tmp_path: Path, bumpy: Path, box: Path, value: str) - def test_without_tolerance(self, tmp_path: Path, bumpy: Path) -> None: target = tmp_path / "x.vtk" - code, output = invoke( - "--dem", str(bumpy), "--start-min-angle", "25", "--out", str(target) - ) + code, output = invoke("--dem", str(bumpy), "--start-min-angle", "25", "--out", str(target)) assert code == USAGE, output assert "No such option" not in output # refused for its value, not unknown assert "--start-min-angle" in output @@ -183,8 +203,16 @@ def test_stats_has_the_counts_and_the_timing_row( ) -> None: md = tmp_path / "x.md" result = mesh( - "--dem", str(bumpy), "--domain", str(box), "--tolerance", "1", - "--out", str(tmp_path / "x.vtk"), "--stats", str(md), + "--dem", + str(bumpy), + "--domain", + str(box), + "--tolerance", + "1", + "--out", + str(tmp_path / "x.vtk"), + "--stats", + str(md), ) report = md.read_text(encoding="utf-8") refinement = table(section(report, "Refinement")) @@ -203,8 +231,18 @@ def test_stats_with_the_pass_off_still_has_the_row( ) -> None: md = tmp_path / "x.md" mesh( - "--dem", str(bumpy), "--domain", str(box), "--tolerance", "1", - "--start-min-angle", "0", "--out", str(tmp_path / "x.vtk"), "--stats", str(md), + "--dem", + str(bumpy), + "--domain", + str(box), + "--tolerance", + "1", + "--start-min-angle", + "0", + "--out", + str(tmp_path / "x.vtk"), + "--stats", + str(md), ) report = md.read_text(encoding="utf-8") refinement = table(section(report, "Refinement")) @@ -252,8 +290,16 @@ def test_the_quarter_circle_with_and_without_the_pass( out = tmp_path / f"q_{tolerance}_{angle}.vtk" began = time.perf_counter() result = mesh( - "--dem", str(KARTVERKET), "--domain", str(domain), "--tolerance", tolerance, - "--start-min-angle", angle, "--out", str(out), + "--dem", + str(KARTVERKET), + "--domain", + str(domain), + "--tolerance", + tolerance, + "--start-min-angle", + angle, + "--out", + str(out), ) elapsed = time.perf_counter() - began vtk = read_vtk(out.read_bytes()) @@ -276,4 +322,3 @@ def test_the_quarter_circle_with_and_without_the_pass( f"quality {quality.groups() if quality else None}" ) assert under[("10", "25")] < under[("10", "0")] - diff --git a/tests/python/test_core_cdt.py b/tests/python/test_core_cdt.py index 1411701..8c420f4 100644 --- a/tests/python/test_core_cdt.py +++ b/tests/python/test_core_cdt.py @@ -292,9 +292,7 @@ def test_rejects_a_vertex_array_that_is_not_n_by_2(self) -> None: def test_carries_a_property_mask_through_unchanged(self, square: np.ndarray) -> None: # Able to fail on its own, and the reason the three refusals below mean # anything: a binding that refused every mask would pass all of them. - result = _core.build_pslg( - square, [([0, 1, 2, 3], _core.ChainRole.Outer, RIVER | ROAD)] - ) + result = _core.build_pslg(square, [([0, 1, 2, 3], _core.ChainRole.Outer, RIVER | ROAD)]) assert result.ok, [d.message for d in result.diagnostics] assert result.pslg is not None assert result.pslg.chains[0].properties == RIVER | ROAD @@ -327,9 +325,7 @@ def test_rejects_a_mask_outside_the_ceiling(self, square: np.ndarray, mask: int) with pytest.raises(ValueError): _core.build_pslg(square, [([0, 1, 2, 3], _core.ChainRole.Outer, mask)]) - def test_an_out_of_range_mask_names_the_chain_it_came_from( - self, square: np.ndarray - ) -> None: + def test_an_out_of_range_mask_names_the_chain_it_came_from(self, square: np.ndarray) -> None: # Never silently, and never anonymously: the refusal has to say which # chain, or a caller with forty chains is told only that one of them is # wrong. Chain 1 rather than chain 0, so a message that hard-codes `0` @@ -812,9 +808,7 @@ def large_pslg() -> Any: step = np.linspace(1.0, 99.0, side) gx, gy = np.meshgrid(step, step * 0.8) jitter = rng.uniform(-0.2, 0.2, size=(2, side, side)) - interior = np.column_stack( - [(gx + jitter[0]).ravel() + EAST, (gy + jitter[1]).ravel() + NORTH] - ) + interior = np.column_stack([(gx + jitter[0]).ravel() + EAST, (gy + jitter[1]).ravel() + NORTH]) ring = np.array( [ [EAST, NORTH], @@ -1012,8 +1006,6 @@ def test_the_stub_declares_the_chains_property_set(self) -> None: def test_stub_declares_the_new_surface(self, name: str) -> None: tree = ast.parse(STUB.read_text(encoding="utf-8")) declared = { - node.name - for node in tree.body - if isinstance(node, ast.ClassDef | ast.FunctionDef) + node.name for node in tree.body if isinstance(node, ast.ClassDef | ast.FunctionDef) } assert name in declared diff --git a/tests/python/test_core_noding.py b/tests/python/test_core_noding.py index b1e1a03..fd58364 100644 --- a/tests/python/test_core_noding.py +++ b/tests/python/test_core_noding.py @@ -127,9 +127,10 @@ def expected_edge_count(graph: Any) -> int: @pytest.fixture def square() -> np.ndarray: """A counterclockwise outer ring at UTM33 magnitudes, closure not stored.""" - return np.array( - [[0.0, 0.0], [700.0, 0.0], [700.0, 700.0], [0.0, 700.0]], dtype=np.float64 - ) + ORIGIN + return ( + np.array([[0.0, 0.0], [700.0, 0.0], [700.0, 700.0], [0.0, 700.0]], dtype=np.float64) + + ORIGIN + ) @pytest.fixture @@ -144,8 +145,7 @@ def crossing_pslg(square: np.ndarray) -> Any: vertices = np.vstack( [ square, - np.array([[100.0, 100.0], [600.0, 600.0], [100.0, 600.0], [600.0, 100.0]]) - + ORIGIN, + np.array([[100.0, 100.0], [600.0, 600.0], [100.0, 600.0], [600.0, 100.0]]) + ORIGIN, ] ) return build( @@ -163,9 +163,7 @@ def disjoint_pslg(square: np.ndarray) -> Any: """An outer ring and one breakline that touches nothing: noding it must be a no-op on the topology, which is the control for every claim below that noding *changed* something.""" - vertices = np.vstack( - [square, np.array([[100.0, 350.0], [600.0, 350.0]]) + ORIGIN] - ) + vertices = np.vstack([square, np.array([[100.0, 350.0], [600.0, 350.0]]) + ORIGIN]) return build( vertices, [ @@ -269,9 +267,7 @@ def test_describe_rejects_a_third_enumeration(self) -> None: class TestNode: - def test_nodes_a_crossing_that_triangulate_used_to_refuse( - self, crossing_pslg: Any - ) -> None: + def test_nodes_a_crossing_that_triangulate_used_to_refuse(self, crossing_pslg: Any) -> None: outcome = _core.node(crossing_pslg, SPACING) assert outcome.status == _core.NodeStatus.Ok assert outcome.ok() is True @@ -369,9 +365,7 @@ def test_a_bad_spacing_is_a_status_and_not_an_exception( assert outcome.pslg is None assert outcome.message.strip() - def test_a_spacing_too_fine_for_the_coordinates_is_a_status( - self, crossing_pslg: Any - ) -> None: + def test_a_spacing_too_fine_for_the_coordinates_is_a_status(self, crossing_pslg: Any) -> None: # `kMaxGridIndex` is 2**51, so at spacing 1e-12 a UTM easting overflows # the lattice. This is the lower bound that makes `--snap-spacing` an # option rather than a hidden constant, and it is a diagnosis with a @@ -450,8 +444,8 @@ def test_indices_of_guards_its_range(self, noded_crossing: Any) -> None: noded_crossing.indices_of(len(noded_crossing.chains)) def test_every_index_is_in_range(self, noded_crossing: Any) -> None: - assert int(np.asarray(noded_crossing.chain_indices).max()) < ( - noded_crossing.vertices.shape[0] + assert ( + int(np.asarray(noded_crossing.chain_indices).max()) < (noded_crossing.vertices.shape[0]) ) def test_roles_survive_noding(self, crossing_pslg: Any, noded_crossing: Any) -> None: @@ -496,13 +490,9 @@ def test_holds_one_entry_per_output_edge(self, noded_crossing: Any) -> None: # ring carries a closing edge and an open breakline does not. A # reinterpret with the wrong length or stride fails here and nowhere # else, because the values it reads are plausible either way. - assert noded_crossing.edge_properties.shape == ( - expected_edge_count(noded_crossing), - ) + assert noded_crossing.edge_properties.shape == (expected_edge_count(noded_crossing),) - def test_carries_both_input_features_after_the_split( - self, noded_crossing: Any - ) -> None: + def test_carries_both_input_features_after_the_split(self, noded_crossing: Any) -> None: # The picture the increment exists to draw: a road and a river, in two # colours, meeting at a constructed node. Both bits must survive, and # neither may be smeared over the other. @@ -519,16 +509,12 @@ def test_the_outer_rings_edges_are_unclassified(self, noded_crossing: Any) -> No for c, chain in enumerate(noded_crossing.chains) if chain.role == _core.ChainRole.Outer ) - base = sum( - expected_edge_count_of(noded_crossing, k) for k in range(outer) - ) + base = sum(expected_edge_count_of(noded_crossing, k) for k in range(outer)) count = expected_edge_count_of(noded_crossing, outer) masks = np.asarray(noded_crossing.edge_properties)[base : base + count] assert np.all(masks == NO_PROPERTIES) - def test_two_features_within_one_cell_merge_into_one_edge( - self, square: np.ndarray - ) -> None: + def test_two_features_within_one_cell_merge_into_one_edge(self, square: np.ndarray) -> None: """A road running along a river: one output edge carrying both bits. The gallery cannot show this -- `Fixture` has no spacing field, and a @@ -718,9 +704,7 @@ def test_the_mesh_contains_the_constructed_node_once(self, noded_crossing: Any) hits = np.flatnonzero(np.abs(vertices - meeting).max(axis=1) <= SPACING) assert hits.size == 1 - def test_the_mesh_vertex_array_begins_with_the_noded_graphs( - self, noded_crossing: Any - ) -> None: + def test_the_mesh_vertex_array_begins_with_the_noded_graphs(self, noded_crossing: Any) -> None: # `triangulate.hpp` obligation 1, restated against the type that now # feeds it: index k means the same point on both sides, and `cli.py`'s # scene join is built entirely on that. @@ -987,9 +971,7 @@ def test_the_outcomes_pslg_is_optional_in_the_stub(self) -> None: if isinstance(node, ast.ClassDef) and node.name == "NodeOutcome" ) pslg = next( - node - for node in cls.body - if isinstance(node, ast.FunctionDef) and node.name == "pslg" + node for node in cls.body if isinstance(node, ast.FunctionDef) and node.name == "pslg" ) assert ast.unparse(pslg.returns or ast.Constant(None)) == "NodedPslg | None" diff --git a/tests/python/test_elevation.py b/tests/python/test_elevation.py index 9cb8c13..da7d303 100644 --- a/tests/python/test_elevation.py +++ b/tests/python/test_elevation.py @@ -50,9 +50,7 @@ def trim() -> Any: def run(trim: Any, valid: np.ndarray) -> Any: - return trim( - vertices=XY, triangles=TRIANGLES, edges=EDGES, edge_masks=MASKS, z=Z, valid=valid - ) + return trim(vertices=XY, triangles=TRIANGLES, edges=EDGES, edge_masks=MASKS, z=Z, valid=valid) def valid_except(*invalid: int) -> np.ndarray: diff --git a/tests/python/test_features.py b/tests/python/test_features.py index 220b90d..e7733db 100644 --- a/tests/python/test_features.py +++ b/tests/python/test_features.py @@ -506,8 +506,7 @@ def test_fingerprint_is_stable_across_processes() -> None: run. """ program = ( - "from tin_engine.features import DEFAULT_VOCABULARY;" - "print(DEFAULT_VOCABULARY.fingerprint())" + "from tin_engine.features import DEFAULT_VOCABULARY;print(DEFAULT_VOCABULARY.fingerprint())" ) digests = [] diff --git a/tests/python/test_grid_domain.py b/tests/python/test_grid_domain.py index b98d10d..9d26fc6 100644 --- a/tests/python/test_grid_domain.py +++ b/tests/python/test_grid_domain.py @@ -87,9 +87,7 @@ def test_coordinates_are_bit_equal_to_the_node_expression( assert len(got) == len(xy), "a node appears twice" assert got == expected_nodes(m, stride) - def test_last_row_and_column_appear_when_stride_does_not_divide( - self, grid_domain: Any - ) -> None: + def test_last_row_and_column_appear_when_stride_does_not_divide(self, grid_domain: Any) -> None: m = meta(10, 14) xy, _ = grid_domain.subsample(m, 4) x_max = m.x_min + (m.cols - 1) * m.delta_x @@ -148,9 +146,7 @@ class TestDefaultStride: ("rows", "cols"), [(2, 2), (3, 4), (256, 10), (257, 10), (10, 5051), (5051, 5051), (511, 512)], ) - def test_formula_and_at_most_256_per_side( - self, grid_domain: Any, rows: int, cols: int - ) -> None: + def test_formula_and_at_most_256_per_side(self, grid_domain: Any, rows: int, cols: int) -> None: stride = grid_domain.default_stride(meta(rows, cols)) assert stride == max(1, math.ceil((max(rows, cols) - 1) / 255)) assert len(axis(max(rows, cols), stride)) <= 256 diff --git a/tests/python/test_io_ply.py b/tests/python/test_io_ply.py index 454943d..2274513 100644 --- a/tests/python/test_io_ply.py +++ b/tests/python/test_io_ply.py @@ -141,9 +141,7 @@ def test_faces_are_a_vertex_index_list(self, surface: bytes) -> None: assert prop.name == "vertex_indices" assert prop.is_list - def test_edges_name_their_two_endpoints_and_carry_one_scalar( - self, constraints: bytes - ) -> None: + def test_edges_name_their_two_endpoints_and_carry_one_scalar(self, constraints: bytes) -> None: properties = parse_header(constraints).element("edge").properties assert [p.name for p in properties[:2]] == ["vertex1", "vertex2"] assert len(properties) == 3, "one scalar beyond the endpoints: the feature mask" @@ -300,9 +298,7 @@ class TestTheTwoFilesRegister: layers line up when a person loads them side by side. """ - def test_the_vertex_blocks_are_byte_identical( - self, surface: bytes, constraints: bytes - ) -> None: + def test_the_vertex_blocks_are_byte_identical(self, surface: bytes, constraints: bytes) -> None: assert element_bytes(surface, "vertex") == element_bytes(constraints, "vertex") def test_the_vertex_blocks_are_not_empty(self, surface: bytes) -> None: diff --git a/tests/python/test_io_vtk_legacy.py b/tests/python/test_io_vtk_legacy.py index 894365f..e3e297b 100644 --- a/tests/python/test_io_vtk_legacy.py +++ b/tests/python/test_io_vtk_legacy.py @@ -486,7 +486,6 @@ def test_it_is_re_exported_from_io(self) -> None: assert "write_vtk" in io.__all__ - class TestPointElevation: """Increment 12 amendment: z is also a point array named `elevation`, so ParaView's Color By offers the heights, and no string field shadows it.""" diff --git a/tests/python/test_refine_golden.py b/tests/python/test_refine_golden.py index f18db19..e9ff291 100644 --- a/tests/python/test_refine_golden.py +++ b/tests/python/test_refine_golden.py @@ -84,7 +84,12 @@ def refined( ) else: out = _core.refine( - to_core(tile), run.mesh, edges, masks, tolerance=TOLERANCE, threads=threads, + to_core(tile), + run.mesh, + edges, + masks, + tolerance=TOLERANCE, + threads=threads, min_angle_deg=min_angle_deg, ) assert out.ok(), out.message diff --git a/tests/python/test_session_state.py b/tests/python/test_session_state.py index ebdb20b..f3430bb 100644 --- a/tests/python/test_session_state.py +++ b/tests/python/test_session_state.py @@ -78,9 +78,7 @@ def test_roadmap_next_skips_shipped_landed_and_unscheduled_rows() -> None: def test_roadmap_next_keeps_the_status_so_the_reader_can_judge_it() -> None: - assert session_state.roadmap_next(ROADMAP)[1] == ( - "20c: Soft quality criterion [to design]" - ) + assert session_state.roadmap_next(ROADMAP)[1] == ("20c: Soft quality criterion [to design]") def test_roadmap_next_honours_n_and_ignores_rows_outside_the_first_table() -> None: @@ -95,9 +93,7 @@ def test_roadmap_next_on_a_file_without_a_table_is_empty() -> None: def test_pending_decisions_collects_ask_ola_lines_from_every_task_file( tmp_path: Path, ) -> None: - (tmp_path / "session.md").write_text( - "SESSION: did a thing.\nASK OLA: fix before landing?\n" - ) + (tmp_path / "session.md").write_text("SESSION: did a thing.\nASK OLA: fix before landing?\n") (tmp_path / "tester-101010.md").write_text("ask ola: keep the slow case?\nother\n") (tmp_path / "developer-111111.md").write_text("nothing pending\n") assert session_state.pending_decisions(tmp_path) == [ @@ -118,8 +114,18 @@ def test_last_landed_is_the_newest_merge_reachable_from_head(repo: Path) -> None def test_last_landed_without_a_merge_says_so(tmp_path: Path) -> None: _git(tmp_path, "init", "-q", "-b", "master") - _git(tmp_path, "-c", "user.email=t@e.invalid", "-c", "user.name=T", - "commit", "-q", "--allow-empty", "-m", "root") + _git( + tmp_path, + "-c", + "user.email=t@e.invalid", + "-c", + "user.name=T", + "commit", + "-q", + "--allow-empty", + "-m", + "root", + ) assert session_state.last_landed(tmp_path) == "(no merge commit reachable from HEAD)" diff --git a/tests/python/test_stats.py b/tests/python/test_stats.py index f73a431..5e5b12b 100644 --- a/tests/python/test_stats.py +++ b/tests/python/test_stats.py @@ -434,4 +434,3 @@ def test_spacing_and_hole_wording( def test_render_is_pure(self, stats: ModuleType, quarter: Any) -> None: assert stats.render(quarter) == stats.render(quarter) - diff --git a/tests/python/test_viz_scene.py b/tests/python/test_viz_scene.py index 5750810..15afb53 100644 --- a/tests/python/test_viz_scene.py +++ b/tests/python/test_viz_scene.py @@ -688,9 +688,7 @@ def test_a_breakline_is_never_closed(self, mesh: FakeMesh) -> None: assert edge_at(scene, 2, 4).role is None assert len([e for e in scene.edges if (int(e.a), int(e.b)) == (4, 5)]) == 1 - def test_a_single_vertex_ring_is_never_closed_into_a_self_loop( - self, mesh: FakeMesh - ) -> None: + def test_a_single_vertex_ring_is_never_closed_into_a_self_loop(self, mesh: FakeMesh) -> None: # This is what the length guard defends, and the only thing it does: # a one-vertex chain closed emits `_key(a, a)`, a self-loop that # violates `SceneEdge`'s `a < b` invariant and that no viewport @@ -891,9 +889,7 @@ def test_the_bbox_covers_a_backend_introduced_vertex( ) -> None: # Absolute tolerance, for `TestBoundingBox`'s reason: at an easting of # 4.3e5 the default relative one is +-0.43 m. - box = scene_module().build_scene( - pslg, mesh_with_backend_vertex, closed_roles=CLOSED - ).bbox + box = scene_module().build_scene(pslg, mesh_with_backend_vertex, closed_roles=CLOSED).bbox assert box.max_x == pytest.approx(EAST + 160.0, rel=0.0, abs=1e-9) assert box.max_y == pytest.approx(NORTH + 130.0, rel=0.0, abs=1e-9) @@ -904,9 +900,7 @@ def test_the_finiteness_guard_inspects_the_meshs_vertices(self, pslg: FakePslg) with pytest.raises(ValueError, match="finite"): scene_module().build_scene(pslg, broken, closed_roles=CLOSED) - def test_a_scene_without_a_mesh_falls_back_to_the_input_vertices( - self, pslg: FakePslg - ) -> None: + def test_a_scene_without_a_mesh_falls_back_to_the_input_vertices(self, pslg: FakePslg) -> None: # The other direction: with no drawable mesh there is no longer array to # prefer, and the input's own vertices are what the picture is of. scene = scene_module().build_scene(pslg, None, ok=False, closed_roles=CLOSED) diff --git a/tests/python/test_viz_svg.py b/tests/python/test_viz_svg.py index 7a656b2..1d311ac 100644 --- a/tests/python/test_viz_svg.py +++ b/tests/python/test_viz_svg.py @@ -503,9 +503,7 @@ class TestFixtureSanity: fixture drifted rather than that the renderer is wrong. """ - def test_the_scene_has_the_primitive_counts_the_assertions_assume( - self, scene: Any - ) -> None: + def test_the_scene_has_the_primitive_counts_the_assertions_assume(self, scene: Any) -> None: assert len(scene.vertices) == VERTEX_COUNT assert len(scene.triangles) == TRIANGLE_COUNT assert len(scene.edges) == EDGE_COUNT @@ -518,9 +516,7 @@ def test_the_four_header_counts_are_pairwise_distinct(self) -> None: counts = [VERTEX_COUNT, TRIANGLE_COUNT, CONSTRAINED_COUNT, 0] assert len(set(counts)) == len(counts) - def test_the_bbox_is_the_one_the_viewport_numbers_were_computed_for( - self, scene: Any - ) -> None: + def test_the_bbox_is_the_one_the_viewport_numbers_were_computed_for(self, scene: Any) -> None: box = scene.bbox assert box.padded is False assert box.max_x - box.min_x == pytest.approx(100.0, rel=0.0, abs=1e-9) @@ -712,15 +708,34 @@ def test_the_boundary_bits_are_accepted(self, bit: int) -> None: @pytest.mark.parametrize( "token", - ['ri"ver', "ri ver", "River", "1river", "_river", "river>", "", "riv", + "", + "riv None: + def test_a_token_that_would_escape_the_class_attribute_is_refused(self, token: str) -> None: """The hostile-input set for the pattern that is the boundary. The two newline cases and the non-ASCII one mirror @@ -780,9 +795,7 @@ def test_the_scale_is_the_smaller_of_the_two_fits(self, scene: Any) -> None: # transform that took the x fit would overflow the map area vertically. assert self.view(scene).scale == pytest.approx(SCALE, rel=0.0, abs=1e-9) - def test_the_bottom_left_world_corner_maps_to_the_bottom_left( - self, scene: Any - ) -> None: + def test_the_bottom_left_world_corner_maps_to_the_bottom_left(self, scene: Any) -> None: x, y = self.view(scene).point(EAST, NORTH) assert x == pytest.approx(MAP_LEFT, rel=0.0, abs=1e-9) assert y == pytest.approx(MAP_BOTTOM, rel=0.0, abs=1e-9) @@ -947,9 +960,7 @@ def test_no_labels_by_default(self, document: ET.Element) -> None: assert labels is None or list(labels) == [] def test_labels_draw_one_index_per_vertex(self, scene: Any) -> None: - document = parse( - viz_module("svg").render_svg(scene, style=make_style(), labels=True) - ) + document = parse(viz_module("svg").render_svg(scene, style=make_style(), labels=True)) texts = children(document, "labels", "text") assert len(texts) == VERTEX_COUNT assert sorted(text_of(t) for t in texts) == sorted(str(i) for i in range(VERTEX_COUNT)) @@ -976,19 +987,13 @@ def test_constrained_and_unconstrained_are_distinguished( token = "constrained" if edge.constrained else "unconstrained" assert token in classes(line), f"edge {(edge.a, edge.b)} is missing {token!r}" - def test_a_ring_edge_carries_its_roles_class( - self, document: ET.Element, scene: Any - ) -> None: + def test_a_ring_edge_carries_its_roles_class(self, document: ET.Element, scene: Any) -> None: assert "role-outer" in classes(edge_element(document, 0, 1, scene)) - def test_a_breakline_carries_its_own_role_class( - self, document: ET.Element, scene: Any - ) -> None: + def test_a_breakline_carries_its_own_role_class(self, document: ET.Element, scene: Any) -> None: assert "role-breakline" in classes(edge_element(document, 4, 5, scene)) - def test_a_property_is_a_class_of_its_own( - self, document: ET.Element, scene: Any - ) -> None: + def test_a_property_is_a_class_of_its_own(self, document: ET.Element, scene: Any) -> None: # A river is a breakline plus a property, so it is a fourth stroke # rather than a fourth role. assert "river" in classes(edge_element(document, 4, 5, scene)) @@ -1000,9 +1005,7 @@ def test_an_edge_with_no_properties_carries_no_property_token( assert "river" not in tokens assert "road" not in tokens - def test_an_edge_with_two_properties_draws_exactly_one_stroke( - self, mesh: FakeMesh - ) -> None: + def test_an_edge_with_two_properties_draws_exactly_one_stroke(self, mesh: FakeMesh) -> None: # The finding this ruling rests on: at gallery scale two overlaid # strokes on one polyline read as a rendering defect, not as two # features. So the SET is carried on the edge, in full, and the @@ -1098,9 +1101,7 @@ def test_the_default_style_names_no_property(self, scene: Any) -> None: assert "river" not in classes(line) assert "road" not in classes(line) - def test_an_unconstrained_edge_carries_no_role( - self, document: ET.Element, scene: Any - ) -> None: + def test_an_unconstrained_edge_carries_no_role(self, document: ET.Element, scene: Any) -> None: tokens = classes(edge_element(document, 0, 5, scene)) assert [t for t in tokens if t.startswith("role-")] == [] @@ -1178,9 +1179,7 @@ def test_the_title_is_passed_through(self, scene: Any) -> None: assert "Gaula, UTM 33N" in text_of(group(document, "header")) def test_the_status_is_passed_through(self, scene: Any) -> None: - document = parse( - viz_module("svg").render_svg(scene, style=make_style(), status="Ok") - ) + document = parse(viz_module("svg").render_svg(scene, style=make_style(), status="Ok")) assert "Ok" in text_of(group(document, "header")) def test_a_title_with_markup_in_it_cannot_break_the_document(self, scene: Any) -> None: @@ -1188,9 +1187,7 @@ def test_a_title_with_markup_in_it_cannot_break_the_document(self, scene: Any) - # the document as far as a browser is concerned and the user gets a # blank page from a file that was written successfully. hostile = ' & <' - document = parse( - viz_module("svg").render_svg(scene, style=make_style(), title=hostile) - ) + document = parse(viz_module("svg").render_svg(scene, style=make_style(), title=hostile)) assert document.tag == tag("svg") assert hostile in text_of(group(document, "header")) assert list(document.iter(tag("script"))) == [] @@ -1214,9 +1211,7 @@ def test_the_legend_names_every_property_stroke_the_style_declares( for _bit, token in GALLERY_STROKES: assert token in legend, f"the legend does not name {token!r}" - def test_the_legend_names_no_property_the_style_does_not_declare( - self, scene: Any - ) -> None: + def test_the_legend_names_no_property_the_style_does_not_declare(self, scene: Any) -> None: # Able to fail on its own, and the assertion that catches the # hard-coded row: under the default style there is no property stroke, # so there is nothing to put a legend row next to. @@ -1297,9 +1292,7 @@ def test_ok_but_empty_says_so_in_those_words(self, empty_scene: Any) -> None: # The design's own wording, and the whole point of the mode having its # own `SceneKind`: a successful call that produced nothing must not read # like a failure and must not read like a success. - document = parse( - viz_module("svg").render_svg(empty_scene, style=make_style(), status="Ok") - ) + document = parse(viz_module("svg").render_svg(empty_scene, style=make_style(), status="Ok")) assert "Ok BUT EMPTY" in text_of(group(document, "header")) def test_a_failure_does_not_claim_to_be_empty_but_ok(self, failed_scene: Any) -> None: @@ -1353,6 +1346,7 @@ def properly_cross( since a T-junction is a different unnoded defect and triangulates differently. """ + def sign(value: float) -> int: return (value > 0.0) - (value < 0.0) @@ -1603,9 +1597,7 @@ def test_no_fixture_carries_a_property_by_accident(self) -> None: assert classified == set(CLASSIFIED_FIXTURES) assert classified < set(GALLERY_NAMES), "every fixture carries a property" for name in sorted(classified): - stray = [ - int(chain.properties) & ~DECLARED_BITS for chain in gallery()[name].chains - ] + stray = [int(chain.properties) & ~DECLARED_BITS for chain in gallery()[name].chains] assert not any(stray), f"{name} sets a bit outside {DECLARED_BITS:#b}" def test_the_sliver_fan_is_near_collinear_without_being_collinear(self) -> None: @@ -1621,9 +1613,7 @@ def test_the_sliver_fan_is_near_collinear_without_being_collinear(self) -> None: assert tight, "no near-collinear triple: the fan has no sliver in it" def test_the_degenerate_fixture_is_exactly_collinear(self) -> None: - points = [ - (float(x), float(y)) for x, y in np.asarray(gallery()["degenerate"].vertices) - ] + points = [(float(x), float(y)) for x, y in np.asarray(gallery()["degenerate"].vertices)] assert len(points) >= 3 assert all(sine_of(points[0], points[1], p) < 1.0e-9 for p in points[2:]) @@ -1731,9 +1721,7 @@ def test_the_lake_ring_is_water_and_the_forest_ring_is_unclassified(self) -> Non assert int(lake.chains[AREA_RINGS["bridge-over-lake"]].properties) == COASTLINE assert int(lake.chains[2].properties) == ROAD forest = gallery()["road-enters-forest"] - assert int(forest.chains[AREA_RINGS["road-enters-forest"]].properties) == ( - NO_PROPERTIES - ) + assert int(forest.chains[AREA_RINGS["road-enters-forest"]].properties) == (NO_PROPERTIES) assert int(forest.chains[2].properties) == ROAD def test_the_wall_leaves_the_domain(self) -> None: diff --git a/tools/check_citations.py b/tools/check_citations.py index b410a48..9242355 100644 --- a/tools/check_citations.py +++ b/tools/check_citations.py @@ -109,18 +109,14 @@ def main() -> int: if target.is_file(): sources.append(target) elif target.is_dir(): - sources.extend( - p for p in sorted(target.rglob("*")) if p.suffix in SCAN_SUFFIXES - ) + sources.extend(p for p in sorted(target.rglob("*")) if p.suffix in SCAN_SUFFIXES) broken: list[str] = [] at_risk: list[str] = [] for source in sources: rel_source = source.relative_to(REPO) if source.is_relative_to(REPO) else source - for number, line in enumerate( - source.read_text(errors="replace").splitlines(), start=1 - ): + for number, line in enumerate(source.read_text(errors="replace").splitlines(), start=1): for cited, start, end in CITATION.findall(line): where = f"{rel_source}:{number}" target = resolve(cited) @@ -137,9 +133,7 @@ def main() -> int: last = max(int(start), int(end or 0)) total = line_count(target) if last > total: - broken.append( - f"{where}: cites '{cited}:{start}' -- file has {total} lines" - ) + broken.append(f"{where}: cites '{cited}:{start}' -- file has {total} lines") continue rel_target = str(target.relative_to(REPO)) if rel_target in touched: diff --git a/tools/check_prohibited_deps.py b/tools/check_prohibited_deps.py index 8123912..3c5809e 100644 --- a/tools/check_prohibited_deps.py +++ b/tools/check_prohibited_deps.py @@ -34,7 +34,7 @@ # RULED quotes the human who asked, with a date. SPELLING means "the same # library under another name" and points at the key it derives from, which must # itself resolve to RULED. A key that is neither does not pass. -RULED = "ruled" #: a human asked for it; the value quotes them, with a date +RULED = "ruled" #: a human asked for it; the value quotes them, with a date SPELLING = "spelling" #: another name for a RULED key; the value is that key PENDING = "pending" #: nobody has ruled; the value says what is unresolved @@ -56,25 +56,33 @@ class Rule(NamedTuple): PROHIBITED = { "cgal": Rule( "CGAL: GPL-encumbered; the migration replaces it with an MIT-licensed CDT", - RULED, _CGAL, "CGAL"), + RULED, + _CGAL, + "CGAL", + ), "gdal": Rule("GDAL: prohibited by CLAUDE.md section 2", RULED, _GDAL, "GDAL"), "ogr": Rule("OGR: part of GDAL", SPELLING, "gdal", "OGR"), "fiona": Rule("Fiona: wraps GDAL", SPELLING, "gdal", "Fiona"), "rasterio": Rule("Rasterio: wraps GDAL", RULED, _RASTERIO, "Rasterio"), "osgeo": Rule("osgeo: the GDAL Python bindings", SPELLING, "gdal", "GDAL"), "gdalwarper.h": Rule( - "GDAL: the warper header does not start with a bare 'gdal'", - SPELLING, "gdal", "GDAL"), + "GDAL: the warper header does not start with a bare 'gdal'", SPELLING, "gdal", "GDAL" + ), "cpl": Rule( - "CPL: GDAL's portability layer (cpl_conv.h, cpl_string.h, ...)", - SPELLING, "gdal", "GDAL"), + "CPL: GDAL's portability layer (cpl_conv.h, cpl_string.h, ...)", SPELLING, "gdal", "GDAL" + ), "date/date.h": Rule( "external date library: superseded by C++20 ", - RULED, _DATE, - "external date libraries"), + RULED, + _DATE, + "external date libraries", + ), "date/tz.h": Rule( "external date library: superseded by C++20 ", - SPELLING, "date/date.h", "external date libraries"), + SPELLING, + "date/date.h", + "external date libraries", + ), } INCLUDE_RE = re.compile(r'^\s*#\s*include\s*[<"]([^>"]+)[>"]') @@ -140,10 +148,12 @@ def build_files() -> list[Path]: found += [p for p in (ROOT / ".github" / "workflows").glob("*.y*ml")] skip = ("legacy", "build", "lib", ".venv") return sorted( - p for p in found + p + for p in found if not any(part.startswith(skip) or part in skip for part in p.relative_to(ROOT).parts) ) + BUILD_RE = re.compile( r"(find_package|target_link_libraries|link_libraries|find_library|FetchContent_Declare" r"|apt-get install|apt install|brew install|vcpkg install|conan install)\b(?P[^\n]*)", @@ -155,8 +165,12 @@ def build_files() -> list[Path]: # (which are path-shaped: "date/date.h") never match a bare CMake token. These # are the build-layer spellings. BUILD_PROHIBITED = { - "date": Rule("external date library: superseded by C++20 ", - SPELLING, "date/date.h", "external date libraries"), + "date": Rule( + "external date library: superseded by C++20 ", + SPELLING, + "date/date.h", + "external date libraries", + ), } @@ -254,7 +268,8 @@ def check_authorities() -> list[str]: parent = everything.get(cursor.authority) if parent is None: findings.append( - f"key {token!r}: derives from {cursor.authority!r}, which is not a key") + f"key {token!r}: derives from {cursor.authority!r}, which is not a key" + ) break if cursor.authority in seen: findings.append(f"key {token!r}: authority chain is a cycle") @@ -264,7 +279,8 @@ def check_authorities() -> list[str]: else: if cursor.kind == PENDING: findings.append( - f"key {token!r}: derives from {cursor.authority[:40]}..., unresolved") + f"key {token!r}: derives from {cursor.authority[:40]}..., unresolved" + ) elif rule.kind == PENDING: findings.append(f"key {token!r}: NOT RULED ON -- {rule.authority}") @@ -314,8 +330,10 @@ def main() -> int: for line in findings: print(f" {line}", file=sys.stderr) if governance: - print("\n -- prohibitions with no author, or out of step with the prose --", - file=sys.stderr) + print( + "\n -- prohibitions with no author, or out of step with the prose --", + file=sys.stderr, + ) for line in governance: print(f" {line}", file=sys.stderr) print( @@ -326,9 +344,11 @@ def main() -> int: ) return 1 - print(f"Prohibited dependency check OK: {scanned} source and build files " - f"plus pyproject.toml are clean, and all {len(PROHIBITED)} prohibitions " - "name who asked for them.") + print( + f"Prohibited dependency check OK: {scanned} source and build files " + f"plus pyproject.toml are clean, and all {len(PROHIBITED)} prohibitions " + "name who asked for them." + ) return 0 diff --git a/tools/session_state.py b/tools/session_state.py index 855a161..807cad7 100644 --- a/tools/session_state.py +++ b/tools/session_state.py @@ -253,9 +253,7 @@ def main() -> int: # Claude Code exports CLAUDE_CODE_SESSION_ID; the older spelling is kept as a # fallback so the script still excludes the current session if that changes. - here = os.environ.get("CLAUDE_CODE_SESSION_ID") or os.environ.get( - "CLAUDE_SESSION_ID", "" - ) + here = os.environ.get("CLAUDE_CODE_SESSION_ID") or os.environ.get("CLAUDE_SESSION_ID", "") others = sorted( (p for p in TRANSCRIPTS.glob("*.jsonl") if p.stem != here), key=lambda p: p.stat().st_mtime, From 20ed49227d2b72309cf17306b987092329b4eb9c Mon Sep 17 00:00:00 2001 From: Ola Skavhaug Date: Sun, 27 Sep 2026 09:53:46 +0200 Subject: [PATCH 06/13] ruff format: exclude .claude/, whose hooks change only with Ola's yes Co-Authored-By: Claude Opus 5.5 --- pyproject.toml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b0b378d..2683702 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -114,8 +114,9 @@ exclude = ["legacy", "docs/benchmarks"] [tool.ruff.format] # docs/ holds design records whose code blocks are quotations, cited by line; -# the formatter would rewrite Python inside Markdown and break both. -exclude = ["docs/**"] +# the formatter would rewrite Python inside Markdown and break both. .claude/ +# holds the push and governance hooks, edited only with Ola's yes. +exclude = ["docs/**", ".claude/**"] [tool.ruff.lint] select = ["E", "F", "I", "N", "UP", "B", "SIM", "RUF", "ASYNC"] From 7086681a2370c3c5d498cb1e07fdaceda069e8cf Mon Sep 17 00:00:00 2001 From: Ola Skavhaug Date: Sun, 27 Sep 2026 09:53:54 +0200 Subject: [PATCH 07/13] CI: ruff format --check gate (Ola); .git-blame-ignore-revs for the reformat Co-Authored-By: Claude Opus 5.5 --- .git-blame-ignore-revs | 5 +++++ .github/workflows/main.yaml | 7 +++++++ 2 files changed, 12 insertions(+) create mode 100644 .git-blame-ignore-revs diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000..a4dcd44 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,5 @@ +# Mechanical commits `git blame` should look past. +# Use: git config blame.ignoreRevsFile .git-blame-ignore-revs + +# ruff format of the tree, when `ruff format --check` became a CI gate +41db2ab24ab76c8e2f7abb5f98ddae174bf4765b diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 3d81a42..b2fb507 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -214,3 +214,10 @@ jobs: - name: ruff if: matrix.python-version == '3.12' run: ruff check . + + # Layout is enforced, not advised. `# fmt: skip` and `# fmt: off` are + # allowed with a reason (CLAUDE.md section 2); docs/ and .claude/ are + # excluded in pyproject.toml. + - name: ruff format + if: matrix.python-version == '3.12' + run: ruff format --check . From 7bca72946df77fa62bf6baeff8363bd2d4d7128b Mon Sep 17 00:00:00 2001 From: Ola Skavhaug Date: Sun, 27 Sep 2026 10:01:53 +0200 Subject: [PATCH 08/13] =?UTF-8?q?Review=20fixes:=20CLAUDE.md=20lists=20the?= =?UTF-8?q?=20format=20gate;=20five=20stale=20citations=20pinned=20or=20co?= =?UTF-8?q?rrected;=20bench-py.md=20points=20at=20=C2=A72?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 1 + docs/benchmarks/bench-py.md | 4 +--- docs/increments/05b-noder-driver.md | 2 +- docs/increments/05c-noder-wiring.md | 7 ++++--- docs/increments/06-cdt-viewer.md | 5 +++-- 5 files changed, 10 insertions(+), 9 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index f68162a..a819734 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -63,6 +63,7 @@ ctest --test-dir build # all registered suites; see tes ```bash mypy # strict, over src_python/tin_engine ruff check . # legacy/ is excluded +ruff format --check . # docs/ and .claude/ are excluded ``` ### Governance gates diff --git a/docs/benchmarks/bench-py.md b/docs/benchmarks/bench-py.md index 891500e..14f1155 100644 --- a/docs/benchmarks/bench-py.md +++ b/docs/benchmarks/bench-py.md @@ -178,9 +178,7 @@ lines in, comments and docstrings out; 688 to 691 depending on whether the two by about 80 lines. The margin rests on 16 regions packed by hand under `# fmt: skip` / `# fmt: off`; formatted normally it is about 780 counted lines (827 raw). That total was the first PR's count, because that PR added the -whole file. The ceiling is per pull request: a later PR counts the lines of -`tools/bench.py` it adds or changes (`git diff` against its base), not the -file's total, so the file's size does not cap the next change. +whole file; CLAUDE.md section 2 says what a later PR counts. ## Ruled by Ola (2026-09-27) diff --git a/docs/increments/05b-noder-driver.md b/docs/increments/05b-noder-driver.md index b8b18e3..15332f7 100644 --- a/docs/increments/05b-noder-driver.md +++ b/docs/increments/05b-noder-driver.md @@ -155,7 +155,7 @@ depends on 6b-ii merging, 5b does not depend on it at all. Three things it buys, in descending order of how load-bearing they are: 1. **It has already falsified a piece of this design, before any code was - written.** `src_python/tin_engine/viz/scene.py:200`'s `build_scene(pslg, + written.** `src_python/tin_engine/viz/scene.py:209`'s `build_scene(pslg, mesh, ...)` joins the PSLG's chain edges to the mesh's masked edges **by vertex-index pair** (`_chain_edges` builds `dict[Pair, ...]` from `indices_of(c)`; `_mesh_edges` builds `set[Pair]` from triangle corners). diff --git a/docs/increments/05c-noder-wiring.md b/docs/increments/05c-noder-wiring.md index 7a73156..05a09d8 100644 --- a/docs/increments/05c-noder-wiring.md +++ b/docs/increments/05c-noder-wiring.md @@ -634,19 +634,20 @@ product. Renaming it destroys that. increment is fixed in that increment's PR or not recorded. `viz/fixtures.py:21-26` says "**Three** of the eight are deliberate failures... ``not-noded`` and ``hole-in-hole`` are backend refusals". After 5c there are -**two**, and `not-noded` is the showcase. The inline comment at `:208-209` — +**two**, and `not-noded` is the showcase. The inline comment at `:208-209` (line numbers here and below are at +`605a60c`, where this record was written) — "the noder (5b) is what fixes it" — becomes a statement about a fix that has landed. **Its two breaklines get property bits.** Today both carry mask `0` -(`viz/fixtures.py:207,210`), while the C++ suite's fixture of the *same +(`viz/fixtures.py:207,210` at `605a60c`), while the C++ suite's fixture of the *same geometry* gives them road and river (`test_noding_node.cpp:162-165`). Two changed literals — `1` for the river bit and `2` for the road bit under `DEFAULT_VOCABULARY`'s numbering — and the picture becomes the user's sentence: a road crossing a river, in two colours, meeting at a constructed node. `fixtures.py` writes the masks as literals with a comment, because `viz/` may -not import a vocabulary (`fixtures.py:193-195` already does exactly this for +not import a vocabulary (`fixtures.py:193-194` at `605a60c` already does exactly this for `BREAKLINE`). Note that `test_noding_node.cpp:76-77` numbers them the other way round — `bit(0)` is its road. That is not a defect to reconcile: **the C++ holds no vocabulary at all**, by increment 7's ruling, and its test constants are diff --git a/docs/increments/06-cdt-viewer.md b/docs/increments/06-cdt-viewer.md index cc1d198..cdd9302 100644 --- a/docs/increments/06-cdt-viewer.md +++ b/docs/increments/06-cdt-viewer.md @@ -732,8 +732,9 @@ The unqualified present-tense form (`grep -nE '^\s*\*'` over `src_python/tin_engine/viz/*.py` and `_core.pyi`, "prints exactly `scene.py:203: *,`") stood here and is **false of the tree today**, in two ways: `scene.py`'s line is now `:212`, and `svg.py` — which did not exist at -`995f258` — contributes two more real undercounted lines (`:313`, `:373`) plus -two docstring lines the pattern also matches. The figure above is unaffected, +`995f258` — contributes two more real undercounted lines plus two docstring lines the +pattern also matches (`grep -nE '^\s*\*' src_python/tin_engine/viz/svg.py` +lists them). The figure above is unaffected, because it is increment 6's measurement of increment 6; a re-runnable claim that silently starts measuring a later tree is not, which is why it is now anchored. Factor 2.16 on the ~90 estimate. The composition is the interesting part, and it From 5437d0f8977c30f67916fbd40ddbc27f85442e2c Mon Sep 17 00:00:00 2001 From: Ola Skavhaug Date: Sun, 27 Sep 2026 10:03:30 +0200 Subject: [PATCH 09/13] 05c: pin the whole passage's citations to 605a60c; 193-195 and the RIVER fixture Co-Authored-By: Claude Opus 5.5 --- docs/increments/05c-noder-wiring.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/increments/05c-noder-wiring.md b/docs/increments/05c-noder-wiring.md index 05a09d8..2dc28f6 100644 --- a/docs/increments/05c-noder-wiring.md +++ b/docs/increments/05c-noder-wiring.md @@ -632,10 +632,10 @@ product. Renaming it destroys that. **Its module docstring becomes wrong and is fixed in this PR**, per `docs/increments/README.md`'s rule that a documentation defect found during an increment is fixed in that increment's PR or not recorded. -`viz/fixtures.py:21-26` says "**Three** of the eight are deliberate failures... +Line numbers in this passage are at `605a60c`, where this record was written. +`viz/fixtures.py:21-26` said "**Three** of the eight are deliberate failures... ``not-noded`` and ``hole-in-hole`` are backend refusals". After 5c there are -**two**, and `not-noded` is the showcase. The inline comment at `:208-209` (line numbers here and below are at -`605a60c`, where this record was written) — +**two**, and `not-noded` is the showcase. The inline comment at `:208-209` — "the noder (5b) is what fixes it" — becomes a statement about a fix that has landed. @@ -647,8 +647,8 @@ changed literals — `1` for the river bit and `2` for the road bit under a road crossing a river, in two colours, meeting at a constructed node. `fixtures.py` writes the masks as literals with a comment, because `viz/` may -not import a vocabulary (`fixtures.py:193-194` at `605a60c` already does exactly this for -`BREAKLINE`). Note that `test_noding_node.cpp:76-77` numbers them the other way +not import a vocabulary (`fixtures.py:193-195` at `605a60c` already does exactly this for +the `RIVER` fixture). Note that `test_noding_node.cpp:76-77` numbers them the other way round — `bit(0)` is its road. That is not a defect to reconcile: **the C++ holds no vocabulary at all**, by increment 7's ruling, and its test constants are local names. It is written down here only so that nobody "fixes" the two into @@ -1285,7 +1285,7 @@ increment's PR, or not recorded: number is dropped in favour of the test's name and a `grep`, per `.claude/REQUIRED-READING.md` on resolved values. The paragraph's claim is still true; only the citation expired. -* **`src_python/tin_engine/viz/fixtures.py:21-26`** — three deliberate failures +* **`src_python/tin_engine/viz/fixtures.py:21-26`** (at `605a60c`) — three deliberate failures become two, and `not-noded` changes role. **Not corrected, because 5b corrected it itself:** the "The C++ rows are *not* From dd4fa88cc1fc69d870067c2be49bf00d88f40572 Mon Sep 17 00:00:00 2001 From: Ola Skavhaug Date: Sun, 27 Sep 2026 10:05:48 +0200 Subject: [PATCH 10/13] after-commit hook: ruff format --check joins the gates (Ola's yes, 2026-09-27) Co-Authored-By: Claude Opus 5.5 --- .claude/hooks/gates_after_commit.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.claude/hooks/gates_after_commit.py b/.claude/hooks/gates_after_commit.py index 9cd69fd..d9be614 100755 --- a/.claude/hooks/gates_after_commit.py +++ b/.claude/hooks/gates_after_commit.py @@ -19,7 +19,7 @@ NOT AUTHORITATIVE. CLAUDE.md is explicit that CI decides, and a whole branch once merged with CI red on every commit. `mypy` and the C++ suite are omitted here -because they need a build; green from this hook means "these four gates passed on +because they need a build; green from this hook means "the GATES below passed on these files", which is strictly less than `gh pr checks`. """ @@ -37,13 +37,14 @@ #: regenerates uv.lock, and uv.lock is gitignored on this branch precisely #: because 1016 lines of it rode into a commit that way. A hook scheduling that #: command on every commit would recreate the artifact forever. CI runs plain -#: `ruff check .` (.github/workflows/main.yaml). +#: `ruff check .` and `ruff format --check .` (.github/workflows/main.yaml). GATES = ( ["python3", "tools/check_prohibited_deps.py"], ["python3", "tools/check_legacy_imports.py"], ["python3", "tools/check_detria_boundary.py"], ["python3", "tools/check_citations.py"], [str(ROOT / ".venv" / "bin" / "ruff"), "check", "."], + [str(ROOT / ".venv" / "bin" / "ruff"), "format", "--check", "."], ) From 2abba36efecc38006e0876e33dcd7f786e31674e Mon Sep 17 00:00:00 2001 From: Ola Skavhaug Date: Sun, 27 Sep 2026 10:05:54 +0200 Subject: [PATCH 11/13] 05c: two 05b citations re-pointed; 'Today' becomes 'At 605a60c' Co-Authored-By: Claude Opus 5.5 --- docs/increments/05c-noder-wiring.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/increments/05c-noder-wiring.md b/docs/increments/05c-noder-wiring.md index 2dc28f6..e7b70e2 100644 --- a/docs/increments/05c-noder-wiring.md +++ b/docs/increments/05c-noder-wiring.md @@ -639,8 +639,8 @@ Line numbers in this passage are at `605a60c`, where this record was written. "the noder (5b) is what fixes it" — becomes a statement about a fix that has landed. -**Its two breaklines get property bits.** Today both carry mask `0` -(`viz/fixtures.py:207,210` at `605a60c`), while the C++ suite's fixture of the *same +**Its two breaklines get property bits.** At `605a60c` both carry mask `0` +(`viz/fixtures.py:207,210`), while the C++ suite's fixture of the *same geometry* gives them road and river (`test_noding_node.cpp:162-165`). Two changed literals — `1` for the river bit and `2` for the road bit under `DEFAULT_VOCABULARY`'s numbering — and the picture becomes the user's sentence: @@ -902,7 +902,7 @@ is priced 50 % above the block it replaces for that reason. ### Gate D — 5c's own, and it is a budget with an obeyable seam The one gate in this project's record that worked fired **before its suite -existed** (`05b-noder-driver.md:430-432`); the one that failed did so because the +existed** (`05b-noder-driver.md:434-435`); the one that failed did so because the committed suite spanned the seam it would have had to cut. So Gate D is armed with that property removed by construction: @@ -1146,7 +1146,7 @@ lifetime or a reinterpret: `InvalidSnapSpacing` rather than raising; a path-like argument in `pslg`'s place is a `TypeError`, matching `build_pslg`'s boundary. -**The end-to-end acceptance criterion**, which is `05b-noder-driver.md:143-146`'s +**The end-to-end acceptance criterion**, which is `05b-noder-driver.md:145-148`'s criterion with the call it was waiting for: ```sh From 91d5b7665ebe34c0ed40d7e72c9718e772fd281e Mon Sep 17 00:00:00 2001 From: Ola Skavhaug Date: Sun, 27 Sep 2026 10:07:11 +0200 Subject: [PATCH 12/13] bench.py: close the fmt:off region, so the format gate covers run() onwards The top-level `# fmt: off` before run() was paired with a `# fmt: on` indented inside the function body. ruff honours `fmt: on` only at the indentation of its `fmt: off`, so formatting stayed off to end of file and `ruff format --check` skipped run(), compare() and child_main(). The run() signature is already in ruff's own format (one parameter per line, none over 100 columns), so no exemption is needed: both lines are removed and ruff format changes nothing. AST unchanged. Co-Authored-By: Claude Opus 5.5 --- tools/bench.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tools/bench.py b/tools/bench.py index bbc21c2..77765b1 100644 --- a/tools/bench.py +++ b/tools/bench.py @@ -619,7 +619,6 @@ def _measure( ] -# fmt: off @app.command() def run( label: Annotated[str, typer.Option("--label", help="Names the evidence directory.")], @@ -637,7 +636,6 @@ def run( threshold: Annotated[float, typer.Option(help="Percent on the median.")] = 5.0, accept_quality: Annotated[bool, typer.Option(help="Waive angle and degree loss.")] = False, ) -> None: - # fmt: on """Build, measure, store the evidence, and judge it against a baseline.""" runner = make_runner() tree, dem = tree.resolve(), dem.resolve() From d6d6bebe4425df21ec758fc5377b4fc5f4b780f2 Mon Sep 17 00:00:00 2001 From: Ola Skavhaug Date: Sun, 27 Sep 2026 10:07:32 +0200 Subject: [PATCH 13/13] bench-py.md: the size note is dated to green; the fmt:off pair and why it went Co-Authored-By: Claude Opus 5.5 --- docs/benchmarks/bench-py.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/benchmarks/bench-py.md b/docs/benchmarks/bench-py.md index 14f1155..a826bbd 100644 --- a/docs/benchmarks/bench-py.md +++ b/docs/benchmarks/bench-py.md @@ -172,13 +172,18 @@ baseline search 60, evidence writing 70, Typer app 50: about 500 lines, under the 700 ceiling. Plus `tools/bench.py` added to mypy's `files` in `pyproject.toml`, so the strict gate covers it. -Measured at green: about 690 lines counted as section 2 counts them (blank +Measured at green (`90f45ae`): about 690 lines counted as section 2 counts them (blank lines in, comments and docstrings out; 688 to 691 depending on whether the two `# fmt: off/on` lines count), 582 without blank lines, so the estimate was low by about 80 lines. The margin rests on 16 regions packed by hand under `# fmt: skip` / `# fmt: off`; formatted normally it is about 780 counted lines (827 raw). That total was the first PR's count, because that PR added the -whole file; CLAUDE.md section 2 says what a later PR counts. +whole file; CLAUDE.md section 2 says what a later PR counts. The `# fmt: +off/on` pair was removed in `91d5b76`: its `on` was indented, so ruff never +switched formatting back on, and the format gate skipped everything from `run` +to the end of the file. The signature it guarded was already in ruff's layout. +The packed regions left are `# fmt: skip` lines (`grep -c "fmt: skip" +tools/bench.py`). ## Ruled by Ola (2026-09-27)