#!/usr/bin/env node
/**
 * grader.mjs — Deterministic grader for project-scaffold skill output.
 *
 * Checks whether the scaffold output meets structural requirements:
 * - Required files exist
 * - No placeholder text (TBD/TODO/FIXME/PLACEHOLDER)
 * - Cross-references are consistent
 * - AGENTS.md is within line limit
 * - Milestone structure is valid
 * - Scripts are present
 * - Sub-skills are installed
 *
 * Usage:
 *   node evals/grader.mjs <project-dir>
 *
 * Exit code 0 = all checks pass. Non-zero = failures detected.
 * Outputs JSON result to stdout.
 */

import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";

const projectDir = process.argv[2] || process.cwd();
const FEEDBACK_PROFILE_PATH = "docs/feedback-profile.json";
const MANIFEST_PATH = "docs/harness-manifest.json";
const FALLBACK_CORE_FILES = [
  "AGENTS.md",
  FEEDBACK_PROFILE_PATH,
  "docs/prompt.md",
  "docs/plan.md",
  "docs/implement.md",
];
const FALLBACK_RECOMMENDED_FILES = [
  "docs/project_spec.md",
  "docs/documentation.md",
  "docs/failures/taxonomy.md",
  "docs/compaction-strategy.md",
];
const FALLBACK_SCRIPT_PAIRS = [
  ["scripts/quality-gates.sh", "scripts/quality-gates.ps1"],
  ["scripts/validate-docs.sh", "scripts/validate-docs.ps1"],
];
const ALL_STANDARD_DOCS = [
  ...FALLBACK_CORE_FILES,
  ...FALLBACK_RECOMMENDED_FILES,
  MANIFEST_PATH,
];
const ALL_STANDARD_SCRIPTS = [
  "scripts/validate-docs.sh",
  "scripts/validate-docs.ps1",
  "scripts/quality-gates.sh",
  "scripts/quality-gates.ps1",
];
const ALL_STANDARD_SUB_SKILLS = [
  "iterate",
  "quality-audit",
  "milestone-promote",
];

const results = {
  overall_pass: true,
  score: 0,
  checks: [],
  missing_files: [],
  placeholder_violations: [],
  cross_ref_errors: [],
};

let totalWeight = 0;
let earnedWeight = 0;

function check(id, weight, pass, notes) {
  results.checks.push({ id, pass, weight, notes });
  totalWeight += weight;
  if (pass) {
    earnedWeight += weight;
  } else {
    results.overall_pass = false;
  }
}

function fileExists(relativePath) {
  return existsSync(join(projectDir, relativePath));
}

function stripBom(content) {
  return typeof content === "string" ? content.replace(/^\uFEFF/, "") : content;
}

function readFile(relativePath) {
  const fullPath = join(projectDir, relativePath);
  if (!existsSync(fullPath)) return null;
  return stripBom(readFileSync(fullPath, "utf8"));
}

function countLines(content) {
  return content.split("\n").length;
}

function unique(values) {
  return [...new Set(values)];
}

function normalizePathList(values) {
  if (!Array.isArray(values)) return [];
  return unique(values.filter((item) => typeof item === "string").map((item) => item.replace(/\\/g, "/")));
}

function skillPath(name) {
  return `.agents/skills/${name}/SKILL.md`;
}

function commandScriptPath(commandText) {
  if (!commandText) return null;
  const match = String(commandText).match(/scripts[\\/][^\s"'`]+/i);
  return match ? match[0].replace(/\\/g, "/") : null;
}

function parseJsonContent(content) {
  return JSON.parse(stripBom(content));
}

function detectCurrentMilestoneFromPlan(content) {
  if (!content) return null;

  for (const line of content.split("\n")) {
    const tableMatch = line.match(/^\|\s*(M\d+)\s*\|.*\|\s*[^|]*\bin progress\b[^|]*\|/i);
    if (tableMatch) return tableMatch[1];
  }

  for (const line of content.split("\n")) {
    const headingMatch = line.match(/^##+\s*(?:->|→|-->|=>)?\s*(M\d+)\b/i);
    if (headingMatch) return headingMatch[1];
  }

  return null;
}

function detectCurrentMilestoneFromPrompt(content) {
  if (!content) return null;

  const headingMatch = content.match(/^##\s*Current Milestone:\s*(M\d+)\b/im);
  if (headingMatch) return headingMatch[1];

  const fallbackMatch = content.match(/\b(M\d+)\b/);
  return fallbackMatch ? fallbackMatch[1] : null;
}

function findPlaceholders(content, relativePath = "") {
  const isValidationScript = /scripts[\\/](validate-docs|quality-gates)\.(sh|ps1)$/i.test(relativePath);
  // Match standalone placeholder markers, not words embedded in descriptions.
  // Patterns: "TBD", "TODO:", "FIXME:", "PLACEHOLDER", "XXX"
  // Ignore case-insensitive occurrences inside normal prose (e.g., "1/1 placeholder test").
  // Only flag lines where the placeholder IS the value (e.g., "North Star: TBD")
  // or appears as a marker (e.g., "TODO: fill this in", "FIXME: broken").
  const lines = content.split("\n");
  let count = 0;
  for (const line of lines) {
    // Skip code blocks and comments that commonly contain TODO
    if (line.trim().startsWith("//") || line.trim().startsWith("#!")) continue;
    if (isValidationScript) {
      if (/\{\{[A-Z_]+\}\}/.test(line)) count++;
      continue;
    }
    // Match: value is entirely a placeholder (after colon/pipe), or TODO:/FIXME: markers
    if (/:\s*TBD\s*$/i.test(line) || /\|\s*TBD\s*\|/i.test(line)) count++;
    if (/\bTODO\s*:/i.test(line)) count++;
    if (/\bFIXME\s*:/i.test(line)) count++;
    if (/:\s*PLACEHOLDER\s*$/i.test(line) || /\|\s*PLACEHOLDER\s*\|/i.test(line)) count++;
    if (/:\s*XXX\s*$/i.test(line) || /\|\s*XXX\s*\|/i.test(line)) count++;
    // Match: {{PLACEHOLDER_NAME}} template variables that were not replaced
    if (/\{\{[A-Z_]+\}\}/.test(line)) count++;
  }
  return count;
}

function hasUnsafeRollback(content) {
  return content.split("\n").some((line) => {
    if (!/git checkout -- \.|git reset --hard/.test(line)) return false;
    return !/never use|forbid|forbidden|do not use|don't use/i.test(line);
  });
}

const harnessManifestContent = readFile(MANIFEST_PATH);
let harnessManifest = null;
let harnessManifestError = null;
if (harnessManifestContent) {
  try {
    harnessManifest = parseJsonContent(harnessManifestContent);
  } catch (error) {
    harnessManifestError = error.message;
  }
}

const feedbackProfileContent = readFile(FEEDBACK_PROFILE_PATH);
let feedbackProfile = null;
let feedbackProfileError = null;
if (feedbackProfileContent) {
  try {
    feedbackProfile = parseJsonContent(feedbackProfileContent);
  } catch (error) {
    feedbackProfileError = error.message;
  }
}

check(
  "harness-manifest",
  5,
  !harnessManifestContent || !harnessManifestError,
  harnessManifestContent
    ? (harnessManifestError
      ? `Invalid ${MANIFEST_PATH}: ${harnessManifestError}`
      : `${MANIFEST_PATH} parsed successfully`)
    : `${MANIFEST_PATH} not present; using heuristic fallback checks`
);

check(
  "feedback-profile",
  5,
  !feedbackProfileContent || !feedbackProfileError,
  feedbackProfileContent
    ? (feedbackProfileError
      ? `Invalid ${FEEDBACK_PROFILE_PATH}: ${feedbackProfileError}`
      : `${FEEDBACK_PROFILE_PATH} parsed successfully`)
    : `${FEEDBACK_PROFILE_PATH} not present; using heuristic fallback checks`
);

const manifestDocumentFiles = normalizePathList(harnessManifest?.documents);
const manifestScriptFiles = normalizePathList(harnessManifest?.scripts);
const manifestSubSkillNames = normalizePathList(harnessManifest?.sub_skills);
const manifestSubSkillFiles = manifestSubSkillNames.map(skillPath);
const manifestDriven = !!harnessManifest && !harnessManifestError;

// ─── Check 1: Core files exist ────────────────────────────────────
const requiredFiles = manifestDriven
  ? unique([...manifestDocumentFiles, ...manifestScriptFiles])
  : FALLBACK_CORE_FILES;
let coreMissing = 0;
for (const f of requiredFiles) {
  if (!fileExists(f)) {
    results.missing_files.push(f);
    coreMissing++;
  }
}
check(
  "required-artifacts",
  15,
  coreMissing === 0,
  coreMissing === 0
    ? `All ${requiredFiles.length} required artifacts exist`
    : `Missing ${coreMissing} required artifacts: ${results.missing_files.join(", ")}`
);

if (manifestDriven) {
  check(
    "recommended-files",
    5,
    true,
    "Harness manifest is present; artifact expectations come from the manifest contract"
  );
} else {
  let recommendedPresent = 0;
  for (const f of FALLBACK_RECOMMENDED_FILES) {
    if (fileExists(f)) recommendedPresent++;
  }
  check(
    "recommended-files",
    5,
    recommendedPresent >= 2,
    `${recommendedPresent}/${FALLBACK_RECOMMENDED_FILES.length} recommended files present`
  );
}

if (manifestDriven) {
  const scriptsExist = manifestScriptFiles.every((path) => fileExists(path));
  check(
    "scripts-present",
    5,
    scriptsExist,
    manifestScriptFiles.length > 0
      ? (scriptsExist
        ? `All ${manifestScriptFiles.length} manifest-declared scripts exist`
        : `Missing manifest-declared scripts: ${manifestScriptFiles.filter((path) => !fileExists(path)).join(", ")}`)
      : "Harness manifest lists no required scripts"
  );
} else {
  let scriptPairsPresent = 0;
  for (const [sh, ps1] of FALLBACK_SCRIPT_PAIRS) {
    if (fileExists(sh) || fileExists(ps1)) scriptPairsPresent++;
  }
  check(
    "scripts-present",
    5,
    scriptPairsPresent === FALLBACK_SCRIPT_PAIRS.length,
    scriptPairsPresent === FALLBACK_SCRIPT_PAIRS.length
      ? "At least one variant per script type exists"
      : `Only ${scriptPairsPresent}/${FALLBACK_SCRIPT_PAIRS.length} script types have at least one variant`
  );
}

// ─── Check 2: No placeholder text ──────────────────────────────────
const DOCS_TO_CHECK = manifestDriven
  ? unique([...manifestDocumentFiles, ...manifestScriptFiles])
  : unique([...ALL_STANDARD_DOCS, ...ALL_STANDARD_SCRIPTS]);

const SUB_SKILL_FILES = manifestDriven
  ? manifestSubSkillFiles
  : ALL_STANDARD_SUB_SKILLS.map(skillPath);

for (const f of unique([...DOCS_TO_CHECK, ...SUB_SKILL_FILES])) {
  const content = readFile(f);
  if (content) {
    const count = findPlaceholders(content, f);
    if (count > 0) {
      results.placeholder_violations.push(`${f} (${count} placeholders)`);
    }
  }
}
check(
  "no-placeholders",
  15,
  results.placeholder_violations.length === 0,
  results.placeholder_violations.length === 0
    ? "No placeholder text found in generated docs"
    : `Placeholders found: ${results.placeholder_violations.join(", ")}`
);

// ─── Check 3: AGENTS.md line limit ─────────────────────────────────
const agentsMd = readFile("AGENTS.md");
if (agentsMd) {
  const lines = countLines(agentsMd);
  check(
    "agents-md-size",
    10,
    lines <= 150,
    `AGENTS.md is ${lines} lines (limit: 150)`
  );
} else {
  check("agents-md-size", 10, false, "AGENTS.md does not exist");
}

// ─── Check 4: Cross-reference consistency ──────────────────────────
if (agentsMd) {
  const backtickRefs = agentsMd.match(/`([^`]+)`/g) || [];
  for (const ref of backtickRefs) {
    const path = ref.replace(/`/g, "");
    if (
      (path.startsWith("docs/") || path.startsWith("scripts/")) &&
      path.includes(".")
    ) {
      if (!fileExists(path)) {
        results.cross_ref_errors.push(`AGENTS.md → ${path}`);
      }
    }
  }
}
check(
  "cross-references",
  15,
  results.cross_ref_errors.length === 0,
  results.cross_ref_errors.length === 0
    ? "All cross-references in AGENTS.md point to existing files"
    : `Broken refs: ${results.cross_ref_errors.join(", ")}`
);

// ─── Check 5: Execution contract consistency ───────────────────────
function extractExecutionContract(content) {
  if (!content) return null;
  const shell = content.match(/^- Primary shell:\s*(.+)$/m)?.[1]?.trim();
  const quality = content.match(/^- Primary quality gate command:\s*`([^`]+)`/m)?.[1];
  const docs = content.match(/^- Primary doc validation command:\s*`([^`]+)`/m)?.[1];
  if (!shell || !quality || !docs) return null;
  return { shell, quality, docs };
}

const projectSpec = readFile("docs/project_spec.md");
const implementMd = readFile("docs/implement.md");
const promptMd = readFile("docs/prompt.md");
const agentsContract = extractExecutionContract(agentsMd);
const manifestContract = harnessManifest?.execution_contract ?? null;
const feedbackProfileContract = feedbackProfile?.execution_contract ?? null;

let executionContractPass = true;
const executionNotes = [];

if (!agentsContract) {
  executionContractPass = false;
  executionNotes.push("AGENTS.md is missing the execution contract");
} else {
  const expectedShell = manifestContract?.primary_shell ?? agentsContract.shell;
  const expectedQuality = manifestContract?.primary_quality_gate_command ?? agentsContract.quality;
  const expectedDocs = manifestContract?.primary_doc_validation_command ?? agentsContract.docs;

  if (manifestContract && agentsContract.shell !== expectedShell) {
    executionContractPass = false;
    executionNotes.push(`AGENTS.md shell does not match ${MANIFEST_PATH}`);
  }
  if (manifestContract && agentsContract.quality !== expectedQuality) {
    executionContractPass = false;
    executionNotes.push(`AGENTS.md quality gate command does not match ${MANIFEST_PATH}`);
  }
  if (manifestContract && agentsContract.docs !== expectedDocs) {
    executionContractPass = false;
    executionNotes.push(`AGENTS.md doc validation command does not match ${MANIFEST_PATH}`);
  }
  if (feedbackProfileContract && feedbackProfileContract.primary_shell !== expectedShell) {
    executionContractPass = false;
    executionNotes.push(`${FEEDBACK_PROFILE_PATH} shell does not match ${MANIFEST_PATH}`);
  }
  if (feedbackProfileContract && feedbackProfileContract.primary_quality_gate_command !== expectedQuality) {
    executionContractPass = false;
    executionNotes.push(`${FEEDBACK_PROFILE_PATH} quality gate command does not match ${MANIFEST_PATH}`);
  }
  if (feedbackProfileContract && feedbackProfileContract.primary_doc_validation_command !== expectedDocs) {
    executionContractPass = false;
    executionNotes.push(`${FEEDBACK_PROFILE_PATH} doc validation command does not match ${MANIFEST_PATH}`);
  }

  if (expectedShell === "windows-powershell") {
    if (!expectedQuality.includes(".ps1") || !expectedDocs.includes(".ps1")) {
      executionContractPass = false;
      executionNotes.push("Windows primary shell must point to .ps1 scripts");
    }
  } else if (expectedShell === "unix-shell") {
    if (!expectedQuality.includes(".sh") || !expectedDocs.includes(".sh")) {
      executionContractPass = false;
      executionNotes.push("Unix primary shell must point to .sh scripts");
    }
  } else {
    executionContractPass = false;
    executionNotes.push("Primary shell must be windows-powershell or unix-shell");
  }

  const qualityScriptPath = commandScriptPath(expectedQuality);
  const docsScriptPath = commandScriptPath(expectedDocs);

  if (manifestDriven && qualityScriptPath && !manifestScriptFiles.includes(qualityScriptPath)) {
    executionContractPass = false;
    executionNotes.push(`Primary quality gate script is not declared in ${MANIFEST_PATH}`);
  }
  if (manifestDriven && docsScriptPath && !manifestScriptFiles.includes(docsScriptPath)) {
    executionContractPass = false;
    executionNotes.push(`Primary doc validation script is not declared in ${MANIFEST_PATH}`);
  }
  if (qualityScriptPath && !fileExists(qualityScriptPath)) {
    executionContractPass = false;
    executionNotes.push(`Primary quality gate script is missing: ${qualityScriptPath}`);
  }
  if (docsScriptPath && !fileExists(docsScriptPath)) {
    executionContractPass = false;
    executionNotes.push(`Primary doc validation script is missing: ${docsScriptPath}`);
  }

  if (projectSpec && !projectSpec.includes(expectedQuality)) {
    executionContractPass = false;
    executionNotes.push("project_spec.md does not reference the primary quality gate command");
  }
  if (projectSpec && !projectSpec.includes(expectedDocs)) {
    executionContractPass = false;
    executionNotes.push("project_spec.md does not reference the primary doc validation command");
  }
  if (projectSpec && !projectSpec.includes(FEEDBACK_PROFILE_PATH)) {
    executionContractPass = false;
    executionNotes.push("project_spec.md does not reference docs/feedback-profile.json");
  }
  if (implementMd && !implementMd.includes(expectedQuality)) {
    executionContractPass = false;
    executionNotes.push("implement.md does not reference the primary quality gate command");
  }
  if (promptMd && !promptMd.includes(expectedDocs)) {
    executionContractPass = false;
    executionNotes.push("prompt.md does not reference the primary doc validation command");
  }
}

check(
  "execution-contract",
  15,
  executionContractPass,
  executionContractPass
    ? "Primary shell and command pair are self-consistent"
    : executionNotes.join("; ")
);

// ─── Check 6: Protocol compliance ────────────────────────────────
// Instead of checking fixed section headers, check that protocol concepts exist.
let protocolScore = 0;
const protocolNotes = [];

// 6a: Execution contract is present in AGENTS.md
if (agentsMd?.includes("Primary shell") && agentsMd?.includes("Primary quality gate command")) {
  protocolScore++;
} else {
  protocolNotes.push("AGENTS.md missing execution contract");
}

// 6b: Hard constraints are documented
if (agentsMd?.includes("Hard Constraints") || agentsMd?.includes("Forbidden")) {
  protocolScore++;
} else {
  protocolNotes.push("AGENTS.md missing hard constraints");
}

// 6c: Feedback loop or harness evolution is referenced
if (agentsMd?.includes("Feedback Loop") || agentsMd?.includes("harness")) {
  protocolScore++;
} else {
  protocolNotes.push("AGENTS.md missing feedback/evolution guidance");
}

// 6d: Escalation conditions are documented
if (agentsMd?.includes("Escalation") || agentsMd?.includes("escalat")) {
  protocolScore++;
} else {
  protocolNotes.push("AGENTS.md missing escalation conditions");
}

// 6e: Machine-readable contract is available when generated
if (!harnessManifestContent || harnessManifest) {
  protocolScore++;
} else {
  protocolNotes.push(`${MANIFEST_PATH} is invalid`);
}

// 6f: Feedback profile is available when generated
if (!feedbackProfileContent || feedbackProfile) {
  protocolScore++;
} else {
  protocolNotes.push(`${FEEDBACK_PROFILE_PATH} is invalid`);
}

check(
  "protocol-compliance",
  10,
  protocolScore >= 5,
  protocolScore >= 5
    ? `Protocol compliance: ${protocolScore}/6 checks passed`
    : `Protocol compliance: ${protocolScore}/6 — ${protocolNotes.join("; ")}`
);

// ─── Check 7: Milestone structure ──────────────────────────────────
const planMd = readFile("docs/plan.md");
if (planMd) {
  // M0 is adaptive — check that it exists, not that it says "Quality Infrastructure"
  const hasM0 = /^##+\s*(?:(?:->|→|-->|=>)\s*)?M0\b/im.test(planMd) || /\|\s*M0\s*\|/.test(planMd);
  const currentMilestone = detectCurrentMilestoneFromPlan(planMd);
  const hasVerification = /\*\*Verification\*\*/i.test(planMd);
  const hasCompactionCheckpoint = /\*\*Compaction Checkpoint\*\*/i.test(planMd);
  const hasQualityScoreHistory = /Quality Score History/i.test(planMd);
  const hasOperationsCadence = /Operations Cadence/i.test(planMd);

  const allPass =
    hasM0 &&
    !!currentMilestone &&
    hasVerification &&
    hasCompactionCheckpoint &&
    hasQualityScoreHistory &&
    hasOperationsCadence;
  const notes = [];
  if (!hasM0) notes.push("Missing M0 milestone");
  if (!currentMilestone) notes.push("Missing current milestone marker");
  if (!hasVerification) notes.push("Missing Verification section");
  if (!hasCompactionCheckpoint) notes.push("Missing Compaction Checkpoint");
  if (!hasQualityScoreHistory) notes.push("Missing Quality Score History");
  if (!hasOperationsCadence) notes.push("Missing Operations Cadence");

  check(
    "milestone-structure",
    15,
    allPass,
    allPass ? "Milestone structure is complete" : notes.join("; ")
  );
} else {
  check("milestone-structure", 15, false, "docs/plan.md does not exist");
}

// ─── Check 8: Milestone consistency (plan.md ↔ prompt.md) ─────────
if (planMd && promptMd) {
  const planCurrent = detectCurrentMilestoneFromPlan(planMd);
  const promptCurrent = detectCurrentMilestoneFromPrompt(promptMd);
  if (planCurrent && promptCurrent) {
    const match = planCurrent === promptCurrent;
    check(
      "milestone-consistency",
      10,
      match,
      match
        ? `Current milestone matches: ${planCurrent}`
        : `Mismatch: plan.md=${planCurrent}, prompt.md=${promptCurrent}`
    );
  } else {
    check(
      "milestone-consistency",
      10,
      false,
      "Could not detect milestone IDs in plan.md or prompt.md"
    );
  }
} else {
  check(
    "milestone-consistency",
    10,
    false,
    "plan.md or prompt.md missing — cannot check consistency"
  );
}

// ─── Check 9: documentation.md summary structure ───────────────────
const documentationMd = readFile("docs/documentation.md");
const documentationExpected = !manifestDriven || manifestDocumentFiles.includes("docs/documentation.md");
if (documentationMd) {
  const hasCurrentState = /^## Current State/m.test(documentationMd);
  const hasHowToRun = /^## How To Run/m.test(documentationMd);
  const hasKnownIssues = /^## Known Issues/m.test(documentationMd);
  const hasDecisionLog = /^## Decision Log/m.test(documentationMd);
  const hasActivityLog = /^## Activity Log/m.test(documentationMd);
  const pass =
    hasCurrentState &&
    hasHowToRun &&
    hasKnownIssues &&
    hasDecisionLog &&
    hasActivityLog;
  const notes = [];
  if (!hasCurrentState) notes.push("Missing Current State section");
  if (!hasHowToRun) notes.push("Missing How To Run section");
  if (!hasKnownIssues) notes.push("Missing Known Issues section");
  if (!hasDecisionLog) notes.push("Missing Decision Log section");
  if (!hasActivityLog) notes.push("Missing Activity Log section");
  check(
    "documentation-structure",
    10,
    pass,
    pass ? "documentation.md summary structure is complete" : notes.join("; ")
  );
} else {
  check(
    "documentation-structure",
    10,
    !documentationExpected,
    documentationExpected
      ? "docs/documentation.md does not exist"
      : "docs/documentation.md not required by the harness manifest"
  );
}

// ─── Check 10: Safe rollback guidance ──────────────────────────────
const unsafeSources = [
  ["docs/implement.md", implementMd],
  ...SUB_SKILL_FILES.map((path) => [path, readFile(path)]),
].filter(([, content]) => content && hasUnsafeRollback(content));

check(
  "safe-rollback",
  10,
  unsafeSources.length === 0,
  unsafeSources.length === 0
    ? "No repository-wide rollback commands found"
    : `Unsafe rollback guidance found in: ${unsafeSources.map(([path]) => path).join(", ")}`
);

// ─── Check 11: Sub-skills installed ────────────────────────────
// Only `iterate` is always required in fallback mode. When a manifest exists,
// its declared sub-skills become the single source of truth.
const REQUIRED_SUB_SKILLS = manifestDriven ? manifestSubSkillNames : ["iterate"];
const OPTIONAL_SUB_SKILLS = manifestDriven
  ? ALL_STANDARD_SUB_SKILLS.filter((skill) => !manifestSubSkillNames.includes(skill))
  : [
      "quality-audit",
      "milestone-promote",
    ];
const skillsDir = join(projectDir, ".agents", "skills");

let requiredInstalled = 0;
const missingRequired = [];
for (const skill of REQUIRED_SUB_SKILLS) {
  const skillPath = join(skillsDir, skill, "SKILL.md");
  if (existsSync(skillPath)) {
    requiredInstalled++;
  } else {
    missingRequired.push(skill);
  }
}

let optionalInstalled = 0;
for (const skill of OPTIONAL_SUB_SKILLS) {
  if (existsSync(join(skillsDir, skill, "SKILL.md"))) optionalInstalled++;
}

check(
  "sub-skills-installed",
  10,
  requiredInstalled === REQUIRED_SUB_SKILLS.length,
  requiredInstalled === REQUIRED_SUB_SKILLS.length
    ? `Required sub-skill(s) installed; ${optionalInstalled} optional sub-skills also present`
    : `Missing required sub-skills: ${missingRequired.join(", ")}`
);

// ─── Check 12: Taxonomy has prevention actions (protocol compliance) ────
const feedbackProfileExpected = !manifestDriven || manifestDocumentFiles.includes(FEEDBACK_PROFILE_PATH);
if (feedbackProfile) {
  const surfaces = feedbackProfile.feedback_surfaces ?? {};
  const hasAllSurfaces = [
    "static_constraint",
    "contract",
    "runtime",
    "business_flow",
  ].every((key) => typeof surfaces[key] === "object" && surfaces[key] !== null);
  const hasBusinessFlows = Array.isArray(feedbackProfile.business_flows);
  const hasHumanInputs = Array.isArray(feedbackProfile.human_inputs);
  const hasPriorityGaps = Array.isArray(feedbackProfile.priority_gaps);
  const hasManifestPath = feedbackProfile.manifest_path === MANIFEST_PATH;
  const validFlowClassifications = (feedbackProfile.business_flows ?? []).every((flow) =>
    ["repo-confirmed", "inferred", "human-confirmed"].includes(flow?.classification)
  );
  const manifestPointsToFeedbackProfile = !manifestDriven || harnessManifest?.feedback_profile_path === FEEDBACK_PROFILE_PATH;
  const notes = [];
  if (!hasAllSurfaces) notes.push("Missing one or more feedback surface objects");
  if (!hasBusinessFlows) notes.push("Missing business_flows array");
  if (!hasHumanInputs) notes.push("Missing human_inputs array");
  if (!hasPriorityGaps) notes.push("Missing priority_gaps array");
  if (!hasManifestPath) notes.push("manifest_path must be docs/harness-manifest.json");
  if (!validFlowClassifications) notes.push("business_flows classifications must be repo-confirmed, inferred, or human-confirmed");
  if (!manifestPointsToFeedbackProfile) notes.push(`${MANIFEST_PATH} must point to ${FEEDBACK_PROFILE_PATH}`);

  check(
    "feedback-profile-structure",
    10,
    notes.length === 0,
    notes.length === 0
      ? "Feedback profile structure is complete"
      : notes.join("; ")
  );
} else {
  check(
    "feedback-profile-structure",
    10,
    !feedbackProfileExpected,
    feedbackProfileExpected
      ? `${FEEDBACK_PROFILE_PATH} does not exist`
      : `${FEEDBACK_PROFILE_PATH} not required by the harness manifest`
  );
}

const taxonomyMd = readFile("docs/failures/taxonomy.md");
const taxonomyExpected = !manifestDriven || manifestDocumentFiles.includes("docs/failures/taxonomy.md");
if (taxonomyMd) {
  const hasPrevention = /[Pp]revention action/i.test(taxonomyMd);
  check(
    "taxonomy-prevention",
    5,
    hasPrevention,
    hasPrevention
      ? "Failure taxonomy includes prevention action protocol"
      : "Failure taxonomy missing prevention action field"
  );
} else {
  check(
    "taxonomy-prevention",
    5,
    !taxonomyExpected,
    taxonomyExpected
      ? "docs/failures/taxonomy.md does not exist"
      : "docs/failures/taxonomy.md not required by the harness manifest"
  );
}

// ─── Compute final score ───────────────────────────────────────────
results.score = totalWeight > 0 ? Math.round((earnedWeight / totalWeight) * 100) : 0;

// ─── Output ────────────────────────────────────────────────────────
console.log(JSON.stringify(results, null, 2));
process.exit(results.overall_pass ? 0 : 1);
