I maintain slnmap, a tool that gives AI coding agents a compiler-accurate graph of .NET codebases. It's built on Roslyn, so if the compiler sees a reference, the graph has the edge. But it stops at the C# boundary. The question I kept getting was: can it cross into the frontend? Can "what breaks if I change this handler" end with "these three React screens"?
I used to think the honest answer was no. I actually had evidence — an earlier prototype of mine tried exactly this with heuristic matching. Name-equality joins, filename-as-truth, take-first guesses written into the graph as real edges. Its own docs ended up flagging the results as unreliable. A graph that guesses is worse than no graph, because someone eventually trusts it.
So before building anything this time, I gave myself one day and one question: is cross-stack linking deterministic, or is it doomed to heuristics no matter what?
The setup was a ~200-line throwaway script using the TypeScript Compiler API — ts.createProgram plus the type checker, the real one, not a regex parser. I pointed it at a production React/Next.js frontend, 2,570 files, and matched what it found against the route data my Roslyn side already extracts.
Three things came out of it.
The type checker solves the exact thing that killed the old prototype. The old attempt gave up on 23% of call sites as "variable URLs" — stuff like apiClient.get(API_ROUTES.VENDORS) where the URL lives in a constant behind a barrel file, three imports away. The checker's literal types fold those constants. Not "probably this string" — this string, resolved the same way the compiler resolves it. Most of that 23% bucket just evaporated.
The numbers held up. 675 frontend HTTP call sites, 96.6% resolved to a deterministic route template. The remaining 3.4% (runtime-computed segments, env-dependent bases) are detectable and countable, so the tool can say "these 23 sites I can't resolve, here's why" instead of guessing. That's already the design rule for the whole tool: anything not statically resolvable gets counted and disclosed, never guessed.
And my favorite part — the experiment caught a real bug before the feature even exists. While matching frontend calls against backend routes, the script found a live dialog POSTing to /organizationusers. That endpoint doesn't exist on the backend. A production screen quietly sending requests into the void. grep would never surface this, because the two halves of the bug live in different languages.
Looking back, the old prototype wasn't wrong to try. It was wrong to disable the type checker (for speed) and then paper over the gap with heuristics. That lesson generalizes, I think: in static analysis, the moment you start guessing to improve coverage, you've traded away the only thing that made the tool worth trusting.
So it's buildable. The C# half already shipped — HTTP endpoints are graph nodes as of v0.7/v0.8, Minimal APIs and attribute-routed controllers, 658/658 registrations resolved on my reference codebase. The TypeScript extractor is what I'm building now. When the two halves join, impact_analysis on a C# handler will end its chain at the React components that break.
slnmap is free and MIT: dotnet tool install --global slnmap — repo at github.com/EMahmoudNabil/slnmap. If you've tried cross-stack analysis and hit the heuristics wall, I'd like to hear how far you got.
Top comments (1)
Compiler-accurate frontend/backend linkage is a great goal because most API drift is discovered too late. The interesting test is not whether the first mapping works, but whether refactors on either side fail loudly before runtime.