Javis OS
Javis OS is an Agentic AI that assists entrepreneurs in business tasks, helping them grow their work.
AI translation
Practice Q&A for The Riverside Practice. Answers for all staff, based only on the organisation’s own documents.
BusinessJavaScript · Rich Text FormatGitHub @F12-SyntexOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
Loading the full README. The saved excerpt is shown below.
A document-grounded reception Q&A assistant for The Riverside Practice. Staff ask
how to do something in EMIS Web (or what to do at the front desk) and get a
step-by-step answer drawn strictly from the practice's own documents, with
clickable sources they can open in-browser.Read the full README ↗A document-grounded reception Q&A assistant for The Riverside Practice. Staff ask how to do something in EMIS Web (or what to do at the front desk) and get a step-by-step answer drawn strictly from the practice's own documents, with clickable sources they can open in-browser.
POST /api/agent, routed through OpenRouter. It is one
model call, not a research loop: the model reads the message, chooses the
template that fits it and fills that template's variables — and the answer is
then the template, rendered in code from the values it returned. Understanding
a message is what a model is good at, so it does that; the shape of what comes
out is what a model is unreliable at, so code does that. The API key and
server-side knowledge never reach the browser./api/ask endpoint before it. ANSWER-PIPELINE-REDESIGN.md records how they
used to work and why they were replaced; ARCHITECTURE.md §8 describes the
turn that runs now.lib/contacts.fuzzy.mjs,
lib/lookup/), and what they hold is shown in the contacts card as structured
data — never retyped through the model's prose, where an unverified number is
stripped out.question_log
(lib/questions/log.js) and read back at /stats. There is no answer cache:
with the answer assembled in code from a page and a template, there was
nothing left worth caching, and its question normaliser now lives in
lib/routing/ as the first rung of the router./questions as it is logged (lib/questions/gaps.mjs).
Staff can add a question there themselves, which is what the list was asked
for; the assistant's own arrive without anybody having to notice them. Same
wording twice is one row with a count, so the gap being hit five times a week
is visible as the thing to write down next. The switch that turns the question
log off at a desk turns this off with it.lib/safety/):
emergency > twoWeekWait > sameDay > routine > admin, checked top down. The card that
renders is the most urgent item the band above it has not already answered
— not the first one, and not the longest one.lib/safety/identifiers.mjs) — and a name or an address it
finds is replaced with [name removed] / [address removed] before the
request is built, before the transcript is written and before anything is
saved. /api/agent runs the same check on arrival, so the
guard belongs to the endpoint and not just to the page. The reader is told
what went, as a count and never as a quote. It redacts rather than blocking
the send: making somebody retype a sentence in a hurry does not get the
name out of the world. Dr, Nurse and Matron introduce a colleague, so
those names stay, as do names in the practice directory — "which days is Dr
Ahmed in" and "the number for Alison Wade" are the job. It does not run on
the Coding mode, in the browser or at the endpoint — see the two
paragraphs below.lib/templates/referrals.mjs carries a table of recorded
referrals with their e-RS pairings, addresses and forms — and that table is a
copy of the practice's list, which had already fallen behind it. "Dietitian"
was on the practice's emailed-referrals page and had never been typed into the
array, so "how do I do a dietitian referral" answered "not recorded in the
practice's notes" about a referral the practice had written down. Worse than a
gap, because a gap is visible. Now the model names what is being referred to
(referralName — extraction, which it is reliable at) and code reads the
Notebook: the page has to actually be a list of emailed referrals, and the
name has to actually appear on one of its list lines. A prose mention on an
unrelated page is not a listing and does not count. The card quotes the line it
found and cites the page. A referral the practice adds to its own page is
answerable the moment they save it. A recorded e-RS pairing still wins over
list membership, being the more specific fact.lib/referrals/nel-tree.data.json, generated from PCIT's published article by
npm run data:nel-tree. The form name is shown exactly as the tree lists it, and
offered to copy, because it is a string somebody retypes into Ctrl+F and a name
spelled nearly right finds nothing. Several matches are shown as several rather
than resolved to one — "dermatology" genuinely is three forms. A form ticked for
another borough only is named as another borough's, which beats silence: the
alternative is somebody scrolling a menu for a button that was never there. The
A form the tree has nothing for gets the honest "no form by that name" card,
naming the document searched and the day it was captured.
Availability is read by tick column, not by counting ticks — the flattened
text of the source PDF gives ticks in order but not which of the five columns
each sat in, so "Wider determinants of health questionnaire NEL" is Newham and
Waltham Forest and nothing in the text says so.lib/referrals/nel-contracts.data.json, the table lifted out of PCIT's PCCIF
mobilisation Sitrep; cards name the specifications, which is what staff would
recognise, not the bulletin). So "which template for the ADHD shared pathway"
gets the
template name verbatim, to copy. The build status beside it is the part that
rots — it came off a daily bulletin which says of itself that the position
changes through the week — so every card leads with "as at 28 July 2026", a
status that is not "For Release" is shown as a warning to check rather than a
fact to repeat, and PCIT's own number is on the card so checking is one call. A
status presented as current weeks after it was written is worse than none: it is
fast, it looks authoritative, and it is wrong.lib/referrals/scope.mjs).lib/commands.mjs): /accurx (where the patient goes and the reason
line, from one paste), /coding (a filing title), /practice (search the
documents, no model at all), /form (a NEL referral form) and /template
(the EMIS template that records a NEL contract). There used to be two more — /triage, which said
where a patient went, and /appt, which wrote the reason line and the booking
notes — and /accurx is both of them on one card, so they were removed rather
than kept as half-answers beside it. A described symptom typed without any
command still reaches the same triage card: that path never went through
/triage.app/_components/ModeSwitch.jsx): pressing it opens the list of modes
over the box — Q&A, AccurX, Coding, Referral form, Contract template, Practice
documents — each named with the line saying
what it does, and the armed one ticked. Nothing was added to the page to make
room for it: the glass had always sat there costing 48px of the field and
doing nothing when it was pressed, and the disc is what says a thing can be
pressed. It is the same size as the ask button opposite, so the field is a box
with a control at each end. The disc fills blue with a white glass whenever
anything other than Q&A is armed, so a field about to answer out of the
referral list cannot look like a field about to answer a question. Arrow keys walk the list, Enter takes a mode, Escape closes it.
Two earlier controls are gone: a row of pills under the field, which was four
buttons hanging off the dock for a choice left alone almost every time and had
to appear and disappear with the cursor to stay out of the way, and before that
a segmented track with a sliding thumb.
/accurx is offered here, first after Q&A: it is the one the practice
reaches for most — a pasted AccurX request answered with the route, the urgency
and the reason line on one card — and hiding the command somebody new to the
desk would most benefit from finding was the wrong trade for a shorter list.
Coding is offered too, and was /document. It was hidden on the argument
that the people who file letters do it by habit and a habit needs no
advertising, which quietly meant an everyday answer was reachable only by
somebody who had already been told it existed. It is a mode now, named for the
helper that does the same job at /coding, and the old spelling still
resolves: typing /document reaches it, is offered under the Coding row while
it is being typed, and is rewritten to /coding in the field, so the habit
teaches the new name instead of being broken by it. It is also the one mode nothing
checks for patient data — neither the redaction nor the screen — because
what is pasted into it is a letter about a patient by definition. See the
guards above. The strip above
the field carries only the send bar and the "Copied" line, and nothing was
added to the header or the footer. Typing "/" still works and still wins over
the disc, being the more specific thing the reader just did. The disc wears
the armed mode's own icon — a sheet of lines for Form, two sheets for
Template, a speech bubble for AccurX, a folder for Coding, a book for Practice,
the magnifying glass for Q&A — and the same glyph is drawn beside each name in the list, so which
mode is armed is legible at a glance rather than inferred from the disc turning
blue. , across a reload as well as
across a message (). It used to last exactly one message; three
referral forms are three questions, and re-arming the picker between each one
was the friction the picker existed to remove. The failure modes are not
symmetric — a wrong says honestly that no list has such an entry, while
a wrong renders a confident triage card with a destination and an
urgency for a question that was never about a patient — so what pays for a mode
that stays put is that it cannot be missed and takes one key to drop: the icon
on the disc, that mode's own placeholder wording, and .
A question asked with a mode carries in the transcript,
because typed into the box stays in the reader's own words for ever
and choosing the mode with a button would otherwise leave nothing on screen
explaining which list the answer came from./form and /template answer from a list, with no model anywhere in the
path. The reader has already said which of the two Primary Care IT lists they
want, and the rest of the line is the query, so the turn is a ranked string
match against a file in this repository: no model call, no tokens, no network.
/form suspected skin cancer names the referral form; /template NEL Housebound Winter takes a template's name and answers with the contract specification
it sits under, which is the direction staff need — the template name is what is
written on the task, and the specification is what the contract is called in
every other conversation about it.lib/referrals/ask.mjs drops the asking words ("which", "do I use",
"please") and the words naming the document itself ("form", "template",
"referral") before either list is searched or judged. It never drops anything
clinical, and it can only lower the bar, so a query that found its row before
still finds it.lib/agent/contract-intent.mjs). "B12 injection" comes
back as OneTemplate NonPrescriber (Treatment Room page), with what that
template records underneath it as the evidence.
The model picks a name; it never writes one: the pick is thrown away unless
the template is on the document character for character, and the contract, if
it named one, is one that template actually records — so a half-remembered
"OneTemplate Nurse (B12 page)" yields no template rather than a wrong one, and
a right template under a wrong contract loses the contract, not the template.
"TBC" and "N/A" are how the document says there is no template, so neither is
ever offered as one to open. The card leads with the fact that it was worked
out rather than looked up — open it and check the entry is there — and lists
the pages that were read.
Two more things make it hold up. When the model will not choose, the names
the search itself used go back through the document's own matcher, so "Wound
care service" off the ICB's page reaches "Simple Wound Care Service" and that
row's own template, with no model in the path. And when nothing on the document
fits at all, the card names Primary Care IT's own pages about the task.lib/referrals/one-templates.data.json, built by npm run data:one-templates
from the documents in rag/sources/). The contract list answers for 42
commissioned specifications and nothing else, which is a small part of a day: a
firearms request, an ambulance, a loaned crutch, a sick note, a smear, hay fever
— none is a contract, all are pages Primary Care IT publish, and every one of
them used to come back "no contract by that name". Contract template now
searches the contract list first — where a contract answers, the reader gets
the build status and the capture date only that document can give — and the page
specifications second, which answer with the page, the template carrying it,
the launcher that opens it (the F12 button) and, where PCIT wrote one, the line
saying what the page records. The heading comes with it: PCIT nest their
lists, so "Injections" is not a page — it is an entry on the Treatment Room
page of OneTemplate NonPrescriber — and the card gives the whole route (press
this launcher, open that template, then that page) rather than a template name
to go scrolling through. The contract card gets the same treatment: the page in
its brackets ("OneTemplate NonPrescriber (Treatment Room page)") is resolved
against the page lists and the card shows what is on that page, so the reader
can see the thing they came to record before they open anything. Naming a template answers with its whole page
list. The page card carries no status and no contract area, because a page
list says what exists, not whether the work is paid for or how far a build has
got — claiming either from the wrong document is exactly the blur this splits
apart. 12 templates, 320 pages, and the same date-stamped honesty as the
contract card.lib/templates/lookup-command.mjs
is the whole scope of both commands and states it once:
Referral form reads Primary Care IT's NEL Referral Tree introduction &
document list (EMIS Web), and Contract template reads Primary Care IT's
NEL Local Contract Specifications. Neither reads the practice's Notebook,
neither falls through to the other's document, and neither calls a model. A
miss is reported as a miss, on a card that names the document searched and the
day it was captured — the one exception being the intent lookup above, which
reads the web to pick a row of the SAME document rather than to answer from
somewhere else. The modes are named for their documents rather than for
the thing being looked for — "Form" and "Template" told a reader nothing about
where an answer would come from, and both were quietly reading more than they
said: Contract template answered out of the Notebook, Referral form out of the
practice's emailed-referrals page. A referral question asked in ordinary
words reads the practice's own material and only the practice's own
material — that is the router's path, and it is where the Notebook belongs.contractTemplate template of its own that
searched the specifications. Both are gone — the template is out of the
selection schema entirely, so the model cannot ask for a document it is not
allowed to read, and the prompt says which mode does hold it. The two lists
answer North East London's questions (what forms exist, what the ICB
commissions), which are not this practice's questions, and blending the two
behind an ordinary question is what made an answer impossible to place./accurx answers both halves of an AccurX request on one card
(lib/templates/accurx.mjs): where the patient goes, and the reason line to
copy into what gets booked. Reception reads the request once and needs both,
so asking for them separately meant pasting the same message twice — and the
second paste is the one that does not happen when the phone rings. Neither
half is new: the routing is the triage card, run through the practice's own
order top to bottom over the destinations in lib/triage/destinations.mjs,
and the wording is the reason line and booking notes under the rules in
lib/templates/writing.mjs. What the command decides is order. Ordinarily the destination and the reason
line sit together at the top, because they are the two things that leave the
card. On a card that ends in an emergency — a red flag, cauda equina, an eye
going straight to Moorfields — the wording drops its Copy, moves below
everything the routing card has to say, and is renamed the handover note:
nobody is booking an appointment off "interrupt the duty doctor now". Which of
the two it is comes from meansNow in lib/templates/route-card.mjs, next
to the destinations it judges, so the two cannot drift apart./accurx also says who to book it with, when the message says somebody has
already dealt with it (lib/templates/accurx.mjs, rules in
lib/templates/writing.mjs). Patients say this themselves and say it in
passing — "I saw Dr Okafor about this in July", "the nurse gave me cream last
month", "I was told to come back if it did not settle" — and it was going
nowhere: the reason line drops it as narrative and the booking notes are about
slots. The same problem twice is one consultation continued, and the
appointment that helps is the one with the clinician who saw it last. So the
card carries a third panel, laid out exactly like the reason line above it:
who dealt with it, when, what came of it, and — where there is an appointment
to book and it was somebody here — book it back with them. It never moves
the destination, which is why the reading is asked for it after naming one:
continuity is who, never where. A follow-up about the same knee still goes
to the physio; it goes to the physio who saw the knee. The quote is checked
with spanWithin like every other quote on the card, a panel whose words are
not in the message keeps the panel and loses the quote, contact somewhere else
(a hospital, a walk-in centre, 111) is named as history rather than as a
booking, and the note says outright that this is what the message claims —
nothing here has read the record. Nothing found renders nothing at all: an
empty finding printed as "nothing says this has been dealt with before" is a
negative nobody checked, which is the mistake the booking notes already made
once./accurx is read as well as matched (lib/templates/accurx-route.mjs). A
patient wrote in after a recent miscarriage: severe daily headaches, swelling
and pain in both legs, shoulder pain, dizziness, bloods awaited. It came
back as a physiotherapy appointment — and nothing was broken. "Leg pain" and
"shoulder pain" made it musculoskeletal, "severe" made it disabling, and a
disabling musculoskeletal problem is an FCP job. Every rule fired correctly
and the answer was wrong, because the words were spread across four complaints
and no regex can see that the swelling is in two legs or that "recent
miscarriage" changes what all of it means together. That is not a tuning
problem; another feature word finds the next gap. So the message is now
read — one call, on its own model role (Settings → AccurX routing), which
names where it goes and writes the reason line.docs/routing.md as data (lib/triage/destinations.mjs): every destination
least-senior-first with what it covers and what it refuses, the practice's
hard gates — no phlebotomy under 16, HPV to 24 and under, six weeks for travel
jabs, health-check bloods before 1 pm — and the nurse-clinic rules.
The Notebook is not in front of it. It was, and it was the wrong source for
this one question: the Notebook is how the practice does things, the guide is
where a task goes, and all the reading ever saw of the Notebook was a list of
page titles to match a heading against — which is the failure mode the reading
exists to replace. Removing it also took a database round-trip and about 3,000
prompt tokens off the one call the receptionist waits for.lib/triage/destinations.mjs describes them now,
so the whole ladder goes into one prompt as data and one reader sees what
nine saw between them, at a tenth of the calls.pt has sore throat: a GP genuinely
can see a sore throat, so naming one is true and wrong — it takes the
patient off the pharmacy that would have dealt with them. It is asked for
the least senior service that can safely deal with the whole message,
and every entry on the ladder carries what it refuses as well as what it
covers, because refusing is the half that decides it. The GP entry says in
its own words that a GP appointment is not the safe default here./accurx and /coding commands name it outright. The picker
returns the template that fits, and the card is rendered in code from it:
app/page.js — the chat UI (React). Persists chat + custom guides to
localStorage.app/api/agent/route.js — the assistant's only answer path: the safety
scan, the directory check, the one selection call, the template render, the
prose fallback and the NDJSON event stream the chat reads.lib/templates/ — the templates: the selection schema and prompt
(route.mjs), each card's renderer, the output tags a Notebook folder can
carry.lib/routing/ — the confidence-scored router in front of the picker:
the question normaliser (exact-match rung) and the trigger-phrase index.lib/agent/ — practice-answer.mjs (the /practice answer, quote-checked
against the passages it cites), contract-intent.mjs, web-search.mjs
(OpenRouter's web-search server tool, used by Instant Lookup).lib/guides/ — the built-in practice guides, categories and helpers.lib/ai/ — the streaming askAgent client (agent-client.js), the
OpenRouter wrapper, quote matching, and the document-coding and medication
helpers.rag/ — the document knowledge base: ingest pipeline, parsers (including
vision image reading and PDF page rendering), and the runtime retrieval store.
See rag/README.md.app/notebook/ + app/api/notebook/ + lib/notebook.js — the
in-app Notebook: practice notes/instructions (with sub-notes) stored in
Postgres and edited at /notebook. Every non-empty page is read fresh and
supplied in full to every Q&A request (no rebuild or retrieval cutoff).lib/ai/context.mjs + rag/context/ — optional extra supplementary
context (direct URLs, or committed files in rag/context/), reconciled into
the same store. See rag/context/README.md.lib/contacts.js + lib/contacts.data.json — the deterministic
telephone directory (exact numbers shown verbatim, never authored by the AI).lib/contacts-store.js + /api/contacts + — the
practice's own contacts, as one list that can be added to. Opened with
nothing typed, lists every one by category; a search runs over all
of them first, with the CQC register below in a box of its own. Contacts are
added, changed and removed from the same page (and a register row can be
saved as a contact). They are the rows of kind
that the bundled sheet is synced into; a save sets
, which the sync leaves alone, and an added one carries
. The chat's directory card and the identifier
redaction's list of names read the same list, so a number added at the desk is
answerable at once. Without a database the bundled sheet stands in,
read-only.The assistant keeps each source type predictable:
GIN) search, and served by
/practice, which shows the passages themselves with no
model in the path (lib/templates/practice.mjs). Unedited is not the same as
unformatted: the words are exactly the document's, but they are laid out the
way every other piece of text in the app is — a heading naming the document,
then its own paragraphs and lists. They used to arrive as blockquotes with the
whitespace collapsed out of them, which read as machine output and was harder
to follow than the policy it came from. The claim that no model touched the
answer is one grey line under the title rather than a banner across the card.rag:ingest updates portable parsed artefacts; a persisted bundle fingerprint
automatically reconciles only changed documents on the next runtime check./knowledge and /api/knowledge/** are deliberately available only from a
loopback host in development; they return 404 in deployed production. The
public app contains no link to this backend./api/knowledge/sync idempotently reconciles the processed file bundle,
Notebook and contacts. A legacy document/contact fallback keeps a fresh
deployment available while its first canonical index builds in the background.Set these in .env.local (see .env.local.example):
| Variable | Purpose |
|---|---|
OPENROUTER_API_KEY | OpenRouter API key (server-side only). |
OPENROUTER_AI_MODEL | Gone. The chat/vision model is a practice setting now: change it at /settings, where it is picked from the live OpenRouter catalogue and stored in Postgres (app_settings). Defaults to google/gemini-3.5-flash-lite. Should be vision-capable — the ingester reads images with it whenever the fast model cannot. A routing variant can be pinned to it there too — :nitro (fastest provider), :floor (cheapest), :free, :online, or anything else typed, e.g. openai/gpt-oss-120b:nitro — and a full id can be typed straight into the search box when the catalogue does not list it. |
OPENROUTER_ANALYSIS_MODEL | Optional default for the fast role — the reading: the template picker, claim extraction, the medicine-name extractor, the ingester's image transcription. Wants a cheap model that calls tools reliably. |
OPENROUTER_WEB_MODEL | Optional default for the web search role — searching the internet and reading a page for a phone number. A search-grounded model such as perplexity/sonar belongs here. Falls back to OPENROUTER_MEDICATION_MODEL, then OPENROUTER_ANALYSIS_MODEL. |
DATABASE_URL | Neon Postgres. Powers the staff rota and the Notebook. |
SUPPLEMENTARY_CONTEXT_URLS | Optional. Direct text/markdown/JSON URLs to inject as extra supplementary context (the Notebook is the main channel and needs no config). |
One turn is not one job, and it splits cleanly in two. Reading — searching the practice's material, skimming what comes back, choosing the next file to open, pulling the names out of a pasted list, transcribing a screenshot — is most of the model calls in a turn and none of its output. Deciding is the answer itself and every judgement in it: one call, and the only thing anybody reads.
So /settings picks the model the practice runs on — the reasoning role,
which every other role falls back to — and two optional overrides sit beside
it: fast, which runs the whole assistant turn (the template picker and the
prose fallback) as well as claim extraction, the medicine-name extractor and
the ingester's image transcription, and web search,
a search-grounded model such as perplexity/sonar for the internet and for
reading a page to lift a number off it. Each is stored in app_settings, so
changing one needs no redeploy. There is no separate vision role: the ingester
reads images with the fast model and falls back to the reasoning model if that
one cannot see.
Every role is optional. Unset, it falls back to the environment variable that used to carry it and then to the reasoning model, so an install that has only ever chosen one model is completely unaffected.
The model does not write the answer. The assistant turn is one structured read on the fast role — which template fits, and its variables — and the card is rendered in code from what the practice wrote. So the fast role can be a small, cheap model without the answer getting any worse: the procedure on the card is the Notebook page, not the model's paraphrase of it. Only the prose fallback, taken when no template fits, is the model's own writing, and the card says so.
Each row on /settings shows its model's advertised rate — input and output per
1M tokens, live from the OpenRouter catalogue — and, underneath, the tokens a
question really used on that role. A rate is not a cost: what a question costs is
the rate multiplied by how many tokens this practice's questions use, and that
depends on its Notebook, its documents and how staff word things. Two installs on
one model differ by more than two models on one install.
So it is measured, not assumed. Every phase of every turn writes a row to
ai_usage — role, model, tokens in, tokens out, no question text — and the page
averages the last 30 days per question (not per call, so a repaired answer
counts as the one question it was).
Measurements belong to the model that produced them. Tokens measured on one model say nothing about the next: a terser model writes fewer, a hungrier one reads more, and pricing yesterday's counts at today's rate reports a number that was never true of either. So a change of model shows nothing until it has been used — and changing back brings the old model's record back untouched, because nothing is ever reset or deleted. A cost per question, by model table lists every model that has answered anything, marking the ones in use.
A role nobody has run on its current model, or one whose model publishes no price, is excluded from the total and named as excluded. Unpriced models sort last in the table rather than cheapest — unknown is not $0.
npm install
npm run dev # http://localhost:3000
npm run build && npm run start
npm run rag:status # what's indexed / pending
npm run rag:ingest # process new or changed documents in rag/sources/
See rag/README.md for the data standard, parsers, citations and storage notes.
checked: false in lib/commands.mjs). The redaction above is off
there, in the browser and at the endpoint, so a
letter pasted into that mode reaches the model exactly as it was pasted.
Coding is handed a document about a patient; that is its input, not an
accident of one. A discharge summary carries a name, a date of birth and a
hospital number because that is what a discharge summary is, and the
redactor ate the words the answer is built from — the site and the department are proper nouns, and a
letter filed under [name removed] is filed nowhere. The same reasoning
already exempted an attached document; a letter pasted into the box is
the same letter, and was treated differently only because of how it
arrived. One flag answers for both sides of the wire so that neither
can be switched off alone: half a guard edits the reader's letter without
being any use against what the letter was always going to carry. What still holds is
the answer itself — the filing title never carries a name, an NHS number or
a date of birth, because the filing rules say so. Like the standalone helpers at /coding, /signpost and
/reason, which have never redacted what is pasted into them, it is the
paste that carries the duty to take identifiers out first — recorded as
exactly that in the DPIA, under "Identifiers left in text pasted into the
reception helpers"./accurx is the one exception to one-call. It may
issue a small structured call per decomposed request, in parallel, returning
an enum and a label — no prose; /accurx additionally asks one per
destination, also in parallel, returning a yes or a no. Code holds the
veto on both: a classification may only raise acuity above what the
scanners found, never lower it, a destination may only move a card to a more
senior one, and a pass that fails or times out leaves the deterministic
answer standing. The regex is the guarantee; the model is a recall booster.question_log rows at /stats and measure recall before trusting any of
it. The panel is what makes a miss visible rather than silent, which is why
it was built first.riva-mode-v1/form/accurx/form knee/lookup/lookupknowledge_entriescontactdata.backendOverridedata.source = 'manual'lib/lookup/ + /lookup — the CQC register half of the page:
every service registered in England, ~57k rows in
cqc.data.json.gz. Far too large for a phone, so cqc.js searches it on the
server behind /api/cqc. Matches on name, town, postcode (either half),
service type, phone number, and acronyms taken from the initials of each
name — HUH reaches Homerton University Hospital, MEH reaches Moorfields.
Rebuild from a newer CQC CSV export with npm run data:cqc -- <path-to-csv>;
numbers are verbatim from the export (the script only restores the leading
zero the spreadsheet drops). It is never mixed into the practice's own list:
nobody here wrote it.lib/lookup/contact-extract.mjs + lib/lookup/web-contact.mjs — how a
number is found for something neither the directory nor the register holds.
The web search picks the pages; the pages are then fetched and read, and
their tel:/mailto: links and visible numbers are pulled out verbatim
(/api/lookup-web). Runs that are not numbers
to ring — a charity registration, an NHS number, a date — are rejected by
shape and by the words around them. No digit anywhere on this path is written
by a model, so the guarantee is the same as for the committed directory.lib/referrals/ + scripts/ingest-snomed-ers.mjs — the referral-routing
fallback. ereferrals.csv is the closed list of Specialty + Clinic Type
pairings e-RS accepts (406 of them); the SNOMED CT description snapshot gives
the clinical wording to reach them. npm run data:ers loads both into Postgres
(ers_directory, snomed_terms). A note resolves to a SNOMED concept, then the
concept and the note's own words are scored against the pairings, weighted by
how rare each word is across the list. The Notebook comes first — this runs
only when the Notebook records no speciality and clinic type, and everything it
returns is labelled a suggestion to check against the doctor's task. There is
no published SNOMED-to-e-RS mapping (no edition of the UK release carries an
e-RS refset), so the join is made on text and is never presented as authoritative.
route-determination.mjs settles which of the two the e-RS card is showing
after the answer is written: a pairing the Notebook records is shown as it
stands, while one determined here fills the card in and carries its provenance
onto it — the SNOMED concept, the e-RS referral-types list, how close the match
was and what else was close. A determined pairing the writer labelled as the
practice's own is relabelled, so it can never reach the reader unmarked.
scope.mjs decides whether any of this applies: it tells a referral being
made from one arriving, one being chased, or the word simply appearing in a
policy question, and it reads the written answer for whether the referral goes
on e-RS at all. Every stage is gated on it — the lookup, the research tool, the
card the writer produced and the card filled in afterwards — so four e-RS
fields never appear above an answer with no e-RS form behind it.lib/triage/destinations.mjs — where the practice sends things, written
down once: the duty doctor, the FCP, Pharmacy First, the nurse clinic and the
diabetes nurse, each with what it takes and what it never takes, plus the
matching for the two nurse routes and the order the checks run in. The triage
cards (lib/templates/triage.mjs, lib/templates/nurse.mjs) and the
signposting page (app/api/signpost/route.js) both read it, so the two cannot
answer the same question differently. It replaced a live read of the "Triaging
notebook" Notebook section, which changed under the code, could not be tested,
and never mentioned the nurse or the diabetes nurse at all — so a smear, a
dressing or a diabetic review was routed by a triage that had never heard of
the nurse clinic.lib/safety/ — the deterministic floor, and the only thing in the app
that runs on every single turn. requests.mjs (the multi-intent gate and the
decomposed spans), spans.mjs (locality — message-wide checks against
span-local assertions), ng12.mjs, redflags.mjs, confidentiality.mjs,
acuity.mjs (the rank table), scan.mjs (which request the card is about,
and what has to be said above it). No model is consulted anywhere in
the folder; lib/templates/safety.mjs renders what it finds.lib/answer-cache/ — answers already given, so the same question is not
researched twice. match.mjs holds the free half (the canonical form of a
question, hashed to a key, and the rules for what may be cached at all);
store.js holds the Postgres half (exact key, then nearest question by
embedding, both filtered on the Notebook fingerprint and the model that wrote
the answer). Read before the agent runs and written after it, and never on the
critical path: if the cache is unavailable the question is simply answered the
slow way.lib/settings.js + lib/model-id.mjs + /settings — runtime
settings in Postgres (app_settings), not the environment. Today that is the
AI model: /settings fuzzy-searches the live OpenRouter catalogue
(/api/settings/models) and stores the chosen slug, so the model can be
changed without a redeploy. model-id.mjs is the id itself — validation plus
the split/join of the routing variant (openai/gpt-oss-120b:nitro), with no
database import, so the browser validates with exactly the rule the save uses.lib/knowledge.js + /knowledge — the canonical Postgres knowledge
layer and localhost-only backend screen. The source types share storage and
conflict review, while the live assistant keeps their context paths separate.lib/routes.js + /index — one registry of every route the app
serves, and the deep index built from it. The landing page at / stays two
links on purpose; /index is the whole list — pages and API endpoints, with
what each is for and which are kept off the front. (The page lives in
app/site-index/ and is rewritten onto /index in next.config.mjs:
index is a reserved route name in the App Router.) Add a route here and it
appears on the index and reads properly in the audit log.lib/questions/ + /questions — the question log (log.js), the
per-machine logging switch (opt-out.mjs), and the open questions list:
gaps.mjs decides which turns the assistant failed to answer and when two
askings are one question, open.js is the store behind
/api/questions/open, and the page is where staff ask what the practice has
not written down and write the answers back. An answer there is readable the
same afternoon; the Notebook page is still what makes it citable, which is
why every row links to it.lib/audit/ + /stats — the activity audit log: pages opened,
questions asked, actions taken and requests that failed, grouped by machine
rather than by IP address. Every machine at the practice shares one public
address and a laptop changes address between the car park and the consulting
room, so the browser mints a random id per machine instead and keeps it
(machine.js); no IP address is stored. client.js wraps window.fetch
once, so every route is covered without a line of logging in any of them, and
batches are flushed with sendBeacon so the last thing done before a machine
is switched off still reaches the log. describe.js holds the privacy rule
that matters: for the tools patient text is pasted into (/api/signpost,
/api/reason, /api/docfile and the rest of CONTENT_NEVER_RECORDED) the
log records that the tool was used and how much text was pasted — never the
text. Stored in audit_machines / audit_events; /stats is deliberately
not linked from the tools page or the menu.public/assets/ — logos, EMIS screenshots, and served document copies.extension/ — the practice's Chrome extension, which shares this
repository but nothing else: its own build, its own versions, and its own
workflow (.github/workflows/build.yml, which only runs when extension/
changed). It is never published to the Chrome Web Store — GitHub Actions
signs a .crx, one script copies it onto the practice shared drive, and
Chrome updates itself from there. See extension/README.md.