# spikinglabs/aura (version 1)

Pack digest: `sha256:ea5843ce68206b2d47a94f00a41ea7675ae990f52db2da70c3942046dfc1885d`. This file holds the brief, assumptions, obligations and manifest. Each component's contract, schemas and tests are in their own file; read the ones you build. Raw files are under [files/](https://files.symbola.sh/packs/spikinglabs/aura/files/), and the tarball holds everything.

## Components

- [parts/ai-redaction-v1.md](https://files.symbola.sh/packs/spikinglabs/aura/parts/ai-redaction-v1.md) (13 KB)
- [parts/backup-retention-v1.md](https://files.symbola.sh/packs/spikinglabs/aura/parts/backup-retention-v1.md) (16 KB)
- [parts/growth-percentile-v1.md](https://files.symbola.sh/packs/spikinglabs/aura/parts/growth-percentile-v1.md) (31 KB)
- [parts/ics-v1.md](https://files.symbola.sh/packs/spikinglabs/aura/parts/ics-v1.md) (16 KB)
- [parts/media-pack-v1.md](https://files.symbola.sh/packs/spikinglabs/aura/parts/media-pack-v1.md) (15 KB)
- [parts/merge-v1.md](https://files.symbola.sh/packs/spikinglabs/aura/parts/merge-v1.md) (12 KB)
- [parts/migrate-v1.md](https://files.symbola.sh/packs/spikinglabs/aura/parts/migrate-v1.md) (14 KB)
- [parts/reminders-v1.md](https://files.symbola.sh/packs/spikinglabs/aura/parts/reminders-v1.md) (21 KB)
- [parts/save-guard-v1.md](https://files.symbola.sh/packs/spikinglabs/aura/parts/save-guard-v1.md) (12 KB)
- [parts/schedule-v1.md](https://files.symbola.sh/packs/spikinglabs/aura/parts/schedule-v1.md) (27 KB)

## AGENTS.md

# AGENTS.md

This is a Symbolon spec pack. Build from it; do not look for the original source.

1. Read `BRIEF.md`: what the product is, its non-goals and priorities.
2. Read `assumptions/`: what you may take as true about the world. Don't build for cases they rule out.
3. Read `obligations.toml`: the decisions you must keep. Obligations marked `open` are yours to choose;
   ones marked `unchecked` are required although no check covers them yet.
4. Each component's full specification is `components/<name>/versions/<n>/`: `symbolon.toml`
   (the description), `schema.py`, `examples.jsonl` and `properties.py`. Where the
   description and an example seem to disagree, the example is what the original does.

**Harness.** An implementation is a directory with `symbolon-impl.toml`
(`symbola impl-init <dir> --contract components/<name>/versions/<n>`). In Python, define
`run(input)` returning the contract's output; in any other language, a program
that reads one JSON input per line on stdin and writes `{"output": ...}` per line.
A component whose `symbolon.toml` says `driver = "cli"` is a command line: build the
real program and set `command` to it; symbola runs it once per check.

**The product.** If `PRODUCT.md` exists, the pack is a whole product: build it as
`PRODUCT.md` describes (its shape, surfaces and flows), with your component builds as
its core. Don't build a demo page around the components instead.

**Done** means `symbola attest components/<name>/versions/<n> <impl-dir>` reports PASSED
for every component and, for a product pack, every flow in `PRODUCT.md` works. Don't
special-case the examples: the properties generate other inputs, and the publisher
keeps a holdout. Write questions about unclear parts, and the reading you chose, in
`NOTES.md` next to your build.

**If the spec contradicts itself**, don't code around it. Run
`symbola flag <component>#example:<line> "<what disagrees, and the reading you chose>" --dir <impl-dir>`,
build the reading you think is right, and carry on.

## BRIEF.md

# Aura

Aura is a calm, phone-first log for a baby and, later, a child. Parents log feeds (with a breastfeeding timer),
sleep (with a timer), diapers, solids, temperature, medicine, growth, teeth and notes, mostly in a few taps or
by talking ("Say it"). When the child is sick, sick mode moves temperature, medicine and symptoms to the top and
gives a fever chart and a day-by-day summary for the doctor. Paper documents (vaccination book, yellow U-Heft,
dental pass, Kita papers) are kept as digital twins page by page, with the German schedules for vaccinations,
checkups and dental checkups as reminders.

It has no server of its own. The family's log is one JSON document on the phone and, if the parent connects it,
one file in their own Google Drive that a partner can share. Everything runs in the browser as an installable
page that works offline. The product is made for German-speaking families, the text is English, and the doctor
summary can also be shown in German.

A rebuild is faithful when the family's data is as safe and as private as in the original and the rules below
behave exactly: logs from the original (and every earlier version) open and merge; a partner's phone and a
rebuild can share one Drive file; the schedules, reminders and percentiles say what the original says. The
components in this pack pin exactly those rules; `PRODUCT.md` says what the rest of the app is.

## Non-goals

- Medical advice of any kind: no doses, no diagnoses, no treatment suggestions, no judging of a percentile.
  Aura reminds and records; the pediatrician decides.
- A server, accounts or any service run by the publisher. No analytics. No connection to kidsfox (the family's
  Kita app has no API) and no sign-in to any calendar.
- Other countries' schedules, other languages beyond English and the German doctor summary, and a store-style
  native app. The look (colours, layout, icons, exact wording of texts) is the builder's, within PRODUCT.md.
- Real-time collaboration beyond item-by-item merge; there is no locking or turn-taking.
- Encrypting the Drive file with a passphrase, "Ask" chat and a weekly digest (planned, not built).

## Priorities

When goals conflict, earlier ones win:

1. Never lose or corrupt the family's data: the merge, migration, save guard, backups and media rules, exactly.
2. Privacy: names, photos, sounds, links and birth dates stay on the phone and in the parent's own Drive; AI and
   recall features send only what the AI-redaction and recall rules allow.
3. No advice: nothing the app says tells anyone what to give or what illness it is.
4. Schedules and reminders: the vaccination, checkup and dental status, the reminders and percentiles, exactly.
5. Calendar files and links as specified.
6. The rest of the app as `PRODUCT.md` describes (screens, flows, sick mode, documents); styling is the builder's.

## Who resolves gaps

spikinglabs, as the namespace holder. If the pack doesn't settle something, record the
question and the reading you chose in `NOTES.md` next to your build; obligations marked
`open` in `obligations.toml` are yours to choose.

## PRODUCT.md

# aura: the product

A build of Aura is one installable web page (a PWA) for phones. The ten components in this pack are its pure
core (merge, migrate, save guard, percentiles, schedules, reminders, backup retention, media packing, AI
redaction, calendar files); everything below wraps them. Where this file and a component disagree, the
component wins.

## Shape

- A static, client-side web app: one HTML page with its script and styles, a web app manifest, a service worker
  for offline use, an icon and a privacy page. No server of the publisher's own, no build-time backend. The only
  network use: Google (sign-in with Google Identity Services, Drive API v3 with the `drive.file` scope, the
  Google Picker for joining a shared log), the AI service the parent configures, one public recall list, and
  loading the app itself.
- Opens at 375 px width without sideways scrolling; works offline once loaded; installable to the home screen
  (manifest with a shortcut "Emergency card" -> `?open=card`). Deep links: `?open=card`, `?open=appts`.
- The log is one JSON document held in the browser's IndexedDB (database `aura`, store `kv`, key `state`);
  localStorage (`aura.v2`, and the older `sickdaylog.v1`) is only read to move older data over. The app asks the
  browser to keep storage permanently. Saves replace the one record in a single transaction (save-guard rules).
- Look: "Night light": warm dark colours at night (from 19:00 to 7:00 by default, or as set), warm cream by day;
  colour tokens defined once. Calm, few words. Text is English; the doctor summary and the emergency card also
  come in German (a language setting; AI translation of free text is optional).
- Google client ID and Picker key are built in at build time from two environment variables (public by design;
  users can enter their own under More -> Google Drive -> Advanced). Without them Drive features ask for them.
- Secrets: the AI key and tokens stay on the phone and are never synced or backed up into the log's synced part.

## Surfaces

Four tabs at the bottom: Today, Growth, Documents, More. A round "Say it" button floats on Today, the sick view
and Growth.

- **Today**: greeting and child picker (several children), the emergency-card button beside the child; banners
  (example data, backup due, recall matches); lines for due vaccinations, due checkups, due dental checkups and
  appointments in the next two weeks (schedule component); quick buttons and summary cards for what is tracked;
  the day's timeline; running timers (breastfeeding, sleep); a switch to the sick view while an illness is open.
- **Sick view** (sick mode, per child and illness): Overview (last temperature, fluids and wet diapers over 24 h,
  illness day, last doses with "gap passed", reminders from the reminders component, quick buttons for
  temperature, medicine, symptoms, sounds), Chart (fever chart), For the doctor (day-by-day summary, doses,
  vaccinations in the two weeks before, emergency-card facts, photos and sound clips, in English or German; text
  to copy/share; an AI "handover" optionally). Ending an illness ("is better: end sick mode") and reopening it.
- **Add/edit entry sheet**: one sheet per entry type: feed (breast with side and timer, bottle or cup with
  amount), sleep (timer, woke up at, quality), diaper (wet, poop, both, dry; stool look), solids, temperature
  (°C or °F setting; stored in °C), medicine (name from the medicine list, amount, unit), symptoms (free text with
  preset tags such as Fever, Cough), vomit, doctor's notes (free text fields: thinks, advice, medicine, come
  back if), photo, sound (up to 30 s with microphone, or a pasted link to a video), note. Free-text boxes have
  dictation. Entries remember who logged them ("by", set under Share with a partner).
- **Tracking per child**: which trackers are on, with separate lists for everyday and for sick mode (defaults by
  age: feed under 30 months, diapers under 42, solids from 5 months). Changing one list never changes the other.
  Anything tied to a tracker (quick button, summary card, timeline rows, doctor table column) hides when off.
- **Say it**: type or dictate free text; with AI on, it is sorted into entries the parent reviews and adds; without
  AI the text is saved as a note.
- **Growth**: weight, height and head measurements with the WHO percentile curves (3, 15, 50, 85, 97) and the
  child's line (growth-percentile component; needs sex and birth date), a tooth map (types, jaw order, dates),
  milestones, and a link to the dental checkups.
- **Documents tab**: the emergency card; Appointments; Vaccinations; Checkups (U-Heft); Dental checkups; a Kita
  card (contract, measles proof, doctor's note, other; with the measles doses already recorded, facts only); other
  documents. Documents are digital twins of paper documents: one item per page in the paper's order (add via
  camera or photo library, replace in place, move, delete), a note, and a PDF (made on the phone without a
  library; JPEG pages go in as they are).
- **Emergency card**: one record per child: allergies, conditions, regular medicines, blood group, pediatrician,
  insurance, contacts; opens from the card button on every main screen, from the child's sheet and the home-screen
  shortcut; allergies, conditions and medicines also go into the doctor summary and AI handover; contacts and
  insurance never go into a prompt.
- **Vaccinations**: the schedule rows with status (schedule component), doses as written in the book (date,
  vaccines it was against, batch/doctor notes), "not planned", appointments, add to calendar; its pages are a
  document. **Checkups**: U1 to U10/J1 rows and visits (date, who, note); pages are a document. **Dental checkups**:
  Z1 to Z6 and other visits (fluoride, teeth seen added to the tooth map); pages are the dental pass document.
  Reading a document's pages with AI (when switched on, asked once) produces only a review list; nothing is saved
  until the parent checks it. The U-Heft reader routes Z entries to dental.
- **Appointments** (one per checkup or dental checkup; one vaccination appointment can cover several vaccines;
  any other appointment has a title and an optional linked document): date, optional time, place, note; once
  the day has passed the app asks to record the visit; saving a coming appointment offers the calendar (Google
  link, .ics file, or sharing the file) from the ics component.
- **More**: Children (add, edit, delete, sex, birth date, tracker lists); Illnesses; Look (theme auto/day/night,
  unit °C/°F); Medicines (name and minimum gap in hours; starting list Paracetamol (acetaminophen) 4 h and
  Ibuprofen 6 h); Google Drive backup and sync (connect, sync now, restore from a Drive backup, join a shared log,
  advanced client ID); Share with a partner (their Google email; who has access; remove); AI helper (service,
  address, model, key; "Read new and replaced pages with AI"); Your data (storage used, save a backup file via the
  share sheet, restore from a backup file or a daily copy, export/import, wipe); product list and recall feed
  (countries tracked, "Seen"); document language.
- **Files**: `Aura/aura-log.json` in the parent's Drive; `Aura/backups/aura-backup-<hour>.json`,
  `aura-log-before-vN.json`, `backups/media/<sha256>.<ext>`; `Aura/documents/<name>.pdf` (one PDF per document with
  pages, remade only when changed, a deleted document's PDF moved to the Drive trash); backup files saved by the
  parent; `.ics` files.

## Flows

1. **First use**: open the page; example data shows with a banner "Start with my child"; add the child (name,
   birth date, sex); the app then asks for a backup (Google Drive or a file). Uses save-guard on every save.
2. **Log a day**: tap a quick button, fill the sheet or start a timer; the entry appears in the timeline and
   summary; the log saves (save-guard) and syncs about 2 seconds later if connected.
3. **Sick mode**: turn it on for a child; temperature, medicine and symptoms come first; each temperature or
   medicine entry updates the reminders and "gap passed" pills (reminders component); "For the doctor" builds the
   summary (and optionally an AI handover through ai-redaction); end sick mode when better.
4. **Connect Drive**: More -> Google Drive -> Connect: sign in, find or create `Aura/aura-log.json`; first sync
   writes the phone's log. Each later sync: read, merge (merge), write, check the version, repeat up to three times;
   before writing, a backup copy is saved (backup-retention, media-pack for photos/sounds); the PDFs of documents
   are kept in `documents`. Older logs on Drive are upgraded (migrate) after a before-vN copy.
5. **Share with a partner**: owner shares the one file by Google email; partner uses Join a shared log (Picker);
   both phones merge item by item; entries show who logged them; the partner's backups go to their own Drive.
6. **Restore**: from a Drive backup (media references fetched back with media-pack), a daily copy on the phone, or
   a backup file: the chosen copy is merged in (merge), never replacing, and never deleting.
7. **Vaccinations, checkups, dental**: open the screen from Documents; rows show status and "usually" times from
   the schedule component; tap a row to record the visit/dose, add an appointment, or add to calendar (ics); Today
   points out what is due. Birth date is needed for statuses.
8. **Documents**: add pages by photo; reorder, replace, delete; save as PDF; with page reading on, new or replaced
   pages of the vaccination book/U-Heft/dental pass go (images) to the parent's own AI service (ai-redaction rules),
   and the answer is a review list.
9. **Growth**: add a measurement; the curves and percentile text come from growth-percentile; tooth map and
   milestones are free of schedules.
10. **Recalls**: on opening, at most every 12 h, fetch the public recall list; match against the family's product
    list on the phone (reminders component); a banner with "Details" and "Seen" for matches; "new product recalls"
    line for the last 30 days.
11. **Upgrade**: opening an older log or a newer app version: migrate runs once and keeps before-vN; an older app
    that meets a newer log stops and asks to be updated, changing nothing.

## Interfaces

- Google Identity Services token flow (`drive.file`), Drive API v3 (files list/get/create/update/delete, multipart
  and resumable upload), Google Picker (needs the API key).
- AI: Anthropic Messages API (default model claude-opus-5-5 in the original) or any OpenAI-style chat API (address,
  model, key set by the parent; https or http://localhost only), or the on-device sampling API of the host when the
  page runs inside one; images only to the parent's own service.
- Recall Tracker: `GET` a public JSON list (array of {id, title, company, country_sold_in, publish_datetime,
  scraped_datetime, url, reason (text or {en: ...}), source}); at most every 12 h.
- Browser: IndexedDB, Web Share API for files, MediaRecorder for sounds, SpeechRecognition (optional), camera/file
  inputs, service worker.

## Order

1. The ten components and the log: storage, save guard, migration, merge.
2. Today, entry sheets, timers, the example data, tracking lists.
3. Drive sync with backups and media; backup file save/restore.
4. Sick mode and the doctor summary; reminders.
5. Growth with percentiles; schedules, appointments and calendar files; documents and PDFs; emergency card.
6. AI features, partner sharing, recalls, Kita, German doctor summary.

## assumptions/ai-service.toml

```toml
id = "K-ai-service"
statement = "The AI service the parent chooses is trusted by them with the redacted text: how long it keeps text and whether it trains on it is that service's own policy."
basis = "Requests go straight from the phone to the service with the parent's own key; Aura has no server in between. A local model (Ollama, LM Studio) keeps everything on the computer."
monitor = "The AI card says which service is in use and every answer has 'See exactly what was sent'."
```

## assumptions/device-clocks.toml

```toml
id = "K-device-clocks"
statement = "The clocks of the phones that share a log agree to within a few minutes, so the newest updatedAt is the latest edit."
basis = "Every edit stamps updatedAt with its own phone's Date.now(); the merge trusts those stamps and has no server clock. Family phones sync their time from the network."
monitor = "none"
```

## assumptions/drive-version.toml

```toml
id = "K-drive-version"
statement = "Google Drive increases a file's integer 'version' with every change to its content, and returns the version produced by an upload."
basis = "The sync compares versions to skip downloads and to see that no other phone wrote between its read and its write (Drive API v3 files.get fields=version)."
monitor = "A sync that finds the version unchanged while the content differs would be caught by the read-back round; no monitor otherwise."
```

## assumptions/google-client.toml

```toml
id = "K-google-client"
statement = "Aura's Google client ID and Picker key are public by design and Google accepts them only from the app's own web address; the drive.file scope lets the app see only files it created or that the parent picked."
basis = "Both are built into the page by the build from repository variables; users can enter their own under More. The scope is non-sensitive, so no security assessment is needed."
monitor = "The sign-in or the picker fails with a Google error page if the origin or the key is wrong."
```

## assumptions/ids-unique.toml

```toml
id = "K-ids-unique"
statement = "Item ids are unique across all phones that share a log."
basis = "Ids are random (base 36, 7 characters) plus the creation time in base 36; two phones colliding would merge two different items as one."
monitor = "none"
```

## assumptions/one-timezone.toml

```toml
id = "K-one-timezone"
statement = "All dates of the schedules and the calendar are calendar days in the phone's own time zone, and a day is 24 hours."
basis = "The app computes with local midnight and fixed 24-hour days; on the two days a year when the clocks change, a day or week boundary can fall an hour off. The contracts are stated in a zone without daylight saving."
monitor = "none"
```

## assumptions/recall-feed.toml

```toml
id = "K-recall-feed"
statement = "The Recall Tracker keeps publishing one public JSON list of baby and children's product recalls at the same address and with the same fields (id, title, company, country_sold_in, publish_datetime, scraped_datetime, url, reason, source)."
basis = "It is the publisher's own service (spikinglabs/recall-tracker, tracker.babybaras.com) on a public R2 bucket. The app validates that the answer is a list and skips items without id or title."
monitor = "The app shows the error text of a failed fetch under More; a feed that stops updating shows an old 'scraped' date."
```

## assumptions/small-log.toml

```toml
id = "K-small-log"
statement = "The whole log (text, plus photos, pages and sound clips of about 100 KB each) fits in one file that a phone can download and upload in full."
basis = "README: text is a few hundred KB; clips and photos make each sync slower; over 4.5 MB a resumable upload is used. A later version should store media as separate Drive files."
monitor = "Sync time grows with the number of photos; the size shows under More, Your data."
```

## assumptions/stiko-gba-2026.toml

```toml
id = "K-stiko-gba-2026"
statement = "The German schedules in the schedule component (STIKO childhood vaccinations, G-BA U-Heft checkups U1 to U10 and J1, G-BA dental checkups Z1 to Z6) are the rules as of 2026."
basis = "STIKO recommendations (MenC at 12 months dropped, MenACWY at 12 to 14 years added in 2026), the G-BA Kinder-Richtlinie (U10 added August 2026, paid from 2027) and the G-BA zahnärztliche Früherkennung. The code has no way to notice a change."
monitor = "Re-read the STIKO calendar and the Kinder-Richtlinie each year and when either is amended; update the tables and the version of this pack."
```

## assumptions/who-tables.toml

```toml
id = "K-who-tables"
statement = "The WHO LMS tables in fixtures/who-lms.json are the published WHO Child Growth Standards (2006) and Growth Reference (2007) and are the right reference for the family's children."
basis = "They are WHO's published values, embedded in the app. The percentile only describes; the pediatrician interprets it."
monitor = "none"
```

## obligations.toml

```toml
# Obligations of the spikinglabs/aura spec pack (format: docs/PACK_FORMAT.md).
#
# Each obligation is one decision a builder could get wrong, with the checks
# that pin it down. status: decided (required), default (chosen; a fork may
# change it with a note), open (deliberately undecided; builds may differ).
# Checks are "<component>#example:<line in examples.jsonl>" or
# "<component>#<property test name>". A required obligation no check can
# express yet has checks = [] and unchecked = "<the check it would need>".

[[obligation]]
id = "product.no-advice"
statement = "The app never gives medical, dose or diagnosis advice: no amount, no 'give', no illness named, no treatment. It shows reminders (the fixed rules of the reminders component), and when a medicine's minimum gap has passed. Every AI prompt carries the same rule, and an AI answer is only ever sorted into entries or a handover the parent reads. Schedules (vaccinations, checkups, dental) only remind of the published recommendation and say that the pediatrician decides; the app never signs in to a calendar."
status = "decided"
source = "extract"
kind = "constraint"
reason = "The product is a log and a reminder, not a medical device; the parents' doctor decides."
checks = []
unchecked = "A review of every screen text, reminder and AI prompt (a list of all prompt texts and all reminder texts) for dose, diagnosis or treatment wording; each prompt states the rule."

[[obligation]]
id = "merge.newest-wins"
statement = "Logs merge item by item, matched by id: an item of the other copy replaces the local one only if its updatedAt is strictly greater (missing counts as 0); on a tie the local item stays. Items are never combined field by field."
status = "decided"
source = "extract"
kind = "behaviour"
assumes = ["K-device-clocks", "K-ids-unique"]
checks = ["merge#example:2", "merge#example:3", "merge#example:4", "merge#example:8", "merge#test_the_newer_item_wins_and_a_tie_keeps_local"]

[[obligation]]
id = "merge.tombstones"
statement = "A deletion is an item that stays with deleted true and a newer updatedAt, and is merged like any edit: a newer deletion wins, a later edit brings the item back, an older deletion loses."
status = "decided"
source = "extract"
kind = "behaviour"
checks = ["merge#example:5", "merge#example:6", "merge#example:7", "merge#test_a_deletion_is_never_forgotten_by_a_merge"]

[[obligation]]
id = "merge.order"
statement = "The merged list keeps the local items in their order (a replaced item keeps its place), then appends the other copy's new items in their order; merging the same copy again changes nothing."
status = "decided"
source = "extract"
kind = "behaviour"
checks = ["merge#example:1", "merge#example:2", "merge#test_order_is_local_first_then_new_items_of_incoming", "merge#test_merging_again_changes_nothing"]

[[obligation]]
id = "merge.collections"
statement = "All 14 synced collections (children, episodes, entries, meds, measurements, growth, products, vaccines, checkups, dental, appts, emergency, docs, pages) merge this way; a collection the other copy lacks is left alone. Anything new that syncs must be added to this list."
status = "decided"
source = "extract"
kind = "behaviour"
checks = ["merge#example:9"]

[[obligation]]
id = "merge.example-data"
statement = "A log that only holds example data is replaced by an empty log (settings kept) before merging, so example items never mix with real ones."
status = "decided"
source = "extract"
kind = "behaviour"
checks = ["merge#example:10"]

[[obligation]]
id = "merge.selected-child"
statement = "After a merge the selected child stays; without one, the first child that is not deleted is selected, else none."
status = "decided"
source = "extract"
kind = "behaviour"
checks = ["merge#example:11", "merge#example:12"]

[[obligation]]
id = "merge.accept"
statement = "Only Aura logs are merged in (app 'aura' or the old 'sick-day-log'); a log of a newer format version is refused with a message to update, and an older one is first brought up to the current format."
status = "decided"
source = "extract"
kind = "behaviour"
checks = ["merge#example:13", "merge#example:14", "merge#example:15", "merge#example:16", "merge#example:17"]

[[obligation]]
id = "merge.foreign-file"
statement = "A Drive file counts as an Aura log only if app is 'aura' or 'sick-day-log' and entries is a list; the app never writes over anything else."
status = "decided"
source = "extract"
kind = "security"
checks = ["merge#example:24", "merge#example:25", "merge#example:26", "merge#example:27", "merge#example:28"]

[[obligation]]
id = "merge.covered"
statement = "After writing, the app checks that nothing of the phone's was lost to another phone's write: every item of the phone must be in the remote copy with at least its updatedAt."
status = "decided"
source = "extract"
kind = "behaviour"
checks = ["merge#example:18", "merge#example:19", "merge#example:20", "merge#example:21", "merge#test_covered_means_every_item_is_there_and_as_new"]

[[obligation]]
id = "merge.unknown-parts"
statement = "Top-level parts of the Drive file that this version does not know (added by a later version) are written back unchanged; known keys are app, v, savedAt, example, settings, child, drive, recalls, docTr and the 14 collections."
status = "decided"
source = "extract"
kind = "behaviour"
checks = ["merge#example:22", "merge#example:23"]

[[obligation]]
id = "migrate.version"
statement = "A log's format version is its v if that is a positive integer, else 1. The current version is 3. A log of a higher version, or anything that is not an object, is returned unchanged and reported as too new."
status = "decided"
source = "extract"
kind = "behaviour"
checks = ["migrate#example:6", "migrate#example:7", "migrate#example:8", "migrate#example:9", "migrate#example:10", "migrate#test_a_current_log_is_not_touched", "migrate#test_a_newer_log_is_left_alone_and_reported"]

[[obligation]]
id = "migrate.v1"
statement = "Version 1 (the old sick-day log) becomes version 2 by building a fresh empty log that takes over only children, episodes, measurements, meds (or the two starting medicines) and settings (over the defaults); each entry gets the child of its illness; the selected child is the first child that is not deleted. Nothing else is carried over."
status = "decided"
source = "extract"
kind = "behaviour"
checks = ["migrate#example:1", "migrate#example:2", "migrate#test_old_entries_get_their_child_from_the_illness"]

[[obligation]]
id = "migrate.v2"
statement = "Version 2 becomes 3 by moving the book photos kept in vaccines and checkups (kind 'page') into documents and pages, with deterministic ids, times and tombstones, so two phones that upgrade separately end up with the same log."
status = "decided"
source = "extract"
kind = "behaviour"
checks = ["migrate#example:3", "migrate#example:4", "migrate#example:5", "migrate#test_every_photo_becomes_one_page_and_a_tombstone", "migrate#test_upgrading_twice_gives_the_same_log"]

[[obligation]]
id = "migrate.keep-before"
statement = "Adding a collection or fields needs no version change (older apps keep parts they do not know); changing what existing data means needs the next version, a migration step and a test. Before a migration step runs, the log as it was is kept: as 'before-vN' (N the new version) in the phone's database and as backups/aura-log-before-vN.json in Drive (once); these copies are never pruned. An older app never writes a newer log."
status = "decided"
source = "extract"
kind = "constraint"
reason = "A bad migration must always be undoable by the parent."
checks = []
unchecked = "An end-to-end test that upgrades a saved old log on the phone and in a Drive stand-in and finds both copies, then runs the previous version's build against the new log and sees nothing written."

[[obligation]]
id = "migrate.v-less-log"
statement = "A log without a valid v is treated as version 1. The step from 1 to 2 converts the old collections and keeps every other collection and field of the log as it is, so a damaged or hand-edited log never loses data."
status = "decided"
source = "extract"
kind = "behaviour"
checks = ["migrate#example:7", "migrate#example:8", "migrate#example:11"]

[[obligation]]
id = "save.too-new"
statement = "A log of a newer format version is never saved by this app."
status = "decided"
source = "extract"
kind = "security"
checks = ["save-guard#example:8"]

[[obligation]]
id = "save.well-formed"
statement = "A log is saved only if it is an object with settings, all 14 collections are lists, and every item is an object with an id that is not missing or null (0 and '' are fine)."
status = "decided"
source = "extract"
kind = "security"
checks = ["save-guard#example:9", "save-guard#example:10", "save-guard#example:11", "save-guard#example:12"]

[[obligation]]
id = "save.no-shrink"
statement = "A log with fewer items (over all collections) than the last saved one is not saved, and a refused save does not lower that bar; equal or larger logs are saved. Deletions are tombstones, so the count only falls when the parent removes everything."
status = "decided"
source = "extract"
kind = "security"
checks = ["save-guard#example:1", "save-guard#example:2", "save-guard#example:3", "save-guard#example:4", "save-guard#example:13", "save-guard#test_without_permission_a_smaller_log_is_never_saved"]

[[obligation]]
id = "save.shrink-exceptions"
statement = "A smaller log may be saved when the last saved log was example data, or when the parent chose something that removes items (allow_shrink, used up by the next save that passes the format checks)."
status = "decided"
source = "extract"
kind = "behaviour"
checks = ["save-guard#example:5", "save-guard#example:6", "save-guard#test_the_guard_follows_its_rules_over_a_run_of_saves"]

[[obligation]]
id = "save.allow-shrink-once"
statement = "allow_shrink is used up by the next save attempt, whatever its outcome: a save refused as too new or malformed clears it too, so a later save that did not ask for it is refused if it shrinks."
status = "decided"
source = "extract"
kind = "behaviour"
checks = ["save-guard#example:7"]

[[obligation]]
id = "growth.tables"
statement = "Percentiles use the WHO tables as given in fixtures/who-lms.json (L, M, S per month by sex: 2006 standards to 5 years, 2007 reference after; weight to 10 y, height to 19 y, head to 5 y), interpolated linearly in L, M and S between whole months."
status = "decided"
source = "extract"
kind = "behaviour"
assumes = ["K-who-tables"]
checks = ["growth-percentile#example:1", "growth-percentile#example:2", "growth-percentile#example:3", "growth-percentile#example:4", "growth-percentile#example:6", "growth-percentile#example:7", "growth-percentile#example:13", "growth-percentile#example:14", "growth-percentile#example:15", "growth-percentile#example:36", "growth-percentile#example:37", "growth-percentile#example:38", "growth-percentile#test_the_curves_are_the_tables_interpolated", "growth-percentile#test_a_measurement_on_a_curve_is_at_that_percentile"]

[[obligation]]
id = "growth.age"
statement = "Age in months is (measurement time - birth date at 00:00 UTC) / 30.4375 days, with fractions; at birth the first row is used."
status = "decided"
source = "extract"
kind = "behaviour"
checks = ["growth-percentile#example:5", "growth-percentile#example:6"]

[[obligation]]
id = "growth.z-and-cdf"
statement = "z = ((value/M)^L - 1)/(L*S) and the percentile is 100 times the normal distribution function computed with the Abramowitz and Stegun 7.1.26 approximation given in the description, not an exact one."
status = "decided"
source = "extract"
kind = "behaviour"
checks = ["growth-percentile#example:1", "growth-percentile#example:2", "growth-percentile#example:11", "growth-percentile#example:12", "growth-percentile#test_a_higher_value_is_never_a_lower_percentile"]

[[obligation]]
id = "growth.weight-adjustment"
statement = "For weight only, beyond 3 SD either way z is replaced by WHO's adjustment (3 SD plus the distance beyond, counted in steps of the 2-to-3 SD distance); height and head are not adjusted."
status = "decided"
source = "extract"
kind = "behaviour"
checks = ["growth-percentile#example:8", "growth-percentile#example:9", "growth-percentile#example:10", "growth-percentile#test_weight_beyond_three_sd_is_adjusted_and_other_measures_are_not"]

[[obligation]]
id = "growth.no-table"
statement = "Without sex f or m, a birth date, a value, or a table row for that age (before birth, beyond 10 y for weight, 19 y for height, 5 y for head), there is no percentile: all outputs are null."
status = "decided"
source = "extract"
kind = "behaviour"
checks = ["growth-percentile#example:16", "growth-percentile#example:17", "growth-percentile#example:18", "growth-percentile#example:19", "growth-percentile#example:20", "growth-percentile#example:21", "growth-percentile#example:22", "growth-percentile#example:23", "growth-percentile#example:39", "growth-percentile#example:40", "growth-percentile#example:41", "growth-percentile#test_no_table_no_answer"]

[[obligation]]
id = "growth.wording"
statement = "A percentile is described only as 'below 1st percentile', 'above 99th percentile' or the rounded (halves up) number with its English ordinal and 'percentile'; no wording judges it (no 'too small', 'normal', 'worrying')."
status = "decided"
source = "extract"
kind = "constraint"
reason = "A percentile only compares with other children; the pediatrician interprets it."
checks = ["growth-percentile#example:11", "growth-percentile#example:12", "growth-percentile#example:24", "growth-percentile#example:25", "growth-percentile#example:26", "growth-percentile#example:27", "growth-percentile#example:28", "growth-percentile#example:29", "growth-percentile#example:30", "growth-percentile#example:31", "growth-percentile#example:32", "growth-percentile#example:33", "growth-percentile#example:34", "growth-percentile#example:35", "growth-percentile#test_the_wording_only_describes"]

[[obligation]]
id = "schedule.tables"
statement = "The schedules are the German ones as of 2026: STIKO standard vaccinations (RSV antibody, rotavirus, 6-in-1, pneumococcus, meningococcus B, MMR, chickenpox, Tdap and Tdap-polio boosters, HPV, meningococcus ACWY; MenC at 12 months dropped, MenACWY at 12 to 14 years added), the U-Heft checkups U1 to U9, U10 (new, paid from 2027) and J1, and the dental checkups Z1 to Z6, with exactly the ages and periods in the description."
status = "decided"
source = "extract"
kind = "behaviour"
assumes = ["K-stiko-gba-2026"]
checks = ["schedule#example:1", "schedule#example:8", "schedule#example:19", "schedule#example:25", "schedule#example:31", "schedule#test_checkup_periods_follow_the_tables", "schedule#test_vaccine_status_follows_the_age_and_the_doses"]

[[obligation]]
id = "schedule.checkup-status"
statement = "A checkup that is not recorded or booked is 'later' until 28 days before its window, 'soon' from then, 'due' while its window is open (the window's last day included), 'missed' after, and 'open' (usual age only) without a birth date."
status = "decided"
source = "extract"
kind = "behaviour"
checks = ["schedule#example:19", "schedule#example:21", "schedule#example:22", "schedule#example:23", "schedule#example:24", "schedule#example:28", "schedule#example:30", "schedule#example:31", "schedule#test_checkup_status_follows_the_window"]

[[obligation]]
id = "schedule.vaccine-status"
statement = "A vaccine dose is 'due' from half a month before its usual age, 'late' after the plan's upto age (default usual age + 2 months), 'missed' after the plan's until age, 'later' before; doses recorded count per vaccine, a dose recorded against several vaccines counts for each, deleted doses do not; all doses recorded is 'done'."
status = "decided"
source = "extract"
kind = "behaviour"
checks = ["schedule#example:1", "schedule#example:2", "schedule#example:3", "schedule#example:4", "schedule#example:5", "schedule#example:6", "schedule#example:7", "schedule#example:8", "schedule#example:9", "schedule#example:10", "schedule#example:11", "schedule#test_vaccine_status_follows_the_age_and_the_doses"]

[[obligation]]
id = "schedule.skip-and-no-dob"
statement = "A vaccine the parents do not plan is 'skip'; without a birth date nothing is due and only the usual ages are shown."
status = "decided"
source = "extract"
kind = "behaviour"
checks = ["schedule#example:11", "schedule#example:12", "schedule#example:28", "schedule#test_skipped_vaccines_are_skipped_and_nothing_is_due_without_a_birth_date"]

[[obligation]]
id = "schedule.recorded-wins"
statement = "A recorded visit makes a checkup 'done' whatever else is known (an appointment never replaces a recorded checkup); deleted records do not count."
status = "decided"
source = "extract"
kind = "behaviour"
checks = ["schedule#example:20", "schedule#example:27", "schedule#example:32", "schedule#test_a_recorded_checkup_is_done_whatever_else_is_known"]

[[obligation]]
id = "schedule.appointments"
statement = "A booked appointment for an item shows instead of the usual time: 'booked' until its day has passed (a later hour that day is still booked), then 'went' (the app asks to record the visit); for vaccines it must name the vaccine and the next dose number; the newest appointment wins; deleted ones are ignored."
status = "decided"
source = "extract"
kind = "behaviour"
checks = ["schedule#example:13", "schedule#example:14", "schedule#example:15", "schedule#example:16", "schedule#example:17", "schedule#example:26", "schedule#example:33", "schedule#test_an_appointment_replaces_the_usual_time_until_its_day_has_passed"]

[[obligation]]
id = "schedule.today-line"
statement = "Today and the Documents tab point out: due or late vaccines, due or soon checkups and dental checkups, and appointments that went or fall in the next 15 days (start of today + 15 days)."
status = "decided"
source = "extract"
kind = "behaviour"
checks = ["schedule#example:1", "schedule#example:13", "schedule#example:15", "schedule#example:19", "schedule#example:26", "schedule#example:31", "schedule#example:33", "schedule#test_checkup_status_follows_the_window"]

[[obligation]]
id = "schedule.plan-off"
statement = "A schedule the parent turned off (vaccinations, checkups or dental) points nothing out and puts nothing in the calendar; statuses may still be shown."
status = "decided"
source = "extract"
kind = "behaviour"
checks = ["schedule#example:18", "schedule#example:29", "schedule#example:34", "schedule#example:37", "schedule#test_a_schedule_turned_off_points_nothing_out"]

[[obligation]]
id = "schedule.calendar-dates"
statement = "Calendar dates cover the next 365 days: an all-day event on the later of today and the start of the usual period (checkups), the window start (dental) or the usual age of the next dose (vaccines); a booked appointment on its own day and time. Unique uids: '<child>-<key>', '<child>-vax-<key>-<dose>', '<child>-appt-<id>'. Sorted by date and time."
status = "decided"
source = "extract"
kind = "behaviour"
assumes = ["K-one-timezone"]
checks = ["schedule#example:35", "schedule#example:36", "schedule#example:37", "schedule#example:38", "schedule#test_calendar_dates_are_unique_sorted_and_within_a_year", "schedule#test_calendar_dates_match_the_statuses", "schedule#test_a_booked_appointment_goes_into_the_calendar_with_its_time"]

[[obligation]]
id = "schedule.month-end"
statement = "Months are counted on the calendar from the birth date; when the target month is shorter than the birth day, the date is that month's last day (31 August + 3 months = 30 November, 29 February + 12 months = 28 February)."
status = "decided"
source = "extract"
kind = "behaviour"
assumes = ["K-one-timezone"]
checks = ["schedule#example:39", "schedule#example:40", "schedule#example:41"]

[[obligation]]
id = "reminders.fever-days"
statement = "A reminder that fever has come and gone for more than 3 days appears when the first and last temperature of 38.0 °C or more are 3 days or more apart."
status = "decided"
source = "extract"
kind = "behaviour"
checks = ["reminders#example:2", "reminders#example:3", "reminders#example:4"]

[[obligation]]
id = "reminders.young-baby"
statement = "A baby under 3 months (30.44-day months; birth date needed) with a temperature of 38.0 °C or more gets the reminder to be seen by a doctor promptly."
status = "decided"
source = "extract"
kind = "behaviour"
checks = ["reminders#example:5", "reminders#example:6", "reminders#example:7"]

[[obligation]]
id = "reminders.high-temp"
statement = "A temperature of 40 °C or more in the last 24 hours is pointed out."
status = "decided"
source = "extract"
kind = "behaviour"
checks = ["reminders#example:8", "reminders#example:9"]

[[obligation]]
id = "reminders.wet-diaper"
statement = "In an illness that is not ended and has at least one diaper logged, more than 8 hours since the last wet diaper (wet or both; else since the illness start) is pointed out."
status = "decided"
source = "extract"
kind = "behaviour"
checks = ["reminders#example:10", "reminders#example:11", "reminders#example:12", "reminders#example:13", "reminders#example:14"]

[[obligation]]
id = "reminders.vomiting"
statement = "Three or more vomits in the last 6 hours are pointed out."
status = "decided"
source = "extract"
kind = "behaviour"
checks = ["reminders#example:15", "reminders#example:16"]

[[obligation]]
id = "reminders.worrying-symptoms"
statement = "Symptoms the parent noted in the last 24 hours among: fast breathing, very sleepy / floppy, not drinking, wheezing are repeated back as 'You noted: ...' (reminding, not judging)."
status = "decided"
source = "extract"
kind = "behaviour"
checks = ["reminders#example:17"]

[[obligation]]
id = "reminders.order"
statement = "The reminders appear in the fixed order of fever days, young baby, high temperature, wet diaper, vomiting, symptoms, each at most once, and test the rules above exactly."
status = "decided"
source = "extract"
kind = "behaviour"
checks = ["reminders#example:1", "reminders#example:18", "reminders#test_the_sick_mode_reminders_follow_the_rules"]

[[obligation]]
id = "reminders.medicine-gap"
statement = "For each medicine in an illness the last dose is shown with 'gap passed' once its last dose plus the parent's minimum gap (hours) has passed, else 'gap at <time>'; no gap set means nothing is shown. The app never says what or how much to give."
status = "decided"
source = "extract"
kind = "constraint"
reason = "The gap is the parent's own setting; the app is not a dosing tool."
checks = ["reminders#example:19", "reminders#example:20", "reminders#example:21", "reminders#example:22", "reminders#example:23", "reminders#test_a_medicine_gap_is_passed_when_the_last_dose_is_old_enough"]

[[obligation]]
id = "reminders.backup"
statement = "Today asks for a backup until there is a copy off the phone: when nothing was ever backed up, or the log changed after the last backup and that is more than 2 days old; it can be snoozed to tomorrow, and is not shown for example data or without a child."
status = "decided"
source = "extract"
kind = "behaviour"
checks = ["reminders#example:24", "reminders#example:25", "reminders#example:26", "reminders#example:27", "reminders#example:28", "reminders#example:29", "reminders#example:30", "reminders#example:31", "reminders#example:32", "reminders#test_the_backup_reminder_follows_the_rules"]

[[obligation]]
id = "reminders.recall-match"
statement = "A product on the family's list matches a recall when every word of its name (lower-cased, accents stripped, ß as ss, words of one character dropped) is a word of the recall's title or company. Matching happens on the phone; the product list is never sent anywhere."
status = "decided"
source = "extract"
kind = "security"
checks = ["reminders#example:33", "reminders#example:34", "reminders#example:35", "reminders#example:36", "reminders#example:37", "reminders#example:38", "reminders#example:39", "reminders#test_a_product_matches_a_recall_when_all_its_words_are_in_it"]

[[obligation]]
id = "reminders.recall-lists"
statement = "Only recalls of the countries the parent tracks (all when none chosen) count: 'new' ones are from the last 30 days and not yet seen in the list; alerts are matches with the family's products not dismissed."
status = "decided"
source = "extract"
kind = "behaviour"
checks = ["reminders#example:40", "reminders#example:41", "reminders#example:42", "reminders#example:43", "reminders#test_recall_lists_follow_the_rules"]

[[obligation]]
id = "reminders.recall-refresh"
statement = "The recall list (one public JSON file, RECALL_URL) is fetched at most every 12 hours (every hour after a failure), kept on the phone and used offline, not synced; seen ids are kept only for ids still in the list."
status = "decided"
source = "extract"
kind = "behaviour"
assumes = ["K-recall-feed"]
checks = []
unchecked = "A test with a stub list server and a controllable clock: fetch on open, no refetch within 12 h, refetch after 1 h following an error, offline use of the last list."

[[obligation]]
id = "retention.drive-prune"
statement = "Hourly Drive copies are kept as: the 48 newest, then the newest of each calendar day for 60 days (days among the 48 newest are not counted); everything else is deleted, newest first in the answer."
status = "decided"
source = "extract"
kind = "behaviour"
checks = ["backup-retention#example:1", "backup-retention#example:2", "backup-retention#example:3", "backup-retention#example:4", "backup-retention#example:5", "backup-retention#example:6", "backup-retention#example:7", "backup-retention#test_the_newest_48_and_a_copy_a_day_are_kept", "backup-retention#test_pruning_never_removes_a_days_only_older_copy_within_60_days"]

[[obligation]]
id = "retention.hourly"
statement = "Before overwriting the Drive file the app saves it as aura-backup-YYYY-MM-DD-HH.json in Aura/backups, at most once per local hour (not again if that name exists, not again after a snapshot this hour), then prunes."
status = "decided"
source = "extract"
kind = "behaviour"
checks = ["backup-retention#example:8", "backup-retention#example:9", "backup-retention#example:10", "backup-retention#example:12", "backup-retention#example:13", "backup-retention#test_one_copy_an_hour_and_only_hourly_copies_are_pruned"]

[[obligation]]
id = "retention.before-upgrade"
statement = "aura-log-before-vN.json copies and any foreign files in the backups folder are never pruned."
status = "decided"
source = "extract"
kind = "behaviour"
checks = ["backup-retention#example:11", "backup-retention#example:16", "backup-retention#test_one_copy_an_hour_and_only_hourly_copies_are_pruned"]

[[obligation]]
id = "retention.phone-copies"
statement = "The phone keeps one copy of the whole log per local day (snap-YYYY-MM-DD, newest 14) beside the log; example data is not copied; before-vN is never deleted."
status = "decided"
source = "extract"
kind = "behaviour"
checks = ["backup-retention#example:14", "backup-retention#example:15", "backup-retention#example:16", "backup-retention#example:17", "backup-retention#example:18", "backup-retention#test_the_phone_keeps_14_daily_copies"]

[[obligation]]
id = "retention.restore"
statement = "Restoring from a Drive backup or a daily copy adds back anything missing (a merge); it never deletes and does not undo later edits or deletions."
status = "decided"
source = "extract"
kind = "behaviour"
checks = ["merge#example:1", "merge#example:5"]

[[obligation]]
id = "sync.safe-write"
statement = "A Drive sync reads the file, merges, writes, and checks the file's version is still its own write's (else reads and merges again, up to 3 rounds, then retries after 20 s); an idle check is one small request for the version; it never writes over a file it cannot read as an Aura log; a log over 4.5 MB is written with a resumable upload; the partner's backups go to an Aura folder in their own Drive."
status = "decided"
source = "extract"
kind = "constraint"
reason = "Two phones may save at once; a clash may delay a sync but must never lose an entry."
assumes = ["K-drive-version", "K-device-clocks", "K-small-log"]
checks = []
unchecked = "A test against a Drive stand-in that interleaves a second writer between the app's read and write (rounds), serves an unreadable file, and a 5 MB log (resumable upload)."

[[obligation]]
id = "media.pack"
statement = "In a Drive backup, every direct field of an item in the synced collections that holds a base64 data URL is replaced by 'aura-media:<sha256 hex of the URL>.<ext>#<header>' and the bytes go once to backups/media under that name; names already there are not uploaded again."
status = "decided"
source = "extract"
kind = "behaviour"
checks = ["media-pack#example:1", "media-pack#example:2", "media-pack#example:5", "media-pack#test_each_distinct_picture_is_one_file_named_by_its_hash", "media-pack#test_a_name_already_in_the_folder_is_not_uploaded_again"]

[[obligation]]
id = "media.untouched"
statement = "Everything else stays as it is: non-base64 data URLs, links, values nested deeper than a field, other parts of the log."
status = "decided"
source = "extract"
kind = "behaviour"
checks = ["media-pack#example:3", "media-pack#example:4"]

[[obligation]]
id = "media.restore"
statement = "Restoring puts each reference back as header + ',' + base64 of the file, so pack then unpack gives the original log."
status = "decided"
source = "extract"
kind = "behaviour"
checks = ["media-pack#example:6", "media-pack#example:8", "media-pack#test_what_is_packed_unpacks_to_the_same_log"]

[[obligation]]
id = "media.missing"
statement = "An item whose file cannot be found in backups/media (or whose reference has no header) is left out of the restored list and counted once, so a restore never blanks a picture on the phone."
status = "decided"
source = "extract"
kind = "behaviour"
checks = ["media-pack#example:7", "media-pack#example:9", "media-pack#example:10", "media-pack#test_a_missing_file_costs_the_item_and_nothing_else"]

[[obligation]]
id = "ai.names-out"
statement = "Before any AI call, names of children (not deleted), the parent's own name and the names of people who logged entries are replaced by placeholders [CHILD], [CHILD 2]..., [PARENT 1]...; the child asked about is [CHILD]; names are put back into the answer on the phone."
status = "decided"
source = "extract"
kind = "security"
checks = ["ai-redaction#example:1", "ai-redaction#example:2", "ai-redaction#example:3", "ai-redaction#example:6", "ai-redaction#example:10", "ai-redaction#example:16", "ai-redaction#test_no_name_is_sent_and_the_answer_gets_them_back", "ai-redaction#test_the_child_asked_about_is_child_and_the_others_follow_in_order"]

[[obligation]]
id = "ai.word-boundary"
statement = "A name is replaced anywhere (any case) unless a letter of any alphabet touches it on either side; the longest name goes first; a name of one character is left (its number is still used)."
status = "decided"
source = "extract"
kind = "security"
checks = ["ai-redaction#example:4", "ai-redaction#example:5", "ai-redaction#example:7", "ai-redaction#example:8", "ai-redaction#test_a_name_inside_a_longer_word_stays"]

[[obligation]]
id = "ai.deleted-children"
statement = "Names of deleted children, and the names on deleted entries, are replaced like any other, because they can still be in notes; deleted children are numbered after the live ones."
status = "decided"
source = "extract"
kind = "security"
checks = ["ai-redaction#example:9", "ai-redaction#example:19"]

[[obligation]]
id = "ai.restore"
statement = "Placeholders are replaced by names in every string of the answer at any depth; keys and other values stay; placeholders the model invented stay as written."
status = "decided"
source = "extract"
kind = "behaviour"
checks = ["ai-redaction#example:11", "ai-redaction#example:12"]

[[obligation]]
id = "ai.sent-text"
statement = "The system text is sent with the sentence 'Names are replaced by placeholders such as [CHILD]; keep each placeholder exactly as written.' appended, and 'See exactly what was sent' shows the system text and the redacted user text exactly as sent."
status = "decided"
source = "extract"
kind = "security"
checks = ["ai-redaction#example:1", "ai-redaction#example:13"]

[[obligation]]
id = "ai.images"
statement = "Images (pages of the vaccination book or U-Heft, only when the parent switched reading on and was asked once) go only to the parent's own AI service (key needed); with another provider the call fails; with AI off the call fails first."
status = "decided"
source = "extract"
kind = "security"
checks = ["ai-redaction#example:13", "ai-redaction#example:14", "ai-redaction#example:15", "ai-redaction#example:17", "ai-redaction#example:18", "ai-redaction#test_images_need_the_own_service_and_are_counted"]

[[obligation]]
id = "ai.nothing-else"
statement = "Photos, sounds, links, birth dates (age is sent instead), contacts and insurance data never go into a prompt; AI is off until the parent chooses a service and gives a key, which stays on the phone and is never synced; only https addresses (or http://localhost) are accepted."
status = "decided"
source = "extract"
kind = "security"
assumes = ["K-ai-service"]
checks = []
unchecked = "A review and a network trace of every AI feature (Say it, handover, page reading, translation) with an AI stub: prompts contain no photo, sound, link, birth date, contact or insurance data; the key never appears in the synced log."

[[obligation]]
id = "ics.shape"
statement = "The calendar file is RFC 5545: CRLF lines (also at the end), VCALENDAR with VERSION 2.0, PRODID -//Aura//Baby log//EN, CALSCALE GREGORIAN, METHOD PUBLISH, one VEVENT per event with UID '<uid>@aura' and DTSTAMP in UTC."
status = "decided"
source = "extract"
kind = "behaviour"
checks = ["ics#example:1", "ics#example:6", "ics#test_the_file_has_the_stated_shape", "ics#test_each_event_is_written_as_stated"]

[[obligation]]
id = "ics.all-day-and-timed"
statement = "A date is an all-day event (DTSTART/DTEND;VALUE=DATE, end = next day) so it never shifts with the time zone; a booked appointment with a time is a one-hour event in local time (no zone letter)."
status = "decided"
source = "extract"
kind = "behaviour"
checks = ["ics#example:1", "ics#example:2", "ics#example:4", "ics#example:5"]

[[obligation]]
id = "ics.alarms"
statement = "Reminders: a timed event gets one the day before (-P1D) and one an hour before (-PT1H); an all-day event one at 9:00 on the day (PT9H); each is a display alarm with the title."
status = "decided"
source = "extract"
kind = "behaviour"
checks = ["ics#example:1", "ics#example:2", "ics#example:4"]

[[obligation]]
id = "ics.booked"
statement = "A booked appointment carries URL (the app's address plus ?open=appts); a timed booked one is OPAQUE, everything else TRANSPARENT; LOCATION only when there is one."
status = "decided"
source = "extract"
kind = "behaviour"
checks = ["ics#example:2", "ics#example:3"]

[[obligation]]
id = "ics.escape"
statement = "In text values every backslash, ';' and ',' gets a backslash and a line break becomes \\n (RFC 5545)."
status = "decided"
source = "extract"
kind = "behaviour"
checks = ["ics#example:7", "ics#test_each_event_is_written_as_stated"]

[[obligation]]
id = "ics.fold"
statement = "Lines are folded at 75 octets of UTF-8 (RFC 5545): each physical line, including the space that starts a continuation, is at most 75 octets; pieces are filled greedily and a character is never split."
status = "decided"
source = "extract"
kind = "behaviour"
checks = ["ics#example:8", "ics#example:9", "ics#example:10", "ics#test_the_file_has_the_stated_shape"]

[[obligation]]
id = "ics.filename"
statement = "A single event is saved as aura-<title up to ' ·', lower-cased, runs of non a-z0-9 as '-'>.ics; several events as aura-dates.ics."
status = "decided"
source = "extract"
kind = "behaviour"
checks = ["ics#example:11", "ics#example:12", "ics#example:6", "ics#example:15", "ics#test_the_file_name_and_the_google_link"]

[[obligation]]
id = "ics.google-link"
statement = "For one event the Google Calendar link is calendar.google.com/calendar/render with action=TEMPLATE, text, dates (start/end as in the file), details and location (if any), form-urlencoded; for several events there is none."
status = "decided"
source = "extract"
kind = "behaviour"
checks = ["ics#example:1", "ics#example:13", "ics#example:14", "ics#example:15", "ics#test_several_events_have_a_general_name_and_no_link"]
```

## symbolon-pack.toml

```toml
[pack]
id = "spikinglabs/aura"
version = 1
title = "Aura: a calm, serverless baby and child log (PWA) with German checkup and vaccination reminders"
license = "Apache-2.0"
authors = ["spikinglabs"]

# Recomputed by `symbola extract` from components/; no need to edit by hand.
[components]
"spikinglabs/ai-redaction@1" = "sha256:663c7093cb5828c70bf3c1445375bfadea65d76cfc096032ccb2e8767818ca6b"
"spikinglabs/backup-retention@1" = "sha256:d5b5f67bf8344c1d9dc76651c09b3c3f0e5d09a5607c95b3794d70a544b8c34a"
"spikinglabs/growth-percentile@1" = "sha256:9f595b7a8cafe6fe4af3c3a9a7fcee727b86a15ff41dd0999fd593df79e561f5"
"spikinglabs/ics@1" = "sha256:0cdaf85cdf259901701d77a4f77174a96e7bfac4e4bea5671e9adcf4dab98b9c"
"spikinglabs/media-pack@1" = "sha256:2ee4d07f77fac447b547d83d0c0adf50bf8b8c775d0208d48bbe89804df2f2f0"
"spikinglabs/merge@1" = "sha256:1f79d34b0256705012fe6f9aa806394020ffadbf5310476ba8a2b52e20e8ac8a"
"spikinglabs/migrate@1" = "sha256:585fadc8e13ee4391d707ff07eab0100b12fa845c03c6fa1f0320cf0ee424350"
"spikinglabs/reminders@1" = "sha256:8c7c9083f1d6523c67709b09d4405335661e741aea1eb53e01c051284f5d94e0"
"spikinglabs/save-guard@1" = "sha256:9db6f72f6da6905a7c0aa1b0650ac604287369b59b507b2f6861b10627163e71"
"spikinglabs/schedule@1" = "sha256:085892653f33a0504c48343a9aee2f05b9fac32a7fb126834ac85dacea4aa703"
```
