<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Patricio Gabriel Maseda</title>
    <description>The latest articles on Forem by Patricio Gabriel Maseda (@patriciomase).</description>
    <link>https://forem.com/patriciomase</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F493473%2F008a992b-9300-4d62-9d6e-494fabf424ca.jpeg</url>
      <title>Forem: Patricio Gabriel Maseda</title>
      <link>https://forem.com/patriciomase</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/patriciomase"/>
    <language>en</language>
    <item>
      <title>Structured AI Interview Rebuilt the Core of an App</title>
      <dc:creator>Patricio Gabriel Maseda</dc:creator>
      <pubDate>Sat, 09 May 2026 02:41:11 +0000</pubDate>
      <link>https://forem.com/patriciomase/structured-ai-interview-rebuilt-the-core-of-an-app-58ha</link>
      <guid>https://forem.com/patriciomase/structured-ai-interview-rebuilt-the-core-of-an-app-58ha</guid>
      <description>&lt;h2&gt;
  
  
  The bug that revealed the real problem
&lt;/h2&gt;

&lt;p&gt;The trigger was a bug report on timed breathing sessions. The app was confusing rounds with reps. Hold values that made sense for counted breathing made no sense for timed breathing. The concept of "20 breaths per minute" and "a round lasts 30 seconds" were being shoehorned into the same fields. And the long full-lungs retention in hormesis — a 60–120 second hold after rapid breathing — was being modeled as just a very long in-rep hold, which broke everything about how retentions should work.&lt;/p&gt;

&lt;p&gt;It wasn't a bug in the usual sense. It was a data model that couldn't express the domain it was trying to represent.&lt;/p&gt;




&lt;h2&gt;
  
  
  The old model
&lt;/h2&gt;

&lt;p&gt;The engine was built around a &lt;code&gt;Cycle&lt;/code&gt;: one inhale + optional holds + one exhale. Rounds were &lt;code&gt;cyclesPerRound&lt;/code&gt;. The custom builder called cycles "rounds." Timed and counted modes were handled by a &lt;code&gt;mode&lt;/code&gt; discriminator on each phase. Long retentions were just cycles with enormous hold values.&lt;/p&gt;

&lt;p&gt;The types looked roughly like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;BreathPhase&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;time&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;inhaleSec&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;exhaleSec&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;holds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HoldConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bpm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;bpm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;holds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HoldConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Cycle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;phases&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;BreathPhase&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
  &lt;span class="na"&gt;cyclesPerRound&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;
  &lt;span class="nx"&gt;repeatLast&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three problems were baked in:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Rounds vs reps conflated.&lt;/strong&gt; &lt;code&gt;cyclesPerRound&lt;/code&gt; was a mapping from internal engine cycles to user-visible rounds — a leaky abstraction that every UI component had to work around.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two breath modes that were almost the same.&lt;/strong&gt; &lt;code&gt;mode: 'time'&lt;/code&gt; and &lt;code&gt;mode: 'bpm'&lt;/code&gt; both described a breath with four phases. The distinction forced a discriminator into every type, every render, every conversion.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retentions as long holds.&lt;/strong&gt; A 90-second empty-lungs retention after hormesis power breathing was stored as &lt;code&gt;holdAfterExhaleSec: 90&lt;/code&gt; — the same field used for a 4-second box-breathing pause. Nothing in the model distinguished them.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The question wasn't how to fix the bug. The question was what the model should actually look like.&lt;/p&gt;




&lt;h2&gt;
  
  
  The interview
&lt;/h2&gt;

&lt;p&gt;Instead of jumping to code, the session started with a structured design interview: one question at a time, each with a stated recommendation and an explicit trade-off. The goal was to lock every decision before a single line changed.&lt;/p&gt;

&lt;p&gt;Here are the four questions that unlocked the architecture.&lt;/p&gt;

&lt;h3&gt;
  
  
  Q1 — Vocabulary
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Right now the code has &lt;code&gt;Cycle&lt;/code&gt; and &lt;code&gt;cyclesPerRound&lt;/code&gt;. You're saying this is wrong. I think the right vocabulary is a 3-level hierarchy:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rep&lt;/strong&gt; = one inhale + optional hold + exhale + optional hold. The atomic breath unit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Round&lt;/strong&gt; = a sequence of reps, defined by either total reps (count mode) or total duration (timed mode).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Program&lt;/strong&gt; = an ordered sequence of rounds.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Does this match your mental model?&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;User:&lt;/strong&gt; yes that matches, program → round → rep, this also works for constructing any breathing&lt;/p&gt;

&lt;p&gt;One exchange. Three levels. The entire naming problem resolved.&lt;/p&gt;




&lt;h3&gt;
  
  
  Q2 — One rep shape or two?
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;A rep is one inhale + optional holds + exhale + optional holds. The question is how it's parameterized.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option A — One unified shape: 4 numbers.&lt;/strong&gt;&lt;br&gt;
Every rep is &lt;code&gt;{ inhaleSec, holdAfterInhaleSec, exhaleSec, holdAfterExhaleSec }&lt;/code&gt;. BPM is just a UI convenience — "20 bpm, equal split" maps to &lt;code&gt;inhaleSec=1.5, exhaleSec=1.5&lt;/code&gt;. Internally there's only one rep type.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option B — Two rep modes.&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;{ mode: 'time', ... }&lt;/code&gt; and &lt;code&gt;{ mode: 'bpm', ... }&lt;/code&gt;. Preserves a conceptual distinction between a metronome-paced breath and a time-paced breath.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Recommendation: Option A. The internal model stays a single 4-tuple. Animation can detect "fast equal split" and pulse instead of expanding smoothly — that's a render-time decision, not a data-model one.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;User:&lt;/strong&gt; Option A, but keep the count UX, we can count up if that's easier&lt;/p&gt;

&lt;p&gt;This was the biggest design unlock. Collapsing two modes into one 4-number shape removed a discriminator from every type downstream. The data model went from "two kinds of breathing phases that mostly behave the same" to "every breath is the same shape; rounds vary." That simplification only became visible by writing it down and asking: &lt;em&gt;is this distinction real?&lt;/em&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Q4 — Retentions as a distinct round type
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Hormesis's recovery is a 60–120s full-lungs hold. Box-breathing holds are 4–10s. These are not the same thing.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option A&lt;/strong&gt; — Retention is a distinct property of a breathing round. Hormesis = breathing round + retention, kept together.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option B&lt;/strong&gt; — Long holds are just long holds inside a rep.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option C&lt;/strong&gt; — Holds are first-class steps in a round's sequence.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Recommendation: A. Matches user mental model: "do 30 power breaths, then hold." Solves "hold values make no sense for timed version" naturally — the 10s cap applies to in-rep holds; retentions are a separate thing with their own range.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;User:&lt;/strong&gt; A&lt;/p&gt;

&lt;p&gt;Two optional fields on &lt;code&gt;Round&lt;/code&gt; — &lt;code&gt;emptyLungsRetentionSec&lt;/code&gt; and &lt;code&gt;fullLungsRetentionSec&lt;/code&gt; — replaced the entire confusion between short holds and long retentions.&lt;/p&gt;




&lt;h3&gt;
  
  
  Q6 — Infinite breathing
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Box breathing currently uses &lt;code&gt;repeatLast: true&lt;/code&gt; so the last cycle loops forever. Where does "breathe like this until I stop" fit in the new model?&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;User:&lt;/strong&gt; there's no need for infinite, never asked for this, if it is in the code we can get rid of it&lt;/p&gt;

&lt;p&gt;One of the most clarifying moments in any design session: discovering that a feature was never actually needed. &lt;code&gt;repeatLast&lt;/code&gt; was removed entirely.&lt;/p&gt;




&lt;h2&gt;
  
  
  The new model
&lt;/h2&gt;

&lt;p&gt;Eleven questions. Every decision locked. Then the types:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Rep&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;inhaleSec&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;           &lt;span class="c1"&gt;// &amp;gt; 0&lt;/span&gt;
  &lt;span class="na"&gt;holdAfterInhaleSec&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;  &lt;span class="c1"&gt;// 0–10s&lt;/span&gt;
  &lt;span class="na"&gt;exhaleSec&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;           &lt;span class="c1"&gt;// &amp;gt; 0&lt;/span&gt;
  &lt;span class="na"&gt;holdAfterExhaleSec&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;  &lt;span class="c1"&gt;// 0–10s&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Round&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;rep&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Rep&lt;/span&gt;
  &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;count&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;time&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;
  &lt;span class="nx"&gt;durationSec&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;
  &lt;span class="nx"&gt;emptyLungsRetentionSec&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;  &lt;span class="c1"&gt;// 0 or 30–180s&lt;/span&gt;
  &lt;span class="nx"&gt;fullLungsRetentionSec&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;   &lt;span class="c1"&gt;// 0 or 15–60s&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Program&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;rounds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Round&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
  &lt;span class="c1"&gt;// id, name, category, level, ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The engine processes each round through five stages in order: &lt;code&gt;reps → emptyRetention → transitionIn → fullRetention → transitionOut&lt;/code&gt;. Time-mode rounds finish the current rep before moving on — never cutting someone off mid-inhale. Skip button appears only on holds of 30 seconds or longer. Transitions between retention types are hardcoded at 2 seconds, matching how the body actually works.&lt;/p&gt;

&lt;p&gt;The old model's concepts — &lt;code&gt;Cycle&lt;/code&gt;, &lt;code&gt;BreathPhase&lt;/code&gt; modes, &lt;code&gt;cyclesPerRound&lt;/code&gt;, &lt;code&gt;repeatLast&lt;/code&gt; — are gone entirely.&lt;/p&gt;




&lt;h2&gt;
  
  
  Implementation in numbers
&lt;/h2&gt;

&lt;p&gt;The rewrite was planned as 9 phases and executed in order:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Phase&lt;/th&gt;
&lt;th&gt;File(s)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Rewrite breath types&lt;/td&gt;
&lt;td&gt;&lt;code&gt;packages/types/src/breath.ts&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2a&lt;/td&gt;
&lt;td&gt;Rewrite breath engine&lt;/td&gt;
&lt;td&gt;&lt;code&gt;packages/engine/src/breathEngine.ts&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2b&lt;/td&gt;
&lt;td&gt;Rewrite catalog&lt;/td&gt;
&lt;td&gt;&lt;code&gt;packages/engine/src/programs.ts&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Rewrite custom programs storage&lt;/td&gt;
&lt;td&gt;&lt;code&gt;apps/mobile/src/lib/customPrograms.ts&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3b&lt;/td&gt;
&lt;td&gt;Supabase migration v2&lt;/td&gt;
&lt;td&gt;&lt;code&gt;supabase/migrations/…_custom_programs_v2.sql&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Rewrite custom builder UI&lt;/td&gt;
&lt;td&gt;&lt;code&gt;apps/mobile/src/screens/CustomBuilderScreen.tsx&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Update SessionScreen&lt;/td&gt;
&lt;td&gt;&lt;code&gt;apps/mobile/src/screens/SessionScreen.tsx&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Update i18n keys&lt;/td&gt;
&lt;td&gt;&lt;code&gt;packages/i18n/src/locales/{en,es}.ts&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Cleanup and type-check&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CLAUDE.md&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Single TypeScript pass. Zero errors across &lt;code&gt;apps/mobile&lt;/code&gt;, &lt;code&gt;packages/engine&lt;/code&gt;, and &lt;code&gt;packages/types&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Two highlights worth noting: the engine became a stage machine — a central &lt;code&gt;advance()&lt;/code&gt; function dispatches to the next step based on current stage and round configuration, which is far easier to reason about than the previous nested condition tree. And a pre-existing pause/resume bug (phase elapsed time not re-incremented after resume) was fixed by re-deriving &lt;code&gt;phaseElapsed&lt;/code&gt; from &lt;code&gt;phaseStartTime&lt;/code&gt; after the pause-duration shift — something that became obvious once the engine had a clear structure.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the interview actually did
&lt;/h2&gt;

&lt;p&gt;The architecture simplification in Q2 — one rep shape instead of two — was only visible because the design was written down and questioned. Free-form prompting would likely have produced code that preserved the &lt;code&gt;mode&lt;/code&gt; discriminator, because that's what the existing code had and it's the path of least resistance.&lt;/p&gt;

&lt;p&gt;The structured interview forced the question: &lt;em&gt;is this distinction real?&lt;/em&gt; It wasn't. Removing it simplified every downstream type, every render, every test.&lt;/p&gt;

&lt;p&gt;That's the pattern: before touching the codebase, spend the time to ask whether the distinctions in your current model reflect actual domain differences — or just accumulated implementation decisions that nobody questioned. An AI that asks focused questions with explicit recommendations and trade-offs is a useful forcing function for that conversation.&lt;/p&gt;

&lt;p&gt;Eleven questions. Nine phases. One TypeScript pass. A breathing engine that can now express anything the domain actually needs.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>architecture</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
