<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Daniel Westgaard</title>
    <description>The latest articles on DEV Community by Daniel Westgaard (@danielwe).</description>
    <link>https://dev.to/danielwe</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%2F3876610%2F1d0996da-2e1c-4cac-979f-f2a9d33d8b15.jpg</url>
      <title>DEV Community: Daniel Westgaard</title>
      <link>https://dev.to/danielwe</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/danielwe"/>
    <language>en</language>
    <item>
      <title>Is GitLab Orbit a developer portal? (Orbit vs the IDPs)</title>
      <dc:creator>Daniel Westgaard</dc:creator>
      <pubDate>Sun, 19 Jul 2026 12:56:53 +0000</pubDate>
      <link>https://dev.to/danielwe/is-gitlab-orbit-a-developer-portal-orbit-vs-the-idps-255n</link>
      <guid>https://dev.to/danielwe/is-gitlab-orbit-a-developer-portal-orbit-vs-the-idps-255n</guid>
      <description>&lt;p&gt;On 31 March 2026, Zohar Einy, the CEO of Port, published a post with a headline that did the rounds all spring. "Backstage is dead. Not because it was bad. But because the world moved on," &lt;a href="https://newsletter.port.io/p/backstage-is-dead" rel="noopener noreferrer"&gt;he wrote&lt;/a&gt;. His sharper line was the one platform engineers kept re-sharing: "The UI isn't where the value is anymore."&lt;/p&gt;

&lt;p&gt;Ten weeks later GitLab shipped Orbit, its Knowledge Graph, and one of the first screenshots on the &lt;a href="https://about.gitlab.com/gitlab-orbit/" rel="noopener noreferrer"&gt;marketing page&lt;/a&gt; is a blast-radius graph. "The context layer for AI agents," the tagline says. Not a portal. Not a catalog. A graph you query.&lt;/p&gt;

&lt;p&gt;So the discourse has a new shape. Backstage is dead, or it still runs most of the internal-developer-portal market, depending on whose post you read that week. Port is the successor, or Orbit is, or the developer portal itself is finished and the graph won.&lt;/p&gt;

&lt;p&gt;Here is the claim I want to plant before any of that. GitLab Orbit is not a developer portal, and it is not trying to be one. It ships no service catalog, no scorecards, no ownership pages, and its own documentation never once uses the word "portal." But the more useful point is that the question "is Orbit a portal, and does it replace Backstage" is aimed at the wrong axis. Whatever you call the thing that claims to know all your services, a catalog, a knowledge graph, a portal, none of Backstage, Port, or Orbit parses the artifact layer. So none of them can tell you which repositories a base-image bump or a Terraform-module change is about to break across a mixed GitHub-and-GitLab estate. That is the question that survives the catalog wars, and right now it has no winner.&lt;/p&gt;

&lt;h2&gt;
  
  
  The question everyone is asking is the wrong one
&lt;/h2&gt;

&lt;p&gt;The trouble with "does Orbit replace Backstage" is that it treats three different things as if they were one product with one job. They all wear the same loose label, the thing that knows about all your services, and that label hides more than it reveals.&lt;/p&gt;

&lt;p&gt;A developer portal is a human-facing catalog with a UI on top: services, owners, docs, APIs, scorecards, golden paths. A knowledge graph like Orbit is a machine-facing index of code symbols and lifecycle metadata, built to answer queries, mostly from agents. An artifact graph is the resolved map of which repository's Dockerfile, Terraform module, or Helm chart feeds which other repository. Three layers, three jobs, three buyers. The catalog wars are being fought between the first two. The third has no combatants at all, which is the part I keep coming back to.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a developer portal actually is
&lt;/h2&gt;

&lt;p&gt;The two products people mean when they say "portal" are Backstage and Port, and at heart each is a registered catalog: a human writes down what exists, and the tool renders it, governs it, and puts a search box in front. That is genuinely useful. The line I want to draw is about how they know what they know, not whether they are worth running.&lt;/p&gt;

&lt;h3&gt;
  
  
  Backstage: the catalog is real, and it is hand-authored
&lt;/h3&gt;

&lt;p&gt;Backstage titles &lt;a href="https://backstage.io/" rel="noopener noreferrer"&gt;its own homepage&lt;/a&gt; "Backstage Software Catalog and Developer Platform," and the catalog is the heart of it: one place for every service, its owner, its docs, its APIs, its scorecards, wired to templated golden paths for spinning up new components. The adoption is not in doubt. Its own &lt;a href="https://backstage.io/blog/2025/12/30/backstage-wrapped-2025/" rel="noopener noreferrer"&gt;"Wrapped 2025" post&lt;/a&gt; counts 3.4k+ adopters and 31k stars as of 30 December 2025. If you want a portal, this is the centre of gravity, and I have &lt;a href="https://riftmap.dev/blog/is-backstage-worth-it/" rel="noopener noreferrer"&gt;a lot of respect for what it does&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The mechanism is the thing to be precise about. Entities enter the catalog as human-authored YAML. Backstage's &lt;a href="https://backstage.io/docs/features/software-catalog/" rel="noopener noreferrer"&gt;own docs&lt;/a&gt; say it plainly: "Teams owning the components are responsible for maintaining the metadata about them, and do so using their normal Git workflow." A component is a &lt;code&gt;catalog-info.yaml&lt;/code&gt; file, committed to a repo, maintained by hand. Discovery processors do crawl your org and register the files they find, but the GitHub provider is pointed at &lt;code&gt;catalogPath: '/catalog-info.yaml'&lt;/code&gt; and finds files that already exist. It does not write them, and it does not infer entities from your code. This is the &lt;a href="https://riftmap.dev/blog/the-catalog-maintenance-trap/" rel="noopener noreferrer"&gt;catalog-maintenance trap&lt;/a&gt;: the YAML is accurate only as of the last person who edited it.&lt;/p&gt;

&lt;p&gt;Backstage is chasing the same wave as everyone else, and fairly. It has shipped MCP server support, and its Wrapped post frames the catalog as infrastructure that now serves "both your human developers and their AI helpers." Concede that plainly. But it is Backstage exposing its already-registered catalog to agents over MCP, not parsing the catalog into existence from your code. The entry mechanism is unchanged.&lt;/p&gt;

&lt;h3&gt;
  
  
  Port: modelled, not parsed, with an AI assist on population
&lt;/h3&gt;

&lt;p&gt;Port self-describes as "the internal developer portal," and its blueprints are the honest expression of its architecture. Port's &lt;a href="https://www.port.io/guide/blueprints" rel="noopener noreferrer"&gt;own guide&lt;/a&gt;: "Blueprints are the foundational building blocks of the internal developer portal. They hold the schema of the entities you wish to represent in the software catalog," and they are "completely customizable." That flexibility, a schema you define yourself rather than one you inherit, is Port's real advantage. You bring your own data model. This is well-built, well-resourced software.&lt;/p&gt;

&lt;p&gt;And Port has moved on drift. Its &lt;a href="https://www.port.io/platform/software-catalog" rel="noopener noreferrer"&gt;software-catalog page&lt;/a&gt; advertises AI that auto-relates catalog components, mapping the connections between components you have already discovered, on top of auto-discovery across a hundred-plus built-in integrations. That is a genuine mitigant, and it would be wrong to say Port's catalog "just drifts" as if nothing had changed. The honest line is narrower. The schema, the blueprint, is still modelled by a human. The AI relates and populates over data Port already holds through its integrations, it does not parse your repositories for structure nobody declared. It is a maintenance aid on a registered architecture. These are still &lt;a href="https://riftmap.dev/blog/modeled-graphs-and-parsed-graphs/" rel="noopener noreferrer"&gt;modelled graphs, not parsed graphs&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Orbit actually is, and why "portal" gets it backwards
&lt;/h2&gt;

&lt;p&gt;Orbit is not a catalog you fill in. It is a graph GitLab builds by parsing your code and lifecycle data, then serves to machines. That is the architectural inverse of a portal, and it is why the question gets it backwards.&lt;/p&gt;

&lt;p&gt;Give Orbit its due, because the scale is real. GitLab's &lt;a href="https://about.gitlab.com/blog/introducing-gitlab-orbit/" rel="noopener noreferrer"&gt;launch blog&lt;/a&gt; reports it indexed over 40,000 projects, 500 million nodes, and 2 billion edges in under 45 minutes. It is a single Rust binary backed by ClickHouse or DuckDB, driven by a YAML ontology, and it "serves the combined graph over a Cypher-like DSL, MCP, REST, and the GitLab CLI." It parses somewhere between twelve and fourteen programming languages, depending which GitLab source you read: the launch blog lists twelve, the &lt;a href="https://github.com/gitlabhq/orbit-knowledge-graph" rel="noopener noreferrer"&gt;GitHub README&lt;/a&gt; says "11+" and names fourteen. On top of that symbol-level code graph sits a layer of SDLC metadata: merge requests, pipelines, security findings, ownership. For anyone who thinks the graph is the substrate agents and humans both call into, Orbit is genuinely impressive and genuinely validating.&lt;/p&gt;

&lt;p&gt;Now the shape of it. A portal is registered: a human writes the catalog, the tool renders it. Orbit is the opposite motion, GitLab parsing the graph out of what your repositories already contain and handing it to a machine to query. There is no catalog surface. The &lt;a href="https://docs.gitlab.com/orbit/" rel="noopener noreferrer"&gt;docs&lt;/a&gt; never use the words "catalog" or "scorecard," describe Orbit as "an analytical system designed for point-in-time SDLC insight," and no Orbit source, blog, docs, or README, uses the word "portal" at all. It is the &lt;a href="https://riftmap.dev/blog/gitlab-orbit-and-the-artifact-layer/" rel="noopener noreferrer"&gt;context layer beneath the SDLC&lt;/a&gt;, not a front door on top.&lt;/p&gt;

&lt;p&gt;Two honest bounds. First, reach: Orbit Remote is GitLab.com (SaaS) only, not self-managed GitLab, on Premium or Ultimate, behind the &lt;code&gt;knowledge_graph&lt;/code&gt; feature flag, disabled by default. GitLab moved it from experiment to beta in 19.1, but beta here is not general availability: it is still behind that flag, still off by default, and its own docs still call it "available for testing, but not ready for production use." That is narrower than "GitLab-only."&lt;/p&gt;

&lt;p&gt;Second, layer: Orbit's marketing reaches for the words "blast radius," and for symbol-level and merge-request-level questions it delivers on them. But its graph is code symbols plus lifecycle metadata. It does not resolve artifact provenance. It does not know that this base image is built by repo X, that this Terraform module resolves to repo Y, that this Helm value feeds a downstream chart. That is Orbit Remote, the hosted service. Orbit also ships Orbit Local, a single-binary CLI that runs entirely on your machine, needs no GitLab instance or network connection, and parses one local repository into a DuckDB, code only, no SDLC data. Local is an even purer symbol graph, one repo on your laptop with no cross-org reach and no artifact provenance, so both shapes sit below the artifact layer. &lt;a href="https://riftmap.dev/blog/symbol-graphs-and-artifact-graphs/" rel="noopener noreferrer"&gt;Symbol graphs and artifact graphs are different categories&lt;/a&gt;, and even the 800-pound gorilla is sitting on the symbol layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Registered, parsed, and the axis nobody is on
&lt;/h2&gt;

&lt;p&gt;There is a clean way to name the difference between a portal and Orbit, and it is &lt;a href="https://riftmap.dev/blog/declared-inferred-registered/" rel="noopener noreferrer"&gt;registered versus parsed&lt;/a&gt;. It is accurate. It is also a narrowing gap, not the durable one.&lt;/p&gt;

&lt;p&gt;Backstage and Port are registered. An entity exists to the tool because a human wrote it down, and it stays true only for as long as humans keep editing it. Orbit is parsed. It reads code and metadata GitLab already holds, so nobody hand-maintains it and it does not drift the way a catalog does. That explains a lot: why the portal goes stale and the graph stays current, why one costs you FTEs and the other costs you infrastructure.&lt;/p&gt;

&lt;p&gt;But watch what all three are doing. Backstage is wrapping its registered catalog in MCP for agents. Port is using AI to relate the entities it already holds. Orbit was born parsed. All three are converging on the same destination, the structured graph an agent queries, so if you build your whole argument on registered-versus-parsed you are building it on the one seam every vendor is actively closing. The durable line runs on a different axis. None of the three parses the artifact layer, and no amount of catalog AI changes that, because the artifact layer was never in any of their catalogs to begin with.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the breaking edge actually lives
&lt;/h2&gt;

&lt;p&gt;The cross-repo edge that breaks when you bump a base image or a shared module is a boring, declared thing, and it is the one I actually get paged about. A Dockerfile &lt;code&gt;FROM&lt;/code&gt; line pinning a downstream service to your base image. A Terraform &lt;code&gt;source&lt;/code&gt; block resolving to the module repo. A Helm value pointing at an image, a GitLab CI &lt;code&gt;include&lt;/code&gt;, a GitHub Actions &lt;code&gt;uses:&lt;/code&gt;, a &lt;code&gt;go.mod&lt;/code&gt; require. It was written down, in a manifest, in a repo. Just not the repo you happen to have open. Backstage knows about that edge only if a human typed it into a &lt;code&gt;catalog-info.yaml&lt;/code&gt;. Port knows it only if you modelled a blueprint and something populated it. Orbit sees code symbols and SDLC metadata on GitLab.com, which is a different layer. The base-image bump, the module change, the Helm value that feeds a downstream chart are invisible to all three. And it is usually the dependency nobody remembers anyway, the one that &lt;a href="https://riftmap.dev/what-is-an-artifact-dependency-graph/" rel="noopener noreferrer"&gt;lived only in the head of the engineer who left&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;That layer is what Riftmap parses. It installs across a whole GitHub or GitLab organisation from one read-only token, with no per-repo YAML to maintain, because Riftmap only ever reads. It reads the edges your manifests declare, deterministically: Dockerfile &lt;code&gt;FROM&lt;/code&gt;, Terraform &lt;code&gt;source&lt;/code&gt;, Helm value references (the &lt;code&gt;repository&lt;/code&gt; under an &lt;code&gt;image:&lt;/code&gt; block in &lt;code&gt;values.yaml&lt;/code&gt;), GitLab CI &lt;code&gt;include&lt;/code&gt;, GitHub Actions &lt;code&gt;uses:&lt;/code&gt;, &lt;code&gt;go.mod&lt;/code&gt; requires. It resolves each one to the repo on the other side. Riftmap connects GitHub orgs, GitLab orgs, or both in one workspace.&lt;/p&gt;

&lt;p&gt;Here is the shape of the edge that sits underneath all three. I scanned the cloudposse organisation on 18 July 2026, 244 repositories in all, and one Terraform module stood out. &lt;code&gt;cloudposse/terraform-null-label&lt;/code&gt; is pulled in by 149 of those repos; the next most-consumed module in the org has 15. Every one of those 149 edges was read from a Terraform &lt;code&gt;source&lt;/code&gt; attribute in a &lt;code&gt;module&lt;/code&gt; block, the string &lt;code&gt;cloudposse/label/null&lt;/code&gt;, resolved through HashiCorp's registry naming convention to the repo that actually publishes it, and pinned to a version. 141 of the 149 point at the current release, &lt;code&gt;0.25.0&lt;/code&gt;. The other eight are stuck on older tags, silently, with nothing anywhere recording that they lag.&lt;/p&gt;

&lt;p&gt;Riftmap scores that edge at 0.9, not a flat 1.0, because the last hop is a convention, HashiCorp's own &lt;code&gt;terraform-&amp;lt;provider&amp;gt;-&amp;lt;name&amp;gt;&lt;/code&gt; rule, rather than the repo sitting literally in the string. I would rather show you the honest 0.9. It is still a deterministic, rule-based resolution, not a guess. And it is a resolved cross-repo module edge: this module lives in that repo, at that version, consumed here. That is exactly the artifact-provenance edge a symbol graph has no node for, and an SDLC metadata layer does not model. It is also not written down in any catalog.&lt;/p&gt;

&lt;p&gt;It is not only Terraform. In a &lt;a href="https://riftmap.dev/blog/cross-repo-blast-radius-9-orgs/" rel="noopener noreferrer"&gt;scan of nine public GitHub orgs&lt;/a&gt; I wrote up earlier, the most load-bearing repository in Grafana's organisation turned out to be a repo of CI workflow definitions, imported by 188 others, an edge Riftmap resolves from a GitHub Actions &lt;code&gt;uses:&lt;/code&gt; string with the org and repo written into it. Across those nine orgs, &lt;a href="https://riftmap.dev/blog/cross-repo-edge-composition/" rel="noopener noreferrer"&gt;infrastructure ran from roughly a third of the cross-repo coupling in the most code-heavy org to nearly all of it in the most infrastructure-heavy one&lt;/a&gt;. None of that is written down in a catalog for a human to browse.&lt;/p&gt;

&lt;h2&gt;
  
  
  So which do you actually need?
&lt;/h2&gt;

&lt;p&gt;These three are not really competitors. They are three layers, and which one you need depends on the question you are trying to answer. They stack. They do not cancel.&lt;/p&gt;

&lt;h3&gt;
  
  
  A catalog and a UI for humans: a portal
&lt;/h3&gt;

&lt;p&gt;If what you want is a browsable home for your services, with owners, docs, scorecards, and golden paths, you want a developer portal. Backstage if you want the huge plugin ecosystem and you are willing to run it and write some React. Port if you want a managed product, your own data model, and AI to help keep the catalog connected, and you are willing to pay for it. Both are the right tool for "give humans one place to find and govern services," and I have written up &lt;a href="https://riftmap.dev/blog/backstage-alternatives/" rel="noopener noreferrer"&gt;the wider field of Backstage alternatives&lt;/a&gt; if you are choosing.&lt;/p&gt;

&lt;h3&gt;
  
  
  A code-and-SDLC graph for agents in a GitLab shop: Orbit
&lt;/h3&gt;

&lt;p&gt;If your estate lives on GitLab.com and your priority is grounding AI agents in code symbols and merge-request history, Orbit is the strongest thing shipping in that lane, with the caveats stated plainly: GitLab.com only, Premium or Ultimate, feature-flagged, and not production-ready as of this writing. It is the graph, not the front door.&lt;/p&gt;

&lt;h3&gt;
  
  
  What an infra change breaks across GitHub and GitLab: an artifact graph
&lt;/h3&gt;

&lt;p&gt;If the question that keeps you up before a base-image bump is which repositories break, none of the above answers it, and you want an artifact graph: a graph that parses those manifests and resolves the cross-repo edges to the artifact on the other side. That is the graph Riftmap builds, across a GitHub org, a GitLab org, or both in one workspace, and you can watch it run against a real organisation at &lt;a href="https://riftmap.dev" rel="noopener noreferrer"&gt;riftmap.dev&lt;/a&gt;. A portal for humans, a knowledge graph for agents, an artifact graph for blast radius. Different questions, different tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  The catalog wars are a fight over the UI
&lt;/h2&gt;

&lt;p&gt;The whole "is the portal dead" argument is a fight about the data model and the interface. Registered versus modelled. Human-facing versus agent-facing. A schema you inherit versus one you define. Real stakes, and Backstage, Port, and Orbit are all in it, swinging.&lt;/p&gt;

&lt;p&gt;But the question I opened with sits one layer underneath all of it. Which repositories does this change break. It does not live in a catalog, a blueprint, or a symbol graph. It lives in the &lt;code&gt;FROM&lt;/code&gt; lines and the &lt;code&gt;source&lt;/code&gt; blocks and the &lt;code&gt;include&lt;/code&gt; directives your pipelines already run, and only a graph that parses those manifests can answer it. The catalog wars will crown a winner. The artifact layer is a different fight, on a different axis, and so far only one kind of graph has shown up to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  A few questions, answered directly
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Is GitLab Orbit a developer portal?&lt;/strong&gt; No. Orbit ships no service catalog, no scorecards, and no ownership UI, and none of its own documentation uses the word "portal." It is a knowledge graph of code symbols and SDLC metadata that GitLab parses from your repositories and serves to AI agents over MCP, REST, and the GitLab CLI. A portal is a registered catalog a human maintains. Orbit is the architectural opposite of that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is the difference between GitLab Orbit and Backstage?&lt;/strong&gt; Backstage is a registered catalog: humans author &lt;code&gt;catalog-info.yaml&lt;/code&gt; files and Backstage renders and governs them behind a UI. Orbit is a parsed graph: GitLab builds it from your code and lifecycle data with no catalog to maintain, and exposes it for querying rather than browsing. Backstage is a human-facing portal; Orbit is a machine-facing graph. They sit at different layers, and as of mid-2026 Backstage runs anywhere while Orbit Remote is GitLab.com-only (Orbit Local is a code-only CLI for a single local repository).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can GitLab Orbit replace Backstage or Port?&lt;/strong&gt; Not really, because they do different jobs. Orbit has no catalog, scorecards, or golden-path UI, so it does not give humans the browsable service directory a portal is for. Backstage and Port do not parse a code-and-SDLC graph for agents the way Orbit does. If you need a portal you still need a portal; Orbit is a graph you would query alongside one, not instead of it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do any of these show infrastructure blast radius across repos?&lt;/strong&gt; No. Backstage, Port, and Orbit do not parse the artifact layer, so none of them resolves which repositories a base-image, Terraform-module, or Helm change will break across your estate. Backstage knows an edge only if a human catalogued it, Port only if you modelled it, and Orbit sees code symbols and SDLC metadata on GitLab.com only. Answering that question needs a tool that parses Dockerfile &lt;code&gt;FROM&lt;/code&gt;, Terraform &lt;code&gt;source&lt;/code&gt;, GitLab CI &lt;code&gt;include&lt;/code&gt;, and GitHub Actions &lt;code&gt;uses:&lt;/code&gt; edges and resolves them to the repo on the other side, which is what an artifact graph like Riftmap does.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;About Riftmap&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Riftmap maps cross-repo dependencies across your entire GitLab or GitHub&lt;br&gt;
organisation — Terraform, Docker, CI templates, Helm, and more. One read-only&lt;br&gt;
token. No YAML to maintain.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>gitlaborbit</category>
      <category>developerportals</category>
      <category>backstage</category>
      <category>blastradius</category>
    </item>
    <item>
      <title>Cross-repo blast radius in nine public platform orgs: the most depended-on repo at Grafana isn't Grafana</title>
      <dc:creator>Daniel Westgaard</dc:creator>
      <pubDate>Tue, 14 Jul 2026 17:56:08 +0000</pubDate>
      <link>https://dev.to/danielwe/cross-repo-blast-radius-in-nine-public-platform-orgs-the-most-depended-on-repo-at-grafana-isnt-326e</link>
      <guid>https://dev.to/danielwe/cross-repo-blast-radius-in-nine-public-platform-orgs-the-most-depended-on-repo-at-grafana-isnt-326e</guid>
      <description>&lt;p&gt;Spend a week on call for a platform team and you will meet the same outage twice. Someone bumps a base image, retags a shared Terraform module, edits a CI template that forty pipelines include. The PR is green. The repo it lives in is fine. Something two repositories away falls over an hour later, and the person who shipped the change had no way to know that repository existed, because nothing in the repo they were editing points outward to the things that depend on it.&lt;/p&gt;

&lt;p&gt;I wanted to know how common that shape really is, so I stopped guessing and measured it. Across nine public platform and infrastructure organisations and 1,297 active repositories, roughly one in four repos is a dependency of at least one sibling repo in the same organisation, and that edge is invisible from inside the repo you are changing.&lt;/p&gt;

&lt;p&gt;I should say the obvious thing first. I build &lt;a href="https://riftmap.dev" rel="noopener noreferrer"&gt;Riftmap&lt;/a&gt;, and Riftmap is the scanner I used. That is a conflict of interest, so this post is written so you can check me rather than trust me. The sample rule is stated before the results. The confidence model is stated, including what it throws away and why. The two false positives my own tool produced are in here, named, with the mechanism. The one organisation that did not fit the finding is in here too, along with the spot-check I ran to make sure the result was not a bug in my own parser. &lt;a href="https://riftmap.dev/data/9org-scan-2026-07-14-edges.csv" rel="noopener noreferrer"&gt;The raw edge list is published&lt;/a&gt;. If any of it is wrong, it is wrong in public.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I scanned, and what counts as an edge
&lt;/h2&gt;

&lt;p&gt;I scanned nine public GitHub organisations in the platform and cloud-native space and parsed the dependency edges their manifests actually declare. The sample: cloudposse, prometheus, open-telemetry, fluxcd, argoproj, ansible-collections, crossplane-contrib, terraform-aws-modules, and grafana.&lt;/p&gt;

&lt;p&gt;The rule, fixed before I looked at any result: public organisations in the platform, infrastructure, or cloud-native space, chosen to span three shapes rather than one. Module farms that exist to publish reusable components (cloudposse, ansible-collections, terraform-aws-modules, crossplane-contrib). Frameworks (fluxcd, open-telemetry). Product organisations that ship a thing (prometheus, argoproj, grafana). They range from 28 to 583 active repositories. I picked them for ecosystem and shape spread, not because I knew what they would look like.&lt;/p&gt;

&lt;p&gt;The important word is &lt;em&gt;parsed&lt;/em&gt;, not inferred. For every repo I read the edges declared in real manifest constructs. Terraform &lt;code&gt;source&lt;/code&gt; blocks, including registry coordinates like &lt;code&gt;terraform-aws-modules/vpc/aws&lt;/code&gt;. &lt;code&gt;go.mod&lt;/code&gt; requires and replaces. Helm &lt;code&gt;Chart.yaml&lt;/code&gt; dependencies. Dockerfile &lt;code&gt;FROM&lt;/code&gt; lines. &lt;code&gt;kustomization.yaml&lt;/code&gt; resources. GitHub Actions &lt;code&gt;uses:&lt;/code&gt; references. npm package dependencies. Each reference is then resolved to the repo in the same organisation that actually produces it, so a Terraform &lt;code&gt;source&lt;/code&gt; or a &lt;code&gt;FROM&lt;/code&gt; becomes a real repo-to-repo edge rather than a string. This is not name-matching and it is &lt;a href="https://riftmap.dev/blog/inferred-context-is-not-a-dependency-graph/" rel="noopener noreferrer"&gt;not embeddings&lt;/a&gt;. It is the graph the repositories already describe about themselves.&lt;/p&gt;

&lt;p&gt;Every edge carries a confidence score, and the score is a statement about how much resolution the edge needed, not a probability that a model assigned to it. A GitHub Actions &lt;code&gt;uses: grafana/shared-workflows/actions/...&lt;/code&gt; reference has the organisation and the repository sitting in the string. There is nothing to resolve, so there is nothing to get wrong. A package coordinate is one step removed. A &lt;code&gt;go.mod&lt;/code&gt; require, an npm package name, a Terraform registry source all have to be resolved to the repository that publishes them, which is deterministic but not free. A bare git URL sitting in a shell script or a README is a text signal rather than a declared dependency, so it scores 0.6 and never gets counted. Open-telemetry is the clearest illustration of what that bar throws away: 413 of its candidate edges were dropped, and 412 of them were bare git URL references.&lt;/p&gt;

&lt;p&gt;Across the nine organisations the confidence bar discarded 1,698 of 4,843 candidate edges, 35% of everything the scanner saw. Every number below is computed on what survived, at confidence 0.8 or above, with archived repositories excluded, deduplicated to distinct repo-to-repo pairs, and counting direct dependents only.&lt;/p&gt;

&lt;p&gt;One integrity check before the results, because it is cheap and it is the first thing I would ask. Every organisation's scanned repo count matched GitHub's live &lt;code&gt;public_repos&lt;/code&gt; count. Grafana came back 586 of 586 with zero errors, of which 583 are active once the archived ones come out. No self-loops, and no &lt;code&gt;workspace:&lt;/code&gt; or &lt;code&gt;file:&lt;/code&gt; local-protocol residue leaking in as fake edges.&lt;/p&gt;

&lt;h2&gt;
  
  
  The most depended-on repo at Grafana isn't Grafana
&lt;/h2&gt;

&lt;p&gt;Grafana is the largest organisation in the sample, 583 active repos. Here is its top ten by direct dependents, with the manifest construct each count came from, so you can see for yourself which rows have room to be wrong.&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;Repo&lt;/th&gt;
&lt;th&gt;Direct dependents&lt;/th&gt;
&lt;th&gt;What it is&lt;/th&gt;
&lt;th&gt;Edge basis&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;&lt;code&gt;grafana/shared-workflows&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;188&lt;/td&gt;
&lt;td&gt;CI workflow definitions&lt;/td&gt;
&lt;td&gt;Actions &lt;code&gt;uses:&lt;/code&gt;, org and repo in the string&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;code&gt;grafana/scenes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;143&lt;/td&gt;
&lt;td&gt;frontend framework&lt;/td&gt;
&lt;td&gt;npm &lt;code&gt;@grafana/scenes&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;code&gt;grafana/grafana&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;92&lt;/td&gt;
&lt;td&gt;the product&lt;/td&gt;
&lt;td&gt;npm &lt;code&gt;@grafana/*&lt;/code&gt; and &lt;code&gt;go.mod&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;code&gt;grafana/plugin-tools&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;83&lt;/td&gt;
&lt;td&gt;plugin scaffolding&lt;/td&gt;
&lt;td&gt;npm &lt;code&gt;@grafana/plugin-tools&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;&lt;code&gt;grafana/pyroscope-go&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;75&lt;/td&gt;
&lt;td&gt;profiling SDK&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;go.mod&lt;/code&gt; require&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;&lt;code&gt;grafana/otel-profiling-go&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;72&lt;/td&gt;
&lt;td&gt;profiling bridge&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;go.mod&lt;/code&gt; require&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;&lt;code&gt;grafana/plugin-ci-workflows&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;63&lt;/td&gt;
&lt;td&gt;CI workflow definitions&lt;/td&gt;
&lt;td&gt;Actions &lt;code&gt;uses:&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;&lt;code&gt;grafana/grafana-plugin-sdk-go&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;plugin SDK&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;go.mod&lt;/code&gt; require&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;&lt;code&gt;grafana/plugin-actions&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;53&lt;/td&gt;
&lt;td&gt;GitHub Actions&lt;/td&gt;
&lt;td&gt;Actions &lt;code&gt;uses:&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;&lt;code&gt;grafana/k6&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;39&lt;/td&gt;
&lt;td&gt;load testing&lt;/td&gt;
&lt;td&gt;first-party product&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The single most load-bearing repository in Grafana's organisation is a repo of CI workflow definitions, with 188 repos importing it. The flagship product is third, at 92. Three of the top ten are CI plumbing.&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%2Fvxvzfcxm976mqv8fiyhe.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%2Fvxvzfcxm976mqv8fiyhe.png" alt="grafana/shared-workflows focused in Riftmap's dependency graph: 188 repositories fan inward to a single CI-workflow repo, and not one of them appears anywhere inside it" width="800" height="935"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I want to be precise about why the top row is worth trusting more than the rest of the post. &lt;code&gt;shared-workflows&lt;/code&gt; is consumed through &lt;code&gt;uses: grafana/shared-workflows/actions/...&lt;/code&gt;. The organisation and the repository are literally in the string. There is no name resolution to get wrong, no upstream package to collide with, no heuristic anywhere in the path. It is the least ambiguous edge type there is, and it produced the largest hub in the sample.&lt;/p&gt;

&lt;p&gt;Those ten repos also absorb 868 of the 1,596 cross-repo dependency edges inside Grafana's organisation, 54% of the coupling in an org of 583 repositories. The load is not spread evenly. It never is.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hub is never the repo you would nominate
&lt;/h2&gt;

&lt;p&gt;Ask a platform engineer which repo in their organisation is the scariest to change and they will name a service. Something with traffic. Something with a runbook.&lt;/p&gt;

&lt;p&gt;In eight of the nine organisations I scanned, the most-depended-on repo is plumbing. &lt;code&gt;grafana/shared-workflows&lt;/code&gt;, CI definitions, 188 dependents. &lt;code&gt;cloudposse/terraform-null-label&lt;/code&gt;, a module whose entire job is generating consistent names and tags, 146. &lt;code&gt;ansible-collections/ansible.netcommon&lt;/code&gt;, network utilities, 27. &lt;code&gt;crossplane-contrib/provider-workflows&lt;/code&gt;, CI definitions again, 23. &lt;code&gt;prometheus/common&lt;/code&gt; and &lt;code&gt;prometheus/client_model&lt;/code&gt;, shared Go libraries and protobuf definitions, tied at 23, with &lt;code&gt;procfs&lt;/code&gt; one behind them. &lt;code&gt;fluxcd/pkg&lt;/code&gt;, a shared Go package, 16. &lt;code&gt;open-telemetry/shared-workflows&lt;/code&gt;, CI definitions, 15. &lt;code&gt;argoproj/pkg&lt;/code&gt;, a shared Go package, 4.&lt;/p&gt;

&lt;p&gt;None of these are services. They are the naming module, the base image, the internal action, the shared utility package. They are also, and this is the part that matters operationally, exactly the repositories that nobody remembers owning. The blast radius concentrates in the things that were set up once, years ago, by an engineer who has since moved teams or left the company. The dependency that used to live only in someone's head is the same dependency that turns out to have 188 repos riding on it.&lt;/p&gt;

&lt;h3&gt;
  
  
  The ninth organisation, and the check I ran on my own parser
&lt;/h3&gt;

&lt;p&gt;Terraform-aws-modules is the one organisation in the sample with no hub at all. Its busiest repos top out at two direct dependents apiece — a three-way tie between &lt;code&gt;terraform-aws-lambda&lt;/code&gt;, &lt;code&gt;terraform-aws-acm&lt;/code&gt; and &lt;code&gt;terraform-aws-kms&lt;/code&gt;, which is another way of saying there is no hub. The entire organisation contains eleven same-org edges.&lt;/p&gt;

&lt;p&gt;That is precisely the result you should be suspicious of, because it is also what a broken parser looks like. Modules in that org reference each other through registry coordinates, &lt;code&gt;source = "terraform-aws-modules/alb/aws"&lt;/code&gt; rather than a git URL, and if my resolver quietly failed to follow registry coordinates back to the repository that publishes them, I would produce a near-empty graph and then write a confident paragraph explaining what it meant.&lt;/p&gt;

&lt;p&gt;So I checked it against ground truth before I believed it. &lt;code&gt;terraform-aws-atlantis/main.tf&lt;/code&gt; calls four sibling modules by registry source: &lt;code&gt;terraform-aws-modules/alb/aws&lt;/code&gt; at 10.2.0, &lt;code&gt;acm/aws&lt;/code&gt; at 6.1.1, &lt;code&gt;ecs/aws&lt;/code&gt; at 6.7.0, and &lt;code&gt;efs/aws&lt;/code&gt; at 2.0.0. Riftmap's graph contains all four, with the versions matching. The resolver follows registry coordinates. The sparsity is real.&lt;/p&gt;

&lt;p&gt;The reason for it is dull and true. Most of the modules in that organisation are leaves. &lt;code&gt;vpc&lt;/code&gt;, &lt;code&gt;s3&lt;/code&gt; and &lt;code&gt;iam&lt;/code&gt; do not compose siblings, they compose AWS resources. Only the composers do, &lt;code&gt;atlantis&lt;/code&gt;, &lt;code&gt;eks&lt;/code&gt;, &lt;code&gt;route53&lt;/code&gt;, &lt;code&gt;notify-slack&lt;/code&gt;, and there are not many of them. So terraform-aws-modules is neither support nor counterexample for the hub finding. It is an organisation with no hub to find. I am telling you that rather than dropping it, because dropping the org that does not fit is how you end up with a finding that is not true.&lt;/p&gt;

&lt;h2&gt;
  
  
  One in four repos is load-bearing, and that is a floor
&lt;/h2&gt;

&lt;p&gt;Across the sample, 320 of 1,297 active repositories are a dependency of at least one sibling in the same organisation. That is 24.7%, roughly one in four.&lt;/p&gt;

&lt;p&gt;It holds three ways. Pooled across all repos it is 24.7%. The median organisation is 25.3%. The mean organisation is 24.8%. Drop the two largest organisations, grafana and cloudposse, which together are 64% of the sample, and the remaining seven still come to 20.5%. Grafana, the largest org in the sample, sits at 27.8%, above pooled, so this is not an artefact of small organisations either.&lt;/p&gt;

&lt;p&gt;Here is every organisation, so you can do your own arithmetic on it.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Organisation&lt;/th&gt;
&lt;th&gt;Shape&lt;/th&gt;
&lt;th&gt;Active repos&lt;/th&gt;
&lt;th&gt;Load-bearing&lt;/th&gt;
&lt;th&gt;%&lt;/th&gt;
&lt;th&gt;Busiest repo&lt;/th&gt;
&lt;th&gt;Direct dependents&lt;/th&gt;
&lt;th&gt;Largest SCC&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;grafana&lt;/td&gt;
&lt;td&gt;product&lt;/td&gt;
&lt;td&gt;583&lt;/td&gt;
&lt;td&gt;162&lt;/td&gt;
&lt;td&gt;27.8%&lt;/td&gt;
&lt;td&gt;&lt;code&gt;shared-workflows&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;188&lt;/td&gt;
&lt;td&gt;53&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cloudposse&lt;/td&gt;
&lt;td&gt;module farm&lt;/td&gt;
&lt;td&gt;241&lt;/td&gt;
&lt;td&gt;61&lt;/td&gt;
&lt;td&gt;25.3%&lt;/td&gt;
&lt;td&gt;&lt;code&gt;terraform-null-label&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;146&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ansible-collections&lt;/td&gt;
&lt;td&gt;module farm&lt;/td&gt;
&lt;td&gt;137&lt;/td&gt;
&lt;td&gt;26&lt;/td&gt;
&lt;td&gt;19.0%&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ansible.netcommon&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;27&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;crossplane-contrib&lt;/td&gt;
&lt;td&gt;contrib farm&lt;/td&gt;
&lt;td&gt;86&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;4.7%&lt;/td&gt;
&lt;td&gt;&lt;code&gt;provider-workflows&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;23&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;open-telemetry&lt;/td&gt;
&lt;td&gt;framework&lt;/td&gt;
&lt;td&gt;80&lt;/td&gt;
&lt;td&gt;19&lt;/td&gt;
&lt;td&gt;23.8%&lt;/td&gt;
&lt;td&gt;&lt;code&gt;shared-workflows&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;terraform-aws-modules&lt;/td&gt;
&lt;td&gt;module farm&lt;/td&gt;
&lt;td&gt;59&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;13.6%&lt;/td&gt;
&lt;td&gt;&lt;code&gt;terraform-aws-lambda&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;prometheus&lt;/td&gt;
&lt;td&gt;product and libraries&lt;/td&gt;
&lt;td&gt;51&lt;/td&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;td&gt;33.3%&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;common&lt;/code&gt; / &lt;code&gt;client_model&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;23&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fluxcd&lt;/td&gt;
&lt;td&gt;framework&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;46.9%&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pkg&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;argoproj&lt;/td&gt;
&lt;td&gt;product&lt;/td&gt;
&lt;td&gt;28&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;28.6%&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pkg&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Pooled&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1,297&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;320&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;24.7%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;grafana/shared-workflows&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;188&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One thing not to do with that table: do not average the percentages by shape. Two of the four module farms carry known measurement gaps. Crossplane-contrib's real hub lives in a different organisation, and terraform-aws-modules has eleven same-org edges in total. A class mean computed on top of two structural undercounts is arithmetic dressed up as a finding, and I would have to take it back the first time somebody actually looked.&lt;/p&gt;

&lt;p&gt;What the number is not is stable. The per-org range runs from 4.7% to 46.9%, and anyone telling you a single percentage generalises to your estate is selling something. Treat 24.7% as the shape of the problem, not as an estimate of your number.&lt;/p&gt;

&lt;p&gt;Now the objection I would raise if I were reading this. You picked organisations that publish shared modules, so of course their repos depend on each other. Except it runs the other way. Flux (46.9%) and Prometheus (33.3%), neither of which exists to publish reusable modules for siblings, top the table. Cloudposse, 241 Terraform modules built for nothing &lt;em&gt;but&lt;/em&gt; reuse, sits at 25.3%, below both. The organisations built around sharing are not the ones that turned out to be most coupled.&lt;/p&gt;

&lt;p&gt;And every methodological choice I made pushes the number down, not up. Archived repositories are excluded. The confidence bar dropped 35% of candidate edges before anything was counted. Only direct dependents are counted, not transitive. Only declared manifest edges are counted, which means runtime coupling is completely invisible: shared databases, internal APIs, message topics, none of it appears. Only same-organisation edges are counted, which is why crossplane-contrib reads a suspiciously low 4.7%. Its real hub, &lt;code&gt;crossplane-runtime&lt;/code&gt;, lives in the &lt;code&gt;crossplane&lt;/code&gt; organisation rather than &lt;code&gt;crossplane-contrib&lt;/code&gt;, so a same-org scan structurally cannot see it. The organisational boundary is an accident of GitHub, and the dependency does not care.&lt;/p&gt;

&lt;p&gt;Every choice makes the number smaller. It is still one in four.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two false positives I had to throw out
&lt;/h2&gt;

&lt;p&gt;Two entries in my first Grafana top ten were wrong, and both were wrong the same way. &lt;strong&gt;A fork that preserves the upstream package identity will absorb every same-org import of the upstream.&lt;/strong&gt; That is the failure mode, stated up front, because it bites hardest on exactly the repos that look most important, and because if you are building this yourself you have the bug until you prove you do not.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;grafana/opentelemetry-go&lt;/code&gt; came out at number two with 182 dependents. It is Grafana's fork of upstream OpenTelemetry Go, and a fork keeps the upstream module path, &lt;code&gt;go.opentelemetry.io/otel&lt;/code&gt;. So when Grafana repos put a plain &lt;code&gt;go.mod&lt;/code&gt; require on upstream otel at v1.43 and v1.44, the resolver matched that module path to the repo in the organisation that declares it, which is the fork. The repo carried 184 inbound edges from 182 distinct repositories. Exactly one of those edges was a genuine &lt;code&gt;replace&lt;/code&gt; directive pointing at the fork. The other 183 were repos importing upstream and being told they depended on a sibling.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;grafana/k6-DefinitelyTyped&lt;/code&gt;, number four at 137, is the same bug in npm. It is a fork of DefinitelyTyped, so it declares &lt;code&gt;@types/lodash&lt;/code&gt;, &lt;code&gt;@types/react&lt;/code&gt;, &lt;code&gt;@types/jest&lt;/code&gt; and several hundred more. Every TypeScript repo at Grafana with &lt;code&gt;@types/*&lt;/code&gt; in its devDependencies got an edge to it. That is not a hub. That is a fork of the type registry collecting an edge from everything that has ever installed types.&lt;/p&gt;

&lt;p&gt;Both are resolver bugs in my own product and both are now filed.&lt;/p&gt;

&lt;p&gt;Neither one touched the headline. &lt;code&gt;shared-workflows&lt;/code&gt; is an Actions path, which is why I lead with it. Removing both leaves the pooled figure unchanged at 24.7%, takes Grafana's load-bearing count from 163 to 162, and moves &lt;code&gt;grafana/grafana&lt;/code&gt; from fifth to third, which is a better story than the one I started with. In the other eight organisations the busiest repo is a first-party project with no upstream twin to collide with, so the bug had nowhere to bite.&lt;/p&gt;

&lt;p&gt;I found this because I went looking for it. If I had not, the number two entry in my headline table would have been an artefact, and the first Grafana engineer to read the post would have said so.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I report direct dependents and not transitive
&lt;/h2&gt;

&lt;p&gt;The first version of this analysis reported transitive blast radius, and the numbers were nonsense in a way that took me a while to see. Ninety-one repos in Grafana's organisation, more than half of everything I had counted as load-bearing, each came back with roughly the same figure. About 315 transitive dependents. Ninety-one different repositories, one answer.&lt;/p&gt;

&lt;p&gt;That is not a distribution, it is a fixed point, and it has an obvious cause once you go looking for one. Grafana's Go repositories form a 53-repo strongly connected component. Alloy, Beyla, augurs, dataplane, Grafana core, the AWS and Azure SDK families, they all mutually import. Inside a strongly connected component every member reaches every other member, so reverse reachability collapses to roughly the same value for all of them, and a "transitive blast radius" stops measuring the repo and starts measuring the mesh. Removing the two forked repos above only took that component from 55 repos to 53, so the mesh is real and not an artefact of my own bug.&lt;/p&gt;

&lt;p&gt;It splits cleanly by ecosystem. The Terraform and Ansible organisations are acyclic, largest strongly connected component of size 1. The Go organisations cycle: grafana 53, open-telemetry 6, prometheus 2, fluxcd 2. Terraform module graphs cannot contain import cycles. Go module graphs across sibling repositories very much can.&lt;/p&gt;

&lt;p&gt;So I report direct dependents only. A transitive count would overstate blast radius in a Go mesh, and I would rather publish a smaller number I can defend than a bigger one I cannot.&lt;/p&gt;

&lt;h2&gt;
  
  
  A repository declares what it depends on, never what depends on it
&lt;/h2&gt;

&lt;p&gt;Every manifest points outward. &lt;code&gt;go.mod&lt;/code&gt; lists what this repo imports. &lt;code&gt;Chart.yaml&lt;/code&gt; lists what this chart pulls in. The Dockerfile names the base image it starts from. Not one of them lists the repos on the other side of the arrow, because that half of every edge only exists at the organisation level. So no amount of looking inside a single repo, however good the linter or the agent, reconstructs it. This is structural. It is not a tooling gap that someone forgot to close.&lt;/p&gt;

&lt;p&gt;A hand-maintained catalogue does not close it either, and this scan is a concrete demonstration of why. Nobody was ever going to file a catalogue entry for a CI workflow file. &lt;code&gt;shared-workflows&lt;/code&gt; is not a service, it has no owner in anybody's mental model, and it accumulated 188 dependents without a single person deciding it was important enough to write down. A &lt;a href="https://riftmap.dev/blog/backstage-alternatives/" rel="noopener noreferrer"&gt;catalogue&lt;/a&gt; is only ever as accurate as the last engineer who remembered to update it, and the repos with the largest blast radius are precisely the ones nobody remembers. Parsed, never catalogued, is the only version of this that survives contact with a real estate.&lt;/p&gt;

&lt;p&gt;Symbol graphs do not close it either, and I want to be fair here, because they are genuinely excellent at what they do. &lt;a href="https://sourcegraph.com" rel="noopener noreferrer"&gt;Sourcegraph&lt;/a&gt; indexes programming-language symbols across repositories, which is a real and hard thing to get right, and it answers "where is this function used" better than anything else. &lt;a href="https://riftmap.dev/blog/gitlab-orbit-and-the-artifact-layer/" rel="noopener noreferrer"&gt;GitLab Orbit&lt;/a&gt; is the most serious thing to arrive in this space, a properly resourced property graph of the whole SDLC, and it is validating rather than threatening to see GitLab build it. But a Terraform module resolving to a repo, a base image built by another repo, a Helm value feeding a downstream chart, a GitHub Actions workflow imported by 188 repositories, those are &lt;a href="https://riftmap.dev/blog/can-ai-check-blast-radius-of-pr-before-merge/" rel="noopener noreferrer"&gt;artifact edges&lt;/a&gt;. They do not live in the symbol layer and they do not live in the SDLC metadata layer. Symbol graphs and artifact graphs are different categories, and everything above is entirely in the artifact layer. The 188 that started this post is an Actions &lt;code&gt;uses:&lt;/code&gt; reference, which no symbol indexer has any reason to model.&lt;/p&gt;

&lt;p&gt;Give an agent every repo in your organisation and you have solved access. You have not given it the map of which repos hold each other up. Those are different gifts, and the second one is the one that pages you at 2am.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this is not
&lt;/h2&gt;

&lt;p&gt;This is public open-source data, and it is public because it is the only data I can show my work on. I cannot publish a scan of a bank.&lt;/p&gt;

&lt;p&gt;So be careful about the transfer. Cloudposse is a module vendor. Grafana is largely a plugin ecosystem. Neither is your enterprise polyrepo, and the 24.7% is not an estimate of your organisation. What does transfer is the mechanism, because the mechanism has nothing to do with whether your code is public. A repository declares what it imports and never what imports it. That is true of your private GitLab estate for exactly the same reason it is true of Grafana's.&lt;/p&gt;

&lt;p&gt;Which means the honest conclusion is not a number. It is that you cannot know your number without looking, and almost nobody has looked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this leaves you
&lt;/h2&gt;

&lt;p&gt;If you operate more than about fifty repositories, the organisation-level dependency graph is not a nice-to-have visualisation. It is the missing half of every shared-component change you make. Before you bump that base image or retag that module, the question "which repos break" has a real answer, and it is very probably not the one in your head, because the answer is a CI workflow file that somebody set up in 2022 and nobody has thought about since.&lt;/p&gt;

&lt;p&gt;That is the problem Riftmap exists to make boring. It scans a whole GitHub or GitLab organisation with one read-only token, parses the &lt;a href="https://riftmap.dev/what-is-cross-repo-dependency-mapping/" rel="noopener noreferrer"&gt;artifact edges&lt;/a&gt;, and answers the &lt;a href="https://riftmap.dev/ai-agent-blast-radius/" rel="noopener noreferrer"&gt;blast-radius question&lt;/a&gt; directly, for you and for the agents you point at your code. The data in this post came out of exactly that scan, bugs included. &lt;a href="https://riftmap.dev/data/9org-scan-2026-07-14-edges.csv" rel="noopener noreferrer"&gt;The raw edge list is here&lt;/a&gt;, together with a script that recomputes every number in the post from it. A live, clickable Grafana graph is planned and on the way; until it lands, you can poke a real one — &lt;a href="https://riftmap.dev/showcase/cloudposse/" rel="noopener noreferrer"&gt;the Cloud Posse org, where a single naming module holds up 146 of its repos&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;About Riftmap&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Riftmap maps cross-repo dependencies across your entire GitLab or GitHub organisation — Terraform, Docker, CI templates, Helm, and more. One read-only token. No YAML to maintain.&lt;/p&gt;




&lt;h2&gt;
  
  
  Frequently asked
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What percentage of repos are shared dependencies in a typical organisation?&lt;/strong&gt;&lt;br&gt;
In a scan of nine public platform organisations covering 1,297 active repositories, 320 repos, or 24.7%, were a dependency of at least one sibling repo in the same organisation. The per-org range was wide, from 4.7% to 46.9%, so treat it as the shape of the problem rather than an estimate of your own organisation. Every methodological choice in the scan biases the figure downward, including a confidence bar that discarded 35% of candidate edges before anything was counted, so 24.7% is a floor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I find every repo that consumes a shared Terraform module?&lt;/strong&gt;&lt;br&gt;
You cannot find it from the module's own repository, because a repo declares what it imports and never what imports it. You need the organisation-level dependency graph: parse the Terraform &lt;code&gt;source&lt;/code&gt; blocks across every repo in the org, including registry coordinates like &lt;code&gt;terraform-aws-modules/vpc/aws&lt;/code&gt;, and resolve each one to the repo that produces it. In the scan above, &lt;code&gt;cloudposse/terraform-null-label&lt;/code&gt; turned out to have 146 direct consumers this way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can an AI coding agent see the blast radius of a cross-repo change?&lt;/strong&gt;&lt;br&gt;
Not from a single repository's checkout. The edges that make a shared change risky are inbound cross-repo edges, and those do not exist anywhere inside the repo being edited. An agent needs an organisation-level artifact dependency graph served to it as a tool call to answer "what breaks if I change this".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why not infer cross-repo dependencies from repo names or code search?&lt;/strong&gt;&lt;br&gt;
Because the failure modes are subtle and they bite hardest on exactly the repos that look most important. In this scan, two of Grafana's apparent top hubs, &lt;code&gt;grafana/opentelemetry-go&lt;/code&gt; and &lt;code&gt;grafana/k6-DefinitelyTyped&lt;/code&gt;, were forks that preserved the upstream package identity, so they absorbed every same-org import of the upstream package. Parsing declared manifest edges and resolving them to a producing repo is what makes a count trustworthy, and even then you have to audit the hubs.&lt;/p&gt;

</description>
      <category>blastradius</category>
      <category>crossrepodependencies</category>
      <category>platformengineering</category>
      <category>dependencygraph</category>
    </item>
    <item>
      <title>Which AI coding assistants can see blast radius before they change code?</title>
      <dc:creator>Daniel Westgaard</dc:creator>
      <pubDate>Tue, 14 Jul 2026 05:11:11 +0000</pubDate>
      <link>https://dev.to/danielwe/which-ai-coding-assistants-can-see-blast-radius-before-they-change-code-3mkn</link>
      <guid>https://dev.to/danielwe/which-ai-coding-assistants-can-see-blast-radius-before-they-change-code-3mkn</guid>
      <description>&lt;p&gt;I keep hitting the same moment, and if you run more than a handful of repositories you probably do too.&lt;/p&gt;

&lt;p&gt;Claude Code is open in the platform repo. Cursor is open in a service two directories over. The base image both of them build on top of just shipped a CVE fix, so I want to bump the &lt;code&gt;FROM&lt;/code&gt; line. Or a Terraform module half the stacks pull in through a &lt;code&gt;source&lt;/code&gt; block needs one variable tightened. The edit itself is thirty seconds of work. The question that decides whether I make it before lunch or spend the afternoon in an incident channel is the one neither window will answer on its own: who else breaks.&lt;/p&gt;

&lt;p&gt;So I went and checked what each of the assistants I actually drive can do about that question today. The honest finding is worth stating flat, because it is easy to lift and easy to misread.&lt;/p&gt;

&lt;p&gt;As of July 2026, none of the three mainstream AI coding assistants, not Claude Code, not Cursor, not GitHub Copilot, resolves the cross-repo blast radius of a change, deterministically, completely, org-wide, on its own. The reason is not model quality, and it is not a feature any of them forgot to ship. The dependency edge that breaks the other repo was declared in a manifest none of them resolved into a graph, often in a repo the assistant never took in at all. Each of the three can answer the question. But only once you hand it a parsed dependency graph to query.&lt;/p&gt;

&lt;p&gt;That is the whole post. The rest is why, and what "hand it a graph" actually means.&lt;/p&gt;

&lt;h2&gt;
  
  
  The question hides a conflation
&lt;/h2&gt;

&lt;p&gt;The question "which assistant sees blast radius" smuggles in an assumption, and the assumption is the whole game: that an assistant which can see your repositories therefore knows how they depend on each other. Those are two different things, and the gap between them is where every one of these tools stops.&lt;/p&gt;

&lt;p&gt;Access is not structure. Claude Code can read every file in seven repos you granted it and still not hold a single edge between them. A text match is not a resolved edge. Cursor can grep those repos and land on a &lt;code&gt;FROM&lt;/code&gt; or a &lt;code&gt;source&lt;/code&gt; line, and still not resolve it to the repo that produces the module, or walk the chain past the first hop. A curated context set is not a parsed graph. Copilot Spaces can hold exactly the repositories a task needs, but only if you already knew which ones to add.&lt;/p&gt;

&lt;p&gt;The edge you actually care about is a specific, boring, declared thing. A Dockerfile &lt;code&gt;FROM&lt;/code&gt; line in a downstream service. A &lt;code&gt;go.mod&lt;/code&gt; require pinning a consumer to your module. A Terraform &lt;code&gt;source&lt;/code&gt; block, a Helm value reference, a GitLab CI &lt;code&gt;include&lt;/code&gt;. It was written down, in a manifest, in a repo. Just not the repo you happen to have open. The set of repositories on the far side of those edges is the blast radius of the change, and when an AI agent is the one making the change fast, that is &lt;a href="https://riftmap.dev/ai-agent-blast-radius/" rel="noopener noreferrer"&gt;AI-agent blast radius&lt;/a&gt;. It is exactly what a single repo's clone cannot show.&lt;/p&gt;

&lt;h2&gt;
  
  
  What each assistant can actually see today
&lt;/h2&gt;

&lt;p&gt;Claude Code, Cursor, and GitHub Copilot have each shipped a real multi-repo capability in 2026, and being precise about what each one is matters, because they are three genuinely different mechanisms and none of them is a dependency graph. Capabilities in this space move fast, so treat everything below as re-checked on 13 July 2026 against each vendor's own current docs and roughly three months of changelog.&lt;/p&gt;

&lt;h3&gt;
  
  
  Claude Code: access, by design
&lt;/h3&gt;

&lt;p&gt;Claude Code's cross-repo mechanism is directory access, and it is built to reach a dependency graph from somewhere else rather than construct one itself. The &lt;a href="https://code.claude.com/docs/en/cli-reference" rel="noopener noreferrer"&gt;&lt;code&gt;--add-dir&lt;/code&gt; flag&lt;/a&gt; grants Claude read and write access to directories outside the one it started in, and &lt;code&gt;permissions.additionalDirectories&lt;/code&gt; makes that grant persistent. The &lt;a href="https://code.claude.com/docs/en/large-codebases" rel="noopener noreferrer"&gt;&lt;code&gt;/add-dir&lt;/code&gt; command&lt;/a&gt; does the same from inside a running session. Anthropic's own large-codebase guidance documents the cross-repo move under &lt;a href="https://code.claude.com/docs/en/large-codebases" rel="noopener noreferrer"&gt;Grant access across packages or repositories&lt;/a&gt;: when a task spans multiple checkouts, you extend &lt;code&gt;--add-dir&lt;/code&gt; or &lt;code&gt;additionalDirectories&lt;/code&gt; to a separately-checked-out repository. All of it is genuinely useful. Point Claude Code at all seven repos and it can read and edit across them in one session.&lt;/p&gt;

&lt;p&gt;What it gives you is access, and access is not the same as the answer. At a handful of repos the difference barely bites: grant Claude all of them and you can work out by hand which one breaks when you bump the base image, because the breaking edge is a real line in a real file, the &lt;code&gt;FROM&lt;/code&gt; in a downstream service's Dockerfile, and once you have added that directory Claude can read it. The difference bites at scale. At an organisation's real size the repo on the far end of the edge is one you never added. Reconstructing the transitive closure across every manifest in the org, on demand, is the parser's deterministic job, not something the model does reliably or unprompted. Claude Code is the tool in this comparison best positioned to call an external dependency graph, and it is built to reach one: it connects to external services over MCP by design, and Anthropic's own guidance is to offload heavy search to an index exposed that way rather than read everything itself. That guidance names a search index, not a dependency graph, and the two are different objects. The architectural move is the same one, though, and a parsed dependency graph is exactly the kind of external service Claude Code is built to call. It is a consumer of the graph, not a rival to it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cursor: the agent can chase the chain, not resolve it
&lt;/h3&gt;

&lt;p&gt;Cursor's agent can attempt the cross-repo chain-walk itself, and being precise about how is what keeps this fair. Alongside the embeddings, &lt;a href="https://cursor.com/docs/agent/tools/search" rel="noopener noreferrer"&gt;Cursor ships Instant Grep&lt;/a&gt;, which its docs describe as a custom search engine that "outperforms ripgrep on large codebases," and the agent reaches for grep automatically when you reference a specific symbol. A declared dependency is a literal string, &lt;code&gt;FROM myorg/base-image&lt;/code&gt; or &lt;code&gt;source = "cloudposse/ssm-parameter-store/aws"&lt;/code&gt;, and grep finds literal strings, so Cursor really can surface a &lt;code&gt;FROM&lt;/code&gt; or a &lt;code&gt;source&lt;/code&gt; line. With a model in the loop it can go further than a bare grep. It can map the Registry shorthand &lt;code&gt;cloudposse/ssm-parameter-store/aws&lt;/code&gt; to the repository &lt;code&gt;terraform-aws-ssm-parameter-store&lt;/code&gt;, because the model knows the Terraform Registry's &lt;code&gt;terraform-&amp;lt;provider&amp;gt;-&amp;lt;name&amp;gt;&lt;/code&gt; convention, then grep for that repository and chain outward a hop at a time. For a coordinated edit across a frontend, a backend, and a shared library I already know are related, I use it and it is good.&lt;/p&gt;

&lt;p&gt;Concede the whole of that. The agent can attempt the chain-walk. The line is not whether it can try, it is what the attempt is. Chaining greps with a model resolving each shorthand by convention is inference. Partial, non-deterministic, bounded to the repositories you added to the workspace, and an expensive multi-grep crawl that gets shakier every hop, this chain running six hops deep and fanning out into several repositories along the way. The model might resolve the shorthand correctly. It might miss the second literal form of the same edge, the explicit &lt;code&gt;git::&lt;/code&gt; URL written elsewhere in the org. It might stop at hop three. One resolved query against a graph that already parsed and resolved every edge is right by construction, complete, and org-wide. The model might get it right. A parsed graph is right.&lt;/p&gt;

&lt;h3&gt;
  
  
  GitHub Copilot: a context set you assemble by hand
&lt;/h3&gt;

&lt;p&gt;GitHub Copilot reaches across repositories through a curated context set and a cloud agent, and each stops short of a dependency graph for the same underlying reason. Copilot Spaces let you assemble a set that can hold whole repositories alongside files, issues, PRs, and free-text notes, and share it across the org. &lt;a href="https://docs.github.com/en/copilot/concepts/context/spaces" rel="noopener noreferrer"&gt;GitHub's docs&lt;/a&gt; describe a Space's sources as "automatically updated as they change, making Copilot an evergreen expert in your project," which is a real strength: the set stays fresh without anyone re-uploading. The &lt;a href="https://github.blog/changelog/2026-04-01-research-plan-and-code-with-copilot-cloud-agent/" rel="noopener noreferrer"&gt;Copilot cloud agent&lt;/a&gt;, renamed from Copilot coding agent in GitHub's own changelog on 1 April 2026, will pick up a task and open a pull request for you.&lt;/p&gt;

&lt;p&gt;Each has a real edge and the same wall. A Space is only ever as good as its curation, because you assemble it by hand: it holds exactly the repositories you decided to add. The cloud agent is scoped tighter still. &lt;a href="https://docs.github.com/en/copilot/concepts/agents/cloud-agent/about-cloud-agent" rel="noopener noreferrer"&gt;GitHub's own documentation&lt;/a&gt; is blunt about the limit: Copilot "cannot make changes across multiple repositories in one run," works one branch at a time, and opens exactly one pull request per task. You can widen what it reads by naming sibling repositories, and I have written up &lt;a href="https://riftmap.dev/blog/how-to-give-copilot-cross-repo-context/" rel="noopener noreferrer"&gt;how to wire that&lt;/a&gt;. But it reads the repositories you point it at, and no mechanism here tells you which those are. The surface-by-surface version of what each Copilot surface can reach is on &lt;a href="https://riftmap.dev/github-copilot-multi-repo/" rel="noopener noreferrer"&gt;GitHub Copilot across multiple repositories&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The comparison, in one table
&lt;/h2&gt;

&lt;p&gt;Put Claude Code, Cursor, and GitHub Copilot side by side on the one axis that matters here, cross-repo blast radius, and the pattern is not three different amounts of the same capability. It is three tools solving access and reasoning, and a fourth kind of thing solving structure. The last row is not a competing assistant. It is the parsed graph the other three can call.&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;How it reaches across repos&lt;/th&gt;
&lt;th&gt;What that answers well&lt;/th&gt;
&lt;th&gt;What it structurally cannot resolve&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Claude Code&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Directory access: &lt;code&gt;--add-dir&lt;/code&gt;, &lt;code&gt;/add-dir&lt;/code&gt;, or &lt;code&gt;permissions.additionalDirectories&lt;/code&gt;. You grant it the clones.&lt;/td&gt;
&lt;td&gt;Reading and editing across every repo you have checked out, in one session.&lt;/td&gt;
&lt;td&gt;Which of those repos breaks if a shared manifest edge changes, and which repo to add in the first place.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cursor&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Instant Grep and an embeddings index across the folders in a multi-root workspace.&lt;/td&gt;
&lt;td&gt;Finding a declared line or a semantically related chunk across the folders you added.&lt;/td&gt;
&lt;td&gt;The agent can infer the chain hop by hop, but that stays non-deterministic and bounded to the workspace, never a resolved, deduplicated graph.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;GitHub Copilot&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A curated Copilot Space and a cloud agent scoped to one repo per task.&lt;/td&gt;
&lt;td&gt;Answering over a context set you assembled by hand.&lt;/td&gt;
&lt;td&gt;Which repositories to put in that set. The cloud agent touches one repo and opens one PR.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;A parsed artifact graph&lt;/strong&gt; (e.g. Riftmap)&lt;/td&gt;
&lt;td&gt;Parses the declared manifest edges across the whole org from one read-only token. No clone needed.&lt;/td&gt;
&lt;td&gt;Which repositories transitively depend on a given repo, returned before you touch it.&lt;/td&gt;
&lt;td&gt;Which function or CI job inside a downstream repo breaks. It resolves repositories, not symbols.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Read down the last column. Every "cannot" in the first three rows is the same shape: an edge whose far end is in a repo the tool never took in, or never resolved past the string. Read the last row's "cannot" and it is a different shape entirely. A real limit, but a limit of resolution, repositories rather than functions, not a limit of reach.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why none of them can resolve it
&lt;/h2&gt;

&lt;p&gt;None of Claude Code, Cursor, or GitHub Copilot resolves cross-repo blast radius on its own, and the reason is structural. Two walls stand in the way, and each of the three hits both. The first is resolution. Reading a repo is not resolving its edges. An edge only enters a graph when something parses the manifest that declares it and resolves the shorthand back to the producing repository. Cursor's agent can chain greps and walk the composition outward, which is real, but a chain-walk is inference, partial, non-deterministic, stopping wherever the crawl runs out. None of the three parses and resolves on its own.&lt;/p&gt;

&lt;p&gt;The second wall is reach, and it is where every cross-repo mechanism the three ship bottoms out. Granting Claude Code the clones. Adding folders to a Cursor workspace. Curating a Copilot Space. Configuring one of Cursor's new cloud multi-repo environments. Fanning Copilot tasks across repositories over the new agent-tasks API. Each terminates in a list of repositories a human chose, and choosing that list is the blast-radius question itself. The agent can try to extend that list by discovery, grepping outward and cloning what it finds. But discovery is bounded to what the agent already reached, it is non-deterministic, and it cannot add the repo that breaks until it already knows it breaks. That last constraint is the resolution wall in the reach wall's clothing, and it is the thing you were trying to find out.&lt;/p&gt;

&lt;p&gt;This is why "just use a bigger context window" and "just embed everything" do not close the gap. Embeddings retrieve by resemblance, and a declared dependency is not a resemblance; that is the argument I make at length in &lt;a href="https://riftmap.dev/blog/inferred-context-is-not-a-dependency-graph/" rel="noopener noreferrer"&gt;inferred context is not a dependency graph&lt;/a&gt;. Symbol graphs, the Sourcegraph-style index of where a function is defined and called, are genuinely excellent and also the wrong layer here: they resolve code symbols, not the infrastructure edges declared in Terraform, Docker, Helm, and CI, which is a &lt;a href="https://riftmap.dev/blog/symbol-graphs-and-artifact-graphs/" rel="noopener noreferrer"&gt;different category of graph entirely&lt;/a&gt;. And this is not a hypothetical failure mode. It is the mechanism behind a measurable rise in change-failure rate as AI-accelerated changes outrun anyone's ability to see their consequences, which I have &lt;a href="https://riftmap.dev/blog/ai-doesnt-understand-blast-radius/" rel="noopener noreferrer"&gt;written up with the 2026 data&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;One honest caveat, because a sharp reader will have it ready. Step one category over from the daily-driver assistants and a tool did ship real cross-repo awareness on this exact layer: Qodo's Cross Repo Review, announced in June 2026. It is not a lightweight player and I want to be fair to it. Its &lt;a href="https://docs.qodo.ai/governance/cross-repo-code-review" rel="noopener noreferrer"&gt;docs&lt;/a&gt; define a relationship type where, in their words, "One repository depends on a build artifact, container image, or pipeline output produced by the other." That is the artifact layer, named explicitly. Qodo reaches it, it is well funded, and it belongs in this conversation. And it is worth being precise about how Qodo comes to know those relationships: its docs say a relationship can be discovered automatically or defined by hand, which puts it across inferred and registered both, not one clean regime. The line I draw is not capability, it is mechanism and moment. Where a relationship feeds the impact call, Qodo's docs disclose the mechanism: "The agent reads the related repositories and traces the impact in both directions: code that depends on the change, and code in the PR that may conflict with the other repository." That is an LLM tracing at review time, which is inference, not a deterministic parser resolving a declared &lt;code&gt;source =&lt;/code&gt; edge the build already executes. And the moment is the other half. Qodo acts on pull requests already opened, and "By default, Qodo targets the main branch of each connected repository," not at the point a developer is deciding what to change in an interactive session. Qodo does not break the argument here. It sharpens it. Hand a tool the cross-repo relationships and it will reason over them. The whole question is where those relationships come from and how they are resolved.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changes the answer: hand it the graph
&lt;/h2&gt;

&lt;p&gt;What changes the answer is not a smarter assistant but a parsed dependency graph the assistant can query, and the effect is the same for all three. Here is the shape of it, with a receipt from a real scan rather than an adjective.&lt;/p&gt;

&lt;p&gt;Take one repository an assistant might plausibly have open. Cloud Posse's &lt;code&gt;terraform-aws-ssm-parameter-store&lt;/code&gt;, a thin wrapper around AWS SSM Parameter Store, the kind of small shared module nobody thinks about until it changes. On its own, any of the three sees one repo. Point a parsed cross-repo graph at the same repo and ask what depends on it, and the answer comes back as a set of repositories.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. resolve the one repo the assistant already has&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-API-Key: &lt;/span&gt;&lt;span class="nv"&gt;$RIFTMAP_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"https://api.riftmap.dev/api/v1/repositories/lookup?url=https://github.com/cloudposse/terraform-aws-ssm-parameter-store"&lt;/span&gt;
&lt;span class="c"&gt;# → { "id": "54052e08-b43f-4b67-975b-d869e9cdc62a", ... }&lt;/span&gt;

&lt;span class="c"&gt;# 2. ask the graph what transitively depends on it&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-API-Key: &lt;/span&gt;&lt;span class="nv"&gt;$RIFTMAP_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"https://api.riftmap.dev/api/v1/repositories/54052e08-b43f-4b67-975b-d869e9cdc62a/impact"&lt;/span&gt;
&lt;span class="c"&gt;# → { "total_affected": 22, "max_depth_reached": 6, ... }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As scanned on 7 July 2026, &lt;code&gt;terraform-aws-ssm-parameter-store&lt;/code&gt; sits upstream of 22 other repositories inside the same Cloud Posse GitHub org, reached across six hops of Terraform module composition. Every edge on the path is a Terraform &lt;code&gt;source =&lt;/code&gt; reference, parsed out of the manifest and resolved back to the repository that produces the module, never a &lt;code&gt;go.mod&lt;/code&gt; or &lt;code&gt;package.json&lt;/code&gt; require. The chain is legible if you walk it. The &lt;code&gt;source&lt;/code&gt; in &lt;code&gt;terraform-aws-iam-system-user&lt;/code&gt;, at &lt;code&gt;main.tf&lt;/code&gt; line 63, points back at the parameter-store module. That module is pulled in by &lt;code&gt;iam-s3-user&lt;/code&gt;, which is pulled in by &lt;code&gt;s3-bucket&lt;/code&gt;, then &lt;code&gt;s3-log-storage&lt;/code&gt;, then &lt;code&gt;lb-s3-bucket&lt;/code&gt;, and six hops out you reach the org's load-balancer modules: &lt;code&gt;terraform-aws-alb&lt;/code&gt;, &lt;code&gt;terraform-aws-nlb&lt;/code&gt;, and &lt;code&gt;terraform-aws-elastic-beanstalk-environment&lt;/code&gt;. None of those load balancers names the parameter-store module anywhere in its own files. The edge is real, declared, and six hops removed from anything a human editing that module would think to grep for.&lt;/p&gt;

&lt;p&gt;That is the number none of the three produces on its own, and the one all three can retrieve in a single call once the graph exists. Two things keep it honest. The call is repo-level, so it tells you which repositories are downstream, not which function or job inside them; for the pre-change question, which repos do I need to think about, that is exactly the resolution you want. And the 22 is a transitive closure, not 22 breaks at once.&lt;/p&gt;

&lt;p&gt;The reason is the pin, and there is a further distinction hiding in it. Cloud Posse pins every one of these edges, this chain's first hop at &lt;code&gt;0.13.0&lt;/code&gt;, the far-end &lt;code&gt;terraform-aws-alb&lt;/code&gt; at &lt;code&gt;0.19.0&lt;/code&gt;, so a bump to the shared module is a migration you schedule, not an outage you wake up to. Each hop propagates only as it bumps its own pin. That pinned case is what this receipt demonstrates. The estate that actually needs this graph is the other one, the &lt;code&gt;source&lt;/code&gt; that is a bare &lt;code&gt;git::&lt;/code&gt; URL with no &lt;code&gt;?ref=&lt;/code&gt;, the &lt;code&gt;FROM&lt;/code&gt; pinned to &lt;code&gt;:latest&lt;/code&gt;, the CI &lt;code&gt;include&lt;/code&gt; that tracks &lt;code&gt;main&lt;/code&gt;. Those edges do not wait for a coordinated bump. They break on the next apply. And the same graph tells the two apart, because Riftmap carries the version constraint and its pin state on every edge, a &lt;code&gt;version_constraint_state&lt;/code&gt; of pinned, floating, or branch. Point Riftmap at your own estate and it splits the affected set in two: the consumers you coordinate an upgrade across, and the ones that break unannounced. That second set is the afternoon in an incident channel this post opened on. That split is Riftmap's to make and hard for the others to reach. Grep finds the string but not the pin semantics behind it. An agent chain-walk has to read and correctly interpret a version constraint at every single hop. Qodo's LLM traces the impact at review time and never reads the pin semantics that decide which case an edge is in.&lt;/p&gt;

&lt;p&gt;What 22 gives you is the set you must consider and coordinate a fix across, which for the kind of shared-module change that opened this post is exactly the right set, four of them direct dependents and the rest transitive, the chain necking through one repository mid-way before it fans out to the load balancers. And 22 is the count inside the one org Riftmap has connected here, the 242-repo Cloud Posse org. Anyone vendoring these modules into their own private infrastructure is invisible to that scan, so the honest figure is at least 22, not exactly 22. The assistant, meanwhile, is only ever one caller of that graph: when I am the one bumping the module, those same two calls run from my own terminal, no agent in the loop at all.&lt;/p&gt;

&lt;p&gt;It matters that the graph is parsed rather than guessed, and the confidence floor is where that shows. In the depth-six graph around this module there are 254 edges, and 79 of them sit below the default 0.8 confidence floor and are excluded from the answer. Every one of those 79 is a &lt;code&gt;git_url_reference&lt;/code&gt;, a heuristic text match at confidence 0.6 to 0.7, a script mentioning a repo URL in passing rather than a manifest declaring a dependency. The eight edges on the spine all resolve at 0.9, the deterministic score for a Terraform Registry shorthand matched against the Registry's naming convention, the same transform the Cursor agent runs hop by hop, here applied mechanically across the whole org and comfortably above the floor. Parsed, not inferred, is not a slogan here. It is the line between the edge the build actually executes and a string that happened to look like one.&lt;/p&gt;

&lt;p&gt;There is no first-party Riftmap MCP server yet, and I would rather say that plainly than dress it up. What exists today is not a workaround, it is the path I actually run. Riftmap publishes an OpenAPI schema, so you point a generic OpenAPI-to-MCP bridge at it and each endpoint becomes a tool the assistant can call, no bespoke server to build. The one thing worth doing is scoping it: the &lt;code&gt;--tag&lt;/code&gt; flags below hand the agent only the read-only graph endpoints (the &lt;code&gt;repositories&lt;/code&gt; and &lt;code&gt;impact&lt;/code&gt; tags — lookup, dependencies, dependents, impact), so it gets the tools it needs to answer "who else breaks" and nothing else, no billing or workspace-admin surface from the rest of the schema. One config block, your key in an &lt;code&gt;X-API-Key&lt;/code&gt; header:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"riftmap"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"@ivotoby/openapi-mcp-server"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"--tag"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"repositories"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"--tag"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"impact"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"env"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"OPENAPI_SPEC_PATH"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://app.riftmap.dev/openapi.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"API_BASE_URL"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://api.riftmap.dev/api/v1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"API_HEADERS"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"X-API-Key:${RIFTMAP_API_KEY}"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the setup I walk through for &lt;a href="https://riftmap.dev/blog/claude-code-cursor-cross-repo-context/" rel="noopener noreferrer"&gt;Claude Code and Cursor&lt;/a&gt;, against the &lt;a href="https://riftmap.dev/for-agents/" rel="noopener noreferrer"&gt;endpoints an agent calls&lt;/a&gt;. The same graph answers at merge time, not just at planning time, and checking a PR's blast radius as a gate is &lt;a href="https://riftmap.dev/blog/can-ai-check-blast-radius-of-pr-before-merge/" rel="noopener noreferrer"&gt;its own piece, organised by graph layer&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Other parsed graphs an agent can call over MCP already exist. The line Riftmap draws is narrower and, I think, more durable. It is the artifact layer, resolved. Dockerfile &lt;code&gt;FROM&lt;/code&gt;, Terraform &lt;code&gt;source&lt;/code&gt;, Helm value references, GitLab CI &lt;code&gt;include&lt;/code&gt;, each edge followed back to the repository that actually produces the image or the module, and served across an entire GitHub or GitLab org from one read-only, org-level token, not a per-developer index of the repos you happened to clone. The receipt above walks one Terraform &lt;code&gt;source&lt;/code&gt; chain; the same parse-and-resolve step runs across the parser suite, so a Dockerfile &lt;code&gt;FROM&lt;/code&gt; line, a Helm value reference, or a GitLab CI &lt;code&gt;include&lt;/code&gt; resolves back to its producing repository the same deterministic way. That is the layer an assistant structurally cannot rebuild from a single repo's checkout, which is the whole reason the answer to who else breaks was never in the box to begin with.&lt;/p&gt;

&lt;p&gt;This is all Riftmap is. It parses the edges your manifests already declare, a dozen ecosystems in all, across an entire GitHub or GitLab org from one read-only token, and serves the graph over an API. It is not a fourth assistant competing with the three. It is the substrate they were always going to have to call. The map of the dependency that used to live only in the head of the engineer who wired that shared module in last time and has since left. If you want that map for your own org rather than Cloud Posse's, that is what &lt;a href="https://riftmap.dev" rel="noopener noreferrer"&gt;Riftmap&lt;/a&gt; is for.&lt;/p&gt;

&lt;h2&gt;
  
  
  The assistant was never the limitation
&lt;/h2&gt;

&lt;p&gt;The race between Claude Code, Cursor, and Copilot is real, and it is a race about access and reasoning, and both of those keep getting better every month. Neither is the thing standing between you and the answer to "who else breaks." The edge that breaks the other repo was declared in a manifest, in a repo none of them opened, and no amount of model improvement puts it back in the box. Give any of the three that parsed graph and all three can answer. The assistant was never the limitation. The missing graph was.&lt;/p&gt;

&lt;h2&gt;
  
  
  Questions I get asked
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Which AI coding assistants can see blast radius before they change code?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;As of July 2026, none of the three mainstream assistants resolves cross-repo blast radius on its own: not Claude Code, not Cursor, not GitHub Copilot. Each has a real multi-repo mechanism (directory access, grep plus a semantic index, curated context sets), but none reads and resolves the declared manifest edges that decide which repositories break when a shared base image, module, or CI template changes. Hand one a parsed graph and it can: pointed at Cloud Posse's &lt;code&gt;terraform-aws-ssm-parameter-store&lt;/code&gt;, that graph returned 22 downstream repositories across the org, six hops out along Terraform &lt;code&gt;source&lt;/code&gt; references resolved back to the producing repo, as scanned on 7 July 2026. The honest answer is whichever assistant you give the graph to.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Are there AI assistants that show blast radius before code changes?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not among the daily-driver assistants, on their own. The closest is one category over: Qodo's Cross Repo Review, a PR-review feature, has an LLM read the related repositories and trace cross-repo impact on already-opened pull requests, on the main branch by default, which is inference at review time rather than a parsed graph queried at planning time. It reaches the artifact layer, which is real, but through tracing, not a deterministic parser resolving the &lt;code&gt;source =&lt;/code&gt; and &lt;code&gt;FROM&lt;/code&gt; edges a build already executes. For an interactive assistant to show blast radius before it opens a PR, you point it at a parsed cross-repo graph, and the affected repositories come back before the change is made.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can Claude Code, Cursor, or Copilot analyse the blast radius of a change on their own?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. Claude Code sees the directories you grant it, Cursor greps and embeds the folders you added to the workspace, and Copilot reasons over a context set you curate, and none of the three resolves a dependency edge whose far end lives in a repo the tool never took in. The limit is structural, not a matter of model quality: a stronger model reading the same single repo still cannot recover an edge that was declared in a manifest somewhere else. They can all answer the question once you hand them a parsed cross-repo dependency graph to query. On their own, they cannot build one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do you give an AI coding assistant cross-repo blast-radius awareness?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You give it a queryable dependency graph, let it resolve the repository it is about to change, and ask which repositories transitively depend on that one. Riftmap builds that graph by parsing the declared edges (Dockerfile &lt;code&gt;FROM&lt;/code&gt;, Terraform &lt;code&gt;source&lt;/code&gt;, &lt;code&gt;go.mod&lt;/code&gt; requires, Helm, CI) across an entire org from one read-only token, and serves it over an HTTP API any assistant can call; there is no first-party MCP server yet, so you point the assistant at the API directly or through a generic OpenAPI-to-MCP bridge pointed at the published schema. Pointed at &lt;code&gt;cloudposse/terraform-aws-ssm-parameter-store&lt;/code&gt;, that graph returned 22 repositories across the org that depend on it, six hops out through Terraform &lt;code&gt;source&lt;/code&gt; references, as scanned on 7 July 2026.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;About Riftmap&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Riftmap maps cross-repo dependencies across your entire GitLab or GitHub organisation — Terraform, Docker, CI templates, Helm, and more. One read-only token. No YAML to maintain.&lt;/p&gt;

</description>
      <category>blastradius</category>
      <category>aicodingassistants</category>
      <category>crossrepodependencies</category>
      <category>dependencygraph</category>
    </item>
    <item>
      <title>Blast radius in software engineering: what it is, how to measure it, how to contain it</title>
      <dc:creator>Daniel Westgaard</dc:creator>
      <pubDate>Wed, 08 Jul 2026 18:23:42 +0000</pubDate>
      <link>https://dev.to/danielwe/blast-radius-in-software-engineering-what-it-is-how-to-measure-it-how-to-contain-it-5gcd</link>
      <guid>https://dev.to/danielwe/blast-radius-in-software-engineering-what-it-is-how-to-measure-it-how-to-contain-it-5gcd</guid>
      <description>&lt;p&gt;You change one small shared module. Tests are green. The type-checker is happy. You merge, because everything you can see from inside that repository says the change is safe. Then a pipeline fails in a repo you had forgotten existed, owned by a team you have never spoken to, that pulls your module in through a line nobody remembers writing.&lt;/p&gt;

&lt;p&gt;Every engineer who has worked in a multi-repo estate has lived some version of this. The canonical public version is left-pad: eleven lines of JavaScript, unpublished by one author in 2016 and used as a transitive dependency by everything from small projects to major companies including Facebook, Netflix, and Spotify. When it vanished, &lt;a href="https://en.wikipedia.org/wiki/Npm_left-pad_incident" rel="noopener noreferrer"&gt;dependent builds across the ecosystem received 404s and failed&lt;/a&gt;, because almost nobody knew how many things quietly depended on it. Left-pad was an unpublish, an availability event rather than a breaking change, but the shape is identical: transitive consumers nobody had counted. The action was trivially safe from where its author stood. The damage happened everywhere else.&lt;/p&gt;

&lt;p&gt;That gap has a name now, and the name is doing too much work. Blast radius is not one measurement. It is two, and most of the tooling that promises to measure it only sees the one that stays inside a single checkout.&lt;/p&gt;

&lt;h2&gt;
  
  
  The word is doing two jobs at once
&lt;/h2&gt;

&lt;p&gt;"Blast radius" quietly conflates two different measurements: the code blast radius your tests, type-checker, and impact-analysis tools already catch inside one repository, and the artifact blast radius that crosses repository boundaries through base images, Terraform &lt;code&gt;source&lt;/code&gt; blocks, and the like, which nothing in a single checkout can see.&lt;/p&gt;

&lt;p&gt;These two live at different layers and fail in different ways. The code radius is the set of functions, files, and callers inside your repo that your change touches. It is real, and it is largely solved for the static edges. Your test suite runs it. Your language server draws it. A file-level import graph traverses it. If your change breaks something one import away, you find out before you merge.&lt;/p&gt;

&lt;p&gt;The artifact radius is the set of other repositories, services, and pipelines wired to the thing you changed through a manifest edge that lives in someone else's repo. From your checkout, that edge is invisible. You cannot grep for it, because the file that references your module is not on your disk. The consuming repo declares the dependency, in its manifest, on its side of the boundary. This is the radius that pages you at 2am, and it is the one your CI never saw.&lt;/p&gt;

&lt;p&gt;The reason this matters more every quarter is speed. AI coding agents now make small, plausible, locally-correct cross-repo changes faster than any human reviewer can trace them, and a 2026 Cortex benchmark of engineering leaders reported change failure rates &lt;a href="https://www.cortex.io/post/ai-is-making-engineering-faster-but-not-better-state-of-ai-benchmark-2026" rel="noopener noreferrer"&gt;up roughly 30% and incidents per pull request up 23.5%&lt;/a&gt; alongside that acceleration. Amazon ran an internal deep-dive in March 2026 into a trend of incidents with, as &lt;a href="https://tech.yahoo.com/ai/deals/articles/amazon-investigates-trend-incidents-high-115550319.html" rel="noopener noreferrer"&gt;reported by the Financial Times&lt;/a&gt;, "high blast radius", tied in part to novel GenAI usage running ahead of established safeguards. The occasion is new. The blind spot is old.&lt;/p&gt;

&lt;h2&gt;
  
  
  What blast radius actually is
&lt;/h2&gt;

&lt;p&gt;Blast radius is the total set of things that break, degrade, or need updating when a change ships, measured along a spectrum from the function you edited out to the repositories you have never checked out. It is a system property, not a code property, and the honest definition has to hold the whole spectrum at once.&lt;/p&gt;

&lt;p&gt;At the narrow end sits the function: change a signature, and every caller in the file feels it. One step out is the module: the internal package that a dozen files import. One step further is the service: the API other services in your process boundary call. These three all live inside a repository, and inside a repository the graph is legible. You own the code, you own the tests, the edges are on your disk.&lt;/p&gt;

&lt;p&gt;The spectrum does not stop at the repo wall. The widest band is the cross-repo artifact edge, where your change reaches a consumer through a construct that neither repository executes together: a Docker &lt;code&gt;FROM&lt;/code&gt; line pinning your base image, a Terraform &lt;code&gt;source&lt;/code&gt; resolving to your module, a Helm value feeding a downstream chart, a GitLab CI &lt;code&gt;include&lt;/code&gt; pulling your pipeline template, a &lt;code&gt;go.mod&lt;/code&gt; require on your published package. Same word, same question, radically different visibility. The industry mostly defines blast radius at the layer it can already see, and then stops.&lt;/p&gt;

&lt;p&gt;That is the fragmentation you hit if you go looking for the definition today. &lt;a href="https://loomai.io/glossary/blast-radius-analysis.html" rel="noopener noreferrer"&gt;LOOM's glossary&lt;/a&gt; scopes it to "how many functions, classes, and files are affected when a single code element changes". &lt;a href="https://axiomrefract.com/learn/what-is-blast-radius" rel="noopener noreferrer"&gt;Axiom Refract&lt;/a&gt; defines it as the set of files that break when a given file changes, computed by BFS over a file-level import graph. &lt;a href="https://recursive.pm/blast-radius" rel="noopener noreferrer"&gt;Recursive&lt;/a&gt; pushes the other way, all the way out to business impact, features and customers and revenue. &lt;a href="https://sixdegree.ai/blog/blast-radius-analysis" rel="noopener noreferrer"&gt;SixDegree&lt;/a&gt; reaches furthest, into infrastructure, data, and people. Each is correct at its layer. None of the widely-cited ones names the actual cross-repo manifest constructs as the edge. That is the gap this hub exists to fill.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to measure it
&lt;/h2&gt;

&lt;p&gt;You measure blast radius by building a directed dependency graph and traversing it backwards from the thing you changed, and every method in common use does exactly that up to the repository boundary and then stops. The differences between methods are real and worth walking honestly, because each one genuinely works where it works.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dependency-graph BFS
&lt;/h3&gt;

&lt;p&gt;The dominant mechanical method, and the one the 2026 tooling wave has converged on, is a breadth-first search over a dependency graph. Nodes are files or symbols, edges are imports or calls, and you traverse the reverse edges out from the changed node: depth 1 is the direct dependents, depth 3 to 5 is the transitive tail. LOOM, Axiom Refract, and Pharaoh all describe this mechanism precisely, and it is the right mechanism. It is deterministic, it is fast, and it answers "what imports this" without guessing.&lt;/p&gt;

&lt;p&gt;Its boundary is the graph you built. A file-level or symbol-level import graph is scoped to one parsed codebase. It has no node for the repo across the org that pins your image, because that node was never in the graph. The BFS is only as wide as its inputs, and its inputs stop at the checkout.&lt;/p&gt;

&lt;h3&gt;
  
  
  Impact-based test selection
&lt;/h3&gt;

&lt;p&gt;A sharper version runs the graph to pick which tests to execute: predict the affected code, run only those tests, skip the rest. When it works it is a genuine accelerator, and for a large single-repo test suite it is worth real money. It answers "which of my tests could this break" with precision.&lt;/p&gt;

&lt;p&gt;It inherits the same wall, plus a second one. The affected tests it can select are the tests in the repository it is running in. A consumer in another repo has its own suite, on its own side of the boundary, and your pre-merge run never invokes it. Worse, static selection is demonstrably imprecise for dynamic dispatch and event-driven code even within one language: the runtime edges are exactly the ones static analysis cannot see.&lt;/p&gt;

&lt;h3&gt;
  
  
  Change-failure-rate, the lagging outcome
&lt;/h3&gt;

&lt;p&gt;DORA's change failure rate is the most-cited blast-radius-adjacent metric, and it is a genuinely useful health number, but it measures something structurally different from the two above. DORA defines it as &lt;a href="https://dora.dev/guides/dora-metrics-four-keys/" rel="noopener noreferrer"&gt;the ratio of deployments that require immediate intervention following a deployment&lt;/a&gt;, a rollback or a hotfix. It is an outcome, aggregated after the fact, across however many deploys you count.&lt;/p&gt;

&lt;p&gt;That makes it a lagging measure, not a graph you can consult beforehand. It tells you how often your changes broke something last quarter. It does not tell you which repositories are structurally exposed to the change sitting in your pull request right now. It is a number to drive down, not a map to read before you merge. Conflating the two is a common and expensive mistake: a good CFR trend and a completely unmapped artifact layer coexist happily, right up until they do not. If you want the metric itself, we wrote a &lt;a href="https://riftmap.dev/blog/measure-your-change-failure-rate/" rel="noopener noreferrer"&gt;practical guide to measuring your change failure rate&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For fact density, none of this changes when you switch ecosystems. Within the &lt;code&gt;open-telemetry&lt;/code&gt; GitHub org, 16 other repos declare a &lt;code&gt;go.mod&lt;/code&gt; require on &lt;code&gt;go.opentelemetry.io/otel&lt;/code&gt;, as of the org's most recent completed scan, 2026-06-28. That edge is a first-class cross-repo dependency, and no single repo's BFS, test selection, or CFR contains it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The radius you can't see from one repo
&lt;/h2&gt;

&lt;p&gt;The widest band, the cross-repo artifact edge, is invisible from any single checkout by definition, and mapping those edges across an entire estate is its own discipline, &lt;a href="https://riftmap.dev/what-is-cross-repo-dependency-mapping/" rel="noopener noreferrer"&gt;cross-repo dependency mapping&lt;/a&gt;, which the rest of this post depends on.&lt;/p&gt;

&lt;p&gt;This is where the bus-factor problem and the artifact problem turn out to be the same problem. The shared module nobody remembers owning is exactly the one that walked out the door with the engineer who last touched it. The edge is declared in a manifest, so it is still there and still load-bearing, but the knowledge of why it exists left the building. A parsed graph does not care who remembers; the edge is in the file whether or not anyone can still explain it. This is the &lt;a href="https://riftmap.dev/blog/dependency-bus-factor/" rel="noopener noreferrer"&gt;dependency bus-factor problem&lt;/a&gt; stated structurally.&lt;/p&gt;

&lt;p&gt;The AI-agent version of this blindness is its own case, because an agent working from one repo's clone structurally cannot see the other side of these edges, which is why &lt;a href="https://riftmap.dev/blog/ai-doesnt-understand-blast-radius/" rel="noopener noreferrer"&gt;AI agents in particular miss the cross-repo radius&lt;/a&gt; and why the &lt;a href="https://riftmap.dev/ai-agent-blast-radius/" rel="noopener noreferrer"&gt;AI-agent blast radius&lt;/a&gt; has become its own conversation. But the agent is the occasion, not the subject. The engineer who owns the estate and holds the pager had this blind spot long before the agents arrived.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to contain it
&lt;/h2&gt;

&lt;p&gt;Containment means three different things depending on which layer you are standing on, and only one of them is about knowing the graph before the change ships. Getting the reader honestly to the third one means conceding the first two in full, because both are real and neither is what a dependency graph does.&lt;/p&gt;

&lt;h3&gt;
  
  
  The cloud sense: cell-based architecture and breach containment
&lt;/h3&gt;

&lt;p&gt;If you type &lt;code&gt;blast radius aws&lt;/code&gt; into a search box, you almost certainly want fault isolation, and that is a legitimate, mature discipline that has nothing to do with cross-repo dependency graphs. AWS's Well-Architected guidance is to build bulkhead, or cell-based, architectures so a failure is contained to a limited slice of the system: &lt;a href="https://docs.aws.amazon.com/wellarchitected/latest/reliability-pillar/rel_fault_isolation_use_bulkhead.html" rel="noopener noreferrer"&gt;if a workload uses 10 cells to service 100 requests, a failure leaves 90% of requests unaffected&lt;/a&gt;. Adjacent to it is the security sense, the scope of what a compromised credential or breached IAM role can reach. Both are real, both are important, and Riftmap does none of it. Cell-based architecture, IAM scoping, and breach containment are a different discipline from mapping which repos consume your Terraform module, and pretending otherwise would be the exact kind of overclaim this blog exists not to make.&lt;/p&gt;

&lt;h3&gt;
  
  
  The change-impact sense: flags and canaries
&lt;/h3&gt;

&lt;p&gt;The other honest sense of containment is exposure control after the code is deployed, and it works. Canary releases and feature flags reduce blast radius by limiting how much traffic meets a bad change. DeployHQ puts it plainly: &lt;a href="https://www.deployhq.com/blog/smoother-deployments-with-canary-releases-a-code-centric-approach" rel="noopener noreferrer"&gt;"Reduced blast radius. A bug that ships to 5% of traffic affects 5% of users, not 100%."&lt;/a&gt; LaunchDarkly frames the same move as decoupling deploy from release, so code can sit dormant behind a flag and be exposed to a ring at a time. This is genuinely valuable and I would not ship anything risky without it.&lt;/p&gt;

&lt;p&gt;It answers a different question, though. Flags and canaries control what percentage of traffic is exposed to a change. They say nothing about which repositories are structurally wired to the thing that changed. The two are complementary, not competing: a canary limits the fallout of a break you did not foresee, and a graph tells you which breaks to foresee in the first place. If you do not know a downstream repo consumes your module, no canary percentage will tell you it exists.&lt;/p&gt;

&lt;h3&gt;
  
  
  Structural containment: know the graph before the change ships
&lt;/h3&gt;

&lt;p&gt;Structural containment is knowing the cross-repo graph before the change ships, so the artifact radius is a fact you read rather than an incident you discover. This is the layer the other two leave uncovered, and it is a parsing problem before it is anything else.&lt;/p&gt;

&lt;p&gt;Here is what that looks like as a real number. Across the 242 GitHub repos in the &lt;code&gt;cloudposse&lt;/code&gt; org, 147 other cloudposse repos declare a Terraform &lt;code&gt;source&lt;/code&gt; reference to &lt;code&gt;cloudposse/terraform-null-label&lt;/code&gt;, the org's own foundational naming-convention module, as of a scan completed 2026-07-07 and queried live 2026-07-08. That is 147 live &lt;code&gt;source =&lt;/code&gt; edges into a single module, discoverable from no single one of those repos' checkouts. Be precise about what the number is and is not: it does not mean 147 repos would break tomorrow. It means 147 repos hold a live declared edge to that module, and whether any given consumer breaks depends on the next release and on whether that consumer is pinned. Here 138 are already on the latest tag and 9 are lagging, so the sharper containment statement is not "147 would break" but "147 consume this, and only 9 are not already on &lt;code&gt;0.25.0&lt;/code&gt;". &lt;code&gt;terraform-null-label&lt;/code&gt; is unusually central even for its own org, so read 147 as this estate's top example, not a typical average. The point is not the size of the number. The point is that you cannot answer the question at all from inside any one of those repos, and the answer changes every time someone bumps a pin. We &lt;a href="https://riftmap.dev/blog/what-242-cloudposse-repos-depend-on/" rel="noopener noreferrer"&gt;walked all 242 cloudposse repos in a separate scan writeup&lt;/a&gt; if you want the full picture.&lt;/p&gt;

&lt;p&gt;This is measurable because the edges are declared. Riftmap parses them, deterministically, from what the manifests actually say rather than inferring them from names or embeddings, across an entire GitHub or GitLab organisation from one read-only token, with no per-repo YAML to maintain. The traversal is a plain breadth-first search over repo-to-repo edges in Postgres. The product's own information model already encodes the distinction this post is built on: the API endpoint that answers the question is &lt;code&gt;GET /repositories/{repo_id}/impact&lt;/code&gt;, its docstring titles it "Transitive downstream blast radius (repo-level BFS)", and it says in as many words that it "does not trace specific files, jobs, or symbols within the source repo". The code radius is deliberately out of scope. The artifact radius is the whole job.&lt;/p&gt;

&lt;p&gt;I want to be straight about two limits, because a sharp reader will raise both. First, a parsed graph is only as complete as what is declared. An HTTP call expressed in no manifest, a queue consumer wired by convention, a service discovered at runtime: none of that is in a &lt;code&gt;source&lt;/code&gt; block, and a declared graph will not show it. That is a real boundary, and runtime discovery is a complementary axis, not a lesser one. SixDegree makes the reasoned case for the opposite priority, &lt;a href="https://sixdegree.ai/blog/blast-radius-analysis" rel="noopener noreferrer"&gt;preferring discovered dependencies over declared ones when they conflict&lt;/a&gt;, and they are not wrong that runtime telemetry catches edges parsing cannot. The trade is honesty: a declared edge is kept honest by the build that already executes it, so it does not drift; a discovered edge needs the change already running to be seen. If the declared-versus-discovered distinction is where your interest is, we drew it out fully in &lt;a href="https://riftmap.dev/blog/declared-inferred-registered/" rel="noopener noreferrer"&gt;declared, inferred, registered&lt;/a&gt;. Second, parsing declared cross-repo edges is not something only Riftmap can do. The open-source tool &lt;a href="https://github.com/cajasmota/grafel" rel="noopener noreferrer"&gt;grafel&lt;/a&gt; traces Terraform and Kubernetes edges across repos as a local daemon, and it is genuinely good work. The line I would draw is scope and shape, not capability: an org-wide hosted graph across a dozen ecosystems, parsed from one token, is a different proposition from a daemon you run locally per machine. Neither claim needs the other to be false.&lt;/p&gt;

&lt;p&gt;If your estate is bigger than you can hold in your head and you want to see the artifact radius before the next change ships rather than after it pages you, &lt;a href="https://app.riftmap.dev" rel="noopener noreferrer"&gt;point Riftmap at your org&lt;/a&gt; and read your own graph before you merge.&lt;/p&gt;

&lt;p&gt;The blast radius you can measure in CI is not the blast radius that pages you at 2am. One lives inside a repository, where your tests already run and the graph is legible. The other lives in the manifests of repos you have never opened, declared in files that are not on your disk, and it is the whole reason "it worked on my branch" and "it took down three teams" are both true at once. Measuring the first has been solved for years. Measuring the second is a parsing problem, and it is the one worth solving before you merge.&lt;/p&gt;

&lt;h2&gt;
  
  
  Questions engineers actually ask
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What is blast radius in software engineering?&lt;/strong&gt;&lt;br&gt;
Blast radius is the total set of things that break, degrade, or need updating when a change ships, measured from the function you edited out to the repositories you have never checked out. It splits into two layers: the code blast radius inside one repo, which your tests and type-checker already catch, and the artifact blast radius across repos, which travels through base images, Terraform &lt;code&gt;source&lt;/code&gt; blocks, Helm values, and CI &lt;code&gt;include&lt;/code&gt;s and is invisible from a single checkout.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do you measure the blast radius of a code change?&lt;/strong&gt;&lt;br&gt;
You build a dependency graph and traverse it backwards from the changed thing: depth 1 is direct dependents, deeper hops are the transitive tail. Inside one repo, a file or symbol import graph and impact-based test selection do this well. To measure the cross-repo radius you have to parse the declared manifest edges in every other repo, because the consuming edge lives in a repo that is not on your disk and no single-repo traversal contains it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do you contain or reduce blast radius?&lt;/strong&gt;&lt;br&gt;
There are three senses. Cloud fault isolation (cell-based architecture, IAM scoping) contains failures at the infrastructure layer. Flags and canaries contain exposure by limiting what percentage of traffic meets a change. Structural containment means knowing the cross-repo dependency graph before the change ships, so the artifact radius is a fact you read rather than an incident you discover. The three are complementary, and only the third is about the dependency graph.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's the difference between blast radius in AWS and the blast radius of a code change?&lt;/strong&gt;&lt;br&gt;
"Blast radius" in an AWS context usually means fault or breach isolation: architecting cells so one failure or one compromised credential reaches a limited slice of the system. The blast radius of a code change is the set of code, services, and downstream repositories affected when that change ships. They share a word and an intuition but operate at different layers, and a tool for one does not do the other.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How does dependency mapping help reduce the blast radius of incidents?&lt;/strong&gt;&lt;br&gt;
Dependency mapping turns the cross-repo blast radius from something you discover mid-incident into something you can read before the change ships. During an incident it lets you traverse the graph backwards from the failing component to every repository that consumes it — direct dependents at depth 1, the transitive tail beyond — so you know what to check and who to page instead of grepping across repos at 2am. Used before a change, the same parsed graph surfaces the artifact radius up front, which is the difference between a contained rollback and a multi-team outage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;About Riftmap&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Riftmap maps cross-repo dependencies across your entire GitLab or GitHub organisation — Terraform, Docker, CI templates, Helm, and more. One read-only token. No YAML to maintain.&lt;/p&gt;

</description>
      <category>blastradius</category>
      <category>dependencygraph</category>
      <category>platformengineering</category>
      <category>terraform</category>
    </item>
    <item>
      <title>What 242 Cloud Posse repos actually depend on</title>
      <dc:creator>Daniel Westgaard</dc:creator>
      <pubDate>Sun, 05 Jul 2026 12:30:45 +0000</pubDate>
      <link>https://dev.to/danielwe/what-242-cloud-posse-repos-actually-depend-on-llm</link>
      <guid>https://dev.to/danielwe/what-242-cloud-posse-repos-actually-depend-on-llm</guid>
      <description>&lt;p&gt;&lt;code&gt;cloudposse/terraform-null-label&lt;/code&gt; is a Terraform module that does nothing at runtime. It generates names and tags. Its own &lt;a href="https://github.com/cloudposse/terraform-null-label" rel="noopener noreferrer"&gt;README&lt;/a&gt; calls it a "Terraform module designed to generate consistent names and tags for resources," and that is the whole job. The kind of module nobody opens a PR against on a Tuesday and thinks twice about.&lt;/p&gt;

&lt;p&gt;Open its &lt;a href="https://github.com/cloudposse/terraform-null-label/network/dependents" rel="noopener noreferrer"&gt;Dependents tab on GitHub&lt;/a&gt;, the tab that is meant to tell you which repositories rely on a repository, and GitHub says: "We haven't found any dependents for this repository yet."&lt;/p&gt;

&lt;p&gt;That is the single most-depended-on module in the entire Cloud Posse org, and the most obvious tool a sceptical reader would reach for reports zero. A Riftmap scan of all 242 Cloud Posse repos found that one module, &lt;code&gt;terraform-null-label&lt;/code&gt;, is declared as a direct dependency by 147 of them, 61% of the org, nearly always on the same generated line of a file called &lt;code&gt;context.tf&lt;/code&gt;. If you have read the earlier posts in this series, you have seen the 147 before: it is the corroborated landmark, not the news. The news is the graph around it. A drop from 147 consumers to 15 with nothing in between, two different centres, and a blast radius that fans across 61% of the org while staying one hop deep. That is the shape I want to plant before we go further.&lt;/p&gt;

&lt;p&gt;This is the third org this series has taken apart, and it is the sharpest shape yet. The first two were a hub-and-spoke org and a loose federation. Cloud Posse is a keystone: hundreds of independent modules, one convention module holding up the majority of them, on a line most reviews never read.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape of the org at a glance
&lt;/h2&gt;

&lt;p&gt;Across 242 repositories, Cloud Posse's public org resolves to 491 cross-repo dependencies pointing at 125 distinct artifacts. All but seven of those 125 are Terraform modules. This is an org that talks to itself almost entirely in Terraform.&lt;/p&gt;

&lt;p&gt;It is worth saying how thin that in-org signal is against everything Cloud Posse declares. Set beside those 491 resolved in-org edges are roughly 12,900 dependency declarations that resolve somewhere else entirely: the AWS provider, the public Terraform registry, upstream Docker images. That is a single aggregate figure, so read it as an order of magnitude rather than a precise ratio, but the order of magnitude is the point. Roughly twenty-six declarations pointing out of the org for every one that stays inside it. Cross-repo-within-the-org is the rare, interesting signal here, not the common one, which is exactly why nobody has it assembled in one place.&lt;/p&gt;

&lt;p&gt;Cloud Posse is a real business, and the description that matters is theirs, not mine. &lt;a href="https://github.com/cloudposse" rel="noopener noreferrer"&gt;Its GitHub org bio&lt;/a&gt; calls it a "DevOps Accelerator for AWS Infrastructure." &lt;a href="https://docs.cloudposse.com/community/contribute/automated-testing/" rel="noopener noreferrer"&gt;Its own docs&lt;/a&gt; describe running one test workflow "for all of our hundreds of terraform modules." Its flagship non-module project, &lt;a href="https://github.com/cloudposse/atmos" rel="noopener noreferrer"&gt;Atmos&lt;/a&gt;, is a "Terraform Orchestration Tool for DevOps" that composes those modules into deployable stacks. So the org is not one big system. It is hundreds of small, independent, publicly published &lt;code&gt;terraform-aws-*&lt;/code&gt; modules that mostly do not import each other. Which makes the one that they all import stand out.&lt;/p&gt;

&lt;h2&gt;
  
  
  The graph view: a star with two different centres
&lt;/h2&gt;

&lt;p&gt;Drawn as a graph, Cloud Posse is a star, and it has two different centres. One node that almost everything points at, and one node that points at almost everything. They are not the same repo.&lt;/p&gt;

&lt;p&gt;The first centre is the supplier. &lt;code&gt;terraform-null-label&lt;/code&gt; sits at 147 inbound edges, roughly ten times the next module's count. Almost every module in the library reaches back to it. The second centre is the consumer. &lt;code&gt;cloudposse/terraform-aws-components&lt;/code&gt;, Cloud Posse's reference-architecture "components" catalog, declares 110 outbound cross-repo dependencies, about eleven times the next-busiest repo (&lt;code&gt;cloudposse/.github&lt;/code&gt;, at 10). One repo supplies the org. A different repo assembles it. A supplier hub and a consumer hub, and reading a single repo's clone shows you neither, because both facts only exist in the relationships between repos.&lt;/p&gt;

&lt;p&gt;Of the 491 edges, 359 are Terraform &lt;code&gt;module { source = ... }&lt;/code&gt; blocks, which is 73% of everything. Another 36 are GitHub Actions &lt;code&gt;uses:&lt;/code&gt; references, 8 are Dockerfile &lt;code&gt;FROM&lt;/code&gt; lines, 4 are reusable-workflow &lt;code&gt;uses:&lt;/code&gt;, and exactly 1 is a &lt;code&gt;go.mod&lt;/code&gt; require. That is the declared graph: every one of those edges is read straight from a manifest the tooling already executes.&lt;/p&gt;

&lt;p&gt;The remaining 83 edges are not a parsed ecosystem at all, and I want to be careful not to dress them up as one. They are &lt;code&gt;git_url_reference&lt;/code&gt; edges: the output of a heuristic regex layer that scans arbitrary text, line by line, for GitHub URLs that point back into the org. A &lt;code&gt;git clone&lt;/code&gt; in a README, a fetch command in a shell script. Every one of those 83 sits at 0.6 or 0.7 confidence by design, because a URL in a comment is not the same kind of fact as a Terraform &lt;code&gt;source&lt;/code&gt; block. This is the &lt;a href="https://riftmap.dev/blog/declared-inferred-registered/" rel="noopener noreferrer"&gt;inferred layer sitting beneath the declared one&lt;/a&gt;, a safety net for edges no formal parser owns, and I keep it visibly separate from the declared graph rather than counting it as a sixth ecosystem. The &lt;a href="https://riftmap.dev/what-is-cross-repo-dependency-mapping/" rel="noopener noreferrer"&gt;cross-repo dependency graph&lt;/a&gt; that matters here is the 408 declared edges. The 83 heuristic ones are there so a real dependency written in a place no parser reads does not vanish silently.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern at the centre: one module, 147 consumers
&lt;/h2&gt;

&lt;p&gt;The centre of Cloud Posse is &lt;code&gt;terraform-null-label&lt;/code&gt;, declared by 147 of the 242 repos. The number is the landmark this series keeps returning to. The fresh part is the shape of the drop-off after it, and the drop-off is a cliff. Ranked by consumer count, the top of the in-degree table reads:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;terraform-null-label&lt;/code&gt;: 147 consumers&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;terraform-aws-route53-cluster-hostname&lt;/code&gt;: 15&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;terraform-aws-security-group&lt;/code&gt;: 13&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;terraform-aws-iam-role&lt;/code&gt;: 9&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;terraform-aws-s3-log-storage&lt;/code&gt; and &lt;code&gt;terraform-aws-sns-topic&lt;/code&gt;: 7 each&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The number two module is consumed by 15 repos. The keystone is consumed by 147. There is nothing in the middle. This is a genuinely different shape from the other two orgs in this series, and the difference is where the blast radius concentrates. &lt;a href="https://riftmap.dev/blog/what-56-prometheus-repos-depend-on/" rel="noopener noreferrer"&gt;Prometheus&lt;/a&gt; was hub-and-spoke: a few core repos, a readable ring of consumers around each, so losing any one hub takes down its own ring and no more. &lt;a href="https://riftmap.dev/blog/what-208-kubernetes-sigs-repos-actually-depend-on/" rel="noopener noreferrer"&gt;The kubernetes-sigs org&lt;/a&gt; was a federation: many sub-projects, dependencies spread thin and evenly, no single load-bearing centre to lose. Cloud Posse is neither. It is a keystone. Pull it and 61% of the org is standing on nothing at once, and there is no second load-bearing node to absorb the fall. Every one of those 147 modules would still parse, still plan, still look fine in its own repo. The dependency that decides the blast radius is the one nobody remembers is load-bearing, because it is boring and it has always been there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the receipts live: &lt;code&gt;context.tf&lt;/code&gt;, line 24
&lt;/h2&gt;

&lt;p&gt;Of the 147 Cloud Posse repos that declare &lt;code&gt;terraform-null-label&lt;/code&gt;, 133 declare it on the same line of the same file: &lt;code&gt;context.tf&lt;/code&gt;, line 24. This is not a coincidence and it is not inference. It is Cloud Posse's own documented convention. Their &lt;a href="https://docs.cloudposse.com/learn/conventions/" rel="noopener noreferrer"&gt;conventions doc&lt;/a&gt; explains it plainly: "By convention, we install this file as &lt;code&gt;context.tf&lt;/code&gt; which is why we call it the &lt;code&gt;context.tf&lt;/code&gt; pattern." A &lt;code&gt;context&lt;/code&gt; object carries the naming and tagging inputs, every &lt;code&gt;cloudposse/terraform-*&lt;/code&gt; module exposes it as &lt;code&gt;module.this&lt;/code&gt;, and the file that defines it is meant to be &lt;a href="https://docs.cloudposse.com/learn/component-development/faq/" rel="noopener noreferrer"&gt;copied byte-for-byte&lt;/a&gt; from the canonical copy in &lt;code&gt;terraform-null-label&lt;/code&gt; itself. The dependency is not authored per repo. It is generated, and the generator puts the &lt;code&gt;module "this"&lt;/code&gt; block in the same place every time.&lt;/p&gt;

&lt;p&gt;Here is the &lt;code&gt;module "this"&lt;/code&gt; block a real consumer carries, fetched live from &lt;code&gt;cloudposse/terraform-aws-route53-cluster-zone&lt;/code&gt;; the &lt;code&gt;source&lt;/code&gt; line is line 24:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;module&lt;/span&gt; &lt;span class="s2"&gt;"this"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"cloudposse/label/null"&lt;/span&gt;
  &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"0.25.0"&lt;/span&gt; &lt;span class="c1"&gt;# requires Terraform &amp;gt;= 0.13.0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;cloudposse/label/null&lt;/code&gt; string is a Terraform registry short-address, and it resolves to the &lt;code&gt;cloudposse/terraform-null-label&lt;/code&gt; repository through the registry's naming convention. Every one of the 133 repos that use &lt;code&gt;context.tf&lt;/code&gt; puts that block at line 24 exactly, with no exceptions in the population. The other 14 of the 147 declare the same dependency somewhere else: &lt;code&gt;main.tf&lt;/code&gt; in seven of them, &lt;code&gt;public.tf&lt;/code&gt; in two, and one each in a handful of other files, at various line numbers. So the honest quantifier is not "every one of the 147." It is 133 of 147, about nine in ten, on &lt;code&gt;context.tf&lt;/code&gt; line 24, and the remaining fourteen off the beaten path. The copy-paste convention is enforced by discipline and codegen, not by the language, which is exactly why a real scan finds a long tail that a "they all do it the same way" assumption would miss.&lt;/p&gt;

&lt;p&gt;The version story is just as legible from the receipts. Of the 147 consumers, 139 pin a clean semantic version and 8 pin a git-tag path like &lt;code&gt;tags/0.3.1&lt;/code&gt; rather than a bare version string. None float on a range — the same all-exact, never-&lt;code&gt;~&amp;gt;&lt;/code&gt; shape I found when I measured &lt;a href="https://riftmap.dev/blog/version-constraints-across-real-terraform-estates/" rel="noopener noreferrer"&gt;version constraints across four public Terraform estates&lt;/a&gt;, where cloudposse pins bare exact versions in 99.8% of its registry module blocks. 138 are on the current &lt;code&gt;0.25.0&lt;/code&gt;; 9 sit behind it. Those 9 are exactly the 8 git-tag pins, which can never string-match a semver release, plus one repo cleanly pinned to the previous &lt;code&gt;0.24.1&lt;/code&gt;. That reconciles a discrepancy an earlier post in this series flagged and left open: "9 lagging" is a recency cut and "8 on a git-tag path" is a syntax cut, and both are correct at once. They were never in conflict. They were counting two different things.&lt;/p&gt;

&lt;h2&gt;
  
  
  If I bump &lt;code&gt;terraform-null-label&lt;/code&gt;, what breaks?
&lt;/h2&gt;

&lt;p&gt;I opened a PR bumping &lt;code&gt;terraform-null-label&lt;/code&gt; to a new tag, then asked Riftmap what it would touch across the org. I ran the impact query the way &lt;a href="https://riftmap.dev/blog/blast-radius-gate-merge-pipeline/" rel="noopener noreferrer"&gt;Post B's merge gate runs it&lt;/a&gt;: confidence floor at 0.8, up to three hops out. The answer came back at 147 affected repositories, and every one of them at depth one. Nothing at depth two. Nothing at depth three.&lt;/p&gt;

&lt;p&gt;That flatness surprised me the first time, and then it made complete sense. The keystone does not cascade because Cloud Posse's high-fan-in modules mostly sit at a single layer rather than stacked in chains. The repos that consume &lt;code&gt;terraform-null-label&lt;/code&gt;'s direct consumers already are direct consumers themselves. The blast radius is exactly as wide as the direct-consumer count, and it stops there. Wide and flat. One file change, 61% of the org in scope, no transitive tail. I should be precise about what this call proves and what it does not: it enumerates the 147 repos whose declared dependency chain resolves back to the module. It does not prove each one would break on a version bump. It is the list of everything you now have to think about, not a verdict that all 147 are broken.&lt;/p&gt;

&lt;p&gt;Now the part this post exists to show. Take that same one-line bump and ask what each detection method would actually return.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Grep&lt;/strong&gt; gets you close to the headline count, and I am not going to pretend otherwise. &lt;code&gt;gh search code --owner cloudposse "cloudposse/label/null"&lt;/code&gt; would surface most of the 147, because for this particular construct the string genuinely is the receipt. That is the honest concession, and it is different from the Go or protobuf cases where the resolved edge is nowhere in the literal text. What grep cannot do is tell you that 8 of those matches pin a git-tag path and 139 pin a semver and none float, or which of the 133-versus-14 files and line numbers each match lives on, or how cleanly each reference resolved. And it cannot tell you how deep the chain runs. Grep returns an undifferentiated list of string matches with no depth attached, so it cannot tell a blast radius that stops at its direct consumers from one that runs several hops past them. That this one is flat is a finding the graph produced, not something grep could confirm. It finds the string. It does not assemble the graph, and it does not carry the state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A symbol graph&lt;/strong&gt; returns nothing, and this is the cleanest possible version of that claim. I have a lot of respect for Sourcegraph; its SCIP symbol graphs are excellent at the category they are in. But that category is programming-language symbols, and a Terraform &lt;code&gt;module&lt;/code&gt; block's &lt;code&gt;source&lt;/code&gt; is HCL configuration data, not a symbol in any language a SCIP indexer parses. Sourcegraph's own &lt;a href="https://sourcegraph.com/docs/code-navigation/writing-an-indexer" rel="noopener noreferrer"&gt;list of indexers&lt;/a&gt; runs scip-go, scip-typescript, scip-clang, scip-java, rust-analyzer, scip-python, scip-ruby, scip-dotnet. There is no Terraform or HCL indexer on it, and there is no partial-credit exception the way there is for &lt;code&gt;go.mod&lt;/code&gt; and &lt;code&gt;package.json&lt;/code&gt;, where the import path is itself a language symbol. A symbol graph does not resolve this edge poorly. It never looks at the file. &lt;a href="https://riftmap.dev/blog/symbol-graphs-and-artifact-graphs/" rel="noopener noreferrer"&gt;Symbol graphs and artifact graphs are different categories&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A catalog&lt;/strong&gt; returns whatever a human registered, which for this is almost certainly nothing. I want to be fair to catalogs first, because they hold things parsing cannot see: ownership, on-call, the tier of a service, which team you page. But a &lt;a href="https://backstage.io/docs/features/software-catalog/well-known-relations/" rel="noopener noreferrer"&gt;Backstage relation&lt;/a&gt;, in Backstage's own words, "is commonly generated based on &lt;code&gt;spec.dependsOn&lt;/code&gt; of the component or resource in question," and &lt;a href="https://docs.port.io/build-your-software-catalog/custom-integration/iac/terraform/" rel="noopener noreferrer"&gt;Port&lt;/a&gt; works the same way inverted, with a human writing the relation into their IaC. Nobody hand-registers 147 &lt;code&gt;context.tf&lt;/code&gt; module sources across 147 repos and keeps them current. So a catalog either has no record of this dependency or a stale, partial one, and you cannot tell which parts are current from inside the catalog.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Riftmap&lt;/strong&gt; returns the parsed graph, current as of the scan: 147 consumers, the 8 git-tag pins and the 139 semver pins, the 133 on &lt;code&gt;context.tf&lt;/code&gt; line 24 and the 14 elsewhere, the flat depth-one blast radius. Not because the count is secret knowledge. The value is not that 147 is un-greppable. The value is resolution, state, transitivity, and honest confidence, assembled once into one graph the org already declared but never had in one place. Which is the whole reason GitHub's Dependents tab still reads zero for it: the graph was always sitting in the manifests, and nobody had assembled it.&lt;/p&gt;

&lt;p&gt;&lt;span&gt;&lt;/span&gt;See it live&lt;/p&gt;

&lt;p&gt;This org, as an interactive graph&lt;/p&gt;

&lt;p&gt;The star shape above is not a diagram — it is the live Cloud Posse graph. Click terraform-null-label and watch 147 repos light up as its dependents, then simulate a breaking change to see the blast radius fan across 61% of the org, one hop deep.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://riftmap.dev/showcase/cloudposse/" rel="noopener noreferrer"&gt;Explore the Cloud Posse graph →&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What Riftmap didn't see, and why
&lt;/h2&gt;

&lt;p&gt;Cloud Posse produces eight kinds of artifact, Riftmap parsed all eight, and only three of them are consumed by another repo inside the org. The production footprint is genuinely multi-ecosystem: 156 Terraform modules, 154 Terraform root configs, 69 Helm charts, 60 generic artifacts, 19 Docker images, 15 Go modules, 11 npm packages, 2 GitLab CI templates. But only Terraform modules, Docker images, and Go modules show up as cross-repo edges at all. Of the 156 Terraform modules the org produces, 118 are consumed by a sibling repo; of the 19 Docker images, 6 are; of the 15 Go modules, 1 is. Zero of the 69 Helm charts, zero of the 11 npm packages, zero of the 154 Terraform root configs, and zero of the 2 GitLab CI templates are consumed by a sibling Cloud Posse repo in this scan.&lt;/p&gt;

&lt;p&gt;That is not a parser gap, and the why is worth being precise about, because the honest answer is more interesting than "we missed some." The Terraform root configs are terminal by design: a root module is meant to be deployed, not &lt;code&gt;source&lt;/code&gt;d by another module, so it is an orphan the same way a deployable stack is an orphan, on purpose. The Helm charts and npm packages most plausibly have real consumers, just not inside this org: Cloud Posse publishes charts and a CLI for the public to use, and an intra-org scan is structurally blind to consumption that happens outside the org boundary. That is the same intra-org caveat the first post in this series already carries. Separately, 43 of the 242 repos, about 18%, touch no cross-repo edge at all, either because they declare nothing the scan recognises as cross-repo or because everything they declare resolves outside the org. The scan skipped nothing and errored on nothing. What it did not draw, it did not draw for a reason it can name.&lt;/p&gt;

&lt;h2&gt;
  
  
  Methodology, briefly
&lt;/h2&gt;

&lt;p&gt;The scan read all 242 repos with zero skipped and zero errors, in about twelve and a half minutes. No model sits anywhere in that parsing path; every edge is read from a manifest construct, not inferred from text or names or embeddings.&lt;/p&gt;

&lt;p&gt;The one number worth dwelling on is the confidence distribution, because it is the opposite shape of the last org-scan and the difference is the interesting part. In this scan, 20.2% of edges resolve at confidence 1.0 and the dominant band is 0.9, at 61.1% of everything. That is not doubt. A confidence score on a parsed edge is not "do we think this edge is real," it is &lt;a href="https://riftmap.dev/blog/declared-inferred-registered/" rel="noopener noreferrer"&gt;how cleanly the reference resolved&lt;/a&gt; to a known target. And 300 of the 359 Terraform edges resolve through the registry short-address convention, turning &lt;code&gt;cloudposse/label/null&lt;/code&gt; into the &lt;code&gt;cloudposse/terraform-null-label&lt;/code&gt; repo, which the resolver grades 0.9 because it took a documented naming rule rather than an exact string match. A real, deterministic, declared edge, resolved by a rule instead of an identity. The heuristic &lt;code&gt;git_url_reference&lt;/code&gt; band is the only part sitting below the 0.8 deterministic floor, at 16.9%. Three org-scans in this series, three different confidence shapes, each one explained entirely by how that particular org names and resolves its own dependencies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two asks
&lt;/h2&gt;

&lt;p&gt;Two asks, both small. First, if you find an edge Riftmap drew wrong or missed on a public org, tell me. A misresolved short-address or an unparsed construct is a bug I want to fix, and I credit the report. The honesty beats above exist because someone has to go check the long tail, and a second pair of eyes on a public graph is worth more than another adjective. Second, and it is the ask that actually matters: run this same scan against an org you own, not one you are reading about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this sits in both series
&lt;/h2&gt;

&lt;p&gt;This post is the third entry in one series and the fourth in another. As "what an org actually depends on," it sits alongside the &lt;a href="https://riftmap.dev/blog/what-56-prometheus-repos-depend-on/" rel="noopener noreferrer"&gt;Prometheus&lt;/a&gt; and &lt;a href="https://riftmap.dev/blog/what-208-kubernetes-sigs-repos-actually-depend-on/" rel="noopener noreferrer"&gt;kubernetes-sigs&lt;/a&gt; scans, three real orgs with three genuinely different shapes: hub-and-spoke, federation, keystone. As the fourth "blast radius before merge" post, it is the worked org behind the argument the other three make in the abstract: &lt;a href="https://riftmap.dev/blog/can-ai-check-blast-radius-of-pr-before-merge/" rel="noopener noreferrer"&gt;Post A's three layers&lt;/a&gt;, where this post's contrast of what each detection method returns comes from; &lt;a href="https://riftmap.dev/blog/blast-radius-gate-merge-pipeline/" rel="noopener noreferrer"&gt;Post B's merge gate&lt;/a&gt;, whose impact contract produced the flat 147; and &lt;a href="https://riftmap.dev/blog/declared-inferred-registered/" rel="noopener noreferrer"&gt;Post C's declared, inferred, registered taxonomy&lt;/a&gt;, which is why the 83 heuristic edges sit apart from the 408 declared ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scan your own org
&lt;/h2&gt;

&lt;p&gt;You can run the same scan against your own org. Everything above came out of one read-only token, an org-level install, and about twelve and a half minutes, with no per-repo YAML to write and no catalog to maintain. If you want to know which repos break before you bump a shared module or a base image, including the dependency that used to live only in the head of the engineer who just left, &lt;a href="https://app.riftmap.dev/?utm_source=blog&amp;amp;utm_medium=cta&amp;amp;utm_campaign=scan&amp;amp;utm_content=what-242-cloudposse-repos-depend-on" rel="noopener noreferrer"&gt;point Riftmap at your org&lt;/a&gt; and look at your own keystone before you change it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;About Riftmap&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Riftmap maps cross-repo dependencies across your entire GitLab or GitHub&lt;br&gt;
organisation — Terraform, Docker, CI templates, Helm, and more. One read-only&lt;br&gt;
token. No YAML to maintain.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Common questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What does the Cloud Posse org depend on?&lt;/strong&gt;&lt;br&gt;
Internally, almost entirely on itself, in Terraform. A Riftmap scan of all 242 Cloud Posse repos (2026-07-04) found 491 cross-repo dependencies pointing at 125 distinct artifacts, and 73% of those edges are Terraform &lt;code&gt;module { source = ... }&lt;/code&gt; blocks. The single most-depended-on artifact is &lt;code&gt;cloudposse/terraform-null-label&lt;/code&gt;, the naming-and-tagging module that 147 of the 242 repos declare. Most of what the org declares actually resolves outside itself, to the AWS provider and public registries, so cross-repo-within-the-org is the rare signal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How many repos depend on terraform-null-label?&lt;/strong&gt;&lt;br&gt;
147 of Cloud Posse's 242 repos, which is 61% of the org, each declaring it through a Terraform &lt;code&gt;module { source = "cloudposse/label/null" }&lt;/code&gt; block. 133 of those 147 declare it on the same generated line, &lt;code&gt;context.tf&lt;/code&gt; line 24, following Cloud Posse's own copied-file convention. 138 are on the current &lt;code&gt;0.25.0&lt;/code&gt; and 9 sit behind it. GitHub's own Dependents tracker, by contrast, reports zero dependents for the repository, because it does not parse Terraform &lt;code&gt;source&lt;/code&gt; blocks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What breaks if you bump a Terraform module used across an org?&lt;/strong&gt;&lt;br&gt;
For &lt;code&gt;terraform-null-label&lt;/code&gt; specifically, the deterministic blast radius is 147 repositories, and all of them are direct consumers: at a 0.8 confidence floor over three hops, nothing appears at depth two or three, so the impact is wide but flat. That count enumerates every repo whose declared dependency chain resolves back to the module; it is the set you now have to review, not a verdict that all 147 will fail to plan. grep can approximate the count because the module source string is the receipt, but it cannot tell you which references are pinned to a git tag versus a semver, which file and line each lives on, or how deep the dependency chain runs. Here it is flat, but grep cannot tell a flat radius from a deep one because its matches carry no depth.&lt;/p&gt;

</description>
      <category>crossrepodependencies</category>
      <category>blastradius</category>
      <category>terraform</category>
      <category>dependencymapping</category>
    </item>
    <item>
      <title>Declared, inferred, registered: the three ways a tool knows a cross-repo dependency exists</title>
      <dc:creator>Daniel Westgaard</dc:creator>
      <pubDate>Sat, 04 Jul 2026 13:37:47 +0000</pubDate>
      <link>https://dev.to/danielwe/declared-inferred-registered-the-three-ways-a-tool-knows-a-cross-repo-dependency-exists-3ca7</link>
      <guid>https://dev.to/danielwe/declared-inferred-registered-the-three-ways-a-tool-knows-a-cross-repo-dependency-exists-3ca7</guid>
      <description>&lt;p&gt;Three lines were open in three tabs on my screen last week, and all three declared a dependency that crosses a repository boundary.&lt;/p&gt;

&lt;p&gt;The first was a Helm chart. In &lt;code&gt;argoproj/argo-helm&lt;/code&gt;, the &lt;code&gt;argo-cd&lt;/code&gt; chart's &lt;code&gt;Chart.yaml&lt;/code&gt; carries a &lt;code&gt;dependencies:&lt;/code&gt; block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;dependencies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis-ha&lt;/span&gt;
    &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;4.38.0&lt;/span&gt;
    &lt;span class="na"&gt;repository&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://dandydeveloper.github.io/charts/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second was Terraform. In &lt;code&gt;cloudposse/terraform-aws-vpc&lt;/code&gt;, the root &lt;code&gt;main.tf&lt;/code&gt; has a &lt;code&gt;module&lt;/code&gt; block whose &lt;code&gt;source&lt;/code&gt; points at another repo entirely:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;module&lt;/span&gt; &lt;span class="s2"&gt;"label"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"cloudposse/label/null"&lt;/span&gt;
  &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"0.25.0"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The third was a Dockerfile. In &lt;code&gt;cilium/cilium&lt;/code&gt;, &lt;code&gt;images/cilium/Dockerfile&lt;/code&gt; builds its release stage from a base image passed in as a build argument:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;${CILIUM_RUNTIME_IMAGE}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;release&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run &lt;code&gt;grep&lt;/code&gt; across the org for any of these and you get a partial answer. It finds the string &lt;code&gt;redis-ha&lt;/code&gt;, but not that the chart resolves against a &lt;code&gt;Chart.lock&lt;/code&gt; you would have to read separately. It finds &lt;code&gt;cloudposse/label/null&lt;/code&gt;, but has no idea that registry short-address maps to the &lt;code&gt;cloudposse/terraform-null-label&lt;/code&gt; repo. It finds &lt;code&gt;${CILIUM_RUNTIME_IMAGE}&lt;/code&gt; and stops, because the real image name is bound somewhere else. Point a symbol graph at the same three files and it finds nothing at all. None of these is a programming-language symbol. No compiler and no SCIP indexer parses a &lt;code&gt;Chart.yaml&lt;/code&gt;, an HCL &lt;code&gt;module&lt;/code&gt; block, or a Dockerfile instruction as source code.&lt;/p&gt;

&lt;p&gt;Here is the claim I want to plant before we go further. Before you merge and run a change, a cross-repo dependency can be known to a tool in three ways: &lt;strong&gt;declared&lt;/strong&gt; in a manifest the machine already executes, &lt;strong&gt;inferred&lt;/strong&gt; from statistical signal, or &lt;strong&gt;registered&lt;/strong&gt; in a catalog a human maintains. (A fourth mode, observing the edge at runtime, needs the change already running, which is exactly what you do not have before merge. More on that below.) Those three regimes are not three qualities of the same thing. They are three different answers to the question &lt;em&gt;how did the tool come to know this edge exists at all&lt;/em&gt;, and each one buys a different, structural failure mode.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Parsed, not inferred" stages a two-horse race and quietly drops a third runner
&lt;/h2&gt;

&lt;p&gt;The slogan I and half the industry reach for is "parsed, not inferred." It is a good slogan and it is doing less work than it sounds like. It stages a two-horse race: on one side the tool that reads what a manifest says, on the other the tool that guesses from embeddings and model output. That framing is real, but it hides the regime that quietly runs a large share of platform teams, which is neither parsed nor inferred. It is &lt;em&gt;registered&lt;/em&gt;: an edge some human typed into a catalog, that no machine executes and no model produced.&lt;/p&gt;

&lt;p&gt;So the honest split is three-way. Declared, inferred, registered. This is a different axis from the one I drew in &lt;a href="https://riftmap.dev/blog/can-ai-check-blast-radius-of-pr-before-merge/" rel="noopener noreferrer"&gt;an earlier post in this series&lt;/a&gt;, where the taxonomy was symbol / live-state / artifact. That split is about &lt;em&gt;which layer of the stack an edge lives on&lt;/em&gt;. This one is about &lt;em&gt;how a tool knows the edge is there&lt;/em&gt;. They compose. An artifact-layer edge can be declared, inferred, or registered, and the same Terraform &lt;code&gt;module source&lt;/code&gt; can show up in all three tools by three different routes. The rest of this post is about that second axis, because it is where the word "parsed" is quietly carrying an argument it never actually made.&lt;/p&gt;

&lt;h2&gt;
  
  
  Declared: the edge the machine already executes
&lt;/h2&gt;

&lt;p&gt;A declared dependency is one written into a manifest that the machine already reads and executes to do its job. Nobody adds a &lt;code&gt;FROM&lt;/code&gt; line to document a dependency. They add it because the build will not produce an image without it. The dependency edge is a side effect of a file that has to be correct for the system to run at all, which is what makes it deterministic. &lt;code&gt;terraform init&lt;/code&gt; resolves the &lt;code&gt;module source&lt;/code&gt; or the plan fails. &lt;code&gt;helm dependency update&lt;/code&gt; pulls the chart named in &lt;code&gt;dependencies:&lt;/code&gt; or the release is incomplete. The edge is not a description of the system. It is part of the system.&lt;/p&gt;

&lt;h3&gt;
  
  
  What counts as declared
&lt;/h3&gt;

&lt;p&gt;The declared regime is wide, and it is precise. Each ecosystem has its own construct, and the point is to name the construct rather than wave at "config files":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Helm &lt;code&gt;Chart.yaml&lt;/code&gt; &lt;code&gt;dependencies:&lt;/code&gt; entries (name, version, repository), which Riftmap reads as &lt;code&gt;helm_dependency&lt;/code&gt; edges.&lt;/li&gt;
&lt;li&gt;Terraform &lt;code&gt;module { source = ... }&lt;/code&gt; blocks, read as &lt;code&gt;terraform_module&lt;/code&gt; edges. Registry short-addresses and git URLs count as cross-repo; a bare &lt;code&gt;./&lt;/code&gt; local path does not, nor do full registry URLs like &lt;code&gt;registry.terraform.io/&lt;/code&gt; or &lt;code&gt;app.terraform.io/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Dockerfile &lt;code&gt;FROM&lt;/code&gt; lines, read as &lt;code&gt;docker_base_image&lt;/code&gt; edges.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;go.mod&lt;/code&gt; &lt;code&gt;require&lt;/code&gt; directives, and the &lt;code&gt;replace&lt;/code&gt; directives that quietly redirect them.&lt;/li&gt;
&lt;li&gt;GitHub Actions &lt;code&gt;uses:&lt;/code&gt; values, whether they point at &lt;code&gt;owner/repo@ref&lt;/code&gt; or a reusable &lt;code&gt;.github/workflows/x.yml@ref&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;GitLab CI &lt;code&gt;include:&lt;/code&gt; in its several forms (&lt;code&gt;project:&lt;/code&gt;, &lt;code&gt;remote:&lt;/code&gt;, &lt;code&gt;component:&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Kustomize remote &lt;code&gt;resources:&lt;/code&gt; and &lt;code&gt;bases:&lt;/code&gt;, read as &lt;code&gt;kustomize_resource&lt;/code&gt; edges.&lt;/li&gt;
&lt;li&gt;npm &lt;code&gt;package.json&lt;/code&gt; dependencies, including the &lt;code&gt;npm:&lt;/code&gt; alias and &lt;code&gt;git+&lt;/code&gt; forms where the imported name and the actual package differ.&lt;/li&gt;
&lt;li&gt;Ansible, where the precision matters. A role's &lt;code&gt;meta/main.yml&lt;/code&gt; &lt;code&gt;dependencies:&lt;/code&gt; list emits a role-to-role edge (&lt;code&gt;ansible_role&lt;/code&gt;) regardless of how the string is dotted. A task in a playbook that calls a three-segment FQCN like &lt;code&gt;polaris.infrastructure.deploy&lt;/code&gt; emits a collection edge (&lt;code&gt;ansible_collection&lt;/code&gt;). Two different files, two different edge types. An FQCN in &lt;code&gt;meta/main.yml&lt;/code&gt; still resolves as a role dependency and not a collection reference, because that file is what Ansible reads when it loads a role's dependencies.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That precision is the whole personality of the declared regime. The edge is not "there is a dependency somewhere in this YAML." It is a named construct with a known grammar and a known resolution step.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why grep only half-sees it
&lt;/h3&gt;

&lt;p&gt;Grep finds the string and misses the meaning, because in every one of these constructs the literal text is not the resolvable target. A Helm &lt;code&gt;version:&lt;/code&gt; is usually a semver range, not a pinned version. A Terraform registry short-address like &lt;code&gt;cloudposse/label/null&lt;/code&gt; has to be resolved through the registry's naming convention before you know which repo backs it. A Dockerfile &lt;code&gt;FROM ${VAR}&lt;/code&gt; names a variable, not an image. A GitLab CI &lt;code&gt;include:&lt;/code&gt; has five distinct shapes and an unqualified shorthand that silently resolves to local-or-remote depending on the string. An npm dependency can be declared under an alias, so the name in the code and the package actually installed are different strings. Grep sees text. The declared edge is text plus a resolution rule, and grep does not run the rule.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why a symbol graph misses it
&lt;/h3&gt;

&lt;p&gt;For most of these constructs, a symbol graph does not miss the edge so much as never look at it, because a symbol graph indexes programming-language symbols and none of these are symbols. Helm, Terraform, Docker, GitLab CI, GitHub Actions, Ansible, Kustomize. A compiler-accurate indexer like Sourcegraph's SCIP has nothing to say about any of them, because they are not code it compiles. This is not a knock on Sourcegraph. Symbol graphs and &lt;a href="https://riftmap.dev/blog/symbol-graphs-and-artifact-graphs/" rel="noopener noreferrer"&gt;artifact graphs are different categories&lt;/a&gt;, and Sourcegraph is genuinely excellent at the category it is in.&lt;/p&gt;

&lt;p&gt;I want to be fair about the two exceptions. For &lt;code&gt;go.mod&lt;/code&gt; and &lt;code&gt;package.json&lt;/code&gt;, the import path is itself a language-level symbol. Sourcegraph's own writeup on &lt;a href="https://sourcegraph.com/blog/cross-repository-code-navigation" rel="noopener noreferrer"&gt;cross-repository code navigation&lt;/a&gt; describes how SCIP's external symbols carry cross-repository dependency information across the languages it indexes, without calling out any ecosystem by name. A Go import path and an npm package name are exactly that kind of symbol, so I read those as edges a symbol graph &lt;em&gt;can&lt;/em&gt; resolve cross-repo when cross-repo indexing is configured. That is my inference from how Go and npm name their imports, not a claim on Sourcegraph's page about those two ecosystems. It is a real capability, and a heavier lift most installs skip. The manifest parser reads the literal &lt;code&gt;require&lt;/code&gt; or &lt;code&gt;dependencies&lt;/code&gt; value regardless of indexing, and it still catches the cases a symbol resolver handles less cleanly: the &lt;code&gt;npm:&lt;/code&gt; alias, the renamed module path, the non-registry git source. Different mechanisms, overlapping coverage, and I would rather concede the overlap than pretend it away.&lt;/p&gt;

&lt;p&gt;The scale is the part that does not fit in a code review. A full Riftmap scan of the &lt;code&gt;cloudposse&lt;/code&gt; GitHub org (242 repos, completed 2026-07-02) found 147 repos declaring a dependency on &lt;code&gt;cloudposse/terraform-null-label&lt;/code&gt; via a Terraform &lt;code&gt;module { source = "cloudposse/label/null" }&lt;/code&gt; block. 138 were on the current &lt;code&gt;0.25.0&lt;/code&gt;. 9 were pinned behind. Nearly every one of those references sits at the same place, &lt;code&gt;context.tf&lt;/code&gt; line 24, the line cloudposse's own module template generates. That is one declared edge, in one construct, in one org, repeated across 147 repos on a single templated line. It is exactly the kind of signal that is trivial to parse and impossible to hold in your head across 242 repositories. The full shape of that org — the cliff from 147 consumers down to 15, the flat depth-one blast radius, and what grep, a symbol graph, and a catalog each return for the same module — is its own scan writeup: &lt;a href="https://riftmap.dev/blog/what-242-cloudposse-repos-depend-on/" rel="noopener noreferrer"&gt;what 242 Cloud Posse repos actually depend on&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The honest failure mode of the declared regime is coverage. A parser is software. It only sees the ecosystems someone wrote a parser for, and it has the blind spots any parser has. If a team declares a dependency in a format nobody has written a parser for, the edge is real and the tool does not see it. That is a genuine limit, and it is a different kind of limit from the two that follow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Inferred: the edge guessed from statistical signal
&lt;/h2&gt;

&lt;p&gt;An inferred dependency is one a tool produces from statistical signal rather than reading it from a declaration. Embedding proximity. Name similarity. Model output. Co-change history. This is the regime that reaches for a coupling nobody wrote down in any artifact at all: two files that always move together in the commit history, a service whose vocabulary sits close to another's in embedding space, a natural-language question about the codebase answered from summaries rather than a parse. When there is no manifest entry and no catalog record to read, inference is the only thing left that can even suggest the edge exists. That is a real place on the map, and declared parsing does not stand on it.&lt;/p&gt;

&lt;p&gt;The failure mode is that inference has no ground truth. It produces a probability that an edge exists, and probabilities are wrong at a rate. This is measured, not folklore. When Richardeau et al. asked a range of LLMs to reproduce Zachary's Karate Club graph, every model got it wrong. The benchmark has 34 nodes and 78 known edges. The best model still added two edges that are not in the graph. Edge-count outputs across models ranged from 8 to 153 against a ground truth of 78 (&lt;a href="https://arxiv.org/abs/2409.00159" rel="noopener noreferrer"&gt;arXiv:2409.00159&lt;/a&gt;). In a code-specific setting it is sharper. On a 15-question architecture-discovery suite against the Shopizer repo, an AST-derived dependency graph scored 15 out of 15. An LLM-extracted knowledge graph scored 13. A vector-only baseline scored 6 (&lt;a href="https://arxiv.org/abs/2601.08773" rel="noopener noreferrer"&gt;arXiv:2601.08773&lt;/a&gt;). The same study documents a coverage failure distinct from being wrong: the LLM extraction pass skipped 377 files outright, so the graph it built was missing large parts of the dependency surface, not just occasionally mistaken about the parts it covered.&lt;/p&gt;

&lt;p&gt;The confidence score is the tell. An inferred edge comes with a number that means "how likely we think this edge is real," and that number is doing load-bearing work, because without it you cannot separate the edges the tool is sure about from the ones it guessed. Turn the threshold up and you drop real edges. Turn it down and you admit false ones. There is no setting that gives you both, because the underlying quantity is a belief, not a fact. Inference is the right tool when nothing is written down anywhere. It is strictly worse than reading the file when the edge is already declared, because guessing at an edge that is sitting in plain text can only add error to something you could have simply read.&lt;/p&gt;

&lt;h2&gt;
  
  
  Registered: the edge a human wrote in a catalog
&lt;/h2&gt;

&lt;p&gt;A registered dependency is one a human wrote into a catalog that no machine executes. Backstage represents it as &lt;code&gt;spec.dependsOn&lt;/code&gt; in a &lt;code&gt;catalog-info.yaml&lt;/code&gt;, which the catalog processor turns into a directional relation at ingestion. Port represents it as a relation between blueprints, single or many, which its docs frame as the software catalog as a dynamic graph database. And I want to concede the real thing first, because it is real: catalogs model relationships that parsing simply cannot see. Ownership. On-call. Which team you page. The tier of a service. There is no manifest the build executes that declares who owns a repo, and a good catalog is the right home for that.&lt;/p&gt;

&lt;p&gt;The failure mode is drift, because a registered edge is true only as of the last human edit, and nothing executes it to force a correction. This is not a competitor's insinuation. It is Backstage's own documented behaviour: &lt;a href="https://github.com/backstage/backstage/issues/20030" rel="noopener noreferrer"&gt;issue #20030&lt;/a&gt; describes how unregistering an entity leaves related entities carrying stale relationships until a later processing pass, and a Group page will show a live warning about relationships to entities that no longer exist. Port's CTO makes the maintenance case directly, though as an interested party. As he puts it on Port's blog, &lt;a href="https://www.port.io/blog/what-are-the-technical-disadvantages-of-backstage" rel="noopener noreferrer"&gt;"YAMLs require maintenance when code changes occur. This results in outdated information that can affect operations and decision-making…"&lt;/a&gt;. And from the adoption side, Roadie, a Backstage-ecosystem vendor and not a rival, reports two customers reaching &lt;a href="https://roadie.io/blog/3-strategies-for-a-complete-software-catalog" rel="noopener noreferrer"&gt;88% and 90% catalog completeness&lt;/a&gt; over roughly four months of active effort. That last number is the one I keep coming back to. Even funded, deliberate catalog work plateaus below 100%, because the catalog is a second job that competes with shipping, and the parts nobody remembered to update are silently indistinguishable from the parts that are current. I have written more on &lt;a href="https://riftmap.dev/blog/backstage-alternatives/" rel="noopener noreferrer"&gt;why teams quietly abandon the catalog&lt;/a&gt; elsewhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  A fourth mode: discovered at runtime, and why it is unavailable before merge
&lt;/h2&gt;

&lt;p&gt;There is a fourth way to know a cross-repo edge exists, and it is neither declared, inferred, nor registered: you can observe it at runtime. Because it lives on a different axis from the other three, I want to name it and set it aside cleanly rather than fold it into inference. A service mesh, DNS, live traffic, a database connection resolved in production. That edge is &lt;em&gt;discovered&lt;/em&gt; by watching the system run, and it is genuinely powerful, because it is the only thing that sees the undeclared HTTP calls a service makes to three others through environment variables injected at runtime, calls no manifest declares. This is the live-state layer, the subject of &lt;a href="https://riftmap.dev/blog/can-ai-check-blast-radius-of-pr-before-merge/" rel="noopener noreferrer"&gt;the first post in this series&lt;/a&gt;. &lt;a href="https://sixdegree.ai/blog/blast-radius-analysis" rel="noopener noreferrer"&gt;SixDegree&lt;/a&gt; calls it "discovered", and their tie-break rule, prefer discovered over declared when the two conflict, is correct for the question it answers. Runtime observation is righter than any manifest about what is talking to what &lt;em&gt;right now&lt;/em&gt;. What it cannot tell you is anything about a change that has not been merged yet, because you cannot observe the traffic of a base-image bump that does not exist in production. The thing you want the blast radius of has not run. That is why this series is about blast radius &lt;em&gt;before merge&lt;/em&gt;, and before merge the edges you can actually know are the declared, inferred, and registered ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does a parsed dependency edge need a confidence score?
&lt;/h2&gt;

&lt;p&gt;Riftmap parses deterministically and still puts a confidence score on every edge, and those two facts only sound contradictory until you see what the score measures. It is not inference confidence. There is no model, so the number can never mean "we think this edge is real." It is resolution confidence: how cleanly the declared reference matched a known target in your org. The resolver's own dataclass documents it in one line. &lt;code&gt;"""Resolution confidence. 1.0 = exact match; lower = heuristic."""&lt;/code&gt; Every value below it comes from a string or path comparison, an &lt;code&gt;if&lt;/code&gt;, not a probability.&lt;/p&gt;

&lt;p&gt;The external precedent for this distinction is, again, Sourcegraph. Their &lt;a href="https://sourcegraph.com/docs/code-navigation/precise-code-navigation" rel="noopener noreferrer"&gt;precise vs search-based code navigation&lt;/a&gt; split does the same thing one layer up. Precise navigation is compiler-accurate when a SCIP index exists. Search-based navigation is what Sourcegraph falls back to, in their own words, "when precise navigation is not available." Neither mode is doubt about whether a symbol is real. The distinction is match quality on how the reference was resolved, precise index versus heuristic search. A declared-edge resolution score is the same shape of thing, one layer down at the artifact level.&lt;/p&gt;

&lt;p&gt;This is where I need to reconcile something honestly, because &lt;a href="https://riftmap.dev/blog/blast-radius-gate-merge-pipeline/" rel="noopener noreferrer"&gt;an earlier post in this series set a merge gate at &lt;code&gt;min_confidence=0.8&lt;/code&gt;&lt;/a&gt;, and it would be easy to read that as "declared edges are always at least 0.8." They are not. The 0.8 floor excludes a separate regex-heuristic layer that scans files no formal parser owns, whose findings sit at 0.4 to 0.7 by design, plus a few declared edges that resolved fuzzily. The score moves for two deterministic reasons: ambiguous declaration syntax, or an imperfect string match to a known target. Neither reason is doubt about existence. The live cloudposse graph proves it: the &lt;code&gt;terraform-null-label&lt;/code&gt; edge I pulled from the production API resolves at &lt;strong&gt;0.9&lt;/strong&gt;, not because anyone is unsure the edge exists, but because turning &lt;code&gt;cloudposse/label/null&lt;/code&gt; into the &lt;code&gt;cloudposse/terraform-null-label&lt;/code&gt; repo took a documented naming-convention rule rather than an exact string match. A &lt;code&gt;${var}&lt;/code&gt;-templated Terraform &lt;code&gt;source&lt;/code&gt; lands at 0.5. The four &lt;code&gt;${VAR}&lt;/code&gt;-templated &lt;code&gt;FROM&lt;/code&gt; lines in that cilium Dockerfile land at 0.7, each of them a real, declared base-image edge whose confidence is lower only because the image name is bound through a build argument. The number answers "how cleanly did this resolve." It never answers "do we think this is real," because nothing in the pipeline is guessing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three regimes differ in what keeps each edge honest
&lt;/h2&gt;

&lt;p&gt;The three regimes differ in the thing that decides whether they stay true: what keeps each edge honest.&lt;/p&gt;

&lt;p&gt;A declared edge is kept honest by the machine that executes it. Get a &lt;code&gt;FROM&lt;/code&gt; line wrong and the build breaks. Get a &lt;code&gt;module source&lt;/code&gt; wrong and &lt;code&gt;terraform init&lt;/code&gt; fails. The manifest is not honest because humans are diligent about it. It is honest because it is load-bearing, and the same machine that consumes the edge re-reads it on every run. An inferred edge is kept honest by nothing. There is no build that fails when the model guesses wrong; you re-roll the dice and get a different graph. A registered edge is kept honest by human diligence alone. Nothing executes a catalog, so it rots at exactly the rate that attention wanders, which is quickly.&lt;/p&gt;

&lt;p&gt;I have to concede a point the research made me sharpen, because a reader who knows the build-dependency-error literature will catch it otherwise. Declared is not infallible. It is true &lt;em&gt;to the manifest&lt;/em&gt;, not true to the world. A Helm &lt;code&gt;dependencies:&lt;/code&gt; entry nobody pruned, a Terraform &lt;code&gt;module&lt;/code&gt; block whose &lt;code&gt;source&lt;/code&gt; still points at code no longer wired into any resource. These are declared-but-dead edges, the same failure shape a catalog has. Declared and registered are both things somebody wrote down, and both can be stale while still parsing cleanly. The difference is not that declared never goes stale. It is that a declared edge lives in the file the machine runs, so it is cheaper to keep current than a catalog is: nobody has to remember to edit it, because the machine re-reads it every time it runs, and a wrong one tends to announce itself by breaking something. A catalog entry that goes wrong just sits there, wrong and quiet.&lt;/p&gt;

&lt;p&gt;That is why the declared regime is the substrate I would want under an agent making a small cross-repo infra change. You cannot ask an agent to maintain a catalog, and you cannot trust it to guess. What you can do is hand it the edges the org already declared, kept current by the same machines that already depend on them being correct, including the base-image or shared-module dependency that used to live only in the head of the engineer who just left.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Riftmap sits
&lt;/h2&gt;

&lt;p&gt;Riftmap lives in the declared regime, on purpose. It reads the edges your manifests already declare. Terraform &lt;code&gt;module source&lt;/code&gt;, Dockerfile &lt;code&gt;FROM&lt;/code&gt;, Helm &lt;code&gt;dependencies:&lt;/code&gt;, GitLab CI &lt;code&gt;include:&lt;/code&gt;, and the rest. It reads them deterministically, with no model anywhere in the parsing path, across an entire GitHub or GitLab organisation from one read-only token. No catalog YAML to maintain, because the edges are parsed straight from the files that already exist and re-read on every scan. It is not trying to be the inference tool for undeclared runtime calls, and it is not a catalog. It is the substrate: the cross-repo artifact graph the org already declared but never had assembled in one place. If you want to see what your own org declares that no single repo's clone can show you, &lt;a href="https://app.riftmap.dev/?utm_source=blog&amp;amp;utm_medium=post&amp;amp;utm_campaign=blast-radius&amp;amp;utm_content=declared-inferred-registered" rel="noopener noreferrer"&gt;run a scan&lt;/a&gt; against a read-only token and look at the graph before you bump the next base image.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;About Riftmap&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Riftmap maps cross-repo dependencies across your entire GitLab or GitHub&lt;br&gt;
organisation — Terraform, Docker, CI templates, Helm, and more. One read-only&lt;br&gt;
token. No YAML to maintain.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Common questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What's the difference between declared, inferred, and registered dependencies?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A declared dependency is written into a manifest the machine already executes to do its job, like a Dockerfile &lt;code&gt;FROM&lt;/code&gt; line or a Terraform &lt;code&gt;module source&lt;/code&gt;, so it is deterministic and re-read on every run. An inferred dependency is guessed from statistical signal such as embeddings or LLM output, so it comes with a probability and no ground truth. A registered dependency is one a human typed into a catalog like Backstage or Port, which no machine executes, so it is accurate only as of the last edit. The three differ in what keeps the edge honest: the machine that runs it, nothing, or human diligence alone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do dependency-mapping tools actually detect dependencies, and are the edges parsed or inferred?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It depends on the tool, and "parsed vs inferred" hides a third option. Some tools parse the edge from a manifest declaration deterministically, some infer it from statistical signal like embeddings or model output, and some read it from a human-maintained catalog. Parsing gives you an edge that is true to the manifest and self-correcting because the machine re-reads it; inference gives you probabilistic coverage of couplings nothing declares; a catalog gives you relationships like ownership that neither can see, at the cost of drift. Observing an edge at runtime is a fourth mode, but it needs the change already running, so it cannot tell you the blast radius of something not yet merged.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why do grep and symbol graphs miss infrastructure dependencies?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Grep finds the literal string but not the resolution rule behind it: a Terraform registry short-address, a Helm semver range, or a Dockerfile &lt;code&gt;FROM ${VAR}&lt;/code&gt; is text plus a rule that maps it to an actual repo, and grep does not run the rule. Symbol graphs index programming-language symbols, and Helm, Terraform, Dockerfile, GitLab CI, GitHub Actions, Ansible, and Kustomize constructs are not symbols any compiler parses. For &lt;code&gt;go.mod&lt;/code&gt; and npm the import path is a language symbol, so a symbol graph can resolve those cross-repo when cross-repo indexing is configured, which most installs skip.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does a parsed dependency edge need a confidence score?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not to say whether the edge exists. A parsed edge is read from a declaration, not guessed, so there is no probability that it is real. A confidence score on a parsed edge measures resolution quality instead: how cleanly the declared reference matched a known target, where 1.0 is an exact match and lower means a documented heuristic like a naming convention was needed. That is the same distinction Sourcegraph draws between precise and search-based navigation, and it is a different quantity from the existence-probability an inference tool attaches to a guessed edge.&lt;/p&gt;

</description>
      <category>crossrepodependencies</category>
      <category>blastradius</category>
      <category>dependencygraph</category>
      <category>platformengineering</category>
    </item>
    <item>
      <title>How to add a blast-radius gate to your merge pipeline</title>
      <dc:creator>Daniel Westgaard</dc:creator>
      <pubDate>Fri, 03 Jul 2026 08:56:40 +0000</pubDate>
      <link>https://dev.to/danielwe/how-to-add-a-blast-radius-gate-to-your-merge-pipeline-4jm2</link>
      <guid>https://dev.to/danielwe/how-to-add-a-blast-radius-gate-to-your-merge-pipeline-4jm2</guid>
      <description>&lt;p&gt;&lt;em&gt;A pull request to a repository that a hundred others build on should not merge with one approval from a phone. Here is a CI gate that routes the review by measured downstream exposure, in two HTTP calls and about forty lines, on GitLab CI or GitHub Actions.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Someone opens a one-line pull request. It bumps the default in a shared Terraform module, or edits the &lt;code&gt;FROM&lt;/code&gt; line in a base image, or changes an &lt;code&gt;include&lt;/code&gt; in a CI template. The plan is clean. The diff is three characters. CI goes green, one reviewer approves on their phone between meetings, and it merges. Then the next &lt;code&gt;terraform init&lt;/code&gt; in six other repositories resolves the new version, and the people who own those repositories find out from their own pipelines.&lt;/p&gt;

&lt;p&gt;The change was correct in isolation. What went wrong was the review. A repository that a hundred others build on had exactly one person look at the thing before it shipped, and that person had no way to see, from inside the pull request, who was standing downstream.&lt;/p&gt;

&lt;p&gt;The industry's answer to this has arrived as a wave of pre-merge blast-radius gates, and they are worth taking seriously. &lt;a href="https://github.com/overmindtech/actions" rel="noopener noreferrer"&gt;Overmind&lt;/a&gt; ships a GitHub Action that submits each pull request's Terraform plan and comments the blast radius straight onto the PR. An &lt;a href="https://dev.to/aws-builders/terraform-plan-shows-what-youre-changing-blast-radius-shows-what-youre-breaking-3324"&gt;open-source project&lt;/a&gt; reads live dependency relationships out of AWS Config and fails the build with a threshold gate when a change fans out too far. Amazon's answer, after its own change-failure numbers moved, was blunter: require senior sign-off on AI-assisted changes from junior and mid-level engineers. Three gates, and every one of them checks a different graph.&lt;/p&gt;

&lt;p&gt;A blast-radius merge gate is only ever as good as the graph it queries. And for the class of change that most needs a second pair of eyes, a base image bump, a shared module rename, a CI-template edit, the graph you want is the artifact graph: which repositories declare a build-time dependency on the thing this pull request changes. That is the graph none of the gates above reads, because a &lt;code&gt;FROM&lt;/code&gt; bump has no Terraform plan and no running resource and no code symbol, and it is the one you can query from CI today in two HTTP calls. This post is &lt;a href="https://riftmap.dev/blog/can-ai-check-blast-radius-of-pr-before-merge/" rel="noopener noreferrer"&gt;Post A&lt;/a&gt; made operational: the three-graph argument, turned into a job you can paste into a pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gate is two GET requests
&lt;/h2&gt;

&lt;p&gt;The whole gate is two GET requests and a threshold. You need one thing that is not in the pipeline, a Riftmap graph of your organisation, which is a one-off read-only scan I will come back to at the end. Given that, the gate resolves itself, because both platforms hand a CI job the repository's own path for free. It is &lt;code&gt;$CI_PROJECT_PATH&lt;/code&gt; on GitLab and &lt;code&gt;${{ github.repository }}&lt;/code&gt; on GitHub Actions, and that is exactly what the lookup call takes. Nested GitLab subgroups are included: on a project at &lt;code&gt;platform/runtime/base-images&lt;/code&gt;, &lt;code&gt;$CI_PROJECT_PATH&lt;/code&gt; is that whole three-segment path, which is exactly the form the scan stores, so the lookup matches nested namespaces without any massaging.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Resolve owner/repo to its Riftmap id.&lt;/span&gt;
&lt;span class="nv"&gt;REPO_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-sf&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-API-Key: &lt;/span&gt;&lt;span class="nv"&gt;$RIFTMAP_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"https://api.riftmap.dev/api/v1/repositories/lookup?full_path=&lt;/span&gt;&lt;span class="nv"&gt;$REPO_PATH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.id'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# 2. Ask who declares a dependency on it.&lt;/span&gt;
curl &lt;span class="nt"&gt;-sf&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-API-Key: &lt;/span&gt;&lt;span class="nv"&gt;$RIFTMAP_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"https://api.riftmap.dev/api/v1/repositories/&lt;/span&gt;&lt;span class="nv"&gt;$REPO_ID&lt;/span&gt;&lt;span class="s2"&gt;/impact?max_depth=3&amp;amp;min_confidence=0.8"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The impact call walks the dependency graph outward from your repository and returns every repository that depends on it, each tagged with a &lt;code&gt;depth&lt;/code&gt; and a &lt;code&gt;confidence&lt;/code&gt;, plus a &lt;code&gt;total_affected&lt;/code&gt; count. Depth 1 is who breaks first: the repositories whose manifests name yours directly, a &lt;a href="https://riftmap.dev/blog/how-to-find-every-consumer-of-your-terraform-module/" rel="noopener noreferrer"&gt;&lt;code&gt;source&lt;/code&gt; block resolving to your module&lt;/a&gt;, a &lt;code&gt;FROM&lt;/code&gt; line pinned to your image, an &lt;code&gt;include&lt;/code&gt; pointing at your template. Deeper hops are the amplification. &lt;code&gt;min_confidence&lt;/code&gt; defaults to &lt;code&gt;0.8&lt;/code&gt;, which drops the heuristic matches and keeps the edges Riftmap parsed rather than guessed, and for a gate you want it there. (That number is resolution confidence, not existence probability, and the &lt;code&gt;0.8&lt;/code&gt; floor is doing something more specific than it looks; a companion post works through &lt;a href="https://riftmap.dev/blog/declared-inferred-registered/" rel="noopener noreferrer"&gt;how a tool knows an edge exists at all&lt;/a&gt; and what the score actually measures.)&lt;/p&gt;

&lt;p&gt;One thing has to be honest before you wire this to anything, because it decides whether the whole idea is useful or noise. The count is the standing consumer population as of the last scan, at the level of the whole repository. It is not a diff of which consumers your specific change breaks. A repository with 147 downstream consumers returns 147 whether this pull request renames an output every one of them uses or fixes a typo in a comment. So this is a gate on &lt;strong&gt;exposure, not on breakage&lt;/strong&gt;. Read the rest of this post with that framing and it stays sharp. Sell it to your team as a breakage detector, and the first person to run it on a busy shared repository will watch it fire on every pull request, including their own README fix, and quietly conclude the tool is broken. It is not measuring danger. It is measuring how many people a mistake here could reach.&lt;/p&gt;

&lt;h2&gt;
  
  
  The GitLab CI recipe
&lt;/h2&gt;

&lt;p&gt;Here is the entire gate as a GitLab CI job that runs on every merge request. It needs one thing configured, a masked CI/CD variable called &lt;code&gt;RIFTMAP_API_KEY&lt;/code&gt; holding a read-only Riftmap key (mint one labelled &lt;code&gt;ci&lt;/code&gt; so you can revoke it independently of the rest).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .gitlab-ci.yml&lt;/span&gt;
&lt;span class="na"&gt;blast-radius&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;alpine:3.20&lt;/span&gt;
  &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$CI_PIPELINE_SOURCE == "merge_request_event"&lt;/span&gt;
  &lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;RIFTMAP_BASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.riftmap.dev/api/v1"&lt;/span&gt;
    &lt;span class="na"&gt;THRESHOLD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;10"&lt;/span&gt;                    &lt;span class="c1"&gt;# direct consumers that warrant the review lane&lt;/span&gt;
  &lt;span class="na"&gt;before_script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;apk add --no-cache curl jq&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;# GitLab hands the job this repo's path for free.&lt;/span&gt;
      &lt;span class="s"&gt;REPO_ID=$(curl -sf -H "X-API-Key: $RIFTMAP_API_KEY" \&lt;/span&gt;
        &lt;span class="s"&gt;"$RIFTMAP_BASE_URL/repositories/lookup?full_path=$CI_PROJECT_PATH" | jq -r '.id')&lt;/span&gt;

      &lt;span class="s"&gt;# A repo Riftmap has not scanned yet returns nothing. Skip loudly rather than pass silently.&lt;/span&gt;
      &lt;span class="s"&gt;if [ -z "$REPO_ID" ] || [ "$REPO_ID" = "null" ]; then&lt;/span&gt;
        &lt;span class="s"&gt;echo "Repo not in the Riftmap graph yet; skipping blast-radius check."&lt;/span&gt;
        &lt;span class="s"&gt;exit 0&lt;/span&gt;
      &lt;span class="s"&gt;fi&lt;/span&gt;

      &lt;span class="s"&gt;IMPACT=$(curl -sf -H "X-API-Key: $RIFTMAP_API_KEY" \&lt;/span&gt;
        &lt;span class="s"&gt;"$RIFTMAP_BASE_URL/repositories/$REPO_ID/impact?max_depth=3&amp;amp;min_confidence=0.8")&lt;/span&gt;

      &lt;span class="s"&gt;DIRECT=$(echo "$IMPACT" | jq '[.affected_repositories[] | select(.depth == 1)] | length')&lt;/span&gt;
      &lt;span class="s"&gt;TOTAL=$(echo "$IMPACT"  | jq '.total_affected')&lt;/span&gt;

      &lt;span class="s"&gt;echo "Downstream consumers: $DIRECT direct, $TOTAL transitive."&lt;/span&gt;
      &lt;span class="s"&gt;if [ "$DIRECT" -ge "$THRESHOLD" ]; then&lt;/span&gt;
        &lt;span class="s"&gt;echo "Over threshold ($THRESHOLD): this change touches a high-fan-in repository."&lt;/span&gt;
      &lt;span class="s"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the complete gate. It resolves the repository, asks who depends on it, counts the direct consumers, and prints the number. It makes only GET requests, so it never trips Riftmap's rate limits, which apply to writes and not reads, and it holds no cloud credentials, because the cloud was never involved. The one guard that earns its place is the empty-&lt;code&gt;REPO_ID&lt;/code&gt; check: a repository the scan has not reached yet, and every brand-new repository is one of those, returns nothing from the lookup, and without the guard the rest of the job would quietly compute nothing and go green while printing blank counts. That is the single worst failure mode a gate can have, looking like it ran and found no exposure when it simply never ran. Skipping loudly keeps the gate honest about the one thing it is entitled to speak on, which is repositories Riftmap has actually scanned. The job then exits 0, which is deliberate. What to do with the number is the next section, and blocking the merge is the option you should reach for last, not first.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same gate in GitHub Actions
&lt;/h2&gt;

&lt;p&gt;The GitHub Actions version is the identical two calls wearing GitHub's pull-request plumbing. &lt;code&gt;curl&lt;/code&gt; and &lt;code&gt;jq&lt;/code&gt; are already on the &lt;code&gt;ubuntu-latest&lt;/code&gt; runner, so there is no install step, and the repository path arrives as &lt;code&gt;${{ github.repository }}&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/blast-radius.yml&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Blast radius&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;blast-radius&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;RIFTMAP_BASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://api.riftmap.dev/api/v1&lt;/span&gt;
      &lt;span class="na"&gt;THRESHOLD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;10"&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Measure downstream exposure&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;RIFTMAP_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.RIFTMAP_API_KEY }}&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;# GitHub hands the job this repo's path for free.&lt;/span&gt;
          &lt;span class="s"&gt;REPO_ID=$(curl -sf -H "X-API-Key: $RIFTMAP_API_KEY" \&lt;/span&gt;
            &lt;span class="s"&gt;"$RIFTMAP_BASE_URL/repositories/lookup?full_path=${{ github.repository }}" | jq -r '.id')&lt;/span&gt;

          &lt;span class="s"&gt;# A repo Riftmap has not scanned yet returns nothing. Skip loudly rather than pass silently.&lt;/span&gt;
          &lt;span class="s"&gt;if [ -z "$REPO_ID" ] || [ "$REPO_ID" = "null" ]; then&lt;/span&gt;
            &lt;span class="s"&gt;echo "Repo not in the Riftmap graph yet; skipping blast-radius check."&lt;/span&gt;
            &lt;span class="s"&gt;exit 0&lt;/span&gt;
          &lt;span class="s"&gt;fi&lt;/span&gt;

          &lt;span class="s"&gt;IMPACT=$(curl -sf -H "X-API-Key: $RIFTMAP_API_KEY" \&lt;/span&gt;
            &lt;span class="s"&gt;"$RIFTMAP_BASE_URL/repositories/$REPO_ID/impact?max_depth=3&amp;amp;min_confidence=0.8")&lt;/span&gt;

          &lt;span class="s"&gt;DIRECT=$(echo "$IMPACT" | jq '[.affected_repositories[] | select(.depth == 1)] | length')&lt;/span&gt;
          &lt;span class="s"&gt;TOTAL=$(echo "$IMPACT"  | jq '.total_affected')&lt;/span&gt;

          &lt;span class="s"&gt;echo "### Blast radius: $DIRECT direct consumers, $TOTAL transitive" &amp;gt;&amp;gt; "$GITHUB_STEP_SUMMARY"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Store the key as an Actions secret named &lt;code&gt;RIFTMAP_API_KEY&lt;/code&gt;. Same forty lines, same two calls, same read-only key. The only real difference between the platforms shows up when you want the gate to say something on the pull request rather than in the job log, which is where they diverge, and it is worth being exact about why.&lt;/p&gt;

&lt;h2&gt;
  
  
  The self-updating version of CODEOWNERS
&lt;/h2&gt;

&lt;p&gt;The useful version of this gate does not block the merge. It routes the review. The instinct with a number and a threshold is to fail the build, and that instinct is the one thing to unlearn here, because the far more valuable job the number can do is decide who should be looking at the change.&lt;/p&gt;

&lt;p&gt;Think about what &lt;code&gt;CODEOWNERS&lt;/code&gt; already does for you. It routes review by path: touch files under &lt;code&gt;modules/&lt;/code&gt;, and the platform team is added as a reviewer automatically. This gate routes review by measured downstream exposure instead: this repository currently has N repositories building on it, so a change to it deserves an owner of that shared surface on the pull request, not just whoever opened it. The difference from a hand-written &lt;code&gt;CODEOWNERS&lt;/code&gt; line is that the number tracks the graph. A module that grows from three consumers to forty crosses your threshold on its own, the day the fortieth repository adds the dependency, with nobody remembering to edit a rule. A module that loses its consumers drops out of the lane the same way. It is &lt;code&gt;CODEOWNERS&lt;/code&gt; that maintains itself against what is actually downstream, rather than against what someone believed was downstream the last time they touched the file.&lt;/p&gt;

&lt;p&gt;That reframing is also why the exposure-not-breakage limit from earlier stops mattering. Routing review by exposure never needed to know whether your change was breaking. It only needs to know how many teams are downstream, because that is what makes pulling in a senior reviewer proportionate. You are sizing a coordination cost, not predicting a failure, and the count is exactly the right instrument for sizing a coordination cost.&lt;/p&gt;

&lt;p&gt;Underneath the routing sits a policy split I &lt;a href="https://riftmap.dev/blog/ai-doesnt-understand-blast-radius/" rel="noopener noreferrer"&gt;proposed in an earlier post&lt;/a&gt; and never actually shipped. A change with no external consumers gets the fast lane, because there is nothing downstream to coordinate and speed is free. A change under the threshold passes with the consumer list posted as a courtesy, so the author knows what they are near. A change over the threshold, or one that touches a repository tagged customer-critical, engages the review lane. Amazon reached for seniority as its proxy because seniority is trivial to encode: junior author, therefore review. Downstream exposure is the proxy Amazon actually meant. A senior engineer changing a shared base image needs the extra eyes more than a junior engineer fixing a log line in a leaf service, and only the graph can tell those two apart.&lt;/p&gt;

&lt;p&gt;Turning the number into a review is where the platforms differ. On GitHub, the built-in token does it for free. Add &lt;code&gt;permissions: { contents: read, pull-requests: write }&lt;/code&gt; and one step:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;GH_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.token }}&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;PR=${{ github.event.pull_request.number }}&lt;/span&gt;
          &lt;span class="s"&gt;if [ "$DIRECT" -ge "$THRESHOLD" ]; then&lt;/span&gt;
            &lt;span class="s"&gt;gh pr edit "$PR" --add-label "blast-radius/high"&lt;/span&gt;
            &lt;span class="s"&gt;gh pr comment "$PR" --body \&lt;/span&gt;
              &lt;span class="s"&gt;"This change affects **$DIRECT** repositories directly ($TOTAL transitively). Engaging the shared-artifact review lane."&lt;/span&gt;
          &lt;span class="s"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On GitLab the merge-request notes API needs a token with &lt;code&gt;api&lt;/code&gt; scope, and the pipeline's own &lt;code&gt;$CI_JOB_TOKEN&lt;/code&gt; will not post notes, so store a project access token as a masked variable (&lt;code&gt;GITLAB_NOTE_TOKEN&lt;/code&gt;) and call the notes endpoint directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;      curl &lt;span class="nt"&gt;-sf&lt;/span&gt; &lt;span class="nt"&gt;--request&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"PRIVATE-TOKEN: &lt;/span&gt;&lt;span class="nv"&gt;$GITLAB_NOTE_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s2"&gt;"body=This change affects &lt;/span&gt;&lt;span class="nv"&gt;$DIRECT&lt;/span&gt;&lt;span class="s2"&gt; repositories directly (&lt;/span&gt;&lt;span class="nv"&gt;$TOTAL&lt;/span&gt;&lt;span class="s2"&gt; transitively)."&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CI_API_V4_URL&lt;/span&gt;&lt;span class="s2"&gt;/projects/&lt;/span&gt;&lt;span class="nv"&gt;$CI_PROJECT_ID&lt;/span&gt;&lt;span class="s2"&gt;/merge_requests/&lt;/span&gt;&lt;span class="nv"&gt;$CI_MERGE_REQUEST_IID&lt;/span&gt;&lt;span class="s2"&gt;/notes"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is what that comment looks like on a pull request to a genuinely high-fan-in repository. When Riftmap &lt;a href="https://riftmap.dev/blog/can-ai-check-blast-radius-of-pr-before-merge/" rel="noopener noreferrer"&gt;scanned Cloud Posse&lt;/a&gt;, &lt;code&gt;terraform-null-label&lt;/code&gt; came back with 147 direct consumers, 61% of the whole organisation:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Blast radius: 147 repositories build on this&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This change is to &lt;code&gt;terraform-null-label&lt;/code&gt;, which 147 repositories in the organisation declare as a direct dependency. Engaging the shared-artifact review lane.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is the comment the forty-line recipe produces from the impact call alone. Pull the consumer view for the artifact as well, the same shape the worked example in Post A returns, and the comment can carry the &lt;a href="https://riftmap.dev/blog/version-constraints-across-real-terraform-estates/" rel="noopener noreferrer"&gt;version detail&lt;/a&gt; that tells a reviewer where the coordination actually lands: of those 147, 138 are on the latest tag and 9 are lagging across six older ones. The reviewer arrives already knowing there are nine repositories to nudge onto the new version, not 147 to panic about. That is the difference between an exposure number and a coordination plan, and it is why the number is worth surfacing where a human will read it.&lt;/p&gt;

&lt;p&gt;Those numbers are a real scan, not a hypothetical. The whole org behind them — why the 147 stays flat at depth one, where the 133 identical &lt;code&gt;context.tf&lt;/code&gt; lines live, and what grep and a symbol graph each return for the same module — is walked through in &lt;a href="https://riftmap.dev/blog/what-242-cloudposse-repos-depend-on/" rel="noopener noreferrer"&gt;what 242 Cloud Posse repos actually depend on&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you do want the gate to hard-stop a merge, that is a one-line change and an opt-in, not the default. Exit non-zero over the threshold, and mark the job as a required check in branch protection or merge-request approval settings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;      &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DIRECT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-ge&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$THRESHOLD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1 &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start with the label and the comment. Reach for &lt;code&gt;exit 1&lt;/code&gt; only on the handful of repositories where a large blast radius genuinely should stop the world, and even then, expect to spend a week tuning &lt;code&gt;THRESHOLD&lt;/code&gt; before anyone trusts a red check that came from a consumer count.&lt;/p&gt;

&lt;p&gt;One last discipline, because the neighbours are already crossing it. The open-source AWS Config gate has an &lt;code&gt;ai-gate&lt;/code&gt; mode, and Port's guide has an LLM reason over catalogue relations and score the risk. Both are reasonable, and a written risk narrative is a genuinely nice thing to drop into a pull request. But it is a judgement, and you should not block a merge on a judgement that can vary between two runs on the same diff. The consumer count is not a judgement. It is a graph traversal that is either right or wrong about who declares the dependency, and it is the part you can safely automate a routing decision on. Gate on the enumeration. Treat the narrative as advice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this gate stops
&lt;/h2&gt;

&lt;p&gt;This gate sees one layer of dependency, and three kinds of blast radius sit outside it. Being exact about all three is the difference between a tool your team keeps and one they mute.&lt;/p&gt;

&lt;p&gt;The first is the one already covered: it enumerates, it does not diff. It answers "who is downstream," not "does this change break them." You keep the false alarms down by holding &lt;code&gt;min_confidence&lt;/code&gt; at &lt;code&gt;0.8&lt;/code&gt; and, if a particular repository is noisy, by only running the job when the files that actually declare interfaces change, through &lt;code&gt;paths:&lt;/code&gt; on GitHub or &lt;code&gt;changes:&lt;/code&gt; on GitLab. What you never do is describe it as catching breaking changes, because it does not, and the copy that says it does is the copy that gets the tool uninstalled.&lt;/p&gt;

&lt;p&gt;The second is freshness. The graph is a scan artifact, not live state, and a gate makes a merge decision, so eventual consistency with your scan cadence is a sharper caveat here than the same lag would be in a dashboard. State it symmetrically and it reads as engineering judgement rather than apology. Overmind and the AWS Config gate read live state, so their answer is current to the second, and they pay for it: both need cloud credentials inside the pipeline and a real plan step before they can say anything at all. Riftmap reads a graph built by a scan, so the answer is current to your scan cadence, and that is precisely why it is two GET requests with no cloud credentials in CI and no &lt;code&gt;terraform plan&lt;/code&gt; in the job. One buys freshness with access. The other buys cheapness with lag. For a coarse routing decision on a mature shared artifact, whose fan-in barely moves week to week, the lag is not what bites you, and you can surface it explicitly anyway. Every repository the lookup returns carries &lt;code&gt;last_scanned_at&lt;/code&gt; and &lt;code&gt;last_activity_at&lt;/code&gt;, so keep the whole lookup payload instead of pulling out only the id, and check the two against each other:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# capture the whole lookup response, not just .id&lt;/span&gt;
&lt;span class="nv"&gt;REPO&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-sf&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-API-Key: &lt;/span&gt;&lt;span class="nv"&gt;$RIFTMAP_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RIFTMAP_BASE_URL&lt;/span&gt;&lt;span class="s2"&gt;/repositories/lookup?full_path=&lt;/span&gt;&lt;span class="nv"&gt;$REPO_PATH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;REPO_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$REPO&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.id'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nv"&gt;STALE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$REPO&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | jq &lt;span class="s1"&gt;'if .last_activity_at &amp;gt; .last_scanned_at then true else false end'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$STALE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"true"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Note: this repo has changed since Riftmap last scanned it; the count may be behind."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The third is scope, and it is where the honest line with the live-state tools lives. This gate is artifact-scoped and source-scoped. Overmind's is plan-scoped and runtime-scoped. Overmind sees the resource someone created in the console that your plan is about to touch, and the &lt;a href="https://riftmap.dev/blog/gitlab-orbit-and-the-artifact-layer/" rel="noopener noreferrer"&gt;GitLab Blast Radius Reviewer&lt;/a&gt; walks the symbol graph and prunes any change with no public symbols, so it catches an exported function's callers and never sees a &lt;code&gt;FROM&lt;/code&gt; bump at all. This gate sees the six repositories whose &lt;code&gt;FROM&lt;/code&gt; line resolves to the image you just rebuilt, and cannot see a runtime HTTP call that no manifest declares. A serious platform team at scale plausibly wants more than one of these running side by side, because they are blind in opposite directions, and the artifact layer is the one that has been &lt;a href="https://riftmap.dev/blog/riftmap-vs-overmind/" rel="noopener noreferrer"&gt;empty until now&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pull requests nobody looks at twice
&lt;/h2&gt;

&lt;p&gt;A merge gate is a bet about which mistakes are worth stopping to look at. The blast-radius gates shipping this year all make that bet on a graph, and the graph decides which mistakes the gate can even see. Bet on the live-cloud graph and you catch the console resource and miss the cross-repo consumer. Bet on the artifact graph and you catch the change your reviewer, and your coding agent, both think is safe: the &lt;code&gt;FROM&lt;/code&gt; line, the &lt;code&gt;source&lt;/code&gt; ref, the shared module whose consumer list nobody has counted since the person who set it up handed in their notice. And it matters more for the agent than the human, because a person opening that pull request at least half-remembers what is downstream, whereas an agent making the same change from one repository's clone knows nothing about the other repositories at all. Wire the number to a review lane, and the pull requests with the largest blast radius stop being the ones that merge with a single approval from a phone. They become the ones the right person was pulled in to see.&lt;/p&gt;

&lt;p&gt;None of this runs until Riftmap has a graph of your organisation to answer the two calls, and that graph is the part worth having. It is one read-only token across your GitLab group or GitHub organisation, no per-repo config and no YAML catalogue to keep current, and once it exists the blast radius of any repository is a single API call. The recipe above is just the reason the scan pays for itself. And there is deliberately no Riftmap Action or GitLab component yet: the gate is forty lines you can read, own, and change, and I would rather ship that than a black box you have to trust. If you would use a maintained drop-in instead, tell me, and I will build it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Questions engineers actually ask
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;How do I add a blast radius check to my CI pipeline?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Compose two Riftmap API calls in a CI job. Look the repository up by its path (&lt;code&gt;$CI_PROJECT_PATH&lt;/code&gt; on GitLab, &lt;code&gt;${{ github.repository }}&lt;/code&gt; on GitHub Actions) to get its id, then call the impact endpoint to get every repository that declares a dependency on it. Count the direct consumers and comment or route on a threshold. It runs in seconds, needs only a read-only Riftmap key, and works the same whether a person or an agent opened the pull request.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I fail a pull request when it affects too many repos?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Set a threshold on the direct downstream consumer count, exit the CI job non-zero when a change is over it, and mark that job as a required check in branch protection. The more useful default is not to block but to route: keep the job advisory and use the number to request review from the owners of the shared surface when exposure is high. Gate on the consumer count, which is a deterministic graph traversal, rather than on an AI-generated risk score, which is a judgement that can vary between runs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need cloud credentials to check blast radius in CI?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not for the artifact layer. Riftmap answers from a dependency graph it built during a one-off scan of your organisation, so the pipeline needs only a read-only Riftmap API key and never touches your cloud. Live-state tools like Overmind and the AWS Config based gates do need cloud access, because they read the current state of your running infrastructure at pull-request time. The trade-off is scope: the artifact graph sees cross-repo build-time dependencies, live-state tools see runtime resources including ones created outside your IaC.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can an AI coding agent run the same blast-radius check?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes, and it matters more for the agent than for a human. A person opening the pull request often half-remembers what is downstream, whereas an agent making the same cross-repo change from a single repository's clone knows nothing about the other repositories at all. The same impact call gives either one the downstream consumer list before the merge, which is context the agent structurally cannot reconstruct on its own.&lt;/p&gt;

</description>
      <category>blastradius</category>
      <category>cicd</category>
      <category>platformengineering</category>
      <category>githubactions</category>
    </item>
    <item>
      <title>Can AI check the blast radius of a PR before you merge?</title>
      <dc:creator>Daniel Westgaard</dc:creator>
      <pubDate>Thu, 02 Jul 2026 14:22:46 +0000</pubDate>
      <link>https://dev.to/danielwe/can-ai-check-the-blast-radius-of-a-pr-before-you-merge-50k0</link>
      <guid>https://dev.to/danielwe/can-ai-check-the-blast-radius-of-a-pr-before-you-merge-50k0</guid>
      <description>&lt;p&gt;&lt;em&gt;Three products shipped the same promise this quarter: see the blast radius of your change before you merge. They mean three different dependency graphs — and each is blind to a kind of breakage the others catch. Picking a tool is really picking which graph gets consulted.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;In the past few weeks I have watched three different products promise the same sentence. Overmind's homepage now leads with engineers seeing the &lt;a href="https://overmind.tech" rel="noopener noreferrer"&gt;blast radius of their changes, before they merge&lt;/a&gt;, delivered by simulating the live cloud environment. A GitLab hackathon project shipped an MIT-licensed &lt;a href="https://medium.com/@poojabhavani19/pre-merge-blast-radius-analysis-with-the-gitlab-orbit-knowledge-graph-f49f3e181e89" rel="noopener noreferrer"&gt;Blast Radius Reviewer agent&lt;/a&gt; to the Duo catalog that runs pre-merge cross-project impact analysis on the Orbit knowledge graph. And Port published an &lt;a href="https://docs.port.io/guides/all/calculate-blast-radius-with-ai/" rel="noopener noreferrer"&gt;official guide&lt;/a&gt; to calculating blast radius with AI before production deploys.&lt;/p&gt;

&lt;p&gt;So, can AI check the blast radius of a PR before you merge? Yes, if there is a dependency graph for it to query. The interesting question is which graph, because at least three different graphs are being sold under that sentence right now, and they see different changes break.&lt;/p&gt;

&lt;p&gt;I build one of these tools, so I have an obvious interest here. I am going to try to earn your trust the boring way, by being precise about what each graph genuinely sees, where each one structurally stops, and then walking one real pre-merge check end to end on a real public organisation, with the actual API responses.&lt;/p&gt;

&lt;h2&gt;
  
  
  One phrase, three graphs
&lt;/h2&gt;

&lt;p&gt;"Blast radius before merge" currently describes at least three products that answer three different questions. The symbol layer asks what code calls the code you changed. The live-state layer asks what running cloud resources your plan touches. The artifact layer asks which repositories build on the thing you changed. Each layer is genuinely good at something the other two structurally cannot do, and none of them is lying to you. They are just answering different questions with the same vocabulary.&lt;/p&gt;

&lt;p&gt;I have made a version of this argument before, scoped to Terraform: &lt;a href="https://riftmap.dev/blog/riftmap-vs-overmind/" rel="noopener noreferrer"&gt;Terraform blast radius is three questions&lt;/a&gt;, the in-config graph, the live-cloud graph, and the cross-repo graph. A pull request is a more general object than a Terraform plan. A PR can be application code, which pulls the symbol layer into the picture, and the old in-config visualisers were never pre-merge gates in any serious sense. There is also a fourth kind of graph on the market, the modeled catalog, which I will come to at the end; it is less a layer than a maintenance regime. So for the question in this post's title, the map has three territories. Here is each one, honestly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The symbol layer: what code calls this code
&lt;/h2&gt;

&lt;p&gt;The symbol layer answers "who calls, imports, or references the code symbols this diff touches", and for application code it is the right layer to ask. The most interesting recent example is that Blast Radius Reviewer agent built on GitLab's Orbit knowledge graph. It extracts the public symbols from the diff, walks direct and transitive references outward up to three hops, weights risk by hop distance, pulls in reachable security findings and ownership, and lands a recommendation on the merge request. When Orbit is unavailable it degrades loudly to single-repo search and labels the result as partial rather than pretending it saw everything. It is well built, it is free, and it is exactly what I would want reviewing a Go or TypeScript change in a GitLab-native org. I have written before about how seriously I take &lt;a href="https://riftmap.dev/blog/gitlab-orbit-and-the-artifact-layer/" rel="noopener noreferrer"&gt;Orbit as a platform&lt;/a&gt;, and this agent is a good example of why.&lt;/p&gt;

&lt;p&gt;Now look at the assumption stated in its own writeup: changes that touch no public symbols are pruned early, because internal-only changes cannot have a cross-project blast radius. For application code, that is a reasonable heuristic. For infrastructure, it is exactly backwards. A Dockerfile &lt;code&gt;FROM&lt;/code&gt; line, a Terraform &lt;code&gt;source&lt;/code&gt; block, a Helm &lt;code&gt;Chart.yaml&lt;/code&gt; dependency, a GitLab CI &lt;code&gt;include&lt;/code&gt; directive: none of these is a public symbol. A diff that bumps them contains nothing for a symbol graph to traverse, so the highest-blast-radius PRs in an infrastructure estate are the ones this layer scores as impactless, by design rather than by defect.&lt;/p&gt;

&lt;p&gt;This is not a niche gap. When I &lt;a href="https://riftmap.dev/blog/cross-repo-edge-composition/" rel="noopener noreferrer"&gt;counted every cross-repo edge in two real organisations&lt;/a&gt;, not one was a code symbol. Symbol graphs and artifact graphs are &lt;a href="https://riftmap.dev/blog/symbol-graphs-and-artifact-graphs/" rel="noopener noreferrer"&gt;different categories&lt;/a&gt;, and the difference is the whole point of this post.&lt;/p&gt;

&lt;h2&gt;
  
  
  The live-state layer: what running resources the plan touches
&lt;/h2&gt;

&lt;p&gt;The live-state layer answers "what, in the cloud that is actually running, does this Terraform plan affect", and nothing else on this list can answer that. Overmind is the serious product here. It takes the plan JSON from your PR, queries your AWS, GCP, or Kubernetes environment in real time through read-only access, and maps the affected resources plus everything that depends on them, including resources created outside Terraform entirely, through the console or CloudFormation or a script someone ran in 2021. Those click-ops resources appear in no manifest anywhere. A parser can never find them, because there is nothing to parse. If your worry is "this plan will take down something nobody wrote down", Overmind's layer is the only one that can see it, and I have a lot of respect for it.&lt;/p&gt;

&lt;p&gt;Its structural boundary is the same thing that makes it powerful: it reasons from a plan against live state. The repositories that consume your module have not planned anything yet. When you change a shared module, the breakage does not happen in your plan. It happens later, in the plans of a hundred downstream repos, one at a time, as each of them eventually bumps. At the moment your PR is open there is no live signal in the place the damage will actually land. Overmind's graph is plan-scoped and runtime-scoped. Riftmap's is artifact-scoped and source-scoped. A serious platform team at scale plausibly wants both, and I mean that as a description of the architecture rather than as diplomacy. The &lt;a href="https://riftmap.dev/blog/riftmap-vs-overmind/" rel="noopener noreferrer"&gt;full comparison&lt;/a&gt; is its own post.&lt;/p&gt;

&lt;h2&gt;
  
  
  The artifact layer: which repos build on what you changed
&lt;/h2&gt;

&lt;p&gt;The artifact layer answers "which repositories declare a build-time dependency on the thing this PR changes", and in today's crop of pre-merge tools it is the question nobody else is answering. Nothing in the current wave walks Terraform &lt;code&gt;source&lt;/code&gt; blocks, Dockerfile &lt;code&gt;FROM&lt;/code&gt; lines, Helm chart dependencies, or CI &lt;code&gt;include&lt;/code&gt; directives across the repositories that were never checked out. So rather than argue it abstractly, here is one real pre-merge check, end to end.&lt;/p&gt;

&lt;p&gt;The organisation is &lt;a href="https://github.com/cloudposse" rel="noopener noreferrer"&gt;Cloud Posse&lt;/a&gt;, which maintains one of the largest public Terraform module estates there is. Riftmap scanned the org on 2026-07-04 with one read-only token: 242 repositories in about twelve and a half minutes, zero errors, no per-repo configuration.&lt;/p&gt;

&lt;p&gt;&amp;lt;!-- Figures re-confirmed 2026-07-05 against fresh full scan 54c4a2eb-9e44-44da-804d-975512082978 (2026-07-04; 242/242 repos, 0 skipped, 0 errors, 12m30.8s). Every headline number below is byte-identical to the original 2026-07-02 scan e9fa7ae3 (147 consumers = 61%, 138 on latest, 9 lagging, latest 0.25.0). Delta table: riftmap-content/posts/cloudposse-org-scan/evidence.md (Gate 4 / E8). --&amp;gt; The PR we will pretend to open is against &lt;a href="https://github.com/cloudposse/terraform-null-label" rel="noopener noreferrer"&gt;terraform-null-label&lt;/a&gt;, their naming and tagging convention module. Say it is a release-prep PR for a new tag that renames an output.&lt;/p&gt;

&lt;p&gt;Start with the two layers above, because the result is instructive. The diff touches HCL &lt;code&gt;output&lt;/code&gt; and &lt;code&gt;locals&lt;/code&gt; blocks, so there are no public symbols to extract; a symbol-layer reviewer prunes this change as having no cross-project impact. And the module declares no &lt;code&gt;resource&lt;/code&gt;, &lt;code&gt;data&lt;/code&gt;, or &lt;code&gt;module&lt;/code&gt; blocks at all, only &lt;code&gt;locals&lt;/code&gt;, &lt;code&gt;variable&lt;/code&gt;, and &lt;code&gt;output&lt;/code&gt; blocks. It provisions nothing, so &lt;code&gt;terraform plan&lt;/code&gt; shows nothing to add, change, or destroy, and there is no live infrastructure for a plan-simulation layer to map. Both layers report, correctly by their own definitions, that this PR touches nothing.&lt;/p&gt;

&lt;p&gt;Now ask the artifact graph:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"https://api.riftmap.dev/api/v1/repositories/{repo_id}/impact?max_depth=10"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$RIFTMAP_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"source_repository"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"terraform-null-label"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"full_path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cloudposse/terraform-null-label"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"affected_repositories"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"full_path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cloudposse/terraform-aws-acm-request-certificate"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"depth"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"confidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"full_path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cloudposse/terraform-aws-alb"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;                     &lt;/span&gt;&lt;span class="nl"&gt;"depth"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"confidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"full_path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cloudposse/terraform-aws-alb-ingress"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;             &lt;/span&gt;&lt;span class="nl"&gt;"depth"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"confidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"total_affected"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;147&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"max_depth_reached"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That response is real and only trimmed. 147 repositories, 61% of the org's 242 repos, declare terraform-null-label as a direct Terraform-module dependency (deduplicated, confidence ≥ 0.8, and intra-org only, a caveat I will come back to). The module that both other layers scored as impactless is the single highest-blast-radius artifact in the estate. Every one of those 147 edges sits at depth 1. This is not a deep chain amplifying a small number; it is 147 repositories importing one module directly, a module that contains no infrastructure at all, only the convention everything else is named by.&lt;/p&gt;

&lt;p&gt;The consumers view is where the pre-merge decision actually gets made, because it carries versions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"artifact"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"artifact_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"terraform_module"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cloudposse/terraform-null-label"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"consumer_count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;147&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"latest_version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0.25.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"consumers_on_latest"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;138&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"consumers_lagging"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"state_breakdown"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"pinned"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;139&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"floating"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"branch"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"absent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"consumers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"repository"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"full_path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cloudposse/terraform-aws-elastic-beanstalk-application"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"version_constraint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0.25.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="nl"&gt;"version_constraint_state"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pinned"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"source_file"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"context.tf"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"source_line"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"repository"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"full_path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cloudposse/terraform-aws-cloudwatch-flow-logs"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"version_constraint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tags/0.3.1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"version_constraint_state"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"branch"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"source_file"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"kinesis.tf"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nl"&gt;"source_line"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"repository"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"full_path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cloudposse/terraform-aws-multi-az-subnets"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"version_constraint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0.24.1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="nl"&gt;"version_constraint_state"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pinned"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"source_file"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"public.tf"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="nl"&gt;"source_line"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the &lt;code&gt;source_file&lt;/code&gt; and &lt;code&gt;source_line&lt;/code&gt; fields. Each edge points at the exact line of the exact manifest where the dependency is declared. Nothing here is guessed. The resolver collapses three declaration forms onto the same module: Terraform-registry version pins like &lt;code&gt;0.25.0&lt;/code&gt;, git-tag refs like &lt;code&gt;tags/0.3.1&lt;/code&gt;, and one raw git URL with no version at all (Cloud Posse's &lt;code&gt;geodesic&lt;/code&gt; image, which references the module that way and sits just outside these 147 as a lower-confidence edge).&lt;/p&gt;

&lt;p&gt;Two honest wrinkles, because precision is the product here. First, zero of the 147 consumers float a version range. Every single one hard-pins, so publishing a new tag breaks nobody at the moment of release. What the number tells you is something more useful: who has to coordinate the upgrade, and the version distribution tells you how well that coordination has gone historically. 138 consumers sit on the latest release and nine are scattered across six older tags, some dating back to the &lt;code&gt;tags/0.3.x&lt;/code&gt; era. Not one repo floats, yet the org is still fragmented across seven distinct versions. That drift is what &lt;a href="https://riftmap.dev/blog/version-constraints-across-real-terraform-estates/" rel="noopener noreferrer"&gt;version constraints look like in real estates&lt;/a&gt; generally, and it is the evidence that "everyone pins" and "everyone is current" are very different sentences.&lt;/p&gt;

&lt;p&gt;Second, when a consumer does bump, null-label's outputs feed resource names and tags, so the consumer's own plan can light up with renames, and at that moment, one consumer at a time, the live-state layer sees it. The artifact layer answers the question that comes before any of those plans exist: who has to open that PR at all.&lt;/p&gt;

&lt;p&gt;There is a fashionable objection to this layer, which is that parsed manifests capture declared dependencies rather than real ones. At the artifact layer the objection dissolves, because the declaration is the mechanism. The build executes the &lt;code&gt;FROM&lt;/code&gt; line. &lt;code&gt;terraform init&lt;/code&gt; resolves the &lt;code&gt;source&lt;/code&gt; block. The pipeline includes the &lt;code&gt;include&lt;/code&gt;. There is no separate runtime truth that the manifest is a stale snapshot of; the manifest is the instruction the machines follow. Undeclared runtime calls between services are a real blind spot, and they belong to a different layer of dependency entirely, one that runtime observation is the right tool for. Which repos build on your module is not a runtime question. It is written down, deterministically, at a file and line number, in repos you have never cloned. It just is not written down in the repo the PR is in, which is why neither the agent that opened the PR nor the reviewer reading it can see it, and why &lt;a href="https://riftmap.dev/blog/inferred-context-is-not-a-dependency-graph/" rel="noopener noreferrer"&gt;inferring it&lt;/a&gt; with embeddings or an LLM's confidence score is the wrong tool for a merge gate.&lt;/p&gt;

&lt;p&gt;The caveat I promised: all of these counts are intra-org. Cloud Posse's modules are used enormously across the public Terraform ecosystem, and none of that external usage appears in this scan. Within their own 242 repositories, the numbers above are exact. In the world, the true blast radius is far larger and nobody's graph sees all of it.&lt;/p&gt;

&lt;p&gt;The full scan behind this one worked example — the cliff from 147 consumers straight down to 15, the two different hubs one repo supplying the org and another assembling it, and what grep, a symbol graph, and a catalog each return for the same module — is its own post: &lt;a href="https://riftmap.dev/blog/what-242-cloudposse-repos-depend-on/" rel="noopener noreferrer"&gt;what 242 Cloud Posse repos actually depend on&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the catalogs sit
&lt;/h2&gt;

&lt;p&gt;Port's blast-radius guide is real and worth taking seriously, and it works on the fourth kind of graph I flagged earlier: a modeled one. You describe your services and their relations as catalog entities, and an AI agent reasons over those relations to produce a risk score and a deployment analysis. The output is a judgement, and the graph it judges from is as accurate as the last person who updated the catalog. I have written about &lt;a href="https://riftmap.dev/blog/modeled-graphs-and-parsed-graphs/" rel="noopener noreferrer"&gt;modeled graphs versus parsed graphs&lt;/a&gt; and about why hand-maintained catalogs &lt;a href="https://riftmap.dev/blog/the-catalog-maintenance-trap/" rel="noopener noreferrer"&gt;drift toward fiction&lt;/a&gt;, so I will keep it to one sentence here: a modeled graph answers with the accuracy of its YAML, and an enumeration beats a judgement anywhere an enumeration is available.&lt;/p&gt;

&lt;h2&gt;
  
  
  The blast radius the AI reports is the blast radius of the graph
&lt;/h2&gt;

&lt;p&gt;The answer to this post's title was never really in doubt. An agent can query any graph you hand it, and every product I have named will genuinely put something called a blast radius on your PR. What you are choosing when you pick one is not whether AI checks your change. It is which graph gets consulted, and therefore which category of breakage stays invisible. Symbol graphs cannot see the &lt;code&gt;FROM&lt;/code&gt; line. Live-state graphs cannot see the repos that have not planned yet. Artifact graphs cannot see the click-ops instance someone made in the console. The teams that get this right will not be the ones that bought the best AI. They will be the ones that knew which question their estate actually needed answered, and made sure a graph existed that could answer it before the merge button did.&lt;/p&gt;

&lt;p&gt;If your estate's risk lives where Cloud Posse's does, in the modules, images, charts, and templates that a hundred repos quietly build on, that graph is the artifact graph, and &lt;a href="https://riftmap.dev/blog/blast-radius-gate-merge-pipeline/" rel="noopener noreferrer"&gt;checking it from CI or an agent&lt;/a&gt; is one HTTP call.&lt;/p&gt;

&lt;p&gt;This post split the graphs by which layer an edge lives on; a companion piece works the orthogonal axis, &lt;a href="https://riftmap.dev/blog/declared-inferred-registered/" rel="noopener noreferrer"&gt;how a tool comes to know an edge exists at all&lt;/a&gt;, whether it is declared in a manifest, inferred from statistical signal, or registered in a catalog.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Can AI check the blast radius of a PR before you merge?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes, if there is a dependency graph for it to query at PR time. The AI resolves what the PR changes, requests the set of affected consumers from the graph, and reports the result before merge. Without a graph, an AI reviewer only sees the repository the PR is in, so cross-repo blast radius stays invisible until something breaks downstream. The practical question is which graph it queries: symbol graphs see code references, live-state graphs see running cloud resources, and artifact graphs see which repositories build on the changed artifact.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What tools show blast radius on a pull request before merge?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Three kinds, by layer. Symbol-layer tools like Sourcegraph and GitLab Orbit (including the open-source Blast Radius Reviewer agent) trace which code references the symbols in a diff. Live-state tools like Overmind simulate a Terraform plan against real-time AWS, GCP, or Kubernetes state and comment the affected resources on the PR. Artifact-layer tools like Riftmap enumerate the repositories whose manifests (Terraform &lt;code&gt;source&lt;/code&gt; blocks, Dockerfile &lt;code&gt;FROM&lt;/code&gt; lines, Helm dependencies, CI includes) consume what the PR changes. They answer different questions, and infrastructure-heavy estates usually need the third.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does Claude Code or Cursor know the blast radius of a change?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not on their own. Coding agents see the repository they have cloned or indexed, so a change to a shared module, base image, or CI template looks safe because the consumers live in repos the agent never opened. Given a queryable dependency graph, though, an agent can check blast radius at planning time with one API call and get the affected repositories back before it opens the PR. The agent is not the limitation; the missing graph is. More on that in &lt;a href="https://riftmap.dev/blog/which-ai-coding-assistants-see-blast-radius/" rel="noopener noreferrer"&gt;which AI coding assistants can see blast radius before they change code&lt;/a&gt;, where Claude Code, Cursor, and Copilot are each measured against this exact question.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is blast radius analysis deterministic or AI-generated?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It depends on the layer, and honest tools are clear about which parts are which. Graph traversal is deterministic: "147 repositories declare this module at these file and line numbers" is an enumeration, and it is either right or wrong. Risk narratives, severity scores, and deployment recommendations are judgements, usually LLM-generated, layered on top of whichever graph the tool has. A reasonable rule for merge gates: gate on the enumeration, treat the narrative as advice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's the best way to use AI to check blast radius before submitting a PR?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Give the AI a queryable dependency graph and have it check that graph at planning time, before it opens the PR, rather than asking the model to guess the blast radius from the diff. The agent resolves what the PR changes, asks the graph for the affected consumers, and reports the repositories back deterministically; for infrastructure changes that means an artifact graph that parses Terraform &lt;code&gt;source&lt;/code&gt; blocks, Dockerfile &lt;code&gt;FROM&lt;/code&gt; lines, Helm dependencies, and CI includes across the org. Gate the merge on the enumeration — which repositories consume what you changed — and treat any LLM-written risk narrative layered on top as advice, not the check.&lt;/p&gt;

</description>
      <category>blastradius</category>
      <category>impactanalysis</category>
      <category>pullrequests</category>
      <category>aicoding</category>
    </item>
    <item>
      <title>You can index every repo in Cursor. It still can't tell you what breaks.</title>
      <dc:creator>Daniel Westgaard</dc:creator>
      <pubDate>Wed, 01 Jul 2026 11:44:12 +0000</pubDate>
      <link>https://dev.to/danielwe/you-can-index-every-repo-in-cursor-it-still-cant-tell-you-what-breaks-4j5i</link>
      <guid>https://dev.to/danielwe/you-can-index-every-repo-in-cursor-it-still-cant-tell-you-what-breaks-4j5i</guid>
      <description>&lt;p&gt;&lt;em&gt;How to tune Cursor's codebase index for a large monorepo or a multi-repo workspace — and the one question no amount of tuning makes it answer.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Open a monorepo of any real size in Cursor and the first thing you meet is the indexing bar, and the first thing you learn is that it can take a while. A repository in the tens of thousands of files can sit there for a long time if you point Cursor at the root and walk away.&lt;/p&gt;

&lt;p&gt;So you do the sensible thing. You read the docs, you write a &lt;code&gt;.cursorignore&lt;/code&gt;, you open the package you actually work in instead of the whole tree, and the bar that took an age now takes a minute. &lt;code&gt;@Codebase&lt;/code&gt; gets faster and sharper. This is real work, it is worth doing, and most of this post is how to do it well.&lt;/p&gt;

&lt;p&gt;Then, one afternoon, you are about to change something shared. A base image half the org builds on. A module three Terraform stacks call. A contract package a dozen services import. And you ask the fast, well-tuned index the one question that actually matters before you press go. What breaks. And the answer comes back confident, and quick, and wrong in a way you cannot see.&lt;/p&gt;

&lt;p&gt;Here is the claim this post runs on. You can tune Cursor's index to cover a fifty-repo workspace and make &lt;code&gt;@Codebase&lt;/code&gt; genuinely fast and useful, and it still will not tell you which repositories break when you change a shared module, because the index answers similarity and a declared dependency is not a similarity relationship. Everything up to that ceiling is worth doing, and I will spend most of the post doing it. The ceiling itself is real, and no amount of tuning moves it, because it was never a tuning problem.&lt;/p&gt;

&lt;p&gt;There is a version of this post all over the internet right now, and most of it is good. The best one I have read walks &lt;a href="https://www.iamraghuveer.com/posts/cursor-codebase-indexing-monorepo/" rel="noopener noreferrer"&gt;multi-repo workspaces and per-service &lt;code&gt;.cursorignore&lt;/code&gt; files&lt;/a&gt; carefully and calls the result a microservices graph explorer. I want to be fair to that framing, because the tuning it describes is correct and I am about to repeat a fair amount of it. But "the index spans all my services" and "the index maps how my services depend on each other" are two different claims, and the distance between them is the whole second half of this post. I have made the &lt;a href="https://riftmap.dev/blog/the-repo-your-agent-didnt-clone/" rel="noopener noreferrer"&gt;structural version of that argument before&lt;/a&gt;, and shown &lt;a href="https://riftmap.dev/blog/claude-code-cursor-cross-repo-context/" rel="noopener noreferrer"&gt;how to wire the missing graph into Claude Code and Cursor&lt;/a&gt;. This one starts somewhere more practical. It starts with your index actually being slow.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Cursor's index actually is
&lt;/h2&gt;

&lt;p&gt;Cursor's index is a semantic search index, and knowing that precisely is what makes the rest of this post make sense. When you open a workspace, Cursor splits your code into chunks along syntactic boundaries, runs each chunk through a custom embedding model to get a vector, and stores those vectors in a &lt;a href="https://cursor.com/blog/secure-codebase-indexing" rel="noopener noreferrer"&gt;remote vector database&lt;/a&gt; built on Turbopuffer, keyed by an obfuscated path and line range. Your source is not kept server-side. Only the embeddings and masked metadata leave your machine, and the chunks are decrypted on the client when the agent needs them. A Merkle tree tracks which files changed so re-indexing only touches what moved, and the index syncs roughly every five minutes.&lt;/p&gt;

&lt;p&gt;When you search, your query becomes a vector too, and Cursor returns the chunks whose vectors sit nearest yours. That is what &lt;code&gt;@Codebase&lt;/code&gt; is underneath. Nearest-neighbour search over embeddings. The official description is exact about it: it returns the most semantically similar code, even when the matching chunk does not contain the words you searched for. Cursor's &lt;a href="https://cursor.com/blog/secure-codebase-indexing" rel="noopener noreferrer"&gt;own evaluation&lt;/a&gt; puts semantic search at around 12.5% more accurate than grep alone on large codebases, with the gain growing as the codebase grows, and that is a real result I have no interest in talking down.&lt;/p&gt;

&lt;p&gt;In 2026 this got more capable, and it is worth being current about, because the workflow changed. You mostly do not type &lt;code&gt;@Codebase&lt;/code&gt; any more. Cursor's Agent &lt;a href="https://cursor.com/docs/agent/tools/search" rel="noopener noreferrer"&gt;picks the search strategy itself&lt;/a&gt;, combining a fast custom grep it calls Instant Grep with semantic search, and it can spawn an Explore subagent that runs many searches in parallel without bloating the main context. Cursor's own line is that you do not choose the tool, you describe what you need and the Agent decides. This is a genuine improvement. But notice what did not change underneath the sophistication. Instant Grep matches strings. Semantic search matches meaning. Both are ways of finding text that resembles other text, and neither resolves a reference in one repository to the artifact another repository builds. The agent got much better at choosing which kind of resemblance to look for. It did not gain a new kind of edge to look over.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tuning the index for a large or multi-repo codebase
&lt;/h2&gt;

&lt;p&gt;The single lever that matters most on a large codebase is how much you ask Cursor to index, because both indexing cost and query noise scale with file count. Cursor's own numbers are blunt about the cost: a large repository indexed naively can take hours to reach its first query, and on the largest repos the ninety-ninth-percentile time-to-first-query is &lt;a href="https://cursor.com/blog/secure-codebase-indexing" rel="noopener noreferrer"&gt;over four hours&lt;/a&gt; before their teammate-index-sharing trick kicks in, with semantic search unavailable until the index is at least 80% built. One &lt;a href="https://www.rapidevelopers.com/cursor-tutorial/how-to-manage-cursor-ai-s-context-window-when-developing-large-monorepos-with-multiple-packages" rel="noopener noreferrer"&gt;monorepo tutorial&lt;/a&gt; clocks an 8,800-file repo at seven to twelve hours from the root, cut to minutes with the right exclusions. Everything below is a way of pointing the index at the code that is load-bearing for your task and keeping everything else out of it. Do them in roughly this order.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scope the index: open the package, not the root
&lt;/h3&gt;

&lt;p&gt;The highest-leverage move on a monorepo is to not open the monorepo. Opening a package directory as the workspace root makes Cursor treat that directory as the whole codebase and index only within it, and on a large tree that is the difference between a minute and several hours.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="c"&gt;# Indexes everything under the root, slowly
&lt;/span&gt;&lt;span class="n"&gt;cursor&lt;/span&gt; /&lt;span class="n"&gt;path&lt;/span&gt;/&lt;span class="n"&gt;to&lt;/span&gt;/&lt;span class="n"&gt;monorepo&lt;/span&gt;

&lt;span class="c"&gt;# Indexes one package, fast
&lt;/span&gt;&lt;span class="n"&gt;cursor&lt;/span&gt; /&lt;span class="n"&gt;path&lt;/span&gt;/&lt;span class="n"&gt;to&lt;/span&gt;/&lt;span class="n"&gt;monorepo&lt;/span&gt;/&lt;span class="n"&gt;packages&lt;/span&gt;/&lt;span class="n"&gt;api&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A shell alias per package you live in (&lt;code&gt;alias ca='cursor /path/to/monorepo/packages/api'&lt;/code&gt;) makes this frictionless. The cost is that references outside the package are no longer in the index, which is fine right up until your task actually crosses a package boundary, and then it is precisely the problem the second half of this post is about.&lt;/p&gt;

&lt;h3&gt;
  
  
  The two ignore files, and which one you actually want
&lt;/h3&gt;

&lt;p&gt;Cursor has two ignore files and they do different jobs, and mixing them up is the most common configuration mistake I see. &lt;code&gt;.cursorignore&lt;/code&gt; is a complete block: a file listed there is not indexed, not read, and not available even when you &lt;code&gt;@&lt;/code&gt;-mention it, as though it did not exist. &lt;code&gt;.cursorindexingignore&lt;/code&gt; is narrower: it keeps a file out of the index and out of search results, but the file stays readable, so you can still pull it in with &lt;code&gt;@Files&lt;/code&gt; when you genuinely need it.&lt;/p&gt;

&lt;p&gt;The practical rule the field has settled on is short. Reach for &lt;code&gt;.cursorindexingignore&lt;/code&gt; first, because it is the reversible choice, and promote a path to &lt;code&gt;.cursorignore&lt;/code&gt; only when the AI should never see it, like a secret.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="c"&gt;# .cursorindexingignore
# Kept out of the index, still reachable with @Files.
&lt;/span&gt;&lt;span class="n"&gt;tests&lt;/span&gt;/&lt;span class="n"&gt;fixtures&lt;/span&gt;/
&lt;span class="n"&gt;e2e&lt;/span&gt;/&lt;span class="n"&gt;recordings&lt;/span&gt;/
&lt;span class="n"&gt;packages&lt;/span&gt;/&lt;span class="n"&gt;legacy&lt;/span&gt;/

&lt;span class="c"&gt;# .cursorignore
# Invisible to indexing AND to all AI features.
&lt;/span&gt;.&lt;span class="n"&gt;env&lt;/span&gt;*
&lt;span class="n"&gt;secrets&lt;/span&gt;/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is one detail in Cursor's &lt;a href="https://cursor.com/docs/reference/ignore-file" rel="noopener noreferrer"&gt;default indexing exclusions&lt;/a&gt; worth knowing, because it is quietly on-topic. Cursor already skips lockfiles by default, &lt;code&gt;package-lock.json&lt;/code&gt;, &lt;code&gt;yarn.lock&lt;/code&gt;, &lt;code&gt;go.sum&lt;/code&gt;, and the rest. Those are the files that record the exact resolved version of every transitive dependency, which is to say the single most precise dependency information in your repository is the first thing the index throws away. It throws it away for sensible reasons, lockfiles are enormous and read as noise to a similarity search. Hold onto that, though, because it is a small preview of the larger point. The index is tuned to find code that reads like your question, and dependency records do not read like anything.&lt;/p&gt;

&lt;h3&gt;
  
  
  Multiple repositories: the multi-root workspace
&lt;/h3&gt;

&lt;p&gt;For genuinely separate repositories, rather than packages in one tree, Cursor supports &lt;a href="https://cursor.com/docs/agent/tools/search" rel="noopener noreferrer"&gt;multi-root workspaces&lt;/a&gt;. A &lt;code&gt;.code-workspace&lt;/code&gt; file lists several folder roots, Cursor indexes all of them, and Agent can reach across the set. A workable pattern for a large estate is to group repositories into a few workspace files by domain, payments with identity with the API gateway in one, catalogue with search with recommendations in another, and switch between them rather than opening everything at once. One caveat to know going in: features that assume a single git root, like worktrees, are disabled in a multi-root workspace.&lt;/p&gt;

&lt;p&gt;Done well, this is genuinely useful. Open &lt;code&gt;web-app&lt;/code&gt;, &lt;code&gt;orders-service&lt;/code&gt;, and the shared contract repo together, and "how does the orders service validate a token" becomes one question instead of four context switches. This is the setup the multi-repo guides call a microservices graph explorer, and I understand why they reach for it. When every service sits in one index, &lt;code&gt;@Codebase&lt;/code&gt; stops being a single-service lookup and starts answering questions that range across the whole set.&lt;/p&gt;

&lt;p&gt;But the word doing too much work in "microservices graph explorer" is graph. The index now spans your services. It does not map them. It can surface the code in &lt;code&gt;orders-service&lt;/code&gt; that mentions the contract, and the code in the contract repo that defines it, because both are text and both might resemble your query. What it cannot do is tell you that &lt;code&gt;orders-service&lt;/code&gt; declares a dependency on that contract, and that &lt;code&gt;billing-service&lt;/code&gt;, which you did not open, declares one too. Spanning a set of repositories and mapping the edges between them are different operations, and the index only performs the first one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Project rules and hierarchical ignore
&lt;/h3&gt;

&lt;p&gt;Project rules and hierarchical ignore are two smaller levers worth setting. Rules live in &lt;a href="https://cursor.com/docs/context/rules" rel="noopener noreferrer"&gt;&lt;code&gt;.cursor/rules/*.mdc&lt;/code&gt;&lt;/a&gt; now, one or more files that describe your architecture and conventions and load into context by relevance. If you are still carrying a root &lt;code&gt;.cursorrules&lt;/code&gt; file, note that it is legacy and ignored in Agent mode, so migrating it is overdue. Rules are where you tell the agent how the monorepo fits together and which boundaries not to cross, and they do help. Notice, though, that they help by you writing the structure down, which makes them a hand-maintained description with the same &lt;a href="https://riftmap.dev/blog/the-catalog-maintenance-trap/" rel="noopener noreferrer"&gt;decay problem every written map has&lt;/a&gt;: the description is only as current as the last engineer to update it, and it drifts from the code at exactly the speed the code changes.&lt;/p&gt;

&lt;p&gt;Hierarchical Cursor Ignore, a setting rather than a file, lets Cursor walk up parent directories collecting &lt;code&gt;.cursorignore&lt;/code&gt; files, so you can keep a global exclusion set at the root of a large monorepo and let each package layer its own on top. It is the right tool for keeping a big tree's index configuration from repeating itself.&lt;/p&gt;

&lt;h3&gt;
  
  
  The freshness you're actually working with
&lt;/h3&gt;

&lt;p&gt;Cursor's index is as current as your checkout, and no more, and it is worth being honest with yourself about what that means. The index reflects the files on your disk. It does not pull your colleagues' commits, so a function a teammate added to the identity service this morning is not in your index until you pull and Cursor re-indexes the file. And even for your own work the index trails the actual files by a sync interval. The consequence is a clean line: the index is reliable for the stable shape of a system, and unreliable for the change that landed an hour ago in a repository you did not open. Both of those are outside it. Keep that boundary in mind, because it compounds with the one the next section is about.&lt;/p&gt;

&lt;h2&gt;
  
  
  The question a tuned index still can't answer
&lt;/h2&gt;

&lt;p&gt;A perfectly tuned Cursor index still cannot tell you which repositories break when you change a shared module, because it answers similarity and a cross-repo dependency is a declared edge, not a similarity relationship. Do all of it. Scope the index to the package, split the workspace by domain, get the file count into the low thousands, write the rules, keep it fresh. You now have a fast, lean, accurate semantic index across every repository you care about, and &lt;code&gt;@Codebase&lt;/code&gt; is as good as it gets. Now ask it the question you opened this whole workflow to answer. You are about to bump a base image, retire a shared module, or change a contract. Which repositories break.&lt;/p&gt;

&lt;p&gt;Here is what happens, concretely, and you can check me on it because the org is public. Take the &lt;a href="https://riftmap.dev/showcase/prometheus/" rel="noopener noreferrer"&gt;Prometheus organisation&lt;/a&gt;: as of Riftmap's May 2026 scan, fifty-six repositories, and when you &lt;a href="https://riftmap.dev/blog/what-56-prometheus-repos-depend-on/" rel="noopener noreferrer"&gt;parse the dependency edges&lt;/a&gt; between them, a hundred and eighty-eight cross-repository edges. A handful of repositories carry most of it, &lt;code&gt;prometheus/common&lt;/code&gt; with twenty-five dependents, &lt;code&gt;client_model&lt;/code&gt; with twenty-four, &lt;code&gt;procfs&lt;/code&gt; with twenty-three, &lt;code&gt;client_golang&lt;/code&gt; with twenty-two. (Those counts drift a little between scans, which is exactly why they are dated here; the &lt;a href="https://riftmap.dev/showcase/prometheus/" rel="noopener noreferrer"&gt;live showcase&lt;/a&gt; always renders the current number.) Clone all fifty-six, open them in one immaculately tuned Cursor multi-root workspace, wait for the index to finish, and ask &lt;code&gt;@Codebase&lt;/code&gt;: what depends on &lt;code&gt;client_golang&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You will get chunks. Files that mention &lt;code&gt;client_golang&lt;/code&gt;, code that resembles your query, the definition itself. What you will not get is the list of twenty-two repositories that declare a &lt;code&gt;require&lt;/code&gt; on it, because that list is not a similarity relationship. It is twenty-two &lt;code&gt;go.mod&lt;/code&gt; files, in twenty-two repositories, each with a line naming &lt;code&gt;client_golang&lt;/code&gt; and a version. A &lt;code&gt;go.mod&lt;/code&gt; require line and the &lt;code&gt;client_golang&lt;/code&gt; source it points at share almost no tokens, nothing an embedding would place near the other. The edge is not latent in the text, waiting to be retrieved. It was declared once, in a manifest, and it is either parsed from that manifest or it is not found.&lt;/p&gt;

&lt;p&gt;And it is finer than a repository count, which is the part that should give you pause before a change. The same scan finds &lt;code&gt;prometheus/common&lt;/code&gt; required twice from &lt;code&gt;prometheus/prometheus&lt;/code&gt; alone, two separate manifests in one repository, each naming &lt;code&gt;common&lt;/code&gt; with its own version, and the graph tracks them as two references rather than folding them into a single repo-to-repo edge.&lt;/p&gt;

&lt;p&gt;"We bumped the dependency" and "we bumped every reference to the dependency" are different statements, and Go monorepos are exactly where that difference hides. A parser surfaces each of those references as its own edge, because it read each manifest and knows how many there are. A similarity index has no concept of "the second &lt;code&gt;go.mod&lt;/code&gt; that requires this". It has chunks, ranked by resemblance, and resemblance was never going to count references in files it treats as prose.&lt;/p&gt;

&lt;p&gt;This is not a tuning failure, and that distinction matters. There is no &lt;code&gt;.cursorignore&lt;/code&gt; you could write, no workspace split, no rule, that turns a nearest-neighbour search into a dependency resolver. The index is answering the question it was built to answer, which is "what code resembles this", and it answers it well. The question a breaking change asks is "what declares a dependency on this", and that is a different question with a different data structure behind it. You cannot tune your way from one to the other, because they were never the same machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  The graph that answers it is parsed, not tuned
&lt;/h2&gt;

&lt;p&gt;The edges the index can't retrieve are not missing, they are declared in the manifests you already have, in constructs built for exactly this. The contract in a &lt;code&gt;go.mod&lt;/code&gt; require or a &lt;code&gt;package.json&lt;/code&gt; dependency. The module in a Terraform &lt;code&gt;source&lt;/code&gt; block. The image in a Dockerfile &lt;code&gt;FROM&lt;/code&gt;. The chart in a &lt;code&gt;Chart.yaml&lt;/code&gt; dependency. The template in a GitLab CI &lt;code&gt;include:project&lt;/code&gt; or a reusable Actions &lt;code&gt;uses:&lt;/code&gt;. I spent &lt;a href="https://riftmap.dev/blog/series/find-every-consumer/" rel="noopener noreferrer"&gt;a whole series&lt;/a&gt; walking those one ecosystem at a time. They are deterministic. Parsed, not inferred. The dependency graph across your organisation already exists, declared and unassembled, in files a similarity index reads as text and a parser reads as edges. This is the difference between &lt;a href="https://riftmap.dev/blog/inferred-context-is-not-a-dependency-graph/" rel="noopener noreferrer"&gt;inferred context and a dependency graph&lt;/a&gt;, and tuning the index is orthogonal to it: a better index is a better answer to a different question.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://riftmap.dev/for-agents/" rel="noopener noreferrer"&gt;Riftmap&lt;/a&gt; is that graph. It parses those manifests across your entire GitHub or GitLab organisation from one read-only token, resolves each reference to the repository that owns the artifact, and returns the answer the index can't, every repository that depends on the thing you are about to change, with the version each one declared. It is the &lt;a href="https://riftmap.dev/showcase/prometheus/" rel="noopener noreferrer"&gt;Prometheus graph above&lt;/a&gt;, for your own org. If you want that graph in front of the agent rather than in a browser tab, wiring it into Cursor and Claude Code is &lt;a href="https://riftmap.dev/blog/claude-code-cursor-cross-repo-context/" rel="noopener noreferrer"&gt;its own post&lt;/a&gt;, because the graph is useful to the engineer holding the pager first and the agent second. Either way it is the same move. Stop asking a tool built for resemblance to answer a question about dependency, and hand over a graph that was parsed for exactly that.&lt;/p&gt;

&lt;p&gt;Tune Cursor's index until it is perfect and you have made it excellent at finding the code that resembles your question. The repositories that break when you change a shared module were never going to resemble your question. They were a set of &lt;code&gt;FROM&lt;/code&gt; lines and &lt;code&gt;require&lt;/code&gt; blocks and &lt;code&gt;source&lt;/code&gt; references, declared once across repositories you may not have even opened, waiting to be read. The index reads them as text. You need something that reads them as edges.&lt;/p&gt;

&lt;h2&gt;
  
  
  Questions teams ask
&lt;/h2&gt;

&lt;p&gt;The same questions come up whenever I help someone tune this, so here they are, answered straight.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I speed up Cursor indexing on a large monorepo?&lt;/strong&gt; Index less. A repository in the tens of thousands of files can take hours to index from the root, so the highest-leverage move is to open the package you work in as the workspace root rather than the whole tree. Add a &lt;code&gt;.cursorignore&lt;/code&gt; for build output, dependencies, and generated files, aim for a few thousand indexed files rather than tens of thousands, and use &lt;code&gt;.cursorindexingignore&lt;/code&gt; for large directories you still want to &lt;code&gt;@&lt;/code&gt;-mention occasionally. Most slow-index complaints come down to indexing &lt;code&gt;node_modules&lt;/code&gt; and vendored code nobody needed in the index in the first place.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Should I use &lt;code&gt;.cursorignore&lt;/code&gt; or &lt;code&gt;.cursorindexingignore&lt;/code&gt;?&lt;/strong&gt; Use &lt;code&gt;.cursorindexingignore&lt;/code&gt; unless the AI should never see the file at all. &lt;code&gt;.cursorindexingignore&lt;/code&gt; keeps a file out of the index and out of search results but leaves it readable, so you can still pull it in with &lt;code&gt;@Files&lt;/code&gt;, which makes it the reversible, lower-risk choice for large or noisy directories. Reserve &lt;code&gt;.cursorignore&lt;/code&gt; for things that must be fully invisible, like secrets or files you never want referenced, because it blocks reading and &lt;code&gt;@&lt;/code&gt;-mentioning as well as indexing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does Cursor's &lt;code&gt;@Codebase&lt;/code&gt; understand dependencies between repositories?&lt;/strong&gt; Not in the sense a breaking change needs. &lt;code&gt;@Codebase&lt;/code&gt; is nearest-neighbour search over an embedding index, so it returns the code most similar to your query, which is a different set from the repositories that declare a dependency on what you are changing. A &lt;code&gt;go.mod&lt;/code&gt; require line, or a Dockerfile &lt;code&gt;FROM&lt;/code&gt;, and the repository it points at are not similar text, so no similarity search reliably connects them. Indexing more repositories widens what &lt;code&gt;@Codebase&lt;/code&gt; can resemble against, but a cross-repo dependency edge has to be parsed from a manifest, not retrieved by similarity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can Cursor index multiple repositories at once?&lt;/strong&gt; Yes. A multi-root workspace, defined in a &lt;code&gt;.code-workspace&lt;/code&gt; file, can hold several repository roots, and Cursor indexes all of them and lets Agent search across the set. That makes &lt;code&gt;@Codebase&lt;/code&gt; span your repositories, which is genuinely useful for understanding a system. It does not make the index map how those repositories depend on each other, which is a separate thing that comes from parsing manifests rather than from a wider index.&lt;/p&gt;

</description>
      <category>cursor</category>
      <category>monorepo</category>
      <category>codebaseindexing</category>
      <category>multirepo</category>
    </item>
    <item>
      <title>Claude Code reads your clone. Cursor reads similarity. Neither sees the graph.</title>
      <dc:creator>Daniel Westgaard</dc:creator>
      <pubDate>Tue, 30 Jun 2026 09:19:21 +0000</pubDate>
      <link>https://dev.to/danielwe/claude-code-reads-your-clone-cursor-reads-similarity-neither-sees-the-graph-487i</link>
      <guid>https://dev.to/danielwe/claude-code-reads-your-clone-cursor-reads-similarity-neither-sees-the-graph-487i</guid>
      <description>&lt;p&gt;&lt;em&gt;Both agents can be handed more than one repository. Here is how to wire cross-repo blast radius into each, and the exact point where Claude Code's clone and Cursor's index each stop.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;You have Claude Code open in one repository and Cursor in another. The task in front of you is small, and the kind agents are good at. Bump a base image. Tighten a variable on a Terraform module. Delete a CI job that you are fairly sure three other projects still call. Before you let the agent run, you want the one thing it cannot tell you. Who else breaks.&lt;/p&gt;

&lt;p&gt;Both tools have a multi-repo story now, so it feels like a question they should be able to answer. Claude Code has &lt;code&gt;/add-dir&lt;/code&gt; and native MCP. Cursor indexes an entire workspace and answers questions about it. You could be forgiven for assuming that somewhere in there is a feature that knows your &lt;code&gt;orders-service&lt;/code&gt; consumes the contract you are about to change. There is not, and the reason is different for each tool. Knowing exactly where each one stops is the difference between wiring up something that genuinely helps and trusting something that quietly does not.&lt;/p&gt;

&lt;p&gt;So here is the claim this post runs on. Claude Code and Cursor can both be handed more than one repository, and neither can tell you which repositories a base-image or Terraform change will break, because Claude Code only sees the repositories you checked out, and Cursor's index answers similarity, not dependency. The edge that breaks the other repo was declared in a manifest, and a manifest is neither a file the agent cloned nor a chunk that embeds near your query.&lt;/p&gt;

&lt;p&gt;I have covered the &lt;a href="https://riftmap.dev/blog/how-to-give-copilot-cross-repo-context/" rel="noopener noreferrer"&gt;Copilot version of this question&lt;/a&gt; before, and made the case for &lt;a href="https://riftmap.dev/blog/the-repo-your-agent-didnt-clone/" rel="noopener noreferrer"&gt;why this blindness is structural rather than a gap a smarter model closes&lt;/a&gt;. This is the Claude Code and Cursor version, and it gets concrete about the wiring.&lt;/p&gt;

&lt;h2&gt;
  
  
  What each agent can actually see today
&lt;/h2&gt;

&lt;p&gt;What Claude Code and Cursor can see across repositories is different for each, and it moves fast enough that this section carries a date. As of June 2026, here is the precise shape of it.&lt;/p&gt;

&lt;p&gt;Claude Code sees the directory you started it in, recursing up the tree to load any &lt;code&gt;CLAUDE.md&lt;/code&gt; it finds along the way. You extend that reach with the &lt;a href="https://code.claude.com/docs/en/claude-directory" rel="noopener noreferrer"&gt;&lt;code&gt;--add-dir&lt;/code&gt; flag, the &lt;code&gt;/add-dir&lt;/code&gt; command, or &lt;code&gt;permissions.additionalDirectories&lt;/code&gt;&lt;/a&gt; in &lt;code&gt;.claude/settings.json&lt;/code&gt;. It reads and greps those trees, and it can edit them. What it does not do is hold a dependency graph of them. Add MCP servers and it gains tools that can. So Claude Code's cross-repo reach is exactly the set of directories you granted, navigated by reading and search, plus whatever tools you wired in.&lt;/p&gt;

&lt;p&gt;Cursor's reach is its index. It chunks your code, embeds each chunk with a custom model, and stores the vectors in a &lt;a href="https://cursor.com/blog/secure-codebase-indexing" rel="noopener noreferrer"&gt;remote vector database&lt;/a&gt;, and &lt;code&gt;@Codebase&lt;/code&gt; answers by finding the chunks whose embeddings sit nearest your query. &lt;a href="https://cursor.com/docs/agent/tools/search" rel="noopener noreferrer"&gt;Multi-root workspaces are supported&lt;/a&gt;, so several repositories can be indexed at once and Agent can reach all of them. The index covers the repositories you opened, it trails your local checkout by a sync interval, and it answers by similarity.&lt;/p&gt;

&lt;p&gt;Notice what both give you. A way to put more repositories in front of the agent. Notice what neither gives you. A way to turn "the agent can &lt;em&gt;see&lt;/em&gt; these repositories" into "the agent knows how these repositories &lt;em&gt;depend&lt;/em&gt; on each other." That second thing is the whole post, and the rest of it is, first, why each tool stops short of it, and then how to hand each tool the part it is missing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Claude Code: widen the window, write the map, add the tool
&lt;/h2&gt;

&lt;p&gt;Claude Code gives you three ways to reach across repositories, and they map cleanly onto the three families every agent's users reach for, in roughly this order. Each one solves a real problem. The first two stop at the same wall, and the third is where the fix actually goes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Widen the window with /add-dir
&lt;/h3&gt;

&lt;p&gt;The first instinct is access, and Claude Code makes it cheap. &lt;code&gt;--add-dir&lt;/code&gt; at launch, &lt;code&gt;/add-dir&lt;/code&gt; mid-session, or &lt;code&gt;permissions.additionalDirectories&lt;/code&gt; in &lt;code&gt;.claude/settings.json&lt;/code&gt; to make a set of sibling directories part of the project so everyone working in that area gets them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# At launch&lt;/span&gt;
claude &lt;span class="nt"&gt;--add-dir&lt;/span&gt; ../orders-service &lt;span class="nt"&gt;--add-dir&lt;/span&gt; ../platform-charts

&lt;span class="c"&gt;# Or permanently, in .claude/settings.json&lt;/span&gt;
&lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="s2"&gt;"permissions"&lt;/span&gt;: &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;"additionalDirectories"&lt;/span&gt;: &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"../orders-service"&lt;/span&gt;, &lt;span class="s2"&gt;"../platform-charts"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each granted directory becomes readable and editable, and as of a recent version Claude Code can even load the &lt;code&gt;CLAUDE.md&lt;/code&gt; from an added directory if you set &lt;code&gt;CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD=1&lt;/code&gt;. At a handful of tightly coupled repositories, with someone keeping the set current, this is the right first move and it costs an afternoon. I want to be fair to it before drawing the line.&lt;/p&gt;

&lt;p&gt;The line is that access is not selection. You chose which directories to add, and you chose them from memory. Nothing in &lt;code&gt;/add-dir&lt;/code&gt; tells you that a third repository consumes the same contract and is not in the set at all. And the failure that bites hardest is quieter still. You can only add a directory that exists on your disk, so the repository you forgot to clone is not a directory you can add. Widening the window shows the agent more rooms. It does not hand it the floor plan, and I made the longer version of that argument in &lt;a href="https://riftmap.dev/blog/repo-access-was-never-the-hard-part/" rel="noopener noreferrer"&gt;Repo access was never the hard part&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Write the map in CLAUDE.md
&lt;/h3&gt;

&lt;p&gt;The second instinct is to write the structure down. This is the &lt;code&gt;CLAUDE.md&lt;/code&gt; and &lt;code&gt;AGENTS.md&lt;/code&gt; layer, files loaded at session start and held for the whole session, layered enterprise, user, and project deep, that describe how the system fits together. More than sixty thousand repositories now carry one. The most documented version is Mabl's, an 850-line coordination graph their agents query at planning time, and it works.&lt;/p&gt;

&lt;p&gt;It also decays, and the engineers writing these files know it. The sharpest line I have read on the pattern comes from someone who &lt;a href="https://karun.me/blog/2026/03/26/structuring-claude-code-for-multi-repo-workspaces/" rel="noopener noreferrer"&gt;layered the files org, team, and repo deep&lt;/a&gt; and concluded: "I learned not to list repos here. Lists go stale. Instead, tell Claude where to look." That is the trap in one sentence. A hand-written map has to be kept current by humans at the same throughput the agents are changing the repositories, and an agent navigating by a stale map does not feel stale. It feels fast, right up until the change lands. A map that decays is the &lt;a href="https://riftmap.dev/blog/the-catalog-maintenance-trap/" rel="noopener noreferrer"&gt;developer-portal catalog problem&lt;/a&gt; wearing new clothes, and it loses to the same thing: a graph parsed from the source, which cannot drift from the source because it is read from it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Add the graph as an MCP tool
&lt;/h3&gt;

&lt;p&gt;The third instinct is the right one. Stop describing the structure and hand the agent a tool that holds it. MCP is native to Claude Code, configured with &lt;code&gt;claude mcp add&lt;/code&gt; or a committable &lt;code&gt;.mcp.json&lt;/code&gt; at the project root, and this is where the fix belongs.&lt;/p&gt;

&lt;p&gt;The catch is what is usually on the other end of that tool call. Most of what you can plug in here is a symbol index or a semantic one. It follows imports and call edges, or it embeds your code and retrieves by similarity. Both stop at the language boundary, the same wall I walked in the &lt;a href="https://riftmap.dev/blog/the-repo-your-agent-didnt-clone/" rel="noopener noreferrer"&gt;flagship&lt;/a&gt;: a &lt;a href="https://riftmap.dev/blog/symbol-graphs-and-artifact-graphs/" rel="noopener noreferrer"&gt;symbol graph&lt;/a&gt; answers "who calls this function" and never sees a &lt;code&gt;FROM&lt;/code&gt; line. The tool Claude Code actually needs at this layer is one that resolves a Dockerfile &lt;code&gt;FROM&lt;/code&gt; to the repository that builds the image, and a Terraform &lt;code&gt;source&lt;/code&gt; to the repository that owns the module. That tool is an &lt;a href="https://riftmap.dev/what-is-an-artifact-dependency-graph/" rel="noopener noreferrer"&gt;artifact graph&lt;/a&gt;, and wiring it in is the back half of this post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cursor: the index is the context model, and it answers the wrong question
&lt;/h2&gt;

&lt;p&gt;Cursor's cross-repo story is its index, and the index is built to answer a different question than the one a breaking change asks. This is the part worth slowing down on, because Cursor is genuinely good and the distinction is easy to miss.&lt;/p&gt;

&lt;p&gt;Here is how it works. Cursor splits your code into chunks, runs each chunk through an embedding model to get a vector, and stores those vectors in a database. When you ask &lt;code&gt;@Codebase&lt;/code&gt; something, your question becomes a vector too, and Cursor returns the chunks whose vectors sit nearest yours. The official description is exact about this: it returns the most semantically similar code chunks, even when they do not contain the keywords you used. Across a multi-root workspace it does that over every repository you opened.&lt;/p&gt;

&lt;p&gt;I want to be generous here, because it deserves it. Semantic search is a real improvement over grep. Cursor's &lt;a href="https://cursor.com/docs/agent/tools/search" rel="noopener noreferrer"&gt;own research&lt;/a&gt; puts it at around 12.5% more accurate on large codebases, and the gain grows with size. Open &lt;code&gt;web-app&lt;/code&gt;, &lt;code&gt;orders-service&lt;/code&gt;, and the shared contract repo in one workspace, and "how does the orders service validate a token" becomes one question instead of four context switches. For understanding a system, exploring it, finding where a concept lives, it is excellent, and nothing below is a knock on that.&lt;/p&gt;

&lt;p&gt;The line is that semantic search returns the code most similar to your query, and a declared dependency is not a similarity relationship. A Dockerfile line that reads &lt;code&gt;FROM platform/base-go:1.21&lt;/code&gt; and the repository that builds &lt;code&gt;platform/base-go&lt;/code&gt; share almost no tokens, no structure, nothing an embedding would place near the other. The edge between them is not latent in the text, waiting to be retrieved. It was declared once, in a manifest, and it is either parsed from that manifest or it is not found.&lt;/p&gt;

&lt;p&gt;Ask &lt;code&gt;@Codebase&lt;/code&gt; "what depends on this base image" and you get the files that mention base images, which is a different set from the repositories that inherit this one. This is the difference between &lt;a href="https://riftmap.dev/blog/inferred-context-is-not-a-dependency-graph/" rel="noopener noreferrer"&gt;inferred context and a dependency graph&lt;/a&gt;, and it is sharpest exactly where Cursor is most loved.&lt;/p&gt;

&lt;p&gt;Two more limits sit underneath that one, and they bite even if you set the similarity question aside. The index covers only the repositories you opened in the workspace, and someone chose that &lt;code&gt;.code-workspace&lt;/code&gt; folder list, from memory, which is the catalog trap again in a different file. And the index trails your checkout by a sync interval, so by Cursor's own guidance it is reliable for stable architecture and unreliable for recent change. The repository you did not open and the push from this morning are both outside it. Cursor indexes what looks related. The thing you need before a base-image bump is what is declared dependent, and those are different graphs built by different machinery.&lt;/p&gt;

&lt;h2&gt;
  
  
  The wall both share, and the graph that clears it
&lt;/h2&gt;

&lt;p&gt;Widen the window, write the map, or index the workspace, and you reach the same wall from three directions. &lt;code&gt;/add-dir&lt;/code&gt; gives access without selection. &lt;code&gt;CLAUDE.md&lt;/code&gt; gives structure that decays. The index gives similarity, never the declared edge. None of them resolves a &lt;code&gt;source&lt;/code&gt; block to the repository that owns the module, because that was never the job any of them was built to do.&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;How it reaches across repos&lt;/th&gt;
&lt;th&gt;What that answers well&lt;/th&gt;
&lt;th&gt;What it can't resolve&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Claude Code&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Reads the directories you grant it with &lt;code&gt;/add-dir&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Anything inside a repository you checked out&lt;/td&gt;
&lt;td&gt;Which repositories a change affects, and any repo you didn't clone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cursor&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Semantic search over an embedding index of the open workspace&lt;/td&gt;
&lt;td&gt;"Where does this live", "how does this work"&lt;/td&gt;
&lt;td&gt;A declared dependency, which is not a similarity relationship&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;But the edges that matter are already written down. The contract package in a &lt;code&gt;package.json&lt;/code&gt; dependency or a &lt;code&gt;go.mod&lt;/code&gt; require. The module in a Terraform &lt;code&gt;source&lt;/code&gt; block. The image in a Dockerfile &lt;code&gt;FROM&lt;/code&gt;. The chart in a &lt;code&gt;Chart.yaml&lt;/code&gt; dependency. The template in a GitLab CI &lt;code&gt;include:project&lt;/code&gt; or a reusable Actions &lt;code&gt;uses:&lt;/code&gt;. I spent &lt;a href="https://riftmap.dev/blog/series/find-every-consumer/" rel="noopener noreferrer"&gt;a whole series&lt;/a&gt; walking those edges one ecosystem at a time. They are deterministic. Parsed, not inferred. The graph that should be answering the agent's question already exists in your organisation's manifests, unassembled. The rest of this post is assembling it once and wiring it into Claude Code and Cursor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wiring the graph into Claude Code and Cursor
&lt;/h2&gt;

&lt;p&gt;A cross-repo dependency graph helps either agent in three ways, and they are worth doing in order, because each one is more setup and more enforcement than the last. The graph here is &lt;a href="https://riftmap.dev/for-agents/" rel="noopener noreferrer"&gt;Riftmap&lt;/a&gt;, which parses these edges across your whole GitHub or GitLab organisation from one read-only token and serves the result over an HTTP API. You can adopt the same architecture with any parsed graph, including one you build yourself.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tier one: let the agent call the API
&lt;/h3&gt;

&lt;p&gt;The simplest version works today and installs nothing. Tell the agent the graph is an HTTP call, and let it make the call with the shell tool it already has. The &lt;a href="https://docs.riftmap.dev/agents/overview" rel="noopener noreferrer"&gt;recommended pattern&lt;/a&gt; is three endpoints. Resolve the working tree to a node, hydrate that node's context in one round-trip, and ask for the transitive cascade when the change actually warrants it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Resolve the working tree's clone URL to a Riftmap repo&lt;/span&gt;
&lt;span class="nv"&gt;REPO_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"https://api.riftmap.dev/api/v1/repositories/lookup?url=https://github.com/myorg/platform-charts"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-API-Key: &lt;/span&gt;&lt;span class="nv"&gt;$RIFTMAP_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.id'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# One round-trip: the repo, its dependencies, its dependents, its artifacts&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"https://api.riftmap.dev/api/v1/repositories/&lt;/span&gt;&lt;span class="nv"&gt;$REPO_ID&lt;/span&gt;&lt;span class="s2"&gt;/context"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-API-Key: &lt;/span&gt;&lt;span class="nv"&gt;$RIFTMAP_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# When the change is breaking, ask for the transitive blast radius&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"https://api.riftmap.dev/api/v1/repositories/&lt;/span&gt;&lt;span class="nv"&gt;$REPO_ID&lt;/span&gt;&lt;span class="s2"&gt;/impact?max_depth=3"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-API-Key: &lt;/span&gt;&lt;span class="nv"&gt;$RIFTMAP_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To make the agent reach for this without being reminded each time, put the instruction where it already reads. A line in &lt;code&gt;CLAUDE.md&lt;/code&gt; for Claude Code, a rule in &lt;code&gt;.cursor/rules/&lt;/code&gt; for Cursor, along the lines of: before planning any change to a shared artifact (a base image, a Terraform module, a Helm chart, a CI template), call the Riftmap API to list the dependents and fold them into the plan.&lt;/p&gt;

&lt;p&gt;This is not theoretical. I have a &lt;a href="https://riftmap.dev/for-agents/" rel="noopener noreferrer"&gt;recorded Claude Code session&lt;/a&gt; doing exactly this. The prompt asks it to delete a &lt;code&gt;helm:deploy&lt;/code&gt; job, because Helm deploys are moving into an umbrella chart, and to flag who would break. The agent calls the API on its own, surfaces 51 consumers across the organisation, and sorts them by whether they pin a release tag, and so have a grace period, or float on &lt;code&gt;main&lt;/code&gt;, and so break on the next pipeline run. It can make that distinction because every dependency edge the API returns carries the &lt;code&gt;version_constraint&lt;/code&gt; the consumer declared. It also flags, unprompted, a caveat about the granularity of one edge, which is the kind of self-correction you want from an agent about to make a breaking change. That is the entire point. The structural account is in front of the agent before the first edit, not discovered in CI twenty minutes later.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tier two: make it a first-class tool with MCP
&lt;/h3&gt;

&lt;p&gt;Calling &lt;code&gt;curl&lt;/code&gt; from a prompt works, but the agent has to be told the shape of the call each time, and the call lives in your instructions rather than in the tool list where it belongs. Both Claude Code and Cursor are MCP clients, so the more native option is to expose the graph as MCP tools.&lt;/p&gt;

&lt;p&gt;Riftmap does not ship its own MCP server yet, and the &lt;a href="https://docs.riftmap.dev/agents/mcp-cli-roadmap" rel="noopener noreferrer"&gt;reason is worth stating plainly&lt;/a&gt;, because it is the honest one: the endpoints are the load-bearing piece, packaging them is mechanical, and the build is being held until real users ask for it rather than designed for a workflow nobody has yet. If you want it, the roadmap page takes an issue, and concrete demand is what unblocks it.&lt;/p&gt;

&lt;p&gt;Until it lands, you bridge in about five minutes, because Riftmap publishes a static OpenAPI schema and there are mature generators that turn any OpenAPI spec into an MCP server. Point one at the schema, give it the API base and your key, and the three endpoints become tools the agent sees in its tool list. For Claude Code, in a committable &lt;code&gt;.mcp.json&lt;/code&gt; at the project root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"riftmap"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"@ivotoby/openapi-mcp-server"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"env"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"OPENAPI_SPEC_PATH"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://app.riftmap.dev/openapi.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"API_BASE_URL"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://api.riftmap.dev/api/v1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"API_HEADERS"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"X-API-Key:${RIFTMAP_API_KEY}"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cursor reads the same &lt;code&gt;mcpServers&lt;/code&gt; format, so the identical block drops into &lt;code&gt;.cursor/mcp.json&lt;/code&gt;, with one change: Cursor's variable syntax for the key is &lt;code&gt;${env:RIFTMAP_API_KEY}&lt;/code&gt; rather than &lt;code&gt;${RIFTMAP_API_KEY}&lt;/code&gt;. One detail in that config is worth knowing rather than copying blind. The schema is served from &lt;code&gt;app.riftmap.dev&lt;/code&gt; and the API answers on &lt;code&gt;api.riftmap.dev&lt;/code&gt;, which is why the base URL is set explicitly instead of being inferred from the spec's own host.&lt;/p&gt;

&lt;p&gt;With either client, "who depends on &lt;code&gt;platform-charts&lt;/code&gt;" is now a tool call the agent makes itself, in Plan Mode or mid-task, with no HTTP in the prompt. When Riftmap's own server ships it will be a thinner version of the same thing, &lt;code&gt;pipx install riftmap-mcp&lt;/code&gt; against the same endpoints, and switching to it is a config line.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tier three: make it a gate, not a hint
&lt;/h3&gt;

&lt;p&gt;A tool the agent may call is still a tool the agent may skip. For the highest-stakes changes you want the graph between the change and &lt;code&gt;main&lt;/code&gt;, not merely available to the planner. There are two honest places to put it.&lt;/p&gt;

&lt;p&gt;In the loop, Claude Code runs hooks, skills, and subagents. A skill the agent invokes during planning, or a check on the commit boundary, can make the dependents query a step that has to happen before a change to a shared artifact proceeds. This keeps the graph in the agent's path rather than its discretion, though it depends on the agent's cooperation to fire.&lt;/p&gt;

&lt;p&gt;At review, the cleaner gate is CI, because it does not depend on the agent cooperating at all. On any pull request that touches a shared component, the pipeline calls &lt;code&gt;/impact&lt;/code&gt;, posts the consumer list as a comment, and the human reviewing the agent's change is checking it against the same structural account the agent planned with, not against memory. This is the architecture I think the whole category lands on, and it is the one &lt;a href="https://www.mabl.com/blog/how-we-built-a-system-for-ai-agents-to-ship-real-code-across-75-repos" rel="noopener noreferrer"&gt;Mabl built by hand&lt;/a&gt; before running agents across a hundred repositories on top of it.&lt;/p&gt;

&lt;p&gt;What makes either gate trustworthy rather than confidently wrong is the freshness contract. Every repository the API returns carries &lt;code&gt;last_scanned_at&lt;/code&gt; and &lt;code&gt;last_activity_at&lt;/code&gt;, and the single rule is that if the repository has been pushed to since Riftmap last scanned it, the graph is treated as stale. For an interactive agent that means warn and proceed with the caveat. For a CI gate it means trigger a rescan and re-poll before the merge. A gate that knows when it is out of date is the opposite of the stale &lt;code&gt;CLAUDE.md&lt;/code&gt; that feels fast right up until it is wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  The graph you hand the agent, not the one it tries to infer
&lt;/h2&gt;

&lt;p&gt;Give Claude Code every repository you have checked out, and give Cursor an index of all of them, and you have given each agent a faster way to read what is already in front of it. You have not given either one the list of repositories that break when it bumps the base image. That list was never a file Claude Code could clone or a chunk Cursor could embed. It was a set of &lt;code&gt;FROM&lt;/code&gt; lines and &lt;code&gt;source&lt;/code&gt; blocks sitting in repositories the agent never opened, declared once and never assembled. The agent cannot infer it, because it was never there to infer. It has to be parsed and handed over.&lt;/p&gt;

&lt;p&gt;Riftmap is that graph, built from one read-only token across your GitHub or GitLab organisation. It parses the manifests that already declare these edges across twelve ecosystems and resolves each reference to the repository that owns the artifact, then serves the result two ways. An interactive blast-radius view for the engineer who owns the estate and holds the pager, and an HTTP API, three endpoints with an OpenAPI schema and a freshness field on every response, that Claude Code and Cursor can call during planning, or that you can put in front of a merge. Auto-discovered, never catalogued. Parsed, not inferred. The MCP server is coming when enough people ask. Until then it is one config block away.&lt;/p&gt;

&lt;h2&gt;
  
  
  Questions teams ask
&lt;/h2&gt;

&lt;p&gt;The same questions come up whenever I help someone wire this in, so here they are, answered straight.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I give Claude Code cross-repo dependency awareness?&lt;/strong&gt; Claude Code sees the repositories you grant it with &lt;code&gt;/add-dir&lt;/code&gt; and reads them, but it does not build a dependency graph across them. To give it cross-repo blast radius, expose a parsed dependency graph as a tool it calls during planning. Either let it call the Riftmap HTTP API from its shell, resolving the repo with &lt;code&gt;lookup&lt;/code&gt; and then asking for &lt;code&gt;context&lt;/code&gt; or &lt;code&gt;impact&lt;/code&gt;, or wrap the OpenAPI schema as an MCP server in &lt;code&gt;.mcp.json&lt;/code&gt;. Then it can ask which repositories a change affects before it edits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does Cursor's @Codebase understand cross-repo dependencies?&lt;/strong&gt; Not in the sense a breaking change needs. &lt;code&gt;@Codebase&lt;/code&gt; is semantic search over an embedding index, so it returns the code most similar to your query, which is a different set from the repositories that declare a dependency on what you are changing. A Dockerfile &lt;code&gt;FROM&lt;/code&gt; line and the repository that builds that base image are not similar text, so no embedding search reliably connects them. Cross-repo dependency edges have to be parsed from manifests, not retrieved by similarity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I add a Riftmap MCP server to Claude Code or Cursor?&lt;/strong&gt; Not a first-party one yet. It is on the roadmap, deferred until there is real demand. Today you bridge in a few minutes: Riftmap publishes a static OpenAPI schema at &lt;code&gt;app.riftmap.dev/openapi.json&lt;/code&gt;, and a generic OpenAPI-to-MCP server turns it into MCP tools you register in &lt;code&gt;.mcp.json&lt;/code&gt; for Claude Code or &lt;code&gt;.cursor/mcp.json&lt;/code&gt; for Cursor. Or skip MCP entirely and have the agent call the three HTTP endpoints directly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why doesn't /add-dir tell Claude Code which repositories a change affects?&lt;/strong&gt; Because &lt;code&gt;/add-dir&lt;/code&gt; grants access, not selection. It makes the directories you name readable and editable, but you chose those directories from memory, and it cannot add a repository that is not checked out on your disk. Knowing which repositories to add is the cross-repo dependency question itself, and that answer comes from a parsed graph, not from a wider window.&lt;/p&gt;

</description>
      <category>aicodingagents</category>
      <category>crossrepocontext</category>
      <category>claudecode</category>
      <category>cursor</category>
    </item>
    <item>
      <title>What 208 kubernetes-sigs repos actually depend on</title>
      <dc:creator>Daniel Westgaard</dc:creator>
      <pubDate>Mon, 29 Jun 2026 12:20:16 +0000</pubDate>
      <link>https://dev.to/danielwe/what-208-kubernetes-sigs-repos-actually-depend-on-19jh</link>
      <guid>https://dev.to/danielwe/what-208-kubernetes-sigs-repos-actually-depend-on-19jh</guid>
      <description>&lt;p&gt;&lt;em&gt;A scan of the kubernetes-sigs organisation, rendered as a graph: 208 repos, 1,128 cross-repo dependencies, and 153 repos that depend on &lt;code&gt;yaml&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;If you've worked on a Kubernetes operator, you have a rough mental model of how kubernetes-sigs fits together. controller-runtime and controller-tools at the framework layer, kubebuilder above them as scaffolding, cluster-api and its army of cloud providers off to one side, kustomize and kind as standalone tools, dozens of CSI drivers each doing their own thing, and an unglamorous foundation of small utility libraries (&lt;code&gt;sigs.k8s.io/yaml&lt;/code&gt;, &lt;code&gt;sigs.k8s.io/json&lt;/code&gt;, &lt;code&gt;sigs.k8s.io/structured-merge-diff&lt;/code&gt;) underneath the lot.&lt;/p&gt;

&lt;p&gt;What you probably haven't seen is what that mental model looks like rendered as a graph.&lt;/p&gt;

&lt;p&gt;This is the second post in the series. The &lt;a href="https://riftmap.dev/blog/what-56-prometheus-repos-depend-on/" rel="noopener noreferrer"&gt;first scanned the Prometheus org&lt;/a&gt; with &lt;a href="https://riftmap.dev" rel="noopener noreferrer"&gt;Riftmap&lt;/a&gt; and used it as a calibration target: 56 repos, well-understood, you could check the scanner against your own intuition. The shape mostly matched. kubernetes-sigs is the harder test. 208 public repos, structured very differently from Prometheus, no single owning team that holds the whole thing in their head.&lt;/p&gt;

&lt;p&gt;The shape mostly matches here too. There are also a few specific things it doesn't see, which I'll get into below.&lt;/p&gt;

&lt;p&gt;The biggest single observation, before any of the screenshots: kubernetes-sigs is a federation. Where Prometheus had a hub-and-spoke shape with &lt;code&gt;client_golang&lt;/code&gt; and &lt;code&gt;prometheus/common&lt;/code&gt; at the centre, kubernetes-sigs has a thin shared utility layer at the bottom and otherwise-independent projects on top. That's not a flaw in the scan. It's what a 208-repo SIG-governed org actually looks like.&lt;/p&gt;

&lt;p&gt;The series has since scanned a third shape again: &lt;a href="https://riftmap.dev/blog/what-242-cloudposse-repos-depend-on/" rel="noopener noreferrer"&gt;Cloud Posse turned out to be a keystone&lt;/a&gt;, one convention module holding up 61% of its 242 repos with a cliff and nothing in between. Three orgs, three genuinely different topologies.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape of the org at a glance
&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmbto15ch6wfpn26drom4.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%2Fmbto15ch6wfpn26drom4.png" alt="Riftmap dashboard view of the kubernetes-sigs org showing 208 repos, 1,128 cross-repo dependencies, 528 distinct artifacts, the top-impact panel, and a dependency breakdown by ecosystem" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The numbers from a single scan: 208 repositories, 1,128 cross-repo dependencies, 528 distinct artifacts consumed somewhere in the org. The breakdown is heavily Go-skewed (857 Go module references, 257 git URL references, 8 GitHub Actions, 5 Kubernetes manifests, 1 Helm), which is what you'd expect for a Go-native operator ecosystem.&lt;/p&gt;

&lt;p&gt;The top of the impact list reads about how a Kubernetes contributor would predict the bottom of it but maybe not the top:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;kubernetes-sigs/yaml&lt;/code&gt; — 152 dependents&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;kubernetes-sigs/json&lt;/code&gt; — 142 dependents&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;kubernetes-sigs/randfill&lt;/code&gt; — 110 dependents&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;kubernetes-sigs/controller-runtime&lt;/code&gt; — 109 dependents&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;kubernetes-sigs/structured-merge-diff&lt;/code&gt; — 102 dependents&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The calibration moment here isn't quite the same as Prometheus. With Prometheus the top of the ranking confirmed the mental model: &lt;code&gt;prometheus/common&lt;/code&gt;, &lt;code&gt;client_golang&lt;/code&gt;, &lt;code&gt;client_model&lt;/code&gt;, the predictable centre. The top of the kubernetes-sigs ranking has controller-runtime where you'd expect it at #4, but the three repos above it are utility libraries most contributors couldn't pick out of a lineup. &lt;code&gt;sigs.k8s.io/yaml&lt;/code&gt; is a thin wrapper around &lt;code&gt;gopkg.in/yaml.v2&lt;/code&gt; with some helpers. &lt;code&gt;sigs.k8s.io/json&lt;/code&gt; is a small JSON marshaller. &lt;code&gt;sigs.k8s.io/randfill&lt;/code&gt; is a randomised-value fuzzer used in test generation.&lt;/p&gt;

&lt;p&gt;These three sit at the top because every operator-flavoured repo in the org imports them, directly or one hop away through controller-runtime or apimachinery, and Riftmap counts every direct import. The story isn't that they're surprising. It's that the most-depended-on things in a 208-repo Kubernetes org are not the ones with public-facing names. That's how shared infrastructure works.&lt;/p&gt;

&lt;p&gt;The out-degree list tells a complementary story. Most-importing repos in the org:&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;Repo&lt;/th&gt;
&lt;th&gt;Out-degree&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;&lt;code&gt;cluster-api&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;29&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;code&gt;kueue&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;29&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cluster-api-provider-aws&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;29&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cluster-api-provider-azure&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;27&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cluster-api-operator&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;21&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The cluster-api family dominates. Each provider imports the core, the shared utilities, and a dozen integration libraries. Combine the two rankings and you get the actual shape of the federation: a small utility floor at the bottom, controller-runtime as the framework layer, and the cluster-api family as the densest coordination cluster on top.&lt;/p&gt;

&lt;h2&gt;
  
  
  The graph view
&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgswypltdqnkxjz47zj06.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%2Fgswypltdqnkxjz47zj06.png" alt="Riftmap default graph view of the kubernetes-sigs org after auto-clustering — the 208 underlying repos folded into cluster groups, with the cluster-api family as a dense group, CSI drivers as a looser constellation, and a wide flat row of standalone projects" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The default graph view shows the kubernetes-sigs org after auto-clustering — all 208 repos folded into a navigable set of cluster nodes. The first thing worth noticing is what's missing. Prometheus's graph had eight tight clusters around clear anchors (&lt;code&gt;common&lt;/code&gt;, &lt;code&gt;client_model&lt;/code&gt;, &lt;code&gt;procfs&lt;/code&gt;, &lt;code&gt;exporter-toolkit&lt;/code&gt;, &lt;code&gt;promci&lt;/code&gt;) and a single dense hub-and-spoke pattern. kubernetes-sigs has a much flatter shape. The cluster-api family forms a recognisable group. The CSI drivers form a loose constellation. The kubebuilder-adjacent repos cluster together. And a wide row of standalone projects (kind, kustomize, gateway-api, kueue, metrics-server) sit on their own.&lt;/p&gt;

&lt;p&gt;That isn't a clustering failure. The auto-clustering groups repos with similar dependency profiles, and on kubernetes-sigs there genuinely are fewer profiles to find. Most cluster-api providers look like each other and cluster together. Most CSI drivers look like each other and cluster together. But &lt;code&gt;kind&lt;/code&gt; doesn't look like &lt;code&gt;kustomize&lt;/code&gt;, and neither looks like &lt;code&gt;gateway-api&lt;/code&gt;, and the graph faithfully reports that.&lt;/p&gt;

&lt;p&gt;The clustering is what makes 208 repos legible at this zoom. Without it you get a hairball. With it you can see, at a glance, which parts of the org are coupled and which parts are independent neighbours.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern at the centre
&lt;/h2&gt;

&lt;p&gt;I picked &lt;code&gt;cluster-api&lt;/code&gt; for the focus-mode shot. controller-runtime would be the easier choice. It's the closest analogue to &lt;code&gt;client_golang&lt;/code&gt; from the Prometheus post, with 109 in-org consumers radiating outward. cluster-api is more interesting. It sits at #10 on the in-degree ranking (38 dependents) but tied for #1 on out-degree (29 imports). That combination is rare. Most heavy importers aren't also heavy producers. Most central repos don't pull in two dozen of their siblings. cluster-api does both.&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%2F70u7gtm6u69k59kp9lyj.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%2F70u7gtm6u69k59kp9lyj.png" alt="Riftmap focus mode on kubernetes-sigs/cluster-api showing the repo at the centre with cluster-api-provider-aws, -azure, -gcp, -vsphere, -ibmcloud, -openstack, -cloudstack and the cluster-api-operator radiating out on one side and controller-runtime, the shared utility imports, and the metrics-server observability subgraph on the other" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What focus mode shows: cluster-api sits in the middle of a two-sided neighbourhood. On the consumer side, the cluster-api-provider family (-aws, -azure, -gcp, -vsphere, -ibmcloud, -openstack, -cloudstack, and a long tail) fans in. On the dependency side, cluster-api pulls in controller-runtime, the shared utilities (yaml, json, randfill, structured-merge-diff), apimachinery integrations, and an observability subgraph including metrics-server.&lt;/p&gt;

&lt;p&gt;You don't need to be a Cluster API maintainer to read this graph. A new engineer joining one of the provider teams could open it on their first day and see the coordination layer that ties their work to everyone else's, plus the upstream layer they share with cluster-api itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the receipts live
&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw66r5yxlltk9bjm0przc.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%2Fw66r5yxlltk9bjm0przc.png" alt="Side panel detail view for cluster-api showing 38 in-org consumers, dependencies grouped by ecosystem, and the metrics-server Helm chart reference highlighted at hack/observability/metrics-server/kustomization.yaml line 5, version 3.13.0, confidence 0.9" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click any node and the side panel opens with file-level evidence. For cluster-api the worth-pausing-on detail is this one: cluster-api consumes the metrics-server Helm chart at version 3.13.0, referenced from &lt;code&gt;hack/observability/metrics-server/kustomization.yaml:5&lt;/code&gt;. Confidence 0.9.&lt;/p&gt;

&lt;p&gt;That's not a Go module edge. It's a Helm chart referenced from a kustomization file, in a different repo, in a directory most contributors never open. The Prometheus post leaned hard on go.mod parsing: three separate &lt;code&gt;go.mod&lt;/code&gt; files in &lt;code&gt;prometheus/prometheus&lt;/code&gt;, each with its own pinned version of &lt;code&gt;client_golang&lt;/code&gt;. Cross-ecosystem edges like the metrics-server one are where the cost of not having this kind of graph compounds. If metrics-server publishes a breaking change in the chart and cluster-api's observability hack directory still references 3.13.0, no Go tooling will flag it. The Helm chart edge sits at confidence 0.9 rather than 1.0 because chart references resolve through registry name matching rather than exact module paths. Across the full scan, 83% of edges resolve at confidence 1.0 and the rest at 0.6 to 0.9. The confidence is part of the receipt, not a hedge.&lt;/p&gt;

&lt;p&gt;A second receipt worth noting, on the Go side: &lt;code&gt;sigs.k8s.io/yaml&lt;/code&gt; is imported by 149 distinct repos via two raw module paths (&lt;code&gt;sigs.k8s.io/yaml&lt;/code&gt; and &lt;code&gt;github.com/kubernetes-sigs/yaml&lt;/code&gt;). Both resolve to the same artifact. 212 total declarations from those 149 unique repos. The gap is the multi-module monorepos importing yaml from several &lt;code&gt;go.mod&lt;/code&gt; files. &lt;code&gt;kubernetes-sigs/cluster-addons&lt;/code&gt; is the strongest example: it's a multi-module monorepo with submodules for bootstrap, coredns, dashboard, flannel, metrics-server, nodelocaldns and others. Each produces its own &lt;code&gt;go_module&lt;/code&gt; artifact (&lt;code&gt;sigs.k8s.io/cluster-addons/bootstrap&lt;/code&gt;, &lt;code&gt;sigs.k8s.io/cluster-addons/coredns&lt;/code&gt;, and so on). Each submodule's yaml import is tracked separately. "We updated yaml in the root go.mod" and "we updated every reference to yaml" remain different statements.&lt;/p&gt;

&lt;p&gt;One related quirk: cluster-addons' &lt;code&gt;kubeproxy/go.mod&lt;/code&gt; declares &lt;code&gt;module addon-operators/kubeproxy&lt;/code&gt; (no domain, no &lt;code&gt;sigs.k8s.io&lt;/code&gt; prefix). Riftmap surfaces it exactly as declared. When you scan an org, the right behaviour is to report what's in source rather than to normalise it.&lt;/p&gt;

&lt;h2&gt;
  
  
  If I changed cluster-api, what breaks?
&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9x241i8i206srq1fcaud.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%2F9x241i8i206srq1fcaud.png" alt="Riftmap Impact Mode canvas view for cluster-api showing the focused neighborhood with red cascade edges radiating outward to all 38 affected repos including cluster-api-provider-aws, -azure, -gcp, -vsphere, -ibmcloud, -openstack, -cloudstack, cluster-api-operator, cluster-api-addon-provider-helm, kueue, and the transitive tail" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Toggle Impact Mode on the same focus view and the canvas lights up. Red cascade edges radiate from cluster-api through every repo in its blast radius. 38 repos affected at maximum depth 3: the seven cluster-api-providers you've heard of, the four you haven't (kubevirt, packet, hetzner, and a handful more), cluster-api-operator, cluster-api-addon-provider-helm, the standalone consumers like kueue, and a small transitive tail.&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%2Ftnw4d8bjqva45g3wk7k1.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%2Ftnw4d8bjqva45g3wk7k1.png" alt="Impact tab in the Riftmap side panel for cluster-api showing 38 repos affected at maximum depth 3 with depth-1 badges on each affected repo and a .md export button at the top" width="402" height="1025"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The side panel has the list with depth labels and the file:line that would need to change in each downstream. There's a Markdown export at the bottom. Click it and you get a copyable list of every affected repo with its evidence pointers, ready to paste into a deprecation announcement, a v1beta migration RFC, or a release-notes block. That loop, "show me the blast radius and let me hand the list to the team that owns each downstream repo," is the same workflow the Prometheus post described. The list is just longer here.&lt;/p&gt;

&lt;p&gt;&lt;span&gt;&lt;/span&gt;See it live&lt;/p&gt;

&lt;p&gt;Try the same Impact Mode yourself&lt;/p&gt;

&lt;p&gt;The kubernetes-sigs scan isn't published as a live demo yet — but the Prometheus org is, with the exact interactive Impact Mode described above. Click any repo and watch its blast radius cascade across the graph.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://riftmap.dev/showcase/prometheus/" rel="noopener noreferrer"&gt;Explore the live graph →&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What Riftmap didn't see (and why)
&lt;/h2&gt;

&lt;p&gt;Same honesty section as the Prometheus post. Three categories.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Real parser gaps.&lt;/strong&gt; Riftmap can't currently parse:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;pyproject.toml&lt;/code&gt;, &lt;code&gt;requirements.txt&lt;/code&gt;, and &lt;code&gt;setup.py&lt;/code&gt;, so the four detected Python packages in the org (&lt;code&gt;inference-perf&lt;/code&gt;, &lt;code&gt;jobset&lt;/code&gt;, &lt;code&gt;k8s-agent-sandbox&lt;/code&gt;, &lt;code&gt;kubespray_component_hash_update&lt;/code&gt;) show as produced artifacts but with no cross-org consumption resolved&lt;/li&gt;
&lt;li&gt;Terraform module consumption from a downstream repo's &lt;code&gt;module "x" { source = "..." }&lt;/code&gt; blocks. Riftmap detects produced Terraform roots and modules (6 and 4 respectively) but doesn't yet resolve the consumer side&lt;/li&gt;
&lt;li&gt;Ansible playbook and collection consumption. Same pattern: 1 collection and 3 playbooks produced, zero consumption resolved&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Cargo.toml&lt;/code&gt;, so the org's one Rust experiment sits unconnected&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are real, named gaps. They are roughly the next four on the parser roadmap, in that order. Current ecosystem coverage is documented in the &lt;a href="https://riftmap.dev/blog/auto-discovering-infrastructure-dependencies-across-10-ecosystems/" rel="noopener noreferrer"&gt;auto-discovery write-up&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Correctly parsed, deliberately not drawn.&lt;/strong&gt; Most CSI driver repos sit as leaves with low or no in-degree. &lt;code&gt;aws-ebs-csi-driver&lt;/code&gt;, &lt;code&gt;azuredisk-csi-driver&lt;/code&gt;, &lt;code&gt;gcp-compute-persistent-disk-csi-driver&lt;/code&gt; and their siblings have no kubernetes-sigs consumers. That's correct. They consume &lt;code&gt;k8s.io/...&lt;/code&gt; upstreams (different org) and external SDKs (&lt;code&gt;aws-sdk-go&lt;/code&gt;, &lt;code&gt;azure-sdk-for-go&lt;/code&gt;), and they produce their own container images that get consumed by cluster operators outside the org. Their dependency graph leaves kubernetes-sigs and never comes back. Drawing them as connected when they aren't would be the bug.&lt;/p&gt;

&lt;p&gt;The GitHub Actions edge count is similarly low (8 in-org references) for the same reason. kubernetes-sigs repos overwhelmingly use &lt;code&gt;actions/checkout&lt;/code&gt;, &lt;code&gt;actions/setup-go&lt;/code&gt;, &lt;code&gt;actions/cache&lt;/code&gt; from the &lt;code&gt;actions/*&lt;/code&gt; org. Cross-org Actions inside kubernetes-sigs are genuinely sparse. The eight that exist are real.&lt;/p&gt;

&lt;p&gt;The single in-org Helm edge looks low until you check the chart sources. kubernetes-sigs charts mostly depend on external bases (bitnami, cert-manager, ingress-nginx, prometheus-community), all of which live in other orgs. The one in-org Helm edge that exists is the metrics-server reference above.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Federation means less clustering.&lt;/strong&gt; Riftmap's auto-clustering finds fewer cohesive groups on kubernetes-sigs than on Prometheus, and that reflects the org rather than a clustering shortcoming. cluster-api providers cluster tightly because they share dependency profiles. CSI drivers form a looser group. Standalone projects (kind, kustomize, gateway-api, kueue) cluster with whichever repos they happen to share imports with, which often isn't very many. The flat shape is the finding.&lt;/p&gt;

&lt;p&gt;The polyglot Python packages are parser gaps. The unconnected CSI drivers are correct silence. Both look the same in the graph at first glance. They are not the same thing, and the difference is what the side panel is for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Methodology, briefly
&lt;/h2&gt;

&lt;p&gt;Before Prometheus and kubernetes-sigs, Riftmap was validated against two private adversarial test groups designed to mimic production orgs. The first (27 repos, 83 expected edges) covers 20 intentional edge cases: diamond dependencies, dual-artifact repos, nested CI includes, ARG-based Docker &lt;code&gt;FROM&lt;/code&gt; lines, Terraform subdirectory syntax, &lt;code&gt;COPY --from&lt;/code&gt; cross-repo references, and version lag across pinned consumers. The second (55 repos, ~135 expected edges across nine ecosystems) adds cross-language artifact reuse, circular Go module deps, multi-artifact single repos, dependency chains five levels deep, and unsupported-ecosystem node rendering. Both have hand-verified ground-truth edge lists. Every scanner change is tested against them.&lt;/p&gt;

&lt;p&gt;The kubernetes-sigs scan completed in 13m 34s across all 208 repos, with 0 skipped and no errors. 83% of edges resolved at confidence 1.0 (exact path matches); the remainder at 0.6 to 0.9 (Helm chart references and Kubernetes image references that resolve through name matching rather than exact paths).&lt;/p&gt;

&lt;p&gt;None of the heuristics involved in artifact resolution are public. The scan output is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two asks
&lt;/h2&gt;

&lt;p&gt;If you work on a kubernetes-sigs repo and you spot something Riftmap got wrong about it, a missing edge, a misattributed version, a parser failure I should add to the roadmap, email me at &lt;a href="mailto:daniel@riftmap.dev"&gt;daniel@riftmap.dev&lt;/a&gt;. I'll fix the parser and credit you in the follow-up.&lt;/p&gt;

&lt;p&gt;If you're running something similar in your own org and want to see what your graph looks like, Riftmap is at &lt;a href="https://riftmap.dev/" rel="noopener noreferrer"&gt;riftmap.dev&lt;/a&gt;. The kubernetes-sigs scan took about 13 minutes for 208 repos. Yours probably finishes in a fraction of that.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>kubernetessigs</category>
      <category>dependencygraph</category>
      <category>crossrepodependencies</category>
    </item>
    <item>
      <title>Change failure rate is up 30% — here's how to measure yours in an afternoon</title>
      <dc:creator>Daniel Westgaard</dc:creator>
      <pubDate>Mon, 29 Jun 2026 12:18:58 +0000</pubDate>
      <link>https://dev.to/danielwe/change-failure-rate-is-up-30-heres-how-to-measure-yours-in-an-afternoon-1ong</link>
      <guid>https://dev.to/danielwe/change-failure-rate-is-up-30-heres-how-to-measure-yours-in-an-afternoon-1ong</guid>
      <description>&lt;p&gt;&lt;em&gt;A practitioner's guide to calculating your team's CFR without a vendor platform — the DORA formula, the SQL, and the AI-assisted vs human-authored split nobody is publishing yet.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Cortex's 2026 benchmark says change failure rate has risen about 30% industry-wide since AI coding adoption accelerated. The number has been quoted in every engineering newsletter I read. It keeps showing up in LinkedIn posts. I cited it myself in my &lt;a href="https://riftmap.dev/blog/ai-doesnt-understand-blast-radius/" rel="noopener noreferrer"&gt;last post&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Here's the uncomfortable follow-up question: &lt;em&gt;what's yours?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Most platform teams I've worked with couldn't give me a number. They could estimate a direction — "it feels worse lately" — but the actual percentage wasn't anywhere. And without a number, the 30% headline is just other people's data. You can't improve what you haven't measured.&lt;/p&gt;

&lt;p&gt;This post walks through how to compute your team's CFR in an afternoon using data you already have, and how to split it in a way nobody is doing yet: AI-assisted PRs vs. human-authored. You don't need a vendor platform for any of this.&lt;/p&gt;

&lt;h2&gt;
  
  
  What CFR actually measures
&lt;/h2&gt;

&lt;p&gt;DORA's definition, lifted from the source:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The percentage of changes to production or releases to users that result in degraded service and subsequently require remediation — a hotfix, rollback, fix forward, or patch.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's the whole thing. Three details matter and they're the ones most vendor posts get slightly wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Only production counts.&lt;/strong&gt; A test that fails in CI isn't a change failure. A canary that catches a bad deploy before it reaches real users isn't one either. If your release engineering is working, a lot of would-be failures never count — which is the point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Remediation has to happen.&lt;/strong&gt; A deployment that's merely suboptimal isn't a failure. The question is whether it needed a rollback, hotfix, fix-forward, or patch after the fact. "We wrote a Jira ticket" isn't remediation; "we pushed another deploy to fix the first one" is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The denominator is changes, not deployments.&lt;/strong&gt; If you push three deploys and two of them are fix-only remediations of the first, you made one change, not three. Fix-only deploys come out of both the numerator and the denominator — they are neither new changes nor new failures in the sense CFR measures.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Number of changes   = Production deployments − Fix-only deployments
CFR                 = Failed changes ÷ Number of changes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://dora.dev/research/2025/dora-report/" rel="noopener noreferrer"&gt;DORA's 2025 report&lt;/a&gt; found that about 16.7% of teams maintain CFR at 4% or below — that's the elite band. Most teams sit well above it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 90-minute version
&lt;/h2&gt;

&lt;p&gt;You need three things, all of which you already have somewhere.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. A list of production deployments.&lt;/strong&gt; From your CI (GitHub Actions, GitLab CI, Jenkins, CircleCI, Argo), filtered to production environment only, successful runs only. Most of these systems have an API or a database you can query. If you can get &lt;code&gt;deployment_id&lt;/code&gt;, &lt;code&gt;service&lt;/code&gt;, &lt;code&gt;deployed_at&lt;/code&gt;, and &lt;code&gt;commit_sha&lt;/code&gt;, you're set.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. A list of production incidents.&lt;/strong&gt; From PagerDuty, Opsgenie, Incident.io, your internal spreadsheet — wherever your on-call logs live. Filter to anything that required an engineering response. You want &lt;code&gt;incident_id&lt;/code&gt;, &lt;code&gt;service&lt;/code&gt;, &lt;code&gt;started_at&lt;/code&gt;, and ideally the SHA or deployment that was identified as the root cause.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. A rule for joining them.&lt;/strong&gt; The simplest rule that works: an incident "belongs to" a deployment if the incident started within some window after the deployment, on the same service. A 24-hour window is standard; some teams use 48 hours for services with slow-burn failure modes. This isn't causal attribution — it's a proxy, and it's close enough.&lt;/p&gt;

&lt;p&gt;Here's the shape of the query once both datasets are in the same place:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;changes&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;deployment_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;deployed_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;commit_sha&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;deployments&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;environment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'production'&lt;/span&gt;
    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'success'&lt;/span&gt;
    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;deployed_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2026-01-01'&lt;/span&gt;
    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="n"&gt;is_fix_only&lt;/span&gt;  &lt;span class="c1"&gt;-- exclude rollbacks/hotfixes&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;failed_changes&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deployment_id&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;changes&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;
  &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;incidents&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;
    &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt;
    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;started_at&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deployed_at&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deployed_at&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'24 hours'&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deployment_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                       &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_changes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;fc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deployment_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                      &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;failed_changes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;fc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deployment_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deployment_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;change_failure_rate&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;changes&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;failed_changes&lt;/span&gt; &lt;span class="n"&gt;fc&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;deployment_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your deployment tool doesn't track &lt;code&gt;is_fix_only&lt;/code&gt;, the practical workaround is a convention — require engineers to prefix fix-only PRs with &lt;code&gt;fix:&lt;/code&gt; or tag them with a &lt;code&gt;fix-only&lt;/code&gt; label, and filter on that. The data gets better once you start asking for it.&lt;/p&gt;

&lt;p&gt;Run the query over the last 90 days. That's your CFR. Longer windows are noisier; shorter ones are too volatile to trust.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cut nobody is making yet
&lt;/h2&gt;

&lt;p&gt;Here's where it gets interesting. The Cortex 30% number is an aggregate. It tells you the industry has gotten worse. It doesn't tell you &lt;em&gt;which of your PRs&lt;/em&gt; are driving your team's number.&lt;/p&gt;

&lt;p&gt;You can find out.&lt;/p&gt;

&lt;p&gt;Tag your PRs. There are several reasonable ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PR label.&lt;/strong&gt; Add an &lt;code&gt;ai-assisted&lt;/code&gt; label manually at review time. Lowest overhead, most honest, relies on the author.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PR template checkbox.&lt;/strong&gt; "Did you use AI coding tools in this PR?" as a checkbox that a small bot reads and labels accordingly. Works well for teams with a review culture that already uses templates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Commit trailer.&lt;/strong&gt; &lt;code&gt;AI-Assisted: yes&lt;/code&gt; or a &lt;code&gt;Co-authored-by: ...&lt;/code&gt; line pointing at a bot account. Survives rebases and is machine-readable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tool-reported attribution.&lt;/strong&gt; Some tooling (&lt;a href="https://github.com/git-ai-project/git-ai" rel="noopener noreferrer"&gt;Git AI's open standard&lt;/a&gt; on Git Notes is a good example) can record which ranges of a diff were model-authored at the source, before the PR is even opened. Heavier setup, higher fidelity.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Any of these is fine. The worst option is to defer tagging until you "find the right platform." Pick a convention, write it down, roll it out on Monday.&lt;/p&gt;

&lt;p&gt;Once PRs are tagged, split the CFR query two ways:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- AI-assisted PRs&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;changes&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;commit_sha&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;sha&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;ai_assisted_prs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;-- Human-authored PRs&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;changes&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;commit_sha&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;sha&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;ai_assisted_prs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you have two CFRs. Compare them.&lt;/p&gt;

&lt;p&gt;If your AI-assisted CFR is meaningfully higher than your human-authored CFR — and, based on every public benchmark from the last six months, it probably is — you have your own version of the 30% number. Not an industry aggregate. Your team's aggregate, on your codebase, for your definition of failure. That number is the one that actually motivates change.&lt;/p&gt;

&lt;p&gt;It's also a fair number in a way the industry stat isn't. If your AI-assisted CFR is &lt;em&gt;lower&lt;/em&gt; than your human CFR, that tells you something real too — your team has figured out how to use these tools well, and the finding is worth internal publicity.&lt;/p&gt;

&lt;p&gt;&lt;span&gt;&lt;/span&gt;See it live&lt;/p&gt;

&lt;p&gt;Cut the failures you can see coming&lt;/p&gt;

&lt;p&gt;Most change failures are blast-radius surprises — a downstream consumer nobody flagged in review. Open the live Prometheus graph, click any repo, and toggle Impact Mode to see exactly who breaks before the change merges.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://riftmap.dev/showcase/prometheus/" rel="noopener noreferrer"&gt;Explore the live graph →&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do with the number
&lt;/h2&gt;

&lt;p&gt;I wrote most of this in the &lt;a href="https://riftmap.dev/blog/ai-doesnt-understand-blast-radius/" rel="noopener noreferrer"&gt;previous post&lt;/a&gt;, so I'll keep it brief.&lt;/p&gt;

&lt;p&gt;The patterns that reduce CFR at teams I've seen up close are the boring ones. Smaller PRs. Trunk-based development with feature flags instead of long-lived branches. Canary deploys with automatic rollback. Strong ownership over shared infrastructure artifacts. And — the one most teams skip — visibility into the &lt;a href="https://riftmap.dev/blog/infrastructure-dependency-problem/" rel="noopener noreferrer"&gt;cross-repo blast radius&lt;/a&gt; of a change before it merges, so that the review can ask the right question rather than a generic one.&lt;/p&gt;

&lt;p&gt;What doesn't work is adding process layers that slow every change without discriminating by risk. The goal isn't to slow the agents down; it's to route high-blast-radius changes through more scrutiny than low-blast-radius ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;How is change failure rate calculated?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;CFR equals the number of failed changes divided by the number of changes, over a given time window. A failed change is a production deployment that required remediation — a rollback, hotfix, fix-forward, or patch. Fix-only deployments are excluded from both sides of the ratio because they aren't new changes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is a good change failure rate?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;DORA's 2025 data suggests that about 16.7% of teams achieve a CFR of 4% or lower, which is the elite band. A CFR in the 0–15% range generally indicates a mature delivery process. Above 30% typically points at gaps in testing, release safety, or ownership clarity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Should I include staging or pre-production failures in CFR?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. CFR is a production-only metric by DORA's definition. A canary that catches a bad deploy before it reaches real users is a win, not a failure — counting it penalises the very controls you want teams to invest in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I track AI-assisted code for CFR purposes?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The simplest approach is a PR label or commit trailer that engineers apply at authoring or review time. More sophisticated options include PR templates with a checkbox, bot-applied labels based on known AI-tool user accounts, and tools like the Git AI open standard that record AI-authored diff ranges in Git Notes. Perfect attribution is not required — a consistent convention used by the team is enough to split the metric meaningfully.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How long should my CFR measurement window be?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Ninety days is the usual default. Shorter windows (two to four weeks) are too noisy for most teams — a single rough week swings the number. Longer windows (six months or more) smooth out recent changes in your delivery practices and are slow to react to regressions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;A week of work, most of which is data plumbing you probably already have, gets you an honest CFR number and a split between AI-assisted and human-authored changes. That's a better starting point than any aggregate benchmark from any vendor report.&lt;/p&gt;

&lt;p&gt;I'm building &lt;a href="https://riftmap.dev" rel="noopener noreferrer"&gt;Riftmap&lt;/a&gt; to solve the other half of this — giving teams visibility into the cross-repo blast radius of a change &lt;em&gt;before&lt;/em&gt; the CFR number moves. Auto-discovery across Terraform, Docker, CI templates, Helm, Go, npm, Python, Ansible, Kubernetes, and Kustomize. One read-only token. No YAML to maintain.&lt;/p&gt;

&lt;p&gt;If this is familiar territory, reach me at &lt;a href="mailto:daniel@riftmap.dev"&gt;daniel@riftmap.dev&lt;/a&gt;, or try a free scan at &lt;a href="https://app.riftmap.dev" rel="noopener noreferrer"&gt;app.riftmap.dev&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Sources referenced
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;DORA, &lt;em&gt;Software delivery performance metrics&lt;/em&gt; — &lt;a href="https://dora.dev/guides/dora-metrics-four-keys/" rel="noopener noreferrer"&gt;dora.dev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Google Cloud / DORA, &lt;em&gt;2025 State of AI-assisted Software Development Report&lt;/em&gt; — &lt;a href="https://dora.dev/research/2025/dora-report/" rel="noopener noreferrer"&gt;dora.dev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Cortex, &lt;em&gt;Engineering in the Age of AI: 2026 Benchmark Report&lt;/em&gt; — &lt;a href="https://www.cortex.io/report/engineering-in-the-age-of-ai-2026-benchmark-report" rel="noopener noreferrer"&gt;cortex.io&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Swarmia, &lt;em&gt;DORA change failure rate — what, why, and how&lt;/em&gt; — &lt;a href="https://www.swarmia.com/blog/dora-change-failure-rate/" rel="noopener noreferrer"&gt;swarmia.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Git AI open standard for AI authorship attribution via Git Notes — &lt;a href="https://github.com/git-ai-project/git-ai" rel="noopener noreferrer"&gt;github.com/git-ai-project&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Related reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://riftmap.dev/blog/ai-doesnt-understand-blast-radius/" rel="noopener noreferrer"&gt;AI Doesn't Understand Blast Radius&lt;/a&gt; — Why change failure rates are up 30% and what's structurally driving it.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://riftmap.dev/blog/ai-coding-agents-need-cross-repo-context/" rel="noopener noreferrer"&gt;AI coding agents need cross-repo context&lt;/a&gt; — What teams running AI coding agents at scale are publishing about the missing dependency substrate.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://riftmap.dev/blog/meta-tribal-knowledge-engine-build-the-graph-first/" rel="noopener noreferrer"&gt;Meta needed 50+ AI agents to map their tribal knowledge&lt;/a&gt; — How a 50-agent system quietly rests on a single graph index that does the heavy lifting.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>dorametrics</category>
      <category>changefailurerate</category>
      <category>platformengineering</category>
      <category>aicoding</category>
    </item>
  </channel>
</rss>
