Skip to content

Contributing

Nabeel Wasif edited this page Sep 29, 2026 · 2 revisions

Contributing

The project is meant to be cloned, read and changed. Canonical source: CONTRIBUTING.md.


1. What we are building

The value you accepted is not in the file you downloaded.

A tool that draws a black rectangle over live text is not a redaction tool. It is an expensive screenshot with a lie attached, and people have leaked real information believing otherwise.

The invariants

A change that breaks one will not be merged, however good the rest of it is.

# Invariant
1 The source file is never mutated.
2 Only status === "accepted" reaches the exporter.
3 Redaction removes; it never covers.
4 Every export is verified before delivery.
5 A model's output is located, never trusted.
6 Nothing logs document content.
7 SHA-256 is not encryption.
8 Keys are random.

Found a way to violate one that the tests do not catch? That is a security issue and a welcome contribution. Report it privately first (Security Model §13), ideally with a failing test.


2. Design positions

No accounts, no auth. The repo is meant to run on your own machine against your own database and storage. The hosted demo is anonymous by design. For multi-user with real identities, fork it: ownership flows through one function (requireDocument), so swapping the identity source is a small change.

No telemetry that leaves the machine. Usage counters are rows in your own database.

No vendor lock-in that breaks local dev. Vercel Blob, Neon and the AI Gateway are the deployed defaults, not requirements. A clone runs against local Postgres, local RustFS, local Tesseract and a local model with no account anywhere. CI proves the first three on every pull request, and the model weekly. A code path that only works on Vercel is a bug. An OpenAI-compatible vendor is a row in lib/ai/providers/compatible.ts, not code.


3. Setup

pnpm setup                                          # writes .env, generates secrets, can start things for you
docker compose up -d postgres rustfs rustfs-init
pnpm db:migrate
pnpm dev

Or docker compose up -d for everything in containers. pnpm setup --local --defaults --yes answers everything with defaults. A model is optional, which is a reasonable way to develop the UI and the way CI runs.

If you touch pnpm setup: every default it prints comes from the code that enforces it (defaultsFor, DEFAULT_EML_LIMITS, DEFAULT_EXPANSION_LIMITS, …), never copied into the script. If you add a limit, export its defaults and its envName and the script picks it up. A setup script that lies about the defaults is worse than none.


4. Before you push

pnpm lint && pnpm typecheck && pnpm test && pnpm build

CI also runs Migrations, Database tests, OCR, the Compose stack (pnpm smoke for every format), and Changelog. See Testing and Benchmarks §5.

If you touched… Also run
OCR, image extraction, where image redactions land pnpm ocr:warm once, then pnpm test (runs the real-engine suites)
cleanup, quotas, rules, Hush tools, anything writing to the DB pnpm test:db against a throwaway TEST_DATABASE_URL
anything the container assembles (native deps, workflow runtime, storage, Dockerfile) docker compose up -d --build --wait && pnpm smoke && docker compose down -v
a parser or an exporter pnpm bench: a row suddenly ten times slower is the signal (quadratic range mapping and string edits do not show up in small unit fixtures)
the provider layer ANONIFY_OLLAMA_TESTS=1 with a pulled Qwen3-VL, or pnpm smoke --ai against a verified model

Adding a format means an entry in lib/documents/formats.ts (typed against DocumentKind, so a half-added kind does not compile), a case in scripts/smoke.ts CASES, and an adversarial case (a guard test requires one).


5. Where things live

Read the doc that covers your area first. Most decisions have a reason, and the reason is usually written down.

Area Doc Wiki
The four layers, identity, concurrency docs/architecture.md Architecture
The durable pipeline, batch export, expiry docs/workflow.md Processing Workflow
Failure classification docs/failure-codes.md Failure Codes
Detection order and cost docs/ai-engine.md Redaction Engine
Providers, verification, prices docs/ai-providers.md, docs/connect-openai.md AI Providers
Each format's pipeline, methods, vaults, mailbox rebuild docs/pipelines.md Supported Formats · Export and Anonymisation · Batches and Mailboxes
Drivers, keys, encryption docs/storage.md Storage and Encryption
Streaming readers docs/streaming.md Streaming IO
The HTTP surface docs/api.md API Reference
The schema docs/data-model.md Data Model
Components, search, rules, Hush docs/editor.md Review Workspace · Search and Rules · Hush Assistant
Store, undo, hooks docs/client-state.md Review Workspace §7
Tokens, report checks, presets, patterns, Hush docs/security-internals.md Security Model
Presets docs/presets.md Redaction Presets
Vercel docs/deploy-vercel.md Deployment
MinIO → RustFS docs/migrate-object-storage-to-rustfs.md Upgrading
Corpus and model benchmarks benchmarks/README.md Testing and Benchmarks
The visual language DESIGN.md

6. Style

  • Comments explain why, not what. Several comments in this codebase exist because a test caught a real bug and the next person deserves to know.
  • Match the surrounding code.
  • Keep processing logic out of React. Nothing in lib/ imports a component.
  • Prefer a simple implementation until a measurement says otherwise.

Tests

A change to redaction behaviour needs a test that reads the produced artifact, not one that asserts a function was called. Build a real file, redact it, then try to get the value back out (tests/security.test.ts is the pattern). Never commit real personal data.


7. Commits and pull requests

  • Explain why in the commit body. What changed is in the diff.
  • One concern per PR.
  • If you found a bug while doing something else, say so in the PR. That is the most valuable sentence in most changelogs.
  • Issue templates: bug report · feature request. Community: CODE_OF_CONDUCT.md.

8. Releases: labels move the version

.github/workflows/release.yml reads a merged PR's labels, moves the version in package.json, tags it and publishes a release. Nobody edits the version by hand.

Bump Labels
minor (1.9.0 → 1.10.0) enhancement, formats
patch (1.9.2 → 1.9.3) bug, performance, testing, ci, infrastructure, ux, accessibility, security, benchmark
nothing docs, documentation, and anything not listed
  • The highest bump wins. docs never vetoes, it only fails to raise.
  • security is a patch on purpose: a bypass fix is urgent, not incompatible.
  • No label produces a major. 2.0.0 is dispatched by hand by an admin.
  • main is protected, so the bump arrives as a release/vX.Y.Z PR that the job opens, merges and tags. If that merge is refused, the PR is left open, and the version is delayed rather than lost. Requirements: Actions may open pull requests must be allowed (org and repo), and the bump PR is opened by GITHUB_TOKEN, so it starts no workflows.
  • The table lives in scripts/release-labels.mjs, shared by release.yml and ci.yml.

The changelog: a fragment per pull request

A PR that moves the version must add a changelog fragment in the same PR. CI fails without one.

changelog.d/<issue-or-pull-request-number>.<section>.md

Sections: added, changed, deprecated, removed, fixed, security. One entry per file, no leading -, written for someone self-hosting Anonify who is deciding whether to pull: what changed in the output, what changed in configuration, what needs a migration. Not the refactors.

MBOX mailboxes are accepted as an upload, and open as a batch with one
document per message

Fragments are separate files so that concurrent PRs never conflict. The release workflow assembles them into CHANGELOG.md, publishes that section as the GitHub release body, and deletes them in the same commit. Never edit CHANGELOG.md or the package.json version by hand. A PR that needs no entry (docs, a pure refactor) should carry no version-moving label. More: changelog.d/README.md.


9. The standing ask

Run the benchmark. The harness, the corpus and the scoring exist, and the pattern baseline is measured. What is missing is a published model run: #153. See Testing and Benchmarks §6.


See also

Clone this wiki locally