<?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: Veaceslav</title>
    <description>The latest articles on DEV Community by Veaceslav (@veaceslav).</description>
    <link>https://dev.to/veaceslav</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%2F626211%2Fa82c2dbb-0d46-4384-8dc7-e440b8114ef7.jpg</url>
      <title>DEV Community: Veaceslav</title>
      <link>https://dev.to/veaceslav</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/veaceslav"/>
    <language>en</language>
    <item>
      <title>Stop calling functions: my first LangGraph graph, where state does the calling</title>
      <dc:creator>Veaceslav</dc:creator>
      <pubDate>Tue, 27 Oct 2026 08:25:10 +0000</pubDate>
      <link>https://dev.to/veaceslav/stop-calling-functions-my-first-langgraph-graph-where-state-does-the-calling-3l0a</link>
      <guid>https://dev.to/veaceslav/stop-calling-functions-my-first-langgraph-graph-where-state-does-the-calling-3l0a</guid>
      <description>&lt;p&gt;Three parts in, JobCopilot had layers that worked but couldn't talk to each other. Part 1 gave me a structured-output layer — &lt;code&gt;ParseOk&lt;/code&gt;/&lt;code&gt;ParseError&lt;/code&gt; as data. Part 2 gave me retrieval — &lt;code&gt;build_context()&lt;/code&gt; pulling the top-8 resume chunks for a job description. The pipeline was one line on paper; in code it was nested calls, and the awkward questions had no home: who retries when the model drifts into prose? Who &lt;em&gt;stops&lt;/em&gt; the retries? Where does the score live between "parsed" and "shown to the user"?&lt;/p&gt;

&lt;p&gt;I did what a React dev does in a new language: I nested functions. The retry loop closed over a closure that closed over a callback, and I couldn't tell you who owned what. Rewriting the flow as a graph — my first with &lt;a href="https://python.langchain.com/docs/introduction/" rel="noopener noreferrer"&gt;LangGraph&lt;/a&gt; — made the nesting disappear. Not because a framework absorbed it, but because the mental model changed: &lt;strong&gt;functions stop calling each other; state flows through a graph, and nodes return patches of that state.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It survived everything I threw at it — after I fixed one wire that made &lt;em&gt;every&lt;/em&gt; job come back &lt;code&gt;"error"&lt;/code&gt;, no matter how perfectly the model answered.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The state is a pydantic model; nodes return patches
&lt;/h2&gt;

&lt;p&gt;If the graph is the wiring, state is everything a run knows. I made it one pydantic model — deliberately the &lt;em&gt;same&lt;/em&gt; discipline as part 1:&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;# state.py — the whole run in one model
&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;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;Literal&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pydantic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;

&lt;span class="n"&gt;THRESHOLD&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;   &lt;span class="c1"&gt;# score at or above this = "worth applying"
&lt;/span&gt;&lt;span class="n"&gt;MAX_CALLS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;    &lt;span class="c1"&gt;# 1 first try + 2 repairs — the budget from part 1
&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AgentState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;One run of the copilot, end to end.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="n"&gt;jd_text&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="n"&gt;context&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="c1"&gt;# resume chunks, from part 2
&lt;/span&gt;    &lt;span class="n"&gt;last_raw&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="c1"&gt;# what the model last said, verbatim
&lt;/span&gt;    &lt;span class="n"&gt;error&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="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;       &lt;span class="c1"&gt;# latest parse failure, if any
&lt;/span&gt;    &lt;span class="n"&gt;attempt&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;0&lt;/span&gt;               &lt;span class="c1"&gt;# how many LLM calls so far
&lt;/span&gt;
    &lt;span class="n"&gt;match_score&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="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;missing_skills&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="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="nc"&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;verdict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Literal&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;match&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;no_match&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;error&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="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rule that makes a graph a graph: &lt;strong&gt;a node never calls another node.&lt;/strong&gt; It receives state, does one job, and returns a &lt;code&gt;dict&lt;/code&gt; with &lt;em&gt;only what changed&lt;/em&gt; — a patch. LangGraph merges the patch and hands state to the next node. &lt;code&gt;retrieve&lt;/code&gt; fills &lt;code&gt;context&lt;/code&gt;; &lt;code&gt;judge&lt;/code&gt; fills the score fields; nobody knows about anybody else.&lt;/p&gt;

&lt;p&gt;Fields without a custom reducer are &lt;strong&gt;last-writer-wins&lt;/strong&gt; — I proved it to myself before trusting it:&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;note_a&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AgentState&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;dict&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;missing_skills&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;Python&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;note_b&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AgentState&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;dict&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;missing_skills&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;Terraform&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;

&lt;span class="c1"&gt;# a -&amp;gt; b in that order: missing_skills == ["Terraform"] — b overwrote a
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For my state that's correct by design: every field is overwrite-on-purpose, so zero reducers. If a list should &lt;em&gt;accumulate&lt;/em&gt; across nodes, opt in with &lt;code&gt;Annotated&lt;/code&gt;:&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;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Annotated&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;merge_lists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;left&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="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="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;right&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="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="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;left&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&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;right&lt;/span&gt; &lt;span class="ow"&gt;or&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;right&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&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;left&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;left&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="n"&gt;x&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;right&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;x&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;left&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AccumState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;missing_skills&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Annotated&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="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;merge_lists&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&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="c1"&gt;# two nodes appending "Python" then "Terraform" -&amp;gt; ["Python", "Terraform"]
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And why pydantic for state instead of the &lt;code&gt;TypedDict&lt;/code&gt; every tutorial starts with? A patch is data from somewhere else, and at merge time nobody type-checks a plain dict:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# TypedDict state, merged by dict spread — type-checkers scream, Python shrugs:
&amp;gt;&amp;gt;&amp;gt; {**{"attempt": 0, "verdict": None}, "verdict": 21, "attempt": "two"}
{"attempt": "two", "verdict": 21}          # garbage, happily stored

# pydantic model:
&amp;gt;&amp;gt;&amp;gt; PS(attempt="two", verdict=21)
pydantic.ValidationError: attempt — Input should be a valid integer...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same lesson as part 1: &lt;strong&gt;shape is a contract, and contracts live in pydantic, not in comments.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The graph is a handful of declared edges
&lt;/h2&gt;

&lt;p&gt;Here is the entire &lt;code&gt;graph.py&lt;/code&gt; — wiring, not logic:&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;# graph.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langgraph.graph&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;END&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;START&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;StateGraph&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;nodes&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Chat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fail_node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;judge_node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;retrieve_node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;route_after_parse&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AgentState&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;build_graph&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Chat&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;g&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;StateGraph&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AgentState&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;retrieve&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;retrieve_node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;judge&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;judge_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;  &lt;span class="c1"&gt;# closure: node gets the LLM client
&lt;/span&gt;    &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fail&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fail_node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_edge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;START&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;retrieve&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_edge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;retrieve&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;judge&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_conditional_edges&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;judge&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;route_after_parse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;# returns "ok" | "retry" | "give_up"
&lt;/span&gt;        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;END&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;retry&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;judge&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;give_up&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;fail&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;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_edge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fail&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;END&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;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Coming from React, this shape clicked instantly: it's a component tree. Components don't import each other to call each other — data flows down a declared structure, and the structure decides what happens next.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;lambda&lt;/code&gt; is deliberate: the graph is built once with a concrete LLM client, and no node ever imports a provider. That's what makes the graph testable — hand it a fake &lt;code&gt;chat&lt;/code&gt; and the whole flow runs with zero network. Which is exactly how I caught the bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. All routing logic in one boring function — and the wire that said "error" to everything
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;add_conditional_edges&lt;/code&gt; takes a &lt;em&gt;router&lt;/em&gt;: a pure function from state to a string, plus a mapping from strings to destinations. Mine is twelve lines and holds the entire loop policy of the agent:&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;# nodes.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;MAX_CALLS&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;route_after_parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AgentState&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;All loop logic in one boring function.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;MAX_CALLS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;retry&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;give_up&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compare with my first wiring attempt, written &lt;em&gt;because&lt;/em&gt; the router concept felt foreign:&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;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_edge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;retrieve&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;judge&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_edge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;judge&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;fail&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="c1"&gt;# "temporarily — I'll wire the real branches next"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I ran it — with a fake &lt;code&gt;chat&lt;/code&gt; returning a &lt;em&gt;perfect&lt;/em&gt; JSON verdict. Result: &lt;code&gt;verdict = "error"&lt;/code&gt;. Every single job. The parse was flawless, the score valid, and the graph marched it into &lt;code&gt;fail_node&lt;/code&gt; anyway: I had declared exactly one outgoing edge from &lt;code&gt;judge&lt;/code&gt;, and a graph never "falls through". Nothing raised, nothing warned — the state machine politely followed the only road I built:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; # plain edge judge -&amp;gt; fail, no conditional edges, perfect JSON from the model
&amp;gt;&amp;gt;&amp;gt; app.invoke(AgentState(jd_text="junior"))
verdict = "error"     # a valid parse, executed through the wrong edge
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix was the conditional edges above, plus two habits that came with it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Route values are data, not flow control.&lt;/strong&gt; If the router returns a key that's not in the mapping, LangGraph doesn't guess — it throws &lt;code&gt;KeyError&lt;/code&gt; at runtime. I know because my typo did exactly that. An unmapped route crashes loudly; a wrongly-wired edge fails silently. I'll take the crash.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Loops are declared, not nested.&lt;/strong&gt; &lt;code&gt;"retry": "judge"&lt;/code&gt; makes a cycle in the graph. The loop exists as an edge, its budget lives in &lt;code&gt;MAX_CALLS&lt;/code&gt;, and its stop condition is one pure function I can unit-test without LLM, network, or shame.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The shift, internalized: in nested functions, control flow was &lt;em&gt;who calls whom&lt;/em&gt; — invisible at a glance. In the graph, it's &lt;em&gt;data in the edges&lt;/em&gt; — visible in one screenful.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Closing the repair loop with state, not recursion
&lt;/h2&gt;

&lt;p&gt;Last piece: &lt;code&gt;judge_node&lt;/code&gt;, the only node that talks to the model. It reads state to decide &lt;em&gt;which&lt;/em&gt; prompt to send — first pass or repair — and returns a patch either way:&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;# nodes.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;schemas&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ParseError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ParseOk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parse_match&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AgentState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;THRESHOLD&lt;/span&gt;

&lt;span class="n"&gt;MATCH_PROMPT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;You are a skeptical technical recruiter.
Compare the job description to the candidate&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s resume excerpt.
Reply with ONLY a JSON object:
{{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;match_score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &amp;lt;int 0-100&amp;gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;missing_skills&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: [&amp;lt;string, ...&amp;gt;]}}

JOB DESCRIPTION:
{jd}

RESUME EXCERPT:
{context}
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="n"&gt;FIX_PROMPT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Your previous reply could not be parsed.

PREVIOUS REPLY:
{previous}

PARSER ERROR:
{error}

Reply with ONLY a valid JSON object:
{{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;match_score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &amp;lt;int 0-100&amp;gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;missing_skills&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: [&amp;lt;string, ...&amp;gt;]}}
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;judge_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AgentState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Chat&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;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;One LLM call, one parse, a patch of state. Loops are the graph&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s job now.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MATCH_PROMPT&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;jd_text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FIX_PROMPT&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;previous&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last_raw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;chat&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parse_match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;# the part-1 contract, unchanged
&lt;/span&gt;
    &lt;span class="n"&gt;patch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;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;last_raw&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;attempt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attempt&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="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ParseOk&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;match_score&lt;/span&gt;
        &lt;span class="n"&gt;patch&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="n"&gt;match_score&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;missing_skills&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;missing_skills&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;verdict&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;match&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;THRESHOLD&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;no_match&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;patch&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="n"&gt;error&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;result&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="n"&gt;patch&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fail_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AgentState&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;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;The model never produced valid JSON. Say so — as state, not a stack trace.&lt;/span&gt;&lt;span class="sh"&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;verdict&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;error&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;Because &lt;code&gt;build_graph&lt;/code&gt; takes &lt;code&gt;chat&lt;/code&gt; as a parameter, the end-to-end test is a fake answer card — call one drifts into prose, call two complies:&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;flaky_llm&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;calls&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;n&lt;/span&gt;&lt;span class="sh"&gt;"&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;chat&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="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="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;calls&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;n&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="mi"&gt;1&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;calls&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;n&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Sure! The match is quite good, I&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;d say around 70 out of 100.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="k"&gt;return&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;match_score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: 70, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;missing_skills&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;Terraform&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;chat&lt;/span&gt;


&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;build_graph&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;flaky_llm&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="n"&gt;final&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;AgentState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jd_text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Senior Frontend Engineer (React, TS)&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;verdict: match | score: 70 | attempts: 2 | error: None
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The prose landed in &lt;code&gt;last_raw&lt;/code&gt;, &lt;code&gt;parse_match&lt;/code&gt; filled &lt;code&gt;error&lt;/code&gt;, the router said &lt;code&gt;"retry"&lt;/code&gt;, and the second call got &lt;code&gt;FIX_PROMPT&lt;/code&gt; with the model's own prose quoted back at it — the part-1 trick, now &lt;em&gt;driven by edges instead of a while-loop&lt;/em&gt;. When the model never complies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;verdict: error | error: no JSON object found in model output | attempts: 3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No exception slicing through three stack frames. &lt;code&gt;fail_node&lt;/code&gt; writes &lt;code&gt;verdict: "error"&lt;/code&gt; and the graph ends — for an agent run overnight over forty postings, a run that &lt;em&gt;ends with a recorded failure&lt;/em&gt; beats ten runs that crashed and took the batch with them. Part 1's "errors as data", promoted from one function to the shape of the whole system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;The first graph, in one sentence: &lt;strong&gt;state is a pydantic contract, nodes return patches, a router returns strings, loops live in edges.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Three things this React-dev-learning-Python took from it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Patches, not calls.&lt;/strong&gt; The merge is last-writer-wins by default; accumulators are opt-in via &lt;code&gt;Annotated&lt;/code&gt;. Every field in my state is overwrite-on-purpose — zero reducers, but verified behavior before trusting it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Routing is a pure function.&lt;/strong&gt; Twelve lines, no LLM, unit-testable — and an unmapped return value crashes with a &lt;code&gt;KeyError&lt;/code&gt;. That's the framework refusing to guess, and I've come to love it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Giving up is a node, not a throw.&lt;/strong&gt; &lt;code&gt;fail&lt;/code&gt; writes &lt;code&gt;verdict: "error"&lt;/code&gt; and the graph ends cleanly. The batch survives; the log tells the truth.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the bug that taught me most: a graph executes exactly the edges you declared — my single &lt;code&gt;judge → fail&lt;/code&gt; wire turned perfect parses into errors with zero pushback from the framework. A state machine has no common sense. That's the feature.&lt;/p&gt;

&lt;p&gt;Next Tuesday, part 4: evals. The judge says 72 — but is 72 &lt;em&gt;right&lt;/em&gt;? Fixtures of job descriptions with expected score ranges, a negative case that must score LOW (a Python-backend posting should &lt;em&gt;not&lt;/em&gt; match a frontend resume), and the first time JobCopilot tells me I'm wrong.&lt;/p&gt;

&lt;p&gt;Follow if you want to watch the rest of the copilot get built — including the parts that break. Build log + daily code: &lt;a href="https://t.me/novamind_hub" rel="noopener noreferrer"&gt;t.me/novamind_hub&lt;/a&gt;. Part 1, in case you missed it: &lt;a href="https://dev.to/veaceslav/stop-prompting-for-valid-json-an-llm-output-layer-that-does-not-break-5264"&gt;Stop prompting for valid JSON: an LLM output layer that does not break&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Do you build agent graphs with a framework or hand-roll the loop? And what's the first thing your graph did that you didn't expect?&lt;/p&gt;

</description>
      <category>python</category>
      <category>ai</category>
      <category>langgraph</category>
      <category>pydantic</category>
    </item>
    <item>
      <title>Redux Refactoring Playbook: From Action-Types Soup to RTK Query — Without Rewriting the App</title>
      <dc:creator>Veaceslav</dc:creator>
      <pubDate>Sat, 24 Oct 2026 07:25:43 +0000</pubDate>
      <link>https://dev.to/veaceslav/redux-refactoring-playbook-from-action-types-soup-to-rtk-query-without-rewriting-the-app-5374</link>
      <guid>https://dev.to/veaceslav/redux-refactoring-playbook-from-action-types-soup-to-rtk-query-without-rewriting-the-app-5374</guid>
      <description>&lt;p&gt;Every React codebase of a certain age has one: the legacy Redux store. Hundreds of thunk actions, manually managed loading flags, per-feature cache invalidation, and a polling timer per data type pulling the full payload whether anything changed or not.&lt;/p&gt;

&lt;p&gt;Ours lived under a real-time monitoring dashboard tracking 1,000+ GSM installations at Orange Romania — and by the time we migrated to RTK Query, the story ended with dashboard responsiveness up ~40%, telemetry traffic down 20%, and "frozen dashboard" complaints down 30%.&lt;/p&gt;

&lt;p&gt;The previous article told that performance story. This one is the refactoring &lt;em&gt;mechanics&lt;/em&gt;: in what order things moved, which decisions unlocked the next one, and the one rule that made a no-rewrite migration possible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule 1 — Strangle, don't rewrite
&lt;/h2&gt;

&lt;p&gt;The store wasn't broken in one place; it was broken &lt;em&gt;everywhere&lt;/em&gt;: a switch-case reducer file nobody fully read, five middleware, &lt;code&gt;isLoading&lt;/code&gt; / &lt;code&gt;isRefreshing&lt;/code&gt; / &lt;code&gt;hasError&lt;/code&gt; flag families per resource, and component-level &lt;code&gt;useEffect&lt;/code&gt; chains firing fetches in a specific order "because it worked."&lt;/p&gt;

&lt;p&gt;A big-bang rewrite was a non-starter: the dashboard was used every day by operations teams. So the rule was:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One page migrates per PR. The rest of the app keeps running on the legacy store untouched.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Redux makes this possible because &lt;code&gt;combineReducers&lt;/code&gt; is natural strangler scaffolding:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rootReducer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;combineReducers&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="c1"&gt;// new pages live here&lt;/span&gt;
  &lt;span class="na"&gt;ordersApi&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ordersApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reducer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;// RTK Query&lt;/span&gt;
  &lt;span class="na"&gt;invoices&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;invoicesSlice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reducer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;// RTK slice&lt;/span&gt;
  &lt;span class="c1"&gt;// legacy pages keep their ducks until their turn&lt;/span&gt;
  &lt;span class="na"&gt;legacyTelemetry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;legacyTelemetryReducer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// switch-case reducer, retired last&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each migration PR carried its own before/after render counts and INP p75 from a fixed scripted interaction. If the numbers moved the wrong way, the PR didn't merge — refactoring without a measurement loop is just rewriting with extra steps.&lt;/p&gt;

&lt;p&gt;The code was the easy half. The pitch mattered as much: I sold this as risk control, not cleanup. Every PR shippable alone meant no feature-freeze window that anyone had to buy; the cost I did have to name up front was the parallel-store tax: two stores side by side meant duplicated fixtures and slower reviews while a slice waited for its turn. Leadership signed off because every migration PR ended with a measurement, not with the word "done". One more org-side lesson: the change-flag contract needed a backend buy-in, and I opened that conversation politely, late. Open it on day one; that delay cost us a month.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule 2 — Selectors are the API. Freeze them first.
&lt;/h2&gt;

&lt;p&gt;The trick that removed 90% of the migration risk: &lt;strong&gt;components never touched state shapes directly.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The legacy store had selector functions (&lt;code&gt;selectOrders&lt;/code&gt;, &lt;code&gt;selectNodeById&lt;/code&gt;, &lt;code&gt;selectIsNodeActive&lt;/code&gt;). Before migrating anything, we froze them as the contract and then made each selector a re-export shim that could source data from either world:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// selectors/orders.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;legacyOrders&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;./legacySelectors&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useGetOrdersQuery&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;../api/ordersApi&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&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;useOrders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;selector&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="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;isFetching&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useGetOrdersQuery&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="c1"&gt;// legacy pages keep working against the same contract&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;useMemo&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nf"&gt;legacyOrders&lt;/span&gt;&lt;span class="p"&gt;()),&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;selector&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;(Simplified — the real shims carried fallbacks during the window when a page's data half-lived in the old store. The point is the shape: &lt;strong&gt;the component-facing API never moved, only its plumbing did.&lt;/strong&gt;)&lt;/p&gt;

&lt;p&gt;Result: a "migration PR" changed the data layer under a page, not forty components over it. That made page-by-page migration realistic rather than a nice idea.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule 3 — Kill loading flags with RTK Query, not with better thunks
&lt;/h2&gt;

&lt;p&gt;There was a middle step available: keep the thunks and clean them up. I'd have kept every race. Request A finishing after request B silently overwrote fresher data, and that was our most common production bug, because each flag family carried its own private version of it.&lt;/p&gt;

&lt;p&gt;So server state moved into RTK Query, and components declare what they need, not how to fetch 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="c1"&gt;// before: every call site, every page&lt;/span&gt;
&lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;fetchOrders&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="c1"&gt;// + component-local: isLoading, ordersError, lastRefreshed...&lt;/span&gt;

&lt;span class="c1"&gt;// after: the component declares the requirement&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;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isLoading&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useGetOrdersQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;pollingInterval&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="c1"&gt;// see Rule 4 — we don't poll blindly&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Seventeen loading-flag families disappeared in this step. After it, a component tracking its own fetch lifecycle existed in exactly one place: a page that legitimately needed manual control.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule 4 — Polling moves from timers to tags
&lt;/h2&gt;

&lt;p&gt;Old polling: one &lt;code&gt;setInterval&lt;/code&gt; per data type, full payload every tick. On a dashboard watching 1,000+ nodes, that's thousands of identical requests an hour and constant re-render churn over data that hadn't changed.&lt;/p&gt;

&lt;p&gt;New polling is no polling — it's a &lt;strong&gt;change-flag endpoint&lt;/strong&gt;: a ten-byte version string the dashbo&lt;br&gt;
ard polls on a timer, and the real data refetches when that string actually changes.&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createApi&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;reducerPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;api&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;baseQuery&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;fetchBaseQuery&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;tagTypes&lt;/span&gt;&lt;span class="p"&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;Nodes&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;endpoints&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;build&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;nodesChanged&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;build&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&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;version&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;void&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;query&lt;/span&gt;&lt;span class="p"&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;nodes/changed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;// returns a version string only&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="na"&gt;getNodes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;build&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;NodeState&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="k"&gt;void&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;query&lt;/span&gt;&lt;span class="p"&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;nodes&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;providesTags&lt;/span&gt;&lt;span class="p"&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;Nodes&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="c1"&gt;// refetch tagged data only when the flag version changes:&lt;/span&gt;
    &lt;span class="c1"&gt;// a listener middleware polls `nodesChanged` (cheap, ~10 lines) and,&lt;/span&gt;
    &lt;span class="c1"&gt;// on version change, dispatches api.util.invalidateTags(['Nodes'])&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;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;flagListener&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createListenerMiddleware&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nx"&gt;flagListener&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startListening&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;predicate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;currentState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;previousState&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="c1"&gt;// the version lives in RTK Query's own cache, so the store&lt;/span&gt;
    &lt;span class="c1"&gt;// snapshot gives us both "current" and "what it used to be"&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cur&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;nodesChanged&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;()(&lt;/span&gt;&lt;span class="nx"&gt;currentState&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prev&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;nodesChanged&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;()(&lt;/span&gt;&lt;span class="nx"&gt;previousState&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;cur&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;cur&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;effect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;api&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="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;util&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invalidateTags&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Nodes&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="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// the flag endpoint itself: cheap on purpose&lt;/span&gt;
&lt;span class="c1"&gt;//   query: () =&amp;gt; 'nodes/changed',   // returns ~10 bytes: a version string&lt;/span&gt;
&lt;span class="c1"&gt;//   pollingInterval: 5000,&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What happens on an update, end to end: the version goes into the cache and&lt;br&gt;
&lt;code&gt;predicate&lt;/code&gt; sees it differ from the previous snapshot, &lt;code&gt;invalidateTags&lt;/code&gt; marks&lt;br&gt;
every tagged entry stale &lt;strong&gt;without removing it&lt;/strong&gt; (the screen keeps the old&lt;br&gt;
data), mounted subscribers immediately refetch — deduplicated to one request&lt;br&gt;
per endpoint+args no matter how many components listen — and one re-render&lt;br&gt;
lands the new data. A component sees &lt;code&gt;isLoading: false, isFetching: true&lt;/code&gt;&lt;br&gt;
during that window, which is why nothing flashes. If the flag endpoint dies,&lt;br&gt;
data simply goes stale until the next successful tick; nothing breaks. If&lt;br&gt;
nodes changed five times between two ticks, one refetch covers it: the&lt;br&gt;
version is a snapshot of "what is true now", not an event queue.&lt;/p&gt;

&lt;h2&gt;
  
  
  What stayed in Redux core
&lt;/h2&gt;

&lt;p&gt;A mistake we avoided (mostly): RTK Query is for &lt;strong&gt;server state only&lt;/strong&gt;. UI-owned state — selected node, filter panel state, column layout, wizard steps — stayed in plain &lt;code&gt;createSlice&lt;/code&gt;. Migrating that into queries would have replaced a working pattern with a worse one.&lt;/p&gt;

&lt;p&gt;The end state: slices for decisions, RTK Query for data, zero custom cache code, zero hand-rolled loading flags.&lt;/p&gt;

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

&lt;p&gt;From the published case, measured as &lt;em&gt;perceived dashboard responsiveness&lt;/em&gt; (how fast fresh data lands after a change, without UI-thread churn):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dashboard responsiveness: &lt;strong&gt;+~40%&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;"Frozen dashboard" complaints: &lt;strong&gt;−30%&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;REST telemetry traffic: &lt;strong&gt;−20%&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What I can't give you is a LOC table with a precise count — we deleted more code than we wrote every migration PR, and by the end the store directory had visibly halved; I kept no exact tally, and a made-up precision here would be dishonest.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Start with RTK Query for any new page.&lt;/strong&gt; Half the migration tail was legacy thunks for features that could have been endpoints from day one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Freeze selectors before touching anything.&lt;/strong&gt; The component-facing API is the migration's unit of safety.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure per PR, not per epic.&lt;/strong&gt; The scripted interaction + INP check cost minutes per PR and caught two regressions a "it looks fine" test never would.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write the change-flag backend contract early.&lt;/strong&gt; The polling redesign was the single biggest win and it needed a backend buy-in — politeness about it cost us a month.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Your refactoring checklist
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Inventory actions/thunks/loading flags — count them, this is your "before" baseline.&lt;/li&gt;
&lt;li&gt;Freeze selectors as the component-facing contract.&lt;/li&gt;
&lt;li&gt;Add RTK Query via &lt;code&gt;combineReducers&lt;/code&gt; alongside the legacy store.&lt;/li&gt;
&lt;li&gt;Migrate one page per PR; measure INP p75 + render count on a fixed interaction.&lt;/li&gt;
&lt;li&gt;Server state → endpoints with tags; UI state stays in slices.&lt;/li&gt;
&lt;li&gt;Replace timers with a change-flag contract before touching fetch logic.&lt;/li&gt;
&lt;li&gt;Delete: action-type strings, flag families, custom cache code. The deletion count is the score.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;The performance numbers come from the same production system as my earlier piece, &lt;a href="https://dev.to/veaceslav/a-40-faster-fault-detection-pipeline-without-websockets-the-polling-trick-we-shipped-e28"&gt;A 40% Faster Fault-Detection Pipeline Without WebSockets&lt;/a&gt; — that one tells the polling-protocol story; this is the data-layer refactoring around it.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>redux</category>
      <category>typescript</category>
      <category>performance</category>
    </item>
    <item>
      <title>RAG without a vector database: fastembed + numpy, cosine by hand</title>
      <dc:creator>Veaceslav</dc:creator>
      <pubDate>Sat, 17 Oct 2026 17:03:37 +0000</pubDate>
      <link>https://dev.to/veaceslav/rag-without-a-vector-database-fastembed-numpy-cosine-by-hand-1ck9</link>
      <guid>https://dev.to/veaceslav/rag-without-a-vector-database-fastembed-numpy-cosine-by-hand-1ck9</guid>
      <description>&lt;p&gt;title: RAG without a vector database: fastembed + numpy, cosine by hand&lt;br&gt;
published: false&lt;br&gt;
description: "JobCopilot part 2: I skipped the vector database. A 33 MB embedding model, numpy, one dot product — and one retrieval bug a tutorial gave me for free."&lt;br&gt;
tags: python, ai, fastembed, numpy&lt;/p&gt;

&lt;p&gt;I'm building JobCopilot in public: a small Python agent that reads a job description, pulls the most relevant chunks of my resume, and lets a skeptical-recruiter prompt do the matching. No framework — raw code first, abstraction only when the pain justifies it. Last week I wrote about the &lt;a href="https://dev.to/veaceslav/stop-prompting-for-valid-json-an-llm-output-layer-that-does-not-break-5264"&gt;structured-output layer&lt;/a&gt;: pydantic, typed errors, retry. This week: the step &lt;em&gt;before&lt;/em&gt; that call — deciding which parts of my resume deserve to be in the prompt at all.&lt;/p&gt;

&lt;p&gt;I came to Python from React. My first instinct was infrastructure-shopping: every "build your own RAG" tutorial reaches for the same cast — a vector database, a chunking framework, an embeddings API. For my scale, a resume that chunks into single digits and one job posting at a time, that's not architecture. That's a second project to babysit, with an API key attached.&lt;/p&gt;

&lt;p&gt;So I skipped all of it: &lt;strong&gt;fastembed for embeddings, numpy for search, a 33 MB model instead of a server.&lt;/strong&gt; It survived every job posting I threw at it — after I fixed one retrieval bug a tutorial handed me for free.&lt;/p&gt;
&lt;h2&gt;
  
  
  1. Chunks in, embeddings out — the one-time script
&lt;/h2&gt;

&lt;p&gt;The index side of "RAG" turned out to be embarrassingly small. This is the store-building script — I run it once whenever my resume changes:&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="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Build my resume store: chunk once, embed once, keep for later.&lt;/span&gt;&lt;span class="sh"&gt;"""&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;re&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;fastembed&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="n"&gt;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;BAAI/bge-small-en-v1.5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;   &lt;span class="c1"&gt;# 384 dims, ~33 MB on disk, CPU-first
&lt;/span&gt;&lt;span class="n"&gt;TOP_K&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;

&lt;span class="n"&gt;_model&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;_embedder&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;fastembed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TextEmbedding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;global&lt;/span&gt; &lt;span class="n"&gt;_model&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;_model&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                      &lt;span class="c1"&gt;# lazy: don't pay on import
&lt;/span&gt;        &lt;span class="n"&gt;_model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fastembed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TextEmbedding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;MODEL&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;_model&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;chunk_resume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;md&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="nb"&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="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Split by ## headings; each section becomes one chunk.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;parts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\n(?=## )&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;md&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="n"&gt;p&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;parts&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;p&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;build_store&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;md_path&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;out_path&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="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;md&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;md_path&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;chunks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;chunk_resume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;md&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;vecs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;_embedder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;emb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vecs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;emb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;emb&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;linalg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;emb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;axis&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;keepdims&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="c1"&gt;# normalize once
&lt;/span&gt;    &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out_path&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;write_text&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chunks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&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;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;emb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tolist&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;chunks&lt;/span&gt;&lt;span class="sh"&gt;"&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;chunks&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dims&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;emb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&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="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="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;build_store&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;resume.md&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;resume_store.json&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;Why each piece earns its line:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;MODEL = "BAAI/bge-small-en-v1.5"&lt;/code&gt;&lt;/strong&gt; — 384-dimension vectors, ~33 MB on disk, runs on CPU, no API key, no runtime network (the model downloads once, then it's local). For a single-language corpus at this scale, "small embedding model" isn't a compromise — it's the correct answer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lazy &lt;code&gt;_embedder()&lt;/code&gt;&lt;/strong&gt; — the model loads on first use, not on import, so scripts and tests that never embed anything don't pay for it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;re.split(r"\n(?=## )", md)&lt;/code&gt;&lt;/strong&gt; — one regex line does the chunking: each &lt;code&gt;##&lt;/code&gt; section keeps its heading attached to its body, bodyless headings get dropped by the emptiness check. My resume lands at a handful of chunks — no chunking framework required.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Normalize once, at build time&lt;/strong&gt; (&lt;code&gt;emb / norm&lt;/code&gt;) — this pays off in the runtime section: cosine similarity on unit vectors collapses into a plain dot product. I'll come back to exactly why.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One run of &lt;code&gt;python store_resume.py&lt;/code&gt; writes a JSON file: chunk strings plus a 384-float vector each. &lt;strong&gt;If you can regenerate your index with one command, it's not infrastructure — it's a build artifact.&lt;/strong&gt; That reframe took most of the mystique out of RAG for me.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The whole runtime is 10 lines: &lt;code&gt;scores = emb @ q&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The query side — this is &lt;code&gt;retrieve.py&lt;/code&gt;, the entire retrieval engine:&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="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Runtime: pull the TOP_K chunks that matter for this job posting.&lt;/span&gt;&lt;span class="sh"&gt;"""&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;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;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;from&lt;/span&gt; &lt;span class="n"&gt;store_resume&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TOP_K&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_embedder&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;retrieve&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;store_path&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;resume_store.json&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;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;tuple&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="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;]]:&lt;/span&gt;
    &lt;span class="n"&gt;store&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="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;store_path&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;chunks&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="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="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chunks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;emb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&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;span class="p"&gt;])&lt;/span&gt;

    &lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;_embedder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;embed&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;linalg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;scores&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;emb&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;                      &lt;span class="c1"&gt;# cosine by hand: one dot product
&lt;/span&gt;    &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;argsort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scores&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;TOP_K&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="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&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;i&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;emb&lt;/code&gt; is &lt;code&gt;(n_chunks, 384)&lt;/code&gt;, &lt;code&gt;q&lt;/code&gt; is &lt;code&gt;(384,)&lt;/code&gt;. One matmul broadcasts the query over every chunk row; the &lt;code&gt;argsort&lt;/code&gt; sorts descending and keeps the top 8.&lt;/p&gt;

&lt;p&gt;Why no vector DB, honestly answered: at single-digit chunks the whole "index" is a matrix, and a full scan is one matmul — microseconds. Ann indexes buy speed at hundreds of thousands of vectors and pay with recall errors plus another service to run. At my scale that trade is upside-down: complexity buys speed I don't need.&lt;/p&gt;

&lt;p&gt;Coming from JavaScript, I'd have typed the cosine formula out manually, looping. The numpy docs (the senior-er voice in my head) said: &lt;strong&gt;if every vector is unit length, the dot product IS the cosine similarity.&lt;/strong&gt; That's why &lt;code&gt;build_store&lt;/code&gt; and &lt;code&gt;retrieve&lt;/code&gt; both normalize immediately.&lt;/p&gt;

&lt;p&gt;What the scores actually look like for the query &lt;em&gt;"Kubernetes, Terraform, SRE on-call"&lt;/em&gt; against a small demo resume (6 chunks — this is my sandbox example, not the real store):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0.784  '## Kubernetes &amp;amp; SRE track record...'
0.573  '## Side projects...'
0.550  '# Demo Resume'
0.504  '## Experience...'
0.496  '## Skills...'
0.487  '## Interests...'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The shape is the story: the relevant chunk breaks away at 0.78, then comes the noise band — chunks that don't match the query, they match &lt;em&gt;the genre of resumes&lt;/em&gt;. Retrieval is only useful while that separation exists. Which brings me to the bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The bug that got me: &lt;code&gt;TOP_K = 12&lt;/code&gt; — ranking, downgraded to bulk-loading
&lt;/h2&gt;

&lt;p&gt;Now the part I'm genuinely not proud of: my first working retrieval had &lt;code&gt;TOP_K = 12&lt;/code&gt;, because a tutorial used it and I kept it. It felt robust — "grab a lot, the LLM will figure out relevance." The effect was the opposite.&lt;/p&gt;

&lt;p&gt;The failure is invisible — no crash, no warning. &lt;code&gt;argsort()[::-1][:12]&lt;/code&gt; just hands you a dozen chunks: the one that actually matched the posting, plus eleven that match &lt;em&gt;the genre of job postings and resumes&lt;/em&gt; — Python keywords, generic resume lines. That tail was exactly what got pasted into the prompt, and the LLM, agreeable as ever, worked it into the missing-skills list with full confidence. Garbage in, confident garbage out.&lt;/p&gt;

&lt;p&gt;The score line told the story in two numbers. With an over-fed &lt;code&gt;TOP_K&lt;/code&gt; on my real store, the top of the list and the tail it dragged along looked 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;0.635  '## Work Experience — the relevant one'
...
0.450  '## Languages — pure filler for this query'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A 0.185 spread where only the first row deserved attention; everything below ~0.55 was noise-band bulk. &lt;strong&gt;No separation.&lt;/strong&gt; Retrieval had stopped ranking and started bulk-loading with extra steps. My fix was one line:&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;TOP_K&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;    &lt;span class="c1"&gt;# was 12 from a tutorial; enough for a resume, small enough to keep the gap
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0.635  '## Work Experience — the relevant one'
0.592  '## Technical Skills — adjacent, still on-topic'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the 8-slot list ends in the languages/education band — nothing under 0.45 enters the prompt. The top row visibly separates again.&lt;/p&gt;

&lt;p&gt;The rule I use now (embarrassingly late in my Python learning): &lt;strong&gt;TOP_K must stay small enough that the top-1 score visibly separates from the rest.&lt;/strong&gt; The moment it stops separating, retrieval is decoration and the prompt is a firehose. On my resume, 8 is enough. And the tutorial never mentioned any of this — perfectly correct code can still be wrong, because a constant you copied was tuned for someone else's scale.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. From ranked chunks to a prompt, with a character budget
&lt;/h2&gt;

&lt;p&gt;Retrieval gives me ranked chunks with scores; the prompt needs one string. I could &lt;code&gt;.join()&lt;/code&gt; the top 8 and move on — but the naive join has two failure modes: a monster section eating the prompt's attention budget, and a torn tail — a chunk cut in half by a &lt;code&gt;[:2400]&lt;/code&gt; slice, half a bullet floating in the prompt.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;build_context&lt;/code&gt; fixes both:&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;build_context&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;store_path&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;resume_store.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                  &lt;span class="n"&gt;max_chars&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;2400&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;Join top chunks into one string for the prompt, and STOP when full.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;=&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;for&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_score&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;retrieve&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="n"&gt;store_path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;+&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;text&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;max_chars&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;                  &lt;span class="c1"&gt;# a chunk that doesn't fit is dropped WHOLE
&lt;/span&gt;        &lt;span class="n"&gt;out&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="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stop at &lt;code&gt;max_chars&lt;/code&gt; even if that uses fewer than TOP_K chunks.&lt;/strong&gt; A prompt-quality knob, not a storage quota: 2400 tightly selected characters beat 6000 of everything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cut at chunk boundaries.&lt;/strong&gt; A chunk that would overflow is skipped whole — no torn half-bullet floating in the prompt.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I tested a &lt;code&gt;Represent this sentence for searching relevant passages:&lt;/code&gt; query prefix (documented for &lt;code&gt;bge&lt;/code&gt; models) against my store: top score +0.014, but the gap to the second row &lt;em&gt;shrank&lt;/em&gt; — a wash for ranking, which is what actually matters at my scale. So there's no query-prefix engineering in this pipeline; normalization is the only trick worth keeping.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Retrieval without a vector DB: chunk by headings → embed once with a 33 MB model → normalize at build time → &lt;code&gt;emb @ q&lt;/code&gt; at runtime → keep top-8 → join into a char-budgeted context. &lt;strong&gt;About 60 lines total&lt;/strong&gt; — and I understand every one of them, which is more than I could say for a vector DB I don't need yet.&lt;/p&gt;

&lt;p&gt;Three things this React-dev-learning-Python took from it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For small corpora, a "vector store" is a JSON file and a matmul. The tutorials don't tell you when your scale justifies something heavier — your own score gap does.&lt;/li&gt;
&lt;li&gt;Copy-pasted defaults have their owner's scale baked in. &lt;code&gt;TOP_K = 12&lt;/code&gt; was fine in the tutorial's corpus and quietly destructive in mine.&lt;/li&gt;
&lt;li&gt;The most honest measure isn't the top score — it's the &lt;strong&gt;gap&lt;/strong&gt; between the top score and the next one. If the gap is gone, nothing downstream matters.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next Tuesday, part 3: my first agent graph — nodes that return partial state patches instead of raising, routing on data instead of exceptions, and the LangGraph mental shift from "function calls function" to "state flows through a graph".&lt;/p&gt;

&lt;p&gt;Follow if you want to watch the rest of the copilot get built — including the parts that break. Build log + daily code: &lt;a href="https://t.me/novamind_hub" rel="noopener noreferrer"&gt;t.me/novamind_hub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;How do you handle retrieval at small scale — still reaching for a vector DB, or plain numpy? And what default did you copy from a tutorial that quietly cost you?&lt;/p&gt;

</description>
      <category>python</category>
      <category>ai</category>
      <category>fastembed</category>
      <category>numpy</category>
    </item>
    <item>
      <title>A 40% Faster Fault-Detection Pipeline Without WebSockets — the Polling Trick We Shipped</title>
      <dc:creator>Veaceslav</dc:creator>
      <pubDate>Tue, 06 Oct 2026 07:01:21 +0000</pubDate>
      <link>https://dev.to/veaceslav/a-40-faster-fault-detection-pipeline-without-websockets-the-polling-trick-we-shipped-e28</link>
      <guid>https://dev.to/veaceslav/a-40-faster-fault-detection-pipeline-without-websockets-the-polling-trick-we-shipped-e28</guid>
      <description>&lt;p&gt;title: A 40% Faster Fault-Detection Pipeline Without WebSockets — the Polling Trick We Shipped&lt;br&gt;
published: false&lt;br&gt;
description: Real-time monitoring for 1,000+ network nodes over plain REST — a change-flag endpoint, RTK Query cache discipline, and an operator map that shows the causal chain, not the noise.&lt;br&gt;
tags: react, rtkquery, architecture&lt;/p&gt;

&lt;p&gt;This is the dev.to version of a story I first published as a LinkedIn article — restructured for a code-first audience. It's a real production system: real-time monitoring for 1,000+ GSM service stations, built for an international partner. And one honest constraint at its core: &lt;strong&gt;we could not touch the backend protocol.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The ticket-queue blind spot
&lt;/h2&gt;

&lt;p&gt;The worst part of a monitoring outage isn't the failure itself. It's not being able to tell the difference between two situations that look identical on a ticket queue:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a node recovered &lt;strong&gt;by itself&lt;/strong&gt;, and&lt;/li&gt;
&lt;li&gt;a node is &lt;strong&gt;still broken&lt;/strong&gt;, but nobody noticed yet.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Our network engineers — a field team plus a back office — lived inside that ambiguity for years. The back office created tickets from Grafana data, and a ticket meant &lt;em&gt;wait&lt;/em&gt;: someone opened it, someone processed it, and a certain amount of time always passed before the incident became visible to people who could act on it.&lt;/p&gt;
&lt;h2&gt;
  
  
  The constraint that reframed everything
&lt;/h2&gt;

&lt;p&gt;I checked the obvious answers first: whether the Romanian backend team could support &lt;strong&gt;WebSockets or SSE&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;They couldn't. The backend was a monolith, and a deep refactor to introduce a new transport protocol was off the table. Not "hard" — &lt;em&gt;off the table&lt;/em&gt;. Different project, different budget, different risk.&lt;/p&gt;

&lt;p&gt;If the client can't be &lt;em&gt;pushed&lt;/em&gt; updates, then maybe it can be &lt;em&gt;woken up&lt;/em&gt; cheaply.&lt;/p&gt;
&lt;h2&gt;
  
  
  The reframe: change the data granularity, not the protocol
&lt;/h2&gt;

&lt;p&gt;My proposal split the information flow into two layers:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. A cheap change-flag.&lt;/strong&gt; The backend exposes a tiny flag/version field on an ordinary REST endpoint — a number that increments whenever anything relevant changes underneath. We designed this together: they implemented a small flags feature to my spec, I tracked it from the frontend.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. A heavy data pull.&lt;/strong&gt; The actual node states — statuses, incidents, telemetry the dashboard renders.&lt;/p&gt;

&lt;p&gt;The frontend polls &lt;strong&gt;only the flag&lt;/strong&gt; on a light interval. When the flag hasn't changed, the request costs nothing meaningful. When it has — RTK Query re-fetches the heavy data &lt;em&gt;and&lt;/em&gt; its cache is immediately invalidated and repopulated. The dashboard behaves as if it were pushed to, while under the hood everything is still plain REST.&lt;/p&gt;

&lt;p&gt;That's the whole trick:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;naive polling:            [timer] → pull full payload → render   (every N seconds, always)
flag-driven polling:      [timer] → pull 1 number  ─┬─ unchanged → nothing
                                                    └─ changed   → invalidate → pull full payload → render
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The RTK Query side: cache and refetch discipline
&lt;/h2&gt;

&lt;p&gt;Two things came out of the RTK Query migration itself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Caching.&lt;/strong&gt; Identical requests were served from cache instead of re-hitting the API — removing redundant traffic the legacy thunk-based store generated on its own.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refetch discipline.&lt;/strong&gt; Refetches happened &lt;em&gt;on demand&lt;/em&gt; — flag changed, cache invalidated, window refocused — not on a dumb interval burning everyone's patience and the backend's CPU.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The polling setup in practice:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// poll the flag only — light interval, e.g. 5s&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;flag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;usePollChangeFlag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;intervalMs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// the heavy query is keyed on the flag value:&lt;/span&gt;
&lt;span class="c1"&gt;// flag moves → new cache key → automatic refetch + repopulate&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nodes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useGetNodesQuery&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;flag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;The exact API shape was refactored a few times since — the pattern is the deliverable, not the signature. The heavy-fetch key follows the flag; refetch happens when and only when the flag moves.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Showing pain, not data: the map
&lt;/h2&gt;

&lt;p&gt;Raw node counts are useless to a human operator. 1,000+ stations on one screen is noise.&lt;/p&gt;

&lt;p&gt;The stations had real topology: &lt;strong&gt;parent–child hierarchy&lt;/strong&gt;. A parent going down didn't just break itself — it degraded its children. So we drew the map differently:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the map shows &lt;strong&gt;only the affected subtree&lt;/strong&gt;, not the whole network;&lt;/li&gt;
&lt;li&gt;when a parent fails, its dependent children light up as &lt;em&gt;potentially damaged&lt;/em&gt;;&lt;/li&gt;
&lt;li&gt;in a typical impact window the map displayed &lt;strong&gt;around 400 nodes&lt;/strong&gt; — the ones potentially hurt through connectivity — instead of 1,000+.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An engineer opens the map and sees a causal chain, not a cloud of dots. And the back office, which had been hand-writing tickets out of Grafana, now pulled the incident structure — parent, children, order of failure — from a single source.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers
&lt;/h2&gt;

&lt;p&gt;We validated the improvement with two existing instruments: &lt;strong&gt;Grafana metrics&lt;/strong&gt; and &lt;strong&gt;the ticket system itself&lt;/strong&gt; (opening-to-resolution timestamps, across the before/after periods).&lt;/p&gt;

&lt;p&gt;Result: &lt;strong&gt;incident detection time improved by ~40%.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Being precise about what the number means — never defend a fuzzy metric in an interview: the &lt;em&gt;clock&lt;/em&gt; we shortened is "failure happens → failure is actionable" — previously dominated by ticket latency, now dominated by flag-poll latency. Nodes that self-recovered no longer polluted the queue and no longer &lt;em&gt;hid&lt;/em&gt; the ones that didn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you can't change the protocol, change the granularity
&lt;/h2&gt;

&lt;p&gt;You don't always need WebSockets. You need the &lt;em&gt;cheapest possible signal that something changed&lt;/em&gt; — a version number, a hash, an etag — and a client disciplined enough to pull the real payload only when that signal moves. Pair it with cache-first architecture (RTK Query, React Query — whatever your stack provides) so heavy data is fetched rarely and reused often.&lt;/p&gt;

&lt;p&gt;And when you present it to a human operator: don't show them the network. Show them the pain — the affected subtree, the causal chain. A map of 400 hurting nodes is worth more than a map of 1,000 quiet ones.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built with React, TypeScript, RTK Query — and with backend partners who were straightforward about what the system could and couldn't give us.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I write about React patterns and the engineering behind my AI tooling — &lt;a href="https://t.me/novamind_hub" rel="noopener noreferrer"&gt;building in public on Telegram&lt;/a&gt;, the LinkedIn version of this story is &lt;a href="https://www.linkedin.com/pulse/how-i-cut-fault-detection-time-40-1000-node-network-without-bezusco-fmpgf" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;How do you do "real-time" when the protocol won't budge — flag-polling, SSE workarounds, or plain polling? Curious what others landed on.&lt;/p&gt;

</description>
      <category>react</category>
      <category>rtkquery</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Stop prompting for valid JSON: an LLM output layer that does not break</title>
      <dc:creator>Veaceslav</dc:creator>
      <pubDate>Tue, 29 Sep 2026 08:45:52 +0000</pubDate>
      <link>https://dev.to/veaceslav/stop-prompting-for-valid-json-an-llm-output-layer-that-does-not-break-5264</link>
      <guid>https://dev.to/veaceslav/stop-prompting-for-valid-json-an-llm-output-layer-that-does-not-break-5264</guid>
      <description>&lt;p&gt;I'm building JobCopilot: a small Python agent that reads a job description, compares it to my resume, and tells me what skills I'm missing. Like a skeptical recruiter in a terminal.&lt;/p&gt;

&lt;p&gt;I want the agent's brain to be an LLM call. But my program needs an &lt;strong&gt;answer in a defined shape&lt;/strong&gt; — like a form with three fields — not a wall of prose. And there's the problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem in one picture
&lt;/h2&gt;

&lt;p&gt;I asked the model: &lt;em&gt;return only raw JSON, no explanation&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;It agreed politely. Then it sent:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Sure! Here's the breakdown in raw JSON:&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "verdict": "partial",
  "match_score": 68,
  "missing_skills": ["FastAPI", "Alemdic"]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;

&lt;p&gt;See it? The JSON is wrapped in a &lt;strong&gt;hello + a code fence&lt;/strong&gt; (the JSON-fence block). Python's &lt;code&gt;json.loads()&lt;/code&gt; says there's no JSON there at all — it sees the words "Sure! Here's..." and dies. If I ask the model again, it does the same thing slightly differently. You can write "I really mean it this time!!!" in the prompt. It does not help. I tried for an hour.&lt;/p&gt;

&lt;p&gt;And when the model &lt;em&gt;does&lt;/em&gt; send clean JSON one night, there are three more ways it betrays you:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Wrong key names&lt;/strong&gt; — it sends &lt;code&gt;skills_missing&lt;/code&gt; today and &lt;code&gt;missing_skills&lt;/code&gt; tomorrow. Your code searched for one of them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wrong types&lt;/strong&gt; — the score arrives as &lt;code&gt;87.5&lt;/code&gt;, or even &lt;code&gt;105&lt;/code&gt;, when your program expects an integer up to 100.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Correct format, wrong content&lt;/strong&gt; — &lt;code&gt;{"verdict": "no", "missing_skills": []}&lt;/code&gt;: shape is perfect, logic is nonsense.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;"A prompt is a suggestion. Only code is a contract." So instead of fighting the prompt, I built a small &lt;strong&gt;validation layer&lt;/strong&gt; — a wall between the model and my program. Whatever comes out of the model, my program either receives a valid form or a precise error report. Here it is, in four steps any beginner can follow.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 1: Unwrap the JSON (8 lines of Python)
&lt;/h2&gt;

&lt;p&gt;The model keeps wrapping JSON in JSON-fence fences no matter what I write in the prompt. Fine — let the code unwrap it:&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;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;

&lt;span class="c1"&gt;# match (.*?) between ```
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;endraw&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
 &lt;span class="n"&gt;pairs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;newlines&lt;/span&gt; &lt;span class="err"&gt;—&lt;/span&gt; &lt;span class="n"&gt;that&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s DOTALL
_FENCE_RE = re.compile(r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;
{% raw %}
```(?:json)?\s*(.*?)\s*```

&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, re.DOTALL)


def extract_json(raw: str) -&amp;gt; dict:
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Takes the model&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="n"&gt;Returns&lt;/span&gt; &lt;span class="n"&gt;clean&lt;/span&gt; &lt;span class="n"&gt;JSON&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    try:
        return json.loads(raw)      # best case: it was already clean JSON
    except json.JSONDecodeError:
        pass                        # no? look for the fence below...

    match = _FENCE_RE.search(raw)
    if match is None:
        raise ValueError(f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no JSON found in: {raw[:200]}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;)

    loaded = json.loads(match.group(1))
    if not isinstance(loaded, dict):
        raise ValueError(f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expected a JSON object, got {type(loaded).__name__}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;)
    return loaded


&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;This function tries the honest path first (&lt;code&gt;json.loads(raw)&lt;/code&gt;), and if the text has chatty greetings, it grabs whatever sits between the triple-backtick marks and parses THAT.&lt;/p&gt;

&lt;p&gt;Three small details that each cost me an evening of confusion:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;re.DOTALL&lt;/code&gt;&lt;/strong&gt; — without it, the dot in my regex stops at the first line break, and the fenced JSON spans many lines. My first version matched nothing, silently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;(?:json)?&lt;/code&gt;&lt;/strong&gt; — the model writes the language hint or a bare triple-backtick fence. This part makes the hint optional, so both work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;search&lt;/code&gt;, not &lt;code&gt;fullmatch&lt;/code&gt;&lt;/strong&gt; — because there's prose around the JSON: "Sure!", intro sentences, sometimes a closing joke.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One bonus detail I did not know: in Python, &lt;code&gt;JSONDecodeError&lt;/code&gt; is a subclass of &lt;code&gt;ValueError&lt;/code&gt;. That means &lt;code&gt;except ValueError&lt;/code&gt; catches both — I'll use this in Step 3.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 2: Check the form field by field (pydantic)
&lt;/h2&gt;

&lt;p&gt;The JSON parses now. But parsing is not validating: &lt;code&gt;{"match_score": 105}&lt;/code&gt; parses perfectly fine. So the next layer checks the &lt;em&gt;shape of the data&lt;/em&gt; field by field:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
python
from typing import Literal
from pydantic import BaseModel, Field, field_validator, model_validator


class JobMatch(BaseModel):
    verdict: Literal["strong", "partial", "no"]   # ONLY these three values
    match_score: int = Field(ge=0, le=100)         # int, 0..100, nothing else
    missing_skills: list[str]                      # a list of strings

    @field_validator("missing_skills")
    @classmethod
    def clean_skills(cls, skills):
        # model sent ["React", "react", "  CSS ", ""] once. Clean it:
        seen = set()
        out = []
        for s in skills:
            key = s.strip().lower()
            if not key or key in seen:
                continue
            seen.add(key)
            out.append(s.strip())
        return out

    @model_validator(mode="after")
    def skeptical_check(self):
        # "no" verdict with empty list = the model gave up
        if self.verdict == "no" and not self.missing_skills:
            raise ValueError("verdict 'no' but no missing skills — model gave up")
        return self


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Read this as a &lt;strong&gt;form with three fields&lt;/strong&gt;, each field guarded:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The verdict must be exactly &lt;code&gt;"strong"&lt;/code&gt;, &lt;code&gt;"partial"&lt;/code&gt; or &lt;code&gt;"no"&lt;/code&gt; — the model once answered &lt;code&gt;"maybe"&lt;/code&gt;; with &lt;code&gt;Literal&lt;/code&gt; that becomes impossible.&lt;/li&gt;
&lt;li&gt;The score must be an integer between 0 and 100. The model sent &lt;code&gt;105&lt;/code&gt; once. Now that is impossible too.&lt;/li&gt;
&lt;li&gt;The skills list gets cleaned: duplicated &lt;code&gt;"React"&lt;/code&gt; and &lt;code&gt;"react"&lt;/code&gt; collapse into one, empty strings are dropped.&lt;/li&gt;
&lt;li&gt;The final check catches nonsense logic: if the verdict says "no match" but lists zero missing skills, the model gave up instead of analyzing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When a value breaks a rule, pydantic produces an error that is &lt;em&gt;specific&lt;/em&gt;:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
text
match_score: Input should be a valid integer, got a number with a fractional
             part [type=int_from_float, input_value=87.5]
verdict:     Input should be 'strong', 'partial' or 'no'
             [type=literal_error, input_value='maybe']


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Notice we threw out about thirty lines of handwritten &lt;code&gt;if "verdict" not in data: ...&lt;/code&gt; code. The declarative model does the same job, and the errors are not "value error" mush — they name the field, the problem and the offending value.&lt;/p&gt;

&lt;p&gt;One honest limit: this layer guarantees the &lt;strong&gt;shape&lt;/strong&gt;, not the truth. It will not catch &lt;code&gt;"Alemdic"&lt;/code&gt; — a misspelling of &lt;code&gt;Alembic&lt;/code&gt; that is perfectly well-formed JSON. Catching wrong-but-valid content is the job of an eval set, and that is the next article in this series.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 3: Return an error as a &lt;em&gt;result&lt;/em&gt;, not a crash
&lt;/h2&gt;

&lt;p&gt;Now the top layer. My rule: the model can fail, and the program must handle that failure like any other value. If a function raises an exception, the caller has to catch it — and if the caller forgets, the whole program dies. If a function &lt;em&gt;returns&lt;/em&gt; the error, the caller just decides what to do.&lt;/p&gt;

&lt;p&gt;If you write TypeScript, you know &lt;code&gt;zod.safeParse()&lt;/code&gt; — it returns a result object with a status. We can build the same in Python with two dataclasses:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
python
from dataclasses import dataclass


@dataclass
class ParseOk:
    match: JobMatch                       # valid data


@dataclass
class ParseError:
    stage: str      # "json" — couldn't extract, or "schema" — invalid
    message: str    # exact reason, for me AND for the next LLM call


def parse_match(raw: str) -&amp;gt; ParseOk | ParseError:
    try:
        data = extract_json(raw)          # step 1
    except ValueError as exc:             # JSONDecodeError IS a ValueError
        return ParseError(stage="json", message=str(exc))

    try:
        return ParseOk(match=JobMatch.model_validate(data))   # step 2
    except ValidationError as exc:
        return ParseError(stage="schema", message=str(exc))


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;The type &lt;code&gt;ParseOk | ParseError&lt;/code&gt; works like a union type from TypeScript: every result is definitely one or the other, and the caller checks with &lt;code&gt;isinstance(result, ParseOk)&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 4: The retry — give the exact error back to the model
&lt;/h2&gt;

&lt;p&gt;There is one more trick with &lt;code&gt;ParseError.message&lt;/code&gt;. The model &lt;strong&gt;can repair itself&lt;/strong&gt; if you tell it exactly what failed. "It failed" teaches nothing. But:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;match_score: Input should be a valid integer, got a number with a fractional part [type=int_from_float, input_value=87.5]&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;...the model fixes almost every time. So the retry loop feeds the exact pydantic error back into the next call:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
python
FIX_PROMPT = """Below was your previous output and the validation error.
Return corrected JSON only.

Previous output:
{previous}

Validation error:
{error}"""


def structured_match(chat, jd_text: str, max_retries: int = 2):
    prompt = MATCH_PROMPT.format(job=jd_text)

    result = None
    for attempt in range(1 + max_retries):
        raw = chat(prompt)
        result = parse_match(raw)
        if isinstance(result, ParseOk):
            return result
        prompt = FIX_PROMPT.format(previous=raw, error=result.message)

    return result   # after max retries: ParseError as data — never None


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Retry at most twice, then give up — but give up &lt;em&gt;as a result&lt;/em&gt;, not a crash. The caller decides: log it, message the user, or end the run.&lt;/p&gt;

&lt;p&gt;(A quick note about frameworks: many LLM providers have native JSON mode. If yours does, use it — it kills most fence problems at the root. But keep the pydantic layer anyway: native mode guarantees shape, not logic, and &lt;code&gt;match_score &amp;lt;= 100&lt;/code&gt; survives on every provider.)&lt;/p&gt;

&lt;h2&gt;
  
  
  The whole picture
&lt;/h2&gt;

&lt;p&gt;Four small pieces, each doing one job:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Unwrap&lt;/strong&gt; the code fence — &lt;code&gt;extract_json&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validate&lt;/strong&gt; field by field — the &lt;code&gt;JobMatch&lt;/code&gt; pydantic model&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Convert&lt;/strong&gt; errors into typed data — &lt;code&gt;ParseOk&lt;/code&gt; / &lt;code&gt;ParseError&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retry&lt;/strong&gt; with the exact error fed back — &lt;code&gt;structured_match&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In one sentence: &lt;strong&gt;code, not the prompt, decides when the model's output is good.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Two things I took from building it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A prompt is a suggestion; a pydantic model is a contract.&lt;/li&gt;
&lt;li&gt;"Errors as data" sounds fancy but is 15 lines: two dataclasses and an &lt;code&gt;isinstance&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next Tuesday, part 2: retrieval — embeddings with &lt;code&gt;fastembed&lt;/code&gt;, cosine similarity computed by hand with numpy, and why my top-8 retrieval stopped discriminating on a resume of 20 chunks.&lt;/p&gt;

&lt;p&gt;Follow if you want to see the rest of the copilot get built — including the parts that will break. Build log + daily code: &lt;a href="https://t.me/novamind_hub" rel="noopener noreferrer"&gt;t.me/novamind_hub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;How do you harden LLM JSON in your projects — native JSON mode, validators, or retries? I'm still collecting failure modes.&lt;/p&gt;

</description>
      <category>python</category>
      <category>ai</category>
      <category>llm</category>
      <category>pydantic</category>
    </item>
    <item>
      <title>Stop bloating your AGENTS.md: reference your conventions instead of pasting them</title>
      <dc:creator>Veaceslav</dc:creator>
      <pubDate>Tue, 22 Sep 2026 17:28:35 +0000</pubDate>
      <link>https://dev.to/veaceslav/stop-bloating-your-agentsmd-reference-your-conventions-instead-of-pasting-them-b2i</link>
      <guid>https://dev.to/veaceslav/stop-bloating-your-agentsmd-reference-your-conventions-instead-of-pasting-them-b2i</guid>
      <description>&lt;p&gt;Stop bloating your AGENTS.md: reference your conventions instead of pasting them&lt;/p&gt;

&lt;p&gt;Most AGENTS.md files I see are bloated walls of text. Here is the problem: your agent reads this file at the start of every task. Everything you put in there is passed to the LLM with every prompt. A 400-line AGENTS.md burns context tokens on every single action — even when the agent only needs one rule from it.&lt;/p&gt;

&lt;p&gt;The fix is simple: keep AGENTS.md as a thin index, move the details into separate docs, and reference them.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it looks like in practice
&lt;/h2&gt;

&lt;p&gt;Here is the real AGENTS.md from my current project (invoice management SPA):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# AGENTS.md&lt;/span&gt;

&lt;span class="gu"&gt;## Project&lt;/span&gt;
IOD Invoice — invoice management SPA.
React 18 + TypeScript + Vite, Ant Design 6, Redux Toolkit (RTK Query),
styled-components, i18next (EN/RO), OIDC/Keycloak auth.

&lt;span class="gu"&gt;## Commands (npm; Node 18+)&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`npm run dev`&lt;/span&gt; — dev server
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`npm run build`&lt;/span&gt; — tsc + production build
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`npm run test`&lt;/span&gt; — vitest watch; &lt;span class="sb"&gt;`npm run test:ci`&lt;/span&gt; — run + coverage
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`npm run lint`&lt;/span&gt; — eslint, zero warnings tolerated

&lt;span class="gu"&gt;## Architecture&lt;/span&gt;
See &lt;span class="sb"&gt;`docs/architecture.md`&lt;/span&gt;

&lt;span class="gu"&gt;## TypeScript Conventions&lt;/span&gt;
See &lt;span class="sb"&gt;`docs/typescript-conventions.md`&lt;/span&gt;

&lt;span class="gu"&gt;## Style&lt;/span&gt;
See &lt;span class="sb"&gt;`docs/style-conventions.md`&lt;/span&gt;

&lt;span class="gu"&gt;## React Components&lt;/span&gt;
See &lt;span class="sb"&gt;`docs/react-components.md`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And in &lt;code&gt;docs/&lt;/code&gt; live the actual conventions — architecture decisions, TypeScript rules, styling rules, component patterns.&lt;/p&gt;

&lt;p&gt;The agent loads the index plus &lt;strong&gt;only the docs it actually needs for the current task&lt;/strong&gt;. A TypeScript task? It reads one conventions file, not your entire wiki. That is the whole trick: your conventions stay detailed, your context stays clean.&lt;/p&gt;

&lt;h2&gt;
  
  
  One gotcha: the CLAUDE.md twin problem
&lt;/h2&gt;

&lt;p&gt;Many of us run more than one agent. Claude Code reads &lt;code&gt;CLAUDE.md&lt;/code&gt;, everything else reads &lt;code&gt;AGENTS.md&lt;/code&gt; — and the two files slowly drift apart until each agent works from different rules.&lt;/p&gt;

&lt;p&gt;The fix is a symlink, so both names point at one file:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;macOS / Linux:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;ln&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; AGENTS.md CLAUDE.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Windows (order is reversed):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;mklink&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;CLAUDE.md&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;AGENTS.md&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Edit one file, both agents see the update. No drift, no duplicated maintenance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus: monorepos want nested AGENTS.md files
&lt;/h2&gt;

&lt;p&gt;In a monorepo, put an &lt;code&gt;AGENTS.md&lt;/code&gt; in each subfolder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app/
├── AGENTS.md          # shared rules
├── client/
│   └── AGENTS.md      # frontend-specific rules
└── backend/
    └── AGENTS.md      # backend-specific rules
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Working inside &lt;code&gt;client/&lt;/code&gt;, the agent loads &lt;strong&gt;both&lt;/strong&gt; — the root file and the subfolder file. Shared conventions live once, stack-specific rules live next to the code they govern.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;Your AGENTS.md is not documentation — it is a &lt;strong&gt;context budget&lt;/strong&gt;. Every line in it is paid on every task. Index + references keeps the rules detailed and the context cheap.&lt;/p&gt;




&lt;p&gt;If this was useful:&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;More React + AI tips daily&lt;/strong&gt; in my Telegram channel: t.me/novamind_hub&lt;/p&gt;

&lt;p&gt;I'm building NovaMind — an AI assistant in Telegram that debugs errors from screenshots (the reason my AGENTS.md files matter so much): t.me/mindrorgebot_bot — 30 messages/day free.&lt;/p&gt;

&lt;p&gt;Building with coding agents? Drop your biggest AGENTS.md pain in the comments — curious which conventions you enforce first 👇&lt;/p&gt;

</description>
      <category>agentsmd</category>
      <category>aiengineering</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Handling Dynamic Role Names in Different Environments with useRoleManagement Hook (Part 2)</title>
      <dc:creator>Veaceslav</dc:creator>
      <pubDate>Mon, 02 Sep 2024 07:13:46 +0000</pubDate>
      <link>https://dev.to/veaceslav/handling-dynamic-role-names-in-different-environments-with-userolemanagement-hook-part-2-1jca</link>
      <guid>https://dev.to/veaceslav/handling-dynamic-role-names-in-different-environments-with-userolemanagement-hook-part-2-1jca</guid>
      <description>&lt;p&gt;In the first part of this series, we explored the foundation of implementing role-based access control in React using the useRoleManagement hook. If you haven't read it yet, you can check it out here &lt;br&gt;
&lt;a href="https://dev.to/veaceslav/implementing-role-based-access-control-in-react-a-deep-dive-into-userolemanagement-hook-129i"&gt;Implementing Role-Based Access Control in React: A Deep Dive into useRoleManagement Hook&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In this second part, we'll dive deeper into managing dynamic role names depending on different environments, such as staging and production. This is crucial for maintaining consistent and secure role management across various stages of development.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Overview&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When developing applications, it's common to have different role names or permissions configurations for various environments. For instance, you might use test or mock role names in development and real, production-ready role names in the production environment. Handling these dynamic role names correctly ensures that your application behaves consistently and securely across different stages of its lifecycle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Environment-Specific Role Configuration&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To manage role names dynamically, we can leverage environment variables. These variables allow us to define different role keys for different environments, which can then be used to load the appropriate permissions configuration.&lt;/p&gt;

&lt;p&gt;Here’s how we can achieve this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Define Environment Variables&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In your .env.development and .env.production files, specify the role keys for each environment. For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;.env.development
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;VITE_ROLE_KEYS_MANAGER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;Manager_Test&lt;/span&gt;
&lt;span class="nx"&gt;VITE_ROLE_KEYS_USER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;USER_Test&lt;/span&gt;
&lt;span class="nx"&gt;VITE_ROLE_KEYS_ADMIN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;Admin_Test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;.env.production
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;VITE_ROLE_KEYS_MANAGER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;Prod_Manager&lt;/span&gt;
&lt;span class="nx"&gt;VITE_ROLE_KEYS_USER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;Prod_USER&lt;/span&gt;
&lt;span class="nx"&gt;VITE_ROLE_KEYS_ADMIN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;Prod_Admin&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This setup ensures that your application uses appropriate role names depending on the environment it’s running in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Update the permissions Object&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Use these environment variables to define the permissions object dynamically. The permissions object maps role keys to specific permissions for each role:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;UserPermissions&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&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="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;VITE_ROLE_KEYS_MANAGER&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;partners&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;view&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;edit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;deleteRow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="c1"&gt;// ... other permissions&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;VITE_ROLE_KEYS_USER&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;partners&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;view&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;edit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;deleteRow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="c1"&gt;// ... other permissions&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;

  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;VITE_ROLE_KEYS_ADMIN&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;partners&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;view&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;edit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;deleteRow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="c1"&gt;// ... other permissions&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 import.meta.env syntax allows you to access environment variables defined in your .env files.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Use the useRoleManagement Hook&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In the first part of this series, we explored the foundation of role-based access control in React using the useRoleManagement hook. If you haven't read it yet, you can check it out &lt;a href="https://dev.to/veaceslav/implementing-role-based-access-control-in-react-a-deep-dive-into-userolemanagement-hook-129i"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The useRoleManagement hook extracts user roles from the decoded JWT token, maps them to permissions using the environment-specific role keys, and then returns the relevant permissions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useRoleManagement&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;pathname&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useLocation&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;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;token&lt;/span&gt;&lt;span class="dl"&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;decodedToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Realm&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;decodedToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jwtDecode&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Realm&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&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;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;Invalid token:&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="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;roles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;decodedToken&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;realm_access&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;roles&lt;/span&gt; &lt;span class="o"&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;roleExists&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ifRoleExists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;roles&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;rolePermissions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getPermissions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;roleExists&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;pathname&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;pathname&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/partners&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;rolePermissions&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="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="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;add&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;view&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;edit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;deleteRow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;confirm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;include&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rolePermissions&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Permissions&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="nx"&gt;add&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;view&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;edit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;deleteRow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;confirm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;include&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;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;By dynamically managing role names through environment variables, you can ensure that your application's role-based access control behaves consistently across different environments. This approach provides flexibility and helps maintain a clear separation between development and production configurations.&lt;/p&gt;

</description>
      <category>react</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Implementing Role-Based Access Control in React: A Deep Dive into useRoleManagement Hook</title>
      <dc:creator>Veaceslav</dc:creator>
      <pubDate>Sun, 14 Jul 2024 18:09:08 +0000</pubDate>
      <link>https://dev.to/veaceslav/implementing-role-based-access-control-in-react-a-deep-dive-into-userolemanagement-hook-129i</link>
      <guid>https://dev.to/veaceslav/implementing-role-based-access-control-in-react-a-deep-dive-into-userolemanagement-hook-129i</guid>
      <description>&lt;p&gt;&lt;strong&gt;Intorduction&lt;/strong&gt;&lt;br&gt;
 Managing user roles and permissions is a critical aspect of web application security and functionality. It ensures that users have appropriate access to different parts of the application based on their roles. In this article, I'll walk you through the implementation of a custom useRoleManagement hook in React, which decodes a JWT token to manage user permissions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Setting Up&lt;/strong&gt;&lt;br&gt;
 Before diving into the implementation, let's set up the necessary tools and libraries. You'll need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;React&lt;/li&gt;
&lt;li&gt;jwt-decode for decoding JWT tokens&lt;/li&gt;
&lt;li&gt;react-router-dom for managing routing and extracting the current path
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install react-router-dom jwt-decode
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;Implementation&lt;/strong&gt;&lt;br&gt;
 Let's start by defining the interfaces for our permissions and JWT payload.&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;JwtPayload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;jwtDecode&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;jwt-decode&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useLocation&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;react-router-dom&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Realm is for authentication mechanism like Keycloak&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Realm&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;JwtPayload&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;realm_access&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;roles&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="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Interface representing the user's permissions&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Permissions&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;add&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;view&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;edit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;confirm&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;include&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;deleteRow&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Interface representing the user's permissions for various pages&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;UserPermissions&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;partners&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;Permissions&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;projects&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;Permissions&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="c1"&gt;// More pages can be added here as needed&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;PermissionKeys&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kr"&gt;keyof&lt;/span&gt; &lt;span class="nx"&gt;UserPermissions&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, we define the permissions for different roles:&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;permissions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;manager&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;partners&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;view&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;edit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;deleteRow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;projects&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;view&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;edit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;deleteRow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="c1"&gt;// Define more pages&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;analyst&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Define permissions for analyst role&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;director&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Define permissions for director role&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;&lt;strong&gt;Check if Role Exists&lt;/strong&gt;&lt;br&gt;
We need a function to check if a role exists and return the associated permissions:&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;ifRoleExists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;roles&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;roles&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;role&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;permissions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;keyof&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;permissions&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="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Boolean&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="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Get Permissions for the Role&lt;/strong&gt;&lt;br&gt;
This function retrieves the permissions for a given role and path:&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;getPermissions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;roleExists&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UserPermissions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pathName&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;withoutSlash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pathName&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&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="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;PermissionKeys&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;roleExists&lt;/span&gt;&lt;span class="p"&gt;?.[&lt;/span&gt;&lt;span class="nx"&gt;withoutSlash&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;pathName&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;&lt;strong&gt;useRoleManagement Hook&lt;/strong&gt;&lt;br&gt;
Finally, we implement the useRoleManagement hook:&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useRoleManagement&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;pathname&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useLocation&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;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;token&lt;/span&gt;&lt;span class="dl"&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;decodedToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Realm&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;decodedToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jwtDecode&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Realm&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&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;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;Invalid token:&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="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;roles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;decodedToken&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;realm_access&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;roles&lt;/span&gt; &lt;span class="o"&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;roleExists&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ifRoleExists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;roles&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;rolePermissions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getPermissions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;roleExists&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;pathname&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;pathname&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/partners&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;rolePermissions&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="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="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;add&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;view&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;edit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;deleteRow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;confirm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;include&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rolePermissions&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Permissions&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="nx"&gt;add&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;view&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;edit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;deleteRow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;confirm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;include&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;&lt;strong&gt;Usage Example&lt;/strong&gt;&lt;br&gt;
Here's how you can use the useRoleManagement hook in a React component:&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="nx"&gt;React&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;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useRoleManagement&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;./useRoleManagement&lt;/span&gt;&lt;span class="dl"&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;Dashboard&lt;/span&gt; &lt;span class="o"&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="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;add&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;view&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;edit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;deleteRow&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRoleManagement&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;view&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;You&lt;/span&gt; &lt;span class="nx"&gt;can&lt;/span&gt; &lt;span class="nx"&gt;view&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/p&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;}
&lt;/span&gt;      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;add&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Add&lt;/span&gt; &lt;span class="nx"&gt;Item&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/button&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;}
&lt;/span&gt;      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;edit&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Edit&lt;/span&gt; &lt;span class="nx"&gt;Item&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/button&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;}
&lt;/span&gt;      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;deleteRow&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Delete&lt;/span&gt; &lt;span class="nx"&gt;Item&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/button&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;}
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&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;default&lt;/span&gt; &lt;span class="nx"&gt;Dashboard&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;br&gt;
In this article, we've implemented a custom useRoleManagement hook in React to manage user permissions based on their roles. This approach ensures that your application remains secure and that users have the appropriate access based on their roles. Feel free to customize the permissions and roles according to your application's requirements.&lt;/p&gt;

&lt;p&gt;I hope you found this guide helpful. If you have any questions or suggestions, please leave a comment below!&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>programming</category>
      <category>react</category>
      <category>typescript</category>
    </item>
  </channel>
</rss>
