Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,13 @@ jobs:
run: sudo apt-get update && sudo apt-get install -y graphviz

# The index codeshot reads. Pinned (repo convention: pin dependency versions).
# Floor is 1.5.0, not just latest-at-time-of-writing: 1.4.1 has a real call-
# resolution bug (fixed by codegraph's LITERAL_RECEIVER_TYPES change) that
# silently fabricates cross-file edges from unrelated builtin calls whose
# name happens to collide with a real project symbol — confirmed on this
# exact repo (see README.md#install and TECHNICAL.md's Known Limitations).
- name: Install codegraph
run: npm install -g @colbymchenry/codegraph@1.4.1
run: npm install -g @colbymchenry/codegraph@1.5.0

# `init` builds the initial index in a fresh checkout (`index` rebuilds an
# existing one and errors if the repo was never initialized — which a CI
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ npm install -g github:inth3shadows/codeshot

**Requirements:**
- Node.js ≥ 18
- [`codegraph`](https://github.com/colbymchenry/codegraph) CLI on PATH, with the target repo indexed (`codegraph init`)
- [`codegraph`](https://github.com/colbymchenry/codegraph) CLI on PATH, **1.5.0 or later**, with the target repo indexed (`codegraph init`). `--architecture` mode needs 1.5.0+ specifically: earlier versions have a call-resolution bug (fixed by codegraph's `LITERAL_RECEIVER_TYPES` fix) that can silently fabricate cross-file edges from unrelated builtin method calls (e.g. `/regex/.test(x)`) whose name happens to collide with a real project symbol.
- `graphviz` (`dot`) on PATH — `brew install graphviz` / `apt install graphviz`

Codeshot checks for both on startup and tells you exactly what's missing and how to install it.
Expand Down
7 changes: 4 additions & 3 deletions TECHNICAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ enumerateSymbols(repoPath, maxSymbols) --- runCodegraph(['query', '--path', repo
--- an empty-string query WITHOUT --limit silently caps around 50 results
regardless of repo size; passing a large --limit instead makes codegraph
return everything it has (see External Dependencies) --- the real cap is
applied client-side via filterCallableSymbols + a slice to maxSymbols
applied client-side via unwrapQueryNodes + a slice to maxSymbols
|
v
symbolBudgetWarning(...) --- warns on stderr if enumeration was capped by --max-symbols
Expand Down Expand Up @@ -120,7 +120,7 @@ It doubles as a live legend for the [Visual Encoding](#visual-encoding) rules be

- **`render/callgraph.js`** — the entire tool. Exports `buildDot(symbol, callers, callees, { maxRender, transitiveEdges })` (pure: turns caller/callee arrays into a DOT digraph string, deduplicating entries with the same `name`+`filePath` so repeated JSON rows don't render as duplicate edges; via `nodeIdentities` it gives each drawn node a graphviz id unique per `name`+`filePath`, so two *distinct* symbols that share a name but live in different files render as two separate boxes instead of silently collapsing into one — graphviz keys a node by the exact string in its edge, so without this the second same-named caller/callee vanishes from the picture; a name that occurs in only one file keeps name-as-id, leaving collision-free graphs byte-for-byte identical to before this existed, and only a colliding name gains a file-qualified id plus a `name\n(basename)` label to tell the boxes apart; when `tooltips` is set (`main` sets it for `svg`/`svgz` output only, since graphviz renders node tooltips as `<a xlink:title>` there and ignores them for raster formats), every drawn node is declared with its `filePath` as a hover tooltip so you can read which file a symbol lives in without cluttering the box — the root symbol is left un-tooltipped since `buildDot` isn't passed its file; if `maxRender` is given, `allocateRenderBudget` splits it as one shared allowance across callers, callees, and `transitiveEdges` — spent in that priority order, so the direct trail is never starved to make room for deeper hops; a `"kind":"file"` node — codegraph's way of saying "this is a module-level/import reference, not a verified function call" — is styled dotted/gray/`"file"` via `edgeStyleAttrs` instead of looking like a real call edge; `transitiveEdges`, an optional array of `{ from, to, depth }` pairs from `--depth > 1` traversal, is colored by `depthColor(depth)` unless it's file-kind — omitting `transitiveEdges`/`maxRender` renders exactly as before either feature existed), `allocateRenderBudget(maxRender, counts)` (pure: the shared-budget split described above, also called from `main` so its stderr notes report what actually got drawn), `dedupeNodes(nodes)` (pure: collapses same-`name`+`filePath` entries, used by both `buildDot` and `main` so distinct-count logic has one source of truth), `dedupeEdges(edges)` (pure: the same idea as `dedupeNodes` but keyed on a `from`+`to` pair, used only for `transitiveEdges`), `depthColor(depth)` (pure: maps hop distance ≥2 to a progressively lighter shade, clamped at the palette's last entry for very deep hops), `isTestRef(node)` (true if a node's name or filePath looks test-related, used to render those edges dashed — applies to `transitiveEdges` too, checked against each edge's `from` node, and is overridden by file-kind styling when both apply; also reused by `buildArchitectureDot` below, passed `{name: '', filePath}` since architecture-mode nodes have no symbol name — the name-based heuristics degrade harmlessly to `false` on an empty name, leaving the path-based ones intact), `truncationWarning(kind, results, limit)` (pure: returns a warning string if `results.length` hit `limit` exactly, else `null` — the *fetch* cap), `renderTruncationNote(kind, distinctCount, cap)` (pure: returns a warning string if the deduplicated count exceeds `cap`, else `null` — the *render* cap; `main` passes each dimension's actual `allocateRenderBudget` allotment as `cap`, not the raw `--max-render`; also reused as-is by `--architecture` mode with `kind: 'files'`), `depthBudgetWarning(truncated, budget)` (pure: returns a warning string if `--depth` traversal hit the internal node budget before finishing, else `null`), `formatMismatchWarning(outFile, format)` (pure: returns a warning string if `--out`'s extension is a real `dot`-recognized format that disagrees with `--format`, else `null`), and `matchSymbolNotFound(out)` (pure: extracts the symbol name from codegraph's plain-text "Symbol not found" message, or `null` if `out` doesn't match that shape). Everything else (`requireOnPath`, `runCodegraph`, `parseCodegraphOutput`, `resolveSymbol`, `collectTransitive`, `main`) is CLI plumbing, not exported — `resolveSymbol` and `collectTransitive` in particular do real I/O (`codegraph` calls), so like `runCodegraph` they're only exercised by the CLI-level tests, not unit-tested directly.

**`--architecture` mode adds:** `filterCallableSymbols(queryResults)` (pure: unwraps `query`'s `{node, score}` result shape and drops `kind === 'file'` entries — a file object isn't a callable symbol, so it's never probed; deliberately does NOT allowlist "callable" kinds like `function`/`method` — probing a `constant` or `variable` just harmlessly returns an empty `callees` array, which is more robust across languages than maintaining a per-language kind list), `symbolBudgetWarning(truncated, budget)` (pure: same shape as `depthBudgetWarning`, fires when `--max-symbols` cut enumeration short), `duplicateNameWarning(symbols)` (pure: warns — with a few real examples — when any probed symbol name appears in more than one file, since `codegraph callees <name>` has no way to disambiguate which file's symbol it means; see Known Limitations), `aggregateFileEdges(symbolEdges)` (pure: dedupes/sums `{fromFile, toFile}` pairs from every probed symbol into weighted `{from, to, weight}` file edges, dropping self-file edges and any edge missing a real `filePath` on either end — an unresolved external/stdlib callee has no file of its own and would otherwise render as a bogus `""` node), `topFilesByWeight(fileEdges, maxRender)` (pure: ranks files by total in+out edge weight and returns the top `maxRender` as a `Set`, or `null` meaning "no cap" — deliberately a simple weight cutoff, not a connected-component/centrality algorithm), `buildArchitectureDot(fileEdges, { maxRender })` (pure: the architecture-mode analog of `buildDot` — a dedicated function rather than a `buildDot` branch, since the semantics genuinely differ: no root-symbol highlight, no caller/callee direction split, no file-kind dotted-edge concept since every node already IS a file), and `architectureOutputBaseName(repoPath)` (pure: `sanitizeForFilename(path.basename(path.resolve(repoPath)))`, used for the default `--out` filename). Unexported CLI plumbing: `enumerateSymbols`, `probeFileEdges`, `runArchitectureMode` (real I/O, only exercised via the CLI-level test), and `renderDotToFile` (shared with symbol mode — the write-tempfile/`dot -T<format>`/delete-tempfile tail, previously inline in `main`, extracted once a second call site needed it).
**`--architecture` mode adds:** `unwrapQueryNodes(queryResults)` (pure: unwraps `query`'s `{node, score}` result shape, dropping only a missing `node` — a `"kind":"file"` entry is deliberately KEPT rather than dropped: `codegraph callees <fileBasename>` is a real, working probe against it, and it's the only way to surface calls made from inside a top-level anonymous callback, which codegraph attributes to the enclosing file rather than any named function — see the note on `probeFileEdges` and Known Limitations. Also deliberately does NOT allowlist "callable" kinds like `function`/`method` for the non-file entries — probing a `constant` or `variable` just harmlessly returns an empty `callees` array, which is more robust across languages than maintaining a per-language kind list), `symbolBudgetWarning(truncated, budget)` (pure: same shape as `depthBudgetWarning`, fires when `--max-symbols` cut enumeration short), `duplicateNameWarning(symbols)` (pure: warns — with a few real examples — when any probed symbol name appears in more than one file, since `codegraph callees <name>` has no way to disambiguate which file's symbol it means; see Known Limitations), `aggregateFileEdges(symbolEdges)` (pure: dedupes/sums `{fromFile, toFile}` pairs from every probed symbol into weighted `{from, to, weight}` file edges, dropping self-file edges and any edge missing a real `filePath` on either end — an unresolved external/stdlib callee has no file of its own and would otherwise render as a bogus `""` node), `topFilesByWeight(fileEdges, maxRender)` (pure: ranks files by total in+out edge weight and returns the top `maxRender` as a `Set`, or `null` meaning "no cap" — deliberately a simple weight cutoff, not a connected-component/centrality algorithm), `buildArchitectureDot(fileEdges, { maxRender })` (pure: the architecture-mode analog of `buildDot` — a dedicated function rather than a `buildDot` branch, since the semantics genuinely differ: no root-symbol highlight, no caller/callee direction split, no file-kind dotted-edge concept since every node already IS a file), and `architectureOutputBaseName(repoPath)` (pure: `sanitizeForFilename(path.basename(path.resolve(repoPath)))`, used for the default `--out` filename). Unexported CLI plumbing: `enumerateSymbols`, `probeFileEdges`, `runArchitectureMode` (real I/O, only exercised via the CLI-level test), and `renderDotToFile` (shared with symbol mode — the write-tempfile/`dot -T<format>`/delete-tempfile tail, previously inline in `main`, extracted once a second call site needed it).
- **`test/run.js`** — assertion-based test suite (Node's built-in `assert`, no framework) covering all of the pure functions above directly. Run via `npm test`.
- **`package.json`** — declares the `codeshot` bin pointing at `render/callgraph.js`, and the `test` script.
- **`.runechoguardignore`** — false-positive suppression list for the RunEcho pre-commit symbol-resolution guard (a local hook, not part of codeshot itself). Bare-call identifiers the guard can't resolve (e.g. Node builtins passed as function parameters) get listed here instead of disabling the guard.
Expand Down Expand Up @@ -172,7 +172,8 @@ There is no service to restart, no rollback beyond `npm uninstall -g codeshot` /
- `--depth`'s `NODE_BUDGET` (200) is a fixed internal constant, not exposed as a flag — a genuinely well-connected symbol at `--depth 3`+ in a large repo can still hit it and produce an incomplete graph (with a stderr warning), and there's currently no way to raise the cap short of editing the constant.
- `--depth`'s traversal treats `--limit`/`--max-render` as global, not per-hop — a symbol with a huge fan-out at hop 2 fetches up to `--limit` results for *each* newly discovered node at that hop, which is the main driver of `NODE_BUDGET` exhaustion; there's no independent per-hop limit to trade off against total node count.
- A cyclic call graph (recursion, or A and B calling each other) can cause `--depth`'s transitive traversal to rediscover the root symbol or an already-drawn depth-1 node as a "from"/"to" endpoint of a deeper edge. This is harmless (graphviz just draws the extra edge; `dedupeEdges` still collapses exact repeats) but can occasionally show what looks like a redundant edge back into an already-visible node.
- **`--architecture` mode's edges can be misattributed to the wrong file when symbol names collide.** `codegraph callees <name>` takes a bare name with no way to disambiguate which file's symbol is meant (unlike `codegraph node -f <file>`, which does support this). In symbol mode this ambiguity affects exactly one user-chosen name — a corner case. In `--architecture` mode, Codeshot probes `codegraph callees` for every enumerated symbol in the whole repo, where generically-named methods (`render`, `init`, `get`, `run`, `String`) existing in more than one file is common, not rare, in most real codebases (confirmed: 12 duplicate names out of 500 probed symbols on a real ~1,900-node Go repo). `duplicateNameWarning` surfaces this on stderr with real examples from the current run, but Codeshot has no way to fix the underlying ambiguity — same as the other `codegraph` indexing gaps documented above, it can only draw what `codegraph` returns.
- **`--architecture` mode's edges can be misattributed to the wrong file when symbol names collide.** `codegraph callees <name>` takes a bare name with no way to disambiguate which file's symbol is meant (unlike `codegraph node -f <file>`, which does support this). In symbol mode this ambiguity affects exactly one user-chosen name — a corner case. In `--architecture` mode, Codeshot probes `codegraph callees` for every enumerated symbol in the whole repo, where generically-named methods (`render`, `init`, `get`, `run`, `String`) existing in more than one file is common, not rare, in most real codebases (confirmed: 12 duplicate names out of 500 probed symbols on a real ~1,900-node Go repo). Since `unwrapQueryNodes` also keeps file nodes in the probed set (see below), the same ambiguity now applies to file basenames too — two files named `index.js` in different directories are indistinguishable to a bare-name `callees` probe. `duplicateNameWarning` surfaces both cases on stderr with real examples from the current run, but Codeshot has no way to fix the underlying ambiguity — same as the other `codegraph` indexing gaps documented above, it can only draw what `codegraph` returns.
- **`--architecture` mode probes file nodes' `callees`, not just named symbols', specifically to catch calls made from inside a top-level anonymous callback** (e.g. `test('...', () => { realCall() })` — a common pattern in test suites, including this repo's own `test/run.js`). codegraph attributes such a call to the enclosing file, not any named function, since no named function contains it; without probing the file node itself, `--architecture` mode would be structurally blind to that entire category of real cross-file dependency. This was confirmed against a real regression-turned-non-regression in codegraph itself: a codegraph 1.4.1 bug (fixed in 1.5.0, see `git log` for `LITERAL_RECEIVER_TYPES` in codegraph's history) briefly caused a *different*, spurious file-node-unrelated edge to appear in this project's own diagram — a call like `/regex/.test(x)` in `render/callgraph.js` got bare-name-matched to `test/run.js`'s own `test(name, fn)` helper purely by name collision. That edge is gone as of codegraph 1.5.0+ (correctly — it was never real); probing file nodes is what makes the *actual* dependency (`test/run.js` calling into `render/callgraph.js`) visible in its place. `probeFileEdges` skips any `"kind":"file"` *callee* it gets back from a probe — the same unverified-reference status that makes symbol mode draw it dotted/gray rather than as a real call (see Visual Encoding) means it must not be counted as a real cross-file edge here either, or this exact fabricated-edge problem reappears via a different mechanism. Verified empirically against this repo's own index (0 `"kind":"file"` callees among 100 probed) — but a repo where a file's *only* top-level reference is an unresolved `require(...)` with no other calls would exercise this path, and there's no dedicated test for it (real I/O against a live index would be needed to construct one). Two costs of probing file nodes, both real but not separately mitigated: enumeration and `--max-symbols` now compete real symbols against file nodes for the same fixed slot budget in an order codegraph doesn't guarantee (see the `--limit` note below) — on a repo near the cap, file nodes could crowd out real-symbol coverage with no warning distinguishing the two; and the probe count (and thus the already-"multi-minute" wall-clock cost) grows by roughly the repo's file count, since `probeFileEdges` is strictly sequential.
- **`--architecture` mode's enumeration query (`codegraph query --json --limit <big> -- ''`) has confirmed, inconsistent `--limit` behavior worth knowing before trusting it.** Without `--limit`, an empty-string query silently caps around 50 results regardless of actual repo size (confirmed on a real 1,870-node index). Passing a large `--limit` (confirmed with both 500 and 2000 against that same index) instead returns *every* result codegraph has — more than the requested number, not capped at it. Codeshot works around this by always passing a very large `--limit` to force the "return everything" behavior, then applying the real `--max-symbols` cap client-side — but the *order* codegraph returns results in in that case is unknown (untested whether it's insertion order, alphabetical, ID-based, or something else), so on a repo larger than `--max-symbols`, the kept subset should not be assumed to sample evenly across the whole repo — it could be clustered by file, directory, or however codegraph happens to have stored them.

<!-- codeshot:arch:start -->
Expand Down
Loading
Loading