# Data model

Entities: performer, reel, platform, candidate copy, case, notice, response, audit event.
All IDs are opaque strings (`perf_…`, `reel_…`, `plat_…`, `cand_…`, `case_…`, `ntc_…`, `resp_…`, `evt_…`).
Timestamps are ISO 8601 UTC. Enum values are lowercase snake_case.

## performer
| field | type | notes |
|---|---|---|
| performer_id | id | |
| display_name | string | |
| handles | list of {platform_id, handle, verified_at} | verified ownership on each platform |
| rights_holders | list of {name, role, contact} | role: self, agent, label, union, estate |
| likeness_opt_in | bool | consent to build voice/face embeddings for lookalike detection; default false |
| jurisdiction | string | country/region code the performer is based in |
| created_at | timestamp | |

## reel
A performer's original work (source side).
| field | type | notes |
|---|---|---|
| reel_id | id | |
| performer_id | id | |
| platform_id | id | where originally published |
| url | string | |
| published_at | timestamp | |
| duration_s | number | |
| title | string | |
| fingerprints | list of {kind, version, hash_ref} | kind: video_phash, audio_chroma, transcript, face_emb, voice_emb |
| evidence_ref | string | archived capture of the original (see pipeline) |

## platform
| field | type | notes |
|---|---|---|
| platform_id | id | |
| name | string | |
| takedown_channel | enum | web_form, email, api, none |
| notice_template | string | ref to template in legal-support-flow |
| sla_days | number | platform's stated response time, nullable |
| jurisdiction | string | country/region whose law the platform applies in its notice process |

## candidate copy
A reel found elsewhere that may copy a source reel.
| field | type | notes |
|---|---|---|
| candidate_id | id | |
| reel_id | id | the source reel it may copy |
| performer_id | id | denormalised for filtering |
| platform_id | id | where the candidate was found |
| url | string | |
| uploader_handle | string | |
| found_at | timestamp | |
| evidence_ref | string | archived capture made before scoring; null means "not yet captured, do not act" |
| match_type | enum | see Match types |
| score | number 0–1 | detector confidence for the match_type. Informational only; never triggers a notice |
| signals | list of {name, value, weight} | the per-detector readouts behind the score |
| segments | list of {src_start, src_end, cand_start, cand_end} | seconds; matched regions |
| derivation_flags | list of enum | ai_watermark, synthetic_voice_score, face_swap_artifacts, generator_metadata; weak evidence, see pipeline |
| status | enum | see Candidate states |
| triage_note | string | free text from human reviewer |
| reviewed_by | string | |
| reviewed_at | timestamp | |
| case_id | id | set once escalated |

## case
| field | type | notes |
|---|---|---|
| case_id | id | |
| performer_id | id | |
| candidate_ids | list of id | one case may bundle several copies by the same uploader |
| status | enum | see Case states |
| route | enum | takedown, likeness_claim, platform_policy, licensing, monitor_only, no_action, refer_counsel |
| | | likeness_claim for voice_clone, lookalike or ai_imitation requires performer.likeness_opt_in = true and the depicted person as claimant or written authority |
| standing | {holder, role, basis_doc, checked_by, checked_at} | role: copyright_owner, exclusive_licensee, authorised_agent, likeness_holder, union_rep, none. basis_doc: ref to the document proving it. likeness_holder cannot send a DMCA notice (see legal-support-flow.md) |
| fair_use_check | {result, checked_by, checked_at, note} | result: likely_permitted, unclear, likely_not_permitted |
| lawyer_review_required | bool | set true on entering refer_counsel or disputed; default true for likeness_claim and cross-jurisdiction cases |
| deadlines | list of {kind, due_at, source} | kind: platform_sla, counter_notice_restore_window, suit_filing_window, recheck |
| opened_at / closed_at | timestamp | |
| recheck_at | timestamp | next scheduled re-check of candidate URLs |
| outcome | enum | removed, licensed, left_up, withdrawn, disputed |

## notice
| field | type | notes |
|---|---|---|
| notice_id | id | |
| case_id | id | |
| platform_id | id | |
| kind | enum | copyright, likeness, impersonation, platform_policy |
| status | enum | draft, approved, sent, acknowledged, removed, rejected, countered, withdrawn |
| drafted_by / drafted_at | string / timestamp | may be `tool`; a tool may draft, never approve |
| approved_by / approved_at | string / timestamp | required human name before status can be sent |
| sent_at | timestamp | |
| body_ref | string | |
| evidence_refs | list of string | |

## response
| field | type | notes |
|---|---|---|
| response_id | id | |
| notice_id | id | |
| from_party | enum | platform, uploader, counsel |
| kind | enum | platform_ack, removed, rejected, counter_notice, uploader_reply, no_response |
| received_at | timestamp | |
| deadline_at | timestamp | e.g. counter-notice reinstatement window |
| body_ref | string | |

## audit event
Append-only. Every state change writes one.
| field | type |
|---|---|
| event_id | id |
| at | timestamp |
| actor | string (human name or `system:<detector>`) |
| entity_type / entity_id | string / id |
| action | string |
| before / after | json |

## Match types
Ordered from strongest to weakest evidence.
| match_type | meaning | typical signals |
|---|---|---|
| exact_reupload | same file or near-identical frames and audio | video_phash + audio_chroma both high over full duration |
| clip_lift | a segment of the source used, possibly with new surrounding content | segment-level phash/chroma |
| re_edit | source cropped, sped, mirrored, filtered, captioned, or re-cut | transformed phash, chroma under tempo shift |
| audio_lift | original audio over new video | audio_chroma only |
| ai_imitation | content generated to reproduce the performance (choreo, delivery, script) | transcript similarity, pose similarity, derivation_flags |
| voice_clone | performer's voice synthesised | voice_emb + derivation_flags; requires likeness_opt_in |
| lookalike | resembles performer's style or persona without direct reuse | face_emb / style signals; weakest, monitor_only by default |

## Candidate states
`discovered → captured → scored → in_triage → (confirmed | dismissed | needs_more_evidence) → escalated | closed`
- A candidate cannot reach `scored` without `evidence_ref`.
- Only a human moves a candidate from `in_triage` onward.
- `dismissed` candidates stay in the store (with reason) so the same URL is not resurfaced.

## Case states
`opened → standing_checked → routed → fair_use_checked → notice_drafted → approved → notice_sent → awaiting_response → response_received → resolved | disputed → closed`
- Side exits from any state: `no_action`, `refer_counsel` (both a route and a status; `refer_counsel` status means the case is parked with counsel).
- `disputed` may return to `awaiting_response` or go to `refer_counsel`.
- `standing_checked` and `fair_use_checked` each require a named checker.
- `monitor_only` and `no_action` routes go `routed → closed` directly, with `recheck_at` set for monitor_only.

## Synthetic seed data recipe
Mark every record `"synthetic": true`. Suggested set:
1. 3 performers: one dancer, one comedian, one singer. The singer has `likeness_opt_in: true`, the others false.
2. 4 platforms: two with `takedown_channel: web_form`, one `api`, one `none`.
3. 6 reels: two per performer, 15–60 s, each with video_phash and audio_chroma fingerprints; singer's reels also voice_emb.
4. 14 candidates covering every match_type at least once, including:
   - one `exact_reupload` with score 0.98 that a reviewer *dismissed* (it was the performer's own cross-post) — shows score ≠ action;
   - one `lookalike` with score 0.41 and `derivation_flags: [ai_watermark]` sitting in `in_triage`;
   - one with `evidence_ref: null` still in `discovered`;
   - one `voice_clone` for the singer with a counter-notice in flight.
5. 5 cases: one per route, each with `standing_checked` by a named synthetic person.
6. Notices and responses for the takedown and licensing cases; one `counter_notice` response with `deadline_at` 10 days after received_at.
7. Audit events: generate one per state transition above, actor alternating between `system:fingerprint` and synthetic reviewer names.
Generate names from a fixed word list and timestamps from a fixed base date so the set is reproducible.
