// Extended views for Result Dashboard: Council debate + SAST battery

const { useState: Sv, useEffect: Ev } = React;

// Slice 6.4.6 Phase 4: Council Mode goes from "Q3 unlock" to live. The
// callout no longer advertises a Q3 release date — it tells the operator
// to switch the mode selector to Council. Slice 6.4.5.3 staged this
// component as a Q3-2026 callout because the backend wasn't exposed yet;
// Phase 2 of this slice landed the backend, so the framing flips.
// Q3Badge is defined in screens-b.jsx (loaded earlier).
function FullModeCallout({ t }) {
  return (
    <div style={{
      padding: 12,
      border: `1px solid ${t.accent.base}`,
      borderRadius: t.radius.md,
      background: `color-mix(in oklch, ${t.accent.base} 8%, ${t.bg1})`,
    }}>
      <div style={{
        display:'flex', alignItems:'center', gap: 8, marginBottom: 6,
      }}>
        <strong style={{
          fontFamily: t.font.ui, fontSize: 13, fontWeight: 700, color: t.fg0,
        }}>Council Mode</strong>
        <Q3Badge t={t} label="Available now — select Council mode"
          tooltip="Council Mode runs live today via the mode selector. Switch the mode toggle below to 'Council' before clicking Audit."/>
      </div>
      <div style={{
        fontFamily: t.font.mono, fontSize: 11, color: t.fg2,
        lineHeight: 1.55,
      }}>
        Council Mode runs 5 specialist agents through 4 rounds of debate,
        including devil's advocate cross-examination and sycophancy
        correction. Switch the mode selector to <strong>Council</strong>
        and run a new audit to see all five agents debate this finding.
      </div>
    </div>
  );
}

// ──────────────────────────────────────────────────────────────
// COUNCIL DEBATE — 5 agents, up to 4 rounds (real-data driven on live
// audits; window.DAEV_COUNCIL fallback only for static fixtures)
// ──────────────────────────────────────────────────────────────

// Slice 6.4.7 Phase 3 (F-212 / HC-54..57): canonical descriptions of
// the 5 specialists, used to enrich the dynamic findings_by_agent map
// returned by the backend. The backend identifies agents by class name
// (SecurityAgent / PerformanceAgent / ...) so we key off those.
const COUNCIL_AGENT_DESCRIPTIONS = {
  SecurityAgent: {
    id: 'sec',
    icon: 'shield',
    lens: 'T-cell · Semgrep + Bandit + gap-fill',
    stack: 'SAST + CWE matching',
    description: 'security · structural pattern match across causal chain',
  },
  ArchitectureAgent: {
    id: 'arch',
    icon: 'sitemap',
    lens: 'B-cell · Radon + pyan + MoE domain prompts',
    stack: 'coupling + cohesion + design',
    description: 'architecture · coupling and design-pattern adherence',
  },
  PerformanceAgent: {
    id: 'perf',
    icon: 'gauge',
    lens: 'Innate · Lizard + profiler heuristics',
    stack: 'complexity + latency budget',
    description: 'performance · cyclomatic complexity and hot-path latency',
  },
  CausalIntegrityAgent: {
    id: 'caus',
    icon: 'graph',
    lens: 'Regulatory T-cell · DoWhy-GCM + causal-learn PC',
    stack: 'full-DAG reasoning',
    description: 'causal-integrity · spurious-correlation filter',
  },
  PropagationAgent: {
    id: 'prop',
    icon: 'broadcast',
    lens: 'Dendritic · NetworkX + ConfidenceDecay=0.1/hop',
    stack: 'blast radius + decay',
    description: 'propagation · downstream blast radius',
  },
};

const COUNCIL_ROUND_DESCRIPTIONS = [
  { id: 0, name: 'Independent Analysis', desc: 'Each agent reasons in epistemic isolation. No cross-talk.' },
  { id: 1, name: 'Anonymized Cross-Review', desc: 'Peer votes exchanged with identities redacted.' },
  { id: 2, name: "Devil's Advocate", desc: 'A challenge prompt tests the strongest finding.' },
  { id: 3, name: 'Sycophancy Correction', desc: 'CONSENSAGENT protocol checks for deference, finalizes verdict.' },
];

function deriveAgentsFromBackend(fx) {
  // Slice 6.4.7 Phase 3 (HC-57): per-agent rows derived from real
  // findings_by_agent + agent_weights. Agents missing from the
  // backend dict (shouldn't happen with the Phase 2 fix, but defensive)
  // fall back to all 5 specialists in canonical order.
  const fba = (fx && fx.council_metadata && fx.council_metadata.findings_by_agent) || {};
  const weights = (fx && fx.agent_weights) || {};
  const names = Object.keys(fba).length > 0
    ? Object.keys(fba)
    : Object.keys(COUNCIL_AGENT_DESCRIPTIONS);
  return names.map(name => {
    const desc = COUNCIL_AGENT_DESCRIPTIONS[name] || {
      id: name.toLowerCase().slice(0, 4),
      icon: 'shield',
      lens: name,
      stack: 'specialist agent',
      description: name,
    };
    return {
      id: desc.id,
      name,
      icon: desc.icon,
      lens: desc.lens,
      stack: desc.stack,
      description: desc.description,
      findings: fba[name] || 0,
      weight: typeof weights[name] === 'number' ? weights[name] : null,
    };
  });
}

function deriveRoundsFromBackend(fx) {
  const total = (fx && fx.council_metadata && fx.council_metadata.rounds_completed) || 0;
  // The orchestrator populates rounds_completed in [0, 4]. Showing 0 rounds
  // would render an empty band; on the degraded path we still show round 0
  // (Independent Analysis) so the UI doesn't read as a dead panel.
  const clamped = Math.max(1, Math.min(4, total || 1));
  return COUNCIL_ROUND_DESCRIPTIONS.slice(0, clamped);
}

function CouncilPanel({ t, fx, mode, onSwitchToFull }) {
  // Slice 6.4.6 Phase 4 (MD-126): mode='council' is the new value the UI
  // sends to /switzerland/gate; mode='full' is the dormant 2-agent path.
  // The 5-agent debate panel renders for council mode only.
  const isCouncilMode = mode === 'council';

  // Slice 6.4.7 Phase 3 (HC-54): live-audit detection — when
  // apiAuditToFixtureShape hydrates fx.council_metadata (mode=council
  // backend response), we render real data; otherwise we fall back to
  // the static window.DAEV_COUNCIL fixture (used by non-council fixtures
  // and the cold-load before any audit fires).
  const isLiveAudit = isCouncilMode && !!(fx && fx.council_metadata);

  // Real-data sources (live audit) vs static fixture (fallback).
  const liveAgents = isLiveAudit ? deriveAgentsFromBackend(fx) : null;
  const liveRounds = isLiveAudit ? deriveRoundsFromBackend(fx) : null;
  const staticCouncil = window.DAEV_COUNCIL;

  const totalRounds = isLiveAudit && liveRounds ? liveRounds.length : 4;
  const [round, setRound] = Sv(isCouncilMode ? Math.max(0, totalRounds - 1) : 0);

  if (!isCouncilMode) {
    // Slice 6.4.5.3 Phase 3 (Item 4): /switzerland/gate runs ONLY SecurityAgent
    // in fast mode (no CausalIntegrityAgent debate, no ConsensusAgent vote).
    // The prior 3-agent condensed-council view fabricated two extra agents'
    // verdicts; honest framing here is "single SecurityAgent + a callout for
    // the other 4 agents that ship in Council Mode." Probe Pack C §A6.3
    // confirmed: fast mode is one agent, zero debate rounds.
    const securityAgent = {
      id: 'sec',
      name: 'SecurityAgent',
      verdict: fx.finding ? 'UNSAFE' : 'CLEAR',
      tone: fx.finding ? 'red' : 'green',
      line: fx.finding
        ? `${fx.finding.cwe || 'CWE-?'} · causal path confirmed`
        : 'no taint reaches a sink',
    };
    return (
      <div style={{ padding: 14 }}>
        <div style={{
          fontFamily: t.font.mono, fontSize: 10, color: t.fg2,
          letterSpacing: 1, textTransform:'uppercase', marginBottom: 10,
          display:'inline-flex', alignItems:'center', gap: 8,
        }}>
          <span style={{ color: t.accent.base }}>//</span> fast mode · single SecurityAgent
          <Q3Badge t={t} label="+ 4 agents in Council mode"
            tooltip="Council Mode adds CausalIntegrityAgent, PerformanceAgent, ArchitectureAgent, and PropagationAgent through 4 rounds of debate. Switch the mode selector to Council to invoke them."/>
        </div>
        <div style={{ display:'flex', flexDirection:'column', gap: 8, marginBottom: 16 }}>
          {(() => {
            const a = securityAgent;
            const c = a.tone === 'red' ? t.semantic.red : t.semantic.green;
            return (
              <div key={a.id} style={{
                padding: '10px 12px',
                background: t.bg1, border: `1px solid ${t.line}`, borderLeft: `3px solid ${c}`,
                borderRadius: t.radius.sm,
              }}>
                <div style={{ display:'flex', alignItems:'center', justifyContent:'space-between' }}>
                  <span style={{ fontFamily: t.font.ui, fontSize: 12.5, fontWeight: 600, color: t.fg0 }}>{a.name}</span>
                  <span style={{
                    fontFamily: t.font.mono, fontSize: 10, fontWeight: 700,
                    color: c, letterSpacing: 1, textTransform:'uppercase',
                  }}>{a.verdict}</span>
                </div>
                <div style={{ fontFamily: t.font.mono, fontSize: 11, color: t.fg2, marginTop: 4 }}>
                  {a.line}
                </div>
              </div>
            );
          })()}
        </div>
        <FullModeCallout t={t}/>
      </div>
    );
  }

  // Pick the rounds + agents source per HC-54: live data wins, fixture
  // falls back. Both shapes share { id, name, desc } for rounds and
  // { id, name, icon, lens, stack, description, findings } for agents.
  const rounds = isLiveAudit ? liveRounds : staticCouncil.rounds;
  const agents = isLiveAudit ? liveAgents : staticCouncil.agents;
  const verdictRoundIdx = rounds.length - 1;
  const isFinalRound = round === verdictRoundIdx;
  const cm = (fx && fx.council_metadata) || null;

  // Slice 6.4.7 Phase 3 (HC-57): per-agent finding count derived from
  // real backend grouping when available. Fallback fixture uses its
  // hardcoded counts.
  const liveAgreed = isLiveAudit
    ? agents.filter(a => a.findings > 0).length
    : null;

  // Final-verdict text — derives from real council_confidence on live
  // audits (HC-54), keeps the static "5/5 agents" copy for fixtures.
  const finalVerdictText = (() => {
    if (isLiveAudit && cm) {
      const conf = Math.max(0, Math.min(1, Number(cm.council_confidence || 0)));
      const verdict = fx.finding ? 'UNSAFE' : 'SAFE';
      const pct = Math.round(conf * 100);
      const total = agents.length;
      const agreed = liveAgreed != null ? liveAgreed : total;
      return `${verdict} · ${pct}% confidence (${agreed}/${total} agents)`;
    }
    return fx.finding
      ? `UNSAFE · ${Math.round((fx.finding && fx.finding.confidence || 0) * 100)}% confidence (5/5 agents)`
      : 'SAFE · 4/5 agents (1 abstain)';
  })();

  return (
    <div style={{ padding:'14px 14px 20px' }}>
      {/* Slice 6.4.7 Phase 3 (HC-54): live audit attribution banner.
          Surfaces audit_id + rounds_completed + disagreement flag so
          investors can see backend reality reflected in the UI. */}
      {isLiveAudit && cm && (
        <div style={{
          padding: '8px 10px', marginBottom: 10,
          background: t.bg1, border: `1px solid ${t.line2}`,
          borderRadius: t.radius.sm,
          fontFamily: t.font.mono, fontSize: 10, color: t.fg2,
          display:'flex', gap: 10, flexWrap:'wrap', alignItems:'center',
        }}>
          <span style={{ color: t.accent.base, fontWeight: 700, letterSpacing: 1 }}>// LIVE</span>
          <span><strong style={{ color: t.fg1 }}>{cm.rounds_completed}</strong>-round CONSENSAGENT debate</span>
          <span>·</span>
          <span>{agents.length} specialists</span>
          <span>·</span>
          <span>{cm.disagreement_flag ? 'disagreement detected' : 'consensus'}</span>
          {cm.round2_resolved && (<>
            <span>·</span>
            <span>devil's advocate resolved</span>
          </>)}
          {fx && fx.audit_id && (<>
            <span>·</span>
            <code style={{ fontSize: 10, color: t.fg3 }}>audit_id={String(fx.audit_id).slice(0, 8)}</code>
          </>)}
        </div>
      )}

      {/* Round selector */}
      <div style={{
        fontFamily: t.font.mono, fontSize: 10, color: t.fg2,
        letterSpacing: 1, textTransform:'uppercase', marginBottom: 8,
      }}>protocol · {rounds.length} round{rounds.length !== 1 ? 's' : ''}</div>
      <div style={{ display:'flex', gap: 4, marginBottom: 16 }}>
        {rounds.map((r,i) => {
          const active = round === i;
          return (
            <button key={r.id != null ? r.id : i} onClick={()=>setRound(i)} style={{
              flex:1, padding:'8px 6px',
              background: active ? t.bg2 : t.bg1,
              border:`1px solid ${active ? t.accent.base : t.line}`,
              borderRadius: t.radius.sm,
              cursor:'pointer', textAlign:'left',
            }}>
              <div style={{ fontFamily: t.font.mono, fontSize: 9, color: t.fg3, letterSpacing: 1 }}>R{i}</div>
              <div style={{ fontFamily: t.font.ui, fontSize: 11, color: active ? t.fg0 : t.fg1, fontWeight: active ? 600 : 500, marginTop: 2, lineHeight: 1.25 }}>
                {r.name}
              </div>
            </button>
          );
        })}
      </div>

      <div style={{
        padding: 10, background: t.bg1, border:`1px solid ${t.line}`,
        borderRadius: t.radius.sm, marginBottom: 16,
        fontFamily: t.font.mono, fontSize: 11.5, color: t.fg1, lineHeight: 1.55,
      }}>
        {(rounds[round] && rounds[round].desc) || ''}
      </div>

      {/* Agents column */}
      <div style={{
        fontFamily: t.font.mono, fontSize: 10, color: t.fg2,
        letterSpacing: 1, textTransform:'uppercase', marginBottom: 8,
      }}>agents · {agents.length}</div>
      <div style={{ display:'flex', flexDirection:'column', gap: 8 }}>
        {agents.map(ag => <AgentCard key={ag.id || ag.name} t={t} agent={ag} round={round} fx={fx}/>)}
      </div>

      {/* Verdict — only on the final round (round count is dynamic for
          live audits; fixed 4 for static fixtures). */}
      {isFinalRound && (
        <div style={{
          marginTop: 18, padding: 12,
          background: fx.finding ? t.semantic.redBg : t.semantic.greenBg,
          border: `1px solid ${fx.finding ? t.semantic.red : t.semantic.green}`,
          borderRadius: t.radius.sm,
        }}>
          <div style={{
            fontFamily: t.font.mono, fontSize: 10,
            color: fx.finding ? t.semantic.red : t.semantic.green,
            letterSpacing: 1, textTransform:'uppercase', fontWeight: 600,
          }}>CONSENSAGENT · final verdict</div>
          <div style={{
            fontFamily: t.font.ui, fontSize: 14, color: t.fg0,
            fontWeight: 600, marginTop: 4,
          }}>{finalVerdictText}</div>
          <div style={{
            fontFamily: t.font.mono, fontSize: 11, color: t.fg1,
            marginTop: 6, lineHeight: 1.55,
          }}>
            {fx.finding
              ? 'Sycophancy check passed — no agent deferred on disagreement. Devil\'s advocate challenge failed to break causal chain.'
              : 'Sanitizer dominates all flow paths in counterfactual. Minor deviation: PerformanceAgent flagged latency concern (non-blocking).'}
          </div>
          {/* Slice 6.4.7 Phase 3 (HC-56): live-audit transcript pointer.
              Visible only when council_metadata.debate_transcript_path
              came back from the backend. */}
          {isLiveAudit && cm && cm.debate_transcript_path && (
            <div style={{
              marginTop: 10, padding: '6px 8px',
              borderTop: `1px dashed ${t.line2}`,
              fontFamily: t.font.mono, fontSize: 10, color: t.fg2,
              wordBreak: 'break-all',
            }}>
              <span style={{ color: t.fg3 }}>debate transcript: </span>
              <code style={{ color: t.fg1 }}>{cm.debate_transcript_path}</code>
            </div>
          )}
        </div>
      )}

      {/* Council → Sentinel handoff — only when there's a finding to monitor */}
      {fx.finding && (() => {
        const fileMap = {
          sqli:      'payments/checkout.py',
          cmdi:      'infra/ssh_executor.py',
          pathtrav:  'storage/s3_proxy.py',
          hardcoded: 'config/secrets.py',
          'xss-js':  'frontend/Profile.tsx',
          'cmdi-js': 'ops/diag.js',
          racecond:  'billing/redeem.py',
        };
        const file = fileMap[fx.id] || (fx.id + '.py');
        return (
          <div>
            <div style={{ height: 1, background: t.line, margin: '14px 0' }}/>
            <div style={{
              fontFamily: t.font.mono, fontSize: 11, color: t.fg2,
              lineHeight: 1.75, padding: '0 2px',
            }}>
              <div>
                <span style={{ color: t.fg0, fontWeight: 700 }}>DAEV AUDIT COMPLETE</span>
                <span style={{ color: t.fg2 }}> · verdict </span>
                <span style={{ color: t.semantic.red, fontWeight: 700 }}>UNSAFE</span>
                <span style={{ color: t.fg2 }}> · evidence signed</span>
              </div>
              <div>
                <span style={{ color: t.semantic.green, fontWeight: 700 }}>↓ SENTINEL</span>
                <span style={{ color: t.fg2 }}>  Monitoring deployment of </span>
                <span style={{ color: t.accent.base }}>{file}</span>
                <span style={{ color: t.fg2 }}> for post-ship drift</span>
              </div>
              <div style={{ paddingLeft: 12, color: t.fg3, fontSize: 10 }}>
                Causal guard armed. Alert threshold: confidence_delta &gt; 0.08
              </div>
            </div>
          </div>
        );
      })()}
    </div>
  );
}

function AgentCard({ t, agent, round, fx }) {
  const hasFinding = agent.findings > 0 && fx.finding;
  // Slice 6.4.7 Phase 3 (HC-57): for live audits, derive vote from real
  // agent_votes attached to fx.finding (the primary finding). When that
  // agent voted on the finding (key present), we treat the vote as
  // UNSAFE; agents with no vote on this finding ABSTAIN. Static
  // fixtures retain the per-round simulated vote pattern.
  const isLiveAudit = !!(fx && fx.council_metadata);
  let vote;
  if (isLiveAudit) {
    if (!fx.finding) {
      vote = 'SAFE';
    } else {
      const votes = (fx.finding.agent_votes || {});
      const hasVote = Object.prototype.hasOwnProperty.call(votes, agent.name);
      // Backend agent_votes maps name -> confidence float. Treat
      // non-zero confidence as UNSAFE; missing or zero as ABSTAIN.
      const conf = hasVote ? Number(votes[agent.name] || 0) : 0;
      vote = conf > 0 ? 'UNSAFE' : (agent.findings > 0 ? 'UNSAFE' : 'ABSTAIN');
    }
  } else {
    vote = !fx.finding ? 'SAFE' :
                 agent.id === 'perf' && round < 3 ? 'ABSTAIN' :
                 round === 0 ? 'UNSAFE' :
                 round === 1 ? 'UNSAFE' :
                 round === 2 ? 'UNSAFE' : 'UNSAFE';
  }

  return (
    <div style={{
      padding: 10,
      background: t.bg1, border:`1px solid ${t.line}`,
      borderRadius: t.radius.sm,
    }}>
      <div style={{ display:'flex', alignItems:'center', gap: 10 }}>
        <div style={{
          width: 28, height: 28, borderRadius: t.radius.sm,
          background: t.bg2, border:`1px solid ${t.line2}`,
          display:'flex', alignItems:'center', justifyContent:'center',
          flexShrink: 0,
        }}>
          <Icon name={agent.icon} size={14} color={t.accent.base}/>
        </div>
        <div style={{ flex:1, minWidth:0 }}>
          <div style={{
            fontFamily: t.font.ui, fontSize: 12, fontWeight: 600, color: t.fg0,
          }}>{agent.name}</div>
          <div style={{
            fontFamily: t.font.mono, fontSize: 10, color: t.fg2, marginTop: 1,
          }}>{agent.lens} · {agent.stack}</div>
        </div>
        <Chip t={t} tone={
          vote === 'UNSAFE' ? 'red' :
          vote === 'SAFE' ? 'green' : 'amber'
        } size="sm">{vote}</Chip>
      </div>
      <div style={{
        marginTop: 8,
        fontFamily: t.font.mono, fontSize: 11, color: t.fg1, lineHeight: 1.55,
      }}>
        {agentReasoning(agent, round, fx)}
      </div>
      {agent.findings > 0 && fx.finding && (
        <div style={{
          marginTop: 8, display:'flex', gap: 6, alignItems:'center',
          fontFamily: t.font.mono, fontSize: 10, color: t.fg2, letterSpacing: 0.5,
        }}>
          <span style={{
            padding:'2px 6px', background: t.bg2, border:`1px solid ${t.line}`,
            borderRadius: 2, color: t.fg1,
          }}>{agent.findings} finding{agent.findings>1?'s':''}</span>
          <span style={{ color: t.fg3 }}>·</span>
          <span>{agent.description}</span>
        </div>
      )}
    </div>
  );
}

// Slice 6.4.5.2 Phase 10 (BUG-77): weave finding-specific data into the
// per-round per-agent prose so adjacent fixtures don't show identical
// Council narratives. The static skeleton is preserved for the
// safe/xss-js/cmdi-js/racecond fallback fixtures (where fx.finding is null
// or carries authored text — this branch only fires when finding exists).
function _archCouplingPhrase(finding) {
  const cwe = finding.cwe || '';
  if (cwe === 'CWE-89') return 'HTTP input → DB query without adapter layer';
  if (cwe === 'CWE-78') return 'CLI input → subprocess.call(shell=True) without escape layer';
  if (cwe === 'CWE-22') return 'user filename → filesystem path without canonicalization';
  if (cwe === 'CWE-79') return 'untrusted text → DOM render without escaping';
  if (cwe === 'CWE-798') return 'literal credentials embedded in source — no secret-store boundary';
  if (cwe === 'CWE-367') return 'check-then-use across non-atomic boundary';
  if (cwe === 'CWE-502') return 'untrusted bytes → deserializer without schema gate';
  return 'untrusted input reaches sensitive sink without a guard';
}
function _intervPhrase(finding) {
  const fix = (finding.fix && finding.fix.after) || finding.intervention_recommendation || '';
  if (!fix) return 'apply the recommended sanitizer';
  return fix.length > 60 ? fix.slice(0, 60) + '…' : fix;
}

function agentReasoning(agent, round, fx) {
  if (!fx.finding) {
    return 'No taint reached sink; sanitizer dominates. No issues from this lens.';
  }
  const f = fx.finding;
  const cwe = f.cwe || 'CWE-?';
  const hops = f.hops || (f.path ? f.path.length : 0);
  const decay = Math.pow(0.9, hops).toFixed(2);
  const arch_phrase = _archCouplingPhrase(f);
  const interv = _intervPhrase(f);

  const R0 = {
    sec:  `Pattern match: ${cwe} · ${f.title}. Direct flow from source to sink across ${hops} hops. ${Math.round((f.confidence || 0)*100)}% confidence.`,
    arch: `Handler exhibits coupling: ${arch_phrase}. Violates single-responsibility.`,
    perf: hops > 3
      ? `Path length ${hops} suggests measurable latency budget; abstaining from security verdict.`
      : 'Latency not materially affected. No allocation hotspot. Abstaining from security verdict.',
    caus: `Full DAG traversal: ${hops}-hop taint chain (${cwe}). No spurious correlations from other inputs.`,
    prop: `Blast radius: 1 route, 0 downstream services. Confidence decay 0.9^${hops} = ${decay}.`,
  };
  const R1 = {
    sec: 'Peer votes (anonymized): 3 UNSAFE, 1 abstain. Reviewing argument strengths against my prior.',
    arch: `Peer causal-agent traces same coupling (${arch_phrase}). Confidence reinforced.`,
    perf: 'Noted: my lens out of scope here. Remaining abstain.',
    caus: `Anonymized peers converge on same ${cwe} chain. PC-algorithm discovered no confounders.`,
    prop: `Cross-vote: all flow-aware agents confirm sink reach. Radius unchanged at decay=${decay}.`,
  };
  const R2 = {
    sec: `Devil's advocate prompt: "Could the input be pre-sanitized upstream?" Counterfactual rejects — no upstream guard in the traced DAG. Mitigation would be: ${interv}`,
    arch: `Challenge survived. Coupling persists even with a hypothetical validator. ${arch_phrase} pattern remains.`,
    perf: 'No shift.',
    caus: `Challenge: ran intervention applying "${interv}" — chain breaks at the sink. Original ${cwe} vulnerability confirmed.`,
    prop: `Challenge: assume 10x traffic — blast radius scales linearly, not exponentially. ${decay} → still high.`,
  };
  const R3 = {
    sec: `Sycophancy check: reaffirmed original UNSAFE on ${cwe}. No deference despite peer convergence — independent evidence holds.`,
    arch: 'Reaffirmed. No deference shift.',
    perf: 'Abstain sustained with reasoning intact.',
    caus: `CONSENSAGENT protocol: ${hops}-hop causal chain is the binding evidence. Verdict locked.`,
    prop: 'Ratified. Confidence decay per hop recorded in final report.',
  };
  return [R0, R1, R2, R3][round][agent.id];
}

// ──────────────────────────────────────────────────────────────
// SAST BATTERY
// ──────────────────────────────────────────────────────────────
// Slice 6.4.5.2 Phase 10 (BUG-78): SastPanel branches on whether the
// per-fixture sast_data hydrated from /audits/{id}/sast. Real per-tool
// data renders RealSastView; legacy/unwired fixtures keep the static
// 7-tool battery so safe/xss-js/cmdi-js/racecond don't regress.
function SastPanel({ t, fx, mode }) {
  const realSast = (window.DAEV_SAST_BY_FIXTURE || {})[fx && fx.id];
  if (realSast && realSast.available) {
    return <RealSastView t={t} fx={fx} sast={realSast}/>;
  }
  return <StaticSastView t={t} fx={fx}/>;
}

function RealSastView({ t, fx, sast }) {
  const tools = sast.tools || [];
  const totalFindings = sast.total_findings || 0;
  const rawFindings = sast.raw_findings || [];

  return (
    <div style={{ padding:'14px 14px 20px' }}>
      <div style={{
        padding: 10, background: t.bg1, border:`1px solid ${t.line}`,
        borderRadius: t.radius.sm, marginBottom: 14,
        display:'flex', gap: 18, alignItems:'center',
      }}>
        <div>
          <div style={{ fontFamily: t.font.mono, fontSize: 10, color: t.fg2, letterSpacing: 1, textTransform:'uppercase' }}>tools ran</div>
          <div style={{ fontFamily: t.font.ui, fontSize: 20, fontWeight: 600, color: t.fg0, marginTop: 2 }}>{tools.length}</div>
        </div>
        <div>
          <div style={{ fontFamily: t.font.mono, fontSize: 10, color: t.fg2, letterSpacing: 1, textTransform:'uppercase' }}>signals</div>
          <div style={{ fontFamily: t.font.ui, fontSize: 20, fontWeight: 600, color: t.fg0, marginTop: 2, fontVariantNumeric:'tabular-nums' }}>{totalFindings}</div>
        </div>
        <div>
          <div style={{ fontFamily: t.font.mono, fontSize: 10, color: t.fg2, letterSpacing: 1, textTransform:'uppercase' }}>scanned at</div>
          <div style={{ fontFamily: t.font.mono, fontSize: 11, color: t.fg1, marginTop: 4 }}>{sast.scanned_at ? new Date(sast.scanned_at).toLocaleTimeString() : '—'}</div>
        </div>
        <div style={{ flex:1 }}/>
        <Chip t={t} tone="green" size="sm">live · /audits/{fx && fx.id ? fx.id.slice(0, 4) : 'X'}/sast</Chip>
      </div>

      <div style={{ display:'flex', flexDirection:'column', gap: 8 }}>
        {tools.map((tool, i) => <RealToolRow key={i} t={t} tool={tool}/>)}
      </div>

      {rawFindings.length > 0 && (
        <details style={{ marginTop: 14 }}>
          <summary style={{
            cursor:'pointer', fontFamily: t.font.mono, fontSize: 11, color: t.fg2,
            padding:'6px 0', userSelect:'none',
          }}>per-rule details ({rawFindings.length})</summary>
          <div style={{
            marginTop: 8, padding: 10,
            background: t.bg1, border:`1px solid ${t.line}`, borderRadius: t.radius.sm,
            fontFamily: t.font.mono, fontSize: 11,
          }}>
            <table style={{ width:'100%', borderCollapse:'collapse' }}>
              <thead>
                <tr style={{ color: t.fg2, fontSize: 10, letterSpacing: 0.5, textTransform:'uppercase' }}>
                  <th style={{ textAlign:'left', padding:'4px 6px' }}>tool</th>
                  <th style={{ textAlign:'left', padding:'4px 6px' }}>rule</th>
                  <th style={{ textAlign:'left', padding:'4px 6px' }}>severity</th>
                  <th style={{ textAlign:'left', padding:'4px 6px' }}>line</th>
                  <th style={{ textAlign:'left', padding:'4px 6px' }}>cwe</th>
                </tr>
              </thead>
              <tbody>
                {rawFindings.map((rf, i) => (
                  <tr key={i} style={{ color: t.fg1, borderTop:`1px solid ${t.line}` }}>
                    <td style={{ padding:'4px 6px' }}>{rf.tool}</td>
                    <td style={{ padding:'4px 6px' }}>{rf.rule_id}</td>
                    <td style={{ padding:'4px 6px', color: rf.severity === 'high' ? t.semantic.red : t.fg1 }}>{rf.severity}</td>
                    <td style={{ padding:'4px 6px' }}>{rf.line}</td>
                    <td style={{ padding:'4px 6px' }}>{(rf.cwe_ids || []).join(', ') || '—'}</td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        </details>
      )}

      {/* Bridge to council */}
      <div style={{
        marginTop: 16, padding: 12,
        background: t.accent.ghost,
        border: `1px solid ${t.accent.dim}`,
        borderRadius: t.radius.sm,
      }}>
        <div style={{
          fontFamily: t.font.mono, fontSize: 10, color: t.accent.base,
          letterSpacing: 1, textTransform:'uppercase', fontWeight: 600,
        }}>→ feed into council</div>
        <div style={{
          fontFamily: t.font.mono, fontSize: 11.5, color: t.fg1,
          marginTop: 4, lineHeight: 1.6,
        }}>
          {totalFindings} raw signal{totalFindings === 1 ? '' : 's'} from {tools.length} tool{tools.length === 1 ? '' : 's'} reduced to {fx.finding ? '1 confirmed finding' : '0 confirmed findings'} after
          causal de-duplication and SecurityAgent causal-flow verification.
          Non-causal matches filtered as spurious correlation. <Q3Badge t={t}
            label="+ 4 agents in Council mode"
            tooltip="Council Mode adds 4 specialist agents through 4 rounds of debate. Switch the mode selector to Council to invoke them."/>
        </div>
      </div>
    </div>
  );
}

function RealToolRow({ t, tool }) {
  return (
    <div style={{
      padding: '10px 12px',
      background: t.bg1, border:`1px solid ${t.line}`,
      borderRadius: t.radius.sm,
      display:'flex', gap: 12, alignItems:'center',
    }}>
      <div style={{ flex: 1, minWidth: 0 }}>
        <div style={{ fontFamily: t.font.ui, fontSize: 13, fontWeight: 600, color: t.fg0 }}>{tool.name}</div>
        <div style={{
          fontFamily: t.font.mono, fontSize: 10.5, color: t.fg2, marginTop: 2,
          overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap',
        }}>
          {(tool.rules_triggered || []).slice(0, 3).join(', ') || 'no rule triggered'}
          {(tool.rules_triggered || []).length > 3 ? `, +${tool.rules_triggered.length - 3}` : ''}
        </div>
      </div>
      <div style={{ fontFamily: t.font.mono, fontSize: 11, color: t.fg1, fontWeight: 600 }}>
        {tool.findings_count || 0} hit{tool.findings_count === 1 ? '' : 's'}
      </div>
    </div>
  );
}

function StaticSastView({ t, fx }) {
  const tools = window.DAEV_SAST || [];
  const totalFindings = tools.reduce((s,x) => s + x.findings, 0);
  const totalRuntime  = tools.reduce((s,x) => s + parseFloat(x.runtime), 0).toFixed(1);

  return (
    <div style={{ padding:'14px 14px 20px' }}>
      <div style={{
        padding: 10, background: t.bg1, border:`1px solid ${t.line}`,
        borderRadius: t.radius.sm, marginBottom: 14,
        display:'flex', gap: 18, alignItems:'center',
      }}>
        <div>
          <div style={{ fontFamily: t.font.mono, fontSize: 10, color: t.fg2, letterSpacing: 1, textTransform:'uppercase' }}>tools ran</div>
          <div style={{ fontFamily: t.font.ui, fontSize: 20, fontWeight: 600, color: t.fg0, marginTop: 2 }}>{tools.length}</div>
        </div>
        <div>
          <div style={{ fontFamily: t.font.mono, fontSize: 10, color: t.fg2, letterSpacing: 1, textTransform:'uppercase' }}>signals</div>
          <div style={{ fontFamily: t.font.ui, fontSize: 20, fontWeight: 600, color: t.fg0, marginTop: 2, fontVariantNumeric:'tabular-nums' }}>{totalFindings}</div>
        </div>
        <div>
          <div style={{ fontFamily: t.font.mono, fontSize: 10, color: t.fg2, letterSpacing: 1, textTransform:'uppercase' }}>total runtime</div>
          <div style={{ fontFamily: t.font.ui, fontSize: 20, fontWeight: 600, color: t.fg0, marginTop: 2, fontVariantNumeric:'tabular-nums' }}>{totalRuntime}s</div>
        </div>
        <div style={{ flex:1 }}/>
        <Chip t={t} tone="ghost" size="sm">before council</Chip>
      </div>

      <div style={{ display:'flex', flexDirection:'column', gap: 8 }}>
        {tools.map(tool => <ToolCard key={tool.name} t={t} tool={tool} fx={fx}/>)}
      </div>

      {/* Bridge to council */}
      <div style={{
        marginTop: 16, padding: 12,
        background: t.accent.ghost,
        border: `1px solid ${t.accent.dim}`,
        borderRadius: t.radius.sm,
      }}>
        <div style={{
          fontFamily: t.font.mono, fontSize: 10, color: t.accent.base,
          letterSpacing: 1, textTransform:'uppercase', fontWeight: 600,
        }}>→ feed into council</div>
        <div style={{
          fontFamily: t.font.mono, fontSize: 11.5, color: t.fg1,
          marginTop: 4, lineHeight: 1.6,
        }}>
          {totalFindings} raw signals reduced to {fx.finding ? '1 confirmed finding' : '0 confirmed findings'} after
          causal de-duplication and SecurityAgent causal-flow verification.
          Non-causal matches filtered as spurious correlation. <Q3Badge t={t}
            label="+ 4 agents in Council mode"
            tooltip="Council Mode adds 4 specialist agents through 4 rounds of debate. Switch the mode selector to Council to invoke them."/>
        </div>
      </div>
    </div>
  );
}

function ToolCard({ t, tool, fx }) {
  const sev = tool.severity;
  const total = sev.critical + sev.high + sev.medium + sev.low + sev.info;
  const buckets = [
    { key:'critical', label:'C', color: t.semantic.red,    count: sev.critical },
    { key:'high',     label:'H', color: t.semantic.red,    count: sev.high },
    { key:'medium',   label:'M', color: t.semantic.amber,  count: sev.medium },
    { key:'low',      label:'L', color: t.fg2,             count: sev.low },
    { key:'info',     label:'I', color: t.accent.base,     count: sev.info },
  ];
  return (
    <div style={{
      padding: 10,
      background: t.bg1, border:`1px solid ${t.line}`,
      borderRadius: t.radius.sm,
      display:'flex', alignItems:'center', gap: 12,
    }}>
      <div style={{ flex:1, minWidth: 0 }}>
        <div style={{ display:'flex', alignItems:'baseline', gap: 8 }}>
          <div style={{ fontFamily: t.font.ui, fontSize: 13, fontWeight: 600, color: t.fg0 }}>{tool.name}</div>
          <div style={{ fontFamily: t.font.mono, fontSize: 10, color: t.fg3 }}>v{tool.version}</div>
        </div>
        <div style={{ fontFamily: t.font.mono, fontSize: 10.5, color: t.fg2, marginTop: 2 }}>{tool.desc}</div>
      </div>

      <div style={{ display:'flex', gap: 4 }}>
        {buckets.map(b => (
          <div key={b.key} title={`${b.key}: ${b.count}`} style={{
            width: 22, height: 22, borderRadius: 3,
            background: b.count > 0 ? b.color : t.bg2,
            border: `1px solid ${b.count > 0 ? b.color : t.line}`,
            display:'flex', alignItems:'center', justifyContent:'center',
            fontFamily: t.font.mono, fontSize: 10, fontWeight: 600,
            color: b.count > 0 ? (t.mode==='dark' ? 'oklch(0.14 0.01 240)' : 'white') : t.fg3,
            fontVariantNumeric:'tabular-nums',
          }}>{b.count || b.label}</div>
        ))}
      </div>
      <div style={{
        fontFamily: t.font.mono, fontSize: 11, color: t.fg1,
        fontVariantNumeric:'tabular-nums', minWidth: 42, textAlign:'right',
      }}>{tool.runtime}</div>
    </div>
  );
}

Object.assign(window, { CouncilPanel, SastPanel });
