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

Two Namespaces, Opposite Directions

~14 min · vocabulary, boundaries, data-integrity, design

Level 0Loose Parts
0 XP0/36 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete

The Same Pair, Twice, Backwards

Track 1 ended with a translation function: settings vocabulary in, routing vocabulary out, applied at the crossing and never stored back. That closed the bug that broke every app.

Later, the workshops needed to record which brain performed a piece of work. They needed one spelling so that provenance records are queryable, and the spelling they needed was the other one — the picker vocabulary, the one a person recognizes. So a second canonicalization exists, and it maps the routing name back to the picker name.

Two functions. Same pair of names. Opposite directions. And the family's rule about them is blunt: never merge them.

Why This Looks Exactly Like a Cleanup Opportunity

Imagine finding both for the first time. Two functions, both mapping between the same two strings, in the same family, both called canonical-something. Every instinct says this is duplication with a typo in it, and the fix is to pick a direction.

It is not duplication. They answer different questions:

  • Which endpoint should this request go to? The answer is in the routing vocabulary, because that is what the routes are named.
  • What should this record say the work was done by? The answer is in the picker vocabulary, because that is what a person reading the record will recognize, and it is what the settings owner publishes.

Merging them would not remove a function; it would change what one of them means. And because both directions have already written identifiers into stored records — one into request logs, the other into provenance rows — the merge would silently reinterpret history. That is data corruption arriving under the banner of a cleanup.

Two mappings between the same values are not necessarily duplicates — check what each one's OUTPUT is for before unifying. The signature is identical, the implementation is nearly identical, and the purposes are opposite. Anything already persisted under either convention makes the merge irreversible in the worst way: not a crash, but a fleet of records that quietly mean something else now.

How to Make the Distinction Survive

The defense is naming and location, not a comment. Each function is named after its destination vocabulary rather than after the generic word canonical — the routing one and the picker one are distinguishable at every call site without opening either. They live in different modules, matching the concern that owns each vocabulary. And each carries, in its own docstring, the sentence that they run in opposite directions on purpose and must never be merged.

That last part is the load-bearing one, because the person who eventually finds both will not be reading the architecture document. They will be reading one of the two functions, at the moment they think they have found a bug.

Code

Both directions, named for their destination·python
# Two vocabularies, both legitimate, both owned by different concerns:
#
#   PICKER  : what a person selects and what a record should say
#             claude | chatgpt | gemini | grok | ollama
#   ROUTING : what the endpoints are actually named
#             claude | codex   | gemini | grok | ollama


# --- direction 1: routing. Lives with the chat client. -------------
_TO_ROUTING = {"chatgpt": "codex", "gpt": "codex"}


def routing_brain(brain: str) -> str:
    """PICKER -> ROUTING. Applied at the moment a settings value
    crosses into a chat route, and NEVER written back to settings:
    the picker namespace belongs to the settings owner.

    Runs in the OPPOSITE direction to `picker_brain` ON PURPOSE.
    Never merge the two - see that function's note.
    """
    return _TO_ROUTING.get((brain or "").strip().lower(),
                           (brain or "").strip().lower())


# --- direction 2: provenance. Lives with the queue kernel. ---------
_TO_PICKER = {"codex": "chatgpt", "gpt": "chatgpt"}


def picker_brain(brain: str) -> str:
    """ROUTING -> PICKER. Used when RECORDING which brain did work,
    so provenance rows are queryable under the name a person knows.

    Runs in the OPPOSITE direction to `routing_brain` ON PURPOSE.
    MERGING THEM IS A DATA MIGRATION, NOT A REFACTOR: rows already
    written under each convention would silently change meaning.
    """
    return _TO_PICKER.get((brain or "").strip().lower(),
                          (brain or "").strip().lower())


# The property that documents the relationship, and the one to keep
# in a test so nobody "simplifies" one of them into the other:
def test_the_two_directions_are_inverses_not_duplicates():
    assert routing_brain("chatgpt") == "codex"
    assert picker_brain("codex") == "chatgpt"
    assert routing_brain(picker_brain("codex")) == "codex"
    # and neither is the identity, which is what a merge would make
    # one of them become:
    assert routing_brain("chatgpt") != "chatgpt"
    assert picker_brain("codex") != "codex"

External links

Exercise

Search your codebase for two functions that map between the same pair of value sets. For each, write one sentence naming what its output is FOR. If the sentences differ, rename both after their destinations and add the do-not-merge note to each. If the sentences are the same, you have found a genuine duplicate — delete one.
Hint
Persistence is the tiebreaker. If either mapping's output has ever been written to storage, a merge is a migration regardless of how clean the code looks afterward, and the cost is not in the code — it is in every row already written.

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.