# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Purpose

`mr-fingerprint-filter` sits between a DynamoDB stream and the fingerprinter Step Function (SFN). It receives change events from the `masters_active` DynamoDB table, filters out noise (e.g., updates that only touch `lastModifiedBy`/`lastModifiedAt`), extracts the affected TUIDs, and starts one SFN execution per changed TUID — skipping duplicates and throttling if the SFN is already at capacity.

## Development Commands

Run from this directory:

```bash
# Run tests + linting
docker compose up --build lint-and-test

# With exit codes (CI-style)
docker compose up --exit-code-from lint-and-test --abort-on-container-exit --build lint-and-test

# Start lambda locally (port 9000)
docker compose up --build -d function

# Invoke with a sample event
curl -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" -d @tests/sample_events/dynamodb/masters_active/insert.json

# Or use the helper script against a running function
./tests/sample_events/call.sh tests/sample_events/dynamodb/masters_active/insert.json

# Skip linting
SKIP_LINT=true docker compose up --build lint-and-test

# Run a specific test
TEST_ARGS="-k test_handler" docker compose up --build lint-and-test

# HTML coverage report (served on port 8000)
COV_REPORT=html docker compose up --build lint-and-test
```

Linting runs `yamllint`, `mypy src/app.py tests/unit/`, and `flake8` (max line 120, single quotes, Google import order).

## Architecture

**Handler flow** (`src/app.py`):
1. `decode_records` — deserializes DynamoDB stream records; routes by ARN table name, normalizing it to a source string like `aws:dynamodb:masters_active`
2. `filter_events` — drops MODIFY events where only `lastModifiedBy`/`lastModifiedAt` changed (recursive deep comparison)
3. `generate_outputs` — dispatches to the correct `InputEvent` subclass by source string; calls `.output()` to produce `OutputEvent` objects
4. `throttle` — checks running SFN execution count; sleeps 30s in a loop until under `SFN_FINGERPRINTER_MAX_RUNNING` (default 500)
5. `execute_sfn` — starts one SFN execution per `OutputEvent`; silently skips `ExecutionAlreadyExists` errors

**Event model** (`src/events/`):
- `InputEvent` (abstract) — base class with `output()` contract
- `OutputEvent` — carries `data_type`, `data_id`, `action`, `unique_id`, `timestamp`; `.exc_id()` builds the SFN execution name; `.to_json()` formats the SFN input payload
- `MasterActive` (`src/events/dynamodb.py`) — the only concrete `InputEvent`; extracts territory→TUID mappings from before/after images using `DeepDiff` to find which TUIDs changed; emits one `OutputEvent(action='updated')` per changed TUID

**Source routing** — the source key (`aws:dynamodb:<table-short-name>`) is parsed from the event ARN in `decode_records`. Adding a new DynamoDB table means adding an entry to the `source_event_map` in `generate_outputs` and creating a new `InputEvent` subclass.

## Key Config

| Variable | Default | Purpose |
|---|---|---|
| `SFN_FINGERPRINTER_ARN` | (required) | ARN of the fingerprinter SFN |
| `SFN_FINGERPRINTER_MAX_RUNNING` | 500 | Throttle ceiling for concurrent SFN executions |
| `ENVIRONMENT` | `dev` | Controls secrets source (`LambdaSecretsManager`) |
| `SENTRY_DSN` | optional | Env var takes precedence over Secrets Manager |

## Test Helpers

`tests/conftest.py` provides two fixtures used across unit tests:
- `masters_active_event(operation, before, after)` — builds a deserialized `masters_active` event dict
- `dynamo_event(table_name, operation, keys, old_image, new_image)` — builds a full DynamoDB `Records` payload in raw stream format (pre-deserialization)
