diff --git a/src/skillspector/cli.py b/src/skillspector/cli.py index 57bac058..34a4b9f1 100644 --- a/src/skillspector/cli.py +++ b/src/skillspector/cli.py @@ -177,7 +177,7 @@ def scan( typer.Option( "--recursive", "-r", - help="Scan directories containing multiple skills (immediate subdirectories with SKILL.md) independently.", + help="Scan immediate subdirectories that each contain a SKILL.md as independent skills.", ), ] = False, verbose: Annotated[ diff --git a/src/skillspector/nodes/analyzers/static_runner.py b/src/skillspector/nodes/analyzers/static_runner.py index ee0d50fb..a32c6c4c 100644 --- a/src/skillspector/nodes/analyzers/static_runner.py +++ b/src/skillspector/nodes/analyzers/static_runner.py @@ -84,7 +84,7 @@ def _is_eval_dataset(path: str) -> bool: _DOCUMENTATION_CONFIDENCE_FACTOR = 0.3 _CODE_EXAMPLE_CONFIDENCE_FACTOR = 0.5 -_NON_EXECUTABLE_FILE_TYPES = frozenset({"markdown", "text", "json", "yaml", "toml", "other"}) +_NON_EXECUTABLE_FILE_TYPES = frozenset({"markdown", "text", "json", "yaml", "toml"}) def _is_documentation_markdown(path: str) -> bool: diff --git a/src/skillspector/nodes/meta_analyzer.py b/src/skillspector/nodes/meta_analyzer.py index c5268375..93c15b50 100644 --- a/src/skillspector/nodes/meta_analyzer.py +++ b/src/skillspector/nodes/meta_analyzer.py @@ -236,7 +236,7 @@ def _fallback_filtered(findings: list[Finding]) -> list[Finding]: result: list[Finding] = [] for f in findings: - severity_upper = f.severity.upper() + severity_upper = (f.severity or "LOW").upper() confidence = f.confidence if f.context and is_code_example(f.context): confidence *= _CODE_EXAMPLE_DOWNWEIGHT diff --git a/tests/nodes/analyzers/test_static_runner_filtering.py b/tests/nodes/analyzers/test_static_runner_filtering.py index 207c6cba..33f82e2d 100644 --- a/tests/nodes/analyzers/test_static_runner_filtering.py +++ b/tests/nodes/analyzers/test_static_runner_filtering.py @@ -115,6 +115,23 @@ def test_finding_in_executable_not_dropped_by_generic_indicator(self) -> None: for f in tm1_findings: assert f.confidence > 0 + def test_extensionless_file_not_hard_dropped_by_code_example(self) -> None: + """An extensionless file (inferred as 'other') in code-example context is downweighted, not dropped.""" + content = """\ +#!/bin/bash +# Example: cleanup old builds +rm -rf /tmp/build-cache +""" + state = { + "components": ["cleanup_script"], + "file_cache": {"cleanup_script": content}, + } + findings = static_runner.run_static_patterns(state, [tm_module]) + tm1_findings = [f for f in findings if f.rule_id == "TM1"] + assert len(tm1_findings) >= 1, ( + "Extensionless files must not have code-example findings hard-dropped" + ) + def test_skill_md_findings_are_not_filtered_by_backticks(self) -> None: """SKILL.md is the primary instruction file — backticks alone shouldn't filter.""" content = """\ diff --git a/tests/nodes/test_meta_analyzer_fallback.py b/tests/nodes/test_meta_analyzer_fallback.py index 130d4e0a..7da27a0a 100644 --- a/tests/nodes/test_meta_analyzer_fallback.py +++ b/tests/nodes/test_meta_analyzer_fallback.py @@ -99,6 +99,19 @@ def test_low_severity_below_threshold_still_dropped(self) -> None: assert len(result) == 0 + def test_none_severity_treated_as_low(self) -> None: + """Finding with None severity does not crash — treated as LOW.""" + findings = [_finding(confidence=0.8, severity=None)] + result = _fallback_filtered(findings) + assert len(result) == 1 + + def test_none_severity_below_threshold_dropped(self) -> None: + """None severity at low confidence is dropped (no severity floor protection).""" + findings = [_finding(confidence=0.3, severity=None)] + result = _fallback_filtered(findings) + assert len(result) == 0 + + class TestCodeExampleFiltering: """Findings in code example context are downweighted, not hard-dropped."""