System architecture overview
Owner-reported feline health monitoring. The system converts unstructured owner speech — “he’s been squinting since the weekend, and he threw up twice yesterday” — into a structured, longitudinal clinical record; tracks symptom episodes across days; alerts when emergent; runs a nightly server-side reasoning round; and produces a veterinarian-facing report.
Status. Deployed and in continuous daily use for one animal as a single-user research prototype, including the nightly reasoning job in production.
Stack. React Native / Expo (TypeScript) client; PostgreSQL (Supabase) with row-level
security; LLM access through a server-side gateway; nightly reasoning driven by pg_cron.
~40 tables, 65 schema migrations, ~150 pure-logic test harnesses with mutation testing.
rec_NNNNNN) is what
retrieval hands to the model, what the model cites back, and what cross-module references use — identity
stays stable across turns, across days, and across the client/server boundary.
The core entity holds both a single-day observation and a multi-day episode, distinguished by lifecycle state — a course is what a day becomes, not a different kind of object.
active → passive → closed, plus
dormant) use model-proposed horizons clamped by floors the model cannot
override: a minimum observation window, and a rule that a chronic course may go passive but
never auto-closes.
An earlier single extraction call both decomposed the utterance and decided where each piece belonged. Under load it fragmented observations, dropped bare answers, and guessed at times. The current design gives the model only the first job. Every extracted entry lands in an append-only observation ledger, and code decides materialization:
Time is two independent axes — “is this happening today” versus “since when” — which is what allows the router to be deterministic. The burden of proof sits on the past side, so “he’s still sneezing, it started Tuesday” routes as an ordinary today-record carrying a three-day-old onset.
| loop | period | job |
|---|---|---|
| turn | seconds | extract, route to ledger, retrieve, respond |
| round server-side, per pet-local day | nightly | merge yesterday’s rows into courses; emit a diagnosis frame (current + append-only log); adjudicate leftover ambiguous entries; run lifecycle transitions; seed tomorrow’s follow-up agenda |
| longitudinal | weeks–months | steady-state profile re-measurement, version-to-version drift, assessment staleness (§5) |
The round is idempotent and catch-up-capable, guarded by a lease against concurrent execution; client-side execution exists only as a degraded fallback.
Each animal has a versioned normal-activity profile — its own steady state (stool frequency and form, intake, activity) — archived exactly once per re-measurement session, so two versions can be compared.
It is deliberately not a per-day comparator. Daily judgment runs on discrete rules with no “normal range” and no weighted deviation score; an earlier design that scored each day against a fitted normal was abandoned. Two scoped exceptions survive:
The profile’s real job is longitudinal: version-to-version drift — continuous fields at ≥30%, food ≥25%; ordinal fields by rank distance.
Drift feeds the persisted health assessment, which is keyed by a hash over its own inputs — and those inputs are restricted to the persistent portrait: banded age rather than exact, chronic background rather than active courses, long-term medication, the profile and its drift. Nothing transient enters, so the hash does not churn daily. There is no “refresh the assessment” code path at all:
re-measuring archives a version → drift becomes computable → the hash changes → the assessment reads as stale and offers regeneration, user-triggered rather than silently spending a model call.
Staleness is a derived property, not an event someone has to remember to fire.
dormant: demoted
without being deleted. The countervailing “interrogates the user” risk is handled by a hard
deterministic cap on how many questions may reach the response prompt — the guard placed where
it is mechanically checkable, not as a soft instruction.