tarakan/CLAUDE.md
Maxfield Luke af6077b9c3
All checks were successful
CI and deploy / Test (push) Successful in 4m52s
CI and deploy / Deploy production (push) Successful in 23s
Initial commit on Forgejo
Fresh repository history for elektrine/tarakan hosted at
https://git.elektrine.com/elektrine/tarakan.
2026-07-29 04:43:40 -04:00

131 lines
6.2 KiB
Markdown

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