# CLAUDE.md

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

## Repository shape

Monorepo of independently-deployed AWS Lambda functions written in TypeScript (Node.js 24, target `>=24` per `engines`). Each lambda lives in `lambda/<name>/` and is self-contained: its own `package.json`, `yarn.lock`, `Dockerfile`, `Dockerfile.tests`, `docker-compose.yaml`, `biome.json`, `jest.config.json`, and `software-catalog.yaml`. There is **no root `package.json`** — yarn commands must be run from inside a lambda directory.

The Jenkins pipeline (`Jenkinsfile`) computes which lambdas changed in a PR (via `getModifiedFunctions` over `**/Dockerfile`) and runs the compliance/test/build/scan/deploy stages **only for those lambdas, in parallel**. Adding a new lambda means adding a new `lambda/<name>/` directory mirroring an existing one — the pipeline picks it up automatically.

Terraform for these lambdas (IAM, Lambda function resources, ECR repos, Step Functions wiring, etc.) lives in the **`terraform-infra`** repo, not here. Infra changes (new lambda registration, env vars, memory/timeout tweaks, Step Functions definition) go there.

## Per-lambda commands

All commands are run inside `lambda/<name>/`. Yarn only — never npm. Check `.nvmrc` (Node 24.12.0) before installing; if you bump versions, you may need `corepack enable`.

| Task | Command |
|---|---|
| Install deps | `yarn` |
| Full PR check (format + lint + unit tests) | `yarn test` |
| Unit tests only | `yarn test:unit` |
| Single test file | `yarn test:unit src/__tests__/<file>.test.ts` |
| Integration tests | `yarn test:integration` |
| Lint (autofix) | `yarn lint` |
| Format (autofix) | `yarn format` |
| Format check (CI parity) | `yarn format:check` |
| Build TS → `build/` | `yarn build` (runs `clean` first if needed) |
| Build Docker image | `yarn dev:docker:build` (needs `GITHUB_TOKEN` in env) |
| Run lambda locally in Docker | `yarn start` then POST to `http://localhost:9000/2015-03-31/functions/function/invocations` |
| Run lambda locally without Docker | `yarn dev:local:run` |
| Run lint+test the way CI does | `docker compose run --rm --build lint-and-test` |

`GITHUB_TOKEN` is required for any Docker build because dependencies install from the GitHub Packages monorepo registry — stage it in `.env` (gitignored) for local builds.

## Architecture

There are two distinct sets of lambdas in this repo. See [`docs/lambdas.md`](docs/lambdas.md) for the per-lambda input/output map.

### Step-function ingest

CSV uploads to S3 drive a Step Functions pipeline. Each lambda receives an `AWSStepFunctionContext` plus a `LambdaInput` and returns a `LambdaResult` that becomes the next lambda's input. Typical chain per entity (sound recording, product, participant, contributor):

```
S3 upload (CSV) → <entity>-deduplicator → <entity>-writer → … → finalizer
```

- **Deduplicators** (`sr-`, `product-`, `participant-`, `contributor-`) read the original CSV from S3, parse + dedupe, write deduped JSON back to S3, return `{ originalFile, dedupedFile }`.
- **Writers** consume the deduped file and persist to the ownership service via Apollo (GraphQL).
- `rules-writer` runs after `sr-writer` and creates ownership rules for the new sound recording.
- **finalizer** is the terminal step — emits a "finished" reporter message (or propagates `originalFileError` from a failed branch).

### AR sync (Art Relations CDC)

Independent MSK + SQS pipeline that propagates changes from the Art Relations database into the ownership service.

- **MSK filters** (`ar-release-filter`, `ar-release-artist-filter`, `ar-track-filter`, `ar-rights-filter`) consume CDC topics, enrich via GraphQL queries, and either write directly via GraphQL (artist filter) or forward to SQS.
- **SQS writers** (`ar-release-writer`, `ar-track-updater`, `ar-rules-writer`) consume SQS and persist via GraphQL. `ar-rules-writer` is the largest — owns `release-territory-restrictions`, `subaccount-royalty-collection(-territories)`, `track-master-rights`, `vendor-contract` write paths.
- `ar-release-writer` is unusual: its output is a CSV uploaded to the ingest pipeline's S3 path, bridging AR sync into the step-function ingest above.

### Shared building blocks

Almost every lambda follows the same skeleton (`src/app.ts`):

1. `getConfig()` from `src/config.ts` (env vars).
2. `Sentry.init({ dsn: config.sentryDsn })` **at module top**, before importing handler code that may throw on import.
3. `initLogger(...)` from `@theorchard/neighbouring-rights-common` with `{ environment, loggerLevel, serviceName, serviceVersion, context: { lambdaContext, stepFnContext } }`.
4. Construct `OwnershipIngestReporter` via `src/utils/reporter.ts` — used for `reportInfoMessage` / `reportErrorMessage` / `reportFatalError` / `reportFinished` to a Kafka topic.
5. `S3Connector` from `@theorchard/s3-utils` for all S3 I/O.
6. Wrap and export: `export const handler = Sentry.wrapHandler(eventHandler);`.
7. Handler errors: `reporter.reportFatalError`, `logger.error`, `Sentry.captureException`, then re-throw. Throw `DontRetryException` for non-retryable conditions (e.g. missing S3 metadata) — Step Functions treats these specially.

Key shared deps: `@theorchard/neighbouring-rights-common` (parsing, reporters, step-function types, `DontRetryException`), `@theorchard/datasource-kafka` (Kafka writes), `@theorchard/ows-logger` (logging), `@theorchard/s3-utils`, `datadog-lambda-js` + `dd-trace` (DD entrypoint is `datadog-lambda-js/dist/handler.handler` per Dockerfile — `DD_LAMBDA_HANDLER` env var points at the real handler).

### Dockerfile structure (all lambdas)

Three-stage build on `public.ecr.aws/lambda/nodejs:24`: `base` (deps via `setup.sh`), `build` (tsc), `runner` (final image). The runner layers in the Datadog and Sentry Lambda extensions and runs as non-root `lambdauser`. `setup.sh` consumes `GITHUB_TOKEN` via BuildKit secret, generates `.npmrc`, runs `yarn install --frozen-lockfile`, then deletes `.npmrc` — never bake the token into a layer.

## Conventions

- **Lint/format**: Biome (`biome.json` per lambda — keep them consistent when copying lambdas). Tab indent, single quotes. Tests/scripts/`__tests__` get relaxed rules (no-non-null-assertion off, etc.).
- **TypeScript**: explicit types; avoid implicit `any`. Keep handlers thin — push logic into pure modules under `src/`.
- **Tests**: `src/__tests__/` for unit, `tests/` for integration. Fixtures in `src/__fixtures__/` (excluded from Biome).
- **Vulnerability suppressions**: `VULNERABILITIES_TO_IGNORE` list lives in the `Jenkinsfile`. CVEs added there unblock the `dockerScan` stage on master — only add with a clear comment naming the package, and prefer fixing over suppressing when a patched version exists.
- **Resolutions vs dependencies**: pin transitive vulns via the `resolutions` field with `^x.y.z` (caret) unless a strict pin is needed for compat. Don't list a transitive in both `dependencies` and `resolutions`.

## Git workflow (from user-level CLAUDE.md)

- Never push to `master`. All changes via PR; manual approval required, no self-merge.
- Branch naming: `NR-####-short-description` for tickets, `MAINT-short-description` for maintenance, `NOTICKET-short-description` otherwise.
