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

Interfaces Are Roles, Dishes Bind Them, Variants Override

~15 min · interfaces, overriding, pydantic, domain-model

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

One slot, three shapes

The concept table says a principle has an interface of ingredient roles, a dish binds them, and a variant overrides them. In the record those are three small JSON shapes, and one key, role, runs through all of them.

LevelColumnOne entryExample
Principleroles{role, examples}향채 (aromatic), with garlic and scallion as examples
Dishbindings{role, value, confidence}기름 (fat) bound to olive oil, high confidence
Variantoverrides{role, from, to, why}기름: olive oil to perilla oil, for its nutty aroma

The role name is the thread. A principle says it needs an aromatic, a fat and a main ingredient. Gambas al ajillo answers with garlic, olive oil and prawns. A home version answers again for one role only: the fat, from olive oil to perilla oil, with a reason. The doc writes that kind of lineage as "Tapas-bar gambas → Perilla-oil gambas (fat: olive oil → perilla oil, because …)". Read it as a diff against the parent. One slot, the old value, the new value, and why.

Who writes each shape

Roles and bindings come from readings. When I read a plate, the rubric asks me for each dish's bindings and each principle's roles, with short Korean role names such as 향채 (aromatic), 기름 (fat), 주재료 (main ingredient) and 열 (heat). That follows Dad's naming ruling: principle names and explanations in Korean, dish names in their own language with a Korean gloss, the app's chrome in English. Pydantic models in backend/reading.py give each shape its fields and a default for every one, so a missing key arrives as an empty string or a medium confidence instead of an error. Track 3 covers how that parse forgives and normalises.

Overrides come only from Dad. There are two doors. On the web, a dish or variant page has a Make my version form. In a conversation on either client, I can propose a variant action, and it becomes a class only when Dad applies it. Both doors end in the same method, derive_variant in backend/domain.py.

The rules a variant has to pass

  • It derives from a dish or another variant, never from a principle. An abstract class is not overridden directly. You override a concrete dish, which already binds the roles.
  • Every override names a role and a new value. An entry missing either is dropped. from and why are optional, and each field is trimmed to a length cap.
  • At least one override must survive, or the variant is refused. A version that changes nothing is the same dish.
  • It is born adopted, with origin dad, takes its parent's cuisine, and starts with exactly one parent.
  • If a plate is named, the plate is tied to it, so a photographed home version starts with evidence.

The route adds its own bounds before any of that runs. VariantIn in backend/app.py takes a name of 1 to 200 characters and 1 to 12 overrides, each with a role and a target value.

The form starts from the parent's answer

On the web, choosing a role in the Make my version form fills from with the value the dish binds to that role, and the role field suggests the dish's own binding roles. You begin with the parent's answer and change it, which is what an override is. For tying, the form offers only this dish's plates that were cooked at home, because a version you made is evidence from your own stove. The phone has no such form; there a variant arrives only as an action applied from a conversation.

An interface made of names

Here the analogy stops being exact, and it is worth seeing where. In a typed language an interface is checked: a class that forgets a method does not compile. Sizzle's interface is a vocabulary. Nothing checks that a dish binds every role its principle lists, or that an override's role appears among the parent's bindings. The form suggests roles and requires none. A role is a short Korean name, and two readings can name the same slot a little differently.

For a record grown from photos and conversation, that is a fair trade. A strict checker would refuse a reading over spelling and lose the plate's knowledge with it. The cost is that consistent role names are a habit, on my side when I read and on Dad's when he names an override. If you want the vocabulary itself, interface, overriding and the rest, OO Quest Track 7, The Complete Toolkit lays it out; this lesson only shows where each word landed.

Code

cwkSizzle, backend/reading.py: the shapes a reading returns for bindings, roles and principles (verbatim, lines 96-114)·python
class Binding(BaseModel):
    role: str = ""
    value: str = ""
    confidence: str = "medium"


class Role(BaseModel):
    role: str = ""
    examples: list[str] = Field(default_factory=list)


class PrincipleReading(BaseModel):
    ref: str = ""
    name: str = ""
    summary: str = ""
    why: str = ""
    roles: list[Role] = Field(default_factory=list)
    method: list[str] = Field(default_factory=list)
    confidence: str = "medium"
cwkSizzle, backend/domain.py: deriving a variant, the only way an override enters the record (verbatim, lines 197-216)·python
    def derive_variant(self, parent_id: str, *, name: str, overrides: list[dict[str, Any]], note: str = "",
                       source_encounter: str | None = None) -> dict[str, Any]:
        """Dad's home version: a subclass of a dish with named overrides."""
        parent = self._require_class(parent_id)
        if parent["kind"] == "principle":
            raise DomainError("a variant derives from a dish or another variant")
        clean = []
        for item in overrides or []:
            role, to = _text(item.get("role"), 80), _text(item.get("to"), 200)
            if role and to:
                clean.append({"role": role, "from": _text(item.get("from"), 200), "to": to,
                              "why": _text(item.get("why"), 500)})
        if not clean:
            raise DomainError("a variant needs at least one override")
        class_id = self.propose_class(kind="variant", name=name, parents=[parent_id], origin="dad",
                                      status="adopted", overrides=clean, summary=note,
                                      cuisine=parent.get("cuisine"), source_encounter=source_encounter)
        if source_encounter:
            self.link(source_encounter, class_id)
        return self.store.klass(class_id)
Illustration: one role through the three levels (hand-written for this lesson, in the shapes the engine stores)·json
{
  "principle": {
    "name": "향을 기름에 먼저 옮기고 그 기름에서 익히기",
    "roles": [
      {"role": "향채", "examples": ["마늘", "파"]},
      {"role": "기름", "examples": ["올리브유", "참기름"]},
      {"role": "주재료", "examples": ["새우", "버섯"]}
    ]
  },
  "dish": {
    "name": "Gambas al ajillo",
    "bindings": [
      {"role": "향채", "value": "마늘", "confidence": "high"},
      {"role": "기름", "value": "올리브유", "confidence": "high"},
      {"role": "주재료", "value": "새우", "confidence": "high"}
    ]
  },
  "variant": {
    "name": "Perilla-oil gambas",
    "overrides": [
      {"role": "기름", "from": "올리브유", "to": "들기름", "why": "고소한 향"}
    ]
  }
}

External links

Exercise

Pick a family of things in software you build that share a shape, such as export formats, payment providers or notification channels. Write the interface as a list of roles with examples, one concrete member as bindings of those roles, and one variant as overrides with role, from, to and why. Then write the rules your derive step enforces: what it may derive from, what an override must carry, and the smallest valid change.
Hint
Decide on purpose whether a role name must match the parent's exactly. If it must, validate it at the derive step and say so in the error. If it need not, keep the role list visible wherever an override is written, the way Sizzle's form suggests the dish's own roles.

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.