#!/usr/bin/env bash
set -euo pipefail

skill_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
skill_file="$skill_root/SKILL.md"

node - "$skill_root" "$skill_file" <<'NODE'
const fs = require("node:fs");
const path = require("node:path");
const { spawnSync } = require("node:child_process");
const { pathToFileURL } = require("node:url");

const [skillRoot, skillFile] = process.argv.slice(2);

const requiredRepoHelperPaths = [
  "scripts/docs-drift-check.mjs",
  "scripts/skill-runtime-scenarios.mjs"
];

const findRepoRootFrom = (startPath) => {
  if (!startPath) return undefined;
  let current = path.resolve(startPath);
  while (true) {
    if (requiredRepoHelperPaths.every((relPath) => fs.existsSync(path.join(current, relPath)))) {
      return current;
    }
    const parent = path.dirname(current);
    if (parent === current) return undefined;
    current = parent;
  }
};

const resolveRepoRoot = () => {
  const candidates = [
    process.env.OPENDEVBROWSER_REPO_ROOT,
    path.resolve(skillRoot, "../.."),
    process.cwd()
  ];
  const seen = new Set();
  for (const candidate of candidates) {
    if (!candidate) continue;
    const resolved = path.resolve(candidate);
    if (seen.has(resolved)) continue;
    seen.add(resolved);
    const repoRoot = findRepoRootFrom(resolved);
    if (repoRoot) return repoRoot;
  }
  return undefined;
};

const repoRoot = resolveRepoRoot();

const requiredPaths = [
  "artifacts/provider-workflows.md",
  "artifacts/parity-gates.md",
  "artifacts/debug-trace-playbook.md",
  "artifacts/fingerprint-tiers.md",
  "artifacts/macro-workflows.md",
  "artifacts/browser-agent-known-issues-matrix.md",
  "artifacts/command-channel-reference.md",
  "artifacts/canvas-governance-playbook.md",
  "artifacts/skill-runtime-surface-matrix.md",
  "assets/templates/mode-flag-matrix.json",
  "assets/templates/ops-request-envelope.json",
  "assets/templates/cdp-forward-envelope.json",
  "assets/templates/robustness-checklist.json",
  "assets/templates/surface-audit-checklist.json",
  "assets/templates/skill-runtime-pack-matrix.json",
  "assets/templates/canvas-handshake-example.json",
  "assets/templates/canvas-generation-plan.v1.json",
  "assets/templates/canvas-feedback-eval.json",
  "assets/templates/canvas-blocker-checklist.json",
  "scripts/odb-workflow.sh",
  "scripts/run-robustness-audit.sh",
  "scripts/validate-skill-assets.sh"
];

const internalPaths = [
  "scripts/resolve-odb-cli.sh",
  "scripts/validator-fixture-cli.sh"
];

const jsonTemplates = [
  "assets/templates/mode-flag-matrix.json",
  "assets/templates/ops-request-envelope.json",
  "assets/templates/cdp-forward-envelope.json",
  "assets/templates/robustness-checklist.json",
  "assets/templates/surface-audit-checklist.json",
  "assets/templates/skill-runtime-pack-matrix.json",
  "assets/templates/canvas-handshake-example.json",
  "assets/templates/canvas-generation-plan.v1.json",
  "assets/templates/canvas-feedback-eval.json",
  "assets/templates/canvas-blocker-checklist.json"
];

const executableScripts = [
  "scripts/odb-workflow.sh",
  "scripts/run-robustness-audit.sh",
  "scripts/validate-skill-assets.sh",
  "scripts/resolve-odb-cli.sh",
  "scripts/validator-fixture-cli.sh"
];

const workflowMarkers = [
  "inspiredesign)",
  "inspiredesign run",
  "inspiredesign harvest",
  "social/pinterest",
  "meta.discovery.acceptedUrls",
  "Multi-pin Pinterest harvest agent pattern",
  "one canonical harvest per selected pin",
  "opens the exact canonical pin in the active managed or extension workflow session",
  "productSuccess=true",
  "artifactAuthority=product_ready",
  "evidenceAuthority=pin_media_ready",
  "nextStepGuidance.readiness",
  "visual-evidence.json",
  "pin-media-index.json",
  "manifest-backed pin-media-index.json",
  "media-analysis.json",
  "deterministic design facts from trusted saved pin media",
  "raw mediaAnalysis in canvas-plan.request.json",
  "--include-prototype-guidance",
  "canvas-preflight",
  "canvas-feedback-eval",
  "release-direct-gates",
  "skill-runtime-audit",
  "tests/skill-loader.test.ts",
  "tests/skill-list-tool.test.ts",
  "tests/cli-skills-installer.test.ts",
  "tests/cli-help-parity.test.ts",
  "npx opendevbrowser --help",
  "npx opendevbrowser help",
  "./skills/opendevbrowser-continuity-ledger/scripts/validate-skill-assets.sh",
  "resolve-odb-cli.sh",
  "./skills/opendevbrowser-motion-design/scripts/validate-skill-assets.sh",
  "CLI_PREFIX"
];

const parityGatePath = "artifacts/parity-gates.md";
const parityGateMarkers = [
  "Contract gates vs live release proof",
  "`tests/parity-matrix.test.ts` is contract coverage",
  "node scripts/provider-direct-runs.mjs --release-gate --out artifacts/release/vX.Y.Z/provider-direct-runs.json",
  "node scripts/live-regression-direct.mjs --release-gate --out artifacts/release/vX.Y.Z/live-regression-direct.json",
  "docs/RELEASE_RUNBOOK.md"
];

const providerWorkflowPath = "artifacts/provider-workflows.md";
const providerWorkflowMarkers = [
  "Agent Pattern F: Pinterest Multi-Pin Design Harvest",
  "meta.discovery.acceptedUrls",
  "not a new product workflow",
  "opens the exact pin in the active managed or extension workflow session",
  "productSuccess=true",
  "artifactAuthority=product_ready",
  "evidenceAuthority=pin_media_ready",
  "media-analysis.json",
  "manifest-backed authority from `pin-media-index.json`",
  "`pin-media-index.json` remains the only pin-media readiness authority",
  "raw `mediaAnalysis` in `canvas-plan.request.json`",
  "## Release evidence lane",
  "node scripts/provider-direct-runs.mjs --release-gate --out artifacts/release/vX.Y.Z/provider-direct-runs.json",
  "node scripts/live-regression-direct.mjs --release-gate --out artifacts/release/vX.Y.Z/live-regression-direct.json",
  "contract coverage, not live release proof"
];

const canvasPlaybookMarkers = [
  "canvas.session.open",
  "canvas.plan.set",
  "canvas.feedback.poll",
  "guidance.recommendedNextCommands",
  "guidance.nextStepGuidance",
  "guidance.paramsExamples",
  "generationPlanIssues",
  "plan_invalid",
  "generation_plan_invalid",
  "CANVAS-01",
  "CANVAS-07"
];

const canvasHandshakeMarkers = [
  "\"planStatus\": \"missing\"",
  "\"allowedValues\"",
  "\"warningClasses\"",
  "\"generationPlanIssues\"",
  "\"mutationPolicy\"",
  "\"recommendedNextCommands\"",
  "\"reason\": \"Handshake is complete. Submit a complete generationPlan before mutation.\""
];

const canvasTemplateMarkers = [
  "CANVAS-01",
  "CANVAS-02",
  "CANVAS-03",
  "CANVAS-04",
  "CANVAS-05",
  "CANVAS-06",
  "CANVAS-07"
];

const templateMarkerPaths = [
  "assets/templates/canvas-feedback-eval.json",
  "assets/templates/canvas-blocker-checklist.json",
  "assets/templates/robustness-checklist.json"
];

const surfaceAuditPath = "assets/templates/surface-audit-checklist.json";

const failures = [];

if (!repoRoot) {
  failures.push(
    `Unable to locate OpenDevBrowser repo helper scripts from ${skillRoot}. Run this validator from the repository root or set OPENDEVBROWSER_REPO_ROOT.`
  );
}

const readUtf8 = (relPath) => fs.readFileSync(path.join(skillRoot, relPath), "utf8");
const hasMarker = (content, marker) => content.includes(marker);
const requireResearchFixtureRejectsDocsSource = () => {
  const fixturePath = path.join(skillRoot, "scripts/validator-fixture-cli.sh");
  if (!fs.existsSync(fixturePath)) return;
  const fixture = fs.readFileSync(fixturePath, "utf8");
  const staleSourcePair = ["web", "docs"].join(",");
  if (fixture.includes(staleSourcePair)) {
    failures.push("Validator fixture contains stale concrete research source pair.");
  }
  const result = spawnSync(
    fixturePath,
    ["research", "run", "--topic", "fixture topic", "--mode", "context", "--sources", "docs"],
    { encoding: "utf8" }
  );
  const combinedOutput = `${result.stdout || ""}\n${result.stderr || ""}`;
  if (result.status === 0) {
    failures.push("Validator fixture accepted invalid concrete research source: docs.");
  } else if (!combinedOutput.includes("Invalid --sources value: docs")) {
    failures.push("Validator fixture invalid source rejection missing expected diagnostics.");
  }
};
(async () => {
  if (!repoRoot) {
    console.error(failures.join("\n"));
    process.exit(1);
  }
  const { getSurfaceCounts } = await import(pathToFileURL(path.join(repoRoot, "scripts", "docs-drift-check.mjs")).href);
  const { getBundledSkillDirectoryPackIds } = await import(pathToFileURL(path.join(repoRoot, "scripts", "skill-runtime-scenarios.mjs")).href);
  const { commandCount, toolCount, opsCommandCount, canvasCommandCount } = getSurfaceCounts();

  const skillDocMarkers = [
    "npx opendevbrowser --help",
    "npx opendevbrowser help",
    "npx opendevbrowser inspiredesign run",
    "./skills/opendevbrowser-best-practices/scripts/odb-workflow.sh inspiredesign",
    `${commandCount} CLI commands, ${toolCount} tools, ${opsCommandCount} \`/ops\` commands, ${canvasCommandCount} \`/canvas\` commands`,
    "Treat `tests/parity-matrix.test.ts` as contract coverage only.",
    "canonical owner of direct-run release evidence policy",
    "Skill Runtime Audit and Realignment",
    "artifacts/skill-runtime-surface-matrix.md",
    "assets/templates/skill-runtime-pack-matrix.json",
    "mutationPolicy.allowedBeforePlan",
    "planStatus",
    "plan_invalid",
    "generationPlanIssues",
    "guidance.recommendedNextCommands",
    "generation_plan_invalid",
    "canvas.code.bind",
    "canvas.code.resolve",
    "bound_app_runtime",
    "annotate --stored"
  ];

  const commandRefMarkers = [
    `CLI commands: \`${commandCount}\``,
    `Plugin tools: \`${toolCount}\``,
    `\`/ops\` command names: \`${opsCommandCount}\``,
    `\`/canvas\` command names: \`${canvasCommandCount}\``,
    "docs/SURFACE_REFERENCE.md",
    "npx opendevbrowser help",
    "pointer-move",
    "pointer.*",
    "canvas.applyRuntimePreviewBridge",
    "canvas.session.open",
    "canvas.session.attach",
    "canvas.feedback.poll",
    "canvas.feedback.next",
    "canvas.feedback.unsubscribe",
    "canvas.code.bind",
    "canvas.code.resolve",
    "canvas_history_requested",
    "canvas.tab.sync",
    "canvas.overlay.sync",
    "feedback.heartbeat",
    "governanceRequirements",
    "generationPlanRequirements",
    "allowedValues",
    "mutationPolicy",
    "planStatus",
    "plan_invalid",
    "generationPlanIssues",
    "warningClasses",
    "recommendedNextCommands",
    "generation_plan_invalid",
    "code-sync",
    "parity",
    "plan_required",
    "annotation:sendPayload",
    "AgentInbox",
    "annotate --stored",
    "storage",
    "opencode",
    "codex",
    "claudecode",
    "ampcli",
    "agents"
  ];

  const surfaceAuditMarkers = [
    `"cliCommands": ${commandCount}`,
    `"tools": ${toolCount}`,
    `"opsCommands": ${opsCommandCount}`,
    `"canvasCommands": ${canvasCommandCount}`,
    "\"docs/README.md\"",
    "\"docs/FIRST_RUN_ONBOARDING.md\"",
    "\"firstContactOwners\"",
    "Pointer controls documented",
    "Ops pointer and preview namespaces documented",
    "Canvas command names documented",
    "Canvas code-sync surface documented",
    "Annotation send/copy semantics documented",
    "Generated help first-contact owners documented",
    "Onboarding metadata owner documented",
    "First-run onboarding proof owner documented"
  ];

  for (const relPath of requiredPaths) {
    if (!fs.existsSync(path.join(skillRoot, relPath))) {
      failures.push(`Missing required asset: ${relPath}`);
    }
  }

  for (const relPath of internalPaths) {
    if (!fs.existsSync(path.join(skillRoot, relPath))) {
      failures.push(`Missing internal helper: ${relPath}`);
    }
  }

  for (const relPath of executableScripts) {
    const fullPath = path.join(skillRoot, relPath);
    if (!fs.existsSync(fullPath)) continue;
    const mode = fs.statSync(fullPath).mode & 0o111;
    if (mode === 0) {
      failures.push(`Script is not executable: ${relPath}`);
    }
  }

  for (const relPath of jsonTemplates) {
    const fullPath = path.join(skillRoot, relPath);
    if (!fs.existsSync(fullPath)) continue;
    try {
      JSON.parse(fs.readFileSync(fullPath, "utf8"));
    } catch {
      failures.push(`Invalid JSON template: ${relPath}`);
    }
  }

  requireResearchFixtureRejectsDocsSource();

  const packMatrixPath = path.join(skillRoot, "assets/templates/skill-runtime-pack-matrix.json");
  if (fs.existsSync(packMatrixPath)) {
    const packMatrix = JSON.parse(fs.readFileSync(packMatrixPath, "utf8"));
    const packIds = Array.isArray(packMatrix.canonicalPacks)
      ? packMatrix.canonicalPacks.map((entry) => entry?.packId).filter((value) => typeof value === "string")
      : [];
    const uniquePackIds = new Set(packIds);
    const bundledSkillNames = getBundledSkillDirectoryPackIds();
    const bundledSkillNameSet = new Set(bundledSkillNames);
    const auditDomainIds = Array.isArray(packMatrix.auditDomains)
      ? packMatrix.auditDomains.map((entry) => entry?.id).filter((value) => typeof value === "string")
      : [];
    const uniqueAuditDomainIds = new Set(auditDomainIds);
    if (packIds.length === 0 || uniquePackIds.size !== packIds.length) {
      failures.push(`Skill runtime pack matrix must contain non-empty unique canonical packs; found ${uniquePackIds.size}/${packIds.length}.`);
    }
    if (bundledSkillNames.length === 0) {
      failures.push("Bundled skill registry did not expose any canonical pack names.");
    }
    if (packIds.some((packId) => !bundledSkillNameSet.has(packId)) || bundledSkillNames.some((packId) => !uniquePackIds.has(packId))) {
      failures.push("Skill runtime pack matrix canonical packs must match src/skills/bundled-skill-directories.ts.");
    }
    if (auditDomainIds.length === 0 || uniqueAuditDomainIds.size !== auditDomainIds.length) {
      failures.push(`Skill runtime pack matrix must contain non-empty unique audit domains; found ${uniqueAuditDomainIds.size}/${auditDomainIds.length}.`);
    }
    if (!Array.isArray(packMatrix.runtimeFamilies) || packMatrix.runtimeFamilies.length === 0) {
      failures.push("Skill runtime pack matrix missing runtimeFamilies.");
    }
  }

  const skillDoc = fs.readFileSync(skillFile, "utf8");
  for (const relPath of requiredPaths) {
    if (!hasMarker(skillDoc, relPath)) {
      failures.push(`SKILL.md missing reference: ${relPath}`);
    }
  }
  for (const marker of skillDocMarkers) {
    if (!hasMarker(skillDoc, marker)) {
      failures.push(`SKILL.md missing marker: ${marker}`);
    }
  }

  const runtimeMatrixPath = "artifacts/skill-runtime-surface-matrix.md";
  if (fs.existsSync(path.join(skillRoot, runtimeMatrixPath))) {
    const runtimeMatrix = readUtf8(runtimeMatrixPath);
    const packMatrix = JSON.parse(fs.readFileSync(path.join(skillRoot, "assets/templates/skill-runtime-pack-matrix.json"), "utf8"));
    const runtimeMatrixMarkers = [
      ...packMatrix.canonicalPacks.map((entry) => entry.packId),
      "Shared runtime families",
      ...packMatrix.runtimeFamilies.map((entry) => entry.label),
      "Audit domains",
      ...packMatrix.auditDomains.map((entry) => entry.id)
    ];
    for (const marker of runtimeMatrixMarkers) {
      if (!hasMarker(runtimeMatrix, marker)) {
        failures.push(`Skill runtime surface matrix missing marker: ${marker}`);
      }
    }
  }

  const commandRefPath = "artifacts/command-channel-reference.md";
  if (fs.existsSync(path.join(skillRoot, commandRefPath))) {
    const commandRef = readUtf8(commandRefPath);
    for (const marker of commandRefMarkers) {
      if (!hasMarker(commandRef, marker)) {
        failures.push(`Command/channel reference missing marker: ${marker}`);
      }
    }
  }

  const workflowPath = "scripts/odb-workflow.sh";
  if (fs.existsSync(path.join(skillRoot, workflowPath))) {
    const workflow = readUtf8(workflowPath);
    for (const marker of workflowMarkers) {
      if (!hasMarker(workflow, marker)) {
        failures.push(`Workflow router missing marker: ${marker}`);
      }
    }
  }

  if (fs.existsSync(path.join(skillRoot, parityGatePath))) {
    const parityGateDoc = readUtf8(parityGatePath);
    for (const marker of parityGateMarkers) {
      if (!hasMarker(parityGateDoc, marker)) {
        failures.push(`Parity gates doc missing marker: ${marker}`);
      }
    }
  }

  if (fs.existsSync(path.join(skillRoot, providerWorkflowPath))) {
    const providerWorkflowDoc = readUtf8(providerWorkflowPath);
    for (const marker of providerWorkflowMarkers) {
      if (!hasMarker(providerWorkflowDoc, marker)) {
        failures.push(`Provider workflows doc missing marker: ${marker}`);
      }
    }
  }

  const canvasPlaybookPath = "artifacts/canvas-governance-playbook.md";
  if (fs.existsSync(path.join(skillRoot, canvasPlaybookPath))) {
    const canvasPlaybook = readUtf8(canvasPlaybookPath);
    for (const marker of canvasPlaybookMarkers) {
      if (!hasMarker(canvasPlaybook, marker)) {
        failures.push(`Canvas playbook missing marker: ${marker}`);
      }
    }
  }

  const canvasHandshakePath = "assets/templates/canvas-handshake-example.json";
  if (fs.existsSync(path.join(skillRoot, canvasHandshakePath))) {
    const canvasHandshake = readUtf8(canvasHandshakePath);
    for (const marker of canvasHandshakeMarkers) {
      if (!hasMarker(canvasHandshake, marker)) {
        failures.push(`Canvas handshake example missing marker: ${marker}`);
      }
    }
  }

  if (fs.existsSync(path.join(skillRoot, surfaceAuditPath))) {
    const surfaceAudit = readUtf8(surfaceAuditPath);
    for (const marker of surfaceAuditMarkers) {
      if (!hasMarker(surfaceAudit, marker)) {
        failures.push(`Surface audit checklist missing marker: ${marker}`);
      }
    }
  }

  for (const relPath of templateMarkerPaths) {
    const fullPath = path.join(skillRoot, relPath);
    if (!fs.existsSync(fullPath)) continue;
    const content = fs.readFileSync(fullPath, "utf8");
    for (const marker of canvasTemplateMarkers) {
      if (!hasMarker(content, marker)) {
        failures.push(`Canvas template missing marker (${marker}): ${relPath}`);
      }
    }
  }

  if (failures.length > 0) {
    for (const failure of failures) {
      console.error(failure);
    }
    process.exit(1);
  }

  console.log(`Skill assets validated: ${requiredPaths.length} files referenced/present, ${jsonTemplates.length} JSON templates parsed.`);
})().catch((error) => {
  console.error(error instanceof Error ? error.message : String(error));
  process.exit(1);
});
NODE
