Skip to content

Increment 36: GeoPackage output (--out X.gpkg) over sqlite3, QGIS-ready - #231

Merged
skavhaug merged 19 commits into
masterfrom
worktree-gpkg-out
Oct 12, 2026
Merged

skavhaug merged 19 commits into
masterfrom
worktree-gpkg-out

Conversation

@skavhaug

Copy link
Copy Markdown
Member

Increment 36, docs/increments/36-geopackage-output.md. 378 net production lines (python3 tools/count_loc.py de9b6c03 HEAD).

  • rasputin mesh --out X.gpkg writes a GeoPackage through Python's sqlite3 only: no GDAL, OGR or Fiona (tools/check_prohibited_deps.py clean).
  • Layers: triangles (PolygonZ, eight attributes, R-tree index), lines, land_cover and land_cover_classes (only when a coded source leaves at least one polygon), run (the run record), and a stored QGIS layer_styles row so the file opens coloured.
  • Fixed 1970 timestamps: two writes of the same mesh are byte-identical.
  • Writer module io/geopackage_write.py; .part housekeeping stays in cli.py so io/repository.py keeps its read-only boundary.

Evidence: @Perf acceptance ACCEPTED (docs/benchmarks/2026-10-11/gpkg-out/): Numedalslågen 3.93 s / 203 MB file, Romsdalen 19.4 s / 810 MB; writer RSS share +32 MB / +188 MB. 10 of 10 mutants killed. Code review rounds 1 and 2 recorded in the increment file, round 2 APPROVED; round 1 found the empty land-cover crash, fixed with a red test and a guard.

🤖 Generated with Claude Code

skavhaug and others added 19 commits October 11, 2026 19:45
…pkg (@architect)

docs/increments/36-geopackage-output.md: the design. A writer under io/
(sqlite3 from the standard library, no GDAL/OGR/Fiona) chosen by the .gpkg
suffix as .vtk and .ply are; layers triangles (PolygonZ with code, name,
colour, z_min, z_max, z_mean, area_m2, slope_deg, R-tree indexed), lines
(constraint edges with their property bits), land_cover (the clipped,
cleaned coded polygons), land_cover_classes (the legend and shares), a run
table holding the run record, and the stored QGIS style. Mirror of the
reader io/geopackage.py, which reads the files back in the tests.

Measured with docs/increments/36-probes/gpkg_write_probe.py on this
worktree's .venv (AC power, SQLite 3.53.1): the quick check's Numedalslagen
mesh (653,039 triangles) 4.0 s and 203 MB against 0.28 s and 29.2 MB for the
binary .vtk, Ola's stopgap 5.1 s; the Romsdalen slope mesh (3,096,202
triangles) 21.5 s and 922 MB at 2.1 GB peak RSS, the stopgap 28.7 s and
4.1 GB. The R-tree is 60 % of the time (about 4.6 us per triangle); feeding
it in Morton order was slower, a 256 MB page cache 25 % faster; building
blobs and rows per batch of 100,000 cut peak RSS from 5.8 GB to 2.1 GB.
Two writes of each mesh gave identical SHA-256; timestamps are fixed text.

Left out: the per-triangle source-polygon ID (its own step, as the ROADMAP
row says) and bulk R-tree loading (36b, for basin scale: about 8 minutes of
index and a 30 GB file at 1e8 triangles). Estimate about 305 net production
lines. Byte-identical .vtk/.ply pinned by test_refine_golden's
GOLDEN_STRIDE_VTK and the quick check's mesh_sha256.

ROADMAP.md: the row numbered 36, status designed, with the figures.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
docs/increments/36-probes/gpkg_write_probe.py formatted with ruff and its
findings fixed (one statement per line, zip strict, context managers, a
lowercase name); the long SQL and report lines carry a file-level E501
exemption. Re-run on the test tile after the change: the same figures
(69,571 triangles, 0.40 s, 20.0 MB, second write identical).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…architect)

docs/increments/36-geopackage-output.md, the GeoPackage output design:

(1) T1 (section 8): the reader's decode_geometry skips the 32-byte
envelope, so T1 now unpacks each blob's header itself (bytes 0-4 magic and
flags, 4-8 srs_id, 8-40 the triangle's minx, maxx, miny, maxy) and names
the oracle per mutant. F1 is written with srs_id 25833: 0 and -1 read the
same in either byte order, 25833 read big-endian is -379322368 (checked
with struct). The unclosed-ring and 3-point mutants are caught by GEOS
("Points of LinearRing do not form a closed linestring"), checked with
shapely 2.2 on the F1 corners.

(2) T2: written with batch=30_000, so F2's 69,571 triangles cross two
boundaries and leave a partial batch of 9,571; the dropped-batch mutant
shows as 60,000 rows against 69,571. Oracles named per mutant.

(3) Section 4: land_cover_classes and run get fid INTEGER PRIMARY KEY
(OGC 12-128 Requirement 119); run.name becomes TEXT UNIQUE.

(4) Section 3: the one probe command behind every candidate figure
(--rtree executemany --cache-mb 256 --stream --per-batch --vtk-time under
/usr/bin/time -l), the stopgap and --twice flags, the mesh behind each row
(test tile command, the quick check's numedalslagen case, the Romsdalen
.vtk in rasputin_scratch), and each rejected variant's flag beside its
figure. The probe's defaults are the rejected variants, said outright.
Row 1 re-run with that command on this worktree: 0.40 s, R-tree 0.24 s,
20.0 MB, 160 MB peak RSS (the table says 0.23 s for the R-tree; single
runs). No other figure re-measured.

Reviewer's idea taken (section 5): write_land_cover states its envelope
order, minx, maxx, miny, maxy, as flag 0b0011 declares; the stopgap's
poly_layer reorders the bounds twice (its lines 229 and 118) and stores
minx, miny, maxx, maxy, confirmed by reading those lines.

Status line updated. Lesson recorded in the handback: a mutant's kill
names the oracle that sees the mutated bytes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…h the CLI (@tester)

docs/increments/36-geopackage-output.md section 8, red before any production
code. 42 tests in two files, all red for the design's reasons and no other:

- tests/python/test_io_geopackage_write.py (unit half, 24 red): 17 error on
  `ModuleNotFoundError: tin_engine.io.geopackage_write`, 5 on
  `AttributeError: tin_engine.crs has no attribute srs_entry`, 1 on the
  writer's source file not existing. F1 (4 vertices, 2 triangles, the 0-2
  diagonal with the `river` bit, codes 312 and 0, srs_id 25833) in a
  `:memory:` connection: T1 round trip through `io/geopackage.py` plus the
  header bytes 0-40 unpacked here (GP, 0, 0b0011, `<i` 25833, `<4d`
  minx,maxx,miny,maxy) and the WKB bytes 40-53 (1, 1003, 1, 4); T2 on F2's
  arrays with batch=30_000 (69,571 rows, counts equal, each R-tree box within
  one float32 step of its triangle); T3 the eight attributes by hand,
  the unnamed code (NULL, #ff00ff), codes=None; T5 `write_run` rows in order,
  fid INTEGER PRIMARY KEY, name UNIQUE; T6 `write_lines` length, bits,
  no column for an unset property; T7 `srs_entry` (EPSG -> WKT1, no code
  -> 100000 WKT2, None/free text -> -1) and the SRS rows.
- tests/python/test_cli_mesh_gpkg.py (CLI half, 18 red): every run with
  `--out X.gpkg` is refused today as "has no known suffix; use .vtk or
  .ply"; the two refusal tests fail on the missing sentence, not on the exit
  code. F2 = the Kartverket tile at --tolerance 10 (needs the codecs extra),
  meshed once to .gpkg with --record and --stats, once to .vtk, twice to one
  path; F3 = the bumpy DEM with CORINE rectangles 311/512. T3 figures
  against the .vtk arrays, land_cover layer and its envelope order; T4
  SHA-256 and fixed timestamps; T5 run rows against --record and --stats,
  land_cover_classes sums, plan areas sum to the domain; T6 lines through
  feature_mask and the vocabulary; T7 EPSG:25833 and `--flat --crs "free
  text"` -> -1; T8 echo, refusals, replace, record/stats, fid-1 = .vtk index;
  T9 the QML (attr code, one category per palette code, palette colours).

Oracles checked against the existing .vtk path before committing: F2 has
69,571 triangles and 383 edges, F3 586 triangles with codes {0, 311, 512}
and plan areas summing to the domain within 1e-7 m^2.

Red run: .venv/bin/python -m pytest tests/python/test_io_geopackage_write.py
tests/python/test_cli_mesh_gpkg.py --no-cov -q  -> 4 failed, 38 errors.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…oper)

The green step of increment 36 (docs/increments/36-geopackage-output.md,
sections 4 and 5). The 42 tests of the red commit 87f41c3 pass
(tests/python/test_io_geopackage_write.py, tests/python/test_cli_mesh_gpkg.py);
no test file is touched.

New module src_python/tin_engine/io/geopackage_write.py, the mirror of the
reader io/geopackage.py, over an open sqlite3.Connection (opens no file,
knows no path, no _core, no pyproj; the standard library's sqlite3 only):

- polygon_blobs / line_blobs: one numpy structured array per batch; GP
  header, version 0, flags 0b0011 (little-endian, 32-byte XY envelope as
  minx, maxx, miny, maxy), srs_id, then ISO WKB (byte order 1, type 1003
  PolygonZ with one ring of four points / 1002 LineStringZ).
- create_geopackage: application_id GPKG, user_version 10300, the four
  gpkg_* tables, the reserved rows -1 and 0, EPSG:4326, and the run's SRS
  once (not again when it is one of those).
- write_triangles(conn, vertices, triangles, codes, palette, srs_id,
  batch=100_000): rows and blobs built per batch from array slices, one
  transaction via executemany; code / land_cover / colour (NULL on every row
  with codes=None; a code the palette lacks: NULL name, #ff00ff), z_min,
  z_max, z_mean, area_m2 (half the cross product), slope_deg
  (atan2(|n_xy|, |n_z|)); registers the layer (POLYGON, z=1) and builds the
  R-tree after the rows.
- build_rtree: the virtual table fed directly in batches under a 256 MB page
  cache (reset afterwards), registered as the write-only gpkg_rtree_index.
- write_lines: LineStringZ per constraint edge, length_m, one 0/1 column per
  vocabulary property set on at least one edge, in bit order; registered, no
  R-tree.
- write_land_cover: 2-D MultiPolygon per (code, polygon) through
  shapely.to_wkb(flavor="iso"), header envelope minx, maxx, miny, maxy.
- write_classes: land_cover_classes (fid INTEGER PRIMARY KEY) from one GROUP
  BY over triangles, so its sums are the table's own.
- write_run(conn, rows): run (fid INTEGER PRIMARY KEY, name TEXT UNIQUE,
  wording, value, number REAL), registered as attributes.
- write_styles: QGIS layer_styles, a categorised fill on `code` with the
  palette's colours and labels, useAsDefault 1, for triangles and land_cover.
- Every last_change and update_time is the fixed text
  1970-01-01T00:00:00.000Z; nothing reads the clock, so two writes of one
  run are byte-identical.

Two departures from section 5 of the design, each forced by an existing test:

- SrsEntry (with UNDEFINED_SRS and CUSTOM_SRS_ID) lives in io/models.py, not
  in geopackage_write.py: crs.py (layer L1) may not import an io/ codec (L2),
  per tests/python/test_layering.py, and the writer may not import crs, per
  the red suite. io.models is L0 and both import it.
- The CLI does not call sqlite3.connect: tests/python/test_io_geopackage.py
  (TestBoundaries.test_only_the_repository_connects) allows only
  io/repository.py to connect, so repository.py gains create_sqlite(path),
  the one writable connection (it removes an existing file first).

Still red, by design of the test: three tests of tests/python/test_layering.py
want table rows for the new module and the two new edges, which the table's
own docstring says are a @tester commit: io.geopackage_write (L2) imports
"features io.geopackage io.models"; crs adds io.models; cli adds
io.geopackage_write.

crs.py: srs_entry(crs) -> SrsEntry, the only place pyproj is asked for WKT:
EPSG code -> srs_id = code with WKT1_GDAL; a CRS pyproj parses but cannot
code -> 100000, organization rasputin, WKT2; None or text pyproj refuses ->
the reserved -1 row. Never raises.

feature_input.py: ClassMap.palette (None), "corine" on the three CORINE maps,
so the writer looks the palette up by name in palettes.PALETTES.

cli.py: MESH_SUFFIXES gains .gpkg; --out-edges with .gpkg is refused ("the
edges are the lines layer of the GeoPackage") and --ascii with .gpkg is
refused ("applies to .vtk and .ply; a GeoPackage is an SQLite file"), both
before anything is meshed; _write_geopackage opens <out>.part, writes the
layers in one `write: geopackage` phase, commits, closes and os.replace()s it
onto --out, so an existing file is replaced, never appended to, and a failed
run removes the .part. The .vtk and .ply branches are untouched
(tests/python/test_refine_golden.py passes).

Verified: the whole Python suite (7157 passed, 17 skipped; the three
layering-table tests above are the only red), mypy strict, ruff check, ruff
format --check, tools/check_prohibited_deps.py, tools/check_detria_boundary.py,
tools/check_citations.py. No C++ file changed. The quick check's
numedalslagen case written once on AC power: `write: geopackage` 3.98 s,
203.3 MB, 653,039 triangles, 67,240 lines, 22 land_cover rows (design:
4.0 s, 203 MB).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…unts them (@developer)

No behaviour change: the templates hold no backslash. docs/increments/36-geopackage-output.md section 9 says the QML template is a raw literal and does not count; CLAUDE.md section 2 excludes the body of a raw literal.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ester)

tests/python/test_layering.py: the dependency map gains the edges increment
36 adds, each checked against the module's import lines before it was added
(`grep -n "^from\|^import"` over the three files):

- L2 `io.geopackage_write`: `features io.geopackage io.models` (new row).
- L1 `crs`: adds `io.models` (SrsEntry, CUSTOM_SRS_ID, UNDEFINED_SRS).
- L5 `cli`: adds `io.geopackage_write`.

All three go down or sideways (L0 into L1, L2 into L2, L2 into L5); no new
entry in UPWARD. The three rows @developer named were all correct; none was
missing or wrong. `tests/python/test_layering.py`: 64 passed.

Whole Python suite (`.venv/bin/python -m pytest -q --no-cov`): 7161 passed,
17 skipped, 1 failed, and the failure is not in a test file this PR touches:
tests/python/test_io_repository.py::TestQ3OneModuleOpensFiles::
test_the_repository_opens_files_read_only finds `unlink:248` in
io/repository.py, the `Path(path).unlink(missing_ok=True)` of the green
commit c59246e's `create_sqlite`. Ola's Q3 ruling (increment 15a) lets
repository.py open files read-only and write nothing outside CacheWriter;
the design's section 5 had cli.py call sqlite3.connect itself, which
tests/python/test_io_geopackage.py's TestBoundaries forbids. A production-code
question, left to the handback; the Q3 guard is not loosened here.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ing out (@tester)

Kill record for the invariant-critical suite T1 to T6
(docs/increments/36-geopackage-output.md, section 8). Each mutant was planted
in a scratch copy of 0943a7a made by `python3 tools/scratch_copy.py HEAD
<scratchpad>/m36`, in src_python/tin_engine/io/geopackage_write.py, and the
two 36 test files (tests/python/test_io_geopackage_write.py,
tests/python/test_cli_mesh_gpkg.py; 42 tests) were run with `--no-cov`;
the copy was restored and `touch`ed after each mutant and confirmed green
before, between and after (42 passed), then removed.

One row per mutant: where planted (function, line of 0943a7a) | the test
and assertion that kills it (the design's oracle first) | result.

M1 T1 header byte-order flag 0: `_records` l.143, flags 0b0011 -> 0b0010 |
  TestRoundTrip::test_each_triangle_reads_back_as_its_polygonz_in_triangle_order,
  the reader raises GeoPackageError "row 1: srs_id -379322368, the layer's is
  25833" (the design's figure); also test_the_header_is_gp_version_0_...,
  `assert blob[3] == 0b0011`; 6 tests fail in all | KILLED
M2 T1 envelope order swapped (minx, miny, maxx, maxy): `_envelopes` l.137 |
  TestRoundTrip::test_the_header_is_gp_version_0_little_endian_with_an_xy_envelope,
  `struct.unpack_from("<4d", blob, 8) == bounds(corners)` (the header unpack;
  the reader never sees it); also the gpkg_contents bounds, the R-tree boxes
  (T2: 69,571 boxes differ) and the lines header; 6 fail | KILLED
M3 T1 WKB type 3 for 1003: `polygon_blobs` l.157 |
  TestRoundTrip::test_the_wkb_is_iso_polygonz_with_one_ring_of_four_points,
  `(geometry_type, rings, points) == (1003, 1, 4)` got (3, 1, 4); and the
  reader: GeoPackageError "unreadable WKB: Points of LinearRing do not form a
  closed linestring" (GEOS reads 4 points of 2 doubles out of the 12 written,
  so it never reaches the design's predicted flat Polygon with has_z false);
  3 fail | KILLED
M4 T1 ring not closed (fourth point = second corner): `polygon_blobs` l.159 |
  TestRoundTrip::test_each_triangle_reads_back_as_its_polygonz_in_triangle_order,
  GeoPackageError "Points of LinearRing do not form a closed linestring", and
  the CLI's TestCli::test_fid_minus_one_is_the_triangle_index_in_the_vtk_...;
  2 fail | KILLED
M5 T1 point count 3: `polygon_blobs` l.158, points 4 -> 3 | the reader as
  M4, and test_the_wkb_is_iso_polygonz_..., got (1003, 1, 3); 3 fail | KILLED
M6 T2 R-tree id off by one: `build_rtree` l.229, range(start, ...) |
  TestIndexMatchesRows::test_every_fid_has_its_own_box_and_the_counts_agree,
  `assert_array_equal(stored[:, 0], np.arange(1, len(triangles) + 1))`; and
  TestRoundTrip::test_the_rtree_holds_each_triangle_box_under_its_fid; 2 fail
  | KILLED
M7 T2 last partial batch dropped: `build_rtree` l.227,
  range(0, len(boxes) - len(boxes) % batch, batch) |
  TestIndexMatchesRows::test_every_fid_has_its_own_box_and_the_counts_agree,
  `rows == indexed == len(triangles)`: 69571 == 60000 (the design's figure,
  reached only because T2 writes with batch=30_000); at F1 the R-tree is
  empty, so test_the_rtree_holds_each_triangle_box_under_its_fid fails too;
  2 fail | KILLED
M8 T2 R-tree miny/maxx swapped: `build_rtree` l.230, columns [0, 2, 1, 3] |
  TestIndexMatchesRows::..., "69571 boxes differ from their triangle; fids
  [1 2 3 4 5]"; and the F1 R-tree test; 2 fail | KILLED
M9 T3 plan area without the half: `write_triangles` l.275 |
  TestAttributes::test_code_name_colour_heights_area_and_slope, "plan area,
  with the half"; and in the CLI file TestRunTable (sum(area_m2) against
  --stats), TestLandCoverClasses (areas sum to the domain) and TestLandCover;
  4 fail | KILLED
M10 T6 a lines column for a property set on no edge: `write_lines` l.317,
  the `if name in used` filter dropped |
  TestLines::test_a_property_set_on_no_edge_gets_no_column, `attributes ==
  {"length_m", "river", "road"}`; also test_the_diagonal_reads_back_... and
  the CLI's TestLines; 3 fail | KILLED

10 of 10 killed; no test added. T4 and T5 have no mutant named in the design.

Red-step scaffolding removed from the red commit 87f41c3's files:
- test_io_geopackage_write.py: the `gw` and `srs_entry` fixtures that
  imported the modules under test with `importlib.import_module` (so that
  collection succeeded before the module existed) are gone; the file imports
  `geopackage_write`, `srs_entry` and `SrsEntry` at the top like any other
  suite. The docstring paragraph explaining that mechanism, and the two
  "red" notes, are gone. The pins list stays as documentation.
- test_cli_mesh_gpkg.py: the two "red" notes in the docstring.
No throwaway tests, xfails or skips were planted for the red step.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
….py (@developer)

tests/python/test_io_repository.py::TestQ3OneModuleOpensFiles::
test_the_repository_opens_files_read_only found `unlink` in
io/repository.py's create_sqlite, added at the green step because
tests/python/test_io_geopackage.py::TestBoundaries lets only the
repository module call sqlite3.connect. Both boundaries now hold: the
repository is the one module that opens files, and outside CacheWriter
it deletes and writes nothing beyond the connection it hands back.

- io/repository.py: create_sqlite is `sqlite3.connect(path)` alone; the
  docstring says removing a stale file and renaming the finished one are
  the caller's.
- cli.py _write_geopackage: `part.unlink(missing_ok=True)` before
  create_sqlite, so a stale `<target>.part` is never appended to; the
  rename onto the target after the write was already there.

Verified in this worktree: full Python suite 7162 passed, 17 skipped
(7161 passed, 1 failed at dddce8e); test_cli_mesh_gpkg.py,
test_io_geopackage_write.py, test_io_geopackage.py and
test_io_repository.py pass explicitly; mypy strict clean; ruff check and
format clean; check_prohibited_deps, check_detria_boundary and
check_citations OK.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…lts recorded, with the Status line and ROADMAP row, by the main session

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ged, GeoPackage writer 3.93 s / +32 MB on Numedalslagen and 19.37 s / +188 MB on Romsdalen, ACCEPTED (@Perf)

Evidence in docs/benchmarks/2026-10-11/gpkg-out/ (README.md with the gate
table, the method and the comparison tables; the raw records and logs under
quick/ and writer/; the two scripts that produced them under scripts/).

Head e2470ae against its base 81a551b, AC power, caffeinated, nothing else
running. The branch touches no C++, so the quick check only.

(a) The worktree's stored baseline (39e3ae3, numpy 2.5.3) gave NO BASELINE
against this venv's numpy 2.5.4, so the base 81a551b was measured with
--tree in a detached scratch worktree first (--save-baseline), then head
against it: NO CHANGE, exit 0; totals -4.3 % to +1.6 %; every mesh_sha256
equal, and equal to master's refreshed baseline (357808b). Against master's
baseline Lagan is +37.7 % (features clip 6.34 -> 11.52 s): increment 35's
saving, which this branch does not contain (the base has the same 11.58 s);
the other cases are +2 to +10 %, which the base run of unchanged code also
shows against the afternoon's record. The stored baseline file was restored
to the branch's committed one afterwards.

(b) /usr/bin/time -l on .venv/bin/rasputin mesh ... --out mesh.gpkg --stats -
(the head run's Release _core installed in the venv first), one warm-up and
3 runs, medians: Numedalslagen write: geopackage 3.932 s (limit 5 s), file
203.3 MB; Romsdalen (tile 6901_3, --tolerance 10 --tolerance-slope 2 25 35)
19.368 s (limit 25 s), file 810.0 MB. Peak RSS of the whole command 2.77 GB
and 2.91 GB against 2.73 GB and 2.78 GB for the same commands writing .vtk;
the writer's own share, measured as the RSS growth inside its phase from a
0.1 s ps trace, +32 MB and +188 MB. Under 1 GB and 3 GB by either bound.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…AP row, by the main session

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…lied, by the main session

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ite the GeoPackage (@tester)

Code review round 1's blocker on increment 36 (GeoPackage output): a coded
`--features` source whose only feature is a line leaves `cover` empty at
cli.py:1146, and `write_land_cover(conn, [], ...)` reaches `_bounds` on a
zero-row array (io/geopackage_write.py:360 -> :172), raising
`ValueError: zero-size array to reduction operation minimum which has no
identity`. The same arguments write `.vtk` with exit 0; probed directly
through the CliRunner, since `invoke` swallows the exception and the test's
failure message shows only exit 1 after the mesh summary.

The test (T3, TestLandCover): a corine-mapped GeoJSON carrying one
LineString (code 122) across the SQUARE domain on the bumpy DEM. Pins:
- the run exits 0 and the `.vtk` twin matches (mesh_pair);
- `land_cover` is absent, both as a table and in `gpkg_contents` -- the
  reading of section 4's "written only when a coded source ran" that the
  reviewer left open (absent or empty); the alternative is one empty
  MultiPolygon layer, which has no envelope to register;
- `triangles.code` is 0 on every row (in no polygon) and equals the
  `.vtk`'s `land_cover_code` array;
- `land_cover_classes` has exactly one row: code 0, its palette name and
  colour, `triangles` = the triangle count.

Red: `pytest tests/python/test_cli_mesh_gpkg.py -k no_polygon_in_the_domain`
fails at mesh_pair's exit-code assertion; the other 20 tests in the file
pass. ruff check and ruff format --check clean on the file.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…kage without a land_cover layer (@developer)

Code review round 1's blocker on increment 36 (GeoPackage output): a coded
--features source that leaves no polygon inside the domain gives an empty
`cover` list (cli.py, the `coded is not None` branch), and
`write_land_cover(conn, [], ...)` crashed in `_bounds` on a zero-row array
(io/geopackage_write.py), so the `.gpkg` run exited 1 where the `.vtk` run
of the same arguments exited 0.

The guard is in `_write_geopackage` (cli.py): `write_land_cover` runs only
when `cover` is non-empty, and `write_styles` gets "land_cover" in its layer
list only in that case, so no `layer_styles` row names a layer that does not
exist. `write_classes` still runs: `land_cover_classes` carries the code-0
row ("in no polygon") with the triangle count. The writer module is
untouched; `write_land_cover`'s contract stays one row per polygon, with the
caller deciding whether there is anything to draw.

Makes tests/python/test_cli_mesh_gpkg.py::TestLandCover::
test_a_coded_run_with_no_polygon_in_the_domain_writes_without_a_land_cover_layer
(red in 55cc3c3) pass; no test file touched. The two increment 36 files and
test_io_repository.py: 84 passed, 1 skipped (Ola's DTM10 archive absent).
mypy strict, ruff check, ruff format --check, check_prohibited_deps,
check_detria_boundary and check_citations green.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…d_cover is written, by the main session

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ADMAP row, by the main session

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@skavhaug
skavhaug enabled auto-merge October 12, 2026 04:44
@skavhaug
skavhaug added this pull request to the merge queue Oct 12, 2026
Merged via the queue into master with commit fa6e485 Oct 12, 2026
15 checks passed
skavhaug added a commit that referenced this pull request Oct 12, 2026
…26a (@developer)

PR #232 (increment 26a, land-cover class raster in the cache) went DIRTY in
the merge queue after master moved. origin/master at a1aec50 brings two
merged PRs: #231 (increment 36, GeoPackage output, merge commit fa6e485)
and #230 (release hygiene, merge commit a1aec50). Merge commit, no rebase:
the branch is pushed and the PR is open.

One conflict, src_python/tin_engine/io/repository.py, in its module
docstring only. Master wrote the paragraph "Increment 36 adds
:func:`create_sqlite` ..." directly under the "Audit PR C" line; this branch
wrote the paragraph "Increment 26a adds the class raster's reads ..." after
a blank line. Resolved by keeping both, 36's first (it merged first), then
26a's, each as its side wrote it. The rest of the file auto-merged, and
both sides' code is present: CachedObject.fetched, CacheRepository
.load_class_window and .class_strips (26a), create_sqlite (36).

ROADMAP.md, cli.py and io/models.py auto-merged. ROADMAP rows 26 and 36 are
byte-identical to their own branch's text. In cli.py, 26a's --year option
and check_values on --dem and 36's .gpkg output path are disjoint
additions; in io/models.py, 26a's ClassWindow and 36's SrsEntry likewise.

Gates, in this worktree's venv, after the merge:
  ruff check .                           all checks passed
  ruff format --check .                  250 files already formatted
  mypy                                   no issues in 68 source files
  python3 tools/check_prohibited_deps.py OK, 399 files clean
  python3 tools/check_citations.py       exit 0; 60 at-risk lines listed,
                                         all of the form "this branch edits
                                         <file>" from the merged edits
  pytest tests/python/                   7236 passed, 17 skipped (environment
                                         skips only), coverage 99% (floor 85)
No test broke. No C++ change on either side; _core not rebuilt.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant