204 lines · 9.4 KB
import type { ConflictHunk } from "../git/merge3";
import { redactSecrets } from "./candidates";
import type { Candidate, SecretVault } from "./candidates";

export type Thresholds = { confidence: number; margin: number };
export const DEFAULT_THRESHOLDS: Thresholds = { confidence: 0.8, margin: 0.3 };

export const RISK_LEVELS = ["cosmetic", "logic", "security-sensitive"] as const;
export type RiskLevel = (typeof RISK_LEVELS)[number];
/** Any real chance of a security-sensitive change routes the file to a human. */
const SECURITY_PROBABILITY_LIMIT = 0.25;
const NEEDS_HUMAN_LIMIT = 0.5;
/** Clef accepts at most 64 questions; two are the per-file risk and needs-human questions. */
export const MAX_HUNKS_PER_FILE = 16;

type ChoiceQuestion = { type: "choice"; instructions: string; criteria: Record<string, string> };
type ScoreQuestion = { type: "score"; instructions: string; criteria: string[] };
type NoulQuestion = { type: "noul"; instructions: string; criteria?: { true: string; false: string } };

export type ClefRequest = {
  model: "clef" | "clef-flash";
  state: unknown;
  questions: Record<string, ChoiceQuestion | ScoreQuestion | NoulQuestion>;
};

type ChoiceAnswer = { type: "choice"; choice: string; probabilities: Record<string, number>; confidence: number };
type ScoreAnswer = { type: "score"; score: number; probabilities: Record<string, number>; confidence: number };
type NoulAnswer = { type: "noul"; noul: number };
export type ClefResponse = {
  answers: Record<string, ChoiceAnswer | ScoreAnswer | NoulAnswer | undefined>;
  usage?: { input_tokens: number; output_tokens: number };
};

export type HunkInput = { hunk: ConflictHunk; candidates: Candidate[] };

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null && !Array.isArray(value);
}

/** Clef output is parsed defensively: anything that is not a probability counts as zero. */