Skip to content

refactor: finalize the event data model#126

Merged
vidit-admin merged 7 commits into
mainfrom
claude/elastic-almeida-14f124
Jul 4, 2026
Merged

refactor: finalize the event data model#126
vidit-admin merged 7 commits into
mainfrom
claude/elastic-almeida-14f124

Conversation

@vidit-admin

Copy link
Copy Markdown
Member

Finalizes the event data model that the earlier bounty + geolocation merge started, so bounties, geolocations, and detections are one events lifecycle end to end (requested → detected → geolocated → closed, one verb per transition).

Model

  • author_id splits into owner_id (edit rights, moves to the fulfiller at geolocate) plus a durable event_geolocators credit table.
  • event_claims becomes event_investigators; the claim action is now POST / DELETE /events/{id}/investigate.
  • location becomes event_coords, plus a new optional capture_source_coords camera point, per-state stamps (requested_at, detected_at, geolocated_at), close_reason, and before_closed_status (the requested vs detected discriminator that keeps close unified without losing the distinction).
  • proof_images folds into media(role = source | proof). Proof images upload at publish via multipart proof_files[] with a placeholder:// rewrite, so nothing sits unattached on S3 and there is no orphan reaper.
  • Stored IP and user agent are dropped from media and auth_events.
  • Four events CHECK constraints: the status domain, coordinates when geolocated, the two terminal stamps, and before_closed_status as a full iff.
  • The /bounties router is removed; bounty, geolocation, and detection are filtered views over /events.

Frontend

Regenerated api-types, repointed the data layer and routes, and added the rich flows: the proof-at-publish editor, a close dialog with a reason, the investigate toggle, capture_source_coords inputs, and single-source enforcement. Every element composes from existing primitives (no new one-off).

Migration

h0j2l4n6p8r0 is a clean redefinition plus reseed (the dev DB is wipeable), reversible, with backfills that run before the new CHECKs so an existing dev DB upgrades cleanly.

Fixes surfaced by an adversarial review

A fresh-context review pass caught four defects that the test suites and a manual smoke test all missed, now closed here:

  • Blocker: the create and request forms posted the source under the wrong multipart key (files vs the backend's file), so both flows 422'd from the UI. No test crossed the real HTTP boundary.
  • The geolocate row lock reused a stale identity-mapped row after with_for_update() (fixed with populate_existing()), so a concurrent geolocate could double-fulfil.
  • The before_closed_status CHECK let a NULL slip through on a closed row (NULL IN (...) is unknown, not false), fixed with an explicit IS NOT NULL.
  • The possible-duplicates probe leaked requested guesses and closed rows once the coords CHECK relaxed, fixed with a status predicate.

Verification

Backend 688 tests, frontend 154 tests, ruff / mypy / tsc / lint / build clean, make hygiene clean (jscpd, vulture, knip, palette). The dev DB was upgraded and reseeded, and the read paths plus the submit form were verified live.

🤖 Generated with Claude Code

vidit-admin and others added 7 commits July 3, 2026 15:28
Closes the last internal `geolocation` names the merge + entity rename
left behind. Behaviour-preserving pure rename (37 insertions, 37 deletions):

- ORM relationship attributes become event(s): User.events, Media.event,
  Tag.events, EventClaim.event, and the matching Event back_populates.
  Internal Python only, no query traverses them by the old name.
- The max_files_per_geolocation setting becomes max_files_per_event.
- The last deferred column proof_images.geolocation_id becomes event_id
  (migration g9i1k3m5o7q9: column + FK constraint + index; the partial
  orphan index predicate follows the rename automatically).

The deliberately-kept external surface is untouched: the /bounties view,
the status values, the "geolocation_id" delete-receipt key, and the
/{geolocation_id} path-param name. data-model.md already documented
event_id, so this only lands the code side.

ruff / ruff format / mypy (82) / pytest (667) green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: vidit-admin <admin@vidit.app>
- design.md: rewrite around the selectable accent (orange = default, not THE
  colour); drop the duplicate colour lookup and the two ~500-word Forms /
  Field-help walls (-43% words); fix stale component refs
  (GeolocationCard/DetailBody -> EntityCard/EventDetailBody, TweetImportBanner
  path).
- engineering.md: fix stale frontend refs (GeolocationStatus -> EventStatus,
  components/geolocation/ -> event/ + landing/, events/ sub-router list); trim
  the CI / Dependabot / DCO justification prose.
- api.md: trim CHANGELOG-style narration on points, possible-duplicates,
  tweet-extract, media origin, and archive import (contract preserved).
- data-model.md: drop a v0.3.1 version milestone (AGENTS.md rule 2) plus
  assorted redundant prose.
- next.md: fold the landed Architecture / Sequencing essays into CHANGELOG
  pointers; fix stale `submitted` -> `geolocated` status literals and em dashes.
- roadmap.md: same `submitted` -> `geolocated` status fix.

No behaviour change; documentation only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: vidit-admin <admin@vidit.app>
Give the backend a dead-code gate analogous to the frontend's knip.
vulture finds unused functions, classes, methods, and fields that
ruff's F401 (unused imports only) misses.

Config under [tool.vulture] in backend/pyproject.toml scans app +
scripts, excludes alembic/versions, and runs at min_confidence 60 so a
genuinely orphaned module-level function (which scores 60) trips the
gate; the default 80 would only ever catch unused locals. The framework
noise a 60 floor surfaces is cleared two ways: ignore_decorators (route
and middleware handlers, Pydantic validators) plus ignore_names
(model_config, the validator cls) for the blanket classes, and
backend/vulture_whitelist.py for the residual attribute-access false
positives (SQLAlchemy Mapped columns, Pydantic response-model fields,
dataclass fields, one test-only helper).

The gate runs in the backend-lint CI job after mypy app, and locally
via make hygiene (or make vulture). vulture_whitelist.py is excluded
from ruff because its bare-name entries are vulture's own format, not
runnable Python.

Baseline triage found no genuine dead code: 108 findings at confidence
60, all framework false positives. One column, users.claimed_at, has no
explicit Python producer or consumer yet (its server_default stamps it
and the assembled-profile claim flow that would write it is a deferred
v0.4 item); it stays as a live, documented column the ORM hydrates on
load.

ruff / mypy (82) / vulture / pytest (667) green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: vidit-admin <admin@vidit.app>
The geolocation_id -> event_id rename in b886021 shortened two list-comprehension lines below ruff's wrap threshold, so ruff format collapses each onto one line. The reformat was not re-run after the mypy-driven fix to those two files, so it landed as drift on the branch. No behaviour change; ruff / mypy / pytest stay green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: vidit-admin <admin@vidit.app>
Complete the model the earlier bounty + geolocation merge started, so
bounties, geolocations, and detections are one events lifecycle end to end.

Backend:
* split author_id into owner_id (edit rights, moves to the fulfiller at
  geolocate) plus a durable event_geolocators credit table
* rename event_claims to event_investigators; the claim action becomes
  POST / DELETE /events/{id}/investigate
* rename location to event_coords, add an optional capture_source_coords
  camera point, per-state stamps (requested_at, detected_at, geolocated_at),
  close_reason, and before_closed_status (the requested vs detected
  discriminator that keeps close unified without losing the distinction)
* fold proof_images into media(role = source | proof); proof images upload
  at publish via multipart proof_files[] with placeholder:// rewrite, so no
  unattached S3 object and no orphan reaper
* drop stored IP and user agent from media and auth_events (privacy)
* four events CHECK constraints: status domain, coords when geolocated, the
  two terminal stamps, and before_closed_status as a full iff
* remove the /bounties router; bounty, geolocation, and detection are now
  filtered views over /events

Frontend: regenerate api-types, repoint the data layer and routes, and add
the rich flows (proof-at-publish editor, close dialog with reason, investigate
toggle, capture_source_coords inputs, single-source enforcement).

Migration h0j2l4n6p8r0 is a clean redefinition plus reseed (the dev DB is
wipeable), reversible, with backfills that satisfy the new CHECKs.

Fixes surfaced by an adversarial review and closed here: the geolocate row
lock reused a stale identity-mapped row (populate_existing); the
before_closed_status CHECK let a NULL slip through on a closed row (explicit
IS NOT NULL); the create and request forms posted the source under the wrong
multipart key; the possible-duplicates probe leaked requested and closed rows
once the coords CHECK relaxed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: vidit-admin <admin@vidit.app>
A three-reviewer pass over the branch surfaced a race, a perf regression,
and several convention gaps. Fixes:

* close() now takes the same row lock as geolocate (populate_existing plus
  with_for_update), so a concurrent fulfil of a requested event can't be
  silently overwritten by an owner-only close reading a stale status
* the card and list media eager-loaders scope to role='source', so read
  paths stop fetching proof rows only to discard them (delete and admin
  still lazy-load every role)
* guard the detection re-import against a coord-less detected row, since
  to_shape(None) would otherwise abort the whole import
* centralize owner-plus-geolocator credit in one _credit_geolocator helper
  so the owner-among-geolocators invariant can't be half-applied
* move the investigate and uninvestigate business logic out of the router
  into the service, matching their sibling verbs
* restore the shared ACCEPTED_IMAGE_MIME constant in the proof editor,
  factor createEventRequest onto the shared form assembler, and reject a
  partially-numeric capture coordinate
* drop dash-as-punctuation from added comments and docstrings, fix the
  stale extract_client_ip and geolocations references, regenerate api-types

Backend 688 tests, frontend 155, ruff / mypy / tsc / lint / build clean,
make hygiene clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: vidit-admin <admin@vidit.app>
Move the event-model refactor entry to the top of Unreleased Changed (newest first) and remove the two intermediate merge/rename entries it supersedes, so the bounty-to-events story reads as one. Drop the removed /bounties endpoint from the v0.5 vetted_only filter row in next.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: vidit-admin <admin@vidit.app>
@vidit-admin vidit-admin merged commit ac8636f into main Jul 4, 2026
11 checks passed
@vidit-admin vidit-admin deleted the claude/elastic-almeida-14f124 branch July 4, 2026 12:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant