# How Findings Are Added to State in SkillSpector Analysis: A Complete Guide

> Learn how findings are added to the SkillSpector state using LangGraph's operator add annotation for efficient analysis. Understand the process in this complete guide.

- Repository: [NVIDIA Corporation/SkillSpector](https://github.com/NVIDIA/SkillSpector)
- Tags: how-to-guide
- Published: 2026-06-14

---

**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`](https://github.com/NVIDIA/SkillSpector/blob/main/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:

```python

# 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:

```python

# 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`](https://github.com/NVIDIA/SkillSpector/blob/main/src/skillspector/nodes/analyzers/static_yara.py):

```python
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:

```python

# 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`](https://github.com/NVIDIA/SkillSpector/blob/main/src/skillspector/nodes/meta_analyzer.py) processes the raw `findings` list and can produce a refined subset:

```python

# 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`](https://github.com/NVIDIA/SkillSpector/blob/main/src/skillspector/nodes/report.py) consumes the findings with a fallback pattern:

```python
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**: `SkillspectorState` in [`src/skillspector/state.py`](https://github.com/NVIDIA/SkillSpector/blob/main/src/skillspector/state.py) declares `findings` with `Annotated[list[Finding], operator.add]` to enable automatic list appending.
- **Node returns**: Analyzer nodes return `AnalyzerNodeResponse` containing `{"findings": [...]}` rather than mutating state directly.
- **Automatic reduction**: LangGraph merges node outputs into state using the `operator.add` annotation, 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:

```python
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.