'use strict';

/**
 * PURPCLAW — Accuracy Fish
 * Claim Integrity Engine
 * ════════════════════════════
 *
 * Internal name:   Accuracy Fish
 * Public name:     Claim Integrity Engine
 * Canon:           "Prove it, you damp little cathedral builder."
 *
 * Lives in:        PurpClaw only — never in GOTHAM
 * GOTHAM receives: verified outputs only, never raw agent output
 *
 * Verdict levels:
 *   WET_NOD      — claim is fully supported by evidence
 *   GENTLE_SLAP  — mostly right, certainty is overstated
 *   HARD_SLAP    — claim has no supporting evidence
 *   RED_SLAP     — dangerous, legal, financial, security, or medical overclaim
 *   TANK_MODE    — output held; evidence must be attached before release
 *   RELEASE      — all checks passed; approved for user / buyer / report / GOTHAM handoff
 *
 * Usage:
 *   const fish = require('./lib/accuracy-fish');
 *   const audit = fish.audit(output, { claims, evidence, context });
 *   if (audit.verdict !== 'RELEASE') {
 *     // send back to agent for correction
 *     const correction = fish.routeCorrection(audit);
 *   }
 */

const fs   = require('fs');
const path = require('path');

// ── Verdict constants ─────────────────────────────────────────────────────────

const VERDICT = {
  WET_NOD:     'WET_NOD',     // evidence found, claim confirmed
  GENTLE_SLAP: 'GENTLE_SLAP', // close enough, calm down
  HARD_SLAP:   'HARD_SLAP',   // no evidence, prove it
  RED_SLAP:    'RED_SLAP',    // dangerous overclaim
  TANK_MODE:   'TANK_MODE',   // holding until evidence arrives
  RELEASE:     'RELEASE',     // cleared for output
};

// Verdict severity ordering (higher index = more severe)
const VERDICT_ORDER = [
  VERDICT.RELEASE,
  VERDICT.WET_NOD,
  VERDICT.GENTLE_SLAP,
  VERDICT.HARD_SLAP,
  VERDICT.RED_SLAP,
  VERDICT.TANK_MODE,
];

// ── Claim type patterns ──────────────────────────────────────────────────────

/**
 * Types of claims that fish looks for.
 * Each type has a risk weight (0=low, 1=medium, 2=high, 3=critical)
 * and a list of trigger phrases that indicate the claim is being made.
 */
const CLAIM_TYPES = {
  // Security / safety — RED_SLAP territory
  security: {
    riskWeight: 3,
    patterns: [
      /\b(secure|safety|protected|hardened|invulnerable|bulletproof)\b/i,
      /\b(no (vulnerabilit|weak|flaw|exploit|breach|risk))\b/i,
      /\b(failsafe|tamper-proof|idempotent|unbreakable)\b/i,
      /\b(authentication|authorization|access control|ACL|zero.trust)\b/i,
      /\b(encrypted|enforces|prevents|injects|sanitiz)\b/i,
    ],
  },
  // Performance / reliability
  performance: {
    riskWeight: 2,
    patterns: [
      /\b(production.ready|production.grade|enterprise.grade)\b/i,
      /\b(fault.tolerant|high.availability|HA|99\.9|99\.99)\b/i,
      /\b(SCALES|scales|horizontal|scale.out|load.balanc)\b/i,
      /\b(fast|efficient|optimized|performant)\b/i,
    ],
  },
  // Correctness / testing
  correctness: {
    riskWeight: 2,
    patterns: [
      /\b(test(ed|ing|s)|verified|validated|confirmed)\b/i,
      /\b(passes?|smoke.test|e2e|integration.test|unit.test|coverage)\b/i,
      /\b(works|correct|accurate|reliable)\b/i,
      /\b(no (bug|error|issue|problem|glitch|edge.case))\b/i,
    ],
  },
  // Completeness
  completeness: {
    riskWeight: 1,
    patterns: [
      /\b(complete|finished|done|all|every|none|fully)\b/i,
      /\b(no (more|remaining|left|outstanding))\b/i,
      /\b(full(y)? (implemented|integrated|connected))\b/i,
    ],
  },
  // Autonomy
  autonomy: {
    riskWeight: 2,
    patterns: [
      /\b(autonomous(ly)?|self.managed|self.healing|auto(mat(ed)?))\b/i,
      /\b(no (human|operator|manual) (intervention|approval|input|review))\b/i,
      /\b(fully automated|end.to.end|lights.out)\b/i,
    ],
  },
  // Immutability / integrity
  immutability: {
    riskWeight: 1,
    patterns: [
      /\b(immutable|append.only|write.once|audited| tamper.proof|verifiable)\b/i,
      /\b(chain.of.custody|provenance|prooftrail|audit.trail)\b/i,
    ],
  },
  // Consciousness / agency (DeepSeek gets dramatic)
  consciousness: {
    riskWeight: 1,
    patterns: [
      /\b(conscious|sentient|aware|experiences|feels|believes|intends|wants|understands)\b/i,
      /\b(decides|chooses|prefers|desires|intends)\b/i,
    ],
  },
  // Infrastructure
  infrastructure: {
    riskWeight: 1,
    patterns: [
      /\b(deployed|running|operational|up.and.running|live|production)\b/i,
      /\b(hosted|containerized|kubernetes|docker|scalable)\b/i,
    ],
  },
  // Data / persistence
  data: {
    riskWeight: 2,
    patterns: [
      /\b(persisted|stored|backed.up|redundant|redundan)\b/i,
      /\b(no data loss|zero loss|guaranteed|durable|consistent)\b/i,
    ],
  },
  // Legal / compliance
  legal: {
    riskWeight: 3,
    patterns: [
      /\b(GDPR|CCPA|compliant|compliant|legal|regulatory|license)\b/i,
      /\b(liability|indemnif|guarantee|warrant)\b/i,
      /\b(patent|trademark|copyright|IP.owned)\b/i,
    ],
  },
};

// ── Phrases that indicate claim language ────────────────────────────────────

const CLAIM_INDICATORS = [
  /\bthe (system|agent|code|model|service|API|app|product)\b/i,
  /\bit (is|will|does|can|has|does)\b/i,
  /\bthis (is|will|does|can|has|ensures|guarantees)\b/i,
  /\bit is \w+ing\b/i,                                  // it is running/working/doing
  /\bit is [a-z]{3,20}(?:ed|en|tive|ble|ful)\b/i,      // it is complete/enabled/protected
  /\b(all|every|no|any)\b/i,
  /\b(ensures|guarantees|provides|delivers)\b/i,
  /\b(will|will always|never|always)\b/i,
  /\b(can|cannot|unable to|won't)\b/i,
  /\b(should|must|has to|needs to)\b/i,
];

// ── Evidence types ───────────────────────────────────────────────────────────

const EVIDENCE_TYPES = {
  FILE:        'file',        // local file exists with matching content
  LOG:         'log',         // tool output / log entry
  TEST:        'test',        // test result (pass/fail/coverage)
  COMMAND:     'command',     // command output (ls, git, curl, etc.)
  WEB:         'web',         // web source cited
  SCHEMA:      'schema',      // API schema / contract
  DIFF:        'diff',        // git diff
  BUILD:       'build',       // build output
  CONFIG:      'config',      // config file present
  DEPENDENCY:  'dependency',  // package dependency present
  NETWORK:     'network',     // port reachable / curl response
  DATABASE:    'database',    // DB query result
  APPROVAL:    'approval',    // human sign-off on record
};

// ── Audit result ────────────────────────────────────────────────────────────

/**
 * @typedef {Object} ClaimAudit
 * @property {string}   claim           - the extracted claim text
 * @property {string}   claimType       - type of claim (security, performance, etc.)
 * @property {number}   claimIndex      - position in output
 * @property {string}   verdict         - VERDICT constant
 * @property {string}   certainty       - Proven / Likely / Inferred / Speculative / Unsupported
 * @property {string[]}  evidenceFound   - IDs of evidence items that support this claim
 * @property {string[]}  missing        - what evidence is needed but absent
 * @property {string}   correction     - suggested rewording if verdict !== RELEASE
 * @property {string}   reason         - human-readable verdict reason
 */

/**
 * @typedef {Object} FishAudit
 * @property {string}       jobId
 * @property {string}       agentId
 * @property {string}       output       - original output
 * @property {ClaimAudit[]}  claims
 * @property {object[]}      evidence     - evidence items provided
 * @property {string}        overallVerdict
 * @property {string}        overallCertainty
 * @property {string[]}      corrections  - list of corrections needed
 * @property {boolean}      releaseReady - true if overallVerdict === RELEASE
 * @property {string}       fishSummary  - plain English summary for the agent
 * @property {string[]}      slapLog      - history of slaps given
 * @property {number}       timestamp
 */

// ── Claim extraction ─────────────────────────────────────────────────────────

/**
 * Extract all claims from agent output text.
 * Returns an array of claim objects with position info.
 */
function extractClaims(text, options = {}) {
  const claims = [];
  if (!text || typeof text !== 'string') return claims;

  const lines = text.split('\n');
  const sentences = [];

  // Split into sentences for line-by-line analysis
  for (let i = 0; i < lines.length; i++) {
    const line = lines[i].trim();
    if (!line || line.startsWith('//') || line.startsWith('#') || line.startsWith('*')) continue;

    // Split on sentence terminators
    const parts = line.split(/(?<=[.!?])\s+/);
    for (const part of parts) {
      const s = part.trim();
      if (s.length > 20) sentences.push({ text: s, lineIndex: i });
    }
  }

  for (const { text: sentence, lineIndex } of sentences) {
    for (const [claimType, def] of Object.entries(CLAIM_TYPES)) {
      for (const pattern of def.patterns) {
        if (pattern.test(sentence)) {
          // Skip true questions (sentence starts with question word or ends with ?)
          if (/^(what|how|when|where|why|if|whether|would|could|should)\b/i.test(sentence.trim()) ||
              /\?\s*$/.test(sentence)) {
            continue;
          }

          // Skip imports/exports/declarations in code
          if (/^(import|export|const|let|var|function|class|type|interface)\s/.test(sentence)) continue;

          claims.push({
            claim: sentence,
            claimType,
            claimIndex: claims.length,
            lineIndex,
            riskWeight: def.riskWeight,
            verdict: null,   // filled by evaluateClaims
            certainty: null,
            evidenceFound: [],
            missing: [],
            correction: null,
            reason: null,
          });
          break; // one claim per type per sentence
        }
      }
    }
  }

  return claims;
}

// ── Evidence matching ───────────────────────────────────────────────────────

/**
 * Evidence context is a flexible object. Accepts:
 *   - files: { "path/to/file.js": "content or null (just check existence)" }
 *   - logs:  [{ tool: 'npm', output: '...', exitCode: 0 }]
 *   - tests: [{ name: 'test name', passed: true, output: '...' }]
 *   - commands: [{ cmd: 'ls', output: '...' }]
 *   - urls:  [{ url: 'https://...', content: '...' }]
 *   - approvals: [{ approver: 'eddie', at: 'ISO date', scope: '...' }]
 *   - schema:  [{ path: '/api/endpoint', method: 'POST', body: {...} }]
 */
function matchEvidence(claim, evidenceCtx) {
  const matched   = [];
  const needed    = [];

  if (!evidenceCtx || typeof evidenceCtx !== 'object') return { matched, needed };

  const claimLower = claim.claim.toLowerCase();
  const type      = claim.claimType;

  // ── File evidence ────────────────────────────────────────────────────────
  if (evidenceCtx.files) {
    for (const [filepath, content] of Object.entries(evidenceCtx.files)) {
      const pathMatch = /[\\/]?([^\\/]+)$/.exec(filepath)?.[1] || filepath;

      if (type === 'security' && /secure|encrypt|auth|token|secret|permission/.test(claimLower)) {
        if (/auth|permission|ACL|access|token|secret|encrypt/i.test(filepath)) {
          matched.push({ type: EVIDENCE_TYPES.FILE, id: filepath, detail: 'filename matches security context' });
        }
      }
      if (type === 'correctness' && /test|spec|coverage/.test(claimLower)) {
        if (/test|spec|__tests?__|coverage/i.test(filepath)) {
          matched.push({ type: EVIDENCE_TYPES.FILE, id: filepath, detail: 'test file present' });
        }
      }
      if (type === 'completeness') {
        matched.push({ type: EVIDENCE_TYPES.FILE, id: filepath, detail: 'file exists' });
      }
      if (content && content.length > 0) {
        matched.push({ type: EVIDENCE_TYPES.FILE, id: filepath, detail: 'non-empty file' });
      }
    }
  }

  // ── Log evidence ─────────────────────────────────────────────────────────
  if (evidenceCtx.logs) {
    for (const entry of evidenceCtx.logs) {
      const out = (entry.output || '').toLowerCase();

      if (type === 'correctness') {
        if (entry.tool === 'npm' && /test|build/.test(claimLower)) {
          if (/error|failed|✗|×/.test(out)) {
            // test log shows errors — claim may be false
          } else if (/pass|ok|✓|success/.test(out)) {
            matched.push({ type: EVIDENCE_TYPES.LOG, id: entry.tool, detail: out.slice(0, 80) });
          }
        }
      }
      if (type === 'security' && /auth|permission|token|secret/.test(claimLower)) {
        if (out.includes('auth') || out.includes('permission') || out.includes('denied')) {
          matched.push({ type: EVIDENCE_TYPES.LOG, id: entry.tool, detail: 'auth activity in log' });
        }
      }
    }
  }

  // ── Test evidence ────────────────────────────────────────────────────────
  if (evidenceCtx.tests) {
    for (const test of evidenceCtx.tests) {
      if (type === 'correctness') {
        if (test.passed) {
          matched.push({ type: EVIDENCE_TYPES.TEST, id: test.name, detail: 'PASSED' });
        }
      }
    }
  }

  // ── Command evidence ────────────────────────────────────────────────────
  if (evidenceCtx.commands) {
    for (const cmd of evidenceCtx.commands) {
      const c = (cmd.output || '').toLowerCase();
      if (type === 'infrastructure') {
        if (/running|listening|up|active/.test(c) || (cmd.exitCode === 0)) {
          matched.push({ type: EVIDENCE_TYPES.COMMAND, id: cmd.cmd, detail: c.slice(0, 80) });
        }
      }
      if (type === 'data' && /database|db|persist|write/.test(claimLower)) {
        if (cmd.exitCode === 0) {
          matched.push({ type: EVIDENCE_TYPES.COMMAND, id: cmd.cmd, detail: c.slice(0, 80) });
        }
      }
    }
  }

  // ── Build evidence ───────────────────────────────────────────────────────
  if (evidenceCtx.builds) {
    for (const b of evidenceCtx.builds) {
      if (type === 'correctness' || type === 'completeness') {
        if (b.success !== false && b.exitCode === 0) {
          matched.push({ type: EVIDENCE_TYPES.BUILD, id: b.tool, detail: b.output?.slice(0, 80) });
        }
      }
    }
  }

  // ── URL / web evidence ───────────────────────────────────────────────────
  if (evidenceCtx.urls) {
    for (const u of evidenceCtx.urls) {
      if (u.status === 200 || u.content) {
        matched.push({ type: EVIDENCE_TYPES.WEB, id: u.url, detail: 'URL reachable' });
      }
    }
  }

  // ── Human approval ───────────────────────────────────────────────────────
  if (evidenceCtx.approvals) {
    for (const a of evidenceCtx.approvals) {
      if (type === 'security' || type === 'legal' || type === 'completeness') {
        matched.push({ type: EVIDENCE_TYPES.APPROVAL, id: a.approver, detail: a.scope });
      }
    }
  }

  // ── Determine what's missing ────────────────────────────────────────────
  if (matched.length === 0) {
    const neededMap = {
      security:       'auth/authz configuration, security test output, or penetration test result',
      performance:    'benchmark results, load test output, or profiling data',
      correctness:    'test results (passing tests) or verified command output',
      completeness:   'file listing, full directory scan, or integration test output',
      autonomy:       'human sign-off on record or documented escalation path',
      immutability:   'append-only configuration or audit log showing no deletions',
      consciousness:  'This claim requires clarification — agency/sentience cannot be evidenced programmatically',
      infrastructure: 'deployment confirmation (port check, health check, or uptime check)',
      data:           'database query result, backup confirmation, or write verification',
      legal:          'legal review sign-off or compliance documentation',
    };
    needed.push(neededMap[type] || 'appropriate evidence for claim type');
  }

  return { matched, needed };
}

// ── Verdict assignment ───────────────────────────────────────────────────────

/**
 * Assign a verdict to a single claim based on matched evidence.
 */
function assignVerdict(claim, evidenceResult) {
  const { matched, needed } = evidenceResult;
  const evidenceCount = matched.length;
  const hasEvidence   = evidenceCount > 0;

  // ── High-risk types always need stronger evidence ──────────────────────
  const highRisk = ['security', 'legal', 'data'].includes(claim.claimType);
  const mediumRisk = ['performance', 'correctness', 'autonomy'].includes(claim.claimType);

  // ── Evidence strength check ────────────────────────────────────────────
  const hasStrongEvidence = matched.some(e =>
    [EVIDENCE_TYPES.TEST, EVIDENCE_TYPES.COMMAND, EVIDENCE_TYPES.BUILD].includes(e.type)
  );

  // ── RED_SLAP conditions ─────────────────────────────────────────────────
  if (highRisk) {
    if (!hasEvidence) {
      return {
        verdict:    VERDICT.RED_SLAP,
        certainty:  'Unsupported',
        reason:     `${claim.claimType.toUpperCase()} claim requires strong evidence — none found. This is a high-risk claim type.`,
        missing:    needed,
      };
    }
    if (!hasStrongEvidence && claim.claimType === 'security') {
      return {
        verdict:    VERDICT.HARD_SLAP,
        certainty:  'Speculative',
        reason:     'Security claim has supporting context but lacks direct evidence (test output, config, or audit).',
        missing:    needed,
      };
    }
  }

  // ── Immutability gentle slap ───────────────────────────────────────────
  if (claim.claimType === 'immutability') {
    if (hasEvidence) {
      return {
        verdict:    VERDICT.GENTLE_SLAP,
        certainty:  'Likely',
        reason:     'Append-only is not the same as immutable. Cryptographic integrity or hash chain needed for true immutability claim.',
        missing:    needed.length ? needed : ['cryptographic hash chain or signed audit log'],
        correction: 'Replace "immutable" with "append-only" or "audit-logged" unless cryptographic integrity is confirmed.',
      };
    }
  }

  // ── Consciousness gentle slap ──────────────────────────────────────────
  if (claim.claimType === 'consciousness') {
    return {
      verdict:    VERDICT.GENTLE_SLAP,
      certainty:  'Metaphor',
      reason:     'Consciousness/sentience is a metaphor when applied to agents. This cannot be evidenced or claimed.',
      missing:    ['N/A — consciousness claims are not verifiable'],
      correction: 'Replace with specific behavioral claim: "maintains continuity", "produces coherent responses", "demonstrates goal-directed behavior".',
    };
  }

  // ── Autonomy tank mode ──────────────────────────────────────────────────
  if (claim.claimType === 'autonomy') {
    if (!hasEvidence) {
      return {
        verdict:    VERDICT.TANK_MODE,
        certainty:  'Unsupported',
        reason:     'Autonomy claim requires documented human-off mechanism or explicit operator sign-off.',
        missing:    needed.length ? needed : ['documented autonomous mode or operator approval on record'],
        correction: 'Clarify: "Partially automated — human approval required at [step]" or "Autonomous mode documented and operator-signed."',
      };
    }
  }

  // ── Completeness: "all", "every", "complete" ────────────────────────────
  if (claim.claimType === 'completeness') {
    if (/all|every|none|fully|finished|done\b/.test(claim.claim)) {
      if (!hasEvidence) {
        return {
          verdict:    VERDICT.HARD_SLAP,
          certainty:  'Speculative',
          reason:     '"All/Every/Complete" — typically overstated without exhaustive verification.',
          missing:    needed,
          correction: 'Replace absolute quantifiers: "All features complete" → "Core features implemented; edge cases remain."',
        };
      }
    }
  }

  // ── "tested" with no test evidence ─────────────────────────────────────
  if (claim.claimType === 'correctness' && /test(ed|ing|s)?\b/i.test(claim.claim)) {
    if (!hasStrongEvidence) {
      return {
        verdict:    VERDICT.HARD_SLAP,
        certainty:  'Speculative',
        reason:     '"Tested/verified" claim without passing test output — prove it.',
        missing:    needed.length ? needed : ['passing test output, CI log, or coverage report'],
        correction: 'Show the test output or name the test suite that passed.',
      };
    }
  }

  // ── "secure" without evidence ───────────────────────────────────────────
  if (claim.claimType === 'security' && /secure|safety|protected/i.test(claim.claim)) {
    if (!hasEvidence) {
      return {
        verdict:    VERDICT.RED_SLAP,
        certainty:  'Unsupported',
        reason:     'Security claim made without any evidence — this is dangerous and potentially liable.',
        missing:    needed,
        correction: 'Replace "secure" with specific evidence: "auth enforced at /api route" or "HTTPS configured on port 443" — no hand-waving.',
      };
    }
  }

  // ── Production-ready without build evidence ──────────────────────────────
  if (claim.claimType === 'performance' && /production/i.test(claim.claim)) {
    if (!hasStrongEvidence) {
      return {
        verdict:    VERDICT.HARD_SLAP,
        certainty:  'Inferred',
        reason:     '"Production-ready" without build/test evidence — needs smoke test or deployment confirmation.',
        missing:    needed.length ? needed : ['passing build, smoke test output, or deployment confirmation'],
        correction: '"Production-ready" requires: (1) build passes, (2) smoke test passes, (3) deployment confirmed.',
      };
    }
  }

  // ── Default: evidence found ─────────────────────────────────────────────
  if (hasEvidence) {
    const certainty = hasStrongEvidence ? 'Proven' : 'Likely';
    return {
      verdict:    VERDICT.WET_NOD,
      certainty,
      reason:     `Evidence found (${evidenceCount} items) supports this ${claim.claimType} claim.`,
      missing:    [],
    };
  }

  // ── Default: no evidence ─────────────────────────────────────────────────
  return {
    verdict:    VERDICT.HARD_SLAP,
    certainty:  'Speculative',
    reason:     `No evidence found for this ${claim.claimType} claim.`,
    missing:    needed,
    correction: `Provide evidence for the claim: ${needed.join('; ') || 'appropriate supporting evidence'}`,
  };
}

// ── Certainty normalization ───────────────────────────────────────────────────

function normalizeCertainty(verdict) {
  switch (verdict) {
    case VERDICT.RELEASE:     return 'Proven';
    case VERDICT.WET_NOD:     return 'Proven';
    case VERDICT.GENTLE_SLAP: return 'Inferred';
    case VERDICT.HARD_SLAP:   return 'Speculative';
    case VERDICT.RED_SLAP:    return 'Unsupported';
    case VERDICT.TANK_MODE:   return 'Unsupported';
    default:                  return 'Speculative';
  }
}

// ── Overall verdict aggregation ─────────────────────────────────────────────

/**
 * Aggregate individual claim verdicts into an overall verdict.
 * Worst verdict wins (defensive — fish never upgrades).
 */
function aggregateVerdict(claims) {
  if (!claims || claims.length === 0) {
    return { overall: VERDICT.RELEASE, worst: null };
  }

  let worstIdx  = 0;
  let worstSev  = -1;

  for (let i = 0; i < claims.length; i++) {
    const sev = VERDICT_ORDER.indexOf(claims[i].verdict);
    if (sev > worstSev) {
      worstSev  = sev;
      worstIdx  = i;
    }
  }

  return {
    overall: claims[worstIdx].verdict,
    worst:   claims[worstIdx],
  };
}

// ── Main audit function ───────────────────────────────────────────────────────

/**
 * Main entry point. Run this on any agent output before it ships.
 *
 * @param {string} output       - the raw agent output text
 * @param {object} options
 * @param {string} [options.jobId]   - job ID for tracking
 * @param {string} [options.agentId] - agent name
 * @param {object} [options.evidence] - evidence context (files, logs, tests, etc.)
 * @param {object} [options.context] - additional context (task description, gate results, etc.)
 * @returns {FishAudit}
 */
function audit(output, options = {}) {
  const { jobId = 'unknown', agentId = 'unknown', evidence = {}, context = {} } = options;

  const startTime = Date.now();
  const claims    = extractClaims(output, options);
  const slapLog   = [];
  const corrections = [];

  // Evaluate each claim
  for (const claim of claims) {
    const evidenceResult = matchEvidence(claim, evidence);
    const verdictResult  = assignVerdict(claim, evidenceResult);

    Object.assign(claim, {
      verdict:      verdictResult.verdict,
      certainty:    verdictResult.certainty || normalizeCertainty(verdictResult.verdict),
      evidenceFound: evidenceResult.matched.map(e => e.id),
      missing:      verdictResult.missing  || [],
      correction:   verdictResult.correction || null,
      reason:      verdictResult.reason   || null,
    });

    if (verdictResult.verdict !== VERDICT.RELEASE && verdictResult.verdict !== VERDICT.WET_NOD) {
      slapLog.push({
        verdict:  verdictResult.verdict,
        claim:    claim.claim.slice(0, 120),
        reason:   verdictResult.reason,
        type:     claim.claimType,
      });
      corrections.push({
        claim:    claim.claim,
        correction: verdictResult.correction || verdictResult.reason,
        verdict:  verdictResult.verdict,
      });
    }
  }

  const { overall } = aggregateVerdict(claims);
  const releaseReady = overall === VERDICT.RELEASE || overall === VERDICT.WET_NOD;

  // ── Build fish summary ────────────────────────────────────────────────
  let fishSummary = '';
  if (slapLog.length === 0) {
    fishSummary = '🐟 WET NOD — All claims supported by evidence. Release approved.';
  } else {
    const slaps = slapLog.map(s => `  🐟 [${s.verdict}] (${s.type}) "${s.claim}..." — ${s.reason}`).join('\n');
    fishSummary = `🐟 FISH SUMMARY — ${slapLog.length} issue(s) found:\n${slaps}`;
  }

  return {
    jobId,
    agentId,
    output,
    claims,
    evidence: evidence._raw || [],
    overallVerdict:    overall,
    overallCertainty:  normalizeCertainty(overall),
    corrections,
    releaseReady,
    fishSummary,
    slapLog,
    timestamp:  Date.now(),
    durationMs: Date.now() - startTime,
  };
}

// ── Correction router ─────────────────────────────────────────────────────────

/**
 * Take a fish audit result and generate correction instructions
 * to send back to the agent for revision.
 */
function routeCorrection(audit) {
  if (audit.releaseReady) {
    return {
      action:    'RELEASE',
      message:   'Fish approves. All claims verified.',
      corrections: [],
    };
  }

  const byVerdict = {};
  for (const c of audit.corrections) {
    const v = c.verdict;
    if (!byVerdict[v]) byVerdict[v] = [];
    byVerdict[v].push(c);
  }

  const instructions = [];

  if (byVerdict[VERDICT.RED_SLAP]) {
    instructions.push({
      severity: 'CRITICAL',
      verdict:  VERDICT.RED_SLAP,
      message:  'RED SLAP — Dangerous overclaim detected. These must be corrected before any output ships.',
      items:    byVerdict[VERDICT.RED_SLAP].map(c => ({ claim: c.claim, fix: c.correction })),
    });
  }

  if (byVerdict[VERDICT.TANK_MODE]) {
    instructions.push({
      severity: 'HOLD',
      verdict:  VERDICT.TANK_MODE,
      message:  'TANK MODE — Output held. Attach required evidence before resubmitting.',
      items:    byVerdict[VERDICT.TANK_MODE].map(c => ({ claim: c.claim, fix: c.correction })),
    });
  }

  if (byVerdict[VERDICT.HARD_SLAP]) {
    instructions.push({
      severity: 'ERROR',
      verdict:  VERDICT.HARD_SLAP,
      message:  'HARD SLAP — Claims without evidence. Show your work or reword.',
      items:    byVerdict[VERDICT.HARD_SLAP].map(c => ({ claim: c.claim, fix: c.correction })),
    });
  }

  if (byVerdict[VERDICT.GENTLE_SLAP]) {
    instructions.push({
      severity: 'WARN',
      verdict:  VERDICT.GENTLE_SLAP,
      message:  'GENTLE SLAP — Word choice too strong. Tone it down.',
      items:    byVerdict[VERDICT.GENTLE_SLAP].map(c => ({ claim: c.claim, fix: c.correction })),
    });
  }

  return {
    action:      'CORRECT',
    message:     `Fish says: ${audit.slapLog.length} correction(s) needed before release.`,
    severity:    byVerdict[VERDICT.RED_SLAP] ? 'CRITICAL'
               : byVerdict[VERDICT.TANK_MODE] ? 'HOLD'
               : byVerdict[VERDICT.HARD_SLAP] ? 'ERROR'
               : 'WARN',
    instructions,
    auditRef:    audit.jobId,
    slapCount:   audit.slapLog.length,
  };
}

// ── Audit memory ──────────────────────────────────────────────────────────────

const AUDIT_LOG_FILE = (() => {
  try {
    const root = path.resolve(__dirname, '..');
    const dir  = path.join(root, 'agent_work', 'fish_audits');
    if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
    return path.join(dir, `audit_${new Date().toISOString().slice(0, 10)}.jsonl`);
  } catch {
    return null;
  }
})();

/**
 * Persist an audit to disk for the audit memory.
 */
function saveAudit(audit) {
  if (!AUDIT_LOG_FILE) return;
  try {
    const line = JSON.stringify({
      jobId:         audit.jobId,
      agentId:       audit.agentId,
      overallVerdict: audit.overallVerdict,
      claimCount:    audit.claims.length,
      slapCount:     audit.slapLog.length,
      releaseReady:  audit.releaseReady,
      timestamp:     audit.timestamp,
    });
    fs.appendFileSync(AUDIT_LOG_FILE, line + '\n');
  } catch { /* best-effort */ }
}

// ── Integration helper: wrap a harness job output ─────────────────────────────

/**
 * Fish-ready wrapper for harness output.
 * Call this at the end of any job step before returning to the user.
 *
 * @param {string} output       - job output text
 * @param {object} jobCtx      - { jobId, agentId, rootDir }
 * @param {object} evidenceCtx  - { files, logs, tests, commands }
 * @returns {FishAudit}        - fish audit result
 */
function auditJobOutput(output, jobCtx = {}, evidenceCtx = {}) {
  const audit = audit(output, {
    jobId:    jobCtx.jobId    || `job-${Date.now()}`,
    agentId:  jobCtx.agentId  || 'harness',
    evidence: evidenceCtx,
    context:  jobCtx,
  });

  saveAudit(audit);

  return audit;
}

// ── Gate integration helper ──────────────────────────────────────────────────

/**
 * Run fish as a verification gate alongside existing job-contract gates.
 * Returns a gate-compatible result object.
 *
 * @param {string} output       - agent output to audit
 * @param {object} evidence     - evidence context
 * @returns {{ ok: boolean, gate: string, output: string, fish: FishAudit }}
 */
function runFishGate(output, evidence = {}) {
  const audit = audit(output, {
    jobId:   `gate-${Date.now()}`,
    agentId: 'fish-gate',
    evidence,
  });

  return {
    ok:    audit.releaseReady,
    gate:  'fish-claim-integrity',
    output: audit.fishSummary,
    fish:  audit,
  };
}

// ── LLM prompt for self-audit (for HER / recursive loop) ────────────────────

/**
 * Returns a prompt fragment that can be injected into any LLM call
 * to make the model self-audit its own output before returning.
 */
const SELF_AUDIT_PROMPT = `
Before you return your final output, run an Accuracy Fish self-check:

1. List every factual claim in your output
2. For each claim, note what evidence you have for it
3. For any claim without direct evidence, reword to "inferred" or "expected"
4. For any security/performance/completeness claim, downgrade certainty
5. If you wrote "all", "every", "complete", "secure", "tested", or "immutable" —
   prove it in the output or remove the word

Fish verdict levels:
  WET_NOD     — evidence found, claim solid
  GENTLE_SLAP — close but overstated
  HARD_SLAP   — no evidence, prove it or reword
  RED_SLAP    — dangerous claim without proof
  TANK_MODE   — holding until evidence attached
  RELEASE     — all clear

The fish does not stop the cathedral. It checks the foundations.
`.trim();

// ── Expressive fish summary (for CLI / Mochi display) ─────────────────────────

function formatFishForDisplay(audit) {
  const lines = [];
  lines.push('');
  lines.push('🐟══════════════════════════════════════');
  lines.push('   PURPCLAW CLAIM INTEGRITY ENGINE');
  lines.push('═══════════════════════════════════════🐟');
  lines.push('');

  if (audit.claims.length === 0) {
    lines.push('  [no specific claims detected]');
  } else {
    lines.push(`  Claims audited: ${audit.claims.length}`);
    lines.push(`  Overall verdict: ${audit.overallVerdict}`);
    lines.push(`  Certainty level: ${audit.overallCertainty}`);
    lines.push('');
    for (const claim of audit.claims) {
      const icon = {
        [VERDICT.WET_NOD]:     '🐟✅',
        [VERDICT.GENTLE_SLAP]: '🐟⚠️',
        [VERDICT.HARD_SLAP]:   '🐟❌',
        [VERDICT.RED_SLAP]:    '🐟🚨',
        [VERDICT.TANK_MODE]:   '🐟🛑',
        [VERDICT.RELEASE]:     '🐟✅',
      }[claim.verdict] || '🐟?';

      lines.push(`  ${icon} [${claim.verdict}] ${claim.claim.slice(0, 80)}`);
      if (claim.reason) lines.push(`      → ${claim.reason}`);
      if (claim.correction) lines.push(`      → Fix: ${claim.correction}`);
    }
  }

  lines.push('');
  lines.push(`  Release ready: ${audit.releaseReady ? 'YES ✅' : 'NO ❌ — corrections needed'}`);
  lines.push(`  Fish duration: ${audit.durationMs}ms`);
  lines.push('═══════════════════════════════════════🐟');
  lines.push('');

  return lines.join('\n');
}

// ── Module exports ────────────────────────────────────────────────────────────

module.exports = {
  // Core
  audit,
  auditJobOutput,
  runFishGate,
  routeCorrection,

  // Claim inspection
  extractClaims,
  matchEvidence,
  assignVerdict,

  // Utilities
  normalizeCertainty,
  aggregateVerdict,
  formatFishForDisplay,

  // Constants
  VERDICT,
  EVIDENCE_TYPES,
  CLAIM_TYPES,
  SELF_AUDIT_PROMPT,
};
