<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Riftmap Blog</title><description>Cross-repository dependency mapping, blast radius, and change-impact analysis for GitLab and GitHub orgs — field notes from building Riftmap.</description><link>https://riftmap.dev/</link><language>en-us</language><item><title>Cross-repo blast radius in nine public platform orgs: the most depended-on repo at Grafana isn&apos;t Grafana</title><link>https://riftmap.dev/blog/cross-repo-blast-radius-9-orgs/</link><guid isPermaLink="true">https://riftmap.dev/blog/cross-repo-blast-radius-9-orgs/</guid><description>Parsed cross-repo dependency edges across 1,297 repos in nine public platform orgs. One in four repos is load-bearing for a sibling. Top hub: 188 dependents.</description><pubDate>Tue, 14 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&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=&quot;https://riftmap.dev&quot;&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=&quot;https://riftmap.dev/data/9org-scan-2026-07-14-edges.csv&quot;&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=&quot;https://riftmap.dev/blog/inferred-context-is-not-a-dependency-graph/&quot;&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&apos;s scanned repo count matched GitHub&apos;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&apos;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;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;p&gt;The single most load-bearing repository in Grafana&apos;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;img src=&quot;https://riftmap.dev/images/blog/shared-workflows-fanin.png&quot; alt=&quot;grafana/shared-workflows focused in Riftmap&apos;s dependency graph: 188 repositories fan inward to a single CI-workflow repo, and not one of them appears anywhere inside it&quot; /&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&apos;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&apos;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 = &quot;terraform-aws-modules/alb/aws&quot;&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&apos;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;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;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&apos;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&apos;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&apos;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&apos;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&apos;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 &quot;transitive blast radius&quot; 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&apos;s mental model, and it accumulated 188 dependents without a single person deciding it was important enough to write down. A &lt;a href=&quot;https://riftmap.dev/blog/backstage-alternatives/&quot;&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=&quot;https://sourcegraph.com&quot;&gt;Sourcegraph&lt;/a&gt; indexes programming-language symbols across repositories, which is a real and hard thing to get right, and it answers &quot;where is this function used&quot; better than anything else. &lt;a href=&quot;https://riftmap.dev/blog/gitlab-orbit-and-the-artifact-layer/&quot;&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=&quot;https://riftmap.dev/blog/can-ai-check-blast-radius-of-pr-before-merge/&quot;&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&apos;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 &quot;which repos break&quot; 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=&quot;https://riftmap.dev/what-is-cross-repo-dependency-mapping/&quot;&gt;artifact edges&lt;/a&gt;, and answers the &lt;a href=&quot;https://riftmap.dev/ai-agent-blast-radius/&quot;&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=&quot;https://riftmap.dev/data/9org-scan-2026-07-14-edges.csv&quot;&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=&quot;https://riftmap.dev/showcase/cloudposse/&quot;&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;hr /&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;
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;
You cannot find it from the module&apos;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;
Not from a single repository&apos;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 &quot;what breaks if I change this&quot;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why not infer cross-repo dependencies from repo names or code search?&lt;/strong&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&apos;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;

</content:encoded><category>blast-radius</category><category>cross-repo-dependencies</category><category>platform-engineering</category><category>dependency-graph</category><category>terraform</category><category>artifact-graph</category><author>Daniel Westgaard</author></item><item><title>Which AI coding assistants can see blast radius before they change code?</title><link>https://riftmap.dev/blog/which-ai-coding-assistants-see-blast-radius/</link><guid isPermaLink="true">https://riftmap.dev/blog/which-ai-coding-assistants-see-blast-radius/</guid><description>None of Claude Code, Cursor, or Copilot resolves cross-repo blast radius on its own. Why the limit is structural — and what changes when you hand one a parsed graph.</description><pubDate>Mon, 13 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&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 &quot;hand it a graph&quot; actually means.&lt;/p&gt;
&lt;h2&gt;The question hides a conflation&lt;/h2&gt;
&lt;p&gt;The question &quot;which assistant sees blast radius&quot; 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=&quot;https://riftmap.dev/ai-agent-blast-radius/&quot;&gt;AI-agent blast radius&lt;/a&gt;. It is exactly what a single repo&apos;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&apos;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&apos;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=&quot;https://code.claude.com/docs/en/cli-reference&quot;&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=&quot;https://code.claude.com/docs/en/large-codebases&quot;&gt;&lt;code&gt;/add-dir&lt;/code&gt; command&lt;/a&gt; does the same from inside a running session. Anthropic&apos;s own large-codebase guidance documents the cross-repo move under &lt;a href=&quot;https://code.claude.com/docs/en/large-codebases&quot;&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&apos;s Dockerfile, and once you have added that directory Claude can read it. The difference bites at scale. At an organisation&apos;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&apos;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&apos;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&apos;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=&quot;https://cursor.com/docs/agent/tools/search&quot;&gt;Cursor ships Instant Grep&lt;/a&gt;, which its docs describe as a custom search engine that &quot;outperforms ripgrep on large codebases,&quot; 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 = &quot;cloudposse/ssm-parameter-store/aws&quot;&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&apos;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=&quot;https://docs.github.com/en/copilot/concepts/context/spaces&quot;&gt;GitHub&apos;s docs&lt;/a&gt; describe a Space&apos;s sources as &quot;automatically updated as they change, making Copilot an evergreen expert in your project,&quot; which is a real strength: the set stays fresh without anyone re-uploading. The &lt;a href=&quot;https://github.blog/changelog/2026-04-01-research-plan-and-code-with-copilot-cloud-agent/&quot;&gt;Copilot cloud agent&lt;/a&gt;, renamed from Copilot coding agent in GitHub&apos;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=&quot;https://docs.github.com/en/copilot/concepts/agents/cloud-agent/about-cloud-agent&quot;&gt;GitHub&apos;s own documentation&lt;/a&gt; is blunt about the limit: Copilot &quot;cannot make changes across multiple repositories in one run,&quot; 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=&quot;https://riftmap.dev/blog/how-to-give-copilot-cross-repo-context/&quot;&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.&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;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;p&gt;Read down the last column. Every &quot;cannot&quot; 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&apos;s &quot;cannot&quot; 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&apos;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&apos;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&apos;s clothing, and it is the thing you were trying to find out.&lt;/p&gt;
&lt;p&gt;This is why &quot;just use a bigger context window&quot; and &quot;just embed everything&quot; 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=&quot;https://riftmap.dev/blog/inferred-context-is-not-a-dependency-graph/&quot;&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=&quot;https://riftmap.dev/blog/symbol-graphs-and-artifact-graphs/&quot;&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&apos;s ability to see their consequences, which I have &lt;a href=&quot;https://riftmap.dev/blog/ai-doesnt-understand-blast-radius/&quot;&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&apos;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=&quot;https://docs.qodo.ai/governance/cross-repo-code-review&quot;&gt;docs&lt;/a&gt; define a relationship type where, in their words, &quot;One repository depends on a build artifact, container image, or pipeline output produced by the other.&quot; 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&apos;s docs disclose the mechanism: &quot;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.&quot; 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 &quot;By default, Qodo targets the main branch of each connected repository,&quot; 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&apos;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;/p&gt;
&lt;pre&gt;&lt;code&gt;# 1. resolve the one repo the assistant already has
curl -s -H &quot;X-API-Key: $RIFTMAP_API_KEY&quot; \
  &quot;https://api.riftmap.dev/api/v1/repositories/lookup?url=https://github.com/cloudposse/terraform-aws-ssm-parameter-store&quot;
# → { &quot;id&quot;: &quot;54052e08-b43f-4b67-975b-d869e9cdc62a&quot;, ... }

# 2. ask the graph what transitively depends on it
curl -s -H &quot;X-API-Key: $RIFTMAP_API_KEY&quot; \
  &quot;https://api.riftmap.dev/api/v1/repositories/54052e08-b43f-4b67-975b-d869e9cdc62a/impact&quot;
# → { &quot;total_affected&quot;: 22, &quot;max_depth_reached&quot;: 6, ... }
&lt;/code&gt;&lt;/pre&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&apos;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&apos;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&apos;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&apos;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&apos;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 &quot;who else breaks&quot; 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;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;mcpServers&quot;: {
    &quot;riftmap&quot;: {
      &quot;command&quot;: &quot;npx&quot;,
      &quot;args&quot;: [
        &quot;-y&quot;, &quot;@ivotoby/openapi-mcp-server&quot;,
        &quot;--tag&quot;, &quot;repositories&quot;, &quot;--tag&quot;, &quot;impact&quot;
      ],
      &quot;env&quot;: {
        &quot;OPENAPI_SPEC_PATH&quot;: &quot;https://app.riftmap.dev/openapi.json&quot;,
        &quot;API_BASE_URL&quot;: &quot;https://api.riftmap.dev/api/v1&quot;,
        &quot;API_HEADERS&quot;: &quot;X-API-Key:${RIFTMAP_API_KEY}&quot;
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That is the setup I walk through for &lt;a href=&quot;https://riftmap.dev/blog/claude-code-cursor-cross-repo-context/&quot;&gt;Claude Code and Cursor&lt;/a&gt;, against the &lt;a href=&quot;https://riftmap.dev/for-agents/&quot;&gt;endpoints an agent calls&lt;/a&gt;. The same graph answers at merge time, not just at planning time, and checking a PR&apos;s blast radius as a gate is &lt;a href=&quot;https://riftmap.dev/blog/can-ai-check-blast-radius-of-pr-before-merge/&quot;&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&apos;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&apos;s, that is what &lt;a href=&quot;https://riftmap.dev&quot;&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 &quot;who else breaks.&quot; 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&apos;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&apos;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;
</content:encoded><category>blast-radius</category><category>ai-coding-assistants</category><category>cross-repo-dependencies</category><category>dependency-graph</category><category>claude-code</category><category>cursor</category><category>github-copilot</category><author>Daniel Westgaard</author></item><item><title>Blast radius in software engineering: what it is, how to measure it, how to contain it</title><link>https://riftmap.dev/blog/blast-radius-software-engineering/</link><guid isPermaLink="true">https://riftmap.dev/blog/blast-radius-software-engineering/</guid><description>Blast radius is really two measurements — the code radius your tests catch, and the artifact radius that crosses repos. In one org, 147 repos hit one module.</description><pubDate>Wed, 08 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&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=&quot;https://en.wikipedia.org/wiki/Npm_left-pad_incident&quot;&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;&quot;Blast radius&quot; 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&apos;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=&quot;https://www.cortex.io/post/ai-is-making-engineering-faster-but-not-better-state-of-ai-benchmark-2026&quot;&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=&quot;https://tech.yahoo.com/ai/deals/articles/amazon-investigates-trend-incidents-high-115550319.html&quot;&gt;reported by the Financial Times&lt;/a&gt;, &quot;high blast radius&quot;, 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=&quot;https://loomai.io/glossary/blast-radius-analysis.html&quot;&gt;LOOM&apos;s glossary&lt;/a&gt; scopes it to &quot;how many functions, classes, and files are affected when a single code element changes&quot;. &lt;a href=&quot;https://axiomrefract.com/learn/what-is-blast-radius&quot;&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=&quot;https://recursive.pm/blast-radius&quot;&gt;Recursive&lt;/a&gt; pushes the other way, all the way out to business impact, features and customers and revenue. &lt;a href=&quot;https://sixdegree.ai/blog/blast-radius-analysis&quot;&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 &quot;what imports this&quot; 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 &quot;which of my tests could this break&quot; 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&apos;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=&quot;https://dora.dev/guides/dora-metrics-four-keys/&quot;&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=&quot;https://riftmap.dev/blog/measure-your-change-failure-rate/&quot;&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&apos;s most recent completed scan, 2026-06-28. That edge is a first-class cross-repo dependency, and no single repo&apos;s BFS, test selection, or CFR contains it.&lt;/p&gt;
&lt;h2&gt;The radius you can&apos;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=&quot;https://riftmap.dev/what-is-cross-repo-dependency-mapping/&quot;&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=&quot;https://riftmap.dev/blog/dependency-bus-factor/&quot;&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&apos;s clone structurally cannot see the other side of these edges, which is why &lt;a href=&quot;https://riftmap.dev/blog/ai-doesnt-understand-blast-radius/&quot;&gt;AI agents in particular miss the cross-repo radius&lt;/a&gt; and why the &lt;a href=&quot;https://riftmap.dev/ai-agent-blast-radius/&quot;&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&apos;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=&quot;https://docs.aws.amazon.com/wellarchitected/latest/reliability-pillar/rel_fault_isolation_use_bulkhead.html&quot;&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=&quot;https://www.deployhq.com/blog/smoother-deployments-with-canary-releases-a-code-centric-approach&quot;&gt;&quot;Reduced blast radius. A bug that ships to 5% of traffic affects 5% of users, not 100%.&quot;&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&apos;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&apos; 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 &quot;147 would break&quot; but &quot;147 consume this, and only 9 are not already on &lt;code&gt;0.25.0&lt;/code&gt;&quot;. &lt;code&gt;terraform-null-label&lt;/code&gt; is unusually central even for its own org, so read 147 as this estate&apos;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=&quot;https://riftmap.dev/blog/what-242-cloudposse-repos-depend-on/&quot;&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&apos;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 &quot;Transitive downstream blast radius (repo-level BFS)&quot;, and it says in as many words that it &quot;does not trace specific files, jobs, or symbols within the source repo&quot;. 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=&quot;https://sixdegree.ai/blog/blast-radius-analysis&quot;&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=&quot;https://riftmap.dev/blog/declared-inferred-registered/&quot;&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=&quot;https://github.com/cajasmota/grafel&quot;&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=&quot;https://app.riftmap.dev&quot;&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 &quot;it worked on my branch&quot; and &quot;it took down three teams&quot; 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;
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;
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;
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&apos;s the difference between blast radius in AWS and the blast radius of a code change?&lt;/strong&gt;
&quot;Blast radius&quot; 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;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;
</content:encoded><category>blast-radius</category><category>dependency-graph</category><category>platform-engineering</category><category>terraform</category><category>cross-repo</category><author>Daniel Westgaard</author></item><item><title>What 242 Cloud Posse repos actually depend on</title><link>https://riftmap.dev/blog/what-242-cloudposse-repos-depend-on/</link><guid isPermaLink="true">https://riftmap.dev/blog/what-242-cloudposse-repos-depend-on/</guid><description>A Riftmap scan of 242 Cloud Posse repos found 147 declaring terraform-null-label, 133 on context.tf line 24, plus what grep, symbol graphs, and catalogs miss.</description><pubDate>Sun, 05 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&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=&quot;https://github.com/cloudposse/terraform-null-label&quot;&gt;README&lt;/a&gt; calls it a &quot;Terraform module designed to generate consistent names and tags for resources,&quot; 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=&quot;https://github.com/cloudposse/terraform-null-label/network/dependents&quot;&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: &quot;We haven&apos;t found any dependents for this repository yet.&quot;&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&apos;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=&quot;https://github.com/cloudposse&quot;&gt;Its GitHub org bio&lt;/a&gt; calls it a &quot;DevOps Accelerator for AWS Infrastructure.&quot; &lt;a href=&quot;https://docs.cloudposse.com/community/contribute/automated-testing/&quot;&gt;Its own docs&lt;/a&gt; describe running one test workflow &quot;for all of our hundreds of terraform modules.&quot; Its flagship non-module project, &lt;a href=&quot;https://github.com/cloudposse/atmos&quot;&gt;Atmos&lt;/a&gt;, is a &quot;Terraform Orchestration Tool for DevOps&quot; 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&apos;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&apos;s reference-architecture &quot;components&quot; 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&apos;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=&quot;https://riftmap.dev/blog/declared-inferred-registered/&quot;&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=&quot;https://riftmap.dev/what-is-cross-repo-dependency-mapping/&quot;&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=&quot;https://riftmap.dev/blog/what-56-prometheus-repos-depend-on/&quot;&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=&quot;https://riftmap.dev/blog/what-208-kubernetes-sigs-repos-actually-depend-on/&quot;&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&apos;s own documented convention. Their &lt;a href=&quot;https://docs.cloudposse.com/learn/conventions/&quot;&gt;conventions doc&lt;/a&gt; explains it plainly: &quot;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.&quot; 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=&quot;https://docs.cloudposse.com/learn/component-development/faq/&quot;&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 &quot;this&quot;&lt;/code&gt; block in the same place every time.&lt;/p&gt;
&lt;p&gt;Here is the &lt;code&gt;module &quot;this&quot;&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;/p&gt;
&lt;pre&gt;&lt;code&gt;module &quot;this&quot; {
  source  = &quot;cloudposse/label/null&quot;
  version = &quot;0.25.0&quot; # requires Terraform &amp;gt;= 0.13.0
&lt;/code&gt;&lt;/pre&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&apos;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 &quot;every one of the 147.&quot; 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 &quot;they all do it the same way&quot; 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. 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: &quot;9 lagging&quot; is a recency cut and &quot;8 on a git-tag path&quot; 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=&quot;https://riftmap.dev/blog/blast-radius-gate-merge-pipeline/&quot;&gt;Post B&apos;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&apos;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;&apos;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 &quot;cloudposse/label/null&quot;&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&apos;s &lt;code&gt;source&lt;/code&gt; is HCL configuration data, not a symbol in any language a SCIP indexer parses. Sourcegraph&apos;s own &lt;a href=&quot;https://sourcegraph.com/docs/code-navigation/writing-an-indexer&quot;&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=&quot;https://riftmap.dev/blog/symbol-graphs-and-artifact-graphs/&quot;&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=&quot;https://backstage.io/docs/features/software-catalog/well-known-relations/&quot;&gt;Backstage relation&lt;/a&gt;, in Backstage&apos;s own words, &quot;is commonly generated based on &lt;code&gt;spec.dependsOn&lt;/code&gt; of the component or resource in question,&quot; and &lt;a href=&quot;https://docs.port.io/build-your-software-catalog/custom-integration/iac/terraform/&quot;&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&apos;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;aside&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;a href=&quot;https://riftmap.dev/showcase/cloudposse/&quot;&gt;Explore the Cloud Posse graph →&lt;/a&gt;
&lt;/aside&gt;
&lt;h2&gt;What Riftmap didn&apos;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 &quot;we missed some.&quot; 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 &quot;do we think this edge is real,&quot; it is &lt;a href=&quot;https://riftmap.dev/blog/declared-inferred-registered/&quot;&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 &quot;what an org actually depends on,&quot; it sits alongside the &lt;a href=&quot;https://riftmap.dev/blog/what-56-prometheus-repos-depend-on/&quot;&gt;Prometheus&lt;/a&gt; and &lt;a href=&quot;https://riftmap.dev/blog/what-208-kubernetes-sigs-repos-actually-depend-on/&quot;&gt;kubernetes-sigs&lt;/a&gt; scans, three real orgs with three genuinely different shapes: hub-and-spoke, federation, keystone. As the fourth &quot;blast radius before merge&quot; post, it is the worked org behind the argument the other three make in the abstract: &lt;a href=&quot;https://riftmap.dev/blog/can-ai-check-blast-radius-of-pr-before-merge/&quot;&gt;Post A&apos;s three layers&lt;/a&gt;, where this post&apos;s contrast of what each detection method returns comes from; &lt;a href=&quot;https://riftmap.dev/blog/blast-radius-gate-merge-pipeline/&quot;&gt;Post B&apos;s merge gate&lt;/a&gt;, whose impact contract produced the flat 147; and &lt;a href=&quot;https://riftmap.dev/blog/declared-inferred-registered/&quot;&gt;Post C&apos;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=&quot;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&quot;&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
organisation — Terraform, Docker, CI templates, Helm, and more. One read-only
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;
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;
147 of Cloud Posse&apos;s 242 repos, which is 61% of the org, each declaring it through a Terraform &lt;code&gt;module { source = &quot;cloudposse/label/null&quot; }&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&apos;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&apos;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;
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;
</content:encoded><category>cross-repo-dependencies</category><category>blast-radius</category><category>terraform</category><category>dependency-mapping</category><category>platform-engineering</category><author>Daniel Westgaard</author></item><item><title>Declared, inferred, registered: the three ways a tool knows a cross-repo dependency exists</title><link>https://riftmap.dev/blog/declared-inferred-registered/</link><guid isPermaLink="true">https://riftmap.dev/blog/declared-inferred-registered/</guid><description>Parsed vs inferred is a two-horse race that hides a third runner: registered. How declared, inferred, and registered dependency tools each know an edge exists.</description><pubDate>Sat, 04 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&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&apos;s &lt;code&gt;Chart.yaml&lt;/code&gt; carries a &lt;code&gt;dependencies:&lt;/code&gt; block:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dependencies:
  - name: redis-ha
    version: 4.38.0
    repository: https://dandydeveloper.github.io/charts/
&lt;/code&gt;&lt;/pre&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;/p&gt;
&lt;pre&gt;&lt;code&gt;module &quot;label&quot; {
  source  = &quot;cloudposse/label/null&quot;
  version = &quot;0.25.0&quot;
}
&lt;/code&gt;&lt;/pre&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;/p&gt;
&lt;pre&gt;&lt;code&gt;FROM ${CILIUM_RUNTIME_IMAGE} AS release
&lt;/code&gt;&lt;/pre&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;&quot;Parsed, not inferred&quot; 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 &quot;parsed, not inferred.&quot; 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=&quot;https://riftmap.dev/blog/can-ai-check-blast-radius-of-pr-before-merge/&quot;&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 &quot;parsed&quot; 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 &quot;config files&quot;:&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&apos;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&apos;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 &quot;there is a dependency somewhere in this YAML.&quot; 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&apos;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&apos;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=&quot;https://riftmap.dev/blog/symbol-graphs-and-artifact-graphs/&quot;&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&apos;s own writeup on &lt;a href=&quot;https://sourcegraph.com/blog/cross-repository-code-navigation&quot;&gt;cross-repository code navigation&lt;/a&gt; describes how SCIP&apos;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&apos;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 = &quot;cloudposse/label/null&quot; }&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&apos;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=&quot;https://riftmap.dev/blog/what-242-cloudposse-repos-depend-on/&quot;&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&apos;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&apos;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=&quot;https://arxiv.org/abs/2409.00159&quot;&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=&quot;https://arxiv.org/abs/2601.08773&quot;&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 &quot;how likely we think this edge is real,&quot; 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&apos;s insinuation. It is Backstage&apos;s own documented behaviour: &lt;a href=&quot;https://github.com/backstage/backstage/issues/20030&quot;&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&apos;s CTO makes the maintenance case directly, though as an interested party. As he puts it on Port&apos;s blog, &lt;a href=&quot;https://www.port.io/blog/what-are-the-technical-disadvantages-of-backstage&quot;&gt;&quot;YAMLs require maintenance when code changes occur. This results in outdated information that can affect operations and decision-making…&quot;&lt;/a&gt;. And from the adoption side, Roadie, a Backstage-ecosystem vendor and not a rival, reports two customers reaching &lt;a href=&quot;https://roadie.io/blog/3-strategies-for-a-complete-software-catalog&quot;&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=&quot;https://riftmap.dev/blog/backstage-alternatives/&quot;&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=&quot;https://riftmap.dev/blog/can-ai-check-blast-radius-of-pr-before-merge/&quot;&gt;the first post in this series&lt;/a&gt;. &lt;a href=&quot;https://sixdegree.ai/blog/blast-radius-analysis&quot;&gt;SixDegree&lt;/a&gt; calls it &quot;discovered&quot;, 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 &quot;we think this edge is real.&quot; It is resolution confidence: how cleanly the declared reference matched a known target in your org. The resolver&apos;s own dataclass documents it in one line. &lt;code&gt;&quot;&quot;&quot;Resolution confidence. 1.0 = exact match; lower = heuristic.&quot;&quot;&quot;&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=&quot;https://sourcegraph.com/docs/code-navigation/precise-code-navigation&quot;&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, &quot;when precise navigation is not available.&quot; 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=&quot;https://riftmap.dev/blog/blast-radius-gate-merge-pipeline/&quot;&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 &quot;declared edges are always at least 0.8.&quot; 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 &quot;how cleanly did this resolve.&quot; It never answers &quot;do we think this is real,&quot; 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&apos;s clone can show you, &lt;a href=&quot;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&quot;&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
organisation — Terraform, Docker, CI templates, Helm, and more. One read-only
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&apos;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 &quot;parsed vs inferred&quot; 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;
</content:encoded><category>cross-repo-dependencies</category><category>blast-radius</category><category>dependency-graph</category><category>platform-engineering</category><category>infrastructure-as-code</category><author>Daniel Westgaard</author></item><item><title>How to add a blast-radius gate to your merge pipeline</title><link>https://riftmap.dev/blog/blast-radius-gate-merge-pipeline/</link><guid isPermaLink="true">https://riftmap.dev/blog/blast-radius-gate-merge-pipeline/</guid><description>A pull request to a repository that a hundred others build on should not merge with one approval from a phone. A CI gate that routes review by measured downstream exposure — two HTTP calls, about forty lines, GitLab CI or GitHub Actions, no cloud credentials in the pipeline.</description><pubDate>Fri, 03 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&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;hr /&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&apos;s answer to this has arrived as a wave of pre-merge blast-radius gates, and they are worth taking seriously. &lt;a href=&quot;https://github.com/overmindtech/actions&quot;&gt;Overmind&lt;/a&gt; ships a GitHub Action that submits each pull request&apos;s Terraform plan and comments the blast radius straight onto the PR. An &lt;a href=&quot;https://dev.to/aws-builders/terraform-plan-shows-what-youre-changing-blast-radius-shows-what-youre-breaking-3324&quot;&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&apos;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=&quot;https://riftmap.dev/blog/can-ai-check-blast-radius-of-pr-before-merge/&quot;&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&apos;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;/p&gt;
&lt;pre&gt;&lt;code&gt;# 1. Resolve owner/repo to its Riftmap id.
REPO_ID=$(curl -sf -H &quot;X-API-Key: $RIFTMAP_API_KEY&quot; \
  &quot;https://api.riftmap.dev/api/v1/repositories/lookup?full_path=$REPO_PATH&quot; | jq -r &apos;.id&apos;)

# 2. Ask who declares a dependency on it.
curl -sf -H &quot;X-API-Key: $RIFTMAP_API_KEY&quot; \
  &quot;https://api.riftmap.dev/api/v1/repositories/$REPO_ID/impact?max_depth=3&amp;amp;min_confidence=0.8&quot;
&lt;/code&gt;&lt;/pre&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=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-terraform-module/&quot;&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=&quot;https://riftmap.dev/blog/declared-inferred-registered/&quot;&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;/p&gt;
&lt;pre&gt;&lt;code&gt;# .gitlab-ci.yml
blast-radius:
  stage: test
  image: alpine:3.20
  rules:
    - if: $CI_PIPELINE_SOURCE == &quot;merge_request_event&quot;
  variables:
    RIFTMAP_BASE_URL: &quot;https://api.riftmap.dev/api/v1&quot;
    THRESHOLD: &quot;10&quot;                    # direct consumers that warrant the review lane
  before_script:
    - apk add --no-cache curl jq
  script:
    - |
      # GitLab hands the job this repo&apos;s path for free.
      REPO_ID=$(curl -sf -H &quot;X-API-Key: $RIFTMAP_API_KEY&quot; \
        &quot;$RIFTMAP_BASE_URL/repositories/lookup?full_path=$CI_PROJECT_PATH&quot; | jq -r &apos;.id&apos;)

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

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

      DIRECT=$(echo &quot;$IMPACT&quot; | jq &apos;[.affected_repositories[] | select(.depth == 1)] | length&apos;)
      TOTAL=$(echo &quot;$IMPACT&quot;  | jq &apos;.total_affected&apos;)

      echo &quot;Downstream consumers: $DIRECT direct, $TOTAL transitive.&quot;
      if [ &quot;$DIRECT&quot; -ge &quot;$THRESHOLD&quot; ]; then
        echo &quot;Over threshold ($THRESHOLD): this change touches a high-fan-in repository.&quot;
      fi
&lt;/code&gt;&lt;/pre&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&apos;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&apos;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;/p&gt;
&lt;pre&gt;&lt;code&gt;# .github/workflows/blast-radius.yml
name: Blast radius
on:
  pull_request:
    branches: [main]

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

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

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

          DIRECT=$(echo &quot;$IMPACT&quot; | jq &apos;[.affected_repositories[] | select(.depth == 1)] | length&apos;)
          TOTAL=$(echo &quot;$IMPACT&quot;  | jq &apos;.total_affected&apos;)

          echo &quot;### Blast radius: $DIRECT direct consumers, $TOTAL transitive&quot; &amp;gt;&amp;gt; &quot;$GITHUB_STEP_SUMMARY&quot;
&lt;/code&gt;&lt;/pre&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=&quot;https://riftmap.dev/blog/ai-doesnt-understand-blast-radius/&quot;&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;/p&gt;
&lt;pre&gt;&lt;code&gt;        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          PR=${{ github.event.pull_request.number }}
          if [ &quot;$DIRECT&quot; -ge &quot;$THRESHOLD&quot; ]; then
            gh pr edit &quot;$PR&quot; --add-label &quot;blast-radius/high&quot;
            gh pr comment &quot;$PR&quot; --body \
              &quot;This change affects **$DIRECT** repositories directly ($TOTAL transitively). Engaging the shared-artifact review lane.&quot;
          fi
&lt;/code&gt;&lt;/pre&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&apos;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;/p&gt;
&lt;pre&gt;&lt;code&gt;      curl -sf --request POST \
        --header &quot;PRIVATE-TOKEN: $GITLAB_NOTE_TOKEN&quot; \
        --data-urlencode &quot;body=This change affects $DIRECT repositories directly ($TOTAL transitively).&quot; \
        &quot;$CI_API_V4_URL/projects/$CI_PROJECT_ID/merge_requests/$CI_MERGE_REQUEST_IID/notes&quot;
&lt;/code&gt;&lt;/pre&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=&quot;https://riftmap.dev/blog/can-ai-check-blast-radius-of-pr-before-merge/&quot;&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=&quot;https://riftmap.dev/blog/version-constraints-across-real-terraform-estates/&quot;&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=&quot;https://riftmap.dev/blog/what-242-cloudposse-repos-depend-on/&quot;&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;/p&gt;
&lt;pre&gt;&lt;code&gt;      [ &quot;$DIRECT&quot; -ge &quot;$THRESHOLD&quot; ] &amp;amp;&amp;amp; exit 1 || true
&lt;/code&gt;&lt;/pre&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&apos;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 &quot;who is downstream,&quot; not &quot;does this change break them.&quot; 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;/p&gt;
&lt;pre&gt;&lt;code&gt;# capture the whole lookup response, not just .id
REPO=$(curl -sf -H &quot;X-API-Key: $RIFTMAP_API_KEY&quot; \
  &quot;$RIFTMAP_BASE_URL/repositories/lookup?full_path=$REPO_PATH&quot;)
REPO_ID=$(echo &quot;$REPO&quot; | jq -r &apos;.id&apos;)

STALE=$(echo &quot;$REPO&quot; | jq &apos;if .last_activity_at &amp;gt; .last_scanned_at then true else false end&apos;)
[ &quot;$STALE&quot; = &quot;true&quot; ] &amp;amp;&amp;amp; echo &quot;Note: this repo has changed since Riftmap last scanned it; the count may be behind.&quot;
&lt;/code&gt;&lt;/pre&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&apos;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=&quot;https://riftmap.dev/blog/gitlab-orbit-and-the-artifact-layer/&quot;&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&apos;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=&quot;https://riftmap.dev/blog/riftmap-vs-overmind/&quot;&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&apos;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&apos;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;
</content:encoded><category>blast-radius</category><category>ci-cd</category><category>platform-engineering</category><category>github-actions</category><category>gitlab-ci</category><category>cross-repo-dependencies</category><author>Daniel Westgaard</author></item><item><title>Can AI check the blast radius of a PR before you merge?</title><link>https://riftmap.dev/blog/can-ai-check-blast-radius-of-pr-before-merge/</link><guid isPermaLink="true">https://riftmap.dev/blog/can-ai-check-blast-radius-of-pr-before-merge/</guid><description>Yes — but only if a dependency graph exists for it to query. At least three different graphs are being sold under one phrase, and each is blind to a different kind of change. Here&apos;s what each one genuinely sees, walked end to end on a real public org.</description><pubDate>Thu, 02 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&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;hr /&gt;
&lt;p&gt;In the past few weeks I have watched three different products promise the same sentence. Overmind&apos;s homepage now leads with engineers seeing the &lt;a href=&quot;https://overmind.tech&quot;&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=&quot;https://medium.com/@poojabhavani19/pre-merge-blast-radius-analysis-with-the-gitlab-orbit-knowledge-graph-f49f3e181e89&quot;&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=&quot;https://docs.port.io/guides/all/calculate-blast-radius-with-ai/&quot;&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;&quot;Blast radius before merge&quot; 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=&quot;https://riftmap.dev/blog/riftmap-vs-overmind/&quot;&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&apos;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 &quot;who calls, imports, or references the code symbols this diff touches&quot;, 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&apos;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=&quot;https://riftmap.dev/blog/gitlab-orbit-and-the-artifact-layer/&quot;&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=&quot;https://riftmap.dev/blog/cross-repo-edge-composition/&quot;&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=&quot;https://riftmap.dev/blog/symbol-graphs-and-artifact-graphs/&quot;&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 &quot;what, in the cloud that is actually running, does this Terraform plan affect&quot;, 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 &quot;this plan will take down something nobody wrote down&quot;, Overmind&apos;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&apos;s graph is plan-scoped and runtime-scoped. Riftmap&apos;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=&quot;https://riftmap.dev/blog/riftmap-vs-overmind/&quot;&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 &quot;which repositories declare a build-time dependency on the thing this PR changes&quot;, and in today&apos;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=&quot;https://github.com/cloudposse&quot;&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;
 The PR we will pretend to open is against [terraform-null-label](https://github.com/cloudposse/terraform-null-label), their naming and tagging convention module. Say it is a release-prep PR for a new tag that renames an output.
&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;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -s &quot;https://api.riftmap.dev/api/v1/repositories/{repo_id}/impact?max_depth=10&quot; \
  -H &quot;Authorization: Bearer $RIFTMAP_TOKEN&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;source_repository&quot;: {
    &quot;name&quot;: &quot;terraform-null-label&quot;,
    &quot;full_path&quot;: &quot;cloudposse/terraform-null-label&quot;
  },
  &quot;affected_repositories&quot;: [
    { &quot;full_path&quot;: &quot;cloudposse/terraform-aws-acm-request-certificate&quot;, &quot;depth&quot;: 1, &quot;confidence&quot;: 1.0 },
    { &quot;full_path&quot;: &quot;cloudposse/terraform-aws-alb&quot;,                     &quot;depth&quot;: 1, &quot;confidence&quot;: 1.0 },
    { &quot;full_path&quot;: &quot;cloudposse/terraform-aws-alb-ingress&quot;,             &quot;depth&quot;: 1, &quot;confidence&quot;: 1.0 }
  ],
  &quot;total_affected&quot;: 147,
  &quot;max_depth_reached&quot;: 1
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That response is real and only trimmed. 147 repositories, 61% of the org&apos;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;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;artifact&quot;: { &quot;artifact_type&quot;: &quot;terraform_module&quot;, &quot;name&quot;: &quot;cloudposse/terraform-null-label&quot;, &quot;consumer_count&quot;: 147 },
  &quot;latest_version&quot;: &quot;0.25.0&quot;,
  &quot;consumers_on_latest&quot;: 138,
  &quot;consumers_lagging&quot;: 9,
  &quot;state_breakdown&quot;: { &quot;pinned&quot;: 139, &quot;floating&quot;: 0, &quot;branch&quot;: 8, &quot;absent&quot;: 0 },
  &quot;consumers&quot;: [
    { &quot;repository&quot;: { &quot;full_path&quot;: &quot;cloudposse/terraform-aws-elastic-beanstalk-application&quot; },
      &quot;version_constraint&quot;: &quot;0.25.0&quot;,     &quot;version_constraint_state&quot;: &quot;pinned&quot;, &quot;source_file&quot;: &quot;context.tf&quot;, &quot;source_line&quot;: 24 },
    { &quot;repository&quot;: { &quot;full_path&quot;: &quot;cloudposse/terraform-aws-cloudwatch-flow-logs&quot; },
      &quot;version_constraint&quot;: &quot;tags/0.3.1&quot;, &quot;version_constraint_state&quot;: &quot;branch&quot;, &quot;source_file&quot;: &quot;kinesis.tf&quot;,  &quot;source_line&quot;: 2 },
    { &quot;repository&quot;: { &quot;full_path&quot;: &quot;cloudposse/terraform-aws-multi-az-subnets&quot; },
      &quot;version_constraint&quot;: &quot;0.24.1&quot;,     &quot;version_constraint_state&quot;: &quot;pinned&quot;, &quot;source_file&quot;: &quot;public.tf&quot;,   &quot;source_line&quot;: 10 }
  ]
}
&lt;/code&gt;&lt;/pre&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&apos;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=&quot;https://riftmap.dev/blog/version-constraints-across-real-terraform-estates/&quot;&gt;version constraints look like in real estates&lt;/a&gt; generally, and it is the evidence that &quot;everyone pins&quot; and &quot;everyone is current&quot; are very different sentences.&lt;/p&gt;
&lt;p&gt;Second, when a consumer does bump, null-label&apos;s outputs feed resource names and tags, so the consumer&apos;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=&quot;https://riftmap.dev/blog/inferred-context-is-not-a-dependency-graph/&quot;&gt;inferring it&lt;/a&gt; with embeddings or an LLM&apos;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&apos;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&apos;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=&quot;https://riftmap.dev/blog/what-242-cloudposse-repos-depend-on/&quot;&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&apos;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=&quot;https://riftmap.dev/blog/modeled-graphs-and-parsed-graphs/&quot;&gt;modeled graphs versus parsed graphs&lt;/a&gt; and about why hand-maintained catalogs &lt;a href=&quot;https://riftmap.dev/blog/the-catalog-maintenance-trap/&quot;&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&apos;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&apos;s risk lives where Cloud Posse&apos;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=&quot;https://riftmap.dev/blog/blast-radius-gate-merge-pipeline/&quot;&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=&quot;https://riftmap.dev/blog/declared-inferred-registered/&quot;&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=&quot;https://riftmap.dev/blog/which-ai-coding-assistants-see-blast-radius/&quot;&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: &quot;147 repositories declare this module at these file and line numbers&quot; 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;
</content:encoded><category>blast-radius</category><category>impact-analysis</category><category>pull-requests</category><category>ai-coding</category><category>terraform</category><category>platform-engineering</category><author>Daniel Westgaard</author></item><item><title>You can index every repo in Cursor. It still can&apos;t tell you what breaks.</title><link>https://riftmap.dev/blog/cursor-monorepo-indexing/</link><guid isPermaLink="true">https://riftmap.dev/blog/cursor-monorepo-indexing/</guid><description>How to tune Cursor&apos;s codebase index for a large monorepo or a fifty-repo workspace, and the one question no amount of tuning makes it answer.</description><pubDate>Wed, 01 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;How to tune Cursor&apos;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;hr /&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&apos;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=&quot;https://www.iamraghuveer.com/posts/cursor-codebase-indexing-monorepo/&quot;&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 &quot;the index spans all my services&quot; and &quot;the index maps how my services depend on each other&quot; are two different claims, and the distance between them is the whole second half of this post. I have made the &lt;a href=&quot;https://riftmap.dev/blog/the-repo-your-agent-didnt-clone/&quot;&gt;structural version of that argument before&lt;/a&gt;, and shown &lt;a href=&quot;https://riftmap.dev/blog/claude-code-cursor-cross-repo-context/&quot;&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&apos;s index actually is&lt;/h2&gt;
&lt;p&gt;Cursor&apos;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=&quot;https://cursor.com/blog/secure-codebase-indexing&quot;&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&apos;s &lt;a href=&quot;https://cursor.com/blog/secure-codebase-indexing&quot;&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&apos;s Agent &lt;a href=&quot;https://cursor.com/docs/agent/tools/search&quot;&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&apos;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&apos;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=&quot;https://cursor.com/blog/secure-codebase-indexing&quot;&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=&quot;https://www.rapidevelopers.com/cursor-tutorial/how-to-manage-cursor-ai-s-context-window-when-developing-large-monorepos-with-multiple-packages&quot;&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;/p&gt;
&lt;pre&gt;&lt;code&gt;# Indexes everything under the root, slowly
cursor /path/to/monorepo

# Indexes one package, fast
cursor /path/to/monorepo/packages/api
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A shell alias per package you live in (&lt;code&gt;alias ca=&apos;cursor /path/to/monorepo/packages/api&apos;&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;/p&gt;
&lt;pre&gt;&lt;code&gt;# .cursorindexingignore
# Kept out of the index, still reachable with @Files.
tests/fixtures/
e2e/recordings/
packages/legacy/

# .cursorignore
# Invisible to indexing AND to all AI features.
.env*
secrets/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There is one detail in Cursor&apos;s &lt;a href=&quot;https://cursor.com/docs/reference/ignore-file&quot;&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=&quot;https://cursor.com/docs/agent/tools/search&quot;&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 &quot;how does the orders service validate a token&quot; 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 &quot;microservices graph explorer&quot; 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=&quot;https://cursor.com/docs/context/rules&quot;&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=&quot;https://riftmap.dev/blog/the-catalog-maintenance-trap/&quot;&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&apos;s index configuration from repeating itself.&lt;/p&gt;
&lt;h3&gt;The freshness you&apos;re actually working with&lt;/h3&gt;
&lt;p&gt;Cursor&apos;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&apos; 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&apos;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=&quot;https://riftmap.dev/showcase/prometheus/&quot;&gt;Prometheus organisation&lt;/a&gt;: as of Riftmap&apos;s May 2026 scan, fifty-six repositories, and when you &lt;a href=&quot;https://riftmap.dev/blog/what-56-prometheus-repos-depend-on/&quot;&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=&quot;https://riftmap.dev/showcase/prometheus/&quot;&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;&quot;We bumped the dependency&quot; and &quot;we bumped every reference to the dependency&quot; 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 &quot;the second &lt;code&gt;go.mod&lt;/code&gt; that requires this&quot;. 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 &quot;what code resembles this&quot;, and it answers it well. The question a breaking change asks is &quot;what declares a dependency on this&quot;, 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&apos;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=&quot;https://riftmap.dev/blog/series/find-every-consumer/&quot;&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=&quot;https://riftmap.dev/blog/inferred-context-is-not-a-dependency-graph/&quot;&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=&quot;https://riftmap.dev/for-agents/&quot;&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&apos;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=&quot;https://riftmap.dev/showcase/prometheus/&quot;&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=&quot;https://riftmap.dev/blog/claude-code-cursor-cross-repo-context/&quot;&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&apos;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&apos;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;
</content:encoded><category>cursor</category><category>monorepo</category><category>codebase-indexing</category><category>multi-repo</category><author>Daniel Westgaard</author></item><item><title>Claude Code reads your clone. Cursor reads similarity. Neither sees the graph.</title><link>https://riftmap.dev/blog/claude-code-cursor-cross-repo-context/</link><guid isPermaLink="true">https://riftmap.dev/blog/claude-code-cursor-cross-repo-context/</guid><description>Both agents can be handed more than one repo — but Claude Code only sees your clone and Cursor&apos;s index answers similarity. How to wire in the dependency graph.</description><pubDate>Tue, 30 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&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&apos;s clone and Cursor&apos;s index each stop.&lt;/em&gt;&lt;/p&gt;
&lt;hr /&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&apos;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=&quot;https://riftmap.dev/blog/how-to-give-copilot-cross-repo-context/&quot;&gt;Copilot version of this question&lt;/a&gt; before, and made the case for &lt;a href=&quot;https://riftmap.dev/blog/the-repo-your-agent-didnt-clone/&quot;&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=&quot;https://code.claude.com/docs/en/claude-directory&quot;&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&apos;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&apos;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=&quot;https://cursor.com/blog/secure-codebase-indexing&quot;&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=&quot;https://cursor.com/docs/agent/tools/search&quot;&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 &quot;the agent can &lt;em&gt;see&lt;/em&gt; these repositories&quot; into &quot;the agent knows how these repositories &lt;em&gt;depend&lt;/em&gt; on each other.&quot; 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&apos;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;/p&gt;
&lt;pre&gt;&lt;code&gt;# At launch
claude --add-dir ../orders-service --add-dir ../platform-charts

# Or permanently, in .claude/settings.json
{
  &quot;permissions&quot;: {
    &quot;additionalDirectories&quot;: [&quot;../orders-service&quot;, &quot;../platform-charts&quot;]
  }
}
&lt;/code&gt;&lt;/pre&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=&quot;https://riftmap.dev/blog/repo-access-was-never-the-hard-part/&quot;&gt;Repo access was never the hard part&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;Write the map in &lt;a href=&quot;http://CLAUDE.md&quot;&gt;CLAUDE.md&lt;/a&gt;&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&apos;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=&quot;https://karun.me/blog/2026/03/26/structuring-claude-code-for-multi-repo-workspaces/&quot;&gt;layered the files org, team, and repo deep&lt;/a&gt; and concluded: &quot;I learned not to list repos here. Lists go stale. Instead, tell Claude where to look.&quot; 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=&quot;https://riftmap.dev/blog/the-catalog-maintenance-trap/&quot;&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=&quot;https://riftmap.dev/blog/the-repo-your-agent-didnt-clone/&quot;&gt;flagship&lt;/a&gt;: a &lt;a href=&quot;https://riftmap.dev/blog/symbol-graphs-and-artifact-graphs/&quot;&gt;symbol graph&lt;/a&gt; answers &quot;who calls this function&quot; 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=&quot;https://riftmap.dev/what-is-an-artifact-dependency-graph/&quot;&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&apos;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&apos;s &lt;a href=&quot;https://cursor.com/docs/agent/tools/search&quot;&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 &quot;how does the orders service validate a token&quot; 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; &quot;what depends on this base image&quot; 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=&quot;https://riftmap.dev/blog/inferred-context-is-not-a-dependency-graph/&quot;&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&apos;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;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&apos;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&apos;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;&quot;Where does this live&quot;, &quot;how does this work&quot;&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;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=&quot;https://riftmap.dev/blog/series/find-every-consumer/&quot;&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&apos;s question already exists in your organisation&apos;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=&quot;https://riftmap.dev/for-agents/&quot;&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=&quot;https://docs.riftmap.dev/agents/overview&quot;&gt;recommended pattern&lt;/a&gt; is three endpoints. Resolve the working tree to a node, hydrate that node&apos;s context in one round-trip, and ask for the transitive cascade when the change actually warrants it.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Resolve the working tree&apos;s clone URL to a Riftmap repo
REPO_ID=$(curl -s \
  &quot;https://api.riftmap.dev/api/v1/repositories/lookup?url=https://github.com/myorg/platform-charts&quot; \
  -H &quot;X-API-Key: $RIFTMAP_API_KEY&quot; | jq -r &apos;.id&apos;)

# One round-trip: the repo, its dependencies, its dependents, its artifacts
curl -s &quot;https://api.riftmap.dev/api/v1/repositories/$REPO_ID/context&quot; \
  -H &quot;X-API-Key: $RIFTMAP_API_KEY&quot;

# When the change is breaking, ask for the transitive blast radius
curl -s &quot;https://api.riftmap.dev/api/v1/repositories/$REPO_ID/impact?max_depth=3&quot; \
  -H &quot;X-API-Key: $RIFTMAP_API_KEY&quot;
&lt;/code&gt;&lt;/pre&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=&quot;https://riftmap.dev/for-agents/&quot;&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=&quot;https://docs.riftmap.dev/agents/mcp-cli-roadmap&quot;&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;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;mcpServers&quot;: {
    &quot;riftmap&quot;: {
      &quot;command&quot;: &quot;npx&quot;,
      &quot;args&quot;: [&quot;-y&quot;, &quot;@ivotoby/openapi-mcp-server&quot;],
      &quot;env&quot;: {
        &quot;OPENAPI_SPEC_PATH&quot;: &quot;https://app.riftmap.dev/openapi.json&quot;,
        &quot;API_BASE_URL&quot;: &quot;https://api.riftmap.dev/api/v1&quot;,
        &quot;API_HEADERS&quot;: &quot;X-API-Key:${RIFTMAP_API_KEY}&quot;
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&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&apos;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&apos;s own host.&lt;/p&gt;
&lt;p&gt;With either client, &quot;who depends on &lt;code&gt;platform-charts&lt;/code&gt;&quot; is now a tool call the agent makes itself, in Plan Mode or mid-task, with no HTTP in the prompt. When Riftmap&apos;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&apos;s path rather than its discretion, though it depends on the agent&apos;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&apos;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=&quot;https://www.mabl.com/blog/how-we-built-a-system-for-ai-agents-to-ship-real-code-across-75-repos&quot;&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&apos;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&apos;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;
</content:encoded><category>ai-coding-agents</category><category>cross-repo-context</category><category>claude-code</category><category>cursor</category><category>mcp</category><category>multi-repo</category><author>Daniel Westgaard</author></item><item><title>The repo your agent didn&apos;t clone is the one it breaks</title><link>https://riftmap.dev/blog/the-repo-your-agent-didnt-clone/</link><guid isPermaLink="true">https://riftmap.dev/blog/the-repo-your-agent-didnt-clone/</guid><description>Coding agents got cross-repo access this year. They still can&apos;t see the blast radius of a base-image bump — because that edge was never a function call, and it lives in a repo they never cloned.</description><pubDate>Mon, 29 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;Coding agents got cross-repo access this year. They still cannot see the blast radius of a base-image bump — because that edge was never a function call, and it lives in a repo they never opened.&lt;/em&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;Last spring someone posted a small VS Code extension to Hacker News. The pitch was one sentence from their own week. They had added a single field to a shared struct, and found out in CI, twenty minutes later, that &lt;a href=&quot;https://news.ycombinator.com/item?id=47246451&quot;&gt;417 files depended on it&lt;/a&gt;. So they built a tool that shows you that tree the moment you save, files colour-coded by how far downstream they sit. It is a good tool. Underneath it is a Rust CLI and tree-sitter, parsing your code into a graph of which file references which.&lt;/p&gt;
&lt;p&gt;Read that story again and notice what kind of dependency it actually is. A field on a struct. Files that reference the struct. Functions that call those functions. It is a &lt;a href=&quot;https://riftmap.dev/blog/symbol-graphs-and-artifact-graphs/&quot;&gt;symbol graph&lt;/a&gt;, it lives inside one repository, and tree-sitter can see all of it. This is the version of blast radius the industry has largely tooled. Sourcegraph indexes it. CodeLayers draws it on save. GitLab&apos;s new Knowledge Graph answers it over MCP. The most-quoted blast-radius anecdote on the internet is a within-repo, symbol-level story, answered by within-repo, symbol-level tools.&lt;/p&gt;
&lt;p&gt;Now hand the same engineer their next task, and hand it to an agent. Bump the base image three services are built on. Tighten a variable on a Terraform module that six repositories source. Rename a value key in a Helm chart that nine deployments inherit. The thing that breaks is real, and it is downstream, and it is not 417 files in this repository. It is eleven services in eleven other repositories, owned by teams who never read the changelog. And the edge that connects the change to the breakage was never a function call. tree-sitter will never find it. It was never in the code. It only ever existed in a manifest, in a repository the agent did not clone.&lt;/p&gt;
&lt;p&gt;So here is the claim this whole post is built on. An AI coding agent can see the repository it cloned. The edges that break when a shared base image, Terraform module, or Helm chart changes live in other repositories, declared as a Dockerfile &lt;code&gt;FROM&lt;/code&gt; line or a Terraform &lt;code&gt;source&lt;/code&gt; block, which means they sit outside the agent&apos;s context by construction, not by oversight. You cannot prompt your way past it. You have to give the agent the graph.&lt;/p&gt;
&lt;h2&gt;The agents&apos; own users have already asked for this&lt;/h2&gt;
&lt;p&gt;The clearest evidence that this is the live problem is that &lt;a href=&quot;https://riftmap.dev/blog/ai-coding-agents-need-cross-repo-context/&quot;&gt;the people running these agents&lt;/a&gt; are filing the feature request themselves, in the agents&apos; own issue trackers, in their own words. In February someone opened a request on the Claude Code repository for multi-repository sessions. The interesting part was not the ask. It was the reasoning. The author wanted the agent to &lt;a href=&quot;https://github.com/anthropics/claude-code/issues/23627&quot;&gt;discover additional repositories as it works, &quot;via dependency analysis, import tracing, or configuration references&quot;&lt;/a&gt;, because, in their words, &quot;I don&apos;t always know which repos are affected when I start the task.&quot; Their example stack was a shared API-contracts library, a Node backend, a React frontend, and a Kubernetes infrastructure repository.&lt;/p&gt;
&lt;p&gt;It is not one request. There is &lt;a href=&quot;https://github.com/anthropics/claude-code/issues/44656&quot;&gt;a second one&lt;/a&gt; that wants the agent to see the API handlers in &lt;code&gt;backend/&lt;/code&gt;, the types they share with &lt;code&gt;frontend/&lt;/code&gt;, and the Terraform definitions in &lt;code&gt;infra/&lt;/code&gt;, and to trace a payment flow end to end across all of them. There is &lt;a href=&quot;https://github.com/anthropics/claude-code/issues/35362&quot;&gt;a third&lt;/a&gt; asking for a &lt;code&gt;--workspace&lt;/code&gt; flag so a single session can coordinate a change to an API in one repository and its client in another. And over on the GitHub side there is the &lt;a href=&quot;https://github.com/orgs/community/discussions/189213&quot;&gt;Copilot request&lt;/a&gt; &lt;a href=&quot;https://riftmap.dev/blog/how-to-give-copilot-cross-repo-context/&quot;&gt;I have written about before&lt;/a&gt;, the web-app and orders-service pair where Copilot cannot detect that the frontend is calling an endpoint the backend just renamed, because the contract lives in a repository outside its scope.&lt;/p&gt;
&lt;p&gt;Four requests, three agents, one shape. None of these people are asking for a better model. They are asking for the thing the model cannot hold in its head: a &lt;a href=&quot;https://riftmap.dev/what-is-cross-repo-dependency-mapping/&quot;&gt;map of how the repositories depend on each other&lt;/a&gt;. Claude Code is not broken here, and neither is Copilot. They are consumers waiting for a graph that nobody has handed them yet. The rest of this post is about why the workarounds they reach for in the meantime all stop in the same place, and what the graph would have to parse to actually answer the question.&lt;/p&gt;
&lt;h2&gt;The workarounds, and where each one stops&lt;/h2&gt;
&lt;p&gt;Spend an hour in the support forums for any of the big coding agents and you will see the same three families of workaround, reached for in roughly this order. Each one solves a real problem. Each one stops at the same wall.&lt;/p&gt;
&lt;h3&gt;Widen the window&lt;/h3&gt;
&lt;p&gt;The first instinct is access. Open more repositories so the agent can read them. Claude Code ships &lt;code&gt;/add-dir&lt;/code&gt;, and the multi-repo feature requests above are mostly asking for the remote and team version of it. VS Code has multi-root workspaces, a &lt;code&gt;.code-workspace&lt;/code&gt; file listing several repository folders that open together. Copilot has Spaces, the one genuinely multi-repo surface GitHub ships, where you assemble a curated set of repositories and chat against it. These are the right first move, and at a handful of repositories they are often enough.&lt;/p&gt;
&lt;p&gt;But access is not structure. The agent can now read &lt;code&gt;orders-service&lt;/code&gt; while it edits &lt;code&gt;web-app&lt;/code&gt;. Nothing tells it that it should, or that a third repository consumes the same contract and is not in scope at all. Widening the window shows the agent more rooms. It does not hand it the floor plan. I made the longer version of this argument in &lt;a href=&quot;https://riftmap.dev/blog/repo-access-was-never-the-hard-part/&quot;&gt;Repo access was never the hard part&lt;/a&gt;, and it is the limit underneath this entire category.&lt;/p&gt;
&lt;h3&gt;Write the map by hand&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; era, files that more than sixty thousand repositories now carry, READMEs for agents that describe how the system fits together. Some teams go further and build a dedicated repository to hold it, an &lt;a href=&quot;https://seylox.github.io/2026/03/05/blog-agents-meta-repo-pattern.html&quot;&gt;agents meta-repository&lt;/a&gt; that serves as the agent&apos;s orientation and working memory. The most documented version is Mabl&apos;s, an 850-line repo coordination graph their agents query at planning time, which they credit with cutting context drift from roughly forty per cent of agent failures to under five. It works.&lt;/p&gt;
&lt;p&gt;It also decays, and the people writing these files know it. The most useful line I have read on the pattern comes from an engineer who &lt;a href=&quot;https://karun.me/blog/2026/03/26/structuring-claude-code-for-multi-repo-workspaces/&quot;&gt;layered the files org, team, and repo deep&lt;/a&gt; and concluded: &quot;I learned not to list repos here. Lists go stale. Instead, tell Claude where to look.&quot; 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 the 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 developer-portal catalog problem 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;Hand the agent a hosted index&lt;/h3&gt;
&lt;p&gt;The third instinct is the most sophisticated, and the closest to right. Stop writing the map by hand and serve a real index over a tool call. Augment exposes its context engine to other agents over MCP. There are &lt;a href=&quot;https://blackdoglabs.io/blog/claude-code-decoded-multi-repo-context&quot;&gt;custom MCP servers&lt;/a&gt; with &lt;code&gt;trace_dependency&lt;/code&gt; and &lt;code&gt;load_symbol&lt;/code&gt; tools that follow the call chain across repository boundaries. CodeLayers does it from tree-sitter. GitLab Orbit does it at GitLab scale, a property graph of code and the whole software lifecycle, queryable over MCP. These are real graphs, and an agent that calls one is genuinely better off than one navigating by a markdown file.&lt;/p&gt;
&lt;p&gt;But look at what every one of them indexes. Functions. Classes. Imports. Call edges. They are symbol graphs, and a symbol graph answers &quot;who calls this function&quot; beautifully. It does not answer &quot;what breaks if I change this base image,&quot; because the answer to that question is not made of function calls. I want to be fair to Orbit in particular, because it is the most serious graph anyone has shipped in this space and a very large, very well-funded proof that the category is real. I &lt;a href=&quot;https://riftmap.dev/blog/gitlab-orbit-and-the-artifact-layer/&quot;&gt;read everything they shipped&lt;/a&gt; before saying a word about it. And it still does not parse a single &lt;code&gt;FROM&lt;/code&gt; line.&lt;/p&gt;
&lt;p&gt;That is the wall all three families hit. Widen the window and you have access without structure. Write the map and you have structure that rots. Serve a symbol index and you have a real graph that stops at the language boundary. None of them parse the layer where a platform team&apos;s worst change actually lives.&lt;/p&gt;
&lt;h2&gt;The edge that was never a function call&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://riftmap.dev/ai-agent-blast-radius/&quot;&gt;Blast radius&lt;/a&gt; is two questions wearing one phrase, and almost every tool in this space answers the wrong one for infrastructure. Ask &quot;what breaks if I change this&quot; at the symbol layer and you get a graph of functions and their callers. Ask it at the infrastructure layer and you get a completely different graph, with completely different nodes and edges, built by a completely different parser.&lt;/p&gt;
&lt;p&gt;The nodes of that second graph are the things your repositories are actually made of and share between each other. A base image. A Terraform module. A Helm chart. A reusable CI template. The edges are the references that bind those things across repositories: a Dockerfile &lt;code&gt;FROM&lt;/code&gt; line pinning the image, a Terraform &lt;code&gt;source&lt;/code&gt; block pointing at the module repo, a &lt;code&gt;terraform_remote_state&lt;/code&gt; lookup, a Helm &lt;code&gt;dependencies&lt;/code&gt; entry or a value reference, a GitLab CI &lt;code&gt;include:project&lt;/code&gt;, a reusable Actions &lt;code&gt;uses:&lt;/code&gt;. These are not function calls and they are not in the code for a symbol graph to find. They are declared in manifests, deterministically, and they point at repositories the agent never opened.&lt;/p&gt;
&lt;p&gt;Side by side, the two graphs look like this.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Symbol graph&lt;/th&gt;
&lt;th&gt;Artifact graph&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Nodes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;functions, classes, files&lt;/td&gt;
&lt;td&gt;base images, Terraform modules, Helm charts, CI templates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Edges&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;calls and imports between symbols&lt;/td&gt;
&lt;td&gt;&lt;code&gt;FROM&lt;/code&gt; lines, &lt;code&gt;source&lt;/code&gt; blocks, &lt;code&gt;include:project&lt;/code&gt;, chart &lt;code&gt;dependencies&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Where the edge lives&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;in the code, in the repo you cloned&lt;/td&gt;
&lt;td&gt;in a manifest, in a repo you didn&apos;t clone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Answers&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;who calls this function&lt;/td&gt;
&lt;td&gt;what breaks if I change this base image&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;A symbol graph and an &lt;a href=&quot;https://riftmap.dev/what-is-an-artifact-dependency-graph/&quot;&gt;artifact graph&lt;/a&gt; are different categories, built by different parsers, and blast radius on an infrastructure change needs the second one.&lt;/p&gt;
&lt;p&gt;This is why the agent&apos;s blindness is structural rather than a gap a smarter model closes. When an agent edits a Terraform module&apos;s output variable, the breakage is in the six repositories that &lt;code&gt;source&lt;/code&gt; that module, and the only record that those six repositories exist is six &lt;code&gt;source&lt;/code&gt; blocks sitting in six manifests across the org. The agent cloned one repository. It has one of the seven facts it needs. No amount of reasoning recovers the other six, because they were never text it could read. They are edges in a graph it was never given.&lt;/p&gt;
&lt;p&gt;And the fix has a specific shape, the same one Mabl reached and the same one the feature requests are circling. Parse the manifests that already declare these edges. Resolve each reference to the repository that owns the artifact. Build the cross-repo graph once, keep it current by re-reading the source, and let the agent query it during planning. Parsed, not inferred. The graph that should be answering the agent&apos;s question already exists in your org&apos;s manifests, unassembled.&lt;/p&gt;
&lt;h2&gt;What this looks like on a real org&lt;/h2&gt;
&lt;p&gt;The transitive reach of a shared artifact is almost always larger than anyone eyeballs, and it is invisible from the place people go to check. I ran the &lt;a href=&quot;https://github.com/open-telemetry&quot;&gt;open-telemetry&lt;/a&gt; GitHub organisation through Riftmap to put a number on it. 105 repositories. A change to &lt;code&gt;opentelemetry-collector&lt;/code&gt; reaches twenty-one of them, across three levels of the dependency graph, every edge a &lt;code&gt;go.mod&lt;/code&gt; require resolved to the repository that owns the module. An engineer editing the collector sees the collector. They do not see the twenty-one. GitHub&apos;s own dependency graph will not show it to them either, because it works per repository and does not traverse the org transitively.&lt;/p&gt;
&lt;p&gt;Two details from that scan are worth holding onto, because they say something about what counting gets you versus what parsing gets you. The most-referenced repository in the entire org is &lt;code&gt;opentelemetry-specification&lt;/code&gt;, cited fifty-one times. Change it and you break no one&apos;s build, because those fifty-one references are documentation links, not dependencies. The collector is referenced far less and ripples through twenty-one repositories. Counting mentions and resolving dependencies are different operations, and only one of them tells you what will actually break.&lt;/p&gt;
&lt;p&gt;Now hold that example up against the infrastructure layer, because it is the gentle version of the problem. A &lt;code&gt;go.mod&lt;/code&gt; require at least lives in a file inside the repository the agent cloned. The dependency is in scope even if its transitive reach is not. Move to the base image built by a separate platform repository, or the Terraform module sourced from a third, and the dependency is not in the agent&apos;s repository at all. It is in another repo entirely, reached by a reference the agent&apos;s tools do not resolve. If the package layer is already bigger than anyone tracks, the artifact layer is the part no one can see at all. That is the gap, and it is the one Riftmap was built for.&lt;/p&gt;
&lt;h2&gt;Why this gets worse as agents get better&lt;/h2&gt;
&lt;p&gt;The faster and more confident agents get, the wider this gap opens, which is the uncomfortable part. A human making a base-image bump at least pauses, half-remembers that the payments team is downstream, and sends a Slack message. An agent makes the change cleanly, passes the same local tests the repository has always been reviewed against, and opens the pull request in ninety seconds. Each change looks correct in isolation because each change is correct in isolation. The defect is at the seam between repositories, and the seam is exactly what neither the agent nor its local tests can see.&lt;/p&gt;
&lt;p&gt;This is why the aggregate numbers move in the wrong direction as AI adoption climbs, and I have walked that data, &lt;a href=&quot;https://riftmap.dev/blog/ai-doesnt-understand-blast-radius/&quot;&gt;Cortex and DORA and Amazon&apos;s own high-blast-radius memo&lt;/a&gt;, in its own post rather than repeat it here. The short version is that local correctness went up and system stability went down, and the two are not in tension. They are the same fact seen from inside one repository and from across the org.&lt;/p&gt;
&lt;h2&gt;The dependency that walked out the door&lt;/h2&gt;
&lt;p&gt;There is a more human version of this, and in every conversation I have had about Riftmap it is the one that lands hardest. The base image that nine services inherit from, or the shared module six repositories source, is very often a dependency nobody wrote down, set up years ago by the senior engineer who has since moved teams or left. The graph lived in their head. When they walked out the door, the org did not lose the dependency. It lost the only copy of the map.&lt;/p&gt;
&lt;p&gt;An agent inherits that amnesia and runs at speed on top of it. It will confidently change the artifact the departed engineer quietly wired into half the estate, because nothing in the repository it cloned records that the wiring exists. This is the highest-stakes edge precisely because it is the least documented one, and it is the strongest reason the map cannot be a thing humans maintain by hand. The dependency that broke production is the one nobody remembered. A parser does not forget, and it does not resign.&lt;/p&gt;
&lt;h2&gt;The graph you don&apos;t have to build&lt;/h2&gt;
&lt;p&gt;Give an agent every repository in your organisation and you have given it access. You have not given it the map. The record of what breaks when it bumps a base image was never in any of those repositories&apos; code, and no symbol graph, however good, will reconstruct an edge that was only ever a &lt;code&gt;source&lt;/code&gt; block in a manifest. Those are different gifts, and only one of them tells the agent what it is about to break.&lt;/p&gt;
&lt;p&gt;Riftmap is the version of that map you do not have to build by hand. One read-only token across your GitHub or GitLab organisation. It parses the manifests that already declare these edges, Terraform &lt;code&gt;source&lt;/code&gt; blocks, Dockerfile &lt;code&gt;FROM&lt;/code&gt; lines, Helm chart dependencies, CI includes, &lt;code&gt;go.mod&lt;/code&gt; requires, across twelve ecosystems, resolves each reference to the repository that owns it, and 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 the agent calls during planning, to ask which repositories a change affects before it opens the pull request. Auto-discovered, never catalogued. Parsed, not inferred.&lt;/p&gt;
&lt;h2&gt;Questions teams ask&lt;/h2&gt;
&lt;p&gt;The same handful of questions comes up every time I walk an engineer through this, so here they are, answered straight.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Can an AI coding agent see which other repos a change will affect?&lt;/strong&gt;
Not on its own. An agent&apos;s context is the repository it has cloned, and the edges that break when a shared base image, Terraform module, or Helm chart changes live in other repositories, declared in manifests the agent never reads. It can see the change. It cannot see the consumers, unless it is given a cross-repo dependency graph to query.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why doesn&apos;t Claude Code or Cursor know about my Terraform or Helm dependencies?&lt;/strong&gt;
Because those dependencies are not function calls. A Dockerfile &lt;code&gt;FROM&lt;/code&gt; line, a Terraform &lt;code&gt;source&lt;/code&gt; block, or a Helm value reference is a manifest declaration that points at another repository, and code-aware tooling indexes symbols and imports, not artifact references across repos. The dependency only exists in a file the agent did not clone, so it sits outside the agent&apos;s context by construction.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How do I give a coding agent cross-repo blast radius?&lt;/strong&gt;
Parse the manifests across your org that already declare the dependencies, resolve each reference to the repository that owns the artifact, and expose the resulting graph as a tool the agent calls during planning. Riftmap does this from one read-only token and serves it over an HTTP API, so the agent can ask which repositories a change affects before it opens a pull request.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Isn&apos;t this just a token-saving optimisation for agents?&lt;/strong&gt;
No. Saving tokens is a side effect, and a declining one as context windows grow. The point is that the artifact-layer dependency graph is information the agent structurally cannot reconstruct from the single repository it cloned, no matter how large its context window gets. It is missing context, not expensive context.&lt;/p&gt;
</content:encoded><category>ai-coding-agents</category><category>cross-repo-context</category><category>blast-radius</category><category>terraform</category><category>artifact-graph</category><author>Daniel Westgaard</author></item><item><title>How to Find Every Consumer of Your Kustomize Base</title><link>https://riftmap.dev/blog/how-to-find-every-consumer-of-your-kustomize-base/</link><guid isPermaLink="true">https://riftmap.dev/blog/how-to-find-every-consumer-of-your-kustomize-base/</guid><description>You own a Kustomize base. Which overlays across your org build on it, at which ref — and what breaks when you change it? Why grep can&apos;t tell you, and what can.</description><pubDate>Sat, 27 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;You have a two-line change staged for &lt;code&gt;platform-base&lt;/code&gt;. It is the overlay every team in the org builds on. Maybe you are tightening a &lt;code&gt;securityContext&lt;/code&gt; so the whole estate runs as non-root. Maybe you are changing a &lt;code&gt;commonLabels&lt;/code&gt; value that Kustomize propagates into selector fields, and a Deployment selector is immutable once it exists, so for everyone downstream this is a delete and recreate rather than a rolling update. Maybe you are dropping a Kubernetes API version that a recent release finally removed. You make the edit, and you reach for a tag.&lt;/p&gt;
&lt;p&gt;Then the question lands, the way it always does right before you cut a release. Who builds on this?&lt;/p&gt;
&lt;p&gt;You look at the base repository for an answer. There is a &lt;code&gt;kustomization.yaml&lt;/code&gt;, a &lt;code&gt;base/&lt;/code&gt; directory of manifests, and a README that three people have edited and nobody has read. None of it names a single overlay that points at this base. Nothing inside the base knows who depends on it. That information lives somewhere else, in repositories you are not currently looking at, and the base has no way to point you towards them.&lt;/p&gt;
&lt;p&gt;This is the same structural fact behind the whole &lt;a href=&quot;https://riftmap.dev/blog/series/find-every-consumer/&quot;&gt;Find Every Consumer series&lt;/a&gt;: the dependency lives in the relationship between repositories, and the artifact you are changing cannot see its own dependents. We have worked through that shape for &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-terraform-module/&quot;&gt;Terraform modules&lt;/a&gt;, &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-docker-base-image/&quot;&gt;Docker base images&lt;/a&gt;, &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-helm-chart/&quot;&gt;Helm charts&lt;/a&gt;, and six other ecosystems. Kustomize is the last of them.&lt;/p&gt;
&lt;p&gt;Here is the conflation worth naming. Kustomize gets filed in everyone&apos;s head as a local templating thing. You write overlays, you run &lt;code&gt;kustomize build&lt;/code&gt;, you get manifests. And for a single repository that is exactly what it is. But the moment a base goes remote, the moment one overlay&apos;s &lt;code&gt;resources:&lt;/code&gt; list points at another repository&apos;s directory, Kustomize stops being a local tool and becomes a cross-repo dependency mechanism with a version. Teams adopt remote bases for precisely that reason, to share a hardened, opinionated base across the org without copying it. And then they get no consumer map, because nothing in the toolchain was built to give them one. So let us answer it properly. If you change a shared Kustomize base, what breaks, and how do you find every overlay that builds on it before you find out the hard way?&lt;/p&gt;
&lt;h2&gt;Where your base actually gets consumed&lt;/h2&gt;
&lt;p&gt;Start with the shape of the problem, because Kustomize reuse spreads a base&apos;s dependents across more places, and more spellings, than people expect.&lt;/p&gt;
&lt;p&gt;The pattern that matters is the remote base. One repository holds a base, a &lt;code&gt;kustomization.yaml&lt;/code&gt; and the manifests it assembles, and overlays in other repositories pull it in by URL through the &lt;code&gt;resources:&lt;/code&gt; list:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# my-app/overlays/production/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - https://github.com/platform/configs//bases/webapp?ref=v1.2.0
namespace: production
images:
  - name: webapp
    newName: registry.example.com/team/webapp
    newTag: v2.1.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That &lt;code&gt;resources:&lt;/code&gt; entry is a cross-repo dependency. The overlay lives in the application team&apos;s repository. The base lives in the platform team&apos;s repository. The &lt;code&gt;?ref=v1.2.0&lt;/code&gt; is a version pin on that dependency, sitting inside a URL string. And this is not an exotic usage. The common shape in practice is composition across teams, an overlay assembling bases that several different teams each own:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;resources:
  # Platform team&apos;s standard webapp base
  - https://github.com/platform/configs//bases/webapp?ref=v1.0.0
  # Security team&apos;s network policies
  - https://github.com/security/policies//network-policies?ref=v2.1.0
  # Observability team&apos;s standard service monitor
  - https://github.com/monitoring/configs//prometheus-servicemonitor?ref=v1.5.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This composition approach is &lt;a href=&quot;https://oneuptime.com/blog/post/2026-02-09-kustomize-remote-bases-git/view&quot;&gt;documented as real-world practice&lt;/a&gt;, and it is exactly the cross-repo coupling that goes unmapped. When I actually counted every cross-repo edge across two real orgs, &lt;a href=&quot;https://riftmap.dev/blog/cross-repo-edge-composition/&quot;&gt;this kind of infrastructure coupling was the entire story — not one edge was a code symbol&lt;/a&gt;. Every one of those three bases lives in a repository other than the one doing the assembling. Each consuming overlay names the base it depends on in a URL, not in a clean, typed dependency block. And the &lt;code&gt;components:&lt;/code&gt; list behaves the same way, as does the older &lt;code&gt;bases:&lt;/code&gt; field that is deprecated but very much still live in real repositories.&lt;/p&gt;
&lt;p&gt;It is worth being precise about how this differs from the &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-helm-chart/&quot;&gt;Helm post&lt;/a&gt;, because the two are the pair that covers Kubernetes config reuse. Helm shares config through templated, versioned charts pulled from a registry, and that post covered the consumer side of it in full, the &lt;code&gt;Chart.yaml&lt;/code&gt; dependencies, the ArgoCD &lt;code&gt;Application&lt;/code&gt; and Flux &lt;code&gt;HelmRelease&lt;/code&gt; manifests, the OCI and HTTP registry forms. Kustomize shares config through overlays that patch a base. The chart is a package; the base is a directory in a repo. The one place the two worlds touch is the inline &lt;code&gt;helmCharts:&lt;/code&gt; field, where a Kustomization renders a Helm chart directly, and that is the seam back to the Helm post. Everything else here is the other half of the problem: the overlay model, where a remote base is the unit of reuse and a URL is the dependency edge.&lt;/p&gt;
&lt;p&gt;This is the same structural shape we covered for &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-terraform-module/&quot;&gt;Terraform modules&lt;/a&gt; pulled by &lt;code&gt;git::&lt;/code&gt; source. A directory in one repository, referenced by URL from another, pinned by a ref. Kustomize just has more spellings for the URL, and that is where it gets hard.&lt;/p&gt;
&lt;h2&gt;The version lives inside the URL, and the URL is a mess&lt;/h2&gt;
&lt;p&gt;Look again at that remote base reference. The version you care about, the thing that decides whether a given overlay is in range of your next release, is not in a &lt;code&gt;version:&lt;/code&gt; field. It is the &lt;code&gt;?ref=&lt;/code&gt; query parameter inside the resource string. To know who is pinned to &lt;code&gt;v1.0.0&lt;/code&gt; and who is riding &lt;code&gt;main&lt;/code&gt;, you have to parse the URL.&lt;/p&gt;
&lt;p&gt;And the URL is not one format. It is a family of formats, and the family is, by the maintainers&apos; own description, not well-specified.&lt;/p&gt;
&lt;p&gt;The official Kustomize docs describe the modern form as an HTTPS or SSH git clone URL, with the in-repo directory appended after a &lt;code&gt;//&lt;/code&gt;, and query parameters for &lt;code&gt;ref&lt;/code&gt;, &lt;code&gt;version&lt;/code&gt;, &lt;code&gt;timeout&lt;/code&gt;, and &lt;code&gt;submodules&lt;/code&gt;. So all of these are valid, and all of them mean a remote base:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;resources:
  # scheme-prefixed HTTPS
  - https://github.com/org/repo//overlays/prod?ref=v1.2.0
  # explicit git:: prefix
  - git::https://gitlab.example.com/infra/base//manifests?ref=v3
  # SSH scheme
  - ssh://git@gitlab.example.com/infra/legacy-base.git//manifests?ref=main
  # scp-like SSH, no scheme at all
  - git@gitlab.com:org/platform/k8s-cluster-addons.git//kustomize/base?ref=v1.0.0
  # scheme-less host, the canonical form in Kustomize&apos;s own examples
  - github.com/org/repo//overlays?ref=v1.0.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The scheme-less form at the bottom is not a shortcut someone invented. It is the canonical form in &lt;a href=&quot;https://github.com/kubernetes-sigs/kustomize/blob/master/examples/remoteBuild.md&quot;&gt;Kustomize&apos;s own remote-build examples&lt;/a&gt;. No leading &lt;code&gt;https://&lt;/code&gt;, no &lt;code&gt;git@&lt;/code&gt;, just a host and a path and a &lt;code&gt;//&lt;/code&gt;. A grep written to find URLs, anything anchored on &lt;code&gt;https?://&lt;/code&gt;, sails straight past it.&lt;/p&gt;
&lt;p&gt;If you think this is me picking the worst case, here is the maintainers&apos; own account of it. There is an &lt;a href=&quot;https://github.com/kubernetes-sigs/kustomize/issues/4454&quot;&gt;open issue&lt;/a&gt; cataloguing the URL forms Kustomize currently accepts, and the list includes &lt;code&gt;github.com/org/repo&lt;/code&gt;, &lt;code&gt;gh:github.com/org/repo&lt;/code&gt;, &lt;code&gt;https://github.com/org/repo&lt;/code&gt;, &lt;code&gt;git::https://github.com/org/repo&lt;/code&gt;, and &lt;code&gt;github.com:org/repo&lt;/code&gt;, with the author noting &quot;and probably a lot more.&quot; It then points out that most of those shortcuts only work for &lt;code&gt;github.com&lt;/code&gt;, so the scheme-less form for a different host may or may not resolve, and concludes that the parsing logic &quot;greatly needs an overhaul.&quot; There is a separate legacy go-getter format, &lt;a href=&quot;https://github.com/kubernetes-sigs/kustomize/blob/master/examples/remoteBuild.md&quot;&gt;still supported for backwards compatibility but no longer recommended&lt;/a&gt;, which means it is also still in real repositories.&lt;/p&gt;
&lt;p&gt;There is one more wrinkle the same issue surfaces, and it matters for anyone trying to detect these. Kustomize tries to load a resource as a file first and as a remote base second, and the thing that disambiguates is the &lt;code&gt;//&lt;/code&gt;. A path with a &lt;code&gt;//&lt;/code&gt; delimiting a subdirectory is treated as a remote base; a plain path like &lt;code&gt;../base&lt;/code&gt; is local and is read directly off disk. So the &lt;code&gt;//&lt;/code&gt; is load-bearing. Any detector has to use it to tell a cross-repo dependency (&lt;code&gt;gitlab.example.com/infra/base//overlays&lt;/code&gt;) apart from a local path that happens to live a directory up (&lt;code&gt;../base&lt;/code&gt;), and from a relative path that happens to contain a double slash for unrelated reasons. Get that wrong in either direction and you either miss real edges or invent ones that do not exist.&lt;/p&gt;
&lt;p&gt;This is the part of the Kustomize story that is genuinely close to uncontested. Plenty of tools can render an overlay. The reverse question, which overlays across the org point at this base and at which ref, requires parsing a URL format that the project itself calls underspecified, and almost nothing does it.&lt;/p&gt;
&lt;h2&gt;The image you deploy is not the image you wrote&lt;/h2&gt;
&lt;p&gt;Remote bases are the headline, but there is a second cross-repo edge in every overlay, and it has a trap in it.&lt;/p&gt;
&lt;p&gt;Kustomize overlays rewrite images through the &lt;code&gt;images:&lt;/code&gt; transformer. The subtlety, the one that quietly resolves the wrong thing if you are not careful, is that the image actually deployed is the &lt;code&gt;newName&lt;/code&gt;, not the name being matched:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;images:
  - name: addon                  # placeholder; a name-matching scan wrongly stops here
    newName: registry.example.com/platform/docker-base-images/python   # the image that deploys
    newTag: &quot;3.12&quot;               # so the real edge is: docker-base-images : python @ 3.12
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Read that the way a naive scan would, keying off the &lt;code&gt;name:&lt;/code&gt; field, and you conclude the overlay depends on an image called &lt;code&gt;addon&lt;/code&gt;. It does not. &lt;code&gt;addon&lt;/code&gt; is just the placeholder being matched in the base; the image that lands in the running pod is &lt;code&gt;registry.example.com/platform/docker-base-images/python&lt;/code&gt;. The real dependency, the artifact whose consumers you would want to track, is the &lt;code&gt;newName&lt;/code&gt;. The original name is metadata, useful to keep but not the edge. A consumer map that resolves the matched name instead of the effective name points at the wrong node, or at no node at all when the placeholder is a bare string with no registry in it.&lt;/p&gt;
&lt;p&gt;Version comes from &lt;code&gt;newTag&lt;/code&gt;, or from a &lt;code&gt;digest&lt;/code&gt; when the overlay pins by content:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;images:
  - name: registry.example.com/platform/docker-base-images/go
    digest: sha256:1111aaaa...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A digest is the most precise pin there is. It is also where the simple, obvious parsing approach falls over, because the version is not the part after the last colon. Split &lt;code&gt;registry.example.com/org/app@sha256:1111...&lt;/code&gt; on &lt;code&gt;:&lt;/code&gt; and you mangle the digest. The whole &lt;code&gt;sha256:...&lt;/code&gt; string is the version, and &lt;code&gt;digest&lt;/code&gt; wins over &lt;code&gt;newTag&lt;/code&gt; when both are present, which is a rule &lt;a href=&quot;https://docs.renovatebot.com/modules/manager/kustomize/&quot;&gt;Kustomize enforces and Renovate documents&lt;/a&gt; precisely because the combination is ambiguous.&lt;/p&gt;
&lt;p&gt;And the images do not only live in overlays. Raw Kubernetes manifests carry them too, in &lt;code&gt;containers&lt;/code&gt;, in &lt;code&gt;initContainers&lt;/code&gt;, and, since Kubernetes 1.25, in &lt;code&gt;ephemeralContainers&lt;/code&gt;, the debug containers people attach to a running pod. That last list is the one most tools forget. A maintenance pod that pulls a private image into an ephemeral container is consuming that image just as surely as a Deployment is, and if your scan walks only &lt;code&gt;containers&lt;/code&gt; you will miss it.&lt;/p&gt;
&lt;p&gt;So a single overlay can declare a remote-base edge to another team&apos;s repository and an image edge to your registry, and a single manifest can declare image edges across three different container lists. None of it looks the same, and all of it is cross-repo.&lt;/p&gt;
&lt;h2&gt;What the Kubernetes and Kustomize tooling tells you, and where it stops&lt;/h2&gt;
&lt;p&gt;The tooling around Kustomize is good, and it answers real questions. It answers the other ones.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Kustomize CLI.&lt;/strong&gt; &lt;code&gt;kustomize build&lt;/code&gt; and &lt;code&gt;kubectl kustomize&lt;/code&gt; render an overlay to its final manifests, resolving the remote bases, applying the patches, rewriting the images. This is genuinely useful and it is the right tool for seeing what an overlay produces. It is also forward and per-overlay. You point it at an overlay and it tells you what that overlay becomes. There is no reverse mode, no &lt;code&gt;kustomize who-builds-on-this&lt;/code&gt;. It cannot take a base and hand you the overlays across the org that consume it, because it only ever looks down from one overlay, never up from one base.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ArgoCD and Flux.&lt;/strong&gt; They deploy Kustomize overlays, and if your org has gone all-in on GitOps then in principle every consuming overlay is declared somewhere as an &lt;code&gt;Application&lt;/code&gt; or a Kustomization CRD. The &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-helm-chart/&quot;&gt;Helm post&lt;/a&gt; went deep on why &quot;in principle&quot; does a lot of work in that sentence, and the same applies here, so I will keep it short: the manifests are spread across a central gitops repo, per-team repos, and per-environment repos, and &quot;show me every Application whose overlay references base X&quot; is not a built-in view in either tool. There is a sharper version of the problem that is specific to remote bases. When an ArgoCD Application points at one repo whose overlay pulls a private base from a second repo, the deploy tooling frequently cannot make that jump, because its credentials are scoped to the first repo. This is a &lt;a href=&quot;https://github.com/argoproj/argo-cd/discussions/12975&quot;&gt;recurring, documented pain point&lt;/a&gt;. The cross-repo edge is real enough to break a deploy, and still invisible to the thing doing the deploying.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Renovate.&lt;/strong&gt; It is the closest any of these come to caring about consumers, and it deserves a fair hearing, because its Kustomize support is real. The &lt;a href=&quot;https://docs.renovatebot.com/modules/manager/kustomize/&quot;&gt;&lt;code&gt;kustomize&lt;/code&gt; manager&lt;/a&gt; parses &lt;code&gt;kustomization.yaml&lt;/code&gt; files, extracts remote resources, image tags, components, and helm charts, resolves the dependency&apos;s source repository, checks for newer SemVer tags, and opens a pull request to bump the version. If you want your remote-base refs and image tags kept current, it does that job.&lt;/p&gt;
&lt;p&gt;But, as with every ecosystem in this series, Renovate has to know who consumes what in order to open those PRs, and it does not expose that knowledge as something you can query. You cannot ask it &quot;which overlays reference my base, and at which ref.&quot; It reacts after a new version is published, repo by repo, and only in repos where it is configured. There are two failure modes specific to Kustomize on top of that. Its remote-target URL parsing is brittle: it does not treat the &lt;code&gt;version=&lt;/code&gt; parameter as a ref, and &lt;a href=&quot;https://github.com/lucacome/renovate-kustomize-remote&quot;&gt;multiple query parameters break it&lt;/a&gt;, so the very URL forms the previous section catalogued are partly outside what it handles. And for raw Kubernetes manifests, the picture is starker. Renovate&apos;s &lt;a href=&quot;https://docs.renovatebot.com/modules/manager/kubernetes/&quot;&gt;Kubernetes manager is off by default and matches no files at all&lt;/a&gt; until you configure file patterns by hand, for a reason the docs state plainly: there is no commonly accepted naming convention for Kubernetes YAML files, and they do not want to check every &lt;code&gt;*.yaml&lt;/code&gt; file just in case. That is not an oversight. It is the same hard problem we are about to get to, and Renovate&apos;s honest answer to it is to make it your job, per repo. Which is exactly the configuration burden the rest of this post argues you should not have to carry.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The general-purpose graphs.&lt;/strong&gt; They sit at a different layer and have a clean category line, the same one drawn across this series. &lt;a href=&quot;https://riftmap.dev/blog/symbol-graphs-and-artifact-graphs/&quot;&gt;Sourcegraph&apos;s symbol graph&lt;/a&gt; is excellent at programming-language symbols and blind to &lt;code&gt;kustomization.yaml&lt;/code&gt;, because a remote base is not a symbol. &lt;a href=&quot;https://riftmap.dev/blog/modeled-graphs-and-parsed-graphs/&quot;&gt;Port&lt;/a&gt; models the catalog you describe in Blueprints rather than parsing the bases your repos already declare, so your base-to-overlay edges are only as complete as what someone remembered to model. &lt;a href=&quot;https://riftmap.dev/blog/the-catalog-maintenance-trap/&quot;&gt;Backstage and the developer-portal category&lt;/a&gt; solve a real problem, but the catalog is only as accurate as the last person to edit it, which is why teams quietly stop editing it. None of these is wrong. They are answering a different question.&lt;/p&gt;
&lt;p&gt;Every tool above is forward, or reactive, or modelled by hand. Render this overlay. Bump this repo when a tag moves. Show me the catalog I wrote down. Not one of them answers the question with your finger on the tag: across all of my repositories, which overlays build on this base, at which ref, and what breaks if I change it.&lt;/p&gt;
&lt;h2&gt;Why this is harder than it looks&lt;/h2&gt;
&lt;p&gt;You might reasonably think this is a grep problem. Search every repository for the base&apos;s repo name, collect the hits, done. It is not, and the reasons are specific to how Kustomize references things.&lt;/p&gt;
&lt;h3&gt;Most remote bases are not URLs&lt;/h3&gt;
&lt;p&gt;The previous section made the case from the maintainers&apos; own issue tracker, so I will not relitigate it, only draw the consequence. A grep anchored on &lt;code&gt;https://&lt;/code&gt; misses the scheme-less canonical form. A grep for the bare host string risks colliding with comments, with image registries that share the host, and with documentation. And the &lt;code&gt;//&lt;/code&gt; that distinguishes a remote base from a local path is the kind of thing a regex can match but cannot reason about, because telling &lt;code&gt;gitlab.example.com/infra/base//overlays&lt;/code&gt; (remote, cross-repo) apart from &lt;code&gt;../base&lt;/code&gt; (local, ignore) and from a path that contains &lt;code&gt;//&lt;/code&gt; incidentally requires understanding that the first has a real dotted host in front of the &lt;code&gt;//&lt;/code&gt; and the others do not. This is parsing, not searching. The honest version of the detector gates on a dotted host segment before the &lt;code&gt;//&lt;/code&gt;, so relative paths stay local and only genuine remote references become edges.&lt;/p&gt;
&lt;h3&gt;The deployed image hides behind a rename&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;newName&lt;/code&gt; trap from earlier is a grep problem in miniature. The string you would search for, the effective image, does not appear next to the &lt;code&gt;name:&lt;/code&gt; field that a naive scan latches onto. It appears in the &lt;code&gt;newName:&lt;/code&gt; line below it, and the matched name above is a placeholder you specifically do not want to resolve. A tool that reads the wrong field produces an edge to a node that does not exist, which is worse than no edge, because it looks like an answer.&lt;/p&gt;
&lt;h3&gt;You cannot just allow-list every YAML file&lt;/h3&gt;
&lt;p&gt;This is the one that has no clean shortcut, and it is the same wall Renovate hit when it left its Kubernetes manager off by default.&lt;/p&gt;
&lt;p&gt;A formal parser earns the right to own a file, so that a cheaper text-grep pass does not also scan it and emit a second, lower-confidence version of the same edge. For most ecosystems that ownership is by filename. A &lt;code&gt;kustomization.yaml&lt;/code&gt; has a fixed name, so a parser can claim it, and the heuristic scanner can cede every file with that name. Good. But raw Kubernetes manifests have no naming convention. They are &lt;code&gt;*.yaml&lt;/code&gt;, the single most common file extension in an infrastructure repository, shared with CI config, Helm values, GitHub Actions workflows, and a hundred other things. You cannot allow-list &lt;code&gt;*.yaml&lt;/code&gt; to a Kubernetes parser without disabling text-based YAML scanning across the entire organisation. So the Kubernetes case cannot be solved by filename. It has to be solved by content: a parser may claim a YAML file only when the content actually is a Kubernetes manifest, an owned pod-spec kind carrying an image. And it has to be deliberately narrow, because a manifest that carries no image, a ConfigMap, a Service, a CRD, should still fall through to the heuristic scanner, in case it carries an incidental in-org URL worth catching. This is a real, fiddly, important distinction, and it is the difference between a graph you can trust and one that double-counts.&lt;/p&gt;
&lt;h3&gt;Templated manifests do not fully parse&lt;/h3&gt;
&lt;p&gt;Some manifests under a &lt;code&gt;templates/&lt;/code&gt; directory are Go-templated YAML that a strict YAML parser rejects outright, because &lt;code&gt;{{ .Values.image }}&lt;/code&gt; is not valid YAML. The best you can do is a regex fallback that recovers the literal &lt;code&gt;image:&lt;/code&gt; references and accepts a lower confidence for them. The honest boundary is the same one every ecosystem in this series eventually hits: a pure &lt;code&gt;{{ .Values.image }}&lt;/code&gt; reference cannot be resolved from source, because the real image is supplied at install time, not committed in the file. A tool that claims to resolve it is guessing. The right move is to recover the literal references, score them honestly, and be straight about the templated ones.&lt;/p&gt;
&lt;h2&gt;What the full answer requires&lt;/h2&gt;
&lt;p&gt;Put all of that together and the requirements for actually answering &quot;who consumes this base&quot; fall out:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Scan every repository in the organisation, with no opt-in and no per-repo config.&lt;/strong&gt; The consuming overlays live in repositories other than the one you are changing, so anything that needs each team to register, annotate, or configure a file pattern will be incomplete the day someone forgets.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Parse every remote-base spelling and pull the ref out of the URL.&lt;/strong&gt; Scheme-prefixed, &lt;code&gt;git::&lt;/code&gt;, &lt;code&gt;ssh://&lt;/code&gt;, scp-like SSH, and the scheme-less canonical form, across &lt;code&gt;resources:&lt;/code&gt;, &lt;code&gt;components:&lt;/code&gt;, and the deprecated &lt;code&gt;bases:&lt;/code&gt;, with the &lt;code&gt;?ref=&lt;/code&gt; or &lt;code&gt;?version=&lt;/code&gt; captured as the version.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Resolve image transformers against the effective &lt;code&gt;newName&lt;/code&gt;, not the matched name,&lt;/strong&gt; and read the whole &lt;code&gt;sha256:...&lt;/code&gt; as the version when a base pins by digest.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Walk &lt;code&gt;containers&lt;/code&gt;, &lt;code&gt;initContainers&lt;/code&gt;, and &lt;code&gt;ephemeralContainers&lt;/code&gt;&lt;/strong&gt; in raw manifests, so a debug container consuming a private image is counted, not missed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Resolve a remote base to the in-org repository that produces it,&lt;/strong&gt; including the scp-like SSH form, with a guard so a public base that happens to share a name with one of yours is not pulled into your graph.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Record the constraint state of every consumer,&lt;/strong&gt; whether they pinned you to a ref, rode a branch, or pinned by digest, so you know which overlays move when you tag and which stay put.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tell these two kinds of edge apart without double-counting them,&lt;/strong&gt; which means owning &lt;code&gt;kustomization.yaml&lt;/code&gt; by filename and Kubernetes manifests by content, and staying current by re-scanning rather than trusting a one-time trawl.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;How Riftmap does this&lt;/h2&gt;
&lt;p&gt;This is the gap Riftmap was built to close, so here is honestly what it does and does not do for Kustomize and Kubernetes.&lt;/p&gt;
&lt;p&gt;Riftmap scans a GitLab or GitHub organisation and parses every repository deterministically. There is no language model in the parse path; the edges come from parsing the actual files, and each edge carries a rule-based confidence score. On the Kustomize side it reads remote bases from &lt;code&gt;resources:&lt;/code&gt;, &lt;code&gt;components:&lt;/code&gt;, and &lt;code&gt;bases:&lt;/code&gt; as a &lt;code&gt;kustomize_resource&lt;/code&gt; edge at confidence &lt;code&gt;0.9&lt;/code&gt;, recognising the scheme-prefixed, &lt;code&gt;git+&lt;/code&gt;, &lt;code&gt;ssh://&lt;/code&gt;, scp-like SSH, and scheme-less host forms, and gating on a dotted host segment so local paths stay local. It reads the &lt;code&gt;images:&lt;/code&gt; transformer as a &lt;code&gt;kustomize_image&lt;/code&gt; edge at &lt;code&gt;0.85&lt;/code&gt;, resolving the effective &lt;code&gt;newName&lt;/code&gt; and keeping the matched name as metadata. On the raw-manifest side it reads &lt;code&gt;containers&lt;/code&gt;, &lt;code&gt;initContainers&lt;/code&gt;, and &lt;code&gt;ephemeralContainers&lt;/code&gt; as &lt;code&gt;k8s_container_image&lt;/code&gt; edges at &lt;code&gt;0.85&lt;/code&gt;, recording a digest pin as its full &lt;code&gt;sha256:...&lt;/code&gt; string. The inline &lt;code&gt;helmCharts:&lt;/code&gt; field resolves as a &lt;code&gt;helm_chart_reference&lt;/code&gt;, which is the one place this hands back to the Helm world. These dependency-type strings are stable, and the confidence scores are deliberately uneven, because some of these edges are more certain than others and the tool says so rather than flattening everything to one confident-looking line.&lt;/p&gt;
&lt;p&gt;The headline for the resolver is the scp-like SSH form, the one with no scheme that a URL parser chokes on. Given &lt;code&gt;git@gitlab.com:polaris-works/platform/k8s-cluster-addons.git//kustomize/base?ref=v1.0.0&lt;/code&gt; in an overlay&apos;s &lt;code&gt;resources:&lt;/code&gt;, Riftmap rewrites the &lt;code&gt;git@host:path&lt;/code&gt; form to &lt;code&gt;host/path&lt;/code&gt;, resolves it to the in-org &lt;code&gt;k8s-cluster-addons&lt;/code&gt; repository, and records &lt;code&gt;v1.0.0&lt;/code&gt; as the version. The scheme-less in-org form pointing at the same base resolves to the same repository, and the two declarations dedupe to a single graph edge, so the impact view shows one dependency, not two, while the declaration view still shows both rows.&lt;/p&gt;
&lt;p&gt;The double-counting problem from earlier gets solved with the content-versus-filename split, made concrete. Riftmap owns &lt;code&gt;kustomization.yaml&lt;/code&gt; by filename and raw manifests by content, so a base is counted once, as a &lt;code&gt;kustomize_resource&lt;/code&gt;, never also as an incidental &lt;code&gt;git_url_reference&lt;/code&gt;. The filename half is easy. The content half is the careful one: the Kubernetes parser claims a &lt;code&gt;*.yaml&lt;/code&gt; file only when it carries an owned pod-spec kind and an image, so a maintenance pod is claimed and ceded, while an image-less ConfigMap beside it still falls through to the heuristic scanner for any org URL it happens to carry. Same double-emit class that bit the ArgoCD parser, solved the same way.&lt;/p&gt;
&lt;p&gt;The numbers come from the &lt;code&gt;polaris-works&lt;/code&gt; test organisation, a 55-repo synthetic org built to stress exactly these cases, on a full re-scan that completed across all 55 repositories with nothing skipped. The Kustomize and Kubernetes fixtures added three graph edges, taking the org from 180 to 183. What makes the three the point is what they collapse from:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Declared in the fixtures&lt;/th&gt;
&lt;th&gt;Becomes in the graph&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;4 image references (2 &lt;code&gt;kustomize_image&lt;/code&gt; transformers + 2 &lt;code&gt;k8s_container_image&lt;/code&gt;, across container and ephemeral)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2 edges&lt;/strong&gt;, both into the single &lt;code&gt;docker-base-images&lt;/code&gt; artifact (&lt;code&gt;python&lt;/code&gt;, &lt;code&gt;go&lt;/code&gt;, &lt;code&gt;node&lt;/code&gt; are tags of it)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5 remote-base references (2 in-org, 3 external)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1 edge&lt;/strong&gt; to the in-org base; the 3 external bases stay unresolved&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9 references in source&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3 edges, 0 new artifacts&lt;/strong&gt; (180 → 183)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Those three edges are &lt;code&gt;warehouse-api&lt;/code&gt; building on &lt;code&gt;k8s-cluster-addons&lt;/code&gt; through the scp-like SSH base, and &lt;code&gt;k8s-cluster-addons&lt;/code&gt; consuming &lt;code&gt;docker-base-images&lt;/code&gt; through both a Kustomize image transformer and a raw Pod container. The collapse is the dedup story made literal: nine references in source become three edges in the graph. And the two cedes held through all of it. Org-wide &lt;code&gt;git_url_reference&lt;/code&gt; stayed at &lt;code&gt;7&lt;/code&gt; and &lt;code&gt;docker_base_image&lt;/code&gt; stayed at &lt;code&gt;19&lt;/code&gt;, which is the proof nothing got double-emitted. The artifact count did not move from &lt;code&gt;87&lt;/code&gt;, because Kustomize and Kubernetes consume; they do not produce. The one consumer-count that changed was &lt;code&gt;docker-base-images&lt;/code&gt;, from &lt;code&gt;19&lt;/code&gt; to &lt;code&gt;20&lt;/code&gt;, as &lt;code&gt;k8s-cluster-addons&lt;/code&gt; was added once, not twice.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://riftmap.dev/images/blog/docker-base-images-consumers.png&quot; alt=&quot;Riftmap&apos;s Dependents panel for the platform/docker-base-images Docker artifact in the polaris-works test org, showing twenty consumers. Most are Dockerfile FROM consumers, each sourced from a Dockerfile (shipment-service, ml-models, warehouse-api, analytics-api, payment-worker, and others). At the bottom, k8s-cluster-addons appears once, tagged K8s and Kustomize, resolved from four references at the same confidence: a container and an ephemeral container in a raw maintenance-pod.yaml manifest, one of them pinned by an sha256 digest, plus a Kustomize image transformer in kustomize/overlays/audit/kustomization.yaml. The python, go, and node images are folded into the single repo-level artifact, so the repo is counted as one consumer, not four.&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;What Riftmap does not claim&lt;/h3&gt;
&lt;p&gt;The honest limits are part of the product, so here they are plainly.&lt;/p&gt;
&lt;p&gt;It does not resolve templated image references. A literal &lt;code&gt;image:&lt;/code&gt; line in a Go-templated manifest is recovered at lower confidence, but a pure &lt;code&gt;{{ .Values.image }}&lt;/code&gt; is unknowable until install time, and the tool does not pretend otherwise.&lt;/p&gt;
&lt;p&gt;A digest pin currently shows its constraint state as &lt;code&gt;branch&lt;/code&gt; rather than &lt;code&gt;pinned&lt;/code&gt;. The resolution is correct, the edge points at the right artifact, but the state label, the thing that buckets a consumer as pinned or floating, falls through to the wrong bucket for &lt;code&gt;sha256:...&lt;/code&gt;. It is cosmetic, it is on the list to fix, and it is the kind of thing worth being upfront about. If you want the full treatment of constraint state, why pinned, floating, and branch are the facts that actually decide your blast radius, &lt;a href=&quot;https://riftmap.dev/blog/version-constraints-across-real-terraform-estates/&quot;&gt;I wrote about it across real estates here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It reads the manifest as committed, not the output of &lt;code&gt;kustomize build&lt;/code&gt;. So a generator that synthesises resources at build time is not expanded, by design, because expanding it would mean executing the build rather than parsing the source. The matched name behind a &lt;code&gt;newName&lt;/code&gt; is stored as metadata but is not yet surfaced on the public dependencies API. And it recognises your bases and images by your org and registry, so a public base or a public image you merely consume shows up as an external dependency, which is the correct answer, not a repository in your graph.&lt;/p&gt;
&lt;p&gt;None of those gaps is hidden behind a confident interface. The point of a blast-radius tool is to be trusted on a Friday afternoon, and you do not earn that by overclaiming.&lt;/p&gt;
&lt;h2&gt;Twelve ecosystems, one graph&lt;/h2&gt;
&lt;p&gt;Step back, because this is the last ecosystem in the series and the shape of the whole thing is finally visible.&lt;/p&gt;
&lt;p&gt;A base&apos;s consumers are the overlays that build on it. An image&apos;s consumers are every file that points at it. And here is the part that only lands once you have done all ten: those are edges into the same graph, and the graph does not care which kind of file the consumer lives in. The &lt;code&gt;docker-base-images&lt;/code&gt; artifact in that scan is consumed by Dockerfile &lt;code&gt;FROM&lt;/code&gt; lines, the subject of the &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-docker-base-image/&quot;&gt;very first post in this series&lt;/a&gt;. It is consumed by Kubernetes Deployment containers. It is consumed by Kustomize image transformers. Three different consumer surfaces, three different file formats, one node, counted once. The Dockerfile, the Deployment, and the overlay are not three separate problems. They are three ways of pointing at the same artifact, and a graph that parses the edges sees them as exactly that.&lt;/p&gt;
&lt;p&gt;That is why the series worked one ecosystem at a time but was only ever building one thing. Each post took an artifact most teams cannot get a consumer list for, a Terraform module, a Helm chart, a Go module, an npm package, an Ansible role, a Kustomize base, and showed that the consumer list is unanswerable for the same reason every time: the dependency was never in the file you were changing. It was in the relationship between repositories, and the artifact you were about to break could not see its own dependents. The fix is not a better grep or a catalog someone has to maintain. It is to parse those relationships across the whole organisation and resolve them into a graph. Parsed, not inferred.&lt;/p&gt;
&lt;p&gt;Your &lt;code&gt;platform-base&lt;/code&gt; change is still sitting there, untagged. Now you can see every overlay that builds on it, at which ref, which ones move when you tag and which stay pinned, and which teams you were about to surprise. Tag it on purpose.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This is the latest post in the &lt;a href=&quot;https://riftmap.dev/blog/series/find-every-consumer/&quot;&gt;Find Every Consumer series&lt;/a&gt;, which works through the same question across &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-docker-base-image/&quot;&gt;Docker base images&lt;/a&gt;, &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-terraform-module/&quot;&gt;Terraform modules&lt;/a&gt;, &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-github-actions-workflow/&quot;&gt;GitHub Actions workflows&lt;/a&gt;, &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-helm-chart/&quot;&gt;Helm charts&lt;/a&gt;, &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-go-module/&quot;&gt;Go modules&lt;/a&gt;, &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-gitlab-ci-template/&quot;&gt;GitLab CI templates&lt;/a&gt;, &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-internal-npm-package/&quot;&gt;npm packages&lt;/a&gt;, &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-internal-python-package/&quot;&gt;Python packages&lt;/a&gt;, and &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-ansible-role/&quot;&gt;Ansible roles&lt;/a&gt;, one ecosystem at a time. It completes the run across the twelve ecosystems Riftmap parses today, and the graph keeps growing as new ones are added. For the technical version of how each ecosystem fights back, see &lt;a href=&quot;https://riftmap.dev/blog/auto-discovering-infrastructure-dependencies-across-10-ecosystems/&quot;&gt;auto-discovering dependencies across ten ecosystems&lt;/a&gt;, and for what this looks like on a real Kubernetes org, see &lt;a href=&quot;https://riftmap.dev/blog/what-208-kubernetes-sigs-repos-actually-depend-on/&quot;&gt;what 208 kubernetes-sigs repos actually depend on&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Riftmap maps cross-repository dependencies across a GitLab or GitHub organisation and answers the change-impact question directly: if I change this, what else breaks? It parses the relationships between your repositories rather than asking you to model them, with no per-repo config. &lt;a href=&quot;https://riftmap.dev/&quot;&gt;Map your org&lt;/a&gt;, or &lt;a href=&quot;https://app.cal.eu/riftmap/map-my-org&quot;&gt;book a walkthrough&lt;/a&gt; and we will map it with you.&lt;/em&gt;&lt;/p&gt;
</content:encoded><category>kustomize</category><category>kubernetes</category><category>blast-radius</category><category>dependency-mapping</category><category>platform-engineering</category><category>devops</category><author>Daniel Westgaard</author></item><item><title>I counted every cross-repo edge in two real orgs. Not one was a code symbol.</title><link>https://riftmap.dev/blog/cross-repo-edge-composition/</link><guid isPermaLink="true">https://riftmap.dev/blog/cross-repo-edge-composition/</guid><description>I scanned Prometheus and Cloud Posse and counted every cross-repo edge. Infrastructure runs from 38% to 99.75% of the coupling. Code symbols: zero.</description><pubDate>Thu, 25 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;I scanned two public GitHub organisations and counted every cross-repository edge between them. The share that is infrastructure rather than code runs from a third to almost all. The share that is a code symbol is zero.&lt;/em&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;Spend a couple of days reading the launches that landed this spring and you start to notice what they have in common. GitLab shipped &lt;a href=&quot;https://about.gitlab.com/blog/introducing-gitlab-orbit/&quot;&gt;Orbit&lt;/a&gt;. Sourcegraph rebuilt its homepage around an AI code-review demo. Both are good, and both are graphs of your code. Both sell you on the same kind of question. Who calls this function. What inherits from this interface. What does changing this method signature touch. Every query in every demo is a code query.&lt;/p&gt;
&lt;p&gt;There is a question underneath all of it that none of those launches answers. Symbol graphs are excellent. I will say that more than once in this post and mean it every time. But how much of what actually holds a real organisation together are they even looking at? Nobody has put a number on it. So I scanned two real organisations and counted.&lt;/p&gt;
&lt;h2&gt;Two graphs, and only one of them gets measured&lt;/h2&gt;
&lt;p&gt;The argument about these graphs has been had. I have had it twice, at length. There is the symbol graph: the functions, classes and imports inside your code, the thing &lt;a href=&quot;https://github.com/sourcegraph/scip&quot;&gt;SCIP&lt;/a&gt; indexes and Sourcegraph serves at the top of its category. And there is the &lt;a href=&quot;https://riftmap.dev/what-is-an-artifact-dependency-graph/&quot;&gt;artifact graph&lt;/a&gt;: the base images, Terraform modules, Helm charts and reusable CI templates your repositories share, bound together by edges that were never code. A Dockerfile &lt;code&gt;FROM&lt;/code&gt; line. A Terraform &lt;code&gt;source&lt;/code&gt; block. A GitLab CI &lt;code&gt;include&lt;/code&gt;. Different nodes, different edges, a different parser entirely. I have worked that distinction through in full for &lt;a href=&quot;https://riftmap.dev/blog/symbol-graphs-and-artifact-graphs/&quot;&gt;Sourcegraph&lt;/a&gt; and for &lt;a href=&quot;https://riftmap.dev/blog/gitlab-orbit-and-the-artifact-layer/&quot;&gt;GitLab Orbit&lt;/a&gt;, and I would rather link those than repeat them.&lt;/p&gt;
&lt;p&gt;What none of us did, in any of those posts, was measure the split. The whole debate is about which graph matters. It has been conducted without the one number that would settle the shape of it. In a real organisation, how much of the cross-repo coupling lives in the artifact graph a symbol tool cannot see? That number is not on the open web. I went and got it.&lt;/p&gt;
&lt;p&gt;One clarification before the data, because the words collide. &quot;Artifact dependency graph&quot; also means something specific in supply-chain security, where it records what went into building a single binary. That is a different graph pointed in the opposite direction. This post is about the cross-repo sense: what consumes your shared artifacts across the org.&lt;/p&gt;
&lt;h2&gt;What I scanned, and how I counted&lt;/h2&gt;
&lt;p&gt;Two public GitHub organisations, chosen to sit at opposite ends on purpose. Prometheus, the monitoring stack, is about as code-centric as an org gets. Go-native, 58 repositories, the kind of place where you would expect the coupling to live in &lt;code&gt;go.mod&lt;/code&gt; and nowhere else. Cloud Posse is the opposite by design. Its entire reason to exist is publishing reusable &lt;code&gt;terraform-aws-&lt;/code&gt; modules, so if any organisation&apos;s coupling lives in infrastructure, it is that one. I am not hiding that choice. The point of the pair is to bracket the range, and you cannot bracket a range without picking the ends.&lt;/p&gt;
&lt;p&gt;Both were scanned on the same day, 2026-06-25, on the same build of Riftmap (v1.6.6, commit &lt;code&gt;273794e&lt;/code&gt;). Prometheus came back with 58 repositories scanned, Cloud Posse with 242.&lt;/p&gt;
&lt;p&gt;Here is the rule every number in this post is counted by, stated once so you can hold me to it.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Counts are directly-declared, in-organisation, cross-repository dependency edges from a single fresh full scan of each GitHub org, where both the consuming repo and the producing repo are inside the org and are not the same repo; references in &lt;code&gt;test/&lt;/code&gt; and &lt;code&gt;examples/&lt;/code&gt; trees are excluded. The canonical headline unit is one distinct consumer-repo to producer edge; raw manifest references are reported alongside. The &quot;no symbol-graph representation&quot; claim is scoped to the infrastructure-artifact bucket only (Terraform sources, Docker &lt;code&gt;FROM&lt;/code&gt;s, CI &lt;code&gt;uses&lt;/code&gt; and includes, Kubernetes images): those edges contain no code symbols or import statements. The language-package bucket (Go, npm, Python) is manifest-declared and partially representable by a symbol indexer via import resolution.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In plain terms. An edge only counts if it stays inside the org and crosses a repository boundary. A dependency that leaves for the public registry does not count. I count what is declared, not the transitive closure underneath it. And the strong claim, the one about edges a symbol graph cannot represent, applies to the infrastructure layer, not to the package edges a symbol index can partly resolve through imports. I will come back to that last point and give the symbol graph its due.&lt;/p&gt;
&lt;p&gt;One more convention. Every figure here is the product&apos;s all-confidence count, the same number &lt;a href=&quot;https://app.riftmap.dev&quot;&gt;app.riftmap.dev&lt;/a&gt; shows and anyone can re-run, with a stricter 0.8-confidence floor reported alongside wherever it changes the picture.&lt;/p&gt;
&lt;p&gt;I will tell you how I know the count is honest, because the way I found out is the best evidence I have. The first scan undercounted Cloud Posse badly. It parsed 1,361 Terraform Registry references like &lt;code&gt;source = &quot;cloudposse/label/null&quot;&lt;/code&gt; and resolved exactly none of them to a repository inside the org, because the resolver was treating the registry namespace as somewhere external. The scan had found a gap in my own tool. I fixed the resolver, re-scanned, and Cloud Posse&apos;s resolved edges went from 199 to 490 while &lt;code&gt;terraform-null-label&lt;/code&gt;&apos;s consumers went from 9 to 147. Prometheus came back identical, edge for edge. A change that moves the Terraform-native org two and a half times over and does not touch a single edge in the Go-native one is the cleanest evidence I can give you that the count measures what it claims to.&lt;/p&gt;
&lt;h2&gt;Where the edges actually live&lt;/h2&gt;
&lt;h3&gt;Prometheus: a third of the coupling is already invisible&lt;/h3&gt;
&lt;p&gt;Start with the org that should be friendliest to the symbol-graph view, and the one I have &lt;a href=&quot;https://riftmap.dev/blog/what-56-prometheus-repos-depend-on/&quot;&gt;scanned before&lt;/a&gt;. 214 cross-repo edges, from 271 references. The breakdown reads exactly like a Go shop. 132 &lt;code&gt;go.mod&lt;/code&gt; requires, 40 &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-github-actions-workflow/&quot;&gt;GitHub Actions &lt;code&gt;uses&lt;/code&gt;&lt;/a&gt;, 38 &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-docker-base-image/&quot;&gt;Dockerfile &lt;code&gt;FROM&lt;/code&gt; lines&lt;/a&gt;, 3 Kubernetes image references, 1 npm dependency.&lt;/p&gt;
&lt;p&gt;Add up the edges a symbol graph cannot represent, the Actions and the Docker images and the Kubernetes references, and you get 38% of the total. Call it a third, in the most code-centric org I could find. At the stricter confidence floor it is 32%, which is the number I would defend hardest. Either way, a third of what holds Prometheus together is infrastructure no symbol indexer sees.&lt;/p&gt;
&lt;p&gt;And the other two-thirds? Those are &lt;code&gt;go.mod&lt;/code&gt; requires. Not function calls. Manifest lines a build tool resolves later against a module registry. I will come back to what a symbol graph does with those. For now the only figure that matters is the one at the bottom of the table. Of 214 cross-repo edges, the number that are code symbols is zero.&lt;/p&gt;
&lt;h3&gt;Cloud Posse: almost all of it is infrastructure&lt;/h3&gt;
&lt;p&gt;Now the other end. 490 edges, from 1,320 references. Here I have to be careful with the confidence split, because it is the one place a sharp reader can push, so let me push first. Of the 408 edges resolved at high confidence, 407 are infrastructure artifacts and exactly one is a language package. That is 99.75%. The product&apos;s headline number is 490 rather than 408, and the difference is 82 lower-confidence heuristic edges that mix real infrastructure couplings with a handful of Go import paths and some noise. I hold those out of the claim. That is why the share is anchored to the 408 I can stand behind, not the 490.&lt;/p&gt;
&lt;p&gt;The composition of the part I can stand behind. 359 &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-terraform-module/&quot;&gt;Terraform module sources&lt;/a&gt;, 36 Actions, 8 Docker base images, 4 reusable workflows, and that single &lt;code&gt;go.mod&lt;/code&gt; require. Cloud Posse is an extreme, and it is an extreme by construction. That is the reason it is here. It shows you where an organisation built entirely out of shared infrastructure actually sits, and it sits at almost exactly 100%. The symbol count, again, is zero.&lt;/p&gt;
&lt;p&gt;Put the two ends together. The infrastructure share of cross-repo coupling runs from 38% in the most code-centric org I could pick to 99.75% in the most infrastructure-centric one. The number that does not move is the symbol count. In both organisations it is zero.&lt;/p&gt;
&lt;h2&gt;One module, 147 repositories&lt;/h2&gt;
&lt;p&gt;Composition is the shape of the thing. Fan-in is where it bites. So here is the single most-consumed artifact in either org, and it retires a guess I made in writing once.&lt;/p&gt;
&lt;p&gt;In the &lt;a href=&quot;https://riftmap.dev/what-is-an-artifact-dependency-graph/&quot;&gt;glossary entry&lt;/a&gt; for the artifact graph I wrote, as a hypothetical, that a shared Terraform module might be sourced by forty repositories. The measured figure for Cloud Posse&apos;s &lt;code&gt;terraform-null-label&lt;/code&gt; is 147. Every one of those 147 repositories pulls it through the same line, &lt;code&gt;module &quot;this&quot; { source = &quot;cloudposse/label/null&quot; }&lt;/code&gt;, sitting in a &lt;code&gt;context.tf&lt;/code&gt;. Behind it the next most-shared modules are &lt;code&gt;route53-cluster-hostname&lt;/code&gt; at 15, &lt;code&gt;security-group&lt;/code&gt; at 13, &lt;code&gt;iam-role&lt;/code&gt; at 9.&lt;/p&gt;
&lt;p&gt;Sit with the 147 for a moment. That is one module, and changing it re-plans 147 repositories across the org. The edge that connects it to each of them is a single string in a manifest. There is no function call. There is no import statement. There is nothing in any symbol table. A symbol graph could index every line of Go in Cloud Posse and represent precisely none of that 147-repository blast radius, because the blast radius was never written in code.&lt;/p&gt;
&lt;p&gt;A note on the number, so it is unimpeachable. 156 repositories reference the module in raw declarations. 147 of those resolve to distinct in-org consumers once self-references and test trees are stripped out. 147 is the figure I stand behind.&lt;/p&gt;
&lt;h2&gt;What a symbol graph does see&lt;/h2&gt;
&lt;p&gt;I have said symbol graphs are excellent twice now, and I want to make the concession real, because the claim I am making is narrower than &quot;symbol tools are blind&quot; and the difference is the whole point.&lt;/p&gt;
&lt;p&gt;A symbol graph is not blind to all of this. For the language-package edges, Prometheus&apos;s 132 &lt;code&gt;go.mod&lt;/code&gt; requires, SCIP carries the relationship. Its external-symbol mechanism resolves an import in one repository to the package that defines it in another, with version metadata attached. Ask Sourcegraph whether &lt;code&gt;node_exporter&lt;/code&gt; depends on &lt;code&gt;client_golang&lt;/code&gt; and it can tell you, and it would be right. I am not going to pretend otherwise to make my number look bigger.&lt;/p&gt;
&lt;p&gt;What a symbol graph cannot do is two separate things. The first is the headline. It cannot represent the infrastructure edges at all. A Dockerfile &lt;code&gt;FROM&lt;/code&gt;, a Terraform &lt;code&gt;source&lt;/code&gt;, a GitLab CI &lt;code&gt;include&lt;/code&gt;, an Actions &lt;code&gt;uses&lt;/code&gt;. None of them contains a symbol or an import for an indexer to resolve, so none of them appears in a symbol index. That is the infrastructure bucket, the one that ran from a third to nearly all, and it is structural. It is not a coverage gap someone closes next quarter. The second is subtler, and it applies even to the package edges a symbol graph can see. An index will tell you &lt;code&gt;node_exporter&lt;/code&gt; imports &lt;code&gt;client_golang&lt;/code&gt;. It will not tell you which version constraint each of the 23 consumers is pinned to, which of them float to your next release, or which reference is hiding in a second &lt;code&gt;go.mod&lt;/code&gt; three directories down. That resolution is the work. It is the thing the artifact graph does and the symbol index does not.&lt;/p&gt;
&lt;p&gt;So here is the precise version of the claim, the one I will defend against anyone at Sourcegraph or GitLab. Not one cross-repo edge in either organisation is a symbol. And the infrastructure share of those edges, the part no symbol index reaches at all, runs from a third to nearly all.&lt;/p&gt;
&lt;h2&gt;The graph that was carrying the weight&lt;/h2&gt;
&lt;p&gt;A symbol graph can read every line of code in your organisation and still not see the base-image bump about to take a dozen services down, or the one-line &lt;code&gt;source&lt;/code&gt; reference binding 147 repositories to a single module. Those edges were never code. They were declarations, sitting in manifests, waiting for a build tool to resolve them long after the symbol indexer had finished its pass. The graph that maps them is a different graph. And in the two organisations I measured, it was carrying somewhere between a third and very nearly all of the weight.&lt;/p&gt;
&lt;p&gt;None of which makes that graph exotic. Past a certain repository count it is as load-bearing as the symbol graph, and a serious setup needs both. The requirements are not subtle either. It has to be parsed from source, so it stays current as the repositories change instead of going stale the moment someone forgets to update a catalog. It has to be queryable, so the engineer reaching for it mid-incident and the agent reaching for it at planning time get the same answer. And it has to read across whatever platforms you actually run on, because the edge that bites is the one running from a GitHub service onto a base image published from GitLab. That is the primitive. It is not a product. It is the thing the industry has spent this year slowly working out that it needs, one launch at a time.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;Where I build that primitive, it is called Riftmap. One read-only token, a GitLab or GitHub organisation parsed &lt;a href=&quot;https://riftmap.dev/blog/auto-discovering-infrastructure-dependencies-across-10-ecosystems/&quot;&gt;across twelve ecosystems&lt;/a&gt;, and the artifact graph comes back as something you can use. A visual blast radius in the UI, the same graph over an API for the agents that need it at planning time. Parsed, not inferred. Auto-discovered, never catalogued. The numbers in this post came out of it, and you can point it at your own org and get yours.&lt;/p&gt;
&lt;h2&gt;A few questions, answered directly&lt;/h2&gt;
&lt;h3&gt;What fraction of cross-repo dependencies are infrastructure rather than code?&lt;/h3&gt;
&lt;p&gt;In two real public organisations measured in June 2026, the infrastructure-artifact share of in-org cross-repository edges ranged from 38% in a Go-native stack (Prometheus) to 99.75% in a Terraform-native one (Cloud Posse). It varies with what the organisation builds, but in both cases zero of the cross-repo edges were code symbols. The infrastructure edges are Terraform &lt;code&gt;source&lt;/code&gt; blocks, Dockerfile &lt;code&gt;FROM&lt;/code&gt; lines, CI includes and reusable workflows, none of which a symbol graph represents.&lt;/p&gt;
&lt;h3&gt;How many repositories typically depend on a single shared Terraform module?&lt;/h3&gt;
&lt;p&gt;In the Cloud Posse organisation, one module (&lt;code&gt;terraform-null-label&lt;/code&gt;) is sourced by 147 repositories, each through a single-line &lt;code&gt;source&lt;/code&gt; reference in a Terraform manifest. High-fan-out shared artifacts like this are the widest-blast-radius and least-visible edges in a polyrepo: changing one of them re-plans every consumer, and the edge to each consumer is a declaration in a manifest rather than anything in code.&lt;/p&gt;
&lt;h3&gt;Can a symbol graph such as SCIP, Sourcegraph or GitLab Orbit index infrastructure dependency edges?&lt;/h3&gt;
&lt;p&gt;No. Infrastructure edges like a Dockerfile &lt;code&gt;FROM&lt;/code&gt;, a Terraform &lt;code&gt;source&lt;/code&gt; or an Actions &lt;code&gt;uses&lt;/code&gt; contain no code symbols and no import statements, so a symbol indexer represents none of them. A symbol graph can resolve language-package dependencies such as a Go import or an npm package through import resolution, but the infrastructure layer requires a different parser entirely.&lt;/p&gt;
</content:encoded><category>cross-repo</category><category>dependency-graph</category><category>artifact-graph</category><category>blast-radius</category><category>terraform</category><author>Daniel Westgaard</author></item><item><title>What version constraints actually look like across real Terraform estates</title><link>https://riftmap.dev/blog/version-constraints-across-real-terraform-estates/</link><guid isPermaLink="true">https://riftmap.dev/blog/version-constraints-across-real-terraform-estates/</guid><description>A version constraint feels like a fact about what&apos;s running. It&apos;s a record of what the author allowed. I measured how that plays out across four real Terraform estates.</description><pubDate>Wed, 24 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I had a mental model of how Terraform module versions get pinned. Roughly the one the documentation implies. You put a &lt;code&gt;version&lt;/code&gt; on a module block, maybe a &lt;code&gt;~&amp;gt;&lt;/code&gt; if you want a little room above the floor, and that is the version the repo is on. I believed it well enough that I never bothered to check it.&lt;/p&gt;
&lt;p&gt;So I checked it. I parsed the &lt;code&gt;source&lt;/code&gt; and &lt;code&gt;version&lt;/code&gt; of every module block across a handful of real, organisation-scale Terraform estates and looked at the actual distribution of how versions are written. Two things I was confident about going in turned out to be wrong, and both were more useful wrong than they would have been right, which is the only reason this is worth writing up.&lt;/p&gt;
&lt;p&gt;The first wrong thing: I expected versioning style to be a per-repo, sometimes per-module, decision. A mix inside any given organisation. It is not. The second: I expected one widely used ecosystem to be a &lt;code&gt;~&amp;gt;&lt;/code&gt; world that a naive exact-match would mostly miss. It turned out to be overwhelmingly exact pins. I will take them in order. The through-line underneath both is the same. How a module version is pinned is an organisation-level convention rather than a per-module choice, and the shape of the constraint, not the number inside it, is what decides whether you can safely automate a change across the estate.&lt;/p&gt;
&lt;h2&gt;I went looking for how versions are pinned, and measured it&lt;/h2&gt;
&lt;p&gt;Method first, because the numbers are the only thing that makes this more than an opinion.&lt;/p&gt;
&lt;p&gt;I parsed module &lt;code&gt;source&lt;/code&gt; and &lt;code&gt;version&lt;/code&gt; across four public corpora, each scanned as a whole organisation rather than cherry-picked repo by repo:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/cloudposse&quot;&gt;cloudposse&lt;/a&gt;, a large and widely used module ecosystem.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/terraform-aws-modules&quot;&gt;terraform-aws-modules&lt;/a&gt;, the curated AWS collection.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/terraform-google-modules&quot;&gt;terraform-google-modules&lt;/a&gt;, the curated GCP collection.&lt;/li&gt;
&lt;li&gt;A mirrored Azure Verified Modules example corpus. AVM lives inside the very large Azure GitHub organisation and is not cleanly scopeable on its own, so I stood up a throwaway organisation mirroring the real AVM example repositories and scanned that.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Then I ran each constraint string through real Terraform and OpenTofu matching semantics rather than synthetic test cases, so the distribution reflects how these constraints actually resolve, not how I imagine they resolve.&lt;/p&gt;
&lt;p&gt;One methodological point matters enough to state up front, because it is part of the argument and not a caveat to bury. I report every figure per corpus and never pool them. A blended &quot;X% of modules pin exact&quot; number across four estates does not describe the world. It describes whichever corpus contributed the most rows. If you take one thing from the method, take that.&lt;/p&gt;
&lt;h2&gt;Versioning style is an organisation-level convention, not a per-module choice&lt;/h2&gt;
&lt;p&gt;This is the structural headline, and it is the thing I got most wrong.&lt;/p&gt;
&lt;p&gt;Within a single estate, versioning style is close to homogeneous. Not a spread. A house style. cloudposse pins exact: 89% of its module declarations are a bare &lt;code&gt;version = &quot;x.y.z&quot;&lt;/code&gt; with no operator, and not a single &lt;code&gt;~&amp;gt;&lt;/code&gt; appears anywhere in the estate. The remainder is almost all git-sourced modules pinned by tag, which is a different problem and a later section. The curated terraform-aws-modules and terraform-google-modules collections are the mirror image, 94% and 95% &lt;code&gt;~&amp;gt;&lt;/code&gt; respectively, with exact pins down in the low single digits. The AVM example corpus lands on the exact-pin side too, overwhelmingly bare pins, around 82%.&lt;/p&gt;
&lt;p&gt;That 82% comes with a caveat I want to state in the same breath, because leaving it out would make the number dishonest. The AVM examples are authored by the AVM team and pinned by CI. They over-represent exact pins compared with how real enterprise consumers of those modules actually write their constraints, which leans far more toward &lt;code&gt;~&amp;gt;&lt;/code&gt;. The example corpus tells you the house style of the people writing the modules, not the house style of the people consuming them. Hold onto that when you read the figure.&lt;/p&gt;
&lt;p&gt;Four corpora, four single-style camps. The reason this is more useful than I expected is operational. If versioning style were a per-module decision, auditing an estate would mean inspecting every repo, because the last repo would tell you nothing about the next one. Because it is an organisation-level convention, the opposite holds. Learn one repo&apos;s convention and you can usually predict the rest of the estate. The unit of analysis is the organisation, not the module. That changes how you scope a migration before you have written a single line of it.&lt;/p&gt;
&lt;h2&gt;What the constraint shape tells you about blast radius&lt;/h2&gt;
&lt;p&gt;Once you stop asking &quot;which version is this repo on&quot; and start asking &quot;is this safe to change,&quot; the constraint shape stops being a formatting detail and becomes the whole question. The shapes do not carry the same amount of information, and the difference between them is exactly the part that decides whether an automated rewrite lands where you meant it to.&lt;/p&gt;
&lt;p&gt;An exact pin tells you what runs. &lt;code&gt;version = &quot;0.3.5&quot;&lt;/code&gt; is unambiguous. The repo is on &lt;code&gt;0.3.5&lt;/code&gt;, you know precisely what a change touches, and you can reason about it.&lt;/p&gt;
&lt;p&gt;A &lt;a href=&quot;https://developer.hashicorp.com/terraform/language/expressions/version-constraints&quot;&gt;bounded &lt;code&gt;~&amp;gt;&lt;/code&gt;&lt;/a&gt; tells you the band. &lt;code&gt;~&amp;gt; 0.4.0&lt;/code&gt; allows &lt;code&gt;0.4.x&lt;/code&gt; and stops before &lt;code&gt;0.5.0&lt;/code&gt;. Both arities behave the same way in spirit: &lt;code&gt;~&amp;gt; 1.2.3&lt;/code&gt; pins the patch floor and holds the minor, &lt;code&gt;~&amp;gt; 1.2&lt;/code&gt; holds the major and allows any minor above. You do not know the exact version, but you know the ceiling, and a ceiling is most of what you need to reason about safety.&lt;/p&gt;
&lt;p&gt;An unbounded &lt;code&gt;&amp;gt;=&lt;/code&gt; tells you a floor and nothing else. &lt;code&gt;&amp;gt;= 0.3.0&lt;/code&gt; with no upper bound permits &lt;code&gt;0.3.0&lt;/code&gt;, and it permits &lt;code&gt;2.4.1&lt;/code&gt;, and it permits whatever shipped this morning. This is the dangerous shape. Not because it is common, but because it looks like a version and is actually an open-ended permission.&lt;/p&gt;
&lt;p&gt;The rest of the taxonomy fills in around those three. A compound comma-AND like &lt;code&gt;&amp;gt;= 0.3.0, &amp;lt; 0.5.0&lt;/code&gt; is a hand-built band, a floor and a ceiling assembled by someone who wanted &lt;code&gt;~&amp;gt;&lt;/code&gt; behaviour with different edges. A bare version with no operator defaults to exact equality. A &lt;code&gt;!=&lt;/code&gt; exclusion carves a hole out of an otherwise-open range. Prereleases like &lt;code&gt;1.0.0-beta1&lt;/code&gt; exist and behave like their own small edge case. None of those are the problem. The problem is the unbounded &lt;code&gt;&amp;gt;=&lt;/code&gt;, and here is why it is worse than it looks.&lt;/p&gt;
&lt;p&gt;Suppose a tool wants to decide whether a given module block matches &lt;code&gt;&amp;gt;= 0.3.0&lt;/code&gt; so it can rewrite it. The honest answer is that it cannot know which version is actually running. The repo could be on &lt;code&gt;0.3.0&lt;/code&gt;. It could be on &lt;code&gt;2.4.1&lt;/code&gt;. The constraint does not say. So what does a tool do when it has to produce a yes-or-no match against a constraint that genuinely has no single answer? One common shortcut is to floor-normalise: collapse &lt;code&gt;&amp;gt;= 0.3.0&lt;/code&gt; down to a &lt;code&gt;0.3.0&lt;/code&gt; pin so the comparison succeeds. And the moment it does that, it has quietly decided the repo is sitting on the floor of a range the author deliberately left open.&lt;/p&gt;
&lt;p&gt;In an automated rewrite, that is a change landing on a module nobody pointed it at. The author wrote &lt;code&gt;&amp;gt;= 0.3.0&lt;/code&gt; precisely to keep their options open above the floor. The tool read that openness as a pin and edited accordingly. The danger here is not frequency. You could go a long way before this bites. The danger is silence. It produces a confident match and a clean diff, and nothing in the output tells you the match was a guess.&lt;/p&gt;
&lt;p&gt;The honest line is to treat the shapes by how much they actually constrain. A bounded &lt;code&gt;~&amp;gt;&lt;/code&gt; carries an implicit ceiling, so normalising it to a concrete version stays inside a band the author already accepted. That is defensible. An unbounded &lt;code&gt;&amp;gt;=&lt;/code&gt; carries no ceiling at all, so any concrete reduction is a guess wearing the costume of a match. Until a tool can do real constraint-against-constraint intersection, the conservative behaviour is for a bounded &lt;code&gt;~&amp;gt;&lt;/code&gt; to normalise and an unbounded &lt;code&gt;&amp;gt;=&lt;/code&gt; to be an explicit no-match. A miss you can see beats a match you cannot trust.&lt;/p&gt;
&lt;h2&gt;The modules your version query cannot see&lt;/h2&gt;
&lt;p&gt;There is one more shape, and it is the one a version audit is structurally blind to.&lt;/p&gt;
&lt;p&gt;A module sourced over git carries its version in the source ref, not in a &lt;code&gt;version&lt;/code&gt; attribute:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;module &quot;example&quot; {
  source = &quot;git::https://example.com/modules/network.git?ref=0.16.0&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There is no &lt;code&gt;version&lt;/code&gt; line here. The version is the &lt;code&gt;?ref=&lt;/code&gt; on the end of &lt;code&gt;source&lt;/code&gt;. Anything that audits an estate by reading the &lt;code&gt;version&lt;/code&gt; attribute looks at this block, finds no &lt;code&gt;version&lt;/code&gt;, and returns null. It does not flag it. It skips it. Silently, in the same way the floor-normalisation problem is silent, and for a related reason: the tool answered a question the data did not actually contain.&lt;/p&gt;
&lt;p&gt;There are two ref shapes, and they are not equally bad. &lt;code&gt;?ref=0.16.0&lt;/code&gt; is a tag, and a tag is semver-recoverable. You can parse the version back out of the URL if you know to look there. &lt;code&gt;?ref=main&lt;/code&gt; is a branch, and a branch is not recoverable at all. There is no version in &lt;code&gt;main&lt;/code&gt;. There is only whatever &lt;code&gt;main&lt;/code&gt; pointed at the last time this was applied, which can be something different tomorrow.&lt;/p&gt;
&lt;p&gt;This is not hypothetical, and it is not rare. In the cloudposse scan, the same estate that is 89% clean exact pins, almost one in ten module declarations are git-sourced with the version sitting in a &lt;code&gt;?ref=tags/0.x.0&lt;/code&gt; rather than a &lt;code&gt;version&lt;/code&gt; attribute. Those refs are recoverable if you know to parse &lt;code&gt;source&lt;/code&gt;. A &lt;code&gt;version&lt;/code&gt; audit never looks there, so it returns null for every one of them. The tidiest-looking corpus in the sample keeps nearly a tenth of its module edges out of reach of the obvious query.&lt;/p&gt;
&lt;p&gt;Sit with that for a second. A branch ref is the least-pinned dependency in the entire estate. It is more open than &lt;code&gt;&amp;gt;= 0.3.0&lt;/code&gt;, because at least &lt;code&gt;&amp;gt;= 0.3.0&lt;/code&gt; names a floor. And it is exactly the dependency your audit cannot see if it only looks at &lt;code&gt;version&lt;/code&gt;. The most dangerous edges hide in the place most tooling does not think to look.&lt;/p&gt;
&lt;h2&gt;Why this is a discovery problem&lt;/h2&gt;
&lt;p&gt;I want to be fair to the tools that do the other half of this, because they are genuinely good at it.&lt;/p&gt;
&lt;p&gt;The actual rewrite, the part where you move a shared module from &lt;code&gt;0.3.x&lt;/code&gt; to &lt;code&gt;0.4.0&lt;/code&gt; across dozens of repos, is a solved and deterministic problem. &lt;a href=&quot;https://docs.openrewrite.org/&quot;&gt;OpenRewrite&lt;/a&gt;&apos;s HCL recipes do exactly this, and they do it well. Point a recipe at code that matches its filter and it produces a clean, reviewable diff with every matching module block flipped and nothing else touched. I have a lot of respect for that work. It is careful, it is deterministic, and it is the easy half.&lt;/p&gt;
&lt;p&gt;It is the easy half because it assumes the hard half is already done. A rewrite recipe is only as good as the set of repos you point it at and the accuracy of the version it thinks each one is on. And that, as the last few sections argued, is where it gets quietly hard. Which repos consume the module. Under which constraint shape. And which ones a naive &lt;code&gt;version&lt;/code&gt; query cannot even see because the version is hiding in a &lt;code&gt;git::&lt;/code&gt; ref. That is not an apply problem. It is a discovery problem, and it sits upstream of every rewrite tool, every codemod, and every careful &lt;code&gt;sed&lt;/code&gt; across the org.&lt;/p&gt;
&lt;p&gt;People do solve discovery today, mostly with grep and scripts, and at small scale that is completely fine. A &lt;code&gt;grep -r &apos;source =&apos; .&lt;/code&gt; across a dozen repos will get you most of the way. It starts to break on exactly the subtleties this post is about. It treats &lt;code&gt;&amp;gt;= 0.3.0&lt;/code&gt; and &lt;code&gt;0.3.0&lt;/code&gt; as different strings without understanding that one is a pin and the other is an open range. It returns nothing for a &lt;code&gt;git::...?ref=main&lt;/code&gt; module because there is no &lt;code&gt;version&lt;/code&gt; to grep for. It works right up until the estate is big enough, or heterogeneous enough, that the edge cases stop being edge cases. (Symbol-graph tools like Sourcegraph are excellent at what they do and entirely orthogonal to this. A module &lt;code&gt;source&lt;/code&gt; edge is not a code symbol. Different category.)&lt;/p&gt;
&lt;p&gt;This is the problem Riftmap exists to solve. It parses the dependency edges your repos already declare, the Terraform &lt;code&gt;source&lt;/code&gt; and &lt;code&gt;version&lt;/code&gt;, the Dockerfile &lt;code&gt;FROM&lt;/code&gt;, the CI &lt;code&gt;include&lt;/code&gt;, the Helm references, across your whole GitHub or GitLab organisation, and &lt;a href=&quot;https://riftmap.dev/what-is-an-artifact-dependency-graph/&quot;&gt;builds the graph underneath them&lt;/a&gt;. Parsed, not inferred. So when you ask &quot;if I change this module, &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-terraform-module/&quot;&gt;what consumes it&lt;/a&gt;, and under which constraint shape,&quot; the answer is read off declared manifests rather than guessed from names or text. It is the same discovery question whether you are &lt;a href=&quot;https://riftmap.dev/blog/deprecate-internal-library-find-consumers/&quot;&gt;deprecating an internal module&lt;/a&gt; or &lt;a href=&quot;https://riftmap.dev/blog/base-image-cve-which-repos-to-fix/&quot;&gt;chasing a CVE through your base images&lt;/a&gt;: the blast radius is the product. The rewrite is what you run once you can see it.&lt;/p&gt;
&lt;p&gt;A version constraint feels like a fact about what is running. It is not. It is a record of what the author was willing to allow. An exact pin happens to make those two things identical, which is probably why it is so easy to forget they were ever different. The unbounded ranges and the branch refs are where they come apart, and treating the permission as a fact is precisely how an automated change ends up somewhere nobody asked it to go.&lt;/p&gt;
</content:encoded><category>terraform</category><category>version-management</category><category>platform-engineering</category><category>dependency-management</category><category>modules</category><category>blast-radius</category><category>infrastructure-as-code</category><author>Daniel Westgaard</author></item><item><title>You changed the shared CI template. Half the org got it instantly. The other half will never see it.</title><link>https://riftmap.dev/blog/changing-shared-ci-template-two-deployments/</link><guid isPermaLink="true">https://riftmap.dev/blog/changing-shared-ci-template-two-deployments/</guid><description>A shared CI change is not a release. It is two deployments at once: instant to the pipelines tracking your branch, silent to the ones pinned to a tag. You hold the roster for neither.</description><pubDate>Tue, 23 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;Every guide on shared CI tells you how to write a reusable template. Almost none tell you how to change one you already have. The hard part was never the YAML. It is that a single edit ships to two disjoint sets of repos, on two different clocks, and your CI platform shows you neither set.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Someone on your platform team made a small, careful change to the shared pipeline.&lt;/p&gt;
&lt;p&gt;Call the project &lt;code&gt;platform/ci&lt;/code&gt;. It started as one file and grew into a little library: a build template, a deploy template, a security-scanning include, some shared rules. Most of the org consumes it. Nobody knows exactly how much of the org, because nothing in the product counts for you. The change itself was trivial and correct. A job called &lt;code&gt;test&lt;/code&gt; was doing two things, so they split it and renamed the original to &lt;code&gt;unit-test&lt;/code&gt;, the way you would tidy any file you owned. They ran it against their own repo, the pipeline went green, they merged.&lt;/p&gt;
&lt;p&gt;Eight minutes later, the first Slack message. Then four more. Services that nobody on the platform team had ever opened were failing at the pipeline-creation step, before a single job ran, because their &lt;code&gt;.gitlab-ci.yml&lt;/code&gt; does &lt;code&gt;extends: .test&lt;/code&gt; and there is no longer a job called &lt;code&gt;test&lt;/code&gt; to extend from. GitLab&apos;s own template guide spells the mechanic out plainly: rename a job that downstream pipelines depend on and the consumer&apos;s config &lt;a href=&quot;https://docs.gitlab.com/development/cicd/templates/&quot;&gt;immediately fails to lint&lt;/a&gt;, because the job it references no longer exists. The on-call engineer traced it back, the platform team added &lt;code&gt;test&lt;/code&gt; as a thin alias for &lt;code&gt;unit-test&lt;/code&gt;, shipped again, and the bleeding stopped. Forty minutes, start to finish. Everyone moved on.&lt;/p&gt;
&lt;p&gt;For five months, nothing.&lt;/p&gt;
&lt;p&gt;Then a payments service that had not been actively developed in over a year picked up an unrelated dependency bump, which meant someone finally moved its pinned &lt;code&gt;ref: v1.4.0&lt;/code&gt; forward to pull the fix. It inherited the eighteen months of template changes it had slept through, including the &lt;code&gt;test&lt;/code&gt; rename, including a few others, all at once. It broke in staging on a Tuesday, and the engineer who got the page had no reason to connect it to a change the platform team had made and forgotten about two quarters earlier. The blame landed on the dependency bump, because that was the visible change. The actual cause had been sitting in &lt;code&gt;platform/ci&lt;/code&gt; since spring.&lt;/p&gt;
&lt;p&gt;The part I keep coming back to is that the same one-line edit broke two completely different sets of repositories, at two different times, for two opposite reasons. One set broke instantly because it was tracking the template&apos;s branch. The other broke months later because it was not. And on the morning of the merge, the platform team could see neither set. They found out who the first group was from the Slack messages. They will never find out who the second group is, because the second group breaks alone, later, attributed to something else.&lt;/p&gt;
&lt;h2&gt;A shared CI change is two deployments wearing one merge&lt;/h2&gt;
&lt;p&gt;We talk about changing a shared CI template as one action. It is two, and they have almost nothing in common except the commit that triggers them.&lt;/p&gt;
&lt;p&gt;The first is a deployment to every consumer that tracks a moving reference. For these repos, your merge is not a proposal they get to evaluate. It is a push straight to their next pipeline run. No rollout, no canary, no opt-in. If the change is breaking, they break at merge time, in their own projects, at whatever moment they next happen to run CI. The blast radius is the whole tracking population and it detonates the instant you click merge.&lt;/p&gt;
&lt;p&gt;The second is a deployment to every consumer pinned to a fixed reference, and it is the strange one, because it has not happened yet and might not happen for a year. These repos are frozen on the version they pinned. Your change is real, it is merged, it is live for everyone else, and it is completely invisible to them. They will receive it at some unknowable future point, whenever someone bumps the pin, usually for an unrelated reason, usually long after anyone remembers what changed. When they receive it, they get the entire accumulated diff in one jump, decoupled from any context that would help them debug it.&lt;/p&gt;
&lt;p&gt;So one change produces an instant, loud failure in one group and a deferred, silent, context-free failure in another. Worst of all, you do not know which of your consumers are in which group. The pin or its absence is the single most important fact about how a consumer will react to your change, and it is a fact you do not hold. This is the conflation the whole problem sits on. &quot;Changing the template&quot; sounds like one job with one audience. It is two deployments to two audiences on two clocks, and the only thing standing between you and a clean change is a census you were never given.&lt;/p&gt;
&lt;h2&gt;The half that breaks while you watch&lt;/h2&gt;
&lt;p&gt;The instant half is the one people have at least felt, because it shows up in the chat channel the same afternoon. The mechanics differ by platform, and the mechanics matter, because they decide how much of your org is in the instant bucket by default.&lt;/p&gt;
&lt;h3&gt;GitLab: the unpinned include detonates at merge&lt;/h3&gt;
&lt;p&gt;In GitLab CI, the &lt;code&gt;ref:&lt;/code&gt; on a project include is optional. When you leave it out, the include resolves to the HEAD of the template project&apos;s default branch, at pipeline creation time. That is &lt;a href=&quot;https://docs.gitlab.com/ci/yaml/&quot;&gt;documented behaviour in the CI/CD YAML reference&lt;/a&gt;, not an accident, and in practice most includes in most orgs are written without a ref because that is the shortest thing that works:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# In some-service/.gitlab-ci.yml
include:
  - project: &apos;platform/ci&apos;
    file: &apos;/templates/build.yml&apos;
# no ref: tracks platform/ci&apos;s default branch HEAD
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Every repo that wrote it this way is riding your &lt;code&gt;main&lt;/code&gt;. A merge to &lt;code&gt;platform/ci&lt;/code&gt; is not a release to them. It is an instant, org-wide deployment of CI configuration to every consumer that did not pin. GitLab says as much in its own docs: including another project&apos;s configuration is, from a security standpoint, &lt;a href=&quot;https://docs.gitlab.com/ci/yaml/&quot;&gt;like pulling a third-party dependency, and no pipeline or notification fires when that other project&apos;s files change&lt;/a&gt;. The dependency is real and it is silent. The default configuration is the dangerous one.&lt;/p&gt;
&lt;p&gt;This is not a theoretical risk that only bites small teams. GitLab has lived it at the scale of the largest CI template library in existence, and the &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-gitlab-ci-template/&quot;&gt;GitLab CI template edition&lt;/a&gt; of the Find Every Consumer series walks through the receipts: a default-branch rename that broke templates with hardcoded refs, and a GitLab engineer candidly describing blast-radius monitoring that amounts to waiting for customers to complain. If the maintainers of GitLab&apos;s own templates operate partially blind here, the &lt;code&gt;platform/ci&lt;/code&gt; repo in your org is not an outlier.&lt;/p&gt;
&lt;h3&gt;GitHub: a moving ref ships on the next run&lt;/h3&gt;
&lt;p&gt;GitHub Actions makes the same trap available with slightly different ergonomics. A reusable workflow or composite action is called by &lt;code&gt;uses:&lt;/code&gt;, and unlike a GitLab include, &lt;code&gt;uses:&lt;/code&gt; always requires a reference. So every caller pins to something. The catch is that &quot;something&quot; is very often a branch:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;jobs:
  build:
    uses: my-org/ci/.github/workflows/build.yml@main   # moving: next run picks up your change
    # vs @v1            -&amp;gt; a tag, only as frozen as the maintainer&apos;s discipline
    # vs @9a8b7c6...    -&amp;gt; a full SHA, the only genuinely immutable pin
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;@main&lt;/code&gt; behaves exactly like the unpinned GitLab include. The caller picks up your change on its next run, with no opt-in. A tag like &lt;code&gt;@v1&lt;/code&gt; looks safer, and is the advice everyone gives, but a git tag is mutable. It is only frozen if the maintainers never re-point it, which is why GitHub&apos;s own &lt;a href=&quot;https://docs.github.com/en/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions&quot;&gt;supply-chain hardening guidance&lt;/a&gt; recommends pinning third-party actions to a full-length commit SHA rather than a tag. The implication for you, the template maintainer, is the same as on GitLab: some unknown fraction of your callers are tracking a moving target and will take your next change immediately, and you cannot tell which from the inside.&lt;/p&gt;
&lt;p&gt;The transitive case sharpens it on both platforms. Template repos include other template repos. A reusable workflow &lt;code&gt;uses:&lt;/code&gt; a composite action that &lt;code&gt;uses:&lt;/code&gt; another. If you change a base template that a wrapper template includes, the repos that break are the ones that included the wrapper, which may never have heard of the base. They depend on you through a layer they did not write and cannot see. Finding the direct callers is a string search. Finding the transitive ones is a graph traversal, which is the whole reason the per-ecosystem editions of this series exist: the &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-github-actions-workflow/&quot;&gt;reusable GitHub Actions workflow edition&lt;/a&gt; covers the call-graph mechanics in full, and they do not reduce to grep.&lt;/p&gt;
&lt;h2&gt;The half you never hear from&lt;/h2&gt;
&lt;p&gt;The pinned consumers are the half nobody plans for, because they make no noise. They are green in CI, building against the version they froze, contributing nothing to your sense of who depends on you. Every signal you have about your consumer base comes from the consumers who are active and tracking. The pinned, dormant ones are statistically invisible right up until the moment they wake up and break.&lt;/p&gt;
&lt;p&gt;And when they break, they break in the worst possible way for debugging. They receive your change divorced from its context. The engineer on that payments service did not get &quot;the &lt;code&gt;test&lt;/code&gt; job was renamed three months ago,&quot; with a changelog and a migration note attached. They got a pipeline that suddenly would not start, bundled with a dependency bump and whatever else had accumulated on &lt;code&gt;platform/ci&lt;/code&gt; since they last pinned. The cause was a quarter old and wearing someone else&apos;s clothes.&lt;/p&gt;
&lt;p&gt;This is the precise failure mode the &lt;a href=&quot;https://riftmap.dev/blog/deprecate-internal-library-find-consumers/&quot;&gt;internal library deprecation&lt;/a&gt; post is built around, and it is worth being explicit that CI is the same problem in a harsher form. There, a service that pinned an old version of a shared package and went quiet never saw the deprecation warning, because the warning lived in a version it was not pulling. The quiet, pinned consumer is the one that breaks, in both cases, because quiet is exactly what &quot;we pinned it and stopped thinking about it&quot; looks like from the outside. The difference, and the reason CI is worse, is what you have to reach them with.&lt;/p&gt;
&lt;h2&gt;CI is the one shared dependency with no signal layer&lt;/h2&gt;
&lt;p&gt;Here is the part that makes a CI template change genuinely harder than a library deprecation, rather than just an instance of it.&lt;/p&gt;
&lt;p&gt;When you deprecate a shared library, the playbook hands you real signalling machinery. It is conditional and often silent, and the deprecation post takes that machinery apart in detail, but it exists. You can add a &lt;code&gt;@deprecated&lt;/code&gt; annotation that an active consumer&apos;s editor will flag. You can raise a &lt;code&gt;DeprecationWarning&lt;/code&gt; that a test suite can surface. You can cut a semver major so the removal lands on a boundary tooling is built to respect. npm will even print &lt;code&gt;npm WARN deprecated&lt;/code&gt; on install. None of it reaches a pinned, dormant consumer reliably, which is the deprecation post&apos;s whole argument, but for the consumers who are paying attention, there is a channel.&lt;/p&gt;
&lt;p&gt;A shared CI template has none of this. There is no &lt;code&gt;@deprecated&lt;/code&gt; for a job name. There is no warning you can attach to an &lt;code&gt;extends&lt;/code&gt; target that fires when a downstream pipeline relies on it. There is no semver boundary, because most &lt;code&gt;include:project&lt;/code&gt; and &lt;code&gt;@main&lt;/code&gt; consumers are not pinning versions at all, and the loosely typed surface a template actually exposes, the job names, the variables, the &lt;code&gt;extends&lt;/code&gt; targets, has no declared interface to mark up in the first place. GitLab&apos;s newer &lt;a href=&quot;https://docs.gitlab.com/ci/inputs/&quot;&gt;&lt;code&gt;spec:inputs&lt;/code&gt;&lt;/a&gt; gives components something like a typed contract, which is real progress, but it covers a sliver of the installed base. The fleet of &lt;code&gt;include:project&lt;/code&gt; templates that nearly every self-managed estate runs on communicates entirely through job names and variables that no compiler checks.&lt;/p&gt;
&lt;p&gt;Strip it down and the signalling options for a CI template change are exactly two. The instant consumers find out when their pipeline turns red, which is to say after the change has already broken them, which is not a signal so much as a casualty report. The pinned consumers find out never, until they bump. The one human channel left is an announcement in a chat channel, and the deprecation post already covers why that reaches everyone except the people you most need it to reach: the team that pinned your template eighteen months ago and reorganised twice is, by definition, the team not reading your announcement.&lt;/p&gt;
&lt;p&gt;So for a library, signalling is the easy half of the job and the census is the hard half. For a shared CI template, the signalling half very nearly does not exist. There is no annotation, no warning, no version boundary, no typed interface, nothing between your merge and someone else&apos;s red pipeline. Which leaves the census not as the harder half of the work. It leaves the census as essentially all of it.&lt;/p&gt;
&lt;h2&gt;Neither platform shows you the roster&lt;/h2&gt;
&lt;p&gt;Once it is clear that the signal will not assemble the consumer list for you, you go to build it by hand, and every route gets you partway to the same wall. I want to be fair to each, because some of them are genuinely useful for part of the problem.&lt;/p&gt;
&lt;p&gt;Code search is the obvious first move and it works for a one-off. Search your group for the template path and you get matches. What you do not get is the ref, the distinction between a real &lt;code&gt;include:&lt;/code&gt; and a comment that mentions the path, or the second hop where a wrapper template hides the forty repos behind it. On GitLab, the good version of code search is the Premium &lt;a href=&quot;https://docs.gitlab.com/user/search/advanced_search/&quot;&gt;advanced search&lt;/a&gt;, which on self-managed you have to stand up and operate yourself. On GitHub, search will not surface reusable-workflow callers across private repos as a clean roster either. Matches are a starting point. They are not a system, and they are certainly not a list with refs and owners attached.&lt;/p&gt;
&lt;p&gt;The platforms&apos; native dependency views are scoped to the wrong place. GitHub has a Dependents tab built for almost exactly this question, and it &lt;a href=&quot;https://docs.github.com/en/code-security/how-tos/secure-your-supply-chain/secure-your-dependencies/exploring-the-dependencies-of-a-repository&quot;&gt;only computes dependents for public repositories&lt;/a&gt;. Your shared CI lives in a private org, which is the one case it does not cover. GitLab deserves real credit here, because it has started to answer the question: the GitLab 19.0 &lt;a href=&quot;https://about.gitlab.com/blog/track-ci-component-usage/&quot;&gt;CI/CD Catalog components analytics&lt;/a&gt; view shows usage counts and, on Ultimate, which projects used a component recently and on which version. But look closely at what that covers. It covers &lt;code&gt;include:component&lt;/code&gt;, resources published to the Catalog, on a thirty-day usage window derived from pipelines that actually ran, with the per-project drill-down gated to Ultimate. The &lt;code&gt;include:project&lt;/code&gt; template fleets that nearly every enterprise estate actually runs on, including the &lt;code&gt;platform/ci&lt;/code&gt; repo in the scene above, are out of scope. The gap is closing for components on the newest tier. For everyone else it is wide open.&lt;/p&gt;
&lt;p&gt;Then you write the script, and several teams have, which is the strongest possible evidence that the question matters. Enumerate every project, fetch every CI config, parse the YAML, extract the includes and &lt;code&gt;uses:&lt;/code&gt; calls, resolve the refs, handle the five different GitLab include forms and the remote includes a literal search misses, reconstruct the nested chains, page through the API, respect the rate limits, run it on a schedule, store the result. That is a real system with a real backlog, and the corner cases are now yours to maintain. The full enumeration mechanics, per platform, are exactly what the &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-gitlab-ci-template/&quot;&gt;GitLab CI template&lt;/a&gt; and &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-github-actions-workflow/&quot;&gt;GitHub Actions workflow&lt;/a&gt; editions of this series document, so I will not re-derive them here. The point for this post is one level up: whichever route you take, you are trying to reconstruct, by hand, a list the platform could have kept and did not.&lt;/p&gt;
&lt;h2&gt;The list is a graph query, not a search&lt;/h2&gt;
&lt;p&gt;Strip the problem back to what you actually need before you touch a shared template, and it is small and specific. Every repository whose CI configuration references this template, directly or transitively. The reference each one declares, and crucially whether it is a moving ref or a pinned one, because that single fact sorts every consumer into the instant bucket or the silent bucket. The team that owns each repo, so you know who to route a migration to. And the order, so you change the base template before the wrappers that include it.&lt;/p&gt;
&lt;p&gt;That is a query against a graph of dependency edges, and the only honest way to build the graph is to parse it. Read the &lt;code&gt;include:&lt;/code&gt; entries in every &lt;code&gt;.gitlab-ci.yml&lt;/code&gt;, resolving &lt;code&gt;project&lt;/code&gt;, &lt;code&gt;remote&lt;/code&gt; and &lt;code&gt;component&lt;/code&gt; forms back to the repos they point at and ignoring &lt;code&gt;local&lt;/code&gt; and GitLab-shipped &lt;code&gt;template:&lt;/code&gt; includes, which are noise in a consumer map. Read the &lt;code&gt;uses:&lt;/code&gt; in every &lt;code&gt;.github/workflows/*.yml&lt;/code&gt;, following reusable workflows and composite actions. Read the template files that template repos themselves carry, so the nested chain from a base template to its end consumers is in the graph and not just the wrapper that includes it directly. Record the ref each consumer declares, or its absence, as a first-class fact. Parsed, not inferred. Not counted from thirty days of pipeline runs, not read off a wiki page someone last edited before the reorg, not pieced together from a chat thread. Read from the CI config that already declares the edge, because that file is the source of truth and it is also exactly where any migration is going to land.&lt;/p&gt;
&lt;p&gt;The pin-or-no-pin column is the one that turns this from a list into a plan. With it, &quot;who breaks at merge and who has time&quot; stops being a guess. You can see the repos riding an unpinned include or a &lt;code&gt;@main&lt;/code&gt; and know they take the change instantly, so they are the ones to fix or warn first. You can see the repos frozen on an old tag and know they are stranded, carrying a debt that comes due whenever they next bump, so they are the ones to migrate deliberately before they wake up and break alone. Two buckets, two plans, derived from a column the platform never gave you.&lt;/p&gt;
&lt;h2&gt;Why the template calcifies&lt;/h2&gt;
&lt;p&gt;There is a second cost to not being able to see the roster, and it is quieter than the incident, which is why it is worse over time.&lt;/p&gt;
&lt;p&gt;When you cannot enumerate who consumes a shared template, the rational thing to do is to never restructure it. Think about what a clean refactor of &lt;code&gt;platform/ci&lt;/code&gt; would ask of you. To rename that job properly, to collapse two templates into one, to drop a variable nobody should be using any more, you have to be willing to say &quot;this will not break anyone,&quot; and if you are wrong, it breaks at merge time, org-wide, in other people&apos;s projects, with your name on the commit. &quot;I am fairly sure nothing extends from this&quot; is not a sentence you want to be holding when forty pipelines fail to start. So the safe move, the locally rational move, is to never make the structural change. Add a new job beside the old one instead of renaming it. Bolt a new variable on rather than reworking the interface. Leave the dead path in, just in case.&lt;/p&gt;
&lt;p&gt;This is how shared CI templates turn into append-only sprawl. Not because nobody wants to clean them up. Because removal and restructuring are unprovable-safe, and the cost of being wrong is paid in production at an inconvenient hour by someone else. The template accretes jobs, flags, and compatibility shims, each one cheap to add and impossible to remove, until it is a file nobody fully understands and everybody is slightly afraid of. The same dynamic that leaves &lt;a href=&quot;https://riftmap.dev/blog/deprecate-internal-library-find-consumers/&quot;&gt;undead modules in a codebase&lt;/a&gt; leaves undead jobs in your pipeline library, for the same reason: you cannot prove the removal is safe, so you never attempt it.&lt;/p&gt;
&lt;p&gt;A trustworthy consumer census changes the calculus entirely. When &quot;who consumes this template, at which ref, owned by whom&quot; is one query, restructuring stops being a gamble and becomes a scheduled piece of work. You see the four repos that extend the job you want to rename, you route the change, you watch the number go to zero, and then you rename it for real. The census is not only the thing you reach for the morning a pipeline falls over. It is the thing that lets a shared template get smaller instead of only ever larger.&lt;/p&gt;
&lt;h2&gt;Two rosters, one merge button&lt;/h2&gt;
&lt;p&gt;So, stripped down. Changing a shared CI template is two deployments wearing one merge, and the renamed job showed which audience each one reaches.&lt;/p&gt;
&lt;p&gt;One deployment is instant and loud. It ships to every consumer tracking your branch, an unpinned GitLab include or a GitHub &lt;code&gt;@main&lt;/code&gt;, the moment you merge, and you hear about it the same afternoon when their pipelines turn red. The other is deferred and silent. It waits, fully merged and completely invisible, on every consumer pinned to a tag, until some unrelated bump pulls them forward into the accumulated change and breaks them alone, months later, blamed on something else. The signalling machinery that would warn either group barely exists for CI: no annotation, no warning, no version boundary, no typed interface, nothing between your edit and someone else&apos;s failed pipeline.&lt;/p&gt;
&lt;p&gt;Which means the roster is the whole game. Not the harder half of the job, the way it is for a library. The entire job. And the roster for both buckets, the instant one and the silent one, with the pin or its absence beside every consumer, was sitting in the CI config across your org the whole time. The template that fifty projects include without a ref is the highest-leverage, least-observed dependency you own. Parse who depends on it before you press merge, because the platform will not, and the alternative is finding out from the people you should have told.&lt;/p&gt;
&lt;p&gt;This is the query &lt;a href=&quot;https://riftmap.dev/&quot;&gt;Riftmap&lt;/a&gt; is built to answer. Point it at your GitHub or GitLab organisation with one read-only token and it parses the CI edges across every repo: &lt;code&gt;include:project&lt;/code&gt;, &lt;code&gt;include:remote&lt;/code&gt; and &lt;code&gt;include:component&lt;/code&gt; on GitLab, reusable-workflow and composite-action &lt;code&gt;uses:&lt;/code&gt; on GitHub, plus the multi-project triggers and cross-project &lt;code&gt;needs:&lt;/code&gt; a string search misses, while deliberately skipping the local and platform-shipped includes that are noise. It parses the template repos&apos; own files too, so the transitive chain from a base template to its end consumers is in the graph. Each edge carries the ref the consumer declares, or its absence, plus the file and line where it lives. Before you merge that job rename, you open the graph, click the template, and read the consumer list, sorted into the ones riding your branch and the ones frozen on a tag. You know who breaks now, who breaks later, and who to tell, instead of finding out who you should have told. And if you want that check to run itself, &lt;a href=&quot;https://riftmap.dev/blog/blast-radius-gate-merge-pipeline/&quot;&gt;the blast-radius gate recipe&lt;/a&gt; wires the same consumer count into a CI job on every merge request.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;Riftmap maps cross-repo dependencies across your entire GitLab or GitHub organisation: Terraform, Docker, CI templates and components, Helm, npm, Go, Python, and more. One read-only token. No YAML to maintain. &lt;a href=&quot;https://app.riftmap.dev&quot;&gt;The free tier is here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Appendix: the argument in short&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Claim.&lt;/strong&gt; Changing a shared CI template is treated as a single task and is actually two deployments. One ships instantly to consumers tracking a moving ref (an unpinned GitLab &lt;code&gt;include:project&lt;/code&gt;, a GitHub &lt;code&gt;@main&lt;/code&gt;) and breaks them at merge time. The other waits, silent and invisible, on consumers pinned to a tag, and breaks them alone whenever they next bump the pin, decoupled from any context. CI is worse than a library deprecation because it has essentially no signalling layer: no &lt;code&gt;@deprecated&lt;/code&gt;, no &lt;code&gt;DeprecationWarning&lt;/code&gt;, no semver boundary, no typed interface for job names and variables. That leaves the consumer census as not the hard half of the job but the whole of it, and the census can only be built by parsing the &lt;code&gt;include:&lt;/code&gt; and &lt;code&gt;uses:&lt;/code&gt; edges every repo&apos;s CI config already declares, with the ref or its absence recorded against each consumer.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Consumer census (CI edition).&lt;/strong&gt; The set of repositories whose CI configuration references a given shared template, directly or transitively, with the reference each one declares (and specifically whether it is moving or pinned) and the team that owns it, derived by parsing &lt;code&gt;.gitlab-ci.yml&lt;/code&gt; and &lt;code&gt;.github/workflows/*.yml&lt;/code&gt; across an organisation rather than from usage telemetry, a wiki, or an announcement.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;FAQ.&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;If I change a shared CI template, which pipelines break?&lt;/em&gt; Two separate groups, on two timelines. Consumers tracking a moving ref (an unpinned GitLab &lt;code&gt;include:project&lt;/code&gt;, which resolves to the template project&apos;s default-branch HEAD, or a GitHub &lt;code&gt;uses: ...@main&lt;/code&gt;) take the change on their next pipeline run and break immediately. Consumers pinned to a tag or SHA do not see the change until someone bumps the pin, then receive the whole accumulated diff at once. Neither GitLab nor GitHub gives the maintainer the list of which consumers are in which group.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;How do I safely change a shared CI template used across many repos?&lt;/em&gt; Build the consumer census first. Parse the &lt;code&gt;include:&lt;/code&gt; and &lt;code&gt;uses:&lt;/code&gt; edges across every repo, record the ref each consumer declares (or its absence), and sort consumers into the moving-ref group that breaks at merge and the pinned group that is stranded. Change base templates before the wrappers that include them. The signal will not do this for you, because a CI template has no deprecation-warning mechanism.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Why does a GitLab CI &lt;code&gt;include&lt;/code&gt; without a &lt;code&gt;ref&lt;/code&gt; get template changes instantly?&lt;/em&gt; Because an omitted &lt;code&gt;ref:&lt;/code&gt; on a project include resolves to the HEAD of the template project&apos;s default branch at pipeline creation time, which is documented behaviour. A merge to that branch is therefore an immediate, org-wide deployment to every consumer that did not pin, with no notification fired.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Can GitHub&apos;s Dependents view or GitLab&apos;s Catalog analytics tell me who consumes my CI template?&lt;/em&gt; Partially, and not for the common case. GitHub&apos;s dependency graph computes dependents for public repositories only, so a private org&apos;s shared workflow is not covered. GitLab&apos;s CI/CD Catalog components analytics covers published Catalog components on a thirty-day usage window, with per-project detail on Ultimate, but not the &lt;code&gt;include:project&lt;/code&gt; template fleets most self-managed estates actually run on.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Related reading.&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-gitlab-ci-template/&quot;&gt;How to find every consumer of your GitLab CI template&lt;/a&gt; — the full GitLab-side enumeration mechanics this post sits on top of: the five include forms, nested chains, triggers and cross-project needs.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-github-actions-workflow/&quot;&gt;How to find every consumer of your reusable GitHub Actions workflow&lt;/a&gt; — the same enumeration, for reusable workflows and composite actions.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://riftmap.dev/blog/deprecate-internal-library-find-consumers/&quot;&gt;You deprecated the internal library. The repos still using it never saw the warning&lt;/a&gt; — the sibling post. Same census-versus-signal split, but for libraries, which at least have a signalling layer; CI is the limit case where that layer is gone.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://riftmap.dev/blog/base-image-cve-which-repos-to-fix/&quot;&gt;A CVE just hit your base image. Your scanner won&apos;t tell you which repos to fix&lt;/a&gt; — the reactive twin: the same cross-repo-versus-single-artifact gap, forced by an external shock instead of a planned edit.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://riftmap.dev/blog/dependency-bus-factor/&quot;&gt;Your senior engineer just left. Your bus factor was measuring the wrong thing&lt;/a&gt; — what happens when the person who held the consumer map in their head walks out.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://riftmap.dev/blog/gitlab-orbit-and-the-artifact-layer/&quot;&gt;GitLab Orbit maps your whole SDLC. It still can&apos;t tell you what an infrastructure change will break&lt;/a&gt; — why a CI &lt;code&gt;include&lt;/code&gt; edge is an artifact edge a symbol graph cannot see.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://riftmap.dev/blog/the-catalog-maintenance-trap/&quot;&gt;The catalog maintenance trap&lt;/a&gt; — why the hand-maintained &quot;consumers of this template&quot; wiki page is wrong by the time you read it.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://riftmap.dev/what-is-cross-repo-dependency-mapping/&quot;&gt;What is cross-repo dependency mapping?&lt;/a&gt; — the glossary definition of the parsed graph this post keeps pointing at.&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>gitlab-ci</category><category>github-actions</category><category>platform-engineering</category><category>dependency-management</category><author>Daniel Westgaard</author></item><item><title>How to Find Every Consumer of Your Ansible Role</title><link>https://riftmap.dev/blog/how-to-find-every-consumer-of-your-ansible-role/</link><guid isPermaLink="true">https://riftmap.dev/blog/how-to-find-every-consumer-of-your-ansible-role/</guid><description>Ansible has no lockfile and no reverse lookup: change a shared role and nothing says who breaks. Why finding every consumer is harder than grep.</description><pubDate>Sun, 21 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;You have got a one-line fix for the &lt;code&gt;hardening&lt;/code&gt; role. A &lt;code&gt;sysctl&lt;/code&gt; value that should have been set from the start, the kind of change nobody notices until an auditor does. You open the role&apos;s repository, make the edit, and reach for a tag.&lt;/p&gt;
&lt;p&gt;Then the question lands, the way it always does right before you cut a release. Who gets this?&lt;/p&gt;
&lt;p&gt;You look at the repository in front of you for an answer. There is a &lt;code&gt;tasks/&lt;/code&gt; directory, a &lt;code&gt;defaults/main.yml&lt;/code&gt;, a &lt;code&gt;meta/main.yml&lt;/code&gt;, and a README that three people have edited and nobody has read. None of it names a single playbook that uses the role. Nothing inside the role knows who depends on it. That information lives somewhere else, in repositories you are not currently looking at, and the role has no way to point you towards them.&lt;/p&gt;
&lt;p&gt;This is not your team being sloppy. It is structural, and Ansible chose it on purpose.&lt;/p&gt;
&lt;p&gt;Here is the proof. Back in 2016, Jeff Geerling, who maintains more widely used Ansible roles than almost anyone alive, filed a request for a lock file. Something like &lt;code&gt;package-lock.json&lt;/code&gt; or &lt;code&gt;poetry.lock&lt;/code&gt;, a file that records exactly which version of each role and collection actually resolved, so a run is reproducible and you can see what you are really shipping. The request sat. When the old issue tracker was archived years later, it was carried over and then quietly abandoned. A decade on, Ansible still has no native lock file.&lt;/p&gt;
&lt;p&gt;Sit with what that means. Every other ecosystem in this series at least commits to telling you which version is installed. npm has &lt;code&gt;package-lock.json&lt;/code&gt;. Go has &lt;code&gt;go.sum&lt;/code&gt;. Python has its various lock formats. Ansible looked at that primitive and decided not to build it. And if the ecosystem will not commit to telling you what version of a role actually ran, it is certainly not going to tell you who consumes that role across your organisation. The reverse question, the one you are asking right now with your finger over the tag, was never in scope.&lt;/p&gt;
&lt;p&gt;So let us answer it properly. If you change a shared Ansible role, what breaks? And how do you find every consumer before you find out the hard way?&lt;/p&gt;
&lt;h2&gt;Where your role actually gets used&lt;/h2&gt;
&lt;p&gt;Start with the shape of the problem, because Ansible reuse spreads a role&apos;s dependents across more places than people expect.&lt;/p&gt;
&lt;p&gt;The canonical layout, the one the official best practice points you towards, is one repository for your playbooks and a separate repository per role. The playbook repository pulls the roles it needs through a &lt;code&gt;requirements.yml&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# platform-playbooks/roles/requirements.yml
roles:
  - name: hardening
    src: https://gitlab.example.com/platform/ansible-role-hardening.git
    scm: git
    version: v2.3.0

  - name: geerlingguy.nginx
    version: &quot;3.1.4&quot;

collections:
  - name: polaris.infrastructure
    source: https://automationhub.example.com/api/galaxy/
    version: &quot;&amp;gt;=1.4.0,&amp;lt;2.0.0&quot;

  - name: community.crypto
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That is one site where your role&apos;s consumers live. There are several more, and they do not look alike.&lt;/p&gt;
&lt;p&gt;A play can pull a role in directly at the play level:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- hosts: webservers
  roles:
    - hardening
    - role: geerlingguy.nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A task can pull one in dynamically or statically, anywhere in the tasks section:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;tasks:
  - ansible.builtin.import_role:
      name: hardening

  - ansible.builtin.include_role:
      name: hardening
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And a role can pull in another role as a dependency, declared in its own &lt;code&gt;meta/main.yml&lt;/code&gt;, which then runs before it, recursively:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# ansible-role-webserver/meta/main.yml
dependencies:
  - role: hardening
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Read that last one again, because it is the one that bites. If &lt;code&gt;webserver&lt;/code&gt; depends on &lt;code&gt;hardening&lt;/code&gt; in its metadata, then every playbook that uses &lt;code&gt;webserver&lt;/code&gt; is also a consumer of &lt;code&gt;hardening&lt;/code&gt;, and not one of those playbooks mentions &lt;code&gt;hardening&lt;/code&gt; anywhere. The dependency is real, it runs in production, and it is invisible to anyone searching the playbook repositories for the word &quot;hardening&quot;.&lt;/p&gt;
&lt;p&gt;So the consumers of your role are scattered across &lt;code&gt;requirements.yml&lt;/code&gt; files, play-level &lt;code&gt;roles:&lt;/code&gt; keys, &lt;code&gt;import_role&lt;/code&gt; and &lt;code&gt;include_role&lt;/code&gt; tasks, and the &lt;code&gt;meta/main.yml&lt;/code&gt; of other roles. Every one of those lives in a repository other than the one you are about to tag. None of them is listed anywhere central. This is the same structural fact behind the whole &lt;a href=&quot;https://riftmap.dev/blog/series/find-every-consumer/&quot;&gt;Find Every Consumer series&lt;/a&gt;: the dependency lives in the relationship between repositories, and the artifact you are changing cannot see its own dependents. It is the exact shape we covered for &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-terraform-module/&quot;&gt;Terraform modules&lt;/a&gt; pulled by git source, only Ansible has more entry points and weaker typing.&lt;/p&gt;
&lt;h2&gt;The version you cannot see&lt;/h2&gt;
&lt;p&gt;Now layer the lock file problem back on top, because it turns a hard problem into a worse one.&lt;/p&gt;
&lt;p&gt;Look again at the &lt;code&gt;requirements.yml&lt;/code&gt; above. The &lt;code&gt;hardening&lt;/code&gt; role is pinned to &lt;code&gt;v2.3.0&lt;/code&gt;. The &lt;code&gt;polaris.infrastructure&lt;/code&gt; collection is pinned to a range, &lt;code&gt;&amp;gt;=1.4.0,&amp;lt;2.0.0&lt;/code&gt;. The &lt;code&gt;community.crypto&lt;/code&gt; collection has no version at all. Each of these is a different relationship to the thing you are about to release, and you have probably got all three patterns live across your estate right now.&lt;/p&gt;
&lt;p&gt;When a version is absent, &lt;code&gt;ansible-galaxy install&lt;/code&gt; takes the latest it can find. When it is a range, you get the newest release inside the range at install time. When it is a branch or &lt;code&gt;main&lt;/code&gt;, you get whatever that branch points at the moment the pipeline runs. And here is the part with no safety net: nothing writes down what actually resolved. There is no lock file to read afterwards. With npm you can at least open &lt;code&gt;package-lock.json&lt;/code&gt; and see the truth, which is why &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-internal-npm-package/&quot;&gt;&quot;the change you ship without shipping it&quot; was the core of the npm post&lt;/a&gt;. In Ansible you cannot, because the file that would hold that truth was never built.&lt;/p&gt;
&lt;p&gt;So tagging &lt;code&gt;v2.4.0&lt;/code&gt; of your role is not shipping a change to a known list of consumers at a known version. It is a fan-out to an unknown set of repositories, an unknown number of which are pinned, ranged, or floating, resolving on a schedule you do not control, with no record of what each one actually ran. You are pushing a change into a system that has deliberately chosen not to remember its own state.&lt;/p&gt;
&lt;p&gt;The first thing a real answer has to do, then, is the thing the ecosystem refuses to. It has to tell you, for every consumer, whether they pinned you, ranged you, or left it floating. &quot;Who is unpinned&quot; is not a nice-to-have. In an ecosystem with no lock file, it is the single most important fact about your blast radius, because the unpinned consumers are the ones that will move the moment you tag, without anyone deciding to.&lt;/p&gt;
&lt;h2&gt;What Ansible&apos;s own tooling tells you, and where it stops&lt;/h2&gt;
&lt;p&gt;The Ansible toolchain is genuinely good, and it answers real questions. It just answers the other ones.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ansible-galaxy collection list&lt;/code&gt; and &lt;code&gt;ansible-galaxy role list&lt;/code&gt; tell you what is installed in the environment you are standing in. That is forward, local, and per-environment. There is no &lt;code&gt;ansible why&lt;/code&gt;. The CLI will happily tell you that a collection is present; it has no concept of which of your repositories declared it, let alone which tasks use it.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ansible-inventory --graph --vars&lt;/code&gt; and &lt;code&gt;--host&lt;/code&gt; will reconstruct what a given host ends up with, variables and all. Again, forward, and per-host. It answers &quot;what does this host get&quot;, never &quot;who across my repositories consumes this&quot;.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://ansible.readthedocs.io/projects/lint/&quot;&gt;ansible-lint&lt;/a&gt; and &lt;a href=&quot;https://ansible.readthedocs.io/projects/molecule/&quot;&gt;Molecule&lt;/a&gt; are excellent, and you should be running both. ansible-lint in particular will flag short module names, deprecated syntax, and a long list of anti-patterns. But linting is per-project quality enforcement and Molecule is per-role testing. Neither builds a cross-repository map of who depends on what. They make each repository better in isolation; they do not connect them.&lt;/p&gt;
&lt;p&gt;It is worth noting how often engineers reach past all of this and build the reverse lookup themselves. There is a published tool, &lt;code&gt;ansible-discover&lt;/code&gt;, whose entire purpose is to list the dependants of a role so you can trigger the right CI jobs when it changes. There is &lt;code&gt;ansible-variables&lt;/code&gt;, which exists to trace where a host&apos;s variables actually come from. Both are small, both are local-path, both work only across the repositories you have already checked out together. They are proof that the question matters enough for people to keep writing the same tool, and they define the gap precisely: the moment the answer spans repositories that are not on one disk at one time, these stop.&lt;/p&gt;
&lt;p&gt;Then there is the registry layer, and this is where people most often expect the answer to live, because it feels like it should. Ansible Galaxy, Automation Hub, and a private Automation Hub do distribution, curation, governance, and supply-chain control, and they do it well. What they record is publication and downloads. A download count is not a dependency edge. Knowing that &lt;code&gt;polaris.infrastructure&lt;/code&gt; was pulled four hundred times this month tells you nothing about which of your repositories declared it, which tasks call into it, or what breaks if you change it. This is the same trap as &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-go-module/&quot;&gt;the Go module proxy&lt;/a&gt;: the proxy logs fetches, and a fetch is not a use.&lt;/p&gt;
&lt;p&gt;Automation Controller, AWX, and the wider Automation Platform sit one layer up and know about Projects, where each Project is a git repository the controller syncs and whose &lt;code&gt;requirements.yml&lt;/code&gt; it installs at sync time. Automation Analytics will report on job runs, host counts, and ROI. All useful, all runtime. The controller knows which Projects exist and installs their requirements; it does not parse and present a cross-Project graph of which Projects depend on which role. It answers &quot;what ran&quot; and &quot;what runs&quot;, not &quot;what is declared, everywhere, right now&quot;. It is the same distinction we drew for &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-gitlab-ci-template/&quot;&gt;GitLab&apos;s pipeline analytics&lt;/a&gt;, where usage events told you about executions and never about the static set of repositories that include a thing.&lt;/p&gt;
&lt;p&gt;And the update bots, which are the closest any of these come to caring about consumers? They treat Ansible as a second-class citizen. Renovate does have an &lt;code&gt;ansible-galaxy&lt;/code&gt; manager that will bump role and collection versions per consumer, which means it implicitly knows about the repositories it is switched on for. But its data source for collections ignores a custom &lt;code&gt;source:&lt;/code&gt; and falls back to public Galaxy, so for an internal collection served from your own Automation Hub, the exact case where blast radius is your problem, Renovate is effectively looking at the wrong registry. Dependabot does not support Ansible Galaxy at all. The feature request has been open since 2021 with a steady trickle of thumbs-up and no support. Even the ecosystem built to watch your dependencies for you does not watch these.&lt;/p&gt;
&lt;p&gt;Every tool above answers a forward question or a distribution question. What is installed here. What does this host get. What did Galaxy serve. What ran last night. Not one of them answers the only question that matters when your finger is on the tag: across all of my repositories, who consumes this role, at what version pin, and what breaks if I change it.&lt;/p&gt;
&lt;h2&gt;Why this is harder than it looks&lt;/h2&gt;
&lt;p&gt;You might reasonably think this is a grep problem. Search every repository for the role name, collect the hits, done. It is not, and the reasons are specific to how Ansible resolves things at runtime rather than at parse time.&lt;/p&gt;
&lt;h3&gt;A role&apos;s real interface is variables it never declares&lt;/h3&gt;
&lt;p&gt;This is the hardest thing in Ansible, and it is worth slowing down for, because it is the one that has no clean answer anywhere.&lt;/p&gt;
&lt;p&gt;A role&apos;s true contract is not its name. It is the set of variables it reads. And in Ansible, those variables are supplied by the consumer, not declared by the role. Your &lt;code&gt;hardening&lt;/code&gt; role reads &lt;code&gt;hardening_sysctl_overrides&lt;/code&gt;. It does not, and cannot, list that as a typed input the way a function signature would. The value is set somewhere out in the consumer&apos;s world: in a &lt;code&gt;group_vars/webservers.yml&lt;/code&gt;, in a &lt;code&gt;host_vars&lt;/code&gt; file, in a &lt;code&gt;vars:&lt;/code&gt; block on a play, in an &lt;code&gt;--extra-vars&lt;/code&gt; flag in a pipeline, in a &lt;code&gt;set_fact&lt;/code&gt;. Ansible resolves which one wins through a precedence order with more than twenty levels, and role defaults sit very near the bottom of it, which is the source of a thousand confused afternoons.&lt;/p&gt;
&lt;p&gt;Now rename that variable. Ship &lt;code&gt;hardening&lt;/code&gt; &lt;code&gt;v2.4.0&lt;/code&gt; where &lt;code&gt;hardening_sysctl_overrides&lt;/code&gt; becomes &lt;code&gt;hardening_sysctl_config&lt;/code&gt;. Nothing errors at parse time. There is no compile step to fail. The old variable simply stops being read, the new one falls back to its default, and a hardening setting silently reverts on every host whose consumer set the value under the old name. There is no grep target, because the breaking change is the absence of a string near a role reference that does not textually mention the role at all. The consumer set a variable; it never said which role the variable was for.&lt;/p&gt;
&lt;p&gt;This is the inverted contract at the heart of Ansible reuse. The role depends on inputs it does not declare. The playbook declares inputs without saying which role they feed. The dependency edge has no home in either file. It is the reason this is genuinely harder than any of the manifest-based ecosystems in this series, and, as we will get to, it is also the honest boundary of what any tool that reads source can claim.&lt;/p&gt;
&lt;h3&gt;The same module call has two spellings&lt;/h3&gt;
&lt;p&gt;A collection ships modules, and the real blast radius of a collection is every task that calls one of its modules. Ansible lets you write that call two ways. Fully qualified, &lt;code&gt;polaris.infrastructure.configure_firewall&lt;/code&gt;, or as a bare &lt;code&gt;configure_firewall&lt;/code&gt; with a play-level &lt;code&gt;collections:&lt;/code&gt; keyword setting up a search path.&lt;/p&gt;
&lt;p&gt;The catch is that the short form is on the way out. The official documentation describes the &lt;code&gt;collections:&lt;/code&gt; keyword as a temporary mechanism from the 2.9 transition and tells you to rewrite content that uses it and prefer the fully qualified name. ansible-lint actively flags it. So modern, well-written playbooks tend to use the fully qualified name directly in every task and carry no &lt;code&gt;collections:&lt;/code&gt; block at all. Which means a grep for the short name misses the modern callers, a grep for the fully qualified name misses the legacy ones, and the bare short name collides with built-ins and with identically named modules in other collections. The usage edge for a collection is real, it is increasingly written in fully qualified form in task bodies, and it is precisely where naive search is weakest.&lt;/p&gt;
&lt;h3&gt;Static and dynamic inclusion do not grep the same&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;roles:&lt;/code&gt; and &lt;code&gt;import_role&lt;/code&gt; are static. Ansible processes them at parse time, so the role name is right there in the file. &lt;code&gt;include_role&lt;/code&gt; is dynamic, evaluated at runtime, and it can take a computed name:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- ansible.builtin.include_role:
    name: &quot;{{ role_for_this_environment }}&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There is no string &lt;code&gt;hardening&lt;/code&gt; to find here. The consumer relationship is a variable that resolves during the run. This is unresolvable from source by anyone, including us, and any tool that claims otherwise is guessing. The honest move is to find the literal cases and be straight about the computed ones.&lt;/p&gt;
&lt;h3&gt;Dependencies hide behind other dependencies&lt;/h3&gt;
&lt;p&gt;Back to &lt;code&gt;meta/main.yml&lt;/code&gt;. Role dependencies are recursive and run before the role that declares them. If &lt;code&gt;webserver&lt;/code&gt; depends on &lt;code&gt;hardening&lt;/code&gt;, and &lt;code&gt;app-tier&lt;/code&gt; depends on &lt;code&gt;webserver&lt;/code&gt;, then a playbook using &lt;code&gt;app-tier&lt;/code&gt; is a transitive consumer of &lt;code&gt;hardening&lt;/code&gt; two hops away, naming neither intermediate. Finding direct consumers is a string search. Finding transitive ones is a graph traversal, and you cannot fake a graph traversal with grep, which is the same wall we hit with &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-gitlab-ci-template/&quot;&gt;nested GitLab CI includes&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;The declaration is not in one place&lt;/h3&gt;
&lt;p&gt;Even the manifest is not one file. A &lt;code&gt;requirements.yml&lt;/code&gt; can pull in other requirements files with an &lt;code&gt;include:&lt;/code&gt; directive. Collections get declared in &lt;code&gt;requirements.yml&lt;/code&gt;, in a collection&apos;s own &lt;code&gt;galaxy.yml&lt;/code&gt; dependencies, in role metadata, and in execution-environment definitions that bundle a requirements file into a container image. &lt;code&gt;ansible.cfg&lt;/code&gt; can redirect where roles and collections resolve from with &lt;code&gt;roles_path&lt;/code&gt; and &lt;code&gt;collections_path&lt;/code&gt;. The string that represents your role appears in a different shape in each of these, and a tool that reads only the root &lt;code&gt;requirements.yml&lt;/code&gt; of each repository will quietly undercount.&lt;/p&gt;
&lt;h2&gt;What the full answer requires&lt;/h2&gt;
&lt;p&gt;Put all of that together and the requirements for actually answering &quot;who consumes this role&quot; fall out:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Scan every repository in the organisation, with no opt-in and no per-repo config.&lt;/strong&gt; The consumers live in repositories other than the one you are changing, so anything that needs each team to register or annotate will be incomplete the day someone forgets.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Parse every declaration site, not just the obvious one.&lt;/strong&gt; &lt;code&gt;requirements.yml&lt;/code&gt; roles and collections, &lt;code&gt;galaxy.yml&lt;/code&gt; dependencies, and &lt;code&gt;meta/main.yml&lt;/code&gt; role-to-role dependencies, at minimum.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Capture usage edges, not only install edges.&lt;/strong&gt; Who declared the role in a manifest is one thing. Who actually invokes it through &lt;code&gt;roles:&lt;/code&gt;, &lt;code&gt;import_role&lt;/code&gt;, and &lt;code&gt;include_role&lt;/code&gt;, and who calls into a collection through fully qualified module names in task bodies, is the thing that breaks.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reconstruct transitive role-dependency chains,&lt;/strong&gt; so a consumer two hops away through &lt;code&gt;meta&lt;/code&gt; dependencies is counted, not missed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Resolve names to the in-org repository that produces the artifact,&lt;/strong&gt; including git-URL sources, with a guard against a public role or collection that happens to share a name with one of yours.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Record the declared version constraint, or its absence.&lt;/strong&gt; Since there is no lock file, the queryable fact you can offer is who pinned you, who ranged you, and who left it floating.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Stay current by re-scanning, not by trusting run telemetry.&lt;/strong&gt; What ran last week is not what is declared today.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Make it one query,&lt;/strong&gt; because a real answer you have to assemble by hand across forty repositories is not an answer you will actually run before a release.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;How Riftmap does this&lt;/h2&gt;
&lt;p&gt;This is the gap Riftmap was built to close, so here is honestly what it does and does not do for Ansible.&lt;/p&gt;
&lt;p&gt;Riftmap scans a GitLab or GitHub organisation and parses every repository deterministically. There is no language model in the parse path; the dependency edges come from parsing the actual files, and each edge carries a rule-based confidence score. That last point matters for Ansible specifically, because some of these edges are genuinely more certain than others, and the tool says so rather than flattening everything to a confident-looking line.&lt;/p&gt;
&lt;p&gt;On the consumer side, it reads roles from &lt;code&gt;requirements.yml&lt;/code&gt;, from &lt;code&gt;meta/main.yml&lt;/code&gt; dependencies, and from play-level &lt;code&gt;roles:&lt;/code&gt;, &lt;code&gt;import_role&lt;/code&gt;, and &lt;code&gt;include_role&lt;/code&gt;, including the modern &lt;code&gt;ansible.builtin.include_role&lt;/code&gt; form and includes nested inside &lt;code&gt;block&lt;/code&gt;, &lt;code&gt;rescue&lt;/code&gt;, and &lt;code&gt;always&lt;/code&gt;. It reads collections from &lt;code&gt;requirements.yml&lt;/code&gt;, from &lt;code&gt;galaxy.yml&lt;/code&gt; dependencies, from the play-level &lt;code&gt;collections:&lt;/code&gt; keyword, and, crucially, from fully qualified module calls in task bodies, which is the usage edge that the fading &lt;code&gt;collections:&lt;/code&gt; keyword is being replaced by. Manifest declarations resolve at full confidence; a play-level &lt;code&gt;roles:&lt;/code&gt; entry or an &lt;code&gt;include_role&lt;/code&gt; name is scored a little lower, because it might point at a role bundled in the same repository rather than a shared one; a fully qualified module call is scored lower still and filtered against a denylist of public namespaces so the graph is about your collections, not &lt;code&gt;ansible.builtin&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;On the producer side, it detects which repository publishes a role, from its &lt;code&gt;meta/main.yml&lt;/code&gt; galaxy metadata, and which publishes a collection, from its &lt;code&gt;galaxy.yml&lt;/code&gt;. An edge forms when a consumer declaration resolves to a producer in the same organisation. A role or collection pulled by git URL resolves to the in-org repository that produces it, which sounds obvious and was, until recently, exactly where things slipped: a collection declared by a bare git &lt;code&gt;source:&lt;/code&gt; with no name used to be parsed and then dropped on the floor, because the resolver had no git fallback for collections. It became a permanent external edge even when the producing repository was sitting right there in the org. That is fixed now, and it is a good illustration of how well infrastructure hides its own edges, that a tool built specifically to find them still had a blind spot for the case where the dependency was declared by URL.&lt;/p&gt;
&lt;p&gt;Because the whole thing is a graph, the transitive case is handled. The impact traversal walks &lt;code&gt;meta&lt;/code&gt; dependency chains, so when you ask what breaks if you change &lt;code&gt;hardening&lt;/code&gt;, you get the playbooks two hops away through &lt;code&gt;webserver&lt;/code&gt; and &lt;code&gt;app-tier&lt;/code&gt;, which is the answer grep structurally cannot give you. Riftmap uses no graph database to do this, just PostgreSQL and a breadth-first walk in application code, which is a deliberate choice covered elsewhere, but the relevant point here is that the traversal exists.&lt;/p&gt;
&lt;p&gt;And the version-state question, the one the missing lock file makes load-bearing, is answered directly. Every consumer of a role or collection carries its constraint state: pinned to an exact version, floating on a range, riding a branch, or absent entirely, and it flags the ones already trailing the latest release. You can filter those consumers down to the unpinned ones and read off, in one number, how many repositories will move the instant you tag, with nobody deciding to. That is the &quot;who is unpinned&quot; answer the opening of this post said you needed, and it is a filter, not a research project.&lt;/p&gt;
&lt;p&gt;The same view works for a role or a collection. Here it is for an internal collection, &lt;code&gt;polaris.infrastructure&lt;/code&gt;, because a collection is where you can see both kinds of edge in one place: most of these consumers declared it in a &lt;code&gt;requirements.yml&lt;/code&gt;, and one of them is in the list only because a playbook calls one of its modules by fully qualified name.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://riftmap.dev/images/blog/ansible-collection-consumers.png&quot; alt=&quot;Riftmap artifact-consumers view for the  Ansible collection in the polaris-works test org: five consumers, four of them unpinned.  rides the  branch and is flagged Behind,  and  carry no version constraint at all,  floats on a  range, and only  pins an exact . The SOURCE column shows each edge resolved from a , and for  from a  fully qualified module call.&quot; /&gt;&lt;/p&gt;
&lt;p&gt;There is even a small, on-theme detail in how the detection works. The cheap pre-filter that decides whether a YAML file is worth parsing as a playbook used to key off the presence of &lt;code&gt;roles:&lt;/code&gt; or &lt;code&gt;collections:&lt;/code&gt; or include tokens, which meant a thoroughly modern playbook that used nothing but fully qualified module calls, no &lt;code&gt;collections:&lt;/code&gt; block at all, would have been skipped before it was ever examined. The best-written playbook was the one that hid best. The detection now keys off &lt;code&gt;hosts:&lt;/code&gt;, the actual marker of a play, so the modern repositories that do everything right are exactly the ones that no longer slip through.&lt;/p&gt;
&lt;h3&gt;What Riftmap does not claim&lt;/h3&gt;
&lt;p&gt;The honest limits are part of the product, so here they are plainly.&lt;/p&gt;
&lt;p&gt;It does not trace variables, and that is on purpose. &quot;Who consumes this variable&quot; means modelling more than twenty precedence levels with per-repository runtime semantics, and the result would be a false-positive machine that quietly undermined every edge you actually trust. This is the inverted contract from earlier, and it is the boundary of what reading source can honestly tell you. Riftmap maps the artifact graph and is straight about the fact that the variable contract sits outside it, because the alternative is pretending to an answer nobody can derive from source without lying about it.&lt;/p&gt;
&lt;p&gt;It does not resolve computed &lt;code&gt;include_role&lt;/code&gt; names, because &lt;code&gt;name: &quot;{{ something }}&quot;&lt;/code&gt; is unknowable until the run. It does not parse &lt;code&gt;import_playbook&lt;/code&gt; references, which are intra-repository file paths rather than cross-repository dependencies. It does not, today, read execution-environment definitions or follow &lt;code&gt;requirements.yml&lt;/code&gt; &lt;code&gt;include:&lt;/code&gt; chains, both of which are on the roadmap rather than in the product. And it recognises your collections by your namespaces, so a public collection you merely consume shows up as an external dependency by design, not as a repository in your graph.&lt;/p&gt;
&lt;p&gt;None of those gaps is hidden behind a confident interface. The point of a blast-radius tool is to be trusted on a Friday afternoon, and you do not earn that by overclaiming.&lt;/p&gt;
&lt;h2&gt;Distribution and blast radius are different questions&lt;/h2&gt;
&lt;p&gt;Step back, and the shape of it is clear. The very things that make Ansible feel light are the things that make its blast radius invisible. No lock file, so no record of what ran. No compile step, so no parse-time failure when a contract breaks. Variables resolved at runtime from wherever the consumer happened to set them. A role pulled from a repository nobody pinned. Ansible will cheerfully run a playbook it has never seen, against a host it has never met, resolving a role nobody locked and reading a variable nobody declared, and it will do it well. That flexibility is the entire point of the tool.&lt;/p&gt;
&lt;p&gt;It is also why &quot;what breaks if I change this role&quot; has no answer in the box. The box was designed not to keep that answer.&lt;/p&gt;
&lt;p&gt;Galaxy and Automation Hub tell you where a role came from and how often it was pulled. That is distribution, and it is a real and useful thing to know. It is a different question from what breaks when you change it. Distribution is about the artifact. Blast radius is about the relationships between repositories, which is the one place the artifact cannot see and the registry was never asked to look. You find every consumer by reading those relationships across the whole organisation and resolving them into a graph. Parsed, not inferred.&lt;/p&gt;
&lt;p&gt;Your &lt;code&gt;hardening&lt;/code&gt; fix is still sitting there, untagged. Now you can see who gets it, who pinned you and who did not, and which playbooks two hops away you were about to surprise. Tag it on purpose.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This post is part of the &lt;a href=&quot;https://riftmap.dev/blog/series/find-every-consumer/&quot;&gt;Find Every Consumer series&lt;/a&gt;, which works through the same question across &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-terraform-module/&quot;&gt;Terraform modules&lt;/a&gt;, &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-docker-base-image/&quot;&gt;Docker base images&lt;/a&gt;, &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-github-actions-workflow/&quot;&gt;GitHub Actions&lt;/a&gt;, &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-helm-chart/&quot;&gt;Helm charts&lt;/a&gt;, &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-go-module/&quot;&gt;Go modules&lt;/a&gt;, &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-gitlab-ci-template/&quot;&gt;GitLab CI templates&lt;/a&gt;, &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-internal-npm-package/&quot;&gt;npm packages&lt;/a&gt;, and &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-internal-python-package/&quot;&gt;Python packages&lt;/a&gt;, one ecosystem at a time.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Riftmap maps cross-repository dependencies across a GitLab or GitHub organisation and answers the change-impact question directly: if I change this, what else breaks? It parses the relationships between your repositories rather than asking you to model them, with no per-repo config. &lt;a href=&quot;https://riftmap.dev/&quot;&gt;Map your org&lt;/a&gt;, or &lt;a href=&quot;https://app.cal.eu/riftmap/map-my-org&quot;&gt;book a walkthrough&lt;/a&gt; and we will map it with you.&lt;/em&gt;&lt;/p&gt;
</content:encoded><category>ansible</category><category>ansible-roles</category><category>ansible-collections</category><category>blast-radius</category><category>dependency-mapping</category><category>platform-engineering</category><category>devops</category><author>Daniel Westgaard</author></item><item><title>Overmind shows you the blast radius in your running cloud. It can&apos;t show you the repos that were building on what you changed.</title><link>https://riftmap.dev/blog/riftmap-vs-overmind/</link><guid isPermaLink="true">https://riftmap.dev/blog/riftmap-vs-overmind/</guid><description>Overmind reads your live AWS, GCP and Kubernetes state to tell you whether a Terraform apply is safe. Riftmap parses source to tell you which other repositories consume what you are changing. Both call it &quot;blast radius&quot;. They are not the same radius.</description><pubDate>Fri, 19 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;Overmind reads your live AWS, GCP and Kubernetes state to tell you whether a Terraform apply is safe. Riftmap parses source to tell you which other repositories consume what you are changing. Both call it &quot;blast radius&quot;. They are not the same radius.&lt;/em&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;The line that anchors &lt;a href=&quot;https://overmind.tech/&quot;&gt;Overmind&apos;s homepage&lt;/a&gt; is a good one. The Terraform plan shows what is in the plan, it says, but &quot;the real impact lives in the running infrastructure&quot;. It is the kind of sentence I wish more infrastructure tools were honest enough to write. A &lt;code&gt;terraform plan&lt;/code&gt; shows you the diff. It does not show you what that diff does once it lands in an account full of resources that depend on each other in ways the plan never spells out. Overmind built a real product on closing exactly that gap, and I have spent the last few days in their docs, their CLI, and their own writing about it, because when a tool reaches for the words your product is built on, the responsible thing is to understand it properly before you say anything about it.&lt;/p&gt;
&lt;p&gt;Here is where I landed. &quot;Terraform blast radius&quot; is not one question. It is at least three, and they are answered by three different kinds of tool that are easy to mistake for one another. Overmind answers one of them, and answers it well. It is not the one a platform team in a 50-to-300-repo polyrepo org runs into most often, and it is not the one Riftmap is built for. The two tools barely overlap once the three questions are clear. So this post is mostly about drawing that line precisely, and being fair to Overmind while I do it. It is the same exercise I ran recently with &lt;a href=&quot;https://riftmap.dev/blog/gitlab-orbit-and-the-artifact-layer/&quot;&gt;GitLab Orbit&lt;/a&gt;, on a different axis.&lt;/p&gt;
&lt;h2&gt;What Overmind actually does&lt;/h2&gt;
&lt;p&gt;I want to be generous here, because Overmind earns it, and because what it is good at and what Riftmap is for sit on different axes.&lt;/p&gt;
&lt;p&gt;Overmind plugs into the Terraform workflow at plan time. You run &lt;code&gt;overmind terraform plan&lt;/code&gt; locally, or you wire &lt;code&gt;overmind changes submit-plan&lt;/code&gt; into CI so it fires on every pull request. From there it does the thing most tools with &quot;blast radius&quot; in the name do not: it reaches into your &lt;em&gt;running&lt;/em&gt; cloud. Using read-only access to your AWS, GCP or Kubernetes, it queries the live account through the provider APIs, builds a dependency map from the resources that actually exist, and works out which of them your plan could disturb. The depth is real, with &lt;a href=&quot;https://github.com/overmindtech/cli&quot;&gt;over 120 AWS resource types and all of Kubernetes&lt;/a&gt; supported. Crucially, that map includes resources Terraform does not manage at all. Something created by hand in the console, or by CloudFormation, or by another tool entirely, still shows up, because the map comes from the cloud and not from your state file. On top of it Overmind posts a machine-generated list of risks straight into the PR, and &lt;code&gt;overmind terraform apply&lt;/code&gt; lets it snapshot before and after a deploy so you can confirm nothing reacted in a way you did not expect. There is also an LLM-driven investigator, &lt;code&gt;overmind explore&lt;/code&gt;, for poking at infrastructure interactively. And to its credit, &lt;a href=&quot;https://docs.overmind.tech/cli/overview&quot;&gt;the credentials never leave your environment&lt;/a&gt;; the CLI talks to the cloud, not the other way around.&lt;/p&gt;
&lt;p&gt;The example Overmind leads with on its own site is the right kind of example, so I will describe it rather than invent a worse one. A change adds a memory limit to a Kubernetes deployment, and it looks safe, because the pods normally sit comfortably under the new ceiling. The limit only bites during a traffic spike, when the JVM heap expands for garbage collection, the pods cross the ceiling, get OOMKilled, and the failures cascade as the load balancer keeps routing traffic to pods that are themselves restarting. That is a genuinely hard thing to catch in review. It is invisible in the diff, invisible in the plan, and it depends on how the system behaves under load in production. This is Overmind&apos;s home turf, and on it Overmind is doing something neither a config visualiser nor a source parser can do. If the question in front of you is &quot;is this apply safe against my live account&quot;, Overmind is a real answer.&lt;/p&gt;
&lt;p&gt;So let me be precise about the shape of what Overmind sees. It reasons about cloud &lt;em&gt;resources&lt;/em&gt;, in the account a plan targets, at the moment that plan exists, including drift and live runtime relationships. Hold that shape in mind, because the line falls exactly along its edges.&lt;/p&gt;
&lt;h2&gt;&quot;Terraform blast radius&quot; is three questions wearing one phrase&lt;/h2&gt;
&lt;p&gt;The reason two engineers can both say &quot;we use a Terraform blast radius tool&quot; and mean completely different things is that the phrase has quietly absorbed three separate questions. They live at different layers. They need different machinery. And most arguments about &quot;the blast radius tool&quot; are really two people standing at different layers talking past each other.&lt;/p&gt;
&lt;h3&gt;One: the resource graph inside a single config&lt;/h3&gt;
&lt;p&gt;Ask &quot;what is the dependency graph of this Terraform configuration, and in what order will things change&quot;, and you want the in-config graph. This is the oldest sense of the term, and it is the one the open-source tool literally named &lt;a href=&quot;https://github.com/28mm/blast-radius&quot;&gt;Blast Radius&lt;/a&gt; built: parse the HCL in a working directory, render the resource DAG with Graphviz, let you see what depends on what before you apply. Inframap, Rover and Terraform Visual live here too. It is good for understanding and documenting one configuration, and it is bounded to one configuration. It tells you nothing about the running cloud and nothing about other repositories. It is the resource ordering inside the directory in front of you, and no more.&lt;/p&gt;
&lt;h3&gt;Two: the live-cloud graph at apply time&lt;/h3&gt;
&lt;p&gt;Ask &quot;given this plan, what in my &lt;em&gt;running&lt;/em&gt; infrastructure could be affected, and is that safe&quot;, and you want the live-cloud graph. This is Overmind. The graph is not derived from the config, it is discovered from the cloud, which is precisely why it can include unmanaged resources and runtime relationships the config never mentions. It is bounded to one account&apos;s live state, and to the moment a plan exists. Overmind itself drew the first half of this line years ago, when it argued that &lt;a href=&quot;https://overmind.tech/blog/difference-terraform-plan-and-overmind-blast-radius&quot;&gt;blast radius is more than another plan-visualisation tool&lt;/a&gt;, and it was right to. The live-cloud graph is a genuine step beyond the in-config graph.&lt;/p&gt;
&lt;h3&gt;Three: the cross-repo graph across the whole org&lt;/h3&gt;
&lt;p&gt;Ask &quot;which &lt;em&gt;other repositories&lt;/em&gt; across my organisation consume the thing I am about to change, and at which version&quot;, and you want the &lt;a href=&quot;https://riftmap.dev/what-is-an-artifact-dependency-graph/&quot;&gt;cross-repo artifact graph&lt;/a&gt;. This is Riftmap. Its nodes are the things repositories share: a Terraform module, a base image, a Helm chart, a reusable CI template. Its edges are the references that bind them across repos: a Terraform &lt;code&gt;source&lt;/code&gt; block, a &lt;code&gt;terraform_remote_state&lt;/code&gt; lookup, a Dockerfile &lt;code&gt;FROM&lt;/code&gt; line, a Helm chart &lt;code&gt;dependencies&lt;/code&gt; entry, a GitLab CI &lt;code&gt;include:project&lt;/code&gt;. It is built by parsing source across every repository in a GitLab or GitHub org, and it is bounded by nothing except the org itself. It does not look at the cloud and it does not need a plan to exist. It answers the question before you make the change.&lt;/p&gt;
&lt;p&gt;Overmind did a real service by separating question two from question one. The thing worth noticing is that question three is a different axis again, and nothing about answering question two gets you any closer to answering it.&lt;/p&gt;
&lt;h2&gt;Where the edge that bites actually lives&lt;/h2&gt;
&lt;p&gt;Picture the change that actually causes the bad afternoon. You take a shared Terraform module from v3 to v4 and rename a variable on the way. The module is fine. Your plan is fine. The thing that breaks is two repos away, in a service owned by a team that has never read your module&apos;s changelog, whose Terraform pins &lt;code&gt;source = &quot;git::https://gitlab.example.com/platform/modules.git//network?ref=v3&quot;&lt;/code&gt; and will fail to plan the next time anyone touches it, or the moment &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-terraform-module/&quot;&gt;Renovate&lt;/a&gt; bumps the ref for them.&lt;/p&gt;
&lt;p&gt;Walk that through Overmind&apos;s machinery and you can see why it cannot be the tool that catches it. Overmind runs on a plan, in the repo where the plan exists. The plan that exists is &lt;em&gt;yours&lt;/em&gt;, in the module repo or wherever you made the edit. The consuming repo has not planned anything. There is no live-cloud signal there yet, because nobody has applied the breaking change, and the failure you actually care about is the one that has not happened. Even if that team also runs Overmind, Overmind will not connect your module edit to their future plan, because it does not model the dependency that links them. That dependency was never a cloud resource. It was a &lt;code&gt;?ref=v3&lt;/code&gt; in a manifest, in a repository Overmind had no reason to look at.&lt;/p&gt;
&lt;p&gt;There is a fair caveat here, and I would rather state it than skate past it. Within a single shared account, Overmind queries the live cloud, so a plan in one repo can surface a resource that another repo also manages, because both touch the same account. In that narrow sense it can cross an ownership boundary at the resource level. But that is one account, at apply time, between resources that already exist. It is not the cross-repo &lt;em&gt;source&lt;/em&gt; edge. It is not the consuming repos that have not applied anything yet. And it does not reach across accounts or across platforms. A module pinned at &lt;code&gt;?ref=v3&lt;/code&gt; in a GitHub repo, while the module is published from a GitLab repo, is an edge no live-cloud tool can draw, because it does not exist in any cloud. It only ever existed in the manifest.&lt;/p&gt;
&lt;p&gt;Then there is the entire artifact layer that has no cloud footprint at all. A shared Go module other services &lt;code&gt;require&lt;/code&gt;. An internal npm package. A Python library half your repos import. A reusable GitLab CI template that &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-gitlab-ci-template/&quot;&gt;thirty pipelines &lt;code&gt;include&lt;/code&gt;&lt;/a&gt;. None of these is an AWS resource, so none of them is visible to a tool whose graph is built from cloud state. They are visible to a tool that parses the manifest edge, whether or not the artifact ever becomes a running resource. That is the difference between discovering the graph from the cloud and parsing it from the source. Parsed, not inferred. And parsed across everything, not only the things that happen to run in a cloud Overmind can read.&lt;/p&gt;
&lt;h2&gt;Two access models, two moments&lt;/h2&gt;
&lt;p&gt;The split shows up cleanly in two practical places, beyond the layer question itself.&lt;/p&gt;
&lt;p&gt;The first is what each tool has to be trusted with. Overmind needs read-only access to your &lt;em&gt;cloud&lt;/em&gt;, because that is where its graph comes from. The credentials stay in your environment, which is the right design, but the access still has to exist and be approved. Riftmap needs read-only access to your &lt;em&gt;Git&lt;/em&gt;, one token, because that is where its graph comes from. These are different surfaces to reason about, and for a security-conscious or regulated team the question of which systems a tool touches is rarely an afterthought. Neither model is universally better. They are answers to different questions, and they go through different approvals.&lt;/p&gt;
&lt;p&gt;The second is &lt;em&gt;when&lt;/em&gt; each tool can answer. Overmind answers when a plan exists, which in practice means in CI, on a pull request, at apply time. Riftmap answers from a standing graph of the whole org that is already there before you write the change. You can ask it the moment you are &lt;em&gt;considering&lt;/em&gt; a breaking change, before there is a branch, let alone a plan. An AI coding agent about to edit a shared module can ask it before it opens the pull request at all, which is the whole point of treating the graph as &lt;a href=&quot;https://riftmap.dev/ai-agent-blast-radius/&quot;&gt;runtime infrastructure for agents&lt;/a&gt;. Apply time is a good moment to check whether a change is safe. It is a late moment to discover who you needed to warn.&lt;/p&gt;
&lt;p&gt;It is worth being precise about determinism too, because it is easy to be unfair on this point. Overmind&apos;s dependency graph is not guessed, it is read from real cloud APIs, which is solid. The part that is a judgement rather than a guarantee is the risk list, which is generated automatically from patterns and analysis of the change and reads as advice. Riftmap&apos;s graph is parsed deterministically from manifests, and the consumer set it returns is an enumeration: these repositories reference this artifact at these versions, full stop, not a confidence score. Both tools are honest about which of their outputs is a fact and which is a judgement. They simply draw that line in different places, because they are doing different jobs. The deterministic case for parsing rather than inferring is something I have made &lt;a href=&quot;https://riftmap.dev/blog/symbol-graphs-and-artifact-graphs/&quot;&gt;at more length elsewhere&lt;/a&gt;, and it applies to the graph, not to Overmind&apos;s risk narrative, which was never trying to be deterministic.&lt;/p&gt;
&lt;h2&gt;They stack&lt;/h2&gt;
&lt;p&gt;I have come to think of Overmind less as a competitor than as the tool that owns the layer directly beneath the one Riftmap owns. The honest version of &quot;when would I use each&quot; is not a contest, it is a sequence.&lt;/p&gt;
&lt;p&gt;You are about to bump a shared module. Riftmap answers the first question: which repositories consume it, who owns them, what versions they pin, so you know whose pipeline you are about to touch and who needs to be in the loop before you merge. For the repos that take the change and apply it, Overmind answers the second question: against their live account, is the resulting plan safe, and does anything in the running infrastructure react badly. One is the blast radius across your source. The other is the blast radius inside a cloud. They are not the same radius, and they do not fire at the same moment.&lt;/p&gt;
&lt;p&gt;I should be equally clear about where Riftmap stops, because a comparison that only lists the other tool&apos;s limits is not worth reading. Riftmap will never catch the OOMKilled-under-load example from earlier. That failure lives in runtime behaviour, in how a JVM uses memory during a traffic spike, and Riftmap does not watch your runtime. It does not know your cloud. It does not see drift. If your worst changes are the ones that look safe in source and only misbehave once they are live, Overmind is reaching a layer Riftmap deliberately does not. The two tools are not redundant. Each is blind exactly where the other sees.&lt;/p&gt;
&lt;p&gt;The useful coincidence is that both expose their graphs to machines. Overmind has a CLI and posts into the PR. Riftmap ships an HTTP API and an MCP server with every deployment. An agent reviewing an infrastructure change could call both: Riftmap for the repositories downstream of the artifact it is editing, Overmind for the runtime safety of the plan it produces. This was &lt;a href=&quot;https://riftmap.dev/blog/ai-coding-agents-need-cross-repo-context/&quot;&gt;always the shape of it&lt;/a&gt;. The agent is a consumer of whichever graph answers its question, and on a real infrastructure change it has more than one question.&lt;/p&gt;
&lt;h2&gt;So which one is &quot;Terraform blast radius&quot;&lt;/h2&gt;
&lt;p&gt;Both are, and that is the whole point. A &lt;code&gt;terraform plan&lt;/code&gt; tells you what you are touching. Overmind tells you what that touches in the account you are about to apply to, drift and runtime included. Riftmap tells you who else was building on it, across every repository and every platform, before the plan exists. Three questions, three tools, one overloaded phrase.&lt;/p&gt;
&lt;p&gt;If your pain is &quot;the apply looked safe and production fell over anyway&quot;, that is the live-cloud question, and Overmind is built for it. If your pain is &quot;we changed a shared module and three teams found out when their pipelines went red&quot;, that is the cross-repo question, and no amount of live-cloud analysis will answer it, because the edge that broke was never in the cloud. It was a pinned &lt;code&gt;source&lt;/code&gt; block in a repo nobody thought to check. Same two words. Different blast, different radius.&lt;/p&gt;
&lt;h2&gt;A few questions, answered directly&lt;/h2&gt;
&lt;h3&gt;Does Overmind show cross-repo Terraform blast radius?&lt;/h3&gt;
&lt;p&gt;No, and the reason is structural rather than a missing feature. Overmind builds its dependency graph from your live cloud state at plan time, scoped to the account a Terraform plan targets. The cross-repo edge that breaks when you change a shared module is a &lt;code&gt;source&lt;/code&gt; block pinned to a version in a &lt;em&gt;different&lt;/em&gt; repository that has not run a plan, often on a different platform or in a different account entirely. That edge exists only in source, so a tool whose graph comes from the cloud cannot draw it. Overmind answers blast radius inside a running account. Cross-repo consumer questions need a graph parsed from source across the whole org.&lt;/p&gt;
&lt;h3&gt;What is the difference between Overmind and Riftmap?&lt;/h3&gt;
&lt;p&gt;They answer different questions about a Terraform change. Overmind reads your live AWS, GCP or Kubernetes state at plan time and tells you what in the &lt;em&gt;running&lt;/em&gt; infrastructure a plan could affect, including drift and resources Terraform does not manage, plus a machine-generated risk assessment in the PR. Riftmap parses source across your entire GitLab or GitHub org and tells you which &lt;em&gt;other repositories&lt;/em&gt; consume the module, image, chart or CI template you are changing, and at which versions, before a plan exists. Live-cloud and apply-time versus source-derived and org-wide. They are complementary, not substitutes.&lt;/p&gt;
&lt;h3&gt;Does &lt;code&gt;terraform plan&lt;/code&gt; show blast radius?&lt;/h3&gt;
&lt;p&gt;Not in any of the three senses people usually mean. &lt;code&gt;terraform plan&lt;/code&gt; shows the diff between your configuration and your state: the resources this configuration will add, change or destroy. It does not render the resource ordering as a graph (the in-config tools do that), it does not show what in your running cloud could be affected including unmanaged resources (Overmind does that), and it does not show which other repositories consume what you are changing (Riftmap does that). The plan is the input to blast-radius analysis, not the analysis.&lt;/p&gt;
&lt;h3&gt;Can Overmind and Riftmap be used together?&lt;/h3&gt;
&lt;p&gt;Yes, and on a real infrastructure change they answer adjacent questions in sequence. Before you ship a breaking change to a shared artifact, Riftmap tells you which repositories and teams consume it, so you know whom to coordinate with and what to gate the merge on. For the repos that apply the change, Overmind tells you whether the resulting plan is safe against their live account. One maps the blast radius across your source. The other maps it inside your cloud. Both expose their graphs over an API or MCP, so an AI coding agent can call either or both during a review.&lt;/p&gt;
</content:encoded><category>overmind</category><category>terraform</category><category>blast-radius</category><category>cross-repo</category><category>dependency-graph</category><author>Daniel Westgaard</author></item><item><title>You deprecated the internal library. The repos still using it never saw the warning.</title><link>https://riftmap.dev/blog/deprecate-internal-library-find-consumers/</link><guid isPermaLink="true">https://riftmap.dev/blog/deprecate-internal-library-find-consumers/</guid><description>A deprecation warning only reaches consumers who recompile, and the repos that pinned the old version go quiet. Deprecating an internal library is a census problem.</description><pubDate>Thu, 18 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;Every deprecation guide tells you how to signal that a module is going away. None of them tell you who to signal. Deprecating an internal library is a consumer-census problem, and the census is the part nobody hands you.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Someone on your team did everything the guides tell you to do.&lt;/p&gt;
&lt;p&gt;They had built an internal library years ago. Call it &lt;code&gt;@acme/auth&lt;/code&gt;, a shared package half the org imports for token handling. It had outlived its design, a cleaner replacement existed, and it was time to pull it. So they did it by the book. They added the &lt;code&gt;@deprecated&lt;/code&gt; tag to the exported functions. They cut a major version so the removal would land on a &lt;a href=&quot;https://semver.org/&quot;&gt;semver&lt;/a&gt; boundary nobody could miss. They wrote a migration guide with before-and-after snippets. They posted in the engineering announcements channel, twice, a month apart. They set a sunset date a full quarter out. And on the date, they removed it and shipped the major.&lt;/p&gt;
&lt;p&gt;For a fortnight, nothing. Then a payments service fell over in staging on a Tuesday afternoon, and the on-call engineer spent two hours tracing a build failure back to a package that, as far as anyone in the channel knew, no longer had any consumers. The service had pinned the old major eighteen months earlier. It had never seen the editor warning, because nobody was actively developing it. It had never read the announcement, because the team that owned it had reorganised and the new owners were not in the channel when the message went out. It had simply kept building against the version it was locked to, quietly, until the version went away.&lt;/p&gt;
&lt;p&gt;The part I keep coming back to is that this deprecation was not done badly. It was done well. Every step in the standard playbook was executed, and the playbook still let a consumer break. Because the playbook is about one half of the problem, and the half that actually bit was the other one.&lt;/p&gt;
&lt;h2&gt;The deprecation playbook is all signal&lt;/h2&gt;
&lt;p&gt;I want to be fair to the playbook, because most of it is correct and hard-won, and the people who wrote it down were solving a real problem.&lt;/p&gt;
&lt;p&gt;The advice converges across ecosystems. In JavaScript and TypeScript you add a &lt;a href=&quot;https://dev.to/dgreene1/how-to-properly-deprecate-3027&quot;&gt;&lt;code&gt;@deprecated&lt;/code&gt; JSDoc tag&lt;/a&gt;, you reach for &lt;code&gt;console.warn&lt;/code&gt; in the function body, and you remove the symbol only on a major version because that is the boundary semver tells consumers to expect breakage on. In Python you raise a &lt;code&gt;DeprecationWarning&lt;/code&gt;, or you wrap the thing in one of the &lt;a href=&quot;https://pypi.org/project/deprecation-factory/&quot;&gt;deprecation decorator libraries&lt;/a&gt; that write the boilerplate for you. In Go you put a &lt;code&gt;// Deprecated:&lt;/code&gt; line in the doc comment. Across all of them you write a migration guide, you announce it where your consumers can see it, and you give people a sunset window rather than yanking the thing from under them. The &lt;a href=&quot;https://docs.npmjs.com/deprecating-and-undeprecating-packages-or-package-versions/&quot;&gt;npm docs&lt;/a&gt; put the underlying ethic plainly: deprecate rather than unpublish, because unpublishing pulls the package with no warning to anyone who relied on it.&lt;/p&gt;
&lt;p&gt;This is genuinely good practice. Do all of it. None of what follows is an argument against signalling a deprecation properly.&lt;/p&gt;
&lt;p&gt;But read back over that list and notice what every item has in common. The &lt;code&gt;@deprecated&lt;/code&gt; tag annotates a symbol your consumers import. The migration guide is written for your consumers. The announcement is addressed to your consumers. The sunset date is a promise to your consumers. Every single step takes the set of consumers as an input it already has. The playbook starts at the point where you know who they are.&lt;/p&gt;
&lt;h2&gt;Every step assumes you already have the list&lt;/h2&gt;
&lt;p&gt;Here is the quiet conflation. We talk about deprecation as one task, and it is two.&lt;/p&gt;
&lt;p&gt;One task is signalling: telling the consumers of a thing that it is going away, on a timeline, with a path off it. The other is the census: knowing who the consumers actually are. The playbook is entirely about the first and silently assumes the second is already done. And for an internal library spread across a polyrepo org, the second is the hard one. It is the one that decides whether the deprecation is safe, and it is the one nothing in the playbook does for you.&lt;/p&gt;
&lt;p&gt;You cannot signal a consumer you cannot name. You cannot set a credible sunset date without knowing how many repos have to move and who owns them. You cannot estimate the migration effort, or stage it sensibly, or tell your own management how risky the removal is, until you have enumerated every repo that imports the thing and the version each one is pinned to. The annotation, the guide, the announcement: all of it is downstream of a list you were assumed to already hold. In a single app you do hold it, because the consumers are in the same repo and the compiler finds them. Across an organisation you do not, because the consumers are in other people&apos;s repos and nothing walks all of them for you.&lt;/p&gt;
&lt;h2&gt;The signal is conditional, and often silent&lt;/h2&gt;
&lt;p&gt;It is tempting to think the signal itself solves the census. Surely if you mark the thing deprecated, the consumers find out. They do not, and it is worth being precise about why, because the failure is mechanical, not careless.&lt;/p&gt;
&lt;p&gt;Take the editor warning first. A &lt;code&gt;@deprecated&lt;/code&gt; JSDoc tag only surfaces for a developer whose tooling is configured to flag it, through a rule like &lt;code&gt;eslint-plugin-import&lt;/code&gt;&apos;s &lt;code&gt;no-deprecated&lt;/code&gt;, and only when someone actually opens that repo and lints or rebuilds it. A service that is locked to the old version, building green in CI, with nobody in their editor that quarter, gets no signal at all. The annotation is sitting in a version of the package that repo is not even pulling.&lt;/p&gt;
&lt;p&gt;Python is sharper still, and the default trips almost everyone. Since Python 3.2, &lt;code&gt;DeprecationWarning&lt;/code&gt; is &lt;a href=&quot;https://docs.python.org/3/library/warnings.html&quot;&gt;ignored by default for every module except &lt;code&gt;__main__&lt;/code&gt;&lt;/a&gt;. &lt;a href=&quot;https://peps.python.org/pep-0565/&quot;&gt;PEP 565&lt;/a&gt; re-enabled it in &lt;code&gt;__main__&lt;/code&gt; in 3.7, but a deprecated function imported from a library runs in the importing module, not in &lt;code&gt;__main__&lt;/code&gt;, so the warning lands in a filter that drops it on the floor. A service can import your deprecated function, run it in production every day, and never emit a visible warning, unless it happens to run its test suite with warnings surfaced or someone has set &lt;code&gt;PYTHONWARNINGS&lt;/code&gt;. The signal fires into silence.&lt;/p&gt;
&lt;p&gt;npm has the same gap from the other direction. The deprecation message shows up as &lt;code&gt;npm WARN deprecated&lt;/code&gt; during install and resolution. A repo with a committed lockfile that is not reinstalling does not resolve anything, so it does not see the warning. The message reaches new installs, not the repos that locked your old version a year ago and have not run a clean install since. For an internal package wired in through a workspace protocol or a private registry, the registry-level deprecation may not reach the consumer at all.&lt;/p&gt;
&lt;p&gt;And then the announcement, which is the one humans trust most and should trust least. It reaches the people who read the channel and already understand that they are affected. The team that pinned your library eighteen months ago and forgot is, by definition, the team that does not know it needs to be reading. The announcement is a broadcast; the consumers you are most worried about are the ones not tuned in.&lt;/p&gt;
&lt;p&gt;So even in the best case, where a consumer is actively developed and tested, the signal is conditional on tooling plus a rebuild. In the common case, where a consumer pinned a version and went quiet, the signal reaches no one. And the quiet consumers are exactly the ones that break, because quiet is what &quot;we pinned it and stopped thinking about it&quot; looks like from the outside.&lt;/p&gt;
&lt;h2&gt;So you go looking for the list&lt;/h2&gt;
&lt;p&gt;Once it is clear the signal will not assemble the census for you, you go and try to build it by hand. Every route you reach for gets you partway and stops at the same wall.&lt;/p&gt;
&lt;p&gt;The internal package registry feels like the obvious source. If you run a private registry that records pulls per consumer, maybe you can read the list off download stats. In practice that data is rarely surfaced cleanly, CI caches inflate and distort the counts, and any repo that vendored the code, pinned a git URL, or wired the dependency through a workspace never shows up as a registry pull at all. Download numbers tell you something about traffic. They do not give you a clean set of repositories with owners.&lt;/p&gt;
&lt;p&gt;The wiki page is worse, because it looks authoritative and is usually wrong. Someone wrote &quot;consumers: X, Y, Z&quot; once, eighteen months and one reorg ago, and a fourth team added itself the week after and never edited the page. This is the same decay that makes platform teams quietly abandon their &lt;a href=&quot;https://riftmap.dev/blog/the-catalog-maintenance-trap/&quot;&gt;service catalogues&lt;/a&gt;. A hand-maintained list of consumers is only ever as accurate as the last person who remembered to update it, and &quot;remember to update the consumers page&quot; is precisely the discipline that does not survive contact with a busy quarter.&lt;/p&gt;
&lt;p&gt;Code search is closer, and for one ecosystem across a handful of repos, grep genuinely works. Across an org it becomes a string of false starts. The import can be aliased, re-exported from a barrel file, pulled in transitively through another internal package that depends on yours, or referenced by a version range rather than a literal you can match on. Walking that down by hand, repo by repo, is the work the &lt;a href=&quot;https://riftmap.dev/blog/series/find-every-consumer/&quot;&gt;Find Every Consumer series&lt;/a&gt; exists to document, one ecosystem at a time, and the recurring lesson of that series is that grep is where you start and not where you finish.&lt;/p&gt;
&lt;p&gt;GitHub&apos;s dependency graph looks like it should just answer this. It has a Dependents view built for exactly this question. Except &lt;a href=&quot;https://docs.github.com/en/code-security/how-tos/secure-your-supply-chain/secure-your-dependencies/exploring-the-dependencies-of-a-repository&quot;&gt;GitHub only computes dependents for public repositories&lt;/a&gt;. Your internal library lives in a private org, which is the one case the feature does not cover.&lt;/p&gt;
&lt;p&gt;And the reverse-dependency tools that do exist are scoped to the wrong thing. &lt;code&gt;apt-cache rdepends&lt;/code&gt; and &lt;code&gt;repoquery --whatrequires&lt;/code&gt; answer reverse-dependency questions for the packages installed on one machine, not for the repos in your org. &lt;code&gt;jdeprscan&lt;/code&gt; and the eslint deprecation rules scan a single repo that already depends and recompiles, to find uses of deprecated APIs inside it. They are good tools. None of them answers &quot;which repositories across my organisation consume this library,&quot; because that question is not a fact about any one repo. It is a fact about the relationship between your library and every other repo, and not one of these tools is looking at all the repos at once.&lt;/p&gt;
&lt;h2&gt;Even automated migration needs the list first&lt;/h2&gt;
&lt;p&gt;The sophisticated end of this is real and deserves credit before I draw the line. There is a whole tier of tooling built to perform the migration across many repositories once you know where it has to land. &lt;a href=&quot;https://docs.renovatebot.com/&quot;&gt;Renovate&lt;/a&gt; and Dependabot open the version bump in each repo they are configured on. &lt;a href=&quot;https://docs.openrewrite.org/&quot;&gt;OpenRewrite&lt;/a&gt; applies structured, type-aware code transformations across a codebase. Allegro combined Dependabot and OpenRewrite into an in-house system to &lt;a href=&quot;https://www.infoq.com/news/2025/05/allegro-code-migrations-scale&quot;&gt;run migrations across more than two thousand microservices&lt;/a&gt;, precisely because doing it by hand when a company-wide library makes a breaking change is brutal and error-prone.&lt;/p&gt;
&lt;p&gt;That is impressive engineering, and it is also the mechanical-edit layer, not the census. Renovate and Dependabot keep dependencies current and tell you nothing about who consumes what; they give you the bump, not the &lt;a href=&quot;https://riftmap.dev/blog/ai-doesnt-understand-blast-radius/&quot;&gt;blast radius&lt;/a&gt;. Every one of these systems operates on the repos it is pointed at, and so it presupposes the list. The repos it was never pointed at, the ones that pinned the old version and dropped out of the automation, are the same quiet consumers from the scene at the top of this post. Automating the edit makes a known migration faster. It does nothing for the consumer you did not know you had, because that consumer was never in the set the automation was handed.&lt;/p&gt;
&lt;h2&gt;The list is a graph query, not a search&lt;/h2&gt;
&lt;p&gt;Strip the problem back and the thing you actually need before you deprecate anything is small and specific. Every repository that declares a dependency on this library, directly or transitively. The version each one pins, so you can separate the repos already off it from the ones stranded on the old major. The team that owns each repo, so you know who to route the migration to. And the order, so you migrate the internal package that re-exports your library before the leaf services that pull it in through that package.&lt;/p&gt;
&lt;p&gt;That is a query against a graph of dependency edges, and the only honest way to build the graph is to parse it. Read the &lt;code&gt;dependencies&lt;/code&gt; in every &lt;code&gt;package.json&lt;/code&gt;, the &lt;code&gt;require&lt;/code&gt; in every &lt;code&gt;go.mod&lt;/code&gt;, the import in every &lt;code&gt;pyproject.toml&lt;/code&gt; and requirements file, the &lt;code&gt;source&lt;/code&gt; in every Terraform module block, the chart references in every Helm values file, the &lt;code&gt;include&lt;/code&gt; in every GitLab CI config. Resolve the version each one pins. Normalise the aliases, the re-exports, and the workspace protocols back to the package they point at. Connect the edges. Parsed, not inferred. Not guessed from names that look similar, not reconstructed from a wiki someone edited last year, not pieced together from a Slack thread. Read from the manifests that already declare the dependency, because those files are the source of truth and they are also exactly where the migration is going to land.&lt;/p&gt;
&lt;p&gt;The ecosystem changes the syntax, not the shape of the problem. The mechanics of doing this properly differ enough per ecosystem that they each deserve their own walkthrough, which is why the Find Every Consumer series takes them one at a time: &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-internal-npm-package/&quot;&gt;internal npm packages&lt;/a&gt;, &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-internal-python-package/&quot;&gt;internal Python packages&lt;/a&gt;, &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-go-module/&quot;&gt;Go modules&lt;/a&gt;, &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-terraform-module/&quot;&gt;Terraform modules&lt;/a&gt;, and &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-helm-chart/&quot;&gt;Helm charts&lt;/a&gt; each fight back in their own way. But the destination is the same in every case. A set of repositories, with versions and owners, derived from source.&lt;/p&gt;
&lt;h2&gt;The debt you keep because you cannot see the list&lt;/h2&gt;
&lt;p&gt;Here is the part that makes this more than an incident story, and it is the reason the census matters even when nothing is actively breaking.&lt;/p&gt;
&lt;p&gt;When you cannot enumerate consumers cheaply, the rational response is to never remove anything. Think about what the alternative asks of you. To delete the old auth library, you have to be willing to say &quot;nothing depends on this any more,&quot; and if you are wrong, a payments service falls over and it is your name on the change. &quot;I am fairly sure nothing uses this&quot; is not a sentence anyone wants to be holding when the pager goes off. So the safe move, the locally rational move, is to leave it. Leave the deprecated library in place. Leave the forked Terraform module nobody has touched in two years. Leave the old endpoint running just in case. Mark it deprecated, maybe, and never actually pull it.&lt;/p&gt;
&lt;p&gt;This is how internal platforms accrete years of undead code. Not because nobody wants to clean it up. Because removal is unprovable-safe, and the cost of being wrong is paid in production at an inconvenient hour. The module stays, the maintenance burden stays, the mental overhead of &quot;is this still load-bearing&quot; stays, and the next engineer inherits all of it plus the same fear that kept the last one from acting.&lt;/p&gt;
&lt;p&gt;A cheap, trustworthy consumer census changes the calculus completely. When you can answer &quot;what depends on this&quot; in one query, with versions and owners, removal stops being a gamble and becomes a decision. You see the four repos still on the old major, you route the migration, you watch the number go to zero, and then you delete with confidence instead of hope. The census is not only the thing you reach for in an incident. It is the thing that lets a codebase shrink at all. Without it, the only safe direction is accretion.&lt;/p&gt;
&lt;h2&gt;Two halves of deprecation&lt;/h2&gt;
&lt;p&gt;So, stripped down. Deprecation is two jobs wearing one word, and the departure of that payments service showed which one the playbook forgot.&lt;/p&gt;
&lt;p&gt;One job is signalling: tell your consumers the thing is going away, on a timeline, with a path off it. The playbook is all about this half, and it is good at it. The &lt;code&gt;@deprecated&lt;/code&gt; tags, the semver majors, the migration guides, the announcements. Do every bit of it.&lt;/p&gt;
&lt;p&gt;The other job is the census: know who your consumers are in the first place. Nothing in the playbook does this half, and every part of the playbook silently assumes it is already done. The signal turns out to be conditional and frequently silent, the obvious sources of the list are stale or scoped to the wrong thing, and even the automation that performs the migration has to be handed the list before it can run.&lt;/p&gt;
&lt;p&gt;The signal tells your consumers. The census tells you who they are. Those are different artefacts, built from different sources, and on the morning a forgotten service falls over, the second one is the only one that would have prevented it. You can deprecate a library in an afternoon. Knowing who was still standing on it is the part you never had.&lt;/p&gt;
&lt;p&gt;This is the query &lt;a href=&quot;https://riftmap.dev/&quot;&gt;Riftmap&lt;/a&gt; is built to answer. Point it at your GitHub or GitLab organisation with one read-only token and it parses the dependency edges across every repo. npm, Python, Go, Terraform, Helm, Docker, GitHub Actions, GitLab CI, and the rest. &quot;What depends on this library, at which version, owned by whom&quot; becomes one query instead of one archaeology dig. Before you mark anything deprecated, you get the census the playbook assumed you already had: every repo still on it, the version each one pins, the team to route the migration to. The annotation tells your consumers. Riftmap tells you who they are. And once the graph exists, the census can meet the change where it happens: &lt;a href=&quot;https://riftmap.dev/blog/blast-radius-gate-merge-pipeline/&quot;&gt;a forty-line CI gate&lt;/a&gt; posts the consumer count on every merge request against the library, so the next breaking change to it gets the review its blast radius deserves.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;Riftmap maps cross-repo dependencies across your entire GitLab or GitHub organisation — Terraform, Docker, CI templates, Helm, npm, Go, Python, and more. One read-only token. No YAML to maintain. &lt;a href=&quot;https://app.riftmap.dev&quot;&gt;The free tier is here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Appendix: the argument in short&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Claim.&lt;/strong&gt; Deprecating an internal library is treated as a single task and is actually two. Signalling (telling consumers it is going away) is what every guide documents and what the tooling is good at. The consumer census (knowing who the consumers are across your repos) is the part nothing in the playbook does, and it is the part that decides whether the removal is safe. The signal is conditional and often silent, the manual sources of the list are stale or wrong-scoped, and even automated migration presupposes the list. The census can only be built by parsing the dependency edges your manifests already declare across every repo.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Consumer census.&lt;/strong&gt; The set of repositories that depend on a given library, with the version each one pins and the team that owns it, derived from the manifests across an organisation rather than from a registry, a wiki, or an announcement.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;FAQ.&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Will a deprecation warning reach consumers who pinned an old version?&lt;/em&gt; Often not. An npm deprecation surfaces as &lt;code&gt;npm WARN deprecated&lt;/code&gt; during install and resolution, so a repo with a committed lockfile that is not reinstalling never sees it. Python&apos;s &lt;code&gt;DeprecationWarning&lt;/code&gt; is ignored by default for every module except &lt;code&gt;__main__&lt;/code&gt;, so a service that imports a deprecated function and runs it in production emits nothing visible unless it runs tests with warnings surfaced. A JSDoc &lt;code&gt;@deprecated&lt;/code&gt; tag only flags for a developer whose linter is configured for it and who rebuilds the repo. The consumers you most need to reach are the quiet, pinned ones, and they are the least likely to see any signal.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;How do I find every repo that depends on an internal library before deprecating it?&lt;/em&gt; Not from the warning, which is conditional and silent for inactive consumers. Not from a hand-maintained consumers page, which goes stale the next time anyone adds a dependency. Not from GitHub&apos;s Dependents view, which only computes dependents for public repositories. You parse the dependency edges your manifests declare (&lt;code&gt;package.json&lt;/code&gt;, &lt;code&gt;go.mod&lt;/code&gt;, &lt;code&gt;pyproject.toml&lt;/code&gt;, Terraform &lt;code&gt;source&lt;/code&gt;, Helm references, CI includes) across every repo, and query the resulting graph.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;How do I find consumers of a private package across repositories?&lt;/em&gt; Reverse-dependency tools like &lt;code&gt;apt-cache rdepends&lt;/code&gt; answer for one machine, and GitHub&apos;s dependency graph only covers public repos. For a private org, the consumer set has to be parsed from the manifests in each repo and assembled into a cross-repo reverse-dependency graph.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;What is the first step in safely deprecating a shared internal library?&lt;/em&gt; The consumer census, before any annotation or announcement. You cannot set a credible sunset date, estimate the migration effort, or stage the rollout until you know which repos consume the library and at which version.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Related reading.&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-internal-npm-package/&quot;&gt;How to find every consumer of your internal npm package&lt;/a&gt; — the per-ecosystem mechanics for the most common version of this problem.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-internal-python-package/&quot;&gt;How to find every consumer of your internal Python package&lt;/a&gt; — the same census, for Python&apos;s packaging and import surface.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://riftmap.dev/blog/changing-shared-ci-template-two-deployments/&quot;&gt;You changed the shared CI template. Half the org got it instantly. The other half will never see it&lt;/a&gt; — the CI limit case of this post, where the signalling layer is gone entirely: no &lt;code&gt;@deprecated&lt;/code&gt;, no &lt;code&gt;DeprecationWarning&lt;/code&gt;, no semver boundary, nothing between your merge and someone else&apos;s red pipeline.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://riftmap.dev/blog/base-image-cve-which-repos-to-fix/&quot;&gt;A CVE just hit your base image. Your scanner won&apos;t tell you which repos to fix&lt;/a&gt; — the reactive twin of this post: same cross-repo-versus-single-artifact gap, forced by an external shock instead of a planned removal.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://riftmap.dev/blog/dependency-bus-factor/&quot;&gt;Your senior engineer just left. Your bus factor was measuring the wrong thing&lt;/a&gt; — what happens when the person who held the consumer map in their head walks out.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://riftmap.dev/blog/the-catalog-maintenance-trap/&quot;&gt;The catalog maintenance trap&lt;/a&gt; — why the hand-maintained consumers page is wrong by the time you read it.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://riftmap.dev/blog/monorepo-vs-polyrepo/&quot;&gt;Monorepo vs polyrepo: the debate is measuring the wrong thing&lt;/a&gt; — why &quot;what depends on this&quot; being queryable matters more than where the code lives.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://riftmap.dev/what-is-cross-repo-dependency-mapping/&quot;&gt;What is cross-repo dependency mapping?&lt;/a&gt; — the glossary definition of the parsed graph this post keeps pointing at.&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>deprecation</category><category>dependency-management</category><category>cross-repo-dependencies</category><category>modules</category><category>platform-engineering</category><author>Daniel Westgaard</author></item><item><title>Your senior engineer just left. Your bus factor was measuring the wrong thing.</title><link>https://riftmap.dev/blog/dependency-bus-factor/</link><guid isPermaLink="true">https://riftmap.dev/blog/dependency-bus-factor/</guid><description>A senior leaves and the code stays readable. What walks out is the cross-repo dependency map they held in their head — the part your bus factor, measured from commits, never counted.</description><pubDate>Wed, 17 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;It is three weeks after the goodbye lunch. The staff engineer who had owned half your platform for six years is gone, the Slack handle greyed out, the laptop returned. And now there is a change that has to happen. Marketing&apos;s base image needs bumping for a CVE, or the old auth module is finally being pulled, or a shared Terraform module everyone forked from needs a breaking edit. Routine work. The kind they would have scoped in an afternoon without opening more than two tabs.&lt;/p&gt;
&lt;p&gt;You open the repo. The code is all there. It is readable. Nothing is encrypted, obfuscated, or missing, and you can follow every function in it. And you still cannot answer the one question that has to come before you touch anything. If I change this, what else breaks.&lt;/p&gt;
&lt;p&gt;The part that took me a while to see clearly is that this sounds like a paradox and is not one. You did not lose their understanding of the code. The code stayed. Git kept all of it. What left was something that was never in the repo to begin with: the map of how the repos depend on each other. Which services build on that base image. Which repos still import the auth module, at which version. Which of the forty Terraform consumers will actually break on the edit and which only look like they might. They held that map in their head, assembled over six years of being the person who got paged, and it walked out the door with them.&lt;/p&gt;
&lt;p&gt;We have a number that is supposed to warn us about exactly this. It is called the bus factor, and on paper their leaving should have tripped it. The uncomfortable thing is that for most teams, the bus factor was quietly measuring something other than the thing that just hurt them.&lt;/p&gt;
&lt;h2&gt;The bus factor you were measuring was an authorship number&lt;/h2&gt;
&lt;p&gt;I want to be fair to the bus factor, because the concept is good and the people who formalised it did careful work. The &lt;a href=&quot;https://en.wikipedia.org/wiki/Bus_factor&quot;&gt;informal definition&lt;/a&gt; is clean: the minimum number of people who have to disappear before a project stalls. A bus factor of one is the canonical danger. And there is a real research literature behind making it computable, going back to the truck-factor work on version-control history and continuing into &lt;a href=&quot;https://www.sciencedirect.com/science/article/pii/S0020025526002847&quot;&gt;recent papers&lt;/a&gt; that model a project as a bipartite graph of contributors and the files or tasks they own. The commercial &lt;a href=&quot;https://contributoriq.com/blog/what-is-bus-factor-how-to-calculate-measure&quot;&gt;calculators&lt;/a&gt; do the same thing in a friendlier wrapper. Point them at a repository, read the commit history, and see which files only one person has meaningfully touched.&lt;/p&gt;
&lt;p&gt;Notice the input in every one of those. It is the commit log. Bus factor, as it is actually computed, is a function of authorship. Who wrote this file, who has touched it since, how concentrated that history is. That is a genuine signal, and it answers a genuine question. If this person leaves, which code has no second author.&lt;/p&gt;
&lt;p&gt;But &quot;which code has no second author&quot; is not the question you are stuck on three weeks after the departure. You are not missing the &lt;em&gt;authorship&lt;/em&gt; of the auth module. You can read it. You are missing what depends on it across the other eighty repos, and that fact was never written into any commit. Authorship concentration and dependency knowledge are two different quantities, and the standard bus factor only measures the first. The senior could have pair-programmed every line they ever wrote and pushed the authorship bus factor to a comfortable three, and you would still be sitting here unable to bump the base image, because the thing that broke was never about who typed the code.&lt;/p&gt;
&lt;p&gt;So it is worth naming the quantity the standard bus factor misses, because it is the one that actually decides how bad a departure turns out to be. Owen Zanzal nudged me toward seeing it this way, and this is the dedicated follow-up I promised in &lt;a href=&quot;https://riftmap.dev/blog/senior-engineer-leaving-tribal-knowledge/&quot;&gt;the senior-engineer piece&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;A &lt;strong&gt;dependency bus factor&lt;/strong&gt; is the number of people who have to leave before your organisation loses the ability to answer &quot;what depends on this&quot; across its repos. It is not about who wrote any given service. It is about who can still tell you, once they are gone, which repos consume a shared module, at which version, and in what order those repos break when it changes. Where the classic bus factor is computed from commit authorship inside repos, the dependency bus factor lives in the edges &lt;em&gt;between&lt;/em&gt; them: the Dockerfile &lt;code&gt;FROM&lt;/code&gt; lines, the &lt;code&gt;go.mod&lt;/code&gt; requires, the Helm value references, the Terraform &lt;code&gt;source&lt;/code&gt; blocks, the GitLab CI &lt;code&gt;include&lt;/code&gt;s. A team can have a perfectly healthy authorship bus factor and a dependency bus factor of one, and the second number stays invisible right up until the one person who held the cross-repo map in their head hands in their notice.&lt;/p&gt;
&lt;h2&gt;What actually walked out the door&lt;/h2&gt;
&lt;p&gt;There is a whole genre of good writing about inheriting an unfamiliar codebase, and I do not want to wave it away, because the advice in it is sound. &lt;a href=&quot;https://understandlegacycode.com/blog/dive-into-an-unfamiliar-codebase-from-its-edges/&quot;&gt;Start from the edges.&lt;/a&gt; Find an entry point and trace it. Run the thing under a debugger and watch what executes. Make small, safe refactors as you go to anchor what you have learned. Talk to whoever is still around. If your problem is &quot;I have been handed one large application and I do not understand its internal logic,&quot; that advice will get you there.&lt;/p&gt;
&lt;p&gt;The problem three weeks after a senior leaves is a different shape, and reading harder does not solve it. The auth module is two hundred lines and I understood it the first afternoon. What I cannot get by reading it is the set of repos elsewhere in the org that require it, because that information is not in the module. It is distributed across every &lt;em&gt;other&lt;/em&gt; repo&apos;s manifests, and no single repo I open will reveal it. The base image is worse, because the dependency runs through indirection the file does not spell out. A leaf service builds &lt;code&gt;FROM acme/runtime-base&lt;/code&gt;, which is itself built &lt;code&gt;FROM debian&lt;/code&gt;, so the repo I am editing does not even mention the thing I am actually trying to reason about. The senior knew the base was two hops up. The Dockerfile does not say so.&lt;/p&gt;
&lt;p&gt;This is why the dependency bus factor is its own quantity and not just &quot;bus factor for teams that documented badly.&quot; The knowledge is not absent because someone was lazy about comments. It is absent from any one place by construction, because a cross-repo dependency is a fact about the &lt;em&gt;relationship&lt;/em&gt; between two repos, and a relationship does not live inside either endpoint. The senior was, in effect, a human index over those relationships. The index is what you lost, and an index is exactly the thing reading one repo at a time cannot rebuild.&lt;/p&gt;
&lt;h2&gt;Why the handover doc was never going to hold it&lt;/h2&gt;
&lt;p&gt;The standard answer to all of this is the knowledge transfer plan. Get it out of their head before they go. Shadowing, a handover document, a wiki page, a fortnight of overlap if the timing is kind. And for a lot of what a senior carries, that genuinely works. The &quot;why did we ever do it this way,&quot; the war stories, the half-broken vendor integration nobody should touch on a Friday afternoon. Tacit, judgement-shaped knowledge, the kind you can only get by asking. Write it down. It is worth every hour.&lt;/p&gt;
&lt;p&gt;But the people who run knowledge transfer for a living are &lt;a href=&quot;https://elium.com/templates/knowledge-transfer-plan/&quot;&gt;blunt about where it fails&lt;/a&gt;. It runs on memory and goodwill, and both are unreliable narrators. The person leaving leaves out half of what they carry, not from carelessness but because the deepest knowledge is the most internalised, the kind they have stopped noticing they have. And the dependency map is the worst possible candidate for hand-capture, for a reason that has nothing to do with effort. It is large, it is mechanical, and it goes stale on the next merge. The afternoon someone writes &quot;services X, Y and Z build on this base,&quot; a fourth team adds itself, and the page is now wrong and nobody knows it is wrong. This is the same failure that makes platform teams quietly abandon their &lt;a href=&quot;https://riftmap.dev/blog/the-catalog-maintenance-trap/&quot;&gt;service catalogues&lt;/a&gt;. A hand-maintained map of what depends on what is only ever as accurate as the last person who remembered to update it, and &quot;remember to update it&quot; is precisely the discipline that does not survive the one person who cared about it leaving.&lt;/p&gt;
&lt;p&gt;So the handover captures the things only a human can give you and quietly drops the one thing you most needed to keep, because that thing is too big and too volatile to write down by hand. You come out of it with the stories and without the map.&lt;/p&gt;
&lt;h2&gt;The one piece of what they knew that you never had to transfer&lt;/h2&gt;
&lt;p&gt;Here is the move, and it is almost annoying once you see it. The dependency map is the single part of what the senior knew that you did not need them to transfer at all. Because unlike the war stories and the judgement calls, it is not tacit. It is written down already, just not in prose. It is in the manifests. Every &lt;code&gt;FROM&lt;/code&gt; line, every &lt;code&gt;require&lt;/code&gt;, every chart reference, every module &lt;code&gt;source&lt;/code&gt; is a dependency edge the senior had memorised and the repo had already declared. You do not have to get it out of anyone&apos;s head. You can read it straight off the files, deterministically, the same answer every time you ask. Parsed, not inferred.&lt;/p&gt;
&lt;p&gt;That is the real split between the two kinds of knowledge a departure takes. The semantic stuff, the intent and the patterns and the reasons, is genuinely lost when the person goes, and a handover is your one shot at it. The structural stuff, the graph of what depends on what, was never really theirs to take. It only felt lost because the only working copy was the one in their head, and nobody had built the other one. I made the longer version of this argument when &lt;a href=&quot;https://riftmap.dev/blog/meta-tribal-knowledge-engine-build-the-graph-first/&quot;&gt;Meta published their tribal-knowledge writeup&lt;/a&gt;: the structural layer is the durable one precisely because parsers are deterministic and re-run on every push, while the human-written layer decays and needs constant tending. A senior leaving is that same argument arriving through a more painful door. You do not transfer the map. You parse it. The closest sibling to this piece, &lt;a href=&quot;https://riftmap.dev/blog/senior-engineer-leaving-tribal-knowledge/&quot;&gt;most of what a departing senior knew was in the repos all along&lt;/a&gt;, works the same split from the angle of the resignation itself.&lt;/p&gt;
&lt;h2&gt;Even if nobody is leaving&lt;/h2&gt;
&lt;p&gt;You do not have to wait for a resignation for this to bite, and the proactive version is where the number earns its name. If you want to lower your dependency bus factor on purpose, the first problem is that you cannot lower a number you cannot see, and you cannot see it from the commit log. The repos that should worry you are not the ones with the messiest history. They are the ones where a single person is the only owner &lt;em&gt;and&lt;/em&gt; a lot of other repos depend on what that repo produces. The danger is the product of two things. Sole ownership, which the commit history can show you. And cross-repo fan-in, which it cannot. A repo one person owns that nothing else depends on is fine. A repo one person owns that forty other repos build on is a dependency bus factor of one with a &lt;a href=&quot;https://riftmap.dev/blog/ai-doesnt-understand-blast-radius/&quot;&gt;blast radius&lt;/a&gt; attached, and it looks identical to the safe one if all you are reading is authorship.&lt;/p&gt;
&lt;p&gt;That product, sole ownership crossed with how hard the rest of the org leans on the thing, is the actual risk surface, and you can only compute it if you have the dependency graph to multiply the ownership against. Find those repos while the person who owns them is still in the building, and a future departure becomes a routine offboarding instead of a three-week archaeology dig.&lt;/p&gt;
&lt;h2&gt;Two bus factors&lt;/h2&gt;
&lt;p&gt;So, stripped down. There are two numbers here, and the departure showed you which one you had been watching.&lt;/p&gt;
&lt;p&gt;The authorship bus factor is real and worth tracking. It is computed from your commit history, it tells you which code has no second author, and it is the one every existing tool measures. Keep an eye on it.&lt;/p&gt;
&lt;p&gt;The dependency bus factor is the one that decided how bad last month actually was. It is not in the commit history. It lives in the edges between your repos, the same &lt;code&gt;FROM&lt;/code&gt; lines and requires and chart references your manifests already declare, and it can only be computed from those. It was the part of what the senior knew that you could have kept without asking them to write a single word down, and the reason it felt catastrophic to lose is that nobody had built the copy that does not live inside a person.&lt;/p&gt;
&lt;p&gt;When the senior left, the code stayed and the map walked out with them. The code was never the part you could not replace. The map was. And the map was the one thing you never needed a goodbye lunch to hand over, because it was sitting in your repositories the whole time, waiting to be read.&lt;/p&gt;
&lt;p&gt;This is the query &lt;a href=&quot;https://riftmap.dev/&quot;&gt;Riftmap&lt;/a&gt; exists to answer. Point it at your GitHub or GitLab organisation with one read-only token and it parses the cross-repo edges across every repo. Terraform, Docker, Helm, Kubernetes, GitHub Actions, GitLab CI, Ansible, Go modules, npm. &quot;What depends on this module, at which version, owned by whom&quot; becomes one query instead of one person. And because it crosses ownership against cross-repo fan-in, it will surface the single-maintainer, high-blast-radius repos, your dependency bus factor of one, while the person who owns them is still on the team. The graph is the copy of the map that does not resign.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Appendix: the argument in short&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Claim.&lt;/strong&gt; The bus factor every tool measures is computed from commit authorship, so it answers &quot;which code has no second author.&quot; The thing that actually breaks when a senior engineer leaves is different: the cross-repo dependency map they held in their head, which is in no commit and inside no single repo. Call that the dependency bus factor. It is the one part of what they knew that you can keep without a handover, because it is already declared in your manifests and can be parsed deterministically.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Dependency bus factor.&lt;/strong&gt; The number of people who must leave before your organisation can no longer answer &quot;what depends on this&quot; across its repos. Computed from the dependency edges &lt;em&gt;between&lt;/em&gt; repos (Dockerfile &lt;code&gt;FROM&lt;/code&gt; lines, &lt;code&gt;go.mod&lt;/code&gt; requires, Helm value references, Terraform &lt;code&gt;source&lt;/code&gt; blocks, CI includes), not from commit authorship inside them.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;FAQ.&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Does the bus factor measure dependency knowledge?&lt;/em&gt; No. As computed in practice it measures authorship concentration from commit history. It tells you which code has a single author, not which repos across the org depend on that code.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;How do you find what depends on what after the engineer who knew leaves?&lt;/em&gt; Not by reading the code, because the edges live between repos and not inside any one of them, and not reliably from a handover doc, because that goes stale on the next merge. You parse the dependency edges your manifests already declare and query the resulting graph.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Can a knowledge transfer plan capture cross-repo dependencies?&lt;/em&gt; Only by hand, and only as a snapshot. A hand-written &quot;what depends on what&quot; map is wrong the next time anyone adds a consumer, the same way service catalogues go stale.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Related reading.&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://riftmap.dev/blog/senior-engineer-leaving-tribal-knowledge/&quot;&gt;Most of what a departing senior knew was in the repos all along&lt;/a&gt; — the sibling to this piece: tribal knowledge is two things wearing one name, and the structural half was declared in your manifests the whole time.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://riftmap.dev/blog/meta-tribal-knowledge-engine-build-the-graph-first/&quot;&gt;Meta needed 50+ AI agents to map their tribal knowledge&lt;/a&gt; — the architectural version of this argument: the parsed dependency graph is the durable layer; the human-written layer decays.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://riftmap.dev/blog/monorepo-vs-polyrepo/&quot;&gt;Monorepo vs polyrepo: the debate is measuring the wrong thing&lt;/a&gt; — why &quot;what depends on this&quot; being queryable matters more than where the code lives.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://riftmap.dev/blog/the-catalog-maintenance-trap/&quot;&gt;The catalog maintenance trap&lt;/a&gt; — why hand-maintained maps of dependencies go stale and get abandoned.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://riftmap.dev/blog/base-image-cve-which-repos-to-fix/&quot;&gt;A CVE just hit your base image. Your scanner won&apos;t tell you which repos to fix&lt;/a&gt; — the same cross-repo-versus-single-artifact gap, on the reactive side.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://riftmap.dev/blog/deprecate-internal-library-find-consumers/&quot;&gt;You deprecated the internal library. The repos still using it never saw the warning&lt;/a&gt; — the planned-removal sibling: the consumer set the departing senior held in their head is the same one a deprecation requires, just summoned by a different trigger.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://riftmap.dev/what-is-cross-repo-dependency-mapping/&quot;&gt;What is cross-repo dependency mapping?&lt;/a&gt; — the glossary definition of the parsed graph this post keeps pointing at.&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>bus-factor</category><category>tribal-knowledge</category><category>platform-engineering</category><category>cross-repo-dependencies</category><category>key-person-risk</category><category>knowledge-transfer</category><category>dependency-graph</category><author>Daniel Westgaard</author></item><item><title>How to Find Every Consumer of Your Internal Python Package</title><link>https://riftmap.dev/blog/how-to-find-every-consumer-of-your-internal-python-package/</link><guid isPermaLink="true">https://riftmap.dev/blog/how-to-find-every-consumer-of-your-internal-python-package/</guid><description>You maintain an internal Python package and a breaking change is coming. Which repos depend on it, at which version — and why pip, PyPI and your private index all answer the wrong question.</description><pubDate>Tue, 16 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;You maintain an internal Python package on a private index. You need to change its API. Which repos across the org depend on it, and at which version? The public Python ecosystem has an answer to that question. The moment you move the package onto your own index, everything that knows the answer is looking somewhere your package never appears.&lt;/em&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;npm puts a Dependents tab on the registry page for every public package. PyPI has nothing of the sort. Open the project page for &lt;code&gt;requests&lt;/code&gt; or &lt;code&gt;flask&lt;/code&gt; and there is no reverse-dependency view, no list of what builds on top of it, no count. What answers the question for public packages is third-party and sits beside the index rather than inside it: Google&apos;s &lt;a href=&quot;https://deps.dev/&quot;&gt;deps.dev&lt;/a&gt; and &lt;a href=&quot;https://libraries.io/&quot;&gt;libraries.io&lt;/a&gt;, both of which crawl the public index and will show you who depends on a given package.&lt;/p&gt;
&lt;p&gt;Now make the package yours. Rename it from &lt;code&gt;confparse&lt;/code&gt; to &lt;code&gt;yourco-config&lt;/code&gt;, set it to private, and publish it to AWS CodeArtifact or the GitLab PyPI registry instead of &lt;code&gt;pypi.org&lt;/code&gt;. deps.dev and &lt;a href=&quot;http://libraries.io&quot;&gt;libraries.io&lt;/a&gt; go dark immediately, because they crawl the public index and your package is not on it. pip has nothing to offer either. &lt;code&gt;pip show yourco-config&lt;/code&gt; lists a &quot;Required-by&quot; field, but it only reflects what is installed in the environment you happen to run it in, and &lt;a href=&quot;https://github.com/pypa/pip/issues/4968&quot;&gt;pip has had an open request for a real reverse-dependency command for years&lt;/a&gt;. Dependabot and Renovate know implicitly who depends on what, because they are configured per repo, but they are updaters, not mappers, and only where they are switched on.&lt;/p&gt;
&lt;p&gt;There is a second gap underneath the first, and it is worth sitting with. Even for a public package, the dependents that deps.dev and &lt;a href=&quot;http://libraries.io&quot;&gt;libraries.io&lt;/a&gt; can show you are mostly &lt;em&gt;other published packages&lt;/em&gt;, because a published package is what an index crawler can see. The things consuming your internal library are overwhelmingly applications. Services, pipelines, DAG repos, batch jobs. None of those are published to any index, so they would not appear as dependents even if your package were public. So the answer exists for the packages that cannot hurt you, and is missing for the one that can. The shared client every service imports, the config package forty repos pull in, the feature library the whole ML platform builds on. The one whose breaking change is your problem is precisely the one with no consumer view at all. This post is about getting that view back, and about why Python makes it genuinely harder than the other ecosystems in &lt;a href=&quot;https://riftmap.dev/blog/series/find-every-consumer/&quot;&gt;this series&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;The scenario&lt;/h2&gt;
&lt;p&gt;Your platform team, or your ML-platform team, publishes a package. Maybe it is &lt;code&gt;yourco-clients&lt;/code&gt;, a generated client for your internal APIs that half the services import. Maybe it is &lt;code&gt;yourco-observability&lt;/code&gt;, the structured-logging and trace-propagation library every service is supposed to use. Maybe it is &lt;code&gt;yourco-config&lt;/code&gt;, a thin package that standardises settings loading so nobody hand-rolls it. Maybe it is &lt;code&gt;yourco-features&lt;/code&gt;, a shared feature-store and data-access layer the whole ML org builds on.&lt;/p&gt;
&lt;p&gt;It started as a way to stop copy-pasting. A few repos adopted it. Then more. And here is where Python diverges from every other post in this series, immediately, before we even get to the hard part. There is no single place a consumer declares the dependency. There is barely a single &lt;em&gt;format&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;One service declares it in &lt;code&gt;requirements.txt&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;yourco-clients==2.4.1
yourco-observability~=1.7
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Another uses the modern standard, &lt;a href=&quot;https://peps.python.org/pep-0621/&quot;&gt;PEP 621&lt;/a&gt; dependencies in &lt;code&gt;pyproject.toml&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[project]
name = &quot;checkout-service&quot;
dependencies = [
    &quot;yourco-clients&amp;gt;=2.4,&amp;lt;3.0&quot;,
    &quot;yourco-observability~=1.7&quot;,
]

[project.optional-dependencies]
dev = [&quot;yourco-testtools&amp;gt;=0.9&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A third is on Poetry, which until recently used its own table with its own syntax. The caret is not a PEP 440 operator, the resolution semantics are Poetry&apos;s:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[tool.poetry.dependencies]
python = &quot;^3.11&quot;
yourco-clients = &quot;^2.4.1&quot;
yourco-observability = &quot;~1.7&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A fourth predates all of that and declares its dependencies in &lt;code&gt;setup.py&lt;/code&gt;, in arbitrary Python:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;setup(
    name=&quot;reporting-service&quot;,
    install_requires=[
        &quot;yourco-clients&amp;gt;=2.4,&amp;lt;3.0&quot;,
        &quot;yourco-observability~=1.7&quot;,
    ],
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A fifth never stood up a private index at all and pulls your code straight from git:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-e git+https://gitlab.yourco.com/platform/yourco-clients.git@v2.4.1#egg=yourco-clients
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And because the package is private, every consumer that does use the index carries routing config that points at it, the way an &lt;code&gt;.npmrc&lt;/code&gt; does for a scoped npm package. In Python that lives in &lt;code&gt;pip.conf&lt;/code&gt;, or a &lt;code&gt;.netrc&lt;/code&gt;, or a Poetry source, or a uv index table:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# pip.conf
[global]
index-url = https://pypi.org/simple
extra-index-url = https://gitlab.yourco.com/api/v4/projects/42/packages/pypi/simple
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Twenty repos adopted the package, across four or five of these mechanisms. Then you stopped counting, because nothing in your toolchain counts for you. Now you need to change it. Drop a parameter, rename an export, cut a major. The question is the one that runs through &lt;a href=&quot;https://riftmap.dev/blog/series/find-every-consumer/&quot;&gt;every post in this series&lt;/a&gt;: &lt;strong&gt;which repos across the org depend on this package, at which version, and which of them break when I publish?&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;The change you ship without shipping it&lt;/h2&gt;
&lt;p&gt;Before the tooling, the part that makes this sharper than it first looks, and it is more acute in Python than in the npm version of &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-internal-npm-package/&quot;&gt;this same argument&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;A loose constraint is a standing instruction to adopt your next release. A consumer on &lt;code&gt;yourco-clients&amp;gt;=2.4&lt;/code&gt; is not pinned. They will take whatever the newest version is the next time their environment is resolved fresh. The &lt;a href=&quot;https://peps.python.org/pep-0440/&quot;&gt;PEP 440 compatible-release operator&lt;/a&gt;, &lt;code&gt;~=1.7&lt;/code&gt;, is the same thing inside a band: it means &lt;code&gt;&amp;gt;=1.7, &amp;lt;2.0&lt;/code&gt;, so every 1.x you publish is a candidate. Poetry&apos;s &lt;code&gt;^2.4.1&lt;/code&gt; resolves to &lt;code&gt;&amp;gt;=2.4.1, &amp;lt;3.0.0&lt;/code&gt;, which is a subscription to every minor and patch you ship in the 2.x line.&lt;/p&gt;
&lt;p&gt;Python makes this land more easily than npm does, for one structural reason. A very large number of Python repos have no committed lockfile. They have a loose &lt;code&gt;requirements.txt&lt;/code&gt; that gets &lt;code&gt;pip install&lt;/code&gt;-ed during a Docker build, on every build, against the live index. There is no &lt;code&gt;poetry.lock&lt;/code&gt; or &lt;code&gt;uv.lock&lt;/code&gt; holding the line. So the resolution is not a one-time event that someone reviews in a pull request. It happens every time the image is rebuilt, silently, on the consumer&apos;s schedule rather than yours. You did not roll out your 2.5.0. They did, the next time CI ran, and the first you hear of a regression is somebody else&apos;s red pipeline.&lt;/p&gt;
&lt;p&gt;This is not a fringe worry, it is being actively argued about right now. uv, the fast-rising resolver, &lt;a href=&quot;https://www.loopwerk.io/articles/2026/uv-ux-mess/&quot;&gt;defaults to constraints with no upper bound&lt;/a&gt;, which means &lt;code&gt;uv lock --upgrade&lt;/code&gt; will happily pull a breaking major across every transitive dependency, and the friction of that has pushed uv to add a &lt;code&gt;--bounds&lt;/code&gt; option so &lt;code&gt;uv add&lt;/code&gt; can produce a safer &lt;code&gt;&amp;gt;=2.13.4,&amp;lt;3.0.0&lt;/code&gt;. The community has not settled on how tight constraints should be. While that argument runs, your consumers are scattered across every position on the spectrum, and you cannot see which.&lt;/p&gt;
&lt;p&gt;The reverse case is just as bad in the other direction. When you do the honest thing and cut a genuine breaking change as a major, &lt;code&gt;2.x&lt;/code&gt; to &lt;code&gt;3.0.0&lt;/code&gt;, a &lt;code&gt;&amp;lt;3.0&lt;/code&gt; pin or a Poetry caret correctly refuses to follow. That is the right behaviour. It also leaves you with a long tail of repos stranded on the old major, indefinitely, with no list of who they are. You cannot deprecate 2.x because you cannot see who is still on it.&lt;/p&gt;
&lt;p&gt;Either way the constraint is the mechanism, and the constraint is exactly what a quick search across your repos cannot evaluate. You need to know who consumes the package and how their constraint relates to what you are about to publish. Both halves of that live in files most audits never open, in formats most scripts do not all parse.&lt;/p&gt;
&lt;h2&gt;What existing tools give you (and where they stop)&lt;/h2&gt;
&lt;p&gt;I want to be fair to the options, because several are genuinely useful for the slice they cover, and I reach for some of them myself.&lt;/p&gt;
&lt;h3&gt;PyPI, deps.dev, &lt;a href=&quot;http://libraries.io&quot;&gt;libraries.io&lt;/a&gt;, GitHub dependents&lt;/h3&gt;
&lt;p&gt;For public packages, deps.dev and &lt;a href=&quot;http://libraries.io&quot;&gt;libraries.io&lt;/a&gt; are the right tools, and I would point you straight at them. GitHub&apos;s dependency graph adds a &quot;Used by&quot; panel for repositories that publish a package, though &lt;a href=&quot;https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/exploring-the-dependencies-of-a-repository&quot;&gt;its own documentation calls the dependent counts approximate&lt;/a&gt;. The structural problem is not that any of these are bad. It is that they are properties of the &lt;em&gt;public&lt;/em&gt; index. A private package is access-controlled by design, served from your own registry behind a token, and never indexed by anything that crawls &lt;code&gt;pypi.org&lt;/code&gt;. The same access control that keeps your code off the public internet keeps it off every public consumer graph. There is nothing to fix here. The data is unreachable, on purpose. And as above, even for public packages these views count published packages far better than they count the unpublished services that are usually your real consumers.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;pip show&lt;/code&gt;, &lt;code&gt;pipdeptree&lt;/code&gt;, and the reverse-dependency tools&lt;/h3&gt;
&lt;p&gt;These do answer a reverse question, and people reach for them first. &lt;code&gt;pip show yourco-clients&lt;/code&gt; lists a &quot;Required-by&quot; field. &lt;a href=&quot;https://pypi.org/project/pipdeptree/&quot;&gt;pipdeptree&lt;/a&gt; and &lt;a href=&quot;https://pypi.org/project/deptree/&quot;&gt;deptree&lt;/a&gt; will invert the tree and show you dependents with &lt;code&gt;-r&lt;/code&gt;. They are the right tools for &quot;what in &lt;em&gt;this environment&lt;/em&gt; depends on this.&quot;&lt;/p&gt;
&lt;p&gt;But they operate on one installed environment at a time, outward from whatever happens to be in that virtualenv. They cannot tell you which &lt;em&gt;other repos&lt;/em&gt; in the org depend on your package. There is no index-side reverse query to ask, either. &lt;a href=&quot;https://github.com/pypa/pip/issues/4968&quot;&gt;pip has had an open request for a reverse-dependency command for years&lt;/a&gt;, and the standing workaround is a script that walks installed distributions. To build the org-wide view you would clone every repo, create a clean environment in each, install, run &lt;code&gt;pipdeptree -r&lt;/code&gt;, and aggregate the output yourself. By the time you finished, the resolutions you installed from would have moved.&lt;/p&gt;
&lt;h3&gt;The private index itself&lt;/h3&gt;
&lt;p&gt;This is the one people assume covers them, because the index is the thing all the packages flow through. AWS CodeArtifact, JFrog Artifactory, Sonatype Nexus, the GitLab PyPI registry, &lt;a href=&quot;https://pypi.org/project/devpi-server/&quot;&gt;devpi&lt;/a&gt;, GemFury. They host your private packages, cache the public ones, and serve both from one endpoint behind auth.&lt;/p&gt;
&lt;p&gt;They are very good at it. What none of them model is consumption at the source level. The index records that some authenticated client downloaded &lt;code&gt;yourco-clients 2.4.1&lt;/code&gt;. It does not record which repo&apos;s &lt;code&gt;pyproject.toml&lt;/code&gt; declared the dependency, which team&apos;s CI pipeline the install ran in, or whether the thing that pulled it was a service you care about or a throwaway branch. It is a distribution and caching layer, not a consumption graph. This is the same gap I described for &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-go-module/&quot;&gt;internal Go module proxies in the Go edition&lt;/a&gt;: a proxy logs fetches, not the manifest that triggered them. The download event is not the dependency edge.&lt;/p&gt;
&lt;p&gt;There is one fair exception worth naming. Some registry products, &lt;a href=&quot;https://blog.inedo.com/python/package-dependencies&quot;&gt;ProGet among them&lt;/a&gt;, do surface a consumer view, listing applications by name and version against a package. That is closer than most, and if you run one, use it. But it sees consumption that flows through &lt;em&gt;that&lt;/em&gt; registry, of packages &lt;em&gt;it&lt;/em&gt; hosts. It does not read the source manifest in every repo regardless of which index they use, and it does not see the git-ref consumption that never touches a registry at all. The next section is mostly a list of the consumption that escapes a registry-centred view.&lt;/p&gt;
&lt;h3&gt;Renovate and Dependabot&lt;/h3&gt;
&lt;p&gt;Both support Python as a first-class ecosystem, including private indexes once you give them credentials, across &lt;code&gt;requirements.txt&lt;/code&gt;, Poetry, Pipfile, and &lt;code&gt;setup.py&lt;/code&gt;. Because they are configured per consumer, they implicitly know which repos depend on what, and they will open pull requests to bump your package when you publish. As with &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-terraform-module/&quot;&gt;Terraform modules&lt;/a&gt; and the rest of the series, the knowledge is in there.&lt;/p&gt;
&lt;p&gt;But they are updaters, not mappers. There is no org-level &quot;show me every repo that depends on &lt;code&gt;yourco-clients&lt;/code&gt;, and what constraint each one declares&quot; view to query. They react to new versions going out. The question you have &lt;em&gt;before&lt;/em&gt; you publish a breaking one, who is currently consuming the old version and how, is not something either tool surfaces. And both only cover repos where they have been switched on for your private index. A team that never configured private-index auth in their Renovate config is simply invisible.&lt;/p&gt;
&lt;h3&gt;Code search, and the script&lt;/h3&gt;
&lt;p&gt;You can search your GitHub org or GitLab group for the package name:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;org:yourco &quot;yourco-clients&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For a one-off audit, fine. It finds files that mention the string and gives you a starting list of repos. Then the familiar problems land all at once, and in Python they land harder. It returns the declared constraint, not the installed version. It will not normalise &lt;code&gt;yourco_clients&lt;/code&gt; and &lt;code&gt;yourco-clients&lt;/code&gt; to the same project. It misses a consumer that pulled the package over &lt;code&gt;git+https&lt;/code&gt;. And the index lags your most recent commits.&lt;/p&gt;
&lt;p&gt;So someone writes the script. Enumerate every repo, fetch every &lt;code&gt;requirements*.txt&lt;/code&gt;, &lt;code&gt;pyproject.toml&lt;/code&gt;, &lt;code&gt;setup.py&lt;/code&gt;, &lt;code&gt;setup.cfg&lt;/code&gt;, &lt;code&gt;Pipfile&lt;/code&gt;, and &lt;code&gt;environment.yml&lt;/code&gt;, parse all of them, handle three &lt;code&gt;pyproject.toml&lt;/code&gt; dialects and arbitrary &lt;code&gt;setup.py&lt;/code&gt; code, normalise names, evaluate PEP 440 specifiers, run it on a schedule. People build exactly this. The clearest evidence is &lt;a href=&quot;https://pypi.org/project/all-repos-depends/&quot;&gt;&lt;code&gt;all-repos-depends&lt;/code&gt;&lt;/a&gt;, a real org-scanner whose providers read the &lt;code&gt;setup.py&lt;/code&gt; AST for the package name and &lt;code&gt;install_requires&lt;/code&gt; and parse the requirements-file conventions. The fact that this keeps getting independently rebuilt is the strongest evidence the question matters. It is also, tellingly, honest about its own limit: it can only read a &lt;code&gt;setup.py&lt;/code&gt; that sets its name literally. Which is the first of the corner cases below, and that tool ran straight into it too.&lt;/p&gt;
&lt;h2&gt;Why this is harder than it looks&lt;/h2&gt;
&lt;p&gt;A naive search for the package name both overcounts and undercounts, because Python dependency consumption is not one fact in one place. It is spread across constructs that each behave differently, and Python has more of them than any other ecosystem in this series.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;There is no single manifest, and the formats disagree.&lt;/strong&gt; Go has one canonical &lt;code&gt;go.mod&lt;/code&gt;. npm has one canonical &lt;code&gt;package.json&lt;/code&gt;. Python has at least six families, and once you count the dialects, closer to nine distinct shapes a scanner has to handle: &lt;code&gt;requirements.txt&lt;/code&gt; and its &lt;code&gt;requirements-*.txt&lt;/code&gt; siblings, split &lt;code&gt;requirements/&lt;/code&gt; trees and pip-tools &lt;code&gt;.in&lt;/code&gt; inputs, &lt;code&gt;setup.py&lt;/code&gt; &lt;code&gt;install_requires&lt;/code&gt;, declarative &lt;code&gt;setup.cfg&lt;/code&gt;, &lt;code&gt;pyproject.toml&lt;/code&gt; in three different dialects (PEP 621 &lt;code&gt;[project]&lt;/code&gt;, Poetry&apos;s &lt;code&gt;[tool.poetry]&lt;/code&gt; with its groups, and the &lt;a href=&quot;https://peps.python.org/pep-0735/&quot;&gt;PEP 735&lt;/a&gt; &lt;code&gt;[dependency-groups]&lt;/code&gt; that uv and recent pip understand), &lt;code&gt;Pipfile&lt;/code&gt;, and conda&apos;s &lt;code&gt;environment.yml&lt;/code&gt; with its nested &lt;code&gt;pip:&lt;/code&gt; block. &lt;a href=&quot;https://peps.python.org/pep-0723/&quot;&gt;PEP 723 even added inline dependencies inside a single &lt;code&gt;.py&lt;/code&gt; script&lt;/a&gt;, so the surface is still growing. The version grammar is not even shared: a Poetry caret is not a PEP 440 operator. And the declared constraint and the installed version are different facts living in different places, with the resolved version buried in whichever of &lt;code&gt;poetry.lock&lt;/code&gt;, &lt;code&gt;Pipfile.lock&lt;/code&gt;, &lt;code&gt;uv.lock&lt;/code&gt;, or a pip-compiled &lt;code&gt;requirements.txt&lt;/code&gt; the repo happens to use. Real orgs mix all of this. To find every consumer you have to read all of it, and reconcile it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The distribution name and the import name are different facts.&lt;/strong&gt; This one is pure Python. You &lt;code&gt;pip install scikit-learn&lt;/code&gt; and &lt;code&gt;import sklearn&lt;/code&gt;. &lt;code&gt;PyYAML&lt;/code&gt; imports as &lt;code&gt;yaml&lt;/code&gt;, &lt;code&gt;beautifulsoup4&lt;/code&gt; as &lt;code&gt;bs4&lt;/code&gt;, &lt;code&gt;opencv-python&lt;/code&gt; as &lt;code&gt;cv2&lt;/code&gt;. Your &lt;code&gt;yourco-data-clients&lt;/code&gt; might &lt;code&gt;import yourco_data&lt;/code&gt;. To bind a &lt;em&gt;declared&lt;/em&gt; dependency you match the &lt;a href=&quot;https://peps.python.org/pep-0503/&quot;&gt;PEP 503 normalised name&lt;/a&gt;, which lowercases and collapses any run of &lt;code&gt;.&lt;/code&gt;, &lt;code&gt;-&lt;/code&gt;, or &lt;code&gt;_&lt;/code&gt; to a single &lt;code&gt;-&lt;/code&gt;, so &lt;code&gt;Yourco.Clients&lt;/code&gt;, &lt;code&gt;yourco_clients&lt;/code&gt;, and &lt;code&gt;yourco-clients&lt;/code&gt; are one project. A grep treats them as three. The confusion is real enough that scikit-learn ships a &lt;a href=&quot;https://github.com/scikit-learn/sklearn-pypi-package&quot;&gt;defensive &lt;code&gt;sklearn&lt;/code&gt; shim on PyPI purely to stop people and tools getting it wrong&lt;/a&gt;, and that shim&apos;s own remediation advice is, word for word, a find-every-consumer task: track down which packages declare &lt;code&gt;sklearn&lt;/code&gt; instead of &lt;code&gt;scikit-learn&lt;/code&gt;. It is enough of a trap that &lt;a href=&quot;https://gitlab.com/gitlab-org/gitlab/-/issues/440391&quot;&gt;GitLab&apos;s own SBOM scanner normalised names by the wrong rule and produced incorrect dependency results&lt;/a&gt;. That normalised &lt;em&gt;distribution&lt;/em&gt; name is what a manifest declares and what you match on. The &lt;em&gt;import&lt;/em&gt; name, the thing that actually appears in &lt;code&gt;import&lt;/code&gt; statements, is a different layer again, and it is the one a symbol graph lives in. More on that distinction when we get to the limits.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;setup.py&lt;/code&gt; is code, not data.&lt;/strong&gt; &lt;code&gt;package.json&lt;/code&gt; is JSON and &lt;code&gt;go.mod&lt;/code&gt; has a defined grammar, so a parser can trust them. &lt;code&gt;setup.py&lt;/code&gt; is a Python script, and &lt;code&gt;install_requires&lt;/code&gt; can be a literal list, or read from a file, or assembled in a loop, or gated on markers computed at runtime. The literal case is statically parseable from the AST. The dynamically constructed case is not knowable without executing untrusted code, which no scanner should do. &lt;code&gt;setup.cfg&lt;/code&gt; and &lt;code&gt;pyproject.toml&lt;/code&gt; are declarative and parse cleanly, so the holdout is specifically the older &lt;code&gt;setup.py&lt;/code&gt; repos, and even the literal case is a best-effort heuristic rather than a guarantee. This is not a footnote you have to take on faith, it shows up in the consumer view as a lower confidence score on &lt;code&gt;setup.py&lt;/code&gt; rows than on the declarative ones. &lt;code&gt;all-repos-depends&lt;/code&gt; hit exactly this wall and drew exactly this line.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The same name is not always your package.&lt;/strong&gt; A repo whose index routing is wrong, or missing, can resolve a &lt;em&gt;public&lt;/em&gt; package that happens to share your internal name, which looks identical in the manifest and is not your code. This is not hypothetical: &lt;a href=&quot;https://pip.pypa.io/en/stable/cli/pip_install/&quot;&gt;pip&apos;s own documentation warns that &lt;code&gt;--extra-index-url&lt;/code&gt; is unsafe precisely because a public index can serve a package with the same name as your private one&lt;/a&gt;, the dependency-confusion problem. So the name in a manifest is a claim, not proof. Binding the name to your repo safely means resolving it to the in-org repo that actually produces that package, not assuming every matching string is yours. A name nothing in your org produces is an external dependency, not a consumer of your code.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The value is not always a version, and git references travel without an index.&lt;/strong&gt; A dependency&apos;s value can carry the real target, and for internal Python this is how a great deal of code travels without anyone standing up a private index at all. &lt;a href=&quot;https://peps.python.org/pep-0508/&quot;&gt;PEP 508 direct references&lt;/a&gt;, &lt;code&gt;yourco-clients @ git+https://github.com/yourco/clients.git@v2.4.1&lt;/code&gt;, resolve straight from a repo. So does &lt;code&gt;-e git+https://...#egg=yourco-clients&lt;/code&gt;, a bare &lt;code&gt;git+...&lt;/code&gt; line, a Poetry &lt;code&gt;{ git = ... }&lt;/code&gt; source, a Pipfile &lt;code&gt;{ git = ..., ref = ... }&lt;/code&gt;, and a &lt;code&gt;git+&lt;/code&gt; line inside a conda &lt;code&gt;pip:&lt;/code&gt; block. Every one of those points at a repo, with the committish standing in for the version. A purely local &lt;code&gt;./libs/shared&lt;/code&gt; or a &lt;code&gt;file:&lt;/code&gt; install carries no cross-repo signal, and a plain wheel or sdist URL is not a repo edge either. So the honest split is: git references are first-class consumers and resolve to a repo, while local paths and non-git URLs are not cross-repo edges at all. A scanner that reads only registry-style names misses the entire git-sourced half of how internal Python is consumed.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;constraints.txt&lt;/code&gt; and includes mean the line you grep is not always the effective version.&lt;/strong&gt; A repo can declare &lt;code&gt;yourco-clients&amp;gt;=2.4&lt;/code&gt; in &lt;code&gt;requirements.in&lt;/code&gt; and then pin it hard via a global &lt;code&gt;-c constraints.txt&lt;/code&gt; that says &lt;code&gt;yourco-clients==2.4.1&lt;/code&gt;. Or chain &lt;code&gt;-r requirements/base.txt&lt;/code&gt; so the real dependency list is assembled across several files. The line you grep is the declared constraint. The effective version, after a constraints overlay, is closer to a lock. For the question this post is about, who adopts your next release, the declared constraint in the dependency line is the load-bearing fact, and the constraints-pinned version is the adjacent, lockfile-shaped question. They are different facts, and a search that reads one file in isolation cannot tell which it is looking at.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Extras and dependency groups change the blast radius, and markers make the edge conditional.&lt;/strong&gt; A consumer that needs your package only as a PEP 621 &lt;code&gt;[project.optional-dependencies]&lt;/code&gt; extra, a Poetry &lt;code&gt;group.test&lt;/code&gt;, or a PEP 735 dev group is a weaker consumer than one that imports it at runtime in production. And a &lt;a href=&quot;https://peps.python.org/pep-0508/&quot;&gt;PEP 508 marker&lt;/a&gt; makes the dependency conditional outright: &lt;code&gt;yourco-clients&amp;gt;=2.4; python_version &amp;gt;= &quot;3.11&quot;&lt;/code&gt;, or &lt;code&gt;; sys_platform == &quot;linux&quot;&lt;/code&gt;. So a consumer may depend on you only inside an extra nobody installs in production, or only on a platform they do not ship. Flattening every declaration into one undifferentiated &quot;depends on&quot; both overstates and understates the blast radius, depending on which way you are wrong. The scope of each declaration, runtime against dev against optional, is part of the answer, not noise to discard.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Namespace packages mean the import namespace is not the unit of dependency.&lt;/strong&gt; &lt;a href=&quot;https://peps.python.org/pep-0420/&quot;&gt;PEP 420 implicit namespace packages&lt;/a&gt; let &lt;code&gt;yourco.clients&lt;/code&gt; and &lt;code&gt;yourco.auth&lt;/code&gt; live in separate distributions, in separate repos, under one shared &lt;code&gt;yourco&lt;/code&gt; namespace. The distributions ship and version independently, so the unit of dependency is the distribution, &lt;code&gt;yourco-clients&lt;/code&gt; or &lt;code&gt;yourco-auth&lt;/code&gt;, not the &lt;code&gt;yourco&lt;/code&gt; namespace they share. A tool that treats the top-level import namespace as one package conflates things that release on different schedules. This is the same crack the distribution-versus-import-name beat opened: the manifest layer is about which distribution you declared, and &quot;who imports &lt;code&gt;yourco.auth&lt;/code&gt;&quot; is a question one level down, at the symbol layer.&lt;/p&gt;
&lt;h2&gt;What the full answer requires&lt;/h2&gt;
&lt;p&gt;To reliably answer &quot;who consumes this internal Python package,&quot; you need a system that:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Scans every repo in the org&lt;/strong&gt;, parsing each manifest family (&lt;code&gt;requirements.txt&lt;/code&gt; and its &lt;code&gt;requirements-*.txt&lt;/code&gt; and split &lt;code&gt;requirements/&lt;/code&gt; and pip-tools &lt;code&gt;.in&lt;/code&gt; variants, &lt;code&gt;pyproject.toml&lt;/code&gt; across the PEP 621, Poetry, and PEP 735 dialects, &lt;code&gt;setup.cfg&lt;/code&gt;, &lt;code&gt;setup.py&lt;/code&gt;, &lt;code&gt;Pipfile&lt;/code&gt;, and conda &lt;code&gt;environment.yml&lt;/code&gt;), without requiring each team to opt in or register&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Normalises distribution names per PEP 503&lt;/strong&gt;, so &lt;code&gt;-&lt;/code&gt;, &lt;code&gt;_&lt;/code&gt;, &lt;code&gt;.&lt;/code&gt;, and case variants bind to one project, and resolves each declared dependency to the repo that actually produces the package, so a public package sharing your internal name resolves as external rather than binding to the wrong repo&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reads the value, not just the name&lt;/strong&gt;, resolving PEP 508 direct references, &lt;code&gt;git+https&lt;/code&gt; and &lt;code&gt;-e&lt;/code&gt; git installs, Poetry and Pipfile git sources, and conda &lt;code&gt;pip:&lt;/code&gt; git lines to the right in-org repo, with the committish recorded as the version&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Keeps real cross-repo references&lt;/strong&gt; while dropping purely local &lt;code&gt;file:&lt;/code&gt; and &lt;code&gt;./path&lt;/code&gt; installs and plain wheel or sdist URLs that carry no cross-repo signal&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Records the scope of each declaration&lt;/strong&gt;, runtime against dev against optional or extra, so a test-only or extras-only consumer is not weighed the same as a runtime one&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Leaves test, example, and fixture trees out of the consumer count&lt;/strong&gt;, so a repo that imports your package only in a test harness does not read as a production consumer&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reports the constraint each repo declares&lt;/strong&gt;, which is the fact that governs who adopts your next release, rather than the exact version a lockfile resolved this minute&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Stays current through rescans&lt;/strong&gt;, rather than a one-time snapshot that is stale the moment a manifest changes&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This is one of the specific problems &lt;a href=&quot;https://riftmap.dev&quot;&gt;Riftmap&lt;/a&gt; is built to solve. It connects to your GitHub or GitLab organisation with one read-only token, scans every repo, and parses &lt;code&gt;requirements.txt&lt;/code&gt;, split &lt;code&gt;requirements/&lt;/code&gt; trees and pip-tools &lt;code&gt;.in&lt;/code&gt; inputs, &lt;code&gt;pyproject.toml&lt;/code&gt; across PEP 621, Poetry groups and PEP 735 dependency groups, &lt;code&gt;setup.cfg&lt;/code&gt;, &lt;code&gt;setup.py&lt;/code&gt;, &lt;code&gt;Pipfile&lt;/code&gt;, and conda &lt;code&gt;environment.yml&lt;/code&gt; including the nested &lt;code&gt;pip:&lt;/code&gt; block. It normalises names per PEP 503 and resolves each declared dependency to the repo that produces the package, so a name nothing in your org produces is treated as external rather than a consumer. It reads the value rather than just the name, so a &lt;code&gt;pip install git+https://gitlab.yourco.com/platform/yourco-clients.git&lt;/code&gt; resolves to the &lt;code&gt;platform/yourco-clients&lt;/code&gt; repo and is recorded as a consumer with no private index required, while purely local &lt;code&gt;file:&lt;/code&gt; installs and plain wheel URLs are skipped. Each edge carries the constraint the consumer declares and the manifest line where it lives. Parsed from what each repo declares, not inferred from what the index happened to serve.&lt;/p&gt;
&lt;p&gt;A few honest limits, in the spirit of the rest of this series. Riftmap reads the declared dependency in the manifest, not the resolved lockfile tree, so it shows the constraint each repo declares, which governs who adopts your next release, rather than the exact version each one has installed right now. It binds at the distribution layer, who declares a dependency on the package, not the import-symbol layer, which repos &lt;code&gt;import&lt;/code&gt; the specific function or class you are changing. For that symbol-level question a symbol graph like &lt;a href=&quot;https://sourcegraph.com/&quot;&gt;Sourcegraph&lt;/a&gt; is the right tool, and a complementary one, because symbol graphs and artifact dependency graphs are different categories. It records a runtime, dev, or optional scope for each declaration, though surfacing that distinction in the consumer view is still on the near-term roadmap, so the panel today shows the manifest, the line, and the constraint rather than a scope label. And because Python has no &lt;code&gt;@scope&lt;/code&gt; convention the way npm does, Riftmap recognises an internal package by the fact that some repo in the scanned org produces it, not by a name prefix. A &lt;code&gt;yourco-&lt;/code&gt; prefix is a useful convention, not a guarantee.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://riftmap.dev/images/blog/python-internal-package-dependents.png&quot; alt=&quot;Riftmap Dependents panel for the internal Python package python-auth, produced by the repo polaris-python-auth, listing the repositories in the org that declare a dependency on it. The Dependents tab shows 17 consumers, the first nine visible. Each row carries a Python source badge, a confidence score, the consumer repository, the producing repository, and on the right the manifest file and line where the dependency is declared plus the version constraint. The rows span the range of Python manifest formats for one package: analytics-api declares it in both pyproject.toml and setup.cfg; migration-scripts, payment-gateway-adapter and etl-pipelines in pyproject.toml; ml-models in a conda environment.yml; payment-worker with an exact pin ==2.4.1; crm-integration in a legacy setup.py scored at 90% — lower than the 100% declarative rows because the setup.py parse is an AST heuristic; and route-optimizer with a Poetry caret ^2.0.0. Exact pins, a Poetry caret, a conda block and a lower-confidence setup.py row side by side: the divergent-constraint picture a single grep cannot assemble.&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The result is the view the rest of this series describes. Before you drop that parameter, rename that export, or cut &lt;code&gt;3.0.0&lt;/code&gt;, you open the graph, click the package, and read the consumer list: every repo that declares a dependency on it, the constraint each one carries, the manifest and line where the dependency lives, whether it came from a registry name or a git reference, and which team owns it. You know who breaks. You know who is riding a loose constraint and will pull your next minor on their next Docker rebuild whether you meant them to or not. You know who is stranded on the old major and needs a migration before you can &lt;a href=&quot;https://riftmap.dev/blog/deprecate-internal-library-find-consumers/&quot;&gt;deprecate it&lt;/a&gt;. No clean-installing thirty repos to run &lt;code&gt;pipdeptree&lt;/code&gt;. No script juggling nine manifest shapes. No waiting to see whose build goes red.&lt;/p&gt;
&lt;h2&gt;The dependency was never written down in one place&lt;/h2&gt;
&lt;p&gt;Here is the closing thought. With the other ecosystems in this series, the reverse question is hard because the consumer graph lives behind access control, or in a proxy that logs the wrong event. That is true for Python too. But Python adds a second, deeper reason, and it is the one worth sitting with.&lt;/p&gt;
&lt;p&gt;Python gives you a dozen honest ways to declare a dependency, and two different names for every package, and no single place that reconciles them. The dependency on your library exists, but it was never written down in one canonical form. It is a constraint in a &lt;code&gt;requirements.txt&lt;/code&gt; here, a PEP 621 entry there, a Poetry caret somewhere else, a &lt;code&gt;git+https&lt;/code&gt; reference in a fourth repo, all under a distribution name that does not match the import name, half of them rerouted by a constraints file you have to read separately. The reverse question is not hard because Python is messy. It is hard because the thing you are trying to find was never recorded as one thing. It lives spread across nine manifest shapes and two namespaces, in the relationship between repos that no single checkout contains. That was never a property of pip, or of PyPI, or of your index. It was a property of asking the question from inside one repo, when the answer was always somewhere between them.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This is the eighth post in the &lt;a href=&quot;https://riftmap.dev/blog/series/find-every-consumer/&quot;&gt;Find Every Consumer&lt;/a&gt; series. Previous posts cover &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-docker-base-image/&quot;&gt;Docker base images&lt;/a&gt;, &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-terraform-module/&quot;&gt;Terraform modules&lt;/a&gt;, &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-github-actions-workflow/&quot;&gt;GitHub Actions workflows&lt;/a&gt;, &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-helm-chart/&quot;&gt;Helm charts&lt;/a&gt;, &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-go-module/&quot;&gt;Go modules&lt;/a&gt;, &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-gitlab-ci-template/&quot;&gt;GitLab CI templates&lt;/a&gt; and &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-internal-npm-package/&quot;&gt;internal npm packages&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;If this is a problem your platform team deals with, I would be interested to hear how you are solving it today. You can find more at &lt;a href=&quot;https://riftmap.dev&quot;&gt;riftmap.dev&lt;/a&gt; or reach me at the address on the &lt;a href=&quot;https://riftmap.dev/about/&quot;&gt;about page&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, Python, Go, npm, and more. One read-only token. No YAML to maintain.&lt;/p&gt;
</content:encoded><category>python</category><category>platform-engineering</category><category>devops</category><category>dependency-management</category><author>Daniel Westgaard</author></item><item><title>A CVE just hit your base image. Your scanner won&apos;t tell you which repos to fix.</title><link>https://riftmap.dev/blog/base-image-cve-which-repos-to-fix/</link><guid isPermaLink="true">https://riftmap.dev/blog/base-image-cve-which-repos-to-fix/</guid><description>A scanner finds the vulnerable base image — not the repos you patch. Why base-image CVE remediation is a cross-repo source problem, and how to get the list.</description><pubDate>Mon, 15 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;In January 2026, &lt;a href=&quot;https://security-tracker.debian.org/tracker/CVE-2026-0861&quot;&gt;CVE-2026-0861&lt;/a&gt; landed in glibc. An integer overflow in the &lt;code&gt;memalign&lt;/code&gt; family, rated high, present in every glibc from 2.30 to 2.42. Which is to say: present in &lt;code&gt;debian:bookworm-slim&lt;/code&gt;, and in the default &lt;code&gt;python&lt;/code&gt;, &lt;code&gt;node&lt;/code&gt;, and &lt;code&gt;golang&lt;/code&gt; tags, all of which are Debian underneath. The &lt;code&gt;-alpine&lt;/code&gt; variants dodged this one, because musl is not glibc. Everything else inherited it.&lt;/p&gt;
&lt;p&gt;If you ran a scanner across your registry, you knew within the hour. The dashboard went red. Trivy, Grype, Docker Scout, whichever one you use, they are good at this now. The CVE is high. It is in your base. Forty images flagged.&lt;/p&gt;
&lt;p&gt;And then you sit there with the one question the dashboard does not answer. Which repositories do I open a pull request in.&lt;/p&gt;
&lt;p&gt;Those feel like the same question. A scanner found the vulnerable image, so surely it can point me at the fix. They are not the same question, and the gap between them is the whole reason a base-image CVE takes three days instead of an afternoon.&lt;/p&gt;
&lt;p&gt;Detecting a vulnerable image and knowing where to fix it are two different jobs. The first is an inventory of what is wrong. It is computed from the image, on the registry side or the runtime side, by reading the layers and matching package versions against an advisory feed. The second is a map of where the edit goes. And the edit does not go into the image. It goes into a Dockerfile, in a repository, that a person owns. Those repositories are a different list, derived from a different source, and your scanner never saw them.&lt;/p&gt;
&lt;p&gt;This post is about that second list, and why the tools that produce the first one structurally cannot produce it.&lt;/p&gt;
&lt;h2&gt;What the scanner actually knows&lt;/h2&gt;
&lt;p&gt;I want to be fair to the scanners, because they are genuinely excellent and the criticism here is narrow.&lt;/p&gt;
&lt;p&gt;Take Docker Scout, the most capable of them at the remediation end. Point it at an image and it builds an SBOM, matches every package against CVE feeds, and shows you the vulnerabilities ranked by severity. Run &lt;code&gt;docker scout recommendations&lt;/code&gt; and it will tell you the base is out of date and which newer tag clears the most CVEs, sometimes as specific as &quot;this tag fixes three&quot;. It ships an Up-to-Date Base Images policy that flags images still sitting on a stale base. With provenance attestations it identifies the exact base image and digest you built from. And with the GitHub integration wired up, it can &lt;a href=&quot;https://docs.docker.com/scout/policy/remediation/&quot;&gt;open the remediation pull request for you&lt;/a&gt;, straight from the dashboard. That is real, and it is good.&lt;/p&gt;
&lt;p&gt;Trivy and Grype sit a little further back, by design. You point them at a target. An image, a filesystem, an SBOM, a running cluster. They tell you what is vulnerable in that target. Same shape. The unit of work is a thing you hand them, and the output is the verdict on that thing.&lt;/p&gt;
&lt;p&gt;And before someone says Renovate already handles this: partly, and it is worth being precise about which part. Renovate and Dependabot will open a base-image bump in each repo they are configured on, one repo at a time. That is genuinely useful, and it is the other half of remediation, the mechanical edit. But they operate per repo and tell you nothing about the consumer set as a whole. They will not tell you that forty repos share this base, which of them are on which tag, who owns each, or that the real first move is a shared internal base two hops up. They keep versions current. They do not give you the &lt;a href=&quot;https://riftmap.dev/blog/ai-doesnt-understand-blast-radius/&quot;&gt;blast radius&lt;/a&gt;. An SBOM has the same problem in the other direction: it is an inventory of what is inside one image, the contents of an artefact rather than the consumers of it.&lt;/p&gt;
&lt;p&gt;Now notice what the unit is in every one of these. It is an image. Or it is one repository, the one that built a given image, reached from the image through its provenance. Scout&apos;s image hierarchy is the ancestry of the image in front of you: what it was built &lt;code&gt;FROM&lt;/code&gt;, going up. That is a real and useful relationship, and it runs in exactly the wrong direction for the question you are now asking.&lt;/p&gt;
&lt;h2&gt;The relationship you need runs the other way&lt;/h2&gt;
&lt;p&gt;A base-image CVE does not ask what this image was built from. It asks who is built on top of this base. That is the inverse relationship, and it is not one a scanner can give you, because it is not visible from any single image.&lt;/p&gt;
&lt;p&gt;Scout, at its best, maps an image to the one repository that produced it. One image, one source repo, via the attestation. That is a one-to-one link, and it answers &quot;where did this image come from&quot;.&lt;/p&gt;
&lt;p&gt;What a base-image CVE forces is a one-to-many link. One base image, every repository in the organisation whose Dockerfile declares &lt;code&gt;FROM&lt;/code&gt; it. Twelve repos, forty, a hundred and ten. Each pinning a different tag. Each owned by a different team. Some built on the base directly, some built on an internal image that is itself built on the base. That fan-out is the remediation topology, and it lives in &lt;code&gt;FROM&lt;/code&gt; lines spread across every repo you have. Not in the registry. Not in the runtime. Not in any one image&apos;s SBOM. In source.&lt;/p&gt;
&lt;h2&gt;&quot;What&apos;s running&quot; is the wrong index for &quot;what to change&quot;&lt;/h2&gt;
&lt;p&gt;The reflex is to reach for the registry or the cluster, because that is where the scanner already looks. Both are the wrong index, and it is worth being precise about why, because the reasons are not edge cases.&lt;/p&gt;
&lt;p&gt;The registry knows which images exist and, with provenance, what each was built from. The runtime knows what is deployed right now. Neither is the set of &lt;code&gt;FROM&lt;/code&gt; lines in your repositories, and the divergence shows up immediately.&lt;/p&gt;
&lt;p&gt;A repo whose image is not currently deployed still has a vulnerable Dockerfile, and it will rebuild the vulnerable base on its next merge. The runtime cannot see it. The tag a Dockerfile pins is frequently not a literal: it is &lt;code&gt;FROM ${REGISTRY}/base:${BASE_VERSION}&lt;/code&gt;, resolved at build time from an &lt;code&gt;ARG&lt;/code&gt; or a CI variable, so the registry&apos;s record of what was built and the repo&apos;s record of what is requested are two different strings. Internal mirrors and pull-through caches rewrite the name, so the image in your registry is &lt;code&gt;harbor.internal/library/python&lt;/code&gt; and the thing you actually have to find across your repos is &lt;code&gt;python&lt;/code&gt;. And the base you care about is often two hops up: your teams build &lt;code&gt;FROM acme/runtime-base&lt;/code&gt;, which is built &lt;code&gt;FROM debian&lt;/code&gt;, so the glibc fix has to propagate from &lt;code&gt;debian&lt;/code&gt; to &lt;code&gt;runtime-base&lt;/code&gt; to the forty leaf repos, and the scanner that flagged forty leaf images cannot tell you that the real first move is one pull request against &lt;code&gt;runtime-base&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Every one of those is a case where the inventory of what is vulnerable and the map of what to edit pull apart. The edit lands in source. So the index has to be built from source.&lt;/p&gt;
&lt;h2&gt;The fix is a graph query, not a scan&lt;/h2&gt;
&lt;p&gt;Strip the panic away and the thing you need at the moment a base-image CVE drops is small and specific. Every repository that declares a dependency on this base, directly or transitively. The tag or digest each one pins, so you can tell who is already on a patched base and who is not. The team that owns each repo, so you know who to route the pull request to. And the order, so you fix &lt;code&gt;runtime-base&lt;/code&gt; before you fix the forty repos that sit on it.&lt;/p&gt;
&lt;p&gt;That is a query against a graph of your &lt;code&gt;FROM&lt;/code&gt; edges. And the only honest way to build that graph is to parse it. Read the &lt;code&gt;FROM&lt;/code&gt; lines in every repository, resolve the &lt;code&gt;ARG&lt;/code&gt; defaults and the multi-stage &lt;code&gt;AS&lt;/code&gt; aliases and the Compose &lt;code&gt;image:&lt;/code&gt; references, normalise the internal-mirror names back to the base they point at, and connect the edges. Parsed, not inferred. Not guessed from image names that happen to look similar. Not reconstructed from a catalogue someone updated last quarter. Not pieced together from a Slack thread. Read from the files that already declare the dependency, because those files are the source of truth, and they are also exactly where your fix is going to land.&lt;/p&gt;
&lt;p&gt;The enumeration has more sharp edges than it looks: ARG-templated tags, multi-stage builds where only one stage matters, Compose files that reference the image with no Dockerfile in sight, repos that produce the base as well as consume it. I wrote up the full mechanics of parsing all of that in &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-docker-base-image/&quot;&gt;how to find every consumer of your Docker base image&lt;/a&gt;. This post is the layer above it. Not how to build the list, but why the scanner that found the CVE was never going to be the thing that hands it to you.&lt;/p&gt;
&lt;h2&gt;Even &quot;we can wait on this one&quot; needs the list&lt;/h2&gt;
&lt;p&gt;There is a version of this where the CVE turns out not to be urgent, and it is worth following through, because it makes the same point from the other side.&lt;/p&gt;
&lt;p&gt;CVE-2026-0861 is a good example. It is rated high, but exploiting it requires an attacker to control both the size and the alignment passed to &lt;code&gt;memalign&lt;/code&gt;, with the alignment pushed into a range no ordinary program ever reaches. In most services it is not practically reachable. A reasonable platform team might decide to let it ride to the next routine base bump rather than scramble at midnight.&lt;/p&gt;
&lt;p&gt;But that is a per-consumer decision, and you cannot make it without the per-consumer list. &quot;Is this reachable in our usage&quot; has a different answer in the one repo that does its own aligned allocation than in the forty that never call &lt;code&gt;memalign&lt;/code&gt; directly. To triage at all, to say these three we patch tonight and the rest wait for the monthly rebuild, you first have to know which repos those are and how each one uses the base. Deprioritising safely is not the absence of the graph. It is one of the things the graph is for. The scanner&apos;s per-image severity score tells you the CVE is high. It does not tell you it is high here, in this repo, given how this repo uses the base, and that last clause is the only one that decides whether anyone loses sleep.&lt;/p&gt;
&lt;h2&gt;The two lists&lt;/h2&gt;
&lt;p&gt;So here is the shape of it, stripped down.&lt;/p&gt;
&lt;p&gt;Your scanner produced a list: the images that are vulnerable. That list is real and you need it. But it is an inventory of what is wrong, indexed by image, computed from the registry and the runtime.&lt;/p&gt;
&lt;p&gt;The list you actually act on is a different one: the repositories that declare &lt;code&gt;FROM&lt;/code&gt; this base, with their tags and their owners and their order. That list is a map of where to go, indexed by repository, and it can only be computed from source, because source is the one place the &lt;code&gt;FROM&lt;/code&gt; edge is written down and the one place the fix can land.&lt;/p&gt;
&lt;p&gt;A scanner is very good at telling you the building has a problem. It is just not the thing that hands you the keys to the rooms you have to walk into. Those are different artefacts, and on the morning a base-image CVE drops, the second one is the only one that shortens the day.&lt;/p&gt;
&lt;p&gt;This is the query &lt;a href=&quot;https://riftmap.dev/&quot;&gt;Riftmap&lt;/a&gt; is built to answer. Point it at your GitLab or GitHub organisation with one read-only token and it parses the &lt;code&gt;FROM&lt;/code&gt; edges across every repo, resolving the ARG defaults, the multi-stage stages, and the internal-mirror names, and builds the consumer graph. When a base-image CVE drops you select the base, and you get the list: every repository on it, direct and transitive, the tag each one pins, the team that owns it. The scanner tells you the image is vulnerable. Riftmap tells you where the fix goes.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;Riftmap scans your GitHub or GitLab organisation with one read-only token, parses Terraform, Docker, Helm, Kustomize, Kubernetes, GitHub Actions, GitLab CI, Ansible, Go modules, and npm, and holds the cross-repo dependency graph as a queryable surface. The &quot;which repos consume this base&quot; query is one call. &lt;a href=&quot;https://app.riftmap.dev&quot;&gt;The free tier is here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;For the parsing detail behind the consumer query, the &lt;a href=&quot;https://riftmap.dev/blog/series/find-every-consumer/&quot;&gt;Find Every Consumer series&lt;/a&gt; walks one ecosystem at a time, starting with &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-docker-base-image/&quot;&gt;Docker base images&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;For the planned-removal version of the same problem — same fan-out, different trigger — see &lt;a href=&quot;https://riftmap.dev/blog/deprecate-internal-library-find-consumers/&quot;&gt;You deprecated the internal library. The repos still using it never saw the warning&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;And for the pre-merge version — checking which repos a change hits before the pull request is open, whether a person or an AI agent is driving — see &lt;a href=&quot;https://riftmap.dev/blog/can-ai-check-blast-radius-of-pr-before-merge/&quot;&gt;Can AI check the blast radius of a PR before you merge?&lt;/a&gt; The gate itself, a forty-line CI job that posts the consumer count on every pull request, is in &lt;a href=&quot;https://riftmap.dev/blog/blast-radius-gate-merge-pipeline/&quot;&gt;How to add a blast-radius gate to your merge pipeline&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</content:encoded><category>docker</category><category>base-image</category><category>cve</category><category>vulnerability-management</category><category>devsecops</category><category>platform-engineering</category><category>blast-radius</category><author>Daniel Westgaard</author></item><item><title>Your senior engineer just gave notice. Most of what they knew was in the repos all along.</title><link>https://riftmap.dev/blog/senior-engineer-leaving-tribal-knowledge/</link><guid isPermaLink="true">https://riftmap.dev/blog/senior-engineer-leaving-tribal-knowledge/</guid><description>Tribal knowledge is two different things wearing one name. The half everyone panics about losing was declared in your Terraform, your Dockerfiles, and your CI config the whole time.</description><pubDate>Sun, 14 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;Tribal knowledge is two different things wearing one name. The half everyone panics about losing was declared in your Terraform, your Dockerfiles, and your CI config the whole time.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;It usually starts with a calendar invite that has no agenda. Thirty minutes, your senior platform engineer, no subject line. You half know before you sit down. They have been here six years. They are leaving in a month.&lt;/p&gt;
&lt;p&gt;The first day you feel it as a personal loss, because it is one. The operational version arrives later, usually in a standup. Someone proposes bumping the base image that half the services build from. Routine work. Then somebody asks who actually knows everything that pulls from it, and the room goes quiet, and every face turns very slightly towards the person who is leaving.&lt;/p&gt;
&lt;p&gt;That quiet is the sound of a team discovering its bus factor in real time. The reflex that follows is always the same. Get it out of their head before they go. Book the knowledge-transfer sessions. Start a wiki page. Pair them with someone for the notice period and hope.&lt;/p&gt;
&lt;p&gt;I want to argue that this reflex is half right, and that the half it gets wrong is the expensive half.&lt;/p&gt;
&lt;h2&gt;The word that hides two different problems&lt;/h2&gt;
&lt;p&gt;We call it tribal knowledge, and we say it as though it were one thing. It is not. Two very different kinds of knowledge shelter under that one phrase, and the panic about a departing engineer conflates them, which is why the panic so often spends its energy in the wrong place.&lt;/p&gt;
&lt;p&gt;The first kind is genuinely tacit. It is the why. Why the payments service retries three times and not five. Which of the two cloud accounts the staging environment actually bills to, and the historical accident that explains it. Who to call at the vendor when a certificate renewal fails silently, because you have learned the hard way that the support queue will not help you. The incident two years ago whose scar tissue is the reason one config flag exists and must never be flipped. None of this is written down, and most of it cannot be derived from anything. It lives in one person. It will leave with them. This kind of knowledge is real, it is valuable, and getting it out before someone walks out the door is worth doing.&lt;/p&gt;
&lt;p&gt;The tools built for this are good at it. &lt;a href=&quot;https://swimm.io&quot;&gt;Swimm&lt;/a&gt;, &lt;a href=&quot;https://www.atlassian.com/software/confluence&quot;&gt;Confluence&lt;/a&gt;, &lt;a href=&quot;https://www.notion.com&quot;&gt;Notion&lt;/a&gt;, a decent internal wiki, an afternoon of recorded walkthroughs. The whole category exists to move the contents of a person&apos;s head into a form the organisation can read later, and for tacit knowledge that is the right move. There is a reason it so rarely happens, and it is not that teams do not care. It is that the person holding the knowledge does not know they are holding anything unusual. To them, the field that two services name differently for the same value, so that mixing them produces output that is wrong but does not error, is not a secret worth recording. It is just how the thing works. You cannot ask someone to write down what they do not know is worth writing down.&lt;/p&gt;
&lt;p&gt;So far, so familiar. Here is the part the panic misses.&lt;/p&gt;
&lt;h2&gt;Half of it was never tribal&lt;/h2&gt;
&lt;p&gt;The second kind of knowledge hiding under tribal knowledge is the structural map. Which repositories depend on which. &lt;a href=&quot;https://riftmap.dev/blog/ai-doesnt-understand-blast-radius/&quot;&gt;What breaks if the base image moves&lt;/a&gt;. Where the shared Terraform module is consumed, and by whom. Which pipelines pull the CI template you are about to edit. Which services still pin the old tag and will fail their next rebuild the moment you ship.&lt;/p&gt;
&lt;p&gt;This is what the standup was really asking for when the room went quiet. And it feels identical to the tacit kind, because it also lived in one person&apos;s head, and because losing the person feels like losing all of it at once. But it has a property the tacit kind does not, and the whole argument turns on this property.&lt;/p&gt;
&lt;p&gt;It was already written down.&lt;/p&gt;
&lt;p&gt;Not in a wiki. In the manifests. Every edge your departing engineer carried in their head was declared somewhere in the source, by someone, on purpose. The base-image relationship is a &lt;code&gt;FROM&lt;/code&gt; line in a Dockerfile. The module relationship is a &lt;code&gt;source&lt;/code&gt; block in Terraform. The chart dependency is a &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-helm-chart/&quot;&gt;value reference in a Helm chart&lt;/a&gt;. The pipeline relationship is an &lt;code&gt;include&lt;/code&gt; in GitLab CI or a reusable workflow in GitHub Actions. The library relationship is a require in a &lt;code&gt;go.mod&lt;/code&gt; or a line in a lockfile. None of these are tacit. They are facts in plain text, in repositories you already own, waiting for someone to read them.&lt;/p&gt;
&lt;p&gt;So why did it ever feel like tribal knowledge? Because nobody else had read all of it. Reading every &lt;code&gt;FROM&lt;/code&gt; line and every &lt;code&gt;source&lt;/code&gt; block and every &lt;code&gt;include&lt;/code&gt; across two hundred repositories, and holding the result in your head as one connected graph, is most of a person&apos;s job for a very long time. Your senior did not do it in a sitting. They accreted it, one incident and one migration and one code review at a time, over six years, until they had quietly become the index. When they leave, the index leaves. But the thing the index pointed at, the actual declared structure, is sitting in the repos exactly where it was this morning, entirely unchanged by their resignation.&lt;/p&gt;
&lt;p&gt;That is the difference that matters. You genuinely cannot regenerate the why from the source. You can absolutely regenerate the what-depends-on-what from the source, because it was never anywhere else to begin with. One is a memory problem. The other is a parsing problem. The panic treats them as one problem, reaches for a memory solution, the wiki and the handover session, and points it at the thing that was a parsing problem all along.&lt;/p&gt;
&lt;p&gt;I want to be precise about the boundary, because this audience will catch me if I am not. Not every coupling between two systems is declared in a manifest. If one service calls another over an internal endpoint that appears in neither side&apos;s configuration, no parser will find that edge, and your senior may well have carried it too. That sort of runtime coupling belongs closer to the tacit pile, and it is worth getting onto a diagram while you still can. But the heavy, expensive structure, the build and deploy and infrastructure substrate that everything else stands on, is overwhelmingly declared. That is the part that looks lost when someone leaves and is not.&lt;/p&gt;
&lt;h2&gt;The handover is the wrong place to rebuild a map&lt;/h2&gt;
&lt;p&gt;Watch what most teams do with the weeks they have left. They put the departing engineer in a room and ask them to draw the dependency diagram. Map the services. List what depends on the shared module. Write the runbook for the base-image bump. It feels responsible. It is mostly waste, for three reasons.&lt;/p&gt;
&lt;p&gt;The first we have already met. They do not know which edges are load-bearing, because to them every edge is just true. They will lovingly document the interesting parts, the clever bits they are proud of, and they will not think to mention the dull tag pin in a sleepy repository that has not changed in a year and will take production down the first time someone bumps the image. The boring edges are the ones that bite. The boring edges are exactly the ones a human brain-dump skips.&lt;/p&gt;
&lt;p&gt;The second is that the diagram is stale the moment it is drawn. It is accurate on the day. Then the first migration after they leave moves something, the diagram does not move with it, and nothing tells you it has drifted. Platform teams have been rediscovering this for years under another name. It is the same reason service catalogs rot, and the same reason so many Backstage rollouts quietly stall, which I &lt;a href=&quot;https://riftmap.dev/blog/backstage-alternatives/&quot;&gt;went through in detail when writing about developer portals&lt;/a&gt;. A hand-maintained model of how the system fits together is only ever as accurate as the last person who remembered to update it, and people stop remembering. Developer portals solve real problems and the teams that adopt them are not naive. The catalog rots anyway. A dependency map drawn by hand is a service catalog with a bus factor of one, drawn by the very person who is about to leave.&lt;/p&gt;
&lt;p&gt;The third reason is the one that actually matters, and it is why this is not just a tooling preference. The notice period is the single most scarce resource you will have for a long time, and you are spending it on the one kind of knowledge a machine could have reconstructed for nothing, while short-changing the kind that genuinely needed a human. Every hour your senior spends drawing boxes and arrows a parser could have produced in a minute is an hour they are not spending on the why. The vendor contact. The incident scar tissue. The flag that must never flip. That is the knowledge that walks out for good, that the handover should exist to protect, and that gets crowded out because everyone is busy rebuilding a map which was in the repositories the entire time.&lt;/p&gt;
&lt;h2&gt;Keep them for what only they know&lt;/h2&gt;
&lt;p&gt;The fix is not a better wiki and it is not a more disciplined handover. It is to stop treating two different problems as one. Separate the piles.&lt;/p&gt;
&lt;p&gt;The tacit pile, the why, is what the human&apos;s last weeks are for. Sit with them. Record it. Ask the awkward questions about the flag and the vendor and the account. That time is irreplaceable, you will not get it back, so protect it from being eaten by box-drawing.&lt;/p&gt;
&lt;p&gt;The structural pile, the what-depends-on-what, does not need the human at all. It needs something to read the manifests across the whole organisation and assemble them into the graph your senior had been assembling by hand. The edges are declared. The only thing ever missing was someone, or something, that had read all of them at once, and kept reading after the person left.&lt;/p&gt;
&lt;p&gt;This is the part of the problem I build for. &lt;a href=&quot;https://riftmap.dev&quot;&gt;Riftmap&lt;/a&gt; reads the declared dependencies across an entire GitHub or GitLab organisation, Terraform, Docker, Helm, CI, package manifests, and builds the &lt;a href=&quot;https://riftmap.dev/what-is-cross-repo-dependency-mapping/&quot;&gt;cross-repo dependency graph&lt;/a&gt; from the source itself, with one read-only token and no catalog to maintain. It is the map your senior held, reconstructed deterministically, and kept current after they are gone, because it re-reads the repositories rather than trusting a diagram somebody drew in their final week. Ask it what breaks if you bump the base image and the answer comes from what the repositories declare today, not from what anyone remembered to write down in March. This is less a new idea than an obvious one once you see the split. Even Meta, with effectively unlimited engineers, landed in the same place on their own pipelines and &lt;a href=&quot;https://engineering.fb.com/2026/04/06/developer-tools/how-meta-used-ai-to-map-tribal-knowledge-in-large-scale-data-pipelines/&quot;&gt;generated a cross-repo dependency index&lt;/a&gt; rather than asking people to maintain a map by hand.&lt;/p&gt;
&lt;p&gt;There is a second thing the graph gives you, and if you are the one who just received the resignation it is the thing I would lead with. Once the structure is parsed, you can ask a question the departing engineer could never have answered honestly about themselves. Which of the repositories that everything else depends on are maintained by exactly one person. The high-blast-radius, single-maintainer substrate. The build image, the shared CI template, the base module the whole organisation leans on, that it turns out precisely one human has touched in a year. That is your next resignation, visible before it arrives. An engineer I have a lot of respect for, &lt;a href=&quot;https://www.linkedin.com/in/owenzanzal/&quot;&gt;Owen Zanzal&lt;/a&gt;, pushed me towards this framing, and it is worth a post of its own, which is coming. The short version is that the moment you have the dependency graph, ownership stops being a question of who wrote the code and becomes a question of who maintains the things everyone else is standing on.&lt;/p&gt;
&lt;h2&gt;The map did not leave&lt;/h2&gt;
&lt;p&gt;When the person who understood how everything fit together hands in their notice, it feels as though the map is leaving with them. It is not. The map was in your manifests the whole time. They were simply the only one who had read all of it. Keep their last weeks for the things only they know. The rest was never theirs to take.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Related reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://riftmap.dev/blog/meta-tribal-knowledge-engine-build-the-graph-first/&quot;&gt;Meta needed 50+ AI agents to map their tribal knowledge&lt;/a&gt; — The machine-scale version of this same split: build the deterministic dependency index first, reserve the AI swarm for the semantic layer that actually decays.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://riftmap.dev/blog/backstage-alternatives/&quot;&gt;Backstage alternatives in 2026: first ask why you wanted Backstage&lt;/a&gt; — Why no developer portal answers the dependency-graph question, and the maintenance trap that makes a hand-drawn map go stale.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://riftmap.dev/blog/ai-doesnt-understand-blast-radius/&quot;&gt;AI Doesn&apos;t Understand Blast Radius&lt;/a&gt; — What actually breaks when you change a shared component without the cross-repo graph in front of you.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://riftmap.dev/blog/the-catalog-maintenance-trap/&quot;&gt;The catalog maintenance trap: why service catalogs go stale&lt;/a&gt; — The structural reason a map a human maintains is wrong exactly when a risky change makes you need it.&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>tribal-knowledge</category><category>platform-engineering</category><category>knowledge-transfer</category><category>key-person-risk</category><category>cross-repo-dependencies</category><category>dependency-graph</category><author>Daniel Westgaard</author></item><item><title>How to Find Every Consumer of Your Internal npm Package</title><link>https://riftmap.dev/blog/how-to-find-every-consumer-of-your-internal-npm-package/</link><guid isPermaLink="true">https://riftmap.dev/blog/how-to-find-every-consumer-of-your-internal-npm-package/</guid><description>You maintain an internal npm package and a breaking change is coming. Which repos depend on it, at which version — and why npm answers this for public packages but not for your scoped ones.</description><pubDate>Sat, 13 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;You maintain a scoped, internal npm package on a private registry. You need to change its API. Which repos across the org depend on it, and at which version? The public npm ecosystem has tools for this question. The moment you put an &lt;code&gt;@&lt;/code&gt; in front of the name and point an &lt;code&gt;.npmrc&lt;/code&gt; at your own registry, every one of them stops working.&lt;/em&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;Open the page for any popular public package on &lt;a href=&quot;http://npmjs.com&quot;&gt;npmjs.com&lt;/a&gt; and click the Dependents tab. For &lt;a href=&quot;https://www.npmjs.com/package/lodash&quot;&gt;&lt;code&gt;lodash&lt;/code&gt;&lt;/a&gt; you get a number in the hundreds of thousands. The data is incomplete and the ranking is crude, but the question &quot;who depends on this&quot; has a first-class home in the public registry, and a small ecosystem around it: &lt;a href=&quot;https://libraries.io/&quot;&gt;libraries.io&lt;/a&gt;, Google&apos;s &lt;a href=&quot;https://deps.dev/&quot;&gt;deps.dev&lt;/a&gt;, a dozen abandoned &lt;code&gt;npm-dependents&lt;/code&gt; CLIs on the registry itself.&lt;/p&gt;
&lt;p&gt;Now scope the package. Rename it from &lt;code&gt;confit&lt;/code&gt; to &lt;code&gt;@yourco/confit&lt;/code&gt;, set its access to restricted, and publish it to Verdaccio or the GitLab package registry instead of &lt;code&gt;registry.npmjs.org&lt;/code&gt;. Everything you just relied on evaporates. There is no Dependents tab, because &lt;a href=&quot;http://npmjs.com&quot;&gt;npmjs.com&lt;/a&gt; never sees the package. &lt;a href=&quot;http://libraries.io&quot;&gt;libraries.io&lt;/a&gt; and deps.dev crawl the public registry, so they are blind to it too. And there is no local fallback either: as the maintainer of one of those abandoned reverse-lookup tools put it, &lt;a href=&quot;https://www.npmjs.com/package/npm-dependents&quot;&gt;there is no npm command for retrieving a list or count of modules that depend on a given package&lt;/a&gt;, and the registry&apos;s own dependents data only &lt;a href=&quot;https://www.npmjs.com/package/npm-list-package-dependents&quot;&gt;covers non-scoped public packages&lt;/a&gt; anyway.&lt;/p&gt;
&lt;p&gt;So the reverse question is answered for the packages that can&apos;t hurt you and unanswered for the ones that can. The package you scoped and locked down, the shared UI kit or the internal SDK that fifteen of your services import, the one whose breaking change is actually your problem, is precisely the one with no consumer view at all. This post is about getting that view back.&lt;/p&gt;
&lt;h2&gt;The scenario&lt;/h2&gt;
&lt;p&gt;Your platform or frontend-platform team publishes &lt;code&gt;@yourco/ui&lt;/code&gt;. Maybe it&apos;s a component library every web app imports. Maybe it&apos;s &lt;code&gt;@yourco/sdk&lt;/code&gt;, a generated client for your internal APIs. Maybe it&apos;s &lt;code&gt;@yourco/config&lt;/code&gt;, a thin package that standardises ESLint, tsconfig, and build settings across the org so nobody hand-rolls them.&lt;/p&gt;
&lt;p&gt;It started as a way to stop copy-pasting. A few apps adopted it. Then more. Consumers declare it the ordinary way, in &lt;code&gt;package.json&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;name&quot;: &quot;@yourco/checkout-web&quot;,
  &quot;dependencies&quot;: {
    &quot;@yourco/ui&quot;: &quot;^4.2.0&quot;,
    &quot;@yourco/sdk&quot;: &quot;~2.1.0&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Because it&apos;s scoped and private, every consumer also carries an &lt;code&gt;.npmrc&lt;/code&gt; that tells the package manager where &lt;code&gt;@yourco&lt;/code&gt; resolves from:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@yourco:registry=https://gitlab.yourco.com/api/v4/packages/npm/
//gitlab.yourco.com/api/v4/packages/npm/:_authToken=${NPM_TOKEN}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Some consumers pin a caret range and float across minors. Some pin tighter. Some live in a monorepo and reference the package over the &lt;a href=&quot;https://pnpm.io/workspaces&quot;&gt;&lt;code&gt;workspace:&lt;/code&gt; protocol&lt;/a&gt; instead of through the registry at all:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;dependencies&quot;: {
    &quot;@yourco/ui&quot;: &quot;workspace:*&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And the version each of them actually has installed lives somewhere else entirely, in a lockfile, which may say something quite different from the range in &lt;code&gt;package.json&lt;/code&gt;. Twenty repos adopted the package. Then you stopped counting, because nothing in your toolchain counts for you. Now you need to change it. Drop a prop, restructure an export, bump a major. The question is the one that runs through &lt;a href=&quot;https://riftmap.dev/blog/series/find-every-consumer/&quot;&gt;every post in this series&lt;/a&gt;: &lt;strong&gt;which repos across the org depend on this package, at which version, and which of them break when I publish?&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;The change you ship without shipping it&lt;/h2&gt;
&lt;p&gt;Before the tooling, the part that makes this sharper than it first looks. A caret range is a standing instruction to adopt your next release automatically.&lt;/p&gt;
&lt;p&gt;A consumer on &lt;code&gt;&quot;@yourco/ui&quot;: &quot;^4.2.0&quot;&lt;/code&gt; is not pinned to 4.2.0. They are subscribed to every 4.x you publish. The next time their CI runs a fresh install, or someone deletes a lockfile and reinstalls, they pull whatever the newest 4.x is and build against it. You did not roll it out. They did not opt in. If your &quot;harmless&quot; 4.3.0 minor carries a regression, you have just deployed it across every caret consumer on their schedule, not yours, and the first you hear of it is someone else&apos;s red pipeline.&lt;/p&gt;
&lt;p&gt;The reverse case is just as bad in the other direction. When you do the honest thing and ship a genuine breaking change as a major, &lt;code&gt;4.x&lt;/code&gt; to &lt;code&gt;5.0.0&lt;/code&gt;, caret ranges correctly refuse to follow. Which means every consumer stays on 4.x until a human edits their &lt;code&gt;package.json&lt;/code&gt; and migrates. That is the right behaviour, but it leaves you with a long tail of repos stranded on the old major, indefinitely, and no list of who they are. You cannot deprecate 4.x because you cannot see who is still on it.&lt;/p&gt;
&lt;p&gt;Either way, the version range is the mechanism, and the version range is exactly what a quick search across your repos cannot evaluate. You need to know who consumes the package and how their range relates to what you are about to publish. Both halves of that live in files most audits never open.&lt;/p&gt;
&lt;h2&gt;What existing tools give you (and where they stop)&lt;/h2&gt;
&lt;p&gt;I want to be fair to the options, because several are genuinely useful for the slice they cover.&lt;/p&gt;
&lt;h3&gt;&lt;a href=&quot;http://npmjs.com&quot;&gt;npmjs.com&lt;/a&gt; Dependents, deps.dev, &lt;a href=&quot;http://libraries.io&quot;&gt;libraries.io&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;For public packages these are the right tools, and I&apos;d point you straight at them. The structural problem is not that they&apos;re bad. It&apos;s that they are properties of the &lt;em&gt;public&lt;/em&gt; registry. A private scoped package is access-controlled by design, served from your own registry behind a token, and never indexed by anything that crawls &lt;code&gt;registry.npmjs.org&lt;/code&gt;. The same access control that keeps your code off the public internet keeps it off every public consumer graph. There is nothing to fix here. The data simply isn&apos;t reachable, on purpose.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;npm ls&lt;/code&gt;, &lt;code&gt;npm explain&lt;/code&gt;, &lt;code&gt;npm why&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;These do answer a reverse question, and people reach for them first. &lt;code&gt;npm explain &amp;lt;pkg&amp;gt;&lt;/code&gt; (and &lt;code&gt;yarn why&lt;/code&gt; or &lt;code&gt;pnpm why&lt;/code&gt; in the other managers) tells you why a package is present in the current install: which dependency path pulled it in. It&apos;s the right tool for &quot;why is this transitive thing in my tree.&quot;&lt;/p&gt;
&lt;p&gt;But it operates on one installed project at a time, outward from that project&apos;s own &lt;code&gt;node_modules&lt;/code&gt;. It cannot tell you which &lt;em&gt;other repos&lt;/em&gt; in the org depend on your package. To build the org-wide view you would run it in every repo, after a clean install in each, and aggregate the output yourself. By the time you finished, the lockfiles you installed from would have moved.&lt;/p&gt;
&lt;h3&gt;The private registry itself&lt;/h3&gt;
&lt;p&gt;This is the one people assume covers them, because the registry is the thing all the packages flow through. Verdaccio describes itself as a &lt;a href=&quot;https://www.verdaccio.org/&quot;&gt;lightweight private proxy registry&lt;/a&gt;: host your private packages, cache the public ones, serve both from one endpoint. The GitLab package registry, &lt;a href=&quot;https://docs.gitlab.com/user/packages/npm_registry/&quot;&gt;Artifactory, Nexus, and GitHub Packages&lt;/a&gt; all do the same job.&lt;/p&gt;
&lt;p&gt;They are very good at it. What none of them model is consumption at the source level. The registry records that some authenticated client downloaded &lt;code&gt;@yourco/ui@4.2.0&lt;/code&gt;. It does not record which repo&apos;s &lt;code&gt;package.json&lt;/code&gt; declared the dependency, which team&apos;s CI pipeline the install ran in, or whether the thing that pulled it was a service you care about or a throwaway branch. It&apos;s a distribution and caching layer, not a consumption graph. This is the same gap I described for &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-go-module/&quot;&gt;internal Go module proxies in the Go edition of this series&lt;/a&gt;: a proxy logs fetches, not the &lt;code&gt;go.mod&lt;/code&gt; that triggered them. The npm version is identical. The download event is not the dependency edge.&lt;/p&gt;
&lt;p&gt;(GitLab has at least started exposing what &lt;em&gt;packages&lt;/em&gt; exist at the group level, after &lt;a href=&quot;https://gitlab.com/gitlab-org/gitlab/-/issues/10003&quot;&gt;years of requests for a group-level packages API&lt;/a&gt;. That tells you what&apos;s published across a group. It still doesn&apos;t tell you who consumes it.)&lt;/p&gt;
&lt;h3&gt;Renovate and Dependabot&lt;/h3&gt;
&lt;p&gt;Both support npm as a first-class ecosystem, including private registries once you give them credentials. Because they&apos;re configured per consumer, they implicitly know which repos depend on what, and they&apos;ll open PRs to bump your package when you publish. As with &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-terraform-module/&quot;&gt;Terraform modules&lt;/a&gt; and the rest of the series, the knowledge is in there.&lt;/p&gt;
&lt;p&gt;But they&apos;re updaters, not mappers. There is no org-level &quot;show me every repo that depends on &lt;code&gt;@yourco/ui&lt;/code&gt;, and what range each one declares&quot; view to query. They react to new versions going out. The question you have before you publish a breaking one, who is currently consuming the old version and how, is not something either tool surfaces. And both only cover repos where they&apos;ve been switched on for your registry. A team that never configured private-scope auth in their Renovate config is simply invisible.&lt;/p&gt;
&lt;h3&gt;Code search, and the script&lt;/h3&gt;
&lt;p&gt;You can search your GitHub org or GitLab group for the package name:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;org:yourco &quot;@yourco/ui&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For a one-off audit, fine. It finds &lt;code&gt;package.json&lt;/code&gt; files that mention the string and gives you a starting list of repos. Then the familiar problems land all at once. It returns the declared range, not the installed version. It doesn&apos;t read the &lt;code&gt;.npmrc&lt;/code&gt; to confirm the package even resolved from your registry. It won&apos;t find a consumer that aliased the package under a different key, or one that references it over &lt;code&gt;workspace:&lt;/code&gt;. And the index lags your most recent commits.&lt;/p&gt;
&lt;p&gt;So someone writes the script. Enumerate every repo, fetch every &lt;code&gt;package.json&lt;/code&gt;, parse the JSON, pull &lt;code&gt;dependencies&lt;/code&gt; and the other three blocks, filter for your scope, then fetch the lockfiles to get real versions, handle three or four lockfile formats, run it on a schedule, store the results. People build exactly this. There&apos;s a &lt;a href=&quot;https://askgit.substack.com/p/microsofts-top-npm-dependencies&quot;&gt;nice writeup of querying every &lt;code&gt;package.json&lt;/code&gt; across Microsoft&apos;s 4,000+ public repos with SQL&lt;/a&gt; to rank their most-declared dependencies, which is the same machinery pointed at the same data. The fact that this keeps getting independently rebuilt is the strongest evidence the question matters. It&apos;s also a parser you now own, with every corner case below as your backlog.&lt;/p&gt;
&lt;h2&gt;Why this is harder than it looks&lt;/h2&gt;
&lt;p&gt;A naive search for the package name both overcounts and undercounts, because npm dependency consumption is not one fact in one place. It&apos;s spread across several constructs that each behave differently.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The declared range and the installed version are different facts.&lt;/strong&gt; &lt;code&gt;package.json&lt;/code&gt; records a range, &lt;code&gt;^4.2.0&lt;/code&gt;. What that range actually resolved to, say &lt;code&gt;4.7.1&lt;/code&gt;, lives in a lockfile, and the lockfiles diverge by manager: npm writes &lt;code&gt;package-lock.json&lt;/code&gt;, Yarn Classic and Yarn Berry write incompatible &lt;code&gt;yarn.lock&lt;/code&gt; formats, pnpm writes &lt;code&gt;pnpm-lock.yaml&lt;/code&gt;, and Bun shipped a binary &lt;code&gt;bun.lockb&lt;/code&gt; you cannot grep at all before &lt;a href=&quot;https://bun.com/docs/pm/lockfile&quot;&gt;switching its default to a text &lt;code&gt;bun.lock&lt;/code&gt;&lt;/a&gt;. For the question this post is about, who adopts your next release and who you&apos;ll have to migrate, the declared range is the load-bearing fact, and it sits in &lt;code&gt;package.json&lt;/code&gt;. The resolved version answers the adjacent question of what&apos;s installed this minute, and it&apos;s buried in whichever lockfile the repo happens to use.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The same name doesn&apos;t always mean the same package.&lt;/strong&gt; Scope-to-registry routing lives in each consumer&apos;s &lt;code&gt;.npmrc&lt;/code&gt;: &lt;code&gt;@yourco:registry=...&lt;/code&gt;. A repo whose &lt;code&gt;.npmrc&lt;/code&gt; is wrong, or missing, can pull a public package that happens to share your internal name, which looks identical in &lt;code&gt;package.json&lt;/code&gt; and is not your code at all. So the name in the dependency block is a claim, not proof. Binding &lt;code&gt;@yourco/ui&lt;/code&gt; to your repo safely means confirming the consumer really resolves your scope from your registry, not assuming every matching string is yours.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The value isn&apos;t always a version range.&lt;/strong&gt; A &lt;code&gt;package.json&lt;/code&gt; entry&apos;s value can carry the real target, and internal packages lean on this constantly. &lt;code&gt;&quot;x&quot;: &quot;npm:@yourco/real@^1&quot;&lt;/code&gt; is an &lt;a href=&quot;https://docs.npmjs.com/cli/v11/commands/npm-install&quot;&gt;alias&lt;/a&gt;: the key is &lt;code&gt;x&lt;/code&gt;, the package is &lt;code&gt;@yourco/real&lt;/code&gt;, and a search for the package name never matches. &lt;code&gt;&quot;@yourco/ui&quot;: &quot;github:yourco/ui#v4&quot;&lt;/code&gt;, or &lt;code&gt;git+https://gitlab.yourco.com/yourco/ui.git&lt;/code&gt;, is a git dependency that resolves straight from a repo and never touches a registry. &lt;code&gt;&quot;shared&quot;: &quot;file:../shared&quot;&lt;/code&gt; is local and carries no cross-repo signal at all. A scanner that reads only the key, or treats every value as semver, misses the aliased consumers and the git-sourced ones entirely. For an internal package, git references are not an edge case. They&apos;re how a lot of orgs consume their own code without standing up a registry at all.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Workspaces make internal consumers invisible to the registry.&lt;/strong&gt; In a monorepo, packages reference each other over the &lt;code&gt;workspace:&lt;/code&gt; protocol, and &lt;a href=&quot;https://pnpm.io/workspaces&quot;&gt;pnpm and Yarn refuse to resolve those to anything but the local copy&lt;/a&gt;. The dependency never touches the registry, never produces a download event, never appears in any registry-side view. A consumer living in the same monorepo as your package has very real blast radius and a registry footprint of exactly zero. &lt;a href=&quot;https://nesbitt.io/2026/01/18/workspaces-and-monorepos-in-package-managers.html&quot;&gt;npm, Yarn, pnpm, and Bun implement workspaces a little differently&lt;/a&gt;, but they share that property: the edge is a symlink on disk, not a registry transaction.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;overrides&lt;/code&gt; and &lt;code&gt;resolutions&lt;/code&gt; can reroute the version behind the manifest&apos;s back.&lt;/strong&gt; npm &lt;code&gt;overrides&lt;/code&gt;, Yarn &lt;a href=&quot;https://yarnpkg.com/configuration/manifest#resolutions&quot;&gt;&lt;code&gt;resolutions&lt;/code&gt;&lt;/a&gt;, and pnpm &lt;code&gt;overrides&lt;/code&gt; let a repo force a dependency, including a deep transitive one, to a pinned version or &lt;a href=&quot;https://pnpm.io/settings#overrides&quot;&gt;a forked replacement&lt;/a&gt;. The dependency block says &lt;code&gt;^4.2.0&lt;/code&gt;, the override quietly swaps in &lt;code&gt;4.6.3&lt;/code&gt; or a patched fork, and the version running is not the one declared. This is the npm analogue of a Go &lt;code&gt;replace&lt;/code&gt; directive, with the same trap: the apparent consumer and the actual consumer are not the same thing.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;There are four declaration blocks, and they mean four different things.&lt;/strong&gt; A package can appear in &lt;code&gt;dependencies&lt;/code&gt;, &lt;code&gt;devDependencies&lt;/code&gt;, &lt;code&gt;peerDependencies&lt;/code&gt;, or &lt;code&gt;optionalDependencies&lt;/code&gt;. A &lt;code&gt;peerDependency&lt;/code&gt; on your package means the consumer expects the host application to supply it, so a breaking change ripples differently than it would for a direct runtime dependency. A &lt;code&gt;devDependency&lt;/code&gt; breaks builds and tooling, not production. Flattening all four into one undifferentiated &quot;depends on&quot; both overstates and understates the blast radius, depending on which way you&apos;re wrong.&lt;/p&gt;
&lt;h2&gt;What the full answer requires&lt;/h2&gt;
&lt;p&gt;To reliably answer &quot;who consumes this internal npm package,&quot; you need a system that:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Scans every repo in the org&lt;/strong&gt;, parsing each &lt;code&gt;package.json&lt;/code&gt;, without requiring each team to opt in or register&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Resolves each declared dependency to the repo that actually produces the package&lt;/strong&gt;, by matching the package name, with a guard so a public package that shares your internal name doesn&apos;t bind to the wrong repo&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reads the dependency value, not just the name&lt;/strong&gt;, so &lt;code&gt;npm:&lt;/code&gt; aliases and &lt;code&gt;github:&lt;/code&gt; or &lt;code&gt;git+https&lt;/code&gt; git references resolve to the right in-org repo instead of being missed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Keeps &lt;code&gt;workspace:&lt;/code&gt; and &lt;code&gt;catalog:&lt;/code&gt; monorepo references&lt;/strong&gt; while skipping purely local &lt;code&gt;file:&lt;/code&gt; and &lt;code&gt;link:&lt;/code&gt; deps that carry no cross-repo signal&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Distinguishes the four declaration blocks&lt;/strong&gt;, so a &lt;code&gt;peerDependency&lt;/code&gt; consumer and a &lt;code&gt;devDependency&lt;/code&gt; consumer aren&apos;t flattened into one edge&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Leaves test, example, and fixture trees out of the consumer count&lt;/strong&gt;, so a repo that imports your package only in a test harness doesn&apos;t read as a production consumer&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Stays current through rescans&lt;/strong&gt;, rather than a one-time snapshot that&apos;s stale the moment a manifest changes&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This is one of the specific problems &lt;a href=&quot;https://riftmap.dev&quot;&gt;Riftmap&lt;/a&gt; is built to solve. It connects to your GitHub or GitLab organisation with one read-only token, scans every repo, and parses every &lt;code&gt;package.json&lt;/code&gt; to extract npm dependency edges. It resolves each declared dependency to the repo that produces the package, and reads the value rather than just the name, so &lt;code&gt;npm:&lt;/code&gt; aliases and &lt;code&gt;github:&lt;/code&gt; or &lt;code&gt;git+https&lt;/code&gt; git references resolve to the right in-org repo instead of being missed. It separates the four declaration blocks into runtime, dev, peer and optional, keeps &lt;code&gt;workspace:&lt;/code&gt; and &lt;code&gt;catalog:&lt;/code&gt; references while dropping purely local &lt;code&gt;file:&lt;/code&gt; deps, and leaves test and example trees out of the consumer count. Each edge carries the constraint the consumer declares and the &lt;code&gt;package.json&lt;/code&gt; line where it lives. Parsed from what each repo declares, not inferred from what the registry happened to serve.&lt;/p&gt;
&lt;p&gt;Two honest limits, in the spirit of the rest of this series. Riftmap reads the declared dependency in &lt;code&gt;package.json&lt;/code&gt;, not the resolved lockfile tree. So it shows the range each repo declares, which is the fact that governs who adopts your next release, rather than the exact version each one has installed this minute. And it recognises internal packages by their scope, so a shop that publishes unscoped packages to a private registry is the current blind spot.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://riftmap.dev/images/blog/npm-internal-package-dependents.png&quot; alt=&quot;Riftmap detail panel for the @polaris-works/design-system package in a test org, Dependents tab showing four consumer repos — logistics-dashboard, node-api-client, customer-web and payment-ui — each row carrying the package.json line where the dependency is declared and the ^2.0.0 range it declares&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The result is the view the rest of this series describes. Before you drop that prop, restructure that export, or cut &lt;code&gt;5.0.0&lt;/code&gt;, you open the graph, click the package, and read the consumer list: every repo that depends on &lt;code&gt;@yourco/ui&lt;/code&gt;, the constraint it declares, whether it&apos;s a runtime, peer, or dev dependency, the &lt;code&gt;package.json&lt;/code&gt; line where the dependency lives, and which team owns it. You know who breaks. You know who&apos;s riding a caret range and will pull your next minor whether you meant them to or not. You know who&apos;s stranded on the old major and needs a migration before you can &lt;a href=&quot;https://riftmap.dev/blog/deprecate-internal-library-find-consumers/&quot;&gt;deprecate it&lt;/a&gt;. No clean-installing thirty repos to run &lt;code&gt;npm ls&lt;/code&gt;. No SQL over every &lt;code&gt;package.json&lt;/code&gt;. No waiting to see whose build goes red.&lt;/p&gt;
&lt;h2&gt;A scope keeps the package private and the consumers invisible&lt;/h2&gt;
&lt;p&gt;Here&apos;s the closing thought. A scoped, restricted package on a private registry is the right call. It keeps proprietary code off the public internet, and it gives you real control over who can pull it. That&apos;s distribution, and the registry does distribution well.&lt;/p&gt;
&lt;p&gt;But distribution and blast radius are different questions, and the registry only answers the first. It knows who is &lt;em&gt;allowed&lt;/em&gt; to pull your package. It has nothing to say about who already did, what they declared, or what breaks when you change it. The public packages, the ones whose blast radius is somebody else&apos;s problem, get a Dependents tab and a crawler ecosystem. Your internal package, the one whose breaking change lands on your own org, gets a download counter. The thing you did to protect the code is the same thing that hides who depends on it. That gap was never a property of npm. It was a property of where you chose to look.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This is the seventh post in the &lt;a href=&quot;https://riftmap.dev/blog/series/find-every-consumer/&quot;&gt;Find Every Consumer&lt;/a&gt; series. Previous posts cover &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-docker-base-image/&quot;&gt;Docker base images&lt;/a&gt;, &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-terraform-module/&quot;&gt;Terraform modules&lt;/a&gt;, &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-github-actions-workflow/&quot;&gt;GitHub Actions workflows&lt;/a&gt;, &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-helm-chart/&quot;&gt;Helm charts&lt;/a&gt;, &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-go-module/&quot;&gt;Go modules&lt;/a&gt; and &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-gitlab-ci-template/&quot;&gt;GitLab CI templates&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;If this is a problem your platform team deals with, I&apos;d be interested to hear how you&apos;re solving it today. You can find more at &lt;a href=&quot;https://riftmap.dev&quot;&gt;riftmap.dev&lt;/a&gt; or reach me at &lt;a href=&quot;mailto:hello@riftmap.dev&quot;&gt;hello@riftmap.dev&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, npm, and more. One read-only token. No YAML to maintain.&lt;/p&gt;
</content:encoded><category>npm</category><category>javascript</category><category>platform-engineering</category><category>devops</category><category>dependency-management</category><author>Daniel Westgaard</author></item><item><title>GitLab Orbit maps your whole SDLC. It still can&apos;t tell you what an infrastructure change will break.</title><link>https://riftmap.dev/blog/gitlab-orbit-and-the-artifact-layer/</link><guid isPermaLink="true">https://riftmap.dev/blog/gitlab-orbit-and-the-artifact-layer/</guid><description>GitLab Orbit is an excellent symbol-and-SDLC graph. It&apos;s also the clearest illustration yet of the one layer that kind of graph can&apos;t reach: the infrastructure dependencies running between your repositories.</description><pubDate>Fri, 12 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;GitLab Orbit is an excellent symbol-and-SDLC graph. It is also the clearest illustration yet of the one layer that kind of graph cannot reach: the infrastructure dependencies running between your repositories.&lt;/em&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;Somewhere in the &lt;a href=&quot;https://about.gitlab.com/blog/introducing-gitlab-orbit/&quot;&gt;GitLab Orbit launch&lt;/a&gt; on 10 June is a line about, in GitLab&apos;s phrasing, &quot;map vulnerability blast radius in minutes&quot;. I have written some version of that line more times than I can count. When a public DevSecOps company ships a graph and reaches for the exact words your product is built on, the responsible thing is to go and read everything they actually shipped before saying a word about it. So I spent two days in the docs, the data model, the source repository, and the customer write-up at the centre of the launch.&lt;/p&gt;
&lt;p&gt;Here is what I found. &quot;Blast radius&quot; is two questions wearing one phrase, and Orbit answers the other one. Ask &quot;what breaks if I change this&quot; at the symbol layer and you get one graph. Ask it at the infrastructure layer and you get a completely different one. Orbit builds the first. It builds it well. It is not the graph that tells a platform team what a base image bump is about to take down.&lt;/p&gt;
&lt;h2&gt;What GitLab actually shipped&lt;/h2&gt;
&lt;p&gt;I want to be generous about this, because Orbit deserves it, and because what Orbit is good at and what Riftmap is for barely overlap.&lt;/p&gt;
&lt;p&gt;Orbit indexes your code and your software lifecycle into one property graph and lets you query it with a Cypher-like language, over MCP, over REST, or from the GitLab CLI. It ships in two shapes. Orbit Local is a single-binary CLI that builds a code-only graph from a repository on your machine. Orbit Remote is the hosted version that spans a top-level &lt;a href=&quot;http://GitLab.com&quot;&gt;GitLab.com&lt;/a&gt; group, and it is the one the launch is really about. It is in public beta for &lt;a href=&quot;http://GitLab.com&quot;&gt;GitLab.com&lt;/a&gt; Premium and Ultimate. On the code side it parses around a dozen general-purpose programming languages and reads out definitions and cross-file references. On the lifecycle side it ingests the objects GitLab already stores: merge requests, pipelines, jobs, deployments, vulnerabilities, ownership. The &lt;a href=&quot;https://docs.gitlab.com/orbit/&quot;&gt;docs&lt;/a&gt; lay all of this out plainly.&lt;/p&gt;
&lt;p&gt;The launch leans hard on one customer, and it is a good choice. Compare the Market ran a careful test of four context strategies for an internal AI code reviewer, across 79 real merge requests with expert-annotated ground truth. The graph-grounded reviewer beat retrieval-augmented generation on inline-comment coverage, roughly 0.70 against 0.58, and the genuinely surprising finding was that RAG did worse than passing the model no context at all. That is well-run engineering and an interesting result, and you can read the &lt;a href=&quot;https://comparethemarketcareers.com/blog/comparing-context-retrieval-approaches-for-ai-code-review/&quot;&gt;whole write-up&lt;/a&gt; rather than the press-release version of it. If you run agents over a large GitLab codebase and you want them to stop burning a third of their token budget crawling files to work out what calls what, Orbit is a real answer to a real problem.&lt;/p&gt;
&lt;p&gt;So let me be clear about what Orbit does that Riftmap does not attempt. Questions like where a function lives, who calls it, what a method signature touches, which pipeline ran a job, who owns a service. Across a GitLab group, Orbit answers all of that from first-party data, and Riftmap never set out to. If that is your question, use Orbit.&lt;/p&gt;
&lt;h2&gt;What GitLab Orbit costs&lt;/h2&gt;
&lt;p&gt;Because the launch keeps drawing pricing questions, here is the shape of it as of July 2026, with the caveat that a public-beta price is a placeholder and GitLab has not committed to what it becomes at general availability.&lt;/p&gt;
&lt;p&gt;There is no separate GitLab Orbit SKU. Orbit is not a line item you buy on top of your subscription; it is a capability gated behind the tier you already pay for. The two shapes cost differently:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Orbit Local&lt;/strong&gt; — the single-binary CLI that builds a code-only graph of one repository on your machine — is effectively free to run. It is a tool you download; there is no hosted service metering it. It is also not the thing the launch is about, because it does not span an organisation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Orbit Remote&lt;/strong&gt; — the hosted, group-wide graph that the launch actually markets — is in public beta for &lt;strong&gt;&lt;a href=&quot;http://GitLab.com&quot;&gt;GitLab.com&lt;/a&gt; Premium and Ultimate&lt;/strong&gt;. In beta that means the cost of Orbit Remote is the cost of the &lt;a href=&quot;http://GitLab.com&quot;&gt;GitLab.com&lt;/a&gt; tier it rides on: Premium and Ultimate are GitLab&apos;s paid tiers, billed per seat, and Orbit Remote is switched on within them rather than priced on its own. It is &lt;a href=&quot;http://GitLab.com&quot;&gt;GitLab.com&lt;/a&gt; only; there is no self-managed Orbit Remote, and no Free-tier access.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The practical read for a platform team: if you are already on &lt;a href=&quot;http://GitLab.com&quot;&gt;GitLab.com&lt;/a&gt; Premium or Ultimate, evaluating Orbit Remote costs you nothing beyond the seats you hold today, and the beta is the moment to test it against your own graph questions before any GA price lands. If you are on GitLab Free, self-managed GitLab, or GitHub, there is no priced path to Orbit Remote at all — which loops straight back to the architectural wall in the next-but-one section: the limit was never the money.&lt;/p&gt;
&lt;h2&gt;The word doing two jobs&lt;/h2&gt;
&lt;p&gt;Here is the conflation the whole launch quietly sits on, and it is not GitLab&apos;s invention. The industry talks about &quot;the dependency graph of your system&quot; as if it were one thing. It is two.&lt;/p&gt;
&lt;p&gt;There is the symbol-and-SDLC graph. Its nodes are directories, files, the classes and functions defined in them, the symbols they import, and alongside those the merge requests, pipelines, deployments and vulnerabilities the platform tracks. Its edges are calls, imports, inheritance, and the lifecycle relationships between objects. This is the graph Orbit builds, and it is the graph that answers &quot;who calls this function&quot; and &quot;which services does this CVE touch&quot;.&lt;/p&gt;
&lt;p&gt;Then there is the &lt;a href=&quot;https://riftmap.dev/what-is-an-artifact-dependency-graph/&quot;&gt;artifact graph&lt;/a&gt;. Its nodes are the things your infrastructure is actually made of and shares between repositories. A base image. A Terraform module. A Helm chart. A reusable CI template. Its edges are the references that bind those things together across repos: a Dockerfile &lt;code&gt;FROM&lt;/code&gt; line, a Terraform &lt;code&gt;source&lt;/code&gt; block, a &lt;code&gt;terraform_remote_state&lt;/code&gt; lookup, a Helm chart &lt;code&gt;dependencies&lt;/code&gt; entry or a value reference, a GitLab CI &lt;code&gt;include:project&lt;/code&gt;. Different nodes, different edges, a different parser surface entirely.&lt;/p&gt;
&lt;p&gt;You do not have to take my word for which of those graphs Orbit is. Take the word of the customer GitLab chose to showcase. Compare the Market describe what they integrated as &quot;a symbol graph&quot;, and every query they demonstrate is a code query: where is this function called, what inherits from this interface, what does changing this method signature affect. The node legend in their own diagram is directory, file, definition, imported symbol. That is the symbol layer, described precisely, by the customer in the launch. Nobody is hiding the ball here. Orbit is a symbol graph with the lifecycle bolted alongside it.&lt;/p&gt;
&lt;p&gt;So Orbit answers blast radius at the symbol layer, change this signature and here are the callers, and at the SDLC layer, this CVE sits in these components owned by these teams. Both are real. Neither is the layer where a platform team&apos;s worst change lives.&lt;/p&gt;
&lt;h2&gt;Where the edges actually live for platform teams&lt;/h2&gt;
&lt;p&gt;Picture the change that actually keeps a platform team up at night. You bump a shared base image. Or you take a common Terraform module from v3 to v4 and tighten a variable along the way. Or you edit the values block on an umbrella Helm chart that nine services inherit from. The thing that breaks is not in the repository you are editing. It is three repos away, in a service owned by a team that has never read your module&apos;s changelog. And the edge that connects you to it was never a function call. It is a &lt;code&gt;source&lt;/code&gt; block pinned to a tag. It was never in the code for a symbol graph to find. It only ever existed in the manifest.&lt;/p&gt;
&lt;p&gt;This is the layer Orbit does not reach, and it is worth being precise about why, because it is not an oversight they will patch on Tuesday. Orbit&apos;s supported-language list is published, and it is general-purpose programming languages, the Pythons and Gos and TypeScripts. There is no HCL parser in it. No Dockerfile parser. No parser for chart manifests. The data model follows from that. It has first-class nodes for functions, files, merge requests, pipelines and vulnerabilities. It has no node for &quot;this base image, consumed by these eleven repositories&quot;, because nothing in the indexing pipeline ever parsed a &lt;code&gt;FROM&lt;/code&gt; line and resolved it across the group.&lt;/p&gt;
&lt;p&gt;This is not laziness, it is a genuinely different problem. Building a symbol graph means running a Tree-sitter parse over source and reading out the definitions and references. That is well understood, and GitLab has done it properly. Building an artifact graph means parsing HCL to follow module &lt;code&gt;source&lt;/code&gt; URLs, reading Dockerfiles to resolve base images back to the repositories that publish them, walking Helm value inheritance and chart dependencies, expanding CI &lt;code&gt;include&lt;/code&gt; across projects, and then reconciling all of it across repositories that were never designed to know about each other. It is a different parser surface and a different resolution problem. It is, more or less, the entire thing Riftmap is, and it is the same line I have drawn before between &lt;a href=&quot;https://riftmap.dev/blog/symbol-graphs-and-artifact-graphs/&quot;&gt;symbol graphs and artifact graphs&lt;/a&gt; and shown in practice when &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-gitlab-ci-template/&quot;&gt;finding every consumer of a GitLab CI template&lt;/a&gt; — or, on the GitHub side, &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-github-actions-workflow/&quot;&gt;every consumer of a reusable GitHub Actions workflow&lt;/a&gt;. Orbit being excellent does not move that line. If anything, it makes the line easier to see. I have since put numbers on it, measuring the split across two real organisations: the infrastructure share of cross-repo coupling ran from 38% to 99.75%, and not one edge was a code symbol — &lt;a href=&quot;https://riftmap.dev/blog/cross-repo-edge-composition/&quot;&gt;I counted every cross-repo edge in two real orgs&lt;/a&gt;. Widening the lens to nine public platform orgs and 1,297 repos, &lt;a href=&quot;https://riftmap.dev/blog/cross-repo-blast-radius-9-orgs/&quot;&gt;one in four repos turned out to be load-bearing for a sibling and the single most-depended-on repo at Grafana was a CI-workflow file with 188 dependents&lt;/a&gt; — exactly the artifact edge a symbol-and-SDLC graph has no node for.&lt;/p&gt;
&lt;h2&gt;Even on its own turf, the graph stops at the platform boundary&lt;/h2&gt;
&lt;p&gt;Set the layer question aside for a moment and there is still a wall, and it is structural rather than a matter of pricing. Orbit Remote is &lt;a href=&quot;http://GitLab.com&quot;&gt;GitLab.com&lt;/a&gt; only. The lifecycle side streams out of GitLab by change-data-capture into a managed graph, and the code side is served over GitLab&apos;s own internal API. There is nowhere in that design for a repository GitLab does not host. Orbit Local exists, but it is a code-only graph of a single repository on your machine, not a picture of your organisation.&lt;/p&gt;
&lt;p&gt;The trouble is that the organisations who feel cross-repo pain most acutely, the 50 to 300 repo polyrepo shops, are rarely tidy single-platform estates. They are GitHub with a bit of GitLab. Or GitHub with a self-managed GitLab for the regulated workloads. Or three acquisitions sitting on three different platforms with no intention of consolidating this year. The dependency that actually bites in that world is the one running from a GitHub application repository onto a base image published from a GitLab repository, and that is precisely the edge a GitLab-only graph cannot draw. Reading the edges from source, and reading them across GitHub and GitLab and Bitbucket alike, is not a feature you bolt onto a platform-native graph afterwards. It is a different starting point.&lt;/p&gt;
&lt;h2&gt;Two graphs, not a fight&lt;/h2&gt;
&lt;p&gt;I have come round to thinking Orbit is less a competitor than a very large, very well-funded proof that this category is real. For the symbol-and-SDLC layer, inside a GitLab shop, it is a good graph, and I would point people at it. The artifact layer is a different graph that answers a different question. Not &quot;who calls this function&quot; but &quot;what breaks if I change this base image&quot;. The two do not compete. They stack.&lt;/p&gt;
&lt;p&gt;And the useful part is that both are MCP servers. An agent reviewing an infrastructure change wants the symbol graph for the code in front of it and the artifact graph for the consequences it cannot see. &lt;a href=&quot;https://riftmap.dev/blog/ai-coding-agents-need-cross-repo-context/&quot;&gt;Claude Code&lt;/a&gt; can call both in the same review. Orbit for the function being edited. Riftmap for the eleven repositories downstream of the image that function&apos;s service is built on. This was always the shape of it. The agent is a consumer of the graph underneath, and the only interesting question was ever which graph. The launch just made the answer sharper.&lt;/p&gt;
&lt;p&gt;So here is where two days of reading actually landed me. The biggest player in the space shipped a dependency graph, reached for the words blast radius, and proved with a real customer that a structured graph beats letting a model guess. And it still does not parse a single &lt;code&gt;FROM&lt;/code&gt; line. The layer where your worst change lives, the infrastructure your repositories quietly share but no symbol graph can see, is still unmapped. Parsed, not inferred. Auto-discovered, never catalogued. Read across every platform you actually run on, not just the one that happens to host the graph. Those turn out to be different gifts. Only one of them tells you what you are about to break. That parsed-versus-registered distinction is a category line of its own, which I pull apart against Port&apos;s model-it-by-hand approach in &lt;a href=&quot;https://riftmap.dev/blog/modeled-graphs-and-parsed-graphs/&quot;&gt;modeled graphs and parsed graphs&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;A few questions, answered directly&lt;/h2&gt;
&lt;h3&gt;What is GitLab Orbit?&lt;/h3&gt;
&lt;p&gt;GitLab Orbit is GitLab&apos;s knowledge graph of a software organisation, launched 10 June 2026 and in public beta for &lt;a href=&quot;http://GitLab.com&quot;&gt;GitLab.com&lt;/a&gt; Premium and Ultimate. It indexes source code in around a dozen general-purpose programming languages together with the SDLC objects GitLab already stores, merge requests, pipelines, deployments, vulnerabilities and ownership, into one property graph you can query with a Cypher-like language, over MCP, over REST, or from the GitLab CLI. It ships in two shapes: Orbit Local, a single-binary CLI that builds a code-only graph of one repository on your machine, and Orbit Remote, the hosted version spanning a top-level &lt;a href=&quot;http://GitLab.com&quot;&gt;GitLab.com&lt;/a&gt; group.&lt;/p&gt;
&lt;h3&gt;How much does GitLab Orbit cost, and which tier is it in?&lt;/h3&gt;
&lt;p&gt;There is no separate GitLab Orbit price or SKU; it is gated behind the GitLab tier you already pay for. Orbit Local, the single-binary CLI, is effectively free to run locally. Orbit Remote, the hosted group-wide graph, is in public beta for &lt;a href=&quot;http://GitLab.com&quot;&gt;GitLab.com&lt;/a&gt; Premium and Ultimate — so its cost is the per-seat cost of those paid tiers, not a separate charge, and evaluating it adds nothing beyond seats you already hold. It is &lt;a href=&quot;http://GitLab.com&quot;&gt;GitLab.com&lt;/a&gt; only, with no self-managed or Free-tier access. A public-beta price is a placeholder, and GitLab has not committed to general-availability pricing.&lt;/p&gt;
&lt;h3&gt;Does GitLab Orbit map infrastructure dependencies across repositories (Terraform, Helm, Docker)?&lt;/h3&gt;
&lt;p&gt;No. Orbit indexes source code in general-purpose programming languages and the SDLC objects GitLab already stores. Its published language list contains no HCL parser, no Dockerfile parser, and no parser for chart manifests, so it does not resolve a Terraform &lt;code&gt;source&lt;/code&gt; block, a Dockerfile &lt;code&gt;FROM&lt;/code&gt; line, or a Helm value reference into a cross-repo edge. It maps blast radius at the code-symbol and lifecycle layers, not at the infrastructure artifact layer.&lt;/p&gt;
&lt;h3&gt;Does GitLab Orbit work across GitHub, or only GitLab?&lt;/h3&gt;
&lt;p&gt;Only GitLab, and the limit is architectural rather than a question of pricing tier. Orbit Remote builds its graph from GitLab data streamed through GitLab&apos;s own internal services, so there is nowhere in the design for a repository GitLab does not host. Orbit Local can graph a single repository on your machine, but it is code-only and does not span an organisation. If your estate is GitHub, mixed, or self-managed, Orbit Remote cannot draw the edges between your repos.&lt;/p&gt;
&lt;h3&gt;What is the difference between a symbol graph and an artifact dependency graph?&lt;/h3&gt;
&lt;p&gt;A symbol graph indexes the things inside your code: files, the functions and classes defined in them, and the calls and imports between them. It answers &quot;who calls this function&quot;. An artifact dependency graph indexes the things your repositories share at the infrastructure layer: base images, Terraform modules, Helm charts, CI templates, and the references that bind them across repos. It answers &quot;what breaks if I change this base image&quot;. They are different graphs, built by different parsers, and blast radius on an infrastructure change needs the second one.&lt;/p&gt;
</content:encoded><category>gitlab-orbit</category><category>dependency-graph</category><category>blast-radius</category><category>terraform</category><category>platform-engineering</category><category>cross-repo</category><author>Daniel Westgaard</author></item><item><title>How to Find Every Consumer of Your GitLab CI Template</title><link>https://riftmap.dev/blog/how-to-find-every-consumer-of-your-gitlab-ci-template/</link><guid isPermaLink="true">https://riftmap.dev/blog/how-to-find-every-consumer-of-your-gitlab-ci-template/</guid><description>You maintain a shared GitLab CI template. Which projects include it, at which ref — and what breaks when you merge? Why GitLab can&apos;t tell you, and what can.</description><pubDate>Thu, 11 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;You maintain a shared GitLab CI template. You need to rename a job, change an input, or restructure the file. Which projects across your org include it? GitLab has been asked this question for nearly six years. Here&apos;s the paper trail, and the actual answer.&lt;/em&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;In October 2020, a platform engineer posted a question on the GitLab forum: &lt;a href=&quot;https://forum.gitlab.com/t/identify-usage-of-template-files/44199&quot;&gt;we generated a lot of internal templates which others can include in their CI/CD pipelines. Is there a way to see how often a template is included in other projects?&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The answer they got was that no API contains this data, and that they could try grepping the nginx and Workhorse access logs on their self-managed instance and aggregating the raw fetch counts with &lt;code&gt;jq&lt;/code&gt;. Web server logs. That was the canonical answer to &quot;who consumes my CI template&quot; in 2020.&lt;/p&gt;
&lt;p&gt;Browse the sidebar of that thread and you find its siblings. &quot;&lt;a href=&quot;https://forum.gitlab.com/t/reporting-on-template-usage-adoption/61156&quot;&gt;Reporting on Template Usage/Adoption&lt;/a&gt;.&quot; &quot;&lt;a href=&quot;https://forum.gitlab.com/t/tool-to-document-ci-template-includes/67719&quot;&gt;Tool to document CI Template includes&lt;/a&gt;.&quot; &quot;&lt;a href=&quot;https://forum.gitlab.com/t/find-out-how-many-times-my-gitlab-ci-file-has-been-used-from-inclusion-in-other-projects/94893&quot;&gt;Find out how many times my gitlab-ci file has been used&lt;/a&gt;.&quot; &quot;&lt;a href=&quot;https://forum.gitlab.com/t/count-the-number-of-usages/38113&quot;&gt;Count the number of usages&lt;/a&gt;&quot; predates the 2020 anchor thread by five months. Different years, same question, zero replies on every one of them. Nearly six years of template maintainers asking the reverse question into the void.&lt;/p&gt;
&lt;p&gt;The forward question, how do I share CI config across projects, is one of the best-documented patterns in GitLab. The reverse question, who is consuming what I shared, has an answer so bad that grep-the-web-server-logs was a genuine improvement on the alternatives. This post is about the reverse question.&lt;/p&gt;
&lt;h2&gt;The scenario&lt;/h2&gt;
&lt;p&gt;Your platform team maintains a &lt;code&gt;devops/ci-templates&lt;/code&gt; project. It started as one file. Now it&apos;s a small library: build templates, deploy templates, a security scanning include, maybe some shared rules and defaults. Other projects consume it the standard way:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# In some-service/.gitlab-ci.yml
include:
  - project: &apos;devops/ci-templates&apos;
    ref: v2.4.0
    file:
      - &apos;/templates/build-go.yml&apos;
      - &apos;/templates/deploy-k8s.yml&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Twenty projects adopted it. Then fifty. Then you stopped counting, because there is nothing in the product that counts for you.&lt;/p&gt;
&lt;p&gt;Now you need to change it. Rename a job that other pipelines &lt;code&gt;extends&lt;/code&gt; from. Change a variable the deploy template expects. Split one file into three. The question is the same one that comes up for every shared infrastructure artifact: &lt;strong&gt;which projects across our org include this template, at which ref, and which of them break when I merge?&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;The part that makes GitLab worse than GitHub here&lt;/h2&gt;
&lt;p&gt;If you&apos;ve read the &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-github-actions-workflow/&quot;&gt;GitHub Actions edition&lt;/a&gt; of this series, you know reusable workflows have the same visibility problem. GitLab&apos;s version has a structural twist that makes it sharper.&lt;/p&gt;
&lt;p&gt;In GitHub Actions, &lt;code&gt;uses:&lt;/code&gt; requires a ref. Every caller pins to &lt;em&gt;something&lt;/em&gt;, even if that something is &lt;code&gt;@main&lt;/code&gt;. In GitLab CI, &lt;code&gt;ref:&lt;/code&gt; on a project include is optional, and when it&apos;s omitted, the include resolves to the HEAD of the template project&apos;s default branch. Per the &lt;a href=&quot;https://docs.gitlab.com/ci/yaml/&quot;&gt;CI/CD YAML reference&lt;/a&gt;, that&apos;s documented behaviour, not an accident.&lt;/p&gt;
&lt;p&gt;In practice, most templates in most orgs are consumed without a ref. Which means a merge to &lt;code&gt;main&lt;/code&gt; in &lt;code&gt;devops/ci-templates&lt;/code&gt; is not a release. It is an instant, org-wide deployment of CI configuration to every consumer that didn&apos;t pin. There is no rollout. There is no opt-in. The blast radius is the whole estate, and it detonates at merge time.&lt;/p&gt;
&lt;p&gt;GitLab says this itself, in writing. The YAML reference now warns that including another project&apos;s CI configuration is, from a security perspective, &lt;a href=&quot;https://docs.gitlab.com/ci/yaml/&quot;&gt;similar to pulling a third-party dependency, and that no pipelines or notifications trigger when the other project&apos;s files change&lt;/a&gt;. Read that second clause again. The dependency is real, and it is silent. GitLab&apos;s own &lt;a href=&quot;https://docs.gitlab.com/development/cicd/templates/&quot;&gt;template development guide&lt;/a&gt; makes the maintainer-side version of the same point: changes to templates consumed via &lt;code&gt;include&lt;/code&gt; can break pipelines for every project using them, which is why GitLab treats its own template changes as breaking changes deferred to major releases.&lt;/p&gt;
&lt;p&gt;And GitLab has lived this at platform scale. The &lt;a href=&quot;https://gitlab.com/gitlab-org/gitlab/-/issues/324131&quot;&gt;&lt;code&gt;master&lt;/code&gt; to &lt;code&gt;main&lt;/code&gt; default-branch rename broke CI templates with hardcoded refs&lt;/a&gt;. In a more recent &lt;a href=&quot;https://gitlab.com/gitlab-org/gitlab/-/merge_requests/179652&quot;&gt;merge request touching the security scanning templates&lt;/a&gt;, a GitLab engineer noted that template changes can prevent whole customer pipelines from starting, described setting up dashboards to monitor for it, and acknowledged that customer feedback would probably surface a problem before their own metrics did. That is the maintainer of the world&apos;s largest CI template library saying, candidly, that part of their blast-radius monitoring is waiting for users to complain. If GitLab&apos;s own platform team operates partially blind here, your &lt;code&gt;devops/ci-templates&lt;/code&gt; repo is not an outlier. It&apos;s the norm.&lt;/p&gt;
&lt;p&gt;Practitioners writing about this confirm the culture. A recent &lt;a href=&quot;https://gitdash.dev/blog/gitlab-versioned-cicd-components&quot;&gt;piece on versioning pipeline logic&lt;/a&gt; puts it plainly: an include pointing at &lt;code&gt;main&lt;/code&gt; means every consumer inherits template changes immediately with no opt-in, manually pinning SHAs or tags across dozens of repos is labour nobody actually does, so teams ride &lt;code&gt;main&lt;/code&gt; and hope. &lt;a href=&quot;https://dev.to/denisrendler/designing-security-workflows-using-gitlab-ci-templates-ph0&quot;&gt;A dev.to author&lt;/a&gt; describes adopting git tags for their templates specifically so colleagues would stop fearing that a template change would break their release process. The fear is the default state. The unpinned include is the default configuration.&lt;/p&gt;
&lt;h2&gt;What existing tools give you (and where they stop)&lt;/h2&gt;
&lt;p&gt;I want to be fair to the options, because some of them are genuinely useful for parts of this.&lt;/p&gt;
&lt;h3&gt;GitLab code search&lt;/h3&gt;
&lt;p&gt;You can search for the template path across a group:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;include &quot;devops/ci-templates&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Basic search will find string matches in blobs. &lt;a href=&quot;https://docs.gitlab.com/user/search/advanced_search/&quot;&gt;Advanced search&lt;/a&gt; does it better and faster, but it&apos;s a Premium/Ultimate feature, and on self-managed it requires you to stand up and operate the search infrastructure behind it, which a lot of instances simply haven&apos;t done.&lt;/p&gt;
&lt;p&gt;Even where it works well, code search gives you matches, not answers. It doesn&apos;t extract the ref. It doesn&apos;t distinguish &lt;code&gt;include: project:&lt;/code&gt; from a comment that happens to mention the path. It doesn&apos;t see the second hop: if your template is included by a &lt;em&gt;wrapper&lt;/em&gt; template in another shared project, code search finds the wrapper, not the forty projects behind it. For a one-off audit it&apos;s a reasonable start. It is not a system.&lt;/p&gt;
&lt;h3&gt;The CI lint API&lt;/h3&gt;
&lt;p&gt;GitLab can show you the fully merged configuration for a single project, includes resolved, via the &lt;a href=&quot;https://docs.gitlab.com/api/lint/&quot;&gt;CI lint endpoint&lt;/a&gt;. This is genuinely good for the forward direction: &quot;what does this project&apos;s pipeline actually consist of.&quot; But it&apos;s per-project, and it answers the wrong direction. To get the reverse view you&apos;d have to call it for every project in the org and parse the results yourself, which brings us to the script people inevitably write.&lt;/p&gt;
&lt;h3&gt;The script&lt;/h3&gt;
&lt;p&gt;Enumerate every project via the API, fetch every &lt;code&gt;.gitlab-ci.yml&lt;/code&gt;, parse the YAML, extract &lt;code&gt;include:&lt;/code&gt; entries, filter for your template, extract refs, handle pagination and rate limits, run it on a schedule, store the results somewhere. Several teams have built exactly this. One platform engineer on r/devops described building an in-house mapper that treats shared CI includes as a first-class dependency edge alongside Terraform sources and Dockerfile &lt;code&gt;FROM&lt;/code&gt; lines. The fact that this keeps getting independently built is the strongest evidence there is that the question matters. It is also a project you now own, with all the corner cases below as your backlog.&lt;/p&gt;
&lt;h3&gt;Renovate&lt;/h3&gt;
&lt;p&gt;Renovate&apos;s &lt;a href=&quot;https://docs.renovatebot.com/modules/manager/gitlabci-include/&quot;&gt;GitLab CI include managers&lt;/a&gt; can detect project includes and open MRs to bump the &lt;code&gt;ref&lt;/code&gt;. As with &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-terraform-module/&quot;&gt;Terraform modules&lt;/a&gt; and &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-github-actions-workflow/&quot;&gt;GitHub Actions&lt;/a&gt;, Renovate implicitly knows who consumes what, because it&apos;s configured per consumer. But it&apos;s an updater, not a mapper. There&apos;s no org-level &quot;show me every project that includes this template&quot; view, and it has nothing to say about the unpinned includes, which are the majority and the most dangerous.&lt;/p&gt;
&lt;h3&gt;CI/CD Catalog analytics&lt;/h3&gt;
&lt;p&gt;This one deserves real credit, because GitLab has started answering the question. With GitLab 19.0, the CI/CD Catalog gained a &lt;a href=&quot;https://about.gitlab.com/blog/track-ci-component-usage/&quot;&gt;Components Analytics view&lt;/a&gt;: usage counts for your published components across all tiers, and on Ultimate, a drill-down showing exactly which projects included a component in a pipeline over the last 30 days and which version each one is on. GitLab&apos;s own framing of the problem in the &lt;a href=&quot;https://gitlab.com/gitlab-org/gitlab/-/work_items/579460&quot;&gt;work item&lt;/a&gt; is almost word-for-word the premise of this series: component maintainers previously had no way to identify which projects used their component or which versions, making breaking changes and deprecations hard to coordinate.&lt;/p&gt;
&lt;p&gt;So the gap is closing. But look at what the closure covers. It covers &lt;code&gt;include:component&lt;/code&gt;, resources published to the CI/CD Catalog. It is usage-event-based, derived from pipelines that actually ran recently, rather than parsed from what repos declare. And the per-project answer is Ultimate-only. The &lt;code&gt;include:project&lt;/code&gt; template fleets, which is what nearly every self-managed enterprise estate actually runs on, including the &lt;code&gt;devops/ci-templates&lt;/code&gt; repo in the scenario above, are not in scope. If you migrated your entire template library to Catalog components and bought Ultimate, GitLab now answers a 30-day usage version of the question. For everyone else, the 2020 forum thread is still the state of the art.&lt;/p&gt;
&lt;p&gt;(GitLab also announced &lt;a href=&quot;https://about.gitlab.com/blog/introducing-gitlab-orbit/&quot;&gt;Orbit&lt;/a&gt; this week, a context graph across code, work items, pipelines and deployments for AI agents to query. It&apos;s aimed at agent context rather than artifact consumers, and it&apos;s early beta, so I gave it &lt;a href=&quot;https://riftmap.dev/blog/gitlab-orbit-and-the-artifact-layer/&quot;&gt;the proper look in a separate post&lt;/a&gt;.)&lt;/p&gt;
&lt;h2&gt;Why this is harder than it looks&lt;/h2&gt;
&lt;p&gt;A naive grep for the template path undercounts and overcounts at the same time, because &lt;code&gt;include&lt;/code&gt; is not one mechanism. It&apos;s five.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;include:
  - local: &apos;/ci/lint.yml&apos;                       # same repo, not a cross-project edge
  - project: &apos;devops/ci-templates&apos;              # the core case
    ref: v2.4.0
    file: &apos;/templates/build-go.yml&apos;
  - remote: &apos;https://gitlab.example.com/devops/ci-templates/-/raw/main/templates/scan.yml&apos;
  - template: &apos;Jobs/SAST.gitlab-ci.yml&apos;         # GitLab-shipped, not yours
  - component: $CI_SERVER_FQDN/devops/components/build-go@2.4.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each form has different semantics, and a consumer-tracking system has to treat them differently. &lt;code&gt;local&lt;/code&gt; includes are same-repo plumbing, not a dependency on you. &lt;code&gt;template&lt;/code&gt; includes point at GitLab&apos;s shipped library, also not you. &lt;code&gt;project&lt;/code&gt; includes are the core case. &lt;code&gt;remote&lt;/code&gt; includes are sneaky: they can point at the exact same file in the exact same template repo, just over raw HTTP, and a search for &lt;code&gt;include: project:&lt;/code&gt; misses them entirely. &lt;code&gt;component&lt;/code&gt; includes wrap the project path in &lt;code&gt;$CI_SERVER_FQDN&lt;/code&gt; variables and version suffixes that a literal string match won&apos;t survive.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Nested includes are where the script dies.&lt;/strong&gt; Template repos include other template repos. Your &lt;code&gt;deploy-k8s.yml&lt;/code&gt; might itself &lt;code&gt;include: project:&lt;/code&gt; a shared rules file from &lt;code&gt;devops/ci-base&lt;/code&gt;. GitLab resolves these chains at pipeline time, &lt;a href=&quot;https://docs.gitlab.com/ci/yaml/&quot;&gt;up to 150 includes deep&lt;/a&gt;, with the added wrinkle that nested includes execute without context as a public user. If you change &lt;code&gt;ci-base&lt;/code&gt;, the projects that break include projects that have never heard of &lt;code&gt;ci-base&lt;/code&gt;. They included a template that included you. Finding the direct consumers is a string search. Finding the transitive ones requires a graph.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Includes are not the only cross-project CI edge.&lt;/strong&gt; Multi-project pipelines via &lt;code&gt;trigger: project:&lt;/code&gt; create a dependency on another project&apos;s pipeline. Parent-child pipelines via &lt;code&gt;trigger: include:&lt;/code&gt; can pull child pipeline definitions from other projects. Cross-project &lt;code&gt;needs: [{project, job, ref}]&lt;/code&gt; creates a dependency on another project&apos;s &lt;em&gt;job artifacts&lt;/em&gt;. None of these are includes, all of them break when the upstream project changes, and a consumer map that only parses &lt;code&gt;include:&lt;/code&gt; misses them.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The breaking surface is loosely typed.&lt;/strong&gt; With &lt;a href=&quot;https://docs.gitlab.com/ci/inputs/&quot;&gt;&lt;code&gt;spec:inputs&lt;/code&gt;&lt;/a&gt;, templates and components now have something like a declared interface, which is genuine progress. But the installed base of &lt;code&gt;include:project&lt;/code&gt; templates communicates through variables, &lt;code&gt;extends&lt;/code&gt; targets, and job names. Rename a job that downstream pipelines &lt;code&gt;extends&lt;/code&gt; from and there is no compile-time error. There&apos;s a pipeline that fails to start, in someone else&apos;s project, at whatever time they next push.&lt;/p&gt;
&lt;h2&gt;What the full answer requires&lt;/h2&gt;
&lt;p&gt;To reliably answer &quot;who consumes this CI template,&quot; you need a system that:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Scans every project in the group hierarchy&lt;/strong&gt;, parsing &lt;code&gt;.gitlab-ci.yml&lt;/code&gt; plus the template files that template repos themselves carry, in &lt;code&gt;templates/&lt;/code&gt; and &lt;code&gt;.gitlab/ci/&lt;/code&gt;, because that&apos;s where the nested chain starts&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Extracts every cross-project edge type&lt;/strong&gt;: &lt;code&gt;include:project&lt;/code&gt; with its ref and file list, &lt;code&gt;include:remote&lt;/code&gt; URLs resolved back to the repos they point at, &lt;code&gt;include:component&lt;/code&gt; references with the host variables and version suffixes stripped, plus &lt;code&gt;trigger:&lt;/code&gt; and cross-project &lt;code&gt;needs:&lt;/code&gt; edges&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Knows which forms to ignore&lt;/strong&gt;: &lt;code&gt;local&lt;/code&gt; includes and GitLab-shipped &lt;code&gt;template:&lt;/code&gt; includes are noise in a consumer map, not signal&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reconstructs nested chains&lt;/strong&gt; so a change to a base template surfaces the transitive consumers, not just the wrapper repo that includes it directly&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Records the ref each consumer declares&lt;/strong&gt;, including its absence, so &quot;who is riding an unpinned include&quot; is a queryable fact rather than a suspicion&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Stays current&lt;/strong&gt; through rescans of what the repos declare, not 30-day windows of what happened to run&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Makes the result one query&lt;/strong&gt;: every consumer of &lt;code&gt;devops/ci-templates&lt;/code&gt;, with the file and line where the include lives&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This is one of the specific problems &lt;a href=&quot;https://riftmap.dev&quot;&gt;Riftmap&lt;/a&gt; is built to solve. It scans a GitLab (or GitHub) org and parses every project&apos;s CI configuration, emitting distinct edge types for project includes, remote includes, catalog components, multi-project triggers and cross-project needs, while deliberately skipping local and GitLab-shipped template includes. Template repos&apos; own &lt;code&gt;templates/*.yml&lt;/code&gt; and &lt;code&gt;.gitlab/ci/*.yml&lt;/code&gt; files are parsed too, so when a template includes another template, that edge is in the graph, and the transitive chain from a base template to its end consumers is reconstructed across the org. Each edge carries the declared ref as a version constraint, or its absence, plus file and line provenance. Parsed from what the repos declare, not inferred from what recently ran.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://riftmap.dev/images/blog/gitlab-ci-template-dependents.png&quot; alt=&quot;Riftmap detail panel for the ci-templates repo in a 60-repo test org (polaris-works/platform), Dependents tab showing 53 consumers — logistics-infra, analytics-api, ml-models, tracking-service, portal-helm, terraform-modules-compute, terraform-modules-tagging and more — each row carrying the .gitlab-ci.yml line 2 where the include lives and the main ref it declares&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://riftmap.dev/images/blog/gitlab-ci-template-breakdown.png&quot; alt=&quot;Riftmap Dependency Breakdown panel for the same scan, CI/CD edges totalling 57 split by type: 52 CI Include, 2 CI Trigger, 1 CI Remote Include, 1 CI Component, and 1 CI Cross-Project Needs — the remote include, catalog component, multi-project triggers and cross-project needs a plain string search would miss&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The result: before you merge that job rename into &lt;code&gt;devops/ci-templates&lt;/code&gt;, you open the graph, click the template repo, and read the consumer list. You know who breaks. You know who&apos;s pinned to a tag and has time, and who&apos;s riding an unpinned include and gets your change at merge time. You know who to notify, instead of finding out who you should have notified.&lt;/p&gt;
&lt;p&gt;For the cross-platform treatment of that merge moment itself — why a shared CI change is really two deployments at once, instant to the unpinned consumers and silent to the pinned ones, and why CI has no deprecation signal to soften either — see &lt;a href=&quot;https://riftmap.dev/blog/changing-shared-ci-template-two-deployments/&quot;&gt;You changed the shared CI template. Half the org got it instantly. The other half will never see it&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;The dependency GitLab told you about&lt;/h2&gt;
&lt;p&gt;Here&apos;s the closing thought. GitLab&apos;s own documentation says that including another project&apos;s CI configuration is like pulling a third-party dependency. Take that sentence seriously and follow it to its conclusion. We have norms for third-party dependencies. We pin them. We track who uses them. We check the blast radius before publishing a breaking change. Somewhere along the way, shared CI templates became the one class of dependency where the ecosystem&apos;s answer to &quot;who depends on this?&quot; was grep your web server logs, and we collectively decided that was fine.&lt;/p&gt;
&lt;p&gt;It was never fine. It was just invisible. The template that fifty projects include without a ref is the highest-leverage, least-observed dependency in your org. Treat it like one.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This is the sixth post in the &lt;a href=&quot;https://riftmap.dev/blog/series/find-every-consumer/&quot;&gt;Find Every Consumer&lt;/a&gt; series. Previous posts cover &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-docker-base-image/&quot;&gt;Docker base images&lt;/a&gt;, &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-terraform-module/&quot;&gt;Terraform modules&lt;/a&gt;, &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-github-actions-workflow/&quot;&gt;GitHub Actions workflows&lt;/a&gt;, &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-helm-chart/&quot;&gt;Helm charts&lt;/a&gt; and &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-go-module/&quot;&gt;Go modules&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;If this is a problem your platform team deals with, I&apos;d be interested to hear how you&apos;re solving it today. You can find more at &lt;a href=&quot;https://riftmap.dev&quot;&gt;riftmap.dev&lt;/a&gt; or reach me at &lt;a href=&quot;mailto:hello@riftmap.dev&quot;&gt;hello@riftmap.dev&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;
</content:encoded><category>gitlab-ci</category><category>platform-engineering</category><category>devops</category><category>dependency-management</category><author>Daniel Westgaard</author></item><item><title>Is Backstage worth it? The real question is whether anyone will use it</title><link>https://riftmap.dev/blog/is-backstage-worth-it/</link><guid isPermaLink="true">https://riftmap.dev/blog/is-backstage-worth-it/</guid><description>The Backstage worth-it debate is always about cost — FTEs, months, TCO. Spotify&apos;s own head of Backstage engineering says that&apos;s not where it dies.</description><pubDate>Wed, 10 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;The &quot;is Backstage worth it&quot; debate is always about cost: how many engineers, how many months, how much it runs per developer. The person who runs Backstage at Spotify will tell you that&apos;s not where it dies.&lt;/em&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;At BackstageCon, and again in an interview with The New Stack, Helen Greul, who heads Backstage engineering at Spotify, gave a number that should reframe the whole question. Outside Spotify, &lt;a href=&quot;https://thenewstack.io/spotifys-backstage-roadmap-aims-to-speed-up-adoption/&quot;&gt;the average Backstage adoption rate is stuck at around 10%&lt;/a&gt;. Inside Spotify it is 99%. And the reason she gave for the gap was not that teams cannot afford the setup. It was that adopters often do not get past the proof of concept, because they never pinned down the problem their developers actually had.&lt;/p&gt;
&lt;p&gt;Read that twice. The person responsible for Backstage at the company that invented it is telling you the tool usually fails &lt;em&gt;after&lt;/em&gt; the hard engineering is done, not before.&lt;/p&gt;
&lt;p&gt;That is worth sitting with, because almost every &quot;is Backstage worth it&quot; debate I see is an argument about the part Greul says is not the problem. Someone quotes the &lt;a href=&quot;https://internaldeveloperplatform.org/developer-portals/backstage/&quot;&gt;community estimate of around $150,000 per 20 developers&lt;/a&gt; in total cost of ownership. Someone else points out it takes two or three full-time engineers and the better part of a year to stand up a real catalog. Both numbers are accurate. Neither one answers the question, because cost tells you what it takes to &lt;em&gt;build&lt;/em&gt; Backstage, and worth is decided by whether anyone &lt;em&gt;uses&lt;/em&gt; what you built.&lt;/p&gt;
&lt;h2&gt;The question everyone asks, and the one that decides it&lt;/h2&gt;
&lt;p&gt;&quot;Worth it&quot; is a ratio. Value returned over what it costs you. The cost side is well documented and not in dispute. The value side is the part that quietly determines the outcome, and value from a developer portal is not delivered at launch. It accrues every time an engineer opens the portal instead of asking in Slack, trusts what it tells them, and acts on it. That only keeps happening if the portal keeps being right.&lt;/p&gt;
&lt;p&gt;So the honest worth-it question is not &quot;can we afford to build it&quot;. Plenty of teams can. It is &quot;once we build it, will it stay true enough that people keep coming back&quot;. The 10% number is the industry&apos;s answer to that question, aggregated across thousands of organisations, and it is not flattering. The build is the table stakes. The trust loop is the game.&lt;/p&gt;
&lt;p&gt;This reframe also explains a finding that looks paradoxical otherwise. Roadie&apos;s &lt;a href=&quot;https://roadie.io/blog/the-2025-state-of-backstage-report/&quot;&gt;2025 State of Backstage Report&lt;/a&gt;, drawn from 105 active practitioners, found that 70% of the companies that describe themselves as &lt;em&gt;very happy&lt;/em&gt; with Backstage still dedicate at least three full-time engineers to maintaining it. The happy teams are not the ones who escaped the cost. They are the ones who pay it indefinitely and consider it worth paying, because for them the loop holds. The question is what makes it hold for them and break for everyone else.&lt;/p&gt;
&lt;h2&gt;Why Spotify gets 99% and you might get 10%&lt;/h2&gt;
&lt;p&gt;The most useful answer I have found comes from a Backstage founder describing, in &lt;a href=&quot;https://www.infoq.com/presentations/backstage-plugin&quot;&gt;an InfoQ talk&lt;/a&gt;, why Spotify&apos;s catalog stayed relevant when so many copies of it rot. The discipline was simple to state and hard to sustain. The metadata for each component lives in that component&apos;s repository, and ownership of the metadata is handed to the team that owns the component. The catalog is not a thing a central team curates. It is a thing every team is on the hook for, next to the code, as part of shipping.&lt;/p&gt;
&lt;p&gt;That is the engine under the 99%. When the data lives where the work happens and the people doing the work own it, the data stays current, so the portal stays trustworthy, so people keep using it, so keeping it current stays worth their while. The loop reinforces itself. Break any link and it runs the other way. The data drifts, the portal gets a reputation for being wrong, people stop checking it, and the team maintaining it is now grooming a graph that nobody trusts. That is what 10% looks like from the inside. Not a portal nobody built. A portal nobody believes.&lt;/p&gt;
&lt;p&gt;I want to be fair here, because this is where the critics get lazy. When the loop holds, Backstage is genuinely excellent, and the market reflects that. It holds &lt;a href=&quot;https://newsletter.getdx.com/p/backstage-and-the-developer-portal-market&quot;&gt;roughly 89% of the internal-developer-portal market as of early 2026&lt;/a&gt;, serving thousands of organisations and millions of developers, and the data from teams who run it well is mostly positive. The plugin ecosystem is unmatched and the CNCF governance means it will outlast any single vendor. Backstage is not a bad tool. It is a tool whose worth is unusually sensitive to one variable, and that variable is whether the data inside it maintains itself or has to be maintained.&lt;/p&gt;
&lt;h2&gt;The diagnostic: does this fact maintain itself, or does someone have to?&lt;/h2&gt;
&lt;p&gt;This gives you a way to predict your own outcome before you spend a quarter finding out. Take the things you want to put in the portal, and for each one ask a single question. Does this fact stay current as a byproduct of how engineers already work, or does it require a separate act of maintenance that nobody is specifically paid to perform?&lt;/p&gt;
&lt;p&gt;Some facts pass easily. Who owns a service. Who is on call. Where the runbook is. What the tech-docs say. The scorecard criteria your platform team defined. These originate with people, they change rarely, and a human decides them on purpose. The catalog model fits them well, because the catalog is the source of truth for that kind of data. There is no other copy to drift away from. For these jobs a portal, Backstage or a managed one, is a good buy, and I would not argue otherwise.&lt;/p&gt;
&lt;p&gt;Other facts fail the test immediately, and they fail it in a specific and predictable place. The cross-repo infrastructure dependencies. Which repositories consume a shared Terraform module, via its &lt;code&gt;source&lt;/code&gt; block. Which services are built on a base image, via a Dockerfile &lt;code&gt;FROM&lt;/code&gt;. Which charts depend on which, via &lt;code&gt;Chart.yaml&lt;/code&gt;. Which pipelines pull a shared template, via a &lt;code&gt;.gitlab-ci.yml&lt;/code&gt; &lt;code&gt;include&lt;/code&gt;. These already exist as declarations in the manifests. The catalog entry that mirrors them is a second copy of a fact the repo already states. Engineers must edit the manifest to ship. Nothing forces them to edit the catalog to match. So the manifest and the catalog entry diverge on the first commit after someone stops being diligent, and on a real team that is roughly immediately. I went through the mechanics of this in detail in &lt;a href=&quot;https://riftmap.dev/blog/the-catalog-maintenance-trap/&quot;&gt;the catalog maintenance trap&lt;/a&gt;, and the architectural version of the argument is in &lt;a href=&quot;https://riftmap.dev/blog/modeled-graphs-and-parsed-graphs/&quot;&gt;modeled graphs and parsed graphs&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The diagnostic, then, is a ratio of its own. The more of your intended value sits in the first bucket, the more Backstage is worth it. The more of it sits in the second, the lower your adoption ceiling, no matter how well you build it, because you are asking people to hand-maintain a copy of facts their commits already changed, and they will not, and the graph will be wrong exactly when it matters.&lt;/p&gt;
&lt;h2&gt;The change that proves it&lt;/h2&gt;
&lt;p&gt;Here is when it matters, made concrete, because this is the scene that sends teams looking in the first place.&lt;/p&gt;
&lt;p&gt;A platform engineer needs to bump a base image, or change a shared Terraform module, the kind of change that fans out across dozens of repos that no single person has in their head. Maybe the person who &lt;em&gt;did&lt;/em&gt; have it in their head is leaving in three weeks, and the dependency view in the portal was supposed to be how that knowledge survived their departure. This is the highest-stakes thing a portal&apos;s dependency graph is meant to do. Tell you what breaks before you ship.&lt;/p&gt;
&lt;p&gt;And it is the exact moment the catalog model lets you down, because the graph is only as current as the last engineer who remembered to update YAML that nothing required them to update. So at the decision point where being wrong is most expensive, you are consulting the data you should trust least. A portal you cannot trust when the change is risky is not a safety net. It is a comfort blanket with holes you find out about during the incident. That is the worth question with the abstraction stripped off. The maintenance cost everyone complains about does not even buy you the one answer you most needed it for.&lt;/p&gt;
&lt;h2&gt;When it is genuinely worth it&lt;/h2&gt;
&lt;p&gt;So let me be precise about when the answer is yes, because it often is.&lt;/p&gt;
&lt;p&gt;If you have a platform team with real frontend capacity, an organisation large enough that the per-developer cost amortises, a genuine need to own and extend the portal, and, most importantly, the organisational will to enforce the metadata-in-the-repo discipline that makes Spotify&apos;s catalog stay true, then Backstage is a defensible and often excellent choice. The teams in Roadie&apos;s &quot;very happy&quot; 70% are real. They earned it by paying the standing cost on purpose and putting the data where the work is.&lt;/p&gt;
&lt;p&gt;And if you are small, the most honest take comes from a Backstage vendor. Roadie themselves &lt;a href=&quot;https://dev.to/jianreis/backstage-alternatives-5ah2&quot;&gt;say plainly that not every organisation needs Backstage&lt;/a&gt;, and that below a certain size adopting it is over-engineering. The mistake is almost never &quot;adopted Backstage&quot;. The mistake is adopting any catalog-model system, Backstage or a commercial successor, for the second-bucket data, and then spending finite organisational willpower keeping humans in sync with facts their repositories already declare. That spend is the maintenance everyone complains about, and for that category of data it does not buy accuracy. It buys a graph that is right up to whenever someone last cared.&lt;/p&gt;
&lt;h2&gt;So, is it worth it?&lt;/h2&gt;
&lt;p&gt;After enough of these conversations I have stopped thinking of worth as a property of Backstage at all. It is a property of the match between Backstage&apos;s data model and the data you intend to put in it. For the facts humans register on purpose, the match is good, and for the right organisation the portal earns its keep handsomely. For the cross-repo infrastructure dependencies, the match is wrong at the root, and no amount of budget, frontend talent, or vendor support fixes a model that asks people to register a copy of what they already declared. You will land in the 10% for that part of the portal specifically, and you will have paid for the privilege.&lt;/p&gt;
&lt;p&gt;If the reason you are evaluating Backstage is some version of &quot;we need to know what breaks across our infrastructure before we change it, and we need that to still be true after the person who knows leaves&quot;, then the worth-it calculation is not close, and not because Backstage is bad. It is because that particular fact should never be maintained by hand in the first place.&lt;/p&gt;
&lt;p&gt;That last job is the one I build for. &lt;a href=&quot;https://riftmap.dev/&quot;&gt;Riftmap&lt;/a&gt; connects to a GitLab or GitHub organisation with one read-only token and parses the infrastructure dependency edges directly from the manifests that already declare them, across Terraform, Docker, Helm, Kubernetes, CI templates, and more. There is no catalog to maintain because there is no second copy. The graph cannot drift from the source, because the source is the input, which means it is still right at the moment you bump the base image or touch the shared module and need to know what is downstream. It is not a developer portal and it will not become one. If the value you are after is golden paths, scorecards, and ownership pages, use Backstage or a managed portal, and I mapped the honest options by job in &lt;a href=&quot;https://riftmap.dev/blog/backstage-alternatives/&quot;&gt;Backstage alternatives in 2026&lt;/a&gt;. If the value you are after is knowing what breaks before you ship, that is a different tool, and &lt;a href=&quot;https://app.riftmap.dev&quot;&gt;the free tier maps your whole org, up to 250 repos&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Is Backstage worth it for a small team?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Usually not, and you do not have to take a competitor&apos;s word for it: Roadie, which sells managed Backstage, says plainly that below a certain size adopting an internal developer portal is over-engineering. Backstage earns its cost in organisations large enough that the per-developer overhead amortises and complex enough that the sprawl is a real problem. For a small team, lighter tools for documentation and ownership usually return more for far less standing cost.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why is Backstage adoption so low outside Spotify?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Spotify&apos;s head of Backstage engineering has put the average external adoption rate at around 10%, against 99% internally, and attributed the gap to teams not getting past the proof of concept because they had not pinned down the developer problem they were solving. The deeper mechanism is trust: Spotify keeps its catalog accurate by storing metadata in each repo and making the owning team responsible for it, so the data stays current and people keep using it. Where that discipline does not hold, the data drifts, the portal loses credibility, and usage collapses regardless of how well it was built.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Is Backstage worth it just for the dependency graph?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;No. The dependency graph is the part of a portal that ages worst, because cross-repo infrastructure dependencies are facts about source files (Terraform &lt;code&gt;source&lt;/code&gt; blocks, Dockerfile &lt;code&gt;FROM&lt;/code&gt; lines, Helm chart dependencies, CI includes) that change with every commit, while the catalog copy of them only changes when a human remembers to edit YAML. You end up maintaining a hand-registered copy that is wrong exactly when a risky change makes you need it. For that job a parsed dependency graph, read straight from the manifests, is the right architecture, not a catalog you keep in sync by hand.&lt;/p&gt;
</content:encoded><category>backstage</category><category>platform-engineering</category><category>developer-portals</category><category>service-catalogs</category><author>Daniel Westgaard</author></item><item><title>Backstage alternatives in 2026: first ask why you wanted Backstage</title><link>https://riftmap.dev/blog/backstage-alternatives/</link><guid isPermaLink="true">https://riftmap.dev/blog/backstage-alternatives/</guid><description>Every &quot;Backstage alternatives&quot; roundup lists the same five portals. None of them asks the question that decides which alternative is right: what job sent you looking in the first place?</description><pubDate>Sun, 07 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;Every &quot;Backstage alternatives&quot; roundup lists the same five portals. None of them asks the question that decides which alternative is right: what job sent you looking in the first place?&lt;/em&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;A senior platform engineer at a Nordic consultancy summarised his Backstage evaluation to me in one sentence: the cost of setting it up and keeping it maintained was bigger than what they got back. He is not an outlier. I have heard the same verdict, in nearly the same words, from engineers across r/devops threads, client engagements, and direct conversations. The team evaluates Backstage seriously, sometimes runs a proof of concept, and walks away. Then they type &quot;Backstage alternatives&quot; into a search box, and the search results take over.&lt;/p&gt;
&lt;p&gt;Go read those results. As of mid-2026, every page that ranks is a vendor roundup, and every roundup follows the same script. &lt;a href=&quot;https://www.port.io/blog/top-backstage-alternatives&quot;&gt;Port lists alternatives&lt;/a&gt; and Port is the best one. &lt;a href=&quot;https://www.cortex.io/post/backstage-alternatives-what-engineering-leaders-need-to-know-in-2026&quot;&gt;Cortex lists alternatives&lt;/a&gt; and Cortex is the most comprehensive. &lt;a href=&quot;https://www.opslevel.com/resources/backstage-io-alternatives-4-top-tools-to-use-instead&quot;&gt;OpsLevel lists alternatives&lt;/a&gt; and OpsLevel is the fully managed answer. The supporting cast rotates between Roadie, Mia-Platform, Configure8, &lt;a href=&quot;http://Rely.io&quot;&gt;Rely.io&lt;/a&gt;, and Atlassian Compass, but the structure never changes. Backstage is hard, here are five portals that are easier, ours is first.&lt;/p&gt;
&lt;p&gt;Here is the thing none of those pages will tell you, because their business depends on not telling you. &quot;Backstage alternatives&quot; is not one search. It is at least three different searches wearing the same query, and the right alternative depends entirely on which one is yours. Two of the three are well served by the portal vendors in those roundups. The third is not served by any of them, because the portals inherit the exact property that made you walk away from Backstage.&lt;/p&gt;
&lt;p&gt;This post is the triage the roundups skip. I will try to be fair to every tool in it, including Backstage, because the engineers reading this can smell a strawman from the next time zone. And I will be upfront that I build a tool that fits exactly one of the three jobs, and explicitly does not fit the other two.&lt;/p&gt;
&lt;h2&gt;What Backstage actually is, honestly&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://backstage.io/&quot;&gt;Backstage&lt;/a&gt; is an open-source framework for building internal developer portals, created at Spotify and open-sourced in March 2020. It remains a &lt;a href=&quot;https://www.cncf.io/projects/backstage/&quot;&gt;CNCF Incubating project&lt;/a&gt; with one of the largest contributor communities in the foundation. It pioneered the developer-portal category, and most of the commercial portals in those roundups exist because Backstage proved the demand first.&lt;/p&gt;
&lt;p&gt;The origin story matters more than people give it credit for. Backstage began as an internal Spotify project called System Z, built so that engineers in a fast-growing organisation could understand ownership, dependencies, and versions across an exploding service landscape. Hold onto that word &quot;dependencies&quot;. It comes back later.&lt;/p&gt;
&lt;p&gt;The criticisms are equally well established, and I will not pretend they are mine. Backstage is a framework, not a product. You clone it, stand up a PostgreSQL database, configure authentication, and start writing or installing plugins, most of which are community-maintained without vendor support. The estimates for what this costs are public and not in dispute. The community site &lt;a href=&quot;http://internaldeveloperplatform.org&quot;&gt;internaldeveloperplatform.org&lt;/a&gt; puts the true cost of ownership at &lt;a href=&quot;https://internaldeveloperplatform.org/developer-portals/backstage/&quot;&gt;around $150,000 per 20 developers&lt;/a&gt;, a figure that Port and OpsLevel both cite in their own marketing. Cortex&apos;s roundup says most organisations need two or three full-time engineers for six months or more just to stand up a basic service catalog. Other practitioners put production-readiness at six to twelve months. Gartner has noted that organisations mistakenly believe Backstage is a ready-to-use portal, and that the rude awakening during implementation leads to projects being put on hold or abandoned.&lt;/p&gt;
&lt;p&gt;So far, the roundups and I agree. Backstage is genuinely expensive to run. Where we part ways is on what that means. The roundup logic is: Backstage is expensive, therefore buy a cheaper portal. The actual logic should be: Backstage is expensive, therefore figure out which part of it you wanted, because you might be able to buy just that part, and for one specific part, no portal sells it.&lt;/p&gt;
&lt;h2&gt;The three searches hiding inside one query&lt;/h2&gt;
&lt;p&gt;When a team types &quot;Backstage alternatives&quot;, they arrived there from one of three places. The triage question is which one.&lt;/p&gt;
&lt;h3&gt;Job one: you want what a portal does&lt;/h3&gt;
&lt;p&gt;Some teams want the portal itself. Golden-path templates for scaffolding new services. Scorecards that track whether services have runbooks, SLOs, and passing security scans. A single pane of glass for ownership, on-call, and documentation. Self-service actions that let a developer spin up an environment without filing a ticket.&lt;/p&gt;
&lt;p&gt;If this is your job, the roundups are right and I have nothing contrarian to offer. The commercial portals are real products built by serious teams, and the honest comparison between them comes down to taste and scale. &lt;a href=&quot;https://www.port.io/&quot;&gt;Port&lt;/a&gt; gives you a flexible data model you configure visually rather than in code, which suits organisations whose workflows do not fit standard patterns. &lt;a href=&quot;https://www.cortex.io/&quot;&gt;Cortex&lt;/a&gt; leans hardest into scorecards and engineering standards, which suits organisations whose pain is &quot;we have 400 services and no idea which ones meet our bar&quot;. &lt;a href=&quot;https://www.opslevel.com/&quot;&gt;OpsLevel&lt;/a&gt; is deliberately opinionated, which suits teams that want the vendor to have made the workflow decisions already. All three will get you to a working portal in weeks instead of quarters, and all three cost real money at scale, which is the trade you are making.&lt;/p&gt;
&lt;p&gt;What I want you to notice is what these products have in common with Backstage underneath the better onboarding. They are all catalog-model systems. Each one maintains a registry of entities, services, teams, resources, and the relationships between them, and that registry is populated by some mix of integrations and humans registering things. That is the right architecture for the portal job. Ownership is something a human decides. A runbook link is something a human writes down. Scorecards evaluate criteria a human defined. The catalog model fits because the data genuinely originates with people.&lt;/p&gt;
&lt;h3&gt;Job two: you want Backstage itself, without operating it&lt;/h3&gt;
&lt;p&gt;Some teams evaluated Backstage and concluded the product was right but the operational burden was not. They want the open-source ecosystem, the plugin library, the CNCF governance, and they want someone else to run it.&lt;/p&gt;
&lt;p&gt;This path matured significantly in the last year. &lt;a href=&quot;https://backstage.spotify.com/&quot;&gt;Spotify Portal for Backstage&lt;/a&gt; went GA in October 2025 as a fully managed, no-code SaaS version of Backstage operated by Spotify itself, with setup wizards in place of the configuration work that used to consume the first quarter. &lt;a href=&quot;https://roadie.io/&quot;&gt;Roadie&lt;/a&gt; has offered managed Backstage for years and remains the established independent option, handling hosting, upgrades, and the GitHub rate-limit problems that bite self-hosters.&lt;/p&gt;
&lt;p&gt;If your evaluation said yes to Backstage&apos;s model and no to its operations, this is your category, and it is a perfectly defensible choice. You keep the ecosystem and shed the toil. I have no quarrel with it.&lt;/p&gt;
&lt;p&gt;But notice, again, what does not change. Managed Backstage is still Backstage. The Software Catalog is still populated by &lt;code&gt;catalog-info.yaml&lt;/code&gt; files in your repos, and the relationships in it, including the &lt;code&gt;dependsOn&lt;/code&gt; entries, are still whatever a human last wrote there. Spotify operating the infrastructure does not update your YAML when an engineer changes a Terraform module source. The hosting was never the part that went stale.&lt;/p&gt;
&lt;h3&gt;Job three: you wanted to see what depends on what&lt;/h3&gt;
&lt;p&gt;Now the third search, the one I keep meeting in the wild.&lt;/p&gt;
&lt;p&gt;A meaningful fraction of teams never wanted golden paths or scorecards. They reached for Backstage because of the dependency graph. They wanted the answer to &quot;what breaks if I change this&quot;, or &quot;which repos consume this base image&quot;, or &quot;the engineer who understood how these sixty repos fit together is leaving in three weeks&quot;. They saw the Software Catalog&apos;s dependency view, recognised the thing they were missing, and adopted a developer portal to get it. That is not a misreading of Backstage. It is the original System Z brief: ownership, dependencies, versions. It is also the kind of question a parsed graph answers concretely rather than in the abstract: when I &lt;a href=&quot;https://riftmap.dev/blog/what-208-kubernetes-sigs-repos-actually-depend-on/&quot;&gt;scanned all 208 kubernetes-sigs repos&lt;/a&gt;, 153 of them turned out to import a single shared module — the sort of fan-in a stale catalog never shows you.&lt;/p&gt;
&lt;p&gt;For this job, the catalog model is not the solution with some maintenance cost attached. The maintenance cost &lt;em&gt;is&lt;/em&gt; the failure mode. I wrote about this pattern at length in &lt;a href=&quot;https://riftmap.dev/blog/the-catalog-maintenance-trap/&quot;&gt;the catalog maintenance trap&lt;/a&gt;, but the short version goes like this. A dependency entry in &lt;code&gt;catalog-info.yaml&lt;/code&gt; is a second, hand-registered copy of a fact your repos already declare. The original declaration is the Terraform &lt;code&gt;source&lt;/code&gt; block, the Dockerfile &lt;code&gt;FROM&lt;/code&gt; line, the &lt;code&gt;go.mod&lt;/code&gt; &lt;code&gt;require&lt;/code&gt;, the &lt;code&gt;.gitlab-ci.yml&lt;/code&gt; &lt;code&gt;include&lt;/code&gt;, the Helm &lt;code&gt;Chart.yaml&lt;/code&gt; dependency. Engineers must edit those files to ship. Nothing forces them to edit the catalog YAML to match, so within weeks the manifest and the catalog entry diverge, and the graph in the portal becomes documentation that was supposed to be authoritative. Which is worse than no graph, because people make blast-radius decisions on the assumption it is current. This is the structural weakness of a &lt;a href=&quot;https://riftmap.dev/blog/declared-inferred-registered/&quot;&gt;registered dependency&lt;/a&gt;: unlike a declared edge the build re-reads on every run, nothing executes a catalog, so it drifts at exactly the rate attention wanders.&lt;/p&gt;
&lt;p&gt;Here is the part the roundups structurally cannot say. Switching portal vendors does not escape this. Port&apos;s marketing makes the point against its rivals better than I could: it criticises YAML-based catalogs for creating developer overhead and not updating in real time from the source of truth, eroding trust and adoption. That criticism is correct, and it applies to the entire category whenever the data in question is the dependency graph, because dependencies are facts about source files, and source files change with every commit. A portal can ingest from integrations, and the good ones do for cloud resources and Kubernetes objects. But the cross-repo dependency edges your infrastructure actually runs on, module sources, image references, CI includes, chart dependencies, live in manifests that no portal in those roundups parses.&lt;/p&gt;
&lt;p&gt;So if job three is your job, the honest answer to &quot;what is the best Backstage alternative&quot; is: not a portal. Any portal. The alternative is a different architecture entirely, one where the graph is parsed from the declarations that already exist instead of modelled from entries you ask humans to register. I went deep on that architectural distinction in &lt;a href=&quot;https://riftmap.dev/blog/modeled-graphs-and-parsed-graphs/&quot;&gt;modeled graphs and parsed graphs&lt;/a&gt;; the one-line version is that a parsed graph cannot go stale relative to the source, because the source is the input.&lt;/p&gt;
&lt;h2&gt;The triage, in one table&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Why you wanted Backstage&lt;/th&gt;
&lt;th&gt;Right category&lt;/th&gt;
&lt;th&gt;Representative options&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Golden paths, scaffolding, scorecards, ownership, self-service&lt;/td&gt;
&lt;td&gt;Commercial developer portal&lt;/td&gt;
&lt;td&gt;Port, Cortex, OpsLevel&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backstage&apos;s model and ecosystem, minus the operations&lt;/td&gt;
&lt;td&gt;Managed Backstage&lt;/td&gt;
&lt;td&gt;Spotify Portal, Roadie&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dependency visibility and blast radius across repos&lt;/td&gt;
&lt;td&gt;Parsed dependency graph&lt;/td&gt;
&lt;td&gt;Riftmap, or build your own parser&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Keeping third-party dependencies up to date&lt;/td&gt;
&lt;td&gt;Automated update tooling&lt;/td&gt;
&lt;td&gt;Renovate, Dependabot&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Code search and symbol navigation across repos&lt;/td&gt;
&lt;td&gt;Code intelligence&lt;/td&gt;
&lt;td&gt;Sourcegraph&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;I added the last two rows because they are the other jobs I see mislabelled as portal problems. &lt;a href=&quot;https://riftmap.dev/blog/the-state-of-infrastructure-dependency-tooling-2026/&quot;&gt;Renovate and Dependabot&lt;/a&gt; keep versions current but tell you nothing about who consumes what. Sourcegraph&apos;s symbol graph is genuinely excellent at code-level navigation and stops at the infrastructure boundary, a distinction I unpacked in &lt;a href=&quot;https://riftmap.dev/blog/symbol-graphs-and-artifact-graphs/&quot;&gt;symbol graphs and artifact graphs&lt;/a&gt;. Neither is a Backstage alternative, but both get evaluated as one, which tells you how muddled this category&apos;s vocabulary is. The muddle gained its biggest entrant in June, when GitLab shipped Orbit, a group-wide symbol-and-SDLC graph. &lt;a href=&quot;https://riftmap.dev/blog/gitlab-orbit-and-the-artifact-layer/&quot;&gt;I read everything GitLab Orbit actually shipped&lt;/a&gt;, and it stops at the same infrastructure boundary as Sourcegraph: no HCL, no Dockerfiles, no artifact edges.&lt;/p&gt;
&lt;p&gt;And a row I deliberately left out: &quot;build your own portal from scratch&quot;. Teams do it. Canva did, then migrated off it, and the engineer who ran that migration described the homegrown portal as something they got value from while using it, not wasted work. That is the right way to think about sunk platform investment generally, including a Backstage proof of concept that taught you which job you actually have.&lt;/p&gt;
&lt;h2&gt;The alternatives, compared head to head&lt;/h2&gt;
&lt;p&gt;The triage table above sorts tools by the job that sent you looking. This one puts the named alternatives side by side on the attributes that actually decide the choice — including the two Spotify options people mean when they search &quot;Spotify Backstage alternatives&quot; (Spotify Portal, the managed Backstage SaaS, and Backstage itself, the Spotify-born framework). The column that matters most for job three is the fourth one: whether the cross-repo dependency graph stays current on its own, or drifts until someone re-registers it by hand.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;What it is&lt;/th&gt;
&lt;th&gt;Graph model&lt;/th&gt;
&lt;th&gt;Spans GitHub + GitLab&lt;/th&gt;
&lt;th&gt;Cross-repo dependency graph stays current?&lt;/th&gt;
&lt;th&gt;Pricing model&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Backstage&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Open-source portal framework (Spotify-born)&lt;/td&gt;
&lt;td&gt;Registered catalog (&lt;code&gt;catalog-info.yaml&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Any host, via plugins&lt;/td&gt;
&lt;td&gt;No — &lt;code&gt;dependsOn&lt;/code&gt; is hand-registered and drifts&lt;/td&gt;
&lt;td&gt;Free framework; high TCO (~$150k per 20 devs)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Spotify Portal&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Managed Backstage SaaS, run by Spotify&lt;/td&gt;
&lt;td&gt;Registered catalog&lt;/td&gt;
&lt;td&gt;Any host, via plugins&lt;/td&gt;
&lt;td&gt;No — same catalog YAML, hosted&lt;/td&gt;
&lt;td&gt;Commercial SaaS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Roadie&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Managed Backstage (independent)&lt;/td&gt;
&lt;td&gt;Registered catalog&lt;/td&gt;
&lt;td&gt;Any host, via plugins&lt;/td&gt;
&lt;td&gt;No — same catalog YAML, hosted&lt;/td&gt;
&lt;td&gt;Commercial SaaS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Port&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Commercial developer portal&lt;/td&gt;
&lt;td&gt;Configured catalog + integrations&lt;/td&gt;
&lt;td&gt;Any host, via integrations&lt;/td&gt;
&lt;td&gt;Partial — integrations refresh cloud/K8s entities, not manifest cross-repo edges&lt;/td&gt;
&lt;td&gt;Commercial; free tier&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cortex&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Commercial developer portal&lt;/td&gt;
&lt;td&gt;Registered catalog + scorecards&lt;/td&gt;
&lt;td&gt;Any host, via integrations&lt;/td&gt;
&lt;td&gt;Partial — same integration limit&lt;/td&gt;
&lt;td&gt;Commercial&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;OpsLevel&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Commercial developer portal&lt;/td&gt;
&lt;td&gt;Registered catalog + integrations&lt;/td&gt;
&lt;td&gt;Any host, via integrations&lt;/td&gt;
&lt;td&gt;Partial — same integration limit&lt;/td&gt;
&lt;td&gt;Commercial&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Riftmap&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Parsed cross-repo dependency graph&lt;/td&gt;
&lt;td&gt;Parsed from manifests (no catalog)&lt;/td&gt;
&lt;td&gt;Yes — GitHub + GitLab from a read-only token&lt;/td&gt;
&lt;td&gt;Yes — re-parsed from source, so it cannot drift relative to the repos&lt;/td&gt;
&lt;td&gt;Free tier (whole org, up to 250 repos); paid tiers&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Read the table by column, not by row. On portal features — templates, scorecards, self-service — Port, Cortex, OpsLevel, and both Backstage flavours are the real field, and the triage table above tells you which fits. On the dependency-graph column, every catalog-model tool lands on &quot;no&quot; or &quot;partial&quot; for the same structural reason: a registered edge is a hand-kept copy of a fact the repos already declare, so it is only ever as current as the last person who remembered to update it. Riftmap is the one row that answers &quot;yes&quot;, and it is the only row that is not a portal — which is the whole point of the third search.&lt;/p&gt;
&lt;h2&gt;Where Backstage genuinely wins&lt;/h2&gt;
&lt;p&gt;I want to be precise about when the answer to &quot;Backstage alternatives&quot; is &quot;none, use Backstage&quot;, because that answer is real.&lt;/p&gt;
&lt;p&gt;If you have a platform team with frontend capacity, a genuine need to own and extend the portal, and an organisation large enough that the per-developer cost of the framework amortises, Backstage is a defensible choice that thousands of organisations have made work. The plugin ecosystem is unmatched. The CNCF governance means it will outlive any single vendor&apos;s funding cycle. And the things humans should register on purpose, ownership, on-call, runbooks, tech docs, are things Backstage handles well precisely because the catalog model fits them.&lt;/p&gt;
&lt;p&gt;The mistake is not adopting Backstage. The mistake is adopting any catalog-model system, Backstage or its commercial successors, &lt;em&gt;for the dependency graph&lt;/em&gt;, and then spending organisational willpower trying to keep humans updating a hand-registered copy of facts the repos already state. That spend is the maintenance cost everyone complains about, and it does not buy accuracy. It buys a graph that is accurate to within whenever someone last cared.&lt;/p&gt;
&lt;h2&gt;The question underneath the query&lt;/h2&gt;
&lt;p&gt;The roundups argue about which portal. After two years of conversations with teams who walked away from Backstage, I think the better argument is about which job. The portal jobs are well served, by the portals and by managed Backstage, and the vendors fighting over that SERP have earned their places in it. The dependency-visibility job is the one that query quietly smuggles in, and it is the one place where every option in every roundup shares Backstage&apos;s actual weakness rather than fixing it.&lt;/p&gt;
&lt;p&gt;If the sentence that sent you searching was some version of &quot;we wanted to know what breaks when we change things, and the catalog could not keep up&quot;, then you were never shopping for a portal. You were shopping for a graph, and the graph already exists, written across your Terraform sources, Dockerfiles, CI includes, chart dependencies, and module files. The work is parsing it, not registering it again by hand.&lt;/p&gt;
&lt;p&gt;That parsing is what I build. &lt;a href=&quot;https://riftmap.dev/&quot;&gt;Riftmap&lt;/a&gt; connects to a GitLab or GitHub org with a read-only token, parses the dependency declarations across twelve ecosystems, Terraform, Docker, Helm, Kubernetes, CI templates, Go, npm, Python, Ansible, and more, and serves the resulting graph two ways: a blast-radius UI for engineers, and a &lt;a href=&quot;https://riftmap.dev/for-agents/&quot;&gt;JSON API for coding agents&lt;/a&gt; that need cross-repo context at planning time. There is no catalog to maintain because there is no catalog. If your job is one of the other two, use the table above with my blessing; Riftmap is not a portal and will not become one. If your job is the third one, &lt;a href=&quot;https://app.riftmap.dev&quot;&gt;the free tier maps your whole org, up to 250 repos&lt;/a&gt; and the first scan takes about ninety seconds, which is less time than reading one more roundup.&lt;/p&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;What is the best alternative to Backstage?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;It depends on which job sent you looking. If you want golden paths, scaffolding, scorecards, and self-service, a commercial developer portal like Port, Cortex, or OpsLevel is the right category. If you want Backstage&apos;s model and plugin ecosystem without operating it, managed Backstage — Spotify Portal or Roadie — fits. If you wanted dependency visibility and blast radius across repos, no portal solves that well, because the dependency graph is a fact about source files; a parsed dependency graph like Riftmap reads it from the manifests instead of asking humans to register it again.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What are the alternatives to Spotify Backstage?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&quot;Spotify Backstage&quot; is two products people conflate. Backstage is the open-source framework Spotify created and open-sourced in 2020; Spotify Portal is Spotify&apos;s own fully managed SaaS version of it, GA since October 2025. Alternatives depend on which one you mean and why. If Spotify Portal&apos;s managed model appeals but you want an independent operator, Roadie offers managed Backstage too. If you want the portal capabilities without Backstage&apos;s plugin architecture, Port, Cortex, and OpsLevel are the commercial portals. And if what drew you to Backstage was its dependency view — knowing what breaks when you change something across repos — no Backstage flavour, managed or not, keeps that graph current, because it lives in hand-registered &lt;code&gt;catalog-info.yaml&lt;/code&gt;; a parsed dependency graph like Riftmap reads it from the manifests instead. The comparison table above puts all of these side by side.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why do teams abandon Backstage?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The most common reason is that the cost of standing it up and keeping it maintained exceeds the value returned. Backstage is a framework, not a finished product: the community site &lt;a href=&quot;http://internaldeveloperplatform.org&quot;&gt;internaldeveloperplatform.org&lt;/a&gt; estimates a total cost of ownership around $150,000 per 20 developers, and roundups note most organisations need two to three full-time engineers for six months or more just to stand up a basic service catalog. The dependency graph in particular goes stale, because catalog YAML is a hand-registered copy of facts the repos already state, and nothing forces engineers to keep it in sync.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Is Backstage still worth using in 2026?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Yes, for the right team. If you have a platform team with frontend capacity, a genuine need to own and extend the portal, and an organisation large enough that the per-developer cost amortises, Backstage is a defensible choice with an unmatched plugin ecosystem and CNCF governance. The mistake is not adopting Backstage — it is adopting any catalog-model system, Backstage or its commercial successors, as the dependency graph, and then spending organisational willpower keeping humans updating facts the repos already declare. The deep-dive on this question, with Spotify&apos;s own external-adoption figure of around 10% and a diagnostic for predicting your own outcome before you commit, is in &lt;a href=&quot;https://riftmap.dev/blog/is-backstage-worth-it/&quot;&gt;is Backstage worth it?&lt;/a&gt;.&lt;/p&gt;
</content:encoded><category>backstage</category><category>platform-engineering</category><category>developer-portals</category><category>service-catalogs</category><category>dependency-management</category><author>Daniel Westgaard</author></item><item><title>How to give GitHub Copilot cross-repo context today</title><link>https://riftmap.dev/blog/how-to-give-copilot-cross-repo-context/</link><guid isPermaLink="true">https://riftmap.dev/blog/how-to-give-copilot-cross-repo-context/</guid><description>Three working ways to get cross-repo context into GitHub Copilot right now — multi-root workspaces, Copilot Spaces, and MCP for the cloud agent — and the one question all three leave you to answer by hand.</description><pubDate>Sat, 06 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;In March 2026, someone filed &lt;a href=&quot;https://github.com/orgs/community/discussions/189213&quot;&gt;a feature request on GitHub Community&lt;/a&gt; that I have thought about more than any product announcement from that month. The setup is two repositories. A web app and an &lt;code&gt;orders-service&lt;/code&gt; it consumes. The web app calls the service&apos;s endpoints, depends on its DTOs, and has to stay aligned with every route rename, payload change, and validation rule. The request walks through what Copilot cannot do across that boundary: it cannot reason about contracts defined in the other repo, cannot detect when the frontend calls an endpoint that no longer exists, cannot coordinate one change across both sides. It ends with two questions. Is this on the roadmap, and are there recommended best practices to approximate this behaviour today?&lt;/p&gt;
&lt;p&gt;GitHub has not answered. I eventually left a reply in the thread myself, because the second question deserved one, and this post is the long version of that reply.&lt;/p&gt;
&lt;p&gt;The honest answer is more useful than &quot;wait for the roadmap&quot;. As of June 2026 there are three working ways to give GitHub Copilot context across repositories. All three are real, all three ship today, and all three have a ceiling that is worth knowing about before you invest in one. Underneath all three sits the same unanswered question, and that question is the part I actually want to get to.&lt;/p&gt;
&lt;h2&gt;What Copilot can see today, precisely&lt;/h2&gt;
&lt;p&gt;It is worth being precise here, because the answer is different for each Copilot surface, and it changes fast enough that this paragraph carries a date.&lt;/p&gt;
&lt;p&gt;Copilot Chat in VS Code sees the workspace you have open. One folder open means one repo&apos;s worth of context. Copilot Chat on &lt;a href=&quot;http://github.com&quot;&gt;github.com&lt;/a&gt; is scoped to a single repository, or to a Space, which we will get to. And the &lt;a href=&quot;https://docs.github.com/copilot/concepts/agents/coding-agent/about-coding-agent&quot;&gt;Copilot cloud agent&lt;/a&gt;, the one you hand an issue and get a pull request back from, runs in its own environment scoped to the single repository where the task was opened, holding a repository-scoped &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; that cannot read its siblings.&lt;/p&gt;
&lt;p&gt;None of this is an oversight. It is a permissions model doing its job. Which is why every working approach to cross-repo context is a way of routing around that model deliberately: widen the workspace, curate a context set, or hand the agent extra credentials. Those are the three approaches, in that order.&lt;/p&gt;
&lt;h2&gt;Approach 1: a multi-root workspace in VS Code&lt;/h2&gt;
&lt;p&gt;The cheapest path, and the one most teams should try first. VS Code supports multi-root workspaces: a &lt;code&gt;.code-workspace&lt;/code&gt; file listing several repository folders that open together in one window.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;folders&quot;: [
    { &quot;path&quot;: &quot;web-app&quot; },
    { &quot;path&quot;: &quot;orders-service&quot; },
    { &quot;path&quot;: &quot;platform-context&quot; }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Copilot Chat indexes across every folder in the workspace, and &lt;code&gt;#codebase&lt;/code&gt; searches all of them. For the web-app and orders-service case this is genuinely transformative: both sides of the contract are in the window, so &quot;does the frontend call anything I just renamed&quot; becomes an answerable question.&lt;/p&gt;
&lt;p&gt;The pattern has grown a refinement that is worth copying. Several teams now pair the workspace with &lt;a href=&quot;https://docs.github.com/en/copilot/how-tos/custom-instructions/adding-repository-custom-instructions-for-github-copilot&quot;&gt;repository custom instructions&lt;/a&gt;, a &lt;code&gt;.github/copilot-instructions.md&lt;/code&gt; per repo, and the more advanced version adds a dedicated context-only repository to the workspace. Arinco published &lt;a href=&quot;https://arinco.com.au/blog/github-copilot-multi-repo-instructions/&quot;&gt;a detailed writeup of running this across a 15-plus-repo platform&lt;/a&gt; this week: a repo containing nothing but Copilot customisation files, added as a workspace folder, whose shared instructions file acts as a routing table describing the architecture and pointing at each repo&apos;s own conventions.&lt;/p&gt;
&lt;p&gt;That is the strongest version of the approach, so let me be fair to it before drawing the line. At two to five tightly coupled repos, with someone who cares keeping the instructions current, this works, and it costs an afternoon.&lt;/p&gt;
&lt;p&gt;It stops in three ways, and they compound.&lt;/p&gt;
&lt;p&gt;First, the workspace does not load itself into the model&apos;s context. The agent searches and greps it, every session, and that cost is paid again every session. Meta&apos;s published numbers put a graph lookup for &quot;what depends on X&quot; at roughly 200 tokens against roughly 6,000 for answering the same question by exploration, a 30x difference I went through in detail in &lt;a href=&quot;https://riftmap.dev/blog/you-dont-need-a-virtual-monorepo/&quot;&gt;the virtual monorepo post&lt;/a&gt;. Grep over a workspace is O(N) in workspace size, and the workspace only ever grows.&lt;/p&gt;
&lt;p&gt;Second, the instructions file decays. It is a hand-written map of how the system fits together, and the system keeps moving after the map is written. The research on hand-written context files is sobering, marginal gains at meaningfully higher inference cost, and Meta&apos;s engineering team named the underlying problem in one line: &quot;context that decays is worse than no context&quot;. A confident agent navigating by a stale map does not feel stale. It feels fast, right up until the change lands.&lt;/p&gt;
&lt;p&gt;Third, somebody chose which folders go in that &lt;code&gt;.code-workspace&lt;/code&gt; file, and they chose from memory. Hold that thought.&lt;/p&gt;
&lt;h2&gt;Approach 2: Copilot Spaces&lt;/h2&gt;
&lt;p&gt;The native option, and the one I see teams miss because it lives on &lt;a href=&quot;http://github.com&quot;&gt;github.com&lt;/a&gt; rather than in the editor. &lt;a href=&quot;https://docs.github.com/en/copilot/concepts/context/spaces&quot;&gt;Copilot Spaces&lt;/a&gt; let you assemble a curated context set, &lt;a href=&quot;https://github.blog/changelog/2025-08-13-add-repositories-to-spaces/&quot;&gt;including entire repositories&lt;/a&gt;, plural, alongside specific files and folders, pull requests, issues, uploaded documents, and free-text notes, then chat with Copilot grounded in exactly that set. You can attach custom instructions, share the space with your organisation, and the GitHub-based sources stay synced as the code changes. Any Copilot licence can use it.&lt;/p&gt;
&lt;p&gt;Credit where due: this is zero infrastructure, it is the only genuinely multi-repo Copilot surface GitHub ships today, and for a team that lives on &lt;a href=&quot;http://github.com&quot;&gt;github.com&lt;/a&gt; it is the lowest-friction answer on this list. A space holding web-app, orders-service, and the API contract files is a real improvement for onboarding questions, contract questions, and &quot;explain how these fit together&quot; questions.&lt;/p&gt;
&lt;p&gt;The ceiling is in how it retrieves. When you attach a whole repository, Copilot searches within it for relevant content rather than loading it, and &lt;a href=&quot;https://docs.github.com/en/enterprise-cloud@latest/copilot/how-tos/provide-context/use-copilot-spaces/create-copilot-spaces&quot;&gt;GitHub&apos;s own guidance&lt;/a&gt; is to curate the specific files that matter because that is what produces the best answers. So the quality of a space is the quality of its curation. Sources are grounded in the latest state of the main branch, which means in-flight work on branches is invisible to it. And a space is a chat surface: it informs the human asking, it does not gate a deploy or coordinate a change. It answers questions about the repos somebody remembered to add.&lt;/p&gt;
&lt;p&gt;That phrase again. Somebody curated the source list, by hand, from memory.&lt;/p&gt;
&lt;h2&gt;Approach 3: give the cloud agent reach with MCP&lt;/h2&gt;
&lt;p&gt;The first two approaches help you, working interactively. The third helps the autonomous path, the cloud agent that takes an issue and opens a pull request, which is exactly the surface the original feature request was filed against.&lt;/p&gt;
&lt;p&gt;The agent&apos;s environment is configured through a &lt;code&gt;copilot-setup-steps.yml&lt;/code&gt; workflow file, and the community workaround for its single-repo scoping is now well documented: &lt;a href=&quot;https://devactivity.com/insights/unlocking-cross-repository-access-for-github-copilot-cloud-agents-boosting-software-engineer-performance/&quot;&gt;configure an MCP server in the agent&apos;s environment&lt;/a&gt;, typically the GitHub MCP server itself, supplied with a fine-grained personal access token stored as an Actions secret, scoped read-only to the sibling repositories it needs. The agent working in web-app can then search and read orders-service mid-task.&lt;/p&gt;
&lt;p&gt;This works, and for teams committed to the cloud agent it is currently the only way to get cross-repo awareness into it at all. It is also the approach with the most operational surface: a PAT to mint, rotate, and audit, a workflow file to maintain per repo, and a security review conversation about why an autonomous agent holds credentials to repositories beyond the one it is changing.&lt;/p&gt;
&lt;p&gt;And it has the limit I keep writing about, because it is the limit underneath this whole product category: this is access, not structure. I made the full argument in &lt;a href=&quot;https://riftmap.dev/blog/repo-access-was-never-the-hard-part/&quot;&gt;Repo access was never the hard part&lt;/a&gt;, so here it is in two sentences. The agent can now read orders-service while editing web-app. Nothing tells it that it should, or that a third repo consumes the same contract and is not in its token&apos;s scope at all.&lt;/p&gt;
&lt;h2&gt;The question all three approaches skip&lt;/h2&gt;
&lt;p&gt;Look at what the three approaches have in common. A &lt;code&gt;.code-workspace&lt;/code&gt; file with a folders list. A Space with a sources list. A PAT scoped to a repository list. Every one of them answers the question &quot;how do I put more repositories in front of Copilot&quot;, and every one of them quietly delegates the harder question back to you: &lt;em&gt;which repositories?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;That list is a hand-maintained model of what depends on what. Maintaining it has a name on this blog, because platform teams have been running this exact experiment for years with service catalogs: it is &lt;a href=&quot;https://riftmap.dev/blog/the-catalog-maintenance-trap/&quot;&gt;the catalog maintenance trap&lt;/a&gt;. The list is accurate the day it is written, it drifts the moment the system changes, and nothing tells you it drifted. A workspace missing the repo that consumes your contract does not error. It just gives the agent a confident, complete-looking view of an incomplete world, which is the precise failure mode where &lt;a href=&quot;https://riftmap.dev/blog/inferred-context-is-not-a-dependency-graph/&quot;&gt;a wrong map beats no map for damage done&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;What surprised me, going through the tooling landscape for this post, is that nothing else in the stack answers the question either. Renovate understands dependency manifests more deeply than almost any tool in existence, one repository at a time. Asked directly whether cross-repository dependency detection was planned, a maintainer&apos;s answer &lt;a href=&quot;https://github.com/renovatebot/renovate/discussions/37836&quot;&gt;this May was unambiguous&lt;/a&gt;: &quot;Renovate only operates on a per-repository basis&quot;, with no plans to change. Dependabot has the same shape. The tools that read your manifests every single day read them one repo at a time, and then forget what they saw. The platform vendors are starting to move, though inside their own walls: &lt;a href=&quot;https://riftmap.dev/blog/gitlab-orbit-and-the-artifact-layer/&quot;&gt;GitLab Orbit&lt;/a&gt;, shipped in June, is a queryable symbol-and-SDLC graph for agents, and it is GitLab-only, so a GitHub estate running Copilot gets nothing from it, and the artifact edges stay unmapped either way.&lt;/p&gt;
&lt;p&gt;So the &quot;which repositories&quot; question goes unanswered by default. But here is the thing: for most of the edges that matter, the answer is already written down.&lt;/p&gt;
&lt;p&gt;I will concede the exception first, because it is real. The rawest version of the web-app to orders-service edge, a &lt;code&gt;fetch&lt;/code&gt; against a service URL, is declared nowhere. No parser will ever find it, and anyone who tells you otherwise is inferring it from names and hoping. But the moment a team formalises that contract, a shared types package, a client SDK, a published schema, and at any scale beyond two repos they do, the edge lands in a manifest. A &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-internal-npm-package/&quot;&gt;&lt;code&gt;package.json&lt;/code&gt; dependency on the contracts package&lt;/a&gt;. A &lt;code&gt;go.mod&lt;/code&gt; require. And the edges that carry the rest of the org are declared the same way: Terraform &lt;code&gt;source&lt;/code&gt; blocks pointing at module repos, Dockerfile &lt;code&gt;FROM&lt;/code&gt; lines pointing at base images built elsewhere, Helm &lt;code&gt;Chart.yaml&lt;/code&gt; dependencies, GitLab CI &lt;code&gt;include&lt;/code&gt;s and reusable GitHub Actions workflow &lt;code&gt;uses:&lt;/code&gt; references. I have spent &lt;a href=&quot;https://riftmap.dev/blog/series/find-every-consumer/&quot;&gt;a whole series&lt;/a&gt; walking those edges one ecosystem at a time. They are deterministic. Parsed, not inferred. The dependency graph that should be writing your workspace file already exists in your org&apos;s manifests, unassembled.&lt;/p&gt;
&lt;h2&gt;Feeding the graph back into Copilot&lt;/h2&gt;
&lt;p&gt;This is the part that turns the argument into a setup, so let me be concrete. A queryable cross-repo dependency graph does not replace the three approaches above. It feeds them, in three places.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;It writes the lists.&lt;/strong&gt; The folders array in the workspace file, the source list in the Space, the repo scope on the agent&apos;s PAT: generate them from a dependents query instead of from memory. &quot;Every repo that consumes the orders-service contract package&quot; is one API call, and when the graph changes, the lists change with it. The curation problem does not get solved by curating harder. It gets solved by deriving.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;It is a tool call during the task.&lt;/strong&gt; In agent mode, Copilot can call out to tools, and the graph as an HTTP API means a planning step can ask &quot;who depends on what I am about to change&quot; before the first edit. With &lt;a href=&quot;https://riftmap.dev/for-agents/&quot;&gt;Riftmap&lt;/a&gt; that is two calls:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Resolve the working tree to a node in the graph
REPO_ID=$(curl -s \
  &quot;https://api.riftmap.dev/api/v1/repositories/lookup?url=https://github.com/myorg/orders-service&quot; \
  -H &quot;X-API-Key: $RIFTMAP_API_KEY&quot; | jq -r &apos;.id&apos;)

# One round-trip: dependencies, dependents, artifacts
curl -s &quot;https://api.riftmap.dev/api/v1/repositories/$REPO_ID/context&quot; \
  -H &quot;X-API-Key: $RIFTMAP_API_KEY&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For the cloud agent, the same pattern works with the API key as an Actions secret in the agent&apos;s environment. The &lt;a href=&quot;https://docs.riftmap.dev/agents/overview&quot;&gt;agent integration guide&lt;/a&gt; covers the full call pattern, and one rule from it matters more than the rest: every response carries &lt;code&gt;last_scanned_at&lt;/code&gt; and &lt;code&gt;last_activity_at&lt;/code&gt;, and if the repo has been pushed to since it was last scanned, the agent treats the graph as stale and says so. That is the &quot;context that decays&quot; problem handled as a contract rather than a hope.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;It is the same map at review time.&lt;/strong&gt; On a pull request touching a shared component, CI queries the transitive impact and posts the consumer list as a comment, so the human reviewing the agent&apos;s change is checking it against the same structural account the agent planned with, instead of against memory.&lt;/p&gt;
&lt;p&gt;That last point is the architecture I think this whole category lands on, and you do not need my product to adopt it. Mabl &lt;a href=&quot;https://www.mabl.com/blog/how-we-built-a-system-for-ai-agents-to-ship-real-code-across-75-repos&quot;&gt;built their own coordination graph by hand&lt;/a&gt; and runs agents across 100-plus repos on top of it. The pattern is the point: a parsed, queryable graph underneath, and Copilot, or whichever agent you run, consuming it. Riftmap is the version of that substrate you do not have to build, auto-discovered from one read-only token across GitLab or GitHub, with the API above on every tier.&lt;/p&gt;
&lt;p&gt;So, to answer the question the feature request actually asked. The best practice for approximating cross-repo context in Copilot today is one of the three approaches above, chosen by which Copilot surface you live in. The best practice for making any of them survive contact with a changing org is to stop hand-maintaining the repository lists they all depend on, and derive them from the graph your manifests already declare.&lt;/p&gt;
&lt;p&gt;Every one of these approaches ends in a list of repositories that somebody has to get right. You can maintain that list, or you can derive it. Only one of those is still correct three months from now.&lt;/p&gt;
</content:encoded><category>ai-coding-agents</category><category>cross-repo-context</category><category>github-copilot</category><category>multi-repo</category><author>Daniel Westgaard</author></item><item><title>Monorepo vs polyrepo: the debate is measuring the wrong thing</title><link>https://riftmap.dev/blog/monorepo-vs-polyrepo/</link><guid isPermaLink="true">https://riftmap.dev/blog/monorepo-vs-polyrepo/</guid><description>Monorepo vs polyrepo is argued as a code-location debate. The real variable is whether &quot;what depends on this&quot; is queryable — and infrastructure never got a vote.</description><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;The monorepo vs polyrepo argument is old enough that Buildkite was comparing it to the Vim and Emacs wars back in 2024. It should have been settled, or at least gone quiet. Instead, in the space of six months, an AI coding vendor re-litigated it for the agent era, a benchmark firm published PR cycle-time data across hundreds of organisations, and half the platform engineering threads I read found their way back to it. Something pulled the question out of retirement. I think the something is worth naming, because it is not really about repositories at all.&lt;/em&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;I maintain a product whose entire reason to exist is that most organisations run polyrepos, so I want to be upfront about where I sit before arguing anything. Riftmap parses cross-repo dependencies. If everyone migrated to a monorepo tomorrow, a good part of my roadmap would evaporate. Read what follows with that in mind, and check the sources, all of which are linked.&lt;/p&gt;
&lt;p&gt;With that declared: I think both camps in this debate are arguing about a proxy. The real variable underneath, the one that decides whether your team ships confidently or plays dependency archaeology at 2am, is something the standard pros-and-cons lists never name. This post walks the honest trade-offs first, because they are real and you deserve a straight answer to the question you searched for. Then it gets to the variable.&lt;/p&gt;
&lt;h2&gt;What each side buys you&lt;/h2&gt;
&lt;p&gt;A monorepo is one repository holding many projects. A polyrepo (or multi-repo) setup gives each project, service, or module its own repository. Both are proven at every scale that matters: Google and Meta run famous monorepos, Amazon and Netflix run famous polyrepos, and none of them are wrong.&lt;/p&gt;
&lt;h3&gt;The monorepo&apos;s case&lt;/h3&gt;
&lt;p&gt;The strongest monorepo argument has always been atomic cross-project change. &lt;a href=&quot;https://www.uber.com/blog/ios-monorepo/&quot;&gt;Uber&apos;s iOS team moved to a monorepo&lt;/a&gt; largely for this: when an API contract and all of its clients live in one repo, a breaking change is one commit, one review, one revert path. No choreographed pull requests across six repositories, no compatibility matrix, no deployment ordering.&lt;/p&gt;
&lt;p&gt;You also get unified dependency versions. One lockfile discipline, one toolchain, one place where &quot;which version of the logging library are we on&quot; has a single answer. And, the part this post will keep returning to, you get a build graph. Bazel, Nx, Pants, and Turborepo all maintain an explicit graph of what depends on what, because they need it to know what to rebuild. That graph is a by-product of the architecture. It comes for free, by construction.&lt;/p&gt;
&lt;p&gt;The cost is that the build system becomes infrastructure. Past a certain size you cannot build everything on every change, so you need change detection, remote caching, and a team that owns the machinery. &lt;a href=&quot;https://medium.com/airbnb-engineering/migrating-airbnbs-jvm-monorepo-to-bazel-33f90eda51ec&quot;&gt;Airbnb&apos;s Bazel migration&lt;/a&gt; is the honest writeup of what that takes. There is also a coordination cost that no build tool fixes: shared standards, shared review culture, and consensus across every team in the repo.&lt;/p&gt;
&lt;p&gt;The data backs the &quot;it depends on operational maturity&quot; framing. &lt;a href=&quot;https://www.faros.ai/blog/monorepo-vs-polyrepo-benchmark-data&quot;&gt;Faros.ai&apos;s benchmark analysis&lt;/a&gt; from March 2026, drawn from PR data across many organisations, found that monorepo teams show markedly more variable PR cycle times than polyrepo teams, with heavy tails at the 90th percentile where some organisations blow past ten days on worst-case PRs. Their read is that well-engineered monorepo infrastructure can match polyrepo performance, but the infrastructure has to keep evolving with repository scale, and when it lags, cycle times absorb the difference. A monorepo is a bet that you will keep funding that evolution.&lt;/p&gt;
&lt;h3&gt;The polyrepo&apos;s case&lt;/h3&gt;
&lt;p&gt;Polyrepos buy autonomy. Each repository has its own CI pipeline, its own release cadence, its own ownership boundary that maps cleanly onto a team. A service can deploy twenty times a day while its neighbour ships monthly, and neither blocks the other. Different stacks can coexist without one build tool having to understand all of them. Deprecating a service means archiving a repo, not excavating a directory.&lt;/p&gt;
&lt;p&gt;The same Faros data shows the flip side of the monorepo&apos;s variance: polyrepo teams sit in a tighter, more predictable cycle-time range. Small repos are comprehensible, reviews are scoped, and nothing in repo A&apos;s CI can make repo B&apos;s pipeline slower.&lt;/p&gt;
&lt;p&gt;The cost is that everything cross-cutting gets harder. A change spanning three services is three PRs with a coordination problem attached. Version skew creeps in quietly until service A and service B disagree about a shared library in a way that only shows up in production. Standardising anything across fifty repos, security scanning, CI conventions, dependency policy, is a campaign rather than a commit.&lt;/p&gt;
&lt;p&gt;And there is one more cost, the one the listicles file under &quot;harder dependency management&quot; and move past in a sentence. In a polyrepo, nobody can answer &quot;what depends on this&quot; without going looking. That sentence is doing more work than the debate gives it credit for, and it is where this post is headed.&lt;/p&gt;
&lt;h3&gt;The trade-offs at a glance&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;Monorepo&lt;/th&gt;
&lt;th&gt;Polyrepo&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cross-project changes&lt;/td&gt;
&lt;td&gt;Atomic, one commit&lt;/td&gt;
&lt;td&gt;Coordinated PRs across repos&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dependency versions&lt;/td&gt;
&lt;td&gt;Unified by tooling&lt;/td&gt;
&lt;td&gt;Skew unless actively managed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Team autonomy&lt;/td&gt;
&lt;td&gt;Shared standards required&lt;/td&gt;
&lt;td&gt;Independent by default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deploy cadence&lt;/td&gt;
&lt;td&gt;Decoupled with effort&lt;/td&gt;
&lt;td&gt;Decoupled by default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Build infrastructure&lt;/td&gt;
&lt;td&gt;Serious investment (Bazel, Nx)&lt;/td&gt;
&lt;td&gt;Per-repo, simple, duplicated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PR cycle times (Faros, 2026)&lt;/td&gt;
&lt;td&gt;Higher variance, heavy P90 tails&lt;/td&gt;
&lt;td&gt;Tighter, more predictable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;What depends on this?&quot;&lt;/td&gt;
&lt;td&gt;Build graph answers it&lt;/td&gt;
&lt;td&gt;Nobody answers it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ownership boundaries&lt;/td&gt;
&lt;td&gt;Directory conventions&lt;/td&gt;
&lt;td&gt;Repository boundaries&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;If you came here for the short answer to &quot;which should we choose&quot;: tightly coupled projects with coordinated releases and an organisation willing to fund build infrastructure point to a monorepo. Loosely coupled services, autonomous teams, and varied stacks point to polyrepo. Most organisations past a certain size end up hybrid regardless. That is the honest conventional answer, and it is fine as far as it goes.&lt;/p&gt;
&lt;p&gt;But look at the table again. Seven of those rows are preferences. One of them is a capability.&lt;/p&gt;
&lt;h2&gt;The variable the debate never names&lt;/h2&gt;
&lt;p&gt;Here is the quiet conflation at the centre of this debate. We argue about where code should live, as if co-location were the prize. It is not. The prize that co-location happens to deliver is a queryable dependency graph.&lt;/p&gt;
&lt;p&gt;In a Bazel monorepo, &quot;what breaks if I change this library&quot; is a command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bazel query &quot;rdeps(//..., //libs/auth)&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In an Nx workspace it is &lt;code&gt;nx graph&lt;/code&gt;, or &lt;code&gt;nx affected&lt;/code&gt; scoped to a change. The answer is exact, it is derived from declared build targets, and it is current as of the last commit, because the build system cannot function without it. This is the monorepo&apos;s one structural advantage that survives every counterargument: not the co-location itself, but the graph the build tooling is forced to maintain on top of it.&lt;/p&gt;
&lt;p&gt;Now run the same question in a 200-repo polyrepo estate. The dependencies are all still there. They are declared, in writing, in the manifests: Terraform &lt;code&gt;source&lt;/code&gt; blocks pointing at module repos, Dockerfile &lt;code&gt;FROM&lt;/code&gt; lines pulling a shared base image, &lt;code&gt;go.mod&lt;/code&gt; requires on an internal module, &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-internal-npm-package/&quot;&gt;&lt;code&gt;package.json&lt;/code&gt; dependencies on an internal npm package&lt;/a&gt;, GitLab CI &lt;code&gt;include&lt;/code&gt; statements pulling a central template, Helm charts referenced by release pipelines. The graph exists. Every edge of it is sitting in a file in version control. What does not exist is any system that reads those files and holds the answer. So the question gets answered by grep across checkouts, by asking the senior engineer who was there when the module was written, or by shipping the change and watching what pages.&lt;/p&gt;
&lt;p&gt;This is the reframe I would push on anyone choosing between the two: &lt;strong&gt;the monorepo vs polyrepo decision is, underneath, a decision about whether your dependency graph is declared to a machine or remembered by people.&lt;/strong&gt; A monorepo makes the graph declared, by construction, as a side effect of the build system. A polyrepo leaves it implicit, scattered across manifests that no single tool reads. Almost everything painful about polyrepos at scale, the coordination overhead, the version skew, the 2am archaeology, traces back to that one missing capability. I wrote about &lt;a href=&quot;https://riftmap.dev/blog/ai-doesnt-understand-blast-radius/&quot;&gt;the blast-radius version of this problem&lt;/a&gt; and &lt;a href=&quot;https://riftmap.dev/blog/the-state-of-infrastructure-dependency-tooling-2026/&quot;&gt;the tooling landscape around it&lt;/a&gt; at length, so I will not re-make those arguments here. The point for this post is narrower: once you see the graph as the variable, the debate changes shape, because repo count turns out to be only one way to influence it. And for a large part of your estate, it is a way you were never offered.&lt;/p&gt;
&lt;h2&gt;Infrastructure never got a vote&lt;/h2&gt;
&lt;p&gt;Read any monorepo vs polyrepo piece and notice what the examples are made of. Application services. An auth service, an API gateway, some clients. The tooling named is Nx, Turborepo, pnpm workspaces, Bazel. The entire debate is shaped like application code, and mostly like TypeScript.&lt;/p&gt;
&lt;p&gt;Infrastructure code lives in a different world, and that world is polyrepo by ecosystem convention rather than by anyone&apos;s choice.&lt;/p&gt;
&lt;p&gt;Start with Terraform. The public registry &lt;a href=&quot;https://developer.hashicorp.com/terraform/registry/modules/publish&quot;&gt;requires one module per repository, named &lt;code&gt;terraform-&amp;lt;PROVIDER&amp;gt;-&amp;lt;NAME&amp;gt;&lt;/code&gt;&lt;/a&gt;. It is a hard format requirement; the registry will not accept anything else. Private registries inherit the convention. Practitioners have been &lt;a href=&quot;https://github.com/hashicorp/terraform/issues/26586&quot;&gt;asking HashiCorp to allow multiple modules per repo since 2020&lt;/a&gt;, precisely because maintaining a repository per module is real overhead, and the answer has not changed. If your organisation has forty internal Terraform modules, the ecosystem&apos;s defaults have already decided you have forty repos.&lt;/p&gt;
&lt;p&gt;The same gravity acts on the rest of the infrastructure estate. Shared Helm charts get their own repos so they can be versioned and published to a chart registry. CI templates get centralised into a repo that dozens of pipelines pull in via GitLab CI &lt;code&gt;include&lt;/code&gt; or reusable GitHub Actions workflow calls, because that is the mechanism the CI systems provide. Base images live in their own repos with their own build pipelines because the registry push is the unit of release. None of these were monorepo-vs-polyrepo decisions. They were defaults that arrived bundled with the tools.&lt;/p&gt;
&lt;p&gt;Two consequences follow. First, the infrastructure layer is where the polyrepo&apos;s missing graph hurts most, because infrastructure components are the highest fan-in nodes in the estate. A Terraform module sourced by forty repos, a base image pulled by every service, a CI template included by eighty pipelines. When I &lt;a href=&quot;https://riftmap.dev/blog/what-208-kubernetes-sigs-repos-actually-depend-on/&quot;&gt;scanned all 208 repositories in the kubernetes-sigs organisation&lt;/a&gt;, a single module, &lt;code&gt;sigs.k8s.io/yaml&lt;/code&gt;, turned out to be imported by 153 of them. In &lt;a href=&quot;https://riftmap.dev/blog/what-56-prometheus-repos-depend-on/&quot;&gt;the Prometheus organisation&lt;/a&gt;, 25 of 56 repos import &lt;code&gt;client_golang&lt;/code&gt;. Those are healthy, well-run open-source orgs, polyrepo by design, and the concentration is the normal shape of a polyrepo estate, not a pathology. The question is only whether anyone can see it.&lt;/p&gt;
&lt;p&gt;Second, and this is the part the migration guides skip: moving your application code to a monorepo does not repatriate the infrastructure layer. The Terraform modules stay where the registry wants them. The CI templates stay where the &lt;code&gt;include&lt;/code&gt; mechanism reaches them. The base images stay where the push pipeline lives. You can spend a year of political capital consolidating services into one repository and still wake up with the highest-blast-radius components of your system scattered across repos whose dependency graph nothing reads. The monorepo migration solves the graph problem for the code that moved, and only for the code that moved.&lt;/p&gt;
&lt;p&gt;So when the conventional advice says &quot;high interdependency points to a monorepo&quot;, it is giving you guidance that the most interdependent part of your stack is structurally unable to take.&lt;/p&gt;
&lt;h2&gt;What AI agents change, and what they don&apos;t&lt;/h2&gt;
&lt;p&gt;The reason this debate came out of retirement is AI coding agents, so let&apos;s take that argument seriously, because the strongest version of it is genuinely strong.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.augmentcode.com/learn/monorepo-vs-polyrepo-ai-s-new-rules-for-repo-architecture&quot;&gt;Augment Code&apos;s piece on the question&lt;/a&gt; makes the case well: an assistant that can see the auth service, the gateway, and the clients in one context window reasons about a cross-service change in a way that an assistant grepping five separate repositories cannot. Large context windows weaken the old polyrepo argument that services are easier to understand in isolation, because the AI can hold the interactions. Their conclusion is that AI shifts the calculation toward monorepos. The observation underneath is correct, and I want to be fair to it before pushing back: agents do perform better with cross-repo visibility, and the teams running agents at scale &lt;a href=&quot;https://riftmap.dev/blog/ai-coding-agents-need-cross-repo-context/&quot;&gt;keep converging on exactly that diagnosis&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The push-back is that the argument conflates two different things: what the agent can &lt;em&gt;see&lt;/em&gt; and what the agent can &lt;em&gt;query&lt;/em&gt;. Visibility is access. Access is not structure.&lt;/p&gt;
&lt;p&gt;Stripe is the cleanest evidence, because Stripe is the maximum case for &quot;monorepo plus large model&quot;. Their internal agent system, Minions, was merging over 1,300 AI-written pull requests a week as of February 2026, against a real monorepo of hundreds of millions of lines. And their published architecture does not dump that monorepo into the model. It cannot; in their own words, a global context dump &quot;would overflow any model&apos;s window&quot;. Instead they built directory-scoped rule files that attach as the agent traverses the tree, and an MCP server exposing nearly 500 internal tools the agent queries for structure it does not hold in context. The team with the world&apos;s most complete co-location still had to build the queryable layer on top. Co-location alone did not deliver it.&lt;/p&gt;
&lt;p&gt;Meta&apos;s number makes the same point from the cost side. In &lt;a href=&quot;https://engineering.fb.com/2026/04/06/developer-tools/how-meta-used-ai-to-map-tribal-knowledge-in-large-scale-data-pipelines/&quot;&gt;their tribal-knowledge engine writeup&lt;/a&gt;, answering &quot;what depends on X&quot; by graph lookup costs around 200 tokens; answering it by letting the agent explore costs around 6,000. A 30x difference that no context-window increase closes, because it is an architecture gap, not a capacity gap. And the &lt;a href=&quot;https://arxiv.org/abs/2602.11988&quot;&gt;Gloaguen et al. study from ETH Zurich&lt;/a&gt; showed that the hand-written context files teams reach for instead, the &lt;code&gt;CLAUDE.md&lt;/code&gt; system maps, buy a marginal +4% agent success rate at +19% inference cost. Prose descriptions of structure decay and do not scale; I went deep on that failure mode in &lt;a href=&quot;https://riftmap.dev/blog/you-dont-need-a-virtual-monorepo/&quot;&gt;the virtual monorepo post&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;So the AI-era version of this debate is not &quot;which layout lets the model see more text&quot;. It is &quot;which layout gives the agent a structure it can query instead of reconstruct&quot;. A monorepo with a real build graph is one good answer: the agent calls &lt;code&gt;bazel query&lt;/code&gt; and gets blast radius in one tool call. A polyrepo with a parsed cross-repo graph is the equivalent answer for everyone else. A polyrepo with neither, or a monorepo whose build graph stops at the application layer while the Terraform modules sit outside it, leaves the agent exploring, and exploring is the expensive, fuzzy mode.&lt;/p&gt;
&lt;p&gt;The agents did not change the answer. They raised the price of not having it, because an agent makes cross-repo changes at a rate no human team ever did, and &lt;a href=&quot;https://riftmap.dev/blog/ai-doesnt-understand-blast-radius/&quot;&gt;change failure rates are already absorbing the difference&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;How to actually choose&lt;/h2&gt;
&lt;p&gt;The framework I would use, with the graph as the explicit variable rather than the silent one.&lt;/p&gt;
&lt;h3&gt;Choose a monorepo when the graph can be complete&lt;/h3&gt;
&lt;p&gt;If your projects are tightly coupled, your releases are coordinated, your stack is uniform enough for one build tool, and your organisation will fund the build infrastructure as a product, a monorepo is a strong choice, and the build graph is the most underrated part of what you are buying. Go in with the Faros caveat in view: the performance of a monorepo tracks the maturity of its infrastructure, and the investment is recurring, not one-off. And check the graph&apos;s edges honestly. If your Terraform, Helm, and CI templates will still live outside the repo, know that your build graph will be blind exactly where the blast radius is largest.&lt;/p&gt;
&lt;h3&gt;Stay polyrepo when autonomy is the constraint that binds&lt;/h3&gt;
&lt;p&gt;If your teams ship on independent cadences, your stacks are heterogeneous, ownership maps to repositories, or external contributors and open-source boundaries are involved, polyrepo is not a legacy state to apologise for. It is the layout the Faros data shows delivering predictable cycle times, and it is the layout your infrastructure ecosystem mandates anyway. The cost you are accepting is the undeclared graph, and the mistake is accepting it silently instead of pricing it.&lt;/p&gt;
&lt;h3&gt;Hybrid is normal, not indecision&lt;/h3&gt;
&lt;p&gt;Core tightly-coupled services in one repo, periphery and infrastructure in their own repos, is where most large organisations land, usually without announcing it. It is a reasonable equilibrium. It also means the graph question does not go away, because the cross-repo edges between the monorepo and everything around it are precisely the ones no build tool covers.&lt;/p&gt;
&lt;h3&gt;The fourth option the listicles never give you&lt;/h3&gt;
&lt;p&gt;Most teams reading this are not choosing. They have 100 or 300 repos, a migration would cost a year of politics, and the conventional framing leaves them with &quot;polyrepo, and suffer&quot; as the default. The option that framing hides: keep the polyrepo, and recover the one structural thing the monorepo would have given you. The graph is already written down in your manifests. The &lt;code&gt;source&lt;/code&gt; blocks, the &lt;code&gt;FROM&lt;/code&gt; lines, the &lt;code&gt;go.mod&lt;/code&gt; requires, the CI &lt;code&gt;include&lt;/code&gt;s are all declarations, and declarations can be parsed. Parsed, not inferred: read deterministically from the files, the way a build system reads its targets, rather than guessed from embeddings or filenames. Do that across the estate and &quot;what depends on this&quot; becomes a query in a polyrepo the same way &lt;code&gt;bazel query&lt;/code&gt; makes it one in a monorepo. That is the substrate I have been arguing for &lt;a href=&quot;https://riftmap.dev/what-is-cross-repo-dependency-mapping/&quot;&gt;across this whole blog&lt;/a&gt;, and it is the option that turns the debate from a migration decision into a tooling decision.&lt;/p&gt;
&lt;p&gt;This is, full disclosure repeated, the thing &lt;a href=&quot;https://riftmap.dev/&quot;&gt;Riftmap&lt;/a&gt; builds: a parsed cross-repo dependency graph over your existing GitLab or GitHub organisation, from one read-only token, with the kubernetes-sigs and Prometheus scans above as public examples of the output. You can also build it yourself; teams like Mabl have, and the &lt;a href=&quot;https://riftmap.dev/blog/the-state-of-infrastructure-dependency-tooling-2026/&quot;&gt;tooling survey&lt;/a&gt; covers the landscape honestly. The point of this post is not the product. The point is that the capability, not the repo count, was always the thing being argued about.&lt;/p&gt;
&lt;h2&gt;The question underneath the question&lt;/h2&gt;
&lt;p&gt;Monorepo vs polyrepo asks where your code should live. After two decades of debate, the trade-offs are well mapped and mostly come down to what your organisation is willing to operate. But the question underneath has a sharper edge: when something changes, can you ask your system what breaks, or do you have to remember? A monorepo is one way to make the graph something you ask. It is not the only way, it stops at the build tool&apos;s borders, and for the infrastructure half of your estate it was never on the table. Choose your repo layout for your teams. Then make sure the graph is declared to a machine either way, because that, not the number of repositories, is the thing the debate was always measuring.&lt;/p&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;What is the difference between a monorepo and a polyrepo?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A monorepo is one repository holding many projects, services, or modules. A polyrepo (or multi-repo) setup gives each project its own repository. Both are proven at every scale: Google and Meta run famous monorepos, Amazon and Netflix run famous polyrepos. The difference that actually decides outcomes is not co-location itself but whether your dependency graph is declared to a machine — a monorepo&apos;s build system maintains a queryable graph by construction, while a polyrepo leaves the same graph implicit in manifests no single tool reads.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Is a monorepo or polyrepo better for AI coding agents?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Neither layout wins automatically. AI agents do reason better with cross-repo visibility, but visibility is access, not structure. What an agent needs is a dependency graph it can query rather than reconstruct. A monorepo with a real build graph provides one — the agent calls &lt;code&gt;bazel query&lt;/code&gt; and gets blast radius in a tool call; a polyrepo with a parsed cross-repo graph provides the equivalent. Stripe&apos;s Minions system, running against a giant monorepo, still had to build directory-scoped rules and an MCP server of nearly 500 queryable tools, because co-location alone did not deliver agent-usable structure.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Can infrastructure code like Terraform modules and Helm charts move into a monorepo?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Largely no. Infrastructure code is polyrepo by ecosystem convention, not by choice. The Terraform registry requires one module per repository in the &lt;code&gt;terraform-&amp;lt;PROVIDER&amp;gt;-&amp;lt;NAME&amp;gt;&lt;/code&gt; format; Helm charts, CI templates, and base images each live in their own repos because that is the unit of release the tooling provides. Moving application code into a monorepo does not repatriate them — and they are the highest fan-in, highest-blast-radius components in most estates, so a monorepo&apos;s build graph stays blind exactly where it matters most.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How do you answer &quot;what depends on this?&quot; in a polyrepo?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The dependency graph already exists in your manifests: Terraform &lt;code&gt;source&lt;/code&gt; blocks, Dockerfile &lt;code&gt;FROM&lt;/code&gt; lines, &lt;code&gt;go.mod&lt;/code&gt; requires, and CI &lt;code&gt;include&lt;/code&gt; statements are all declarations sitting in version control. What is missing is a system that reads them and holds the answer. Parsing those declarations across the estate — deterministically, rather than inferring from embeddings or filenames — turns &quot;what depends on this&quot; into a query in a polyrepo the same way &lt;code&gt;bazel query&lt;/code&gt; makes it one in a monorepo. That recovers the capability a monorepo provides as a by-product, without a migration.&lt;/p&gt;

&lt;hr /&gt;
&lt;h2&gt;Sources referenced&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Buildkite, &lt;em&gt;Monorepo vs. polyrepo: How to choose&lt;/em&gt; — &lt;a href=&quot;https://buildkite.com/resources/blog/monorepo-polyrepo-choosing/&quot;&gt;buildkite.com&lt;/a&gt;, March 2024&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://Faros.ai&quot;&gt;Faros.ai&lt;/a&gt;, &lt;em&gt;Monorepo vs Polyrepo: What the PR Benchmark Data Actually Shows&lt;/em&gt; — &lt;a href=&quot;https://www.faros.ai/blog/monorepo-vs-polyrepo-benchmark-data&quot;&gt;faros.ai/blog&lt;/a&gt;, March 2026&lt;/li&gt;
&lt;li&gt;Augment Code, &lt;em&gt;Monorepo vs Polyrepo: AI&apos;s New Rules for Repo Architecture&lt;/em&gt; — &lt;a href=&quot;https://www.augmentcode.com/learn/monorepo-vs-polyrepo-ai-s-new-rules-for-repo-architecture&quot;&gt;augmentcode.com/learn&lt;/a&gt;, updated January 2026&lt;/li&gt;
&lt;li&gt;Uber Engineering, &lt;em&gt;Building the new iOS monorepo&lt;/em&gt; — &lt;a href=&quot;https://www.uber.com/blog/ios-monorepo/&quot;&gt;uber.com/blog&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Airbnb Engineering, &lt;em&gt;Migrating Airbnb&apos;s JVM monorepo to Bazel&lt;/em&gt; — &lt;a href=&quot;https://medium.com/airbnb-engineering/migrating-airbnbs-jvm-monorepo-to-bazel-33f90eda51ec&quot;&gt;medium.com/airbnb-engineering&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;HashiCorp, &lt;em&gt;Publish modules to the Terraform registry&lt;/em&gt; — &lt;a href=&quot;https://developer.hashicorp.com/terraform/registry/modules/publish&quot;&gt;developer.hashicorp.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;hashicorp/terraform issue #26586, &lt;em&gt;Allow multiple modules from the same repo&lt;/em&gt; — &lt;a href=&quot;https://github.com/hashicorp/terraform/issues/26586&quot;&gt;github.com&lt;/a&gt;, open since 2020&lt;/li&gt;
&lt;li&gt;Engineering at Meta, &lt;em&gt;How Meta used AI to map tribal knowledge in large-scale data pipelines&lt;/em&gt; — &lt;a href=&quot;https://engineering.fb.com/2026/04/06/developer-tools/how-meta-used-ai-to-map-tribal-knowledge-in-large-scale-data-pipelines/&quot;&gt;engineering.fb.com&lt;/a&gt;, April 2026&lt;/li&gt;
&lt;li&gt;Signadot, &lt;em&gt;Coding Agents Are Only as Good as the Signals You Feed Them&lt;/em&gt; — &lt;a href=&quot;https://thenewstack.io/coding-agents-feedback-signals/&quot;&gt;thenewstack.io&lt;/a&gt;, April 2026&lt;/li&gt;
&lt;li&gt;Gloaguen et al., ETH Zurich and &lt;a href=&quot;http://LogicStar.ai&quot;&gt;LogicStar.ai&lt;/a&gt;, &lt;em&gt;Do Context Files Help Coding Agents?&lt;/em&gt; — &lt;a href=&quot;https://arxiv.org/abs/2602.11988&quot;&gt;arxiv.org/abs/2602.11988&lt;/a&gt;, February 2026&lt;/li&gt;
&lt;li&gt;Joel Parker Henderson, &lt;em&gt;monorepo-vs-polyrepo&lt;/em&gt; — &lt;a href=&quot;https://github.com/joelparkerhenderson/monorepo-vs-polyrepo&quot;&gt;github.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Riftmap, &lt;em&gt;You don&apos;t need a virtual monorepo. You need a graph.&lt;/em&gt; — &lt;a href=&quot;https://riftmap.dev/blog/you-dont-need-a-virtual-monorepo/&quot;&gt;riftmap.dev/blog&lt;/a&gt;, May 2026&lt;/li&gt;
&lt;li&gt;Riftmap, &lt;em&gt;What 208 kubernetes-sigs repos actually depend on&lt;/em&gt; — &lt;a href=&quot;https://riftmap.dev/blog/what-208-kubernetes-sigs-repos-actually-depend-on/&quot;&gt;riftmap.dev/blog&lt;/a&gt;, May 2026&lt;/li&gt;
&lt;li&gt;Riftmap, &lt;em&gt;What 56 Prometheus repos actually depend on&lt;/em&gt; — &lt;a href=&quot;https://riftmap.dev/blog/what-56-prometheus-repos-depend-on/&quot;&gt;riftmap.dev/blog&lt;/a&gt;, April 2026&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Appendix: structured summary&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Claim:&lt;/strong&gt; The monorepo vs polyrepo debate argues about a proxy. The variable that decides outcomes is whether the organisation&apos;s dependency graph is declared to a machine or remembered by people. A monorepo delivers a queryable graph as a by-product of its build system (Bazel &lt;code&gt;rdeps&lt;/code&gt;, &lt;code&gt;nx graph&lt;/code&gt;); a polyrepo leaves the same graph implicit in manifests that no single tool reads. Infrastructure code (Terraform modules, Helm charts, CI templates, base images) is polyrepo by ecosystem convention, cannot follow an application-code monorepo migration, and is where fan-in, and therefore blast radius, concentrates.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Definitions:&lt;/strong&gt; A monorepo is one repository containing many projects. A polyrepo gives each project or module its own repository. Both are proven at all scales.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Evidence:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;http://Faros.ai&quot;&gt;Faros.ai&lt;/a&gt; (March 2026): monorepo teams show higher variance in PR cycle times with heavy P90 tails; polyrepo teams sit in a tighter, more predictable range. Monorepo performance tracks investment in build infrastructure.&lt;/li&gt;
&lt;li&gt;HashiCorp&apos;s public Terraform registry requires one module per repository in the &lt;code&gt;terraform-&amp;lt;PROVIDER&amp;gt;-&amp;lt;NAME&amp;gt;&lt;/code&gt; format; practitioners have requested multi-module repos since 2020 (hashicorp/terraform #26586) without a change.&lt;/li&gt;
&lt;li&gt;Riftmap org scans: 153 of 208 kubernetes-sigs repos import &lt;code&gt;sigs.k8s.io/yaml&lt;/code&gt;; 25 of 56 Prometheus repos import &lt;code&gt;client_golang&lt;/code&gt;. High fan-in is the normal shape of a polyrepo estate.&lt;/li&gt;
&lt;li&gt;Stripe Minions (1,300+ AI-written merged PRs/week against a monorepo of hundreds of millions of lines) does not use a global context dump; it uses directory-scoped rules and an MCP server exposing ~500 queryable tools. Co-location alone did not deliver agent-usable structure.&lt;/li&gt;
&lt;li&gt;Meta: &quot;what depends on X&quot; costs ~200 tokens as a graph lookup vs ~6,000 by agent exploration, a 30x architecture gap.&lt;/li&gt;
&lt;li&gt;Gloaguen et al. (arXiv:2602.11988): hand-written context files improve agent success +4% at +19% inference cost; they do not substitute for queryable structure.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Decision framework:&lt;/strong&gt; Choose a monorepo when coupling is tight, releases are coordinated, and the organisation will fund build infrastructure as a recurring product. Stay polyrepo when team autonomy, heterogeneous stacks, or ecosystem conventions dominate. Hybrid is the common equilibrium. The fourth option, missing from conventional comparisons: keep the polyrepo and recover the monorepo&apos;s structural advantage by parsing the dependency graph already declared in manifests (Terraform &lt;code&gt;source&lt;/code&gt; blocks, Dockerfile &lt;code&gt;FROM&lt;/code&gt; lines, &lt;code&gt;go.mod&lt;/code&gt; requires, CI &lt;code&gt;include&lt;/code&gt;s), making &quot;what depends on this&quot; a query rather than an excavation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Audience:&lt;/strong&gt; Platform engineers, DevOps/SRE, and engineering leaders weighing repository architecture, especially teams running AI coding agents across multiple repositories or maintaining shared infrastructure components.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Related reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://riftmap.dev/blog/you-dont-need-a-virtual-monorepo/&quot;&gt;You don&apos;t need a virtual monorepo. You need a graph.&lt;/a&gt; — the agent-context version of this argument, and where the workspace pattern&apos;s ceiling sits.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://riftmap.dev/blog/how-to-give-copilot-cross-repo-context/&quot;&gt;How to give GitHub Copilot cross-repo context today&lt;/a&gt; — the three working ways to set it up, and the repository-list problem all three share.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://riftmap.dev/blog/cursor-monorepo-indexing/&quot;&gt;You can index every repo in Cursor. It still can&apos;t tell you what breaks.&lt;/a&gt; — how far a tuned semantic index gets you across a big or multi-repo tree, and the dependency question no tuning answers.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://riftmap.dev/blog/ai-doesnt-understand-blast-radius/&quot;&gt;AI Doesn&apos;t Understand Blast Radius&lt;/a&gt; — why change failure rates are absorbing the cost of the undeclared graph.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://riftmap.dev/blog/the-state-of-infrastructure-dependency-tooling-2026/&quot;&gt;The State of Infrastructure Dependency Tooling in 2026&lt;/a&gt; — what Backstage, Renovate, HCP Terraform Explorer, Nx, and DIY scripts each cover, and the gap.

&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>monorepo</category><category>polyrepo</category><category>multi-repo</category><category>platform-engineering</category><category>dependency-graph</category><category>infrastructure-as-code</category><category>ai-coding-agents</category><author>Daniel Westgaard</author></item><item><title>The CRA&apos;s 24-hour clock is a cross-repo question. Your SBOM answers a different one.</title><link>https://riftmap.dev/blog/cra-sbom-cross-repo-question/</link><guid isPermaLink="true">https://riftmap.dev/blog/cra-sbom-cross-repo-question/</guid><description>The CRA&apos;s 24-hour reporting clock asks which products ship an exploited component. Your SBOM lists what is inside one artifact. Those are different questions, and the gap between them is a cross-repo IaC dependency graph.</description><pubDate>Tue, 02 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;It is a Tuesday in late September 2026. A maintainer publishes a fix for an actively exploited vulnerability in a base image your platform team maintains: &lt;code&gt;company/base-runtime&lt;/code&gt;. Somewhere in a Slack channel a security engineer asks the question the next twenty-four hours turn on. Not &quot;what is inside &lt;code&gt;base-runtime&lt;/code&gt;&quot;. Your SBOM scanner answered that months ago and the component is right there in the inventory. The question is the other one: &quot;which of the products we have placed on the EU market actually ship this image, and at which tag?&quot; That question is not in any SBOM you currently generate. It is a cross-repo question, and the clock is already running.&lt;/em&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;A note on what this post is, and isn&apos;t&lt;/h2&gt;
&lt;p&gt;This is not a compliance guide, and I am not a lawyer. There are good CRA compliance guides written by people who are, and I link to several below. This is an engineering post about a structural mismatch: the shape of the artifact the regulation asks you to keep, versus the shape of the question the regulation&apos;s timelines force you to answer under pressure.&lt;/p&gt;
&lt;p&gt;The argument is narrow. A Software Bill of Materials is a &lt;em&gt;vertical&lt;/em&gt; inventory: the components inside one product, down through its dependency tree. The Cyber Resilience Act&apos;s reporting clock, when it starts ticking, asks a &lt;em&gt;horizontal&lt;/em&gt; question: across every repository in your organisation, which products ship the affected component, and at which version. Those are different graphs. The SBOM is necessary and the regulation is right to mandate it. It is just not sufficient for the question the 24-hour deadline actually asks, and the missing piece is not a better SBOM. It is the cross-repo dependency graph that tells you where each SBOM entry propagates.&lt;/p&gt;
&lt;p&gt;Riftmap does not generate CRA-format SBOMs today. That work is on the roadmap, and I will be explicit about where the line currently sits rather than imply the product does something it does not. What Riftmap builds right now is the horizontal graph, the part that answers &quot;which repos ship this&quot;, and that turns out to be the part the SBOM tooling category structurally does not produce.&lt;/p&gt;
&lt;p&gt;If you are a platform lead or a CISO at a European manufacturer reading about September 2026 reporting deadlines, the practical takeaway is this: budget for SBOM generation, yes, but understand that generating SBOMs and answering &quot;where is this component deployed across our estate&quot; are two projects, not one.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;The two questions the regulation forces together&lt;/h2&gt;
&lt;p&gt;The Cyber Resilience Act entered into force on 10 December 2024. Two dates matter for engineering planning. From &lt;a href=&quot;https://digital-strategy.ec.europa.eu/en/policies/cra-reporting&quot;&gt;11 September 2026&lt;/a&gt;, manufacturers must report actively exploited vulnerabilities and severe incidents through ENISA&apos;s single reporting platform, with an early warning inside 24 hours, a full notification inside 72 hours, and a final report no later than 14 days after a corrective measure is available. From &lt;a href=&quot;https://digital-strategy.ec.europa.eu/en/policies/cra-summary&quot;&gt;11 December 2027&lt;/a&gt;, the full set of obligations applies, including the SBOM requirement that sits in the technical documentation.&lt;/p&gt;
&lt;p&gt;Most of the public attention has gone to the SBOM mandate, and the mandate itself is modest in scope. Annex I, Part II(1) requires manufacturers to draw up a software bill of materials &quot;in a commonly used and machine-readable format covering at least the top-level dependencies of the product.&quot; That is the legal floor: top-level dependencies, machine-readable, retained as documentation a market surveillance authority may request. The SBOM tooling category already clears this floor comfortably. Syft, Trivy, cdxgen, and the rest produce CycloneDX or SPDX output that lists components and their transitive trees inside an artifact. The SBOM is, for most teams, a solved generation problem.&lt;/p&gt;
&lt;p&gt;The reporting clock is the part that is not solved, and it asks a different question.&lt;/p&gt;
&lt;p&gt;When an actively exploited vulnerability lands in a component you ship, the 24-hour early warning does not ask &quot;what is inside product X.&quot; It assumes you already know that. It asks, in effect, &quot;which of your products with digital elements are affected, and what is the scope of the exposure.&quot; For a single product with one SBOM, that is a lookup. For a manufacturer shipping dozens of products that share internal base images, shared Terraform modules, common Helm charts, and reusable CI workflows, it is a fan-out problem across the whole estate. The exploited component is one node. The set of products that ship it is the answer, and that set is spread across every repository that consumes the node directly or transitively.&lt;/p&gt;
&lt;p&gt;That is a cross-repo dependency question. It is the question this blog has been about since the first post. The CRA simply attached a 24-hour deadline and a fine of up to €15 million or 2.5% of global turnover to getting it wrong.&lt;/p&gt;
&lt;h2&gt;Why the SBOM does not answer it&lt;/h2&gt;
&lt;p&gt;Here is the part that trips up the budgeting conversation. A team hears &quot;the CRA wants SBOMs&quot; and &quot;the CRA has a 24-hour reporting clock&quot; and assumes the SBOM is the thing that answers the clock. It is not, and the reason is in the shape of the artifact.&lt;/p&gt;
&lt;p&gt;An SBOM is scoped to a product. It is the inventory of components that go into one shippable thing: one container image, one application build, one firmware blob. CycloneDX and SPDX both model this as a tree: the product at the root, direct dependencies below it, transitive dependencies below those. The tooling that generates it is build-time or filesystem-scoped on purpose. Syft scans an image or a directory. Trivy scans a target. The output is faithful to one artifact and says nothing about the others.&lt;/p&gt;
&lt;p&gt;So when the exploited component is in a shared base image, the SBOM of &lt;em&gt;that base image&lt;/em&gt; tells you what is inside the base image. It does not tell you which application images were built &lt;code&gt;FROM&lt;/code&gt; it, in which repositories, at which tags. Each of those downstream products has its own SBOM, and the component appears in each of those too, but only if those SBOMs were generated, retained, indexed, and queryable as a set, with the base-image relationship preserved as a resolved edge rather than a string. In practice that index does not exist as a by-product of running an SBOM generator. The SBOM generators do not build it, and they are explicit that infrastructure relationships are out of scope.&lt;/p&gt;
&lt;h3&gt;The tooling draws the line itself&lt;/h3&gt;
&lt;p&gt;You do not have to take my word for the scope boundary. The SBOM and scanning tools say it themselves. Grype&apos;s own documentation is blunt about it: it is a vulnerability scanner and nothing more, and &lt;a href=&quot;https://secure-pipelines.com/ci-cd-security/ci-cd-security-scanners-compared-trivy-grype-snyk-checkov/&quot;&gt;if you need Terraform, CloudFormation, or Kubernetes manifest analysis, you need a separate tool&lt;/a&gt;. Trivy does scan IaC, but for &lt;em&gt;misconfigurations&lt;/em&gt;: it tells you a security group is too open, not which repositories consume the module that defines it. Checkov runs on infrastructure code to catch policy violations within a configuration. None of these tools resolves the cross-repo artifact relationship: this base image is consumed by those eight application repos, four of which float to the new tag on next build and four of which are pinned behind.&lt;/p&gt;
&lt;p&gt;The category split is the same one I wrote about in &lt;a href=&quot;https://riftmap.dev/blog/symbol-graphs-and-artifact-graphs/&quot;&gt;Symbol graphs and artifact graphs&lt;/a&gt;. SBOM generators inventory what is inside an artifact. They do not build the graph of which artifacts consume which other artifacts across an organisation, because that graph requires a parser estate that understands &lt;code&gt;FROM company/base:${TAG}&lt;/code&gt;, &lt;code&gt;source = &quot;git::...?ref=v3.2.0&quot;&lt;/code&gt;, &lt;code&gt;uses: company/actions/deploy@v2&lt;/code&gt;, and the registry and git resolution behind each of them. That is artifact-graph work, and it is structurally outside what a component inventory produces.&lt;/p&gt;
&lt;h2&gt;What the 24-hour clock actually asks for&lt;/h2&gt;
&lt;p&gt;Walk through the incident concretely, because the gap is clearest under time pressure.&lt;/p&gt;
&lt;p&gt;A CVE is published for &lt;code&gt;company/base-runtime&lt;/code&gt; and there is evidence of active exploitation in the wild. The CRA clock starts when you become aware. Inside 24 hours you owe ENISA an early warning. To write it, you need to know the scope: which products with digital elements that you have placed on the EU market are affected.&lt;/p&gt;
&lt;p&gt;With the standard SBOM pipeline, you have an inventory per product, somewhere: in a registry, in an artifact store, in Dependency-Track if you run it. To answer the scope question you would need every product&apos;s SBOM, indexed together, with the base-image edge resolved so that &quot;ships &lt;code&gt;base-runtime&lt;/code&gt;&quot; is a query rather than a grep. Most teams do not have this. What they have is the base image&apos;s own SBOM, a CI system that built the downstreams, and a frantic afternoon of &lt;code&gt;grep -r &quot;base-runtime&quot;&lt;/code&gt; across repositories followed by manual reading of each Dockerfile to work out whether the tag in question is actually the one in production after build-arg substitution.&lt;/p&gt;
&lt;p&gt;The grep finds the files. It does not resolve the answer. Which &lt;code&gt;FROM&lt;/code&gt; lines pin the affected tag directly, which use &lt;code&gt;${BASE_TAG}&lt;/code&gt; resolved from a build arg in a separate workflow file, which inherit it transitively through an intermediate internal base image that is itself built &lt;code&gt;FROM company/base-runtime&lt;/code&gt;. None of that comes out of a text search. It comes out of a parser that reads the Dockerfile, finds the default, reads the build invocation to see if it is overridden, and follows the intermediate-image chain. That is exactly the resolution work a cross-repo artifact graph does once, ahead of time, so that during the incident the scope query is a lookup instead of an investigation.&lt;/p&gt;
&lt;h3&gt;The shape of the answer you need&lt;/h3&gt;
&lt;p&gt;The early-warning notification needs the scope of affected products. The 14-day final report needs the remediation status: which products have been patched, which are pending, which are out of scope because the affected code path is not reachable. Both are queries against the same horizontal graph:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Which repositories consume &lt;code&gt;company/base-runtime&lt;/code&gt; directly?&lt;/li&gt;
&lt;li&gt;Which consume it transitively through an internal wrapper image?&lt;/li&gt;
&lt;li&gt;Of those, which pin the affected tag versus float to it on next build?&lt;/li&gt;
&lt;li&gt;For each affected product, what is the remediation state once the fixed tag is published?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A component inventory does not have these answers because it was never scoped to ask them. A cross-repo dependency graph is built to. This is not a knock on SBOMs. It is the observation that the regulation mandates one artifact (the vertical inventory) and its reporting clock demands a different one (the horizontal graph), and teams that conflate the two will discover the gap at the worst possible moment, with a 24-hour deadline running.&lt;/p&gt;
&lt;h2&gt;Where NIS2 and DORA fit, honestly&lt;/h2&gt;
&lt;p&gt;It is tempting to stack all three EU regimes into one regulatory wall and imply they all mandate the same thing. They do not, and a compliance-literate reader will catch the overstatement, so here is the honest version.&lt;/p&gt;
&lt;p&gt;The CRA is the one with an explicit, named SBOM mandate in the legal text. NIS2 raises expectations for software supply chain security. Article 21 requires in-scope entities to manage &lt;a href=&quot;https://anchore.com/sbom/nis2-compliance-and-sboms/&quot;&gt;security-related aspects of the relationships with their direct suppliers&lt;/a&gt;, but it &lt;a href=&quot;https://sbomify.com/compliance/&quot;&gt;does not mandate SBOMs by name&lt;/a&gt;. DORA, which has applied to financial entities since January 2025, emphasises ICT third-party risk management and a Register of Information covering third-party providers, rather than an SBOM requirement as such.&lt;/p&gt;
&lt;p&gt;So the accurate framing is not &quot;three laws all demand IaC SBOMs.&quot; It is that three overlapping EU regimes are pushing the same direction (software supply chain transparency and the ability to answer &quot;what are we exposed to, and where&quot; on a deadline), and the CRA is the one that makes the SBOM explicit and attaches the sharpest clock. The cross-repo graph is useful under all three for the same underlying reason: every one of them, in its own language, eventually asks a manufacturer or an essential entity to know where a given component or supplier sits across its estate. But the CRA&apos;s 24-hour reporting obligation is the concrete, dated forcing function, and it is the one to plan against first.&lt;/p&gt;
&lt;h2&gt;What this means for how you budget the work&lt;/h2&gt;
&lt;p&gt;If you are scoping CRA readiness for an engineering organisation, the practical decomposition is two projects, not one.&lt;/p&gt;
&lt;p&gt;The first project is SBOM generation and retention: wire Syft or cdxgen or your build-tool&apos;s native CycloneDX plugin into CI, produce a machine-readable SBOM per product covering at least top-level dependencies, retain it as technical documentation, and ideally manage it in something like Dependency-Track so vulnerability correlation is continuous rather than incident-time. This is well-trodden ground with mature tools and it clears the Annex I floor.&lt;/p&gt;
&lt;p&gt;The second project is the horizontal graph: the ability to answer, across every repository, which products ship a given component (base image, shared module, chart, workflow) and at which version, with the resolution work done ahead of the incident rather than during it. This is the project most teams have not separated out, because the SBOM conversation absorbs it. It is also the project that determines whether the 24-hour clock is a lookup or a fire drill.&lt;/p&gt;
&lt;p&gt;The two projects share inputs. The same parser estate that resolves &quot;which repos consume &lt;code&gt;base-runtime&lt;/code&gt;&quot; is reading the same Dockerfiles, Terraform sources, and Helm charts that feed component inventories. There is a real convergence here, and over time the artifact graph and the per-product SBOM become two views of one resolved dataset. That convergence is on Riftmap&apos;s roadmap and it is the subject of a future post once the SBOM-export work ships. For now the honest statement is narrower: Riftmap builds the horizontal graph today, and the horizontal graph is the half of CRA-readiness the SBOM tooling category does not cover.&lt;/p&gt;
&lt;h2&gt;What Riftmap returns for the cross-repo half&lt;/h2&gt;
&lt;p&gt;Concretely, the incident question (&quot;which repositories ship &lt;code&gt;company/base-runtime&lt;/code&gt;, and at which tag&quot;) is a single call against the &lt;a href=&quot;https://riftmap.dev/what-is-an-artifact-dependency-graph/&quot;&gt;artifact graph&lt;/a&gt; rather than an afternoon of grep and manual Dockerfile reading.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GET /api/v1/artifacts/{artifact_id}/consumers
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;artifact&quot;: {
    &quot;id&quot;: &quot;a17c4f02-8b9d-4e51-9c2a-1f7e6d3b8a90&quot;,
    &quot;artifact_type&quot;: &quot;docker_image&quot;,
    &quot;name&quot;: &quot;base-runtime&quot;,
    &quot;source_repository_id&quot;: &quot;c2d8e1f4-3a6b-4c9d-8e2f-7b1a9d4c6e30&quot;,
    &quot;registry_url&quot;: &quot;registry.company.com/platform/base-runtime&quot;,
    &quot;version&quot;: &quot;3.4.1&quot;,
    &quot;consumer_count&quot;: 6,
    &quot;is_orphan&quot;: false
  },
  &quot;consumers&quot;: [
    {
      &quot;repository&quot;: {
        &quot;id&quot;: &quot;f1a2b3c4-d5e6-4f70-8a91-0b2c3d4e5f60&quot;,
        &quot;name&quot;: &quot;checkout-api&quot;,
        &quot;full_path&quot;: &quot;polaris-works/payments/checkout-api&quot;
      },
      &quot;version_constraint&quot;: &quot;3.4.1&quot;,
      &quot;source_file&quot;: &quot;Dockerfile&quot;, &quot;source_line&quot;: 1,
      &quot;is_latest&quot;: true,
      &quot;import_count&quot;: 1
    },
    {
      &quot;repository&quot;: {
        &quot;id&quot;: &quot;a9b8c7d6-e5f4-4039-8271-6a5b4c3d2e10&quot;,
        &quot;name&quot;: &quot;ledger-worker&quot;,
        &quot;full_path&quot;: &quot;polaris-works/payments/ledger-worker&quot;
      },
      &quot;version_constraint&quot;: &quot;3.4.1&quot;,
      &quot;source_file&quot;: &quot;Dockerfile&quot;, &quot;source_line&quot;: 2,
      &quot;is_latest&quot;: true,
      &quot;import_count&quot;: 1
    },
    {
      &quot;repository&quot;: {
        &quot;id&quot;: &quot;12340000-5678-4abc-9def-000011112222&quot;,
        &quot;name&quot;: &quot;invoicing-svc&quot;,
        &quot;full_path&quot;: &quot;polaris-works/finance/invoicing-svc&quot;
      },
      &quot;version_constraint&quot;: &quot;3.3.0&quot;,
      &quot;source_file&quot;: &quot;Dockerfile&quot;, &quot;source_line&quot;: 1,
      &quot;is_latest&quot;: false,
      &quot;import_count&quot;: 1
    },
    {
      &quot;repository&quot;: {
        &quot;id&quot;: &quot;33334444-5555-4666-8777-888899990000&quot;,
        &quot;name&quot;: &quot;internal-base-python&quot;,
        &quot;full_path&quot;: &quot;polaris-works/platform/internal-base-python&quot;
      },
      &quot;version_constraint&quot;: &quot;3.4.1&quot;,
      &quot;source_file&quot;: &quot;Dockerfile&quot;, &quot;source_line&quot;: 1,
      &quot;is_latest&quot;: true,
      &quot;import_count&quot;: 1
    }
  ],
  &quot;total_consumers&quot;: 6,
  &quot;consumers_on_latest&quot;: 5,
  &quot;consumers_lagging&quot;: 1,
  &quot;latest_version&quot;: &quot;3.4.1&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The consumer table is the scope of the early-warning notification, already resolved. Each row carries the version constraint the consumer pins, the source file and line where the &lt;code&gt;FROM&lt;/code&gt; lives, and whether that pin is on the latest published version. &lt;code&gt;consumers_lagging: 1&lt;/code&gt; is &lt;code&gt;invoicing-svc&lt;/code&gt;, still on &lt;code&gt;3.3.0&lt;/code&gt;, evaluated against the published version list rather than left for you to work out by hand. That is the count you reason about for the report: which products ship the affected image, and which are behind.&lt;/p&gt;
&lt;p&gt;Two of the resolution problems are worth calling out because they are exactly where a grep over &lt;code&gt;FROM&lt;/code&gt; lines goes wrong, and they are resolved before these rows are produced rather than surfaced as separate fields. The first is build-arg substitution: a &lt;code&gt;FROM company/base-runtime:${BASE_TAG}&lt;/code&gt; line only resolves to a real tag once the build argument is evaluated, so the consumer relationship has to be recorded against the actual base image rather than left as a literal &lt;code&gt;${BASE_TAG}&lt;/code&gt; string a text search would skip over. The second is the intermediate image: &lt;code&gt;internal-base-python&lt;/code&gt; is itself built &lt;code&gt;FROM base-runtime&lt;/code&gt; and is in turn consumed by other application repos, so the products that inherit the affected base through that wrapper are reachable by walking the graph one hop further, with a second call against &lt;code&gt;internal-base-python&lt;/code&gt; as the artifact. Neither of those is a field you parse out of the response. They are resolution work the graph did so the response is already correct.&lt;/p&gt;
&lt;p&gt;This does not generate the CRA SBOM. It answers the question the SBOM does not: where, across the estate, the affected component actually ships. The two halves are complementary, and the second half is the one with no existing category occupying it.&lt;/p&gt;
&lt;h2&gt;The short version&lt;/h2&gt;
&lt;p&gt;The CRA mandates a software bill of materials covering at least the top-level dependencies of each product. That is a vertical inventory, scoped to one artifact, and the SBOM tooling category (Syft, Trivy, cdxgen, Dependency-Track) produces it well.&lt;/p&gt;
&lt;p&gt;The CRA&apos;s reporting clock, which starts on 11 September 2026, asks a different question. When an actively exploited component lands in something you ship, you owe a scope assessment inside 24 hours: which of your products with digital elements are affected, across every repository in your estate. That is a horizontal, cross-repo question. It is not what an SBOM is shaped to answer, and the SBOM tools say as much themselves: infrastructure relationships across repositories are explicitly out of their scope.&lt;/p&gt;
&lt;p&gt;The gap between the two is a cross-repo IaC dependency graph: the resolved set of which products consume which shared components, across Docker base images, Terraform modules, Helm charts, and reusable workflows, with build-arg substitution evaluated and intermediate-image chains followed. Budget CRA readiness as two projects: SBOM generation, which is solved, and the horizontal graph, which is the half that turns the 24-hour clock from a fire drill into a lookup.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://riftmap.dev/&quot;&gt;Riftmap&lt;/a&gt; builds that horizontal graph today. It does not yet emit CRA-format SBOMs. That convergence is coming, and it is a post for the day it ships. For now the claim is the narrow, true one: the part of CRA readiness that the SBOM category structurally does not cover is the part Riftmap exists to build.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;Riftmap scans your GitHub or GitLab organisation with a read-only token, parses Terraform, Docker, Helm, Kustomize, Kubernetes, GitHub Actions, GitLab CI, Ansible, Go modules, and npm, and builds the cross-repo artifact graph as a queryable surface, for engineers in the UI and for agents over MCP. The &quot;which products ship this component&quot; query is one call. Five minutes to first graph. &lt;a href=&quot;https://app.riftmap.dev&quot;&gt;The free tier is here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;For the per-ecosystem parsing detail behind the consumer queries, the &lt;a href=&quot;https://riftmap.dev/blog/series/find-every-consumer/&quot;&gt;Find Every Consumer series&lt;/a&gt; goes one ecosystem at a time, starting with &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-docker-base-image/&quot;&gt;Docker base images&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</content:encoded><category>cra</category><category>sbom</category><category>cross-repo-dependencies</category><category>supply-chain-security</category><category>infrastructure</category><category>blast-radius</category><category>compliance</category><author>Daniel Westgaard</author></item><item><title>Repo access was never the hard part</title><link>https://riftmap.dev/blog/repo-access-was-never-the-hard-part/</link><guid isPermaLink="true">https://riftmap.dev/blog/repo-access-was-never-the-hard-part/</guid><description>The multi-repo agent race is solving repository access. But access is plumbing — the cross-repo dependency graph is the part nobody upstream is parsing.</description><pubDate>Sun, 31 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Spend an hour reading the support forums for any of the big coding agents right now and you will notice the same thread, over and over, in slightly different words. Someone has their codebase split across a handful of repositories. They point an agent at it. The agent can only see one repo. They want it to see the rest.&lt;/p&gt;
&lt;p&gt;This is not a niche complaint. It is one of the loudest things engineering teams are asking for in 2026, and the vendors have heard it. In May, Cursor &lt;a href=&quot;https://cursor.com/blog/cloud-agent-development-environments&quot;&gt;shipped multi-repo environments for its cloud agents&lt;/a&gt;, so that a single environment can hold every repository an agent needs. On the GitHub side, Copilot&apos;s cloud agent still runs inside the single repo where the task was opened, and the community has &lt;a href=&quot;https://devactivity.com/insights/unlocking-cross-repository-access-for-github-copilot-cloud-agents-boosting-software-engineer-performance/&quot;&gt;built its own cross-repo workarounds&lt;/a&gt; with MCP servers and scoped tokens while a native solution is expected. Agent HQ is &lt;a href=&quot;https://www.helpnetsecurity.com/2026/02/05/github-enables-coding-agents/&quot;&gt;pulling Copilot, Claude and Codex into the same GitHub workflows&lt;/a&gt;. Everyone is racing in the same direction.&lt;/p&gt;
&lt;p&gt;I want to be generous about this, because it is genuine demand validation and I have been saying for a while that multi-repo work is where agents earn their keep. But I also want to name what the race is actually about, because I think most of the industry has quietly mislabelled the finish line.&lt;/p&gt;
&lt;h2&gt;The race is about access&lt;/h2&gt;
&lt;p&gt;Read the threads closely and the problem is always the same shape. Can the agent clone the other repo. Does the token have the right scope. Will the environment hold all seven repositories. Can a build secret reach the running agent. This is real, fiddly, important work, and it is also plumbing. It is the difference between an agent that can reach your code and one that cannot.&lt;/p&gt;
&lt;p&gt;Plumbing gets solved. It always does. The gap between a single-repo agent and a multi-repo one is a few releases of integration work, and the largest, best-funded tooling companies in the world are spending those releases right now. If your mental model is &quot;multi-repo is hard because access is hard,&quot; that model has a short shelf life. Within a couple of quarters, access across repositories will be a checkbox.&lt;/p&gt;
&lt;p&gt;So it is worth asking what is left once the checkbox is ticked.&lt;/p&gt;
&lt;h2&gt;The quiet conflation&lt;/h2&gt;
&lt;p&gt;Here is the sentence I want to sit with. When Cursor announced multi-repo environments, the framing was that with several repos in scope, agents can &lt;a href=&quot;https://cursor.com/blog/cloud-agent-development-environments&quot;&gt;&quot;reason about how a change in one part of the codebase affects others.&quot;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;That is a reasonable thing to say, and at the level of code symbols it is often true. But notice the move inside it. It slides from &quot;the agent can see the repos&quot; to &quot;the agent understands how they depend on one another,&quot; and those are not the same claim. The first is about scope. The second is about structure. Scope is necessary for structure. It is nowhere near sufficient for it.&lt;/p&gt;
&lt;p&gt;This is the conflation running underneath the whole multi-repo race. Access is being sold, understandably, as understanding. Putting the repositories in front of the agent is treated as the hard part, and the assumption is that comprehension follows for free once the files are reachable. It does not, and the reason it does not is in how these agents actually work out what depends on what.&lt;/p&gt;
&lt;h2&gt;How an agent actually resolves a cross-repo relationship&lt;/h2&gt;
&lt;p&gt;Once an agent has all your repos in scope, it still has to answer the question that matters: if I change this, what else breaks. There are three families of technique it can use, and it is worth being precise about each, because none of them is built for the thing I care about.&lt;/p&gt;
&lt;p&gt;The first is agentic search, which is where Claude Code and several others have landed. No index, just grep and read, the model navigating the tree the way a person would. There are &lt;a href=&quot;https://yage.ai/share/why-coding-agents-still-use-grep-en-20260327.html&quot;&gt;good arguments for this&lt;/a&gt;: grep returns a broad cluster of matches from which the model can infer the codebase&apos;s organisation and naming conventions. The key word is infer. The structure is reconstructed from text and from names, and names lie. A service called &lt;code&gt;payments-api&lt;/code&gt; and a Terraform module called &lt;code&gt;payments&lt;/code&gt; may be tightly coupled or completely unrelated, and grep cannot tell you which.&lt;/p&gt;
&lt;p&gt;The second is retrieval over embeddings, the classic RAG approach, which is what codebase indexing in editors leans on. This is semantic similarity over chunks of code. It is excellent at &quot;show me code that looks like this&quot; and it is still, fundamentally, operating on the text. Similarity is not dependency. Two files can be deeply coupled and read nothing alike, and two files can read almost identically and never touch each other in production.&lt;/p&gt;
&lt;p&gt;The third, and the most rigorous, is a real symbol graph. Sourcegraph&apos;s SCIP is the best example, and I have a lot of respect for it. It powers &lt;a href=&quot;https://sourcegraph.com/blog/cross-repository-code-navigation&quot;&gt;cross-repository code navigation&lt;/a&gt;: click a function in one repo and jump to its definition in another, compiler-accurate, no pattern matching. But look at what SCIP actually indexes. It was &lt;a href=&quot;https://sourcegraph.com/blog/announcing-scip&quot;&gt;built to index programming languages&lt;/a&gt; to power go-to-definition and find-references, with a separate language-specific indexer for each language, scip-typescript, scip-python, scip-java, scip-clang. The unit is the programming-language symbol. Cross-repo, yes. But cross-repo &lt;em&gt;symbols&lt;/em&gt;.&lt;/p&gt;
&lt;h2&gt;Where the edges actually live&lt;/h2&gt;
&lt;p&gt;Here is the problem with all three. The dependency that wrecks your afternoon on a multi-repo change is usually not a function call.&lt;/p&gt;
&lt;p&gt;It is the Terraform module in one repo that sources a module in another. It is the Helm chart that deploys an image tag built somewhere else entirely. It is the service whose CI pipeline publishes an artifact that three other pipelines consume. It is the base image in a Dockerfile that lives in a platform repo two teams over. These are the edges that turn a one-line change into an incident, and they share two awkward properties.&lt;/p&gt;
&lt;p&gt;They are not programming-language symbols, so the symbol graph, however precise, never sees them. SCIP resolving a TypeScript import across repos is genuinely useful and completely blind to the fact that the same repo&apos;s Helm values point at an image your change just rebuilt.&lt;/p&gt;
&lt;p&gt;And they are not reliably inferable from text or names, so grep and embeddings can only guess at them. The guess is sometimes right, which is the dangerous part, because it is right often enough to be trusted and wrong often enough to hurt.&lt;/p&gt;
&lt;p&gt;These edges are, however, declared. Precisely, deterministically, in the manifests: the Terraform source blocks, the Helm value references, the image tags, the CI artifact names, the package manifests across every ecosystem in the org. The information is not missing. It is sitting in the files, in a form that has to be parsed rather than inferred. I have written before about &lt;a href=&quot;https://riftmap.dev/blog/series/find-every-consumer/&quot;&gt;finding every consumer of a thing across an org&lt;/a&gt; and about why &lt;a href=&quot;https://riftmap.dev/blog/you-dont-need-a-virtual-monorepo/&quot;&gt;you do not need a virtual monorepo&lt;/a&gt; to do it. The short version: parse the manifests, per ecosystem, and the graph is just there.&lt;/p&gt;
&lt;h2&gt;Why this gets worse as agents get better&lt;/h2&gt;
&lt;p&gt;You could argue this gap has always existed and teams have muddled through. True. What changes the stakes is autonomy. Cursor has said that &lt;a href=&quot;https://cursor.com/blog/cloud-agent-development-environments&quot;&gt;roughly a third of its own merged pull requests&lt;/a&gt; are now opened by agents, and that number has been climbing. Multiply that by an agent that can now touch seven repos in a single session.&lt;/p&gt;
&lt;p&gt;An agent operating across a multi-repo system on an inferred map is simply a faster way to ship a cross-repo break. It will make the change confidently, because the relationship it could not see did not show up in its grep results. And then the change lands in front of a human reviewer who is verifying against the exact same blind spot, because the reviewer is also reading the diff and reasoning about blast radius from memory and naming.&lt;/p&gt;
&lt;p&gt;This is the part I keep coming back to. The agent making the change and the engineer verifying it need the same thing, and it is not more access. It is a shared, structural account of how the system is actually wired. They need to be looking at the same map, and right now neither of them has one.&lt;/p&gt;
&lt;h2&gt;The layer nobody upstream is parsing&lt;/h2&gt;
&lt;p&gt;So this is the bet. Access across repositories is being commoditised in front of us, fast, by companies far larger than mine, and that is fine, because access was never the interesting part. The interesting part is the cross-repo dependency structure, and the reason the incumbents are not parsing it is not that they cannot. It is that their retrieval is built on inference and on language symbols, and parsing infrastructure manifests deterministically across ten or more ecosystems sits at an angle to how their systems are designed.&lt;/p&gt;
&lt;p&gt;That is the layer I am building Riftmap to be. It scans a GitLab or GitHub org, parses the manifests across all the ecosystems that actually carry the dependency edges, and produces one queryable graph that answers &quot;if I change this repo, what else breaks.&quot; Not inferred. Parsed. The same map for the agent and for the person reviewing what the agent did. For GitHub Copilot specifically, &lt;a href=&quot;https://riftmap.dev/blog/how-to-give-copilot-cross-repo-context/&quot;&gt;three ways to give it cross-repo context today&lt;/a&gt; is the practical companion to this argument — the workspace, Spaces, and MCP approaches, and where each one&apos;s repository list should come from.&lt;/p&gt;
&lt;p&gt;Give an agent every repo in your org and you have given it access. You have not given it the map. Those are different gifts, and the second one is the one that stops the change from breaking three services you forgot were downstream.&lt;/p&gt;
</content:encoded><category>ai-coding-agents</category><category>cross-repo-context</category><category>multi-repo</category><category>dependency-graph</category><category>cursor</category><category>infrastructure-as-code</category><author>Daniel Westgaard</author></item><item><title>Inferred context is not a dependency graph</title><link>https://riftmap.dev/blog/inferred-context-is-not-a-dependency-graph/</link><guid isPermaLink="true">https://riftmap.dev/blog/inferred-context-is-not-a-dependency-graph/</guid><description>An enterprise context engine makes your agent generate better code. A parsed dependency graph is the thing you gate a deploy on. Two jobs, two kinds of machinery, and why a confidence score is the wrong guarantee for the second one.</description><pubDate>Sat, 30 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A pattern I keep running into, first as a consultant and lately in places like r/CursorAI: a developer is working in one repo, asks an agent to change something that touches a shared library living in another repo, and the agent confidently writes code against an interface that changed six months ago. The methods do not exist any more. The suggestion looks right, reviews fine to a tired eye, and breaks at integration time.&lt;/p&gt;
&lt;p&gt;The fix everyone reaches for now is an enterprise context engine that indexes the whole organisation and feeds the agent more context. Tabnine shipped a strong one this year. I want to be fair about this up front, because the rest of the post draws a line and the line only means something if the thing on the other side of it is good: the problem these engines target is real, grounding an agent in your actual systems clearly beats letting it guess from training data, and Tabnine is a serious product built by serious people.&lt;/p&gt;
&lt;p&gt;But there are two different questions hiding under the phrase &quot;cross-repo context,&quot; and they want different machinery. This is the third in a loose series about that confusion. The first looked at &lt;a href=&quot;https://riftmap.dev/blog/modeled-graphs-and-parsed-graphs/&quot;&gt;why a modelled catalog and a parsed graph are different categories&lt;/a&gt;; the second at &lt;a href=&quot;https://riftmap.dev/blog/symbol-graphs-and-artifact-graphs/&quot;&gt;why a code-symbol graph stops where infrastructure starts&lt;/a&gt;. This one is about the difference between context a model infers and a graph a parser derives, and why that difference is invisible right up until the moment it is the whole cost.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;A note on what this post is, and isn&apos;t&lt;/h2&gt;
&lt;p&gt;This is not a competitive teardown. Tabnine&apos;s &lt;a href=&quot;https://www.tabnine.com/enterprise-context-engine/&quot;&gt;Enterprise Context Engine&lt;/a&gt; went generally available in February 2026, works alongside Cursor, GitHub Copilot and Claude Code rather than replacing them, and serves whichever agent a team already uses. It connects to repositories, CI, code review, docs and ticketing, builds a continuously updated model of the organisation, and hands the relevant slice to an agent at generation time. Tabnine was named a Visionary in the 2026 Gartner Magic Quadrant for Enterprise AI Coding Agents, for the second year running. The customer logos are the kind you would expect: large, regulated, careful. None of that is marketing fog. It is a real answer to a real problem, and if you are running agents across a large codebase it is worth your attention.&lt;/p&gt;
&lt;p&gt;The argument here is narrower. &quot;Context that makes my agent generate better code&quot; and &quot;the graph I check before I ship a breaking change&quot; are different jobs, and the second one has a tolerance for error that inference cannot meet by construction. That is not a flaw in any context engine. It follows directly from the design choice that makes inference powerful in the first place.&lt;/p&gt;
&lt;p&gt;If you are a platform engineer evaluating the wave of context infrastructure arriving this year, the practical takeaway is the same one I keep landing on: most teams need both kinds of thing, and the tools should look as different as the questions do.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Two questions hiding under &quot;cross-repo context&quot;&lt;/h2&gt;
&lt;p&gt;Sit with the problem for a minute and it splits cleanly.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The first question is generative.&lt;/strong&gt; When the agent writes a call into the shared library, what does that library actually contain right now? What are its conventions, its recent changes, the half-finished migration nobody wrote down? This is a context problem. The better your index of code, conventions and history, the fewer wrong calls the agent makes. An inferred context layer is the right tool here, and a good one is genuinely hard to build. It reads everything, models the relationships semantically, and hands the agent the slice that matters. When it is slightly wrong, the cost is a suggestion you correct, which you were going to review anyway. The error is cheap because it lands inside a loop that already has a human in it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The second question is structural.&lt;/strong&gt; Before I change this shared thing, what across the organisation actually breaks? This is not a context problem. It is a graph problem. And the tolerance for being slightly wrong is on a completely different scale. If the answer to &quot;what depends on this Terraform module&quot; misses three repos, you do not find out at review time. You find out in production, three hours later, with Slack on fire. The error is expensive because it lands outside the loop, after the human has already signed off.&lt;/p&gt;
&lt;p&gt;Same surface verb, &quot;what depends on this,&quot; and almost nothing else in common. The first wants the model&apos;s best, fastest, most fluent guess. The second wants an answer you would be willing to gate a deploy on. That last phrase is the bar I hold everything in this post to, and it is a deliberately higher bar than &quot;context that makes my agent a bit better.&quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Three ways to answer &quot;what depends on this&quot;&lt;/h2&gt;
&lt;p&gt;Here is the part that has shifted since I first started writing about this. A year ago the honest framing was a binary: inferred context versus a parsed graph. That binary is no longer quite right, because &quot;deterministic&quot; has become contested vocabulary. Several good products now claim some version of structure or determinism, and they mean genuinely different things by it. There are three architectures in the market, not two, and it is worth being precise about all three.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Inferred.&lt;/strong&gt; A model reads your artifacts and decides what relates to what. This is the RAG-and-semantics camp: Tabnine&apos;s Enterprise Context Engine is the strongest current example, and its own materials are admirably direct about the mechanism, describing the engine as combining semantic retrieval with structural reasoning and enriching context with &quot;inferred relationships.&quot; The edges are produced by a model&apos;s judgement. That is exactly the right tool for the generative question, where a fluent, broad, slightly-fuzzy picture beats a narrow exact one.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Registered.&lt;/strong&gt; A human writes each entity into a typed catalog and the tooling queries it. This is the developer-portal camp: Backstage, Port, Roadie. Roadie &lt;a href=&quot;https://roadie.io/blog/context-engineering-for-developers-ai-infrastructure/&quot;&gt;made the case&lt;/a&gt; earlier this year, and made it well, that a typed entity graph with declared schemas gives an agent deterministic answers rather than the fuzzy output of semantic search over docs. They are right that it is deterministic to query. The catch is in the word &lt;em&gt;registered&lt;/em&gt;: a catalog entry records what an engineer knew at the moment they wrote it, so the graph is only as current as the last person who updated the YAML, and the &lt;a href=&quot;https://riftmap.dev/blog/the-catalog-maintenance-trap/&quot;&gt;reason platform teams quietly abandon these catalogs&lt;/a&gt; is that the maintenance burden outpaces the value within a couple of quarters. Deterministic to read, stale by construction.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Parsed.&lt;/strong&gt; Edges extracted from the source files that already define them. A Terraform &lt;code&gt;module&lt;/code&gt; block referencing a git URL. A Dockerfile &lt;code&gt;FROM&lt;/code&gt; pulling an internal image. A &lt;code&gt;dependencies&lt;/code&gt; entry in a &lt;code&gt;Chart.yaml&lt;/code&gt;. A reusable workflow referenced by &lt;code&gt;uses:&lt;/code&gt;. These are not inferred and they are not separately registered. They are already written down, in manifests, in formats that parse deterministically, and the relationship either exists in the source or it does not. This is the camp Riftmap sits in, and the property that matters is that it is deterministic &lt;em&gt;and&lt;/em&gt; self-updating: there is nothing for a human to maintain and nothing for a model to guess, because the declaration is the dependency.&lt;/p&gt;
&lt;p&gt;The three are not ranked. Inference wins the generative question. The registered catalog carries metadata a parser will never see, like ownership and on-call. But for the specific question &quot;what breaks if I change this,&quot; only one of the three gives you an answer with no maintenance debt and no probability attached. That is the one you can gate a deploy on.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Why inference is the wrong guarantee for the structural question&lt;/h2&gt;
&lt;p&gt;An inferred graph is built by a model reading artifacts and deciding what relates to what. That is powerful for the messy, semantic, undocumented stuff, and I am not dismissing it: a &lt;code&gt;docker pull&lt;/code&gt; buried in a shell script, a convention that took three RFCs to settle, an architectural decision that lives only in someone&apos;s head. Recovering that is real value and it is hard to do well.&lt;/p&gt;
&lt;p&gt;But for &quot;what consumes this,&quot; inference has a property you cannot design away: you do not know what it missed. A confidence score tells you how sure the model is, not whether the edge exists. For impact analysis that is precisely the wrong guarantee, because the entire point of blast radius is that the expensive failures are the edges you did not know about. A tool that is usually right about which repos depend on your module is no help in the one case where being wrong is the whole cost. &quot;Ninety-two per cent confident these are your consumers&quot; is unshippable: you cannot merge a breaking change to a shared module against a probability distribution over its consumer set. You need the actual set, derived from actual source, with an audit trail you can hand to the consumer teams before you ship. A smaller graph you can audit beats a broader graph you have to trust.&lt;/p&gt;
&lt;p&gt;This is not a lonely opinion, which is the other thing that has changed in the last year. The distinction between deterministic and inferential machinery is becoming load-bearing across the field. Martin Fowler&apos;s site published a piece on &lt;a href=&quot;https://martinfowler.com/articles/harness-engineering.html&quot;&gt;harness engineering&lt;/a&gt; that splits an agent&apos;s supporting tools into computational ones, which are deterministic, cheap and safe to run on every change, and inferential ones, which are semantic, expensive and non-deterministic, useful precisely where you can tolerate the fuzz. Independent academic work points the same way: a January 2026 paper from Tel Aviv University introduces the &lt;a href=&quot;https://arxiv.org/abs/2601.10112&quot;&gt;Repository Intelligence Graph&lt;/a&gt;, a deterministic, evidence-backed map extracted from build and test artifacts that agents treat as the authoritative description of repository structure. Giving three commercial agents that deterministic graph improved mean accuracy by 12.2% and cut completion time by 53.9%, with the largest gains in exactly the multilingual, cross-toolchain repositories where inference struggles most. Even Roadie, from the registered camp, is arguing that structure beats semantic retrieval for the questions an agent needs to act on.&lt;/p&gt;
&lt;p&gt;The field is converging on the idea that some questions want a deterministic answer and some want an inferred one, and that conflating them is the mistake. The structural question is firmly in the first bucket.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;The coverage gap inference does not close&lt;/h2&gt;
&lt;p&gt;There is a second problem, separate from the confidence-score one, and it is the one I care most about because it is where the actual work lives.&lt;/p&gt;
&lt;p&gt;The dependency edges that bite hardest in a DevOps organisation are not function calls. They are a Terraform &lt;code&gt;module&lt;/code&gt; block referencing a git URL with a &lt;code&gt;?ref=&lt;/code&gt; pin. A Dockerfile &lt;code&gt;FROM&lt;/code&gt; pulling an internal base image whose tag is set by a build-arg in a separate CI file. A Helm chart depending on another chart through OCI, or HTTPS, or a Flux source pointer. A CI template included across thirty repos. A reusable GitHub Actions workflow referenced by &lt;code&gt;uses: org/repo/.github/workflows/deploy.yml@v2&lt;/code&gt;. These are declared, in manifests, in formats that parse deterministically. You do not need a model to read a &lt;code&gt;dependencies&lt;/code&gt; block in a &lt;code&gt;Chart.yaml&lt;/code&gt;. You need a parser, and then you need a resolver that canonicalises the git URL across its three URL forms, evaluates the semver constraint against the published versions, follows the umbrella chart that re-exports your chart, and chases the build-arg into the CI file that sets it.&lt;/p&gt;
&lt;p&gt;Most code-intelligence and context engines stop at the language boundary and never touch this layer. That is not an oversight, it is the same structural fact I &lt;a href=&quot;https://riftmap.dev/blog/symbol-graphs-and-artifact-graphs/&quot;&gt;worked through at length for Sourcegraph&lt;/a&gt;: an IaC dependency is not a symbol. It is a value inside a string that an infrastructure tool evaluates at plan or build time. A semantic model can guess at these relationships, and it will get many of them, but &quot;many&quot; is the failure mode, not the success. The whole point of parsing this layer is that the answer is complete and verifiable, with each edge clicking through to the exact line that created it.&lt;/p&gt;
&lt;p&gt;This is also why the IaC layer is the part inference is least equipped to fake. A model trained on a lot of code can reason fluently about a Go interface. The relationship between a Terragrunt root and the module it pins at &lt;code&gt;~&amp;gt; 3.2&lt;/code&gt;, mediated by an intermediate wrapper module that floats on &lt;code&gt;main&lt;/code&gt;, is not the kind of thing you reason fluently about. It is the kind of thing you parse, resolve, and check.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;What about the tools that parse deterministically too?&lt;/h2&gt;
&lt;p&gt;The honest objection here is not &quot;isn&apos;t this just Tabnine.&quot; Tabnine infers; the line against it is clean. The sharper objection comes from the small but growing set of tools that &lt;em&gt;do&lt;/em&gt; parse deterministically: local context engines like &lt;a href=&quot;https://vexp.dev/&quot;&gt;vexp&lt;/a&gt; and open-source analysers like &lt;code&gt;codeindex&lt;/code&gt; build dependency graphs straight from an AST with tree-sitter, with nodes for functions, classes and types and edges for calls and imports, no model in the path. The Repository Intelligence Graph paper above is in the same spirit, extracted from build systems. These are deterministic, parsed and auditable, and they are right to be.&lt;/p&gt;
&lt;p&gt;They also stop at the same place Sourcegraph does. The graph is a &lt;em&gt;code-symbol&lt;/em&gt; graph: functions, types, imports, usually within a single project on a single machine. That is genuinely useful, and for &quot;who calls this function&quot; it is the correct architecture. But it is not the cross-repo infrastructure-artifact layer. None of these tools resolves a Terraform module source across an organisation, evaluates Helm version constraints across three reference formats, or follows a Docker base image through a build-arg into the CI file that sets it. The parsed-graph idea is spreading, which I take as validation, and it is spreading at the language-symbol level while the cross-repo IaC artifact layer stays unserved. That gap is the entire reason Riftmap exists.&lt;/p&gt;
&lt;p&gt;So the map has three axes, not one. Inferred context engines (Tabnine) for the generative question. Registered catalogs (Backstage, Port, Roadie) for org metadata. Parsed graphs for the structural question, splitting again into code-symbol parsers (vexp, codeindex, Sourcegraph&apos;s SCIP) and the artifact parser estate that handles cross-repo IaC. Riftmap is the last of those, and as far as I can find it is currently the only one.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;What this means in practice&lt;/h2&gt;
&lt;p&gt;I think these tools compose rather than compete, and I mean that more precisely than the usual &quot;we play nicely together.&quot;&lt;/p&gt;
&lt;p&gt;If you are running an agent across a large codebase, a context engine that grounds its generation is worth having. Feed it the cross-repo picture and your agent hallucinates less and writes against the interface that exists today rather than the one from six months ago. That is a real win and I would not argue against it for a second.&lt;/p&gt;
&lt;p&gt;But when the actual decision in front of you is &quot;I am about to change this thing, what is the blast radius,&quot; you want a graph that was parsed, not inferred. Edges you can click through to the exact line that created them. A version constraint already evaluated against the published versions, so you know which consumers float onto your new release and which are pinned and safe for now. No confidence score, because there is nothing to be unsure about: the module either references that source or it does not.&lt;/p&gt;
&lt;p&gt;Concretely, that is the difference between an agent reasoning over a semantic graph and an agent making a call like this before it plans a change:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GET /api/v1/repositories/{id}/impact

{
  &quot;affected&quot;: [
    { &quot;repo&quot;: &quot;acme/platform-prod&quot;, &quot;depth&quot;: 1, &quot;version_constraint&quot;: &quot;~&amp;gt; 3.2&quot; },
    { &quot;repo&quot;: &quot;acme/monitoring&quot;,    &quot;depth&quot;: 2, &quot;version_constraint&quot;: &quot;&amp;gt;= 3.0&quot; }
  ],
  &quot;total_affected&quot;: 7,
  &quot;last_scanned_at&quot;: &quot;2026-05-30T08:14:00Z&quot;,
  &quot;last_commit_sha&quot;: &quot;a1b3f9c&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That response carries a freshness contract, not a confidence score. If the repo has been pushed to since the graph last looked, the agent knows the data may be stale and can trigger a rescan before it trusts the answer. Staleness is detectable and fixable. A missed edge in an inferred graph is neither.&lt;/p&gt;
&lt;p&gt;This is the multi-context future I &lt;a href=&quot;https://riftmap.dev/blog/symbol-graphs-and-artifact-graphs/&quot;&gt;sketched in the Sourcegraph piece&lt;/a&gt;: a serious agent setup in 2027 composes several specialised context layers, each with its own grammar and freshness model and MCP server. Symbol context for code, artifact context for infrastructure, ticket context, docs context, runtime context. An inferred context engine is one of those layers and a good one. The parsed artifact graph is a different layer answering a different question. The composition is the point, not the competition.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;The short version&lt;/h2&gt;
&lt;p&gt;There are two questions under &quot;cross-repo context.&quot; The generative one, &quot;what does this library contain,&quot; wants the model&apos;s best guess, and an inferred context engine is the right tool for it. Tabnine&apos;s is strong and the problem it solves is real.&lt;/p&gt;
&lt;p&gt;The structural one, &quot;what breaks if I change this,&quot; wants a different guarantee. A confidence score answers the wrong question, because blast radius is precisely about the edges you did not know existed, and the IaC edges that hurt most are declared in manifests a parser can read completely rather than guessed at semantically. For that question you want a graph that was parsed, not inferred: deterministic, self-updating, auditable to the exact line, carrying a freshness contract instead of a probability.&lt;/p&gt;
&lt;p&gt;The market has three architectures for this now, not two. Inferred context for generation, registered catalogs for metadata, parsed graphs for impact. The parsed camp is filling in at the code-symbol level and still empty at the cross-repo infrastructure level. That is the line I have drawn with &lt;a href=&quot;https://riftmap.dev/&quot;&gt;Riftmap&lt;/a&gt;: deterministic parsing first, across the IaC and DevOps ecosystems where the edges are declared and verifiable, with no model guessing anywhere in the path that answers &quot;what breaks.&quot;&lt;/p&gt;
&lt;p&gt;Inferred context is useful. It is just answering a different question than the one you ask right before you ship a breaking change. For that question you do not want the model&apos;s best guess. You want the graph.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This is the kind of question Riftmap is built to answer. It scans your GitHub or GitLab organisation with a read-only token, parses Terraform, Docker, Helm, Kustomize, Kubernetes, GitHub Actions, GitLab CI, Ansible, Go modules and npm, and builds the cross-repo artifact graph as a queryable surface, for engineers in the UI and for agents over the API. Around ninety seconds to first graph. If you have read this far, &lt;a href=&quot;https://app.riftmap.dev&quot;&gt;the free tier is here&lt;/a&gt;, and the &lt;a href=&quot;https://riftmap.dev/for-agents/&quot;&gt;agent integration guide&lt;/a&gt; shows how to wire the graph in as a tool call.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;If you want the underlying parsing work one ecosystem at a time, the &lt;a href=&quot;https://riftmap.dev/blog/series/find-every-consumer/&quot;&gt;Find Every Consumer series&lt;/a&gt; goes through Docker base images, Terraform modules, GitHub Actions workflows, Helm charts and Go modules in turn.&lt;/em&gt;&lt;/p&gt;
</content:encoded><category>ai-coding-agents</category><category>cross-repo-context</category><category>context-engine</category><category>dependency-graph</category><category>tabnine</category><category>blast-radius</category><category>infrastructure-as-code</category><author>Daniel Westgaard</author></item><item><title>Modeled graphs and parsed graphs: two architectures for cross-repo context in the agentic era</title><link>https://riftmap.dev/blog/modeled-graphs-and-parsed-graphs/</link><guid isPermaLink="true">https://riftmap.dev/blog/modeled-graphs-and-parsed-graphs/</guid><description>Why Port&apos;s Blueprints can model the catalog you describe but not the dependency graph your repos already declare — and why modeled graphs and parsed graphs are different categories.</description><pubDate>Tue, 26 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;A platform engineer asks two questions on the same morning. First: &quot;Who owns the &lt;code&gt;payments-api&lt;/code&gt; service, what&apos;s its on-call rotation, and is it production-ready?&quot; Second: &quot;I&apos;m bumping the shared &lt;code&gt;terraform-modules/networking&lt;/code&gt; module to v3.3.0 — which application repos consume it, at what version pin?&quot; The first is a modeled-catalog question and Port answers it at the top of the category. The second looks similar from a distance, has the same surface frame — &quot;what&apos;s the cross-repo state of this thing&quot; — and is, mechanically, a completely different graph. This post is about why.&lt;/em&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;A note on what this post is, and isn&apos;t&lt;/h2&gt;
&lt;p&gt;This is not a competitive teardown. &lt;a href=&quot;https://www.port.io/blog/port-100m-series-c&quot;&gt;Port raised $100M in December 2025 at an $800M valuation&lt;/a&gt;, led by General Atlantic with participation from Accel, Bessemer, and Team8. Their customer list runs through GitHub, British Telecom, Visa, Sonar, StubHub, and Nando&apos;s, and they &lt;a href=&quot;https://techcrunch.com/2025/12/11/port-raises-100m-at-800m-valuation-to-take-on-spotifys-backstage/&quot;&gt;reported 300% revenue growth in the prior year&lt;/a&gt;. Their repositioning as the Agentic Engineering Platform is one of the cleaner category pivots in platform-engineering tooling, and Zohar Einy&apos;s May 2026 post &lt;a href=&quot;https://www.port.io/blog/hidden-technical-debt-of-agentic-engineering&quot;&gt;&lt;em&gt;The hidden technical debt of agentic engineering&lt;/em&gt;&lt;/a&gt; is the most useful taxonomy of agent infrastructure debt anyone has written this year.&lt;/p&gt;
&lt;p&gt;The argument here is narrower: modeled catalogs and source-derived dependency graphs are orthogonal categories. This isn&apos;t a flaw in Port. It follows from the design choices that make Blueprints work in the first place. The same way Sourcegraph&apos;s symbol graph is structurally outside what an IaC dependency graph needs — which I covered in the &lt;a href=&quot;https://riftmap.dev/blog/symbol-graphs-and-artifact-graphs/&quot;&gt;companion post on symbol graphs and artifact graphs&lt;/a&gt; — Port&apos;s modeled catalog is structurally outside what a source-derived dependency graph produces.&lt;/p&gt;
&lt;p&gt;If you&apos;re shopping for an IDP and want a feature comparison, this isn&apos;t that — Port&apos;s &lt;a href=&quot;https://www.port.io/compare/backstage-vs-port&quot;&gt;own comparison pages&lt;/a&gt; cover the modeled-catalog category they compete in well, and this post is about an adjacent category that doesn&apos;t appear there. The practical takeaway for platform teams evaluating &quot;context infrastructure for the agentic era&quot; is that the modeling layer and the parsed-dependency layer are peers, not alternatives, and the tools should look as different as the questions do.&lt;/p&gt;
&lt;h2&gt;Two questions that look similar from a distance&lt;/h2&gt;
&lt;p&gt;Sit down with a platform team in 2026 and they keep coming back to two shapes of question. They sound similar enough that vendor categories blur them.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Question one:&lt;/strong&gt; &lt;em&gt;Who owns the &lt;code&gt;payments-api&lt;/code&gt; service? What&apos;s the team&apos;s on-call rotation? Is the service tagged as production-ready by our scorecards? What&apos;s its tier-of-criticality? Which Jira project tracks its work?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;This is a &lt;strong&gt;modeled-catalog question&lt;/strong&gt;. The answers don&apos;t live in source files. They live in policies, processes, and human-registered ownership boundaries. The right shape of tool is a catalog: someone registers that &lt;code&gt;payments-api&lt;/code&gt; is owned by &lt;code&gt;team-checkout&lt;/code&gt;, on-call goes through PagerDuty rotation &lt;code&gt;checkout-primary&lt;/code&gt;, tier is &lt;code&gt;T1&lt;/code&gt;, runbook lives at this Confluence URL. The catalog aggregates that information across hundreds of services and exposes a unified surface. Port is the standard-bearer for this category in 2026 and has earned that position.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Question two:&lt;/strong&gt; &lt;em&gt;I&apos;m bumping &lt;code&gt;terraform-modules/networking&lt;/code&gt; from v3.2.0 to v3.3.0. Which application repos consume it? Of those, which are pinned to exact v3.2.0, which to &lt;code&gt;~&amp;gt; 3.2&lt;/code&gt;, which to &lt;code&gt;&amp;gt;= 3.0&lt;/code&gt;? Which umbrella modules re-export &lt;code&gt;networking&lt;/code&gt; and pull it in transitively? What about the consumers that reference it as &lt;code&gt;git::https://gitlab.company.com/infra/terraform-modules.git//networking?ref=v3.2.0&lt;/code&gt; with a git ref pin?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;This is a &lt;strong&gt;parsed-dependency question&lt;/strong&gt;. The answers already exist, written down in machine-readable form, in the customer&apos;s source files: &lt;code&gt;source = &quot;...&quot;&lt;/code&gt; blocks in HCL, &lt;code&gt;dependencies:&lt;/code&gt; lists in &lt;code&gt;Chart.yaml&lt;/code&gt;, &lt;code&gt;FROM&lt;/code&gt; lines in Dockerfiles, &lt;code&gt;uses:&lt;/code&gt; strings in GitHub Actions workflows, &lt;code&gt;include:&lt;/code&gt; directives in GitLab CI YAML, &lt;code&gt;require&lt;/code&gt; statements in &lt;code&gt;go.mod&lt;/code&gt;. Nobody registers those relationships into a catalog. The engineer declares them once, in source, when they write the IaC that consumes the artifact. To answer the question, something has to parse that source.&lt;/p&gt;
&lt;p&gt;Both questions are reasonable. The first has a clean tool category — Port and the IDP space have made this their territory. The second has a different shape and needs different machinery.&lt;/p&gt;
&lt;h2&gt;Port&apos;s Context Lake, read carefully&lt;/h2&gt;
&lt;p&gt;Port&apos;s clearest architectural statement in 2026 lives on their &lt;a href=&quot;https://www.port.io/platform/context-lake&quot;&gt;Context Lake product page&lt;/a&gt;. The four feature claims, verbatim:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Feed agents with complete SDLC data — Create a unified knowledge graph. &lt;strong&gt;Bring your own data model so agents understand your world.&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Build a library of actions for agents — Give agents what they need to operate autonomously.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Set access rules for agents — Protect your organization from destructive AI actions by enforcing guardrails.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Exposed via MCP — The context lake is accessible to every agent in your ecosystem.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The phrase to sit with is the one I emphasized: &lt;em&gt;&lt;strong&gt;bring your own data model so agents understand your world&lt;/strong&gt;&lt;/em&gt;. That sentence is the architecture compressed into ten words. The Context Lake holds exactly what the customer&apos;s data model models. The customer&apos;s data model is defined in Blueprints. Blueprints are JSON Schemas the platform team writes.&lt;/p&gt;
&lt;p&gt;Team8, one of Port&apos;s lead investors, summarized the same architecture in their &lt;a href=&quot;https://team8.vc/investing-in-ports-100m-c-round-doubling-down-on-agentic-engineering/&quot;&gt;December 2025 investment post&lt;/a&gt;: &lt;em&gt;&quot;Blueprints power a living graph of context, while agentic workflows run on top.&quot;&lt;/em&gt; The word &quot;living&quot; matters: continuously ingested, evolving, kept current by integrations. It does not mean source-derived. The Blueprints are alive in the sense that the data flowing into them is fresh. The Blueprints themselves remain the contract the platform team defined.&lt;/p&gt;
&lt;p&gt;Stefan Daugaard Poulsen at Eficode — now &lt;a href=&quot;https://www.eficode.com/blog/choosing-an-internal-developer-portal-is-hard-but-port-made-it-easy&quot;&gt;an official Port partner&lt;/a&gt; and one of the most experienced IDP consultants in the Nordic market — describes the same architecture from the inside: &lt;em&gt;&quot;When you start out with Port you put your attention into defining blueprints, which are the basic building blocks of your portal. Each blueprint consists of properties which you can populate from your different integrations.&quot;&lt;/em&gt; That&apos;s not criticism — that&apos;s how Port is designed to work. The platform team designs the schema; the integrations populate it. Stefan&apos;s broader post is unambiguously positive — titled &lt;em&gt;&quot;Choosing an internal developer portal is hard, but Port made it easy&quot;&lt;/em&gt; — and the quoted line is describing the intended workflow approvingly, not flagging modeling overhead as a problem.&lt;/p&gt;
&lt;h2&gt;What a Blueprint looks like&lt;/h2&gt;
&lt;p&gt;Here&apos;s &lt;a href=&quot;https://docs.port.io/build-your-software-catalog/customize-integrations/configure-data-model/relate-blueprints/&quot;&gt;Port&apos;s own canonical example&lt;/a&gt; of how a Blueprint declares its relations to other Blueprints:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;identifier&quot;: &quot;microservice&quot;,
  &quot;title&quot;: &quot;Microservice&quot;,
  &quot;schema&quot;: {
    &quot;properties&quot;: {
      &quot;language&quot;: { &quot;type&quot;: &quot;string&quot; },
      &quot;on-call&quot;: { &quot;type&quot;: &quot;string&quot;, &quot;format&quot;: &quot;email&quot; },
      &quot;slack-notifications&quot;: { &quot;type&quot;: &quot;string&quot;, &quot;format&quot;: &quot;url&quot; }
    }
  },
  &quot;relations&quot;: {
    &quot;owningTeam&quot;: {
      &quot;title&quot;: &quot;Owning Team&quot;,
      &quot;target&quot;: &quot;team&quot;,
      &quot;required&quot;: true,
      &quot;many&quot;: false
    },
    &quot;deployedTo&quot;: {
      &quot;title&quot;: &quot;Deployed To&quot;,
      &quot;target&quot;: &quot;environment&quot;,
      &quot;required&quot;: false,
      &quot;many&quot;: true
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Every property the catalog will track is declared in the schema. Every relation the graph will represent is also declared in the schema: the &lt;code&gt;target&lt;/code&gt; Blueprint, whether the relation is &lt;code&gt;required&lt;/code&gt;, whether it&apos;s &lt;code&gt;many&lt;/code&gt;. The graph topology is whatever the schema says.&lt;/p&gt;
&lt;p&gt;For modeled-catalog questions — ownership, scorecards, on-call, business criticality, deployment tier, compliance class — this is exactly the right architecture. Those properties are not declared in &lt;code&gt;main.tf&lt;/code&gt;. They are registered by humans on purpose because humans are the source of truth for them.&lt;/p&gt;
&lt;h2&gt;What Riftmap returns for the parallel question&lt;/h2&gt;
&lt;p&gt;Now consider the parallel artifact for the parsed-dependency question. You&apos;re the owner of a shared Helm chart at v1.2.0 and you want the list of consumer repos, their version constraints, and which of them will float to the next release.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GET /api/v1/artifacts/{helm_chart_id}/consumers
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;artifact&quot;: {
    &quot;id&quot;: &quot;b3790d64-c693-47d5-83b0-3a2c3872faf9&quot;,
    &quot;artifact_type&quot;: &quot;helm_chart&quot;,
    &quot;name&quot;: &quot;helm-library-chart&quot;,
    &quot;source_repository&quot;: {
      &quot;full_path&quot;: &quot;polaris-works/platform/helm-library-chart&quot;
    },
    &quot;version&quot;: &quot;1.2.0&quot;,
    &quot;consumer_count&quot;: 5,
    &quot;is_orphan&quot;: false
  },
  &quot;consumers&quot;: [
    {
      &quot;repository&quot;: { &quot;full_path&quot;: &quot;polaris-works/data/data-helm&quot; },
      &quot;version_constraint&quot;: &quot;1.1.0&quot;,
      &quot;source_file&quot;: &quot;Chart.yaml&quot;, &quot;source_line&quot;: 8,
      &quot;is_latest&quot;: false
    },
    {
      &quot;repository&quot;: { &quot;full_path&quot;: &quot;polaris-works/portal/portal-helm&quot; },
      &quot;version_constraint&quot;: &quot;&amp;gt;=1.0.0 &amp;lt;2.0.0&quot;,
      &quot;source_file&quot;: &quot;Chart.yaml&quot;, &quot;source_line&quot;: 8,
      &quot;is_latest&quot;: true
    },
    {
      &quot;repository&quot;: { &quot;full_path&quot;: &quot;polaris-works/payments/payments-helm&quot; },
      &quot;version_constraint&quot;: &quot;~1.2.0&quot;,
      &quot;source_file&quot;: &quot;Chart.yaml&quot;, &quot;source_line&quot;: 8,
      &quot;is_latest&quot;: true
    }
  ],
  &quot;consumers_on_latest&quot;: 4,
  &quot;consumers_lagging&quot;: 1,
  &quot;latest_version&quot;: &quot;1.2.0&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Every relationship in this response was derived by reading the consumer repos&apos; &lt;code&gt;Chart.yaml&lt;/code&gt; files — no Blueprint, no Relation declared in advance, no integration custom-built for &lt;code&gt;helm_chart&lt;/code&gt; semantics. The parser estate already knows what a Helm chart is and what its consumer relationships look like, because the IaC tools have their own grammars and the resolver implements those grammars. The constraint shapes the parser normalises (exact-current &lt;code&gt;1.2.0&lt;/code&gt;, exact-behind &lt;code&gt;1.1.0&lt;/code&gt;, tilde range &lt;code&gt;~1.2.0&lt;/code&gt;, explicit range &lt;code&gt;&amp;gt;=1.0.0 &amp;lt;2.0.0&lt;/code&gt;) are evaluated against the chart&apos;s published version, server-side, before the agent or engineer sees the response.&lt;/p&gt;
&lt;p&gt;The contrast is the structural argument. Port&apos;s API requires a data model because &lt;em&gt;you&lt;/em&gt; are the source of truth for which Blueprints matter at your organization, and that&apos;s correct for the questions Port is built to answer. Riftmap&apos;s API does not require a data model because the source code &lt;em&gt;is&lt;/em&gt; the source of truth for the dependency graph, and that&apos;s correct for the questions Riftmap is built to answer.&lt;/p&gt;
&lt;h2&gt;What Port&apos;s &quot;Terraform integration&quot; actually does&lt;/h2&gt;
&lt;p&gt;It&apos;s worth being precise here, because Port and Terraform appear in the same sentence a lot. Port has two distinct things they call &quot;Terraform&quot;:&lt;/p&gt;
&lt;p&gt;The first is the &lt;a href=&quot;https://docs.port.io/build-your-software-catalog/manage-port-across-environments/&quot;&gt;&lt;code&gt;port_blueprint&lt;/code&gt; Terraform provider&lt;/a&gt;. This lets you define Port&apos;s Blueprints in HCL instead of clicking through the Builder UI. It&apos;s catalog-as-code: your catalog &lt;em&gt;schema&lt;/em&gt; is versioned in a Terraform state file. The provider manages Port&apos;s catalog. It does not parse your &lt;code&gt;.tf&lt;/code&gt; source files for module dependencies.&lt;/p&gt;
&lt;p&gt;The second is the &lt;a href=&quot;https://docs.port.io/build-your-software-catalog/sync-data-to-catalog/terraform-cloud/&quot;&gt;Terraform Cloud integration&lt;/a&gt;. This connects Port to HCP Terraform&apos;s REST API and ingests Organizations, Projects, Workspaces, Runs, State Versions, State Files, and Health Assessments into Port Entities. It&apos;s an excellent integration for what it does — you can see which workspaces ran which Terraform plans and surface failed runs in a dashboard. It is entirely API-shaped: the data comes from &lt;code&gt;app.terraform.io&lt;/code&gt;&apos;s REST endpoints, not from parsing customer source files. If your Terraform doesn&apos;t run through Terraform Cloud (a great many organizations run it through Atlantis, Spacelift, env0, Scalr, custom GitHub Actions, or &lt;code&gt;terraform apply&lt;/code&gt; from a bastion), the integration has nothing to ingest.&lt;/p&gt;
&lt;p&gt;There&apos;s a sharper distinction worth naming, because it shapes the rest of the argument. Port&apos;s integrations parse the outputs of upstream systems — Terraform Cloud&apos;s REST API, Kubernetes&apos; CRD schemas, Datadog&apos;s runbook YAML, GitHub&apos;s CODEOWNERS, PagerDuty&apos;s escalation policies. That is a kind of parsing, and Port does it well. What Port&apos;s integration estate doesn&apos;t do is parse customer source files for the IaC tool&apos;s own dependency-evaluation semantics — &lt;code&gt;terraform init&lt;/code&gt;&apos;s module installer, Helm&apos;s chart dependency resolver, &lt;code&gt;docker build&lt;/code&gt;&apos;s ARG substitution, GitHub Actions&apos; reusable workflow resolution. Those evaluators are part of the IaC tools themselves, not API surfaces of upstream vendors. The dependency relationship between the &lt;code&gt;networking&lt;/code&gt; module&apos;s source repo and the consumer repos lives in HCL &lt;code&gt;source = &quot;...&quot;&lt;/code&gt; declarations across the consumer repos, evaluated by Terraform itself at &lt;code&gt;init&lt;/code&gt; time, and Port&apos;s integration estate doesn&apos;t reach that layer.&lt;/p&gt;
&lt;h2&gt;Where the architecture pushes back&lt;/h2&gt;
&lt;p&gt;Concretely, here are the shapes of dependency that show up in every infrastructure-heavy organization and that the Blueprint-plus-integration model doesn&apos;t naturally absorb. I covered the parsing details in the &lt;a href=&quot;https://riftmap.dev/blog/series/find-every-consumer/&quot;&gt;&lt;em&gt;Find Every Consumer&lt;/em&gt; series&lt;/a&gt;, so I&apos;ll keep these short.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. Terraform module sources with git URLs and ref pins.&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;module &quot;vpc&quot; {
  source = &quot;git::https://gitlab.company.com/infra/terraform-modules.git//networking?ref=v3.2.0&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To model this in Port you&apos;d need a &lt;code&gt;terraform_module&lt;/code&gt; Blueprint, an &lt;code&gt;application_repo&lt;/code&gt; Blueprint, and a &lt;code&gt;consumes_module&lt;/code&gt; Relation between them — plus an integration that walks every consumer repo, parses HCL, normalizes git URLs across &lt;code&gt;https://&lt;/code&gt;, &lt;code&gt;git@&lt;/code&gt;, and stripped-&lt;code&gt;.git&lt;/code&gt; forms, parses the &lt;code&gt;?ref=&lt;/code&gt; query parameter, and evaluates whether the ref is a tag, a branch, or a commit SHA. Port doesn&apos;t ship that integration. (&lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-terraform-module/&quot;&gt;How to find every consumer of your Terraform module&lt;/a&gt;.)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. Docker &lt;code&gt;FROM&lt;/code&gt; with build-arg substitution.&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ARG BASE_TAG=latest
FROM company/base-runtime:${BASE_TAG}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The actual base image consumed in production depends on &lt;code&gt;--build-arg&lt;/code&gt; values that live in a separate file: &lt;code&gt;.github/workflows/build.yml&lt;/code&gt;, &lt;code&gt;docker-bake.hcl&lt;/code&gt;, &lt;code&gt;docker-compose.yml&lt;/code&gt;, a Makefile target. Resolving the real consumer relationship requires reading the Dockerfile, finding the default, then reading the build invocation across YAML and shell. (&lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-docker-base-image/&quot;&gt;How to find every consumer of your Docker base image&lt;/a&gt;.)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. Helm chart consumers across three reference formats.&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Chart.yaml dependency
dependencies:
  - name: platform-services
    version: &quot;~3.2.0&quot;
    repository: &quot;oci://registry.company.com/charts&quot;
    alias: monitoring
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# ArgoCD Application
spec:
  source:
    chart: platform-services
    repoURL: https://charts.company.com
    targetRevision: 3.2.1
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# Flux HelmRelease
spec:
  chart:
    spec:
      chart: platform-services
      version: &quot;&amp;gt;=3.0.0 &amp;lt;4.0.0&quot;
      sourceRef:
        kind: HelmRepository
        name: company-charts
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;All three reference the same chart. The first uses OCI, the second HTTPS, the third a Flux source pointer that needs separate resolution. A complete answer to &quot;who consumes &lt;code&gt;platform-services&lt;/code&gt; at v3.2.0&quot; needs to normalize all three forms to a canonical chart identity, evaluate constraints against published versions, follow Flux source pointers, and follow umbrella charts that re-export &lt;code&gt;platform-services&lt;/code&gt; as a subchart. (&lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-helm-chart/&quot;&gt;How to find every consumer of your Helm chart&lt;/a&gt;.)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. Reusable GitHub Actions workflows.&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;jobs:
  deploy:
    uses: company/actions/.github/workflows/deploy.yml@v2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;uses:&lt;/code&gt; value is a single string encoding &lt;code&gt;&amp;lt;owner&amp;gt;/&amp;lt;repo&amp;gt;/&amp;lt;path&amp;gt;@&amp;lt;ref&amp;gt;&lt;/code&gt;. Resolving &quot;who uses my deploy workflow&quot; means parsing this format, normalizing the repo identifier, evaluating the ref pin, and following transitive includes when a reusable workflow itself calls other reusable workflows. None of that is exposed in the GitHub REST API in structured form. (&lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-github-actions-workflow/&quot;&gt;How to find every consumer of your reusable GitHub Actions workflow&lt;/a&gt;.)&lt;/p&gt;
&lt;p&gt;Port&apos;s Blueprint system can absolutely &lt;em&gt;represent&lt;/em&gt; the resulting graph once it&apos;s been computed. What it can&apos;t naturally do is &lt;em&gt;compute&lt;/em&gt; it — because the integration estate is shaped to consume upstream system APIs, not to evaluate IaC tools&apos; dependency semantics against customer source files.&lt;/p&gt;
&lt;h2&gt;The maintenance gradient, scoped properly&lt;/h2&gt;
&lt;p&gt;I covered the broader version of this in the &lt;a href=&quot;https://riftmap.dev/blog/the-catalog-maintenance-trap/&quot;&gt;catalog maintenance trap post&lt;/a&gt; and applied it to the buying decision in &lt;a href=&quot;https://riftmap.dev/blog/backstage-alternatives/&quot;&gt;Backstage alternatives in 2026&lt;/a&gt;; the unqualified version of that critique applies more to first-generation IDPs (Backstage in particular) than to Port&apos;s current architecture. For the data Port&apos;s integrations cover — services discovered via Git, deployments via Kubernetes, on-call via PagerDuty — Port&apos;s integration-fed catalog materially reduces the drift problem.&lt;/p&gt;
&lt;p&gt;The drift problem applies precisely where it can&apos;t apply differently: data that lives in customer source files for which no upstream API exposes structured semantics. The relationship between &lt;code&gt;payments-helm&lt;/code&gt;&apos;s &lt;code&gt;Chart.yaml&lt;/code&gt; and &lt;code&gt;helm-library-chart&lt;/code&gt; at v1.2.0 is not in GitHub&apos;s REST API as structured dependency data. It&apos;s in YAML that the customer wrote, that Helm evaluates at install time, against a registry that the customer also wrote. To get it into a Blueprint, somebody — Port, a platform team, an Ocean partner — has to evaluate that grammar against those sources. The catalog-maintenance argument is unchanged for this specific layer: ask a human to write down what&apos;s already declared in source and the registered copy goes stale. Build a parser that reads the source on every scan and it doesn&apos;t.&lt;/p&gt;
&lt;p&gt;This is the gap. Not modeled catalogs in general — modeled catalogs for IaC-source-derived data specifically.&lt;/p&gt;
&lt;h2&gt;Steelmanned objections&lt;/h2&gt;
&lt;p&gt;A few objections, taken at their strongest.&lt;/p&gt;
&lt;h3&gt;&quot;Couldn&apos;t Port add IaC parsers via the Ocean framework?&quot;&lt;/h3&gt;
&lt;p&gt;In principle, yes. Port&apos;s &lt;a href=&quot;https://ocean.getport.io/&quot;&gt;Ocean Extensibility Framework&lt;/a&gt; lets anyone build a custom integration that ingests data into Port&apos;s Blueprints. Nothing prevents a third party — or Port themselves — from shipping an Ocean integration that parses customer Terraform, Helm, and Docker sources, and populates Port Entities with the resulting graph.&lt;/p&gt;
&lt;p&gt;The natural place for that work to live is not inside Port. It&apos;s at the layer below Port — a parser estate that does the IaC-specific resolution once and exposes structured output that &lt;em&gt;any&lt;/em&gt; downstream consumer can ingest. Port via Ocean. Backstage via a plugin. Cortex via their integration framework. An MCP server for any agent. A REST API for custom dashboards. The parser estate is the same; the consumers differ. The composition story isn&apos;t competitive — it&apos;s compositional: a source-derived dependency graph is most useful when it&apos;s exposed through every catalog and every agent tool list, not when it&apos;s locked inside any single IDP.&lt;/p&gt;
&lt;p&gt;What Port couldn&apos;t do without changing the architecture is make source-derived parsing a &lt;em&gt;native&lt;/em&gt; primitive — the way SCIP is native to Sourcegraph. The Blueprint-and-integration model is committed to &quot;data model is platform-team-defined, populated by integrations from external systems.&quot; Native parsing of customer source for IaC dependency semantics would mean a separate ingestion pipeline that derives Blueprints rather than being populated into them. That&apos;s a sister architecture, not an extension.&lt;/p&gt;
&lt;h3&gt;&quot;Isn&apos;t Port&apos;s AEP pitch the same context-for-agents pitch as Riftmap&apos;s?&quot;&lt;/h3&gt;
&lt;p&gt;The pitches converge at the surface and diverge at the layer. Both products are betting that &quot;context for agents&quot; is the next major budget category in platform engineering. Both expose MCP. Both make a credible case that agents without structured context are dangerous in production.&lt;/p&gt;
&lt;p&gt;The layer is where they diverge. Port&apos;s context layer is the modeled SDLC context: services, owners, scorecards, incident history, deployment history, on-call rotations, ticketing relationships. The agent picking up a Jira ticket needs to know which service is affected, who owns it, what the production tier is, what changed recently. Riftmap&apos;s context layer is the source-derived IaC dependency graph: which Terraform modules are consumed by which repos at which versions, which Helm charts are deployed against which Kubernetes manifests, which GHA workflows are reused across the org. The agent doing a cross-repo refactor needs to know which downstream consumers will break.&lt;/p&gt;
&lt;p&gt;An agent doing serious work in 2026–2027 needs both inputs. The dual-audience framing here is identical to the one in the &lt;a href=&quot;https://riftmap.dev/blog/symbol-graphs-and-artifact-graphs/&quot;&gt;symbol-graphs-and-artifact-graphs post&lt;/a&gt;: specialized context layers, composed by the agent&apos;s tool list, exposed through their respective MCP servers.&lt;/p&gt;
&lt;p&gt;Zohar Einy&apos;s runtime-context example in &lt;a href=&quot;https://www.port.io/blog/hidden-technical-debt-of-agentic-engineering&quot;&gt;the hidden-tech-debt post&lt;/a&gt; is actually consistent with this. His example splits cleanly across the two architectures: &lt;em&gt;&quot;what language and framework the service uses&quot;&lt;/em&gt; and &lt;em&gt;&quot;who owns the downstream service it calls&quot;&lt;/em&gt; are modeled-catalog answers. &lt;em&gt;&quot;How retries are handled in other services in this org&quot;&lt;/em&gt; and &lt;em&gt;&quot;whether there&apos;s been a recent config change&quot;&lt;/em&gt; sit closer to source-derived. The taxonomy his post lays out is right; the inference that one platform layer should provide all of it doesn&apos;t follow.&lt;/p&gt;
&lt;h3&gt;&quot;Doesn&apos;t Port&apos;s roadmap inevitably head this direction?&quot;&lt;/h3&gt;
&lt;p&gt;Maybe, eventually. The strategic question isn&apos;t whether Port could ship a feature labeled &quot;Terraform module dependency graph.&quot; It&apos;s whether the architecture that ships it is Blueprint-shaped or parser-shaped. A Blueprint-shaped version — where Port asks the platform team to declare &lt;code&gt;terraform_module&lt;/code&gt; and &lt;code&gt;application_repo&lt;/code&gt; Blueprints and populate them via a custom Ocean integration — is mostly already possible today and is the recommended path for custom data models. A parser-shaped version — where Port natively ingests customer Terraform source and derives the graph — would require a different architectural commitment.&lt;/p&gt;
&lt;p&gt;I expect the Blueprint-shaped path to ship first, because it fits Port&apos;s existing architecture. I expect it to underperform what dedicated parsers produce, because the resolver complexity is real and you can&apos;t compete with multi-quarter parser investments by writing a custom integration on top of a generic ingestion framework.&lt;/p&gt;
&lt;h2&gt;Composition, not competition&lt;/h2&gt;
&lt;p&gt;Every vendor in adjacent categories has repositioned around &quot;context for agents&quot; in 2026. Sourcegraph 7.0 in February called itself &lt;a href=&quot;https://sourcegraph.com/blog/a-new-era-for-sourcegraph-the-intelligence-layer-for-ai-coding-agents-and-developers&quot;&gt;&quot;the intelligence layer for AI coding agents and developers&quot;&lt;/a&gt;. Port relaunched at the December 2025 Series C as the &lt;a href=&quot;https://www.citybiz.co/article/782957/port-secures-100m-series-c-led-by-general-atlantic-to-accelerate-its-agentic-engineering-platform/&quot;&gt;Agentic Engineering Platform&lt;/a&gt; with the explicit framing that &lt;em&gt;&quot;humans and agents will run the SDLC together.&quot;&lt;/em&gt; Cursor, Claude Code, and Amp all ship MCP integrations and rules files. The category language is converging from three different starting points.&lt;/p&gt;
&lt;p&gt;The architecture taking shape across the agent ecosystem is multi-context composition. An agent working on a non-trivial cross-cutting change pulls from several specialized context layers:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Symbol context.&lt;/strong&gt; The code-graph layer Sourcegraph owns: cross-repo function calls, type usages, API references.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Modeled catalog context.&lt;/strong&gt; The IDP layer Port, Cortex, OpsLevel, Compass, and Backstage compete for: ownership, scorecards, incident history, deployment history, on-call rotations, business criticality.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Source-derived artifact context.&lt;/strong&gt; The IaC dependency layer Riftmap is built for: Terraform module consumers, Helm chart consumers, Docker base image consumers, GHA workflow consumers, transitive umbrella resolution, version constraint evaluation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Runtime telemetry context.&lt;/strong&gt; Datadog, Honeycomb, Sentry, log aggregators.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Project and decision context.&lt;/strong&gt; Linear, Jira, Notion, Confluence, ADR repos.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each layer is its own product category, with its own architecture and its own MCP server. The agent&apos;s tool list composes them. &lt;strong&gt;Cross-layer orchestration — composing the modeled catalog, the parsed dependency graph, the symbol graph, and the telemetry feeds into a single agent context — isn&apos;t owned by any one platform. It&apos;s configured by the platform team and executed by the agent.&lt;/strong&gt; Within-layer orchestration is a different and legitimate question — Port&apos;s Workflow Orchestrator is exactly that pitch for the modeled-catalog layer, and it&apos;s a coherent product inside that scope. Neither it nor any tool from an adjacent layer competes to be the cross-layer orchestrator, because the cross-layer orchestrator is the agent.&lt;/p&gt;
&lt;p&gt;This is also the structural answer to the LLM-loop objection (couldn&apos;t Cursor or Claude Code just grep through this?) — the &lt;a href=&quot;https://riftmap.dev/what-is-an-artifact-dependency-graph/&quot;&gt;artifact-graph&lt;/a&gt; case breaks the glob-and-grep loop because the truth requires resolver semantics, not just text matching. The &lt;a href=&quot;https://riftmap.dev/blog/symbol-graphs-and-artifact-graphs/&quot;&gt;symbol-graphs-and-artifact-graphs post&lt;/a&gt; covers the architectural reason in detail.&lt;/p&gt;
&lt;p&gt;The test for whether this category has matured is whether nobody is surprised that the IaC dependency layer lives behind its own MCP server, just as nobody is surprised that the code-symbol layer lives behind its own.&lt;/p&gt;
&lt;h2&gt;The short version&lt;/h2&gt;
&lt;p&gt;Modeled catalogs hold what humans register about their systems. The schema is whatever the platform team designed; the contents are whatever the integrations ingested. The source of truth is human judgment. For &lt;em&gt;&quot;who owns this service, what tier is it, is it production-ready, what&apos;s the on-call rotation&quot;&lt;/em&gt; — modeled catalog, Port, end of discussion.&lt;/p&gt;
&lt;p&gt;Parsed dependency graphs hold what infrastructure-as-code already declares about itself. The schema follows the ecosystems&apos; own grammars; the contents are derived by parsing source files; the source of truth is the customer&apos;s repos. For &lt;em&gt;&quot;who consumes my Terraform module at v3.2.0, which Helm releases pin which chart version, which reusable GHA workflow is called across the org&quot;&lt;/em&gt; — parsed dependency graph, &lt;a href=&quot;https://riftmap.dev/&quot;&gt;Riftmap&lt;/a&gt;, built for exactly this.&lt;/p&gt;
&lt;p&gt;The questions sound similar. The graphs are different shapes. Most platform teams have both questions, and the right answer is to use both kinds of tool — the same way a serious agent setup composes multiple specialized context layers instead of asking any one platform to serve every question.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This is the kind of question Riftmap is built to answer. It scans your GitHub or GitLab organisation with a read-only token, parses Terraform, Docker, Helm, Kustomize, Kubernetes, GitHub Actions, GitLab CI, Ansible, Go modules, and npm, and builds the cross-repo artifact graph as a queryable surface — for engineers in the UI, for agents over MCP. Five minutes to first graph. If you&apos;ve read this far, &lt;a href=&quot;https://app.riftmap.dev&quot;&gt;the free tier is here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;If you&apos;re interested in the architectural distinction from a different angle, the &lt;a href=&quot;https://riftmap.dev/blog/symbol-graphs-and-artifact-graphs/&quot;&gt;symbol-graphs-and-artifact-graphs post&lt;/a&gt; covers the same shape of argument for Sourcegraph and code-symbol graphs, and the &lt;a href=&quot;https://riftmap.dev/blog/the-catalog-maintenance-trap/&quot;&gt;catalog maintenance trap&lt;/a&gt; is the shorter companion piece on why modeled catalogs go stale for source-derivable data.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;For the underlying parsing work for any single ecosystem, the &lt;a href=&quot;https://riftmap.dev/blog/series/find-every-consumer/&quot;&gt;Find Every Consumer series&lt;/a&gt; goes one ecosystem at a time: &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-docker-base-image/&quot;&gt;Docker base images&lt;/a&gt;, &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-terraform-module/&quot;&gt;Terraform modules&lt;/a&gt;, &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-github-actions-workflow/&quot;&gt;GitHub Actions workflows&lt;/a&gt;, &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-helm-chart/&quot;&gt;Helm charts&lt;/a&gt;, &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-go-module/&quot;&gt;Go modules&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</content:encoded><category>platform-engineering</category><category>port</category><category>internal-developer-portal</category><category>blueprints</category><category>context-lake</category><category>agentic-engineering</category><author>Daniel Westgaard</author></item><item><title>Symbol graphs and artifact graphs: why Sourcegraph stops where infrastructure starts</title><link>https://riftmap.dev/blog/symbol-graphs-and-artifact-graphs/</link><guid isPermaLink="true">https://riftmap.dev/blog/symbol-graphs-and-artifact-graphs/</guid><description>Why Sourcegraph&apos;s symbol graph can&apos;t tell you who consumes your Helm chart at v3.2.0 — and why symbol graphs and artifact graphs are different categories.</description><pubDate>Fri, 22 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;A platform engineer asks two questions on the same morning. First: &quot;I&apos;m renaming this Go method — who calls it?&quot; Second: &quot;I&apos;m bumping our shared Terraform networking module to v3.3.0 — which application repos will run &lt;code&gt;terraform plan&lt;/code&gt; against the new version?&quot; The first is a symbol-graph question and Sourcegraph answers it at the top of the category. The second looks similar from a distance, has the same surface verb — &quot;who depends on this&quot; — and is, mechanically, a completely different graph. This post is about why.&lt;/em&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;A note on what this post is, and isn&apos;t&lt;/h2&gt;
&lt;p&gt;This is not a competitive teardown. Sourcegraph indexed 54 billion lines of code, &lt;a href=&quot;https://github.com/sourcegraph/scip&quot;&gt;created SCIP&lt;/a&gt; as an open language-agnostic protocol for code intelligence, and shipped &lt;a href=&quot;https://sourcegraph.com/blog/cross-repository-code-navigation&quot;&gt;cross-repository navigation at enterprise scale&lt;/a&gt; — at the time of writing they host SCIP indexes for over 45,000 public repos and serve customers including Reddit, Stripe, Canva, MongoDB, and Dropbox through their MCP server. Those are real engineering achievements and the product solves real problems well.&lt;/p&gt;
&lt;p&gt;The argument here is narrower: code-symbol graphs and infrastructure-artifact graphs are orthogonal categories, and the second one is structurally outside what Sourcegraph&apos;s index produces. This isn&apos;t a flaw in Sourcegraph. It follows from the design choices that make symbol graphs work in the first place.&lt;/p&gt;
&lt;p&gt;If you&apos;re a platform engineer evaluating &quot;context infrastructure&quot; — code search, IDPs, cross-repo tooling for humans and agents — the practical takeaway is that most teams need both kinds of graph, and the tools should look as different as the questions do.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Two questions that look similar from a distance&lt;/h2&gt;
&lt;p&gt;Sit down with a platform team and they will keep coming back to two questions. They sound similar enough that vendor categories blur them.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Question one:&lt;/strong&gt; &lt;em&gt;I&apos;m changing this function. Who calls it?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;This is a code-symbol question. The nodes are functions, methods, classes, types, modules. The edges are references — call sites, imports, type usages, inheritance. The engine that builds this graph is a language indexer: it parses TypeScript or Go or Java with a compiler-aware tool and emits a structured record of where each symbol is defined and where it&apos;s referenced. Sourcegraph is built directly on this category and is the standard against which everything in it is measured.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Question two:&lt;/strong&gt; &lt;em&gt;I&apos;m bumping this shared Terraform module to v3.3.0. Which application repos will re-plan? Which of them are pinned to v3.2.0, which to &lt;code&gt;~&amp;gt; 3.2&lt;/code&gt;, and which to &lt;code&gt;&amp;gt;= 3.0&lt;/code&gt;? Of those that float, which umbrella modules pull mine transitively?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;This is an infrastructure-artifact question. The nodes are Terraform modules, Docker base images, Helm charts, Kustomize bases, reusable GitHub Actions workflows, Kubernetes manifests. The edges are &lt;em&gt;artifact references inside infrastructure-as-code source files&lt;/em&gt;: &lt;code&gt;source = &quot;git::https://...?ref=v3.2.0&quot;&lt;/code&gt;, &lt;code&gt;FROM company/base:${BASE_TAG}&lt;/code&gt;, &lt;code&gt;dependencies[].repository: oci://...&lt;/code&gt;, &lt;code&gt;uses: company/actions/deploy@v2&lt;/code&gt;, &lt;code&gt;bases: - ../base-app&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The two graphs have the same surface verb — &quot;who depends on this&quot; — and almost nothing else in common.&lt;/p&gt;
&lt;h2&gt;What SCIP indexes — and what it doesn&apos;t&lt;/h2&gt;
&lt;p&gt;The clearest way to see why is to look at what SCIP, the protocol underneath Sourcegraph&apos;s cross-repo navigation, is actually designed to capture.&lt;/p&gt;
&lt;p&gt;Sourcegraph&apos;s &lt;a href=&quot;https://sourcegraph.com/blog/cross-repository-code-navigation&quot;&gt;own description&lt;/a&gt; of what SCIP indexes covers two categories of structured data: &lt;em&gt;symbols&lt;/em&gt; — carrying definition location, symbol metadata (function vs. class vs. variable vs. etc.), and package ownership including which repository and version defines the symbol — and &lt;em&gt;external symbols&lt;/em&gt; — tracking cross-repository dependencies, the symbols defined in other packages, and version data for each dependency.&lt;/p&gt;
&lt;p&gt;The keyword is &lt;em&gt;symbols&lt;/em&gt;. SCIP indexes the things a compiler or language server understands as named declarations: functions, types, methods, namespaces. To produce this data you need a parser that understands the &lt;em&gt;programming language&lt;/em&gt;&apos;s scoping and name-resolution rules. The same Sourcegraph post lists the official language coverage for auto-indexing as TypeScript, JavaScript, Python, Go, Java, Scala, Kotlin, and C/C++. The broader &lt;a href=&quot;https://github.com/sourcegraph/scip&quot;&gt;SCIP protocol&lt;/a&gt; has additional community indexers — for Ruby, .NET, Dart, PHP, Rust — but the marketed coverage of Sourcegraph&apos;s product is the eight-language list above.&lt;/p&gt;
&lt;p&gt;All eight are general-purpose programming languages. The indexer for each is built on the relevant language&apos;s compiler or language server. Notice what isn&apos;t there, and isn&apos;t on the roadmap: HCL, Dockerfile, Helm &lt;code&gt;Chart.yaml&lt;/code&gt;, Kubernetes manifest schemas, GitHub Actions workflow YAML, GitLab CI YAML, Kustomize, Ansible playbooks. Not because Sourcegraph forgot about infrastructure-as-code, but because IaC dependency relationships are not symbols. They&apos;re values inside strings that the IaC tool evaluates at deploy or build time. The grammar SCIP describes — definition location, symbol metadata, references — doesn&apos;t fit the shape of those relationships.&lt;/p&gt;
&lt;p&gt;This is by design, not by oversight. A language indexer that tried to also produce IaC artifact edges would be solving a different problem with the wrong abstraction.&lt;/p&gt;
&lt;h2&gt;How Sourcegraph itself recommends answering IaC dependency questions&lt;/h2&gt;
&lt;p&gt;The strongest evidence for the category split is in Sourcegraph&apos;s own canonical content. Three of their flagship posts on impact analysis and blast radius — the questions that overlap most with the &lt;a href=&quot;https://riftmap.dev/what-is-an-artifact-dependency-graph/&quot;&gt;artifact-graph&lt;/a&gt; use case — fall back to regex search over manifest files at exactly the point where the answer turns infrastructural.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://sourcegraph.com/blog/multi-repo-search-how-to-search-across-multiple-repositories&quot;&gt;Multi-repo search: How to search across multiple repositories&lt;/a&gt; (March 2025) introduces blast radius and impact analysis as a multi-repo search use case, with the symbol-graph navigation as the precise tool: &quot;It uses SCIP indexes to resolve actual symbol references, so you see real function calls and imports, not just string matches that happen to contain the function name.&quot; For the symbol case, that&apos;s the right answer.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://sourcegraph.com/blog/why-code-search-at-scale-is-essential-when-you-grow-beyond-one-repository&quot;&gt;Why code search at scale is essential when you grow beyond one repository&lt;/a&gt; (December 2025) walks through &quot;impact analysis before changes&quot; with a worked example. The example reaches for the artifact case — a Go service depending on a shared library — and the recommended query is:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;repo:myorg/.* file:go.mod content:&quot;auth-lib&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That&apos;s &lt;code&gt;content:&lt;/code&gt; regex over the contents of &lt;code&gt;go.mod&lt;/code&gt; files, scoped to repos matching &lt;code&gt;myorg/.*&lt;/code&gt;. It works. It will find every &lt;code&gt;go.mod&lt;/code&gt; in the org that mentions &lt;code&gt;auth-lib&lt;/code&gt;. It will not tell you which version each consumer is pinned to in a way the next tool in your pipeline can act on without further parsing, it won&apos;t follow &lt;code&gt;replace&lt;/code&gt; directives, and it won&apos;t resolve module path aliases.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://sourcegraph.com/blog/cross-repository-code-navigation&quot;&gt;Cross-Repository Code Navigation&lt;/a&gt; (January 2026) reaches the same pattern for npm. The post&apos;s worked example for &quot;Finding All Usages of a Dependency&quot; is:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;context:global file:package.json &quot;your-internal-lib&quot;:\s*&quot;[~^]?1\.2\.3&quot; patterntype:regexp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Again — regex over &lt;code&gt;package.json&lt;/code&gt; strings. Again it works for finding the &lt;em&gt;files&lt;/em&gt;. It doesn&apos;t return a structured consumer list with resolved version constraints across &lt;code&gt;~&lt;/code&gt;, &lt;code&gt;^&lt;/code&gt;, &lt;code&gt;&amp;gt;=&lt;/code&gt;, and exact-pin forms; the regex enumerates each one explicitly.&lt;/p&gt;
&lt;p&gt;This is not a critique of those posts. They&apos;re honest and they reach for the right tool. &lt;em&gt;Symbol search isn&apos;t the tool for this question — for this question, text matching over manifest files is.&lt;/em&gt; But that itself is the observation worth sitting with: when Sourcegraph&apos;s own writers reach for dependency-impact use cases, they pivot from SCIP navigation to regex over manifest files. The reason is structural. SCIP doesn&apos;t index manifest files because manifest files are not source code in the sense SCIP was designed for. They&apos;re declarations a build tool will later resolve against an external system — a module registry, a container registry, a git remote, a chart repository, a Kubernetes API server.&lt;/p&gt;
&lt;p&gt;The regex falls back because the symbol graph has no opinion about manifest declarations. It can&apos;t. That&apos;s not what it indexes.&lt;/p&gt;
&lt;h2&gt;The homepage demo, read carefully&lt;/h2&gt;
&lt;p&gt;Sourcegraph&apos;s current homepage runs a side-by-side comparison that illustrates the category split better than any external critique could.&lt;/p&gt;
&lt;p&gt;The scenario: a developer asks an AI coding agent to add a &lt;code&gt;Role&lt;/code&gt; field to a &lt;code&gt;User&lt;/code&gt; struct in &lt;code&gt;models/user.go&lt;/code&gt;. The baseline agent edits &lt;code&gt;models/user.go&lt;/code&gt; and &lt;code&gt;database/user_store.go&lt;/code&gt;, declares itself done, and offers a parting suggestion to add a migration. The post-it-note list of &quot;what the agent missed&quot; reads:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Auth middleware — no role check, any user can access admin routes&lt;/li&gt;
&lt;li&gt;API response DTO — role never returned to clients&lt;/li&gt;
&lt;li&gt;Audit logging — role changes not tracked, no compliance trail&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/admin&lt;/code&gt; frontend routes — no guard, UI still accessible to all&lt;/li&gt;
&lt;li&gt;Invite flow — new users created without a default role&lt;/li&gt;
&lt;li&gt;4 integration tests — assert on user shape, will break&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Then the same agent runs with Sourcegraph MCP. The agent calls &lt;code&gt;sg_keyword_search &quot;User struct&quot; across 2,847 repositories&lt;/code&gt;, gets 31 files across 7 layers, and produces a complete change.&lt;/p&gt;
&lt;p&gt;This is a strong demo and the win is real. But notice the exact shape of the problem and the exact shape of the offered tool. Every item on the &quot;missed&quot; list is a &lt;em&gt;cross-cutting code-level concern&lt;/em&gt;: a middleware function that checks the user, a DTO type that serializes the user, an audit hook that writes when a user changes, a route guard that consults the user, an invite handler that constructs a user, integration tests that assert on the user shape. Every one of those is a symbol relationship. They are exactly what SCIP indexes and exactly what &lt;code&gt;find_references&lt;/code&gt; and &lt;code&gt;keyword_search&lt;/code&gt; are designed to surface.&lt;/p&gt;
&lt;p&gt;Now imagine an adjacent scenario: a platform engineer is bumping the shared &lt;code&gt;terraform-modules/networking&lt;/code&gt; module from &lt;code&gt;v3.2.0&lt;/code&gt; to &lt;code&gt;v3.3.0&lt;/code&gt; because the new version renames a variable. What does an equivalent demo look like?&lt;/p&gt;
&lt;p&gt;The missed items aren&apos;t &lt;code&gt;middleware/auth.go&lt;/code&gt;, &lt;code&gt;api/dto/user_response.go&lt;/code&gt;, &lt;code&gt;routes/admin/guard.ts&lt;/code&gt;. They&apos;re: the eight application repos that pin &lt;code&gt;?ref=v3.2.0&lt;/code&gt; directly in a Terragrunt root, the four repos that pin &lt;code&gt;~&amp;gt; 3.2&lt;/code&gt; and will float to the new version on next plan, the two umbrella modules in &lt;code&gt;infra-platform-modules&lt;/code&gt; that re-export &lt;code&gt;networking&lt;/code&gt; and are themselves consumed by another twelve repos, the one repo where the module is pulled through an intermediate &lt;code&gt;terraform-aws-modules/internal-wrapper&lt;/code&gt; chain, and the three GitOps repos that reference the module path in an Atlantis &lt;code&gt;repos.yaml&lt;/code&gt;. None of those relationships is a symbol. None of them appears in a SCIP index. A search for &lt;code&gt;terraform-modules/networking&lt;/code&gt; returns a list of files containing that string and leaves the resolution work — git URL canonicalization, ref-pin parsing, semver constraint evaluation, transitive umbrella resolution, GitOps wiring — to whoever reads the results.&lt;/p&gt;
&lt;p&gt;The homepage demo is correct that an AI agent without cross-repository context misses cross-cutting changes. It&apos;s also correct that a symbol graph closes that gap for code-level concerns. It just doesn&apos;t close the same gap for IaC-level concerns, because those concerns aren&apos;t built out of symbols.&lt;/p&gt;
&lt;h2&gt;What a symbol graph cannot resolve — four worked examples&lt;/h2&gt;
&lt;p&gt;Concretely, here are four shapes of dependency that show up in every infrastructure-heavy org, and that no code-symbol indexer can resolve without becoming a different product.&lt;/p&gt;
&lt;h3&gt;1. Terraform module sources with git URLs and ref pins&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;module &quot;vpc&quot; {
  source = &quot;git::https://gitlab.company.com/infra/terraform-modules.git//networking?ref=v3.2.0&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Nothing here is a symbol. &lt;code&gt;source&lt;/code&gt; is an HCL attribute. The value is a single string with overloaded semantics:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;git::&lt;/code&gt; is a Terraform protocol prefix&lt;/li&gt;
&lt;li&gt;&lt;code&gt;https://gitlab.company.com/infra/terraform-modules.git&lt;/code&gt; is a canonical git URL — which Riftmap normalizes against the same git remote whether it appears as &lt;code&gt;https://&lt;/code&gt;, &lt;code&gt;git@gitlab...&lt;/code&gt;, or with &lt;code&gt;.git&lt;/code&gt; stripped&lt;/li&gt;
&lt;li&gt;&lt;code&gt;//networking&lt;/code&gt; is a subdirectory inside the module repo&lt;/li&gt;
&lt;li&gt;&lt;code&gt;?ref=v3.2.0&lt;/code&gt; is a git ref pin (could equally be a branch, a tag, or a commit SHA)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To answer &quot;which repos consume the &lt;code&gt;networking&lt;/code&gt; module of &lt;code&gt;terraform-modules&lt;/code&gt; at v3.2.0&quot; you need a parser that understands this string format and a resolver that walks the repo URL back to a canonical identity. A code indexer sees an HCL attribute assignment with a string literal.&lt;/p&gt;
&lt;h3&gt;2. Docker &lt;code&gt;FROM&lt;/code&gt; with build-arg substitution&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;ARG BASE_TAG=latest
FROM company/base-runtime:${BASE_TAG}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The actual base image consumed in production depends on the &lt;code&gt;--build-arg&lt;/code&gt; passed at build time, which usually lives in a separate file — &lt;code&gt;.github/workflows/build.yml&lt;/code&gt;, a &lt;code&gt;docker-bake.hcl&lt;/code&gt;, a &lt;code&gt;docker-compose.yml&lt;/code&gt;, a Makefile target. Resolving the real consumer relationship requires reading the Dockerfile, finding the default, then reading the build invocation to see if it&apos;s overridden. A symbol graph doesn&apos;t model build-time evaluation of CI variables across YAML files.&lt;/p&gt;
&lt;h3&gt;3. Helm chart consumers across three reference formats&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# Chart.yaml dependency
dependencies:
  - name: platform-services
    version: &quot;~3.2.0&quot;
    repository: &quot;oci://registry.company.com/charts&quot;
    alias: monitoring  # the chart is installed under this name
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# ArgoCD Application
spec:
  source:
    chart: platform-services
    repoURL: https://charts.company.com
    targetRevision: 3.2.1
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# Flux HelmRelease
spec:
  chart:
    spec:
      chart: platform-services
      version: &quot;&amp;gt;=3.0.0 &amp;lt;4.0.0&quot;
      sourceRef:
        kind: HelmRepository
        name: company-charts
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;All three reference the same chart. The first uses OCI; the second uses HTTPS; the third uses a Flux &lt;code&gt;HelmRepository&lt;/code&gt; pointer that needs to be resolved separately to a registry. The first uses a semver constraint &lt;code&gt;~3.2.0&lt;/code&gt; and an alias that breaks name-matching in values overrides. The second is an exact pin. The third is an explicit semver range. A complete answer to &quot;who consumes &lt;code&gt;platform-services&lt;/code&gt;&quot; requires (a) normalizing all three forms to a canonical chart identity, (b) evaluating each version constraint against the chart&apos;s published versions, (c) following Flux source pointers, and (d) following umbrella charts that re-export &lt;code&gt;platform-services&lt;/code&gt; as a subchart. None of this is a symbol relationship. We covered the full Helm case in detail in &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-helm-chart/&quot;&gt;How to Find Every Consumer of Your Helm Chart&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;4. Reusable GitHub Actions workflows&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;jobs:
  deploy:
    uses: company/actions/.github/workflows/deploy.yml@v2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;uses:&lt;/code&gt; value is a single string encoding &lt;code&gt;&amp;lt;owner&amp;gt;/&amp;lt;repo&amp;gt;/&amp;lt;path&amp;gt;@&amp;lt;ref&amp;gt;&lt;/code&gt;. Resolving &quot;who uses my deploy workflow&quot; means parsing this format, normalizing the repo identifier, evaluating the ref pin, and following transitive includes when a reusable workflow itself calls other reusable workflows. None of it lives in a programming language&apos;s symbol table.&lt;/p&gt;
&lt;p&gt;The pattern across all four: the consumer relationship is encoded in a string inside an IaC declaration, and resolving it requires understanding the IaC tool&apos;s grammar and its evaluation semantics. SCIP indexes none of this, on purpose. A different protocol indexes it, and a different parser estate emits it.&lt;/p&gt;
&lt;h2&gt;What each tool actually returns&lt;/h2&gt;
&lt;p&gt;Concretely: imagine you&apos;re the owner of &lt;code&gt;platform-services&lt;/code&gt; Helm chart at v3.2.0 and you&apos;re about to publish v3.3.0, which renames a top-level value key.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;With Sourcegraph, the recommended pattern is regex over &lt;code&gt;Chart.yaml&lt;/code&gt;&lt;/strong&gt; (and ArgoCD &lt;code&gt;Application&lt;/code&gt; manifests, and Flux &lt;code&gt;HelmRelease&lt;/code&gt; CRDs, and any shell scripts that call &lt;code&gt;helm install&lt;/code&gt;, each a separate query):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;context:global file:Chart.yaml &quot;platform-services&quot; patterntype:regexp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;What you get back is a list of file matches: each result is a file path, a line number, a snippet showing &lt;code&gt;platform-services&lt;/code&gt; in context, and PageRank-style ranking. You then open each file to read the version constraint, work out which chart identity is being referenced (HTTPS vs OCI vs alias), and manually evaluate which consumers will float to v3.3.0 versus stay on v3.2.x. The work scales with the number of results.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;With Riftmap, the call against the same artifact looks like this:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GET /api/v1/artifacts/{helm_chart_id}/consumers
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;artifact&quot;: {
    &quot;id&quot;: &quot;b3790d64-c693-47d5-83b0-3a2c3872faf9&quot;,
    &quot;artifact_type&quot;: &quot;helm_chart&quot;,
    &quot;name&quot;: &quot;helm-library-chart&quot;,
    &quot;source_repository&quot;: {
      &quot;full_path&quot;: &quot;polaris-works/platform/helm-library-chart&quot;
    },
    &quot;version&quot;: &quot;1.2.0&quot;,
    &quot;consumer_count&quot;: 5,
    &quot;is_orphan&quot;: false
  },
  &quot;consumers&quot;: [
    {
      &quot;repository&quot;: { &quot;name&quot;: &quot;data-helm&quot;,               &quot;full_path&quot;: &quot;polaris-works/data/data-helm&quot; },
      &quot;version_constraint&quot;: &quot;1.1.0&quot;,
      &quot;source_file&quot;: &quot;Chart.yaml&quot;, &quot;source_line&quot;: 8,
      &quot;is_latest&quot;: false,
      &quot;import_count&quot;: 1
    },
    {
      &quot;repository&quot;: { &quot;name&quot;: &quot;portal-helm&quot;,             &quot;full_path&quot;: &quot;polaris-works/portal/portal-helm&quot; },
      &quot;version_constraint&quot;: &quot;&amp;gt;=1.0.0 &amp;lt;2.0.0&quot;,
      &quot;source_file&quot;: &quot;Chart.yaml&quot;, &quot;source_line&quot;: 8,
      &quot;is_latest&quot;: true,
      &quot;import_count&quot;: 1
    },
    {
      &quot;repository&quot;: { &quot;name&quot;: &quot;logistics-helm&quot;,          &quot;full_path&quot;: &quot;polaris-works/logistics/logistics-helm&quot; },
      &quot;version_constraint&quot;: &quot;1.2.0&quot;,
      &quot;source_file&quot;: &quot;Chart.yaml&quot;, &quot;source_line&quot;: 8,
      &quot;is_latest&quot;: true,
      &quot;import_count&quot;: 1
    },
    {
      &quot;repository&quot;: { &quot;name&quot;: &quot;payments-multi-artifact&quot;, &quot;full_path&quot;: &quot;polaris-works/payments/payments-multi-artifact&quot; },
      &quot;version_constraint&quot;: &quot;1.2.0&quot;,
      &quot;source_file&quot;: &quot;Chart.yaml&quot;, &quot;source_line&quot;: 8,
      &quot;is_latest&quot;: true,
      &quot;import_count&quot;: 1
    },
    {
      &quot;repository&quot;: { &quot;name&quot;: &quot;payments-helm&quot;,           &quot;full_path&quot;: &quot;polaris-works/payments/payments-helm&quot; },
      &quot;version_constraint&quot;: &quot;~1.2.0&quot;,
      &quot;source_file&quot;: &quot;Chart.yaml&quot;, &quot;source_line&quot;: 8,
      &quot;is_latest&quot;: true,
      &quot;import_count&quot;: 1
    }
  ],
  &quot;total_consumers&quot;: 5,
  &quot;consumers_on_latest&quot;: 4,
  &quot;consumers_lagging&quot;: 1,
  &quot;latest_version&quot;: &quot;1.2.0&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Five consumers, four pin shapes the resolver normalises (exact-current &lt;code&gt;1.2.0&lt;/code&gt;, exact-behind &lt;code&gt;1.1.0&lt;/code&gt;, tilde range &lt;code&gt;~1.2.0&lt;/code&gt;, explicit range &lt;code&gt;&amp;gt;=1.0.0 &amp;lt;2.0.0&lt;/code&gt;), and &lt;code&gt;consumers_lagging: 1&lt;/code&gt; already evaluated against the chart&apos;s published version — the consumer table is the structured answer to &quot;who consumes my chart at v1.2.0&quot; without leaving anything for the agent to parse.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://riftmap.dev/images/blog/symbol-graphs-helm-consumers.png&quot; alt=&quot;Riftmap artifact-consumers view for : five umbrella consumers across the polaris-works testbed, one () flagged Behind for pinning  while the chart sits at . The two range-form constraints (, ) render alongside two exact-current pins.&quot; /&gt;&lt;/p&gt;
&lt;p&gt;What you get back is a structured consumer list. The same chart identity has been resolved across HTTPS, OCI, and Flux pointer forms before you see it. Each consumer carries its version constraint (&lt;code&gt;~3.2.0&lt;/code&gt;, &lt;code&gt;&amp;gt;=3.0.0 &amp;lt;4.0.0&lt;/code&gt;, exact &lt;code&gt;3.2.1&lt;/code&gt;) already evaluated against the published versions, so consumers within range of v3.3.0 are flagged. Umbrella charts that re-export &lt;code&gt;platform-services&lt;/code&gt; are followed transitively; their downstream consumers appear with their own constraints. The work doesn&apos;t scale with the result count — the structure has been resolved once, server-side, by the parser estate.&lt;/p&gt;
&lt;p&gt;Neither answer makes the other one wrong. They&apos;re answers to different questions, with different shapes, returned by different machinery.&lt;/p&gt;
&lt;h2&gt;Steelmanning the obvious objections&lt;/h2&gt;
&lt;p&gt;A few obvious objections, taken at their strongest.&lt;/p&gt;
&lt;h3&gt;&quot;Couldn&apos;t Sourcegraph add IaC parsers?&quot;&lt;/h3&gt;
&lt;p&gt;Yes, in principle. SCIP is an open protocol. Someone could write &lt;code&gt;scip-hcl&lt;/code&gt; or &lt;code&gt;scip-helm&lt;/code&gt; and emit Terraform-shaped or Helm-shaped records into the same index format Sourcegraph already serves.&lt;/p&gt;
&lt;p&gt;The hard part isn&apos;t the parser. The hard part is the &lt;em&gt;evaluator&lt;/em&gt;. The output of &lt;code&gt;scip-hcl&lt;/code&gt; would have to be a different kind of fact — not &quot;this symbol is defined here and referenced there&quot; but &quot;this &lt;code&gt;source = &quot;git::...&quot;&lt;/code&gt; string, when evaluated by Terraform&apos;s module installer against the current state of remote git repositories, resolves to repo X at commit Y, in the context of which an attribute called Z is defined, which is consumed by a &lt;code&gt;module&lt;/code&gt; block in repo W that depends on commit V of the same module.&quot; That&apos;s not a symbol-graph fact. It&apos;s an artifact-graph fact. You can serialize it in SCIP if you want, but the data model and the consumer code paths in Sourcegraph (go-to-definition, find-references, version-aware symbol lookup) are built for the symbol case. You&apos;d have to build a parallel pipeline that resolves git URLs, parses semver constraints across &lt;code&gt;~&lt;/code&gt;/&lt;code&gt;^&lt;/code&gt;/&lt;code&gt;&amp;gt;=&lt;/code&gt;, follows umbrella charts, evaluates &lt;code&gt;ARG&lt;/code&gt; defaults, walks reusable workflow chains, and normalizes container image references across registries. At that point you&apos;ve built a different product that happens to share an index format with the first one.&lt;/p&gt;
&lt;p&gt;The same logic applies to any code-symbol vendor — Cody, Greptile, OpenGrep, Augment, the rest of the category. The category isn&apos;t structurally suited to the artifact question. That&apos;s the whole point.&lt;/p&gt;
&lt;h3&gt;&quot;Can&apos;t an LLM with Cody&apos;s @-mention just figure this out?&quot;&lt;/h3&gt;
&lt;p&gt;This is the version of the objection that requires the most care, because Anthropic&apos;s lead on Claude Code has staked out a clear public position on the opposite side. In Boris Cherny&apos;s &lt;a href=&quot;https://newsletter.pragmaticengineer.com/p/building-claude-code-with-boris-cherny&quot;&gt;Pragmatic Engineer interview&lt;/a&gt;, the team described trying local vector databases, recursive model-based indexing, and other RAG-shaped approaches for agentic search. The conclusion was that &quot;plain glob and grep, driven by the model, beat everything,&quot; and Claude Code shipped with that as the architecture.&lt;/p&gt;
&lt;p&gt;Don&apos;t read this as a counter-argument to refute. Read it as structural validation of the category split. The Cherny bet is specifically that for &lt;em&gt;symbol-level&lt;/em&gt; questions inside a working session, an LLM driving primitive tools (glob, grep, read) outperforms a pre-built index whose maintenance cost includes staleness, permissions, and integration complexity. That&apos;s a coherent and defensible position for the symbol case, and a careful reader of the Pragmatic Engineer interview will notice why it works: when the model greps for a function name, the relevant truth — the call site, the import statement, the type signature — is &lt;em&gt;sitting in the source code the model can read&lt;/em&gt;. The model can iterate. It runs another grep, opens another file, and converges on a verified answer within the session. The primitive tool plus the model plus the source code is a complete loop.&lt;/p&gt;
&lt;p&gt;The artifact case breaks the loop because the truth isn&apos;t sitting in the source files in a form grep can converge on.&lt;/p&gt;
&lt;p&gt;Grep &lt;code&gt;terraform-modules/networking&lt;/code&gt; across an org and you get a list of files that mention the string. What grep cannot return — and what reading those files cannot recover without rebuilding the parser estate inside the conversation — is the &lt;em&gt;resolved&lt;/em&gt; answer: which of those references canonicalize to the same module across &lt;code&gt;git::https://&lt;/code&gt;, &lt;code&gt;git@gitlab:&lt;/code&gt;, and stripped-&lt;code&gt;.git&lt;/code&gt; URL forms; which version constraints (&lt;code&gt;~3.2.0&lt;/code&gt;, &lt;code&gt;^3.0.0&lt;/code&gt;, &lt;code&gt;&amp;gt;=3.0.0 &amp;lt;4&lt;/code&gt;, exact &lt;code&gt;3.2.1&lt;/code&gt;) include v3.3.0 after evaluation against the published version list; which Flux &lt;code&gt;HelmRepository&lt;/code&gt; source pointer in &lt;code&gt;gitops/sources/&lt;/code&gt; resolves to which registry; which umbrella module re-exports &lt;code&gt;networking&lt;/code&gt; and pulls in its own downstream consumers; which &lt;code&gt;ARG&lt;/code&gt;-substituted &lt;code&gt;FROM&lt;/code&gt; line in a Dockerfile actually resolves to which base image after CI evaluation. The resolver complexity doesn&apos;t surface in grep output. It has to be built once, somewhere, against the IaC tools&apos; grammars and against external registries — or the model has to rebuild it on every query, against text matches, with no audit trail.&lt;/p&gt;
&lt;p&gt;That&apos;s why dependency questions want deterministic answers and why &quot;92% confidence that these are your consumers&quot; is unshippable. You can&apos;t merge a change to a shared Terraform module against an LLM probability distribution over its consumer set; you need the actual set, derived from actual source, with an audit trail you can hand to the consumer teams before you ship. The deterministic-graph approach has to survive even if Cherny&apos;s bet wins everywhere it&apos;s making its bet — because in the artifact case the model can&apos;t iterate to verification from primitive tools. The verification primitive is the parser estate itself.&lt;/p&gt;
&lt;h3&gt;&quot;Doesn&apos;t &lt;code&gt;terraform graph&lt;/code&gt; already solve this?&quot;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;terraform graph&lt;/code&gt; is excellent and frequently misunderstood. It produces the DAG of a &lt;em&gt;single&lt;/em&gt; configuration — the resource dependencies inside one root module, used by Terraform&apos;s planner to schedule create/update/destroy operations in the correct order. It is not cross-repo, it is not cross-ecosystem, and it has no opinion about Docker base images, Helm charts, or reusable workflows. The same applies to Atlantis stack graphs, Spacelift stack dependencies, and HashiCorp Cloud Platform&apos;s Module Explorer — each solves a slice of the Terraform-orchestration problem from within a specific runner. The artifact graph in this post sits one level up: source-derived, cross-repo, cross-ecosystem, runner-agnostic.&lt;/p&gt;
&lt;h2&gt;What this means for the agent layer&lt;/h2&gt;
&lt;p&gt;If you&apos;ve followed agent-context infrastructure in 2026 at all, you&apos;ve watched every vendor in adjacent categories reposition around &quot;the intelligence layer for AI coding agents and developers.&quot; Sourcegraph 7.0 (February 25, 2026) used that exact phrase to &lt;a href=&quot;https://sourcegraph.com/blog/a-new-era-for-sourcegraph-the-intelligence-layer-for-ai-coding-agents-and-developers&quot;&gt;formalize the shift&lt;/a&gt;. Their MCP server lists Reddit, Stripe, MongoDB, Canva, Dropbox as customers using &lt;code&gt;keyword_search&lt;/code&gt;, &lt;code&gt;nls_search&lt;/code&gt;, &lt;code&gt;go_to_definition&lt;/code&gt;, &lt;code&gt;find_references&lt;/code&gt;, &lt;code&gt;commit_search&lt;/code&gt;, &lt;code&gt;diff_search&lt;/code&gt;, &lt;code&gt;deepsearch&lt;/code&gt;, and a small set of related primitives.&lt;/p&gt;
&lt;p&gt;That&apos;s the right shape of MCP server for symbol-level agent context. It&apos;s the shape Sourcegraph has been building toward for over a decade and the customer list reflects that fit.&lt;/p&gt;
&lt;p&gt;It&apos;s also not the &lt;em&gt;only&lt;/em&gt; MCP server a serious agent setup will compose by 2027.&lt;/p&gt;
&lt;p&gt;The architecture taking shape across the agent ecosystem is multi-context composition. An agent working on a non-trivial cross-cutting change pulls from several specialized context layers: symbol context (the code-graph layer Sourcegraph dominates), artifact context (the IaC dependency layer Riftmap exists to provide), ticket and project context (Linear, Jira, GitHub Issues), documentation and decision context (Notion, Confluence, ADR repos), observability and runtime context (Datadog, Honeycomb, Sentry, log aggregators). Each layer has its own grammar, its own retrieval primitives, its own freshness model, its own MCP server. The agent&apos;s orchestration layer composes them.&lt;/p&gt;
&lt;p&gt;In that picture, Riftmap and Sourcegraph aren&apos;t alternatives and they aren&apos;t complements in the usual &quot;we play well together&quot; sense either. They&apos;re &lt;em&gt;peers in a converging architecture&lt;/em&gt;. The fact that the IaC artifact layer now has dedicated infrastructure is not evidence that Sourcegraph is wrong about code-symbol context. It&apos;s evidence that the architecture is maturing — that the field has gotten serious enough about agent context to specialize the layers instead of asking one tool to serve every question.&lt;/p&gt;
&lt;p&gt;Both MCP servers can sit in the same agent&apos;s tool list. They answer questions Sourcegraph was built to answer well, and questions Sourcegraph was reasonably not built to answer at all. The composition is the point.&lt;/p&gt;
&lt;h2&gt;The short version&lt;/h2&gt;
&lt;p&gt;Symbol graphs index how code calls itself. The nodes are functions and types; the edges are references; the indexer is a language-aware parser; the protocol is SCIP. Sourcegraph is the standard-bearer for this category and has been for over a decade. For &quot;where is this function called, who depends on this API, what&apos;s the blast radius of a refactor&quot; — symbol graph, Sourcegraph, end of discussion.&lt;/p&gt;
&lt;p&gt;Artifact graphs index how infrastructure consumes itself. The nodes are Terraform modules and Docker images and Helm charts and reusable workflows; the edges are artifact references inside IaC source; the parser estate is one-per-ecosystem and the resolution heuristics are IaC-tool-specific. For &quot;who consumes my Helm chart at v3.2.0, which repos will re-plan on a Terraform module bump, what&apos;s the blast radius of changing a shared GitHub Actions workflow&quot; — artifact graph, &lt;a href=&quot;https://riftmap.dev/&quot;&gt;Riftmap&lt;/a&gt;, built for exactly this.&lt;/p&gt;
&lt;p&gt;The questions sound similar. The graphs are different shapes. Most platform teams have both questions, and the right answer is to use both kinds of tool — the same way a serious agent setup in 2026 composes multiple specialized context layers instead of asking any one tool to serve every question.&lt;/p&gt;
&lt;p&gt;That&apos;s the test for when this category has matured: when nobody is surprised that the IaC dependency graph lives behind its own MCP server.&lt;/p&gt;
&lt;p&gt;I have since measured this across two real organisations — Prometheus and Cloud Posse — counting every cross-repo edge: the infrastructure share runs from a third to nearly all, and not one edge is a code symbol. The breakdown is in &lt;a href=&quot;https://riftmap.dev/blog/cross-repo-edge-composition/&quot;&gt;I counted every cross-repo edge in two real orgs&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The category has also since gained its largest entrant. GitLab shipped Orbit in June 2026, a property graph of code symbols and SDLC objects spanning a whole &lt;a href=&quot;http://GitLab.com&quot;&gt;GitLab.com&lt;/a&gt; group, served to agents over MCP. It is a serious graph, and it draws the same line this post draws: no HCL parser, no Dockerfile parser, no artifact edges. I spent two days in the docs and the data model and wrote up &lt;a href=&quot;https://riftmap.dev/blog/gitlab-orbit-and-the-artifact-layer/&quot;&gt;what GitLab Orbit maps, and the layer it cannot reach&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;There is a second axis worth separating from this one. This post splits graphs by &lt;em&gt;what&lt;/em&gt; they index — code symbols versus infrastructure artifacts. A different split asks &lt;em&gt;how&lt;/em&gt; the graph is produced: parsed from the manifests your repositories already declare, or modeled by hand in a catalog someone maintains. I drew that line against Port&apos;s Blueprints in &lt;a href=&quot;https://riftmap.dev/blog/modeled-graphs-and-parsed-graphs/&quot;&gt;modeled graphs and parsed graphs&lt;/a&gt;, and pushed &lt;a href=&quot;https://riftmap.dev/blog/declared-inferred-registered/&quot;&gt;declared vs inferred&lt;/a&gt; further into a three-way split — declared, inferred, or registered — in a separate post on how a tool comes to know an edge exists at all.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This is the kind of question Riftmap is built to answer. It scans your GitHub or GitLab organisation with a read-only token, parses Terraform, Docker, Helm, Kustomize, Kubernetes, GitHub Actions, GitLab CI, Ansible, Go modules, and npm, and builds the cross-repo artifact graph as a queryable surface — for engineers in the UI, for agents over MCP. Five minutes to first graph. If you&apos;ve read this far, &lt;a href=&quot;https://app.riftmap.dev&quot;&gt;the free tier is here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;If you&apos;re interested in the underlying parsing work for any single ecosystem, the &lt;a href=&quot;https://riftmap.dev/blog/series/find-every-consumer/&quot;&gt;Find Every Consumer series&lt;/a&gt; goes one ecosystem at a time: &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-docker-base-image/&quot;&gt;Docker base images&lt;/a&gt;, &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-terraform-module/&quot;&gt;Terraform modules&lt;/a&gt;, &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-github-actions-workflow/&quot;&gt;GitHub Actions workflows&lt;/a&gt;, &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-helm-chart/&quot;&gt;Helm charts&lt;/a&gt;, &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-go-module/&quot;&gt;Go modules&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</content:encoded><category>sourcegraph</category><category>scip</category><category>code-symbol-graph</category><category>artifact-graph</category><category>cross-repo-context</category><category>ai-coding-agents</category><category>platform-engineering</category><author>Daniel Westgaard</author></item><item><title>Cross-repo context is in product docs. The graph is not.</title><link>https://riftmap.dev/blog/cross-repo-context-is-in-product-docs/</link><guid isPermaLink="true">https://riftmap.dev/blog/cross-repo-context-is-in-product-docs/</guid><description>The vocabulary moved into vendor docs in sixty days. The parser-derived cross-repo dependency graph it describes hasn&apos;t shipped in any AI coding product.</description><pubDate>Tue, 19 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;Where the vocabulary moved, what the products shipped against it, and the layer that&apos;s still uncovered.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Six months ago, &quot;cross-repo context&quot; lived in &lt;a href=&quot;http://dev.to&quot;&gt;dev.to&lt;/a&gt; posts and HN comments. Today it lives in vendor product documentation, in CPO interviews, in feature pages. It is in &lt;a href=&quot;https://docs.warp.dev/agent-platform/capabilities/codebase-context/&quot;&gt;Warp&apos;s Codebase Context docs&lt;/a&gt;. It is on &lt;a href=&quot;https://www.augmentcode.com/product/context-engine-mcp&quot;&gt;Augment Code&apos;s product pages&lt;/a&gt; as a feature called the Context Engine MCP. The phrase JetBrains coined in March to describe what AI agents produce without structural understanding, &quot;shadow tech debt,&quot; was the framing of their Junie CLI launch. And in Microsoft&apos;s &lt;a href=&quot;https://news.microsoft.com/source/features/ai/whats-next-in-ai-7-trends-to-watch-in-2026/&quot;&gt;December 2025 look-ahead to AI in 2026&lt;/a&gt;, GitHub&apos;s chief product officer Mario Rodriguez named the category by a different name, &quot;repository intelligence,&quot; and framed it as the new competitive advantage for AI-assisted software.&lt;/p&gt;
&lt;p&gt;The vocabulary has crossed a threshold. In roughly sixty days it has moved from practitioner discourse into vendor product surfaces, which is faster than most category terms travel. The phrase used to belong to people who were losing sleep over the problem. Now it also belongs to people selling against it.&lt;/p&gt;
&lt;p&gt;That distinction matters because vocabulary leads product. The language a vendor uses in docs encodes a promise about what the product delivers. When several vendors describe their offerings using the same phrase, the readers of those docs start to expect the phrase to mean something specific. The category forms in language before it forms in products. Once the phrase is in CPO interviews and product documentation, the window for defining what it means starts to close.&lt;/p&gt;
&lt;p&gt;This post is about the gap between the two. The vocabulary moved. The products moved, but mostly not in the direction the vocabulary points. The thing the language most directly describes, a queryable cross-repo dependency graph derived from source manifests, exists. It lives in &lt;a href=&quot;https://engineering.fb.com/2026/04/06/developer-tools/how-meta-used-ai-to-map-tribal-knowledge-in-large-scale-data-pipelines/&quot;&gt;Meta&apos;s tribal knowledge engine&lt;/a&gt;, in &lt;a href=&quot;https://www.mabl.com/blog/how-we-built-a-system-for-ai-agents-to-ship-real-code-across-75-repos&quot;&gt;Mabl&apos;s 850-line hand-maintained coordination graph&lt;/a&gt;, and in &lt;a href=&quot;https://riftmap.dev/&quot;&gt;Riftmap&lt;/a&gt;. It does not yet live in the product that most readers reach for when they read &quot;cross-repo context&quot; in their AI tool&apos;s docs.&lt;/p&gt;
&lt;h2&gt;What the products actually ship&lt;/h2&gt;
&lt;p&gt;The landscape past the vocabulary line is finer-grained than I thought before doing the research. Four distinct things sit under the same phrase. Each is real. Each answers a different question. Only one of them is the thing the language most directly promises, and that one is the one no consumer vendor has shipped.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The workspace pattern.&lt;/strong&gt; This is the largest cohort by vendor count. Warp, Cursor, Cline, Continue, Aider, Zed, Windsurf, GitHub Copilot, and Claude Code all sit here. They give the agent access to more files. Warp&apos;s docs are explicit: &lt;em&gt;&quot;During cross-repo tasks, Warp&apos;s Agents have access to the file paths of all indexed repos.&quot;&lt;/em&gt; Cursor&apos;s recommended pattern is the multi-root workspace; their community forum has open feature requests for cross-repo agent communication from &lt;a href=&quot;https://forum.cursor.com/t/working-on-multiple-repositories/146972&quot;&gt;December 2025&lt;/a&gt; and &lt;a href=&quot;https://forum.cursor.com/t/feature-request-cross-window-cross-repo-agent-communication/151603&quot;&gt;February 2026&lt;/a&gt;, both still open. GitHub Copilot&apos;s most relevant recent shipping is &lt;a href=&quot;https://github.com/orgs/community/discussions/173575&quot;&gt;picking the repository when assigning issues to Copilot&lt;/a&gt;, which lets an agent operate in a different repo per assignment. That is the workspace pattern at organisation scale. (For the three workarounds that give Copilot cross-repo context today — multi-root workspaces, Copilot Spaces, and MCP for the cloud agent — and where each one stops, see &lt;a href=&quot;https://riftmap.dev/blog/how-to-give-copilot-cross-repo-context/&quot;&gt;how to give Copilot cross-repo context today&lt;/a&gt;.) None of these is a dependency graph. None resolves an artifact to its consumers. The agent sees more files. It does not see how they relate.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The semantic retrieval index.&lt;/strong&gt; This is &lt;a href=&quot;https://www.augmentcode.com/product/context-engine-mcp&quot;&gt;Augment Code&apos;s Context Engine MCP&lt;/a&gt;. Augment indexes code across multiple repositories, runs the index locally, and exposes a retrieval interface (&lt;code&gt;codebase-retrieval&lt;/code&gt;, &lt;code&gt;codebase-search&lt;/code&gt;) that other agents can call via MCP. Augment publishes benchmark numbers showing real quality gains for downstream tools that wire it in. This is genuine substrate work. It answers questions of the form &lt;em&gt;&quot;find me code relevant to authentication&quot;&lt;/em&gt; very well. The retrieval is semantic, with embeddings over code chunks, which means the index is excellent for navigation and comprehension. It is not, by design, a parser-derived index of how a Terraform module gets consumed by which Helm chart and deployed by which CI pipeline.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The code-symbol graph.&lt;/strong&gt; This is &lt;a href=&quot;https://sourcegraph.com/docs/cody&quot;&gt;Sourcegraph Cody&lt;/a&gt;. Cody sits on a code intelligence graph that understands symbols, including function definitions, references, and call paths, and supports queries across multiple repositories. &lt;em&gt;&quot;Where is this function used?&quot;&lt;/em&gt; gets a precise answer that includes callers in repositories the user has not opened. This is closest to what the word &quot;graph&quot; implies, but it is a code-symbol graph, not an infrastructure-dependency graph. It is also enterprise-only since Sourcegraph discontinued the Free and Pro tiers in July 2025. Pricing starts at $59 per user per month, and the chat UI caps &lt;code&gt;@&lt;/code&gt;-mention selection at ten repositories per query. Real product, with the scope its product design implies: code symbols, not infrastructure manifests.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The JS/TS project graph.&lt;/strong&gt; &lt;a href=&quot;https://nx.dev/docs/concepts/synthetic-monorepos&quot;&gt;Nx Synthetic Monorepos&lt;/a&gt;, announced in their &lt;a href=&quot;https://nx.dev/blog/nx-2026-roadmap&quot;&gt;2026 roadmap&lt;/a&gt;, is the closest publicly-shipped equivalent to the architectural bet under Riftmap. Nx parses cross-repo dependencies into a workspace graph, runs a coordinator agent that walks the graph and spawns per-repo workers, and ships impact analysis and conformance rules across boundaries. This is the same shape of architecture. The ecosystem coverage is JavaScript and TypeScript first, with plugin-based extension to other languages, and the orientation is build orchestration and CI rather than infrastructure-as-code blast radius. Nx independently arriving at the same answer is, if anything, the strongest single piece of evidence that the substrate is the right architecture. Their product extends the JS/TS monorepo ecosystem across repository boundaries. It does not parse Dockerfile FROM lines, Terraform module sources, or Helm chart dependencies across twelve manifest ecosystems.&lt;/p&gt;
&lt;p&gt;Each of these does something real. None of them is the artifact the vocabulary most directly describes.&lt;/p&gt;
&lt;h2&gt;The layer that hasn&apos;t moved&lt;/h2&gt;
&lt;p&gt;The thing the vocabulary points to most directly is a parser-derived, queryable dependency graph across the manifests that govern deployment. That means Terraform module sources and registries, Dockerfile FROM statements across registries, Helm chart dependencies, Kubernetes manifest references, Kustomize overlays, ArgoCD application sources, Go modules, npm packages, Python requirements, Ansible role dependencies, GitLab CI includes, and GitHub Actions workflow calls. Twelve ecosystems, give or take, that together encode how an organisation&apos;s software actually runs in production.&lt;/p&gt;
&lt;p&gt;The question this layer answers is structurally distinct from the questions the four above answer. &quot;Where is this function called?&quot; lives in code symbols. &quot;Find me code relevant to authentication&quot; lives in semantic embeddings. &quot;Which packages need to rebuild?&quot; lives in the JS/TS workspace graph. &lt;em&gt;&quot;If I bump &lt;code&gt;python:3.11-slim&lt;/code&gt; to &lt;code&gt;python:3.12-slim&lt;/code&gt;, which services pull that base image, which Helm charts deploy those services, which CI pipelines need to retrigger, and which downstream Terraform modules reference the resulting images?&quot;&lt;/em&gt; lives in the infrastructure-manifest graph. None of the four above will give you that answer, because none of them parses those files in that way.&lt;/p&gt;
&lt;p&gt;Meta&apos;s &lt;a href=&quot;https://engineering.fb.com/2026/04/06/developer-tools/how-meta-used-ai-to-map-tribal-knowledge-in-large-scale-data-pipelines/&quot;&gt;tribal knowledge engine post&lt;/a&gt; named the cost of not having this layer at runtime. The &quot;what depends on X&quot; question costs roughly 6,000 tokens as a multi-file exploration and roughly 200 tokens as a single graph lookup. That is a thirty-times architecture-level efficiency gap on one of the most common planning questions an agent asks. The number is durable. It is not a model-quality problem and it does not close as context windows grow. It is the cost of reconstructing structure from grep every session instead of querying an index that already knows.&lt;/p&gt;
&lt;p&gt;Meta built that index in-house. &lt;a href=&quot;https://www.mabl.com/blog/how-we-built-a-system-for-ai-agents-to-ship-real-code-across-75-repos&quot;&gt;Mabl built theirs by hand&lt;/a&gt;, with 850 lines of registry maintained by a platform team across 79 repositories. &lt;a href=&quot;https://www.harness.io/blog/your-repo-is-a-knowledge-graph-you-just-dont-query-it-yet&quot;&gt;Harness named it&lt;/a&gt; in their April essay on Source Context Management, framing it as &lt;em&gt;&quot;the blast radius of every change before it merges.&quot;&lt;/em&gt; Harness sells CD, so the post is content marketing rather than competing product. The architectural conclusion these three teams reached independently, that the dependency graph should be parser-derived, queryable as a primitive, and treated as runtime infrastructure rather than per-session context, is the conclusion Riftmap shipped a productised version of. The substrate the vocabulary now describes in product docs is the same one Mabl, Meta, and Harness named in engineering blogs. It is the one that has not yet made its way into any consumer AI coding product.&lt;/p&gt;
&lt;h2&gt;The CPO and the feature request&lt;/h2&gt;
&lt;p&gt;The clearest signal of where this category is going might be the asymmetry between what GitHub says and what GitHub ships.&lt;/p&gt;
&lt;p&gt;In early December 2025, GitHub&apos;s chief product officer Mario Rodriguez named &quot;repository intelligence&quot; as the inflection point for 2026 in &lt;a href=&quot;https://news.microsoft.com/source/features/ai/whats-next-in-ai-7-trends-to-watch-in-2026/&quot;&gt;Microsoft&apos;s annual AI trends piece&lt;/a&gt;. The framing is direct: AI that understands code at the level of relationships and history rather than just lines of code. In Rodriguez&apos;s words, repository intelligence &quot;will become a competitive advantage by providing the structure and context for smarter, more reliable AI.&quot; The phrasing is good. The category is real.&lt;/p&gt;
&lt;p&gt;In March 2026, a GitHub user filed &lt;a href=&quot;https://github.com/orgs/community/discussions/189213&quot;&gt;Discussion #189213&lt;/a&gt;, titled &lt;em&gt;Feature Request: Cross-Repository Context for Copilot (Web ↔ Microservice)&lt;/em&gt;, describing the exact problem the CPO is naming. Web app consumes microservice; service API changes; both repos need coordinated updates; Copilot should be able to see across the boundary. Three upvotes. Two months later, the discussion is still marked Unanswered. It is one specific feature request; I do not want to read too much into a single thread. But the asymmetry is concrete. The CPO of the dominant code platform names the category. The same platform&apos;s flagship coding product has not yet answered the feature request that defines it. That is the clearest signal I have found that the language is moving faster than the products.&lt;/p&gt;
&lt;p&gt;Cursor&apos;s situation is similar in shape. The product is excellent within a single repo. The community forum has cross-repo feature requests from December 2025 and February 2026, both still open, both with the same pattern of users describing dependency relationships across repositories that the agent cannot see.&lt;/p&gt;
&lt;p&gt;This is not an indictment of either vendor. It is a description of where the leading edge of a category sits when the language has crossed into mainstream product surfaces but the substrate has not. Vendors who use the phrase get credit for the diagnosis. Vendors who ship the layer underneath get credit for the category. The two are different things and right now they are owned by different people.&lt;/p&gt;
&lt;p&gt;The window where that authorship gets decided is finite. Once a major vendor ships a parser-derived cross-repo dependency graph as a first-class feature, the category gets named after their product and everyone else becomes a comparison. That has not happened yet. It is the most interesting bet to be making over the next two quarters.&lt;/p&gt;
&lt;h2&gt;Where Riftmap sits&lt;/h2&gt;
&lt;p&gt;Riftmap is the productised version of the layer Meta, Mabl, and Harness have separately described. Twelve parser ecosystems today (Terraform, Dockerfile, Helm, Kubernetes, Kustomize, ArgoCD, Go, npm, Python, Ansible, GitLab CI, GitHub Actions), auto-discovered from a single read-only token, with more on the way. The roadmap extends the parser surface into contract layers (OpenAPI, protobuf, GraphQL) and schema registries, because the manifest plane and the contract plane are both part of the same dependency graph at runtime. No registry to maintain. No catalog YAML. The graph re-runs on every push; freshness is by construction rather than by process. Three endpoints (&lt;code&gt;/repositories/lookup&lt;/code&gt;, &lt;code&gt;/repositories/{id}/context&lt;/code&gt;, &lt;code&gt;/repositories/{id}/impact&lt;/code&gt;) are designed to be called by an agent during planning rather than read by a human in a dashboard.&lt;/p&gt;
&lt;p&gt;The position against the four above is not that they are wrong. Augment&apos;s semantic retrieval is excellent at what it does. Cody&apos;s code-symbol graph is excellent at what it does. Nx&apos;s synthetic monorepo is the right answer for organisations whose primary stack is JS/TS. The position is that none of them is the parser-derived infrastructure-manifest graph, because that is not what they were designed to be. The reader who needs that graph today either builds it like Mabl, runs it like Meta, or queries Riftmap.&lt;/p&gt;
&lt;h3&gt;The bear case&lt;/h3&gt;
&lt;p&gt;If a major model provider (Anthropic, OpenAI, GitHub) ships a platform-level MCP for cross-repo context as a standard feature, the API layer of this category gets commoditised fast. The defence against that scenario is the part that is hardest to copy: the breadth of the parser surface (twelve ecosystems is more work than it looks, particularly around private registries and transitive resolution), and the freshness contract (the index is parser-derived and re-runs on every push, which is structurally different from any context layer that depends on LLM summarisation). A platform MCP for cross-repo context would still need someone to actually parse the manifests. That work does not become easier because the agent has a new channel to call into.&lt;/p&gt;
&lt;h3&gt;The bull case&lt;/h3&gt;
&lt;p&gt;The bull case is the architectural choice underneath all of this, and it is the one I keep coming back to. Declarations are deterministic. A Terraform module source, a Dockerfile FROM statement, a Helm chart dependency, a GitHub Actions &lt;code&gt;uses:&lt;/code&gt; reference: these are file-based facts. A parser produces the same graph for the same input on every run. There is no temperature, no top-p, no retry-and-hope-it-converges. The output is binary. A dependency either exists in the graph or it does not.&lt;/p&gt;
&lt;p&gt;That has three consequences that compound over time, and they are why I think the deterministic-first architecture wins this category rather than loses it.&lt;/p&gt;
&lt;p&gt;The first is testability. Every parser case is a fixture. &lt;em&gt;&quot;Given this Dockerfile with this ARG default, the graph should contain edge X→Y.&quot;&lt;/em&gt; You can write that test, run it in a millisecond, and pin the behaviour for the lifetime of the codebase. You cannot write that test against an LLM-extracted dependency graph; the same input produces different outputs across runs, model versions, and prompt revisions. The deterministic foundation makes the system improvable in a way that probabilistic extraction is not. Every false positive becomes a permanent test case. Every edge case becomes a permanent regression check. Improvement compounds.&lt;/p&gt;
&lt;p&gt;The second is cost. A parser pass over a repository is microseconds and free. An LLM pass over the same repository is hundreds of milliseconds and bills per token. At organisation scale, with continuous re-scanning on every push, the cost gap is several orders of magnitude. The product that built its dependency layer on a deterministic foundation can offer it cheaply forever. The product that wired LLM extraction into the same loop pays a continuous tax that grows linearly with the size of the codebase and the frequency of change.&lt;/p&gt;
&lt;p&gt;The third is what sits on top. The graph being deterministic does not mean AI has no role. It means AI sits on top of it for the things AI is genuinely good at: ranking, summarising, explaining, planning, prioritising. The agent calls &lt;code&gt;/repositories/{id}/impact&lt;/code&gt; to get the deterministic blast radius, then uses the model to reason about which of the affected repos matter most, which changes are highest-risk, and how to sequence the work. The base layer is the part that needs to be correct. The reasoning layer is the part that needs to be flexible. The bet is that the right architecture for AI coding agents at scale is deterministic at the base and probabilistic at the top, not the other way around. I &lt;a href=&quot;https://riftmap.dev/blog/ai-doesnt-understand-blast-radius/&quot;&gt;argued this case in more detail in the blast radius post&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The thing I most want to be wrong about is the timing. If the window is wider than two quarters, the bet is easier. If it is narrower, the next post should already be live.&lt;/p&gt;
&lt;h2&gt;Closing&lt;/h2&gt;
&lt;p&gt;The vocabulary moved. The CPO of the largest code platform on the planet has named the category. Several vendors use the phrase in their product surfaces. JetBrains coined a phrase for what AI ships without it. The language now describes a thing more precisely than most of the products that use the language actually deliver.&lt;/p&gt;
&lt;p&gt;The layer the language describes is the parser-derived cross-repo dependency graph. It exists in Meta&apos;s in-house system, in Mabl&apos;s hand-maintained registry, and in Riftmap. It does not yet exist in the AI coding product that most readers of this post will reach for tomorrow. The window for that to change is open. The next two quarters decide who ends up owning what the language now promises.&lt;/p&gt;
&lt;p&gt;Riftmap is the bet that whoever ships the deterministic substrate during this window, while the language is still being formed and before a major vendor consolidates the category, is the one whose product the language ends up describing.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;If you&apos;re running AI coding agents across more than a handful of repositories and the language in their docs is starting to feel ahead of what they deliver, the layer underneath is the gap. You can build it yourself, the way Mabl did. Or you can &lt;a href=&quot;https://app.riftmap.dev&quot;&gt;start a free scan&lt;/a&gt; and let the parsers find it.&lt;/em&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Sources referenced&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Warp, &lt;em&gt;Codebase Context&lt;/em&gt; — &lt;a href=&quot;https://docs.warp.dev/agent-platform/capabilities/codebase-context/&quot;&gt;docs.warp.dev&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Susanna Ray, Microsoft, &lt;em&gt;What&apos;s next in AI: 7 trends to watch in 2026&lt;/em&gt; — &lt;a href=&quot;https://news.microsoft.com/source/features/ai/whats-next-in-ai-7-trends-to-watch-in-2026/&quot;&gt;news.microsoft.com&lt;/a&gt;, December 8, 2025 (Mario Rodriguez on repository intelligence, section 6)&lt;/li&gt;
&lt;li&gt;Augment Code, &lt;em&gt;Context Engine MCP&lt;/em&gt; — &lt;a href=&quot;https://www.augmentcode.com/product/context-engine-mcp&quot;&gt;augmentcode.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Sourcegraph, &lt;em&gt;Cody documentation&lt;/em&gt; — &lt;a href=&quot;https://sourcegraph.com/docs/cody&quot;&gt;sourcegraph.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Nx, &lt;em&gt;Synthetic Monorepos&lt;/em&gt; — &lt;a href=&quot;https://nx.dev/docs/concepts/synthetic-monorepos&quot;&gt;nx.dev&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Nx, &lt;em&gt;2026 Roadmap: Expanding Agent Autonomy&lt;/em&gt; — &lt;a href=&quot;https://nx.dev/blog/nx-2026-roadmap&quot;&gt;nx.dev/blog&lt;/a&gt;, February 4, 2026&lt;/li&gt;
&lt;li&gt;prateek-odev, &lt;em&gt;Feature Request: Cross-Repository Context for Copilot (Web ↔ Microservice)&lt;/em&gt; — &lt;a href=&quot;https://github.com/orgs/community/discussions/189213&quot;&gt;GitHub Discussion #189213&lt;/a&gt;, March 2026&lt;/li&gt;
&lt;li&gt;Cursor Community Forum, &lt;em&gt;Working on multiple repositories&lt;/em&gt; — &lt;a href=&quot;https://forum.cursor.com/t/working-on-multiple-repositories/146972&quot;&gt;forum.cursor.com&lt;/a&gt;, December 2025&lt;/li&gt;
&lt;li&gt;Cursor Community Forum, &lt;em&gt;Feature Request: Cross-Window / Cross-Repo Agent Communication&lt;/em&gt; — &lt;a href=&quot;https://forum.cursor.com/t/feature-request-cross-window-cross-repo-agent-communication/151603&quot;&gt;forum.cursor.com&lt;/a&gt;, February 2026&lt;/li&gt;
&lt;li&gt;Engineering at Meta, &lt;em&gt;How Meta used AI to map tribal knowledge in large-scale data pipelines&lt;/em&gt; — &lt;a href=&quot;https://engineering.fb.com/2026/04/06/developer-tools/how-meta-used-ai-to-map-tribal-knowledge-in-large-scale-data-pipelines/&quot;&gt;engineering.fb.com&lt;/a&gt;, April 6, 2026&lt;/li&gt;
&lt;li&gt;Geoff Cooney, mabl, &lt;em&gt;How We Built a System for AI Agents to Ship Real Code Across 75+ Repos&lt;/em&gt; — &lt;a href=&quot;https://www.mabl.com/blog/how-we-built-a-system-for-ai-agents-to-ship-real-code-across-75-repos&quot;&gt;mabl.com&lt;/a&gt;, April 8, 2026&lt;/li&gt;
&lt;li&gt;Ompragash Viswanathan, Harness, &lt;em&gt;Your Repo Is a Knowledge Graph. You Just Don&apos;t Query It Yet.&lt;/em&gt; — &lt;a href=&quot;https://www.harness.io/blog/your-repo-is-a-knowledge-graph-you-just-dont-query-it-yet&quot;&gt;harness.io/blog&lt;/a&gt;, April 1, 2026&lt;/li&gt;
&lt;li&gt;Riftmap, &lt;em&gt;AI Doesn&apos;t Understand Blast Radius: Why Change Failure Rates Are Up 30%&lt;/em&gt; — &lt;a href=&quot;https://riftmap.dev/blog/ai-doesnt-understand-blast-radius/&quot;&gt;riftmap.dev/blog&lt;/a&gt;, April 19, 2026&lt;/li&gt;
&lt;li&gt;Riftmap, &lt;em&gt;You don&apos;t need a virtual monorepo. You need a graph.&lt;/em&gt; — &lt;a href=&quot;https://riftmap.dev/blog/you-dont-need-a-virtual-monorepo/&quot;&gt;riftmap.dev/blog&lt;/a&gt;, May 12, 2026&lt;/li&gt;
&lt;li&gt;Riftmap, &lt;em&gt;AI coding agents need cross-repo context&lt;/em&gt; — &lt;a href=&quot;https://riftmap.dev/blog/ai-coding-agents-need-cross-repo-context/&quot;&gt;riftmap.dev/blog&lt;/a&gt;, May 12, 2026&lt;/li&gt;
&lt;li&gt;Riftmap, &lt;em&gt;Meta needed 50+ AI agents to map their tribal knowledge&lt;/em&gt; — &lt;a href=&quot;https://riftmap.dev/blog/meta-tribal-knowledge-engine-build-the-graph-first/&quot;&gt;riftmap.dev/blog&lt;/a&gt;, May 8, 2026&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Appendix: structured summary&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Claim:&lt;/strong&gt; Vocabulary describing cross-repo dependency intelligence has crossed into vendor product documentation, but the layer the vocabulary most directly describes, a parser-derived cross-repo dependency graph across infrastructure manifests, has not yet shipped in any consumer-facing AI coding product. Four distinct things now sit under the same phrase: the workspace pattern (Warp, Cursor, Cline, Continue, Aider, Zed, Windsurf, GitHub Copilot, Claude Code), semantic retrieval (Augment Code&apos;s Context Engine MCP), code-symbol graph (Sourcegraph Cody), and JS/TS project graph (Nx Synthetic Monorepos). None of the four covers the infrastructure-manifest layer where deployment-time blast radius lives.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Evidence:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Warp Codebase Context docs, Augment Context Engine MCP product pages, Sourcegraph Cody @-mention semantics, and Nx Synthetic Monorepos in their 2026 roadmap all use the cross-repo vocabulary explicitly.&lt;/li&gt;
&lt;li&gt;Mario Rodriguez, GitHub&apos;s chief product officer, named &quot;repository intelligence&quot; as the inflection point for 2026 AI in Microsoft&apos;s annual trends piece (December 2025).&lt;/li&gt;
&lt;li&gt;GitHub Copilot Discussion #189213 (Cross-Repository Context for Copilot, March 2026) remained Unanswered two months after filing.&lt;/li&gt;
&lt;li&gt;Cursor community forum has cross-repo feature requests open from December 2025 and February 2026.&lt;/li&gt;
&lt;li&gt;Meta&apos;s published number: graph lookup for &quot;what depends on X&quot; costs ~200 tokens; multi-file exploration costs ~6,000. A ~30x architectural efficiency gap.&lt;/li&gt;
&lt;li&gt;Mabl&apos;s 850-line Repo Coordination Graph spans 79+ repositories and is maintained by hand.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Architectural takeaway:&lt;/strong&gt; The vocabulary leads the products by approximately two quarters. The kind of substrate the language most directly describes, a parser-derived infrastructure-manifest dependency graph across Terraform, Docker, Helm, Kubernetes, CI manifests, and other ecosystems, exists only in in-house systems (Meta), hand-maintained registries (Mabl), and Riftmap. The window for owning the productised definition of &quot;cross-repo context&quot; closes when a major vendor ships that layer.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why deterministic-first wins:&lt;/strong&gt; Declarations are deterministic. A parser produces the same graph for the same input on every run, which makes the system testable as fixtures, cheap to operate at organisation scale, and reliable to wire into agent planning loops. AI sits on top of the graph for ranking, summarising, and prioritising. The base layer is the part that needs to be correct; the reasoning layer is the part that needs to be flexible. The right architecture for AI coding agents at scale is deterministic at the base and probabilistic at the top.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Audience:&lt;/strong&gt; Platform engineers, DevOps leads, and engineering managers running AI coding agents across multi-repo organisations, particularly those whose dependency graph crosses infrastructure-as-code boundaries.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Related reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://riftmap.dev/blog/ai-doesnt-understand-blast-radius/&quot;&gt;AI Doesn&apos;t Understand Blast Radius: Why Change Failure Rates Are Up 30%&lt;/a&gt; — the deterministic-first architecture argument in full, with the 2025–2026 data on AI-assisted change failure rates.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://riftmap.dev/blog/you-dont-need-a-virtual-monorepo/&quot;&gt;You don&apos;t need a virtual monorepo. You need a graph.&lt;/a&gt; — why the workspace pattern hits a maintenance ceiling between 50 and 100 repos, and what comes after it.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://riftmap.dev/blog/ai-coding-agents-need-cross-repo-context/&quot;&gt;AI coding agents need cross-repo context&lt;/a&gt; — three teams shipped the same diagnosis in two weeks. Two built the dependency graph layer. One built around it.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://riftmap.dev/blog/meta-tribal-knowledge-engine-build-the-graph-first/&quot;&gt;Meta needed 50+ AI agents to map their tribal knowledge&lt;/a&gt; — the durable layer in Meta&apos;s stack is the part you can build today.&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>cross-repo-context</category><category>ai-coding-agents</category><category>cross-repo-dependencies</category><category>dependency-graph</category><category>platform-engineering</category><category>blast-radius</category><category>polyrepo</category><category>repository-intelligence</category><author>Daniel Westgaard</author></item><item><title>What 208 kubernetes-sigs repos actually depend on</title><link>https://riftmap.dev/blog/what-208-kubernetes-sigs-repos-actually-depend-on/</link><guid isPermaLink="true">https://riftmap.dev/blog/what-208-kubernetes-sigs-repos-actually-depend-on/</guid><description>I scanned all 208 kubernetes-sigs repos with Riftmap. Here&apos;s the cross-repo dependency graph, including the 153 repos that import sigs.k8s.io/yaml.</description><pubDate>Sat, 16 May 2026 00:00:00 GMT</pubDate><content:encoded>&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;hr /&gt;
&lt;p&gt;If you&apos;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&apos;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=&quot;https://riftmap.dev/blog/what-56-prometheus-repos-depend-on/&quot;&gt;first scanned the Prometheus org&lt;/a&gt; with &lt;a href=&quot;https://riftmap.dev&quot;&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&apos;t see, which I&apos;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&apos;s not a flaw in the scan. It&apos;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=&quot;https://riftmap.dev/blog/what-242-cloudposse-repos-depend-on/&quot;&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;img src=&quot;https://riftmap.dev/images/blog/kubernetes-sigs/dashboard.png&quot; alt=&quot;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&quot; /&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&apos;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&apos;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&apos;d expect it at #4, but the three repos above it are utility libraries most contributors couldn&apos;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&apos;t that they&apos;re surprising. It&apos;s that the most-depended-on things in a 208-repo Kubernetes org are not the ones with public-facing names. That&apos;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;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;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;img src=&quot;https://riftmap.dev/images/blog/kubernetes-sigs/graph-default.png&quot; alt=&quot;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&quot; /&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&apos;s missing. Prometheus&apos;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&apos;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&apos;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&apos;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&apos;t also heavy producers. Most central repos don&apos;t pull in two dozen of their siblings. cluster-api does both.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://riftmap.dev/images/blog/kubernetes-sigs/focus-cluster-api.png&quot; alt=&quot;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&quot; /&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&apos;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&apos;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;img src=&quot;https://riftmap.dev/images/blog/kubernetes-sigs/sidebar-cluster-api.png&quot; alt=&quot;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&quot; /&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&apos;s not a Go module edge. It&apos;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&apos;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&apos;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&apos;s yaml import is tracked separately. &quot;We updated yaml in the root go.mod&quot; and &quot;we updated every reference to yaml&quot; remain different statements.&lt;/p&gt;
&lt;p&gt;One related quirk: cluster-addons&apos; &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&apos;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;img src=&quot;https://riftmap.dev/images/blog/kubernetes-sigs/impact-canvas.png&quot; alt=&quot;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&quot; /&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&apos;ve heard of, the four you haven&apos;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;img src=&quot;https://riftmap.dev/images/blog/kubernetes-sigs/impact-tab.png&quot; alt=&quot;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&quot; /&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&apos;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, &quot;show me the blast radius and let me hand the list to the team that owns each downstream repo,&quot; is the same workflow the Prometheus post described. The list is just longer here.&lt;/p&gt;
&lt;aside&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&apos;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;a href=&quot;https://riftmap.dev/showcase/prometheus/&quot;&gt;Explore the live graph →&lt;/a&gt;
&lt;/aside&gt;
&lt;h2&gt;What Riftmap didn&apos;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&apos;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&apos;s &lt;code&gt;module &quot;x&quot; { source = &quot;...&quot; }&lt;/code&gt; blocks. Riftmap detects produced Terraform roots and modules (6 and 4 respectively) but doesn&apos;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&apos;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=&quot;https://riftmap.dev/blog/auto-discovering-infrastructure-dependencies-across-10-ecosystems/&quot;&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&apos;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&apos;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&apos;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&apos;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=&quot;mailto:daniel@riftmap.dev&quot;&gt;daniel@riftmap.dev&lt;/a&gt;. I&apos;ll fix the parser and credit you in the follow-up.&lt;/p&gt;
&lt;p&gt;If you&apos;re running something similar in your own org and want to see what your graph looks like, Riftmap is at &lt;a href=&quot;https://riftmap.dev/&quot;&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;
</content:encoded><category>kubernetes</category><category>kubernetes-sigs</category><category>dependency-graph</category><category>cross-repo-dependencies</category><category>platform-engineering</category><category>open-source</category><author>Daniel Westgaard</author></item><item><title>You don&apos;t need a virtual monorepo. You need a graph.</title><link>https://riftmap.dev/blog/you-dont-need-a-virtual-monorepo/</link><guid isPermaLink="true">https://riftmap.dev/blog/you-dont-need-a-virtual-monorepo/</guid><description>Two patterns give AI coding agents cross-repo context. The virtual monorepo scales by hand and breaks past ~100 repos; a queryable dependency graph scales by construction.</description><pubDate>Tue, 12 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;In the past weeks, two engineers have published some of the most concrete writeups I&apos;ve seen of how to give AI coding agents context across more than one repository. Owen Zanzal&apos;s &lt;a href=&quot;https://medium.com/devops-ai/the-virtual-monorepo-pattern-how-i-gave-claude-code-full-system-context-across-35-repos-43b310c97db8&quot;&gt;The Virtual Monorepo Pattern&lt;/a&gt; (March 23, 2026) and Rafferty Uy&apos;s &lt;a href=&quot;https://raffertyuy.com/raztype/repo-of-repos-pattern/&quot;&gt;Repo-of-Repos&lt;/a&gt; (May 2, 2026). Both describe a similar shape of solution. Bundle your repos into a workspace folder. Hand the agent a system map. Ship.&lt;/p&gt;
&lt;p&gt;Both posts are good engineering. They&apos;re worth reading carefully. They also describe the strongest version of an approach that stops working at exactly the scale where the problem starts to matter, and the failure mode is the kind that&apos;s painful to retrofit.&lt;/p&gt;
&lt;p&gt;What follows is an argument with the pattern, not with the authors. Both posts are the kind of thing I want more of: specific, shipped, written from real use. The disagreement is about where the pattern&apos;s ceiling is, and what&apos;s worth building above it.&lt;/p&gt;
&lt;p&gt;This is the third post in a loose series. The &lt;a href=&quot;https://riftmap.dev/blog/meta-tribal-knowledge-engine-build-the-graph-first/&quot;&gt;first&lt;/a&gt; looked at Meta&apos;s tribal knowledge engine and the structural-vs-semantic decomposition hidden inside it. The &lt;a href=&quot;https://riftmap.dev/blog/ai-coding-agents-need-cross-repo-context/&quot;&gt;second&lt;/a&gt; walked through three teams converging on cross-repo context as runtime infrastructure. This one is about the alternative response, why I think it&apos;s a stopgap rather than a destination, and the asymmetric upgrade path that makes the difference matter.&lt;/p&gt;
&lt;h2&gt;What the workspace pattern gets right&lt;/h2&gt;
&lt;p&gt;Owen Zanzal&apos;s piece is the cleanest articulation. Three files in a workspace directory:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;.repos&lt;/code&gt;, a bash script that clones every relevant repo into a structured local folder.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CLAUDE.md&lt;/code&gt;, a hand-written system map describing how services relate to each other.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;README.md&lt;/code&gt;, a deeper narrative for &lt;em&gt;why&lt;/em&gt; the system is structured the way it is.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;He calls this the virtual monorepo pattern. The thesis line is the part worth taking seriously: &lt;em&gt;&quot;the problem isn&apos;t repo structure. The problem is context visibility. Your AI assistant doesn&apos;t need to live in a monorepo. It just needs to see one. You don&apos;t need a monorepo. You need a monorepo view.&quot;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;I think that&apos;s a correct assessment, and it&apos;s the right diagnosis for most teams in this position. They do not actually want to migrate to a monorepo — and as I argue in &lt;a href=&quot;https://riftmap.dev/blog/monorepo-vs-polyrepo/&quot;&gt;monorepo vs polyrepo: the debate is measuring the wrong thing&lt;/a&gt;, the repo layout was never really the variable that mattered anyway. The migration cost is real, the political friction across teams is real, and the CI/CD changes are real. The workspace pattern delivers a wider context window without paying any of those costs. It&apos;s purely additive. It exists alongside the existing repo structure. Nothing breaks.&lt;/p&gt;
&lt;p&gt;Rafferty Uy&apos;s &lt;code&gt;repo-of-repos&lt;/code&gt; pattern, published a week ago, is a more polished version of the same idea, named after the agent (Tony) it powers. An outer &quot;agent&quot; repo pulls in all related repos under a &lt;code&gt;repos/&lt;/code&gt; folder as workspace folders, while commits still flow back to each underlying repo&apos;s own origin. He ships it as a &lt;a href=&quot;https://github.com/raffertyuy/repo-of-repos&quot;&gt;GitHub template&lt;/a&gt;. He explicitly names three things that have changed in the last year that make the pattern work right now: context engineering is now a discipline, context windows got significantly larger, and &quot;agents grep before they answer.&quot;&lt;/p&gt;
&lt;p&gt;I want to single out the second of those because it&apos;s the load-bearing assumption of the entire pattern, and the rest of this post is going to push on it.&lt;/p&gt;
&lt;h2&gt;The load-bearing assumption&lt;/h2&gt;
&lt;p&gt;The workspace pattern works as long as two things hold simultaneously:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The bundled workspace fits in the agent&apos;s context window.&lt;/li&gt;
&lt;li&gt;Grepping over that workspace is cheap enough that the agent reaches for it routinely.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Both of those hold for Owen Zanzal at 35 repos. They hold for Rafferty Uy at the size he describes. They will hold for many readers of this post. However, I don&apos;t think they hold at 200 repos, and the failure isn&apos;t gradual.&lt;/p&gt;
&lt;p&gt;Frontier model context windows are around a million tokens at the upper end. A typical mid-sized service repository runs to several hundred thousand lines of code, much of it dependencies and tests. Bundle 200 of them and you don&apos;t fit. You don&apos;t fit in the next generation either, because organisations grow repos faster than frontier labs ship context length increases.&lt;/p&gt;
&lt;p&gt;Even when the workspace fits in the window, the workspace doesn&apos;t load itself into the prompt. Agents grep before they answer, in Rafferty&apos;s framing. Grep is O(N) over workspace size. Asking &quot;what depends on X&quot; by grepping 200 repos costs token, time, and latency that doesn&apos;t scale, and the answer is fuzzy at best. Meta&apos;s published number on this is the cleanest data point in the public record: a graph lookup answers &quot;what depends on X&quot; in around 200 tokens, the same question by exploration costs around 6,000. That&apos;s a 30x reduction. It isn&apos;t a frontier-model problem. It&apos;s an architecture problem.&lt;/p&gt;
&lt;p&gt;There&apos;s also a third issue, smaller in any single session but compounding over time. The &lt;code&gt;CLAUDE.md&lt;/code&gt; system map at the heart of the workspace pattern is a hand-written context file. The Gloaguen et al. paper from ETH Zurich and &lt;a href=&quot;http://LogicStar.ai&quot;&gt;LogicStar.ai&lt;/a&gt; (February 2026) studied this directly across four coding agents on 138 niche-repository tasks plus SWE-bench Lite. Their finding: developer-written context files give a marginal +4% improvement in agent success rate, at +19% inference cost. LLM-generated context files give a -3% effect at +20% cost. Across the board, &quot;context files do not provide effective overviews.&quot; Agents take the same number of steps to find relevant files whether a context file is present or not.&lt;/p&gt;
&lt;p&gt;That doesn&apos;t make &lt;code&gt;CLAUDE.md&lt;/code&gt; useless. The +4% case is real. But it caps the upside of the pattern, and it makes maintenance non-negotiable. Owen Zanzal himself names this cost explicitly: &lt;em&gt;&quot;Keeping &lt;code&gt;.repos&lt;/code&gt; in sync. New repos need to be added to the script. Repos that change names or move need to be updated. This is low-friction but not zero-friction.&quot;&lt;/em&gt; That&apos;s true at 35 repos. At 200, with five new repos a quarter, it is a part-time job nobody&apos;s job description includes.&lt;/p&gt;
&lt;h2&gt;How the pattern actually breaks&lt;/h2&gt;
&lt;p&gt;I want to be specific about how this breaks, because abstract failure modes don&apos;t land. Five concrete things go wrong as you scale.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;One.&lt;/strong&gt; The bundled workspace exceeds the context window. The agent silently truncates or the editor refuses to load the folder. You can mitigate this with multiple smaller workspaces scoped to different domains, which Owen Zanzal recommends as a workaround, but you&apos;ve now manually re-implemented domain partitioning that the dependency graph would have given you for free.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Two.&lt;/strong&gt; The agent grep over a 200-repo workspace becomes a 30x token tax on every &quot;what depends on X&quot; query. This isn&apos;t theoretical. Even Stripe, which has a real monorepo with hundreds of millions of lines of mostly Ruby code, doesn&apos;t dump everything into the agent&apos;s context. Their published architecture for the Minions agent system uses directory-scoped rule files that attach automatically as the agent traverses the filesystem, &lt;em&gt;&quot;rather than a single global context dump that would overflow any model&apos;s window.&quot;&lt;/em&gt; If Stripe at monorepo scale can&apos;t dump it all, neither can your virtual monorepo at 200 repos.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Three.&lt;/strong&gt; &lt;code&gt;CLAUDE.md&lt;/code&gt; decays. New service added, old service deprecated, schema changes, ownership boundaries shift. The system map drifts from reality. By the time someone notices, the agent has been confidently shipping changes against a stale model of the system. This is the failure mode Meta&apos;s engineering team named in their April 6 post on tribal knowledge: &lt;em&gt;&quot;context that decays is worse than no context.&quot;&lt;/em&gt; It&apos;s the reason their system uses a self-refreshing critic swarm. The workspace pattern has no equivalent, because nothing&apos;s running against it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Four.&lt;/strong&gt; Workspace gives the agent read access. It does not give the agent dependency awareness. The agent can grep through every repo in the workspace and still ship a change to the API repo that breaks the consumer it never opened that session. The cross-repo dependency graph is exactly the data structure that closes that gap, and grepping the workspace doesn&apos;t reconstruct it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Five.&lt;/strong&gt; You can&apos;t bolt the substrate on cheaply once the workspace pattern has settled in. Once teams are committing &lt;code&gt;CLAUDE.md&lt;/code&gt; updates, scripts depend on &lt;code&gt;.repos&lt;/code&gt; layout, and onboarding tells new engineers &quot;open the agent workspace,&quot; migrating to a queryable graph is a second project, not a refinement of the first. The substrate has to exist before you need it.&lt;/p&gt;
&lt;h2&gt;The point both authors gesture toward but don&apos;t reach&lt;/h2&gt;
&lt;p&gt;Here&apos;s what surprised me when I read Owen Zanzal&apos;s piece carefully. His final section is called &quot;Where This Pattern Can Go.&quot; It reads:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Auto-generating &lt;code&gt;CLAUDE.md&lt;/code&gt; from service metadata, API contracts, and event schemas, keeping the system map current as the system changes.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Dependency graphs derived from import analysis, Terraform dependency trees, or event topic mappings, and feeding those into the AI context automatically.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Architecture doc generation as a CI artifact, so the README stays in sync with the actual system rather than drifting toward fiction.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That is the substrate. He has named it precisely. He has written the bullet points for what comes next. The only thing he hasn&apos;t done is build it.&lt;/p&gt;
&lt;p&gt;This is the pattern with most workspace posts I&apos;ve read. The author solves the immediate problem at their current scale, lists &quot;auto-generated dependency graph&quot; as future work, and ships. Rafferty Uy&apos;s &lt;code&gt;repo-of-repos&lt;/code&gt; template assumes the agent grepping the workspace is the operating model and never asks at what scale grepping stops being affordable. Both posts are written for the scale where the workspace is the right level of investment. The substrate question opens up at a different scale, and neither author was writing at that scale.&lt;/p&gt;
&lt;p&gt;I think the better read is: the workspace pattern is the stopgap, and the substrate is what it grows into. Specifically, the auto-generated dependency graph bullet at the end of Owen Zanzal&apos;s post is doing the load-bearing work. Without it, the pattern hits the maintenance ceiling somewhere between 50 and 100 repos. With it, the maintenance ceiling moves from &quot;engineer with a weekend&quot; to &quot;platform that auto-discovers.&quot;&lt;/p&gt;
&lt;p&gt;That second thing is what &lt;a href=&quot;https://riftmap.dev/&quot;&gt;Riftmap&lt;/a&gt; exposes today. It&apos;s what Mabl built by hand into their 850-line Repo Coordination Graph in &lt;a href=&quot;https://www.mabl.com/blog/how-we-built-a-system-for-ai-agents-to-ship-real-code-across-75-repos&quot;&gt;Part 1&lt;/a&gt; and now extended to 100+ repositories with explicit blast-radius-for-tech-debt framing in &lt;a href=&quot;https://www.mabl.com/blog/how-we-built-a-system-for-ai-agents-to-ship-real-code-across-75-repos-part-2&quot;&gt;Part 2&lt;/a&gt;. It&apos;s the cross-repo dependency index Meta produced as a byproduct of their tribal knowledge engine. It&apos;s what Harness, the CD vendor, called the &quot;knowledge graph&quot; that lets you see &lt;em&gt;&quot;the blast radius of every change before it merges&quot;&lt;/em&gt; in their &lt;a href=&quot;https://www.harness.io/blog/your-repo-is-a-knowledge-graph-you-just-dont-query-it-yet&quot;&gt;April 1 essay&lt;/a&gt; on Source Context Management. Four independent teams have built it or named it as a primitive in the past six weeks. The workspace pattern is the thing you ship while you wait for that primitive to exist for your stack. If it already exists, the pattern is the alternative bet, not the better one.&lt;/p&gt;
&lt;h2&gt;Stripe is the proof&lt;/h2&gt;
&lt;p&gt;If I had to pick one piece of evidence that the workspace pattern alone doesn&apos;t carry you, it&apos;s Stripe.&lt;/p&gt;
&lt;p&gt;Stripe runs an internal coding agent system called Minions. As of February 2026, Minions produced over 1,300 merged pull requests per week, all written entirely by AI, all reviewed by humans. They run against Stripe&apos;s actual codebase, which is hundreds of millions of lines of mostly Ruby. Stripe is not 35 repos. They are the maximum case for what &quot;monorepo plus large model&quot; can do.&lt;/p&gt;
&lt;p&gt;And it isn&apos;t enough. Stripe built an MCP server called Toolshed that exposes nearly 500 internal tools to Minions. They built directory-scoped rule files instead of a global context dump because, in their own published words, the global dump &lt;em&gt;&quot;would overflow any model&apos;s window.&quot;&lt;/em&gt; They built pre-warmed devboxes, deterministic verification gates, and a custom fork of Block&apos;s Goose agent. Signadot&apos;s piece &lt;a href=&quot;https://thenewstack.io/coding-agents-feedback-signals/&quot;&gt;Coding Agents Are Only as Good as the Signals You Feed Them&lt;/a&gt; puts it cleanly: &lt;em&gt;&quot;Stripe did not achieve this volume simply by pointing a large language model at its monorepo.&quot;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;If the team with the world&apos;s most demanding monorepo can&apos;t simply bundle everything and let the agent figure it out, the bet that a virtual monorepo will solve it for the rest of us might be structurally unsound. The substrate is what you build because the workspace alone, even when the workspace is real, isn&apos;t where the leverage lives.&lt;/p&gt;
&lt;h2&gt;The asymmetric upgrade path&lt;/h2&gt;
&lt;p&gt;Here&apos;s how I think about the buy/build decision for teams in this range.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;At 30 to 50 repos with one engineer who has a weekend.&lt;/strong&gt; The workspace pattern is genuinely fine. Build it. Owen Zanzal&apos;s three files will get you most of the way. Rafferty Uy&apos;s &lt;code&gt;repo-of-repos&lt;/code&gt; template will get you the rest. Don&apos;t overthink it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;At 100+ repos and growing.&lt;/strong&gt; The workspace pattern is a maintenance bet you&apos;re going to lose. &lt;code&gt;CLAUDE.md&lt;/code&gt; will decay, &lt;code&gt;.repos&lt;/code&gt; will drift, and the agent will gain false confidence in stale information. The fix is structural, not procedural. The dependency graph derived from source code, refreshed on every push, is what closes the gap, and you want it before you need it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;At any scale, if the substrate already exists for your stack.&lt;/strong&gt; The workspace is the alternative bet, not the better one. If &lt;a href=&quot;https://app.riftmap.dev/&quot;&gt;Riftmap&lt;/a&gt; or something like it can already auto-discover your dependency graph from a read-only org token, the workspace pattern is solving by hand a problem you don&apos;t have to solve manually.&lt;/p&gt;
&lt;p&gt;The asymmetry that matters is on the upgrade path. Going from no-workspace to workspace is a weekend. Going from workspace-with-decayed-&lt;code&gt;CLAUDE.md&lt;/code&gt;-and-stale-dependency-bullets-in-prose to a queryable graph is a project. The migration tax goes up the longer you wait. &quot;We&apos;ll do the graph later&quot; is how teams end up with a context file they don&apos;t trust and a dependency model they have to recompute by reading.&lt;/p&gt;
&lt;h2&gt;Closing&lt;/h2&gt;
&lt;p&gt;Owen Zanzal&apos;s deeper principle from the closing of his post is &lt;em&gt;&quot;context beats structure for AI effectiveness.&quot;&lt;/em&gt; That&apos;s a good line and I agree with it as far as it goes. The structural codebase organisation isn&apos;t what determines whether an agent ships safe code; the context the agent has is.&lt;/p&gt;
&lt;p&gt;The version of that principle I&apos;d push for is one step further: structure beats context that has to be rewritten by hand. The kind of context that wins is the kind that updates when the code does, by construction, on every push. The workspace pattern delivers context. The substrate delivers context that doesn&apos;t decay.&lt;/p&gt;
&lt;p&gt;I believe if you&apos;re going to invest in either, invest in the one that doesn&apos;t need a maintainer.&lt;/p&gt;
&lt;p&gt;The architectural bet hasn&apos;t changed. Deterministic parsers first. Graph as the durable layer. AI as the layer on top, anchored to verified structure rather than reconstructing it from grep across a workspace that someone has to remember to update.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;If you&apos;re running AI coding agents across more than a handful of repositories and finding yourself maintaining a &lt;code&gt;CLAUDE.md&lt;/code&gt; that&apos;s drifting from reality, that&apos;s the gap. You can build the substrate yourself, the way Mabl did. Or you can &lt;a href=&quot;https://app.riftmap.dev/&quot;&gt;start a free scan&lt;/a&gt; and let the parsers find it.&lt;/em&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Sources referenced&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Owen Zanzal, &lt;em&gt;The &quot;Virtual Monorepo&quot; Pattern: How I Gave Claude Code Full-System Context Across 35 Repos&lt;/em&gt; — &lt;a href=&quot;https://medium.com/devops-ai/the-virtual-monorepo-pattern-how-i-gave-claude-code-full-system-context-across-35-repos-43b310c97db8&quot;&gt;medium.com/devops-ai&lt;/a&gt;, March 23, 2026&lt;/li&gt;
&lt;li&gt;Rafferty Uy, &lt;em&gt;Repo-of-Repos: Tony&apos;s Multi-Repo Workspace for AI Coding Agents&lt;/em&gt; — &lt;a href=&quot;https://raffertyuy.com/raztype/repo-of-repos-pattern/&quot;&gt;raffertyuy.com&lt;/a&gt;, May 2, 2026&lt;/li&gt;
&lt;li&gt;Ompragash Viswanathan, Harness, &lt;em&gt;Your Repo Is a Knowledge Graph. You Just Don&apos;t Query It Yet.&lt;/em&gt; — &lt;a href=&quot;https://www.harness.io/blog/your-repo-is-a-knowledge-graph-you-just-dont-query-it-yet&quot;&gt;harness.io/blog&lt;/a&gt;, April 1, 2026&lt;/li&gt;
&lt;li&gt;Geoff Cooney, mabl, &lt;em&gt;How We Built a System for AI Agents to Ship Real Code Across 75+ Repos&lt;/em&gt; (Part 1 of 2) — &lt;a href=&quot;https://www.mabl.com/blog/how-we-built-a-system-for-ai-agents-to-ship-real-code-across-75-repos&quot;&gt;mabl.com&lt;/a&gt;, April 8, 2026&lt;/li&gt;
&lt;li&gt;mabl, &lt;em&gt;How We Built a System for AI Agents to Ship Real Code Across 75+ Repos&lt;/em&gt; (Part 2 of 2) — &lt;a href=&quot;https://www.mabl.com/blog/how-we-built-a-system-for-ai-agents-to-ship-real-code-across-75-repos-part-2&quot;&gt;mabl.com&lt;/a&gt;, April 28, 2026&lt;/li&gt;
&lt;li&gt;Engineering at Meta, &lt;em&gt;How Meta used AI to map tribal knowledge in large-scale data pipelines&lt;/em&gt; — &lt;a href=&quot;https://engineering.fb.com/2026/04/06/developer-tools/how-meta-used-ai-to-map-tribal-knowledge-in-large-scale-data-pipelines/&quot;&gt;engineering.fb.com&lt;/a&gt;, April 6, 2026&lt;/li&gt;
&lt;li&gt;Gloaguen et al., ETH Zurich and &lt;a href=&quot;http://LogicStar.ai&quot;&gt;LogicStar.ai&lt;/a&gt;, &lt;em&gt;Do Context Files Help Coding Agents?&lt;/em&gt; — &lt;a href=&quot;https://arxiv.org/abs/2602.11988&quot;&gt;arxiv.org/abs/2602.11988&lt;/a&gt;, February 2026&lt;/li&gt;
&lt;li&gt;Signadot, &lt;em&gt;Coding Agents Are Only as Good as the Signals You Feed Them&lt;/em&gt; — &lt;a href=&quot;https://thenewstack.io/coding-agents-feedback-signals/&quot;&gt;thenewstack.io&lt;/a&gt;, April 2026&lt;/li&gt;
&lt;li&gt;Riftmap, &lt;em&gt;AI coding agents need cross-repo context&lt;/em&gt; — &lt;a href=&quot;https://riftmap.dev/blog/ai-coding-agents-need-cross-repo-context/&quot;&gt;riftmap.dev/blog&lt;/a&gt;, May 12, 2026&lt;/li&gt;
&lt;li&gt;Riftmap, &lt;em&gt;Meta needed 50+ AI agents to map their tribal knowledge&lt;/em&gt; — &lt;a href=&quot;https://riftmap.dev/blog/meta-tribal-knowledge-engine-build-the-graph-first/&quot;&gt;riftmap.dev/blog&lt;/a&gt;, May 8, 2026&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Appendix: structured summary&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Claim:&lt;/strong&gt; Two patterns have emerged for giving AI coding agents context across multiple repositories. The workspace pattern (Owen Zanzal, Rafferty Uy) bundles repos into a local workspace and hands the agent a hand-written system map. The substrate pattern (Mabl, Meta, Harness, Riftmap) builds a queryable cross-repo dependency graph derived from source code. Both work at small scale. Only the substrate survives the transition past ~100 repos, because the workspace pattern&apos;s load-bearing assumptions (workspace fits the context window, grep is cheap, hand-written context stays current) all break at the same point.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Evidence:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Frontier model context windows top out around 1M tokens; 200 mid-sized service repos do not fit, and the next generation of models will not close the gap because organisations add repos faster than context lengths grow.&lt;/li&gt;
&lt;li&gt;Meta&apos;s published data: graph lookup for &quot;what depends on X&quot; costs ~200 tokens; the same answered by exploration costs ~6,000. A 30x architecture-not-model gap.&lt;/li&gt;
&lt;li&gt;Gloaguen et al. (arXiv:2602.11988): hand-written context files give +4% agent success rate at +19% cost. LLM-generated context files give -3% at +20% cost. Context files do not provide effective overviews.&lt;/li&gt;
&lt;li&gt;Stripe Minions (1,300+ merged PRs/week, hundreds of millions of LOC): even with a real monorepo, Stripe uses directory-scoped rule files because a global context dump &lt;em&gt;&quot;would overflow any model&apos;s window.&quot;&lt;/em&gt; The Toolshed MCP server exposes ~500 tools to agents.&lt;/li&gt;
&lt;li&gt;Harness, mabl, Meta, Riftmap (and adjacent: Augment, Depwire, Modulus) have all named the queryable dependency graph as a substrate primitive in the past six weeks.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Architectural takeaway:&lt;/strong&gt; The workspace pattern is a stopgap with a known maintenance ceiling between 50 and 100 repos. The substrate is what it grows into. Owen Zanzal&apos;s &quot;Where This Pattern Can Go&quot; section names auto-generated dependency graphs from import analysis, Terraform trees, and event topic mappings as the next step; the substrate is exactly that, exposed as a queryable graph rather than as bullets in prose. The migration tax goes up the longer you wait, so substrate-first is the correct call for any team above 100 repos and any team that expects to be there within a year.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Audience:&lt;/strong&gt; Platform engineers, DevOps leads, and engineering managers running AI coding agents across more than a handful of repositories, especially those weighing whether to invest in a virtual-monorepo workspace or in a queryable dependency substrate.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Related reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://riftmap.dev/blog/meta-tribal-knowledge-engine-build-the-graph-first/&quot;&gt;Meta needed 50+ AI agents to map their tribal knowledge&lt;/a&gt; — the durable piece of Meta&apos;s stack is the part you can build today.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://riftmap.dev/blog/ai-coding-agents-need-cross-repo-context/&quot;&gt;AI coding agents need cross-repo context&lt;/a&gt; — three teams shipped the same diagnosis in two weeks. Two built the dependency graph substrate. One built around it.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://riftmap.dev/blog/ai-doesnt-understand-blast-radius/&quot;&gt;AI Doesn&apos;t Understand Blast Radius&lt;/a&gt; — why AI coding tools optimise for local correctness while production breaks at the cross-repo graph they can&apos;t see.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://riftmap.dev/blog/cursor-monorepo-indexing/&quot;&gt;You can index every repo in Cursor. It still can&apos;t tell you what breaks.&lt;/a&gt; — the Cursor-specific version of this ceiling: a tuned index spans your repos but can&apos;t map the edges between them.&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>ai-coding-agents</category><category>cross-repo-context</category><category>virtual-monorepo</category><category>platform-engineering</category><category>polyrepo</category><category>cross-repo-dependencies</category><category>blast-radius</category><author>Daniel Westgaard</author></item><item><title>AI coding agents need cross-repo context. The teams running them at scale are already building it themselves.</title><link>https://riftmap.dev/blog/ai-coding-agents-need-cross-repo-context/</link><guid isPermaLink="true">https://riftmap.dev/blog/ai-coding-agents-need-cross-repo-context/</guid><description>Three teams shipped the same diagnosis in two weeks: AI coding agents need cross-repo context. Two built the dependency graph substrate. One built around it.</description><pubDate>Sun, 10 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Three weeks ago, the Cortex 2026 Engineering in the Age of AI Benchmark put incidents per pull request up 23.5% and change failure rates up roughly 30% since AI adoption accelerated. I &lt;a href=&quot;https://riftmap.dev/blog/ai-doesnt-understand-blast-radius/&quot;&gt;wrote about that data and what it means for blast radius&lt;/a&gt; shortly after it landed.&lt;/p&gt;
&lt;p&gt;What I underestimated at the time was how fast the language was going to shift. The phrase that&apos;s settled into the conversation since isn&apos;t &quot;blast radius&quot; or &quot;service catalog.&quot; It&apos;s &lt;a href=&quot;https://riftmap.dev/what-is-cross-repo-dependency-mapping/&quot;&gt;&lt;em&gt;cross-repo context&lt;/em&gt;&lt;/a&gt;. And it&apos;s almost always being used in the same sentence as &quot;AI coding agents.&quot; The reason becomes obvious once you read what teams operating AI coding agents at scale are publishing right now. They&apos;re not writing about AI making them faster. They&apos;re writing about the infrastructure they had to build so AI could ship safely across more than one repo at a time.&lt;/p&gt;
&lt;p&gt;Three teams have published, in the last six weeks, the same diagnosis with three different solutions. The shape of what they&apos;re converging on is more interesting than any single one of them, and it has direct implications for how the rest of us should be thinking about agent runtime infrastructure.&lt;/p&gt;
&lt;h2&gt;What three teams just shipped&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Neilos (@neil_agentic on &lt;a href=&quot;http://dev.to&quot;&gt;dev.to&lt;/a&gt;), March 27.&lt;/strong&gt; A solo founder running 15+ repositories across Go, Rust, TypeScript, Python, and C++, coordinating ten specialised Claude Code agents through Telegram. His post is &lt;a href=&quot;https://dev.to/neil_agentic/how-i-manage-15-repos-with-claude-code-without-losing-my-mind-2ood&quot;&gt;How I Manage 15+ Repos with Claude Code (Without Losing My Mind)&lt;/a&gt;. The diagnostic line is direct: you end up &quot;copy-pasting context between sessions, manually tracking which PR depends on which, and babysitting agents that can&apos;t see the full picture.&quot;&lt;/p&gt;
&lt;p&gt;His solution, called &lt;code&gt;ttal&lt;/code&gt;, separates &quot;read and explore&quot; from &quot;write.&quot; A lightweight exploration agent can read across any repository to gather context. Worker agents only ever write to one repository at a time, in isolated git worktrees. A manager agent holds the cross-repo plan in its head and routes work to the right repo at the right time. There&apos;s no dependency graph as a data structure. The cross-repo coordination lives in the manager agent&apos;s reasoning and a project registry that maps names to file paths.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Mabl, April 8.&lt;/strong&gt; A team of 25 engineers managing 100+ repositories, pushing 200+ pull requests and 40+ production releases a month before AI acceleration. By February 2026, 39% of their commits were AI-assisted, 60% in infrastructure repos. Their post, &lt;a href=&quot;https://www.mabl.com/blog/how-we-built-a-system-for-ai-agents-to-ship-real-code-across-75-repos&quot;&gt;How We Built a System for AI Agents to Ship Real Code Across 75+ Repos&lt;/a&gt;, describes a four-layer architecture. The layer that matters here is what they call &quot;Cross Repo Base.&quot;&lt;/p&gt;
&lt;p&gt;Inside Cross Repo Base sits a &quot;Repo Coordination Graph&quot;: 850+ lines of structured registry covering 79 repositories with detailed dependency graphs, Pub/Sub topic maps, database table ownership, and prescribed release ordering. When an agent works on a cross-repo feature, it queries this graph to determine which repos need updates, what the merge order should be (upstream libraries before consumers), and which reviewers to tag based on CODEOWNERS and dependency chains. They report that without this layer, &quot;every agent invocation required a human to provide context.&quot; With it, &quot;context drift dropped from ~40% of our failures to &amp;lt;5%.&quot;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Meta, April 6.&lt;/strong&gt; I wrote about &lt;a href=&quot;https://riftmap.dev/blog/meta-tribal-knowledge-engine-build-the-graph-first/&quot;&gt;the Meta tribal knowledge engine post&lt;/a&gt; earlier this week, so I&apos;ll keep this short. Meta built a multi-stage AI pipeline with 50+ specialised agents to map tribal knowledge across one of their large data pipelines. Buried near the end is the line that matters: &quot;we generated a cross-repo dependency index and data flow maps showing how changes propagate across repositories. This turns &apos;What depends on X?&apos; from a multi-file exploration (~6000 tokens) into a single graph lookup (~200 tokens).&quot;&lt;/p&gt;
&lt;p&gt;A 30x token reduction for one of the most common questions an agent asks during planning.&lt;/p&gt;
&lt;h2&gt;What&apos;s the same, what&apos;s different&lt;/h2&gt;
&lt;p&gt;The pain is identical. All three describe agents shipping locally-correct code that breaks consumers the agent didn&apos;t know existed. Mabl frames it as context drift. Meta frames it as tribal knowledge. Neilos frames it as agents who &quot;can&apos;t see the full picture.&quot; The phenomenon is one phenomenon: the dependency graph that determines whether a change is safe lives outside any single repository&apos;s boundary, which means it lives outside the agent&apos;s context window.&lt;/p&gt;
&lt;p&gt;The solutions diverge in a way that&apos;s instructive.&lt;/p&gt;
&lt;p&gt;Mabl and Meta both built dependency graphs as a queryable substrate. Mabl&apos;s lives as a structured 850-line registry that agents consult at planning time. Meta&apos;s lives as a parser-derived graph index that agents call as a tool. The shapes are slightly different but the operating model is the same. Deterministic structure. Refreshed automatically (Mabl from registry updates and CODEOWNERS, Meta from re-parsing source). Queryable as a single lookup rather than a multi-file exploration. The graph exists separately from the agents and outlasts any single agent session.&lt;/p&gt;
&lt;p&gt;Neil&apos;s &lt;code&gt;ttal&lt;/code&gt; takes a different approach. The cross-repo information lives in the manager agent&apos;s working memory plus a TOML file mapping project names to file system paths, with the architecture isolating writes to one repo at a time and keeping the coordination model in the manager&apos;s prompt. This is well-fitted to a solo operator with ten agents and fifteen repositories. At Mabl&apos;s scale, the same problem reshapes the solution. Once you have 25 engineers across 79 repos, no single agent&apos;s context window is big enough to hold the coordination model and no manual registry stays current at that throughput, so the dependency model has to live outside any agent.&lt;/p&gt;
&lt;p&gt;What I read into this is that the dependency graph as a queryable substrate isn&apos;t a stylistic choice. It&apos;s what falls out of the problem when you scale it. Three teams hit the same wall. Two engineered the substrate explicitly. One handled it through coordination, which is the right call at solo scale. Once the team running the agents grows past that, the substrate is what you build.&lt;/p&gt;
&lt;h2&gt;Why this should be a primitive, not something every team rebuilds&lt;/h2&gt;
&lt;p&gt;The Mabl post lists this layer almost matter-of-factly. The Meta post mentions it in one paragraph after sections on agent swarms and critic passes. Both teams treated it as table stakes for what they were actually trying to do. And in both cases, the structural layer is the cheapest, most reusable, most durable part of what they built. Mabl&apos;s &lt;a href=&quot;http://CLAUDE.md&quot;&gt;CLAUDE.md&lt;/a&gt; per repo decays without a maintainer. Meta&apos;s LLM-generated context files decay without a self-refreshing critic swarm. The dependency graph doesn&apos;t decay, because parsers run on every push.&lt;/p&gt;
&lt;p&gt;Which raises the question for every team that isn&apos;t Meta or Mabl: do you really want to be writing your own 850-line &lt;code&gt;repo-coordination-graph.md&lt;/code&gt; by hand? And keeping it current as repos are added, archived, renamed, and refactored? At a 50-engineer org, this is a fully-loaded engineer-month of upfront work and an ongoing maintenance tax that nobody&apos;s job description includes. At a 200-engineer org, it&apos;s a dedicated platform team responsibility competing with everything else they could be doing.&lt;/p&gt;
&lt;p&gt;This primitive belongs above any single team. The graph itself is structural, derived from source code, and identical in shape across orgs. The thing you actually want from it (which repositories import from which, who consumes which artifact, what&apos;s in the transitive blast radius of a change) is the same question whether you have 50 repos or 500. There is no good reason for every multi-repo org running AI coding agents to be writing their own version of this from scratch. It&apos;s a substrate, not a feature.&lt;/p&gt;
&lt;p&gt;The vendors have since started shipping exactly this shape. &lt;a href=&quot;https://riftmap.dev/blog/gitlab-orbit-and-the-artifact-layer/&quot;&gt;GitLab Orbit&lt;/a&gt;, launched in June, is a queryable property graph of code symbols and SDLC objects that agents consult over MCP, which is the substrate argument made by the largest player in DevOps. What it indexes, and the infrastructure artifact layer it leaves unmapped, got its own post.&lt;/p&gt;
&lt;h2&gt;What this looks like as an API&lt;/h2&gt;
&lt;p&gt;Riftmap auto-discovers the cross-repo dependency graph from a GitHub or GitLab org via a single read-only token, parsing manifests across twelve ecosystems: Terraform, Docker, Helm, Kubernetes, Kustomize, ArgoCD, Go, npm, Python, Ansible, GitHub Actions, and GitLab CI (and more to be added in the future). The graph is queryable through an HTTP API designed to be called by AI agents during planning.&lt;/p&gt;
&lt;p&gt;The natural agent flow is three calls. Given an agent working in a clone of &lt;code&gt;github.com/myorg/payments-api&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 1. Resolve the local clone URL to a Riftmap repo
curl -s &quot;$RIFTMAP_BASE_URL/repositories/lookup?url=https://github.com/myorg/payments-api&quot; \
  -H &quot;X-API-Key: $RIFTMAP_API_KEY&quot;

# 2. Hydrate context in one round-trip:
#    repo + dependencies + dependents + artifacts + freshness fields
curl -s &quot;$RIFTMAP_BASE_URL/repositories/$REPO_ID/context&quot; \
  -H &quot;X-API-Key: $RIFTMAP_API_KEY&quot;

# 3. Only when actually needed: transitive blast radius
curl -s &quot;$RIFTMAP_BASE_URL/repositories/$REPO_ID/impact?max_depth=3&amp;amp;min_confidence=0.8&quot; \
  -H &quot;X-API-Key: $RIFTMAP_API_KEY&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Three calls to answer the questions Mabl&apos;s Repo Coordination Graph and Meta&apos;s dependency index were built to answer. The full schema is published at &lt;code&gt;https://app.riftmap.dev/openapi.json&lt;/code&gt;. Fetch it once, hand it to your agent, and the rest is typed &lt;code&gt;httpx&lt;/code&gt; or &lt;code&gt;fetch&lt;/code&gt; calls. Python and TypeScript versions of these examples are in the &lt;a href=&quot;https://docs.riftmap.dev/agents/examples&quot;&gt;agent integration docs&lt;/a&gt;, along with the freshness contract every response carries (&lt;code&gt;last_scanned_at&lt;/code&gt;, &lt;code&gt;last_commit_sha&lt;/code&gt;, &lt;code&gt;last_activity_at&lt;/code&gt;, &lt;code&gt;archived&lt;/code&gt;) so an agent can tell when the graph is stale and either warn the user or trigger a rescan.&lt;/p&gt;
&lt;p&gt;This is the substrate today, exposed as an HTTP API. An MCP server and CLI are next, in that order, so any Claude Code, Cursor, or Cline agent can drop them in without orchestrating HTTP calls. The order and current state are tracked on the &lt;a href=&quot;https://docs.riftmap.dev/agents/mcp-cli-roadmap&quot;&gt;public roadmap page&lt;/a&gt;.&lt;/p&gt;
&lt;aside&gt;
&lt;p&gt;&lt;span&gt;&lt;/span&gt;See it live&lt;/p&gt;
&lt;p&gt;What that graph actually looks like&lt;/p&gt;
&lt;p&gt;The same cross-repo substrate, rendered for humans instead of agents: a live scan of the Prometheus org. Click any repo to see its dependents, its artifacts, and the transitive blast radius of changing it — no token required.&lt;/p&gt;
&lt;a href=&quot;https://riftmap.dev/showcase/prometheus/&quot;&gt;Explore the live graph →&lt;/a&gt;
&lt;/aside&gt;
&lt;h2&gt;The interface outlasts the model&lt;/h2&gt;
&lt;p&gt;The reasonable objection at this point is: won&apos;t models eventually get good enough at cross-repo reasoning that none of this is necessary? Won&apos;t Claude 5 or whatever comes next just figure it out?&lt;/p&gt;
&lt;p&gt;I don&apos;t think the question goes the way the objection assumes. Even granting that models keep getting better at cross-repo reasoning, three things stay true.&lt;/p&gt;
&lt;p&gt;First, parsing the dependency manifests of a multi-repo organisation from inside a model&apos;s context window is doing a job a parser could do deterministically and cheaply. Even if a frontier model does it perfectly, it&apos;s still paying token cost on every invocation for what is structurally a precomputed lookup. Meta&apos;s 30x token reduction isn&apos;t a model-quality problem. It&apos;s an architecture problem. A graph index will always be cheaper to query than to reconstruct.&lt;/p&gt;
&lt;p&gt;Second, models change every six months. The dependency graph doesn&apos;t. A queryable graph behind a stable API has the same shape whether the agent calling it is Claude 4.7, Claude 5, or whatever is running in 2027. The interface outlasts the model. Investing in the substrate is on a different time horizon to investing in any specific agent&apos;s context strategy.&lt;/p&gt;
&lt;p&gt;Third, the freshness problem is structural. A model trained today does not know about repositories your team created last week. A graph derived from last night&apos;s scan does. As long as the rate of change in your repository graph exceeds the rate at which models retrain, that gap exists, and the substrate is how you close it.&lt;/p&gt;
&lt;p&gt;What models getting better at cross-repo reasoning probably does change is what gets layered on top of the graph. Better semantic reasoning over verified structure. Better automated PR review against known consumer impact. Better natural-language interfaces to the graph itself. Those are real things, and they&apos;ll matter. They all sit above the substrate, not in place of it.&lt;/p&gt;
&lt;h2&gt;Closing&lt;/h2&gt;
&lt;p&gt;The three posts I started this with are the same shape of evidence I built &lt;a href=&quot;https://riftmap.dev/&quot;&gt;Riftmap&lt;/a&gt;, just with bigger names attached. Engineers willing to spend real engineering time building a thing means the thing is needed. When three independent teams publish the same diagnosis in two weeks, the pattern is the pattern.&lt;/p&gt;
&lt;p&gt;The architectural bet hasn&apos;t changed. Deterministic parsers first. Graph as the durable layer. AI as the layer on top, anchored to verified structure rather than reconstructing it from scratch. What&apos;s changed is who&apos;s confirming it publicly, and how much louder the confirmation has gotten.&lt;/p&gt;
&lt;p&gt;If you&apos;re running AI coding agents across more than five or six repositories and finding yourself writing prompts that include cross-repo dependency information by hand, or reviewing PRs where the agent shipped a change that broke a consumer it didn&apos;t know existed, that&apos;s the gap. You can build the substrate yourself the way Mabl did, on the trajectory the Mabl post describes. Or you can &lt;a href=&quot;https://app.riftmap.dev&quot;&gt;start a free scan&lt;/a&gt; and skip the 850-line registry. Either way, the substrate is the substrate. It&apos;s the part that doesn&apos;t lie.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Sources referenced&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Geoff Cooney, mabl, &lt;em&gt;How We Built a System for AI Agents to Ship Real Code Across 75+ Repos [Part 1 of 2]&lt;/em&gt; — &lt;a href=&quot;https://www.mabl.com/blog/how-we-built-a-system-for-ai-agents-to-ship-real-code-across-75-repos&quot;&gt;mabl.com&lt;/a&gt;, April 8, 2026&lt;/li&gt;
&lt;li&gt;Engineering at Meta, &lt;em&gt;How Meta used AI to map tribal knowledge in large-scale data pipelines&lt;/em&gt; — &lt;a href=&quot;https://engineering.fb.com/2026/04/06/developer-tools/how-meta-used-ai-to-map-tribal-knowledge-in-large-scale-data-pipelines/&quot;&gt;engineering.fb.com&lt;/a&gt;, April 6, 2026&lt;/li&gt;
&lt;li&gt;Neilos (@neil_agentic), &lt;em&gt;How I Manage 15+ Repos with Claude Code (Without Losing My Mind)&lt;/em&gt; — &lt;a href=&quot;https://dev.to/neil_agentic/how-i-manage-15-repos-with-claude-code-without-losing-my-mind-2ood&quot;&gt;dev.to/neil_agentic&lt;/a&gt;, March 27, 2026&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=&quot;https://www.cortex.io/report/engineering-in-the-age-of-ai-2026-benchmark-report&quot;&gt;cortex.io&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Riftmap, &lt;em&gt;AI Doesn&apos;t Understand Blast Radius&lt;/em&gt; — &lt;a href=&quot;https://riftmap.dev/blog/ai-doesnt-understand-blast-radius/&quot;&gt;riftmap.dev/blog&lt;/a&gt;, April 19, 2026&lt;/li&gt;
&lt;li&gt;Riftmap, &lt;em&gt;Meta needed 50+ AI agents to map their tribal knowledge&lt;/em&gt; — &lt;a href=&quot;https://riftmap.dev/blog/meta-tribal-knowledge-engine-build-the-graph-first/&quot;&gt;riftmap.dev/blog&lt;/a&gt;, May 8, 2026&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Appendix: structured summary&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Claim:&lt;/strong&gt; AI coding agents at scale need a queryable cross-repo dependency graph as runtime infrastructure. Three independent teams have just published the same diagnosis. Two built the substrate. The third handled it through coordination, which works at solo scale.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Evidence:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Neilos (&lt;code&gt;ttal&lt;/code&gt;, Mar 27): solo operator, 15+ repos, 10 specialised agents. Diagnostic: &quot;manually tracking which PR depends on which.&quot; Solution: coordination layer with manager/worker planes, fitted to solo scale.&lt;/li&gt;
&lt;li&gt;Mabl (Apr 8): 25 engineers, 100+ repos, 39% of commits AI-assisted by Feb 2026. Solution: 850-line Repo Coordination Graph spanning 79 repositories, queried by agents at planning time. Reduced context drift from ~40% to &amp;lt;5% of failures.&lt;/li&gt;
&lt;li&gt;Meta (Apr 6): cross-repo dependency index reduces &quot;what depends on X&quot; from ~6,000 tokens to ~200 tokens, a 30x efficiency gain.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Architectural takeaway:&lt;/strong&gt; The dependency graph as a queryable substrate isn&apos;t a stylistic choice. It&apos;s what falls out of the problem when scale forces externalisation of cross-repo coordination. The graph itself is structural, parser-derived, and identical in shape across orgs, which makes it a primitive that should live above any single team rather than be re-implemented inside each one.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What Riftmap exposes today:&lt;/strong&gt; auto-discovered graph across 12 ecosystems via a single read-only org token. HTTP API with &lt;code&gt;/repositories/lookup&lt;/code&gt;, &lt;code&gt;/repositories/{id}/context&lt;/code&gt;, &lt;code&gt;/repositories/{id}/impact&lt;/code&gt;. Static OpenAPI schema at &lt;code&gt;https://app.riftmap.dev/openapi.json&lt;/code&gt;. MCP server and CLI on the &lt;a href=&quot;https://docs.riftmap.dev/agents/mcp-cli-roadmap&quot;&gt;public roadmap&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Audience:&lt;/strong&gt; Platform engineers, DevOps leads, and engineering managers running AI coding agents across more than a handful of repositories.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Related reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://riftmap.dev/blog/ai-doesnt-understand-blast-radius/&quot;&gt;AI Doesn&apos;t Understand Blast Radius&lt;/a&gt; — The data behind change failure rates being up 30%, and what to put between the agent and main.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://riftmap.dev/blog/meta-tribal-knowledge-engine-build-the-graph-first/&quot;&gt;Meta needed 50+ AI agents to map their tribal knowledge&lt;/a&gt; — Why the cross-repo dependency graph is the most durable piece of Meta&apos;s tribal knowledge stack.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://riftmap.dev/blog/measure-your-change-failure-rate/&quot;&gt;Change failure rate is up 30% — here&apos;s how to measure yours in an afternoon&lt;/a&gt; — The DORA formula, the SQL, and the AI-assisted vs human-authored split nobody publishes.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://riftmap.dev/blog/how-to-give-copilot-cross-repo-context/&quot;&gt;How to give GitHub Copilot cross-repo context today&lt;/a&gt; — the three working ways to get cross-repo context into Copilot, and the repository-list problem all three leave you to solve by hand.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://riftmap.dev/blog/you-dont-need-a-virtual-monorepo/&quot;&gt;You don&apos;t need a virtual monorepo. You need a graph.&lt;/a&gt; — why the workspace pattern is a stopgap, and the queryable graph it grows into past ~100 repos.&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>ai-coding-agents</category><category>cross-repo-context</category><category>platform-engineering</category><category>cross-repo-dependencies</category><category>polyrepo</category><category>blast-radius</category><author>Daniel Westgaard</author></item><item><title>Meta needed 50+ AI agents to map their tribal knowledge. The most durable piece of their stack is the part you can build today.</title><link>https://riftmap.dev/blog/meta-tribal-knowledge-engine-build-the-graph-first/</link><guid isPermaLink="true">https://riftmap.dev/blog/meta-tribal-knowledge-engine-build-the-graph-first/</guid><description>A close read of Meta&apos;s April 2026 tribal knowledge engine, the academic paper they cited, and the architectural argument hidden inside both.</description><pubDate>Fri, 08 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;In early April, Meta&apos;s engineering team published &lt;a href=&quot;https://engineering.fb.com/2026/04/06/developer-tools/how-meta-used-ai-to-map-tribal-knowledge-in-large-scale-data-pipelines/&quot;&gt;a piece&lt;/a&gt; describing how they used a swarm of 50+ specialised AI agents to map tribal knowledge across one of their large data pipelines: four repositories, three languages, 4,100+ files. The post has been making the rounds in platform engineering circles for good reason. It names a problem most polyrepo organisations live with quietly, and shows what an industrial-scale solution looks like.&lt;/p&gt;
&lt;p&gt;I read it twice. I think there are actually two posts inside it. One is a complex AI orchestration story about agent swarms, critic passes, and self-refreshing context files. That&apos;s the one most takes have focused on. The other is a quieter architectural argument that gets one paragraph and is the more useful piece for everyone who isn&apos;t Meta.&lt;/p&gt;
&lt;p&gt;This is about the second one.&lt;/p&gt;
&lt;h2&gt;What Meta actually built&lt;/h2&gt;
&lt;p&gt;Meta&apos;s pipeline is config-as-code: Python configurations, C++ services, Hack automation scripts working together across multiple repositories. A single data field onboarding touches six subsystems that must stay in sync. Pointing AI coding agents at this codebase produced predictable results. The agents would compile but be subtly wrong. They didn&apos;t know which &quot;deprecated&quot; enum values must never be removed because of serialisation compatibility. They didn&apos;t know that two configuration modes use different field names for the same operation, and that swapping them produces silent incorrect output. None of this was documented anywhere outside engineers&apos; heads.&lt;/p&gt;
&lt;p&gt;Meta&apos;s response was a multi-stage AI pipeline they call a &quot;pre-compute engine.&quot; Two explorer agents map the codebase. Eleven module analysts read every file and answer five questions per module. Two writers produce 25–35 line context files. Ten or more critic passes do three rounds of independent quality review. Four fixer agents apply corrections. The output is 59 concise context files covering 100% of code modules, up from ~5% before. Every few weeks, the system self-refreshes: validates file paths, detects coverage gaps, re-runs critics, auto-fixes stale references.&lt;/p&gt;
&lt;p&gt;The results, on a six-task evaluation, are real: roughly 40% fewer tool calls per task. Workflow guidance that used to take two days of asking engineers now takes 30 minutes.&lt;/p&gt;
&lt;p&gt;I&apos;m not going to argue with any of that. It&apos;s clearly working at Meta. But it works at Meta partly because Meta has a platform team large enough to build, run, and continuously maintain a self-refreshing critic swarm. That&apos;s not a small footnote. It&apos;s the whole reason the system stays trustworthy over time, and it&apos;s the part that doesn&apos;t transfer to a 100-engineer company.&lt;/p&gt;
&lt;h2&gt;The argument hidden in the article&lt;/h2&gt;
&lt;p&gt;Buried near the end of Meta&apos;s post, under the heading &quot;What We Built,&quot; there&apos;s one paragraph that did more for me than the rest of the article combined. I&apos;ll quote it because it&apos;s the most striking number in the entire piece:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Beyond individual contextual files, we generated a cross-repo dependency index and data flow maps showing how changes propagate across repositories. This turns &quot;What depends on X?&quot; from a multi-file exploration (~6000 tokens) into a single graph lookup (~200 tokens).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That&apos;s a 30x reduction in token cost for one of the most common questions agents and engineers ask: if I change this thing, what else breaks?&lt;/p&gt;
&lt;p&gt;Meta describes the dependency index almost as an aside, after the long sections on the agent swarm. But it&apos;s structurally different from everything else in their stack, and the difference matters.&lt;/p&gt;
&lt;p&gt;The 59 context files are LLM-generated artifacts. They have to be quality-gated by critic agents, validated for hallucinations, and refreshed periodically because code drift makes them stale. That&apos;s why the self-refresh mechanism exists, and why it requires a platform team to run. Context that decays is worse than no context, by Meta&apos;s own admission.&lt;/p&gt;
&lt;p&gt;The cross-repo dependency index is different. It&apos;s a structured graph derived from parsing actual source files. It doesn&apos;t need critic agents to validate quality because parsers are deterministic: the same input produces the same output. It doesn&apos;t decay between scans because it&apos;s not a snapshot of human-readable knowledge. It&apos;s a queryable index of what the code actually says. You re-scan on every push, and it&apos;s fresh by construction.&lt;/p&gt;
&lt;p&gt;Two layers, two operating models. One is volatile and needs a maintenance team. The other is durable and maintains itself.&lt;/p&gt;
&lt;h2&gt;The paper Meta cited deserves a closer read&lt;/h2&gt;
&lt;p&gt;Meta acknowledges, briefly, &lt;a href=&quot;https://arxiv.org/abs/2602.11988&quot;&gt;a recent paper&lt;/a&gt; from ETH Zurich and &lt;a href=&quot;http://LogicStar.ai&quot;&gt;LogicStar.ai&lt;/a&gt; (Gloaguen et al., February 2026) that found AI-generated context files reduced agent success rates on open-source Python repositories. Meta&apos;s response is reasonable: those repositories are already well-represented in model training data, so context files are redundant noise there. Their own codebase, with proprietary tribal knowledge, is the opposite case.&lt;/p&gt;
&lt;p&gt;That&apos;s a fair distinction, but the paper&apos;s actual findings are richer than Meta&apos;s framing suggests, and worth engaging with directly.&lt;/p&gt;
&lt;p&gt;Across four coding agents (Claude Code, Codex, Qwen Code, and a Codex variant) on 138 niche-repository tasks plus SWE-bench Lite, the paper found:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;LLM-generated context files marginally hurt performance&lt;/strong&gt; (-3% on average) while increasing inference cost by over 20%.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Developer-written context files marginally helped&lt;/strong&gt; (+4% on average), still at +19% cost.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Context files do not provide effective overviews.&lt;/strong&gt; This is a direct subsection title in the paper. Agents took the same number of steps to find relevant files whether a context file was present or not.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Context files lead to more exploration and testing, not less.&lt;/strong&gt; Agents read more, search more, and test more when given a context file. They follow the instructions, but the instructions make tasks harder.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Stronger models don&apos;t generate better context files.&lt;/strong&gt; The expected scaling story doesn&apos;t hold.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The paper&apos;s own conclusion: context files have only marginal effect on agent behaviour, and are likely only desirable when manually written.&lt;/p&gt;
&lt;p&gt;The honest read of this is not &quot;context files don&apos;t work.&quot; It&apos;s that even in the best case, they give a +4% improvement at +20% cost, and only when humans write them. For LLM-generated context files, which is what Meta&apos;s swarm produces, the average effect is negative.&lt;/p&gt;
&lt;p&gt;A caveat in the paper&apos;s favour: the evaluation is Python-only. The authors note that niche languages with less training data representation might benefit more from context files. Meta&apos;s codebase includes Hack and C++ alongside Python. So Meta sits exactly at the edge of what the paper studies. Their case for context files is defensible, but it isn&apos;t cleanly contradicted by the paper either. The honest position is that the jury is still out on whether Meta&apos;s quality-gated, critic-validated context files clear the bar that naive &lt;code&gt;/init&lt;/code&gt; commands don&apos;t.&lt;/p&gt;
&lt;h2&gt;A different decomposition of the same problem&lt;/h2&gt;
&lt;p&gt;Meta&apos;s five-question framework for module analysts is the most portable idea in their post. The questions are:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;What does this module configure?&lt;/li&gt;
&lt;li&gt;What are the common modification patterns?&lt;/li&gt;
&lt;li&gt;What are the non-obvious patterns that cause build failures?&lt;/li&gt;
&lt;li&gt;What are the cross-module dependencies?&lt;/li&gt;
&lt;li&gt;What tribal knowledge is buried in code comments?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Look at these and you&apos;ll notice they decompose into two categories. Question 4 is a structural question. The answer exists in the source code itself, in import statements, configuration references, module declarations, and infrastructure-as-code definitions. Parsers can extract it deterministically. Questions 1, 2, 3, and 5 are semantic. They require interpretation: what is the intent behind this code, what patterns recur across changes, what isn&apos;t written down. These are LLM-shaped questions, and they&apos;re exactly the kind the ETH paper says context files struggle with.&lt;/p&gt;
&lt;p&gt;This decomposition matters because it tells you where to invest first. The structural layer is cheap to build, deterministic, and stays fresh by construction. The semantic layer is expensive to build, requires quality gates, and decays without maintenance. If you have unlimited platform resources, you build both. If you have a small team, you build the structural layer first, because it gives you most of the leverage at a fraction of the cost, and because the semantic layer becomes more useful when it can anchor to verified structure.&lt;/p&gt;
&lt;p&gt;There&apos;s a deeper architectural argument here too. When an LLM generates context about a codebase from scratch, it&apos;s discovering structure and writing prose about it. Hallucination lives in that gap. When an LLM generates context on top of a verified dependency graph, it&apos;s annotating known structure with semantic claims. The structural ground truth constrains what the LLM can plausibly say. That&apos;s a more reliable architecture, and it&apos;s only available to you if you&apos;ve built the graph first.&lt;/p&gt;
&lt;h2&gt;What this means for teams without a platform team&lt;/h2&gt;
&lt;p&gt;If you have 50 to 500 repositories and a small platform team or no platform team at all, here&apos;s what I&apos;d take from Meta&apos;s post.&lt;/p&gt;
&lt;p&gt;You can&apos;t afford a self-refreshing critic swarm with 50+ specialised agents. You probably don&apos;t need one. The piece of Meta&apos;s stack with the highest leverage per dollar is the cross-repo dependency index — the structural layer the AI tooling community has started calling &lt;a href=&quot;https://riftmap.dev/what-is-cross-repo-dependency-mapping/&quot;&gt;cross-repo context&lt;/a&gt; — and that&apos;s the piece you can have today without building any of Meta&apos;s orchestration layer. It&apos;s also the piece that gives you the freshness signal Meta argues is non-negotiable for trust (&quot;context that decays is worse than no context&quot;), because parsers run on every push and you can timestamp every edge in the graph.&lt;/p&gt;
&lt;p&gt;The order matters. Build the structural layer first. Make &quot;what depends on X&quot; a 200-token graph lookup, not a 6,000-token exploration. Make blast-radius analysis a query, not an interview. Then, if you still want LLM-generated context on top, do it knowing the graph constrains what the LLM can say and the quality bar gets clearer.&lt;/p&gt;
&lt;p&gt;Meta&apos;s post is genuinely useful evidence. It validates that tribal knowledge is the bottleneck for AI agents in proprietary codebases. It validates that cross-repo dependency mapping is the cheapest, most reusable primitive in any solution. It also, accidentally, makes a strong case that the rest of the stack is optional for everyone who can&apos;t afford to maintain it.&lt;/p&gt;
&lt;p&gt;The graph is the substrate that doesn&apos;t lie. Everything else gets layered on top.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This is the architectural bet I made when I started building &lt;a href=&quot;https://riftmap.dev/&quot;&gt;Riftmap&lt;/a&gt;: deterministic parsers first, graph as the durable layer, AI as a future enhancement that anchors to verified structure. If you&apos;re thinking about cross-repo dependency mapping in your own organisation, the &lt;a href=&quot;https://riftmap.dev/what-is-cross-repo-dependency-mapping/&quot;&gt;glossary page&lt;/a&gt; is a good starting point.&lt;/em&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Appendix: structured summary&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Claim:&lt;/strong&gt; Meta&apos;s tribal knowledge engine is two systems. A self-refreshing AI swarm that needs platform-team maintenance, and a cross-repo dependency graph mentioned almost as an aside. The graph is the durable substrate. The swarm is the volatile layer on top.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Evidence from Meta:&lt;/strong&gt; Cross-repo dependency index reduces &quot;what depends on X&quot; from ~6,000 tokens to ~200 tokens, a 30x token efficiency gain. Self-refresh required for context files because &quot;context that decays is worse than no context.&quot;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Evidence from Gloaguen et al. (arXiv:2602.11988, Feb 2026):&lt;/strong&gt; LLM-generated context files reduce agent success rates by ~3% on average at +20% inference cost. Human-written context files improve success rates by ~4% at +19% cost. Context files do not provide effective overviews. Stronger models do not generate better context files. Evaluation is Python-only; niche languages may differ.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Architectural takeaway:&lt;/strong&gt; Meta&apos;s five-question framework decomposes into structural (cross-module dependencies, deterministically extractable) and semantic (intent, patterns, undocumented knowledge, LLM-shaped). The structural layer is cheap to build and self-fresh. The semantic layer is expensive and decays. Build the structural layer first. The semantic layer becomes more useful when it anchors to verified structure.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Audience this is written for:&lt;/strong&gt; Platform engineers, DevOps leads, and SRE teams at organisations with 50–500 repositories, polyrepo architectures, and mixed infrastructure-as-code. Especially those without a Meta-scale platform team.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Related reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://riftmap.dev/blog/ai-coding-agents-need-cross-repo-context/&quot;&gt;AI coding agents need cross-repo context&lt;/a&gt; — Three teams shipped the same diagnosis in two weeks. Two built the dependency graph substrate. One built around it.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://riftmap.dev/blog/ai-doesnt-understand-blast-radius/&quot;&gt;AI Doesn&apos;t Understand Blast Radius&lt;/a&gt; — Why AI coding tools optimise for local correctness while production breaks at the cross-repo graph they can&apos;t see.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://riftmap.dev/blog/measure-your-change-failure-rate/&quot;&gt;Change failure rate is up 30% — here&apos;s how to measure yours in an afternoon&lt;/a&gt; — Calculate your team&apos;s CFR in an afternoon, including the AI-assisted vs human-authored split.&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>platform-engineering</category><category>tribal-knowledge</category><category>cross-repo-dependencies</category><category>cross-repo-context</category><category>ai-coding-agents</category><category>polyrepo</category><author>Daniel Westgaard</author></item><item><title>The catalog maintenance trap: why service catalogs go stale</title><link>https://riftmap.dev/blog/the-catalog-maintenance-trap/</link><guid isPermaLink="true">https://riftmap.dev/blog/the-catalog-maintenance-trap/</guid><description>Backstage and the developer-portal category solve a real problem. The reason platform teams quietly abandon them is something different, and it points at the shape of what actually works.</description><pubDate>Thu, 07 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;Backstage and the developer-portal category solve a real problem. The reason platform teams quietly abandon them is something different, and it points at the shape of what actually works.&lt;/em&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;Every few weeks I run into the same observation from someone in platform engineering: their team looked at Backstage, evaluated it seriously, maybe even ran a proof of concept, and walked away. The reason is rarely that it doesn&apos;t do what it advertises. The reason is that the work of keeping it running turned out to be bigger than the value it returned.&lt;/p&gt;
&lt;p&gt;I&apos;ve now heard this pattern in r/devops threads, in conversations with engineers who built their own internal alternatives, and most recently from a platform engineer who summarised his evaluation in a single sentence: the cost of maintaining it was bigger than what we got back.&lt;/p&gt;
&lt;p&gt;That sentence has been bouncing around my head, because it names something I haven&apos;t seen named clearly. I&apos;m calling it &lt;strong&gt;the catalog maintenance trap&lt;/strong&gt;: the gap between what a service catalog promises and what it costs to keep it true.&lt;/p&gt;
&lt;h2&gt;What catalogs actually require&lt;/h2&gt;
&lt;p&gt;Backstage, Port, OpsLevel, and the rest of the developer-portal category are built around a simple model. Each service is described by a YAML file (in Backstage&apos;s case, &lt;code&gt;catalog-info.yaml&lt;/code&gt;) that lists the service, its owner, its dependencies, links to runbooks, on-call rotations, and so on. The portal aggregates these files and gives the whole organisation a single pane of glass.&lt;/p&gt;
&lt;p&gt;The model is clean. The trouble is the verb tense. The catalog describes the world &lt;em&gt;as of the last time someone updated it&lt;/em&gt;. Every dependency added, removed, or renamed since then is invisible until a human notices and edits the YAML. Multiply that across a hundred repos and a few dozen engineers, and the catalog drifts faster than anyone wants to admit.&lt;/p&gt;
&lt;p&gt;This isn&apos;t a Backstage flaw. It&apos;s a property of any system where the source of truth is a manually-maintained catalog. The same trap exists in any &quot;register your dependencies in this file&quot; approach.&lt;/p&gt;
&lt;h2&gt;Why teams stop maintaining it&lt;/h2&gt;
&lt;p&gt;The maintenance gradient is brutal. On day one, the catalog is shiny and motivating, and the platform team writes the first batch of YAML by hand. Over the next few weeks, two or three engineers add their services. Then onboarding starts to slow down. Then someone changes a dependency without updating the catalog. Then the dashboard shows an outdated graph. Then a new hire asks &quot;is this accurate?&quot; and the honest answer is &quot;kind of, in places.&quot;&lt;/p&gt;
&lt;p&gt;At that point the catalog has stopped being an authoritative graph and started being documentation that was &lt;em&gt;supposed to be&lt;/em&gt; authoritative. Which is worse than not having it, because people make decisions on the assumption that it&apos;s accurate.&lt;/p&gt;
&lt;p&gt;The platform team has three options at this stage:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Mandate catalog updates as part of every PR review. An organisational tax that nobody enforces consistently.&lt;/li&gt;
&lt;li&gt;Build automation to keep the catalog current. Which is the actual problem, only now there&apos;s also a YAML schema in the loop.&lt;/li&gt;
&lt;li&gt;Quietly let it rot and rely on tribal knowledge again.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Option three is what most teams converge on. Not because the platform engineers are lazy, but because the cost of options one and two exceeds the value of having a catalog that&apos;s accurate to within a week.&lt;/p&gt;
&lt;h2&gt;The two escape routes&lt;/h2&gt;
&lt;p&gt;When teams give up on the catalog, they tend to take one of two paths.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Path one: monorepo.&lt;/strong&gt; Consolidate everything into one repository and let a build tool like Nx or Turborepo maintain the dependency graph implicitly. This works, but it isn&apos;t a tooling decision. It&apos;s an architecture decision that takes years to execute, often isn&apos;t feasible across business units, and doesn&apos;t help with the infrastructure dependency surface (Terraform, Docker, Helm, CI templates) the way it helps with application code.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Path two: do nothing.&lt;/strong&gt; Accept that the dependency graph lives in the heads of the senior engineers. Ask around when you need to make a breaking change. Hope nobody on the relevant team is on holiday. This is what most organisations actually do, and it&apos;s also what creates the conditions for the three-hour Slack outage when a base image changes and six teams break in sequence.&lt;/p&gt;
&lt;p&gt;Neither escape route is satisfying. The catalog promised to solve this problem, and the alternatives are either an architectural migration or institutional amnesia.&lt;/p&gt;
&lt;h2&gt;The thing the catalog model gets wrong&lt;/h2&gt;
&lt;p&gt;The catalog model assumes humans should be the source of truth about dependencies. They shouldn&apos;t be. The actual source of truth already exists, written down in machine-readable form, in the repositories themselves: Terraform &lt;code&gt;source = &quot;...&quot;&lt;/code&gt; blocks, Dockerfile &lt;code&gt;FROM&lt;/code&gt; statements, &lt;code&gt;go.mod&lt;/code&gt; &lt;code&gt;require&lt;/code&gt; directives, &lt;code&gt;.gitlab-ci.yml&lt;/code&gt; &lt;code&gt;include&lt;/code&gt; references, Helm &lt;code&gt;Chart.yaml&lt;/code&gt; dependencies. The dependency graph is &lt;em&gt;already declared&lt;/em&gt;. It&apos;s just declared across thousands of files in dozens of formats.&lt;/p&gt;
&lt;p&gt;The work isn&apos;t getting humans to register a second copy in YAML. The work is parsing the declarations that already exist and stitching them into one queryable graph.&lt;/p&gt;
&lt;p&gt;This is what auto-discovery means in practice. No catalog. No annotations. No PR template reminding people to update the manifest. The graph is read from the source files, and the source files are the ones engineers are already editing because they have to in order to ship code.&lt;/p&gt;
&lt;p&gt;This is the difference between documentation that engineers are asked to maintain and a graph that maintains itself.&lt;/p&gt;
&lt;h2&gt;What this actually changes&lt;/h2&gt;
&lt;p&gt;Once discovery is automatic, the value proposition shifts. You stop selling &quot;a place to write down what you have&quot; and start selling &quot;a query interface for the truth your repos already encode.&quot; The questions change too:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Not &quot;is the catalog up to date?&quot; but &quot;what&apos;s the blast radius of changing this module?&quot;&lt;/li&gt;
&lt;li&gt;Not &quot;did everyone fill in their YAML?&quot; but &quot;which repos still pin to the old major version?&quot;&lt;/li&gt;
&lt;li&gt;Not &quot;who owns this service?&quot; but &lt;a href=&quot;https://riftmap.dev/blog/deprecate-internal-library-find-consumers/&quot;&gt;&quot;if I deprecate this artifact, which teams need to be in the room?&quot;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These are the questions platform teams actually ask. The catalog model could only answer them if the catalog was perfect, which it never was. Auto-discovery answers them by skipping the catalog step entirely.&lt;/p&gt;
&lt;h2&gt;Where Backstage still fits&lt;/h2&gt;
&lt;p&gt;I want to be careful not to overclaim. Backstage genuinely solves problems that auto-discovery doesn&apos;t: service ownership across hundreds of services, documentation aggregation, templated scaffolding for new services, a unified frontend for on-call and runbooks. Those are real jobs, and a service catalog is a reasonable tool for them.&lt;/p&gt;
&lt;p&gt;The mistake is using the catalog as the dependency graph. The catalog is good at things humans &lt;em&gt;want&lt;/em&gt; to register on purpose (this service is owned by team X, on-call is via PagerDuty, the runbook is in Confluence). It is bad at things that change underneath every commit (the actual graph of what consumes what).&lt;/p&gt;
&lt;p&gt;For dependency visibility specifically, the catalog model isn&apos;t the right shape. Something that reads the source and rebuilds the graph on every scan is.&lt;/p&gt;
&lt;h2&gt;What we built&lt;/h2&gt;
&lt;p&gt;I&apos;m working on &lt;a href=&quot;https://riftmap.dev/&quot;&gt;Riftmap&lt;/a&gt; because this is the gap I kept hitting at client engagements as a consultant, and it&apos;s the gap the engineers I talk to keep describing. Riftmap connects to a GitLab or GitHub org with a read-only token, parses the dependency declarations that already exist across Terraform, Docker, Helm, CI pipelines, Go, npm, Python, Ansible, Kubernetes, and Kustomize, and presents the graph with &lt;a href=&quot;https://riftmap.dev/blog/ai-doesnt-understand-blast-radius/&quot;&gt;blast radius analysis&lt;/a&gt;. There is no catalog YAML to maintain. The graph rebuilds itself when repos change.&lt;/p&gt;
&lt;p&gt;If your team has evaluated Backstage and walked away because the maintenance cost was bigger than the value, or if you&apos;re staring at that same trade-off right now, I&apos;d be curious to hear about it. The pattern I described in this post is built from a small number of conversations and the consistent signal in r/devops and r/terraform threads. The more I hear from teams in this position, the better the tool gets.&lt;/p&gt;
&lt;p&gt;If you&apos;re earlier in the decision and the question is still &quot;is Backstage worth adopting at all?&quot;, the companion to this piece reframes that debate around adoption rather than cost — Spotify&apos;s own head of Backstage engineering puts external adoption at around 10% and explains why: &lt;a href=&quot;https://riftmap.dev/blog/is-backstage-worth-it/&quot;&gt;is Backstage worth it?&lt;/a&gt;. And if you&apos;re actively evaluating alternatives, I wrote a separate triage of the &quot;Backstage alternatives&quot; question by the job that sent you looking — portals, managed Backstage, or a parsed dependency graph: &lt;a href=&quot;https://riftmap.dev/blog/backstage-alternatives/&quot;&gt;Backstage alternatives in 2026&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;You can try Riftmap free at &lt;a href=&quot;https://app.riftmap.dev&quot;&gt;app.riftmap.dev&lt;/a&gt;, or reach me directly at &lt;a href=&quot;mailto:daniel@riftmap.dev&quot;&gt;daniel@riftmap.dev&lt;/a&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;If you&apos;ve hit the catalog maintenance trap and want to compare notes, I&apos;d genuinely like to hear how it went. Honest accounts of what worked and what didn&apos;t are the most valuable input I get.&lt;/em&gt;&lt;/p&gt;
</content:encoded><category>platform-engineering</category><category>backstage</category><category>service-catalogs</category><category>developer-portals</category><category>dependency-management</category><author>Daniel Westgaard</author></item><item><title>What 56 Prometheus repos actually depend on</title><link>https://riftmap.dev/blog/what-56-prometheus-repos-depend-on/</link><guid isPermaLink="true">https://riftmap.dev/blog/what-56-prometheus-repos-depend-on/</guid><description>I scanned all 56 repos in the Prometheus org with Riftmap. Here&apos;s the cross-repo dependency graph, including the 25 repos that import client_golang.</description><pubDate>Sun, 26 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;A scan of the Prometheus organisation, rendered as a graph: 56 repos, 197 cross-repo dependencies, and 25 repos that depend on &lt;code&gt;client_golang&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Update (June 2026):&lt;/strong&gt; The figures and screenshots below are from the original April 2026 scan (Riftmap v1.5). A fresh re-scan on 2026-06-25 (Riftmap v1.6.6) counts the org&apos;s coupling under a stricter, deduplicated canonical unit: &lt;strong&gt;214 in-org cross-repo edges from 271 raw references&lt;/strong&gt;, with &lt;code&gt;client_golang&lt;/code&gt; resolving to &lt;strong&gt;23&lt;/strong&gt; distinct in-org consumers. I broke down what that composition is actually made of — and why not one of those 214 edges is a code symbol — in &lt;a href=&quot;https://riftmap.dev/blog/cross-repo-edge-composition/&quot;&gt;I counted every cross-repo edge in two real orgs&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;p&gt;If you&apos;ve worked with Prometheus, you have a rough mental model of how its 50-odd repos fit together: a core server, a client library, a constellation of exporters, some shared infrastructure. What you probably haven&apos;t seen is what that mental model looks like rendered as a graph, or what it tells you about the blast radius of changing any one repo.&lt;/p&gt;
&lt;p&gt;I scanned the Prometheus org with &lt;a href=&quot;https://riftmap.dev&quot;&gt;Riftmap&lt;/a&gt;, the tool I built for mapping cross-repo dependencies. The point of starting here isn&apos;t to surface anything exotic. It&apos;s the opposite: Prometheus is well-known enough that you can check the scanner&apos;s output against your own intuition. If the shape doesn&apos;t match, the tool is broken.&lt;/p&gt;
&lt;p&gt;The shape mostly matches. There are also a few specific things it doesn&apos;t see, which I&apos;ll get into below.&lt;/p&gt;
&lt;h2&gt;The shape of the org at a glance&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://riftmap.dev/images/blog/prometheus/dashboard.png&quot; alt=&quot;Riftmap dashboard view of the Prometheus org showing 56 repos, 197 dependencies, 30 consumed artifacts, top-impact list, and a dependency breakdown by ecosystem&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The numbers from a single scan: 56 repositories, 197 cross-repo dependencies, 30 distinct artifacts being consumed somewhere in the org. The breakdown skews Go-heavy (122 Go module references, 35 Docker, 34 CI/CD includes, 3 npm, 3 Kubernetes), which is what you&apos;d expect for a Go-native observability stack.&lt;/p&gt;
&lt;p&gt;The top of the impact list reads about how a Prometheus contributor would predict it:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;prometheus/common&lt;/code&gt; — 28 dependents&lt;/li&gt;
&lt;li&gt;&lt;code&gt;prometheus/client_golang&lt;/code&gt; — 25 dependents&lt;/li&gt;
&lt;li&gt;&lt;code&gt;prometheus/client_model&lt;/code&gt; — 23 dependents&lt;/li&gt;
&lt;li&gt;&lt;code&gt;prometheus/prometheus&lt;/code&gt; — 23 dependents&lt;/li&gt;
&lt;li&gt;&lt;code&gt;prometheus/exporter-toolkit&lt;/code&gt; — 22 dependents&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you&apos;d guessed this ranking unprompted you&apos;d have gotten most of it right. That&apos;s the calibration: the ranking confirms the mental model rather than challenging it. Where the graph starts paying for itself is one layer down, when you ask not just &quot;what&apos;s central&quot; but &quot;what specifically depends on this, and through which file.&quot;&lt;/p&gt;
&lt;h2&gt;The graph view&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://riftmap.dev/images/blog/prometheus/graph-default.png&quot; alt=&quot;Riftmap default graph view of the Prometheus org showing 35 nodes and 94 edges, with eight clusters visible and a row of unconnected repos along the bottom&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The default graph view shows 35 nodes and 94 edges, after Riftmap&apos;s auto-clustering groups repos with similar dependency profiles. Eight clusters surface in this scan: a &lt;code&gt;common&lt;/code&gt; cluster anchored on &lt;code&gt;prometheus/common&lt;/code&gt;, a &lt;code&gt;client_model&lt;/code&gt; cluster grouping the libraries that import the data model, a &lt;code&gt;promci&lt;/code&gt; cluster for shared CI infrastructure, an &lt;code&gt;exporter-toolkit&lt;/code&gt; cluster pulling in the exporters that depend on it, and a &lt;code&gt;procfs&lt;/code&gt; cluster around the Linux process-level libraries. The remaining three are smaller groupings that the auto-clustering pulls out based on shared dependency profiles.&lt;/p&gt;
&lt;p&gt;The bottom rows are the foundation: repos with no detected in-org edges. Some of those are intentional and I&apos;ll come back to them. The clustering is what makes 56 nodes legible at this zoom level. Without it you get the standard hairball graph that no one ever zooms into a second time.&lt;/p&gt;
&lt;h2&gt;The pattern at the center&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://riftmap.dev/images/blog/prometheus/focus-client-golang.png&quot; alt=&quot;Riftmap focus mode on prometheus/client_golang showing the repo highlighted at the top with all 25 connected repos arranged in a grid below, including alertmanager, blackbox_exporter, node_exporter, snmp_exporter, statsd_exporter, consul_exporter, memcached_exporter, mysql_exporter, haproxy_exporter, and pushgateway&quot; /&gt;&lt;/p&gt;
&lt;p&gt;This is the strongest single screenshot in the scan. Focus mode on &lt;code&gt;client_golang&lt;/code&gt; strips the canvas down to that repo and its 25-repo neighborhood. Every Prometheus exporter you&apos;ve heard of (&lt;code&gt;alertmanager&lt;/code&gt;, &lt;code&gt;node_exporter&lt;/code&gt;, &lt;code&gt;blackbox_exporter&lt;/code&gt;, &lt;code&gt;snmp_exporter&lt;/code&gt;, &lt;code&gt;statsd_exporter&lt;/code&gt;, &lt;code&gt;consul_exporter&lt;/code&gt;, &lt;code&gt;memcached_exporter&lt;/code&gt;, &lt;code&gt;mysql_exporter&lt;/code&gt;, &lt;code&gt;haproxy_exporter&lt;/code&gt;, &lt;code&gt;pushgateway&lt;/code&gt;) sits one edge away.&lt;/p&gt;
&lt;p&gt;The pattern is exactly what the design intent says it should be: every exporter in the org imports the canonical Go client library. The graph captures it cleanly, and you can read the relationship from a single pane without grepping through 25 separate &lt;code&gt;go.mod&lt;/code&gt; files.&lt;/p&gt;
&lt;p&gt;This is also where the &quot;external observer&quot; framing earns its keep. You don&apos;t need a Prometheus maintainer to walk you through which repos depend on &lt;code&gt;client_golang&lt;/code&gt;. A new engineer joining the org could open this graph on their first day and see the topology a senior engineer carries in their head.&lt;/p&gt;
&lt;h2&gt;Where the receipts live&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://riftmap.dev/images/blog/prometheus/sidebar-multi-manifest.png&quot; alt=&quot;Side panel detail view for client_golang showing 4 in-org repos and 58 external references, with multi-manifest entries visible: github.com/google/go-cmp tracked at exp/go.mod:6 v0.7.0, go.mod:8 v0.7.0, and tutorials/whatsup/go.mod:24 v0.7.0&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Click any node and the side panel opens with the file-level evidence behind every edge. For &lt;code&gt;client_golang&lt;/code&gt;: 4 in-org consumers, 58 external references resolved across &lt;code&gt;go.mod&lt;/code&gt;, &lt;code&gt;Dockerfile&lt;/code&gt;, and similar manifests.&lt;/p&gt;
&lt;p&gt;The detail worth pausing on: &lt;code&gt;client_golang&lt;/code&gt; shows up in the &lt;code&gt;prometheus/prometheus&lt;/code&gt; repo three separate times, at &lt;code&gt;exp/go.mod:6&lt;/code&gt;, &lt;code&gt;go.mod:8&lt;/code&gt;, and &lt;code&gt;tutorials/whatsup/go.mod:24&lt;/code&gt;. Three distinct &lt;code&gt;go.mod&lt;/code&gt; files in one repo, each importing the same library, each tracked as its own reference with its own pinned version. If you&apos;ve ever bumped a dependency in the root &lt;code&gt;go.mod&lt;/code&gt; and forgotten the one buried in &lt;code&gt;tutorials/whatsup/&lt;/code&gt;, this is the class of bug that hides in Go monorepos. Riftmap surfaces each manifest individually rather than collapsing them into a single repo-to-repo edge, specifically because &quot;we updated the dependency&quot; and &quot;we updated every reference to the dependency&quot; are not the same statement.&lt;/p&gt;
&lt;h2&gt;If I changed client_golang, what breaks?&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://riftmap.dev/images/blog/prometheus/impact-canvas.png&quot; alt=&quot;Riftmap Impact Mode canvas view for client_golang showing the focused neighborhood with red cascade edges radiating outward to all 25 affected repos&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Toggle Impact Mode on the same focus view from the previous section and the canvas lights up. Red cascade edges radiate outward from &lt;code&gt;client_golang&lt;/code&gt; through every repo in the blast radius. 25 repos affected at maximum depth 2: 24 direct dependents and 1 transitive. Every exporter, the alertmanager, &lt;code&gt;pushgateway&lt;/code&gt;, the test infrastructure repo, all visible in a single pane.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://riftmap.dev/images/blog/prometheus/impact-tab.png&quot; alt=&quot;Impact tab in the Riftmap side panel for client_golang showing 25 repos affected at maximum depth 2, with a .md export button visible and each affected repo listed with a depth-1 badge: alertmanager, blackbox_exporter, collectd_exporter, common, compliance, consul_exporter, exporter-toolkit, graphite_exporter, haproxy_exporter&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The side panel has the receipts. Each affected repo is listed with its depth label and the file:line of the import that would need to change. There&apos;s a Markdown export at the bottom of the panel. Click it and you get a copyable list of every affected repo with file references, ready to paste into a deprecation announcement, a migration RFC, or a release-notes block. That workflow, &quot;show me the blast radius and let me hand the list to the team that owns each downstream repo,&quot; is the loop the tool is designed for.&lt;/p&gt;
&lt;aside&gt;
&lt;p&gt;&lt;span&gt;&lt;/span&gt;See it live&lt;/p&gt;
&lt;p&gt;This walkthrough is a live graph — run it yourself&lt;/p&gt;
&lt;p&gt;Everything above is a static screenshot. The real scan is interactive: open the Prometheus showcase, focus client_golang, and toggle Impact Mode to watch the same 25-repo blast radius cascade in your browser.&lt;/p&gt;
&lt;a href=&quot;https://riftmap.dev/showcase/prometheus/&quot;&gt;Explore the Prometheus graph →&lt;/a&gt;
&lt;/aside&gt;
&lt;h2&gt;What Riftmap didn&apos;t see (and why)&lt;/h2&gt;
&lt;p&gt;The &quot;what we missed&quot; section in a scan write-up matters more than any of the wins above. Two distinct things are happening in the bottom row of orphan repos in the graph, and the difference is worth being precise about.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Real parser gaps.&lt;/strong&gt; Riftmap can&apos;t currently parse:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pom.xml&lt;/code&gt; and Maven coordinates, so &lt;code&gt;client_java&lt;/code&gt;, &lt;code&gt;client_java-benchmarks&lt;/code&gt;, and &lt;code&gt;jmx_exporter&lt;/code&gt; come out unconnected&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pyproject.toml&lt;/code&gt; and &lt;code&gt;setup.py&lt;/code&gt;, so &lt;code&gt;client_python&lt;/code&gt; shows no edges&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Gemfile&lt;/code&gt;, so &lt;code&gt;client_ruby&lt;/code&gt; and &lt;code&gt;prometheus_api_client_ruby&lt;/code&gt; show no edges&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Cargo.toml&lt;/code&gt;, so &lt;code&gt;client_rust&lt;/code&gt; shows no edges&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These are real, named gaps. The repos exist, they have published dependencies, and Riftmap&apos;s view of the org is missing their cross-repo relationships because the parsers don&apos;t exist yet. They are the next four ecosystems on the parser roadmap, in roughly that order. If you see something like this in your own org&apos;s scan, this is what it means. The current ecosystem coverage is documented in the &lt;a href=&quot;https://riftmap.dev/blog/auto-discovering-infrastructure-dependencies-across-10-ecosystems/&quot;&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; Different category: &lt;code&gt;lezer-promql&lt;/code&gt; shows up in the orphan row, but if you click into it the side panel reads &quot;0 in-org repos · 5 external references&quot; and lists every detected dependency: &lt;code&gt;@lezer/generator ^0.15.2&lt;/code&gt;, &lt;code&gt;@lezer/lr ^0.15.8&lt;/code&gt;, &lt;code&gt;@rollup/plugin-node-resolve ^9.0.0&lt;/code&gt;, &lt;code&gt;mocha ^8.1.3&lt;/code&gt;, &lt;code&gt;rollup ^2.27.1&lt;/code&gt;. The npm parser ran. It read &lt;code&gt;package.json&lt;/code&gt;. It extracted all five dependencies cleanly. They&apos;re just not in the Prometheus org, so they don&apos;t produce in-org edges, and the repo correctly appears as a leaf.&lt;/p&gt;
&lt;p&gt;That distinction is the whole point of an in-org dependency graph: it tells you what your own changes cascade into, not what the entire npm registry contains. &lt;code&gt;lezer-promql&lt;/code&gt;&apos;s dependencies all leave the org, so the graph faithfully reports zero in-org edges. That&apos;s a feature.&lt;/p&gt;
&lt;p&gt;The polyglot client libraries are a parser gap. &lt;code&gt;lezer-promql&lt;/code&gt; is correct silence. Both look the same in the graph at first glance. They are not the same thing.&lt;/p&gt;
&lt;p&gt;This was the first org in the series, picked because its shape is easy to check against intuition. The scans since have found very different shapes on the same method: &lt;a href=&quot;https://riftmap.dev/blog/what-208-kubernetes-sigs-repos-actually-depend-on/&quot;&gt;kubernetes-sigs came out a flat federation&lt;/a&gt; with no single load-bearing centre, and &lt;a href=&quot;https://riftmap.dev/blog/what-242-cloudposse-repos-depend-on/&quot;&gt;Cloud Posse a single keystone&lt;/a&gt; where one module holds up 61% of its 242 repos.&lt;/p&gt;
&lt;h2&gt;Methodology, briefly&lt;/h2&gt;
&lt;p&gt;Before Prometheus, 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 9 ecosystems) adds cross-language artifact reuse (one proto repo consumed by Python, Go, and npm), circular Go module deps, multi-artifact single repos, dependency chains five levels deep, and unsupported-ecosystem node rendering. Both groups have a hand-verified ground-truth edge list that every scanner change is tested against. The Prometheus scan completed in 2m37s across all 56 repos. 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 Prometheus and you spot something Riftmap got wrong about a repo you maintain, a missing edge, a misattributed version, a parser failure I should add to the roadmap, email me at &lt;a href=&quot;mailto:daniel@riftmap.dev&quot;&gt;daniel@riftmap.dev&lt;/a&gt;. I&apos;ll fix the parser and credit you in a follow-up.&lt;/p&gt;
&lt;p&gt;If you&apos;re running something similar in your own org and want to see what your graph looks like, Riftmap is at &lt;a href=&quot;https://riftmap.dev&quot;&gt;riftmap.dev&lt;/a&gt;. The scan I ran on Prometheus took under three minutes. Yours probably takes about the same.&lt;/p&gt;
</content:encoded><category>prometheus</category><category>observability</category><category>dependency-graph</category><category>cross-repo-dependencies</category><category>platform-engineering</category><category>open-source</category><category>monitoring</category><author>Daniel Westgaard</author></item><item><title>Change failure rate is up 30% — here&apos;s how to measure yours in an afternoon</title><link>https://riftmap.dev/blog/measure-your-change-failure-rate/</link><guid isPermaLink="true">https://riftmap.dev/blog/measure-your-change-failure-rate/</guid><description>A practitioner&apos;s guide to calculating change failure rate for your team without a vendor platform — the DORA formula, the SQL, and the one cut nobody is making yet: AI-assisted PRs vs human-authored.</description><pubDate>Tue, 21 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;A practitioner&apos;s guide to calculating your team&apos;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;hr /&gt;
&lt;p&gt;Cortex&apos;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=&quot;https://riftmap.dev/blog/ai-doesnt-understand-blast-radius/&quot;&gt;last post&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Here&apos;s the uncomfortable follow-up question: &lt;em&gt;what&apos;s yours?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Most platform teams I&apos;ve worked with couldn&apos;t give me a number. They could estimate a direction — &quot;it feels worse lately&quot; — but the actual percentage wasn&apos;t anywhere. And without a number, the 30% headline is just other people&apos;s data. You can&apos;t improve what you haven&apos;t measured.&lt;/p&gt;
&lt;p&gt;This post walks through how to compute your team&apos;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&apos;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&apos;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&apos;s the whole thing. Three details matter and they&apos;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&apos;t a change failure. A canary that catches a bad deploy before it reaches real users isn&apos;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&apos;s merely suboptimal isn&apos;t a failure. The question is whether it needed a rollback, hotfix, fix-forward, or patch after the fact. &quot;We wrote a Jira ticket&quot; isn&apos;t remediation; &quot;we pushed another deploy to fix the first one&quot; 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;/p&gt;
&lt;pre&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;p&gt;&lt;a href=&quot;https://dora.dev/research/2025/dora-report/&quot;&gt;DORA&apos;s 2025 report&lt;/a&gt; found that about 16.7% of teams maintain CFR at 4% or below — that&apos;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&apos;re set.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. A list of production incidents.&lt;/strong&gt; From PagerDuty, Opsgenie, &lt;a href=&quot;http://Incident.io&quot;&gt;Incident.io&lt;/a&gt;, 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 &quot;belongs to&quot; 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&apos;t causal attribution — it&apos;s a proxy, and it&apos;s close enough.&lt;/p&gt;
&lt;p&gt;Here&apos;s the shape of the query once both datasets are in the same place:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;WITH changes AS (
  SELECT
    deployment_id,
    service,
    deployed_at,
    commit_sha
  FROM deployments
  WHERE environment = &apos;production&apos;
    AND result = &apos;success&apos;
    AND deployed_at &amp;gt;= &apos;2026-01-01&apos;
    AND NOT is_fix_only  -- exclude rollbacks/hotfixes
),
failed_changes AS (
  SELECT DISTINCT c.deployment_id
  FROM changes c
  JOIN incidents i
    ON i.service = c.service
    AND i.started_at BETWEEN c.deployed_at AND c.deployed_at + INTERVAL &apos;24 hours&apos;
)
SELECT
  COUNT(DISTINCT c.deployment_id)                       AS total_changes,
  COUNT(DISTINCT fc.deployment_id)                      AS failed_changes,
  COUNT(DISTINCT fc.deployment_id) * 1.0
    / COUNT(DISTINCT c.deployment_id)                   AS change_failure_rate
FROM changes c
LEFT JOIN failed_changes fc USING (deployment_id);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If your deployment tool doesn&apos;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&apos;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&apos;s where it gets interesting. The Cortex 30% number is an aggregate. It tells you the industry has gotten worse. It doesn&apos;t tell you &lt;em&gt;which of your PRs&lt;/em&gt; are driving your team&apos;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; &quot;Did you use AI coding tools in this PR?&quot; 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=&quot;https://github.com/git-ai-project/git-ai&quot;&gt;Git AI&apos;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 &quot;find the right platform.&quot; 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;/p&gt;
&lt;pre&gt;&lt;code&gt;-- AI-assisted PRs
SELECT ... FROM changes c
WHERE c.commit_sha IN (SELECT sha FROM ai_assisted_prs)
-- Human-authored PRs
SELECT ... FROM changes c
WHERE c.commit_sha NOT IN (SELECT sha FROM ai_assisted_prs)
&lt;/code&gt;&lt;/pre&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&apos;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&apos;s also a fair number in a way the industry stat isn&apos;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;aside&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;a href=&quot;https://riftmap.dev/showcase/prometheus/&quot;&gt;Explore the live graph →&lt;/a&gt;
&lt;/aside&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=&quot;https://riftmap.dev/blog/ai-doesnt-understand-blast-radius/&quot;&gt;previous post&lt;/a&gt;, so I&apos;ll keep it brief.&lt;/p&gt;
&lt;p&gt;The patterns that reduce CFR at teams I&apos;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=&quot;https://riftmap.dev/blog/infrastructure-dependency-problem/&quot;&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&apos;t work is adding process layers that slow every change without discriminating by risk. The goal isn&apos;t to slow the agents down; it&apos;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&apos;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&apos;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&apos;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&apos;s a better starting point than any aggregate benchmark from any vendor report.&lt;/p&gt;
&lt;p&gt;I&apos;m building &lt;a href=&quot;https://riftmap.dev&quot;&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=&quot;mailto:daniel@riftmap.dev&quot;&gt;daniel@riftmap.dev&lt;/a&gt;, or try a free scan at &lt;a href=&quot;https://app.riftmap.dev&quot;&gt;app.riftmap.dev&lt;/a&gt;.&lt;/p&gt;
&lt;hr /&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=&quot;https://dora.dev/guides/dora-metrics-four-keys/&quot;&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=&quot;https://dora.dev/research/2025/dora-report/&quot;&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=&quot;https://www.cortex.io/report/engineering-in-the-age-of-ai-2026-benchmark-report&quot;&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=&quot;https://www.swarmia.com/blog/dora-change-failure-rate/&quot;&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=&quot;https://github.com/git-ai-project/git-ai&quot;&gt;github.com/git-ai-project&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Related reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://riftmap.dev/blog/ai-doesnt-understand-blast-radius/&quot;&gt;AI Doesn&apos;t Understand Blast Radius&lt;/a&gt; — Why change failure rates are up 30% and what&apos;s structurally driving it.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://riftmap.dev/blog/ai-coding-agents-need-cross-repo-context/&quot;&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=&quot;https://riftmap.dev/blog/meta-tribal-knowledge-engine-build-the-graph-first/&quot;&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;
</content:encoded><category>dora-metrics</category><category>change-failure-rate</category><category>platform-engineering</category><category>ai-coding</category><category>devops</category><author>Daniel Westgaard</author></item><item><title>AI Doesn&apos;t Understand Blast Radius: Why Change Failure Rates Are Up 30%</title><link>https://riftmap.dev/blog/ai-doesnt-understand-blast-radius/</link><guid isPermaLink="true">https://riftmap.dev/blog/ai-doesnt-understand-blast-radius/</guid><description>AI coding tools optimise for local correctness, but production breaks at the edges of the cross-repo dependency graph they can&apos;t see. A look at the 2025–2026 data — Cortex, DORA, CodeRabbit, and Amazon&apos;s own high-blast-radius memo — and what to put between the agent and main.</description><pubDate>Sun, 19 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;The data is in. Cortex 2026 puts change failure rates up ~30%. DORA 2025 found AI amplifies team weaknesses. Amazon&apos;s own memo called it &quot;high blast radius.&quot; Here&apos;s why, and what to do about it.&lt;/em&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;In March 2026, Amazon&apos;s senior vice-president of eCommerce services sent a briefing note to engineers. The Financial Times got hold of it. The phrase that jumped off the page — and then onto every tech outlet over the next 48 hours — was &quot;high blast radius.&quot;&lt;/p&gt;
&lt;p&gt;The note described a pattern of production incidents at Amazon retail. Some were tied to AI coding tools. One contributing factor, per the internal document, was novel generative-AI usage for which best practices and safeguards were not yet fully established. A separately reported AWS incident from December involved Amazon&apos;s internal AI coding assistant, Kiro, deleting and recreating an entire environment when asked to apply a targeted fix. AWS spent roughly 13 hours getting the service back.&lt;/p&gt;
&lt;p&gt;This is a big-tech company with world-class engineering culture, shipping outages that took customers offline — and the phrase their own leadership reached for was one every DevOps engineer recognises. &lt;em&gt;Blast radius&lt;/em&gt;: the set of things downstream of a change that can fail when the change goes wrong.&lt;/p&gt;
&lt;p&gt;The Amazon story isn&apos;t an outlier. It&apos;s what the data already predicted. The industry now has a name for the version of this that AI coding agents create: &lt;a href=&quot;https://riftmap.dev/ai-agent-blast-radius/&quot;&gt;AI agent blast radius&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;The numbers are in, and they&apos;re not good&lt;/h2&gt;
&lt;p&gt;Three reports published in the last six months agree on the pattern, even though they measure different things.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://www.cortex.io/report/engineering-in-the-age-of-ai-2026-benchmark-report&quot;&gt;Cortex&apos;s 2026 Engineering in the Age of AI Benchmark&lt;/a&gt;.&lt;/strong&gt; Pull requests per author up 20% year-over-year. Incidents per pull request up 23.5%. Change failure rates up roughly 30%. Nearly 90% of engineering leaders say their teams use AI coding tools, but only about a third have formal governance policies in place.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://dora.dev/research/2025/dora-report/&quot;&gt;Google&apos;s 2025 DORA State of AI-assisted Software Development Report&lt;/a&gt;.&lt;/strong&gt; Around 90% AI adoption across respondents, confirming the Cortex number. Roughly 30% of developers say they have little or no trust in AI-generated code. And Google&apos;s own framing of the central finding is blunt: AI is an amplifier, not a repair tool. Teams that already had strong testing and platform foundations saw AI make them faster &lt;em&gt;and&lt;/em&gt; more stable. Teams that didn&apos;t saw the opposite. Across the board, AI adoption showed a negative relationship with delivery stability.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;CodeRabbit&apos;s State of AI vs. Human Code Generation.&lt;/strong&gt; Analysing production pull requests at scale, AI-generated code carried roughly 1.7x the issue rate of human code. Logic errors were worse. Security issues were worse. Performance issues — the kind that don&apos;t trip a test suite but degrade the system — were significantly worse.&lt;/p&gt;
&lt;p&gt;A separate joint study from Sun Yat-sen University and Alibaba put 18 coding agents against 100 real-world codebases over 233 days. Passing tests once was easy. Maintaining a codebase for eight months without breaking it turned out to be the part AI agents fell apart on.&lt;/p&gt;
&lt;p&gt;Stack Overflow&apos;s engineering blog noted in passing that 2025 saw more widespread outages and incident volume than any prior year on record. That&apos;s not solely an AI story — infrastructure keeps getting more complex — but the correlation is hard to ignore.&lt;/p&gt;
&lt;p&gt;So, the top-line: teams are shipping more, faster, and breaking things in ways they don&apos;t always know how to fix. The velocity is real. The rework is also real. And the ratio has gotten worse.&lt;/p&gt;
&lt;h2&gt;What &quot;blast radius&quot; actually means&lt;/h2&gt;
&lt;p&gt;In software and infrastructure, the blast radius of a change is the set of everything downstream that depends on it, directly or transitively, and could break when that change lands. For the full treatment — what blast radius is, how to measure it, and how to contain it across the whole spectrum from one function to a cross-repo artifact edge — see the definitional guide to &lt;a href=&quot;https://riftmap.dev/blog/blast-radius-software-engineering/&quot;&gt;blast radius in software engineering&lt;/a&gt;. This post is the AI-agent case of it: why the agents in particular can&apos;t see the widest band.&lt;/p&gt;
&lt;p&gt;If you edit a function inside a single repo and nothing else in the org imports it, the blast radius is one repo. If you rename a variable in a shared Terraform module, the blast radius is every repo that sources that module at any version where the rename takes effect. If you bump the base tag of a Docker image that&apos;s pulled by 40 Dockerfiles across the org, the blast radius is 40 services and whatever depends on &lt;em&gt;them&lt;/em&gt; at runtime.&lt;/p&gt;
&lt;p&gt;Blast radius isn&apos;t a property of the code you&apos;re editing. It&apos;s a property of the system around the code you&apos;re editing.&lt;/p&gt;
&lt;p&gt;This is why the concept survives so well in post-mortems. When a change breaks production, the question is rarely &quot;did the diff look correct?&quot; It&apos;s almost always &quot;who was consuming this, and did anyone tell them?&quot;&lt;/p&gt;
&lt;aside&gt;
&lt;p&gt;&lt;span&gt;&lt;/span&gt;See it live&lt;/p&gt;
&lt;p&gt;Blast radius is easier to see than to describe&lt;/p&gt;
&lt;p&gt;Here it is as a live, interactive graph: open a real 56-repo org scan, click any repository, and toggle Impact Mode to watch every downstream dependent light up — the exact view an AI coding agent never has.&lt;/p&gt;
&lt;a href=&quot;https://riftmap.dev/showcase/prometheus/&quot;&gt;Explore the live graph →&lt;/a&gt;
&lt;/aside&gt;
&lt;h2&gt;Why AI coding tools can&apos;t see it&lt;/h2&gt;
&lt;p&gt;Here&apos;s the core structural problem: AI coding tools optimise for local correctness.&lt;/p&gt;
&lt;p&gt;They read the file you&apos;re editing. They read files in the same repo, maybe the working set you have open. A capable agent will pull in repository search results, run tests, and iterate. A really capable agent might read the repository&apos;s README and infer some conventions. All of that happens &lt;em&gt;inside a single repository&apos;s boundary&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;The organisation-wide &lt;a href=&quot;https://riftmap.dev/blog/infrastructure-dependency-problem/&quot;&gt;cross-repo dependency graph&lt;/a&gt; — which other repos import this module, which services pull this image, which pipelines include this template — lives outside that boundary. It isn&apos;t in the training data. It isn&apos;t in the agent&apos;s context window. It isn&apos;t in any of the tools the agent has available unless someone has explicitly built those tools and wired them up. This is not specific to one product: I measured &lt;a href=&quot;https://riftmap.dev/blog/which-ai-coding-assistants-see-blast-radius/&quot;&gt;Claude Code, Cursor, and GitHub Copilot against exactly this question&lt;/a&gt;, and none of the three resolves cross-repo blast radius on its own — the limit is structural, not a matter of which assistant you pick.&lt;/p&gt;
&lt;p&gt;To the AI, the code is a text to transform. To the system, the code is a contract with unknown counterparties. Those two framings disagree often enough that Cortex can measure it in the aggregate — a 23.5% lift in incidents per PR — and Amazon can experience it in the specific, as a 13-hour AWS recovery and a mandatory engineering meeting.&lt;/p&gt;
&lt;p&gt;This is why the gap widens as you ship more AI-assisted code. Each individual change looks fine locally. Each change is merged against the same tests it&apos;s always been reviewed against. But the blast radius of each change was never something the tests were measuring in the first place.&lt;/p&gt;
&lt;h2&gt;Why tests don&apos;t save you&lt;/h2&gt;
&lt;p&gt;There&apos;s a reasonable objection at this point: &lt;em&gt;if the tests pass, surely that catches most of it?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;No. And this is one of the more uncomfortable findings of the last year.&lt;/p&gt;
&lt;p&gt;When the same model writes both the feature and the tests, the tests share the model&apos;s assumptions. They exercise the code the way the model already imagined the code would be exercised. Cases the model didn&apos;t anticipate are cases the tests don&apos;t cover. This isn&apos;t hypothetical — it&apos;s been observed empirically by teams measuring AI-authored test coverage against production incidents, and it&apos;s consistent with what the Alibaba long-horizon study found: agents that passed benchmarks on day one fell apart over eight months of real maintenance.&lt;/p&gt;
&lt;p&gt;Tests also run inside a single repository&apos;s CI. They can&apos;t run consumer services that live in other repos. A Terraform module&apos;s unit tests can&apos;t tell you that a staging environment in another repo will fail to plan after your variable rename. A Docker base image&apos;s integration tests can&apos;t tell you that a worker service in another team&apos;s repo pins to the previous tag and will fail its next rebuild.&lt;/p&gt;
&lt;p&gt;Your test suite is a local safety net. Blast radius is an organisational problem. These are different layers, and the industry has not been investing in the organisational one.&lt;/p&gt;
&lt;h2&gt;The three failure modes I see most often&lt;/h2&gt;
&lt;p&gt;I&apos;ve been building &lt;a href=&quot;https://riftmap.dev&quot;&gt;Riftmap&lt;/a&gt; for about a year, which means I&apos;ve spent a lot of time looking at real cross-repo dependency graphs in real orgs. When AI-assisted changes break things, the failure almost always lands in one of three modes.&lt;/p&gt;
&lt;h3&gt;1. The shared library refactor with unknown consumers&lt;/h3&gt;
&lt;p&gt;An engineer asks an AI agent to clean up an internal library — rename a function, tighten a type, remove a deprecated parameter. The agent does it cleanly. The repository&apos;s own tests pass. The PR gets merged.&lt;/p&gt;
&lt;p&gt;Three hours later, two other services fail their next deploy because they were still calling the old signature. Nobody on the originating team knew those services existed. The agent certainly didn&apos;t.&lt;/p&gt;
&lt;p&gt;This is the exact shape of most internal &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-go-module/&quot;&gt;Go module&lt;/a&gt;, Python package, and npm package breakages I see. The library repo&apos;s test coverage was strong. The problem was that &quot;strong test coverage&quot; meant something different from &quot;known consumer coverage.&quot;&lt;/p&gt;
&lt;h3&gt;2. The base image bump with silent pins&lt;/h3&gt;
&lt;p&gt;An engineer asks the agent to upgrade a shared &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-docker-base-image/&quot;&gt;Docker base image&lt;/a&gt; — new language runtime, new security patches. The Dockerfile in the base image repo builds fine. The CI goes green.&lt;/p&gt;
&lt;p&gt;Meanwhile, 30 downstream Dockerfiles across the org pinned to the old tag explicitly, and nobody notified them. Or, worse, they pinned to &lt;code&gt;:latest&lt;/code&gt; and started pulling the new base silently, and three of them have a binary incompatibility with the new glibc. The outages happen at random times over the next week as each service rebuilds.&lt;/p&gt;
&lt;h3&gt;3. The Terraform module rename that breaks plans across the org&lt;/h3&gt;
&lt;p&gt;An engineer refactors a shared VPC or IAM module. The agent renames &lt;code&gt;subnet_ids&lt;/code&gt; to &lt;code&gt;private_subnet_ids&lt;/code&gt; because it&apos;s clearer. The module&apos;s examples still work; its own tests still pass.&lt;/p&gt;
&lt;p&gt;Fourteen other repos source that &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-terraform-module/&quot;&gt;Terraform module&lt;/a&gt; and pass the old argument name. Their next &lt;code&gt;terraform plan&lt;/code&gt; fails. If they were on autopilot pipelines, they might not notice until the next deployment window. If any of them were in the middle of incident response, they now have an extra incident to deal with.&lt;/p&gt;
&lt;p&gt;These three patterns repeat across every ecosystem I&apos;ve looked at — Helm chart value renames, &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-github-actions-workflow/&quot;&gt;GitHub Actions workflow&lt;/a&gt; input changes, reusable CI template variables, Kustomize patch paths, Ansible role parameters. The shape is always the same: the change is correct locally, and the consumers weren&apos;t in the room.&lt;/p&gt;
&lt;h2&gt;What to put between the AI and main&lt;/h2&gt;
&lt;p&gt;The honest answer is that you don&apos;t stop AI-assisted changes from landing in production — that ship has sailed in almost every org that has adopted these tools, and the Cortex and DORA data suggest forcing it into reverse isn&apos;t where the productivity math lands. The question is what you put between the agent and &lt;code&gt;main&lt;/code&gt; so that blast-radius-class mistakes get caught before they cost you a weekend.&lt;/p&gt;
&lt;p&gt;Four things actually help, based on what I see working and what the governance data says is missing at most orgs.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A current, queryable dependency graph across your orgs.&lt;/strong&gt; Not a YAML catalog that humans maintain. Not a nightly grep script. A system that &lt;a href=&quot;https://riftmap.dev/blog/auto-discovering-infrastructure-dependencies-across-10-ecosystems/&quot;&gt;scans your repos and parses the actual manifests&lt;/a&gt;, resolves the consumer-producer relationships, and stays fresh. This is the thing most teams don&apos;t have, and it&apos;s the thing that makes everything else possible.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;PR-time blast radius diffing.&lt;/strong&gt; When an agent opens a pull request against a shared module, image, chart, or CI template, the review surface should include &lt;em&gt;who consumes this, at which version, and which consumers are affected by this specific change&lt;/em&gt;. Not a generic &quot;this module is used a lot&quot; notice. A diff of the consumer impact. This is the single highest-leverage gate you can add.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Automatic downstream owner notification.&lt;/strong&gt; The humans who own the services that depend on the changed artifact need to hear about the change at the moment it&apos;s proposed, not the moment it breaks. Every platform engineering team will tell you this is already their life; formalising it removes the step where the change slips past because the right person was on leave.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A policy split between low-risk and high-risk surface area.&lt;/strong&gt; Amazon&apos;s post-incident response was to require senior sign-off on AI-assisted changes made by junior and mid-level engineers. That&apos;s one blunt answer. A better one, if you have the graph, is: automatic merge allowed for changes with a blast radius under some threshold (single repo, no external consumers), human senior review required for changes that touch shared artifacts with N+ downstream consumers or services tagged as customer-critical. The agent gets the speed where speed is safe. The humans get the attention where attention matters. This split is no longer a proposal — &lt;a href=&quot;https://riftmap.dev/blog/blast-radius-gate-merge-pipeline/&quot;&gt;the CI recipe that implements it&lt;/a&gt; is about forty lines, on GitLab CI or GitHub Actions.&lt;/p&gt;
&lt;p&gt;None of these are novel ideas. The reason most teams haven&apos;t implemented them is that the foundation — the cross-repo dependency graph — doesn&apos;t exist in their org yet. Without it, every other gate is a judgement call made by whoever&apos;s reviewing the PR, and judgement calls scale poorly when PR volume is up 20% year on year.&lt;/p&gt;
&lt;h2&gt;FAQ&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;What is &quot;blast radius&quot; in software engineering?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Blast radius is the set of downstream systems, services, or repos that depend on the thing you&apos;re changing and could fail when the change lands. It&apos;s a property of the dependency graph around your code, not of the code itself. A change to a function nobody else calls has a blast radius of one; a change to a shared Terraform module sourced by 40 repos has a blast radius of 40.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Does AI coding raise change failure rate?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Yes, in the aggregate, based on every major 2025–2026 report that has measured it. The Cortex 2026 Benchmark puts change failure rate up about 30% and incidents per PR up 23.5% since AI adoption accelerated. Google&apos;s 2025 DORA report found AI has a negative relationship with software delivery stability overall, with the worst effects in teams that lacked strong testing and platform foundations before adoption. CodeRabbit measured roughly 1.7x the defect rate in AI-generated code versus human-written code in production pull requests.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why does AI-generated code break in production more often than human-written code?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Three structural reasons. First, AI tools optimise for local correctness within a single repository and can&apos;t see the organisation-wide dependency graph — which repos import this module, which services pull this image. Second, AI-authored tests share the same blind spots as AI-authored code, so passing tests doesn&apos;t mean the change is safe. Third, review processes haven&apos;t scaled to match PR volume, and larger PRs with less review time let more blast-radius-class defects through.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What&apos;s a &quot;high blast radius&quot; incident?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;It&apos;s an incident whose effects reach far beyond the immediate change — taking out multiple services, customer-facing endpoints, or critical paths rather than a single component. The phrase entered mainstream tech press when &lt;a href=&quot;https://www.cio.com/article/4143381/amazon-is-linking-site-hiccups-to-ai-efforts-2.html&quot;&gt;Amazon used it in an internal March 2026 briefing note&lt;/a&gt; about a pattern of production incidents tied partly to AI-assisted changes. The contrast is with a low-blast-radius incident that affects one service and is contained quickly.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How do you stop AI-generated changes from breaking production?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;You can&apos;t eliminate the risk, but you can gate it. The most effective controls are: maintain a current cross-repo dependency graph so you know who consumes what; surface consumer impact as part of the PR review for changes to shared artifacts; automatically notify downstream owners when upstream artifacts change; and apply stricter review requirements (human senior sign-off, staged rollouts, canaries) to changes whose blast radius exceeds a defined threshold.&lt;/p&gt;

&lt;h2&gt;Closing&lt;/h2&gt;
&lt;p&gt;A lot of the public conversation about AI coding has settled into two camps: evangelists who think the velocity gains speak for themselves, and sceptics who think the whole thing is a productivity mirage. The data is more interesting than either position. AI coding tools genuinely make individual engineers faster. They also increase the rate at which changes break things in production. Both are true at once, and the second effect is largest in exactly the area the tools are structurally unable to see — the cross-repo dependency graph.&lt;/p&gt;
&lt;p&gt;The Cortex report frames this as a governance gap. The DORA report frames it as an amplification effect. Amazon&apos;s post-incident response framed it as a blast-radius problem and a review policy problem. They&apos;re all describing the same hole in the stack.&lt;/p&gt;
&lt;p&gt;If you maintain shared infrastructure artifacts — Terraform modules, Docker base images, Helm charts, CI templates, Go modules, anything others consume — the single most useful thing you can do in 2026 is build the visibility layer that your AI coding tools and your CI pipelines don&apos;t have. Know who consumes what. Know what this change affects. Make it a PR-time artifact, not a tribal-knowledge artifact.&lt;/p&gt;
&lt;p&gt;That&apos;s what I&apos;m building &lt;a href=&quot;https://riftmap.dev&quot;&gt;Riftmap&lt;/a&gt; to do: auto-discover cross-repo dependencies across your GitLab or GitHub org — Terraform, Docker, CI templates, Helm, Go, npm, Python, Ansible, Kubernetes, Kustomize — and give you a queryable graph with visual blast radius analysis. One read-only token. No per-repo YAML to maintain.&lt;/p&gt;
&lt;p&gt;If you&apos;re feeling this problem at your org, I&apos;d genuinely like to hear how it shows up. Reach me at &lt;a href=&quot;mailto:daniel@riftmap.dev&quot;&gt;daniel@riftmap.dev&lt;/a&gt;, or try a free scan at &lt;a href=&quot;https://app.riftmap.dev&quot;&gt;app.riftmap.dev&lt;/a&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Sources referenced&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Cortex, &lt;em&gt;Engineering in the Age of AI: 2026 Benchmark Report&lt;/em&gt; — &lt;a href=&quot;https://www.cortex.io/report/engineering-in-the-age-of-ai-2026-benchmark-report&quot;&gt;cortex.io&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=&quot;https://dora.dev/research/2025/dora-report/&quot;&gt;dora.dev&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;CodeRabbit, &lt;em&gt;State of AI vs. Human Code Generation Report&lt;/em&gt; (2025)&lt;/li&gt;
&lt;li&gt;Sun Yat-sen University &amp;amp; Alibaba, long-horizon benchmark of 18 AI coding agents on 100 codebases over 233 days (2026)&lt;/li&gt;
&lt;li&gt;Financial Times reporting (March 2026) on Amazon&apos;s &quot;high blast radius&quot; internal memo, covered in &lt;a href=&quot;https://www.cio.com/article/4143381/amazon-is-linking-site-hiccups-to-ai-efforts-2.html&quot;&gt;CIO.com&lt;/a&gt;, Fortune, Tom&apos;s Hardware, and Business Insider&lt;/li&gt;
&lt;li&gt;Stack Overflow engineering blog on 2025 outage volume&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Related reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://riftmap.dev/blog/ai-coding-agents-need-cross-repo-context/&quot;&gt;AI coding agents need cross-repo context&lt;/a&gt; — Three teams just shipped the same diagnosis: agents need a queryable cross-repo dependency graph as runtime infrastructure.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://riftmap.dev/blog/meta-tribal-knowledge-engine-build-the-graph-first/&quot;&gt;Meta needed 50+ AI agents to map their tribal knowledge&lt;/a&gt; — How Meta&apos;s 50-agent effort came down to a single cross-repo dependency index.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://riftmap.dev/blog/measure-your-change-failure-rate/&quot;&gt;Change failure rate is up 30% — here&apos;s how to measure yours in an afternoon&lt;/a&gt; — Practitioner&apos;s guide to calculating your own CFR, with the AI-assisted vs human-authored cut.&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>ai-coding</category><category>platform-engineering</category><category>infrastructure</category><category>dependency-management</category><category>devops</category><category>blast-radius</category><category>change-failure-rate</category><author>Daniel Westgaard</author></item><item><title>How to Find Every Consumer of Your Go Module</title><link>https://riftmap.dev/blog/how-to-find-every-consumer-of-your-go-module/</link><guid isPermaLink="true">https://riftmap.dev/blog/how-to-find-every-consumer-of-your-go-module/</guid><description>You maintain an internal Go module. A breaking API change is coming. Which repos across your org import it — and at which version? Here&apos;s why the answer is harder than it should be.</description><pubDate>Fri, 17 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;You maintain a shared Go module. A breaking API change is coming. Which repos across your org import it — and at which version?&lt;/em&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;You own &lt;code&gt;github.com/company/platform-lib&lt;/code&gt;. Maybe it&apos;s your internal observability library that every service imports for structured logging and trace propagation. Maybe it&apos;s a shared HTTP middleware package that standardises authentication, rate limiting, and request IDs across all your APIs. Maybe it&apos;s a configuration loader that services use to pull settings from your config management system at startup.&lt;/p&gt;
&lt;p&gt;It started as a few shared utilities to avoid copy-pasting. Twenty services adopted it. Then more. Some import it directly and call into it heavily. Some pull it in transitively because another shared library depends on it. Some are on the latest version. Some are three majors behind.&lt;/p&gt;
&lt;p&gt;Now you need to change it. Maybe you&apos;re removing a deprecated function that&apos;s been generating noise in the godoc for two years. Maybe you&apos;re bumping to &lt;code&gt;v2&lt;/code&gt; because you need to restructure the package API in a backwards-incompatible way. Maybe you&apos;re changing the signature of a middleware constructor to accept an options struct instead of positional arguments.&lt;/p&gt;
&lt;p&gt;The question is the same one that runs through &lt;a href=&quot;https://riftmap.dev/blog/series/find-every-consumer/&quot;&gt;every post in this series&lt;/a&gt;: &lt;strong&gt;which repos across the org import this module, and at which version?&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;The scenario&lt;/h2&gt;
&lt;p&gt;Here is what Go module consumption looks like in practice. Your platform team publishes a module, and other repos declare it in their &lt;code&gt;go.mod&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// service-a/go.mod
module github.com/company/service-a

go 1.22

require (
    github.com/company/platform-lib v1.8.3
    github.com/company/auth-middleware v2.1.0
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Some repos import the module directly and use it throughout their code. Others pull it in as an indirect dependency — because a library they depend on imports your module — and Go records it with an &lt;code&gt;// indirect&lt;/code&gt; comment:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;require (
    github.com/company/platform-lib v1.8.3
    github.com/company/some-other-lib v0.4.1 // indirect
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Some repos are pinned to a specific release tag. Some reference a pseudo-version pointing to a specific commit:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;github.com/company/platform-lib v0.0.0-20260301153042-a1b2c3d4e5f6
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Some have overridden the module source entirely with a &lt;code&gt;replace&lt;/code&gt; directive — pointing to a local checkout or a patched fork:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;replace github.com/company/platform-lib =&amp;gt; ../platform-lib
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;All of these represent consumption of your module. None of them look the same. And right now, they live in different repos, owned by different teams, that you probably can&apos;t enumerate off the top of your head.&lt;/p&gt;
&lt;h2&gt;What existing tools give you (and where they stop)&lt;/h2&gt;
&lt;h3&gt;pkg.go.dev&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;pkg.go.dev&lt;/code&gt; is the official Go package documentation and discovery site. For public modules, it indexes which packages import a given package and shows an importer list. This is the closest existing tool to &quot;who uses this?&quot;&lt;/p&gt;
&lt;p&gt;The limitation is sharp: &lt;code&gt;pkg.go.dev&lt;/code&gt; only indexes public modules fetched through the public Go module proxy at &lt;code&gt;proxy.golang.org&lt;/code&gt;. If your module is hosted on an internal GitLab or GitHub instance, protected by authentication, and routed through a private &lt;code&gt;GOPROXY&lt;/code&gt;, it does not appear in &lt;code&gt;pkg.go.dev&lt;/code&gt; at all. Every consumer in your org is invisible to it.&lt;/p&gt;
&lt;p&gt;For most platform teams managing internal shared libraries, this makes &lt;code&gt;pkg.go.dev&lt;/code&gt; entirely useless for the question at hand. Even for public modules, the importer list only covers packages that &lt;code&gt;pkg.go.dev&lt;/code&gt; has itself indexed — not a comprehensive count of every organisation that imports the module.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;go mod graph&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;go mod graph&lt;/code&gt; command prints the full module dependency graph for the current module — a recursive list of every direct and transitive dependency. It&apos;s useful for understanding what a single module pulls in.&lt;/p&gt;
&lt;p&gt;But it only works outward from the current module. There is no &lt;code&gt;go mod reverse-graph&lt;/code&gt;. You&apos;d have to clone every repo in the org, run &lt;code&gt;go mod graph&lt;/code&gt; in each one, collect the output, and invert it yourself — which is exactly the manual trawl the question is trying to avoid. And the moment you finish, the results are stale.&lt;/p&gt;
&lt;h3&gt;Renovate and Dependabot&lt;/h3&gt;
&lt;p&gt;Both Renovate and Dependabot support Go modules as a first-class ecosystem. They detect &lt;code&gt;require&lt;/code&gt; entries in &lt;code&gt;go.mod&lt;/code&gt; and open pull requests when newer versions are published — which means they implicitly know which repos consume which modules.&lt;/p&gt;
&lt;p&gt;But they don&apos;t expose that knowledge as a queryable view. You cannot ask Renovate or Dependabot &quot;show me every repo in the org that imports &lt;code&gt;platform-lib&lt;/code&gt;, and what version each is on.&quot; They react to new versions being published. The reverse — who&apos;s consuming the current version before you publish a breaking one — is not something they surface.&lt;/p&gt;
&lt;p&gt;Both tools also only cover repos where they&apos;ve been configured. A team that hasn&apos;t set up Go module support in their config, or doesn&apos;t use either tool at all, is invisible.&lt;/p&gt;
&lt;h3&gt;GitHub code search&lt;/h3&gt;
&lt;p&gt;You can search for your module path across all repos in an org:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;org:my-org &quot;github.com/company/platform-lib&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This finds &lt;code&gt;go.mod&lt;/code&gt; files that contain the module path and gives you a list of repos. For a one-off audit, it&apos;s a workable starting point.&lt;/p&gt;
&lt;p&gt;The familiar problems apply: results don&apos;t include version numbers without opening each file individually, the search index lags behind recent commits, and it doesn&apos;t account for &lt;code&gt;replace&lt;/code&gt; directives. If a repo has replaced your module with a fork or local path, the &lt;code&gt;require&lt;/code&gt; entry still appears — but what&apos;s actually being compiled is different. Code search can&apos;t tell you that.&lt;/p&gt;
&lt;h3&gt;Athens (GOPROXY)&lt;/h3&gt;
&lt;p&gt;Athens is a widely-used open-source Go module proxy that organisations run internally for private modules and caching. It serves module downloads and maintains a local cache.&lt;/p&gt;
&lt;p&gt;Athens tracks which modules were fetched and at which version. What it doesn&apos;t track is which &lt;em&gt;source repo&lt;/em&gt; triggered the download. A build server fetching &lt;code&gt;platform-lib v1.8.3&lt;/code&gt; is recorded as a download event, but not which team&apos;s CI pipeline it was, which service repo it was building, or which &lt;code&gt;go.mod&lt;/code&gt; declared the dependency. Athens is a supply-chain cache, not a consumption graph. This is the same gap every private registry has — the npm version of it, where a download event is not the &lt;code&gt;package.json&lt;/code&gt; that declared the dependency, gets &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-internal-npm-package/&quot;&gt;its own post in this series&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Why this is harder than it looks&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Major version suffixes create distinct module identities.&lt;/strong&gt; Go&apos;s module system treats &lt;code&gt;github.com/company/platform-lib&lt;/code&gt; and &lt;code&gt;github.com/company/platform-lib/v2&lt;/code&gt; as completely separate modules. A repo that imports &lt;code&gt;platform-lib/v2&lt;/code&gt; has a different dependency than one that imports &lt;code&gt;platform-lib&lt;/code&gt;. Grepping for &lt;code&gt;platform-lib&lt;/code&gt; finds both — but a tool that tracks module identities needs to recognise that these are different versions of the same thing, and that consumers may be on any combination of &lt;code&gt;v1&lt;/code&gt;, &lt;code&gt;v2&lt;/code&gt;, &lt;code&gt;v3&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This also means publishing a new major version doesn&apos;t automatically affect v1 consumers. They continue importing the old module path until they explicitly update both the &lt;code&gt;go.mod&lt;/code&gt; entry and all import paths in their source code. Understanding who&apos;s still on v1 requires knowing who&apos;s importing which module path — and treating those as a unified dependency family.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;replace&lt;/code&gt; directives obscure the real source.&lt;/strong&gt; A &lt;code&gt;replace&lt;/code&gt; directive can substitute your module with a different source — a local path, a fork, or a different module entirely:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;replace github.com/company/platform-lib =&amp;gt; github.com/company/platform-lib-patched v1.8.3-hotfix
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;From the outside, the &lt;code&gt;require&lt;/code&gt; entry still references &lt;code&gt;github.com/company/platform-lib&lt;/code&gt;. A scanner that reads only &lt;code&gt;require&lt;/code&gt; entries sees this as a consumer on v1.8.3. But the code being compiled is coming from a patched fork. The apparent consumer is not actually running your module — and a tool that reports it as one gives you a misleading picture of who&apos;s affected by a change to the canonical source.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Indirect dependencies inflate the consumer list.&lt;/strong&gt; Go&apos;s &lt;code&gt;go.mod&lt;/code&gt; includes both direct and indirect dependencies, with &lt;code&gt;// indirect&lt;/code&gt; annotations on the latter. If your module is a transitive dependency — imported by a library that&apos;s imported by a service — it appears in that service&apos;s &lt;code&gt;go.mod&lt;/code&gt; even though the service&apos;s code never directly calls into yours.&lt;/p&gt;
&lt;p&gt;This matters when reasoning about impact. A service that directly imports your module and calls its functions will break if you change the API. A service that has it only as an indirect dependency may not be affected at all, depending on how much of your interface the intermediate library exposes. Treating all &lt;code&gt;go.mod&lt;/code&gt; entries as equivalent overstates the blast radius; ignoring indirect entries understates it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;go.work&lt;/code&gt; workspaces cross module boundaries.&lt;/strong&gt; Go 1.18 introduced workspace mode, where a &lt;code&gt;go.work&lt;/code&gt; file at the root of a checkout declares multiple local modules as part of a unified workspace. When workspace mode is active, dependencies between modules in the &lt;code&gt;go.work&lt;/code&gt; are resolved locally rather than through &lt;code&gt;go.mod&lt;/code&gt; &lt;code&gt;require&lt;/code&gt; declarations.&lt;/p&gt;
&lt;p&gt;Some consumers of your module may be developing against it in a shared workspace, with no explicit &lt;code&gt;require&lt;/code&gt; entry in their &lt;code&gt;go.mod&lt;/code&gt; during development. A scanner that only reads &lt;code&gt;go.mod&lt;/code&gt; files misses these entirely.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Vendor directories shadow the dependency graph.&lt;/strong&gt; Repos that vendor their dependencies with &lt;code&gt;go mod vendor&lt;/code&gt; keep a copy of all transitive dependencies under &lt;code&gt;vendor/&lt;/code&gt;, with canonical version information in &lt;code&gt;vendor/modules.txt&lt;/code&gt;. Some repos modify vendored code directly — technically discouraged, but not uncommon in practice. These modifications break the assumption that a version string in &lt;code&gt;go.mod&lt;/code&gt; corresponds exactly to the canonical upstream source. A consumer may appear to be on v1.8.3 while actually running a locally modified version of it.&lt;/p&gt;
&lt;aside&gt;
&lt;p&gt;&lt;span&gt;&lt;/span&gt;See it live&lt;/p&gt;
&lt;p&gt;Every importer of a Go module, live&lt;/p&gt;
&lt;p&gt;This is the graph&apos;s home turf — the Prometheus scan is mostly Go. Open it, focus client_golang, and watch all 25 repos that import it fan out at once, each with its go.mod file and line. That&apos;s &quot;find every consumer,&quot; rendered.&lt;/p&gt;
&lt;a href=&quot;https://riftmap.dev/showcase/prometheus/&quot;&gt;Explore the live graph →&lt;/a&gt;
&lt;/aside&gt;
&lt;h2&gt;What the full answer requires&lt;/h2&gt;
&lt;p&gt;To reliably answer &quot;who imports this module,&quot; you need a system that:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Scans every repo in the org&lt;/strong&gt;, parsing &lt;code&gt;go.mod&lt;/code&gt; files — without requiring opt-in or registration from each team&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Handles all reference forms&lt;/strong&gt; — tagged versions, pseudo-versions, and &lt;code&gt;replace&lt;/code&gt; directives — and records what module source is actually being resolved, not just what appears in &lt;code&gt;require&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tracks all major version suffixes&lt;/strong&gt; as distinct but related module identities, so you see the full picture across &lt;code&gt;v1&lt;/code&gt;, &lt;code&gt;v2&lt;/code&gt;, &lt;code&gt;v3&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Distinguishes direct from indirect dependencies&lt;/strong&gt;, so you know which consumers call into your module directly and which pull it in transitively through another library&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Follows transitive edges&lt;/strong&gt; to show second-order consumers: repos that depend on your module through an intermediate shared library&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Keeps results current&lt;/strong&gt; through regular rescans rather than a one-time snapshot&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This is one of the specific problems &lt;a href=&quot;https://riftmap.dev&quot;&gt;Riftmap&lt;/a&gt; is built to solve. It connects to your GitHub or GitLab organisation, scans every repo, and parses &lt;code&gt;go.mod&lt;/code&gt; files to extract module dependencies — including all &lt;code&gt;require&lt;/code&gt; entries, &lt;code&gt;replace&lt;/code&gt; directives, and version strings. It resolves module paths across major version suffixes and builds a cross-repo dependency graph you can query by module.&lt;/p&gt;
&lt;p&gt;The result is the same kind of view described throughout this series: before you &lt;a href=&quot;https://riftmap.dev/blog/deprecate-internal-library-find-consumers/&quot;&gt;remove a function&lt;/a&gt;, bump a major version, or change a constructor signature, you open the graph, click the module, and see every repo that imports it — their version, whether they&apos;re a direct or indirect consumer, and which teams own them. You know who&apos;ll break. You know who to notify. You know who&apos;s still on v1 and will need a migration path before v2 ships.&lt;/p&gt;
&lt;p&gt;No manual &lt;code&gt;go mod graph&lt;/code&gt; inversions across dozens of repos. No org-wide code search with manual version lookups. No waiting to see whose CI goes red after you merge.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This is the fifth post in the &lt;a href=&quot;https://riftmap.dev/blog/series/find-every-consumer/&quot;&gt;Find Every Consumer&lt;/a&gt; series. Previous posts cover &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-docker-base-image/&quot;&gt;Docker base images&lt;/a&gt;, &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-terraform-module/&quot;&gt;Terraform modules&lt;/a&gt;, &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-github-actions-workflow/&quot;&gt;GitHub Actions workflows&lt;/a&gt;, and &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-helm-chart/&quot;&gt;Helm charts&lt;/a&gt;. Next up: &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-gitlab-ci-template/&quot;&gt;GitLab CI templates&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;If this is a problem your platform team deals with, I&apos;d be interested to hear how you&apos;re solving it today. You can find more at &lt;a href=&quot;https://riftmap.dev&quot;&gt;riftmap.dev&lt;/a&gt; or reach me at &lt;a href=&quot;mailto:daniel@riftmap.dev&quot;&gt;daniel@riftmap.dev&lt;/a&gt;.&lt;/p&gt;
</content:encoded><category>go</category><category>golang</category><category>platform-engineering</category><category>devops</category><category>dependency-management</category><category>modules</category><author>Daniel Westgaard</author></item><item><title>How to Find Every Consumer of Your Helm Chart</title><link>https://riftmap.dev/blog/how-to-find-every-consumer-of-your-helm-chart/</link><guid isPermaLink="true">https://riftmap.dev/blog/how-to-find-every-consumer-of-your-helm-chart/</guid><description>You maintain a shared Helm chart. A breaking value rename or API version bump is coming. Which deployments across your org depend on it — and at which version? Here&apos;s why the answer is harder than it should be.</description><pubDate>Thu, 16 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;You maintain a shared Helm chart. A breaking change is coming. Which teams across the org are running it — and at which version?&lt;/em&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;You own a shared Helm chart. Maybe it&apos;s a &lt;code&gt;platform-services&lt;/code&gt; chart that bundles your observability stack: Prometheus, Alertmanager, and a custom metrics exporter your platform team maintains. Maybe it&apos;s an &lt;code&gt;ingress-gateway&lt;/code&gt; chart that every product team deploys at the edge of their namespace. Maybe it&apos;s a company-wide chart for spinning up a standard service with your logging sidecar, network policies, and PodDisruptionBudget pre-configured.&lt;/p&gt;
&lt;p&gt;It started as a convenience. Teams stopped reinventing the same Kubernetes manifests. A dozen clusters adopted it. Then more. Some teams deploy it via ArgoCD. Some use Flux. Some have &lt;code&gt;helm install&lt;/code&gt; calls baked into their CI pipelines. Some consume it as a subchart inside their own umbrella chart. Some use a plain &lt;code&gt;Chart.yaml&lt;/code&gt; with a &lt;code&gt;dependencies:&lt;/code&gt; block and run &lt;code&gt;helm dependency update&lt;/code&gt; before deploy.&lt;/p&gt;
&lt;p&gt;Now you need to change it. Maybe a value key is being renamed — &lt;code&gt;replicaCount&lt;/code&gt; is becoming &lt;code&gt;replicas&lt;/code&gt; to align with upstream conventions. Maybe you&apos;re dropping support for a Kubernetes API version that was removed in 1.32. Maybe you&apos;re restructuring the chart into subcharts to make individual components opt-in rather than all-or-nothing.&lt;/p&gt;
&lt;p&gt;The question is the same one it always is: &lt;strong&gt;which repos across the org consume this chart, and at which version?&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;The scenario&lt;/h2&gt;
&lt;p&gt;Here is what Helm chart consumption looks like in practice. Your platform team publishes a chart to an internal chart repository or OCI registry. Consumers reference it in a few different ways:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;As a standalone &lt;code&gt;helm install&lt;/code&gt;:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;helm install platform-services oci://registry.company.com/helm-charts/platform-services --version 3.2.1 -n monitoring
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;As a dependency in another chart&apos;s &lt;code&gt;Chart.yaml&lt;/code&gt;:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# some-service/chart/Chart.yaml
dependencies:
  - name: platform-services
    version: &quot;~3.2.0&quot;
    repository: &quot;https://charts.company.com&quot;
    alias: monitoring
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;As an ArgoCD &lt;code&gt;Application&lt;/code&gt; manifest:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# gitops/apps/some-team/monitoring.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
spec:
  source:
    chart: platform-services
    repoURL: https://charts.company.com
    targetRevision: 3.2.1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;As a Flux &lt;code&gt;HelmRelease&lt;/code&gt; CRD:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# gitops/releases/monitoring.yaml
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
spec:
  chart:
    spec:
      chart: platform-services
      version: &quot;&amp;gt;=3.0.0 &amp;lt;4.0.0&quot;
      sourceRef:
        kind: HelmRepository
        name: company-charts
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;All four patterns express the same dependency. None of them look the same in source code. And right now, they probably live in different repos, written by different teams, using different GitOps tooling.&lt;/p&gt;
&lt;h2&gt;What existing tools give you (and where they stop)&lt;/h2&gt;
&lt;h3&gt;Helm itself&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;helm&lt;/code&gt; CLI knows about releases deployed to a cluster. &lt;code&gt;helm list -A&lt;/code&gt; shows every release across all namespaces in a given cluster, including which chart and version is installed. &lt;code&gt;helm history &amp;lt;release&amp;gt;&lt;/code&gt; shows the version history for a specific release.&lt;/p&gt;
&lt;p&gt;This is useful once you have access to the cluster. The limitation is that it&apos;s cluster-scoped and live-state-only. You can see what&apos;s running right now, but it tells you nothing about &lt;em&gt;source repos&lt;/em&gt; — which repo contains the &lt;code&gt;values.yaml&lt;/code&gt; driving the release, which team owns it, or whether the same chart is deployed to a different cluster you don&apos;t have access to. Most companies have at least three clusters (dev, staging, prod), and many have dozens. You&apos;d need access to all of them, and even then you&apos;d have a list of running releases, not a map of source-code consumers.&lt;/p&gt;
&lt;h3&gt;Artifact Hub and Chart Museums&lt;/h3&gt;
&lt;p&gt;Artifact Hub is the public index for Helm charts. It tracks download counts and shows chart metadata, but it&apos;s a discovery tool for chart &lt;em&gt;users&lt;/em&gt;, not a graph of chart &lt;em&gt;consumers&lt;/em&gt;. If your chart is published internally — to a private chart repository or an OCI registry — it doesn&apos;t appear in Artifact Hub at all. And even for public charts, neither Artifact Hub nor any chart museum stores information about which orgs or repos have installed a particular chart version.&lt;/p&gt;
&lt;h3&gt;ArgoCD and Flux&lt;/h3&gt;
&lt;p&gt;If your org has committed fully to GitOps and every Helm release is declared as an ArgoCD &lt;code&gt;Application&lt;/code&gt; or a Flux &lt;code&gt;HelmRelease&lt;/code&gt; manifest in a Git repo, then in theory all your consumers are visible in those manifests. The problem is &quot;in theory.&quot; In practice:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Not every team has migrated to GitOps. Some still run &lt;code&gt;helm upgrade&lt;/code&gt; in CI pipelines or from engineer laptops.&lt;/li&gt;
&lt;li&gt;The GitOps manifests may live in many different repos — a central gitops repo, per-team repos, per-environment repos — rather than a single queryable location.&lt;/li&gt;
&lt;li&gt;ArgoCD&apos;s UI shows you the apps it&apos;s managing, but querying &quot;show me every app that references chart &lt;code&gt;platform-services&lt;/code&gt;&quot; is not a built-in view. You&apos;d need to iterate through the Application CRDs or query the Argo CD API.&lt;/li&gt;
&lt;li&gt;Flux doesn&apos;t have a central query surface for &quot;which &lt;code&gt;HelmRelease&lt;/code&gt; objects across all clusters reference chart X at version Y&quot; — that information is spread across &lt;code&gt;HelmRelease&lt;/code&gt; YAML files in whatever repos your GitOps setup uses.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Renovate&lt;/h3&gt;
&lt;p&gt;Renovate supports Helm chart version bumps as a first-class feature. Configured in a repo, it detects chart references in &lt;code&gt;Chart.yaml&lt;/code&gt; dependencies, ArgoCD Application manifests, and Flux HelmRelease CRDs, and opens pull requests when new chart versions are published.&lt;/p&gt;
&lt;p&gt;As with Terraform and GitHub Actions: Renovate &lt;em&gt;must&lt;/em&gt; know who consumes what in order to open those PRs. But it doesn&apos;t expose that knowledge as a queryable view. You can&apos;t ask Renovate &quot;which repos reference my chart and at which version?&quot; It reacts after a new version is published. The consumer list before you publish a breaking change is not something it surfaces.&lt;/p&gt;
&lt;p&gt;Renovate also only covers repos where it has been configured. A team that hasn&apos;t added Helm to their Renovate config — or doesn&apos;t use Renovate at all — is invisible.&lt;/p&gt;
&lt;h3&gt;Grep&lt;/h3&gt;
&lt;p&gt;Clone every repo. Search for the chart name:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;grep -r &quot;platform-services&quot; --include=&quot;*.yaml&quot; --include=&quot;*.yml&quot; ~/repos/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This finds direct name references in YAML files. It doesn&apos;t distinguish between chart references and random YAML comments, doesn&apos;t extract version constraints, misses OCI-format references where the chart name is embedded in a registry URL, and ignores any &lt;code&gt;helm install&lt;/code&gt; calls in shell scripts or Makefile targets. And as always: the results are stale by the next commit.&lt;/p&gt;
&lt;h2&gt;Why this is harder than it looks&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Reference formats are highly inconsistent.&lt;/strong&gt; A Helm chart can be referenced via a traditional HTTP chart repository, an OCI registry, or a local path. These look nothing alike:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Traditional repository (in Chart.yaml dependencies)
repository: &quot;https://charts.company.com&quot;

# OCI registry (in Chart.yaml dependencies, Helm 3.8+)
repository: &quot;oci://registry.company.com/helm-charts&quot;

# OCI in a Flux HelmRepository
spec:
  type: oci
  url: oci://registry.company.com/helm-charts

# Inline OCI reference in a helm CLI call
helm install platform-services oci://registry.company.com/helm-charts/platform-services --version 3.2.1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Resolving all of these back to the same chart identity requires knowing how your registry namespaces charts and how your chart repositories map to OCI paths — out-of-band information that no grep can recover.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Version constraints vary widely.&lt;/strong&gt; Unlike Docker image &lt;code&gt;FROM&lt;/code&gt; statements (which usually pin to a specific tag) or Terraform module &lt;code&gt;ref=&lt;/code&gt; pins, Helm consumers regularly use semver constraints:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;version: &quot;~3.2.0&quot;      # patch-compatible only: &amp;gt;=3.2.0 &amp;lt;3.3.0
version: &quot;^3.0.0&quot;      # minor-compatible: &amp;gt;=3.0.0 &amp;lt;4.0.0
version: &quot;&amp;gt;=3.0.0 &amp;lt;4&quot;  # explicit range
version: &quot;*&quot;           # any version (dangerous)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A consumer pinned to &lt;code&gt;^3.0.0&lt;/code&gt; will automatically receive your &lt;code&gt;3.3.0&lt;/code&gt; release — they don&apos;t have to explicitly upgrade. A consumer on &lt;code&gt;~3.2.0&lt;/code&gt; won&apos;t. Understanding who will be affected by a minor version bump versus a major bump requires evaluating constraints, not just comparing version strings.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Umbrella charts create transitive consumers.&lt;/strong&gt; An umbrella chart is a Helm chart whose primary purpose is to bundle multiple subcharts together. If &lt;code&gt;team-a&lt;/code&gt; publishes an umbrella chart &lt;code&gt;team-a-stack&lt;/code&gt; that includes your &lt;code&gt;platform-services&lt;/code&gt; chart as a dependency, and &lt;code&gt;team-b&lt;/code&gt; deploys &lt;code&gt;team-a-stack&lt;/code&gt; — then &lt;code&gt;team-b&lt;/code&gt; is a transitive consumer of &lt;code&gt;platform-services&lt;/code&gt; through &lt;code&gt;team-a-stack&lt;/code&gt;. A breaking change to &lt;code&gt;platform-services&lt;/code&gt; affects &lt;code&gt;team-b&lt;/code&gt;, but a search for &lt;code&gt;platform-services&lt;/code&gt; in their repos finds nothing.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GitOps and imperative installs coexist.&lt;/strong&gt; In most organisations, some teams manage Helm releases declaratively (ArgoCD, Flux) and others do it imperatively (&lt;code&gt;helm install&lt;/code&gt; or &lt;code&gt;helm upgrade&lt;/code&gt; in CI pipelines, shell scripts, or Makefiles). The declarative installs leave traces in Git repos as YAML manifests. The imperative installs may leave no trace in source code at all — just a live release visible in &lt;code&gt;helm list&lt;/code&gt;. A complete consumer map needs to combine both sources.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Chart aliases break name matching.&lt;/strong&gt; A &lt;code&gt;Chart.yaml&lt;/code&gt; dependency can declare an &lt;code&gt;alias&lt;/code&gt;, which changes the name used to refer to the subchart in values overrides:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dependencies:
  - name: platform-services
    version: &quot;~3.2.0&quot;
    repository: &quot;https://charts.company.com&quot;
    alias: monitoring   # the chart is installed as &quot;monitoring&quot;, not &quot;platform-services&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A naive search for &lt;code&gt;platform-services&lt;/code&gt; in values files will find nothing, because the values are nested under &lt;code&gt;monitoring:&lt;/code&gt;. Tools that rely on name matching alone miss aliased consumers entirely.&lt;/p&gt;
&lt;aside&gt;
&lt;p&gt;&lt;span&gt;&lt;/span&gt;See it live&lt;/p&gt;
&lt;p&gt;See chart consumers as a graph&lt;/p&gt;
&lt;p&gt;Chart-to-chart and kustomize references are exactly the kind of cross-repo edge the graph captures. Open the live Prometheus scan, click any node, and its Dependents tab shows every consumer with the manifest and line behind each one.&lt;/p&gt;
&lt;a href=&quot;https://riftmap.dev/showcase/prometheus/&quot;&gt;Explore the live graph →&lt;/a&gt;
&lt;/aside&gt;
&lt;h2&gt;What the full answer requires&lt;/h2&gt;
&lt;p&gt;To reliably answer &quot;who consumes this chart,&quot; you need a system that:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Scans every repo in the org&lt;/strong&gt;, including GitOps repos, service repos, and platform repos — without requiring registration or manual catalog entries&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Parses all reference formats&lt;/strong&gt; — &lt;code&gt;Chart.yaml&lt;/code&gt; dependencies, ArgoCD Application manifests, Flux HelmRelease CRDs, and &lt;code&gt;helm install&lt;/code&gt; calls in CI configs and shell scripts&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Resolves OCI and HTTP registry references&lt;/strong&gt; back to a canonical chart identity, so consumers using different registry URL formats for the same chart are recognised as the same dependency&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Evaluates version constraints&lt;/strong&gt; rather than just matching exact version strings, so you know which consumers are within range of your next release&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Follows umbrella chart transitive dependencies&lt;/strong&gt;, so second-order consumers are visible, not just direct chart references&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Keeps results current&lt;/strong&gt; through regular rescans, not a one-time manual trawl&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This is one of the specific problems &lt;a href=&quot;https://riftmap.dev&quot;&gt;Riftmap&lt;/a&gt; is built to solve. It connects to your GitHub or GitLab organisation, scans every repo, and extracts chart references from &lt;code&gt;Chart.yaml&lt;/code&gt; dependency blocks, ArgoCD Application and ApplicationSet manifests, Flux HelmRelease CRDs, and &lt;code&gt;helm&lt;/code&gt; calls in CI configs. It resolves OCI and HTTP registry URLs to a unified chart identity and builds a cross-repo dependency graph you can query by chart name and version.&lt;/p&gt;
&lt;p&gt;The result is the same kind of view described in the earlier posts in this series: before you rename a value key or drop a Kubernetes API version, you open the graph, click the chart, and see every consumer — their version constraint, the repo that owns the deployment declaration, and which constraint ranges include your next release. You know who&apos;ll automatically receive the change, who&apos;s pinned to an older range, and who you need to contact before shipping.&lt;/p&gt;
&lt;p&gt;No cluster access required. No cross-org Slack ping. No grep marathon across forty repos.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This is the fourth post in the &lt;a href=&quot;https://riftmap.dev/blog/series/find-every-consumer/&quot;&gt;Find Every Consumer&lt;/a&gt; series. Previous posts cover &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-docker-base-image/&quot;&gt;Docker base images&lt;/a&gt;, &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-terraform-module/&quot;&gt;Terraform modules&lt;/a&gt;, and &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-github-actions-workflow/&quot;&gt;GitHub Actions workflows&lt;/a&gt;. Next up: Go modules.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;If this is a problem your platform team deals with, I&apos;d be interested to hear how you&apos;re solving it today. You can find more at &lt;a href=&quot;https://riftmap.dev&quot;&gt;riftmap.dev&lt;/a&gt; or reach me at &lt;a href=&quot;mailto:daniel@riftmap.dev&quot;&gt;daniel@riftmap.dev&lt;/a&gt;.&lt;/p&gt;
</content:encoded><category>helm</category><category>kubernetes</category><category>platform-engineering</category><category>devops</category><category>dependency-management</category><category>gitops</category><author>Daniel Westgaard</author></item><item><title>How to Find Every Consumer of Your Reusable GitHub Actions Workflow</title><link>https://riftmap.dev/blog/how-to-find-every-consumer-of-your-github-actions-workflow/</link><guid isPermaLink="true">https://riftmap.dev/blog/how-to-find-every-consumer-of-your-github-actions-workflow/</guid><description>You maintain a shared GitHub Actions workflow. You need to rename an input, drop a step, or change a required secret. Which repos across your org call it — and at which ref? Here&apos;s why the answer is harder than it should be.</description><pubDate>Wed, 15 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;You maintain a shared GitHub Actions workflow. You need to rename an input, drop a step, or change a required secret. Which repos across your org call it? Here&apos;s why the answer is harder than it should be.&lt;/em&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;You maintain a reusable GitHub Actions workflow. Maybe it&apos;s &lt;code&gt;deploy-to-eks.yml&lt;/code&gt; in your platform team&apos;s &lt;code&gt;.github&lt;/code&gt; repo, or a &lt;code&gt;ci-lint-test.yml&lt;/code&gt; that standardises how every service in the org runs its CI pipeline. It started as a way to stop every team from copy-pasting the same 80-line workflow file. It worked. Twenty repos adopted it. Then forty. Then you lost count.&lt;/p&gt;
&lt;p&gt;Now you need to change it. Maybe you&apos;re replacing a deprecated action (&lt;code&gt;actions/setup-node@v4&lt;/code&gt; is being forced to Node 24 and you need to bump to &lt;code&gt;v6&lt;/code&gt;). Maybe you&apos;re renaming an input from &lt;code&gt;node-version&lt;/code&gt; to &lt;code&gt;runtime-version&lt;/code&gt; because the workflow now supports Bun and Deno too. Maybe you&apos;re removing a step that uploaded artifacts to a bucket that no longer exists.&lt;/p&gt;
&lt;p&gt;The question is the same one that comes up for every shared infrastructure artifact: &lt;strong&gt;which repos across our org call this workflow, and at which ref?&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;The scenario&lt;/h2&gt;
&lt;p&gt;Here&apos;s what a typical reusable workflow consumption looks like. Your platform team publishes a workflow in a central repository, and other repos call it:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# In some-service/.github/workflows/ci.yml
jobs:
  test:
    uses: my-org/platform-workflows/.github/workflows/ci-lint-test.yml@v2.1.0
    with:
      node-version: &quot;20&quot;
    secrets: inherit
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Some repos pin to a tag. Some pin to a branch like &lt;code&gt;main&lt;/code&gt;. Some pin to a full commit SHA for supply-chain security. Some don&apos;t pin at all and reference &lt;code&gt;@main&lt;/code&gt;, which means they&apos;re calling whatever version is on HEAD right now.&lt;/p&gt;
&lt;p&gt;You need to find all of them, understand which version each one uses, and figure out which will break when you rename that input. Right now, most teams do this by searching in GitHub, asking in Slack, or just pushing the change and waiting for workflows to fail. None of these are a real answer.&lt;/p&gt;
&lt;h2&gt;What existing tools give you (and where they stop)&lt;/h2&gt;
&lt;h3&gt;GitHub code search&lt;/h3&gt;
&lt;p&gt;GitHub&apos;s code search is the most natural place to look. You can search across all repos in an org:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;org:my-org &quot;uses: my-org/platform-workflows/.github/workflows/ci-lint-test.yml&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This works for finding direct string matches. For a one-off audit, it&apos;s often good enough. But it has real limitations:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It doesn&apos;t extract the ref. You can see that a repo calls the workflow, but the &lt;code&gt;@v2.1.0&lt;/code&gt; or &lt;code&gt;@main&lt;/code&gt; part requires manually opening each result and reading the YAML.&lt;/li&gt;
&lt;li&gt;Results are point-in-time. If you&apos;re planning a breaking change, you need to know who&apos;s calling the workflow &lt;em&gt;now&lt;/em&gt; — not when the search index was last updated.&lt;/li&gt;
&lt;li&gt;The search index can lag. GitHub&apos;s code search is eventually consistent. Repos that were recently updated might not show current results immediately.&lt;/li&gt;
&lt;li&gt;It doesn&apos;t handle indirection. If a repo has a &quot;wrapper&quot; workflow that calls your reusable workflow, and other workflows call the wrapper, GitHub code search only finds the direct callers.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Dependabot&lt;/h3&gt;
&lt;p&gt;Dependabot supports GitHub Actions as a first-class ecosystem. In a repo&apos;s &lt;code&gt;dependabot.yml&lt;/code&gt;, you can configure it to watch for action version updates:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;updates:
  - package-ecosystem: &quot;github-actions&quot;
    directory: &quot;/&quot;
    schedule:
      interval: &quot;weekly&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Dependabot will then open pull requests when actions referenced in the repo&apos;s workflows have new versions available. This includes reusable workflows referenced with &lt;code&gt;uses:&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The same pattern as Renovate for Terraform: Dependabot &lt;em&gt;implicitly&lt;/em&gt; knows who consumes what, because it&apos;s configured per-repo. But it doesn&apos;t expose this as a queryable view. You can&apos;t ask Dependabot &quot;which repos in my org call &lt;code&gt;platform-workflows/ci-lint-test.yml&lt;/code&gt;?&quot; It&apos;s a version updater, not a dependency mapper.&lt;/p&gt;
&lt;p&gt;There&apos;s an additional limitation: Dependabot only monitors repos where it&apos;s been explicitly enabled. If a team hasn&apos;t added &lt;code&gt;github-actions&lt;/code&gt; to their &lt;code&gt;dependabot.yml&lt;/code&gt; — or doesn&apos;t have a &lt;code&gt;dependabot.yml&lt;/code&gt; at all — their workflow dependencies are invisible.&lt;/p&gt;
&lt;h3&gt;Renovate&lt;/h3&gt;
&lt;p&gt;Renovate also supports GitHub Actions workflows, including reusable workflow references. It can detect &lt;code&gt;uses:&lt;/code&gt; directives in workflow files and open PRs when new versions are available.&lt;/p&gt;
&lt;p&gt;Renovate has a slight edge over Dependabot here: it can be configured org-wide via a shared preset, so you don&apos;t need every repo to opt in individually. But the core limitation is identical — it&apos;s a dependency &lt;em&gt;updater&lt;/em&gt;, not a dependency &lt;em&gt;mapper&lt;/em&gt;. It reacts when a new version exists. It doesn&apos;t give you the blast radius before you publish the change.&lt;/p&gt;
&lt;h3&gt;GitHub API&lt;/h3&gt;
&lt;p&gt;The GitHub API lets you list workflows and workflow runs across repositories. You can enumerate all repos in an org, fetch their workflow files, and parse the YAML yourself.&lt;/p&gt;
&lt;p&gt;This is essentially building a custom scanner. The API gives you the raw data, but assembling it into an answer requires:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Paginating through every repo in the org&lt;/li&gt;
&lt;li&gt;Fetching the contents of every &lt;code&gt;.github/workflows/*.yml&lt;/code&gt; file&lt;/li&gt;
&lt;li&gt;Parsing YAML to extract &lt;code&gt;uses:&lt;/code&gt; directives&lt;/li&gt;
&lt;li&gt;Filtering for your specific reusable workflow&lt;/li&gt;
&lt;li&gt;Extracting the ref from each reference&lt;/li&gt;
&lt;li&gt;Handling rate limits (the GitHub API is aggressive about rate limiting for large orgs)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Teams that go this route end up building a script, running it periodically, and storing the results somewhere. It works. It&apos;s also a project you now have to maintain, and the results are stale between runs.&lt;/p&gt;
&lt;h3&gt;Grep&lt;/h3&gt;
&lt;p&gt;Clone everything, grep for the workflow reference:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;grep -r &quot;platform-workflows/.github/workflows/ci-lint-test.yml&quot; --include=&quot;*.yml&quot; ~/repos/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Same trade-offs as always: works once, stale immediately, doesn&apos;t extract versions, doesn&apos;t catch transitive calls, and at 100+ repos it&apos;s slow enough that nobody does it proactively.&lt;/p&gt;
&lt;h2&gt;Why this is harder than it looks&lt;/h2&gt;
&lt;p&gt;Reusable workflow references look simpler than Terraform module sources or Docker image references — there&apos;s really only one syntax. But the complexity is in what surrounds that syntax.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Ref types vary widely.&lt;/strong&gt; The &lt;code&gt;@ref&lt;/code&gt; portion of a workflow call can be a tag, a branch, or a full SHA:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Tag — the common case
uses: my-org/platform-workflows/.github/workflows/deploy.yml@v2.1.0

# Branch — consuming HEAD, inherently unstable
uses: my-org/platform-workflows/.github/workflows/deploy.yml@main

# SHA — supply-chain hardened, but opaque
uses: my-org/platform-workflows/.github/workflows/deploy.yml@a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A tag tells you the version. A branch tells you almost nothing — the consumer is on whatever commit &lt;code&gt;main&lt;/code&gt; happened to point to when they last ran. A SHA is precise but requires resolving the commit to a tag or branch to understand which &lt;em&gt;version&lt;/em&gt; the consumer is actually using. Answering &quot;who&apos;s still on v1.x?&quot; requires understanding all three forms.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Reusable workflows vs. composite actions.&lt;/strong&gt; GitHub Actions has two mechanisms for sharing CI logic: reusable workflows (called with &lt;code&gt;uses:&lt;/code&gt; at the &lt;em&gt;job&lt;/em&gt; level) and composite actions (called with &lt;code&gt;uses:&lt;/code&gt; at the &lt;em&gt;step&lt;/em&gt; level). They look similar in YAML but behave differently:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Reusable workflow — job level
jobs:
  deploy:
    uses: my-org/platform-workflows/.github/workflows/deploy.yml@v2

# Composite action — step level
jobs:
  build:
    steps:
      - uses: my-org/platform-actions/setup-toolchain@v3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Both are dependencies on shared code in other repos. Both break when the interface changes. A consumer tracking system needs to find both, because platform teams often maintain a mix of reusable workflows and composite actions — and the same &quot;who&apos;s using this?&quot; question applies to both.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The &lt;code&gt;.github&lt;/code&gt; repository.&lt;/strong&gt; GitHub has a special convention: a repo named &lt;code&gt;.github&lt;/code&gt; in an org can contain workflow templates that appear in every repo&apos;s &quot;Actions&quot; tab. If your platform team publishes starter workflows through the &lt;code&gt;.github&lt;/code&gt; repo, those templates get copied (not referenced) into consuming repos. Once copied, they&apos;re no longer linked to the source — there&apos;s no &lt;code&gt;uses:&lt;/code&gt; directive pointing back, just a disconnected copy that may or may not have been modified since.&lt;/p&gt;
&lt;p&gt;This means some of your &quot;consumers&quot; are invisible. They took the template, modified it, and now maintain their own fork. You can&apos;t find them by searching for &lt;code&gt;uses:&lt;/code&gt; directives because there aren&apos;t any.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Inputs and secrets are the breaking surface.&lt;/strong&gt; Unlike Terraform modules, where the interface is defined by variables and outputs with explicit types, reusable workflow inputs are loosely typed. A workflow declares its inputs:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;on:
  workflow_call:
    inputs:
      node-version:
        required: true
        type: string
    secrets:
      DEPLOY_KEY:
        required: true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you rename &lt;code&gt;node-version&lt;/code&gt; to &lt;code&gt;runtime-version&lt;/code&gt;, every caller that passes &lt;code&gt;node-version&lt;/code&gt; will silently fail — the input just won&apos;t be received, and the workflow will use its default or break downstream. There&apos;s no compile-time error. The failure shows up at runtime, in the middle of a CI run, and the error message often doesn&apos;t clearly point to the renamed input.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Workflow call chains.&lt;/strong&gt; Reusable workflows can call other reusable workflows, up to ten levels deep and fifty total workflow calls per run (GitHub increased these limits in late 2025). If workflow A calls workflow B, which calls workflow C, and you change C&apos;s interface, both A and B are affected — but only B directly references C. Finding A requires resolving the full call chain.&lt;/p&gt;
&lt;aside&gt;
&lt;p&gt;&lt;span&gt;&lt;/span&gt;See it live&lt;/p&gt;
&lt;p&gt;Every workflow consumer, resolved&lt;/p&gt;
&lt;p&gt;Reusable-workflow and action references scatter across hundreds of YAML files. The live graph resolves them to repos: open the Prometheus scan, click a node, and the Dependents tab is your consumer list — no org-wide grep required.&lt;/p&gt;
&lt;a href=&quot;https://riftmap.dev/showcase/prometheus/&quot;&gt;Explore the live graph →&lt;/a&gt;
&lt;/aside&gt;
&lt;h2&gt;What the full answer requires&lt;/h2&gt;
&lt;p&gt;To reliably answer &quot;who consumes this reusable workflow,&quot; you need a system that:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Scans every repo in the org&lt;/strong&gt;, parsing all &lt;code&gt;.github/workflows/*.yml&lt;/code&gt; files — not just repos that have opted into Dependabot or Renovate&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Extracts &lt;code&gt;uses:&lt;/code&gt; directives&lt;/strong&gt; at both the job level (reusable workflows) and step level (composite actions), because platform teams typically maintain both&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Resolves ref types&lt;/strong&gt; — tags, branches, and SHAs — to a normalised version, so you can answer &quot;who&apos;s still on v1.x?&quot; regardless of how the ref was specified&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Follows workflow call chains&lt;/strong&gt; so you see transitive consumers, not just direct callers&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Keeps the graph current&lt;/strong&gt; through regular rescans, not one-off audits&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Makes the result queryable&lt;/strong&gt;: &quot;show me every consumer of &lt;code&gt;ci-lint-test.yml&lt;/code&gt;, grouped by ref, with the team that owns each calling repo&quot;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This is one of the specific problems &lt;a href=&quot;https://riftmap.dev&quot;&gt;Riftmap&lt;/a&gt; is built to solve. It scans a GitHub (or GitLab) org, parses every workflow file in every repo, extracts all &lt;code&gt;uses:&lt;/code&gt; directives — including reusable workflow calls, composite action references, and standard action pins — and builds a cross-repo dependency graph that you can query by artifact.&lt;/p&gt;
&lt;p&gt;The result: before you rename that input or remove that step, you open the graph, click the workflow, and see exactly which repos call it and which ref each one pins to. You know who&apos;ll break. You know who to notify. You know who&apos;s pinned to &lt;code&gt;main&lt;/code&gt; and will see the change immediately versus who&apos;s on a tagged release and has time to migrate.&lt;/p&gt;
&lt;p&gt;No org-wide grep. No stale GitHub code search. No &quot;let&apos;s push it and see whose CI goes red.&quot;&lt;/p&gt;
&lt;p&gt;For the cross-platform treatment of the change moment itself — why editing a shared reusable workflow is two deployments at once, instant to the &lt;code&gt;@main&lt;/code&gt; callers and silent to the ones pinned to a tag, and why CI has no deprecation signal to warn either group — see &lt;a href=&quot;https://riftmap.dev/blog/changing-shared-ci-template-two-deployments/&quot;&gt;You changed the shared CI template. Half the org got it instantly. The other half will never see it&lt;/a&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This is the third post in the &lt;a href=&quot;https://riftmap.dev/blog/series/find-every-consumer/&quot;&gt;Find Every Consumer&lt;/a&gt; series. Previous posts cover &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-docker-base-image/&quot;&gt;Docker base images&lt;/a&gt; and &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-terraform-module/&quot;&gt;Terraform modules&lt;/a&gt;. Next up: &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-helm-chart/&quot;&gt;Helm charts&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;If this is a problem your platform team deals with, I&apos;d be interested to hear how you&apos;re solving it today. You can find more at &lt;a href=&quot;https://riftmap.dev&quot;&gt;riftmap.dev&lt;/a&gt; or reach me at &lt;a href=&quot;mailto:daniel@riftmap.dev&quot;&gt;daniel@riftmap.dev&lt;/a&gt;.&lt;/p&gt;
</content:encoded><category>github-actions</category><category>platform-engineering</category><category>devops</category><category>dependency-management</category><author>Daniel Westgaard</author></item><item><title>How to Find Every Consumer of Your Terraform Module</title><link>https://riftmap.dev/blog/how-to-find-every-consumer-of-your-terraform-module/</link><guid isPermaLink="true">https://riftmap.dev/blog/how-to-find-every-consumer-of-your-terraform-module/</guid><description>When you need to make a breaking change to a shared Terraform module, which repos are affected? Here&apos;s why the answer is harder than it should be.</description><pubDate>Tue, 14 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;You maintain a shared Terraform module. A breaking change is coming. Which repos will it hit?&lt;/em&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;You own &lt;code&gt;terraform-modules/networking&lt;/code&gt;. Or &lt;code&gt;infra/modules/vpc&lt;/code&gt;. Or a module published to your internal Terraform registry. Dozens of teams call it. Some pin to a specific version. Some track a branch. Some you haven&apos;t heard from in months.&lt;/p&gt;
&lt;p&gt;Now you need to remove a variable, rename an output, or restructure the resource hierarchy. The question is the same one it always is: &lt;strong&gt;which repos across the org depend on this module, and at which version?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This should be a five-second lookup. It isn&apos;t.&lt;/p&gt;
&lt;h2&gt;The scenario&lt;/h2&gt;
&lt;p&gt;Your platform team publishes a shared Terraform module. Across the org, other repos call it in their root modules or in other shared modules:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;module &quot;vpc&quot; {
  source  = &quot;git::https://gitlab.company.com/infra/terraform-modules.git//networking?ref=v3.2.0&quot;
  cidr    = &quot;10.0.0.0/16&quot;
  regions = [&quot;eu-west-1&quot;]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Some teams pin to a tag like &lt;code&gt;v3.2.0&lt;/code&gt;. Some track &lt;code&gt;main&lt;/code&gt;. Some reference a specific commit SHA for reproducibility. Some pull the module from your internal Terraform registry. Some use Terragrunt, where the source is declared in a &lt;code&gt;terragrunt.hcl&lt;/code&gt; file rather than a &lt;code&gt;.tf&lt;/code&gt; file at all.&lt;/p&gt;
&lt;p&gt;You need to find all of them before you merge that breaking change — not after.&lt;/p&gt;
&lt;h2&gt;What existing tools give you (and where they stop)&lt;/h2&gt;
&lt;h3&gt;HCP Terraform Explorer&lt;/h3&gt;
&lt;p&gt;HashiCorp Cloud Platform&apos;s Module Explorer is the closest thing to a native solution. It builds a graph of module relationships across your HCP Terraform workspaces, showing which workspaces call which modules and at which version.&lt;/p&gt;
&lt;p&gt;The limitation is the boundary: it only knows about workspaces registered in HCP Terraform. If your org runs Atlantis, the open-source Terraform runner, or manages state in S3 without using HCP Terraform at all, Module Explorer has no visibility into those workspaces. It also maps workspace-to-module relationships, not &lt;em&gt;source-repo&lt;/em&gt;-to-module relationships — which means you know which HCP workspace is affected, but not which engineering team owns it or which Git repo to open a PR against.&lt;/p&gt;
&lt;h3&gt;Renovate&lt;/h3&gt;
&lt;p&gt;Renovate can detect module &lt;code&gt;source&lt;/code&gt; blocks in &lt;code&gt;.tf&lt;/code&gt; files and open pull requests when a newer version is published to a registry. In that sense, it implicitly knows which repos use which modules — it has to, in order to know where to open PRs.&lt;/p&gt;
&lt;p&gt;But Renovate doesn&apos;t surface this as a queryable view. You can&apos;t ask it: &quot;before I publish this breaking version, show me every repo that will receive a PR.&quot; It reacts after a new version appears. The blast radius before you publish is not something it answers.&lt;/p&gt;
&lt;h3&gt;Atlantis&lt;/h3&gt;
&lt;p&gt;Atlantis tracks which repos it manages and which Terraform workspaces they contain, but it operates per pull request. It applies plans and shows you the output for a given PR, not a cross-repo dependency graph. There&apos;s no built-in way to query &quot;which repos across all projects call this module.&quot;&lt;/p&gt;
&lt;h3&gt;Grep&lt;/h3&gt;
&lt;p&gt;The fallback. Clone every repo, run &lt;code&gt;grep -r &quot;terraform-modules.git//networking&quot;&lt;/code&gt;, collect results. It works once, the results are stale by the next commit, and it misses the cases described below.&lt;/p&gt;
&lt;h2&gt;Why this is harder than it looks&lt;/h2&gt;
&lt;p&gt;Terraform modules can be referenced in at least four structurally different ways, each of which requires a different parsing strategy.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Git sources with subpaths and refs:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;source = &quot;git::https://gitlab.company.com/infra/terraform-modules.git//networking/vpc?ref=v3.2.0&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;//&lt;/code&gt; separates the repo from the subpath. The &lt;code&gt;?ref=&lt;/code&gt; is the version pin. Grepping for the module name finds this. Grepping for the version doesn&apos;t, if someone parameterises it with a local variable.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Internal Terraform registry sources:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;source  = &quot;registry.company.com/infra/networking/aws&quot;
version = &quot;~&amp;gt; 3.0&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This looks nothing like a Git URL. It&apos;s a registry source with a semver constraint. Connecting this back to a Git repo requires knowing how your registry resolves module sources — which is out-of-band information no grep can recover.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Relative paths between modules:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;source = &quot;../../modules/networking&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Common in monorepos or when a module calls a sub-module. Resolving this dependency requires knowing the directory structure of the calling repo and normalising the path. A cross-repo grep for the module name won&apos;t find this at all.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Terragrunt:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Terragrunt wraps Terraform and declares module sources in &lt;code&gt;terragrunt.hcl&lt;/code&gt; instead of &lt;code&gt;.tf&lt;/code&gt; files:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;terraform {
  source = &quot;git::https://gitlab.company.com/infra/terraform-modules.git//networking?ref=v3.2.0&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is syntactically similar to HCL but lives in a different file entirely. Any tool that only scans &lt;code&gt;.tf&lt;/code&gt; files misses every Terragrunt-based consumer.&lt;/p&gt;
&lt;p&gt;Beyond source formats, there&apos;s the &lt;strong&gt;transitive dependency problem&lt;/strong&gt;. If Module A depends on your networking module, and Module B depends on Module A, a breaking change to your module cascades through two layers. A list of direct consumers isn&apos;t enough if you need to know the full blast radius before shipping.&lt;/p&gt;
&lt;p&gt;There&apos;s a subtler trap hiding in the &lt;code&gt;version&lt;/code&gt; attribute itself. A pin like &lt;code&gt;&amp;gt;= 0.3.0&lt;/code&gt;, or a &lt;code&gt;git::...?ref=main&lt;/code&gt; source with no &lt;code&gt;version&lt;/code&gt; line at all, isn&apos;t a fact about what&apos;s running — it&apos;s a permission, and treating it as a fact is how an automated change lands on a repo nobody pointed it at. I pulled apart &lt;a href=&quot;https://riftmap.dev/blog/version-constraints-across-real-terraform-estates/&quot;&gt;what version constraints actually look like across real Terraform estates&lt;/a&gt; — measured per corpus — in a companion post.&lt;/p&gt;
&lt;aside&gt;
&lt;p&gt;&lt;span&gt;&lt;/span&gt;See it live&lt;/p&gt;
&lt;p&gt;Every module consumer, in one pane&lt;/p&gt;
&lt;p&gt;The hard part of this post — tracing module source references back to the repos that use them — is exactly what the graph does continuously. Open the live Prometheus scan, click a node, and read its consumers off the Dependents tab instead of grepping across repos.&lt;/p&gt;
&lt;a href=&quot;https://riftmap.dev/showcase/prometheus/&quot;&gt;Explore the live graph →&lt;/a&gt;
&lt;/aside&gt;
&lt;h2&gt;What the full answer requires&lt;/h2&gt;
&lt;p&gt;To reliably answer &quot;who consumes this module,&quot; you need a system that:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Scans every repo in the org&lt;/strong&gt; without requiring registration or manual catalog entries&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Parses &lt;code&gt;.tf&lt;/code&gt; and &lt;code&gt;terragrunt.hcl&lt;/code&gt; files&lt;/strong&gt; so consumers of either style are captured&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Resolves all source formats&lt;/strong&gt; — Git URLs, registry sources, and relative paths — back to an identifiable module identity&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tracks version pins and constraints&lt;/strong&gt; so you know which consumers are already on the new version and which still need updating&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Follows transitive edges&lt;/strong&gt; so you can see second- and third-order dependents, not just direct callers&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Keeps results current&lt;/strong&gt; through scheduled rescans rather than a one-time snapshot&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This is one of the specific problems &lt;a href=&quot;https://riftmap.dev&quot;&gt;Riftmap&lt;/a&gt; is built to solve. It connects to your GitLab or GitHub organisation, clones every repo, and runs static analysis across &lt;code&gt;.tf&lt;/code&gt; files, &lt;code&gt;terragrunt.hcl&lt;/code&gt; files, and CI configs. It resolves module sources against your org&apos;s internal registry, your Git hosting, and public registries, then builds a cross-repo dependency graph you can query by module.&lt;/p&gt;
&lt;p&gt;The result is the same kind of view described in the &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-docker-base-image/&quot;&gt;Docker base image post&lt;/a&gt; in this series: click on the module in the graph, see every repo that depends on it, what version each one uses, and who owns it. Direct consumers and transitive dependents, not just a grepped list.&lt;/p&gt;
&lt;p&gt;If you&apos;re making a change to a shared Terraform module and want to know the blast radius before you merge, that&apos;s the answer the tooling currently lacks — and what this graph makes available without a manual trawl through hundreds of repos.&lt;/p&gt;
&lt;p&gt;You can try it at &lt;a href=&quot;https://riftmap.dev&quot;&gt;riftmap.dev&lt;/a&gt;, or reach me at &lt;a href=&quot;mailto:hello@riftmap.dev&quot;&gt;hello@riftmap.dev&lt;/a&gt; if you want to talk through your setup.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This is the second post in the &lt;a href=&quot;https://riftmap.dev/blog/series/find-every-consumer/&quot;&gt;Find Every Consumer&lt;/a&gt; series. Previous post covers &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-docker-base-image/&quot;&gt;Docker base images&lt;/a&gt;. Next up: &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-github-actions-workflow/&quot;&gt;GitHub Actions workflows&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</content:encoded><category>terraform</category><category>platform-engineering</category><category>devops</category><category>dependency-management</category><category>infrastructure</category><category>blast-radius</category><category>modules</category><author>Daniel Westgaard</author></item><item><title>How to Find Every Consumer of Your Docker Base Image</title><link>https://riftmap.dev/blog/how-to-find-every-consumer-of-your-docker-base-image/</link><guid isPermaLink="true">https://riftmap.dev/blog/how-to-find-every-consumer-of-your-docker-base-image/</guid><description>When a CVE hits your Docker base image, which repos are affected? Here&apos;s why the answer is harder than it should be, and what a real solution requires.</description><pubDate>Mon, 13 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;You maintain a shared base image. A CVE drops. Which repos are affected? Here&apos;s why the answer is harder than it should be.&lt;/em&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;You maintain an internal Docker base image. Maybe it&apos;s &lt;code&gt;platform/node-base&lt;/code&gt; or &lt;code&gt;company/python-runtime&lt;/code&gt;. A dozen repos use it. Or thirty. Or you&apos;re not sure how many, because nobody&apos;s counted since the last team reorganisation.&lt;/p&gt;
&lt;p&gt;Then a critical CVE hits the base OS layer, and you need to push a patched version. The question that matters is simple: &lt;strong&gt;which repos across our org pull this image, and at which version?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This should be easy to answer. It isn&apos;t.&lt;/p&gt;
&lt;h2&gt;The scenario&lt;/h2&gt;
&lt;p&gt;Here&apos;s what this looks like in practice. Your platform team builds and publishes &lt;code&gt;registry.company.com/platform/base-image&lt;/code&gt;. Across the org, Dockerfiles reference it:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;FROM registry.company.com/platform/base-image:v2.1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Some repos pin to a specific tag. Some use &lt;code&gt;latest&lt;/code&gt;. Some have &lt;code&gt;ARG&lt;/code&gt;-parameterized &lt;code&gt;FROM&lt;/code&gt; statements where the tag is injected at build time. Some reference the image not in a Dockerfile but in a &lt;code&gt;docker-compose.yml&lt;/code&gt; or a CI pipeline config.&lt;/p&gt;
&lt;p&gt;You need to find all of them, check which version each one uses, and coordinate the update. Right now, most teams do this by grepping, asking on Slack, or checking CI logs. None of these give you a complete, current answer.&lt;/p&gt;
&lt;h2&gt;What existing tools give you (and where they stop)&lt;/h2&gt;
&lt;p&gt;Several tools address parts of the Docker base image problem. Each one is useful. None of them answer the full question.&lt;/p&gt;
&lt;h3&gt;Docker Scout&lt;/h3&gt;
&lt;p&gt;Docker Scout analyzes a &lt;em&gt;built image&lt;/em&gt; and tells you what base image it uses, whether that base is outdated, and what vulnerabilities it contains. &lt;code&gt;docker scout recommendations&lt;/code&gt; will suggest updated base images with fewer CVEs.&lt;/p&gt;
&lt;p&gt;This is valuable, but it works per-image, not per-org. It answers &quot;what base image does &lt;em&gt;this&lt;/em&gt; image use?&quot; It doesn&apos;t answer &quot;which of my 200 repos use &lt;em&gt;this&lt;/em&gt; base image?&quot; You&apos;d need to run Scout against every image in your registry and correlate the results back to source repos, which is a project in itself.&lt;/p&gt;
&lt;h3&gt;Renovate&lt;/h3&gt;
&lt;p&gt;Renovate detects &lt;code&gt;FROM&lt;/code&gt; statements in Dockerfiles and can open pull requests when a newer tag is available. It &lt;em&gt;implicitly&lt;/em&gt; knows who consumes what, because it&apos;s configured per-repo and parses the Dockerfiles.&lt;/p&gt;
&lt;p&gt;But Renovate doesn&apos;t expose this as a queryable view. You can&apos;t ask Renovate &quot;show me every repo that references &lt;code&gt;platform/base-image&lt;/code&gt;.&quot; It reacts when a new version appears. It doesn&apos;t give you the pre-release blast radius: before you push the patched image, which repos and teams will need to update?&lt;/p&gt;
&lt;h3&gt;Backstage / Roadie Tech Insights&lt;/h3&gt;
&lt;p&gt;Roadie (a managed Backstage provider) has a Tech Insights feature that can parse Dockerfiles via regex, extract base image versions, and create scorecards tracking migration progress. This is the closest existing solution to the consumer-tracking problem.&lt;/p&gt;
&lt;p&gt;The limitation is that it requires Backstage to be set up with &lt;code&gt;catalog-info.yaml&lt;/code&gt; per repo. You&apos;re tracking Docker base images through a service catalog that depends on manual registration. If a repo isn&apos;t in the catalog, its Dockerfile is invisible.&lt;/p&gt;
&lt;h3&gt;Container registries&lt;/h3&gt;
&lt;p&gt;Your registry (Docker Hub, Harbor, ACR, ECR) knows which images have been &lt;em&gt;pulled&lt;/em&gt; and how often. Some provide pull statistics by tag. But pull logs don&apos;t tell you which &lt;em&gt;source repo&lt;/em&gt; initiated the pull. A CI pipeline pulling &lt;code&gt;base-image:v2.1&lt;/code&gt; shows up as a pull event, not as &quot;repo &lt;code&gt;frontend-api&lt;/code&gt; depends on this image via its Dockerfile on line 3.&quot;&lt;/p&gt;
&lt;h3&gt;Grep&lt;/h3&gt;
&lt;p&gt;The fallback everyone uses. Clone all the repos, run &lt;code&gt;grep -r &quot;platform/base-image&quot;&lt;/code&gt;, assemble the results. It works once. The results are stale immediately. It misses parameterized &lt;code&gt;FROM&lt;/code&gt; statements, compose files, and CI configs. And at a hundred repos, it takes long enough that nobody does it proactively.&lt;/p&gt;
&lt;h2&gt;Why this is harder than it looks&lt;/h2&gt;
&lt;p&gt;The core difficulty is that Docker base image dependencies live in multiple places and multiple formats.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Dockerfiles&lt;/strong&gt; are the obvious source, but &lt;code&gt;FROM&lt;/code&gt; statements can use &lt;code&gt;ARG&lt;/code&gt; substitution:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ARG BASE_TAG=v2.1
FROM registry.company.com/platform/base-image:${BASE_TAG}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A simple grep for the image name finds this. A simple grep for the &lt;em&gt;version&lt;/em&gt; doesn&apos;t, because the version is in a variable.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Docker Compose files&lt;/strong&gt; reference images differently:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;services:
  api:
    image: registry.company.com/platform/base-image:v2.1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is a dependency on the same image, declared in a completely different file format.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;CI pipeline configs&lt;/strong&gt; often pull images directly:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;build:
  image: registry.company.com/platform/base-image:v2.1
  script:
    - make build
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A GitLab CI &lt;code&gt;image:&lt;/code&gt; directive or a GitHub Actions &lt;code&gt;container:&lt;/code&gt; field is another consumption point for the same base image, and it&apos;s in yet another file.&lt;/p&gt;
&lt;p&gt;Then there&apos;s the &lt;strong&gt;producer-side problem&lt;/strong&gt;. Knowing which repos &lt;em&gt;consume&lt;/em&gt; the image is half the story. You also need to know which repo &lt;em&gt;builds&lt;/em&gt; it. That&apos;s usually a CI pipeline with &lt;code&gt;docker build&lt;/code&gt; and &lt;code&gt;docker push&lt;/code&gt; commands, not something declared in a manifest. Connecting the consumer graph to the producer requires cross-referencing multiple file types within and across repos.&lt;/p&gt;
&lt;p&gt;No single tool today connects all of these surfaces into one view.&lt;/p&gt;
&lt;aside&gt;
&lt;p&gt;&lt;span&gt;&lt;/span&gt;See it live&lt;/p&gt;
&lt;p&gt;This is the &quot;Dependents&quot; view, live&lt;/p&gt;
&lt;p&gt;Riftmap parses every Dockerfile base image and manifest import across an org and resolves them to repos. Open the live Prometheus scan, click any node, and its Dependents tab lists every consumer — with the file and line behind each edge.&lt;/p&gt;
&lt;a href=&quot;https://riftmap.dev/showcase/prometheus/&quot;&gt;Explore the live graph →&lt;/a&gt;
&lt;/aside&gt;
&lt;h2&gt;What the full answer requires&lt;/h2&gt;
&lt;p&gt;To reliably answer &quot;who consumes this base image,&quot; you need a system that:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Scans every repo in the org&lt;/strong&gt;, not just the ones registered in a catalog&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Parses Dockerfiles, Compose files, and CI configs&lt;/strong&gt;, because the same image can be referenced in all three&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Handles variable substitution&lt;/strong&gt; in &lt;code&gt;FROM&lt;/code&gt; statements by resolving &lt;code&gt;ARG&lt;/code&gt; defaults&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Detects which repos produce which images&lt;/strong&gt; by scanning CI configs for build and push commands&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Keeps the results current&lt;/strong&gt; through scheduled or event-triggered rescans&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Makes the graph queryable&lt;/strong&gt;: &quot;show me every consumer of &lt;code&gt;platform/base-image&lt;/code&gt;, grouped by version&quot;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This is one of the specific problems &lt;a href=&quot;https://riftmap.dev&quot;&gt;Riftmap&lt;/a&gt; is built to solve. It scans a GitLab or GitHub org, parses Dockerfiles (including multi-stage builds, ARG defaults, and Compose files), detects which repos produce which images via CI config analysis, and builds a cross-repo dependency graph that you can query by artifact.&lt;/p&gt;
&lt;p&gt;The result: when that CVE drops, you click on the base image in the graph and immediately see every repo that depends on it, which version each one uses, and who owns them. No grepping. No Slack archaeology. No stale spreadsheet from last quarter.&lt;/p&gt;
&lt;p&gt;If this is a problem your platform team deals with, I&apos;d be interested to hear how you&apos;re solving it today. You can find more at &lt;a href=&quot;https://riftmap.dev&quot;&gt;riftmap.dev&lt;/a&gt; or reach me at &lt;a href=&quot;mailto:hello@riftmap.dev&quot;&gt;hello@riftmap.dev&lt;/a&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;This is the first post in the &lt;a href=&quot;https://riftmap.dev/blog/series/find-every-consumer/&quot;&gt;Find Every Consumer&lt;/a&gt; series. Next up: &lt;a href=&quot;https://riftmap.dev/blog/how-to-find-every-consumer-of-your-terraform-module/&quot;&gt;Terraform modules&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</content:encoded><category>docker</category><category>platform-engineering</category><category>devops</category><category>dependency-management</category><category>container-security</category><category>blast-radius</category><category>base-image</category><author>Daniel Westgaard</author></item><item><title>The State of Infrastructure Dependency Tooling in 2026</title><link>https://riftmap.dev/blog/the-state-of-infrastructure-dependency-tooling-2026/</link><guid isPermaLink="true">https://riftmap.dev/blog/the-state-of-infrastructure-dependency-tooling-2026/</guid><description>An honest survey of the infrastructure dependency tooling landscape in 2026 — what Backstage, Renovate, HCP Terraform Explorer, Nx, Wiz, and DIY scripts each solve, where they fall short, and the gap that none of them fill.</description><pubDate>Thu, 09 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;An honest survey of what exists, what each tool actually solves, and where the gap is widest.&lt;/em&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;When I started researching cross-repo infrastructure dependency management, first as a practitioner hitting the problem at client sites, then as someone building a tool to solve it, I expected to find a crowded space. Dependency management is a well-understood problem in software engineering. Package managers have solved it for application code. Surely someone had solved it for infrastructure.&lt;/p&gt;
&lt;p&gt;They hadn&apos;t. Not because nobody tried, but because the tools that exist were built to solve &lt;em&gt;adjacent&lt;/em&gt; problems. Each one is good at what it does. None of them answer the specific question that platform teams keep asking: &lt;strong&gt;if I change this shared module, image, or template, which repos break and who do I need to notify?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This post is a genuine attempt to map the landscape: what exists today, where each tool shines, and where it stops. I&apos;ve used or evaluated most of these tools, talked with engineers who rely on them daily, and spent time in community discussions where people describe what&apos;s working and what isn&apos;t. I&apos;ve tried to be fair. Every tool here solves a real problem for real teams. The point is not that they&apos;re bad. It&apos;s that the &lt;em&gt;specific&lt;/em&gt; problem of cross-repo infrastructure dependency visibility sits in a gap between them.&lt;/p&gt;
&lt;h2&gt;How to think about the landscape&lt;/h2&gt;
&lt;p&gt;The tools people reach for when they encounter the dependency visibility problem fall into six categories. Understanding the categories matters more than evaluating individual tools, because the gap isn&apos;t a missing feature in any one product. It&apos;s a missing &lt;em&gt;category&lt;/em&gt;.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Service catalogs and developer portals&lt;/strong&gt; — &quot;what exists and who owns it&quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dependency update automation&lt;/strong&gt; — &quot;keep everything on the latest version&quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Platform-specific dependency explorers&lt;/strong&gt; — &quot;see relationships within one ecosystem&quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Monorepo build tools&lt;/strong&gt; — &quot;solve the problem by putting everything in one repo&quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Security and compliance scanners&lt;/strong&gt; — &quot;find vulnerabilities and license issues across repos&quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DIY scripts and manual approaches&lt;/strong&gt; — &quot;build exactly what you need, maintain it forever&quot;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Each category addresses a real need. Each one gets mistaken for a solution to the visibility problem. Let&apos;s look at why.&lt;/p&gt;
&lt;h2&gt;Service catalogs: Backstage, Port, OpsLevel&lt;/h2&gt;
&lt;h3&gt;What they do well&lt;/h3&gt;
&lt;p&gt;Service catalogs, with Backstage being the most prominent and Port and OpsLevel as managed alternatives, give engineering organisations a central place to answer &quot;what services do we run, who owns them, and where do they live?&quot; Backstage in particular has become the standard for developer portals at larger companies. It integrates with CI/CD, on-call systems, documentation, and cloud resources. It gives teams a single pane of glass for service ownership.&lt;/p&gt;
&lt;p&gt;Port and OpsLevel take a similar approach with less self-hosting overhead. They provide scorecards, maturity tracking, and integrations with cloud providers. For organisations that need a developer portal, especially those with hundreds of services and unclear ownership, these tools are genuinely valuable.&lt;/p&gt;
&lt;h3&gt;Where they stop&lt;/h3&gt;
&lt;p&gt;The dependency model in service catalogs is &lt;strong&gt;registered, not discovered.&lt;/strong&gt; In Backstage, you define a &lt;code&gt;catalog-info.yaml&lt;/code&gt; per repo that describes the service, its owner, and its dependencies. This information is exactly as accurate as the last time someone updated that file.&lt;/p&gt;
&lt;p&gt;In practice, catalog YAML goes stale quickly. Engineers update code. They don&apos;t update the catalog. The dependency that got added three months ago isn&apos;t in the YAML. The one that got removed six months ago is still listed. As one engineer described it to me, it&apos;s &quot;documentation with extra steps.&quot;&lt;/p&gt;
&lt;p&gt;This isn&apos;t a flaw in Backstage; it&apos;s a design choice. Service catalogs are built for &lt;em&gt;service metadata&lt;/em&gt; (ownership, documentation links, runbook URLs), not for &lt;em&gt;dependency graph accuracy&lt;/em&gt;. They&apos;re excellent at answering &quot;who owns this service?&quot; They&apos;re not designed to answer &quot;which 40 repos consume this Terraform module at which version?&quot; If you&apos;re weighing the catalog category against the alternatives for this specific job, I triage that decision in &lt;a href=&quot;https://riftmap.dev/blog/backstage-alternatives/&quot;&gt;Backstage alternatives in 2026&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Port has invested in auto-discovery features that pull some metadata from cloud resources and Git, which narrows the gap. But the cross-repo infrastructure dependency graph (Terraform modules sourcing other modules, Docker base images consumed by dozens of repos, CI templates included across the org) still requires either manual registration or a separate discovery mechanism.&lt;/p&gt;
&lt;h3&gt;Who should use them&lt;/h3&gt;
&lt;p&gt;Any organisation over ~50 services that needs a central developer portal with ownership tracking, documentation, and service maturity scoring. They&apos;re complementary to dependency visibility tooling, not a replacement for it.&lt;/p&gt;
&lt;h2&gt;Dependency update automation: Renovate and Dependabot&lt;/h2&gt;
&lt;h3&gt;What they do well&lt;/h3&gt;
&lt;p&gt;Renovate and Dependabot solve a specific, well-defined problem: when a dependency has a new version available, automatically open a pull request to update it. Renovate in particular is remarkably flexible. It supports dozens of package ecosystems, custom versioning schemes, grouping strategies, and scheduling controls. For teams that want to stay current on their dependencies, Renovate is close to best-in-class.&lt;/p&gt;
&lt;p&gt;Dependabot, integrated directly into GitHub, offers a lower-configuration path to the same outcome. It&apos;s particularly strong for npm, pip, Go modules, and GitHub Actions workflows.&lt;/p&gt;
&lt;p&gt;Both tools are excellent at what they do, and many teams should be using one of them regardless of what else they adopt for dependency visibility.&lt;/p&gt;
&lt;h3&gt;Where they stop&lt;/h3&gt;
&lt;p&gt;The core distinction is between &lt;strong&gt;prevention&lt;/strong&gt; and &lt;strong&gt;visibility&lt;/strong&gt;. Renovate and Dependabot solve prevention: they keep consumers updated so that version drift doesn&apos;t accumulate. They react &lt;em&gt;after&lt;/em&gt; a new version is published by opening PRs across consuming repos.&lt;/p&gt;
&lt;p&gt;What they don&apos;t provide is the &lt;em&gt;pre-release&lt;/em&gt; view. Before you publish version 2.0 of your shared Terraform module, before you even cut the release, you want to know: who is consuming the current version? How many repos will be affected? Which teams own them? Are any of them pinned to a version that&apos;s already three releases behind?&lt;/p&gt;
&lt;p&gt;Renovate can tell you that a new version is available. It cannot tell you the blast radius of a change you haven&apos;t published yet. These are different problems, and confusing them leads teams to believe they have dependency visibility when they actually have dependency &lt;em&gt;automation&lt;/em&gt;. This is the same gap that bites when you go to deprecate an internal library: the automation can perform the bump in every repo it&apos;s been pointed at, but it presupposes the list of consumers, and the repos pinned to the old version that fell out of the automation are exactly the ones that break on removal — &lt;a href=&quot;https://riftmap.dev/blog/deprecate-internal-library-find-consumers/&quot;&gt;I&apos;ve written up that angle separately&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;There&apos;s a subtler gap too: Renovate and Dependabot work at the &lt;em&gt;package&lt;/em&gt; level, not the &lt;em&gt;repo&lt;/em&gt; level. They know that &lt;code&gt;package.json&lt;/code&gt; depends on &lt;code&gt;@company/utils@1.2.0&lt;/code&gt;. They don&apos;t know that the repo containing that &lt;code&gt;package.json&lt;/code&gt; also has a Dockerfile that pulls an internal base image, a CI config that includes a shared template, and a Terraform module that sources three other internal modules. The cross-ecosystem picture is outside their scope.&lt;/p&gt;
&lt;h3&gt;Who should use them&lt;/h3&gt;
&lt;p&gt;Every team that consumes shared dependencies and wants to avoid version drift. Use them &lt;em&gt;alongside&lt;/em&gt; dependency visibility tooling, not instead of it.&lt;/p&gt;
&lt;h2&gt;Platform-specific explorers: HCP Terraform, Artifactory, GitHub Dependency Graph&lt;/h2&gt;
&lt;h3&gt;HCP Terraform Explorer&lt;/h3&gt;
&lt;p&gt;HashiCorp Cloud Platform (HCP Terraform, formerly Terraform Cloud) includes a module explorer that shows which workspaces use which modules. If your entire Terraform workflow runs through HCP Terraform (plan, apply, state management, all of it) this gives you a reasonable view of Terraform-specific module relationships.&lt;/p&gt;
&lt;p&gt;The limitation is scope. It only works for Terraform. It only works for workspaces managed by HCP Terraform. If your org has a mix of HCP Terraform, self-hosted Terraform runners, CI-driven applies, and Atlantis (and many do) you get a partial view. The Docker images, CI templates, Helm charts, and Ansible roles that are part of the same dependency graph are invisible.&lt;/p&gt;
&lt;p&gt;For teams that are fully committed to HCP Terraform and only need Terraform module visibility, this is a solid native solution. For everyone else, it&apos;s a slice of the picture.&lt;/p&gt;
&lt;h3&gt;JFrog Artifactory&lt;/h3&gt;
&lt;p&gt;Artifactory is a universal artifact repository. For Terraform, it can serve as a private module registry with metadata about published modules: versions, download counts, and some dependency information.&lt;/p&gt;
&lt;p&gt;The gap is on the consumer side. Artifactory knows what modules have been &lt;em&gt;published&lt;/em&gt;. It doesn&apos;t reliably tell you which repos are &lt;em&gt;consuming&lt;/em&gt; a given module at which version right now. The publishing-side view and the consuming-side view are different, and Artifactory was built for the former.&lt;/p&gt;
&lt;h3&gt;GitHub Dependency Graph and Dependabot Alerts&lt;/h3&gt;
&lt;p&gt;GitHub&apos;s built-in dependency graph parses lock files and manifests to show what each repo depends on. It&apos;s tightly integrated with Dependabot alerts for security vulnerabilities.&lt;/p&gt;
&lt;p&gt;It&apos;s good for application dependencies: npm, pip, Go modules, Ruby gems. It&apos;s limited for infrastructure dependencies. Terraform module sources, Docker base image relationships, CI template includes, Helm chart dependencies. These aren&apos;t part of GitHub&apos;s dependency graph model. And the view is per-repo, not org-wide: you can see what &lt;em&gt;one repo&lt;/em&gt; depends on, but you can&apos;t easily ask &quot;who across my org depends on &lt;em&gt;this&lt;/em&gt; artifact?&quot;&lt;/p&gt;
&lt;h3&gt;Who should use them&lt;/h3&gt;
&lt;p&gt;Teams that operate primarily within a single ecosystem and a single platform. If you&apos;re a pure Terraform Cloud shop, HCP Explorer gives you module visibility. If you&apos;re managing artifact publishing, Artifactory is the right tool for that. If you need per-repo application dependency visibility on GitHub, the built-in graph works. None of them provide the cross-ecosystem, org-wide view.&lt;/p&gt;
&lt;h2&gt;Monorepo build tools: Nx, Turborepo, Bazel&lt;/h2&gt;
&lt;h3&gt;What they do well&lt;/h3&gt;
&lt;p&gt;This is the category that genuinely &lt;em&gt;solves&lt;/em&gt; the dependency visibility problem, within its context. Monorepo build tools like Nx, Turborepo, and Bazel are built around a dependency graph. They know which packages depend on which. They can tell you exactly what&apos;s affected by a change. They run only the tests and builds that are actually impacted.&lt;/p&gt;
&lt;p&gt;Nx in particular is impressive here. Its &lt;code&gt;nx affected&lt;/code&gt; command does precisely what platform teams wish they could do across a polyrepo: given a set of changed files, determine which projects in the repo are affected and need to be rebuilt or retested. It has a visual graph explorer, dependency analysis, and caching that skips unaffected work. Turborepo provides similar affected-project detection with a focus on speed and simplicity. Bazel takes it further with hermetic builds and fine-grained dependency tracking at the file level.&lt;/p&gt;
&lt;p&gt;If your organisation operates as a monorepo, these tools give you dependency visibility almost for free. The graph is implicit in the repo structure, and the build tool maintains it automatically.&lt;/p&gt;
&lt;h3&gt;Where they stop&lt;/h3&gt;
&lt;p&gt;The limitation is the prerequisite: you have to be in a monorepo.&lt;/p&gt;
&lt;p&gt;Most infrastructure teams dealing with the cross-repo dependency problem are not in a monorepo, and migrating to one is not a realistic option. An organisation with three hundred existing repos, multiple teams with separate access controls, compliance boundaries between business units, and repos inherited through acquisitions cannot consolidate into a single repository as a quarter-long project. It&apos;s a multi-year architectural migration with significant operational risk.&lt;/p&gt;
&lt;p&gt;Even organisations that &lt;em&gt;want&lt;/em&gt; to move toward a monorepo often can&apos;t do it all at once. They might consolidate application code but keep infrastructure repos separate. Or they might run a monorepo for one team while other teams stay on polyrepo. The dependency graph still crosses that boundary, and monorepo build tools only see what&apos;s inside the monorepo.&lt;/p&gt;
&lt;p&gt;There&apos;s also a scope question. Nx and Turborepo are primarily designed for application code (TypeScript/JavaScript, with growing support for other languages). The infrastructure dependency graph, which includes Terraform modules, Docker base images, CI templates, Helm charts, and Ansible roles, doesn&apos;t map cleanly onto a monorepo build tool&apos;s model. Bazel is more general-purpose, but the overhead of Bazel adoption is substantial and rarely justified purely for dependency visibility.&lt;/p&gt;
&lt;p&gt;One more nuance worth noting: even inside a monorepo, the build tool&apos;s dependency graph tracks &lt;em&gt;build-time&lt;/em&gt; relationships. It knows that package A imports package B. It doesn&apos;t necessarily know that a Dockerfile in one directory pulls a base image that&apos;s built by a CI job defined in another directory, or that a Terraform module sources another module via a git URL pointing at a subdirectory. The infrastructure dependency surface is different from the application dependency surface, even within a single repo.&lt;/p&gt;
&lt;h3&gt;Who should use them&lt;/h3&gt;
&lt;p&gt;Any team already operating in a monorepo, or actively planning a migration to one. Nx and Turborepo are excellent tools. The point is not that monorepos are wrong. It&apos;s that &quot;switch to a monorepo&quot; is not actionable advice for the organisations that feel the cross-repo dependency problem most acutely.&lt;/p&gt;
&lt;h2&gt;Security and compliance scanners: Wiz, Snyk, SBoM tools&lt;/h2&gt;
&lt;h3&gt;What they do well&lt;/h3&gt;
&lt;p&gt;Security scanners like Wiz and Snyk, along with the broader SBoM (Software Bill of Materials) ecosystem, solve an important and well-funded problem: finding known vulnerabilities (CVEs) and license compliance issues across an organisation&apos;s software supply chain.&lt;/p&gt;
&lt;p&gt;Wiz in particular has an attractive deployment model for this discussion: org-level installation via webhook or cloud connector, with no per-repo configuration needed. It scans broadly and automatically. The &lt;em&gt;install pattern&lt;/em&gt; is exactly right for dependency discovery.&lt;/p&gt;
&lt;p&gt;Snyk provides deep analysis of application dependencies, container images, and infrastructure-as-code configurations. It&apos;s strong on identifying &lt;em&gt;security risks&lt;/em&gt; within dependency trees.&lt;/p&gt;
&lt;h3&gt;Where they stop&lt;/h3&gt;
&lt;p&gt;These tools are built around a security and compliance model, not a dependency visibility model. The question they answer is &quot;do any of my dependencies have known vulnerabilities?&quot;, not &quot;which repos depend on this internal module and what&apos;s the blast radius if I change it?&quot;&lt;/p&gt;
&lt;p&gt;The distinction matters in practice. Wiz can tell you that a container image has a CVE. It doesn&apos;t tell you that 30 repos across your org use that image as a base, that 12 of them are pinned to an outdated version, and that updating the image will require coordinated changes across four teams. Snyk can tell you that a Python package has a license issue. It doesn&apos;t show you the cross-repo dependency graph between your internal Terraform modules, CI templates, and Helm charts.&lt;/p&gt;
&lt;p&gt;There&apos;s also a scope gap: most security scanners focus on application dependencies (npm packages, Python libraries, container images) and cloud configuration. Terraform module-to-module relationships, CI template includes, and Ansible role dependencies are generally outside their scanning model.&lt;/p&gt;
&lt;p&gt;The pricing model is also relevant. Enterprise security platforms are priced for security budgets, not platform engineering budgets. Using a security scanner as a general-purpose dependency mapping tool would be expensive overkill for the dependency visibility problem specifically.&lt;/p&gt;
&lt;h3&gt;Who should use them&lt;/h3&gt;
&lt;p&gt;Any organisation that takes software supply chain security seriously, which should be everyone. These tools are essential for what they do. They just don&apos;t do dependency visibility, and shouldn&apos;t be expected to.&lt;/p&gt;
&lt;h2&gt;DIY approaches: grep, scripts, and custom crawlers&lt;/h2&gt;
&lt;h3&gt;What they do well&lt;/h3&gt;
&lt;p&gt;This is the category that tells you the most about the problem. When multiple independent engineers, with no coordination between them, build nearly identical tools, you&apos;re looking at a genuine unmet need.&lt;/p&gt;
&lt;p&gt;The common pattern: a scheduled job that shallow-clones every repo in the org (or uses the GitLab/GitHub API), greps or parses Dockerfiles, Terraform source blocks, and CI include directives, dumps the results to SQLite or a spreadsheet, and maybe renders a graph with something like Observable Framework or a simple web page.&lt;/p&gt;
&lt;p&gt;These solutions &lt;em&gt;work&lt;/em&gt;. They prove the approach is sound. Some of them are impressively capable, handling hundreds or even thousands of repos, tracking multiple file types, producing usable output. The engineers who build them solve their team&apos;s immediate problem, often in a day or two of effort.&lt;/p&gt;
&lt;h3&gt;Where they stop&lt;/h3&gt;
&lt;p&gt;Bespoke solutions have a consistent set of failure modes:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;They go stale.&lt;/strong&gt; A nightly cron job means the graph is up to 24 hours out of date. A manually-triggered script means it&apos;s as fresh as whoever last remembered to run it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;They&apos;re fragile.&lt;/strong&gt; When a repo is renamed, archived, or changes its structure, the script breaks. Handling these edge cases, and the dozens of others that emerge over time, requires ongoing maintenance that nobody budgeted for.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;They&apos;re not generalised.&lt;/strong&gt; Each one handles the specific file types and repo structures of one org. Moving to a new org, or adding support for a new ecosystem, means rewriting.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;They don&apos;t survive their creator.&lt;/strong&gt; The engineer who built the script in a weekend moves to another team or another company. Nobody else understands how it works. Six months later, it&apos;s running but nobody trusts the output. Or it&apos;s silently broken. Or it&apos;s been turned off.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;They lack a query interface.&lt;/strong&gt; Most DIY solutions produce a static output. Asking &quot;what&apos;s the blast radius of changing module X?&quot; means writing a new query or script, not clicking a button.&lt;/p&gt;
&lt;p&gt;The meta-point: the fact that people keep building these, and that the solutions converge on the same architecture, validates both the problem and the general approach. What&apos;s missing is a product that does it well, keeps doing it, and handles the long tail of edge cases that a weekend hack can&apos;t.&lt;/p&gt;
&lt;h3&gt;Who should use them&lt;/h3&gt;
&lt;p&gt;Teams with a specific, narrow need (maybe just Terraform modules across 30 repos) and an engineer willing to maintain a custom script. For anything broader or longer-lived, the maintenance cost exceeds the build cost within months.&lt;/p&gt;
&lt;h2&gt;The gap in the landscape&lt;/h2&gt;
&lt;p&gt;Line up the categories side by side and the gap becomes clear:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Service catalogs&lt;/strong&gt; know what exists and who owns it, but dependencies are registered, not discovered, and go stale.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Dependency update tools&lt;/strong&gt; keep consumers current, but only react after a version is published and don&apos;t show blast radius beforehand.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Platform-specific explorers&lt;/strong&gt; show relationships within one ecosystem, but are blind to everything outside it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Monorepo build tools&lt;/strong&gt; solve the problem structurally, but only if you&apos;re already in a monorepo. Most infrastructure teams aren&apos;t.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Security scanners&lt;/strong&gt; find vulnerabilities across repos, but don&apos;t map infrastructure dependency graphs.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;DIY scripts&lt;/strong&gt; prove the approach works, but are bespoke, brittle, and don&apos;t survive their creator.&lt;/p&gt;
&lt;p&gt;The missing category is &lt;strong&gt;cross-ecosystem infrastructure dependency visibility&lt;/strong&gt;: a tool that automatically discovers how repos depend on each other across &lt;em&gt;all&lt;/em&gt; the ecosystems a platform team uses (Terraform, Docker, CI templates, Helm, Ansible, Python, Go, npm, Kubernetes), keeps that graph current without manual maintenance, and makes it queryable.&lt;/p&gt;
&lt;p&gt;Specifically, the gap has these characteristics:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Auto-discovered, not registered.&lt;/strong&gt; The dependency graph must be built from what&apos;s actually in the repos: the Terraform source blocks, the Dockerfile FROM statements, the CI include directives. Any approach that requires humans to maintain a catalog will go stale. The community has been clear and consistent on this point.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cross-ecosystem.&lt;/strong&gt; Real infrastructure dependency graphs cross tool boundaries. A Terraform module change affects Docker builds which affect CI pipelines which affect Helm deployments. Single-ecosystem tools miss exactly the connections where surprise breakage happens.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Consumer-side visibility.&lt;/strong&gt; Most existing tools show the &lt;em&gt;producer&lt;/em&gt; side: what modules exist, what images are published, what templates are available. The question platform teams actually need answered is the &lt;em&gt;consumer&lt;/em&gt; side: who is pulling this artifact, at which version, right now?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Org-level installation.&lt;/strong&gt; At a hundred or more repos, anything that requires per-repo setup is a non-starter. The installation model should be a single read-only access token at the org level, similar to how security scanners deploy.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Freshness without manual effort.&lt;/strong&gt; The graph must stay current through scheduled or event-driven rescans. Staleness is the number one reason DIY solutions get abandoned. Any productised solution that doesn&apos;t solve freshness will follow the same path.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Blast radius before you push.&lt;/strong&gt; The highest-value query is prospective, not retrospective: before I publish a breaking change, show me every repo and team that will be affected. This requires the full transitive graph, not just direct dependencies.&lt;/p&gt;
&lt;h2&gt;Where I think this is heading&lt;/h2&gt;
&lt;p&gt;The dependency visibility gap isn&apos;t going to close on its own. If anything, it&apos;s widening. Organisations are adopting more infrastructure tools, not fewer. The shift toward platform engineering means more shared components consumed across more repos. AI-assisted development is accelerating code output without any corresponding improvement in dependency awareness.&lt;/p&gt;
&lt;p&gt;I expect we&apos;ll see this category emerge properly over the next couple of years. The building blocks exist: Git platform APIs for repo enumeration, well-understood file formats for parsing, graph databases and recursive queries for traversal, and a clear community demand validated by the fact that people keep building their own versions.&lt;/p&gt;
&lt;p&gt;Some existing tools may expand into this space. Backstage&apos;s plugin ecosystem could support auto-discovery. Renovate&apos;s deep parser library could be extended for visibility queries. Security platforms like Wiz, which already have the org-level deployment model, could add infrastructure dependency graphs alongside their vulnerability scanning.&lt;/p&gt;
&lt;p&gt;What I think is more likely is that purpose-built tools will emerge. Tools designed specifically for cross-repo infrastructure dependency visibility, with auto-discovery, cross-ecosystem support, and blast radius analysis as core capabilities rather than bolted-on features.&lt;/p&gt;
&lt;p&gt;That&apos;s what I&apos;m building with &lt;a href=&quot;https://riftmap.dev&quot;&gt;Riftmap&lt;/a&gt;. It scans a GitLab or GitHub org, auto-discovers cross-repo dependencies across Terraform, Docker, CI pipelines, Python, Go, npm, Ansible, Helm, Kubernetes, and Kustomize, and presents the graph with interactive blast radius analysis. Org-level install, no per-repo config, no YAML to maintain.&lt;/p&gt;
&lt;p&gt;It&apos;s currently in early access, and it&apos;s far from finished. But the foundation is live: twelve ecosystem parsers, a resolver that matches consumers to producers across the org, incremental scanning to keep the graph fresh, and a visual graph with impact mode that shows exactly which repos are affected by a given change.&lt;/p&gt;
&lt;p&gt;If you&apos;re working in this space, whether as an engineer dealing with the problem, someone building tooling, or someone evaluating solutions, I&apos;d welcome the conversation. The more perspectives I hear, the better the tooling gets for everyone.&lt;/p&gt;
&lt;p&gt;You can see more at &lt;a href=&quot;https://riftmap.dev&quot;&gt;riftmap.dev&lt;/a&gt;, or reach me at &lt;a href=&quot;mailto:hello@riftmap.dev&quot;&gt;hello@riftmap.dev&lt;/a&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;If you&apos;re interested in following along as Riftmap develops, sign up for early access at &lt;a href=&quot;https://riftmap.dev&quot;&gt;riftmap.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</content:encoded><category>platform-engineering</category><category>infrastructure</category><category>dependency-management</category><category>devops</category><category>multi-repo</category><category>blast-radius</category><category>backstage</category><category>renovate</category><category>terraform</category><category>service-catalog</category><category>infrastructure-as-code</category><category>tooling</category><author>Daniel Westgaard</author></item><item><title>Auto-Discovering Infrastructure Dependencies Across 10 Ecosystems</title><link>https://riftmap.dev/blog/auto-discovering-infrastructure-dependencies-across-10-ecosystems/</link><guid isPermaLink="true">https://riftmap.dev/blog/auto-discovering-infrastructure-dependencies-across-10-ecosystems/</guid><description>A technical deep-dive into auto-discovering cross-repo dependencies across Terraform, Docker, CI pipelines, Python, Go, npm, Ansible, Helm, Kubernetes, and Kustomize — and why each ecosystem fights back.</description><pubDate>Sat, 04 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;It sounds simple: clone every repo, parse the files, build a graph. Here&apos;s why each ecosystem fights back, and what it actually takes to map cross-repo dependencies automatically.&lt;/em&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;In &lt;a href=&quot;https://riftmap.dev/blog/infrastructure-dependency-problem&quot;&gt;the last post&lt;/a&gt;, I wrote about the infrastructure dependency visibility gap. The fact that most platform teams have no way to answer &quot;if I change this, what else breaks?&quot; across their repos. The community response confirmed what I&apos;d seen at every client: people are building brittle grep scripts, maintaining stale spreadsheets, or just relying on whoever has been around the longest.&lt;/p&gt;
&lt;p&gt;The obvious next question is: why doesn&apos;t anyone just &lt;em&gt;parse the repos and build a graph&lt;/em&gt;?&lt;/p&gt;
&lt;p&gt;The answer is that people do. Multiple engineers I&apos;ve spoken with have built their own versions; a nightly cron job, some shell scripts, a SQLite database. And those solutions work, for a while, for one org, for the file types they remembered to handle. Then they hit an edge case, go stale, or the person who built it moves on.&lt;/p&gt;
&lt;p&gt;The core approach is right: scan every repo, parse the files that declare dependencies, resolve them to actual repos, build a directed graph. But the devil is in the details, and each ecosystem has its own set of devils. This post walks through what it actually takes to auto-discover cross-repo dependencies across Terraform, Docker, CI pipelines, Python, Go, npm, Ansible, Helm, and Kubernetes, and why the cross-ecosystem problem is harder than any individual one.&lt;/p&gt;
&lt;h2&gt;The approach: parse what&apos;s already there&lt;/h2&gt;
&lt;p&gt;The principle behind auto-discovery is simple: &lt;strong&gt;the dependencies are already declared in the source files.&lt;/strong&gt; A Terraform module has a &lt;code&gt;source&lt;/code&gt; attribute pointing at a git URL. A Dockerfile has a &lt;code&gt;FROM&lt;/code&gt; statement naming a base image. A GitLab CI config has &lt;code&gt;include:&lt;/code&gt; directives referencing templates in other repos.&lt;/p&gt;
&lt;p&gt;You don&apos;t need humans to fill in a catalog. You don&apos;t need a YAML manifest per repo. The dependency information exists. It&apos;s just scattered across a dozen file formats in hundreds of repos, with no unified view.&lt;/p&gt;
&lt;p&gt;So the pipeline looks like this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Enumerate&lt;/strong&gt; — list every repo in the GitLab group or GitHub org via API&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Clone&lt;/strong&gt; — shallow-clone each repo (depth 1, just the default branch)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Parse&lt;/strong&gt; — walk the file tree, dispatch each file to the right parser based on filename and path&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Detect artifacts&lt;/strong&gt; — identify what each repo &lt;em&gt;produces&lt;/em&gt; (a Terraform module, a Docker image, a Python package, a Helm chart)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Resolve&lt;/strong&gt; — match parsed dependency references to known repos or artifacts in the org&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Store&lt;/strong&gt; — persist the graph as queryable relationships&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Steps 1 and 2 are straightforward. Steps 3 through 5 are where every ecosystem has opinions about how to make your life difficult.&lt;/p&gt;
&lt;h2&gt;Terraform: where version refs hide in query strings&lt;/h2&gt;
&lt;p&gt;Terraform is usually the first ecosystem people think of for cross-repo dependencies, and on the surface it looks easy. A module block has a &lt;code&gt;source&lt;/code&gt; attribute:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;module &quot;vpc&quot; {
  source = &quot;git::https://gitlab.com/infra/modules/vpc.git?ref=v2.1.0&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You parse the &lt;code&gt;source&lt;/code&gt; string, extract the git URL and the ref, normalize the path to &lt;code&gt;infra/modules/vpc&lt;/code&gt;, match it against known repos in the org — done.&lt;/p&gt;
&lt;p&gt;Except it&apos;s never that clean. Here&apos;s what you actually encounter in the wild:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Multiple URL formats.&lt;/strong&gt; The same module might be sourced as &lt;code&gt;git::https://...&lt;/code&gt;, &lt;code&gt;git@gitlab.com:...&lt;/code&gt; (SSH), a bare HTTPS URL without the &lt;code&gt;git::&lt;/code&gt; prefix, or a Terraform registry address like &lt;code&gt;app.terraform.io/org/module/provider&lt;/code&gt;. Each format needs different parsing logic to extract the same canonical repo path.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Subdirectory references.&lt;/strong&gt; Terraform supports the double-slash convention: &lt;code&gt;git::https://gitlab.com/infra/modules.git//networking/vpc?ref=v1.0&lt;/code&gt;. The repo is &lt;code&gt;infra/modules&lt;/code&gt;, but the module is in a subdirectory. This means one repo can &lt;em&gt;produce&lt;/em&gt; multiple distinct modules, and your parser needs to handle that relationship.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Variable interpolation.&lt;/strong&gt; You&apos;ll find modules like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;module &quot;service&quot; {
  source = &quot;git::https://gitlab.com/${var.infra_group}/modules/service.git&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can&apos;t resolve &lt;code&gt;${var.infra_group}&lt;/code&gt; without running Terraform, and the whole point of static analysis is that you &lt;em&gt;don&apos;t&lt;/em&gt; run Terraform. The practical choice is to flag these as lower-confidence dependencies and extract what you can from the static portion of the string.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Public registry vs. internal modules.&lt;/strong&gt; A source like &lt;code&gt;hashicorp/consul/aws&lt;/code&gt; points to the public Terraform Registry — it&apos;s not an internal dependency and should be skipped. But &lt;code&gt;app.terraform.io/your-org/vpc/aws&lt;/code&gt; &lt;em&gt;is&lt;/em&gt; internal. The parser needs to distinguish between public and private registries.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The &lt;code&gt;modules.json&lt;/code&gt; trap.&lt;/strong&gt; Some people suggest parsing &lt;code&gt;.terraform/modules/modules.json&lt;/code&gt;, which contains the resolved module tree. The problem: this file only exists if someone has run &lt;code&gt;terraform init&lt;/code&gt;, it&apos;s usually in &lt;code&gt;.gitignore&lt;/code&gt;, and it reflects one person&apos;s local state — not the repo&apos;s declared dependencies. It&apos;s not a reliable source for org-wide discovery.&lt;/p&gt;
&lt;p&gt;The meta-lesson from Terraform: even within a single ecosystem, the same logical relationship (&quot;repo A depends on repo B&quot;) can be expressed in half a dozen syntactically different ways, and your parser needs to normalize all of them to the same canonical form.&lt;/p&gt;
&lt;h2&gt;Docker: the base image puzzle&lt;/h2&gt;
&lt;p&gt;Dockerfiles look deceptively simple:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;FROM node:18-alpine
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That&apos;s a public image — skip it. But:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;FROM registry.company.com/platform/base-image:v2.1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That&apos;s an internal base image, and it means this repo depends on whatever repo builds and publishes &lt;code&gt;platform/base-image&lt;/code&gt;. Tracking these relationships across an org is one of the highest-value things a dependency graph can do. Docker base image updates are a constant source of surprise breakage.&lt;/p&gt;
&lt;p&gt;Here&apos;s where it gets complicated:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Build arguments and variable substitution.&lt;/strong&gt; Real-world Dockerfiles frequently use &lt;code&gt;ARG&lt;/code&gt; to parameterize the base image:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ARG REGISTRY=registry.company.com
ARG BASE_VERSION=latest
FROM ${REGISTRY}/platform/base-image:${BASE_VERSION}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can resolve &lt;code&gt;ARG&lt;/code&gt; defaults by parsing the Dockerfile top-to-bottom, substituting the default values into the &lt;code&gt;FROM&lt;/code&gt; statement. But if the &lt;code&gt;ARG&lt;/code&gt; is overridden at build time via &lt;code&gt;--build-arg&lt;/code&gt;, the static default might be wrong. Again: lower confidence, but still useful signal.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Multi-stage builds.&lt;/strong&gt; A modern Dockerfile might have several &lt;code&gt;FROM&lt;/code&gt; statements:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;FROM node:18 AS builder
RUN npm ci &amp;amp;&amp;amp; npm run build

FROM registry.company.com/platform/nginx:1.25
COPY --from=builder /app/dist /usr/share/nginx/html
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first &lt;code&gt;FROM&lt;/code&gt; is a public image. The second is an internal dependency. The &lt;code&gt;COPY --from=builder&lt;/code&gt; is a reference to an earlier &lt;em&gt;stage&lt;/em&gt;, not an external image — the parser needs to track named stages and skip internal references. If you naively treat every &lt;code&gt;FROM&lt;/code&gt; and &lt;code&gt;COPY --from&lt;/code&gt; as a dependency, you&apos;ll generate false edges in the graph.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Docker Compose adds another layer.&lt;/strong&gt; A &lt;code&gt;docker-compose.yml&lt;/code&gt; might reference images directly:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;services:
  api:
    image: registry.company.com/backend/api-service:latest
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is a dependency declaration in a completely different file format (YAML vs. Dockerfile syntax), but it represents the same kind of relationship.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The consumer-side problem.&lt;/strong&gt; Knowing that repo X &lt;em&gt;uses&lt;/em&gt; a Docker image is only half the story. You also need to know which repo &lt;em&gt;builds&lt;/em&gt; that image. This isn&apos;t declared in the Dockerfile. It&apos;s usually in the CI pipeline config (&lt;code&gt;docker build -t&lt;/code&gt; and &lt;code&gt;docker push&lt;/code&gt; commands). Connecting the consumer side (&quot;this repo uses image Y&quot;) to the producer side (&quot;this repo builds image Y&quot;) requires cross-referencing information from different file types within the same repo.&lt;/p&gt;
&lt;h2&gt;CI templates: the invisible dependency layer&lt;/h2&gt;
&lt;p&gt;CI pipeline configs are arguably the most important dependency surface to track, and the most neglected.&lt;/p&gt;
&lt;h3&gt;GitLab CI&lt;/h3&gt;
&lt;p&gt;GitLab CI supports several forms of cross-repo inclusion:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;include:
  - project: &apos;platform/ci-templates&apos;
    ref: &apos;v2.0&apos;
    file: &apos;/terraform-plan.yml&apos;

  - remote: &apos;https://gitlab.com/platform/ci-templates/-/raw/main/deploy.yml&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;project:&lt;/code&gt; form gives you a clean repo reference and an optional version ref. The &lt;code&gt;remote:&lt;/code&gt; form gives you a URL that needs to be parsed back into a repo path.&lt;/p&gt;
&lt;p&gt;There&apos;s also &lt;code&gt;trigger:&lt;/code&gt;, where one pipeline triggers another project&apos;s pipeline, and &lt;code&gt;image:&lt;/code&gt;, where a job specifies a Docker image. Another dependency surface hiding in the CI config.&lt;/p&gt;
&lt;p&gt;A subtle gotcha: GitLab CI supports &lt;code&gt;!reference&lt;/code&gt; tags for reusing configuration fragments. These are valid YAML tags but not standard YAML. A naive YAML parser will choke on them. Your parser needs to handle this gracefully, either by pre-processing the file or by configuring the YAML loader to ignore unknown tags.&lt;/p&gt;
&lt;h3&gt;GitHub Actions&lt;/h3&gt;
&lt;p&gt;GitHub Actions reusable workflows have their own syntax:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;jobs:
  deploy:
    uses: org/shared-workflows/.github/workflows/deploy.yml@v1.2.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And composite or JavaScript actions:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;steps:
  - uses: org/custom-action@main
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;uses:&lt;/code&gt; string encodes the org, repo, path, and ref in a single string. You need to parse it, separate the org/repo from the workflow path, handle the &lt;code&gt;@ref&lt;/code&gt; suffix, and skip public actions (anything under &lt;code&gt;actions/*&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;The real challenge with CI templates is that they&apos;re often pinned to a &lt;em&gt;branch&lt;/em&gt; rather than a tag — &lt;code&gt;@main&lt;/code&gt; instead of &lt;code&gt;@v2.0&lt;/code&gt;. This means version tracking is inherently fuzzy. You can tell that repo A depends on repo B&apos;s CI template, but &quot;which version&quot; is just &quot;whatever&apos;s on main right now.&quot; This is exactly the kind of implicit, hard-to-track dependency that causes surprise breakage.&lt;/p&gt;
&lt;h2&gt;Python, Go, and npm: package ecosystems with their own quirks&lt;/h2&gt;
&lt;p&gt;These three share a common pattern — they have declared dependency manifests — but each has its own flavour of complexity.&lt;/p&gt;
&lt;h3&gt;Python&lt;/h3&gt;
&lt;p&gt;Python dependencies can be declared in at least four places: &lt;code&gt;requirements.txt&lt;/code&gt;, &lt;code&gt;pyproject.toml&lt;/code&gt;, &lt;code&gt;setup.cfg&lt;/code&gt;, and &lt;code&gt;setup.py&lt;/code&gt;. Each has a different syntax. And only some of those declarations point at internal packages — most are public PyPI packages that you should skip.&lt;/p&gt;
&lt;p&gt;The interesting ones for cross-repo tracking are &lt;em&gt;editable git installs&lt;/em&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-e git+https://gitlab.com/org/internal-utils.git@v1.2#egg=internal-utils
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And &lt;code&gt;pyproject.toml&lt;/code&gt; dependencies pointing at internal packages published to a private PyPI registry. Matching &quot;package name in a requirements file&quot; to &quot;the repo that builds that package&quot; requires knowing what each repo &lt;em&gt;produces&lt;/em&gt; — which is why the artifact detection step (identifying that a given repo is the source of a Python package, based on its &lt;code&gt;pyproject.toml&lt;/code&gt; or &lt;code&gt;setup.py&lt;/code&gt;) is essential.&lt;/p&gt;
&lt;h3&gt;Go&lt;/h3&gt;
&lt;p&gt;Go modules are cleaner than most. The &lt;code&gt;go.mod&lt;/code&gt; file is authoritative:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;require (
    gitlab.com/org/shared-lib v1.3.0
    github.com/org/internal-sdk v0.9.2
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The module path &lt;em&gt;is&lt;/em&gt; the repo path (more or less). The version is explicit. The main challenge is filtering: most &lt;code&gt;require&lt;/code&gt; entries are public modules (&lt;code&gt;github.com/stretchr/testify&lt;/code&gt;, &lt;code&gt;golang.org/x/net&lt;/code&gt;). You need a way to identify which entries point to repos within your org and skip the rest.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;replace&lt;/code&gt; directives add a wrinkle — they can redirect a module path to a local directory or a different remote path, which changes the effective dependency.&lt;/p&gt;
&lt;h3&gt;npm&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;package.json&lt;/code&gt; is the source of truth. For internal cross-repo dependencies, you&apos;re looking for scoped packages or git URL references:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;dependencies&quot;: {
    &quot;@company/ui-components&quot;: &quot;^2.1.0&quot;,
    &quot;@company/shared-utils&quot;: &quot;git+https://github.com/org/shared-utils.git#v1.0&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Scoped packages (&lt;code&gt;@company/...&lt;/code&gt;) from a private registry need to be matched to the repo that publishes them. Git URL references can be parsed directly. Public npm packages are filtered out.&lt;/p&gt;
&lt;p&gt;One thing all three ecosystems share: the link between &quot;this repo &lt;em&gt;consumes&lt;/em&gt; package X&quot; and &quot;this repo &lt;em&gt;produces&lt;/em&gt; package X&quot; is not always obvious from a single file. You need to first discover what each repo publishes (by reading its &lt;code&gt;pyproject.toml&lt;/code&gt;, &lt;code&gt;go.mod&lt;/code&gt; module declaration, or &lt;code&gt;package.json&lt;/code&gt; name field), then match consumers to producers across the org.&lt;/p&gt;
&lt;h2&gt;Ansible and Helm: infrastructure-specific dependency patterns&lt;/h2&gt;
&lt;h3&gt;Ansible&lt;/h3&gt;
&lt;p&gt;Ansible dependencies appear in &lt;code&gt;requirements.yml&lt;/code&gt; (roles and collections), &lt;code&gt;galaxy.yml&lt;/code&gt; (collection metadata and dependencies), and &lt;code&gt;meta/main.yml&lt;/code&gt; (role dependencies).&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# requirements.yml
roles:
  - src: git+https://gitlab.com/org/ansible-roles/nginx.git
    version: v2.0

collections:
  - name: company.shared_collection
    version: &quot;&amp;gt;=1.0&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The complexity here is similar to Python: matching a Galaxy-style name (&lt;code&gt;company.shared_collection&lt;/code&gt;) to the repo that publishes it requires artifact detection. Git URL sources are more straightforward but still need normalisation.&lt;/p&gt;
&lt;h3&gt;Helm&lt;/h3&gt;
&lt;p&gt;Helm chart dependencies are declared in &lt;code&gt;Chart.yaml&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dependencies:
  - name: redis
    version: &quot;17.x&quot;
    repository: &quot;https://charts.bitnami.com/bitnami&quot;
  - name: auth-service
    version: &quot;1.2.0&quot;
    repository: &quot;https://helm.internal.company.com&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Public chart repositories (Bitnami, stable, etc.) are filtered out. Internal repository references need to be matched to the repos that build those charts. And &lt;code&gt;file://&lt;/code&gt; references to local charts in the same repo are internal — not cross-repo dependencies.&lt;/p&gt;
&lt;h2&gt;Kubernetes and Kustomize: the deployment layer&lt;/h2&gt;
&lt;p&gt;Kubernetes manifests and Kustomize configurations add another dependency surface — one that&apos;s often overlooked because it&apos;s at the &quot;deployment&quot; end of the pipeline rather than the &quot;build&quot; end.&lt;/p&gt;
&lt;p&gt;Kustomize&apos;s &lt;code&gt;kustomization.yaml&lt;/code&gt; can reference resources and bases from other repos:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;resources:
  - https://github.com/org/k8s-base//manifests/monitoring?ref=v1.0

bases:
  - github.com/org/shared-platform//overlays/production
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These are cross-repo references with the same double-slash subdirectory convention as Terraform. They need the same kind of URL normalisation and repo matching.&lt;/p&gt;
&lt;p&gt;Kubernetes manifests themselves reference Docker images in container specs, Helm charts in &lt;code&gt;HelmRelease&lt;/code&gt; custom resources, and ConfigMaps or Secrets by name. The image references connect back to the Docker dependency surface — another example of how the graph crosses ecosystem boundaries.&lt;/p&gt;
&lt;h2&gt;The real challenge: resolution&lt;/h2&gt;
&lt;p&gt;Parsing is the visible work. But the step that makes or breaks the dependency graph is &lt;strong&gt;resolution&lt;/strong&gt;. Taking a parsed reference and matching it to an actual repo or artifact in your org.&lt;/p&gt;
&lt;p&gt;A Dockerfile says &lt;code&gt;FROM registry.company.com/platform/base-image:v2&lt;/code&gt;. Which repo builds that image? The registry path might not match the repo path. The image might be built by a CI pipeline in a repo named &lt;code&gt;docker-base-images&lt;/code&gt;, pushed to a registry path of &lt;code&gt;platform/base-image&lt;/code&gt;. Connecting those requires understanding the &lt;em&gt;producing&lt;/em&gt; side, not just the &lt;em&gt;consuming&lt;/em&gt; side.&lt;/p&gt;
&lt;p&gt;This is why artifact detection matters so much. Before you can resolve &quot;this repo uses artifact X,&quot; you need to know &quot;that repo produces artifact X.&quot; For Docker images, this means scanning CI configs for &lt;code&gt;docker push&lt;/code&gt; commands. For Python packages, it means reading &lt;code&gt;pyproject.toml&lt;/code&gt; to find the package name. For Helm charts, it means reading &lt;code&gt;Chart.yaml&lt;/code&gt;. Each ecosystem has a different way of declaring what a repo produces, and the resolver needs to cross-reference all of it.&lt;/p&gt;
&lt;p&gt;Resolution also has to deal with ambiguity. A Docker image reference like &lt;code&gt;base-image:v2&lt;/code&gt;, without a full registry prefix, could match multiple repos. A Python package name might be normalised differently (&lt;code&gt;my_package&lt;/code&gt; vs. &lt;code&gt;my-package&lt;/code&gt;). Terraform module paths might use SSH vs. HTTPS URLs for the same repo. The resolver needs normalisation rules, fuzzy matching strategies, and a confidence model — because some matches are certain and others are best-effort.&lt;/p&gt;
&lt;p&gt;Getting this right is the difference between a dependency graph that people trust and one they abandon after finding three false edges.&lt;/p&gt;
&lt;h2&gt;Why cross-ecosystem matters&lt;/h2&gt;
&lt;p&gt;Any individual ecosystem&apos;s parsing problem is tractable. The Python community could build a Python dependency tracker. The Terraform community could build a module graph tool. And some have.&lt;/p&gt;
&lt;p&gt;But the actual dependency graph in a real organisation doesn&apos;t respect ecosystem boundaries. A Terraform module produces infrastructure that a Docker image is built on, which a CI pipeline deploys, which references a Helm chart, which pulls a shared Ansible role.&lt;/p&gt;
&lt;p&gt;If you only see the Terraform slice, you miss the Docker dependency that&apos;s about to break your deployment pipeline. If you only see Docker, you miss the Terraform module change that will change the infrastructure your image runs on.&lt;/p&gt;
&lt;p&gt;The value of cross-ecosystem discovery isn&apos;t additive — it&apos;s multiplicative. Each new ecosystem you add doesn&apos;t just give you more nodes in the graph. It reveals &lt;em&gt;connections between ecosystems&lt;/em&gt; that were previously invisible. Those connections are exactly where surprise breakage lives.&lt;/p&gt;
&lt;h2&gt;What I learned building this&lt;/h2&gt;
&lt;p&gt;I&apos;ve been building &lt;a href=&quot;https://riftmap.dev&quot;&gt;Riftmap&lt;/a&gt; to solve this problem — auto-discovering cross-repo dependencies across all the ecosystems described above and presenting them as a queryable, visual graph with blast radius analysis.&lt;/p&gt;
&lt;p&gt;A few things I&apos;ve learned along the way:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The parser is the easy part.&lt;/strong&gt; Extracting &lt;code&gt;source = &quot;...&quot;&lt;/code&gt; from a Terraform file is straightforward. The hard parts are resolution (matching references to actual repos), freshness (keeping the graph current as repos change), and staleness detection (knowing when a previously-discovered dependency no longer exists because a repo was renamed or archived).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Confidence matters.&lt;/strong&gt; Not all discovered dependencies are equally certain. A Terraform &lt;code&gt;source&lt;/code&gt; with a full git URL and a pinned ref is high confidence. A Docker &lt;code&gt;FROM&lt;/code&gt; with variable substitution and no default value is low confidence. Exposing this confidence to users — rather than pretending everything is equally certain — is critical for trust.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The graph is the product, not the report.&lt;/strong&gt; Every DIY solution I&apos;ve seen generates a static output — a CSV, a SQLite dump, a rendered image. The real value comes when the graph is interactive, queryable, and always current. &quot;Show me every repo affected if I change this module&quot; should be a click, not a pipeline run.&lt;/p&gt;
&lt;p&gt;If you&apos;re building infrastructure for a platform team and this problem resonates, I&apos;d love to hear how it shows up in your stack. The edge cases are different for every org, and understanding them is how the tooling gets better.&lt;/p&gt;
&lt;p&gt;You can see more at &lt;a href=&quot;https://riftmap.dev&quot;&gt;riftmap.dev&lt;/a&gt;, or reach me at &lt;a href=&quot;mailto:hello@riftmap.dev&quot;&gt;hello@riftmap.dev&lt;/a&gt;.&lt;/p&gt;
</content:encoded><category>platform-engineering</category><category>infrastructure</category><category>terraform</category><category>docker</category><category>kubernetes</category><category>helm</category><category>ansible</category><category>python</category><category>go</category><category>npm</category><category>dependency-management</category><category>devops</category><category>multi-repo</category><category>static-analysis</category><category>infrastructure-as-code</category><author>Daniel Westgaard</author></item><item><title>The Infrastructure Dependency Problem No One Has Solved</title><link>https://riftmap.dev/blog/infrastructure-dependency-problem/</link><guid isPermaLink="true">https://riftmap.dev/blog/infrastructure-dependency-problem/</guid><description>Why every platform team eventually builds the same brittle script, and why it keeps breaking. A deep look at cross-repo dependency visibility — the infrastructure problem hiding in plain sight.</description><pubDate>Mon, 30 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I&apos;ve worked as an infrastructure consultant in Norway for about two and a half years. That&apos;s not long. But in that time, every single client I&apos;ve worked with has had a version of the same problem.&lt;/p&gt;
&lt;p&gt;Someone pushes a change to a shared Terraform module. An hour later, Slack lights up. Three teams are broken. Nobody knew they depended on that module. Or rather, &lt;em&gt;one person&lt;/em&gt; knew. And they left six months ago.&lt;/p&gt;
&lt;p&gt;The dependency chain was always there. It just wasn&apos;t visible to anyone.&lt;/p&gt;
&lt;p&gt;This post is about that problem: the complete absence of cross-repo dependency visibility in modern infrastructure teams. It&apos;s a problem I kept running into, kept hearing about, and eventually decided to build something to fix. But before I talk about the fix, I want to talk about why the problem exists, why it&apos;s getting worse, and why the &quot;obvious&quot; solutions don&apos;t actually work.&lt;/p&gt;
&lt;h2&gt;What is cross-repo dependency visibility?&lt;/h2&gt;
&lt;p&gt;Most engineering teams today operate in a polyrepo setup. Not because they chose it carefully — usually because the org grew, teams split, acquisitions happened, and now there are a hundred or more repos across GitLab or GitHub.&lt;/p&gt;
&lt;p&gt;Inside those repos, there are dependencies everywhere. A Terraform module in one repo is sourced by twelve others. A Docker base image built in one pipeline is pulled by thirty Dockerfiles across the org. A GitLab CI template is included by eighty pipelines. A Helm chart references another internal chart. An Ansible role depends on a shared collection.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cross-repo dependency visibility&lt;/strong&gt; means being able to answer one question: &lt;em&gt;if I change this thing, what else is affected?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Not &quot;what &lt;em&gt;might&lt;/em&gt; be affected.&quot; Not &quot;let me grep around and hope I find everything.&quot; But a clear, always-current, automatically maintained answer.&lt;/p&gt;
&lt;p&gt;Almost nobody has this.&lt;/p&gt;
&lt;h2&gt;Why this problem shows up at every growing org&lt;/h2&gt;
&lt;p&gt;The pattern is remarkably consistent. Here&apos;s how it plays out:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;At 10–30 repos&lt;/strong&gt;, you probably don&apos;t need tooling. A senior engineer holds the dependency map in their head. They can tell you &quot;oh, if you touch the VPC module, repos A, B, and C use it.&quot; This works fine. It&apos;s fast. It&apos;s accurate. It requires zero infrastructure.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;At 50–100 repos&lt;/strong&gt;, the mental model starts cracking. Nobody can hold all the relationships in their head anymore. People start grepping across repos; cloning everything locally, running &lt;code&gt;ripgrep&lt;/code&gt;, building a picture manually. It takes hours. The results are point-in-time snapshots that are stale by the next morning.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;At 100–500+ repos&lt;/strong&gt;, grep breaks down entirely. The dependency graph is too large, too dynamic, and spans too many ecosystems to track manually. The &quot;right&quot; answer to &quot;who uses this module?&quot; becomes &quot;ask in the #platform channel and wait for replies.&quot;&lt;/p&gt;
&lt;p&gt;The breaking point is almost always the same: someone pushes a breaking change and multiple downstream consumers fail. Nobody knew who to notify. Or a senior engineer leaves and their mental map of the org leaves with them. Or a new hire joins the platform team and literally cannot understand what depends on what.&lt;/p&gt;
&lt;h2&gt;It&apos;s not a Terraform problem — it&apos;s an org-wide visibility problem&lt;/h2&gt;
&lt;p&gt;When I first encountered this, I thought it was a Terraform thing. Terraform modules get sourced across repos, and there&apos;s no built-in way to know who&apos;s consuming a given module at which version.&lt;/p&gt;
&lt;p&gt;But it&apos;s much bigger than Terraform.&lt;/p&gt;
&lt;p&gt;The real-world dependency graph for a platform team includes Terraform module sources (via git URL or registry), Docker base images (often pinned to &lt;code&gt;:latest&lt;/code&gt; everywhere), GitLab CI templates included by dozens of pipelines, GitHub Actions reusable workflows, Helm charts referencing other internal charts, Ansible roles and collections, Python packages installed from internal git URLs, Go modules, and npm packages published to internal registries.&lt;/p&gt;
&lt;p&gt;These dependencies cross ecosystem boundaries. A Terraform module might reference a Docker image that&apos;s built by a CI pipeline that uses a shared template. The graph doesn&apos;t respect neat tool categories, and no tool today shows you the full picture across all of them.&lt;/p&gt;
&lt;h2&gt;The tribal knowledge problem&lt;/h2&gt;
&lt;p&gt;The part of this that resonates most with people — I&apos;ve seen it in conversations, in Reddit threads, in every team I&apos;ve worked with — isn&apos;t the technical complexity. It&apos;s the human cost.&lt;/p&gt;
&lt;p&gt;Infrastructure dependency knowledge is &lt;strong&gt;tribal knowledge&lt;/strong&gt;. It lives in the heads of whoever has been around the longest. There&apos;s no shared, always-current view. There&apos;s no way for a new engineer to sit down and understand the full dependency picture without weeks of archaeology.&lt;/p&gt;
&lt;p&gt;When that senior engineer goes on holiday, decisions slow down. When they leave the company, that knowledge is gone permanently. The team doesn&apos;t even know what they&apos;ve lost until something breaks and nobody can explain why.&lt;/p&gt;
&lt;p&gt;This is the real cost: not that the dependency graph is complex, but that &lt;strong&gt;the only copy of it exists in human memory, and human memory doesn&apos;t scale, doesn&apos;t transfer, and doesn&apos;t survive attrition&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;What people try — and where each approach falls short&lt;/h2&gt;
&lt;p&gt;I&apos;ve seen teams try every reasonable approach. Each one solves part of the problem and fails at the rest.&lt;/p&gt;
&lt;h3&gt;Grep / ripgrep across repos&lt;/h3&gt;
&lt;p&gt;The first tool everyone reaches for. Clone the repos, run a search, assemble the picture manually.&lt;/p&gt;
&lt;p&gt;It works, for a while. The results are a point-in-time snapshot with no history, no dashboard, and no way to handle transitive dependencies. At a hundred repos, it takes hours. It doesn&apos;t catch renamed references or implicit dependencies. And the results are stale the moment you close the terminal.&lt;/p&gt;
&lt;h3&gt;Backstage or a service catalog&lt;/h3&gt;
&lt;p&gt;Backstage looks like the right answer on paper. It&apos;s a developer portal with a service catalog. You define your dependencies in a &lt;code&gt;catalog-info.yaml&lt;/code&gt; file per repo, and Backstage renders a graph.&lt;/p&gt;
&lt;p&gt;The problem: it requires humans to register and maintain those YAML files. In practice, the catalog goes stale within weeks. As one engineer put it to me, it&apos;s &quot;documentation with extra steps.&quot; If the dependency is registered in a YAML file that nobody updates, it&apos;s just a different kind of tribal knowledge.&lt;/p&gt;
&lt;h3&gt;Renovate or Dependabot&lt;/h3&gt;
&lt;p&gt;These tools are excellent at what they do: they automatically open pull requests when a dependency has a new version available. They solve the &lt;em&gt;upgrade lag&lt;/em&gt; problem.&lt;/p&gt;
&lt;p&gt;But they don&apos;t solve the &lt;em&gt;visibility&lt;/em&gt; problem. Renovate doesn&apos;t tell you &quot;who is consuming module X at which version right now.&quot; It reacts &lt;em&gt;after&lt;/em&gt; a new version is published. It can&apos;t answer the question you need before you make a breaking change: &quot;if I push v2.0 of this module, which 40 repos need to coordinate?&quot;&lt;/p&gt;
&lt;h3&gt;HCP Terraform Explorer&lt;/h3&gt;
&lt;p&gt;HashiCorp&apos;s cloud platform has a module explorer that shows workspace-to-module relationships. It&apos;s the closest native solution.&lt;/p&gt;
&lt;p&gt;But it only works for Terraform. It only works if your plan and apply runs through HCP Terraform. If your org has a mix of Terraform Cloud, self-hosted runners, Docker, CI templates, and Helm — and most do — you get a partial view with large blind spots.&lt;/p&gt;
&lt;h3&gt;&quot;Just use a monorepo&quot;&lt;/h3&gt;
&lt;p&gt;This is the most commonly suggested solution, and it&apos;s the least helpful for the teams who actually have this problem.&lt;/p&gt;
&lt;p&gt;Monorepos are a valid architecture. But suggesting a monorepo to an org with three hundred existing repos, multiple teams with separate access controls, compliance boundaries, and maybe some repos inherited through an acquisition — that&apos;s like suggesting &quot;just rewrite it in Rust.&quot; Technically valid. Not actionable.&lt;/p&gt;
&lt;p&gt;And a monorepo still doesn&apos;t give you blast radius analysis. It just makes the grep slightly easier.&lt;/p&gt;
&lt;h3&gt;Building it yourself&lt;/h3&gt;
&lt;p&gt;This is the one that surprised me. Multiple teams I&apos;ve talked to — and many more on Reddit — have independently built their own bespoke solutions. Nightly cron jobs that shallow-clone every repo, grep Dockerfiles and Terraform source blocks, dump the results to SQLite or a spreadsheet.&lt;/p&gt;
&lt;p&gt;These solutions work. They prove the approach is sound. But they&apos;re brittle, org-specific, hard to maintain, and they go stale between runs. Nobody has the bandwidth to turn their weekend hack into a real product. They &lt;em&gt;want&lt;/em&gt; a product to exist. It just doesn&apos;t.&lt;/p&gt;
&lt;aside&gt;
&lt;p&gt;&lt;span&gt;&lt;/span&gt;See it live&lt;/p&gt;
&lt;p&gt;See the problem solved, live&lt;/p&gt;
&lt;p&gt;Every workaround above goes stale because it&apos;s maintained by hand. This isn&apos;t: open the live Prometheus graph, scanned straight from source, click any repo, and read its cross-repo dependencies and blast radius from one pane.&lt;/p&gt;
&lt;a href=&quot;https://riftmap.dev/showcase/prometheus/&quot;&gt;Explore the live graph →&lt;/a&gt;
&lt;/aside&gt;
&lt;h2&gt;Why the problem is getting worse, not better&lt;/h2&gt;
&lt;p&gt;Two trends are making this more painful every year.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Tool diversity is increasing.&lt;/strong&gt; Five years ago, a platform team might have only been dealing with Terraform. Today the same team manages Terraform modules, Docker images, CI pipelines, Helm charts, Kubernetes manifests, and maybe Ansible. Every new tool in the stack adds a new class of cross-repo dependency that no existing tool tracks.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AI-assisted development is accelerating the sprawl.&lt;/strong&gt; Teams ship code faster than ever with AI coding tools. But the AI writes the module — it doesn&apos;t know that three repos in another part of the org reference the interface it just changed. AI accelerates the &lt;em&gt;creation&lt;/em&gt; of infrastructure code without any awareness of the existing dependency graph. That gap compounds every sprint.&lt;/p&gt;
&lt;p&gt;The irony is that the tools keep getting better at everything &lt;em&gt;except&lt;/em&gt; showing you how it all connects.&lt;/p&gt;
&lt;h2&gt;The pattern that works: auto-discovery from code&lt;/h2&gt;
&lt;p&gt;Across every DIY solution I&apos;ve seen — and across the engineering community discussions I&apos;ve been part of — the approach that works is always the same:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Scan the org&lt;/strong&gt; — enumerate every repo via the GitLab or GitHub API.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Parse the actual files&lt;/strong&gt; — Terraform source blocks, Dockerfile &lt;code&gt;FROM&lt;/code&gt; statements, CI include directives, Helm chart dependencies, Python requirements, Go module files, npm manifests. Not metadata. Not YAML someone fills in. The real source files.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Build a directed graph&lt;/strong&gt; — repo A depends on artifact X, which is produced by repo B. Store this as a queryable graph.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Keep it fresh&lt;/strong&gt; — webhook-triggered or scheduled rescans so the graph is never stale.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Make it queryable&lt;/strong&gt; — &quot;show me everyone who consumes this Docker base image&quot; should be a one-click answer, not a two-day grep expedition.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The fact that independent practitioners keep arriving at this same architecture tells you something important: the approach is sound. What&apos;s missing is a product that does it well, keeps it current, and works across every ecosystem a team actually uses.&lt;/p&gt;
&lt;h2&gt;What I&apos;m building&lt;/h2&gt;
&lt;p&gt;This is the problem that led me to build &lt;a href=&quot;https://riftmap.dev&quot;&gt;Riftmap&lt;/a&gt;. It scans a GitLab or GitHub organisation, automatically discovers cross-repo dependencies across Terraform, Docker, CI templates, Python, Go, npm, Ansible, Helm, Kubernetes, Kustomize (and more coming) — and builds a queryable dependency graph with visual blast radius analysis.&lt;/p&gt;
&lt;p&gt;No per-repo configuration. No YAML to maintain. One read-only token at the org level, and it discovers everything from the actual code.&lt;/p&gt;
&lt;p&gt;It&apos;s currently in early access. If this problem sounds familiar, I&apos;d genuinely like to hear how it shows up at your org — whether or not Riftmap is the right solution for you. The more I understand how different teams experience this, the better the tooling gets for everyone.&lt;/p&gt;
&lt;p&gt;You can see more at &lt;a href=&quot;https://riftmap.dev&quot;&gt;riftmap.dev&lt;/a&gt;, or reach me directly at &lt;a href=&quot;mailto:hello@riftmap.dev&quot;&gt;hello@riftmap.dev&lt;/a&gt;.&lt;/p&gt;
</content:encoded><category>platform-engineering</category><category>infrastructure</category><category>terraform</category><category>dependency-management</category><category>devops</category><category>multi-repo</category><category>blast-radius</category><author>Daniel Westgaard</author></item></channel></rss>