How I decide.
Filed, dated, and searchable.
259 on file
Filed notes
A gate that always fails teaches everyone to ignore the gate
Every run of the code scanner failed on an authorization error, leaving the one red check on a green stack. I deleted the step, and because my own required-tooling clause still mandates it, filed that as an open conflict rather than narrowing the clause to fit.
The font split by who wrote the words, not by type size
Contributions arrive translated, and my display face covers no Cyrillic, Greek, Vietnamese or CJK, so a line set in it breaks into tofu mid-sentence. I moved participant text to a second family at every size, which cost the thin weight the design had been built on.
Running TypeScript 7 cost me every type-aware lint rule
Staying on TypeScript 6 would have kept my linter working, and no published version of the type-aware tooling supports 7. I took 7 and moved the toolchain to Biome, retiring the framework ruleset and every check that needs type information—permanently, not until someone catches up.
A column whose only writer declined to write it is worse than absent
An earlier migration left a status column annotated as written only by the feature I was then building, and I decided against writing it. Dropping the column, its index and its enum means closure is now counted across answer rows at selection time rather than read from a boolean.
Three result files named configurations that never ran
I amended my governing document twice on numbers a script had fabricated: it wrote its model and thinking arguments into the output filename while hard-coding the cheap tier. Two review bots caught what I had not, so I re-ran every comparison on recordings the prompts had never seen.
The rate-limit row records that something was submitted, never what
A limiter has to count withheld and failed submissions, and those leave no published row to count. Rather than loosen either constraint I added a counter storing only that a submission happened, never what it held, and filed the rule it bends as an open violation.
I kept a call split my own measurement said was worthless
One merged call matches four separate ones for accuracy at the tier I ship on, and saves fifteen hundredths of a cent per contribution. I kept the four anyway, because merged drops to zero of ten the moment the model tier does, and tier is what gets cut for cost.
I never gave a recording an address, so there was nothing to delete
Nothing writes a recording to object storage, because I hold the bytes for one request and release them on every exit path. That retired most of the code enforcing my retention rules, and any replay with it—a response lost in transit means the contribution is gone.
I put generated audio in a column because the first bucket becomes where audio goes
I gave generated playback a column rather than a bucket. A first bucket standing beside a rule about audio not accumulating becomes, to whoever reads the repository next, the obvious place audio belongs. Three to four megabytes leave Postgres on every uncached press.
The rule I deleted was measuring a paragraph, not a call shape
Crisis, illegal content and relevance each held their own model call, and I had measurements defending the split. The dedicated crisis prompt turned out to carry a weighing clause the merged one lacked. Controlled for that, both caught ten of ten, and the prohibition came out.
Told the presentation was the problem, I changed the data instead
Every card carried a blurb and a fact — a summary of a summary — and the blurb was the half being indexed and shown. I deleted the blurb, put the fact itself on the tile, and cut 103 cards that turned out to be describing rather than deciding.
I traded stronger copy for accurate credit on every award I shared with a team
A rewrite turned my award cards from passive voice into first person and, in doing so, claimed sole ownership of work my team did. I pulled sixteen of them back to shared credit. The hedged verbs read weaker than the version that overclaimed, and that is the accurate one.
Forty-one cards publish under my own word floor because the alternative was inventing the reason
My rules set a thirty-word minimum on every card. Forty-one could not reach it without me supplying a rationale the original decision never recorded. I left them short and live rather than pad them. A rule I wrote loses to a claim I cannot support.
Guardrails and model cross-review replaced the code reading I gave up
Instead of reading every generated path on Carbon Trace, I leaned on strict guardrails, Claude Opus self-review, Codex implementation support, and occasional Antigravity input. That let me pick up the frontend stack and the UX decisions in context rather than stopping to study them first.
Every Save the Sun commit was signed, attributed, and scanned in CI
On Save the Sun I enforced provenance in the pipeline: signed atomic commits, AI-attribution footers on every commit, and secret-scanning plus CodeQL running in CI on every push.
Save the Sun won Best Google AI Usage in the June Solstice Game Jam 2026
I submitted Save the Sun as The Oracle and the Wolf and won Best Google AI Usage in DEV's June Solstice Game Jam 2026. Gemini plays a live deduction rival on top of a deterministic engine that never hands it the answer.
Every automated gate replaced a review I stopped doing by hand
The pipeline started as tests and that was not enough. I added Sonar, then Lighthouse CI, then Playwright, keeping each tool that caught something the others did not. If I am not reading every diff myself, something has to be doing that job instead.
I put a database between the AI and the search index so I could review before publishing
I wanted the AI writing cards, and the Algolia MCP was still in beta. I built a server that writes to Supabase instead, so I could read every card before pushing to Algolia. Algolia is production; Supabase is where AI output waits for me.
I containerized to Cloud Run because DEV added embed support and suggested it
DEV added support for embedding a live Cloud Run service and recommended it when the New Year challenge opened. I containerized the Next.js app instead of taking the one-click Vercel deploy, because the embed was worth more than the convenience and I already knew GCP.
The card writer was built for OpenAI first and converted to MCP when I added Claude
The first version targeted ChatGPT specifically, before the Codex app existed. When I started using Claude I rewrote it as an MCP server so either model could write cards, which meant giving up anything OpenAI-specific I had leaned on.
Cards are soft-deleted so Supabase migrations do not force an Algolia reindex
I soft-delete cards instead of removing rows, because hard deletes would mean tearing down and rebuilding the Algolia index on every Supabase migration. Algolia documents soft deletion as the pattern for keeping an index stable across upstream schema changes, so deleted cards stay as rows with a timestamp.
I split the Lighthouse budgets and let mobile score lower
Mobile failed the shared budget and I did not understand mobile performance well enough to fix it properly. I split the configs and set mobile lower rather than block the pipeline on something I could not yet diagnose. It has been climbing since.
Ruled out an AI ATC-audio pipeline after confirming OpenSky already broadcasts callsign
I researched fine-tuning Whisper on ATC audio to resolve flight callsigns for Metal Birds Feed, assuming the ADS-B feed only carried icao24. OpenSky's live state-vector API returns callsign alongside icao24 already, so I dropped the audio pipeline rather than build GPU inference and LiveATC permission requests for data already available.
ICAO hex is a nullable enrichment field, never a required key
The ICAO 24-bit address is a Mode-S allocation separate from registration, so a national register carries it only when it derives it from a public formula, as the FAA does. I keep hex nullable rather than a join requirement, so a source that omits it still onboards.
I put the data validation and end-to-end testing hours in before a platform pilot launched
A platform pilot was heading for launch. I contributed the data validation and end-to-end testing hours ahead of it rather than trusting the design to hold on its own, and the team's launch came off clean. The Home Depot gave me a Homer Award on July 15, 2026.
The object store is the job state; there is no status database
I refused a second datastore. The rendered object in the public bucket is the ready state, and the metadata file is written last, so a crash leaves a job retryable and never leaves a job marked ready whose output does not exist.
I queued the work rather than pay Cloud Run for idle CPU
Detaching the job from the request requires Cloud Run's no-cpu-throttling, which charges for a container doing nothing. I ran the pipeline on a Cloud Tasks worker request that calls back into the service, so a user can close the tab. Background execution bills instance time; a worker request bills work.
A prompt example that is the right answer gets parroted back
My prompt examples described a plausible commit arc, and the model reproduced them rather than reading the evidence in front of it. I rewrote the examples to describe an arc no real repository could have, so they teach shape without supplying a conclusion.
A weekend has no room to discover a project-killer on Sunday
Before building anything I proved two things: that a stored procedure could reach the GitHub API through an external access integration, and that Cortex ran in my region. Either failing would have ended the project, and I had two days to find out.
The 500-commit ingest cap prints itself on the output
I stop ingest at five hundred commits to bound the bill, and when a repository is longer the output says so. Reporting a slice as a whole life is false, and the honest fix is to admit the boundary on the artifact. A cap you hide is a lie.
A function clicked into existence in a UI does not live in your git history
Cortex AI Function Studio was the fastest way to prototype the narration call, but the object it creates exists only inside the account. I rewrote it as a hand-written UDF over AI_COMPLETE so the function ships in the repo and deploys with the CLI.
A repo with no real story gets told so, and the model never runs
I gated every storyline on a fifteen-commit floor: below it the card reads "no story here" and no model is called at all. A tool that admits when there is nothing to say is the one you believe when it says otherwise.
The model reads the arc of a commit history and never the motive behind it
I let the model interpret the arc — the gaps, the hours, the returns — but forbade it from asserting intent or feeling. The moment the output guesses at why, nothing else it claims is trustworthy. Timestamps are evidence; motivation is invention.
A twenty-thousand-commit repo bills the same as a two-hundred-commit one
Narration cost cannot grow with repository size, so I budgeted the model's evidence at a quarter of the commit lines, floored at twenty and capped at one hundred forty. The demo and a production run are the same architecture — there is no rewrite waiting between them.
A profile aggregates unrelated work into noise; a repository has an arc
The original plan read a developer's entire year. I cut it to a single repository because a repo has a clean shape — commits start, cluster, pause, restart, or stop — and that shape is what gets narrated. A year of a profile is several unrelated shapes overlaid.
Two reads of the same repo were handing the model different commits
I ordered evidence by distance from the pivot and then by timestamp, which is not a total order — rebased and batch-pushed commits share an authored time. SHA became the last tiebreak, because a narrator that changes its evidence between runs cannot be trusted about any of it.
The model wrote "fifty-six commits after midnight" about forty-seven
Handed every fact as a single object, the model read the total commit count and printed it as the night count. I split each fact into its own labelled argument and barred the model from emitting digits at all. The renderer composes every number now.
I raised the model's temperature from 0 to 0.4 and moved the guardrails into SQL
Temperature zero is the responsible number and it wrote nothing worth reading. I turned the sampler up until the writing had a pulse, then paid for it with thirteen SQL reject checks, so the warehouse rather than the sampler decides what reaches a card.
SQL picks the story in a commit history; Cortex only writes the sentence
I put the narrative decision in plain SQL and left the model only the prose. Six detectors score the history and one wins deterministically, so the same repo always yields the same story and the only non-deterministic step is a sentence.
I built the daily results summary that made platform testing output readable
A platform testing effort the team was running produced system-behavior output nobody outside it could read. I built the system-behavior report and the daily results summary in a format the whole team could use without translation. The Home Depot gave me a Homer Award on July 1, 2026.
I tore out the live voice session and rebuilt it as push-to-talk
I scrapped my first voice approach — a continuous live audio session — and rebuilt it as push-to-talk: hold, speak, release, transcribed server-side through the exact same pipeline as the text box. Voice and typing became identical, and the key never reached the browser.
I matched each AI role to a model tier instead of using one model everywhere
Rather than run one model for everything, I matched each AI role to a tier: a full-strength model where reading the player's words exactly mattered, and a lighter, cheaper one for the rival so he'd play looser — plus cheap tiers for speech-to-text and text-to-speech.
I made the text-to-speech endpoint speak only server-owned lines
I built the text-to-speech endpoint so it never voices client-supplied text — only server-owned lines by known ID or opaque per-session lookup. That keeps it from being abused as a free speech service or an injection surface, and it is rate-limited per session on top.
I sized my own rate limits to trip before the vendor's quota
I put a per-surface rate limiter in front of every AI call, keyed by client and session and sized so a whole game never throttles — but abuse trips my limiter before the vendor's quota, with the billing cap as the hard spend stop.
I kept the public layout and the hidden answer on two unlinkable seeds
In my AI deduction game I drew the public board layout and the hidden answer from two separate random seeds that can never be linked — otherwise the visible arrangement could leak the answer. The secrecy guarantee lives in the engine, not in the shuffle.
I built in dependency order and tested each layer before the next
I built the game in strict dependency order — core engine first, then the language layer, then the opponent — and each module shipped with its own test suite before the next began. Nothing was layered onto untested foundations.
I shipped with no database and still made a refresh resume the round
I deliberately shipped with no database or accounts — sessions live in memory behind an opaque cookie. A refresh still resumes the exact round, with the same answer, board order, and progress, so the simpler architecture cost the player nothing.
I set coverage floors by blast radius, not a blanket number
On this game project I tiered CI test-coverage gates by risk, not one flat number: the core engine carried a 100% bar because a bug there means an unfair round, and the UI a far lower one. Config could only raise a threshold, never weaken it.
I built the debug view as on-stage proof that the engine owns truth
To prove my game's AI couldn't cheat, I built a live debug log that tags every result as either deterministic engine truth or AI inference. It lets me show, in real time, that code owns every fact and the AI only supplies the voice.
I scope with explicit non-goals and documented rejected options
I scope by writing down what a project will NOT do, in versioned milestones, and I record rejected options with the reason they lost. Documenting the roads not taken keeps a solo, deadline-driven build honest.
I let the AI play the opponent and rejected a scripted one on purpose
For my deduction game's AI rival, I rejected a scripted opponent — a predictable one is beatable like a puzzle and wastes the model. I let the AI reason live instead, with a weighted-random fallback only when it errors. Its capacity to misjudge is the feature.
I treated accessibility and graceful degradation as CI gates, not polish
I treated accessibility as a build gate, not polish: missing WCAG 2.1 AA contrast or the Lighthouse accessibility bar fails the build. The game stays fully playable with every visual and audio flourish off — a feature that can't degrade cleanly doesn't ship.
I capped the AI opponent at a 12-year-old's reasoning so it would feel human
I deliberately weakened my game's AI opponent so it would feel human: I ran him on a lighter, cheaper model tier and prompted him to reason one clue at a time and guess when 'sure enough.' A fallible opponent feels more alive than a flawless one.
I kept the API key in a secrets manager and let a test enforce the masking
I kept the Save the Sun API key only in a secrets manager, masked it at the logging layer, and wrote a test that asserts it never leaks. The masking is enforced by the suite, not by remembering to mask each new log line.
I test the engine's leash, not the model's brain
On an AI game project, my testing rule was 'test the engine's leash, not the model's brain.' I never assert what the AI chooses — only that illegal moves are rejected, the hidden answer never leaks, and a deterministic fallback always catches AI failures.
I made the engine the only source of truth and never let the AI see the secret
Save the Sun is a browser deduction game where you race an AI to a hidden answer. I gave the AI four roles — reading questions, playing the rival, transcribing speech, voicing lines — but kept all truth in a deterministic engine that never lets it see the secret.
DATA_LICENSES.md collapsed from 1,844 lines to 273
I cut DATA_LICENSES.md from 1,844 lines to 273 by moving verbatim agency replies, recon notes, license-drift logs, and roughly 150 awaiting-reply entries into the source-onboarding-checklist and license-matrix. The public file now answers a downstream consumer's questions, and nothing more.
Attribution docs publish copy-paste-ready example text, not rule descriptions
I reframed the Attribution section to publish the exact text a downstream consumer pastes into their UI, not a description of what attribution rules require. Fabricated boilerplate like “this material has been changed” was stripped from every block once I noticed it came from neither the agency nor the license.
Every source promotion ends with a stale-reference grep sweep
After every source-status change I grep for the prior label and prior URL across DATA_LICENSES, README, license-matrix, and source-onboarding-checklist and confirm zero hits. Three promotions in a row had left stale Personal-use references and outdated register URLs hiding in one file or another.
Stopped guessing agency emails after two same-day bounces
I stopped using pattern-guessed agency emails after Lebanon and Dominican Republic both bounced on the same day. Every address now gets verified against a real contact page before a draft is written. Verified channel first, drafted body second.