Hello Curious Coders,
The context layer came back fast, and it came back clean.
Claude read the plan, built the Dictionaries and Games contexts, wrote the fixtures, ran the tests, and showed the trace through project_eval. The error handling had the shape we asked for. The functions had names we would have chosen ourselves. Nothing in the file looked wrong.
That is exactly where review matters.
When generated code looks idiomatic, follows your conventions, and passes the first checks, the remaining problems are usually not syntax problems. They are requirement problems.
So Bruce slows down and reads with a lens.
Two Layers, Two Kinds of Code
The lens is the split this module has been building toward: functional core and boundary.
The functional core is where you embrace certainty. It is CRC: constructors that produce a data type, reducers that do one unit of work, and converters that show the result to whoever consumes it. In the Wordle core, that means new creates a game, play applies a move, and render produces the shape the interface needs.
Because that layer is predictable, it can live in pipes. Data goes in, data comes out, and each step does one small thing.
The boundary is where certainty runs out. Databases, user input, and process machinery can fail for reasons your function did not choose, so errors become data. Success comes back as {:ok, value}. Failure comes back as {:error, reason}, and when the failure needs more information, the tuple carries it.
That is why boundary code often uses with instead of a pipe: keep moving while each operation returns the expected success shape, and stop as soon as one does not.
Bruce describes this as the difference between the happy pipe world and the more careful with world. Necessary complexity belongs at the boundary. The review question is whether it stayed there.
π― Join Groxio's Newsletter
Weekly lessons on Elixir, system design, and AI-assisted development β plus stories from our training and mentoring sessions.
We respect your privacy. No spam, unsubscribe anytime.
Start With the Public API
When Bruce opens the Dictionaries context, he does not begin by studying every private helper. He starts with the public API and the @docs, because exported functions are the surface the rest of the application depends on.
get_by_theme returns tagged tuples, which gives callers a clear shape to match. validate has a slightly different contract: success returns :ok, because a valid word has nothing else to say, while failure returns {:error, reason}.
The composition is the shape we wanted:
def validate(word, dictionary) do
with {:ok, normalized} <- normalize(word),
{:ok, _entry} <- fetch_word(normalized, dictionary) do
:ok
else
{:error, reason} -> {:error, reason}
end
end
That is boundary code doing boundary work. It normalizes, checks, and lets failures travel as data.
There are small style quibbles. A query could pipe into Repo.one. Some filter clauses could become function heads instead of living inside one anonymous function. Those are the kind of notes you can leave without changing the direction of the work.
Then the review moves to the game, and the problem changes.
The Requirement It Missed
Wordle.new takes a single dictionary_id.
That sounds reasonable until you read it against the actual game. In Wordle, guesses and answers do not come from the same list. A guessed word must be checked against a broad dictionary of valid words. The answer must come from a narrower list of acceptable solution words.
Here is the shape the generated API implied:
Wordle.new(dictionary_id)
One dictionary handles both jobs.
The domain needs a different promise:
Wordle.new(answer_dictionary_id, guess_dictionary_id)
Or maybe the design should use one dictionary with an answer_eligible flag. That decision can wait. The requirement cannot.
A four-letter word with an s stuck on the end may be a valid guess, but it should not become the secret answer. Collapse those lists into one dictionary_id, and the game either rejects real guesses or chooses answers it should never choose.
So Bruce writes the missing rule down immediately:
WORDLE: answers must be validated from a different dictionary than a guess.
The lesson is not that Claude wrote bad Elixir. It did the opposite. Tidewave gave it live project knowledge. The CRC-builder skill gave it the shape of the core. The context instructions gave it tagged tuples and with at the boundary.
The form was right. The requirement was wrong.
βClaude did a fantastic job giving me idiomatic Elixir wrapped in our opinions, but it missed a major requirement in the Wordle piece. The only way to catch this is to slow the tempo down and provide our review as we go.β
- Bruce Tate
Tempo Is the Technique
The practical move is to review smaller chunks, and to read the public contract before the private implementation.
After the agent builds a piece, ask one question of every exported function: what does this promise, and does my domain agree?
That question catches mistakes style review will miss. Wordle.new(dictionary_id) is not ugly code. It is a wrong promise. Once that promise spreads into fixtures, tests, contexts, and UI state, the cost goes up.
So keep the rhythm deliberate: explore the existing code, plan the next slice, let the agent build it, verify it, then stop and read. Tidewave lets the model handle larger chunks than before because it can inspect the running application, but larger chunks are still chunks.
Private helper style can wait. A wrong public promise cannot.
Where This Leaves Us
AI-assisted Elixir gets much better when the model can see your application and follow your patterns. That changes the kind of review you need to do.
You stop spending all your attention on whether the code looks like Elixir and start asking whether the code tells the truth about the domain.
The next step is checkpointing: Git commits that mean something, a practice for throwing away work that does not fit the plan, and the user interface, where the model will give us more useful work and more to verify.
βYouβre in charge, not the agent.β
- Bruce Tate
π€ Review AI Work With Structured Oversight
This comes from Bruce's AI Agents course β the anti-vibe-coding curriculum. Learn to slow the tempo, review public promises, and use the Ask β Plan β Agent framework to catch missed requirements without losing architecture decisions or letting AI become a crutch. Available via monthly subscription β try it for one month.
β Paulo & Bruce