# `MobDev.SecurityScan.StateFile`
[🔗](https://github.com/genericjam/mob_dev/blob/master/lib/mob_dev/security_scan/state_file.ex#L1)

Read/write the state sidecar for `mix mob.security_scan.log`.

The state file is a small JSON document that records the
last-known set of findings and when each was first seen. Diff
computation between runs depends on it.

Default location: `.security_scan/state.json` at the project root.
Should be **checked into git** so a fresh CI run knows what the
prior baseline was — without it, every scheduled run reports every
finding as 'new' and the changelog becomes useless.

## Schema

    {
      "version": 1,
      "last_run_at": "2026-05-07T05:30:00Z",
      "findings": [
        {
          "key": "EEF-CVE-2026-32689|phoenix|1.8.5",
          "id": "EEF-CVE-2026-32689",
          "severity": "high",
          "package": "phoenix",
          "version": "1.8.5",
          "fixed_in": "1.7.22",
          "title": "...",
          "url": "...",
          "source": "osv_scanner",
          "layer": "hex_deps",
          "first_seen_at": "2026-05-07T05:30:00Z"
        }
      ]
    }

# `entry`

```elixir
@type entry() :: %{
  key: key(),
  id: String.t() | nil,
  severity: atom(),
  package: String.t() | nil,
  version: String.t() | nil,
  fixed_in: String.t() | nil,
  title: String.t() | nil,
  url: String.t() | nil,
  source: atom() | nil,
  layer: atom() | nil,
  first_seen_at: DateTime.t() | String.t()
}
```

Finding-as-stored: same fields as Finding plus key + first_seen_at.

# `key`

```elixir
@type key() :: String.t()
```

Dedup key derived from id|package|version.

# `state`

```elixir
@type state() :: %{
  version: integer(),
  last_run_at: DateTime.t() | nil,
  findings: [entry()]
}
```

Loaded state map.

# `empty`

```elixir
@spec empty() :: state()
```

Empty initial state for first-time scans.

# `from_report`

```elixir
@spec from_report(
  MobDev.SecurityScan.Report.t(),
  MobDev.SecurityScan.Diff.t(),
  DateTime.t()
) :: state()
```

Build the next state from the current report and a `Diff` (which
carries `first_seen_at` for findings that already existed).

# `load`

```elixir
@spec load(Path.t()) :: state()
```

Load state from a JSON file. Returns `empty/0` if the file is missing.

# `save`

```elixir
@spec save(Path.t(), state()) :: :ok
```

Encode + write the given state to disk. Creates parent dirs as needed.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
