factcircuit
FactCircuit는 불변 출처와 단계별 검증, 제한된 검색, 재현 가능한 평가를 제공하는 감사 가능한 주장·증거 검증 루프입니다
도시 하천을 촬영해 Forel‑Ule 색상으로 읽고 시민 관찰을 불확실성 고려 생태 평가로 전환한다
Photograph an urban stream, read its colour on the Forel–Ule scale, and turn citizen observations into uncertainty-aware ecological assessments.
데이터TypeScript · CSSGitHub @it-withend온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
A resident notices a stream. Rivulet tells the city how much to trust it, and where to look next.
A trust and quality layer for citizen observations of urban freshwater: photo and a few plain questions
in, an uncertainty-aware stream status, a One Health reading and a FHIR export out.
🌐 Live rivulet-xi.vercel.app
🏁 Track 2 — Data-to-Insight also 5 & 7
🧱 Stack Next.js · Supabase · FHIR
📄 Licence Apache-2.0
Built for the OneAquaHealth IEEE Global Hackathon 2026.README 전문 보기 ↗A resident notices a stream. Rivulet tells the city how much to trust it, and where to look next.
A trust and quality layer for citizen observations of urban freshwater: photo and a few plain questions in, an uncertainty-aware stream status, a One Health reading and a FHIR export out.
Map · City report · Method · Open data · Architecture · API · Science
Built for the OneAquaHealth IEEE Global Hackathon 2026.
Why this exists · Status · How it works · Screenshots · Documentation · Highlights · Scientific basis · Relationship to OneAquaHealth · What Rivulet does not claim · What is next · Synthetic demonstration data · Getting started · Project layout · Contributing & security · Licence and attribution
| Build | — type-check, ESLint and the Vitest suite |
| FHIR conformance | — full validator log |
| Standard | |
| Dependencies | npm audit --omit=dev reports 0 vulnerabilities (checked 2026-10-02); ci.yml re-checks on every push |
/observe/moderate, flag_reasonobservers.trust_score/city/certificates/<id>, /api/fhir/Observationflowchart LR
A[Resident report<br/>photo + survey] --> B{Plausibility checks<br/>GPS, distance, rate,<br/>optional AI photo check}
B -- all pass --> C[Auto-approved]
B -- any doubt --> D[Held for review]
D --> E{Moderator}
E -- approve --> C
E -- reject --> X[Excluded for good]
C --> F[Trust weight<br/>peer agreement]
F --> G[Bayesian estimate<br/>90% credible interval]
G --> H[Status class or<br/>insufficient data]
H --> I[Map, stream page,<br/>city report, FHIR]
flowchart TB
U[Resident or city browser] -->|HTTPS| V[Next.js app on Vercel<br/>pages + route handlers]
V --> S[(Supabase<br/>Postgres + PostGIS, RLS)]
V -.optional.-> Q[Groq vision<br/>is this water?]
O[OpenStreetMap / Overpass] -->|seed scripts| S
E[Sentinel-2] -->|pre-fetched by script| S
V --> F[FHIR Bundle<br/>OAH IG profiles]
F --> R[Researchers and city systems]
G[GitHub Actions] -->|builds IG, runs HL7 validator| F
Screenshots show the live prototype. Coimbra's reports are synthetic demonstration data — see What Rivulet does not claim.
docs/ARCHITECTURE.md — layers, data model, security and privacy, performancedocs/SCIENCE.md — the science engine, its basis, and how the survey relates to the OneAquaHealth field protocolsdocs/API.md — every endpoint, including the read-only FHIR API/method — the method as a live page, every constant included, generated from the code's own registry/method./city names where to take care and where a visit would help most.Seven stages, all in src/lib/science/ with tests beside them, and no learned model deciding a status:
The full registry, with a rationale for every entry (cited standard, or a declared prior with why it was chosen), lives
in src/lib/science/method-parameters.ts and is rendered live at /method;
ScoreDisclosure (src/components/water/ScoreDisclosure.tsx) surfaces it in the product itself under "Why this
score?". Full citations are in src/lib/science/method-version.ts; the fuller description, a diagram and the
relation to the OneAquaHealth field protocols are in docs/SCIENCE.md.
FHIR resources declare the HL7 Europe OneAquaHealth Implementation Guide profiles for indicators and locations. Real
measurements (pH, dissolved oxygen, temperature, nitrate) use the guide's codes; signs a resident sees but cannot
measure (clarity, sewage smell, algae, foam, dead fish, litter, flow state, the animal-group score) use a separate
Rivulet code system, served at /fhir/CodeSystem/rivulet-derived — see
Relationship to OneAquaHealth for exactly what was and was not checked.
Human review. /moderate lists everything currently held for review, with the reason, and lets a moderator
approve (human_approved, counted exactly like auto_approved) or reject (rejected, permanently excluded) an
observation; either action recomputes trust for every observer on that water body. There are no moderator accounts —
set RIVULET_MODERATOR_TOKEN to a shared passphrase; the page is unusable until that is configured. This is
deliberately minimal: right for a two-person team reviewing a pilot, not a production moderation system.
Optional photo check. If GROQ_API_KEY is set, a small (~160px, never stored) thumbnail of each submitted photo
is sent once to a Groq vision model asking "does this look like water?". A confident "no" — a meme, a selfie, a
screenshot — flags the observation for review instead of silently entering the model; a missing key, a timeout, or
an uncertain answer all mean the check is skipped, never that the observation is flagged. The full-resolution photo
is never uploaded, matching the on-device colour reading. Every plausibility signal is evaluated independently and
all that apply are recorded in flag_reason, so a report already flagged for one reason still gets its photo
checked for the others.
Certificates as a downloadable, printable document. /certificates/<id> renders as a landscape card (seal,
drawn signature, QR verification code) instead of a plain page, and /api/certificates/<id>/pdf (pure pdf-lib, no
headless browser) returns the same design as an actual PDF file for a CV or portfolio. A revoked certificate, or one
whose signature does not verify, is refused as a PDF rather than rendered.
Limitations, stated explicitly. Photo-derived colour is a proxy for water quality, not a laboratory measurement. Citizen observations are spatially biased toward accessible banks. Below a minimum data confidence, Rivulet reports "insufficient data" rather than guessing — missing evidence is never presented as good news.
The OneAquaHealth consortium has already built a CitizenScience App, a Community platform, an AI-based Environmental Surveillance System, a Decision Support System, and a FHIR Implementation Guide. Rivulet is an independent prototype: it is not affiliated with, endorsed by, or connected to the consortium's systems, and it is deliberately the complement to that stack, not a replacement for it — citizen data collection already exists, so Rivulet builds the quality and trust layer that turns resident observations into estimates with measurable uncertainty, expressed in WFD classes and encoded in the consortium's own FHIR Implementation Guide. What it does use, and what it does not:
Used — the OAH-FHIR Implementation Guide (hl7-eu/oah, FHIR 4.0.1, canonical http://hl7.eu/fhir/ig/oah).
Rivulet serves a read-only FHIR endpoint at /fhir (with a CapabilityStatement at /fhir/metadata; see
docs/API.md) and a one-stream export at /api/fhir/Observation?waterbody=<id>. Locations declare
…/StructureDefinition/location-oah and Observations declare …/StructureDefinition/observation-indicators-oah;
each report also gets a Provenance (author and assembling app), and a One Health warning becomes a DetectedIssue
evidenced by the reports behind it. Where the guide's code system (temporarySystem-oah-eu) has a code for
something Rivulet reports (pH, dissolved oxygen, water temperature, nitrate, …) that code is used; where it has none
(water colour, resident-reported signs, the flow state and animal-group score a resident reports, the classified
outcome) Rivulet's own code system is used and served at its canonical URL. Every exported value also carries the
stated way it was derived as an Observation.note, so a proxy is never mistaken for a measurement.
Validated in CI with the official HL7 validator. .github/workflows/validate-fhir.yml builds the guide from
source (hl7-eu/oah at a pinned commit, SUSHI 3.20.1), generates FHIR from Rivulet's own mapping code
(validation/generate-samples.ts calls the same functions the API serves) and runs validator_cli 6.10.4 against
FHIR 4.0.1 plus the guide and Rivulet's own CodeSystem. The workflow publishes the full log and the exact resources
it checked to the fhir-validation-report
branch. Current result: 0 errors across five checked files (a sample export, the endpoint's search bundles, its
CapabilityStatement and its CodeSystem). The warnings are of two kinds: resources without a narrative (dom-6, a
best-practice recommendation) and UCUM codes the offline validator cannot look up. The job found and fixed real
defects along the way — profile canonicals built from the profile name instead of its id, Location missing the
mandatory identifier and mode, and code displays that disagreed with Rivulet's CodeSystem.
It covers a bundle built from three representative reports (35 resources) and the endpoint's own responses, not
every row in the live database. It runs offline (-tx n/a), so external terminologies (UCUM units, SNOMED) are not
checked. Known gaps: performer is a display string, not a reference; the guide binds code to its indicator value
set as preferred, so Rivulet's own codes are permitted. The guide's foam concept is "Foam/colour/smell" (three
signs in one), so Rivulet keeps its narrower surface-foam code rather than stretching the guide's meaning.
To reproduce locally you need Node.js 22 and Java 17+:
bash validation/build-ig.sh && npx tsx validation/generate-samples.ts && bash validation/validate.sh
Not used — OneAquaHealth's applications. The CitizenScience App, Community, city dashboards and Resilience Map are login-gated, and no public API or open-data endpoint for them was found in the project's public material (checked 2026-09-21). Rivulet therefore neither reads from nor writes to them; the FHIR export is the interoperability path. The Resilience Map covers the same five pilot cities (Benevento, Coimbra, Ghent, Oslo, Toulouse) and can export CSV, which would be a natural cross-check for a later phase. The city report links out to the Resilience Map, the Citizen Science project and the Catalogue of Measures.
Complement, not overlap. OneAquaHealth's app guides a fuller stream assessment (clarity and flow, vegetation, wildlife, signs of pollution or alteration, land use, erosion) with photos and video. Rivulet covers the two-minute case: water colour and visible warning signs, and adds what citizen data needs before it can be trusted — plausibility checks, observer trust, uncertainty and human review. Rivulet does not assess flow, vegetation, wildlife, erosion or macroinvertebrates from field sampling.
is_synthetic = true and tagged HTEST in FHIR; they show the product working, not the real state of any stream.The model has not yet been validated against independent measurements. A pilot would do that:
Success would be judged on repeat participation, the share of streams with a fresh report, the share of raised warnings that independent checks confirm, and the time and cost of a useful check.
The seed script (scripts/seed/seed.ts) inserts realistic demonstration observations alongside the real
Overpass-sourced water bodies so the map, water body pages, and FHIR export have something to show during the
pilot. Every seeded row is marked observations.is_synthetic = true. That flag propagates end to end:
CommunityCounters on the landing page counts only real (is_synthetic = false) observations./api/fhir/Observation) tags every resource derived from a synthetic observation with
meta.tag = [{ system: "http://terminology.hl7.org/CodeSystem/v3-ActReason", code: "HTEST", display: "test health data" }], the standard HL7 code for test health data, so a downstream consumer can filter demo data out of
genuine citizen observations.npm install
npm install also runs a postinstall script (scripts/copy-maplibre-worker.mjs) that copies the MapLibre GL
worker files into public/maplibre/, which the map needs to render under Turbopack.
Copy .env.example to .env.local and fill in your own Supabase project's values (never commit this file or its
contents):
cp .env.example .env.local
NEXT_PUBLIC_SUPABASE_URLNEXT_PUBLIC_SUPABASE_ANON_KEYSUPABASE_SERVICE_ROLE_KEYRIVULET_ISSUER_PRIVATE_KEY — optional, needed to issue signed certificates (see below)RIVULET_MODERATOR_TOKEN — optional, needed to use /moderate (see below)GROQ_API_KEY — optional, needed for the photo authenticity check (see below)Apply the database migrations in supabase/migrations/ to that project (in order), then seed it:
npx tsx scripts/seed/fetch-waterbodies.ts
npx tsx --env-file=.env.local scripts/seed/seed.ts
npx tsx --env-file=.env.local scripts/seed/seed-observers.ts
npx tsx --env-file=.env.local scripts/recompute-trust.ts
npx tsx --env-file=.env.local scripts/seed/seed-exposure.ts Coimbra
The last two steps create the synthetic demo observers and compute their trust scores; without them the people
leaderboard is empty. seed-exposure.ts loads the OpenStreetMap places used by the One Health reading; run it once
per city.
To add a city whose OpenStreetMap name differs from its display name, pass the OSM name third:
seed-city.ts Tashkent 4 "Toshkent shahri" (canals are included, as urban aryks are where residents meet water).
The original seed (fetch-waterbodies.ts) only queried river|stream, so Coimbra, Toulouse, Oslo and Gent were
missing their real canals — Ghent in particular is known for them. scripts/seed/add-canals.ts <City> <admin_level> tops a city up with just its waterway=canal ways; it cannot duplicate an existing row, since the
original seed never fetched that kind. Where the named-area query is too expensive for the public Overpass mirrors
(it is, for Gent), pass a bounding box instead: add-canals.ts Gent bbox "50.98,3.58,51.14,3.88". Agricultural
drainage (waterway=drain/ditch) is deliberately left out — almost entirely unnamed and outside what a resident
would call an urban stream.
Sentinel-2 readings are pre-fetched with npx tsx --env-file=.env.local scripts/satellite/ingest.ts Coimbra 6, which
reads each band once per scene for the whole city (a few minutes per scene); the live site never calls a satellite
API. Most urban streams are narrower than a 10 m pixel, so most get an honest "no clear satellite view". To add a
city that is not in the bundled seed, run npx tsx --env-file=.env.local scripts/seed/seed-city.ts <City> <admin_level>.
Signed volunteer certificates also need RIVULET_ISSUER_PRIVATE_KEY, an Ed25519 private key as base64-encoded
PKCS#8 DER. Without it the certificate API answers issuer_unavailable rather than failing.
npm run dev
Open http://localhost:3000 to view the app.
| Path | What it is |
|---|---|
src/app/ | Next.js App Router pages and route handlers, including the read-only FHIR endpoint at src/app/fhir/ and the export at src/app/api/fhir/Observation/route.ts |
src/lib/science/ | Pure scientific/statistical functions (uncertainty, aggregation), each with tests in a colocated __tests__/ directory |
src/lib/fhir/ | FHIR resource serialisation for the OneAquaHealth Implementation Guide |
src/lib/offline/ | The report queue a poor connection falls back to |
src/components/ui/ | The design system (see /design for a live reference) |
validation/ | Builds the OAH IG from source and runs the HL7 validator against Rivulet's own output |
Scripts: npm run dev starts the server, npm run build does a type-checked production build, npm start
runs it, npm run lint runs ESLint, npm test runs the Vitest suite once (npm run test:watch keeps it running).
Stack: Next.js 16 (App Router, Turbopack) with TypeScript (strict), React 19, Tailwind CSS 4, MapLibre GL,
Supabase (Postgres, PostGIS, row-level security), Zod, pdf-lib, node:crypto (Ed25519), and Vitest with Testing
Library and jsdom. The @/* import alias resolves to src/.
Issues and pull requests are welcome. Every push and pull request runs CI: the change should type-check, pass
ESLint with zero warnings, pass the Vitest suite, and — if it touches FHIR mapping or the science engine that feeds
it — pass the HL7 validator against the OneAquaHealth guide (see .github/workflows/). Found a security
issue? Please read SECURITY.md rather than opening a public issue.
Code: Apache License 2.0 (see NOTICE). Stream geometry and nearby places: © OpenStreetMap contributors, Open Database Licence. Satellite cross-check: contains modified Copernicus Sentinel data 2026. Standards: HL7 FHIR R4 and the OneAquaHealth Implementation Guide by HL7 Europe. Rivulet is an independent prototype and is not endorsed by the OneAquaHealth consortium, the EU or IEEE.