// Playbook — procedural knowledge extracted from accepted/partially- // accepted runs. Different from pathway_memory's bug_fingerprints (which // are pattern-detectors) — playbooks describe HOW to handle a task type. import { ValidationResult, requireString, requireIsoTimestamp, requireProvenance, requireStringArray, } from "./types"; export const PLAYBOOK_SCHEMA_VERSION = 1; export interface Playbook { schema_version: number; playbook_id: string; task_type: string; // e.g. "scrum_review", "pr_audit", "staffing.fill" problem_pattern: string; // when does this playbook apply? useful_context: string[]; // what to retrieve before running model_routing_path: string[]; // ordered model attempts that worked commands_worked: string[]; commands_failed: string[]; validation_steps: string[]; repo_files_touched: string[]; recovery_strategy: string; // what to do when the path fails known_failure_modes: string[]; escalation_threshold: string; // when to switch to a stronger model acceptance_criteria: string[]; // how to know it succeeded source_run_ids: string[]; // FK to EvidenceRecord.run_id (provenance — every playbook traces to source) created_at: string; provenance: { source_file: string; line_offset?: number; sig_hash: string; recorded_at: string }; } export function validatePlaybook(input: unknown): ValidationResult { const errors: string[] = []; if (typeof input !== "object" || input === null) return { valid: false, errors: ["expected object"] }; const r = input as Record; let ok = true; if (r.schema_version !== PLAYBOOK_SCHEMA_VERSION) { errors.push(`schema_version: expected ${PLAYBOOK_SCHEMA_VERSION}, got ${JSON.stringify(r.schema_version)}`); ok = false; } ok = requireString(r.playbook_id, "playbook_id", errors) && ok; ok = requireString(r.task_type, "task_type", errors) && ok; ok = requireString(r.problem_pattern, "problem_pattern", errors) && ok; ok = requireString(r.recovery_strategy, "recovery_strategy", errors) && ok; ok = requireString(r.escalation_threshold, "escalation_threshold", errors) && ok; ok = requireIsoTimestamp(r.created_at, "created_at", errors) && ok; ok = requireStringArray(r.useful_context, "useful_context", errors) && ok; ok = requireStringArray(r.model_routing_path, "model_routing_path", errors) && ok; ok = requireStringArray(r.commands_worked, "commands_worked", errors) && ok; ok = requireStringArray(r.commands_failed, "commands_failed", errors) && ok; ok = requireStringArray(r.validation_steps, "validation_steps", errors) && ok; ok = requireStringArray(r.repo_files_touched, "repo_files_touched", errors) && ok; ok = requireStringArray(r.known_failure_modes, "known_failure_modes", errors) && ok; ok = requireStringArray(r.acceptance_criteria, "acceptance_criteria", errors) && ok; ok = requireStringArray(r.source_run_ids, "source_run_ids", errors) && ok; if (Array.isArray(r.source_run_ids) && r.source_run_ids.length === 0) { errors.push("source_run_ids: must be non-empty — every playbook traces to source evidence (spec non-negotiable)"); ok = false; } if (Array.isArray(r.acceptance_criteria) && r.acceptance_criteria.length === 0) { errors.push("acceptance_criteria: must be non-empty — every playbook needs success criteria (spec non-negotiable)"); ok = false; } ok = requireProvenance(r.provenance, "provenance", errors) && ok; if (!ok) return { valid: false, errors }; return { valid: true, value: r as unknown as Playbook }; }