<?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>Sentinel: An Introduction</title>
      <dc:creator>Philip Shaw</dc:creator>
      <pubDate>Wed, 02 Sep 2026 09:22:19 +0000</pubDate>
      <link>https://dev.to/virorum/sentinel-an-introduction-2ldd</link>
      <guid>https://dev.to/virorum/sentinel-an-introduction-2ldd</guid>
      <description>&lt;p&gt;import PlaneArchitecture from '../../assets/sentinel-plane-architecture.svg';&lt;/p&gt;

&lt;p&gt;Does it matter if a soil moisture sensor goes quiet for three days?&lt;/p&gt;

&lt;p&gt;On its own, probably not. But if an irrigation decision is derived from that reading, three days of silence is three days of watering against a number that stopped being true, and nothing in the system said so.&lt;/p&gt;

&lt;p&gt;Move up the scale. Indoor air quality is degrading because the air conditioning has stopped working — not thrown an error, stopped, which from the outside looks identical to an air conditioner with nothing to do. Move up again. A battery bank charging off solar, in an enclosed space, begins to climb in temperature. Do you want to know about that now, or the next time you happen to look at a dashboard?&lt;/p&gt;

&lt;p&gt;Those three questions have the same shape but completely different answers and priorities. What separates them is not technical, it's a judgement about what matters, how much, and how quickly. That call gets made somewhere regardless. Written down, it is a specification. Left unwritten, it is whatever the code or user happened to do.&lt;/p&gt;


&lt;p&gt;&lt;strong&gt;Problem Statement:&lt;/strong&gt; Underneath all three questions is the same problem: a system that cannot tell a reading from a memory of one. A value that arrived three days ago still presents as a value, a device that has stopped reporting looks like a device with nothing to report, and a command that was sent looks like a command that worked — none of it announcing itself, all of it producing plausible numbers and unbroken graphs right up until a decision gets made against something that stopped being true. Sentinel exists to make that difference explicit at every hop, so that acting on a number the system cannot vouch for is a choice rather than an accident.&lt;/p&gt;

&lt;p&gt;There is an obvious objection, which is that this is a solved problem and you can buy it. However, I have spent a long time cajoling systems and their processes to fit around real world situations they were not designed to address — reframing the problem until it fitted the tool. That works until the reshaping costs more than the tool saves, and at that point it is worth exploring something with the flexibility those use-cases actually need.&lt;/p&gt;

&lt;p&gt;So this design is written down, and in public.&lt;/p&gt;

&lt;p&gt;A specification at this scale is not trivially derived from a set of working assumptions; the assumptions are the easy part, and they usually create a false sense of simplicity that unravels the moment you think the work is finished.&lt;/p&gt;

&lt;p&gt;Publishing holds me to the choices I made during the design. Not as choices I must live with, but ones that I must litigate clearly when I adopt or change them. What I decided, what I claimed they would achieve, and what had to change when the design met the thing it describes.&lt;/p&gt;

&lt;p&gt;That pressure is the same in a one-maintainer repository as it is in a corporate monorepo, and so are the failure modes. Arguing the changes in public makes each one a worked example rather than a commit message.&lt;/p&gt;

&lt;p&gt;The spec is here to be read and argued with.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Sentinel?
&lt;/h2&gt;

&lt;p&gt;A platform for monitoring and controlling devices across several network segments: collecting readings over mixed protocols, normalising them into a single semantic model, deriving values from them, evaluating automation rules, and driving physical outputs with verification and safety interlocks.&lt;/p&gt;

&lt;p&gt;The name is the job. A sentinel stands watch, knows the difference between quiet and absent, and raises an alarm rather than acting on a guess. Two properties of this design follow that description more closely than they follow any conventional monitoring platform: data quality is a first-class field, so the system is explicit about what it does not know; and absence of data is a signal in its own right rather than a gap to be interpolated over.&lt;/p&gt;

&lt;p&gt;It is written for a system built and operated by one person, and several decisions follow from that constraint rather than from first principles. Where they do, the constraint is stated alongside the decision.&lt;/p&gt;

&lt;p&gt;Components take the system name as a prefix: &lt;code&gt;sentinel-core&lt;/code&gt;, &lt;code&gt;sentinel-driver-modbus&lt;/code&gt;, &lt;code&gt;sentinel-registry&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How this series is organised
&lt;/h2&gt;

&lt;p&gt;Eleven parts, published weekly. This one is the map and the rationale for Sentinel's design; the ten that follow are the documents that define it.&lt;/p&gt;

&lt;p&gt;Every mechanism in this system has exactly one owning document. Where this part mentions a state set, a policy name, or a lifecycle, it names it only to say where it is defined.&lt;/p&gt;

&lt;p&gt;Each part is published as it was drafted, rather than as a tidied-up retrospective. The system is being built while the series runs, so where the plan meets reality and loses, a follow-up says what changed and why. Three figures in the design are unmeasured today — sustainable ingest throughput, cold-start time, and the retention policy values — and all three get measured during the build, inside the window this series runs in. You will see the numbers when I do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading order
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;em&gt;The Semantic Model&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Driver Contract&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Ingest Path &amp;amp; Log Schema&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Watchdog &amp;amp; Derived Points&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Command Plane&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Rules Engine&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Core Runtime&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Security Model&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Observability&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Retention &amp;amp; Compaction&lt;/em&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;em&gt;Core Runtime&lt;/em&gt;, &lt;em&gt;Security Model&lt;/em&gt; and &lt;em&gt;Observability&lt;/em&gt; come after the mechanisms they constrain: what enforces single-instance operation and what the system is sized for, which boundary carries the authorisation weight, and what signals every mechanism must expose.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Retention &amp;amp; Compaction&lt;/em&gt; reads last because it uses descriptor fields defined across four earlier documents, but it is a mechanism rather than a policy appendix — it constrains the log schema, the derived-point layer and the reprocess operation, and part of it cannot be deferred without a data migration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scope
&lt;/h2&gt;

&lt;p&gt;These ten documents cover the core and the data pipeline: collection, ingest, state, derivation, automation, control, runtime, security, instrumentation and storage lifecycle.&lt;/p&gt;

&lt;p&gt;Two things have been deliberately left out, because neither changes anything above: &lt;strong&gt;alert routing and notification delivery&lt;/strong&gt;, and the &lt;strong&gt;read API and UI&lt;/strong&gt;. &lt;em&gt;Observability&lt;/em&gt; says what must be watchable and what should raise an alarm; but &lt;em&gt;how&lt;/em&gt; an alarm reaches a person is not specified here. &lt;em&gt;Rules Engine&lt;/em&gt; names actions with a channel; the channel model is not specified here either.&lt;/p&gt;

&lt;p&gt;The one constraint those inherit: alerting runs outside the core, because alerting inside the core cannot report the core's absence.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two decisions that shape everything else
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Log-first core.&lt;/strong&gt; Current state is a projection of an append-only event log, never the primary artefact. Every observation, quality transition, command and command outcome is an appended event. Restart is snapshot plus replay of the tail. This is the whole answer to restarting without losing state, and retrofitting it later is painful, so it goes in first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Hard driver/core boundary.&lt;/strong&gt; The core has zero protocol knowledge. Drivers have zero knowledge of rules, units or storage. The boundary is exactly two contracts: a normalised observation flowing north, and a command envelope flowing south. The failure this prevents is an observability or collection concern leaking outward into the layers around it, so that every part of the system ends up coupled to whichever part was built first. Making the boundary a wire protocol between processes means it cannot be casually violated.&lt;/p&gt;

&lt;p&gt;Everything else in these documents is downstream of those two choices, and most of it follows fairly directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Non-goals
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;This is a system built from scratch, with no migration path in or out.&lt;/strong&gt; No document should acquire a migration section, a dual-run plan or a compatibility shim.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multiple concurrent cores are not a goal.&lt;/strong&gt; Exactly one core instance runs at a time, enforced rather than assumed. The reasoning, the enforcement mechanism, and the two conditions that would reopen the question are in &lt;em&gt;Core Runtime&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keeping raw data indefinitely is not a goal.&lt;/strong&gt; Nothing is retained in original sample detail forever; measurements are rolled up and their samples dropped. State transitions are a different thing and are kept — the distinction is samples versus history, and it is in &lt;em&gt;Retention &amp;amp; Compaction&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A second storage tier is not a goal.&lt;/strong&gt; No object-store archive, no cold tier. Data not worth keeping in Postgres is not worth keeping.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nothing here describes existing infrastructure.&lt;/strong&gt; These documents specify what to build. Where they name a protocol, a device or a deployment shape, that is a design target rather than an inventory, and no document should assert that any component currently exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  The planes
&lt;/h2&gt;



&lt;p&gt;The diagram is illustrative. This section is the normative description of the decomposition; where the two disagree, this text wins and the diagram should be corrected.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Devices&lt;/strong&gt; are anything that produces readings or accepts writes, over any protocol, push or poll.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Driver plane.&lt;/strong&gt; Protocol adapters, deployed as driver hosts, one per network segment, each supervising per-device driver instances. Drivers translate a protocol into observations and attempt writes. They hold no semantics, no verification state and no automation. Defined in &lt;em&gt;Driver Contract&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bus.&lt;/strong&gt; Durable subject streams carrying observations, availability, quarantine, commands and command results. Every driver-plane subject leads with the host, which is what makes per-host credentials workable. The observation stream is the system's outage buffer, bounded by size rather than age and sized from tolerable outage and utilisation — see &lt;em&gt;Core Runtime&lt;/em&gt;. Subject layout is defined in &lt;em&gt;Driver Contract&lt;/em&gt;; it is also the primary authorisation surface, so the two are designed together — see &lt;em&gt;Security Model&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Core.&lt;/strong&gt; A single process — see &lt;em&gt;Core Runtime&lt;/em&gt; — containing four stages that are logically distinct and physically together:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Normalise&lt;/em&gt; — validate, deduplicate, resolve against the registry, check the publisher, convert to canonical units, evaluate the deadband. Defined in &lt;em&gt;Ingest Path &amp;amp; Log Schema&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Event log&lt;/em&gt; — the durable append-only record, split by volume, retention class and query shape. Schema in &lt;em&gt;Ingest Path &amp;amp; Log Schema&lt;/em&gt;; the class split in &lt;em&gt;Retention &amp;amp; Compaction&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;State and registry&lt;/em&gt; — the current projection, plus the versioned registry that says what every point means. Projection defined in &lt;em&gt;Ingest Path &amp;amp; Log Schema&lt;/em&gt;; registry content defined in &lt;em&gt;The Semantic Model&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Derivation and watchdog&lt;/em&gt; — freshness deadlines and derived points, both of which emit into the same pipeline as device data. Defined in &lt;em&gt;Watchdog &amp;amp; Derived Points&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The diagram places derived points and timers inside the rules engine box. That is an over-simplification, and it is the one worth flagging, because it survived several drafts before the distinction it blurs turned out to matter. Derived points are ordinary points, computed before rules evaluate; the watchdog that ages them is not part of the engine at all. That separation is exactly what keeps the rules engine small, and it is argued properly in &lt;em&gt;Watchdog &amp;amp; Derived Points&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rules engine.&lt;/strong&gt; Evaluates predicates over transitions and produces claims. It sends no commands, holds no state between evaluations, and has no visibility into whether a claim reached hardware. Defined in &lt;em&gt;Rules Engine&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Command plane.&lt;/strong&gt; Owns the durable claim set and its resolution into a desired value, deliverability and reconciliation of reported against desired, confirmation and verification, and the safety and interlock model. Returns to the bus as commands to driver instances. Defined in &lt;em&gt;Command Plane&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The single closed loop is: device → driver → bus → core → rules → command plane → bus → driver → device, with the log recording every hop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scale
&lt;/h2&gt;

&lt;p&gt;The design targets, stated once here and derived from in &lt;em&gt;Core Runtime&lt;/em&gt;: 200+ devices across 16+ network segments, 2,000+ device timeseries points, 1,000+ derived points, and roughly 700 self-points that fall out of instrumenting the rest — about &lt;strong&gt;4,000 points&lt;/strong&gt; in total, at &lt;strong&gt;~220 observations per second&lt;/strong&gt;, designed for 500.&lt;/p&gt;

&lt;p&gt;Two figures matter more than their size suggests. Self-points are a fifth of the registry and are easy to omit when sizing anything. And sustainable ingest throughput is the one term still unmeasured, which makes it the term everything else waits on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where things are defined
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concern&lt;/th&gt;
&lt;th&gt;Owning document&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Point identity, hierarchy, descriptor fields&lt;/td&gt;
&lt;td&gt;The Semantic Model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Closed vocabularies: quantity, subject, enum types, retention class, modifiers&lt;/td&gt;
&lt;td&gt;The Semantic Model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Units, canonicalisation, affine conversion, aggregation safety&lt;/td&gt;
&lt;td&gt;The Semantic Model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quality states and legal transitions&lt;/td&gt;
&lt;td&gt;The Semantic Model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Timestamps and sequence numbers&lt;/td&gt;
&lt;td&gt;The Semantic Model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Observation envelope&lt;/td&gt;
&lt;td&gt;The Semantic Model (shape), Driver Contract (driver-supplied half)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Registry authority, quarantine and unit-mismatch handling&lt;/td&gt;
&lt;td&gt;The Semantic Model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Driver host and instance model, lifecycle, assignments&lt;/td&gt;
&lt;td&gt;Driver Contract&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bus subjects, stream limits, buffering, availability&lt;/td&gt;
&lt;td&gt;Driver Contract&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Driver-side command handling and the prohibitions list&lt;/td&gt;
&lt;td&gt;Driver Contract&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Log schema, hypertable split, ingest pipeline ordering, dedup, publisher check, projection, restart, reprocess&lt;/td&gt;
&lt;td&gt;Ingest Path &amp;amp; Log Schema&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Registry versioning and descriptor storage&lt;/td&gt;
&lt;td&gt;Ingest Path &amp;amp; Log Schema&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Freshness policies, watchdog behaviour, staleness suppression, flap damping&lt;/td&gt;
&lt;td&gt;Watchdog &amp;amp; Derived Points&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Derived point descriptors, operators, quality propagation, evaluation, rehydration&lt;/td&gt;
&lt;td&gt;Watchdog &amp;amp; Derived Points&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Restricted expression grammar, and its two binding contexts&lt;/td&gt;
&lt;td&gt;Watchdog &amp;amp; Derived Points&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claim model, bands, resolution, suspension&lt;/td&gt;
&lt;td&gt;Command Plane&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Desired/reported, deliverability, convergence, command state machine, confirmation modes&lt;/td&gt;
&lt;td&gt;Command Plane&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fail-safe versus fail-clear, interlock constraints&lt;/td&gt;
&lt;td&gt;Command Plane&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Intent API, validation, rate limits, idempotency, escape hatch&lt;/td&gt;
&lt;td&gt;Command Plane&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rule shape, triggers, evaluation, claims, warmup, loop protection&lt;/td&gt;
&lt;td&gt;Rules Engine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rule and registry load-time validation, rule testing&lt;/td&gt;
&lt;td&gt;Rules Engine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Single-writer enforcement, core epoch, startup and shutdown ordering&lt;/td&gt;
&lt;td&gt;Core Runtime&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scale targets, outage figures, buffer sizing, drain behaviour&lt;/td&gt;
&lt;td&gt;Core Runtime&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backpressure and ingest lag&lt;/td&gt;
&lt;td&gt;Core Runtime&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Availability while the core is down, restart time, geographic split&lt;/td&gt;
&lt;td&gt;Core Runtime&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Threat model, principals, credentials, bus permissions&lt;/td&gt;
&lt;td&gt;Security Model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Human authentication, capabilities, what each principal may do&lt;/td&gt;
&lt;td&gt;Security Model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Secret handling, audit requirements, exposure assumptions&lt;/td&gt;
&lt;td&gt;Security Model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Self-point catalogue, naming, what raises an alarm&lt;/td&gt;
&lt;td&gt;Observability&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retention classes, policy values, rollup derivation, compaction&lt;/td&gt;
&lt;td&gt;Retention &amp;amp; Compaction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exit criteria for each build step&lt;/td&gt;
&lt;td&gt;the document owning that step's mechanism&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Conventions
&lt;/h2&gt;

&lt;p&gt;Most of these were extracted from a specific error rather than asserted as principle, and each names the failure it prevents.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One definition per concept.&lt;/strong&gt; Cross-reference rather than restate, including in this note. A concept that appears in two documents has one normative home and one pointer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Vocabularies and schemas are code.&lt;/strong&gt; The closed vocabularies, point descriptors and rule definitions live as versioned files in the repository and load into Postgres by migration. Prose in these notes explains them; it does not define them once the code exists.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;State machines are enums with tests.&lt;/strong&gt; Quality transitions, driver lifecycle, command lifecycle, convergence and arbitration bands appear as tables in prose here and must exist as checked enums in code, with a test that no code path produces an illegal transition. Prose tables are for readers and will rot.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every claim a document makes has a test that would fail if it stopped being true, and that test lives in the same document.&lt;/strong&gt; Prose cannot fail loudly. The exit criteria at the end of each document are that test set, and they are what says a build step is finished — not a feature checklist, but a demonstration that the step's central property actually holds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A boolean that has to carry three meanings is an enum.&lt;/strong&gt; Quality was the first instance and convergence the second. When a field needs to say "not yet", "no", and "not applicable", write the enum the first time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Store what composes, not what is convenient to read.&lt;/strong&gt; Native alongside canonical, sum and count rather than average. Anything derived can be recomputed from a composable base; a base that has already been reduced cannot be recovered, and the reduction is usually wrong at the next level up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Storage optimisations that discard raw rows are not optimisations.&lt;/strong&gt; Derived-point windows and reprocess both read the raw tables, so a row not written is a mechanism quietly broken. Every observation writes a row; volume is managed by retention and compaction, which are deliberate and bounded, rather than by suppression at write time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Behaviour that varies by data type is derived from the descriptor, not authored per point.&lt;/strong&gt; Which rollup functions are valid follows from &lt;code&gt;value_type&lt;/code&gt;, &lt;code&gt;agg_safe&lt;/code&gt; and the aggregation modifier; which retention class applies follows from &lt;code&gt;kind&lt;/code&gt;. Restating what the registry already knows is an invitation to state it wrongly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One number per concept.&lt;/strong&gt; Where an ordering exists, it is expressed once. Bands are an ordered enum and there is no parallel priority integer; the same discipline applies anywhere else a rank appears.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two numbers that differ by an order of magnitude are two numbers.&lt;/strong&gt; Planned restart and tolerable unplanned outage were briefly treated as one quantity, which would have sized the outage buffer against the restart case. Before reusing a figure in a second place, check that the two cases actually coincide.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Identity comes from the message, not from the moment of handling.&lt;/strong&gt; Anything a component assigns to an event at processing time is different on a retry, and therefore cannot be part of an identity or dedup key. This is why &lt;code&gt;ingested_at&lt;/code&gt; is taken from the bus rather than from the core's clock.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Authority comes from the connection, never from the payload.&lt;/strong&gt; Who a request or a message is from is established by the credential that carried it or the binding it arrived on, never by a field inside it. Owner and band are derived from the authenticated principal; a driver's right to publish for a point is checked against the registry rather than taken from the subject it chose.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Assumptions that several documents rely on are enforced somewhere.&lt;/strong&gt; Single-writer was assumed by five mechanisms before anything checked it. An assumption held by convention is one that gets broken on a quiet afternoon, and the failure modes that follow are usually silent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nothing expires data by age except a retention policy chosen for it.&lt;/strong&gt; Age limits on a buffer delete messages that were never consumed, silently, between components that both believe the handoff succeeded. Buffers are bounded by size and refuse new work when full; age-based deletion is confined to the places where dropping old data is the actual intent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Physical layout that varies by policy is decided before there is data.&lt;/strong&gt; Retention attaches to a table rather than to a row, so a per-class policy is a table split, and a table split after the fact rewrites everything ever collected. The values can wait; the shape cannot.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An operation that cannot cover its whole scope refuses, rather than covering part of it.&lt;/strong&gt; Reprocess spans retention classes with different horizons, and correcting the reachable two thirds would leave right and wrong data indistinguishable. The scope is validated first, the refusal carries a reason, and narrowing it is a deliberate second command.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A signal about a component must not depend on that component.&lt;/strong&gt; Core self-points are injected at the projection rather than published through the queue they describe, and alerting runs outside the core so it survives the core being down. A monitor that fails with the thing it monitors is not a monitor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Never report a fact about the core as a fact about a device.&lt;/strong&gt; Ingest lag would otherwise surface as fleet-wide staleness, and the system would take protective action across the estate because it was slow. Where the cause is internal, the system says so and suppresses the derived signal — staleness and warmup deadlines both pause on the same condition.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wait on conditions, not on clocks.&lt;/strong&gt; A timeout should mean the thing being waited for failed, not that a precondition had not arrived yet. Where a component waits for another to become ready, it gates on the readiness signal and the wait is unbounded; where the wait is genuinely unsafe to leave open, it escalates rather than expiring.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deployment parameters are read, not assumed.&lt;/strong&gt; Retention windows, intervals and limits set at deployment are checked against configuration at load time by querying them, never by hardcoding the current value. A constant copied into validation code is a constraint that silently stops holding the day the original changes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A mechanism does not ship without saying how its failure would be noticed.&lt;/strong&gt; Its signals go in &lt;em&gt;Observability&lt;/em&gt; in the same commit that defines it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Anything encoded as a string suffix will have to be parsed later.&lt;/strong&gt; This applies to units, phases, aggregation and circuit position. It is promoted to a field instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build order
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Semantic model and vocabularies — point identity, quantity, unit, subject, quality, retention class&lt;/li&gt;
&lt;li&gt;Event log, state projection, snapshot and replay, cold start&lt;/li&gt;
&lt;li&gt;Driver contract, plus one push driver and one poll driver&lt;/li&gt;
&lt;li&gt;Freshness watchdog and derived points&lt;/li&gt;
&lt;li&gt;Command plane, including arbitration and the confirmation modes&lt;/li&gt;
&lt;li&gt;Rules engine&lt;/li&gt;
&lt;li&gt;Instrumentation and timeseries export&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Steps 1 and 2 are the expensive ones to change later. Everything from 3 onwards is additive if those are right.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Each step is finished when the exit criteria in its owning document pass.&lt;/strong&gt; They live there rather than here, because a criterion is the proof of that document's claim and only stays truthful next to it. A separate test plan would drift within a month, leaving two places describing what ingest guarantees.&lt;/p&gt;

&lt;p&gt;Security is not a step. The subject layout and the publisher check land in step 3, bus credentials with the first driver host, and forward auth with the API when it exists. Deferring it to a step of its own is how it becomes a retrofit.&lt;/p&gt;

&lt;p&gt;Neither is the runtime. The advisory lock and the startup ordering belong in step 2, with the first process that writes; the core epoch lands in step 3 alongside the command envelope.&lt;/p&gt;

&lt;p&gt;Neither is observability. Each step adds its own self-points as it goes, which is also how the ingest path gets exercised before there are devices to exercise it.&lt;/p&gt;

&lt;p&gt;Retention is split across both: the class field, the hypertable split and the ingest routing belong in steps 1 and 2, because they are physical; the policy values and the compaction job follow once volume is measured.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ingest throughput is measured during step 2&lt;/strong&gt;, against the design rate in &lt;em&gt;Core Runtime&lt;/em&gt;. It is the term the stream sizing and the recovery time both depend on, and it is cheap to measure with synthetic load before any driver exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  Open items
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sustainable ingest throughput.&lt;/strong&gt; The only unmeasured term in the buffer and recovery arithmetic. Measured on per-observation validation, registry lookup and conversion in application code, which is where the ceiling sits — not on the batch insert.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cold-start time.&lt;/strong&gt; The planned-restart number, dominated by derived-point rehydration at a thousand-plus derived points. Measured rather than estimated, and kept small.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retention policy values.&lt;/strong&gt; The classes and the split are decided; the durations are not. They need measured volume, and the driver for measurement raw retention is how long it is plausible to want to reprocess.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All three are measurements rather than decisions, and the first two are taken during step 2.&lt;/p&gt;

&lt;p&gt;Settled: scale targets and the four-hour tolerable outage, both in &lt;em&gt;Core Runtime&lt;/em&gt;. Retention classes and the samples-versus-history distinction, in &lt;em&gt;Retention &amp;amp; Compaction&lt;/em&gt;. Exit criteria, in each owning document.&lt;/p&gt;

&lt;p&gt;Out of scope: alert routing, and the read API and UI.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Next, on 9 September — &lt;em&gt;The Semantic Model&lt;/em&gt;.&lt;/strong&gt; Why the primary key for every point in this system is a dotted string you can paste into a grep, and never a UUID.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>iot</category>
      <category>monitoring</category>
    </item>
    <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>
