Skip to content
C.W.K.
Lesson 01 of 06 · published

One Reading Grows the Tree

~15 min · integrate, class-tree, proposals, python

Level 0Mise en Place
0 XP0/43 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete

Where integrate sits

By the time integrate runs, the hard part is over. Pippa has looked at the plate and answered in one JSON object, and the engine has validated that object against the rubric's Pydantic models. The worker records reading.recorded with the whole body, then hands the same parsed PlateReading to integrate (backend/reading.py, lines 383-389). A reply that fails validation never reaches this function, so the tree does not move.

That order is the first thing worth copying. Pippa's words are stored as a reading before any of them become structure. If integrate ever did something odd, the reading it worked from would still be there to inspect.

The model proposes, plain code decides

The tree is never handed to the model to edit. Pippa gets Dad's tree as data in her input (dads_tree, a compact catalog of principle and dish ids and names), and the rubric asks her to reuse those ids when something truly matches. She returns names, ids and descriptions. Everything that touches the record after that is ordinary deterministic Python. No model call happens inside integrate.

For each dish the reading names, the function does four things in a fixed order:

  1. Resolve the principles. At most two per dish (dish.principles[:2]), which matches the rubric's "one root principle per dish, two at most". Each is looked up first by the ref id Pippa gave, accepted only if that id really is a principle, then by name. A known principle is reused. An unknown one is proposed as a new class. A principle with neither a usable id nor a name is skipped. One whose ref resolves needs no name, because the id is accepted before the name is ever checked.
  2. Resolve the dish. By dish_ref (a dish or variant id), then by original name or Korean gloss. If nothing matches, a new dish is proposed with the resolved principles as its parents.
  3. Parent it. ensure_parent writes a class.parented event for each principle edge that does not exist yet, so a known dish can gain a parent it did not have.
  4. Link the plate. One class.linked event ties the encounter to the dish, stamped with source: "reading", the reading's id and Pippa's confidence for that dish.

Then come the siblings, which get the next lesson to themselves.

Every write is an event

Notice what integrate never does: it never writes SQL. propose_class, ensure_parent and the link all go through sizzle.record, which appends an event to the journal and then applies it to the SQLite mirror. Track 5 is about that spine. For this track the consequence is enough. The tree is built from the class events in order: class.proposed, class.parented and class.linked from this function, class.revised when a proposal's words are refreshed or Dad edits a field, and class.adopted and class.renamed from Dad. Those alone are not enough, though. Which links count, and what evidence a class shows, depend on the plates and their readings: the current-links view keeps a reading's link only while that reading is the plate's current one, and evidence reads each plate's kind. So it is the whole journal, replayed in order, that rebuilds the tree exactly.

propose_class also refuses to invent structure. It drops any parent id the mirror does not know, trims every text field, and writes only the fields that carry a value. Every class integrate creates starts as status: "proposed", with an origin saying who started it: reading for a principle or dish named on the plate, sibling for an inferred sibling. The third origin, dad, never comes through here. A variant Dad derives is recorded by derive_variant with origin dad and status: "adopted" from its first event, because it is his from the start.

What one plate of gambas grows

Take the design's own generic example. A tapas-bar plate of gambas al ajillo arrives while the tree is still empty. The reading names one dish and one principle, roughly "move aroma into fat first, then cook in that fat". After integrate the journal holds:

EventWhat it adds
class.proposed (principle)The abstract class, with its summary, why, roles and method skeleton
class.proposed (dish)Gambas al ajillo with its Korean gloss, cuisine and role bindings, already parented to the principle
class.linkedThis plate tied to the dish, as the reading's claim
class.proposed, once per siblingAglio e olio, scallion-oil fried rice and the rest, all inferred

A second plate of the same dish adds one more class.linked. If the dish is still a proposal and Pippa described it differently this time, it also adds a class.revised. No second gambas class appears. Lesson 3 shows why.

A dish whose reading names no principle at all is still proposed, with no parents. The Tree page lists it under "Not yet under a principle" until a later reading gives it one.

Principle: Let the model name things. Let code decide what those names do to the record. That boundary is the place you can test.

The only way in is through a plate

Sizzle ships with no taxonomy. Invariant 6 of its architecture says the tree grows and is never imported. Every principle in the record exists because some plate made Pippa name it, and every one of them stays a proposal until Dad adopts it, renames it or ties a plate to it.

The oo-quest's Track 1 (The Straw) argues that understanding is inheritance: you grab something you already know and climb from there. integrate behaves the same way with each plate. It reaches for the classes already in the tree before it proposes a new one, and a new class arrives already hanging from whatever it could reuse.

Code

cwkSizzle, backend/reading.py: integrate, first half: principles, dish, parents, link (verbatim, lines 208-223)·python
def integrate(sizzle: Sizzle, encounter_id: str, reading_id: str, reading: PlateReading) -> None:
    """Grow the tree from one reading: resolve or propose the principles and
    dishes it names, parent them, link the plate, and add inferred siblings."""
    principle_by_name: dict[str, str] = {}
    for dish in reading.dishes:
        principle_ids: list[str] = []
        for p in dish.principles[:2]:
            pid = _resolve_principle(sizzle, p, encounter_id)
            if pid:
                principle_ids.append(pid)
                principle_by_name[_key(p.name)] = pid
        dish_id = _resolve_dish(sizzle, dish, encounter_id, principle_ids)
        for pid in principle_ids:
            sizzle.ensure_parent(dish_id, pid)
        sizzle.record("class.linked", {"encounter_id": encounter_id, "class_id": dish_id, "source": "reading",
                                       "reading_id": reading_id, "confidence": dish.confidence})
cwkSizzle, backend/domain.py: propose_class drops unknown parents; ensure_parent writes only a new edge (verbatim, lines 147-169)·python
    def propose_class(self, *, kind: str, name: str, parents: list[str] | None = None, origin: str = "reading",
                      status: str = "proposed", source_encounter: str | None = None, **fields: Any) -> str:
        if kind not in CLASS_KINDS:
            raise DomainError("unknown class kind")
        name = _text(name, 200)
        if not name:
            raise DomainError("a class needs a name")
        class_id = new_ulid()
        data = {"id": class_id, "kind": kind, "name": name, "origin": origin, "status": status,
                "source_encounter": source_encounter,
                "parents": [p for p in (parents or []) if self.store.klass(p) is not None]}
        for key in ("gloss", "cuisine", "summary", "why", "parameters", "verdict"):
            if fields.get(key):
                data[key] = _text(fields[key])
        for key in ("roles", "method", "bindings", "overrides"):
            if fields.get(key):
                data[key] = fields[key]
        self.record("class.proposed", data)
        return class_id

    def ensure_parent(self, child: str, parent: str) -> None:
        if child != parent and not self.store.has_parent(child, parent):
            self.record("class.parented", {"child": child, "parent": parent})

External links

Exercise

Pick a feature in your own software where a model returns structured output that should change your data: tags for a note, a category for an expense, a company for a contact. Write its integrate step as a function that takes the validated output and your current catalog and returns a list of events. Reuse by id first, then by name, and propose a new entity only when neither matches. Then write a test that feeds the same output in twice and asserts that the second run proposes nothing new.
Hint
Send the catalog to the model with ids and ask it to echo an id when it reuses something. Make your propose step drop any parent id you do not know, the way propose_class does, so a hallucinated id becomes a missing edge instead of a broken one.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign in — Please sign in to comment.

No comments yet — be the first.