Skip to content

Phase 3.2 — SynthesisStrategy interface + freeStrategy implementation #1739

@CraigBuckmaster

Description

@CraigBuckmaster

Part of epic #1725. Depends on #1738, Phase 2 complete. Phase 3 — Premium synthesis + review.

Objective

Introduce the strategy abstraction that lets the screen render the right synthesis experience without knowing the user's tier or flag state. This card delivers the interface and the free implementation; #3.3 and Phase 4 add the premium implementations.

Files

Create

  • app/src/services/guidedStudy/synthesis/strategy.ts — interface + factory
  • app/src/services/guidedStudy/synthesis/freeStrategy.ts
  • app/src/services/guidedStudy/synthesis/__tests__/strategy.test.ts

Modify

  • app/src/screens/StudySessionScreen.tsx — call factory, render strategy output

Strategy contract

// strategy.ts
import type { GuidedStudyPlan } from '../types';
import type { CapturedInputs } from '../capturedInputs';

export type SynthesisStrategyKind = 'free' | 'premium_structured' | 'premium_amicus';

export interface SynthesisRunContext {
  plan: GuidedStudyPlan;
  captured: CapturedInputs;
  sessionId: number | null;
  bookId: string;
  chapterNum: number;
}

export interface SynthesisRunResult {
  kind: SynthesisStrategyKind;
  /** What the screen renders. The strategy returns React-shaped descriptors,
   *  not JSX, so test/preview code can render them in isolation. */
  output: SynthesisOutputBlock[];
  /** Persisted artifact (null for free — nothing saved). */
  artifact: ReviewArtifact | null;
}

export type SynthesisOutputBlock =
  | { type: 'recap_section'; label: string; content: string }
  | { type: 'cta_button'; label: string; action: 'copy' | 'share' | 'upgrade' }
  | { type: 'streaming_placeholder'; }     // premium_amicus only
  | { type: 'amicus_text'; html: string; citations: CitationRef[] }
  | { type: 'footer_note'; text: string };

export interface SynthesisStrategy {
  kind: SynthesisStrategyKind;
  run(ctx: SynthesisRunContext, callbacks?: {
    onAmicusDelta?: (token: string) => void;
    onAmicusComplete?: () => void;
    onError?: (err: Error) => void;
  }): Promise<SynthesisRunResult>;
}

export function chooseStrategy(args: {
  isPremium: boolean;
  amicusFlagEnabled: boolean;
  amicusCanUse: boolean;  // result of useAmicusAccess.canUse
}): SynthesisStrategy;

chooseStrategy logic:

This card delivers freeStrategy only. The other two are stubs that throw new Error('not implemented') and are filled in by their respective cards.

freeStrategy implementation

Mode-shaped recap. Reads captured and mode from plan. Returns SynthesisOutputBlock[]:

// Pseudocode
const blocks: SynthesisOutputBlock[] = [];
for (const section of recapSectionsForMode(plan.mode, captured)) {
  blocks.push({ type: 'recap_section', label: section.label, content: section.content });
}
blocks.push({ type: 'cta_button', label: 'Copy to clipboard', action: 'copy' });
blocks.push({ type: 'cta_button', label: 'Share', action: 'share' });
blocks.push({
  type: 'footer_note',
  text: 'Companion+ saves this for spaced review and brings it back when it matters.',
});
return { kind: 'free', output: blocks, artifact: null };

recapSectionsForMode lives in freeStrategy.ts and uses the same mode→sections mapping from #2.7's SynthesisFreeRecap. Refactor #2.7 so SynthesisFreeRecap consumes SynthesisOutputBlock[] rather than computing recap shape inline. This unifies the two paths.

Screen wiring

In StudySessionScreen.tsx synthesize step:

const access = useAmicusAccess();
const strategy = useMemo(() => chooseStrategy({
  isPremium,
  amicusFlagEnabled: isFlagEnabled('GUIDED_STUDY_AMICUS_SYNTHESIS'),
  amicusCanUse: access.canUse,
}), [isPremium, access.canUse]);

// Run when synthesis fields are populated enough; render result.output.

Acceptance

  1. freeStrategy produces correct SynthesisOutputBlock[] for each mode.
  2. SynthesisFreeRecap from Merge pull request #1 from CraigBuckmaster/codex/review-recent-master… #2.7 is refactored to consume blocks.
  3. chooseStrategy returns freeStrategy for non-premium; throws "not implemented" for premium paths until Merge pull request #2 from CraigBuckmaster/master #3.3 lands.
  4. tsc, lint, test clean.

Out of scope

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions