critical priority low complexity backend pending backend specialist Tier 1

Acceptance Criteria

A `RolloutEvaluator` class (or top-level pure functions) exists in `lib/core/feature_flags/rollout_evaluator.dart`
`isVersionEligible(String currentVersion, String minAppVersion)` returns true if currentVersion >= minAppVersion using correct semantic version ordering (2.10.0 > 2.9.0, not string comparison)
`isActivationDateMet(DateTime activationDate, DateTime now)` returns true if activationDate is on or before now
`isInRolloutPercentage(String organizationId, int rolloutPercent)` returns a deterministic bool — the same orgId always maps to the same bucket, enabling reproducible flag state across app restarts
`evaluate(FeatureFlagConfig config, String organizationId, String currentVersion, DateTime now)` combines all three checks: all applicable conditions must pass for the flag to be considered active; absent conditions (null minAppVersion, null activationDate, rolloutPercent = 100) are treated as always-passing
The evaluator has no constructor dependencies (no I/O, no network, no database) — all inputs arrive as parameters
All methods are synchronous and return bool
Rollout percentage hashing uses a stable algorithm (e.g., CRC32 or FNV-1a of the orgId UUID bytes modulo 100) documented in code comments
Edge cases handled: rolloutPercent of 0 always returns false; rolloutPercent of 100 always returns true without hashing

Technical Requirements

frameworks
Dart (latest)
performance requirements
All evaluations are synchronous and must complete in under 1ms — no async operations permitted
Hash computation for rollout percentage must be O(1) relative to the number of flags
security requirements
The rollout hash must not be reversible to the organization_id — use a one-way hash function
Rollout percentage must not be influenced by client-supplied data other than the organization_id from the authenticated JWT

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

For semantic version comparison, avoid string lexicographic comparison — split on '.' and compare integer tuples. Consider using the `pub_semver` package if already in the project, otherwise implement a 3-tuple comparison directly (it is a 10-line function). For the rollout hash, compute `crc32(utf8.encode(organizationId)) % 100` and check if the result is less than `rolloutPercent`. Dart does not have a built-in CRC32, but the `crc32` package is tiny; alternatively, use a simple polynomial hash: `organizationId.codeUnits.fold(0, (h, c) => (h * 31 + c) & 0xFFFFFFFF) % 100`.

Document the chosen algorithm clearly with a comment block — changing the algorithm later would change bucket assignments for all organizations, so it must be treated as a breaking change. The `FeatureFlagConfig` parameter should be a lightweight data class holding optional `minAppVersion`, `activationDate`, and `rolloutPercent` fields.

Testing Requirements

Pure unit tests with no mocking required — all inputs are passed as parameters. Test cases: (1) version comparison: 2.10.0 > 2.9.0, 1.0.0 < 1.0.1, equal versions return true; (2) activation date: past date passes, future date fails, exact current moment passes; (3) rollout 100% always true, 0% always false; (4) rollout hash is deterministic — same orgId called 1000 times returns same result; (5) rollout hash distribution: generate 1000 random UUIDs, verify roughly 50% fall within 50% bucket (statistical, not exact); (6) evaluate() returns false if any condition fails; (7) evaluate() returns true when all conditions pass; (8) null/absent conditions treated as passing. Target 100% branch coverage — this is a pure logic module with no external dependencies to mock.

Component
Rollout Condition Evaluator
service low
Epic Risks (3)
high impact medium prob security

Supabase RLS policies for organization_configs may have gaps that allow cross-organization reads if the JWT claim for organization_id is absent or malformed, leading to data leakage between tenants.

Mitigation & Contingency

Mitigation: Implement RLS policies using auth.uid() joined against a memberships table to derive organization_id rather than trusting a client-supplied claim. Write integration tests that simulate a cross-org read attempt and assert it returns zero rows.

Contingency: If a gap is discovered post-launch, immediately disable the affected RLS policy, roll back the migration, and re-implement with a parameterized policy tested against all organization fixture data.

medium impact medium prob technical

Dart does not have a built-in semantic version comparison library; a naive string comparison (e.g., '2.10.0' < '2.9.0' lexicographically) would cause rollout evaluator to produce incorrect eligibility results for organizations on different app versions.

Mitigation & Contingency

Mitigation: Use the pub.dev `pub_semver` package or implement a proper three-segment integer comparison. Add parameterized unit tests covering 20+ version pairs including double-digit minor/patch segments.

Contingency: If incorrect comparison is discovered in production, push a hotfix with corrected comparison logic and temporarily disable phase-gated flags until all affected organizations have updated to the corrected version.

medium impact low prob technical

Persistent local cache written to shared_preferences or Hive could become corrupted or deserialized incorrectly after an app update changes the FeatureFlag schema, causing startup crashes or all flags defaulting to disabled.

Mitigation & Contingency

Mitigation: Wrap all cache reads in try/catch with explicit fallback to the all-disabled default map. Version the cache key (e.g., `feature_flags_v2_{orgId}`) so schema changes automatically invalidate old entries.

Contingency: If cache corruption is detected in a release, publish an app update that clears the versioned cache key on first launch and re-fetches from Supabase.