Dad's two sentences
The second mode began as two sentences Dad said on the design day. The first one set its shape:
실용이면 브레인 하나면 되지만, 고품질이면 브레인을 복수 선택해서 다른 버전을 볼 수 있어야 하고, 원문을 보지 않은 브레인의 cold-review도 선택적으로 가능하게. 길이 제한 없어야 하고.
Practical needs one brain. High quality picks several brains so he can compare different versions, optionally adds a cold review by a brain that never saw the original, and has no length limit.
The second one named what that shape serves:
고품질은 용도도 선택 가능하게. 고품질이지만 단순 소셜미디어 포스트 용인지, 에세이인지, 에디토리얼인지, 책인지, 일상 대화용인지, 자막용인지 등등.
High quality also lets him choose the purpose: a social media post, an essay, an editorial, a book, everyday conversation, subtitles, and so on.
This track follows those two sentences into code. This lesson covers the two choices Dad makes before a word is written: the purpose and the seats.
Six rows Dad can edit
The important word is "등등", and so on. A list that ends that way will grow, so Spark keeps purposes as data. backend/corpus.py declares PURPOSE_SEEDS: six dictionaries, each with an id, a name, a guidance paragraph and a constraints line. The comment above them quotes Dad's sentence and states the rule: seeded once, and after that they are rows Dad edits and adds to.
| Purpose | What its row asks for |
|---|---|
| Social media post | Natural, in the voice of the platform and the poster; stay within the platform's length when one is named (X: 280 characters) |
| Essay | Reflective first person; the author's line of thought, paragraphing and rhythm |
| Editorial | A clear thesis in a newspaper's register; every fact, number and name exactly as stated |
| Book | The author's voice first; names and terms consistent, the running glossary binding; chapters and headings stay where they are |
| Everyday conversation | How people say it out loud, with the politeness the relationship calls for |
| Subtitles | One cue in, one cue out, numbers and timings untouched; at most two lines of about 42 characters, readable at about 17 characters per second |
Nothing in the engine branches on these ids. The writer's system prompt and the reviewer's both read a row through one helper, _purpose_lines, which prints the name, the guidance and the constraints, or "none beyond the guidance" when the constraints are empty. A seventh purpose is a POST /api/purposes with a name and its guidance. Changing the essay's guidance is a PATCH that may touch only name, guidance, constraints and order. Neither needs a deploy.
In Dad's OOP terms, the code holds the class and the corpus holds the instances. OO Quest teaches that lens. Here it simply means the list is his.
Seeded once, and never into the dark
Seeding is where a data-driven list usually goes wrong. seed_purposes runs when the engine starts and records a purpose.create only for a seed id the log has never had. A purpose Dad edited keeps his edit through every restart, because its row already exists and the seed is skipped.
The second guard is quieter. Spark's log is veiled, and without the passphrase the mirror is built with some lines it cannot read. Such a mirror may only look empty. Seeding then would write a second creation for rows the log already holds. So while the corpus reports opaque lines, seeding writes nothing. The test in the third code block pins the first behaviour, along with the idempotent create: the same client request id twice yields one row. The opaque guard has no test of its own: the early return 0 in the first code block is all there is of it.
A seat is a brain, an effort and a model
The other choice is who writes. A seat is SeatIn: a brain, an optional effort, an optional model. A job is VersionsIn: a client request id, a purpose id, a target language, one to eight writer seats and up to eight reviewer seats. Every seat passes through _seat before anything starts.
_seat judges against the family brain catalog, which Spark does not own. backend/kit_pippa_brains.py is a generated file vendored from cwkSiblingKit: seven brains (Claude, ChatGPT, Kimi, Grok, GLM, Gemini, Ollama) and the efforts each one offers. An unknown brain is a 400. An effort the brain does not offer is a 400.
Ollama broke that second rule in a useful way. Its levels belong to each model, so the catalog lists none for it, and the old check turned every explicit Ollama effort into a 400. The fix in 793151d lets a brain with an empty list pass and leaves the judgement to the owner, which knows the seat's model. A brain with a list is still held to it. The phone applies the same idea to its menus: a seat's effort choices follow its model when the live menu names that model's own levels, and a chosen level the model lacks stays on the chip marked as not offered instead of being silently swapped.
Where the picker and its memory come from
GET /api/brains returns the catalog with each brain's efforts, plus the live vessel menu, which the engine asks for through its utility worker. If that ask fails, the picker still gets the catalog along with the error. The web draws seats with the kit's KitBrainSeat, and the phone reads the kit's generated PippaBrainCatalog.swift.
The form opens on the last set Dad used, and Spark keeps no preferences file for it. last_version_set finds the newest job in the corpus that is not a revision and rebuilds purpose, language, writer seats and reviewers from that job's turns. The route's docstring puts it in one line: read back from the newest job in the corpus, never kept anywhere else.
Anvil Quest shows where Spark's brain-seat picker came from, and Sibling Kit Quest shows how the catalog and KitBrainSeat reach every app as vendored files. These seats never touch Spark's practical or live slots (Track 4): high quality picks its brains per job.