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

6.2 KiB

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.