Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
88 commits
Select commit Hold shift + click to select a range
1bb629f
feat(engine): add streams entity type and distance-bounded flow tracing
prayaslashkari Aug 25, 2026
0ff2f39
feat(engine): extend bounded traces one flowline past the cutoff
prayaslashkari Aug 25, 2026
658070f
Merge remote-tracking branch 'origin/main' into feat/stream-distance-…
prayaslashkari Aug 27, 2026
65660a1
Merge remote-tracking branch 'origin/main' into feat/stream-distance-…
prayaslashkari Aug 27, 2026
3818e81
fix(substance): read labels from rdfs:label and stop dropping unlabel…
prayaslashkari Sep 8, 2026
8d1bb43
feat(dropdowns): always show the Select all count, and comma format c…
prayaslashkari Sep 8, 2026
5b7aa74
docs: correct the substance label model in the wiki, schema and debug…
prayaslashkari Sep 8, 2026
fb85193
docs(changelog): add week 37 and correct the week 36 substance entry
prayaslashkari Sep 8, 2026
54172bd
fix(substance): name all 101 substances via the source parameter
prayaslashkari Sep 9, 2026
826dcca
docs: record the substance label chain and the QLever aggregate quirks
prayaslashkari Sep 9, 2026
ebe0e74
fix(prefixes): drop the dead me_egad_data v1 prefix
prayaslashkari Sep 11, 2026
4bdf238
docs: correct the Material dropdown page after the namespace consolid…
prayaslashkari Sep 11, 2026
f4da9f8
fix(samples): derive a single-valued result_value from qudt:quantityV…
prayaslashkari Sep 11, 2026
e346d39
fix(samples): repair the dead non-detect filter
prayaslashkari Sep 11, 2026
7cbe0e8
fix(samples): stop the required unit join from hiding non-detects
prayaslashkari Sep 11, 2026
ef0c40b
fix(material): point material type URIs at the live root namespace
prayaslashkari Sep 11, 2026
1574327
docs(changelog): record the material IRI fix and correct week 37
prayaslashkari Sep 11, 2026
6bf13d8
feat(material): make the group headings selectable and collapsible
prayaslashkari Sep 11, 2026
c66febd
docs: update the dropdown wiki for the material tree, and record two …
prayaslashkari Sep 11, 2026
11b5427
fix(namespaces): migrate stored questions off the dead -data vocabulary
prayaslashkari Sep 11, 2026
f87ab9d
docs(plans): consolidate the material and upstream-breaking-change ch…
prayaslashkari Sep 11, 2026
282801a
perf(engine): bound query work by splitting on failure
prayaslashkari Sep 14, 2026
37d9a4f
fix(engine): bound request time, simplify the split budget
prayaslashkari Sep 14, 2026
71dc271
docs: record the 429 finding where the next person will look
prayaslashkari Sep 14, 2026
e2aaa6f
feat(cache): serve published and dashboard results from a result cache
prayaslashkari Sep 14, 2026
767e1b5
ci: warm the result cache from a button in the Actions tab
prayaslashkari Sep 14, 2026
9dd9da5
Merge branch 'development' into perf/bounded-query-execution
prayaslashkari Sep 14, 2026
54634c9
Merge branch 'perf/bounded-query-execution' into phase-3-result-cache
prayaslashkari Sep 14, 2026
d46404a
Merge pull request #38 from SAWGraph/perf/bounded-query-execution
prayaslashkari Sep 14, 2026
247cc6a
Merge remote-tracking branch 'origin/development' into phase-3-result…
prayaslashkari Sep 14, 2026
a10a325
Merge pull request #40 from SAWGraph/phase-3-result-cache
prayaslashkari Sep 14, 2026
5c7199f
Merge feat/stream-distance-tracing (PR #32) into development
prayaslashkari Sep 14, 2026
25f5a3c
Merge pull request #41 from SAWGraph/stream-trim-connecting
prayaslashkari Sep 14, 2026
81377ba
fix(cache): include maxDistanceKm in the result-cache key
prayaslashkari Sep 14, 2026
6d3bc05
Merge pull request #42 from SAWGraph/fix/cache-key-distance
prayaslashkari Sep 14, 2026
0b21295
docs: record the distance bound, the rejected trim, and the cache-key…
prayaslashkari Sep 14, 2026
a119e16
fix: name the slice that failed, and give advice that applies
prayaslashkari Sep 14, 2026
81cbef2
docs: record which slice fails and what rescues it (Part 8.1)
prayaslashkari Sep 14, 2026
dc39b23
docs: rewrite ARCHITECTURE.md against the code it describes
prayaslashkari Sep 14, 2026
9ecbbe0
docs: archive the two finished execution plans
prayaslashkari Sep 14, 2026
b4c73be
feat: weekly dashboard health check with a status page
prayaslashkari Sep 14, 2026
e03aa92
ci: serialise every workflow that queries the graph
prayaslashkari Sep 14, 2026
ff8ad0c
docs: record what a matrix sweep actually costs, per mode
prayaslashkari Sep 14, 2026
39de9fa
docs: one dated file per matrix sweep, with a generated index
prayaslashkari Sep 14, 2026
a0fdaca
fix: put the commit in the sweep filename
prayaslashkari Sep 14, 2026
9128aa9
fix: engine-mode sweeps crashed on the first query
prayaslashkari Sep 14, 2026
6578707
docs: record the contaminated sweep, and stop the index lying about o…
prayaslashkari Sep 15, 2026
15baaf3
docs: record the contaminated sweep, and stop the index lying about o…
prayaslashkari Sep 15, 2026
4dfa1e5
Merge pull request #43 from SAWGraph/docs/stream-distance-and-cache-key
prayaslashkari Sep 15, 2026
62e56f7
Merge pull request #44 from SAWGraph/fix/name-the-failing-slice
prayaslashkari Sep 15, 2026
cbd8d3f
fix: the sample popup named substances by a different rule than the d…
prayaslashkari Sep 15, 2026
c35d722
fix: pipeline step labels named only one of the two blocks
prayaslashkari Sep 16, 2026
f28f389
fix: facility popups link to EPA FRS only
prayaslashkari Sep 16, 2026
354462d
Merge pull request #49 from SAWGraph/fix/facility-popup-frs-links
prayaslashkari Sep 16, 2026
eff8197
Merge pull request #48 from SAWGraph/fix/pipeline-step-labels
prayaslashkari Sep 16, 2026
0ce9697
Merge remote-tracking branch 'origin/development' into fix/substance-…
Copilot Sep 16, 2026
e61692d
Merge pull request #47 from SAWGraph/fix/substance-naming-and-repeat-…
prayaslashkari Sep 16, 2026
90f8e5d
fix: upstream questions traced the wrong way
prayaslashkari Sep 16, 2026
48192ca
fix: well hydration re-derived the trace the old way
prayaslashkari Sep 16, 2026
5d1a67c
docs: record the upstream direction bug, and make the self-checks run
prayaslashkari Sep 16, 2026
3fa9caf
feat(dashboard): upstream questions, and four distinct shapes on the …
prayaslashkari Sep 16, 2026
9b21bf9
style: one blue palette and Inter across the app
prayaslashkari Sep 17, 2026
73bcce0
feat(dashboard): intro band explaining what Explorer is
prayaslashkari Sep 17, 2026
9a8b43b
style: no em dashes in user-facing copy
prayaslashkari Sep 17, 2026
65f7444
fix(map): Esri canvas tiles for the light and dark basemaps
prayaslashkari Sep 17, 2026
b5e9b7e
feat(dashboard): map thumbnail on each prebuilt card
prayaslashkari Sep 17, 2026
ee968bd
Merge pull request #51 from SAWGraph/fix/upstream-direction
prayaslashkari Sep 17, 2026
8e9b67a
fix(dashboard): thumbnail row layout leaked onto the community cards
prayaslashkari Sep 17, 2026
e1ca5c8
Merge pull request #53 from SAWGraph/fix/community-card-alignment
prayaslashkari Sep 17, 2026
66f3fda
fix(engine): stop two silent filters in the fused queries
prayaslashkari Sep 18, 2026
b7bcc73
fix(engine): bound the flowline layer at both ends
prayaslashkari Sep 19, 2026
1522403
Merge pull request #56 from SAWGraph/fix-upstream-flowline
prayaslashkari Sep 19, 2026
813f008
docs(plans): draft the symmetric cell-widening fix
prayaslashkari Sep 19, 2026
a232aec
Merge fix-upstream-flowline: draft plan for symmetric cell widening
prayaslashkari Sep 19, 2026
718193c
refactor(engine): one definition of "is this side constrained"
prayaslashkari Sep 19, 2026
18acb5b
docs: record the non-detect behaviour, the stream-layer bug, and the …
prayaslashkari Sep 19, 2026
7302d7c
docs(changelog): week 38 entries for the two silent filters and the s…
prayaslashkari Sep 19, 2026
002e529
fix(pipeline): align the timeline dot with the first line of its text
prayaslashkari Sep 19, 2026
e84a11f
test(matrix): sweep the flow-distance bound, which nothing covered
prayaslashkari Sep 20, 2026
ec221d9
fix(engine): seed the bounded trace from the constrained side
prayaslashkari Sep 20, 2026
4454b80
test(engine): golden-file snapshot of the SPARQL every shape generates
prayaslashkari Sep 20, 2026
3de9eb2
docs(matrix): the warm before-and-after for the bounded seed-side fix
prayaslashkari Sep 20, 2026
0abdc49
docs: the seed-side fix, the sweep that could not see it, and two traps
prayaslashkari Sep 20, 2026
98190de
Merge pull request #55 from SAWGraph/fix/fused-seed-order-and-obs-joins
prayaslashkari Sep 20, 2026
abca208
fix(engine): rank how narrow a side is, instead of asking whether it is
prayaslashkari Sep 20, 2026
d00d893
docs: narrowing a question by industry used to break it
prayaslashkari Sep 20, 2026
e73d84c
docs: correct the industry-code limit, and clear the clause blamed fo…
prayaslashkari Sep 20, 2026
01a6899
Merge pull request #58 from SAWGraph/fix/narrowness-ranking
prayaslashkari Sep 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# Frontend
# URL of the sawgraph-api service (e.g. http://localhost:3001 in dev,
# https://sawgraph-api.onrender.com in prod).
# URL of the API service:
# local dev http://localhost:3001
# development https://sawgraph-explorer-api-development.up.railway.app
# production https://sawgraph-explorer-api.up.railway.app
VITE_API_BASE_URL=http://localhost:3001
72 changes: 72 additions & 0 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
name: Checks

# The repo's self-checks, on every pull request. They were all runnable before
# this file existed and none of them ran unless someone remembered to, which is
# how an engine bug that every one of these styles of check would have caught
# (upstream tracing the relation backwards, see docs/DEBUGGING.md 2026-09-16)
# survived from the first commit to 2026-09-16.
#
# Everything here is offline and takes seconds. No secrets, no SPARQL, no
# endpoint concurrency group: the live-endpoint workflows (health-check.yml,
# warm-cache.yml) are the ones that have to queue.

on:
pull_request:
push:
branches: [main, development]
workflow_dispatch:

jobs:
checks:
runs-on: ubuntu-latest
timeout-minutes: 15

steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: '20'
cache: npm

# react-leaflet's peer range does not include React 19 (see CLAUDE.md)
- run: npm ci --legacy-peer-deps

- name: Build (tsc -b && vite build)
run: npm run build

# Each of these asserts something that is silent when wrong: a wrong
# answer, a mislabelled step, a cache key that collides. They fail the
# build rather than reporting, because a self-check nobody is obliged to
# read is a self-check that does not exist.
- name: Trace direction
run: npm run check-trace-direction

- name: Flowline scope
run: npm run check-flowline-scope

- name: Cache key
run: npm run check-cache-key

- name: Step labels
run: npm run check-step-labels

- name: Substance labels
run: npm run check-substance-labels

- name: Query join order and observation joins
run: npm run check-query-joins

# Golden-file diff of the SPARQL every shape generates. The one above
# asserts a rule; this one notices when a change moves a shape nobody was
# thinking about, which is the failure mode of a shared query builder.
- name: Query snapshots
run: npm run check-query-snapshots

# Not blocking yet: `npm run lint` reports 15 pre-existing errors, all in
# components and none related to the engine. Make this a required step in
# the same change that clears them, not before, or the signal is noise
# from day one.
- name: Lint (advisory)
run: npm run lint
continue-on-error: true
135 changes: 135 additions & 0 deletions .github/workflows/health-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
name: Dashboard health

# Runs every dashboard question against the live endpoints once a week and
# writes down what happened: worked or not, how long, how many rows. The point
# is dating a regression. Without this, "the Indiana question broke sometime in
# the last month" is the best anyone can say.
#
# Both branches are measured because they hit the same SPARQL endpoints — so a
# difference between main and development is a difference in *our* code.
#
# No secrets. This never reads or writes the result cache: a cached answer would
# report on the cache rather than on the graph.
#
# The schedule only fires once this file is on the default branch (main) —
# GitHub ignores cron in workflows that live only on a feature branch.

on:
schedule:
- cron: '0 6 * * 1' # Mondays, 06:00 UTC
workflow_dispatch:

# One job at a time across *every* workflow that queries the graph — this group
# name is shared with warm-cache.yml on purpose. Both run the same eight heavy
# questions, and a warm run started by hand on a Monday morning would otherwise
# land on top of the health cron. That is not hypothetical: three questions were
# recorded as broken while this was being built, purely because something else
# was querying at the same time, and all three passed on retry.
concurrency:
group: sawgraph-endpoint
cancel-in-progress: false

permissions:
contents: write # commit the history file and status page
issues: write # open an issue when a question regresses

jobs:
measure:
runs-on: ubuntu-latest
timeout-minutes: 90

steps:
- name: Check out development (the branch results are committed to)
uses: actions/checkout@v4
with:
ref: development

- name: Check out main alongside it
uses: actions/checkout@v4
with:
ref: main
path: main-ref

- uses: actions/setup-node@v4
with:
node-version: '20'
cache: npm

# react-leaflet's peer range does not include React 19 (see CLAUDE.md)
- run: npm ci --legacy-peer-deps

- name: Measure development
id: dev
# bash, not the default sh-with-e: without pipefail the exit code would
# be tee's, and a regression would sail through as a pass.
shell: bash
run: npm run health-check -- development | tee dev.log
continue-on-error: true

- name: Install main's dependencies
working-directory: main-ref
run: npm ci --legacy-peer-deps

# Runs main's code, but appends to development's history file so both
# branches land in one commit and one status page.
- name: Measure main
id: main
working-directory: main-ref
shell: bash
run: npm run health-check -- main --out "$GITHUB_WORKSPACE/docs/health/history.jsonl" | tee "$GITHUB_WORKSPACE/main.log"
continue-on-error: true

- name: Commit the results
run: |
git config user.name 'github-actions[bot]'
git config user.email 'github-actions[bot]@users.noreply.github.com'
git add docs/health/
if git diff --cached --quiet; then
echo "Nothing changed."
else
git commit -m "chore: dashboard health $(date -u +%Y-%m-%d)"
git push
fi

- name: Summary
if: always()
run: |
{
echo "### Dashboard health"
echo
echo '```'
tail -24 dev.log 2>/dev/null || echo "development: no output"
echo
tail -24 main.log 2>/dev/null || echo "main: no output"
echo '```'
} >> "$GITHUB_STEP_SUMMARY"

# Exit code 2 from the script means a question that worked last week does
# not work now. An issue rather than just a red X, because a red X in a
# weekly job is easy to miss for another week.
- name: Open an issue for regressions
if: steps.dev.outcome == 'failure' || steps.main.outcome == 'failure'
env:
GH_TOKEN: ${{ github.token }}
run: |
{
echo "The weekly health check found a dashboard question that worked before and does not now."
echo
echo "See [the run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) and [docs/health/STATUS.md](${{ github.server_url }}/${{ github.repository }}/blob/development/docs/health/STATUS.md)."
echo
for log in dev.log main.log; do
[ -f "$log" ] || continue
echo "### ${log%.log}"
echo '```'
sed -n '/REGRESSIONS/,$p' "$log" 2>/dev/null | head -40
echo '```'
done
} > issue.md
gh issue create \
--title "Dashboard health: a question regressed ($(date -u +%Y-%m-%d))" \
--body-file issue.md \
--label bug

- name: Fail if a question regressed
if: steps.dev.outcome == 'failure' || steps.main.outcome == 'failure'
run: exit 1
85 changes: 85 additions & 0 deletions .github/workflows/warm-cache.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
name: Warm result cache

# Runs the dashboard questions and stores their results, so visitors get them
# instantly instead of waiting 10-70s for a live pipeline run.
#
# Manual on purpose: a cached entry only goes stale when the knowledge graph
# reloads or its 30-day TTL expires, and both are things we control. To make it
# periodic anyway, add a schedule trigger:
#
# schedule:
# - cron: '0 6 * * 1' # Mondays, 06:00 UTC
#
# Setup (once): add the repository secrets CACHE_WRITE_TOKEN_DEV and
# CACHE_WRITE_TOKEN_PROD, matching CACHE_WRITE_TOKEN on each API service.

on:
workflow_dispatch:
inputs:
environment:
description: Which API to warm
type: choice
required: true
default: development
options: [development, production]
only:
description: 'Optional: only questions whose title contains this text'
type: string
required: false
allow_partial:
description: Cache results even when some slices failed
type: boolean
default: false

# One job at a time across *every* workflow that queries the graph — shared with
# health-check.yml, so a warm run and the weekly health check queue rather than
# compete for the same graph engine. Warming both environments now serialises
# too, which costs a few minutes and buys clean measurements.
concurrency:
group: sawgraph-endpoint
cancel-in-progress: false

jobs:
warm:
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: '20'
cache: npm

# react-leaflet's peer range does not include React 19 (see CLAUDE.md)
- run: npm ci --legacy-peer-deps

- name: Warm the cache
env:
API_BASE: >-
${{ inputs.environment == 'production'
&& 'https://sawgraph-explorer-api.up.railway.app'
|| 'https://sawgraph-explorer-api-development.up.railway.app' }}
CACHE_WRITE_TOKEN: >-
${{ inputs.environment == 'production'
&& secrets.CACHE_WRITE_TOKEN_PROD
|| secrets.CACHE_WRITE_TOKEN_DEV }}
ALLOW_PARTIAL: ${{ inputs.allow_partial && '1' || '0' }}
run: |
if [ -z "$CACHE_WRITE_TOKEN" ]; then
echo "::error::No cache write token for '${{ inputs.environment }}'."
echo "::error::Add CACHE_WRITE_TOKEN_DEV / CACHE_WRITE_TOKEN_PROD as repository secrets."
exit 1
fi
npm run warm-cache -- "${{ inputs.only }}" | tee warm-cache.log

- name: Summary
if: always()
run: |
{
echo "### Warm result cache — ${{ inputs.environment }}"
echo
echo '```'
tail -20 warm-cache.log 2>/dev/null || echo "no output"
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
43 changes: 32 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,19 @@ npm run preview # Preview production build

## Deployment

Deployed on **Railway** from the `main` branch. Build config lives in the Railway
dashboard, not in this repo — there is no `railway.json` or `nixpacks.toml` here.
Deployed on **Railway**, two environments, each with a frontend and an API
service. Build config lives in the Railway dashboard, not in this repo — there
is no `railway.json` or `nixpacks.toml` here.

| Environment | Branch | Frontend | API |
| --- | --- | --- | --- |
| Production | `main` | https://sawgraph-explorer.up.railway.app | https://sawgraph-explorer-api.up.railway.app |
| Development | `development` | https://sawgraph-explorer-development.up.railway.app | https://sawgraph-explorer-api-development.up.railway.app |

The frontend reaches the API through `VITE_API_BASE_URL`, baked in at build
time, so each environment's frontend points at its own API. The API allows the
frontend's origin via `FRONTEND_ORIGIN`. Both services share a Postgres
instance per environment.

## How queries work

Expand All @@ -56,9 +67,14 @@ When you click Apply, the query engine:
2. **Executes** steps sequentially (`engine/executor.ts`), threading S2 cell sets between steps
3. **Renders** results as map layers (`resultTransformer.ts` → `MapFeature[]`)

Supported entity types: **samples**, **facilities**, **water bodies**
Supported entity types: **samples**, **facilities**, **water bodies**, **wells**, **streams**
Supported relationships: **near** (~1–2 km), **downstream**, **upstream**

Downstream/upstream traces accept an optional cumulative flowpath cutoff
(`Within N km of flow`). Unset, the trace is the full transitive closure.
`node scripts/flow-distance-check.mjs` verifies the bounded trace against the
live endpoints.

## SPARQL endpoints

All hosted at `apps.okn.us`:
Expand All @@ -85,11 +101,16 @@ All hosted at `apps.okn.us`:

Inside `docs/`:

| File | Contents |
| ----------------- | --------------------------------------------------- |
| `ARCHITECTURE.md` | System design, module boundaries, data flow |
| `SCHEMA.md` | Predicate inventories, class counts, endpoint roles |
| `DEBUGGING.md` | Documented bugs with root causes and fixes |
| `CONVENTIONS.md` | Coding standards, endpoint selection rules |
| `changelog/` | Weekly changelogs (`YYYY-Www.md`) |
| `plans/` | Feature planning: `drafts/` → `active/` → `done/` |
| File | Contents |
| ------------------- | --------------------------------------------------------------- |
| `ARCHITECTURE.md` | System design, module boundaries, data flow |
| `SCHEMA.md` | Predicate inventories, class counts, endpoint roles |
| `QUERY-MATRIX.md` | Every query shape, measured — plus the error catalogue (Part 4) |
| `health/STATUS.md` | Weekly dashboard health: working, timings, row-count drift |
| `query-matrix/` | One CSV per sweep, dated, with a generated index |
| `DEBUGGING.md` | Documented bugs with root causes and fixes |
| `CONVENTIONS.md` | Coding standards, endpoint selection rules |
| `wiki/` | One page per filter dropdown; source of truth for the GitHub Wiki |
| `queries/` | Write-ups of individual real questions |
| `changelog/` | Weekly changelogs (`YYYY-Www.md`) |
| `plans/` | Feature planning: `drafts/` → `active/` → `done/` |
Loading
Loading