One graph or two: how we chose CodeNodes for RCF's spec-to-code bridgeCopy link
RCF's traceability chain was complete on the spec side and stopped dead at the code. Here is the design call I made to carry it across, and the two-graph approach I turned down to get there.
RCF's job is to hold a chain intact from a business requirement down to the test that proves it. Requirement, user story, acceptance criterion, test spec, test case. Every link is a real edge in a graph, and a chain-integrity check walks it on every commit and fails the build the moment a reference stops resolving. That part works. I run it against my own builds every working day.
Then the chain reaches the code, and it stops.
The last hop is a testPointer: a string field on the test case where someone writes src/foo.test.js#L42. It's a note, not an edge. The walker doesn't follow it into the file. And it points at the test, which is a further hop away from the implementation the requirement is really about. So the honest state of the graph, until last week, was this: complete over the spec, with pointers to tests, and no representation of the code inside it at all.
That leaves blast radius broken in both directions. Change a requirement and rcf impact enumerates every story, criterion, spec and test case downstream, then stops at the test case. I know which tests to re-run. I don't know which source files carry the change. Go the other way, change src/store/writer.js, and RCF has nothing to say at all. There is no edge running from a source file back into the spec. To find out what a code change threatens, you run the tests and watch what falls over.
There is one case where that gap actually bites, and it's the one that kept nagging me. The competent refactor. An engineer, or an agent, renames a function, moves the logic somewhere tidier, updates the callers, and keeps every test green. Nothing fails. The chain still reports clean, because the chain never reached the code. The link between an acceptance criterion and the thing meant to satisfy it is now pointing at a symbol that moved, and nobody has been told. Green suite, broken correspondence, silent.
The thread that started itCopy link
This came into focus for me in a LinkedIn thread with Manoj Kolluri, who is working the same gap from the other end. His approach is to build a dependency graph of the whole codebase, graphify-style, and then isolate the network of nodes a spec change would touch. Parse the code into a graph, seed it with the nodes the spec touches, traverse to the impacted set, focus the verification there.
It's a good approach, and I'll come back to why it composes well with what I ended up building. But talking it through forced me to catalogue every way I could think of to carry the chain across the boundary, and the useful realisation was that they don't sit on one menu. They split into two layers.
A seeder answers one question: how do you get from an acceptance criterion to the initial set of code it directly touches? Its output is a set of criterion-to-path links, ideally criterion-to-symbol.
An extender answers a different one: given those seeds, how do you walk the codebase's own dependency graph to find everything transitively affected?
Neither layer alone gives you per-file blast radius. A seeder without an extender gives you direct hits and misses the ripple. An extender without a seeder is a walk with no starting point.
The honest menu of seedersCopy link
I looked at six ways to produce the seeds.
Symbol matching. Scan the spec for identifier-shaped tokens, resolve them against a tag file. Deterministic, no author effort, symmetric. Only as good as whether the spec actually mentions the symbols, and many criteria are behavioural, not identifier-shaped.
Semantic inference. Match spec text to source by LLM or embedding similarity. Rejected on the spot. It breaks the one rule RCF doesn't bend: no model judgement in the core tool. If it ever enters the estate it belongs in a harness above rcf, not inside it.
Manual annotation. Inline // @rcf AC-301-1 markers, scanned into a map. It works, and it adds a permanent hand-maintained surface that rots the moment code moves.
Spec-tagged commits. Trailers in commit messages, parsed out of git history. Deterministic, but the grain is wrong. A typo fix and a load-bearing refactor get attributed the same way.
The test-coverage bridge. RCF already has criterion to test case to testPointer. Run the tests with coverage collection in the test stage and you get test case to source paths for free, whatever nyc or jest or pytest-cov produced. Compose them and you have criterion to code, deterministic, no author effort, both directions. Fine-grained on tested code, blind to anything untested.
FBS (Functional Build Specification) post-hoc paths. When a build spec is marked complete, capture the files it touched via git diff and store them on the spec. Cheap, coarse, complete on shipped code.
For a while, the last two were my answer. The coverage bridge plus the post-hoc paths. They're cheap, additive, they hold the no-LLM line, and between them they cover most real change-impact queries: precise on tested code, coarse but complete on shipped code. If you'd asked me a week ago what the first phase was, that's what I'd have said.
Why I turned down the two-graph approachCopy link
Manoj's graph is over code. RCF's is over spec. The tempting move is to run both and join them at query time. I didn't, and it's worth being precise about why, because the external dependency-graph tools (graphify, dependency-cruiser, the per-language equivalents) are genuinely capable. Given seeds, they traverse beautifully.
The catch is that traversal is the extender half. It doesn't answer the seeder question, it assumes it's already answered. Every external code graph still needs someone to hand it the nodes the spec touches. That's the seed problem, and it's the hard part. Parse, traverse, focus are the solved-ish steps. Seed is the one step that's genuinely difficult, and it's the one the external graph leaves to you. Manoj said as much himself; he called the seeding the work-in-progress.
Then there's a structural cost I didn't want to carry. Two graphs means two sources of truth. The spec's truth lives in RCF, the code's truth lives in the external graph, and the correspondence between them, the exact thing I care about, lives in neither. When it drifts, the drift is invisible, because no single system holds both ends of the link. That is split-brain, and it is precisely the failure mode RCF exists to kill on the spec side. I wasn't about to reintroduce it at the code boundary.
Add the per-language dependency, one graph tool per language, each with its own accuracy holes around dynamic dispatch and DI containers, and the external graph reads as a fine on-demand helper and a poor foundation. It answers the extender question well. It doesn't answer the seeder question, and the seeder question was the one I was stuck on.
The CodeNodeCopy link
So I built the option I'd been deferring, to see whether it earned its cost.
A CodeNode: a first-class RCF document, same as any other in the chain. It carries a path and symbol as its identity (src/store/writer.js#updateDocument), the acceptance criteria it implements, and its dependencies on other CodeNodes. The walker learns three new maps, and the graph extends into the code. One graph, from requirement down to source symbol, walkable in both directions.
The property that makes it worth the cost is one the two-graph split can't have: the link breaks visibly. Because a CodeNode lives inside the same graph the integrity check walks, a criterion pointing at a symbol that has moved becomes a dangling edge, and rcf validate catches it exactly like any other broken link.
I built a proof of concept to test that claim. The verdict was go, and the results are what carry the decision:
- The competent refactor, the case that started all this. I refactored so every existing test stayed green, the exact case where the suite passes and a human forgets.
rcf validatereturned exit 3 with a structuredstaleCodeerror naming the node whose symbol had moved. The thing that slips past a green suite did not slip past this. - Zero false positives across the run.
- Repair was a single field edit. Point the node at the new symbol, validate goes green.
- Granularity paid for itself. A symbol-level CodeNode gave a one-criterion blast radius where file-level attribution gave four. The finer identity lands a change on the criteria it actually touches, not the whole file's worth.
- Fully additive. 725 of 725 existing tests stayed green; the walker extension disturbed nothing already there.
- The dependency edges gave the nicest result. To build the test I'd drawn the links between code nodes by hand. Then I ran a standard tool that reads the code and works out what depends on what. It confirmed every link I'd drawn and found three real ones I'd missed. The hand-drawing was only scaffolding for the test; the signal that matters is that an automated pass can confirm and fill in those links, so the graph never depends on a human remembering to draw them.
That last one is the reconciliation with Manoj. The external graph tool I turned down as a foundation is excellent as a check on those links. It composes. It just composes as a helper on top of the one graph, not as a second graph beside it. His extender isn't a competitor to the CodeNode; it's the thing that fills in and audits the dependency edges once the seeds exist inside RCF. The shared interchange is a path and a symbol. His work sits directly on top of mine. That's the complement I wanted out of the thread and didn't have a mechanism for a week ago.
What it still can't doCopy link
The numbers don't get to carry an overclaim, so here's the limit.
A CodeNode links a criterion to an identifier. If someone changes what a function does while leaving its name and signature intact, and the tests don't catch the behaviour change, the CodeNode won't either. The identifier still resolves, the edge is still valid, the meaning drifted underneath it. Semantic drift behind an intact name is undetectable by any deterministic method. Not this one, not the two-graph one, not any of the six seeders. What actually catches a function quietly changing what it does is a properly defined acceptance criterion and a test case that genuinely implements it. That is the real defence, and it always was. The graph's job is narrower: keep the links between a criterion, its test and the code visible, and make them break the moment the code moves out from under the spec. The CodeNode catches the structural break, the moved or deleted symbol. It does not read intent, and I'm not going to pretend it does.
Two more costs I took on with open eyes. Granularity is a trade: symbol-level nodes are precise, and there are more of them to keep current. And a CodeNode is a document, which means it rots if nothing keeps it honest. Which is where the two seeders I'd started with come back in.
The coverage bridge and the build-diff paths come back as generatorsCopy link
The coverage bridge and the post-hoc path capture didn't lose the argument. They changed jobs.
Instead of being competing seeders that produce a separate map, they become the machinery that generates and maintains the CodeNodes. Coverage tells you which source a criterion's tests actually exercise. The mark-complete diff tells you which files a build spec touched. Both feed the CodeNode set so it isn't all hand-authored, and both keep it current as the code moves. The thing I liked about them, deterministic, harness-side, no author effort, is exactly what you want from a generator. They were never the wrong idea. They were answering the seeder question for a graph I hadn't built yet.
Where this leaves itCopy link
One graph, not two. The chain runs from the requirement to the source symbol and back, and it breaks in the open when the code moves out from under the spec. It's a proof of concept, not a shipped feature; the next phase is the build. But the design call is made, and the PoC is why I'm confident making it.
If you're carrying the same gap, the question the thread handed me is the one to sit with. Are you seeding, or are you extending? Most tools reach for the extender, because it's the satisfying, visible half, the graph you can draw. The seed is where the honesty is.
More on the methodology at stravica.ai/rcf-methodology.
Blurted out by Barry, refined by Dave.