ccr-platform / backend /app /models.py
devaanand's picture
feat(scoring): anchor vectors (bipolar constructs) [spec 0006]
f341328
Raw
History Blame Contribute Delete
9.58 kB
"""ORM models.
IDs are UUID strings (portable across SQLite/Postgres). JSON-ish payloads
(column lists, construct items, job metadata/summaries) are stored as JSON
text - they are read-mostly blobs, not queried relationally.
"""
import uuid
from datetime import datetime, timezone
from sqlalchemy import Boolean, Float, ForeignKey, String, Text
from sqlalchemy.orm import Mapped, mapped_column
from .db import Base
def _uuid() -> str:
return uuid.uuid4().hex
def _now() -> str:
return datetime.now(timezone.utc).isoformat(timespec="seconds")
class User(Base):
__tablename__ = "users"
id: Mapped[str] = mapped_column(String(32), primary_key=True, default=_uuid)
email: Mapped[str] = mapped_column(String(255), unique=True, index=True)
name: Mapped[str] = mapped_column(String(120), default="")
password_hash: Mapped[str] = mapped_column(Text) # scrypt$salt$digest (auth.py)
role: Mapped[str] = mapped_column(String(16), default="external")
# external | lab | maintainer | pi (auth.ROLES; legacy "member" reads as
# external). lab+ = unlimited saved runs; maintainer/pi also get /admin.
# Staff roles are grantable only by ADMIN_EMAILS env-allowlisted admins
# (admin.py guards), so the API cannot self-escalate; the env allowlist
# remains the bootstrap and break-glass admin path.
created_at: Mapped[str] = mapped_column(String(32), default=_now)
class RoleAssignment(Base):
"""Pre-provisioned access: a role bound to an email BEFORE the account
exists (PI request 2026-07-22 - e.g. an external collaborator who must
land with full credentials on first sign-in, Google or password).
Unlike invite links (bearer tokens, external/lab only), assignments are
email-bound and may carry staff roles - so creating one for pi/maintainer
requires escalation rights. Claimed rows are kept as history."""
__tablename__ = "role_assignments"
id: Mapped[str] = mapped_column(String(32), primary_key=True, default=_uuid)
email: Mapped[str] = mapped_column(String(255), unique=True, index=True)
role: Mapped[str] = mapped_column(String(16))
assigned_by: Mapped[str] = mapped_column(String(255), default="")
created_at: Mapped[str] = mapped_column(String(32), default=_now)
claimed_at: Mapped[str] = mapped_column(String(32), default="") # "" = pending
class Invite(Base):
"""Invite links, stateful so they can be listed, revoked, and traced.
The signed token (auth.py) still proves authenticity, but redemption
requires this row to be live: not revoked, not expired. Revocation is
soft (revoked_at) so history and redemptions stay visible. Redemptions
are a read-mostly JSON list [{email, at}], per the repo's JSON-blob
convention."""
__tablename__ = "invites"
id: Mapped[str] = mapped_column(String(32), primary_key=True, default=_uuid)
role: Mapped[str] = mapped_column(String(16))
token: Mapped[str] = mapped_column(Text) # stored so admins can re-copy the link
created_by: Mapped[str] = mapped_column(String(255), default="")
created_at: Mapped[str] = mapped_column(String(32), default=_now)
expires_at: Mapped[str] = mapped_column(String(32)) # ISO date, inclusive
revoked_at: Mapped[str] = mapped_column(String(32), default="") # "" = active
redemptions_json: Mapped[str] = mapped_column(Text, default="[]")
class AdminAudit(Base):
"""Append-only trail of admin actions (who did what to whom, when).
What makes multiple admins trustworthy: role grants, password resets,
deletions, invites, requeues, and verification changes all land here.
Never updated or deleted from the app."""
__tablename__ = "admin_audit"
id: Mapped[str] = mapped_column(String(32), primary_key=True, default=_uuid)
at: Mapped[str] = mapped_column(String(32), default=_now)
actor_email: Mapped[str] = mapped_column(String(255))
action: Mapped[str] = mapped_column(String(40)) # e.g. set_role, invite_created
target: Mapped[str] = mapped_column(String(300), default="") # email/name acted on
detail: Mapped[str] = mapped_column(Text, default="")
class Project(Base):
__tablename__ = "projects"
id: Mapped[str] = mapped_column(String(32), primary_key=True, default=_uuid)
name: Mapped[str] = mapped_column(String(200))
description: Mapped[str] = mapped_column(Text, default="")
archived: Mapped[bool] = mapped_column(Boolean, default=False)
owner_user_id: Mapped[str] = mapped_column(String(32), default="") # "" = anonymous
created_at: Mapped[str] = mapped_column(String(32), default=_now)
class Corpus(Base):
__tablename__ = "corpora"
id: Mapped[str] = mapped_column(String(32), primary_key=True, default=_uuid)
project_id: Mapped[str] = mapped_column(ForeignKey("projects.id"))
filename: Mapped[str] = mapped_column(String(300))
path: Mapped[str] = mapped_column(Text)
n_rows: Mapped[int] = mapped_column()
columns_json: Mapped[str] = mapped_column(Text) # list[str]
parse_info_json: Mapped[str] = mapped_column(Text, default="{}") # how the file was parsed
suggested_text_column: Mapped[str] = mapped_column(String(200), default="")
created_at: Mapped[str] = mapped_column(String(32), default=_now)
class Construct(Base):
__tablename__ = "constructs"
id: Mapped[str] = mapped_column(String(32), primary_key=True, default=_uuid)
name: Mapped[str] = mapped_column(String(200))
description: Mapped[str] = mapped_column(Text, default="")
reference: Mapped[str] = mapped_column(Text, default="") # citation
items_json: Mapped[str] = mapped_column(Text) # list[str]
reverse_flags_json: Mapped[str] = mapped_column(Text, default="[]") # list[bool], parallel to items
is_seed: Mapped[bool] = mapped_column(Boolean, default=False)
# Library identity (spec 0004): versioned append-only; hash via reference algorithm.
construct_slug: Mapped[str] = mapped_column(String(120), default="")
version: Mapped[int] = mapped_column(default=1)
item_hash: Mapped[str] = mapped_column(String(64), default="")
verification_status: Mapped[str] = mapped_column(String(24), default="draft")
# draft | needs_verification | verified | archived
language: Mapped[str] = mapped_column(String(12), default="en")
category: Mapped[str] = mapped_column(String(80), default="")
# AI-generated provenance: "" for typed/uploaded/library constructs; else
# {"model", "prompt_version", "generated_at"} (ITEM_GENERATION.md). Set
# when the construct was saved from a generated draft - the label persists
# even after the researcher edits items (the seed was AI; item_hash covers
# as-generated vs edited).
generation_json: Mapped[str] = mapped_column(Text, default="")
created_at: Mapped[str] = mapped_column(String(32), default=_now)
class GenerationEvent(Base):
"""One row per successful LLM item generation - the per-user daily cap
(CCR_USER_MAX_GENERATIONS_PER_DAY) counts today's rows. DB-backed rather
than a cookie counter because generation spends real API money, and
signed-in users could clear cookies; a table survives that (and restarts
on Postgres deployments). Rows are tiny and append-only."""
__tablename__ = "generation_events"
id: Mapped[str] = mapped_column(String(32), primary_key=True, default=_uuid)
user_id: Mapped[str] = mapped_column(String(32), index=True)
model: Mapped[str] = mapped_column(String(120), default="")
created_at: Mapped[str] = mapped_column(String(32), default=_now)
class Job(Base):
__tablename__ = "jobs"
id: Mapped[str] = mapped_column(String(32), primary_key=True, default=_uuid)
project_id: Mapped[str] = mapped_column(ForeignKey("projects.id"))
corpus_id: Mapped[str] = mapped_column(ForeignKey("corpora.id"))
construct_id: Mapped[str] = mapped_column(ForeignKey("constructs.id"))
# Multi-construct runs: the full ordered id list. construct_id stays the
# first entry (FK + legacy rows, whose "[]" here means "just construct_id").
construct_ids_json: Mapped[str] = mapped_column(Text, default="[]")
# Anchor-vector (bipolar) runs (spec 0006): the contrasting opposite-pole
# construct and the similarity metric. "" on both = a normal, non-anchored
# run. Plain String (not FK) so the additive SQLite auto-migration can add
# it to existing DBs, mirroring construct_ids_json.
opposite_construct_id: Mapped[str] = mapped_column(String(32), default="")
similarity_metric: Mapped[str] = mapped_column(String(12), default="") # "" | "cosine" | "dot"
text_column: Mapped[str] = mapped_column(String(200))
model_name: Mapped[str] = mapped_column(String(200)) # registry id (or test fake)
language: Mapped[str] = mapped_column(String(12), default="en") # selected analysis language
status: Mapped[str] = mapped_column(String(20), default="queued")
# queued -> running -> completed | failed
progress: Mapped[float] = mapped_column(Float, default=0.0) # 0..1
error: Mapped[str] = mapped_column(Text, default="")
metadata_json: Mapped[str] = mapped_column(Text, default="{}") # reproducibility record
summary_json: Mapped[str] = mapped_column(Text, default="{}") # results summary
result_path: Mapped[str] = mapped_column(Text, default="")
created_at: Mapped[str] = mapped_column(String(32), default=_now)
started_at: Mapped[str] = mapped_column(String(32), default="")
finished_at: Mapped[str] = mapped_column(String(32), default="")