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

Proposal Versus Adoption: Who Owns the Words

~13 min · adoption, ownership, proposals, refresh

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

Two owners for one row

Every class in Sizzle has a status of proposed or adopted. A proposed class is Pippa's current understanding of something. An adopted class is Dad's. The difference decides one question that comes up on every reading: when Pippa describes a known class again, in different words, whose words stay?

The answer lives in a nine-line function called _refresh, and its docstring says the whole rule. A proposal is Pippa's current understanding, so a newer reading rewrites its description, a new rubric's voice included. An adopted class is Dad's and keeps its words.

What a newer reading may rewrite

During integrate, _refresh is called from three places, each with its own list of fields:

WhereFields refreshed while the class is proposed
A principle the reading resolvedsummary, why, roles, method
A dish the reading found on the plategloss, cuisine, summary (Pippa's evidence from this plate), bindings
A sibling mentioned again, only if it began as a siblinggloss, cuisine, summary (the new why_same)

Three details make the rule safe. An empty value never overwrites a field, because the filter keeps only values that are present and different. A variant is never refreshed at all: _resolve_dish only refreshes a class whose kind is dish, and a variant is born adopted anyway. And name is in none of the lists. Even while a class is still Pippa's proposal, a reading cannot rename it. A class keeps the name it was born with until Dad renames it.

Every change goes through revise_class, which records a class.revised event. The journal therefore keeps every version of a proposal's description, in order, even though the class page shows only the newest.

Three doors into Dad's tree

A proposal becomes Dad's through exactly three doors, all of them his:

  1. Adopt. The "Adopt into my tree" button records class.adopted.
  2. Rename. class.renamed sets the new name and the status adopted in the same update. Naming a thing is taking it.
  3. Tie a plate. When Dad links a plate to a class himself, link records the link with source: "dad" and then class.adopted if the class was not adopted yet.

A variant skips the doors because Dad derived it, so it is born adopted with origin: "dad". In a conversation, Pippa can suggest any of the three as a typed action, and nothing happens until Dad presses the button (Track 6 covers that boundary). What never adopts anything is a reading. Its own link carries source: "reading" and changes no status, so a sibling Dad has eaten ten times is still a proposal until he says otherwise. Evidence and adoption are separate axes.

When the voice itself changed

The rule earned its docstring on Sizzle's first day. After the first real plate, Dad read the analysis and said:

plate에 나오는 분석은 피파 말투가 아니야. 이상해.

The analysis on the plate isn't in Pippa's voice. It's strange.

Rubric v2 told the reading to speak in Pippa's own 반말, and the bump re-read every plate. Through _refresh, each re-read rewrote in the new voice the description of every still-proposed class it named. Any class Dad had adopted would have kept the words he adopted. Nobody had to write a migration for the tree. The words followed the rubric exactly as far as Pippa owned them.

The engine test pins both halves. It re-reads a plate with a newer why for the principle and sees the proposal take it. Then it adopts the principle, re-reads with yet another why, and sees the adopted words survive.

Principle: Give every generated record an owner. Let the generator refresh what it still owns, and let one human act end that ownership for good.

One edit box away from a gap

The class route, PATCH /api/classes/{class_id}, accepts description fields such as summary and why as well as verdict and parameters, and it does not adopt. Both clients only ever send verdict and parameters, and no reading touches either of those, so nothing is lost today. If you add an edit box for a proposal's summary, though, the next reading that names that class would overwrite Dad's edit. In your own version, make a human edit to a refreshable field adopt the record, or move that field out of the refresh list.

The oo-quest's Track 9 is about a mold that hardens into a cage. Proposals in Sizzle stay soft on purpose: Pippa keeps reshaping them with each plate, and nothing sets until Dad's hand does it.

Code

cwkSizzle, backend/reading.py: _refresh, the ownership rule (verbatim, lines 246-254)·python
def _refresh(sizzle: Sizzle, known: dict, fields: dict) -> None:
    """A proposal is Pippa's current understanding, so a newer reading
    rewrites its description (a new rubric's voice included). An adopted
    class is Dad's and keeps its words."""
    if known["status"] != "proposed":
        return
    changes = {key: value for key, value in fields.items() if value and value != known.get(key)}
    if changes:
        sizzle.revise_class(known["id"], changes)
cwkSizzle, backend/store.py: adopting and renaming both set the status (verbatim, lines 275-281)·python
    def _on_class_adopted(self, d: dict[str, Any], at: str) -> None:
        self.conn.execute("UPDATE classes SET status = 'adopted', updated_at = ? WHERE id = ?", (at, d["id"]))

    def _on_class_renamed(self, d: dict[str, Any], at: str) -> None:
        self.conn.execute(
            "UPDATE classes SET name = ?, status = 'adopted', updated_at = ? WHERE id = ?", (d["name"], at, d["id"])
        )
cwkSizzle, backend/tests/test_rubric.py: a proposal takes the newest words until Dad adopts it (verbatim, lines 35-56)·python
def test_a_proposal_takes_the_newest_words_until_dad_adopts_it(make_client, fake_pippa):
    client = make_client()
    encounter_id = capture(client, "req-refresh-1")["encounter"]["id"]
    _done(client, encounter_id)
    principle_id = client.get("/api/tree").json()["principles"][0]["id"]

    newer = copy.deepcopy(GAMBAS)
    newer["dishes"][0]["principles"][0]["ref"] = principle_id
    newer["dishes"][0]["principles"][0]["why"] = "마늘 향은 기름에 녹거든. 그래서 기름부터 데우는 거야."
    fake_pippa.reading = newer
    client.post(f"/api/encounters/{encounter_id}/reread")
    _done(client, encounter_id)
    assert client.get(f"/api/classes/{principle_id}").json()["class"]["why"] == newer["dishes"][0]["principles"][0]["why"]

    client.post(f"/api/classes/{principle_id}/adopt")
    adopted_words = newer["dishes"][0]["principles"][0]["why"]
    later = copy.deepcopy(newer)
    later["dishes"][0]["principles"][0]["why"] = "a later reading that must not overwrite Dad's class"
    fake_pippa.reading = later
    client.post(f"/api/encounters/{encounter_id}/reread")
    _done(client, encounter_id)
    assert client.get(f"/api/classes/{principle_id}").json()["class"]["why"] == adopted_words

External links

Exercise

Take any record your software generates with a model: a summary, a suggested title, an auto-filled description. Give it an owner field with two values, generated and accepted. Write its refresh function with an explicit allowlist of fields a newer generation may rewrite, and leave the identity field off that list. Then write three tests: a newer generation rewrites a generated record, an accepted record keeps its words, and an empty generated value never blanks an existing field.
Hint
Copy the two guards in _refresh: return early unless the record is still generated, and keep only values that are non-empty and different. Then decide what a human edit to a refreshable field does. Either it accepts the record, or that field leaves the allowlist.

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.