Skip to content
TestLookupTestLookup
Search Ctrl K

Search TestLookup

Enter a term to search the local static index.

    Data dictionary

    This reference lists every PostgreSQL table that TestLookup’s code defines in this version. It is built from the SQLAlchemy model code, without connecting to a database, so it shows the database layout the code expects. Your database matches it once its database updates (migrations) have run.

    The dictionary below lists 160 tables, each with its model class and number of columns. The tables that control how long data is kept, and data exports and deletions, include retention_holds, instance_retention_policy, project_data_jobs, subject_rights_jobs and suite_membership_parity_runs.

    Migration 0250 removed four tables that nothing read or wrote: contract_violations, ai_provenance_records, tenant_metric_snapshots and quality_gates. The older suite_memberships and suite_membership_events tables are still there. TestLookup still writes to them while the suite_membership_legacy_writes flag is on, which it is by default. See Suite-membership cutover.

    Migration 0252 added three columns to project_data_jobs, taking it from 24 to 27, for exports with run history and for imports uploaded in parts:

    • options: an export’s history window and its limit on runs.
    • upload: an import’s upload state while the job is uploading.
    • cleanup: the import’s record of what it wrote outside PostgreSQL.

    The status check gained uploading, and the ck_project_data_jobs_uploading_is_import check allows that status only on imports.

    For each table, the sections below give:

    • each column’s type, whether it can be empty (nullable), and whether it is part of the primary key;
    • defaults set by the database or by the application;
    • foreign keys, and what happens to a row when the row it links to is deleted;
    • indexes and constraints;
    • the relationships declared in the model, where there are any.

    The reference does not explain what each column means. Where a table has a description, it comes from the comment in the model’s code, and each section links to that model. A JSON or JSONB column does not by itself check what it stores. The application’s data formats, serializers and service rules can add checks beyond the database.

    Each store has its own job:

    • PostgreSQL is the main database. It holds the official application records and the records of work in progress.
    • MongoDB holds detailed evidence and documents.
    • File storage holds report files, attachments, evidence, exports, and knowledge or training files.
    • Redis (bundled as Valkey 8.1) holds the job queue, live data, temporary data, usage limits and caches.
    • ChromaDB (optional) holds search indexes.

    ChromaDB’s test-result search index is built from PostgreSQL, and a full re-index rebuilds it. Knowledge and agent memory are different. knowledge_chunks and agent_memory_entries keep their details and memory rows in PostgreSQL, but the chunk text and the vectors live only in ChromaDB. They come back only when you sync each knowledge source again, or as new investigations arrive. A row in PostgreSQL can depend on evidence stored outside it.

    Alembic migrations decide the layout of your installed database. In this version the latest migration is 0255. It is the last of 255 migrations under backend/migrations/versions/, which run one after another with no merges. Compare it with the output of alembic current on your database. The generated migration inventory on GitHub shows the history.

    A change to the database layout needs a migration, a model update, any affected data-format or serializer update, tests, regenerated references and a review of the clients. A model changed in the code does nothing to your database until its migration runs.

    Each table below links to its model in backend/app/models/postgres.py in this version, and the architecture sections link to the storage and lifecycle code. Three models are easy to mix up:

    • TestCase is one test’s result in one run.
    • CanonicalTestCase is the test’s permanent identity, which links the same test across runs.
    • ManagedTestCase is a test case that a person wrote.

    Keep them apart in queries, analytics and the data the screens use.

    This page was generated from the code of the current development version, not from a live database. So three dates can differ: when this page was published, when your installation was deployed, and when your database was last migrated.

    StoreDataWhat to know
    PostgreSQLProjects, members, runs, test results, each test’s permanent identity, written test cases, analyses, records of releases, reviews, policies and agent actions, the activity log and outboxesTransactions, foreign-key and unique constraints, and checked changes of state. The installed layout comes from the Alembic migrations
    MongoDBRaw report data such as Allure and TestNG, API request data, execution logs, AI data, pod events, run summaries, saved decision copies, reports and attempts, and live eventsFlexible documents with their own indexes. No transaction covers MongoDB and PostgreSQL together
    RedisThe Celery job queue and results, live event streams and the copies sent to viewers, session buffers, upload status, caches and usage limitsHow long items are kept, and what is removed when memory runs low, both matter. An expired upload status does not mean the run was deleted from PostgreSQL
    File storage (MinIO, S3 or local)Reports waiting to be processed, attachments and evidence, exports, and knowledge and training filesWriting a record and writing its file can fail separately. Set up the storage provider and retention so that they agree
    ChromaDBNumber summaries of text (embeddings) for search and document lookup, and indexes built from themOptional. The test-result search index is built from PostgreSQL and can be rebuilt; knowledge-chunk text and agent-memory vectors are stored only here. The embedding model and its version affect compatibility and offline packaging

    The main MongoDB collection names, and the indexes created at start-up, are listed in db/mongo.py. That list is not complete, because some services choose their own collection names, such as pipeline_event_log. Published decision documents are read from MongoDB by decision_report_service, so MongoDB’s evidence and reports must be part of every backup and restore. TestLookup tries to create the indexes at start-up and carries on if that fails, so check the logs for errors. The choice of file-storage provider is made in db/storage.py. One successful write to one store never proves that a whole operation finished.

    Diagram: Core relational conceptsDiagram: Core relational concepts
    Open the diagram full size
    Relationships in this diagram, as text
    1. PROJECT grants PROJECT_MEMBER (zero or more)
    2. USER joins PROJECT_MEMBER (zero or more)
    3. PROJECT contains TEST_RUN (zero or more)
    4. TEST_RUN records TEST_CASE (zero or more)
    5. TEST_CASE analyzed AI_ANALYSIS (zero or more)
    6. PROJECT organizes TEST_SUITE (zero or more)
    7. TEST_SUITE identifies CANONICAL_TEST_CASE (zero or more)
    8. CANONICAL_TEST_CASE links TEST_CASE (zero or more)
    9. PROJECT authors MANAGED_TEST_CASE (zero or more)
    10. TEST_RUN investigated AGENT_PIPELINE_RUN (zero or more)
    11. AGENT_PIPELINE_RUN records AGENT_STAGE_RESULT (zero or more)
    12. PROJECT plans RELEASE (zero or more)
    13. RELEASE evaluates RELEASE_GATE_DECISION (zero or more)

    The diagram shows only the main tables, not every foreign key. The tables below give the exact columns, the optional links and what happens on delete.

    TestCase is one test’s result in one run. CanonicalTestCase is the test’s permanent identity, which links the same test across runs. ManagedTestCase is a test case that a person wrote and reviewed, and it moves through stages from draft to archived. Keep these three apart in analytics and in the data types the screens use. A run can have suite, CI, release and environment details without every test belonging to a written test case.

    A test’s fingerprint comes from its class and test name. In the older helper it is a SHA-256 hash cut to 16 hex characters. A run’s identity is a separate hash that also takes its source into account, and the CI run URL, provider, repository and build stop separate jobs from being mixed up. Because the database refuses duplicates and each run ID is always worked out the same way, a report delivered twice ends up in the same run. Different sources can get build labels that tell them apart. Source code: ingestion.py and ingestion_pipeline.py.

    Pydantic checks request types, lengths, allowed values, list sizes and the rules that compare fields. SQLAlchemy types and database constraints add a second check. Services check which projects a user can reach, whether a change of stage is allowed, who may decide and which version is current, and the business rules that must always hold. A JSONB column accepts any nested data, so the version numbers of the agent and evidence formats matter.

    IDs (UUIDs) and dates are formatted by Pydantic and the route serializers, and API clients should use the ISO date and time formats the API lists. Units differ: test durations usually use milliseconds, retry and deadline settings use seconds, and each confidence field follows its own stated scale. Do not assume one confidence range for every data format. Model validators, model_dump, custom serializers and hand-built response dictionaries can all change how a field looks. Source code: serializers.

    An unknown environment means the environment was not recorded. TestLookup does not treat it as a shared “default” environment. The time the tests ran and the time TestLookup received them can differ, especially when a file is uploaded late. When TestLookup links runs to a release, it uses the details of where and when the tests ran, if it has them. Source code: run environment and execution time.

    Request handlers usually decide when a change is saved (committed). Services add their changes, activity-log entries and outbox jobs to the handler’s database session. Do not add a commit inside a service, because that splits one unit of work in two. PostgreSQL row locks, updates that check the current status, and lease checks stop two changes to the same record from clashing. When a release gets a new current decision, TestLookup first saves the old one as no longer current and only then marks the new one, so that the partial unique indexes always hold.

    A deletion covers several stores and the data built from them. Retention, deletion jobs and markers for deleted runs (tombstones) stop a late background job from bringing removed data back. Bulk operations must keep each project’s data separate and keep the activity log. A backup and a restore must cover all the main stores from the same moment. A backup of PostgreSQL alone does not hold all the evidence.

    A change to the database layout needs a migration, a model update, any data-format and serializer updates, tests, a regenerated data dictionary and a review of the clients. Your database has these changes only after its migrations run.

    Each table section below lists its columns with their type, whether they can be empty, whether they are part of the primary key, their defaults and their foreign keys. It then lists the table’s constraints and indexes, and links to its model. The migration inventory records how the installed database layout got there. Application checks and serializers can add rules beyond these, and a JSON or JSONB column does not by itself check what it stores.

    TableModelColumns
    access_audit_logsAccessAuditLog9
    agent_action_dispatch_outboxAgentActionDispatchOutbox12
    agent_action_ledgerAgentActionLedger22
    agent_child_dispatch_outboxAgentChildDispatchOutbox13
    agent_configsAgentConfig10
    agent_investigationsAgentInvestigation30
    agent_invocationsAgentInvocation18
    agent_memory_entriesAgentMemoryEntry21
    agent_pipeline_runsAgentPipelineRun24
    agent_runsAgentRun16
    agent_stage_resultsAgentStageResult36
    ai_analysisAIAnalysis17
    ai_analysis_versionsAIAnalysisVersion26
    ai_eval_baselinesAIEvalBaseline19
    ai_eval_datasetsAIEvalDataset10
    ai_eval_gate_runsAIEvalGateRun16
    ai_eval_item_outputsAIEvalItemOutput13
    ai_eval_reviewer_qualityAIEvalReviewerQuality25
    ai_eval_runsAIEvalRun30
    ai_eval_shadow_pairsAIEvalShadowPair16
    ai_feedbackAIFeedback12
    api_keysApiKey12
    app_settingsAppSetting6
    canonical_test_casesCanonicalTestCase20
    chat_messagesChatMessage6
    chat_sessionsChatSession9
    classification_hintsClassificationHint10
    compliance_packsCompliancePack15
    compliance_signing_keysComplianceSigningKey8
    coverage_snapshotsCoverageSnapshot8
    decision_report_eval_cyclesDecisionReportEvalCycle13
    decision_report_feedbackDecisionReportFeedback17
    decision_report_supersession_requestsDecisionReportSupersessionRequest14
    deep_findingsDeepFinding13
    defect_candidatesDefectCandidate19
    defectsDefect35
    deletion_jobsDeletionJob28
    digest_subscriptionsDigestSubscription20
    dismissed_duplicate_pairsDismissedDuplicatePair6
    drop_folder_buildsDropFolderBuild10
    duplicate_test_case_candidatesDuplicateTestCaseCandidate11
    evidence_artifactsEvidenceArtifact23
    failure_attributionFailureAttribution10
    failure_clustersFailureCluster11
    feature_flagsFeatureFlag10
    federated_identitiesFederatedIdentity10
    fix_attemptsFixAttempt21
    flaky_classifier_calibrationFlakyClassifierCalibration10
    flaky_coach_resultsFlakyCoachResult19
    flaky_detection_stateFlakyDetectionState13
    flaky_quarantine_requestsFlakyQuarantineRequest35
    flaky_scoreFlakyScore11
    generation_batchesGenerationBatch17
    generation_case_sourcesGenerationCaseSource12
    github_integrationsGitHubIntegration16
    gitlab_integrationsGitLabIntegration14
    identity_eventsIdentityEvent11
    insight_detector_statesInsightDetectorState7
    instance_retention_policyInstanceRetentionPolicy12
    integration_health_checksIntegrationHealthCheck8
    integration_probe_resultsIntegrationProbeResult9
    knowledge_chunksKnowledgeChunk12
    knowledge_sourcesKnowledgeSource16
    knowledge_sync_eventsKnowledgeSyncEvent12
    live_event_receiptsLiveEventReceipt9
    live_ingestion_attemptsLiveIngestionAttempt11
    live_projection_checkpointsLiveProjectionCheckpoint6
    live_sessionsLiveSession21
    managed_test_casesManagedTestCase52
    mfa_recovery_codesMfaRecoveryCode5
    model_versionsModelVersion16
    notification_logsNotificationLog24
    notification_preferencesNotificationPreference12
    notification_test_statesNotificationTestState10
    notification_transition_policiesNotificationTransitionPolicy8
    password_reset_tokensPasswordResetToken8
    perf_baselinesPerfBaseline14
    product_usage_eventsProductUsageEvent6
    project_activity_eventsProjectActivityEvent24
    project_data_jobsProjectDataJob27
    project_insightsProjectInsight16
    project_llm_quotaProjectLlmQuota12
    project_llm_usageProjectLlmUsage10
    project_membersProjectMember6
    project_retention_policiesProjectRetentionPolicy15
    project_workflow_defaultsProjectWorkflowDefault6
    projectsProject24
    quarantine_lifecycle_policiesQuarantineLifecyclePolicy11
    refresh_token_recordsRefreshTokenRecord8
    release_attribution_rulesReleaseAttributionRule11
    release_decisionsReleaseDecision21
    release_gate_decisionsReleaseGateDecision19
    release_gate_policiesReleaseGatePolicy13
    release_outcomesReleaseOutcome7
    release_phasesReleasePhase15
    release_test_run_linksReleaseTestRunLink9
    releasesRelease27
    report_exportsReportExport16
    report_share_linksReportShareLink12
    requirement_coverageRequirementCoverage8
    retention_holdsRetentionHold14
    review_requestsReviewRequest22
    run_baselinesRunBaseline11
    run_cadence_windowsRunCadenceWindow10
    run_cadencesRunCadence19
    run_commit_rangesRunCommitRange11
    run_comparison_reportsRunComparisonReport16
    run_diffsRunDiff5
    run_downstream_outboxRunDownstreamOutbox23
    run_intelligence_snapshotsRunIntelligenceSnapshot9
    run_tombstonesRunTombstone6
    saved_viewsSavedView11
    scim_group_mappingsSCIMGroupMapping4
    scim_group_membersSCIMGroupMember2
    scim_groupsSCIMGroup6
    scim_tokensSCIMToken10
    search_query_eventsSearchQueryEvent14
    secret_refsSecretRef11
    semantic_reindex_jobsSemanticReindexJob19
    service_ownership_rulesServiceOwnershipRule12
    settings_audit_logSettingsAuditLog8
    sso_configurationsSSOConfiguration24
    subject_rights_jobsSubjectRightsJob22
    suite_executionsSuiteExecution24
    suite_membership_eventsSuiteMembershipEvent11
    suite_membership_parity_runsSuiteMembershipParityRun15
    suite_membershipsSuiteMembership16
    suite_run_reviewsSuiteRunReview10
    systemic_cluster_epicsSystemicClusterEpic9
    systemic_flake_clusterSystemicFlakeCluster11
    systemic_flake_cluster_memberSystemicFlakeClusterMember5
    team_notification_channelsTeamNotificationChannel8
    tenant_onboarding_statusTenantOnboardingStatus7
    test_attachmentsTestAttachment8
    test_case_audit_logsTestCaseAuditLog15
    test_case_commentsTestCaseComment10
    test_case_historyTestCaseHistory8
    test_case_requirement_linksTestCaseRequirementLink9
    test_case_reviewsTestCaseReview13
    test_case_versionsTestCaseVersion30
    test_casesTestCase51
    test_execution_reviewsTestExecutionReview10
    test_health_recommendationsTestHealthRecommendation11
    test_plan_itemsTestPlanItem13
    test_plansTestPlan22
    test_runsTestRun50
    test_step_runsTestStepRun10
    test_stepsTestStep17
    test_strategiesTestStrategy24
    test_suite_ownersTestSuiteOwner6
    test_suitesTestSuite8
    timeline_notesTimelineNote9
    user_invitationsUserInvitation13
    user_ui_dismissalsUserUIDismissal4
    usersUser20
    value_metric_assumptionsValueMetricAssumptions8
    webhook_deliveriesWebhookDelivery18
    webhook_subscriptionsWebhookSubscription20
    workflow_definitionsWorkflowDefinition22
    workflow_replay_corpusWorkflowReplayCorpus14

    backend/app/models/postgres.py:4799

    Append-only audit trail for user role and project membership changes.

    Append-only by application convention, not by database enforcement — no UPDATE trigger, no revoked grant, no WORM storage. The property is held by the backend.audit-write-discipline quality gate (scripts/quality_gate.py), which fails CI on application code that UPDATEs an audit table or DELETEs from one outside services/retention_service.py.

    Rows are removed by the retention purge on the audit clock (ProjectRetentionPolicy.audit_days; floor 365 days, default 2555 ≈ 7 years, validated ≥ runs_days), and only for projects that explicitly enabled a policy.

    Beyond that limit, keeping the rows safe is the operator’s responsibility: direct Postgres credentials can still rewrite or drop rows. Real immutability comes from restricted UPDATE/DELETE grants for the app role, WORM / object-lock storage for shipped logs, and off-host backups.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    actor_user_idUUIDTrueFalse—users.id / SET NULL
    actor_nameVARCHAR(200)TrueFalse—
    target_user_idUUIDTrueFalse—users.id / SET NULL
    project_idUUIDTrueFalse—projects.id / SET NULL
    actionVARCHAR(50)FalseFalse—
    before_valueJSONTrueFalse—
    after_valueJSONTrueFalse—
    created_atDATETIMEFalseFalseserver=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: actor_user_id
    • ForeignKeyConstraint unnamed: project_id
    • ForeignKeyConstraint unnamed: target_user_id
    • PrimaryKeyConstraint unnamed: id
    • Index ix_aal_actor (unique=False): access_audit_logs.actor_user_id; options {}
    • Index ix_aal_created (unique=False): access_audit_logs.created_at; options {}
    • Index ix_aal_target (unique=False): access_audit_logs.target_user_id; options {}

    backend/app/models/postgres.py:6580

    A saved delivery job for an approved action. It never contains prompts.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    action_idUUIDFalseFalse—agent_action_ledger.id / CASCADE
    project_idUUIDFalseFalse—projects.id / CASCADE
    idempotency_keyVARCHAR(128)FalseFalse—
    statusVARCHAR(20)FalseFalseserver=pending; application=pending
    attemptsINTEGERFalseFalseserver=0; application=0
    next_attempt_atDATETIMETrueFalse—
    lease_expires_atDATETIMETrueFalse—
    sent_atDATETIMETrueFalse—
    last_errorVARCHAR(200)TrueFalse—
    created_atDATETIMEFalseFalseserver=now()
    updated_atDATETIMEFalseFalseserver=now(); onupdate=now()

    Constraints and indexes:

    • CheckConstraint ck_agent_action_outbox_attempts_nonnegative: attempts >= 0
    • CheckConstraint ck_agent_action_outbox_status: status IN ('pending', 'sending', 'sent', 'failed')
    • ForeignKeyConstraint unnamed: action_id
    • ForeignKeyConstraint unnamed: project_id
    • PrimaryKeyConstraint unnamed: id
    • UniqueConstraint unnamed: idempotency_key
    • UniqueConstraint uq_agent_action_outbox_action: action_id
    • Index ix_agent_action_outbox_status_next (unique=False): agent_action_dispatch_outbox.status, agent_action_dispatch_outbox.next_attempt_at; options {}

    backend/app/models/postgres.py:6121

    Append-only proposal/execution ledger for agent-originated actions.

    The ledger is the saved approval record for changes to outside systems. The request payload is sanitized and hashed at proposal time; execution and rollback outcomes are recorded separately so a caller can never replace a proposal with a different payload under the same idempotency key.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    project_idUUIDFalseFalse—projects.id / CASCADE
    test_run_idUUIDTrueFalse—test_runs.id / SET NULL
    pipeline_run_idUUIDTrueFalse—agent_pipeline_runs.id / SET NULL
    actor_user_idUUIDTrueFalse—users.id / SET NULL
    action_typeVARCHAR(60)FalseFalse—
    target_typeVARCHAR(60)FalseFalse—
    target_idVARCHAR(255)TrueFalse—
    statusVARCHAR(20)FalseFalseserver=proposed; application=proposed
    approval_requiredBOOLEANFalseFalseserver=true; application=True
    idempotency_keyVARCHAR(128)FalseFalse—
    request_sha256VARCHAR(64)FalseFalse—
    request_payloadJSONBFalseFalseapplication=dict
    approved_byUUIDTrueFalse—users.id / SET NULL
    approved_atDATETIMETrueFalse—
    execution_started_atDATETIMETrueFalse—
    execution_completed_atDATETIMETrueFalse—
    result_payloadJSONBTrueFalse—
    rollback_payloadJSONBTrueFalse—
    error_codeVARCHAR(100)TrueFalse—
    created_atDATETIMEFalseFalseserver=now()
    updated_atDATETIMEFalseFalseserver=now(); onupdate=now()

    Constraints and indexes:

    • CheckConstraint ck_agent_action_request_hash: request_sha256 ~ '^[0-9a-f]{64}$'
    • CheckConstraint ck_agent_action_status: status IN ('proposed', 'pending_review', 'approved', 'executing', 'executed', 'failed', 'rejected', 'rolled_back')
    • ForeignKeyConstraint unnamed: actor_user_id
    • ForeignKeyConstraint unnamed: approved_by
    • ForeignKeyConstraint unnamed: pipeline_run_id
    • ForeignKeyConstraint unnamed: project_id
    • ForeignKeyConstraint unnamed: test_run_id
    • PrimaryKeyConstraint unnamed: id
    • UniqueConstraint uq_agent_action_project_idempotency: project_id, idempotency_key
    • Index ix_agent_action_project_created (unique=False): agent_action_ledger.project_id, agent_action_ledger.created_at; options {}
    • Index ix_agent_action_status (unique=False): agent_action_ledger.project_id, agent_action_ledger.status; options {}
    • Index ix_agent_action_target (unique=False): agent_action_ledger.project_id, agent_action_ledger.target_type, agent_action_ledger.target_id; options {}

    backend/app/models/postgres.py:2414

    A saved dispatch record for an Investigator child that the planner started.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    spawn_keyVARCHAR(64)FalseFalse—
    investigation_idUUIDFalseFalse—agent_investigations.id / CASCADE
    parent_pipeline_run_idUUIDFalseFalse—agent_pipeline_runs.id / CASCADE
    project_idUUIDFalseFalse—projects.id / CASCADE
    run_idUUIDFalseFalse—test_runs.id / CASCADE
    statusVARCHAR(20)FalseFalseserver=pending; application=pending
    attemptsINTEGERFalseFalseserver=0; application=0
    next_attempt_atDATETIMETrueFalse—
    sent_atDATETIMETrueFalse—
    created_atDATETIMEFalseFalseserver=now()
    updated_atDATETIMEFalseFalseserver=now(); onupdate=now()
    last_errorTEXTTrueFalse—

    Constraints and indexes:

    • CheckConstraint ck_agent_child_outbox_attempts_nonnegative: attempts >= 0
    • CheckConstraint ck_agent_child_outbox_spawn_key_sha256: spawn_key ~ '^[0-9a-f]{64}$'
    • CheckConstraint ck_agent_child_outbox_status: status IN ('pending', 'sending', 'sent', 'failed')
    • ForeignKeyConstraint unnamed: investigation_id
    • ForeignKeyConstraint unnamed: parent_pipeline_run_id
    • ForeignKeyConstraint unnamed: project_id
    • ForeignKeyConstraint unnamed: run_id
    • PrimaryKeyConstraint unnamed: id
    • UniqueConstraint unnamed: investigation_id
    • UniqueConstraint unnamed: spawn_key
    • Index ix_agent_child_dispatch_status_next_attempt (unique=False): agent_child_dispatch_outbox.status, agent_child_dispatch_outbox.next_attempt_at; options {}

    backend/app/models/postgres.py:6367

    A project’s configuration of one agent (architecture E4.1, section 4.2).

    mode and enabled are columns; config holds the rest of the validated AgentConfigV1 document. A missing row means the defaults. Every write bumps config_version.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    project_idUUIDFalseFalse—projects.id / CASCADE
    agent_idVARCHAR(80)FalseFalse—
    enabledBOOLEANFalseFalseserver=true; application=True
    modeVARCHAR(10)FalseFalseserver=shadow; application=shadow
    configJSONBFalseFalseserver='{}'::jsonb; application=dict
    config_versionINTEGERFalseFalseserver=1; application=1
    updated_byUUIDTrueFalse—users.id / SET NULL
    created_atDATETIMEFalseFalseserver=now()
    updated_atDATETIMEFalseFalseserver=now()

    Constraints and indexes:

    • CheckConstraint ck_agent_configs_mode: mode IN ('shadow', 'suggest', 'act')
    • CheckConstraint ck_agent_configs_version_positive: config_version >= 1
    • ForeignKeyConstraint unnamed: project_id
    • ForeignKeyConstraint unnamed: updated_by
    • PrimaryKeyConstraint unnamed: id
    • UniqueConstraint uq_agent_configs_project_agent: project_id, agent_id

    backend/app/models/postgres.py:2270

    One Investigator run against a test run (shadow mode).

    Persists the full InvestigationDetail wire shape served by GET /api/v1/investigations/{id}: lifecycle status, mode, trigger, budget + spend JSONB, the per-hypothesis results (written incrementally as hypothesis nodes complete — the UI polls), the synthesis verdict, the prompt-version snapshot, and the cooperative-cancel flag.

    One ACTIVE investigation per run is enforced by the partial unique index uq_agent_investigations_one_active_per_run (migration 0108) over ACTIVE_STATUSES — the API’s 409 is backed by a real constraint.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    project_idUUIDFalseFalse—projects.id / CASCADE
    run_idUUIDFalseFalse—test_runs.id / CASCADE
    scope_typeVARCHAR(20)FalseFalseserver=run; application=run
    failure_cluster_idUUIDTrueFalse—failure_clusters.id / CASCADE
    cluster_scope_sha256VARCHAR(64)TrueFalse—
    cluster_member_test_idsJSONBFalseFalseserver='[]'::jsonb; application=list
    parent_pipeline_run_idUUIDTrueFalse—agent_pipeline_runs.id / CASCADE
    parent_task_idVARCHAR(255)TrueFalse—
    spawn_lineage_idUUIDTrueFalse—
    spawn_depthINTEGERFalseFalseserver=0; application=0
    spawn_keyVARCHAR(64)TrueFalse—
    selection_reasonTEXTTrueFalse—
    statusVARCHAR(20)FalseFalseapplication=queued
    modeVARCHAR(10)FalseFalseapplication=shadow
    triggered_byVARCHAR(40)FalseFalseapplication=manual
    requested_byUUIDTrueFalse—users.id / SET NULL
    budgetJSONBFalseFalseapplication=dict
    spendJSONBFalseFalseapplication=dict
    hypothesesJSONBFalseFalseapplication=list
    verdictJSONBTrueFalse—
    prompt_versionsJSONBTrueFalse—
    model_infoJSONBTrueFalse—
    cancel_requestedBOOLEANFalseFalseapplication=False
    cancelled_byVARCHAR(255)TrueFalse—
    errorTEXTTrueFalse—
    started_atDATETIMETrueFalse—
    completed_atDATETIMETrueFalse—
    created_atDATETIMEFalseFalseserver=now()
    updated_atDATETIMEFalseFalseserver=now(); onupdate=now()

    Constraints and indexes:

    • CheckConstraint ck_agent_investigation_cluster_members_array: jsonb_typeof(cluster_member_test_ids) = 'array'
    • CheckConstraint ck_agent_investigation_cluster_scope_sha256: cluster_scope_sha256 IS NULL OR cluster_scope_sha256 ~ '^[0-9a-f]{64}$'
    • CheckConstraint ck_agent_investigation_scope_consistency: (scope_type = 'run' AND failure_cluster_id IS NULL AND cluster_scope_sha256 IS NULL AND parent_pipeline_run_id IS NULL AND parent_task_id IS NULL AND spawn_lineage_id IS NULL AND spawn_key IS NULL AND spawn_depth = 0 AND jsonb_array_length(cluster_member_test_ids) = 0) OR (scope_type = 'failure_cluster' AND failure_cluster_id IS NOT NULL AND cluster_scope_sha256 IS NOT NULL AND parent_pipeline_run_id IS NOT NULL AND parent_task_id IS NOT NULL AND length(parent_task_id) > 0 AND spawn_lineage_id IS NOT NULL AND spawn_key IS NOT NULL AND spawn_depth >= 1 AND jsonb_array_length(cluster_member_test_ids) > 0)
    • CheckConstraint ck_agent_investigation_scope_type: scope_type IN ('run', 'failure_cluster')
    • CheckConstraint ck_agent_investigation_spawn_depth: spawn_depth >= 0 AND spawn_depth <= 8
    • CheckConstraint ck_agent_investigation_spawn_key_sha256: spawn_key IS NULL OR spawn_key ~ '^[0-9a-f]{64}$'
    • ForeignKeyConstraint unnamed: failure_cluster_id
    • ForeignKeyConstraint unnamed: parent_pipeline_run_id
    • ForeignKeyConstraint unnamed: project_id
    • ForeignKeyConstraint unnamed: requested_by
    • ForeignKeyConstraint unnamed: run_id
    • PrimaryKeyConstraint unnamed: id
    • Index ix_agent_investigations_project_created (unique=False): agent_investigations.project_id, agent_investigations.created_at; options {}
    • Index ix_agent_investigations_run (unique=False): agent_investigations.run_id; options {}
    • Index uq_agent_investigations_active_failure_cluster (unique=True): agent_investigations.parent_pipeline_run_id, agent_investigations.failure_cluster_id; options {"postgresql_where": "scope_type = 'failure_cluster' AND status IN ('queued', 'running', 'synthesizing')"}
    • Index uq_agent_investigations_active_run_scope (unique=True): agent_investigations.run_id; options {"postgresql_where": "scope_type = 'run' AND status IN ('queued', 'running', 'synthesizing')"}
    • Index ux_agent_investigations_spawn_key (unique=True): agent_investigations.spawn_key; options {"postgresql_where": "spawn_key IS NOT NULL"}

    backend/app/models/postgres.py:6287

    One call of a single agent through the public API (architecture E1.2).

    Carries no status of its own: the invocation runs as the pipeline run named by pipeline_run_id (minted here before the worker creates that run, so it is not a foreign key), and status, attempts and review are read from it.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    project_idUUIDFalseFalse—projects.id / CASCADE
    agent_idVARCHAR(80)FalseFalse—
    stage_nameVARCHAR(60)FalseFalse—
    test_run_idUUIDFalseFalse—test_runs.id / CASCADE
    pipeline_run_idUUIDFalseFalse—
    workflow_typeVARCHAR(20)FalseFalse—
    modeVARCHAR(10)FalseFalseserver=async; application=async
    requested_byUUIDTrueFalse—users.id / SET NULL
    correlation_idVARCHAR(128)TrueFalse—
    created_atDATETIMEFalseFalseserver=now()
    dispatched_atDATETIMETrueFalse—
    cancel_requestedBOOLEANFalseFalseserver=false; application=False
    idempotency_keyVARCHAR(128)TrueFalse—
    request_sha256VARCHAR(64)TrueFalse—
    resolved_config_snapshotJSONBTrueFalse—
    input_payloadJSONBTrueFalse—
    input_sha256VARCHAR(64)TrueFalse—

    Constraints and indexes:

    • CheckConstraint ck_agent_invocations_input_pair: (input_payload IS NULL) = (input_sha256 IS NULL)
    • CheckConstraint ck_agent_invocations_input_sha256: input_sha256 IS NULL OR input_sha256 ~ '^[0-9a-f]{64}$'
    • CheckConstraint ck_agent_invocations_mode: mode IN ('sync', 'async')
    • ForeignKeyConstraint unnamed: project_id
    • ForeignKeyConstraint unnamed: requested_by
    • ForeignKeyConstraint unnamed: test_run_id
    • PrimaryKeyConstraint unnamed: id
    • Index ix_agent_invocations_project_created (unique=False): agent_invocations.project_id, agent_invocations.created_at; options {}
    • Index ix_agent_invocations_run_agent (unique=False): agent_invocations.test_run_id, agent_invocations.agent_id, agent_invocations.created_at; options {}
    • Index ux_agent_invocations_pipeline_run (unique=True): agent_invocations.pipeline_run_id; options {}
    • Index ux_agent_invocations_scoped_idempotency_key (unique=True): agent_invocations.requested_by, agent_invocations.project_id, agent_invocations.agent_id, agent_invocations.idempotency_key; options {"postgresql_where": "idempotency_key IS NOT NULL"}

    backend/app/models/postgres.py:6066

    Unified memory linking a run/snapshot to related entities (clusters, defects, release decisions, ownership) for project-scoped historical recall.

    Each entry represents a single relationship discovered during a pipeline run. Agents query these entries to retrieve similar historical failures, prior defect decisions, and release outcomes for the same project.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    project_idUUIDFalseFalse—projects.id / CASCADE
    run_idUUIDFalseFalse—test_runs.id / CASCADE
    pipeline_run_idUUIDTrueFalse—agent_pipeline_runs.id / SET NULL
    entity_typeVARCHAR(50)FalseFalse—
    entity_idVARCHAR(200)FalseFalse—
    error_signatureTEXTTrueFalse—
    failure_categoryVARCHAR(50)TrueFalse—
    root_cause_summaryTEXTTrueFalse—
    payloadJSONTrueFalse—
    confidenceINTEGERTrueFalse—
    resolutionVARCHAR(50)TrueFalse—
    source_typeVARCHAR(40)FalseFalseapplication=pipeline_agent
    trust_levelVARCHAR(30)FalseFalseapplication=derived
    lifecycle_statusVARCHAR(20)FalseFalseapplication=active
    source_snapshot_idVARCHAR(128)TrueFalse—
    source_hashVARCHAR(64)TrueFalse—
    expires_atDATETIMETrueFalse—
    superseded_by_idUUIDTrueFalse—agent_memory_entries.id / SET NULL
    superseded_atDATETIMETrueFalse—
    created_atDATETIMEFalseFalseserver=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: pipeline_run_id
    • ForeignKeyConstraint unnamed: project_id
    • ForeignKeyConstraint unnamed: run_id
    • ForeignKeyConstraint unnamed: superseded_by_id
    • PrimaryKeyConstraint unnamed: id
    • Index ix_ame_created (unique=False): agent_memory_entries.created_at; options {}
    • Index ix_ame_entity (unique=False): agent_memory_entries.entity_type, agent_memory_entries.entity_id; options {}
    • Index ix_ame_project_entity (unique=False): agent_memory_entries.project_id, agent_memory_entries.entity_type; options {}
    • Index ix_ame_project_id (unique=False): agent_memory_entries.project_id; options {}
    • Index ix_ame_run_id (unique=False): agent_memory_entries.run_id; options {}

    backend/app/models/postgres.py:2582

    Tracks a single execution of the multi-agent pipeline for a test run.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    test_run_idUUIDFalseFalse—test_runs.id / CASCADE
    requested_byUUIDTrueFalse—users.id / SET NULL
    parent_pipeline_run_idUUIDTrueFalse—agent_pipeline_runs.id / SET NULL
    parent_task_idVARCHAR(255)TrueFalse—
    spawn_depthINTEGERFalseFalseserver=0; application=0
    workflow_typeVARCHAR(20)FalseFalseapplication=offline
    statusVARCHAR(20)FalseFalseapplication=pending
    started_atDATETIMETrueFalse—
    completed_atDATETIMETrueFalse—
    errorTEXTTrueFalse—
    created_atDATETIMEFalseFalseserver=now()
    attemptINTEGERFalseFalseserver=1; application=1
    max_attemptsINTEGERFalseFalseserver=5; application=5
    next_retry_atDATETIMETrueFalse—
    lease_ownerVARCHAR(255)TrueFalse—
    lease_expires_atDATETIMETrueFalse—
    fencing_tokenVARCHAR(64)TrueFalse—
    heartbeat_atDATETIMETrueFalse—
    cancel_requestedBOOLEANFalseFalseserver=false; application=False
    review_policyVARCHAR(40)FalseFalseserver=human_required; application=human_required
    rerun_ofUUIDTrueFalse—agent_pipeline_runs.id / SET NULL
    execution_metadataJSONTrueFalse—
    provenance_metadataJSONTrueFalse—

    Constraints and indexes:

    • CheckConstraint ck_agent_pipeline_spawn_depth: spawn_depth >= 0 AND spawn_depth <= 8
    • CheckConstraint ck_agent_pipeline_status: status IN ('pending', 'running', 'retry_wait', 'completed', 'passed', 'failed')
    • ForeignKeyConstraint unnamed: parent_pipeline_run_id
    • ForeignKeyConstraint unnamed: requested_by
    • ForeignKeyConstraint unnamed: rerun_of
    • ForeignKeyConstraint unnamed: test_run_id
    • PrimaryKeyConstraint unnamed: id
    • Index ix_pipeline_runs_status (unique=False): agent_pipeline_runs.status; options {}
    • Index ix_pipeline_runs_test_run (unique=False): agent_pipeline_runs.test_run_id; options {}

    ORM navigation and cascade declarations:

    stages: Mapped[list['AgentStageResult']] = relationship('AgentStageResult', back_populates='pipeline_run', cascade='all, delete-orphan')

    backend/app/models/postgres.py:2480

    Agent activity ledger: one row per agent execution — what ran, why, what it proposed, what it actually did (always nothing in shadow/suggest), and what it cost.

    Written on every investigation completion/cancel/failure; mirrored as a saved event in the Mongo pipeline event log; copied into release compliance packs (agent_activity.json).

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    agent_idVARCHAR(50)FalseFalse—
    project_idUUIDFalseFalse—projects.id / CASCADE
    run_idUUIDTrueFalse—test_runs.id / SET NULL
    modeVARCHAR(10)FalseFalseapplication=shadow
    triggerVARCHAR(40)FalseFalseapplication=manual
    statusVARCHAR(20)FalseFalse—
    summaryTEXTFalseFalseapplication=
    actions_proposedJSONBFalseFalseapplication=list
    actions_takenJSONBFalseFalseapplication=list
    tokensINTEGERFalseFalseapplication=0
    cost_usdFLOATFalseFalseapplication=0.0
    duration_msINTEGERFalseFalseapplication=0
    prompt_registry_digestVARCHAR(64)TrueFalse—
    details_pathVARCHAR(500)TrueFalse—
    created_atDATETIMEFalseFalseserver=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: project_id
    • ForeignKeyConstraint unnamed: run_id
    • PrimaryKeyConstraint unnamed: id
    • Index ix_agent_runs_project_agent_created (unique=False): agent_runs.project_id, agent_runs.agent_id, agent_runs.created_at; options {}

    backend/app/models/postgres.py:2651

    Per-stage result for an AgentPipelineRun.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    pipeline_run_idUUIDFalseFalse—agent_pipeline_runs.id / CASCADE
    task_keyVARCHAR(255)TrueFalse—
    capability_idVARCHAR(120)TrueFalse—
    parent_task_keyVARCHAR(255)TrueFalse—
    failure_cluster_idUUIDTrueFalse—failure_clusters.id / SET NULL
    attemptINTEGERFalseFalseserver=1; application=1
    selectedBOOLEANTrueFalse—
    requiredBOOLEANTrueFalse—
    dependenciesJSONTrueFalse—
    allocated_budgetJSONTrueFalse—
    stop_reasonVARCHAR(100)TrueFalse—
    idempotency_keyVARCHAR(64)TrueFalse—
    lease_expires_atDATETIMETrueFalse—
    stage_nameVARCHAR(50)FalseFalse—
    statusVARCHAR(20)FalseFalseapplication=pending
    started_atDATETIMETrueFalse—
    completed_atDATETIMETrueFalse—
    result_dataJSONTrueFalse—
    errorTEXTTrueFalse—
    skipped_reasonTEXTTrueFalse—
    execution_pathVARCHAR(50)TrueFalse—
    fallback_usedBOOLEANTrueFalse—
    checkpoint_dataJSONTrueFalse—
    input_tokensINTEGERTrueFalse—
    output_tokensINTEGERTrueFalse—
    total_tokensINTEGERTrueFalse—
    llm_calls_countINTEGERTrueFalse—
    cost_usdFLOATTrueFalse—
    error_categoryVARCHAR(30)TrueFalse—
    confidence_scoreINTEGERTrueFalse—
    evidence_countINTEGERTrueFalse—
    route_rationaleTEXTTrueFalse—
    decision_logJSONTrueFalse—
    fallback_reasonVARCHAR(200)TrueFalse—
    analysis_modeVARCHAR(20)TrueFalse—

    Constraints and indexes:

    • CheckConstraint ck_agent_stage_attempt_positive: attempt >= 1
    • CheckConstraint ck_agent_stage_idempotency_sha256: idempotency_key IS NULL OR idempotency_key ~ '^[0-9a-f]{64}$'
    • ForeignKeyConstraint unnamed: failure_cluster_id
    • ForeignKeyConstraint unnamed: pipeline_run_id
    • PrimaryKeyConstraint unnamed: id
    • Index ix_agent_stage_results_stage_status (unique=False): agent_stage_results.stage_name, agent_stage_results.status; options {}
    • Index ix_stage_results_pipeline (unique=False): agent_stage_results.pipeline_run_id; options {}
    • Index ux_agent_stage_pipeline_idempotency (unique=True): agent_stage_results.pipeline_run_id, agent_stage_results.idempotency_key; options {"postgresql_where": "idempotency_key IS NOT NULL"}
    • Index ux_agent_stage_pipeline_task_key (unique=True): agent_stage_results.pipeline_run_id, agent_stage_results.task_key; options {"postgresql_where": "task_key IS NOT NULL"}

    ORM navigation and cascade declarations:

    pipeline_run: Mapped['AgentPipelineRun'] = relationship('AgentPipelineRun', back_populates='stages')

    backend/app/models/postgres.py:1523

    Stored AI triage results per test case: the LATEST answer (fast read).

    created_at means “last generated at”: the pipeline upsert rewrites it, with llm_provider / llm_model, on every re-analysis, so readers that order by it pick the newest analysis. Every earlier answer is kept as an immutable row of :class:AIAnalysisVersion.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    test_case_idUUIDFalseFalse—test_cases.id / CASCADE
    root_cause_summaryTEXTTrueFalse—
    failure_categoryVARCHAR(30)TrueFalse—
    backend_error_foundBOOLEANFalseFalseapplication=False
    pod_issue_foundBOOLEANFalseFalseapplication=False
    is_flakyBOOLEANFalseFalseapplication=False
    confidence_scoreINTEGERTrueFalse—
    recommended_actionsJSONTrueFalse—
    evidence_referencesJSONTrueFalse—
    tools_usedJSONTrueFalse—
    role_actionsJSONTrueFalse—
    llm_providerVARCHAR(50)TrueFalse—
    llm_modelVARCHAR(100)TrueFalse—
    requires_human_reviewBOOLEANFalseFalseapplication=False
    created_atDATETIMEFalseFalseserver=now()
    routing_metadataJSONBTrueFalse—

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: test_case_id
    • PrimaryKeyConstraint unnamed: id
    • UniqueConstraint unnamed: test_case_id

    ORM navigation and cascade declarations:

    test_case: Mapped['TestCase'] = relationship('TestCase', back_populates='ai_analysis')

    backend/app/models/postgres.py:1565

    One immutable version of a test result’s AI analysis (migration 0205).

    ai_analysis holds the latest answer per test case; every answer any of the four writers produced (pipeline, single-test analyze, human correction, bulk reclassify) is appended here, with its trigger, its pipeline or actor and a content_sha256 (ai_history_core.analysis_content_sha256, byte-identical to ANALYSIS_CONTENT_SHA256_SQL).

    Dedup: a pipeline version is unique per (test_case_id, pipeline_run_id) (a replayed pipeline inserts nothing); the other triggers are skipped when the hash equals the latest version’s. Rows die with their run (runs clock, CASCADE) and are capped per test by the retention policy. test_run_id / project_id / test_fingerprint are denormalised so retention, export and history lists need no join.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueserver=gen_random_uuid(); application=uuid4
    test_case_idUUIDFalseFalse—test_cases.id / CASCADE
    test_run_idUUIDFalseFalse—test_runs.id / CASCADE
    project_idUUIDFalseFalse—projects.id / CASCADE
    test_fingerprintVARCHAR(64)FalseFalse—
    versionINTEGERFalseFalse—
    triggerVARCHAR(24)FalseFalse—
    pipeline_run_idUUIDTrueFalse—agent_pipeline_runs.id / SET NULL
    actor_user_idUUIDTrueFalse—users.id / SET NULL
    content_sha256VARCHAR(64)FalseFalse—
    failure_categoryVARCHAR(30)TrueFalse—
    confidence_scoreINTEGERTrueFalse—
    root_cause_summaryTEXTTrueFalse—
    is_flakyBOOLEANFalseFalseserver=false; application=False
    backend_error_foundBOOLEANFalseFalseserver=false; application=False
    pod_issue_foundBOOLEANFalseFalseserver=false; application=False
    requires_human_reviewBOOLEANFalseFalseserver=false; application=False
    recommended_actionsJSONTrueFalse—
    evidence_referencesJSONTrueFalse—
    tools_usedJSONTrueFalse—
    role_actionsJSONTrueFalse—
    llm_providerVARCHAR(50)TrueFalse—
    llm_modelVARCHAR(100)TrueFalse—
    analysis_modeVARCHAR(20)TrueFalse—
    provenanceJSONBFalseFalseserver='{}'::jsonb; application=dict
    created_atDATETIMEFalseFalseserver=now()

    Constraints and indexes:

    • CheckConstraint ck_ai_analysis_versions_version: version >= 1
    • ForeignKeyConstraint unnamed: actor_user_id
    • ForeignKeyConstraint unnamed: pipeline_run_id
    • ForeignKeyConstraint unnamed: project_id
    • ForeignKeyConstraint unnamed: test_case_id
    • ForeignKeyConstraint unnamed: test_run_id
    • PrimaryKeyConstraint unnamed: id
    • UniqueConstraint uq_ai_analysis_versions_case_version: test_case_id, version
    • Index ix_ai_analysis_versions_pipeline (unique=False): ai_analysis_versions.pipeline_run_id; options {"postgresql_where": "pipeline_run_id IS NOT NULL"}
    • Index ix_ai_analysis_versions_project_fp (unique=False): ai_analysis_versions.project_id, ai_analysis_versions.test_fingerprint, created_at DESC; options {}
    • Index ix_ai_analysis_versions_run (unique=False): ai_analysis_versions.test_run_id; options {}
    • Index uq_ai_analysis_versions_case_pipeline (unique=True): ai_analysis_versions.test_case_id, ai_analysis_versions.pipeline_run_id; options {"postgresql_where": "trigger = 'pipeline' AND pipeline_run_id IS NOT NULL"}

    backend/app/models/postgres.py:5783

    The reference metrics for each agent and task_type that new results are compared with at the gate.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    task_typeVARCHAR(50)FalseFalse—
    agent_nameVARCHAR(100)FalseFalse—
    prompt_versionVARCHAR(50)FalseFalseapplication=v1
    model_nameVARCHAR(200)TrueFalse—
    baseline_accuracyFLOATTrueFalse—
    baseline_precisionFLOATTrueFalse—
    baseline_recallFLOATTrueFalse—
    baseline_f1FLOATTrueFalse—
    min_accuracyFLOATFalseFalseapplication=0.8
    min_f1FLOATFalseFalseapplication=0.75
    max_regression_pctFLOATFalseFalseapplication=5.0
    eval_run_idUUIDTrueFalse—ai_eval_runs.id / SET NULL
    dataset_idUUIDTrueFalse—ai_eval_datasets.id / SET NULL
    is_activeBOOLEANFalseFalseapplication=True
    created_byUUIDTrueFalse—users.id / SET NULL
    created_atDATETIMEFalseFalseserver=now()
    updated_atDATETIMEFalseFalseserver=now(); onupdate=now()
    metric_kindVARCHAR(20)FalseFalseserver='label_agreement'; application=label_agreement

    Constraints and indexes:

    • CheckConstraint ck_aeb_metric_kind: metric_kind IN ('label_agreement','replay')
    • ForeignKeyConstraint unnamed: created_by
    • ForeignKeyConstraint unnamed: dataset_id
    • ForeignKeyConstraint unnamed: eval_run_id
    • PrimaryKeyConstraint unnamed: id
    • UniqueConstraint uq_aeb_task_agent_prompt: task_type, agent_name, prompt_version
    • Index ix_aeb_task_agent (unique=False): ai_eval_baselines.task_type, ai_eval_baselines.agent_name; options {}

    backend/app/models/postgres.py:5657

    Labeled evaluation dataset for measuring AI quality over time.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    nameVARCHAR(255)FalseFalse—
    descriptionTEXTTrueFalse—
    task_typeVARCHAR(50)FalseFalse—
    itemsJSONFalseFalseapplication=list
    item_countINTEGERFalseFalseapplication=0
    created_byUUIDTrueFalse—users.id / SET NULL
    is_activeBOOLEANFalseFalseapplication=True
    created_atDATETIMEFalseFalseserver=now()
    updated_atDATETIMEFalseFalseserver=now(); onupdate=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: created_by
    • PrimaryKeyConstraint unnamed: id
    • Index ix_aed_task_type (unique=False): ai_eval_datasets.task_type; options {}

    backend/app/models/postgres.py:5820

    Historical record of an agent-stack release gate decision.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    change_idVARCHAR(200)FalseFalse—
    statusVARCHAR(20)FalseFalse—
    manifest_checksum_sha256VARCHAR(64)FalseFalse—
    manifestJSONBFalseFalse—
    gate_resultsJSONBFalseFalseapplication=list
    blocking_gatesJSONBFalseFalseapplication=list
    version_changesJSONBFalseFalseapplication=list
    gate_typeVARCHAR(40)TrueFalse—
    project_idUUIDTrueFalse—projects.id / CASCADE
    agent_idVARCHAR(80)TrueFalse—
    baseline_tierVARCHAR(20)TrueFalse—
    candidate_tierVARCHAR(20)TrueFalse—
    sample_countINTEGERTrueFalse—
    evaluated_byUUIDTrueFalse—users.id / SET NULL
    evaluated_atDATETIMEFalseFalseserver=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: evaluated_by
    • ForeignKeyConstraint unnamed: project_id
    • PrimaryKeyConstraint unnamed: id
    • Index ix_aeg_change_id (unique=False): ai_eval_gate_runs.change_id; options {}
    • Index ix_aeg_evaluated_at (unique=False): ai_eval_gate_runs.evaluated_at; options {}
    • Index ix_aeg_manifest_checksum (unique=False): ai_eval_gate_runs.manifest_checksum_sha256; options {}
    • Index ix_aeg_status (unique=False): ai_eval_gate_runs.status; options {}
    • Index ix_aeg_tier_comparison_lookup (unique=False): ai_eval_gate_runs.project_id, ai_eval_gate_runs.agent_id, ai_eval_gate_runs.candidate_tier, ai_eval_gate_runs.evaluated_at; options {"postgresql_where": "gate_type = 'tier_comparison'"}

    backend/app/models/postgres.py:5744

    One replayed item of a candidate evaluation run (migration 0231).

    Written one at a time by the worker (INSERT … ON CONFLICT (eval_run_id, item_index) DO NOTHING); deleting the run (or its dataset) deletes them. expected / output are stored sanitized and re-sanitized on read.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    eval_run_idUUIDFalseFalse—ai_eval_runs.id / CASCADE
    item_indexINTEGERFalseFalse—
    item_keyVARCHAR(160)TrueFalse—
    statusVARCHAR(20)FalseFalse—
    error_codeVARCHAR(100)TrueFalse—
    expectedJSONBFalseFalse—
    outputJSONBTrueFalse—
    scoreJSONBFalseFalseserver='{}'::jsonb; application=dict
    latency_msINTEGERTrueFalse—
    tokensINTEGERTrueFalse—
    mode_usedVARCHAR(20)TrueFalse—
    created_atDATETIMEFalseFalseserver=now()

    Constraints and indexes:

    • CheckConstraint ck_aeio_item_index_nonnegative: item_index >= 0
    • CheckConstraint ck_aeio_status: status IN ('correct','incorrect','unscored','error')
    • ForeignKeyConstraint unnamed: eval_run_id
    • PrimaryKeyConstraint unnamed: id
    • UniqueConstraint uq_aeio_run_item: eval_run_id, item_index
    • Index ix_aeio_run_status (unique=False): ai_eval_item_outputs.eval_run_id, ai_eval_item_outputs.status; options {}

    backend/app/models/postgres.py:5887

    A G3 reviewer-quality observation batch or scheduled 30-day rollup.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    project_idUUIDFalseFalse—projects.id / CASCADE
    agent_idVARCHAR(80)FalseFalse—
    sourceVARCHAR(24)FalseFalse—
    statusVARCHAR(24)FalseFalse—
    manifest_checksum_sha256VARCHAR(64)FalseFalse—
    window_started_atDATETIMEFalseFalse—
    window_ended_atDATETIMEFalseFalse—
    mutation_observationsJSONBFalseFalseapplication=list
    clean_observationsJSONBFalseFalseapplication=list
    human_outcomesJSONBFalseFalseapplication=list
    metricsJSONBFalseFalseapplication=list
    regressionsJSONBFalseFalseapplication=list
    semantic_sample_countsJSONBFalseFalseapplication=dict
    clean_sample_countINTEGERFalseFalseapplication=0
    human_outcome_countINTEGERFalseFalseapplication=0
    deterministic_recallFLOATTrueFalse—
    second_model_recallFLOATTrueFalse—
    recall_deltaFLOATTrueFalse—
    false_flag_rateFLOATTrueFalse—
    false_omission_rateFLOATTrueFalse—
    auto_disable_eligibleBOOLEANFalseFalseapplication=False
    auto_disable_appliedBOOLEANFalseFalseapplication=False
    evaluated_byUUIDTrueFalse—users.id / SET NULL
    evaluated_atDATETIMEFalseFalseserver=now()

    Constraints and indexes:

    • CheckConstraint ck_aerq_sample_counts_nonnegative: clean_sample_count >= 0 AND human_outcome_count >= 0
    • CheckConstraint ck_aerq_source: source IN ('observation_batch', 'scheduled')
    • CheckConstraint ck_aerq_status: status IN ('pass', 'fail', 'insufficient_samples')
    • CheckConstraint ck_aerq_window_order: window_started_at <= window_ended_at
    • ForeignKeyConstraint unnamed: evaluated_by
    • ForeignKeyConstraint unnamed: project_id
    • PrimaryKeyConstraint unnamed: id
    • Index ix_aerq_project_agent_evaluated (unique=False): ai_eval_reviewer_quality.project_id, ai_eval_reviewer_quality.agent_id, ai_eval_reviewer_quality.evaluated_at; options {}

    backend/app/models/postgres.py:5677

    Record of running an evaluation dataset against a model version.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    dataset_idUUIDFalseFalse—ai_eval_datasets.id / CASCADE
    model_nameVARCHAR(200)FalseFalse—
    model_version_idUUIDTrueFalse—model_versions.id / SET NULL
    task_typeVARCHAR(50)FalseFalse—
    precisionFLOATTrueFalse—
    recallFLOATTrueFalse—
    f1_scoreFLOATTrueFalse—
    accuracyFLOATTrueFalse—
    agreement_rateFLOATTrueFalse—
    item_resultsJSONBTrueFalse—
    total_itemsINTEGERFalseFalseapplication=0
    correct_itemsINTEGERFalseFalseapplication=0
    fallback_usedBOOLEANFalseFalseapplication=False
    evaluated_atDATETIMEFalseFalseserver=now()
    duration_msINTEGERTrueFalse—
    metric_kindVARCHAR(20)FalseFalseserver='label_agreement'; application=label_agreement
    statusVARCHAR(20)FalseFalseserver='completed'; application=completed
    candidateJSONBTrueFalse—
    project_idUUIDTrueFalse—projects.id / SET NULL
    requested_byUUIDTrueFalse—users.id / SET NULL
    started_atDATETIMETrueFalse—
    completed_atDATETIMETrueFalse—
    heartbeat_atDATETIMETrueFalse—
    scored_itemsINTEGERFalseFalseserver=0; application=0
    unscored_itemsINTEGERFalseFalseserver=0; application=0
    error_itemsINTEGERFalseFalseserver=0; application=0
    items_cappedBOOLEANFalseFalseserver=false; application=False
    error_codeVARCHAR(100)TrueFalse—
    metricsJSONBTrueFalse—

    Constraints and indexes:

    • CheckConstraint ck_aer_metric_kind: metric_kind IN ('label_agreement','replay')
    • CheckConstraint ck_aer_status: status IN ('queued','running','completed','partial','failed')
    • ForeignKeyConstraint unnamed: dataset_id
    • ForeignKeyConstraint unnamed: model_version_id
    • ForeignKeyConstraint fk_aer_project_id: project_id
    • ForeignKeyConstraint fk_aer_requested_by: requested_by
    • PrimaryKeyConstraint unnamed: id
    • Index ix_aer_dataset (unique=False): ai_eval_runs.dataset_id; options {}
    • Index ix_aer_time (unique=False): ai_eval_runs.evaluated_at; options {}

    backend/app/models/postgres.py:5857

    One of a limited set of live result pairs from two model sizes, waiting for a label from a person or a confirmed answer.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    project_idUUIDFalseFalse—projects.id / CASCADE
    agent_idVARCHAR(80)FalseFalse—
    sample_keyVARCHAR(128)FalseFalse—
    incumbent_tierVARCHAR(20)FalseFalse—
    candidate_tierVARCHAR(20)FalseFalse—
    incumbent_outputJSONBFalseFalse—
    candidate_outputJSONBFalseFalse—
    incumbent_tokensINTEGERFalseFalseapplication=0
    candidate_tokensINTEGERFalseFalseapplication=0
    total_tokensINTEGERFalseFalseapplication=0
    label_statusVARCHAR(20)FalseFalseapplication=pending
    labelJSONBTrueFalse—
    labelled_byUUIDTrueFalse—users.id / SET NULL
    labelled_atDATETIMETrueFalse—
    created_atDATETIMEFalseFalseserver=now()

    Constraints and indexes:

    • CheckConstraint ck_aesp_label_status: label_status IN ('pending', 'human_labelled', 'golden_match')
    • CheckConstraint ck_aesp_tokens_nonnegative: total_tokens >= 0
    • ForeignKeyConstraint unnamed: labelled_by
    • ForeignKeyConstraint unnamed: project_id
    • PrimaryKeyConstraint unnamed: id
    • UniqueConstraint uq_ai_eval_shadow_pair_sample: project_id, agent_id, sample_key
    • Index ix_aesp_project_agent_created (unique=False): ai_eval_shadow_pairs.project_id, ai_eval_shadow_pairs.agent_id, ai_eval_shadow_pairs.created_at; options {}

    backend/app/models/postgres.py:2782

    Human feedback on AI triage results — the primary training signal.

    Sources:

    • manual: engineer rates analysis card in the UI (explicit)
    • jira_resolved: Jira ticket created by AI was resolved (implicit positive)
    • jira_invalid: Jira ticket closed as invalid/won’t-fix (implicit negative)
    • category_correction: engineer changed the failure_category in the UI
    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    analysis_idUUIDFalseFalse—ai_analysis.id / CASCADE
    test_case_idUUIDFalseFalse—test_cases.id / CASCADE
    user_idUUIDTrueFalse—users.id / SET NULL
    ratingVARCHAR(25)FalseFalse—
    corrected_categoryVARCHAR(30)TrueFalse—
    corrected_root_causeTEXTTrueFalse—
    commentTEXTTrueFalse—
    sourceVARCHAR(50)FalseFalseapplication=manual
    exportedBOOLEANFalseFalseapplication=False
    eval_manifest_checksumVARCHAR(64)TrueFalse—
    created_atDATETIMEFalseFalseserver=now()

    Constraints and indexes:

    • CheckConstraint ck_ai_feedback_eval_manifest_checksum: eval_manifest_checksum IS NULL OR eval_manifest_checksum ~ '^[0-9a-f]{64}$'
    • ForeignKeyConstraint unnamed: analysis_id
    • ForeignKeyConstraint unnamed: test_case_id
    • ForeignKeyConstraint unnamed: user_id
    • PrimaryKeyConstraint unnamed: id
    • Index ix_ai_feedback_analysis (unique=False): ai_feedback.analysis_id; options {}
    • Index ix_ai_feedback_created (unique=False): ai_feedback.created_at; options {}
    • Index ix_ai_feedback_created_at (unique=False): ai_feedback.created_at; options {}
    • Index ix_ai_feedback_exported (unique=False): ai_feedback.exported; options {}
    • Index ix_ai_feedback_rating (unique=False): ai_feedback.rating; options {}
    • Index ix_ai_feedback_test_case_id (unique=False): ai_feedback.test_case_id; options {}

    backend/app/models/postgres.py:4527

    Scoped personal access token (PAT) for CI/CD and API access.

    When project_id is NULL the key is user-scoped and inherits the owning user’s project permissions. When set, the key is project-scoped — requests using this key are restricted to the specified project.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    user_idUUIDFalseFalse—users.id / CASCADE
    project_idUUIDTrueFalse—projects.id / CASCADE
    nameVARCHAR(100)FalseFalse—
    key_hashVARCHAR(255)FalseFalse—
    key_hintVARCHAR(12)FalseFalse—
    scopesJSONFalseFalseapplication=list
    is_activeBOOLEANFalseFalseapplication=True
    expires_atDATETIMETrueFalse—
    last_used_atDATETIMETrueFalse—
    created_atDATETIMEFalseFalseserver=now()
    minted_by_key_idUUIDTrueFalse—api_keys.id / SET NULL

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: project_id
    • ForeignKeyConstraint unnamed: user_id
    • ForeignKeyConstraint fk_api_keys_minted_by_key_id: minted_by_key_id
    • PrimaryKeyConstraint unnamed: id
    • UniqueConstraint unnamed: key_hash
    • Index ix_api_keys_minted_by_key_id (unique=False): api_keys.minted_by_key_id; options {}
    • Index ix_api_keys_project_id (unique=False): api_keys.project_id; options {}

    backend/app/models/postgres.py:4454

    Key-value store for application-level configuration (e.g. SMTP settings).

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    keyVARCHAR(100)FalseTrue—
    valueJSONTrueFalse—
    secret_ref_idUUIDTrueFalse—secret_refs.id / SET NULL
    is_secret_backedBOOLEANFalseFalseapplication=False
    updated_byUUIDTrueFalse—users.id / SET NULL
    updated_atDATETIMEFalseFalseserver=now(); onupdate=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: secret_ref_id
    • ForeignKeyConstraint unnamed: updated_by
    • PrimaryKeyConstraint unnamed: key

    backend/app/models/postgres.py:1231

    Project-scoped test case identity.

    Merges the prior SuiteMembership lifecycle model with a relational anchor that the per-run test_cases table FKs to. Identified by (project_id, test_fingerprint) — one row per logical test per project. Every CanonicalTestCase belongs to exactly one TestSuite; manual re-linking moves the row between suites without losing run history.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    project_idUUIDFalseFalse—projects.id / CASCADE
    test_suite_idUUIDFalseFalse—test_suites.id / RESTRICT
    test_fingerprintVARCHAR(64)FalseFalse—
    test_nameVARCHAR(1000)FalseFalse—
    class_nameVARCHAR(500)TrueFalse—
    statusVARCHAR(20)FalseFalseserver=active; application=active
    sourceVARCHAR(20)FalseFalseserver=execution; application=execution
    first_seen_run_idUUIDTrueFalse—test_runs.id / SET NULL
    last_seen_run_idUUIDTrueFalse—test_runs.id / SET NULL
    deleted_at_run_idUUIDTrueFalse—test_runs.id / SET NULL
    managed_test_case_idUUIDTrueFalse—managed_test_cases.id / SET NULL
    retirement_confirmed_atDATETIMETrueFalse—
    retirement_confirmed_by_idUUIDTrueFalse—users.id / SET NULL
    retirement_reasonVARCHAR(500)TrueFalse—
    deleted_observed_atDATETIMETrueFalse—
    review_tagVARCHAR(50)TrueFalse—
    tagsJSONTrueFalse—
    created_atDATETIMEFalseFalseserver=now()
    updated_atDATETIMEFalseFalseserver=now(); onupdate=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: deleted_at_run_id
    • ForeignKeyConstraint unnamed: first_seen_run_id
    • ForeignKeyConstraint unnamed: last_seen_run_id
    • ForeignKeyConstraint unnamed: managed_test_case_id
    • ForeignKeyConstraint unnamed: project_id
    • ForeignKeyConstraint unnamed: retirement_confirmed_by_id
    • ForeignKeyConstraint unnamed: test_suite_id
    • PrimaryKeyConstraint unnamed: id
    • UniqueConstraint uq_canonical_test_cases_project_fp: project_id, test_fingerprint
    • Index ix_ctc_deleted_at_run (unique=False): canonical_test_cases.deleted_at_run_id; options {"postgresql_where": "deleted_at_run_id IS NOT NULL"}
    • Index ix_ctc_fingerprint (unique=False): canonical_test_cases.test_fingerprint; options {}
    • Index ix_ctc_first_seen_run (unique=False): canonical_test_cases.first_seen_run_id; options {"postgresql_where": "first_seen_run_id IS NOT NULL"}
    • Index ix_ctc_last_seen_run (unique=False): canonical_test_cases.last_seen_run_id; options {"postgresql_where": "last_seen_run_id IS NOT NULL"}
    • Index ix_ctc_project_id (unique=False): canonical_test_cases.project_id; options {}
    • Index ix_ctc_project_status (unique=False): canonical_test_cases.project_id, canonical_test_cases.status; options {}
    • Index ix_ctc_test_suite_id (unique=False): canonical_test_cases.test_suite_id; options {}
    • Index uq_ctc_managed_test_case_id (unique=True): canonical_test_cases.managed_test_case_id; options {"postgresql_where": "managed_test_case_id IS NOT NULL"}

    ORM navigation and cascade declarations:

    project: Mapped['Project'] = relationship('Project', back_populates='canonical_test_cases')
    test_suite: Mapped['TestSuite'] = relationship('TestSuite', back_populates='canonical_test_cases')
    test_cases: Mapped[list['TestCase']] = relationship('TestCase', back_populates='canonical_test_case')
    managed_test_case: Mapped[Optional['ManagedTestCase']] = relationship('ManagedTestCase')
    steps: Mapped[list['TestStep']] = relationship('TestStep', back_populates='canonical_test_case', cascade='all, delete-orphan', lazy='select')
    attachments: Mapped[list['TestAttachment']] = relationship('TestAttachment', back_populates='canonical_test_case', cascade='all, delete-orphan', lazy='select')

    backend/app/models/postgres.py:2758

    A single message in a ChatSession.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    session_idUUIDFalseFalse—chat_sessions.id / CASCADE
    roleVARCHAR(20)FalseFalse—
    contentTEXTFalseFalse—
    sourcesJSONTrueFalse—
    created_atDATETIMEFalseFalseserver=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: session_id
    • PrimaryKeyConstraint unnamed: id
    • Index ix_chat_messages_session (unique=False): chat_messages.session_id, chat_messages.created_at; options {}

    ORM navigation and cascade declarations:

    session: Mapped['ChatSession'] = relationship('ChatSession', back_populates='messages')

    backend/app/models/postgres.py:2736

    A conversation session between a user and the Conversation Agent.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    user_idUUIDFalseFalse—users.id / CASCADE
    project_idUUIDTrueFalse—projects.id / SET NULL
    active_test_run_idUUIDTrueFalse—test_runs.id / SET NULL
    active_report_idVARCHAR(64)TrueFalse—
    active_report_versionINTEGERTrueFalse—
    titleVARCHAR(500)TrueFalse—
    created_atDATETIMEFalseFalseserver=now()
    updated_atDATETIMEFalseFalseserver=now(); onupdate=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: active_test_run_id
    • ForeignKeyConstraint unnamed: project_id
    • ForeignKeyConstraint unnamed: user_id
    • PrimaryKeyConstraint unnamed: id
    • Index ix_chat_sessions_active_test_run_id (unique=False): chat_sessions.active_test_run_id; options {}
    • Index ix_chat_sessions_user (unique=False): chat_sessions.user_id; options {}

    ORM navigation and cascade declarations:

    messages: Mapped[list['ChatMessage']] = relationship('ChatMessage', back_populates='session', cascade='all, delete-orphan')

    backend/app/models/postgres.py:5326

    A QA lead’s rule: “<field> contains <text> -> <category>” for one project.

    Applied before any classification engine runs (a human correction of the same test still wins); the oldest enabled matching hint, in (created_at, id) order, is the one used. Case-insensitive substring match, never a user regex; one rule per (project, field, lower(text)).

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    project_idUUIDFalseFalse—projects.id / CASCADE
    match_fieldVARCHAR(20)FalseFalse—
    patternVARCHAR(200)FalseFalse—
    categoryVARCHAR(30)FalseFalse—
    noteVARCHAR(500)TrueFalse—
    enabledBOOLEANFalseFalseserver=true; application=True
    created_byUUIDTrueFalse—users.id / SET NULL
    created_atDATETIMEFalseFalseserver=now()
    updated_atDATETIMEFalseFalseserver=now(); onupdate=now()

    Constraints and indexes:

    • CheckConstraint ck_classification_hints_category: category IN ('PRODUCT_BUG', 'INFRASTRUCTURE', 'TEST_DATA', 'AUTOMATION_DEFECT', 'FLAKY')
    • CheckConstraint ck_classification_hints_match_field: match_field IN ('error_message', 'test_name', 'suite_name', 'class_name')
    • ForeignKeyConstraint unnamed: created_by
    • ForeignKeyConstraint unnamed: project_id
    • PrimaryKeyConstraint unnamed: id
    • Index ix_classification_hints_project_enabled (unique=False): classification_hints.project_id, classification_hints.enabled; options {}
    • Index uq_classification_hints_project_rule (unique=True): classification_hints.project_id, classification_hints.match_field, lower(pattern); options {}

    backend/app/models/postgres.py:7413

    Generated compliance export pack (ZIP) for a release decision.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    release_idUUIDTrueFalse—releases.id / SET NULL
    project_idUUIDFalseFalse—projects.id / CASCADE
    test_run_idUUIDTrueFalse—test_runs.id / SET NULL
    minio_keyVARCHAR(500)FalseFalse—
    manifest_sha256VARCHAR(64)FalseFalse—
    signatureTEXTTrueFalse—
    signing_key_idVARCHAR(64)TrueFalse—
    file_countINTEGERFalseFalseapplication=0
    bytesINTEGERFalseFalseapplication=0
    retention_expires_atDATETIMEFalseFalse—
    generated_atDATETIMEFalseFalseserver=now()
    generated_by_user_idUUIDTrueFalse—users.id / SET NULL
    metadata_snapshotJSONBTrueFalse—
    notesTEXTTrueFalse—

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: generated_by_user_id
    • ForeignKeyConstraint unnamed: project_id
    • ForeignKeyConstraint unnamed: release_id
    • ForeignKeyConstraint unnamed: test_run_id
    • PrimaryKeyConstraint unnamed: id
    • Index ix_compliance_packs_project (unique=False): compliance_packs.project_id, compliance_packs.generated_at; options {}
    • Index ix_compliance_packs_release (unique=False): compliance_packs.release_id; options {}

    backend/app/models/postgres.py:7490

    The deployment’s Ed25519 compliance-pack signing keys (migration 0225).

    The PUBLIC key lives here for ever (an old pack keeps verifying after a rotation); the PRIVATE key lives only in secret_refs (scope SIGNING_SECRET_SCOPE, key_name = key_id), Fernet-encrypted by secret_service, and is expired (ciphertext cleared) when the key is retired. At most one active key (uq_compliance_signing_keys_active).

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    key_idVARCHAR(64)FalseTrue—
    algorithmVARCHAR(20)FalseFalseserver=ed25519; application=ed25519
    public_key_pemTEXTFalseFalse—
    created_atDATETIMEFalseFalseserver=now()
    created_by_user_idUUIDTrueFalse—users.id / SET NULL
    retired_atDATETIMETrueFalse—
    retired_by_user_idUUIDTrueFalse—users.id / SET NULL
    retired_reasonVARCHAR(500)TrueFalse—

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: created_by_user_id
    • ForeignKeyConstraint unnamed: retired_by_user_id
    • PrimaryKeyConstraint unnamed: key_id
    • Index uq_compliance_signing_keys_active (unique=True): compliance_signing_keys.algorithm; options {"postgresql_where": "retired_at IS NULL", "sqlite_where": "retired_at IS NULL"}

    backend/app/models/postgres.py:1823

    One coverage snapshot a project a UTC day (migration 0217).

    Written by the daily beat (take_coverage_snapshot through coverage_snapshot_service.stage_snapshot: an upsert on (project_id, snapshot_day)) and read by the Coverage History tab. The count columns keep their 0001 names; since migration 0217 they mean:

    • total_tests — tests in the catalogue on snapshot_day;
    • automated_count — distinct tests exercised in the 30 UTC days ending snapshot_day;
    • total_suites — distinct suites exercised in those 30 days.

    snapshot_date is when the row was written, snapshot_day the day it describes (the API speaks those clear names).

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    project_idUUIDFalseFalse—projects.id / CASCADE
    snapshot_dayDATEFalseFalse—
    snapshot_dateDATETIMEFalseFalseserver=now()
    total_suitesINTEGERFalseFalseapplication=0
    total_testsINTEGERFalseFalseapplication=0
    automated_countINTEGERFalseFalseapplication=0
    suite_coverageJSONBTrueFalse—

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: project_id
    • PrimaryKeyConstraint unnamed: id
    • Index uq_coverage_snapshots_project_day (unique=True): coverage_snapshots.project_id, coverage_snapshots.snapshot_day; options {}

    backend/app/models/postgres.py:5949

    Saved evidence for one evaluation cycle over a set of reports.

    Outside the project-scoped purge on purpose, and it must stay that way. This table has neither project_id nor test_run_id, which the retention epic flagged as “structurally unreachable by any project-scoped purge, forever”.

    The decision recorded here is that adding a scope column would be WRONG, not merely unnecessary. A cycle evaluates the report-generation corpus as a whole and spans reports drawn from many projects; stamping any single project_id on it would be a lie, and purging it when that project was deleted would destroy an attestation that still describes live behaviour elsewhere. It belongs with the eval-gate evidence (prompt_manifest_eval), not with tenant data.

    Its growth is limited by how often cycles run, not by a clock: cycle_key is unique and one row is written per evaluation cycle — an attestation event, orders of magnitude rarer than a test run. If that cadence ever changes, this needs a DEPLOYMENT-wide clock, not a project-scoped one.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    cycle_keyVARCHAR(128)FalseFalse—
    corpus_versionVARCHAR(120)FalseFalse—
    corpus_sha256VARCHAR(64)FalseFalse—
    report_countINTEGERFalseFalseapplication=0
    statusVARCHAR(20)FalseFalse—
    metricsJSONBFalseFalseapplication=dict
    checksJSONBFalseFalseapplication=list
    unavailable_metricsJSONBFalseFalseapplication=list
    consecutive_passesINTEGERFalseFalseapplication=0
    evaluated_byUUIDTrueFalse—users.id / SET NULL
    evaluated_atDATETIMEFalseFalseserver=now()
    created_atDATETIMEFalseFalseserver=now()

    Constraints and indexes:

    • CheckConstraint ck_drec_consecutive_nonnegative: consecutive_passes >= 0
    • CheckConstraint ck_drec_corpus_hash: corpus_sha256 ~ '^[0-9a-f]{64}$'
    • CheckConstraint ck_drec_report_count: report_count >= 0
    • CheckConstraint ck_drec_status: status IN ('pass', 'warn', 'fail')
    • ForeignKeyConstraint unnamed: evaluated_by
    • PrimaryKeyConstraint unnamed: id
    • UniqueConstraint uq_drec_cycle_key: cycle_key
    • Index ix_drec_corpus_evaluated (unique=False): decision_report_eval_cycles.corpus_version, decision_report_eval_cycles.evaluated_at; options {}
    • Index ix_drec_status (unique=False): decision_report_eval_cycles.status; options {}

    backend/app/models/postgres.py:2822

    Structured human feedback bound to one immutable DecisionReport version.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    project_idUUIDFalseFalse—projects.id / CASCADE
    test_run_idUUIDFalseFalse—test_runs.id / CASCADE
    report_idVARCHAR(64)FalseFalse—
    report_versionINTEGERFalseFalse—
    report_evidence_sha256VARCHAR(64)FalseFalse—
    user_idUUIDTrueFalse—users.id / SET NULL
    feedback_kindVARCHAR(30)FalseFalse—
    utility_ratingVARCHAR(25)TrueFalse—
    claim_idVARCHAR(128)TrueFalse—
    claim_kindVARCHAR(20)TrueFalse—
    correction_typeVARCHAR(30)TrueFalse—
    corrected_valueJSONTrueFalse—
    reasonTEXTTrueFalse—
    evidence_refsJSONFalseFalseapplication=list
    idempotency_keyVARCHAR(128)FalseFalse—
    created_atDATETIMEFalseFalseserver=now()

    Constraints and indexes:

    • CheckConstraint ck_decision_report_feedback_kind: feedback_kind IN ('utility', 'claim_correction')
    • CheckConstraint ck_decision_report_feedback_report_version_positive: report_version >= 1
    • CheckConstraint ck_decision_report_feedback_utility_rating: utility_rating IS NULL OR utility_rating IN ('useful', 'partially_useful', 'not_useful')
    • ForeignKeyConstraint unnamed: project_id
    • ForeignKeyConstraint unnamed: test_run_id
    • ForeignKeyConstraint unnamed: user_id
    • PrimaryKeyConstraint unnamed: id
    • UniqueConstraint uq_decision_report_feedback_user_idempotency: user_id, idempotency_key
    • Index ix_decision_report_feedback_created (unique=False): decision_report_feedback.created_at; options {}
    • Index ix_decision_report_feedback_created_at (unique=False): decision_report_feedback.created_at; options {}
    • Index ix_decision_report_feedback_report (unique=False): decision_report_feedback.project_id, decision_report_feedback.test_run_id, decision_report_feedback.report_id, decision_report_feedback.report_version; options {}

    backend/app/models/postgres.py:6011

    A saved request to publish a report version enriched by child investigations. Sending it twice is safe.

    The parent deep pipeline never carries child prompts or evidence through a broker payload. It records this small, tenant-bound request instead; a worker later re-resolves the immutable parent report and terminal child rows before publishing a new DecisionReportV1 version.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    project_idUUIDFalseFalse—projects.id / CASCADE
    test_run_idUUIDFalseFalse—test_runs.id / CASCADE
    parent_pipeline_run_idUUIDFalseFalse—agent_pipeline_runs.id / CASCADE
    statusVARCHAR(20)FalseFalseserver=pending; application=pending
    reasonVARCHAR(120)FalseFalseapplication=children_terminal
    attemptsINTEGERFalseFalseserver=0; application=0
    next_attempt_atDATETIMETrueFalse—
    claimed_atDATETIMETrueFalse—
    published_report_idVARCHAR(64)TrueFalse—
    published_report_versionINTEGERTrueFalse—
    errorTEXTTrueFalse—
    created_atDATETIMEFalseFalseserver=now()
    updated_atDATETIMEFalseFalseserver=now(); onupdate=now()

    Constraints and indexes:

    • CheckConstraint ck_drsr_attempts_nonnegative: attempts >= 0
    • CheckConstraint ck_drsr_status: status IN ('pending', 'processing', 'published', 'rejected', 'failed')
    • ForeignKeyConstraint unnamed: parent_pipeline_run_id
    • ForeignKeyConstraint unnamed: project_id
    • ForeignKeyConstraint unnamed: test_run_id
    • PrimaryKeyConstraint unnamed: id
    • UniqueConstraint uq_drsr_parent_pipeline: parent_pipeline_run_id
    • Index ix_drsr_status_next_attempt (unique=False): decision_report_supersession_requests.status, decision_report_supersession_requests.next_attempt_at; options {}

    backend/app/models/postgres.py:2964

    Deep investigation result per failure cluster.

    Writers (AI-F4): the deep pipeline persists one row per (test_run_id, cluster_id) via agents/deep_persistence.py (log_evidence.origin == "pipeline"); the demo seed scripts tag theirs origin == "seed". causal_chain / affected_services / contract_violations are only populated by seeds today. The contract_validation and log_intelligence stages DO run in the deep graph and produce findings, but nothing folds their output into these columns — so they stay None rather than carrying a value this table would imply came from the cluster synthesis.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    test_run_idUUIDFalseFalse—test_runs.id / CASCADE
    cluster_idVARCHAR(20)FalseFalse—
    root_causeTEXTTrueFalse—
    failure_categoryVARCHAR(30)TrueFalse—
    confidence_scoreINTEGERTrueFalse—
    causal_chainJSONTrueFalse—
    evidenceJSONTrueFalse—
    affected_servicesJSONTrueFalse—
    contract_violationsJSONTrueFalse—
    log_evidenceJSONTrueFalse—
    recommended_actionsJSONTrueFalse—
    created_atDATETIMEFalseFalseserver=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: test_run_id
    • PrimaryKeyConstraint unnamed: id
    • Index ix_deep_findings_run (unique=False): deep_findings.test_run_id; options {}

    backend/app/models/postgres.py:1794

    Staging area for defect candidates before promotion to full defects.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    run_idUUIDFalseFalse—test_runs.id / CASCADE
    cluster_idVARCHAR(20)FalseFalse—
    severityVARCHAR(20)FalseFalseapplication=HIGH
    owner_teamVARCHAR(255)TrueFalse—
    componentVARCHAR(255)TrueFalse—
    titleVARCHAR(500)FalseFalse—
    descriptionTEXTTrueFalse—
    duplicate_ofUUIDTrueFalse—
    is_duplicateBOOLEANFalseFalseapplication=False
    evidence_bundleJSONTrueFalse—
    criticality_scoresJSONTrueFalse—
    composite_scoreFLOATTrueFalse—
    failure_categoryVARCHAR(30)TrueFalse—
    member_countINTEGERFalseFalseapplication=0
    statusVARCHAR(30)FalseFalseapplication=pending
    promoted_defect_idUUIDTrueFalse—defects.id / SET NULL
    created_atDATETIMEFalseFalseserver=now()
    updated_atDATETIMEFalseFalseserver=now(); onupdate=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: promoted_defect_id
    • ForeignKeyConstraint unnamed: run_id
    • PrimaryKeyConstraint unnamed: id
    • Index ix_defect_cand_run (unique=False): defect_candidates.run_id; options {}
    • Index ix_defect_cand_status (unique=False): defect_candidates.status; options {}

    backend/app/models/postgres.py:1663

    Defect records linked to test cases or failure clusters.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    test_case_idUUIDTrueFalse—test_cases.id / SET NULL
    project_idUUIDFalseFalse—projects.id / CASCADE
    jira_ticket_idVARCHAR(50)TrueFalse—
    jira_ticket_urlVARCHAR(1000)TrueFalse—
    jira_statusVARCHAR(50)TrueFalse—
    ai_confidence_scoreINTEGERTrueFalse—
    failure_categoryVARCHAR(30)TrueFalse—
    resolution_statusVARCHAR(50)FalseFalseapplication=OPEN
    created_atDATETIMEFalseFalseserver=now()
    resolved_atDATETIMETrueFalse—
    cluster_idVARCHAR(255)TrueFalse—
    titleVARCHAR(255)TrueFalse—
    descriptionTEXTTrueFalse—
    severityVARCHAR(20)TrueFalse—
    componentVARCHAR(255)TrueFalse—
    owner_teamVARCHAR(255)TrueFalse—
    assignee_user_idUUIDTrueFalse—users.id / SET NULL
    labelsJSONTrueFalse—
    criticality_scoresJSONTrueFalse—
    release_idUUIDTrueFalse—releases.id / SET NULL
    affects_releasesJSONTrueFalse—
    evidence_bundleJSONTrueFalse—
    duplicate_ofUUIDTrueFalse—
    is_duplicateBOOLEANFalseFalseapplication=False
    promotion_sourceVARCHAR(50)TrueFalse—
    approval_statusVARCHAR(20)FalseFalseapplication=approved
    approved_byUUIDTrueFalse—users.id / SET NULL
    approved_atDATETIMETrueFalse—
    policy_evaluationJSONTrueFalse—
    signature_fingerprintVARCHAR(64)TrueFalse—
    recurrence_countINTEGERFalseFalseserver=0; application=0
    last_recurrence_atDATETIMETrueFalse—
    external_status_atDATETIMETrueFalse—
    external_status_conflictBOOLEANFalseFalseserver=false; application=False

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: approved_by
    • ForeignKeyConstraint unnamed: assignee_user_id
    • ForeignKeyConstraint unnamed: project_id
    • ForeignKeyConstraint unnamed: release_id
    • ForeignKeyConstraint unnamed: test_case_id
    • PrimaryKeyConstraint unnamed: id
    • Index ix_defects_project_assignee (unique=False): defects.project_id, defects.assignee_user_id; options {"postgresql_where": "assignee_user_id IS NOT NULL"}
    • Index ix_defects_project_id (unique=False): defects.project_id; options {}
    • Index ix_defects_project_signature (unique=False): defects.project_id, defects.signature_fingerprint; options {"postgresql_where": "signature_fingerprint IS NOT NULL"}
    • Index ix_defects_project_status_severity (unique=False): defects.project_id, defects.resolution_status, defects.severity; options {}
    • Index ix_defects_test_case_open_unique (unique=True): defects.test_case_id; options {"postgresql_where": "resolution_status = 'OPEN' AND test_case_id IS NOT NULL"}

    ORM navigation and cascade declarations:

    test_case: Mapped[Optional['TestCase']] = relationship('TestCase', back_populates='defects')
    approver: Mapped[Optional['User']] = relationship('User', foreign_keys=[approved_by])

    backend/app/models/postgres.py:7786

    One record of a deletion actually happening (migration 0147).

    Retention already writes a purge record into settings_audit_log, which is never purged and stays the compliance evidence. This table is the OPERATIONAL surface: what ran, what it removed, and — the part the audit row cannot express — whether it is still running, and whether it finished.

    Written on its own session. A failed or partial status written inside the transaction that failed is erased by that transaction’s rollback, so the only states a same-session writer can ever record are the successful ones. Same reason the purge-audit row is written after the commit.

    resolved_run_ids records exactly which runs went, and candidate_hash fingerprints that set. Today that is an audit detail; criteria deletion (S5) reuses both to freeze a previewed candidate set and refuse to execute if the world moved underneath it.

    Status vocabulary is limited to states something can actually reach: queued|previewed|running|awaiting_collection|completed|failed|partial|expired. previewed was withheld until S5’s preview endpoint gave it a producer; awaiting_collection (the criteria worker, after a verified collect export) and expired (the export janitor beat) arrived with the export-before-delete. cancelled is still NOT declared, because nothing can produce it — shipping a state nothing writes is the defect this epic catalogues elsewhere.

    Export before delete (migration 0241): a criteria deletion may build an archive first (export_tier / export_target); nothing is deleted until export_verified_at is set (the stored archive was re-read and verified) and, for collect, until an acknowledgement with the exact export_manifest_sha256 arrives before collect_expires_at.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    project_idUUIDFalseFalse—projects.id / CASCADE
    job_kindVARCHAR(20)FalseFalse—
    statusVARCHAR(20)FalseFalseapplication=queued
    criteriaJSONBTrueFalse—
    resolved_run_idsJSONBTrueFalse—
    candidate_hashVARCHAR(64)TrueFalse—
    countsJSONBTrueFalse—
    bytes_reclaimedBIGINTTrueFalse—
    holds_honouredJSONBTrueFalse—
    references_brokenJSONBTrueFalse—
    errorTEXTTrueFalse—
    requested_by_idUUIDTrueFalse—users.id / SET NULL
    requested_atDATETIMEFalseFalseserver=now()
    started_atDATETIMETrueFalse—
    finished_atDATETIMETrueFalse—
    export_tierVARCHAR(10)TrueFalse—
    export_targetVARCHAR(10)TrueFalse—
    export_bytesBIGINTTrueFalse—
    export_manifest_sha256VARCHAR(64)TrueFalse—
    export_archive_sha256VARCHAR(64)TrueFalse—
    export_locationVARCHAR(500)TrueFalse—
    export_verified_atDATETIMETrueFalse—
    export_metaJSONBTrueFalse—
    collect_expires_atDATETIMETrueFalse—
    collected_atDATETIMETrueFalse—
    acknowledged_atDATETIMETrueFalse—
    acknowledged_by_idUUIDTrueFalse—users.id / SET NULL

    Constraints and indexes:

    • CheckConstraint ck_deletion_jobs_export_pair: (export_tier IS NULL) = (export_target IS NULL)
    • CheckConstraint ck_deletion_jobs_export_target: export_target IS NULL OR export_target IN ('collect', 'retain')
    • CheckConstraint ck_deletion_jobs_export_tier: export_tier IS NULL OR export_tier IN ('summary', 'evidence', 'full')
    • ForeignKeyConstraint unnamed: project_id
    • ForeignKeyConstraint unnamed: requested_by_id
    • ForeignKeyConstraint fk_deletion_jobs_acknowledged_by: acknowledged_by_id
    • PrimaryKeyConstraint unnamed: id
    • Index ix_deletion_jobs_project_started (unique=False): deletion_jobs.project_id, deletion_jobs.started_at; options {}
    • Index ix_deletion_jobs_status (unique=False): deletion_jobs.status; options {}

    backend/app/models/postgres.py:5612

    User subscription to a scheduled or event-driven report delivery.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    user_idUUIDFalseFalse—users.id / CASCADE
    project_idUUIDTrueFalse—projects.id / CASCADE
    saved_view_idUUIDTrueFalse—saved_views.id / SET NULL
    saved_view_nameVARCHAR(255)TrueFalse—
    nameVARCHAR(255)FalseFalse—
    scheduleVARCHAR(20)FalseFalseapplication=WEEKLY
    channelVARCHAR(20)FalseFalseapplication=email
    is_activeBOOLEANFalseFalseapplication=True
    is_pausedBOOLEANFalseFalseapplication=False
    scope_typeVARCHAR(20)TrueFalseapplication=project
    scope_valueVARCHAR(255)TrueFalse—
    trigger_filterVARCHAR(20)TrueFalseapplication=all
    send_when_unchangedBOOLEANFalseFalseapplication=True
    report_attachmentBOOLEANFalseFalseapplication=False
    last_delivered_atDATETIMETrueFalse—
    next_delivery_atDATETIMETrueFalse—
    delivery_countINTEGERFalseFalseapplication=0
    created_atDATETIMEFalseFalseserver=now()
    updated_atDATETIMEFalseFalseserver=now(); onupdate=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: project_id
    • ForeignKeyConstraint unnamed: saved_view_id
    • ForeignKeyConstraint unnamed: user_id
    • PrimaryKeyConstraint unnamed: id
    • Index ix_ds_next (unique=False): digest_subscriptions.next_delivery_at; options {}
    • Index ix_ds_schedule_active (unique=False): digest_subscriptions.schedule, digest_subscriptions.is_active, digest_subscriptions.is_paused; options {}
    • Index ix_ds_user (unique=False): digest_subscriptions.user_id; options {}

    backend/app/models/postgres.py:3420

    Suppression record so a dismissed duplicate pair stays dismissed (Phase 4).

    Consulted by the detector before staging a candidate so a re-detection run never resurfaces a pair the user already dismissed. Same fixed ordering (case_a_id < case_b_id, DB CHECK) + uniqueness on (project_id, case_a_id, case_b_id) as the candidate table.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    project_idUUIDFalseFalse—projects.id / CASCADE
    case_a_idUUIDFalseFalse—managed_test_cases.id / CASCADE
    case_b_idUUIDFalseFalse—managed_test_cases.id / CASCADE
    dismissed_by_user_idUUIDTrueFalse—users.id / SET NULL
    dismissed_atDATETIMEFalseFalseserver=now()

    Constraints and indexes:

    • CheckConstraint ck_dismissed_dup_canonical_order: case_a_id < case_b_id
    • ForeignKeyConstraint unnamed: case_a_id
    • ForeignKeyConstraint unnamed: case_b_id
    • ForeignKeyConstraint unnamed: dismissed_by_user_id
    • ForeignKeyConstraint unnamed: project_id
    • PrimaryKeyConstraint unnamed: id
    • UniqueConstraint uq_dismissed_dup_pair: project_id, case_a_id, case_b_id
    • Index ix_dismissed_dup_project (unique=False): dismissed_duplicate_pairs.project_id; options {}

    backend/app/models/postgres.py:8555

    One build folder <project>/runs/<build>/ seen by the drop-folder watcher (migration 0254).

    status is services/drop_folder_watcher.BUILD_STATUSES (CHECKed): pending (no marker file yet; checked with HEAD each sweep for 24 h) -> queued (the marker file went to ingest_test_run; task_id + queued_at set, ck_dfb_queued_has_task) | baseline (the marker file predates the watcher’s first sweep: history, never queued) | abandoned (24 h without a marker file) | refused (the key fails sentinel_key_problem). Every status but pending is terminal. Rows are NOT deleted with runs: a deleted slug-prefixed run, whose objects stay in the bucket, must not come back. project_segment is the key’s first segment (a project id or slug).

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    build_prefixVARCHAR(1024)FalseFalse—
    project_segmentVARCHAR(100)FalseFalse—
    statusVARCHAR(10)FalseFalseserver='pending'; application=pending
    sentinel_etagVARCHAR(255)TrueFalse—
    sentinel_last_modifiedDATETIMETrueFalse—
    first_seen_atDATETIMEFalseFalseserver=now()
    last_checked_atDATETIMEFalseFalseserver=now()
    queued_atDATETIMETrueFalse—
    task_idVARCHAR(64)TrueFalse—

    Constraints and indexes:

    • CheckConstraint ck_dfb_queued_has_task: status <> 'queued' OR (queued_at IS NOT NULL AND task_id IS NOT NULL)
    • CheckConstraint ck_dfb_status: status IN ('pending', 'queued', 'baseline', 'abandoned', 'refused')
    • PrimaryKeyConstraint unnamed: id
    • Index ix_dfb_pending_first_seen (unique=False): drop_folder_builds.first_seen_at; options {"postgresql_where": "status = 'pending'"}
    • Index uq_dfb_build_prefix (unique=True): drop_folder_builds.build_prefix; options {}

    backend/app/models/postgres.py:3377

    One detected near-duplicate PAIR of authored ManagedTestCases (Phase 4).

    Tiered, offline-first, per-project duplicate detection (migration 0094). Exactly one row per logical pair: the writer always stores the pair in the order case_a_id < case_b_id (also a DB CHECK), and the pair is written only once per (project_id, case_a_id, case_b_id).

    Every row explains itself: it carries the component breakdown (component_scores), an aggregate score (0.0-1.0), a human-readable reason, the detection method (fingerprint|structural|semantic), and a band (exact|strong|possible).

    MERGE IS NON-DESTRUCTIVE this phase: resolving a pair only flips status (open → merged|dismissed) and may set a soft deprecate flag on the losing case. It never deletes cases or redirects test_fingerprint.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    project_idUUIDFalseFalse—projects.id / CASCADE
    case_a_idUUIDFalseFalse—managed_test_cases.id / CASCADE
    case_b_idUUIDFalseFalse—managed_test_cases.id / CASCADE
    bandVARCHAR(20)FalseFalse—
    scoreFLOATFalseFalse—
    reasonTEXTTrueFalse—
    methodVARCHAR(20)FalseFalse—
    component_scoresJSONBTrueFalse—
    detected_atDATETIMEFalseFalseserver=now()
    statusVARCHAR(20)FalseFalseserver='open'; application=open

    Constraints and indexes:

    • CheckConstraint ck_dup_candidate_canonical_order: case_a_id < case_b_id
    • ForeignKeyConstraint unnamed: case_a_id
    • ForeignKeyConstraint unnamed: case_b_id
    • ForeignKeyConstraint unnamed: project_id
    • PrimaryKeyConstraint unnamed: id
    • UniqueConstraint uq_dup_candidate_pair: project_id, case_a_id, case_b_id
    • Index ix_dup_candidate_project_status (unique=False): duplicate_test_case_candidates.project_id, duplicate_test_case_candidates.status; options {}

    backend/app/models/postgres.py:4580

    Tenant-bound immutable evidence captured from an executed tool.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    run_idUUIDTrueFalse—test_runs.id / SET NULL
    project_idUUIDTrueFalse—projects.id / CASCADE
    producer_pipeline_run_idUUIDTrueFalse—agent_pipeline_runs.id / SET NULL
    cluster_idVARCHAR(20)TrueFalse—
    test_case_idUUIDTrueFalse—test_cases.id / SET NULL
    artifact_typeVARCHAR(50)FalseFalse—
    source_systemVARCHAR(100)FalseFalse—
    uri_or_refVARCHAR(1000)TrueFalse—
    summary_excerptTEXTTrueFalse—
    relevance_scoreFLOATTrueFalse—
    schema_versionINTEGERFalseFalseapplication=2
    source_versionVARCHAR(100)TrueFalse—
    content_sha256VARCHAR(64)TrueFalse—
    content_size_bytesBIGINTTrueFalse—
    media_typeVARCHAR(100)TrueFalse—
    sensitivityVARCHAR(20)TrueFalse—
    freshnessVARCHAR(20)TrueFalse—
    observed_atDATETIMETrueFalse—
    integrity_statusVARCHAR(30)FalseFalseapplication=legacy_unverified
    retention_classVARCHAR(30)FalseFalseapplication=artifacts
    idempotency_keyVARCHAR(64)TrueFalse—
    created_atDATETIMEFalseFalseserver=now()

    Constraints and indexes:

    • CheckConstraint ck_evidence_content_sha256: content_sha256 IS NULL OR content_sha256 ~ '^[0-9a-f]{64}$'
    • CheckConstraint ck_evidence_content_size_nonnegative: content_size_bytes IS NULL OR content_size_bytes >= 0
    • ForeignKeyConstraint unnamed: producer_pipeline_run_id
    • ForeignKeyConstraint unnamed: project_id
    • ForeignKeyConstraint unnamed: run_id
    • ForeignKeyConstraint unnamed: test_case_id
    • PrimaryKeyConstraint unnamed: id
    • Index ix_evidence_cluster (unique=False): evidence_artifacts.cluster_id; options {}
    • Index ix_evidence_pipeline (unique=False): evidence_artifacts.producer_pipeline_run_id; options {}
    • Index ix_evidence_project_run (unique=False): evidence_artifacts.project_id, evidence_artifacts.run_id; options {}
    • Index ix_evidence_run_id (unique=False): evidence_artifacts.run_id; options {}
    • Index ix_evidence_run_test (unique=False): evidence_artifacts.run_id, evidence_artifacts.test_case_id; options {}
    • Index ux_evidence_idempotency (unique=True): evidence_artifacts.idempotency_key; options {}

    backend/app/models/postgres.py:6820

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    project_idUUIDFalseFalse—projects.id / CASCADE
    test_case_idUUIDFalseFalse—test_cases.id / CASCADE
    test_run_idUUIDFalseFalse—test_runs.id / CASCADE
    test_fingerprintVARCHAR(64)FalseFalse—
    verdictVARCHAR(32)FalseFalse—
    confidenceFLOATFalseFalseapplication=0.0
    rationaleVARCHAR(1000)FalseFalseapplication=
    inputsJSONBFalseFalseapplication=dict
    computed_atDATETIMEFalseFalseserver=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: project_id
    • ForeignKeyConstraint unnamed: test_case_id
    • ForeignKeyConstraint unnamed: test_run_id
    • PrimaryKeyConstraint unnamed: id
    • Index ix_failure_attribution_project_verdict (unique=False): failure_attribution.project_id, failure_attribution.verdict; options {}
    • Index ix_failure_attribution_run (unique=False): failure_attribution.test_run_id; options {}
    • Index ux_failure_attribution_test_case (unique=True): failure_attribution.test_case_id; options {}

    backend/app/models/postgres.py:2888

    Semantic cluster of failures grouped by root-cause similarity.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    test_run_idUUIDFalseFalse—test_runs.id / CASCADE
    pipeline_run_idUUIDTrueFalse—agent_pipeline_runs.id / SET NULL
    cluster_idVARCHAR(20)FalseFalse—
    labelVARCHAR(500)FalseFalse—
    representative_errorTEXTTrueFalse—
    member_test_idsJSONFalseFalseapplication=list
    sizeINTEGERFalseFalseapplication=1
    cohesion_scoreFLOATTrueFalse—
    regression_classificationVARCHAR(50)TrueFalse—
    created_atDATETIMEFalseFalseserver=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: pipeline_run_id
    • ForeignKeyConstraint unnamed: test_run_id
    • PrimaryKeyConstraint unnamed: id
    • UniqueConstraint uq_failure_clusters_pipeline_cluster: pipeline_run_id, cluster_id
    • Index ix_failure_clusters_run (unique=False): failure_clusters.test_run_id; options {}

    backend/app/models/postgres.py:6628

    Named capability toggle gated by project, role, and rollout percent.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    keyVARCHAR(80)FalseFalse—
    descriptionTEXTTrueFalse—
    enabled_globalBOOLEANFalseFalseapplication=False
    enabled_projectsJSONBTrueFalse—
    enabled_rolesJSONBTrueFalse—
    rollout_percentINTEGERFalseFalseapplication=100
    created_atDATETIMEFalseFalseserver=now()
    updated_atDATETIMEFalseFalseserver=now(); onupdate=now()
    updated_by_user_idUUIDTrueFalse—users.id / SET NULL

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: updated_by_user_id
    • PrimaryKeyConstraint unnamed: id
    • Index ix_feature_flags_key (unique=True): feature_flags.key; options {}

    backend/app/models/postgres.py:5059

    Links an external IdP subject to a local User.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    user_idUUIDFalseFalse—users.id / CASCADE
    sso_config_idUUIDFalseFalse—sso_configurations.id / CASCADE
    external_idVARCHAR(1000)FalseFalse—
    external_emailVARCHAR(255)TrueFalse—
    external_display_nameVARCHAR(500)TrueFalse—
    external_groupsJSONTrueFalseapplication=list
    last_login_atDATETIMETrueFalse—
    created_atDATETIMEFalseFalseserver=now()
    updated_atDATETIMEFalseFalseserver=now(); onupdate=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: sso_config_id
    • ForeignKeyConstraint unnamed: user_id
    • PrimaryKeyConstraint unnamed: id
    • UniqueConstraint uq_federated_identity: sso_config_id, external_id
    • Index ix_federated_user (unique=False): federated_identities.user_id; options {}

    backend/app/models/postgres.py:2518

    One (fixer run, candidate test, attempt) of the Fixer.

    The Fixer selects flaky/quarantined tests, generates a test-code-only candidate fix, validates it by rerunning the test in a sandbox, and — in suggest mode only — opens a DRAFT PR. This row is the progress surface the UI polls (GET .../fixer/attempts) and the per-stage audit trail.

    status moves through fixed stages: selected → diagnosing → generating → validating → one terminal of validated (all reruns passed; shadow mode, or suggest with no PR opened), rejected_globs (the diff touched a non-test file — rejected BEFORE any execution), failed_validation (the fix did not validate), pr_opened (validated + a draft PR was created), error (infra trouble — no runner, docker unavailable, generation offline), or skipped_budget (a budget cap or the kill switch stopped it).

    Invariant: a PR is opened ONLY from validated in suggest mode. error and failed_validation NEVER produce a pr_url.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    project_idUUIDFalseFalse—projects.id / CASCADE
    fixer_run_idUUIDFalseFalse—
    test_fingerprintVARCHAR(64)FalseFalse—
    test_nameVARCHAR(500)TrueFalse—
    statusVARCHAR(30)FalseFalseapplication=selected
    attempt_noINTEGERFalseFalseapplication=1
    patch_summaryTEXTTrueFalse—
    patchTEXTTrueFalse—
    validation_rerunsINTEGERTrueFalse—
    validation_passedINTEGERTrueFalse—
    runner_typeVARCHAR(30)TrueFalse—
    runner_log_digestVARCHAR(128)TrueFalse—
    egress_openedBOOLEANFalseFalseapplication=False
    pr_urlVARCHAR(1000)TrueFalse—
    pr_numberINTEGERTrueFalse—
    pr_stateVARCHAR(20)TrueFalse—
    ledger_run_idUUIDTrueFalse—
    reasonTEXTTrueFalse—
    created_atDATETIMEFalseFalseserver=now()
    completed_atDATETIMETrueFalse—

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: project_id
    • PrimaryKeyConstraint unnamed: id
    • Index ix_fix_attempts_fixer_run (unique=False): fix_attempts.fixer_run_id; options {}
    • Index ix_fix_attempts_project_created (unique=False): fix_attempts.project_id, fix_attempts.created_at; options {}

    backend/app/models/postgres.py:6994

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    project_idUUIDFalseFalse—projects.id / CASCADE
    methodVARCHAR(50)FalseFalse—
    specificityFLOATTrueFalse—
    sample_countINTEGERFalseFalseapplication=0
    true_negativesINTEGERFalseFalseapplication=0
    false_positivesINTEGERFalseFalseapplication=0
    insufficient_reasonVARCHAR(200)TrueFalse—
    window_daysINTEGERFalseFalseapplication=90
    computed_atDATETIMEFalseFalseserver=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: project_id
    • PrimaryKeyConstraint unnamed: id
    • Index ux_flaky_calibration_project_method (unique=True): flaky_classifier_calibration.project_id, flaky_classifier_calibration.method; options {}

    backend/app/models/postgres.py:4908

    Project-level flaky test coaching results with quarantine recommendations.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    project_idUUIDFalseFalse—projects.id / CASCADE
    test_fingerprintVARCHAR(64)FalseFalse—
    test_nameVARCHAR(1000)FalseFalse—
    suite_nameVARCHAR(500)TrueFalse—
    failure_rateFLOATFalseFalse—
    total_runsINTEGERFalseFalseapplication=0
    failed_runsINTEGERFalseFalseapplication=0
    flaky_sinceDATETIMETrueFalse—
    last_failure_atDATETIMETrueFalse—
    quarantine_recommendationVARCHAR(30)FalseFalseapplication=MONITOR
    stabilization_actionsJSONTrueFalseapplication=list
    impact_scoreFLOATFalseFalseapplication=0.0
    status_historyJSONTrueFalseapplication=list
    flaky_confidence_lowFLOATTrueFalse—
    flaky_confidence_highFLOATTrueFalse—
    is_flaky_confidenceFLOATTrueFalse—
    created_atDATETIMEFalseFalseserver=now()
    updated_atDATETIMEFalseFalseserver=now(); onupdate=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: project_id
    • PrimaryKeyConstraint unnamed: id
    • Index ix_fcr_fingerprint (unique=False): flaky_coach_results.test_fingerprint; options {}
    • Index ix_fcr_project (unique=False): flaky_coach_results.project_id; options {}
    • Index ix_fcr_quarantine (unique=False): flaky_coach_results.quarantine_recommendation; options {}

    backend/app/models/postgres.py:7030

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    project_idUUIDFalseFalse—projects.id / CASCADE
    test_fingerprintVARCHAR(64)FalseFalse—
    test_nameVARCHAR(1000)TrueFalse—
    first_seen_atDATETIMEFalseFalse—
    first_seen_is_exactBOOLEANFalseFalseapplication=False
    screen_reasonVARCHAR(32)FalseFalse—
    first_screened_atDATETIMETrueFalse—
    first_scored_atDATETIMETrueFalse—
    first_scored_confidenceVARCHAR(20)TrueFalse—
    observation_countINTEGERFalseFalseapplication=0
    last_swept_atDATETIMETrueFalse—
    updated_atDATETIMEFalseFalseserver=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: project_id
    • PrimaryKeyConstraint unnamed: id
    • Index ix_flaky_detection_state_project_scored (unique=False): flaky_detection_state.project_id, flaky_detection_state.first_scored_at; options {}
    • Index ix_flaky_detection_state_project_swept (unique=False): flaky_detection_state.project_id, flaky_detection_state.last_swept_at; options {}
    • Index ux_flaky_detection_state_project_fingerprint (unique=True): flaky_detection_state.project_id, flaky_detection_state.test_fingerprint; options {}

    backend/app/models/postgres.py:7533

    Workflow record for proposing, approving, and enforcing quarantine of a flaky test.

    One row per quarantine lifecycle. When an approved quarantine is RELEASED and the test flakes again, a fresh row is created rather than reusing the old one — this keeps the history immutable and lets QA leads see every past decision on the same test.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    project_idUUIDFalseFalse—projects.id / CASCADE
    test_fingerprintVARCHAR(64)FalseFalse—
    test_nameVARCHAR(500)TrueFalse—
    suite_nameVARCHAR(500)TrueFalse—
    statusVARCHAR(32)FalseFalseapplication=PROPOSED
    detection_methodVARCHAR(50)FalseFalseapplication=pass_fail_ratio
    flip_rateFLOATTrueFalse—
    flip_window_sizeINTEGERTrueFalse—
    pass_countINTEGERTrueFalse—
    fail_countINTEGERTrueFalse—
    detected_atDATETIMEFalseFalseserver=now()
    last_failure_atDATETIMETrueFalse—
    proposed_atDATETIMETrueFalse—
    approved_atDATETIMETrueFalse—
    approved_by_user_idUUIDTrueFalse—users.id / SET NULL
    rejected_atDATETIMETrueFalse—
    rejected_by_user_idUUIDTrueFalse—users.id / SET NULL
    quarantine_startDATETIMETrueFalse—
    quarantine_expires_atDATETIMETrueFalse—
    quarantine_duration_daysINTEGERFalseFalseapplication=14
    recheck_atDATETIMETrueFalse—
    rationaleJSONBTrueFalse—
    reviewer_notesTEXTTrueFalse—
    owner_user_idUUIDTrueFalse—users.id / SET NULL
    defect_idUUIDTrueFalse—defects.id / SET NULL
    sla_daysINTEGERTrueFalse—
    stale_atDATETIMETrueFalse—
    stale_notified_atDATETIMETrueFalse—
    consecutive_passesINTEGERFalseFalseapplication=0
    last_stability_run_idUUIDTrueFalse—test_runs.id / SET NULL
    ready_to_promoteBOOLEANFalseFalseapplication=False
    ready_notified_atDATETIMETrueFalse—
    created_atDATETIMEFalseFalseserver=now()
    updated_atDATETIMEFalseFalseserver=now(); onupdate=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: approved_by_user_id
    • ForeignKeyConstraint unnamed: defect_id
    • ForeignKeyConstraint unnamed: last_stability_run_id
    • ForeignKeyConstraint unnamed: owner_user_id
    • ForeignKeyConstraint unnamed: project_id
    • ForeignKeyConstraint unnamed: rejected_by_user_id
    • PrimaryKeyConstraint unnamed: id
    • Index ix_fqr_fingerprint (unique=False): flaky_quarantine_requests.project_id, flaky_quarantine_requests.test_fingerprint; options {}
    • Index ix_fqr_project_status (unique=False): flaky_quarantine_requests.project_id, flaky_quarantine_requests.status; options {}

    backend/app/models/postgres.py:6960

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    project_idUUIDFalseFalse—projects.id / CASCADE
    test_fingerprintVARCHAR(64)FalseFalse—
    test_nameVARCHAR(1000)TrueFalse—
    scoreFLOATFalseFalseapplication=0.0
    componentsJSONBFalseFalseapplication=dict
    weightsJSONBFalseFalseapplication=dict
    observation_countINTEGERFalseFalseapplication=0
    confidenceVARCHAR(20)FalseFalseapplication=none
    window_daysINTEGERFalseFalseapplication=30
    computed_atDATETIMEFalseFalseserver=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: project_id
    • PrimaryKeyConstraint unnamed: id
    • Index ix_flaky_score_project_score (unique=False): flaky_score.project_id, flaky_score.score; options {}
    • Index ux_flaky_score_project_fingerprint (unique=True): flaky_score.project_id, flaky_score.test_fingerprint; options {}

    backend/app/models/postgres.py:3685

    One row per AI test-case generation request (grounded or raw).

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    project_idUUIDFalseFalse—projects.id / CASCADE
    created_by_idUUIDTrueFalse—users.id / SET NULL
    prompt_textTEXTTrueFalse—
    source_idsJSONTrueFalse—
    generation_modeVARCHAR(20)FalseFalseapplication=raw
    generation_configJSONTrueFalse—
    cases_generatedINTEGERFalseFalseapplication=0
    cases_acceptedINTEGERFalseFalseapplication=0
    cases_rejectedINTEGERFalseFalseapplication=0
    coverage_scoreINTEGERTrueFalse—
    statusVARCHAR(20)FalseFalseapplication=pending
    error_messageTEXTTrueFalse—
    prompt_redactedBOOLEANFalseFalseapplication=False
    llm_model_usedVARCHAR(200)TrueFalse—
    created_atDATETIMEFalseFalseserver=now()
    completed_atDATETIMETrueFalse—

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: created_by_id
    • ForeignKeyConstraint unnamed: project_id
    • PrimaryKeyConstraint unnamed: id
    • Index ix_gb_author (unique=False): generation_batches.created_by_id; options {}
    • Index ix_gb_project_created (unique=False): generation_batches.project_id, generation_batches.created_at; options {}

    backend/app/models/postgres.py:3716

    Maps a generated ManagedTestCase to the KnowledgeChunks cited for it.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    batch_idUUIDFalseFalse—generation_batches.id / CASCADE
    case_idUUIDFalseFalse—managed_test_cases.id / CASCADE
    source_idUUIDFalseFalse—knowledge_sources.id / CASCADE
    chunk_vector_idVARCHAR(64)FalseFalse—
    relevance_scoreFLOATTrueFalse—
    section_headingVARCHAR(500)TrueFalse—
    chunk_text_previewVARCHAR(500)TrueFalse—
    source_content_hash_at_generationVARCHAR(64)TrueFalse—
    is_staleBOOLEANFalseFalseapplication=False
    stale_detected_atDATETIMETrueFalse—
    created_atDATETIMEFalseFalseserver=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: batch_id
    • ForeignKeyConstraint unnamed: case_id
    • ForeignKeyConstraint unnamed: source_id
    • PrimaryKeyConstraint unnamed: id
    • UniqueConstraint uq_gcs_case_chunk: case_id, chunk_vector_id
    • Index ix_gcs_batch_id (unique=False): generation_case_sources.batch_id; options {}
    • Index ix_gcs_case_id (unique=False): generation_case_sources.case_id; options {}
    • Index ix_gcs_source_id (unique=False): generation_case_sources.source_id; options {}
    • Index ix_gcs_stale (unique=False): generation_case_sources.is_stale; options {}

    backend/app/models/postgres.py:7265

    Per-project GitHub Checks API integration config.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    project_idUUIDFalseFalse—projects.id / CASCADE
    enabledBOOLEANFalseFalseapplication=False
    repo_ownerVARCHAR(255)FalseFalse—
    repo_nameVARCHAR(255)FalseFalse—
    api_base_urlVARCHAR(500)FalseFalseapplication=https://api.github.com
    has_patBOOLEANFalseFalseapplication=False
    auth_kindVARCHAR(10)FalseFalseserver=pat; application=pat
    installation_idBIGINTTrueFalse—
    pr_comment_modeVARCHAR(20)FalseFalseserver=failures_only; application=failures_only
    last_posted_atDATETIMETrueFalse—
    last_errorTEXTTrueFalse—
    last_error_atDATETIMETrueFalse—
    created_atDATETIMEFalseFalseserver=now()
    updated_atDATETIMEFalseFalseserver=now(); onupdate=now()
    updated_by_user_idUUIDTrueFalse—users.id / SET NULL

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: project_id
    • ForeignKeyConstraint unnamed: updated_by_user_id
    • PrimaryKeyConstraint unnamed: id
    • UniqueConstraint unnamed: project_id
    • Index ix_github_integrations_project (unique=True): github_integrations.project_id; options {}

    backend/app/models/postgres.py:7330

    Per-project GitLab integration config.

    Mirror of GitHubIntegration for GitLab (self-managed or gitlab.com). Drives the sticky MR note and commit status outbound surfaces. The PAT lives in secret_service under scope gitlab_integration, key project:{project_id}:pat — never a column, so a DB dump cannot recover active tokens.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    project_idUUIDFalseFalse—projects.id / CASCADE
    enabledBOOLEANFalseFalseserver=false; application=False
    base_urlVARCHAR(500)FalseFalseserver=https://gitlab.com; application=https://gitlab.com
    project_pathVARCHAR(500)FalseFalseserver=; application=
    mr_comment_modeVARCHAR(20)FalseFalseserver=failures_only; application=failures_only
    commit_status_enabledBOOLEANFalseFalseserver=true; application=True
    has_patBOOLEANFalseFalseserver=false; application=False
    last_posted_atDATETIMETrueFalse—
    last_errorTEXTTrueFalse—
    last_error_atDATETIMETrueFalse—
    created_atDATETIMEFalseFalseserver=now()
    updated_atDATETIMEFalseFalseserver=now(); onupdate=now()
    updated_by_user_idUUIDTrueFalse—users.id / SET NULL

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: project_id
    • ForeignKeyConstraint unnamed: updated_by_user_id
    • PrimaryKeyConstraint unnamed: id
    • UniqueConstraint unnamed: project_id
    • Index ix_gitlab_integrations_project (unique=True): gitlab_integrations.project_id; options {}

    backend/app/models/postgres.py:5131

    Append-only audit trail for SSO, SCIM, and identity lifecycle events.

    Append-only by application convention, not by database enforcement — no UPDATE trigger, no revoked grant, no WORM storage. The property is held by the backend.audit-write-discipline quality gate (scripts/quality_gate.py), which fails CI on application code that UPDATEs an audit table or DELETEs from one outside services/retention_service.py.

    Not currently covered by any retention clock: the purge does not touch this table (it has no project scope), so rows accumulate until an operator prunes them out-of-band.

    Beyond that, keeping the rows safe is the operator’s responsibility: direct Postgres credentials can still rewrite or drop rows. Real immutability comes from restricted UPDATE/DELETE grants for the app role, WORM / object-lock storage for shipped logs, and off-host backups.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    event_typeVARCHAR(40)FalseFalse—
    user_idUUIDTrueFalse—users.id / SET NULL
    sso_config_idUUIDTrueFalse—sso_configurations.id / SET NULL
    actor_idUUIDTrueFalse—users.id / SET NULL
    actor_nameVARCHAR(200)TrueFalse—
    detailJSONTrueFalse—
    ip_addressVARCHAR(45)TrueFalse—
    successBOOLEANFalseFalseapplication=True
    error_messageTEXTTrueFalse—
    created_atDATETIMEFalseFalseserver=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: actor_id
    • ForeignKeyConstraint unnamed: sso_config_id
    • ForeignKeyConstraint unnamed: user_id
    • PrimaryKeyConstraint unnamed: id
    • Index ix_identity_event_created (unique=False): identity_events.created_at; options {}
    • Index ix_identity_event_type (unique=False): identity_events.event_type; options {}
    • Index ix_identity_event_user (unique=False): identity_events.user_id; options {}

    backend/app/models/postgres.py:1976

    The last evaluation of each detector for each project (migration 0243).

    Absence is not health: a detector that could not measure stores measured = False with a reason_code (an insight_vocab.NOT_MEASURED key), and the list shows that sentence instead of “no insights”.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    project_idUUIDFalseTrue—projects.id / CASCADE
    kindVARCHAR(20)FalseTrue—
    evaluated_atDATETIMEFalseFalse—
    measuredBOOLEANFalseFalse—
    reason_codeVARCHAR(40)TrueFalse—
    subjects_measuredINTEGERFalseFalseserver=0; application=0
    firingINTEGERFalseFalseserver=0; application=0

    Constraints and indexes:

    • CheckConstraint ck_insight_detector_states_kind: kind IN ('perf_drift', 'flake_rising', 'coverage_drop', 'cost_budget')
    • ForeignKeyConstraint unnamed: project_id
    • PrimaryKeyConstraint unnamed: project_id, kind

    backend/app/models/postgres.py:8315

    The instance-wide retention defaults (migration 0242).

    ONE row (id = 1, CHECK ck_instance_retention_single_row; inserted by the migration). A project that leaves a field NULL (or has no policy row) inherits the field from here, per field; a NULL here means the code default (retention_service.EffectiveRetentionPolicy).

    enabled ships false and is switched on only by the previewed, typed, audited admin action (retention_instance_service.enable_instance_policy); a project’s explicit enabled = false is never overridden. enabled_at / enabled_by_id record the last enable. The AI caps are NOT inherited.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idSMALLINTFalseTrueapplication=1
    enabledBOOLEANFalseFalseserver=false; application=False
    raw_events_daysINTEGERTrueFalse—
    runs_daysINTEGERTrueFalse—
    artifacts_daysINTEGERTrueFalse—
    audit_daysINTEGERTrueFalse—
    keep_last_runsINTEGERTrueFalse—
    purge_window_start_utcSMALLINTTrueFalse—
    enabled_atDATETIMETrueFalse—
    enabled_by_idUUIDTrueFalse—users.id / SET NULL
    updated_atDATETIMEFalseFalseserver=now(); onupdate=now()
    updated_by_user_idUUIDTrueFalse—users.id / SET NULL

    Constraints and indexes:

    • CheckConstraint ck_instance_retention_keep_last: keep_last_runs IS NULL OR keep_last_runs BETWEEN 0 AND 1000
    • CheckConstraint ck_instance_retention_single_row: id = 1
    • CheckConstraint ck_instance_retention_window: purge_window_start_utc IS NULL OR purge_window_start_utc BETWEEN 0 AND 23
    • ForeignKeyConstraint unnamed: enabled_by_id
    • ForeignKeyConstraint unnamed: updated_by_user_id
    • PrimaryKeyConstraint unnamed: id

    backend/app/models/postgres.py:4726

    Integration health status: the latest snapshot per (provider, scope).

    Scope '' = deployment-wide (every row before migration 0223); else a project id (per-project GitHub / GitLab rows). The primary key is (provider, scope_key) since 0223.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    providerVARCHAR(50)FalseTrue—
    scope_keyVARCHAR(64)FalseTrueserver=; application=
    statusVARCHAR(20)FalseFalseapplication=unknown
    last_checked_atDATETIMETrueFalse—
    messageTEXTTrueFalse—
    response_msINTEGERTrueFalse—
    consecutive_failuresINTEGERFalseFalseapplication=0
    last_success_atDATETIMETrueFalse—

    Constraints and indexes:

    • PrimaryKeyConstraint unnamed: provider, scope_key

    backend/app/models/postgres.py:4745

    Historical record of each integration health probe.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    providerVARCHAR(50)FalseFalse—
    scope_keyVARCHAR(64)FalseFalseserver=; application=
    statusVARCHAR(20)FalseFalse—
    response_msINTEGERTrueFalse—
    messageTEXTTrueFalse—
    auth_validBOOLEANTrueFalse—
    payload_validBOOLEANTrueFalse—
    checked_atDATETIMEFalseFalseserver=now()

    Constraints and indexes:

    • PrimaryKeyConstraint unnamed: id
    • Index ix_ipr_provider_scope_time (unique=False): integration_probe_results.provider, integration_probe_results.scope_key, integration_probe_results.checked_at; options {}
    • Index ix_ipr_provider_time (unique=False): integration_probe_results.provider, integration_probe_results.checked_at; options {}

    backend/app/models/postgres.py:3655

    PostgreSQL metadata for each chunk stored in ChromaDB knowledge_chunks collection.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    source_idUUIDFalseFalse—knowledge_sources.id / CASCADE
    project_idUUIDFalseFalse—projects.id / CASCADE
    vector_idVARCHAR(64)FalseFalse—
    section_headingVARCHAR(500)TrueFalse—
    requirement_idVARCHAR(200)TrueFalse—
    chunk_indexINTEGERFalseFalse—
    chunk_text_previewVARCHAR(500)TrueFalse—
    token_countINTEGERTrueFalse—
    sync_versionINTEGERFalseFalseapplication=1
    is_activeBOOLEANFalseFalseapplication=True
    created_atDATETIMEFalseFalseserver=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: project_id
    • ForeignKeyConstraint unnamed: source_id
    • PrimaryKeyConstraint unnamed: id
    • UniqueConstraint unnamed: vector_id
    • Index ix_kc_project_active (unique=False): knowledge_chunks.project_id, knowledge_chunks.is_active; options {}
    • Index ix_kc_source_active (unique=False): knowledge_chunks.source_id, knowledge_chunks.is_active; options {}
    • Index ix_kc_sync_version (unique=False): knowledge_chunks.source_id, knowledge_chunks.sync_version; options {}

    backend/app/models/postgres.py:3594

    Registry of external knowledge sources attached to a project for RAG-grounded test generation.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    project_idUUIDFalseFalse—projects.id / CASCADE
    source_typeVARCHAR(30)FalseFalse—
    titleVARCHAR(500)FalseFalse—
    canonical_urlVARCHAR(2000)FalseFalse—
    external_idVARCHAR(500)TrueFalse—
    owner_idUUIDTrueFalse—users.id / SET NULL
    sync_statusVARCHAR(20)FalseFalseapplication=pending
    last_synced_atDATETIMETrueFalse—
    sync_errorTEXTTrueFalse—
    content_hashVARCHAR(64)TrueFalse—
    classificationVARCHAR(20)FalseFalseapplication=internal
    is_archivedBOOLEANFalseFalseapplication=False
    storage_pathVARCHAR(1000)TrueFalse—
    created_atDATETIMEFalseFalseserver=now()
    updated_atDATETIMEFalseFalseserver=now(); onupdate=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: owner_id
    • ForeignKeyConstraint unnamed: project_id
    • PrimaryKeyConstraint unnamed: id
    • UniqueConstraint uq_ks_project_url: project_id, canonical_url
    • Index ix_ks_owner (unique=False): knowledge_sources.owner_id; options {}
    • Index ix_ks_project_status (unique=False): knowledge_sources.project_id, knowledge_sources.sync_status; options {}
    • Index ix_ks_project_type (unique=False): knowledge_sources.project_id, knowledge_sources.source_type; options {}

    backend/app/models/postgres.py:3629

    Audit log for every sync attempt on a KnowledgeSource.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    source_idUUIDFalseFalse—knowledge_sources.id / CASCADE
    project_idUUIDFalseFalse—projects.id / CASCADE
    triggerVARCHAR(20)FalseFalseapplication=manual
    statusVARCHAR(20)FalseFalse—
    content_hashVARCHAR(64)TrueFalse—
    previous_hashVARCHAR(64)TrueFalse—
    content_changedBOOLEANFalseFalseapplication=False
    chunk_countINTEGERTrueFalse—
    duration_msINTEGERTrueFalse—
    error_messageTEXTTrueFalse—
    created_atDATETIMEFalseFalseserver=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: project_id
    • ForeignKeyConstraint unnamed: source_id
    • PrimaryKeyConstraint unnamed: id
    • Index ix_kse_project_created (unique=False): knowledge_sync_events.project_id, knowledge_sync_events.created_at; options {}
    • Index ix_kse_source_created (unique=False): knowledge_sync_events.source_id, knowledge_sync_events.created_at; options {}

    backend/app/models/postgres.py:595

    A receipt for each event, so that the same event is not processed twice, plus limited, cleaned-up evidence of each attempt.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    attempt_idUUIDFalseFalse—live_ingestion_attempts.id / CASCADE
    run_idUUIDFalseFalse—test_runs.id / CASCADE
    event_idVARCHAR(64)FalseFalse—
    stream_idVARCHAR(64)FalseFalse—
    event_indexINTEGERFalseFalse—
    event_typeVARCHAR(50)FalseFalse—
    payloadJSONBFalseFalseapplication=dict
    projected_atDATETIMEFalseFalseserver=now()

    Constraints and indexes:

    • CheckConstraint ck_live_event_receipt_index: event_index >= 0
    • ForeignKeyConstraint unnamed: attempt_id
    • ForeignKeyConstraint unnamed: run_id
    • PrimaryKeyConstraint unnamed: id
    • UniqueConstraint uq_live_event_receipt_event: event_id
    • Index ix_live_event_receipt_run_stream (unique=False): live_event_receipts.run_id, live_event_receipts.stream_id; options {}

    backend/app/models/postgres.py:562

    A saved record that an accepted live batch reached the stored run data.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    run_idUUIDFalseFalse—test_runs.id / CASCADE
    session_idVARCHAR(255)FalseFalse—
    batch_idVARCHAR(255)FalseFalse—
    event_countINTEGERFalseFalse—
    first_event_idVARCHAR(64)TrueFalse—
    last_event_idVARCHAR(64)TrueFalse—
    first_stream_idVARCHAR(64)TrueFalse—
    last_stream_idVARCHAR(64)TrueFalse—
    projected_atDATETIMEFalseFalseserver=now()
    created_atDATETIMEFalseFalseserver=now()

    Constraints and indexes:

    • CheckConstraint ck_live_ingestion_attempt_count: event_count >= 0
    • ForeignKeyConstraint unnamed: run_id
    • PrimaryKeyConstraint unnamed: id
    • UniqueConstraint uq_live_ingestion_attempt_session_batch: session_id, batch_id
    • Index ix_live_ingestion_attempt_run_created (unique=False): live_ingestion_attempts.run_id, live_ingestion_attempts.created_at; options {}

    backend/app/models/postgres.py:624

    Committed per-run high watermark for Redis evidence projection.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    run_idUUIDFalseTrue—test_runs.id / CASCADE
    stream_keyVARCHAR(500)FalseFalse—
    consumer_groupVARCHAR(100)FalseFalse—
    last_stream_idVARCHAR(64)FalseFalse—
    last_event_idVARCHAR(64)FalseFalse—
    updated_atDATETIMEFalseFalseserver=now(); onupdate=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: run_id
    • PrimaryKeyConstraint unnamed: run_id

    backend/app/models/postgres.py:3763

    Tracks an active live test execution session from a client machine.

    Client machines register a session before streaming events. The session_token (stored as a SHA-256 hash here; plaintext lives in Redis) is used for lightweight authentication on the hot-path batch endpoint — avoiding JWT decode + DB query overhead at 10k+ concurrent sessions.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    project_idUUIDFalseFalse—projects.id / CASCADE
    run_idVARCHAR(100)FalseFalse—
    client_nameVARCHAR(255)FalseFalse—
    machine_idVARCHAR(255)TrueFalse—
    build_numberVARCHAR(100)TrueFalse—
    frameworkVARCHAR(50)TrueFalse—
    branchVARCHAR(255)TrueFalse—
    commit_hashVARCHAR(64)TrueFalse—
    session_token_hashVARCHAR(64)FalseFalse—
    statusVARCHAR(20)FalseFalseapplication=active
    release_nameVARCHAR(255)TrueFalse—
    launch_nameVARCHAR(255)TrueFalse—
    suite_nameVARCHAR(500)TrueFalse—
    total_testsINTEGERFalseFalseapplication=0
    events_receivedINTEGERFalseFalseapplication=0
    extra_metadataJSONTrueFalse—
    started_atDATETIMEFalseFalse—
    last_event_atDATETIMETrueFalse—
    completed_atDATETIMETrueFalse—
    created_atDATETIMEFalseFalseserver=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: project_id
    • PrimaryKeyConstraint unnamed: id
    • Index ix_live_sessions_project_status (unique=False): live_sessions.project_id, live_sessions.status; options {}
    • Index ix_live_sessions_run_id (unique=False): live_sessions.run_id; options {}
    • Index ix_live_sessions_session_token_hash (unique=True): live_sessions.session_token_hash; options {}
    • Index ix_live_sessions_started_at (unique=False): live_sessions.started_at; options {}
    • Index ix_live_sessions_status (unique=False): live_sessions.status; options {}
    • Index ix_live_sessions_token_hash (unique=False): live_sessions.session_token_hash; options {}
    • Index ux_live_sessions_active_project_run (unique=True): live_sessions.project_id, live_sessions.run_id; options {"postgresql_where": "status = 'active'"}

    backend/app/models/postgres.py:3095

    Manually authored or AI-generated test case with full lifecycle management.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    project_idUUIDFalseFalse—projects.id / CASCADE
    titleVARCHAR(500)FalseFalse—
    descriptionTEXTTrueFalse—
    objectiveTEXTTrueFalse—
    preconditionsTEXTTrueFalse—
    stepsJSONTrueFalse—
    expected_resultTEXTTrueFalse—
    test_dataTEXTTrueFalse—
    test_typeVARCHAR(50)FalseFalseapplication=functional
    priorityVARCHAR(20)FalseFalseapplication=medium
    severityVARCHAR(20)FalseFalseapplication=major
    feature_areaVARCHAR(500)TrueFalse—
    suite_nameVARCHAR(500)TrueFalse—
    test_suite_idUUIDTrueFalse—test_suites.id / SET NULL
    tagsJSONTrueFalse—
    parametersJSONTrueFalse—
    statusVARCHAR(30)FalseFalseapplication=draft
    versionINTEGERFalseFalseapplication=1
    lifecycle_state_changed_atDATETIMETrueFalse—
    approved_atDATETIMETrueFalse—
    approved_by_idUUIDTrueFalse—users.id / SET NULL
    needs_update_reasonVARCHAR(100)TrueFalse—
    deprecation_reasonVARCHAR(500)TrueFalse—
    deprecated_atDATETIMETrueFalse—
    deprecated_by_idUUIDTrueFalse—users.id / SET NULL
    archived_atDATETIMETrueFalse—
    archived_by_idUUIDTrueFalse—users.id / SET NULL
    edited_since_approval_versionINTEGERTrueFalse—
    author_idUUIDTrueFalse—users.id / SET NULL
    assignee_idUUIDTrueFalse—users.id / SET NULL
    reviewer_idUUIDTrueFalse—users.id / SET NULL
    is_automatedBOOLEANFalseFalseapplication=False
    automation_statusVARCHAR(30)FalseFalseapplication=not_automated
    test_fingerprintVARCHAR(64)TrueFalse—
    dup_fingerprintVARCHAR(64)TrueFalse—
    ai_generatedBOOLEANFalseFalseapplication=False
    ai_generation_promptTEXTTrueFalse—
    ai_quality_scoreINTEGERTrueFalse—
    ai_review_notesJSONTrueFalse—
    estimated_duration_minutesINTEGERTrueFalse—
    last_executed_atDATETIMETrueFalse—
    last_execution_statusVARCHAR(20)TrueFalse—
    generation_batch_idUUIDTrueFalse—generation_batches.id / SET NULL
    is_staleBOOLEANFalseFalseapplication=False
    stale_reasonTEXTTrueFalse—
    faithfulness_scoreFLOATTrueFalse—
    faithfulness_evaluatorVARCHAR(30)TrueFalse—
    faithfulness_evaluated_atDATETIMETrueFalse—
    needs_review_reasonVARCHAR(500)TrueFalse—
    created_atDATETIMEFalseFalseserver=now()
    updated_atDATETIMEFalseFalseserver=now(); onupdate=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: approved_by_id
    • ForeignKeyConstraint unnamed: archived_by_id
    • ForeignKeyConstraint unnamed: assignee_id
    • ForeignKeyConstraint unnamed: author_id
    • ForeignKeyConstraint unnamed: deprecated_by_id
    • ForeignKeyConstraint unnamed: generation_batch_id
    • ForeignKeyConstraint unnamed: project_id
    • ForeignKeyConstraint unnamed: reviewer_id
    • ForeignKeyConstraint unnamed: test_suite_id
    • PrimaryKeyConstraint unnamed: id
    • Index ix_managed_test_cases_status (unique=False): managed_test_cases.status; options {}
    • Index ix_mtc_assignee (unique=False): managed_test_cases.assignee_id; options {"postgresql_where": "assignee_id IS NOT NULL"}
    • Index ix_mtc_author (unique=False): managed_test_cases.author_id; options {}
    • Index ix_mtc_dup_fingerprint (unique=False): managed_test_cases.dup_fingerprint; options {}
    • Index ix_mtc_fingerprint (unique=False): managed_test_cases.test_fingerprint; options {}
    • Index ix_mtc_project_fingerprint (unique=False): managed_test_cases.project_id, managed_test_cases.test_fingerprint; options {}
    • Index ix_mtc_project_last_executed (unique=False): managed_test_cases.project_id, managed_test_cases.last_executed_at; options {}
    • Index ix_mtc_project_status (unique=False): managed_test_cases.project_id, managed_test_cases.status; options {}

    backend/app/models/postgres.py:238

    Single-use recovery code for a user with TOTP enabled.

    Only the SHA-256 digest is stored, so a database compromise cannot recover a usable code. Recovery codes are minted with ~130 bits of entropy from secrets.token_hex, which is why a plain digest is adequate here and a password KDF is not — there is nothing to brute-force. (Same reasoning as ApiKey.key_hash and ReportShareLink.token_hash.)

    A used code is retained with used_at set rather than deleted, so “which codes are still live” and “a recovery code was burned on <date>” both remain answerable. Rows are deleted only when the whole set is regenerated or MFA is disabled.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    user_idUUIDFalseFalse—users.id / CASCADE
    code_hashVARCHAR(64)FalseFalse—
    used_atDATETIMETrueFalse—
    created_atDATETIMEFalseFalseserver=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: user_id
    • PrimaryKeyConstraint unnamed: id
    • UniqueConstraint unnamed: code_hash
    • Index ix_mfa_recovery_code_hash (unique=True): mfa_recovery_codes.code_hash; options {}
    • Index ix_mfa_recovery_user (unique=False): mfa_recovery_codes.user_id; options {}

    backend/app/models/postgres.py:2851

    Registry of fine-tuned model versions per training track.

    Tracks the full lifecycle: training → evaluation → active/retired. The model_registry service uses this table + Redis for hot-swap lookups.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    trackVARCHAR(30)FalseFalse—
    model_nameVARCHAR(200)FalseFalse—
    providerVARCHAR(30)FalseFalse—
    statusVARCHAR(20)FalseFalseapplication=training
    training_examplesINTEGERFalseFalseapplication=0
    holdout_examplesINTEGERFalseFalseapplication=0
    eval_accuracyFLOATTrueFalse—
    baseline_accuracyFLOATTrueFalse—
    eval_detailsJSONTrueFalse—
    provider_job_idVARCHAR(200)TrueFalse—
    training_file_pathVARCHAR(1000)TrueFalse—
    promoted_atDATETIMETrueFalse—
    retired_atDATETIMETrueFalse—
    created_atDATETIMEFalseFalseserver=now()
    updated_atDATETIMEFalseFalseserver=now(); onupdate=now()

    Constraints and indexes:

    • PrimaryKeyConstraint unnamed: id
    • Index ix_model_versions_track_status (unique=False): model_versions.track, model_versions.status; options {}

    backend/app/models/postgres.py:3031

    Audit trail for every dispatched notification.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    user_idUUIDTrueFalse—users.id / SET NULL
    project_idUUIDTrueFalse—projects.id / SET NULL
    run_idUUIDTrueFalse—test_runs.id / SET NULL
    preference_idUUIDTrueFalse—notification_preferences.id / SET NULL
    delivery_keyVARCHAR(64)TrueFalse—
    delivery_metadataJSONBTrueFalse—
    delivery_attemptsINTEGERFalseFalseserver=0; application=0
    delivery_tokenUUIDTrueFalse—
    delivery_started_atDATETIMETrueFalse—
    delivery_lease_expires_atDATETIMETrueFalse—
    next_delivery_atDATETIMETrueFalse—
    channelVARCHAR(20)FalseFalse—
    event_typeVARCHAR(50)FalseFalse—
    titleVARCHAR(500)FalseFalse—
    bodyTEXTFalseFalse—
    statusVARCHAR(20)FalseFalseapplication=pending
    error_detailTEXTTrueFalse—
    routed_teamVARCHAR(255)TrueFalse—
    routing_fallbackVARCHAR(50)TrueFalse—
    is_readBOOLEANFalseFalseapplication=False
    sent_atDATETIMETrueFalse—
    created_atDATETIMEFalseFalseserver=now()
    updated_atDATETIMEFalseFalseserver=now(); onupdate=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: preference_id
    • ForeignKeyConstraint unnamed: project_id
    • ForeignKeyConstraint unnamed: run_id
    • ForeignKeyConstraint unnamed: user_id
    • PrimaryKeyConstraint unnamed: id
    • Index ix_notif_log_project (unique=False): notification_logs.project_id; options {}
    • Index ix_notif_log_user_created (unique=False): notification_logs.user_id, notification_logs.created_at; options {}
    • Index ix_notification_log_delivery_due (unique=False): notification_logs.status, notification_logs.next_delivery_at; options {}
    • Index uq_notification_log_delivery_key (unique=True): notification_logs.delivery_key; options {}

    backend/app/models/postgres.py:2130

    Per-user, per-channel notification configuration.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    user_idUUIDFalseFalse—users.id / CASCADE
    project_idUUIDTrueFalse—projects.id / CASCADE
    channelVARCHAR(20)FalseFalse—
    enabledBOOLEANFalseFalseapplication=True
    eventsJSONBFalseFalseapplication=list
    failure_rate_thresholdFLOATTrueFalseapplication=80.0
    email_overrideVARCHAR(255)TrueFalse—
    slack_webhook_urlVARCHAR(2000)TrueFalse—
    teams_webhook_urlVARCHAR(2000)TrueFalse—
    created_atDATETIMEFalseFalseserver=now()
    updated_atDATETIMEFalseFalseserver=now(); onupdate=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: project_id
    • ForeignKeyConstraint unnamed: user_id
    • PrimaryKeyConstraint unnamed: id
    • UniqueConstraint uq_notif_pref: user_id, project_id, channel

    backend/app/models/postgres.py:2158

    Per-(project, test_fingerprint) rolling state for transition-based notifications.

    The transition engine (services/notification_transitions.py) updates one row per logical test at run finalization and emits a notification only when the tracked state CHANGES (pass→confirmed-failing, failing→recovered, entered the known-flaky set). last_run_id is the anchor that makes repeats safe: re-finalizing the same run skips rows already stamped with that run, so transitions never double-fire (per-(entity, run) convention for safe repeats).

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    project_idUUIDFalseFalse—projects.id / CASCADE
    test_fingerprintVARCHAR(64)FalseFalse—
    stateVARCHAR(20)FalseFalseapplication=passing
    consecutive_failuresINTEGERFalseFalseapplication=0
    last_notified_stateVARCHAR(20)TrueFalse—
    is_known_flakyBOOLEANFalseFalseapplication=False
    last_run_idUUIDTrueFalse—test_runs.id / SET NULL
    created_atDATETIMEFalseFalseserver=now()
    updated_atDATETIMEFalseFalseserver=now(); onupdate=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: last_run_id
    • ForeignKeyConstraint unnamed: project_id
    • PrimaryKeyConstraint unnamed: id
    • UniqueConstraint uq_notif_test_state_project_fp: project_id, test_fingerprint
    • Index ix_notif_test_states_project (unique=False): notification_test_states.project_id; options {}

    backend/app/models/postgres.py:2204

    Per-project policy for transition-based notifications.

    One row per project. Semantics of a missing row = the new-project default: transitions ON, per-run spam OFF. Migration 0103 backfills an explicit row (transitions OFF, per-run ON) for every project existing at upgrade time so EXISTING projects keep their current behaviour with no surprise change; projects created afterwards get the new defaults.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    project_idUUIDFalseFalse—projects.id / CASCADE
    transitions_enabledBOOLEANFalseFalseapplication=True
    per_run_events_enabledBOOLEANFalseFalseapplication=False
    enabled_eventsJSONBFalseFalseapplication=list
    consecutive_failure_thresholdINTEGERFalseFalseapplication=2
    created_atDATETIMEFalseFalseserver=now()
    updated_atDATETIMEFalseFalseserver=now(); onupdate=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: project_id
    • PrimaryKeyConstraint unnamed: id
    • UniqueConstraint unnamed: project_id

    backend/app/models/postgres.py:4865

    A single-use password-reset link (migration 0233). Hash only.

    issued_by_user_id NULL = the user asked for it (forgot password); set = an administrator issued it from User Management.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    user_idUUIDFalseFalse—users.id / CASCADE
    token_hashVARCHAR(64)FalseFalse—
    expires_atDATETIMEFalseFalse—
    used_atDATETIMETrueFalse—
    issued_by_user_idUUIDTrueFalse—users.id / SET NULL
    request_ip_hashVARCHAR(64)TrueFalse—
    created_atDATETIMEFalseFalseserver=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: issued_by_user_id
    • ForeignKeyConstraint unnamed: user_id
    • PrimaryKeyConstraint unnamed: id
    • UniqueConstraint unnamed: token_hash
    • Index ix_password_reset_tokens_user_id (unique=False): password_reset_tokens.user_id; options {}

    backend/app/models/postgres.py:7080

    Per-test running duration statistics.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    project_idUUIDFalseFalse—projects.id / CASCADE
    test_fingerprintVARCHAR(64)FalseFalse—
    test_nameVARCHAR(500)TrueFalse—
    suite_nameVARCHAR(500)TrueFalse—
    sample_countINTEGERFalseFalseapplication=0
    mean_msFLOATFalseFalseapplication=0.0
    m2FLOATFalseFalseapplication=0.0
    stddev_msFLOATFalseFalseapplication=0.0
    p95_msFLOATTrueFalse—
    last_observed_msINTEGERTrueFalse—
    last_observed_atDATETIMETrueFalse—
    created_atDATETIMEFalseFalseserver=now()
    updated_atDATETIMEFalseFalseserver=now(); onupdate=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: project_id
    • PrimaryKeyConstraint unnamed: id
    • UniqueConstraint uq_perf_baseline_fingerprint: project_id, test_fingerprint
    • Index ix_perf_baseline_project (unique=False): perf_baselines.project_id; options {}

    backend/app/models/postgres.py:4782

    Tracks product adoption events for analytics.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    user_idUUIDTrueFalse—users.id / SET NULL
    project_idUUIDTrueFalse—projects.id / SET NULL
    event_nameVARCHAR(100)FalseFalse—
    event_payloadJSONTrueFalse—
    created_atDATETIMEFalseFalseserver=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: project_id
    • ForeignKeyConstraint unnamed: user_id
    • PrimaryKeyConstraint unnamed: id
    • Index ix_pue_created (unique=False): product_usage_events.created_at; options {}
    • Index ix_pue_event (unique=False): product_usage_events.event_name; options {}
    • Index ix_pue_user (unique=False): product_usage_events.user_id; options {}

    backend/app/models/postgres.py:8406

    Append-only, project-scoped product feed of everything that happens.

    This is a derived read surface, not the compliance record. The four compliance tables (access_audit_logs, settings_audit_log, test_case_audit_logs, identity_events) keep their role and their retention guarantees; where a ledger row mirrors one of them it carries source_table / source_id so a reader can get back to the row that legally matters.

    Append-only by application convention, not by database enforcement — same boundary as the other audit tables, held by the backend.audit-write-discipline quality gate (scripts/quality_gate.py), which fails CI on application code that UPDATEs this table or DELETEs from it outside services/retention_service.py.

    Rows are removed by the retention purge on the audit clock (ProjectRetentionPolicy.audit_days; floor 365 days, validated to be

    = runs_days so ledger rows outlive the runs they describe), and only for projects that explicitly enabled a policy.

    ON DELETE CASCADE on project_id is deliberate and has one visible consequence: there is no project.deleted event here, because the row would be deleted by the very thing it records. That event goes to access_audit_logs, whose FK is ON DELETE SET NULL.

    entity_id is TEXT, not UUID, on purpose: live-stream sessions address runs by slug (LiveSession.run_id), and forcing a cast here would either drop those events or 500 the ingest path.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    project_idUUIDFalseFalse—projects.id / CASCADE
    release_idUUIDTrueFalse—releases.id / SET NULL
    occurred_atDATETIMEFalseFalseserver=now()
    recorded_atDATETIMEFalseFalseserver=now()
    categoryVARCHAR(20)FalseFalse—
    event_typeVARCHAR(60)FalseFalse—
    schema_versionSMALLINTFalseFalseapplication=1
    actor_typeVARCHAR(20)FalseFalse—
    actor_idUUIDTrueFalse—users.id / SET NULL
    actor_nameVARCHAR(200)TrueFalse—
    actor_refVARCHAR(120)TrueFalse—
    entity_typeVARCHAR(40)FalseFalse—
    entity_idVARCHAR(120)FalseFalse—
    entity_labelVARCHAR(300)TrueFalse—
    target_typeVARCHAR(40)TrueFalse—
    target_idVARCHAR(120)TrueFalse—
    summaryVARCHAR(500)FalseFalse—
    diffJSONTrueFalse—
    contextJSONTrueFalse—
    source_tableVARCHAR(40)TrueFalse—
    source_idUUIDTrueFalse—
    request_idVARCHAR(64)TrueFalse—
    group_keyVARCHAR(120)TrueFalse—

    Constraints and indexes:

    • CheckConstraint ck_pae_actor_type: actor_type IN ('user', 'api_key', 'service_account', 'system', 'agent')
    • CheckConstraint ck_pae_category: category IN ('runs', 'analysis', 'release', 'quality', 'configuration', 'membership', 'test_management', 'integration', 'agent', 'system')
    • ForeignKeyConstraint unnamed: actor_id
    • ForeignKeyConstraint unnamed: project_id
    • ForeignKeyConstraint unnamed: release_id
    • PrimaryKeyConstraint unnamed: id
    • Index ix_pae_actor_time (unique=False): project_activity_events.actor_id, occurred_at DESC, id DESC; options {"postgresql_where": "actor_type = 'user'"}
    • Index ix_pae_group_key (unique=False): project_activity_events.project_id, project_activity_events.group_key; options {}
    • Index ix_pae_project_actor (unique=False): project_activity_events.project_id, project_activity_events.actor_id, project_activity_events.occurred_at; options {}
    • Index ix_pae_project_category_time (unique=False): project_activity_events.project_id, project_activity_events.category, project_activity_events.occurred_at, project_activity_events.id; options {}
    • Index ix_pae_project_entity (unique=False): project_activity_events.project_id, project_activity_events.entity_type, project_activity_events.entity_id, project_activity_events.occurred_at; options {}
    • Index ix_pae_project_event_type (unique=False): project_activity_events.project_id, project_activity_events.event_type, project_activity_events.occurred_at; options {}
    • Index ix_pae_project_release (unique=False): project_activity_events.project_id, project_activity_events.release_id, project_activity_events.occurred_at; options {"postgresql_where": "release_id IS NOT NULL"}
    • Index ix_pae_project_time (unique=False): project_activity_events.project_id, project_activity_events.occurred_at, project_activity_events.id; options {}

    backend/app/models/postgres.py:7911

    One project export or import (migration 0247).

    A new table rather than a deletion_jobs kind: an import has no project until its transaction commits (project_id NULL until then) and must survive the project it created being archived (SET NULL, never CASCADE). kind / status are the project_export_vocab vocabularies (CHECKed). storage_key (the export archive, or the import’s upload) is never returned and never logged. Migration 0252 added options / upload / cleanup, the uploading status (a parted import upload) and its CHECK.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    kindVARCHAR(10)FalseFalse—
    statusVARCHAR(12)FalseFalseserver='queued'; application=queued
    project_idUUIDTrueFalse—projects.id / SET NULL
    project_nameVARCHAR(255)TrueFalse—
    requested_by_idUUIDTrueFalse—users.id / SET NULL
    client_tokenUUIDTrueFalse—
    storage_keyTEXTTrueFalse—
    bytesBIGINTTrueFalse—
    file_countINTEGERTrueFalse—
    manifest_sha256VARCHAR(64)TrueFalse—
    archive_sha256VARCHAR(64)TrueFalse—
    signatureVARCHAR(16)TrueFalse—
    signing_key_idVARCHAR(64)TrueFalse—
    countsJSONBFalseFalseserver='{}'::jsonb; application=dict
    reportJSONBTrueFalse—
    progressJSONBTrueFalse—
    error_codeVARCHAR(40)TrueFalse—
    errorTEXTTrueFalse—
    attemptsINTEGERFalseFalseserver=0; application=0
    requested_atDATETIMEFalseFalseserver=now()
    started_atDATETIMETrueFalse—
    finished_atDATETIMETrueFalse—
    expires_atDATETIMETrueFalse—
    optionsJSONBFalseFalseserver='{}'; application=dict
    uploadJSONBTrueFalse—
    cleanupJSONBTrueFalse—

    Constraints and indexes:

    • CheckConstraint ck_project_data_jobs_kind: kind IN ('export', 'import')
    • CheckConstraint ck_project_data_jobs_status: status IN ('uploading', 'queued', 'running', 'succeeded', 'failed', 'expired')
    • CheckConstraint ck_project_data_jobs_uploading_is_import: status <> 'uploading' OR kind = 'import'
    • ForeignKeyConstraint unnamed: project_id
    • ForeignKeyConstraint unnamed: requested_by_id
    • PrimaryKeyConstraint unnamed: id
    • Index ix_pdj_project_requested (unique=False): project_data_jobs.project_id, requested_at DESC; options {}
    • Index ix_pdj_requester_kind (unique=False): project_data_jobs.requested_by_id, project_data_jobs.kind, requested_at DESC; options {}
    • Index ix_pdj_status_requested (unique=False): project_data_jobs.status, project_data_jobs.requested_at; options {}
    • Index uq_pdj_import_token (unique=True): project_data_jobs.requested_by_id, project_data_jobs.client_token; options {"postgresql_where": "kind = 'import' AND status <> 'failed'"}
    • Index uq_pdj_one_live_export (unique=True): project_data_jobs.project_id; options {"postgresql_where": "kind = 'export' AND status IN ('queued', 'running')"}

    backend/app/models/postgres.py:1901

    One episode of one detector firing on one subject (migration 0243).

    Opened the first time an evaluation fires; CONFIRMED by every later evaluation that still fires (last_seen_at / severity / evidence refreshed, detection_count + 1); RESOLVED once — cleared (a measured evaluation judged the subject quiet) or stale (not confirmed for insight_vocab.STALE_AFTER_HOURS). A resolved row is never reopened: the next firing of the subject is a new row.

    kind / severity / resolved_reason are plain strings over insight_vocab (the CHECKs list the same literals, kept in step by the vocabulary parity test). The upsert identity is the partial unique index over the OPEN rows; both partial indexes carry a sqlite_where twin, and every CHECK uses only IN / = / boolean logic, so a SQLite create_all still builds the table. No FK targets test_runs: the evidence carries run ids as values.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    project_idUUIDFalseFalse—projects.id / CASCADE
    kindVARCHAR(20)FalseFalse—
    subject_keyVARCHAR(600)FalseFalse—
    subject_labelVARCHAR(1000)FalseFalse—
    window_keyVARCHAR(20)FalseFalse—
    severityVARCHAR(10)FalseFalse—
    evidenceJSONBFalseFalse—
    first_seen_atDATETIMEFalseFalseserver=now()
    last_seen_atDATETIMEFalseFalse—
    detection_countINTEGERFalseFalseserver=1; application=1
    resolved_atDATETIMETrueFalse—
    resolved_reasonVARCHAR(10)TrueFalse—
    dismissed_atDATETIMETrueFalse—
    dismissed_by_idUUIDTrueFalse—users.id / SET NULL
    dismissed_by_nameVARCHAR(255)TrueFalse—

    Constraints and indexes:

    • CheckConstraint ck_project_insights_kind: kind IN ('perf_drift', 'flake_rising', 'coverage_drop', 'cost_budget')
    • CheckConstraint ck_project_insights_resolved_pair: (resolved_at IS NULL) = (resolved_reason IS NULL)
    • CheckConstraint ck_project_insights_resolved_reason: resolved_reason IS NULL OR resolved_reason IN ('cleared', 'stale')
    • CheckConstraint ck_project_insights_severity: severity IN ('low', 'medium', 'high')
    • ForeignKeyConstraint unnamed: dismissed_by_id
    • ForeignKeyConstraint unnamed: project_id
    • PrimaryKeyConstraint unnamed: id
    • Index ix_project_insights_project_last_seen (unique=False): project_insights.project_id, project_insights.last_seen_at; options {}
    • Index uq_project_insights_open (unique=True): project_insights.project_id, project_insights.kind, project_insights.subject_key, project_insights.window_key; options {"postgresql_where": "resolved_at IS NULL", "sqlite_where": "resolved_at IS NULL"}

    backend/app/models/postgres.py:6697

    Per-project LLM spend config used by the usage-based billing gate.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    project_idUUIDFalseFalse—projects.id / CASCADE
    enabledBOOLEANFalseFalseapplication=True
    period_typeVARCHAR(20)FalseFalseapplication=MONTHLY
    included_usdFLOATFalseFalseapplication=0.0
    overage_rate_usdFLOATFalseFalseapplication=1.0
    hard_cap_usdFLOATFalseFalseapplication=0.0
    soft_warn_threshold_pctINTEGERFalseFalseapplication=100
    at_cap_actionVARCHAR(32)FalseFalseapplication=AUTO_DOWNGRADE_TO_ML
    created_atDATETIMEFalseFalseserver=now()
    updated_atDATETIMEFalseFalseserver=now(); onupdate=now()
    updated_by_user_idUUIDTrueFalse—users.id / SET NULL

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: project_id
    • ForeignKeyConstraint unnamed: updated_by_user_id
    • PrimaryKeyConstraint unnamed: id
    • UniqueConstraint unnamed: project_id

    backend/app/models/postgres.py:8366

    Running per-period LLM cost meter for a project.

    One row per (project_id, period_start). record_usage upserts with atomic arithmetic increments so two workers writing concurrently don’t clobber each other. The (project_id, period_start) unique index is what makes the upsert safe.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    project_idUUIDFalseFalse—projects.id / CASCADE
    period_startDATETIMEFalseFalse—
    period_endDATETIMEFalseFalse—
    total_cost_usdFLOATFalseFalseapplication=0.0
    total_input_tokensINTEGERFalseFalseapplication=0
    total_output_tokensINTEGERFalseFalseapplication=0
    total_llm_callsINTEGERFalseFalseapplication=0
    cap_hitsINTEGERFalseFalseapplication=0
    last_updated_atDATETIMEFalseFalseserver=now(); onupdate=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: project_id
    • PrimaryKeyConstraint unnamed: id
    • UniqueConstraint uq_project_period: project_id, period_start
    • Index ix_llm_usage_period (unique=False): project_llm_usage.period_start; options {}

    backend/app/models/postgres.py:4511

    Per-project role assignment for a user.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    user_idUUIDFalseFalse—users.id / CASCADE
    project_idUUIDFalseFalse—projects.id / CASCADE
    roleVARCHAR(20)FalseFalseapplication=QA_ENGINEER
    created_atDATETIMEFalseFalseserver=now()
    sourceVARCHAR(80)TrueFalse—

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: project_id
    • ForeignKeyConstraint unnamed: user_id
    • PrimaryKeyConstraint unnamed: id
    • UniqueConstraint uq_project_member: user_id, project_id

    backend/app/models/postgres.py:8214

    Per-project data retention policy (migration 0113).

    One row per project; a missing row — and, since migration 0242, every NULL column of the seven inheritable fields (enabled, the four day windows, keep_last_runs, purge_window_start_utc) — inherits, PER FIELD: the instance policy (InstanceRetentionPolicy) when it sets the field, else the code defaults in retention_service.EffectiveRetentionPolicy (disabled; raw events 90 d, runs 365 d, artifacts 180 d, audit 2555 d ≈ 7 y; keep 10; window 02:00 UTC) — no project-creation hook needed (ValueMetricAssumptions pattern). Explicit project values always win.

    Four retention classes, each with its own clock:

    • raw_events_days — live event docs + TestRun.event_archive + raw ingest payloads in Mongo.
    • runs_days — TestRun rows (Postgres CASCADE) + run-scoped Mongo docs + the run’s MinIO uploads.
    • artifacts_days — MinIO report/upload/pipeline artifacts.
    • audit_days — access/test-case audit rows, pipeline event log, expired compliance packs. Must be ≥ runs_days (enforced in the service) so audit records always outlive the runs they describe. settings_audit_log (which holds the purge-audit records themselves) is NEVER purged.

    The six policy columns (enabled, the four windows, keep_last_runs) carry NO default and NO server_default since migration 0242, because NULL means “inherit” — DDL and ORM dropped them together so they cannot drift (the #433 lesson). The resolver is the ONLY reader of a NULL.

    Schedule (migration 0242): purge_window_start_utc (0-23, NULL = inherit) is the UTC hour the hourly beat purges the project in; purge_paused_until skips the scheduled purge until then (project-only, never inherited).

    AI history caps (migration 0206): ai_versions_per_run (per kind) and ai_versions_per_test; NULL = the code defaults 20 / 10 (ai_history_core.DEFAULT_CAP_PER_RUN / DEFAULT_CAP_PER_TEST). They apply at write time whatever enabled says; enabled only gates the scheduled sweep.

    Keep-last floor (migration 0240): keep_last_runs (0-1000, NULL = inherit, code default 10) — the scheduled purge always keeps the newest N runs of the project, plus its newest PASSED run and the newest PASSED run of each suite; 0 keeps only the PASSED runs. The runs clock only.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    project_idUUIDFalseFalse—projects.id / CASCADE
    enabledBOOLEANTrueFalse—
    raw_events_daysINTEGERTrueFalse—
    runs_daysINTEGERTrueFalse—
    artifacts_daysINTEGERTrueFalse—
    audit_daysINTEGERTrueFalse—
    ai_versions_per_runINTEGERTrueFalse—
    ai_versions_per_testINTEGERTrueFalse—
    keep_last_runsINTEGERTrueFalse—
    purge_window_start_utcSMALLINTTrueFalse—
    purge_paused_untilDATETIMETrueFalse—
    created_atDATETIMEFalseFalseserver=now()
    updated_atDATETIMEFalseFalseserver=now(); onupdate=now()
    updated_by_user_idUUIDTrueFalse—users.id / SET NULL

    Constraints and indexes:

    • CheckConstraint ck_retention_ai_versions_per_run: ai_versions_per_run IS NULL OR ai_versions_per_run BETWEEN 1 AND 100
    • CheckConstraint ck_retention_ai_versions_per_test: ai_versions_per_test IS NULL OR ai_versions_per_test BETWEEN 1 AND 100
    • CheckConstraint ck_retention_keep_last_runs: keep_last_runs >= 0 AND keep_last_runs <= 1000
    • CheckConstraint ck_retention_purge_window: purge_window_start_utc IS NULL OR purge_window_start_utc BETWEEN 0 AND 23
    • ForeignKeyConstraint unnamed: project_id
    • ForeignKeyConstraint unnamed: updated_by_user_id
    • PrimaryKeyConstraint unnamed: id
    • UniqueConstraint unnamed: project_id

    backend/app/models/postgres.py:6494

    A project’s default workflow for one base (migration 0230).

    A trigger that names no workflow resolves this row at task start (services/workflow_default_service.resolve_run_workflow); no row means the built-in runs. workflow_version NULL = the latest PUBLISHED version at each run’s start. No foreign key to workflow_definitions and no relationship attributes: a default that no longer resolves is recorded as default_unavailable on the pipeline and the built-in runs instead.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    project_idUUIDFalseTrue—projects.id / CASCADE
    baseVARCHAR(20)FalseTrue—
    workflow_idVARCHAR(80)FalseFalse—
    workflow_versionINTEGERTrueFalse—
    updated_byUUIDTrueFalse—users.id / SET NULL
    updated_atDATETIMEFalseFalseserver=now()

    Constraints and indexes:

    • CheckConstraint ck_pwd_base: base IN ('offline','deep')
    • CheckConstraint ck_pwd_version_positive: workflow_version IS NULL OR workflow_version >= 1
    • CheckConstraint ck_pwd_workflow_is_fork: workflow_id LIKE 'wf.%'
    • ForeignKeyConstraint unnamed: project_id
    • ForeignKeyConstraint unnamed: updated_by
    • PrimaryKeyConstraint pk_project_workflow_defaults: project_id, base

    backend/app/models/postgres.py:265

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    nameVARCHAR(255)FalseFalse—
    slugVARCHAR(100)FalseFalse—
    descriptionTEXTTrueFalse—
    jira_project_keyVARCHAR(50)TrueFalse—
    splunk_indexVARCHAR(255)TrueFalse—
    ocp_namespaceVARCHAR(255)TrueFalse—
    jenkins_job_patternVARCHAR(500)TrueFalse—
    component_owner_mapJSONTrueFalse—
    source_rootsJSONTrueFalse—
    start_dateDATETIMETrueFalse—
    end_dateDATETIMETrueFalse—
    tagsJSONTrueFalse—
    is_activeBOOLEANFalseFalseapplication=True
    archived_atDATETIMETrueFalse—
    archived_by_user_idUUIDTrueFalse—users.id / SET NULL
    manager_user_idUUIDTrueFalse—users.id / SET NULL
    default_qa_lead_user_idUUIDTrueFalse—users.id / SET NULL
    allow_unreviewed_distributionBOOLEANFalseFalseserver=false; application=False
    require_case_reviewBOOLEANFalseFalseserver=false; application=False
    automation_target_pctSMALLINTFalseFalseserver=60; application=60
    cluster_epic_suggestionsBOOLEANFalseFalseserver=false; application=False
    created_atDATETIMEFalseFalseserver=now()
    updated_atDATETIMEFalseFalseserver=now(); onupdate=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: default_qa_lead_user_id
    • ForeignKeyConstraint unnamed: manager_user_id
    • ForeignKeyConstraint fk_projects_archived_by: archived_by_user_id
    • PrimaryKeyConstraint unnamed: id
    • Index ix_projects_default_qa_lead_user_id (unique=False): projects.default_qa_lead_user_id; options {}
    • Index ix_projects_manager_user_id (unique=False): projects.manager_user_id; options {}
    • Index ix_projects_name (unique=False): projects.name; options {}
    • Index ix_projects_slug (unique=True): projects.slug; options {}

    ORM navigation and cascade declarations:

    test_runs: Mapped[list['TestRun']] = relationship('TestRun', back_populates='project', lazy='dynamic')
    test_suites: Mapped[list['TestSuite']] = relationship('TestSuite', back_populates='project', cascade='all, delete-orphan')
    canonical_test_cases: Mapped[list['CanonicalTestCase']] = relationship('CanonicalTestCase', back_populates='project', cascade='all, delete-orphan')

    backend/app/models/postgres.py:7663

    Per-project quarantine lifecycle configuration.

    One row per project; a missing row resolves to the defaults in flaky_quarantine_service.EffectiveLifecyclePolicy (SLA 14 days, no auto-defect, no auto-promote, promote after 20 consecutive passes, detection floor 20% flip rate over 10 runs) — no project-creation hook needed.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    project_idUUIDFalseFalse—projects.id / CASCADE
    sla_daysINTEGERFalseFalseapplication=14
    auto_create_defectBOOLEANFalseFalseapplication=False
    auto_promoteBOOLEANFalseFalseapplication=False
    promote_after_passesINTEGERFalseFalseapplication=20
    max_active_quarantinedINTEGERFalseFalseserver=8; application=8
    detection_flip_rate_thresholdFLOATFalseFalseapplication=0.2
    detection_min_runsINTEGERFalseFalseapplication=10
    created_atDATETIMEFalseFalseserver=now()
    updated_atDATETIMEFalseFalseserver=now(); onupdate=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: project_id
    • PrimaryKeyConstraint unnamed: id
    • UniqueConstraint unnamed: project_id

    backend/app/models/postgres.py:5252

    Server-side record of issued refresh tokens for rotation and replay detection.

    Each refresh token carries a random jti claim; only the SHA-256 hex digest is persisted. On refresh, the record is marked rotated_to_id and a new record is created. Presenting an already-rotated or revoked token triggers family-wide revocation for the owning user (replay_detected = true).

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    user_idUUIDFalseFalse—users.id / CASCADE
    jti_hashVARCHAR(64)FalseFalse—
    issued_atDATETIMEFalseFalseserver=now()
    expires_atDATETIMEFalseFalse—
    revoked_atDATETIMETrueFalse—
    rotated_to_idUUIDTrueFalse—
    replay_detectedBOOLEANFalseFalseapplication=False

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: user_id
    • PrimaryKeyConstraint unnamed: id
    • Index ix_rtr_expires_at (unique=False): refresh_token_records.expires_at; options {}
    • Index ix_rtr_jti_hash (unique=True): refresh_token_records.jti_hash; options {}
    • Index ix_rtr_user_id (unique=False): refresh_token_records.user_id; options {}

    backend/app/models/postgres.py:4290

    Per-project rule mapping run metadata to a release (migration 0154).

    The bridge for teams that cannot send an explicit release_name: match on what a run already carries and name the release it belongs to. Without these, such projects fall straight to the active release, which cannot tell a hotfix branch from a release candidate from trunk CI.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    project_idUUIDFalseFalse—projects.id / CASCADE
    nameVARCHAR(255)FalseFalse—
    priorityINTEGERFalseFalseserver=100; application=100
    is_enabledBOOLEANFalseFalseserver=true; application=True
    match_fieldVARCHAR(30)FalseFalse—
    match_patternVARCHAR(255)FalseFalse—
    target_release_nameVARCHAR(255)FalseFalse—
    created_by_idUUIDTrueFalse—users.id / SET NULL
    created_atDATETIMEFalseFalseserver=now()
    updated_atDATETIMEFalseFalseserver=now(); onupdate=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: created_by_id
    • ForeignKeyConstraint unnamed: project_id
    • PrimaryKeyConstraint unnamed: id
    • Index ix_attribution_rules_project_priority (unique=False): release_attribution_rules.project_id, release_attribution_rules.priority; options {}

    backend/app/models/postgres.py:2997

    Release gate decision produced by ReleaseRiskAgent.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    test_run_idUUIDFalseFalse—test_runs.id / CASCADE
    pipeline_run_idUUIDTrueFalse—agent_pipeline_runs.id / SET NULL
    recommendationVARCHAR(20)FalseFalse—
    risk_scoreINTEGERFalseFalseapplication=50
    blocking_issuesJSONTrueFalse—
    conditions_for_goJSONTrueFalse—
    reasoningTEXTTrueFalse—
    dimension_scoresJSONTrueFalse—
    composite_riskFLOATTrueFalse—
    score_model_versionINTEGERTrueFalse—
    human_overrideTEXTTrueFalse—
    overridden_byUUIDTrueFalse—users.id / SET NULL
    input_snapshotJSONTrueFalse—
    override_auditJSONTrueFalse—
    original_recommendationVARCHAR(20)TrueFalse—
    original_risk_scoreINTEGERTrueFalse—
    policy_idUUIDTrueFalse—release_gate_policies.id / SET NULL
    policy_evaluationJSONTrueFalse—
    created_atDATETIMEFalseFalseserver=now()
    updated_atDATETIMEFalseFalseserver=now(); onupdate=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: overridden_by
    • ForeignKeyConstraint unnamed: pipeline_run_id
    • ForeignKeyConstraint unnamed: policy_id
    • ForeignKeyConstraint unnamed: test_run_id
    • PrimaryKeyConstraint unnamed: id
    • UniqueConstraint unnamed: test_run_id
    • Index ix_release_decisions_pipeline_run_id (unique=False): release_decisions.pipeline_run_id; options {}

    backend/app/models/postgres.py:5434

    A go/no-go verdict for a RELEASE, not for a run (migration 0156).

    ReleaseDecision — the table this sits beside, not replaces — is keyed test_run_id UNIQUE: exactly one verdict per run, answering “is this run shippable?”. That is a real question, and it is not the one a release manager asks. “Is 2.4.0 shippable?” is a judgement over the whole set of runs attributed to the release, and it has no run to hang off.

    A verdict is evidence about a moment. Re-evaluating UPDATEs nothing: it inserts a new row and demotes the previous one, so the history of what was decided, on what evidence, under which policy, survives. A gate that rewrites its own past cannot be audited, and “why did we ship that?” is exactly the question this table exists to answer months later.

    One CURRENT release-level verdict per release, and one CURRENT verdict per phase — with history rows carrying is_current = false and constrained by neither. Partial rather than plain, for the same reason is_active is on Release: an unfiltered unique index would collapse the history this table is built to keep.

    The phase column and its index exist so that phase-level rows do not need a second migration against a live table.

    denominator, run_ids, policy_snapshot and status_rollup are stored, not recomputed on read. A policy edited next month must not silently restate last month’s verdict, and a run deleted by retention must not change what the gate saw. The FK to the policy is kept to show where it came from and is SET NULL — the snapshot is the authority, the pointer is the reference.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    release_idUUIDFalseFalse—releases.id / CASCADE
    phase_idUUIDTrueFalse—release_phases.id / CASCADE
    is_currentBOOLEANFalseFalseserver=false; application=False
    verdictVARCHAR(20)FalseFalse—
    denominatorINTEGERFalseFalseapplication=0
    evidence_countINTEGERFalseFalseapplication=0
    run_idsJSONTrueFalse—
    status_rollupJSONTrueFalse—
    attribution_mixJSONTrueFalse—
    ingestion_completeBOOLEANTrueFalse—
    incomplete_runsJSONTrueFalse—
    policy_idUUIDTrueFalse—release_gate_policies.id / SET NULL
    policy_snapshotJSONTrueFalse—
    baseline_release_idUUIDTrueFalse—releases.id / SET NULL
    blocking_reasonsJSONTrueFalse—
    conditions_for_goJSONTrueFalse—
    created_by_idUUIDTrueFalse—users.id / SET NULL
    created_atDATETIMEFalseFalseserver=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: baseline_release_id
    • ForeignKeyConstraint unnamed: created_by_id
    • ForeignKeyConstraint unnamed: phase_id
    • ForeignKeyConstraint unnamed: policy_id
    • ForeignKeyConstraint unnamed: release_id
    • PrimaryKeyConstraint unnamed: id
    • Index ix_rgd_phase_current (unique=True): release_gate_decisions.release_id, release_gate_decisions.phase_id; options {"postgresql_where": "is_current IS TRUE AND phase_id IS NOT NULL"}
    • Index ix_rgd_release_created (unique=False): release_gate_decisions.release_id, release_gate_decisions.created_at; options {}
    • Index ix_rgd_release_current (unique=True): release_gate_decisions.release_id; options {"postgresql_where": "is_current IS TRUE AND phase_id IS NULL"}

    backend/app/models/postgres.py:5173

    Versioned release gate policy — per-project or system-wide default.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    project_idUUIDTrueFalse—projects.id / CASCADE
    versionINTEGERFalseFalse—
    nameVARCHAR(255)FalseFalse—
    descriptionTEXTTrueFalse—
    rulesJSONFalseFalse—
    is_activeBOOLEANFalseFalseapplication=False
    is_draftBOOLEANFalseFalseapplication=True
    created_byUUIDFalseFalse—users.id / SET NULL
    activated_byUUIDTrueFalse—users.id / SET NULL
    activated_atDATETIMETrueFalse—
    created_atDATETIMEFalseFalseserver=now()
    updated_atDATETIMEFalseFalseserver=now(); onupdate=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: activated_by
    • ForeignKeyConstraint unnamed: created_by
    • ForeignKeyConstraint unnamed: project_id
    • PrimaryKeyConstraint unnamed: id
    • UniqueConstraint uq_rgp_project_version: project_id, version
    • Index ix_rgp_project_active (unique=False): release_gate_policies.project_id, release_gate_policies.is_active; options {}
    • Index uq_rgp_one_active_project (unique=True): release_gate_policies.project_id; options {"postgresql_where": "is_active IS TRUE AND project_id IS NOT NULL"}
    • Index uq_rgp_one_active_system (unique=True): release_gate_policies.is_active; options {"postgresql_where": "is_active IS TRUE AND project_id IS NULL"}
    • Index uq_rgp_system_version (unique=True): release_gate_policies.version; options {"postgresql_where": "project_id IS NULL"}

    backend/app/models/postgres.py:4240

    A human-marked production incident or rollback for a release (E9.9).

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    release_idUUIDFalseFalse—releases.id / CASCADE
    project_idUUIDFalseFalse—projects.id / CASCADE
    outcome_kindVARCHAR(20)FalseFalse—
    reasonTEXTFalseFalse—
    marked_by_user_idUUIDTrueFalse—users.id / SET NULL
    marked_atDATETIMEFalseFalseserver=now()

    Constraints and indexes:

    • CheckConstraint ck_release_outcomes_kind: outcome_kind IN ('incident', 'rollback')
    • CheckConstraint ck_release_outcomes_reason: length(trim(reason)) >= 3
    • ForeignKeyConstraint unnamed: marked_by_user_id
    • ForeignKeyConstraint unnamed: project_id
    • ForeignKeyConstraint unnamed: release_id
    • PrimaryKeyConstraint unnamed: id
    • Index ix_release_outcomes_project_marked (unique=False): release_outcomes.project_id, release_outcomes.marked_at; options {}
    • Index ix_release_outcomes_release_marked (unique=False): release_outcomes.release_id, release_outcomes.marked_at; options {}

    backend/app/models/postgres.py:4336

    A phase / milestone within a Release lifecycle.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    release_idUUIDFalseFalse—releases.id / CASCADE
    nameVARCHAR(255)FalseFalse—
    phase_typeVARCHAR(50)FalseFalseapplication=qa_testing
    statusVARCHAR(30)FalseFalseapplication=pending
    descriptionTEXTTrueFalse—
    order_indexINTEGERFalseFalseapplication=0
    planned_startDATETIMETrueFalse—
    planned_endDATETIMETrueFalse—
    actual_startDATETIMETrueFalse—
    actual_endDATETIMETrueFalse—
    exit_criteriaJSONTrueFalse—
    notesTEXTTrueFalse—
    created_atDATETIMEFalseFalseserver=now()
    updated_atDATETIMEFalseFalseserver=now(); onupdate=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: release_id
    • PrimaryKeyConstraint unnamed: id
    • Index ix_release_phases_release (unique=False): release_phases.release_id; options {}

    ORM navigation and cascade declarations:

    release: Mapped['Release'] = relationship('Release', back_populates='phases')

    backend/app/models/postgres.py:4371

    Links a test run to a release for metrics aggregation.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    release_idUUIDFalseFalse—releases.id / CASCADE
    test_run_idUUIDFalseFalse—test_runs.id / CASCADE
    phase_idUUIDTrueFalse—release_phases.id / SET NULL
    link_sourceVARCHAR(30)TrueFalse—
    is_primaryBOOLEANFalseFalseserver=false; application=False
    linked_by_idUUIDTrueFalse—users.id / SET NULL
    project_idUUIDTrueFalse—
    linked_atDATETIMEFalseFalseserver=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: linked_by_id
    • ForeignKeyConstraint unnamed: phase_id
    • ForeignKeyConstraint unnamed: release_id
    • ForeignKeyConstraint unnamed: test_run_id
    • PrimaryKeyConstraint unnamed: id
    • UniqueConstraint uq_release_test_run: release_id, test_run_id
    • Index ix_rtr_links_primary (unique=True): release_test_run_links.test_run_id; options {"postgresql_where": "is_primary IS TRUE"}
    • Index ix_rtr_links_release (unique=False): release_test_run_links.release_id; options {}
    • Index ix_rtr_links_test_run (unique=False): release_test_run_links.test_run_id; options {}

    ORM navigation and cascade declarations:

    release: Mapped['Release'] = relationship('Release', back_populates='test_run_links')

    backend/app/models/postgres.py:4041

    A software release tracked through the QA lifecycle.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    project_idUUIDFalseFalse—projects.id / CASCADE
    nameVARCHAR(255)FalseFalse—
    versionVARCHAR(100)TrueFalse—
    descriptionTEXTTrueFalse—
    statusVARCHAR(30)FalseFalseapplication=planning
    is_defaultBOOLEANFalseFalseserver=false; application=False
    is_activeBOOLEANFalseFalseserver=false; application=False
    is_auto_namedBOOLEANFalseFalseserver=false; application=False
    activated_atDATETIMETrueFalse—
    deactivated_atDATETIMETrueFalse—
    release_typeVARCHAR(20)TrueFalse—
    sort_keyVARCHAR(64)TrueFalse—
    baseline_release_idUUIDTrueFalse—releases.id / SET NULL
    cutoff_start_atDATETIMETrueFalse—
    cutoff_end_atDATETIMETrueFalse—
    target_environmentVARCHAR(100)TrueFalse—
    source_systemVARCHAR(20)TrueFalse—
    external_idVARCHAR(255)TrueFalse—
    external_urlVARCHAR(1000)TrueFalse—
    last_synced_atDATETIMETrueFalse—
    planned_dateDATETIMETrueFalse—
    released_atDATETIMETrueFalse—
    created_by_idUUIDTrueFalse—users.id / SET NULL
    owner_user_idUUIDTrueFalse—users.id / SET NULL
    created_atDATETIMEFalseFalseserver=now()
    updated_atDATETIMEFalseFalseserver=now(); onupdate=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: baseline_release_id
    • ForeignKeyConstraint unnamed: created_by_id
    • ForeignKeyConstraint unnamed: owner_user_id
    • ForeignKeyConstraint unnamed: project_id
    • PrimaryKeyConstraint unnamed: id
    • Index ix_releases_external_identity (unique=True): releases.project_id, releases.source_system, releases.external_id; options {"postgresql_where": "external_id IS NOT NULL"}
    • Index ix_releases_owner (unique=False): releases.owner_user_id; options {"postgresql_where": "owner_user_id IS NOT NULL"}
    • Index ix_releases_project_active (unique=True): releases.project_id; options {"postgresql_where": "is_active IS TRUE"}
    • Index ix_releases_project_default (unique=True): releases.project_id; options {"postgresql_where": "is_default IS TRUE"}
    • Index ix_releases_project_sort (unique=False): releases.project_id, releases.sort_key; options {}
    • Index ix_releases_project_status (unique=False): releases.project_id, releases.status; options {}

    ORM navigation and cascade declarations:

    phases: Mapped[list['ReleasePhase']] = relationship('ReleasePhase', back_populates='release', cascade='all, delete-orphan', order_by='ReleasePhase.order_index')
    test_run_links: Mapped[list['ReleaseTestRunLink']] = relationship('ReleaseTestRunLink', back_populates='release', cascade='all, delete-orphan')

    backend/app/models/postgres.py:8163

    One background export of a report (migration 0194).

    Rendered by generate_report_export on the default queue; the file lives in object storage at storage_key until expires_at (requested

    • 7 days), when the nightly sweep deletes it and this row. The life cycle and its guarantees (claim, fencing by attempts, failures written on their own session, retry) are in services/report_export_service.
    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    project_idUUIDFalseFalse—projects.id / CASCADE
    requested_by_idUUIDTrueFalse—users.id / SET NULL
    reportVARCHAR(40)FalseFalse—
    formatVARCHAR(10)FalseFalse—
    paramsJSONBFalseFalse—
    statusVARCHAR(20)FalseFalseapplication=queued
    attemptsINTEGERFalseFalseapplication=0
    storage_keyTEXTTrueFalse—
    filenameVARCHAR(255)TrueFalse—
    size_bytesBIGINTTrueFalse—
    errorTEXTTrueFalse—
    requested_atDATETIMEFalseFalseserver=now()
    started_atDATETIMETrueFalse—
    finished_atDATETIMETrueFalse—
    expires_atDATETIMEFalseFalse—

    Constraints and indexes:

    • CheckConstraint ck_report_exports_format: format IN ('pdf', 'xlsx')
    • CheckConstraint ck_report_exports_status: status IN ('queued', 'running', 'completed', 'failed')
    • ForeignKeyConstraint unnamed: project_id
    • ForeignKeyConstraint unnamed: requested_by_id
    • PrimaryKeyConstraint unnamed: id
    • Index ix_report_exports_expires_at (unique=False): report_exports.expires_at; options {}
    • Index ix_report_exports_project_user_requested (unique=False): report_exports.project_id, report_exports.requested_by_id, report_exports.requested_at; options {}

    backend/app/models/postgres.py:5224

    Time-limited share token for run intelligence reports.

    The raw token is shown once at creation and never persisted. Only the SHA-256 hex digest (token_hash) is stored, so a DB compromise cannot recover active share tokens.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    run_idUUIDFalseFalse—test_runs.id / CASCADE
    project_idUUIDFalseFalse—projects.id / CASCADE
    token_hashVARCHAR(64)FalseFalse—
    report_layoutVARCHAR(20)FalseFalseapplication=executive
    created_by_idUUIDTrueFalse—users.id / SET NULL
    created_by_nameVARCHAR(200)TrueFalse—
    expires_atDATETIMEFalseFalse—
    is_revokedBOOLEANFalseFalseapplication=False
    access_countINTEGERFalseFalseapplication=0
    last_accessed_atDATETIMETrueFalse—
    created_atDATETIMEFalseFalseserver=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: created_by_id
    • ForeignKeyConstraint unnamed: project_id
    • ForeignKeyConstraint unnamed: run_id
    • PrimaryKeyConstraint unnamed: id
    • UniqueConstraint unnamed: token_hash
    • Index ix_rsl_expires (unique=False): report_share_links.expires_at; options {}
    • Index ix_rsl_run (unique=False): report_share_links.run_id; options {}
    • Index ix_rsl_token_hash (unique=True): report_share_links.token_hash; options {}

    backend/app/models/postgres.py:3744

    Tracks which requirements are covered/uncovered by a generation batch.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    batch_idUUIDFalseFalse—generation_batches.id / CASCADE
    project_idUUIDFalseFalse—projects.id / CASCADE
    requirement_idVARCHAR(200)FalseFalse—
    requirement_textTEXTTrueFalse—
    coverage_statusVARCHAR(20)FalseFalseapplication=uncovered
    covered_by_case_idsJSONTrueFalse—
    created_atDATETIMEFalseFalseserver=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: batch_id
    • ForeignKeyConstraint unnamed: project_id
    • PrimaryKeyConstraint unnamed: id
    • UniqueConstraint uq_rc_batch_req: batch_id, requirement_id
    • Index ix_rc_batch_id (unique=False): requirement_coverage.batch_id; options {}
    • Index ix_rc_project_req (unique=False): requirement_coverage.project_id, requirement_coverage.requirement_id; options {}

    backend/app/models/postgres.py:8077

    A retention hold on a project, a release, a run or a compliance pack (migration 0239).

    While a hold is LIVE (released_at IS NULL AND (expires_at IS NULL OR expires_at > now)) nothing it covers is purged or deleted: every purge and deletion path excludes it with a NOT EXISTS anti-join (services/retention_hold_service.py). A project hold has entity_id IS NULL and covers the whole project; a release hold also holds the release’s runs, packs and their decision reports; a run hold (and every run a release hold covers) also holds the run it is compared against, one hop.

    entity_id has no foreign key, on purpose: the entity can be one of several types (entity_type says which table), and it cannot be deleted while the hold lives. entity_label and the two *_by_name columns are snapshots taken when the row is written. A hold is never deleted — releasing it stamps released_at with a required release_reason. The state (live / expired / released, retention_vocab.HOLD_STATES) is derived on read and never stored.

    Every CHECK uses only length / trim / IN / boolean logic, so a SQLite create_all still builds the table; postgresql_where is ignored there.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    project_idUUIDFalseFalse—projects.id / CASCADE
    entity_typeVARCHAR(20)FalseFalse—
    entity_idUUIDTrueFalse—
    entity_labelVARCHAR(300)TrueFalse—
    reasonTEXTFalseFalse—
    expires_atDATETIMETrueFalse—
    placed_by_idUUIDTrueFalse—users.id / SET NULL
    placed_by_nameVARCHAR(200)TrueFalse—
    placed_atDATETIMEFalseFalseserver=now()
    released_atDATETIMETrueFalse—
    released_by_idUUIDTrueFalse—users.id / SET NULL
    released_by_nameVARCHAR(200)TrueFalse—
    release_reasonTEXTTrueFalse—

    Constraints and indexes:

    • CheckConstraint ck_retention_holds_entity_id: (entity_type = 'project' AND entity_id IS NULL) OR (entity_type <> 'project' AND entity_id IS NOT NULL)
    • CheckConstraint ck_retention_holds_entity_type: entity_type IN ('project', 'release', 'run', 'compliance_pack')
    • CheckConstraint ck_retention_holds_reason: length(trim(reason)) >= 3
    • CheckConstraint ck_retention_holds_release_reason: released_at IS NULL OR release_reason IS NOT NULL
    • ForeignKeyConstraint unnamed: placed_by_id
    • ForeignKeyConstraint unnamed: project_id
    • ForeignKeyConstraint unnamed: released_by_id
    • PrimaryKeyConstraint unnamed: id
    • Index ix_retention_holds_live (unique=False): retention_holds.project_id, retention_holds.entity_type, retention_holds.entity_id; options {"postgresql_where": "released_at IS NULL"}
    • Index ix_retention_holds_project_placed (unique=False): retention_holds.project_id, retention_holds.placed_at; options {}

    backend/app/models/postgres.py:6189

    One human review for one AI report-producing run (architecture section 8.1).

    Every AI-generated report is a proposal until a human accepts it. There is one LIVE request per subject — the reports a run produced inherit its review state — enforced by a partial unique index; superseded rows are kept, so the history of who accepted what survives a re-run.

    notes is free text and is never exported (section 8.1); reason_code is the closed vocabulary that becomes an eval label.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    project_idUUIDFalseFalse—projects.id / CASCADE
    kindVARCHAR(20)FalseFalseserver=report; application=report
    subject_typeVARCHAR(30)FalseFalse—
    subject_idVARCHAR(128)FalseFalse—
    pipeline_run_idUUIDTrueFalse—agent_pipeline_runs.id / SET NULL
    test_run_idUUIDTrueFalse—test_runs.id / SET NULL
    workflow_typeVARCHAR(20)TrueFalse—
    capability_idVARCHAR(160)TrueFalse—
    stateVARCHAR(20)FalseFalseserver=pending_review; application=pending_review
    requested_byUUIDTrueFalse—users.id / SET NULL
    created_byVARCHAR(40)FalseFalseserver=system; application=system
    reviewed_byUUIDTrueFalse—users.id / SET NULL
    reviewed_atDATETIMETrueFalse—
    reason_codeVARCHAR(40)TrueFalse—
    notesTEXTTrueFalse—
    evidence_bundle_sha256VARCHAR(64)TrueFalse—
    eval_manifest_checksumVARCHAR(64)TrueFalse—
    ai_disclaimer_versionVARCHAR(40)FalseFalse—
    superseded_byUUIDTrueFalse—review_requests.id / SET NULL
    created_atDATETIMEFalseFalseserver=now()
    updated_atDATETIMEFalseFalseserver=now(); onupdate=now()

    Constraints and indexes:

    • CheckConstraint ck_review_requests_eval_manifest_checksum: eval_manifest_checksum IS NULL OR eval_manifest_checksum ~ '^[0-9a-f]{64}$'
    • CheckConstraint ck_review_requests_evidence_hash: evidence_bundle_sha256 IS NULL OR evidence_bundle_sha256 ~ '^[0-9a-f]{64}$'
    • CheckConstraint ck_review_requests_kind: kind IN ('report', 'eval_drift')
    • CheckConstraint ck_review_requests_reason_code: reason_code IS NULL OR reason_code IN ('wrong_category', 'unsupported_claim', 'missing_evidence', 'contradiction', 'stale_data', 'other')
    • CheckConstraint ck_review_requests_rejection_has_reason: state <> 'rejected' OR reason_code IS NOT NULL
    • CheckConstraint ck_review_requests_settled_has_time: state NOT IN ('accepted', 'rejected') OR reviewed_at IS NOT NULL
    • CheckConstraint ck_review_requests_state: state IN ('pending_review', 'accepted', 'rejected', 'superseded')
    • CheckConstraint ck_review_requests_subject_type: subject_type IN ('pipeline_run', 'invocation', 'decision_report', 'summary', 'capability')
    • ForeignKeyConstraint unnamed: pipeline_run_id
    • ForeignKeyConstraint unnamed: project_id
    • ForeignKeyConstraint unnamed: requested_by
    • ForeignKeyConstraint unnamed: reviewed_by
    • ForeignKeyConstraint unnamed: superseded_by
    • ForeignKeyConstraint unnamed: test_run_id
    • PrimaryKeyConstraint unnamed: id
    • Index ix_review_requests_pending_scope (unique=False): review_requests.project_id, review_requests.test_run_id, review_requests.workflow_type; options {"postgresql_where": "state = 'pending_review'"}
    • Index ix_review_requests_project_state (unique=False): review_requests.project_id, review_requests.state, review_requests.created_at; options {}
    • Index uq_review_requests_live_subject (unique=True): review_requests.kind, review_requests.subject_type, review_requests.subject_id; options {"postgresql_where": "state <> 'superseded'"}

    backend/app/models/postgres.py:4648

    Records which earlier run was chosen as the comparison for a run, and why.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    run_idUUIDFalseFalse—test_runs.id / CASCADE
    baseline_run_idUUIDTrueFalse—test_runs.id / SET NULL
    selection_reasonVARCHAR(100)FalseFalse—
    classificationVARCHAR(50)FalseFalse—
    baseline_build_numberVARCHAR(100)TrueFalse—
    pass_rate_deltaFLOATTrueFalse—
    commit_rangeJSONTrueFalse—
    config_driftJSONTrueFalse—
    computed_atDATETIMEFalseFalseserver=now()
    baseline_purged_atDATETIMETrueFalse—

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: baseline_run_id
    • ForeignKeyConstraint unnamed: run_id
    • PrimaryKeyConstraint unnamed: id
    • UniqueConstraint unnamed: run_id
    • Index ix_run_baselines_baseline (unique=False): run_baselines.baseline_run_id; options {}
    • Index ix_run_baselines_run_id (unique=True): run_baselines.run_id; options {}

    backend/app/models/postgres.py:2088

    What the cadence beat decided for one expected window (migration 0244).

    The window of deadline k is (deadline[k-1] + grace, deadline[k] + grace] (UTC). outcome is met / missed / paused (run_cadence_vocab.WINDOW_OUTCOMES); unique on (cadence_id, window_start), so a redelivered tick decides nothing twice and a later resume never rewrites history. project_id is denormalised for the status read.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    cadence_idUUIDFalseFalse—run_cadences.id / CASCADE
    project_idUUIDFalseFalse—projects.id / CASCADE
    window_startDATETIMEFalseFalse—
    window_endDATETIMEFalseFalse—
    deadlineDATETIMEFalseFalse—
    outcomeVARCHAR(10)FalseFalse—
    run_countINTEGERFalseFalseserver=0; application=0
    first_run_atDATETIMETrueFalse—
    checked_atDATETIMEFalseFalseserver=now()

    Constraints and indexes:

    • CheckConstraint ck_run_cadence_windows_outcome: outcome IN ('met', 'missed', 'paused')
    • CheckConstraint ck_run_cadence_windows_range: window_end > window_start
    • ForeignKeyConstraint unnamed: cadence_id
    • ForeignKeyConstraint unnamed: project_id
    • PrimaryKeyConstraint unnamed: id
    • UniqueConstraint uq_run_cadence_windows_cadence_start: cadence_id, window_start
    • Index ix_run_cadence_windows_project_deadline (unique=False): run_cadence_windows.project_id, run_cadence_windows.deadline; options {}

    backend/app/models/postgres.py:2006

    One expected run cadence: “a run of <suite | any> arrives by <time>” (migration 0244).

    TestLookup’s own EXPECTATION — nothing reads or writes CI; pausing it pauses the expectation and its alerts, nothing else. mode is weekly (weekdays bitmask, bit i = run_cadence_vocab.WEEKDAYS[i],

    • expected_by, the LOCAL wall-clock deadline) or cron (a normalised m h * * dow); the CHECKs keep the two shapes exclusive. suite_key is the lower + trimmed suite (NULL = any run of the project); the two partial unique indexes allow one “any run” cadence and one cadence per suite per project, each with a sqlite_where twin. effective_from is when the CURRENT definition took effect; checked_through is the beat’s cursor (the last decided window end).
    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    project_idUUIDFalseFalse—projects.id / CASCADE
    suite_nameVARCHAR(500)TrueFalse—
    suite_keyVARCHAR(500)TrueFalse—
    modeVARCHAR(10)FalseFalse—
    weekdaysSMALLINTTrueFalse—
    expected_byTIMETrueFalse—
    cronVARCHAR(100)TrueFalse—
    grace_minutesSMALLINTFalseFalseserver=60; application=60
    effective_fromDATETIMEFalseFalse—
    checked_throughDATETIMEFalseFalse—
    paused_atDATETIMETrueFalse—
    paused_untilDATETIMETrueFalse—
    paused_reasonVARCHAR(200)TrueFalse—
    paused_by_idUUIDTrueFalse—users.id / SET NULL
    created_by_idUUIDTrueFalse—users.id / SET NULL
    created_atDATETIMEFalseFalseserver=now()
    updated_atDATETIMEFalseFalseserver=now(); onupdate=now()
    timezoneVARCHAR(64)FalseFalse—

    Constraints and indexes:

    • CheckConstraint ck_run_cadences_grace: grace_minutes BETWEEN 0 AND 720
    • CheckConstraint ck_run_cadences_mode: mode IN ('weekly', 'cron')
    • CheckConstraint ck_run_cadences_mode_fields: (mode = 'weekly' AND weekdays BETWEEN 1 AND 127 AND expected_by IS NOT NULL AND cron IS NULL) OR (mode = 'cron' AND cron IS NOT NULL AND weekdays IS NULL AND expected_by IS NULL)
    • CheckConstraint ck_run_cadences_pause_fields: (paused_until IS NULL) = (paused_reason IS NULL) AND (paused_until IS NULL) = (paused_at IS NULL)
    • CheckConstraint ck_run_cadences_suite_pair: (suite_name IS NULL) = (suite_key IS NULL)
    • ForeignKeyConstraint unnamed: created_by_id
    • ForeignKeyConstraint unnamed: paused_by_id
    • ForeignKeyConstraint unnamed: project_id
    • PrimaryKeyConstraint unnamed: id
    • Index uq_run_cadences_project_any (unique=True): run_cadences.project_id; options {"postgresql_where": "suite_key IS NULL", "sqlite_where": "suite_key IS NULL"}
    • Index uq_run_cadences_project_suite (unique=True): run_cadences.project_id, run_cadences.suite_key; options {"postgresql_where": "suite_key IS NOT NULL", "sqlite_where": "suite_key IS NOT NULL"}

    backend/app/models/postgres.py:2913

    Commit range associated with a run (migration 0110).

    One row per run (run_id UNIQUE, so writing it again for the same run is safe). Records the commits landed since the run’s last passing comparison run. source: connector (fetched from the GitHub integration), supplied (caller pushed the list on ingest — air-gapped path, no VCS call; supplied always wins over connector), or unavailable (neither path yielded data — an empty list, shown as such). commits is a limited list of {sha, author, message, files, committed_at} ordered oldest→newest. Suspect ranking reads this table.

    base_source (migration 0116) records HOW base_commit was anchored — a strong anchor and a weak one must never look alike:

    • supplied — the caller pushed the base ref itself.
    • green_baseline — last fully-green prior run (strongest).
    • last_completed_run — most recent completed prior run, regardless of pass/fail. WEAKER: “landed since” is then only true relative to that run, not relative to a known-good state.
    • unavailable — no base could be determined.
    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    run_idUUIDFalseFalse—test_runs.id / CASCADE
    project_idUUIDFalseFalse—projects.id / CASCADE
    base_commitVARCHAR(64)TrueFalse—
    head_commitVARCHAR(64)TrueFalse—
    base_run_idUUIDTrueFalse—
    sourceVARCHAR(20)FalseFalseserver=unavailable
    base_sourceVARCHAR(30)FalseFalseserver=unavailable; application=unavailable
    commitsJSONBFalseFalseserver=[]; application=list
    resolved_atDATETIMEFalseFalseserver=now()
    created_atDATETIMEFalseFalseserver=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: project_id
    • ForeignKeyConstraint unnamed: run_id
    • PrimaryKeyConstraint unnamed: id
    • UniqueConstraint uq_run_commit_ranges_run: run_id
    • Index ix_run_commit_ranges_project_resolved (unique=False): run_commit_ranges.project_id, run_commit_ranges.resolved_at; options {}

    backend/app/models/postgres.py:4692

    Cached AI report for a run or suite comparison.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    project_idUUIDFalseFalse—projects.id / CASCADE
    left_run_idUUIDFalseFalse—test_runs.id / CASCADE
    right_run_idUUIDFalseFalse—test_runs.id / CASCADE
    suite_nameVARCHAR(500)TrueFalse—
    suite_name_normalizedVARCHAR(500)FalseFalseapplication=
    compare_payloadJSONFalseFalse—
    ai_reportJSONTrueFalse—
    statusVARCHAR(20)FalseFalseapplication=queued
    fallback_usedBOOLEANFalseFalseapplication=False
    prompt_versionVARCHAR(50)FalseFalseapplication=run_compare_v1
    model_nameVARCHAR(200)TrueFalse—
    created_by_user_idUUIDTrueFalse—users.id / SET NULL
    error_messageTEXTTrueFalse—
    created_atDATETIMEFalseFalseserver=now()
    updated_atDATETIMEFalseFalseserver=now(); onupdate=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: created_by_user_id
    • ForeignKeyConstraint unnamed: left_run_id
    • ForeignKeyConstraint unnamed: project_id
    • ForeignKeyConstraint unnamed: right_run_id
    • PrimaryKeyConstraint unnamed: id
    • UniqueConstraint uq_run_comparison_report_scope: project_id, left_run_id, right_run_id, suite_name_normalized, prompt_version
    • Index ix_run_comparison_reports_project (unique=False): run_comparison_reports.project_id, run_comparison_reports.created_at; options {}
    • Index ix_run_comparison_reports_runs (unique=False): run_comparison_reports.left_run_id, run_comparison_reports.right_run_id; options {}

    backend/app/models/postgres.py:4673

    Persisted diff payload for a run — avoids recomputation.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    run_idUUIDFalseFalse—test_runs.id / CASCADE
    baseline_run_idUUIDTrueFalse—test_runs.id / SET NULL
    diff_payloadJSONFalseFalse—
    computed_atDATETIMEFalseFalseserver=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: baseline_run_id
    • ForeignKeyConstraint unnamed: run_id
    • PrimaryKeyConstraint unnamed: id
    • UniqueConstraint unnamed: run_id
    • Index ix_run_diffs_baseline_run_id (unique=False): run_diffs.baseline_run_id; options {}
    • Index ix_run_diffs_run_id (unique=True): run_diffs.run_id; options {}

    backend/app/models/postgres.py:642

    A saved job to publish one operation that follows receiving results.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    run_idUUIDFalseFalse—test_runs.id / CASCADE
    project_idUUIDFalseFalse—projects.id / CASCADE
    operationVARCHAR(60)FalseFalse—
    input_versionVARCHAR(64)FalseFalse—
    payloadJSONBFalseFalseapplication=dict
    queueVARCHAR(80)FalseFalseapplication=default
    priorityINTEGERFalseFalseapplication=5
    statusVARCHAR(20)FalseFalseserver=pending; application=pending
    attemptsINTEGERFalseFalseserver=0; application=0
    dispatch_failuresINTEGERFalseFalseserver=0; application=0
    execution_attemptsINTEGERFalseFalseserver=0; application=0
    next_attempt_atDATETIMETrueFalse—
    lease_expires_atDATETIMETrueFalse—
    dispatch_tokenUUIDTrueFalse—
    processing_task_idVARCHAR(80)TrueFalse—
    published_atDATETIMETrueFalse—
    processing_started_atDATETIMETrueFalse—
    completed_atDATETIMETrueFalse—
    completion_detailVARCHAR(200)TrueFalse—
    last_errorVARCHAR(200)TrueFalse—
    created_atDATETIMEFalseFalseserver=now()
    updated_atDATETIMEFalseFalseserver=now(); onupdate=now()

    Constraints and indexes:

    • CheckConstraint ck_run_downstream_outbox_attempts_nonnegative: attempts >= 0
    • CheckConstraint ck_run_downstream_outbox_dispatch_failures_nonnegative: dispatch_failures >= 0
    • CheckConstraint ck_run_downstream_outbox_execution_attempts_nonnegative: execution_attempts >= 0
    • CheckConstraint ck_run_downstream_outbox_status: status IN ('waiting', 'pending', 'sending', 'published', 'processing', 'completed', 'failed')
    • ForeignKeyConstraint unnamed: project_id
    • ForeignKeyConstraint unnamed: run_id
    • PrimaryKeyConstraint unnamed: id
    • UniqueConstraint uq_run_downstream_operation_version: run_id, operation, input_version
    • Index ix_run_downstream_outbox_due_project (unique=False): run_downstream_outbox.status, run_downstream_outbox.next_attempt_at, run_downstream_outbox.project_id, run_downstream_outbox.created_at; options {}

    backend/app/models/postgres.py:4561

    Cached run intelligence payload for fast page loads.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    run_idUUIDFalseFalse—test_runs.id / CASCADE
    schema_versionINTEGERFalseFalseapplication=1
    payloadJSONFalseFalse—
    generated_atDATETIMEFalseFalseserver=now()
    fallback_usedBOOLEANFalseFalseapplication=False
    staleBOOLEANFalseFalseapplication=False
    created_atDATETIMEFalseFalseserver=now()
    updated_atDATETIMEFalseFalseserver=now(); onupdate=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: run_id
    • PrimaryKeyConstraint unnamed: id
    • UniqueConstraint unnamed: run_id
    • Index ix_ris_generated (unique=False): run_intelligence_snapshots.generated_at; options {}
    • Index ix_ris_run_id (unique=True): run_intelligence_snapshots.run_id; options {}

    backend/app/models/postgres.py:7745

    A run that was deleted on purpose and must not come back (migration 0148).

    Five code paths create a TestRun from a caller-supplied id on a SELECT miss — the stream stub, persist_live_session, the live-session drainer, the live-persist Celery task, and ingestion_pipeline when a run_id is passed. Each is correct on its own terms; together they are why a per-run delete could not previously be offered. Delete a run while any is in flight and the row reappears seconds later with its events, objects and archive already gone.

    Refusing to delete an IN_PROGRESS run narrows that window but does not close it: a Celery task already holding the id does not re-read the status.

    run_id is the primary key and, on purpose, is not a foreign key to test_runs — the row it names has been deleted, which is the whole point. Retention retires tombstones on the audit clock so this does not become a table that only grows.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    run_idUUIDFalseTrue—
    project_idUUIDFalseFalse—projects.id / CASCADE
    deleted_atDATETIMEFalseFalseserver=now()
    deleted_by_idUUIDTrueFalse—users.id / SET NULL
    reasonTEXTTrueFalse—
    deletion_job_idUUIDTrueFalse—deletion_jobs.id / SET NULL

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: deleted_by_id
    • ForeignKeyConstraint unnamed: deletion_job_id
    • ForeignKeyConstraint unnamed: project_id
    • PrimaryKeyConstraint unnamed: run_id
    • Index ix_run_tombstones_project_deleted (unique=False): run_tombstones.project_id, run_tombstones.deleted_at; options {}

    backend/app/models/postgres.py:5377

    Persisted filter/scope configuration — personal or shared.

    The filters JSON field supports both legacy filter-only payloads and analytics widget configurations: Legacy: {“severity”: “critical”, “date_range”: 7} Analytics: {“page”: “dashboard”, “widgets”: [“w1”, “w2”], “filters”: {…}, “version”: 1}

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    user_idUUIDFalseFalse—users.id / CASCADE
    project_idUUIDTrueFalse—projects.id / CASCADE
    nameVARCHAR(255)FalseFalse—
    descriptionTEXTTrueFalse—
    pageVARCHAR(50)TrueFalse—
    filtersJSONFalseFalse—
    is_sharedBOOLEANFalseFalseapplication=False
    is_defaultBOOLEANFalseFalseapplication=False
    created_atDATETIMEFalseFalseserver=now()
    updated_atDATETIMEFalseFalseserver=now(); onupdate=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: project_id
    • ForeignKeyConstraint unnamed: user_id
    • PrimaryKeyConstraint unnamed: id
    • Index ix_sv_project (unique=False): saved_views.project_id; options {}
    • Index ix_sv_user (unique=False): saved_views.user_id; options {}

    backend/app/models/postgres.py:5100

    An administrator’s grant — members of the group get project_role on the project.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    group_idUUIDFalseFalse—scim_groups.id / CASCADE
    project_idUUIDFalseFalse—projects.id / CASCADE
    project_roleVARCHAR(20)FalseFalse—

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: group_id
    • ForeignKeyConstraint unnamed: project_id
    • PrimaryKeyConstraint unnamed: id
    • UniqueConstraint uq_scim_group_mapping: group_id, project_id

    backend/app/models/postgres.py:5092

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    group_idUUIDFalseTrue—scim_groups.id / CASCADE
    user_idUUIDFalseTrue—users.id / CASCADE

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: group_id
    • ForeignKeyConstraint unnamed: user_id
    • PrimaryKeyConstraint unnamed: group_id, user_id
    • Index ix_scim_group_members_user (unique=False): scim_group_members.user_id; options {}

    backend/app/models/postgres.py:5079

    A group an IdP pushes over SCIM /Groups.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    display_nameVARCHAR(255)FalseFalse—
    external_idVARCHAR(255)TrueFalse—
    versionINTEGERFalseFalseserver=1; application=1
    created_atDATETIMEFalseFalseserver=now()
    updated_atDATETIMEFalseFalseserver=now(); onupdate=now()

    Constraints and indexes:

    • PrimaryKeyConstraint unnamed: id
    • UniqueConstraint unnamed: display_name

    backend/app/models/postgres.py:5112

    Bearer token for SCIM 2.0 provisioning endpoints.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    nameVARCHAR(255)FalseFalse—
    token_hashVARCHAR(64)FalseFalse—
    token_hintVARCHAR(12)FalseFalse—
    sso_config_idUUIDTrueFalse—sso_configurations.id / SET NULL
    created_by_idUUIDTrueFalse—users.id / SET NULL
    is_activeBOOLEANFalseFalseapplication=True
    last_used_atDATETIMETrueFalse—
    expires_atDATETIMETrueFalse—
    created_atDATETIMEFalseFalseserver=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: created_by_id
    • ForeignKeyConstraint unnamed: sso_config_id
    • PrimaryKeyConstraint unnamed: id
    • UniqueConstraint unnamed: token_hash
    • Index ix_scim_token_hash (unique=True): scim_tokens.token_hash; options {}

    backend/app/models/postgres.py:5404

    One search a person ran on the /search page (migration 0227). No query text: q_hash is an HMAC-SHA256 keyed from APP_SECRET_KEY over the normalised query (search_usage_service.query_hash), so a database reader cannot dictionary-reverse it; no user id (no per-person trail). Kept 30 days (USAGE_RETENTION_DAYS), deleted nightly by purge_search_usage_events and by a project’s retention purge.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    project_idUUIDTrueFalse—projects.id / CASCADE
    q_hashVARCHAR(64)FalseFalse—
    q_lengthSMALLINTFalseFalse—
    word_countSMALLINTFalseFalseserver=0; application=0
    filter_countSMALLINTFalseFalseserver=0; application=0
    entity_scopeVARCHAR(20)FalseFalse—
    mode_requestedVARCHAR(10)FalseFalse—
    mode_usedVARCHAR(10)FalseFalse—
    fallback_reasonVARCHAR(30)TrueFalse—
    result_countINTEGERFalseFalse—
    counts_exactBOOLEANFalseFalseserver=true; application=True
    latency_msINTEGERFalseFalse—
    created_atDATETIMEFalseFalseserver=now()

    Constraints and indexes:

    • CheckConstraint ck_search_query_events_nonnegative: q_length >= 0 AND word_count >= 0 AND filter_count >= 0 AND result_count >= 0 AND latency_ms >= 0
    • ForeignKeyConstraint unnamed: project_id
    • PrimaryKeyConstraint unnamed: id
    • Index ix_search_query_events_created (unique=False): search_query_events.created_at; options {}
    • Index ix_search_query_events_project_created (unique=False): search_query_events.project_id, created_at DESC; options {}

    backend/app/models/postgres.py:4434

    Stores sensitive values (API keys, tokens, passwords) separately from settings.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    scopeVARCHAR(100)FalseFalse—
    providerVARCHAR(50)FalseFalseapplication=db
    key_nameVARCHAR(255)FalseFalse—
    encrypted_valueTEXTTrueFalse—
    external_refVARCHAR(500)TrueFalse—
    masked_valueVARCHAR(50)TrueFalse—
    rotation_statusVARCHAR(30)FalseFalseapplication=active
    updated_byUUIDTrueFalse—users.id / SET NULL
    created_atDATETIMEFalseFalseserver=now()
    updated_atDATETIMEFalseFalseserver=now(); onupdate=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: updated_by
    • PrimaryKeyConstraint unnamed: id
    • Index ix_secret_refs_scope_key (unique=True): secret_refs.scope, secret_refs.key_name; options {}

    backend/app/models/postgres.py:736

    Saved progress for one rebuild of the search-by-meaning index, for one project or all of them. A check stops an older worker from overwriting it.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    scope_keyVARCHAR(80)FalseTrue—
    project_idUUIDTrueFalse—projects.id / CASCADE
    job_idUUIDFalseFalseapplication=uuid4
    state_versionINTEGERFalseFalseserver=1; application=1
    statusVARCHAR(20)FalseFalseserver=running; application=running
    high_water_created_atDATETIMEFalseFalse—
    high_water_idUUIDFalseFalse—
    cursor_created_atDATETIMETrueFalse—
    cursor_idUUIDTrueFalse—
    processed_countBIGINTFalseFalseserver=0; application=0
    lease_ownerUUIDTrueFalse—
    lease_expires_atDATETIMETrueFalse—
    fence_tokenBIGINTFalseFalseserver=1; application=1
    collectionVARCHAR(63)TrueFalse—
    embedder_keyVARCHAR(200)TrueFalse—
    last_errorVARCHAR(500)TrueFalse—
    started_atDATETIMEFalseFalseserver=now()
    completed_atDATETIMETrueFalse—
    updated_atDATETIMEFalseFalseserver=now(); onupdate=now()

    Constraints and indexes:

    • CheckConstraint ck_semantic_reindex_job_processed_count: processed_count >= 0
    • CheckConstraint ck_semantic_reindex_job_state_version: state_version = 1
    • CheckConstraint ck_semantic_reindex_job_status: status IN ('running', 'finalizing', 'succeeded')
    • ForeignKeyConstraint unnamed: project_id
    • PrimaryKeyConstraint unnamed: scope_key
    • UniqueConstraint uq_semantic_reindex_jobs_job_id: job_id
    • Index ix_semantic_reindex_jobs_status_lease (unique=False): semantic_reindex_jobs.status, semantic_reindex_jobs.lease_expires_at; options {}

    backend/app/models/postgres.py:5280

    Maps a matcher pattern (suite, component, package, path) to a team/service owner.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    project_idUUIDFalseFalse—projects.id / CASCADE
    match_typeVARCHAR(30)FalseFalse—
    match_patternVARCHAR(500)FalseFalse—
    service_nameVARCHAR(255)FalseFalse—
    team_nameVARCHAR(255)FalseFalse—
    team_contactVARCHAR(500)TrueFalse—
    priorityINTEGERFalseFalseapplication=0
    is_activeBOOLEANFalseFalseapplication=True
    created_byUUIDTrueFalse—users.id / SET NULL
    created_atDATETIMEFalseFalseserver=now()
    updated_atDATETIMEFalseFalseserver=now(); onupdate=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: created_by
    • ForeignKeyConstraint unnamed: project_id
    • PrimaryKeyConstraint unnamed: id
    • Index ix_sor_active (unique=False): service_ownership_rules.project_id, service_ownership_rules.is_active; options {}
    • Index ix_sor_project (unique=False): service_ownership_rules.project_id; options {}

    backend/app/models/postgres.py:4471

    Append-only audit trail for settings and secret changes.

    Append-only by application convention, not by database enforcement — no UPDATE trigger, no revoked grant, no WORM storage. The property is held by the backend.audit-write-discipline quality gate (scripts/quality_gate.py), which fails CI on application code that UPDATEs an audit table or DELETEs from one outside services/retention_service.py.

    Unlike the other audit tables this one is never purged: it has no project scope and it holds the retention purge-audit records themselves (setting_key = "retention_purge:{project_id}"), so the audit clock skips it on purpose. It therefore grows without limit until an operator prunes it out-of-band.

    Beyond that, keeping the rows safe is the operator’s responsibility: direct Postgres credentials can still rewrite or drop rows. Real immutability comes from restricted UPDATE/DELETE grants for the app role, WORM / object-lock storage for shipped logs, and off-host backups.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    setting_keyVARCHAR(100)FalseFalse—
    actionVARCHAR(30)FalseFalse—
    actor_idUUIDTrueFalse—users.id / SET NULL
    actor_nameVARCHAR(200)TrueFalse—
    changed_fieldsJSONTrueFalse—
    reasonTEXTTrueFalse—
    created_atDATETIMEFalseFalseserver=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: actor_id
    • PrimaryKeyConstraint unnamed: id
    • Index ix_settings_audit_actor (unique=False): settings_audit_log.actor_id; options {}
    • Index ix_settings_audit_key (unique=False): settings_audit_log.setting_key; options {}

    backend/app/models/postgres.py:5002

    Tenant/system-level SSO configuration for a SAML identity provider.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    display_nameVARCHAR(255)FalseFalse—
    provider_typeVARCHAR(20)FalseFalseapplication=SAML
    idp_entity_idVARCHAR(1000)TrueFalse—
    idp_sso_urlVARCHAR(2000)TrueFalse—
    idp_slo_urlVARCHAR(2000)TrueFalse—
    idp_certificateTEXTTrueFalse—
    sp_entity_idVARCHAR(1000)TrueFalse—
    sp_acs_urlVARCHAR(2000)TrueFalse—
    oidc_issuerVARCHAR(1000)TrueFalse—
    oidc_client_idVARCHAR(255)TrueFalse—
    oidc_scopesVARCHAR(500)TrueFalse—
    oidc_groups_claimVARCHAR(255)TrueFalse—
    audienceVARCHAR(1000)TrueFalse—
    role_mappingJSONTrueFalseapplication=dict
    default_roleVARCHAR(20)FalseFalseapplication=VIEWER
    group_attributeVARCHAR(255)TrueFalse—
    enforcement_modeVARCHAR(20)FalseFalseapplication=OPTIONAL
    is_activeBOOLEANFalseFalseapplication=False
    last_test_atDATETIMETrueFalse—
    last_test_successBOOLEANTrueFalse—
    last_test_errorTEXTTrueFalse—
    created_atDATETIMEFalseFalseserver=now()
    updated_atDATETIMEFalseFalseserver=now(); onupdate=now()

    Constraints and indexes:

    • CheckConstraint ck_sso_config_fields_by_provider: (provider_type <> 'SAML' OR (idp_entity_id IS NOT NULL AND idp_sso_url IS NOT NULL AND idp_certificate IS NOT NULL AND sp_entity_id IS NOT NULL AND sp_acs_url IS NOT NULL)) AND (provider_type <> 'OIDC' OR (oidc_issuer IS NOT NULL AND oidc_client_id IS NOT NULL))
    • PrimaryKeyConstraint unnamed: id
    • Index ix_sso_config_active (unique=False): sso_configurations.is_active; options {}
    • Index uq_sso_config_single_active (unique=True): sso_configurations.is_active; options {"postgresql_where": "is_active IS TRUE", "sqlite_where": "is_active IS TRUE"}

    backend/app/models/postgres.py:8002

    One personal-data export or erasure of one person (migration 0253).

    A new table rather than a third project_data_jobs kind: the export janitor and re-dispatcher read that table without a kind filter, and an erasure carries its own resumable state (phase = the LAST phase committed). kind / status / source / phase are the subject_rights_vocab vocabularies (CHECKed). subject_user_id is RESTRICT: the user row is never deleted (an erasure pseudonymises it in place). storage_key (the export archive) is never returned and never logged.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    kindVARCHAR(10)FalseFalse—
    statusVARCHAR(12)FalseFalseserver='queued'; application=queued
    sourceVARCHAR(8)FalseFalseserver='admin'; application=admin
    phaseVARCHAR(10)TrueFalse—
    subject_user_idUUIDFalseFalse—users.id / RESTRICT
    requested_by_idUUIDTrueFalse—users.id / SET NULL
    storage_keyTEXTTrueFalse—
    bytesBIGINTTrueFalse—
    file_countINTEGERTrueFalse—
    manifest_sha256VARCHAR(64)TrueFalse—
    archive_sha256VARCHAR(64)TrueFalse—
    signatureVARCHAR(16)TrueFalse—
    signing_key_idVARCHAR(64)TrueFalse—
    countsJSONBFalseFalseserver='{}'::jsonb; application=dict
    error_codeVARCHAR(40)TrueFalse—
    errorTEXTTrueFalse—
    attemptsINTEGERFalseFalseserver=0; application=0
    requested_atDATETIMEFalseFalseserver=now()
    started_atDATETIMETrueFalse—
    finished_atDATETIMETrueFalse—
    expires_atDATETIMETrueFalse—

    Constraints and indexes:

    • CheckConstraint ck_srj_erasure_is_admin: kind <> 'erasure' OR source = 'admin'
    • CheckConstraint ck_srj_kind: kind IN ('export', 'erasure')
    • CheckConstraint ck_srj_phase: phase IS NULL OR phase IN ('lockout', 'postgres', 'objects', 'redis', 'done')
    • CheckConstraint ck_srj_source: source IN ('self', 'admin')
    • CheckConstraint ck_srj_status: status IN ('queued', 'running', 'succeeded', 'failed', 'expired')
    • ForeignKeyConstraint unnamed: requested_by_id
    • ForeignKeyConstraint unnamed: subject_user_id
    • PrimaryKeyConstraint unnamed: id
    • Index ix_srj_requester (unique=False): subject_rights_jobs.requested_by_id, requested_at DESC; options {}
    • Index ix_srj_status_requested (unique=False): subject_rights_jobs.status, subject_rights_jobs.requested_at; options {}
    • Index ix_srj_subject_kind (unique=False): subject_rights_jobs.subject_user_id, subject_rights_jobs.kind, requested_at DESC; options {}
    • Index uq_srj_one_live_erasure (unique=True): subject_rights_jobs.subject_user_id; options {"postgresql_where": "kind = 'erasure' AND status IN ('queued', 'running')"}
    • Index uq_srj_one_live_export (unique=True): subject_rights_jobs.subject_user_id; options {"postgresql_where": "kind = 'export' AND status IN ('queued', 'running')"}

    backend/app/models/postgres.py:1093

    How long one suite of one run took (migration 0203).

    One row per run per EFFECTIVE suite (analytics_scope.effective_suite_sql), keyed by suite_key = run_compare_service.normalize_suite_name of it, so a re-finalize or a live re-drain upserts the same row. Written by suite_execution_service.upsert_for_run and the one-shot backfill.

    summed_ms is the sum of the suite’s test durations; wall_ms is how long it took on the clock, from source: report (the report stated it), derived (earliest case start to latest case finish) or derived_sum (no wall clock known: wall_ms and parallelism stay NULL rather than repeat the sum). setup_ms / teardown_ms are suite-level fixtures (before-all / after-all) as the report states them. run_created_at and branch are copied from the run because derived rows have no started_at and history reads window on the run’s time. Deleted with its run (CASCADE).

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    project_idUUIDFalseFalse—projects.id / CASCADE
    test_run_idUUIDFalseFalse—test_runs.id / CASCADE
    suite_nameVARCHAR(500)FalseFalse—
    suite_keyVARCHAR(500)FalseFalse—
    started_atDATETIMETrueFalse—
    finished_atDATETIMETrueFalse—
    wall_msBIGINTTrueFalse—
    summed_msBIGINTTrueFalse—
    setup_msBIGINTTrueFalse—
    teardown_msBIGINTTrueFalse—
    test_countINTEGERFalseFalseserver=0; application=0
    timed_countINTEGERFalseFalseserver=0; application=0
    passedINTEGERFalseFalseserver=0; application=0
    failedINTEGERFalseFalseserver=0; application=0
    brokenINTEGERFalseFalseserver=0; application=0
    skippedINTEGERFalseFalseserver=0; application=0
    unknownINTEGERFalseFalseserver=0; application=0
    parallelismFLOATTrueFalse—
    sourceVARCHAR(16)FalseFalse—
    run_created_atDATETIMEFalseFalse—
    branchVARCHAR(255)TrueFalse—
    created_atDATETIMEFalseFalseserver=now()
    updated_atDATETIMEFalseFalseserver=now(); onupdate=now()

    Constraints and indexes:

    • CheckConstraint ck_suite_executions_source: source IN ('report', 'derived', 'derived_sum')
    • ForeignKeyConstraint unnamed: project_id
    • ForeignKeyConstraint unnamed: test_run_id
    • PrimaryKeyConstraint unnamed: id
    • UniqueConstraint uq_suite_executions_run_suite: test_run_id, suite_key
    • Index ix_suite_executions_project_suite_run (unique=False): suite_executions.project_id, suite_executions.suite_key, suite_executions.run_created_at; options {}
    • Index ix_suite_executions_test_run_id (unique=False): suite_executions.test_run_id; options {}

    backend/app/models/postgres.py:3916

    Immutable audit log of suite membership changes detected during sync.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    project_idUUIDFalseFalse—projects.id / CASCADE
    suite_nameVARCHAR(500)FalseFalse—
    test_fingerprintVARCHAR(64)FalseFalse—
    test_nameVARCHAR(1000)FalseFalseapplication=
    event_typeVARCHAR(20)FalseFalse—
    run_idUUIDTrueFalse—test_runs.id / SET NULL
    old_valuesJSONTrueFalse—
    new_valuesJSONTrueFalse—
    detailsTEXTTrueFalse—
    created_atDATETIMEFalseFalseserver=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: project_id
    • ForeignKeyConstraint unnamed: run_id
    • PrimaryKeyConstraint unnamed: id
    • Index ix_sme_project_suite (unique=False): suite_membership_events.project_id, suite_membership_events.suite_name, suite_membership_events.created_at; options {}
    • Index ix_sme_run (unique=False): suite_membership_events.run_id; options {}

    backend/app/models/postgres.py:3937

    One comparison of the legacy suite memberships with the test catalog (migration 0248).

    An instance-wide run (no project_id column: project ids live inside the JSON for display only). trigger / status are the suite_parity_vocab vocabularies (CHECKed). Written ONLY by worker/suite_parity_runner.py: the claim (running) and the outcome (clean / diffs / failed). The rows are the evidence for the 7-day clock; uq_smpr_one_running keeps one comparison at a time.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    triggerVARCHAR(10)FalseFalse—
    requested_by_idUUIDTrueFalse—users.id / SET NULL
    statusVARCHAR(10)FalseFalse—
    started_atDATETIMEFalseFalseserver=now()
    finished_atDATETIMETrueFalse—
    window_runsINTEGERFalseFalse—
    projects_checkedINTEGERFalseFalseserver=0; application=0
    keys_comparedINTEGERFalseFalseserver=0; application=0
    blocking_diffsINTEGERFalseFalseserver=0; application=0
    expected_divergencesINTEGERFalseFalseserver=0; application=0
    countsJSONBFalseFalseserver='{}'::jsonb; application=dict
    per_projectJSONBFalseFalseserver='[]'::jsonb; application=list
    sampleJSONBFalseFalseserver='[]'::jsonb; application=list
    error_codeVARCHAR(64)TrueFalse—

    Constraints and indexes:

    • CheckConstraint ck_smpr_finished: (status = 'running') = (finished_at IS NULL)
    • CheckConstraint ck_smpr_status: status IN ('running', 'clean', 'diffs', 'failed')
    • CheckConstraint ck_smpr_trigger: trigger IN ('beat', 'admin')
    • ForeignKeyConstraint unnamed: requested_by_id
    • PrimaryKeyConstraint unnamed: id
    • Index ix_smpr_started (unique=False): started_at DESC; options {}
    • Index uq_smpr_one_running (unique=True): suite_membership_parity_runs.status; options {"postgresql_where": "status = 'running'"}

    backend/app/models/postgres.py:3886

    Tracks which test cases belong to which suite, linked to the run that confirmed membership.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    project_idUUIDFalseFalse—projects.id / CASCADE
    suite_nameVARCHAR(500)FalseFalse—
    test_fingerprintVARCHAR(64)FalseFalse—
    test_nameVARCHAR(1000)FalseFalse—
    class_nameVARCHAR(500)TrueFalse—
    managed_test_case_idUUIDTrueFalse—managed_test_cases.id / SET NULL
    sourceVARCHAR(20)FalseFalseapplication=execution
    statusVARCHAR(20)FalseFalseapplication=active
    last_seen_run_idUUIDTrueFalse—test_runs.id / SET NULL
    first_seen_run_idUUIDTrueFalse—test_runs.id / SET NULL
    deleted_at_run_idUUIDTrueFalse—test_runs.id / SET NULL
    review_tagVARCHAR(50)TrueFalse—
    tagsJSONTrueFalse—
    created_atDATETIMEFalseFalseserver=now()
    updated_atDATETIMEFalseFalseserver=now(); onupdate=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: deleted_at_run_id
    • ForeignKeyConstraint unnamed: first_seen_run_id
    • ForeignKeyConstraint unnamed: last_seen_run_id
    • ForeignKeyConstraint unnamed: managed_test_case_id
    • ForeignKeyConstraint unnamed: project_id
    • PrimaryKeyConstraint unnamed: id
    • UniqueConstraint uq_suite_membership: project_id, suite_name, test_fingerprint
    • Index ix_sm_fingerprint (unique=False): suite_memberships.test_fingerprint; options {}
    • Index ix_sm_project_suite (unique=False): suite_memberships.project_id, suite_memberships.suite_name; options {}
    • Index ix_sm_status (unique=False): suite_memberships.status; options {}

    backend/app/models/postgres.py:1054

    Human-in-the-loop overlay on AI analysis for a (run, suite) (migration 0076).

    Non-gating: the AI pipeline still completes runs without waiting for a review. State machine: pending → confirmed | acknowledged | review_later. Unique on (test_run_id, suite_name) so each run+suite has at most one review (later updates mutate the row instead of inserting).

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    project_idUUIDFalseFalse—projects.id / CASCADE
    suite_nameVARCHAR(500)FalseFalse—
    test_run_idUUIDFalseFalse—test_runs.id / CASCADE
    reviewer_user_idUUIDTrueFalse—users.id / SET NULL
    stateVARCHAR(20)FalseFalseserver='pending'; application=pending
    noteTEXTTrueFalse—
    reviewed_atDATETIMETrueFalse—
    created_atDATETIMEFalseFalseserver=now()
    updated_atDATETIMEFalseFalseserver=now(); onupdate=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: project_id
    • ForeignKeyConstraint unnamed: reviewer_user_id
    • ForeignKeyConstraint unnamed: test_run_id
    • PrimaryKeyConstraint unnamed: id
    • UniqueConstraint uq_suite_run_reviews_run_suite: test_run_id, suite_name
    • Index ix_suite_run_reviews_project_suite (unique=False): suite_run_reviews.project_id, suite_run_reviews.suite_name; options {}
    • Index ix_suite_run_reviews_state (unique=False): suite_run_reviews.state; options {}
    • Index ix_suite_run_reviews_test_run_id (unique=False): suite_run_reviews.test_run_id; options {}

    backend/app/models/postgres.py:6913

    A Jira epic a person filed for a recurring systemic cluster (migration 0255).

    Keyed by membership_key — the cluster identity that survives the nightly delete + re-insert of systemic_flake_cluster (for an UNCHANGED member set). A member change is a new key and so no link (no overlap matching). One epic per (project, membership_key): uq_sce_project_membership. cluster_label / member_fingerprints snapshot the cluster at filing. Never a Defect row (it would count in /defects, the KPIs and the gate).

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    project_idUUIDFalseFalse—projects.id / CASCADE
    membership_keyVARCHAR(32)FalseFalse—
    jira_keyVARCHAR(50)FalseFalse—
    jira_urlVARCHAR(1000)TrueFalse—
    cluster_labelVARCHAR(500)TrueFalse—
    member_fingerprintsJSONTrueFalse—
    created_byUUIDTrueFalse—users.id / SET NULL
    created_atDATETIMEFalseFalseserver=now()

    Constraints and indexes:

    • ForeignKeyConstraint fk_sce_created_by_users: created_by
    • ForeignKeyConstraint fk_sce_project_id_projects: project_id
    • PrimaryKeyConstraint unnamed: id
    • Index uq_sce_project_membership (unique=True): systemic_cluster_epics.project_id, systemic_cluster_epics.membership_key; options {}

    backend/app/models/postgres.py:6858

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    project_idUUIDFalseFalse—projects.id / CASCADE
    cluster_keyVARCHAR(32)FalseFalse—
    labelVARCHAR(500)FalseFalse—
    cause_familyVARCHAR(40)TrueFalse—
    sizeINTEGERFalseFalseapplication=0
    cohesionFLOATTrueFalse—
    co_failure_runsINTEGERFalseFalseapplication=0
    window_daysINTEGERFalseFalseapplication=60
    computed_atDATETIMEFalseFalseserver=now()
    membership_keyVARCHAR(32)TrueFalse—

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: project_id
    • PrimaryKeyConstraint unnamed: id
    • Index ix_systemic_cluster_project_membership (unique=False): systemic_flake_cluster.project_id, systemic_flake_cluster.membership_key; options {}
    • Index ux_systemic_cluster_project_key (unique=True): systemic_flake_cluster.project_id, systemic_flake_cluster.cluster_key; options {}

    backend/app/models/postgres.py:6893

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    cluster_idUUIDFalseFalse—systemic_flake_cluster.id / CASCADE
    test_fingerprintVARCHAR(64)FalseFalse—
    test_nameVARCHAR(1000)TrueFalse—
    failure_runsINTEGERFalseFalseapplication=0

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: cluster_id
    • PrimaryKeyConstraint unnamed: id
    • Index ux_systemic_member_cluster_fingerprint (unique=True): systemic_flake_cluster_member.cluster_id, systemic_flake_cluster_member.test_fingerprint; options {}

    backend/app/models/postgres.py:2234

    Per-(project, team) notification target for ownership-routed transition notifications.

    Teams exist only as free-text team_name strings on service_ownership_rules rows — many rules can share one team, so the channel lives in its own table keyed by (project, team_name) instead of being duplicated per rule. A transition event whose test resolves (via the ownership rules) to a team with an active row here is delivered directly to that team’s channel; everything else falls back to the project’s default notification preferences.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    project_idUUIDFalseFalse—projects.id / CASCADE
    team_nameVARCHAR(255)FalseFalse—
    channel_typeVARCHAR(20)FalseFalse—
    targetVARCHAR(2000)FalseFalse—
    is_activeBOOLEANFalseFalseapplication=True
    created_atDATETIMEFalseFalseserver=now()
    updated_atDATETIMEFalseFalseserver=now(); onupdate=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: project_id
    • PrimaryKeyConstraint unnamed: id
    • UniqueConstraint uq_team_notif_channel_project_team: project_id, team_name
    • Index ix_team_notif_channels_project (unique=False): team_notification_channels.project_id; options {}

    backend/app/models/postgres.py:4766

    Tracks onboarding wizard progress per project.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    project_idUUIDFalseFalse—projects.id / CASCADE
    step_keyVARCHAR(50)FalseFalse—
    statusVARCHAR(20)FalseFalseapplication=pending
    completed_atDATETIMETrueFalse—
    completed_byUUIDTrueFalse—users.id / SET NULL
    created_atDATETIMEFalseFalseserver=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: completed_by
    • ForeignKeyConstraint unnamed: project_id
    • PrimaryKeyConstraint unnamed: id
    • Index ix_tos_project_step (unique=True): tenant_onboarding_status.project_id, tenant_onboarding_status.step_key; options {}

    backend/app/models/postgres.py:1458

    Index-only attachment metadata for the latest-run snapshot.

    Phase 1 stores references (source_ref) only — bytes are not proxied. Anchored to the test’s permanent record (CASCADE) like steps; test_step_id (CASCADE, nullable) links a step-scoped attachment, NULL = test-level. Migration 0093.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    canonical_test_case_idUUIDFalseFalse—canonical_test_cases.id / CASCADE
    test_step_idUUIDTrueFalse—test_steps.id / CASCADE
    source_test_run_idUUIDTrueFalse—test_runs.id / SET NULL
    nameVARCHAR(500)FalseFalse—
    source_refVARCHAR(1000)TrueFalse—
    media_typeVARCHAR(100)TrueFalse—
    created_atDATETIMEFalseFalseserver=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: canonical_test_case_id
    • ForeignKeyConstraint unnamed: source_test_run_id
    • ForeignKeyConstraint unnamed: test_step_id
    • PrimaryKeyConstraint unnamed: id
    • Index ix_test_attachments_canonical (unique=False): test_attachments.canonical_test_case_id; options {}
    • Index ix_test_attachments_step (unique=False): test_attachments.test_step_id; options {}

    ORM navigation and cascade declarations:

    canonical_test_case: Mapped['CanonicalTestCase'] = relationship('CanonicalTestCase', back_populates='attachments')
    test_step: Mapped[Optional['TestStep']] = relationship('TestStep', back_populates='attachments')

    backend/app/models/postgres.py:3828

    Append-only compliance audit trail for all test management actions.

    Append-only by application convention, not by database enforcement. There is no UPDATE trigger, no revoked grant and no WORM storage on this table — the only trigger in the whole migration set is the search-vector one in 0001. What actually holds the property is the backend.audit-write-discipline quality gate (scripts/quality_gate.py): it fails CI on any application code that UPDATEs an audit table, or DELETEs from one outside services/retention_service.py.

    Rows are removed by the retention purge on the audit clock (ProjectRetentionPolicy.audit_days; floor 365 days, default 2555 ≈ 7 years, and validated to be ≥ runs_days so audit rows outlive the runs they describe). Purges only run for projects that explicitly enable a policy, and every execute-mode purge writes its own settings_audit_log record.

    Beyond that limit, keeping the rows safe is the operator’s responsibility: anyone holding direct Postgres credentials can still rewrite or drop rows. Real immutability comes from outside the application — restricted UPDATE/DELETE grants for the app role, WORM / object-lock storage for shipped logs, and off-host backups.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    entity_typeVARCHAR(30)FalseFalse—
    entity_idUUIDFalseFalse—
    project_idUUIDTrueFalse—projects.id / SET NULL
    actionVARCHAR(50)FalseFalse—
    actor_idUUIDTrueFalse—users.id / SET NULL
    actor_nameVARCHAR(200)TrueFalse—
    old_valuesJSONTrueFalse—
    new_valuesJSONTrueFalse—
    detailsTEXTTrueFalse—
    reasonVARCHAR(500)TrueFalse—
    policy_snapshotJSONTrueFalse—
    transition_fromVARCHAR(30)TrueFalse—
    transition_toVARCHAR(30)TrueFalse—
    created_atDATETIMEFalseFalseserver=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: actor_id
    • ForeignKeyConstraint unnamed: project_id
    • PrimaryKeyConstraint unnamed: id
    • Index ix_tcal_actor (unique=False): test_case_audit_logs.actor_id; options {}
    • Index ix_tcal_entity (unique=False): test_case_audit_logs.entity_type, test_case_audit_logs.entity_id; options {}
    • Index ix_tcal_project_created (unique=False): test_case_audit_logs.project_id, test_case_audit_logs.created_at; options {}
    • Index ix_test_case_audit_logs_created_at (unique=False): test_case_audit_logs.created_at; options {}

    backend/app/models/postgres.py:3313

    Threaded comment on a ManagedTestCase.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    test_case_idUUIDFalseFalse—managed_test_cases.id / CASCADE
    author_idUUIDTrueFalse—users.id / SET NULL
    contentTEXTFalseFalse—
    comment_typeVARCHAR(30)FalseFalseapplication=general
    parent_idUUIDTrueFalse—test_case_comments.id / SET NULL
    step_numberINTEGERTrueFalse—
    is_resolvedBOOLEANFalseFalseapplication=False
    created_atDATETIMEFalseFalseserver=now()
    updated_atDATETIMEFalseFalseserver=now(); onupdate=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: author_id
    • ForeignKeyConstraint unnamed: parent_id
    • ForeignKeyConstraint unnamed: test_case_id
    • PrimaryKeyConstraint unnamed: id
    • Index ix_tcc_test_case (unique=False): test_case_comments.test_case_id; options {}

    backend/app/models/postgres.py:1495

    Denormalized history for fast timeline queries.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    test_case_idUUIDFalseFalse—test_cases.id / CASCADE
    test_run_idUUIDFalseFalse—test_runs.id / CASCADE
    test_fingerprintVARCHAR(64)FalseFalse—
    statusVARCHAR(20)FalseFalse—
    duration_msINTEGERTrueFalse—
    failure_categoryVARCHAR(30)TrueFalse—
    created_atDATETIMEFalseFalseserver=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: test_case_id
    • ForeignKeyConstraint unnamed: test_run_id
    • PrimaryKeyConstraint unnamed: id
    • Index ix_history_fingerprint_date (unique=False): test_case_history.test_fingerprint, test_case_history.created_at; options {}
    • Index ix_history_fingerprint_date_status (unique=False): test_case_history.test_fingerprint, test_case_history.created_at, test_case_history.status; options {}
    • Index ix_history_test_case_id (unique=False): test_case_history.test_case_id; options {}
    • Index ix_history_test_run_id (unique=False): test_case_history.test_run_id; options {}

    ORM navigation and cascade declarations:

    test_case: Mapped['TestCase'] = relationship('TestCase', back_populates='history')

    backend/app/models/postgres.py:3334

    A requirement key a person typed on an authored test case (migration 0229).

    MANUAL only: no source column, no knowledge-source key, no backfill. requirement_key is the key as written; requirement_key_norm the same key through core.test_library_vocab.normalize_requirement_key, the form every comparison uses (unique per case). project_id is the case’s, denormalised so a project’s known keys are one index read. link_type: verifies | partially_verifies | related (REQUIREMENT_LINK_TYPES). A link is not a content edit: it never bumps the case version; the timeline and the ledger record it.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    project_idUUIDFalseFalse—projects.id / CASCADE
    test_case_idUUIDFalseFalse—managed_test_cases.id / CASCADE
    requirement_keyVARCHAR(200)FalseFalse—
    requirement_key_normVARCHAR(200)FalseFalse—
    requirement_titleVARCHAR(500)TrueFalse—
    link_typeVARCHAR(20)FalseFalseserver='verifies'; application=verifies
    created_by_idUUIDTrueFalse—users.id / SET NULL
    created_atDATETIMEFalseFalseserver=now()

    Constraints and indexes:

    • CheckConstraint ck_tcrl_key_norm_nonempty: requirement_key_norm <> ''
    • CheckConstraint ck_tcrl_link_type: link_type IN ('verifies', 'partially_verifies', 'related')
    • ForeignKeyConstraint unnamed: created_by_id
    • ForeignKeyConstraint unnamed: project_id
    • ForeignKeyConstraint unnamed: test_case_id
    • PrimaryKeyConstraint unnamed: id
    • UniqueConstraint uq_tcrl_case_key: test_case_id, requirement_key_norm
    • Index ix_tcrl_project_key (unique=False): test_case_requirement_links.project_id, test_case_requirement_links.requirement_key_norm; options {}

    backend/app/models/postgres.py:3277

    Review cycle for a ManagedTestCase.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    test_case_idUUIDFalseFalse—managed_test_cases.id / CASCADE
    reviewer_idUUIDTrueFalse—users.id / SET NULL
    requested_by_idUUIDTrueFalse—users.id / SET NULL
    statusVARCHAR(30)FalseFalseapplication=pending
    ai_review_completedBOOLEANFalseFalseapplication=False
    ai_quality_scoreINTEGERTrueFalse—
    ai_review_notesJSONTrueFalse—
    ai_reviewed_atDATETIMETrueFalse—
    human_notesTEXTTrueFalse—
    reviewed_atDATETIMETrueFalse—
    created_atDATETIMEFalseFalseserver=now()
    updated_atDATETIMEFalseFalseserver=now(); onupdate=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: requested_by_id
    • ForeignKeyConstraint unnamed: reviewer_id
    • ForeignKeyConstraint unnamed: test_case_id
    • PrimaryKeyConstraint unnamed: id
    • Index ix_tcr_reviewer (unique=False): test_case_reviews.reviewer_id; options {}
    • Index ix_tcr_test_case (unique=False): test_case_reviews.test_case_id; options {}
    • Index uq_test_case_reviews_one_open_per_case (unique=True): test_case_reviews.test_case_id; options {"postgresql_where": "status IN ('pending', 'in_progress')"}

    backend/app/models/postgres.py:3225

    Immutable snapshot of a ManagedTestCase at each save.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    test_case_idUUIDFalseFalse—managed_test_cases.id / CASCADE
    versionINTEGERFalseFalse—
    titleVARCHAR(500)FalseFalse—
    descriptionTEXTTrueFalse—
    objectiveTEXTTrueFalse—
    preconditionsTEXTTrueFalse—
    stepsJSONTrueFalse—
    parametersJSONTrueFalse—
    expected_resultTEXTTrueFalse—
    test_dataTEXTTrueFalse—
    test_typeVARCHAR(50)TrueFalse—
    priorityVARCHAR(20)TrueFalse—
    severityVARCHAR(20)TrueFalse—
    feature_areaVARCHAR(500)TrueFalse—
    suite_nameVARCHAR(500)TrueFalse—
    test_suite_idUUIDTrueFalse—
    tagsJSONTrueFalse—
    estimated_duration_minutesINTEGERTrueFalse—
    is_automatedBOOLEANTrueFalse—
    automation_statusVARCHAR(30)TrueFalse—
    test_fingerprintVARCHAR(64)TrueFalse—
    statusVARCHAR(30)FalseFalse—
    changed_by_idUUIDTrueFalse—users.id / SET NULL
    change_summaryVARCHAR(500)TrueFalse—
    change_typeVARCHAR(30)FalseFalseapplication=updated
    changed_fieldsJSONTrueFalse—
    sourceVARCHAR(16)TrueFalse—
    assignee_idUUIDTrueFalse—users.id / SET NULL
    created_atDATETIMEFalseFalseserver=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: assignee_id
    • ForeignKeyConstraint unnamed: changed_by_id
    • ForeignKeyConstraint unnamed: test_case_id
    • PrimaryKeyConstraint unnamed: id
    • UniqueConstraint uq_test_case_versions_case_version: test_case_id, version
    • Index ix_tcv_test_case (unique=False): test_case_versions.test_case_id; options {}

    backend/app/models/postgres.py:823

    Individual test case result within a run.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    test_run_idUUIDFalseFalse—test_runs.id / CASCADE
    test_fingerprintVARCHAR(64)FalseFalse—
    canonical_test_case_idUUIDTrueFalse—canonical_test_cases.id / SET NULL
    test_nameVARCHAR(1000)FalseFalse—
    full_nameVARCHAR(2000)TrueFalse—
    suite_nameVARCHAR(500)TrueFalse—
    class_nameVARCHAR(500)TrueFalse—
    package_nameVARCHAR(500)TrueFalse—
    statusVARCHAR(20)FalseFalseapplication=TestStatus.UNKNOWN
    duration_msINTEGERTrueFalse—
    severityVARCHAR(20)TrueFalse—
    featureVARCHAR(500)TrueFalse—
    storyVARCHAR(500)TrueFalse—
    epicVARCHAR(500)TrueFalse—
    ownerVARCHAR(255)TrueFalse—
    tagsJSONTrueFalse—
    failure_categoryVARCHAR(30)TrueFalse—
    error_messageTEXTTrueFalse—
    assigned_to_user_idUUIDTrueFalse—users.id / SET NULL
    triage_statusVARCHAR(30)FalseFalseserver=PENDING_REVIEW; application=PENDING_REVIEW
    triage_notesVARCHAR(2000)TrueFalse—
    triage_updated_atDATETIMETrueFalse—
    triage_updated_by_user_idUUIDTrueFalse—users.id / SET NULL
    retry_countINTEGERTrueFalse—
    is_flaky_runBOOLEANTrueFalse—
    stack_traceTEXTTrueFalse—
    step_countINTEGERTrueFalse—
    source_uuidVARCHAR(255)TrueFalse—
    source_history_idVARCHAR(255)TrueFalse—
    source_test_case_idVARCHAR(255)TrueFalse—
    parser_formatVARCHAR(100)TrueFalse—
    parser_versionVARCHAR(100)TrueFalse—
    source_parametersJSONTrueFalse—
    source_linksJSONTrueFalse—
    source_labelsJSONTrueFalse—
    source_extensionsJSONTrueFalse—
    service_nameVARCHAR(255)TrueFalse—
    component_namesJSONTrueFalse—
    steps_presentBOOLEANFalseFalseserver=false; application=False
    started_atDATETIMETrueFalse—
    finished_atDATETIMETrueFalse—
    setup_msINTEGERTrueFalse—
    call_msINTEGERTrueFalse—
    teardown_msINTEGERTrueFalse—
    worker_idVARCHAR(64)TrueFalse—
    timing_sourceVARCHAR(16)TrueFalse—
    minio_s3_prefixVARCHAR(1000)TrueFalse—
    has_attachmentsBOOLEANFalseFalseapplication=False
    created_atDATETIMEFalseFalseserver=now()
    updated_atDATETIMEFalseFalseserver=now(); onupdate=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: assigned_to_user_id
    • ForeignKeyConstraint unnamed: canonical_test_case_id
    • ForeignKeyConstraint unnamed: test_run_id
    • ForeignKeyConstraint unnamed: triage_updated_by_user_id
    • PrimaryKeyConstraint unnamed: id
    • UniqueConstraint uq_test_cases_run_fingerprint: test_run_id, test_fingerprint
    • Index ix_test_cases_assigned_to_user_id (unique=False): test_cases.assigned_to_user_id; options {}
    • Index ix_test_cases_assignee_triage (unique=False): test_cases.assigned_to_user_id, test_cases.triage_status; options {}
    • Index ix_test_cases_canonical (unique=False): test_cases.canonical_test_case_id; options {}
    • Index ix_test_cases_fingerprint (unique=False): test_cases.test_fingerprint; options {}
    • Index ix_test_cases_run_duration (unique=False): test_cases.test_run_id, duration_ms DESC NULLS LAST; options {}
    • Index ix_test_cases_run_status (unique=False): test_cases.test_run_id, test_cases.status; options {}
    • Index ix_test_cases_run_suite (unique=False): test_cases.test_run_id, test_cases.suite_name; options {}
    • Index ix_test_cases_semantic_reindex_keyset (unique=False): test_cases.created_at, test_cases.id; options {}
    • Index ix_test_cases_test_fingerprint (unique=False): test_cases.test_fingerprint; options {}

    ORM navigation and cascade declarations:

    test_run: Mapped['TestRun'] = relationship('TestRun', back_populates='test_cases')
    history: Mapped[list['TestCaseHistory']] = relationship('TestCaseHistory', back_populates='test_case')
    ai_analysis: Mapped[Optional['AIAnalysis']] = relationship('AIAnalysis', back_populates='test_case', uselist=False)
    defects: Mapped[list['Defect']] = relationship('Defect', back_populates='test_case')
    canonical_test_case: Mapped[Optional['CanonicalTestCase']] = relationship('CanonicalTestCase', back_populates='test_cases')

    backend/app/models/postgres.py:1155

    Per-TestCase human review overlay for AI-flagged failures (migration 0081).

    When the AI analysis pipeline flags a failure as requires_human_review (model missing, low confidence, fallback path), the UI shows a “Pending Human Review” tag. This row records the human’s verdict once they look: reviewed (AI was right), defect_filed (ticket created; defect_link captures the URL), false_positive (flake or test bug; downstream un-tags), or reproducible (failure confirmed locally, awaiting fix).

    Naming note: the older test_case_reviews table belongs to the managed-test-AUTHORING workflow (review of an authored test definition before it’s published). This table is keyed on test_cases.id — the execution row — and is unrelated.

    One row per test_case_id (UNIQUE). Transitions mutate the row in-place; cross-test audit lives in test_case_audit_logs for cases that need a timeline. Keep this table small and queryable for the inbox + dashboards.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    test_case_idUUIDFalseFalse—test_cases.id / CASCADE
    project_idUUIDFalseFalse—projects.id / CASCADE
    stateVARCHAR(30)FalseFalseserver='pending_review'; application=pending_review
    reviewed_by_user_idUUIDTrueFalse—users.id / SET NULL
    defect_linkVARCHAR(2000)TrueFalse—
    noteTEXTTrueFalse—
    transitioned_atDATETIMETrueFalse—
    created_atDATETIMEFalseFalseserver=now()
    updated_atDATETIMEFalseFalseserver=now(); onupdate=now()

    Constraints and indexes:

    • CheckConstraint ck_ter_defect_link_required: state <> 'defect_filed' OR NULLIF(BTRIM(defect_link), '') IS NOT NULL
    • CheckConstraint ck_ter_state_valid: state IN ('pending_review', 'reviewed', 'defect_filed', 'false_positive', 'reproducible')
    • ForeignKeyConstraint unnamed: project_id
    • ForeignKeyConstraint unnamed: reviewed_by_user_id
    • ForeignKeyConstraint unnamed: test_case_id
    • PrimaryKeyConstraint unnamed: id
    • UniqueConstraint uq_ter_test_case_id: test_case_id
    • Index ix_ter_project_state (unique=False): test_execution_reviews.project_id, test_execution_reviews.state; options {}
    • Index ix_ter_reviewed_by (unique=False): test_execution_reviews.reviewed_by_user_id; options {}

    backend/app/models/postgres.py:4887

    Persisted test health findings per run from TestHealthAgent.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    test_run_idUUIDFalseFalse—test_runs.id / CASCADE
    test_case_idUUIDFalseFalse—test_cases.id / CASCADE
    test_nameVARCHAR(1000)FalseFalse—
    health_scoreINTEGERFalseFalse—
    violationsJSONTrueFalseapplication=list
    critical_countINTEGERFalseFalseapplication=0
    warning_countINTEGERFalseFalseapplication=0
    recommendationTEXTTrueFalse—
    anti_patternsJSONTrueFalseapplication=list
    created_atDATETIMEFalseFalseserver=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: test_case_id
    • ForeignKeyConstraint unnamed: test_run_id
    • PrimaryKeyConstraint unnamed: id
    • Index ix_thr_run (unique=False): test_health_recommendations.test_run_id; options {}
    • Index ix_thr_test_case (unique=False): test_health_recommendations.test_case_id; options {}

    backend/app/models/postgres.py:3494

    A test case entry within a TestPlan.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    plan_idUUIDFalseFalse—test_plans.id / CASCADE
    test_case_idUUIDFalseFalse—managed_test_cases.id / CASCADE
    order_indexINTEGERFalseFalseapplication=0
    priority_overrideVARCHAR(20)TrueFalse—
    execution_statusVARCHAR(30)FalseFalseapplication=not_run
    executed_by_idUUIDTrueFalse—users.id / SET NULL
    executed_atDATETIMETrueFalse—
    execution_notesTEXTTrueFalse—
    actual_duration_minutesINTEGERTrueFalse—
    executed_case_versionINTEGERTrueFalse—
    test_case_result_idUUIDTrueFalse—test_cases.id / SET NULL
    created_atDATETIMEFalseFalseserver=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: executed_by_id
    • ForeignKeyConstraint unnamed: plan_id
    • ForeignKeyConstraint unnamed: test_case_id
    • ForeignKeyConstraint unnamed: test_case_result_id
    • PrimaryKeyConstraint unnamed: id
    • UniqueConstraint uq_plan_test_case: plan_id, test_case_id
    • Index ix_tpi_plan (unique=False): test_plan_items.plan_id; options {}

    backend/app/models/postgres.py:3451

    Named collection of test cases forming an executable test plan.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    project_idUUIDFalseFalse—projects.id / CASCADE
    nameVARCHAR(500)FalseFalse—
    descriptionTEXTTrueFalse—
    objectiveTEXTTrueFalse—
    statusVARCHAR(30)FalseFalseapplication=draft
    planned_start_dateDATETIMETrueFalse—
    planned_end_dateDATETIMETrueFalse—
    actual_start_dateDATETIMETrueFalse—
    actual_end_dateDATETIMETrueFalse—
    created_by_idUUIDTrueFalse—users.id / SET NULL
    assigned_to_idUUIDTrueFalse—users.id / SET NULL
    ai_generatedBOOLEANFalseFalseapplication=False
    ai_generation_contextTEXTTrueFalse—
    total_casesINTEGERFalseFalseapplication=0
    executed_casesINTEGERFalseFalseapplication=0
    passed_casesINTEGERFalseFalseapplication=0
    failed_casesINTEGERFalseFalseapplication=0
    blocked_casesINTEGERFalseFalseapplication=0
    tagsJSONTrueFalse—
    created_atDATETIMEFalseFalseserver=now()
    updated_atDATETIMEFalseFalseserver=now(); onupdate=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: assigned_to_id
    • ForeignKeyConstraint unnamed: created_by_id
    • ForeignKeyConstraint unnamed: project_id
    • PrimaryKeyConstraint unnamed: id
    • Index ix_tp_project (unique=False): test_plans.project_id; options {}
    • Index ix_tp_status (unique=False): test_plans.status; options {}

    backend/app/models/postgres.py:349

    Represents a single CI/CD pipeline execution (Jenkins build).

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    project_idUUIDFalseFalse—projects.id / CASCADE
    build_numberVARCHAR(100)FalseFalse—
    jenkins_jobVARCHAR(500)TrueFalse—
    trigger_sourceVARCHAR(50)TrueFalse—
    branchVARCHAR(255)TrueFalse—
    commit_hashVARCHAR(64)TrueFalse—
    statusVARCHAR(20)FalseFalseapplication=LaunchStatus.IN_PROGRESS
    ingestion_sourceVARCHAR(20)FalseFalseserver=unknown; application=unknown
    ci_providerVARCHAR(30)TrueFalse—
    ci_repoVARCHAR(300)TrueFalse—
    pr_numberINTEGERTrueFalse—
    ci_actorVARCHAR(120)TrueFalse—
    ci_run_urlVARCHAR(1000)TrueFalse—
    ingestion_identityVARCHAR(64)TrueFalse—
    ingestion_attempted_testsINTEGERTrueFalse—
    ingestion_rejected_testsINTEGERFalseFalseserver=0; application=0
    ingestion_completeBOOLEANTrueFalse—
    ingestion_rejection_reasonsJSONBTrueFalse—
    environmentVARCHAR(100)TrueFalse—
    primary_release_idUUIDTrueFalse—releases.id / SET NULL
    ai_recommendationVARCHAR(20)TrueFalse—
    total_testsINTEGERFalseFalseapplication=0
    passed_testsINTEGERFalseFalseapplication=0
    failed_testsINTEGERFalseFalseapplication=0
    skipped_testsINTEGERFalseFalseapplication=0
    broken_testsINTEGERFalseFalseapplication=0
    unknown_testsINTEGERFalseFalseserver=0; application=0
    pass_rateFLOATTrueFalse—
    duration_msINTEGERTrueFalse—
    exec_started_atDATETIMETrueFalse—
    exec_finished_atDATETIMETrueFalse—
    wall_clock_msBIGINTTrueFalse—
    timing_sourceVARCHAR(16)TrueFalse—
    ocp_pod_nameVARCHAR(255)TrueFalse—
    ocp_nodeVARCHAR(255)TrueFalse—
    ocp_namespaceVARCHAR(255)TrueFalse—
    ocp_metadataJSONTrueFalse—
    minio_prefixVARCHAR(1000)TrueFalse—
    tagsJSONTrueFalse—
    primary_suite_nameVARCHAR(500)TrueFalse—
    suite_namesJSONTrueFalse—
    event_archiveJSONBTrueFalse—
    event_archive_atDATETIMETrueFalse—
    duration_findingsJSONBTrueFalse—
    run_contextJSONBTrueFalse—
    start_timeDATETIMETrueFalse—
    end_timeDATETIMETrueFalse—
    created_atDATETIMEFalseFalseserver=now()
    updated_atDATETIMEFalseFalseserver=now(); onupdate=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: primary_release_id
    • ForeignKeyConstraint unnamed: project_id
    • PrimaryKeyConstraint unnamed: id
    • Index ix_test_runs_created_at (unique=False): test_runs.created_at; options {}
    • Index ix_test_runs_ingestion_identity (unique=False): test_runs.ingestion_identity; options {}
    • Index ix_test_runs_primary_suite_name (unique=False): test_runs.primary_suite_name; options {}
    • Index ix_test_runs_project_branch_created (unique=False): test_runs.project_id, coalesce(lower(branch), ''), test_runs.created_at; options {}
    • Index ix_test_runs_project_pr (unique=False): test_runs.project_id, test_runs.pr_number; options {"postgresql_where": "pr_number IS NOT NULL"}
    • Index ix_test_runs_project_recommendation_created (unique=False): test_runs.project_id, test_runs.ai_recommendation, test_runs.created_at; options {}
    • Index ix_test_runs_project_release_created (unique=False): test_runs.project_id, test_runs.primary_release_id, test_runs.created_at; options {}
    • Index ix_test_runs_project_status (unique=False): test_runs.project_id, test_runs.status; options {}
    • Index ix_test_runs_project_status_created (unique=False): test_runs.project_id, test_runs.status, test_runs.created_at; options {}
    • Index uq_test_runs_legacy_build (unique=True): test_runs.project_id, test_runs.build_number, test_runs.jenkins_job; options {"postgresql_where": "ingestion_identity IS NULL"}
    • Index uq_test_runs_project_ingestion_identity (unique=True): test_runs.project_id, test_runs.ingestion_identity; options {"postgresql_where": "ingestion_identity IS NOT NULL"}

    ORM navigation and cascade declarations:

    project: Mapped['Project'] = relationship('Project', back_populates='test_runs')
    test_cases: Mapped[list['TestCase']] = relationship('TestCase', back_populates='test_run', lazy='dynamic')

    backend/app/models/postgres.py:1406

    Per-run, compact step-outcome history for cross-run step-flip analysis.

    Distinct from :class:TestStep, which is a LATEST-RUN-ONLY snapshot (delete+reinsert on every ingest) and therefore cannot answer “which step flipped between run N-1 and run N”. test_step_runs instead RETAINS one flat row per (canonical_test_case_id, source_test_run_id, ordinal) so a test’s step outcomes accumulate across runs (migration 0097).

    Kept small on purpose: only the identity (ordinal/depth/name/keyword) and the per-run signal (status/duration_ms) needed to detect a step-flip. The heavy, PII-bearing columns (assertion_message/trace, expected/actual, parameters, attachments) live ONLY on the latest-run :class:TestStep snapshot and are NOT duplicated per run. source_test_run_id is CASCADE (the row IS about that run — when the run is deleted its step history goes with it), unlike the snapshot’s SET NULL pointer to where it came from.

    Written by ingestion alongside the snapshot, safe to write again per (canonical, run) (delete this run’s rows then reinsert). NEVER committed by the service — the ingestion router owns the commit.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    canonical_test_case_idUUIDFalseFalse—canonical_test_cases.id / CASCADE
    source_test_run_idUUIDFalseFalse—test_runs.id / CASCADE
    ordinalINTEGERFalseFalse—
    depthINTEGERFalseFalseserver=0; application=0
    nameVARCHAR(2000)FalseFalse—
    keywordVARCHAR(50)TrueFalse—
    statusVARCHAR(20)FalseFalse—
    duration_msINTEGERTrueFalse—
    created_atDATETIMEFalseFalseserver=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: canonical_test_case_id
    • ForeignKeyConstraint unnamed: source_test_run_id
    • PrimaryKeyConstraint unnamed: id
    • UniqueConstraint uq_test_step_runs_canonical_run_ordinal: canonical_test_case_id, source_test_run_id, ordinal
    • Index ix_test_step_runs_source_run (unique=False): test_step_runs.source_test_run_id; options {}

    backend/app/models/postgres.py:1342

    One granular step in the LATEST-RUN-ONLY snapshot for a logical test.

    LOCKED retention model (migration 0093): steps anchor to the project-scoped canonical_test_cases identity — exactly ONE snapshot per (project_id, test_fingerprint) — NOT to the per-run test_cases rows that accumulate. On ingest of a newer run, ingestion DELETEs this canonical test’s steps and INSERTs the new ones inside the ingestion-pipeline transaction. source_test_run_id only records where it came from (which run produced the snapshot). parent_step_id (self-FK, CASCADE) models nested steps.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    canonical_test_case_idUUIDFalseFalse—canonical_test_cases.id / CASCADE
    source_test_run_idUUIDTrueFalse—test_runs.id / SET NULL
    parent_step_idUUIDTrueFalse—test_steps.id / CASCADE
    ordinalINTEGERFalseFalse—
    depthINTEGERFalseFalseserver=0; application=0
    nameVARCHAR(2000)FalseFalse—
    keywordVARCHAR(50)TrueFalse—
    statusVARCHAR(20)FalseFalse—
    duration_msINTEGERTrueFalse—
    start_msBIGINTTrueFalse—
    assertion_messageTEXTTrueFalse—
    assertion_traceTEXTTrueFalse—
    expected_valueTEXTTrueFalse—
    actual_valueTEXTTrueFalse—
    parametersJSONBTrueFalse—
    created_atDATETIMEFalseFalseserver=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: canonical_test_case_id
    • ForeignKeyConstraint unnamed: parent_step_id
    • ForeignKeyConstraint unnamed: source_test_run_id
    • PrimaryKeyConstraint unnamed: id
    • Index ix_test_steps_canonical_ordinal (unique=False): test_steps.canonical_test_case_id, test_steps.ordinal; options {}
    • Index ix_test_steps_parent (unique=False): test_steps.parent_step_id; options {}
    • Index ix_test_steps_source_run (unique=False): test_steps.source_test_run_id; options {}

    ORM navigation and cascade declarations:

    canonical_test_case: Mapped['CanonicalTestCase'] = relationship('CanonicalTestCase', back_populates='steps')
    children: Mapped[list['TestStep']] = relationship('TestStep', back_populates='parent', cascade='all, delete-orphan', lazy='select')
    parent: Mapped[Optional['TestStep']] = relationship('TestStep', back_populates='children', remote_side='TestStep.id')
    attachments: Mapped[list['TestAttachment']] = relationship('TestAttachment', back_populates='test_step', cascade='all, delete-orphan', lazy='select')

    backend/app/models/postgres.py:3526

    AI-generated or manually authored test strategy document for a project.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    project_idUUIDFalseFalse—projects.id / CASCADE
    nameVARCHAR(500)FalseFalse—
    version_labelVARCHAR(50)FalseFalseapplication=v1.0
    statusVARCHAR(30)FalseFalseapplication=draft
    objectiveTEXTTrueFalse—
    scopeTEXTTrueFalse—
    out_of_scopeTEXTTrueFalse—
    test_approachTEXTTrueFalse—
    risk_assessmentJSONTrueFalse—
    test_typesJSONTrueFalse—
    entry_criteriaJSONTrueFalse—
    exit_criteriaJSONTrueFalse—
    environmentsJSONTrueFalse—
    automation_approachTEXTTrueFalse—
    defect_managementTEXTTrueFalse—
    ai_generatedBOOLEANFalseFalseapplication=True
    generation_contextTEXTTrueFalse—
    ai_model_usedVARCHAR(100)TrueFalse—
    created_by_idUUIDTrueFalse—users.id / SET NULL
    approved_by_idUUIDTrueFalse—users.id / SET NULL
    approved_atDATETIMETrueFalse—
    created_atDATETIMEFalseFalseserver=now()
    updated_atDATETIMEFalseFalseserver=now(); onupdate=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: approved_by_id
    • ForeignKeyConstraint unnamed: created_by_id
    • ForeignKeyConstraint unnamed: project_id
    • PrimaryKeyConstraint unnamed: id
    • Index ix_ts_project (unique=False): test_strategies.project_id; options {}

    backend/app/models/postgres.py:1027

    Explicit owner for a (project, suite_name) pair (migration 0076).

    Keyed by suite_name (string) to match the legacy aggregated Test Suites view served from /api/v1/test-management/suites. Falls back to Project.manager_user_id when no row exists for a given suite.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    project_idUUIDFalseFalse—projects.id / CASCADE
    suite_nameVARCHAR(500)FalseFalse—
    owner_user_idUUIDTrueFalse—users.id / SET NULL
    created_atDATETIMEFalseFalseserver=now()
    updated_atDATETIMEFalseFalseserver=now(); onupdate=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: owner_user_id
    • ForeignKeyConstraint unnamed: project_id
    • PrimaryKeyConstraint unnamed: id
    • UniqueConstraint uq_test_suite_owners_proj_suite: project_id, suite_name
    • Index ix_test_suite_owners_project_id (unique=False): test_suite_owners.project_id; options {}

    backend/app/models/postgres.py:992

    Project-scoped grouping of test cases.

    Replaces the prior string-based suite_name model with its own entity. Every project gets a row with is_default=True named Default Suite ({project.name}) — new test cases ingested without an explicit suite are auto-assigned to it.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    project_idUUIDFalseFalse—projects.id / CASCADE
    nameVARCHAR(500)FalseFalse—
    descriptionTEXTTrueFalse—
    is_defaultBOOLEANFalseFalseserver=false; application=False
    tagsJSONTrueFalse—
    created_atDATETIMEFalseFalseserver=now()
    updated_atDATETIMEFalseFalseserver=now(); onupdate=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: project_id
    • PrimaryKeyConstraint unnamed: id
    • UniqueConstraint uq_test_suites_project_name: project_id, name
    • Index ix_test_suites_project_default (unique=True): test_suites.project_id; options {"postgresql_where": "is_default IS TRUE"}
    • Index ix_test_suites_project_id (unique=False): test_suites.project_id; options {}

    ORM navigation and cascade declarations:

    project: Mapped['Project'] = relationship('Project', back_populates='test_suites')
    canonical_test_cases: Mapped[list['CanonicalTestCase']] = relationship('CanonicalTestCase', back_populates='test_suite', cascade='all, delete-orphan')

    backend/app/models/postgres.py:1860

    A note attached to a project’s daily charts (migration 0218).

    An inclusive UTC day range (starts_on .. ends_on; a one-day note has both equal), 1-280 characters of text, and optionally ONE suite (suite_name; None = every suite of the project). Created by a QA engineer or above; changed or removed by its author or a QA lead or above (timeline_note_service.can_edit).

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    project_idUUIDFalseFalse—projects.id / CASCADE
    suite_nameVARCHAR(500)TrueFalse—
    starts_onDATEFalseFalse—
    ends_onDATEFalseFalse—
    created_by_idUUIDTrueFalse—users.id / SET NULL
    created_atDATETIMEFalseFalseserver=now()
    updated_atDATETIMEFalseFalseserver=now(); onupdate=now()
    textVARCHAR(280)FalseFalse—

    Constraints and indexes:

    • CheckConstraint ck_timeline_notes_range: ends_on >= starts_on
    • CheckConstraint ck_timeline_notes_text: char_length(text) BETWEEN 1 AND 280
    • ForeignKeyConstraint unnamed: created_by_id
    • ForeignKeyConstraint unnamed: project_id
    • PrimaryKeyConstraint unnamed: id
    • Index ix_timeline_notes_project_starts (unique=False): timeline_notes.project_id, timeline_notes.starts_on; options {}

    backend/app/models/postgres.py:4837

    Email invite token for onboarding new users.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    emailVARCHAR(255)FalseFalse—
    roleVARCHAR(20)FalseFalseapplication=UserRole.QA_ENGINEER
    tokenVARCHAR(64)TrueFalse—
    token_hashVARCHAR(64)TrueFalse—
    invited_by_idUUIDTrueFalse—users.id / SET NULL
    is_usedBOOLEANFalseFalseapplication=False
    used_atDATETIMETrueFalse—
    revoked_atDATETIMETrueFalse—
    accepted_user_idUUIDTrueFalse—users.id / SET NULL
    project_membershipsJSONBTrueFalse—
    expires_atDATETIMEFalseFalse—
    created_atDATETIMEFalseFalseserver=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: invited_by_id
    • ForeignKeyConstraint fk_user_invitations_accepted_user: accepted_user_id
    • PrimaryKeyConstraint unnamed: id
    • UniqueConstraint unnamed: token
    • Index ix_user_invitations_email (unique=False): user_invitations.email; options {}
    • Index uq_user_invitations_token_hash (unique=True): user_invitations.token_hash; options {}

    backend/app/models/postgres.py:5569

    A UI prompt this user has dismissed (migration 0145).

    Generic on purpose: one row per (user_id, dismissal_key). The first consumer is the retention-activation nudge, which exists because ProjectRetentionPolicy.enabled defaults to False and nothing in the product ever asked an operator to turn retention on — the feature shipped present, discoverable, and inert.

    Per-USER, not per-browser. localStorage was the cheaper option and is wrong here: the same operator on a second machine would be re-prompted to enable a destructive background job they had already declined.

    Writing twice is safe — dismissing twice is a no-op, so the endpoint can be retried safely and a double-clicked button cannot raise.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    user_idUUIDFalseFalse—users.id / CASCADE
    dismissal_keyVARCHAR(100)FalseFalse—
    dismissed_atDATETIMEFalseFalseserver=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: user_id
    • PrimaryKeyConstraint unnamed: id
    • UniqueConstraint uq_user_ui_dismissal: user_id, dismissal_key
    • Index ix_uuid_user (unique=False): user_ui_dismissals.user_id; options {}

    backend/app/models/postgres.py:177

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    emailVARCHAR(255)FalseFalse—
    usernameVARCHAR(100)FalseFalse—
    full_nameVARCHAR(255)TrueFalse—
    hashed_passwordVARCHAR(255)FalseFalse—
    roleVARCHAR(20)FalseFalseapplication=VIEWER
    is_activeBOOLEANFalseFalseapplication=True
    is_service_accountBOOLEANFalseFalseserver=false; application=False
    is_syntheticBOOLEANFalseFalseserver=false; application=False
    must_change_passwordBOOLEANFalseFalseapplication=False
    avatar_colorVARCHAR(20)TrueFalse—
    mfa_enabledBOOLEANFalseFalseapplication=False
    mfa_enrolled_atDATETIMETrueFalse—
    mfa_last_used_stepBIGINTTrueFalse—
    last_login_atDATETIMETrueFalse—
    failed_login_attemptsINTEGERFalseFalseapplication=0
    locked_untilDATETIMETrueFalse—
    created_atDATETIMEFalseFalseserver=now()
    updated_atDATETIMEFalseFalseserver=now(); onupdate=now()
    erased_atDATETIMETrueFalse—

    Constraints and indexes:

    • PrimaryKeyConstraint unnamed: id
    • Index ix_users_email (unique=True): users.email; options {}
    • Index ix_users_erased_at (unique=False): users.erased_at; options {"postgresql_where": "erased_at IS NOT NULL"}
    • Index ix_users_username (unique=True): users.username; options {}

    backend/app/models/postgres.py:7704

    Per-project tunable assumptions for the engineer-hours-saved model (migration 0112).

    One row per project; a missing row resolves to the defaults in value_metrics_service.EffectiveAssumptions (triage 20 min/failure, blocked-run wait 30 min, defect filing 15 min) — no project-creation hook needed (same pattern as QuarantineLifecyclePolicy).

    Every defaulted column carries a matching server_default so the migration DDL and the ORM cannot drift (the #433 lesson).

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    project_idUUIDFalseFalse—projects.id / CASCADE
    triage_minutes_per_failureFLOATFalseFalseserver=20.0; application=20.0
    blocked_run_wait_minutesFLOATFalseFalseserver=30.0; application=30.0
    defect_filing_minutesFLOATFalseFalseserver=15.0; application=15.0
    created_atDATETIMEFalseFalseserver=now()
    updated_atDATETIMEFalseFalseserver=now(); onupdate=now()
    updated_by_user_idUUIDTrueFalse—users.id / SET NULL

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: project_id
    • ForeignKeyConstraint unnamed: updated_by_user_id
    • PrimaryKeyConstraint unnamed: id
    • UniqueConstraint unnamed: project_id

    backend/app/models/postgres.py:7189

    Audit trail for every webhook delivery attempt.

    One row per (subscription, event emission). Retries update the same row — attempt_count is incremented and the final outcome lands in status. Rows older than 30 days are pruned by a celery beat task to keep the table small.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    subscription_idUUIDFalseFalse—webhook_subscriptions.id / CASCADE
    run_idUUIDTrueFalse—test_runs.id / SET NULL
    event_typeVARCHAR(64)FalseFalse—
    event_payloadJSONBTrueFalse—
    delivery_keyVARCHAR(64)TrueFalse—
    statusVARCHAR(20)FalseFalseapplication=PENDING
    attempt_countINTEGERFalseFalseapplication=0
    dispatch_attemptsINTEGERFalseFalseserver=0; application=0
    dispatch_failuresINTEGERFalseFalseserver=0; application=0
    next_dispatch_atDATETIMETrueFalse—
    dispatch_lease_expires_atDATETIMETrueFalse—
    dispatch_tokenUUIDTrueFalse—
    http_statusINTEGERTrueFalse—
    response_previewVARCHAR(2000)TrueFalse—
    errorTEXTTrueFalse—
    delivered_atDATETIMETrueFalse—
    created_atDATETIMEFalseFalseserver=now()

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: run_id
    • ForeignKeyConstraint unnamed: subscription_id
    • PrimaryKeyConstraint unnamed: id
    • Index ix_webhook_delivery_dispatch_due (unique=False): webhook_deliveries.status, webhook_deliveries.next_dispatch_at; options {}
    • Index ix_webhook_delivery_run_id (unique=False): webhook_deliveries.run_id; options {}
    • Index ix_webhook_delivery_status (unique=False): webhook_deliveries.status, webhook_deliveries.created_at; options {}
    • Index ix_webhook_delivery_sub_created (unique=False): webhook_deliveries.subscription_id, webhook_deliveries.created_at; options {}
    • Index uq_webhook_delivery_delivery_key (unique=True): webhook_deliveries.delivery_key; options {}

    backend/app/models/postgres.py:7130

    Customer-managed outbound webhook subscription.

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    project_idUUIDFalseFalse—projects.id / CASCADE
    nameVARCHAR(255)FalseFalse—
    target_urlVARCHAR(1000)FalseFalse—
    eventsJSONBFalseFalseapplication=list
    enabledBOOLEANFalseFalseapplication=True
    has_secretBOOLEANFalseFalseapplication=False
    max_retriesINTEGERFalseFalseapplication=5
    last_delivered_atDATETIMETrueFalse—
    last_failure_atDATETIMETrueFalse—
    last_errorTEXTTrueFalse—
    failure_countINTEGERFalseFalseapplication=0
    total_deliveredINTEGERFalseFalseapplication=0
    consecutive_failuresINTEGERFalseFalseserver=0; application=0
    breaker_thresholdINTEGERFalseFalseserver=10; application=10
    disabled_atDATETIMETrueFalse—
    disabled_reasonVARCHAR(40)TrueFalse—
    created_atDATETIMEFalseFalseserver=now()
    updated_atDATETIMEFalseFalseserver=now(); onupdate=now()
    updated_by_user_idUUIDTrueFalse—users.id / SET NULL

    Constraints and indexes:

    • ForeignKeyConstraint unnamed: project_id
    • ForeignKeyConstraint unnamed: updated_by_user_id
    • PrimaryKeyConstraint unnamed: id
    • Index ix_webhook_sub_enabled (unique=False): webhook_subscriptions.enabled, webhook_subscriptions.project_id; options {}
    • Index ix_webhook_sub_project (unique=False): webhook_subscriptions.project_id; options {}

    backend/app/models/postgres.py:6400

    One immutable-on-publish version of a project workflow (E3.1).

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    project_idUUIDFalseFalse—projects.id / CASCADE
    workflow_idVARCHAR(80)FalseFalse—
    versionINTEGERFalseFalse—
    nameVARCHAR(120)FalseFalse—
    descriptionTEXTTrueFalse—
    baseVARCHAR(16)FalseFalse—
    definitionJSONBFalseFalse—
    statusVARCHAR(16)FalseFalseserver=draft; application=draft
    published_atDATETIMETrueFalse—
    eval_verdictVARCHAR(24)TrueFalse—
    eval_coverageFLOATTrueFalse—
    eval_gate_run_idUUIDTrueFalse—ai_eval_gate_runs.id / SET NULL
    evaluated_atDATETIMETrueFalse—
    eval_regression_acceptedBOOLEANFalseFalseserver=false; application=False
    eval_regression_reasonTEXTTrueFalse—
    eval_regression_accepted_byUUIDTrueFalse—users.id / SET NULL
    eval_regression_accepted_atDATETIMETrueFalse—
    created_byUUIDTrueFalse—users.id / SET NULL
    updated_byUUIDTrueFalse—users.id / SET NULL
    created_atDATETIMEFalseFalseserver=now()
    updated_atDATETIMEFalseFalseserver=now(); onupdate=now()

    Constraints and indexes:

    • CheckConstraint ck_workflow_definitions_base: base IN ('offline', 'deep', 'live')
    • CheckConstraint ck_workflow_definitions_eval_coverage: eval_coverage IS NULL OR (eval_coverage >= 0 AND eval_coverage <= 1)
    • CheckConstraint ck_workflow_definitions_eval_evidence: (eval_verdict IS NULL AND eval_coverage IS NULL AND eval_gate_run_id IS NULL AND evaluated_at IS NULL) OR (eval_verdict IS NOT NULL AND eval_coverage IS NOT NULL AND eval_gate_run_id IS NOT NULL AND evaluated_at IS NOT NULL)
    • CheckConstraint ck_workflow_definitions_eval_verdict: eval_verdict IS NULL OR eval_verdict IN ('pass', 'fail', 'insufficient_samples')
    • CheckConstraint ck_workflow_definitions_published_at: (status = 'published' AND published_at IS NOT NULL) OR (status = 'draft' AND published_at IS NULL)
    • CheckConstraint ck_workflow_definitions_regression_acceptance: (eval_regression_accepted IS FALSE AND eval_regression_reason IS NULL AND eval_regression_accepted_by IS NULL AND eval_regression_accepted_at IS NULL) OR (eval_regression_accepted IS TRUE AND eval_verdict = 'fail' AND length(trim(eval_regression_reason)) > 0 AND eval_regression_accepted_by IS NOT NULL AND eval_regression_accepted_at IS NOT NULL)
    • CheckConstraint ck_workflow_definitions_status: status IN ('draft', 'published')
    • CheckConstraint ck_workflow_definitions_version_positive: version >= 1
    • ForeignKeyConstraint unnamed: created_by
    • ForeignKeyConstraint unnamed: eval_gate_run_id
    • ForeignKeyConstraint unnamed: eval_regression_accepted_by
    • ForeignKeyConstraint unnamed: project_id
    • ForeignKeyConstraint unnamed: updated_by
    • PrimaryKeyConstraint unnamed: id
    • UniqueConstraint uq_workflow_definitions_project_workflow_version: project_id, workflow_id, version
    • Index ix_workflow_definitions_project_status (unique=False): workflow_definitions.project_id, workflow_definitions.status; options {}

    backend/app/models/postgres.py:6532

    A project run output cached for deterministic workflow evaluation (E9.5).

    ColumnSQL typeNullablePKDefaultForeign key / on delete
    idUUIDFalseTrueapplication=uuid4
    project_idUUIDFalseFalse—projects.id / CASCADE
    pipeline_run_idUUIDFalseFalse—agent_pipeline_runs.id / CASCADE
    test_run_idUUIDFalseFalse—test_runs.id / CASCADE
    step_idVARCHAR(80)FalseFalse—
    agent_idVARCHAR(120)FalseFalse—
    prompt_versionVARCHAR(80)FalseFalse—
    input_hashVARCHAR(64)FalseFalse—
    outputJSONBFalseFalse—
    stage_statusVARCHAR(20)FalseFalse—
    degradedBOOLEANFalseFalseserver=false; application=False
    cost_usdFLOATFalseFalseserver=0; application=0.0
    latency_msINTEGERFalseFalseserver=0; application=0
    created_atDATETIMEFalseFalseserver=now()

    Constraints and indexes:

    • CheckConstraint ck_workflow_replay_cost_nonnegative: cost_usd >= 0
    • CheckConstraint ck_workflow_replay_input_hash: input_hash ~ '^[0-9a-f]{64}$'
    • CheckConstraint ck_workflow_replay_latency_nonnegative: latency_ms >= 0
    • ForeignKeyConstraint unnamed: pipeline_run_id
    • ForeignKeyConstraint unnamed: project_id
    • ForeignKeyConstraint unnamed: test_run_id
    • PrimaryKeyConstraint unnamed: id
    • UniqueConstraint uq_workflow_replay_agent_prompt_input: agent_id, prompt_version, input_hash
    • Index ix_workflow_replay_project_run (unique=False): workflow_replay_corpus.project_id, workflow_replay_corpus.pipeline_run_id; options {}

    For how the stores work together and how records move through their stages, see the architecture page.