Skip to content

Ablation table — integrity & credibility controls

This page lists, for each integrity or credibility control DOGFOOD actually ships, what it buys (the threat it addresses) and how the system would behave without it. It is a rigor artifact: every row maps to code in src/, and the "Evidence" column names the file, test, and command a reviewer can check independently. The behavior WITHOUT column is a reasoned consequence of removing the control — not the output of a measured ablation run — so read it as "what protection is lost," not as a benchmark. Any quantitative figure below is a committed diagnostic copied from docs/JUDGING.md; it is emitted by manage.py normalize_report and must be reconfirmed on a fresh run at freeze rather than trusted as static. One honest scope note governs the whole spine: it is tamper-evident (detect / evident), not tamper-proof, and — because the operator holds the database and the signing key — it does not bind the operator against themselves without an externally retained anchor (see docs/THREAT-MODEL.md §A8).

Control What it buys (threat addressed) System behavior WITHOUT it (the ablation) Evidence in repo (file · test · command)
Tamper-evident audit hash-chain Every audited write (ballot, submission, judge assignment, rubric re-weight, publish) appends an immutable, hash-chained event inside the same transaction as the business write, so a business row cannot exist without its audit row. A later insert, delete, reorder, or field edit within the stored rows becomes detectable at a specific seq. Best-effort DETECT, not prevention. No ordered, linked record of writes: a quietly changed or removed ballot, role grant, or re-weight is indistinguishable from legitimate history. No post-hoc reconciliation is possible; tamper-evidence collapses to trusting the live rows. src/audit/hashchain.py (verify_chain) · src/audit/service.py (record_event, atomic co-commit; killer rollback test) · src/audit/models.py (AuditHead singleton locked FOR UPDATE, AuditEvent UNIQUE(instance_id, seq)) · src/audit/tests.py · manage.py audit_verify
Ed25519-signed checkpoint + release bundle + offline verifier Lets an independent party verify, offline on a clean machine, that the chain recomputes and its head is signed by the pinned public key — and that the published ranking, its signed run, and the signed checkpoint are one consistent artifact under one signer. A checkpoint that left the operator's control before a disputed change is what extends detection past the operator boundary. The chain is only self-checkable on the operator's own host. A key-holding operator could rewrite every row and re-sign a self-consistent history (or equivocate). With no external anchor, operator tampering is detectable "only if the operator was careless" — tamper-evident, never tamper-proof. src/audit/receipts.py (sign_checkpoint/verify_checkpoint, tag dogfood.audit.checkpoint.v1) · src/audit/verify.py (python -m audit.verify <dir>) · src/normalize/release.py (python -m normalize.release <dir>) · manage.py release_bundle · manage.py audit_export
Append-only ballot versioning Each score write appends a write-once BallotRevision (UNIQUE(ballot, version), DB CHECK scores 1..5); a prior score survives as its own immutable row, so an overwrite cannot erase what came before. Ballot keeps only the latest score as a denormalized read-pointer. History is append-only, not merely evident in the chain. A re-score overwrites in place; the previous value is gone from the row and survives only as an audit-payload figure — history is evident but not independently reconstructable as ballot data. A silent overwrite of the current score is harder to contradict. src/judging/models.py (BallotRevision, uniq_ballot_version, ck_ballotrevision_scores_1_5) · src/judging/services.py (record_ballot appends a revision + audit event in one transaction.atomic) · src/judging/migrations/0002_ballot_versioning.py · src/judging/tests.py
Signed, reproducible normalization run Freezes and signs a published ranking over the exact inputs it consumed — weighted ballots in pinned order (each pinned to its BallotRevision), rubric weights, and the pinned λ — then an independent party re-runs the same estimator code path from those pinned inputs and confirms the ranking canonicalizes to the signed result_hash. Proves the published order is exactly what this engine version produces from those ballots. A ranking is only ever a live recompute. There is no way to prove, after the fact, which inputs produced a published order or that a later recompute still matches it; reproducibility of an official result rests on trust. src/normalize/runs.py (publish_run/export_bundle) · src/normalize/signing.py (sign_run/verify_run, tag dogfood.normalize.run.v1) · src/normalize/verify.py (python -m normalize.verify <dir>, re-runs engine.compute_leaderboard) · manage.py normalize_publish · tests/test_normalize_signing.py
Review-diagnostics — a diagnostic, not fraud detection Surfaces where a ranking is thin or fragile so the organizer can judge it: leave-one-ballot-out residuals, single-ballot and single-judge decision influence, coverage / articulation judges, and the raw judge spread (stdev of per-judge mean composite) as an upper bound on removable severity — σ = 0.420 (committed figure — reconfirm with a fresh normalize_report at freeze). Explicitly a read for human judgment, never an accusation. The organizer sees only the point ranking, with no view of which results hinge on a single ballot or judge, how much severity spread exists, or where coverage is thin — fragile results look exactly as solid as robust ones. src/normalize/diagnostics.py · src/normalize/services.py (proof_report → raw_judge_spread; diagnostics_report) · manage.py review_diagnostics (help: "NOT fraud detection") · src/normalize/tests.py (test_raw_judge_spread_is_stdev_of_per_judge_means)
No-signal permutation test + per-rank standard error (display-only) Answers the one question a point ranking cannot ask of itself: is the fitted quality spread consensus, or could judge severity + noise alone produce it? A within-judge permutation builds the null and returns a one-sided Monte-Carlo p-value against a pre-registered α = 0.05; a per-submission q_se gives each quality point its own ±. Built to withhold a verdict on a low-signal panel rather than manufacture one. The leaderboard presents a point order with no honest signal of whether the data supports it; a low-signal panel reads as a confident ranking. Both are display-only — never fed back into q or the signed ranking (compute_leaderboard never calls signal_test; canonical_result carries neither), so this control adds credibility without ever moving a published result_hash. src/normalize/engine.py (signal_test, SIGNAL_ALPHA = 0.05; q_se in rank_report; compute_leaderboard/canonical_result omit both) · src/normalize/services.py (proof_report → signal_p / q_se_median / q_se_max) · tests/test_normalize_engine.py
Rubric-weight non-negativity CheckConstraint A negative criterion weight would silently invert the weighted 0..5 composite every ranking rests on. It is bounded non-negative three ways, deepest last — the set_rubric_weights service, the admin form's clean_weight, and a DB CHECK(weight >= 0) — so a value below zero cannot be persisted even by a writer that bypasses the service. An operator could set a weight negative straight through the admin, off the service path, distorting every downstream score with no error and no guard beneath the application. src/judging/models.py (RubricWeight ck_rubric_weight_nonneg) · src/judging/migrations/0003_rubric_weight_nonneg.py · src/judging/services.py (set_rubric_weights rejects negative / NaN / inf / zero-sum)
Rate-limit / login throttle Best-effort throttling of the real write and login surfaces — login per client IP; submission_write / ballot_write / comment_write / invite_redeem per authenticated user; vote_write per campaign + voter; webhook_write and assignment_write per event + user — over one shared-cache fixed-window counter, returning 429 + Retry-After. Counts correctly across all workers (not once per process), slowing credential stuffing and write floods. Login and write endpoints are un-throttled: credential stuffing and write floods run at full speed. It is a throttle, not a lockout, and it fails open by design — a limiter outage must never become an availability outage; it is abuse control, not an authorization boundary (the event-scoped grants are the real gate and do not fail open). src/portal/ratelimit.py (parse_rate/hit, fail-open fixed window) · src/portal/settings.py DOGFOOD_RATE_LIMITS — 7 keys: login 10/m, invite_redeem 20/h, submission_write 60/h, ballot_write 120/h, vote_write 20/m, comment_write 60/h, webhook_write 60/h (each env-overridable). An eighth policy name, assignment_write, is read at its call site as .get("assignment_write", "60/h"), so it is enforced from an in-code default and is the one limit not env-tunable · ratelimit.hit( in 7 files / 9 call sites: accounts/views.py (login), submissions/views.py (×2, submission_write), judging/views.py (×2, ballot_write + assignment_write), events/views.py (invite_redeem), comments/views.py (comment_write), voting/views.py (vote_write), webhooks/views.py (webhook_write) · tests/test_ratelimit.py · src/accounts/tests.py · src/voting/tests.py (RateLimitTests)
Finalization governance (live-vs-finalized; results frozen, not recomputed) Splits the organizer's live, recomputing leaderboard (results-in-waiting, organizer-gated 401/403/200) from the public official result, which is the frozen result of a signed run served verbatim — never a live recompute — and published append-only as versioned, audited ResultPublication rows. So a later re-weight or new ballot cannot silently mutate an already-published result, and the public ranking cross-checks against the offline bundle. Every read would be a live recompute; a published "official" ranking could drift as ballots or weights change, with no frozen, versioned record of what was published, when, and by whom. "Official" would be mutable and unauditable. src/normalize/results.py (current_results serves frozen run.result verbatim; publish_results atomic append-only + results.published audit event) · src/normalize/views.py (public results/results.json vs organizer-gated live leaderboard; results_publish console) · src/normalize/tests.py (ResultPublicationTests)
Explain-my-rank derived from the frozen signed result A team's own rank explanation is built by reading one row out of the published run's frozen result — never a live recompute — so what a team is told and what the offline verifier re-derives are the same artifact and cannot drift apart. It reports that row's rank, q, raw mean, Δ, ballot count, bootstrap rank interval (seed disclosed, and worded as a range rather than a promise), a severity-direction band that explicitly does not accuse any judge, and a cross-component caveat when the graph splits. Access is authenticated and scoped to the owning team or an event organizer; every other case is a uniform 404 that leaks no existence, and before publication it is 404 even for the owner. One honest boundary: the adjacent-pair win percentage win_next is stored in the frozen result but is deliberately stripped from the canonical projection before hashing, so it is frozen and display-only rather than covered by result_hash. A team asking "why did I place here?" gets either nothing or a fresh recompute that can disagree with the published, signed ranking — the explanation becomes a second, unsigned source of truth, and the most-read number in the event is the one nobody can verify. src/normalize/explain.py (find_row, explain_row; pure, reads a single signed row) · src/normalize/views.py (explain_rank → results_svc.current_results, owner-or-organizer, uniform 404, private, no-store) · src/normalize/results.py (current_results returns run.result verbatim) · src/normalize/engine.py (_UNSIGNED_ROW_FIELDS = ("win_next",)) · tests/test_explain.py (test_figures_echo_the_signed_row_unchanged, test_limitation_is_honest_about_scope) · src/normalize/tests.py (ExplainRankViewTests, test_win_next_is_display_only_and_off_the_signed_projection)
Verifiable results certificate — a portable, self-verifying attestation of a published result A self-contained certificate over an event's official published results that a third party can re-check offline without ever touching the deployment. It copies the signed run's result_hash, signature, signer fingerprint, public key, and ranking verbatim — no new key, no second signature over the ranking, no recompute — and before it issues anything it self-verifies twice: it re-runs the same offline bundle verifier a judge runs (verify.verify_bundle over a freshly exported bundle, which re-runs the estimator from the pinned ballots and checks the signature + hashes) and re-checks the certificate envelope's own Ed25519 signature, refusing to emit (CommandError / API 404) if either fails or the run is unpublished/unsigned. So a certificate is only ever produced for a run that actually reproduces and is validly signed, and the API serves it read-only through an explicit output-only allowlist carrying no ballots, per-judge scores, judge identity, or PII. Its scope is exactly the signed run's (docs/THREAT-MODEL.md §A6/§A11): decisive against the operator only if the public key + fingerprint were pinned by an independent party before judging. "I placed here / I judged this event, here is the official result" is an unverifiable claim the recipient must take on trust: the signed run exists, but there is no single portable artifact that restates it, proves it still reproduces, and can be handed to someone who never touched the deployment. Re-checking a published result would mean re-exporting a bundle and running the verifier by hand, so the event's most-shared number — the final ranking — becomes the one nobody re-verifies. src/normalize/certificate.py (CERTIFICATE_KIND = "dogfood.results-certificate.v1", build_certificate / certificate_for_event, verify_certificate_signature, render/render_html/render_text; copies the run verbatim — no new crypto over the ranking) · src/normalize/management/commands/certificate.py (self-verifies via normalize.verify.verify_bundle and re-checks the envelope signature before emitting; refuses unpublished; operator/ad-hoc, off the checker routes) · src/api/views.py (EventCertificateView, GET-only) · src/api/urls.py (events/<ext_id>/certificate/) · src/api/serializers.py (ResultsCertificateSerializer, output-only allowlist) · tests/test_certificate.py (test_signature_block_verifies_and_tamper_fails, test_refuses_unpublished_or_unsigned, test_html_and_text_are_offline_and_pii_free) · src/api/test_certificate.py (CertificateApiTests — test_certificate_result_hash_equals_signed_publication, test_certificate_has_no_pii, test_unpublished_event_has_no_certificate_404) · manage.py certificate --event <id> [--format json\|html\|txt]
Awards podium derived from the signed result (recognition is not ranking) The public podium (GET /events/<event_ext_id>/awards) is a projection over the frozen, signed run's rows — top-N, optionally scoped inside one track (filtered before truncation, then re-numbered) — never a live recompute, and it stays neutral with no ranking at all until results are published. Awards are deliberately a recognition layer off the integrity spine: a Prize is organizer configuration, is not part of the signed run, and assigning a winner never changes the judged, signed result — it only changes which name that prize line shows, flagged winner_source: "assigned" vs "derived". Shown separately and labelled as such, the organizer-only review top-up planner (GET /events/<event_ext_id>/awards/topup) reads the live, unsigned standings and flags, per podium prize, the contenders at or straddling that prize's rank cutoff, with a reason each: fewer recorded reviews than the coverage target (min_ballots=3), a bootstrap rank interval that spans the cutoff, or a q-gap across the cutoff below gap=0.25. It is a pre-finalization review-planning aid — it ranks nothing, assigns no score, is not the final ranking, and is not fraud detection. Its honest limitation, stated in its own docstring: it reads live unsigned data, so its output moves as reviews arrive and it is not reproducible from a signed artifact. The podium becomes its own recompute — free to disagree with the published signed ranking, and free to change between two page loads — and a prize assignment reads as an edit to the judged result rather than a layer on top of it. Without the top-up planner, an organizer finalizes with no view of which prize cutoffs are still thin or straddled, so the places where a few more reviews would matter most look identical to the settled ones. src/awards/podium.py (compute_podium, place_at; pure stdlib, input never mutated) · src/awards/services.py (published_result → normalize.results.current_results; public_awards returns {"published": False} pre-publication; prize_lines; review_topup reads normalize_services.leaderboard) · src/awards/topup.py (review_topup_plan) · src/awards/models.py (Prize — "deliberately NOT part of the signed, reproducible normalization run") · src/awards/views.py (podium public @require_GET; topup behind _organizer_or_response, Cache-Control: private, no-store) · tests/test_podium.py (test_track_filter_before_truncation_then_renumber, test_input_is_not_mutated) · tests/test_topup.py (test_flags_at_third_place_cutoff, test_deterministic_and_input_not_mutated) · src/awards/tests.py (test_public_awards_derives_from_frozen_result, test_public_podium_page_hidden_until_published, test_assigned_winner_overrides_derivation, AwardsTopupViewTests)
Judge recusal / conflict-of-interest (planner-assignability only) A JudgeRecusal(judge, team, reason) row — unique on (judge, team) at the DB level — removes every one of that team's submitted projects from that judge's assignable set before the auto-assignment planner runs: the pure expand_recusals fans the team-level declaration out to submission-level pairs, and _pair_eligible refuses them alongside the standing rule that a judge is never planned onto their own team's project. The declared reason is stored with the row, so the why lives in the system rather than in someone's memory. The honest scope is narrow and worth stating plainly: this changes only who MAY be planned. It does not block a ballot write, does not touch existing JudgeAssignment rows or any recorded ballot (append-only history is untouched), does not gate the manual assign view, is filed through Django admin by a staff user rather than an organizer-facing route, and the recusal itself is not written to the tamper-evident audit chain. The planner is free to hand a judge a project they have declared a conflict on, so conflicted pairings have to be spotted by eye and undone by hand after the fact — and the declaration exists nowhere the system can act on, so the same conflict recurs on the next assignment pass. src/judging/models.py (JudgeRecusal, uniq_judge_recusal; "changes only who MAY be planned, never any recorded ballot") · src/judging/migrations/0004_judge_recusal.py · src/judging/recusal.py (expand_recusals, pure, zero Django) · src/judging/services.py (_assignment_inputs builds the per-judge recused set) · src/judging/assignment.py (_pair_eligible) · src/judging/admin.py (JudgeRecusalAdmin) · tests/test_recusal.py (test_recused_team_projects_never_go_to_that_judge, test_recused_judge_still_reviews_other_teams) · src/judging/tests.py (AutoAssignRecusalTests.test_planner_honors_db_recusals_both_ways, test_duplicate_recusal_is_refused_by_db)
Duplicate-title diagnostic — display-only, ballot-independent, not fraud detection Groups submissions that share a track and a whitespace-collapsed, casefolded title, and shows the clusters on the organizer diagnostics page so a human can look. It is built to make the weakest claim that is still useful: it reads nothing but submission rows, is independent of every ballot (submissions cluster before any judging happens), changes no state, and is never fed into the leaderboard, the signed result, or any hash — so it cannot move a result_hash. Sharing a title is a prompt to look, not a verdict: two teams can independently pick the same name, and one team may legitimately supersede its own entry. Same title in a different track is not flagged; singletons are dropped; cluster order is deterministic. An accidental double-submission, or a re-submission that should have superseded its predecessor, sits in the field unnoticed — scoring twice and splitting its own ballots between two rows — and nothing tells the organizer there is anything to look at. src/normalize/duplicates.py (normalize_title, find_duplicate_clusters; pure, zero Django, "NOT a ranking input, a fraud score, or a gate") · src/normalize/services.py (duplicate_clusters, read-only over SUBMITTED rows) · src/normalize/views.py (diagnostics injects duplicate_clusters; organizer-gated 401/403 — HTML panel only, deliberately not in diagnostics.json) · tests/test_duplicates.py (test_same_track_same_title_case_and_whitespace_is_one_cluster, test_same_title_different_tracks_not_flagged, test_singletons_are_ignored) · src/normalize/tests.py (DuplicateSubmissionsDiagnosticTests)
Ed25519-signed participation records Signs a judge's or participant's record over a frozen field set (RECORD_FIELDS = kind, role, event ext_id, event name, subject, items, issued_at, attestation) under its own domain-separation tag, so a holder can hand the JSON to a third party who fetches the public key from a well-known route — fingerprint returned in X-Signing-Key-Fingerprint — and confirm that not one field was edited after issue. Records are computed on the fly and carry participation only: no ballot, no per-judge score (the tests assert those tokens are absent). Each is fetchable only by its own subject: 401 anonymous, 403 for the wrong role, and 403 for a judge requesting another judge's record. Scope note, and it is the record's own signed attestation text: because the operator holds the signing key, a record is decisive to a third party only if they pinned the public key and its fingerprint before judging. There is no offline record-verifier CLI — verification is the pure verify_record function plus the hosted /records/verify endpoint, which checks against this deployment's key. A "certificate" is unverifiable text the recipient must take on trust; any field — event, role, project list, date — can be edited after issue with nothing to detect it, and the claim "I judged this event" has to be re-confirmed by hand by the issuer every time anyone asks. src/records/signing.py (record_material/sign_record/verify_record, _RECORD_TAG = b"dogfood.record.v1", RECORD_FIELDS; key helpers re-exported from audit.receipts — no new crypto) · src/records/services.py (judge_record, participant_record, verify_record_json, RECORD_KIND = "dogfood.participation-record.v1") · src/records/views.py + src/portal/urls.py (/records/signing-key and /.well-known/dogfood-signing-key, public PEM) · src/records/test_signing.py (test_frozen_tag_and_fields, test_tamper_any_field_fails, test_wrong_key_fails) · src/records/tests.py (RecordEndpointTests — test_judge_record_ok_and_verifies, test_judge_record_cross_judge_403, test_verify_doctored_record_is_false)
Outbound webhook signing + SSRF guard Two distinct controls on one surface. Signing: every delivery carries X-Dogfood-Signature: sha256=<hmac> computed over "<unix-seconds>." + canonical JSON body with the per-endpoint secret, and the exact bytes signed are the exact bytes sent and stored — so a receiver can reject a forged body, and a captured body cannot be re-headered with a fresh timestamp. SSRF: the URL is validated at registration and again immediately before each connect — http/https only, no embedded credentials, and every resolved address (A and AAAA, with IPv4-mapped IPv6 unwrapped) checked against loopback / private / link-local / reserved / multicast / unspecified plus explicit cloud-metadata addresses; a resolution failure is fail-closed, and redirects are refused at the HTTP layer rather than followed. Honest limits, all of them: delivery is best-effort and synchronous (no queue, no worker, no automatic retry — the models say "never assured and never at-least-once"); the validated IP is not pinned to the socket, so a small resolve-then-connect window remains; there is no port restriction; freshness enforcement is the receiver's job, as the sender publishes no tolerance window and ships no receiver-side reference verifier; the per-endpoint secret is stored in plaintext and only masked in the UI; and only endpoint registration is rate-limited, not delivery or retry. A registered URL becomes an SSRF primitive: the server would fetch cloud-metadata or RFC1918 addresses on the operator's behalf, turning an organizer-facing text field into an internal-network read. And an unsigned POST is indistinguishable from one sent by anyone who learned the endpoint, so a receiver cannot tell a real delivery from a forgery or a replay. src/webhooks/ssrf.py (validate_url, is_blocked_ip, _ALLOWED_SCHEMES, _METADATA_IPS, _blocked_flags) · src/webhooks/services.py (sign_body, _canonical_body, new_secret; _http_post re-validates before connect and installs _NoRedirect so a 3xx is refused, not followed; DELIVERY_TIMEOUT = 5) · src/webhooks/models.py (WebhookDelivery records attempts/status/response_code/signature/payload; audit events webhook.delivered / webhook.delivery_failed) · src/webhooks/test_ssrf.py (test_blocked_ip_literal_urls_raise_without_dns, test_name_resolving_to_internal_is_blocked, test_name_with_any_internal_address_is_blocked, test_scheme_credential_and_schemeless_rejected) · src/webhooks/tests.py (test_register_ssrf_blocked_url_422_no_row, test_deliver_success_records_signature_and_audit — recomputes the HMAC and asserts equality)
Ed25519-signed event bundle export / import Exports an event's structural graph — event, tracks, teams, team members, memberships, submissions, rubric weights, the voting-campaign window — as one signed document whose signed pre-image is the whole bundle dict canonicalized, so a single edited byte fails verification rather than only the fields someone remembered to cover. Import verifies the signature before opening a transaction and refuses an edited document outright (400), refuses to overwrite an existing event (409), and reconstructs the graph plus its bundle.imported audit event inside one transaction.atomic, so a rejected row rolls the whole import back. results_published is forced back to False on import because a bundle carries no signed publication to back it, and unknown user emails are reported as skipped_users rather than minted. Site-administrator only (401 anonymous / 403 even for an event organizer). Honest scope: the bundle carries no ballots, no scores, and no audit chain, it does contain member emails and display names by design (it is an operator backup of the operator's own data), and it is signed by this deployment's key, so it re-imports only where that key is the deployment key. An event migration or backup is an unauthenticated blob: a row can be added, retitled, or dropped between export and import with nothing to detect it, an import can half-apply and leave a partially reconstructed event behind, and an imported event could arrive claiming its results were already published. src/bundles/signing.py (bundle_material/sign_bundle/verify_bundle_sig, _BUNDLE_TAG = b"dogfood.bundle.v1"; "NOT proof of results integrity and NOT a measure of merit") · src/bundles/services.py (build_bundle, sign_export, verify_signed_bundle, import_bundle, BundleInvalid/BundleConflict) · src/bundles/views.py (_site_admin_or_response) · src/bundles/test_signing.py (test_frozen_tag, test_tamper_any_field_fails, test_verify_signed_bundle_and_wrong_fingerprint) · src/bundles/tests.py (test_export_superuser_ok_and_verifies — asserts ballots/scores are absent; test_import_tampered_bundle_400, test_import_existing_ext_id_conflict_409, test_round_trip_import_reconstructs_counts)
Community-voting integrity (eligibility list · confirmed-email token · one-allocation accounting) Three layers so a public vote is a vote rather than a click-count. (1) Eligibility: in email_gated mode the organizer's EligibleVoter allow-list is matched on a normalized email (trim/lowercase, +tag stripped, Gmail dots folded), and a non-listed address is refused at confirm time with 403 before any Voter row exists. (2) Confirmed email: a 128-bit uuid4 token is minted and written to an outbound-email record addressed to that address (this build persists the message rather than sending SMTP), and only redeeming that link seats a voter identity in the session; a second attempt at an address that already has an identity is refused 409 and audited vote.duplicate_refused. (3) One allocation: enforced in the database by uniq_allocation_voter_submission, with one identity per campaign held by the partial uniques uniq_voter_campaign_user / uniq_voter_campaign_email; a recast replaces the prior ballot inside one transaction.atomic that also writes the vote.cast audit event, and the quadratic credit budget is checked before any write (an over-budget ballot writes nothing). And the load-bearing separation: community votes cannot move the judged ranking — src/normalize/ contains no reference to the voting app at all, so a popularity campaign can never touch q or a result_hash. Honest limits: vote tokens are stored raw and never expire, confirming is idempotent rather than strictly single-use, there is no ballot secrecy from the operator (the vote.cast payload records the allocation map), and email_link mode accepts any address, so disposable-mail domains defeat it — only email_gated restricts to a curated list. The public vote degenerates into a click-counter: one person votes as many identities, a never-invited address counts the same as an invited one, a recast double-counts instead of replacing, an over-budget ballot lands partially written, and — worst — a popularity number sits next to the judged result with nothing structural keeping them apart. src/voting/models.py (EligibleVoter, Voter, VoteToken, VoteAllocation; uniq_allocation_voter_submission, uniq_voter_campaign_user, uniq_voter_campaign_email, uniq_eligible_campaign_email) · src/voting/migrations/0001_initial.py (same constraint names as DDL) · src/voting/services.py (normalize_email, request_vote_token, confirm_token → NotEligible/DuplicateVoter, cast_ballot atomic replace, within_budget) · src/voting/test_pure.py (EmailNormalizationTests, QuadraticBudgetTests) · src/voting/tests.py (test_gated_ineligible_confirm_403, test_duplicate_email_refused_409_and_audited, test_recast_replaces_prior_ballot, test_over_budget_409_writes_nothing) · command: grep -rin voting src/normalize/ returns nothing
Personal API tokens stored as a sha256 hash only A minted Bearer token is 256 bits of secrets.token_urlsafe(32) behind a dgf_ marker, shown once at mint and never persisted: the row stores only sha256(raw) in a unique column, plus a non-secret 12-character display prefix and the usual timestamps. Authentication is an indexed equality probe on the digest, so the credential never has to exist at rest for lookup to work. Revocation is a soft revoked_at stamp scoped to the owning user (so one user can never revoke another's token) and takes effect on the very next request. The consequence that matters: a stolen database dump yields no usable credential — recovering a raw token means brute-forcing CSPRNG entropy, not cracking a password. Blast radius is deliberately tiny: the token authenticates exactly one endpoint, GET /api/v1/me/, which is read-only and built from filter(user=request.user), and a Bearer header's absence leaves every other /api/v1/ route byte-identical to anonymous. Honest limits: sha256 is not a slow KDF — defensible here only because the pre-image is machine-generated high-entropy output with no user-chosen component, so there is no dictionary to run; the match is a SQL index probe rather than a constant-time compare; tokens carry no scopes and no expiry; TLS is assumed; and token use stamps last_used_at rather than writing to the audit chain. The raw credential lives in the database, so a DB dump, a stray backup, or a logged row hands over every user's live API credential verbatim — and revoking afterwards does nothing about the copy already taken. src/apitokens/tokens.py (generate_token, hash_token, TOKEN_PREFIX, token_display_prefix) · src/apitokens/services.py (create_token persists only token_hash=hash_token(raw) and returns the raw in-process; resolve_token, revoke_token) · src/apitokens/models.py (token_hash = models.CharField(max_length=64, unique=True) — "raw NEVER stored") · src/apitokens/authentication.py (BearerTokenAuthentication; no header → None, so public routes stay anonymous) · tests/test_apitokens.py (test_hash_token_matches_direct_sha256, test_generate_token_is_distinct_each_call) · src/apitokens/tests.py (test_create_resolve_revoke_lifecycle — asserts ApiToken.objects.filter(token_hash=raw) is empty) · src/api/tests.py (test_revoked_token_is_401, test_valid_bearer_does_not_change_public_endpoint)

How to reproduce the figures

Every number the write-up quotes is emitted by a management command, so the docs cannot drift from the code. Bring the stack up first, then run these from src/ (they are operator/organizer tools and touch none of the acceptance-checker routes). Reconfirm any committed figure on a fresh run at freeze.

python manage.py normalize_report            # components, gauge, λ sweep, raw_judge_spread, within-σ reduction (add --json)
python manage.py review_diagnostics           # residuals, decision influence, coverage — NOT fraud detection (add --json)
python manage.py audit_verify                 # recompute the in-DB hash chain and confirm the head matches its tip
python manage.py normalize_publish --export /tmp/nbundle   # sign a reproducible run, then self-verify the bundle
python -m normalize.verify /tmp/nbundle                    # re-run the estimator from pinned inputs, offline
python manage.py release_bundle /tmp/release  # ONE signed bundle: ranking + signed run + signed audit checkpoint
python -m normalize.release /tmp/release      # verify the whole chain of custody, offline

A PASS from the offline verifiers proves internal consistency and reproduction from the pinned inputs; per the scope note above and docs/THREAT-MODEL.md §A8, it is decisive against the operator only if an independent party pinned public-key.pem and its fingerprint before judging and retained their own copy of the checkpoint.