diff --git a/app/src/components/guidedStudy/SynthesisFreeRecap.tsx b/app/src/components/guidedStudy/SynthesisFreeRecap.tsx index dcd821bc..ab65db7c 100644 --- a/app/src/components/guidedStudy/SynthesisFreeRecap.tsx +++ b/app/src/components/guidedStudy/SynthesisFreeRecap.tsx @@ -5,17 +5,13 @@ import * as Sharing from 'expo-sharing'; import * as FileSystem from 'expo-file-system/legacy'; import { ChevronRight, Copy, Share2 } from 'lucide-react-native'; import { fontFamily, radii, spacing, useTheme } from '../../theme'; +import { isFlagEnabled } from '../../config/featureFlags'; import type { CapturedInputs } from '../../services/guidedStudy/capturedInputs'; import type { CapturedTextRef } from '../../services/guidedStudy/stepBindings'; import { getCapturedText } from '../../services/guidedStudy/stepBindings'; import type { GuidedStudyMode } from '../../services/guidedStudy/types'; import { logger } from '../../utils/logger'; -// Phase 3.1 (#1738) replaces this inline constant with an import from a -// dedicated feature-flags module. Until then the flag stays off so the -// premium-nudge footer copy reflects the structured (non-Amicus) path. -const GUIDED_STUDY_AMICUS_SYNTHESIS = false; - interface RecapSection { label: string; ref: CapturedTextRef; @@ -152,7 +148,7 @@ export function SynthesisFreeRecap({ if (sections.length === 0) return null; - const footerCopy = GUIDED_STUDY_AMICUS_SYNTHESIS + const footerCopy = isFlagEnabled('GUIDED_STUDY_AMICUS_SYNTHESIS') ? PREMIUM_FOOTER_AMICUS_ON : PREMIUM_FOOTER_AMICUS_OFF; diff --git a/app/src/config/__tests__/featureFlags.test.ts b/app/src/config/__tests__/featureFlags.test.ts new file mode 100644 index 00000000..efe6fdd2 --- /dev/null +++ b/app/src/config/__tests__/featureFlags.test.ts @@ -0,0 +1,27 @@ +import { FEATURE_FLAGS, isFlagEnabled, type FeatureFlag } from '../featureFlags'; + +describe('featureFlags', () => { + it('GUIDED_STUDY_AMICUS_SYNTHESIS defaults to false', () => { + expect(FEATURE_FLAGS.GUIDED_STUDY_AMICUS_SYNTHESIS).toBe(false); + expect(isFlagEnabled('GUIDED_STUDY_AMICUS_SYNTHESIS')).toBe(false); + }); + + it('FEATURE_FLAGS is exhaustively typed by FeatureFlag', () => { + // Round-trip through FeatureFlag — caller can iterate via a typed + // narrowing. If a flag is added to FEATURE_FLAGS without updating the + // type, tsc fails on this assignment. + const keys = Object.keys(FEATURE_FLAGS) as FeatureFlag[]; + expect(keys).toContain('GUIDED_STUDY_AMICUS_SYNTHESIS'); + for (const key of keys) { + expect(typeof FEATURE_FLAGS[key]).toBe('boolean'); + } + }); + + it('isFlagEnabled returns the stored boolean', () => { + // Spot-check the helper actually reads from FEATURE_FLAGS rather than + // hardcoding a value. + expect(isFlagEnabled('GUIDED_STUDY_AMICUS_SYNTHESIS')).toBe( + FEATURE_FLAGS.GUIDED_STUDY_AMICUS_SYNTHESIS, + ); + }); +}); diff --git a/app/src/config/featureFlags.ts b/app/src/config/featureFlags.ts new file mode 100644 index 00000000..661faa38 --- /dev/null +++ b/app/src/config/featureFlags.ts @@ -0,0 +1,28 @@ +/** + * Feature flags for in-flight work that needs to ship behind a switch. + * + * These are compile-time constants on purpose — no remote config in this + * codebase yet. Flipping a flag is a single-line PR + EAS Update. + * + * Add a comment per flag explaining what to verify before flipping. + */ +export const FEATURE_FLAGS = { + /** + * Enables Amicus-drafted synthesis at the end of guided study sessions. + * Before flipping true, verify: + * - Epic #1446 (Amicus AI Study Partner) is past Phase 3 (chat working + * against the production worker). + * - The mode-specific system prompts in app/src/services/amicus/prompts/ + * guidedStudy.ts have been reviewed. + * - Anthropic prompt caching is hitting on the system prompt (>80% cache + * rate observed in dev for at least 100 calls). + * When false, premium users get the structured-form synthesis path. + */ + GUIDED_STUDY_AMICUS_SYNTHESIS: false, +} as const; + +export type FeatureFlag = keyof typeof FEATURE_FLAGS; + +export function isFlagEnabled(flag: FeatureFlag): boolean { + return FEATURE_FLAGS[flag]; +}