You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Cross-language resolution scope for trace / impact / investigate / changed — cymbal resolves references by name only (no receiver/type/import resolution), so a call could mis-resolve to a same-named symbol in an unrelated language. A new --resolve-scope flag (same | family | all, default family) constrains which languages a name resolves to, defaulting to the interop family of the symbol it resolves from so legitimate cross-language calls still resolve. Families: jvm (java/kotlin/scala), js (javascript/typescript/tsx), c (c/cpp); every other language scopes to itself. In --graph mode, an out-of-scope name collision is surfaced as a new scope_filtered entry in the unresolved diagnostics list (distinct from external), so the reason no edge was drawn is explicit. The active scope is reported as resolve_scope in frontmatter and JSON. An unknown --resolve-scope value is a hard error.
trace filters unresolved callees by default — callees that don't resolve to an indexed symbol (stdlib, third-party, builtins) are dropped by default; pass --include-unresolved to keep them in text/JSON (and as dashed ext: nodes in --graph). Graph mode still records all unresolved callees in the unresolved diagnostics list regardless.
Ambiguous graph nodes are annotated — because resolution is name-only, two distinct symbols with the same name (e.g. Dup in different packages, or a Go App and a TSX App) collapse into a single --graph node. Such a node now carries definition_count and a definitions list (path, language, start_line) in JSON, and a Name (N defs) cue in mermaid/dot, so a consumer can see the conflation and investigate each definition rather than trusting one merged node.
trace / impact flag a starting symbol that exists in more than one language — if the name you ask about is defined in several languages, the output now lists those languages as symbol_languages (e.g. App=go,tsx). It tells you the results cover every language's version of that name, since --resolve-scope decides how a call resolves, not which same-named symbol you meant.
impact and trace split callers/callees and flag truncation — impact's header/JSON now report total_callers: N (P production, T test[, U unknown]) so you can triage production blast radius separately from test fan-out at a glance. Both impact and trace gain a truncated field (frontmatter line + JSON boolean), set when a per-symbol --limit was hit, so partial result sets are never presented as complete (detected by over-fetching one row past the limit). Callers are classified by file path with anchored patterns (/tests/, _test.go, *Test.java, *.spec.ts, …) — never bare test/spec substrings, so production code like C# *Specification.cs or Rails tests_controller.rb is not mis-flagged; genuinely ambiguous paths (/testing/, /fixtures/, /examples/) classify as unknown. Validated across 8 repos / 7 languages.
impact --no-tests — excludes callers in test files (keeps production and unknown). Classification happens during traversal, so test callers never consume the --limit budget ahead of production callers; test nodes are hidden from output but still traversed so production code reachable through a test isn't lost.
cymbal changed — diff-scoped impact in one call — maps a git diff to the symbols it touches and reports each one's references and transitive impact, so reviewing "what does this change affect?" is a single command instead of parsing the diff and running impact per symbol. Defaults to unstaged working-tree changes (git diff); --staged diffs the staged changes, --base <ref> diffs the working tree against another single ref (e.g. your branch point — ranges like a..b are rejected, since their new side isn't the working tree). Changed symbols are attributed by parsing the actual diffed blobs on both sides — added/modified lines map to symbols in the new version, deleted lines to symbols in the old version — so whole-symbol deletions are named (not mis-attributed to a neighbour), --staged attribution matches the staged content even with unstaged edits present, and each changed line maps to its enclosing navigable definition (function/method/type, including Python/Rust methods) rather than a function-local. Per symbol it prints exact, un-truncated references counts (reference_rows / referencing_files, split production/test/unknown) and a capped impact caller summary with its own truncated flag; deleted symbols are listed separately, and ambiguous (multi-definition) names carry definition_count + a definitions list. Bounded by --max-symbols (default 40) and a --max-impact soft cap on total caller rows (default 500), with --limit/--depth per symbol and --no-tests passthrough. Impact and references are name-scoped (cymbal resolves by name), so counts for a name with several definitions may span them (reported as definition_count); --resolve-scope (same | family | all, default family) constrains cross-language resolution as on trace/impact, and the active scope is echoed as resolve_scope in frontmatter and JSON. Operates only on the current worktree; arbitrary commit ranges whose new side isn't the working tree are unsupported.
index.ReferenceCountsWithScope — library API returning complete (un-truncated) name-scoped reference counts split by production/test/unknown file class, for changed and other callers.
investigate accepts --stdin for piped batch lookups — cymbal investigate already took multiple symbol names as positional args; it now also reads newline-separated names from stdin (--stdin), matching search / show / refs / impact / trace / impls. This makes the standard pipe work — cymbal outline svc.go -s --names | cymbal investigate --stdin — and closes a gap where the SessionStart hook already advertised piped batching for investigate. Names from args and stdin are merged and deduped (first-seen order), with #-prefixed and blank lines skipped.
impact now reports exact reference metrics and completeness context — alongside the (limit-capped) caller analysis, impact adds a metrics block: the exact, un-truncated count of references to the symbol (reference_rows / referencing_files, split production/test/unknown), so you get true reference breadth even when callers are truncated. JSON also gains depth, limit, definition_count, and — for ambiguous names — a definitions list of locations; lookup failures surface as references_error / definition_count_error rather than a misleading zero. When a name has more definitions than were requested, definition_count is shown in the header too. No categorical risk label is emitted — raw, inspectable metrics are surfaced instead. References are name-scoped (exact for uniquely-named symbols, a conflated over-count for colliding names) and count reference sites, a distinct and larger metric than the deduplicated caller count.
Changed
BREAKING: single-symbol trace --json / impact --json now return the same object shape as multi-symbol — previously a single symbol emitted a bare JSON array while multiple symbols emitted {symbols, …, results}. Both now emit the object form ({symbols, …, resolve_scope, results: [{row, hit_symbols}]}), so agent consumers parse one consistent shape regardless of symbol count. Scripts that parsed the top-level array must read .results[].row instead.
Fixed
--graph no longer mislabels class-nested methods as external — the graph builder's symbol metadata was restricted to top-level (depth=0) symbols, while trace/impact resolve against symbols at any depth. As a result, in class-based languages (Java, Python, TypeScript, …) a real method calling another method showed up in trace --graph / impact --graph as an external (stdlib/third-party) node with no resolved edge — even though trace text and refs correctly identified it. Graph metadata now covers all depths, so nested methods resolve as real nodes/edges. Go was largely unaffected (its functions/methods are top-level).
--graph keeps a name that's in scope under one definition but excluded under another — when a name had several definitions, --graph-scope / --exclude were judged against a single arbitrary definition, so a node could be dropped even when an in-scope definition existed. Visibility now considers every definition of the name and displays an in-scope one.
OpenCode hook installs now target the plugin directory OpenCode actually loads — user-scope cymbal hook install opencode now writes to ~/.config/opencode/plugins/cymbal-opencode.js instead of the Windows-native %APPDATA%\opencode\plugins path that OpenCode 1.15.9 ignores. The installer also honors OPENCODE_CONFIG_DIR by writing to $OPENCODE_CONFIG_DIR/plugins/cymbal-opencode.js, matching OpenCode's custom config directory behavior.
OpenCode's managed plugin now loads on OpenCode 1.15.13 — the generated plugin exposes a single CymbalPlugin export instead of extra module exports that OpenCode tried to load as plugins, and it now nudges OpenCode's Bash, Grep, and Glob tools toward cymbal-first code navigation. Fixes #63.