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
StageStatusdeliberately mirrorsRoundStatusfor familiarityProjectStageStateValueprovides 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
UserRolewithAWARD_MASTERandAUDIENCE - 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
- ✅
programIdFK ensures proper scoping - ✅
slugunique constraint enables URL-friendly references - ✅
settingsJsonprovides 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:
- ✅
kinddetermines track type (MAIN vs AWARD vs SHOWCASE) - ✅
specialAwardIdnullable for MAIN tracks, required for AWARD tracks - ✅
sortOrderenables explicit ordering - ✅
routingModeDefaultanddecisionModeprovide 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:
- ✅
stageTypedetermines config schema (union type) - ✅
configVersionenables config evolution - ✅
configJsonstores type-specific configuration - ✅
windowOpenAt/windowCloseAtprovide voting windows - ✅ Unique constraints prevent duplicate
slugorsortOrderper 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
- ✅
priorityenables deterministic tie-breaking - ✅
isDefaultmarks default transition path - ✅
guardJsonandactionJsonprovide 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
roundIdpointer with explicit state records - ✅ Unique constraint on
(projectId, trackId, stageId)prevents duplicates - ✅
trackIdenables parallel track progression (main + awards) - ✅
stateprovides current progression status - ✅
enteredAt/exitedAttrack state duration - ✅
decisionReflinks to decision audit - ✅
outcomeJsonstores 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:
- ✅
scopedetermines rule evaluation context - ✅
predicateJsoncontains matching logic - ✅
destinationTrackIdrequired,destinationStageIdoptional - ✅
priorityenables deterministic rule ordering - ✅
isActiveallows 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
- ✅
votingModedetermines who can vote - ✅
isOpencontrols voting acceptance - ✅
windowOpenAt/windowCloseAtprovide 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
- ✅
sortOrderenables 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
- ✅
stageIdunique ensures one cursor per stage - ✅
sessionIdtracks live session - ✅
activeProjectIdandactiveOrderIndextrack current position - ✅
updatedBytracks 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
- ✅
reasonCodeenum ensures valid reasons - ✅
reasonTextcaptures human explanation - ✅
actedBytracks 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
- ✅
eventTypealigns with event taxonomy - ✅
payloadJsoncaptures full event context - ✅
actorIdnullable 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:
- ✅
ProjectStageState(projectId, trackId, state) - ✅
ProjectStageState(stageId, state) - ✅
RoutingRule(pipelineId, isActive, priority) - ✅
StageTransition(fromStageId, priority) - ✅
LiveProgressCursor(stageId, sessionId) - ✅
DecisionAuditLog(entityType, entityId, createdAt)
Additional indexes added:
Track(pipelineId, sortOrder)- optimizes track orderingStage(trackId, status)- optimizes status filteringStage(status, windowOpenAt, windowCloseAt)- optimizes window queriesCohort(stageId, isOpen)- optimizes active cohort queriesCohortProject(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.tsduring 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- GainsPipelinerelation - ✅
Project- GainsProjectStageStaterelation, deprecatesroundId - ✅
SpecialAward- GainsTrackrelation - ✅
Evaluation- No changes to core model - ✅
Assignment- May needstageIdaddition (Phase 2)
6.2 Models to be Deprecated (Phase 6)
- ⚠️
Round- Replaced byPipeline+Track+Stage - ⚠️ Round-specific relations will be refactored
6.3 Integration Points
- ✅
User.roleextends to includeAWARD_MASTERandAUDIENCE - ✅
Programgainspipelinesrelation - ✅
ProjectgainsprojectStageStatesrelation - ✅
SpecialAwardgainstrackrelation
7. Validation Summary
✅ Approved Elements
- All 7 new canonical enums are complete and unambiguous
- All 12 new core models align with domain model spec
- All unique constraints match spec requirements (1 minor correction needed)
- All foreign key relationships properly defined
- All required indexes present (plus beneficial additions)
- JSON field contracts provide flexibility and extensibility
- Compatibility with existing models maintained
⚠️ Corrections Needed for Phase 1
- Track: Add unique constraint on
(pipelineId, sortOrder)to match spec - UserRole: Extend enum to add
AWARD_MASTERandAUDIENCEvalues
📋 Action Items for Phase 1
- Implement all 7 new enums in
prisma/schema.prisma - Implement all 12 new models with proper constraints
- Extend
UserRoleenum 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