Software engineer — backend & product

Bernard Odon

Brazil · RemoteGitHubLinkedIn
Runtime · api: prod-apps-01
ytlearndevelopment
not public yetcase study →
correndodevelopment
not public yetcase study →
foto3x4facildevelopment
not public yetcase study →
Probed every 30sFull status →
← All projects

Diretas

A Go crossword generator with one non-negotiable invariant: 150 filled cells, proven by a solver before the API responds.

Go · solver · Next.js · 2026

Go API, solver · Next.js editor · 2026

Diretas generates Diretas-style crossword pages — the Portuguese-language kind where a clue sits inside the grid itself instead of in a side list — and manages the bank of clue → answer pairs behind them. The interface is deliberately small: one button to generate a page, one screen to manage the answer bank. The engineering is in what the API refuses to return.

Problem

A generated crossword page is only useful if it's actually solvable and complete. A page with an empty cell, an orphaned letter, or a clue missing its associated answer isn't a draft that needs polish — it's broken, and handing it to an editor to catch by hand defeats the point of generating it at all.

Constraints

  • Exactly 150 used cells, a fixed 10×15 layout. Not "as full as the bank allows" — exactly full, every time.
  • The answer bank is finite and changes over time as pairs are added or reworked; the generator has to work with whatever bank exists right now, not an idealized one.
  • The active product surface is two screens. An earlier model — magazines, reserved answers, drafts, approval — still exists in legacy data, but the current path doesn't use it (see the project's own ADR-0005).

Mechanism

The generation endpoint doesn't return a best-effort page. It runs generation, then hands the candidate to a solver; if the solver can't prove the grid is fully and validly filled from the current bank, the API does not respond with a page at all. Composition and verification are separate steps, and the second one is a hard gate on the first.

Decisions

Measure bank supply by answer length during construction

Chose
Track how much of the bank's supply, broken down by answer length, is still available as the grid is being built.
Rejected
Building the grid first and checking supply only at the end.
Cost
The construction step has to carry more state as it goes, instead of staying a simple fill-and-check loop.

A mutation only survives if the solver still completes the page

Chose
Any change that adds or reorients answers is only kept once the solver confirms the bank still fills the whole grid.
Rejected
Accepting bank edits immediately and dealing with unsolvable states later.
Cost
Editing the bank is slower — every meaningful edit re-runs a solve, not just a write.

Among complete solutions, select the best composition

Chose
When more than one fully-solved layout is possible, the generator picks the best one by a defined selection rule rather than the first one found.
Rejected
Returning the first solver result unconditionally.
Cost
Generation does more work per request than a first-fit approach would.

What broke

The zero-empty-cells contract is unforgiving on purpose, which means the failure mode isn't a bad page — it's a rejected request when the current bank genuinely can't fill a 150-cell grid. That's a real, expected outcome as the bank shrinks or shifts composition, not a bug: the interface has no draft or partial state for a reason, so an unsolvable bank has to surface as "generate again once the bank changes," not as a broken export.

What I'd change

The zero-empty-cells rule is exactly right for the current two-screen scope. If the legacy magazine/reservation model ever comes back into the active path, the solver-as-gate approach needs to extend to that world too — it isn't obvious yet that the same all-or-nothing check still makes sense once answers can be reserved across multiple pages.

On this page