<?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: Corneliu Croitoru</title>
    <description>The latest articles on DEV Community by Corneliu Croitoru (@cornelcroi).</description>
    <link>https://dev.to/cornelcroi</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%2F4096252%2F62c1be34-48b5-4181-bfa7-b999c24d5ceb.jpg</url>
      <title>DEV Community: Corneliu Croitoru</title>
      <link>https://dev.to/cornelcroi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/cornelcroi"/>
    <language>en</language>
    <item>
      <title>You just write. The places find themselves.</title>
      <dc:creator>Corneliu Croitoru</dc:creator>
      <pubDate>Sun, 06 Sep 2026 22:00:00 +0000</pubDate>
      <link>https://dev.to/cornelcroi/you-just-write-the-places-find-themselves-2f2a</link>
      <guid>https://dev.to/cornelcroi/you-just-write-the-places-find-themselves-2f2a</guid>
      <description>&lt;p&gt;On my travel site you write a trip report the way you tell it to a friend. No field for the hotel. You publish, and a few minutes later a section appears under your story: the places you mentioned, pinned on a map, with the name a map would use.&lt;/p&gt;

&lt;p&gt;An LLM does one part of that. Here is the whole pipeline, and where the model's job ends.&lt;/p&gt;

&lt;h2&gt;
  
  
  The contract: a reader, not a writer
&lt;/h2&gt;

&lt;p&gt;The model gets the text of the report and a few rules. These are verbatim from the prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- Only extract NAMED places: a place must have a proper name a map would show
  (e.g. "Restaurante O Tasco", "Mercado dos Lavradores"). Never extract
  descriptive references: "the old town", "restaurants near the river",
  "our hotel", "a nice restaurant", "the beach" — these are not names.
- Most short reports mention no named places — returning an empty places
  list is the correct and common answer.
- Do NOT invent places. Only extract what is explicitly mentioned.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things are in there on purpose.&lt;/p&gt;

&lt;p&gt;What a name is, with counter-examples. "The old town" is not a place a map can show. Left alone, a model will happily return "Old Town", geocode it to something, and put a pin in the wrong district.&lt;/p&gt;

&lt;p&gt;An empty answer is normal. That line took me the longest to learn. Ask a model to find places, it finds places. Most short reports name nothing. Without that sentence, the model fills the list with guesses.&lt;/p&gt;

&lt;p&gt;Never invent. Not because the instruction is enough. It is not. Because the next step assumes the model might be wrong anyway.&lt;/p&gt;

&lt;p&gt;The output is a small JSON contract: a name, a type (hotel, restaurant, beach, attraction, campsite, apartment), what the author did there (stayed, ate, visited), and a one-line caption taken from the text. Nothing about whether the author liked it. That question comes later, and it is asked to the author. One tap, or leave it blank.&lt;/p&gt;

&lt;h2&gt;
  
  
  The map's name, not the author's word
&lt;/h2&gt;

&lt;p&gt;One rule looks small and does a lot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"the place's official name as it appears on a map, in its local
language (e.g. 'Mercado de Triana' even if the text says 'marché de
Triana') — never the author's translation."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;People write on the site in three languages, in the language they think in. A French traveller writes "le marché de Triana". A Spanish one, "el Mercado de Triana". An English one, "Triana market". Three names, one building. If each became its own place, the map would show three pins and no reader would see that three people went there. So the model gives back the map's name. Everything else hangs on that name.&lt;/p&gt;

&lt;h2&gt;
  
  
  Every name has to pass a check the model cannot touch
&lt;/h2&gt;

&lt;p&gt;Here is the part I trust. It is not the model.&lt;/p&gt;

&lt;p&gt;Each name goes to OpenStreetMap, with the report's destination and country as context. The map answers with a real object, coordinates and an identity. Or it does not.&lt;/p&gt;

&lt;p&gt;If it does not, one typo-tolerant search on the same map data gets a second look. Then the place is dropped. Not guessed. Not stored as a name without a pin. The log says "place dropped (no geocode)" and that is the end of it. A wrong pin is worse than no pin, because a reader trusts a pin.&lt;/p&gt;

&lt;p&gt;If it does, two more gates run in plain code before anything is written:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Is it the kind of thing we pin?&lt;/strong&gt; A closed list of map classes. A hotel, a restaurant, a beach, a viewpoint: yes. A whole region that happens to carry the same name: no. A model that returns "Madeira" for a report about Madeira did nothing wrong. The gate is what keeps it off the map.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is it near the trip?&lt;/strong&gt; A distance guard around the destination. A restaurant with the right name on the wrong continent fails here, silently.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two towns with the same name is a real problem and it gets its own article: two Calhetas, 1,199 km apart, and the afternoon a verification pipeline picked the wrong one. For today, the shape is the point. The model proposes a name. The map decides. The model cannot argue.&lt;/p&gt;

&lt;p&gt;Then the place is stored once, under its map identity. The next report that names it, in any language, lands on the same row.&lt;/p&gt;

&lt;h2&gt;
  
  
  It runs after publish, from a trigger, never from the request
&lt;/h2&gt;

&lt;p&gt;Nothing here happens while the author waits. Publishing flips the report's status. A trigger on that change puts one job on a queue: "extract the places of report X". Not the text. Just the id.&lt;/p&gt;

&lt;p&gt;A worker drains the queue a few jobs a minute, with retries. That pace is a choice, and what happens when a trigger fires 179 times in one afternoon is the next article in this series. The job reads the body from the stored record, by id. It never accepts text from a caller. Nothing outside the store can hand it a body to extract from. Same design as the moderation pipeline I wrote about, same reason: the model only ever sees what the author actually wrote.&lt;/p&gt;

&lt;p&gt;The author sees a quiet note on their own report, "we're reading your story to find the places you mention". It goes away on its own when the job lands. Readers never see it. If the last retry fails, the note still resolves, to the honest state: no places yet, here is how to add one.&lt;/p&gt;

&lt;h2&gt;
  
  
  When it is wrong, the author wins
&lt;/h2&gt;

&lt;p&gt;It is wrong sometimes. A campsite the model missed. A café pinned to the wrong town. So the author can add a place, move one, or remove one. A removed place never comes back: the removal is a mark on the row, not a delete, so a re-run of the extractor cannot undo a human decision.&lt;/p&gt;

&lt;p&gt;That is the other half of "the model is a reader". You can correct a reader. You cannot correct a writer, you can only argue with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the model is denied
&lt;/h2&gt;

&lt;p&gt;Opinions. One line in the contract: sentiment "is NOT yours to classify". The model does not get to decide that the traveller recommended the hotel. The traveller answers "Would you stay here again?" themselves, with the same four answers the trip gets. No answer, no verdict, the row stays plain. Readers see the answered ones as small ink stamps next to the place. Every stamp is a person's tap. Never a model's guess.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lesson
&lt;/h2&gt;

&lt;p&gt;An extractor is a reader, not a writer. Give it a contract that says what a valid answer looks like, and says out loud that "nothing" is a valid answer. Then pass every answer through a source of truth the model cannot influence. Then let the human overrule. The model in the middle can be small, cheap and sometimes wrong. The map stays right.&lt;/p&gt;

&lt;p&gt;The exact tuning of the gates stays behind the scenes on purpose. The shape is the part you can take.&lt;/p&gt;

&lt;p&gt;If you built extraction on top of an LLM: which check caught the most, the one the model could not talk its way past? Surprise me.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The site is &lt;a href="https://www.backfrommytrip.com" rel="noopener noreferrer"&gt;Back From My Trip&lt;/a&gt;: trip reports by people who were there, each ending on one question. Would I go back?&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>webdev</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>The librarian pattern: how I keep my AI coding assistant from breaking my app</title>
      <dc:creator>Corneliu Croitoru</dc:creator>
      <pubDate>Wed, 26 Aug 2026 19:24:11 +0000</pubDate>
      <link>https://dev.to/cornelcroi/the-librarian-pattern-how-i-keep-my-ai-coding-assistant-from-breaking-my-app-5396</link>
      <guid>https://dev.to/cornelcroi/the-librarian-pattern-how-i-keep-my-ai-coding-assistant-from-breaking-my-app-5396</guid>
      <description>&lt;p&gt;&lt;strong&gt;One index file, one doc per feature flow, and a 40-line bash hook. That's the whole system.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I build &lt;a href="https://www.backfrommytrip.com" rel="noopener noreferrer"&gt;Back From My Trip&lt;/a&gt; — a travel site where real travellers write trip reports — almost entirely with an AI coding assistant (Claude Code). Solo project, 14 feature flows, lots of moving parts: moderation pipelines, verification with proof documents, place extraction, imports.&lt;/p&gt;

&lt;p&gt;Every AI-assisted project I've seen hits the same two walls:&lt;/p&gt;

&lt;p&gt;1 - The assistant changes code without knowing the rules of the feature it just touched. Tests pass. The flow is broken.&lt;br&gt;
2 - You try to fix that by feeding it more context — and now every session starts by loading half the repo into the model.&lt;/p&gt;

&lt;p&gt;The fix I use is old. Librarians solved it before computers existed.&lt;/p&gt;
&lt;h2&gt;
  
  
  The librarian pattern
&lt;/h2&gt;

&lt;p&gt;A librarian doesn't know every book by heart. They check the catalog, and the catalog tells them the shelf. (The little drawers full of index cards, for those old enough to remember them.)&lt;/p&gt;

&lt;p&gt;My documentation works the same way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One file per flow.&lt;/strong&gt; Every feature flow lives in its own markdown file: &lt;code&gt;flows/moderation.md&lt;/code&gt;, &lt;code&gt;flows/verification.md&lt;/code&gt;, &lt;code&gt;flows/search.md&lt;/code&gt;... 14 files today. Each one is the authoritative end-to-end description: what triggers what, in what order, what happens on every branch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One index.&lt;/strong&gt; &lt;code&gt;FLOWS.md&lt;/code&gt; lists them all with a one-line summary each.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The assistant reads the index, finds the right file, and loads only that one. Small context, right context. It never needs the whole library — it needs the shelf.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6mqo0vun5faq66ltw53o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6mqo0vun5faq66ltw53o.png" alt="The real FLOWS.md, as plain text — what the assistant reads" width="800" height="550"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That's the library. But a library is only useful if the books are true. Here is how I keep it in sync.&lt;/p&gt;
&lt;h2&gt;
  
  
  Piece 1: every source file names its law
&lt;/h2&gt;

&lt;p&gt;The first lines of &lt;code&gt;verify-proof-document.ts&lt;/code&gt;, exactly as committed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// FLOW-CRITICAL: implements flows/verification.md&lt;/span&gt;
&lt;span class="c1"&gt;// Read the doc(s) before changing behavior here. A change that alters a&lt;/span&gt;
&lt;span class="c1"&gt;// documented flow needs explicit user confirmation first, and the doc updated&lt;/span&gt;
&lt;span class="c1"&gt;// in the same commit.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It sits at the very top on purpose: the assistant reads a file from the first line, so it cannot touch the code without meeting the rule first. Same for me.&lt;/p&gt;

&lt;p&gt;No registry, no config that maps files to docs. The mapping lives where it cannot be missed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Piece 2: a hook that notices when the doc was forgotten
&lt;/h2&gt;

&lt;p&gt;A pre-commit hook checks every staged source file: if it declares flow docs in its header and none of them are in the commit, it warns. The whole thing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/sh&lt;/span&gt;
&lt;span class="c"&gt;# Flow-doc guard. Source files declare the flows they implement in a&lt;/span&gt;
&lt;span class="c"&gt;# FLOW-CRITICAL header comment naming flows/*.md chunks. If such a file is&lt;/span&gt;
&lt;span class="c"&gt;# committed and NONE of its named flow docs are in the same commit, warn —&lt;/span&gt;
&lt;span class="c"&gt;# the change may have altered a documented flow without updating the doc.&lt;/span&gt;
&lt;span class="c"&gt;# Warning only: plenty of edits (typos, styling) legitimately don't touch&lt;/span&gt;
&lt;span class="c"&gt;# the flow, and a hard block just teaches people to bypass the hook.&lt;/span&gt;

&lt;span class="nv"&gt;staged&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git diff &lt;span class="nt"&gt;--cached&lt;/span&gt; &lt;span class="nt"&gt;--name-only&lt;/span&gt; &lt;span class="nt"&gt;--diff-filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ACMR&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;warned&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0

&lt;span class="k"&gt;for &lt;/span&gt;f &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nv"&gt;$staged&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  case&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
    &lt;span class="k"&gt;*&lt;/span&gt;.ts|&lt;span class="k"&gt;*&lt;/span&gt;.tsx|&lt;span class="k"&gt;*&lt;/span&gt;.sql&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;;;&lt;/span&gt;
    &lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt; &lt;span class="p"&gt;;;&lt;/span&gt;
  &lt;span class="k"&gt;esac&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;continue

  &lt;/span&gt;&lt;span class="nv"&gt;docs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-5&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="s1"&gt;'flows/[a-z-]*\.md'&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$docs&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;continue

  &lt;/span&gt;&lt;span class="nv"&gt;found&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
  &lt;span class="k"&gt;for &lt;/span&gt;doc &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nv"&gt;$docs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
    if &lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$staged&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-qx&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$doc&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
      &lt;/span&gt;&lt;span class="nv"&gt;found&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
      &lt;span class="nb"&gt;break
    &lt;/span&gt;&lt;span class="k"&gt;fi
  done

  if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$found&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-eq&lt;/span&gt; 0 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"flow-doc guard: &lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt; changed, but none of its flow docs are in this commit:"&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;doc &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nv"&gt;$docs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"  - &lt;/span&gt;&lt;span class="nv"&gt;$doc&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done
    &lt;/span&gt;&lt;span class="nv"&gt;warned&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
  &lt;span class="k"&gt;fi
done

if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$warned&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-eq&lt;/span&gt; 1 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"flow-doc guard: if the flow itself didn't change, ignore this. (warning only)"&lt;/span&gt;
&lt;span class="k"&gt;fi

&lt;/span&gt;&lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details I care about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It warns, it never blocks.&lt;/strong&gt; Plenty of edits (typos, styling) legitimately don't touch the flow. A hard block just teaches people to bypass the hook. A warning teaches the assistant — it sees the message and updates the doc in the same commit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;head -5&lt;/code&gt;.&lt;/strong&gt; The declaration must be in the first five lines. If it's not at the top, it doesn't count. Placement is the contract.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Piece 3: two lines of standing instructions
&lt;/h2&gt;

&lt;p&gt;In the project's instructions file (CLAUDE.md in my case — every assistant has an equivalent):&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Flow docs are law — read the chunk before changing behavior.&lt;br&gt;
A change that &lt;em&gt;alters&lt;/em&gt; a documented flow requires my explicit confirmation first.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The second line matters more than it looks. The assistant can fix bugs freely, but changing documented behavior needs a human yes. The docs are not notes about the system — they are the spec the system must keep obeying.&lt;/p&gt;

&lt;h2&gt;
  
  
  The loop
&lt;/h2&gt;

&lt;p&gt;So every change runs the same cycle:&lt;/p&gt;

&lt;p&gt;1 - &lt;strong&gt;Flow docs&lt;/strong&gt; — one file per flow, one index&lt;br&gt;
2 - &lt;strong&gt;Read&lt;/strong&gt; — the assistant loads only the flow concerned&lt;br&gt;
3 - &lt;strong&gt;Change&lt;/strong&gt; — the code, guided by the doc&lt;br&gt;
4 - &lt;strong&gt;Update&lt;/strong&gt; — the flow doc, in the same commit&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhp5tuwfvy0ovovab996v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhp5tuwfvy0ovovab996v.png" alt="The loop: flow docs → read → change → update, and back" width="800" height="673"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The loop feeds itself. The doc the assistant reads next time is always true, because updating it was part of the last change. Documentation rot — the thing every team accepts as inevitable — becomes structurally impossible, not heroically avoided.&lt;/p&gt;

&lt;p&gt;Most tooling in this space attacks the problem from the other side: detect drift, then repair it with an agent that sweeps the repo at night. I'd rather have no drift to detect.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest limits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The hook checks file presence, not content. A lazy one-line doc update passes. The real check is me reading the diff.&lt;/li&gt;
&lt;li&gt;At my size, one index is enough. With more content the pattern nests: a top index pointing to chapter indexes, one file per chapter pointing to the actual docs. Real libraries do the same — the catalog says which floor, the floor says which shelf.&lt;/li&gt;
&lt;li&gt;The assistant follows the header rule because the instructions file reinforces it. The header alone, without the standing instruction, gets ignored under pressure.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why not a framework
&lt;/h2&gt;

&lt;p&gt;Every week I see a new framework, a new magic method that promises to solve this. I run away from them — I generally like to go against the current. Markdown files, a bash hook, two rules. Everything here is readable in five minutes and will still work in ten years.&lt;/p&gt;

&lt;p&gt;This is how I've built &lt;a href="https://www.backfrommytrip.com" rel="noopener noreferrer"&gt;backfrommytrip.com&lt;/a&gt; from the beginning without the assistant quietly breaking one of the flows — and I use the same method at work, on a much bigger codebase.&lt;/p&gt;

&lt;p&gt;Are you using an even simpler method to keep your assistant in line? Hard to go simpler than markdown and bash, but surprise me.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>productivity</category>
      <category>claudecode</category>
    </item>
  </channel>
</rss>
