diff --git a/src/skillspector/inspection_ledger.py b/src/skillspector/inspection_ledger.py index 953ace9f2..4b0f42db3 100644 --- a/src/skillspector/inspection_ledger.py +++ b/src/skillspector/inspection_ledger.py @@ -526,8 +526,10 @@ def analyzer_status_event( def analyzer_status_for_events( analyzer_id: str, events: Iterable[InspectionLedgerEvent] ) -> AnalyzerStatusEvent: - """Summarize an analyzer's terminal work without exposing event payloads.""" - terminal_events = list(events) + """Summarize analyzer work; scope-boundary records remain separate exclusions.""" + terminal_events = [ + event for event in events if event.get("record_type") != LedgerRecordType.SCOPE_BOUNDARY + ] if not terminal_events: return analyzer_status_event( analyzer_id=analyzer_id, diff --git a/tests/nodes/test_finalize_inspection_ledger.py b/tests/nodes/test_finalize_inspection_ledger.py index 67f2ca594..093784a87 100644 --- a/tests/nodes/test_finalize_inspection_ledger.py +++ b/tests/nodes/test_finalize_inspection_ledger.py @@ -22,6 +22,7 @@ LedgerReason, LedgerRecordType, analyzer_status_event, + analyzer_status_for_events, finalize_ledger, guard_analyzer_node, inspection_work_id, @@ -125,6 +126,55 @@ def test_completed_work_is_covered_and_resolves_emitted_finding_ids() -> None: assert effective_ids == [finding.finding_id] +def test_out_of_scope_boundaries_do_not_count_as_analyzer_failures() -> None: + """Non-fatal scope boundaries are not planned work for an analyzer.""" + analyzer_id = "static_patterns_prompt_injection" + inspected = ledger_event( + outcome=LedgerOutcome.COMPLETED, + phase="static", + analyzer_id=analyzer_id, + path="scripts/run.py", + ) + excluded = ledger_event( + outcome=LedgerOutcome.OUT_OF_SCOPE, + record_type=LedgerRecordType.SCOPE_BOUNDARY, + phase="static", + analyzer_id=analyzer_id, + path="assets/font.ttf", + reason=LedgerReason.BINARY_CONTENT, + ) + + completeness, _ = finalize_ledger( + { + "components": ["scripts/run.py", "assets/font.ttf"], + "findings": [], + "inspection_ledger": [inspected, excluded], + "analyzer_status_events": [ + analyzer_status_for_events(analyzer_id, [inspected, excluded]) + ], + "artifact_inventory": [ + {"path": "scripts/run.py", "disposition": "complete"}, + { + "path": "assets/font.ttf", + "disposition": "out_of_scope", + "content_kind": "binary", + }, + ], + } + ) + + status = next( + row for row in completeness["analyzer_statuses"] if row["analyzer_id"] == analyzer_id + ) + assert status["status"] == "completed" + assert status["planned_work"] == 1 + assert status["completed"] == 1 + assert status["failed"] == 0 + assert completeness["scope_exclusions"][0]["path"] == "assets/font.ttf" + assert completeness["status"] == "complete" + assert completeness["coverage_percent"] == 100.0 + + def test_missing_terminal_row_becomes_fatal_unaccounted_work() -> None: work_id = inspection_work_id("behavioral_ast", "broken.py", None, None) diff --git a/tests/test_inspection_ledger.py b/tests/test_inspection_ledger.py index e8d73cc9c..dc5fc4c7b 100644 --- a/tests/test_inspection_ledger.py +++ b/tests/test_inspection_ledger.py @@ -8,6 +8,7 @@ from skillspector.inspection_ledger import ( LedgerOutcome, LedgerReason, + LedgerRecordType, analyzer_status_for_events, inspection_work_id, ledger_event, @@ -69,6 +70,28 @@ def test_analyzer_status_for_events_summarizes_terminal_work() -> None: } +def test_analyzer_status_for_scope_boundaries_has_no_planned_work() -> None: + """Scope-boundary records are not work assigned to the analyzer.""" + event = ledger_event( + outcome=LedgerOutcome.OUT_OF_SCOPE, + record_type=LedgerRecordType.SCOPE_BOUNDARY, + phase="static", + analyzer_id="static_test", + path="assets/font.ttf", + reason=LedgerReason.BINARY_CONTENT, + ) + + status = analyzer_status_for_events("static_test", [event]) + + assert status == { + "analyzer_id": "static_test", + "status": "not_applicable", + "planned_work": [], + "reason_code": LedgerReason.NO_APPLICABLE_FILES, + "message": "No files matched this analyzer's applicability contract.", + } + + @pytest.mark.parametrize( ("reason", "expected_outcome"), [