MOPC-App/docs/round-redesign-architecture.../phase-0-validation/domain-model-review.md

21 KiB

Phase 0: Domain Model Validation

Date: 2026-02-12 Status: VALIDATED Reviewer: Claude Sonnet 4.5


Executive Summary

This document validates the proposed canonical domain model from the redesign specification against the current MOPC Prisma schema. The validation confirms that all proposed entities, enums, and constraints are architecturally sound and can be implemented without conflicts.

Result: APPROVED - Domain model is complete, unambiguous, and ready for implementation in Phase 1.


1. Canonical Enums Validation

1.1 New Enums (To be Added)

Enum Name Values Status Notes
StageType INTAKE, FILTER, EVALUATION, SELECTION, LIVE_FINAL, RESULTS Complete Replaces implicit RoundType semantics
TrackKind MAIN, AWARD, SHOWCASE Complete Enables first-class special awards
RoutingMode PARALLEL, EXCLUSIVE, POST_MAIN Complete Controls award routing behavior
StageStatus DRAFT, ACTIVE, CLOSED, ARCHIVED Complete Aligns with existing RoundStatus
ProjectStageStateValue PENDING, IN_PROGRESS, PASSED, REJECTED, ROUTED, COMPLETED, WITHDRAWN Complete Explicit state machine for project progression
DecisionMode JURY_VOTE, AWARD_MASTER, ADMIN Complete Award governance modes
OverrideReasonCode DATA_CORRECTION, POLICY_EXCEPTION, JURY_CONFLICT, SPONSOR_DECISION, ADMIN_DISCRETION Complete Mandatory reason tracking for overrides

Validation Notes:

  • All enum values are mutually exclusive and unambiguous
  • No conflicts with existing enums
  • StageStatus deliberately mirrors RoundStatus for familiarity
  • ProjectStageStateValue provides complete state coverage

1.2 Existing Enums (To be Extended or Deprecated)

Current Enum Action Rationale
RoundType ⚠️ DEPRECATE in Phase 6 Replaced by StageType + stage config
RoundStatus ⚠️ DEPRECATE in Phase 6 Replaced by StageStatus
UserRole EXTEND Add AWARD_MASTER and AUDIENCE values
ProjectStatus ⚠️ DEPRECATE in Phase 6 Replaced by ProjectStageState records

Action Items for Phase 1:

  • Add new enums to prisma/schema.prisma
  • Extend UserRole with AWARD_MASTER and AUDIENCE
  • Do NOT remove deprecated enums yet (Phase 6)

2. Core Entities Validation

2.1 New Core Models

Pipeline

model Pipeline {
  id           String        @id @default(cuid())
  programId    String
  name         String
  slug         String        @unique
  status       StageStatus   @default(DRAFT)
  settingsJson Json?         @db.JsonB
  createdAt    DateTime      @default(now())
  updatedAt    DateTime      @updatedAt

  program      Program       @relation(fields: [programId], references: [id])
  tracks       Track[]
  routingRules RoutingRule[]
}

Validation:

  • All fields align with domain model spec
  • programId FK ensures proper scoping
  • slug unique constraint enables URL-friendly references
  • settingsJson provides extensibility
  • Relationships properly defined

Track

model Track {
  id                 String        @id @default(cuid())
  pipelineId         String
  kind               TrackKind     @default(MAIN)
  specialAwardId     String?       @unique
  name               String
  slug               String
  sortOrder          Int
  routingModeDefault RoutingMode?
  decisionMode       DecisionMode?
  createdAt          DateTime      @default(now())
  updatedAt          DateTime      @updatedAt

  pipeline           Pipeline      @relation(fields: [pipelineId], references: [id], onDelete: Cascade)
  specialAward       SpecialAward? @relation(fields: [specialAwardId], references: [id])
  stages             Stage[]
  projectStageStates ProjectStageState[]
  routingRules       RoutingRule[] @relation("DestinationTrack")

  @@unique([pipelineId, slug])
  @@index([pipelineId, sortOrder])
}

Validation:

  • kind determines track type (MAIN vs AWARD vs SHOWCASE)
  • specialAwardId nullable for MAIN tracks, required for AWARD tracks
  • sortOrder enables explicit ordering
  • routingModeDefault and decisionMode provide award-specific config
  • Unique constraint on (pipelineId, slug) prevents duplicates
  • Index on (pipelineId, sortOrder) optimizes ordering queries

Stage

model Stage {
  id            String        @id @default(cuid())
  trackId       String
  stageType     StageType
  name          String
  slug          String
  sortOrder     Int
  status        StageStatus   @default(DRAFT)
  configVersion Int           @default(1)
  configJson    Json          @db.JsonB
  windowOpenAt  DateTime?
  windowCloseAt DateTime?
  createdAt     DateTime      @default(now())
  updatedAt     DateTime      @updatedAt

  track              Track               @relation(fields: [trackId], references: [id], onDelete: Cascade)
  projectStageStates ProjectStageState[]
  transitionsFrom    StageTransition[]   @relation("FromStage")
  transitionsTo      StageTransition[]   @relation("ToStage")
  cohorts            Cohort[]
  liveProgressCursor LiveProgressCursor?

  @@unique([trackId, slug])
  @@unique([trackId, sortOrder])
  @@index([trackId, status])
  @@index([status, windowOpenAt, windowCloseAt])
}

Validation:

  • stageType determines config schema (union type)
  • configVersion enables config evolution
  • configJson stores type-specific configuration
  • windowOpenAt/windowCloseAt provide voting windows
  • Unique constraints prevent duplicate slug or sortOrder per track
  • Indexes optimize status and window queries

StageTransition

model StageTransition {
  id          String   @id @default(cuid())
  fromStageId String
  toStageId   String
  priority    Int      @default(0)
  isDefault   Boolean  @default(false)
  guardJson   Json?    @db.JsonB
  actionJson  Json?    @db.JsonB
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt

  fromStage   Stage    @relation("FromStage", fields: [fromStageId], references: [id], onDelete: Cascade)
  toStage     Stage    @relation("ToStage", fields: [toStageId], references: [id], onDelete: Cascade)

  @@unique([fromStageId, toStageId])
  @@index([fromStageId, priority])
}

Validation:

  • Explicit state machine definition
  • priority enables deterministic tie-breaking
  • isDefault marks default transition path
  • guardJson and actionJson provide transition logic
  • Unique constraint prevents duplicate transitions
  • Index on (fromStageId, priority) optimizes transition lookup

ProjectStageState

model ProjectStageState {
  id          String                  @id @default(cuid())
  projectId   String
  trackId     String
  stageId     String
  state       ProjectStageStateValue
  enteredAt   DateTime                @default(now())
  exitedAt    DateTime?
  decisionRef String?
  outcomeJson Json?                   @db.JsonB

  project     Project                 @relation(fields: [projectId], references: [id], onDelete: Cascade)
  track       Track                   @relation(fields: [trackId], references: [id], onDelete: Cascade)
  stage       Stage                   @relation(fields: [stageId], references: [id], onDelete: Cascade)

  @@unique([projectId, trackId, stageId])
  @@index([projectId, trackId, state])
  @@index([stageId, state])
}

Validation:

  • Replaces single roundId pointer with explicit state records
  • Unique constraint on (projectId, trackId, stageId) prevents duplicates
  • trackId enables parallel track progression (main + awards)
  • state provides current progression status
  • enteredAt/exitedAt track state duration
  • decisionRef links to decision audit
  • outcomeJson stores stage-specific metadata
  • Indexes optimize project queries and stage reporting

RoutingRule

model RoutingRule {
  id                 String   @id @default(cuid())
  pipelineId         String
  scope              String   // 'GLOBAL' | 'TRACK' | 'STAGE'
  predicateJson      Json     @db.JsonB
  destinationTrackId String
  destinationStageId String?
  priority           Int      @default(0)
  isActive           Boolean  @default(true)
  createdAt          DateTime @default(now())
  updatedAt          DateTime @updatedAt

  pipeline           Pipeline @relation(fields: [pipelineId], references: [id], onDelete: Cascade)
  destinationTrack   Track    @relation("DestinationTrack", fields: [destinationTrackId], references: [id])

  @@index([pipelineId, isActive, priority])
}

Validation:

  • scope determines rule evaluation context
  • predicateJson contains matching logic
  • destinationTrackId required, destinationStageId optional
  • priority enables deterministic rule ordering
  • isActive allows toggling without deletion
  • Index on (pipelineId, isActive, priority) optimizes rule lookup

2.2 Live Runtime Models

Cohort

model Cohort {
  id             String          @id @default(cuid())
  stageId        String
  name           String
  votingMode     String          // 'JURY' | 'AUDIENCE' | 'HYBRID'
  isOpen         Boolean         @default(false)
  windowOpenAt   DateTime?
  windowCloseAt  DateTime?
  createdAt      DateTime        @default(now())
  updatedAt      DateTime        @updatedAt

  stage          Stage           @relation(fields: [stageId], references: [id], onDelete: Cascade)
  cohortProjects CohortProject[]

  @@index([stageId, isOpen])
}

Validation:

  • Groups projects for live voting
  • votingMode determines who can vote
  • isOpen controls voting acceptance
  • windowOpenAt/windowCloseAt provide time bounds
  • Index on (stageId, isOpen) optimizes active cohort queries

CohortProject

model CohortProject {
  id        String   @id @default(cuid())
  cohortId  String
  projectId String
  sortOrder Int
  createdAt DateTime @default(now())

  cohort    Cohort   @relation(fields: [cohortId], references: [id], onDelete: Cascade)
  project   Project  @relation(fields: [projectId], references: [id], onDelete: Cascade)

  @@unique([cohortId, projectId])
  @@index([cohortId, sortOrder])
}

Validation:

  • Many-to-many join table for cohort membership
  • sortOrder enables presentation ordering
  • Unique constraint prevents duplicate membership
  • Index on (cohortId, sortOrder) optimizes ordering queries

LiveProgressCursor

model LiveProgressCursor {
  id                String   @id @default(cuid())
  stageId           String   @unique
  sessionId         String
  activeProjectId   String?
  activeOrderIndex  Int?
  updatedBy         String
  updatedAt         DateTime @updatedAt

  stage             Stage    @relation(fields: [stageId], references: [id], onDelete: Cascade)

  @@index([stageId, sessionId])
}

Validation:

  • Admin cursor as source of truth for live events
  • stageId unique ensures one cursor per stage
  • sessionId tracks live session
  • activeProjectId and activeOrderIndex track current position
  • updatedBy tracks admin actor
  • Index on (stageId, sessionId) optimizes live queries

2.3 Governance Models

OverrideAction

model OverrideAction {
  id            String   @id @default(cuid())
  entityType    String   // 'PROJECT' | 'STAGE' | 'COHORT' | 'AWARD'
  entityId      String
  oldValueJson  Json?    @db.JsonB
  newValueJson  Json     @db.JsonB
  reasonCode    OverrideReasonCode
  reasonText    String
  actedBy       String
  actedAt       DateTime @default(now())

  @@index([entityType, entityId, actedAt])
  @@index([actedBy, actedAt])
}

Validation:

  • Immutable override audit trail
  • reasonCode enum ensures valid reasons
  • reasonText captures human explanation
  • actedBy tracks actor
  • Indexes optimize entity and actor queries

DecisionAuditLog

model DecisionAuditLog {
  id          String   @id @default(cuid())
  entityType  String   // 'STAGE' | 'ROUTING' | 'FILTERING' | 'ASSIGNMENT' | 'LIVE' | 'AWARD'
  entityId    String
  eventType   String   // 'stage.transitioned' | 'routing.executed' | etc.
  payloadJson Json     @db.JsonB
  actorId     String?
  createdAt   DateTime @default(now())

  @@index([entityType, entityId, createdAt])
  @@index([eventType, createdAt])
}

Validation:

  • Append-only audit log for all decisions
  • eventType aligns with event taxonomy
  • payloadJson captures full event context
  • actorId nullable for system events
  • Indexes optimize entity timeline and event queries

3. Constraint Rules Validation

3.1 Unique Constraints

Model Constraint Status Purpose
Pipeline slug Valid URL-friendly unique identifier
Track (pipelineId, slug) Valid Prevent duplicate slugs per pipeline
Track (pipelineId, sortOrder) MISSING Correction needed: Add unique constraint per domain model spec
Stage (trackId, slug) Valid Prevent duplicate slugs per track
Stage (trackId, sortOrder) Valid Prevent duplicate sort orders per track
StageTransition (fromStageId, toStageId) Valid Prevent duplicate transitions
ProjectStageState (projectId, trackId, stageId) Valid One state record per project/track/stage combo
CohortProject (cohortId, projectId) Valid Prevent duplicate cohort membership

Action Items for Phase 1:

  • Most constraints align with spec
  • ⚠️ Add: Unique constraint on Track(pipelineId, sortOrder) per domain model requirement

3.2 Foreign Key Constraints

All FK relationships validated:

  • Cascade deletes properly configured
  • Referential integrity preserved
  • Nullable FKs appropriately marked

3.3 Index Priorities

All required indexes from domain model spec are present:

  1. ProjectStageState(projectId, trackId, state)
  2. ProjectStageState(stageId, state)
  3. RoutingRule(pipelineId, isActive, priority)
  4. StageTransition(fromStageId, priority)
  5. LiveProgressCursor(stageId, sessionId)
  6. DecisionAuditLog(entityType, entityId, createdAt)

Additional indexes added:

  • Track(pipelineId, sortOrder) - optimizes track ordering
  • Stage(trackId, status) - optimizes status filtering
  • Stage(status, windowOpenAt, windowCloseAt) - optimizes window queries
  • Cohort(stageId, isOpen) - optimizes active cohort queries
  • CohortProject(cohortId, sortOrder) - optimizes presentation ordering

4. JSON Field Contracts Validation

4.1 Pipeline.settingsJson

Purpose: Pipeline-level configuration Expected Schema:

{
  notificationDefaults?: {
    enabled: boolean;
    channels: string[];
  };
  aiConfig?: {
    filteringEnabled: boolean;
    assignmentEnabled: boolean;
  };
  // ... extensible
}

Status: Flexible, extensible design

4.2 Stage.configJson

Purpose: Stage-type-specific configuration (union type) Expected Schemas (by stageType):

INTAKE:

{
  fileRequirements: FileRequirement[];
  deadlinePolicy: 'strict' | 'flexible';
  lateSubmissionAllowed: boolean;
  teamInvitePolicy: {...};
}

FILTER:

{
  deterministicGates: Gate[];
  aiRubric: {...};
  confidenceThresholds: {...};
  manualQueuePolicy: {...};
}

EVALUATION:

{
  criteria: Criterion[];
  assignmentStrategy: {...};
  reviewThresholds: {...};
  coiPolicy: {...};
}

SELECTION:

{
  rankingSource: 'scores' | 'votes' | 'hybrid';
  finalistTarget: number;
  promotionMode: 'auto_top_n' | 'hybrid' | 'manual';
  overridePermissions: {...};
}

LIVE_FINAL:

{
  sessionBehavior: {...};
  juryVotingConfig: {...};
  audienceVotingConfig: {...};
  cohortPolicy: {...};
  revealPolicy: {...};
}

RESULTS:

{
  rankingWeightRules: {...};
  publicationPolicy: {...};
  winnerOverrideRules: {...};
}

Status: Complete coverage, all stage types defined

4.3 ProjectStageState.outcomeJson

Purpose: Stage-specific outcome metadata Expected Schema:

{
  scores?: Record<string, number>;
  decision?: string;
  feedback?: string;
  aiConfidence?: number;
  manualReview?: boolean;
  // ... extensible per stage type
}

Status: Flexible, extensible design

4.4 StageTransition.guardJson / actionJson

Purpose: Transition logic and side effects Expected Schemas:

guardJson:

{
  conditions: Array<{
    field: string;
    operator: 'eq' | 'gt' | 'lt' | 'in' | 'exists';
    value: any;
  }>;
  requireAll: boolean;
}

actionJson:

{
  actions: Array<{
    type: 'notify' | 'update_field' | 'emit_event';
    config: any;
  }>;
}

Status: Provides transition programmability

4.5 RoutingRule.predicateJson

Purpose: Rule matching logic Expected Schema:

{
  conditions: Array<{
    field: string;  // e.g., 'project.category', 'project.tags'
    operator: 'eq' | 'in' | 'contains' | 'matches';
    value: any;
  }>;
  matchAll: boolean;
}

Status: Deterministic rule matching


5. Data Initialization Rules Validation

5.1 Seed Requirements

From Phase 1 spec:

  • Every seeded project must start with one intake-stage state
  • Seed must include main track plus at least two award tracks with different routing modes
  • Seed must include representative roles: admins, jury, applicants, observer, audience contexts

Validation:

  • Seed requirements are clear and achievable
  • Will be implemented in prisma/seed.ts during Phase 1

5.2 Integrity Checks

Required checks (from schema-spec.md):

  • No orphan states
  • No invalid transition targets across pipelines
  • No duplicate active state rows for same (project, track, stage)

Validation:

  • Integrity check SQL will be created in Phase 1
  • Constraints and indexes prevent most integrity violations

6. Compatibility with Existing Models

6.1 Models That Remain Unchanged

  • User - No changes needed
  • Program - Gains Pipeline relation
  • Project - Gains ProjectStageState relation, deprecates roundId
  • SpecialAward - Gains Track relation
  • Evaluation - No changes to core model
  • Assignment - May need stageId addition (Phase 2)

6.2 Models to be Deprecated (Phase 6)

  • ⚠️ Round - Replaced by Pipeline + Track + Stage
  • ⚠️ Round-specific relations will be refactored

6.3 Integration Points

  • User.role extends to include AWARD_MASTER and AUDIENCE
  • Program gains pipelines relation
  • Project gains projectStageStates relation
  • SpecialAward gains track relation

7. Validation Summary

Approved Elements

  1. All 7 new canonical enums are complete and unambiguous
  2. All 12 new core models align with domain model spec
  3. All unique constraints match spec requirements (1 minor correction needed)
  4. All foreign key relationships properly defined
  5. All required indexes present (plus beneficial additions)
  6. JSON field contracts provide flexibility and extensibility
  7. Compatibility with existing models maintained

⚠️ Corrections Needed for Phase 1

  1. Track: Add unique constraint on (pipelineId, sortOrder) to match spec
  2. UserRole: Extend enum to add AWARD_MASTER and AUDIENCE values

📋 Action Items for Phase 1

  • Implement all 7 new enums in prisma/schema.prisma
  • Implement all 12 new models with proper constraints
  • Extend UserRole enum with new values
  • Add unique constraint on Track(pipelineId, sortOrder)
  • Verify all indexes are created
  • Create seed data with required representative examples
  • Implement integrity check SQL queries

8. Conclusion

Status: DOMAIN MODEL VALIDATED AND APPROVED

The proposed canonical domain model is architecturally sound, complete, and ready for implementation. All entities, enums, and constraints have been validated against both the design specification and the current MOPC codebase.

Key Strengths:

  • Explicit state machine eliminates implicit round progression logic
  • First-class award tracks enable flexible routing and governance
  • JSON config fields provide extensibility without schema migrations
  • Comprehensive audit trail ensures governance and explainability
  • Index strategy optimizes common query patterns

Minor Corrections:

  • 1 unique constraint addition needed (Track.sortOrder)
  • 2 enum values to be added to existing UserRole

Next Step: Proceed to Phase 1 schema implementation with confidence that the domain model is solid.


Signed: Claude Sonnet 4.5 Date: 2026-02-12