<?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: Ryosuke Tsuji</title>
    <description>The latest articles on DEV Community by Ryosuke Tsuji (@ryantsuji).</description>
    <link>https://dev.to/ryantsuji</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%2F3843591%2F8b126f91-f561-4e6b-8492-814b18d680ec.jpg</url>
      <title>DEV Community: Ryosuke Tsuji</title>
      <link>https://dev.to/ryantsuji</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ryantsuji"/>
    <language>en</language>
    <item>
      <title>Observability Design for the AI Era — Application / Infrastructure / CI / LLM, Each in Its Own Shape (Part 1)</title>
      <dc:creator>Ryosuke Tsuji</dc:creator>
      <pubDate>Mon, 06 Jul 2026 23:44:23 +0000</pubDate>
      <link>https://dev.to/ryantsuji/observability-design-for-the-ai-era-application-infrastructure-ci-llm-each-in-its-own-56eg</link>
      <guid>https://dev.to/ryantsuji/observability-design-for-the-ai-era-application-infrastructure-ci-llm-each-in-its-own-56eg</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;AI assistance disclosure: This article was drafted with the help of Claude. All technical content, design decisions, code references, and screenshots reflect production systems I designed and operate at airCloset; the prose was revised by me prior to publication.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Hi, I'm &lt;a href="https://x.com/ryantsuji" rel="noopener noreferrer"&gt;Ryan&lt;/a&gt;, CTO at airCloset.&lt;/p&gt;

&lt;p&gt;In the previous series, &lt;a href="https://dev.to/ryantsuji/making-the-context-across-46-repositories-semantically-searchable-for-ai-part-2-51d9"&gt;code-graph deep dive (Part 2)&lt;/a&gt;, I wrote about making a 46-repo codebase semantically searchable for AI. The final issue I left open in that piece was &lt;strong&gt;the absence of dynamic analysis&lt;/strong&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What lives on the graph is the fact that "this edge exists statically." How often that edge actually gets used in production isn't recorded.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A graph that gives you static facts is one thing. Telling AI &lt;strong&gt;what's actually happening in production right now&lt;/strong&gt; is a separate problem. So the same shaping discipline I applied to the static graph needs to apply to the observability stack too.&lt;/p&gt;

&lt;p&gt;This post is the first half of that story. I split it into two: Part 1 (this post) covers &lt;strong&gt;how I shape four different monitoring surfaces&lt;/strong&gt; (application / infrastructure / CI / LLM). Part 2 covers PII handling, the integration surface, and Self-Healing — coming a week later.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Does "Observable to AI" Even Mean?
&lt;/h2&gt;

&lt;p&gt;The biggest lesson from the code-graph series was: &lt;strong&gt;the data has to be shaped before AI can consume it&lt;/strong&gt;. Throwing 46 repositories of source at a model blows past the context window and invites hallucination. So we shaped it — static analysis into a graph, boundary nodes given meaning, SAME_ENTITY joins between graphs — and only then handed it over.&lt;/p&gt;

&lt;p&gt;The observability stack has the exact same problem. Throw raw production logs at AI and you get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sheer log volume that drowns the context window&lt;/li&gt;
&lt;li&gt;No way for the model to tell errors from noise&lt;/li&gt;
&lt;li&gt;Metrics, logs, and traces that don't link to each other&lt;/li&gt;
&lt;li&gt;Questions like "what are we spending right now" that raw logs don't answer at all&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In other words, &lt;strong&gt;logs have to be reshaped before AI can use them.&lt;/strong&gt; Same problem, different domain.&lt;/p&gt;

&lt;p&gt;The catch is that the &lt;em&gt;right&lt;/em&gt; shape depends on &lt;strong&gt;what you want AI to answer&lt;/strong&gt;. At cortex (the internal AI platform), I split the monitoring surface into four axes and let each one settle into its own form:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: "cortex" here refers to airCloset's internal AI platform codename. Unrelated to Snowflake Cortex, Palo Alto Networks Cortex, etc.&lt;/p&gt;
&lt;/blockquote&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%2Fn2bfma8hhmtirm6lc5zq.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%2Fn2bfma8hhmtirm6lc5zq.png" alt="Four monitoring axes, each shaped to the question's nature, then handed to AI" width="800" height="450"&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;Monitoring target&lt;/th&gt;
&lt;th&gt;What you want AI to answer&lt;/th&gt;
&lt;th&gt;Shape&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Application&lt;/td&gt;
&lt;td&gt;"What's happening in production right now?" (exploration)&lt;/td&gt;
&lt;td&gt;log + trace&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Infrastructure&lt;/td&gt;
&lt;td&gt;"Do we have enough resources? Anything down?" (time series)&lt;/td&gt;
&lt;td&gt;metric&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI&lt;/td&gt;
&lt;td&gt;"What broke? Since when?" (alert + history)&lt;/td&gt;
&lt;td&gt;log + alert&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LLM&lt;/td&gt;
&lt;td&gt;"How much are we spending? Who's using how much?" (real-time + structured aggregation)&lt;/td&gt;
&lt;td&gt;metric + structured records&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;"Just push everything through OTel and dump it all in Loki" is an option. But the moment you do, you're asking one backend to answer wildly different kinds of questions — real-time "what's spending right now" alongside "monthly cost broken down by team via SQL" — and one of them is going to suffer. Splitting by purpose is the choice I made.&lt;/p&gt;

&lt;p&gt;Let me walk through each of the four axes. Application and infrastructure are the foundation, so I'll keep those brief. CI and LLM are where the AI-era design judgments actually surface, so I'll dig into those.&lt;/p&gt;

&lt;h2&gt;
  
  
  Application — OTel + Loki + Tempo, the Standard Stack
&lt;/h2&gt;

&lt;p&gt;The foundation is unremarkable. Every cortex application is instrumented with &lt;a href="https://opentelemetry.io/" rel="noopener noreferrer"&gt;OpenTelemetry&lt;/a&gt;, with traces going to Tempo, logs to Loki, and metrics to Mimir — the standard Grafana Cloud setup.&lt;/p&gt;

&lt;p&gt;There's no special trick here. What matters is the discipline: &lt;strong&gt;every app emits logs and traces in the same shape&lt;/strong&gt;. That uniformity is what lets AI later run something like &lt;code&gt;{service_name="&amp;lt;service&amp;gt;"} |~ "error"&lt;/code&gt; through MCP and investigate across services.&lt;/p&gt;

&lt;p&gt;I covered the actual instrumentation in &lt;a href="https://dev.to/ryantsuji/fixed-before-anyone-notices-stronger-after-every-fix-self-healing-recurrence-prevention-series-1e86"&gt;AI Harness Series Part 4 (Self-Healing)&lt;/a&gt;, so I'll leave the details there. The point worth repeating is: &lt;strong&gt;a standard OTel stack, properly laid down, is the precondition for everything AI-driven that comes later&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Infrastructure — Cloud Run / BigQuery / Pub/Sub Metrics, All Into Mimir
&lt;/h2&gt;

&lt;p&gt;cortex runs on GCP and stitches together Cloud Run, Cloud Run Jobs, BigQuery, Pub/Sub, Cloud Tasks, and the usual suspects. Each GCP resource's metrics (CPU, memory, execution count, latency, queue dwell time, etc.) flow through Cloud Monitoring into Mimir.&lt;/p&gt;

&lt;p&gt;Nothing special here either — just standard GCP metrics, all gathered into one Mimir instance. But that "one place" property pays off later: AI can answer "which service used the most CPU last week?" or "is there a worker with a clogged queue?" naturally, because everything is queryable from a single store. MCP picks it up from there.&lt;/p&gt;

&lt;p&gt;That's it for the foundation. Standard observability stacks are well-documented elsewhere; go read Grafana's and OpenTelemetry's docs if you want the details.&lt;/p&gt;

&lt;p&gt;The interesting AI-era design judgments are in the next two axes — CI and LLM.&lt;/p&gt;

&lt;h2&gt;
  
  
  CI — Ship Logs to Loki via Post-Hoc Pull, Not Webhook Push
&lt;/h2&gt;

&lt;p&gt;cortex runs CI on GitHub Actions, and I ship every CI log into Grafana Loki.&lt;/p&gt;

&lt;p&gt;"Why? GitHub Actions has a perfectly good UI for that" is a reasonable question. The reasons are concrete:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Having AI hit the GitHub Actions API on every investigation is slow and auth-heavy. Ingesting into Loki once means AI can &lt;strong&gt;query it ad-hoc&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;One Loki instance holds CI logs and application logs together, so you can &lt;strong&gt;cross-query&lt;/strong&gt; them&lt;/li&gt;
&lt;li&gt;LogQL alerts turn CI failure into a structured signal&lt;/li&gt;
&lt;li&gt;AI can ask "any tests that have been broken since last week?" in natural language&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But the shipping mechanism is unusual. The choice cortex made:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Don't push logs from inside the CI run. After the run finishes, pull them from the GitHub API.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&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%2F1gf6svjrpf0vosl8e7xe.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%2F1gf6svjrpf0vosl8e7xe.png" alt="Shipping CI logs via post-hoc pull instead of webhook push" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Concretely:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;When the Test job ends, a &lt;code&gt;workflow_run&lt;/code&gt; event fires&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;separate workflow&lt;/strong&gt; dedicated to log shipping triggers&lt;/li&gt;
&lt;li&gt;That workflow pulls logs from the GitHub API (&lt;code&gt;/repos/.../actions/jobs/.../logs&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Ships them to Grafana Cloud as structured JSON (job / status / ref / pr / commit / output, etc.) via OTLP &lt;code&gt;/v1/logs&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Filter on &lt;code&gt;{service_name="ci", ref="main", status="failure"}&lt;/code&gt; and you get just the main-branch CI failures, cleanly.&lt;/p&gt;

&lt;p&gt;Why pull instead of push:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CI execution and observability decouple.&lt;/strong&gt; If shipping fails, the test run is unaffected. You can also retry / replay shipping independently&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No path for PR code to touch the API key.&lt;/strong&gt; The shipping workflow runs in the default-branch context and uses base-repo secrets, not whatever a fork PR brought. The test workflow itself never touches the Grafana API key — that's a structural guarantee, not a "we trust it won't leak"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shipping failure becomes observable.&lt;/strong&gt; If shipping lives inside CI, a shipping bug means the observability stack goes silent — and you don't notice. Split them, and the shipping workflow's success / failure is itself something you can alert on&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The moment a main-branch failure shows up, a LogQL alert fires and Slack gets pinged. That's the trigger for Self-Healing, which I cover in Part 2.&lt;/p&gt;

&lt;h2&gt;
  
  
  LLM — Gemini and Claude Code, Two Different Shapes
&lt;/h2&gt;

&lt;p&gt;The last axis is LLM observability. cortex uses both Gemini API and Claude Code (Anthropic's official CLI) heavily, and &lt;strong&gt;since both cost money, I want visibility into how they're used&lt;/strong&gt; (though the billing models differ — Gemini is pay-per-use, Claude Code is a subscription, and that difference matters later). The reason I shape them differently isn't really about "what kind of question" — it's about &lt;strong&gt;where you can instrument — the instrumentation locus&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gemini&lt;/strong&gt; — I own the calling code, so I can wrap every call with a common helper and emit metrics inline. Prometheus is the natural fit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Claude Code&lt;/strong&gt; — It's an external CLI; I can't wrap its calls from the inside. Usage shows up as records after the fact. A structured store (BigQuery) is the natural fit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The "real-time vs SQL aggregation" framing of the question is a consequence of where you can instrument, not the cause. With that clarified, here's how each one plays out.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gemini — Prometheus, Cost Visible in Real Time via Client-Side Estimation
&lt;/h3&gt;

&lt;p&gt;cortex uses Gemini everywhere: db-graph table description generation, code-graph field type inference, general context generation. What I want to see is &lt;strong&gt;what's expensive right now, with no lag&lt;/strong&gt;. If a runaway prompt or batch job kicks off, I don't want to wait until tomorrow's billing report.&lt;/p&gt;

&lt;p&gt;So every Gemini call goes through a common wrapper (&lt;code&gt;traceGeminiCall&lt;/code&gt;) that emits four metrics per call:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;gemini.tokens.total&lt;/code&gt; — cumulative tokens (labels: &lt;code&gt;model&lt;/code&gt; / &lt;code&gt;service&lt;/code&gt; / &lt;code&gt;type=prompt|completion&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;gemini.requests.total&lt;/code&gt; — request count (labels: &lt;code&gt;model&lt;/code&gt; / &lt;code&gt;service&lt;/code&gt; / &lt;code&gt;status&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;gemini.request.duration&lt;/code&gt; — latency histogram&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;gemini.cost.usd&lt;/code&gt; — estimated cost (labels: &lt;code&gt;model&lt;/code&gt; / &lt;code&gt;service&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The design choice that splits opinions is: &lt;strong&gt;who computes the cost?&lt;/strong&gt; Two options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A. Pull from Google Cloud Billing API after the fact&lt;/strong&gt; — accurate, but billing lags by hours to a day, and &lt;strong&gt;there's no per-task cost granularity&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;B. Compute client-side from token counts × a price table&lt;/strong&gt; — instant, with &lt;strong&gt;per-task granularity attached by you&lt;/strong&gt;, but the price table needs upkeep&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I picked B. The price table lives in a constant called &lt;code&gt;GEMINI_PRICING&lt;/code&gt; and gets manually bumped whenever Google moves prices. Just &lt;code&gt;gemini-3-flash&lt;/code&gt; / &lt;code&gt;gemini-3-pro&lt;/code&gt; with input/output unit prices each. Nothing fancy.&lt;/p&gt;

&lt;p&gt;The real reason for B is &lt;strong&gt;per-task granularity&lt;/strong&gt;, not just speed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You can't tune what you can't attribute.&lt;/strong&gt; Cloud Billing (with BQ export) will slice by SKU, project, and resource label, but not by call-site context. What you actually want to trim is "the db-graph table description generation cost $X," "the code-graph field type inference cost $Y," "that one prompt cost $Z" — call-site-context granularity, which billing simply doesn't carry. With client-side wrapping you attach &lt;code&gt;service&lt;/code&gt; / &lt;code&gt;model&lt;/code&gt; / call-site context as labels, and you can later slice by any of them in PromQL.&lt;/li&gt;
&lt;li&gt;Real-time visibility is a bonus — runaway prompts and batches don't wait for tomorrow's billing.&lt;/li&gt;
&lt;li&gt;Price table maintenance is light (Google doesn't change prices often), so the upkeep cost is trivial.&lt;/li&gt;
&lt;li&gt;Cloud Billing API authentication, fetching, normalization, fan-out is its own pipeline of weight you'd have to maintain.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then I emit &lt;code&gt;gemini_cost_usd_USD_total&lt;/code&gt; as a cumulative Prometheus counter (the doubled &lt;code&gt;usd_USD&lt;/code&gt; comes from OTel meter name &lt;code&gt;gemini.cost.usd&lt;/code&gt; combined with the unit &lt;code&gt;USD&lt;/code&gt; during Prometheus exporter conversion) and PromQL can answer "how much did we spend in the last hour" directly: &lt;code&gt;sum(increase(gemini_cost_usd_USD_total[1h]))&lt;/code&gt;. Alert fires at $1/hour, info severity, into Slack. Simple as that.&lt;/p&gt;

&lt;p&gt;Prometheus is what you want when the question is "right now."&lt;/p&gt;

&lt;h3&gt;
  
  
  Claude Code — Send to BigQuery, Built for SQL Aggregation
&lt;/h3&gt;

&lt;p&gt;Every developer at the company uses Claude Code. But the economics differ from Gemini: it's a subscription, so token usage doesn't translate straight into a dollar figure. What I'm after here is less the cost itself and more the &lt;strong&gt;usage picture&lt;/strong&gt; — &lt;strong&gt;who's using how much, how many tokens per repo, how well the cache is landing&lt;/strong&gt; — so I can turn it into better usage.&lt;/p&gt;

&lt;p&gt;The question that split opinion: "Should Claude Code usage go to Loki too?"&lt;/p&gt;

&lt;p&gt;The answer: &lt;strong&gt;No, into BigQuery.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Why? Because Claude Code usage is, fundamentally, a &lt;strong&gt;structured ledger&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;email&lt;/code&gt; — the user&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;repository&lt;/code&gt; — which repo it was used in&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;timestamp&lt;/code&gt; — when&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;input_tokens&lt;/code&gt; / &lt;code&gt;output_tokens&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cache_creation_input_tokens&lt;/code&gt; / &lt;code&gt;cache_read_input_tokens&lt;/code&gt; — prompt-cache effectiveness included&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the questions you want to ask look like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Last week, what's the cumulative spend for Team A members?"&lt;/li&gt;
&lt;li&gt;"How much did edits on Repo X cost over the past month?"&lt;/li&gt;
&lt;li&gt;"What's the prompt-cache hit ratio difference between teams?"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of these are &lt;strong&gt;SQL aggregation questions&lt;/strong&gt;. LogQL aggregation and joins on Loki are painful. BigQuery, with a DAY partition and email as the primary key, just writes naturally.&lt;/p&gt;

&lt;p&gt;So the Claude Code → BigQuery pipeline runs in four stages:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Emit&lt;/strong&gt; — A bundled analyzer in Claude Code POSTs &lt;code&gt;UsageInput&lt;/code&gt; (token info only, no email) to an internal endpoint&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auth proxy&lt;/strong&gt; — A Cloudflare Edge Router worker validates &lt;code&gt;CORTEX_API_KEY&lt;/code&gt; and stamps the user's email onto the request as &lt;code&gt;X-Cortex-User-Email&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ingest&lt;/strong&gt; — A Cloud Run API dedupes and publishes to Pub/Sub&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Persist&lt;/strong&gt; — A Cloud Run worker pulls from Pub/Sub, validates the schema, and streaming-inserts to BigQuery&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Two structural points worth calling out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Identity authority lives at the Edge Router.&lt;/strong&gt; User identity is resolved exactly once, there. The emit side (Claude Code) never holds the email. This shuts down whole classes of client-side id-spoofing and social-engineering paths structurally&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pub/Sub gives async decoupling.&lt;/strong&gt; Ingest and worker are separate, so backpressure on the worker doesn't affect ingest response times. On failure, Pub/Sub DLQ retries up to five times&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What sits in BigQuery is visible day-by-day through the internal portal I'll cover in Part 2. Here's what it actually looks like:&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%2Fwcvlbckus0t4aqx7s98s.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%2Fwcvlbckus0t4aqx7s98s.png" alt="Claude Code usage dashboard — 78.0B tokens over the past 30 days, 96% of which is cache read" width="799" height="445"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The numbers are interesting enough to mention briefly: in the last 30 days, &lt;strong&gt;78.0B tokens / 384K messages / 47 users / 79 repositories&lt;/strong&gt;. The one to focus on is &lt;strong&gt;Cache Read Input at 75.1B (96% of total)&lt;/strong&gt; — prompt-cache is dramatically effective. On a subscription this doesn't show up as a dollar figure, but cache read tokens carry roughly 1/10 the effective input rate, so if you were paying per-token API pricing for the same usage, this works out to roughly &lt;strong&gt;7× more efficient at the blended input level&lt;/strong&gt; versus the cache-less counterfactual. Being able to see usage efficiency as a concrete number like this is the point of the visualization; "aggregation-shaped backend matched to the question" is the design choice that makes this kind of metric &lt;strong&gt;fall out of SQL naturally and show up daily&lt;/strong&gt;. Doing the same thing in LogQL would be a battle.&lt;/p&gt;

&lt;p&gt;As a side note: &lt;strong&gt;MCP tool-call logs&lt;/strong&gt; end up in BigQuery too (&lt;code&gt;cortex.mcp_tool_calls&lt;/code&gt;), but via a simpler path — each MCP server just writes records directly, no OTel in the loop. The "annotation graph MCP used ~50,000 times by ~73 people" figure from the previous series came from this exact table.&lt;/p&gt;

&lt;p&gt;The core point of this layer is: &lt;strong&gt;don't dogmatically force everything through OTel — match the tool to the qualitative nature of the aggregation.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  To Be Continued
&lt;/h2&gt;

&lt;p&gt;That's the four axes (application / infrastructure / CI / LLM) and the design judgments behind each. The &lt;strong&gt;write-side&lt;/strong&gt; of the observability stack is wrapped up.&lt;/p&gt;

&lt;p&gt;But shaping the write side isn't the whole story. The moment production data flows through the stack, &lt;strong&gt;PII&lt;/strong&gt; becomes a constraint you have to design around. And the data has to actually be &lt;strong&gt;consumable by AI&lt;/strong&gt; through MCP, with a thoughtful integration surface for both humans (web dashboards) and AI (MCP). Connect all of that, and &lt;strong&gt;the real driver of Self-Healing&lt;/strong&gt; comes into focus from the observability side. That's the Part 2 story.&lt;/p&gt;

&lt;p&gt;Thanks for reading. Part 2, "Observability Design for the AI Era — Reconciling PII Protection With AI Searchability, and Driving Self-Healing," follows in a week.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>observability</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Making the Context Across 46 Repositories Semantically Searchable for AI (Part 2)</title>
      <dc:creator>Ryosuke Tsuji</dc:creator>
      <pubDate>Mon, 29 Jun 2026 23:50:05 +0000</pubDate>
      <link>https://dev.to/ryantsuji/making-the-context-across-46-repositories-semantically-searchable-for-ai-part-2-51d9</link>
      <guid>https://dev.to/ryantsuji/making-the-context-across-46-repositories-semantically-searchable-for-ai-part-2-51d9</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;AI assistance disclosure: This article was drafted with the help of Claude. All technical content, design decisions, code references, and screenshots reflect production systems I designed and operate at airCloset; the prose was revised by me prior to publication.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Hi, I'm &lt;a href="https://x.com/ryantsuji" rel="noopener noreferrer"&gt;Ryan&lt;/a&gt;, CTO at airCloset.&lt;/p&gt;

&lt;p&gt;In &lt;a href="https://dev.to/ryantsuji/building-one-knowledge-graph-across-46-repositories-with-static-analysis-part-1-egm"&gt;Part 1&lt;/a&gt;, I wrote about unifying 46 repositories of production code into a single knowledge graph via static analysis. The graph itself got built, but I closed the post with &lt;strong&gt;four open issues&lt;/strong&gt;: no semantic search, node explosion, having to open the file to actually know what a function does, and the cost of writing a new parser every time a new boundary pattern showed up.&lt;/p&gt;

&lt;p&gt;This Part 2 is about &lt;strong&gt;how I solved the first one — the entry-point problem (no semantic search).&lt;/strong&gt; The other three are left exactly as Part 1 described them — I'll come back to them at the end, together with the new issues that surfaced once the entry-point problem was out of the way.&lt;/p&gt;

&lt;p&gt;The reason to start with the entry-point problem is simple: if the graph exists but the only way to reach it is grep, the model ends up inferring anyway. The whole point — &lt;strong&gt;"give the model verified facts, not inference"&lt;/strong&gt; — falls apart. So the entry-point problem had to be solved before the others.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hint Was in db-graph
&lt;/h2&gt;

&lt;p&gt;Months earlier, I'd already solved the same structural problem in a different domain — the &lt;a href="https://zenn.dev/aircloset/articles/2731787582881a" rel="noopener noreferrer"&gt;db-graph project&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Internally, we had a large number of DB tables spread across many services, and &lt;strong&gt;no single person had the full picture&lt;/strong&gt;. Different people knew different pieces well, but the whole map didn't fit in anyone's head. So I built db-graph: extract schemas statically from ORM definitions, generate per-table descriptions with Gemini, embed them as 768-dimensional vectors in the graph, and make the whole thing semantically searchable in natural language.&lt;/p&gt;

&lt;p&gt;At the time of that article it covered 991 tables. Today it spans &lt;strong&gt;21 schemas / 1,133 tables / 10,815 columns&lt;/strong&gt;, and finding data in natural language without knowing table names is just how people work now.&lt;/p&gt;

&lt;p&gt;The pattern that proved out there:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Static-analysis graph + AI-generated context = natural-language semantic search works.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Bringing the Same Pattern to code-graph
&lt;/h2&gt;

&lt;p&gt;If it worked for db-graph, it should work for code-graph. The moment that thought landed, I noticed something:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;code-graph already contains "DB table nodes" as boundary nodes&lt;/strong&gt; — they're one of the boundary node types I covered in Part 1.&lt;/p&gt;

&lt;p&gt;So if I just &lt;strong&gt;join&lt;/strong&gt; code-graph and db-graph, code-graph automatically inherits db-graph's semantic context. Without writing a single annotation, the existing assets alone make the graph meaningfully richer.&lt;/p&gt;

&lt;p&gt;That's where the idea of "joining graphs" first came up — not treating each graph as its own island, but designing the joins between them.&lt;/p&gt;

&lt;h2&gt;
  
  
  But API / Event / Page Still Need Meaning — and Annotating Every Function Is Off the Table
&lt;/h2&gt;

&lt;p&gt;Joining db-graph took care of DB context. But the remaining boundaries (API / Event) and the graph's entry-point type (Page) still need meaning attached. Static analysis alone can't pull intent out of those, so context has to come from somewhere else.&lt;/p&gt;

&lt;p&gt;The choice was clear: &lt;strong&gt;write the intent directly into the code via annotations&lt;/strong&gt; (the same approach used by cortex's internal knowledge graph, which I covered in &lt;a href="https://dev.to/ryantsuji/the-heart-of-the-ai-harness-a-knowledge-graph-of-the-ai-by-the-ai-for-the-ai-series-part-2-53bm"&gt;AI Harness Series, Part 2&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;The catch: you can't annotate all the functions across 46 repos. There must be tens of thousands of them. Asking established teams running an existing production codebase to retroactively annotate everything is just not realistic.&lt;/p&gt;

&lt;p&gt;But here's the second realization:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What matters is just the boundary nodes.&lt;/strong&gt; So if I only annotate around the boundaries, that's enough.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When an AI agent asks "what breaks if I change this code" or "what other repos call this API," what it needs isn't a per-function logic explanation. It needs &lt;strong&gt;boundary intent&lt;/strong&gt; — what is this screen for, what does this API return, what milestone in the business does this Event mark.&lt;/p&gt;

&lt;p&gt;= &lt;strong&gt;Minimum annotations, maximum meaning.&lt;/strong&gt; That became the heart of the design.&lt;/p&gt;

&lt;h2&gt;
  
  
  Designing the annotation graph
&lt;/h2&gt;

&lt;p&gt;Putting it together (internally we call this annotation graph &lt;strong&gt;service-product-graph&lt;/strong&gt;, or SPG):&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%2Frg6cw54333sci83uz1g1.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%2Frg6cw54333sci83uz1g1.png" alt="Three graphs joined as peers form a knowledge graph that carries meaning" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Three graphs sit &lt;strong&gt;as peers, joined by SAME_ENTITY edges&lt;/strong&gt;. There's no hierarchy — &lt;strong&gt;you can start from any graph and reach the others&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;code-graph (structure)&lt;/strong&gt; — functions / classes / boundary nodes from static analysis (46 repos)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;db-graph (DB context)&lt;/strong&gt; — 1,133 tables, semantically described&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;annotation graph (intent)&lt;/strong&gt; — &lt;code&gt;@graph-*&lt;/code&gt; tags written only around boundaries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The entry point for AI agents is a single &lt;strong&gt;MCP server&lt;/strong&gt; that traverses all three graphs. AI agents never hit db-graph directly — the annotation graph's MCP server proxies db-graph calls on their behalf.&lt;/p&gt;

&lt;p&gt;The annotation graph has 7 node types: Page / Section / Dialog / Field / Action / Api / Task. The early version was screen-focused and called &lt;code&gt;screen-graph&lt;/code&gt;, but once it grew to cover backend Api / Task, it was renamed to service-product-graph.&lt;/p&gt;

&lt;h2&gt;
  
  
  An Annotation Example
&lt;/h2&gt;

&lt;p&gt;Here's what an annotation looks like (fictional, but close in shape to the real ones):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/**
 * @graph-page /home
 * @graph-business Main screen. Members can see what they're currently renting, buy items, and initiate returns.
 * @graph-label Home Screen
 * @graph-has-section banners, wearing-items, wearing-return, delivery-status
 * @graph-has-dialog buying-modal, return-modal
 * @graph-navigates-to /return-procedure, /checkout, /my-karte
 * @graph-calls GET /api/v1/wearing
 * @graph-reads admin_delivery_orders, admin_rental_items
 * @graph-flow styling-loop
 * @graph-status monthly-member
 */&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things matter here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;@graph-business&lt;/code&gt;&lt;/strong&gt; carries the intent text (in our actual codebase it's written in Japanese). This is exactly what gets vectorized — it's the substance of semantic search.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;@graph-flow&lt;/code&gt; / &lt;code&gt;@graph-status&lt;/code&gt;&lt;/strong&gt; carry where this sits in the member lifecycle (free signup → monthly subscription → styling loop → cancellation, etc.) and which member segment it's for. They add a second dimension of meaning: "this screen shows up inside the styling loop for monthly members."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There's also &lt;code&gt;@graph-case&lt;/code&gt; (the conditional pattern tag that test cases derive from), but that's for another time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running Annotations Without Interfering With the Day-to-Day Dev Workflow
&lt;/h2&gt;

&lt;p&gt;This is where it gets practical.&lt;/p&gt;

&lt;p&gt;Once I committed to building annotation graph, here were the constraints:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Engineers run normal product dev with human code review&lt;/li&gt;
&lt;li&gt;AI review isn't wired up on every repo yet — cortex's fully automated review (covered in &lt;a href="https://dev.to/ryantsuji/ai-isnt-something-to-trust-its-something-to-design-series-final-30aa"&gt;AI Harness Series, Part 6&lt;/a&gt;) only works inside the cortex monorepo&lt;/li&gt;
&lt;li&gt;Asking humans to review annotations on top of their normal review load is a non-starter&lt;/li&gt;
&lt;li&gt;Even a split like "humans review the code, the AI reviews the annotations" inside the same PR mixes two review streams together and just confuses everyone&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In other words: &lt;strong&gt;don't mix humans and AI inside the same PR&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The solution was to physically separate annotations onto their own branch.&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%2Ff5sbie95vgkzk6wz5pdn.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%2Ff5sbie95vgkzk6wz5pdn.png" alt="Separate the AI-managed annotation branch from the human-managed main branch" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Leave main untouched; engineers' normal flow stays exactly as it was&lt;/li&gt;
&lt;li&gt;Stand up a separate &lt;strong&gt;annotation branch&lt;/strong&gt; that's the AI's exclusive territory&lt;/li&gt;
&lt;li&gt;When main changes, a webhook fires&lt;/li&gt;
&lt;li&gt;The annotation branch handles &lt;strong&gt;generating&lt;/strong&gt; the diff annotations &lt;strong&gt;and reviewing&lt;/strong&gt; them — the AI does both, end-to-end&lt;/li&gt;
&lt;li&gt;From the engineer's side, they only touch main and don't even need to know annotations exist&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the "every line of code passes through an AI gate" ideal from &lt;a href="https://dev.to/ryantsuji/ai-isnt-something-to-trust-its-something-to-design-series-final-30aa"&gt;AI Harness Series, Part 6&lt;/a&gt;, adapted to the constraints of an existing organization. cortex (the internal AI platform) is a monorepo I assemble from scratch, so "every commit passes the AI gate" actually holds there. For the 46-repo production system, that precondition doesn't hold. So instead of giving up on the ideal, I split it: &lt;strong&gt;engineers' workflow on one branch, AI's annotation workflow on another, both running in parallel&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Protecting Cross-Graph Consistency With an SLO
&lt;/h3&gt;

&lt;p&gt;Just running the annotation pipeline doesn't guarantee the &lt;strong&gt;quality of the joins&lt;/strong&gt; between the three graphs (code-graph / db-graph / annotation graph). So there's a set of SLOs that automatically check the consistency across the entire graph.&lt;/p&gt;

&lt;p&gt;The main rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;API chain connectivity&lt;/strong&gt; — at least &lt;strong&gt;95%&lt;/strong&gt; of &lt;code&gt;HANDLES_API&lt;/code&gt; handlers must have downstream function calls (= no handlers that receive an API and then do nothing)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DB access completeness&lt;/strong&gt; — at least &lt;strong&gt;80%&lt;/strong&gt; of DB read/write edges must be &lt;strong&gt;joined to db-graph column nodes&lt;/strong&gt; (= code-graph's DB boundaries are connected to db-graph's meaning)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Event field resolution&lt;/strong&gt; — at least &lt;strong&gt;70%&lt;/strong&gt; of Event edges must carry field-level information&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No ambiguous edges&lt;/strong&gt; — name-resolution-ambiguous edges must be &lt;strong&gt;0&lt;/strong&gt; (severity: error)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are really just &lt;strong&gt;a naive question — "shouldn't the boundaries connect to each other?" — turned into an SLO.&lt;/strong&gt; If anything drops below threshold, an alert fires, and the trustworthiness of the whole graph gets defended every day.&lt;/p&gt;

&lt;p&gt;The daily boundary-analysis cron from Part 1 (5% connection-rate drop = alert) was code-graph-only. This is a &lt;strong&gt;cross-graph SLO&lt;/strong&gt; — it guards the joins between graphs themselves. Add a parser to one repo, write a new annotation, change a schema — whatever happens, by the next morning a quality drop in any join becomes visible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Joining the Static Graph and the Annotation Graph via SAME_ENTITY Bridges
&lt;/h2&gt;

&lt;p&gt;I've been writing "join" casually, but the actual joining wasn't that straightforward.&lt;/p&gt;

&lt;p&gt;Static-analysis API / Page / Task nodes and annotation graph API / Page / Task nodes are created as &lt;strong&gt;separate nodes&lt;/strong&gt;. They mean the same thing, but their names / paths / identifiers don't match by themselves — there's nothing automatic about lining them up.&lt;/p&gt;

&lt;p&gt;To connect them, we generate a separate edge type called SAME_ENTITY. There are three bridges:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;API bridge&lt;/strong&gt; — API path normalization with a 4-stage fallback

&lt;ol&gt;
&lt;li&gt;Per-repo prefix conversion (e.g., normalize console-side &lt;code&gt;/console/api/&lt;/code&gt; to &lt;code&gt;/api/&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Version stripping (&lt;code&gt;/v1.x/&lt;/code&gt; → &lt;code&gt;/&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Parameter normalization (unify &lt;code&gt;/:id&lt;/code&gt;, &lt;code&gt;/{id}&lt;/code&gt; to &lt;code&gt;/:dynamic&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Exact match → tolerate trailing &lt;code&gt;?&lt;/code&gt; → strip trailing &lt;code&gt;:dynamic?&lt;/code&gt; → finally fall back to a dynamic-dispatch boundary &lt;code&gt;:dynamic&lt;/code&gt;, loosening progressively&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Page bridge&lt;/strong&gt; — 6 strategies applied in priority order (URL direct match, component path match, itemId match, PascalCase normalization match, parent-directory linking, strip dynamic segments and match parent URL)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Task bridge&lt;/strong&gt; — 8 per-repo patterns&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There was also one operational footgun. The first implementation used &lt;code&gt;INSERT NOT EXISTS&lt;/code&gt; to avoid duplicates. But BigQuery's streaming-buffer visibility lag let duplicates slip in — in one repo the edges doubled from 106 to 214 overnight. We fixed it by rewriting to &lt;code&gt;MERGE INTO&lt;/code&gt; to make the operation idempotent.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Result: Entering the Graph from "the subscription-fee calculation"
&lt;/h2&gt;

&lt;p&gt;With all of this in place, the entry-point problem from the end of Part 1 was finally solved:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"the subscription-fee calculation for members seems off"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Throw this natural-language query at annotation graph and vector search returns the related nodes (Page / Api / Function / DB table) &lt;strong&gt;as facts&lt;/strong&gt;. From there, SAME_ENTITY takes you over to code-graph functions, including callers and callees in other repos. From the DB boundaries in code-graph, you can cross into db-graph and pull the relevant columns.&lt;/p&gt;

&lt;p&gt;The entry point can be anywhere — "what calls this table?" starts from db-graph, "what's the blast radius of this function?" starts from code-graph, both walk the same connected network. From a single natural-language query, or from a specific node, &lt;strong&gt;you can now traverse all three graphs and get every relevant piece of code plus every relevant DB schema&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The Part 1 lament — &lt;strong&gt;"the graph is there but the entry point is missing"&lt;/strong&gt; — could finally be put to bed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Real Usage Numbers
&lt;/h3&gt;

&lt;p&gt;From 2026-04-16 (first production deployment) to the time of writing — about 2.5 months — the annotation graph's MCP server has handled &lt;strong&gt;~50,000 calls from ~73 users&lt;/strong&gt;. The breakdown:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Engineers (PI Division + QA + relevant engineering teams)&lt;/strong&gt; — ~47,000 calls / 51 users&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Non-engineers (stylists / customer support / mall operations / executives / administration)&lt;/strong&gt; — ~2,800 calls / 21 users&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The interesting line is the second one. "Search the codebase in natural language" is usually an engineer's tool — but once the entry-point problem was solved, &lt;strong&gt;people outside engineering&lt;/strong&gt; started using it too, asking things like "how does this feature actually work?" or "what's in this DB?" in their own words.&lt;/p&gt;

&lt;p&gt;This is adjacent to the "non-engineers writing specs with AI" trend I covered in &lt;a href="https://dev.to/ryantsuji/the-author-doesnt-have-to-be-an-engineer-how-the-harness-holds-quality-series-part-5-12e4"&gt;AI Harness Series, Part 5&lt;/a&gt; — &lt;strong&gt;a graph that can be queried by meaning starts to matter org-wide.&lt;/strong&gt; Call volume is overwhelmingly dominated by engineers, of course. The interesting thing is the &lt;strong&gt;range of job roles starting to pick it up&lt;/strong&gt;. That's the real impact of solving the entry-point problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  MCP as the Single Front Door
&lt;/h2&gt;

&lt;p&gt;The MCP server is the cross-graph entry point. It exposes six tools — service search / service detail / API detail / data-flow tracing / impact-radius tracing / business-rule full-text search — and that's the only entry point AI agents ever touch.&lt;/p&gt;

&lt;p&gt;One design choice worth calling out: &lt;strong&gt;AI agents never talk to db-graph directly.&lt;/strong&gt; The annotation graph's MCP proxies db-graph calls. From the agent's side, the mental model stays simple: "ask one MCP and get everything back."&lt;/p&gt;

&lt;p&gt;That makes the full chain — "Screen → API → Code → DB → Column" — traversable in a single MCP tool call.&lt;/p&gt;

&lt;h2&gt;
  
  
  April–May Timeline of Trial and Error
&lt;/h2&gt;

&lt;p&gt;Same approach as Part 1 (pulling commits from Jan–Mar). For Part 2, the key commits are from April–May.&lt;/p&gt;

&lt;h3&gt;
  
  
  April: Expansion and the First Bridges
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;2026-04-14&lt;/strong&gt; ─ &lt;code&gt;refactor(graph): rename screen-graph to service-product-graph&lt;/code&gt; — declaration that the scope expands from screen-only to whole-service&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2026-04-15&lt;/strong&gt; ─ &lt;code&gt;feat(graph): add Api and Task node types to service-product-graph parser&lt;/code&gt; — Api / Task node types added&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2026-04-15&lt;/strong&gt; ─ &lt;code&gt;feat(mcp): add cross-graph tools to service-product-graph MCP&lt;/code&gt; — &lt;strong&gt;cross-graph tools land&lt;/strong&gt; (the single front door across all three graphs)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2026-04-15&lt;/strong&gt; ─ &lt;code&gt;feat(graph): add SAME_ENTITY bridge edges between service-product-graph and code-graph&lt;/code&gt; — &lt;strong&gt;first bridges&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2026-04-18&lt;/strong&gt; ─ &lt;code&gt;feat(graph): resolve Redis keys to code-graph boundary nodes&lt;/code&gt; — boundary resolution through Redis&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2026-04-19&lt;/strong&gt; ─ &lt;code&gt;feat(service-product-graph): add EventBridge EMITS_TO support + SAME_ENTITY bridge&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2026-04-20&lt;/strong&gt; ─ &lt;code&gt;feat(code-graph, service-product-graph): improve SAME_ENTITY boundary bridge coverage&lt;/code&gt; — 4-stage fallback locked in&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2026-04-21&lt;/strong&gt; ─ &lt;code&gt;feat(auto-review): SPG annotation auto-maintenance pipeline&lt;/code&gt; — &lt;strong&gt;AI auto-maintenance pipeline&lt;/strong&gt; (= what Part 1 hinted at with "humans alone can't, but AI can")&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2026-04-22&lt;/strong&gt; ─ &lt;code&gt;feat(service-product-graph): add Task SAME_ENTITY bridge to code-graph&lt;/code&gt; — all three bridges in place&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  May: Stabilizing and Expanding
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;2026-05-01&lt;/strong&gt; ─ Annotation generation moves from local execution to a Cloud Run Job; operation stabilizes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2026-05-05&lt;/strong&gt; ─ &lt;code&gt;feat(spg): add mall repos to SPG indexing&lt;/code&gt; — mall repos indexed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2026-05-06&lt;/strong&gt; ─ &lt;code&gt;feat(spg): add Go-aware parser&lt;/code&gt; — &lt;strong&gt;Go support&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2026-05-06 to 08&lt;/strong&gt; ─ Page bridge strategies expanded to six, connection rate hits 100%&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What This Timeline Says
&lt;/h3&gt;

&lt;p&gt;April 15 was the day "expansion + cross-graph tools + bridges" landed in close succession. Over the next week, "Redis / EventBridge / Task bridges / annotation auto-maintenance" stacked up week over week.&lt;/p&gt;

&lt;p&gt;In particular, &lt;strong&gt;the annotation auto-maintenance pipeline on April 21&lt;/strong&gt; is where the "humans alone can't do this, but AI can" promise from Part 1 got cashed in. From that point on, annotation shifted from "humans grind through writing them" to "design the whole operation assuming AI writes them."&lt;/p&gt;

&lt;h2&gt;
  
  
  What Still Isn't Solved
&lt;/h2&gt;

&lt;p&gt;Solving the entry-point problem didn't make everything clean. A few issues remain.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Maintaining Annotation Coverage
&lt;/h3&gt;

&lt;p&gt;The frontend side is annotated heavily. Backend / Go / batch are still thin. &lt;strong&gt;Some nodes will always be missing annotations&lt;/strong&gt; — that's structural, and you can't drive it to zero. It's an ongoing operational issue.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Bridge Mis-Joins Aren't Fully Eliminated Structurally
&lt;/h3&gt;

&lt;p&gt;The Page bridge in particular has cases where multiple annotation Pages map to the same boundary — that's structural and unavoidable. Adding more strategies got coverage to 100%, but &lt;strong&gt;guaranteeing "every join is correct" 100% is hard&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. No Dynamic Analysis
&lt;/h3&gt;

&lt;p&gt;The graph only carries the fact that "this edge exists statically." How often that edge actually &lt;strong&gt;gets used in production&lt;/strong&gt; isn't recorded. Piping production execution counts back into the static graph and surfacing dead-code edges as a separate signal — that's still untouched.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Onboarding Cost When a New Repo Joins Production
&lt;/h3&gt;

&lt;p&gt;Every time a new repo enters production, the bridge normalization rules and per-repo patterns need adjusting. This is the annotation-graph-side version of Part 1's fourth issue (the cost of adding a new parser for every new boundary pattern).&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing: Not "Thrown Away," but "Evolved"
&lt;/h2&gt;

&lt;p&gt;In Part 1's closing note, I touched on the fact that the cortex side (the internal AI platform) bailed out of the code-graph approach &lt;strong&gt;early&lt;/strong&gt; and bet on an annotation-based knowledge graph instead. The bail-out was fast enough that calling it "thrown away" wouldn't be wrong — but looking back across this whole series, the more accurate word is &lt;strong&gt;"evolved."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;What it evolved into, in the end, is &lt;strong&gt;three graphs joined as peers&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;code-graph (structure)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;db-graph (DB context)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;annotation graph (boundary intent)&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Joined by SAME_ENTITY, served to the agent through MCP. The thing static analysis alone couldn't deliver — querying by meaning — became workable by reusing the db-graph success pattern and adding minimal annotations only at the boundaries.&lt;/p&gt;

&lt;p&gt;And one more framing: paired with the &lt;a href="https://dev.to/ryantsuji/ai-isnt-something-to-trust-its-something-to-design-series-final-30aa"&gt;AI Harness Series, Parts 1–6&lt;/a&gt;, this series sits as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AI Harness series&lt;/strong&gt; — how to live with AI when you're assembling the system from scratch yourself&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;code-graph-deep-dive series (Part 1 + Part 2)&lt;/strong&gt; — how to live with AI inside an existing organization's running production system&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;= the same philosophy (design without trusting AI), implemented under two different sets of constraints.&lt;/p&gt;

&lt;p&gt;Thanks for reading this far.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>knowledgegraph</category>
      <category>staticanalysis</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Got the Top 7 Badge — honestly thrilled 🙌</title>
      <dc:creator>Ryosuke Tsuji</dc:creator>
      <pubDate>Wed, 24 Jun 2026 00:21:42 +0000</pubDate>
      <link>https://dev.to/ryantsuji/got-the-top-7-badge-honestly-thrilled-535</link>
      <guid>https://dev.to/ryantsuji/got-the-top-7-badge-honestly-thrilled-535</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/devteam/top-7-featured-dev-posts-of-the-week-55d8" class="crayons-story__hidden-navigation-link"&gt;Top 7 Featured DEV Posts of the Week&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
      &lt;a href="https://dev.to/devteam/top-7-featured-dev-posts-of-the-week-55d8" class="crayons-article__context-note crayons-article__context-note__feed"&gt;&lt;p&gt;Cyberpunk cat RPGs and robot personalities&lt;/p&gt;

&lt;/a&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;
          &lt;a class="crayons-logo crayons-logo--l" href="/devteam"&gt;
            &lt;img alt="The DEV Team logo" 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%2Forganization%2Fprofile_image%2F1%2Fd908a186-5651-4a5a-9f76-15200bc6801f.jpg" class="crayons-logo__image" width="800" height="800"&gt;
          &lt;/a&gt;

          &lt;a href="/jess" class="crayons-avatar  crayons-avatar--s absolute -right-2 -bottom-2 border-solid border-2 border-base-inverted  "&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%2Fuser%2Fprofile_image%2F264%2Fb75f6edf-df7b-406e-a56b-43facafb352c.jpg" alt="jess profile" class="crayons-avatar__image" width="400" height="400"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/jess" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Jess Lee
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Jess Lee
                &lt;a href="/++"&gt;&lt;img alt="Subscriber" class="subscription-icon" src="https://assets.dev.to/assets/subscription-icon-805dfa7ac7dd660f07ed8d654877270825b07a92a03841aa99a1093bd00431b2.png" width="166" height="102"&gt;&lt;/a&gt;
              
              &lt;div id="story-author-preview-content-3971915" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/jess" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&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%2Fuser%2Fprofile_image%2F264%2Fb75f6edf-df7b-406e-a56b-43facafb352c.jpg" class="crayons-avatar__image" alt="" width="400" height="400"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Jess Lee&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

            &lt;span&gt;
              &lt;span class="crayons-story__tertiary fw-normal"&gt; for &lt;/span&gt;&lt;a href="/devteam" class="crayons-story__secondary fw-medium"&gt;The DEV Team&lt;/a&gt;
            &lt;/span&gt;
          &lt;/div&gt;
          &lt;a href="https://dev.to/devteam/top-7-featured-dev-posts-of-the-week-55d8" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Jun 23&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/devteam/top-7-featured-dev-posts-of-the-week-55d8" id="article-link-3971915"&gt;
          Top 7 Featured DEV Posts of the Week
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag crayons-tag--filled  " href="/t/discuss"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;discuss&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/top7"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;top7&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/devteam/top-7-featured-dev-posts-of-the-week-55d8" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/raised-hands-74b2099fd66a39f2d7eed9305ee0f4553df0eb7b4f11b01b6b1b499973048fe5.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/exploding-head-daceb38d627e6ae9b730f36a1e390fca556a4289d5a41abb2c35068ad3e2c4b5.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;34&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/devteam/top-7-featured-dev-posts-of-the-week-55d8#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              7&lt;span class="hidden s:inline"&gt;&amp;nbsp;comments&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            2 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
      <category>community</category>
      <category>devjournal</category>
      <category>watercooler</category>
      <category>writing</category>
    </item>
    <item>
      <title>Building One Knowledge Graph Across 46 Repositories With Static Analysis (Part 1)</title>
      <dc:creator>Ryosuke Tsuji</dc:creator>
      <pubDate>Mon, 22 Jun 2026 23:54:01 +0000</pubDate>
      <link>https://dev.to/ryantsuji/building-one-knowledge-graph-across-46-repositories-with-static-analysis-part-1-egm</link>
      <guid>https://dev.to/ryantsuji/building-one-knowledge-graph-across-46-repositories-with-static-analysis-part-1-egm</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;AI assistance disclosure: This article was drafted with the help of Claude. All technical content, design decisions, code references, and screenshots reflect production systems I designed and operate at airCloset; the prose was revised by me prior to publication.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Hi, I'm &lt;a href="https://x.com/ryantsuji" rel="noopener noreferrer"&gt;Ryan&lt;/a&gt;, CTO at airCloset.&lt;/p&gt;

&lt;p&gt;This post is about unifying a production codebase spanning &lt;strong&gt;46 repositories&lt;/strong&gt; across multiple services into one knowledge graph, using static analysis.&lt;/p&gt;

&lt;p&gt;Internally we call it &lt;strong&gt;code-graph&lt;/strong&gt;, and I built it between January and March of this year.&lt;/p&gt;

&lt;p&gt;Three things I want to write down:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why "just letting AI read the code" isn't enough, and why I had to chase down the connections that cross repository boundaries&lt;/li&gt;
&lt;li&gt;How I extracted boundaries across 46 repos and a zoo of frameworks (jQuery / AngularJS / Express / NestJS / TypeORM / Redux Axios ...)&lt;/li&gt;
&lt;li&gt;What 3 months of trial and error solved, and what it didn't&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is Part 1, covering the construction of code-graph itself, the painful parts, and the issues that remained. &lt;a href="https://dev.to/ryantsuji/making-the-context-across-46-repositories-semantically-searchable-for-ai-part-2-51d9"&gt;Part 2&lt;/a&gt; is about &lt;strong&gt;service-product-graph (SPG)&lt;/strong&gt; — a layer I built on top of code-graph to compensate for what static analysis couldn't do alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Was This For?
&lt;/h2&gt;

&lt;p&gt;A long-running production codebase usually looks something like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Multiple services and multiple teams touching it&lt;/li&gt;
&lt;li&gt;Each era's framework still alive and mixed in&lt;/li&gt;
&lt;li&gt;Dependencies via API, DB, and Event are &lt;strong&gt;tangled&lt;/strong&gt; — not clean 1:1 front-to-back relationships:

&lt;ul&gt;
&lt;li&gt;The same API gets called from multiple repositories (= n:1 callers)&lt;/li&gt;
&lt;li&gt;The same DB table is written to and read from across multiple repositories (= n:n)&lt;/li&gt;
&lt;li&gt;For Events, just looking at the emit side doesn't tell you how completely the subscribe side is covered — it's practically untraceable&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The starting point was wanting to ask AI: "show me the blast radius," "tell me what breaks if I change this," — across this entire codebase.&lt;/p&gt;

&lt;p&gt;The naive answer is: &lt;strong&gt;"just hand all 46 repositories worth of code to AI and let it analyze."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;But that doesn't work, for two reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Context window&lt;/strong&gt;: 46 repositories × years of accumulated code is just not a size you can hand to an AI in one shot&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hallucination&lt;/strong&gt;: even if you could, "read everything and extract the relationships" is an inference task. It misses things, it makes mistakes. That's not usable for impact analysis on a production system&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the first idea I landed on was: &lt;strong&gt;build a knowledge graph externally, via static analysis&lt;/strong&gt;. That's the starting point of code-graph.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scale: 46 Repositories
&lt;/h2&gt;

&lt;p&gt;The target splits into two graphs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;air-closet graph (37 repos)&lt;/strong&gt;: a graph that spans multiple services like airCloset, Men's, WMS, and more&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;mall graph (9 repos)&lt;/strong&gt;: airCloset Mall and related&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So &lt;strong&gt;46 repositories&lt;/strong&gt; in total.&lt;/p&gt;

&lt;p&gt;The thing to notice is that this isn't "one service with 37 repos." It's a &lt;strong&gt;collection of multiple services&lt;/strong&gt; that adds up to that scale. Making the dependencies that cross service boundaries visible as cross-repo edges is exactly what the &lt;strong&gt;boundary nodes&lt;/strong&gt; discussion below is about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Boundary Nodes Matter
&lt;/h2&gt;

&lt;p&gt;This is the heart of the article.&lt;/p&gt;

&lt;p&gt;When you want AI to understand code, getting it to "read what's in front of it, plus what's next to it" is honestly not hard. grep, open the file, hand it to the model — that works fine.&lt;/p&gt;

&lt;p&gt;For a small codebase, that's enough. But at scale, you hit the context window and hallucination problems mentioned above. I suspect most readers can relate.&lt;/p&gt;

&lt;p&gt;One way to improve this is to &lt;strong&gt;statically analyze the codebase, convert it into a knowledge graph, and serve it to AI through MCP&lt;/strong&gt;. That's the approach.&lt;/p&gt;

&lt;p&gt;The first step was static analysis with &lt;strong&gt;tree-sitter&lt;/strong&gt; (an OSS library that parses source code into syntax trees — it supports a lot of languages and is what VS Code and similar editors use for syntax highlighting; I genuinely recommend it if you want to build something in this space). It's a great tool, but on its own it doesn't solve everything.&lt;/p&gt;

&lt;p&gt;What it doesn't solve is &lt;strong&gt;tracing relationships that cross boundaries — APIs, databases, and so on&lt;/strong&gt;. tree-sitter can extract the relationships between variables, functions, and other in-language constructs. But it can't extract those boundaries.&lt;/p&gt;

&lt;p&gt;The thing that humans and AI alike get stuck on, in practice, is exactly that — code connections that cross boundaries:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The same API is being called from another repo you weren't looking at&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;The frontend in repo A and the nightly batch in repo C might both hit &lt;code&gt;/api/v1/users/me&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Looking at just one of the repos, AI has no way of knowing&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The same DB table is being read or written by some batch process you don't know about&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;When you're modifying service-side code, some batch in a different location might be reading and writing the same table&lt;/li&gt;
&lt;li&gt;Misjudge the blast radius and you get data inconsistency&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The subscribers for this event might not be fully accounted for&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;With distributed pub/sub, looking only at the emit side doesn't let you cover the subscribe side&lt;/li&gt;
&lt;li&gt;Something runs somewhere you don't know about&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In short: getting AI to understand &lt;strong&gt;the code on the other side of a boundary&lt;/strong&gt;, without hallucinating. That's the goal.&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%2Fr9ptjgc8b4o04tahj6qr.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%2Fr9ptjgc8b4o04tahj6qr.png" alt="Boundary nodes bridge code across repository walls" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you have boundary nodes, AI can answer "this API is also called from repo X" &lt;strong&gt;as a fact&lt;/strong&gt;. Instead of asking AI to infer, you hand it &lt;strong&gt;a fact that's already been resolved&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Yes, there is inference during the extraction phase — TypeScript Compiler and Gemini both contribute. But the results are persisted as confirmed values in the graph, and a daily boundary-analysis cron (covered below) lets us notice drift the next morning. By the time AI consumes the graph, only verified facts flow to it.&lt;/p&gt;

&lt;p&gt;AI has a tendency to answer "with whatever it can see" rather than saying "I don't know." That's where silent hallucinations creep in — wrong answers that neither AI nor the human catches. Boundary nodes are what physically prevents that. They give AI a verified place to stand.&lt;/p&gt;

&lt;h2&gt;
  
  
  Construction: tree-sitter Base, With TypeScript Compiler and Gemini Where Needed
&lt;/h2&gt;

&lt;p&gt;Normal code structure (function calls, class inheritance, imports) is &lt;strong&gt;relatively straightforward&lt;/strong&gt; to extract with tree-sitter. Walk the AST, turn functions / methods / classes / fields into nodes, connect references with edges. Just grind through it.&lt;/p&gt;

&lt;p&gt;The catch is that while tree-sitter is great at building syntax trees, it's &lt;strong&gt;weak on type information and scope resolution&lt;/strong&gt;. To accurately follow a field access chain like &lt;code&gt;user.preferences.theme&lt;/code&gt;, you need to resolve what type the variable &lt;code&gt;user&lt;/code&gt; is and where it's defined. tree-sitter alone can't reach that.&lt;/p&gt;

&lt;p&gt;So for field-access resolution we use &lt;strong&gt;TypeScript Compiler API&lt;/strong&gt; and &lt;strong&gt;Gemini&lt;/strong&gt; in combination. tree-sitter extracts the structure → TypeScript Compiler resolves variables and types → for the dynamic cases that even that can't reach, Gemini infers. Three stages with distinct responsibilities, which is how we push field-access accuracy up.&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%2Fkfhv76j1mrukazipy94i.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%2Fkfhv76j1mrukazipy94i.png" alt="3-stage field access resolution: tree-sitter, TypeScript Compiler, Gemini" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We define 21 edge types:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;CALLS&lt;/code&gt; (function call) / &lt;code&gt;EXTENDS&lt;/code&gt; (inheritance) / &lt;code&gt;IMPLEMENTS&lt;/code&gt; (interface implementation), etc. — the basic structure tree-sitter can give us&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CALLS_API&lt;/code&gt; (caller) / &lt;code&gt;HANDLES_API&lt;/code&gt; (handler) — API boundary&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;EMITS_TO&lt;/code&gt; (emitter) / &lt;code&gt;SUBSCRIBES_TO&lt;/code&gt; (subscriber) — Event boundary&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;WRITES_TO&lt;/code&gt; / &lt;code&gt;READS_FROM&lt;/code&gt; — DB boundary&lt;/li&gt;
&lt;li&gt;and more&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The real battle starts when you try to extract the boundary edges (&lt;code&gt;CALLS_API&lt;/code&gt; / &lt;code&gt;HANDLES_API&lt;/code&gt; / &lt;code&gt;EMITS_TO&lt;/code&gt; / &lt;code&gt;SUBSCRIBES_TO&lt;/code&gt; / &lt;code&gt;WRITES_TO&lt;/code&gt; / &lt;code&gt;READS_FROM&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Extracting and Joining Boundary Nodes: 3 Months of Trial and Error (Jan–Mar)
&lt;/h2&gt;

&lt;p&gt;Unlike normal code, boundaries (API endpoints, DB tables, Event topics) are &lt;strong&gt;written in wildly different ways&lt;/strong&gt; depending on the framework, language, technical area, library, repository, and the person who wrote it.&lt;/p&gt;

&lt;p&gt;Take "define an API endpoint": is it Express? NestJS with a &lt;code&gt;@Get()&lt;/code&gt; decorator? A Fastify route? Each one produces a completely different AST shape. And the same repo can contain multiple patterns simultaneously.&lt;/p&gt;

&lt;p&gt;And it's not just extraction that's hard. &lt;strong&gt;Joining the extracted boundaries on the graph&lt;/strong&gt; is its own headache. For the same API path or DB table name, you get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Casing variation: camelCase / snake_case / PascalCase&lt;/li&gt;
&lt;li&gt;Trailing-slash variation (&lt;code&gt;/users/me&lt;/code&gt; vs. &lt;code&gt;users/me&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;The boundary name itself is a variable (&lt;code&gt;${baseUrl}/users/me&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;…all mixed together. Normalizing all of that and correctly joining caller to handler, emitter to subscriber, writer to reader was genuinely the painful part.&lt;/p&gt;

&lt;p&gt;And this had to happen across all 46 repositories × the framework zoo.&lt;/p&gt;

&lt;p&gt;Looking back at the actual git history from that period, you see new parsers and detectors being added almost every week, noise filters going in, and concept renames landing. Here are the main commits from January through March, in order (the commit prefix starts as &lt;code&gt;graph-rag&lt;/code&gt; — the stack was originally named after the "knowledge graph + RAG for LLM consumption" framing — and is renamed to &lt;code&gt;code-graph&lt;/code&gt; on February 15; a few late-February commits still carry a short-lived &lt;code&gt;graph&lt;/code&gt; prefix from that transition):&lt;/p&gt;

&lt;h3&gt;
  
  
  January: Starting Out, and Realizing tree-sitter Alone Isn't Enough
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;2026-01-15&lt;/strong&gt; ─ &lt;code&gt;feat(graph-rag): add TypeScript parser with tree-sitter&lt;/code&gt; — the starting commit&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2026-01-15&lt;/strong&gt; ─ &lt;code&gt;feat(graph-rag): add graph builder with BigQuery storage&lt;/code&gt; — graph data is written to BigQuery&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2026-01-19&lt;/strong&gt; ─ &lt;code&gt;feat(graph-rag): add TypeScript Compiler-based variable resolution for field extraction&lt;/code&gt; — realized that &lt;strong&gt;tree-sitter alone couldn't resolve variable types&lt;/strong&gt;, brought in the TypeScript Compiler API alongside it&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  February: Framework Diversity, Fighting Noise
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;2026-02-02&lt;/strong&gt; ─ &lt;code&gt;feat(graph-rag): add frontend parser for jQuery/Vanilla JS codebase&lt;/code&gt; — &lt;strong&gt;jQuery / Vanilla JS&lt;/strong&gt; frontend code&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2026-02-03&lt;/strong&gt; ─ &lt;code&gt;feat(graph-rag): add AngularJS Page detection for frontend BFS&lt;/code&gt; — &lt;strong&gt;AngularJS&lt;/strong&gt; page detection (older framework, still very much running)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2026-02-15&lt;/strong&gt; ─ &lt;code&gt;refactor(code-graph): consolidate 18 MCP tools into 5 with deep subgraph traversal&lt;/code&gt; — the toolset had ballooned to 18, consolidated to 5 (also the moment the stack was unified under the name &lt;code&gt;code-graph&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2026-02-18&lt;/strong&gt; ─ &lt;code&gt;fix(code-graph): reduce graph noise by filtering Type nodes, external lib CALLS, and Storybook files&lt;/code&gt; — noise reduction: filter out Type nodes, external library CALLS, Storybook files&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2026-02-19&lt;/strong&gt; ─ &lt;code&gt;fix(code-graph): extract path aliases from tsconfig paths in addition to make-symlink&lt;/code&gt; + &lt;code&gt;fix(code-graph): resolve @alias path imports for CommonJS symlink patterns&lt;/code&gt; — &lt;strong&gt;the path-alias pain&lt;/strong&gt;: tsconfig paths, make-symlink, and on top of that the CommonJS symlink pattern — three different mechanisms to support&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2026-02-19&lt;/strong&gt; ─ &lt;code&gt;feat(code-graph): add stop_at=boundary option to trace_connections&lt;/code&gt; — option to stop traversal at boundary nodes (explicit traversal scoping / node-explosion mitigation)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2026-02-21&lt;/strong&gt; ─ &lt;code&gt;feat(graph): add typeORM JOIN detection, NestJS decorator parsing, Fetcher API detection&lt;/code&gt; — &lt;strong&gt;TypeORM JOINs / NestJS decorators / Fetcher API&lt;/strong&gt; support&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2026-02-21&lt;/strong&gt; ─ &lt;code&gt;fix(graph): pass fullFileCode to Redux Axios variable resolver for scope-based extraction&lt;/code&gt; — &lt;strong&gt;Redux Axios&lt;/strong&gt; variable resolver fix&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  March: Concept Cleanup and Precision
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;2026-03-08&lt;/strong&gt; ─ &lt;code&gt;refactor(code-graph): rename __external__ to __boundary__&lt;/code&gt; — &lt;strong&gt;concept cleanup&lt;/strong&gt;: standardize on "boundary node" rather than "external resource"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2026-03-16&lt;/strong&gt; ─ &lt;code&gt;refactor: remove db-dictionary from code-graph stack&lt;/code&gt; — split the DB schema dictionary (the layer that lets you look up table / column definitions) off into its own graph to evolve independently&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2026-03-24&lt;/strong&gt; ─ &lt;code&gt;fix(code-graph): infer table names from dynamic variable names&lt;/code&gt; — table-name inference from dynamic variable names&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2026-03-24&lt;/strong&gt; ─ &lt;code&gt;feat(code-graph): add orphan boundary node cleanup script&lt;/code&gt; — cleanup script for orphan boundary nodes&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What This Timeline Tells You
&lt;/h3&gt;

&lt;p&gt;Every single week there's a new framework or pattern being handled. The work of "extracting boundary nodes" is, fundamentally, &lt;strong&gt;adding parsers for each new way people write the boundary&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Just listing the frameworks / mechanisms that showed up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;tree-sitter (TypeScript / JavaScript / Go / Dart (Flutter))&lt;/li&gt;
&lt;li&gt;TypeScript Compiler (variable resolution)&lt;/li&gt;
&lt;li&gt;jQuery / Vanilla JS&lt;/li&gt;
&lt;li&gt;AngularJS&lt;/li&gt;
&lt;li&gt;Express / Koa / Fastify&lt;/li&gt;
&lt;li&gt;NestJS (decorator parsing)&lt;/li&gt;
&lt;li&gt;TypeORM (DB JOIN detection)&lt;/li&gt;
&lt;li&gt;Fetcher API&lt;/li&gt;
&lt;li&gt;Redux Axios (variable resolver)&lt;/li&gt;
&lt;li&gt;3 different path-alias schemes (tsconfig paths / make-symlink / CommonJS symlink)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This isn't a "TypeScript / JavaScript / Go / Dart static analysis" story you can wrap up in one sentence. The air-closet codebase is a collection of long-running production systems where every era's framework still coexists. We had to pick up, from the AST, the era-specific meaning of "here's an API endpoint," "here's a DB call," "here's an Event subscription."&lt;/p&gt;

&lt;h3&gt;
  
  
  Why I Was So Particular About Accuracy
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;90% is completely unusable.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Take "list every piece of code that calls this API." If you recall only 90% of the callers, then 10% of the relevant code is invisible to AI. When you're using code-graph for blast-radius investigation, &lt;strong&gt;that invisible 10% is what causes the incident&lt;/strong&gt;. That's single-hop recall.&lt;/p&gt;

&lt;p&gt;And it gets worse the further you walk. For multi-hop graph traversal, every hop multiplies in: at 0.9 per hop you get 0.81 at 2 hops, 0.729 at 3, ~0.59 at 5, ~0.35 at 10 — after just a handful of hops you're at less than half. Push it to 0.99 and you get 0.98 at 2 hops, 0.95 at 5, ~0.90 at 10. &lt;strong&gt;Whether the system is usable in practice is decided by that single-digit difference between 90% and 99%&lt;/strong&gt; — and it bites you on both axes: single-hop recall when you're enumerating, multi-hop confidence when you're traversing.&lt;/p&gt;

&lt;p&gt;So every time a new boundary pattern showed up, we'd add a new custom parser, &lt;strong&gt;aiming to keep the boundary connection rate above 99%&lt;/strong&gt;. We can't measure extraction recall directly — there's no ground-truth "every boundary that should exist" denominator — so the indicator we actually measure daily is "what fraction of callers / handlers are correctly connected on the graph" = the connection rate. The next section is about how that's monitored.&lt;/p&gt;

&lt;h2&gt;
  
  
  Boundary Analysis Is Running Today
&lt;/h2&gt;

&lt;p&gt;The code-graph we built is still running daily.&lt;/p&gt;

&lt;p&gt;Concretely, a &lt;strong&gt;boundary-analysis cron&lt;/strong&gt; runs at JST 7:00 every morning. What it does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;API boundaries&lt;/strong&gt;: match &lt;code&gt;CALLS_API&lt;/code&gt; (caller) with &lt;code&gt;HANDLES_API&lt;/code&gt; (handler), and aggregate cross-repo connection rates&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Event boundaries&lt;/strong&gt;: match &lt;code&gt;EMITS_TO&lt;/code&gt; (emit) with &lt;code&gt;SUBSCRIBES_TO&lt;/code&gt; (subscribe)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DB boundaries&lt;/strong&gt;: aggregate cases where &lt;code&gt;WRITES_TO&lt;/code&gt; and &lt;code&gt;READS_FROM&lt;/code&gt; from &lt;strong&gt;different repositories&lt;/strong&gt; touch the same table (= implicit cross-repo DB dependency)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The day-over-day numbers get compared, and if the connection rate drops by more than 5%, we get a Grafana alert.&lt;/p&gt;

&lt;p&gt;This whole thing only makes sense &lt;strong&gt;because we have boundary nodes to compare against&lt;/strong&gt;. We're monitoring the &lt;strong&gt;quality of the extracted boundaries themselves&lt;/strong&gt; on a daily cadence. The kind of drift the connection rate catches by the next morning: "a parser fell behind a new pattern and a class of boundaries went invisible," "the repository layout changed and path aliases stopped resolving." There are failure modes the connection rate alone can't see — a caller-side parser regression that drops callers entirely will leave the surviving handlers still looking "connected" to whatever callers remain, and the missing ones slip out silently. That's a separate axis we cover with day-over-day absolute node counts per repo / pattern.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Still Doesn't Work
&lt;/h2&gt;

&lt;p&gt;Even after all that, a handful of issues remain that I can't solve at the root.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. No Semantic Search (an Entry-Point Problem)
&lt;/h3&gt;

&lt;p&gt;The search MCP tool only does LIKE-based substring matching.&lt;/p&gt;

&lt;p&gt;If you're in the middle of development and want to follow connections starting from a function you're already looking at, that's fine — you can pull it up by function name or filename directly.&lt;/p&gt;

&lt;p&gt;The problem shows up when you're investigating a production bug or a customer support ticket. You have no idea what filenames or function names are involved at the start. When the input is "the subscription-fee calculation for members seems off," and you want to walk to the related code from there, no natural-language query into the graph means &lt;strong&gt;you can't find the entry point in the first place&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The intent was: "instead of grepping the whole codebase, navigate relevance via graph RAG." What we ended up with is a structure where you have to grep at the entry point and infer your way in.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Node Explosion
&lt;/h3&gt;

&lt;p&gt;If you naively turn the AST into a graph, every builtin function, anonymous function, and internal utility becomes a node. The &lt;code&gt;map&lt;/code&gt; call you don't care about, the internal helper you don't care about — they're all nodes.&lt;/p&gt;

&lt;p&gt;Trigger a traversal starting from one node, and within a few hops you're dragging in helpers, types, and primitives until the node count explodes. There's no axis built into the graph structure for "filter by relevance."&lt;/p&gt;

&lt;p&gt;We work around it with explicit controls like stopping traversal at boundary nodes, but that's a workaround, not a root fix.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. To Know What a Function Actually Does, You Still Have to Read the File
&lt;/h3&gt;

&lt;p&gt;The graph tells you "something is here," "this calls out to another repo." But what the function actually &lt;strong&gt;does&lt;/strong&gt; still requires opening the file.&lt;/p&gt;

&lt;p&gt;That makes the graph slow on its own. The codebase-investigation tool we built later uses the graph to narrow down candidate files and then hands those to Git Server MCP to actually read — but the underlying graph-only resolution limit doesn't go away.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Operational Cost of Adding Parsers for Every New Boundary Pattern
&lt;/h3&gt;

&lt;p&gt;Every time a new framework or library enters the codebase, we have to learn "how do they write boundaries in this thing" and add a new parser.&lt;/p&gt;

&lt;p&gt;The parser directory already has 10+ custom detectors / extractors. There's no sign of the maintenance and extension cost going down — &lt;strong&gt;every time a new tech stack enters the codebase, the same work repeats&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Side Note: A Different Call Elsewhere — cortex
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: "cortex" in this section is the internal codename for an AI platform I've been building in-house at airCloset. Unrelated to existing commercial products like Snowflake Cortex or Palo Alto Networks Cortex.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Setting code-graph aside for a moment: I also have a separate project — &lt;strong&gt;cortex&lt;/strong&gt; — where I'm building an in-house AI platform from scratch (currently a single monorepo with 100+ apps).&lt;/p&gt;

&lt;p&gt;On that project I did initially try the same approach as code-graph, but bailed out early and went with an &lt;strong&gt;annotation-based knowledge graph&lt;/strong&gt; instead:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It's a monorepo I'm assembling myself, so I can realistically annotate everything at once&lt;/li&gt;
&lt;li&gt;Use JSDoc tags to write intent directly into the code, and build the graph from that&lt;/li&gt;
&lt;li&gt;Vectorize that intent and store it on the node, so semantic search works&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The decision to "write intent into the code and graph it" — and the trial and error that led to it — I covered in detail in a separate series. If interested: &lt;a href="https://dev.to/ryantsuji/the-heart-of-the-ai-harness-a-knowledge-graph-of-the-ai-by-the-ai-for-the-ai-series-part-2-53bm"&gt;AI Harness Series, Part 2 (The Knowledge Graph at the Heart of cortex)&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Annotation-Based Won't Work for Production Systems
&lt;/h2&gt;

&lt;p&gt;And no, you can't take the same approach for the production-side codebase that code-graph deals with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Annotating all 46 repos at once isn't realistic&lt;/li&gt;
&lt;li&gt;Long-running production systems, touched by multiple teams, with mixed frameworks&lt;/li&gt;
&lt;li&gt;The precondition "put annotations into the code" doesn't hold&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the choice was: &lt;strong&gt;keep code-graph (static analysis) as the base, and evolve by layering on additional graph layers&lt;/strong&gt; to compensate.&lt;/p&gt;

&lt;p&gt;How we're trying to solve the issues above, I cover in &lt;a href="https://dev.to/ryantsuji/making-the-context-across-46-repositories-semantically-searchable-for-ai-part-2-51d9"&gt;Part 2&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  To Be Continued
&lt;/h2&gt;

&lt;p&gt;That's it for Part 1. &lt;a href="https://dev.to/ryantsuji/making-the-context-across-46-repositories-semantically-searchable-for-ai-part-2-51d9"&gt;Part 2&lt;/a&gt; covers how we try to get past the issues above.&lt;/p&gt;

&lt;p&gt;The real story is less "thrown away" and more "&lt;strong&gt;evolved&lt;/strong&gt;."&lt;/p&gt;

&lt;p&gt;Thanks for reading this far.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>knowledgegraph</category>
      <category>staticanalysis</category>
      <category>typescript</category>
    </item>
    <item>
      <title>AI Isn't Something to Trust — It's Something to Design (Series Final)</title>
      <dc:creator>Ryosuke Tsuji</dc:creator>
      <pubDate>Tue, 16 Jun 2026 00:02:03 +0000</pubDate>
      <link>https://dev.to/ryantsuji/ai-isnt-something-to-trust-its-something-to-design-series-final-30aa</link>
      <guid>https://dev.to/ryantsuji/ai-isnt-something-to-trust-its-something-to-design-series-final-30aa</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;AI assistance disclosure: This article was drafted with the help of Claude. All technical content, design decisions, code references, and screenshots reflect production systems I designed and operate at airCloset; the prose was revised by me prior to publication.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Hi, I'm &lt;a href="https://x.com/ryantsuji" rel="noopener noreferrer"&gt;Ryan&lt;/a&gt;, CTO at airCloset.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer&lt;/strong&gt;: "cortex" in this article is the internal codename for an AI platform built in-house at airCloset. It is unrelated to existing commercial services like Snowflake Cortex or Palo Alto Networks Cortex.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Across the five posts of this series I've worked through how cortex's harness is put together, one piece at a time: the overall picture, the knowledge graph, Auto Review, Self-Healing + Recurrence Prevention, and non-engineer PRs. Having walked through all of them, I want to step one level down for the wrap-up. &lt;strong&gt;Why am I building this thing in the first place?&lt;/strong&gt; That's what this post is about.&lt;/p&gt;

&lt;p&gt;The five posts might look independent, but the root is one thing, and the series doesn't close cleanly without that one thing being put into words. Together with the philosophy, I want to look back at the failures that don't show up when you only write about what worked — what I threw away, where I tripped — as a reference point for anyone trying something similar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Series Index
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Theme&lt;/th&gt;
&lt;th&gt;Key scene&lt;/th&gt;
&lt;th&gt;Article&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Series intro: cortex's harness&lt;/td&gt;
&lt;td&gt;PRs auto-merge / incidents self-heal before you notice&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ryantsuji/building-a-real-ai-harness-auto-reviewed-prs-self-healing-ops-and-non-engineer-contributors-3lfa"&gt;ai-harness-intro&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Product Graph (cpg)&lt;/td&gt;
&lt;td&gt;Code, docs, DB, infra unified into one graph&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ryantsuji/the-heart-of-the-ai-harness-a-knowledge-graph-of-the-ai-by-the-ai-for-the-ai-series-part-2-53bm"&gt;cortex-product-graph&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;AI PR review&lt;/td&gt;
&lt;td&gt;webhook → AI review → auto-fix → squash merge&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ryantsuji/human-on-the-loop-ai-reviewing-ai-prs-at-cortex-769-prsmonth-while-raising-the-quality-bar-4lh5"&gt;cortex-auto-review&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Self-Healing + observability + auto-added guardrails&lt;/td&gt;
&lt;td&gt;Alert → AI investigates → fix PR + new lint/type gate → auto redeploy&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ryantsuji/fixed-before-anyone-notices-stronger-after-every-fix-self-healing-recurrence-prevention-series-1e86"&gt;cortex-self-healing&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Democratizing the maintenance phase&lt;/td&gt;
&lt;td&gt;Domain experts open PRs to production; the harness owns the quality gate&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ryantsuji/the-author-doesnt-have-to-be-an-engineer-how-the-harness-holds-quality-series-part-5-12e4"&gt;cortex-non-engineer-prs&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Series wrap-up&lt;/td&gt;
&lt;td&gt;The underlying philosophy plus a retrospective on the failures and lessons&lt;/td&gt;
&lt;td&gt;This post ← you are here&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Origin — What I Was Thinking About in 2025
&lt;/h2&gt;

&lt;p&gt;When I started building cortex, there was one question I wanted to answer:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;How do I get AI to understand the system accurately?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If AI could understand the system accurately, then PR review, bug investigation, and fixes could all be delegated, and even non-engineers could open up their own development. Conversely, as long as I was stuck on "understand it accurately," everything downstream was sitting on unstable ground. So I spent a lot of time on &lt;strong&gt;the prerequisite layer&lt;/strong&gt; before any of the individual mechanisms.&lt;/p&gt;

&lt;p&gt;The two obvious approaches both hit walls.&lt;/p&gt;

&lt;h3&gt;
  
  
  Wall 1: The Context Window Limit
&lt;/h3&gt;

&lt;p&gt;The first reflex is "just give it all the information it might need." Stuff the codebase, docs, DB schema, infra definitions all into the prompt, and AI gets the whole picture.&lt;/p&gt;

&lt;p&gt;That fails on size. Codebase + docs + schemas + infra at our company doesn't come close to fitting into any realistic context window.&lt;/p&gt;

&lt;p&gt;"Surely context windows will keep growing, and this'll work eventually?" — the more I thought about it, the less of a future I saw in that direction.&lt;/p&gt;

&lt;p&gt;Even with a model whose context window is very large like Gemini, behavior gets unstable when you push it close to the limit. Middle information gets dropped, irrelevant tokens skew the conclusion sideways. This isn't a model-selection problem; it's a structural attention problem. The more unrelated tokens you mix in, the more the attention ratio toward relevant tokens drops mechanically. This is the documented &lt;strong&gt;"lost in the middle"&lt;/strong&gt; phenomenon (information placed at the start and end of long inputs gets used; &lt;strong&gt;information placed in the middle is effectively ignored&lt;/strong&gt;), and &lt;strong&gt;stuff the context window full and you routinely end up in a state where the information you thought you handed over isn't actually visible to the model&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;"Lost in the middle" itself may get mitigated as long-context models improve, so I treat it as empirical supporting evidence rather than the core argument. The real wall is the &lt;strong&gt;recursive&lt;/strong&gt; one beneath it: even if "size" is solved, you immediately need &lt;strong&gt;a higher-level context to judge which tokens are necessary and which aren't&lt;/strong&gt;. That problem is recursive and &lt;strong&gt;can't be resolved by context window size, in principle&lt;/strong&gt;. Information has to be structured, or AI doesn't make correct judgments. That's true of humans too — but humans are a notch better off, because &lt;strong&gt;LLMs don't notice they don't know, and they answer with confidence anyway&lt;/strong&gt;. Silently wrong is worse than visibly stuck.&lt;/p&gt;

&lt;p&gt;The keep-growing-context-windows path didn't have a real resolution in sight.&lt;/p&gt;

&lt;h3&gt;
  
  
  Wall 2: Don't Lean on Learning Either
&lt;/h3&gt;

&lt;p&gt;The other obvious move is to make AI itself learn. Fine-tune per organization, teach it our codebase, our docs, our business. I considered it. Currently not doing it.&lt;/p&gt;

&lt;p&gt;Two reasons. One: getting learning into actual production was still research-phase (in 2025 then; still in 2026 as I write this) and the road to real deployment is still long. The other is thornier: &lt;strong&gt;even if you could learn it, "forgetting" is extremely hard&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A business system has to reflect "the current truth." When the design changes, the DB schema changes, the business rules change, &lt;strong&gt;you want to actively erase old knowledge&lt;/strong&gt;. But "delete just this piece of what's baked into the LLM weights" is unsolved at the research level — there's even a field name for it, &lt;strong&gt;machine unlearning&lt;/strong&gt;, which tells you how hard it is. And on top of that, teaching the model new things also &lt;strong&gt;destroys unrelated existing knowledge&lt;/strong&gt; (called &lt;strong&gt;destructive interference&lt;/strong&gt; / catastrophic forgetting). Lean on learning and both hit at once: the cost of keeping things consistent explodes.&lt;/p&gt;

&lt;p&gt;Rather than treating "doesn't learn" as a downside, I came around to: &lt;strong&gt;because it doesn't learn, swapping out the external knowledge is enough to reflect the current state, and the consistency story is much simpler&lt;/strong&gt;. That was the call at the time.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Way Out — GraphRAG + MCP
&lt;/h3&gt;

&lt;p&gt;With no future in the context-window direction or the learning direction, I came across the &lt;strong&gt;GraphRAG&lt;/strong&gt; concept.&lt;/p&gt;

&lt;p&gt;GraphRAG itself is widely discussed elsewhere; for me, what it meant was the framing: "&lt;strong&gt;supply only the context that's needed, at the moment it's needed&lt;/strong&gt;." Combined with &lt;strong&gt;MCP&lt;/strong&gt; (Anthropic's protocol for connecting LLMs to external tools), AI can go fetch what it needs on its own.&lt;/p&gt;

&lt;p&gt;What was decisive was that this structure lets AI traverse the graph agentically. Rather than "read everything and find related parts by inference," AI &lt;strong&gt;gets to the node it needs and pulls the fact out&lt;/strong&gt;. Which leads to:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Instead of making AI infer, supply facts as context.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That one sentence became the core of cortex's entire design philosophy.&lt;/p&gt;

&lt;p&gt;The first thing I built was a static-analysis-based &lt;strong&gt;code-graph&lt;/strong&gt;, which I then threw away after trial and error, and arrived at the annotation-based &lt;strong&gt;product-graph (cpg)&lt;/strong&gt; — details in the trial-and-error section.&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%2F416pzfhy6z28i4wa2g72.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%2F416pzfhy6z28i4wa2g72.png" alt="2025 origin. Neither growing the context window nor relying on learning had a future; GraphRAG + MCP became the way through." width="800" height="490"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  I Don't Trust AI to Begin With
&lt;/h2&gt;

&lt;p&gt;The origin section in one line:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;I don't trust AI to fill in the blanks for me.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;"Don't trust" here is not the same as "have no faith in." This isn't doubting Claude / GPT / Gemini's generation quality. What I mean is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;It doesn't know context it wasn't handed.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;It doesn't, on its own and without being told, produce the ideal state.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first one is a truth no amount of model progress will change. Architecturally, LLMs can't know things that weren't in the training data and aren't in this session's context. "Surely smarter models will pick up on it" — I don't think that future is coming. Smarter is a real direction; smart alone doesn't compensate for not knowing.&lt;/p&gt;

&lt;p&gt;The second one is about responsibility, and humans owning it. AI can't decide on its own what "ideal" means. When it tries, it lands on a generic best-practice answer slightly off from the actual situation. Ideal depends on the business, the organization, the moment in time — none of which is visible to AI unless a human verbalizes it and hands it over.&lt;/p&gt;

&lt;p&gt;So that conviction is &lt;strong&gt;not underestimating AI's capability; it's a design decision to not let AI auto-complete the prerequisites&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Mastering AI is not about giving it freedom — it's about confining its output to a predictable range.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The mechanism for confining it is the harness this series has been describing.&lt;/p&gt;

&lt;h2&gt;
  
  
  So I Build Harnesses to Hold AI to Determinism
&lt;/h2&gt;

&lt;p&gt;Reading each post through the lens of "don't make AI infer; lean on determinism" surfaces that the five of them are all the same conviction showing up in different layers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 2 — Knowledge Graph&lt;/strong&gt;: Instead of making AI search the codebase, this mechanism tilts toward making the codebase legible. With &lt;code&gt;@graph-*&lt;/code&gt; annotations, code / docs / DB / infra are unified into one graph, so AI doesn't have to grep + infer to find related parts. This is the direct implementation of "supply facts as context" from the origin section. → &lt;a href="https://dev.to/ryantsuji/the-heart-of-the-ai-harness-a-knowledge-graph-of-the-ai-by-the-ai-for-the-ai-series-part-2-53bm"&gt;cortex-product-graph&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 3 — Auto Review Dimensions&lt;/strong&gt;: Nine review dimensions (responsibility / severity / type SSoT / etc.) are fixed in advance. When AI does the review, what to check isn't something it gets to infer. "Looking at the PR as a whole" gives AI too much room for inference, so dimensions are split and &lt;strong&gt;each is judged as its own question&lt;/strong&gt;. &lt;strong&gt;Dimensions = locked by the harness, evaluation = AI's job.&lt;/strong&gt; → &lt;a href="https://dev.to/ryantsuji/human-on-the-loop-ai-reviewing-ai-prs-at-cortex-769-prsmonth-while-raising-the-quality-bar-4lh5"&gt;cortex-auto-review&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 4 — Self-Healing + Recurrence Prevention&lt;/strong&gt;: Alert → investigation → fix PR → redeploy. The flow itself is fixed. AI doesn't get to think through "how should we respond to incidents" each time. And Recurrence Prevention — adding lint / CI gates so the same trap can't be stepped on twice — is &lt;strong&gt;mechanical refusal at the gate, not trust-AI-not-to-do-it-again&lt;/strong&gt;. Or put differently: I don't expect AI never to repeat a mistake. → &lt;a href="https://dev.to/ryantsuji/fixed-before-anyone-notices-stronger-after-every-fix-self-healing-recurrence-prevention-series-1e86"&gt;cortex-self-healing&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 5 — Non-Engineer PRs&lt;/strong&gt;: If the harness weren't holding quality, business-side folks opening PRs directly to production wouldn't survive a single day. Conversely, with the three mechanisms above stacked up (context locked, dimensions locked, traps locked out mechanically), the person closest to the requirements can ship the change directly. The translation layer and the engineering priority queue disappeared as a downstream consequence of the determinism push. → &lt;a href="https://dev.to/ryantsuji/the-author-doesnt-have-to-be-an-engineer-how-the-harness-holds-quality-series-part-5-12e4"&gt;cortex-non-engineer-prs&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So what's covered across the five posts is "don't make AI infer; lean on determinism" implemented at different layers. The root is one conviction.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "Don't Make AI Infer, Lean on Determinism" Actually Means
&lt;/h2&gt;

&lt;p&gt;Let me sharpen this phrase that's come up a few times.&lt;/p&gt;

&lt;p&gt;"Lean on determinism" does &lt;strong&gt;not&lt;/strong&gt; mean "give AI zero room to infer." Code generation, judging review findings, hypothesizing root causes from error logs — these are domains where AI not inferring is the same as no work getting done.&lt;/p&gt;

&lt;p&gt;Where I want to lean on determinism is in domains where &lt;strong&gt;variance isn't allowed&lt;/strong&gt;. Specifically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Which part of the codebase to look at&lt;/strong&gt; — don't have AI guess by analogy; pull it deterministically from the knowledge graph&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Which review dimensions to apply&lt;/strong&gt; — don't let AI pick "the important-looking dimensions"; lock the dimension list in advance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How to respond to incidents&lt;/strong&gt; — don't make AI think through the workflow each time; fix the alert → fix PR path&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not stepping on the same trap twice&lt;/strong&gt; — don't ask AI to "try to be careful"; let lint / CI mechanically refuse it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What implements this line — where inference is allowed vs. where it isn't — is the harness. To borrow the metaphor from Part 5, the harness lays down &lt;strong&gt;rails you can't fall off&lt;/strong&gt;. On top of the rails, AI runs free (inference works as inference); but it can't fall off the rails sideways.&lt;/p&gt;

&lt;p&gt;Put differently, this is equivalent to the framing in &lt;a href="https://dev.to/ryantsuji/the-heart-of-the-ai-harness-a-knowledge-graph-of-the-ai-by-the-ai-for-the-ai-series-part-2-53bm"&gt;Part 2 (cortex-product-graph)&lt;/a&gt;: "where hallucination gets confined." Saying "no inference allowed" isn't quite right — the harness isn't a thing that makes hallucination go to zero. It's a thing that confines hallucination to places where hallucination is OK (i.e., the inference-allowed zone). The structure and facts about the codebase are pulled deterministically, so &lt;strong&gt;the retrieval process itself has no opening for hallucination&lt;/strong&gt;; hallucinations on the judgment side get filtered downstream by tests / lint / dimension-by-dimension reviews. The places where hallucination is allowed and the places where it isn't are &lt;strong&gt;physically split by the harness&lt;/strong&gt;. That's the continuation of the Part 2 framing.&lt;/p&gt;

&lt;p&gt;Step back one more level and what the harness is really doing is &lt;strong&gt;shifting when inference happens&lt;/strong&gt;. The annotations and descriptions on the graph were also written by AI originally — there is inference baked into them. But that inference is &lt;strong&gt;write-time&lt;/strong&gt; — happens once, reviewed, then frozen — not &lt;strong&gt;read-time&lt;/strong&gt; (happens every query, &lt;strong&gt;unverified at the point of use&lt;/strong&gt;). The graph is &lt;strong&gt;frozen, reviewed inference&lt;/strong&gt;, which is exactly why the read side can treat it as fact. "Leaning on determinism" can be rephrased as &lt;strong&gt;not letting unverified inference run on every query&lt;/strong&gt;.&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%2Fiffzn4u10c1qk6t6dgze.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%2Fiffzn4u10c1qk6t6dgze.png" alt="Inference-allowed zone (top, green) and inference-forbidden zone (bottom, orange). The harness implements this boundary — i.e., decides where hallucination gets confined." width="800" height="490"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is also the underlying basis for &lt;a href="https://dev.to/ryantsuji/building-a-real-ai-harness-auto-reviewed-prs-self-healing-ops-and-non-engineer-contributors-3lfa"&gt;Part 1 (Series Intro)&lt;/a&gt;'s claim "models commoditize; harnesses differentiate." Model-side quality is converging across Claude / GPT / Gemini, but the harness is &lt;strong&gt;codebase-specific and business-specific&lt;/strong&gt;, so this is where org-level differentiation actually comes from.&lt;/p&gt;

&lt;p&gt;Worth flagging: &lt;strong&gt;the position of this boundary moves with model capability&lt;/strong&gt;. As agentic search and reasoning get stronger, today's "must be deterministic" zone might be tomorrow's "inference is good enough" zone — and in fact cortex itself depends on AI's ability to traverse the graph agentically. But &lt;strong&gt;the boundary itself never disappears&lt;/strong&gt;. How information is structured, where the line gets drawn between fact and inference — &lt;strong&gt;whether you hold that line explicitly as a design decision&lt;/strong&gt; is what differentiates organizations, across every model generation.&lt;/p&gt;

&lt;p&gt;A note: this framing isn't confined to cortex's harness. The same stance shapes &lt;a href="https://dev.to/ryantsuji/democratizing-internal-data-building-an-mcp-server-that-lets-you-search-991-tables-in-natural-1da5"&gt;db-graph MCP&lt;/a&gt;, the natural-language interface over internal DB schemas, and &lt;a href="https://dev.to/ryantsuji/bridging-i-want-to-build-and-i-want-to-publish-safely-for-non-engineers-sandbox-mcp-392a"&gt;Sandbox MCP&lt;/a&gt;, which lets non-engineers safely publish AI-built apps. &lt;strong&gt;It's the through-line in any platform we build that's based on AI doing meaningful work.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One level more abstract: &lt;strong&gt;the individual features aren't where the value is.&lt;/strong&gt; The value sits in the conviction itself. cortex / db-graph / Sandbox MCP are all that one conviction translated into our own use cases.&lt;/p&gt;

&lt;p&gt;The way I think about "design":&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Design is translating an abstract principle into a concrete implementation that fits your own use cases.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It's not drawing class diagrams, and it's not laying out architecture diagrams — it's the translation work of &lt;strong&gt;"how does this principle take shape under our business / codebase / constraints?"&lt;/strong&gt; That's where each organization's distinctiveness lives, and that's the value that can't be copied.&lt;/p&gt;

&lt;p&gt;Said the other way: another organization copying cortex's surface doesn't reproduce the substance. What gets asked of every org is &lt;strong&gt;how it translates this principle into its own use cases&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trial and Error That Got Me Here
&lt;/h2&gt;

&lt;p&gt;Everything I've written about above is the form that ended up working. Getting to that form involved &lt;strong&gt;a lot of throwing away&lt;/strong&gt;. Two representative examples worth keeping on record, plus one shorter one.&lt;/p&gt;

&lt;h3&gt;
  
  
  I Spent Two Months on Static-Analysis code-graph, Then Threw It Out
&lt;/h3&gt;

&lt;p&gt;The first thing I built was static-analysis-based &lt;strong&gt;code-graph&lt;/strong&gt;: extracting AST data — imports, call graphs, type dependencies — and putting that into a graph DB. At a glance, the obvious implementation of "make AI understand the codebase."&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why two months?&lt;/strong&gt; code-graph wasn't just cortex; it spanned our consumer-facing services and internal-system repositories too — &lt;strong&gt;over 40 repos in total&lt;/strong&gt; (cortex being one of them). The mechanically-extractable AST data (imports / call graphs / type dependencies) was usable as-is via tree-sitter, but each repo had its own API endpoints / DB schema / event definitions / Pub/Sub topology, and &lt;strong&gt;extracting those boundary nodes (where an app meets the outside) goes beyond mechanical AST analysis and had to be implemented per-repo-type&lt;/strong&gt; — that's where the time went.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So I spent two months out of the first three on this, and got something that worked end-to-end.&lt;/p&gt;

&lt;p&gt;And then I threw it away.&lt;/p&gt;

&lt;p&gt;Why: static analysis is great at capturing &lt;strong&gt;structure&lt;/strong&gt;, but it can't traverse on &lt;strong&gt;intent or business context&lt;/strong&gt;. Concretely, three things broke:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No semantic entry point for search&lt;/strong&gt; — if I want to query the codebase with "show me the function calculating member subscription billing," I can't get there unless I already know the function name or file. A graph built only from static analysis has no semantic-tag entry pointing to "what is this code for?"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The graph contains only code&lt;/strong&gt; — internal helpers / utilities / types / arguments all become nodes, so traversal from any function &lt;strong&gt;blows up within a few hops&lt;/strong&gt;, dragging in helpers and primitives. There's no axis to filter on semantic relatedness&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What I actually wanted was code + DB schema + docs + infra on one graph&lt;/strong&gt; — given a function, I want to pull, in one query, the DB tables it touches, the docs where the design lives, and the linked business requirement. A code-only graph just can't do that&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;→ I switched to the annotation-based approach (&lt;code&gt;@graph-*&lt;/code&gt; JSDoc tags write the business intent into the code, and that gets unified with DB schema / docs / infra into one graph). Searchable semantically, and when you traverse, only related stuff comes back. That's the current &lt;strong&gt;product-graph (cpg)&lt;/strong&gt;. &lt;strong&gt;Don't drag sunk cost forward and you'll get to the final form&lt;/strong&gt; — discarding two months of investment instead of trying to recoup it was the foundation for everything that came after.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setting Coverage 90% as a Solo Target Broke the Implementations
&lt;/h3&gt;

&lt;p&gt;Test coverage is still gated at 90%+ (as covered in &lt;a href="https://dev.to/ryantsuji/human-on-the-loop-ai-reviewing-ai-prs-at-cortex-769-prsmonth-while-raising-the-quality-bar-4lh5"&gt;Part 3&lt;/a&gt;). That part hasn't changed. But there was a period when &lt;strong&gt;Coverage was treated as a standalone target&lt;/strong&gt;, and during that period the implementation visibly got worse.&lt;/p&gt;

&lt;p&gt;Specifically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Heavy default-value use that hides branches&lt;/strong&gt;: &lt;code&gt;function(input = {})&lt;/code&gt; style writes the missing-input branch out of the test path. Coverage goes up, protection against unexpected input is gone&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Catch-and-swallow over throw&lt;/strong&gt;: try / catch returning &lt;code&gt;null&lt;/code&gt;. Don't throw → no need to test "doesn't throw," and Coverage is satisfied. Invalid state silently propagates&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Early returns that flatten too much&lt;/strong&gt;: dump complex conditions through an "early return" escape. Tests pass; what should have been validation just isn't there anymore&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Result: &lt;strong&gt;Coverage 90%, quality lower than before&lt;/strong&gt;. When you look at Coverage alone, the shortest path to "satisfy it" is &lt;strong&gt;a weaker implementation that passes the tests&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Two lessons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Set a number as a target, and the number becomes the goal&lt;/strong&gt;. Coverage is a "minimum floor" — not "a goal to hit"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't evaluate any metric alone&lt;/strong&gt;. Coverage has to be evaluated alongside responsibility separation / exception design / boundary value coverage / etc.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then, as the follow-up: I added linting that &lt;strong&gt;mechanically closes off the routes that let you weaken implementations to satisfy Coverage&lt;/strong&gt;. Two specific examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;no-silent-catch&lt;/code&gt;&lt;/strong&gt;: AST-level ban on empty catch and silent-handler patterns like &lt;code&gt;.catch(() =&amp;gt; null)&lt;/code&gt;. Catch bodies have to have a &lt;strong&gt;function call (logger included) / re-throw / new / await&lt;/strong&gt; — otherwise it's an error. Catches the "weaken throws to satisfy Coverage but lose observability in production" pattern structurally. The violation message routes you to &lt;code&gt;@cortex/otel/logger&lt;/code&gt; for structured logging, so the chain through Cloud Run OTel → Loki / Grafana stays intact&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;vitest-strong-matchers&lt;/code&gt;&lt;/strong&gt;: bans weak matchers like &lt;code&gt;toBeTruthy&lt;/code&gt; / &lt;code&gt;toBeDefined&lt;/code&gt; / &lt;code&gt;toContain&lt;/code&gt; / &lt;code&gt;toBe(true|false)&lt;/code&gt; / &lt;code&gt;expect.any&lt;/code&gt; / &lt;code&gt;expect.objectContaining&lt;/code&gt;. Catches "any assertion that passes" patterns at the AST level, and points you instead toward &lt;code&gt;toStrictEqual&lt;/code&gt; / &lt;code&gt;toMatchInlineSnapshot&lt;/code&gt; that pin down the full output. This is one notch above Coverage — a &lt;strong&gt;test quality&lt;/strong&gt; concern — but it lines up because the same reflection applies: &lt;strong&gt;don't let a number become the goal&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On top of that, cortex's &lt;a href="https://github.com/air-closet/cortex/blob/main/docs/guidelines/testing.md" rel="noopener noreferrer"&gt;testing guideline&lt;/a&gt; opens with "&lt;strong&gt;Coverage is not the goal, just a supporting indicator&lt;/strong&gt;," and threshold-lowering / &lt;code&gt;istanbul ignore&lt;/code&gt; workarounds get bounced as Critical in Auto Review. So even when Coverage is satisfied, "this is intentionally deleting a branch" / "this is swallowing the exception" comes back as a Major finding.&lt;/p&gt;

&lt;p&gt;From the lesson "a single metric warps implementation," we descended through &lt;strong&gt;guideline that states the principle → lint that mechanically rejects → Auto Review that evaluates as a dimension&lt;/strong&gt; before Coverage 90% finally functioned as the "minimum floor" it should have been all along. This too sits in the lineage of the &lt;strong&gt;Recurrence Prevention&lt;/strong&gt; mechanism from Part 4 (so the same trap can't be stepped on twice).&lt;/p&gt;

&lt;h3&gt;
  
  
  Parallel Sub-Agent → Sequential Evaluation
&lt;/h3&gt;

&lt;p&gt;Third: an internal-structure call about Auto Review. &lt;strong&gt;Distribute the 9 dimensions to parallel sub-agents and evaluate concurrently&lt;/strong&gt; — the plausible-looking design ("parallel = faster, parallel should also hold quality") I tried first and ended up throwing out.&lt;/p&gt;

&lt;p&gt;What actually happened: &lt;strong&gt;time, cost, and accuracy all got worse&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Time got worse&lt;/strong&gt;: each sub-agent has its own startup, its own context load, its own result aggregation overhead. "9-way parallel = 9x faster" didn't hold; there were even cases where sequential evaluation in one session ended up faster&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost got worse&lt;/strong&gt;: each sub-agent loads PR diff + guidelines + related code independently — common context loads ran 9 times. Token consumption measured at &lt;strong&gt;just under 4x — not the naive 9x&lt;/strong&gt; (the context other than diff is shared across many dimensions, which is what kept it from blowing up to a full 9x)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accuracy didn't hold&lt;/strong&gt;: parallel sub-agents don't see each other's verdicts, so the same problem comes back as "APPROVE" from one and "REQUEST_CHANGES" from another. Duplicate findings show up too. Without a "what kind of PR is this as a whole?" pass to anchor on, dimensional findings drift toward local optima and the overall picture gets worse&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Switching to sequential evaluation: same session goes through 9 dimensions in sequence, so context loads once, and each dimension's call has the previous dimension's verdict in front of it. &lt;strong&gt;All three — time, cost, accuracy — improve simultaneously.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Of course, sequential evaluation introduces &lt;strong&gt;order dependence between dimensions&lt;/strong&gt; — earlier verdicts can shape later ones. That's a real trade-off, and I accepted it knowingly. &lt;strong&gt;Inter-dimension consistency at the cost of some order sensitivity&lt;/strong&gt; is more useful as a 9-dimension review than fully independent dimensions that contradict each other.&lt;/p&gt;

&lt;p&gt;The takeaway: the distributed-systems intuition that "&lt;strong&gt;parallel = faster, parallel = quality holds&lt;/strong&gt;" &lt;strong&gt;breaks its own assumptions in an AI harness&lt;/strong&gt;. Unlike parallelizing across CPU cores on your machine, with AI &lt;strong&gt;the context isn't shared memory; it's per-process state&lt;/strong&gt;. Sequential evaluation in one session ends up better on speed, token efficiency, and inter-dimension consistency at the same time — a structural property that's easy to miss at design time.&lt;/p&gt;

&lt;h3&gt;
  
  
  What This Section Is Really Saying
&lt;/h3&gt;

&lt;p&gt;The form I've described across the series is &lt;strong&gt;the result of a lot of trial and error&lt;/strong&gt;. Not starting with the right answer and laying it out from there. The decisions of throwing things away with sunk costs included, the trap of letting a metric I chose turn into the goal, the distributed setup that looked natural but worked against me — those are the things I walked through before landing at the current shape.&lt;/p&gt;

&lt;p&gt;Not easy. I don't pretend it was. But &lt;strong&gt;if you do walk through it, real results follow&lt;/strong&gt; — that's the honest read on it now.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing the Series
&lt;/h2&gt;

&lt;p&gt;What I most wanted to communicate across these six posts comes down to one thing:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;AI coding is not about "how to use AI" — it's about designing the environment AI runs in.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Or, put another way:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;AI isn't something to trust. It's something to design.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Assuming a large codebase&lt;/strong&gt;: prompt engineering / model selection / tool selection — each matters individually, but polishing them alone doesn't get you to auto-merging PRs, auto-healing incidents, or non-engineer development. Getting there requires building &lt;strong&gt;a codebase / business flow / observability / repair cycle where AI doesn't need to infer&lt;/strong&gt;. That's not an individual AI skill — that's &lt;strong&gt;an environment-design problem&lt;/strong&gt; (conversely, for a small project of a few dozen files, today's AI models work fine standalone. &lt;strong&gt;Harnesses become essential when scale exceeds what one person can hold in their head.&lt;/strong&gt;).&lt;/p&gt;

&lt;p&gt;And the conviction at the root of environment design is, repeating myself, "&lt;strong&gt;I don't trust AI to fill in the blanks for me&lt;/strong&gt;" — looking the reality in the face that context that wasn't handed over isn't known, and the ideal state doesn't happen without being told. Once you accept that premise, &lt;strong&gt;what to build clarifies naturally&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Looking back, four decisions ended up being the ones that mattered:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Locked the conviction first&lt;/strong&gt;: putting words to the root ("AI isn't something to trust") gave priority order to every mechanism. If I'd started from technique, I don't think I'd have made it to the current form&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Invested with throwing-out as the default&lt;/strong&gt;: like I did with code-graph at the two-month mark, I went into things with "throwing this out is OK." Drag sunk cost forward and you can't move forward&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refused standalone numerical targets&lt;/strong&gt;: the moment a metric like Coverage 90% becomes the goal on its own, implementations warp. Designed the system so it gets evaluated alongside other dimensions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Designed for "no inference," not around AI's capability&lt;/strong&gt;: I prioritized building structure where AI doesn't have to infer, instead of relying on what AI can do. That's what made the system stable end-to-end, I think&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If even one of these is useful to someone starting on something similar, that would be great.&lt;/p&gt;




&lt;h2&gt;
  
  
  Afterword — Where Engineering Careers Are Heading
&lt;/h2&gt;

&lt;p&gt;Slipping off the wrap-up topic — this is something I've been turning over recently, written here in a "loosely held thought" tone, so feel free to skim.&lt;/p&gt;

&lt;p&gt;As harnesses mature, I think &lt;strong&gt;engineering work splits along two directions&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;One direction is &lt;strong&gt;value creation from problem identification and business design&lt;/strong&gt;. In the world of Part 5 — where non-engineer PRs work — "writing code" stops being scarce, and the actual scarce thing becomes &lt;strong&gt;the ability to define what to build&lt;/strong&gt;. The person closest to the requirements (a PMO, a business manager, a domain-deep engineer) ends up driving Claude Code through to the merged PR themselves. This direction looks less like "engineer" and more like a &lt;strong&gt;business designer who moves between domain and implementation&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The other direction is &lt;strong&gt;building the foundation that lets all of that happen safely and quickly&lt;/strong&gt;. Non-engineers can open PRs to the production repo only because the harness underneath holds quality — knowledge graph, Auto Review, Self-Healing, Recurrence Prevention, lint, CI, tests, observability stack, all interlocked. Designing / maintaining / evolving that gets &lt;em&gt;harder&lt;/em&gt;, not easier. As the &lt;strong&gt;house-builder side, rail-layer side&lt;/strong&gt;, this demands deep infra understanding / security instinct / observability design / a feel for AI's architectural quirks.&lt;/p&gt;

&lt;p&gt;I'm building cortex, so I'm spending more time on the latter; building "a foundation where the business can run its own changes" is genuinely fun for me. &lt;strong&gt;That said, I'm not the type who fully commits to one side&lt;/strong&gt; — I move between listening to business questions and assembling the foundation, and the satisfaction from each is its own kind. This isn't a "which is more important?" question — the harness exists precisely so the former is possible, and the former being alive is what gives the latter meaning. They're mutually dependent.&lt;/p&gt;

&lt;p&gt;Maybe the era of polishing &lt;strong&gt;just&lt;/strong&gt; "coding ability" is shifting slightly. Where to put your value — or whether to move between both directions — becomes a question more engineers will need to choose into intentionally.&lt;/p&gt;




&lt;p&gt;Six posts in, thanks for sticking with me to the end.&lt;/p&gt;




&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Theme&lt;/th&gt;
&lt;th&gt;Key scene&lt;/th&gt;
&lt;th&gt;Article&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Series intro: cortex's harness&lt;/td&gt;
&lt;td&gt;PRs auto-merge / incidents self-heal before you notice&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ryantsuji/building-a-real-ai-harness-auto-reviewed-prs-self-healing-ops-and-non-engineer-contributors-3lfa"&gt;ai-harness-intro&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Product Graph (cpg)&lt;/td&gt;
&lt;td&gt;Code, docs, DB, infra unified into one graph&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ryantsuji/the-heart-of-the-ai-harness-a-knowledge-graph-of-the-ai-by-the-ai-for-the-ai-series-part-2-53bm"&gt;cortex-product-graph&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;AI PR review&lt;/td&gt;
&lt;td&gt;webhook → AI review → auto-fix → squash merge&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ryantsuji/human-on-the-loop-ai-reviewing-ai-prs-at-cortex-769-prsmonth-while-raising-the-quality-bar-4lh5"&gt;cortex-auto-review&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Self-Healing + observability + auto-added guardrails&lt;/td&gt;
&lt;td&gt;Alert → AI investigates → fix PR + new lint/type gate → auto redeploy&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ryantsuji/fixed-before-anyone-notices-stronger-after-every-fix-self-healing-recurrence-prevention-series-1e86"&gt;cortex-self-healing&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Democratizing the maintenance phase&lt;/td&gt;
&lt;td&gt;Domain experts open PRs to production; the harness owns the quality gate&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ryantsuji/the-author-doesnt-have-to-be-an-engineer-how-the-harness-holds-quality-series-part-5-12e4"&gt;cortex-non-engineer-prs&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Series Final&lt;/td&gt;
&lt;td&gt;The underlying philosophy plus a retrospective on the failures and lessons&lt;/td&gt;
&lt;td&gt;This post&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

</description>
      <category>ai</category>
      <category>architecture</category>
      <category>devops</category>
      <category>engineering</category>
    </item>
    <item>
      <title>Part 5 ("The Author Doesn't Have to Be an Engineer") has been generating sharp comments.
Worth a read for the thread alone.</title>
      <dc:creator>Ryosuke Tsuji</dc:creator>
      <pubDate>Thu, 11 Jun 2026 01:06:48 +0000</pubDate>
      <link>https://dev.to/ryantsuji/part-5-the-author-doesnt-have-to-be-an-engineer-has-been-generating-sharp-comments-worth-a-1pao</link>
      <guid>https://dev.to/ryantsuji/part-5-the-author-doesnt-have-to-be-an-engineer-has-been-generating-sharp-comments-worth-a-1pao</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/ryantsuji/the-author-doesnt-have-to-be-an-engineer-how-the-harness-holds-quality-series-part-5-12e4" class="crayons-story__hidden-navigation-link"&gt;The Author Doesn't Have to Be an Engineer: How the Harness Holds Quality (Series Part 5)&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
      &lt;a href="https://dev.to/ryantsuji/the-author-doesnt-have-to-be-an-engineer-how-the-harness-holds-quality-series-part-5-12e4" class="crayons-article__context-note crayons-article__context-note__feed"&gt;&lt;p&gt;Self-healing guardrails for business-side PRs&lt;/p&gt;

&lt;/a&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/ryantsuji" class="crayons-avatar  crayons-avatar--l  "&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%2Fuser%2Fprofile_image%2F3843591%2F8b126f91-f561-4e6b-8492-814b18d680ec.jpg" alt="ryantsuji profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/ryantsuji" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Ryosuke Tsuji
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Ryosuke Tsuji
                
              
              &lt;div id="story-author-preview-content-3849367" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/ryantsuji" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&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%2Fuser%2Fprofile_image%2F3843591%2F8b126f91-f561-4e6b-8492-814b18d680ec.jpg" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Ryosuke Tsuji&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/ryantsuji/the-author-doesnt-have-to-be-an-engineer-how-the-harness-holds-quality-series-part-5-12e4" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Jun 8&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/ryantsuji/the-author-doesnt-have-to-be-an-engineer-how-the-harness-holds-quality-series-part-5-12e4" id="article-link-3849367"&gt;
          The Author Doesn't Have to Be an Engineer: How the Harness Holds Quality (Series Part 5)
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/ai"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;ai&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/devops"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;devops&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/engineering"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;engineering&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/github"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;github&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/ryantsuji/the-author-doesnt-have-to-be-an-engineer-how-the-harness-holds-quality-series-part-5-12e4" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/exploding-head-daceb38d627e6ae9b730f36a1e390fca556a4289d5a41abb2c35068ad3e2c4b5.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;19&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/ryantsuji/the-author-doesnt-have-to-be-an-engineer-how-the-harness-holds-quality-series-part-5-12e4#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              30&lt;span class="hidden s:inline"&gt;&amp;nbsp;comments&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            16 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
      <category>career</category>
      <category>discuss</category>
      <category>softwareengineering</category>
      <category>testing</category>
    </item>
    <item>
      <title>The Author Doesn't Have to Be an Engineer: How the Harness Holds Quality (Series Part 5)</title>
      <dc:creator>Ryosuke Tsuji</dc:creator>
      <pubDate>Mon, 08 Jun 2026 23:32:30 +0000</pubDate>
      <link>https://dev.to/ryantsuji/the-author-doesnt-have-to-be-an-engineer-how-the-harness-holds-quality-series-part-5-12e4</link>
      <guid>https://dev.to/ryantsuji/the-author-doesnt-have-to-be-an-engineer-how-the-harness-holds-quality-series-part-5-12e4</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;AI assistance disclosure: This article was drafted with the help of Claude. All technical content, design decisions, code references, and screenshots reflect production systems I designed and operate at airCloset; the prose was revised by me prior to publication.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Hi, I'm &lt;a href="https://x.com/ryantsuji" rel="noopener noreferrer"&gt;Ryan&lt;/a&gt;, CTO at airCloset.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer&lt;/strong&gt;: "cortex" in this article is the internal codename for an AI platform built in-house at airCloset. It is unrelated to existing commercial services like Snowflake Cortex or Palo Alto Networks Cortex.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In &lt;a href="https://dev.to/ryantsuji/building-a-real-ai-harness-auto-reviewed-prs-self-healing-ops-and-non-engineer-contributors-3lfa"&gt;Part 1 (Series Intro)&lt;/a&gt;, I wrote about how cortex's harness has matured to the point where &lt;strong&gt;non-engineers (business-side managers, PMOs, and the like) can open PRs to the production repository&lt;/strong&gt;. The harness here is the runtime foundation for AI in production -- the combination of the knowledge graph, Auto Review, Self-Healing, and Recurrence Prevention covered across Parts 1 through 4.&lt;/p&gt;

&lt;p&gt;Part 5 is what comes next: &lt;strong&gt;that harness has now reached the layer of who actually writes the code&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;"Surely an engineer is still checking afterward, right?" -- I expect a lot of readers will land here with that question. So this post leads with &lt;strong&gt;one concrete example&lt;/strong&gt; before anything else.&lt;/p&gt;

&lt;p&gt;Part 5 covers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;What kinds of PRs are actually shipping&lt;/strong&gt; -- two recent ones in detail&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What works and what doesn't&lt;/strong&gt; -- the boundary between adding on top of an existing stack and standing up new infrastructure&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why this holds for non-engineers&lt;/strong&gt; -- how the four mechanisms from Parts 1-4 carry it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What's next -- into toC services&lt;/strong&gt; -- the direction of travel for consumer-facing scale&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The deeper toC implementation story will live in a separate post; here you'll get the framing and the direction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Series
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Theme&lt;/th&gt;
&lt;th&gt;Key scene&lt;/th&gt;
&lt;th&gt;Article&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Series intro: cortex harness&lt;/td&gt;
&lt;td&gt;PRs merging unattended / incidents fixed before anyone notices&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ryantsuji/building-a-real-ai-harness-auto-reviewed-prs-self-healing-ops-and-non-engineer-contributors-3lfa"&gt;ai-harness-intro&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Product Graph (cpg)&lt;/td&gt;
&lt;td&gt;Code / docs / DB / infra unified into one graph&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ryantsuji/the-heart-of-the-ai-harness-a-knowledge-graph-of-the-ai-by-the-ai-for-the-ai-series-part-2-53bm"&gt;cortex-product-graph&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Auto PR review&lt;/td&gt;
&lt;td&gt;webhook -&amp;gt; AI review -&amp;gt; auto-fix -&amp;gt; squash merge&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ryantsuji/human-on-the-loop-ai-reviewing-ai-prs-at-cortex-769-prsmonth-while-raising-the-quality-bar-4lh5"&gt;cortex-auto-review&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Self-Healing + observability + auto-added guardrails&lt;/td&gt;
&lt;td&gt;Alert -&amp;gt; AI investigates -&amp;gt; fix PR + new lint/type gate -&amp;gt; auto redeploy + same pattern auto-rejected from then on&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ryantsuji/fixed-before-anyone-notices-stronger-after-every-fix-self-healing-recurrence-prevention-series-1e86"&gt;cortex-self-healing&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Democratizing the maintenance phase&lt;/td&gt;
&lt;td&gt;Domain experts open PRs to production; the harness owns the quality gate&lt;/td&gt;
&lt;td&gt;This article ← you are here&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Series Final&lt;/td&gt;
&lt;td&gt;The underlying philosophy plus a retrospective on the failures and lessons&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ryantsuji/ai-isnt-something-to-trust-its-something-to-design-series-final-30aa"&gt;cortex-philosophy&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Start with one scene
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;+1,742 line / 41 file&lt;/strong&gt; PR lands on the internal dashboard web app. Title: "PL dashboard ver.2". The change opens up project visibility to managers and team leads across multiple business units, scoping what each person sees to their own division or team. It adds an SSoT in the shared types package, new routes on the API server with SQL involving &lt;code&gt;INNER JOIN&lt;/code&gt; and &lt;code&gt;LEFT JOIN&lt;/code&gt;, new pages and view-state on the web app, and a personal-settings surface -- the whole stack of things you'd expect for a real feature.&lt;/p&gt;

&lt;p&gt;The point is, this isn't a typo fix or a string swap. Entities, repositories, API routes, screens, filters, personal settings -- every layer you'd normally touch for a feature got touched. &lt;strong&gt;A few days of work for an experienced engineer, in scale terms.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The review-fix cycle ran like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;PR open&lt;/strong&gt; (+1,742 / 41 files)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;auto-review pass 1&lt;/strong&gt;: Major finding (a permission-scope fall-through -- data from other divisions leaking into the view that shouldn't be there) plus a handful of Minor items&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;author bot push&lt;/strong&gt;: closes the scope fall-through, addresses the Minor items&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;auto-review pass 2&lt;/strong&gt;: Nit items remaining, plus a lint catch (&lt;code&gt;no-empty-function&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;author bot push&lt;/strong&gt;: lint clean&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;auto-review pass 3&lt;/strong&gt;: still some COMMENTED nits, not yet APPROVE&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;author bot push (iteration 2)&lt;/strong&gt;: hardens loading skeleton, reverts an unnecessary JSDoc tweak&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;auto-review pass 4: APPROVED&lt;/strong&gt; → CI green + APPROVE both met → auto-merge → production&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;From PR open to merge: &lt;strong&gt;four review-fix rounds, three author-bot pushes, zero human reviewers in the loop.&lt;/strong&gt; The reviews come from the auto-review bot, the fixes come from an author bot (an automated review-response agent that the PR author has running on their machine), the final APPROVE is submitted by the AI, and an auto-merge script picks it up the instant CI is green. Production lands with &lt;strong&gt;56/56 shared type checks (SSoT), 2,284/2,284 API tests, 1,113/1,113 web specs, and 0 lint errors.&lt;/strong&gt; (cortex splits the lint job between &lt;a href="https://oxc.rs/docs/guide/usage/linter" rel="noopener noreferrer"&gt;oxlint&lt;/a&gt; for general checks and a custom eslint plugin for the &lt;code&gt;@graph-*&lt;/code&gt; rules.)&lt;/p&gt;

&lt;p&gt;The second review pass is worth noting. "Scope fall-through" is a somewhat technical finding -- a hole in the permission filter meant data from divisions other than your own could leak into the view. This is an internal dashboard, so it's not an external-leak incident, but &lt;strong&gt;"only see what's relevant to you" is the whole point of a dashboard like this&lt;/strong&gt; -- losing it doesn't just risk an information slip, it drowns the user in noise that they shouldn't be filtering through in the first place. That's the kind of issue that's easy to merge by mistake and painful to notice in production. &lt;strong&gt;The fact that auto-review caught it on pass one and bounced it back for the author side to fix is what makes this whole flow viable for non-engineers.&lt;/strong&gt; Without that loop, a PR of this size from a non-engineer would be a bad bet.&lt;/p&gt;

&lt;p&gt;And: &lt;strong&gt;the author of this PR is not an engineer&lt;/strong&gt;. A business-side teammate handed a feature description to Claude Code, leaned on the knowledge graph (covered in &lt;a href="https://dev.to/ryantsuji/the-heart-of-the-ai-harness-a-knowledge-graph-of-the-ai-by-the-ai-for-the-ai-series-part-2-53bm"&gt;Part 2&lt;/a&gt;) to pull in the relevant existing code, and the +1,742 line PR is what came back. The four review-fix rounds above are what happened next.&lt;/p&gt;

&lt;p&gt;That setup lines up directly with the central claim of this post: &lt;strong&gt;the person who knows the business requirements best, instead of organizing them and handing them to an engineer, runs them through Claude Code to production themselves.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Quick clarification on "write." When I say "write" in this article, I don't mean &lt;strong&gt;typing line by line in an editor&lt;/strong&gt;. I mean &lt;strong&gt;handing the business requirements to Claude Code, judging the resulting diffs and AI review comments with domain knowledge, and seeing it through to a production merge&lt;/strong&gt; -- the whole arc. Most of the actual diff is written by Claude Code; review feedback is handled by the author bot. What the human does is three things: put what they want into words, make the judgment calls along the way ("does this fit, is this off"), and sign off when it's ready to merge. None of that is implementation work in the technical sense. That's what "write" means here.&lt;/p&gt;

&lt;p&gt;There's still a learning curve, of course -- the prompts you give Claude Code, where to point it for context. But &lt;strong&gt;none of that is learning to program.&lt;/strong&gt; What you need is the ability to articulate what you want clearly, not syntax or framework knowledge.&lt;/p&gt;

&lt;p&gt;The harness covers quality, so even at +1,742 lines / 41 files, this works.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Out of scope&lt;/strong&gt;: this post does &lt;em&gt;not&lt;/em&gt; cover the path where non-engineers freely ship apps to a sandbox environment instead of opening PRs against the production repo. That's a different mechanism, covered in an earlier post: &lt;a href="https://dev.to/ryantsuji/bridging-i-want-to-build-and-i-want-to-publish-safely-for-non-engineers-sandbox-mcp-392a"&gt;Bridging "I Want to Build" and "I Want to Publish Safely" for Non-Engineers with a Custom Sandbox MCP&lt;/a&gt;. This post is specifically about &lt;strong&gt;opening PRs against the production repo&lt;/strong&gt; -- the front door that's traditionally been engineer-only.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  When you need a change, you can make it yourself
&lt;/h2&gt;

&lt;p&gt;The point of the previous section is this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;When you need a change, you make it yourself, without flagging an engineer.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When that holds, work like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"I want a new metric on the dashboard"&lt;/li&gt;
&lt;li&gt;"The aggregation filter doesn't match how the business actually operates"&lt;/li&gt;
&lt;li&gt;"I want a small business-support feature embedded in the production app"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;stops queueing behind whatever an engineer is in the middle of. The fix lands when the need lands.&lt;/p&gt;

&lt;p&gt;Think about the old flow. Someone on the business side notices a small thing that needs to change. They write the requirements up. They open a ticket or a Slack thread for an engineer. The engineer is in the middle of something else, so it queues. When they finally get to it, the interpretation drifts from what the business actually meant, there's a back-and-forth, a review pass, and only then does it ship. Even a small change takes days to a week in wall-clock time.&lt;/p&gt;

&lt;p&gt;That's the cost of a &lt;strong&gt;translation layer between business understanding and code&lt;/strong&gt;, and it gets worse the busier the engineer is. The business's improvement cycle ends up paced by engineering's backlog.&lt;/p&gt;

&lt;p&gt;When the person who knows the requirements writes the change themselves, that translation layer and that queue both disappear.&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%2F9qelj5u7og19uc7gogaw.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%2F9qelj5u7og19uc7gogaw.png" alt="Business request to production -- the translation layer and queue go away, taking the cycle from days to hours" width="800" height="410"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here are two recent examples of that working.&lt;/p&gt;

&lt;h3&gt;
  
  
  Two non-engineer PRs that recently shipped
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;PR&lt;/th&gt;
&lt;th&gt;Kind&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;What changed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;#1573&lt;/td&gt;
&lt;td&gt;Deep bug fix&lt;/td&gt;
&lt;td&gt;+348 -177 / 7 files&lt;/td&gt;
&lt;td&gt;The dashboard's actuals number was unfairly exceeding the target. Root cause: the "which teams to aggregate" definition was asymmetric between target side and actuals side. Fix lifts the shared "teams to include" list into its own file and points both sides at it. &lt;strong&gt;Tests added too.&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;#1557&lt;/td&gt;
&lt;td&gt;Feature build on top of existing stack&lt;/td&gt;
&lt;td&gt;+1,742 -227 / 41 files&lt;/td&gt;
&lt;td&gt;The PL dashboard v2 from the opening scene. &lt;strong&gt;Entities, repositories, API, UI -- all touched&lt;/strong&gt;, but the web app itself (the stack) was already standing; this rides on top.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Different shapes, but both are non-engineer PRs that made it all the way to merge.&lt;/p&gt;

&lt;h4&gt;
  
  
  #1573 -- a deep root-cause fix
&lt;/h4&gt;

&lt;p&gt;This started from "the number looks wrong" on the business side, and the PR went all the way down to a data-integrity issue. The surface symptom: "the actuals number on the dashboard exceeds the monthly target, with the achievement reading 101% even though the team knows that's not real." The lazy fix would be a fudge factor or a clamp on the display. That's not what happened.&lt;/p&gt;

&lt;p&gt;The author dug into the aggregation queries and pinned the real cause: &lt;strong&gt;the actuals side and the target side were reading from different tables, and the definition of "which teams count" wasn't symmetric between them.&lt;/strong&gt; Teams that don't carry a target value (designers, PMOs, and so on) didn't show up on the target side but were getting counted on the actuals side, so the numerator was inflated against the denominator.&lt;/p&gt;

&lt;p&gt;The fix is structural, not cosmetic. A single file defines "the teams in scope for this aggregation" as a shared list, and both sides reference it. &lt;strong&gt;No future drift between target-side and actuals-side definitions&lt;/strong&gt; -- it's locked in by the shared constant.&lt;/p&gt;

&lt;p&gt;The handling of "what data falls out of an aggregation" and "are the target and actuals sides really symmetric" is the kind of thing engineers miss too. &lt;strong&gt;A non-engineer working through it down to the structural level and fixing it there&lt;/strong&gt; is what stands out about this PR.&lt;/p&gt;

&lt;h4&gt;
  
  
  #1557 -- a big feature build on top of an existing stack
&lt;/h4&gt;

&lt;p&gt;This is the PR the opening scene walked through. +1,742 / 41 files spanning entity, repository, API, and UI -- &lt;strong&gt;a scale of change that's well past what people usually mean when they say "modification."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;What lets a non-engineer ship this size of change is that &lt;strong&gt;the web app itself (the stack) is already standing.&lt;/strong&gt; Nobody's standing up a new app, no new Cloud Run service needed defining, no new dependency packages, no new directory structure. The change adds a route, a page, and a repository entry inside the existing structure that's already there. It rides on what's been built.&lt;/p&gt;

&lt;p&gt;This is the "on top of an existing stack" range. That's where the boundary is, and the next section spells it out.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note on terminology&lt;/strong&gt;: "modification" in this article is broader than "small tweaks to existing logic." It includes adding new entities, new endpoints, and new pages on top of an existing stack. The line I'm drawing is between &lt;strong&gt;building on top of a stack&lt;/strong&gt; vs. &lt;strong&gt;standing the stack up in the first place.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What works, what doesn't
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The principle: standing up a stack is hard, building on top of one isn't
&lt;/h3&gt;

&lt;p&gt;The cleanest dividing line for non-engineer development isn't "modification vs. new development." It's &lt;strong&gt;"on top of an existing stack" vs. "stand up a new stack."&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Standing up a new stack&lt;/strong&gt; (work that starts from infrastructure: a new web app from scratch, a new Cloud Run service defined from a Dockerfile, a brand-new BigQuery pipeline) → engineering work&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adding to an existing stack&lt;/strong&gt; (a new page in an app that already exists, a new endpoint on an existing API, a new data source on an existing pipeline) → non-engineers can do this&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All three of the example PRs above sit on the second side. The stack itself was already built (by me, for the most part), so they get to work inside it. "Stand up a new app from scratch" or "define infrastructure (Dockerfile / IaC) from zero" are still engineer territory.&lt;/p&gt;

&lt;p&gt;Put another way: &lt;strong&gt;renovations and new rooms inside an existing house are open to anyone. Building the house itself is engineering.&lt;/strong&gt; Get the structure wrong -- the load-bearing parts, the wiring, the plumbing -- and the cost of recovery is high. That's the part of stack design where there's still too much "if this is wrong, everything downstream breaks" risk to hand to AI.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's left for engineers: laying the rails -- the stack and the harness itself
&lt;/h3&gt;

&lt;p&gt;The flip side: &lt;strong&gt;the rail-laying work&lt;/strong&gt; -- standing up a stack, and &lt;strong&gt;extending the harness itself&lt;/strong&gt; -- is what non-engineers don't touch yet. Both require a different kind of knowledge:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Infrastructure&lt;/strong&gt;: containers, IaC, the operational characteristics of cloud services. Cloud Run resource ceilings, cold starts, Pub/Sub at-least-once semantics, BigQuery partition / cluster design, how Pulumi stacks split. Get this wrong and a thing that compiles can still fall over in production&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authentication for external integrations&lt;/strong&gt;: OAuth, webhooks, how you handle API keys and where they sit in Secret Manager. One small slip leaks credentials into the repo or lets a webhook fire something you didn't intend&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security fundamentals&lt;/strong&gt;: what to never expose, where to sanitize, where the privilege boundary cuts. SQL injection, XSS, SSRF, broken authorization -- "it works" isn't enough here&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Harness design and extension&lt;/strong&gt;: adding a new Auto Review dimension, changing Self-Healing logic, writing a new lint rule (e.g. in &lt;code&gt;eslint-plugin-graph&lt;/code&gt;), structuring guidelines. &lt;strong&gt;Decisions that require understanding how the whole flywheel hangs together&lt;/strong&gt; -- the most meta layer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last bullet -- harness extension -- has an important implication: &lt;strong&gt;for non-engineers to keep being able to ship to production, someone has to keep the harness evolving.&lt;/strong&gt; Recurrence Prevention (Part 4) is the automatic loop that adds lint / CI guards / guidelines per trap. But the architecture of the harness itself -- the structure of dimensions, the calibration of judgment, the design of the Self-Healing flow, the shape of the knowledge graph -- those are a meta layer that still requires engineering judgment.&lt;/p&gt;

&lt;p&gt;Concrete case: the current nine Auto Review dimensions (&lt;code&gt;[Graph]&lt;/code&gt; / &lt;code&gt;[Architecture]&lt;/code&gt; / &lt;code&gt;[Security]&lt;/code&gt; / &lt;code&gt;[Test]&lt;/code&gt; / &lt;code&gt;[Doc]&lt;/code&gt; / &lt;code&gt;[Impact]&lt;/code&gt; / &lt;code&gt;[Observability]&lt;/code&gt; / &lt;code&gt;[AI-Antipattern]&lt;/code&gt; / &lt;code&gt;[Recurrence]&lt;/code&gt;) were designed by observing past incidents and fix patterns. When a tenth dimension becomes necessary -- say, a "breaking change check on dependency upgrades" axis -- decisions about responsibility splits with existing dimensions and where to set thresholds are made by looking at the whole structure. That's the kind of engineering work that stays.&lt;/p&gt;

&lt;p&gt;The harness provides "rails you can't derail from." Laying those rails -- and laying the foundation those rails sit on -- is a different job, and it's still on engineering. &lt;strong&gt;Engineers lay the rails; anyone can run on them.&lt;/strong&gt; That's the boundary today.&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%2Fn0pvl864tgh1o4vpzlru.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%2Fn0pvl864tgh1o4vpzlru.png" alt="Three layers -- the upper layer is the non-engineer surface; the lower two (harness and stack) are engineering work" width="800" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this works for non-engineers
&lt;/h2&gt;

&lt;p&gt;This is a short recap, because everything that makes it work was already covered in Parts 1 through 4. &lt;strong&gt;Four mechanisms reinforcing each other&lt;/strong&gt; -- that's what lets non-engineers operate safely on top of an existing stack.&lt;/p&gt;

&lt;h3&gt;
  
  
  ① The knowledge graph pulls relevant code from "what you want to do"
&lt;/h3&gt;

&lt;p&gt;cortex-product-graph from &lt;a href="https://dev.to/ryantsuji/the-heart-of-the-ai-harness-a-knowledge-graph-of-the-ai-by-the-ai-for-the-ai-series-part-2-53bm"&gt;Part 2&lt;/a&gt; -- the unified graph fusing code, docs, DB schema, and infrastructure into one knowledge base (implementation name: cpg) -- carries this layer.&lt;/p&gt;

&lt;p&gt;Non-engineers don't need to know function names or repo structure. A natural-language question like "I want to add a metric column to the dashboard" goes to Claude Code, which hits the knowledge graph with a semantic search and gets back the relevant nodes -- the screen, the API, the DB, the docs -- in one or two hops. &lt;strong&gt;You can get started without knowing the technical vocabulary.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For #1557: the author told Claude Code "I want PL dashboard v2 with division/team scoping for non-PI-Div PMOs and team leads," and the knowledge graph pulled the existing &lt;code&gt;/projects&lt;/code&gt; route, &lt;code&gt;project-repository.ts&lt;/code&gt;, &lt;code&gt;FilterHeaders.tsx&lt;/code&gt;, and &lt;code&gt;ProjectTable.tsx&lt;/code&gt; as the relevant nodes. The author never needed to know what file to edit. &lt;strong&gt;That's how the translation layer drops out.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  ② Auto Review enforces quality at the gate
&lt;/h3&gt;

&lt;p&gt;The 9-dimension automated review from &lt;a href="https://dev.to/ryantsuji/human-on-the-loop-ai-reviewing-ai-prs-at-cortex-769-prsmonth-while-raising-the-quality-bar-4lh5"&gt;Part 3&lt;/a&gt; is the next layer. &lt;code&gt;[Graph]&lt;/code&gt; / &lt;code&gt;[Architecture]&lt;/code&gt; / &lt;code&gt;[Security]&lt;/code&gt; / &lt;code&gt;[Test]&lt;/code&gt; / &lt;code&gt;[Doc]&lt;/code&gt; / &lt;code&gt;[Impact]&lt;/code&gt; / &lt;code&gt;[Observability]&lt;/code&gt; / &lt;code&gt;[AI-Antipattern]&lt;/code&gt; / &lt;code&gt;[Recurrence]&lt;/code&gt; -- the AI returns REQUEST_CHANGES on what's missing and loops with the author bot until APPROVE -- the four-round example from the opening scene is exactly this in motion.&lt;/p&gt;

&lt;p&gt;The point is this: &lt;strong&gt;the first PR doesn't have to be perfect.&lt;/strong&gt; The author doesn't need to ship a completed, security-hole-free version on the first try. Push the initial PR and the rest gets sorted by the auto-review and the author bot bouncing off each other. The reason &lt;strong&gt;the author bot doesn't spin off into a loop of confused fixes&lt;/strong&gt; is that the knowledge graph holds the full codebase context: changes are made with structural awareness of what they touch, so misreadings of the review feedback don't compound.&lt;/p&gt;

&lt;h3&gt;
  
  
  ③ Self-Healing catches what slips through to production
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://dev.to/ryantsuji/fixed-before-anyone-notices-stronger-after-every-fix-self-healing-recurrence-prevention-series-1e86"&gt;Part 4&lt;/a&gt; covered Self-Healing. If something does break in production, the AI starts from the alert, investigates root cause, opens a fix PR, and gets it auto-redeployed -- the entire loop runs without humans. &lt;strong&gt;Incidents triggered by a non-engineer's change recover on their own, hands-off.&lt;/strong&gt; That's what makes the bar to opening a PR feel survivable.&lt;/p&gt;

&lt;p&gt;This isn't "non-engineers are safe because nothing can go wrong." It's "even if something goes wrong, the harness has it covered." The system is designed to &lt;strong&gt;minimize damage&lt;/strong&gt;, not eliminate failure. The three-layer construction (Observation → Repair → Strengthening) from Part 4 is what makes that net real.&lt;/p&gt;

&lt;h3&gt;
  
  
  ④ Recurrence Prevention keeps the trap count from growing
&lt;/h3&gt;

&lt;p&gt;The Recurrence Prevention loop from the back half of Part 4. &lt;strong&gt;Every trap that gets stepped on gets nailed down in the same PR&lt;/strong&gt;, so the next attempt at the same pattern gets caught. The form depends: mechanizable traps become lint or CI guards; less-mechanizable ones become entries in the guideline docs (&lt;code&gt;docs/gotchas&lt;/code&gt;, severity docs) that the AI reviewer reads. Either way, the catch happens before merge. Non-engineers contribute to this loop too -- when they hit a trap, the doc entry that prevents the next person from hitting it can come from them.&lt;/p&gt;

&lt;p&gt;As this compounds, &lt;strong&gt;the rails get denser.&lt;/strong&gt; Where there was once a loose "don't go that way" guideline, every incident adds another small rail saying "or this way, or this way, or this way," and the lane that's safe to walk gets clearer. The denser the rails, the safer non-engineers are in the lane.&lt;/p&gt;

&lt;p&gt;→ The four pieces aren't independent components. &lt;strong&gt;Each one's output feeds the next one's input.&lt;/strong&gt; This is the Guides + Sensors flywheel from Part 1 in action. I won't re-explain the details since they're in the prior posts, but &lt;strong&gt;non-engineers shipping to production is the result of all four wheels turning together.&lt;/strong&gt; Take any one out and the level of upfront knowledge required to write to production jumps, and the whole thing collapses.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next -- carrying the pattern to consumer-facing services
&lt;/h2&gt;

&lt;p&gt;cortex is an internal AI platform, so the system as it stands can't be lifted into a toC production service as-is. &lt;strong&gt;The biggest issue is the difference in quality bar.&lt;/strong&gt; For toC, "detect after user impact → Self-Healing fix" is too late. The requirement becomes: incidents don't happen, and when something is about to ship, there's review and testing on top of human sign-off.&lt;/p&gt;

&lt;p&gt;That said, the &lt;strong&gt;shape&lt;/strong&gt; of the harness -- a knowledge graph for context, 9-dimension AI review, an author bot responding to feedback -- carries over directly. The thing that changes is &lt;strong&gt;the final step&lt;/strong&gt;: cortex's auto-merge becomes "&lt;strong&gt;AI does the prep, a human signs off&lt;/strong&gt;." Not by giving up the AI's range, but by having the AI handle the heavy lifting (test writing, environment setup, test runs, the 9-dimension review) and leaving only the final APPROVE on a human. "If the human sign-off stays, engineer time doesn't really decrease, does it?" -- but historically engineers were spending the bulk of their time on the implementation, the test writing, the environment setup, the self-review, the back-and-forth on review. Sign-off itself is the smallest piece of that pie. With AI doing the prep work, what an engineer spends time on shifts from implementation labor to &lt;strong&gt;quality judgment&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A caveat on the knowledge graph: &lt;strong&gt;it only earns its keep at large codebase scale.&lt;/strong&gt; If the codebase fits in one AI context window, a cross-repo graph is unnecessary. The reason cortex (100+ apps) and the toC side (40+ repos) need one is because the scale forces it.&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%2F59dzskllpk4kp8jirits.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%2F59dzskllpk4kp8jirits.png" alt="cortex's shape carried into toC services -- internal knowledge graph → service-side knowledge graph / auto-merge → AI-prep + human sign-off / autonomous Self-Healing → human final call -- three things shift, the rest holds" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The concrete plan is real (extending the knowledge graph across the toC side's 40+ repositories, designing the AI-prep flow, etc.), and the full version goes in a separate post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap-up
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The person who knows the business requirements best, instead of writing them up for an engineer, runs them through to production directly.&lt;/strong&gt; Quality is held by the harness, so what's required from the writer is domain knowledge and the ability to direct an AI well. Business asks stop queuing behind engineering, and the cycle speeds up&lt;/li&gt;
&lt;li&gt;The four mechanisms from Parts 1-4 (knowledge graph / Auto Review / Self-Healing / Recurrence Prevention) form a reinforcing flywheel. &lt;strong&gt;The first PR doesn't have to be perfect, and what does break is repaired automatically.&lt;/strong&gt; That's the design&lt;/li&gt;
&lt;li&gt;The boundary: &lt;strong&gt;engineers lay the rails, anyone can run on them.&lt;/strong&gt; Standing up the stack (infrastructure, authentication, security) and extending the harness itself (new lint rules, new review dimensions, Self-Healing flow design) stay on engineering&lt;/li&gt;
&lt;li&gt;Carrying this to consumer-facing toC services, &lt;strong&gt;the knowledge graph (a 40+ repo cross-repo graph on the service side) covers the context layer, but the quality bar shifts, so auto-merge becomes "AI prep + human sign-off."&lt;/strong&gt; Details in a separate post&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;In &lt;strong&gt;Part 6&lt;/strong&gt; I'll wrap the series with the philosophy at the foundation -- &lt;strong&gt;why this design, what got given up, what got kept&lt;/strong&gt;. The series so far has been about "the parts that are working"; Part 6 puts the failures and the dead ends on the table too, including the gap between the philosophy and the actual implementation. A retrospective for myself, and -- I hope -- a reference for anyone heading down a similar road.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>devops</category>
      <category>engineering</category>
      <category>github</category>
    </item>
    <item>
      <title>Fixed Before Anyone Notices, Stronger After Every Fix: Self-Healing + Recurrence Prevention (Series Part 4)</title>
      <dc:creator>Ryosuke Tsuji</dc:creator>
      <pubDate>Mon, 01 Jun 2026 23:57:25 +0000</pubDate>
      <link>https://dev.to/ryantsuji/fixed-before-anyone-notices-stronger-after-every-fix-self-healing-recurrence-prevention-series-1e86</link>
      <guid>https://dev.to/ryantsuji/fixed-before-anyone-notices-stronger-after-every-fix-self-healing-recurrence-prevention-series-1e86</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;AI assistance disclosure: This article was drafted with the help of Claude. All technical content, design decisions, code references, and screenshots reflect production systems I designed and operate at airCloset; the prose was revised by me prior to publication.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Hi, I'm &lt;a href="https://x.com/ryantsuji" rel="noopener noreferrer"&gt;Ryan&lt;/a&gt;, CTO at airCloset.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer&lt;/strong&gt;: "cortex" in this article is the internal codename for an AI platform built in-house at airCloset. It is unrelated to existing commercial services like Snowflake Cortex or Palo Alto Networks Cortex.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In &lt;a href="https://dev.to/ryantsuji/human-on-the-loop-ai-reviewing-ai-prs-at-cortex-769-prsmonth-while-raising-the-quality-bar-4lh5"&gt;Part 3&lt;/a&gt; I covered &lt;strong&gt;AI reviewing AI PRs&lt;/strong&gt; -- the auto-review pipeline that defends quality &lt;strong&gt;at the PR stage&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This post is the other side: &lt;strong&gt;defending quality in production&lt;/strong&gt;, via &lt;strong&gt;Self-Healing&lt;/strong&gt;. A production alert fires, an AI investigates it, opens a fix PR, the PR goes through the same auto-review pipeline from Part 3, gets auto-merged and auto-redeployed. And the same fix PR is &lt;strong&gt;required to add a new Guide -- whether that's a lint rule, CI guard, type constraint, or guideline update&lt;/strong&gt; -- so the same anti-pattern gets auto-rejected from then on. The guardrails grow every time.&lt;/p&gt;

&lt;p&gt;"Incidents get fixed automatically" is catchy on its own, but on its own it's probably not enough in the long run. You have to &lt;strong&gt;close the recurrence class while you fix the incident&lt;/strong&gt; -- self-healing &lt;strong&gt;plus&lt;/strong&gt; self-strengthening -- before the quality gates start to compound over time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with last month's numbers
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;115 Self-Healing PRs merged in the past 30 days.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Effectively all of them merged and deployed without human involvement.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Humans only step in when the AI judges "this is not something code can fix."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's the current state of "incident response" at cortex.&lt;/p&gt;

&lt;p&gt;Don't read "115 = 115 user-impacting incidents" though. Roughly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;About half (54) are Deploy Failed-style alerts&lt;/strong&gt; -- CI / Pulumi deploy step caught a failure, the AI absorbed it &lt;strong&gt;before it shipped to production&lt;/strong&gt;. Recently the &lt;code&gt;[Recurrence]&lt;/code&gt; loop (covered later) has been piling up countermeasures here, so this bucket is trending down anecdotally&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The remaining 61 are production-runtime alerts&lt;/strong&gt; (Service Error Log Detected / Pipeline Failure / Generator Failure etc.) -- the service is running in production, but an error-log threshold or consecutive-failure threshold tripped. The AI absorbed them before they propagated to user impact&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So it's less "incident response" than "&lt;strong&gt;production anomalies that monitoring caught, fixed 115 times by AI before anyone woke up&lt;/strong&gt;." The number of incidents humans actually have to acknowledge is in the low single digits per month.&lt;/p&gt;

&lt;p&gt;There's also a clear pattern of &lt;strong&gt;the same service firing repeatedly&lt;/strong&gt; (e.g. &lt;code&gt;gcs-transformer&lt;/code&gt; is 25 of the 61) -- which is exactly what the &lt;code&gt;[Recurrence]&lt;/code&gt; loop covered later is supposed to &lt;strong&gt;eliminate by turning into lint or type gates&lt;/strong&gt;. That's the back half of this post.&lt;/p&gt;

&lt;p&gt;One more honest note: &lt;strong&gt;the recent month's number is slightly inflated&lt;/strong&gt;. The codebase had a fair number of "silent catch" patterns -- catch blocks that swallow exceptions without logging anything. We added the &lt;code&gt;no-silent-catch&lt;/code&gt; lint rule and &lt;strong&gt;swept the existing silent catches in batches&lt;/strong&gt;, which exposed previously hidden production errors as alerts. So part of the spike is "monitoring caught up to reality." Once the &lt;code&gt;[Recurrence]&lt;/code&gt; loop converts these into lint over time, the number should converge. &lt;strong&gt;"Things we couldn't see, we can see now" is a quality improvement&lt;/strong&gt; -- what we're seeing is the catch-up phase.&lt;/p&gt;

&lt;p&gt;One more thing worth saying: doing this by hand is utterly unsustainable. Running 115 manual cycles of "ack alert -&amp;gt; read logs -&amp;gt; context switch -&amp;gt; understand the code -&amp;gt; fix -&amp;gt; open PR -&amp;gt; review -&amp;gt; deploy" would bankrupt any team's engineering bandwidth. &lt;strong&gt;The system absorbs them without anyone noticing, and converts the fix into a new Guide (lint / CI guard / type constraint / guideline) at the same time&lt;/strong&gt; -- that's the actual subject of this post.&lt;/p&gt;

&lt;p&gt;The moment an alert fires, the AI starts an investigation, traces Loki / Product Graph / git blame to root cause, opens a fix PR, runs it through the auto-review from &lt;a href="https://dev.to/ryantsuji/human-on-the-loop-ai-reviewing-ai-prs-at-cortex-769-prsmonth-while-raising-the-quality-bar-4lh5"&gt;Part 3&lt;/a&gt;, APPROVE -&amp;gt; auto-merge -&amp;gt; auto-redeploy. One full loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Series
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Theme&lt;/th&gt;
&lt;th&gt;Key scene&lt;/th&gt;
&lt;th&gt;Article&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Series intro: cortex harness&lt;/td&gt;
&lt;td&gt;PRs merging unattended / incidents fixed before anyone notices&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ryantsuji/building-a-real-ai-harness-auto-reviewed-prs-self-healing-ops-and-non-engineer-contributors-3lfa"&gt;ai-harness-intro&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Product Graph (cpg)&lt;/td&gt;
&lt;td&gt;Code / docs / DB / infra unified into one graph&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ryantsuji/the-heart-of-the-ai-harness-a-knowledge-graph-of-the-ai-by-the-ai-for-the-ai-series-part-2-53bm"&gt;cortex-product-graph&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Auto PR review&lt;/td&gt;
&lt;td&gt;webhook -&amp;gt; AI review -&amp;gt; auto-fix -&amp;gt; squash merge&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ryantsuji/human-on-the-loop-ai-reviewing-ai-prs-at-cortex-769-prsmonth-while-raising-the-quality-bar-4lh5"&gt;cortex-auto-review&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Self-Healing + observability + auto-added guardrails&lt;/td&gt;
&lt;td&gt;Alert -&amp;gt; AI investigates -&amp;gt; fix PR + new lint/type gate -&amp;gt; auto redeploy + same pattern auto-rejected from then on&lt;/td&gt;
&lt;td&gt;This article ← you are here&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Democratizing the maintenance phase&lt;/td&gt;
&lt;td&gt;Domain experts open PRs to production; the harness owns the quality gate&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ryantsuji/the-author-doesnt-have-to-be-an-engineer-how-the-harness-holds-quality-series-part-5-12e4"&gt;cortex-non-engineer-prs&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Series Final&lt;/td&gt;
&lt;td&gt;The underlying philosophy plus a retrospective on the failures and lessons&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ryantsuji/ai-isnt-something-to-trust-its-something-to-design-series-final-30aa"&gt;cortex-philosophy&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Big picture -- the three layers: Observation, Repair, Strengthening
&lt;/h2&gt;

&lt;p&gt;For Self-Healing to work, you need an &lt;strong&gt;Observation layer&lt;/strong&gt; in front and a &lt;strong&gt;Strengthening layer&lt;/strong&gt; (recurrence prevention) behind it. Self-Healing itself is the middle &lt;strong&gt;Repair layer&lt;/strong&gt;. The "self-healing + self-strengthening" loop only spins up when all three are in place.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Prerequisites&lt;/strong&gt;: The three layers only stand up on top of two prior pieces: &lt;strong&gt;cpg&lt;/strong&gt; (the unified code / docs / DB / infra knowledge graph from &lt;a href="https://dev.to/ryantsuji/the-heart-of-the-ai-harness-a-knowledge-graph-of-the-ai-by-the-ai-for-the-ai-series-part-2-53bm"&gt;Part 2&lt;/a&gt;) and the &lt;strong&gt;Observability stack&lt;/strong&gt; covered in this post.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No Observability&lt;/strong&gt; -&amp;gt; the observation layer is empty, nothing gets detected -&amp;gt; the repair layer never even fires&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No cpg&lt;/strong&gt; -&amp;gt; the AI cannot see "where else does this trap exist" -&amp;gt; the repair layer does symptom-level patching at best, and the strengthening layer's horizontal expansion stops working&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Put differently: &lt;strong&gt;trying to copy this setup without those two will just multiply incidents&lt;/strong&gt;. An AI that blindly looks at error logs and rewrites production code is just speeding up the rate at which &lt;code&gt;gh pr create&lt;/code&gt; ships accidents. cpg and Observability are the &lt;strong&gt;minimum bar&lt;/strong&gt; for being able to delegate auto-repair to AI.&lt;/p&gt;

&lt;p&gt;Note also that cortex is a &lt;strong&gt;several-hundred-thousand-line codebase&lt;/strong&gt;, and at that scale loading the whole codebase as AI context is &lt;strong&gt;impossible for the AI as well&lt;/strong&gt; (let alone for a human). Tell the AI to trace impact with just grep and file reads, and it'll run out of context window before it finds anything. cpg is what lets it ask "which other code does this function's change ripple into" and get the answer in one hop. Small repos may not need this. Past a certain scale, cpg is not optional, it's &lt;strong&gt;required&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In Fowler's Guides / Sensors terms from Part 1, cpg and Observability are &lt;strong&gt;the substrate that supports both Guides (pre-execution controls like lint) and Sensors (post-execution gates like auto-review and Self-Healing)&lt;/strong&gt;. Observability feeds Sensors via firing alerts; cpg feeds the Guides side by supplying the auto-review with impact-scoping context. &lt;strong&gt;Neither belongs on one side only&lt;/strong&gt; -- they're foundational to both, and Self-Healing and auto-review only function on top of this substrate. That's the structural claim this post is built around.&lt;/p&gt;
&lt;/blockquote&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%2F9oqc8k3k9f6ljwddm50b.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%2F9oqc8k3k9f6ljwddm50b.png" alt="Three layers -- Observation -&gt; Repair -&gt; Strengthening loop" width="800" height="500"&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;Layer&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;Key components&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Observation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Real-time detection of production anomalies&lt;/td&gt;
&lt;td&gt;OTel SDK / Loki / Mimir / Tempo / Faro / Grafana / Pino logs with trace_id&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Repair&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;AI receives the alert, investigates root cause, opens a fix PR, auto-review, auto-merge, auto-redeploy&lt;/td&gt;
&lt;td&gt;Event Relay -&amp;gt; SSE -&amp;gt; &lt;code&gt;self-healing&lt;/code&gt; mode script -&amp;gt; claude -p (worktree) -&amp;gt; gh pr create&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Strengthening&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The fix PR is required to add a new Guide (lint / CI guard / type constraint / guideline). The same anti-pattern can't reach production again&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@cortex/eslint-plugin-graph&lt;/code&gt; (26 rules), &lt;code&gt;scripts/check-*.ts&lt;/code&gt; (13 guards), &lt;a href="https://github.com/air-closet/cortex-review-guidelines/blob/main/en/guidelines/recurrence-prevention.md" rel="noopener noreferrer"&gt;&lt;code&gt;recurrence-prevention.md&lt;/code&gt;&lt;/a&gt;, the &lt;code&gt;[Recurrence]&lt;/code&gt; lens of auto-review&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I'll walk through them in order.&lt;/p&gt;

&lt;h2&gt;
  
  
  Observation -- where do the alerts come from?
&lt;/h2&gt;

&lt;p&gt;cortex's production observability is built on &lt;strong&gt;Grafana Cloud + OpenTelemetry&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;OTel SDK&lt;/strong&gt; (the shared &lt;code&gt;@cortex/otel&lt;/code&gt; package) -- every service calls &lt;code&gt;initOtel({ serviceName })&lt;/code&gt; at its entry point. Trace / metric / log all go out via OTLP to Grafana Cloud&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Loki&lt;/strong&gt; (logs) -- Pino structured logs get &lt;code&gt;trace_id&lt;/code&gt; automatically. trace and log are cross-referenced&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mimir&lt;/strong&gt; (metrics) -- Cloud Run / pipeline / Gemini API token usage, etc.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tempo&lt;/strong&gt; (traces) -- distributed tracing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Faro&lt;/strong&gt; (frontend) -- captures browser JS errors / performance / network failures&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grafana&lt;/strong&gt; -- dashboards + Alert Rules + Notification Policy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We also have &lt;strong&gt;a strict definition of log levels, anchored on business impact&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Level&lt;/th&gt;
&lt;th&gt;Definition&lt;/th&gt;
&lt;th&gt;Examples&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;warn&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Business-foreseeable, &lt;strong&gt;does not need immediate action&lt;/strong&gt; (retryable / self-recovers).&lt;/td&gt;
&lt;td&gt;Search query returned 0 results, optional field unset, short retry due to rate limit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;error&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Data recovery / re-run will definitely be needed afterward&lt;/strong&gt;. Impact expected to be under 20%.&lt;/td&gt;
&lt;td&gt;"User record that should exist isn't there," BigQuery insert failure, per-record enrichment failure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;fatal&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The feature as a whole &lt;strong&gt;fails for 20%+ of requests&lt;/strong&gt;. Service-continuity broken, fatal config missing, full upstream outage.&lt;/td&gt;
&lt;td&gt;OTel init failure, required secret missing at startup, full input data source outage for a pipeline&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The key point is to &lt;strong&gt;not pick the level mechanically based on the exception class name&lt;/strong&gt; like &lt;code&gt;NotFoundError&lt;/code&gt;. Same "record not found" situation: "this record must exist and doesn't" is &lt;code&gt;error&lt;/code&gt; / &lt;code&gt;fatal&lt;/code&gt;; "user search returned 0 hits" is &lt;code&gt;warn&lt;/code&gt;. &lt;strong&gt;The level is decided by business impact&lt;/strong&gt; -- "does this require data recovery later," "is the whole feature down" -- not by the type. Without this discipline you simultaneously get monitoring fatigue and missed critical incidents. Self-Healing reacts mainly to &lt;code&gt;error&lt;/code&gt;-threshold trips; &lt;code&gt;fatal&lt;/code&gt; is the human-escalation side.&lt;/p&gt;

&lt;p&gt;Alert Rules are &lt;strong&gt;managed declaratively in Pulumi&lt;/strong&gt;, grouped by service into categories like &lt;code&gt;BOT / Pipeline / Transformer / Generator / Gemini / CI / Deploy / Service Catch-All&lt;/code&gt;. When we add a new service, one line in infra code spins up the dashboards and alerts automatically.&lt;/p&gt;

&lt;p&gt;This is "the infrastructure that lets &lt;strong&gt;the AI see the same things humans see&lt;/strong&gt;." Self-Healing picks up alerts coming off this stack.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Observability can't catch, Self-Healing can't fix either
&lt;/h3&gt;

&lt;p&gt;Honest disclaimer: Self-Healing can only react to &lt;strong&gt;what the observation layer can detect as an anomaly&lt;/strong&gt;. "Observability is everything" is literally true here.&lt;/p&gt;

&lt;p&gt;What the current stack catches is roughly &lt;strong&gt;logic-level errors&lt;/strong&gt; -- exceptions, error logs, deploy failures, external-API call failures, threshold-based metric anomalies.&lt;/p&gt;

&lt;p&gt;What it doesn't catch:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;UI errors&lt;/strong&gt; -- the logic ran, no error logs, but the screen &lt;strong&gt;shows something different from intent / shows the wrong value&lt;/strong&gt;. Faro catches client-side JS exceptions and network failures, but "the logic ran and the output is just wrong" never fires an alert&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Silent data corruption&lt;/strong&gt; -- aggregated values slowly drift, bad values get into a table. Unless it crosses a threshold or schema check, nothing detects it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Perceived UX degradation&lt;/strong&gt; -- requests feel slow, the UX feels off. Only catchable once SLO / latency thresholds trip&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So Self-Healing is "&lt;strong&gt;AI replacing the human in the loop for incidents the observation layer can catch&lt;/strong&gt;." &lt;strong&gt;The coverage of the observation layer itself is the prerequisite.&lt;/strong&gt; Holes in observation stay as blind spots that neither auto-review nor Self-Healing reaches.&lt;/p&gt;

&lt;p&gt;This isn't really a limitation of Self-Healing -- it's the &lt;strong&gt;importance of growing the observation stack&lt;/strong&gt;, which cortex keeps investing in continuously. (From &lt;a href="https://dev.to/ryantsuji/building-a-real-ai-harness-auto-reviewed-prs-self-healing-ops-and-non-engineer-contributors-3lfa"&gt;Part 1&lt;/a&gt;, Observability is one of the "supporting foundations" beneath the flywheel.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Repair -- the Self-Healing flow
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;MODE=self-healing&lt;/code&gt; runs the same &lt;code&gt;webhook-server&lt;/code&gt; script as the auto-review setup from &lt;a href="https://dev.to/ryantsuji/human-on-the-loop-ai-reviewing-ai-prs-at-cortex-769-prsmonth-while-raising-the-quality-bar-4lh5"&gt;Part 3&lt;/a&gt;, but listening for Grafana firing alerts.&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%2Fjoa2w71p3guljzdg6sou.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%2Fjoa2w71p3guljzdg6sou.png" alt="Self-Healing full flow -- median 30 min to 1 hr from firing alert to production recovery" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The textual flow looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Grafana Alert Rule firing]
   ↓ POST /webhook/grafana
[Event Relay (in-house)] -- persisted in Firestore
   ↓ SSE push (event: grafana-alert)
[self-healing mode script]
   ↓ throttle check (same fingerprint skipped for 4h)
   ↓ 👀 reaction in Slack to signal "I'm on it"
   ↓ git worktree add -b hotfix/auto-alert-{service}-{ts} origin/main
   ↓ run claude -p inside the worktree
     - search related code via Product Graph MCP
     - pull error logs from Loki via Grafana MCP
     - identify root cause and fix
     - update tests as needed
     - conventional commit
   ↓ git push + gh pr create
[fix PR]
   ↓ auto-review (the Part 3 pipeline)
   ↓ APPROVE -&amp;gt; auto-merge -&amp;gt; auto-redeploy
[recovered]
   ↓ ✅ in the Slack thread
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What happens when the AI judges "this is not fixable in code"
&lt;/h3&gt;

&lt;p&gt;Not every alert is fixable by code. The implementation has a rule: "if you judge it unfixable, exit without changing anything." In that case Slack gets a notification of the form "&lt;strong&gt;This alert cannot be addressed in code. Investigation: ...&lt;/strong&gt;" -- &lt;strong&gt;including what the AI investigated&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Worth clarifying on the numbers side: the headline &lt;strong&gt;115&lt;/strong&gt; is "Self-Healing runs that reached PR-created -&amp;gt; merged -&amp;gt; deployed." This "unfixable, exit clean" case is a &lt;strong&gt;separate bucket&lt;/strong&gt;, happening several times a month (external transient outages, infra / config issues that aren't code, cases too complex for the AI to judge confidently). &lt;strong&gt;The "humans show up" bucket is this separate one&lt;/strong&gt; -- it isn't a "some of the 115 failed" failure rate.&lt;/p&gt;

&lt;p&gt;Here's what a real Slack message looks like (a &lt;code&gt;styling-pattern-watch-transformer&lt;/code&gt; case where the GitHub PAT had expired):&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ This alert cannot be addressed in code. Investigation:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Investigation summary&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Checked the error logs in Loki for the past hour. The single error source for &lt;code&gt;styling-pattern-watch-transformer&lt;/code&gt; right now is &lt;strong&gt;expiration / revocation of the GitHub PAT (Personal Access Token)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I found&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The BQ snapshot pipeline is healthy: 7 consecutive &lt;code&gt;[BQ] Snapshot rows inserted&lt;/code&gt; successes. The earlier autodetect type-drift problem was resolved in an earlier PR that put &lt;code&gt;SAFE_CAST(... AS INT64)&lt;/code&gt; in place.&lt;/li&gt;
&lt;li&gt;The single ongoing error (multiple occurrences in the past hour): &lt;code&gt;GithubUnauthorizedError: GitHub API /commits/master unauthorized (401): PAT likely expired/revoked&lt;/code&gt; -- the &lt;code&gt;/check-drift&lt;/code&gt; handler returns 401 every time Cloud Scheduler invokes it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Root cause&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The GitHub PAT held in Secret Manager for this service has expired or been revoked. The code already classifies the 401 with a dedicated &lt;code&gt;GithubUnauthorizedError&lt;/code&gt; and is already sending the "PAT needs rotation" notification to Slack. &lt;strong&gt;The detection / notification design on the code side is working as intended.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Resolution: exit without changes&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is an external-credential operational issue (PAT expiry), and what needs to happen is &lt;strong&gt;a human rotates the PAT in Secret Manager&lt;/strong&gt;. Not the kind of incident code can resolve. Therefore: no code change, no commit, no PR.&lt;/p&gt;

&lt;p&gt;Operator actions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add a new version of the target secret in GCP Console / Secret Manager with a valid PAT (&lt;code&gt;repo&lt;/code&gt; scope, read access to the target repository)&lt;/li&gt;
&lt;li&gt;No Cloud Run revision redeploy needed (&lt;code&gt;secretKeyRef version:latest&lt;/code&gt; is referenced)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;At this level of detail, "what was investigated, why code can't fix it, what the human should do" all come out in one Slack message. Open the thread and the operator can act immediately. The productivity gap vs. "alerts just forwarded blindly" is significant.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deduplication
&lt;/h3&gt;

&lt;p&gt;A throttle ensures the same &lt;code&gt;fingerprint&lt;/code&gt; (Grafana's unique alert identifier) is &lt;strong&gt;not re-processed for 4 hours&lt;/strong&gt;. Without this, alerts that fire again before the fix PR has merged would spawn another worktree, another fix PR, and so on -- an easy infinite loop.&lt;/p&gt;

&lt;p&gt;We also &lt;strong&gt;permanently skip&lt;/strong&gt; any &lt;code&gt;alertname&lt;/code&gt; containing &lt;code&gt;credential&lt;/code&gt;. Credential incidents carry leakage risk if the AI touches them, so they're explicitly escalated to humans.&lt;/p&gt;

&lt;h3&gt;
  
  
  Self-Healing and Part 3 auto-review -- "the fixer AI" and "the reviewer AI" are independent
&lt;/h3&gt;

&lt;p&gt;This is the most consequential design choice of the agent setup, so calling it out explicitly.&lt;/p&gt;

&lt;p&gt;PRs opened by Self-Healing are &lt;strong&gt;not special PRs, just fix PRs&lt;/strong&gt;. They go through the Part 3 auto-review pipeline &lt;strong&gt;under exactly the same conditions&lt;/strong&gt; -- the 9 lenses (Graph / Architecture / Security / Test / Doc / Impact / Observability / AI-Antipattern / Recurrence) get checked in order. Critical / Major findings -&amp;gt; &lt;code&gt;REQUEST_CHANGES&lt;/code&gt;; Nit-only / no findings + CI green -&amp;gt; &lt;code&gt;APPROVE&lt;/code&gt; -&amp;gt; auto-merge.&lt;/p&gt;

&lt;p&gt;The important bit: &lt;strong&gt;this is not a monolithic "AI fixing AI" loop&lt;/strong&gt;. The fixer-side AI and the reviewer-side AI are fully independent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Different process, different session&lt;/strong&gt;: the self-healing-mode AI and the reviewer-mode AI are launched as separate &lt;code&gt;claude -p&lt;/code&gt; processes. They do not share context&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Different input sources&lt;/strong&gt;: the fixer builds the problem from Grafana alert + Loki + cpg. The reviewer judges from the PR diff + cpg + review guidelines&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Different objectives&lt;/strong&gt;: the fixer is optimizing for "stop the incident." The reviewer is judging "does this violate the 9 lenses or the severity contract?" A deliberate separation of concerns where the two roles' incentives are intentionally misaligned&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As a result, &lt;strong&gt;PRs the fixer dashed off get blocked by the reviewer&lt;/strong&gt; (REQUEST_CHANGES -&amp;gt; back to the fixer). The AI does not approve its own output. "Just-make-it-work" fixes don't get through.&lt;/p&gt;

&lt;p&gt;This is the often-debated &lt;strong&gt;review-independence&lt;/strong&gt; problem in LLM-agent operation, solved here in the obvious way: split the work across separate agents.&lt;/p&gt;

&lt;h3&gt;
  
  
  A concrete example: meet subscription's 409 ALREADY_EXISTS
&lt;/h3&gt;

&lt;p&gt;Take the alert from the Google Meet recording auto-fetch service I covered in &lt;a href="https://dev.to/ryantsuji/how-we-built-an-automated-meeting-intelligence-system-with-google-meet-slack-and-rag-42ln"&gt;the Meeting Intelligence post&lt;/a&gt;. On 2026-05-21, Self-Healing opened a fix PR titled &lt;code&gt;fix(meet-subscription-renewal): auto-fix for Service Error Log Detected&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The trigger error from Loki:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Workspace Events API request failed: 409 Conflict
"Subscription associated with the resource already exists."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;How the AI investigated:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pinned the error in Loki&lt;/strong&gt; -- ran &lt;code&gt;{service_name="meet-subscription-renewal"} | json | level=~"ERROR|error|Error"&lt;/code&gt; via Grafana MCP, picked up the &lt;code&gt;Failed to renew Meet subscription&lt;/code&gt; stack trace&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Traced the call path in Product Graph&lt;/strong&gt; -- identified &lt;code&gt;renewSubscriptions&lt;/code&gt; -&amp;gt; &lt;code&gt;createMeetSubscription&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-referenced past PRs&lt;/strong&gt; -- the "opposite-direction inconsistency" (name in Firestore but missing from Google = 404) had already been self-healed in another PR with &lt;code&gt;patchMeetSubscriptionTtl&lt;/code&gt; -&amp;gt; null fallback. &lt;strong&gt;The current direction (still on Google's side but missing from Firestore = 409) was the gap&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verdict&lt;/strong&gt;: "the same pattern may exist elsewhere" -- a [Recurrence] decision matrix "&lt;strong&gt;horizontal expansion required&lt;/strong&gt;" case&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Instead of a quick patch, &lt;strong&gt;it implemented the same-direction self-healing symmetrically to the opposite-direction fallback that was already there&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Made &lt;code&gt;createMeetSubscription&lt;/code&gt; idempotent&lt;/li&gt;
&lt;li&gt;If POST returns 409, extract the existing Subscription name from the response and call &lt;code&gt;patchMeetSubscriptionTtl&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The caller writes the return value back into Firestore, so the next renewal converges to the normal PATCH path (&lt;strong&gt;self-healing&lt;/strong&gt;)&lt;/li&gt;
&lt;li&gt;Per the existing &lt;code&gt;graph/no-silent-catch&lt;/code&gt; lint, JSON.parse failures are also &lt;code&gt;logger.warn&lt;/code&gt; + &lt;code&gt;serializeError&lt;/code&gt; for structured logging&lt;/li&gt;
&lt;li&gt;Three tests added&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is what "Self-Healing pushing all the way to root cause and rolling the fix out horizontally" looks like in practice. &lt;strong&gt;"Close the recurrence class, don't just suppress the symptom"&lt;/strong&gt; (the spirit of &lt;a href="https://github.com/air-closet/cortex-review-guidelines/blob/main/en/guidelines/recurrence-prevention.md" rel="noopener noreferrer"&gt;&lt;code&gt;recurrence-prevention.md&lt;/code&gt;&lt;/a&gt;) executed autonomously by the AI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Strengthening -- Guides (lint + guidelines) grow automatically
&lt;/h2&gt;

&lt;p&gt;This is the layer that &lt;strong&gt;keeps Self-Healing from being just "auto-repair."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In Fowler's Guides / Sensors terms from &lt;a href="https://dev.to/ryantsuji/building-a-real-ai-harness-auto-reviewed-prs-self-healing-ops-and-non-engineer-contributors-3lfa"&gt;Part 1&lt;/a&gt;, the Strengthening layer is &lt;strong&gt;the place where Guides grow&lt;/strong&gt; -- i.e. the pre-execution controls that prevent AI from deviating in the first place. cortex's Guides come in two flavors:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Machine-read Guides&lt;/strong&gt;: lint / type / CI guard / coverage thresholds / Prettier -- enforced at commit / CI time&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Human-and-AI-read Guides&lt;/strong&gt;: guidelines like &lt;a href="https://github.com/air-closet/cortex-review-guidelines/blob/main/en/guidelines/recurrence-prevention.md" rel="noopener noreferrer"&gt;&lt;code&gt;recurrence-prevention.md&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://github.com/air-closet/cortex-review-guidelines/blob/main/en/guidelines/severity.md" rel="noopener noreferrer"&gt;&lt;code&gt;severity.md&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://github.com/air-closet/cortex-review-guidelines/blob/main/en/guidelines/ai-antipattern.md" rel="noopener noreferrer"&gt;&lt;code&gt;ai-antipattern.md&lt;/code&gt;&lt;/a&gt;, etc. -- used as decision criteria by auto-review&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The 9 lenses, severity contract, and no-downgrade rules from &lt;a href="https://dev.to/ryantsuji/human-on-the-loop-ai-reviewing-ai-prs-at-cortex-769-prsmonth-while-raising-the-quality-bar-4lh5"&gt;Part 3&lt;/a&gt; are the latter; the auto-added lints in Part 4 are the former. &lt;strong&gt;Together they form the Guides surface&lt;/strong&gt;. Lints are "formalized guidelines," guidelines are "lints that haven't been formalized yet."&lt;/p&gt;

&lt;p&gt;The Sensors side -- Self-Healing and auto-review -- &lt;strong&gt;grow these Guides every time they run&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Self-Healing's root-cause investigation finds "the same pattern exists elsewhere" -&amp;gt; demands horizontal expansion + a new lint (= new Guide)&lt;/li&gt;
&lt;li&gt;Auto-review's &lt;code&gt;[Recurrence]&lt;/code&gt; lens blocks PRs that fix without adding lint&lt;/li&gt;
&lt;li&gt;Both depend on &lt;a href="https://dev.to/ryantsuji/the-heart-of-the-ai-harness-a-knowledge-graph-of-the-ai-by-the-ai-for-the-ai-series-part-2-53bm"&gt;cpg&lt;/a&gt; to see impact scope across the codebase&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;cpg is what lets the AI ask "where else does this trap exist." Self-Healing and auto-review (= the Sensors side) &lt;strong&gt;share cpg as a substrate, and each run thickens Guides by one notch&lt;/strong&gt;.&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%2F5m28vtcko417bg44sr3e.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%2F5m28vtcko417bg44sr3e.png" alt="cpg as the shared substrate; Self-Healing and auto-review (Sensors) grow Guides" width="800" height="543"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What happens every time Self-Healing runs (the recurrence-prevention-first flow)
&lt;/h3&gt;

&lt;p&gt;Every fix PR Self-Healing opens is checked for &lt;code&gt;[Recurrence]&lt;/code&gt; by auto-review. The decision matrix:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;Required action&lt;/th&gt;
&lt;th&gt;Form&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Same trap stepped on 2+ times&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Lint required&lt;/strong&gt; (custom ESLint rule / type constraint / CI guard)&lt;/td&gt;
&lt;td&gt;Machine (new Guide)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pattern may exist elsewhere&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Horizontal expansion required&lt;/strong&gt; (cpg traversal for similar nodes, fix all of them in this PR)&lt;/td&gt;
&lt;td&gt;Investigation + fix&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cannot be machine-checked but worth formalizing&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Add to an existing guideline&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Guideline entry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One-off, no value in formalization&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Nothing&lt;/strong&gt; (bug fix only)&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;When the "stepped on 2+ times" situation applies, &lt;strong&gt;the fix PR can't merge without a new lint included&lt;/strong&gt;. So every Self-Healing run produces:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Horizontal expansion via cpg&lt;/strong&gt; -- not just the immediate fix target, every similar node enumerated&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A new Guide added in the same PR&lt;/strong&gt; -- ESLint custom rule / type constraint / CI guard / guideline entry, one of the four&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;All existing violations cleared in the same PR&lt;/strong&gt; -- no &lt;code&gt;warn&lt;/code&gt;-as-deferral, &lt;code&gt;error&lt;/code&gt; on first introduction&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto-review -&amp;gt; auto-merge -&amp;gt; auto-redeploy&lt;/strong&gt; -- the regular Part 3 pipeline&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Going forward, writing the same pattern gets mechanically rejected by CI / lint&lt;/strong&gt; -- the recurrence class is structurally closed&lt;/li&gt;
&lt;/ol&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%2F3imbzr9glo3hdw7wqwps.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%2F3imbzr9glo3hdw7wqwps.png" alt="5 steps every Self-Healing run produces -- recurrence-prevention-first flow" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;"Add the guard while you fix the bug" runs as a self-sustaining loop driven by Self-Healing.&lt;/p&gt;

&lt;h3&gt;
  
  
  "We'll do it later" and "introduce as &lt;code&gt;warn&lt;/code&gt;" are banned
&lt;/h3&gt;

&lt;p&gt;A couple of important contract clauses from the guidelines:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Plan to lint later," "lint when we refactor," "another PR will handle this" -- &lt;strong&gt;all banned&lt;/strong&gt;. If it can be addressed in this PR, it must be&lt;/li&gt;
&lt;li&gt;"Existing violations remain, so introduce as &lt;code&gt;warn&lt;/code&gt; and promote to &lt;code&gt;error&lt;/code&gt; later" -- &lt;strong&gt;not accepted&lt;/strong&gt;. This is deferral in disguise. The responsibility for the &lt;code&gt;warn&lt;/code&gt;-&amp;gt;&lt;code&gt;error&lt;/code&gt; promotion goes nowhere and the rule rots&lt;/li&gt;
&lt;li&gt;If you add a lint rule, &lt;strong&gt;fix all existing violations in the same PR and ship at &lt;code&gt;error&lt;/code&gt;&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These extend the &lt;strong&gt;no-downgrade rules&lt;/strong&gt; from &lt;a href="https://dev.to/ryantsuji/human-on-the-loop-ai-reviewing-ai-prs-at-cortex-769-prsmonth-while-raising-the-quality-bar-4lh5"&gt;Part 3&lt;/a&gt; -- preempting the typical escape hatches.&lt;/p&gt;

&lt;h3&gt;
  
  
  The "step on it, mechanize it" lineage
&lt;/h3&gt;

&lt;p&gt;Custom Guides currently piled up in cortex:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;graph/no-silent-catch&lt;/code&gt;&lt;/strong&gt; (ESLint) -- the source of the "inflated number" mentioned in the intro. Bans catch blocks that swallow exceptions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stacktrace-preservation guideline&lt;/strong&gt; (codified as a Major violation in &lt;a href="https://github.com/air-closet/cortex-review-guidelines/blob/main/en/guidelines/observability.md" rel="noopener noreferrer"&gt;&lt;code&gt;observability.md&lt;/code&gt;&lt;/a&gt;, caught by auto-review) -- forbids &lt;code&gt;logger.error(err.message)&lt;/code&gt; style logs that drop the stack and keep only the message string. Forces the &lt;code&gt;err&lt;/code&gt; field to hold &lt;code&gt;serializeError(error)&lt;/code&gt; so &lt;code&gt;name&lt;/code&gt; / &lt;code&gt;message&lt;/code&gt; / &lt;code&gt;stack&lt;/code&gt; are preserved as structured fields. &lt;strong&gt;Observability is everything&lt;/strong&gt; here, so logs that drop stack info are treated as inherently broken&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;cortex-quality/require-fetch-timeout&lt;/code&gt;&lt;/strong&gt; (oxlint -- a Rust-implemented JS/TS lint that runs ESLint-compatible rule sets, dozens of times faster than ESLint due to the Rust impl. cortex uses oxlint for the standard ruleset and ESLint for custom rules that need AST-level work) -- mandates &lt;code&gt;signal: AbortSignal.timeout(...)&lt;/code&gt; on external &lt;code&gt;fetch&lt;/code&gt; calls. Born from a case where a no-timeout &lt;code&gt;fetch&lt;/code&gt; hung indefinitely and triggered a Cloud Tasks redelivery storm&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;graph/no-bq-string-timestamp-param&lt;/code&gt;&lt;/strong&gt; (ESLint) -- from a case where passing TIMESTAMP as a string to a BigQuery query parameter NULLed the value out through a serializer bug and silently failed every INSERT&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;graph/require-firestore-ignore-undefined&lt;/code&gt;&lt;/strong&gt; (ESLint) -- forces &lt;code&gt;ignoreUndefinedProperties: true&lt;/code&gt; on &lt;code&gt;new Firestore()&lt;/code&gt;. From a case where a single NULL row caused a 100% failure rate in a sync batch&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;check-otel-env-injection&lt;/code&gt;&lt;/strong&gt; (CI guard) -- the recurrence prevention for the Cloud Run OTel env injection case below&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TypeScript type tightening&lt;/strong&gt; (type level) -- tighter function signatures, branded types for ID disambiguation, exhaustive discriminated unions, etc. Patterns that can't be lint-caught but are catchable at the type level get closed from the type side&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These aren't textbook-learnable rules -- they're "&lt;strong&gt;stepped on once, then mechanized&lt;/strong&gt;." The number of traps the organization has stepped on translates directly into the number of Guides piled up (across ESLint / oxlint / CI guard / types).&lt;/p&gt;

&lt;h3&gt;
  
  
  How does the AI write a lint rule without breaking it?
&lt;/h3&gt;

&lt;p&gt;Three structural things keep this sane:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Existing rules are the template&lt;/strong&gt;: &lt;code&gt;packages/eslint-plugin-graph/src/rules/&lt;/code&gt; already holds 26 custom rules, each as &lt;code&gt;.ts&lt;/code&gt; + &lt;code&gt;.test.ts&lt;/code&gt; pairs. New rules follow the same shape, so the AI never has to write the AST-walking boilerplate from scratch&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tests first&lt;/strong&gt;: violation / pass fixtures go into &lt;code&gt;.test.ts&lt;/code&gt; first, implementation fills in TDD-style. Coverage threshold (90% statements + branches) is gated by the &lt;a href="https://dev.to/ryantsuji/human-on-the-loop-ai-reviewing-ai-prs-at-cortex-769-prsmonth-while-raising-the-quality-bar-4lh5"&gt;Part 3&lt;/a&gt; auto-review, so a lint without tests cannot merge&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;lint / type / CI guard sit in the same "mechanize" bucket&lt;/strong&gt;: the decision matrix in &lt;a href="https://github.com/air-closet/cortex-review-guidelines/blob/main/en/guidelines/recurrence-prevention.md" rel="noopener noreferrer"&gt;&lt;code&gt;recurrence-prevention.md&lt;/code&gt;&lt;/a&gt; groups lint / type constraint / CI guard together as the "lint-required" row, and leaves the choice within that bucket (write it as a lint? express it at the type level? add a separate CI guard?) to the AI based on how much AST work is involved and whether runtime semantics matter. Traps that need AST inspection but actually hinge on runtime behavior usually end up as a type constraint (branded type / discriminated union / signature tightening) rather than a custom lint&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So "AI writes a lint rule" is supported by &lt;strong&gt;existing rule corpus + the test harness + the mechanize-bucket selection criteria&lt;/strong&gt; -- three together. The path where the AI hand-rolls raw ESLint API and bricks something is structurally closed.&lt;/p&gt;

&lt;h3&gt;
  
  
  A concrete example: Cloud Run OTel env injection -&amp;gt; promoted to CI guard
&lt;/h3&gt;

&lt;p&gt;Multiple services hit this trap: when a Cloud Run Service / Job is defined in Pulumi, forgetting to inject &lt;code&gt;OTEL_EXPORTER_OTLP_ENDPOINT&lt;/code&gt; and &lt;code&gt;GRAFANA_CLOUD_API_KEY&lt;/code&gt; via &lt;code&gt;secretKeyRef&lt;/code&gt; causes OTel init to be skipped in production, no trace/log reaches Grafana, and incidents become silently invisible.&lt;/p&gt;

&lt;p&gt;The normal response would be "we'll be more careful next time." At cortex:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Incident surfaces -&amp;gt; Self-Healing opens a fix PR (adds the env injection to the affected service)&lt;/li&gt;
&lt;li&gt;Auto-review's &lt;code&gt;[Recurrence]&lt;/code&gt; decides "same trap stepped on -&amp;gt; lint required"&lt;/li&gt;
&lt;li&gt;The same PR adds &lt;code&gt;scripts/check-otel-env-injection.ts&lt;/code&gt; (CI guard) -- mechanically asserts OTel env injection across all Cloud Run resource definitions under &lt;code&gt;infra/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;All other existing services get their env injection added in the same PR&lt;/li&gt;
&lt;li&gt;Merge -&amp;gt; deploy -&amp;gt; any future write of the same kind gets rejected by CI&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's what "the guardrails grow every time Self-Healing runs" looks like in practice. The trap is "stepped on -&amp;gt; mechanically checked from then on."&lt;/p&gt;

&lt;h3&gt;
  
  
  Where Guides stand right now (in numbers)
&lt;/h3&gt;

&lt;p&gt;Snapshot of cortex's Guide inventory:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Count&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;
&lt;strong&gt;Custom ESLint rules&lt;/strong&gt; (&lt;code&gt;@cortex/eslint-plugin-graph&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;26&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;no-silent-catch&lt;/code&gt; / &lt;code&gt;require-firestore-ignore-undefined&lt;/code&gt; / &lt;code&gt;no-bq-string-timestamp-param&lt;/code&gt; etc.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;CI guards&lt;/strong&gt; (&lt;code&gt;scripts/check-*.ts&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;check-otel-env-injection&lt;/code&gt; / &lt;code&gt;check-cloudscheduler-oidctoken-audience&lt;/code&gt; etc.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Standard oxlint rules&lt;/strong&gt; (set to &lt;code&gt;error&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;183&lt;/td&gt;
&lt;td&gt;Base config ships everything at error&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;TypeScript strict gates&lt;/strong&gt; (baseline)&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;strict&lt;/code&gt; / &lt;code&gt;noImplicitAny&lt;/code&gt; / &lt;code&gt;strictNullChecks&lt;/code&gt; / &lt;code&gt;noUncheckedIndexedAccess&lt;/code&gt; etc.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;TypeScript type tightening&lt;/strong&gt; (per-recurrence)&lt;/td&gt;
&lt;td&gt;grows over time&lt;/td&gt;
&lt;td&gt;branded type / discriminated union / function-signature tightening etc. Patterns that can't be lint-caught but can be type-caught are closed from the type side&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Test coverage thresholds&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;statements + branches 90%&lt;/td&gt;
&lt;td&gt;Uniform across all packages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Prettier&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1 config&lt;/td&gt;
&lt;td&gt;Format auto-fix&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Guidelines&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;the entire review-guidelines repo&lt;/td&gt;
&lt;td&gt;Used as the decision basis by auto-review&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The first two categories plus the type-tightening row -- &lt;strong&gt;Custom ESLint, CI guard, type tightening&lt;/strong&gt; -- are the part that &lt;strong&gt;compounds over time&lt;/strong&gt; through the &lt;code&gt;[Recurrence]&lt;/code&gt; lens every time Self-Healing or auto-review runs. &lt;strong&gt;The guardrails grow with time.&lt;/strong&gt; That's the substance of the Strengthening layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The whole loop, from the top
&lt;/h2&gt;

&lt;p&gt;When you compose the three layers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[production anomaly] -&amp;gt; Observation layer (OTel/Loki/Grafana) -&amp;gt; Alert firing
                                              ↓
                                       Event Relay -&amp;gt; SSE
                                              ↓
[Self-Healing mode script]
   - claude -p in worktree
   - root cause via cpg + Loki + git blame
   - commit fix
   - (if applicable) add new lint / type gate too
   - gh pr create
                                              ↓
[Auto-review (Part 3)] -- 9 lenses in order, especially [Recurrence] forces
                         recurrence-prevention action (lint / horizontal expansion / guideline entry)
                                              ↓
                          APPROVE + CI green
                                              ↓
[auto-merge -&amp;gt; Turborepo build -&amp;gt; Pulumi parallel deploy]
                                              ↓
[production recovered + same anti-pattern mechanically rejected from now on]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The loop &lt;strong&gt;completes without human intervention&lt;/strong&gt;. Not just repair, but the quality gates that grow with every repair -- that's the "auto-recovery + auto-strengthening" substance at cortex.&lt;/p&gt;

&lt;p&gt;That said, as the front of the article spelled out, &lt;strong&gt;the loop is only viable because cpg and Observability exist&lt;/strong&gt;. cpg makes horizontal expansion possible; Observability turns production anomalies into structured data. With those two in place at the foundation, AI can stand on the side that does Repair and Strengthening. &lt;strong&gt;Self-Healing is not a standalone mechanism. It's a Sensor riding on top of cortex's Guides (cpg + Observability + lint + guidelines).&lt;/strong&gt; That's the single most important framing in this post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Self-Healing by the numbers
&lt;/h2&gt;

&lt;p&gt;Breaking the headline down further.&lt;/p&gt;

&lt;h3&gt;
  
  
  Main firing categories
&lt;/h3&gt;

&lt;p&gt;What kicked off Self-Healing in the past 30 days (with the mapping back to the front-of-post 2 buckets):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Bucket&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Service Error Log Detected&lt;/strong&gt; (most frequent)&lt;/td&gt;
&lt;td&gt;Production-runtime (61 side)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Pipeline Failure&lt;/strong&gt; -- data pipeline failing a configured number of times in a row&lt;/td&gt;
&lt;td&gt;Production-runtime (61 side)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Generator Failure&lt;/strong&gt; -- AI generation jobs (embedding / annotation etc.) failing&lt;/td&gt;
&lt;td&gt;Production-runtime (61 side)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Deploy Failed&lt;/strong&gt; -- deploy step failures (Pulumi up / Cloud Run revision failed)&lt;/td&gt;
&lt;td&gt;Deploy step (54 side)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Alert-firing to production-recovery time
&lt;/h3&gt;

&lt;p&gt;Median &lt;strong&gt;30 minutes to 1 hour&lt;/strong&gt;. Roughly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Alert firing -&amp;gt; AI investigation start: under 1 minute (Event Relay + SSE)&lt;/li&gt;
&lt;li&gt;AI investigation + fix + PR open: 3-8 minutes&lt;/li&gt;
&lt;li&gt;Auto-review (including the &lt;a href="https://dev.to/ryantsuji/human-on-the-loop-ai-reviewing-ai-prs-at-cortex-769-prsmonth-while-raising-the-quality-bar-4lh5"&gt;Part 3&lt;/a&gt; &lt;strong&gt;10.8 review-fix iterations on average&lt;/strong&gt;): 20-45 minutes&lt;/li&gt;
&lt;li&gt;Auto-merge + deploy: 3-10 minutes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Many of these finish before anyone wakes up (alert fires early morning -&amp;gt; by the time people come in, there's just a ✅ in Slack).&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed / Bridge to Part 5
&lt;/h2&gt;

&lt;p&gt;We've now covered &lt;strong&gt;the cortex picture across Parts 1-4&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://dev.to/ryantsuji/building-a-real-ai-harness-auto-reviewed-prs-self-healing-ops-and-non-engineer-contributors-3lfa"&gt;Part 1&lt;/a&gt;: the cortex big picture and harness-engineering framing&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/ryantsuji/the-heart-of-the-ai-harness-a-knowledge-graph-of-the-ai-by-the-ai-for-the-ai-series-part-2-53bm"&gt;Part 2&lt;/a&gt;: Product Graph (cpg) -- the AI's "brain"&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/ryantsuji/human-on-the-loop-ai-reviewing-ai-prs-at-cortex-769-prsmonth-while-raising-the-quality-bar-4lh5"&gt;Part 3&lt;/a&gt;: auto-review -- defending quality at the PR stage&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Part 4 (this post): Self-Healing + Observability + auto-added guardrails -- defending quality in production while growing the quality gates themselves&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The engineering role has shifted, over the last half-year, from "&lt;strong&gt;write&lt;/strong&gt;, &lt;strong&gt;review&lt;/strong&gt;, &lt;strong&gt;fix&lt;/strong&gt;, &lt;strong&gt;merge&lt;/strong&gt;, &lt;strong&gt;deploy&lt;/strong&gt;, &lt;strong&gt;incident-respond&lt;/strong&gt;" -- all of that -- toward &lt;strong&gt;looking at the whole system from above and tuning it&lt;/strong&gt;. &lt;code&gt;human-on-the-loop&lt;/code&gt;, working at the Policy layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 5&lt;/strong&gt; covers the harness reaching the "who writes the code" layer. The center of it is &lt;strong&gt;domain experts (business-side managers, PMOs — non-engineers) opening PRs to production&lt;/strong&gt;, with a concrete walk-through of a +1,742 line / 41 file feature PR that landed with zero human reviewers in the loop. What guarantees the quality is the harness stack built across this series — "whoever writes, the harness owns the quality gate" is the Part 5 framing.&lt;/p&gt;

&lt;p&gt;The toC service expansion gets a brief mention at the end for direction, but the full implementation discussion lives in a separate post.&lt;/p&gt;

&lt;p&gt;The actual series wrap-up is &lt;strong&gt;Part 6&lt;/strong&gt;. The center of it is &lt;strong&gt;the underlying philosophy&lt;/strong&gt; -- why I picked this design, what I gave up, what I kept. Alongside that, since the series so far has been mostly "what's working," I want to look back at the failures and dead ends behind that surface, and the gap between the philosophy and the implementation. A retrospective for myself, and -- hopefully -- a reference for anyone starting down a similar path.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>devops</category>
      <category>github</category>
      <category>observability</category>
    </item>
    <item>
      <title>Human-on-the-Loop: AI Reviewing AI PRs at cortex -- 769 PRs/month while raising the quality bar (Series Part 3)</title>
      <dc:creator>Ryosuke Tsuji</dc:creator>
      <pubDate>Tue, 26 May 2026 14:35:43 +0000</pubDate>
      <link>https://dev.to/ryantsuji/human-on-the-loop-ai-reviewing-ai-prs-at-cortex-769-prsmonth-while-raising-the-quality-bar-4lh5</link>
      <guid>https://dev.to/ryantsuji/human-on-the-loop-ai-reviewing-ai-prs-at-cortex-769-prsmonth-while-raising-the-quality-bar-4lh5</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;AI assistance disclosure: This article was drafted with the help of Claude. All technical content, design decisions, code references, and screenshots reflect production systems I designed and operate at airCloset; the prose was revised by me prior to publication.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Hi, I'm &lt;a href="https://x.com/ryantsuji" rel="noopener noreferrer"&gt;Ryan&lt;/a&gt;, CTO at airCloset.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer&lt;/strong&gt;: "cortex" in this article is the internal codename for an AI platform built in-house at airCloset. It is unrelated to existing commercial services like Snowflake Cortex or Palo Alto Networks Cortex.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In &lt;a href="https://dev.to/ryantsuji/building-a-real-ai-harness-auto-reviewed-prs-self-healing-ops-and-non-engineer-contributors-3lfa"&gt;Part 1 (intro)&lt;/a&gt; I covered the high level -- &lt;strong&gt;AI driving both PR reviews and incident response on top of cortex&lt;/strong&gt;. In &lt;a href="https://dev.to/ryantsuji/the-heart-of-the-ai-harness-a-knowledge-graph-of-the-ai-by-the-ai-for-the-ai-series-part-2-53bm"&gt;Part 2 (Product Graph)&lt;/a&gt; I went deep on &lt;strong&gt;cpg&lt;/strong&gt;, the unified knowledge graph that fuses code, docs, DB schemas and infra into a single business-aware index.&lt;/p&gt;

&lt;p&gt;This post is about &lt;strong&gt;the automated PR review pipeline&lt;/strong&gt; -- AI reviews the PR, a separate AI applies the fixes, and the system merges automatically once policy gates pass. The usual critiques of AI-assisted development ("&lt;strong&gt;the reviewer becomes the bottleneck&lt;/strong&gt;" and "&lt;strong&gt;AI code drops the quality bar&lt;/strong&gt;") don't really apply here. The rest of this post unpacks why.&lt;/p&gt;

&lt;h2&gt;
  
  
  Series
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Theme&lt;/th&gt;
&lt;th&gt;Key scene&lt;/th&gt;
&lt;th&gt;Article&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Series intro: cortex harness&lt;/td&gt;
&lt;td&gt;PRs merging unattended / incidents fixed before anyone notices&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ryantsuji/building-a-real-ai-harness-auto-reviewed-prs-self-healing-ops-and-non-engineer-contributors-3lfa"&gt;ai-harness-intro&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Product Graph (cpg)&lt;/td&gt;
&lt;td&gt;Code / docs / DB / infra unified into one graph&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ryantsuji/the-heart-of-the-ai-harness-a-knowledge-graph-of-the-ai-by-the-ai-for-the-ai-series-part-2-53bm"&gt;cortex-product-graph&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Auto PR review&lt;/td&gt;
&lt;td&gt;webhook -&amp;gt; AI review -&amp;gt; auto-fix -&amp;gt; squash merge&lt;/td&gt;
&lt;td&gt;This article ← you are here&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Self-Healing + observability + auto-added guardrails&lt;/td&gt;
&lt;td&gt;Alert -&amp;gt; AI investigates -&amp;gt; fix PR + new lint/type gate -&amp;gt; auto redeploy + same-pattern writes get auto-rejected&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ryantsuji/fixed-before-anyone-notices-stronger-after-every-fix-self-healing-recurrence-prevention-series-1e86"&gt;cortex-self-healing&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Democratizing the maintenance phase&lt;/td&gt;
&lt;td&gt;Domain experts open PRs to production; the harness owns the quality gate&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ryantsuji/the-author-doesnt-have-to-be-an-engineer-how-the-harness-holds-quality-series-part-5-12e4"&gt;cortex-non-engineer-prs&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Series Final&lt;/td&gt;
&lt;td&gt;The underlying philosophy plus a retrospective on the failures and lessons&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ryantsuji/ai-isnt-something-to-trust-its-something-to-design-series-final-30aa"&gt;cortex-philosophy&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Start with last month's numbers
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;769 PRs merged.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Median time to merge: 31 minutes.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Human review involvement per PR: near-zero.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's a typical 30 days on cortex (Apr 21 -- May 21).&lt;/p&gt;

&lt;p&gt;Every one of those 769 PRs had an AI reviewer as the first reviewer, with &lt;strong&gt;an average of 10.8 review-fix loop iterations per PR (max 56)&lt;/strong&gt;. 1 in 5 merged within 10 minutes, roughly half within 30 minutes. What humans do now is look at review outcomes and &lt;strong&gt;tune the review prompt and the guidelines themselves&lt;/strong&gt; -- this is &lt;strong&gt;human-on-the-loop, not human-in-the-loop&lt;/strong&gt;. &lt;strong&gt;Humans operate on the policy layer, not the execution layer.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Past 30 days&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PRs merged&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;769&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI reviewer coverage&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;100%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Avg review iterations / PR&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;10.8&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Max review iterations&lt;/td&gt;
&lt;td&gt;56&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per-PR human review&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~0%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Median time-to-merge&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;31 min&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Merged within 10 min&lt;/td&gt;
&lt;td&gt;20%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Merged within 30 min&lt;/td&gt;
&lt;td&gt;49%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is a typical month on cortex now.&lt;/p&gt;

&lt;p&gt;The common refrain -- "&lt;strong&gt;AI speeds up writing but reviews still bottleneck&lt;/strong&gt;" and "&lt;strong&gt;AI-written code lowers quality&lt;/strong&gt;" -- is something cortex absorbs through &lt;strong&gt;a pipeline where neither failure mode can take hold&lt;/strong&gt;. Let me break it down.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the review bottleneck stops forming
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The conventional wisdom: the reviewer becomes the bottleneck
&lt;/h3&gt;

&lt;p&gt;As AI writes faster, the load on whoever reviews the output grows proportionally. Anthropic's internal blog (&lt;a href="https://www.anthropic.com/news/how-anthropic-teams-use-claude-code" rel="noopener noreferrer"&gt;How Anthropic teams use Claude Code&lt;/a&gt;) reports the same pattern -- &lt;strong&gt;the bottleneck has shifted from writing to reviewing&lt;/strong&gt;, and senior engineers' work has moved from writing code toward integrating and reviewing AI output.&lt;/p&gt;

&lt;p&gt;cortex hit exactly this. The moment we ran Claude Code at full throttle, &lt;strong&gt;writing speed jumped by an order of magnitude or more&lt;/strong&gt;. Meanwhile the human time available to read and approve PRs only grew linearly. If the reviewer (=me) took a day off, the whole org stalled -- a classic single point of failure.&lt;/p&gt;

&lt;h3&gt;
  
  
  cortex's answer: move the reviewer role to AI as well
&lt;/h3&gt;

&lt;p&gt;Part 1 and Part 2 kept asking the same recurring question: "&lt;strong&gt;how far do you push the harness?&lt;/strong&gt;" cortex went all-in: &lt;strong&gt;the AI writes the code, the AI reviews the code&lt;/strong&gt;. What humans keep their hands on is "&lt;strong&gt;tuning the prompts and guidelines themselves&lt;/strong&gt;" -- not making decisions inside each individual PR, but watching the system from above and adjusting.&lt;/p&gt;

&lt;p&gt;Three conditions had to hold for this to work:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;The AI reviewer has enough context&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A generic AI reviewer &lt;strong&gt;only sees the PR diff&lt;/strong&gt;. The diff alone hides business meaning, upstream/downstream dependencies, and prior incident history. cortex feeds the &lt;strong&gt;Product Graph (cpg)&lt;/strong&gt; from &lt;a href="https://dev.to/ryantsuji/the-heart-of-the-ai-harness-a-knowledge-graph-of-the-ai-by-the-ai-for-the-ai-series-part-2-53bm"&gt;Part 2&lt;/a&gt; -- &lt;strong&gt;a knowledge graph that fuses code, docs, DB schemas, and infra into one structure, with each node carrying business role and upstream/downstream dependencies&lt;/strong&gt; -- into the AI reviewer, so it can &lt;strong&gt;trace impact into code that the PR didn't even touch&lt;/strong&gt;. It catches:&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- Missed upstream/downstream fixes
- Missed doc updates
- Tests that should have been updated but weren't

Diff-only AI review can never reach this territory.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Reviews are not improvisational&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If reviews shift day to day, the team gets confused, and the AI can't be told what "correct" looks like. We enforce this by passing &lt;strong&gt;an explicit review-guideline document&lt;/strong&gt; as the mandatory citation source for every review (we open-sourced a snapshot, see below).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;False positives don't blanket-block merges&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Treating every false positive as Critical breaks the workflow. We control this with &lt;strong&gt;a severity hierarchy (Critical / Major / Minor / Nit) plus strict no-downgrade rules&lt;/strong&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So: the cpg from &lt;a href="https://dev.to/ryantsuji/the-heart-of-the-ai-harness-a-knowledge-graph-of-the-ai-by-the-ai-for-the-ai-series-part-2-53bm"&gt;Part 2&lt;/a&gt; solves "&lt;strong&gt;what context the AI sees&lt;/strong&gt;," the review guidelines solve "&lt;strong&gt;what the AI should do&lt;/strong&gt;" as &lt;strong&gt;Guides (pre-execution control)&lt;/strong&gt;, and the severity ladder + no-downgrade rules solve "&lt;strong&gt;what the AI must not do&lt;/strong&gt;" as &lt;strong&gt;Sensors (post-execution control)&lt;/strong&gt;. This maps cleanly onto Martin Fowler's Guides / Sensors taxonomy (introduced back in &lt;a href="https://dev.to/ryantsuji/building-a-real-ai-harness-auto-reviewed-prs-self-healing-ops-and-non-engineer-contributors-3lfa"&gt;Part 1&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;One more upstream layer: before any of those three kicks in, &lt;strong&gt;a 500-lines-per-file lint&lt;/strong&gt; keeps every file in any PR small enough to fit in a single AI session. That alone keeps AI review from breaking down, and unlike a human reviewer, the AI doesn't lose focus. There are plenty of other lints in front of the AI reviewer too, but the full picture belongs to &lt;strong&gt;Part 4 (Self-Healing + observability + auto-added guardrails)&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the auto-review system is wired
&lt;/h2&gt;

&lt;p&gt;The implementation is &lt;strong&gt;a script running on each developer's machine&lt;/strong&gt;. GitHub webhooks land on an in-house &lt;strong&gt;Event Relay server&lt;/strong&gt;, get persisted to Firestore, and each developer's machine subscribes as an SSE client. On reconnect, Last-Event-ID replays anything missed -- zero event loss, single webhook registration. &lt;strong&gt;Reviewer-mode machines stay always-on&lt;/strong&gt;, so any incoming review fires immediately. &lt;strong&gt;Author mode runs in the background on the PR author's own machine&lt;/strong&gt;, alongside their normal dev work.&lt;/p&gt;

&lt;h3&gt;
  
  
  How we ended up with Event Relay
&lt;/h3&gt;

&lt;p&gt;The current setup wasn't the original design.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;First&lt;/strong&gt;: GitHub webhook → &lt;a href="https://smee.io/" rel="noopener noreferrer"&gt;smee.io&lt;/a&gt; → each machine&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Then&lt;/strong&gt;: GitHub webhook → Cloudflare Tunnel → each machine&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Now&lt;/strong&gt;: GitHub webhook → in-house &lt;strong&gt;Event Relay&lt;/strong&gt; with Firestore persistence → SSE to each machine&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both smee.io and Cloudflare Tunnel ran into &lt;strong&gt;connection drops and missed deliveries&lt;/strong&gt;, which caused real misses for us. Switching to the in-house Event Relay brought event loss to zero (&lt;strong&gt;Firestore persistence + Last-Event-ID replay&lt;/strong&gt;), and the relay turned into a general-purpose layer we could reuse.&lt;/p&gt;

&lt;p&gt;The webhook ingestion for &lt;strong&gt;Self-Healing&lt;/strong&gt; (covered in Part 4) actually goes through &lt;strong&gt;the exact same Event Relay&lt;/strong&gt;. GitHub, Grafana, and other webhook sources get consolidated through one relay, and each machine's SSE client subscribes to whichever events it cares about. &lt;strong&gt;Having a single general-purpose webhook relay is a piece of infra that keeps paying off in unexpected ways&lt;/strong&gt; -- worth investing in early.&lt;/p&gt;

&lt;p&gt;When the reviewer's machine receives an event, the script spawns &lt;code&gt;claude -p&lt;/code&gt; and walks through 9 dimensions (Graph / Architecture / Security / Test / Doc / Impact / Observability / AI-Antipattern / Recurrence) sequentially, then reads the verdict marker the AI emitted at the end and posts &lt;code&gt;APPROVE&lt;/code&gt; or &lt;code&gt;REQUEST_CHANGES&lt;/code&gt; via &lt;code&gt;gh pr review&lt;/code&gt;.&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%2F79easc3e030ab4tyrcdf.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%2F79easc3e030ab4tyrcdf.png" alt="Auto review pipeline — distributed webhook architecture running on every developer's machine"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A few notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Modes split the role&lt;/strong&gt; -- the same script started with &lt;code&gt;--mode reviewer&lt;/code&gt; becomes the reviewer process; with &lt;code&gt;--mode author&lt;/code&gt; it becomes the PR-author response process. The machine of whoever is assigned as reviewer runs reviewer mode; the machine of whoever opened the PR runs author mode. Event Relay multicasts the events, and &lt;strong&gt;each machine reacts in a distributed way&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per-PR worktree isolation&lt;/strong&gt; -- author mode merges &lt;code&gt;origin/main&lt;/code&gt; into a fresh worktree before spawning the AI. Multiple PRs can be handled in parallel without file state contaminating across them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;9 dimensions checked sequentially in one session&lt;/strong&gt; -- not parallel sub-agents. A single &lt;code&gt;claude -p&lt;/code&gt; session walks the 9 dimensions while keeping context shared, which also catches cross-dimension contradictions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review guidelines: public snapshot&lt;/strong&gt; -- &lt;a href="https://github.com/air-closet/cortex-review-guidelines" rel="noopener noreferrer"&gt;air-closet/cortex-review-guidelines&lt;/a&gt; (JP/EN). The live guidelines are inside cortex (private repo) and evolve daily; the public repo is a snapshot extracted for reference.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;:::message alert&lt;br&gt;
&lt;strong&gt;Guidelines alone scale only to projects in the tens-of-thousands-of-lines range.&lt;/strong&gt; At cortex's scale (&lt;strong&gt;over 1M lines of code&lt;/strong&gt;), the &lt;strong&gt;knowledge graph from &lt;a href="https://dev.to/ryantsuji/the-heart-of-the-ai-harness-a-knowledge-graph-of-the-ai-by-the-ai-for-the-ai-series-part-2-53bm"&gt;Part 2&lt;/a&gt; (cpg) is a hard prerequisite&lt;/strong&gt;. Porting the guidelines without cpg won't reproduce the same review quality -- the AI reviewer simply can't navigate the codebase fast enough to reason about impact.&lt;br&gt;
:::&lt;/p&gt;
&lt;h3&gt;
  
  
  Why sequential single-session review, not parallel sub-agents
&lt;/h3&gt;

&lt;p&gt;We initially tried splitting the 9 dimensions across parallel sub-agents. Three problems emerged: cpg / guidelines / PR diff got injected 9 times (token cost balloons), cross-dimension findings couldn't reference each other (a &lt;code&gt;[Test]&lt;/code&gt; issue rooted in a &lt;code&gt;[Graph]&lt;/code&gt; violation gets dropped in isolation), and aggregating 9 outputs into a single verdict required its own machinery.&lt;/p&gt;

&lt;p&gt;A single sequential session fixes all three: one cpg/guideline load, earlier findings stay in context for later dimensions (cross-dimension consistency comes for free), and one verdict marker at the end is the entire aggregation step.&lt;/p&gt;

&lt;p&gt;We also &lt;strong&gt;swap &lt;code&gt;CLAUDE.md&lt;/code&gt; to a review-specific version&lt;/strong&gt; at startup. The default &lt;code&gt;CLAUDE.md&lt;/code&gt; is dense with development-time context (Product Graph ops, prod-data safety, MCP ordering) -- noise for a reviewer. The review-specific version centers on severity, no-downgrade, and the verdict marker spec, keeping AI attention on the review task.&lt;/p&gt;

&lt;p&gt;Cutting wasted context lifts judgment precision and token cost at the same time.&lt;/p&gt;
&lt;h3&gt;
  
  
  Operational knobs
&lt;/h3&gt;

&lt;p&gt;A few filters and toggles we apply in actual use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Draft (WIP) PRs are excluded.&lt;/strong&gt; GitHub Draft state is received but skipped; review starts firing once the author flips it to Ready for Review.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Specific PRs can be targeted manually.&lt;/strong&gt; The webhook is the normal trigger, but you can also kick off a review against a specific PR number from the CLI -- useful after a CI failure or for re-checking a single PR.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto-merge is the PR author's call.&lt;/strong&gt; Whether the pipeline runs through to auto-merge after APPROVE + CI green is set by the PR author. Default is on; for changes that go directly to prod, the author can flip it off and hit merge themselves.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Output structure: tags and severity
&lt;/h2&gt;

&lt;p&gt;Every auto-review comment is structured as &lt;strong&gt;tag + severity + concrete example&lt;/strong&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  Tags (dimensions)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tag&lt;/th&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;Primary target&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[Graph]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Product Graph integrity&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@graph-*&lt;/code&gt; JSDoc, node dependencies, doc consistency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[Doc]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Doc consistency&lt;/td&gt;
&lt;td&gt;Doc updates that should follow code changes, doc placement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[Impact]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Impact analysis&lt;/td&gt;
&lt;td&gt;Missed upstream/downstream fixes, &lt;code&gt;via:&lt;/code&gt; field inconsistency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[Security]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Security&lt;/td&gt;
&lt;td&gt;Auth, input validation, secrets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[Architecture]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Composable Architecture&lt;/td&gt;
&lt;td&gt;app/package boundaries, dependency direction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[Test]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Test quality&lt;/td&gt;
&lt;td&gt;Coverage, matchers, naming&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[Observability]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Observability&lt;/td&gt;
&lt;td&gt;Structured logging, no-truncate rules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[AI-Antipattern]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;AI-generated code traps&lt;/td&gt;
&lt;td&gt;Hallucinated APIs, fallback overuse, dead code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[Recurrence]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Recurrence prevention&lt;/td&gt;
&lt;td&gt;Bug-fix triage (lint / horizontal rollout / new guideline)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h3&gt;
  
  
  Severity
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Severity&lt;/th&gt;
&lt;th&gt;Criteria&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Critical&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Security, data corruption, prod-risk, doc inconsistency, missing &lt;code&gt;@graph-*&lt;/code&gt;, quality-bar relaxation&lt;/td&gt;
&lt;td&gt;&lt;code&gt;REQUEST_CHANGES&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Major&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Spec violation, Composable Architecture violation, missing tests&lt;/td&gt;
&lt;td&gt;&lt;code&gt;REQUEST_CHANGES&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Minor&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Naming, maintainability, light refactor&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;REQUEST_CHANGES&lt;/code&gt; (must be resolved)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Nit&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Style preference, minor inconsistency&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;APPROVE&lt;/code&gt; (comment only)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The single most important rule is &lt;strong&gt;"no downgrade"&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"&lt;strong&gt;Following existing patterns&lt;/strong&gt;" is not a valid reason to downgrade (existing violations are a separate problem to fix, not justification for new violations).&lt;/li&gt;
&lt;li&gt;"&lt;strong&gt;Will be addressed in a separate PR&lt;/strong&gt;" or "&lt;strong&gt;incrementally&lt;/strong&gt;" is not a valid reason to drop Critical/Major to Nit.&lt;/li&gt;
&lt;li&gt;"&lt;strong&gt;Leave a TODO/FIXME&lt;/strong&gt;" is not a valid deferral path.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is spelled out in &lt;a href="https://github.com/air-closet/cortex-review-guidelines/blob/main/en/guidelines/severity.md" rel="noopener noreferrer"&gt;&lt;code&gt;severity.md&lt;/code&gt;&lt;/a&gt;, and the AI literally cites it back when it issues &lt;code&gt;REQUEST_CHANGES&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  Example: PR migrating the embedding model from gemini-embedding-001 to gemini-embedding-2
&lt;/h3&gt;

&lt;p&gt;A real review comment is more useful than abstract description. Here's a typical one. This is a feature PR (&lt;code&gt;feat(meet): dual-write embeddings to new 'embedding' column (v2)&lt;/code&gt;) that was merged on 2026-05-19, after &lt;strong&gt;6 review-fix iterations across 1.5 hours&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First review (07:35:25)&lt;/strong&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The dual-write approach, the idempotent migration script, and the &lt;code&gt;Promise.all&lt;/code&gt; parallelization all look sound. Please address the 3 Critical and 2 Minor findings below before merging.&lt;/p&gt;


&lt;h2&gt;
  
  
  Critical
&lt;/h2&gt;
&lt;h3&gt;
  
  
  [Graph] Missing &lt;code&gt;@graph-business&lt;/code&gt; tag (x3)
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;graph-integrity.md&lt;/code&gt; requires &lt;code&gt;@graph-business&lt;/code&gt; on every top-level declaration under the app layer (severity: Critical).&lt;br&gt;
All 3 new declarations below are missing it (detailed comments in separate threads).&lt;br&gt;
&lt;strong&gt;Existing code is also missing it, but "following existing patterns" is not a valid downgrade reason (severity.md).&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;generateEmbeddingV2&lt;/code&gt; (v2 embedding generation function)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;EMBEDDING_MODEL_V2&lt;/code&gt; (model name config)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;EMBEDDING_LOCATION_V2&lt;/code&gt; (region config)&lt;/li&gt;
&lt;/ul&gt;


&lt;h3&gt;
  
  
  [Graph] &lt;code&gt;embedMeetContent&lt;/code&gt;'s &lt;code&gt;@graph-connects&lt;/code&gt; doesn't reflect &lt;code&gt;generateEmbeddingV2&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The JSDoc on &lt;code&gt;embedMeetContent&lt;/code&gt; has &lt;code&gt;@graph-connects generateEmbedding [calls] Generate embedding&lt;/code&gt;, but no corresponding &lt;code&gt;@graph-connects&lt;/code&gt; line has been added for the newly introduced &lt;code&gt;generateEmbeddingV2&lt;/code&gt; call.&lt;br&gt;
The graph will be missing an edge to &lt;code&gt;generateEmbeddingV2&lt;/code&gt;.&lt;/p&gt;


&lt;pre class="highlight diff"&gt;&lt;code&gt;   * @graph-connects generateEmbedding [calls] Generate embedding
&lt;span class="gi"&gt;+  * @graph-connects generateEmbeddingV2 [calls] v2 embedding generation (dual-write)
&lt;/span&gt;   * @graph-connects insertMeetChunks [calls] Insert chunks into BQ
&lt;/code&gt;&lt;/pre&gt;



&lt;h3&gt;
  
  
  [Doc] Corresponding BigQuery schema doc is not updated
&lt;/h3&gt;

&lt;p&gt;The "BigQuery schema" section in the related doc is missing the new &lt;code&gt;embedding&lt;/code&gt; column.&lt;br&gt;
Both &lt;code&gt;graph-integrity.md&lt;/code&gt; and &lt;code&gt;severity.md&lt;/code&gt; define doc inconsistency as Critical.&lt;/p&gt;


&lt;pre class="highlight diff"&gt;&lt;code&gt; | `created_at`  | TIMESTAMP   | Created at                              |
&lt;span class="gi"&gt;+| `embedding`   | FLOAT64[]   | Embedding vector (v2: gemini-embedding-2) |
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;



&lt;h2&gt;
  
  
  Minor
&lt;/h2&gt;
&lt;h3&gt;
  
  
  [Test] &lt;code&gt;textEmbeddingV2&lt;/code&gt; value is not asserted
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;objectContaining&lt;/code&gt; allows extra fields, so the test still passes even when the v2 value is never set.&lt;/p&gt;


&lt;pre class="highlight diff"&gt;&lt;code&gt;         textEmbedding: [0.1, 0.2, 0.3],
&lt;span class="gi"&gt;+        textEmbeddingV2: [0.1, 0.2, 0.3],
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;h3&gt;
  
  
  [Test] No isolated scenario for "v2 returns null"
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;generateEmbeddingV2: mockGenerateEmbedding&lt;/code&gt; reuses the v1 mock, so the case "v2 returns null while v1 succeeds" is not independently verified.&lt;/p&gt;



&lt;p&gt;&lt;code&gt;&amp;lt;!-- VERDICT:REQUEST_CHANGES --&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The takeaway is the precision of the details.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;File + line numbers&lt;/strong&gt; are concrete.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Suggested fixes are in diff format&lt;/strong&gt; (copy-paste ready).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source guideline&lt;/strong&gt; (&lt;code&gt;graph-integrity.md&lt;/code&gt; / &lt;code&gt;severity.md&lt;/code&gt;) is cited explicitly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The typical excuse&lt;/strong&gt; ("existing code has the same problem") is &lt;strong&gt;pre-emptively closed&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;The trailing &lt;code&gt;&amp;lt;!-- VERDICT:REQUEST_CHANGES --&amp;gt;&lt;/code&gt; is a &lt;strong&gt;machine-readable verdict marker&lt;/strong&gt; -- the trigger that moves the PR into &lt;code&gt;REQUEST_CHANGES&lt;/code&gt; state.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After this, the PR author (= usually another AI running on the author's machine) pushes a fix, the reviewer re-reviews. The next review confirms all 3 Criticals are actually resolved, raises the next Major / Critical, and so on. &lt;strong&gt;6 iterations in 1.5 hours&lt;/strong&gt;, finally APPROVE, auto-merge.&lt;/p&gt;

&lt;p&gt;Plotted on a timeline:&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%2Flgqoasedyujmet592wk2.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%2Flgqoasedyujmet592wk2.png" alt="Real example of the review-fix loop — embedding model migration PR, 6 iterations in 1.5 hours"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With a human reviewer, this is "Critical x3 -&amp;gt; wait until tomorrow for the fix -&amp;gt; re-review the day after" -- 2 to 3 days per PR. cortex closes it in &lt;strong&gt;90 minutes&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The difference between human review and auto review is not just speed. A single AI session walks all 9 dimensions in order and cites the guideline each time, which makes it &lt;strong&gt;much harder to miss the "deep" findings humans drop because their attention drifted&lt;/strong&gt; -- doc consistency, recurrence-prevention judgments, weak matchers. Side-by-side comparison:&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%2Fyr8gzw2bernzvgz01iwn.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%2Fyr8gzw2bernzvgz01iwn.png" alt="Before / After — human review era vs. cortex's auto-review era"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is why the review bottleneck never forms here.&lt;/p&gt;
&lt;h2&gt;
  
  
  Evolving the guidelines: catching the moments AI gets it wrong, then fixing the rules
&lt;/h2&gt;

&lt;p&gt;The review guidelines I've been referring to are &lt;strong&gt;not a static document&lt;/strong&gt;. Running this in production surfaces recurring patterns where &lt;strong&gt;the AI mis-judges a specific class of issue&lt;/strong&gt;. Each time that happens, we don't add a comment to the individual PR; we &lt;strong&gt;rewrite the guideline so the AI behaves correctly next time&lt;/strong&gt; -- this is the meta-layer humans actually operate on.&lt;/p&gt;

&lt;p&gt;A few concrete failures we hit on cortex, and how we closed each one by changing the rule, not the PR.&lt;/p&gt;
&lt;h3&gt;
  
  
  1. AI was downgrading because "existing code has the same issue"
&lt;/h3&gt;

&lt;p&gt;Early on, immediately after flagging a violation the AI would add "&lt;strong&gt;however, since existing code has the same violation, I'm downgrading this to Nit&lt;/strong&gt;" and self-downgrade. The result: violations on newly added code kept dropping to Nit, and the system kept emitting Approve.&lt;/p&gt;

&lt;p&gt;We closed this by adding &lt;strong&gt;the no-downgrade rule&lt;/strong&gt; to &lt;code&gt;severity.md&lt;/code&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Following existing patterns" is not a valid downgrade reason: if existing code violates a guideline, new code following that pattern still gets flagged at the same severity. Deferral language like "consider during the next refactor" is not accepted.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That wasn't enough on its own. Over time other excuse patterns surfaced -- "&lt;strong&gt;will be addressed in a separate PR&lt;/strong&gt;," "&lt;strong&gt;will be addressed in the next session&lt;/strong&gt;," "&lt;strong&gt;out of scope&lt;/strong&gt;," "&lt;strong&gt;incrementally&lt;/strong&gt;" -- so we added those as forbidden downgrade categories too. We also explicitly forbade &lt;strong&gt;deferring via TODO/FIXME comments in code&lt;/strong&gt;. The mindset is: &lt;strong&gt;close every typical excuse path preemptively&lt;/strong&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  2. The final verdict had 3 options, and "comment-only" left PRs in limbo
&lt;/h3&gt;

&lt;p&gt;The final verdict at the end of every review was originally &lt;code&gt;APPROVE&lt;/code&gt; / &lt;code&gt;REQUEST_CHANGES&lt;/code&gt; / &lt;code&gt;COMMENT&lt;/code&gt; (approve / request changes / comment-only). When the AI picked &lt;code&gt;COMMENT&lt;/code&gt; -- for example when only Minor issues existed -- the script took no action, the PR sat in review-pending forever, and ultimately someone had to manually pick it up. Classic anti-pattern, and it kept happening.&lt;/p&gt;

&lt;p&gt;We &lt;strong&gt;collapsed the verdict to 2 options&lt;/strong&gt;. Anything Minor or above is &lt;code&gt;REQUEST_CHANGES&lt;/code&gt;, a missing verdict marker defaults to &lt;code&gt;REQUEST_CHANGES&lt;/code&gt; (safe side), and only Nit-only or no findings (with CI passing) yields &lt;code&gt;APPROVE&lt;/code&gt;. The principle: "&lt;strong&gt;if the judgment is ambiguous, fail-safe by defaulting to the blocking side (&lt;code&gt;REQUEST_CHANGES&lt;/code&gt;)&lt;/strong&gt;." Going all-in on that design eliminated the stuck-PR class entirely.&lt;/p&gt;
&lt;h3&gt;
  
  
  3. Checklist items had no severity, so the AI's judgment kept drifting
&lt;/h3&gt;

&lt;p&gt;Originally, each guideline (&lt;code&gt;graph-integrity.md&lt;/code&gt;, &lt;code&gt;testing.md&lt;/code&gt;, etc.) was just a &lt;strong&gt;bulleted checklist&lt;/strong&gt;. Items like "Is the test name descriptive?" or "Are mocks minimized?" were listed, but &lt;strong&gt;without per-item severity&lt;/strong&gt;. As a result, the same violation could land as Major in one PR and Nit in another, depending on the session.&lt;/p&gt;

&lt;p&gt;We &lt;strong&gt;converted every guideline's checklist into a &lt;code&gt;severity&lt;/code&gt; / &lt;code&gt;scope&lt;/code&gt; / &lt;code&gt;criterion&lt;/code&gt; table&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Severity&lt;/th&gt;
&lt;th&gt;Scope&lt;/th&gt;
&lt;th&gt;Criterion&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Critical&lt;/td&gt;
&lt;td&gt;All PRs&lt;/td&gt;
&lt;td&gt;Missing &lt;code&gt;@graph-business&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Major&lt;/td&gt;
&lt;td&gt;App layer only&lt;/td&gt;
&lt;td&gt;Missing tests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Minor&lt;/td&gt;
&lt;td&gt;Shared packages only&lt;/td&gt;
&lt;td&gt;More than 3 function args&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nit&lt;/td&gt;
&lt;td&gt;All PRs&lt;/td&gt;
&lt;td&gt;Naming inconsistency&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;scope&lt;/code&gt; column is &lt;strong&gt;a machine-decidable filter&lt;/strong&gt; for which paths a check applies to, so the AI reviewer doesn't trigger irrelevant items on PRs outside that scope. Just putting it in a table -- the judgment reproducibility jumped significantly.&lt;/p&gt;
&lt;h3&gt;
  
  
  4. The existing guidelines didn't catch AI-specific traps
&lt;/h3&gt;

&lt;p&gt;After running this for a while we noticed AI-generated code has its own cluster of antipatterns -- &lt;strong&gt;calling APIs that don't exist&lt;/strong&gt; (hallucinated APIs -- something like &lt;code&gt;user.findOrCreate()&lt;/code&gt; that looks plausible but isn't actually defined), &lt;strong&gt;swallowing errors and returning fallback values&lt;/strong&gt; (e.g., silently returning an empty array when an upstream API fails), &lt;strong&gt;leaving unused functions&lt;/strong&gt; (a refactor adds the new function but doesn't delete the old one, leaving dead code), &lt;strong&gt;expanding the modification scope beyond what was asked&lt;/strong&gt; (you ask it to change one function and it reformats the whole file), &lt;strong&gt;adding unnecessary backward-compatibility code&lt;/strong&gt; (creating a deprecated alias for an internal-only function) -- and &lt;code&gt;security.md&lt;/code&gt; / &lt;code&gt;testing.md&lt;/code&gt; couldn't catch these. There's a &lt;strong&gt;distinct class of "mistakes only AIs make."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We added a dedicated &lt;strong&gt;&lt;code&gt;ai-antipattern.md&lt;/code&gt;&lt;/strong&gt; for this. Reviews now pick these up explicitly under the &lt;code&gt;[AI-Antipattern]&lt;/code&gt; tag. &lt;strong&gt;Reviewing AI output requires designing around AI-specific traps&lt;/strong&gt; -- you don't get there just by porting human review heuristics onto an AI.&lt;/p&gt;
&lt;h3&gt;
  
  
  5. The AI tries to relax "the standard itself"
&lt;/h3&gt;

&lt;p&gt;The last and most important pattern. When the AI was writing fix PRs, occasionally instead of fixing the guideline violation it would write &lt;strong&gt;a PR that relaxes the guideline&lt;/strong&gt;. For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lower the test coverage threshold to avoid writing more tests&lt;/li&gt;
&lt;li&gt;Narrow the in-house lint rule's scope to make the violation go away&lt;/li&gt;
&lt;li&gt;Soften the guideline doc language from "recommended" to "preferred" to weaken the binding constraint&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the AI builds a formally-coherent justification: "&lt;strong&gt;existing code already violates this, so let's adjust the standard to match the implementation.&lt;/strong&gt;" Left unchecked, &lt;strong&gt;the AI gradually walks the quality bar down&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;We closed this by adding &lt;strong&gt;"quality-bar relaxation" as a Critical&lt;/strong&gt; in &lt;code&gt;severity.md&lt;/code&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A PR that relaxes the quality bar -- guideline doc, lint rule, coverage threshold -- must not be Approved by the AI reviewer. It is sent back with &lt;code&gt;REQUEST_CHANGES&lt;/code&gt;. &lt;strong&gt;A human reviewer's approval is required&lt;/strong&gt;. "Existing code already violates this" is not a valid justification for relaxation.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is the one explicit boundary where &lt;strong&gt;we deliberately do not give the AI autonomous Approve authority&lt;/strong&gt;. Whether the standard itself moves is a human decision. It's the &lt;strong&gt;meta-level safety valve&lt;/strong&gt; for the "AI reviewing AI" architecture.&lt;/p&gt;
&lt;h3&gt;
  
  
  Evolving the guidelines is the meta-layer humans actually operate on
&lt;/h3&gt;

&lt;p&gt;The common thread: "&lt;strong&gt;when the AI gets it wrong, don't override the individual PR -- rewrite the guideline so the fix propagates forward.&lt;/strong&gt;"&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AI escapes via "existing code has the same issue" -&amp;gt; add no-downgrade rule&lt;/li&gt;
&lt;li&gt;AI picks "comment-only" and PR stalls -&amp;gt; collapse to 2-option verdict&lt;/li&gt;
&lt;li&gt;AI's judgment drifts -&amp;gt; add severity / scope columns to every item&lt;/li&gt;
&lt;li&gt;AI falls into its own traps -&amp;gt; add the AI-Antipattern category&lt;/li&gt;
&lt;li&gt;AI tries to relax the standard -&amp;gt; classify standard-relaxation as Critical, require human Approve&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As long as this loop turns, the guideline is &lt;strong&gt;a living document that absorbs the failure patterns AI produces in production&lt;/strong&gt;. &lt;strong&gt;Don't try to write the perfect guideline up front. Catch the moment AI gets it wrong, and write the rule for that moment.&lt;/strong&gt; That's the actual mechanism behind "quality doesn't drop even when humans aren't inside the loop."&lt;/p&gt;

&lt;p&gt;And one more thread. Right now, the trigger for "AI got it wrong, time to rewrite the guideline" is still mostly a human judgment, but &lt;strong&gt;parts of that maintenance are gradually becoming automatable too&lt;/strong&gt;. &lt;strong&gt;Self-Healing&lt;/strong&gt; (Part 4 next time) -- where AI investigates production incidents, opens a fix PR, runs it through auto-review, and auto-redeploys -- requires every fix PR to write one of {add lint, add guideline, horizontal rollout} under the &lt;code&gt;[Recurrence]&lt;/code&gt; lens. So the &lt;strong&gt;AI is increasingly participating in the maintenance of its own review criteria&lt;/strong&gt;, with humans still in the loop on adoption. I'll come back to this in Part 4.&lt;/p&gt;
&lt;h2&gt;
  
  
  Auto-fix: a separate AI applies the changes and pushes
&lt;/h2&gt;

&lt;p&gt;Once &lt;code&gt;REQUEST_CHANGES&lt;/code&gt; lands, &lt;strong&gt;the same script running on the PR author's machine, but in author mode&lt;/strong&gt;, picks up the event and starts working.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[REQUEST_CHANGES detected]
   | SSE push via Event Relay
[Author mode boots on PR author's machine]
   | Merge origin/main into a worktree
   |  (lockfile resolved up front, remaining conflicts handled by AI)
   | Read the auto-review comment as context
   | Run claude -p inside the worktree
   | Commit + push the changes
   | New SHA is delivered back to the reviewer's machine via Event Relay -&amp;gt; re-review
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two design choices matter here.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reviewer and author run on different machines in different sessions&lt;/strong&gt; -- reviewer mode and author mode are the same script, but they run on different machines in different processes. "Is the original critique correct?" is judged independently. Unlike a single AI fixing its own complaints, the judgment passes between two separate sessions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;All iteration stays inside the same PR&lt;/strong&gt; -- we don't spawn a new PR. The "&lt;strong&gt;fix the root cause, no deferrals&lt;/strong&gt;" rule from Part 2 and the review guidelines kicks in here: if the AI tries to escape via &lt;code&gt;TODO/FIXME&lt;/code&gt; or by splitting work out into a separate PR, the next review rejects it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Auto-merge + parallel deploy
&lt;/h2&gt;

&lt;p&gt;Once auto-review returns APPROVE and CI is fully green, the &lt;code&gt;auto-merge&lt;/code&gt; script runs and squash-merges the PR.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Auto review APPROVE + CI green]
   |
auto-merge script
   | squash merge to main
   |
[main updated]
   |
Turborepo build (affected packages only)
   |
Pulumi up (multiple stacks in parallel)
   |- API services
   |- pipeline services
   |- MCP servers
   `- infra
   |
[Deploy complete]
   |
cpg index rebuilt (only changed nodes regenerate embeddings -- see Part 2)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;pulumi up &amp;lt;stack1&amp;gt; &amp;lt;stack2&amp;gt; ...&lt;/code&gt; runs in parallel, so deploying 9 stacks at once finishes in about 8-12 minutes. End to end, merge-to-production is averaging 10-15 minutes.&lt;/p&gt;

&lt;p&gt;This compounds nicely with Self-Healing PRs. &lt;strong&gt;Incident alert -&amp;gt; Self-Healing identifies root cause -&amp;gt; opens a fix PR -&amp;gt; auto review pass -&amp;gt; auto merge -&amp;gt; auto deploy&lt;/strong&gt; runs as a single closed loop without human involvement (covered in Part 4).&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers, in more detail
&lt;/h2&gt;

&lt;p&gt;Unpacking the headline numbers a bit further.&lt;/p&gt;

&lt;h3&gt;
  
  
  Depth of the review-fix loop
&lt;/h3&gt;

&lt;p&gt;Across 769 PRs in 30 days, the &lt;strong&gt;average per PR was 10.8 review iterations, max 56&lt;/strong&gt;. The fact that the average is past 10 means &lt;strong&gt;the first review almost always surfaces at least one finding&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The embedding-model migration PR shown earlier needed 6 iterations to merge, and that's representative of the average PR. &lt;strong&gt;What would take a human reviewer days, cortex resolves in minutes.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What the auto reviewer typically flags
&lt;/h3&gt;

&lt;p&gt;The most common findings out of the first review:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;[Graph] Missing &lt;code&gt;@graph-business&lt;/code&gt;&lt;/strong&gt; -- a prerequisite cpg leans on (from Part 2). The classic finding on newly added declarations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;[Doc] Doc inconsistency&lt;/strong&gt; -- code changed but the corresponding &lt;code&gt;docs/&lt;/code&gt; section was not updated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;[Test] Weak matchers&lt;/strong&gt; -- &lt;code&gt;objectContaining&lt;/code&gt; weakening value assertions, single-property checks via &lt;code&gt;toBe&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;[Observability] Unstructured error logs&lt;/strong&gt; -- &lt;code&gt;event&lt;/code&gt; field or required keys deviating from the structured-log spec.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;[Recurrence] No recurrence-prevention action&lt;/strong&gt; -- a bug-fix PR description not declaring which of {lint / horizontal rollout / add guideline / nothing} applies.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are categories &lt;strong&gt;human reviewers frequently miss in practice&lt;/strong&gt;, especially doc consistency and recurrence-prevention checks. The AI reviewer applies them mechanically on every PR.&lt;/p&gt;

&lt;h3&gt;
  
  
  Actual false-positive rate
&lt;/h3&gt;

&lt;p&gt;It's not zero. A few times a month we get "this is Nit, not Major" type misjudgments. The fix path is the one described above -- not a comment on the individual PR, but a guideline edit that corrects the judgment for all subsequent reviews.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed / Bridge to Part 4
&lt;/h2&gt;

&lt;p&gt;Over the past six months, the engineer's role on cortex shifted from "&lt;strong&gt;writer&lt;/strong&gt;" and "&lt;strong&gt;reviewer&lt;/strong&gt;" to "&lt;strong&gt;operator&lt;/strong&gt;" -- the human running the system, not acting inside each individual decision.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AI writes the code (Claude Code)&lt;/li&gt;
&lt;li&gt;AI reviews the code (auto review)&lt;/li&gt;
&lt;li&gt;A different AI applies the fixes (author mode running on the PR author's machine)&lt;/li&gt;
&lt;li&gt;AI decides when to merge (auto-merge script)&lt;/li&gt;
&lt;li&gt;Deploys go in parallel (Turborepo + Pulumi)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What stays in human hands: "&lt;strong&gt;what to build at all&lt;/strong&gt; (product / requirements)," "&lt;strong&gt;is this direction actually right&lt;/strong&gt; (architectural judgment)," "&lt;strong&gt;which guideline to add and where&lt;/strong&gt;," and "&lt;strong&gt;look at the reviews and adjust prompts and guidelines accordingly&lt;/strong&gt;." High-abstraction work -- &lt;strong&gt;not individual decisions, but watching the whole system from above and steering&lt;/strong&gt;. &lt;strong&gt;From human-in-the-loop to human-on-the-loop&lt;/strong&gt;, you could say.&lt;/p&gt;

&lt;p&gt;The widely-reported phenomena -- "AI lowers quality," "the reviewer becomes the bottleneck" -- happen when &lt;strong&gt;the harness is extended on the writer side only, and the reviewer side is left to humans&lt;/strong&gt;. If writing speeds up and reviewing doesn't, of course it bottlenecks. Of course things get missed.&lt;/p&gt;

&lt;p&gt;cortex is the opposite. &lt;strong&gt;We extended the harness on the reviewer side first, before fully extending it on the writer side&lt;/strong&gt;. Anthropic's observation that the bottleneck shifts from writing to reviewing is exactly right -- which is precisely why "&lt;strong&gt;move the reviewer role to AI as well&lt;/strong&gt;" is the answer cortex chose.&lt;/p&gt;

&lt;p&gt;"The AI writes the code, the AI reviews the code." That's the core of cortex's auto-review pipeline. &lt;strong&gt;Quality drop and review bottleneck are functions of how far you extend the harness&lt;/strong&gt; -- they are not inherent to AI-assisted development.&lt;/p&gt;




&lt;p&gt;Up next in &lt;strong&gt;&lt;a href="https://dev.to/ryantsuji/fixed-before-anyone-notices-stronger-after-every-fix-self-healing-recurrence-prevention-series-1e86?lang=en"&gt;Part 4 — Self-Healing + Recurrence Prevention&lt;/a&gt;&lt;/strong&gt;: a pipeline where a production alert (observed via OTel/Loki/Mimir/Tempo/Faro) triggers AI investigation, an AI-authored fix PR plus a new lint/type gate, auto-review, auto-merge, and auto-redeploy. The fix and a recurrence-prevention guardrail land together, so the same class of incident structurally can't fire again. If auto review protects quality at PR time, Part 4 protects it &lt;strong&gt;at production time, while growing the quality gates themselves&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The headline number above includes Self-Healing PRs (production alerts that AI investigates, fixes, and auto-deploys). For certain classes of incidents, the fix is already merged before anyone has time to react — that's where cortex sits today.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>codereview</category>
      <category>devops</category>
      <category>productivity</category>
    </item>
    <item>
      <title>The Heart of the AI Harness: A Knowledge Graph of the AI, by the AI, for the AI (Series Part 2)</title>
      <dc:creator>Ryosuke Tsuji</dc:creator>
      <pubDate>Tue, 19 May 2026 14:16:20 +0000</pubDate>
      <link>https://dev.to/ryantsuji/the-heart-of-the-ai-harness-a-knowledge-graph-of-the-ai-by-the-ai-for-the-ai-series-part-2-53bm</link>
      <guid>https://dev.to/ryantsuji/the-heart-of-the-ai-harness-a-knowledge-graph-of-the-ai-by-the-ai-for-the-ai-series-part-2-53bm</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;AI assistance disclosure: This article was drafted with the help of Claude. All technical content, design decisions, code references, and screenshots reflect production systems I designed and operate at airCloset; the prose was revised by me prior to publication.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Hi, I'm &lt;a href="https://x.com/ryantsuji" rel="noopener noreferrer"&gt;Ryan&lt;/a&gt;, CTO at airCloset.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer&lt;/strong&gt;: "cortex" and "cortex-product-graph" referenced in this article are internal code names for an AI platform developed in-house at airCloset. They are unrelated to existing commercial services such as Snowflake Cortex or Palo Alto Networks Cortex.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In &lt;a href="https://dev.to/ryantsuji/building-a-real-ai-harness-auto-reviewed-prs-self-healing-ops-and-non-engineer-contributors-3lfa"&gt;Part 1 (Series Intro)&lt;/a&gt;, I wrote about how &lt;strong&gt;AI handles PR reviews and incident response&lt;/strong&gt; on top of a platform we call cortex. At the center of that flywheel is the &lt;strong&gt;Product Graph&lt;/strong&gt; (implementation name: &lt;code&gt;cortex-product-graph&lt;/code&gt;, or cpg) — a unified knowledge graph of code, docs, DB schemas, and infrastructure definitions, queryable through semantic search.&lt;/p&gt;

&lt;p&gt;In Part 1, I described cpg at a high level: "all of cortex is indexed in one graph." This post goes deeper — &lt;strong&gt;how it's built, why we landed on this design, and what actually changed&lt;/strong&gt; once it was in place.&lt;/p&gt;

&lt;h2&gt;
  
  
  Series Index
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Theme&lt;/th&gt;
&lt;th&gt;Key scene&lt;/th&gt;
&lt;th&gt;Article&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Series intro: cortex's harness&lt;/td&gt;
&lt;td&gt;PRs auto-merge / incidents self-heal before you notice&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ryantsuji/building-a-real-ai-harness-auto-reviewed-prs-self-healing-ops-and-non-engineer-contributors-3lfa"&gt;ai-harness-intro&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Product Graph (cpg)&lt;/td&gt;
&lt;td&gt;Code, docs, DB, infra unified into one graph&lt;/td&gt;
&lt;td&gt;this post ← you are here&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;AI PR review&lt;/td&gt;
&lt;td&gt;webhook → AI review → auto-fix → squash merge&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ryantsuji/human-on-the-loop-ai-reviewing-ai-prs-at-cortex-769-prsmonth-while-raising-the-quality-bar-4lh5"&gt;cortex-auto-review&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Self-Healing + observability + auto-added guardrails&lt;/td&gt;
&lt;td&gt;Alert → AI investigates → fix PR + new lint/type gate → auto redeploy + same-pattern writes get auto-rejected&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ryantsuji/fixed-before-anyone-notices-stronger-after-every-fix-self-healing-recurrence-prevention-series-1e86"&gt;cortex-self-healing&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Democratizing the maintenance phase&lt;/td&gt;
&lt;td&gt;Domain experts open PRs to production; the harness owns the quality gate&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ryantsuji/the-author-doesnt-have-to-be-an-engineer-how-the-harness-holds-quality-series-part-5-12e4"&gt;cortex-non-engineer-prs&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Series Final&lt;/td&gt;
&lt;td&gt;The underlying philosophy plus a retrospective on the failures and lessons&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ryantsuji/ai-isnt-something-to-trust-its-something-to-design-series-final-30aa"&gt;cortex-philosophy&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Start with One Scene
&lt;/h2&gt;

&lt;p&gt;"I want to change the calculation logic behind the 'bug rate' KPI on the dashboard. &lt;strong&gt;Where is it, and what might break?&lt;/strong&gt;" — imagine that question comes up before you touch any code.&lt;/p&gt;

&lt;p&gt;When you ask an AI this directly, with no function name and no file path given, it hits cpg with a semantic search and pulls the relevant nodes in one shot. What comes back isn't just functions — it includes &lt;strong&gt;BigQuery tables&lt;/strong&gt; and &lt;strong&gt;API endpoints&lt;/strong&gt; alongside the code. And at the end of the response, there's a &lt;strong&gt;"next action candidates (Runbook)"&lt;/strong&gt; block that tells the AI to re-probe starting from the BQ table with the most reads and writes flowing through it.&lt;/p&gt;

&lt;p&gt;The final answer looks like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Calculation site&lt;/strong&gt;: &lt;code&gt;calculateRatePer100pt&lt;/code&gt; / &lt;code&gt;calculateBugCount&lt;/code&gt; — both pure functions with no I/O side effects; safe to change in isolation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Writers (upstream)&lt;/strong&gt;: &lt;code&gt;syncKpiMetrics&lt;/code&gt; / &lt;code&gt;writeKpiMetrics&lt;/code&gt; / &lt;code&gt;backfillKpiMetrics&lt;/code&gt; all write to the &lt;code&gt;kpi_bug_rate_per_100pt&lt;/code&gt; table; these are the real aggregation batch jobs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Readers (downstream)&lt;/strong&gt;: &lt;code&gt;BigQueryKpiRepository.getSummaryByDate&lt;/code&gt; reads via BigQuery → &lt;code&gt;/kpi/bugs&lt;/code&gt; API → KPI dashboard page&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Related docs&lt;/strong&gt;: &lt;code&gt;docs/generator/kpi.md&lt;/code&gt; defines bug rate; updating the code without updating docs would leave them stale&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;"Update the docs together, and schedule the deploy when the aggregation batch isn't running" — that's a decision you can make with confidence.&lt;/p&gt;

&lt;p&gt;I personally know all this — I wrote it. But that's exactly the problem: &lt;strong&gt;anyone else who wanted to touch this had to track me down&lt;/strong&gt;. Three months ago, "finding out where something lives and what would break" meant finding me. Now, this same investigation is done by &lt;strong&gt;PMO members (non-engineers) using cpg on their own&lt;/strong&gt;. grep didn't get them there; documentation didn't get them there. One natural-language question did.&lt;/p&gt;

&lt;p&gt;What makes that possible is cpg — a graph where you can follow "&lt;strong&gt;what you want to do&lt;/strong&gt;" in plain language to the relevant nodes in one or two hops, even when you don't know the function name. The &lt;strong&gt;Runbook structure&lt;/strong&gt; — where the tool's return value itself contains the next tool call to make — is what lets the AI re-select its starting point and drill deeper on its own.&lt;/p&gt;

&lt;p&gt;That's the setup. Now let me explain how it's built.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Static Analysis Alone Couldn't Do
&lt;/h2&gt;

&lt;p&gt;cortex has a separate system that &lt;strong&gt;graph-analyzes the production codebase using static analysis&lt;/strong&gt; (I'll write about this in its own post — just touching it here). It parses JS/TS code with AST analysis across our external-facing production repos, automatically extracting function call graphs, API endpoints, DB access patterns, and event pub/sub relationships.&lt;/p&gt;

&lt;p&gt;This works well for what it does, and &lt;strong&gt;we still use it actively in the production repos&lt;/strong&gt;. But when we tried applying the same approach to cortex itself, it didn't get us where we wanted to go.&lt;/p&gt;

&lt;p&gt;Three specific gaps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;No context&lt;/strong&gt; — nodes exist but carry no &lt;em&gt;meaning&lt;/em&gt;. "What is this API for?" "Why does this column exist?" isn't in the graph. Ask "where is the code that calculates the KPI bug rate?" and you'll miss unless the function name happens to look like it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No entry point&lt;/strong&gt; — you &lt;strong&gt;already have to know&lt;/strong&gt; the file path or function name before search can start. "Let me go find it" doesn't work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explosion after 1–2 hops&lt;/strong&gt; — starting from any node, related nodes multiply exponentially within a couple of hops, far exceeding what an AI can process in one context window. Trace results become too long to use.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The summary: &lt;strong&gt;mechanically accurate, but no semantic weighting&lt;/strong&gt;. To be genuinely useful to AI, you need one more layer: "&lt;strong&gt;what matters, and why things are connected.&lt;/strong&gt;"&lt;/p&gt;

&lt;h2&gt;
  
  
  Meanwhile, DB Graph Was Working
&lt;/h2&gt;

&lt;p&gt;Around the same time, a different approach — the &lt;a href="https://dev.to/ryantsuji/democratizing-internal-data-building-an-mcp-server-that-lets-you-search-991-tables-in-natural-1da5"&gt;DB Graph MCP&lt;/a&gt; we'd built — was working exactly as intended.&lt;/p&gt;

&lt;p&gt;DB Graph is an MCP server with access to &lt;strong&gt;15 schemas and 991 tables&lt;/strong&gt; inside cortex, supporting semantic search over tables and columns with &lt;strong&gt;AI-generated descriptions&lt;/strong&gt;. A natural-language query like "tables related to return processing confirmation" would find semantically connected nodes even when the table name doesn't contain those words.&lt;/p&gt;

&lt;p&gt;After thinking about why this worked, the answer became clear: &lt;strong&gt;DB Graph has a business-context description attached to every node, and that description is what feeds into the embeddings&lt;/strong&gt;. That semantic weight is what "finding by meaning" actually runs on.&lt;/p&gt;

&lt;p&gt;Static-analysis code graph had none of that. Type relationships and call graphs exist — but "&lt;strong&gt;why this function exists&lt;/strong&gt;" was never written anywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hypothesis — Bring DB Graph's Essence into the Code Graph
&lt;/h2&gt;

&lt;p&gt;The hypothesis was simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"A business-context description on every node, loaded into embeddings" — if that's the core of why DB Graph works, then doing the same thing for the code graph should structurally overcome the limits of static analysis.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The problem was: &lt;strong&gt;where do you put the "business context"?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;All the options:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Location&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;th&gt;Problem&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;External docs&lt;/td&gt;
&lt;td&gt;Design docs / wiki / Notion&lt;/td&gt;
&lt;td&gt;Separate from code. Drifts instantly. Nobody maintains it.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;External metadata&lt;/td&gt;
&lt;td&gt;Sidecar YAML / &lt;code&gt;*.meta.json&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Dual-management. Breaks on rename.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dedicated graph DB&lt;/td&gt;
&lt;td&gt;Write annotations directly into Neo4j / Neptune&lt;/td&gt;
&lt;td&gt;Dual-management again. Doesn't show up in PR diffs — unreviewable.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TypeScript decorator&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@GraphNode({...})&lt;/code&gt; in code&lt;/td&gt;
&lt;td&gt;Lives in the transpiled output = runtime dependency. Can't be extracted by AST alone.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DSL file&lt;/td&gt;
&lt;td&gt;Custom &lt;code&gt;.graph&lt;/code&gt; file format&lt;/td&gt;
&lt;td&gt;High learning cost. No editor support out of the box.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;JSDoc comments&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@graph-business&lt;/code&gt; / &lt;code&gt;@graph-connects&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Physically co-located with the code. Extractable by AST alone. Zero runtime dependency.&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The choice of &lt;strong&gt;JSDoc over decorators&lt;/strong&gt; was intentional:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zero runtime dependency&lt;/strong&gt;: decorators survive into the transpiled output and can affect runtime behavior. JSDoc has no executable runtime semantics; with production builds that strip comments, it leaves no runtime artifact.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generalizes beyond TypeScript&lt;/strong&gt;: the same &lt;code&gt;@graph-*&lt;/code&gt; syntax can extend to Pulumi definitions in &lt;code&gt;infra/&lt;/code&gt; and Markdown frontmatter in &lt;code&gt;docs/&lt;/code&gt;. Decorators are locked to TypeScript syntax.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Single AST pass&lt;/strong&gt;: ts-morph can walk declarations and extract JSDoc in one scan. Decorators sometimes require type resolution, which slows builds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shows up naturally in PR diffs&lt;/strong&gt;: JSDoc sits directly above the code it annotates, so when code changes, the JSDoc diff appears in the same file. Reviewers can't miss it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Doubles as documentation for both humans and AI&lt;/strong&gt;: JSDoc already serves as IDE hover text and AI-readable context. Putting &lt;code&gt;@graph-business&lt;/code&gt; there means it simultaneously explains the declaration to a human reading the code, and gives a coding AI semantic context about the surrounding functions. Graph metadata that also functions as inline documentation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Note that the essence of this design is &lt;strong&gt;using parseable annotations co-located with code as the SSoT&lt;/strong&gt; — TypeScript / JSDoc is just one implementation. The same pattern works in any language with comparable comment + AST primitives: Python docstrings + &lt;code&gt;ast&lt;/code&gt;, Go comments + &lt;code&gt;go/ast&lt;/code&gt;, Rust &lt;code&gt;///&lt;/code&gt; + &lt;code&gt;syn&lt;/code&gt;. &lt;strong&gt;What matters isn't &lt;em&gt;where&lt;/em&gt; you write the annotations, but the invariant: "physically co-located with the code, extractable by AST alone."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Same goes for the monorepo: &lt;strong&gt;this pattern doesn't depend on cortex being a monorepo&lt;/strong&gt;. If anything, &lt;strong&gt;its real value shows when repositories are split and AI can't easily follow code across them&lt;/strong&gt;. In a monorepo, the AI can still grep / read files across the whole tree; in a multi-repo, the cross-repo calls and data flows are the hard part to follow. Run the same build per repo, emit nodes / edges, aggregate into a central graph, and those cross-repo connections become reachable in one hop. We actually run a parallel knowledge graph over our external-facing production repos (multi-repo) using the same pattern — more on that in a separate post.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Approach — Abandon Code Inference, Make JSDoc the SSoT
&lt;/h2&gt;

&lt;p&gt;The code graph's problem was &lt;strong&gt;no meaning&lt;/strong&gt;. The answer is simple: &lt;strong&gt;embed the meaning directly in the code&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For cortex's own code graph, we &lt;strong&gt;completely abandoned the approach of inferring graph structure from code&lt;/strong&gt;. Instead:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Every declaration — function / class / method / API / Page / Cron / etc. — gets a dedicated JSDoc tag. The graph is assembled from those.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This means the &lt;strong&gt;SSoT (Single Source of Truth) for business context becomes the code itself&lt;/strong&gt;. There's no gap between docs and code, because &lt;strong&gt;the JSDoc in the code is the authoritative source&lt;/strong&gt;. The structural problem of "AI makes mistakes because docs are stale" is resolved at the level of where the data lives.&lt;/p&gt;

&lt;p&gt;Placing the two side by side — "a graph from code inference alone" versus "a knowledge graph with JSDoc as SSoT" — makes the difference in what's carried on each node immediately visible:&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%2Fgxsr5j73ufyjias48w9q.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%2Fgxsr5j73ufyjias48w9q.png" alt="Before / After — graph from code inference alone vs. knowledge graph with JSDoc as SSoT" width="800" height="540"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's a concrete example of the tags (from cpg's own source):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/**
 * Set embeddings on nodes in place.
 * Compares textForEmbedding against existing BQ data; only re-generates
 * for nodes where the text has changed.
 *
 * @graph-stack product-graph
 * @graph-domain Engineering
 * @graph-business Compares hash of textForEmbedding against existing BQ nodes; re-generates
 *   embedding only for nodes where text has changed. Unchanged nodes reuse BQ embeddings.
 * @graph-connects cortex.product_graph_nodes [queries, via:id] read existing embeddings
 * @graph-connects vertex-ai-embedding [calls] generate embeddings for changed nodes
 */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;generateEmbeddings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ProductGraphNode&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;
  &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;force&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="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="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="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;What each tag does:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tag&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@graph-node&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Explicitly declares node type (defaults to Function)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@graph-stack&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The infra stack this declaration belongs to&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@graph-domain&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Business domain (comma-separated, multiple allowed)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@graph-business&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;What this declaration specifically does&lt;/strong&gt; — the body of the embedding input&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@graph-connects&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Connection targets (multiple allowed; &lt;code&gt;via:&lt;/code&gt; for parameter-level tracking; &lt;code&gt;none&lt;/code&gt; to explicitly declare no connections)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The key is that &lt;code&gt;@graph-business&lt;/code&gt; &lt;strong&gt;feeds directly into the embedding input&lt;/strong&gt;. It's not the node name — it's a &lt;strong&gt;natural-language sentence&lt;/strong&gt; that carries semantic weight into search. In practice, almost all of these sentences are written by AI: during the normal flow of writing code in cortex, the AI writes the JSDoc alongside the code (and thanks to the ESLint enforcement below, it doesn't forget).&lt;/p&gt;

&lt;h3&gt;
  
  
  Making Omissions Physically Impossible
&lt;/h3&gt;

&lt;p&gt;This design collapses the moment someone leaves a tag out. One function without &lt;code&gt;@graph-business&lt;/code&gt; = that function is invisible to semantic search. One without &lt;code&gt;@graph-connects&lt;/code&gt; = the data flow through that function is absent from the graph.&lt;/p&gt;

&lt;p&gt;So we built &lt;strong&gt;enforcement that makes omissions physically impossible&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;5 ESLint plugins&lt;/strong&gt; — tag presence validation, syntax validation, naming convention enforcement (stack / domain allowlists), &lt;code&gt;@graph-connects&lt;/code&gt; required, &lt;code&gt;@graph-connects none&lt;/code&gt; misuse detection (flags when &lt;code&gt;none&lt;/code&gt; appears on code that calls external services)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automated PR review&lt;/strong&gt; (Part 1 ③) — tags missing are flagged as &lt;code&gt;[Graph] Critical&lt;/code&gt;; docs inconsistency is flagged as &lt;code&gt;[Doc] Critical&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result: &lt;strong&gt;"write a declaration → business context is always written with it"&lt;/strong&gt; holds as an invariant. Add a function → its meaning and connections are necessarily in its JSDoc.&lt;/p&gt;

&lt;p&gt;One honest note: &lt;strong&gt;forcing "5 JSDoc tags on every declaration" on humans would blow up in code review within three days&lt;/strong&gt;. Writing a &lt;code&gt;@graph-business&lt;/code&gt; sentence per function, enumerating &lt;code&gt;@graph-connects&lt;/code&gt; exhaustively, checking the naming allowlists — that's genuinely tedious at scale.&lt;/p&gt;

&lt;p&gt;This works because &lt;strong&gt;AI writes the code&lt;/strong&gt;. Writing four required JSDoc tags (plus optional &lt;code&gt;@graph-node&lt;/code&gt; when the default &lt;code&gt;Function&lt;/code&gt; type isn't enough) is rounding error on top of writing the code itself. With ESLint and automated review in the feedback loop, the AI doesn't miss tags — and human reviewers only need to check "is this tag factually correct?" not "is it there?"&lt;/p&gt;

&lt;p&gt;:::message&lt;br&gt;
This design is one that &lt;strong&gt;can't realistically be maintained when humans write code&lt;/strong&gt;, but &lt;strong&gt;becomes viable the moment AI does&lt;/strong&gt;. It's an AI-first design. The premise of AI-first development is what lets business context be fixed in code as the SSoT.&lt;br&gt;
:::&lt;/p&gt;
&lt;h3&gt;
  
  
  Where Hallucination Happens Shifts
&lt;/h3&gt;

&lt;p&gt;Viewed from another angle, what's going on here is that &lt;strong&gt;the location of hallucination shifts&lt;/strong&gt;. &lt;strong&gt;Where you contain hallucination is, I think, fundamental to AI harness design&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;As I &lt;a href="https://dev.to/ryantsuji/graph-rag-isnt-a-one-shot-anymore-the-case-for-agentic-graph-rag-mcps-1dj5"&gt;wrote elsewhere&lt;/a&gt;, when you combine AI with a graph system, "&lt;strong&gt;hallucination doesn't disappear — it just changes location.&lt;/strong&gt;" For cpg, here's where it lands:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Graph build / query phase&lt;/strong&gt;: &lt;strong&gt;No fresh LLM generation.&lt;/strong&gt; Once reviewed metadata lands in the graph, the ts-morph AST pass, the BigQuery MERGE, and the MCP query responses are all deterministic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JSDoc writing phase&lt;/strong&gt;: This is the entry point for hallucination. Whether &lt;code&gt;@graph-business&lt;/code&gt; is factually accurate, or whether &lt;code&gt;@graph-connects&lt;/code&gt; is exhaustively listed — these can go wrong since the AI is writing them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But &lt;strong&gt;the entry point is locked down by automated PR review&lt;/strong&gt;. Missing tags get &lt;code&gt;[Graph] Critical&lt;/code&gt;; factual drift gets &lt;code&gt;[Doc] Critical&lt;/code&gt;. When something's wrong, either the AI that wrote the code or another reviewer AI catches it and fixes it.&lt;/p&gt;

&lt;p&gt;The result: &lt;strong&gt;once data lands in the graph, it can be treated as deterministically sourced from reviewed code, not as a fresh generated answer that might hallucinate on every query&lt;/strong&gt;. AI agents calling cpg don't have to guard against "this might be a generated lie" on every returned node or edge. The tools can be designed as "return facts only" without compromise.&lt;/p&gt;
&lt;h2&gt;
  
  
  Build — AST to Graph via ts-morph
&lt;/h2&gt;

&lt;p&gt;Once JSDoc is established as the SSoT, the rest is mechanics: extract it and assemble the graph. The implementation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;AST-analyze JS/TS with ts-morph&lt;/strong&gt; — walk every declaration (function / class / method / type / enum / variable / expression statement / &lt;code&gt;export default&lt;/code&gt; / etc.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extract &lt;code&gt;@graph-*&lt;/code&gt; tags from JSDoc&lt;/strong&gt; — collect the four required tags plus optional &lt;code&gt;@graph-node&lt;/code&gt; and normalize into a &lt;code&gt;ParsedGraphTags&lt;/code&gt; structure&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generate nodes&lt;/strong&gt; — use &lt;code&gt;qualifiedName = "&amp;lt;filePath&amp;gt;:&amp;lt;name&amp;gt;"&lt;/code&gt; as the node ID&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generate edges&lt;/strong&gt; — one edge per &lt;code&gt;@graph-connects&lt;/code&gt; entry, with &lt;code&gt;via:&lt;/code&gt; / &lt;code&gt;cardinality&lt;/code&gt; and other metadata preserved&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generate embeddings&lt;/strong&gt; — send &lt;code&gt;@graph-business&lt;/code&gt; text to Vertex AI Embedding (&lt;code&gt;gemini-embedding-2&lt;/code&gt;) and vectorize it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Load into BigQuery&lt;/strong&gt; — MERGE all nodes / edges into &lt;code&gt;cortex.product_graph_nodes&lt;/code&gt; / &lt;code&gt;cortex.product_graph_edges&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Because &lt;code&gt;@graph-business&lt;/code&gt; goes directly into the embedding input, querying "&lt;strong&gt;code that calculates the KPI bug rate&lt;/strong&gt;" in natural language returns a hit based on semantic proximity of the description — even when the function name contains neither "bug" nor "rate."&lt;/p&gt;

&lt;p&gt;The overall flow: the three tracks (&lt;code&gt;apps/&lt;/code&gt; / &lt;code&gt;infra/&lt;/code&gt; / &lt;code&gt;docs/&lt;/code&gt;) each go through their own parser, are merged into a single node set by the generator, and only nodes whose text has changed are sent to Vertex AI before being stored in BigQuery:&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%2Fcox0uq3d52e058rdgug8.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%2Fcox0uq3d52e058rdgug8.png" alt="Build pipeline — assembling one knowledge graph from JSDoc, Pulumi, and docs" width="800" height="470"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Build Cost Is Effectively Zero
&lt;/h3&gt;

&lt;p&gt;The build runs automatically on push to main via GitHub Actions, using a differential embedding approach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Compare &lt;code&gt;textForEmbedding&lt;/code&gt; of each BQ node against the new text&lt;/li&gt;
&lt;li&gt;Unchanged nodes reuse their existing BQ embeddings&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Only changed nodes go to Vertex AI&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A typical push changes a few dozen nodes, so cost is &lt;strong&gt;under $0.001&lt;/strong&gt;. Full regeneration (for recovery, triggered via &lt;code&gt;workflow_dispatch&lt;/code&gt;) is ~$0.075 for 8,000+ nodes.&lt;/p&gt;
&lt;h3&gt;
  
  
  Why BigQuery, Not a Graph Database
&lt;/h3&gt;

&lt;p&gt;When people hear "knowledge graph," they often imagine a dedicated graph DB (Neo4j, Neptune, Memgraph, etc.). cortex runs on &lt;strong&gt;just two BigQuery tables&lt;/strong&gt; (&lt;code&gt;product_graph_nodes&lt;/code&gt; / &lt;code&gt;product_graph_edges&lt;/code&gt;). Three reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Different cost structure&lt;/strong&gt; — dedicated graph DBs set a floor of "always-on cluster cost"; for the current implementation, BQ is &lt;strong&gt;storage + on-demand queries only&lt;/strong&gt;. Even with continuous AI traffic, it's clearly cheaper than running a server 24/7.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vector search / cosine similarity / SQL in the same place&lt;/strong&gt; — BQ has &lt;a href="https://cloud.google.com/bigquery/docs/vector-search" rel="noopener noreferrer"&gt;&lt;code&gt;VECTOR_SEARCH&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-distance" rel="noopener noreferrer"&gt;&lt;code&gt;ML.DISTANCE&lt;/code&gt;&lt;/a&gt;, so semantic search over &lt;code&gt;@graph-business&lt;/code&gt; embeddings, filter by node properties, and adjacent-node JOINs can all live in &lt;strong&gt;one query&lt;/strong&gt;. That matters when "semantic search + property filter + neighbor JOIN" is the standard access pattern.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Migration-ready for GQL once BQ Graph goes GA&lt;/strong&gt; — BQ already has &lt;a href="https://cloud.google.com/bigquery/docs/graph-overview" rel="noopener noreferrer"&gt;Graph in BigQuery&lt;/a&gt; in Preview; once it ships GA, you can put a graph view over the existing tables and likely shift to &lt;code&gt;MATCH (n)-[e]-&amp;gt;(m)&lt;/code&gt; queries in GQL. &lt;strong&gt;The current table design is already migration-ready.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In short: &lt;strong&gt;get the graph DB's future strength (GQL) while running on plain BQ tables today&lt;/strong&gt;. Compared to adding a graph DB on top of a generic RAG stack (pgvector / Pinecone / etc.), fewer systems to operate and lower learning curve.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Core Part Is Available as an Open-Source Sample
&lt;/h3&gt;

&lt;p&gt;The &lt;strong&gt;"parse JSDoc annotations with AST analysis and output a graph"&lt;/strong&gt; part is small enough to reproduce cleanly, so I published it as a working sample:&lt;/p&gt;

&lt;p&gt;🔗 &lt;strong&gt;&lt;a href="https://github.com/thujikun/graph-jsdoc-extractor" rel="noopener noreferrer"&gt;graph-jsdoc-extractor&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It's a ~500-line library that extracts &lt;code&gt;@graph-*&lt;/code&gt; and outputs ndjson of &lt;code&gt;{ kind: "node", ... }&lt;/code&gt; / &lt;code&gt;{ kind: "edge", ... }&lt;/code&gt; objects. Comes with a &lt;code&gt;pnpm run example&lt;/code&gt; that runs end-to-end. For those who just want to see the output format without cloning, the built ndjson is checked in: &lt;strong&gt;&lt;a href="https://github.com/thujikun/graph-jsdoc-extractor/blob/main/examples/sample/output.ndjson" rel="noopener noreferrer"&gt;examples/sample/output.ndjson&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This is intentionally just the "turn code into a graph" part. The real value in cortex starts when &lt;strong&gt;docs and DB schemas land on the same graph&lt;/strong&gt; — that's the next section.&lt;/p&gt;
&lt;h2&gt;
  
  
  Connections — Landing Docs and DB on the Same Graph
&lt;/h2&gt;

&lt;p&gt;Looking at the sample ndjson, a &lt;code&gt;@graph-connects users [reads_from, via:id]&lt;/code&gt; entry has &lt;code&gt;users&lt;/code&gt; stored as a &lt;strong&gt;raw string&lt;/strong&gt; in &lt;code&gt;targetId&lt;/code&gt;. Leaving that as-is means it's just a string. Resolving &lt;code&gt;users&lt;/code&gt; into a &lt;strong&gt;rich node carrying column definitions, partition info, and per-column descriptions&lt;/strong&gt; — that's where the resolution power of search takes a real step forward.&lt;/p&gt;

&lt;p&gt;cortex does this in three directions.&lt;/p&gt;
&lt;h3&gt;
  
  
  1. DB Schemas as Nodes in the Same Graph
&lt;/h3&gt;

&lt;p&gt;cpg ingests not just code but cortex's DB schemas in the same build. A &lt;code&gt;@graph-connects users [queries, via:id]&lt;/code&gt; on the code side gets resolved at build time into a &lt;strong&gt;rich Table node&lt;/strong&gt; carrying column definitions, partition metadata, and descriptions (if the same-named stub exists, its internals are replaced while its ID and all inbound edges survive).&lt;/p&gt;

&lt;p&gt;The key point: &lt;strong&gt;table and column descriptions aren't AI-generated annotations attached after the fact — they're pulled directly from the &lt;code&gt;description&lt;/code&gt; fields in the Pulumi schema definitions&lt;/strong&gt;. Here's what that looks like (excerpt from cpg's own table definition):&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;productGraphNodesTable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;gcp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bigquery&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cortex-prod-product-graph-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="na"&gt;datasetId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cortex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;tableId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;product_graph_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;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Product Graph nodes — unified knowledge graph of code + DB + docs. &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Auto-generated from JSDoc @graph-* tags&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;STRING&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;REQUIRED&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Unique node ID (graphId:nodeType:filePath:name format)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;nodeType&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;STRING&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;REQUIRED&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Node type — ApiEndpoint, BigQueryTable, Function, Module, Document, etc.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;qualifiedName&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;STRING&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Fully qualified name — filePath:exportName format&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="c1"&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;Both the table-level and column-level descriptions &lt;strong&gt;become the embedding input for semantic search directly from the Pulumi definition&lt;/strong&gt;. The same philosophy as cpg's JSDoc — "write the description at the place the thing is defined" — runs all the way through the DB layer. Fix a Pulumi &lt;code&gt;description&lt;/code&gt; → semantic search improves. Same mechanics as fixing a JSDoc.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Docs Auto-Promoted to Nodes via Directory Convention
&lt;/h3&gt;

&lt;p&gt;Markdown files under &lt;code&gt;docs/&lt;/code&gt; also land in the graph. The mechanism is simple: &lt;strong&gt;the directory structure is conventionalized&lt;/strong&gt; so that which stack and domain each doc belongs to is deterministically resolvable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docs/{category}/{name}.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Examples from cpg itself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;docs/product-graph/README.md&lt;/code&gt; → stack: &lt;code&gt;product-graph&lt;/code&gt;, domain: &lt;code&gt;Engineering&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;docs/code-graph/README.md&lt;/code&gt; → stack: &lt;code&gt;code-graph&lt;/code&gt;, domain: &lt;code&gt;Engineering&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;docs/mcp/db-graph/README.md&lt;/code&gt; → stack: &lt;code&gt;mcp-db-graph-server&lt;/code&gt;, domain: &lt;code&gt;Engineering&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each file is ingested as a &lt;strong&gt;Document node&lt;/strong&gt; in the graph, and a &lt;code&gt;documented_by&lt;/code&gt; edge is auto-generated from code nodes whose &lt;code&gt;@graph-stack&lt;/code&gt; matches the doc's stack. Code under &lt;code&gt;apps/graph/product/&lt;/code&gt; all carries &lt;code&gt;@graph-stack product-graph&lt;/code&gt;, so it's automatically linked to &lt;code&gt;docs/product-graph/README.md&lt;/code&gt;. Change code → related docs are already linked.&lt;/p&gt;

&lt;p&gt;This means an AI reviewer can answer "did this code change leave related docs stale?" &lt;strong&gt;in one graph hop&lt;/strong&gt; (that's the source of the &lt;code&gt;[Doc] Critical&lt;/code&gt; comments from Part 1).&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Infrastructure Definitions as Nodes
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;@graph-*&lt;/code&gt; tags go on Pulumi code in &lt;code&gt;infra/&lt;/code&gt; too. An example from cortex's own graph infrastructure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/**
 * @graph-node {CronSchedule}
 * @graph-stack code-graph
 * @graph-domain Engineering
 * @graph-business graph-boundary-daily: runs cross-repository boundary analysis at 7:00 AM JST
 *   daily (auto-detecting API, DB, and Event connections across repos)
 * @graph-connects graph-index-job [triggers] trigger Cloud Run Job
 */&lt;/span&gt;
&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;gcp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cloudscheduler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Job&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-graph-boundary-schedule`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This becomes a &lt;strong&gt;CronSchedule node&lt;/strong&gt; in the graph, connected to the target CloudRunJob node by a &lt;code&gt;triggers&lt;/code&gt; edge. The Pulumi definition is itself a graph entry point — "&lt;strong&gt;what code runs in this cron?&lt;/strong&gt;" is now answerable by graph traversal.&lt;/p&gt;

&lt;h3&gt;
  
  
  Result: Four Layers on One Graph
&lt;/h3&gt;

&lt;p&gt;Adding the three together, the node types in the graph look like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Node type&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Function / Class / Method&lt;/td&gt;
&lt;td&gt;Code (JSDoc)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ApiEndpoint / Page&lt;/td&gt;
&lt;td&gt;Code (JSDoc &lt;code&gt;@graph-node&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BigQueryTable / FirestoreCollection (stub)&lt;/td&gt;
&lt;td&gt;Code &lt;code&gt;@graph-connects&lt;/code&gt; targets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Table / Column / Schema&lt;/strong&gt; (rich)&lt;/td&gt;
&lt;td&gt;Schema files defined in Pulumi&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Document&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Directory parser over &lt;code&gt;docs/&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CronSchedule / PubSubTopic / CloudRunService&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;infra/&lt;/code&gt; JSDoc&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Edge types correspondingly:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Edge type&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;calls / queries / reads_from / writes_to / publishes / triggers&lt;/td&gt;
&lt;td&gt;code → other nodes (&lt;code&gt;@graph-connects&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;documented_by&lt;/td&gt;
&lt;td&gt;code → Document (auto-generated on stack match)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HAS_TABLE / HAS_COLUMN&lt;/td&gt;
&lt;td&gt;Schema → Table → Column (DB side)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;shares_topic&lt;/td&gt;
&lt;td&gt;Between boundary nodes sharing a topic&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Code ↔ DB ↔ docs ↔ infra&lt;/strong&gt; — all reachable in one hop on the same graph. This is what "Product Graph" means: cortex's unified knowledge graph.&lt;/p&gt;

&lt;p&gt;Here's an actual visualization of a slice of cpg itself. Starting from &lt;code&gt;generateEmbeddings&lt;/code&gt; (code), you can see &lt;code&gt;cortex.product_graph_nodes&lt;/code&gt; (BigQueryTable) with its columns, the Pulumi table definition resource, &lt;code&gt;docs/product-graph/README.md&lt;/code&gt;, external services like Vertex AI, and a separate layer's &lt;code&gt;graph-boundary-daily&lt;/code&gt; (CronSchedule) — &lt;strong&gt;all connected by edges on the same node set&lt;/strong&gt;:&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%2Fwa75qtqgt7043xrl8w5f.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%2Fwa75qtqgt7043xrl8w5f.png" alt="Product Graph — a knowledge graph with four layers on the same node set" width="800" height="575"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Where the Sample Stops
&lt;/h3&gt;

&lt;p&gt;graph-jsdoc-extractor &lt;strong&gt;intentionally leaves out&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Resolving &lt;code&gt;@graph-connects&lt;/code&gt; targets to real node IDs&lt;/strong&gt; (cortex uses a seven-stage resolver; the rules are project-specific)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Same-name merging&lt;/strong&gt; (cortex promotes DB-schema-side rich nodes to replace stubs; the merge source is project-specific)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The docs directory convention parser&lt;/strong&gt; (cortex's &lt;code&gt;docs/{category}/{name}.md&lt;/code&gt; convention is cortex-specific)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Embedding generation&lt;/strong&gt; (Vertex AI setup is up to you)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are parts where &lt;strong&gt;the right answer differs per project&lt;/strong&gt; — naming conventions, where docs live, which embedding model to use, when to promote a stub to a rich node. Baking one answer into the sample library would make it harder to use, not easier. The sample draws the line at JSDoc → graph structure, and this article's job is "here's how we did it in cortex — translate it to your project's context."&lt;/p&gt;

&lt;h2&gt;
  
  
  MCP Tool Design and the Runbook Pattern
&lt;/h2&gt;

&lt;p&gt;The graph is now assembled. Next: &lt;strong&gt;how AI uses it&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;cpg runs as an MCP server (&lt;code&gt;cortex-product-graph&lt;/code&gt;). From the AI's side, three tools are visible, applying the &lt;strong&gt;three-layer tool design&lt;/strong&gt; (search / detail / traverse) from &lt;a href="https://dev.to/ryantsuji/graph-rag-isnt-a-one-shot-anymore-the-case-for-agentic-graph-rag-mcps-1dj5"&gt;the Agentic Graph RAG MCP post&lt;/a&gt; directly to cpg:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;search_product_graph_nodes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Find entry points (vector search + name search)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;get_product_graph_node_detail&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deterministically fetch detail by ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;trace_product_graph_connections&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;BFS subgraph traversal (&lt;code&gt;via_filter&lt;/code&gt; for parameter-level tracking)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three layers only shows you what's &lt;em&gt;in&lt;/em&gt; the graph. For jumping from graph nodes to the actual data they point to, &lt;strong&gt;supplementary tools live in the same MCP&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Supplementary tool&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;read_file&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Pass a node's &lt;code&gt;path&lt;/code&gt; property directly to fetch source (Function / Class / Method / ApiEndpoint / Document — any code-origin node carries &lt;code&gt;path&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;grep_code&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Pattern search across the repository&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;git_blame&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Last author, commit, and timestamp per line&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;query_product_graph_bq&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Direct SQL against BigQuery. Find a BQTable node in the graph, then jump to its live data (executed via user OAuth, so BQ IAM applies as-is)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;read_firestore&lt;/code&gt; / &lt;code&gt;write_firestore&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Read/write Firestore collections. Find a FirestoreCollection node in the graph, then go to the live documents (Firestore access follows the same user / environment permission boundary; cpg provides the entry point, not a bypass around IAM)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;list_product_graph_stacks&lt;/code&gt; / &lt;code&gt;list_product_graph_domains&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Lists all stack / domain names present in the graph; useful for orienting before a search&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In other words, cpg's MCP is &lt;strong&gt;a two-tier design: the three-layer structure for graph traversal + supplementary tools for descending into live data (source code / BQ / Firestore)&lt;/strong&gt;. The AI can do "search by meaning → traverse by structure → pull live data" &lt;strong&gt;entirely within one MCP server&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Runbook Pattern — Return Values Contain the Next Action
&lt;/h3&gt;

&lt;p&gt;Every MCP response ends with a &lt;strong&gt;"related nodes (next action candidates)" block&lt;/strong&gt;. For example, after a search returns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;3 nodes found:
- apps/generator/kpi/src/kpi-calculator.ts:calculateBugCount (Function)
- backlog_no_embedding.kpi_bug_rate_per_100pt (BigQueryTable)
- /kpi/bugs (ApiEndpoint)

## Related nodes (next action candidates)

### 🛠 Code (1)
- apps/generator/kpi/src/kpi-calculator.ts:calculateBugCount
  → `get_product_graph_node_detail("apps/generator/kpi/src/kpi-calculator.ts:calculateBugCount")`

### 🗄 DB tables (1)
- backlog_no_embedding.kpi_bug_rate_per_100pt
  → `trace_product_graph_connections(start_node: "backlog_no_embedding.kpi_bug_rate_per_100pt", direction: "backward")`

### 🌐 API (1)
- /kpi/bugs
  → `get_product_graph_node_detail("/kpi/bugs")`
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Copy-pasteable tool calls are lined up by node type, showing exactly what to call next.&lt;/strong&gt; The AI gets new options on every call, so it never has to figure out "what should I do now?"&lt;/p&gt;

&lt;p&gt;Here's the AI ↔ MCP loop in diagram form. The MCP bundles next action candidates into every search response; the AI picks one and makes the next call, repeating:&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%2Ffe0siod62k9ohnj4pz9j.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%2Ffe0siod62k9ohnj4pz9j.png" alt="Runbook pattern — tool return values contain the next tool call to make" width="800" height="490"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;usecase&lt;/code&gt; Parameter — Switching the Runbook
&lt;/h3&gt;

&lt;p&gt;Every tool accepts a &lt;strong&gt;&lt;code&gt;usecase&lt;/code&gt; parameter&lt;/strong&gt; where the AI declares what kind of investigation it's doing:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;usecase&lt;/th&gt;
&lt;th&gt;Strategy (summary of what cpg optimizes for)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;general&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Basic investigation with unknown entry point. Default.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;design&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Understanding existing feature structure. Read business / connections via &lt;code&gt;get_product_graph_node_detail&lt;/code&gt;. Deep trace is unnecessary; Document nodes take priority.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;impact&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Trace upstream and downstream impact deeply. Hit &lt;code&gt;trace_product_graph_connections&lt;/code&gt; with &lt;code&gt;direction=both&lt;/code&gt; / &lt;code&gt;max_depth=5&lt;/code&gt;. Code + DB + infra + schedules are all on the same graph, so one traversal covers a wide area.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;test-create&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Test design. Fetch detail to read parameters and connected DB / called functions.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;test-review&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Compare existing tests against implementation coverage. Cross-check branch structure of target Function / Method against test case count.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;code-review&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Check impact of changes and detect &lt;code&gt;@graph-business&lt;/code&gt; violations. Trace impact → detail to check business / source.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bug&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deep trace from error origin. &lt;code&gt;direction=both&lt;/code&gt; / &lt;code&gt;max_depth=5&lt;/code&gt; for upstream callers + downstream data flow.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The same &lt;code&gt;search_product_graph_nodes&lt;/code&gt; call with &lt;code&gt;usecase: "code-review"&lt;/code&gt; returns next action candidates optimized for "verify the change's impact first." With &lt;code&gt;usecase: "bug"&lt;/code&gt; it returns candidates optimized for "trace deep from error origin + fetch logs." The Runbook switches to match the declared intent.&lt;/p&gt;

&lt;p&gt;This matters because &lt;strong&gt;having the AI declare "what kind of investigation I'm doing"&lt;/strong&gt; yields different angles from the same graph. Auto Review internally fires with &lt;code&gt;code-review&lt;/code&gt;; Self-Healing fires with &lt;code&gt;bug&lt;/code&gt; — the flywheel elements from Part 1 each run a different Runbook.&lt;/p&gt;

&lt;h3&gt;
  
  
  CLAUDE.md Convention — Forcing AI to Always Hit cpg First
&lt;/h3&gt;

&lt;p&gt;Throughout this post I've said "the AI uses cpg," but AI doesn't &lt;strong&gt;spontaneously choose&lt;/strong&gt; cpg. Claude Code defaults to grep / glob / file read as its first instinct. To flip that, the root CLAUDE.md in cortex opens with:&lt;/p&gt;

&lt;blockquote&gt;
&lt;h2&gt;
  
  
  Product Graph MCP (cortex-product-graph)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;This is the single most important asset in this repository.&lt;/strong&gt; cortex-product-graph MCP indexes all code, DB schemas, docs, and infra into a unified knowledge graph with business context. It knows everything about this repository.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Always query Product Graph MCP first&lt;/strong&gt; before grep/glob/file reads. It returns richer, contextualized results.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If Product Graph MCP is unavailable&lt;/strong&gt; (auth expired, server down) and you are NOT in autonomous/auto mode, &lt;strong&gt;stop all work immediately&lt;/strong&gt; and ask the user to authenticate. Do not proceed with degraded grep-only investigation.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;Two things matter here. First, the explicit ordering — "cpg first, grep only as fallback." Second, &lt;strong&gt;fallback to grep is explicitly forbidden if cpg is unavailable&lt;/strong&gt;. Without that second clause, the AI happily degrades to "cpg seems down, I'll just grep" and proceeds with stale context and wrong assumptions. With it, cpg unavailability is a hard stop, not a graceful degradation.&lt;/p&gt;

&lt;p&gt;One clause in CLAUDE.md, and Claude Code's first move on any code investigation is pinned to cpg. Article writing, Auto Review, Self-Healing — all follow the same convention, so the entry point is always unified.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Live Example — Investigating cpg with cpg
&lt;/h2&gt;

&lt;p&gt;Enough abstraction. Let me walk through a real cpg query: &lt;strong&gt;using cpg to investigate cpg's own builder core&lt;/strong&gt; — the meta-example.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Semantic search for "the code that extracts graph source data from code annotations"
&lt;/h3&gt;

&lt;p&gt;No function name assumed. Just the intent in plain language:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;search_product_graph_nodes(
  query: "code that extracts graph source data from annotations written in code",
  search_mode: "semantic",
  usecase: "design"
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Top 5 results:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- apps/graph/product/src/parsers/jsdoc-parser.ts:applyGraphTag (Function)
- apps/graph/product/src/parsers/jsdoc-parser.ts:extractTagsFromNode (Function)
- packages/eslint-plugin-graph/src/utils/jsdoc-utils.ts:extractGraphTags (Function)
- apps/graph/product/src/parsers/jsdoc-parser.ts:parseJSDocExports (Function)
- packages/eslint-plugin-graph/src/utils/jsdoc-utils.ts:getGraphTagValue (Function)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The query contained neither "JSDoc" nor "&lt;code&gt;@graph-*&lt;/code&gt;" nor "parser" — yet the intent found the right nodes &lt;strong&gt;via the &lt;code&gt;@graph-business&lt;/code&gt; embedding&lt;/strong&gt;. grep cannot do this.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Trace downstream from that node (&lt;code&gt;usecase: "design"&lt;/code&gt; prioritizes Documents)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;trace_product_graph_connections(
  start_node: "apps/graph/product/src/parsers/jsdoc-parser.ts:parseJSDocExports",
  direction: "forward",
  usecase: "design"
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Edges returned:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- parseJSDocExports --calls--&amp;gt; extractDeclarationsFromFile
- parseJSDocExports --calls--&amp;gt; extractTagsFromNode
- parseJSDocExports --reads_from[via:filePath]--&amp;gt; filesystem
- parseJSDocExports --documented_by--&amp;gt; docs/product-graph/README.md (Document)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last one — &lt;code&gt;documented_by&lt;/code&gt; — is the point: &lt;strong&gt;the edge from code to the Document node was auto-generated&lt;/strong&gt;. Following it with &lt;code&gt;read_file&lt;/code&gt; retrieves &lt;code&gt;docs/product-graph/README.md&lt;/code&gt; — and with it, &lt;strong&gt;the background, design rationale, and tag specification for this implementation&lt;/strong&gt;, all in one hop.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: The meta-structure — this article itself is written with cpg
&lt;/h3&gt;

&lt;p&gt;This article was drafted by Claude Code, not by me — I provided direction and review. That Claude Code has cpg MCP connected, so every time I said "show a real example from cpg's own code" or "use a cpg-related infra example," Claude queried cpg to pull actual function names, JSDoc, Pulumi definitions, and docs structure, then embedded them in the text.&lt;/p&gt;

&lt;p&gt;In other words: the &lt;strong&gt;&lt;code&gt;generateEmbeddings&lt;/code&gt; JSDoc, the Pulumi &lt;code&gt;productGraphNodesTable&lt;/code&gt; description, the &lt;code&gt;graph-boundary-daily&lt;/code&gt; cron annotation, the auto-link to &lt;code&gt;docs/product-graph/README.md&lt;/code&gt;&lt;/strong&gt; — none of these came from my memory. &lt;strong&gt;Claude queried cpg and found the real artifacts&lt;/strong&gt;. My role is only the review judgment: "this is right / this is wrong."&lt;/p&gt;

&lt;p&gt;This is the pattern repeating across all of cortex. &lt;strong&gt;Humans set the direction; AI uses cpg to verify and generate implementations / text / reviews&lt;/strong&gt;. Part 1's ③ Auto Review and ④ Self-Healing run on the same structure. Article writing isn't a special case — as long as cpg exists, AI-driven work always takes this shape.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Changed / Bridge to Part 3
&lt;/h2&gt;

&lt;p&gt;That covers the inside of cpg. A closing summary of how it affects cortex as a whole:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. I stopped running grep&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Without knowing file names or symbol names, I can get the relevant code back by just describing what I want to do. The combination of 120+ apps and a team of one works because of this, more than anything else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Auto Review produces context-grounded comments&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;[Graph]&lt;/code&gt; / &lt;code&gt;[Impact]&lt;/code&gt; / &lt;code&gt;[Doc]&lt;/code&gt; / &lt;code&gt;[Security]&lt;/code&gt; level comments Part 1's ③ Auto Review produces all stand on cpg. The substance is &lt;strong&gt;review carried out with the entire codebase as context&lt;/strong&gt; — that's the real benefit of the cpg integration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Self-Healing can trace from error origin to root cause&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Part 1's ④ Self-Healing can hop from a Grafana alert → code → dependent tables → related docs in one graph traversal because cpg exists. It fires with &lt;code&gt;usecase: "bug"&lt;/code&gt; and takes the shortest path from error to root cause.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. The static-analysis code graph is working somewhere else&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I said "we abandoned code inference" at the top, but that was specifically for cortex itself. For the external-facing production repositories (the core of the business), a different approach supplies context, and static analysis continues to run there. More on that in a separate post.&lt;/p&gt;

&lt;p&gt;Most AI coding setups try to make the AI better at reading an &lt;em&gt;unchanged&lt;/em&gt; repository. cpg takes the opposite approach: &lt;strong&gt;change the repository's information structure so AI has a first-class semantic map to read&lt;/strong&gt;. That's the line between "another GraphRAG" and what cpg actually is.&lt;/p&gt;

&lt;p&gt;In that sense, Product Graph is literally a knowledge graph of the AI, by the AI, for the AI: generated alongside AI-written code, maintained through AI review, and consumed by AI agents as their primary map of the product.&lt;/p&gt;

&lt;p&gt;Coming up in &lt;strong&gt;&lt;a href="https://dev.to/ryantsuji/human-on-the-loop-ai-reviewing-ai-prs-at-cortex-769-prsmonth-while-raising-the-quality-bar-4lh5"&gt;Part 3 — automated PR review&lt;/a&gt;&lt;/strong&gt;: the full pipeline of automated PR review built on top of cpg — from GitHub webhook ingestion through AI review / automated fix / automated merge / parallel deploy. What happens when Auto Review fires with &lt;code&gt;usecase: "code-review"&lt;/code&gt;, how &lt;code&gt;[Graph] Critical&lt;/code&gt; comments are generated, and the worktree mechanism that lets AI apply fixes and push back.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>graphrag</category>
      <category>jsdoc</category>
      <category>mcp</category>
    </item>
    <item>
      <title>Minimal post (test fixture)</title>
      <dc:creator>Ryosuke Tsuji</dc:creator>
      <pubDate>Sun, 17 May 2026 16:48:43 +0000</pubDate>
      <link>https://dev.to/ryantsuji/minimal-post-test-fixture-1pk2</link>
      <guid>https://dev.to/ryantsuji/minimal-post-test-fixture-1pk2</guid>
      <description>&lt;p&gt;Minimal test fixture used by &lt;code&gt;$slug.test.tsx&lt;/code&gt;. No headings, no tags — covers the&lt;br&gt;
null branches of TOC rendering and tag-list rendering in &lt;code&gt;routes/posts/$slug.tsx&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Slugs prefixed with &lt;code&gt;_&lt;/code&gt; are excluded from &lt;code&gt;/posts&lt;/code&gt; listing (production publishing&lt;br&gt;
surface) but remain reachable via direct &lt;code&gt;getRenderedPost(slug, lang)&lt;/code&gt; (the&lt;br&gt;
&lt;code&gt;virtual:rendered-posts&lt;/code&gt; lookup that backs &lt;code&gt;/posts/$slug&lt;/code&gt;) so test fixtures can&lt;br&gt;
be SSR'd without polluting the index.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Building a Real AI Harness: Auto-Reviewed PRs, Self-Healing Ops, and Non-Engineer Contributors (Series Intro)</title>
      <dc:creator>Ryosuke Tsuji</dc:creator>
      <pubDate>Tue, 12 May 2026 16:34:39 +0000</pubDate>
      <link>https://dev.to/ryantsuji/building-a-real-ai-harness-auto-reviewed-prs-self-healing-ops-and-non-engineer-contributors-3lfa</link>
      <guid>https://dev.to/ryantsuji/building-a-real-ai-harness-auto-reviewed-prs-self-healing-ops-and-non-engineer-contributors-3lfa</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;AI assistance disclosure: This article was drafted with the help of Claude. All technical content, design decisions, code references, and screenshots reflect production systems I designed and operate at airCloset; the prose was revised by me prior to publication.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Hi, I'm &lt;a href="https://x.com/ryantsuji" rel="noopener noreferrer"&gt;Ryan&lt;/a&gt;, CTO at airCloset.&lt;/p&gt;

&lt;p&gt;In my previous posts I've introduced &lt;a href="https://dev.to/ryantsuji/we-built-17-mcp-servers-to-let-ai-run-our-internal-operations-3lk2"&gt;the full picture of our 17 internal MCP servers&lt;/a&gt;, &lt;a href="https://dev.to/ryantsuji/democratizing-internal-data-building-an-mcp-server-that-lets-you-search-991-tables-in-natural-1da5"&gt;an MCP server that searches 991 internal tables in natural language&lt;/a&gt;, &lt;a href="https://dev.to/ryantsuji/we-built-a-custom-graph-rag-to-let-ai-answer-did-that-initiative-actually-work-3oda"&gt;a custom Graph RAG for measuring initiative impact&lt;/a&gt;, and &lt;a href="https://dev.to/ryantsuji/bridging-i-want-to-build-and-i-want-to-publish-safely-for-non-engineers-sandbox-mcp-392a"&gt;the Sandbox MCP that lets non-engineers publish AI-built apps safely&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;All of those run on top of an internal AI development platform we call &lt;strong&gt;cortex&lt;/strong&gt;. This post is the first in a series about cortex itself — the platform, the design choices, and the operational experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  Series Index
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Theme&lt;/th&gt;
&lt;th&gt;Key scene&lt;/th&gt;
&lt;th&gt;Article&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Series intro: cortex's harness&lt;/td&gt;
&lt;td&gt;PRs auto-merge / incidents self-heal before you notice&lt;/td&gt;
&lt;td&gt;this post ← you are here&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Product Graph (cpg)&lt;/td&gt;
&lt;td&gt;Code, docs, DB, infra unified into one graph&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ryantsuji/the-heart-of-the-ai-harness-a-knowledge-graph-of-the-ai-by-the-ai-for-the-ai-series-part-2-53bm"&gt;cortex-product-graph&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;AI PR review&lt;/td&gt;
&lt;td&gt;webhook → AI review → auto-fix → squash merge&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ryantsuji/human-on-the-loop-ai-reviewing-ai-prs-at-cortex-769-prsmonth-while-raising-the-quality-bar-4lh5"&gt;cortex-auto-review&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Self-Healing + observability + auto-added guardrails&lt;/td&gt;
&lt;td&gt;Alert → AI investigates → fix PR + new lint/type gate → auto redeploy + same-pattern writes get auto-rejected&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ryantsuji/fixed-before-anyone-notices-stronger-after-every-fix-self-healing-recurrence-prevention-series-1e86"&gt;cortex-self-healing&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Democratizing the maintenance phase&lt;/td&gt;
&lt;td&gt;Domain experts open PRs to production; the harness owns the quality gate&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ryantsuji/the-author-doesnt-have-to-be-an-engineer-how-the-harness-holds-quality-series-part-5-12e4"&gt;cortex-non-engineer-prs&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Series Final&lt;/td&gt;
&lt;td&gt;The underlying philosophy plus a retrospective on the failures and lessons&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/ryantsuji/ai-isnt-something-to-trust-its-something-to-design-series-final-30aa"&gt;cortex-philosophy&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Two Scenes, Up Front
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Scene 1: PRs merge themselves
&lt;/h3&gt;

&lt;p&gt;Monday morning. An engineer implements a feature locally, pushes a branch, opens a PR.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A few minutes later, the AI reviewer comes back with REQUEST_CHANGES. Multiple comments:

&lt;ul&gt;
&lt;li&gt;"This data formatting duplicates &lt;code&gt;formatRow()&lt;/code&gt; in the shared package. Please consolidate."&lt;/li&gt;
&lt;li&gt;"You changed an API response type, but the related docs (&lt;code&gt;docs/api/...&lt;/code&gt;) still describe the old shape."&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;A separate AI agent spawns a worktree, applies the fixes, pushes a follow-up commit&lt;/li&gt;
&lt;li&gt;Re-review comes back as APPROVE&lt;/li&gt;
&lt;li&gt;Auto squash-merge&lt;/li&gt;
&lt;li&gt;GitHub Actions detects only the changed stacks and deploys them to Cloud Run / Cloudflare Pages&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;No human touched any of this&lt;/strong&gt;. The engineer refreshes the PR tab and notices it's already merged.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scene 2: Incidents fix themselves before you notice
&lt;/h3&gt;

&lt;p&gt;7 AM. A Grafana alert fires: "BQ pipeline failed 3 times in a row."&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An AI receives the webhook, fetches the error logs from Loki via the &lt;strong&gt;Grafana MCP&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Walks the &lt;strong&gt;Product Graph&lt;/strong&gt; (implementation name: &lt;code&gt;cortex-product-graph&lt;/code&gt; — a unified knowledge graph of the codebase, docs, DB schemas, and infrastructure definitions; covered later in this post and in Part 2) to trace the pipeline's code, dependent tables, and related docs, identifying the root cause&lt;/li&gt;
&lt;li&gt;Opens a fix PR&lt;/li&gt;
&lt;li&gt;AI reviewer APPROVE → auto squash-merge → automatic redeploy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By the time the engineer logs in at 9 AM, Slack already shows: "pipeline patched." The only incidents engineers personally handle are the ones AI genuinely can't crack.&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%2Froni34pik4wrqw9156g5.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%2Froni34pik4wrqw9156g5.png" alt="Two automation loops" width="799" height="411"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What's behind both scenes is the dev environment described in the rest of this post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Industry Context — "Harness Engineering"
&lt;/h2&gt;

&lt;p&gt;Before I get to cortex, one paragraph of context. Over the past six months, &lt;strong&gt;the practice of building proper foundations for AI agents in production&lt;/strong&gt; has crystallized into a recognized industry trend.&lt;/p&gt;

&lt;p&gt;"Harness" itself isn't a new word. In AI specifically, it traces back to &lt;strong&gt;EleutherAI's &lt;a href="https://github.com/EleutherAI/lm-evaluation-harness" rel="noopener noreferrer"&gt;lm-evaluation-harness&lt;/a&gt; (2020)&lt;/strong&gt; — the LLM evaluation framework that put the term in active use. What changed in the past six months is its elevation into an engineering discipline for &lt;strong&gt;LLM agents in production&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Feb 2026&lt;/strong&gt;: OpenAI published &lt;a href="https://openai.com/index/harness-engineering/" rel="noopener noreferrer"&gt;"Harness engineering: leveraging Codex in an agent-first world"&lt;/a&gt;, describing how a small internal team led by Codex shipped &lt;strong&gt;1 million lines in 5 months&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;A few days later, Mitchell Hashimoto (HashiCorp co-founder, Terraform creator) distilled it into the formula &lt;code&gt;Agent = Model + Harness&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;April 2026&lt;/strong&gt;: Martin Fowler (author of &lt;em&gt;Refactoring&lt;/em&gt;, ThoughtWorks Chief Scientist) published &lt;a href="https://martinfowler.com/articles/harness-engineering.html" rel="noopener noreferrer"&gt;"Harness engineering for coding agent users"&lt;/a&gt;, establishing the &lt;strong&gt;Guides (proactive controls) / Sensors (reactive controls)&lt;/strong&gt; framing&lt;/li&gt;
&lt;li&gt;Same month: Anthropic and Cursor each published their own harness write-ups&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The catchphrase that's gone viral: &lt;strong&gt;"2025 was the year of agents. 2026 is the year of harnesses."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The framing is: &lt;strong&gt;the model itself is rapidly commoditizing&lt;/strong&gt; (the gap between Claude / GPT / Gemini is narrowing from the user side). Where you actually get differentiation is &lt;strong&gt;how you design the harness — the foundation that lets AI run in production&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;cortex is most cleanly read as &lt;strong&gt;a real attempt to build that "harness" inside a real company&lt;/strong&gt;. In this post I'll organize cortex using Fowler's Guides / Sensors framing.&lt;/p&gt;

&lt;p&gt;From here, I'll show &lt;strong&gt;how the "harness beats model" thesis takes concrete shape on cortex&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who Builds the Code
&lt;/h2&gt;

&lt;p&gt;For the first few months, &lt;strong&gt;I built 100% of cortex by myself&lt;/strong&gt;. The accurate framing isn't "without a harness, others can't safely PR" but rather "&lt;strong&gt;without a harness, no one — including me with extra hands — could ride this thing&lt;/strong&gt;."&lt;/p&gt;

&lt;p&gt;Even back then, between &lt;a href="https://dev.to/ryantsuji/how-we-built-an-automated-meeting-intelligence-system-with-google-meet-slack-and-rag-42ln"&gt;our Google Meet recording pipeline&lt;/a&gt; (Japanese), about half of the &lt;a href="https://dev.to/ryantsuji/we-built-17-mcp-servers-to-let-ai-run-our-internal-operations-3lk2"&gt;17 MCP servers&lt;/a&gt;, and a long tail of unpublished features, &lt;strong&gt;roughly 50 loosely-coupled applications were already running&lt;/strong&gt;. Each one had its purpose, background, and data flow documented carefully. But the volume was such that &lt;strong&gt;even with AI in the loop, you couldn't realistically have it read all the relevant docs and absorb the whole picture for any given change&lt;/strong&gt;. The codebase had outgrown what a person — or an AI given pieces — could hold in their head at once.&lt;/p&gt;

&lt;p&gt;Recently, with the harness in place, &lt;strong&gt;non-engineers&lt;/strong&gt; (business-side managers, PMOs, etc.) have started shipping PRs to cortex too. As of writing, the cumulative commit ratio is &lt;strong&gt;~91% me, ~9% other recent contributors&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you imagine non-engineers opening PRs against a production repo, "can quality really hold?" is the obvious question. In cortex, the answer is yes, because &lt;strong&gt;AI review and automation own the quality gates&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PRs missing annotations, tests, or lint cleanliness get REQUEST_CHANGES from the AI reviewer&lt;/li&gt;
&lt;li&gt;A separate AI agent applies the fixes&lt;/li&gt;
&lt;li&gt;Until everything is satisfied, nothing merges&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So whoever writes a PR — engineer or not — &lt;strong&gt;at the moment it merges, the same quality bar is met&lt;/strong&gt;. The key point: it's not "you can write freely," it's "&lt;strong&gt;you can write inside rails that don't let you derail&lt;/strong&gt;." The author's job stops at "communicating the intent precisely"; the harness owns code correctness.&lt;/p&gt;

&lt;p&gt;The shift is from "&lt;strong&gt;X could write that because they're X&lt;/strong&gt;" to "&lt;strong&gt;X can write that because of cortex&lt;/strong&gt;." That property only emerges once the harness is built — and it's the core of cortex's design.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Running
&lt;/h2&gt;

&lt;p&gt;cortex consists of microservices, jobs, MCP servers, web frontends, Cloudflare Workers, and so on. As of writing, there are &lt;strong&gt;123 apps&lt;/strong&gt;. The features I've already covered in past posts are each composed of multiple apps — but even adding them up by feature, &lt;strong&gt;only about 10% of cortex has been written about&lt;/strong&gt;. The remaining 90% hasn't appeared in a post yet. A few examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A unified product UX measurement web app&lt;/strong&gt; — UX metrics, screen analysis, funnels, and error analysis in one place&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A dev-org portal web app&lt;/strong&gt; — KPIs (bug rate, etc.), per-member GitHub Activity, QA evaluation results, plus an AI chat that answers natural-language questions about KPIs via Agentic RAG&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A family of Slack bots&lt;/strong&gt; for operational support:

&lt;ul&gt;
&lt;li&gt;A config bot that lets you manage job configurations (DBs, attendance SaaS, Google Drive, etc.) directly from Slack&lt;/li&gt;
&lt;li&gt;An accounting-assist bot that takes invoice OCR and drafts payment requests / expense filings in our accounting SaaS&lt;/li&gt;
&lt;li&gt;In-channel knowledge search, issue/request management, meeting creation; a BigQuery cross-table RAG bot; a Google Drive cross-corpus RAG bot&lt;/li&gt;
&lt;li&gt;A marketing bot that returns insights (trend, creative analysis) from BigQuery marketing data&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An APM auto-analysis agent&lt;/strong&gt; that runs daily on monitoring-SaaS APM data, detects performance issues, and opens tickets in our issue-tracking SaaS&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An AI-bot auditor bot&lt;/strong&gt; that runs E2E tests against the Slack bots above and detects spec drift&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;…and so on. &lt;strong&gt;Each will get its own dedicated post later in the series.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Scale at a glance:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Count&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;apps (microservices, jobs, MCP servers, web, etc.)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;123&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;packages (shared libraries)&lt;/td&gt;
&lt;td&gt;66&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MCP servers&lt;/td&gt;
&lt;td&gt;19&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pulumi stacks&lt;/td&gt;
&lt;td&gt;110&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TypeScript (implementation)&lt;/td&gt;
&lt;td&gt;~&lt;strong&gt;630K lines&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tests&lt;/td&gt;
&lt;td&gt;~&lt;strong&gt;560K lines&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Markdown documentation&lt;/td&gt;
&lt;td&gt;~&lt;strong&gt;110K lines / 389 files&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Duration&lt;/td&gt;
&lt;td&gt;~&lt;strong&gt;5 months&lt;/strong&gt; (intensive development: ~4 months)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Merged PRs&lt;/td&gt;
&lt;td&gt;~&lt;strong&gt;790&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The 4-Element Flywheel — cortex's Harness
&lt;/h2&gt;

&lt;p&gt;What lets "&lt;strong&gt;~4 months of intensive dev, mostly solo&lt;/strong&gt;" coexist with "&lt;strong&gt;non-engineers shipping into the same repo&lt;/strong&gt;" is a harness design that &lt;strong&gt;delegates quality to AI and automation across every layer&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;cortex's harness is structured as a &lt;strong&gt;flywheel&lt;/strong&gt; of 4 elements, mapped to Fowler's &lt;strong&gt;Guides (proactive) / Sensors (reactive)&lt;/strong&gt; split, that &lt;strong&gt;mutually reinforce one another&lt;/strong&gt;.&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%2Fh17ehxbeab84i6jfxtnw.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%2Fh17ehxbeab84i6jfxtnw.png" alt="cortex AI Harness Flywheel" width="800" height="382"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  ① Product Graph (Guides — supplying the right context)
&lt;/h3&gt;

&lt;p&gt;All of cortex — &lt;strong&gt;code, documentation, DB schemas, infrastructure definitions&lt;/strong&gt; — is indexed in real time as a single unified graph. It's queryable via MCP through &lt;strong&gt;semantic search&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;"Where is the code that calculates this KPI?" → "Which BQ tables does that code touch?" → "What are those tables' column definitions?" → "What docs are related?" — all of these can be answered from a single query traversal. That graph becomes the context source for everything the AI does.&lt;/p&gt;

&lt;p&gt;This is the foundation that &lt;strong&gt;"structurally reduces how often the AI gets confused."&lt;/strong&gt; Where grep tells you "where the string appears," the Product Graph tells you "&lt;strong&gt;what is connected, why, and how&lt;/strong&gt;." Implementation details come in Part 2.&lt;/p&gt;

&lt;h3&gt;
  
  
  ② Lint / Quality Gates (Guides — physically blocking deviations)
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;eslint-disable&lt;/code&gt; / &lt;code&gt;oxlint-disable&lt;/code&gt; are forbidden anywhere in the repo. In hand-written code, occurrences of &lt;code&gt;: any&lt;/code&gt; / &lt;code&gt;as any&lt;/code&gt; / TODO / FIXME are &lt;strong&gt;0&lt;/strong&gt; (excluding generated files and unavoidable external-library cases). &lt;strong&gt;Type checking&lt;/strong&gt; (using &lt;strong&gt;tsgo&lt;/strong&gt; — Microsoft's Go port of the TypeScript compiler, ~10× faster than &lt;code&gt;tsc&lt;/code&gt;; we use it to keep CI time down) runs on the entire codebase in CI.&lt;/p&gt;

&lt;p&gt;On top of that, test coverage is enforced at &lt;strong&gt;≥90% for statements / branches / functions / lines&lt;/strong&gt;. &lt;strong&gt;Lowering the threshold to pass is forbidden&lt;/strong&gt; — you write tests instead.&lt;/p&gt;

&lt;p&gt;With every escape hatch sealed, &lt;strong&gt;even when the AI writes wrong code, it doesn't merge&lt;/strong&gt;. This is also what stabilizes AI review judgments downstream.&lt;/p&gt;

&lt;h3&gt;
  
  
  ③ Auto Review (Sensors — auto-fixing until the bar is met)
&lt;/h3&gt;

&lt;p&gt;Scene 1 above is exactly this. The implementation-side note: &lt;strong&gt;AI review here isn't "lint with extra steps" — every comment is grounded in Product-Graph traversal of the actual impact&lt;/strong&gt;. That's where it earns its keep. To give you a feel, comments that actually fire fall into categories like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;[Graph] Critical&lt;/strong&gt; — missing annotation that breaks an edge in the graph&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;[Impact] Critical&lt;/strong&gt; — a BQ MERGE statement referencing a column not present in the existing target table; would fail in production&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;[Doc] Critical&lt;/strong&gt; — code change that left related docs stale&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;[Security] Minor&lt;/strong&gt; — &lt;code&gt;execSync&lt;/code&gt; doing string interpolation on an env var, opening a command injection vector&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What you might mentally classify as "AI review" — surface-level — isn't this. &lt;strong&gt;Comments here are produced with the entire codebase carried as context&lt;/strong&gt;, which is what the Product Graph integration buys you.&lt;/p&gt;

&lt;p&gt;The only PRs that actually need a human are "AI review hits a hard case." Day-to-day PRs go from push to merge without anyone touching them.&lt;/p&gt;

&lt;h3&gt;
  
  
  ④ Self-Healing (Sensors — re-injecting production anomalies into the loop)
&lt;/h3&gt;

&lt;p&gt;Scene 2 above is exactly this. Starting from a Grafana alert, the AI traces the root cause through Product Graph + Loki + git blame, opens a fix PR, and pushes it through ③ Auto Review until it's auto-merged. &lt;strong&gt;Re-injecting anomalies into the loop&lt;/strong&gt; is the essence of Sensors. Details in a later post.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Makes It a Flywheel
&lt;/h3&gt;

&lt;p&gt;These 4 elements &lt;strong&gt;mutually reinforce one another&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;① Product Graph exists, so ③ Auto Review can comment with real impact awareness&lt;/li&gt;
&lt;li&gt;② Lint enforces the ground rules, so ③ Auto Review can assume "everything in the codebase meets the bar"&lt;/li&gt;
&lt;li&gt;③ Auto Review exists, so new code lands in ① Product Graph with correct semantic annotations&lt;/li&gt;
&lt;li&gt;④ Self-Healing's incidents loop back through ③, maintaining the quality bar all the way back to ①&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The harness's effectiveness scales with the size of the codebase&lt;/strong&gt;, not against it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Supporting Foundations
&lt;/h3&gt;

&lt;p&gt;Three foundations make the 4 elements possible (covered in detail in Part 4):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tests and coverage&lt;/strong&gt;: ~630K lines of implementation, ~560K lines of tests (&lt;strong&gt;impl : test ≒ 1.13 : 1&lt;/strong&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Documentation&lt;/strong&gt;: ~110K lines / 389 files, written &lt;strong&gt;for both humans and AI&lt;/strong&gt;, also ingested as Document nodes in the Product Graph&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observability&lt;/strong&gt;: Frontend = Faro, backend = OTel, infrastructure and CI logs all consolidated in Grafana. &lt;strong&gt;The AI sees the same data humans see.&lt;/strong&gt; Gemini API token usage and cost are tracked separately in Prometheus.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Technical Foundation
&lt;/h2&gt;

&lt;p&gt;cortex is a &lt;strong&gt;full-TypeScript monorepo&lt;/strong&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Stack&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Applications (&lt;code&gt;apps/&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;TypeScript (Hono, TanStack Router, Vite, etc.)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shared packages (&lt;code&gt;packages/&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;TypeScript&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Infrastructure (&lt;code&gt;infra/&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;TypeScript (&lt;strong&gt;Pulumi&lt;/strong&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Edge (&lt;code&gt;worker/&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;TypeScript (Cloudflare Workers)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lint plugins&lt;/td&gt;
&lt;td&gt;TypeScript&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Doc scripts&lt;/td&gt;
&lt;td&gt;TypeScript (tsx)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Having everything in one language is &lt;strong&gt;a much bigger win when viewed from the AI's side&lt;/strong&gt; than from a human's. Specifically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You can feed the AI ASTs and type definitions directly as context&lt;/strong&gt; — no language boundary fragments the picture&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refactors don't cross language boundaries&lt;/strong&gt; — one ESLint plugin can inspect and auto-fix &lt;code&gt;apps/&lt;/code&gt;, &lt;code&gt;packages/&lt;/code&gt;, and &lt;code&gt;infra/&lt;/code&gt; together&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Edges don't break in the Product Graph&lt;/strong&gt; — for example, a Cloud Run service definition (&lt;code&gt;infra/&lt;/code&gt;, TS) connects in a single graph to the Hono route (&lt;code&gt;apps/&lt;/code&gt;, TS) it actually invokes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When you ask the AI "what does this change affect?", the reason it can hop &lt;code&gt;infra → apps → packages&lt;/code&gt; and answer in one round-trip is that all of this is one language.&lt;/p&gt;

&lt;p&gt;Build is parallelized via &lt;a href="https://turbo.build/" rel="noopener noreferrer"&gt;Turborepo&lt;/a&gt; and &lt;a href="https://pnpm.io/" rel="noopener noreferrer"&gt;pnpm workspaces&lt;/a&gt;. Deploys go through GitHub Actions, which &lt;strong&gt;detects only changed stacks&lt;/strong&gt; and applies them in parallel via Pulumi.&lt;/p&gt;

&lt;h2&gt;
  
  
  Numbers (snapshot at time of writing)
&lt;/h2&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%2Ftcdo80auzk2j147fb9qr.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%2Ftcdo80auzk2j147fb9qr.png" alt="Scale" width="799" height="333"&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;&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Duration&lt;/td&gt;
&lt;td&gt;~&lt;strong&gt;5 months&lt;/strong&gt; (intensive development: ~4 months)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Commits&lt;/td&gt;
&lt;td&gt;~&lt;strong&gt;4,000&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Merged PRs&lt;/td&gt;
&lt;td&gt;~&lt;strong&gt;790&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;% of commits authored by me&lt;/td&gt;
&lt;td&gt;~&lt;strong&gt;91%&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;apps&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;123&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;packages&lt;/td&gt;
&lt;td&gt;66&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MCP servers&lt;/td&gt;
&lt;td&gt;19&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pulumi stacks&lt;/td&gt;
&lt;td&gt;110&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TypeScript (implementation)&lt;/td&gt;
&lt;td&gt;~630K lines&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TypeScript (tests)&lt;/td&gt;
&lt;td&gt;~560K lines&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Markdown documentation&lt;/td&gt;
&lt;td&gt;~&lt;strong&gt;110K lines / 389 files&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;as any&lt;/code&gt; / TODO / unjustified lint-disable in hand-written code&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;0&lt;/strong&gt; (excluding generated files / unavoidable external-library cases)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Coverage gate&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;90%&lt;/strong&gt; (statements / branches / functions / lines)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  The PR-flow Switch That Multiplied Throughput
&lt;/h3&gt;

&lt;p&gt;Up until April, &lt;strong&gt;I was AI-assisted reviewing every change carefully on my own machine and then committing directly to main&lt;/strong&gt;. The review bar was unchanged, but throughput was bottlenecked on my hands.&lt;/p&gt;

&lt;p&gt;In April, switching to &lt;strong&gt;fine-grained, PR-based operation&lt;/strong&gt; (auto review → auto fix → auto merge) dramatically changed the per-month merged-PR count:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Month&lt;/th&gt;
&lt;th&gt;Merged PRs&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2026-02&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026-03&lt;/td&gt;
&lt;td&gt;23&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;2026-04&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;518&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026-05 (through the 10th)&lt;/td&gt;
&lt;td&gt;235&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A &lt;strong&gt;~22× jump&lt;/strong&gt; between March and April. Total commits actually went down (because committing directly to main was replaced by going through PRs), so this isn't "I wrote more code." This is "&lt;strong&gt;the manual review step got replaced by the harness, and the throughput ceiling moved&lt;/strong&gt;." &lt;strong&gt;The 22× is exactly the moment a human reviewer was swapped for Auto Review&lt;/strong&gt; — clean evidence of the flywheel property where the harness's effectiveness scales with codebase size.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's Required for These Numbers to Hold
&lt;/h3&gt;

&lt;p&gt;These numbers are &lt;strong&gt;not explained by "we use AI" alone&lt;/strong&gt;. The prerequisites:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Full TypeScript monorepo&lt;/strong&gt; — code, tests, infrastructure, scripts all under one static-analysis system&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Composable Architecture&lt;/strong&gt; — &lt;code&gt;packages/&lt;/code&gt; holds reusable parts; &lt;code&gt;apps/&lt;/code&gt; compose them. Direct imports between &lt;code&gt;apps/&lt;/code&gt; are forbidden — everything routes through &lt;code&gt;packages/&lt;/code&gt;. This is what guarantees components don't interfere with each other.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Strict quality gates&lt;/strong&gt; — lint / coverage / annotations are run "no lowering, no working around"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unified graph&lt;/strong&gt; — code, docs, DB, infrastructure on a single graph as the foundation that lets the AI act with context&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto PR review / auto fix / auto merge / auto self-healing&lt;/strong&gt; — the harness that swaps the rate-limiting manual step for AI&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unified observability&lt;/strong&gt; — humans and AI see the same data (OTel + Faro + Prometheus)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The design has to be in place first, and AI runs on top of it. That's what makes both volume and quality possible at the same time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Composable Architecture&lt;/strong&gt; in particular is what drives the headcount-of-one production. Because components don't interfere, &lt;strong&gt;multiple Claude Code sessions can run in parallel on different parts of the codebase&lt;/strong&gt;. In practice, I've run up to ~10 sessions in parallel at peak — this multiplies with the harness's effectiveness.&lt;/p&gt;

&lt;p&gt;It's &lt;strong&gt;system design, not magic&lt;/strong&gt;. Each piece will get its own deep-dive in this series.&lt;/p&gt;

&lt;h2&gt;
  
  
  Some Honest Caveats
&lt;/h2&gt;

&lt;p&gt;If you've read this far, it might sound like everything runs perfectly on autopilot. It doesn't. Three things I want to be upfront about:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. High code quality doesn't prevent bugs.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;What the harness protects is &lt;strong&gt;"correctness of the code"&lt;/strong&gt; — not &lt;strong&gt;"correctness of the spec."&lt;/strong&gt; Even when implementation is clean, getting the spec interpretation wrong still ships bugs. AI review can catch "code contradicts the documented spec," but if the spec itself is wrong, the issue sails right through. That part is still a human responsibility.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The work is split deliberately.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;New pipelines that connect to external APIs, and anything touching secure data, are &lt;strong&gt;handled by engineers&lt;/strong&gt;. Non-engineers mostly work on &lt;strong&gt;modifications to features that already exist&lt;/strong&gt; (peeking at our business-side members' PRs makes it concrete pretty quickly). &lt;strong&gt;"Non-engineers can develop too"&lt;/strong&gt; means &lt;strong&gt;"the harness provides rails they can't derail from, so they can safely modify in maintenance mode"&lt;/strong&gt; — not "anyone can build anything from scratch."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. This level of automation works because it's an internal platform.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes, cortex's full-auto deploy works partly because Composable Architecture cleanly separates apps and infrastructure. But honestly, &lt;strong&gt;a big part of it is that this is an internal-only platform&lt;/strong&gt;. If something breaks, only employees are affected, and we can roll back fast. The same approach can't be applied directly to consumer products or systems where downtime is immediately critical (warehouse management, for example). We've started moves to close that gap on the consumer side too, but that's a separate post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Series Roadmap
&lt;/h2&gt;

&lt;p&gt;The series is planned as 6 parts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 1: Series Intro&lt;/strong&gt; (this post)&lt;br&gt;
   The big picture of what cortex is and why it works in "harness" form. The map to the rest of the series.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://dev.to/ryantsuji/the-heart-of-the-ai-harness-a-knowledge-graph-of-the-ai-by-the-ai-for-the-ai-series-part-2-53bm"&gt;Part 2: Product Graph — code, docs, DB, infrastructure as one unified graph&lt;/a&gt;&lt;/strong&gt; ★ recommended next&lt;br&gt;
   The implementation side: how the unified graph is built and maintained. What happens when you take the design principles from &lt;a href="https://dev.to/ryantsuji/graph-rag-isnt-a-one-shot-anymore-the-case-for-agentic-graph-rag-mcps-1dj5"&gt;the Agentic Graph RAG MCP post&lt;/a&gt; and apply them to the entire cortex codebase.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 3: AI reviews, fixes, merges, and deploys PRs&lt;/strong&gt;&lt;br&gt;
   GitHub webhook → AI review → on REQUEST_CHANGES, AI fixes via worktree → auto squash merge → changed-stack detection → parallel deploy: the full pipeline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 4: Incidents self-heal, guardrails self-strengthen&lt;/strong&gt;&lt;br&gt;
   Grafana alert → AI investigation (Loki + Product Graph + git blame) → fix PR + new lint/type gate → auto merge → automatic redeploy: the auto self-healing system. Also covers the full OTel + Loki + Mimir + Tempo + Faro stack, Gemini cost tracking, and how the quality gates are designed to be "non-loweriable, non-bypassable, and self-growing."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 5: Scaling the harness from cortex to toC services&lt;/strong&gt;&lt;br&gt;
   The first half covers how business members can already open PRs directly to cortex -- and where that breaks (additions to existing pipelines work; new pipelines and architectural changes still need humans in the loop). The second half is the roadmap and the thinking behind scaling cortex's harness across the whole product org (multiple services, multiple infra stacks, multiple teams).&lt;/p&gt;

&lt;p&gt;Each post stands on its own, but &lt;strong&gt;Part 2 (Product Graph) is the foundation for the others&lt;/strong&gt;, so the recommended reading order is Part 1 → Part 2 → any.&lt;/p&gt;

&lt;p&gt;Cadence: Tuesdays or Thursdays, 8–10 AM JST.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;Building cortex, what's struck me is that &lt;strong&gt;in an AI-era dev environment, "absorbing everything that comes after the writing" wins over "reducing the burden on the writer"&lt;/strong&gt;. Tests, lint, types, coverage, code review, incident response — instead of "these get in the way, let's reduce them," the choice that worked was "&lt;strong&gt;have the AI do all of them, without compromise&lt;/strong&gt;." The counterintuitive result is that quality and dev speed both go up at the same time.&lt;/p&gt;

&lt;p&gt;And it expands two things — &lt;strong&gt;how much one engineer can ship&lt;/strong&gt;, and &lt;strong&gt;how much non-engineers can participate&lt;/strong&gt; — well beyond what was possible before. That's the texture of the "harness" we've built on top of cortex.&lt;/p&gt;

&lt;p&gt;In subsequent parts, I'll walk through the individual mechanisms that make this work.&lt;/p&gt;

&lt;p&gt;→ Part 2: &lt;a href="https://dev.to/ryantsuji/the-heart-of-the-ai-harness-a-knowledge-graph-of-the-ai-by-the-ai-for-the-ai-series-part-2-53bm"&gt;Product Graph — code, docs, DB, infrastructure as one unified graph&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>devops</category>
      <category>graphrag</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
