tags: [codeview, d3, graph, adapter-pattern, byproduct, open-source, ego-mode, ui] related:
- 037_two-layers-semantic-structural.md
- 032_pre-index-your-codebase.md status: current —
038 — codeview: How a Code Intelligence UI Gets Built as a Byproduct
The best tools in a project are often the ones that weren’t planned.
codeview started as a single sentence: “can we have a visual for all this?” The session already had CodeGraph indexed, NLP contracts working, and a merged adapter that combined both. The question was whether the data could be made browsable — a graph you could actually look at and navigate rather than query through a CLI.
Three hours later it was running against the DarJS project showing 2,042 nodes and 1,537 edges.
This is the story of how it got built, and why the architectural decisions made it open-sourceable as a side effect.
The Decision That Determined Everything
The first decision was the one that mattered: should codeview know anything about DarJS?
The easy path was to hardcode it. The data was sitting in a DarJS project. The server could just read _publish/contracts-graph.json directly. Done in twenty minutes.
The better path was to make it not know. Define an adapter interface — four methods — and put all the DarJS-specific reading behind it. Then codeview could work against any project that had a data source it understood.
The interface:
function validateAdapter(adapter) {
const required = ['getNodes', 'getEdges', 'getDetails', 'getHealth'];
for (const method of required) {
if (typeof adapter[method] !== 'function') {
throw new Error(`Adapter missing required method: ${method}`);
}
}
}
Four methods. Any object that implements them is a valid data source. Nothing else is required.
This decision cost maybe thirty extra minutes. It meant writing DarJSAdapter, CodeGraphAdapter, and MergedAdapter as separate classes rather than inline logic. In exchange, the tool became usable on any project, not just DarJS. The open-source path opened.
Auto-Detection
With adapters in place, the next question was how to start the server. The cleanest answer: don’t make the user pick. Detect what’s present and use it.
function detect(projectDir) {
const contractsPath = path.join(projectDir, '_publish/contracts-graph.json');
const dbPath = path.join(projectDir, '.codegraph/codegraph.db');
const hasDar = fs.existsSync(contractsPath);
const hasCodeGraph = fs.existsSync(dbPath);
if (hasDar && hasCodeGraph) {
return { adapter: new MergedAdapter(contractsPath, dbPath), sources: ['darjs', 'codegraph'] };
}
if (hasCodeGraph) {
return { adapter: new CodeGraphAdapter(dbPath), sources: ['codegraph'] };
}
if (hasDar) {
return { adapter: new DarJSAdapter(contractsPath), sources: ['darjs'] };
}
throw new Error('No data source found. Run codegraph init or dar contracts build first.');
}
Point it at a directory. It figures out the rest. The CLI becomes one command:
codeview serve /path/to/project
# or, from DarJS:
dar codeview
No configuration. No flags to pick a source. If both are present, it uses both.
No Build Step
The UI runs in the browser with no bundler. Alpine.js handles reactivity. D3 v7 handles the graph. Both come from CDN.
<script defer src="https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js"></script>
The server is a plain Node.js http.createServer. Static files from public/. JSON from seven endpoints. No Express, no webpack, no TypeScript compilation.
This is a deliberate choice for a tool at this stage. A build step adds friction to every change and every deployment. For a tool that will be iterated on, cloned, modified, and potentially run in unfamiliar environments, the zero-dependency path keeps the door open. Anyone with Node.js can run it.
The tradeoff is that public/app.js and public/graph.js are vanilla ES5. No modules, no imports. For a tool this size — two files, ~300 lines each — that is not a real constraint.
The Graph Design Problem
Force-directed graphs have a cognitive load problem. Put two thousand nodes on a canvas with edges between them and you get a hairball. No node is navigable. The graph is visually impressive and practically useless.
Three decisions reduced this to something workable:
Node size by reference count. Nodes with ten or more references render at 11px radius. Nodes with four or more at 7px. Everything else at 4px. The high-traffic nodes — framework entry points, shared utilities — are immediately visible as larger circles. The long tail of leaf nodes recedes into background dots.
Labels hidden until zoom threshold. Labels appear only when zoom exceeds 1.4x. At full zoom-out, you see the shape of the graph — clusters, density, isolated nodes. Zoom in to read names. This is how maps work, and it works here for the same reason.
Ego mode. Click a node. Click “Focus: show connections only.” Everything non-adjacent drops to 4% opacity. One node and its direct neighbors, clearly visible. The full graph dims to a ghost.
function focusEgo(nodeId) {
const connected = new Set([nodeId]);
validEdges.forEach(e => {
const sid = typeof e.source === 'object' ? e.source.id : e.source;
const tid = typeof e.target === 'object' ? e.target.id : e.target;
if (sid === nodeId) connected.add(tid);
if (tid === nodeId) connected.add(sid);
});
nodeEl.select('circle').attr('opacity', d => connected.has(d.id) ? 1 : 0.04);
linkEl.attr('opacity', e => {
const sid = typeof e.source === 'object' ? e.source.id : e.source;
const tid = typeof e.target === 'object' ? e.target.id : e.target;
return connected.has(sid) && connected.has(tid) ? 0.7 : 0.02;
});
}
Ego mode is the feature that makes the graph useful for actual work. “What does this function connect to?” is the question you ask twenty times a session. With ego mode, it is one click.
Separation of Concerns in Practice
The three layers — adapter, server, UI — are genuinely separate. The adapter knows nothing about HTTP. The server knows nothing about D3. The UI knows nothing about SQLite.
This isn’t just architecture for its own sake. It means each layer can change independently:
- Add a new data source (a TypeScript compiler API adapter, a Rust symbol indexer) without touching the server or UI
- Change the API shape (add pagination, add filtering) without touching the adapters or UI
- Rewrite the UI (switch from D3 to vis.js, or add a different tab) without touching adapters or server
For an open-source tool, this matters more than usual. Contributors arrive with different strengths. The person who wants to add a new adapter should not need to understand D3. The person who wants to improve the graph layout should not need to understand SQLite.
The interface is the contract between layers. Keep it small and stable, and the layers can evolve separately.
What “Byproduct” Actually Means
The word byproduct implies something incidental. That is slightly misleading. The tool was intentionally built. But it was built as a side effect of the session’s primary work — making DarJS’s code intelligence visible — rather than as a planned standalone project.
The difference matters for how it was scoped. There was no PRD. No user research. No feature list. The scope came entirely from the question “what is the minimum that makes this data browsable and navigable?”
Node graph with ego mode, search, and a detail panel. Impact analysis tab. Health tab. One command to start. Everything else was cut.
Tools built this way tend to stay lean. There is no feature backlog from a planning phase, no stakeholder requirements to satisfy. There is the data, the question, and the simplest interface that answers it.
The adapter pattern made it open-sourceable. The auto-detection made it zero-configuration. The no-build-step made it instantly forkable. None of these were marketing decisions. They were the natural result of building a tool you want to work with yourself, with no layers of indirection between the intent and the implementation.
That is what “byproduct” means in practice: a tool that exists because it was the most direct path to understanding something, and turns out to be useful outside that original context.
codeview is at autonomous/codeview/. Run it with dar codeview from any DarJS project, or codeview serve /path/to/project against any project that has a .codegraph/ directory.