diff --git a/raleigh/EVIDENCE-LEDGER.md b/raleigh/EVIDENCE-LEDGER.md index 4ff2552..ac0dfdb 100644 --- a/raleigh/EVIDENCE-LEDGER.md +++ b/raleigh/EVIDENCE-LEDGER.md @@ -170,7 +170,7 @@ The following public service boundaries were exercised successfully on 2026-07-2 ### Verification -- `PYTHONDONTWRITEBYTECODE=1 python3 -m unittest raleigh/tests/test_raleigh.py`: **354 tests passed**. +- `PYTHONDONTWRITEBYTECODE=1 python3 -m unittest raleigh/tests/test_raleigh.py`: **355 tests passed**. - `python3 -m ruff check raleigh/scripts/raleighlib/core.py raleigh/scripts/raleighlib/public_safety_stats.py raleigh/scripts/raleighlib/cli.py`: passed. - `python3 scripts/validate-evals.py raleigh`: **11 eval manifests validated**. - `ruby scripts/validate-skills.rb`: **110 canonical skills validated**. @@ -181,7 +181,7 @@ The following public service boundaries were exercised successfully on 2026-07-2 - Live `fire stats --year 2026 --json`: returned seven official published categories, including medical `7,882`, source revision/retrieval metadata, the annual document URL, and the aggregate-only privacy warning. - Live `fire reports --year 2025 --quarter 1 --json`: returned the canonical official quarterly page. - Live `fire reports --date 2026-07-24 --json`: exercised the unchanged ArcGIS incident-report path successfully. -- Three review passes found and then verified fixes for terminal-control handling, path traversal and URL components, empty tables, document availability, redirect targets, JSON:API relationship ambiguity, and eval coverage; the final pass reported no actionable findings. +- Review passes found and then verified fixes for terminal-control handling, path traversal and URL components, empty tables, document availability, redirect targets, malformed or ambiguous JSON:API relationships, and eval coverage. ### Remaining boundaries and follow-up triggers diff --git a/raleigh/scripts/raleighlib/public_safety_stats.py b/raleigh/scripts/raleighlib/public_safety_stats.py index 5be595e..2df8505 100644 --- a/raleigh/scripts/raleighlib/public_safety_stats.py +++ b/raleigh/scripts/raleighlib/public_safety_stats.py @@ -151,17 +151,22 @@ def _fetch_page(agency: str) -> tuple[dict[str, Any], dict[str, str]]: if not isinstance(data, dict) or not isinstance(included, list): raise PublishedStatisticsError("published statistics source returned invalid JSON:API data") attrs = data.get("attributes") + path = attrs.get("path") if isinstance(attrs, dict) else None if ( data.get("type") != "node--service" or data.get("id") != source["id"] or not isinstance(attrs, dict) + or not isinstance(path, dict) or attrs.get("status") is not True or attrs.get("title") != source["title"] - or attrs.get("path", {}).get("alias") != urllib.parse.urlparse(source["page_url"]).path + or path.get("alias") != urllib.parse.urlparse(source["page_url"]).path ): raise PublishedStatisticsError("published statistics page identity changed") - relationship = data.get("relationships", {}).get("field_content_primary", {}) + relationships = data.get("relationships") + if not isinstance(relationships, dict): + raise PublishedStatisticsError("published statistics content relationships are invalid") + relationship = relationships.get("field_content_primary", {}) related = relationship.get("data") if isinstance(relationship, dict) else None if not isinstance(related, list): raise PublishedStatisticsError("published statistics content relationship is missing") diff --git a/raleigh/tests/test_raleigh.py b/raleigh/tests/test_raleigh.py index db23594..30bc4d0 100644 --- a/raleigh/tests/test_raleigh.py +++ b/raleigh/tests/test_raleigh.py @@ -3071,6 +3071,21 @@ class PublishedPublicSafetyStatisticsTests(unittest.TestCase): with self.assertRaisesRegex(public_safety_stats.PublishedStatisticsError, "no published"): public_safety_stats.reports("police", 2026, 4) + def test_malformed_jsonapi_nested_objects_fail_visibly(self): + for field, value, message in ( + ("path", None, "page identity changed"), + ("relationships", None, "relationships are invalid"), + ): + with self.subTest(field=field): + fixture = self._fixture("police") + if field == "path": + fixture["data"]["attributes"][field] = value + else: + fixture["data"][field] = value + with patch("raleighlib.public_safety_stats.core.json_request", return_value=fixture): + with self.assertRaisesRegex(public_safety_stats.PublishedStatisticsError, message): + public_safety_stats.reports("police") + def test_report_link_outside_canonical_origins_fails_closed(self): fixture = self._fixture("police") html = fixture["included"][0]["attributes"]["field_stories_text_formatted"]["value"]