How Findings Are Added to State in SkillSpector Analysis: A Complete Guide
Findings are added to the SkillSpector state by returning them from analyzer nodes, where LangGraph automatically appends them to the shared state list using the operator.add annotation on the findings field in SkillspectorState.
NVIDIA's SkillSpector uses a LangGraph-based workflow to orchestrate static and semantic analysis across your codebase. Understanding how findings flow through the shared state is essential for customizing analyzers or debugging the analysis pipeline. This guide examines the exact mechanism—from state definition to final report generation—based on the current implementation in the NVIDIA/SkillSpector repository.
Understanding the SkillspectorState Definition
The state mechanism centers on SkillspectorState, defined in src/skillspector/state.py as a TypedDict that travels through every node in the analysis graph.
The TypedDict Structure with operator.add
The findings field uses Python's Annotated type with operator.add to declare a reduction behavior:
# src/skillspector/state.py
from typing import Annotated
import operator
class SkillspectorState(TypedDict, total=False):
# Accumulated findings (reducer: analyzer nodes append to this list)
findings: Annotated[list[Finding], operator.add]
filtered_findings: list[Finding]
# ... other fields
This annotation tells the LangGraph engine to reduce incoming findings payloads by appending them to the existing list. When any node returns a findings key, the runtime automatically merges that list into the state using list addition, eliminating the need for manual state mutation.
How Analyzer Nodes Return Findings
Each analyzer communicates with the state through a strict return type that matches the expected state updates.
The AnalyzerNodeResponse Pattern
Analyzers return an AnalyzerNodeResponse, defined in the analyzer modules as:
# src/skillspector/nodes/analyzers/static_yara.py
from typing import TypedDict
class AnalyzerNodeResponse(TypedDict):
"""Strict analyzer update payload for graph state."""
findings: list[Finding]
Nodes never modify the state directly. Instead, they return a dictionary containing the findings key, and LangGraph handles the aggregation.
Example from Static YARA Analyzer
The YARA analyzer demonstrates this pattern in src/skillspector/nodes/analyzers/static_yara.py:
def node(state: SkillspectorState) -> AnalyzerNodeResponse:
findings = []
# ... yara matching logic ...
for af in yara_matches:
findings.append(analyzer_finding_to_finding(af))
return {"findings": findings}
When this node executes, the returned findings list is automatically added to state["findings"] by the graph engine.
The LangGraph Reduction Mechanism
The state aggregation happens automatically at the graph engine level, not in your node code.
Automatic State Aggregation
When an analyzer node returns its payload, LangGraph inspects the SkillspectorState type hints. Because findings is declared as Annotated[list[Finding], operator.add], the engine executes:
# Conceptual behavior (handled by LangGraph)
state["findings"] = state.get("findings", []) + returned_findings
This ensures findings accumulate in the order nodes execute, creating a complete, ordered collection without race conditions or manual synchronization.
Filtering and Consuming Findings
After aggregation, the system supports optional filtering before report generation.
Meta-Analyzer Filtering
The meta-analyzer in src/skillspector/nodes/meta_analyzer.py processes the raw findings list and can produce a refined subset:
# Returns filtered_findings to state
return {"filtered_findings": refined_findings_list}
Unlike the raw findings field, filtered_findings is not annotated with operator.add, so it replaces any previous value rather than appending.
Report Generation
The report node in src/skillspector/nodes/report.py consumes the findings with a fallback pattern:
findings = state.get("filtered_findings", state.get("findings", []))
This allows the report to use filtered results when available, otherwise defaulting to the complete aggregated list.
Summary
- State definition:
SkillspectorStateinsrc/skillspector/state.pydeclaresfindingswithAnnotated[list[Finding], operator.add]to enable automatic list appending. - Node returns: Analyzer nodes return
AnalyzerNodeResponsecontaining{"findings": [...]}rather than mutating state directly. - Automatic reduction: LangGraph merges node outputs into state using the
operator.addannotation, ensuring findings accumulate without manual intervention. - Filtering workflow: The meta-analyzer can produce
filtered_findings, which report nodes prefer over the raw accumulated list. - Immutability: Nodes remain pure functions—state changes only occur through the graph's reduction mechanism based on return values.
Frequently Asked Questions
How do I create a custom analyzer that adds findings to state?
Define your node function to accept SkillspectorState and return AnalyzerNodeResponse with a findings list. The LangGraph engine automatically appends your returned findings to the shared state:
def my_analyzer(state: SkillspectorState) -> AnalyzerNodeResponse:
findings = []
if "suspicious_pattern" in state.get("components", []):
findings.append(Finding(
rule_id="CUSTOM001",
file="example.py",
severity="high",
message="Suspicious pattern detected"
))
return {"findings": findings}
Why does the findings field use operator.add instead of standard assignment?
The Annotated[list[Finding], operator.add] declaration enables the LangGraph reduction pattern. When multiple analyzer nodes run in parallel or sequence, each returns a partial findings list. The operator.add annotation tells the engine to concatenate these lists into a single cumulative collection rather than replacing the previous value, preserving findings from all analyzers.
What is the difference between findings and filtered_findings in the state?
The findings field accumulates raw output from all analyzer nodes via the operator.add reducer. The filtered_findings field (defined without the annotation) is written by the meta-analyzer node to store a processed subset. Report nodes check for filtered_findings first, falling back to the raw findings list if no filtering occurred, allowing the workflow to optionally refine results before final output.
Can analyzer nodes modify state directly instead of returning findings?
No. Analyzer nodes should never mutate the state parameter directly. The SkillSpector architecture relies on immutable state updates—nodes must return an AnalyzerNodeResponse containing the findings key. The LangGraph runtime handles the actual state update using the reduction annotation, ensuring predictable, debuggable workflow execution.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →