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

A Topic Addresses a Set

~13 min · contracts, generalization, api-design, evolution

Level 0Cold Workshop
0 XP0/43 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete

The Axis Nobody Wrote Down

Earlier in this track, the typed pointer was defended as the cheap half to generalize: a kind plus an identifier, so that a future pipeline could point at a different kind of material without renegotiating the seam. That bet has been tested twice. It passed the first test without anyone noticing. The second test found an assumption the shape had been carrying all along without saying so.

The first test came before this quest was even published. A second pipeline turns a published essay into a narrated reading, and it points at site content. A new kind, named in the kind field and carrying fields of its own, with nothing on the calling side changed — exactly what the field was for.

The second test arrived on 2026-08-27 with a third production character: one topic, explained. Its input is shaped like a research notebook. You might hand it a conversation, a web page, an uploaded document, a note in your own words, several of those at once, or nothing but the topic itself. And {kind, id} cannot say that, because the shape had quietly encoded two facts about its target: that it is one thing, and that it already exists somewhere to be fetched. A topic is neither. A topic addresses a set, and the set may be empty.

What Changed, and How Little

The contract grew one optional field. A pointer may carry sources, a list in which every entry is typed — conversation, url, file, or note — and the registry of source kinds is the gate an unknown entry fails at. Two flags on each pipeline decide who may use it: whether it accepts sources, and whether its pointer may leave the identifier blank. Both default to off. The conversation pipeline behaves byte for byte as before, and its tests assert that it refuses a source list outright, because a pipeline whose whole axis is one conversation must not quietly grow a second source of material.

Notice where the change did not go. The request still carries no material: a source set is a set of references, dereferenced into the working directory at run time like any other pointer. Uploaded files land in the engine's store addressed by their content hash, and a session on a laptop reads them through the engine exactly as it reads a conversation, never from a stale local copy. And the workshop still parses nothing — no document-conversion dependency arrived. The pipeline puts the bytes where the authoring session can open them, and the session opens them itself.

A Gate That Needs an Artifact, Not an Exit Code

Because nothing parses the sources, nothing can claim they were read. So this pipeline's first gate after the brief, before any script exists, is a sources stage whose record has to be evidence that every attachment was actually opened — not a parser's success code. The same pipeline treats every source as material and never as a draft. A conversation about the topic is read for its order, its keywords, and the angles its participants engaged with, and deliberately not for its depth or its sentences. The script is written from zero.

Two small invariants fell out of building it, and both exist to keep an existing guarantee from eroding. Uploads travel as a raw body with the filename in a header rather than as a multipart form, because the command-line client is standard-library only on purpose, and a hand-rolled multipart encoder is how that promise would have ended. And the compose screen reads the list of pipelines and their flags from the engine instead of keeping its own copy, because a hardcoded copy of a gate drifts from the gate — and the drift arrives as a rejection nobody at the keyboard can explain.

A contract's shape carries assumptions its fields never name. Cardinality: one thing, or many. Existence: already stored somewhere, or not yet. Ownership: held by one system, or several. When you pre-generalize a contract, write those down beside it. Each is an axis the next use case can arrive on, and naming it in advance is what turns a renegotiation into one optional field.

Code

Three pointer kinds, one kind field·json
// conversation pipeline: one thing that already exists
{ "pipeline": "pippalog-episode",
  "pointer": { "kind": "conversation", "id": "<opaque-identifier>" } }

// reading pipeline (command-line only): a different kind, its own fields
{ "pipeline": "prose-reading",
  "pointer": { "kind": "site-content", "path": "<content path>", "locale": "ko" } }

// explainer pipeline: a topic addresses a set, possibly empty
{ "pipeline": "pippa-explains-episode",
  "pointer": {
    "kind": "topic",
    "id": "",
    "sources": [
      { "kind": "conversation", "id": "<opaque-identifier>" },
      { "kind": "url",  "url": "https://example.org/some-article" },
      { "kind": "file", "sha256": "<content hash of an upload>" },
      { "kind": "note", "text": "start from what a cache even is" }
    ]
  } }

// a blank id means the engine names the topic from the job's slug.
Two flags, both off unless a pipeline opts in·python
SOURCE_KINDS = {            # the gate: an unregistered kind is refused
    "conversation": "id",   # served by the engine's material route
    "url": "url",           # snapshotted into the working directory
    "file": "sha256",       # an upload, addressed by its content hash
    "note": "text",         # plain words, inline
}

PIPELINES = {
    "pippalog-episode": {
        "pointer_kind": "conversation",
        # accepts_sources / pointer_id_optional absent -> False
    },
    "pippa-explains-episode": {
        "pointer_kind": "topic",
        "pointer_id_optional": True,   # a topic may be only a sentence
        "accepts_sources": True,       # zero or more, each typed
    },
}


def validate(pipeline: str, pointer: dict) -> None:
    spec = PIPELINES[pipeline]
    if pointer.get("sources") and not spec.get("accepts_sources"):
        raise ValueError(
            f"{pipeline} takes one {spec['pointer_kind']}, not a source set")
    for src in pointer.get("sources", []):
        field = SOURCE_KINDS.get(src.get("kind"))
        if field is None or not src.get(field):
            raise ValueError(f"unregistered or incomplete source: {src!r}")

External links

Exercise

Take a request or event shape in a system you maintain, and for its main reference field write down the three assumptions this lesson names: one or many, already existing or not yet, held by one system or several. Then invent the most plausible next use case that breaks one of them. Design the smallest contract change that admits it, with a default that keeps every existing consumer's behavior identical — and write the test that proves an old consumer refuses the new shape.
Hint
The refusal test is the part people skip, because the change feels purely additive. But an optional field that old consumers silently accept and ignore is a field that someone will eventually fill in for them, assuming it meant something.

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 inPlease sign in to comment.

No comments yet — be the first.