A rule that needs a file format
On the day Spark was designed, Dad settled two translation modes, practical and high quality, and then one rule over both of them:
실용이든 고품질이든, 맥락은 쌓여야 해. 한번 쓰고 버리는 게 아니라 모든게 코퍼스 자료야.
"Practical or high quality, the context has to build up. Nothing is used once and thrown away; everything is corpus material."
A sentence like that is easy to agree with and easy to break. A cache gets cleared, a table gets migrated, a row gets updated in place, and the earlier reading is gone. So Spark keeps the rule in the shape of its storage. The engine has exactly one ground truth: an append-only log of operations. Every page you see, a moment, a trip, a search result, is computed from that log and can be computed from it again.
Three things on disk
| Path under the data root | What it holds | Standing |
|---|---|---|
corpus/<YYYY-MM>.jsonl | Every op, one JSON object per line, one file per UTC month of receipt | Ground truth |
media/<moment_id>/<media_id>/<filename> | Photos, recordings and documents exactly as uploaded, never re-encoded | Kept bytes |
mirror.sqlite3 | The SQLite view every read goes through | Derived, never truth |
The data root is not written in the code either. It comes from the family registry through the host's profile, and an environment variable is the explicit override that tests use. One process writes the log: the engine. Nothing else appends to it, and nothing ever rewrites it.
Why the month of receipt
A file per month keeps each file small and makes order obvious: 2027-03.jsonl sorts before 2027-04.jsonl by name alone. The month is the one in which the engine received the op, in UTC, never the month the photo was taken. A capture that sat on the phone for a week still lands at the end of the current file, so the log only ever grows at its tail.
The capture's own time is a separate fact, carried inside the record: captured_at, timezone and local_date, supplied by the client that saw it happen. The engine adds received_at from its own clock, and the helper that produces it says so in its docstring: server receipt time, never capture truth. Keeping the two apart is what later lets Spark tell a late capture from a fresh one.
The append
The first code block is the whole write, and the read that mirrors it. Three details carry the weight:
- The line is veiled first.
encrypt_for_writeturns the JSON into a Fernet token when the key is present and passes it through plain when it is not. - A torn tail gets a newline before the next op. If an earlier append was cut short before its newline, by a power loss or a killed process, the next op would glue itself onto that fragment. Both would read back as one undecodable line, and replay would throw away a good record along with the broken one. Checking the last byte costs one seek.
fsyncbefore returning. Whenappendreturns, the bytes have reached the disk. The caller, the one write path you meet in the next lesson, touches the mirror only after that.
A test pins the torn case. It writes half an op by hand, appends a real one after it, deletes the mirror and reopens. The real moment is there, and health reports exactly one corrupt line. The test's fragment is plain JSON. With the veil on, a torn tail is a cut Fernet token and counts as opaque, with the consequences shown in the last section.
Reading it back
replay walks the month files in name order and each file in line order. It never sorts by id, on purpose. Spark's ids are ULIDs, which begin with a millisecond timestamp, so they look sortable. Two ids minted in the same millisecond differ only in random low bits, though, and sorting them would scramble two ops whose order was known. The order of lines in the files is the order things happened to the engine, and it is the only order replay trusts.
A line replay cannot read is skipped and counted, never fatal. Its first character decides the bucket. A line that starts with { and still fails to parse is corrupt: a torn or mangled plain line. Any other failure is opaque. Usually that is a veiled line under a key this process does not hold right now. A torn veiled line lands there too, since a cut token still starts with g.
The veil, and what it is for
Each line is encrypted on its own with Fernet, through the family kit's Veil, the same pattern cwkPippa uses for its conversation logs. Line by line, because an append-only file has to stay appendable; encrypting the whole file would mean rewriting it for every op. Reading needs no flag. A Fernet token starts with g in base64 and a plain JSON line starts with {, so one file may hold both, and a plain line written before the key existed still reads afterwards.
The passphrase lives in the macOS Keychain on the service host. The threat model is modest by design: a preview thumbnail, a glance over a shoulder, a backup copy landing somewhere new. It does not stop someone who holds both the disk and the Keychain. The policy is graceful too. A Mac without the key writes plain text, logs one warning, and the health route says "veil": false, so a missing key is loudly visible and never fatal. Media files and the mirror stay plain working surfaces. Only the JSONL ground truth is veiled.
A blind mirror says it is blind
The dangerous case runs the other way: the key existed when the lines were written and cannot be read now, because the keychain is locked or the files are a copy on another Mac. Replay skips those lines, so the mirror it builds is missing moments while looking perfectly healthy. The second code block is the engine refusing to trust such a mirror. It leaves the log fingerprint unset, so the next boot or the next explicit rebuild tries again, and health reports degraded: true with the number of opaque lines.
One more guard rides on the same signal. At startup the engine seeds Dad's six translation purposes if the log has never had them. While any line is opaque it seeds nothing, because a mirror built blind may only look empty, and seeding then would write rows that already sit in the lines it cannot read.