From d15eee50dd3d6a2df357aeed17a3b048fc206d26 Mon Sep 17 00:00:00 2001 From: Eric Minish Date: Wed, 5 Aug 2026 09:48:10 -0400 Subject: [PATCH 1/3] Expose --depth's node-discovery cap as --max-depth-nodes The 200-node safety cap on --depth traversal was a fixed constant the code explicitly flagged as "not exposed as a flag (yet)". Mirrors the existing --max-symbols pattern (same validation, same rejection when combined with --architecture) so a well-connected symbol that legitimately needs a higher cap to finish at --depth 3+ can get one. Default behavior is unchanged. --- README.md | 3 ++- TECHNICAL.md | 10 ++++----- USAGE.md | 2 +- render/callgraph.js | 37 +++++++++++++++++++++--------- test/run.js | 55 +++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 90 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 8bc298f..3069e71 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,8 @@ codeshot [--path ] [--out ] [--limit ] [--max-re - `--format` — output format, passed straight to `dot -T` (defaults to `png`). `svg` is a good alternative for large graphs — it stays crisp at any zoom level and keeps text selectable, unlike a raster PNG. In `svg` (and `svgz`) output, each node also carries the file its symbol lives in as a hover tooltip, so you can tell same-named symbols apart without cluttering the boxes (open the file in a browser to see them; GitHub's SVG sanitizer may strip tooltips when the image is embedded in a README). Any format `dot -T` supports works; an unsupported one fails with `dot`'s own error listing the valid ones. - `--embed ` — instead of just writing an image, insert (or, on re-runs, refresh in place) the diagram inside an existing markdown doc, using idempotent `` markers (the `doctoc`/`terraform-docs` pattern). The image is written to a stable path next to the doc so the relative link resolves and both can be committed. Refreshes an existing doc — it won't create one. Works in both symbol and `--architecture` mode. See [USAGE.md](USAGE.md#embedding-a-diagram-in-your-docs-and-keeping-it-fresh). - `--check` — (only with `--embed`) verify the committed diagram and its doc block are current without changing anything: exit `0` if up to date, exit `1` if the code has drifted from the committed image. Built for a CI job / pre-commit hook so a stale diagram fails the build. Compares rendered bytes, so it needs the same `graphviz` version that generated the committed image. -- `--depth` — how many hops of callers-of-callers / callees-of-callees to draw beyond the direct trail (defaults to `1`, i.e. today's direct-only behavior; must be a positive integer). Codeshot fetches this itself, one sequential `codegraph` call per newly discovered node — CodeGraph has no multi-hop traversal of its own for `callers`/`callees`. Each additional hop is drawn in a progressively lighter shade so you can tell how far a node is from the symbol at a glance. There's an internal, non-configurable safety cap on total nodes discovered (a well-connected symbol at `--depth 3`+ can otherwise mean hundreds of sequential `codegraph` calls); Codeshot warns on stderr if it hit that cap before finishing — see [TECHNICAL.md](TECHNICAL.md#configuration) for the exact number and rationale. +- `--depth` — how many hops of callers-of-callers / callees-of-callees to draw beyond the direct trail (defaults to `1`, i.e. today's direct-only behavior; must be a positive integer). Codeshot fetches this itself, one sequential `codegraph` call per newly discovered node — CodeGraph has no multi-hop traversal of its own for `callers`/`callees`. Each additional hop is drawn in a progressively lighter shade so you can tell how far a node is from the symbol at a glance. There's a safety cap on total nodes discovered (a well-connected symbol at `--depth 3`+ can otherwise mean hundreds of sequential `codegraph` calls); Codeshot warns on stderr if it hit that cap before finishing — see `--max-depth-nodes` below to raise it, or [TECHNICAL.md](TECHNICAL.md#configuration) for the default and rationale. +- `--max-depth-nodes` — raises (or lowers) `--depth`'s safety cap on total discovered nodes (defaults to `200`; must be a positive integer). Only applies with `--depth > 1`; has no effect with `--architecture` and is rejected if passed alongside it. Useful for a genuinely well-connected symbol whose graph is real but incomplete at the default cap — see the depth-budget warning it's meant to answer. ## Whole-repo architecture diagram diff --git a/TECHNICAL.md b/TECHNICAL.md index 6c41616..48f10b2 100644 --- a/TECHNICAL.md +++ b/TECHNICAL.md @@ -5,7 +5,7 @@ Codeshot is a single-file CLI (`render/callgraph.js`) with no server, no persistent state, and no dependencies beyond two external binaries it shells out to. ``` -argv (symbol, --path, --out, --limit, --max-render, --format, --depth) +argv (symbol, --path, --out, --limit, --max-render, --format, --depth, --max-depth-nodes) | v requireOnPath('codegraph') --- exits with an install hint if missing @@ -144,9 +144,10 @@ No environment variables, no config file. All behavior is controlled by CLI argu | `--limit` | `50` | Max callers/callees fetched from `codegraph` (its own CLI default is 20). Must be a positive integer — rejected with an error otherwise, since `codegraph` silently returns an empty result for a malformed limit rather than erroring itself. `codegraph`'s JSON has no total/truncated field, so Codeshot's only signal that more may exist is the result count hitting `--limit` exactly — when it does, a warning is printed to stderr. That heuristic false-positives for a symbol with exactly `--limit` real results and no more; there's no way to distinguish "exactly complete" from "truncated" without a total field from `codegraph`. | | `--max-render` | unset (no cap) | Caps how many *distinct* (post-dedup) nodes are actually drawn, independent of `--limit`. This is **one shared budget across callers, callees, and `--depth`'s transitive edges combined** — not an independent `N` for each dimension (that was the original design and is arguably still the more obvious reading of "cap how many callers/callees are drawn", but it meant `--max-render 20` could still draw up to 60 nodes once `--depth` added a third dimension, defeating the flag's actual purpose of keeping the image readable; corrected 2026-07-03). Spent in priority order — direct callers first, then direct callees, then transitive edges — so the direct trail is never starved to make room for deeper hops. Exists because `--limit` controls what's fetched, not what's legible — a symbol with hundreds of real callers is still complete but produces an unusably tall image at a high `--limit`. Opt-in and orthogonal to `--limit`: fetch wide (to get an accurate truncation signal) while rendering narrow. Must be a positive integer if given. When it truncates, a stderr warning per dimension states how many of that dimension's distinct total were actually rendered (its real allotment, not the raw `--max-render` value). | | `--format` | `png` | Passed straight to `dot -T` — no allowlist of its own, so any format `dot -T` supports works, and an unsupported one surfaces `dot`'s own error (which lists the valid ones) rather than a Codeshot-invented one. `svg` is the notable alternative: unlike a raster PNG it stays crisp at any zoom and keeps text selectable, which helps more than `--max-render` alone when a graph is dense but you still want to inspect all of it. | -| `--depth` | `1` (today's direct-only behavior) | How many hops of callers-of-callers / callees-of-callees to draw beyond the direct trail. Must be a positive integer. `codegraph`'s own `callers`/`callees` have no traversal depth of their own, so Codeshot implements this client-side: `collectTransitive` recursively calls `callers`/`callees` on each newly discovered node, sequentially (same concurrency-safety reason as the depth-1 calls — see External Dependencies), up to `--depth` hops or `NODE_BUDGET` (200, a fixed constant, not itself configurable) total discovered nodes, whichever comes first. Fan-out is multiplicative with depth and branching factor, so the budget exists specifically to stop a well-connected symbol at `--depth 3`+ from turning into hundreds of sequential `codegraph` calls; if the budget is hit first, `depthBudgetWarning` prints a stderr note that the graph beyond that point is incomplete. Each hop beyond the first is drawn in a progressively lighter edge color (`depthColor`) so distance from the symbol is visible at a glance; `--limit` and `--max-render` are NOT applied per-hop, only globally to the depth-1 fetch/render as before. Has no meaning with `--architecture` (no multi-hop file traversal concept) and is rejected if explicitly passed alongside it. | +| `--depth` | `1` (today's direct-only behavior) | How many hops of callers-of-callers / callees-of-callees to draw beyond the direct trail. Must be a positive integer. `codegraph`'s own `callers`/`callees` have no traversal depth of their own, so Codeshot implements this client-side: `collectTransitive` recursively calls `callers`/`callees` on each newly discovered node, sequentially (same concurrency-safety reason as the depth-1 calls — see External Dependencies), up to `--depth` hops or `--max-depth-nodes` (below) total discovered nodes, whichever comes first. Fan-out is multiplicative with depth and branching factor, so the budget exists specifically to stop a well-connected symbol at `--depth 3`+ from turning into hundreds of sequential `codegraph` calls; if the budget is hit first, `depthBudgetWarning` prints a stderr note that the graph beyond that point is incomplete. Each hop beyond the first is drawn in a progressively lighter edge color (`depthColor`) so distance from the symbol is visible at a glance; `--limit` and `--max-render` are NOT applied per-hop, only globally to the depth-1 fetch/render as before. Has no meaning with `--architecture` (no multi-hop file traversal concept) and is rejected if explicitly passed alongside it. | +| `--max-depth-nodes` | `200` (`DEFAULT_NODE_BUDGET`) | Only meaningful with `--depth > 1`: raises or lowers `collectTransitive`'s total-discovered-node safety cap (formerly the fixed, unconfigurable `NODE_BUDGET` constant). Must be a positive integer. Same rationale as `--max-symbols` below — a genuinely well-connected symbol at `--depth 3`+ can legitimately need a higher cap to finish, and that's a coverage/speed tradeoff only the caller can make. Rejected if explicitly passed alongside `--architecture` (no multi-hop file traversal to bound). | | `--architecture` | `false` | Switches to whole-repo file-level dependency graph mode instead of a single symbol's trail — see the second pipeline diagram above. Mutually exclusive with the `` positional (rejected if both given); `` becomes optional-and-forbidden rather than required. | -| `--max-symbols` | `500` | `--architecture`-only: caps how many enumerated symbols get probed. Unlike `--depth`'s `NODE_BUDGET`, this **is** exposed as a flag rather than a fixed internal constant — deliberately, since `--architecture` is a multi-minute O(symbols) sequential scan by nature (confirmed: 500 symbols took over 5 minutes against a real ~1,900-node repo), so users legitimately need to trade coverage for speed themselves rather than wait on a hidden safety net tuned for an already-fast operation. Must be a positive integer. `symbolBudgetWarning` prints a stderr note when this cuts enumeration short. | +| `--max-symbols` | `500` | `--architecture`-only: caps how many enumerated symbols get probed, exposed as a flag for the same reason `--max-depth-nodes` above is — `--architecture` is a multi-minute O(symbols) sequential scan by nature (confirmed: 500 symbols took over 5 minutes against a real ~1,900-node repo), so users legitimately need to trade coverage for speed themselves rather than wait on a hidden safety net tuned for an already-fast operation. Must be a positive integer. `symbolBudgetWarning` prints a stderr note when this cuts enumeration short. | ## Visual Encoding @@ -169,8 +170,7 @@ There is no service to restart, no rollback beyond `npm uninstall -g codeshot` / - `test/run.js` includes one CLI-level test that runs the real `codeshot` binary against this repo's own `codegraph` index (querying a symbol from `render/callgraph.js` itself) to catch `codegraph` output-shape drift — but it skips itself (rather than failing) when `codegraph` isn't on `PATH` or this repo hasn't been `codegraph init`'d, since that's a dev-environment convenience a fresh clone or CI won't have. So the contract is only actually exercised on machines set up for it; elsewhere it's still only indirectly covered via `buildDot`'s output against the real `dot` binary. - `isTestRef` is a naming-convention heuristic (word-boundary `Test`/`Spec` prefix or suffix in the name, or a `test`/`tests`/`spec`/`__tests__` directory or `.test.`/`.spec.` filename), not a semantic check — a production symbol that happens to follow test-like naming (e.g. a function literally named `Test`) would still be misclassified. - **Real, verified codegraph indexing gaps that codeshot has no way to detect or correct, and silently renders as if they were the whole truth** (found by testing codeshot against `runecho`, `codegraph-upstream`, `honeyslate`, and `secret-broker` — real external repos, not this one): same-named methods on unrelated classes/types are sometimes merged into one node, sometimes one is silently dropped, inconsistently between cases; aliased imports (`from x import load as load_config`) can return zero callers for a function with several real call sites; and confirmed-real call sites (verified by reading the actual source) are sometimes simply missing from `codegraph callers`'s response with no indication anything was omitted. None of these are fixable from codeshot's side — it only draws what `codegraph` returns — but they're worth knowing before trusting a sparse-looking graph as complete. -- `--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. +- `--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 `--max-depth-nodes` exhaustion; there's no independent per-hop limit to trade off against total node count (though the total budget itself is now configurable — see `--max-depth-nodes` above). - 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 ` takes a bare name with no way to disambiguate which file's symbol is meant (unlike `codegraph node -f `, 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 enumeration query (`codegraph query --json --limit -- ''`) 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. diff --git a/USAGE.md b/USAGE.md index 357c3a5..512aed2 100644 --- a/USAGE.md +++ b/USAGE.md @@ -108,7 +108,7 @@ fall back to a raw byte-compare, which does require CI to use the same - **`--out diagram.svg` produced a PNG (or vice versa)** — Codeshot only ever writes what `--format` says; it never infers format from `--out`'s extension. If you see this, you forgot `--format svg` (or whichever format matches the extension you wanted) — Codeshot now warns about this mismatch on stderr before it happens, so check for that warning first. - **The image looks unreadable / too cluttered** — This usually means the symbol has a very large number of callers or callees. Rerun with `--max-render ` (e.g. `--max-render 30`) to cap how many are drawn — Codeshot will still tell you on stderr how many were left out. If the nodes themselves are legible but hard to read at the zoom level a PNG forces on you, try `--format svg` instead — it stays crisp at any zoom, so it's worth trying before reaching for `--max-render` if you still want to see everything. - **"codeshot: showing N callers/callees — ... may have cut off more"** — Rerun with a higher `--limit` if you need the full picture (see `TECHNICAL.md` for why this warning can occasionally be a false alarm). -- **"codeshot: --depth traversal stopped early (internal safety cap of 200 discovered nodes)"** — The symbol is heavily connected enough that `--depth` hit an internal limit before finishing; the graph you got is real but incomplete beyond that point. Try a smaller `--depth` (2 instead of 3), a lower `--limit`, or a more specific, less-central symbol. +- **"codeshot: --depth traversal stopped early (internal safety cap of 200 discovered nodes)"** — The symbol is heavily connected enough that `--depth` hit its node-discovery cap before finishing; the graph you got is real but incomplete beyond that point. Try a smaller `--depth` (2 instead of 3), a lower `--limit`, a more specific, less-central symbol, or raise the cap itself with `codeshot --depth 3 --max-depth-nodes 500` (default is 200). - **`--depth` runs slowly** — Each additional hop makes one sequential `codegraph` call per newly discovered node (CodeGraph itself has no multi-hop traversal for `callers`/`callees`, so Codeshot does this client-side), so a well-connected symbol at `--depth 2` or higher can take noticeably longer than the default `--depth 1`. This is expected, not a bug. - **`--architecture` is taking a long time** — Expected on anything past a small repo: it's one sequential `codegraph` call per enumerated symbol, and there's no way to parallelize it (concurrent `codegraph` calls against one index race and fail). Rerun with a smaller `--max-symbols` (e.g. `--max-symbols 100`) for a faster, partial scan — Codeshot warns on stderr when the scan is cut short by the cap so you know the result is incomplete. - **`--architecture`'s diagram is a hairball / unreadable** — Same fix as symbol mode: `--max-render ` (e.g. `--max-render 20`) keeps only the busiest N files by total call-edge weight and drops the rest. Some remaining files can end up with no surviving edges if all their edges pointed at a dropped file — that's expected, not a bug, at aggressive `--max-render` values. diff --git a/render/callgraph.js b/render/callgraph.js index 8bff508..ce7b5ea 100755 --- a/render/callgraph.js +++ b/render/callgraph.js @@ -39,15 +39,15 @@ const STRUCTURAL_CHECK_FORMATS = new Set(['svg']); // newly discovered node, per hop — on a well-connected symbol that fans out // fast. This caps total discovered nodes across both directions combined so // one request can't turn into hundreds of sequential codegraph invocations. -// Not exposed as a flag (yet): a fixed safety cap, not a tuning knob. -const NODE_BUDGET = 200; +// Exposed as --max-depth-nodes (default below) for a symbol whose real graph +// genuinely needs a higher cap to finish at --depth 3+. +const DEFAULT_NODE_BUDGET = 200; // --architecture probes every enumerated symbol sequentially (one codegraph // call each) to build the file-level graph — a multi-minute operation on a -// mid-size repo, unlike NODE_BUDGET's quiet safety net on an already-fast -// --depth traversal. Users legitimately need to trade coverage for speed -// themselves, so this — deliberately, unlike NODE_BUDGET — IS exposed as a -// flag (--max-symbols). +// mid-size repo, the same reasoning --max-depth-nodes above now shares: users +// legitimately need to trade coverage for speed themselves, so both are +// exposed as flags (--max-symbols here, --max-depth-nodes for --depth). const DEFAULT_MAX_SYMBOLS = 500; function requireOnPath(bin, installHint) { @@ -793,7 +793,7 @@ function finishOutput(dot, { format, outFile, embedFile, check, markerId, alt }) console.log(outFile); } -const USAGE = 'Usage: callgraph.js [--path ] [--out ] [--limit ] [--max-render ] [--format ] [--depth ] [--embed [--check]]\n or: callgraph.js --architecture [--path ] [--out ] [--limit ] [--max-render ] [--max-symbols ] [--format ] [--embed [--check]]'; +const USAGE = 'Usage: callgraph.js [--path ] [--out ] [--limit ] [--max-render ] [--format ] [--depth ] [--max-depth-nodes ] [--embed [--check]]\n or: callgraph.js --architecture [--path ] [--out ] [--limit ] [--max-render ] [--max-symbols ] [--format ] [--embed [--check]]'; async function main() { let values, positionals; @@ -808,6 +808,7 @@ async function main() { 'max-render': { type: 'string' }, format: { type: 'string', default: 'png' }, depth: { type: 'string', default: '1' }, + 'max-depth-nodes': { type: 'string' }, architecture: { type: 'boolean', default: false }, 'max-symbols': { type: 'string', default: String(DEFAULT_MAX_SYMBOLS) }, embed: { type: 'string' }, @@ -828,6 +829,10 @@ async function main() { const flagIndex = process.argv.indexOf('--depth'); const badValue = flagIndex !== -1 ? process.argv[flagIndex + 1] : undefined; console.error(`codeshot: --depth must be a positive integer, got '${badValue}'`); + } else if (err.code === 'ERR_PARSE_ARGS_INVALID_OPTION_VALUE' && /--max-depth-nodes/.test(err.message)) { + const flagIndex = process.argv.indexOf('--max-depth-nodes'); + const badValue = flagIndex !== -1 ? process.argv[flagIndex + 1] : undefined; + console.error(`codeshot: --max-depth-nodes must be a positive integer, got '${badValue}'`); } else if (err.code === 'ERR_PARSE_ARGS_INVALID_OPTION_VALUE' && /--max-symbols/.test(err.message)) { const flagIndex = process.argv.indexOf('--max-symbols'); const badValue = flagIndex !== -1 ? process.argv[flagIndex + 1] : undefined; @@ -896,6 +901,18 @@ async function main() { console.error('codeshot: --depth has no effect with --architecture (there is no multi-hop file traversal)'); process.exit(1); } + let maxDepthNodes = DEFAULT_NODE_BUDGET; + if (values['max-depth-nodes'] !== undefined) { + maxDepthNodes = Number(values['max-depth-nodes']); + if (!Number.isInteger(maxDepthNodes) || maxDepthNodes <= 0) { + console.error(`codeshot: --max-depth-nodes must be a positive integer, got '${values['max-depth-nodes']}'`); + process.exit(1); + } + if (values.architecture) { + console.error('codeshot: --max-depth-nodes has no effect with --architecture (there is no multi-hop file traversal)'); + process.exit(1); + } + } const maxSymbols = Number(values['max-symbols']); if (!Number.isInteger(maxSymbols) || maxSymbols <= 0) { console.error(`codeshot: --max-symbols must be a positive integer, got '${values['max-symbols']}'`); @@ -975,10 +992,10 @@ async function main() { let transitiveEdges = []; if (depth > 1) { const discovered = new Set(dedupeNodes([...(callers || []), ...(callees || [])]).map(n => `${n.name} ${n.filePath}`)); - const callerResult = await collectTransitive('callers', repoPath, limit, depth, dedupeNodes(callers || []), discovered, NODE_BUDGET); - const calleeResult = await collectTransitive('callees', repoPath, limit, depth, dedupeNodes(callees || []), discovered, NODE_BUDGET); + const callerResult = await collectTransitive('callers', repoPath, limit, depth, dedupeNodes(callers || []), discovered, maxDepthNodes); + const calleeResult = await collectTransitive('callees', repoPath, limit, depth, dedupeNodes(callees || []), discovered, maxDepthNodes); transitiveEdges = [...callerResult.edges, ...calleeResult.edges]; - const budgetWarning = depthBudgetWarning(callerResult.truncated || calleeResult.truncated, NODE_BUDGET); + const budgetWarning = depthBudgetWarning(callerResult.truncated || calleeResult.truncated, maxDepthNodes); if (budgetWarning) console.error(budgetWarning); } diff --git a/test/run.js b/test/run.js index 58c82ba..190c935 100644 --- a/test/run.js +++ b/test/run.js @@ -369,6 +369,32 @@ test('--depth rejects non-positive-integer values before reaching codegraph', () } }); +test('--max-depth-nodes rejects non-positive-integer values before reaching codegraph', () => { + const { execFileSync } = require('child_process'); + for (const bad of ['abc', '0', '-5', '3.5', 'NaN']) { + let threw = false; + try { + execFileSync('node', [require('path').join(__dirname, '..', 'render', 'callgraph.js'), 'Foo', '--max-depth-nodes', bad], { encoding: 'utf8', stdio: 'pipe' }); + } catch (err) { + threw = true; + assert.match(err.stderr, /--max-depth-nodes must be a positive integer/); + } + assert.strictEqual(threw, true, `expected --max-depth-nodes ${bad} to be rejected`); + } +}); + +test('--architecture rejects an explicit --max-depth-nodes (no multi-hop file traversal to bound)', () => { + const { execFileSync } = require('child_process'); + let threw = false; + try { + execFileSync('node', [require('path').join(__dirname, '..', 'render', 'callgraph.js'), '--architecture', '--max-depth-nodes', '500'], { encoding: 'utf8', stdio: 'pipe' }); + } catch (err) { + threw = true; + assert.match(err.stderr, /--max-depth-nodes has no effect with --architecture/); + } + assert.strictEqual(threw, true, 'expected --architecture + --max-depth-nodes to be rejected'); +}); + test('missing symbol argument is rejected with a codeshot-prefixed message', () => { const { execFileSync } = require('child_process'); let threw = false; @@ -504,6 +530,35 @@ test('CLI --depth traversal runs end-to-end against this repo\'s own real codegr } }); +test('CLI --max-depth-nodes lowers the --depth traversal budget end-to-end against this repo\'s own real codegraph index', () => { + const { execFileSync } = require('child_process'); + const path = require('path'); + const fs = require('fs'); + const os = require('os'); + const repoRoot = path.join(__dirname, '..'); + const callgraphJs = path.join(repoRoot, 'render', 'callgraph.js'); + + try { + execFileSync('codegraph', ['callers', '--path', repoRoot, '--limit', '1', '--json', '--', 'buildDot'], { stdio: 'pipe' }); + } catch { + console.log(' # skipped: `codegraph` not on PATH or this repo is not codegraph-indexed'); + return; + } + + const out = path.join(os.tmpdir(), `codeshot-maxdepthnodes-${Date.now()}.dot`); + try { + // buildDot has at least one real caller/callee in this repo, so seeding the + // depth-2 traversal's discovered set already meets a budget of 1 — the + // traversal must report it hit the (lowered, not default 200) cap. + const { spawnSync } = require('child_process'); + const result = spawnSync('node', [callgraphJs, 'buildDot', '--path', repoRoot, '--out', out, '--format', 'dot', '--depth', '2', '--max-depth-nodes', '1'], { encoding: 'utf8' }); + assert.strictEqual(result.status, 0, `expected a successful render even when the depth budget is hit, got stderr: ${result.stderr}`); + assert.match(result.stderr, /internal safety cap of 1 discovered nodes/, 'expected the lowered --max-depth-nodes value to appear in the truncation warning'); + } finally { + fs.rmSync(out, { force: true }); + } +}); + test('CLI resolves a fuzzy/partial query to its canonical name for the rendered root label', () => { const { execFileSync } = require('child_process'); const path = require('path'); From e9567604ee6e181b3b7f493dadb3c91b93ae5f39 Mon Sep 17 00:00:00 2001 From: Eric Minish Date: Wed, 5 Aug 2026 10:02:57 -0400 Subject: [PATCH 2/3] Drop stale "internal" from the depth-budget-cap message The cap stopped being internal-only in the previous commit --max-depth-nodes made it externally configurable, but the stderr message, USAGE.md, and TECHNICAL.md still called it "internal", undercutting the flag that answers it. Points users at --max-depth-nodes instead. --- TECHNICAL.md | 2 +- USAGE.md | 4 ++-- render/callgraph.js | 2 +- test/run.js | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/TECHNICAL.md b/TECHNICAL.md index 48f10b2..50b6fe2 100644 --- a/TECHNICAL.md +++ b/TECHNICAL.md @@ -118,7 +118,7 @@ It doubles as a live legend for the [Visual Encoding](#visual-encoding) rules be ## File Descriptions -- **`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 `` 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. +- **`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 `` 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 its node-discovery cap (`--max-depth-nodes`) 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 ` 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`/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`. diff --git a/USAGE.md b/USAGE.md index 512aed2..156acea 100644 --- a/USAGE.md +++ b/USAGE.md @@ -20,7 +20,7 @@ Optional flags: - Fetch more callers/callees for a heavily-used symbol: `codeshot --limit 200` (default is 50) - Keep the image readable for a heavily-used symbol: `codeshot --limit 200 --max-render 30` — fetches up to 200 (so the truncation warning stays accurate) but only draws the first 30 distinct callers/callees, instead of a huge image - Render as SVG instead of PNG: `codeshot --format svg --out diagram.svg` — stays crisp when you zoom in and keeps text selectable, useful for a diagram you'll want to inspect closely rather than just glance at -- See callers-of-callers / callees-of-callees, not just the direct trail: `codeshot --depth 2` — each extra hop is drawn in a progressively lighter color so you can tell how far a node is from the symbol; Codeshot fetches this itself (CodeGraph has no multi-hop query of its own), so a heavily-connected symbol at `--depth 3`+ can be slow, and Codeshot will warn on stderr if it hit an internal safety cap before finishing +- See callers-of-callers / callees-of-callees, not just the direct trail: `codeshot --depth 2` — each extra hop is drawn in a progressively lighter color so you can tell how far a node is from the symbol; Codeshot fetches this itself (CodeGraph has no multi-hop query of its own), so a heavily-connected symbol at `--depth 3`+ can be slow, and Codeshot will warn on stderr if it hit its node-discovery safety cap before finishing (raise it with `--max-depth-nodes`, default `200`) - If the symbol name itself starts with a dash (rare — e.g. a mangled/generated name), put flags first and separate the name with `--`: `codeshot --path /path/to/repo -- -MangledName` **Reading the diagram:** boxes are code symbols; the symbol you asked about is highlighted darker. Arrows point in call direction — an arrow into your symbol is a caller, an arrow out is something it calls. Dashed arrows mean the caller is test code, so you can tell "is this only exercised by tests" at a glance. A dotted gray arrow labeled "file" means CodeGraph could only trace a module-level/import reference, not an actual function call site (common with dependency-injection patterns) — treat it with more skepticism than a solid arrow. @@ -108,7 +108,7 @@ fall back to a raw byte-compare, which does require CI to use the same - **`--out diagram.svg` produced a PNG (or vice versa)** — Codeshot only ever writes what `--format` says; it never infers format from `--out`'s extension. If you see this, you forgot `--format svg` (or whichever format matches the extension you wanted) — Codeshot now warns about this mismatch on stderr before it happens, so check for that warning first. - **The image looks unreadable / too cluttered** — This usually means the symbol has a very large number of callers or callees. Rerun with `--max-render ` (e.g. `--max-render 30`) to cap how many are drawn — Codeshot will still tell you on stderr how many were left out. If the nodes themselves are legible but hard to read at the zoom level a PNG forces on you, try `--format svg` instead — it stays crisp at any zoom, so it's worth trying before reaching for `--max-render` if you still want to see everything. - **"codeshot: showing N callers/callees — ... may have cut off more"** — Rerun with a higher `--limit` if you need the full picture (see `TECHNICAL.md` for why this warning can occasionally be a false alarm). -- **"codeshot: --depth traversal stopped early (internal safety cap of 200 discovered nodes)"** — The symbol is heavily connected enough that `--depth` hit its node-discovery cap before finishing; the graph you got is real but incomplete beyond that point. Try a smaller `--depth` (2 instead of 3), a lower `--limit`, a more specific, less-central symbol, or raise the cap itself with `codeshot --depth 3 --max-depth-nodes 500` (default is 200). +- **"codeshot: --depth traversal stopped early (safety cap of 200 discovered nodes)"** — The symbol is heavily connected enough that `--depth` hit its node-discovery cap before finishing; the graph you got is real but incomplete beyond that point. Try a smaller `--depth` (2 instead of 3), a lower `--limit`, a more specific, less-central symbol, or raise the cap itself with `codeshot --depth 3 --max-depth-nodes 500` (default is 200). - **`--depth` runs slowly** — Each additional hop makes one sequential `codegraph` call per newly discovered node (CodeGraph itself has no multi-hop traversal for `callers`/`callees`, so Codeshot does this client-side), so a well-connected symbol at `--depth 2` or higher can take noticeably longer than the default `--depth 1`. This is expected, not a bug. - **`--architecture` is taking a long time** — Expected on anything past a small repo: it's one sequential `codegraph` call per enumerated symbol, and there's no way to parallelize it (concurrent `codegraph` calls against one index race and fail). Rerun with a smaller `--max-symbols` (e.g. `--max-symbols 100`) for a faster, partial scan — Codeshot warns on stderr when the scan is cut short by the cap so you know the result is incomplete. - **`--architecture`'s diagram is a hairball / unreadable** — Same fix as symbol mode: `--max-render ` (e.g. `--max-render 20`) keeps only the busiest N files by total call-edge weight and drops the rest. Some remaining files can end up with no surviving edges if all their edges pointed at a dropped file — that's expected, not a bug, at aggressive `--max-render` values. diff --git a/render/callgraph.js b/render/callgraph.js index ce7b5ea..6bc9d12 100755 --- a/render/callgraph.js +++ b/render/callgraph.js @@ -413,7 +413,7 @@ function formatMismatchWarning(outFile, format) { function depthBudgetWarning(truncated, budget) { if (!truncated) return null; - return `codeshot: --depth traversal stopped early (internal safety cap of ${budget} discovered nodes) — the graph beyond this point is incomplete; rerun with a smaller --depth or --limit, or a more specific symbol, to stay under the cap.`; + return `codeshot: --depth traversal stopped early (safety cap of ${budget} discovered nodes) — the graph beyond this point is incomplete; rerun with a smaller --depth or --limit, a more specific symbol, or a higher --max-depth-nodes to raise the cap.`; } // codegraph's callers/callees fuzzy-match a partial/inexact query (e.g. `New` diff --git a/test/run.js b/test/run.js index 190c935..04c05fd 100644 --- a/test/run.js +++ b/test/run.js @@ -553,7 +553,7 @@ test('CLI --max-depth-nodes lowers the --depth traversal budget end-to-end again const { spawnSync } = require('child_process'); const result = spawnSync('node', [callgraphJs, 'buildDot', '--path', repoRoot, '--out', out, '--format', 'dot', '--depth', '2', '--max-depth-nodes', '1'], { encoding: 'utf8' }); assert.strictEqual(result.status, 0, `expected a successful render even when the depth budget is hit, got stderr: ${result.stderr}`); - assert.match(result.stderr, /internal safety cap of 1 discovered nodes/, 'expected the lowered --max-depth-nodes value to appear in the truncation warning'); + assert.match(result.stderr, /safety cap of 1 discovered nodes/, 'expected the lowered --max-depth-nodes value to appear in the truncation warning'); } finally { fs.rmSync(out, { force: true }); } From 7e52503b671b63de44c30879062aeac99efd7b43 Mon Sep 17 00:00:00 2001 From: Eric Minish Date: Wed, 5 Aug 2026 14:07:37 -0400 Subject: [PATCH 3/3] Fix --max-depth-nodes/--architecture guard inconsistencies Two issues from code review: - The --architecture rejection now only fires when the explicit value differs from the default (200), matching --depth's own precedent (values.depth !== '1'). Previously any explicit --max-depth-nodes was rejected outright, even one matching the default, disagreeing with --depth's "only reject if it would actually change anything" behavior for what should be an identical contract. - TECHNICAL.md's config row claimed "same rationale as --max-symbols" in a way that implied enforcement parity that doesn't exist: --max-symbols has no guard at all outside --architecture (silently ignored), while --max-depth-nodes hard-errors on a real mismatch inside --architecture. Reworded to credit --max-symbols only for the "why expose this as a flag" reasoning, and --depth for the actual --architecture-rejection precedent. --- TECHNICAL.md | 2 +- render/callgraph.js | 2 +- test/run.js | 23 +++++++++++++++++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/TECHNICAL.md b/TECHNICAL.md index 50b6fe2..a78af0d 100644 --- a/TECHNICAL.md +++ b/TECHNICAL.md @@ -145,7 +145,7 @@ No environment variables, no config file. All behavior is controlled by CLI argu | `--max-render` | unset (no cap) | Caps how many *distinct* (post-dedup) nodes are actually drawn, independent of `--limit`. This is **one shared budget across callers, callees, and `--depth`'s transitive edges combined** — not an independent `N` for each dimension (that was the original design and is arguably still the more obvious reading of "cap how many callers/callees are drawn", but it meant `--max-render 20` could still draw up to 60 nodes once `--depth` added a third dimension, defeating the flag's actual purpose of keeping the image readable; corrected 2026-07-03). Spent in priority order — direct callers first, then direct callees, then transitive edges — so the direct trail is never starved to make room for deeper hops. Exists because `--limit` controls what's fetched, not what's legible — a symbol with hundreds of real callers is still complete but produces an unusably tall image at a high `--limit`. Opt-in and orthogonal to `--limit`: fetch wide (to get an accurate truncation signal) while rendering narrow. Must be a positive integer if given. When it truncates, a stderr warning per dimension states how many of that dimension's distinct total were actually rendered (its real allotment, not the raw `--max-render` value). | | `--format` | `png` | Passed straight to `dot -T` — no allowlist of its own, so any format `dot -T` supports works, and an unsupported one surfaces `dot`'s own error (which lists the valid ones) rather than a Codeshot-invented one. `svg` is the notable alternative: unlike a raster PNG it stays crisp at any zoom and keeps text selectable, which helps more than `--max-render` alone when a graph is dense but you still want to inspect all of it. | | `--depth` | `1` (today's direct-only behavior) | How many hops of callers-of-callers / callees-of-callees to draw beyond the direct trail. Must be a positive integer. `codegraph`'s own `callers`/`callees` have no traversal depth of their own, so Codeshot implements this client-side: `collectTransitive` recursively calls `callers`/`callees` on each newly discovered node, sequentially (same concurrency-safety reason as the depth-1 calls — see External Dependencies), up to `--depth` hops or `--max-depth-nodes` (below) total discovered nodes, whichever comes first. Fan-out is multiplicative with depth and branching factor, so the budget exists specifically to stop a well-connected symbol at `--depth 3`+ from turning into hundreds of sequential `codegraph` calls; if the budget is hit first, `depthBudgetWarning` prints a stderr note that the graph beyond that point is incomplete. Each hop beyond the first is drawn in a progressively lighter edge color (`depthColor`) so distance from the symbol is visible at a glance; `--limit` and `--max-render` are NOT applied per-hop, only globally to the depth-1 fetch/render as before. Has no meaning with `--architecture` (no multi-hop file traversal concept) and is rejected if explicitly passed alongside it. | -| `--max-depth-nodes` | `200` (`DEFAULT_NODE_BUDGET`) | Only meaningful with `--depth > 1`: raises or lowers `collectTransitive`'s total-discovered-node safety cap (formerly the fixed, unconfigurable `NODE_BUDGET` constant). Must be a positive integer. Same rationale as `--max-symbols` below — a genuinely well-connected symbol at `--depth 3`+ can legitimately need a higher cap to finish, and that's a coverage/speed tradeoff only the caller can make. Rejected if explicitly passed alongside `--architecture` (no multi-hop file traversal to bound). | +| `--max-depth-nodes` | `200` (`DEFAULT_NODE_BUDGET`) | Only meaningful with `--depth > 1`: raises or lowers `collectTransitive`'s total-discovered-node safety cap (formerly the fixed, unconfigurable `NODE_BUDGET` constant). Must be a positive integer. Exposed as a flag for the same reason `--max-symbols` below is — a genuinely well-connected symbol at `--depth 3`+ can legitimately need a higher cap to finish, and that's a coverage/speed tradeoff only the caller can make. Its `--architecture` rejection follows `--depth`'s precedent above, not `--max-symbols`'s: like `--depth`, it's rejected only when an *explicit, non-default* value is passed alongside `--architecture` (`codeshot --architecture --max-depth-nodes 200` is a silent no-op, same as `--depth 1`) — unlike `--max-symbols`, which has no `--architecture`-only guard at all and is simply ignored outside that mode. | | `--architecture` | `false` | Switches to whole-repo file-level dependency graph mode instead of a single symbol's trail — see the second pipeline diagram above. Mutually exclusive with the `` positional (rejected if both given); `` becomes optional-and-forbidden rather than required. | | `--max-symbols` | `500` | `--architecture`-only: caps how many enumerated symbols get probed, exposed as a flag for the same reason `--max-depth-nodes` above is — `--architecture` is a multi-minute O(symbols) sequential scan by nature (confirmed: 500 symbols took over 5 minutes against a real ~1,900-node repo), so users legitimately need to trade coverage for speed themselves rather than wait on a hidden safety net tuned for an already-fast operation. Must be a positive integer. `symbolBudgetWarning` prints a stderr note when this cuts enumeration short. | diff --git a/render/callgraph.js b/render/callgraph.js index 6bc9d12..96242f1 100755 --- a/render/callgraph.js +++ b/render/callgraph.js @@ -908,7 +908,7 @@ async function main() { console.error(`codeshot: --max-depth-nodes must be a positive integer, got '${values['max-depth-nodes']}'`); process.exit(1); } - if (values.architecture) { + if (values.architecture && maxDepthNodes !== DEFAULT_NODE_BUDGET) { console.error('codeshot: --max-depth-nodes has no effect with --architecture (there is no multi-hop file traversal)'); process.exit(1); } diff --git a/test/run.js b/test/run.js index 04c05fd..84cdb21 100644 --- a/test/run.js +++ b/test/run.js @@ -395,6 +395,29 @@ test('--architecture rejects an explicit --max-depth-nodes (no multi-hop file tr assert.strictEqual(threw, true, 'expected --architecture + --max-depth-nodes to be rejected'); }); +test('--architecture accepts an explicit --max-depth-nodes that matches its own default (same non-divergence rule as --depth)', () => { + // --depth 1 explicitly passed alongside --architecture is a silent no-op + // because it matches --depth's own default (values.depth !== '1' in main). + // --max-depth-nodes must follow the same rule for its own default (200), not + // just "was --max-depth-nodes passed at all" — otherwise the two flags + // disagree on what "explicitly passed" means for an identical "rejected only + // if it would actually change anything" contract. + const { execFileSync } = require('child_process'); + const path = require('path'); + const repoRoot = path.join(__dirname, '..'); + const callgraphJs = path.join(repoRoot, 'render', 'callgraph.js'); + + try { + execFileSync('codegraph', ['callers', '--path', repoRoot, '--limit', '1', '--json', '--', 'buildDot'], { stdio: 'pipe' }); + } catch { + console.log(' # skipped: `codegraph` not on PATH or this repo is not codegraph-indexed'); + return; + } + + const out = execFileSync('node', [callgraphJs, '--architecture', '--max-depth-nodes', '200', '--path', repoRoot, '--format', 'dot'], { encoding: 'utf8', stdio: 'pipe' }); + assert.ok(out.trim().length > 0, 'expected --architecture --max-depth-nodes 200 to succeed and print an output path, not be rejected'); +}); + test('missing symbol argument is rejected with a codeshot-prefixed message', () => { const { execFileSync } = require('child_process'); let threw = false;