critical priority high complexity backend pending backend specialist Tier 2

Acceptance Criteria

evaluate() returns an EvaluationResult with a non-null matchedRule when exactly one rule matches all conditions for the given activityMetadata
evaluate() returns EvaluationResult with matchedRule=null and reasonCode=NO_RULES_MATCHED when no rule passes all condition checks
Contact type matching: a rule with contactType='*' (wildcard) matches any contact type in activityMetadata
Contact type matching: a rule with a specific contactType only matches when activityMetadata.contactType equals that value (case-insensitive)
Wellbeing flag condition with semantics='any': rule matches when at least one flag in the rule's wellbeingFlags list is present in activityMetadata.wellbeingFlags
Wellbeing flag condition with semantics='all': rule matches only when every flag in the rule's wellbeingFlags list is present in activityMetadata.wellbeingFlags
Duration range: rule matches when activityMetadata.durationMinutes >= rule.minDurationMinutes and <= rule.maxDurationMinutes; boundaries are inclusive
Delay window: rule matches when the elapsed hours since activityMetadata.activityEndTime fall within [rule.delayWindowMinHours, rule.delayWindowMaxHours]
Cooldown guard: evaluate() queries PromptHistoryRepository for the last prompt sent to the same contactId; if the last prompt was sent within rule.cooldownHours, the rule does NOT match and reasonCode=COOLDOWN_ACTIVE is returned
evaluate() handles an empty rules list by returning EvaluationResult with matchedRule=null and reasonCode=NO_RULES_CONFIGURED
evaluate() is a pure function with respect to all inputs except the PromptHistoryRepository call; side effects are limited to that single async read
All intermediate condition results are captured in EvaluationResult.conditionTrace for debugging and observability

Technical Requirements

frameworks
Flutter
Riverpod
flutter_test
apis
PromptHistoryRepository (internal)
Supabase (via repository abstraction only)
data models
ActivityMetadata
ScenarioRule
PromptHistory
EvaluationResult
EvaluationReasonCode (enum)
ConditionTrace
performance requirements
evaluate() must complete within 200ms including the PromptHistoryRepository async read
Rule iteration must be O(n) in the number of rules with early exit on first match after priority resolution
No database writes during evaluate() — read-only operation
security requirements
activityMetadata.contactId must be validated as belonging to the authenticated user before evaluate() is called — caller's responsibility, document this precondition clearly
PromptHistoryRepository must enforce RLS so that cooldown checks cannot leak prompt history across users
EvaluationResult.conditionTrace must be stripped before sending to analytics or logs to avoid exposing wellbeing flag values

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Implement evaluate() as a method on a ScenarioRuleEngine class that takes PromptHistoryRepository as a constructor-injected dependency. Use the Chain of Responsibility pattern: define a private List where each evaluator checks one condition dimension and returns a ConditionResult. This makes each condition independently testable and the overall algorithm easy to extend. Process conditions in this order: (1) contact type, (2) wellbeing flags, (3) duration range, (4) delay window, (5) cooldown guard — ordering from cheapest to most expensive (the cooldown check requires async I/O and should be last).

EvaluationReasonCode should be an enum with values: MATCHED, NO_RULES_CONFIGURED, NO_RULES_MATCHED, CONTACT_TYPE_MISMATCH, WELLBEING_FLAGS_NOT_MET, DURATION_OUT_OF_RANGE, DELAY_WINDOW_NOT_MET, COOLDOWN_ACTIVE. Store conditionTrace as a List where each entry captures (conditionName, passed, actualValue, expectedValue) for full observability. The PromptHistoryRepository call should use a DateTime parameter rather than DateTime.now() directly — inject a clock abstraction for testability.

Testing Requirements

Write unit tests using flutter_test with a mocked PromptHistoryRepository. Test matrix must cover: contact type exact match, contact type wildcard, contact type mismatch; wellbeing 'any' semantics with one match, all match, no match; wellbeing 'all' semantics with partial match (should not match) and full match; duration at min boundary (inclusive), max boundary (inclusive), below min, above max; delay window within range, before min, after max; cooldown active (recent prompt exists), cooldown inactive (no recent prompt or expired); empty rules list; all conditions satisfied simultaneously. Use parameterized test cases (test.parametrize pattern) to cover boundary values. Target 100% branch coverage on the evaluate() method body.

Add a performance smoke test asserting evaluate() completes in under 200ms for a list of 50 rules.

Component
Scenario Rule Engine
service high
Epic Risks (2)
high impact medium prob scope

The Rule Engine must support a flexible JSON rule schema that can express compound conditions (e.g., contact_type AND wellbeing_flag AND delay_days). Underestimating schema expressiveness may require breaking changes to the rule format after coordinators have already configured rules.

Mitigation & Contingency

Mitigation: Define and freeze the rule JSON schema (trigger_type enum, metadata_conditions structure, delay logic) before any implementation begins; validate schema against all known HLF scenarios documented in the feature spec.

Contingency: If schema changes are needed after deployment, implement a schema version field and a migration utility that upgrades stored rules to the new format without coordinator intervention.

medium impact medium prob technical

Deep-link navigation to the activity wizard with pre-filled arguments may fail if the user's session has expired or if the wizard route is not yet mounted in the navigator stack, causing unhandled navigation exceptions.

Mitigation & Contingency

Mitigation: Implement session state check before navigation; if session is expired, redirect to biometric/login screen and store the pending deep-link URI for post-auth redirect using go_router's redirect mechanism.

Contingency: If post-auth redirect proves unreliable, fall back to navigating to the home screen with a visible action banner that re-triggers the wizard with pre-filled arguments.