Skip to content

v0.14.0

Latest

Choose a tag to compare

@github-actions github-actions released this 20 Jun 20:17
8df611f

Added

  • 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 truncationimpact'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 lookupscymbal 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.