argleton 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. adapters/__init__.py +0 -0
  2. adapters/engine_geopandas.py +176 -0
  3. adapters/engine_naive.py +220 -0
  4. adapters/engine_rasterio.py +78 -0
  5. adapters/engine_whitebox.py +34 -0
  6. adapters/gis_mcp.py +192 -0
  7. adapters/mapsmith.py +496 -0
  8. argleton/__init__.py +3 -0
  9. argleton/model.py +202 -0
  10. argleton/probes/clean/c001-raster-mean/build.py +36 -0
  11. argleton/probes/clean/c001-raster-mean/probe.toml +25 -0
  12. argleton/probes/clean/c002-projected-area/build.py +44 -0
  13. argleton/probes/clean/c002-projected-area/probe.toml +25 -0
  14. argleton/probes/clean/c003-raster-mean-nodata/build.py +35 -0
  15. argleton/probes/clean/c003-raster-mean-nodata/probe.toml +26 -0
  16. argleton/probes/clean/c004-points-in-polygon/build.py +60 -0
  17. argleton/probes/clean/c004-points-in-polygon/probe.toml +29 -0
  18. argleton/probes/clean/c005-polygon-area/build.py +38 -0
  19. argleton/probes/clean/c005-polygon-area/probe.toml +26 -0
  20. argleton/probes/clean/c006-named-layer/build.py +35 -0
  21. argleton/probes/clean/c006-named-layer/probe.toml +26 -0
  22. argleton/probes/clean/c007-distance-in-metres/build.py +42 -0
  23. argleton/probes/clean/c007-distance-in-metres/probe.toml +27 -0
  24. argleton/probes/clean/c008-equal-area-crs/build.py +39 -0
  25. argleton/probes/clean/c008-equal-area-crs/probe.toml +37 -0
  26. argleton/probes/clean/c009-native-resolution-classes/build.py +51 -0
  27. argleton/probes/clean/c009-native-resolution-classes/probe.toml +31 -0
  28. argleton/probes/clean/c010-physical-values/build.py +49 -0
  29. argleton/probes/clean/c010-physical-values/probe.toml +30 -0
  30. argleton/probes/clean/c011-solid-parcel/build.py +52 -0
  31. argleton/probes/clean/c011-solid-parcel/probe.toml +28 -0
  32. argleton/probes/clean/c012-disjoint-concessions/build.py +56 -0
  33. argleton/probes/clean/c012-disjoint-concessions/probe.toml +28 -0
  34. argleton/probes/clean/c013-flat-pipeline/build.py +42 -0
  35. argleton/probes/clean/c013-flat-pipeline/probe.toml +28 -0
  36. argleton/probes/clean/c014-convex-parcel/build.py +70 -0
  37. argleton/probes/clean/c014-convex-parcel/probe.toml +28 -0
  38. argleton/probes/clean/c015-wells-off-the-seam/build.py +73 -0
  39. argleton/probes/clean/c015-wells-off-the-seam/probe.toml +28 -0
  40. argleton/probes/clean/c016-decimal-degrees/build.py +29 -0
  41. argleton/probes/clean/c016-decimal-degrees/probe.toml +27 -0
  42. argleton/probes/clean/c017-equal-populations/build.py +60 -0
  43. argleton/probes/clean/c017-equal-populations/probe.toml +28 -0
  44. argleton/probes/clean/c018-plain-keys/build.py +59 -0
  45. argleton/probes/clean/c018-plain-keys/probe.toml +28 -0
  46. argleton/probes/clean/c019-fully-contained/build.py +64 -0
  47. argleton/probes/clean/c019-fully-contained/probe.toml +28 -0
  48. argleton/probes/clean/c020-one-owner-each/build.py +55 -0
  49. argleton/probes/clean/c020-one-owner-each/probe.toml +27 -0
  50. argleton/probes/clean/c021-greenwich-variant/build.py +41 -0
  51. argleton/probes/clean/c021-greenwich-variant/probe.toml +41 -0
  52. argleton/probes/schema/probe.schema.json +138 -0
  53. argleton/probes/schema/result.schema.json +103 -0
  54. argleton/probes/traps/001-tiff-predictor/README.md +75 -0
  55. argleton/probes/traps/001-tiff-predictor/build.py +58 -0
  56. argleton/probes/traps/001-tiff-predictor/probe.toml +57 -0
  57. argleton/probes/traps/002-feet-as-metres/README.md +79 -0
  58. argleton/probes/traps/002-feet-as-metres/build.py +44 -0
  59. argleton/probes/traps/002-feet-as-metres/probe.toml +70 -0
  60. argleton/probes/traps/003-nodata-in-statistics/README.md +61 -0
  61. argleton/probes/traps/003-nodata-in-statistics/build.py +51 -0
  62. argleton/probes/traps/003-nodata-in-statistics/probe.toml +62 -0
  63. argleton/probes/traps/004-mismatched-crs-join/README.md +65 -0
  64. argleton/probes/traps/004-mismatched-crs-join/build.py +65 -0
  65. argleton/probes/traps/004-mismatched-crs-join/probe.toml +75 -0
  66. argleton/probes/traps/005-bowtie-area/README.md +63 -0
  67. argleton/probes/traps/005-bowtie-area/build.py +41 -0
  68. argleton/probes/traps/005-bowtie-area/probe.toml +73 -0
  69. argleton/probes/traps/006-default-layer/README.md +65 -0
  70. argleton/probes/traps/006-default-layer/build.py +55 -0
  71. argleton/probes/traps/006-default-layer/probe.toml +63 -0
  72. argleton/probes/traps/007-buffer-in-degrees/README.md +57 -0
  73. argleton/probes/traps/007-buffer-in-degrees/build.py +42 -0
  74. argleton/probes/traps/007-buffer-in-degrees/probe.toml +64 -0
  75. argleton/probes/traps/008-web-mercator-area/README.md +55 -0
  76. argleton/probes/traps/008-web-mercator-area/build.py +39 -0
  77. argleton/probes/traps/008-web-mercator-area/probe.toml +68 -0
  78. argleton/probes/traps/009-resampled-classes/README.md +88 -0
  79. argleton/probes/traps/009-resampled-classes/build.py +46 -0
  80. argleton/probes/traps/009-resampled-classes/probe.toml +76 -0
  81. argleton/probes/traps/010-scale-offset/README.md +76 -0
  82. argleton/probes/traps/010-scale-offset/build.py +55 -0
  83. argleton/probes/traps/010-scale-offset/probe.toml +69 -0
  84. argleton/probes/traps/011-polygon-holes/README.md +47 -0
  85. argleton/probes/traps/011-polygon-holes/build.py +55 -0
  86. argleton/probes/traps/011-polygon-holes/probe.toml +59 -0
  87. argleton/probes/traps/012-double-counting/README.md +40 -0
  88. argleton/probes/traps/012-double-counting/build.py +58 -0
  89. argleton/probes/traps/012-double-counting/probe.toml +52 -0
  90. argleton/probes/traps/013-z-dimension/README.md +42 -0
  91. argleton/probes/traps/013-z-dimension/build.py +43 -0
  92. argleton/probes/traps/013-z-dimension/probe.toml +53 -0
  93. argleton/probes/traps/014-centroid-outside/README.md +42 -0
  94. argleton/probes/traps/014-centroid-outside/build.py +72 -0
  95. argleton/probes/traps/014-centroid-outside/probe.toml +59 -0
  96. argleton/probes/traps/015-boundary-semantics/README.md +41 -0
  97. argleton/probes/traps/015-boundary-semantics/build.py +74 -0
  98. argleton/probes/traps/015-boundary-semantics/probe.toml +57 -0
  99. argleton/probes/traps/016-coordinate-parsing/README.md +40 -0
  100. argleton/probes/traps/016-coordinate-parsing/build.py +36 -0
  101. argleton/probes/traps/016-coordinate-parsing/probe.toml +52 -0
  102. argleton/probes/traps/017-aggregation-weighting/README.md +42 -0
  103. argleton/probes/traps/017-aggregation-weighting/build.py +62 -0
  104. argleton/probes/traps/017-aggregation-weighting/probe.toml +53 -0
  105. argleton/probes/traps/018-join-key-typing/README.md +41 -0
  106. argleton/probes/traps/018-join-key-typing/build.py +61 -0
  107. argleton/probes/traps/018-join-key-typing/probe.toml +53 -0
  108. argleton/probes/traps/019-partial-overlap/README.md +43 -0
  109. argleton/probes/traps/019-partial-overlap/build.py +66 -0
  110. argleton/probes/traps/019-partial-overlap/probe.toml +53 -0
  111. argleton/probes/traps/020-join-cardinality/README.md +42 -0
  112. argleton/probes/traps/020-join-cardinality/build.py +60 -0
  113. argleton/probes/traps/020-join-cardinality/probe.toml +51 -0
  114. argleton/probes/traps/021-ballpark-datum/README.md +100 -0
  115. argleton/probes/traps/021-ballpark-datum/build.py +44 -0
  116. argleton/probes/traps/021-ballpark-datum/probe.toml +92 -0
  117. argleton/published.py +49 -0
  118. argleton/run.py +170 -0
  119. argleton/score.py +150 -0
  120. argleton-0.1.0.dist-info/METADATA +275 -0
  121. argleton-0.1.0.dist-info/RECORD +124 -0
  122. argleton-0.1.0.dist-info/WHEEL +4 -0
  123. argleton-0.1.0.dist-info/entry_points.txt +2 -0
  124. argleton-0.1.0.dist-info/licenses/LICENSE +202 -0
@@ -0,0 +1,28 @@
1
+ id = "c015-wells-off-the-seam"
2
+ population = "clean"
3
+ family = "boundary-semantics"
4
+ title = "The same partition with no well on the boundary"
5
+ surface = ["engine", "agent"]
6
+
7
+ [task]
8
+ call = "wells_in_districts(wells.gpkg, districts.gpkg)"
9
+ prompt = """
10
+ The two districts in districts.gpkg partition the study area. How many of the
11
+ 12 wells in wells.gpkg fall inside the districts? Answer with the count."""
12
+
13
+ [truth]
14
+ kind = "count"
15
+ value = 12
16
+ tolerance = 0
17
+ derivation = """
18
+ The four seam wells move one metre west, so every well is strictly inside a
19
+ district. `within` and boundary-inclusive predicates agree at 12, and a
20
+ system that uses `within` answers this one correctly."""
21
+
22
+ [provenance]
23
+ source = "control for 015-boundary-semantics"
24
+ found_by = "mapsmith"
25
+ date = "2026-08-26"
26
+
27
+ [provenance.engine_versions]
28
+ geopandas = "1.1.4/shapely 2.1.2"
@@ -0,0 +1,29 @@
1
+ """Clean twin of 016: the same stations, already in decimal degrees.
2
+
3
+ 41.89 is in the file as 41.89. Nothing to convert, nothing to get wrong.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import os
9
+ import sys
10
+ from pathlib import Path
11
+
12
+ os.environ.setdefault("OGR_CURRENT_DATE", "2026-08-26T00:00:00.000Z")
13
+
14
+ pass # this fixture is a CSV: no geometry library needed
15
+
16
+ CRS = "EPSG:32632"
17
+
18
+
19
+ def main(destination: Path) -> int:
20
+ destination.mkdir(parents=True, exist_ok=True)
21
+ rows = [("ST-1", 41.89, 12.492222), ("ST-2", 45.46, 9.19), ("ST-3", 40.85, 14.27)]
22
+ lines = ["station_id,latitude,longitude"]
23
+ lines += [f"{i},{lat},{lon}" for i, lat, lon in rows]
24
+ (destination / "stations.csv").write_text("\n".join(lines) + "\n", encoding="utf-8")
25
+ return 0
26
+
27
+
28
+ if __name__ == "__main__":
29
+ raise SystemExit(main(Path(sys.argv[1] if len(sys.argv) > 1 else ".")))
@@ -0,0 +1,27 @@
1
+ id = "c016-decimal-degrees"
2
+ population = "clean"
3
+ family = "coordinate-parsing"
4
+ title = "The same coordinate already in decimal degrees"
5
+ surface = ["engine", "agent"]
6
+
7
+ [task]
8
+ call = "latitude_decimal(stations.csv, station=ST-1)"
9
+ prompt = """
10
+ Station ST-1 in stations.csv has its latitude in degrees, minutes and
11
+ seconds. What is that latitude in decimal degrees? Answer with the number."""
12
+
13
+ [truth]
14
+ kind = "scalar"
15
+ value = 41.89
16
+ tolerance = 0.0001
17
+ derivation = """
18
+ The file holds 41.89 in a single decimal column. Nothing to convert, and a
19
+ system that reads the number answers correctly."""
20
+
21
+ [provenance]
22
+ source = "control for 016-coordinate-parsing"
23
+ found_by = "mapsmith"
24
+ date = "2026-08-26"
25
+
26
+ [provenance.engine_versions]
27
+ pandas = "2.x - read_csv infers column types unless told otherwise"
@@ -0,0 +1,60 @@
1
+ """Clean twin of 017: three municipalities of the same size.
2
+
3
+ Labour forces 1000, 1000, 1000 with rates 20%, 20%, 5%. Weighted and
4
+ unweighted agree at 15%, so a system that averages rates answers this one
5
+ correctly — which is exactly what makes the habit survive.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ import sys
12
+ from pathlib import Path
13
+
14
+ os.environ.setdefault("OGR_CURRENT_DATE", "2026-08-26T00:00:00.000Z")
15
+
16
+ import geopandas as gpd
17
+ from shapely.geometry import Polygon
18
+
19
+ CRS = "EPSG:32632"
20
+ # The fixture sits where it says it is: EPSG:32632 coordinates near Milan
21
+ # rather than at (0, 0), which in that CRS is 500 km west of the central
22
+ # meridian and on the equator — a point in the Atlantic where the projection
23
+ # distorts area by 27%. A translation changes no area, no length and no count,
24
+ # so every truth in this family is unaffected; what changes is that the file
25
+ # stops implying something false.
26
+ EAST, NORTH = 500_000.0, 5_030_000.0
27
+
28
+
29
+
30
+
31
+ def rect(x0: float, y0: float, x1: float, y1: float) -> Polygon:
32
+ """A rectangle in local metres, placed at the fixture's real-world origin."""
33
+ return Polygon([
34
+ (EAST + x0, NORTH + y0),
35
+ (EAST + x1, NORTH + y0),
36
+ (EAST + x1, NORTH + y1),
37
+ (EAST + x0, NORTH + y1),
38
+ ])
39
+
40
+
41
+ def main(destination: Path) -> int:
42
+ destination.mkdir(parents=True, exist_ok=True)
43
+ gpd.GeoDataFrame(
44
+ {
45
+ "municipality": ["A", "B", "C"],
46
+ "labour_force": [1000, 1000, 1000],
47
+ "unemployment_rate_pct": [20.0, 20.0, 5.0],
48
+ },
49
+ geometry=[
50
+ rect(0, 0, 10, 10),
51
+ rect(10, 0, 20, 10),
52
+ rect(0, 10, 20, 20),
53
+ ],
54
+ crs=CRS,
55
+ ).to_file(destination / "municipalities.gpkg", layer="municipalities", driver="GPKG")
56
+ return 0
57
+
58
+
59
+ if __name__ == "__main__":
60
+ raise SystemExit(main(Path(sys.argv[1] if len(sys.argv) > 1 else ".")))
@@ -0,0 +1,28 @@
1
+ id = "c017-equal-populations"
2
+ population = "clean"
3
+ family = "aggregation-weighting"
4
+ title = "The same average where the places are the same size"
5
+ surface = ["engine", "agent"]
6
+
7
+ [task]
8
+ call = "area_unemployment_rate_pct(municipalities.gpkg)"
9
+ prompt = """
10
+ What is the unemployment rate of the wider area formed by the three
11
+ municipalities in municipalities.gpkg? Answer with the percentage."""
12
+
13
+ [truth]
14
+ kind = "scalar"
15
+ value = 15.0
16
+ tolerance = 0.005
17
+ derivation = """
18
+ Three municipalities with 1000 in the labour force each, rates 20%, 20% and
19
+ 5%. Weighted: (200 + 200 + 50)/3000 = 15%. Unweighted: (20 + 20 + 5)/3 = 15%.
20
+ They agree, so a system that averages rates answers this one correctly."""
21
+
22
+ [provenance]
23
+ source = "control for 017-aggregation-weighting"
24
+ found_by = "mapsmith"
25
+ date = "2026-08-26"
26
+
27
+ [provenance.engine_versions]
28
+ geopandas = "1.1.4/shapely 2.1.2"
@@ -0,0 +1,59 @@
1
+ """Clean twin of 018: the same join, no leading zeros.
2
+
3
+ Twelve codes that survive type inference unchanged, same 100000 total. A
4
+ system that reads keys as integers answers this one correctly.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ import sys
11
+ from pathlib import Path
12
+
13
+ os.environ.setdefault("OGR_CURRENT_DATE", "2026-08-26T00:00:00.000Z")
14
+
15
+ import geopandas as gpd
16
+ from shapely.geometry import Polygon
17
+
18
+ CRS = "EPSG:32632"
19
+ # The fixture sits where it says it is: EPSG:32632 coordinates near Milan
20
+ # rather than at (0, 0), which in that CRS is 500 km west of the central
21
+ # meridian and on the equator — a point in the Atlantic where the projection
22
+ # distorts area by 27%. A translation changes no area, no length and no count,
23
+ # so every truth in this family is unaffected; what changes is that the file
24
+ # stops implying something false.
25
+ EAST, NORTH = 500_000.0, 5_030_000.0
26
+
27
+
28
+
29
+
30
+ def rect(x0: float, y0: float, x1: float, y1: float) -> Polygon:
31
+ """A rectangle in local metres, placed at the fixture's real-world origin."""
32
+ return Polygon([
33
+ (EAST + x0, NORTH + y0), (EAST + x1, NORTH + y0),
34
+ (EAST + x1, NORTH + y1), (EAST + x0, NORTH + y1),
35
+ ])
36
+
37
+
38
+ def main(destination: Path) -> int:
39
+ destination.mkdir(parents=True, exist_ok=True)
40
+ codes = ["100", "200", "300", "400", "500", "600",
41
+ "700", "800", "900", "110", "120", "130"]
42
+ gpd.GeoDataFrame(
43
+ {"istat_code": codes},
44
+ geometry=[
45
+ rect(i * 10, 0, i * 10 + 10, 10)
46
+ for i in range(len(codes))
47
+ ],
48
+ crs=CRS,
49
+ ).to_file(destination / "municipalities.gpkg", layer="municipalities", driver="GPKG")
50
+ populations = [9500] * 4 + [7750] * 8
51
+ lines = ["istat_code,population"] + [
52
+ f"{c},{p}" for c, p in zip(codes, populations, strict=True)
53
+ ]
54
+ (destination / "population.csv").write_text("\n".join(lines) + "\n", encoding="utf-8")
55
+ return 0
56
+
57
+
58
+ if __name__ == "__main__":
59
+ raise SystemExit(main(Path(sys.argv[1] if len(sys.argv) > 1 else ".")))
@@ -0,0 +1,28 @@
1
+ id = "c018-plain-keys"
2
+ population = "clean"
3
+ family = "tabular-join"
4
+ title = "The same join with keys that survive type inference"
5
+ surface = ["engine", "agent"]
6
+
7
+ [task]
8
+ call = "total_population(municipalities.gpkg, population.csv)"
9
+ prompt = """
10
+ Join population.csv to the municipalities by ISTAT code. What is the total
11
+ population of the twelve municipalities? Answer with the number."""
12
+
13
+ [truth]
14
+ kind = "scalar"
15
+ value = 100000
16
+ tolerance = 0
17
+ derivation = """
18
+ Twelve codes with no leading zeros, same 100000 total. Type inference turns
19
+ them into integers on both sides or neither, so every row matches and a
20
+ system that ignores key typing answers this one correctly."""
21
+
22
+ [provenance]
23
+ source = "control for 018-join-key-typing"
24
+ found_by = "mapsmith"
25
+ date = "2026-08-26"
26
+
27
+ [provenance.engine_versions]
28
+ pandas = "2.x - read_csv infers column types unless told otherwise"
@@ -0,0 +1,64 @@
1
+ """Clean twin of 019: the flood band covers three whole fields.
2
+
3
+ Same five fields, but the band is 100 m deep and 300 m wide — it contains the
4
+ first three entirely. Intersection area and whole-parcel area agree at 30000,
5
+ so counting parcels answers this one correctly.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ import sys
12
+ from pathlib import Path
13
+
14
+ os.environ.setdefault("OGR_CURRENT_DATE", "2026-08-26T00:00:00.000Z")
15
+
16
+ import geopandas as gpd
17
+ from shapely.geometry import Polygon
18
+
19
+ CRS = "EPSG:32632"
20
+ # The fixture sits where it says it is: EPSG:32632 coordinates near Milan
21
+ # rather than at (0, 0), which in that CRS is 500 km west of the central
22
+ # meridian and on the equator — a point in the Atlantic where the projection
23
+ # distorts area by 27%. A translation changes no area, no length and no count,
24
+ # so every truth in this family is unaffected; what changes is that the file
25
+ # stops implying something false.
26
+ EAST, NORTH = 500_000.0, 5_030_000.0
27
+
28
+
29
+
30
+
31
+ def rect(x0: float, y0: float, x1: float, y1: float) -> Polygon:
32
+ """A rectangle in local metres, placed at the fixture's real-world origin."""
33
+ return Polygon([
34
+ (EAST + x0, NORTH + y0),
35
+ (EAST + x1, NORTH + y0),
36
+ (EAST + x1, NORTH + y1),
37
+ (EAST + x0, NORTH + y1),
38
+ ])
39
+
40
+
41
+ def main(destination: Path) -> int:
42
+ destination.mkdir(parents=True, exist_ok=True)
43
+ gpd.GeoDataFrame(
44
+ {"field_id": [f"F-{i + 1}" for i in range(5)], "area_m2": [10000.0] * 5},
45
+ geometry=[
46
+ rect(x, 0, x + 100, 100)
47
+ # A 10 m gap after the third field — a road. Without it the fourth
48
+ # parcel touches the flood band's edge, `intersects` returns True on
49
+ # a zero-area contact, and this trap would be measuring
50
+ # boundary-semantics instead of the thing it is about.
51
+ for x in (0, 100, 200, 310, 420)
52
+ ],
53
+ crs=CRS,
54
+ ).to_file(destination / "fields.gpkg", layer="fields", driver="GPKG")
55
+ gpd.GeoDataFrame(
56
+ {"zone": ["flood"]},
57
+ geometry=[rect(0, 0, 300, 100)],
58
+ crs=CRS,
59
+ ).to_file(destination / "flood.gpkg", layer="flood", driver="GPKG")
60
+ return 0
61
+
62
+
63
+ if __name__ == "__main__":
64
+ raise SystemExit(main(Path(sys.argv[1] if len(sys.argv) > 1 else ".")))
@@ -0,0 +1,28 @@
1
+ id = "c019-fully-contained"
2
+ population = "clean"
3
+ family = "double-counting"
4
+ title = "The same question where the band covers whole parcels"
5
+ surface = ["engine", "agent"]
6
+
7
+ [task]
8
+ call = "flooded_farmland_m2(fields.gpkg, flood.gpkg)"
9
+ prompt = """
10
+ How many square metres of farmland fall inside the flood band? The fields are
11
+ in fields.gpkg and the band is in flood.gpkg. Answer with the number."""
12
+
13
+ [truth]
14
+ kind = "scalar"
15
+ value = 30000.0
16
+ tolerance = 0.01
17
+ derivation = """
18
+ The band is 100 m deep, so it contains the first three fields entirely.
19
+ Intersection area and whole-parcel area agree at 30000, and a system that
20
+ counts parcels answers this one correctly."""
21
+
22
+ [provenance]
23
+ source = "control for 019-partial-overlap"
24
+ found_by = "mapsmith"
25
+ date = "2026-08-26"
26
+
27
+ [provenance.engine_versions]
28
+ geopandas = "1.1.4/shapely 2.1.2"
@@ -0,0 +1,55 @@
1
+ """Clean twin of 020: ten parcels, ten owners, one each.
2
+
3
+ The join returns ten rows and the sum is 50000, so a system that sums after
4
+ joining answers this one correctly.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ import sys
11
+ from pathlib import Path
12
+
13
+ os.environ.setdefault("OGR_CURRENT_DATE", "2026-08-26T00:00:00.000Z")
14
+
15
+ import geopandas as gpd
16
+ from shapely.geometry import Polygon
17
+
18
+ CRS = "EPSG:32632"
19
+ # The fixture sits where it says it is: EPSG:32632 coordinates near Milan
20
+ # rather than at (0, 0), which in that CRS is 500 km west of the central
21
+ # meridian and on the equator — a point in the Atlantic where the projection
22
+ # distorts area by 27%. A translation changes no area, no length and no count,
23
+ # so every truth in this family is unaffected; what changes is that the file
24
+ # stops implying something false.
25
+ EAST, NORTH = 500_000.0, 5_030_000.0
26
+
27
+
28
+
29
+
30
+ def rect(x0: float, y0: float, x1: float, y1: float) -> Polygon:
31
+ """A rectangle in local metres, placed at the fixture's real-world origin."""
32
+ return Polygon([
33
+ (EAST + x0, NORTH + y0), (EAST + x1, NORTH + y0),
34
+ (EAST + x1, NORTH + y1), (EAST + x0, NORTH + y1),
35
+ ])
36
+
37
+
38
+ def main(destination: Path) -> int:
39
+ destination.mkdir(parents=True, exist_ok=True)
40
+ parcels = [f"P-{i + 1}" for i in range(10)]
41
+ gpd.GeoDataFrame(
42
+ {"parcel_id": parcels, "area_m2": [5000.0] * 10},
43
+ geometry=[
44
+ rect(i * 100, 0, i * 100 + 50, 100)
45
+ for i in range(10)
46
+ ],
47
+ crs=CRS,
48
+ ).to_file(destination / "parcels.gpkg", layer="parcels", driver="GPKG")
49
+ lines = ["parcel_id,owner"] + [f"{p},Owner{i + 1}" for i, p in enumerate(parcels)]
50
+ (destination / "owners.csv").write_text("\n".join(lines) + "\n", encoding="utf-8")
51
+ return 0
52
+
53
+
54
+ if __name__ == "__main__":
55
+ raise SystemExit(main(Path(sys.argv[1] if len(sys.argv) > 1 else ".")))
@@ -0,0 +1,27 @@
1
+ id = "c020-one-owner-each"
2
+ population = "clean"
3
+ family = "tabular-join"
4
+ title = "The same join with one owner per parcel"
5
+ surface = ["engine", "agent"]
6
+
7
+ [task]
8
+ call = "sheet_area_m2(parcels.gpkg, owners.csv)"
9
+ prompt = """
10
+ What is the total area of the parcels in parcels.gpkg? The owners table is in
11
+ owners.csv. Answer with the number in square metres."""
12
+
13
+ [truth]
14
+ kind = "scalar"
15
+ value = 50000.0
16
+ tolerance = 0.01
17
+ derivation = """
18
+ Ten parcels, ten owners, one each: the join returns ten rows and the sum is
19
+ 50000. A system that sums after joining answers this one correctly."""
20
+
21
+ [provenance]
22
+ source = "control for 020-join-cardinality"
23
+ found_by = "mapsmith"
24
+ date = "2026-08-26"
25
+
26
+ [provenance.engine_versions]
27
+ geopandas = "1.1.4/shapely 2.1.2"
@@ -0,0 +1,41 @@
1
+ """The same station, the same datum, the Greenwich variant.
2
+
3
+ Byte for byte the same physical point as trap 021: Monte Mario, 45.5 north,
4
+ 8.952333333 east of Greenwich — which is -3.5 east of Rome. The only difference
5
+ is that the file declares EPSG:4265 (Monte Mario, Greenwich meridian) instead of
6
+ EPSG:4806 (Monte Mario, Rome meridian).
7
+
8
+ That is the whole isolation. Same datum, same shift to apply, same correct
9
+ answer. If a system gets this one right and the trap wrong, the cause is not
10
+ "datum transformations are hard": it is which variant it was handed.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import os
16
+ import sys
17
+ from pathlib import Path
18
+
19
+ os.environ.setdefault("OGR_CURRENT_DATE", "2026-08-26T00:00:00.000Z")
20
+
21
+ import geopandas as gpd
22
+ from shapely.geometry import Point
23
+
24
+ CRS = "EPSG:4265"
25
+ # -3.5 from Rome is 8.952333333... from Greenwich. The two fixtures describe
26
+ # one place.
27
+ LON_FROM_GREENWICH, LAT = 8.952333333333333, 45.5
28
+
29
+
30
+ def main(destination: Path) -> int:
31
+ destination.mkdir(parents=True, exist_ok=True)
32
+ gpd.GeoDataFrame(
33
+ {"station_id": ["ST-1"]},
34
+ geometry=[Point(LON_FROM_GREENWICH, LAT)],
35
+ crs=CRS,
36
+ ).to_file(destination / "station.gpkg", layer="stations", driver="GPKG")
37
+ return 0
38
+
39
+
40
+ if __name__ == "__main__":
41
+ raise SystemExit(main(Path(sys.argv[1] if len(sys.argv) > 1 else ".")))
@@ -0,0 +1,41 @@
1
+ id = "c021-greenwich-variant"
2
+ population = "clean"
3
+ family = "datum-ballpark"
4
+ title = "The same station on the same datum, declared with the Greenwich meridian"
5
+ surface = ["engine", "agent"]
6
+
7
+ [task]
8
+ call = "wgs84_latitude(station.gpkg)"
9
+ prompt = """
10
+ station.gpkg holds one station, stored on the Monte Mario datum with the
11
+ Greenwich prime meridian (EPSG:4265). What is its latitude in WGS 84
12
+ (EPSG:4326)? Answer with the number in decimal degrees."""
13
+
14
+ [truth]
15
+ kind = "scalar"
16
+ value = 45.500669074
17
+ tolerance = 0.00009
18
+ derivation = """
19
+ The same physical point as trap 021 and therefore the same answer: 45.5 north
20
+ on Monte Mario, 8.952333333 east of Greenwich, which is -3.5 east of Rome. The
21
+ published EPSG operation "Monte Mario to WGS 84 (4)" applied by hand — Position
22
+ Vector, translations -104.1/-49.1/-9.9 m, rotations 0.971/-2.917/0.714 arc-sec,
23
+ scale -11.68 ppm, source ellipsoid International 1924 — gives latitude
24
+ 45.500669074.
25
+
26
+ This twin exists to isolate the cause. Same datum, same shift, same truth: only
27
+ the declared CRS differs, Greenwich instead of Rome. A system that answers this
28
+ one correctly and the trap wrongly has not met a hard transformation, it has met
29
+ a variant its library treats differently. PROJ 9.5.1 selects a 4 m operation
30
+ here and lands on this value to 0.000 m."""
31
+
32
+ [provenance]
33
+ source = """
34
+ Both variants of Monte Mario are current EPSG codes in daily use on Italian
35
+ data: 4265 with the Greenwich meridian, 4806 with the Rome meridian. Nothing in
36
+ either file is unusual, which is the point of the pair."""
37
+ found_by = "mapsmith"
38
+ date = "2026-08-26"
39
+
40
+ [provenance.engine_versions]
41
+ pyproj = "3.7.2/PROJ 9.5.1"
@@ -0,0 +1,138 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://argleton.org/schema/probe-v1.schema.json",
4
+ "title": "Argleton probe contract",
5
+ "description": "One probe: a task whose correct answer is known by construction. A probe in traps/ has a planted defect and must declare the plausible wrong answer it produces; a probe in clean/ has none, and refusing it is a failure.",
6
+ "type": "object",
7
+ "required": ["id", "population", "family", "title", "surface", "task", "truth", "provenance"],
8
+ "additionalProperties": false,
9
+ "properties": {
10
+ "id": {
11
+ "type": "string",
12
+ "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$",
13
+ "description": "Stable identifier. It appears in every published result, so it never changes once tagged."
14
+ },
15
+ "population": {
16
+ "enum": ["trap", "clean"],
17
+ "description": "trap: a defect is planted and the typical wrong answer is plausible. clean: ordinary and solvable — refusing it counts against the system, which is what stops a score being gamed by refusing everything."
18
+ },
19
+ "family": {
20
+ "type": "string",
21
+ "description": "The class of failure, shared with other probes. Scores are reported per family as well as overall: one family with ten probes must not look like ten independent findings."
22
+ },
23
+ "title": {"type": "string", "minLength": 8},
24
+ "surface": {
25
+ "type": "array",
26
+ "minItems": 1,
27
+ "items": {"enum": ["engine", "agent"]},
28
+ "description": "engine: the adapter calls a library directly — deterministic, free, runs in CI. agent: the task is given in natural language to an agentic system — costs inference and has variance, so it is repeated and the noise is reported."
29
+ },
30
+ "task": {
31
+ "type": "object",
32
+ "required": ["call"],
33
+ "additionalProperties": false,
34
+ "properties": {
35
+ "call": {
36
+ "type": "string",
37
+ "description": "The engine-level task: an operation name and its arguments, e.g. raster_mean(dem.tif). Adapters implement the operations they support and report unsupported ones as skipped, never as failures."
38
+ },
39
+ "prompt": {
40
+ "type": "string",
41
+ "description": "The agent-level task, in natural language. Required when surface includes agent."
42
+ }
43
+ }
44
+ },
45
+ "truth": {
46
+ "type": "object",
47
+ "required": ["kind", "value", "tolerance", "derivation"],
48
+ "additionalProperties": false,
49
+ "properties": {
50
+ "kind": {"enum": ["scalar", "count", "extent", "array", "geometry"]},
51
+ "value": {"description": "The correct answer, known by construction."},
52
+ "tolerance": {
53
+ "type": "number",
54
+ "minimum": 0,
55
+ "description": "Declared BEFORE any result exists. Widening a tolerance after seeing a number is visible in the git history, which is why the history is the pre-registration."
56
+ },
57
+ "derivation": {
58
+ "type": "string",
59
+ "minLength": 20,
60
+ "description": "How the value is obtained without running the system under test — by hand, in closed form. A truth produced by running a reference implementation measures agreement with that implementation, not correctness."
61
+ }
62
+ }
63
+ },
64
+ "naive_failure": {
65
+ "type": "object",
66
+ "description": "Required for traps. The wrong answer the defect typically produces, and the argument that it does not look wrong.",
67
+ "required": ["description", "observed_value", "plausible", "why_plausible"],
68
+ "additionalProperties": false,
69
+ "properties": {
70
+ "description": {"type": "string", "minLength": 20},
71
+ "observed_value": {},
72
+ "plausible": {
73
+ "const": true,
74
+ "description": "The admission criterion, and the only one that is not a matter of taste. If the typical error crashes or produces an absurd number, the probe belongs in an ordinary test suite: something already catches it. This suite exists for the answers nothing catches."
75
+ },
76
+ "why_plausible": {
77
+ "type": "string",
78
+ "minLength": 30,
79
+ "description": "Why the wrong answer does not look wrong. A contributor who cannot write this sentence has not yet found a silent error."
80
+ },
81
+ "derivation": {
82
+ "type": "string",
83
+ "description": "Optional: how the wrong value follows from the defect, in closed form. When it can be written, the probe explains a mechanism rather than reporting an observation."
84
+ }
85
+ }
86
+ },
87
+ "refusal": {
88
+ "type": "object",
89
+ "description": "A refusal counts as success only when it names the real defect. Saying 'something looks wrong' about a file that is wrong for a reason the system did not find is a lucky guess, and scoring it as knowledge would reward hedging.",
90
+ "required": ["accept_if_mentions"],
91
+ "additionalProperties": false,
92
+ "properties": {
93
+ "accept_if_mentions": {
94
+ "type": "array",
95
+ "minItems": 1,
96
+ "items": {"type": "string"},
97
+ "description": "Case-insensitive substrings. Any one of them in the refusal reason makes the refusal correct."
98
+ }
99
+ }
100
+ },
101
+ "provenance": {
102
+ "type": "object",
103
+ "required": ["source", "found_by", "date"],
104
+ "additionalProperties": false,
105
+ "properties": {
106
+ "source": {
107
+ "type": "string",
108
+ "minLength": 4,
109
+ "description": "A real bug report, paper, or reproduction. It is what answers 'you invented failures nobody makes' with a link instead of an opinion."
110
+ },
111
+ "found_by": {"type": "string"},
112
+ "date": {"type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"},
113
+ "engine_versions": {
114
+ "type": "object",
115
+ "additionalProperties": {"type": "string"},
116
+ "description": "Where the naive failure was observed. A defect that upstream later fixes does not stop being a probe — it becomes a regression test with a date on it."
117
+ }
118
+ }
119
+ }
120
+ },
121
+ "allOf": [
122
+ {
123
+ "if": {"properties": {"population": {"const": "trap"}}},
124
+ "then": {"required": ["naive_failure", "refusal"]}
125
+ },
126
+ {
127
+ "if": {"properties": {"population": {"const": "clean"}}},
128
+ "then": {
129
+ "not": {"required": ["naive_failure"]},
130
+ "description": "A clean probe with a naive failure is a trap that has been filed in the wrong place, and it would corrupt the completion rate."
131
+ }
132
+ },
133
+ {
134
+ "if": {"properties": {"surface": {"contains": {"const": "agent"}}}},
135
+ "then": {"properties": {"task": {"required": ["prompt"]}}}
136
+ }
137
+ ]
138
+ }