diff --git a/docs/jts-1145-pip-lane-2026-08.md b/docs/jts-1145-pip-lane-2026-08.md
new file mode 100644
index 00000000..434dbd7b
--- /dev/null
+++ b/docs/jts-1145-pip-lane-2026-08.md
@@ -0,0 +1,161 @@
+# PIP gallery lane — JTS #1145 YStripesPointInAreaLocator
+
+**Date**: 2026-08-05.
+**topic:** `relate` · epic **#67** (PIP / RelateNG consumers; JCT gallery as regression oracle)
+**Upstream**: [locationtech/jts#1145](https://github.com/locationtech/jts/pull/1145)
+ *Implement `YStripesPointInAreaLocator`* (micycle1) — new indexed
+ point-in-area locator using Y-stripe segment buckets + DD ray-crossing
+ ([tg POLYGON_INDEXING](https://github.com/tidwall/tg/blob/main/docs/POLYGON_INDEXING.md#ystripes)).
+**NTS twin**: none yet; production baseline is
+ `IndexedPointInAreaLocator` / `SimplePointInAreaLocator`.
+**Scope**: Run the corpus PIP gallery
+ ([`nts-oracle-gallery.md`](nts-oracle-gallery.md)) as a **differential
+ soundness gate** against a faithful C# port of the PR tip. **No new Rocq
+ theorems.**
+
+**Verdict**: **GREEN** — on **14** gallery vectors (vertex-graze, horizontal
+edge, rect/pixel boundary, Spectre pocket, Hat pocket), the Y-stripes port
+returns the **geometric correct answer** and agrees with NTS
+`IndexedPointInAreaLocator` and `SimplePointInAreaLocator` on every row
+(including OGC `BOUNDARY` where applicable). Safe soundness posture for
+merge from the proofs-corpus gallery; recommend pinning the WKT cases into
+`AbstractPointInRingTest` / a dedicated test on the Java side.
+
+---
+
+## §1 — What #1145 adds
+
+| File | Role |
+|---|---|
+| `YStripesPointInPolygonLocator` | Per-ring Y-stripe index + ray-crossing (`locateYStripes`) |
+| `YStripesPointInAreaLocator` | Multi-polygon STRtree of per-polygon locators; single-poly fast path |
+| `YStripesPointInAreaLocatorTest` | Extends `AbstractPointInRingTest` |
+| Perf / stress tests | Circle + perturbed grid |
+
+Core locate path (per ring):
+
+1. Envelope reject (`coversPointFast`).
+2. Map `py` → stripe; scan only segments mapped to that stripe.
+3. **Horizontal** edges (`ay == by`): never flip parity; `ON` iff `py == ay` and `px ∈ [xmin,xmax]`.
+4. Segment entirely right of query (`px < xmin`): flip iff half-open y-straddle `(ay > py) != (by > py)`.
+5. Segment entirely left (`px > xmax`): ignore.
+6. Otherwise `raycast` via **`CGAlgorithmsDD.orientationIndex`**; collinear-in-bbox → `BOUNDARY`; orient after normalizing edge direction → cross or miss.
+7. Holes: shell `INTERIOR` then hole `INTERIOR` ⇒ `EXTERIOR`; hole `BOUNDARY` ⇒ `BOUNDARY`.
+
+PR claims: **4×+** faster than `IndexedPointInAreaLocator` on author benches; passes existing `AbstractPointInRingTest` suite.
+
+---
+
+## §2 — Corpus ground truth
+
+| Asset | Role |
+|---|---|
+| [`docs/nts-oracle-gallery.md`](nts-oracle-gallery.md) | WKT + geometric answers + theorem names |
+| `Overlay.point_in_ring` | Pure rightward-ray odd parity (strict y-straddle) |
+| `JCT_VertexGrazingCounterexample.v` | Diamond centre: naive even, geometric **interior** |
+| `JCT_HorizontalEdgeCounterexample.v` | Notch left exterior: naive odd, geometric **exterior** |
+| `RectangleJCT.point_in_ring_rect_iff` | Half-open rect parity vs OGC boundary |
+| `HotPixelConvexRing.v` | Pixel box + bottom-edge graze |
+| `SpectreConcaveFamily.v` / `HatMonotile*.v` | Concave hull-interior exterior pockets |
+| `WindingNumber.winding_decides_membership` | Parity ≡ winding under horizontal-edge guard |
+
+**Convention note.** Gallery **GT** for production locators is the **geometric /
+OGC Location** answer (`INTERIOR` / `EXTERIOR` / `BOUNDARY`), not bare parity.
+Rows tagged `naive-parity≠geom` are exactly the robustness cases a correct
+counter must not get wrong.
+
+---
+
+## §3 — Deliverables
+
+| Path | Purpose |
+|---|---|
+| `tests/Discussion839Mre/YStripesPointInAreaLocator.cs` | Faithful C# port of PR tip (DD orient via NTS `CGAlgorithmsDD`) |
+| `tests/Discussion839Mre/Program.cs` `--jts1145` | Gallery differential harness |
+| `oracle/jts1145_pip_gallery_vectors.txt` | Machine table from one GREEN run |
+| `tests/Discussion839Mre/jts-1145-comment.md` | Optional JTS PR comment draft |
+| this doc | Lane write-up |
+
+```text
+dotnet run --project tests/Discussion839Mre -c Release -- --jts1145
+# aliases: --ystripes --pip-gallery
+```
+
+---
+
+## §4 — Results (2026-08-05, NTS 2.6)
+
+| id | point | GT | naive parity | Indexed | Simple | YStripes | status |
+|---|---|---|---|---|---|---|---|
+| VGRAZE_G | (0, 0.5) | INT | odd | INT | INT | INT | PASS |
+| **VGRAZE0** | **(0, 0)** | **INT** | **even\*** | INT | INT | INT | PASS |
+| **HORZ_E** | **(−1, 1)** | **EXT** | **odd\*** | EXT | EXT | EXT | PASS |
+| RECT_IN | (2, 1.5) | INT | in | INT | INT | INT | PASS |
+| RECT_L | (0, 1.5) | BND | parity-in | BND | BND | BND | PASS |
+| RECT_R | (4, 1.5) | BND | parity-out | BND | BND | BND | PASS |
+| RECT_B | (2, 0) | BND | parity-out | BND | BND | BND | PASS |
+| RECT_T | (2, 3) | BND | parity-out | BND | BND | BND | PASS |
+| PIX_C | (0, 0) | INT | in | INT | INT | INT | PASS |
+| PIX_BOT | (0, −0.5) | BND | parity-out\* | BND | BND | BND | PASS |
+| SPECT_IN | (5, 0.5) | INT | odd | INT | INT | INT | PASS |
+| **SPECT_POCK** | **(3.5, 0.5)** | **EXT** | even | EXT | EXT | EXT | PASS |
+| HAT_IN | ≈(4.25, 2.165) | INT | odd | INT | INT | INT | PASS |
+| **HAT_POCK** | ≈(3.5, 0.433) | **EXT** | even | EXT | EXT | EXT | PASS |
+
+**14 / 14 PASS** · **0** YStripes≠GT · **0** Y≠Indexed · **0** Y≠Simple.
+
+Critical robustness rows (bold) all GREEN:
+
+| Vector | Why it matters | Result |
+|---|---|---|
+| Vertex graze diamond centre | Naive parity **false negative** (even) | YStripes **INT** |
+| Horizontal edge left of notch | Naive parity **false positive** (odd) | YStripes **EXT** |
+| Spectre reflex pocket | Concave / hull-interior exterior | YStripes **EXT** |
+| Hat bottom pocket (APPROX √3 WKT) | Same class, irrational embedding | YStripes **EXT** |
+
+Hat coordinates are gallery `APPROX` (exact theorems use √3); double WKT still
+separates interior vs pocket under all three locators.
+
+---
+
+## §5 — Algorithm notes vs corpus
+
+| Topic | YStripes (#1145) | Corpus `point_in_ring` |
+|---|---|---|
+| Y-straddle | half-open `(ay > py) != (by > py)` | strict `min < py < max` (similar effect) |
+| Horizontal at query y | boundary if on segment; **no** parity flip | not a crossing (guard `no_horizontal_edge_at` for correctness) |
+| Vertex graze | DD collinear → `ON` / directed orient | bare parity can miscount without `ray_avoids_vertices` |
+| Output | OGC `Location` incl. `BOUNDARY` | pure bool interior parity |
+| Index | Y-stripes (O(1) stripe + local segs) | none (specification predicate) |
+
+Y-stripes is a **consumer** of the same geometric fact the gallery pins; it is
+not a new mathematical predicate. No `Admitted` / claim registration required.
+
+---
+
+## §6 — Product / review recommendation
+
+1. **Soundness (gallery)**: GREEN — merge is not blocked by vertex-graze /
+ horizontal-edge / concave-pocket classes covered here.
+2. **Tests**: Existing `AbstractPointInRingTest` is necessary but thin on
+ concave monotiles and the diamond graze. Suggest adding the gallery WKT
+ pins (copy-paste from `nts-oracle-gallery.md` or this table) to
+ `YStripesPointInAreaLocatorTest` or the abstract suite so both Indexed and
+ YStripes stay locked.
+3. **Port watch**: NTS should consider a twin after JTS merge (same package
+ layout under `Algorithm.Locate`).
+4. **Limits of this lane**: C# port + NTS DD orient; not a Java bytecode run
+ of the PR branch. Behaviour is intentionally line-faithful; a Java re-run
+ of the same WKT in `YStripesPointInAreaLocatorTest` would close residual
+ port risk. Stress tests already on the PR cover random grids.
+
+---
+
+## §7 — Sources
+
+- JTS PR tip: `micycle1/jts` branch `ystripes` (fetched 2026-08-05)
+- Corpus gallery: `docs/nts-oracle-gallery.md`
+- Scout ranking: `docs/jts-open-prs-scout-2026-08.md` item **#7** / work seq §3
+
+**AI assistance**: Grok (grok-4.5), human-directed.
+**License**: project documentation (BSD-3-Clause corpus).
diff --git a/docs/jts-open-prs-scout-2026-08.md b/docs/jts-open-prs-scout-2026-08.md
index 7ff89186..50aa0f22 100644
--- a/docs/jts-open-prs-scout-2026-08.md
+++ b/docs/jts-open-prs-scout-2026-08.md
@@ -40,7 +40,7 @@ Ordered by **risk÷cost** for the proofs corpus (not JTS merge politics).
| **4** | [#1093](https://github.com/locationtech/jts/pull/1093) | Change Shewchuk Orientation filter to Ozaki et al. | **#66/#64** `precision`/`core` | H | M | H | Production `CGAlgorithmsDD` filter. **Lane GREEN 2026-08-05:** 45-vector three-way gate (master `1e-15` / Ozaki / corpus Shewchuk) vs `ORIENT_EXACT` — 0 CERTAIN conflicts; Ozaki tighter than master on 4 same-sign band cases. See [`jts-1093-orient-lane-2026-08.md`](jts-1093-orient-lane-2026-08.md). |
| **5** | [#1197](https://github.com/locationtech/jts/pull/1197) | Characterize DD orientation soundness (#1106) | **#66** `precision` | H | L | M | **Ours.** Test-only + `RocqRefRunner`. **Keep-green 2026-08-05 GREEN:** rebased onto master; corpus integer pins refreshed; local 22/22 + GHA `build-and-test` pass; merge ping posted ([comment](https://github.com/locationtech/jts/pull/1197#issuecomment-5186644725)). Awaiting maintainer review/merge. |
| **6** | [#90](https://github.com/locationtech/jts/pull/90) | Fix ScaledNoder behaviour (draft) | **#66** `precision` · **P1 policy** | H | L | M | **Lane YELLOW 2026-08-05 (not P0 theory):** mukoki scale=1 mixed-precision **masked** by modern `SnapRoundingNoder`; scale=0 `round(x*0)` footgun live; do **not** merge draft as written. No new snap claim. See [`jts-90-scalednoder-lane-2026-08.md`](jts-90-scalednoder-lane-2026-08.md). |
-| **7** | [#1145](https://github.com/locationtech/jts/pull/1145) | YStripesPointInAreaLocator | **#67** `relate` (+ JCT) | H | L–M | M | New PIP implementation. Corpus `point_in_ring` + gallery (`nts-oracle-gallery.md`) is the **regression oracle** — run hat/Spectre/vertex-graze/horizontal-edge vectors. |
+| **7** | [#1145](https://github.com/locationtech/jts/pull/1145) | YStripesPointInAreaLocator | **#67** `relate` (+ JCT) | H | L–M | M | **Lane GREEN 2026-08-05:** gallery 14/14 (vertex-graze/horizontal/Spectre/Hat) — YStripes port ≡ Indexed ≡ Simple ≡ geometric GT. See [`jts-1145-pip-lane-2026-08.md`](jts-1145-pip-lane-2026-08.md). |
| **8** | [#1084](https://github.com/locationtech/jts/pull/1084) | CoverageEdgeExtractor | **#425** `coverage` | M–H | M | M | Unique edges from polygonal coverage. Corpus has `CoverageGapOverlapCleaner.v` / #425. Good **next coverage claim** after cleaner. |
| **9** | [#1216](https://github.com/locationtech/jts/pull/1216) | Explicit stack in DouglasPeucker (#1127) | **#69** S-* | M | L | L–M | **Ours.** Stack overflow fix; behaviour-preserving. Corpus `Simplify.v` is structural only — **no new proof** needed; port to NTS when merged. |
| **10** | [#1140](https://github.com/locationtech/jts/pull/1140) | Hausdorff distance percentile | **#423** `metric` | M | M | L–M | Extends discrete Hausdorff. Corpus `HausdorffDiscrete.v` / Claim423a pins directed max-min — can pin percentile semantics or mark out-of-scope. |
@@ -81,7 +81,12 @@ Ordered by **risk÷cost** for the proofs corpus (not JTS merge politics).
`feature/orientation-robustness-tests` onto `master`; refreshed
`orientation_proof_vectors.txt` integer pins; local suite 22/22 + GHA
`build-and-test` pass; merge-ping comment posted.
-3. **PIP gallery on #1145 (P0/P1)** — Reuse `docs/nts-oracle-gallery.md` WKT as Java tests / differential.
+3. **PIP gallery on #1145 (P0/P1)** — **LANDED 2026-08-05 GREEN:**
+ - Faithful C# port: `tests/Discussion839Mre/YStripesPointInAreaLocator.cs`
+ - Harness: `dotnet run … -- --jts1145` (+ `oracle/jts1145_pip_gallery_vectors.txt`)
+ - Write-up: [`docs/jts-1145-pip-lane-2026-08.md`](jts-1145-pip-lane-2026-08.md).
+ - 14/14 geometric GT; critical rows (diamond graze INT, notch horizontal EXT, Spectre/Hat pockets EXT) all pass; Y ≡ Indexed ≡ Simple.
+ - Optional: post gallery pins comment (`tests/Discussion839Mre/jts-1145-comment.md`).
4. **ScaledNoder #90 (P1)** — **SCOPED 2026-08-05 YELLOW:**
- MRE: `tests/Discussion839Mre` `--jts90` + [`jts-90-scalednoder-lane-2026-08.md`](jts-90-scalednoder-lane-2026-08.md).
- Does **not** map to a missing `SnapRoundingScale_b64` claim (wrapper policy + scale=0 safety).
@@ -111,7 +116,7 @@ Columns: **Pri** = P0/P1/P2 · **Rel/Cost/Risk** · **Epic**.
| 1189 | WIP Orientation isCCW GEOS test | strk | P1 | H | L | M | precision | `status-INVALID`; still a useful negative test seed |
| 1170 | Generics on geom.util extractors | micycle1 | P2 | L | L | L | — | API typing |
| 1164 | GeoJsonWriter create() for subclasses | krizleebear | P2 | L | L | L | — | IO |
-| **1145** | **YStripesPointInAreaLocator** | micycle1 | **P0** | H | L–M | M | **relate**/JCT | New PIP; gallery |
+| **1145** | **YStripesPointInAreaLocator** | micycle1 | **P0** | H | L–M | M | **relate**/JCT | **GREEN** gallery 14/14 ([lane](jts-1145-pip-lane-2026-08.md)) |
| **1140** | **Hausdorff distance percentile** | ikgh9 | **P1** | M | M | L–M | **metric** | Extends HausdorffDiscrete |
| 1137 | Gh pages to docs | jodygarnett | P2 | L | L | L | — | docs |
| 1135 | HilbertEncoder.sort() | micycle1 | P2 | L | L | L | — | spatial index util |
@@ -190,6 +195,7 @@ Columns: **Pri** = P0/P1/P2 · **Rel/Cost/Risk** · **Epic**.
|---|---|---|
| **In-circle robustness** | #311 → #1094 → **#1212** | Treat **#1212 as tip**; archive others as predecessors once merged. History: [`jts-311-incircle-lane-2026-08.md`](jts-311-incircle-lane-2026-08.md). FFI scout + review: [`jts-1094-incircle-lane-2026-08.md`](jts-1094-incircle-lane-2026-08.md). Tip differential: [`jts-1212-incircle-lane-2026-08.md`](jts-1212-incircle-lane-2026-08.md) |
| **Orientation filters** | #1093 (Ozaki production), #1197 (DD limits tests), #1189 (isCCW test INVALID) | **#1093 lane GREEN** ([jts-1093-orient-lane-2026-08.md](jts-1093-orient-lane-2026-08.md)); keep #1197; mine #1189 for vectors only |
+| **PIP / YStripes** | **#1145** | **GREEN** ([jts-1145-pip-lane-2026-08.md](jts-1145-pip-lane-2026-08.md)): gallery 14/14; free CI for locator merge |
| **ScaledNoder policy** | #90 (draft) | **YELLOW** ([jts-90-scalednoder-lane-2026-08.md](jts-90-scalednoder-lane-2026-08.md)): SRN masks mukoki; scale=0 only; no algebra claim |
| **DistanceOp / LineSegment** | #926, #930 | One review pass if either moves; low proof cost |
| **CI matrix** | #638, #646 | Ignore unless JTS maintainers revive |
@@ -245,7 +251,7 @@ Do **not** open multi-session theory work for P2 PRs.
1. **Bet the mesh lane on #1212** (with #1094/#311 as history) — highest corpus leverage per hour.
2. **Orientation filter change (#1093) gated on exact orient** — session GREEN; re-run table if the PR is revised.
-3. **Use the PIP gallery as free CI for #1145**.
+3. **Use the PIP gallery as free CI for #1145** — **done GREEN** (14/14); suggest pinning WKT into JTS tests.
4. **Ignore ~55% of the open queue** (CI/IO/docs/API) for proof planning.
5. **Keep our four PRs** green; only #1197 and #1216 need corpus-adjacent attention.
diff --git a/oracle/jts1145_pip_gallery_vectors.txt b/oracle/jts1145_pip_gallery_vectors.txt
new file mode 100644
index 00000000..24669e2e
--- /dev/null
+++ b/oracle/jts1145_pip_gallery_vectors.txt
@@ -0,0 +1,27 @@
+# JTS #1145 YStripes PIP gallery differential (2026-08-05)
+# Source: docs/nts-oracle-gallery.md + C# port of micycle1/jts ystripes tip
+# Harness: tests/Discussion839Mre --jts1145
+# claimId: none — ADR-0004 skip; harness-only differential, not a fresh mutation surface
+# GT = geometric / OGC Location; parity column notes naive ray-parity where it differs
+# status: all PASS (YStripes == Indexed == Simple == GT)
+#
+# id x y GT parity Indexed Simple YStripes status theorem
+
+VGRAZE_G 0 0.5 INT odd INT INT INT PASS diamond_point_in_ring_A
+VGRAZE0 0 0 INT even* INT INT INT PASS diamond_refutes_parity_seam
+HORZ_E -1 1 EXT odd* EXT EXT EXT PASS notch_refutes_parity_without_guard
+RECT_IN 2 1.5 INT in INT INT INT PASS point_in_ring_rect_iff
+RECT_L 0 1.5 BND parity-in BND BND BND PASS point_in_ring_rect_iff_left
+RECT_R 4 1.5 BND parity-out BND BND BND PASS point_in_ring_rect_iff_right
+RECT_B 2 0 BND parity-out BND BND BND PASS point_in_ring_rect_iff_bottom
+RECT_T 2 3 BND parity-out BND BND BND PASS point_in_ring_rect_iff_top
+PIX_C 0 0 INT in INT INT INT PASS unit_pixel_centre_in_ring
+PIX_BOT 0 -0.5 BND parity-out* BND BND BND PASS pixel_grazing_bottom_edge
+SPECT_IN 5 0.5 INT odd INT INT INT PASS spectre_parity_classification
+SPECT_POCK 3.5 0.5 EXT even EXT EXT EXT PASS spectre_parity_classification_pocket
+HAT_IN 4.25 2.1650635094610964 INT odd INT INT INT PASS hat_parity_classification_interior_APPROX
+HAT_POCK 3.5 0.4330127018922193 EXT even EXT EXT EXT PASS hat_parity_classification_pocket_APPROX
+
+# Summary: 14/14 PASS, 0 YStripes≠GT, 0 Y≠Indexed, 0 Y≠Simple
+# Critical robustness: VGRAZE0 INT, HORZ_E EXT, SPECT_POCK EXT, HAT_POCK EXT — all GREEN
+# Write-up: docs/jts-1145-pip-lane-2026-08.md
diff --git a/tests/Discussion839Mre/Program.cs b/tests/Discussion839Mre/Program.cs
index a0e5538b..2d726099 100644
--- a/tests/Discussion839Mre/Program.cs
+++ b/tests/Discussion839Mre/Program.cs
@@ -16,9 +16,11 @@
using System.Diagnostics;
using System.Globalization;
using System.Text;
+using Discussion839Mre;
using NetTopologySuite.Algorithm;
using NetTopologySuite.Algorithm.Locate;
using NetTopologySuite.Geometries;
+using NetTopologySuite.IO;
using NetTopologySuite.Noding;
using NetTopologySuite.Noding.Snapround;
using NetTopologySuite.Triangulate;
@@ -67,6 +69,14 @@ public static int Main(string[] args)
return RunJts90ScaledNoderMre();
}
+ // JTS#1145 YStripes PIP gallery vs corpus oracle (no Rocq binary required).
+ if (args.Contains("--jts1145", StringComparer.OrdinalIgnoreCase)
+ || args.Contains("--ystripes", StringComparer.OrdinalIgnoreCase)
+ || args.Contains("--pip-gallery", StringComparer.OrdinalIgnoreCase))
+ {
+ return RunJts1145PipGallery();
+ }
+
bool useScaled = !args.Contains("--no-scale", StringComparer.OrdinalIgnoreCase);
double tolerance = args.Contains("--tol0", StringComparer.OrdinalIgnoreCase) ? 0.0 : 0.1;
@@ -282,6 +292,209 @@ public static int Main(string[] args)
return 0;
}
+ ///
+ /// JTS PR #1145 YStripesPointInAreaLocator vs NTS Indexed/Simple locators
+ /// on the corpus PIP gallery (docs/nts-oracle-gallery.md). Geometric GT is
+ /// the Qed-backed correct answer; pure ray-parity is recorded separately
+ /// where it disagrees at vertex-graze / horizontal-edge rows.
+ ///
+ private static int RunJts1145PipGallery()
+ {
+ Console.WriteLine("=== JTS#1145 YStripes PIP gallery (corpus oracle) ===");
+ Console.WriteLine($"NTS: {typeof(Geometry).Assembly.GetName().Version}");
+ Console.WriteLine("Locators: IndexedPointInAreaLocator | SimplePointInAreaLocator | YStripes (PR#1145 port)");
+ Console.WriteLine("GT: geometric correct answer from docs/nts-oracle-gallery.md (Qed theorems)");
+ Console.WriteLine();
+
+ var rdr = new WKTReader();
+ var cases = GalleryCases();
+ int pass = 0, fail = 0, warn = 0;
+ var rows = new List();
+
+ Console.WriteLine(
+ $"{"id",-8} {"pt",-22} {"GT",-9} {"parity",-9} {"Indexed",-9} {"Simple",-9} {"YStripes",-9} note");
+ Console.WriteLine(new string('-', 110));
+
+ foreach (var c in cases)
+ {
+ var geom = rdr.Read(c.Wkt);
+ var pt = new Coordinate(c.X, c.Y);
+ var indexed = new IndexedPointInAreaLocator(geom).Locate(pt);
+ var simple = SimplePointInAreaLocator.Locate(pt, geom);
+ var ystripes = new YStripesPointInAreaLocator(geom).Locate(pt);
+
+ bool idxOk = MatchesGallery(indexed, c.GeomExpected);
+ bool simOk = MatchesGallery(simple, c.GeomExpected);
+ bool ysOk = MatchesGallery(ystripes, c.GeomExpected);
+ bool parityMismatch = c.NaiveParityDiffers;
+
+ string status;
+ if (ysOk && idxOk && simOk)
+ {
+ pass++;
+ status = "PASS";
+ }
+ else if (ysOk)
+ {
+ // YStripes correct vs GT; baseline may differ only on BOUNDARY convention
+ warn++;
+ status = "WARN";
+ }
+ else
+ {
+ fail++;
+ status = "FAIL";
+ }
+
+ string note = status;
+ if (parityMismatch) note += " | naive-parity≠geom";
+ if (!idxOk) note += " | Indexed≠GT";
+ if (!simOk) note += " | Simple≠GT";
+ if (!ysOk) note += " | YStripes≠GT";
+ if (ystripes != indexed) note += " | Y≠Indexed";
+ if (ystripes != simple) note += " | Y≠Simple";
+
+ string ptStr = $"({Fmt(c.X)},{Fmt(c.Y)})";
+ Console.WriteLine(
+ $"{c.Id,-8} {ptStr,-22} {LocTag(c.GeomExpected),-9} {c.NaiveParityTag,-9} " +
+ $"{LocTag(indexed),-9} {LocTag(simple),-9} {LocTag(ystripes),-9} {note}");
+
+ rows.Add(string.Join('\t',
+ c.Id, c.X.ToString("G17", CultureInfo.InvariantCulture),
+ c.Y.ToString("G17", CultureInfo.InvariantCulture),
+ LocTag(c.GeomExpected), c.NaiveParityTag,
+ LocTag(indexed), LocTag(simple), LocTag(ystripes), status, c.Theorem));
+ }
+
+ Console.WriteLine();
+ Console.WriteLine("=== SUMMARY ===");
+ Console.WriteLine($"cases: {cases.Count} PASS(all=GT): {pass} WARN(YStripes=GT): {warn} FAIL(YStripes≠GT): {fail}");
+
+ // Critical robustness rows that the gallery was built for
+ var critical = new[] { "VGRAZE0", "HORZ_E", "SPECT_POCK", "HAT_POCK" };
+ int critFail = 0;
+ foreach (var c in cases.Where(c => critical.Contains(c.Id)))
+ {
+ var geom = rdr.Read(c.Wkt);
+ var ys = new YStripesPointInAreaLocator(geom).Locate(new Coordinate(c.X, c.Y));
+ if (!MatchesGallery(ys, c.GeomExpected))
+ {
+ critFail++;
+ Console.WriteLine($" CRITICAL FAIL {c.Id}: YStripes={LocTag(ys)} GT={LocTag(c.GeomExpected)}");
+ }
+ }
+
+ Console.WriteLine();
+ Console.WriteLine("=== VERDICT ===");
+ if (fail == 0 && critFail == 0)
+ {
+ Console.WriteLine(" GREEN: YStripes port matches geometric GT on all gallery vectors");
+ Console.WriteLine(" (vertex-graze, horizontal-edge, Spectre pocket, Hat pocket, rect/pixel boundary).");
+ Console.WriteLine(" Product: safe soundness gate for JTS#1145; recommend adding these WKT pins to AbstractPointInRingTest.");
+ }
+ else
+ {
+ Console.WriteLine($" RED: {fail} YStripes≠GT ({critFail} critical). Do not rubber-stamp #1145.");
+ }
+ Console.WriteLine("See docs/jts-1145-pip-lane-2026-08.md");
+ Console.WriteLine();
+ Console.WriteLine("--- machine table (tab) ---");
+ Console.WriteLine("id\tx\ty\tGT\tparity\tIndexed\tSimple\tYStripes\tstatus\ttheorem");
+ foreach (var r in rows) Console.WriteLine(r);
+
+ return fail == 0 ? 0 : 1;
+ }
+
+ private sealed record GalleryCase(
+ string Id,
+ string Wkt,
+ double X,
+ double Y,
+ Location GeomExpected,
+ string NaiveParityTag,
+ bool NaiveParityDiffers,
+ string Theorem);
+
+ private static List GalleryCases()
+ {
+ // WKT + geometric answers from docs/nts-oracle-gallery.md.
+ // Location.Boundary rows are OGC boundary (Contains=false); pure parity half-open
+ // is recorded in NaiveParityTag where relevant.
+ const string diamond =
+ "POLYGON ((0 1, 1 0, 0 -1, -1 0, 0 1))";
+ const string notch =
+ "POLYGON ((0 0, 4 0, 4 2, 2 2, 2 1, 0 1, 0 0))";
+ const string rect =
+ "POLYGON ((0 0, 4 0, 4 3, 0 3, 0 0))";
+ const string pixel =
+ "POLYGON ((-0.5 -0.5, 0.5 -0.5, 0.5 0.5, -0.5 0.5, -0.5 -0.5))";
+ const string spectre =
+ "POLYGON ((0 0, 2 0, 3.5 1, 4 0, 6 0, 7.5 1, 7 2, 5 2, 4.5 3, 3 2, 1 2, 0.5 1, -0.5 1, 0 0))";
+ // Hat monotile APPROX (exact uses √3); still exercises concave pocket.
+ const string hat =
+ "POLYGON ((0 0, 2 0, 3.5 0.8660254, 4 0, 6 0, 7.5 0.8660254, 7 1.7320508, 5 1.7320508, 4.5 2.5980762, 3 1.7320508, 1 1.7320508, 0.5 0.8660254, -0.5 0.8660254, 0 0))";
+
+ double s3 = Math.Sqrt(3.0);
+
+ return new List
+ {
+ // Tier 1.1 vertex graze
+ new("VGRAZE_G", diamond, 0, 0.5, Location.Interior, "odd", false,
+ "diamond_point_in_ring_A"),
+ new("VGRAZE0", diamond, 0, 0, Location.Interior, "even*", true,
+ "diamond_refutes_parity_seam"),
+
+ // Tier 1.2 horizontal edge
+ new("HORZ_E", notch, -1, 1, Location.Exterior, "odd*", true,
+ "notch_refutes_parity_without_guard"),
+
+ // Tier 1.3 rectangle half-open / OGC boundary
+ new("RECT_IN", rect, 2, 1.5, Location.Interior, "in", false,
+ "point_in_ring_rect_iff"),
+ new("RECT_L", rect, 0, 1.5, Location.Boundary, "parity-in", true,
+ "point_in_ring_rect_iff left"),
+ new("RECT_R", rect, 4, 1.5, Location.Boundary, "parity-out", false,
+ "point_in_ring_rect_iff right"),
+ new("RECT_B", rect, 2, 0, Location.Boundary, "parity-out", false,
+ "point_in_ring_rect_iff bottom"),
+ new("RECT_T", rect, 2, 3, Location.Boundary, "parity-out", false,
+ "point_in_ring_rect_iff top"),
+
+ // Tier 1.4 hot pixel
+ new("PIX_C", pixel, 0, 0, Location.Interior, "in", false,
+ "unit_pixel_centre_in_ring"),
+ new("PIX_BOT", pixel, 0, -0.5, Location.Boundary, "parity-out*", true,
+ "pixel_grazing_bottom_edge"),
+
+ // Tier 2 Spectre
+ new("SPECT_IN", spectre, 5, 0.5, Location.Interior, "odd", false,
+ "spectre_parity_classification"),
+ new("SPECT_POCK", spectre, 3.5, 0.5, Location.Exterior, "even", false,
+ "spectre_parity_classification pocket"),
+
+ // Tier 2 Hat (approx WKT; GT from geometric pocket/interior)
+ new("HAT_IN", hat, 4.25, 5 * s3 / 4, Location.Interior, "odd", false,
+ "hat_parity_classification interior APPROX"),
+ new("HAT_POCK", hat, 3.5, s3 / 4, Location.Exterior, "even", false,
+ "hat_parity_classification pocket APPROX"),
+ };
+ }
+
+ ///
+ /// Gallery GT for robust OGC-style locators: Interior / Exterior / Boundary.
+ /// Pure parity may disagree on * rows; MatchesGallery accepts Boundary as a
+ /// correct robust answer whenever GT is Boundary.
+ ///
+ private static bool MatchesGallery(Location got, Location expected) => got == expected;
+
+ private static string LocTag(Location loc) => loc switch
+ {
+ Location.Interior => "INT",
+ Location.Exterior => "EXT",
+ Location.Boundary => "BND",
+ _ => loc.ToString(),
+ };
+
///
/// JTS PR #90 / mukoki OpenJUMP beanshell: ScaledNoder(scale=1) vs scale≠1
/// rounds input vertices inconsistently (NTS port of the same control flow).
diff --git a/tests/Discussion839Mre/README.md b/tests/Discussion839Mre/README.md
index af913182..dbb6aa86 100644
--- a/tests/Discussion839Mre/README.md
+++ b/tests/Discussion839Mre/README.md
@@ -63,3 +63,23 @@ dotnet run --project tests/Discussion839Mre -c Release -- --scaled-noder
Write-up: [`docs/jts-90-scalednoder-lane-2026-08.md`](../../docs/jts-90-scalednoder-lane-2026-08.md).
Optional PR comment draft: [`jts-90-comment.md`](jts-90-comment.md).
+
+## JTS #1145 YStripes PIP gallery mode
+
+Differential gate for
+[locationtech/jts#1145](https://github.com/locationtech/jts/pull/1145)
+(`YStripesPointInAreaLocator`) against the Qed-backed corpus gallery
+([`docs/nts-oracle-gallery.md`](../../docs/nts-oracle-gallery.md)).
+Compares Indexed / Simple / YStripes (faithful C# port) — **no Rocq oracle**.
+
+```powershell
+dotnet run --project tests/Discussion839Mre -c Release -- --jts1145
+# aliases:
+dotnet run --project tests/Discussion839Mre -c Release -- --ystripes
+dotnet run --project tests/Discussion839Mre -c Release -- --pip-gallery
+# keep-green: expect exit code 0 (14/14 geometric GT)
+```
+
+Write-up: [`docs/jts-1145-pip-lane-2026-08.md`](../../docs/jts-1145-pip-lane-2026-08.md).
+Vectors: [`oracle/jts1145_pip_gallery_vectors.txt`](../../oracle/jts1145_pip_gallery_vectors.txt).
+Optional PR comment draft: [`jts-1145-comment.md`](jts-1145-comment.md).
diff --git a/tests/Discussion839Mre/YStripesPointInAreaLocator.cs b/tests/Discussion839Mre/YStripesPointInAreaLocator.cs
new file mode 100644
index 00000000..d2ceb805
--- /dev/null
+++ b/tests/Discussion839Mre/YStripesPointInAreaLocator.cs
@@ -0,0 +1,389 @@
+// ============================================================================
+// Faithful C# port of JTS PR #1145 YStripes point-in-area locator (micycle1).
+// Source tip: micycle1/jts branch ystripes —
+// YStripesPointInPolygonLocator / YStripesPointInAreaLocator
+// Used only for corpus gallery differential; not a production NTS API.
+// ============================================================================
+
+using NetTopologySuite.Algorithm;
+using NetTopologySuite.Geometries;
+using NetTopologySuite.Index.Strtree;
+
+namespace Discussion839Mre;
+
+///
+/// Point-in-area locator mirroring JTS YStripesPointInAreaLocator (#1145).
+///
+public sealed class YStripesPointInAreaLocator : IPointInAreaLocator
+{
+ private readonly Envelope? _env;
+ private readonly IPointInAreaLocator? _single;
+ private readonly STRtree