You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Data Dictionary — Quality Scorecard System (Cycle 1)
Per the Data-Driven Development Framework v1.1, §7.2: "For each field define data type,
length/format, business rules, nullability, keys/identity and validation rules." This
document is that contract, table by table, for every column created by
backend/alembic/versions/0001_initial_schema.py and mapped in backend/app/models/.
Conventions used below:
Data type is the Postgres type as created by the migration.
Length/format captures precision/scale, string length caps, or JSON shape where relevant.
Business rule is the plain-English rule the column exists to support.
Nullability is NOT NULL or NULL as enforced by the DB.
Validation rule is the concrete DB-level mechanism (CHECK constraint, trigger, unique
index) or, where validation is intentionally left to the application layer, says so
explicitly.
All primary keys are client-generated UUIDv4 (uuid.uuid4() in Python), not
DB-sequence-generated. All tables except scorecard_embeddings, chat_sessions, and
chat_messages (and audit_log) carry created_at/updated_at timestamps
(TIMESTAMPTZ, server_default = now(), updated_at also ON UPDATE now()); those
three tables carry only created_at (chat_sessions also has last_activity_at in
place of updated_at) since they are append-mostly logs, not mutable records.
1. users
Field
Data type
Length/format
Business rule
Nullability
Keys/Identity
Validation rule
id
UUID
v4
Unique identifier for a person who can own scorecards, evaluate, or design.
NOT NULL
Primary key
Generated client-side (uuid.uuid4).
email
VARCHAR
max 320 chars (RFC 5321 mailbox limit)
Sign-in identifier and the address for reset / verification mail.
NOT NULL
Unique (uq_users_email), indexed
Application-layer format check (plain string, not EmailStr, because seed accounts use a reserved TLD); normalised to lower case.
name
VARCHAR
max 200 chars
Display name.
NOT NULL
—
Non-empty enforced by Pydantic (str, no blank-check at DB level — left to app layer).
org_id
UUID
v4
Reserved for future multi-tenant/org scoping. No organizations table exists yet in Cycle 1 (full RBAC/org model is deferred per the plan), so this is a plain scalar, not a FK.
NULL
—
None at DB level (deliberately deferred; documented gap).
role
VARCHAR
max 50 chars, default 'user' (migration 0013)
admin or user for people; system marks the internal marker row. Legacy member / designer / evaluator were normalised to user.
NOT NULL
—
Validated in the API (admin|user); not a DB enum. See docs/plan-sharing-rbac.md.
auth_provider_id
VARCHAR
max 255 chars
External OIDC subject id, reserved; OAuth links live in oauth_identities.
NULL
Unique (uq_users_auth_provider_id)
Uniqueness only; format is provider-specific and unvalidated in Cycle 1.
created_at
TIMESTAMPTZ
—
Row creation time.
NOT NULL
—
server_default = now().
updated_at
TIMESTAMPTZ
—
Last row modification time.
NOT NULL
—
server_default = now(), ON UPDATE now().
2. scorecards
Field
Data type
Length/format
Business rule
Nullability
Keys/Identity
Validation rule
id
UUID
v4
Unique identifier for a scorecard "definition" (the versionless shell; actual KPI content lives in scorecard_versions).
NOT NULL
Primary key
—
name
VARCHAR
max 255 chars
Human-chosen scorecard name.
NOT NULL
—
Non-empty via Pydantic; no DB-level uniqueness (two scorecards may share a name — owner_id + name is not unique by design, since re-use/cloning is an explicit product flow).
owner_id
UUID
v4
The user who created/owns this scorecard.
NOT NULL
FK → users.id (ON DELETE RESTRICT)
A user with owned scorecards cannot be deleted (FK RESTRICT) — prevents orphaning scorecards.
domain
VARCHAR
max 120 chars
Free-text business domain/category (e.g. "Customer Support", "Software Engineering"), used for filtering/similarity grouping.
NULL
Indexed
None — open vocabulary by design (Cycle 1 has no fixed domain taxonomy).
purpose_statement
TEXT
unbounded
The scorecard's stated purpose (per the Quality Scorecard Framework's "purpose" step); also the primary text embedded for similarity search.
NULL
—
None at DB level.
scope
TEXT
unbounded
What is/isn't covered by this scorecard.
NULL
—
None at DB level.
target_score
NUMERIC
precision 4, scale 2 (0.00–99.99 representable)
The target weighted score used for the QTC (Quality/Time/Cost) gate and bullet-chart target tick.
NULL
—
CHECK (target_score IS NULL OR (target_score >= 0 AND target_score <= 10)) (ck_scorecards_target_score_range) — scores are on a 0–10 scale.
status
ENUM scorecard_status
draft | published | archived
Lifecycle state (see plan's "lifecycle states" scenario category).
NOT NULL, default draft
—
Postgres native enum — only the three listed values are representable at all.
current_version_id
UUID
v4
Points at the scorecard_versions row currently considered "live" for this scorecard. Circular with scorecard_versions.scorecard_id; the FK is added via ALTER TABLE after both tables exist (see migration §"Deferred circular FK").
NULL
FK → scorecard_versions.id (ON DELETE SET NULL, use_alter=True)
If the current version is deleted, this reverts to NULL rather than blocking the delete.
created_at / updated_at
TIMESTAMPTZ
—
Standard audit timestamps.
NOT NULL
—
server_default = now() (+ON UPDATE now() for updated_at).
3. scorecard_versions
Field
Data type
Length/format
Business rule
Nullability
Keys/Identity
Validation rule
id
UUID
v4
Unique identifier for one immutable-in-spirit version of a scorecard's KPI tree/guidelines.
NOT NULL
Primary key
—
scorecard_id
UUID
v4
The parent scorecard this version belongs to.
NOT NULL
FK → scorecards.id (ON DELETE CASCADE), indexed
Deleting a scorecard cascades to delete all its versions (and, transitively, KPI nodes/guidelines).
version_number
INTEGER
≥ 1 by convention
Monotonically increasing version counter per scorecard (v1, v2, ...).
NOT NULL
Unique with scorecard_id (uq_scorecard_version_number)
Uniqueness enforced; the "increasing" property itself is an app-layer convention (not DB-checked, since e.g. gaps from a deleted draft version are acceptable).
guideline_notes
TEXT
unbounded
Free-text changelog/rationale for this version (e.g. "reworded guideline language for clarity").
NULL
—
None.
created_by
UUID
v4
The user who authored this version.
NOT NULL
FK → users.id (ON DELETE RESTRICT)
Prevents deleting a user who authored a version still on record.
is_active
BOOLEAN
—
Marks whether this version is the historically "active" one vs. superseded (distinct from scorecards.current_version_id, which is the pointer; is_active is a per-version flag useful when browsing version history).
NOT NULL, default true
—
None at DB level (app sets exactly one active version per scorecard by convention; not DB-enforced in Cycle 1 — documented gap, candidate for a Cycle 2/3 constraint).
created_at / updated_at
TIMESTAMPTZ
—
Standard audit timestamps.
NOT NULL
—
server_default = now() (+ON UPDATE now()).
4. kpi_nodes
Self-referential materialized-path hierarchy (max depth 4), per the plan's ltree-based
design.
Field
Data type
Length/format
Business rule
Nullability
Keys/Identity
Validation rule
id
UUID
v4
Unique identifier for one KPI/parameter node.
NOT NULL
Primary key
—
scorecard_version_id
UUID
v4
Which scorecard version this node belongs to.
NOT NULL
FK → scorecard_versions.id (ON DELETE CASCADE), indexed
Deleting a version cascades to delete its whole KPI tree.
parent_id
UUID
v4
Self-reference to the parent node; NULL for a Level-1 (root) node.
NULL
FK → kpi_nodes.id (ON DELETE CASCADE), indexed
Deleting a parent cascades to delete its subtree.
path
LTREE (via sqlalchemy_utils.LtreeType)
dot-separated labels, one label per ancestor (e.g. a1b2c3....d4e5f6...); each label is the owning node's UUID hex (32 lowercase hex chars — the only characters ltree labels reliably accept)
Materialized path for O(1) ancestor queries and fast subtree lookups.
NOT NULL
GiST index (ix_kpi_nodes_path_gist, USING GIST (path))
Computed server-side by the API/seed layer as parent.path + '.' + this_node_id.hex (or just the label, for roots) — not user-supplied. No DB constraint ties path to parent_id/level consistency in Cycle 1 (documented gap — a mismatch would only arise from a bug in the write path, not from user input, since it is never client-settable via the API).
level
INTEGER
1–4
Depth in the hierarchy (Level1→Level4 per the Quality Scorecard Framework).
NOT NULL
—
CHECK (level >= 1 AND level <= 4) (ck_kpi_nodes_level_range). The API additionally refuses to create a child under a Level-4 parent (422), since level 5 would violate this CHECK.
name
VARCHAR
max 255 chars
The KPI/parameter's display name.
NOT NULL
Unique among siblings (see below)
CREATE UNIQUE INDEX uq_kpi_nodes_sibling_name ON kpi_nodes (scorecard_version_id, COALESCE(parent_id, '00000000-...'::uuid), name) — two siblings (same parent, or both roots of the same version) cannot share a name. Postgres treats NULL as always-distinct in a plain UNIQUE constraint, so root nodes (parent_id IS NULL) are normalized via COALESCE to a sentinel UUID so duplicate root names are still caught.
weight
NUMERIC
precision 5, scale 2 (0.00–999.99 representable; business range is 0–100)
This node's weight within its sibling group, as a percentage.
NOT NULL
—
CHECK (weight >= 0 AND weight <= 100) (ck_kpi_nodes_weight_range) plus the deferred constraint trigger trg_kpi_node_weight_sum (see below): all siblings' weights (same parent_id, or same scorecard_version_id for roots) must sum to exactly 100.00 (±0.01 tolerance) by the end of the transaction. A sibling group can legitimately be empty mid-transaction (e.g. after deleting the last sibling) — that is not checked.
display_order
INTEGER
—
UI ordering hint among siblings.
NOT NULL, default 0
—
None — any integer, including duplicates/negatives, is accepted (purely presentational).
created_at / updated_at
TIMESTAMPTZ
—
Standard audit timestamps.
NOT NULL
—
server_default = now() (+ON UPDATE now()).
Weight-sum trigger detail (trg_kpi_node_weight_sum, function check_kpi_node_weight_sum()):
a DEFERRABLE INITIALLY DEFERREDCONSTRAINT TRIGGER (AFTER INSERT OR UPDATE OF weight, parent_id, scorecard_version_id OR DELETE, FOR EACH ROW) that only evaluates at
transaction COMMIT (or on SET CONSTRAINTS trg_kpi_node_weight_sum IMMEDIATE), so a
multi-row insert of a full sibling group is checked once, as a whole, rather than
rejecting each row before its siblings exist. A plain CHECK constraint cannot express
this because Postgres CHECK constraints cannot aggregate across other rows.
5. kpi_guidelines
Field
Data type
Length/format
Business rule
Nullability
Keys/Identity
Validation rule
id
UUID
v4
Unique identifier for one guideline rung.
NOT NULL
Primary key
—
kpi_node_id
UUID
v4
The KPI this guideline rung belongs to.
NOT NULL
FK → kpi_nodes.id (ON DELETE CASCADE), indexed
Deleting a KPI node cascades to delete its guidelines.
score_level
INTEGER
0–10
Which of the 11 qualitative/quantitative rungs this row defines (per the Quality Scorecard Framework's 11-level 0–10 guideline model).
NOT NULL
Unique with kpi_node_id (uq_kpi_guidelines_node_level)
CHECK (score_level >= 0 AND score_level <= 10) (ck_kpi_guidelines_score_level_range). A KPI node is not required to have all 11 levels present (see the "missing guideline level" flawed-data scenario in the seed script) — completeness is an app-layer/Cycle-3 concern, not a DB constraint, since a node under active drafting legitimately has an incomplete set.
qualitative_text
TEXT
unbounded
The written description of what this score level means.
NOT NULL
—
Non-empty enforced by Pydantic; no DB-level length/emptiness check.
quantitative_criteria
JSONB
free-form object; two documented shapes in use: {"metric": str, "operator": str, "value": number} (numeric) or {"category": str} (categorical)
Machine-checkable criteria backing the qualitative text, where applicable (per the plan's "numeric vs categorical quantitative guidelines" business-variant scenario).
NULL
—
No JSON-schema enforcement at the DB level (Postgres JSONB accepts any valid JSON); shape is a documented app-layer convention only.
created_at / updated_at
TIMESTAMPTZ
—
Standard audit timestamps.
NOT NULL
—
server_default = now() (+ON UPDATE now()).
6. scorecard_embeddings
Field
Data type
Length/format
Business rule
Nullability
Keys/Identity
Validation rule
id
UUID
v4
Unique identifier for one embedding row.
NOT NULL
Primary key
—
scorecard_version_id
UUID
v4
The scorecard version this embedding represents (one embedding per version — re-embedding is an UPDATE, not a new row; see the "embedding-model change requiring backfill" migration scenario).
NOT NULL
FK → scorecard_versions.id (ON DELETE CASCADE), unique (uq_scorecard_embeddings_version_id)
Uniqueness enforced at the DB level — at most one embedding per version, always.
embedding
VECTOR(1024) (pgvector, via pgvector.sqlalchemy.Vector)
fixed 1024 dimensions (Amazon Titan Text Embeddings V2 output size, per the plan)
The similarity-search vector for this version's purpose/KPI text.
NOT NULL
HNSW index (ix_scorecard_embeddings_embedding_hnsw, USING hnsw (embedding vector_cosine_ops))
pgvector enforces the fixed dimension (1024) at the type level — a vector of any other length is rejected on insert. Cycle 1 populates this with a deterministic synthetic vector (no live Bedrock/Titan call — that integration is Cycle 1c); values are real 1024-dim unit-scaled floats, just not produced by the real embedding model yet.
embedding_model
VARCHAR
max 100 chars
Which embedding model produced this vector (e.g. amazon.titan-embed-text-v2:0), so a model change can be detected and backfilled.
NOT NULL
—
None beyond NOT NULL — free text.
source_text_hash
VARCHAR
max 64 chars (SHA-256 hex digest length)
Hash of the source text that was embedded, so the caller can detect "does this embedding need to be refreshed" without re-hashing large text.
NOT NULL
—
None beyond NOT NULL — the hash algorithm/format is an app-layer convention (SHA-256 hex, in the seed script).
created_at / updated_at
TIMESTAMPTZ
—
Standard audit timestamps.
NOT NULL
—
server_default = now() (+ON UPDATE now()).
7. evaluations
Field
Data type
Length/format
Business rule
Nullability
Keys/Identity
Validation rule
id
UUID
v4
Unique identifier for one evaluation run.
NOT NULL
Primary key
—
scorecard_version_id
UUID
v4
Which exact scorecard version this evaluation was scored against (pins the guideline set used, even if the scorecard is later versioned again).
NOT NULL
FK → scorecard_versions.id (ON DELETE RESTRICT), indexed
A scorecard version with evaluations against it cannot be deleted — preserves evaluation history (see "version bump preserves old evaluations" migration scenario).
name
VARCHAR
max 255 chars
Human-readable label for this evaluation run.
NOT NULL
—
Non-empty via Pydantic.
evaluated_by
UUID
v4
The user who ran/submitted this evaluation.
NOT NULL
FK → users.id (ON DELETE RESTRICT)
Prevents deleting a user with evaluation history on record.
The 7-band RAG colour result the final score maps to (per the Quality Scorecard Framework's RAG palette — kept as a separate concept from the app's #FFF700 brand palette).
NULL (until scored)
—
Postgres native enum; the score→band mapping itself (rag_band_for_score() in app/models/enums.py) is applied in application code, not the DB.
submitted_at
TIMESTAMPTZ
—
When the evaluation was finalized/submitted.
NULL (until submitted)
—
None beyond nullability.
domain
VARCHAR
max 120 chars
Denormalized copy of scorecards.domain (via scorecard_version → scorecard), for fast filtering/reporting without a join.
NULL
Indexed
None — kept in sync by the write path, not a DB trigger (documented as an intentional denormalization per the plan's schema sketch, not auto-synced at the DB level in Cycle 1).
created_at / updated_at
TIMESTAMPTZ
—
Standard audit timestamps.
NOT NULL
—
server_default = now() (+ON UPDATE now()).
8. evaluation_kpi_results
Field
Data type
Length/format
Business rule
Nullability
Keys/Identity
Validation rule
id
UUID
v4
Unique identifier for one per-KPI scoring result.
NOT NULL
Primary key
—
evaluation_id
UUID
v4
The evaluation this result belongs to.
NOT NULL
FK → evaluations.id (ON DELETE CASCADE), indexed
Deleting an evaluation cascades to delete its KPI results.
kpi_node_id
UUID
v4
Which KPI node this result scores.
NOT NULL
FK → kpi_nodes.id (ON DELETE RESTRICT), indexed
A KPI node referenced by a result cannot be deleted. Plus: trg_eval_kpi_result_version_match (see below) — this node must belong to the same scorecard_version_id as the parent evaluation.
score
NUMERIC
precision 4, scale 2
The judge's (or human override's) score for this KPI, 0–10.
NOT NULL
—
CHECK (score >= 0 AND score <= 10) (ck_eval_kpi_results_score_range).
matched_guideline_level
INTEGER
0–10
Which guideline rung (kpi_guidelines.score_level) the reasoning matched to — the "basis for the rating".
NULL
—
CHECK (matched_guideline_level IS NULL OR (matched_guideline_level >= 0 AND matched_guideline_level <= 10)) (ck_eval_kpi_results_matched_level_range). Not enforced to equal a row that actually exists in kpi_guidelines for this node (documented gap — acceptable in Cycle 1 since guideline sets may be incomplete, per kpi_guidelines above).
reasoning_text
TEXT
unbounded
Written justification for the score (the "reasoning-before-score" requirement from the plan's judge design).
NULL
—
None at DB level.
evidence_quotes
JSONB
array or object of quoted evidence strings
Direct quotes from the evaluated input supporting the score (for the contextual-grounding check mentioned in the plan).
NULL
—
None — free-form.
created_at / updated_at
TIMESTAMPTZ
—
Standard audit timestamps.
NOT NULL
—
server_default = now() (+ON UPDATE now()).
Cross-version trigger detail (trg_eval_kpi_result_version_match, function
check_eval_kpi_result_version_match()): a BEFORE INSERT OR UPDATE OF evaluation_id, kpi_node_idFOR EACH ROW trigger (not deferred — fires immediately) that looks up
evaluations.scorecard_version_id for NEW.evaluation_id and kpi_nodes.scorecard_version_id
for NEW.kpi_node_id, and raises if they differ. This is the "cross-version KPI reference"
invalid-data scenario from the plan's catalogue — there is no plain FK for it because it
spans two tables through a third column, so it needed a trigger rather than a CHECK/FK.
9. chat_sessions
Field
Data type
Length/format
Business rule
Nullability
Keys/Identity
Validation rule
id
UUID
v4
Unique identifier for one chat-builder session.
NOT NULL
Primary key
—
user_id
UUID
v4
The user driving this chat session.
NOT NULL
FK → users.id (ON DELETE CASCADE), indexed
Deleting a user deletes their chat sessions.
status
ENUM chat_session_status
active | completed | abandoned
Session lifecycle state.
NOT NULL, default active
—
Postgres native enum.
context_summary
TEXT
unbounded
Rolling summary of the conversation (for context-window management by the Cycle 1c LangGraph builder).
NULL
—
None.
target_scorecard_id
UUID
v4
If this session is refining an existing scorecard rather than building one from scratch.
NULL
FK → scorecards.id (ON DELETE SET NULL)
If the target scorecard is deleted, this reverts to NULL rather than blocking the delete.
created_at
TIMESTAMPTZ
—
Session start time.
NOT NULL
—
server_default = now().
last_activity_at
TIMESTAMPTZ
—
Last message time (used for session listing/sort).
NOT NULL
—
server_default = now(), ON UPDATE now().
10. chat_messages
Field
Data type
Length/format
Business rule
Nullability
Keys/Identity
Validation rule
id
UUID
v4
Unique identifier for one chat message.
NOT NULL
Primary key
—
session_id
UUID
v4
The session this message belongs to.
NOT NULL
FK → chat_sessions.id (ON DELETE CASCADE), indexed
Deleting a session deletes its messages.
role
ENUM chat_message_role
user | assistant | system | tool
Who/what produced this message.
NOT NULL
—
Postgres native enum.
content
TEXT
unbounded
The message text.
NOT NULL
—
Non-empty enforced by Pydantic where applicable; no DB-level check (a tool-call-only assistant turn may have minimal content).
A result's KPI node belongs to the same scorecard version as its evaluation.
GiST index
kpi_nodes.path
Fast ltree ancestor/subtree queries.
HNSW index
scorecard_embeddings.embedding
Fast approximate cosine-similarity search.
Everything in the table above is proven against a real Postgres instance by
backend/app/scripts/generate_scenarios.py's "invalid/flawed data" scenarios (weight-sum
97%/103%, duplicate sibling names, cross-version KPI reference, out-of-range score) and by
backend/tests/test_weight_trigger.py.
Tables and columns added after migration 0001
Summary only; the ORM models in backend/app/models/ and the migrations in backend/alembic/versions/ are the source of truth. Diagram: architecture.md.
Migration
Change
0002-0009
Judge ensemble fields and review flag on evaluation_kpi_results; chat turn marker, chat_turn_events (live trace), session title, turn error columns; scoring formula and KPI flags; category nodes carry no weight
evaluations.lease_owner, lease_expires_at, heartbeat_at, cancel_requested_at; the same lease / cancel fields on chat_sessions prefixed turn_, plus turn_message, turn_first (the in-progress marker becomes a queued job); table idempotency_keys (PK user_id, scope, key)
0012_auth_ownership
users: password_hash, email_verified_at, is_active, failed_logins, locked_until, sessions_valid_after; tables refresh_tokens (hashed, family_id, used_at, revoked_at), password_reset_tokens (reset and verify tokens), oauth_identities; evaluations.owner_id (the owning user, distinct from lease_owner); audit_log.ip, user_agent, request_id
0013_sharing_rbac
users.username (unique on lower()), users.deleted_at; roles normalised to admin/user; tables scorecard_collaborators (unique scorecard_id, user_id), scorecard_invitations (status pending/accepted/declined/revoked, partial unique on pending), scorecard_activity (append-only: an UPDATE trigger raises), notifications (unique per-user dedupe_key); list and job-slot indexes on evaluations
scorecards.deleted_at, deleted_by; partial indexes for the trash listing and the retention purge
0016_eval_cancel_reason
evaluations.cancel_reason (cancelled_by_trash marks jobs to resume on restore)
0017_eval_renamed_at
evaluations.renamed_at (set when a user renames the evaluation; the Excel export then labels it with that name, and the AI pipeline never replaces it)
Evaluation statuses are pending, in_progress, completed, failed, queued, ingesting, processing, scoring; a cancelled evaluation is failed with error_code = 'cancelled'.
Documented gaps (intentionally out of scope for Cycle 1, per the plan)
users.role vocabulary, kpi_nodes.path/level internal consistency, evaluations.domain
denormalization sync, and evaluation_kpi_results.matched_guideline_level referencing an
actual existing kpi_guidelines row are all enforced by the application/seed layer, not
the database, in Cycle 1. Candidates for Cycle 2/3 hardening if evidence from real usage
shows they need DB-level enforcement.
Row-Level Security (RLS) and org-level multi-tenancy are still not implemented: access control is enforced in the application layer (backend/app/authz.py), and users.org_id is an unused scalar.
Authentication (password + rotating refresh tokens, optional OAuth) and two-role RBAC were added later; see docs/plan-sharing-rbac.md.