<?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>DEV Community: Robert</title>
    <description>The latest articles on DEV Community by Robert (@robswierk).</description>
    <link>https://dev.to/robswierk</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4053993%2Fb8f114ea-e5d9-4003-b6a1-1549fdddee94.png</url>
      <title>DEV Community: Robert</title>
      <link>https://dev.to/robswierk</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/robswierk"/>
    <language>en</language>
    <item>
      <title>133 commits across 5 repos in 8 active days</title>
      <dc:creator>Robert</dc:creator>
      <pubDate>Mon, 31 Aug 2026 10:00:00 +0000</pubDate>
      <link>https://dev.to/robswierk/133-commits-across-5-repos-in-8-active-days-578n</link>
      <guid>https://dev.to/robswierk/133-commits-across-5-repos-in-8-active-days-578n</guid>
      <description>&lt;p&gt;133 commits landed between 2026-08-22 and 2026-08-30, touching 355 files. Lines added came to 20,158 against 2,140 removed.&lt;/p&gt;

&lt;p&gt;The work spread across five repos: FitGro (3 commits), Life-Race-V2 (19), NeuraGrowthHTML (9), duratio (2), and new-Orqestra (100). Orqestra work alone accounted for 100 of the 133 commits in the window. The record does not describe what drove that volume or what the work consisted of.&lt;/p&gt;

&lt;p&gt;Life-Race-V2 saw 19 commits, including a scale fix on 2026-08-29. The rooms and the cars disagreed by a factor of about three. Roblox vehicle packs draw a car 20.6 studs long; the catalogue asked for 14, so VehicleBodyLibrary was shrinking the car to 68%. The nine buildings were laid out at about 1.67 times life size, with a garage carrying a 21 stud ceiling, a 15 stud door, and a workbench eleven metres long. Multiply the two and a correctly drawn car looks like a model on the floor of a warehouse.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;When one repo accounts for the majority of commits in a window, the raw numbers are the only record of where the work actually went.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://neuragrowth.co/log/133-commits-across-5-repos-in-8-active-days/" rel="noopener noreferrer"&gt;neuragrowth.co&lt;/a&gt;. NeuraGrowth is a one-person digital-products studio; this is the log of what its pipeline does and where it breaks.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>programming</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Niche pipeline produced 0 new niches on 28 August</title>
      <dc:creator>Robert</dc:creator>
      <pubDate>Sat, 29 Aug 2026 10:00:00 +0000</pubDate>
      <link>https://dev.to/robswierk/niche-pipeline-produced-0-new-niches-on-28-august-225h</link>
      <guid>https://dev.to/robswierk/niche-pipeline-produced-0-new-niches-on-28-august-225h</guid>
      <description>&lt;p&gt;The 09:30 digest on 28 August reported 0 new niches in the last 24 hours. The day before, the same digest at 09:30 on 27 August reported 4 new niches, so the drop to zero is a single-day gap, not a persistent pattern.&lt;/p&gt;

&lt;p&gt;The backlog held 55 proposals pending approval and 0 products in review. The backlog count was the same on both days, which means the pending proposals were not being cleared during this period either.&lt;/p&gt;

&lt;p&gt;Pipeline cost for 28 August was $0.8349 against a $5.00 daily cap, putting spend at 17 percent of cap. The day before, cost was $0.7234, or 14 percent of cap. Both days the pipeline spent money. The facts do not say why generation produced nothing on 28 August despite the pipeline running.&lt;/p&gt;

&lt;p&gt;A non-zero daily cost is not a reliable signal that the pipeline generated output. The two digests together show the pipeline can spend a similar amount whether it produces 4 niches or zero.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A non-zero daily cost does not confirm that the pipeline produced output.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://neuragrowth.co/log/niche-pipeline-produced-0-new-niches-on-28-august/" rel="noopener noreferrer"&gt;neuragrowth.co&lt;/a&gt;. NeuraGrowth is a one-person digital-products studio; this is the log of what its pipeline does and where it breaks.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>programming</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>A discovery run took 60 minutes instead of 8 and then failed</title>
      <dc:creator>Robert</dc:creator>
      <pubDate>Fri, 28 Aug 2026 10:00:01 +0000</pubDate>
      <link>https://dev.to/robswierk/a-discovery-run-took-60-minutes-instead-of-8-and-then-failed-18ne</link>
      <guid>https://dev.to/robswierk/a-discovery-run-took-60-minutes-instead-of-8-and-then-failed-18ne</guid>
      <description>&lt;p&gt;One discover_trending_niches run took 60.0 minutes and failed. The median across the 8 runs we have on record is 8.2 minutes, making this run 7.3 times slower than that median.&lt;/p&gt;

&lt;p&gt;The logs do not say why the run was slow or why it failed. We know the start time, the duration, and the outcome. The cause is not recorded.&lt;/p&gt;

&lt;p&gt;A daily digest from the same period shows 55 niche proposals sitting in the backlog pending approval, 4 new niches added in the last 24 hours, and a daily API spend of $0.7234 against a $5.00 cap.&lt;/p&gt;

&lt;p&gt;When a run fails with no cause recorded, the only honest statement is that the facts needed to diagnose it are absent. The 7.3x duration anomaly is real and documented. What drove it is not.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;When a run lands at 7.3x its normal duration and the logs record no cause, the facts needed to diagnose it are simply absent.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://neuragrowth.co/log/a-discovery-run-took-60-minutes-instead-of-8-and-then-failed/" rel="noopener noreferrer"&gt;neuragrowth.co&lt;/a&gt;. NeuraGrowth is a one-person digital-products studio; this is the log of what its pipeline does and where it breaks.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>programming</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>A blog post was rejected at the gate for being too short</title>
      <dc:creator>Robert</dc:creator>
      <pubDate>Thu, 27 Aug 2026 10:00:00 +0000</pubDate>
      <link>https://dev.to/robswierk/a-blog-post-was-rejected-at-the-gate-for-being-too-short-3790</link>
      <guid>https://dev.to/robswierk/a-blog-post-was-rejected-at-the-gate-for-being-too-short-3790</guid>
      <description>&lt;p&gt;At 00:22 on 27 August, the gate rejected a blog post. The reason given was length: 559 words against a minimum of 700.&lt;/p&gt;

&lt;p&gt;Nothing was published. The alert noted that the material was not consumed and will return at the next run.&lt;/p&gt;

&lt;p&gt;The facts do not say why the post came in under the limit after its correction pass. That gap in the record is worth noting.&lt;/p&gt;

&lt;p&gt;Two note rejections fired in the same period. One post had a title at 93 characters against a 90-character limit. Another had body text at 148 words against a 150-word minimum. All three alerts share the same pattern: text that did not pass the check after correction, landing just outside a hard threshold.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The gate holds material rather than publish it short, and it will retry on the next run.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://neuragrowth.co/log/a-blog-post-was-rejected-at-the-gate-for-being-too-short/" rel="noopener noreferrer"&gt;neuragrowth.co&lt;/a&gt;. NeuraGrowth is a one-person digital-products studio; this is the log of what its pipeline does and where it breaks.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>programming</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>A TypeError rejecting 'temperature' stopped discover_trending_niches on 2 consecutive days</title>
      <dc:creator>Robert</dc:creator>
      <pubDate>Thu, 27 Aug 2026 00:08:58 +0000</pubDate>
      <link>https://dev.to/robswierk/a-typeerror-rejecting-temperature-stopped-discovertrendingniches-on-2-consecutive-days-16k8</link>
      <guid>https://dev.to/robswierk/a-typeerror-rejecting-temperature-stopped-discovertrendingniches-on-2-consecutive-days-16k8</guid>
      <description>&lt;p&gt;On 2026-08-25 and again on 2026-08-26, discover_trending_niches produced the same error: TypeError("AsyncMessages.create() got an unexpected keyword argument 'temperature'"). The failure count was 2, across 2 distinct days.&lt;/p&gt;

&lt;p&gt;The error tells us that 'temperature' was passed to AsyncMessages.create() and that the method does not accept it. The facts do not say why the argument was included or what changed to make it invalid.&lt;/p&gt;

&lt;p&gt;No output was produced on either run. The facts record 0 USD wasted across both failures.&lt;/p&gt;

&lt;p&gt;This is the second recurring error pattern we have logged for discover_trending_niches in August 2026. The previous pattern, between 2026-08-17 and 2026-08-24, was a different error involving tasks being reaped at worker startup.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;When a TypeError names an exact method and an exact argument, that pair is the full signal the error gives you - the facts will not say more.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://neuragrowth.co/log/a-typeerror-rejecting-temperature-stopped-discover-trending-niches-on/" rel="noopener noreferrer"&gt;neuragrowth.co&lt;/a&gt;. NeuraGrowth is a one-person digital-products studio; this is the log of what its pipeline does and where it breaks.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>programming</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>The Schema That Got Too Big To Compile</title>
      <dc:creator>Robert</dc:creator>
      <pubDate>Wed, 26 Aug 2026 14:31:28 +0000</pubDate>
      <link>https://dev.to/robswierk/the-schema-that-got-too-big-to-compile-cge</link>
      <guid>https://dev.to/robswierk/the-schema-that-got-too-big-to-compile-cge</guid>
      <description>&lt;h2&gt;
  
  
  Episodes as data, not as prose
&lt;/h2&gt;

&lt;p&gt;I generate short animated episodes for a children's channel, and the model does not write a screenplay. It emits a structured object: a list of beats, each with a narrator line, an optional character line, staging cues, camera framing, props, and a symbol on screen. A renderer interprets that object into video.&lt;/p&gt;

&lt;p&gt;The reason for structured output here is not tidiness. A model asked to invent an action will invent an action that does not exist, and the failure is invisible until someone watches the finished file. If every action and prop is a &lt;code&gt;Literal&lt;/code&gt; in the schema, an invented one cannot leave the parser. The schema is the vocabulary of the world.&lt;/p&gt;

&lt;p&gt;Which means the schema grows every time the world does. That is by design, and it is exactly what walked me into the ceiling.&lt;/p&gt;

&lt;h2&gt;
  
  
  One field, and nothing generates
&lt;/h2&gt;

&lt;p&gt;I drew ten new props, which widened one enum, and separately added a &lt;code&gt;backdrop&lt;/code&gt; field so a beat could say where it happens. The next call came back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;400 invalid_request_error: The compiled grammar is too large, which
would cause performance issues. Simplify your tool schemas or reduce
the number of strict tools.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part is what "the next call" means. It was not the long-format episode that broke, the one with fifty beats. It was every call. A thirty-second short with nine beats failed identically, because the limit is on the compiled schema, not on the output. Nothing in the system could produce an episode at all.&lt;/p&gt;

&lt;p&gt;This was the second time in three days. The first was a one-line field I had added to record which structural template an episode used, and the symptom then was exactly as total. Both times the overnight run failed into a container log and nobody found out until morning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the space actually goes
&lt;/h2&gt;

&lt;p&gt;There is no published number for the ceiling, so I measured against my own schema by serialising it and printing the size of each definition:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Beat      1551 chars, 13 fields
    backdrop   164
    tone       163
    drift      132
    props      121
PropCue   1212 chars, 10 fields
    kind       296
    at         121
CharCue    654 chars,  4 fields
    act        246
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Working: about 4,680 characters of JSON schema. Failing: about 4,850. Those are my numbers on one provider and should not be treated as a universal constant, but the shape of the cost generalises.&lt;/p&gt;

&lt;p&gt;Two things dominate, and only one of them is obvious.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enums cost what they list.&lt;/strong&gt; &lt;code&gt;kind&lt;/code&gt; is 296 characters because it enumerates twenty-seven props. Drawing ten new ones was not free; it widened that enum in every prop, in every beat.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Optional enums cost roughly double.&lt;/strong&gt; This is the part I had not thought about. In Pydantic, &lt;code&gt;x: Literal["a","b"] | None = None&lt;/code&gt; produces an &lt;code&gt;anyOf&lt;/code&gt; with two branches: the enum and a null type. The same field written as &lt;code&gt;x: Literal["a","b"] = "a"&lt;/code&gt; produces just the enum. Same expressiveness for the model, meaningfully less grammar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Defaults instead of nullables
&lt;/h2&gt;

&lt;p&gt;I converted three optional enums to non-nullable fields with defaults: camera framing, camera drift, and the mood tag handed to the speech synthesiser.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;- shot:  Literal["wide", "mid", "close"] | None = None
&lt;/span&gt;&lt;span class="gi"&gt;+ shot:  Literal["wide", "mid", "close"] = "wide"
&lt;/span&gt;&lt;span class="gd"&gt;- drift: Drift | None = None
&lt;/span&gt;&lt;span class="gi"&gt;+ drift: Drift = "hold"
&lt;/span&gt;&lt;span class="gd"&gt;- tone:  Tone | None = None
&lt;/span&gt;&lt;span class="gi"&gt;+ tone:  Tone = "calm"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That took the schema back under the ceiling and generation resumed, with the new field and the ten new props still in place. No capability was lost. The model simply always states the framing, the drift and the mood rather than sometimes leaving them out, which if anything makes the output easier to read.&lt;/p&gt;

&lt;p&gt;Worth saying plainly: a field the model must not fill has no business in the schema at all. My first ceiling breach was a bookkeeping field recording which template an episode used, something the code stamps after validation. It became a private attribute and left the grammar entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Assert the budget where you can see it
&lt;/h2&gt;

&lt;p&gt;Twice in three days, with the same total symptom, is a pattern rather than an accident. Vocabulary grows; that is the whole point of this design. So the ceiling needs to announce itself at build time rather than at one in the morning:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;SCHEMA_BUDGET_CHARS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4820&lt;/span&gt;

&lt;span class="n"&gt;_size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EpisodeDraft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;model_json_schema&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;_size&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;SCHEMA_BUDGET_CHARS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Episode schema is %d chars against a budget of %d. The provider &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;will refuse to compile the grammar and NO episode will be &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;generated. Drop a field or turn an optional Literal into one &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;with a default.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;_size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SCHEMA_BUDGET_CHARS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A log line, not an exception, because crashing the API process over a schema measurement trades one outage for another. The message names the failure and the two fixes, so whoever hits it next does not have to rediscover the arithmetic.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you use structured outputs
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The limit is on the schema, not the response.&lt;/strong&gt; Crossing it fails your smallest call as surely as your largest, which makes it look like an outage rather than a size problem.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prefer a default over a nullable enum.&lt;/strong&gt; It is the cheapest headroom available and it changes nothing about what the model can express.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep bookkeeping fields out.&lt;/strong&gt; Anything your code fills in after validation should not be in the grammar the model is compiled against.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure before you widen a vocabulary.&lt;/strong&gt; Adding ten items to an enum is a schema change in every place that enum appears, which in a nested list is more places than you think.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Put the budget in the code.&lt;/strong&gt; Discovering the ceiling from a 400 during an unattended run is the expensive way to learn it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the diagnostic habit that mattered most: after the long-format call failed, I tested a short one before assuming the long format was special. It failed too, which turned "the complicated case is broken" into "everything is broken" in one cheap call. Verifying the blast radius is usually faster than reasoning about it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://neuragrowth.co/blog/schema-grammar-ceiling/" rel="noopener noreferrer"&gt;neuragrowth.co&lt;/a&gt;. I run a one-person digital-products studio and write up what breaks in production.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you write CLAUDE.md files, I keep a set of working templates here: &lt;a href="https://neuragrowth.co/free/claude-md-templates/" rel="noopener noreferrer"&gt;neuragrowth.co/free/claude-md-templates&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>programming</category>
      <category>software</category>
    </item>
    <item>
      <title>Your Health Check Is Talking To Itself</title>
      <dc:creator>Robert</dc:creator>
      <pubDate>Tue, 25 Aug 2026 10:00:00 +0000</pubDate>
      <link>https://dev.to/robswierk/your-health-check-is-talking-to-itself-2naa</link>
      <guid>https://dev.to/robswierk/your-health-check-is-talking-to-itself-2naa</guid>
      <description>&lt;h2&gt;
  
  
  Two workers, two runtimes, one broker
&lt;/h2&gt;

&lt;p&gt;I was adding video rendering to a pipeline that had never rendered video. Rendering needs Node and a headless Chromium; nothing else in the system needs either, and neither belongs on a host that also holds the production database. So the renderer became its own container image with its own Celery worker on its own queue, which is the ordinary way to run heterogeneous work under one broker.&lt;/p&gt;

&lt;p&gt;The backend sends a job by task name. The render worker picks it up, produces a file on a shared volume, and returns the path. Two images, two runtimes, one Redis between them. Clean.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ready, healthy, deaf
&lt;/h2&gt;

&lt;p&gt;I deployed it, triggered an episode, and watched the logs. The render worker said everything you want to see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[INFO/MainProcess] mingle: searching for neighbors
[INFO/MainProcess] mingle: all alone
[INFO/MainProcess] render@8d1fd465fd70 ready.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then nothing. No task received. Meanwhile the sending task sat blocked, waiting on a result that was never going to arrive, and the container health check kept returning success on a sixty-second interval.&lt;/p&gt;

&lt;p&gt;The cause was two characters. My worker had a sensible-looking default:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;BROKER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;REDIS_URL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;redis://redis:6379/0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rest of the system publishes on database &lt;strong&gt;1&lt;/strong&gt; and keeps results on database &lt;strong&gt;2&lt;/strong&gt;. The worker was listening on database 0, an empty room in the same building. The episode was sitting in a queue it never looked at.&lt;/p&gt;

&lt;h2&gt;
  
  
  The health check agreed with the worker
&lt;/h2&gt;

&lt;p&gt;A misconfigured worker is a five-minute bug. What made this worth writing down is that I had a health check specifically designed to catch a dead worker, it ran every minute, and it passed the entire time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;celery &lt;span class="nt"&gt;-A&lt;/span&gt; worker:app inspect ping &lt;span class="nt"&gt;-d&lt;/span&gt; render@&lt;span class="nv"&gt;$HOSTNAME&lt;/span&gt; &lt;span class="nt"&gt;--timeout&lt;/span&gt; 10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That command is a good health check. It does not just look at the process table: it round-trips a message through the broker and back, so it fails if the worker is wedged, if the event loop is blocked, or if the broker is unreachable. I had chosen it deliberately over a naive process check.&lt;/p&gt;

&lt;p&gt;But &lt;code&gt;inspect ping&lt;/code&gt; round-trips through &lt;em&gt;the worker's own broker connection&lt;/em&gt;. It read the same environment variable, connected to the same database 0, sent a ping into that empty room, and the worker answered it. Both ends of the check were on the wrong side of the misconfiguration, so the check confirmed, correctly and uselessly, that a worker nobody could reach was alive and well.&lt;/p&gt;

&lt;p&gt;This is the same class of error as a backup timer that reports "active" while the backup has not completed in three days, or a video file that every metadata tool calls valid because they all read the same intact header. The instrument and the fault share an assumption.&lt;/p&gt;

&lt;h2&gt;
  
  
  Delete the default
&lt;/h2&gt;

&lt;p&gt;The repair was not a better health check. It was removing the thing that made a wrong configuration possible:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;BROKER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CELERY_BROKER_URL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;RESULT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CELERY_RESULT_BACKEND&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both required, no fallback, both passed through from the same environment file the backend reads rather than written out a second time in the compose file. Missing configuration now stops the container instead of producing a healthy-looking one, and there is no second copy of a broker URL to drift.&lt;/p&gt;

&lt;p&gt;That is the trade a default hides. A fallback value turns a loud startup failure into a quiet runtime one, and it does it precisely in the situation where you most need the noise: the first deploy, when nothing is known to work yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check the channel, not your end of it
&lt;/h2&gt;

&lt;p&gt;Three questions I now ask of any health check before trusting it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Can this pass while the thing it monitors is completely broken?&lt;/strong&gt; If yes, it is checking a process, not a result. Rewrite it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Does it share configuration with its subject?&lt;/strong&gt; A check that reads the same environment variable inherits the same mistake. The useful check comes from the other end of the wire, or from the artifact that should exist.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What would this look like if the answer were "nobody is listening"?&lt;/strong&gt; Usually: exactly like success. That is the whole problem.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For queues specifically, the check that would have caught this is boring and effective: the sender enqueues a trivial job and asserts a result comes back within N seconds. It crosses the same boundary the real work crosses, so it cannot be fooled by an assumption only one side holds. It costs one queue message a minute.&lt;/p&gt;

&lt;p&gt;The worker was fixed in five minutes. The health check took longer, because the first version was not wrong, it was simply looking in the wrong direction, and those are harder to notice than a check that is missing.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://neuragrowth.co/blog/healthcheck-talking-to-itself/" rel="noopener noreferrer"&gt;neuragrowth.co&lt;/a&gt;. I run a one-person digital-products studio and write up what breaks in production.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you write CLAUDE.md files, I keep a set of working templates here: &lt;a href="https://neuragrowth.co/free/claude-md-templates/" rel="noopener noreferrer"&gt;neuragrowth.co/free/claude-md-templates&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>monitoring</category>
      <category>python</category>
      <category>docker</category>
    </item>
    <item>
      <title>The Validation Rule That Could Never Fail</title>
      <dc:creator>Robert</dc:creator>
      <pubDate>Mon, 24 Aug 2026 15:20:23 +0000</pubDate>
      <link>https://dev.to/robswierk/the-validation-rule-that-could-never-fail-1505</link>
      <guid>https://dev.to/robswierk/the-validation-rule-that-could-never-fail-1505</guid>
      <description>&lt;h2&gt;
  
  
  A pipeline that writes children's cartoons
&lt;/h2&gt;

&lt;p&gt;I run a small studio where an agent pipeline produces short animated episodes for a children's channel: a language model writes the script as structured data, a text-to-speech provider reads it, and a renderer turns the whole thing into video. Nobody watches it happen. That is the point, and it is also the problem, because the only thing standing between a language model and a published video is a set of validation gates.&lt;/p&gt;

&lt;p&gt;One of those gates checks a claim the script makes out loud. In an alphabet episode the narrator says something like "P, as in pillow", and the gate verifies that the word actually starts with the letter being taught. It is a small rule with a real job: an episode that shows a flower while saying it starts with P teaches a four-year-old something false.&lt;/p&gt;

&lt;h2&gt;
  
  
  Zero problems, all day
&lt;/h2&gt;

&lt;p&gt;I shipped the gate and ran it across every episode the channel had produced. It reported problems in five of seven files: a prop that vanished mid-scene, a narrator repeating a character's line, two characters standing in the same place. Useful, specific, actionable. Exactly what a new gate should look like.&lt;/p&gt;

&lt;p&gt;The letter rule, specifically, reported nothing. I had a flower labelled as starting with P sitting in one of those episodes, so I went looking. The pattern printed correctly in the source:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;_LETTER_LIKE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\b([a-z]{1,6})\s+jak\s+([a-z]+)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IGNORECASE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It compiled. Its flags were right. Applied to text that obviously matched, it returned an empty list. So I printed the compiled pattern back out, as characters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;repr: '\x08([a-z]{1,6})\\s+jak\\s+([a-z]+)'
kody: ['0x8', '0x28', '0x5b', '0x61', ...]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;0x8&lt;/code&gt;. The pattern did not start with a word boundary. It started with a literal backspace character, because somewhere in the editing that produced the file the string had stopped being raw and &lt;code&gt;\b&lt;/code&gt; had been interpreted as the escape it is in an ordinary Python string.&lt;/p&gt;

&lt;p&gt;The regex was therefore asking for text containing a backspace, followed by a short word, followed by "jak". No document in the world contains that. The rule could not match anything, ever, under any input.&lt;/p&gt;

&lt;h2&gt;
  
  
  Silence has two causes and one appearance
&lt;/h2&gt;

&lt;p&gt;Nothing about this looked wrong. The module imported. The pattern compiled without a warning, because a backspace is a perfectly legal character in a regular expression. The gate ran on every episode and returned an empty list of problems, which is precisely what a gate returns when the content is fine.&lt;/p&gt;

&lt;p&gt;That is the whole shape of the bug. A working check on clean input and a dead check on dirty input are the same event from the outside: no output. Every other gate I wrote that day was firing, which made the silence read as good news rather than as an absence of news.&lt;/p&gt;

&lt;p&gt;It would have shipped. The next automated run would have written an alphabet episode, put a flower on screen under the letter P, passed the letter rule, rendered, mastered, and left a finished file with a green checkmark next to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running the gate on known-bad input
&lt;/h2&gt;

&lt;p&gt;I did not find this by reading the code. I found it because I had a rule about new gates: before believing one, point it at material I already know is broken and confirm it complains.&lt;/p&gt;

&lt;p&gt;I had that material. A reviewer had watched the episodes and written down exactly what was wrong with each one, in plain language, days earlier. So the test was not synthetic and it was not a fixture I invented to match my own assumptions. It was seven real files and a list of seven real defects, and the question was simply whether the gates found the things a person had already found.&lt;/p&gt;

&lt;p&gt;Six of seven defects were caught. The seventh, the flower, was not, and that gap is what sent me to the pattern. The episode a reviewer had praised came back with zero problems, which was the other half of the evidence: the gates were not simply firing at random.&lt;/p&gt;

&lt;h2&gt;
  
  
  A new check is a hypothesis until it fails on purpose
&lt;/h2&gt;

&lt;p&gt;The general version, which I now apply to anything that reports problems: &lt;strong&gt;a check you have never seen fail is a check you have never seen work.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In practice that means three cheap habits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Keep the bad input.&lt;/strong&gt; When something is wrong in production, the artifact is worth more than the fix. It is the only test case you did not have to imagine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assert the positive.&lt;/strong&gt; A test that says "no problems on clean input" passes for a deleted function. Pair it with one that says "exactly this problem on this dirty input".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Print what compiled, not what you wrote.&lt;/strong&gt; Source and runtime disagree more often than you expect, especially anywhere a string passes through a template, an editor macro or a shell. Two lines of &lt;code&gt;repr()&lt;/code&gt; would have shown me the backspace immediately.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the same failure class as a monitoring dashboard that is green because its data source stopped: the indicator is not reporting health, it is reporting nothing, and nothing renders as fine. I wrote about three production outages with that exact shape in &lt;a href="https://dev.to/blog/silent-failures-agentic-pipeline/"&gt;an earlier post&lt;/a&gt;. This one was cheaper only because it was caught before it reached anybody.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gate found the bug twice
&lt;/h2&gt;

&lt;p&gt;Once the pattern was repaired, it flagged the flower in the alphabet episode and flagged it again in the weekly recap film, which had inherited the same claim. Two independent rules caught it: one comparing the props on screen against the letter being taught, one parsing the spoken claim itself.&lt;/p&gt;

&lt;p&gt;That redundancy was not planned. It is simply what happens when you write the rule as a statement about the world rather than as a statement about one file format, and it is the reason I now prefer gates that check claims over gates that check structure.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://neuragrowth.co/blog/gate-that-never-matched/" rel="noopener noreferrer"&gt;neuragrowth.co&lt;/a&gt;. I run a one-person digital-products studio and write up what breaks in production.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you write CLAUDE.md files, I keep a set of working templates here: &lt;a href="https://neuragrowth.co/free/claude-md-templates/" rel="noopener noreferrer"&gt;neuragrowth.co/free/claude-md-templates&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>testing</category>
      <category>python</category>
      <category>devops</category>
      <category>programming</category>
    </item>
    <item>
      <title>One Game Costs $9.99. The Next One Is Free.</title>
      <dc:creator>Robert</dc:creator>
      <pubDate>Fri, 21 Aug 2026 10:00:02 +0000</pubDate>
      <link>https://dev.to/robswierk/one-game-costs-999-the-next-one-is-free-5e9j</link>
      <guid>https://dev.to/robswierk/one-game-costs-999-the-next-one-is-free-5e9j</guid>
      <description>&lt;h2&gt;
  
  
  Same studio, same month, opposite price tags
&lt;/h2&gt;

&lt;p&gt;NeuraGrowth publishes two games right now, and a visitor comparing them side by side would be right to raise an eyebrow. One asks for money. The other asks for nothing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Grand Skat&lt;/strong&gt; is a classic Skat card game. It teaches you the rules across seven lessons, then lets you play offline against the computer or at the free online tables. It costs $9.99 on itch.io for the PC edition, and it is on the Microsoft Store for Windows. One purchase covers the premium features wherever you play. You can also play a free version in the browser at weldowo.pl without buying anything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Unity in Capsule&lt;/strong&gt; is a neon twin-stick survivors game. One capsule against endless waves, level-up perks, four modes, an online leaderboard. A run takes ten to fifteen minutes. It costs nothing. Not a trial, not a demo with the good half locked away. The whole game, free on itch.io, donations welcome if you feel like it.&lt;/p&gt;

&lt;p&gt;People assume the free one is bait for the paid one. It is not. They were priced by the same rule, and the rule produced two different answers.&lt;/p&gt;

&lt;h2&gt;
  
  
  A price tag is a claim that the thing is finished
&lt;/h2&gt;

&lt;p&gt;When you pay for something, you are not just buying features. You are buying the maker's statement that this is done, that it works, and that you will not be asked for more money later to make it whole. That claim is the actual product. Break it once and the next thing you publish starts from behind.&lt;/p&gt;

&lt;p&gt;Grand Skat can make that claim. The rules are complete, the lessons are written, the offline game works with no connection, and there is nothing behind a second paywall. It has a defined finish line and it crossed it.&lt;/p&gt;

&lt;p&gt;Unity in Capsule cannot make that claim yet, and pretending otherwise would be a lie told for $4. It is an early build. We are still adding to it. The honest version of that sentence is not "early access, $4.99, roadmap coming soon". The honest version is: it is free while it is growing, play it now, tell us what breaks.&lt;/p&gt;

&lt;h2&gt;
  
  
  A leaderboard with nobody on it is just a spreadsheet
&lt;/h2&gt;

&lt;p&gt;There is a practical reason on top of the honest one. Unity in Capsule has an online leaderboard with top scores for each of its four modes: Boss Hunt, Kill Count, Survival and Point Run. A leaderboard is only interesting when there are people on it. Charging admission to a scoreboard nobody has climbed yet is a good way to guarantee it stays empty.&lt;/p&gt;

&lt;p&gt;Grand Skat does not have that problem. Skat is a hundred-year-old game with a fixed rulebook. Its value does not depend on how many other people bought it this week. That difference in how the two games create value is exactly why one can carry a price on day one and the other cannot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four things our games will never do
&lt;/h2&gt;

&lt;p&gt;"Free" has been ruined by a decade of games that are free the way a fishing hook is free. So it is worth being specific about what we are not doing, because these are commitments, not current limitations.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No ads.&lt;/strong&gt; Not between runs, not on the death screen, not a rewarded video for one more life.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No in-app purchases.&lt;/strong&gt; There is no shop, no currency, no starter bundle. There is nothing to sell you inside the game because we did not build a hole for it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No energy timers.&lt;/strong&gt; You can play until you are bored. Nothing charges up while you are away.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No loot boxes and no paid seasons.&lt;/strong&gt; The rare golden perks that change a run outright, the allied minions, the support towers, the healing orb, the screen-clearing purge, are earned in the run. They are not sold.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If Unity in Capsule ever gets a price, it will be because it became a finished game worth one, and everyone who played it for free will still have the whole thing. The one arrangement we will not use is the one where the game is free but your attention is the product.&lt;/p&gt;

&lt;h2&gt;
  
  
  It is the same rule that prices the flashcards
&lt;/h2&gt;

&lt;p&gt;This is not a games policy. It is how the whole shop works, and the games just made it visible.&lt;/p&gt;

&lt;p&gt;Every printable and every guide in the catalogue is a one-time purchase. You download the file and it is yours. No subscription, no per-seat licence, no cloud account that can decide to stop working. The free sample decks on this site are complete small decks, not the first four cards of a deck you have to buy to finish. Same rule, different medium: pay once for something finished, or take the free thing that is genuinely free.&lt;/p&gt;

&lt;p&gt;A studio that sells a card game, a survivors game, bilingual flashcards and developer guides could easily end up with four different sets of ethics. Having one rule that survives contact with all four is worth more than any single price tag.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://neuragrowth.co/blog/one-game-paid-one-free/" rel="noopener noreferrer"&gt;neuragrowth.co&lt;/a&gt;. I run a one-person digital-products studio and write up what breaks in production.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you write CLAUDE.md files, I keep a set of working templates here: &lt;a href="https://neuragrowth.co/free/claude-md-templates/" rel="noopener noreferrer"&gt;neuragrowth.co/free/claude-md-templates&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>indiehackers</category>
      <category>gamedev</category>
      <category>business</category>
      <category>discuss</category>
    </item>
    <item>
      <title>No Roadmap, No Hype: How a Solo Studio Picks What to Build Next</title>
      <dc:creator>Robert</dc:creator>
      <pubDate>Thu, 20 Aug 2026 10:00:01 +0000</pubDate>
      <link>https://dev.to/robswierk/no-roadmap-no-hype-how-a-solo-studio-picks-what-to-build-next-340b</link>
      <guid>https://dev.to/robswierk/no-roadmap-no-hype-how-a-solo-studio-picks-what-to-build-next-340b</guid>
      <description>&lt;h2&gt;
  
  
  A plan you cannot keep is just a way to disappoint people
&lt;/h2&gt;

&lt;p&gt;Big shops publish roadmaps because they have teams that need coordinating and investors who want a story. I have neither. A public roadmap from a one-person studio is a list of promises I would end up breaking the first time reality voted differently, and a broken promise costs more trust than a missing plan ever would.&lt;/p&gt;

&lt;p&gt;So NeuraGrowth ships without announcing what is next. Not out of secrecy, out of honesty. I would rather show you a finished thing than pre-sell you a slide.&lt;/p&gt;

&lt;h2&gt;
  
  
  It starts as a private itch, not a market gap
&lt;/h2&gt;

&lt;p&gt;Every product began because I wanted it to exist for my own use. The bilingual decks because my own kid needed better cards. The dev guides because I kept re-solving the same Claude Code problems. The card game because the digital Skat I wanted to play was not good enough. Market-gap language comes later, in the listing. The real trigger is always personal.&lt;/p&gt;

&lt;p&gt;This is a filter, not a limitation. If I would not use it, I do not have the judgement to make it well, and a product I cannot judge is a product I should not sell.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ship the smallest complete version
&lt;/h2&gt;

&lt;p&gt;The goal is never the biggest version. It is the smallest one that is genuinely complete: a deck that teaches one topic properly, a guide that solves one problem end to end. Small and finished beats big and half-done every time, because a finished small thing teaches you something real the day it ships, and a big plan teaches you nothing until it is too late to change.&lt;/p&gt;

&lt;p&gt;Complete is the hard word. Small is easy. Small-and-complete means no obvious gap, no chapter I know is missing, nothing I would be embarrassed to show a buyer who read it closely. That bar is what keeps small from becoming thin.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let reality vote before you plan the sequel
&lt;/h2&gt;

&lt;p&gt;Once a thing is live, the next decision is not mine alone anymore. What sells, what gets a second look, what someone emails me about, those are votes. A deck that moves gets a sibling. A guide that goes quiet does not get a part two just because I sketched one. I would rather follow a small real signal than a big imagined one.&lt;/p&gt;

&lt;p&gt;This is why the range looks a little unpredictable from outside. It is supposed to. It follows demand and my own curiosity, not a slide I committed to in January. When both point the same way, that is the next build.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you give up, and why it is worth it
&lt;/h2&gt;

&lt;p&gt;No roadmap means you cannot pre-order the future, and I cannot hype one. Some people want that and I understand it. What you get instead is that nothing on this site is vaporware. If it has a page, it exists, it is finished, and one person is standing behind it. Everything else is just my next itch, and you will see it the day it is done, not the day I imagined it.&lt;/p&gt;

&lt;p&gt;If you want to know what is coming, the honest answer is: whatever I would want to buy next, shipped the moment it is complete. The newsletter is where it lands first. That is the whole plan, and it is the only one I can actually keep.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://neuragrowth.co/blog/no-roadmap-solo-studio/" rel="noopener noreferrer"&gt;neuragrowth.co&lt;/a&gt;. I run a one-person digital-products studio and write up what breaks in production.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you write CLAUDE.md files, I keep a set of working templates here: &lt;a href="https://neuragrowth.co/free/claude-md-templates/" rel="noopener noreferrer"&gt;neuragrowth.co/free/claude-md-templates&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>buildinpublic</category>
      <category>product</category>
      <category>software</category>
      <category>startup</category>
    </item>
    <item>
      <title>Why Bilingual EN+PL Beats Trilingual EN+ES+PL on Etsy (Live Data)</title>
      <dc:creator>Robert</dc:creator>
      <pubDate>Wed, 19 Aug 2026 10:00:01 +0000</pubDate>
      <link>https://dev.to/robswierk/why-bilingual-enpl-beats-trilingual-enespl-on-etsy-live-data-47j8</link>
      <guid>https://dev.to/robswierk/why-bilingual-enpl-beats-trilingual-enespl-on-etsy-live-data-47j8</guid>
      <description>&lt;h2&gt;
  
  
  Three numbers, same campaign
&lt;/h2&gt;

&lt;p&gt;We ran the same Etsy Ads budget across fifteen flashcard listings over a recent two-week window. The three of those listings most relevant to this post:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;World Flags Watercolor Flashcards (EN+PL bilingual):&lt;/strong&gt; 225 impressions, 5 clicks, &lt;strong&gt;2.2 percent CTR&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bilingual Community Helpers Flashcards (EN+PL):&lt;/strong&gt; 151 impressions, 3 clicks, &lt;strong&gt;2.0 percent CTR&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;30-Day Bilingual Coloring Book for Toddlers (EN+ES+PL trilingual):&lt;/strong&gt; 83 impressions, 1 click, &lt;strong&gt;1.2 percent CTR&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The trilingual listing did not get less impressions because it was less promoted. It pulled less clicks per impression. A buyer scrolling Etsy looked at it and moved on roughly twice as often as the bilingual cards. Same shop, same audience filter, same kind of cover art, same price range.&lt;/p&gt;

&lt;h2&gt;
  
  
  More languages should mean more buyers
&lt;/h2&gt;

&lt;p&gt;The naive read of the bilingual vs trilingual gap is: more languages, wider audience. A Polish-Spanish family in California buys it. A Spanish-Polish-American household buys it. Three languages must reach more households than two.&lt;/p&gt;

&lt;p&gt;The numbers say no. The three-language listing reaches fewer click-ready buyers per impression. The instinct is wrong because the buyer is not selecting on language coverage. The buyer is selecting on whether the deck matches their specific home.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two clear audiences beats three blurry ones
&lt;/h2&gt;

&lt;p&gt;An EN+PL flashcard deck has a single, instantly-recognisable audience. A Polish parent abroad teaching their kid Polish on top of English. The Etsy thumbnail says "this is exactly for me" in a quarter of a second. The buyer clicks before they read the description.&lt;/p&gt;

&lt;p&gt;An EN+ES+PL deck has a fuzzier signal. Is it for a Spanish home learning Polish and English. For a Polish home learning Spanish. For a multicultural classroom. The buyer scrolling Etsy can not tell, in the quarter-second they get on the thumbnail, whether this product is for them. That moment of "I'm not sure" is exactly when they scroll past.&lt;/p&gt;

&lt;p&gt;The third language did not subtract value. It subtracted clarity. And in a 220-pixel thumbnail competing for 0.25 seconds of attention, clarity is the entire game.&lt;/p&gt;

&lt;h2&gt;
  
  
  Numerical specificity stacks on top of language clarity
&lt;/h2&gt;

&lt;p&gt;The bilingual winners share a second pattern. Every one of them has a specific number in the title: 54 countries. 48 mythology cards. 80 vocabulary cards. Each number tightens the audience further. A 54-country deck is for a parent who wants comprehensive geography exposure. A 48-card mythology deck is for the parent of a kid who is into Greek heroes specifically.&lt;/p&gt;

&lt;p&gt;The numbers are not boasting. They are filtering. Anyone whose use case does not match the number scrolls past, and that is exactly what the listing wants. The remaining ten percent are pre-qualified buyers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Specificity is not a niche risk, it is the conversion
&lt;/h2&gt;

&lt;p&gt;Most home-printable shops hedge. They list "bilingual cards" without naming the languages, or "kids deck" without naming the age range, or "vocabulary" without naming the count. They are protecting themselves from looking too narrow. The data says the opposite: the broader the framing, the lower the click-through.&lt;/p&gt;

&lt;p&gt;What scrolling a buyer wants in 0.25 seconds is not "this MIGHT be for me". It is "this IS for me". The way to deliver that signal is to be more specific, not less.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five small calls based on the same data
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Bilingual EN+PL is the canonical format for the kid catalogue.&lt;/strong&gt; Adding a third language has to clear a much higher bar than "why not". If we cannot name the parent at the moment of click, we keep it at two languages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every kid deck title carries a number.&lt;/strong&gt; 24 vocabulary cards. 48 mythology cards. 7 continents. The deck has to actually contain that count, but if it does, the number goes in the title.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every kid deck title carries an explicit age range.&lt;/strong&gt; "Ages 7 to 11" or "Ages 3 to 5". Vague "for kids" loses to specific ranges in the same ad budget by another quarter of a CTR point.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trilingual is a separate product line, not an upgrade.&lt;/strong&gt; If we make a trilingual deck, it gets a different SEO angle than the bilingual one. Not "bilingual but better", a different positioning aimed at the multi-language home directly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The data review is weekly, not monthly.&lt;/strong&gt; Two weeks of campaign data was enough to surface this gap. We re-pull and re-decide every Wednesday, not at end of quarter.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Trilingual decks have a role, just not this one
&lt;/h2&gt;

&lt;p&gt;Nothing in the numbers above says trilingual products are bad. They have a real audience: the multi-language home that explicitly wants three languages on one card. That household exists, that household buys, and we will keep serving it.&lt;/p&gt;

&lt;p&gt;What the numbers say is that for the broad Etsy search browse path, a trilingual listing does not win against a bilingual listing on click-through. The conclusion is to position trilingual decks differently, not to make fewer of them. Different framing, different keyword set, different ad copy. Same way "Ages 3 to 5" and "Ages 7 to 11" are not the same product even when they cover the same topic.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you sell printables too, the framework transfers
&lt;/h2&gt;

&lt;p&gt;The numbers above are from our shop, but the click-through dynamics are not unique to bilingual flashcards. Three questions to run against your own catalogue.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Can the buyer name themselves in the thumbnail.&lt;/strong&gt; If a Polish parent abroad sees your card and recognises themselves in a quarter of a second, the click-through is high. If they have to read the description to find out whether the deck is for them, you have already lost most of the impression budget.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Does the title contain a filtering number.&lt;/strong&gt; If yes, every uninterested buyer self-eliminates and you do not pay for their impression to convert. If no, you are paying for impressions that scroll past.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Does the title contain an age range.&lt;/strong&gt; "Ages 6 to 10" performs in our shop at roughly the same lift as a counted number. Both filter the audience, both raise click-through.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is sophisticated. The data above is fifteen listings over two weeks on a small shop. But the gap between bilingual and trilingual on the same budget is large enough that we are willing to change the catalogue around it. Maybe you will look at your own numbers and find the same.&lt;/p&gt;

&lt;h2&gt;
  
  
  Clear is the conversion lever, not loud
&lt;/h2&gt;

&lt;p&gt;A common piece of advice for new Etsy shops is to be bolder. Bigger thumbnails, brighter colours, more keywords stuffed into the title. The data from our shop says a different lever pays more: be clearer about who the deck is for and what is in it. The buyer is not asking for louder. They are asking for an immediate answer to "is this for me". When the answer arrives in a quarter of a second, the click happens. When it does not, the impression goes nowhere.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://neuragrowth.co/blog/bilingual-beats-trilingual-etsy/" rel="noopener noreferrer"&gt;neuragrowth.co&lt;/a&gt;. I run a one-person digital-products studio and write up what breaks in production.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you write CLAUDE.md files, I keep a set of working templates here: &lt;a href="https://neuragrowth.co/free/claude-md-templates/" rel="noopener noreferrer"&gt;neuragrowth.co/free/claude-md-templates&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>data</category>
      <category>analytics</category>
      <category>indiehackers</category>
      <category>ai</category>
    </item>
    <item>
      <title>Behind Every NeuraGrowth Cover: Watercolor and AI, Honest Workflow</title>
      <dc:creator>Robert</dc:creator>
      <pubDate>Tue, 18 Aug 2026 10:00:01 +0000</pubDate>
      <link>https://dev.to/robswierk/behind-every-neuragrowth-cover-watercolor-and-ai-honest-workflow-3ifo</link>
      <guid>https://dev.to/robswierk/behind-every-neuragrowth-cover-watercolor-and-ai-honest-workflow-3ifo</guid>
      <description>&lt;h2&gt;
  
  
  If you ask "did a human paint this", you deserve a straight answer
&lt;/h2&gt;

&lt;p&gt;Etsy now requires AI-assisted listings to say so. Most printable shops respond with a one-line disclosure in tiny type at the bottom of the description and call it done. That is technically compliant and also a missed chance to actually explain anything to the parent on the other end of the transaction.&lt;/p&gt;

&lt;p&gt;So instead of a disclosure paragraph, here is a full walkthrough. Same workflow on every cover, no curated exceptions. If at any step you decide the result is dishonest about the labour involved, you tell me and I will rethink the step. The whole point of running this solo is that there is exactly one inbox to write to.&lt;/p&gt;

&lt;h2&gt;
  
  
  The art brief is the long part
&lt;/h2&gt;

&lt;p&gt;Every product starts with an art brief I write by hand. Not a Pinterest mood board. A short specification: dominant subject, framing, palette family (we have three, all warm), illustration density, what the cover must communicate at thumbnail size on an Etsy search page where the visitor sees roughly 220 pixels of total information before they decide whether to click.&lt;/p&gt;

&lt;p&gt;That brief is maybe a hundred words long and it takes me an hour to write well, because it is the only step where my taste is the entire input. AI assist does not help here. If the brief is vague, the cover will be vague.&lt;/p&gt;

&lt;h2&gt;
  
  
  The render is where AI does heavy lifting
&lt;/h2&gt;

&lt;p&gt;I feed the brief into a custom rendering pipeline built on top of a few diffusion models tuned for watercolor and line-art outputs. The pipeline generates four candidates at a time and I cull aggressively. Most decks need eight to twelve rounds before a cover passes my own bar, which means thirty to fifty rejected images sit in the trash for every one that ships.&lt;/p&gt;

&lt;p&gt;The AI did the brushwork. I am not going to pretend a human picked up a brush. What I will tell you is that the prompt, the candidate culling, and the cover composition are decisions I make every single time, and I refuse covers where the AI got too clever and inserted a sixth finger, a misaligned eye, or a colour palette that drifts off our three warm families.&lt;/p&gt;

&lt;h2&gt;
  
  
  The colour pass is non-negotiable
&lt;/h2&gt;

&lt;p&gt;AI renders out of the box have a tell. Too many simultaneous focal points, off-brand cyan slipping into a warm-palette piece, contrast that looks great on a 4K monitor but mush on a 6-inch listing thumbnail. So every accepted render goes through a manual colour pass: shadows clipped, contrast lifted, off-brand hues nudged out, and the focal subject sharpened.&lt;/p&gt;

&lt;p&gt;On a typical cover this is a fifteen-minute pass per image. On a deck like Solar System Brain Deck, where every planet needs to read distinctly at thumbnail size on a 40-card spread, the colour pass is closer to two hours of work across the deck. This is the step that separates a stock-feeling AI image from something that looks like the deck was art-directed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reject or ship, no in-between
&lt;/h2&gt;

&lt;p&gt;The last step is a single yes-or-no question I ask myself before a cover leaves the build folder. Would I put my name on this on Etsy. Not "is this good enough for the price point". Just: name on it, yes or no.&lt;/p&gt;

&lt;p&gt;A surprising amount gets rejected here, even after passing steps two and three. Sometimes it is a vibe thing: the deck reads as cheerful but the cover reads as flat. Sometimes the subject is technically perfect but it does not match the rest of the deck. Whatever the reason, if I would not put my name on it, the cover goes back to step two and we re-roll. The deadline for a drop is always later than this gate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three places I refuse to let the pipeline drive
&lt;/h2&gt;

&lt;p&gt;People ask me where the automation stops. Three places where the AI pipeline is locked out by design, even though it would technically be cheaper to let it run.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Subject choice.&lt;/strong&gt; What goes on the cover is a decision about what the product is for. That is a brand call, not a render call. AI does not pick our subjects, ever.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The ship-or-reject gate.&lt;/strong&gt; No automated quality threshold replaces the yes-or-no on whether I would put my name on the result. If it ever became automated, the brand would die quietly inside a year.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The reply when something is wrong.&lt;/strong&gt; If you email about a cover that disappointed you, the reply comes from me. Not a template. Not a bot. The person who picked the subject and pressed ship is the person who answers when it did not land.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Hybrid is not a compromise, it is the work
&lt;/h2&gt;

&lt;p&gt;The honest framing of a NeuraGrowth cover is: a custom AI pipeline did the painterly work that would have taken a freelance illustrator six hours per cover, and I did the brand-direction work that no AI can do for me yet. Both are real labour. Both make the cover what it is. Saying "AI made it" undersells the brief and the colour pass. Saying "I made it by hand" would be a straight lie about the brushwork.&lt;/p&gt;

&lt;p&gt;So we say hybrid, in plain English, in every listing. The watercolor is real watercolor-style rendering. The decisions about what to put on the cover and whether it ships are mine. If at any point you want a deeper look at one of the covers, write me, and I will walk you through which step did what for that specific piece.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://neuragrowth.co/blog/behind-neuragrowth-watercolor-ai/" rel="noopener noreferrer"&gt;neuragrowth.co&lt;/a&gt;. I run a one-person digital-products studio and write up what breaks in production.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you write CLAUDE.md files, I keep a set of working templates here: &lt;a href="https://neuragrowth.co/free/claude-md-templates/" rel="noopener noreferrer"&gt;neuragrowth.co/free/claude-md-templates&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>automation</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
