From 220ee09b19923dc1bf0ed3ec6607bcf40591f4d2 Mon Sep 17 00:00:00 2001 From: Nandana Dileep Date: Mon, 6 Apr 2026 00:04:37 +0530 Subject: [PATCH 1/4] fix: positional path arg, deduplicate hotspots, suppress spurious sarif error Fixes #12 and #13. - quality/hotspots: accept an optional positional DIRECTORY argument so 'codexa quality .' and 'codexa hotspots .' work without --path. Previously Click rejected the bare '.' with 'Got unexpected extra argument (.)'. - hotspots: deduplicate callable_symbols by (file_path, name) before scoring so that re-indexed or multiply-parsed symbols do not produce repeated entries in the hotspot list with identical scores. - quality: suppress the 'Could not load sarif: No module named sarif_om' ERROR that bandit logs on every import when the optional sarif_om package is not installed. The filter is applied only during bandit's import and removed immediately after, so genuine bandit errors are still surfaced. --- semantic_code_intelligence/ci/hotspots.py | 11 +++++++++- semantic_code_intelligence/ci/quality.py | 22 +++++++++++++++++-- .../cli/commands/hotspots_cmd.py | 13 +++++++++-- .../cli/commands/quality_cmd.py | 13 +++++++++-- 4 files changed, 52 insertions(+), 7 deletions(-) diff --git a/semantic_code_intelligence/ci/hotspots.py b/semantic_code_intelligence/ci/hotspots.py index cd88e92..5c9ca1b 100644 --- a/semantic_code_intelligence/ci/hotspots.py +++ b/semantic_code_intelligence/ci/hotspots.py @@ -172,7 +172,16 @@ def analyze_hotspots( w_fan_out += extra w_churn = 0.0 - callable_symbols = [s for s in symbols if s.kind in ("function", "method")] + # Deduplicate by (file_path, name) so that re-indexed or multiply-parsed + # symbols don't appear as separate hotspot entries with identical scores. + _seen: set[tuple[str, str]] = set() + callable_symbols = [] + for s in symbols: + if s.kind in ("function", "method"): + key = (s.file_path, s.name) + if key not in _seen: + _seen.add(key) + callable_symbols.append(s) # ── Per-symbol raw metrics ─────────────────────────────────── # Complexity diff --git a/semantic_code_intelligence/ci/quality.py b/semantic_code_intelligence/ci/quality.py index eb61277..51e38c0 100644 --- a/semantic_code_intelligence/ci/quality.py +++ b/semantic_code_intelligence/ci/quality.py @@ -354,8 +354,26 @@ def detect_duplicates( # ── Bandit security linting (optional) ─────────────────────────────── try: - from bandit.core import manager as _bandit_manager - from bandit.core import config as _bandit_config + import logging as _logging + + # Bandit eagerly loads all its formatters at import time, including a + # SARIF formatter that requires the optional `sarif_om` package. When + # `sarif_om` is absent bandit logs an ERROR that appears on every run + # even when SARIF output was never requested. Suppress that specific + # message during the import so users only see errors that are relevant + # to them. + class _SuppressSarifFilter(_logging.Filter): + def filter(self, record: _logging.LogRecord) -> bool: + return "Could not load 'sarif'" not in record.getMessage() + + _bandit_root = _logging.getLogger("bandit") + _sarif_filter = _SuppressSarifFilter() + _bandit_root.addFilter(_sarif_filter) + try: + from bandit.core import manager as _bandit_manager + from bandit.core import config as _bandit_config + finally: + _bandit_root.removeFilter(_sarif_filter) _HAS_BANDIT = True except ImportError: # pragma: no cover diff --git a/semantic_code_intelligence/cli/commands/hotspots_cmd.py b/semantic_code_intelligence/cli/commands/hotspots_cmd.py index 8a0109f..f90d459 100644 --- a/semantic_code_intelligence/cli/commands/hotspots_cmd.py +++ b/semantic_code_intelligence/cli/commands/hotspots_cmd.py @@ -18,11 +18,17 @@ @click.command("hotspots") +@click.argument( + "directory", + default=None, + required=False, + type=click.Path(exists=True, file_okay=False, resolve_path=True), +) @click.option( "--path", "-p", default=".", type=click.Path(exists=True, file_okay=False, resolve_path=True), - help="Project root path.", + help="Project root path (alternative to the positional argument).", ) @click.option( "--json-output", "--json", "json_mode", @@ -47,6 +53,7 @@ @click.pass_context def hotspots_cmd( ctx: click.Context, + directory: str | None, path: str, json_mode: bool, pipe: bool, @@ -62,6 +69,8 @@ def hotspots_cmd( codexa hotspots + codexa hotspots . + codexa hotspots --top-n 10 --json codexa hotspots --no-git --pipe @@ -69,7 +78,7 @@ def hotspots_cmd( from semantic_code_intelligence.ci.hotspots import analyze_hotspots from semantic_code_intelligence.context.engine import CallGraph, ContextBuilder, DependencyMap - root = Path(path).resolve() + root = Path(directory or path).resolve() builder = ContextBuilder() dep_map = DependencyMap() diff --git a/semantic_code_intelligence/cli/commands/quality_cmd.py b/semantic_code_intelligence/cli/commands/quality_cmd.py index 32387c3..b0524a4 100644 --- a/semantic_code_intelligence/cli/commands/quality_cmd.py +++ b/semantic_code_intelligence/cli/commands/quality_cmd.py @@ -112,12 +112,18 @@ def _output_report_rich(report: "QualityReport", root: Path) -> None: @click.command("quality") +@click.argument( + "directory", + default=None, + required=False, + type=click.Path(exists=True, file_okay=False, resolve_path=True), +) @click.option( "--path", "-p", default=".", type=click.Path(exists=True, file_okay=False, resolve_path=True), - help="Project root path.", + help="Project root path (alternative to the positional argument).", ) @click.option( "--json-output", @@ -148,6 +154,7 @@ def _output_report_rich(report: "QualityReport", root: Path) -> None: @click.pass_context def quality_cmd( ctx: click.Context, + directory: str | None, path: str, json_mode: bool, complexity_threshold: int, @@ -163,6 +170,8 @@ def quality_cmd( codexa quality + codexa quality . + codexa quality --json codexa quality --safety-only --pipe @@ -172,7 +181,7 @@ def quality_cmd( from semantic_code_intelligence.ci.quality import analyze_project, QualityReport from semantic_code_intelligence.llm.safety import SafetyValidator - root = Path(path).resolve() + root = Path(directory or path).resolve() if safety_only: # Fast path: only safety scan From 7873b0067e28e40a2ceae654d1c093d4a3835a39 Mon Sep 17 00:00:00 2001 From: Mounir Elsrogy <90654832+M9nx@users.noreply.github.com> Date: Wed, 8 Apr 2026 09:16:27 +0200 Subject: [PATCH 2/4] Update semantic_code_intelligence/ci/quality.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- semantic_code_intelligence/ci/quality.py | 36 ++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/semantic_code_intelligence/ci/quality.py b/semantic_code_intelligence/ci/quality.py index 51e38c0..3803a20 100644 --- a/semantic_code_intelligence/ci/quality.py +++ b/semantic_code_intelligence/ci/quality.py @@ -366,14 +366,50 @@ class _SuppressSarifFilter(_logging.Filter): def filter(self, record: _logging.LogRecord) -> bool: return "Could not load 'sarif'" not in record.getMessage() + def _logger_ancestry(logger: _logging.Logger) -> list[_logging.Logger]: + ancestry: list[_logging.Logger] = [] + current: _logging.Logger | None = logger + while current is not None: + ancestry.append(current) + if not current.propagate: + break + parent = current.parent + current = parent if isinstance(parent, _logging.Logger) else None + return ancestry + + def _add_filter_to_handlers( + logger: _logging.Logger, + log_filter: _logging.Filter, + ) -> list[_logging.Handler]: + filtered_handlers: list[_logging.Handler] = [] + seen_handlers: set[int] = set() + for current_logger in _logger_ancestry(logger): + for handler in current_logger.handlers: + handler_id = id(handler) + if handler_id in seen_handlers: + continue + handler.addFilter(log_filter) + filtered_handlers.append(handler) + seen_handlers.add(handler_id) + return filtered_handlers + + def _remove_filter_from_handlers( + handlers: list[_logging.Handler], + log_filter: _logging.Filter, + ) -> None: + for handler in handlers: + handler.removeFilter(log_filter) + _bandit_root = _logging.getLogger("bandit") _sarif_filter = _SuppressSarifFilter() + _filtered_handlers = _add_filter_to_handlers(_bandit_root, _sarif_filter) _bandit_root.addFilter(_sarif_filter) try: from bandit.core import manager as _bandit_manager from bandit.core import config as _bandit_config finally: _bandit_root.removeFilter(_sarif_filter) + _remove_filter_from_handlers(_filtered_handlers, _sarif_filter) _HAS_BANDIT = True except ImportError: # pragma: no cover From b233d47a36a46b86f24dc5319601ad97029ed4ad Mon Sep 17 00:00:00 2001 From: Mounir Elsrogy <90654832+M9nx@users.noreply.github.com> Date: Wed, 8 Apr 2026 09:17:42 +0200 Subject: [PATCH 3/4] Update semantic_code_intelligence/cli/commands/hotspots_cmd.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- semantic_code_intelligence/cli/commands/hotspots_cmd.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/semantic_code_intelligence/cli/commands/hotspots_cmd.py b/semantic_code_intelligence/cli/commands/hotspots_cmd.py index f90d459..e85fb5d 100644 --- a/semantic_code_intelligence/cli/commands/hotspots_cmd.py +++ b/semantic_code_intelligence/cli/commands/hotspots_cmd.py @@ -78,6 +78,10 @@ def hotspots_cmd( from semantic_code_intelligence.ci.hotspots import analyze_hotspots from semantic_code_intelligence.context.engine import CallGraph, ContextBuilder, DependencyMap + if directory is not None and ctx.get_parameter_source("path") == click.core.ParameterSource.COMMANDLINE: + raise click.UsageError( + "Provide either the positional 'directory' argument or '--path', not both." + ) root = Path(directory or path).resolve() builder = ContextBuilder() dep_map = DependencyMap() From 3d5fe3d380b7571102008b9d7ad6492c4afec532 Mon Sep 17 00:00:00 2001 From: Mounir Elsrogy <90654832+M9nx@users.noreply.github.com> Date: Wed, 8 Apr 2026 09:17:51 +0200 Subject: [PATCH 4/4] Update semantic_code_intelligence/cli/commands/quality_cmd.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- semantic_code_intelligence/cli/commands/quality_cmd.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/semantic_code_intelligence/cli/commands/quality_cmd.py b/semantic_code_intelligence/cli/commands/quality_cmd.py index b0524a4..7637c51 100644 --- a/semantic_code_intelligence/cli/commands/quality_cmd.py +++ b/semantic_code_intelligence/cli/commands/quality_cmd.py @@ -181,8 +181,15 @@ def quality_cmd( from semantic_code_intelligence.ci.quality import analyze_project, QualityReport from semantic_code_intelligence.llm.safety import SafetyValidator - root = Path(directory or path).resolve() + directory_path = Path(directory).resolve() if directory else None + option_path = Path(path).resolve() + if directory_path is not None and directory_path != option_path: + raise click.UsageError( + "Cannot use both the positional 'directory' argument and '--path' with different values." + ) + + root = directory_path or option_path if safety_only: # Fast path: only safety scan from semantic_code_intelligence.parsing.parser import EXTENSION_TO_LANGUAGE