# Tarakan

A public security record. Contributors run their own AI agents locally against
public repositories and publish findings pinned to exact commits; other
contributors independently re-check them. Tarakan also hosts git (smart HTTP and
SSH), runs a credits/bounty economy, and tracks the same class of bug across
unrelated codebases.

`AGENTS.md` holds the Phoenix/Elixir framework conventions and is authoritative
for those. This file is only what is specific to *this* project — the domain
words, the invariants, and the traps.

## Commands

```
mix precommit        # compile --warnings-as-errors, deps.unlock --unused, format, test
mix test             # runs ecto.create + migrate first
mix assets.build     # two esbuild profiles, see "Assets" below
```

Run `mix precommit` before considering work finished.

## Domain vocabulary

These are coined nouns; they mean specific things and the UI capitalises them.

- **Report** (`Scan`) — one agent or human review, pinned to a commit SHA.
- **Finding** (`Finding`) — one issue inside a Report. Immutable.
- **Canonical finding** — deduplicated issue assembled from Findings that share
  a deterministic fingerprint (normalized path + line_start + title). Only
  exact fingerprints auto-link; no embeddings, no LLM merge.
- **Check** (`FindingCheck`) — an independent verdict on a canonical finding at
  a commit: `confirmed` / `disputed` / `fixed`.
- **Job** (`ReviewTask`) — a pickup ticket for agents. Optional; a Report does
  not need one.
- **Infestation** — the same finding *class* across repositories, keyed by
  `pattern_key` (normalized title only, no path or line).
- **Contract** (`Bounty`) — user-facing name for a bounty. The schema says
  bounty; the UI says contract.

## Invariants

Break these and the product stops meaning anything.

- **The record is public at creation.** Status is a label, moderation is a
  takedown. Never add a visibility gate that hides a finding pending review.
- **Everything is commit-pinned.** A finding without a SHA is not a finding.
  This is what makes regressions, archaeology and reproduction possible.
- **Verification is quorum-based, not authorial.** A submitter cannot verify
  their own finding. Agent checks corroborate; they do not create quorum.
- **Contributor text reaches other people's agents.** Finding titles, check
  notes, fix evidence and job descriptions are all attacker-reachable and are
  fed into prompts that run on someone else's machine with their subscription.
  Anything user-authored that enters an agent-facing payload goes through
  `Tarakan.PromptSafety` first. The client re-sanitizes independently
  (`internal/untrusted`) because it is the side that pays for an injection.
- **Money paths take row locks.** Every bounty/credit mutation reads its row
  `FOR UPDATE`. The credit ledger is append-only with a partial unique index
  that makes mints and refunds idempotent — rely on it rather than checking
  first.
- **Denial looks like absence.** An authenticated-but-unauthorized caller gets
  the same 404 as a missing record, so the API is not an existence oracle.
  Unauthenticated callers get a 401 challenge instead.

## Design language

Cyberpunk night-shift terminal: flat, high-contrast, monospace, phosphor on
black. Tokens live in `assets/css/app.css` (`--color-ink`, `--color-phosphor`,
`--color-signal`, …) — use them, never literal colours.

Two rules the user enforces strictly:

- **No invented copy.** Every element shows real, useful data. No taglines, no
  filler stats, no placeholder rows standing in for evidence that does not
  exist. If there is nothing yet, say so tersely ("None yet").
- **No explainer captions.** A caption under every control reads as vibecoded.
  Prose belongs in the sections built for it (`#how-it-works`, the policy
  pages); data sections carry labels and numbers.

Section headings are plain noun phrases ("Trending infestations", "Fixes
carried"), not sentences.

## Assets

`app.js` is a **deferred ES module** (`--splitting --format=esm`), so nothing in
it runs before first paint. Two consequences:

- Anything needed before paint goes in `assets/js/theme.js`, which has its own
  esbuild profile, ships as a classic IIFE, and is loaded **blocking** in
  `<head>`. It must not gain `defer` or `type="module"` — a test asserts this.
- Heavy dependencies use dynamic `import()` so they become separate chunks.
  three.js is 132 KB gzipped versus 43 KB for everything else; it loads only
  when the hero field mounts.

CSP is `script-src 'self'` — no inline scripts, no CDNs. Vendor into
`assets/vendor/`.

## Traps

Things that have actually bitten, in this repo.

- **Never run fixtures against the test database outside the sandbox.** A
  diagnostic script with `Sandbox.mode(:auto)` wrote real rows and broke tests
  that assert `update_all` touches exactly one row. If tests fail oddly, check
  `psql -d tarakan_test -c "select count(*) from accounts"` and the stale bare
  repos under `tmp/test_hosted`.
- **Postgres truncates index names at 63 characters.** Name long ones
  explicitly or the constraint you reference in a changeset will not exist.
- **`AnalyticsCache` is process-global; the database is per-test.** Test config
  sets `ttl_ms: 0` to bypass it. Anything cached there needs the same.
- **Foreign keys are not indexed automatically** and cascades scan the child
  table without one. Add an index with every `references(...)`.
- **Scan submission is N+1** — roughly 24 queries per additional finding in
  `FindingMemory.assimilate_scan/1`. Known, measured, not yet fixed. Do not
  make it worse.
- **Git SSH is disabled in production** (`GIT_SSH_ENABLED`). The host key lives
  on its own volume and is backed up; regenerating it looks like a
  man-in-the-middle to every existing client.

## Commits

Conventional Commits: `feat:`, `fix:`, `chore:`, `docs:`, `refactor:`, `test:`,
`perf:`, then a lowercase summary. One type per commit — if a change needs two
prefixes, it is two commits.

**No trailers.** Do not append `Co-Authored-By` or `Claude-Session`.

Merging to `main` deploys straight to production
(`.github/workflows/ci-deploy.yml` — no tags, no releases). A `feat:` touching
`/api` is a live API change; note in the body anything the separately-versioned
Go client (`~/tarakan-client`) must ship first.
