<?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: MAURIZIO ARGONETO</title>
    <description>The latest articles on DEV Community by MAURIZIO ARGONETO (@margoneto).</description>
    <link>https://dev.to/margoneto</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%2F3777379%2F2cd3d65a-bce3-4661-a120-23b2ba8078b2.jpg</url>
      <title>DEV Community: MAURIZIO ARGONETO</title>
      <link>https://dev.to/margoneto</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/margoneto"/>
    <language>en</language>
    <item>
      <title>CVE reachability with an LLM and a code graph</title>
      <dc:creator>MAURIZIO ARGONETO</dc:creator>
      <pubDate>Fri, 30 Oct 2026 13:10:12 +0000</pubDate>
      <link>https://dev.to/margoneto/cve-reachability-with-an-llm-and-a-code-graph-cl0</link>
      <guid>https://dev.to/margoneto/cve-reachability-with-an-llm-and-a-code-graph-cl0</guid>
      <description>&lt;p&gt;Asking a language model whether a CVE affects your code is easy. Deciding when not to believe the answer is the hard part.&lt;/p&gt;

&lt;p&gt;On one real project we started with a hundred critical CVEs and went through them by hand. Seven or eight out of ten could not be reached from our code. That ratio comes from one codebase, so treat it as an anecdote. Anyone who runs a scanner knows the effect anyway: when everything is red, nobody opens the dashboard, and each CVE costs a developer an afternoon of reading advisories and grepping code.&lt;/p&gt;

&lt;p&gt;vex-engine is the pipeline I built to give that afternoon back. It runs on GitLab CI, reads each CVE with an LLM on Amazon Bedrock, looks for it in the code with the graph that OWASP dep-scan builds, and commits a CycloneDX VEX verdict to the repository. Dependency-Track then shows only the CVEs that need a person.&lt;/p&gt;

&lt;p&gt;There are two ways to get this wrong. A false positive costs a developer some time. A false negative hides a real vulnerability behind a confident &lt;code&gt;not_affected&lt;/code&gt;, and preventing that is the reason for most of the design. So I follow one example through the pipeline, a Pillow CVE in an image decoder, and at every stage I ask the same question: &lt;strong&gt;could this stage hide a real vulnerability, and what stops it?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The code, the CI template and the lab app are on &lt;a href="https://gitlab.com/argomauro/vex-engine" rel="noopener noreferrer"&gt;GitLab&lt;/a&gt;. Every snippet below comes from that repository.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffrsg1e5lxjcsygp4t9wc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffrsg1e5lxjcsygp4t9wc.png" alt=" " width="799" height="121"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture in one paragraph
&lt;/h2&gt;

&lt;p&gt;On every push, Trivy reads &lt;code&gt;requirements.txt&lt;/code&gt; and writes a CycloneDX SBOM. Dependency-Track matches each component against NVD, GitHub and OSV and returns the CVEs. In parallel, OWASP dep-scan 6.3 reads the source and builds a code property graph with its atom engine. The analysis then takes one CVE at a time through three modules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Strategist&lt;/strong&gt; (LLM call): reads the advisory and writes a search plan.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Locator&lt;/strong&gt; (no model): queries the graph.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auditor&lt;/strong&gt; (second LLM call): judges the evidence.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The verdict goes into &lt;code&gt;vex.json&lt;/code&gt;. Exploitable CVEs open one GitLab issue per component, and people close issues with labels that flow back into the VEX.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffksinlj40r8guvvpq9ld.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffksinlj40r8guvvpq9ld.png" alt=" " width="800" height="313"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a regex scanner was not enough
&lt;/h2&gt;

&lt;p&gt;The first version also had three phases, but the middle one was a Scanner. The Strategist wrote search patterns, the Scanner ran them as regular expressions over the files that imported the library, and the Auditor received thirty lines above and below every match.&lt;/p&gt;

&lt;p&gt;Noise went down. Then it met this:&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;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;safe_load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;settings.yml&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;   &lt;span class="c1"&gt;# safe
&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                &lt;span class="c1"&gt;# vulnerable
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A text search finds both lines. The Auditor gets two blocks of code with no idea where &lt;code&gt;request.body&lt;/code&gt; comes from or who calls the surrounding function. It has to guess, and on a bad day it guesses &lt;code&gt;not_affected&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I replaced the Scanner with a Locator that queries a code graph. The Scanner stayed in the codebase with two smaller jobs, covered below. The division of labour stayed the same, and the rest of the post builds on it: the LLM reads the CVE at the start and judges facts at the end. The graph does the looking.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: turn the CVE text into a search plan with Bedrock
&lt;/h2&gt;

&lt;p&gt;A CVE is written for people. "Heap overflow in the TIFF decoder when processing crafted images" does not tell you which function in your code triggers it, and no database does either. NVD used to add structured data such as scores and affected configurations to every entry; in April NIST announced it would stop enriching every CVE. Reading an advisory and mapping it to code used to need someone with security experience, every time.&lt;/p&gt;

&lt;p&gt;The Strategist does that reading. It receives the CVE description and the package name and returns a plan as strict JSON. For CVE-2017-18342 (PyYAML before 5.1, &lt;code&gt;yaml.load()&lt;/code&gt; can execute arbitrary code on untrusted data) the plan looks like this, with illustrative values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"trigger_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"api_call"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"vulnerable_symbols"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"yaml.load"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"yaml.load_all"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"conditions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"the argument comes from untrusted input and no SafeLoader is passed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"safe_alternatives"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"yaml.safe_load"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"yaml.safe_load_all"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"import_aliases"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"yaml"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"attack_vector"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"a YAML document with a python/object tag executes code when loaded"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each field has a consumer downstream. The Locator uses &lt;code&gt;vulnerable_symbols&lt;/code&gt; and &lt;code&gt;import_aliases&lt;/code&gt; (Pillow is imported as &lt;code&gt;PIL&lt;/code&gt;, PyYAML as &lt;code&gt;yaml&lt;/code&gt;). The Auditor uses &lt;code&gt;conditions&lt;/code&gt;, &lt;code&gt;attack_vector&lt;/code&gt; and &lt;code&gt;safe_alternatives&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is the first place a real vulnerability can disappear. If the Strategist names the wrong function, the graph looks for the wrong thing. This part of the prompt guards against it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- vulnerable_symbols: ALL the PUBLIC functions/classes an application may call that
  lead to the vulnerable code, fully qualified from the TOP-LEVEL import name.
  If the bug is inside an internal decoder or C module (e.g. TiffDecode.c), name
  the public entry points that reach it (e.g. "Image.open" for image decoders).
  If it is in a class used internally by convenience functions, list those too
  (e.g. a bug in requests.Session redirects: "requests.get", "requests.post", ...).
  Missing an entry point hides a real vulnerability, so when unsure list more, not fewer.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the Pillow example the bug lives in a C decoder that no application calls directly. Applications call &lt;code&gt;Image.open&lt;/code&gt;, which reaches the decoder when it opens a TIFF. The prompt asks the Strategist to make that jump, from where the bug is to where your code touches it.&lt;/p&gt;

&lt;p&gt;The plan also says how the bug fires:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;api_call&lt;/code&gt;: the application must call a specific function.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;framework_implicit&lt;/code&gt;: the library acts on its own. A CVE in Django's multipart parser fires on every upload before your view runs, and your code never names the parser.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;config&lt;/code&gt;: a setting turns the bug on.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Strategist picks the type from the CVE text, and the type decides which question the Locator asks.&lt;/p&gt;

&lt;p&gt;Nothing uses the answer before validation. The JSON must parse, the trigger type must be one of the three values, and symbol names are normalised. If Bedrock is unreachable, a keyword heuristic produces a fallback plan and the pipeline marks it as heuristic, because a guessed plan must never clear a CVE. Plans are cached per run, so each CVE/package pair goes to the model once.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A wrong symbol list is the cheapest way to hide a vulnerability. Ask for more entry points, never fewer, and record where each plan came from.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 2: match symbols against the dep-scan code graph
&lt;/h2&gt;

&lt;p&gt;The Locator uses no model. It reads the graph that &lt;a href="https://github.com/owasp-dep-scan/dep-scan" rel="noopener noreferrer"&gt;OWASP dep-scan&lt;/a&gt; builds with atom, which combines three views of the same code. The syntax tree knows that &lt;code&gt;Image&lt;/code&gt; in your file is Pillow's module. The call graph knows who calls whom. The data flow knows where a value travels.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8cxrf3drxgqbvt2xp2dt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8cxrf3drxgqbvt2xp2dt.png" alt=" " width="800" height="368"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;dep-scan exports the graph as two JSON slices. &lt;code&gt;usages&lt;/code&gt; lists every call with its arguments and line. &lt;code&gt;reachables&lt;/code&gt; lists flows that start from external input, such as an HTTP request. The CI job pins the image, because the slice format changes between atom versions, and writes a marker file only when the slices are complete:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;vex_reachability&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;analyze&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io/owasp-dep-scan/dep-scan:v6.3.0&lt;/span&gt;
    &lt;span class="na"&gt;entrypoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;allow_failure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;if depscan -i "${VEX_APP_SRC}" -o "${CI_PROJECT_DIR}/depscan-out" -t python \&lt;/span&gt;
        &lt;span class="s"&gt;--reachability-analyzer SemanticReachability \&lt;/span&gt;
        &lt;span class="s"&gt;--explanation-mode EndpointsAndReachables \&lt;/span&gt;
        &lt;span class="s"&gt;--vdb-scope app --no-banner \&lt;/span&gt;
        &lt;span class="s"&gt;&amp;amp;&amp;amp; [ -s depscan-out/python-usages.slices.json ]; then&lt;/span&gt;
        &lt;span class="s"&gt;touch depscan-out/.complete&lt;/span&gt;
      &lt;span class="s"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;dep-scan exits 0 even when it produces no slices. Partial slices would report "library not used" for every file that failed to parse, and the analysis would close CVEs it never looked at. Without &lt;code&gt;.complete&lt;/code&gt;, the Locator treats the graph as missing.&lt;/p&gt;

&lt;p&gt;Matching is one small function. atom writes resolved calls such as &lt;code&gt;PIL.py:&amp;lt;module&amp;gt;.Image.open&lt;/code&gt;. The Locator splits them into segments and compares from the end:&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;symbol_matches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;qualified&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;symbol&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;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;sym&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&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;symbol&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;()&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;sym&lt;/span&gt; &lt;span class="ow"&gt;or&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;sym&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;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;qualified&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;qualified&lt;/span&gt;&lt;span class="p"&gt;[&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;sym&lt;/span&gt;&lt;span class="p"&gt;):]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;sym&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;PIL.Image.open&lt;/code&gt; matches &lt;code&gt;[pil, image, open]&lt;/code&gt;. &lt;code&gt;yaml.load&lt;/code&gt; matches &lt;code&gt;yaml.py:&amp;lt;module&amp;gt;.load&lt;/code&gt; and never &lt;code&gt;safe_load&lt;/code&gt;, because the last segment differs. That fixes the case that sank the Scanner.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Pin the dep-scan image and check that the slices exist. A graph that failed silently looks exactly like a codebase that never uses the library.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 3: route each CVE by what the graph finds
&lt;/h2&gt;

&lt;p&gt;The graph can give four answers, and each has a fixed next step.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fto0qgmr7ammrb7gvkztm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fto0qgmr7ammrb7gvkztm.png" alt=" " width="800" height="478"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The vulnerable function is called
&lt;/h3&gt;

&lt;p&gt;For every call site the Locator collects the file and line, the origin of each argument, the external-input flows that cross the enclosing function, every path from an entry point to the call, and the source of each function on those paths. All of it goes to the Auditor. This is the Pillow case: &lt;code&gt;Image.open&lt;/code&gt; is called at line 17 of &lt;code&gt;upload_avatar&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The library is used, but not through the vulnerable function
&lt;/h3&gt;

&lt;p&gt;This looks like good news, and it is the outcome most likely to hide a real problem, because the Strategist can still miss an entry point. Take a CVE in the redirect handling of &lt;code&gt;requests.Session&lt;/code&gt;. The Strategist names &lt;code&gt;requests.Session&lt;/code&gt;. The application only does this:&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;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_supplied_url&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;requests.get&lt;/code&gt; builds a &lt;code&gt;Session&lt;/code&gt; internally, so the vulnerable code runs. Stopping at "not called" would be wrong. Before trusting it, the Locator runs three checks, one for each way "not called" could be false:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;th&gt;Fails when&lt;/th&gt;
&lt;th&gt;Why it matters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;The symbols came from the LLM&lt;/td&gt;
&lt;td&gt;Bedrock was down and the keyword heuristic produced them&lt;/td&gt;
&lt;td&gt;a guessed list proves nothing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;They are real, callable APIs&lt;/td&gt;
&lt;td&gt;the plan names &lt;code&gt;TiffDecode.c&lt;/code&gt;, a private &lt;code&gt;_imaging&lt;/code&gt; module or a decoder plugin&lt;/td&gt;
&lt;td&gt;no application calls a C file, so "not called" is trivially true&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A text search agrees&lt;/td&gt;
&lt;td&gt;grep finds &lt;code&gt;ImageMath.eval(&lt;/code&gt; where the graph saw nothing&lt;/td&gt;
&lt;td&gt;the graph has a hole: an unparsed file, an alias, a &lt;code&gt;getattr&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Any failure sends the CVE to &lt;code&gt;in_triage&lt;/code&gt;. If all three pass, the Auditor receives every use of the library and one question: does any of these calls reach the vulnerable code internally?&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The bug fires without a call
&lt;/h3&gt;

&lt;p&gt;For &lt;code&gt;framework_implicit&lt;/code&gt; and &lt;code&gt;config&lt;/code&gt; there is no symbol to look for. If the library is imported, the Locator collects every place the application uses it and the Auditor decides whether the vulnerable feature is in use. On the lab app it sees &lt;code&gt;upload_avatar&lt;/code&gt; reading &lt;code&gt;request.FILES&lt;/code&gt;, so multipart uploads are in use.&lt;/p&gt;

&lt;p&gt;If the library is not imported anywhere, the CVE goes straight to &lt;code&gt;in_triage&lt;/code&gt;. A server, a plugin or a string in the settings can load a package without any import in your code.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. The graph is missing
&lt;/h3&gt;

&lt;p&gt;If dep-scan failed or the language is not Python, the old Scanner runs. If it finds uses, the Auditor judges. If it finds nothing, the CVE goes to &lt;code&gt;in_triage&lt;/code&gt;, because without the graph "not found in the text" proves nothing.&lt;/p&gt;

&lt;p&gt;The relationship between the graph and the regex comes down to one rule:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If the graph says "called", believe it. If it says "not called" or "not imported", the text has to agree. When they disagree, doubt wins.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 4: when the library is never imported, rebuild the tree from PyPI
&lt;/h2&gt;

&lt;p&gt;For an &lt;code&gt;api_call&lt;/code&gt; CVE, "never imported" sounds conclusive. It isn't, because the graph only sees your code. If Django uses sqlparse internally, a sqlparse CVE can be reached through Django, and the graph never looks inside Django.&lt;/p&gt;

&lt;p&gt;An SBOM generated from &lt;code&gt;requirements.txt&lt;/code&gt; has no dependency tree. The Locator rebuilds one from the &lt;code&gt;requires_dist&lt;/code&gt; metadata on PyPI, breadth-first from the SBOM's components. On the lab app that is 34 packages in about two seconds.&lt;/p&gt;

&lt;p&gt;PyPI cannot tell you whether Django calls the vulnerable function in sqlparse. It only tells you that Django declares a dependency on it. That is enough to know when a CVE must stay open:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Library&lt;/th&gt;
&lt;th&gt;PyPI tree&lt;/th&gt;
&lt;th&gt;Verdict&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;jinja2&lt;/td&gt;
&lt;td&gt;direct, nothing else requires it&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;not_affected&lt;/code&gt; if no text search finds an import&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sqlparse&lt;/td&gt;
&lt;td&gt;required by Django&lt;/td&gt;
&lt;td&gt;&lt;code&gt;in_triage&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;private package&lt;/td&gt;
&lt;td&gt;PyPI returns 404&lt;/td&gt;
&lt;td&gt;&lt;code&gt;in_triage&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The jinja2 row is the only case the Locator closes on its own, without calling the LLM. Before it does, a regex looks for any import of the package, including dynamic ones through &lt;code&gt;importlib&lt;/code&gt; or &lt;code&gt;__import__&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Optional extras count as dependents, but the walk does not traverse them. Following every extra makes every library transitive (Pillow → &lt;code&gt;docs&lt;/code&gt; extra → sphinx → jinja2) and nothing could ever be closed. The known trade-off: a package reached only through another package's extra, such as &lt;code&gt;requests[security]&lt;/code&gt; → pyOpenSSL → cryptography, can look direct.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The graph sees your code. PyPI sees what your dependencies declare. Neither one is allowed to clear a CVE on a guess.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 5: let the Auditor judge evidence
&lt;/h2&gt;

&lt;p&gt;The Auditor receives the Strategist's plan, the CVE description and a block of evidence built from the slices. The prompt tells it what kind of material it is reading:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;EVIDENCE FROM THE CODE GRAPH (OWASP dep-scan). For every call site of the
vulnerable API you get: the arguments' origin, the external-input flows that
cross the enclosing function, every call path from an entrypoint, and the code
of every function on those paths. These are facts from data-flow analysis, not
guesses: the vulnerable API IS called at these lines.

For each call site decide:
1. Can attacker-controlled data (HTTP request, uploaded file, message, CLI arg)
   reach the call through one of the paths? Follow the arguments' origin.
2. Are the trigger conditions met (arguments, options, file format, settings)?
3. Is there validation or a safe alternative that blocks the attack?
The CVE is "affected" if at least one call site is exploitable.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A rule engine can't answer these questions. Is there validation before the call? Is the argument a constant or an upload? Does the file format match the CVE? Is &lt;code&gt;yaml.load&lt;/code&gt; called with a &lt;code&gt;SafeLoader&lt;/code&gt;? A person answers in seconds when looking at the right snippet. The graph's job is to put that snippet, and the path that leads to it, in front of the model.&lt;/p&gt;

&lt;p&gt;The evidence is capped so prompts stay small: at most 8 call sites per CVE, 5 paths per call site, call depth 8, and 80 lines per function. That is enough to follow a request from a Django view to a library call in a typical web app.&lt;/p&gt;

&lt;p&gt;The Auditor answers with strict JSON: a verdict that is exactly &lt;code&gt;affected&lt;/code&gt; or &lt;code&gt;not_affected&lt;/code&gt;, a confidence between 0 and 1, a reasoning that cites the path, and the call sites it considers exploitable.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The graph decides what the model sees. The model decides what it means.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  What the Auditor cannot verify
&lt;/h3&gt;

&lt;p&gt;In case 2 above (library used, vulnerable function not called) the Auditor is asked whether &lt;code&gt;requests.get&lt;/code&gt; reaches &lt;code&gt;Session&lt;/code&gt; internally. Nothing in the evidence answers that. The engine passes no library source, no documentation and no web search, so the model answers from training data.&lt;/p&gt;

&lt;p&gt;For widely used libraries such as requests, Pillow, PyYAML and Django, that is usually right: the mapping between public functions and internal classes is stable and well documented. For a niche package, or a release newer than the model's training cutoff, it can be wrong. A smaller model knows less than a larger one.&lt;/p&gt;

&lt;p&gt;Two things contain this. The confidence threshold sends an unsure answer to &lt;code&gt;in_triage&lt;/code&gt;, which is what usually happens with a library the model barely knows. And every reasoning is written into the VEX and committed, where a reviewer can read and challenge it. A model that is confidently wrong passes the threshold; only the review catches it.&lt;/p&gt;

&lt;p&gt;The next step is to replace memory with facts: have atom also build a graph of the installed library and show the Auditor the real path from &lt;code&gt;requests.get&lt;/code&gt; to &lt;code&gt;Session&lt;/code&gt;. vex-engine does not do this yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: fail closed
&lt;/h2&gt;

&lt;p&gt;The final rule does not depend on the model:&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;# Fail-closed: None (doubt, error, low confidence) -&amp;gt; in_triage, stays visible
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;is_reachable&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;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;in_triage&lt;/span&gt;&lt;span class="sh"&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;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exploitable&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;is_reachable&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;not_affected&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A verdict with confidence of 0.8 or more (&lt;code&gt;VEX_MIN_CONFIDENCE&lt;/code&gt;) becomes &lt;code&gt;exploitable&lt;/code&gt; or &lt;code&gt;not_affected&lt;/code&gt;. Everything else becomes &lt;code&gt;in_triage&lt;/code&gt; and stays on the dashboard for a person: a malformed answer, a lower confidence, a timeout, a missing package URL from Dependency-Track.&lt;/p&gt;

&lt;p&gt;One failure mode only shows up over time. An LLM is not deterministic, and on an ambiguous CVE it can flip between &lt;code&gt;not_affected&lt;/code&gt; and &lt;code&gt;exploitable&lt;/code&gt; on every run, closing and reopening the same issue. The bot counts its own flips. After two (&lt;code&gt;VEX_MAX_AUTO_FLIPS&lt;/code&gt;) it stops touching the issue, leaves it open and labels it &lt;code&gt;vex::needs-human&lt;/code&gt;. A human decision resets the counter.&lt;/p&gt;

&lt;p&gt;This rule is what makes a smaller model safe to use. I ran the whole pipeline with a 12B open model through Ollama on a laptop. It produced more &lt;code&gt;in_triage&lt;/code&gt; verdicts than the Bedrock model and no hidden errors.&lt;/p&gt;

&lt;p&gt;Worst case, vex-engine leaves you with the dashboard you already had.&lt;/p&gt;

&lt;h2&gt;
  
  
  One CVE, end to end
&lt;/h2&gt;

&lt;p&gt;Back to the Pillow decoder CVE on the lab app (old Pillow, PyYAML and Django, with insecure code written on purpose).&lt;/p&gt;

&lt;p&gt;The Strategist returns &lt;code&gt;trigger_type: api_call&lt;/code&gt; with &lt;code&gt;PIL.Image.open&lt;/code&gt; among the symbols, because that is the public function that reaches the decoder. The plan came from the model, and the symbol is a real API.&lt;/p&gt;

&lt;p&gt;The Locator finds &lt;code&gt;Image.open&lt;/code&gt; in the graph and builds the evidence. Simplified:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Call site 1: Image.open @ vuln_demo.py:17 (in upload_avatar)
Vulnerable symbol: PIL.Image.open
Arguments: img_file &amp;lt;- request.FILES['avatar']
Call paths (entrypoint -&amp;gt; call):
  &amp;lt;module&amp;gt; --(line 56)--&amp;gt; upload_avatar --(line 17)--&amp;gt; Image.open
Code of the functions involved:
  def upload_avatar(request):
      ...
      img_file = request.FILES['avatar']
      with Image.open(img_file) as img:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Auditor does not have to find the call or trace the data. It only decides whether an attacker controls that file: it is a user upload, and nothing validates it before the decoder runs. Verdict &lt;code&gt;affected&lt;/code&gt;, confidence above the threshold, call site &lt;code&gt;vuln_demo.py:17&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The result is a VEX entry marked &lt;code&gt;exploitable&lt;/code&gt; and a GitLab issue for the Pillow component with the file, the line, the origin of the argument and the path from the entry point. The developer who opens it can go straight to the fix.&lt;/p&gt;

&lt;p&gt;A full run on the lab app looks like this. The app is vulnerable by design, so the exploitable share is far higher than in a real codebase:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkq9nlex3aqdk79l3d538.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkq9nlex3aqdk79l3d538.png" alt=" " width="800" height="240"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Of 80 CVEs, 40 are closed with a written reason, 6 stay visible for a person, and 34 exploitable ones are grouped into two issues, one for Pillow and one for Django.&lt;/p&gt;

&lt;h2&gt;
  
  
  Write verdicts to vex.json and let people close the loop
&lt;/h2&gt;

&lt;p&gt;Every verdict is written to &lt;code&gt;vex.json&lt;/code&gt; in the application's repository, using standard CycloneDX fields, and committed with &lt;code&gt;[skip ci]&lt;/code&gt; only when something changed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CVE-XXXX-YYYY"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"analysis"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"state"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"not_affected"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"justification"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"code_not_reachable"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"response"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"will_not_fix"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"detail"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Vulnerability CVE-XXXX-YYYY analyzed by LLM Analyzer (python) on 2026-10-20. &amp;lt;Auditor reasoning&amp;gt;"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  A Dependency-Track trap
&lt;/h3&gt;

&lt;p&gt;In the uploaded VEX, &lt;code&gt;affects[].ref&lt;/code&gt; must stay the Dependency-Track project UUID (the BOM's &lt;code&gt;metadata.component.bom-ref&lt;/code&gt;). If you rewrite it with the component's purl, Dependency-Track returns HTTP 200 and silently discards the verdict. The only trace is &lt;code&gt;Unable to locate affected element&lt;/code&gt; in its logs. vex-engine keeps the purl in a separate property, &lt;code&gt;vex:component-purl&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Closing labels
&lt;/h3&gt;

&lt;p&gt;Issues open only for exploitable CVEs, one per component. People close them with a label that says why, and the pipeline turns the label into a VEX decision:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq00ljx5rv2a9lonm39ms.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq00ljx5rv2a9lonm39ms.png" alt=" " width="800" height="708"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Label&lt;/th&gt;
&lt;th&gt;VEX written&lt;/th&gt;
&lt;th&gt;Expiry&lt;/th&gt;
&lt;th&gt;What the bot does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;vex::fixed&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;nothing forced, it's a claim&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;next analysis checks the graph; still reachable → issue reopens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;vex::false-positive&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false_positive&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;90 days&lt;/td&gt;
&lt;td&gt;skipped until expiry, then back to &lt;code&gt;in_triage&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;vex::mitigated&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;not_affected&lt;/code&gt; + &lt;code&gt;protected_by_mitigating_control&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;90 days&lt;/td&gt;
&lt;td&gt;same&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;vex::risk-accepted&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;exploitable&lt;/code&gt; + &lt;code&gt;will_not_fix&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;90 days&lt;/td&gt;
&lt;td&gt;stays visibly exploitable&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If &lt;code&gt;VEX_APPROVERS&lt;/code&gt; is set and someone else closes the issue, the bot reopens it. Two &lt;code&gt;vex::&lt;/code&gt; labels at once count as a conflict. A person reopening an issue revokes the decision. When a library is upgraded and its CVEs leave the SBOM, the issue closes by itself.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A decision that never expires becomes a blind spot. Ninety days is a setting. The expiry itself is the point.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Keep the analysis current with three triggers
&lt;/h2&gt;

&lt;p&gt;A pipeline that runs only on push misses new CVEs on code nobody touches, and misses the decisions people make on issues. vex-engine runs on three triggers:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fff80y2ujg63dd65zqc0w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fff80y2ujg63dd65zqc0w.png" alt=" " width="799" height="462"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Push or manual run.&lt;/strong&gt; The full chain, from SBOM to verdict. On push only CRITICAL and HIGH CVEs go to the model (&lt;code&gt;VEX_MODE=critical&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Issue webhook.&lt;/strong&gt; Closing or reopening a security issue, or adding a &lt;code&gt;vex::&lt;/code&gt; label to a closed one, runs &lt;code&gt;vex_issue_sync --issues-only&lt;/code&gt;: no scan, no LLM, Dependency-Track updated in about a minute.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Schedules.&lt;/strong&gt; A nightly run syncs issues and expires decisions with no LLM. A weekly full analysis (&lt;code&gt;VEX_MODE=full&lt;/code&gt;) catches new CVEs on unchanged code.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two jobs write &lt;code&gt;vex.json&lt;/code&gt;, so they share a GitLab resource group and run one at a time. Set its process mode to &lt;code&gt;oldest_first&lt;/code&gt; to keep them in order. A project enables everything by including one template:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;project&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;demo-security/vex-engine'&lt;/span&gt;
    &lt;span class="na"&gt;file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ci-template.yml'&lt;/span&gt;
    &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;

&lt;span class="na"&gt;stages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;scan&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;upload-bom&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;analyze&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;sync-vex&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What it costs on Amazon Bedrock
&lt;/h2&gt;

&lt;p&gt;The LLM is called in two places, the Strategist and the Auditor, and only during an analysis. The SBOM, Dependency-Track, the graph, the webhook and the nightly sync make no model calls. The Strategist runs once per CVE in scope; the Auditor runs only for CVEs the graph does not settle on its own.&lt;/p&gt;

&lt;p&gt;The default model is Amazon Nova 2 Lite. The task is narrow and structured (read text, return JSON), so a small model is enough. Switching model is one variable, and the same prompts run locally through Ollama, in which case no code leaves the machine:&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="nv"&gt;LLM_PROVIDER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;bedrock                         &lt;span class="c"&gt;# or: ollama&lt;/span&gt;
&lt;span class="nv"&gt;BEDROCK_MODEL_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;eu.amazon.nova-2-lite-v1:0  &lt;span class="c"&gt;# default&lt;/span&gt;
&lt;span class="nv"&gt;OLLAMA_MODEL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;gemma4:12b                      &lt;span class="c"&gt;# local option&lt;/span&gt;
&lt;span class="nv"&gt;VEX_MIN_CONFIDENCE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0.8
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A run costs cents on Bedrock and nothing locally. The real infrastructure cost is the graph: atom runs on the JVM and the dep-scan image is about 3.8 GB, so cache it on your runners.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the analysis can't see
&lt;/h2&gt;

&lt;p&gt;Each of these ends in &lt;code&gt;in_triage&lt;/code&gt;, never in &lt;code&gt;not_affected&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Malware without a CVE.&lt;/strong&gt; Out of scope.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bugs deep inside a C extension.&lt;/strong&gt; The graph only sees the Python that calls into it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Argument-level data flow.&lt;/strong&gt; Python data flow is tracked at function level: the graph knows that external input crosses a function, not which argument carries it. That's why the final judgement stays with the Auditor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Private packages.&lt;/strong&gt; PyPI doesn't know them, so they can't be placed in the dependency tree.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Checklist
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Configuration&lt;/th&gt;
&lt;th&gt;What it guarantees&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Strategist prompt&lt;/td&gt;
&lt;td&gt;all public entry points, more when unsure&lt;/td&gt;
&lt;td&gt;the graph looks for every way in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fallback plans&lt;/td&gt;
&lt;td&gt;marked as heuristic&lt;/td&gt;
&lt;td&gt;a guessed plan never clears a CVE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dep-scan job&lt;/td&gt;
&lt;td&gt;pinned image, &lt;code&gt;.complete&lt;/code&gt; marker&lt;/td&gt;
&lt;td&gt;a failed graph is treated as missing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Symbol matching&lt;/td&gt;
&lt;td&gt;segment suffix match&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;yaml.load&lt;/code&gt; never matches &lt;code&gt;safe_load&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Not called" checks&lt;/td&gt;
&lt;td&gt;LLM origin, real API, regex agreement&lt;/td&gt;
&lt;td&gt;a hole in the graph ends in triage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PyPI tree&lt;/td&gt;
&lt;td&gt;direct leaf only, extras not traversed&lt;/td&gt;
&lt;td&gt;transitive libraries stay visible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auditor evidence&lt;/td&gt;
&lt;td&gt;8 sites, 5 paths, depth 8, 80 lines&lt;/td&gt;
&lt;td&gt;small prompts with the right snippet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Verdict rule&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;VEX_MIN_CONFIDENCE=0.8&lt;/code&gt;, fail closed&lt;/td&gt;
&lt;td&gt;doubt never hides a CVE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anti-oscillation&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;VEX_MAX_AUTO_FLIPS=2&lt;/code&gt;, &lt;code&gt;vex::needs-human&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;an unstable verdict goes to a person&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VEX upload&lt;/td&gt;
&lt;td&gt;keep &lt;code&gt;affects[].ref&lt;/code&gt; as the project UUID&lt;/td&gt;
&lt;td&gt;Dependency-Track keeps the verdicts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Labels&lt;/td&gt;
&lt;td&gt;approvers, 90-day expiry&lt;/td&gt;
&lt;td&gt;decisions are attributed and revisited&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Writers&lt;/td&gt;
&lt;td&gt;shared resource group&lt;/td&gt;
&lt;td&gt;concurrent jobs don't drop decisions&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Everything here ran with OWASP dep-scan 6.3.0, Dependency-Track 4.13 and Amazon Nova 2 Lite on Bedrock, on Python projects. The slice format changes between atom versions, so expect the matching code to move.&lt;/p&gt;

&lt;p&gt;To run it on your own project: clone &lt;a href="https://gitlab.com/argomauro/vex-engine" rel="noopener noreferrer"&gt;vex-engine&lt;/a&gt;, point &lt;code&gt;VEX_APP_SRC&lt;/code&gt; at your source, add the &lt;code&gt;include:&lt;/code&gt; block above to your &lt;code&gt;.gitlab-ci.yml&lt;/code&gt;, and start with &lt;code&gt;LLM_PROVIDER=ollama&lt;/code&gt; if you'd rather keep code on your machine for the first run. The lab app in the repo is the fastest way to see all four routing cases.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>cve</category>
      <category>cybersecurity</category>
      <category>ai</category>
    </item>
    <item>
      <title>The rule that killed "Just Start Coding" at our Hackathon</title>
      <dc:creator>MAURIZIO ARGONETO</dc:creator>
      <pubDate>Fri, 18 Sep 2026 10:00:53 +0000</pubDate>
      <link>https://dev.to/margoneto/the-rule-that-killed-just-start-coding-at-our-hackathon-jeb</link>
      <guid>https://dev.to/margoneto/the-rule-that-killed-just-start-coding-at-our-hackathon-jeb</guid>
      <description>&lt;p&gt;GDG Basilicata and the IEEE Student Branch at the University of Basilicata, together with the university's Department of Sciences, booked a room built for fifty to eighty students, split them into teams of three or four, and gave them eight hours to build something with Google's Gemini models. Nobody installed a GPU driver. Nobody touched a local model weight. Every team redeemed a free API key from Google AI Studio and worked entirely in the browser.&lt;/p&gt;

&lt;p&gt;That's the first pillar of the format we built, and it's the boring one: cloud-first, zero local setup. It matters because the fastest way to kill momentum in a student hackathon is a broken CUDA install at hour two. Remove the infrastructure problem and you get eight real hours of thinking and building instead of six hours of pip errors.&lt;/p&gt;

&lt;p&gt;The interesting part is what we did with the time that freed up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Writing code fast isn't the hard part anymore
&lt;/h2&gt;

&lt;p&gt;Any team in that room could ask an agentic coding tool for a working prototype in twenty minutes. That capability is common now, which is exactly why we didn't grade on it. Our scoring grid put 35 of 100 points on a file most hackathons never ask for: SPEC.md.&lt;/p&gt;

&lt;p&gt;Before touching an IDE, each team spent an hour and a half writing down what they were building, for whom, and how they'd know it worked — functional requirements, architecture, expected output, non-functional constraints. A mentor had to sign off before the team could move to code. We called this phase Spec-Driven Development, and it changed the room's energy. Teams argued about scope before they argued about syntax. Some rewrote their spec twice before a mentor approved it.&lt;/p&gt;

&lt;p&gt;Only after that did agentic coding tools come into play — Google Antigravity, Cursor, VS Code with Gemini Code Assist. The agent reads SPEC.md, scaffolds the app, and helps wire up the Gemini API calls. The team's job shifts from typing code to checking what the agent produced against the spec they wrote an hour earlier. That's a different skill than typing fast, and it's the one we wanted to measure.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pitch is engineered too
&lt;/h2&gt;

&lt;p&gt;In the last hour before submission lock, teams didn't write slides from a blank deck. They uploaded SPEC.md, their code, and their documentation into NotebookLM and used it to generate a three-minute pitch script, an FAQ for the Q&amp;amp;A round, and, for teams who wanted it, an audio overview they could play as part of the demo. Thirty of the hundred points went here: how clearly a team could explain what they built and defend it in two minutes of jury questions.&lt;/p&gt;

&lt;p&gt;Four tracks were open on the day: study tools for university life, services for local government and territory, autonomous agents, and multimodal apps mixing text, image, audio or code. Output had to land as a web app, a dashboard, or an AI agent, nothing more exotic than that, on purpose — a constraint that stops a team from disappearing into scope an eight-hour sprint can't survive.&lt;/p&gt;

&lt;h2&gt;
  
  
  The schedule, hour by hour
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;th&gt;Phase&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;07:30–08:30&lt;/td&gt;
&lt;td&gt;Room setup, projector and Wi-Fi checks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;08:30–09:15&lt;/td&gt;
&lt;td&gt;Check-in and team matchmaking&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;09:15–09:45&lt;/td&gt;
&lt;td&gt;Keynote: stack, tracks, rubric&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;09:45–11:00&lt;/td&gt;
&lt;td&gt;Spec Definition — SPEC.md, mentor sign-off&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11:00–11:15&lt;/td&gt;
&lt;td&gt;Coffee break&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11:15–13:00&lt;/td&gt;
&lt;td&gt;Agentic Coding Sprint&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;13:00–14:00&lt;/td&gt;
&lt;td&gt;Lunch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;14:00–16:00&lt;/td&gt;
&lt;td&gt;Integration &amp;amp; polish, Gemini API wiring&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16:00–17:00&lt;/td&gt;
&lt;td&gt;Pitch prep with NotebookLM, submission upload&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;17:00&lt;/td&gt;
&lt;td&gt;Submission lock&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;17:00–17:30&lt;/td&gt;
&lt;td&gt;Jury's technical filter, pitch order set&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;17:30–18:45&lt;/td&gt;
&lt;td&gt;Live pitch showcase: 3-minute pitch, 2-minute Q&amp;amp;A per team&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;18:45–19:15&lt;/td&gt;
&lt;td&gt;Jury deliberation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;19:15–19:45&lt;/td&gt;
&lt;td&gt;Awards&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;19:45–20:30&lt;/td&gt;
&lt;td&gt;Teardown&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The deadline is not a suggestion. At minus sixty, minus thirty and minus ten minutes, someone walks the room and calls it out loud. Write access to the submission folder closes automatically at 17:00, no five-minute grace period — the first team you grant one to will tell the next twenty it's negotiable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five roles, none of them optional
&lt;/h2&gt;

&lt;p&gt;Running this for a room that size, with a jury on top, needs more than a tech lead. We split it five ways.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;Owns&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Event Lead&lt;/td&gt;
&lt;td&gt;Overall coordination, university relations, MC on the day&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Logistics &amp;amp; Venue Lead&lt;/td&gt;
&lt;td&gt;Room booking, Wi-Fi, power strips, catering&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tech &amp;amp; Mentor Lead&lt;/td&gt;
&lt;td&gt;API keys, Google AI Studio support, Spec-Driven Dev guidance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Marketing &amp;amp; Community Lead&lt;/td&gt;
&lt;td&gt;Graphics, social channels, photo and video&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Platform &amp;amp; Judging Lead&lt;/td&gt;
&lt;td&gt;Submission platform, CSV exports, the judges' scoring sheet&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Mentors and judges are different people doing different jobs. Mentors unblock teams on specs, API access, or NotebookLM extraction; they don't score anything. Judges score the spec, the working demo, and the pitch, and nothing else.&lt;/p&gt;

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

&lt;p&gt;University Wi-Fi struggled under dozens of simultaneous connections, so we asked IT for a dedicated SSID ahead of time and kept a couple of 4G hotspots on hand as backup. A handful of students got stuck redeeming their AI Studio API key; mentors carried spare keys to unblock them on the spot instead of debugging Google's account flow live in front of a team on the clock. Teams with no prior exposure to writing specs stalled in the first phase, which is why the quickstart doc ships a pre-filled template instead of a blank file:&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;# SPEC.md&lt;/span&gt;

&lt;span class="gu"&gt;## Obiettivo&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;One&lt;/span&gt; &lt;span class="na"&gt;sentence:&lt;/span&gt; &lt;span class="na"&gt;what&lt;/span&gt; &lt;span class="na"&gt;problem&lt;/span&gt; &lt;span class="na"&gt;does&lt;/span&gt; &lt;span class="na"&gt;this&lt;/span&gt; &lt;span class="na"&gt;solve&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="na"&gt;for&lt;/span&gt; &lt;span class="na"&gt;whom&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="gu"&gt;## Requisiti Funzionali&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; ...

&lt;span class="gu"&gt;## Requisiti Non Funzionali&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; ...

&lt;span class="gu"&gt;## Output Atteso&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;Web&lt;/span&gt; &lt;span class="na"&gt;app&lt;/span&gt; &lt;span class="err"&gt;/&lt;/span&gt; &lt;span class="na"&gt;Dashboard&lt;/span&gt; &lt;span class="err"&gt;/&lt;/span&gt; &lt;span class="na"&gt;Agent&lt;/span&gt; &lt;span class="err"&gt;—&lt;/span&gt; &lt;span class="na"&gt;what&lt;/span&gt; &lt;span class="na"&gt;does&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;done&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt; &lt;span class="na"&gt;look&lt;/span&gt; &lt;span class="na"&gt;like&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Handing teams a template doesn't lower the bar — a mentor still has to approve it — it just gets them past the blank-page freeze in the first ten minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why another chapter could run this tomorrow
&lt;/h2&gt;

&lt;p&gt;None of this needs a compute budget. The API key is free, the IDEs are free or already on student laptops, and NotebookLM runs in a browser tab. What it needs is a room, five people willing to own the roles above, three or four mentors, and a rubric that rewards a good spec and a good pitch as much as a working demo. The competition template we cloned on Kaggle — visibility set to Restricted, rubric swapped in, CSV export at lock time — is reusable as-is by any group running a Google-model hackathon.&lt;/p&gt;

&lt;p&gt;The retrospective after this run flagged the usual fixes for next time: earlier mentor briefings, a longer matchmaking window, clearer wording on what counts as "multimodal." None of that touches the four pillars. Cloud-first, spec before code, an agent that reads the spec instead of guessing, and a pitch built from artifacts the team already produced — that combination is what let a computer science student and a humanities student sit at the same table and ship the same prototype.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>hackathon</category>
      <category>google</category>
      <category>community</category>
    </item>
    <item>
      <title>Building a smart greenhouse in the woods: the architectural experiment of DevFest Basilicata - DevF{or}est</title>
      <dc:creator>MAURIZIO ARGONETO</dc:creator>
      <pubDate>Wed, 12 Aug 2026 15:38:40 +0000</pubDate>
      <link>https://dev.to/margoneto/building-a-smart-greenhouse-in-the-woods-the-architectural-experiment-of-devfest-basilicata--15j</link>
      <guid>https://dev.to/margoneto/building-a-smart-greenhouse-in-the-woods-the-architectural-experiment-of-devfest-basilicata--15j</guid>
      <description>&lt;p&gt;_By Maurizio Argoneto and Francesco di Donato &lt;a class="mentioned-user" href="https://dev.to/francesco_didonato_d1f65"&gt;@francesco_didonato_d1f65&lt;/a&gt; _&lt;/p&gt;

&lt;p&gt;How do you organize a tech conference that isn't just the usual sequence of slides in a dark hotel room? GDG Basilicata answered that question by bringing 66 developers to &lt;em&gt;La Majonica&lt;/em&gt;, a forest in the municipality of Tito (PZ, Italy), for the third edition of DevFest — rebranded for the day as &lt;strong&gt;"DevForest"&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;But the real innovation wasn't just the location. The event broke the classic format by introducing a single, fascinating constraint: &lt;strong&gt;technical interdependence&lt;/strong&gt;. Instead of isolated workshops, the entire day had one global goal — build a working &lt;strong&gt;smart greenhouse&lt;/strong&gt;, live, piece by piece, across different teams.&lt;/p&gt;

&lt;p&gt;Here's how we merged Botany, IoT, 3D Printing, Cloud computing, and Generative AI into a single, functioning pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  🤝 Talks as "integration contracts"
&lt;/h2&gt;

&lt;p&gt;The morning was dedicated to five 30-minute talks. These weren't just theoretical presentations — they were actual technical &lt;em&gt;contracts&lt;/em&gt;: each speaker handed over the exact specifications (JSON formats, endpoints, physical tolerances) that their group had to respect during the afternoon Build Sprint, so the whole system could talk to itself.&lt;/p&gt;

&lt;p&gt;The 5 pillars of our ecosystem:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Botany&lt;/strong&gt; (Francesco Albanese) — Biology before code. He defined the plant's vital thresholds (soil moisture, air temperature), which became the conditional logic of our software, and made the case for why IoT actually matters for energy and water conservation.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Electronics / IoT&lt;/strong&gt; (Soumaya Erradi) — The "nervous system". Using an &lt;code&gt;ESP32&lt;/code&gt; microcontroller, a &lt;code&gt;DHT11&lt;/code&gt; sensor, and a capacitive soil sensor, the team wrote the C++ firmware to read environmental data and drive a water pump relay.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;3D Fabrication&lt;/strong&gt; (Luigi Tuccillo) — The "armor". Bare hardware doesn't survive in a greenhouse. Using Tinkercad and AI-assisted modeling, the team designed and printed a PETG case (heat- and moisture-resistant) with tolerances tight enough to properly house the ESP32.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cloud &amp;amp; Data&lt;/strong&gt; (Gregorio Palamà) — The "brain". A serverless Cloud Function on Firebase evaluated incoming data and made irrigation decisions by querying Gemini.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;App &amp;amp; Generative AI&lt;/strong&gt; (Mike Trizio) — The "voice". A dashboard built in &lt;strong&gt;Angular&lt;/strong&gt; to show real-time data and let users ask, by voice, &lt;em&gt;"How is the plant doing?"&lt;/em&gt; — and get an empathetic answer read back via text-to-speech.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  🏗️ The real architecture (spoiler: not everything went 100% smoothly)
&lt;/h2&gt;

&lt;p&gt;For the Dev.to crowd, here's the juicy part. IoT always looks easy in online tutorials — things change when you're in a forest with limited connectivity.&lt;/p&gt;

&lt;p&gt;We ruled out a &lt;em&gt;push&lt;/em&gt; model from the Cloud to the ESP32: the device wasn't exposed to the internet, and there was no way to configure NAT/firewall rules out in the woods. So the architecture we actually shipped in the afternoon was built around &lt;strong&gt;polling&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The final stack:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ESP32 → Spring Boot API Gateway (Cloud Run) → External Firestore → Angular App
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The system handled 4 parallel flows:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Telemetry&lt;/strong&gt; — The ESP32 sent POST requests to the Java gateway, which performed an &lt;em&gt;upsert&lt;/em&gt; of the telemetry data (humidity, temperature) into Firestore.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Manual command&lt;/strong&gt; — The user clicked a button on the Angular app, sending a command to the Gateway.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Chat with Gemini&lt;/strong&gt; — The Angular app built a dynamic prompt by injecting live sensor data and sent it to Gemini to generate a conversational response about the plant's health.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Scheduled automation&lt;/strong&gt; — A Firebase Node.js (TypeScript) Function ran every hour. It applied a pre-filter (e.g. soil moisture &lt;code&gt;&amp;lt; 40%&lt;/code&gt;), passed 48 hours of historical data to Gemini in a batch, and forced the output to match a structured &lt;strong&gt;Zod&lt;/strong&gt; schema (&lt;code&gt;shouldWater&lt;/code&gt;, &lt;code&gt;durationSeconds&lt;/code&gt;, &lt;code&gt;reason&lt;/code&gt;). If the answer was positive, it queued the command on the Gateway.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The polling trick and the in-memory queue
&lt;/h3&gt;

&lt;p&gt;To get the irrigation command to the physical pump, the ESP32 sent a &lt;code&gt;GET /adjustments&lt;/code&gt; request to the Spring Boot Gateway at regular intervals. The Gateway kept an in-memory command queue (a &lt;code&gt;ConcurrentHashMap&lt;/code&gt; of &lt;code&gt;ConcurrentLinkedQueue&lt;/code&gt;). Once read, the command was removed — &lt;em&gt;exactly-once delivery&lt;/em&gt; — and executed by the pump.&lt;/p&gt;

&lt;h2&gt;
  
  
  🔍 Retrospective: what worked and what "exploded"
&lt;/h2&gt;

&lt;p&gt;The best part of building things live is the evening retrospective — held, literally, around a campfire.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The big wins:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Separation of concerns.&lt;/strong&gt; Because the JSON contracts were nailed down in the morning, teams could work in parallel. If the Cloud team got stuck, the IoT team just kept testing against mocked data.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The dual use of Gemini.&lt;/strong&gt; Using AI for two completely different jobs — a creative, natural-language response on the frontend, and an algorithmic decision constrained by a Zod schema on the backend — turned out to be a winning combination.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Technical debt (don't repeat this in prod):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The in-memory queue.&lt;/strong&gt; A RAM &lt;code&gt;HashMap&lt;/code&gt; for the command queue is perfect for a hackathon and disastrous for scalability. If the Cloud Run instance restarted (or scaled out), commands were simply lost. For v2, the plan is to move the queue to a Firestore collection (&lt;code&gt;pendingCommands&lt;/code&gt;) or to Cloud Tasks / Pub/Sub.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Polling latency.&lt;/strong&gt; Being a pull system, there was a noticeable delay between the AI's decision and the actual pump activation, dictated by the ESP32's polling cycle.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Tricky IAM permissions.&lt;/strong&gt; Firestore lived in a different Google Cloud project than Cloud Run. Without explicitly granting the &lt;code&gt;roles/datastore.user&lt;/code&gt; role to the service account, the system failed silently with &lt;code&gt;PERMISSION_DENIED&lt;/code&gt; errors.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  📹 Event coverage &amp;amp; video references
&lt;/h2&gt;

&lt;p&gt;The DevForest experiment caught the attention of both local media and the tech community. Worth a look if you want to see the greenhouse — and the woods — in action: &lt;a href="https://devfest.gdgbasilicata.it/" rel="noopener noreferrer"&gt;Web site&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  🎯 Conclusion
&lt;/h2&gt;

&lt;p&gt;As Maurizio Argoneto put it:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"The day was focused on nature and technology — we had the opportunity to experience, through talks and labs, what it means not just to design, but to practically build something real. We did this with 3D-printed elements, sensors capable of reading a plant's vital state, all the way to transforming this data in the cloud and designing an app for monitoring and interaction."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The success of DevFest Basilicata didn't hinge on which libraries were used — it hinged on forcing different teams to depend on each other in an environment with real physical and network constraints. It was the physical embodiment of what it actually means to build and maintain distributed software.&lt;/p&gt;

&lt;p&gt;If you want to replicate the project, the entire Spring Boot gateway code is open source, and you can test it with the Firebase CLI and a simple ESP32.&lt;/p&gt;

&lt;p&gt;See you at the next DevFest! (Hopefully with a persistent message queue 🚀)&lt;br&gt;
&lt;/p&gt;

</description>
      <category>ai</category>
      <category>devfest</category>
      <category>iot</category>
      <category>cloud</category>
    </item>
    <item>
      <title>From Code Smells to Strategy Spells - Directing AI with Human Judgment</title>
      <dc:creator>MAURIZIO ARGONETO</dc:creator>
      <pubDate>Mon, 13 Apr 2026 14:49:30 +0000</pubDate>
      <link>https://dev.to/margoneto/from-code-smells-to-strategy-spells-directing-ai-with-human-judgment-e5c</link>
      <guid>https://dev.to/margoneto/from-code-smells-to-strategy-spells-directing-ai-with-human-judgment-e5c</guid>
      <description>&lt;p&gt;In 2026, the question is no longer whether AI can code. It already does. Your new "Junior AI" coworker is tireless, never complains about long meetings, and can write 5,000 lines of code in a second. It has read every architecture book and knows every design pattern by heart, from Strategy to Repository.&lt;/p&gt;

&lt;p&gt;But there is a trap. AI knows how to &lt;strong&gt;apply&lt;/strong&gt; a pattern, but it doesn't know &lt;strong&gt;when&lt;/strong&gt; — and more importantly, &lt;strong&gt;when not&lt;/strong&gt; — to apply it. Without human judgment, AI suffers from "algorithmic over-zealousness." It will build a rocket just to go to the supermarket, leaving you with a 300-line "elegant" disaster for a problem that only needed 50 lines of simple code.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;AI knows how to apply a pattern, but it doesn't know when — or when not — to apply it.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The $440 Million Warning: Knight Capital
&lt;/h2&gt;

&lt;p&gt;Technical debt is often dismissed as an aesthetic problem, but history tells a different story. On August 1, 2012, Knight Capital lost &lt;strong&gt;$440 million in just 45 minutes&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The cause? A combination of "dead code" left festering in production for eight years and the reuse of a simple variable flag. A cosmetic-level mistake triggered an architectural monster. The lesson is clear: &lt;strong&gt;technical debt is a fatal business risk&lt;/strong&gt;, and AI, in its rush to "clean up," can easily trigger these dormant disasters if it lacks context.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Compass for Technical Debt: The 3-Level Framework
&lt;/h2&gt;

&lt;p&gt;To direct AI effectively, we must first categorize the "smell." This is a practical 3-level diagnostic framework:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5778scvikxy6ud8c7kwp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5778scvikxy6ud8c7kwp.png" alt=" " width="800" height="588"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Fig. 1 — The 3-Level Code Smell Framework with Impact Scores&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Level 1: Cosmetic
&lt;/h3&gt;

&lt;p&gt;Magic numbers and unclear names (e.g., &lt;code&gt;msg[:160]&lt;/code&gt;). These require simple renames or constants. &lt;strong&gt;Patterns are never needed here.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Level 2: Structural
&lt;/h3&gt;

&lt;p&gt;Duplicated logic or 200-line methods. We fix these by extracting private methods. We only introduce patterns &lt;strong&gt;if the logic is guaranteed to grow&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Level 3: Architectural
&lt;/h3&gt;

&lt;p&gt;The "God Class" that does everything. This is where "Strategy Spells" (design patterns) are justified — &lt;strong&gt;but only if the business context allows for the added complexity&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Solution: "Ask First, Code Later"
&lt;/h2&gt;

&lt;p&gt;The secret to clean code in 2026 isn't a better prompt; it's a better workflow. The methodology uses three composable workflows to inject human judgment into the machine:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb9x0xcdvs4tycua4ng5u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb9x0xcdvs4tycua4ng5u.png" alt=" " width="800" height="753"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Fig. 2 — Human-Guided Agentic Refactoring Workflow (Cursor · Windsurf · GitHub Copilot · Antigravity · any agentic IDE)&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The Interview (&lt;code&gt;/refactor-interview&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;Before touching a single line of code, the AI must ask &lt;strong&gt;6 strategic questions&lt;/strong&gt;. How big is the team? Is the logic stable? If you are a team of three and the logic won't change, the AI is explicitly forbidden from using complex patterns like Strategy or ABC interfaces. We turn advice into hard constraints.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;About the team:&lt;/strong&gt; How many people work on this file? Is it in production?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;About logic stability:&lt;/strong&gt; Will the conditions or cases change? Is this logic duplicated elsewhere?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;About technical requirements:&lt;/strong&gt; Does this need unit testing in isolation? Any non-functional constraints?&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. The Strategic Scan (&lt;code&gt;/refactor-analyze&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;Once context is set, the AI scans the file. It doesn't just list duplications; it assigns &lt;strong&gt;Impact Scores (1–3)&lt;/strong&gt; and maps smell dependencies. It identifies the "root cause" so we don't fix symptoms while leaving the real disaster behind.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Smell&lt;/th&gt;
&lt;th&gt;Level&lt;/th&gt;
&lt;th&gt;Impact&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Magic number &lt;code&gt;160&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;L1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Replace with &lt;code&gt;SMS_CHAR_LIMIT&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unclear parameter &lt;code&gt;u&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;L1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Rename to &lt;code&gt;user&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Structural duplication&lt;/td&gt;
&lt;td&gt;L2&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Branches repeat the same pattern&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;God Class&lt;/td&gt;
&lt;td&gt;L3&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Does 5 different things&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  3. Constrained Execution (&lt;code&gt;/refactor-execute&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;The final step is "Controlled Execution." The AI proposes a &lt;strong&gt;3–5 step plan&lt;/strong&gt; with risk estimates. It shows step-by-step diffs and waits for human approval and "green tests" before proceeding. This eliminates the "Black Box" and ensures the AI remains a partner, not a rogue agent.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Every pattern introduced requires a comment in the code:&lt;/em&gt;&lt;/p&gt;


&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Pattern: [name] — justification: [specific reason from the interview]
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Conclusion: You Are the Pilot
&lt;/h2&gt;

&lt;p&gt;When we compared a "Naive AI" (no context) with an "Interviewed AI" (with context) on the same messy file, the result was undeniable:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk5aqa3zq7qlpoabfrydg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk5aqa3zq7qlpoabfrydg.png" alt=" " width="800" height="424"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Fig. 3 — Naive AI vs Interviewed AI: same file, same tests, very different outcomes&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;300 lines of over-engineered complexity vs. &lt;strong&gt;180 lines of clean, maintainable code&lt;/strong&gt;. Both passed the tests, but only one is sustainable for a human team.&lt;/p&gt;

&lt;p&gt;In the age of AI, your value as a Senior Developer has shifted. It is no longer about the speed of your typing, but the &lt;strong&gt;quality of your judgment&lt;/strong&gt;. AI is the engine, but &lt;strong&gt;you are the Pilot&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Remember: Ask first, code later.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;&lt;em&gt;Maurizio Argoneto is CTO at Publisys Spa and AWS Community Hero&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>architecture</category>
      <category>codequality</category>
      <category>softwareengineering</category>
    </item>
  </channel>
</rss>
