name: constraint-preflight description: Pre-flight verification for scheduling constraint development. Use when adding, modifying, or testing constraints to ensure they are properly implemented, exported, registered, and tested before commit.
Constraint Pre-Flight Verification
Prevents the "implemented but not registered" bug where constraints are created, tested, and exported but never added to the ConstraintManager factory methods.
Two Truths: Constraints Affect Prescriptive Layer
CRITICAL CONTEXT:
| Truth Type | Tables | Constraint Role |
|---|---|---|
| Prescriptive | rotation_templates, weekly_patterns, constraints | Define what SHOULD happen |
| Descriptive | half_day_assignments | Record what DID happen |
Constraints are part of the prescriptive layer. They define rules that the solver uses to transform templates into assignments.
┌─────────────────────────────────────────────────────────────────┐
│ PRESCRIPTIVE LAYER │
│ rotation_templates + weekly_patterns + CONSTRAINTS │
└─────────────────────────────────────────────────────────────────┘
│
▼ expansion_service + solver
┌─────────────────────────────────────────────────────────────────┐
│ DESCRIPTIVE LAYER │
│ half_day_assignments │
└─────────────────────────────────────────────────────────────────┘
Impact of constraint changes:
- Adding/modifying constraints changes prescriptive truth
- Changes only take effect after RE-GENERATING the schedule
- Existing
half_day_assignmentswon't auto-update - you must regenerate
Testing implication:
- Test constraints against template expectations (prescriptive)
- Verify generated assignments reflect constraint behavior (descriptive)
When This Skill Activates
- When creating new scheduling constraints
- When modifying existing constraints
- Before committing constraint-related changes
- When the user asks to verify constraint registration
- After adding constraints to
__init__.pyexports
The Constraint Gap Problem
What Goes Wrong
A common failure mode in constraint development:
1. Create constraint class in backend/app/scheduling/constraints/*.py ✓
2. Write tests for constraint logic ✓
3. Export constraint in __init__.py ✓
4. Tests pass locally ✓
5. ⚠️ FORGET to register in ConstraintManager.create_default() ✗
6. Commit and push ✓
7. Schedule generation doesn't use the constraint! 💥
Why It Happens
- Tests verify constraint logic in isolation
- Tests don't verify the constraint is actually used by the scheduler
- Manual verification of "registered at line X" is error-prone
- No CI check catches unregistered constraints
Pre-Flight Verification Script
Run this before committing any constraint changes:
cd /home/user/Autonomous-Assignment-Program-Manager/backend
python ../scripts/verify_constraints.py
What It Checks
- Registration - All exported constraints are in
ConstraintManager.create_default() - Weight Hierarchy - Soft constraint weights follow documented order
- Manager Consistency - Constraints in both
create_default()andcreate_resilience_aware()
Expected Output
============================================================
CONSTRAINT PRE-FLIGHT VERIFICATION
============================================================
This script verifies constraint implementation completeness.
Run this before committing constraint changes.
============================================================
CONSTRAINT REGISTRATION VERIFICATION
============================================================
Registered constraints (23 total):
- 1in7Rule: ENABLED
- 80HourRule: ENABLED
- Availability: ENABLED
- CallSpacing: ENABLED weight=8.0
- ClinicCapacity: ENABLED
...
Block 10 Constraint Check:
[OK] CallSpacingConstraint
[OK] SundayCallEquityConstraint
[OK] TuesdayCallPreferenceConstraint
[OK] WeekdayCallEquityConstraint
[OK] ResidentInpatientHeadcountConstraint
[OK] PostFMITSundayBlockingConstraint
============================================================
WEIGHT HIERARCHY VERIFICATION
============================================================
Call equity weight hierarchy:
[OK] SundayCallEquity: weight=10.0
[OK] CallSpacing: weight=8.0
[OK] WeekdayCallEquity: weight=5.0
[OK] TuesdayCallPreference: weight=2.0
============================================================
MANAGER CONSISTENCY VERIFICATION
============================================================
Block 10 constraints in both managers:
[OK] ResidentInpatientHeadcount
[OK] PostFMITSundayBlocking
[OK] SundayCallEquity
[OK] CallSpacing
[OK] WeekdayCallEquity
[OK] TuesdayCallPreference
============================================================
SUMMARY
============================================================
Registration: PASS
Weight Hierarchy: PASS
Manager Consistency: PASS
[SUCCESS] All verifications passed!
Constraint Development Checklist
When creating a new constraint:
Step 1: Implement Constraint Class
# backend/app/scheduling/constraints/my_constraint.py
class MyNewConstraint(SoftConstraint):
"""
Docstring explaining the constraint's purpose.
"""
def __init__(self, weight: float = 5.0) -> None:
super().__init__(
name="MyNewConstraint",
constraint_type=ConstraintType.EQUITY,
weight=weight,
priority=ConstraintPriority.MEDIUM,
)
def add_to_cpsat(self, model, variables, context) -> None:
# CP-SAT implementation
pass
def add_to_pulp(self, model, variables, context) -> None:
# PuLP implementation
pass
def validate(self, assignments, context) -> ConstraintResult:
# Validation implementation
pass
Step 2: Export in __init__.py
# backend/app/scheduling/constraints/__init__.py
from .my_constraint import MyNewConstraint
__all__ = [
# ... existing exports ...
"MyNewConstraint",
]
Step 3: Register in Manager (CRITICAL!)
# backend/app/scheduling/constraints/manager.py
from .my_constraint import MyNewConstraint
class ConstraintManager:
@classmethod
def create_default(cls) -> "ConstraintManager":
manager = cls()
# ... existing constraints ...
manager.add(MyNewConstraint(weight=5.0)) # ADD THIS!
return manager
@classmethod
def create_resilience_aware(cls, ...) -> "ConstraintManager":
manager = cls()
# ... existing constraints ...
manager.add(MyNewConstraint(weight=5.0)) # ADD THIS TOO!
return manager
Step 4: Write Tests
# backend/tests/test_my_constraint.py
from app.scheduling.constraints import MyNewConstraint, ConstraintManager
class TestMyNewConstraint:
def test_constraint_initialization(self):
constraint = MyNewConstraint()
assert constraint.name == "MyNewConstraint"
assert constraint.weight == 5.0
def test_constraint_registered_in_manager(self):
"""CRITICAL: Verify constraint is actually used!"""
manager = ConstraintManager.create_default()
registered_types = {type(c) for c in manager.constraints}
assert MyNewConstraint in registered_types
Step 5: Run Pre-Flight Verification
cd backend
python ../scripts/verify_constraints.py
Step 6: Commit Only If All Pass
git add .
git commit -m "feat: add MyNewConstraint for [purpose]"
Test Coverage
The test_constraint_registration.py file provides automated CI coverage:
# Key tests that prevent the registration gap:
class TestConstraintRegistration:
def test_block10_hard_constraints_in_default_manager(self):
"""Verify hard constraints are registered."""
def test_block10_soft_constraints_in_default_manager(self):
"""Verify soft constraints are registered."""
def test_call_equity_weight_hierarchy(self):
"""Verify weights follow: Sunday > Spacing > Weekday > Tuesday."""
class TestConstraintExportIntegrity:
def test_all_call_equity_exports_registered(self):
"""All exported classes must be in manager."""
def test_inpatient_constraint_registered(self):
"""ResidentInpatientHeadcountConstraint is registered."""
Quick Commands
# Run pre-flight verification
cd backend && python ../scripts/verify_constraints.py
# Run constraint registration tests only
cd backend && pytest tests/test_constraint_registration.py -v
# Run all constraint tests
cd backend && pytest tests/test_*constraint*.py -v
# Check manager.py for registrations
grep -n "manager.add" backend/app/scheduling/constraints/manager.py
Key Files
| File | Purpose |
|---|---|
scripts/verify_constraints.py | Pre-flight verification script |
backend/tests/test_constraint_registration.py | CI tests for registration |
backend/app/scheduling/constraints/manager.py | Where constraints are registered |
backend/app/scheduling/constraints/__init__.py | Where constraints are exported |
Weight Hierarchy Reference
For call equity constraints, follow this hierarchy (highest impact first):
| Constraint | Weight | Rationale |
|---|---|---|
| SundayCallEquity | 10.0 | Worst call day, highest priority |
| CallSpacing | 8.0 | Burnout prevention |
| WeekdayCallEquity | 5.0 | Balance Mon-Thu calls |
| TuesdayCallPreference | 2.0 | Academic scheduling preference |
| DeptChiefWednesdayPreference | 1.0 | Personal preference (lowest) |
Workflow Diagram
┌──────────────────────────────────────────────────────────────────┐
│ CONSTRAINT PRE-FLIGHT WORKFLOW │
├──────────────────────────────────────────────────────────────────┤
│ │
│ STEP 1: Implement Constraint Class │
│ ┌────────────────────────────────────────────────┐ │
│ │ Create class in constraints/*.py │ │
│ │ Implement: __init__, add_to_cpsat, │ │
│ │ add_to_pulp, validate │ │
│ └────────────────────────────────────────────────┘ │
│ ↓ │
│ STEP 2: Export in __init__.py │
│ ┌────────────────────────────────────────────────┐ │
│ │ Add import statement │ │
│ │ Add to __all__ list │ │
│ └────────────────────────────────────────────────┘ │
│ ↓ │
│ STEP 3: Register in ConstraintManager (CRITICAL!) │
│ ┌────────────────────────────────────────────────┐ │
│ │ Import in manager.py │ │
│ │ Add to create_default() │ │
│ │ Add to create_resilience_aware() │ │
│ │ ⚠️ MUST BE IN BOTH FACTORY METHODS │ │
│ └────────────────────────────────────────────────┘ │
│ ↓ │
│ STEP 4: Write Tests │
│ ┌────────────────────────────────────────────────┐ │
│ │ Unit tests for constraint logic │ │
│ │ Registration test in manager │ │
│ │ Integration test with scheduler │ │
│ └────────────────────────────────────────────────┘ │
│ ↓ │
│ STEP 5: Run Pre-Flight Verification (MANDATORY) │
│ ┌────────────────────────────────────────────────┐ │
│ │ python ../scripts/verify_constraints.py │ │
│ │ ✓ Registration check │ │
│ │ ✓ Weight hierarchy check │ │
│ │ ✓ Manager consistency check │ │
│ └────────────────────────────────────────────────┘ │
│ ↓ │
│ STEP 6: Commit Only If All Pass │
│ ┌────────────────────────────────────────────────┐ │
│ │ All verifications PASS │ │
│ │ Tests PASS │ │
│ │ → Safe to commit │ │
│ └────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────┘
Concrete Usage Example: Adding FridayCallAvoidanceConstraint
Scenario: Program director wants to minimize Friday call assignments to improve weekend coverage.
Complete Implementation Walkthrough
Step 1: Implement the Constraint Class
# backend/app/scheduling/constraints/friday_call_avoidance.py
"""Soft constraint to minimize Friday call assignments."""
from typing import Any, Dict, List
from app.scheduling.constraints.base import SoftConstraint, ConstraintResult
from app.scheduling.constraints.types import ConstraintType, ConstraintPriority
class FridayCallAvoidanceConstraint(SoftConstraint):
"""
Minimize Friday inpatient call assignments to improve weekend coverage.
Clinical rationale: Friday call often extends into Saturday coverage,
reducing resident availability for weekend shifts.
Weight: 3.0 (medium-low priority, below call equity constraints)
"""
def __init__(self, weight: float = 3.0) -> None:
super().__init__(
name="FridayCallAvoidance",
constraint_type=ConstraintType.PREFERENCE,
weight=weight,
priority=ConstraintPriority.MEDIUM,
)
def add_to_cpsat(
self, model: Any, variables: Dict[str, Any], context: Dict[str, Any]
) -> None:
"""Add penalty for Friday call assignments in CP-SAT solver."""
# Implementation details...
pass
def add_to_pulp(
self, model: Any, variables: Dict[str, Any], context: Dict[str, Any]
) -> None:
"""Add penalty for Friday call assignments in PuLP solver."""
# Implementation details...
pass
def validate(
self, assignments: List[Any], context: Dict[str, Any]
) -> ConstraintResult:
"""Validate Friday call distribution."""
# Implementation details...
pass
Step 2: Export in init.py
# backend/app/scheduling/constraints/__init__.py
from .friday_call_avoidance import FridayCallAvoidanceConstraint
__all__ = [
# ... existing exports ...
"CallSpacingConstraint",
"SundayCallEquityConstraint",
"TuesdayCallPreferenceConstraint",
"WeekdayCallEquityConstraint",
# NEW:
"FridayCallAvoidanceConstraint", # ← Add this!
]
Step 3: Register in ConstraintManager (CRITICAL!)
# backend/app/scheduling/constraints/manager.py
from .friday_call_avoidance import FridayCallAvoidanceConstraint
class ConstraintManager:
@classmethod
def create_default(cls) -> "ConstraintManager":
"""Create manager with standard constraint set."""
manager = cls()
# ... existing constraints ...
# Call equity constraints (weight hierarchy matters!)
manager.add(SundayCallEquityConstraint(weight=10.0))
manager.add(CallSpacingConstraint(weight=8.0))
manager.add(WeekdayCallEquityConstraint(weight=5.0))
manager.add(TuesdayCallPreferenceConstraint(weight=2.0))
# NEW: Add Friday avoidance (weight=3.0, below call equity)
manager.add(FridayCallAvoidanceConstraint(weight=3.0)) # ← Add this!
return manager
@classmethod
def create_resilience_aware(
cls,
n1_compliant: bool = True,
utilization_cap: float = 0.8,
defense_level: int = 2,
) -> "ConstraintManager":
"""Create manager with resilience-aware constraints."""
manager = cls()
# ... existing constraints ...
# Call preferences
manager.add(SundayCallEquityConstraint(weight=10.0))
manager.add(CallSpacingConstraint(weight=8.0))
manager.add(WeekdayCallEquityConstraint(weight=5.0))
manager.add(TuesdayCallPreferenceConstraint(weight=2.0))
# NEW: Add here too!
manager.add(FridayCallAvoidanceConstraint(weight=3.0)) # ← And this!
return manager
Step 4: Write Tests
# backend/tests/test_friday_call_avoidance.py
import pytest
from app.scheduling.constraints import (
FridayCallAvoidanceConstraint,
ConstraintManager,
)
class TestFridayCallAvoidanceConstraint:
def test_constraint_initialization(self):
"""Verify constraint initializes with correct values."""
constraint = FridayCallAvoidanceConstraint()
assert constraint.name == "FridayCallAvoidance"
assert constraint.weight == 3.0
assert constraint.constraint_type == ConstraintType.PREFERENCE
def test_custom_weight(self):
"""Test constraint with custom weight."""
constraint = FridayCallAvoidanceConstraint(weight=5.0)
assert constraint.weight == 5.0
def test_constraint_registered_in_default_manager(self):
"""CRITICAL: Verify constraint is in create_default()."""
manager = ConstraintManager.create_default()
registered_types = {type(c) for c in manager.constraints}
assert FridayCallAvoidanceConstraint in registered_types
def test_constraint_registered_in_resilience_manager(self):
"""CRITICAL: Verify constraint is in create_resilience_aware()."""
manager = ConstraintManager.create_resilience_aware()
registered_types = {type(c) for c in manager.constraints}
assert FridayCallAvoidanceConstraint in registered_types
def test_validate_friday_distribution(self):
"""Test validation logic for Friday call assignments."""
# Implementation...
pass
Step 5: Run Pre-Flight Verification
cd /home/user/Autonomous-Assignment-Program-Manager/backend
# Run verification script
python ../scripts/verify_constraints.py
Expected Output:
============================================================
CONSTRAINT PRE-FLIGHT VERIFICATION
============================================================
============================================================
CONSTRAINT REGISTRATION VERIFICATION
============================================================
Registered constraints (24 total): # ← Was 23, now 24
- 1in7Rule: ENABLED
- 80HourRule: ENABLED
- Availability: ENABLED
- CallSpacing: ENABLED weight=8.0
...
- FridayCallAvoidance: ENABLED weight=3.0 # ← NEW!
Block 10 Constraint Check:
[OK] CallSpacingConstraint
[OK] SundayCallEquityConstraint
[OK] TuesdayCallPreferenceConstraint
[OK] WeekdayCallEquityConstraint
[OK] FridayCallAvoidanceConstraint # ← NEW!
============================================================
WEIGHT HIERARCHY VERIFICATION
============================================================
Call preference weight hierarchy:
[OK] SundayCallEquity: weight=10.0
[OK] CallSpacing: weight=8.0
[OK] WeekdayCallEquity: weight=5.0
[OK] FridayCallAvoidance: weight=3.0 # ← NEW! Correctly positioned
[OK] TuesdayCallPreference: weight=2.0
============================================================
MANAGER CONSISTENCY VERIFICATION
============================================================
Block 10 constraints in both managers:
[OK] FridayCallAvoidance # ← In both create_default() and create_resilience_aware()
============================================================
SUMMARY
============================================================
Registration: PASS ✓
Weight Hierarchy: PASS ✓
Manager Consistency: PASS ✓
[SUCCESS] All verifications passed!
Step 6: Run Tests and Commit
# Run tests
pytest tests/test_friday_call_avoidance.py -v
pytest tests/test_constraint_registration.py -v
# All pass? Commit!
git add backend/app/scheduling/constraints/friday_call_avoidance.py
git add backend/app/scheduling/constraints/__init__.py
git add backend/app/scheduling/constraints/manager.py
git add backend/tests/test_friday_call_avoidance.py
git commit -m "$(cat <<'EOF'
feat: add FridayCallAvoidanceConstraint to minimize Friday calls
Implements soft constraint (weight=3.0) to reduce Friday inpatient
call assignments, improving weekend coverage availability.
- Constraint registered in both default and resilience-aware managers
- Weight positioned below call equity (5.0) but above Tuesday preference (2.0)
- Verified with pre-flight check and registration tests
EOF
)"
Failure Mode Handling
Failure Mode 1: Constraint Not Registered
Symptom:
$ python ../scripts/verify_constraints.py
[ERROR] FridayCallAvoidanceConstraint exported but NOT registered in ConstraintManager!
Root cause: Forgot Step 3 (registering in manager.py)
Recovery:
# 1. Add to manager.py
from .friday_call_avoidance import FridayCallAvoidanceConstraint
# 2. Add to BOTH factory methods
def create_default(cls):
manager.add(FridayCallAvoidanceConstraint(weight=3.0)) # Add this!
def create_resilience_aware(cls, ...):
manager.add(FridayCallAvoidanceConstraint(weight=3.0)) # And this!
# 3. Re-run verification
python ../scripts/verify_constraints.py
# Should now PASS
Failure Mode 2: Weight Hierarchy Violation
Symptom:
$ python ../scripts/verify_constraints.py
[WARNING] Weight hierarchy violated:
SundayCallEquity: 10.0
CallSpacing: 8.0
FridayCallAvoidance: 9.0 ← TOO HIGH! Should be < 8.0
WeekdayCallEquity: 5.0
Root cause: Weight set too high, violating call equity hierarchy
Recovery:
# 1. Adjust weight in manager.py
# OLD:
manager.add(FridayCallAvoidanceConstraint(weight=9.0)) # Wrong!
# NEW:
manager.add(FridayCallAvoidanceConstraint(weight=3.0)) # Correct
# 2. Document rationale
# Weight must be < 5.0 (below WeekdayCallEquity)
# but > 2.0 (above TuesdayCallPreference)
# because Friday avoidance is more important than day preference
# 3. Re-run verification
python ../scripts/verify_constraints.py
Failure Mode 3: Missing from One Manager
Symptom:
$ python ../scripts/verify_constraints.py
[ERROR] Manager consistency check FAILED:
FridayCallAvoidance in create_default() ✓
FridayCallAvoidance in create_resilience_aware() ✗ MISSING!
Root cause: Added to create_default() but forgot create_resilience_aware()
Recovery:
# Add to BOTH methods:
@classmethod
def create_resilience_aware(cls, ...):
manager = cls()
# ... other constraints ...
manager.add(FridayCallAvoidanceConstraint(weight=3.0)) # ← Add this!
return manager
Failure Mode 4: Tests Fail Despite Correct Code
Symptom:
$ pytest tests/test_constraint_registration.py -v
FAILED test_constraint_registered_in_default_manager
AssertionError: FridayCallAvoidanceConstraint not in registered types
Root cause: Test ran before constraint was imported in manager
Recovery:
# 1. Verify import exists in manager.py
grep "FridayCallAvoidanceConstraint" backend/app/scheduling/constraints/manager.py
# 2. If missing, add import:
from .friday_call_avoidance import FridayCallAvoidanceConstraint
# 3. Clear Python cache
find . -type d -name __pycache__ -exec rm -rf {} +
find . -type f -name "*.pyc" -delete
# 4. Re-run tests
pytest tests/test_constraint_registration.py -v
Integration with Other Skills
With automated-code-fixer
Scenario: Pre-flight fails, automated-code-fixer can add registration
[constraint-preflight detects missing registration]
→ "FridayCallAvoidanceConstraint exported but not registered"
[Invoke automated-code-fixer]
→ automated-code-fixer adds manager.add() lines to both methods
→ Re-runs verification
→ All checks PASS
→ Commits fix
With test-writer
Workflow:
[User creates new constraint class]
[constraint-preflight activated]
Step 1-3: Implement, export, register
Step 4: Invoke test-writer skill
"Generate comprehensive tests for FridayCallAvoidanceConstraint:
- Initialization tests
- Registration tests
- Validation logic tests
- Weight hierarchy tests"
[test-writer generates test suite]
[constraint-preflight verifies tests cover registration]
With code-review
Pre-commit integration:
[About to commit constraint changes]
[constraint-preflight runs verification]
→ All checks PASS
[Invoke code-review skill]
→ Reviews constraint implementation
→ Checks weight rationale is documented
→ Verifies clinical justification in docstring
→ Approves or requests changes
[Commit only after both skills approve]
With pr-reviewer
PR workflow:
[PR created with new constraint]
[pr-reviewer activated]
→ Detects constraint-related changes
→ Invokes constraint-preflight automatically
→ Runs verification in CI
→ Includes verification output in PR review:
"Constraint Pre-Flight Check: PASS ✓
- Registration verified
- Weight hierarchy correct
- Manager consistency confirmed"
Validation Checklist
Pre-Implementation Checklist
- Constraint purpose is clear and documented
- Clinical/operational rationale defined
- Weight determined relative to existing constraints
- Decided if hard or soft constraint
- Identified which managers need registration
Implementation Checklist
- Constraint class created with all required methods
- Docstring explains purpose and rationale
- Exported in
__init__.py - Imported in
manager.py - Added to
create_default() - Added to
create_resilience_aware()(if applicable) - Weight documented with justification
Testing Checklist
- Unit tests for constraint logic
- Registration test in default manager
- Registration test in resilience manager (if applicable)
- Weight hierarchy test (for soft constraints)
- Integration test with scheduler
- All tests PASS
Verification Checklist
- Run
python ../scripts/verify_constraints.py - Registration check: PASS
- Weight hierarchy check: PASS
- Manager consistency check: PASS
- Run
pytest tests/test_constraint_registration.py -v: ALL PASS - Run full test suite: ALL PASS
Pre-Commit Checklist
- All verification checks PASS
- All tests PASS
- No linting errors
- Weight rationale documented in code
- Clinical justification in docstring
- Ready to commit
Escalation Checklist
Escalate to human if ANY of these are true:
- New constraint category (not equity/preference/workload)
- Weight hierarchy decision needs clinical input
- Affects ACGME compliance rules
- Conflicts with existing constraints
- Requires new solver techniques
- Pre-flight verification fails with unclear errors
Escalation Rules
Escalate to human when:
- Pre-flight verification fails with unclear errors
- Weight hierarchy decisions need clinical input
- New constraint category needs architectural review
- Constraint affects ACGME compliance rules