Contribute
Contributing
Ovillo is open source and welcomes help. This page is for developers: the ground rules, how the repository is laid out, how to run it safely, which tests to run, and how to send a change.
The project in one paragraph
Ovillo is a local-first, encrypted notebook for a family's care history. It is vanilla ES modules with no build step: index.html, app.mjs and styles.css run in the browser, and server.mjs runs on loopback to serve static files, store the encrypted vault and act as the only route out to an assistant provider. A thin Electron shell in desktop/ packages it as a Mac app. Source: github.com/BraedenBDev/ovillo. For the design, read Architecture and Privacy and security first.
Principles
A change that breaks one of these will not be merged, however good the feature.
- Synthetic data only. Never put real patient records in the repository, tests, fixtures, screenshots or commits. Tests and scripts run against a temporary data folder. Never point anything at your own
~/.health-pictureor at an instance you are really using. - Sealed when closed. While the vault is locked, nothing listens and nothing syncs. No relays, no webhooks, no external MCP servers.
- Personal details never leave unscrubbed. Every assistant request goes through
agentStepinapp.mjs, which runsscrubMessagesfromsrc/redact.mjsafter a local name model (src/ner.mjs) has taught it people named without a title. Replies and tool inputs are restored locally.tests/e2e.mjsasserts that no provider request names the test patient. Keep that true. Scrubbing is not complete, and the docs say so; do not write code or copy that implies otherwise. - Egress is fixed. The page's content security policy allows only
'self'(andblob:). Onlyserver.mjscontacts a provider: Sign in with ChatGPT, or the family's own OpenAI-compatible key over https to public hosts. Nothing else is added. The desktop app also checks GitHub for updates from its main process. - Code decides what waits. With an assistant connected, the assistant decides how to file items, but the code decides which items it may touch.
holdReasoninsrc/record.mjs, not the model, decides what is held. Held items, document owners and anything about someone else's values stay with rules the code enforces, and every assistant decision is marked and undoable. Delete, export, settings and moving anything between people stay human-only.
Repository layout
| Path | What is in it |
|---|---|
index.html, app.mjs, styles.css, viewer.html, viewer.css | The page, its main script and styles, and the in-app PDF viewer. |
server.mjs | Loopback server: static files from an allowlist, the ciphertext vault, and the route to an assistant provider. Reads PORT (default 4173) and HP_DATA_DIR. |
src/ | Shared modules: reading and extraction, the record and its holds, labs and units, scrubbing, the assistant's prompts and tools, ui-*.mjs screens, and i18n.mjs (all visible text). |
src/providers/ | Provider adapters (openai-responses.mjs, chat-completions.mjs). Only the server uses them; they are never served to the page. |
desktop/ | Electron shell, build config, staging script and smoke test. Electron lives only here. See Building the app. |
tests/ | Unit tests (*.test.mjs), the browser end-to-end suite (e2e.mjs and e2e-checks/), the mutation gate, the scrubbing benchmark, layout screenshots and a hand-run assistant eval. |
fixtures/, samples/ | Synthetic records, documents and photos for tests and the built-in sample. |
scripts/ | fetch-assets.mjs (builds vendor/) and make-fixtures.mjs. |
vendor/ | Gitignored. PDF reader, OCR, embeddings model, image decoders. Built by npm run assets. |
docs/ | Design documents: architecture, threat model, contracts. |
Getting set up
You need Node.js 20 or newer (the engines field in package.json).
- Clone the repository and open a terminal in it.
- Install the development dependencies:
npm install. - Build
vendor/withnpm run assets. It copies the readers fromnode_modulesand downloads the OCR language data and the embeddings model once, checking each against a pinned SHA-256. - Start the server against a throwaway data folder, on a port other than your own copy's.
- Open the address it prints in a browser and create a new vault with a passphrase you do not mind forgetting.
npm install
npm run assets
HP_DATA_DIR="$(mktemp -d)" PORT=4180 node server.mjs
Then open http://127.0.0.1:4180. The vault in that folder is yours to delete when you finish. To try the app without any documents of your own, use the synthetic files in fixtures/ and samples/.
Important. Always set HP_DATA_DIR to a temporary folder and a spare PORT when developing. Without it the server uses ~/.health-picture, which is where a real family's vault lives. Never load your own or anyone's real records into a development copy.
Tests
| Command | When to run it |
|---|---|
npm test | Always. The unit tests, about 150 of them, and fast. |
perl -e 'alarm 560; exec @ARGV' node tests/e2e.mjs | Before a pull request that touches behaviour you can see. A full browser run of about eight minutes on your local Chrome. It can hang, so the alarm ends it after 560 seconds. Set CHROME_PATH if Chrome is not found. |
node tests/mutate-gate.mjs | By hand, after touching a hold in src/record.mjs or src/labs.mjs. It removes each hold from a temporary copy and must print "0 mutations survived". |
node tests/scrub-bench.mjs vendor/meddocan/test/brat --limit 250 | By hand, after changing scrubbing. It reports recall for each kind of personal detail against the synthetic MEDDOCAN cases. --show-leaks prints what slipped through. |
npm run shots -- <outdir> | Before any layout change. It saves a screenshot of every tab at 1290, 900 and 390 pixels wide with the assistant docked. |
The MEDDOCAN corpus is not in the repository. It lives in the gitignored vendor/meddocan/, from Zenodo record 4279323. Download it yourself if you work on scrubbing.
For layout work, look at the screenshots rather than trusting a green test. A laptop window with the assistant open leaves the page about 880 pixels wide, so check the 900 and 390 shots as well as 1290. Page layout rules are @container main queries on #main-content, which measure the page and not the window.
The desktop shell has its own smoke test, covered in Building the app.
Translations
Ovillo ships in English and Spanish. Every new piece of visible text needs both, added to src/i18n.mjs. tests/i18n.test.mjs checks that the two languages have the same keys, so a missing Spanish string fails npm test. Write plain, warm sentences and do not use em dashes in any UI string. If your Spanish is not good enough, say so in the pull request and a maintainer will help rather than machine-translate it.
Assistant prompts
The assistant's prompts and tool descriptions live in src/agent-prompts.mjs and src/agent-tools.mjs. When you change any of them, bump PROMPT_VERSION in src/agent-prompts.mjs and update the pinned version and hash in tests/assistant-cases.test.mjs. That test fails until you do, on purpose, so a prompt never changes silently.
tests/assistant-live.mjs runs the real prompts against your own provider and costs quota, so it is run by hand and is never part of npm test. It needs --yes-send, and takes the provider from HP_LIVE_BASE_URL, HP_LIVE_KEY and HP_LIVE_MODEL, or from --config <file>. --dry-run stops before any network call, and the script prints scores only. It runs on synthetic cases; keep it that way.
Commits and pull requests
- Keep each change small and focused: one idea per pull request.
- Write plain commit messages that say what changed and why. No trailers added by tools.
- Stage files by name (
git add path/to/file), not withgit add -Aorgit add ., so you never commit a stray file. - In the pull request, describe how you tested it: which commands you ran and what you saw. For layout changes, attach screenshots made from synthetic data.
- Never include real medical data, or screenshots of real records, anywhere in a commit, issue or pull request.
Reporting bugs and security issues
Report ordinary bugs as GitHub issues on the repository. Say what you did, what you expected and what happened. Do not paste personal data, document text or screenshots of a real record; describe the shape of the problem or reproduce it with a synthetic file.
Report a security problem privately by email to possible@almostimpossible.agency rather than in a public issue. Include steps to reproduce, and again, no real records.
Licence
[Licence to be chosen before the repository goes public]
Something unclear or out of date? Tell us on GitHub.