All files / server/services/tutor/strategy effective-tutor-strategy.ts

91.66% Statements 11/12
100% Branches 2/2
100% Functions 3/3
90% Lines 9/10

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75    36x   36x             6400x       36x                                   36x                                                           1273x         1273x 3258x 3258x        
import { z } from "zod";
 
const SAFE_ID = /^[a-z][a-z0-9-]{0,63}$/;
 
const questionKindWeightsSchema = z.object({
  recall: z.number().int().min(0).max(100),
  concept: z.number().int().min(0).max(100),
  application: z.number().int().min(0).max(100),
  prediction: z.number().int().min(0).max(100),
  transfer: z.number().int().min(0).max(100),
}).strict().refine(
  (value) => Object.values(value).reduce((sum, weight) => sum + weight, 0) === 100,
  "Question-kind weights must sum to 100",
);
 
export const effectiveTutorStrategySchema = z.object({
  schemaVersion: z.literal(1),
  id: z.string().regex(SAFE_ID),
  questionKindWeights: questionKindWeightsSchema,
  sketchSpecificity: z.enum(["prefer", "strict"]),
  repetition: z.enum(["strict", "relaxed"]),
  remediation: z.enum(["scaffold-first", "question-first"]),
  clarification: z.enum(["same-indicator", "new-indicator"]),
  progression: z.enum(["mastery-then-advance", "advance-immediately"]),
  scaffolding: z.enum(["prefer-content", "prefer-generated"]),
  feedbackVerbosity: z.enum(["short", "detailed"]),
  hintFirst: z.boolean(),
  adaptiveDifficulty: z.literal("current-contract"),
}).strict();
 
export type EffectiveTutorStrategy = z.infer<typeof effectiveTutorStrategySchema>;
export type TutorStrategyResolutionSource = "example" | "repository" | "built-in";
 
export const BUILT_IN_TUTOR_STRATEGY: EffectiveTutorStrategy = {
  schemaVersion: 1,
  id: "built-in-default",
  questionKindWeights: {
    recall: 10,
    concept: 25,
    application: 35,
    prediction: 15,
    transfer: 15,
  },
  sketchSpecificity: "prefer",
  repetition: "strict",
  remediation: "scaffold-first",
  clarification: "same-indicator",
  progression: "mastery-then-advance",
  scaffolding: "prefer-content",
  feedbackVerbosity: "short",
  hintFirst: true,
  adaptiveDifficulty: "current-contract",
};
 
export type StrategyResolution = {
  readonly strategy: EffectiveTutorStrategy;
  readonly source: TutorStrategyResolutionSource;
};
 
export function resolveEffectiveTutorStrategy(input: {
  readonly perExample?: unknown;
  readonly repositoryDefault?: unknown;
}): StrategyResolution {
  const candidates: readonly { value: unknown; source: TutorStrategyResolutionSource }[] = [
    { value: input.perExample, source: "example" },
    { value: input.repositoryDefault, source: "repository" },
    { value: BUILT_IN_TUTOR_STRATEGY, source: "built-in" },
  ];
  for (const candidate of candidates) {
    const parsed = effectiveTutorStrategySchema.safeParse(candidate.value);
    if (parsed.success) return { strategy: parsed.data, source: candidate.source };
  }
  throw new Error("Built-in Tutor strategy is invalid");
}