<?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: Philip Shaw</title>
    <description>The latest articles on DEV Community by Philip Shaw (@virorum).</description>
    <link>https://dev.to/virorum</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%2F4013836%2Fe5cca143-b113-4aa0-909c-5ae3ceddc5af.png</url>
      <title>DEV Community: Philip Shaw</title>
      <link>https://dev.to/virorum</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/virorum"/>
    <language>en</language>
    <item>
      <title>Notes to Self: The Interview Between an Issue and a Spec</title>
      <dc:creator>Philip Shaw</dc:creator>
      <pubDate>Fri, 14 Aug 2026 12:25:24 +0000</pubDate>
      <link>https://dev.to/virorum/notes-to-self-the-interview-between-an-issue-and-a-spec-4phc</link>
      <guid>https://dev.to/virorum/notes-to-self-the-interview-between-an-issue-and-a-spec-4phc</guid>
      <description>&lt;p&gt;On 1 August I opened an issue that was three sentences long. A hundred and one minutes later&lt;br&gt;
the feature was merged, and the document that got it there ran to&lt;br&gt;
&lt;a href="https://glitchedpixel.io/specs/feat-0007/" rel="noopener noreferrer"&gt;457 lines&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I didn't write those 457 lines. In fact, I didn't &lt;em&gt;have&lt;/em&gt; to write any more documentation, and not because I simply allowed Claude to run amok.&lt;/p&gt;

&lt;p&gt;Here is the issue in full — &lt;code&gt;control-api#265&lt;/code&gt;, 225 characters:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;control-api#265 — Manifest-backed dashboard feeds&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;For each dashboard, auto create a manifest keyed by dashboard_id.

For each sensor the dashboard uses, tag it to be included in the manifest.
When a dashboard definition is updated, add / remove tags from sensors accordingly.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From that genesis moment, this is the lifecycle of the issue all the way through to landing:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Time (UTC)&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;14:25&lt;/td&gt;
&lt;td&gt;Issue #265 opened — 225 characters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;14:54&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;FEAT-0007&lt;/code&gt; spec committed — 457 lines&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;15:35&lt;/td&gt;
&lt;td&gt;Spec merged (PR #266)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;15:51&lt;/td&gt;
&lt;td&gt;Implementation committed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16:06&lt;/td&gt;
&lt;td&gt;Implementation merged (PR #267, 15 files), issue closed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The interesting part isn't the speed. It's the step at 14:54 that landed a previously non-existent spec document, and what happened in the twenty-nine minutes before it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The issue was never a specification
&lt;/h2&gt;

&lt;p&gt;I often write issues like this one...the way most people write shopping lists. &lt;em&gt;Actuator address is not ensured?&lt;/em&gt;&lt;br&gt;
&lt;em&gt;Baseline the trace correctly.&lt;/em&gt; &lt;em&gt;With the pre-rolls, the frame-rate looks out.&lt;/em&gt; They're abbreviated to&lt;br&gt;
the point of being cryptic to everyone else. I write them this way deliberately: I'm usually mid-something&lt;br&gt;
else when I notice a problem, or have an idea for a better route to the solution. The cost of a full write-up right at that moment would be a fractured sense of flow. As most engineers will tell you, the transitions into and out of flow are the most disruptive parts of their working day.&lt;/p&gt;

&lt;p&gt;This terse form of issue-writing can be all you need, and it's worth being precise about &lt;em&gt;why&lt;/em&gt; it works and the trade-offs it includes. It is &lt;strong&gt;not&lt;/strong&gt; because "the issues are good enough".&lt;/p&gt;

&lt;p&gt;They aren't. When you pick one of these up later, you can reconstruct everything missing from memory.&lt;/p&gt;

&lt;p&gt;In my case with control-api#265, it was a particular panel type I was thinking about, a constraint I hit last week with it, the reason the&lt;br&gt;
obvious approach was off the table, etc... That reconstruction was free and, more importantly, it was&lt;br&gt;
&lt;strong&gt;invisible&lt;/strong&gt; to any external observer. When you are the issue filer and its eventual closer, you never experience this rehydration as filling a gap. You see the issue's sparse wording as an aide-memoire for the fuller context that is still cached in your head.&lt;/p&gt;

&lt;p&gt;What bites here is when you work in a team (of other engineers or coding agents): something has to bridge this gap or the issue will get resolved using a foundation of well intentioned misinterpretation and apparently sensible assumptions.&lt;/p&gt;

&lt;p&gt;An agent has something both better and worse than an engineer's recall — it can read&lt;br&gt;
every file in the repository, including ones you have forgotten, and it can read exactly nothing&lt;br&gt;
of your latent intentions.&lt;/p&gt;

&lt;p&gt;Fortunately, the fix is not to write longer issues. Capture is working; it's the handoff that's broken, the context sync.&lt;/p&gt;
&lt;h2&gt;
  
  
  An agent as interviewer asks for decisions, not descriptions
&lt;/h2&gt;

&lt;p&gt;The obvious version of this step — "the agent asks clarifying questions" — undersells it, and&lt;br&gt;
would be annoying in practice. Being interrogated about your own bug report is worse than&lt;br&gt;
writing the report properly in the first place.&lt;/p&gt;

&lt;p&gt;What makes it worth doing is the ordering. The agent goes and reads first, then comes back&lt;br&gt;
with what it found and asks you to &lt;em&gt;decide&lt;/em&gt; — not to &lt;em&gt;describe&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;You can see the split in the spec that grew out of my dashboard issue. Its reference list is entirely material the&lt;br&gt;
agent uncovered on its own:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;FEAT-0007 §2 — References (excerpt)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`app/repositories/sensor_repo.py:123-126`&lt;/span&gt; (&lt;span class="sb"&gt;`get_sensors_with_tag`&lt;/span&gt;, the JSONB
  &lt;span class="sb"&gt;`.contains()`&lt;/span&gt; query pattern this spec's reconciliation reuses)
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`app/services/sensor_service.py:42-84,131-174`&lt;/span&gt; (&lt;span class="sb"&gt;`upsert_sensor`&lt;/span&gt; /
  &lt;span class="sb"&gt;`_fan_out_upsert`&lt;/span&gt; — precedent for both the reserved-tag-prefix guard and the
  "best-effort, must never fail the primary write" fan-out posture this spec copies)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nobody asked me where the JSONB containment query lived, or which existing service had already&lt;br&gt;
settled the question of what a fan-out failure should do to the primary write. Those are&lt;br&gt;
enrichment lookups, and lookups are the agent's half of the job.&lt;/p&gt;

&lt;p&gt;What came back to me as decisions to make were the things no amount of reading resolves: whether one SSE stream per&lt;br&gt;
dashboard was actually the point, whether a second repository was allowed to be changed for&lt;br&gt;
this, where the boundary of the work sat. The spec's &lt;em&gt;Out of Scope&lt;/em&gt; section is five such&lt;br&gt;
rulings written down.&lt;/p&gt;

&lt;p&gt;That's the trade that makes the interview cheap enough to bother with. The expensive half is&lt;br&gt;
already done by the time it reaches you.&lt;/p&gt;
&lt;h2&gt;
  
  
  The best question contradicted the issue
&lt;/h2&gt;

&lt;p&gt;Read my second sentence again: &lt;em&gt;for each sensor the dashboard uses, tag it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;There's an assumption buried in there — that the control-api server can tell which sensors a dashboard&lt;br&gt;
uses. It can't, and the spec says so at length:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;FEAT-0007 — Design Decision (excerpt)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;The obvious-looking alternative — have control-api decode &lt;span class="sb"&gt;`layout`&lt;/span&gt; itself and derive
the sensor set — does not actually work cleanly, for one panel type specifically.
&lt;span class="sb"&gt;`chart`&lt;/span&gt;/&lt;span class="sb"&gt;`gauge`&lt;/span&gt; panels store &lt;span class="sb"&gt;`series[].{deviceId,sensorRef}`&lt;/span&gt; plus a panel-level
&lt;span class="sb"&gt;`dataType`&lt;/span&gt;, which &lt;span class="ge"&gt;*would*&lt;/span&gt; parse cleanly. But a &lt;span class="sb"&gt;`health_stats`&lt;/span&gt; panel stores only
&lt;span class="sb"&gt;`healthStats: {bodyDeviceId?, heartRateDeviceId?}`&lt;/span&gt; (confirmed in
&lt;span class="sb"&gt;`serializeDashboardLayout`&lt;/span&gt;) — its 7 &lt;span class="sb"&gt;`sensor_ref`&lt;/span&gt;s are frontend constants
(&lt;span class="sb"&gt;`BODY_SENSOR_REFS`&lt;/span&gt;, &lt;span class="sb"&gt;`HR_SENSOR_REF`&lt;/span&gt; in &lt;span class="sb"&gt;`useHealthStatsPanel.ts`&lt;/span&gt;), and each role's
&lt;span class="sb"&gt;`data_type`&lt;/span&gt; is discovered at render time from &lt;span class="sb"&gt;`listDeviceSensors`&lt;/span&gt;, not stored anywhere.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When I wrote the issue I was picturing chart panels, where my sentence is true. Health-stats&lt;br&gt;
panels store two device IDs and resolve the rest at render time in a &lt;em&gt;different repository&lt;/em&gt;.&lt;br&gt;
A server-side parser would have to duplicate a list of constants that already lives in the frontend,&lt;br&gt;
with no shared source of truth and no error when it happens to drift.&lt;/p&gt;

&lt;p&gt;So the spec proposes the opposite: the client sends an explicit &lt;code&gt;sensors&lt;/code&gt; list alongside the&lt;br&gt;
layout, and the server never parses the layout at all. That decision brings a new API rule&lt;br&gt;
with it — a &lt;code&gt;PATCH&lt;/code&gt; supplying &lt;code&gt;layout&lt;/code&gt; without &lt;code&gt;sensors&lt;/code&gt; is rejected with a &lt;code&gt;422&lt;/code&gt;, so a layout&lt;br&gt;
change can't silently leave the tag set stale.&lt;/p&gt;

&lt;p&gt;None of that is in my three sentences. One line of it directly contradicts them.&lt;/p&gt;

&lt;p&gt;This is the part I want to defend hardest. An interview process that only &lt;em&gt;elaborated&lt;/em&gt; what I wrote would be&lt;br&gt;
formatting. This one went and checked, found my premise didn't hold, and brought back a&lt;br&gt;
reversal — before any code existed to be wrong.&lt;/p&gt;
&lt;h2&gt;
  
  
  The numbering is the fossil record
&lt;/h2&gt;

&lt;p&gt;Something else survives in the finished document. Its requirements run FR-1 through FR-10 —&lt;br&gt;
but there's also an FR-6a, an FR-10a, an NFR-3a, an AC-8a and an AC-8b.&lt;/p&gt;

&lt;p&gt;A list written straight through numbers 1 to &lt;em&gt;n&lt;/em&gt;. Letters mean insertion into a list that had&lt;br&gt;
already been numbered. Each one marks a place where a later question forced something in&lt;br&gt;
between.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;FR-6a&lt;/code&gt; is the one I'd point at. &lt;code&gt;FR-6&lt;/code&gt; had established that dashboard tags are reserved, so a&lt;br&gt;
caller's tag edit must preserve them. Then:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;FEAT-0007 — FR-6a (excerpt)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;The prior-tags read [...] is already wrapped in &lt;span class="sb"&gt;`try/except Exception:
logfire.exception(...)`&lt;/span&gt; so it can never break the upsert — but under FR-6, silently
treating a failed read as &lt;span class="sb"&gt;`old_tags = []`&lt;/span&gt; would mean &lt;span class="sb"&gt;`preserved`&lt;/span&gt; comes back empty and
the caller's list wins outright, wiping every &lt;span class="sb"&gt;`dashboard-*`&lt;/span&gt; tag on that sensor: the
exact outcome FR-6 exists to prevent.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An existing &lt;code&gt;try&lt;/code&gt;/&lt;code&gt;except&lt;/code&gt; in unrelated code turns the new requirement into precisely the&lt;br&gt;
failure it was written to stop. Follow FR-6 faithfully and you ship the bug. That is not a&lt;br&gt;
question anyone thinks to ask about a three-sentence issue, and it was answered in a spec document&lt;br&gt;
before any code was written rather than in a post-mortem.&lt;/p&gt;

&lt;p&gt;The whole document is published alongside this article — &lt;a href="https://glitchedpixel.io/specs/feat-0007/" rel="noopener noreferrer"&gt;FEAT-0007: Manifest-Backed Dashboard&lt;br&gt;
Feeds&lt;/a&gt; — so the excerpts above can be read in context.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the spec bought afterwards
&lt;/h2&gt;

&lt;p&gt;Implementation merged thirty-one minutes after the spec did, across 15 files. What I want to&lt;br&gt;
draw out isn't the speed — it's this line from the implementation PR's test plan:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;AC-8b (migration's &lt;code&gt;server_default&lt;/code&gt; behaving correctly against a pre-populated table) has no&lt;br&gt;
automated test — this repo has no migration-test harness (integration tests use&lt;br&gt;
&lt;code&gt;metadata.create_all&lt;/code&gt;, not Alembic). Verified by construction: [...]&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;One acceptance criterion, honestly marked unmet, with the reason. That's only possible because&lt;br&gt;
AC-8b existed to be unmet. Without the spec there'd be no gap — just a migration nobody had&lt;br&gt;
thought to test, and no record that anyone noticed.&lt;/p&gt;

&lt;p&gt;A spec's most underrated property is that it gives the work something to fail against.&lt;/p&gt;

&lt;h2&gt;
  
  
  Doing it on purpose
&lt;/h2&gt;

&lt;p&gt;The shape I'd suggest, in the order that matters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Keep writing bald issues.&lt;/strong&gt; The three-sentence note is doing its job. Don't move the cost
back to capture time; you'll just stop capturing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Put the interview at triage pickup, not at capture.&lt;/strong&gt; This is the whole trick. At pickup you're
already thinking about the problem and the agent can read the code &lt;em&gt;now&lt;/em&gt;, against the repo as
it currently is rather than as it was when you filed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make the agent read before it asks.&lt;/strong&gt; Questions that arrive with file paths attached are
answerable in a sentence. Questions that arrive cold are homework.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scale the output to the work.&lt;/strong&gt; A small change wants an implementation-plan comment on the
issue itself. A feature wants a numbered document — mine live at
&lt;code&gt;docs/specs/FEAT-NNNN-&amp;lt;slug&amp;gt;.md&lt;/code&gt;, and get their own PR ahead of the implementation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write it back where the issue lives.&lt;/strong&gt; This one I learned by losing it. Enrichment that
stays in the session dies with the session — the issue still reads like three sentences a
month later, and the next pickup starts from nothing. &lt;code&gt;FEAT-0007&lt;/code&gt; survives because it was
committed; plenty of equally good reasoning hasn't.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the honest limits. This costs about half an hour of agent time and a handful of real&lt;br&gt;
decisions from you, which is absurd overhead for a typo and roughly break-even for a&lt;br&gt;
one-file fix. It leans hard on the agent being able to read &lt;em&gt;everything&lt;/em&gt; relevant — the&lt;br&gt;
insight that reversed my issue came out of a sibling repository, and an agent without that&lt;br&gt;
repository would have cheerfully specified the parser. And it can't manufacture intent: if&lt;br&gt;
you genuinely don't know what you wanted, the interview surfaces that rather than solving it.&lt;br&gt;
Which is still better than finding out from the diff.&lt;/p&gt;

&lt;p&gt;None of this is a claim that issues should be specifications. Mine aren't and won't be. The&lt;br&gt;
issue is a pointer, written in the two minutes I could spare, and it's the right artifact for&lt;br&gt;
that job.&lt;/p&gt;

&lt;p&gt;The interview is just the step that dereferences it — the one I used to run silently, in my&lt;br&gt;
own head, without ever noticing I was running it at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Applicable to Coding Agents &lt;strong&gt;and&lt;/strong&gt; Engineers
&lt;/h2&gt;

&lt;p&gt;None of the above is specific to working with coding agents. The agent-led interview that spawned the spec doc is recognisably a tech review with a peer, team lead or stakeholder.&lt;/p&gt;

&lt;p&gt;AI assisted software development is not so much about learning entirely new paradigms as it is about having the immediate opportunity to enlist the expertise you need to do a job well.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>documentation</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>Trust, Doubt, and a Production Rate-Limit Incident</title>
      <dc:creator>Philip Shaw</dc:creator>
      <pubDate>Sun, 12 Jul 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/virorum/trust-doubt-and-a-production-rate-limit-incident-4l1c</link>
      <guid>https://dev.to/virorum/trust-doubt-and-a-production-rate-limit-incident-4l1c</guid>
      <description>&lt;p&gt;A production incident post-mortem where the debugging partner was an AI agent — the real story is when I believed it, when I didn't, and why that mattered.&lt;/p&gt;

</description>
      <category>debugging</category>
      <category>incidentreview</category>
      <category>observability</category>
    </item>
    <item>
      <title>Quieting 4xx noise in FastAPI + Logfire without losing your 500s</title>
      <dc:creator>Philip Shaw</dc:creator>
      <pubDate>Wed, 01 Jul 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/virorum/quieting-4xx-noise-in-fastapi-logfire-without-losing-your-500s-4f8k</link>
      <guid>https://dev.to/virorum/quieting-4xx-noise-in-fastapi-logfire-without-losing-your-500s-4f8k</guid>
      <description>&lt;p&gt;A one-line APIRoute fix for a non-obvious interaction between FastAPI's exception handling and Logfire's tracing instrumentation.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Your CLAUDE.md Is an Architecture Document — Write It Like One</title>
      <dc:creator>Philip Shaw</dc:creator>
      <pubDate>Tue, 30 Jun 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/virorum/your-claudemd-is-an-architecture-document-write-it-like-one-3pn3</link>
      <guid>https://dev.to/virorum/your-claudemd-is-an-architecture-document-write-it-like-one-3pn3</guid>
      <description>&lt;p&gt;The patterns worth stealing from a FastAPI project whose agent-instructions file quietly became its architecture doc — layering, config quarantine, the DI trap that breaks transactions, and more.&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>codingagents</category>
      <category>fastapi</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
