Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/skillspector/inspection_ledger.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
50 changes: 50 additions & 0 deletions tests/nodes/test_finalize_inspection_ledger.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
LedgerReason,
LedgerRecordType,
analyzer_status_event,
analyzer_status_for_events,
finalize_ledger,
guard_analyzer_node,
inspection_work_id,
Expand Down Expand Up @@ -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)

Expand Down
23 changes: 23 additions & 0 deletions tests/test_inspection_ledger.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from skillspector.inspection_ledger import (
LedgerOutcome,
LedgerReason,
LedgerRecordType,
analyzer_status_for_events,
inspection_work_id,
ledger_event,
Expand Down Expand Up @@ -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"),
[
Expand Down
Loading