diff --git a/docs/health/STATUS.md b/docs/health/STATUS.md index 347bea4..a1c06da 100644 --- a/docs/health/STATUS.md +++ b/docs/health/STATUS.md @@ -15,18 +15,37 @@ look wrong, there is a date to point at. ## development -**8 of 8 working** · 2026-09-14 21:44 UTC · `9ecbbe0` +**8 of 8 working** · 2026-10-05 13:54 UTC · `d8484d5` | | Question | Time | Targets | Anchors | Reaches | Change since last run | | --- | --- | ---: | ---: | ---: | ---: | --- | -| ✅ | Samples Near Agricultural Chemical Facilities in Maine | 22s | 29 | 11 | — | _first run_ | -| ✅ | Samples Near Landfills & DOD Sites in Penobscot and Knox County | 11s | 60 | 8 | — | _first run_ | -| ✅ | Surface Water Bodies Near Landfills & DOD Sites in Penobscot and Knox County | 4s | 103 | 6 | — | _first run_ | -| ✅ | Samples Downstream of Waste Treatment Facilities in Indiana | 14s | 123 | 882 | 15696 | _first run_ | -| ✅ | Maine Private Wells Near Landfills & DoD Sites | 19s | 751 | 38 | — | _first run_ | -| ✅ | Maine Private Wells Near Wastewater Treatment Facilities | 14s | 2431 | 135 | — | _first run_ | -| ✅ | Maine Private Wells Near Airports & Air Transportation Sites (AFFF) | 14s | 4425 | 174 | — | _first run_ | -| ✅ | PFHpA Groundwater Samples Downstream from Facilities in Cumberland County | 159s | 49 | 268 | 362 | _first run_ | +| ⚠️ | Facilities Upstream from PFHpA Groundwater Contamination in Cumberland County | 261s | 49 | 265 | 84 | **boundaries 16 → 0** | +| ✅ | Samples Near Landfills & DOD Sites in Penobscot and Knox County | 32s | 60 | 8 | — | — | +| ✅ | Samples Downstream of Waste Treatment Facilities in Indiana | 44s | 123 | 882 | 13988 | — | +| ✅ | Chemical & Plastics Manufacturers Upstream from Streams in Cook County, IL | 57s | 526 | 732 | — | — | +| ✅ | Samples Near Agricultural Chemical Facilities in Maine | 6s | 29 | 11 | — | — | +| ✅ | Surface Water Bodies Near Landfills & DOD Sites in Penobscot and Knox County | 3s | 103 | 6 | — | — | +| ✅ | Maine Private Wells Near Landfills & DoD Sites | 12s | 751 | 38 | — | — | +| ✅ | Maine Private Wells Near Wastewater Treatment Facilities | 8s | 2431 | 135 | — | — | + +**Incomplete answers:** + +- Facilities Upstream from PFHpA Groundwater Contamination in Cumberland County — could not answer whole query + +## main + +**8 of 8 working** · 2026-10-05 14:01 UTC · `ca2be08` + +| | Question | Time | Targets | Anchors | Reaches | Change since last run | +| --- | --- | ---: | ---: | ---: | ---: | --- | +| ✅ | Facilities Upstream from PFHpA Groundwater Contamination in Cumberland County | 162s | 49 | 265 | 84 | — | +| ✅ | Samples Near Landfills & DOD Sites in Penobscot and Knox County | 12s | 60 | 8 | — | — | +| ✅ | Samples Downstream of Waste Treatment Facilities in Indiana | 32s | 123 | 882 | 13988 | — | +| ✅ | Chemical & Plastics Manufacturers Upstream from Streams in Cook County, IL | 25s | 526 | 732 | — | — | +| ✅ | Samples Near Agricultural Chemical Facilities in Maine | 6s | 29 | 11 | — | — | +| ✅ | Surface Water Bodies Near Landfills & DOD Sites in Penobscot and Knox County | 2s | 103 | 6 | — | — | +| ✅ | Maine Private Wells Near Landfills & DoD Sites | 11s | 751 | 38 | — | — | +| ✅ | Maine Private Wells Near Wastewater Treatment Facilities | 8s | 2431 | 135 | — | — | --- diff --git a/docs/health/history.jsonl b/docs/health/history.jsonl index f65349a..cb4c3d1 100644 --- a/docs/health/history.jsonl +++ b/docs/health/history.jsonl @@ -6,3 +6,35 @@ {"runAt":"2026-09-14T21:44:17.124Z","ref":"development","commit":"9ecbbe0","id":"wells-near-wwtp-maine","title":"Maine Private Wells Near Wastewater Treatment Facilities","attempts":1,"status":"success","ms":14266,"counts":{"FIND_TARGET_ENTITIES":2431,"GET_ANCHOR_DETAILS":135,"GET_REGION_BOUNDARIES":16},"partialFailed":[],"partialSkipped":[],"error":null} {"runAt":"2026-09-14T21:44:17.124Z","ref":"development","commit":"9ecbbe0","id":"wells-near-airports-maine","title":"Maine Private Wells Near Airports & Air Transportation Sites (AFFF)","attempts":1,"status":"success","ms":14222,"counts":{"FIND_TARGET_ENTITIES":4425,"GET_ANCHOR_DETAILS":174,"GET_REGION_BOUNDARIES":16},"partialFailed":[],"partialSkipped":[],"error":null} {"runAt":"2026-09-14T21:44:17.124Z","ref":"development","commit":"9ecbbe0","id":"facilities-downstream-pfhpa-gw-cumberland","title":"PFHpA Groundwater Samples Downstream from Facilities in Cumberland County","attempts":1,"status":"success","ms":159024,"counts":{"FIND_TARGET_ENTITIES":49,"GET_ANCHOR_DETAILS":268,"GET_REGION_BOUNDARIES":16,"GET_FLOWLINE_GEOMETRIES":362},"partialFailed":[],"partialSkipped":[],"error":null} +{"runAt":"2026-09-28T13:12:21.508Z","ref":"development","commit":"01a6899","id":"facilities-upstream-pfhpa-gw-cumberland","title":"Facilities Upstream from PFHpA Groundwater Contamination in Cumberland County","attempts":1,"status":"success","ms":174079,"counts":{"FIND_TARGET_ENTITIES":49,"GET_ANCHOR_DETAILS":265,"GET_REGION_BOUNDARIES":16,"GET_FLOWLINE_GEOMETRIES":84},"partialFailed":[],"partialSkipped":[],"error":null} +{"runAt":"2026-09-28T13:12:21.508Z","ref":"development","commit":"01a6899","id":"samples-near-landfill-dod-penobscot-knox","title":"Samples Near Landfills & DOD Sites in Penobscot and Knox County","attempts":1,"status":"success","ms":26122,"counts":{"FIND_TARGET_ENTITIES":60,"GET_ANCHOR_DETAILS":8,"GET_REGION_BOUNDARIES":16},"partialFailed":[],"partialSkipped":[],"error":null} +{"runAt":"2026-09-28T13:12:21.508Z","ref":"development","commit":"01a6899","id":"samples-downstream-waste-indiana","title":"Samples Downstream of Waste Treatment Facilities in Indiana","attempts":1,"status":"success","ms":65580,"counts":{"FIND_TARGET_ENTITIES":123,"GET_ANCHOR_DETAILS":882,"GET_REGION_BOUNDARIES":92,"GET_FLOWLINE_GEOMETRIES":13988},"partialFailed":[],"partialSkipped":[],"error":null} +{"runAt":"2026-09-28T13:12:21.508Z","ref":"development","commit":"01a6899","id":"facilities-upstream-streams-cook","title":"Chemical & Plastics Manufacturers Upstream from Streams in Cook County, IL","attempts":1,"status":"success","ms":31712,"counts":{"FIND_TARGET_ENTITIES":526,"GET_ANCHOR_DETAILS":732,"GET_REGION_BOUNDARIES":102},"partialFailed":[],"partialSkipped":[],"error":null} +{"runAt":"2026-09-28T13:12:21.508Z","ref":"development","commit":"01a6899","id":"samples-near-agchem-maine","title":"Samples Near Agricultural Chemical Facilities in Maine","attempts":1,"status":"success","ms":4918,"counts":{"FIND_TARGET_ENTITIES":29,"GET_ANCHOR_DETAILS":11,"GET_REGION_BOUNDARIES":16},"partialFailed":[],"partialSkipped":[],"error":null} +{"runAt":"2026-09-28T13:12:21.508Z","ref":"development","commit":"01a6899","id":"waterbodies-near-landfill-dod-penobscot-knox","title":"Surface Water Bodies Near Landfills & DOD Sites in Penobscot and Knox County","attempts":1,"status":"success","ms":2508,"counts":{"FIND_TARGET_ENTITIES":103,"GET_ANCHOR_DETAILS":6,"GET_REGION_BOUNDARIES":16},"partialFailed":[],"partialSkipped":[],"error":null} +{"runAt":"2026-09-28T13:12:21.508Z","ref":"development","commit":"01a6899","id":"wells-near-landfill-dod-maine","title":"Maine Private Wells Near Landfills & DoD Sites","attempts":1,"status":"success","ms":12166,"counts":{"FIND_TARGET_ENTITIES":751,"GET_ANCHOR_DETAILS":38,"GET_REGION_BOUNDARIES":16},"partialFailed":[],"partialSkipped":[],"error":null} +{"runAt":"2026-09-28T13:12:21.508Z","ref":"development","commit":"01a6899","id":"wells-near-wwtp-maine","title":"Maine Private Wells Near Wastewater Treatment Facilities","attempts":1,"status":"success","ms":8184,"counts":{"FIND_TARGET_ENTITIES":2431,"GET_ANCHOR_DETAILS":135,"GET_REGION_BOUNDARIES":16},"partialFailed":[],"partialSkipped":[],"error":null} +{"runAt":"2026-09-28T13:17:49.579Z","ref":"main","commit":"ca2be08","id":"facilities-upstream-pfhpa-gw-cumberland","title":"Facilities Upstream from PFHpA Groundwater Contamination in Cumberland County","attempts":1,"status":"success","ms":133245,"counts":{"FIND_TARGET_ENTITIES":49,"GET_ANCHOR_DETAILS":265,"GET_REGION_BOUNDARIES":16,"GET_FLOWLINE_GEOMETRIES":84},"partialFailed":[],"partialSkipped":[],"error":null} +{"runAt":"2026-09-28T13:17:49.579Z","ref":"main","commit":"ca2be08","id":"samples-near-landfill-dod-penobscot-knox","title":"Samples Near Landfills & DOD Sites in Penobscot and Knox County","attempts":1,"status":"success","ms":5761,"counts":{"FIND_TARGET_ENTITIES":60,"GET_ANCHOR_DETAILS":8,"GET_REGION_BOUNDARIES":16},"partialFailed":[],"partialSkipped":[],"error":null} +{"runAt":"2026-09-28T13:17:49.579Z","ref":"main","commit":"ca2be08","id":"samples-downstream-waste-indiana","title":"Samples Downstream of Waste Treatment Facilities in Indiana","attempts":1,"status":"success","ms":9002,"counts":{"FIND_TARGET_ENTITIES":123,"GET_ANCHOR_DETAILS":882,"GET_REGION_BOUNDARIES":92,"GET_FLOWLINE_GEOMETRIES":13988},"partialFailed":[],"partialSkipped":[],"error":null} +{"runAt":"2026-09-28T13:17:49.579Z","ref":"main","commit":"ca2be08","id":"facilities-upstream-streams-cook","title":"Chemical & Plastics Manufacturers Upstream from Streams in Cook County, IL","attempts":1,"status":"success","ms":12162,"counts":{"FIND_TARGET_ENTITIES":526,"GET_ANCHOR_DETAILS":732,"GET_REGION_BOUNDARIES":102},"partialFailed":[],"partialSkipped":[],"error":null} +{"runAt":"2026-09-28T13:17:49.579Z","ref":"main","commit":"ca2be08","id":"samples-near-agchem-maine","title":"Samples Near Agricultural Chemical Facilities in Maine","attempts":1,"status":"success","ms":2552,"counts":{"FIND_TARGET_ENTITIES":29,"GET_ANCHOR_DETAILS":11,"GET_REGION_BOUNDARIES":16},"partialFailed":[],"partialSkipped":[],"error":null} +{"runAt":"2026-09-28T13:17:49.579Z","ref":"main","commit":"ca2be08","id":"waterbodies-near-landfill-dod-penobscot-knox","title":"Surface Water Bodies Near Landfills & DOD Sites in Penobscot and Knox County","attempts":1,"status":"success","ms":2358,"counts":{"FIND_TARGET_ENTITIES":103,"GET_ANCHOR_DETAILS":6,"GET_REGION_BOUNDARIES":16},"partialFailed":[],"partialSkipped":[],"error":null} +{"runAt":"2026-09-28T13:17:49.579Z","ref":"main","commit":"ca2be08","id":"wells-near-landfill-dod-maine","title":"Maine Private Wells Near Landfills & DoD Sites","attempts":1,"status":"success","ms":6635,"counts":{"FIND_TARGET_ENTITIES":751,"GET_ANCHOR_DETAILS":38,"GET_REGION_BOUNDARIES":16},"partialFailed":[],"partialSkipped":[],"error":null} +{"runAt":"2026-09-28T13:17:49.579Z","ref":"main","commit":"ca2be08","id":"wells-near-wwtp-maine","title":"Maine Private Wells Near Wastewater Treatment Facilities","attempts":1,"status":"success","ms":6766,"counts":{"FIND_TARGET_ENTITIES":2431,"GET_ANCHOR_DETAILS":135,"GET_REGION_BOUNDARIES":16},"partialFailed":[],"partialSkipped":[],"error":null} +{"runAt":"2026-10-05T13:54:47.766Z","ref":"development","commit":"d8484d5","id":"facilities-upstream-pfhpa-gw-cumberland","title":"Facilities Upstream from PFHpA Groundwater Contamination in Cumberland County","attempts":1,"status":"success","ms":261413,"counts":{"FIND_TARGET_ENTITIES":49,"GET_ANCHOR_DETAILS":265,"GET_FLOWLINE_GEOMETRIES":84},"partialFailed":["whole query"],"partialSkipped":[],"error":null} +{"runAt":"2026-10-05T13:54:47.766Z","ref":"development","commit":"d8484d5","id":"samples-near-landfill-dod-penobscot-knox","title":"Samples Near Landfills & DOD Sites in Penobscot and Knox County","attempts":1,"status":"success","ms":32149,"counts":{"FIND_TARGET_ENTITIES":60,"GET_ANCHOR_DETAILS":8,"GET_REGION_BOUNDARIES":16},"partialFailed":[],"partialSkipped":[],"error":null} +{"runAt":"2026-10-05T13:54:47.766Z","ref":"development","commit":"d8484d5","id":"samples-downstream-waste-indiana","title":"Samples Downstream of Waste Treatment Facilities in Indiana","attempts":1,"status":"success","ms":43874,"counts":{"FIND_TARGET_ENTITIES":123,"GET_ANCHOR_DETAILS":882,"GET_REGION_BOUNDARIES":92,"GET_FLOWLINE_GEOMETRIES":13988},"partialFailed":[],"partialSkipped":[],"error":null} +{"runAt":"2026-10-05T13:54:47.766Z","ref":"development","commit":"d8484d5","id":"facilities-upstream-streams-cook","title":"Chemical & Plastics Manufacturers Upstream from Streams in Cook County, IL","attempts":1,"status":"success","ms":57382,"counts":{"FIND_TARGET_ENTITIES":526,"GET_ANCHOR_DETAILS":732,"GET_REGION_BOUNDARIES":102},"partialFailed":[],"partialSkipped":[],"error":null} +{"runAt":"2026-10-05T13:54:47.766Z","ref":"development","commit":"d8484d5","id":"samples-near-agchem-maine","title":"Samples Near Agricultural Chemical Facilities in Maine","attempts":1,"status":"success","ms":6061,"counts":{"FIND_TARGET_ENTITIES":29,"GET_ANCHOR_DETAILS":11,"GET_REGION_BOUNDARIES":16},"partialFailed":[],"partialSkipped":[],"error":null} +{"runAt":"2026-10-05T13:54:47.766Z","ref":"development","commit":"d8484d5","id":"waterbodies-near-landfill-dod-penobscot-knox","title":"Surface Water Bodies Near Landfills & DOD Sites in Penobscot and Knox County","attempts":1,"status":"success","ms":2691,"counts":{"FIND_TARGET_ENTITIES":103,"GET_ANCHOR_DETAILS":6,"GET_REGION_BOUNDARIES":16},"partialFailed":[],"partialSkipped":[],"error":null} +{"runAt":"2026-10-05T13:54:47.766Z","ref":"development","commit":"d8484d5","id":"wells-near-landfill-dod-maine","title":"Maine Private Wells Near Landfills & DoD Sites","attempts":1,"status":"success","ms":12133,"counts":{"FIND_TARGET_ENTITIES":751,"GET_ANCHOR_DETAILS":38,"GET_REGION_BOUNDARIES":16},"partialFailed":[],"partialSkipped":[],"error":null} +{"runAt":"2026-10-05T13:54:47.766Z","ref":"development","commit":"d8484d5","id":"wells-near-wwtp-maine","title":"Maine Private Wells Near Wastewater Treatment Facilities","attempts":1,"status":"success","ms":8121,"counts":{"FIND_TARGET_ENTITIES":2431,"GET_ANCHOR_DETAILS":135,"GET_REGION_BOUNDARIES":16},"partialFailed":[],"partialSkipped":[],"error":null} +{"runAt":"2026-10-05T14:01:55.352Z","ref":"main","commit":"ca2be08","id":"facilities-upstream-pfhpa-gw-cumberland","title":"Facilities Upstream from PFHpA Groundwater Contamination in Cumberland County","attempts":1,"status":"success","ms":161946,"counts":{"FIND_TARGET_ENTITIES":49,"GET_ANCHOR_DETAILS":265,"GET_REGION_BOUNDARIES":16,"GET_FLOWLINE_GEOMETRIES":84},"partialFailed":[],"partialSkipped":[],"error":null} +{"runAt":"2026-10-05T14:01:55.352Z","ref":"main","commit":"ca2be08","id":"samples-near-landfill-dod-penobscot-knox","title":"Samples Near Landfills & DOD Sites in Penobscot and Knox County","attempts":1,"status":"success","ms":11785,"counts":{"FIND_TARGET_ENTITIES":60,"GET_ANCHOR_DETAILS":8,"GET_REGION_BOUNDARIES":16},"partialFailed":[],"partialSkipped":[],"error":null} +{"runAt":"2026-10-05T14:01:55.352Z","ref":"main","commit":"ca2be08","id":"samples-downstream-waste-indiana","title":"Samples Downstream of Waste Treatment Facilities in Indiana","attempts":1,"status":"success","ms":31682,"counts":{"FIND_TARGET_ENTITIES":123,"GET_ANCHOR_DETAILS":882,"GET_REGION_BOUNDARIES":92,"GET_FLOWLINE_GEOMETRIES":13988},"partialFailed":[],"partialSkipped":[],"error":null} +{"runAt":"2026-10-05T14:01:55.352Z","ref":"main","commit":"ca2be08","id":"facilities-upstream-streams-cook","title":"Chemical & Plastics Manufacturers Upstream from Streams in Cook County, IL","attempts":1,"status":"success","ms":24644,"counts":{"FIND_TARGET_ENTITIES":526,"GET_ANCHOR_DETAILS":732,"GET_REGION_BOUNDARIES":102},"partialFailed":[],"partialSkipped":[],"error":null} +{"runAt":"2026-10-05T14:01:55.352Z","ref":"main","commit":"ca2be08","id":"samples-near-agchem-maine","title":"Samples Near Agricultural Chemical Facilities in Maine","attempts":1,"status":"success","ms":5756,"counts":{"FIND_TARGET_ENTITIES":29,"GET_ANCHOR_DETAILS":11,"GET_REGION_BOUNDARIES":16},"partialFailed":[],"partialSkipped":[],"error":null} +{"runAt":"2026-10-05T14:01:55.352Z","ref":"main","commit":"ca2be08","id":"waterbodies-near-landfill-dod-penobscot-knox","title":"Surface Water Bodies Near Landfills & DOD Sites in Penobscot and Knox County","attempts":1,"status":"success","ms":2498,"counts":{"FIND_TARGET_ENTITIES":103,"GET_ANCHOR_DETAILS":6,"GET_REGION_BOUNDARIES":16},"partialFailed":[],"partialSkipped":[],"error":null} +{"runAt":"2026-10-05T14:01:55.352Z","ref":"main","commit":"ca2be08","id":"wells-near-landfill-dod-maine","title":"Maine Private Wells Near Landfills & DoD Sites","attempts":1,"status":"success","ms":11054,"counts":{"FIND_TARGET_ENTITIES":751,"GET_ANCHOR_DETAILS":38,"GET_REGION_BOUNDARIES":16},"partialFailed":[],"partialSkipped":[],"error":null} +{"runAt":"2026-10-05T14:01:55.352Z","ref":"main","commit":"ca2be08","id":"wells-near-wwtp-maine","title":"Maine Private Wells Near Wastewater Treatment Facilities","attempts":1,"status":"success","ms":8060,"counts":{"FIND_TARGET_ENTITIES":2431,"GET_ANCHOR_DETAILS":135,"GET_REGION_BOUNDARIES":16},"partialFailed":[],"partialSkipped":[],"error":null} diff --git a/package-lock.json b/package-lock.json index e4f6eb5..c67ca75 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,7 +35,8 @@ "tsx": "^4.23.13", "typescript": "~5.9.3", "typescript-eslint": "^8.48.0", - "vite": "^7.3.1" + "vite": "^7.3.1", + "vitest": "^3.2.7" } }, "node_modules/@babel/code-frame": { @@ -1608,6 +1609,24 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1976,6 +1995,121 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, + "node_modules/@vitest/expect": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", + "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", + "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.7", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", + "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz", + "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.7", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz", + "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz", + "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz", + "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -2040,6 +2174,16 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/babel-plugin-macros": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", @@ -2118,6 +2262,16 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -2158,6 +2312,23 @@ "url": "https://www.paypal.me/kirilvatev" } }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -2175,6 +2346,16 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2285,6 +2466,16 @@ } } }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -2324,6 +2515,13 @@ "is-arrayish": "^0.2.1" } }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, "node_modules/esbuild": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", @@ -2563,6 +2761,16 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -2573,6 +2781,16 @@ "node": ">=0.10.0" } }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3007,6 +3225,13 @@ "loose-envify": "cli.js" } }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -3017,6 +3242,16 @@ "yallist": "^3.0.2" } }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/memoize-one": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", @@ -3205,6 +3440,23 @@ "node": ">=8" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -3532,6 +3784,13 @@ "node": ">=8" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -3551,6 +3810,20 @@ "node": ">=0.10.0" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -3564,6 +3837,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, "node_modules/stylis": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", @@ -3595,6 +3888,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -3612,6 +3919,36 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.6.tgz", + "integrity": "sha512-u8KszXvGfU68hVcZpRHKG28T0krMuv2G5nDhiHaMLen/gIuFEgIJhaJuO69qjnXg5paSrbPMFfx3brNuN8eVSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/ts-api-utils": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", @@ -4318,6 +4655,102 @@ } } }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz", + "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.7", + "@vitest/mocker": "3.2.7", + "@vitest/pretty-format": "^3.2.7", + "@vitest/runner": "3.2.7", + "@vitest/snapshot": "3.2.7", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.7", + "@vitest/ui": "3.2.7", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -4334,6 +4767,23 @@ "node": ">= 8" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", diff --git a/package.json b/package.json index 69af60f..bc7c07c 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,8 @@ "check-query-joins": "tsx scripts/check-query-joins.mts", "check-query-snapshots": "tsx scripts/check-query-snapshots.mts", "query-matrix": "tsx scripts/query-matrix.mts", - "health-check": "tsx scripts/health-check.mts" + "health-check": "tsx scripts/health-check.mts", + "test": "vitest run" }, "dependencies": { "@tanstack/react-query": "^5.90.21", @@ -48,6 +49,7 @@ "tsx": "^4.23.13", "typescript": "~5.9.3", "typescript-eslint": "^8.48.0", - "vite": "^7.3.1" + "vite": "^7.3.1", + "vitest": "^3.2.7" } } diff --git a/src/App.css b/src/App.css index 87d1768..0e42462 100644 --- a/src/App.css +++ b/src/App.css @@ -2556,3 +2556,128 @@ font-weight: 600; cursor: pointer; } + +/* Floats bottom-left like the map's own controls, and above the 500 error + overlay so the SPARQL of a failed run is still one click away. */ +.sparql-chip { + position: absolute; + bottom: 0.75rem; + left: 0.75rem; + z-index: 900; + display: inline-flex; + align-items: center; + gap: 0.375rem; + padding: 0.375rem 0.75rem; + background: white; + border: 1px solid var(--color-primary-200); + border-radius: var(--radius-md); + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.12); + color: var(--color-primary-700); + font-size: var(--text-sm); + font-weight: 600; + cursor: pointer; +} + +.sparql-chip:hover { + background: var(--color-primary-50); + border-color: var(--color-primary-700); +} + +.sparql-modal { + display: flex; + flex-direction: column; + width: 90%; + max-width: 900px; + max-height: 85vh; + background: white; + border-radius: var(--radius-lg); + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.25); + font-size: var(--text-sm); +} + +.sparql-modal-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.75rem 1rem; + border-bottom: 1px solid var(--color-gray-200); +} + +.sparql-modal-header h2 { + margin: 0; + font-size: var(--text-base); +} + +.sparql-modal-close { + padding: 0 0.375rem; + background: none; + border: none; + color: var(--color-gray-600); + font-size: 1.25rem; + line-height: 1; + cursor: pointer; +} + +.sparql-modal-body { + overflow-y: auto; + padding: 1rem; + display: flex; + flex-direction: column; + gap: 1rem; +} + +.sparql-block-header { + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 0.375rem; + color: var(--color-gray-700); +} + +.sparql-block-header > span:first-child { + flex: 1; + min-width: 0; +} + +.sparql-block-failed { + padding: 0.0625rem 0.375rem; + background: var(--color-red-100); + border-radius: var(--radius-sm, 4px); + color: var(--color-red-700); + font-size: var(--text-xs, 0.75rem); + font-weight: 600; +} + +.sparql-copy { + display: inline-flex; + align-items: center; + flex-shrink: 0; + padding: 0.25rem 0.5rem; + background: var(--color-primary-50); + border: 1px solid var(--color-primary-200); + border-radius: var(--radius-sm, 4px); + color: var(--color-primary-700); + font-size: var(--text-xs, 0.75rem); + font-weight: 600; + cursor: pointer; +} + +.sparql-block-error { + margin: 0 0 0.375rem; + color: var(--color-red-700); + font-size: var(--text-xs, 0.75rem); +} + +.sparql-block pre { + margin: 0; + max-height: 320px; + overflow: auto; + padding: 0.75rem; + background: var(--color-gray-50); + border: 1px solid var(--color-gray-200); + border-radius: var(--radius-md); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-size: var(--text-xs, 0.75rem); + line-height: 1.5; + white-space: pre; +} diff --git a/src/components/Layout/MainContent.tsx b/src/components/Layout/MainContent.tsx index 604894e..4c04637 100644 --- a/src/components/Layout/MainContent.tsx +++ b/src/components/Layout/MainContent.tsx @@ -3,6 +3,7 @@ import { ResultsMap } from '../Map/ResultsMap'; import { PipelineProgressStrip } from '../Pipeline/PipelineProgressStrip'; import { PartialResultsNotice } from '../Pipeline/PartialResultsNotice'; import { CachedResultNotice } from '../Pipeline/CachedResultNotice'; +import { SparqlChip } from '../Pipeline/SparqlChip'; import { useQueryStore } from '../../store/queryStore'; import { useMapLayers } from '../../hooks/useMapLayers'; import { useQueryPipeline } from '../../hooks/useQueryPipeline'; @@ -29,6 +30,7 @@ export function MainContent() { + ); diff --git a/src/components/Pipeline/SparqlChip.tsx b/src/components/Pipeline/SparqlChip.tsx new file mode 100644 index 0000000..ad982f4 --- /dev/null +++ b/src/components/Pipeline/SparqlChip.tsx @@ -0,0 +1,118 @@ +import { useEffect, useState } from 'react'; +import { useQueryStore } from '../../store/queryStore'; +import { PREBUILT_QUERIES } from '../../constants/prebuiltQueries'; +import { canonicalQuestion } from '../../engine/cacheKey'; +import type { SentQuery } from '../../engine/executor'; + +// Prebuilt questions only, and only while unedited: an edited prebuilt is a +// different question, and its SPARQL is no longer the one we vouch for. +function useIsUneditedPrebuilt(): boolean { + const activeQueryId = useQueryStore((s) => s.activeQueryId); + const question = useQueryStore((s) => s.question); + const prebuilt = PREBUILT_QUERIES.find((q) => q.id === activeQueryId); + if (!prebuilt) return false; + return ( + JSON.stringify(canonicalQuestion(question)) === + JSON.stringify(canonicalQuestion(prebuilt.question)) + ); +} + +export function SparqlChip() { + const isPrebuilt = useIsUneditedPrebuilt(); + const isRunning = useQueryStore((s) => s.isRunning); + const queries = useQueryStore((s) => s.pipelineResult?.queries); + const [open, setOpen] = useState(false); + + if (!isPrebuilt || isRunning || !queries?.length) return null; + + return ( + <> + setOpen(true)}> + + + + + See Full SPARQL + + {open && setOpen(false)} />} + > + ); +} + +function SparqlModal({ queries, onClose }: { queries: SentQuery[]; onClose: () => void }) { + useEffect(() => { + const onKey = (e: KeyboardEvent) => e.key === 'Escape' && onClose(); + window.addEventListener('keydown', onKey); + return () => window.removeEventListener('keydown', onKey); + }, [onClose]); + + return ( + + e.stopPropagation()} + role='dialog' + aria-modal='true' + aria-label='Full SPARQL' + > + + Full SPARQL + + × + + + + {queries.map((q, i) => ( + + ))} + + + + ); +} + +function SparqlBlock({ sent }: { sent: SentQuery }) { + const [copied, setCopied] = useState(false); + + const copy = async () => { + try { + await navigator.clipboard.writeText(sent.query.trim()); + setCopied(true); + setTimeout(() => setCopied(false), 1500); + } catch { + /* clipboard unavailable */ + } + }; + + return ( + + + + Step {sent.step + 1} · {sent.description} · {sent.endpoint} + {sent.scope && ` · ${sent.scope}`} + + {sent.error && Failed} + void copy()} + aria-label='Copy SPARQL' + title='Copy SPARQL' + > + {copied ? ( + 'Copied' + ) : ( + + + + + )} + + + {sent.error && {sent.error}} + + {sent.query.trim()} + + + ); +} diff --git a/src/engine/cacheKey.ts b/src/engine/cacheKey.ts index 935b056..0df07b8 100644 --- a/src/engine/cacheKey.ts +++ b/src/engine/cacheKey.ts @@ -7,7 +7,7 @@ import type { AnalysisQuestion } from '../types/query'; // // Bump WIRE_VERSION when the set of result keys we store changes (see wire.ts) — // old entries would then be missing data the map expects. -const WIRE_VERSION = 'w1'; +const WIRE_VERSION = 'w2'; // w2: results carry the SPARQL that produced them // Bump DATA_VERSION after a knowledge-graph reload to make every existing key // unreachable in one move. diff --git a/src/engine/executor.ts b/src/engine/executor.ts index 38f34f0..ec271fa 100644 --- a/src/engine/executor.ts +++ b/src/engine/executor.ts @@ -1,5 +1,6 @@ import type { AnalysisQuestion } from '../types/query'; import type { SparqlRow } from '../types/sparql'; +import type { EndpointKey } from '../constants/endpoints'; import type { PipelineStep, PipelineContext } from './planner'; import type { Scope } from './scope'; import { executeSparql } from './sparqlClient'; @@ -15,17 +16,30 @@ export interface PartialFailure { skipped: string[]; } +// One request as it was actually sent, kept so the exact SPARQL behind a map +// can be shown and copied. A step that was split appears once per slice. +export interface SentQuery { + step: number; + description: string; + endpoint: EndpointKey; + scope?: string; + query: string; + error?: string; +} + export interface PipelineSuccess { status: 'success'; data: Record; // Set when some slices of the work failed but enough succeeded to show a map. partial?: PartialFailure[]; + queries?: SentQuery[]; } export interface PipelineEmpty { status: 'empty'; failedAtStep: number; message: string; + queries?: SentQuery[]; } export interface PipelineError { @@ -33,6 +47,7 @@ export interface PipelineError { failedAtStep: number; message: string; error: Error; + queries?: SentQuery[]; } export type PipelineResult = PipelineSuccess | PipelineEmpty | PipelineError; @@ -85,6 +100,7 @@ async function runStep( step: PipelineStep, context: PipelineContext, report: (chunksDone: number, chunksTotal: number) => void, + record: (sent: Omit) => void, ): Promise { const startedAt = Date.now(); const queue: (Scope | undefined)[] = step.initialScopes?.(context) ?? [undefined]; @@ -122,6 +138,7 @@ async function runStep( try { const result = await executeSparql(step.endpoint, query, { cache: step.cache }); + record({ scope: scope?.label, query }); // Slices can overlap — the same river reach is reached from several // anchors — so merge as a set. Whole-row identity is the right key: for // aggregate rows the GROUP BY key never spans slices (chunking always @@ -136,6 +153,7 @@ async function runStep( } catch (err) { const error = err instanceof Error ? err : new Error(String(err)); lastError = error; + record({ scope: scope?.label, query, error: error.message }); const splittable = error instanceof SparqlError && isSplittable(error.kind); if (splittable && !scope) knownTooLarge.add(query); const smaller = splittable ? await step.divide?.(context, scope) : null; @@ -166,6 +184,7 @@ export async function executePipeline( results: {}, }; const partial: PartialFailure[] = []; + const queries: SentQuery[] = []; for (let i = 0; i < steps.length; i++) { const step = steps[i]; @@ -174,11 +193,16 @@ export async function executePipeline( let outcome: StepOutcome; try { - outcome = await runStep(step, context, (chunksDone, chunksTotal) => { - if (chunksTotal > 1) { - onProgress({ ...base, status: 'running', chunksDone, chunksTotal }); - } - }); + outcome = await runStep( + step, + context, + (chunksDone, chunksTotal) => { + if (chunksTotal > 1) { + onProgress({ ...base, status: 'running', chunksDone, chunksTotal }); + } + }, + (sent) => queries.push({ step: i, description: step.description, endpoint: step.endpoint, ...sent }), + ); } catch (err) { onProgress({ ...base, status: 'failed' }); return { @@ -186,6 +210,7 @@ export async function executePipeline( failedAtStep: i, message: `Error at step: ${step.description}`, error: err instanceof Error ? err : new Error(String(err)), + queries, }; } @@ -198,6 +223,7 @@ export async function executePipeline( failedAtStep: i, message: `Error at step: ${step.description}`, error: outcome.lastError ?? new Error('Query failed'), + queries, }; } if (missing.length > 0) { @@ -222,6 +248,7 @@ export async function executePipeline( status: 'empty', failedAtStep: i, message: `No results at step: ${step.description}`, + queries, }; } } @@ -248,5 +275,6 @@ export async function executePipeline( status: 'success', data: context.results, ...(partial.length > 0 ? { partial } : {}), + queries, }; } diff --git a/src/engine/wire.test.ts b/src/engine/wire.test.ts new file mode 100644 index 0000000..173eb61 --- /dev/null +++ b/src/engine/wire.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, it } from 'vitest'; +import { fromWire, isWireResult, toWire } from './wire'; +import type { PipelineSuccess } from './executor'; + +describe('wire', () => { + it('keeps the SPARQL that produced a result through the cache round trip', () => { + const result: PipelineSuccess = { + status: 'success', + data: { FIND_TARGET_ENTITIES: [{ iri: 'http://example.org/a' }] }, + queries: [ + { step: 0, description: 'Finding samples', endpoint: 'federation', query: 'SELECT * {}' }, + { step: 3, description: 'Loading details', endpoint: 'federation', scope: 'batch 1', query: 'SELECT ?x {}', error: 'timeout' }, + ], + }; + const wire = JSON.parse(JSON.stringify(toWire(result))); + expect(isWireResult(wire)).toBe(true); + expect(fromWire(wire).queries).toEqual(result.queries); + }); + +}); diff --git a/src/engine/wire.ts b/src/engine/wire.ts index b221e29..ba618a8 100644 --- a/src/engine/wire.ts +++ b/src/engine/wire.ts @@ -1,5 +1,5 @@ import type { SparqlRow } from '../types/sparql'; -import type { PartialFailure, PipelineSuccess } from './executor'; +import type { PartialFailure, PipelineSuccess, SentQuery } from './executor'; // The shape a cached result takes over the wire and in Postgres. // @@ -10,6 +10,8 @@ export interface WireResult { status: 'success'; data: Record; partial?: PartialFailure[]; + // The SPARQL that produced this result, for the "See Full SPARQL" modal. + queries?: SentQuery[]; computedAt: string; } @@ -32,6 +34,7 @@ export function toWire(result: PipelineSuccess, computedAt = new Date().toISOStr status: 'success', data, ...(result.partial?.length ? { partial: result.partial } : {}), + ...(result.queries?.length ? { queries: result.queries } : {}), computedAt, }; } @@ -41,6 +44,7 @@ export function fromWire(wire: WireResult): PipelineSuccess { status: 'success', data: wire.data, ...(wire.partial?.length ? { partial: wire.partial } : {}), + ...(wire.queries?.length ? { queries: wire.queries } : {}), }; }
{sent.error}
+ {sent.query.trim()} +
{sent.query.trim()}