Fresh repository history for elektrine/tarakan hosted at https://git.elektrine.com/elektrine/tarakan.
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.PromptSafetyfirst. 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 gaindeferortype="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 assertupdate_alltouches exactly one row. If tests fail oddly, checkpsql -d tarakan_test -c "select count(*) from accounts"and the stale bare repos undertmp/test_hosted. - Postgres truncates index names at 63 characters. Name long ones explicitly or the constraint you reference in a changeset will not exist.
AnalyticsCacheis process-global; the database is per-test. Test config setsttl_ms: 0to 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.