Where the edges come from
When Dad taps a line and asks what it means, I name the roots of the words that carry it, where they are real: a Latin or Greek etymon, an Arabic loanword in Spanish, a hanja shared by Korean, Japanese and Chinese. Each one is recorded as a root result with four fields worth knowing: the root (lat. aqua), its language (la), a gloss (water), and the expression it lies under (agua). Track 3 showed how those lines are parsed. What matters here is that the engine keys both texts with the same fold the keep star uses, so every root result carries a root_key and an expression_key.
That makes each root result an edge in a graph: from a class to one thing that descends from it. This lesson is what Spark builds out of those edges, and which part of the design is still on paper.
The roots list: most connected first
The first code block is the Roots tab. It reads every root result on a moment that is not deleted, groups them by root_key, and counts the distinct expressions under each. The root text and language come from the first time the root was named; the gloss is the first non-empty one. Then it sorts by the number of expressions, most first, and alphabetically after that.
The order is the point. A root with one descendant is trivia. A root with six is a key that opens six doors, and it should sit at the top of the list Dad scrolls. On the phone each row shows the root, then its gloss, language and count, such as water · la · 3 expressions.
A root page
Tap a root and the second code block runs. It collects every root result for that key in log order and keeps one entry per expression. Each entry carries two things: met, every result whose own source_key equals the expression's key, with the capture truth of its moment; and named_in, the moment where I first named the edge.
The two can disagree, and the page handles it honestly. While explaining agua I might add that acuático comes from the same root. Dad has never met acuático on a sign or a card, so its met list is empty. The edge is still knowledge he received, so the phone shows the expression with a single row, Named while explaining a moment, which opens that moment. Everything he did meet shows as a row with the moment's line, the source and its translation.
The edges run both ways. An expression page lists the roots behind the phrase, so Dad can climb from a word to its class and back down to every sibling. Both directions are queries over the same rows, with no second table to keep in step.
The OOP frame, made of rows
Track 1 put Dad's lens in one line: moments are instances, roots are classes. A root page is that frame drawn from data. The root is the class. The expressions under it are what inherits from it, and they belong to whatever languages they belong to: Spanish agua, Portuguese água and Italian acqua all land on the lat. aqua page if I named that root while explaining each of them, whichever trip they came from. The hanja 學 gathers Korean 학교 and the 学校 of Japanese and Chinese the same way. Each meeting is an instance: one sign, one card, one sentence at one table.
Notice what the schema does not have. There is no class table and no language column on a page. A root exists because a result names it, and its page is a query. Delete a moment and its edges leave every page; restore it and they return, because every read joins on deleted = 0. If this way of seeing is new to you, OO Quest builds it from the ground up.
Identity is the spelling
There is an honest limit here, and it is worth seeing before you copy the design. A root's identity is the folded text I wrote. lat. aqua and Latin aqua would be two pages. Nothing in the engine knows they are the same etymon, and nothing merges them. The same holds for expressions: agua and el agua have different keys.
What keeps the graph together is the prompt. It shows me one format by example, the root with a short language prefix, a language code and an English gloss, and I follow it. When the identity of a record is free text produced by a model, consistency is a convention you teach by example. Keep the key function simple and visible, so you can see the drift when it happens.
Not yet built: a thread per root
The architecture goes further than the code, and Dad's ruling is that this part is taught as design. It gives every material a home conversation in cwkPippa: a moment its own thread, a trip spark:trip:<id>, and a root or an expression a thread of its own, spark:root:<id> and spark:expr:<id>. In the code, only moment threads exist. The engine opens a thread in exactly one place, for a moment, and the web docks the Sidekick only on a moment.
The third code block shows why a root thread cannot simply be switched on. cwkPippa's binder accepts the four kinds, but every reference must be a 26-character ULID, because Spark mints every id. A root's key is text like lat. aqua, which no ULID pattern matches. Building root threads needs a decision first: mint an id for each root, or teach the binder to take a text key. Neither exists yet. Today, when Dad wants to talk about a root, he asks in a moment's thread or in any Pippa conversation, and I read the root page through the Spark reader that lesson 4 describes.