-
-
blastRadius — Agentic code-risk analysis tool. Enter a repo path to ingest it, then step through a 3-stage workflow.
-
BlastRadius after successfully ingesting the demo_repos/buzzbuddy_backend repo.
-
Impact graph for _run_agent_turn_or_502: 4 affected functions, 3 edges, within 2 hops.
-
AI-generated risk analysis: _run_agent_turn_or_502 flagged HIGH severity as a critical error-handling boundary.
Inspiration
Every engineer knows the moment: you're about to change a function and you pause, because you're not sure what else calls it. grep gives you text matches, not structure. IDEs give you "find usages" for one hop, if you're lucky. Nobody actually walks the call graph and tells you, in plain English, what's at stake.
We wanted to build something that treats the call graph as a first-class thing you can traverse and reason over — not a side artifact of static analysis, but the actual data model the program operates on. Jac, where the graph is the data structure and traversal is a language primitive, was the obvious fit.
How we built it
graph.jac— the schema:File/Functionnodes,Calls/Importsedges.parser.py— a stdlib-ast-based Python parser, zero dependencies.ingest.jac—def:pub ingest_repo(repo_path), deliberately a function and not a walker, since it builds the graph rather than walking it.blastradius.jac—walker:pub BlastRadius(target_fn, max_hops), the real traversal: spawned onto aFunctionnode, it followsCallsedges outward hop by hop, guarded against cycles and bounded bymax_hops, collecting every affected function into areportslist as it moves.agent.jac—def:pub explain_risk(...) by llm(tools=[...]), a typed LLM call with real tool-calling (read_source,suggest_tests), grounded in the walker's output viasemprompt annotations that explicitly tell the model to trust the traversal instead of re-guessing it.main.sv.jac— the entry point that wires every public endpoint into one running service.
Everything — REST endpoints, Swagger docs, a live graph view, persistence — comes out of a single jac start. There's no separate backend to stand up.
We verified it end-to-end against its own source: ingesting the BlastRadius repo itself and asking for the blast radius of _module_path correctly returns _module_path and its one real caller, parse_repo.
Challenges we ran into
The hardest bugs weren't in our four .jac files — they were in getting a framework actually call a real model in production, and each one only surfaced because werefused to trust a 200 OK as proof anything actually worked:
- A silently-mocked LLM call.
explain_riskworked fine injac testagainst aMockLLM, but once wired tojac startwith a real API key, it kept returning the exact same canned sentence no matter the input. We proved it wasn't hitting any model at all by feediid API key and nonsense function names — same output, same success code, every time.The real root cause took real digging to find:.test.jacfiles are annexes that permanently compile into their base module, not test-scoped files — so our test file's module-levelglob llm = MockLLM(...)was overriding the ambient model in every run mode,jac startincltest we wrote. The fix was moving the mock into a runtime reassignment inside thetest body itself, so it only shadows the global for that one test's execution instead of merging into the compiled module permanently. - A platform assumption we didn't know about. Deploying to JacHammer, the previen't read main.jac
— even though ourjac.tomlexplicitly setsentry-point ="main.sv.jac". JacHammer's preview supervisor hardcodesmain.jac` regardless of that config, so we added a matching entry file. - A silent download failure. Trying to avoid needing our own OpenAI billing, we via
jac model pull— which reported success (exit code 0) twice while actuallyleaving a truncated, unusable file on disk both times, stalled at the identical byte offset. Traced to unauthenticated Hugging Face Hub rate limiting that the tool doesn't surface as a failure.
What we learned
Jac's graph-as-data-model is genuinely different from bolting a graph library onto Python — walkers as first-class traversal primitives make "compute the blast radius" a natural fit instead of aworkaround dressed up to look like one. We also relearned a harder lesson: with any tatus code is not evidence of success. Every real bug we hit this weekend wasinvisible until we deliberately forced a failure path — a bad API key, a nonsense input, a byte-for-byte disk check — and watched the tool still claim it worked.
That same instinct carried into hardening the API itself once it was actually working: ingest_repo on a nonexistent path used to return a fake 200 OK with all-zero counts instead of a clear error, and BlastRadius on an unknown function name returned an ambiguous empty lis clearly instead of silently. We also found that our two LLM tool functions(read_source, suggest_tests) had zero direct test coverage — they were only ever exercised if the model happened to call them — so we added tests that call them directly, not just through the model's own judgment.
Built With
- anthropic
- byllm
- claude
- graph
- jac
- jachammer
- jaclang
- kubernetes
- litellm
- llm
- python
- rest-api
- sqlite
- swagger
Log in or sign up for Devpost to join the conversation.