<?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: Daniel Romitelli</title>
    <description>The latest articles on DEV Community by Daniel Romitelli (@romiteld).</description>
    <link>https://dev.to/romiteld</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%2F2564609%2F45e9921e-df6d-47a9-a7b5-344290cb30a0.jpg</url>
      <title>DEV Community: Daniel Romitelli</title>
      <link>https://dev.to/romiteld</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/romiteld"/>
    <language>en</language>
    <item>
      <title>Voice Input Is an Intent Language</title>
      <dc:creator>Daniel Romitelli</dc:creator>
      <pubDate>Mon, 03 Aug 2026 21:23:33 +0000</pubDate>
      <link>https://dev.to/romiteld/voice-input-is-an-intent-language-384d</link>
      <guid>https://dev.to/romiteld/voice-input-is-an-intent-language-384d</guid>
      <description>&lt;p&gt;If you dictate into an editor, sometimes you mean to type, sometimes you mean to command, and the app has to tell the difference before it acts.&lt;/p&gt;

&lt;p&gt;That is the real problem. A transcript like “close tab” can be harmless prose or a destructive instruction. Treating every spoken phrase as synthetic keystrokes gives the editor a ghost typist with too much confidence.&lt;/p&gt;

&lt;p&gt;I built the voice-control path in a desktop IDE as a small compiler pipeline: microphone event, Python bridge process, transcript event, Node.js command parser, then either an IDE action or text insertion.&lt;/p&gt;

&lt;h2&gt;
  
  
  The split
&lt;/h2&gt;

&lt;p&gt;Python listens and transcribes. Node.js decides. The renderer displays state and inserts text at the chosen destination.&lt;/p&gt;

&lt;p&gt;That division is the design. The Python bridge owns audio capture, voice activity detection, transcription, settings, and a line-oriented command protocol. The Electron main process owns Inter-Process Communication (IPC), privileged actions, and the parser that turns a final transcript into an editor operation.&lt;/p&gt;

&lt;p&gt;The bridge speaks JavaScript Object Notation (JSON) lines over standard input and standard output. It accepts commands such as start, stop, status, and quit. It emits events such as ready, listening, final transcript, idle, error, and status. That protocol keeps the speech runtime replaceable while keeping workspace mutation inside the Electron side.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart LR
 subgraph PythonBridgeProcess
  mic[Microphone Input] --&amp;gt; recorder[Audio Recorder]
  recorder --&amp;gt; vad[Voice Activity Detection]
  vad --&amp;gt; skip[Skip Transcription]
  vad --&amp;gt; stt[Speech To Text]
  stt --&amp;gt; final[Final Transcript Event]
 end
 subgraph ElectronMainProcess
  manager[Voice Manager] --&amp;gt; parser[Command Parser]
  parser --&amp;gt; action[Privileged IDE Action]
  parser --&amp;gt; insert[Insertion Event]
 end
 subgraph RendererProcess
  status[Status And Ghost Text]
  editor[Editor Or Terminal Target]
 end
 final --&amp;gt; jsonLine[JSON Line Over Stdout]
 jsonLine --&amp;gt; manager
 action --&amp;gt; ipcStatus[IPC Status Update]
 ipcStatus --&amp;gt; status
 insert --&amp;gt; ipcInsert[IPC Insertion Update]
 ipcInsert --&amp;gt; editor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The cost is operational shape. A child process has lifecycle, readiness, stderr handling, and environment discovery. I accepted that because the alternative was worse: audio code spread through the renderer, or transcription code gaining accidental access to editor commands.&lt;/p&gt;

&lt;h2&gt;
  
  
  Silence is a billable input unless it is filtered early
&lt;/h2&gt;

&lt;p&gt;Voice activity detection (VAD) is the first classifier in the path. Silence wastes transcription calls and can still produce accidental text if it reaches speech-to-text, so the bridge checks for speech before sending audio onward.&lt;/p&gt;

&lt;p&gt;That choice also keeps failure behavior crisp. When the host has no capture device, the bridge can still start and respond to commands, but the final result is empty. The renderer surfaces that as a “No speech detected” condition instead of quietly pretending dictation succeeded.&lt;/p&gt;

&lt;p&gt;The tradeoff is dependency coupling. The bridge has to find and import the existing speech package, including recorder, transcriber, VAD, and settings modules. That is less tidy than a packaged library boundary, but it reuses the proven speech stack rather than cloning it inside the desktop app.&lt;/p&gt;

&lt;h2&gt;
  
  
  The parser decides between action and text
&lt;/h2&gt;

&lt;p&gt;The main process never treats a final transcript as immediate typing. It passes the string into the command parser.&lt;/p&gt;

&lt;p&gt;The current implementation has three practical outcomes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Transcript shape&lt;/th&gt;
&lt;th&gt;Parser result&lt;/th&gt;
&lt;th&gt;Example behavior&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Empty transcript&lt;/td&gt;
&lt;td&gt;Reject&lt;/td&gt;
&lt;td&gt;Show no-speech feedback, do not edit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exact command phrase&lt;/td&gt;
&lt;td&gt;Execute command&lt;/td&gt;
&lt;td&gt;Save file, close tab, undo, redo, format&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Payload command&lt;/td&gt;
&lt;td&gt;Execute command with argument&lt;/td&gt;
&lt;td&gt;Find text, jump to a line, send terminal input&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Any other non-empty phrase&lt;/td&gt;
&lt;td&gt;Insert text&lt;/td&gt;
&lt;td&gt;Dictation lands at the active editor or terminal target&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That table is the intent language. It is small on purpose. Exact phrases get command privileges. Payload commands must have a recognizable verb and argument shape. Everything else becomes content.&lt;/p&gt;

&lt;p&gt;This means the implemented no-op path belongs to empty or silent input, not fuzzy ambiguity. I would rather keep that rule visible than pretend the parser has a confidence model it does not have. If a future version adds ambiguous rejection, it should be an explicit parser result, not a side effect of a missed match.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
start[Start]
transcript[Transcript]
emptySilence[Empty or Silence]
exactCommand[Exact Phrase or Payload Command]
unmatchedText[Unmatched Non Empty Text]
noOp[No Op]
command[Command]
dictation[Dictation]
ideAction[IDE Action]
textInsertion[Text Insertion]
endState[End]
start --&amp;gt; transcript
transcript --&amp;gt; emptySilence
transcript --&amp;gt; exactCommand
transcript --&amp;gt; unmatchedText
emptySilence --&amp;gt; noOp
exactCommand --&amp;gt; command
unmatchedText --&amp;gt; dictation
command --&amp;gt; ideAction
dictation --&amp;gt; textInsertion
noOp --&amp;gt; endState
ideAction --&amp;gt; endState
textInsertion --&amp;gt; endState
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Collision policy is where this pattern earns its keep. “Find” by itself should not become a search without a target. “Find database” can become a command because it has a verb and payload. “Close tab” can execute because it is an exact phrase. “The phrase close tab appears in the docs” inserts as text unless the grammar says otherwise.&lt;/p&gt;

&lt;p&gt;That creates maintenance work. Each new command adds another phrase that can collide with normal prose. The parser has to stay conservative: short destructive commands need exact matches, payload commands need argument validation, and ordinary sentences need a predictable insertion path.&lt;/p&gt;

&lt;h2&gt;
  
  
  The renderer is only the destination
&lt;/h2&gt;

&lt;p&gt;The renderer integration handles status, provisional text while transcription is pending, insertion at the cursor, command display, and terminal forwarding. It knows whether focus is in the editor or terminal. It does not decide whether a transcript is allowed to save a file or close a tab.&lt;/p&gt;

&lt;p&gt;The global voice trigger follows the same pattern. The hotkey path routes through the application menu event into the renderer-facing control flow, rather than letting a shortcut handler become a second command executor. The manager still owns start, stop, status, and bridge messaging.&lt;/p&gt;

&lt;p&gt;That keeps one chain of custody for spoken input. Audio becomes a transcript. A transcript becomes a parser result. Only then does the desktop IDE apply the effect.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treat speech like code
&lt;/h2&gt;

&lt;p&gt;The useful model is a compiler. The microphone produces raw input. The bridge turns sound into a token stream. The parser classifies intent. The execution layer applies a constrained result.&lt;/p&gt;

&lt;p&gt;Voice input becomes safer when spoken words cross that language boundary before they touch the workspace. The machinery is ordinary: a Python child process, JSON lines, an Electron manager, IPC, and a command parser. The discipline is in refusing to treat speech as typing with a microphone attached.&lt;/p&gt;




&lt;p&gt;🎧 &lt;strong&gt;Listen to the audiobook&lt;/strong&gt; — &lt;a href="https://open.spotify.com/show/4ABVd5yDVfbX9HlV5JjT7D" rel="noopener noreferrer"&gt;Spotify&lt;/a&gt; · &lt;a href="https://play.google.com/store/audiobooks/details/How_to_Architect_an_Enterprise_AI_System_And_Why_t?id=AQAAAECafz8_tM&amp;amp;hl=en" rel="noopener noreferrer"&gt;Google Play&lt;/a&gt; · &lt;a href="https://www.craftedbydaniel.com/audiobook" rel="noopener noreferrer"&gt;All platforms&lt;/a&gt;&lt;br&gt;
🎬 &lt;a href="https://youtube.com/playlist?list=PLRteDbGJPYDb9XNjecvHplGlgW7tIv_q6" rel="noopener noreferrer"&gt;Watch the visual overviews on YouTube&lt;/a&gt;&lt;br&gt;
📖 &lt;a href="https://www.craftedbydaniel.com/blog/series/how-to-architect-an-enterprise-ai-system-and-why-the-engineer-still-matters" rel="noopener noreferrer"&gt;Read the full 13-part series&lt;/a&gt;&lt;/p&gt;

</description>
      <category>voiceinput</category>
      <category>electron</category>
      <category>ipc</category>
      <category>desktopide</category>
    </item>
    <item>
      <title>Provenance Belongs in the Image Table</title>
      <dc:creator>Daniel Romitelli</dc:creator>
      <pubDate>Mon, 03 Aug 2026 21:23:23 +0000</pubDate>
      <link>https://dev.to/romiteld/provenance-belongs-in-the-image-table-4d30</link>
      <guid>https://dev.to/romiteld/provenance-belongs-in-the-image-table-4d30</guid>
      <description>&lt;p&gt;A generated image looks finished until review starts.&lt;/p&gt;

&lt;p&gt;Someone approves the first version. Someone else crops it. A branded copy goes out. Another edit changes the prompt. A week later, the useful question is simple: which prompt, model, seed, size, parent image, and publishing settings produced the version on screen?&lt;/p&gt;

&lt;p&gt;In a content studio, I put those answers in the PostgreSQL row that stores the image. Logs explain what happened during a run, then rotate away. Object storage keeps the bytes and forgets why they exist. The row is the only one of the three that survives edits, review, and publishing.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The row is the receipt
&lt;/h2&gt;

&lt;p&gt;The table in &lt;code&gt;apps/api/src/database/init-ai-images-table.js&lt;/code&gt; treats generated and edited images as one record type. An original image gets its own row. An edit gets another row, with &lt;code&gt;original_image_id&lt;/code&gt; pointing back to the parent.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;ai_generated_images&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;SERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;image_url&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

  &lt;span class="c1"&gt;-- what produced it&lt;/span&gt;
  &lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'fal-ai/imagen4'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;model_version&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;seed&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;width&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;height&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

  &lt;span class="c1"&gt;-- how it derives from another row&lt;/span&gt;
  &lt;span class="n"&gt;is_edited&lt;/span&gt; &lt;span class="nb"&gt;BOOLEAN&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;FALSE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;original_image_id&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;ai_generated_images&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;edit_prompt&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;edit_strength&lt;/span&gt; &lt;span class="nb"&gt;DECIMAL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;

  &lt;span class="c1"&gt;-- what actually shipped&lt;/span&gt;
  &lt;span class="n"&gt;branded_url&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;branding_options&lt;/span&gt; &lt;span class="n"&gt;JSONB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

  &lt;span class="n"&gt;metadata&lt;/span&gt; &lt;span class="n"&gt;JSONB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;tags&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That self-reference is the design choice. It makes the image table append-only-ish: new variants are inserted as new rows instead of overwriting the earlier state. The cost is more rows and more discipline at write time. The benefit is editable history that product screens and debugging queries can follow.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
  original["Original row: prompt, model, seed, dimensions"]
  editA["Edited child: edit_prompt, edit_strength, edit_steps"]
  editB["Edited child: edit_prompt, edit_guidance_scale"]
  brandedA["Branded output: branded_url, branding_options"]
  brandedB["Branded output: branded_url, branding_options"]
  original --&amp;gt; editA
  original --&amp;gt; editB
  editA --&amp;gt; brandedA
  editB --&amp;gt; brandedB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. Why not a separate audit table
&lt;/h2&gt;

&lt;p&gt;An audit table is the obvious alternative. Write the image row, append an event per generation and edit somewhere else, reconstruct history when asked.&lt;/p&gt;

&lt;p&gt;I did not do that, and the reason is where the cost lands. An audit table answers questions about history. The product asks questions about the current thing. Every screen that shows an image wants the prompt next to it. A gallery filters by model. A review queue sorts by whether a row is a branded variant. Each of those becomes a join against a log that grows faster than the images do.&lt;/p&gt;

&lt;p&gt;The self-reference costs a recursive query when someone wants the whole branch, which is rare. The audit table costs a join on every render, which is constant. I moved the expensive case onto the rare one.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Columns carry facts the product asks about
&lt;/h2&gt;

&lt;p&gt;A value gets its own column when the application filters, sorts, joins, or explains by it. &lt;code&gt;metadata&lt;/code&gt; and &lt;code&gt;branding_options&lt;/code&gt; stay &lt;code&gt;JSONB&lt;/code&gt; because those shapes change more often than the core record, and &lt;code&gt;tags&lt;/code&gt; is an array so classification can be indexed.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field group&lt;/th&gt;
&lt;th&gt;Question it answers&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;prompt&lt;/code&gt;, &lt;code&gt;negative_prompt&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;What text guided the run&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;model&lt;/code&gt;, &lt;code&gt;seed&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Which settings identify it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;width&lt;/code&gt;, &lt;code&gt;height&lt;/code&gt;, &lt;code&gt;aspect_ratio&lt;/code&gt;, &lt;code&gt;resolution&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;What shape came back&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;is_edited&lt;/code&gt;, &lt;code&gt;original_image_id&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Whether it derives from another row&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;edit_prompt&lt;/code&gt;, &lt;code&gt;edit_strength&lt;/code&gt;, &lt;code&gt;edit_guidance_scale&lt;/code&gt;, &lt;code&gt;edit_steps&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Which transform produced the child&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;branded_url&lt;/code&gt;, &lt;code&gt;branding_options&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Which publishable variant was created&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;metadata&lt;/code&gt;, &lt;code&gt;tags&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Extra details and classification&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Seven indexes cover the access patterns: owner and session for scoping, creation time for the default sort, saved and public flags for the two filters the gallery exposes, and model for the question that only comes up during review. Six of those are ordinary B-tree indexes on scalars. The seventh is not:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_ai_images_tags&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;ai_generated_images&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;GIN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;tags&lt;/code&gt; is an array, so a B-tree cannot help: a B-tree indexes a value, and the query asks whether a value sits inside a collection. A Generalized Inverted Index (GIN) inverts that. It stores one entry per distinct tag pointing at every row carrying it, which turns containment into a lookup instead of a scan.&lt;/p&gt;

&lt;p&gt;Every index is a tax on writes. Seven of them on a table that grows a row per edit is a real cost, and I took it because these images are written once and then browsed, filtered, revisited and argued about for weeks.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Lineage becomes a query
&lt;/h2&gt;

&lt;p&gt;Once parent links live in the same table as the model parameters, review can ask for a whole branch without reading logs. This recursive Common Table Expression (CTE) starts from one original row and returns every descendant with the fields needed to reproduce or debug the result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="k"&gt;RECURSIVE&lt;/span&gt; &lt;span class="n"&gt;image_lineage&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="c1"&gt;-- anchor: the row you are asking about, at depth 0&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;original_image_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;image_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="n"&gt;edit_prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;edit_strength&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;branded_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;depth&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;ai_generated_images&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;

  &lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;

  &lt;span class="c1"&gt;-- recursive arm: anything whose parent is already in the result&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;original_image_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;image_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;edit_prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;edit_strength&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;branded_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;depth&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;ai_generated_images&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt;
  &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;image_lineage&lt;/span&gt; &lt;span class="n"&gt;parent&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;original_image_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;image_lineage&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;depth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two arms are doing different jobs. The anchor selects one row and calls it depth zero. The recursive arm joins the table back onto the results so far, so each pass picks up the children of everything found in the pass before it. Postgres repeats that until a pass returns nothing.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;depth&lt;/code&gt; is the column that makes the output readable. Without it the result is an unordered pile of rows; with it, ordering by depth then time reproduces the order the edits actually happened in. One original branches into many children, and every node carries enough state to explain why it exists.&lt;/p&gt;

&lt;p&gt;The recursion has no depth limit, which is fine while edits are made by people. A loop would hang it, and a row cannot become its own ancestor through the normal write path, so I have not added a guard. If edits ever get generated in a batch, that assumption is the first thing I would revisit.&lt;/p&gt;

&lt;p&gt;The insert path in &lt;code&gt;apps/api/src/database/azure-postgres.js&lt;/code&gt; follows the same contract: explicit columns for the core settings and edit parameters, JSON for the irregular details.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. What the row cannot tell you
&lt;/h2&gt;

&lt;p&gt;Two gaps, and both are visible in the schema above.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;model&lt;/code&gt; stores a slug like &lt;code&gt;fal-ai/imagen4&lt;/code&gt;, not a version. A provider can change weights behind that name without changing the string, and the row will keep claiming a reproducibility it no longer has. Same prompt, same seed, same model value, different image. The fix is a pinned version in its own column rather than buried in &lt;code&gt;metadata&lt;/code&gt;, because reproducibility is a question the product asks directly.&lt;/p&gt;

&lt;p&gt;The foreign key carries no &lt;code&gt;ON DELETE&lt;/code&gt; clause, so Postgres refuses to remove a parent that still has children. Cleanup becomes a deliberate walk down the tree instead of one statement. That is the right default here, and it is worth stating plainly: anyone who reaches for &lt;code&gt;ON DELETE CASCADE&lt;/code&gt; to make a purge easier is deleting the receipts.&lt;/p&gt;

&lt;p&gt;Branding obeys the same rule. &lt;code&gt;image_url&lt;/code&gt; records what the model returned, &lt;code&gt;branded_url&lt;/code&gt; the version intended for use, &lt;code&gt;branding_options&lt;/code&gt; the publishing configuration. A reviewer questioning the final visual can separate model output, edit choice, and branding treatment without opening a log.&lt;/p&gt;




&lt;p&gt;🎧 &lt;strong&gt;Listen to the audiobook&lt;/strong&gt; — &lt;a href="https://open.spotify.com/show/4ABVd5yDVfbX9HlV5JjT7D" rel="noopener noreferrer"&gt;Spotify&lt;/a&gt; · &lt;a href="https://play.google.com/store/audiobooks/details/How_to_Architect_an_Enterprise_AI_System_And_Why_t?id=AQAAAECafz8_tM&amp;amp;hl=en" rel="noopener noreferrer"&gt;Google Play&lt;/a&gt; · &lt;a href="https://www.craftedbydaniel.com/audiobook" rel="noopener noreferrer"&gt;All platforms&lt;/a&gt;&lt;br&gt;
🎬 &lt;a href="https://youtube.com/playlist?list=PLRteDbGJPYDb9XNjecvHplGlgW7tIv_q6" rel="noopener noreferrer"&gt;Watch the visual overviews on YouTube&lt;/a&gt;&lt;br&gt;
📖 &lt;a href="https://www.craftedbydaniel.com/blog/series/how-to-architect-an-enterprise-ai-system-and-why-the-engineer-still-matters" rel="noopener noreferrer"&gt;Read the full 13-part series&lt;/a&gt;&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>databasedesign</category>
      <category>generativeai</category>
      <category>provenance</category>
    </item>
    <item>
      <title>CarSegNet v2: One Lot Photo to a Showroom Composite</title>
      <dc:creator>Daniel Romitelli</dc:creator>
      <pubDate>Sun, 02 Aug 2026 01:57:31 +0000</pubDate>
      <link>https://dev.to/romiteld/carsegnet-v2-one-lot-photo-to-a-showroom-composite-fbn</link>
      <guid>https://dev.to/romiteld/carsegnet-v2-one-lot-photo-to-a-showroom-composite-fbn</guid>
      <description>&lt;p&gt;A buyer does not open a listing and ask what the mask IoU was. They look at the mirror stalk, the antenna, the gaps between wheel spokes, the glass, and the tire line where black rubber meets whatever floor you claim the car is sitting on. If asphalt from the old lot still glows through the spokes, or the car floats a finger-width above a showroom floor that never saw those tires, they feel it before they can name it. The click goes cold. The unit sits.&lt;/p&gt;

&lt;p&gt;That is the sales problem in one frame. CarSegNet v2 is the still-image system I built so a dealership can take one usable lot photo and walk away with showroom-grade listing media: a composite, a cutout, and an alpha matte written as first-class artifacts. The buyer stakes are real. So are the ops stakes that never show up in a segmentation paper.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ops case: same-day media without inventing ROI
&lt;/h2&gt;

&lt;p&gt;A car that arrives on a Tuesday and waits until Thursday for a photo package is inventory that cannot earn an inquiry online. It is on the lot. It is paid for. It is not sellable as a listing until someone produces media. Same-day listing media turns delivery into something a shopper can find the same afternoon. That is money saved by shortening unsellable days, and money earned by putting the unit in front of demand sooner. I am not going to invent a dollar figure or an ROI percentage for that. CarSegNet and AutoLens measure matting, topology, and pipeline plumbing. They do not publish a controlled study of dollars per unit. The category of loss is still obvious to anyone who has watched a delivered car sit dark on the website while the lot fills with snow.&lt;/p&gt;

&lt;p&gt;Production crews and staging are the other spend. Not every unit needs a full crew day, a swept bay, and a lighting package to go live. When the ask is routine inventory media, paying that setup for every VIN is a recurring cost you feel in the calendar as much as in the invoice. A path that starts from one lot photo and lands on a cached showroom plate cuts how often that crew has to turn up for ordinary listings. Special cars can still get special shoots. The rest of the row should not wait on them.&lt;/p&gt;

&lt;p&gt;I am writing this from Buffalo winters, not from a coasting climate. Outdoor lot shoots fail when snow is falling sideways and the windshields are white. Not every dealer can stage indoor photoshoots for every unit all season. When weather kills traditional production, inventory media still has to move. One photo to a showroom composite is the ops path for those weeks: keep listings alive when the lot is unusable as a set.&lt;/p&gt;

&lt;h2&gt;
  
  
  What one photo produces
&lt;/h2&gt;

&lt;p&gt;One still goes in. The pipeline builds a vehicle matte, places the car on a cached or prompted showroom plate, and writes three artifacts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;*_composite.png&lt;/code&gt; for the listing&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;*_cutout.png&lt;/code&gt; for reuse&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;*_alpha.png&lt;/code&gt; as evidence, not a hidden intermediate&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The alpha file is part of the product. If a merchandising manager rejects the frame, you can see what the matte actually decided. The stack behind that path is three named pieces, each with a clear honesty boundary:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;CarSegNet&lt;/strong&gt; still path: semantic prior, NeuralSegJet (NSJ) alpha refine, optional interior openings, plate, composite.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WindowSweep&lt;/strong&gt;: paired glass supervision so cabin stays while reflected lot scenery can leave. Capture contract and audit tooling are real; a production glass model is not shipped.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CCR&lt;/strong&gt; (Counterfactual Correctability Routing): product name for propose-then-verify repair routing. Openings safety and the metrics review gate are the live machinery today; a standalone CCR module is not.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Cached plates are the reliable production path: same floor, same lighting language, no provider drift between units. Prompted plates are optional when the plate library needs a new look, not something you should spend on every VIN.&lt;/p&gt;

&lt;h2&gt;
  
  
  Topology is the hard part, and the buyer feels it first
&lt;/h2&gt;

&lt;p&gt;Most car cutout failures are edge failures: halo on the roofline, jagged mirror stalks, weak tire contact. Cars add a second class of error that region IoU barely notices: enclosed background. Wheel openings, spokes, grilles, roof rails, and glass are holes inside the object. A model can score well on the outer silhouette while painting those openings as solid vehicle. The lot then shines through a "solid" wheel, or the glass turns into painted metal.&lt;/p&gt;

&lt;p&gt;Think of it like cutting a paper stencil of a car for a spray booth. Cutting the outer outline is one job. Cutting the windows and the gaps between spokes so paint can pass through is another. A perfect outline with the windows left filled is still a bad stencil. Boundary refinement can sharpen the contour. It cannot reopen every interior gap once the prior marked it foreground. That is why CarSegNet keeps those jobs in separate stages: each error has one place to inspect.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart LR
  photo(["Dealer Lot Photo"]) --&amp;gt; prior["Semantic Vehicle Prior"] --&amp;gt; alpha["NSJ Alpha Refiner"]
  alpha --&amp;gt; holes{{"Opening Repair Optional"}} --&amp;gt; plate[("Cached Or Generated Plate")]
  plate --&amp;gt; comp["Composite"] --&amp;gt; booth(["TypeScript Booth Client"])

  classDef input fill:#1c2e28,stroke:#4a7a68,color:#d5ebe3
  classDef model fill:#1a2740,stroke:#4a6d9a,color:#d0dced
  classDef gate fill:#2a2436,stroke:#6e5f8c,color:#e2d8f0
  classDef plateNode fill:#2a261a,stroke:#8a7848,color:#efe6c8
  classDef output fill:#1a2c36,stroke:#4a7a8e,color:#cde4ee
  classDef client fill:#1e2a24,stroke:#5a8a6e,color:#d4ebe0

  class photo input
  class prior,alpha model
  class holes gate
  class plate plateNode
  class comp output
  class booth client
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The openings stage is real code in &lt;code&gt;carsegnet/openings.py&lt;/code&gt;, but &lt;code&gt;configs/default.yaml&lt;/code&gt; ships with &lt;code&gt;openings.enabled: false&lt;/code&gt;. Keep it off until the locked 608-frame topology acceptance gate passes. The topology thesis still drives the design when that stage is disabled, because region IoU alone will green-light a filled wheel, and a filled wheel is exactly what a shopper rejects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Carvana topology: strong outer, almost no hole recovery
&lt;/h2&gt;

&lt;p&gt;The loudest evidence for that split is the Carvana topology audit on a 608-frame, 38-vehicle validation split with the corrected still path (semantic text prompt, no detector box, NSJ on the training band).&lt;/p&gt;

&lt;p&gt;Outer metrics for the trained NSJ checkpoint look strong: IoU 0.9908, boundary IoU 0.4762, SAD 5.4460, zero frames below 0.90 IoU. Topology tells a different story. Against 1,193 ground-truth enclosed holes, trained NSJ matched 5, left 1,188 missing, and introduced 111 extras. Raw semantic SAM matched 1. The fallback refiner matched 28 and exploded extras.&lt;/p&gt;

&lt;p&gt;Preserve the trained checkpoint as a boundary ablation, not a production model that "solved openings." Those numbers are checkpoint validation on studio backgrounds with binary glass masks. They are not an untouched holdout and not a dealer-lot benchmark. Real lot photos and WindowSweep need separate evaluation. That contrast is why openings is a separate stage, not a footnote under IoU.&lt;/p&gt;

&lt;h2&gt;
  
  
  Glass, correctability, and what the booth actually does
&lt;/h2&gt;

&lt;p&gt;Glass is where cheap background tools lie. They treat a windshield as keep-or-remove, and either erase the cabin or leave the lot reflection welded into the glass. What you want on a listing is different: keep the seats, the headrests, the cabin volume; replace the scene that is only sitting in the reflection.&lt;/p&gt;

&lt;p&gt;WindowSweep is the supervision contract for that job. Same vehicle, same camera pose, same cabin rays, once with the target side glass raised and once lowered. Similar framing is not enough. If the camera moves, the pair is not teaching glass. It is teaching camera shake. The F-150 pilot under &lt;code&gt;data/windowsweep/f150_pilot_001/&lt;/code&gt; holds five declared pairs. Audit accepts both close pairs at the coarse geometry gate and rejects all three wide pairs because the camera moved. Those accepted pairs are enough to test registration and a first glass-loss prototype. They are not a training corpus. AutoLens product copy describes WindowSweep as in training for the same reason: the capture contract and audit tooling are real; a production glass model is not being claimed as shipped.&lt;/p&gt;

&lt;p&gt;CCR names the dry-fit habit before you glue a repair: try a local detail fix in a controlled way, keep it only when the result earns its keep. There is no &lt;code&gt;ccr.py&lt;/code&gt; in the CarSegNet tree today. What ships is the same discipline on interior openings and batch QA. &lt;code&gt;openings.py&lt;/code&gt; proposes a hole cut, then refuses it if the proposal adds foreground, leaves the interior trust region, or changes the outer silhouette when &lt;code&gt;verify_outer=True&lt;/code&gt;. &lt;code&gt;metrics.py&lt;/code&gt; reports region accuracy, boundary accuracy, matting error, enclosed-hole topology, and a pass/fail gate so a batch job can route only questionable frames to a human. AutoLens currently lists CCR as coming soon. The openings safety contract and the metrics review gate are already how CarSegNet refuses to "fix" a unit into a worse listing.&lt;/p&gt;

&lt;p&gt;After the matte lands on a plate, compose does four deterministic jobs in one pass: shift car colour toward the plate (global LAB, gain clamped), blur a plate that is sharper than the subject, drop a contact shadow under the tire band, and wrap a little plate colour into the vehicle edge. That stack can make a good matte believable. It cannot hide a broken one, and global LAB will not fix mixed lot lighting or reflective chrome that still mirrors the original lot.&lt;/p&gt;

&lt;p&gt;The booth itself is thin on purpose. A TypeScript client talks to a FastAPI service over a single CUDA worker so concurrent jobs do not fight one 24 GB card. Progress rides a WebSocket with HTTP polling as fallback. Voice is a small control layer for overrides, not a second vision stack. CLI and booth share the same &lt;code&gt;CarSegNet&lt;/code&gt; class: booth for interactive unit work, CLI for batch folders and matte-only runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure modes I keep in the open
&lt;/h2&gt;

&lt;p&gt;A few limits are load-bearing, not footnotes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If the segmenter fills a wheel and supplies no interior contour, NSJ cannot reopen it. That sentence is in &lt;code&gt;PAPER.md&lt;/code&gt; and matches the Carvana topology table.&lt;/li&gt;
&lt;li&gt;Openings stay disabled until acceptance evidence locks decoder thresholds. Enabling them without a readable weights checkpoint is a hard &lt;code&gt;PipelineError&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Openings are still-image only; enabling them on video raises &lt;code&gt;PipelineError&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;WindowSweep's F-150 pilot is a geometry and glass-loss test bed, not a multi-vehicle training corpus.&lt;/li&gt;
&lt;li&gt;CCR as a full product routing layer is named in the AutoLens stack and not yet a standalone shipped module; openings safety and the metrics review gate are the live correctability machinery today.&lt;/li&gt;
&lt;li&gt;Harmonisation is global LAB. Mixed lot lighting will not suddenly look like a single softbox.&lt;/li&gt;
&lt;li&gt;Reflective bodywork that mirrors the original lot is out of scope; the pipeline does not invent occluded geometry or rewrite chrome reflections.&lt;/li&gt;
&lt;li&gt;Plate parallax on video is capped (about 3.5 percent of frame width in config). It cannot invent occluded geometry.&lt;/li&gt;
&lt;li&gt;Isolated 1080p wall-clock timing is still an open benchmark in the README. This post does not invent an end-to-end minute count.&lt;/li&gt;
&lt;li&gt;AGPL YOLO26 weights matter for commercial packaging; the detector is optional, depth is harder to replace.&lt;/li&gt;
&lt;li&gt;Two external integrations still need live confirmation per the README: SAM 3.1 &lt;code&gt;handle_stream_request&lt;/code&gt; on a real clip, and one live &lt;code&gt;gpt-image-2&lt;/code&gt; call. &lt;code&gt;carsegnet doctor&lt;/code&gt; is the first stop before spending GPU hours or image credits.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The product ask sounds simple: one lot photo, one showroom composite, list the car today. The engineering ask is different: keep outer silhouette quality, interior hole honesty, glass supervision, plate style stability, and booth UX from collapsing into a single opaque model call that you cannot debug when a merchandising manager rejects the frame. A showroom composite is only as strong as the mask topology, the glass contract, and the refusal to publish a bad repair. That is also the sales case: same-day listing media without waiting on crews, staging bays, or a clear Buffalo sky, with named components you can point to when a buyer, a GM, or a skeptic asks what happens when the wheel spokes and the windshield disagree with the lot behind them.&lt;/p&gt;




&lt;h2&gt;
  
  
  For implementers
&lt;/h2&gt;

&lt;p&gt;Repo evidence for the claims above lives in named files: &lt;code&gt;carsegnet/pipeline.py&lt;/code&gt;, &lt;code&gt;carsegnet/openings.py&lt;/code&gt;, &lt;code&gt;carsegnet/metrics.py&lt;/code&gt;, &lt;code&gt;carsegnet/compose.py&lt;/code&gt;, &lt;code&gt;carsegnet/background.py&lt;/code&gt;, &lt;code&gt;scripts/audit_windowsweep.py&lt;/code&gt;, and the config flags in &lt;code&gt;configs/default.yaml&lt;/code&gt;. The rest of this section is the CLI, cascade, and booth detail a sales reader does not need in the first third.&lt;/p&gt;

&lt;h3&gt;
  
  
  CLI and booth entry
&lt;/h3&gt;

&lt;p&gt;CLI still path with a cached plate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;carsegnet run photos/unit.jpg &lt;span class="nt"&gt;--bg-file&lt;/span&gt; plates/showroom.png
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Prompted plate generation (optional path; needs &lt;code&gt;OPENAI_API_KEY&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;carsegnet run photos/unit.jpg &lt;span class="nt"&gt;--bg-prompt&lt;/span&gt; &lt;span class="s2"&gt;"wet city street at night, neon reflections, no cars"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Booth / service entry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python server/app.py &lt;span class="nt"&gt;--host&lt;/span&gt; 0.0.0.0 &lt;span class="nt"&gt;--port&lt;/span&gt; 8080 &lt;span class="nt"&gt;--config&lt;/span&gt; configs/default.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;-c&lt;/code&gt; is the short form of &lt;code&gt;--config&lt;/code&gt; in &lt;code&gt;server/app.py&lt;/code&gt;. &lt;code&gt;--warm&lt;/code&gt; exists and starts a background thread that imports &lt;code&gt;carsegnet.pipeline&lt;/code&gt;; it does not yet call the worker's full model warm path, so treat it as a light import warm, not a guarantee that every weight is resident.&lt;/p&gt;

&lt;p&gt;The repo targets a single 24 GB GPU worker (the README calls out an RTX 3090 Ti class card). Video uses the same service shape and runs longer. Still images are the center of the sellable path.&lt;/p&gt;

&lt;h3&gt;
  
  
  Still path cascade
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;carsegnet/pipeline.py&lt;/code&gt; owns &lt;code&gt;CarSegNet.process_image&lt;/code&gt; and &lt;code&gt;CarSegNet.matte_image&lt;/code&gt;. The still flow is linear: read the image, build the matte, load or create the plate, composite, write artifacts. Models load lazily on first use so a cold CLI call pays import cost once per process, not at module import time.&lt;/p&gt;

&lt;p&gt;Default segmentation uses the text prompt &lt;code&gt;car&lt;/code&gt; from &lt;code&gt;configs/default.yaml&lt;/code&gt;. The YOLO26 box path in &lt;code&gt;carsegnet/backends.py&lt;/code&gt; is opt-in via &lt;code&gt;segmenter.use_box_prompt&lt;/code&gt;. Depth from &lt;code&gt;yolo26x-depth.pt&lt;/code&gt; is on by default and feeds hero selection, contact shadow attenuation, and video plate parallax. If depth fails, the pipeline logs a warning and continues without it; shadows and parallax degrade, the matte path does not hard-stop. On a crowded lot that matters: you want the near car in the frame, not the largest blob two rows back.&lt;/p&gt;

&lt;p&gt;Alpha refinement lives in &lt;code&gt;carsegnet/nsj.py&lt;/code&gt; and &lt;code&gt;carsegnet/refine.py&lt;/code&gt;. NeuralSegJet (NSJ) replaced dense Conditional Random Field (CRF) cleanup because &lt;code&gt;pydensecrf&lt;/code&gt; does not build cleanly on the Python 3.12 stack the rest of the repo requires. NSJ works inside a trimap: confident foreground, confident background, and a narrow unknown band. A colour guided filter uses image colour so local smoothing follows visible edges. That band is good for hairline silhouette geometry: antenna wire, mirror stem, tire contact. Enclosed holes are left to &lt;code&gt;carsegnet/openings.py&lt;/code&gt; when enabled, which is the right separation of jobs for a product you intend to sell and support.&lt;/p&gt;

&lt;h3&gt;
  
  
  Seed, extent, and the interior trust region
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;carsegnet/openings.py&lt;/code&gt; treats each candidate hole as a connected-component problem with two maps and one safety region.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Seed map:&lt;/strong&gt; strict. High-confidence enclosed background only (wheel wells, floor through spokes, grille voids, glass). Seeds authorize a cut; they do not invent support alone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extent map:&lt;/strong&gt; looser. Grows from a seed through nearby pixels that look like the same opening, capped by growth distance and area limits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interior trust region:&lt;/strong&gt; prior-derived vehicle interior after excluding an outer silhouette guard band. Accepted pixels must stay inside it so the stage cannot eat fenders, bumpers, mirrors, or tire edges.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Decode rejects components that lack a seed, fall outside size limits, leave the trust region, or would change the exterior silhouette after subtraction (&lt;code&gt;apply_opening_mask&lt;/code&gt;). Application is subtractive only inside the trust region. It raises &lt;code&gt;OpeningSafetyError&lt;/code&gt; if the proposal adds foreground alpha, edits pixels outside trust, or changes the outer silhouette when &lt;code&gt;verify_outer=True&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The craft analogy is a gasket, not a paint roller. The outer seal has to stay true. The bolt holes inside the gasket are separate cuts. If you enlarge a bolt hole until it breaches the outer seal, the part is scrap. Openings is built to prefer a missed hole over a breached fender, because a merchandising manager can send a filled wheel back for review, and cannot unsell a chewed quarter panel that already went live.&lt;/p&gt;

&lt;p&gt;When openings stay disabled (the current default), the topology gate in &lt;code&gt;carsegnet/metrics.py&lt;/code&gt; still reports the problem. Enclosed background components are matched one-to-one only when both directional coverages clear 50 percent. QA can flag every missing or extra hole even if region IoU looks excellent.&lt;/p&gt;

&lt;h3&gt;
  
  
  WindowSweep audit path
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python scripts/audit_windowsweep.py &lt;span class="nt"&gt;--self-test&lt;/span&gt;
python scripts/audit_windowsweep.py &lt;span class="se"&gt;\&lt;/span&gt;
  data/windowsweep/f150_pilot_001/windowsweep_manifest.json &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--report&lt;/span&gt; runs/windowsweep/f150_pilot_001.audit.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;scripts/audit_windowsweep.py&lt;/code&gt; verifies the manifest, source SHA-256 hashes, and full-frame camera geometry before a pair is allowed near training. All eleven F-150 pilot images come from one vehicle and one session, and the manifest does not yet seal local glass regions, capture controls, split assignments, or training-rights evidence. The README says that plainly.&lt;/p&gt;

&lt;p&gt;For new captures, mount the camera, then lock focus, exposure, and white balance before moving the window. Keep every frame from a vehicle and capture session in one split.&lt;/p&gt;

&lt;h3&gt;
  
  
  Plates and compositing detail
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;carsegnet/background.py&lt;/code&gt; owns plates. The prompt path calls OpenAI Images via &lt;code&gt;client.images.generate&lt;/code&gt;, reads &lt;code&gt;data[0].b64_json&lt;/code&gt;, and defaults to the pinned snapshot &lt;code&gt;gpt-image-2-2026-04-21&lt;/code&gt; in &lt;code&gt;configs/default.yaml&lt;/code&gt; and that module. Pinning costs migration work when the provider changes. It also stops a cached matte from pairing with a drifting plate style. Retired DALL-E aliases raise &lt;code&gt;BackgroundError&lt;/code&gt; rather than silently falling through. Prompt results land in &lt;code&gt;./.bgcache&lt;/code&gt; keyed by SHA-256 of prompt, model, size, and quality.&lt;/p&gt;

&lt;p&gt;Default background size is &lt;code&gt;2048x1152&lt;/code&gt; at &lt;code&gt;quality: high&lt;/code&gt;, with retries and a 180 second timeout. That is why a live plate call is called out separately from matte work: it is money and latency outside the GPU path.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;carsegnet/compose.py&lt;/code&gt; applies four deterministic adjustments after fitting the plate to the frame:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Job&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Harmonisation&lt;/td&gt;
&lt;td&gt;Shift the car LAB statistics toward the plate (Lightness, A green-red, B blue-yellow), gain clamped.&lt;/td&gt;
&lt;td&gt;Unusual paint and tinted glass can move too far. Global LAB will not fix mixed illuminants.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Defocus matching&lt;/td&gt;
&lt;td&gt;Blur the plate when it is sharper than the subject (Laplacian variance, capped sigma).&lt;/td&gt;
&lt;td&gt;Missing subject sharpness cannot be recovered.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Contact shadow&lt;/td&gt;
&lt;td&gt;Ground the tires on the new floor from the lowest band of the silhouette, depth-attenuated when depth exists.&lt;/td&gt;
&lt;td&gt;A bad footprint gives a weak shadow, and the car starts to float.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Light wrap&lt;/td&gt;
&lt;td&gt;Mix a little plate colour into the vehicle edge.&lt;/td&gt;
&lt;td&gt;Excess wrap reads as haze on the body line.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Server, WebSocket, and voice
&lt;/h3&gt;

&lt;p&gt;There are two booth surfaces in the repo. &lt;code&gt;server/app.py&lt;/code&gt; serves the FastAPI API and the static &lt;code&gt;web/index.html&lt;/code&gt; client at &lt;code&gt;/&lt;/code&gt;. &lt;code&gt;web-next/&lt;/code&gt; is a separate Next.js booth client that talks to the same API. Neither hosts the models in the browser.&lt;/p&gt;

&lt;p&gt;GPU work runs behind FastAPI on CUDA. One worker thread serializes jobs so concurrent model loads do not fight a single 24 GB card. Job states move &lt;code&gt;queued → running → done | error&lt;/code&gt;. Progress publishes on WebSocket &lt;code&gt;/api/jobs/{job_id}/events&lt;/code&gt;. &lt;code&gt;web-next/lib/useJob.ts&lt;/code&gt; follows that socket and falls back to HTTP polling every second (up to about 15 minutes) when sockets are blocked. Uploads are &lt;code&gt;POST /api/jobs&lt;/code&gt; with multipart file plus JSON overrides; outputs are path-confined under &lt;code&gt;runtime/outputs/{job_id}/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Voice is a small control layer, not a second vision stack. &lt;code&gt;web-next/lib/useVoice.ts&lt;/code&gt; uses browser speech-to-text; audio stays on the machine. The server parses the transcript in &lt;code&gt;carsegnet/voice.py&lt;/code&gt; into a typed action (with a regex fallback when no API key is present) and clamps overrides before they hit the worker.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart LR
  browser(["TypeScript Booth Client"]) --&amp;gt; api["FastAPI Service"] --&amp;gt; worker["Single GPU Worker"]
  worker --&amp;gt; models[["Prior NSJ Openings"]]
  worker --&amp;gt; outputs[("Composite Cutout Alpha")]

  classDef client fill:#1e2a24,stroke:#5a8a6e,color:#d4ebe0
  classDef service fill:#1a2740,stroke:#4a6d9a,color:#d0dced
  classDef compute fill:#241e30,stroke:#6e5f8c,color:#e2d8f0
  classDef model fill:#1a2740,stroke:#4a6d9a,color:#d0dced
  classDef artifact fill:#1a2c36,stroke:#4a7a8e,color:#cde4ee

  class browser client
  class api service
  class worker compute
  class models model
  class outputs artifact
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;CLI supports batch folders, config overrides (&lt;code&gt;--set refine.band_px=18&lt;/code&gt;), and matte-only runs (&lt;code&gt;carsegnet matte&lt;/code&gt;). Pick CLI or booth based on who is watching the screen and how many VINs are in the queue, not based on which path sounds more advanced.&lt;/p&gt;

&lt;h3&gt;
  
  
  Measurements that prove plumbing vs topology
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;python tests/selftest.py&lt;/code&gt; exercises orchestration, encoding, compositing, openings safety contracts, and quality-assurance reporting on CPU with stubbed model backends. Forty checks, no network, no GPU, no model downloads. That proves plumbing, not dealer-lot matte quality.&lt;/p&gt;

&lt;p&gt;The synthetic selftest hard case (thin antenna, dilated and blurred prior) shows the fallback refiner can recover boundary IoU on that scene; it does not measure wheel openings or glass transmission. Upstream SAM 3.1 speed claims and config comments about &lt;code&gt;video.compile&lt;/code&gt; are not local dealer-lot timings either. If you are selling this system, sell the measured parts as measured and the gated parts as gated.&lt;/p&gt;

&lt;p&gt;Separating SAM prior, NSJ band refine, optional openings repair, WindowSweep glass supervision, CCR-style propose-then-verify routing, pinned or cached plates, and deterministic compose means each failure has a file. &lt;code&gt;carsegnet/openings.py&lt;/code&gt; for seed or trust violations. &lt;code&gt;carsegnet/compose.py&lt;/code&gt; for a shadow that does not sit under the tires. &lt;code&gt;carsegnet/background.py&lt;/code&gt; for a plate model that drifted. &lt;code&gt;scripts/audit_windowsweep.py&lt;/code&gt; for a glass pair whose camera moved. &lt;code&gt;web-next/lib/useJob.ts&lt;/code&gt; for a socket that died and should have fallen back to polling.&lt;/p&gt;




&lt;p&gt;🎧 &lt;strong&gt;Listen to the audiobook&lt;/strong&gt; — &lt;a href="https://open.spotify.com/show/4ABVd5yDVfbX9HlV5JjT7D" rel="noopener noreferrer"&gt;Spotify&lt;/a&gt; · &lt;a href="https://play.google.com/store/audiobooks/details/How_to_Architect_an_Enterprise_AI_System_And_Why_t?id=AQAAAECafz8_tM&amp;amp;hl=en" rel="noopener noreferrer"&gt;Google Play&lt;/a&gt; · &lt;a href="https://www.craftedbydaniel.com/audiobook" rel="noopener noreferrer"&gt;All platforms&lt;/a&gt;&lt;br&gt;
🎬 &lt;a href="https://youtube.com/playlist?list=PLRteDbGJPYDb9XNjecvHplGlgW7tIv_q6" rel="noopener noreferrer"&gt;Watch the visual overviews on YouTube&lt;/a&gt;&lt;br&gt;
📖 &lt;a href="https://www.craftedbydaniel.com/blog/series/how-to-architect-an-enterprise-ai-system-and-why-the-engineer-still-matters" rel="noopener noreferrer"&gt;Read the full 13-part series&lt;/a&gt;&lt;/p&gt;

</description>
      <category>carsegnet</category>
      <category>windowsweep</category>
      <category>ccr</category>
      <category>computervision</category>
    </item>
    <item>
      <title>Messages Need a Protocol Before They Need a Chat UI</title>
      <dc:creator>Daniel Romitelli</dc:creator>
      <pubDate>Fri, 31 Jul 2026 20:07:15 +0000</pubDate>
      <link>https://dev.to/romiteld/messages-need-a-protocol-before-they-need-a-chat-ui-37oj</link>
      <guid>https://dev.to/romiteld/messages-need-a-protocol-before-they-need-a-chat-ui-37oj</guid>
      <description>&lt;p&gt;A patient taps a kiosk and asks for help. A staff member replies from another screen. The same message may appear in the app, trigger a push notification, fall back to Short Message Service (SMS), and later show as read.&lt;/p&gt;

&lt;p&gt;If each screen decides what those events mean, the thread turns into a rumor mill. One client marks the message as sent. Another treats a push attempt as delivery. A third retries after a network pause. In a clinic, that ambiguity creates audit gaps, extra staff work, and a patient expectation problem: sent must mean something actionable.&lt;/p&gt;

&lt;p&gt;The rule I wanted was simple: every message has a durable state, every transition has an owner, and urgent communication can leave the app channel when policy allows it.&lt;/p&gt;

&lt;p&gt;That rule is why I built &lt;code&gt;BidirectionalMessagingService&lt;/code&gt; as the coordination layer for kiosk messaging, rather than treating chat as a screen-level feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The invariant comes before the interface
&lt;/h2&gt;

&lt;p&gt;The kiosk already had services for push, SMS, email, and templates. Those services deliver through channels. They do not decide the truth of the thread.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;BidirectionalMessagingService&lt;/code&gt; owns the message lifecycle: creation, delivery policy, acknowledgement handling, read receipts, fallback decisions, and teardown. Push, SMS, and email remain transports.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
patient[Patient Kiosk] --&amp;gt; service[BidirectionalMessagingService]
staff[Staff Client] --&amp;gt; service
service --&amp;gt; thread[Message Record]
service --&amp;gt; push[Push Notification]
service --&amp;gt; sms[SMS Fallback]
service --&amp;gt; email[Email Delivery]
service --&amp;gt; receipt[Read Receipt]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The cost is ceremony. A basic widget can append text and look finished. This version asks callers to provide role, priority, permitted channels, and acknowledgement behavior. That extra shape buys one authority for changing the record.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The row carries the user-facing truth
&lt;/h2&gt;

&lt;p&gt;The message model stores identity, direction, status, timestamps, and attachments. The durable status set is intentionally small:&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="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;delivered&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;read&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;failed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nl"&gt;sentAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nl"&gt;deliveredAt&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nl"&gt;readAt&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four states, and a timestamp for three of them. There is no &lt;code&gt;queued&lt;/code&gt;, no &lt;code&gt;pending&lt;/code&gt;, no &lt;code&gt;retrying&lt;/code&gt;. Every one of those describes what a worker is doing rather than what happened to the patient, and a status a patient cannot act on has no business in a column they can see.&lt;/p&gt;

&lt;p&gt;Queue membership stays in memory while work is active. It never becomes a database value. A queued item is a worker condition, not a patient-visible fact. Persisting it would force every client to explain whether the item is safe, blocked, or retrying.&lt;/p&gt;

&lt;p&gt;Delivery options are policy, rather than a single flag:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;What it decides&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;channels&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Which transports may carry the message&lt;/td&gt;
&lt;td&gt;More combinations to test&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;urgency&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;How aggressively the system alerts users&lt;/td&gt;
&lt;td&gt;Greater risk of alert fatigue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;requireDeliveryConfirmation&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Whether delivery needs acknowledgement&lt;/td&gt;
&lt;td&gt;More bookkeeping&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;fallbackToSMS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Whether the message may leave the app channel&lt;/td&gt;
&lt;td&gt;Higher external dependency surface&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;translationLanguage&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Whether content needs language adaptation&lt;/td&gt;
&lt;td&gt;More transformation risk&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Timeouts live in the same policy layer. The clock starts with the active delivery attempt, and every permitted transport races against it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;allSettled&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;deliveryPromises&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;successfulDelivery&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fulfilled&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;success&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;messages&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;successfulDelivery&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;delivered&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;failed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;deliveredAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;successfulDelivery&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;allSettled&lt;/code&gt; rather than &lt;code&gt;all&lt;/code&gt; is the whole point. One transport failing is not the message failing. Push can time out while SMS gets through, and the row should say &lt;code&gt;delivered&lt;/code&gt; because a human received it. The status collapses many attempts into the single fact a patient and a nurse both need. Presence can influence routing, but it never creates another stored status.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Receipts and teardown are side effects
&lt;/h2&gt;

&lt;p&gt;A read action moves the message to &lt;code&gt;read&lt;/code&gt;. Notifying the sender is a side effect of that transition. If the sender notification fails, the patient still read the message. Mixing those facts would make the history less reliable.&lt;/p&gt;

&lt;p&gt;The same ownership applies to runtime resources. Realtime subscriptions and pending work need an explicit end, so the service owns cleanup instead of scattering it across screens:&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="nf"&gt;cleanup&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;activeChannels&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unsubscribe&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;activeChannels&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clear&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;messageQueue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clear&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three lines of teardown, and the queue is cleared alongside the subscriptions on purpose. Anything still waiting to send belonged to the session that just ended. Draining it into the next one would deliver a previous patient's message to whoever is standing at the kiosk now.&lt;/p&gt;

&lt;p&gt;That matters in a shared device flow. One patient can walk away, another can begin check-in, and an old subscription should have no vote in the new session.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. The review table
&lt;/h2&gt;

&lt;p&gt;Once the lifecycle is explicit, each operation has one test: it changes the durable record, triggers a side effect, or both.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;th&gt;Owner&lt;/th&gt;
&lt;th&gt;Durable status outcome&lt;/th&gt;
&lt;th&gt;Side effects&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Staff sends message&lt;/td&gt;
&lt;td&gt;Messaging service&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sent&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Channel delivery attempts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transport confirms delivery&lt;/td&gt;
&lt;td&gt;Channel adapter&lt;/td&gt;
&lt;td&gt;&lt;code&gt;delivered&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Optional confirmation notice&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Patient opens message&lt;/td&gt;
&lt;td&gt;Messaging service&lt;/td&gt;
&lt;td&gt;&lt;code&gt;read&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Read receipt notification&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Active attempt expires&lt;/td&gt;
&lt;td&gt;Messaging service&lt;/td&gt;
&lt;td&gt;&lt;code&gt;failed&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Optional fallback path before failure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sender receives receipt notice&lt;/td&gt;
&lt;td&gt;Receipt handler&lt;/td&gt;
&lt;td&gt;No new message status&lt;/td&gt;
&lt;td&gt;User interface acknowledgement&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Consumer chat can tolerate fuzzy indicators. A clinic kiosk has less room for soft meaning because staff coordinate care through the thread and patients expect a reply to reach someone. With explicit states and owners, one status record survives channel retries, fallback routing, and client differences. The interface can vary; the thread still tells one story.&lt;/p&gt;




&lt;p&gt;🎧 &lt;strong&gt;Listen to the audiobook&lt;/strong&gt; — &lt;a href="https://open.spotify.com/show/4ABVd5yDVfbX9HlV5JjT7D" rel="noopener noreferrer"&gt;Spotify&lt;/a&gt; · &lt;a href="https://play.google.com/store/audiobooks/details/How_to_Architect_an_Enterprise_AI_System_And_Why_t?id=AQAAAECafz8_tM&amp;amp;hl=en" rel="noopener noreferrer"&gt;Google Play&lt;/a&gt; · &lt;a href="https://www.craftedbydaniel.com/audiobook" rel="noopener noreferrer"&gt;All platforms&lt;/a&gt;&lt;br&gt;
🎬 &lt;a href="https://youtube.com/playlist?list=PLRteDbGJPYDb9XNjecvHplGlgW7tIv_q6" rel="noopener noreferrer"&gt;Watch the visual overviews on YouTube&lt;/a&gt;&lt;br&gt;
📖 &lt;a href="https://www.craftedbydaniel.com/blog/series/how-to-architect-an-enterprise-ai-system-and-why-the-engineer-still-matters" rel="noopener noreferrer"&gt;Read the full 13-part series&lt;/a&gt;&lt;/p&gt;

</description>
      <category>messaging</category>
      <category>healthcare</category>
      <category>typescript</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>A Cache Key Is an Equivalence Relation</title>
      <dc:creator>Daniel Romitelli</dc:creator>
      <pubDate>Tue, 28 Jul 2026 08:54:10 +0000</pubDate>
      <link>https://dev.to/romiteld/a-cache-key-is-an-equivalence-relation-51g5</link>
      <guid>https://dev.to/romiteld/a-cache-key-is-an-equivalence-relation-51g5</guid>
      <description>&lt;p&gt;A retry should not become a new creative decision.&lt;/p&gt;

&lt;p&gt;When a video job fails halfway through and runs again, the user still asked for the same scene. If a timestamp or retry count changes the lookup, the system pays for another clip. If the lookup ignores a model route or seed, it can return the wrong artifact with a perfectly valid URL.&lt;/p&gt;

&lt;p&gt;I built the video generation pipeline’s cache around that split. The hash is the system’s definition of sameness.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The request definition gets trimmed before hashing
&lt;/h2&gt;

&lt;p&gt;The compiler produces a &lt;code&gt;GenerationContract&lt;/code&gt;: prompt inputs, selected model route, generation mode, constraints, seed, and execution metadata. Only the artifact-defining fields enter the digest.&lt;/p&gt;

&lt;p&gt;The implementation lives in &lt;code&gt;lib/scene-compiler/ast-cache.ts&lt;/code&gt;:&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="cm"&gt;/**
 * Hash the compiled AST to a 64-char hex string (SHA-256).
 * Identical contracts produce identical hashes regardless of
 * volatile per-request fields like timestamps or scene IDs.
 */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;hashCompiledAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;GenerationContract&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extractHashableContent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;createHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="cm"&gt;/**
 * Build a cache key with the `ast:` prefix for Supabase lookup.
 */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;buildCacheKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;GenerationContract&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`ast:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;hashCompiledAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Abstract Syntax Tree (AST) hashing is the mechanism; classification is the design. The hashable content includes &lt;code&gt;sourceInputs.prompt&lt;/code&gt;, &lt;code&gt;sourceInputs.imageUrl&lt;/code&gt;, sorted &lt;code&gt;referenceImageUrls&lt;/code&gt;, &lt;code&gt;chosenModel&lt;/code&gt;, &lt;code&gt;chosenEndpoint&lt;/code&gt;, &lt;code&gt;generationMode&lt;/code&gt;, sorted &lt;code&gt;constraints&lt;/code&gt;, and &lt;code&gt;seed ?? null&lt;/code&gt;. Volatile fields such as &lt;code&gt;compiledAt&lt;/code&gt;, &lt;code&gt;sceneId&lt;/code&gt;, &lt;code&gt;idempotencyKey&lt;/code&gt;, and &lt;code&gt;retries&lt;/code&gt; stay out.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
  contract[GenerationContract] --&amp;gt; split{Classify each field}
  split --&amp;gt;|artifact identity| keep["prompt, imageUrl, referenceImageUrls,&amp;lt;br/&amp;gt;chosenModel, chosenEndpoint,&amp;lt;br/&amp;gt;generationMode, constraints, seed"]
  split --&amp;gt;|run history| drop["compiledAt, sceneId,&amp;lt;br/&amp;gt;idempotencyKey, retries"]
  keep --&amp;gt; norm["Sort constraints by type, then target"]
  norm --&amp;gt; sha["SHA-256 over the serialized fields"]
  sha --&amp;gt; key["Cache key, ast prefix plus 64 hex"]
  drop --&amp;gt; excluded["Never reaches the digest"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The cost is ongoing ownership. Every new field in the request shape needs a decision: artifact identity or run history. Ambiguity becomes either wasted generation or false reuse.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Normalization removes accidental difference
&lt;/h2&gt;

&lt;p&gt;Constraints are structured values, and array order can reflect construction path rather than meaning. I normalize them before serialization:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sortConstraints&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;constraints&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SceneConstraint&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;constraints&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;typeCompare&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;localeCompare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;typeCompare&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;typeCompare&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;localeCompare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That rule makes two request definitions with the same constraint set hash together even if they were assembled in a different order. The tradeoff is explicit: ordering cannot carry semantic weight here. If priority later depends on position, this function has to change before the data model does.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The two bugs sit on opposite sides
&lt;/h2&gt;

&lt;p&gt;Hash too much, and every retry misses. &lt;code&gt;compiledAt&lt;/code&gt; lets the clock break reuse. &lt;code&gt;retries&lt;/code&gt; turns attempt two into a separate artifact. &lt;code&gt;idempotencyKey&lt;/code&gt; belongs to transport safety, so it should not alter creative identity.&lt;/p&gt;

&lt;p&gt;Hash too little, and stale output looks correct. Prompt-only reuse ignores &lt;code&gt;chosenModel&lt;/code&gt;, &lt;code&gt;chosenEndpoint&lt;/code&gt;, &lt;code&gt;generationMode&lt;/code&gt;, and &lt;code&gt;seed&lt;/code&gt;. That failure is worse than a miss: the pipeline receives a real video URL and continues with the wrong clip.&lt;/p&gt;

&lt;p&gt;Tests pin the promises: metadata changes such as &lt;code&gt;compiledAt&lt;/code&gt;, &lt;code&gt;sceneId&lt;/code&gt;, and &lt;code&gt;idempotencyKey&lt;/code&gt; preserve &lt;code&gt;hashCompiledAST&lt;/code&gt;, while &lt;code&gt;buildCacheKey&lt;/code&gt; must match &lt;code&gt;^ast:[a-f0-9]{64}$&lt;/code&gt;. The same file also defines provenance-aware steering hashes with a separate &lt;code&gt;steer:&lt;/code&gt; prefix, so namespacing is part of the storage contract.&lt;/p&gt;

&lt;p&gt;Retries keep a stable artifact identity, and different routes and seeds stay isolated. A content-addressable lookup is only as correct as the equality rule behind it; write that rule in the language of the generated thing, then let the hash enforce it.&lt;/p&gt;




&lt;p&gt;🎧 &lt;strong&gt;Listen to the audiobook&lt;/strong&gt; — &lt;a href="https://open.spotify.com/show/4ABVd5yDVfbX9HlV5JjT7D" rel="noopener noreferrer"&gt;Spotify&lt;/a&gt; · &lt;a href="https://play.google.com/store/audiobooks/details/How_to_Architect_an_Enterprise_AI_System_And_Why_t?id=AQAAAECafz8_tM&amp;amp;hl=en" rel="noopener noreferrer"&gt;Google Play&lt;/a&gt; · &lt;a href="https://www.craftedbydaniel.com/audiobook" rel="noopener noreferrer"&gt;All platforms&lt;/a&gt;&lt;br&gt;
🎬 &lt;a href="https://youtube.com/playlist?list=PLRteDbGJPYDb9XNjecvHplGlgW7tIv_q6" rel="noopener noreferrer"&gt;Watch the visual overviews on YouTube&lt;/a&gt;&lt;br&gt;
📖 &lt;a href="https://www.craftedbydaniel.com/blog/series/how-to-architect-an-enterprise-ai-system-and-why-the-engineer-still-matters" rel="noopener noreferrer"&gt;Read the full 13-part series&lt;/a&gt;&lt;/p&gt;

</description>
      <category>caching</category>
      <category>typescript</category>
      <category>videogeneration</category>
      <category>systemsdesign</category>
    </item>
    <item>
      <title>Workflow JSON Is Generated Code</title>
      <dc:creator>Daniel Romitelli</dc:creator>
      <pubDate>Mon, 27 Jul 2026 22:51:11 +0000</pubDate>
      <link>https://dev.to/romiteld/workflow-json-is-generated-code-1nfk</link>
      <guid>https://dev.to/romiteld/workflow-json-is-generated-code-1nfk</guid>
      <description>&lt;p&gt;A screen recording can show a whole job without explaining it. Someone opens an inbox, checks a sender, copies a value into a customer record, compares it with a spreadsheet, sends a summary, and moves on.&lt;/p&gt;

&lt;p&gt;You can see the work. Automation still has to survive a harder test: can the system put the job back together without dropping a step, wiring the wrong action, or importing something that looks right and fails later?&lt;/p&gt;

&lt;p&gt;I built the n8n side of the screen-analysis project around that question. The generator does not treat the final file as a bag of text. It turns discovered automations into &lt;code&gt;N8NWorkflow&lt;/code&gt;, &lt;code&gt;N8NNode&lt;/code&gt;, and connection objects, then emits n8n-compatible JavaScript Object Notation (JSON). That's more ceremony than gluing strings together, but it catches a class of mistakes string assembly invites.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Keep the platform vocabulary narrow
&lt;/h2&gt;

&lt;p&gt;Every emitted node type comes from &lt;code&gt;NodeType&lt;/code&gt; in &lt;code&gt;n8n_workflow_generator.py&lt;/code&gt;, an enumeration covering the triggers, language model nodes, and application integrations the generator knows how to create. Need another n8n node? I add it there before generation can use it.&lt;/p&gt;

&lt;p&gt;That costs editing speed. A one-off node can't slip through by spelling a new identifier in a prompt. The gain is sharper failure: unsupported platform names fail in Python instead of hiding inside an importable file.&lt;/p&gt;

&lt;p&gt;Agent configurations in &lt;code&gt;n8n_agent_templates.py&lt;/code&gt; work the same way. &lt;code&gt;AgentTemplate&lt;/code&gt; names the available patterns; &lt;code&gt;AgentConfig&lt;/code&gt; carries the prompt, tools, integrations, trigger preferences, model choice, temperature, and iteration limit. The prompt gets one field. It doesn't double as the container for everything else.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;__future__&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;annotations&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;enum&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Enum&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AgentTemplate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Enum&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;EMAIL_TRIAGE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email_triage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;CRM_DATA_SYNC&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;crm_data_sync&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;CALENDAR_ASSISTANT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;calendar_assistant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;DOCUMENT_PROCESSOR&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;document_processor&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;COMMUNICATION_ROUTER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;communication_router&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;REPORT_GENERATOR&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;report_generator&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;LEAD_QUALIFIER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lead_qualifier&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;TASK_MANAGER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;task_manager&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;VOICE_ASSISTANT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;voice_assistant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;MULTI_AGENT_ORCHESTRATOR&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;multi_agent_orchestrator&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AgentConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AgentTemplate&lt;/span&gt;
    &lt;span class="n"&gt;system_prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default_factory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;integrations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default_factory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;triggers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default_factory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;llm_model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemini-2.5-flash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.7&lt;/span&gt;
    &lt;span class="n"&gt;max_iterations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The schema is also a constraint. If a new automation needs a concept &lt;code&gt;AgentConfig&lt;/code&gt; cannot express, I extend the model first. Experiments get slower, and the export path stays honest.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Build objects before JSON
&lt;/h2&gt;

&lt;p&gt;The generator's structured form is the n8n graph itself: nodes plus named connections. There's no second private graph format sitting behind it. &lt;code&gt;N8NWorkflow&lt;/code&gt;, &lt;code&gt;N8NNode&lt;/code&gt;, and connection records are the representation between analysis results and the saved JSON file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
  analysis[Discovered automation] --&amp;gt; workflow[N8NWorkflow]
  workflow --&amp;gt; nodes[N8NNode objects]
  workflow --&amp;gt; connections[Connection records]
  nodes --&amp;gt; json[n8n JSON export]
  connections --&amp;gt; json
  json --&amp;gt; importer[REST importer]
  importer --&amp;gt; status[Import status]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That distinction matters. A detected step such as “classify this email” is mapped to concrete n8n nodes only when the generator has enough context to choose a trigger, model, integration, and connection order. Positioning is computed separately from identity, so the canvas stays readable without tying layout to node IDs.&lt;/p&gt;

&lt;p&gt;The tradeoff is flexibility. Deterministic placement cannot match a hand-arranged canvas, and typed construction is heavier than editing a JSON file directly. For generated automations, I prefer predictable inspection over perfect visual layout.&lt;/p&gt;

&lt;p&gt;The core object shape is simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;__future__&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;annotations&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;


&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;N8NNode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default_factory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default_factory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;type_version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;to_dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="n"&gt;node_dict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;position&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;parameters&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;typeVersion&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;type_version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;node_dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;credentials&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;credentials&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;node_dict&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the part that makes the JSON feel like generated code. The object owns identity, type, parameters, credentials, version, and position before serialization happens. By the time the file exists, the important decisions have already passed through inspectable Python structures.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Treat import as deployment state
&lt;/h2&gt;

&lt;p&gt;Generation ends at a file; operation begins when that file reaches n8n through the Representational State Transfer (REST) API. In &lt;code&gt;n8n_importer.py&lt;/code&gt;, importer failures have a named exception, and import progress has explicit states.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;enum&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Enum&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;N8NError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Custom exception for n8n API errors.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ImportStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Enum&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;PENDING&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pending&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;IMPORTING&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;importing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;SUCCESS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;success&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;FAILED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;failed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;REQUIRES_CREDENTIALS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;requires_credentials&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A credential problem and a failed import need different recovery paths, so they get different labels. The deploy flow can generate automations, create supporting agent files, import them, and leave activation switched off while credentials are handled.&lt;/p&gt;

&lt;p&gt;That separation removes convenience. One button that generates, imports, credentials, and activates would be faster for a demo. In production, splitting those actions makes partial failure recoverable.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. The table I test against
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Question it answers&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;AgentTemplate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Which automation pattern is being built?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;AgentConfig&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Which prompt, tools, integrations, trigger, and model settings describe it?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;NodeType&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Which n8n identifiers may be emitted?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;N8NWorkflow&lt;/code&gt; / &lt;code&gt;N8NNode&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Which graph becomes JSON?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;ImportStatus&lt;/code&gt; / &lt;code&gt;N8NError&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;What happened when the artifact reached n8n?&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This costs more than string interpolation: extra enums, dataclasses, object construction, save steps, and importer reports. It also makes schema changes explicit. I pay the cost because automations inferred from screen recordings already start with uncertainty; the export path should reduce it.&lt;/p&gt;

&lt;p&gt;Workflow JSON is code the moment it can move data, call models, and route work. Treating it as generated code is how I keep a discovered process from becoming an imported accident.&lt;/p&gt;




&lt;p&gt;🎧 &lt;strong&gt;Listen to the audiobook&lt;/strong&gt; — &lt;a href="https://open.spotify.com/show/4ABVd5yDVfbX9HlV5JjT7D" rel="noopener noreferrer"&gt;Spotify&lt;/a&gt; · &lt;a href="https://play.google.com/store/audiobooks/details/How_to_Architect_an_Enterprise_AI_System_And_Why_t?id=AQAAAECafz8_tM&amp;amp;hl=en" rel="noopener noreferrer"&gt;Google Play&lt;/a&gt; · &lt;a href="https://www.craftedbydaniel.com/audiobook" rel="noopener noreferrer"&gt;All platforms&lt;/a&gt;&lt;br&gt;
🎬 &lt;a href="https://youtube.com/playlist?list=PLRteDbGJPYDb9XNjecvHplGlgW7tIv_q6" rel="noopener noreferrer"&gt;Watch the visual overviews on YouTube&lt;/a&gt;&lt;br&gt;
📖 &lt;a href="https://www.craftedbydaniel.com/blog/series/how-to-architect-an-enterprise-ai-system-and-why-the-engineer-still-matters" rel="noopener noreferrer"&gt;Read the full 13-part series&lt;/a&gt;&lt;/p&gt;

</description>
      <category>n8n</category>
      <category>workflowautomation</category>
      <category>codegeneration</category>
      <category>screenanalysis</category>
    </item>
    <item>
      <title>Why I Kept Search Scope Inside a Single Supabase RPC</title>
      <dc:creator>Daniel Romitelli</dc:creator>
      <pubDate>Mon, 27 Jul 2026 16:03:04 +0000</pubDate>
      <link>https://dev.to/romiteld/why-i-kept-search-scope-inside-a-single-supabase-rpc-212a</link>
      <guid>https://dev.to/romiteld/why-i-kept-search-scope-inside-a-single-supabase-rpc-212a</guid>
      <description>&lt;p&gt;The embedding was right. The similarity score was right. The answer was completely wrong.&lt;/p&gt;

&lt;p&gt;The chunk that came back shared vocabulary with the query, shared naming conventions, shared architectural patterns. It also came from the wrong repository. Nothing crashed, nothing timed out, and I went two days without catching it.&lt;/p&gt;

&lt;p&gt;Plausible neighbors from the wrong scope is the failure mode I'd rank as the most dangerous one in vector search. The embedding math was fine. The rows were valid. The trouble was that search decisions were getting assembled in too many places, so by the time the request reached PostgreSQL, the database had to guess which parts belonged together.&lt;/p&gt;

&lt;p&gt;So I made the RPC the single source of truth. The caller sends the query embedding, the candidate count, and the JSONB filter together. The SQL function receives those same values together. The index is built for the same embedding column the function reads. Once those pieces move as one unit, the path stops wandering.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
  query[Query embedding] --&amp;gt; payload[RPC payload]
  count[match_count] --&amp;gt; payload
  filter[JSONB filter] --&amp;gt; payload
  payload --&amp;gt; rpc[Supabase RPC]
  rpc --&amp;gt; sql[search_embeddings]
  sql --&amp;gt; rows[Ranked rows]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The bug was scope, not similarity math
&lt;/h2&gt;

&lt;p&gt;My first mistake was treating search as a handful of knobs instead of one request object. Build the embedding in one place, the metadata filter in another, decide candidate depth a layer above that, and the call boundary turns to mush. The function still runs. But nobody can point at one object and say: this is the exact search intent.&lt;/p&gt;

&lt;p&gt;That matters most when retrieval is scoped, and in this codebase the filter is no afterthought. It can describe a repo, a file path, a language, a type, or any combination of metadata fields that belong in the same search slice. If I'm looking for TypeScript files in a specific repository, I want that expressed as part of the same request that carries the vector. I don't want the application inferring scope from session state, hidden defaults, or whatever the previous call happened to do.&lt;/p&gt;

&lt;p&gt;The symptom was plausible-but-wrong neighbors, because vector similarity is happy to rank related text from the wrong place. Retrieval bugs are slippery for exactly that reason. The results don't look random. They look close enough to distract you. A chunk from the wrong repo can still share concepts, terminology, or naming conventions with the query, and if the filter is applied too late, that wrong row can look like a good answer right up until you inspect the metadata closely.&lt;/p&gt;

&lt;p&gt;The fix was to make the request shape dull and explicit. The caller decides the search scope. The database enforces that scope. The index serves that same scope. No second pass trying to patch over a weaker request after the fact.&lt;/p&gt;

&lt;h2&gt;
  
  
  The caller sends one object
&lt;/h2&gt;

&lt;p&gt;The wrapper I use is intentionally small. It doesn't hide the request shape, and it doesn't smuggle in extra search behavior. Vector, count, filter, passed straight into &lt;code&gt;search_embeddings&lt;/code&gt;.&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;SupabaseClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@supabase/supabase-js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;SearchFilter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;SearchResult&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;document_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;chunk_text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;similarity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;runSearch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SupabaseClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;queryEmbedding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;
  &lt;span class="nx"&gt;matchCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SearchFilter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;SearchResult&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rpc&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;SearchResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;search_embeddings&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;query_embedding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;queryEmbedding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;match_count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;matchCount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I like this shape because it's hard to misunderstand. At call time only three inputs matter: the embedding, the number of rows to return, and the structured scope filter. Searching inside one repo means passing a repo filter. Narrowing by language means adding a language key. Restricting by file type or path, same deal. The caller isn't building a query plan. It's declaring intent.&lt;/p&gt;

&lt;p&gt;The same function can be called with a narrow filter or an empty one. An empty filter means search the whole corpus. A populated one means search the subset that matches the metadata predicate. Very different outcomes, and I want that difference visible right where the request gets created.&lt;/p&gt;

&lt;p&gt;A typical call site stays just as clear:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;runSearch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;supabase&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;the author/portfolio&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;language&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;typescript&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the point. The search boundary should be obvious at a glance. When I'm debugging a bad answer later, I want to inspect one payload and know exactly what the database was asked to do.&lt;/p&gt;

&lt;h2&gt;
  
  
  The database function matches the same interface
&lt;/h2&gt;

&lt;p&gt;On the PostgreSQL side, &lt;code&gt;search_embeddings&lt;/code&gt; accepts the same three inputs the caller sends. The metadata filter stays inside SQL, where it belongs. Rows get filtered first, then ranked by vector distance.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="n"&gt;search_embeddings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;query_embedding&lt;/span&gt; &lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;match_count&lt;/span&gt; &lt;span class="nb"&gt;integer&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;filter&lt;/span&gt; &lt;span class="n"&gt;jsonb&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'{}'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;jsonb&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;RETURNS&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;document_id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;chunk_text&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;similarity&lt;/span&gt; &lt;span class="nb"&gt;double&lt;/span&gt; &lt;span class="nb"&gt;precision&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;metadata&lt;/span&gt; &lt;span class="n"&gt;jsonb&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;LANGUAGE&lt;/span&gt; &lt;span class="k"&gt;sql&lt;/span&gt;
&lt;span class="k"&gt;STABLE&lt;/span&gt;
&lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;document_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chunk_text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;query_embedding&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;similarity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;embeddings&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;filter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'{}'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;jsonb&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt; &lt;span class="o"&gt;@&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;filter&lt;/span&gt;
  &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;query_embedding&lt;/span&gt;
  &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="n"&gt;match_count&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;$$&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;idx_embeddings_embedding&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;embeddings&lt;/span&gt;
  &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;hnsw&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="n"&gt;vector_cosine_ops&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The line carrying the weight is the metadata predicate: &lt;code&gt;e.metadata @&amp;gt; filter&lt;/code&gt;. That isn't a cleanup step after ranking. It's part of the search itself. Rows outside the requested scope never enter the ranked candidate set.&lt;/p&gt;

&lt;p&gt;The design matters because the database is the only place that can apply the filter consistently at the same moment it applies similarity. Filter in the application after ranking, and the query can still surface neighbors from the wrong scope first. Put the filter inside SQL, and ranking only happens across rows that already belong to the same metadata neighborhood as the request.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;similarity&lt;/code&gt; field is there for the caller's benefit. Cosine distance still drives the ordering internally. Externally, I want a score where larger reads as better. Returning both that score and the raw chunk text gives the next stage enough context to render, inspect, or rerank without another round trip.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;STABLE&lt;/code&gt; marker fits the way I use the function too. For a fixed snapshot and a fixed input payload, this is a deterministic retrieval step. It isn't a side-effect machine. It's a search function.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why JSONB belongs in the request, not outside it
&lt;/h2&gt;

&lt;p&gt;The filter is JSONB because the scope is structured rather than free-form. A metadata filter can say more than one thing at once, and it needs to do that without collapsing into a pile of ad hoc parameters. One object can describe a repository slice, a file path constraint, a language constraint, or a file-type constraint.&lt;/p&gt;

&lt;p&gt;So there's a single place to express the search boundary. To retrieve only TypeScript chunks from a particular repository, I don't want to assemble a special query for that case. I want to build a JSONB object like &lt;code&gt;{ repo: 'the author/portfolio', language: 'typescript' }&lt;/code&gt; and pass it straight through. The SQL predicate then enforces exactly that constraint.&lt;/p&gt;

&lt;p&gt;It's also why I keep the filter visible at the RPC boundary rather than burying it in a helper that rewrites inputs behind my back. Hidden rewrite logic is how search calls become hard to reason about. A JSONB object is simple enough to inspect, easy to log, and unambiguous in SQL.&lt;/p&gt;

&lt;p&gt;Debugging gets a second benefit out of this. When a query returns too much, I can loosen the filter and watch the effect immediately. When it returns too little, I can inspect which metadata keys are actually present in the table. Because the filter is part of the request, there's no mystery about which layer decided the corpus was too broad or too narrow.&lt;/p&gt;

&lt;p&gt;The same holds when I expand the metadata model. If I start attaching more structure to a chunk, file type or path segments say, the RPC signature doesn't need to change. I update the JSONB shape, then let the same &lt;code&gt;search_embeddings&lt;/code&gt; function enforce the new predicate. The request boundary holds still while the metadata vocabulary grows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the index is part of the same story
&lt;/h2&gt;

&lt;p&gt;I don't think about the index as a separate optimization pass. It's part of the same guarantee the RPC makes. If the function says it'll search the &lt;code&gt;embeddings&lt;/code&gt; table with cosine distance, the index should be built for that exact access path.&lt;/p&gt;

&lt;p&gt;Which is why the HNSW index sits right beside the function in my mental model. The function defines the agreement. The index makes that agreement fast enough to lean on all the time. &lt;code&gt;vector_cosine_ops&lt;/code&gt; matches the ranking strategy, so the storage layer isn't fighting the retrieval layer.&lt;/p&gt;

&lt;p&gt;What's nice about HNSW here is that it matches the shape of the workload I care about: lots of dense vector searches, with a metadata filter that keeps the working set scoped before anything gets ranked. I'm not asking the index to do the filter's job. Each piece does the job it's good at. The metadata predicate narrows the rows, and the vector index ranks whatever remains.&lt;/p&gt;

&lt;p&gt;That separation is what keeps the system predictable. If I ever need to inspect performance, I know where to look. Wrong neighbors showing up, I inspect the filter. Right neighbors arriving slowly, I inspect the index and the shape of the vector column. The responsibilities don't blur together.&lt;/p&gt;

&lt;h2&gt;
  
  
  What went wrong the first time
&lt;/h2&gt;

&lt;p&gt;The first broken version of this path made the request feel more flexible than it really was. The caller knew one thing, the search function inferred another, and the database was left to reconcile them later. That's accidental complexity of exactly the sort that makes retrieval bugs hard to pin down.&lt;/p&gt;

&lt;p&gt;The visible symptom was a result set that looked sane at a glance. The hidden problem was a request that didn't fully describe the scope of the search. Which is why the bug survived long enough to matter. Nothing crashed. Nothing timed out. The system answered the wrong question with confidence.&lt;/p&gt;

&lt;p&gt;Once I stopped spreading that decision across layers, the failure mode went away. The request object became the single place where I could answer three questions at once:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What text or embedding is this search about?&lt;/li&gt;
&lt;li&gt;How deep should the candidate set be?&lt;/li&gt;
&lt;li&gt;Which metadata fields are allowed to participate?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Much better debugging surface than a trail of local variables and implicit defaults. When I have to reason about a bad answer, I want to reason about one request object and one SQL function. That's enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I trust the boundary now
&lt;/h2&gt;

&lt;p&gt;The version I trust is the one where the search request is explicit enough that the database never has to guess. The caller passes the vector, the candidate count, and the JSONB filter in one place. The SQL function applies the filter inside the ranking query. The HNSW index is built on the same embedding column the function reads. Every step agrees on the same shape.&lt;/p&gt;

&lt;p&gt;That's the whole reason search scope lives inside a single Supabase RPC. Not because it's fashionable, and not because it makes the code shorter, but because it keeps the search intent attached to the request that asked for it. The RPC boundary becomes the line where scope is declared and enforced.&lt;/p&gt;

&lt;p&gt;Once I made that change, retrieval stopped feeling like a chain of guesses and started feeling like a reliable interface again. That matters in a system where a correct answer is only useful if it comes from the right slice of data. In the next pass, I'm pushing that same discipline further into the ingestion side, because retrieval only stays trustworthy when the embeddings and metadata that feed it are just as deliberate.&lt;/p&gt;




&lt;p&gt;🎧 &lt;strong&gt;Listen to the audiobook&lt;/strong&gt; — &lt;a href="https://open.spotify.com/show/4ABVd5yDVfbX9HlV5JjT7D" rel="noopener noreferrer"&gt;Spotify&lt;/a&gt; · &lt;a href="https://play.google.com/store/audiobooks/details/How_to_Architect_an_Enterprise_AI_System_And_Why_t?id=AQAAAECafz8_tM&amp;amp;hl=en" rel="noopener noreferrer"&gt;Google Play&lt;/a&gt; · &lt;a href="https://www.craftedbydaniel.com/audiobook" rel="noopener noreferrer"&gt;All platforms&lt;/a&gt;&lt;br&gt;
🎬 &lt;a href="https://youtube.com/playlist?list=PLRteDbGJPYDb9XNjecvHplGlgW7tIv_q6" rel="noopener noreferrer"&gt;Watch the visual overviews on YouTube&lt;/a&gt;&lt;br&gt;
📖 &lt;a href="https://www.craftedbydaniel.com/blog/series/how-to-architect-an-enterprise-ai-system-and-why-the-engineer-still-matters" rel="noopener noreferrer"&gt;Read the full 13-part series&lt;/a&gt;&lt;/p&gt;

</description>
      <category>supabase</category>
      <category>postgres</category>
      <category>pgvector</category>
      <category>rag</category>
    </item>
    <item>
      <title>The AgentGroupChat Pattern That Keeps the Mapper from Drifting</title>
      <dc:creator>Daniel Romitelli</dc:creator>
      <pubDate>Mon, 27 Jul 2026 16:02:52 +0000</pubDate>
      <link>https://dev.to/romiteld/the-agentgroupchat-pattern-that-keeps-the-mapper-from-drifting-101e</link>
      <guid>https://dev.to/romiteld/the-agentgroupchat-pattern-that-keeps-the-mapper-from-drifting-101e</guid>
      <description>&lt;p&gt;The first version of this orchestration failed in a very specific way: the mapper kept producing templates that looked plausible to a human and were wrong for the platform. The failure was not that the model could not reason. What I had built was one broad conversation carrying too many jobs at once, so a wrong structure got accepted early and then rode all the way to the end. By the time the validator complained, the chain had already lost the distinction between analysis, mapping, generation, and approval.&lt;/p&gt;

&lt;p&gt;So I stopped treating the system like a prompt stack and started treating it like a state machine. In the workflow analyzer SaaS, &lt;code&gt;runner/azure_foundry/src/orchestrator.py&lt;/code&gt; does the important work. It creates the kernel, registers the plugins, assembles the agents, defines who speaks next, and decides when the run is done. Wrapped around that orchestration is the state manager in &lt;code&gt;runner/azure_foundry/src/state_manager.py&lt;/code&gt;, which lets a run resume from the last durable message history instead of inventing a fresh conversation every time. One design choice, and the behavior of the whole pipeline changed.&lt;/p&gt;

&lt;p&gt;The architecture I wanted was easy to describe and annoying to get right. Each agent gets one contract. Each contract produces one kind of output. The validator is the gate that decides whether that output can move forward. The analyzer looks at the workflow analysis and identifies automation opportunity. The mapper turns that opportunity into abstract integration steps. The generator turns the mapped steps into a platform-shaped template. And the validator checks the structure, then either approves the run or sends it back to the generator.&lt;/p&gt;

&lt;p&gt;Splitting the roles was part of what made this work. The other part was making the next speaker explicit, which is what &lt;code&gt;AgentGroupChat&lt;/code&gt; gives me here: a selection strategy, a termination strategy, and a history buffer that can be rehydrated before the chat starts. With those pieces in place, the system stops behaving like a free-form exchange and starts behaving like a controlled pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  What broke first
&lt;/h2&gt;

&lt;p&gt;The first architecture looked tidy on paper and failed under repetition. I had an analysis step, a mapping step, a generation step, and a validation step, but the conversation around them was too loose. The mapper started inventing platform syntax because nothing in the orchestration made that impossible. The generator then built on top of that invented syntax. The validator only saw the problem after the wrong shape had already been repeated several times.&lt;/p&gt;

&lt;p&gt;Retries were worse. If the run timed out or the validator rejected the output, the next attempt often lost the conversation history that explained why the output had failed, so the system retried from a weak starting point and repeated the same mistake. What it needed was not more creativity. It needed a strict memory of what had already happened, plus a strict rule about which agent was allowed to correct which class of mistake.&lt;/p&gt;

&lt;p&gt;Hence the narrow contract chain with validator-led convergence. The validator does not try to repair everything. It does one job: decide whether the generated template is acceptable. No, and the generator gets another pass. Yes, and the run terminates. Correction stays local instead of every agent piling into every mistake.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core cycle in &lt;code&gt;AgentGroupChat&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The orchestration in &lt;code&gt;runner/azure_foundry/src/orchestrator.py&lt;/code&gt; is built around &lt;code&gt;AgentGroupChat&lt;/code&gt; with two strategies attached: a selection strategy that decides the next speaker, and a termination strategy that decides when to stop. That pairing is the heart of the pattern.&lt;/p&gt;

&lt;p&gt;The selection strategy reads the conversation history and follows a fixed pipeline:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Analyzer speaks first.&lt;/li&gt;
&lt;li&gt;Mapper speaks second.&lt;/li&gt;
&lt;li&gt;Generator speaks third.&lt;/li&gt;
&lt;li&gt;Validator speaks fourth.&lt;/li&gt;
&lt;li&gt;If the validator returns INVALID, the next speaker is the Generator again.&lt;/li&gt;
&lt;li&gt;If the validator returns WORKFLOW_APPROVED, the chat ends.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Compare that to a single model answering everything inside one giant instruction block. Different shape entirely. The selection strategy makes the pipeline visible to the runtime, and it gives me somewhere to encode the retry rule explicitly instead of hiding it inside a paragraph of instructions.&lt;/p&gt;

&lt;p&gt;The termination strategy carries just as much weight. The validator is the only agent whose result can end the run, and the run still has a maximum iteration cap of 10 so a bad cycle cannot spin forever. That cap earns its place because agent cycles do not usually fail in dramatic ways. They fail by wobbling. Repeat a small wobble enough times and you get wasted tokens, delayed jobs, and outputs that never settle.&lt;/p&gt;

&lt;p&gt;Here is the shape of that control flow in a standalone Python example that mirrors the same idea. It does not depend on Azure or Semantic Kernel, but it shows the exact contract I want the orchestrator to enforce:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;enum&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Enum&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AgentName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Enum&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;ANALYZER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Analyzer&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
    &lt;span class="n"&gt;MAPPER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Mapper&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
    &lt;span class="n"&gt;GENERATOR&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Generator&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
    &lt;span class="n"&gt;VALIDATOR&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Validator&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;


&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;rehydrate_history&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;history_state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="nc"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;history_state&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;next_speaker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;history&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;AgentName&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;history&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;AgentName&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ANALYZER&lt;/span&gt;

    &lt;span class="n"&gt;last&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;history&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;role&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;AgentName&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VALIDATOR&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;WORKFLOW_APPROVED&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;INVALID&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;AgentName&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GENERATOR&lt;/span&gt;

    &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;AgentName&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ANALYZER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;AgentName&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MAPPER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;AgentName&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GENERATOR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;AgentName&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VALIDATOR&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;seen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;role&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;history&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;role&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;AgentName&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ANALYZER&lt;/span&gt;

    &lt;span class="n"&gt;last_agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AgentName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;last_agent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;restored&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;rehydrate_history&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Analyzer&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Detected CRM handoff&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Analyzer&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Mapper&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Trigger + action pair identified&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Mapper&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Generator&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;triggers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: [], &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;actions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: []}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Generator&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Validator&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;INVALID: missing triggers and actions&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Validator&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;next_speaker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;restored&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Small as it is, that example captures the behavior I care about most: the validator does not restart the chain, and the chain does not forget where it left off. If the validator rejects the output, the next speaker is the Generator, not the Analyzer, because the analysis step already did its job.&lt;/p&gt;

&lt;h2&gt;
  
  
  How history gets rehydrated before the run
&lt;/h2&gt;

&lt;p&gt;Resumability matters here because the orchestration runs inside Prompt Flow, not inside a single in-memory toy conversation. In the real flow, &lt;code&gt;state_manager.py&lt;/code&gt; loads the prior session history before the orchestrator node runs. That history arrives as &lt;code&gt;history_state&lt;/code&gt;, and the orchestrator reconstructs &lt;code&gt;ChatMessageContent&lt;/code&gt; objects from each saved message so &lt;code&gt;AgentGroupChat&lt;/code&gt; can continue the conversation instead of starting over.&lt;/p&gt;

&lt;p&gt;Easy detail to miss. Expensive one to ignore. Without rehydration, every timeout becomes a reset. With it, the system can pick up from the last durable state and keep going. If the validator has already rejected a malformed template, that rejection stays in history. If the mapper already established the target platform and the apps involved, that context is still available. If the previous run ended halfway through generation, the next run does not have to relearn the same facts.&lt;/p&gt;

&lt;p&gt;The lifecycle is straightforward:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;load session history from &lt;code&gt;state_manager.py&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;reconstitute the saved messages into the chat history&lt;/li&gt;
&lt;li&gt;run the orchestrator with the restored history&lt;/li&gt;
&lt;li&gt;persist the resulting execution state after the run&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Durability comes from exactly that. The system continues the same conversation under the same constraints, rather than pretending the previous attempt never happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  The template library earns its keep
&lt;/h2&gt;

&lt;p&gt;The template library in &lt;code&gt;runner/template_library&lt;/code&gt; hands the generator a starting point that already matches the platform family. The generator should not be inventing a whole import shape from memory. It should be filling a known skeleton with detected apps, triggers, and actions.&lt;/p&gt;

&lt;p&gt;Starting from a template base, the generator can spend its effort on the parts that actually need reasoning: mapping the workflow into the right platform structure, inserting the right application names, filling the right fields. The library keeps it from wandering into a malformed top-level structure.&lt;/p&gt;

&lt;p&gt;Which is also why the validator can stay narrow. If the generator is working from a known shape, the validator does not need to be a general-purpose critic. The validator only has to check the contract that matters for the chosen platform: do the expected keys exist, is the structure complete, and does the result satisfy the platform rules well enough to be accepted?&lt;/p&gt;

&lt;p&gt;Every piece leans on the others. The template library constrains the starting point, the generator fills it, the validator checks it, and the selection strategy decides whether the system should move forward or send the work back to the generator.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real runtime wiring in the orchestrator
&lt;/h2&gt;

&lt;p&gt;The orchestrator itself is where the system becomes code-first instead of prompt-first. The kernel gets initialized with Azure OpenAI configuration from the environment, the validation plugin is registered, and the template library plugin is registered, all before the conversation begins. Then the agents are assembled and the selection and termination strategies are attached.&lt;/p&gt;

&lt;p&gt;Ordering is not incidental here. The model cannot speak before the kernel has its service. The generator cannot rely on the template library unless the plugin is present. The validator cannot enforce the schema unless the validation plugin is loaded. Writing the orchestration as code makes those dependencies explicit, where a prose prompt would leave them implicit.&lt;/p&gt;

&lt;p&gt;The runtime wiring also keeps the Azure configuration outside the orchestration logic itself. The environment supplies the deployment name, endpoint, and API key, and the kernel uses those values when it constructs the Azure chat completion service. Orchestration stays focused on behavior rather than connection plumbing.&lt;/p&gt;

&lt;p&gt;In practice, the structure looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
  load[Load state by session_id] --&amp;gt; rehydrate[Rebuild chat history]
  rehydrate --&amp;gt; analyzer[Analyzer]
  analyzer --&amp;gt; mapper[Mapper]
  mapper --&amp;gt; generator[Generator]
  generator --&amp;gt; validator[Validator]
  validator -- WORKFLOW_APPROVED --&amp;gt; stop[Terminate]
  validator -- INVALID --&amp;gt; generator
  validator -- other --&amp;gt; generator
  stop --&amp;gt; persist[Persist state]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important arrow is the one that returns from Validator to Generator. That is the local correction path. Invalid output does not reset the analysis, erase the mapping, or jump back to the start. It goes back to the one agent responsible for producing the template.&lt;/p&gt;

&lt;h2&gt;
  
  
  A runnable validator that actually checks structure
&lt;/h2&gt;

&lt;p&gt;The validator must reject malformed output or it is just another polite participant in the conversation. Here is a minimal, runnable version that checks a template shape and returns a real verdict:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;


&lt;span class="n"&gt;SUPPORTED_PLATFORMS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;zapier&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;make&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;n8n&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;select_template&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;platform_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;platform_key&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;SUPPORTED_PLATFORMS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Unsupported platform: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;platform&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;platform_key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;_automation&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;triggers&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;actions&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;validate_template&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;template_json&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;template&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;template_json&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JSONDecodeError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;invalid&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;errors&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Invalid JSON: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;triggers&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;actions&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Missing key: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;SUPPORTED_PLATFORMS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Unsupported platform: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;platform&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;triggers&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;triggers&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;At least one trigger is required&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;actions&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;actions&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;At least one action is required&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;valid&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;invalid&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;errors&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;bad&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;demo&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;triggers&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;actions&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]})&lt;/span&gt;
    &lt;span class="n"&gt;good&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;demo&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;triggers&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;new_event&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;actions&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;create_record&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]})&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;validate_template&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bad&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Zapier&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;validate_template&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;good&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Zapier&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the behavior I want the real validator path to approximate: reject missing structure, reject empty structure, and hand back a verdict the selection strategy can use to decide the next speaker. The exact shape of this standalone example matters far less than the discipline behind it. Validation is not a suggestion. It is a gate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this pattern is easier to trust
&lt;/h2&gt;

&lt;p&gt;This design works because every failure has one owner. Weak analysis, and the Analyzer is the problem. Invented structure in the mapping, and the Mapper is the problem. A template that does not fit the platform, and the Generator is the problem. A template that violates the contract, and the Validator says so. Debugging gets much easier when I am not trying to infer which layer went wrong from a cloud of blended instructions.&lt;/p&gt;

&lt;p&gt;The retry logic gets saner too. When the validator rejects the output, I do not want the entire conversation to restart. I want the generator to take another pass with the same context still in memory, and that is what the selection strategy enforces: the orchestrator reads the last message, sees the invalid verdict, and routes execution back to the generator. Nobody else has to renegotiate the earlier steps.&lt;/p&gt;

&lt;p&gt;State rehydration matters here because a retry is a different animal from a fresh run. A fresh run throws away the exact information I need most: what failed, what was already agreed, and which platform shape was already chosen. Rehydration preserves that state so the next attempt can repair the actual fault instead of replaying the whole conversation.&lt;/p&gt;

&lt;p&gt;What comes out of it is a system that can fail locally without collapsing globally. Sounds like a small thing until you watch a long run survive a rejection, recover from persisted history, and land on a valid template without re-deriving the entire workflow from scratch.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually settled it
&lt;/h2&gt;

&lt;p&gt;Using multiple agents is not the strongest part of this orchestration. What settled things was the constraint: agents bounded by code, the next speaker chosen by strategy, the validator owning the final gate, and session history that survives retries. Once those pieces were in place, the mapper stopped wandering and the generator stopped improvising against the wrong shape.&lt;/p&gt;

&lt;p&gt;That gave me exactly what I wanted from the workflow analyzer SaaS: a chain that can analyze, map, generate, validate, and resume without pretending a failed attempt never happened. Next I am interested in pushing more of the template shape into the library itself, so the generator begins with an even tighter platform skeleton and the validator has less to reject in the first place.&lt;/p&gt;




&lt;p&gt;🎧 &lt;strong&gt;Listen to the audiobook&lt;/strong&gt; — &lt;a href="https://open.spotify.com/show/4ABVd5yDVfbX9HlV5JjT7D" rel="noopener noreferrer"&gt;Spotify&lt;/a&gt; · &lt;a href="https://play.google.com/store/audiobooks/details/How_to_Architect_an_Enterprise_AI_System_And_Why_t?id=AQAAAECafz8_tM&amp;amp;hl=en" rel="noopener noreferrer"&gt;Google Play&lt;/a&gt; · &lt;a href="https://www.craftedbydaniel.com/audiobook" rel="noopener noreferrer"&gt;All platforms&lt;/a&gt;&lt;br&gt;
🎬 &lt;a href="https://youtube.com/playlist?list=PLRteDbGJPYDb9XNjecvHplGlgW7tIv_q6" rel="noopener noreferrer"&gt;Watch the visual overviews on YouTube&lt;/a&gt;&lt;br&gt;
📖 &lt;a href="https://www.craftedbydaniel.com/blog/series/how-to-architect-an-enterprise-ai-system-and-why-the-engineer-still-matters" rel="noopener noreferrer"&gt;Read the full 13-part series&lt;/a&gt;&lt;/p&gt;

</description>
      <category>semantickernel</category>
      <category>azurefoundry</category>
      <category>promptflow</category>
      <category>agentorchestration</category>
    </item>
    <item>
      <title>Validation Geometry Is Part of the Model</title>
      <dc:creator>Daniel Romitelli</dc:creator>
      <pubDate>Mon, 27 Jul 2026 16:02:41 +0000</pubDate>
      <link>https://dev.to/romiteld/validation-geometry-is-part-of-the-model-nhn</link>
      <guid>https://dev.to/romiteld/validation-geometry-is-part-of-the-model-nhn</guid>
      <description>&lt;p&gt;The dangerous part was not the classifier. The label file was, sitting there looking convenient.&lt;/p&gt;

&lt;p&gt;A saved &lt;code&gt;train_y.npy&lt;/code&gt; artifact existed, and for this baseline it was a trap. It contained magnitude-filtered positive targets only, which made it unusable for a directional classifier. Train on it anyway and the result would look like a model comparison while actually being a dataset-artifact comparison.&lt;/p&gt;

&lt;p&gt;So I made the LightGBM minute baseline read Pramaana's per-asset feature parquet files directly. I wasn't trying to tune trees until they confessed. I was trying to close the model-class capacity objection in the minute-level ceiling experiment without letting label construction, overlapping windows, or temporal bleed sneak into the room wearing a lab coat.&lt;/p&gt;

&lt;p&gt;The research question behind the ICAIF 2026 paper is deliberately narrow: minute-scale cryptocurrency direction from OHLCV candles appears reproducibly capped near 52% across a broad set of model and feature configurations, while the same research stack recovers materially more directional information at the hourly horizon. Across seven minute configurations and approximately 36 million rows of minute-scale OHLCV history, the observed range is 51.4% to 52.3%. The LightGBM baseline is the seventh configuration: 46 microstructure-proxy features, a 15-minute forward return target in basis points, a stride-15 de-overlap, a no-trade filter at &lt;code&gt;|target| &amp;gt; 10 BPS&lt;/code&gt;, and a per-asset 85/15 temporal split with a 15-row purge before validation.&lt;/p&gt;

&lt;p&gt;The transferable idea is simple and annoyingly easy to violate: in time-series ML, validation geometry isn't bookkeeping. It's part of the model.&lt;/p&gt;

&lt;h2&gt;
  
  
  The baseline was a geometry test, not a tuning contest
&lt;/h2&gt;

&lt;p&gt;A naive capacity comparison asks, “Does a different model class beat the neural setup?” That sounds reasonable until the labels are temporal, overlapping, filtered, and asset-scoped. The better question is, “Can I compare model classes without changing the target semantics or leaking nearby time into validation?”&lt;/p&gt;

&lt;p&gt;So the script documents the protocol before it imports anything. That docstring is doing more than explaining a file. It pins down the shape of the experiment.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;#!/usr/bin/env python3
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Run a LightGBM directional baseline on the frozen M6 sniper matrix.

This closes the model-class capacity objection for the paper&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s minute-level
ceiling section. The script reads Pramaana&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s per-asset
``data/tmp_sniper_feat_*.parquet`` files directly because the saved
``train_y.npy`` artifact currently contains magnitude-filtered positive targets
only and is therefore not usable for a directional classifier.

Protocol matched to ``scripts/preprocess_sniper.py``:
  - 46 engineered microstructure-proxy features
  - 15-minute forward return target in BPS
  - stride-15 de-overlap
  - |target| &amp;gt; 10 BPS no-trade filter
  - per-asset 85/15 temporal split with a 15-row purge before validation
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;__future__&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;annotations&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;lightgbm&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;lgb&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;polars&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pl&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;scipy&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;stats&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sklearn.metrics&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;balanced_accuracy_score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;roc_auc_score&lt;/span&gt;


&lt;span class="n"&gt;TRAIN_RATIO&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.85&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I like this kind of comment because you can falsify it. Every important experimental choice is named: feature count, target horizon, de-overlap, no-trade filtering, split ratio, and purge width. If the number later changes, the protocol has to change with it. (The real script also exposes these knobs as &lt;code&gt;argparse&lt;/code&gt; CLI flags; I trimmed the flag-parsing boilerplate from this excerpt to keep the protocol in focus.)&lt;/p&gt;

&lt;p&gt;The saved label artifact failed the most basic requirement for this comparison. It did not represent both directions, and directional classification needs the sign of the target. Magnitude-filtered positive targets only? That's a different task, not a neutral shortcut. So the baseline reconstructs the binary label from the 15-minute forward return in basis points inside the feature-parquet path, rather than inheriting a label artifact whose semantics were already wrong for this purpose.&lt;/p&gt;

&lt;p&gt;There's a useful mental model here. A time-series split is less like cutting a deck of cards and more like cutting wet paint. The boundary smears unless you leave room for it to dry. In this baseline, the 15-row purge is that dry strip between training and validation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The timeline is the experiment
&lt;/h2&gt;

&lt;p&gt;The pipeline has only a few stages, but the order matters. The baseline starts from per-asset parquet files, reconstructs the directional target from the forward return, filters out the no-trade zone, de-overlaps with stride 15, applies a per-asset temporal split, inserts a purge gap, and only then fits and validates the model.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
  parquet[Per asset feature parquet] --&amp;gt; labels[Reconstruct sign label]
  labels --&amp;gt; filter[No trade filter]
  filter --&amp;gt; stride[Stride 15 de overlap]
  stride --&amp;gt; split[Per asset temporal split]
  split --&amp;gt; train[Training interval]
  split --&amp;gt; purge[15 row purge gap]
  purge --&amp;gt; validate[Validation interval]
  validate --&amp;gt; metrics[Accuracy and AUC]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The diagram looks almost too ordinary, which is exactly the trap. The ordinary-looking arrows are where most of the statistical damage would happen if they were skipped or reordered.&lt;/p&gt;

&lt;p&gt;For each asset, the geometry is anchored by time rather than random assignment. The training interval comes first. The validation interval comes last. The purge gap sits between them. The overlapping-window hazard is caused by the target construction itself: a 15-minute forward return means nearby rows can share future information unless the split respects the horizon. The stride-15 de-overlap reduces that hazard, and the purge gap protects the split boundary.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Geometry choice&lt;/th&gt;
&lt;th&gt;What it protects against&lt;/th&gt;
&lt;th&gt;Concrete value in this baseline&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Per-asset temporal split&lt;/td&gt;
&lt;td&gt;Cross-time contamination within each asset&lt;/td&gt;
&lt;td&gt;85/15 train/validation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Purge gap&lt;/td&gt;
&lt;td&gt;Boundary bleed from nearby rows&lt;/td&gt;
&lt;td&gt;15 rows before validation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;De-overlap&lt;/td&gt;
&lt;td&gt;Repeated labels from overlapping horizons&lt;/td&gt;
&lt;td&gt;stride-15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No-trade filter&lt;/td&gt;
&lt;td&gt;Tiny targets treated as tradable direction&lt;/td&gt;
&lt;td&gt;`&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Label reconstruction&lt;/td&gt;
&lt;td&gt;Wrong task inherited from saved labels&lt;/td&gt;
&lt;td&gt;sign of 15-minute forward return in BPS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Raw tree features&lt;/td&gt;
&lt;td&gt;Scaling mismatch for tree baseline&lt;/td&gt;
&lt;td&gt;tree model does not require RobustScaler&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The naive version would be shorter. Load {% raw %}&lt;code&gt;X&lt;/code&gt;, load &lt;code&gt;y&lt;/code&gt;, fit classifier, report accuracy. It would also be wrong in exactly the way that makes a result hard to debug: the code would run, the metrics would print, and the comparison would look scientific. The error would live in the meaning of &lt;code&gt;y&lt;/code&gt; and the geometry of the split, not in a stack trace.&lt;/p&gt;

&lt;p&gt;That distinction matters because most bad financial ML baselines do not fail loudly. They often fail by making the wrong thing convenient. A cached label array, a random split helper, a global shuffle, a validation set reused for early stopping, a forward-return target created before de-overlap: none of these choices necessarily creates an obvious programming error. They create an evidentiary error. The model may be implemented correctly while the experiment answers a question I did not intend to ask.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the saved label file had to be rejected
&lt;/h2&gt;

&lt;p&gt;A directional classifier is only as honest as its labels. In this baseline, the target is “15-minute forward return in BPS; binary label is sign(target).” That gives the model a two-sided problem: up versus down after filtering out the no-trade zone.&lt;/p&gt;

&lt;p&gt;The existing saved &lt;code&gt;train_y.npy&lt;/code&gt; artifact did not satisfy that contract. It contained magnitude-filtered positive targets only. That makes it unsuitable for a directional classifier because it no longer represents the binary sign task the baseline is supposed to measure. There is no clever model-side fix for that. Once the target artifact encodes the wrong task, using a different classifier just gives the wrong task a new costume.&lt;/p&gt;

&lt;p&gt;The baseline therefore goes back to &lt;code&gt;data/tmp_sniper_feat_*.parquet&lt;/code&gt;. That choice matters because the feature parquet is upstream of the bad shortcut. It lets the script reconstruct labels under the same protocol used by the sniper preprocessing path: 46 engineered microstructure-proxy features, 15-minute forward return in basis points, stride-15 de-overlap, the no-trade filter, and the per-asset temporal split with purge.&lt;/p&gt;

&lt;p&gt;This is the part of baseline design that feels unglamorous but decides whether the result means anything. A model-class objection says, “Maybe the neural family is the reason the minute result clusters near 52%.” A contaminated label artifact would make the answer meaningless. Reconstructing labels from feature parquet keeps the comparison focused on capacity instead of accidentally comparing two different tasks.&lt;/p&gt;

&lt;p&gt;The same principle applies outside this specific paper. If an intermediate artifact was built for a different objective, it is not a neutral cache. It is an encoded research decision. A saved target array can carry filtering, horizon, class definition, censoring, asset selection, and split assumptions. If those assumptions no longer match the experiment, downstream code should not pretend the file is just bytes on disk. It is a contract, and in this case the contract was wrong for the classifier I needed to run.&lt;/p&gt;

&lt;h2&gt;
  
  
  The capacity objection needed a clean target
&lt;/h2&gt;

&lt;p&gt;The minute-ceiling section reports seven configurations. The first three use tens of millions of labels and increasingly rich feature sets. The fourth and fifth reduce sample size but de-overlap and alter the loss. The sixth uses a different 15-minute basis-point target and compact microstructure-proxy features. The seventh replaces the neural CQR family with a classical tree model on the frozen microstructure-proxy matrix.&lt;/p&gt;

&lt;p&gt;That seventh row is the key capacity-control move. If the ceiling were merely an artifact of the neural setup, a different model class on the same frozen feature/target construction should have had a chance to break away. Instead, the LightGBM baseline reports 52.231% accuracy with CI &lt;code&gt;[52.042, 52.420]&lt;/code&gt;, AUC &lt;code&gt;0.53046&lt;/code&gt;, balanced accuracy &lt;code&gt;52.21%&lt;/code&gt;, and &lt;code&gt;n=268342&lt;/code&gt; validation samples. In the minute configurations table, that appears as 52.23% ± 0.189 for row 7.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The ceiling holds: 52.231% accuracy, 95% CI &lt;code&gt;[52.042, 52.420]&lt;/code&gt;.&lt;/strong&gt;&lt;br&gt;
Swap the neural CQR family for a classical tree on the same frozen feature and target matrix, and the result lands in the same 51.4–52.3% band. The minute ceiling is not an artifact of the neural setup.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For metric provenance, this was a CPU-only LightGBM run from &lt;code&gt;scripts/run_lightgbm_sniper_baseline.py&lt;/code&gt; using the Python scientific stack in the paper environment: Polars for parquet loading, NumPy for arrays, SciPy for the binomial interval and significance calculation, scikit-learn metrics for balanced accuracy and AUC, and the LightGBM scikit-learn API for the classifier. The run writes &lt;code&gt;data/lightgbm_sniper_baseline.json&lt;/code&gt; and &lt;code&gt;data/lightgbm_sniper_baseline_model.txt&lt;/code&gt;; the reported metric is taken from that JSON artifact, not copied by hand into the manuscript.&lt;/p&gt;

&lt;p&gt;The important part is not that LightGBM has a particular personality. It is that the baseline swapped model class while holding the validation geometry and target construction in place. Without that, “LightGBM versus neural” would be a noisy argument about everything except the model.&lt;/p&gt;

&lt;p&gt;The training block reflects that narrow purpose. It uses a tree classifier with raw engineered features, reserves the final 15% of the training side for early stopping, and computes class weighting from the fit subset. The held-out validation interval remains untouched by early stopping.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;X_train&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y_train&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;X_val&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y_val&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;feature_names&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;asset_stats&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_load_sniper_arrays&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pramaana&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;fit_end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X_train&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.85&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;X_fit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;X_es&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;X_train&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="n"&gt;fit_end&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;X_train&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;fit_end&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt;
&lt;span class="n"&gt;y_fit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y_es&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;y_train&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="n"&gt;fit_end&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;y_train&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;fit_end&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt;

&lt;span class="n"&gt;n_pos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y_fit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="n"&gt;n_neg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y_fit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;n_pos&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;scale_pos_weight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;n_neg&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n_pos&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;clf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lgb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;LGBMClassifier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;n_estimators&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;learning_rate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.03&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;num_leaves&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;31&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;max_depth&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;min_child_samples&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;subsample&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.85&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;colsample_bytree&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.85&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;reg_alpha&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;reg_lambda&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;scale_pos_weight&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;scale_pos_weight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;objective&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;binary&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;metric&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;binary_logloss&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;n_jobs&lt;/span&gt;&lt;span class="o"&gt;=-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;random_state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;force_col_wise&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;verbosity&lt;/span&gt;&lt;span class="o"&gt;=-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;clf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;X_fit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;y_fit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;eval_set&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;X_es&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y_es&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
    &lt;span class="n"&gt;eval_metric&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;binary_logloss&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;callbacks&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="n"&gt;lgb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;early_stopping&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stopping_rounds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;lgb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log_evaluation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;period&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The hyperparameters are not the story I care about here. The non-obvious detail is the split inside the training side: the final 15% of train is used only for early stopping, which keeps validation as the final held-out interval rather than letting it become a tuning surface.&lt;/p&gt;

&lt;p&gt;That one choice prevents a common baseline failure. If I had passed the paper validation interval as the LightGBM &lt;code&gt;eval_set&lt;/code&gt;, early stopping would have made the validation set part of the training procedure. The model would not directly fit labels from validation, but the selected number of boosting rounds would be chosen by validation performance. That is enough to contaminate the final metric. The point of the baseline is not to squeeze the last basis point out of LightGBM; it is to answer whether a classical tree classifier breaks the minute ceiling under the same target and split discipline.&lt;/p&gt;

&lt;p&gt;The results JSON captures the protocol in a compact form. This is the artifact I want beside the paper because it records not only the metric, but also the target, preprocessing, split, and scaling assumptions that make the metric interpretable.&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"LightGBM M7 sniper baseline"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"created_utc"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-05-04T02:58:24Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/home/the author/Development/Python/crypto-fl-v2/data/tmp_sniper_feat_*.parquet"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"protocol"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"feature_set"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"46 microstructure proxy features"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"15-minute forward return in BPS; binary label is sign(target)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"preprocessing"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"stride-15 de-overlap; |target| &amp;gt; 10 BPS no-trade filter"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"split"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Per-asset 85/15 temporal split with 15-row purge before validation; final 15% of train used only for early stopping"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"scaling"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Raw engineered features; tree model does not require RobustScaler"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"train_samples"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1520254&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"early_stop_samples"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;228039&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"validation_samples"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;268342&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"n_features"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;46&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I care more about the &lt;code&gt;protocol&lt;/code&gt; object than the timestamp. A metric without this surrounding geometry is just a number looking for a story, and the protocol is what stops the wrong story from attaching itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Temporal bleed arrives without a stack trace
&lt;/h2&gt;

&lt;p&gt;The hard part about temporal bleed is that it rarely announces itself. No exception fires to tell you, “Your validation rows are too close to your training rows.” What you get is a better-looking number, and that number is seductive because it can be explained as model quality.&lt;/p&gt;

&lt;p&gt;The minute experiments are especially exposed to this because the target horizon is short and overlapping labels are easy to create accidentally. A 15-minute forward return target means row &lt;code&gt;t&lt;/code&gt; and row &lt;code&gt;t+1&lt;/code&gt; can be describing heavily shared future intervals. If the split boundary cuts through those neighborhoods without a purge, the validation side can remain too close to what the training side has already seen.&lt;/p&gt;

&lt;p&gt;So the geometry is engineered at multiple levels rather than resting on one protective trick.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Hazard&lt;/th&gt;
&lt;th&gt;Naive baseline failure&lt;/th&gt;
&lt;th&gt;Geometry response&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Saved label artifact has wrong semantics&lt;/td&gt;
&lt;td&gt;Classifier trains on a target that is not the directional task&lt;/td&gt;
&lt;td&gt;Reconstruct labels from feature parquet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Adjacent windows share future information&lt;/td&gt;
&lt;td&gt;Validation resembles training near the boundary&lt;/td&gt;
&lt;td&gt;Insert 15-row purge before validation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Overlapping targets inflate sample familiarity&lt;/td&gt;
&lt;td&gt;Many rows encode nearly the same horizon&lt;/td&gt;
&lt;td&gt;Apply stride-15 de-overlap&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Asset timelines differ&lt;/td&gt;
&lt;td&gt;Global shuffle mixes unrelated time positions&lt;/td&gt;
&lt;td&gt;Split per asset temporally&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Early stopping erodes validation independence&lt;/td&gt;
&lt;td&gt;Held-out set becomes part of model selection&lt;/td&gt;
&lt;td&gt;Use final 15% of train only for early stopping&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is also why I resist treating train/test split as an afterthought in financial ML. For IID tabular data, the split is often a convenience. For time series, it's a claim about causality: what information was available before the prediction time, and what was not. If that claim is false, the model can be perfectly implemented and still be scientifically useless.&lt;/p&gt;

&lt;p&gt;The per-asset split matters for a second reason. Cryptocurrency pairs don't all have identical listing histories, liquidity regimes, or missing-data structure. A global split by row count would mix assets at unrelated calendar positions. A global random split would be worse. The baseline needs each asset’s validation samples to come from the end of that asset’s own history, with the purge applied at that asset boundary. That preserves the meaning of “future held out” even when the panel is irregular.&lt;/p&gt;

&lt;p&gt;The no-trade filter is a different kind of guardrail. It doesn't prevent leakage. What it prevents is the classifier being evaluated on economically tiny moves as if every infinitesimal return were a meaningful directional event. The target is still a research abstraction rather than a full trading simulator with costs and execution, but the &lt;code&gt;|target| &amp;gt; 10 BPS&lt;/code&gt; filter keeps the sign label from being dominated by noise around zero. That matters when the entire empirical question is about a narrow 51.4% to 52.3% band. At that scale, sloppy target construction can easily masquerade as something real.&lt;/p&gt;

&lt;h2&gt;
  
  
  The minute ceiling becomes more credible when the baseline cannot cheat
&lt;/h2&gt;

&lt;p&gt;The paper's data-and-methods section frames the smaller de-overlapped and microstructure-proxy runs as controls against cleaner labels, different short-horizon targets, and a classical tree classifier. The LightGBM row belongs to that logic. No new headline result. A stress test aimed at one specific objection.&lt;/p&gt;

&lt;p&gt;The minute ceiling is not “models cannot beat chance.” The reported range sits above chance in a statistical sense. The issue is effect size. Across the seven configurations, directional accuracy remains inside a 0.9 percentage-point band, from 51.4% to 52.3%. With sample sizes in the millions for the larger configurations, the open question isn't whether the models detect something. It's why substantial changes in features, scaling, loss functions, target construction, model class, and sample count don't move the result into a stronger range.&lt;/p&gt;

&lt;p&gt;The hourly positive control is what keeps this from becoming nihilism. At the hourly horizon, the FT-Transformer/CQR walk-forward run reached 54.69% directional accuracy on 386,056 future-held-out hourly samples, with a 95% interval of 54.53–54.84 and 80.53% conformal coverage. That result comes from the five-fold expanding walk-forward CQR run recorded in &lt;code&gt;backtest_results/walkforward_cqr_hourly.json&lt;/code&gt; and the corresponding resume log, executed with the PyTorch FT-Transformer/CQR training stack on the project’s CUDA workstation environment. One runtime distinction is worth stating plainly: the hourly result is not a LightGBM CPU run. It's a neural FT-Transformer/CQR evaluation under expanding walk-forward temporal validation, with the metric emitted by the walk-forward script and then pulled into &lt;code&gt;data/results_manifest.json&lt;/code&gt; for paper table generation.&lt;/p&gt;

&lt;p&gt;That contrast matters because it shows the research stack does not mechanically inflate every task. Minute OHLCV direction clusters near the ceiling. Hourly direction recovers more directional information under a stricter temporal protocol.&lt;/p&gt;

&lt;p&gt;But the contrast only means something if the minute baseline is clean. Had the LightGBM run used a broken label artifact, the row would not close the capacity objection. It would open a new hole. Reconstructing labels from parquet and enforcing split geometry is what lets the baseline answer the narrow question it was built to answer.&lt;/p&gt;

&lt;p&gt;The evidence-bundle design reinforces this. The paper repository is separate from the trading system because the paper is the manuscript and reproducibility layer, while Pramaana is the experiment apparatus. The paper artifacts point back to the scripts and result files that generated each table row: &lt;code&gt;scripts/run_lightgbm_sniper_baseline.py&lt;/code&gt;, &lt;code&gt;data/lightgbm_sniper_baseline.json&lt;/code&gt;, &lt;code&gt;data/lightgbm_sniper_baseline_model.txt&lt;/code&gt;, and the per-asset parquet inputs for the LightGBM capacity control; &lt;code&gt;backtest_results/walkforward_cqr_hourly.json&lt;/code&gt;, the walk-forward logs, and &lt;code&gt;scripts/walkforward_cqr_hourly.py&lt;/code&gt; for the hourly positive control. That's the level at which a result becomes inspectable. A table row should not be a manually typed number; it should be the visible tip of an artifact chain.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the feature importances can and cannot say
&lt;/h2&gt;

&lt;p&gt;The LightGBM run also writes a model file and feature importances. The top features include &lt;code&gt;return_60m&lt;/code&gt;, &lt;code&gt;z_effort_60m&lt;/code&gt;, &lt;code&gt;price_vs_vwap_5m&lt;/code&gt;, and &lt;code&gt;z_effort_15m&lt;/code&gt;. I don't treat those importances as a market theory. Tree feature importance is useful for checking that the model is not obviously broken. It isn't a causal explanation of minute returns.&lt;/p&gt;

&lt;p&gt;What it can say is more modest: the classifier found most of its splits in recent return, effort, and VWAP-relative features, which is consistent with the feature family the baseline was meant to test. That helps catch the opposite failure mode, a model that reports a plausible metric while accidentally training on an ID column, timestamp encoding, or mislabeled target. Feature inspection is not proof. It's a useful diagnostic once the validation geometry is already correct.&lt;/p&gt;

&lt;p&gt;This is also why I report AUC alongside accuracy. Accuracy answers the sign-decision question at the default threshold. AUC checks whether the probability ranking carries directional information independent of that threshold. In the LightGBM run, AUC is &lt;code&gt;0.53046&lt;/code&gt;, which aligns with the story told by 52.231% accuracy: the model is picking up something small, not discovering a dramatically separable classification boundary.&lt;/p&gt;

&lt;p&gt;The high-confidence slice is similarly restrained. The JSON records a threshold of &lt;code&gt;p &amp;gt;= 0.60 or p &amp;lt;= 0.40&lt;/code&gt;, with 2,305 samples, 0.859% coverage, and 57.007% accuracy. Interesting as a calibration and selectivity diagnostic. It does not rescue the minute problem. A tiny high-confidence region with better accuracy is not the same as a broad tradable edge, especially before costs, slippage, and execution constraints. For the paper’s central claim, the full validation metric is the right number to emphasize.&lt;/p&gt;

&lt;h2&gt;
  
  
  The discipline is to make shortcuts impossible
&lt;/h2&gt;

&lt;p&gt;The small engineering choice I'd generalize is this: when a saved artifact has ambiguous or wrong semantics, don't patch around it downstream. Rebuild from the nearest artifact whose meaning is still compatible with the experiment.&lt;/p&gt;

&lt;p&gt;In this case, the nearest compatible artifact was the per-asset feature parquet. That forced the script to carry the target definition, filtering rule, de-overlap policy, split geometry, purge width, and scaling assumption in the same place as the model run. Less convenient than loading &lt;code&gt;train_y.npy&lt;/code&gt;. More honest.&lt;/p&gt;

&lt;p&gt;There's a tradeoff. Reconstructing labels from parquet couples the baseline to the upstream feature files and requires the paper repo to reference Pramaana's data path. The evidence bundle handles that by recording local evidence anchors rather than pretending the manuscript repository contains every large matrix. Compact artifacts get tracked directly with metadata and hashes; larger upstream matrices are referenced by path and size unless full hashing is requested. An engineering compromise, and one that keeps the paper repository focused on reproducibility rather than becoming a warehouse for heavyweight experiment outputs.&lt;/p&gt;

&lt;p&gt;The payoff is that the baseline result has a shape I can defend. It says: on the frozen M6 sniper matrix, with 46 microstructure-proxy features, 15-minute forward-return sign labels, stride-15 de-overlap, a no-trade filter, per-asset temporal splitting, a 15-row purge, and a held-out validation interval, a classical tree model lands at 52.231% accuracy. That's a capacity-control statement, not a vague leaderboard entry.&lt;/p&gt;

&lt;p&gt;It also changes how I evaluate future baselines. I want the experiment script to make the invalid path difficult. If the wrong label file is easy to load, someone eventually will. If early stopping on validation is a one-line convenience, it will creep in. If a random split helper is available in the same file as time-series code, it will eventually be called in the wrong place. Good research code does not merely implement the correct protocol; it removes temptations that produce attractive but meaningless numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I changed in how I think about baselines
&lt;/h2&gt;

&lt;p&gt;I used to think of baselines as simpler models. I now think of them as simpler claims. A good baseline should remove one objection at a time. This LightGBM run removes the objection that the minute ceiling is merely a neural architecture artifact. It doesn't claim to solve execution, transaction costs, order-book dynamics, or richer market state. It says only that replacing the model family on this engineered short-horizon matrix does not break the ceiling.&lt;/p&gt;

&lt;p&gt;That restraint is the virtue. A baseline that tries to answer every objection simultaneously becomes another opaque system. Answer one objection under carefully preserved geometry and it becomes useful evidence.&lt;/p&gt;

&lt;p&gt;For time-series ML, the geometry is the evidence. The split, purge, stride, and label source aren't clerical details that come after the “real” modeling work. They're the rails that keep the model from learning yesterday's shadow of tomorrow.&lt;/p&gt;

&lt;p&gt;A classifier trained on the wrong artifact can look competent. A classifier trained inside the wrong timeline can look brilliant. I'd rather have the modest number I can trust than the impressive one produced by a boundary I forgot to draw. The next credible test won't be another minute-candle model with a larger parameter count. It will be the one that adds the state variables the minute horizon is missing (order-book pressure, queue dynamics, spread formation, and execution-aware microstructure) while keeping the same discipline about labels, time, and evidence.&lt;/p&gt;

&lt;p&gt;In time-series work, validation geometry is not an implementation detail. It is the contract that makes a model's number mean something. The same discipline that protects a live trading gate or a self-indexing memory write protects the scientific claim.&lt;/p&gt;




&lt;p&gt;🎧 &lt;strong&gt;Listen to the audiobook&lt;/strong&gt; — &lt;a href="https://open.spotify.com/show/4ABVd5yDVfbX9HlV5JjT7D" rel="noopener noreferrer"&gt;Spotify&lt;/a&gt; · &lt;a href="https://play.google.com/store/audiobooks/details/How_to_Architect_an_Enterprise_AI_System_And_Why_t?id=AQAAAECafz8_tM&amp;amp;hl=en" rel="noopener noreferrer"&gt;Google Play&lt;/a&gt; · &lt;a href="https://www.craftedbydaniel.com/audiobook" rel="noopener noreferrer"&gt;All platforms&lt;/a&gt;&lt;br&gt;
🎬 &lt;a href="https://youtube.com/playlist?list=PLRteDbGJPYDb9XNjecvHplGlgW7tIv_q6" rel="noopener noreferrer"&gt;Watch the visual overviews on YouTube&lt;/a&gt;&lt;br&gt;
📖 &lt;a href="https://www.craftedbydaniel.com/blog/series/how-to-architect-an-enterprise-ai-system-and-why-the-engineer-still-matters" rel="noopener noreferrer"&gt;Read the full 13-part series&lt;/a&gt;&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>timeseries</category>
      <category>financialml</category>
      <category>lightgbm</category>
    </item>
    <item>
      <title>Vector Split by Chunk: Why My Retrieval Stops at the Boundary I Drew</title>
      <dc:creator>Daniel Romitelli</dc:creator>
      <pubDate>Mon, 27 Jul 2026 16:02:31 +0000</pubDate>
      <link>https://dev.to/romiteld/vector-split-by-chunk-why-my-retrieval-stops-at-the-boundary-i-drew-3llf</link>
      <guid>https://dev.to/romiteld/vector-split-by-chunk-why-my-retrieval-stops-at-the-boundary-i-drew-3llf</guid>
      <description>&lt;p&gt;A draft of mine missed the exact file span it needed. The vector was "close." But the chunk I wanted was buried inside a larger blob, and the miss was clean enough to be embarrassing: the sort of thing that passes in a demo and grates in production, because the answer is almost right the way a blurry photo is almost a portrait.&lt;/p&gt;

&lt;p&gt;Dense retrieval people have a name for the mechanic underneath it. &lt;strong&gt;Representational collapse&lt;/strong&gt;. Ask one embedding to summarize an entire document and the model picks the dominant mode, then discards the long tail. The precise sentence you wanted lives in that tail. So you get back the right neighborhood and the wrong house, topically coherent and operationally useless.&lt;/p&gt;

&lt;p&gt;The fix is to stop asking one vector to do that work. Split the text before embedding, embed each slice on its own, and let retrieval search the slices instead of the document. Everything below falls out of that one decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  The boundary I chose on purpose
&lt;/h2&gt;

&lt;p&gt;I split embeddings by chunk because the retrieval layer needed a smaller unit than a document. A whole-file vector is too coarse when the system has to answer from a specific path, a specific code block, or a specific rewrite. A chunk sits in between: small enough to isolate one semantic mode, large enough to keep the local context wrapped around it.&lt;/p&gt;

&lt;p&gt;The implementation starts with a windowing function in &lt;code&gt;lib/supabaseVectorStorage.ts&lt;/code&gt; that does one thing, slice text into overlapping fixed-size chunks. It's the dumbest viable splitter, and that's deliberate. Smarter ones exist. Sentence-aware splitters, recursive structural splitters, semantic-similarity splitters like LangChain's &lt;code&gt;SemanticChunker&lt;/code&gt;. Each brings its own failure modes and its own tuning surface. A naive windowed splitter has exactly two knobs and no hidden behavior, which is what you want from a baseline before you let cleverness in.&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="cm"&gt;/**
 * Split text into chunks for embedding
 */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;splitTextIntoChunks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;chunkSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;overlap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;chunkSize&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chunk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;end&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;end&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;overlap&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The overlap does more work than it looks like it does. It encodes a locality assumption: an important sentence often straddles a chunk boundary, and when it does, the embedding for either neighbor still anchors near that sentence in vector space. Drop the overlap and coherent thoughts become disjoint halves with weaker mutual similarity. Common starting values in production run 10 to 20%. This one runs at 20% (200 of 1000), the upper end, trading a bit of storage for resilience at the boundaries. The Pinecone team's &lt;a href="https://www.pinecone.io/learn/chunking-strategies/" rel="noopener noreferrer"&gt;chunking strategies guide&lt;/a&gt; is the canonical writeup if you want the longer comparison.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why the naive version fails
&lt;/h3&gt;

&lt;p&gt;Embedding the whole document and calling it done works right up until retrieval needs a narrow answer. Then the vector starts behaving like a resume summary. It remembers the broad shape and forgets the sentence where the detail actually lives. That's the precision side of the precision/recall tradeoff giving way: you keep finding &lt;em&gt;related&lt;/em&gt; documents and missing the &lt;em&gt;exact&lt;/em&gt; one.&lt;/p&gt;

&lt;p&gt;Chunk-level embedding changes the unit of truth. Instead of one embedding representing everything, many embeddings represent adjacent slices, and cosine similarity decides which slice the query sits closest to. More embeddings per document means more candidate matches, which means a much better chance of landing on the specific span that answers the question rather than the document that happens to contain it.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the chunk becomes a vector
&lt;/h2&gt;

&lt;p&gt;Once the text is split, each chunk gets embedded independently with OpenAI. The call is tied to the chunk text, not the parent document.&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="cm"&gt;/**
 * Generate embedding for a given text using OpenAI
 */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;generateEmbedding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;openai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;embeddings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text-embedding-3-large&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;dimensions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Error generating embedding:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things in that call are worth naming. &lt;code&gt;input: text&lt;/code&gt; receives the chunk and not the document, which is the whole purpose of the split. And &lt;code&gt;dimensions: 2000&lt;/code&gt; leans on OpenAI's &lt;strong&gt;Matryoshka representation learning&lt;/strong&gt;: &lt;code&gt;text-embedding-3-large&lt;/code&gt; is natively 3072-dimensional, but it's trained so that any leading prefix of the vector is itself a usable embedding. Truncating to 2000 dims keeps roughly 99% of the retrieval quality at two-thirds of the storage cost and roughly two-thirds of the cosine computation per query. Across tens of thousands of chunks behind an HNSW index, that compounds fast.&lt;/p&gt;

&lt;p&gt;The storage path follows the same logic when chunks get inserted. Each one becomes its own record with its own index and metadata.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chunkPromises&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunkText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;generateEmbedding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunkText&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;document_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;documentId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;chunk_index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;chunk_text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;chunkText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Pass as array, Supabase will handle vector conversion&lt;/span&gt;
    &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;chunk_index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;total_chunks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chunksWithEmbeddings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunkPromises&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Insert chunks with embeddings&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;embError&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;embeddings&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunksWithEmbeddings&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The non-obvious part is the metadata. It carries &lt;code&gt;chunk_index&lt;/code&gt; and &lt;code&gt;total_chunks&lt;/code&gt; alongside the vector, so a chunk is usable on its own &lt;em&gt;and&lt;/em&gt; still locatable inside the original document. That's what enables a downstream pattern called &lt;strong&gt;context expansion&lt;/strong&gt;: when a chunk matches, you can also fetch its neighbors by index and widen the window the LLM sees without polluting the similarity score. Retrieval stays narrow. Presentation gets to be generous.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why this pattern beats one vector per file
&lt;/h3&gt;

&lt;p&gt;A single file vector is cheap to reason about and expensive to trust, because it compresses too much. Split first, embed second, and you preserve the local neighborhood around each idea, which is what lets the search layer return the exact span instead of the general topic. The honest cost is more rows, more embeddings, more storage. But in retrieval, precision is usually the thing that keeps the rest of the system from sounding vaguely confident and slightly wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  The retrieval path that makes the split worth it
&lt;/h2&gt;

&lt;p&gt;The strongest evidence that the split matters is a retrieval helper that bypasses vector similarity entirely when I already know the file path. In &lt;code&gt;supabase/functions/_shared/blog-utils.ts&lt;/code&gt;, it queries embeddings by &lt;code&gt;metadata-&amp;gt;&amp;gt;'file_path' LIKE '%&amp;lt;path&amp;gt;%'&lt;/code&gt; so I can guarantee the accuracy agent sees chunks from the file the draft explicitly mentions.&lt;/p&gt;

&lt;p&gt;That's &lt;strong&gt;hybrid retrieval&lt;/strong&gt; in its simplest form. Lexical exact-match (the &lt;code&gt;LIKE&lt;/code&gt; on file path) and dense vector search (the cosine query) live side by side and compose by union. Vector search is what I want for semantic recall, "find me the chunk that talks about this idea, even if it uses different words." Path-based retrieval is what I reach for when the system needs to stop being poetic and start being literal, "find me chunks from this exact file." Production retrieval stacks at scale usually go further and combine BM25 lexical scores with dense vectors via reciprocal rank fusion. This is the minimal version of that pattern, sized to the problem.&lt;/p&gt;

&lt;p&gt;And the chunk-level split is what makes the path-based path useful, because a file can now return multiple precise spans instead of one oversized blob. If I'm asking about a function, I want the function's neighborhood, not the whole neighborhood's autobiography.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape of the storage model
&lt;/h2&gt;

&lt;p&gt;The storage row carries the &lt;code&gt;document_id&lt;/code&gt;, &lt;code&gt;chunk_index&lt;/code&gt;, &lt;code&gt;chunk_text&lt;/code&gt;, &lt;code&gt;embedding&lt;/code&gt;, and metadata. That shape is what keeps downstream retrieval and debugging sane. When I inspect a result, I can tell where it came from, where it sits in the source, and how many sibling chunks exist around it. Simple enough to survive maintenance, specific enough to survive scrutiny, which is a rarer combination than it should be.&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="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;DocumentChunk&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;document_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;chunk_index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;chunk_text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="nl"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The practical tradeoff: granularity buys control, not magic
&lt;/h2&gt;

&lt;p&gt;Chunking doesn't make retrieval smart by itself. It gives the search layer a better surface to work with, and that surface has a goldilocks zone. Too large and chunks blur together into mid-density topical clouds. Too small and they lose enough context to become syntactic fragments whose embeddings collapse into the average. The 1000-character window here is roughly two paragraphs of prose, or one tight function: large enough to carry meaning, small enough to admit only one dominant idea per row.&lt;/p&gt;

&lt;p&gt;The overlap is the design's safety net. It encodes the assumption that meaning is &lt;em&gt;local but not aligned&lt;/em&gt;, since important sentences refuse to respect window boundaries, and it pays a storage tax to keep adjacent chunks neighbors in vector space. The same tradeoff shows up in sliding-window attention, which preserves locality at the cost of duplicated work, and in n-gram lexical indexes, which overlap at the boundary to avoid losing cross-token matches.&lt;/p&gt;

&lt;p&gt;The other cost is operational. More chunks, more embeddings. More embeddings, more insert work and more bytes in the HNSW graph. I accept that, because the alternative is a retrieval system that keeps returning the right topic and the wrong answer, which is a very expensive way to be unhelpful.&lt;/p&gt;

&lt;h2&gt;
  
  
  The retrieval story, drawn plainly
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
  A["Source text"] --&amp;gt; B["Chunk split"]
  B --&amp;gt; C["OpenAI embed"]
  C --&amp;gt; D["Embeddings table"]
  D --&amp;gt; E["File-path query"]
  D --&amp;gt; F["Vector search"]
  E --&amp;gt; G["Chunk result"]
  F --&amp;gt; G
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why I kept the split at the chunk boundary
&lt;/h2&gt;

&lt;p&gt;I could have pushed more logic into the embedding step. Late-interaction models like ColBERT preserve per-token embeddings and aggregate at query time, which gives you precision without choosing a chunk size up front. I could have tried to recover precision later with cross-encoder reranking. But both of those move the problem instead of solving it. They paper over a representation that's wrong at the unit of storage.&lt;/p&gt;

&lt;p&gt;So I split by chunk, embed by chunk, store by chunk, and retrieve by chunk. Every stage speaks the same language, which makes the system easier to inspect. Better recall is part of the payoff. The bigger part is a retrieval stack whose granularity matches the way I actually ask questions.&lt;/p&gt;

&lt;p&gt;When the answer lives inside a file, I want the search layer to arrive with a scalpel, not a shovel.&lt;/p&gt;




&lt;p&gt;🎧 &lt;strong&gt;Listen to the audiobook&lt;/strong&gt; — &lt;a href="https://open.spotify.com/show/4ABVd5yDVfbX9HlV5JjT7D" rel="noopener noreferrer"&gt;Spotify&lt;/a&gt; · &lt;a href="https://play.google.com/store/audiobooks/details/How_to_Architect_an_Enterprise_AI_System_And_Why_t?id=AQAAAECafz8_tM&amp;amp;hl=en" rel="noopener noreferrer"&gt;Google Play&lt;/a&gt; · &lt;a href="https://www.craftedbydaniel.com/audiobook" rel="noopener noreferrer"&gt;All platforms&lt;/a&gt;&lt;br&gt;
🎬 &lt;a href="https://youtube.com/playlist?list=PLRteDbGJPYDb9XNjecvHplGlgW7tIv_q6" rel="noopener noreferrer"&gt;Watch the visual overviews on YouTube&lt;/a&gt;&lt;br&gt;
📖 &lt;a href="https://www.craftedbydaniel.com/blog/series/how-to-architect-an-enterprise-ai-system-and-why-the-engineer-still-matters" rel="noopener noreferrer"&gt;Read the full 13-part series&lt;/a&gt;&lt;/p&gt;

</description>
      <category>embeddings</category>
      <category>rag</category>
      <category>vectorsearch</category>
      <category>supabase</category>
    </item>
    <item>
      <title>Four Vectors, One Record: How I Split Embeddings Before They Hit Search</title>
      <dc:creator>Daniel Romitelli</dc:creator>
      <pubDate>Mon, 27 Jul 2026 16:02:20 +0000</pubDate>
      <link>https://dev.to/romiteld/four-vectors-one-record-how-i-split-embeddings-before-they-hit-search-211i</link>
      <guid>https://dev.to/romiteld/four-vectors-one-record-how-i-split-embeddings-before-they-hit-search-211i</guid>
      <description>&lt;p&gt;The failure that pushed me into this design wasn't subtle. My blended embedding kept returning candidates who looked fine at a glance and wrong on inspection. Ask about a very specific certification and you'd get people with the right industry background and no credential. Ask about deep experience in a narrow domain and you'd get candidates whose skills summary happened to mention the right keywords, with a work history that didn't support the match. The vector was doing exactly what I'd asked it to do: compressing an entire record into one semantic point. A candidate record just isn't one thing.&lt;/p&gt;

&lt;p&gt;Dense-retrieval people have a name for this. &lt;strong&gt;Representational collapse&lt;/strong&gt;: ask one embedding to boil down a heterogeneous object and the model gravitates to the dominant mode, discarding the long tail. For a document, that tail is a paragraph. For a candidate record, the tail is an entire field. Credentials get smoothed over by industry text. Career trajectory gets smoothed over by a polished summary. One vector turns a structured record into a compromise the search layer then has to live with.&lt;/p&gt;

&lt;p&gt;A profile has several different centers of meaning, several &lt;strong&gt;semantic modes&lt;/strong&gt; that don't collapse cleanly into one. Work history answers one class of search intent. Skills and designations answer another. A broad profile summary is useful for discovery, but it's a poor substitute for the details that actually separate one candidate from the next. So stop asking one embedding to carry all of them.&lt;/p&gt;

&lt;p&gt;That's what &lt;code&gt;app/agents/embedding_agent.py&lt;/code&gt; is for in this system. It generates four parallel embeddings for the same logical record: &lt;code&gt;profile_vector&lt;/code&gt;, &lt;code&gt;experience_vector&lt;/code&gt;, &lt;code&gt;skills_vector&lt;/code&gt;, and &lt;code&gt;general_vector&lt;/code&gt;. In retrieval terms that's &lt;strong&gt;late fusion&lt;/strong&gt;, meaning you embed each view independently, defer the "which view matched" decision until query time, and let the search layer pick the best-matching vector per query. Then &lt;code&gt;app/jobs/embedding_generator.py&lt;/code&gt; takes over the production concerns. It validates the input, applies retry logic, and routes terminal failures to the dead-letter queue. The split is deliberate. The agent handles semantic decomposition and caching; the job handles durability and failure control. And the reason for two layers is the &lt;strong&gt;bulkhead pattern&lt;/strong&gt;, keeping the failure modes of one concern from contaminating the other, the way watertight compartments work on a ship.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape of the pipeline
&lt;/h2&gt;

&lt;p&gt;I like a system that's honest about its steps.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
  sourceRecord[Source record] --&amp;gt; fieldSplit[Split into 4 field views]
  fieldSplit --&amp;gt; embeddingAgent[Embedding agent generates 4 vectors]
  embeddingAgent --&amp;gt; redisCache[Redis cache with 24h TTL]
  embeddingAgent --&amp;gt; embeddingJob[Embedding job]
  embeddingJob --&amp;gt; validate[Validate length and dimension]
  validate --&amp;gt; retryFailures[Retry transient failures]
  validate --&amp;gt; deadLetter[Route terminal failures to DLQ]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The agent, its constants, and the cache key
&lt;/h2&gt;

&lt;p&gt;The embedding agent is specialized on purpose. Its whole assignment: produce four specific vectors, keep them cached, and run fast enough that repeated calls don't punish the API.&lt;/p&gt;

&lt;p&gt;The contract sits in four constants at the top of the file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;EMBEDDING_MODEL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text-embedding-3-large&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;EMBEDDING_DIMENSIONS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3072&lt;/span&gt;  &lt;span class="c1"&gt;# text-embedding-3-large native dimensions
&lt;/span&gt;&lt;span class="n"&gt;EMBEDDING_TTL_SECONDS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;86400&lt;/span&gt;  &lt;span class="c1"&gt;# 24 hours
&lt;/span&gt;&lt;span class="n"&gt;CACHE_KEY_PREFIX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;emb:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those four lines are the boundary conditions for the whole embedding pipeline, what a working systems engineer would call a &lt;strong&gt;contract module&lt;/strong&gt;. The model choice defines the embedding family. The dimension count defines what the rest of the system has to expect, and what the HNSW or IVF index was sized for; if the two ever fall out of step, the mismatch shows up immediately. The TTL is the literal floor on how stale the cache can get, which makes it a direct knob on the freshness/cost tradeoff. And the prefix scopes the embedding namespace inside Redis, because a multi-tenant cache without prefixes becomes archaeology the moment anything goes wrong.&lt;/p&gt;

&lt;p&gt;The cache key itself is where the multi-vector output stays safe.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_generate_cache_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vector_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;all&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Generate a deterministic cache key from query text.

    Returns: emb:{md5_hash}:{vector_type}
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;normalized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;query_hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;md5&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;normalized&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;CACHE_KEY_PREFIX&lt;/span&gt;&lt;span class="si"&gt;}{&lt;/span&gt;&lt;span class="n"&gt;query_hash&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;vector_type&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;vector_type&lt;/code&gt; token is the piece that makes this cache safe for a four-vector bundle. Call it &lt;strong&gt;key-space scoping&lt;/strong&gt; in the small. Each view gets its own deterministic slot, so a lookup for &lt;code&gt;profile_vector&lt;/code&gt; can't come back holding the value cached for &lt;code&gt;skills_vector&lt;/code&gt;. Without the token the keys would collide and a lookup would return whichever vector wrote last. With it, the cache stays correct under concurrent writes from all four views. The &lt;code&gt;lower().strip()&lt;/code&gt; normalization is what gives the key its &lt;strong&gt;deterministic property&lt;/strong&gt;: semantically identical queries that differ only in whitespace or case land in the same slot. Repeated content returns quickly. New content pays the model cost once. And the cache stays legible because nothing else in Redis uses the &lt;code&gt;emb:&lt;/code&gt; prefix.&lt;/p&gt;

&lt;p&gt;On a miss, the agent fires all four model calls in parallel. The vectors are independent semantic views, so there's no reason to serialize them. &lt;code&gt;asyncio.gather&lt;/code&gt; runs the four embedding requests concurrently, and the worst case is &lt;strong&gt;fan-out latency&lt;/strong&gt; rather than four-times-sequential. In practice the four calls finish in roughly the wall-clock time of a single one. The agent also tracks cache hits, cache misses, embeddings generated, errors, tokens used, and total latency. Those metrics are what let me tell a slow cache miss from a model issue from a bad keying strategy once the system gets noisy. Observability has to be richer than the failure mode you're trying to debug, or you end up guessing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The job that makes the system durable
&lt;/h2&gt;

&lt;p&gt;The agent solves the semantic problem. The job in &lt;code&gt;app/jobs/embedding_generator.py&lt;/code&gt; solves the production problem.&lt;/p&gt;

&lt;p&gt;It doesn't invent vectors. It makes sure the vectors entering the index are valid, and that failures get classified correctly when something goes wrong. Three responsibilities: content-length validation, embedding-dimension validation, and retry/DLQ routing.&lt;/p&gt;

&lt;p&gt;The retry policy is &lt;strong&gt;exponential backoff with capped intervals&lt;/strong&gt;, 30 seconds, then 2 minutes, then 10 minutes for transient failures. That growth pattern (~4x between steps) gives a temporary upstream issue room to clear without flooding the pipeline with immediate retries, and the cap keeps the worst-case retry budget bounded. The shape is older than queue infrastructure itself, since TCP's RTO calculation does the same thing, and it works because most transient failures recover on a timescale that doesn't require sub-second polling.&lt;/p&gt;

&lt;p&gt;But not every failure deserves another attempt. Some are &lt;strong&gt;terminal&lt;/strong&gt; by definition. Content too long? Then the input has to change. Dimension count doesn't match the expected shape? Then the embedding isn't fit for indexing. Those cases go straight to the DLQ as &lt;strong&gt;poison messages&lt;/strong&gt;, input that no amount of retrying will fix, because the retry policy operates on time and not on the cause of the failure.&lt;/p&gt;

&lt;p&gt;That's the right line to draw. A retryable failure says "try again later." A terminal one says "this record needs intervention or upstream correction." Confusing the two is how queues turn into &lt;strong&gt;retry-storm amplifiers&lt;/strong&gt;, with the same bad input cycling through the worker pool, burning capacity that healthy traffic needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why terminal failures go straight to the DLQ
&lt;/h2&gt;

&lt;p&gt;I treat the dead-letter queue as an operational control surface, not a waste bin. When a record lands there because its content is too long or its dimensions are wrong, that's worth knowing. It tells me exactly what kind of correction is needed.&lt;/p&gt;

&lt;p&gt;It also keeps the normal path clean. Retryable failures get another chance. Invalid records get isolated. The DLQ becomes the place where malformed input, schema changes upstream, or data quality problems surface, instead of hiding under repeated attempts that read as noise on a dashboard. Which is why the terminal list is explicit: &lt;code&gt;content_too_long&lt;/code&gt; and &lt;code&gt;dimension_mismatch&lt;/code&gt;. Neither one is a transient condition. Retrying them would only burn time and make the failure harder to interpret.&lt;/p&gt;

&lt;p&gt;The job's logic protects downstream search from poison data. Unglamorous sentence. It's also the entire point of the job.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real benefit of four vectors
&lt;/h2&gt;

&lt;p&gt;The payoff here has nothing to do with multi-vector being better in the abstract. What it buys is a candidate record that can answer multiple search intents without being flattened into a single compromise representation. Classical information retrieval solved this exact problem with &lt;strong&gt;BM25F&lt;/strong&gt;, field-weighted BM25, which lets a search over "title, body, anchor text" weight each field separately rather than concatenating them into one bag of words. The four-view embedding is the dense-retrieval analog. Each field gets its own representation, and the search layer composes them at query time instead of leaning on a pre-flattened average.&lt;/p&gt;

&lt;p&gt;The work-history view preserves sequence and career motion. The skills view preserves credentials, designations, and specific capabilities. The profile view keeps the higher-level picture intact. And the general view gives broad coverage when a query isn't narrowly about one field, the safety net for anything that doesn't map cleanly onto a single mode.&lt;/p&gt;

&lt;p&gt;The record keeps its internal structure and still becomes searchable. Had I stayed with one blended embedding, queries about certification would have kept sliding toward domain-adjacent candidates, and queries about career history would have kept overvaluing polished summary language. Splitting the record before embedding makes those tradeoffs explicit instead of accidental. The search layer can now choose which view drives the score, and which one only gets to break ties.&lt;/p&gt;

&lt;h2&gt;
  
  
  How this changes the way I debug search
&lt;/h2&gt;

&lt;p&gt;One of the best side effects of the split is that debugging got a lot cleaner. When the embedding layer is monolithic, every retrieval complaint feels like one problem. With four vectors and a separate job layer, &lt;strong&gt;each failure mode has a coordinate&lt;/strong&gt;, so I can point at which vector, which stage, and which agent owns the problem instead of triangulating.&lt;/p&gt;

&lt;p&gt;Latency high? I look at cache misses and generation time. Burning API calls? Cache hit rate. Records failing to persist? Validation and DLQ counts. Embedding shapes wrong? Then I know the issue lives in the contract, not in the ranking layer. That mapping from symptom to layer is what makes a system &lt;strong&gt;observable&lt;/strong&gt; in the precise sense. It doesn't mean "we have dashboards." It means every reasonable question has a corresponding metric to read.&lt;/p&gt;

&lt;p&gt;That division of responsibility is what makes the whole thing tractable. The embedding agent is about generating the right semantic inputs. The job is about making sure those inputs survive production constraints. Each layer stays narrow enough to inspect without guessing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I kept the implementation narrow
&lt;/h2&gt;

&lt;p&gt;I didn't want the embedding path turning into a catch-all orchestration layer. Plenty of systems get hard to reason about because one component starts doing semantic prep, retry control, persistence, error handling, and cache management all at once. Designers call that &lt;strong&gt;god-object accretion&lt;/strong&gt;, where convenience eats coherence one feature at a time.&lt;/p&gt;

&lt;p&gt;This one stays focused. The agent prepares and caches the vectors. The job validates and routes outcomes. The search layer consumes the resulting embeddings through the normal indexing path. That separation keeps each piece easier to test, easier to replace, and free of accidental coupling. Change the retry behavior and semantic preparation needs no rewrite. Adjust the vector split and dead-letter routing never moves. Same logic as &lt;strong&gt;single-responsibility design&lt;/strong&gt; at the class level, applied at the service boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the split preserved
&lt;/h2&gt;

&lt;p&gt;Accuracy was part of what I wanted to protect. The bigger thing was the shape of the candidate record itself. A blended embedding makes a record searchable while smoothing away the distinctions that separate one candidate from the next: experience isn't the same as skills, and a polished title can hide a work history that tells a different story. Four semantic views keep those distinctions somewhere search can still reach them.&lt;/p&gt;

&lt;p&gt;That's the real win. The record stays one record, but it no longer has to pretend it only means one thing.&lt;/p&gt;

&lt;p&gt;Next is connecting this multi-vector representation to the retrieval side with the same discipline: explicit field intent, explicit score handling, and no hidden magic between the index and the ranking layer. &lt;strong&gt;Late-interaction models&lt;/strong&gt; like ColBERT push the idea further still, preserving per-token vectors and aggregating at query time, which buys precision without committing to a field schema up front. That's a much bigger lift, though, and the four-view split is the right step from where I started. Once the record isn't flattened anymore, the search layer has to earn its keep.&lt;/p&gt;




&lt;p&gt;🎧 &lt;strong&gt;Listen to the audiobook&lt;/strong&gt; — &lt;a href="https://open.spotify.com/show/4ABVd5yDVfbX9HlV5JjT7D" rel="noopener noreferrer"&gt;Spotify&lt;/a&gt; · &lt;a href="https://play.google.com/store/audiobooks/details/How_to_Architect_an_Enterprise_AI_System_And_Why_t?id=AQAAAECafz8_tM&amp;amp;hl=en" rel="noopener noreferrer"&gt;Google Play&lt;/a&gt; · &lt;a href="https://www.craftedbydaniel.com/audiobook" rel="noopener noreferrer"&gt;All platforms&lt;/a&gt;&lt;br&gt;
🎬 &lt;a href="https://youtube.com/playlist?list=PLRteDbGJPYDb9XNjecvHplGlgW7tIv_q6" rel="noopener noreferrer"&gt;Watch the visual overviews on YouTube&lt;/a&gt;&lt;br&gt;
📖 &lt;a href="https://www.craftedbydaniel.com/blog/series/how-to-architect-an-enterprise-ai-system-and-why-the-engineer-still-matters" rel="noopener noreferrer"&gt;Read the full 13-part series&lt;/a&gt;&lt;/p&gt;

</description>
      <category>embeddings</category>
      <category>search</category>
      <category>redis</category>
      <category>python</category>
    </item>
    <item>
      <title>Coverage Before Creativity: The RAG Gate That Keeps My Blog Pipeline Honest</title>
      <dc:creator>Daniel Romitelli</dc:creator>
      <pubDate>Mon, 27 Jul 2026 16:02:09 +0000</pubDate>
      <link>https://dev.to/romiteld/coverage-before-creativity-the-rag-gate-that-keeps-my-blog-pipeline-honest-5gcj</link>
      <guid>https://dev.to/romiteld/coverage-before-creativity-the-rag-gate-that-keeps-my-blog-pipeline-honest-5gcj</guid>
      <description>&lt;p&gt;The first failure I had to get rid of in the blog pipeline wasn't a bad paragraph. It was a bad evidence set. The system would find a few nearby chunks, mistake density for coverage, and then draft as if that narrow slice stood in for the whole repository. Text like that sounds confident right up until you put it next to the code. So I stopped treating topic selection as a writing problem and started treating it as a retrieval coverage problem.&lt;/p&gt;

&lt;p&gt;That distinction does real work. If the upstream evidence is thin, no amount of prompt polish rescues the result, and the draft will still overfit whichever cluster of files happened to match the query first. I wanted the pipeline disciplined about breadth before it got creative about prose. So the gate moved earlier: query fan-out, file-path-aware dedupe, breadth validation, pinned excerpts, and only then the writing pass.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gate lives before the writing step
&lt;/h2&gt;

&lt;p&gt;In my pipeline the important work happens before generation starts. The dispatcher is where the topic search fans out through multiple lanes: curated highlights, a fixed RAG query pool, and recent commit-derived queries. That's deliberate. A single semantic search tends to collapse into the same dense corners of the codebase, which is exactly where a system gets persuasive and shallow at once.&lt;/p&gt;

&lt;p&gt;The dispatcher doesn't need to know how the post will read yet. Its job is to prove that the candidate topic has enough distinct evidence behind it to deserve a draft. Which means the retrieval layer has to do more than collect relevant chunks. It has to show spread, and it has to show that the match didn't come from one file, one subsystem, or one repetitive cluster of adjacent chunks.&lt;/p&gt;

&lt;p&gt;That's why the shared blog utilities matter. The generator path imports &lt;code&gt;checkRagSufficiency&lt;/code&gt; and &lt;code&gt;fetchFailureEvidence&lt;/code&gt; from &lt;code&gt;supabase/functions/_shared/blog-utils.ts&lt;/code&gt;, and that's the right place for it: the part of the system that decides whether retrieval is good enough should sit close to the code that evaluates it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
  A[Dispatcher: curated highlights] --&amp;gt; B[Query fan-out]
  C[Dispatcher: fixed RAG query pool] --&amp;gt; B
  D[Dispatcher: recent commit queries] --&amp;gt; B
  B --&amp;gt; E[RAG retrieval]
  E --&amp;gt; F[Dedupe by repo and file path]
  F --&amp;gt; G[Breadth check]
  G --&amp;gt;|passes| H[Pin excerpts]
  G --&amp;gt;|fails| I[Reject candidate]
  H --&amp;gt; J[Stage-compose merge]
  J --&amp;gt; K[Generator / drafting]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That flow is the real control surface. By the time the generator runs, a decision has already happened: is the evidence wide enough to trust?&lt;/p&gt;

&lt;h2&gt;
  
  
  Three query lanes, three different jobs
&lt;/h2&gt;

&lt;p&gt;The fan-out isn't random, and it isn't a single blended prompt pretending to be a strategy. I built it as three separate lanes, because each one catches a different failure mode.&lt;/p&gt;

&lt;p&gt;Curated highlight queries keep the system anchored in the kinds of features and systems I already know are worth revisiting. Those posts usually come out of places I've touched repeatedly: workflow orchestration, retrieval, caching, parsing, state management, security boundaries, or data transformation. They help the pipeline remember what's already interesting in the repository family.&lt;/p&gt;

&lt;p&gt;The fixed query pool is the broadest lane. It's there to force coverage across architectural themes and implementation patterns instead of letting one topic family take over. This is the lane that goes looking for general system shape: event-driven flows, retrieval logic, prompt construction, orchestration, caching, retry paths, auth boundaries, model inference, ETL, and state machines. A selector living only inside the curated highlights would turn too self-referential, and one living only inside the fixed pool would turn too generic. Running both is what keeps the output grounded and varied.&lt;/p&gt;

&lt;p&gt;Recent commit queries add the temporal dimension. They bias the selector toward what actually changed recently rather than letting the system settle into evergreen topics that no longer reflect the repository's current shape. That matters because the most obvious topic is often the wrong one once recent work has moved the architecture. A topic can be semantically relevant and still be stale in practice.&lt;/p&gt;

&lt;p&gt;The reason for splitting the lanes is simple. No single lane is trusted to decide the topic alone. They feed the same retrieval pass, but for different reasons: one preserves editorial continuity, one broadens the architectural search, one keeps the system current. Merged, the three give me a candidate set that's much harder to fool with local similarity alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the dedupe key is repo plus file path
&lt;/h2&gt;

&lt;p&gt;Once retrieval returns a pile of chunks, the next problem is repetition. Similarity search loves repetition. A single file can dominate a result set by surfacing multiple overlapping excerpts, especially when the file is dense or when several queries land in the same section of code. Let that through and the draft starts building itself around one artifact instead of one system.&lt;/p&gt;

&lt;p&gt;Which is why file-path-aware dedupe carries so much weight here. I want repeated hits from the same repo and file path to collapse early. Five adjacent chunks all sounding relevant doesn't impress me if they're pointing at the same paragraph of the same file. What I care about is whether the sample spans distinct parts of the codebase.&lt;/p&gt;

&lt;p&gt;Repo identity belongs in the key too. In a multi-repo setup, two chunks can look similar for completely unrelated reasons. Both might describe retrieval logic, or orchestration, or prompt shaping, while living in different systems that shouldn't count as interchangeable evidence. Repo plus file path tells me whether the sampling is broad or just rediscovering the same neighborhood under different search terms.&lt;/p&gt;

&lt;p&gt;The practical effect is a retrieval layer that's less greedy. The first obvious cluster stops earning extra representation, and repeated evidence stops counting as coverage. Smaller candidate set. Considerably more trustworthy one.&lt;/p&gt;

&lt;p&gt;There's a second-order benefit. Dedupe reduces the risk that a single implementation detail becomes the skeleton of the whole post. Without it, a draft can end up over-explaining one helper, one file, one branch of logic, purely because retrieval happened to hit it several times. That bias gets broken before the generator ever sees the prompt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measuring breadth instead of eyeballing it
&lt;/h2&gt;

&lt;p&gt;After dedupe I don't ask whether the chunks feel diverse. I measure whether the sample is wide enough to support a post. That's the entire point of the breadth gate: reject candidate sets that are semantically plausible but structurally weak.&lt;/p&gt;

&lt;p&gt;This is where &lt;code&gt;checkRagSufficiency&lt;/code&gt; fits into the pipeline. The name is exactly what the behavior needs to be, a sufficiency check. If the retrieved set can't prove enough spread across the repository, it should not advance. The system should fail closed rather than guess.&lt;/p&gt;

&lt;p&gt;What I like about a threshold is that it changes what retrieval means. Retrieval stops being a convenience layer that gathers whatever sits closest and becomes a gate that has to establish evidence quality before writing begins. The failure mode shifts from "draft written from a narrow slice" to "candidate rejected because the sample is too narrow." I'll take the second one every time.&lt;/p&gt;

&lt;p&gt;That rejection path earns its keep. It catches the query pool landing too hard in one subsystem, recent changes dominating the semantic neighborhood, one file throwing off too many overlapping hits. Plenty of retrieval bugs look like success right until the prompt is assembled. The breadth check is the thing that stops them from becoming published text.&lt;/p&gt;

&lt;p&gt;I like that the failures can produce something concrete, too. &lt;code&gt;fetchFailureEvidence&lt;/code&gt; belongs in the same shared utility layer because it gives me a way to inspect why a candidate was rejected, which is useful during tuning. If a topic keeps failing breadth, I can see whether the cause is query bias, inadequate file-path diversity, or a retrieval window too small for the amount of material I want to cover.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stage-compose step adds a second guardrail
&lt;/h2&gt;

&lt;p&gt;File-path retrieval and semantic retrieval aren't competing systems. File-path retrieval gives me boundary-aware evidence; semantic retrieval gives me breadth across related concepts. In &lt;code&gt;blog-stage-compose&lt;/code&gt; I merge both and dedupe a second time, since the two strategies can land on the same excerpt from different angles, and the evidence set should not regress back into repetition right before drafting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pinned excerpts hold the evidence in place
&lt;/h2&gt;

&lt;p&gt;Once a candidate survives the coverage gate, I pin the excerpts that explain why the topic is worth writing about. Not a cosmetic step. It buys stability, because without pinned evidence the drafting stage has too much freedom to wander away from the exact chunks that earned the topic in the first place.&lt;/p&gt;

&lt;p&gt;Pinned excerpts act like an anchor for the generation pass. They preserve the evidence trail, and they hold the prompt to the source material the draft was built from. That matters because the strongest failure mode in a retrieval-driven blog system isn't outright hallucination. It's the gradual slide: the draft starts from real evidence and then generalizes past what was actually retrieved.&lt;/p&gt;

&lt;p&gt;Pinning the survivors makes that harder. The generation stage has to stay connected to the specific implementation details that passed the gate. Review gets easier too, since I can inspect exactly which chunks were considered important enough to carry forward.&lt;/p&gt;

&lt;p&gt;Pinning and breadth checking together are what give the pipeline its shape. Breadth says the sample is wide enough. Pinning says these are the exact pieces that justify the topic. Between them, they stop the generator from inventing confidence the retrieval layer didn't earn.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I prefer rejection over a weak draft
&lt;/h2&gt;

&lt;p&gt;I'm completely comfortable with a pipeline that says no. I want it to say no when the evidence is bad. A weak candidate shouldn't get rescued by a polished prompt, and if the retrieval set is narrow, rejection is the honest response.&lt;/p&gt;

&lt;p&gt;That discipline keeps the blog output specific. It ties the writing to actual systems instead of generic patterns, and it saves me from editing around a draft that was born from a bad evidence shape. A rejected topic costs less time than a published post that looks right but misses the structure of the thing it claims to describe.&lt;/p&gt;

&lt;p&gt;This matters more in a multi-repo environment. Once you have a handful of systems with overlapping concepts, retrieval can get too eager to collapse them into one theme. A good gate has to resist that collapse: respect repository boundaries, file boundaries, evidence density boundaries. If those boundaries aren't visible in the sample, the draft should not happen yet.&lt;/p&gt;

&lt;p&gt;That's the philosophical change I stopped treating as optional. The system doesn't owe me a draft. It owes me a trustworthy sample, and that sample has to prove the topic is broad enough, current enough, and distinct enough to justify writing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual win isn't creativity; it's control
&lt;/h2&gt;

&lt;p&gt;What changed here wasn't my ability to generate prose. What changed was the quality of the evidence stack the prose starts from. The dispatcher fans out through curated highlights, a fixed query pool, and recent commit queries. The retrieval layer dedupes by stable keys, the sufficiency gate checks for breadth, and stage-compose merges file-path and semantic evidence before deduping a second time. Pinned excerpts hold the final prompt to what survived.&lt;/p&gt;

&lt;p&gt;That sequence turns retrieval into a control system rather than a suggestion engine. It enforces a minimum standard before a draft is allowed to exist, and that's the kind of discipline a blog pipeline needs if it's going to write about real systems with real precision.&lt;/p&gt;

&lt;p&gt;The result is fewer bad drafts, but also a pipeline that knows what it doesn't know early enough to stop itself. When the evidence is wide, the writing stage does the part it's good at. When the evidence is narrow, the system does the part I'm more grateful for. It refuses to pretend it knows.&lt;/p&gt;




&lt;p&gt;🎧 &lt;strong&gt;Listen to the audiobook&lt;/strong&gt; — &lt;a href="https://open.spotify.com/show/4ABVd5yDVfbX9HlV5JjT7D" rel="noopener noreferrer"&gt;Spotify&lt;/a&gt; · &lt;a href="https://play.google.com/store/audiobooks/details/How_to_Architect_an_Enterprise_AI_System_And_Why_t?id=AQAAAECafz8_tM&amp;amp;hl=en" rel="noopener noreferrer"&gt;Google Play&lt;/a&gt; · &lt;a href="https://www.craftedbydaniel.com/audiobook" rel="noopener noreferrer"&gt;All platforms&lt;/a&gt;&lt;br&gt;
🎬 &lt;a href="https://youtube.com/playlist?list=PLRteDbGJPYDb9XNjecvHplGlgW7tIv_q6" rel="noopener noreferrer"&gt;Watch the visual overviews on YouTube&lt;/a&gt;&lt;br&gt;
📖 &lt;a href="https://www.craftedbydaniel.com/blog/series/how-to-architect-an-enterprise-ai-system-and-why-the-engineer-still-matters" rel="noopener noreferrer"&gt;Read the full 13-part series&lt;/a&gt;&lt;/p&gt;

</description>
      <category>rag</category>
      <category>supabase</category>
      <category>nextjs</category>
      <category>typescript</category>
    </item>
  </channel>
</rss>
